【webpack】异步导入原理

94 阅读2分钟

背景及用户角度

webpack懒加载分离代码方式有如下几种

  • 入口起点:配置entry
  • 同一份代码引用:splitChunks即代码分割
  • 动态导入:通过内联模块分离代码

此处主要讨论第三点

main.js中写下如下代码


// 实际场景可能是点击事件之类的
setTimeout(()=>{
    import('./test.js').then(res=>{
        res.say()
    })
})

test.js

export function say(){
    console.log('说一些东西')
}

配置

 "webpack": "^5.73.0",
  "webpack-cli": "^4.10.0",
module.exports = {
  mode: "development",
  devtool: false,
  entry: {
    main: "./src/main.js",
  },
  output: {
    filename: "main.js", //定义打包后的文件名称
    path: path.resolve(__dirname, "./dist"), //必须是绝对路径
  },
};

主要代码

代码主要内容会被打包为如下(经优化)

main.js

require.e('src_test_js')
.then(require.bind(require,'./src/test.js'))
.then(res=>{
    res.say()
})

src_test_js.main.js

    (self["webpackChunLib"] = self["webpackChunLib"] || []).push([
      ["src_test_js"],
      {
        "./src/test.js": function (module, exports, require) {
          require.setModuleTag();
          require.defineProperties(exports, {
            say: () => say,
          });
          var say = function () {
            conosle.log("说明一些东西");
          };
        },
      },
    ]);

原理

大致原理过程如下

1、创建script标签,通过jsonp加载文件

2、执行其中脚本,运行push将内容合并至modules

3、通过require.bind(require,'./src/test.js')执行modules中对应的模板,拿到模块返回内容

4、执行对应的内容

上帝视角

接下来通过上帝视角看看require.e以及文件关联

各个函数

require.e

​ 代码大致:引入promise,执行require.j 并返回promise.all执行完成

​ 功能:返回require.j整合promise.all ,promise.all中执行完成时机即为 modules合并

require.j

​ 代码大致:赋值promise.all,jsonp导入文件

​ 功能: j 为promise.all添加执行项;sonp导入文件,导入时执行js文件;

src_test_js.main.js

​ 功能:调用全局对象webPackChunk.push功能,完成modules合并

webPackChunk

​ 功能:合并modules,合并完成后执行promise.all中每个promise.resolve完成promise.all调用

至此require.e执行完成,modules完成导入

接下来则是像上一节一样利用require完成数据输入

效果

如下:src_test_js.main.js会延迟一秒导入

recording.gif

总体代码如下

mian.js

const modules = {}
function require(id){
    const module = {exports:{}}
    modules[id](module,module.exports,require)
    return module.exports
}
// 设置module标志
require.setModuleTag=function(exports){
    Object.defineProperty(exports,Symbol.toStringTag,{
        value:'esModule'
    })
    Object.defineProperty(exports,'_es_moduel',{
        value:true
    })
}
// 设置属性
require.defineProperties = function(exports,definition){
    for(let key in definition){
        Object.defineProperty(exports,key,{
            enumerable:true,
            get:definition[key]
        })
    }
}
// 异步调用
require.e = function(chunkId){
    const promises = []
    require.j(chunkId,promises)
    return Promise.all(promises)
}
// output中的配置
require.publicPath = './'
const installChunk = {}
require.j = function(chunkId,promises){
    var promise = new Promise((resolve,reject)=>{
        installChunk[chunkId] = [resolve,reject]
    })
    promises.push(promise)
    var script = document.createElement('script')
    script.src = require.publicPath+chunkId+'.main.js'
    document.body.appendChild(script)
}
// 外部文件jsonp导入后会执行 整合module主函数
var webPackChunk = function([chunkId,moreModule]){
    let resolves = []
    for(let i =0;i<chunkId.length;i++){
        resolves.push(installChunk[chunkId][0]) 
        // 标志完成了
        installChunk[chunkId] = 0
    }
    for(let key in moreModule){
        modules[key] = moreModule[key]
    }
    //执行完promise.all
    while(resolves.length){
        resolves.shift()()
    }
}
var chunkLoadingGlobal = this["webpackChunklib"] = this["webpackChunklib"] || [];
chunkLoadingGlobal.push = webPackChunk

setTimeout(()=>{
    require.e('src_test_js').then(require.bind(require,'./src/test.js')).then((module)=>{
        const {say} = module
        say()
    })
},1000)

src_test_js.main.js


(this.webpackChunklib =this.webpackChunklib||[]).push([
    ['src_test_js'],{
        './src/test.js'(module,exports,require){
            require.setModuleTag(exports)
            require.defineProperties(exports,{
                say:()=>say
            })
            function say(){
                console.log('说一些东西')
            }
        }
    }
])