这是 webpack+React 系列配置过程记录的第四篇。其他内容请参考:
自从前几篇文章介绍如何搭建 React+Webpack 单页面应用开发环境之后,我就基于这个环境对我的书籍分享网站的管理后台进行业务代码的实现。随着业务代码量的增加,我自定义的 React 组件也越来越多,这导致每次我刷新浏览器地址的时候都要等待挺久的一段时间。
解决这个问题的思路还是比较简单,分块加载每次需要用到什么就加载什么。基于这个思路进一步扩展一下,我想要针对 CDN 后者浏览器的缓存做一下优化,从而让浏览器每次只加载被我修改的那部分代码。
参考 Webpack 官方文档,代码分割可以从以下几个方面进行。
之前我们的 CSS 样式通过 Webpack 编译到 JS 代码中,然后由 JS 代码动态插入到 head 标签里。这种加载 CSS 样式的方式,一方面会让 JS 代码非常大,另一方面会导致在异步加载方式渲染页面的时候网页会闪烁。
这里我们换一种加载方式,让 CSS 代码作为独立资源导出。这样就减少了 JS 代码规模,利用浏览器的多个连接同时加载 JS 代码和 CSS 代码,提高加载速度。这需要用到一个 Webpack 的插件:ExtractTextPlugin。
安装 ExtractTextPlugin:
- npminstall--save-dev extract-text-webpack-plugin
修改 webpack.config.js 文件:
- // 引入ExtractTextPlugin
- varExtractTextPlugin = 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: [
- newExtractTextPlugin('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';
- ...
- // 应用
- Red Text
- ...
修改一下 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>
启动,然后在浏览器查看一下效果。
启用开发者工具查看网络请求,发现确实请求了 styles.css 和 out.js 文件;而且请求到的 index.html 内容中,head 标签内也没有发现嵌入了样式代码。
第三方依赖在开发过程中属于不常变化的部分,导出到一个独立文件。
假设我的项目使用了第三方库 jQuery,因此我使用
安装了 jQuery 依赖。
- npm install --save jquery
首先我们在 src/index.js 中添加对 jQuery 的调用代码,这是为了模拟实际开发中对第三方依赖的调用。如果你的代码没有调用依赖的代码,Webpack 找不到入口,也就没有必要为之导出 JS 文件了。
index.js 的内容改动如下:
- ...
- ReactDOM.render(
- ,
- document.getElementById('main')
- );
- // 添加的代码import $ from 'jquery';
- $('body').append('
- Hello vendor
- ');
- 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: [
- newExtractTextPlugin('styles.css'),
- new webpack.HotModuleReplacementPlugin(),
- new webpack.NoEmitOnErrorsPlugin(),
- new webpack.optimize.CommonsChunkPlugin({
- name: 'vendor',
- minChunks: function (module) {
- // TODO 对其他第三方依赖也要在这里进行代码分割
- returnmodule.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: Loading...,
- error: Error
- };
- 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:
- });
- }).
- catch(() = >{
- this.setState({
- module: props.error
- });
- });
- }
- render() {
- return this.state.module;
- }
- }
使用方法
- <Route
- exact path='/book'
- render={()=>}
- />
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 修改成异步加载,运行结果如下所示:
来源: http://www.cnblogs.com/developerdaily/p/6886134.html