0%

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

小插曲

一直想搭建一个博客网站,作为工作以来的知识积累汇总,也算有个云存档,复习起来也方便。当时的打算是,把新项目定位成个人门户,不仅可以展示博客,还可以运载一些代码 demo,或者其他任何类型的东西,并且使用 React 体系编写,成为一个 SPA 项目,这样整个网站都是受控状态,能够更加迎合一些突发奇想的 idea。

一开始要实现两个页面,

  • 博客列表,显示博客列表
  • 博客详情,加载 markdown 文件

要获取博客列表,需要先静态分析本地的 markdown 文件,输出一个 manifest 文件,在运行时加载 manifest 文件获取所有博客的信息,当时设计的 manifest 文件格式是这样的,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
interface Slug {
id: string;
depth: number;
tagName: string;
text: string;
}
interface FrontMatter {
title?: string;
description?: string;
date?: string;
author?: string;
categories?: [string, string];
tags?: string[];
complexity?: "easy" | "ordinary" | "hard";
}
interface BlogContent {
id: string;
md5: string;
filename: string;
slugs: Slug[] | null;
}
type BlogDesc = FrontMatter & BlogContent;
interface Manifest {
index: number;
cache: {
[key: string]: BlogDesc;
};
}

本地编写的 md 文件需要支持 front matter 注释,这样可以赋予博客更多信息。

博客详情页面的加载资源不能是 md 格式,所以需要将 md 格式转换成网页可用的 html 格式,并且希望详情页有标题导航,能快速滚动到网页对应锚点位置。

综合如上的需求,我开始写 md 编译脚本,

  1. 提取 md 文件中的 front matter 注释,输出成 manifest 文件
  2. 将 md 格式转换成 html 格式,输出到直到 cache 目录,供运行时加载
  3. 为 html 中的 h 节点增加锚点,并且把每个文件的锚点信息也输出到 manifest 文件
  4. html 文件最终会应用于 React 的 dangerouslySetInnerHTML 属性,这就涉及到网络安全问题,需要对 html 做一个防 XSS 攻击的安全过滤

具体实现还是依靠 AST 中间态来自定义处理,主要使用了 unified 包。

之所以称之为小插曲,是因为并没有继续做下去。主要面临了几个问题,

  1. 本地 markdown 编辑体验较差
  2. 我实现了如上需求的 md 格式转换脚本,发现还是太简陋了,需要支持更多的编译配置项就需要更详细的需求规划,工作量有点大,不如使用 hexo 这个博客搭建工具
  3. 语雀平台的云编辑功能很强大,并且支持在云编辑完后触发 webhook 做一些自动化部署

权衡利弊后,原本的个人门户项目依然需要,但是博客系统分离到 hexo 项目上,考虑个人门户项目是否可以采用微前端架构,作为一个主应用,后续可以接入更多子应用。

接下来本文主要讲述博客搭建过程。

搭建流程

  1. 使用hexo初始化 blog 项目
  2. 选择一个 theme,我选的是theme-next

概述

异常监控是为了能提前预警前端项目发生的异常,我们可以捕获异常,并将其发送给负责收集异常的后台服务,这样就能对错误进行汇总和分析。

例子

前端部分可以这么做:

1
2
3
4
window.onerror = function (msg, url, lineno, colno) {
const img = new Image();
img.src = `http://api/sendError`; // 可以附加本项目信息,为了后台服务定位错误来源和错误分类
};

window.onerror可以全局捕获错误信息,包括外部加载的 JS,不过如果是跨域的外部资源,得使用<script crossorigin></script>,否则无法捕获。这里之所以使用 img 标签,仅仅是为了方便的调用 get 请求。

监控服务接收到错误信息和项目信息后存入数据库,并做可视化展示,这样程序员就能更直观和高效的排查错误。

过滤干扰日志

不过实际收集到的错误信息可能不全是自己前端项目的错误,因为前端页面所在的容器平台可能会注入第三方的脚本,这些脚本本身也可能会抛异常,当一个项目的体量变大,或者监控后台对接的项目越来越多,其接受到的错误信息也会急剧增多,第三方脚本干扰的信息会造成程序员无法快速定位自身项目的异常问题,这就需要对错误信息进行过滤。

下面三种是常见的干扰日志:

  1. 第 1 个是第三方脚本注入
  2. 第 2 个是容器脚本的注入
  3. 第 3 个是由手机制造商脚本注入

概述

unified 是一个通过语法树来处理文本的工具,它支持 remark (Markdown),retext (natural language),和 rehype (HTML)的格式文本。

