以下文章来源于Enjo ,作者Enjo
没理
bundle bundle bundle 重要的事情讲三遍
一个基础的打包编译工具可以做什么
1.解析
a. 词法分析:分词,生成 tokens
b. 语法分析:生成抽象语法树 ast
2.转换
a. ES5 语法转换为 ES5
b. 处理模块,收集依赖
3. 生成code
生成一个可以在浏览器加执行的 js 文件。
webpack 是什么?
它是一个将一切资源(如scripts / images / styles/ assets)都当成模块的模块化打包工具。
webpack 是如何生成 bundle 的?
生成 bundle 文件后,html 页面就可以利用 script 标签的 src 去引入该文件。
实现一个基础版的 webpack
所谓基础版的 webpack 是指不包含 不包含 不包含 loader 和 plugin。
1. 先看一个demo
打包后的文件
2. 实现 webpack
测试文件 bundle.js
测试文件 bundle.js
2.1 定义 Webpack 类
定义 Webpack 类
2.2 实现 lib/webpack.js
第一步:从入口文件出发,读取入口文件内容
实现:定义 readFile 方法,读取文件内容
第二步:文件内容转化为 AST
定义parseContent 方法,使用 @babel/parser 的 parse 方法以模块模式将文件内容转化为抽象语法树 AST。
第三步:收集依赖关系
对 AST 节点进行递归遍历,收集当前模块的依赖关系,保存在 dependencies 里。 通过 @babel/traverse 模块对AST节点进行遍历,找到 type是 ImportDeclaration 的节点,保存当前节点的依赖关系。
第四步:AST 转化为 code
通过 @babel/core 模块的 transformFromAst 方法把 AST 转化为可执行的代码。
第五步:生成当前文件完整的文件关系依赖映射
经过第一~第五步,生成当前modulePath的关系依赖映射:
{ filename: modulePath, // 文件路径,形如 ./src/index.js dependencies, // 当前文件的依赖模块,形如{'./a.js': './src/a.js'} code // 浏览器可执行的代码 }
第六步:生成关系图谱
从入口文件出发,对当前文件的所有依赖都执行第一~第五步的过程,递归遍历当前模块依赖的所有模块,最后生成依赖关系图谱 graph。
最后,生成 bundle
把依赖关系图谱转换为浏览器可执行的 JS 文件。 1. 从入口文件开始执行生成的代码片段 code; 2. 模块是否有依赖:
a. 有依赖,则自定义 require 方法,把模块的相对路径转化为相对于根目录的路径,如 require("./a.js") ---> require("./src/a.js"); b. 无依赖,执行 code 即可 3. 导出的模块都挂载在 exports 上。
生成的代码可以在浏览器直接输出结果,大家可以试下:
代码语言:javascript复制
(function(graph) {
// webpackBootstrap
function require(modulePath){
// 缺失了 require 补齐:require("./a.js") ---> require("./src/a.js")
function localRequire(relativePath) {
return require(graph[modulePath].dependencies[relativePath]);
}
const exports = {};
// 自执行函数前的表达式或者方法执行必须加分号,否则自执行函数会被当作表达式或者方法的参数执行
(function(require, exports, code){
eval(code);
})(localRequire, exports, graph[modulePath].code)
return exports;
}
require('./src/index.js');
})({"./src/index.js":{"dependencies":{"./a.js":"./src/a.js"},"code":""use strict";nnvar _a = require("./a.js");nnconsole.log('Hello bundle ', _a.a);"},"./src/a.js":{"dependencies":{"./b.js":"./src/b.js"},"code":""use strict";nnObject.defineProperty(exports, "__esModule", {n value: truen});nexports.a = void 0;nnvar _b = require("./b.js");nnvar a = 'A' _b.b;nexports.a = a;"},"./src/b.js":{"dependencies":{},"code":""use strict";nnObject.defineProperty(exports, "__esModule", {n value: truen});nexports.b = void 0;nvar b = 'B';nexports.b = b;"}})
完整代码
代码语言:javascript复制
const webpack = require('./lib/webpack-wechat.js')
const config = require('./webpack.config.js')
new webpack(config)
// webpack.js
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
class Webpack {
constructor(options) {
this.entry = options.entry
this.output = options.output
this.bundleFile()
}
// 1. 读取文件内容
readFile(modulePath) {
const content = fs.readFileSync(modulePath, 'utf-8')
return content
}
// 2. 解析文件内容生成 ast
parseContent(content) {
const ast = parser.parse(content, {
sourceType: 'module'
})
return ast
}
// 3. 从入口文件开始收集模块的依赖,生成依赖关系图
// 收集单个模块的依赖,生成 code
transform(modulePath) {
const content = this.readFile(modulePath)
const ast = this.parseContent(content)
const dependencies = {}
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(modulePath)
dependencies[node.source.value] = './' path.join(dirname, node.source.value)
}
})
const { code } = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return {
filename: modulePath,
dependencies,
code
}
}
generateDependenciesGraph(entry) {
const entryModule = this.transform(entry)
const graphArray = [entryModule]
for (let i = 0; i < graphArray.length; i ) {
const module = graphArray[i]
const { dependencies } = module
if (dependencies) {
for (let key in dependencies) {
graphArray.push(this.transform(dependencies[key]))
}
}
}
let graph = {}
graphArray.forEach(item => {
const { filename, dependencies, code } = item
graph[filename] = {
dependencies,
code
}
})
return graph
}
// 生成 bundle
bundleFile() {
const { entry } = this
const graph = JSON.stringify(this.generateDependenciesGraph(entry))
const content = `
(function(graph) {
// webpackBootstrap
function require(modulePath){
// 缺失了 require 补齐:require("./a.js") ---> require("./src/a.js")
function localRequire(relativePath) {
return require(graph[modulePath].dependencies[relativePath]);
}
const exports = {};
// 自执行函数前的表达式或者方法执行必须加分号,否则自执行函数会被当作表达式或者方法的参数执行
(function(require, exports, code){
eval(code);
})(localRequire, exports, graph[modulePath].code)
return exports;
}
require('${entry}');
})(${graph})
`
const { path: relativePath, filename } = this.output
const bundlePath = path.join(relativePath, filename)
fs.writeFileSync(bundlePath, content, 'utf-8')
}
}
module.exports = Webpack
最后
本文只是实现了一个简单的 webpack,不包含 loader 和 plugin。让大家对 webpack 打包编译过程有个简单的了解。webpack 引入 loader 和 plugin 的完整流程推荐大家看下这篇文章:一文掌握Webpack编译流程。
摘自上文:
1. 初始化参数: 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。
2. 开始编译: 根据我们的webpack配置注册好对应的插件调用 compile.run 进入编译阶段,在编译的第一阶段是 compilation,他会注册好不同类型的module对应的 factory,不然后面碰到了就不知道如何处理了。
3.编译模块: 进入 make 阶段,会从 entry 开始进行两步操作:第一步是调用 loaders 对模块的原始代码进行编译,转换成标准的JS代码, 第二步是调用 acorn 对JS代码进行语法分析,然后收集其中的依赖关系。每个模块都会记录自己的依赖关系,从而形成一颗关系树。
4. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
5. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
文中如有不同见解,大家可以评论区讨论,欢迎大家提出宝贵的意见和建议。