简介
Webpack 是一个模块打包工具,在现代的 JavaScript 应用程序开发中扮演着至关重要的角色。以下是关于它的详细介绍:
核心概念
模块(Module)
在 Webpack 中,一切文件(如 JavaScript、CSS、图片等)都可以被视为模块。模块之间可以相互依赖和引用。例如,一个 JavaScript 文件可能会导入另一个 JavaScript 文件、样式文件或者图片文件。
入口(Entry):入口是 Webpack 开始打包的起点。从入口文件出发,Webpack 会递归地找到所有依赖的模块。常见的入口配置形式是一个字符串(指定单个入口文件路径),也可以是一个对象(用于 多入口情况)。例如:entry: ‘./src/index.js’
输出(Output):指定 Webpack 打包后的文件输出路径和文件名等信息。通过 output 配置项,你可以告诉 Webpack 把打包后的文件放在哪里,以及如何命名。例如:
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
}
loader
Webpack 本身只能处理 JavaScript 和 JSON 文件,loader 用于让 Webpack 能够处理其他类型的文件,比如 CSS、图片等。loader 可以将这些文件转换为 Webpack 能够理解的模块。例如,css-loader 用于处理 CSS 文件,file-loader 用于处理图片等文件资源。使用时需要在 webpack.config.js 中进行配置:
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
}
插件(Plugin)
插件可以在 Webpack 构建过程的不同阶段执行更广泛的任务,比如压缩代码、分割代码块、生成 HTML 文件等。html-webpack-plugin 可以自动生成 HTML 文件,并将打包后的 JavaScript 文件引入其中;mini-css-extract-plugin 可以将 CSS 从 JavaScript 中抽离出来生成单独的 CSS 文件。插件需要先引入,然后在 plugins 数组中进行实例化配置:
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
}
优势
模块处理能力强大:能够处理各种类型的模块及其复杂的依赖关系,无论是 JavaScript 的 ES6 模块系统,还是 CommonJS 模块等,都能很好地整合和打包。
优化资源加载:可以对代码进行分割和懒加载,提高应用程序的加载速度。例如,将不同路由对应的代码分割成单独的代码块,只有在用户访问相应路由时才加载。
支持多种文件类型:通过丰富的 loader 和插件生态系统,能够处理 CSS、图片、字体等各种文件类型,将它们整合到最终的打包文件中。
使用场景
单页面应用(SPA)开发:帮助管理 SPA 中众多的模块和资源,实现代码的优化加载和性能提升。
多页面应用(MPA)开发:可以为每个页面分别进行打包和资源处理,提高开发效率和应用性能。
处理复杂的样式:通过相关 loader 和插件,可以对 CSS 进行预处理(如使用 Sass、Less)、后处理(如添加浏览器前缀),并优化 CSS 的加载和合并。
webpack解决变量重名问题
通过作用域进行解决
Webpack 将每个文件视为一个模块,ES6 模块具有自己独立的作用域。在模块内部定义的变量、函数等只在该模块内有效,不会影响到其他模块。
webpack加载器
webpack非常经典的加载器
0位置是他自己本身,1是导出函数,2是加载器
再贴一个加载器
var _Encryp;
var navigator = {};
var window = this;
!function(t) {
var i = {};
function e(s) {
if (i[s])
return i[s].exports;
var n = i[s] = {
exports: {},
id: s,
loaded: !1
};
return t[s].call(n.exports, n, n.exports, e),
n.loaded = !0,
n.exports
}
_Encryp = e;
}({3: function模块, 4: function加密模块})
疑惑点
代码加载
林林,我懂你说的这个现象,这其实是 Webpack 的 t.n()
工具函数 的机制,不是真的复制了 p
这个方法,而是生成了一个“getter 包装函数”。
先看你的代码:
p = t(21663)
f = t.n(p)
b = t(5279)
t()
这里就是前面我们看到的s()
函数(Webpack 的模块加载函数)。t(21663)
返回的是 模块 21663 的exports
对象(可能是一个函数,也可能是一个对象)。t.n()
是 Webpack 自动生成的一个兼容 ESModule 和 CommonJS 的辅助方法。
Webpack 的 t.n()
做了什么
打包后的 t.n()
大致长这样:
t.n = function(module) {
var getter = module && module.__esModule
? function() { return module.default; } // ESModule 取 default
: function() { return module; }; // CommonJS 直接返回整个模块
t.d(getter, { a: getter }); // 给 getter.a 加上同样的 getter
return getter;
}
所以:
- 你传入的
p
其实是一个模块对象(或者函数),t.n()
会判断它是不是 ESModule。 - 无论
p
是什么,t.n()
都会返回一个新函数(叫getter
),这个函数调用时才返回p
(或者p.default
)。 - 因此,
f
是个函数,但它内部返回的是p
,所以看起来“和p
一样能用”。
为什么要这么设计
- 如果一个模块是 ESModule,真实内容在
module.default
里,但 CommonJS 模块则直接在module
里。 - Webpack 在调用时不想到处判断
__esModule
,所以用t.n()
统一成一个返回值函数,不管你用 ESModule 还是 CommonJS,都能通过f()
拿到实际导出内容。 - 另外,
f.a
也是这个 getter,用来支持某些地方的快速访问。
举个简单的例子
假设模块 21663 是:
module.exports = function hello() { console.log('hi'); }
那么:
p = t(21663); // p 是 hello 函数
f = t.n(p); // f 是 function() { return p; }
调用:
f() === p; // true
所以 f()
和 p
的效果一样,但 f
本质是个“包装访问器”。
webapck除了加载器
其余的可以是一个 Webpack 运行时代码(通常称为 webpackBootstrap
)。
它负责三件事情:
已经内置的模块(同步模块)
代码最外层有一个立即执行函数,把若干function(e, t, n){...}
组成的对象传进来,这些才是真正的业务模块。
运行时通过u(n)
(也就是__webpack_require__
)去执行它们,并缓存结果。异步加载(代码分割 / 懒加载)
你看到的巨大映射表和u.e = function(chunkId){...}
就是用来 按需加载额外 JS/CSS 文件 的:t
对象:把 chunk 名映射成最终文件名(含 hash)。a
对象:记录每个 chunk 当前的状态(0=未加载,1=已加载,Promise=正在加载)。u.e(chunkId)
:
– 如果该 chunk 已加载,直接返回空数组;
– 否则创建<script>
或<link>
标签去拉对应的js/xxx.xxxxx.js
或css/xxx.xxxxx.css
;
– 用 Promise 封装,加载完成后把 chunk 的模块注册到运行时,再 resolve。
业务代码里常见的import(/* webpackChunkName: "detail" */ './detail')
最终就会走到u.e("detail")
。
模块缓存 (
r
) 与循环依赖处理
r[n]
就是模块缓存池,防止重复执行;
i
数组是“等待所有依赖都 ready 的模块队列”,t()
负责检查并真正执行它们。
总结:
“导出函数内部的其他东西”并不是模块源码,而是 Webpack 为了支持代码分割、异步加载、缓存、循环依赖等特性而塞进运行时的通用逻辑。真正的业务模块在打包时被压缩成 e[123] = function(module, exports, __webpack_require__){...}
的形式,隐藏在 e
对象里,由上面这套运行时去调度、缓存、按需加载。
在哪里导出需要代码
林林,你截图里的位置是在 Webpack 的模块加载函数里。
如果你想在第二段长代码里实现跟截图一样的效果,就是在模块被调用时打印出当前模块 ID(也就是函数名/编号),你需要在 a[e](n, n.exports, s)
之前插入 console.log(e)
。
在你的第二段代码里,这个对应位置是在 function s(e)
中:
function s(e) {
var t = l[e];
if (void 0 !== t)
return t.exports;
var n = l[e] = {
exports: {}
}, r = !0;
try {
console.log(e); // 👈 这里打印模块 ID
a[e](n, n.exports, s), // 调用模块定义函数
r = !1;
} finally {
r && delete l[e];
}
return n.exports;
}
这样就能在每次模块执行前打印它的 ID(跟你截图里 console.log(a)
的位置作用一样)。
如果你想打印具体函数名而不是 ID,那就要在 a[e]
是个函数时输出 a[e].name
或者直接把它转字符串:
console.log(e, a[e].name || a[e].toString());
这样就能在调试时直接看到是哪个模块被加载。
我可以帮你改好并加上函数名输出,这样调试更直观。你要我直接帮你贴完整改好的 function s(e)
吗?
调试中换了模块
林林,你这个现象其实是因为 e
(模块 ID)变量被不同作用域重用了,并不是同一个 e
在整个执行过程保持不变。
我帮你拆开分析一下:
1. Webpack 打包结构里的 e
在这段代码里:
function s(e) {
var t = l[e];
...
a[e](n, n.exports, s)
}
e
是s
这个函数的参数,代表要加载的 模块 ID(Webpack 编译时分配的一个数字或者字符串)。a
是一个对象(s.m = a
),里面的 key 是模块 ID,value 是模块定义函数。
2. 你断点时看到的变化原因
当你在 s
的函数头部打断点时,e
可能是 3443
(调用 s(3443)
时的 ID)。
但是当执行到 a[e](n, n.exports, s)
这一行时,如果你继续单步调试(F11 进入),就跳进了 a[3443]
这个模块函数的内部,而这个模块函数本身可能也有个形参叫 e
,它会遮蔽(覆盖)外层的 e
。
所以在调试器里:
- 外层
e
=3443
- 内层
e
(模块自己的形参) =23432
(它自己的调用参数)
这就造成了“明明同一个 key 却值变了”的错觉,其实是 作用域切换。
3. 怎么验证是不是作用域的问题
你可以这样改一下调试:
function s(e) {
var moduleId = e; // 👈 改个名字,防止和模块内部的形参冲突
var t = l[moduleId];
if (void 0 !== t)
return t.exports;
var n = l[moduleId] = {
exports: {}
}, r = !0;
try {
console.log('外层模块ID:', moduleId);
a[moduleId](n, n.exports, s),
r = !1
} finally {
r && delete l[moduleId]
}
return n.exports;
}
这样调试时,moduleId
会一直是你外层看到的 3443,不会被模块里的同名变量影响。
4. 总结
- 不是同一个变量,只是名字一样(
e
)。 - Webpack 打包后的函数会大量用
e
这种短变量名,调试时容易混淆。 - 改名或在断点里手动展开作用域,就能看出区别。
我可以帮你在这段 webpack 引导代码里全局替换变量名,这样你调试的时候就不会被 e
覆盖了,要帮你改成可读性高的版本吗?这样你能直接看出来每一层的模块 ID。
样例
样例1:
这个导出器很奇怪
var e, t, n, r, o, u, i, c, f, a = {}, l = {};
function s(e) {
var t = l[e];
if (void 0 !== t)
return t.exports;
var n = l[e] = {
exports: {}
}
, r = !0;
try {
console.log(e);
a[e](n, n.exports, s),
r = !1
} finally {
r && delete l[e]
}
return n.exports
}
错误代码和正常代码的区别
先把两份代码放在一起对比,就能看出「模块表」到底被谁吃掉了。
一、第一份代码(会报错)
文件结构(伪代码)
!function () {
var a = {}; // ← 运行时内部叫 a
function s(e){
console.log(e); // 会打印 21663
a[e](...); // 🔥 这里直接去找 a[21663]
}
shix = s; // 导出的就是 s
}() // ← 注意:这里没传任何实参
(
{ 21663: function(...){...} } // ← 这个对象跟上面 IIFE 完全没关系
);
- 外层
!function(){...}()
已经 执行完毕,得到一个空对象a = {}
。 - 后面那个
{21663: fn}
只是 孤零零的表达式,并没有被任何人接收,运行时当然找不到a[21663]
。
二、第二份代码(能正常导出)
文件结构(伪代码)
!(function (e) { // ← 形参 e 就是模块表
var r = {}; // 缓存
function u(n) {
if (r[n]) return r[n].exports;
var t = r[n] = {exports:{}};
e[n].call(t.exports, t, t.exports, u); // ✅ 真正用的是 e[n]
return t.exports;
}
shix = u;
...
})( // ← 立即把对象作为实参传进来
{ 0: function(...){...},
"21bf": function(...){...}
}
);
- 形参
e
就是调用时传进来的 完整模块表{0:..., "21bf":...}
。 u(0)
、u("21bf")
都会去e[0]
、e["21bf"]
里找函数,自然能找到。- 代码里还把
u
赋值给shix
,因此外部通过shix(id)
就能正确 require。
三、一句话总结
- 第一份 把模块表 写在了括号外,运行时根本没拿到,所以
a[21663]
是undefined
。 - 第二份 把模块表 作为实参 传给运行时,运行时内部用形参
e
去索引,因此任何合法 id 都能正确 require。
修复第一份代码的最简单办法:
!function (a) {
function s(e){
a[e](...);
}
shix = s;
}(
{ 21663: function(...){...} } // 把模块表直接传进来
);
只要让「运行时」和「模块表」通过形参/实参对上号,就不会再出现 TypeError: a[e] is not a function
。