使用目的

我想要使用 unified 来实现如下功能,

  • 将 md 文档转换成 html 格式
  • 为 html 下的 heading 标签增加唯一 id,同时导出此 html 的锚点信息,用于构建此 html 的导航菜单

例子

实现此功能的核心还是转化成 AST 来做。根据 unified使用文档,一个大纲代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var fs = require("fs");
var path = require("path");
var unified = require("unified");
var markdown = require("remark-parse");
var remark2rehype = require("remark-rehype");
var anthor = require("rehype-slugs");
var sanitize = require("hast-util-sanitize");
var format = require("rehype-format");
var html = require("rehype-stringify");
var report = require("vfile-reporter");

var anthors;
unified()
.use(markdown) // 先将md文档转成md规范的AST
.use(remark2rehype) // 将md规范的AST转换成html规范的AST
.use(sanitize) // 安全处理防止XSS攻击
.use(anthor, {
// 根据AST提取锚点信息
maxDepth: 3,
callback: function (res) {
anthors = res;
},
})
.use(format) // 格式化此AST
.use(html) // 最终将AST转换成html文本
.process(fs.readFileSync(path.resolve(__dirname, "./example.md")), function (
err,
file
) {
console.log(anthors);
console.log(String(file));
});

上述使用到的包除了 rehype-slugs 是自己实现之外其余都可在 unified 的使用教程的例子中找到,后文也会对这些工具包进行一个汇总。

unified 介绍

unified 官方的描述,

unified is an interface for processing text using syntax trees. Syntax trees are a representation of text understandable to programs. Those programs, called plugins, take these trees and inspect and modify them. To get to the syntax tree from text, there is a parser. To get from that back to text, there is a compiler. This is the process of a processor.

1
2
3
4
5
6
7
8
9
10
11
| ........................ process ........................... |
| .......... parse ... | ... run ... | ... stringify ..........|

+--------+ +----------+
Input ->- | Parser | ->- Syntax Tree ->- | Compiler | ->- Output
+--------+ | +----------+
X
|
+--------------+
| Transformers |
+--------------+

总的来讲,unified 先把输入通过 Parser 转换成语法树,再通过一系列 Transformers 来修改这个语法树,最终通过 Compiler 输出。

parse 阶段

我们需要使用不同的 Parser 来解析成不同规范的语法树,unified 主要支持三种规范,分别是:

  • remark (Markdown)
  • retext (natural language)
  • rehype (HTML)

remark及其附属工具族都以remark-开头,对应符合mdast规范的语法树。

retext及其附属工具族都以retext-开头,对应符合nlcst规范的语法树。

rehype及其附属工具族都以rehype-开头,对应符合hast规范的语法树。

通常都是使用对应的[remark|retext|rehype]-parse包来完成。

run 阶段

就是使用对应的工具族来修改对应的语法树,它的 run 阶段使用trough来处理,灵感来自ware,类似 Koa 的剥洋葱中间件组装形式。

stringify 阶段

通常都是使用对应的remark|retext|rehype-stringify包来完成。

AST 构建工具

unist 是一个通用语法树规范,mdast, hast, xast, 和 nlcst 都继承自 unist。

至此应该对 unified 体系的工作流程有了大体的了解。

虽然如上面介绍的,在 parse 阶段使用对应的 parser 完成解析,但我们也可以使用工具直接构建 AST,这里介绍如下几种工具,

如何实现 run 阶段对应的工具包呢

可以参考我对 rehype-slugs 的实现,而我又是参考 mdast-util-toc 实现的。写完后发现有个类似的,不过功能不太一样。

安全性

因为由这些工具生成的内容可能会被直接用在 html 中,造成 cross-site scripting (XSS),可以使用 rehype-sanitize 对 AST 进行安全处理,并且最好把 rehype-sanitize 放在所有 plugins 最后面,因为其他 plugins 也不一定安全。

还是等社区用的人多了再用吧,初步试用了下有好多坑。。。

概述

在初次遇见前端项目的时候,当时就有一个困惑,依赖包为何要以项目为单位进行维护?正常的逻辑应该是,所有项目的依赖包都存放在一个本地中心仓库,初次安装或者不同版本时更新到中心仓库,这样从以项目为单位的分散管理变成统一管理,可以大大提高利用率啊,就像后端项目的 Maven 一样。如果有哪位朋友知道当初这么设计的原因请留言告知。

根据 Node Resolution 的策略,寻找依赖是个很低效的过程,具体分析可以参考这里

