# 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 引擎:

打包输出代码:
```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;
```