当前位置: 代码迷 >> 综合 >> webpack-webpack4源码分析
  详细解决方案

webpack-webpack4源码分析

热度:31   发布时间:2023-12-11 23:24:22.0

一、webpack4核心架构

webpack本质是一个静态模块打包器。

webapck 配置中最重要的有:

  1. 入口
  2. 出口
  3. 各种解析loader
  4. 优化
  5. 插件

二、优化

webpack 默认进行 production 模式一些简单的、基本的处理,webpack默认 production 模式就已经处理了,比如箭头函数,专门为 development  处理是没有意义的。但 production 模式不是所有的优化都做了,这就需要手动优化。

一、tree-shaking

引用函数不纯,需要指定纯函数注释 /*pure*/;需要分析scope。

对于 production 模式,默认做 tree-shaking ,但是不会分析函数内的 scope。如果我在一个小函数内倒入一个大包,那么这个大包也会被编译出来。所以这时我需要插件分析 scope。

必须是可以被解构的包,才能进行 tree-shaking。

三、loader

  • loader 按照逆序执行:[loader-a,loader-b]先执行 loader-a。
  • loader 传入源文件或二进制文件;输出处理后的文件 + sourcemap(可选)。
  • 一些 loader 需要标记异步处理。
  • loader 和 其他 webpack 处理模式如出一辙,都是拿到源文件然后通过设置的 option 进行处理。

四、编译后的结果

一、同步引入

流程:

  1. webpack 将会创建一个立即执行匿名函数,如果在 wepback.config.js 中设置了导出模块名,那么这个函数将会把返回值指向这个标识符。
  2. 匿名函数的参数是一个大的同步执行函数的包,用模块地址→模块内容的形式组织起来。在 dev 环境下,模块内容为 eval,这是为了开发速度快做的简单处理;在 prod 环境下,将会做优化,形成真正的代码。
  3. 匿名函数的函数体是一个大的闭包函数。它提供真正的执行模块的方法 __webpack_require__,这个方法非常重要!当执行这个方法时,会按照引入的模块名,缓存了所有这些模块的导出状态。默认执行入口模块,并且模块内的引入关系,都是通过在父级模块中通过这个方法调用子模块的方式。
  4. 任意模块引入同步模块,子模块都会被打入父级模块所在 js 文件。任何模块引入异步模块,子模块都会创建新的模块。
  5. 无论是单个入口还是多入口,流程都是类似的。多入口引入相同的 js 模块时,会重复打包这个 js 分别到两个入口文件下,这会造成一定的浪费,所以要考虑优化。

假设源文件为:

//    sync.js
export default '我是 sync';
//    index.js
import syncfrom './sync';setTimeout(() => {console.log(sync);
}, 1000);

则编译过的文件大致为:

(function (modules){function requireFn(moduleId) {//  如果以前有调用过 moduleId:function 这个函数if (requireFn.memory[moduleId]) {console.log('使用缓存');//  这里默认用了 defaultreturn requireFn.memory[moduleId].default;}//  第一次访问该模块const module = {};//  做一个缓存requireFn.memory[moduleId] = module;modules[moduleId](requireFn.memory, module, requireFn);}//  所有文件的缓存requireFn.memory = {};//  初始执行,挂在到 requireFn.export,有 export {...} 时才能看得出,它用的是 Object.defineProperty + 闭包requireFn.export = requireFn('index');//  暴露给全局,如果设置了的话,整个index.js的函数执行将被付给这个标识符return requireFn.export;
})({index: function (modules, exports, requireFn){//  dev源码为eval,为了编译快,prod环境为代码//  这一步是 importrequireFn('sync');setTimeout(() => {console.log(requireFn.memory['sync'].default);}, 1000);},sync: function (modules, exports, requireFn){//  这里只用 default 为了方便exports.default = ('我是 sync');},
});

二、异步引入

流程:

  1. 每一个异步引入模块的方式,都会创建一个异步文件,这个文件的文件名可以用魔法注释的方法定义 /* webpackChunkName: 'async name' */。
  2. 在父级 js 中会创建一个专门用于缓存异步 js 模块的列表 webpackJsonp 暴露到全局环境 window 中;还会创建 webpackJsonpCallback 方法 用于设置异步模块的执行缓存;并且设置 webpackJsonp 的 push 方法 为 webpackJsonpCallback
  3. 在父级 js 中会调用方法 __webpack_require__.e 引入异步 js,__webpack_require__.e 会创建一个 script 标签,src 就是异步 js 文件的地址。
  4. 新创建的 script 标签会执行它的代码,它会在全局的异步模块列表 webpackJsonp 中 push(会看 2,这个 push 实际是 webpackJsonpCallback) 进这个异步模块的代码。
  5. webpackJsonpCallback 方法会去执行 __wepback_require__,此时与同步模块类似了,并且,这个异步模块也被缓存到了父级 js 中。
  6. 更深层的异步引入原理也是类似的,所有父级 js 中提供了所有异步模块引入的方法,所有模块执行都是通过 __webpack_require__ 这个方法,它将所有需要的参数提供给了所有模块。

那个 * webpackChunkName: 'async name' */ ,webpackChunkName 后面的冒号必须紧挨着 webpackChunkName,中间不能有空格。

源文件:

//  async.js
(window.webpackJsonp && window.webpackJsonp.webpackJsonpCallback({'async': function (modules, exports, requireFn) {exports.default = '我是 async';}
}))

编译后的文件: 

(function (modules){//  只是提供了一个对象window.webpackJsonp = [];//  异步模块执行webpackJsonp.webpackJsonpCallback = function (obj) {Object.keys(obj).forEach(moduleId => {//  绑定异步模块到主模块上modules[moduleId] = obj[moduleId];});}//  加载异步jsrequireFn.e = function (moduleId) {//  创建 scriptconst script = document.createElement('script');//  promise是为了延迟到 script 加载完const promise = new Promise((resolve) => {//  一些验证script.onload = function () {resolve();}});//  设置 srcscript.src = `${moduleId}.js`;//  添加到文档流document.head.appendChild(script);//  promise.all 是因为有其他情况,比如 async 引入另一个 asyncreturn Promise.all([promise]);}function requireFn(moduleId) {//  如果以前有调用过 moduleId:function 这个函数if (requireFn.memory[moduleId]) {console.log('使用缓存');//  这里默认用了 defaultreturn requireFn.memory[moduleId].default;}//  第一次访问该模块const module = {};//  做一个缓存requireFn.memory[moduleId] = module;modules[moduleId](requireFn.memory, module, requireFn);}//  所有文件的缓存requireFn.memory = {};//  初始执行,挂在到 requireFn.export,有 export {...} 时才能看得出,它用的是 Object.defineProperty + 闭包requireFn.export = requireFn('index');//  暴露给全局,如果设置了的话,整个index.js的函数执行将被付给这个标识符return requireFn.export;
})({index: function (modules, exports, requireFn){//  dev源码为eval,为了编译快,prod环境为代码//  这一步是 importrequireFn('sync');setTimeout(() => {console.log(requireFn.memory['sync'].default);//  在这里异步加载 async 模块requireFn.e('async').then(() => {requireFn('async');//  调用后,就有这个模块了console.log(requireFn.memory['async'].default);});}, 1000);},sync: function (modules, exports, requireFn){//  这里只用 default 为了方便exports.default = ('我是 sync');},
});
//    async.js
(window.webpackJsonp && window.webpackJsonp.webpackJsonpCallback({'async': function (modules, exports, requireFn){exports.default = '我是 async';},
}));

  相关解决方案