关键词
AST、Babel、自定义loader、自定义webpack
何为AST?
AST全称是AbStract Syntax tree,也就是抽象语法树,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构
对于前端来讲,它就好似一个很复杂的对象,里面包含着各种各种的key和value,对应着源代码中的不同部分。
通过借助在线工具 astexplorer,我们可以很方便的查看源代码转以后的AST长啥样。
因为后面会自定义一个给async/await函数自动封装一个try catch的loader,所以就以下方代码为例分析AST中常用字段的含义
async function func() {
try {
let res = await asyncFunc()
// do something
} catch (e) {
console.log(e)
}
}
(ps: 因为后面需要用@babel/parser将源代码转译为AST,所以这里使用的是@babel/parser的转译规则)
首先async function,是一个异步函数声明语句,它对应的是图中的1,FunctionDeclaration,在AST中,declaration即声明语句,比如const a = 1、let a = 1就是VariableDeclaration,而class A {}就是ClassDeclaration
对于async function来讲,**它与其他普通的function声明函数对比就是它的async属性值为true,**而普通声明函数值为false,
它的body部分也就是函数块儿,**是一个BlockStatement,这代表一个语句块,是可以独立执行的单位。**函数块儿是BlockStatement,与之类似的有try {} catch(e){},是一个TryStatement,for (let i = 0;i < 10;i ++) {}是一个ForStatement
而对于tryStatement来讲,它内部有三大块儿,try是一块,catch是一块,finally又是一块,也就是图中的5、6、7。当然他们在其他语句中没有普遍性,但在后续的自定义loader中,它们是很重要的标识符。
在日常开发中,try模块儿中一般会放着比较复杂的逻辑,我们将其展开来看
会发现,try中其实也是一个BlockStatement,这与其祖先节点FunctionDeclaration的BlockStatement性质一致,只是里面装的东西不同。
在其内部,有个VariableDeclaration对应的源代码的let声明语句。
在声明语句内部,我们找到了AwaitExpression,**它是一个表达式,它与Statement的区别就是,它执行完之后有返回值。**例如this关键字是ThisExpression,1 + 2是BinaryExpression
AST能干嘛?
我们知道可通过某些工具将源代码转为AST这样的树结构,也就是前端意义上的对象。那既然是对象,我们肯定可以使用某些api来操作这个对象,也就代表我们能在代码的编译层面对代码做一些手脚了,这也就是webpack中loader的实现原理,甚至也是webpack的实现原理。
那有这么强大的工具吗?有的,babel。
什么是babel?
Babel 是一个 JavaScript 编译器
- 语法转换
- 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过引入第三方 polyfill 模块,例如 core-js)
- 源码转换(codemods)
我们需要用到babel下几个重要的插件
-
@babel/parser
能将源代码转译为
AST -
@babel/traverse
转译后的
AST非常复杂,我们怎么能找到想要操作的节点呢?@babel/traverse可以对AST深度遍历,当遍历到我们想要找的节点后,会抛出钩子执行我们注入的回调函数 -
@babel/types
当我们找到目标节点后,我们想对该节点修修剪剪,做一些操作,这时候就需要用到
@babel.types这样一个工具库,它里面有很多工具函数供我们使用 -
@babel/core
能将
AST转为源代码
自定义loader
需求:
async/await是目前较佳的异步请求函数封装方案,但是它不如Promise那样能用catch灵活的处理异常,所以一般会在async函数内,await语句外套一个try/catch语句来捕获异常。每次都写一遍try/catch很麻烦,现在需要有个loader能为打包后的async函数自动加上try/catch处理异常
实现:
1、parser转译:
const parser = require("@babel/parser");
const { getOptions } = require("loader-utils"); // 'loader-utils'中的getOptions可拿到由配置文件传入的options
module.exports = function(source) {
// 获取配置文件传入的options
let options = getOptions(this)
let ast = parser.parse(source, {
sourceType: "module", // 支持es6module
plugins: ["dynamicImport"] // 支持动态import
})
// 解析传入的catchCode生成catchNode节点,便于挂载到catchClause上
let catchNode = parser.parse(options.catchCode).program.body
}
loader其实就是一个工具函数,接收源代码进行处理,再返回处理后的源代码
2、traverse遍历找到目标节点:
首先分析一下待处理的源代码
async function func() {
let res = await asyncFunc()
console.log(res)
}
我们的目标就是要通过
awaitExpression向外查找,查找到async函数FunctionDeclaration和BlockStatement,这两者之间就是该插入TryStatement的位置
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types"); // 引入@babel/types
const loaderUtils = require("loader-utils");
module.exports = function(source) {
let options = getOptions(this)
let ast = parser.parse(source, {
sourceType: "module",
plugins: ["dynamicImport"]
})
let catchNode = parser.parse(options.catchCode).program.body
traverse(ast, {
AwaitExpression(path) {
// 向外查找async函数的BlockStatement
while (path && path.node) {
// 父节点
let parentPath = path.parentPath;
// 是BlockStatement 且 父节点是async函数
if ( t.isBlockStatement(path.node) && t.isFunctionExpression(parentPath.node, { async: true })) {
// 插入TryStatement
// 已经有了try/catch直接退出
} else if ( t.isBlockStatement(path.node) && t.isTryStatement(parentPath.node)) {
return
}
// 向外遍历
path = parentPath
}
}
})
}
3、插入TryStatement:
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types"); // 引入@babel/types
const loaderUtils = require("loader-utils");
module.exports = function(source) {
let options = getOptions(this)
let ast = parser.parse(source, {
sourceType: "module",
plugins: ["dynamicImport"]
})
let catchNode = parser.parse(options.catchCode).program.body
traverse(ast, {
AwaitExpression(path) {
while (path && path.node) {
let parentPath = path.parentPath;
if ( t.isBlockStatement(path.node) && t.isFunctionExpression(parentPath.node, { async: true })) {
// 新建tryStatement
let tryAst = t.tryStatement(
// 将async函数的BlockStatement替换成try语句的内容
path.node,
// 替换catchClause
t.catchClause(
t.identifier('e'),
t.blockStatement(catchNode)
)
)
// 替换当前节点
path.replaceWithMultiple(tryAst)
return
// 已经有了try/catch直接退出
} else if ( t.isBlockStatement(path.node) && t.isTryStatement(parentPath.node)) {
return
}
// 向外遍历
path = parentPath
}
}
})
}
4、core转回源代码:
// core转回源代码
return core.transformFromAstSync(ast, null).code
5、完整代码:
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const core = require("@babel/core");
const { getOptions } = require("loader-utils"); // 'loader-utils'中的getOptions可拿到由配置文件传入的options
module.exports = function (source) {
// 获取配置文件传入的options
let options = getOptions(this)
let ast = parser.parse(source, {
sourceType: "module", // 支持es6module
plugins: ["dynamicImport"] // 支持动态import
})
// 解析传入的catchCode生成catchNode节点,便于挂载到catchClause上
let catchNode = parser.parse(options.catchCode).program.body
traverse(ast, {
AwaitExpression(path) {
// 向外查找async函数的BlockStatement
while (path && path.node) {
// 父节点
let parentPath = path.parentPath
// 是BlockStatement 且 父节点是async函数
if ( t.isBlockStatement(path.node) && t.isFunctionExpression(parentPath.node, { async: true }) ) {
// 新建tryStatement
let tryAst = t.tryStatement(
// 将async函数的BlockStatement替换成try语句的内容
path.node,
// 替换catchClause
t.catchClause(
t.identifier('e'),
t.blockStatement(catchNode)
)
)
// 替换当前节点
path.replaceWithMultiple(tryAst)
return
// 已经有了try/catch直接退出
} else if ( t.isBlockStatement(path.node) && t.isTryStatement(parentPath.node)) {
return
}
// 向外遍历
path = parentPath
}
}
})
// core转回源代码
return core.transformFromAstSync(ast, null).code
}
6、使用:
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: resolve(__dirname, 'loaders/asyncAddTry.js'),
options: {
catchCode: `console.log('error!')`
}
}
]
}
]
}
自定义简易webpack
ps:只实现了基础的打包功能
简易webpack主要构建流程:
- 读取配置文件(webpack.config.js)中的关键参数,构建相关属性
- 处理文件之间的依赖关系,生成依赖关系图
- 构造编译器(打包后的bundle文件),识别依赖关系图
- 将编译器写入出口目录中
1、构建Compiler类,读取配置文件参数
class Compiler {
constructor (options) {
const { entry, output } = options
this.entry = entry
this.output = output
this.modules = [] // 记录依赖的所有文件
}
// 启动编译
run () {
// do something
}
}
2、处理文件之间的依赖关系,生成依赖关系图
既然要构建依赖关系图,首先肯定要明确图中的每个节点要包含哪些必要属性
-
文件路径
知道依赖的是那个文件
-
文件代码
经过处理的,开箱即用的代码
-
文件依赖
知道当前文件又依赖哪些文件,便于整个依赖关系图的展开
const fs = require("fs")
const path = require("path")
const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const { transformFromAst } = require("@babel/core")
const Parser = {
// 获取AST
getAst: path => {
const content = fs.readFileSync(path, "utf-8")
return parser.parse(content, {
sourceType: "module" // 可识别import语法
})
},
// 获取依赖
getDependecies: (ast, filename) => {
const dependecies = {}
traverse(ast, {
ImportDeclaration({ node }) {
// 返回目录
const dirname = path.dirname(filename)
// 找到根目录相对于文件的位置
const filepath = "./" + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
},
// 将ast转译为浏览器可识别的代码
getCode: ast => {
const { code } = transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
return code
}
}
class Compiler {
constructor(options) {
const { entry, output } = options
this.entry = entry
this.output = output
this.modules = []
}
run () {
// 构建入口文件节点
const info = this.build(this.entry)
this.modules.push(info)
this.modules.forEach(({ dependecies }) => {
// 该文件有依赖
if (dependecies) {
for (const dependency in dependecies) {
// 递归调用拿到所有依赖的文件对象
this.modules.push(this.build(dependecies[dependency]))
}
}
})
// 组合成依赖关系图
const dependencyGraph = this.modules.reduce(
(graph, item) => ({
...graph,
// 以路径做key,便于后续解析
[item.filename]: {
dependecies: item.dependecies,
code: item.code
}
}),
{}
)
}
// 生成单个节点
build (filename) {
const { getAst, getDependecies, getCode } = Parser
const ast = getAst(filename)
const dependecies = getDependecies(ast, filename)
const code = getCode(ast)
return {
filename,
dependecies,
code
}
}
}
3、构造编译器(打包后的bundle文件),识别依赖关系图
this.generate(dependencyGraph)
generate (arr) {
// 拿到出口路径
const filePath = path.join(this.output.path, this.output.filename)
const bundle = `(function(graph){
function require(moduleId){
function localRequire(relativePath){
return require(graph[moduleId].dependecies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[moduleId].code)
return exports
}
require('${this.entry}')
})(${JSON.stringify(arr)})`
}
提炼一下核心代码
(function(graph) {
function require(moduleId) {
// 重定义require函数,递归遍历依赖
function localRequire(relativePath) {
return require(graph[moduleId].dependecies[relativePath])
}
var exports = {};
(function(require, exports, code) {
eval(code)
})(localRequire, exports, graph[moduleId].code)
// 导出exports传给require语句赋值的变量
return exports
}
require('./src/index.js')
})({
"./src/index.js": {
"dependecies": {
"./hello.js": "./src/hello.js",
"./add.js": "./src/add.js"
},
"code": "\"use strict\";\n\nvar _hello = require(\"./hello.js\");\n\nvar _add = require(\"./add.js\");\n\nconsole.log((0, _hello.say)('webpack'));\nconsole.log((0, _add.add)(1, 2));"
},
"./src/hello.js": {
"dependecies": {},
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.say = say;\n\nfunction say(name) {\n return \"hello \".concat(name);\n}"
},
"./src/add.js": {
"dependecies": {},
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.add = add;\n\nfunction add(num1, num2) {\n return num1 + num2;\n}"
}
})
4、将编译器写入出口目录中
const fs = require("fs")
const del = require('del')
generate (arr) {
// 拿到出口路径
const filePath = path.join(this.output.path, this.output.filename)
const bundle = `(function(graph){
function require(moduleId){
function localRequire(relativePath){
return require(graph[moduleId].dependecies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[moduleId].code)
return exports
}
require('${this.entry}')
})(${JSON.stringify(arr)})`
// 先删掉
del.sync([this.output.path])
// 新建目录
fs.mkdirSync(this.output.path)
// 写入文件
fs.writeFile(filePath, bundle, (err) => {
console.log(err)
});
}
5、使用
const Complier = require("./Compiler");
const options = require("./webpack.config");
new Complier(options).run();
完整代码
const fs = require("fs")
const del = require('del')
const path = require("path")
const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const { transformFromAst } = require("@babel/core")
const Parser = {
// 获取AST
getAst: path => {
const content = fs.readFileSync(path, "utf-8")
return parser.parse(content, {
sourceType: "module" // 可识别import语法
})
},
// 获取依赖
getDependecies: (ast, filename) => {
const dependecies = {}
traverse(ast, {
ImportDeclaration({ node }) {
// 返回目录
const dirname = path.dirname(filename)
// 找到根目录相对于文件的位置
const filepath = "./" + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
},
// 将ast转译为浏览器可识别的代码,为何不用源代码
getCode: ast => {
const { code } = transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
return code
}
}
class Compiler {
constructor(options) {
const { entry, output } = options
this.entry = entry
this.output = output
this.modules = []
}
// 构建启动
run () {
const info = this.build(this.entry)
this.modules.push(info)
this.modules.forEach(({ dependecies }) => {
// 该文件有依赖
if (dependecies) {
for (const dependency in dependecies) {
// 递归调用拿到所有依赖的文件对象
this.modules.push(this.build(dependecies[dependency]))
}
}
})
// 组合成依赖图
const dependencyGraph = this.modules.reduce(
(graph, item) => ({
...graph,
[item.filename]: {
dependecies: item.dependecies,
code: item.code
}
}),
{}
)
console.log('依赖图', dependencyGraph)
this.generate(dependencyGraph)
}
build (filename) {
const { getAst, getDependecies, getCode } = Parser
const ast = getAst(filename)
// 拿到文件对应的依赖
const dependecies = getDependecies(ast, filename)
// 拿到经过@babel/core处理的浏览器可识别的代码
const code = getCode(ast)
return {
filename,
dependecies,
code
}
}
generate (arr) {
// 拿到出口路径
const filePath = path.join(this.output.path, this.output.filename)
const bundle = `(function(graph){
function require(moduleId){
function localRequire(relativePath){
return require(graph[moduleId].dependecies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[moduleId].code)
return exports
}
require('${this.entry}')
})(${JSON.stringify(arr)})`
// 先删掉
del.sync([this.output.path])
// 新建目录
fs.mkdirSync(this.output.path)
// 写入文件
fs.writeFile(filePath, bundle, (err) => {
console.log(err)
})
}
}
module.exports = Compiler;