共计 4011 个字符,预计需要花费 11 分钟才能阅读完成。
webpack
作为一款打包工具,在学习它之前,对它感到特地生疏,最近花了一些工夫,学习了下。
学习的最大播种是手写一个繁难的打包工具webpack-demo
。
webpack-demo
分为次要分为三个局部:
- 生成形象语法树
- 获取各模块依赖
- 生成浏览器可能执行的代码
依赖筹备
src
目录下有三个文件:index.js
、message.js
、word.js
。他们的依赖关系是:index.js
是入口文件,其中 index.js
依赖 message.js
,message.js
依赖word.js
。
index.js
:
import message from "./message.js";
console.log(message);
message.js
:
import {word} from "./word.js";
const message = `say ${word}`;
export default message;
word.js
:
var word = "uccs";
export {word};
当初要要编写一个 bundle.js
将这三个文件打包成浏览器可能运行的文件。
打包的相干配置项写在 webpack.config.js
中。配置比拟繁难只有 entry
和output
。
const path = require("path");
module.exports = {entry: path.join(__dirname, "./src/index.js"),
output: {path: path.join(__dirname, "dist"),
filename: "main.js",
},
};
代码剖析
获取入口文件的代码
通过 node
提供的 fs.readFileSync
获取入口文件的内容
const fs = require("fs");
const content = fs.readFileSync("./src/index.html", "utf-8");
拿到入口文件的内容后,就须要获取到它的依赖 ./message
。因为它是string
类型。天然就想到用字符串截取的形式获取,然而这种形式太过麻烦,如果依赖项有很多的话,这个表达式就会特地简单。
那有什么更好的形式能够获取到它的依赖呢?
生成形象语法树
babel
提供了一个解析代码的工具@babel/parser
,这个工具有个办法parse
,接管两个参数:
code
:源代码options
:源代码应用ESModule
,须要传入sourceType: module
function getAST(entry) {const source = fs.readFileSync(entry, "utf-8");
return parser.parse(source, {sourceType: "module",});
}
这个 ast
是个对象,叫做形象语法树,它能够示意以后的这段代码。
ast.program.body
寄存着咱们的程序。通过形象语法树能够找到申明的语句,申明语句搁置就是相干的依赖关系。
通过下图能够看到第一个是 import
申明,第二个是表达式语句。
接下来就是拿到这段代码中的所有依赖关系。
一种形式是本人写遍历,去遍历 body
中的type: ImportDeclaration
,这种形式呢有点麻烦。
有没有更好的形式去获取呢?
获取相干依赖
babel
就提供一个工具@babel/traverse
,能够疾速找到ImportDeclaration
。
traverse
接管两个参数:
ast
:形象语法树options
:遍历,须要找出什么样的元素,比方ImportDeclaration
,只有形象语法树中有ImportDeclaration
就会进入这个函数。
function getDependencies(ast, filename) {const dependencies = {};
traverse(ast, {ImportDeclaration({ node}) {const dirname = path.dirname(filename);
const newFile = path.join(dirname, node.source.value);
dependencies[node.source.value] = newFile;
},
});
return dependencies;
}
ImportDeclaration
:会接管到一个节点node
,会剖析出所有的ImportDeclaration
。
通过上图能够看到 node.source.value
就是依赖。将依赖保留到 dependencies
对象中就行了,这外面的依赖门路是绝对于 bundle.js
或者是绝对路径,否则打包会出错。
代码转换
依赖剖析完了之后,源代码是须要转换的,因为 import
语法在浏览器中是不能间接运行的。
babel
提供了一个工具 @babel/core
,它是babel
的外围模块,提供了一个 transformFromAst
办法,能够将 ast
转换成浏览器能够运行的代码。
它接管三个参数:
ast
:形象语法树code
:不须要,可传入null
options
:在转换的过程中须要用的presents: ["@babel/preset-env"]
function transform(ast) {const { code} = babel.transformFromAst(ast, null, {presets: ["@babel/preset-env"],
});
return code;
}
获取所有依赖
入口文件剖析好之后,它的相干依赖放在 dependencies
中。下一步将要去依赖中的模块,一层一层的剖析最终把所有模块的信息都剖析进去,如何实现这个性能?
先定义一个 buildModule
函数,用来获取 entryModule
。entryModule
包含filename
、code
、dependencies
function buildModule(filename) {let ast = getAST(filename);
return {
filename,
code: transform(ast),
dependencies: getDependencies(ast, filename),
};
}
通过遍历 modules
获取所有的模块信息,当第一次走完 for
循环后,message.js
的模块剖析被推到 modules
中,这时候 modules
的长度变成了 2
,所以它会继续执行for
循环去剖析 message.js
,发现message.js
的依赖有 word.js
,将会调用buildModule
剖析依赖,并推到 modules
中。modules
的长度变成了 3
,在去剖析word.js
的依赖,发现没有依赖了,完结循环。
通过一直的循环,最终就能够把入口文件和它的依赖,以及它依赖的依赖都推到 modules
中。
const entryModule = this.buildModule(this.entry);
this.modules.push(entryModule);
for (let i = 0; i < this.modules.length; i++) {const { dependencies} = this.modules[i];
if (dependencies) {for (let j in dependencies) {
// 有依赖调用 buildmodule 再次剖析,保留到 modules
this.modules.push(this.buildModule(dependencies[j]));
}
}
}
modules
是个的数组,在最终生成浏览器可执行代码上有点艰难,所以这里做一个转换
const graphArray = {};
this.modules.forEach((module) => {graphArray[module.filename] = {
code: module.code,
dependencies: module.dependencies,
};
});
生成浏览器可执行的代码
所有的依赖计算完之后,就须要生成浏览器能执行的代码。
这段代码是一个自执行函数,将 graph
传入。
graph
传入时须要用 JSON.stringify
转换一下,因为在字符串中间接传入对象,会变成[object Object]
。
在打包后的代码中,有个 require
办法,这个办法浏览器是不反对的,所有咱们须要定义这个办法。
require
在导入门路时须要做一个门路转换,否在将找不到依赖,所以定义了localRequire
。
require
外部还是一个自执行函数,接管三个参数:localRequire
、exports
、code
。
const graph = JSON.stringify(graphArray);
const outputPath = path.join(this.output.path, this.output.filename);
const bundle = `
(function(graph){function require(module){function localRequire(relativePath){return require(graph[module].dependencies[relativePath])
}
var exports = {};
(function(require, exports, code){eval(code)
})(localRequire, exports, graph[module].code)
return exports;
}
require("${this.entry}")
})(${graph})
`;
fs.writeFileSync(outputPath, bundle, "utf-8");
总结
通过手写一个简略的打包工具后,对 webpack
外部依赖剖析、代码转换有了更深的了解,不在是一个能够应用的黑盒了。
参考资料:从根底到实战 手把手带你把握新版 Webpack4.0