普通视图

发现新文章,点击刷新页面。
昨天以前一个坏掉的番茄

聊聊 Rerank:从 BERT 到大模型的技术旅程

作者 SimonAKing
2024年12月9日 18:40

从搜索引擎到大语言模型,Rerank 技术一直在默默发挥着”最后一公里”的关键作用。

前言

在 NLP 场景中,Rerank 作为一个关键环节,承担着对多路召回、多数据来源、多模态、多结构等不同类型数据的归一化和精筛作用。它能有效地整合和优化各类召回结果,对提升检索系统的整体性能至关重要。

本文将介绍 rerank 相关的技术概念、业界进展,以及对业务 产生价值的可能性。

image
Rerank-从 BERT 到大模型的技术旅程/overall.png

正文

什么是 Rerank

Rerank 并不是新兴的技术,其发展历史可追溯到搜索引擎,其历程可浓缩为 3 个主要阶段:

image
Rerank-从 BERT 到大模型的技术旅程/timeline.png

一句话介绍:Rerank 是一种对初步检索结果进行重排序的优化技术,以提高问答结果的准确性。

在 RAG 应用中,如果没有 rerank 会给到模型 粗召后的文档,会出现文档与 query 深层语义相关性不足(仅表面相似)问题,导致生成质量下降。

一个 bad case:

image
Rerank-从 BERT 到大模型的技术旅程/badcase.png
image
Rerank-从 BERT 到大模型的技术旅程/Influence.png

不相关段落对于 RAG 效果的影响 https://arxiv.org/pdf/2410.05983

具体而言,rerank 的加持会带来:

  1. 排除无关信息:Rerank 会进一步深度理解语义,从向量的相似度、关键词的词频权重到理解复杂语义(如 cross-encoder 架构);

  2. 有更多排序可能性:能够增加多维度的特征进行加权计算(如 文档完整性、时效性、权威性..) ;

  3. 能基于文档上下文、query 意图排序:排序后的文档 不仅是语义关联,还能提升准确反映 query 真实意图的文档权重。

    举个例子:query 是某个 topic 相关的问答,如:苹果公司最新的产品发布会有哪些亮点?

再精排后,可以再进一步过滤 低相关的 documents 或者 基于模型窗口限制、注意力缺陷(如 lost in the middle),根据 相关分数 重排 documents ,能够有效提升 RAG 应用的问答效果。

业界实践

因为涉及到了精排,所以会产生额外的延迟;且 rerank 模型也需要一定的参数量,对于长尾场景可能也会有偏差。近年来的研究已经提出了多种创新方法来应对这些挑战。以下将介绍一些具有代表性的论文。

Passage Re-ranking with BERT

📄 Paper: https://arxiv.org/abs/1901.04085

该工作开创性地提出了一种基于 BERT 的 rerank 模型,证明了 预训练模型在 rerank 任务的效果,也影响了后续相关的研究工作。

image
Rerank-从 BERT 到大模型的技术旅程/p1.png

核心思路是将 rerank 视为 二分类任务,假设输入是 query-document pairs,会采取 point-wise 思路(二分类)将 query-document 拼接成单个序列:[CLS] query [SEP] document [SEP],[SEP] 分割后的 token 会增加 token_type_ids 作为 llm embedding 的一部分(辅助信息 用于区分 query 与 document)。

经过 多层 self-attention,[CLS] token 能表示 query 与 document 的匹配度,最后通过一个分类层预测 [CLS] token 表示的相关性得分。

训练时,相关的数据作为正样本,不相关的数据作为负样本,使用交叉熵损失进行优化。

关键代码介绍:

class SimpleBertReranker(nn.Module):
def __init__(self, bert_model_name='bert-base-uncased'):
super().__init__()
self.bert = BertModel.from_pretrained(bert_model_name)
self.classifier = nn.Linear(self.bert.config.hidden_size, 1) # 分类层

def forward(self, input_ids, attention_mask, token_type_ids):
# 获取 BERT 的输出
outputs = self.bert(input_ids=input_ids,
attention_mask=attention_mask,
token_type_ids=token_type_ids)
# 使用 [CLS] token 的表示
cls_output = outputs.pooler_output
# 通过分类层得到相关性得分
score = self.classifier(cls_output)
return score

tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
query = "What is the capital of France?"
document = "Paris is the capital of France."

# 编码输入
inputs = tokenizer(query, document, return_tensors='pt', padding=True, truncation=True)

# 初始化模型
model = SimpleBertReranker()

# 计算相关性得分
with torch.no_grad():
score = model(**inputs)
print("Relevance score:", score.item())

