一直以来,对webpack总有种说不清道不明的感觉,一来对于我这种前端小白来说webpack用的频率太低,二来即便用到也是各种插件loader啥的满天飞,配置文件里面一把梭,从来没有关注过webpack本身的“pack”这个功能。这回有空看了这个库,算是对webpack有了一定的感性认知。故此写下一点笔记。
我们为什么需要webpack?
随着前端越来越复杂,前端代码需要像后端一样拥有模块化开发。但是浏览器本身(早年间)并不支持模块化,但对于开发者而言模块化开发是有诸多好处的。webpack在此看见了“商机”,与其等代理浏览器支持模块化,不如发明一种转换工具。让开发者开发的时候使用模块化,等到要上生产环境时,把所有代码打包到一起供浏览器使用。
那么我们如何将模块化的项目打包在一个文件里面呢?
写过后端代码肯定知道,纵使代码之间依赖错综复杂,但一定有一个入口文件,这个入口文件便是这个项目启动的关键。因此同理也适用于前端模块化项目。
当我们找到入口文件后,通过一些方法可以将所有依赖的文件全部遍历到。这样,我们就收集了项目中的所有用到的模块。
最后,将所有的模块打包到一起,就构成了最终的打包的代码。
总共分两步:
step1:从入口文件出发,收集沿途的模块信息以及依赖。
step2:将上一步收集的模块依赖信息打包到一起。
step1:从入口文件出发,收集沿途的模块信息以及依赖。
let ID = 0;function createAsset(filename) {// 将文件内容以字符串形式保存到content中const content = fs.readFileSync(filename, "utf-8");// 将文件内容解析成AST树const ast = babylon.parse(content, {sourceType: "module"});const dependencies = [];// 收集文件AST树中的imoprt requre这种引入语句。并将其内容作为依赖存入dependencies数组中traverse(ast, {ImportDeclaration: ({ node }) => {dependencies.push(node.source.value);}});const id = ID++;// 将content的文件内容转换成浏览器可执行的语法。const { code } = transformFromAst(ast, null, {presets: ["env"]});return {id,filename,dependencies,code};}
function createGraph(entry) {// 作为入口const mainAsset = createAsset(entry);// 这个队列存放所有模块文件代码以及模块对应的依赖。const queue = [mainAsset];for (const asset of queue) {asset.mapping = {}; // 该模块asset的依赖{path: id}这种形式const dirname = path.dirname(asset.filename);asset.dependencies.forEach(relativePath => {const absolutePath = path.join(dirname, relativePath);const child = createAsset(absolutePath);asset.mapping[relativePath] = child.id;queue.push(child);});}return queue;}
createGraph函数的作用从入口文件开始收集沿途的模块信息以及依赖。
step2:将上一步收集的模块依赖信息打包到一起。
function bundle(graph) {let modules = "";graph.forEach(mod => {modules += `${mod.id}: [function (require, module, exports) {${mod.code}},${JSON.stringify(mod.mapping)},],`;});const result = `(function(modules) {function require(id) {const [fn, mapping] = modules[id];function localRequire(name) {return require(mapping[name]);}const module = { exports : {} };fn(localRequire, module, module.exports);return module.exports;}require(0);})({${modules}})`;return result;}const graph = createGraph("./entry.js");const result = bundle(graph);// 写入到我们的dist目录下fs.mkdirSync("./dist");fs.writeFileSync("./dist/bundle.js", result);
这一步,不算很好理解。
大概原理就是:bundle函数参数graph,拥有所有模块的代码。这一步的目的就是重写require方法以及export对象,require的代码内容应该在graph里面取得。最后将这些代码通过字符串拼接的方式拼接在一起,生成一个文件,写入到文件中就是打包后的文件了。
(function(modules) {function require(id) {const [fn, mapping] = modules[id];function localRequire(name) {return require(mapping[name]);}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 = "world");},{}]});
