webpack

发布于:2025-08-17 ⋅ 阅读:(19) ⋅ 点赞:(0)

简介

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 的加载和合并。

image-20250203105210570

webpack解决变量重名问题

通过作用域进行解决

Webpack 将每个文件视为一个模块,ES6 模块具有自己独立的作用域。在模块内部定义的变量、函数等只在该模块内有效,不会影响到其他模块。

webpack加载器

webpack非常经典的加载器

image-20250203111503743

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;
}

所以:

  1. 你传入的 p 其实是一个模块对象(或者函数),t.n() 会判断它是不是 ESModule。
  2. 无论 p 是什么,t.n() 都会返回一个新函数(叫 getter),这个函数调用时才返回 p(或者 p.default)。
  3. 因此,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)。
它负责三件事情:

  1. 已经内置的模块(同步模块)
    代码最外层有一个立即执行函数,把若干 function(e, t, n){...} 组成的对象传进来,这些才是真正的业务模块。
    运行时通过 u(n)(也就是 __webpack_require__)去执行它们,并缓存结果。

  2. 异步加载(代码分割 / 懒加载)
    你看到的巨大映射表和 u.e = function(chunkId){...} 就是用来 按需加载额外 JS/CSS 文件 的:

    • t 对象:把 chunk 名映射成最终文件名(含 hash)。
    • a 对象:记录每个 chunk 当前的状态(0=未加载,1=已加载,Promise=正在加载)。
    • u.e(chunkId)
      – 如果该 chunk 已加载,直接返回空数组;
      – 否则创建 <script><link> 标签去拉对应的 js/xxx.xxxxx.jscss/xxx.xxxxx.css
      – 用 Promise 封装,加载完成后把 chunk 的模块注册到运行时,再 resolve。
      业务代码里常见的 import(/* webpackChunkName: "detail" */ './detail') 最终就会走到 u.e("detail")
  3. 模块缓存 (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)
}
  • es 这个函数的参数,代表要加载的 模块 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


网站公告

今日签到

点亮在社区的每一天
去签到