Document Ranking with a Pretrained Sequence-to-Sequence Model

📄 Paper: https://arxiv.org/abs/2003.06713

该工作提出了一种不同于 BERT 模型 分类任务的方式,基于 T5 模型 将 rerank 视为生成任务。

image
Rerank-从 BERT 到大模型的技术旅程/p2.png
image
Rerank-从 BERT 到大模型的技术旅程/p3.png

输入序列的格式是 Query: q Document: d Relevant: 格式,输出是 true、false 这两种 token。

在训练时,模型被微调为根据正负样本输出 “true” 或 “false” 作为目标 token;在推理阶段 通过对目标 token 进行 softmax 计算概率来进行重排。

关键代码介绍:

class T5Reranker:
def __init__(self):
# 加载预训练的 T5 模型
self.model = load_pretrained_t5()

def prepare_input(self, query, document):
# 构造输入序列格式
return f"Query: {query} Document: {document} Relevant:"

def fine_tune(self, train_data):
"""训练过程
train_data: [(query, document, is_relevant), ...]
"""
for query, document, is_relevant in train_data:
input_text = self.prepare_input(query, document)
target = "true" if is_relevant else "false"
self.model.train(input_text, target)

def rerank(self, query, candidate_documents):
"""重排序过程"""
results = []

for doc in candidate_documents:
# 构造输入序列
input_text = self.prepare_input(query, doc)

# 获取模型输出的 logits
logits = self.model.get_logits(input_text)

# 只对"true"和"false" token 计算 softmax
true_false_logits = logits[["true_token_id", "false_token_id"]]
probs = softmax(true_false_logits)

# 使用"true" token 的概率作为相关性分数
relevance_score = probs["true_token_id"]
results.append((doc, relevance_score))

# 根据相关性分数排序
ranked_docs = sorted(results, key=lambda x: x[1], reverse=True)
return ranked_docs

Is ChatGPT Good at Search? Investigating Large Language Models as Re-Ranking Agents

📄 Paper: https://arxiv.org/abs/2304.09542

该工作调研了 GPT 模型(GPT-4) 在 rerank 任务上的表现,基于当时的 sota 模型(GPT4)再结合合适的方法(滑动窗口+ listwise),可使得 GPT 模型达到 SOTA 结果。

image
Rerank-从 BERT 到大模型的技术旅程/p4.png
image
Rerank-从 BERT 到大模型的技术旅程/p5.png

通过图 c 的 prompt 要求 LLMs 生成相关性的段落 ID,如果超出模型窗口时 会使用 滑动窗口策略:

首先对(M-w)到 M 的段落进行排名,然后滑动窗口,对(M-w-s)到(M-s)的段落重新排名,重复此过程直到所有段落都被重新排名。

image
Rerank-从 BERT 到大模型的技术旅程/p6.png

Large Language Models are Effective Text Rankers

📄 Paper: https://arxiv.org/abs/2306.17563

上篇工作证明了使用 LLMs 在 rerank 场景的效果,不过由于其黑盒,并且对初始段落顺序的敏感(使用 BM25 召回的顺序会优于随机顺序),仅能依靠商业大模型(开源模型失败率高),研究价值不高。

不同于基于 sota 模型做 listwise,该工作提出了 PRP(Pairwise Ranking Prompting) 一种新策略(下图 1),核心思路是 拆解问题,每个任务的难度降低,对 LLMs 的能力要求也会降低。

通过合适的评分机制 再结合高效的排序方式,能做到在 小模型上达到不错效果。

image
Rerank-从 BERT 到大模型的技术旅程/p7.png
image
Rerank-从 BERT 到大模型的技术旅程/p8.png

Leveraging Passage Embeddings for Efficient Listwise Reranking with Large Language Models

📄 Paper: https://arxiv.org/abs/2406.14848

上面两篇工作都是基于 LLMs 进行 rerank 任务,但 LLMs 会有幻觉以及 latency 居高不下的问题,本篇工作提出了一种 PE Rank 的技术。

仍然采用 listwise 的方式,有两种 document 的处理方式,其中一种是不会将召回的 document 给到 LLMs,而是会将 document 替换成 特殊标记,类似于 soft prompt,prompt 如下:

I will provide you with {{n}} passages, each with a special token representing the passage
enclosed in [].
Rank the passages based on their relevance to the search query: {{query}}.
Passage 1: [{{embedding}}]
...
Passage {{n}}: [{{embedding}}]
Search Query: {{query}}
Rank the {{n}} passages above based on their relevance to the search query in descending order.
Only output the {{n}} unique special token in the ranking.

