Webpack 結(jié)合 React-Router 實現(xiàn)按需加載

對于大型的 Web 應(yīng)用來說,如果將所有的代碼都放在一個文件中,然后一次性加載,這對于頁面的性能來說可能存在問題,特別是當(dāng)很多代碼需要滿足一定的條件才需要加載的情況下。Webpack 可以允許將代碼分割成為不同的 chunk,然后按需加載這些 chunk,這種特性就是我們常說的 code splitting。在本章節(jié)中會主要論述 Webpack 與 React-Router 一起實現(xiàn)按需加載的內(nèi)容。其中包括如何針對開發(fā)環(huán)境和生產(chǎn)環(huán)境配置不同的 webpack.config.js 內(nèi)容以及 Webpack 按需加載的表現(xiàn),通過本章節(jié)的學(xué)習(xí)應(yīng)該能夠?qū)W會如何實現(xiàn)按需加載,以及如何使用該特性提升首頁加載性能。

開發(fā)環(huán)境搭建

配置 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,
      })),
    ]),
  };
};

首先應(yīng)該關(guān)注如下的方法:

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

這個方法表示,只有在生產(chǎn)模式下才會添加特定的插件,如果不是在生產(chǎn)模式下,那么給插件就不需要添加。比如上面的 UglifyJsPlugin 插件壓縮代碼,在開發(fā)模式下是不需要的,只有在項目上線以后才需要將 js/css 代碼進(jìn)行壓縮。假如現(xiàn)在處于開發(fā)階段,那么一般是需要啟動 webpack-dev-server 的,來看看如何對 webpack-dev-server 進(jìn)行配置:

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');
注意:上面直接調(diào)用 webpack 方法會得到一個 Compiler 對象,webpack-dev-server 的很多功能,比如 HMR 都是基于這個對象來完成的,包括從內(nèi)存中拿到資源來處理請求(參考 webpack-dev-server 的 lazyload 部分)。此時我們直接調(diào)用 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"
  },

此時可以通過下面簡單的命令來替換復(fù)雜的 webpack 命令(參數(shù)很長):

npm start
//或者 npm run start
npm run build
//相當(dāng)于 rm -rf build/* | NODE_ENV=production webpack --config webpack/webpack.config.js --progress --env.prod

此時應(yīng)該注意到了,在特定的命令后面,比如 start 命令后面會有 env.dev,而 build 后會存在 env.prod,所以,我們可以在 webpack.config.js 中通過 env 對象判斷當(dāng)前所處的環(huán)境,從而在生產(chǎn)模式下添加特定的 webpack 插件。比如上面說的 UglifyJsPlugin 或者 DedupePlugin 等等。此時運行 npm start 就可以啟動服務(wù)器,在瀏覽器中打開 http://localhost:3000 就可以看到當(dāng)前的頁面了。

入口文件分析

在 Webpack 中一個重要的概念就是入口文件,通過入口文件可以構(gòu)建前面章節(jié)所說的模塊依賴圖譜。來看看上面的入口文件的內(nèi)容:

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

而 root.js 中的內(nèi)容如下:

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 結(jié)合 React-Router 實現(xiàn)按需加載的核心代碼,來看看真實的代碼結(jié)構(gòu):

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 方法是一樣的,這部分內(nèi)容在 webpack1 中就已經(jīng)引入了。比如上面的配置:

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

表示如果路由滿足 /users 的時候就會動態(tài)加載 components 下的 Users 組件,而且 Users 組件的內(nèi)容是不會和入口文件打包在一起的,而是會單獨打包到一個獨立的 chunk 中的,只有這樣才能實現(xiàn)按需加載的功能。而且針對上面的 loadRoute 方法也做一個說明:

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

其中加載 module.default 是因為 ES6 的模塊機(jī)制導(dǎo)致的,可以查看導(dǎo)出的模塊內(nèi)容:

//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)

所以,我們通過第三個參數(shù)可以指定該 chunk 的名稱,如果不指定該 chunk 的名稱,將獲得下面的 0.xx、1.xx 這種 Webpack 自動分配的 chunk 名稱。
依賴圖譜分析
當(dāng)使用了 code splitting 特性,可以使用很多工具來查看每一個 chunk 中都包含了什么特定的模塊。比如我常用的這個

webpack 官方分析工具。下面是我使用了這個工具查看本章節(jié)例子中的 stats.json 得到的依賴圖譜。

enter image description here

