React配合Webpack實現(xiàn)代碼分割與異步加載

這是Webpack+React系列配置過程記錄的第四篇。其他內容請參考:

自從前幾篇文章介紹如何搭建React+Webpack單頁面應用開發(fā)環(huán)境之后,我就基于這個環(huán)境對我的書籍分享網站的管理后臺進行業(yè)務代碼的實現(xiàn)。隨著業(yè)務代碼量的增加,我自定義的React組件也越來越多,這導致每次我刷新瀏覽器地址的時候都要等待挺久的一段時間。

解決這個問題的思路還是比較簡單,分塊加載每次需要用到什么就加載什么。基于這個思路進一步擴展一下,我想要針對CDN后者瀏覽器的緩存做一下優(yōu)化,從而讓瀏覽器每次只加載被我修改的那部分代碼。

代碼切割

參考Webpack官方文檔,代碼分割可以從以下幾個方面進行。

CSS資源

之前我們的CSS樣式通過Webpack編譯到JS代碼中,然后由JS代碼動態(tài)插入到head標簽里。這種加載CSS樣式的方式,一方面會讓JS代碼非常大,另一方面會導致在異步加載方式渲染頁面的時候網頁會閃爍。

這里我們換一種加載方式,讓CSS代碼作為獨立資源導出。這樣就減少了JS代碼規(guī)模,利用瀏覽器的多個連接同時加載JS代碼和CSS代碼,提高加載速度。這需要用到一個Webpack的插件:ExtractTextPlugin。

安裝ExtractTextPlugin:

npm install --save-dev extract-text-webpack-plugin

修改webpack.config.js文件:

// 引入ExtractTextPlugin
var ExtractTextPlugin = require('extract-text-webpack-plugin');

// 修改module.rules中關于CSS的節(jié)點的內容
//{
//  test: /\.css$/,
//  use: ['style-loader', 'css-loader']
//},
{
    test: /-m\.css$/,
    use: ExtractTextPlugin.extract({
        fallback: "style-loader",
        use: [
            {
                 loader: 'css-loader',
                 options: {
                     modules: true,
                     localIdentName: '[path][name]-[local]-[hash:base64:5]'
                  }
             }
        ]
    })
},
{
    test: /^((?!(-m)).)*\.css$/,
    use: ExtractTextPlugin.extract({
        fallback: 'style-loader',
        use: 'css-loader'
    })
}

// 在webpack的plugins節(jié)點增加下面一行:
plugins: [
  new ExtractTextPlugin('styles.css'), // 增加的行,樣式將輸出到styles.css
  new webpack.HotModuleReplacementPlugin(),
  new webpack.NoEmitOnErrorsPlugin()
]

上面的配置使用ExtractTextPlugin讓Webpack把結果生成到styles.css文件中。這個文件對外的訪問目錄與js一樣。我在這里使用了兩種處理CSS文件的方式。首先是帶-m結尾的文件,我使用css-loader的啟用了模塊化處理,讓我能夠在js中以對象的方式應用css樣式。然后是非-m結尾的文件,讓webpack調用css-loader和style-loader默認處理。

下面驗證一下效果。

在src目錄下我創(chuàng)建一個css文件,BasicExample-m.css,內容如下:

.red {
    color: red;
}

在BasicExample.js文件中引入css文件,然后在js中應用red樣式到一個p標簽(這也是我為什么要讓css文件名是-m結尾的原因)。改動如下:

...
// 引入
import styles from './BasicExample-m.css';
...
// 應用
<p className={styles.red}>Red Text</p>
...

修改一下index.html,讓它引入styles.css即可。

<html>
  <head>
    <link rel="stylesheet" href="/styles.css"/>
  </head>
  <body>
    <p>Hello world</p>
    <div id='main'></div>
    <script src="/out.js"></script>
  </body>
</html>

啟動,然后在瀏覽器查看一下效果。

CSS樣式代碼分割

啟用開發(fā)者工具查看網絡請求,發(fā)現(xiàn)確實請求了styles.css和out.js文件;而且請求到的index.html內容中,head標簽內也沒有發(fā)現(xiàn)嵌入了樣式代碼。

第三方依賴

第三方依賴在開發(fā)過程中屬于不常變化的部分,導出到一個獨立文件。

假設我的項目使用了第三方庫jQuery,因此我使用npm install --save jquery安裝了jQuery依賴。

首先我們在src/index.js中添加對jQuery的調用代碼,這是為了模擬實際開發(fā)中對第三方依賴的調用。如果你的代碼沒有調用依賴的代碼,Webpack找不到入口,也就沒有必要為之導出JS文件了。

index.js的內容改動如下:

...

ReactDOM.render(
  <AppContainer>
    <BasicExample/>
  </AppContainer>,
  document.getElementById('main')
);
// 添加的代碼
import $ from 'jquery';
$('body').append('<p>Hello vendor</p>');

if (module.hot) {
  module.hot.accept();
}

接下來開始真正配置針對第三方依賴的代碼分割,需要用到Webpack內置的優(yōu)化插件CommonsChunkPlugin。修改webpack.config.js文件中output節(jié)點和plugins節(jié)點的代碼:

...
entry: {
  main:[
    'react-hot-loader/patch',
    'webpack-hot-middleware/client',
    './src/index.js'
  ]
},
output: {
  filename: '[name].js',
  path: path.resolve(__dirname, 'public')
},
...
plugins: [
    new ExtractTextPlugin('styles.css'),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin(),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: function (module) {
        // TODO 對其他第三方依賴也要在這里進行代碼分割
        return module.context && module.context.indexOf('jquery') !== -1;
      }
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'common'
    })
  ]