image
Rerank-从 BERT 到大模型的技术旅程/p9.png

核心有两点:

  1. 在输入时通过一个训练好的 mlp 层将 token embedding 映射成 llm embedding,能够压缩文档上下文(类似于 xRag),给到模型的输入见图 3。
  2. 输出时约束了 decode ,解码空间 仅能包含文档 ID,能根治 LLMs 的幻觉问题,因为 词表空间变小了,所以也加快了生成速度。

相应的在训练时,分两阶段 输入的映射层训练,以及 decoding 时的排序学习训练。

业界进展

近一年,多家组织/公司 推出了各自的 rerank 模型,一些亮点:

模型发布时间亮点
Cohere Rerank-3.52024/12/3- 相比前代模型提升了 10% 的准确率
Jina Reranker v22024/06/25- 支持跨语言重排(cross-lingual reranking)
BGE Re-Ranker v2.02024/03/18- 在 MTEB 重排任务上达到 SOTA 水平

这些进展表明,rerank 未来会向着更高的准确率和更低的 latency 发展,使用场景也不会局限于 文档排序。


结束语

通过对 rerank 技术的深入探讨,我们可以看到:

  1. 技术演进趋势

    • 从简单的 BERT 二分类,到 T5 生成式方法,再到利用大模型能力,rerank 技术在不断创新
    • 业界正在探索如何平衡效果与效率,比如通过 embedding 压缩、约束解码等方式优化性能
    • 开源社区和商业产品都在积极推进,为不同场景提供了丰富的选择
  2. 对业务的价值

    • 提升搜索质量:通过深度语义理解,能更准确地识别用户意图,减少无关结果
    • 优化 RAG 应用:通过精准的文档筛选和重排,显著提升大模型问答的准确性
    • 支持多样化场景:
      • 产品分析:更准确地发现用户反馈,包括应用商店评论、社交媒体讨论等
      • 信息转化:提高相关信息的发现效率,降低噪音
      • 对话应用:提供更精准的知识库匹配

回顾 rerank 的发展历程,从 BERT 二分类到基于 LLM 策略的工作,技术在不断突破;从单一的搜索场景到如今的 RAG 应用,价值在持续提升。随着 AI 应用的普及,rerank 作为连接召回和生成的关键环节,在提升问答质量方面发挥着重要的作用。

转载本站文章请注明作者和出处 SimonAKing,请勿用于任何商业用途。

前端架构发展史

作者 SimonAKing
2021年1月22日 22:21

前端改革 30 年,从一片荒芜到微前端兴起。

前言

本文主要简述前端系统级架构的演讲历史,不涉及应用级架构: MVC,MVMM…

正文

前置术语

  • 应用:指的是一个整体的应用,可由多个模块组成。
  • 模块:指的是整体应用下 被划分的子应用。

无架构

在 1990 年,Tim Berners-Lee 在他的 NeXT 电脑上部署了第一套“主机-网站-浏览器”构成的 Web 系统,这标志着前端的开始。

在最初,前端是一片荒芜的,经过浏览器大战,W3C 标准化的过程 前端才慢慢成长了起来。

在此时期,诞生了 CSS、JavaScript、DHTML 等重要技术。前端也从最开始的纯静态页面,逐步存在于 PHP、ASP、JSP 这些模板技术中。

由于前端涉及到的功能简单,只需操作 DOM,并不需要代码管理与模块支持。

所以 最初的前端只是 MVC 架构中的 View,如下面的模板,没有架构可言。

<html>
<head><title>Car {{ $car->id }}</title></head>
<body>
<h1>Car {{ $car->id }}</h1>
<ul>
<li>Make: {{ $car->make }}</li>
<li>Model: {{ $car->model }}</li>
<li>Produced on: {{ $car->produced_on }}</li>
</ul>
</body>
</html>

前后端分离架构

在 1999 年,IE 浏览器支持了 XMLHttpRequest 接口,允许 JavaScript 异步发出 HTTP 请求,这为以后的前端发展 埋下了重要伏笔。

2005 年,在 Google 广泛应用 Ajax 通信 获得一系列产品成功后,这种不需要刷新页面就可以与服务器通信的技术 开始被开发者所重视。

但 Ajax 带来的不仅是一种可异步通信的技术,而是开发模式上的解耦。

前端不再是后端的模板,前端也可以实时地获取数据,动态地渲染内容。

在此时期,前端逐步从纯内容展示的静态网页 向 具备数据交互的动态网页转变,前端开发变得复杂起来。

与此同时,互联网带来的商业价值 推动了其他技术的产生,这其中就包括了 大名鼎鼎的 V8 。

