Webpack 结合 React-Router 实现按需加载

对于大型的 Web 应用来说,如果将所有的代码都放在一个文件中,然后一次性加载,这对于页面的性能来说可能存在问题,特别是当很多代码需要满足一定的条件才需要加载的情况下。Webpack 可以允许将代码分割成为不同的 chunk,然后按需加载这些 chunk,这种特性就是我们常说的 code splitting。在本章节中会主要论述 Webpack 与 React-Router 一起实现按需加载的内容。其中包括如何针对开发环境和生产环境配置不同的 webpack.config.js 内容以及 Webpack 按需加载的表现,通过本章节的学习应该能够学会如何实现按需加载,以及如何使用该特性提升首页加载性能。

开发环境搭建

配置 webpack.config.js

配置如下的 webpack.config.js:

//webpack.config.js
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = env => {
  const ifProd = plugin =>  env.prod ? plugin : undefined;
  const removeEmpty = array => array.filter(p => !!p);
  return {
    entry: {
      app: path.join(__dirname, '../src/'),
      vendor: ['react', 'react-dom', 'react-router'],
    },
    output: {
      filename: '[name].[hash].js',
      path: path.join(__dirname, '../build/'),
    },
    module: {
      loaders: [
        {
          test: /\.(js)$/, 
          exclude: /node_modules/, 
          loader: 'babel-loader', 
          query: {
            cacheDirectory: true,
          },
        },
      ],
    },
    plugins: removeEmpty([
      new webpack.optimize.CommonsChunkPlugin({
        name: 'vendor',
        minChunks: Infinity,
        filename: '[name].[hash].js',
      }),
      new HtmlWebpackPlugin({
        template: path.join(__dirname, '../src/index.html'),
        filename: 'index.html',
        inject: 'body',
        hash:true
      }),
      ifProd(new webpack.optimize.DedupePlugin()),
      ifProd(new webpack.optimize.UglifyJsPlugin({
        compress: {
          'screw_ie8': true,
          'warnings': false,
          'unused': true,
          'dead_code': true,
        },
        output: {
          comments: false,
        },
        sourceMap: false,
      })),
    ]),
  };
};

首先应该关注如下的方法:

const ifProd = plugin =>  env.prod ? plugin : undefined;

这个方法表示,只有在生产模式下才会添加特定的插件,如果不是在生产模式下,那么给插件就不需要添加。比如上面的 UglifyJsPlugin 插件压缩代码,在开发模式下是不需要的,只有在项目上线以后才需要将 js/css 代码进行压缩。假如现在处于开发阶段,那么一般是需要启动 webpack-dev-server 的,来看看如何对 webpack-dev-server 进行配置:

const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const webpackConfig = require('./webpack.config');
const path = require('path');
const env = {dev: process.env.NODE_ENV };
const devServerConfig = {
  contentBase: path.join(__dirname, '../build/'),
  historyApiFallback: { disableDotRule: true },
  stats: { colors: true }
};
const server = new WebpackDevServer(webpack(webpackConfig(env)), devServerConfig);
server.listen(3000, 'localhost');
注意:上面直接调用 webpack 方法会得到一个 Compiler 对象,webpack-dev-server 的很多功能,比如 HMR 都是基于这个对象来完成的,包括从内存中拿到资源来处理请求(参考 webpack-dev-server 的 lazyload 部分)。此时我们直接调用 listen 方法来完成 webpack-dev-server 的启动。

配置 package.json 的 script

"scripts": {
    "start": "NODE_ENV=development node webpack/webpack-dev-server --env.dev",
    "build": "rm -rf build/* | NODE_ENV=production webpack --config webpack/webpack.config.js --progress --env.prod"
  },

此时可以通过下面简单的命令来替换复杂的 webpack 命令(参数很长):

npm start
//或者 npm run start
npm run build
//相当于 rm -rf build/* | NODE_ENV=production webpack --config webpack/webpack.config.js --progress --env.prod

此时应该注意到了,在特定的命令后面,比如 start 命令后面会有 env.dev,而 build 后会存在 env.prod,所以,我们可以在 webpack.config.js 中通过 env 对象判断当前所处的环境,从而在生产模式下添加特定的 webpack 插件。比如上面说的 UglifyJsPlugin 或者 DedupePlugin 等等。此时运行 npm start 就可以启动服务器,在浏览器中打开 http://localhost:3000 就可以看到当前的页面了。

