往期精彩文章推荐
🔥🔥🔥微前端无界(wujie)源码浅析——子应用加载与js、css沙箱实现
🏠🏠🏠微前端我劝你千万别无脑冲qiankun
一个js库就把你的网页的底裤🩲都扒了——import-html-entry
🧶创建js沙箱都有哪些方式
两分钟快速了解css样式隔离方案有哪几种
这一次再也不怕webpack面试了【webpack配置、处理资源】
🏝️🏝️🏝️一个babel插件让项目中所有antd table实现拖拽控制列宽
github仓库地址: mini-webpack
Webpack 是一个功能强大的打包工具,可以将项目中的各个模块打包成一个浏览器可以执行的文件。虽然 Webpack 本身功能非常复杂,但它的核心原理相对简单。本文将带你用不到 100 行代码实现一个简单版的 Webpack,实现以下功能:
- 支持自定义 Plugin 插件体系
- 支持自定义 Loader 加载体系
- 支持 JavaScript 模块的依赖打包
- 在不同文件中引入相同模块时实现缓存
接下来逐步讲解每个功能的实现,并展示如何构建一个简单的打包工具。
1. Webpack 的基本架构
Webpack 的核心工作是解析项目中的模块及其依赖,然后将它们打包成一个文件。在这个过程中,它会应用各种插件(Plugin)和加载器(Loader)来处理不同类型的文件。我们先从项目的入口文件开始,读取它的内容,解析模块依赖,再递归处理这些依赖。
2. 构建基本的 Webpack 类
首先,我们定义一个基本的 Webpack 类,并在构造函数中接受一些配置项,如入口文件路径、输出路径、插件和加载器。以下是代码实现:
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');
class Webpack {
constructor(options) {
this.entry = options.entry; // 入口文件
this.output = options.output; // 输出配置
this.plugins = options.plugins || []; // 插件
this.loaders = options.module?.rules || []; // 加载器
this.cache = {}; // 模块缓存
// 初始化插件
this.initPlugins();
}
// 初始化插件
initPlugins() {
this.plugins.forEach(plugin => {
if (typeof plugin.apply === 'function') {
plugin.apply(this);
}
});
}
}
这段代码初始化了 Webpack 类,并在构造函数中接收配置项。initPlugins 方法用于初始化插件。
3. 解析文件内容
接下来,我们编写 readModule 方法来读取文件内容并解析它。这个方法会通过 Babel 将代码解析成 AST(抽象语法树),遍历 AST 找到所有的模块依赖,并转换代码以便打包。
readModule(filename) {
const relativeFilename = './' + path.relative(process.cwd(), filename);
if (this.cache[relativeFilename]) {
return this.cache[relativeFilename];
}
let content = fs.readFileSync(filename, 'utf-8');
// 应用 loaders
for (const loader of this.loaders) {
if (loader.test.test(filename)) {
const loaderFn = require(loader.use);
content = loaderFn(content);
}
}
const ast = parser.parse(content, {
sourceType: 'module',
});
const dependencies = [];
// 遍历 AST 获取模块依赖
traverse(ast, {
ImportDeclaration({ node }) {
dependencies.push(node.source.value);
},
});
// 使用 Babel 转换代码
const { code } = babel.transformFromAstSync(ast, null, {
presets: ['@babel/preset-env'],
});
const moduleInfo = {
filename: relativeFilename, // 使用相对路径作为 filename
dependencies,
code,
};
// 缓存模块
this.cache[relativeFilename] = moduleInfo;
return moduleInfo;
}
这里我们用 Babel 来解析代码的 AST,并找到所有的 import 声明,从而提取出模块依赖。同时,我们利用 Babel 将代码转换成兼容的 JavaScript 代码。
4. 构建依赖图
在拿到入口模块的信息后,我们还需要递归处理它的所有依赖模块。我们通过 buildDependencyGraph 方法构建依赖图。
buildDependencyGraph(entry) {
const entryPath = path.resolve(entry);
const entryModule = this.readModule(entryPath);
const graph = [entryModule];
for (const module of graph) {
module.mapping = {};
const dirname = path.dirname(module.filename);
module.dependencies.forEach((relativePath) => {
// 获取依赖模块的绝对路径
const absolutePath = path.resolve(path.dirname(entryPath), dirname, relativePath);
// 解析为相对于项目根目录的相对路径
const childModule = this.readModule(absolutePath);
const relativeChildPath = './' + path.relative(process.cwd(), absolutePath);
module.mapping[relativePath] = relativeChildPath;
graph.push(childModule);
});
}
return graph;
}
buildDependencyGraph 方法从入口文件开始,递归读取所有依赖模块并构建一个包含所有模块的依赖图。
5. 实现模块打包和缓存
有了依赖图后,我们就可以将所有模块打包到一个文件中。我们通过 bundle 方法将模块包装在一个立即执行函数中,并实现一个简单的缓存机制。
bundle() {
const graph = this.buildDependencyGraph(this.entry);
const modules = graph.map(module => {
return `
'${module.filename}': function (require, module, exports) {
${module.code}
}
`;
}).join(',');
// 生成最终的打包文件
const result = `
(function(modules) {
const cache = {};
function require(filename) {
if (cache[filename]) {
return cache[filename].exports;
}
const module = cache[filename] = {
exports: {}
};
modules[filename](require, module, module.exports);
return module.exports;
}
require('./${path.relative(process.cwd(), this.entry)}');
})({${modules}});
`;
// 输出到指定的文件
fs.writeFileSync(path.resolve(this.output.path), result, 'utf-8');
}
在这里,我们用一个立即执行函数将所有模块封装起来,并为每个模块提供 require 函数。require 函数会检查模块是否已经加载,如果是,则直接从缓存中返回,否则执行模块代码。
6. 运行打包
最后,我们通过 run 方法来启动整个打包过程:
run() {
this.bundle();
}
这段代码会触发 bundle 方法,生成最终的打包文件。
7. 测试我们的 Webpack
假设有以下项目结构:
/src
|-- index.js
|-- base.js
index.js 文件内容如下:
import base from './base.js';
console.log(base, 'index.js');
base.js 文件内容如下:
export default 'base';
通过配置我们的 Webpack, webpack.config.js中内容如下:
const path = require('path');
const ExamplePlugin = require('./ExamplePlugin.js');
module.exports = {
entry: './index.js', // 入口文件
output: {
path: path.resolve(__dirname, '../dist/bundle.js'), // 输出文件路径
},
plugins: [
new ExamplePlugin(),
],
module: {
rules: [
],
},
};
run.js中内容:
const Webpack = require('./mini-webpack.js');
const config = require('./webpack.config.js');
const compiler = new Webpack(config);
compiler.run();
运行run.js
node run.js
运行后会在 dist 目录下生成一个 bundle.js 文件,里面包含打包后的代码。
8、打包产物
打包产物如下:
(function (modules) {
const cache = {};
function require(filename) {
if (cache[filename]) {
return cache[filename].exports;
}
const module = (cache[filename] = {
exports: {},
});
modules[filename](require, module, module.exports);
return module.exports;
}
require('./index.js');
})({
'./index.js': function (require, module, exports) {
"use strict";
var _base = _interopRequireDefault(require("./base.js"));
function _interopRequireDefault(e) {
return e && e.__esModule ? e : { default: e };
}
console.log(_base.default, 'index.js');
},
'./base.js': function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = void 0;
var base = 'base';
var _default = (exports.default = base);
},
});
上面产物代码可以直接复制在浏览器控制台中运行