在 V8 开源的第二年,Nodejs 出现了,让 JS 可以 run everywhere。

也正是因为它把前端快速带进了刀耕火种时代,其中最受益的就是工程化的兴起。

  • 构建工具:gulp webpack
  • 包管理:npm yarn
  • 排名第一的包生态

除此之外,标准化组织 W3C 也在 08 年发布 HTML5 正式草案。

随着资本的重视,工程化的兴起,开源社区的努力,开发模式的创新,整个前端开发 开始呈现野蛮增长现象。

从 2010 年 10 月出现的 Backbone 开始,Knockout、Anjular、Ember、Meteor、Vue 相继出现,前端开发颇有一副 不管 MVC 、MVVM 还是 全栈开发,我全都要的气势。

在此期间,有一股潮流慢慢被大家所认可,就是单页面应用。

但单页面应用代表的不只是工程技术化的产物,而重要的是一种网页 应用化的思想。

而随着单页面应用的流行,移动端的兴起,前后端分离的架构也成为了行业的标准实践。

由此,前端不再是后端 MVC 中的 V,而是单独的一层。

前端开始进入前后端分离时代。

微前端架构

微前端的概念由 ThoughtWorks 于 2016 年的一期技术雷达提出,

摘自技术雷达:

我们已经从引入微服务架构中获得了明显的好处,微服务架构可以让团队裁剪出独立部署的交付物以及可维护的服务。不幸的是,我们还看到许多团队在后端服务之上创建了前端单体——一个单一,庞大和杂乱无绪的浏览器应用。我们首选的(经过验证的)方法是将基于浏览器的代码拆分成微前端。在这种方法中,Web 应用程序被分解为多个特性,每个特性都由不同的前后端团队拥有。这确保每个特性都独立于其他特性开发,测试和部署。这样可以使用多种技术来重新组合特性——有时候是页面,有时候是组件——最终整合成一个内聚的用户体验。

“微”是一个很神奇的概念,底层的微内核设计,大行其道的微服务概念,以及本章的微前端主题。

微 实质上代表的概念是模块化,而模块化具有的思想是: 单一职责、关注分离,分而治之,这些思想 在软件工程中体可谓是金玉良言。

在我看来 微前端的出现 是一种趋势,是 WEB2.0 时代应用复杂性的一种体现,也标志着前端从刀耕火种的时代跨入了工业化时代。

在一些小型应用中,往往几个 单页面就足以支撑起整个应用。

而在一些中大型的应用中,业务的复杂性、工程的庞大性 都难以控制,如果继续使用传统的开发模式,不仅应用的状态难以管理,打包出来的应用难以优化,就连协作开发都存在很大的耦合性。

微前端 使用去中心化的思想可以有效分解大型应用的复杂度,将大型应用划分成一个个独立的模块,根据功能、业务场景来进行自由编排,这些模块通过统一的入口注册组合,通过约定的方式来进行通信,各个模块可以做到并行开发,独立部署,而且技术栈也随之解耦。

Application
Application

但是微前端并不是提升研发效率的银弹,需要根据具体场景来衡量是否使用,而架构就是权衡的艺术,微前端为我们提供了一种系统级的维度。

使用场景

适合场景

  1. 聚合型的应用

    这种应用主要存在 业务场景多样且业务之间依赖性很低的场景。

    就比如 Top 运营系统,每一个模块都可以单独抽成一个应用进行开发 且 满足单一职责原则,替换掉某一模块时也不会影响全局应用。

  2. 逻辑重复型的应用

    这种应用的特征是,子模块之间存在很多共同的业务逻辑,重复建设严重。

    就比如 电商系统,在 PC 端,H5 终端 都会有商品推荐,购买商品这些基本的功能,属于业务逻辑基本相同,UI 相差较大。

    微前端可以将子模块之间相同的业务逻辑 提取到主应用,然后各个子模块通过约定的方式进行通信,这样子模块的维护性会得到提升。

  3. 需保留老模块的应用

    在现代的前端开发时,由于技术栈日新月异,如何处理遗留模块 是一个无论绕过的问题。

    如果旧模块可以继续使用,并且重写需要花很多时间力气的话,那么使用微前端 整合进现有应用是一个很好的方案。

  4. 需数据隔离的应用

    数据隔离指的是 状态隔离,样式隔离。

    如果一个应用的单个页面存在多种模块时,数据冲突 不能忽视,比如模块中的全局变量,事件,通用样式 很可能造成问题。

    这种问题通常需要”沙箱化”来解决,而微前端提供了很好的实践。

    “沙箱化”还能提供应用的容错性,当一个子模块出现错误时,不会影响到全局应用,真正做到弹性应用。

  5. 技术栈灵活的应用

    使用微前端在系统级层面进行模块地抽象组合,磨平了不同技术栈带来的差异,做到模块之间技术栈的隔离。

    在同一应用下 每个团队可以根据自身需求,选择最合适自己的技术栈。

  6. 需要敏捷化开发的应用

    因为微服务提供了 模块级的组件化架构,完全可以做到多个模块之间并行开发,独立部署,无论模块的需求如何增长,都不会影响到全局应用。

    并且天然的增量构建,有利于持续部署。在团队协作开发层面也是一种很好的实践。

  7. 无法可持续迭代的应用

    这种应用一般都是巨石型应用,存在的问题很明显: 随着业务需求的迭代,代码量级越来越大,开发效率越来越低,新业务的接入成本随之变高。

    这种场景只能从技术维度来进行切入,需将巨石应用划分模块来处理臃肿。