入口文件分析

在 Webpack 中一个重要的概念就是入口文件,通过入口文件可以构建前面章节所说的模块依赖图谱。来看看上面的入口文件的内容:

//src/index.js
import React from 'react';
import { render } from 'react-dom';
import Root from './root';
render(<Root />, document.getElementById('App'));

而 root.js 中的内容如下:

import React from 'react';
import Router from 'react-router/lib/Router';
import browserHistory from 'react-router/lib/browserHistory';
import routes from './routes';
const Root = () => <Router history={browserHistory} routes={routes} />;
export default Root;

此时,在 Router 组件中的 routes 配置就是指的前端路由对象,而这也是 Webpack 结合 React-Router 实现按需加载的核心代码,来看看真实的代码结构:

import Core from './components/Core';
function errorLoading(error) {
  throw new Error(`Dynamic page loading failed: ${error}`);
}
function loadRoute(cb) {
  return module => cb(null, module.default);
}
export default {
  path: '/',
  component: Core,
  indexRoute: {
    getComponent(location, cb) {
      System.import('./components/Home')
        .then(loadRoute(cb))
        .catch(errorLoading);
    },
  },
  childRoutes: [
    {
      path: 'about',
      getComponent(location, cb) {
        System.import('./components/About')
          .then(loadRoute(cb))
          .catch(errorLoading);
      },
    },
    {
      path: 'users',
      getComponent(location, cb) {
        System.import('./components/Users')
          .then(loadRoute(cb))
          .catch(errorLoading);
      },
    },
    {
      path: '*',
      getComponent(location, cb) {
        System.import('./components/Home')
          .then(loadRoute(cb))
          .catch(errorLoading);
      },
    },
  ],
};
注意:上面的例子使用的是 react-router 的对象配置方式,它的作用和下面的配置是一样的
<Route path="/" component={Core}>
    <IndexRoute component={Home}/>
   <Route path="about" component={About}/>
    <Route path="users" component={Users}>
    <Route path="*" component={Home}/>
 </Route>

其中,最重要的代码就是上面看到的 System.import,它和 require.ensure 方法是一样的,这部分内容在 webpack1 中就已经引入了。比如上面的配置:

{
      path: 'users',
      getComponent(location, cb) {
        System.import('./components/Users')
          .then(loadRoute(cb))
          .catch(errorLoading);
      },
  }

表示如果路由满足 /users 的时候就会动态加载 components 下的 Users 组件,而且 Users 组件的内容是不会和入口文件打包在一起的,而是会单独打包到一个独立的 chunk 中的,只有这样才能实现按需加载的功能。而且针对上面的 loadRoute 方法也做一个说明:

function loadRoute(cb) {
  return module => cb(null, module.default);
}

其中加载 module.default 是因为 ES6 的模块机制导致的,可以查看导出的模块内容:

//Users.js
import React from 'react';
const Users = () => <div>Users</div>;
export default Users;

其实是通过 export default 来完成的,如果引入了 babel-plugin-add-module-export 就不需要这样处理了,可以参考 __esModule是什么?。如果要将上面的代码 users 路由修改为 require.ensure 加载,可以使用如下方式:

<Route path="users" getComponent={(location, cb) => {
          require.ensure([], require => {
              cb(null, require('./components/Users').default)
          },'users');
        }} />
  </Route>

注意:require.ensure的签名如下:

require.ensure(dependencies, callback, chunkName)

所以,我们通过第三个参数可以指定该 chunk 的名称,如果不指定该 chunk 的名称,将获得下面的 0.xx、1.xx 这种 Webpack 自动分配的 chunk 名称。
依赖图谱分析
当使用了 code splitting 特性,可以使用很多工具来查看每一个 chunk 中都包含了什么特定的模块。比如我常用的这个

webpack 官方分析工具。下面是我使用了这个工具查看本章节例子中的 stats.json 得到的依赖图谱。

enter image description here

