Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webpack之分割代码按需加载 #12

Open
liujie2019 opened this issue Feb 24, 2019 · 0 comments
Open

webpack之分割代码按需加载 #12

liujie2019 opened this issue Feb 24, 2019 · 0 comments

Comments

@liujie2019
Copy link
Owner

1. 为什么需要按需加载

随着互联网的发展,一个网页需要承载的功能越来越多。采用单页应用作为前端架构的网站会面临着网页需要加载的代码量很大的问题,因为许多功能都被集中做到了一个HTML里。这会导致网页加载缓慢、交互卡顿,使用户体验非常糟糕

导致这个问题的根本原因在于:一次性加载所有功能对应的代码,但其实用户在每个阶段只可能使用其中一部分功能。所以解决以上问题的方法就是用户当前需要用什么功能就只加载这个功能对应的代码,也就是所谓的按需加载

2. 如何使用按需加载

在为单页应用做按需加载优化时,一般采用以下原则:

  • 将整个网站划分成一个个小功能,再按照每个功能的相关程度把它们分成几类。
  • 将每一类合并为一个Chunk,按需加载对应的Chunk
  • 对于用户首次打开网站时需要看到的画面所对应的功能,不要对它们做按需加载,而是放到执行入口所在的Chunk中,以降低用户能感知的网页加载时间。
  • 对于不依赖大量代码的功能点,例如依赖Chart.js去画图表、依赖flv.js去播放视频的功能点,可再对其进行按需加载。

被分割出去的代码的加载需要一定的时机去触发,即当用户操作到了或者即将操作到对应的功能时再去加载对应的代码。 被分割出去的代码的加载时机需要开发者自己去根据网页的需求去衡量和确定。

由于被分割出去进行按需加载的代码在加载的过程中也需要耗时,所以可以预估用户接下来可能会进行的操作,并提前加载好对应的代码,从而让用户感知不到网络加载时间。

3. 用Webpack实现按需加载

Webpack内置了强大的分割代码的功能去实现按需加载,实现起来非常简单。举个例子,现在需要做这样一个进行了按需加载优化的网页:

  • 网页首次加载时只加载main.js文件,网页会展示一个按钮,在main.js文件中只包含监听按钮事件和加载按需加载的代码
  • 在按钮被点击时才去加载被分割出去的show.js文件,在加载成功后再执行 show.js里的函数。

其中main.js文件内容如下:

window.document.getElementById('btn').addEventListener('click', function () {
  // 当按钮被点击后才去加载 show.js 文件,文件加载成功后执行文件导出的函数
  import(/* webpackChunkName: "show" */ './show').then((show) => {
    show('Webpack');
  })
});

show.js文件内容如下:

module.exports = function (content) {
  window.alert('Hello ' + content);
};

代码中最关键的一句是:

import(/* webpackChunkName: "show" */ './show')

Webpack内置了对import(*)语句的支持,当Webpack遇到了类似的语句时会这样处理:

  • ./show.js为入口新生成一个Chunk
  • 当代码执行到import所在的语句时才会去加载由Chunk对应生成的文件。
  • import返回一个Promise,当文件加载成功时可以在Promisethen 方法中获取到show.js导出的内容。

在使用import()分割代码后,浏览器要支持Promise API才能让代码正常运行,因为import()返回一个Promise,它依赖Promise。对于不原生支持 Promise的浏览器,可以注入Promise polyfill

/* webpackChunkName: "show" */的含义是:为动态生成的Chunk赋予一个名称,以方便我们追踪和调试代码。 如果不指定动态生成的Chunk的名称,则其默认的名称将会是[id].js/* webpackChunkName: "show" */是在Webpack3中引入的新特性,在Webpack3之前是无法为动态生成的Chunk赋予名称的。

为了正确输出在/ webpackChunkName: “show” /中配置的ChunkName,还需要配置下Webpack,具体配置如下:

