前言
现在的webpack功能非常巨大,我们不可能实现所有功噢,所有本篇文章不考虑plugins,loaders,多文件打包等复杂问题,仅仅实现一个简单的bundle.js。将其中的ES6代码转为ES5代码,并将这些文件打包,生成一段能在浏览器正常运行的代码啦
本篇文章将从以下几个模块来分析如何实现一个简单的webpack
一、什么是webpack
二、webpack的构建流程
三、简单实现原理
四、准备工作
五、代码实现
一、什么是webpack
看下官方定义:
本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle
简言之,webpack是一个打包模块化 JavaScript 的工具,在 webpack里一切文件皆模块,通过 Loader 转换文件,通过 Plugin 注入钩子,最后输出由多个模块组合成的文件,专注于构建模块化项目
webpack的核心概念
- 入口(entry)
- 输出(output)
- loader: 模块转换器、将所有的非JS文件转换为webpack能够有效识别的模块
- 插件(plugins): 在webpack运行的生命周期中会广播出许多事件,plugin可以监听这些事件,在合适的时机通过webpack提供的API改变输出结果
二、webpack的构建流程
图片来源网上
- 初始化参数: 生成options (将webpack.config.js和shell中的参数,合并中options对象)
- 实例化complier对象 (webpack全局的配置对象,包含entry,output,loader,plugins等所有配置信息)
- 实例化Compilation对象 (compiler.run方法执行,开始编译过程,生成Compilation对象)
- 分析入口js文件,调用AST引擎(acorn)处理入口文件,生成抽象语法树AST,根据AST构建模块的所有依赖
- 通过loader处理入口文件的所有依赖,转换为js模块,生成AST,继续遍历,构建依赖的依赖,递归,直至所有依赖分析完毕
- 对生成的所有module进行处理,调用plugins,合并,拆分,生成chunk
- 将chunk生成为对应bundle文件,输出到指定目录
三、简单实现原理
- 解析一个文件及其依赖
- 构建一个依赖关系图
- 将所有东西打包成一个单文件
四、准备工作
在编写构建工具之前,需要下载四个包
1.@babel/parser: 分析我们通过 fs.readFileSync 读取的文件内容,返回 AST (抽象语法树) 或者Babylon(Babel的解析器) 2.@babel/traverse: 可以遍历 AST, 并负责替换、移除和添加节点,拿到必要的数据 3.@babel/core: babel 核心模块,有个transformFromAst方法,可以将 AST 转化为浏览器可以运行的代码 4.@babel/preset-env: 将代码转化成 ES5 代码
五、代码实现
1.解析文件及其依赖
- babylon解析代码生成AST;
- babel-travere遍历AST,通过importDeclaration方法获取import引入的模块,计算依赖的模块集合
- babel-core 的transformFromAst属性结合babel/preset-env 将ES6->ES5
代码实现
const fs = require('fs');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
const { transformFromAst } = require('babel-core');
// 解析一个文件及其依赖
function createAsset(filename) {
const content = fs.readFileSync(filename, "utf-8");
/**
* sourceType 可以是 "module" 或者 "script",它表示 Babylon 应该用哪种模式来解析。
* "module" 将会在严格模式下解析并且允许模块定义,"script" 则不会
* sourceType 的默认值是 "script" 并且在发现 import 或 export 时产生错误,使用 scourceType: "module" 来避免这些错误
*/
const ast = babylon.parse(content, {
sourceType: "module",
});
// 存储分析的依赖
const dependencies = [];
// 遍历AST抽象语法树
traverse(ast, {
// 获取通过import引入的模块
ImportDeclaration: ({ node }) => {
dependencies.push(node.source.value);
},
});
const id = ID++; // 记录每一个载入的模块id,可清晰的看到当前的依赖的模块
const { code } = transformFromAst(ast, null, {
presets: ["env"],
});
return {
id,
filename,
dependencies,
code
};
}
执行结果
{ id: 0,
filename: './src/entry.js',
dependencies: [ './message.js' ],
code:
'"use strict";\n\nvar _message = require("./message.js");\n\nvar _message2 = _interopRequireDefault(_message);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconsole.log(_message2.default);' }
看结果就可以理解,输出的依赖是什么啦~
2.分析模块间的依赖、生成依赖图谱
从入口递归分析,生成整个项目的依赖图谱
代码实现
// 构建一个依赖关系图
function createGraph(entry) {
const mainAssets = createAsset(entry);
const queue = [mainAssets];
for (const asset of queue) {
const dirname = path.dirname(asset.filename);
asset.mapping = {};
asset.dependencies.forEach((relativePath) => {
const absolutePath = path.join(dirname, relativePath);
/**
* src/message.js ./message.js
* src/name.js
*/
const child = createAsset(absolutePath);
asset.mapping[relativePath] = child.id;
queue.push(child);
});
}
return queue;
}
打印
const graph = createGraph("./src/entry.js");
console.log(graph)
执行结果
[ { id: 0,
filename: './src/entry.js',
dependencies: [ './message.js' ],
code:
'"use strict";\n\nvar _message = require("./message.js");\n\nvar _message2 = _interopRequireDefault(_message);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconsole.log(_message2.default);',
mapping: { './message.js': 1 } },
{ id: 1,
filename: 'src/message.js',
dependencies: [ './name.js' ],
code:
'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\n\nvar _name = require("./name.js");\n\nexports.default = "hello " + _name.name + "!";',
mapping: { './name.js': 2 } },
{ id: 2,
filename: 'src/name.js',
dependencies: [],
code:
'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nvar name = exports.name = \'watermelon\';',
mapping: {} } ]
3.将所有东西打包成一个单文件
/// bundle.js
function bundle(graph) {
// 生成代码字符串
let modules = "";
graph.forEach((mod) => {
modules += `${mod.id}:[
function(require,module,exports){
${mod.code}
},
${JSON.stringify(mod.mapping)}
],`;
});
/**
* 打印 moudles,可看出是个这样0: [...], 1: [...]形式的字符串,最后在导入模块的时候,会给这个字符串加上
* 一个 {}, => {0: [...], 1: [...]},你没看错,这是一个对象,这个对象里用数字作为 key、
* 一个数组作为值、[0] 第一个就是我们被包裹的代码,[1]第二个就是对应的 mapping
*/
const result = `
(function(modules){
function require(id){
const [fn, mapping] = modules[id];
// 代码引入文件时根据相对路径,这里需要把相对路径跟id进行一个映射
function localRequire(relativePath){
// require(id)--递归调用require(id),实现模块的自动导入
return require(mapping[relativePath])
}
const module = {exports:{}};
fn(localRequire,module,module.exports)
return module.exports;
}
require(0); // 执行入口模块
})({${modules}})
`;
return result;
}
const graph = createGraph("./src/entry.js");
const result = bundle(graph);
console.log(result)
直接在命令行输入 node bundle.js
执行结果
(function (modules) {
function require(id) {
const [fn, mapping] = modules[id];
// 代码引入文件时根据相对路径,这里需要把相对路径跟id进行一个映射
function localRequire(relativePath) {
// require(id)-- 实现模块的自动倒入
return require(mapping[relativePath])
}
const module = { exports: {} };
fn(localRequire, module, module.exports)
return module.exports;
}
require(0); // 执行入口模块
})({
0: [
function (require, module, exports) {
"use strict";
var _message = require("./message.js");
var _message2 = _interopRequireDefault(_message);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
console.log(_message2.default);
},
{ "./message.js": 1 }
], 1: [
function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _name = require("./name.js");
exports.default = "hello " + _name.name + "!";
},
{ "./name.js": 2 }
], 2: [
function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var name = exports.name = 'watermelon';
},
{}
],
})
可看到打包处理的代码从整体来看,就是一个立即执行函数,大致结构如下
(function(modules) {
...
})({
...
})
我们的代码被加载到页面中的时候,是需要立即执行的,所以输出的bundle.js本质上要是一个立即执行函数,
const module = { exports : {} };
fn(localRequire, module, module.exports);
return module.exports;
等价于
const newModule = {exports: {}}
fn(childRequire, newModule, newModule.exports)
return newModule.exports // 这个模块的 exports 对象
4.生成的JS文件可在浏览器运行
1.复制上述生成的字符串代码直接在浏览器的控制台执行
2.建立一个dist目录,将字符串放在main.js文件里,测试
执行结果
3.当然也可自动指定输出的文件 增加下面代码
// bundle.js
const build = file => {
const content = bundle(createGraph(file))
// 写入到dist/main.js
fs.mkdirSync('./dist')
fs.writeFileSync('./dist/main.js', content)
}
build('./src/entry.js')
结尾:
目前为止,我们已经实现了一个简单的webpack啦,当然真正意义上的webpack实现还需要考虑非常多的因素,不过通过这个简单的例子,相信你对webpack做的事情,有了比较清楚的了解了。