# mini-webpack **Repository Path**: zyl-ll/mini-webpack ## Basic Information - **Project Name**: mini-webpack - **Description**: 手写简单版webpack打包器,学习webpack打包原理! - **Primary Language**: JavaScript - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-07-10 - **Last Updated**: 2022-11-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 打包过程: 1. 初始化参数:初始化 shell、webpack.config.js 里的参数 1. 生成配置对象、开始编译:根据配置生成 compiler 对象,加载各个 loader、plugin,执行 run 方法开始编译 1. 确定入口:确认各个入口文件 1. 编译内容:根据各个入口文件,使用 loader 等进行编译,并递归找到所有依赖。 1. 完成模块编译:编译完成后得到所有模块编译后的内容,以及他们的依赖图谱。 1. 输入资源:根据上面的依赖关系,把各个 module 生成对应 chunk。 1. 生成文件:根据配置,输出对应文件。 ## 写一个 mini-webpack: 参考: ## 编写步骤: webpack 的最主要功能是把若干文件,按照各自的引入方式打包到一个文件里。
webpack 只能处理 js 的文件,所以我们先简历几个 js 为示例。
目录: ```latex │ age.js │ main.js │ student.js └─constant name.js name.json student.js ``` 以 main.js 为入口文件内容如下: ```javascript import nameList from "./constant/name.js"; import students from "./student.js"; import lolName from "./constant/name.json"; console.log("你好 这里是main student:", students); console.log("你好 这里是main lolName:", lolName); console.log("name", nameList[3]); ``` ### 初始化项目: 使用`npm init`初始化项目,在 package.json 文件配置`"type": "module"`
安装: ```json "type": "module", "dependencies": { "@babel/core": "^7.18.6", "@babel/preset-env": "^7.18.6", "babel-preset-env": "^1.7.0" } ``` ### 1、获取文件内容、解析依赖关系: 使用 node 的 fs 模块读取文件内容,使用`@babel/core`这个库解析依赖关系。
建立一个函数叫 createAsset();传入参数 filePath 文件路径。 - fs.readFileSync 读取文件内容 - babel.parse 解析文件成 ast,以 module 形式。 - traverse 遍历 ast 找到 引入模块,把所有引入装到数组中。 ```javascript import fs from "fs"; import babel from "@babel/core"; import path from "path"; function createAsset(filePath) { // 获取文件内容 // 解析依赖关系 let codeContent = fs.readFileSync(filePath, { encoding: "utf-8" }); // console.log(filePath + "文件内容:\n", codeContent); const astCode = babel.parse(codeContent, { sourceType: "module", }); const deps = []; // 存放所有依赖数组 babel.traverse(astCode, { ImportDeclaration({ node }) { console.log("node.source.value:", node.source.value); deps.push(node.source.value); }, }); const name = filePath.split("\\").pop(); return { filePath, name, code, deps, }; } ``` ### 2、把 es6 的导入方式编程 cmj 的 require 导入 转换成 ast 后,使用`transformFromAstSync` 可以边 ast 编程代码,并配置自己想要的环境。
这么做的目的是为了后续自己重写 require 依赖。 ```javascript const { code } = babel.transformFromAstSync(astCode, null, { presets: ["@babel/preset-env"], }); ``` ### 3、创建依赖图 根据一个入口文件,递归解析出它所有的依赖文件,返回一个数组。 ```javascript // 创建依赖图 function createGraph(entryPath) { console.log("entryPath", entryPath); const mainAsset = createAsset(entryPath); const queue = [mainAsset]; // 使用一个数组 和 for of 的方式找到所有依赖。 for (let asset of queue) { // 遍历 asset 的deps 去找到新的文件 asset.deps.forEach((pathItem) => { const childAsset = createAsset(path.resolve(asset.filePath, "..", pathItem)); if (childAsset) queue.push(childAsset); }); } return queue; } ``` ### 3、输出文件的模板 我们已经找到了所有文件,以及他们的依赖。这时候想一想我们怎么怎么输出一个文件,里面包含这么多文件内容,还需要让每个文件都有一个独立的运行环境,不能有变量污染。 - **让每个文件都有一个独立的运行环境:** 我们知道函数里的变量是隔离的,所以我们可以把拿到的文件内容都放到一个函数里面。这样就实现了文件间的变量隔离。
也许你会问,函数可以访问全局变量啊!这个也很简单,我们全局不建立变量,就不会污染了。 所以,思路是 建立一个立即执行函数,把各个文件的内容放到函数里,并作为参数传给立即执行函数。 那么问题又来了,每个文件里有 引入啊!引入别的文件了,怎么办,没事。我们刚才已经把引入替换成了 require,所以我们自己构建一个 require 函数,他的目的是执行一个文件拿到这个文件里`module.exports`的内容。
立即执行函数内容如下: ```javascript (function (totalModules) { // require 函数的思路就是 闭包写一个module,运行require后,返回这个module里 exports 的值 function require(filePath) { // totalModules 是一个全局的map 里面key是文件路径,value是 这个文件内容组成的一个函数。 const fn = totalModules[filePath]; const module = { exports: {}, }; // 找到模块 fn(require, module, module.exports); return module.exports; } require("./main.js"); // 运行一下入口文件 })({ "./main.js": function (require, module, exports) { const nameList = require("./name.js"); console.log("你好 这里是mainjs"); console.log("name", nameList[2]); }, "./name.js": function (require, module, exports) { console.log("namejs:"); const nameList = ["zzz", "clj", "zyc", "zx"]; module.exports = nameList; }, }); ``` 上述文件 把 main.js 作为入口文件,运行后 可以正确获取到 name.js 的内容并打印。
**注意点:**每个装文件内容的函数都需要传入我们写的`require, module, exports`这三个参数。通过闭包的形式把 module 传给相应函数,这样函数运行完,我们也拿到了该文件要输出的 module。 ### 4、改造模板,同名函数怎么办 上述模板 如果遇到同名函数,会读取错误的文件。所以为了解决这个问题,我们可以给每个文件打包后的内容,对应一个唯一的 id。也就是 key 为 id,内容写一个数组,第一项是函数,第二项是这个文件自己的依赖对象。 ```javascript (function (totalModules) { // require 函数的思路就是 闭包写一个module,运行require后,返回这个module里 exports 的值 function require(id) { if (!totalModules[id]) return console.error("未找到这个id对应的module:", id); // totalModules 是一个全局的map 里面key是文件路径,value是 这个文件内容组成的一个函数。 const [fn, mapping] = totalModules[id]; const module = { exports: {}, }; // 把文件中使用的require 替换成这个的localRequire,目的是在自己的mapping里找到对应 module id function localRequire(filePath) { return require(mapping[filePath]); } // 找到模块 fn(localRequire, module, module.exports); return module.exports; } require(1); // 运行一下入口文件 })({ 1: [ function (require, module, exports) { const nameList = require("./name.js"); console.log("你好 这里是mainjs"); console.log("name", nameList[2]); }, { "./name.js": 2, }, ], 2: [ function (require, module, exports) { console.log("namejs:"); const nameList = ["zzz", "clj", "zyc", "zx"]; module.exports = nameList; }, {}, ], }); ``` 同时我们需要改造一下`createAsset` 和 `createGraph`这两个函数内容。 ```javascript let id = 99; // 建立全局遍历 id function createAsset(filePath) { /* .... */ return { filePath, name, code, deps, mapping: {}, id: id++, // 每生成一个模块就id++ }; } // 创建依赖图 function createGraph(entryPath) { /* .... */ for (let asset of queue) { asset.deps.forEach((pathItem) => { /* .... */ asset.mapping[pathItem] = childAsset.id; // 每个文件都有自己独立的mapping依赖 if (childAsset) queue.push(childAsset); }); } return queue; } ``` ### 5、把依赖图结合要输出的 bundle 模板 刚才我们介绍的 bundle 模板都是自己写好的内容,那怎么把我们解析好的依赖图打进去呢!
我们使用 es6 的模板字符串来构建 ejs 引擎: ![image.png](https://cdn.nlark.com/yuque/0/2022/png/12830161/1656954402209-9199073e-f8d8-4705-a02b-13918a555017.png#clientId=u0d998902-b645-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=313&id=u7966544b&margin=%5Bobject%20Object%5D&name=image.png&originHeight=313&originWidth=680&originalType=binary&ratio=1&rotation=0&showTitle=false&size=113607&status=done&style=none&taskId=u76640852-bc34-40a6-8b49-7fae3589b33&title=&width=680) 打包输出代码: ```javascript function myWebpack(webpackConfig) { $webpackConfig = webpackConfig; if (!fs.existsSync(webpackConfig.entry)) { return console.log("======= 入口文件不存在 ======="); } const entryPath = path.resolve(webpackConfig.entry); const graph = createGraph(entryPath); // console.log("graph:", graph); // 打包输出 console.log("__dirname", __dirname); const bundleTempalte = fs.readFileSync("./webpack/bundleTem.js"); if (graph.length) { // 通过node的沙箱环境和es6的模板字符串构建模板引擎 const bundleCode = vm.runInNewContext(bundleTempalte, { data: graph, arrToObjText: function (arr) { let resText = "{\n"; arr.forEach((oneModule) => { resText += `${oneModule.id} : [ function (require, module, exports) { ${oneModule.code}; }, ${JSON.stringify(oneModule.mapping)} ]`; resText += ",\n"; }); resText += "\n}"; return resText; }, entryId: graph[0].id, }); // console.log("bundle code:", bundleCode); if (!fs.existsSync(path.resolve(webpackConfig.output, ".."))) { return console.log("======= 输入路径不存在 ======="); } fs.writeFileSync(webpackConfig.output, bundleCode); } else { console.log("打包失败"); } } ``` 上面引入的`./webpack/bundleTem.js`文件如下: ```javascript `(function (totalModules) { // require 函数的思路就是 闭包写一个module,运行require后,返回这个module里 exports 的值 function require(id) { if (!totalModules[id]) return console.error("未找到这个id对应的module:", id); // totalModules 是一个全局的map 里面key是文件路径,value是 这个文件内容组成的一个函数。 const [fn, mapping] = totalModules[id]; const module = { exports: {}, }; // 把文件中使用的require 替换成这个的localRequire,目的是在自己的mapping里找到对应 module id function localRequire(filePath) { return require(mapping[filePath]); } // 找到模块 fn(localRequire, module, module.exports); return module.exports; } require(${entryId}); // 运行一下入口文件 })( ${arrToObjText(data)} );`; ``` 至此完成 webpack 对 js 文件的打包工作。 webapck 对其它类型的打包需要依赖 loader 去实现。 ## 编写 loader 和插件等: ### 编写 loader: > **简单用法** > 当一个 loader 在资源中使用,这个 loader 只能传入一个参数 - **一个包含资源文件内容的字符串**。 > 同步 loader 可以 return 一个代表已转换模块(transformed module)的单一值。在更复杂的情况下,loader 也可以通过使用 this.callback(err, values...) 函数,返回任意数量的值。错误要么传递给这个 this.callback 函数,要么抛给(thrown in)同步 loader 。 > loader 会返回一个或者两个值。第一个值的类型是 JavaScript 代码的字符串或者 buffer。第二个可选值是 SourceMap,它是个 JavaScript 对象。 mini-webpack 完整源码,包含 loader:
注意:配置的 loader rule.use 需要是一个函数. ```javascript import fs from "fs"; import babel from "@babel/core"; import path from "path"; import vm from "node:vm"; const __dirname = path.resolve(); let $webpackConfig = null; let id = 99; function createAsset(filePath) { // 获取文件内容 // 解析依赖关系 let codeContent = fs.readFileSync(filePath, { encoding: "utf-8" }); // console.log(filePath + "文件内容:\n", codeContent); // loader的处理 if ($webpackConfig) { $webpackConfig.module.rules.forEach((rule) => { if (rule.test.test(filePath)) { if (Array.isArray(rule.use)) { rule.use.reverse().forEach((fn) => { codeContent = fn(codeContent); }); } else { codeContent = rule.use(codeContent); } } }); } const astCode = babel.parse(codeContent, { sourceType: "module", }); const deps = []; // 存放所有依赖数组 babel.traverse(astCode, { ImportDeclaration({ node }) { console.log("node.source.value:", node.source.value); deps.push(node.source.value); }, }); const { code } = babel.transformFromAstSync(astCode, null, { presets: ["@babel/preset-env"], }); // console.log(code); const name = filePath.split("\\").pop(); return { filePath, name, code, deps, mapping: {}, id: id++, }; } // 创建依赖图 function createGraph(entryPath) { console.log("entryPath", entryPath); const mainAsset = createAsset(entryPath); const queue = [mainAsset]; // const for (let asset of queue) { // 遍历 asset 的deps 去找到新的文件 asset.deps.forEach((pathItem) => { const childAsset = createAsset(path.resolve(asset.filePath, "..", pathItem)); // 把新文件创建的asset在放到数组中。这样for of 会去遍历新文件的依赖。 asset.mapping[pathItem] = childAsset.id; if (childAsset) queue.push(childAsset); }); } return queue; } function myWebpack(webpackConfig) { $webpackConfig = webpackConfig; if (!fs.existsSync(webpackConfig.entry)) { return console.log("======= 入口文件不存在 ======="); } const entryPath = path.resolve(webpackConfig.entry); const graph = createGraph(entryPath); // console.log("graph:", graph); // 打包输出 console.log("__dirname", __dirname); const bundleTempalte = fs.readFileSync("./webpack/bundleTem.js"); if (graph.length) { // 通过node的沙箱环境和es6的模板字符串构建模板引擎 const bundleCode = vm.runInNewContext(bundleTempalte, { data: graph, arrToObjText: function (arr) { let resText = "{\n"; arr.forEach((oneModule) => { resText += `${oneModule.id} : [ function (require, module, exports) { ${oneModule.code}; }, ${JSON.stringify(oneModule.mapping)} ]`; resText += ",\n"; }); resText += "\n}"; return resText; }, entryId: graph[0].id, }); // console.log("bundle code:", bundleCode); if (!fs.existsSync(path.resolve(webpackConfig.output, ".."))) { return console.log("======= 输入路径不存在 ======="); } fs.writeFileSync(webpackConfig.output, bundleCode); } else { console.log("打包失败"); } } export default myWebpack; ```