Yarn 的 PnP 特性就是解决 Node 包管理低效的问题,开启 PnP 特性后,使用 Yarn install 初始化项目时不再生成 node_modules 目录,而是生成一个.pnp.js 文件,该文件维护了包对应的磁盘位置和依赖项,如下格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[
"react",
new Map([
[
"16.13.1",
{
packageLocation: path.resolve(
__dirname,
"../../Library/Caches/Yarn/v6/npm-react-16.13.1-2e818822f1a9743122c063d6410d85c1e3afe48e-integrity/node_modules/react/"
),
packageDependencies: new Map([
["loose-envify", "1.4.0"],
["object-assign", "4.1.1"],
["prop-types", "15.7.2"],
["react", "16.13.1"],
]),
},
],
]),
];

未开启 PnP 时 Yarn install 实际做了以下事情:

  1. 将依赖包的版本区间解析为某个具体的版本号,下载对应版本依赖的 tar 包到本地离线镜像
  2. 将依赖从离线镜像解压到本地缓存
  3. 将依赖从缓存拷贝到当前目录的 node_modules 目录
  4. We apply the computed changes (a bunch of rsync operations, basically)

在开启 PnP 之后的 Yarn2 版本:

  1. 下载对应版本依赖的 tar 包到本地缓存(合并了离线镜像和本地缓存)
  2. 生成.pnp.js,建立依赖模块到本地磁盘的映射关系

原理

PnP 会使用自定义的 resolver 来处理 require()请求,以此来覆盖原本的 Node Resolution 策略,但同时会造成一些影响,请见下文的注意事项。

好处

  • 节省 install 时间
  • 所有 npm 模块都会存放在全局的缓存目录下, 依赖树扁平化, 避免拷贝和重复
  • 提高模块加载效率. Node 为了查找模块, 需要调用大量的 stat 和 readdir 系统调用. pnp 通过 Yarn 获取或者模块信息, 直接定位模块
  • 不再受限于 node_modules 同名模块不同版本不能在同一目录
  • 高效协作开发,我们可以使用 Zero-Installs,把.pnp.js 提交到版本控制中去,其他人 clone 该项目后不再需要执行 yarn install 操作即可直接运行,注意一下需要 ingore 的文件

如何使用

使用 yarn --pnp或者直接在 package.json 增加如下配置:

1
2
3
4
5
{
"installConfig": {
"pnp": true
}
}

loose 模式

在 yarnrc.yml 配置 pnpMode: loose

默认的 strict 模式下,PnP 会阻止不在显式依赖列表中的依赖(即没有定义在 package.json 中)。开启 loose 模式后,PnP 会利用 node-modules 的提升策略(把深层次的依赖提升到顶层安装)把这些原本会被提升的包记录在“fallback pool”,当未显式定义的依赖但在“fallback pool”清单中时不会被阻止 resolve。不过当同个包的有不同版本时,无法确定哪个版本会被提升,因此会生成 warning。

注意事项

  • 部分第三方包自己实现了 Node Resolution 策略

有一些包可能自己实现了 resolver 来处理 require()请求(除了已结合 PnP API 规范的),这可能会和 PnP 产生冲突异常,可以在 Yarn官方仓库反馈。大多数都可以通过 loose 模式或者插件解决,但是 Flow 和 React Native 和 PnP 完全不兼容,可以在.yarnrc.yml 中配置nodeLinker: node-modules切换为生成 node_modules 文件夹的模式。

  • script 脚本命令需要前置增加 yarn 命令

node index.js => yarn node index.js

  • vscode 需要配置 Editor SDKs

https://yarnpkg.com/advanced/editor-sdks

  • 当需要修改第三方包源码进行调试时

使用 yarn unplug packageName 来将某个指定依赖拷贝到项目中的 .pnp/unplugged 目录下,之后 .pnp.js 中的 resolver 就会自动加载这个 unplug 的版本。调试完毕后,再执行 yarn unplug –clear packageName 可移除本地 .pnp/unplugged 中的对应依赖。

参考

https://yarnpkg.com/features/pnp
https://nodejs.org/api/modules.html#modules_all_together
https://stackoverflow.com/questions/53135221/what-does-yarn-pnp
https://www.zhihu.com/question/367871981?utm_source=qq
https://github.com/yarnpkg/berry/issues/634
http://loveky.github.io/2019/02/11/yarn-pnp/

场景

公司内部有一个公共平台,需要集成各个项目组的相关对内业务,提供给公司员工使用。

