功能越来越多,资源越来越大,特别是如果引用了体积较大的第三方库的时候,是时候做一下代码分割了
按需加载是刚需
❀ react-loadable
import React, { useState } from 'react';import Loadable from 'react-loadable'const LoadableComponent = Loadable({ loader: () => import('./comp.js'), loading: (props) => { if (props.error || props.timedOut) { return <button onClick={props.retry}>retrybutton> } return <div>loadingdiv> }})export default function () { const [show, setShow] = useState(false) return ( <div> <button onClick={() => setShow(!show)}>LazyLoadbutton> {show ? (<LoadableComponent />) : null} div> );};
❀ 也可以自己写
这是个很小的库,简单翻译过来是这样子的
import React, { useState } from 'react';const loader = () => import('./comp.js')class LoadableComponent extends React.Component { constructor(props) { super(props) this.state = { comp: null, err: null } } componentDidMount() { this.loadModule() } loadModule() { loader().then((obj) => { this.setState({ comp: obj && obj.__esModule ? obj.default : obj }) }).catch((err) => { this.setState({ err }) }) } render() { if (this.state.err) { return <div>errdiv> } else if (this.state.comp) { return React.createElement(this.state.comp, this.props) } return <div>Loading ...div> }}export default function () { const [show, setShow] = useState(false) return ( <div> <button onClick={() => setShow(!show)}>LazyLoadbutton> {show ? (<LoadableComponent />) : null} div> );};
❀ Lazy + Suspense
从 React16.6 开始支持
import React, { useState } from 'react';const LazyComponent = React.lazy(() => import('./comp.js'))class ErrorBoundary extends React.Component { constructor(props) { super(props) this.state = { error: null } } componentDidCatch(error) { this.setState({ error }) } render() { return this.state.error ? (<div>errordiv>) : this.props.children }}const SuspenseComponent = function () { return ( <ErrorBoundary> <React.Suspense fallback={(<div>loadingdiv>)}> <LazyComponent /> React.Suspense> ErrorBoundary> )}export default function () { const [show, setShow] = useState(false) return ( <div> <button onClick={() => setShow(!show)}>LazyLoadbutton> {show ? (<SuspenseComponent />) : null} div> );};
❀ webpack 代码分割
https://webpack.js.org/guides/code-splitting/
Code Splitting 有多种方法
1. Entry Points
entry: { index: './src/index.js', another: './src/another-module.js',}
但是可能会有重复代码模块,所以通常用来做多入口的打包
entry: { index: { import: './src/index.js', dependOn: 'shared' }, another: { import: './src/another-module.js', dependOn: 'shared' }, shared: 'lodash',}
dependOn 可以指定依赖的模块,以此来避免打包时的代码重复
2. SplitChunksPlugin
把公共模块提取出来,添加到一个已有的 entry chunk 中,或者生成一个新的 chunk
entry: {...},optimization: { splitChunks: { chunks: 'all', },}
3. Dynamic Imports
3-1. 用 require.ensure 动态加载模块,是 webpack 特有的,已被 import() 取代
require.ensure( dependencies: String[], callback: function(require), errorCallback: function(error), chunkName: String)
dependencies 对应的文件会被拆分到一个单独的 bundle 中
3-2. 用 import() 动态加载模块, webpack 解析的时候会自动进行代码分割
import() 类型是 function(string path): Promise
因为是 Promise 类型,所以老版本浏览器要加上 es6-promise 或者 promise-polyfill
import("./comp.js").then((obj) => { console.log(obj);})
需要配置 @babel/plugin-syntax-dynamic-import 来支持
.babelrc{ "plugins": [ "@babel/plugin-syntax-dynamic-import", ... ], ...}
- import 'comp.js' 会被打到 mainChunk 里面去
- import('comp.js') 会被分割成单独的 chunk 文件,在需要的时候异步加载进来
- // 还可以指定 chunkName
- import(/* webpackChunkName: "comp" */ 'comp.js')
这个叫‘魔法注释’
https://webpack.js.org/api/module-methods/#import-1
const loader = () => import('./comp.js')
首先会由 './comp.js' 生成一个 chunk,上面那行编译之后会变成这样
const loader = () => __webpack_require__.e(/*! import() */ 1) .then(__webpack_require__.bind(null, /*! ./comp.js */ "./src/comp.js"));
__webpack_require__.e 的定义在 bundle.js 里面,根据 chunkId 异步加载模块,返回一个 promise
__webpack_require__.e = function requireEnsure(chunkId) { // 返回的是一个 promise var promises = []; var installedChunkData = installedChunks[chunkId]; // installedChunks 用来记录加载过的 chunks if (installedChunkData !== 0) { // 0 表示已经加载过了. 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'); script.src = jsonpScriptSrc(chunkId); // "static/js/1.chunk.js" // 超时或报错的回调 var onScriptComplete = function (event) { // avoid mem leaks in IE. script.onerror = script.onload = null; clearTimeout(timeout); var chunk = installedChunks[chunkId]; if (chunk !== 0) { if (chunk) { chunk[1](error); // reject(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);};
'./comp.js' 生成的 chunk 是这样的
(this["webpackJsonpmy-app"] = this["webpackJsonpmy-app"] || []).push([ [1], { "xxx1.js": (function (module, exports, __webpack_require__) { ... }), "xxx2.js": (function (module, exports, __webpack_require__) { ... })} ])
事实上所有的 chunk 都是这样的,含义就是一个一个的代码模块,而 bundle 是用来处理加载模块的
bundle 里面重新定义了 this["webpackJsonpmy-app"] 的 push 方法
var jsonpArray = this["webpackJsonpmy-app"] = this["webpackJsonpmy-app"] || [];var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);jsonpArray.push = webpackJsonpCallback;for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);var parentJsonpFunction = oldJsonpFunction;
webpackJsonpCallback 可以看做 chunk 加载完毕之后的回调,用来执行 requireEnsure 的时候注册的 resolve 方法,然后把模块内容注册到 modules 中以方便其他文件调用
function webpackJsonpCallback(data) { var chunkIds = data[0]; var moreModules = data[1]; var executeModules = data[2]; // add "moreModules" to the modules object, // then flag all "chunkIds" as loaded and fire callback var resolves = []; for (var i = 0; i < chunkIds.length; i++) { var chunkId = chunkIds[i]; if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) { resolves.push(installedChunks[chunkId][0]); // promise.resolve } installedChunks[chunkId] = 0; // 表示已加载完成,下次在 requireEnsure 就不会再重复获取了 } for (var moduleId in moreModules) { if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } } if (parentJsonpFunction) parentJsonpFunction(data); while (resolves.length) { resolves.shift()(); // 执行所有的 resolve } // add entry modules from loaded chunk to deferred list deferredModules.push.apply(deferredModules, executeModules || []); // run deferred modules when all chunks ready return checkDeferredModules();};
试一试byMe