不适合场景

  1. 应用之间存在很强的业务依赖 或者 拆分的粒度很难掌握,如果强行使用微前端规范,很可能起到相反的作用。
  2. 小型应用,没必要过度设计。
  3. 对性能有要求的应用,微前端因为多加了一层抽象,如果不是从工程化进行模块组合的话,极有可能为页面增加很多无意义的网络请求,内存消耗,影响最终的用户体验。
  4. 基础设施较弱的应用。

结束语

下篇文章将讲述微前端的实践原理,敬请关注。

转载本站文章请注明作者和出处 SimonAKing,请勿用于任何商业用途。

手撸一个静态文档生成器[译]

作者 SimonAKing
2020年2月29日 12:05

目前有很多优秀的静态文档生成器,它们的工作原理比你想象的要简单得多。

前言

原文: Build a static site generator in 40 lines with Node.js

作者: Douglas Matoso

翻译许可:

image
image
img
img

为什么要造这个轮子

当我计划建立个人网站时,我的需求很简单,做一个只有几个页面的网站,放置一些关于自己的信息,我的技能和项目就够了。

毫无疑问,它应该是纯静态的(不需要后端服务,可托管在任何地方)。

我曾经使用过Jekyll, HugoHexo这些知名的静态文档生成器,但我认为它们有太多的功能,我不想为我的网站增加这么多的复杂性。

所以我觉得,针对我的需求,一个简单的静态文档生成器就可以满足。

嗯,手动构建一个简单的生成器,应该不会那么难。

正文

需求分析

这个生成器必须满足以下条件:

  • EJS模板生成HTML文件。

  • 具有布局文件,所有页面都应该具有相同的页眉,页脚,导航等。

  • 允许可重用布局组件。

  • 站点的大致信息封装到一个配置文件中。

  • 从 JSON 文件中读取数据。

    例如:项目列表,这样我可以轻松地迭代和构建项目页面。

为什么使用 EJS 模板?

因为 EJS 很简单,它只是嵌入在 HTML 中的 JavaScript 而已。

项目结构

public/
src/
assets/
data/
pages/
partials/
layout.ejs
site.config.js
  • public: 生成站点的位置。
  • src: 源文件。
  • src/assets: 包含 CSS, JS, 图片 等
  • src/data: 包含 JSON 数据。
  • src/pages: 根据其中的 EJS 生成 HTML 页面的模板文件夹。
  • src/layout.ejs: 主要的原页面模板,包含特殊<%-body%>占位符,将插入具体的页面内容。
  • site.config.js: 模板中全局配置文件。

生成器

生成器代码位于scripts/build.js文件中,每次想重建站点时,执行npm run build命令即可。

实现方法是将以下脚本添加到package.jsonscripts块中:

"build": "node ./scripts/build"

下面是完整的生成器代码:

const fse = require('fs-extra')
const path = require('path')
const { promisify } = require('util')
const ejsRenderFile = promisify(require('ejs').renderFile)
const globP = promisify(require('glob'))
const config = require('../site.config')

const srcPath = './src'
const distPath = './public'

// clear destination folder
fse.emptyDirSync(distPath)

// copy assets folder
fse.copy(`${srcPath}/assets`, `${distPath}/assets`)

// read page templates
globP('**/*.ejs', { cwd: `${srcPath}/pages` })
.then((files) => {
files.forEach((file) => {
const fileData = path.parse(file)
const destPath = path.join(distPath, fileData.dir)

// create destination directory
fse.mkdirs(destPath)
.then(() => {
// render page
return ejsRenderFile(`${srcPath}/pages/${file}`, Object.assign({}, config))
})
.then((pageContents) => {
// render layout with page contents
return ejsRenderFile(`${srcPath}/layout.ejs`, Object.assign({}, config, { body: pageContents }))
})
.then((layoutContent) => {
// save the html file
fse.writeFile(`${destPath}/${fileData.name}.html`, layoutContent)
})
.catch((err) => { console.error(err) })
})
})
.catch((err) => { console.error(err) })