暂时取名公共平台为 Public,有三个项目组对内业务 A、B、C,使用 React 开发。

All In One

ABC 业务作为 Public 项目的子模块放在同一个项目里,做统一的版本控制和打包编译。

1
2
3
4
5
├── Public
├── Common
├── A
├── B
├── C

会有如下问题:

  1. A 迭代发版时 B 和 C 可能并不需要。
  2. A 修改了 Common 中的组件,但因为并不了解 B 和 C 的业务,导致该组件不兼容 B 和 C 产生 bug。
  3. 中途需要接入项目组 D,D 是使用 Vue 开发的。

在长时间跨度的单体应用中,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用,变得难以维护。

Micro Frontend

  1. 形成主应用和微应用体系,主应用作为所有微应用的载体,同时可以向微应用分发数据,并且最好是单向的,或者考虑使用回调函数的形式向微应用提供修改主应用的有限能力。
  2. 微应用具备单独运行能力,同时又可接入主应用,可以通过全局遍历来判断是否处在微前端体系下,这个全局变量由主应用注入。
  3. 主应用监测 URL 的变化来决定拉取对应微应用的代码入口文件进行初始化操作,微应用输出一些钩子函数向主应用提供管控自己的能力,主应用可以在各个阶段调用微应用暴露的钩子函数。主微应用之间采用这种轻接触的交流方式,可以保证双方的自治。
  4. 主应用需要为每个微应用提供一个相对独立的沙盒环境,避免一系列冲突。
  5. 由于主应用使用的是编译后的微应用代码(即微应用对于主应用而言就相当于一个外部模块),所以天然的具备技术栈无关性,但同时微应用的打包配置中需要做些标识性配置,以 webpack 为例,修改 output 的 library、libraryTarget、jsonpFunction 参数,其实目的就是为了在主应用运行环境中,每个微应用有自己的命名空间。

微前端框架

使用阿里开源的 qiankun

变化

在微前端体系下,原本的单体应用变成了现在的微应用,项目构成是否发生了什么变化呢?

其实没有太多变化,以 React 为例,原本我们会在入口文件中调用 ReactDOM.render()将组件挂载到 container 中,并且在打包编译后以外部脚本被 HTML 文件引入;在微前端体系下,为了同时也能独立运行,只需在打包的入口文件输出钩子函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (window.__MICRO_FRONTEND__) {
oldRender();
}
function oldRender(props) {
// do something
ReactDOM.render(<App />, document.getElementById("root"));
}
export async function bootstrap() {}
export async function mount(props) {
oldRender(props);
}
export async function unmount() {
ReactDOM.unmountComponentAtNode(document.getElementById("root"));
}
// export other lifecycles

打包编译后(可以配置成 umd 的输出格式),这个入口文件作为模块被主应用引入。

在主应用中,需要提前注册好要对接的微应用,具体流程配置、路由调度原理、沙箱构建原理等就不介绍了,可以自行查找这方面的资料。

FAQ

  • 主应用是监测 URL 的变化来决定接入对应微应用的,但是微应用也有自己的路由系统,此时如何解决路由的接管权?

TODO

  • 主应用的拉取微应用代码的策略,即如何应对微应用变更?

TODO

  • 每个微应用沙盒环境构建策略?

TODO,为什么不使用 iframe,参考qiankun 为什么不使用 iframe

参考

https://github.com/single-spa/single-spa
https://qiankun.umijs.org/zh/guide

与或运算来做类型判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const A = 0b0000000000000001;
const B = 0b0000000000000010;
const C = 0b0000000000000100;
const D = 0b0000000000001000;
const E = 0b0000000000010000;

const AB = A | B;
const CD = C | D;

let current = random();
if (current & AB) {
console.log("属于AB");
} else if (current & CD) {
console.log("属于CD");
} else {
console.log("属于E");
}
if (current > C) {
console.log("在C阶段之后");
}
function random() {
const pool = [A, B, C, D, E];
return pool[Math.floor(5 * Math.random())];
}

按位异或快速取整

1
console.log(20.69 | 0); //20

逗号表达式转移 this 指向

1
2
3
4
5
6
7
8
9
const name = "bar";
const foo = {
name: "foo",
fn() {
console.log(this.name);
},
};
foo.fn(); // foo
(0, foo.fn)(); // bar

运算符快速类型转换

利用加号运算符触发 JS 的默认转换

1
2
3
+new Date(); // 1599441814711
// new Date().getTime(); // 1599441814711
// new Date().valueOf(); // 1599441814711