webpack-webpack4源码分析

一、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('使用缓存');
            //  这里默认用了 default
            return 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环境为代码
        //  这一步是 import
        requireFn('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];
        });
    }


    //  加载异步js
    requireFn.e = function (moduleId) {
        //  创建 script
        const script = document.createElement('script');
        //  promise是为了延迟到 script 加载完
        const promise = new Promise((resolve) => {
            //  一些验证
            script.onload = function () {
                resolve();
            }
        });
        //  设置 src
        script.src = `${moduleId}.js`;
        //  添加到文档流
        document.head.appendChild(script);
        //  promise.all 是因为有其他情况,比如 async 引入另一个 async
        return Promise.all([promise]);
    }

    function requireFn(moduleId) {
        //  如果以前有调用过 moduleId:function 这个函数
        if (requireFn.memory[moduleId]) {
            console.log('使用缓存');
            //  这里默认用了 default
            return 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环境为代码
        //  这一步是 import
        requireFn('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';
    },
}));

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值