前言
掘金上有太多非常好的写webpack的文章了,就不班门弄斧了,这里快速聊一下,webpack是怎么一回事。
文前求赞,写得很辛苦,如果对您有帮助,麻烦点赞、收藏、评论三连。谢谢了!🥲🥲🥲
webpack 是啥?
webpack就是用来搭建前端工程的,它运行在node环境中,它所做的事情,简单来说,就是打包。
具体来说,就是以某个模块作为入口,根据入口分析出所有模块的依赖关系,然后对各种模块进行合并、压缩,形成最终的打包结果。
webpack 解决了什么问题呢?为什么要用它?
现代前端开发已经变得十分的复杂,所以我们开发过程中会遇到如下的问题:
- 需要通过模块化的方式来开发
- 使用一些高级的特性来加快我们的开发效率或者安全性,比如通过ES6+、TypeScript开发脚本逻辑,通过sass、less等方式来编写css样式代码
- 监听文件的变化来并且反映到浏览器上,提高开发的效率
- JavaScript 代码需要模块化,HTML 和 CSS 这些资源文件也会面临需要被模块化的问题
- 开发完成后我们还需要将代码进行压缩、合并以及其他相关的优化
而webpack恰巧可以解决以上问题,也就是提供:模块打包、语法兼容性,开发支持,性能优化等核心功能。
怎么用webpack?
核心概念如下:
- Entry:编译入口,webpack编译的起点
- Compiler:编译管理器,
webpack启动后会创建compiler对象,该对象一直存活直到结束退出 - Compilation:单次编辑过程的管理器,比如 watch = true 时,运行过程中只有一个
compiler但每次文件变更触发重新编译时,都会创建一个新的compilation对象 - Dependence:依赖对象,
webpack基于该类型记录模块间依赖关系 - Module:
webpack内部所有资源都会以“module”对象形式存在,所有关于资源的操作、转译、合并都是以 “module” 为基本单位进行的 - Chunk:编译完成准备输出时,
webpack会将module按特定的规则组织成一个一个的chunk,这些chunk某种程度上跟最终输出一一对应 - Loader:资源内容转换器,其实就是实现从内容A转换B的转换器
- Plugin:webpack构建过程中,会在特定的时机广播对应的事件,插件监听这些事件,在特定时间点介入编译过程
entry point(入口起点)
入口起点(entry point) 指示 webpack 应该使用哪个模块,来作为构建其内部 依赖图(dependency graph)的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
module.exports = { entry: "./src/index.js"};
output
output 属性告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。主要输出文件的默认值是 ./dist/main.js,其他生成文件默认放置在 ./dist 文件夹中。
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require("path");
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.bundle.js',
},
}
Loader加载器
loader 用于对模块的源代码进行转换。loader 可以使你在 import 或"load(加载)"模块时预处理文件。默认只能处理json与js,其他文件需要通过专⻔的加载器处理。
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
}
loader是链式传递的,对文件资源从上一个loader传递到下一个,而loader的处理也遵循着从下到上的顺序,我们简单了解一下loader的开发原则:
- 单一原则:每个
Loader只做一件事,简单易用,便于维护; - 链式调用:
Webpack会按顺序链式调用每个Loader; - 统一原则:遵循
Webpack制定的设计规则和结构,输入与输出均为字符串,各个Loader完全独立,即插即用; - 无状态原则:在转换不同模块时,不应该在loader中保留状态;
plugin插件
扩展插件,在 webpack 构建过程的特定时机注入扩展逻辑,用来改变或优化构建结果;
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
}
实现一个plugin的基本框架:
const fs = require('fs')
var http = require('http');
class UploadSourceMapWebpackPlugin {
constructor(options) {
this.options =options
}
apply(compiler) {
//打包结束后执行
compiler.hooks.done.tap("upload-sourcemap-plugin", status=> {
console.log('webpack runing')
});
}
}
module.exports = UploadSourceMapWebpackPlugin;
配置示例
来看一个基础配置示例,适用于开发和生产环境。它整合了代码分割、缓存优化、Tree Shaking 等性能提升技术:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 自动生成 HTML 文件
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // 抽离 CSS
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); // 压缩 CSS
const TerserPlugin = require('terser-webpack-plugin'); // 压缩 JS
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); // 清理旧的构建文件
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
// 入口文件
entry: './src/index.js',
// 输出文件
output: {
path: path.resolve(__dirname, 'dist'),
filename: isProduction ? 'js/[name].[contenthash:8].js' : 'js/[name].js',
assetModuleFilename: 'assets/[hash][ext][query]', // 资源文件的输出路径
clean: true, // 自动清理旧文件(Webpack 5 内置)
},
// 模式配置
mode: isProduction ? 'production' : 'development',
// 开发服务器
devServer: {
static: path.join(__dirname, 'dist'),
compress: true,
port: 3000,
open: true,
hot: true, // 启用 HMR 热更新
},
// 模块解析规则
module: {
rules: [
{
test: /\.js$/, // 处理 JS 文件
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'], // 转换为兼容性较好的 JS 代码
},
},
},
{
test: /\.css$/, // 处理 CSS 文件
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
'css-loader',
'postcss-loader', // 自动加前缀
],
},
{
test: /\.(png|jpe?g|gif|svg)$/i, // 处理图片资源
type: 'asset', // 根据资源大小选择输出方式
parser: {
dataUrlCondition: {
maxSize: 8 * 1024, // 小于 8KB 的文件转换为 Base64
},
},
},
{
test: /\.(woff2?|eot|ttf|otf)$/i, // 处理字体资源
type: 'asset/resource',
},
],
},
// 插件配置
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html', // HTML 模板文件
minify: isProduction
? {
collapseWhitespace: true, // 移除空格
removeComments: true, // 移除注释
removeRedundantAttributes: true, // 移除多余的属性
useShortDoctype: true, // 使用短文档类型
}
: false,
}),
new CleanWebpackPlugin(), // 清理构建目录
...(isProduction
? [new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash:8].css' })]
: []),
],
// 性能优化
optimization: {
minimize: isProduction, // 仅在生产环境启用压缩
minimizer: [
new TerserPlugin({
parallel: true, // 多线程压缩
terserOptions: {
compress: {
drop_console: true, // 移除 console
},
},
}),
new CssMinimizerPlugin(),
],
splitChunks: {
chunks: 'all', // 提取公共模块
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
runtimeChunk: 'single', // 提取运行时代码
},
// 路径解析
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'), // 配置路径别名
},
extensions: ['.js', '.json', '.css'], // 自动解析的文件扩展名
},
// 开启 Source Map(开发环境)
devtool: isProduction ? false : 'source-map',
};
};
配置详解
-
基础功能
- 入口文件:
src/index.js - 输出目录:
dist,包含哈希文件名以支持缓存。
- 入口文件:
-
开发环境优化
- 启用 HMR 热更新。
- 使用
source-map提供源码调试支持。
-
生产环境优化
- CSS 抽离与压缩: 使用
MiniCssExtractPlugin和CssMinimizerPlugin处理样式文件。 - JS 压缩: 使用
TerserPlugin压缩并移除多余代码(如console.log)。 - 代码分割: 使用
splitChunks提取公共模块,优化加载速度。 - 缓存优化: 文件名带有
contenthash,方便浏览器缓存。
- CSS 抽离与压缩: 使用
-
资源处理
- 小于 8KB 的图片转换为 Base64,减少 HTTP 请求。
- 字体文件作为独立资源输出。
-
插件功能
- 自动生成 HTML 文件。
- 清理旧的构建文件。
- 支持路径别名,简化文件导入。
优化效果
- 开发环境: 提升启动速度,支持实时更新,增强调试体验。
- 生产环境: 减小打包文件体积,优化加载性能,确保浏览器缓存利用率最大化。
- 通用支持: 灵活处理各种资源文件,适应多样化项目需求。
webpack的构建流程
webpack 的运行流程是一个串行的过程,它的工作流程就是将各个插件串联起来
在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条webpack机制中,去改变webpack的运作,使得整个系统扩展性良好
从启动到结束会依次执行以下三大步骤:
- 初始化流程:从配置文件和
Shell语句中读取与合并参数,并初始化需要使用的插件和配置插件等执行环境所需要的参数 - 编译构建流程:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理
- 输出流程:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统
初始化流程
从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
配置文件默认下为webpack.config.js,也或者通过命令的形式指定配置文件,主要作用是用于激活webpack的加载项和插件
关于文件配置内容分析,如下注释:
var node_modules = path.resolve(__dirname, 'node_modules');
var pathToReact = path.resolve(node_modules, 'react/dist/react.min.js');
module.exports = {
// 入口文件,是模块构建的起点,同时每一个入口文件对应最后生成的一个 chunk。
entry: './path/to/my/entry/file.js',
// 文件路径指向(可加快打包过程)。
resolve: {
alias: {
'react': pathToReact
}
},
// 生成文件,是模块构建的终点,包括输出文件与输出路径。
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].js'
},
// 这里配置了处理各模块的 loader ,包括 css 预处理 loader ,es6 编译 loader,图片处理 loader。
module: {
loaders: [
{
test: /\.js$/,
loader: 'babel',
query: {
presets: ['es2015', 'react']
}
}
],
noParse: [pathToReact]
},
// webpack 各插件对象,在 webpack 的事件流中执行对应的方法。
plugins: [
new webpack.HotModuleReplacementPlugin()
]
};
webpack 将 webpack.config.js 中的各个配置项拷贝到 options 对象中,并加载用户配置的 plugins
完成上述步骤之后,则开始初始化Compiler编译对象,该对象掌控者webpack声明周期,不执行具体的任务,只是进行一些调度工作
class Compiler extends Tapable {
constructor(context) {
super();
this.hooks = {
beforeCompile: new AsyncSeriesHook(["params"]),
compile: new SyncHook(["params"]),
afterCompile: new AsyncSeriesHook(["compilation"]),
make: new AsyncParallelHook(["compilation"]),
entryOption: new SyncBailHook(["context", "entry"])
// 定义了很多不同类型的钩子
};
// ...
}
}
function webpack(options) {
var compiler = new Compiler();
...// 检查options,若watch字段为true,则开启watch线程
return compiler;
}
...
Compiler 对象继承自 Tapable,初始化时定义了很多钩子函数
编译构建流程
根据配置中的 entry 找出所有的入口文件
module.exports = {
entry: './src/file.js'
}
初始化完成后会调用Compiler的run来真正启动webpack编译构建流程,主要流程如下:
compile开始编译make从入口点分析模块及其依赖的模块,创建这些模块对象build-module构建模块seal封装构建结果emit把各个chunk输出到结果文件
compile 编译
执行了run方法后,首先会触发compile,主要是构建一个Compilation对象
该对象是编译阶段的主要执行者,主要会依次下述流程:执行模块创建、依赖收集、分块、打包等主要任务的对象
make 编译模块
当完成了上述的compilation对象后,就开始从Entry入口文件开始读取,主要执行_addModuleChain()函数,如下:
_addModuleChain(context, dependency, onModule, callback) {
...
// 根据依赖查找对应的工厂函数
const Dep = /** @type {DepConstructor} */ (dependency.constructor);
const moduleFactory = this.dependencyFactories.get(Dep);
// 调用工厂函数NormalModuleFactory的create来生成一个空的NormalModule对象
moduleFactory.create({
dependencies: [dependency]
...
}, (err, module) => {
...
const afterBuild = () => {
this.processModuleDependencies(module, err => {
if (err) return callback(err);
callback(null, module);
});
};
this.buildModule(module, false, null, null, err => {
...
afterBuild();
})
})
}
过程如下:
_addModuleChain中接收参数dependency传入的入口依赖,使用对应的工厂函数NormalModuleFactory.create方法生成一个空的module对象
回调中会把此module存入compilation.modules对象和dependencies.module对象中,由于是入口文件,也会存入compilation.entries中
随后执行buildModule进入真正的构建模块module内容的过程
build module 完成模块编译
这里主要调用配置的loaders,将我们的模块转成标准的JS模块
在用 Loader 对一个模块转换完后,使用 acorn 解析转换后的内容,输出对应的抽象语法树(AST),以方便 Webpack 后面对代码的分析
从配置的入口模块开始,分析其 AST,当遇到require等导入其它模块语句时,便将其加入到依赖的模块列表,同时对新找出的依赖模块递归分析,最终搞清所有模块的依赖关系
输出流程
seal 输出资源
seal方法主要是要生成chunks,对chunks进行一系列的优化操作,并生成要输出的代码
webpack 中的 chunk ,可以理解为配置在 entry 中的模块,或者是动态引入的模块
根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表
emit 输出完成
在确定好输出内容后,根据配置确定输出的路径和文件名
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].js'
}
在 Compiler 开始生成文件前,钩子 emit 会被执行,这是我们修改最终文件的最后一个机会
从而webpack整个打包过程则结束了
webpack 的核心原理是啥? 如何手写一个mini-webpack?
我们直接来实现一个最简化版的webpack,帮助我们理解webpack的核心原理。
1.解析文件,生成AST
假设我们有如下目录:
-src
- add.js
- minus.js
- index.js
-index.html
文件内容如下,在 index.js 中引入 add.js 和 minus.js ,再在 index.html 中引入 index.js
//add.js
export default (a,b)=>{return a+b;}
//minus.js
export const minus = (a,b)=>{return a-b }
//index.js
import add from "./add.js";
import {minus} from "./minus.js";
const sum =add(1,2);
const division = minus(2,1);
console.log(sum);
console.log(division);
//html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script src="./src/index.js"></script>
</body>
</html>
我们来写一个简易版webpack把上面的资源进行打包
//获取主入口文件
const fs = require('fs');
const getModuleInfo = (file)=>{
const body = fs.readFileSync(file,'utf-8');
console.log(body);
}
getModuleInfo("./src/index.js");
打印内容如下,就是index.js的内容
继续拓展,引入@babel/parse
npm install @babel/parse
const fs = require('fs')
const parser = require('@babel/parser')
const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8')
const ast = parser.parse(body, {
sourceType: 'module' //表示解析es模块
})
console.log(ast)
}
getModuleInfo('./src/index.js')
我们再来看打印结果如下,这个时候babel/parser就把我们的资源解析成一个ast语法树了。
这棵 AST 是对
./src/index.js 的完整语法表示,File 是根节点,Program 包含程序主体,body 包括各个语法结构的细节。AST 的作用是便于工具对代码进行分析和转换(如编译、代码检查、代码优化等)。
2. 收集依赖
完成语法树的解析后我们再来引入一个东西:@babel/traverse
npm install @babel/traverse
const fs = require('fs');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const path = require('path');
const babel = require('@babel/core');
const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8'); // 读取文件内容
const ast = parser.parse(body, { sourceType: 'module' }); // 解析为 AST
const deps = {};
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
const abspath = './' + path.resolve(dirname, node.source.value); // 修正路径拼接问题
deps[node.source.value] = abspath;
}
});
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
});
const moduleInfo = { file, deps, code };
console.log(moduleInfo); // 打印模块信息
return moduleInfo;
}
getModuleInfo('./src/index.js');
解读一下以上代码:
引入必要模块
const fs = require('fs');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const path = require('path');
fs:用于文件读取。@babel/parser:解析代码为 AST(抽象语法树)。@babel/traverse:用于遍历和操作 AST。path:处理文件路径。
函数 getModuleInfo
定义一个函数来解析文件的模块信息,包括依赖关系和编译后的代码:
const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8'); // 读取文件内容
const ast = parser.parse(body, { sourceType: 'module' }); // 解析为 AST
file:文件路径。body:读取的文件内容。ast:用 Babel 将文件内容解析为抽象语法树。
提取依赖
const deps = {};
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
const abspath = './' + path.join(dirname, node.source.value);
deps[node.source.value] = abspath;
}
});
deps:一个对象,存储模块依赖的映射关系。traverse:遍历 AST 中的ImportDeclaration节点(即import语句)。node.source.value:模块导入的路径,例如./module.js。dirname:当前文件所在目录。abspath:将模块路径解析为绝对路径。
结果:deps 对象以模块路径为键、绝对路径为值,形如:
{
'./module.js': './src/module.js'
}
代码转换
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
});
- 使用 Babel 将 AST 转换为 ES5 代码。
presets:指定转换规则,这里使用@babel/preset-env将现代 JavaScript 转换为较低版本的兼容代码。
注意:这里的 babel 没有定义,需要通过 @babel/core 模块引入。
构建模块信息
const moduleInfo = { file, deps, code };
return moduleInfo;
-
moduleInfo:一个对象,包含:file:文件路径。deps:模块依赖映射。code:转换后的代码。
调用函数
getModuleInfo('./src/index.js');
- 对指定文件
./src/index.js调用getModuleInfo,解析其依赖和代码。
输出示例
假设 ./src/index.js 内容为:
import fs from 'fs';
import path from 'path';
运行后输出:
{
file: './src/index.js',
deps: {
'fs': './fs',
'path': './path'
},
code: '"use strict";\n\nvar _fs = _interopRequireDefault(require("fs"));\n\nvar _path = _interopRequireDefault(require("path"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }'
}
3. 加载所有文件
我们使用 parseModules("./src/index.js") 来代替 getModuleInfo 作为入口函数调用,整体代码如下:
const fs = require('fs');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const path = require('path');
const babel = require('@babel/core');
const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8'); // 读取文件内容
const ast = parser.parse(body, { sourceType: 'module' }); // 解析为 AST
const deps = {};
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
const abspath = './' + path.resolve(dirname, node.source.value); // 修正路径拼接问题
deps[node.source.value] = abspath;
}
});
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
});
const moduleInfo = { file, deps, code };
console.log(moduleInfo); // 打印模块信息
return moduleInfo;
}
//新增的代码
const parseModules = file => {
const entry = getModuleInfo(file) //在这里调用getModuleInfo
const temp = [entry]
for (let i = 0; i < temp.length; i++) {
const deps = temp[i].deps
if (deps) {
for (const key in deps) {
if (deps.hasOwnProperty(key)) {
temp.push(getModuleInfo(deps[key]))
}
}
}
console.log(temp)
const depsGraph = {}
temp.forEach(moduleInfo => {
depsGraph[moduleInfo.file] = {
deps: moduleInfo.deps,
code: moduleInfo.code,
}
})
console.log(depsGraph)
return depsGraph
}
}
parseModules("./src/index.js")
我们来逐行解析一下这个新增的函数parseModules, 这段代码的主要功能是:
- 解析指定入口文件及其所有依赖的模块信息。
- 构建一个依赖图对象(
depsGraph),其中每个模块的信息包含依赖关系和代码。
函数定义与入口模块解析
const parseModules = file => {
const entry = getModuleInfo(file); // 调用 getModuleInfo 解析入口模块信息
const temp = [entry]; // 初始化数组存储模块信息,从入口模块开始
file:入口文件路径。getModuleInfo(file):之前定义的函数,用于解析模块信息,返回一个包含文件路径、依赖关系和代码的对象。temp:数组,用于存储所有模块的解析结果,初始只包含入口模块。
遍历并解析模块依赖
for (let i = 0; i < temp.length; i++) {
const deps = temp[i].deps; // 当前模块的依赖关系对象
if (deps) {
for (const key in deps) { // 遍历模块的依赖
if (deps.hasOwnProperty(key)) { // 确保是对象自身的属性
temp.push(getModuleInfo(deps[key])); // 调用 getModuleInfo 解析依赖模块,并加入 temp 数组
}
}
}
-
遍历依赖:循环处理
temp数组中的每个模块,获取其依赖信息(deps)。 -
deps:一个对象,存储当前模块的依赖路径(键)及其绝对路径(值)。 -
对每个依赖:
- 使用
getModuleInfo解析依赖模块信息。 - 将解析后的模块信息添加到
temp数组。
- 使用
-
动态增长:
temp的长度会随着解析新增模块而增加,确保所有递归依赖模块都被解析。
构建依赖图
console.log(temp); // 打印当前所有已解析的模块信息
const depsGraph = {};
temp.forEach(moduleInfo => {
depsGraph[moduleInfo.file] = {
deps: moduleInfo.deps, // 模块的依赖关系
code: moduleInfo.code, // 模块转换后的代码
};
});
console.log(depsGraph); // 打印最终的依赖图
return depsGraph; // 返回依赖图
-
depsGraph:一个对象,表示依赖图,键是文件路径,值是一个对象,包含模块的依赖和代码。 -
temp.forEach:遍历temp数组,逐个处理模块信息,将其转换为依赖图的节点。 -
每个模块节点结构:
{ "模块文件路径": { deps: { "依赖路径": "依赖绝对路径" }, code: "转换后的代码" } } -
返回值:最终返回
depsGraph对象,用于表示整个依赖关系和代码内容。
调用函数
parseModules("./src/index.js");
- 调用
parseModules,以./src/index.js作为入口文件。 - 函数将解析入口文件及其递归依赖,并构建依赖图。
输出解析
假设入口文件和依赖如下:
index.js:
import './a.js';
import './b.js';
a.js:
import './c.js';
b.js:
(无依赖)
c.js:
(无依赖)
解析过程:
- 入口文件
index.js被解析,添加到temp。 - 解析
index.js的依赖a.js和b.js,将它们添加到temp。 - 解析
a.js的依赖c.js,将其添加到temp。 b.js和c.js无依赖,结束解析。
最终输出的 depsGraph:
{
'./src/index.js': {
deps: { './a.js': './src/a.js', './b.js': './src/b.js' },
code: '转换后的 index.js 代码'
},
'./src/a.js': {
deps: { './c.js': './src/c.js' },
code: '转换后的 a.js 代码'
},
'./src/b.js': {
deps: {},
code: '转换后的 b.js 代码'
},
'./src/c.js': {
deps: {},
code: '转换后的 c.js 代码'
}
}
4. 构建最终的资源,补充require和exports
我们拿到了资源图谱之后,我们要去构建最终的资源 我们引入一个函数bundle,使用这个函数来做新的入口函数,然后继续包住上面的parseModules 构建最终的资源
const bundle = file => {
const depsGraph = JSON.stringify(parseModules(file))
return `(
function (graph) {
function require(file) {
function absRequire(relPath) {
return require(graph[file].deps[relPath])
}
var exports = {}
(function (require,exports,code) {
eval(code)
})(absRequire,exports,graph[file].code)
return exports
}
require('${file}')
})(${depsGraph})`
}
const content = bundle('./src/index.js')
console.log(content);
//写入到我们的dist目录下
fs.mkdirSync('./dist');
fs.writeFileSync('./dist/bundle.js',content)
完整解析一下,这段代码的主要功能是:
- 通过调用
parseModules(file)解析入口文件及其依赖,生成依赖图。 - 将依赖图和自定义的模块加载器打包成一个自执行函数。
- 打包后的代码会写入到项目的
./dist/bundle.js文件中,用于浏览器或其他运行环境。
生成依赖图
const depsGraph = JSON.stringify(parseModules(file));
parseModules(file):调用之前定义的函数,解析入口文件及所有依赖模块,返回模块依赖图(depsGraph)。JSON.stringify(depsGraph):将depsGraph转换为字符串格式,以便嵌入到打包代码中。
假设依赖图结构(示例):
{
"./src/index.js": {
deps: { "./a.js": "./src/a.js" },
code: "console.log('index'); require('./a.js');"
},
"./src/a.js": {
deps: {},
code: "console.log('a');"
}
}
定义自执行函数
最终的bundle里面的内容就是如下格式
return `(
function (graph) {
...
}
)(${depsGraph})`;
- 自执行函数(IIFE) :用来运行打包后的代码。
graph:作为参数传入的模块依赖图(depsGraph)。- 这个函数的逻辑主要用于加载和执行模块代码。
模拟模块加载
function require(file) {
function absRequire(relPath) {
return require(graph[file].deps[relPath]);
}
var exports = {};
(function (require, exports, code) {
eval(code);
})(absRequire, exports, graph[file].code);
return exports;
}
模块加载的核心逻辑:
-
require(file):接收一个模块路径,加载并执行模块代码。 -
absRequire(relPath):- 用于加载相对路径的依赖模块。
- 根据依赖图中当前模块的
deps属性,找到依赖模块的路径,然后递归调用require。
-
exports对象:- 用于存储模块的导出内容。
- 执行模块代码时,将
exports作为上下文传递,模块可以通过修改它导出内容。
-
执行模块代码:
- 使用
(function(require, exports, code) { eval(code); })执行模块代码。 - 模拟 CommonJS 的模块系统,传递
require和exports。
- 使用
执行入口模块
require('${file}');
- 使用入口文件路径作为参数,启动模块加载过程。
- 从入口模块开始,递归加载并执行所有依赖模块。
生成打包代码并输出
console.log(content);
// 写入到 dist 目录下
fs.mkdirSync('./dist');
fs.writeFileSync('./dist/bundle.js', content);
步骤说明:
-
fs.mkdirSync('./dist'):- 创建一个名为
dist的目录,用于存储打包后的文件。 - 如果目录已经存在,会抛出错误;可以用
fs.mkdirSync('./dist', { recursive: true })来避免错误。
- 创建一个名为
-
fs.writeFileSync('./dist/bundle.js', content):- 将打包代码写入到
./dist/bundle.js文件中。 - 如果文件已存在,会覆盖原内容。
- 将打包代码写入到
打包代码示例
假设入口文件和依赖如下:
-
index.js:console.log('index'); require('./a.js'); -
a.js:javascript 复制代码 console.log('a');
生成的打包代码内容:
(
function (graph) {
function require(file) {
function absRequire(relPath) {
return require(graph[file].deps[relPath]);
}
var exports = {};
(function (require, exports, code) {
eval(code);
})(absRequire, exports, graph[file].code);
return exports;
}
require('./src/index.js');
}
)({
"./src/index.js": {
deps: { "./a.js": "./src/a.js" },
code: "console.log('index'); require('./a.js');"
},
"./src/a.js": {
deps: {},
code: "console.log('a');"
}
});
运行结果:
- 调用
require('./src/index.js'),打印index。 - 加载依赖模块
a.js,打印a。
总结
主要有以下功能
- 依赖图生成:通过
parseModules构建模块依赖关系。 - 模块加载器:实现了一个自定义的
require函数,支持递归加载执行。 - 打包输出:将模块加载逻辑和依赖图写入到
dist/bundle.js文件中。
5. 完整代码
以下就是一个mini-webpack的实现完整代码
const fs = require('fs');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const path = require('path');
const babel = require('@babel/core');
const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8'); // 读取文件内容
const ast = parser.parse(body, { sourceType: 'module' }); // 解析为 AST
const deps = {};
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
const abspath = './' + path.resolve(dirname, node.source.value); // 修正路径拼接问题
deps[node.source.value] = abspath;
}
});
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
});
const moduleInfo = { file, deps, code };
console.log(moduleInfo); // 打印模块信息
return moduleInfo;
}
const parseModules = file => {
const entry = getModuleInfo(file) //在这里调用getModuleInfo
const temp = [entry]
for (let i = 0; i < temp.length; i++) {
const deps = temp[i].deps
if (deps) {
for (const key in deps) {
if (deps.hasOwnProperty(key)) {
temp.push(getModuleInfo(deps[key]))
}
}
}
console.log(temp)
const depsGraph = {}
temp.forEach(moduleInfo => {
depsGraph[moduleInfo.file] = {
deps: moduleInfo.deps,
code: moduleInfo.code,
}
})
console.log(depsGraph)
return depsGraph
}
}
const bundle = (file) => {
const depsGraph = JSON.stringify(parseModules(file))
const content= `(
function (graph) {
function require(file) {
function absRequire(relPath) {
return require(graph[file].deps[relPath])
}
var exports = {}
(function (require,exports,code) {
eval(code)
})(absRequire,exports,graph[file].code)
return exports
}
require('${file}')
})(${depsGraph})`
console.log(content);
// 创建 dist 目录并写入文件
if (!fs.existsSync('./dist')) {
fs.mkdirSync('./dist');
}
fs.writeFileSync('./dist/bundle.js', content);
};
bundle('./src/index.js');