Webpack 之 code spliting
code spliting
接下来说的 code spliting 都是指 dynamic imports (动态导入),不涉及如何设置多个入口起点问题
关于动态导入,有两种方法:一种是处于ES提案第三阶段的 import(),另一种是 commonjs 草案的 require.ensure,这个只是个草案,并没有纳入规范,但是 webpack 利用这个实现了早期的动态导入。
以 import()
语法为例,测试用例如下:
// index.js import(/* webpackChunkName: "foo" */ './foo').then(({ foo }) => { console.log(foo) }) import(/* webpackChunkName: "bar" */ './bar').then((module) => { console.log(module.default(), module.bar1()) })
// foo.js export const foo = 2
// bar.js export default function bar() { console.log('bar es Modules') } export function bar1() { console.log('bar es Modules') }
因为这是一个处于 stage 3 的语法,所以默认 babel 识别不了,这时候会报错,需要在 babel 配置中引入 @babel/plugin-syntax-dynamic-import
插件
{ plugins: [ ['@babel/plugin-syntax-dynamic-import'] ] }
同时由于默认的 eslint 语法解析器只能解析 stage 4 以及规范中的语法,所以使用 eslint 会报语法错误,所以需要将 eslint 的语法解析器更换为 babel-eslint
npm i babel-eslint -D
eslint 配置文件
{ "parser": "babel-eslint", }
这个时候通过 webpack 打包之后就会生成三个文件,入口文件 index
内容如下:
/******/ (function(modules) { // webpackBootstrap /******/ // install a JSONP callback for chunk loading /******/ function webpackJsonpCallback(data) { /******/ var chunkIds = data[0]; /******/ var moreModules = data[1]; /******/ /******/ /******/ // add "moreModules" to the modules object, /******/ // then flag all "chunkIds" as loaded and fire callback /******/ var moduleId, chunkId, i = 0, resolves = []; /******/ for(;i < chunkIds.length; i++) { /******/ chunkId = chunkIds[i]; /******/ if(installedChunks[chunkId]) { /******/ resolves.push(installedChunks[chunkId][0]); /******/ } /******/ installedChunks[chunkId] = 0; /******/ } /******/ for(moduleId in moreModules) { /******/ if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { /******/ modules[moduleId] = moreModules[moduleId]; /******/ } /******/ } /******/ if(parentJsonpFunction) parentJsonpFunction(data); /******/ /******/ while(resolves.length) { /******/ resolves.shift()(); /******/ } /******/ /******/ }; /******/ /******/ /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // object to store loaded and loading chunks /******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched /******/ // Promise = chunk loading, 0 = chunk loaded /******/ var installedChunks = { /******/ "index": 0 /******/ }; /******/ /******/ /******/ /******/ // script path function /******/ function jsonpScriptSrc(chunkId) { /******/ return __webpack_require__.p + "" + ({"bar":"bar","foo":"foo"}[chunkId]||chunkId) + ".js" /******/ } /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ // This file contains only the entry chunk. /******/ // The chunk loading function for additional chunks /******/ __webpack_require__.e = function requireEnsure(chunkId) { /******/ var promises = []; /******/ /******/ /******/ // JSONP chunk loading for javascript /******/ /******/ 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) { /******/ installedChunkData = installedChunks[chunkId] = [resolve, reject]; /******/ }); /******/ promises.push(installedChunkData[2] = promise); /******/ /******/ // start chunk loading /******/ var script = document.createElement('script'); /******/ var onScriptComplete; /******/ /******/ script.charset = 'utf-8'; /******/ script.timeout = 120; /******/ if (__webpack_require__.nc) { /******/ script.setAttribute("nonce", __webpack_require__.nc); /******/ } /******/ script.src = jsonpScriptSrc(chunkId); /******/ /******/ onScriptComplete = function (event) { /******/ // avoid mem leaks in IE. /******/ script.onerror = script.onload = null; /******/ clearTimeout(timeout); /******/ var chunk = installedChunks[chunkId]; /******/ if(chunk !== 0) { /******/ if(chunk) { /******/ var errorType = event && (event.type === 'load' ? 'missing' : event.type); /******/ var realSrc = event && event.target && event.target.src; /******/ var error = new Error('Loading chunk ' + chunkId + ' failed.n(' + errorType + ': ' + realSrc + ')'); /******/ error.type = errorType; /******/ error.request = realSrc; /******/ chunk[1](error); /******/ } /******/ installedChunks[chunkId] = undefined; /******/ } /******/ }; /******/ var timeout = setTimeout(function(){ /******/ onScriptComplete({ type: 'timeout', target: script }); /******/ }, 120000); /******/ script.onerror = script.onload = onScriptComplete; /******/ document.head.appendChild(script); /******/ } /******/ } /******/ return Promise.all(promises); /******/ }; /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); /******/ } /******/ }; /******/ /******/ // define __esModule on exports /******/ __webpack_require__.r = function(exports) { /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); /******/ } /******/ Object.defineProperty(exports, '__esModule', { value: true }); /******/ }; /******/ /******/ // create a fake namespace object /******/ // mode & 1: value is a module id, require it /******/ // mode & 2: merge all properties of value into the ns /******/ // mode & 4: return value when already ns object /******/ // mode & 8|1: behave like require /******/ __webpack_require__.t = function(value, mode) { /******/ if(mode & 1) value = __webpack_require__(value); /******/ if(mode & 8) return value; /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; /******/ var ns = Object.create(null); /******/ __webpack_require__.r(ns); /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); /******/ return ns; /******/ }; /******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __webpack_require__.n = function(module) { /******/ var getter = module && module.__esModule ? /******/ function getDefault() { return module['default']; } : /******/ function getModuleExports() { return module; }; /******/ __webpack_require__.d(getter, 'a', getter); /******/ return getter; /******/ }; /******/ /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = "/"; /******/ /******/ // on error function for async loading /******/ __webpack_require__.oe = function(err) { console.error(err); throw err; }; /******/ /******/ var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || []; /******/ var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); /******/ jsonpArray.push = webpackJsonpCallback; /******/ jsonpArray = jsonpArray.slice(); /******/ for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]); /******/ var parentJsonpFunction = oldJsonpFunction; /******/ /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 0); /******/ }) /************************************************************************/ /******/ ({ /***/ "./src/index.js": /*!**********************!* !*** ./src/index.js ***! **********************/ /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { __webpack_require__.e(/*! import() | foo */ "foo").then(__webpack_require__.bind(null, /*! ./foo */ "./src/foo.js")).then(function (_ref) { var foo = _ref.foo; console.log(foo); }); __webpack_require__.e(/*! import() | bar */ "bar").then(__webpack_require__.bind(null, /*! ./bar */ "./src/bar.js")).then(function (module) { console.log(module.default(), module.bar1()); }); /***/ }), /***/ 0: /*!****************************!* !*** multi ./src/index.js ***! ****************************/ /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { module.exports = __webpack_require__(/*! /Users/huruji/Documents/huruji/github/saso/examples/code-spliting/src/index.js */"./src/index.js"); /***/ }) /******/ });
简单看下就可以发现,加载模块的核心是一个名为 __webpack_require__.e
的方法,这个方法主要做的事情是
- 判断
chunk
是否被缓存过,没有缓存则加载模块,缓存过则直接resolve
- 动态创建
script
标签,onload
之后会返回一个Promise
,onerror
后直接使用reject
,最后会被添加到document.head
上
因为内部使用的是 Promise
所以同样我们也可以使用 async await
code spliting with vue
官方文档中的 异步组件 一章已经介绍了这种技术,只需要在注册组件的时候使用 import()
语法即可,一个简单的例子如下:
<template> <section> <div>app</div> <loading></loading> </section> </template> <script> export default { components: { loading: () => import("./Loading") } } </script>
除此之外,Vue 从 2.3 版本开始为了方便用户处理加载异步 chunk 时的 loading 状态和 error 状态,可以返回一个含有状态的对象,官方示例如下:
const AsyncComponent = () => ({ // 需要加载的组件 (应该是一个 `Promise` 对象) component: import('./MyComponent.vue'), // 异步组件加载时使用的组件 loading: LoadingComponent, // 加载失败时使用的组件 error: ErrorComponent, // 展示加载时组件的延时时间。默认值是 200 (毫秒) delay: 200, // 如果提供了超时时间且组件加载也超时了, // 则使用加载失败时使用的组件。默认值是:`Infinity` timeout: 3000 })
一个简单的栗子,我们模拟 5 秒的 loading 状态,如下:
<template> <section> <div>app</div> <loading></loading> </section> </template> <script> import Loading from "./Loading" import Error from "./Error" export default { components: { loading: () => ({ component: (() => { return new Promise(resolve => { setTimeout(() => { import("./List").then(resolve) }, 5000) }) })(), loading: Loading, error: Error }) } } </script>
code spliting with react
React 的 code spliting 的实现类似,需要使用 React.lazy
方法处理异步的组件,同时在 jsx 中需要使用 react 内置的 Suspense
组件包裹异步组件,同时可以指定 fallback
props 作为 loding 时的替代显示,如下:
import React from 'react' const Suspense = React.Suspense import Loading from './Loading' import Error from './Error' const List = React.lazy( () => new Promise((resolve, reject) => { setTimeout(() => { import('./List').then(resolve) }, 5000) }) ) export default class App extends React.Component { render() { return ( <div> <div>app</div> <Suspense fallback={<Loading />}> <List /> </Suspense> </div> ) } }
如果你需要对加载异步组件出错的情况做处理,你可以使用 react 的 Error Boundaries ,通过定义一个实现了 getDerivedStateFromError
静态方法的 react 组件,并且包裹相应的异步组件即可
import React from 'react' export default class Error extends React.Component { constructor(props) { super(props) this.state = { hasError: false } } static getDerivedStateFromError(error) { return { hasError: true } } render() { if (this.state.hasError) { return <h1>Something went wrong.</h1> } return this.props.children } }
父组件修改为
import React from 'react' const Suspense = React.Suspense import Loading from './Loading' import Error from './Error' const List = React.lazy( () => new Promise((resolve, reject) => { setTimeout(() => { import('./List').then(() => { reject() }) }, 5000) }) ) export default class App extends React.Component { render() { return ( <div> <div>app</div> <Error> <Suspense fallback={<Loading />}> <List /> </Suspense> </Error> </div> ) } }
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论