代码分片可以让你把应用分成多个包, 使你的用户能逐步加载应用而变得流行起来在这篇文章中, 我们将会看一下什么是代码分片和怎么去做, 了解怎么去配合 React Router 去实现它
现在是 2018 年你的用户不需要为了一小块内容而去下载整个应用如果一个用户下载所有的代码, 仅仅是为了请求一个注册页面是毫无意义的而且用户在注册时并不需要下载用户设置页的巨大富文本编辑器代码如果要下载那么多内容的话, 是很浪费的而且对于一些用户, 他们会抱怨不尊重没有特别好带宽的他们这个点子近年不仅很热, 而且实现难度以指数级降低甚至还有有了一个很酷的名字, 代码分片
这个点子很简单, 即按需加载实践的话, 它可能有一点复杂而复杂的原因并不是代码分片本身, 而是现在有各种各样的工具来做这个事情而且每个人对哪个方式最好都有自己的看法当你第一次开始着手的时候, 可能很困难分析什么是什么
最常见的两种做法是使用 webpack 和它的包加载器 (bundle-loader), 或者使用 ECMAScript 的 stage3 提案的动态 import() 任何机会不用 Webpack, 我就不用, 因此在这篇文章中, 我将会使用动态 import()
如果你很熟悉 ES 模块, 你应该知道它们是静态的意思就是说你必须在编译时确定你要引入和导出的内容, 而不是运行时这也意味着你不能基于一些条件来动态导入一个模块导入的内容必须声明在文件的最开头否则会抛出一个错误
- if (!user) {
- import * as api from './api' // 不能这样做, import 和 export 只能出现在文件顶部
- }
现在, 如果 import 不需要是静态的怎么办? 意味着上面的代码可以工作? 将会给我们带来什么好处? 首先这意味着我可以按着需要加载某个模块这非常强大, 它让我们更接近按用户需要下载代码的想象
- if (editPost === true) {
- import * as edit from './editpost'
- edit.showEditor()
- }
假设__editpost__包含一个非常大的富文本编辑器, 我们需要保证用户在没有使用它的时候不会去下载它
另外一个很酷的例子用于遗留支持你可以在浏览器确定确实没有的时候才下载对应代码
好消息 (我在上文中曾间接提及) 这种类型的方法确实存在, 它被 Create React App(React 项目的一种官方创建方法)支持, 而且它是 ECMAScript stage3 的提案不同的是替换了你之前使用 import 的方式它使用起来像一个方法, 并返回一个 Promise, 一旦模块完全加载, 就会把这个模块 resolve 回来
- if (editPost === true) {
- import('./editpost')
- .then(module => module.showEditor())
- .catch(e =>)
- }
特别好, 对吧?
现在我们知道怎么动态引入模块了, 下一步是找出怎么结合 React 和 React Router 来使用它
第一个 (可能是最大的一个) 问题, 我们对 React 代码分片时, 我们应该对哪里进行分片? 典型的回答有两个
在路由的层次上分片
在组件的层次上分片
而更加常见的做法是在路由的层次上进行分片你已经把你的应用分成了不同的路由, 因此根据这个来代码分片是自然而然的事情
让我以一个简单的 React Router 例子开始我们将有三条路由分别是: /,/topics,/settings
- import React, { Component } from 'react'
- import {
- BrowserRouter as Router,
- Route,
- Link,
- } from 'react-router-dom'
- import Home from './Home'
- import Topics from './Topics'
- import Settings from './Settings'
- class App extends Component {
- render() {
- return (
- <Router>
- <div>
- <ul>
- <li><Link to='/'>Home</Link></li>
- <li><Link to='/topics'>Topics</Link></li>
- <li><Link to='/settings'>Settings</Link></li>
- </ul>
- <hr />
- <Route exact path='/' component={Home} />
- <Route exact path='/topics' component={Topics} />
- <Route exact path='/settings' component={Settings} />
- </div>
- </Router>
- )
- }
- }
- export default App
现在, 假设我们的__/settings__路由内容非常多它包含一个富文本编辑器, 和一个原始超级马里奥兄弟的拷贝, 和盖伊法利的高清图片当用户不在__/settings__路由上时, 我们不想让他们下载全部这些内容让我们使用我们 React 和动态引入 (import()) 的知识来分片__/settings__路由
就像我们在 React 里解决任何问题一样, 我们先写一个组件我们将叫它__DynamicImport__这个组件的目的是动态的加载一个模块, 只要模块加载好了, 就把它传给它子节点(children)
- const Settings = (props) => (
- <DynamicImport load={() => import('./Settings')}>
- {(Component) => Component === null
- ? <Loading />
- : <Component {...props} />}
- </DynamicImport>
- )
上面的代码告诉我们两个重要的要素第一, 这个组件在执行时会接受一个属性__load__, 将使用我们前面提到的语法动态引入一个模块第二, 这个组件会接受一个函数作为他的子节点, 这个函数需要和引入进来的模块一起调用
在我们深入思考__DynamicImport__的实现的之前, 让我们想一下我们会怎么实现第一件事我们需要确定的是要调用 props.load 这让我们返回一个 Promise, 当它 resolve 的时候应该返回模块接着, 一旦我们有了模块, 我们需要一种方式去触发重渲染, 因此我们要把模块传给 props.children 并且调用它怎样在 React 里面触发重渲染呢? 设置 state(setState)通过把动态引入的模块加入到__DynamicImport__的 state 里面, 就像我们之前使用的一样, 我们遵循和 React 同样的过程 - 获取数据 -> 设置到 state 里 -> 重渲染而这一次我们只是把获取数据替换成了引入模块
好了, 首先, 让我们加入初始的状态到组件里
- class DynamicImport extends Component {
- state = {
- component: null
- }
- }
现在, 我们需要调 props.load 方法这将返回一个 promise 同时在 resolve 后有一个模块
- class DynamicImport extends Component {
- state = {
- component: null
- }
- componentWillMount () {
- this.props.load()
- .then(component => {
this.setState(() =>{
- component
- )}
- })
- }
- }
这里有一个疑难杂症如果我们 ES 模块和 commonjs 模块混用时, ES 模块会有一个. default 属性, 而 commonjs 模块并没有让我们改变一下代码, 适应一下上面的情况
- this.props.load()
- .then(component => {
- this.setState(() => {
- component: component.default ?
- component.default : component
- })
- })
- })
现在我们动态引入的模块并且把它加入到了 state 里面, 最后一件事就是 render 方法长什么样了如果你会记得, 当__DynamicImport__使用的时候, 它看起来像这样
- const Settings = (props) => (
- <DynamicImport load={() => import('./Settings')}>
- {(Component) => Component === null
? <Loading/>
- : <Component {...props} />}
- </DynamicImport>
- )
注意我们给组件传了一个函数作为子节点这意味着我们需要执行这个函数, 传递的是这个引入在 state 里的组件
- class DynamicImport extends Component {
- state = {
- component: null
- }
- componentWillMount () {
- this.props.load()
- .then((component) => {
- this.setState({
- component: component.default
- ? component.default
- : component
- })
- })
- }
- render() {
- return this.props.children(this.state.component)
- }
- }
欧了, 现在任何时候我们动态引入一个模块, 我们可以把它包裹在__DynamicImport__如果我们之前尝试用这种方法到我们路由上, 我们的代码会看起来像这样
- import React, { Component } from 'react'
- import {
- BrowserRouter as Router,
- Route,
- Link
- } from 'react-router-dom'
- class DynamicImport extends Component {
- state = {
- component: null
- }
- componentWillMount () {
- this.props.load()
- .then((component) => {
- this.setState({
- component: component.default
- ? component.default
- : component
- })
- })
- }
- render() {
- return this.props.children(this.state.component)
- }
- }
- const Home = (props) => (
- <DynamicImport load={() => import('./Home')}>
- {(Component) => Component === null
- ? <p>Loading</p>
- : <Component {...props} />
- }
- </DynamicImport>
- )
- const Topics = (props) => (
- <DynamicImport load={() => import('./Settings')}>
- {(Component) => Component === null
- ? <p>Loading</p>
- : <Component {...props}/>
- }
- </DynamicImport>
- )
- class App extends Component {
- render() {
- return (
- <Router>
- <div>
- <ul>
- <li><Link to='/'>Home</Link></li>
- <li><Link to='/topics'>Topics</Link></li>
- <li><Link to='/settings'>Settings</Link></li>
- </ul>
- <hr />
- <Route exact path='/' component={Home} />
- <Route path='/topics' component={Topics} />
- <Route path='/settings' component={Settings} />
- </div>
- </Router>
- )
- }
- }
- export default App
我们怎么知道这个确实起作用并且分片了我们的路由呢? 如果你用一个 React 官方的 Create React App 创建一个应用跑一下__npm run build__, 你将看到应用被分片了
每一个包被一一引入进了我们的应用
你到了这一步, 可以跳个舞轻松一下了
还记得我讲到有两种层级的代码分片方式吗? 我们曾放在手边的引导
以路由层级分片
以组建层级分片
至此, 我们只讲了路由层级的代码分片到这里很多人就停止了在路由层级上代码分片, 就像刷牙一样, 你天天刷, 牙齿大部分很干净, 但是还会有蛀牙
除了思考用路由的分片方式, 你应该想想怎么用组件的方式去分片如果你在弹层里面有很多内容, 路由分片还是会下载弹层的代码, 无论这个弹层是否显示
从这一点看, 它更多是在你大脑里的一种变更而不是新知识你已经知道如何使用动态引入, 现在你需要找出哪些组件是在用到时才要下载的
如果我不提 React Loadable 那我就是哑巴了它是一个通过动态引入加载组件的高阶组件重要的是, 它处理所有我们提到的事情, 并把它做成了一个精致的 API 它甚至处理了很多很边角的事情, 比如我们没有考虑服务端渲染和错误处理看看它吧, 如果你想要一个简单, 开箱即用的解决方案的话
(完, 逃)
来源: https://juejin.im/post/5aafad256fb9a028b86dd393