3. 实现 createGraph 函数
在 createGraph()
函数中,我们将递归所有依赖模块,循环分析每个依赖模块依赖,生成一份依赖图谱。 为了方便测试,我们补充下 consts.js
和 info.js
文件的代码,增加一些依赖关系:
// src/consts.js export const company = "平安"; // src/info.js import { company } from "./consts.js"; export default `你好,${company}`;
接下来开始实现 createGraph()
函数,它需要接收一个入口文件的路径( entry
)作为参数:
// leo_webpack.js function createGraph(entry) { const mainAsset = createAssets(entry); // 获取入口文件下的内容 const queue = [mainAsset]; // 入口文件的结果作为第一项 for(const asset of queue){ const dirname = path.dirname(asset.filename); asset.mapping = {}; asset.dependencies.forEach(relativePath => { const absolutePath = path.join(dirname, relativePath); // 转换文件路径为绝对路径 const child = createAssets(absolutePath); asset.mapping[relativePath] = child.id; // 保存模块ID queue.push(child); // 递归去遍历所有子节点的文件 }) } return queue; }
上面代码:
首先通过 createAssets()
函数读取入口文件的内容,并作为依赖关系的队列(依赖图谱) queue
数组的第一项,接着遍历依赖图谱 queue
每一项,再遍历将每一项中的依赖 dependencies
依赖数组,将依赖中的每一项拼接成依赖的绝对路径(absolutePath
),作为 createAssets()
函数调用的参数,递归去遍历所有子节点的文件,并将结果都保存在依赖图谱 queue
中。
注意, mapping
对象是用来保存文件的相对路径和模块 ID 的对应关系,在 mapping
对象中,我们使用依赖文件的相对路径作为 key
,来存储保存模块 ID。
然后我们修改启动函数:
// leo_webpack.js - const result = createAssets('./src/index.js'); + const graph = createGraph("./src/index.js"); + console.log(graph);
这时我们将得到一份包含所有文件依赖关系的依赖图谱:
这个依赖图谱,包含了所有文件模块的依赖,以及模块的代码内容。下一步只要实现 bundle()
函数,将结果输出即可。
4. 实现 bundle 函数
从前面介绍,我们知道,函数 createGraph()
会返回一个包含每个依赖相关信息(id / filename / code / dependencies)的依赖图谱 queue
,这一步就将使用到它了。
在 bundle()
函数中,接收一个依赖图谱 graph
作为参数,最后输出编译后的结果。
4.1 读取所有模块信息
我们首先声明一个变量 modules
,值为字符串类型,然后对参数 graph
进行遍历,将每一项中的 id
属性作为 key
,值为一个数组,包括一个用来执行代码 code
的方法和序列化后的 mapping
,最后拼接到 modules
中。
// leo_webpack.js function bundle(graph) { let modules = ""; graph.forEach(item => { modules += ` ${item.id}: [ function (require, module, exports){ ${item.code} }, ${JSON.stringify(item.mapping)} ], ` }) }
上面代码:
在 modules
中每一项的值中,下标为 0 的元素是个函数,接收三个参数 require
/ module
/ exports
,为什么会需要这三个参数呢?
原因是:构建工具无法判断是否支持require
/ module
/ exports
这三种模块方法,所以需要自己实现(后面步骤会实现),然后方法内的 code
才能正常执行。
4.2 返回最终结果
接着,我们来实现 bundle()
函数返回值的处理:
// leo_webpack.js function bundle(graph) { //... return ` (function(modules){ function require(id){ const [fn, mapping] = modules[id]; function localRequire(relativePath){ return require(mapping[relativePath]); } const module = { exports: {} } fn(localRequire, module, module.exports); return module.exports; } require(0); })({${modules}}) ` }
上面代码:
最终 bundle
函数返回值是一个字符串,包含一个自执行函数(IIFE),其中函数参数是一个对象, key
为 modules
, value
为前面拼接好的 modules
字符串,即 {modules: modules字符串}
。
在这个自执行函数中,实现了 require
方法,接收一个 id
作为参数,在方法内部,分别实现了 localRequire
/ module
/ modules.exports
三个方法,并作为参数,传到 modules[id]
中的 fn
方法中,最后初始化 require()
函数(require(0);
)。
4.3 代码小结
// leo_webpack.js function bundle(graph) { let modules = ""; graph.forEach(item => { modules += ` ${item.id}: [ function (require, module, exports){ ${item.code} }, ${JSON.stringify(item.mapping)} ], ` }) return ` (function(modules){ function require(id){ const [fn, mapping] = modules[id]; function localRequire(relativePath){ return require(mapping[relativePath]); } const module = { exports: {} } fn(localRequire, module, module.exports); return module.exports; } require(0); })({${modules}}) ` }
5. 执行代码
当我们上面方法都实现以后,就开始试试吧:
// leo_webpack.js const graph = createGraph("./src/index.js"); const result = bundle(graph); console.log(result)
这时候可以看到终端输出类似这样的代码,是字符串,这里为了方便查看而复制到控制台了:
这就是打包后的代码咯~
那么如何让这些代码执行呢?用 eval()
方法咯:
// leo_webpack.js const graph = createGraph("./src/index.js"); const result = bundle(graph); eval(result);
这时候就能看到控制台输出 你好,平安
。那么我们就完成一个简单的 Webpack 构建工具啦~
能看到这里的朋友,为你点个赞~
三、总结
本文主要介绍了 Webpack 的构建流程和构建原理,并在此基础上,和大家分享了手写 Webpack 的实现过程,希望大家对 Webpack 构建流程能有更深了解,毕竟面试贼喜欢问啦~