通过这个图谱,可以看到很多内容。比如其中的 entry 因为含有 webpack 的特定加载环境,所以需要在所有的 chunk 加载之前加载,这部分内容在前面章节也已经讲过;而 initial 部分表示在 webpack.config.js 中配置的入口文件。其他的 id 为 0/1/2 的 chunk 就是动态产生的 chunk,比如通过 System.import 或者 require.ensure 产生的动态的模块。

还有一点就是其中的 names 列,因为我们调用 require.ensure 的时候并没有指定当前的 chunk 的名称,即第三个参数,所以 names 就是为空数组。而且很多如 assets、modules、warnings、errors 等信息都可以在这个页面进行查看,此处不再赘述。当然,也可以使用第三方的工具来查看我们的 stats.json 的内容。

按需加载的表现

当页面初始加载的时候会看到下面的内容:


enter image description here

其中 vendor.js 应该很好理解,就是包含上面配置的框架代码:

 vendor: ['react', 'react-dom', 'react-router'],

这部分如果不理解,可以回到前面章节进行复习。而 app.js 就是入口文件内容,即不包含动态加载的模块的内容。而另外一个0.xxxx的 chunk 就是我们上面配置的:

indexRoute: {
    getComponent(location, cb) {
      System.import('./components/Home')
        .then(loadRoute(cb))
        .catch(errorLoading);
    },
  }

因为 indexRoute 表示默认初始化的子组件。而当你访问localhost:3000/about的时候将会看到下面的内容:

enter image description here

其中2.xx的内容就是上面配置的about组件的内容:

{
      path: 'about',
      getComponent(location, cb) {
        System.import('./components/About')
          .then(loadRoute(cb))
          .catch(errorLoading);
      },
    }

而当访问localhost:3000/users的时候将会看到下面的内容:

enter image description here

其中1.xx就是上面配置的如下内容:

{
      path: 'users',
      getComponent(location, cb) {
        System.import('./components/Users')
          .then(loadRoute(cb))
          .catch(errorLoading);
      },
},

所以,按需加载的表现就是:当访问特定的路由的时候才会加载特定的模块,而不会将所有的代码一股脑的一次性全部加载进来。这对于优化首页加载的速度是很好的方案。同时,如果在上面运行的是 npm run start,那么在相应的目录下会看不到输出的文件,这是因为此时启动的是 webpack-dev-server,而 webpack-dev-server 会将输出的内容直接写出到内存中,而不是写到具体的文件系统中。但是如果运行的是

npm run build

,会发现文件会写到文件系统中。来看看在 Webpack 中如何指定自己的输出结果到底是内存还是具体的文件系统:

const MemoryFS = require("memory-fs");
const webpack = require("webpack");
const fs = new MemoryFS();
const compiler = webpack({ /* options*/ });
compiler.outputFileSystem = fs;
compiler.run((err, stats) => {
  // Read the output later:
  const content = fs.readFileSync("...");
});

通过 Webpack 的官方实例会发现,其实只要我们指定了 compiler.outFileSystem 为 MemoryFS 实例,那么在 run/watch 等方法中就可以通过相应的方法从内存中读取文件而不是文件系统了。这种方式在开发模式下还是很有用的,但是在生产模式下建议不要使用。

本章总结

通过本章节的学习,应该对于 webpack+react-router 实现按需加载方案有了一个总体的认识。其实现的核心是通过 require.ensure 或者 System.import 来完成的。其中,本实例的完整代码可以点击这里查看

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

推荐阅读更多精彩内容

  • GitChat技术杂谈 前言 本文较长,为了节省你的阅读时间,在文前列写作思路如下: 什么是 webpack,它要...
    萧玄辞阅读 12,674评论 7 110
  • 1. 前言 随着前端项目的不断扩大,一个原本简单的网页应用所引用的js文件可能变得越来越庞大。尤其在近期流行的单页...
    cbw100阅读 2,180评论 2 8
  • 版权声明:本文为博主原创文章,未经博主允许不得转载。 webpack介绍和使用 一、webpack介绍 1、由来 ...
    it筱竹阅读 11,028评论 0 21
  • 目录第1章 webpack简介 11.1 webpack是什么? 11.2 官网地址 21.3 为什么使用 web...
    lemonzoey阅读 1,731评论 0 1
  • webpack 介绍 webpack 是什么 为什么引入新的打包工具 webpack 核心思想 webpack 安...
    yxsGert阅读 6,450评论 2 71