fi3ework / blog

📝
861 stars 51 forks source link

简要分析前端代码从 Babel 编译到打包的流程 #31

Open fi3ework opened 6 years ago

fi3ework commented 6 years ago

简要分析前端代码从 Babel 编译到打包的流程

Babel

目前前端代码的编译器基本都是 Babel,更确切地说是源码到源码的编译器,通常也叫做“转换编译器(transpiler)”。 意思是说你为 Babel 提供一些 JavaScript 代码,Babel 更改这些代码,然后返回给你新生成的代码。实际上 Babel 就是完成了"ES6 代码的字符串 -> ES5 代码的字符串"的转化,虽然输入输出都字符串,但是整个过程需要经历三个阶段:

  1. 解析

Babylon 解析和理解 JavaScript 代码

  1. 变换

babel-traverse 分析和修改 AST

  1. 重建

babel-generator 将 AST 转换回正常的代码

AST

抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。

目前来说,已经有 babylon(现在已变成 babel/core)来帮助我们完成第一步的 AST 生成工作和第三步的再建

Babel 使用一个基于 ESTree 并修改过的 AST,它的内核说明文档可以在[这里](https://github. com/babel/babel/blob/master/doc/ast/spec. md)找到。.

其中第二步变化,包括各种 babel 插件就在 AST 的这个阶段进行操作。

Babel 插件

上面已经提到了 babel 的 plugin 是第二步中,插件拿到代码的 AST 进行处理再返回。

先介绍 Babel 插件的基本格式:

export default function({ types: t }) {
  return {
    visitor: {
      // visitor contents
      Identifier(path, state) {/* do something */},
      ASTNodeTypeHere(path, state) {/* do something */}
    }
  };
};

Babel 通过 babel-traverse 来遍历各个节点,我们在 visitor 中的属性的 key 就是会被遍历到并处理的 AST 中的节点名,并且还能传入这个节点的两个参数:path 和 state。最外层的函数传入的是 babel,一般直接解构获得 babel-types 来辅助进行类型判断。

Path 是表示两个节点之间连接的对象。当你有一个 Identifier() 成员方法的访问者时,你实际上是在访问路径而非节点。 通过这种方式,你操作的就是节点的响应式表示(译注:即路径)而非节点本身。

path 中包含了我们可以修改的属性,也包含添加、更新、移动和删除节点有关的其他很多方法。

state 是从 plugin 的配置文件中传递的参数。

我们来看一个 babel 官方的插件的实现:babel-plugin-transform-undefined-to-void ,有些代码中允许 undefined 被修改,这个插件的功能是使用 void 0 代替 undefined 来保证 undefined 的正确性。

module.exports = function({ types: t }) {
  const VOID_0 = t.unaryExpression("void", t.numericLiteral(0), true); // 生成 `void 0` 对象

  return {
    name: "transform-undefined-to-void",
    visitor: {
      ReferencedIdentifier(path) { // 如果是被引用的标识符
        if (path.node.name === "undefined") { // 如果被赋值的对象是 undefined
          path.replaceWith(VOID_0); // 将 undefined 替换为 `void 0`
        }
      }
    }
  };
};

打包

整体流程参考 minipack,minipack 写了很详细的注释,也有 中文版 的,不再过多重复。

这里再整理一下整个流程的思路:

image

  1. 对打包的入口模块生成 AST,目的是找出所有 import 的声明,就能知道分析的模块依赖了哪些模块,在生成 AST 之后,通过 Babel 将 ES6 的代码转为 ES5 的代码,并将 import 转为兼容性更好的 CommonJS 写法。
  2. 得到了入口文件的 ES5 代码及其依赖模块的路径,我们就可以迭代生成所有模块的 AST,并生成以每个模块的 id 作为索引,其对应的 code 及一个 path -> id 的对象作为值的数组。
  3. 每个模块的执行都需要 require,modules,export 这三个参数,将每个模块对应的 code 报在 function(require, modules, export){ code } 中,在执行每个模块的时候按照相同的函数签名传入对应参数即可。

参考