接下来,我将解释代码中的具体组成部分。

依赖

我们只需要三个依赖项:

  • ejs

    把我们的模板编译成HTML

  • fs-extra

    Node 文件模块的衍生版,具有更多的功能,并增加了Promise的支持。

  • glob

    递归读取目录,返回包含与指定模式匹配的所有文件,类型是数组。

Promisify

我们使用Node提供的util.promisify将所有回调函数转换为基于Promise的函数。

它使我们的代码更短,更清晰,更易于阅读。

const { promisify } = require('util')
const ejsRenderFile = promisify(require('ejs').renderFile)
const globP = promisify(require('glob'))

加载配置

在顶部,我们加载站点配置文件,以稍后将其注入模板渲染中。

const config = require('../site.config')

站点配置文件本身会加载其他JSON数据,例如:

const projects = require('./src/data/projects')

module.exports = {
site: {
title: 'NanoGen',
description: 'Micro Static Site Generator in Node.js',
projects
}
}

清空站点文件夹

我们使用fs-extra提供的emptyDirSync函数清空 生成后的站点文件夹。

fse.emptyDirSync(distPath)

拷贝静态资源

我们使用fs-extra提供的copy函数,该函数以递归方式复制静态资源 到站点文件夹。

fse.copy(`${srcPath}/assets`, `${distPath}/assets`)

编译页面模板

首先我们使用glob(已被 promisify)递归读取src/pages文件夹以查找.ejs文件。

它将返回一个匹配给定模式的所有文件数组。

