已被阅读 909 次 | 文章分类:javascript | 2022-03-04 20:52
通过写一个简单的webpack工具,理解打包过程中的技术以及基础理论
1 AST概念
抽象语法树( Abstract Syntax Tree,AST ),或简称语法树( Syntax tree ),是源代码语法结构的一种抽象表示。 它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
2 AST应用
在前端,可以通过 Javascript 解析器将我们程序的源代码映射成为一棵语法树,而树的每个节点对应着代码里的一种结构;比如表达式,声明语句,赋值语句等都会被映射为语法树上的一个节点,进而我们就可以通过操作语法树上的节点来控制我们的源代码;总结下来就是AST三板斧:
(1) 将源代码映射成AST
(2) 操作AST进行遍历更新
(3) 将更新后的AST再转换为源
说到以上三点,作为一个前端开发者,必然想到babel,下面就以babel来解释一下AST在编译中的应用
3 babel与AST的关系
Babel解析器是一个javascript解析器;最常见的功能就是将我们的高版本js语法解析为浏览器识别的es5语法;为了使用高版本js带来的便利,工作中会经常用es6或者es7来开发,但是某些浏览器并不能完全兼容该语法,所以需要babel将他们编译为浏览器可识别的es5的语法
@babel/parser
负责将源码解析为AST;该api负责将javascript源码根据表达式,函数声明,变量定义,导入声明等类型,解析为树节点,用树的方式将代码组织起来;方便后续遍历更新
@babel/traverse
<strong>负责遍历操作AST节点;</strong>通过该api可以方便的获取所有节点,比如获取所有变量类型节点,或者获取所有的console.log()表达式,获取所有debugger等等;在打包时设置的去除所有打印语句或者debugger关键词等;就是在这一步实现,我们根据传入的规则,将一些不需要的节点遍历删除或者更改;
@babel/core
在遍历更新完ast后,将更新后的AST重新编译为浏览器可兼容的低版本源码;
4 代码演示
4.1 新建一个目录
在根目录下新建src文件夹,然后在src文件夹新建如下三个测试文件
// hello.js
export const hello="hello"
// name.js
export const name="name"
// message.js
import {hello} from './hello.js'
import {name} from './name.js'
export default function message() {
console.log(`${hello} ${name}!`)
4.2 新建入口文件
在src目录下新建entry.js
// 这是一个入口文件
// 导入
import message from './message.js'
import {name} from './name.js'
// 变量
const value="xiaobai";
// 函数
function getName(){
return value
}
// 表达式
message()
let res=value==="xiaobai"?true:false
console.log('----name-----: ', name)
到这里准备工作完成了;下面剧通过AST三板斧对入口文件关联的所有代码进行打包
4.3 安装babel包
执行脚本时,读取该配置文件;跟vue cli中vue.config.js是一个道理
const path = require('path')
module.exports = {
entry: 'src/entry.js',
output: {
filename: "bundle.js",
path: path.resolve(__dirname, './dist'),
}
}
4.5 代码编译脚本文件(打包核心部分)
根目录下新建一个index.js;待会我们直接执行node index.js 即可打包我们的代码;跟vue工程中 npm run build是一个道理;
(1)第一步:获取所有源码内容
根据三板斧流程,获取更新后的源码
const fs = require('fs');
const path= require('path')
// 获取配置文件
const config = require('./minipack.config');
// 获取入口文件路径
const entry = config.entry;
// 获取入口文件内容
const mainAssert = createAsset(entry)
// 获取入口文件内容
function createAsset(){
// 1 获取AST
const content = fs.readFileSync(entry, 'utf-8');
const babelParser = require('@babel/parser')
const ast = babelParser.parse(content, {
sourceType: "module"
})
const dependencies = []
// 2 遍历AST;获取入口文件的所有依赖
const traverse = require('@babel/traverse').default
traverse(ast, {
// 遍历所有的 import 模块,并将相对路径放入 dependencies
ImportDeclaration: ({node}) => {
dependencies.push(node.source.value)
}
})
// 3 AST编译为源码
const {transformFromAst} = require('@babel/core');
const {code} = transformFromAst(ast, null, {
presets: ['@babel/preset-env'], // 代码解析规则
})
// 返回结果
return {
dependencies,
code,
}
}
我们打印ast变量,看一下ast树的组织节点长啥样:
打印查看可能不太直观,我们借助一个网站查看:https://astexplorer.net/
将entry.js文件内容复制到左侧,右侧自动展示出AST树,可以直观的看到源码是如何被以节点的方式组织;如下
7行源码,分别在body节点中被管理;主要有导入声明、变量声明、函数声明、表达式声明等类型;打开最后一个输出语句表达式看一下;结构如下
看到标志符的类型是console;其实我们再vue工程中打包的时候,会加如一些规则插件,比如去掉console,debugger等;在编译过程中,就是在利用Travser遍历AST后,然后根据节点名称去做删除操作的;将删除后的AST再转换成新的源码。
(2)递归解析所有依赖,形成依赖关系图
我们根据解析后的入口文件AST,将所有以依赖的文件用数组管理起来;代码如下
// entry: 入口文件绝对地址
const queue = {
[entry]: mainAssert
}
// 递归解析所有的依赖项,生成一个依赖关系图
// 遍历 queue,获取每一个 asset 及其所以依赖模块并将其加入到队列中,直至所有依赖模块遍历完成
for (let filename in queue) {
let assert = queue[filename]
recursionDep(filename, assert)
console.log("queue",queue);
}
/**
* 递归遍历,获取所有的依赖
* @param {*} assert 入口文件
*/
function recursionDep(filename, assert) {
// 跟踪所有依赖文件(模块唯一标识符)
assert.mapping = {}
// 由于所有依赖模块的 import 路径为相对路径,所以获取当前绝对路径
const dirname = path.dirname(filename)
assert.dependencies.forEach(relativePath => {
// 获取绝对路径,以便于 createAsset 读取文件
const absolutePath = path.join(dirname, relativePath)
// 与当前 assert 关联
assert.mapping[relativePath] = absolutePath
// 依赖文件没有加入到依赖图中,才让其加入,避免模块重复打包
if (!queue[absolutePath]) {
// 获取依赖模块内容
const child = createAsset(absolutePath)
// 将依赖放入 queue,以便于继续调用 recursionDep 解析依赖资源的依赖,
// 直到所有依赖解析完成,这就构成了一个从入口文件开始的依赖图
queue[absolutePath] = child
if(child.dependencies.length > 0) {
// 继续递归
recursionDep(absolutePath, child)
}
}
})
}
(3) 根据依赖关系数组,返回浏览器可执行的js文件
这一步就是将入口文件所依赖的所有js文件代码,写入到匿名IIFE函数中;
// 使用依赖图,返回一个可以在浏览器运行的 JavaScript 文件
let modules = ''
for (let filename in queue) {
let mod = queue[filename]
modules += `'${filename}': [
function(require, module, exports) {
${mod.code}
},
${JSON.stringify(mod.mapping)},
],`
}
const result = `
(function(modules) {
function require(moduleId) {
const [fn, mapping] = modules[moduleId]
function localRequire(name) {
return require(mapping[name])
}
const module = {exports: {}}
fn(localRequire, module, module.exports)
return module.exports
}
require('${entry}')
})({${modules}})
`
(4)将可执行js代码,写入到文件;利用nodejs fs的writeFile将文件写入到js文件;
// 写入 ./dist/bundle.js
fs.writeFile(`./dist/bundle.js`, result, (err) => {
if (err) throw err;
console.log('文件已被保存');
})
QQ:3410192267 | 技术支持 微信:popstarqqsmall
Copyright ©2017 xiaobaigis.com . 版权所有 鲁ICP备17027716号