...

首先修改了輸出的filename,使之根據(jù)模塊名稱命名文件。并且配置了入口為main,因此將代碼將導出到main.js而不是原來我們配置的out.js了。

你可能會注意到我兩次用到了CommonsChunkPlugin插件。這樣做是有原因的。我配置了名為vendor的導出項,用于導出第三方依賴的代碼到vendor.js。但是由于Webpack在導出代碼的時候會往代碼里面加入運行時相關的代碼。這就造成我們的main.js和vendor.js都包含同樣的Webpack運行時相關代碼。所以我配置了第二個名為common的導出項,把這部分的代碼抽離出來存放在common.js中。

代碼切割后的輸出

最后在index.html中引用common.js、vendor.js和main.js。需要注意的是這三個文件之間是有依賴關系的。vendor和main依賴了common,main依賴了vendor。都是調用關系,注意即可。

運行可以看到頁面顯示了jQuery插入的“Hello vendor”了。打開控制臺也可以看到網頁請求的內容。

代碼分割后的網絡資源
應用代碼

對應用里面的代碼進行分割就不是通過配置Webpack實現(xiàn)的,而是使用Webpack提供的dynamic import方式實現(xiàn)。Webpack針對React或Vue等框架都有不同的解決方法。我盡在這里介紹React配合react-router如何實現(xiàn)異步加載React組件。

首先需要知道的是dynamic import通過返回Promise的方式實現(xiàn)異步加載功能。

import('./component.js')
    .then((m) => {
        // 處理異步加載到的模塊m
    })
    .catch((err) => {
        // 錯誤處理
    });

要注意的是import的參數(shù)不能使用變量,簡單原則是至少要讓Webpack知曉應該預先加載哪些內容。這里的參數(shù)除了使用常量之外,還可以使用模板字符串`componentDir/${name}.js`。

其實到這里基本完成代碼切割了,接下來做得就是結合react-router實現(xiàn)按模塊異步加載。這是跟業(yè)務代碼相關的,因此每個人的做法都是不一樣的。所以以下代碼僅供參考。

異步加載

我參考react-router的例子寫了個簡單的異步加載組件AsyncLoader.js,內容:

import React from 'react';

export default class AsyncLoader extends React.Component {

  static propTypes = {
    path: React.PropTypes.string.isRequired,
    loading: React.PropTypes.element,
  };

  static defaultProps = {
    path: '',
    loading: <p>Loading...</p>,
    error: <p>Error</p>
  };

  constructor(props) {
    super(props);
    this.state = {
      module: null
    };
  }

  componentWillMount() {
    this.load(this.props);
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.path !== this.props.path
      || nextProps.error !== this.props.error
      || nextProps.loading !== this.props.loading) {
      this.load(nextProps);
    }
  }

  load(props) {

    this.setState({module: props.loading});

    // TODO:異步代碼的路徑希望做成可以配置的方式
    import(`./path/${props.path}`)
      .then((m) => {
        let Module = m.default ? m.default : m;
        console.log("module: ", Module);
        this.setState({module: <Module/>});
      }).catch(() => {
        this.setState({module: props.error});
      });
  }

  render() {
    return this.state.module;
  }
}

使用方法

<Route 
    exact path='/book' 
    render={()=><AsyncLoader path={'./components/Book.js'}/>} 
/>

Webpack打包的時候會根據(jù)import的參數(shù)生成相應的js文件,默認使用id(webpack生成的,從0開始)命名這個文件。

這個過程中我踩了一個坑,這里提出來供大家參考一下。

問題是這樣的,當前路徑為http://localhost/books時發(fā)出異步加載請求,瀏覽器請求的代碼為正常的http://localhost/0.js;但是當前路徑為http://localhost/books/detail時發(fā)出異步加載請求,瀏覽器請求的是http://localhost/books/0.js,而/books/0.js這個文件是不存在的。

這個問題折磨了我挺長時間的。后來發(fā)現(xiàn)解決辦法很簡單,只需要在webpack.config.js文件的output節(jié)點中添加publicPath屬性和值就可以了。雖然沒有官方文檔可以參考,但是我測試發(fā)現(xiàn),Webpack生成js的時候,如果沒有指明publicPath則生成的代碼中異步請求是相對于當前地址開始的;否則是相對于publicPath的值。

我把BasicExample.js中的Counter.js修改成異步加載,運行結果如下所示:

異步加載

本文來自作者同步博客

源碼下載地址:https://pan.baidu.com/s/1bpoyH23

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

相關閱讀更多精彩內容

  • GitChat技術雜談 前言 本文較長,為了節(jié)省你的閱讀時間,在文前列寫作思路如下: 什么是 webpack,它要...
    蕭玄辭閱讀 12,916評論 7 110
  • 寫在開頭 先說說為什么要寫這篇文章, 最初的原因是組里的小朋友們看了webpack文檔后, 表情都是這樣的: (摘...
    Lefter閱讀 5,452評論 4 31
  • webpack 介紹 webpack 是什么 為什么引入新的打包工具 webpack 核心思想 webpack 安...
    yxsGert閱讀 6,681評論 2 71
  • 無意中看到zhangwnag大佬分享的webpack教程感覺受益匪淺,特此分享以備自己日后查看,也希望更多的人看到...
    小小字符閱讀 8,382評論 7 35
  • 作者:小 boy (滬江前端開發(fā)工程師)本文原創(chuàng),轉載請注明作者及出處。原文地址:https://www.smas...
    iKcamp閱讀 2,833評論 0 18

友情鏈接更多精彩內容