globP('**/*.ejs', { cwd: `${srcPath}/pages` })
.then((files) => {

对于找到的每个模板文件,我们使用Nodepath.parse函数来分隔文件路径的各个组成部分(例如目录,名称和扩展名)。

然后,我们在站点目录中使用fs-extra提供的mkdirs函数创建与之对应的文件夹。

files.forEach((file) => {
const fileData = path.parse(file)
const destPath = path.join(distPath, fileData.dir)

// create destination directory
fse.mkdirs(destPath)

然后,我们使用EJS编译文件,并将配置数据作为数据参数。

由于我们使用的是已 promisify 的ejs.renderFile函数,因此我们可以返回调用结果,并在下一个promise链中处理结果。

.then(() => {
// render page
return ejsRenderFile(`${srcPath}/pages/${file}`, Object.assign({}, config))
})

在下一个then块中,我们得到了已编译好的页面内容。

现在,我们编译布局文件,将页面内容作为body属性传递进去。

.then((pageContents) => {
// render layout with page contents
return ejsRenderFile(`${srcPath}/layout.ejs`, Object.assign({}, config, { body: pageContents }))
})

最后,我们得到了生成好的编译结果(布局+页面内容的 HTML),然后将其保存到对应的HTML文件中。

.then((layoutContent) => {
// save the html file
fse.writeFile(`${destPath}/${fileData.name}.html`, layoutContent)
})

调试服务器

为了使查看结果更容易,我们在package.jsonscripts中添加一个简单的静态服务器。

"serve": "serve ./public"

运行 npm run serve 命令,打开http://localhost:5000就看到结果了。

进一步探索

Markdown

大多数静态文档生成器都支持以Markdown格式编写内容。

并且,它们还支持以YAML格式在顶部添加一些元数据,如下所示:

---
title: Hello World
date: 2013/7/13 20:46:25
---

只需要一些修改,我们就可以支持相同的功能了。

首先,我们必须增加两个依赖:

然后,我们将glob的匹配模式更新为包括.md文件,并保留.ejs,以支持渲染复杂页面。

如果想要部署一些纯 HTML 页面,还需包含.html

globP('**/*.@(md|ejs|html)', { cwd: `${srcPath}/pages` })

对于每个文件,我们都必须加载文件内容,以便可以在顶部提取到元数据。

.then(() => {
// read page file
return fse.readFile(`${srcPath}/pages/${file}`, 'utf-8')
})

我们将加载后的内容传递给front-matter

它将返回一个对象,其中attribute属性便是提取后的元数据。

然后,我们使用此数据扩充站点配置。

.then((data) => {
// extract front matter
const pageData = frontMatter(data)
const templateConfig = Object.assign({}, config, { page: pageData.attributes })

现在,我们根据文件扩展名将页面内容编译为 HTML。

如果是.md,则利用marked函数编译;

如果是.ejs,我们继续使用EJS编译;

如果是.html,便无需编译。

let pageContent

switch (fileData.ext) {
case '.md':
pageContent = marked(pageData.body)
break
case '.ejs':
pageContent = ejs.render(pageData.body, templateConfig)
break
default:
pageContent = pageData.body
}

最后,我们像以前一样渲染布局。

增加元数据,最明显的一个意义是,我们可以为每个页面设置单独的标题,如下所示:

---
title: Another Page
---

并让布局动态地渲染这些数据:

<title><%= page.title ? `${page.title} | ` : '' %><%= site.title %></title>

如此一来,每个页面将具有唯一的<title>标签。

多种布局的支持

另一个有趣的探索是,在特定的页面中使用不同的布局。

比如专门为站点首页设置一个独一无二的布局:

---
layout: minimal
---

我们需要有单独的布局文件,我将它们放在src/layouts文件夹中:

src/layouts/
default.ejs
mininal.ejs

如果front matter出现了布局属性,我们将利用layouts文件夹中同名模板文件进行渲染; 如果未设置,则利用默认模板渲染。

const layout = pageData.attributes.layout || 'default'

return ejsRenderFile(`${srcPath}/layouts/${layout}.ejs`,
Object.assign({}, templateConfig, { body: pageContent })
)

即使添加了这些新特性,构建脚本也才只有60行。

下一步

如果你想更进一步,可以添加一些不难的附加功能:

  • 可热重载的调试服务器

    你可以使用像live-server (内置自动重新加载) 或 chokidar (观察文件修改以自动触发构建脚本)这样的模块去完成。

  • 自动部署

    添加脚本以将站点部署到GitHub Pages等常见的托管服务,或仅通过SSH(使用scprsync等命令)将文件上传到你自己的服务器上。

  • 支持 CSS/JS 预处理器

    在静态文件被复制到站点文件前,增加一些预处理器(SASS 编译为 CSS,ES6 编译为 ES5 等)。

  • 更好的日志打印

    添加一些 console.log 日志输出 来更好地分析发生了什么。

    你可以使用chalk包来完善这件事。

反馈? 有什么建议吗? 请随时发表评论或与我联系!


结束语

这个文章的完整示例可以在这里找到:https://github.com/doug2k1/nanogen/tree/legacy。

一段时间后,我决定将项目转换为CLI模块,以使其更易于使用,它位于上面链接的master分支中。

译者:

今日本想写一篇ants(一个高性能的goroutine池)源码解析,奈何环境太吵,静不下心,遂罢。

这是一篇我前些日子无意间看到的文章,虽然是17年的文章,在读完之后仍对我产生了一些思考。

希望这篇文章对你有所帮助。

转载本站文章请注明作者和出处 SimonAKing,请勿用于任何商业用途。

昨日青空

作者 SimonAKing
2019年12月19日 15:32

起风了。

起风了

忽然 一瞬间长大

就像 被时间的手 擦模糊的画

我们啊 各自要去哪

问题好傻 谁又能 回答


想念从 不说话

来不及的再见 多喧哗

陪我看大雨落下

潮湿的心 滴滴答答 带着温柔 又想起你啊


我好想你 在起风的夜里

我好想你 在人群的缝隙

你听见吗 这一句喜欢你

追得上你背影吗

那些大喊过的名字 没完成的约定

全都藏在心底 开出寂寞的花

你好吗 为什么长大就要走散啊


你现在在哪里 隔我多远距离

是否勇敢飞行 有没有人爱你

每当我想起你 世界突然安静

你也一样吗

青春有你出席 不是为了让你缺席

好想沿着回忆 狂奔向你 昨日的青空

随少年 挥手消失在 人海之中


你在吗 你要幸福啊

我想你啊


Vim 修炼秘籍之语法篇

作者 SimonAKing
2018年3月18日 16:44

欲练此功,必先自宫。

前言

少年,我看你骨骼精奇,是万中无一的武学奇才,维护世界和平就靠你了,我这有本秘籍《Vim 修炼秘籍》,见与你有缘,就十块卖给你了! —— 本秘籍建议零售价:10 元

如果你是一名 Vimer,那么恭喜你,你的 Vim 技能马上要升级了 😈!如果你之前不了解过 Vim ,那么也没关系,本文就当成学英语语法了!好,废话不多说,秘籍送上。ps: 建议先熟悉一遍 Vim 修炼秘籍之命令篇,本秘籍食用更佳

正文

核心秘诀

Vim 功法之究极总结: (操作次数)+ 操作行为 + 操作范围 下面,我会将此秘诀 亲自传授于你。

秘诀解析

先列一些常用的 Vim 招式给你瞧瞧,看你是否是传说的练武奇才:

c2w       ——  删除当前光标后紧邻的两个单词,并转换为普通模式
dip —— 删除当前光标所处的一段内容
y3f{char} —— 从当前字符,一直复制到第三个 {char} 字符
>G —— 当前行到结尾,整体缩进一个 <Tab>
v`{mark} —— 从当前字符,选择到 {mark} 标记位置
cit —— 删除当前光标所处标签中的内容
如果把招式看成语言,解析其语法,不难发现一个共同点:

每个招式必须包括 谓语(操作行为) 与 宾语(操作对象)

好,既然有了线索,我这就带你各个击破!

谓语(操作行为)

众所周知,谓语是 动词词性的。

在 Vim 功法中,常用的动词如下:

  1. c

    删除内容,进入普通模式

  2. d

    删除

  3. y

    复制

  4. v

    选择

  5. >,<

    缩进

  6. =

    智能缩进

  7. gu,gU,g~

    改变内容大小写

宾语(操作对象)

宾语代表的是操作对象,而在 Vim 中,所需操作的对象即是 操作的范围。而常用的操作范围命令如下:

  1. w

    从当前位置,到下一个单词首字母,单词由特殊字符(”.等)作为间隔

    内容: .main {xxx} , 光标处在字符 m 招式:caw结果:. {xxx}

  2. W

    从当前位置,到下一个单词首字母,单词由空格作为间隔

    内容: .main {xxx} , 光标处在字符 m 招式:caW 结果: {xxx}

  3. e

    从当前位置,到本单词尾字母

  4. ge

    从当前位置到上一个单词尾字母

  5. b

    1. 通常代替括号中的内容
    2. 上一个单词首字母
  6. p

    段落 paragraph

  7. s

    句子 sentence

  8. t

    标签中的内容

  9. B

    通常代替花括号中的内容

  10. 符号 {) "'<

    代表的就是 该符号中的内容

  11. G,gg

    结尾,开头

  12. j,k,h,l

    上下左右

  13. /{word}<Enter>

    从当前字符 到下一个 word 之间的内容

  14. f{char}

    从当前字符到下一个 char 字符之间的内容

  15. `mark

    从当前字符到 mark 标记 之间的内容

  16. 双写动词,操作范围默认是当前行

    1. dd 删除当前行
    2. cc 删除当前行 并转换为普通模式
    3. >> 缩进当前行
    4. v 动词例外,使用 V 选中当前行。
    5. gU 命令是一种特殊情况,可用 gUU 来使它作用于当前行。

数词(操作次数)

假如现在有个任务,让你删除两个单词,你会怎么做?

  1. d2w

    删除两个单词

  2. 2dw

    执行两次删除单词的命令

虽然这两式结果相同,但意义却有所差别,数词灵活多变,可放在 谓语(操作行为)前,又可放在 宾语(操作对象)前,

  • 放在 谓语前,代表操作次数,
  • 放在 宾语前,代表操作范围的扩张次数

介词(修饰范围)

常用的介词就两个:i(inside) , a(around)举例说明:光标处在字符 c(main)(div class="vim-test" I am Vimer! div)(main)

分别操作:

  • dab结果:(main)(main)
  • dib结果:(main)()(main)

顾名思义,i 就是内部的内容,a 就是连同包裹的内容 一起干掉。

招式实战

我已经教授了你 Vim 秘籍的基本功 :(操作次数)+ 操作行为 + 操作范围 现在,利用你所学,来实战巩固吧。

  1. 将下面标签中的内容修改了: I am Vimer! (光标所处 字符 c )

    (div class="vim-test")you are Bimer!(div)

  2. 修改下面字符串为 https://tomotoes.com !(光标所处 第一个 w 字符)

    https://www.tomotoes.com

  3. 将文件 所有内容格式化,写出 你认为的命令

  4. 将下面字符串改为 I am Vimer (光标所处 I 字符)

    I am VIMER

  5. 将下面字符串的每个字符替换为 ‘!’

    I love github and juejin

等待你的评论答案。

结束语

好的功夫是练出来的,请你以后务必勤加练习。

好了,Vim 语法先告一段落,希望本文能给你带来帮助。

如果你想了解 Vim 其他方面 如:Ex 命令,寄存器等,请关注我的后续文章。

转载本站文章请注明作者和出处 tomotoes.com,请勿用于任何商业用途。

❌
❌