通過這個圖譜,可以看到很多內(nèi)容。比如其中的 entry 因為含有 webpack 的特定加載環(huán)境,所以需要在所有的 chunk 加載之前加載,這部分內(nèi)容在前面章節(jié)也已經(jīng)講過;而 initial 部分表示在 webpack.config.js 中配置的入口文件。其他的 id 為 0/1/2 的 chunk 就是動態(tài)產(chǎn)生的 chunk,比如通過 System.import 或者 require.ensure 產(chǎn)生的動態(tài)的模塊。

還有一點就是其中的 names 列,因為我們調(diào)用 require.ensure 的時候并沒有指定當(dāng)前的 chunk 的名稱,即第三個參數(shù),所以 names 就是為空數(shù)組。而且很多如 assets、modules、warnings、errors 等信息都可以在這個頁面進(jìn)行查看,此處不再贅述。當(dāng)然,也可以使用第三方的工具來查看我們的 stats.json 的內(nèi)容。

按需加載的表現(xiàn)

當(dāng)頁面初始加載的時候會看到下面的內(nèi)容:


enter image description here

其中 vendor.js 應(yīng)該很好理解,就是包含上面配置的框架代碼:

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

這部分如果不理解,可以回到前面章節(jié)進(jìn)行復(fù)習(xí)。而 app.js 就是入口文件內(nèi)容,即不包含動態(tài)加載的模塊的內(nèi)容。而另外一個0.xxxx的 chunk 就是我們上面配置的:

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

因為 indexRoute 表示默認(rèn)初始化的子組件。而當(dāng)你訪問localhost:3000/about的時候?qū)吹较旅娴膬?nèi)容:

enter image description here

其中2.xx的內(nèi)容就是上面配置的about組件的內(nèi)容:

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

而當(dāng)訪問localhost:3000/users的時候?qū)吹较旅娴膬?nèi)容:

enter image description here

其中1.xx就是上面配置的如下內(nèi)容:

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

所以,按需加載的表現(xiàn)就是:當(dāng)訪問特定的路由的時候才會加載特定的模塊,而不會將所有的代碼一股腦的一次性全部加載進(jìn)來。這對于優(yōu)化首頁加載的速度是很好的方案。同時,如果在上面運行的是 npm run start,那么在相應(yīng)的目錄下會看不到輸出的文件,這是因為此時啟動的是 webpack-dev-server,而 webpack-dev-server 會將輸出的內(nèi)容直接寫出到內(nèi)存中,而不是寫到具體的文件系統(tǒng)中。但是如果運行的是

npm run build

,會發(fā)現(xiàn)文件會寫到文件系統(tǒng)中。來看看在 Webpack 中如何指定自己的輸出結(jié)果到底是內(nèi)存還是具體的文件系統(tǒng):

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 的官方實例會發(fā)現(xiàn),其實只要我們指定了 compiler.outFileSystem 為 MemoryFS 實例,那么在 run/watch 等方法中就可以通過相應(yīng)的方法從內(nèi)存中讀取文件而不是文件系統(tǒng)了。這種方式在開發(fā)模式下還是很有用的,但是在生產(chǎn)模式下建議不要使用。

本章總結(jié)

通過本章節(jié)的學(xué)習(xí),應(yīng)該對于 webpack+react-router 實現(xiàn)按需加載方案有了一個總體的認(rèn)識。其實現(xiàn)的核心是通過 require.ensure 或者 System.import 來完成的。其中,本實例的完整代碼可以點擊這里查看。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • GitChat技術(shù)雜談 前言 本文較長,為了節(jié)省你的閱讀時間,在文前列寫作思路如下: 什么是 webpack,它要...
    蕭玄辭閱讀 12,867評論 7 110
  • 1. 前言 隨著前端項目的不斷擴(kuò)大,一個原本簡單的網(wǎng)頁應(yīng)用所引用的js文件可能變得越來越龐大。尤其在近期流行的單頁...
    cbw100閱讀 2,243評論 2 8
  • 版權(quán)聲明:本文為博主原創(chuàng)文章,未經(jīng)博主允許不得轉(zhuǎn)載。 webpack介紹和使用 一、webpack介紹 1、由來 ...
    it筱竹閱讀 11,443評論 0 21
  • 目錄第1章 webpack簡介 11.1 webpack是什么? 11.2 官網(wǎng)地址 21.3 為什么使用 web...
    lemonzoey閱讀 1,816評論 0 1
  • webpack 介紹 webpack 是什么 為什么引入新的打包工具 webpack 核心思想 webpack 安...
    yxsGert閱讀 6,662評論 2 71

友情鏈接更多精彩內(nèi)容