探究webpack5懒加载原理

591 阅读6分钟

webpack镇楼
不废话看看官方怎么谈5,Webpack5的新特性

  • 1.使用持久化缓存提高构建性能
  • 2.使用更好的算法和默认值改进长期缓存(long-term caching)
  • 3.清理内部结构而不引入任何破坏性的变化
  • 4.引入一些breaking changes,以便尽可能长的使用v5版本

光说不练假把式

安装webpack(v5)威武版,不用用怎么知道他有多好用?

npm init -y 
npm i webpack@next --save-dev

可以直接通过@next方式来安装webpack5版本,目前版本是"^5.0.0-alpha.23"

先来看下基本结构

├── bootstrap.js  // 手动启动webpack
├── pack.js       // 自己实现的webpack
├── package-lock.json
├── package.json
├── README.md
├── src
│   ├── a.js     // 入口文件会引用 a.js
│   └── index.js // 打包的入口文件
└── webpack.config.js // webpack配置文件

a.js只是导出个变量而已,非常的简单

module.exports = 'webyouxuan'

index.js负责引入a.js模块

let webyouxuan= require('./a');
console.log(webyouxuan)

webpack.config文件

const path = require('path');
module.exports = {
    mode:'development',
    entry:'./src/index.js',
    output:{
        path:path.join(__dirname,'dist'), 
        filename:'main.js'
    }
}

你会发现和webpack4的配置基本没有变化

开始打包,你会发现npx webpack 需要webpack-cli的支持,比较尴尬的是,到目前还没有与之匹配的webpack-cli,没办法啦,我们只好手动启动webpack了~~~

bootstrap.js 引入webpack进行打包项目

const webpack = require('webpack');
const webpackOptions = require('./webpack.config');
// 需要将 配置文件传入到webpack中,打包成功后我们打印stats信息
webpack(webpackOptions,(err,stats)=>{
    if(err){
        console.log(err);
    }else{
        console.log(stats.toJson())
    }
})

看下打包出来的信息:

我们需要掌握一些关键词:

  • module:在webpack中所有文件都是模块,一个模块会对应一个文件,webpack会通过入口找到所有依赖的模块
  • chunk:代码块,一个chunk由多个模块组合而成
  • assets:打包出的资源,一般和chunk个数相同

查看编译出的源码

发现比webpack4,打包出来的结果确实少了不少!更简洁,更容易读懂(这里我已把注释删掉)。

// 2.整体函数是个自执行函数
((modules) => { // 3.module传入的为所有打包后的结果
  var installedModules = {};
  function __webpack_require__(moduleId) { 
    if (installedModules[moduleId]) { // 做缓存的可以先不理 
      return installedModules[moduleId].exports;
    }
    var module = (installedModules[moduleId] = { // 5.创建模块,每个模块都有一个exports对象
      i: moduleId,
      l: false,
      exports: {}
    });
    modules[moduleId](module, module.exports, __webpack_require__); // 6.调用对应的模块函数,将模块exports传入
    module.l = true;
    // 8.用户会将结果放到module.exports对象上
    return module.exports;
  }
  function startup() {
    // 通过入口开始加载
    return __webpack_require__("./src/index.js"); // 默认返回的是 module.exports结果;
  }
  return startup(); // 4.启动加载
})({ // 1.列出打包后的模块
  "./src/a.js": module => {
    eval(
      "module.exports = 'webyouxuan'\n\n//# sourceURL=webpack:///./src/a.js?"
    );
  },
  "./src/index.js": (__unusedmodule, __unusedexports, __webpack_require__) => { // 7.加载其他模块,拿到其他模块的module.exports结果
    eval(
      'let webyouxuan = __webpack_require__(/*! ./a */ "./src/a.js");\nconsole.log(webyouxuan)\n\n//# sourceURL=webpack:///./src/index.js?'
    );
  }
});

总的来说不难理解,其实还是内部实现了个___webpack_require__方法,如果看不懂就多来几遍,看懂了发现也没什么。。。

这样我们可以直接把main.js在html中直接引入啦~,发现是不是已经可以打印出webyouxuan啦,顺便做个广告,关注我们!持续推送精品文章,给你点个赞👍

懒加载

我们在index入口文件中,采用 import语法动态导入文件

const button = document.createElement('button');
button.innerHTML = '关注 webyouxuan';
document.body.appendChild(button);
document.addEventListener('click',()=>{
    import('./a').then(data=>{
        console.log(data.default);
    })
});

再回头看编译的结果,貌似好像打包出来的结果有些复杂啦,没关系!其实核心很简单就是个jsonp加载文件。

打包出来的结果多了个src_a_js.main.js

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
  ["src_a_js"],
  { 
    "./src/a.js": module => {
      eval(
        "module.exports = 'webyouxuan'\n\n//# sourceURL=webpack:///./src/a.js?"
      );
    }
  }
]);

最终会通过script标签加载这个文件,加载后默认会调用 window下的 webpackJsonp的push方法

我们来看看 main.js 发生了哪些变化,话说有代码些小复杂,分段来看