module.exports = {
  // JS 执行入口文件
  entry: {
    main: './main.js',
  },
  output: {
    // 为从entry中配置生成的Chunk配置输出文件的名称
    filename: '[name].js',
    // 为动态加载的Chunk配置输出文件的名称
    chunkFilename: '[name].js',
  }
};

其中,最关键的一行是chunkFilename: '[name].js',,它专门指定动态生成的 Chunk在输出时的文件名称。如果没有这一行,则分割出的代码的文件名称将会是 [id].js

4. 按需加载与ReactRouter

在实战中,不可能会有上面那么简单的场景,接下来举一个实战中的例子:对采用了 ReactRouter的应用进行按需加载优化。这个例子由一个单页应用构成,这个单页应用由两个子页面构成,通过ReactRouter在两个子页面之间切换和管理路由。

这个单页应用的入口文件main.js如下:

import React, { PureComponent, createElement } from 'react';
import {render} from 'react-dom';
import {HashRouter, Route, Link} from 'react-router-dom';
import PageHome from './pages/home';

/**
 * 异步加载组件
 * @param load 组件加载函数,load 函数会返回一个 Promise,在文件加载完成时 resolve
 * @returns {AsyncComponent} 返回一个高阶组件用于封装需要异步加载的组件
 */
function getAsyncComponent(load) {
  return class AsyncComponent extends PureComponent {

    componentDidMount() {
      // 在高阶组件 DidMount 时才去执行网络加载步骤
      load().then(({default: component}) => {
        // 代码加载成功,获取到了代码导出的值,调用 setState 通知高阶组件重新渲染子组件
        this.setState({
          component,
        })
      });
    }

    render() {
      const {component} = this.state || {};
      // component 是 React.Component 类型,需要通过 React.createElement 生产一个组件实例
      return component ? createElement(component) : null;
    }
  }
}

// 根组件
function App() {
  return (
    <HashRouter>
      <div>
        <nav>
          <Link to='/'>Home</Link> | <Link to='/about'>About</Link> | <Link to='/login'>Login</Link>
        </nav>
        <hr/>
        <Route exact path='/' component={PageHome}/>
        <Route path='/about' component={getAsyncComponent(
          // 异步加载函数,异步地加载 PageAbout 组件
          () => import(/* webpackChunkName: 'page-about' */'./pages/about')
        )}
        />
        <Route path='/login' component={getAsyncComponent(
          // 异步加载函数,异步地加载 PageAbout 组件
          () => import(/* webpackChunkName: 'page-login' */'./pages/login')
        )}
        />
      </div>
    </HashRouter>
  )
}

// 渲染根组件
render(<App/>, window.document.getElementById('app'));

以上代码中最关键的部分是getAsyncComponent函数,它的作用是配合 ReactRouter去按需加载组件,具体含义请看代码中的注释。

由于以上源码需要通过Babel去转换后才能在浏览器中正常运行,需要在Webpack 中配置好对应的babel-loader,源码先交给babel-loader处理后再交给 Webpack去处理其中的import(*)语句。

但这样做后你很快会发现一个问题:Babel报出错误说不认识import(*)语法。 导致这个问题的原因是:import(*)语法还没有被加入到在使用ES6语言中提到的 ECMAScript标准中去,为此我们需要安装一个Babel插件babel-plugin-syntax-dynamic-import,并且将其加入到.babelrc中去:

{
  "presets": [
    "env",
    "react"
  ],
  "plugins": [
    "syntax-dynamic-import"
  ]
}

执行Webpack构建后,你会发现输出了三个文件:

  • main.js:执行入口所在的代码块,同时还包括PageHome所需的代码,因为用户首次打开网页时就需要看到PageHome的内容,所以不对其进行按需加载,以降低用户能感知到的加载时间;
  • page-about.js:当用户访问/about时才会加载的代码块;
  • page-login.js:当用户访问/login时才会加载的代码块。

同时我们还会发现,page-about.js和page-login.js这两个文件在首页是不会加载的,而是会在当你切换到了对应的子页面后文件才会开始加载。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Development

No branches or pull requests

1 participant