Webpack构建优化—按需加载

为什么需要按需加载

随着互联网的发展,一个网页需要承载的功能越来越多。 对于采用单页应用作为前端架构的网站来说,会面临着一个网页需要加载的代码量很大的问题,因为许多功能都集中的做到了一个HTML里。 这会导致网页加载缓慢、交互卡顿,用户体验将非常糟糕。
导致这个问题的根本原因在于一次性的加载所有功能对应的代码,但其实用户每一阶段只可能使用其中一部分功能。 所以解决以上问题的方法就是用户当前需要用什么功能就只加载这个功能对应的代码,也就是所谓的按需加载。

如何使用按需加载

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

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

被分割出去的代码的加载需要一定的时机去触发,也就是当用户操作到了或者即将操作到对应的功能时再去加载对应的代码。 被分割出去的代码的加载时机需要开发者自己去根据网页的需求去衡量和确定。
由于被分割出去进行按需加载的代码在加载的过程中也需要耗时,你可以预言用户接下来可能会进行的操作,并提前加载好对应的代码,从而让用户感知不到网络加载时间。

用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

按需加载与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(*)语法还没有被加入到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.jspage-login.js这两个文件在首页是不会加载的,而是会当你切换到了对应的子页面后文件才会开始加载。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352

推荐阅读更多精彩内容

  • GitChat技术杂谈 前言 本文较长,为了节省你的阅读时间,在文前列写作思路如下: 什么是 webpack,它要...
    萧玄辞阅读 12,685评论 7 110
  • webpack 优化主要分为两部分,一是优化构建速度,二是优化输出质量。所谓优化构建速度,那就是要打包快,优化输出...
    DJL箫氏阅读 1,682评论 0 3
  • 作者:小 boy (沪江前端开发工程师)本文原创,转载请注明作者及出处。原文地址:https://www.smas...
    iKcamp阅读 2,752评论 0 18
  • 你真的了解前端模块化么? 告别「webpack配置工程师」 webpack是一个强大而复杂的前端自动化工具。其中一...
    AlienZHOU阅读 9,128评论 2 16
  • 壹 梅花又纷飞落下。这是分别后的第七年。 每年的今天,我都会到此赏梅。今年亦如是。 ...
    Mkkkk阅读 472评论 0 0