先来看index模块做了那些事,为了看着方便我来把代码整理一下

 "./src/index.js":
 ((__unusedmodule, __unusedexports, __webpack_require__) => {
eval(`
    const button = document.createElement('button');
    button.innerHTML = '关注 webyouxuan';
    document.body.appendChild(button);
    document.addEventListener('click',()=>{
       __webpack_require__.e("src_a_js").then(
          __webpack_require__.t.bind(__webpack_require__, "./src/a.js", 7)).then(data=>{
            console.log(data.default);
          })
     })
    `);
 })

这里出现了两个方法 __webpack_require__.e__webpack_require__.t

__webpack_require__.e方法看似是用来加载文件的,咱们来找一找

__webpack_require__.f = {};
__webpack_require__.e = (chunkId) => { // chunkId => src_a_js动态加载的模块
    return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
        __webpack_require__.f[key](chunkId, promises); // 调用j方法 将数组传入
        return promises;
    }, []));
};


var installedChunks = {
    "main": 0 // 默认main已经加载完成
};
// f方法上有个j属性
__webpack_require__.f.j = (chunkId, promises) => {
    var installedChunkData = Object.prototype.hasOwnProperty.call(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
    if(installedChunkData !== 0) {  // 默认installedChunks肯定没有要加载的模块
        if(installedChunkData) {
            promises.push(installedChunkData[2]);
        } else {
            if(true) { // 创建一个promise 把当前的promise 成功失败保存到 installedChunks
                var promise = new Promise((resolve, reject) => {
                    installedChunkData = installedChunks[chunkId] = [resolve, reject];
                });
                // installedChunks[src_a_js] = [resolve,reject,promise]
                promises.push(installedChunkData[2] = promise);
                // 这句的意思是看是否配置publicPath,配置了就加个前缀
                var url = __webpack_require__.p + __webpack_require__.u(chunkId);
                // 1)创建script标签
                var script = document.createElement('script');
                var onScriptComplete;
                script.charset = 'utf-8';
                script.timeout = 120;
                script.src = url; // 2)开始加载这个文件
                var error = new Error();
                onScriptComplete = function (event) { //  完成工作
                    script.onerror = script.onload = null;
                    clearTimeout(timeout);
                    var reportError = loadingEnded();
                    if(reportError) {
                        var errorType = event && (event.type === 'load' ? 'missing' : event.type);
                        var realSrc = event && event.target && event.target.src;
                        error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
                        error.name = 'ChunkLoadError';
                        error.type = errorType;
                        error.request = realSrc;
                        reportError(error);
                    }
                };
                var timeout = setTimeout(function(){ // 超时工作
                    onScriptComplete({ type: 'timeout', target: script });
                }, 120000);
                script.onerror = script.onload = onScriptComplete;
                document.head.appendChild(script); // 3)将标签插入到页面
            } else installedChunks[chunkId] = 0;

        }
    }
};

虽然代码量比较多,其实核心就干了一件事 :创建script标签,文件加载回来那肯定就会调用push方法咯!

先跳过这段看下面的

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
  ["src_a_js"],
  { 
    "./src/a.js": module => {
      eval(
        "module.exports = 'webyouxuan'\n\n//# sourceURL=webpack:///./src/a.js?"
      );
    }
  }
]);
function webpackJsonpCallback(data) { // 3) 文件加载后会调用此方法
    var chunkIds = data[0]; // data是什么来着,你看看src_a_js怎么写的你就知道了 看上面! ["src_a_js"]
    var moreModules = data[1]; // 获取新增的模块

    var moduleId, chunkId, i = 0, resolves = [];
    for(;i < chunkIds.length; i++) {
        chunkId = chunkIds[i];
        if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
            // installedChunks[src_a_js] = [resolve,reject,promise] 这个是上面做的
            // 很好理解 其实就是取到刚才放入的promise的resolve方法
            resolves.push(installedChunks[chunkId][0]);
        }
        installedChunks[chunkId] = 0; // 模块加载完成
    }
    for(moduleId in moreModules) { // 将新增模块与默认的模块进行合并 也是就是modules模块,这样modules中就多了动态加载的模块
        if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
            __webpack_require__.m[moduleId] = moreModules[moduleId];
        }
    }
    if(runtime) runtime(__webpack_require__);
    if(parentJsonpFunction) parentJsonpFunction(data);

    while(resolves.length) { // 调用promise的resolve方法,这样e方法就调用完成了
        resolves.shift()();
    }
};

var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || []; // 1) window["webpackJsonp"]等于一个数组
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback; // 2) 重写了数组的push方法
var parentJsonpFunction = oldJsonpFunction;

加载的文件会调用 webpackJsonpCallback方法,内部就是将新增的模块合并到modules上,并且让__webpack_require__.e完成

__webpack_require__.t = function(value, mode) { // t方法其实很简单就是
    if(mode & 1) value = this(value); // 就是调用__webpack_require__加载最新的模块
};

这样用户就可以拿到新增的模块结果啦~~~,源码虽难,但是多看几遍总会有收获!

到此我们就将webpack5的懒加载功能整个过了一遍,其实思路和webpack4的懒加载一样呢~,不过不得不说webpack5打包出来的代码更加简洁啦! 期待webpack5正式发版!!!