webpack是一个打包工具,他的宗旨是一切静态资源皆可打包。css
首先咱们经过一个制做一个打包文件的原型。html
假设有两个js模块,这里咱们先假设这两个模块是复合commomjs标准的es5模块。前端
语法和模块化规范转换的事咱们先放一放,后面说。vue
咱们的目的是将这两个模块打包为一个能在浏览器端运行的文件,这个文件其实叫bundle.js。node
好比webpack
// index.js
var add = require('add.js').default
console.log(add(1 , 2))
// add.js
exports.default = function(a,b) {return a + b}
复制代码
假设在浏览器中直接执行这个程序确定会有问题 最主要的问题是浏览器中没有exports对象与require方法因此必定会报错。es6
咱们须要经过模拟exports对象和require方法web
首先咱们知道若是在nodejs打包的时候咱们会使用fs.readfileSync()来读取js文件。这样的话js文件会是一个字符串。而若是须要将字符串中的代码运行会有两个方法分别是new Function与Eval。编程
在这里面咱们选用执行效率较高的eval。json
exports = {}
eval('exports.default = function(a,b) {return a + b}') // node文件读取后的代码字符串
exports.default(1,3)
复制代码
上面这段代码的运行结果能够将模块中的方法绑定在exports对象中。因为子模块中会声明变量,为了避免污染全局咱们使用一个自运行函数来封装一下。
var exports = {}
(function (exports, code) {
eval(code)
})(exports, 'exports.default = function(a,b){return a + b}')
复制代码
require函数的功能比较简单,就是根据提供的file名称加载对应的模块。
首先咱们先看看若是只有一个固定模块应该怎么写。
function require(file) {
var exports = {};
(function (exports, code) {
eval(code)
})(exports, 'exports.default = function(a,b){return a + b}')
return exports
}
var add = require('add.js').default
console.log(add(1 , 2))
复制代码
完成了固定模块,咱们下面只须要稍加改动,将全部模块的文件名和代码字符串整理为一张key-value表就能够根据传入的文件名加载不一样的模块了。
(function (list) {
function require(file) {
var exports = {};
(function (exports, code) {
eval(code);
})(exports, list[file]);
return exports;
}
require("index.js");
})({
"index.js": ` var add = require('add.js').default console.log(add(1 , 2)) `,
"add.js": `exports.default = function(a,b){return a + b}`,
});
复制代码
固然要说明的一点是真正webpack生成的bundle.js文件中还须要增长模块间的依赖关系。
叫作依赖图(Dependency Graph)
相似下面的状况。
{
"./src/index.js": {
"deps": { "./add.js": "./src/add.js" },
"code": "....."
},
"./src/add.js": {
"deps": {},
"code": "......"
}
}
复制代码
另外,因为大多数前端程序都习惯使用es6语法因此还须要预先将es6语法转换为es5语法。
总结一下思路,webpack打包能够分为如下三个步骤:
分析依赖
ES6转ES5
替换exports和require
下面进入功能实现阶段。
咱们的目标是将如下两个个互相依赖的ES6Module打包为一个能够在浏览器中运行的一个JS文件(bundle.js)
/src/add.js
export default (a, b) => a + b
复制代码
/src/index.js
import add from "./add.js";
console.log(add(1 , 2))
复制代码
分析模块分为如下三个步骤:
模块的分析至关于对读取的文件代码字符串进行解析。这一步其实和高级语言的编译过程一致。须要将模块解析为抽象语法树AST。咱们借助babel/parser来完成。
AST (Abstract Syntax Tree)抽象语法树 在计算机科学中,或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每一个节点都表示源代码中的一种结构。( astexplorer.net/)
yarn add @babel/parser
yarn add @babel/traverse
yarn add @babel/core
yarn add @babel/preset-env
复制代码
读取文件
收集依赖
编译与AST解析
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");
function getModuleInfo(file) {
// 读取文件
const body = fs.readFileSync(file, "utf-8");
// 转化AST语法树
const ast = parser.parse(body, {
sourceType: "module", //表示咱们要解析的是ES模块
});
// 依赖收集
const deps = {};
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
const abspath = "./" + path.join(dirname, node.source.value);
deps[node.source.value] = abspath;
},
});
// ES6转成ES5
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
const moduleInfo = { file, deps, code };
return moduleInfo;
}
const info = getModuleInfo("./src/index.js");
console.log("info:", info);
复制代码
运行结果以下:

上一步开发的函数能够单独解析某一个模块,这一步咱们须要开发一个函数从入口模块开始根据依赖关系进行递归解析。最后将依赖关系构成为依赖图(Dependency Graph)
/** * 模块解析 * @param {*} file * @returns */
function parseModules(file) {
const entry = getModuleInfo(file);
const temp = [entry];
const depsGraph = {};
getDeps(temp, entry);
temp.forEach((moduleInfo) => {
depsGraph[moduleInfo.file] = {
deps: moduleInfo.deps,
code: moduleInfo.code,
};
});
return depsGraph;
}
/** * 获取依赖 * @param {*} temp * @param {*} param1 */
function getDeps(temp, { deps }) {
Object.keys(deps).forEach((key) => {
const child = getModuleInfo(deps[key]);
temp.push(child);
getDeps(temp, child);
});
}
复制代码
这一步咱们须要将刚才编写的执行函数和依赖图合成起来输出最后的打包文件。
function bundle(file) {
const depsGraph = JSON.stringify(parseModules(file));
return `(function (graph) { function require(file) { function absRequire(relPath) { return require(graph[file].deps[relPath]) } var exports = {}; (function (require,exports,code) { eval(code) })(absRequire,exports,graph[file].code) return exports } require('${file}') })(${depsGraph})`;
}
!fs.existsSync("./dist") && fs.mkdirSync("./dist");
fs.writeFileSync("./dist/bundle.js", content);
复制代码
最后能够编写一个简单的测试程序测试一下结果。
<script src="./dist/bundle.js"></script>
复制代码
ok 学费了。
后面有兴趣的话你们能够在考虑一下如何加载css文件或者图片base64 Vue SFC .vue。