美文网首页ReactReactWeb前端之路
React配合Webpack实现代码分割与异步加载

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

作者: chardlau | 来源:发表于2017-05-21 21:41 被阅读1986次

    这是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

    相关文章

      网友评论

      • 2cafb5a3ccda:webpack.config.js 中配置完 extract-text-webpack-plugin 之后,打包会报错
      • Misswang_00c0:您好 配置完css后 报这样的错是怎么回事
        npm ERR! Windows_NT 6.1.7601
        npm ERR! argv "C:\\Program Files\\nodejs\\node.exe" "C:\\Program Files\\nodejs\\
        node_modules\\npm\\bin\\npm-cli.js" "start"
        npm ERR! node v6.11.4
        npm ERR! npm v3.10.10
        npm ERR! code ELIFECYCLE
        npm ERR! demo@1.0.0 start: `node server.js`
        npm ERR! Exit status 1
        npm ERR!
        npm ERR! Failed at the demo@1.0.0 start script 'node server.js'.
        npm ERR! Make sure you have the latest version of node.js and npm installed.
        npm ERR! If you do, this is most likely a problem with the demo package,
        npm ERR! not with npm itself.
        npm ERR! Tell the author that this fails on your system:
        npm ERR! node server.js
        npm ERR! You can get information on how to open an issue for this project with:
        npm ERR! npm bugs demo
        npm ERR! Or if that isn't available, you can get their info via:
        npm ERR! npm owner ls demo
        npm ERR! There is likely additional logging output above.

        npm ERR! Please include the following file with any support request:
      • 蓓蕾心晴:main.js:1 Uncaught ReferenceError: webpackJsonp is not defined
        at main.js:1
        (anonymous) @ main.js:1
        vendor.js:1 Uncaught ReferenceError: webpackJsonp is not defined
        at vendor.js:1

        始终在报这个错。您知道是什么原因吗?
        chardlau:应该是配置了webpack的CommonsChunkPlugin生成了公共文件,但是使用时没有先引入这个公共文件。
      • 蓓蕾心晴:entry: {
        main:[
        'react-hot-loader/patch',
        'webpack-hot-middleware/client',
        './src/index.js'
        ]
        },

        'react-hot-loader/patch'后面少了个逗号
        chardlau:这都发现了,真厉害。我已经改过来了。
      • 蓓蕾心晴:请教一下 -m结尾的文件 是什么文件呢?
        蓓蕾心晴:@chardlau 谢谢 明白啦!
        chardlau:-m表示这个文件的样式会被模块化成js对象,可以在js代码中直接使用。而没有这个后缀的则表示不模块化
        蓓蕾心晴:重新问一下,为什么起-m的文件名呢?
      • 星南樱射:按照这个做完之后,运行报错:TypeError: chunk.sortModules is not a function.
        查了一下,跟版本有关:初始webpack是2.7.0,安装最新的 3.11.0 就好了。不知道有没有其他人有相同情况的
      • chardlau:有简友反馈这篇文章对应的代码没法正确热更新。由于无法重现问题,所以我自己翻了一下代码,发现了好几个问题:
        1. 修改BasicExample-m.css里面的样式并不会热更新;
        2. 当我在测试页面选择topics页面下操作,浏览器上为http://localhost:2000/topics/props-v-state时;这时候热更新发出的请求也是基于topics这个前缀的,这样就无法正确请求到更新内容,出现404错误,也会导致无法更新的问题。
        3. 在BasicExample里面引入antd组件,antd的样式会污染外部的样式。
        chardlau:解决3:可以通过模块化css规避该问题。
        chardlau:解决2:没有在webpack.config.js的output节点添加publicPath:"/",导致webpack-hot-middleware/client在接收更新事件后,发起同步操作时请求manifest路径使用了相对路径“http://localhost:2000/topics/xxxxx.json";,而不是从根路径开始请求径“http://localhost:2000/xxxxx.json";。
        chardlau:解决1:代码配置了ExtractTextPlugin插件导致的,配置了ExtractTextPlugin会把样式独立成css文件,不会加入热更新。在后续的生成环境与开发环境分离时解决。

      本文标题:React配合Webpack实现代码分割与异步加载

      本文链接:https://www.haomeiwen.com/subject/lzpvxxtx.html