## 动态加载 动态加载即按需加载,对于提高性能、减少打包体积,节省流量很有必要,ssr时需要同时处理服务端和客户端 服务端要每次都找到并渲染出动态加载的模块 客户端只有页面出来初始时同步渲染要提前加载好动态模块,之后的路由都走正常的客户端异步加载方式 ## 实现 动态加载使用import()语法实现,打包工具会转化语法,执行时创建script标签加载js文件 ### webpack处理方式 使用import用于拆包,webpack4会自己处理import语法,将其转化为__webpack_require__.e以处理动态加载的模块 如果import的包依赖其他包,webpack会使用Promise.all包一层,否者只使用__webpack_require__.e来处理 ```javascript // 无依赖 __webpack_require__.e(0).then(__webpack_require__.bind(null, "./dynamic_module_path")) // 有依赖 Promise.all([ __webpack_require__.e(0), __webpack_require__.e(1), __webpack_require__.e(2), ]).then(__webpack_require__.bind(null, "./dynamic_module_path")) ``` #### 客户端__webpack_require__.e函数 ```javascript __webpack_require__.e = function requireEnsure(chunkId) { var promises = [] ... // css chunk var installedChunkData = installedChunks[chunkId] if(installedChunkData !== 0) { // 0 means "already installed". // a Promise means "currently loading". if(installedChunkData) { promises.push(installedChunkData[2]) } else { // setup Promise in chunk cache var promise = new Promise(function(resolve, reject) { ... }) promises.push(installedChunkData[2] = promise) } } return Promise.all(promises); } ``` ### 服务端__webpack_require__.e函数 ```javascript // 当作普通包加载 __webpack_require__.e = function requireEnsure(chunkId) { const promises = [] const installedChunkData = installedChunks[chunkId] if (installedChunkData !== 0) { requireChunk(chunkId) // 找到chunkId对的真是文件路径,直接require进来 } // 是否含有installedChunks都返回空数组,因为require chunk是个同步操作,已经调用webpackJsonpCallback将chunk和模块导入 return Promise.all(promises) } ``` ## 同步方式执行 按上面的结果看无论是否有依赖都会返回一个promise,意味着异步操作 也就是说如果我想再render这个同步方法里就拿到返回值,结果自然是取不到要渲染的异步加载组件 ### Promise.all 分析以上代码,其实在服务端是能立即加载好chunks的,并且每次都返回一个promise.all,其值是一个空数组 因此两种模式下的返回值应该是 ```javascript Promise.all([]).then(__webpack_require__.bind(null, "./dynamic_module_path")) Promise.all([ Promise.all([]), Promise.all([]), ... ]).then(__webpack_require__.bind(null, "./dynamic_module_path")) ``` 因此我有一个大胆的想法,重写下promise.all方法,如果参数是空数组第一个then改成同步执行 在重写之前我也思考了这里是否会对业务正常逻辑造成影响,这样会给Promise.all造成的影响是如果Promise.all传入一个空数组,第一次then会以同步的方式执行,但在考虑了其他所有方案的复杂度和对业务的侵入性之后,我还是觉得值得这样去做,算作一种约束 代码越简单出错越少 ### 避免影响全局Promise.all 即使Promise.all的参数值为空也不同步执行,只判断_isSyncPromise属性是否存在,需要我们在客户端webpack-runtime里引入插件改变为空的传值,将传空数组的地方标记下属性 #### webpack插件 ```javascript const replaceReg = /Promise\.all\(([^()]*)\)/g ... const source = new ConcatSource() // 替换webpack中的Promise.all参数用于标识同步的Promise const moduleSource = modules.source().replace(replaceReg, ((_match, pos) => { return `Promise.all(${pos} && ${pos}.length === 0 ? { _isSyncPromise: true } : ${pos})` })) ... ``` #### 重写Promise.all函数 ```javascript if (!Promise._all) { !(function() { function getType(context) { return Object.prototype.toString.call(context).slice(8, -1).toLowerCase() } function checkValue(arr) { if (arr.length > 0) { var hasValue = false for (var i in arr) { var item = arr[i] if (!hasValue) { var type = getType(item) if (type === 'array') { hasValue = checkValue(item) } else if(type === 'object' && item._isSyncPromise) { hasValue = false } else { hasValue = true } } } return hasValue } return !arr._isSyncPromise } function SyncPromise(value, error) { this.value = value this.error = error this.finish = true this._isSyncPromise = true } SyncPromise.prototype.then = function(onFulfilled) { var error = null var value = null try { value = onFulfilled(this) } catch(e) { error = e } return new SyncPromise(value, error) } Promise._all = Promise.all Promise.all = function () { var value = arguments[0] if (typeof value === 'object') { var isSyncValue = !checkValue(value) if (isSyncValue) { return new SyncPromise() // 这里是给webpack后的then调用 } } return Promise._all.apply(Promise, arguments) } })() } ``` 为空时实际返回值 ```javascript Promise.all({ _isSyncPromise: true }).then(__webpack_require__.bind(null, "./dynamic_module_path")) Promise.all([ Promise.all({ _isSyncPromise: true }), Promise.all({ _isSyncPromise: true }), ... ]).then(__webpack_require__.bind(null, "./dynamic_module_path")) // 最后返回给我们的值 const dynamicModule = import('./dynamic_module_path') dynamicModule == { _isSyncPromise, value, error, finish, } ```