React配合Webpack实现代码分割与异步加载

这是Webpack+React系列配置过程记录的第四篇。其他内容请参考:

自从前几篇文章介绍如何搭建React+Webpack单页面应用开发环境之后,我就基于这个环境对我的书籍分享网站的管理后台进行业务代码的实现。随着业务代码量的增加,我自定义的React组件也越来越多,这导致每次我刷新浏览器地址的时候都要等待挺久的一段时间。

解决这个问题的思路还是比较简单,分块加载每次需要用到什么就加载什么。基于这个思路进一步扩展一下,我想要针对CDN后者浏览器的缓存做一下优化,从而让浏览器每次只加载被我修改的那部分代码。

代码切割

参考Webpack官方文档,代码分割可以从以下几个方面进行。

CSS资源

之前我们的CSS样式通过Webpack编译到JS代码中,然后由JS代码动态插入到head标签里。这种加载CSS样式的方式,一方面会让JS代码非常大,另一方面会导致在异步加载方式渲染页面的时候网页会闪烁。

这里我们换一种加载方式,让CSS代码作为独立资源导出。这样就减少了JS代码规模,利用浏览器的多个连接同时加载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的节点的内容
//{
//  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节点增加下面一行:
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目录下我创建一个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样式代码分割

启用开发者工具查看网络请求,发现确实请求了styles.css和out.js文件;而且请求到的index.html内容中,head标签内也没有发现嵌入了样式代码。

第三方依赖

第三方依赖在开发过程中属于不常变化的部分,导出到一个独立文件。

假设我的项目使用了第三方库jQuery,因此我使用npm install --save jquery安装了jQuery依赖。

首先我们在src/index.js中添加对jQuery的调用代码,这是为了模拟实际开发中对第三方依赖的调用。如果你的代码没有调用依赖的代码,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内置的优化插件CommonsChunkPlugin。修改webpack.config.js文件中output节点和plugins节点的代码:

...
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,使之根据模块名称命名文件。并且配置了入口为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实现的,而是使用Webpack提供的dynamic import方式实现。Webpack针对React或Vue等框架都有不同的解决方法。我尽在这里介绍React配合react-router如何实现异步加载React组件。

首先需要知道的是dynamic import通过返回Promise的方式实现异步加载功能。

import('./component.js')
    .then((m) => {
        // 处理异步加载到的模块m
    })
    .catch((err) => {
        // 错误处理
    });

要注意的是import的参数不能使用变量,简单原则是至少要让Webpack知晓应该预先加载哪些内容。这里的参数除了使用常量之外,还可以使用模板字符串`componentDir/${name}.js`

其实到这里基本完成代码切割了,接下来做得就是结合react-router实现按模块异步加载。这是跟业务代码相关的,因此每个人的做法都是不一样的。所以以下代码仅供参考。

异步加载

我参考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打包的时候会根据import的参数生成相应的js文件,默认使用id(webpack生成的,从0开始)命名这个文件。

这个过程中我踩了一个坑,这里提出来供大家参考一下。

问题是这样的,当前路径为http://localhost/books时发出异步加载请求,浏览器请求的代码为正常的http://localhost/0.js;但是当前路径为http://localhost/books/detail时发出异步加载请求,浏览器请求的是http://localhost/books/0.js,而/books/0.js这个文件是不存在的。

这个问题折磨了我挺长时间的。后来发现解决办法很简单,只需要在webpack.config.js文件的output节点中添加publicPath属性和值就可以了。虽然没有官方文档可以参考,但是我测试发现,Webpack生成js的时候,如果没有指明publicPath则生成的代码中异步请求是相对于当前地址开始的;否则是相对于publicPath的值。

我把BasicExample.js中的Counter.js修改成异步加载,运行结果如下所示:

异步加载

本文来自作者同步博客

源码下载地址:https://pan.baidu.com/s/1bpoyH23

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

推荐阅读更多精彩内容

  • GitChat技术杂谈 前言 本文较长,为了节省你的阅读时间,在文前列写作思路如下: 什么是 webpack,它要...
    萧玄辞阅读 12,663评论 7 110
  • 写在开头 先说说为什么要写这篇文章, 最初的原因是组里的小朋友们看了webpack文档后, 表情都是这样的: (摘...
    Lefter阅读 5,269评论 4 31
  • webpack 介绍 webpack 是什么 为什么引入新的打包工具 webpack 核心思想 webpack 安...
    yxsGert阅读 6,440评论 2 71
  • 无意中看到zhangwnag大佬分享的webpack教程感觉受益匪浅,特此分享以备自己日后查看,也希望更多的人看到...
    小小字符阅读 8,133评论 7 35
  • 作者:小 boy (沪江前端开发工程师)本文原创,转载请注明作者及出处。原文地址:https://www.smas...
    iKcamp阅读 2,740评论 0 18