webpack优化-05-03-优化输出质量-减少用户能感知到的加载时间即首屏加载时间

293 阅读21分钟

区分环境

为什么需要区分环境

在开发网页的时候,一般都会有多套运行环境,如:

  • 在开发过程中方便开发调试的环境
  • 发布到线上为用户使用的运行环境 虽然两套环境是从同一套代码编译过来,但是代码内容不一样:
  • 线上代码进行过压缩
  • 开发用的代码包含一些用于提示开发者的日志
  • 开发用的代码所链接的后端数据的接口地址也可能和线上环境不同,因为要避免在开发过程中造成对线上数据的影响

所以,为了尽可能复用代码,在构建过程中需要根据目标代码,所以我们需要一套机制在源码中区分环境

如何区分环境

通过以下方式区分:

if(process.env.NODE_ENV == 'production') {
    console.log('你正在线上环境');
}else {
    console.log('你正在使用开发环境')
}

原理大概是通过环境变量的值去判断执行哪个分支。

当代码出现使用process模块的语句时,webpack就会自动打包进process模块的代码以支持非Node的运行环境,当代码中没有使用process时就不会打包进process模块的代码,这个注入的process模块的作用是模拟node中的process,来支持上面的process.env.NODE_ENV === 'production'语句。

在构建线上环境代码时,需要为当前的运行环境设置NODE_ENV = 'production',webpack的相关配置如下:

const DefinePlugin = require('webpack/lib/DefinePlugin');
module.exports = {
    plugins: [
        new DefinePlugin({
            // 定义NODE_ENV环境变量为production
            'process.env': {
                NODE_ENV: JSON.string('production')
            }
        })
    ]
}

JSON.string包裹字符串是因为环境变量的值需要一个由双引号包裹的字符串,而JSON.string('production')的值正好等于'"production"'

执行构建后我们会在输出的文件中发现如下代码:

if(true) {
    console.log('你正在线上环境');
}else {
    console.log('你正在使用开发环境')
}

webpack定义的环境变量值被带入了源码中,process.env.NODE_ENV === 'production'直接被替换成true,并且由此时访问process的语句被替换且不存在了,webpack也不会将process模块包含到输出文件中了。

DefinePlugins定义的环境变量只对webpack需要处理的代码有效,而不会影响Node.js运行时的环境变量的值。 通过Shell脚本的方式定义的环境变量如NODE_ENV=production webpack,webpack是不认识的,对webpack需要处理的diamanté中的代码中的环境区分语句是没有作用的。

所以只需要通过DefinePlugin定义环境变量,能使上面介绍的环境变量区分语句正常工作,没必要再次通过Shell脚本的方式定义一遍。

如果想让webpack使用通过shell脚本的方式定义的环境变量,则可以使用EnvironmentPlugin,代码如new webpack.EnvironmentPlugin(['NODE_ENV'])等价于new webpack.DefinePlugin({'process.env.NODE_ENV': JSON.string(process.env.NODE_ENV)})

结合 UglifyJS

前面的代码还可以进一步优化,因为if(true)语句永远只会执行前一个分支中的代码,也就是说最佳输出应该是直接为console.log('你正在线上环境')

可是webpack没有实现去除死代码的功能,UglifyJS可以做这件事

第三方库中的环境区分

以React为例,做了两套环境区分,如下:

  • 开发环境:包含类型检查,HTML元素检查等针对开发正的警告日志代码
  • 线上环境:代码了所有针对开发者的代码,只保留让React能正常运行的部分来优化大小和性能。

React源码中有大量的类似下面的代码:

if(process.env.NODE_ENV !== 'production') {
    warning(false, '%s(...): Can only update a mounted or mounting component...')
}

如果不定义NODE_ENV=production,那么这些警告日志会被包含到输出的代码中,输出的文件将会非常大。

process.env.NODE_ENV !== 'production'中的NODE_ENV'production'两个值是社区的约定,通常使用这条判断语句来区分开发环境和线上环境。

压缩代码

压缩 JavaScript

目前最成熟的js代码压缩工具:UglifyJS,我们需要通过插件的形式在webpack接入UglifyJS。目前有两个比较成熟的插件:

  • UglifyJSPlugin:通过封装UglifyJS实现压缩
  • ParallelUglifyPlugin:多进程并行处理压缩

接下来看一下关于配置UglifyJS,看一些常用选项:

  • sourceMap:默认不生成,不详细介绍了
  • beautify:是否输出可读性较强的代码,默认为输出,但是为了达到更好的压缩效果,可以设置为false
  • comments:是否保留代码中的注释,默认保留,但是为了达到更好的压缩效果,可以设置为false
  • compress.warnings:是否在UglifyJS删除没有用到的代码时输出警告信息,可以设置为false
  • drop_console:是否删除代码中的所有console语句,默认不删除,开启后不仅可以提升代码压缩的效果,也可以兼容不支持console语句的ie浏览器
  • collapse_vars:是否内嵌虽然已定义了,但只用到一次的变量。(如:var x = 5; y = x转换成 y = 5; 默认为不转换,为了达到更好的压缩效果,可以设置为false)
  • reduce_vars:是否提取出现了多次但是没有定义成变量去引用的静态值。(如:将x = 'Hello'; y = 'Hello'; 转换成var a = 'Hello'; x = a; y = b, 默认不转换,为了达到更好的压缩效果,可以设置为false。)

So,在不影响代码正确执行的前提下,最优化的代码压缩配置如下:

const UglifyJSPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
module.exports = {
    plugins: [
        // 压缩输出的js代码
        new UglifyJSPlugin({
            compress: {
                // 在UglifyJS删除没有用到的代码时不输出警告
                warnings: false,
                // 删除所有的console语句
                drop_console: true,
                //内嵌已定义但是只用一次的变量
                collapse_vars: true,
                // 提取出现了多次但是没有定义成变量去引用的静态值
                reduce_vars: true,
            },
            output: {
                // 最紧凑的输出
                beautify: false,
                // 删除所有注释
                comments: false,
            }
        })
    ]
}

webpack还提供了更简单的方法接入UglifyJsPlugin,直接在启动webpack时带上--optimize-minimize参数,即webpack --optimize-minimize,这样webpack会自动为我们注入一个带有默认配置的UglifyJSPlugin。

压缩 ES6

虽然当前大多数js引擎还不完全支持es6的新特性,但在特定的运行环境下已经可以直接执行es6代码了。 运行es6的代码相对于转换后的es5代码有如下优点:

  • 对于一样的逻辑es6实现的代码量较少
  • js引擎对es6的语法做了性能优化,,例如针对const申明的变量有更快的读取速度。

所以在运行环境允许的情况下我们尽可能的使用原声es6代码去运行,而不是使用转换后的es5代码。

用上面所讲的压缩方法去压缩es6代码时,发现UglifyJS报错退出,因为UglifyJS只理解es5代码,为了压缩es6代码,需要使用专门针对es6代码的UglifyES

UglifyJS和UglifyES是同一项目的不同分支,它们的配置项基本相同,只是接入webpack时有所区别。为了webpack介入UglifyES时,不能使用内置的UglifyJsPlugin,而是需要单独安装和使用最新版本的uglifyjs-webpack-pluginnpm i -D uglifyjs-webpack-plugin@beta

webpack相关配置代码如下:

const UglifyESPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
    plugins: [
        new UglifyESPlugin({
            // 多嵌套了一层
            uglifyOptions: {
                compress: {
                    // 在UglifyJS删除没有用到的代码时不输出警告
                    warnings: false,
                    // 删除所有console语句,可以兼容ie浏览器
                    drop_console: true,
                    // 内嵌已定义但是只用到了一次的变量
                    collapse_vars: true,
                    // 提取出现多次但是没有定义成变量去引用的静态值
                    reduce_vars: true,
                },
                output: {
                    // 最紧凑的输出
                    beautify: false,
                    // 删除所有注释
                    comments: false,
                }
            }
        })
    ]
}

同时为了不让babel-loader输出es5语法的代码,需要去掉.babelrc配置文件中的babel-preset-env(因为babel-preset-env负责将es6代码转换成es5代码),但还是要保留其他babel插件

压缩 css

压缩css,比较成熟可靠的压缩工具:cssnano,基于PostCSS cssnano能理解css代码含义,不仅仅是删掉空格,比如

  • 它会把margin: 10px 20px 10px 20px被压缩成margin: 10px 20px
  • color: #ff0000被压缩成color: red 将cssnano接入webpack:因为css-loader已经内置了,要开启cssnano去压缩袋吗,则只需开启css-loader的minimize选项,相关的webpack配置如下:
const path = require('path');
const {WebPlugin} = require('web-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin')
module.exports = {
    module: {
        rules: [
            {
                test:/\.css/, // 增加对css文件的支持
                // 提取Chunk中的CSS代码到单独的文件中
                use: ExtractTextPlugin.extract({
                    // 通过minimize选项压缩css代码
                    use: ['css-loader?minimize']
                }),
            },
        ]
    },
    plugins: [
        //  用webplugin生成对应的HTML文件
        new WebPlugin({
            template: './template.html', // HTML模版文件所在的文件路径
            filename: 'index.html' // 输出的HTML文件名称
        }),
        new ExtractTextPlugin({
            filename: `[name]_[contenthash:8].css`, // 为输出的css文件名称加上hash值
        }),
    ],
}

CDN 加速

什么是 CDN(内容分发网络)

前面通过压缩代码的手段减小网络传输的大小,但实际最影响用户体验的还是网页首次打开时的加载等待,根本原因是网络传输过程消耗较大,CDN的作用就是加速网络传输。

将资源部署到世界各地,使用户在访问时按照就近原则从离其最近的服务器获取资源,来加快资源的获取速度。

接入 CDN

为网站接入CDN,需要将网页的静态资源上传到CDN服务上,在服务这些静态资源时通过CDN服务提供的URL地址去访问。(因为CDN服务一般会为资源开启很长时间的缓存,所以用户会在很长一段时间内还在运行之前的版本,这会导致新的发布不能立即生效)

比较成熟的做法

  • 针对html文件,不开启缓存,将html放在自己的服务器上,同时关闭自己服务器的缓存。
  • 针对静态的js,css,图片等文件,开启CDN和缓存,上传到CDN,同时未每个文件带上由文件内容算出的Hash值。带上Hash值的原因是每个文件会随着文件的内容变化而变化,只要文件的内容变化,对应的URL就会发生变化,他就会被重新下载,无论缓存时间多长。

然后将之前的相对路径都变成绝对的指向CDN服务器的URL地址,如下图 更改路径

除此之外,浏览器还有一个规则是同一时刻针对同一个域名的资源的并行请求有限制(大概四个左右,有的浏览器不同),所以以上的做法有很大的问题,因为所有的静态资源都被放在同一个CDN服务的域名(cdn.com)下,如果网页的资源很多,会导致资源阻塞,同时因为只能加载几个,必须等其他资源加载完才能继续加载。

想要解决这个问题,我们需要将这些静态资源分散到不同的CDN服务上,如:将js文件放到js.cdn.com,css文件放到css.cdn.com...

可是使用多个域名又会带来新的问题,增加域名解析时间。所以根据需求自我衡量。

用Webpack 实现 CDN 的接入

构建需要实现以下几点:

  • 静态资源的导入url需要变成指向CDN服务的绝对路径的URL。
  • 静态文件名要带上由文件内容算出来的Hash值,以防止被缓存。
  • 将不同类型的资源放到不同域名的CDN服务上,防止资源的并行加载被阻塞。

webpack配置:

const path = require('path');
cosnt ExtractTextPlugin = require('extract-text-webpack-plugin');
const (WebPlugin) = require('web-webpack-plugin');

module.exports = {
    // 省略entry配置
    output: {
        // 为输出的JS文件名加上hash值
        filename: '[name]_[chunkhash:8].js',
        path: path.resolve(__dirname, './dist'),
        // 指定存放JS文件的CDN目录URL
        publicPath: '//js.cdn.com/id/',
    },
    module: {
        rules: [
            {
                // 增加对CSS文件的支持
                test: /\.css/,
                // 提取chunk中的css代码到单独的文件中
                use: ExtractTextPlugin.extract({
                    // 压缩css代码
                    use: ['css-loader?minmize'],
                    // 指定存放css中导入的资源的CDN目录URL
                    publicPath: '//img.cdn.com/id'
                }),
            },
            {
                //增加对png文件的支持
                text: /\.png/,
                // 为输出的png文件加上hash值
                use: ['file-loader?name=[name]_[hash:8].[ext]'],
            },
            // 省略其他Loader配置
        ]
    },
    plugins: [
        // 使用WebPlugin自动生成Html
        new WebPlugin({
            // Html模版文件所在的问价路径
            template: './template.html',
            // 输出的Html文件名
            filename: 'index.html',
            // 指定存放css文件的cdn目录url
            stylePublicPath: '//css.cdn.com/id/',
        }),
        new ExtractTextPlugin({
            // 为输出的css文件名加上hash值
            filename: `[name]_[contenthash:8].css`,
        }),
        // 省略代码压缩插件配置...
    ]
}

以上代码的核心部分是通过publicPath参数设置存放静态资源的CDN目录URL,为了让不同类型的资源输出到不同的CDN,分别进行如下设置

  • 在output.publicPlugin中设置js地址
  • 在css-loader.publicPath中设置被css导入的资源的地址
  • 在WebPlugin.stylePublicPath中设置css文件的地址

设置好publicPath后,webPlugin在生成html文件并将css-loader转换成css代码时,会考虑到配置中的publicPath,用对应的线上地址替换原来的相对地址

使用 Tree Shaking

认识 Tree Shaking

它可以用来提出js中用不上的死代码,依赖静态的es6模块化语法,es5的无法分析

接入 Tree Shaking

为了将采用es6模块化的代码提交给webpack,需要配置Babel让其保留es6模块化语句,修改.babelrc文件:

{
    "preset": [
        [
            "env",
            {
                // 关闭Babel的模块转化功能,保留原本的es6模块化语法
                "modeules": false
            }
        ]
    ]
}
  • 可以在配置好Babel后,运行webpack带上参数--display-used-exports参数,方便追踪tree shaking工作
  • webpack只是指出了哪些函数被用上了,哪些没有用上,想要剔除还得使用UglifyJS处理一遍,也可以在启动时带上--optimize-minimize参数来实现
  • 在大量使用第三方库时,tree shaking似乎不生效的原因是大部分npm代码都采用CommonJS语法,导致tree shaking无法正常工作,有些库考虑到这一点,发布到Npm上时会同时提供两份代码,一份CommonJS模块化语法,一份采用ES6模块化语法。 例如:发布到npm上帝目录为:es-采用es6模块化语法 | lib-采用es5模块化语法 在package.json文件中有两个字段:
{
    "main": "lib/index.js", // 指名采用CommonJS模块化的代码入口
    "jsnext:main": "es/index.js" // 指明采用ES6模块化的代码入口
}

在webpack配置文件寻找规则:

module.exports = {
    resolve: {
        // 针对Npm中第三方模块采用jsnext:main中指向ES6模块化语法的文件
        mainFields: ['jsnext:main', 'browser', 'main']
    },
}

解释:优先使用jsnext:main座位入口,如果不存在,jsnext:main就会采用browser或者main并将其作为入口

提取公共代码

为什么需要提取公共代码

一个项目由多个页面组成,很可能所有页面都采用同样的技术栈及同一套样式代码,就导致这些页面之间有很多相同的代码 如果每个页面的代码都将这些公共的部分包含进去,会造成以下问题

  • 相同的资源被重复加载,浪费用户的流量和服务器的成本
  • 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。 将多个页面的公共方法抽离成单独的文件来优化以上问题,用户访问了一个网页,用户第一次访问后,这些页面的公共代码的文件已经被浏览器缓存中缓存起来,在用户切换到其他页面时,就不会再重新加载存放公共代码的文件。而是直接从缓存中获取。有如下好处:
  • 减少网络传输流量,降低服务器成本。
  • 虽然用户第一次打开网站的速度得不到优化,但之后访问其他页面的速度会大大提升。

如何提取公共代码

  • 根据网站使用的技术栈,找出网站的所有页面都需要用到的基础库,将他们提取到一个单独的文件base.js(为了长期缓存,只要不升级基础库的版本,base.js文件就不会变化,hash值不会被更新,缓存就不会被更新,每次发布时,浏览器都会使用被缓存的base.js文件,不用重新下载base.js文件)中,该文件包含了所有网页的基础运行环境
  • 剔除各个页面中被base.js包含的部分代码后,再找出所有页面都依赖的公共部分的代码,将它们提取出来并放到common.js中。
  • 为每个网页都生成一个单独的文件,在这个文件中不再包含base.js和common.js中包含的部分,而只包含各个页面单独需要的部分代码。

如何通过 Webpack 提取公共代码

webpack内置了专门用于提取公共部分的插件CommonsChunkPlugin,CommonsChunkPlugin的大致使用方法如下:

const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
// 从网页a和网页b中抽离出公共部分,放到common中
new CommonsChunkPlugin({
    // 从哪些Chunk中提取
    chunks: ['a', 'b'],
    // 提取出的公共部分形成一个新的Chunk
    name: 'common'
})

每个CommonsChunkPlugin实例都会生成一个新的Chunk,在这个Chunk包含了被提取的代码,在使用的过程中必须指定name属性,以告诉插件新生成的Chunk的名称,其中chunks属性指明从哪些已有的Chunk中提取,不填该属性,则默认从所有已知的Chunk中提取。

为了将基础运行库从common中抽离到base中,需要做如下处理: 1、需要配置一个Chunk,在这个Chunk中只依赖所有页面都依赖的基础库及所有页面都使用的样式,为此需要在项目中写一个文件base.js来藐视base Chunk所依赖的模块,文件内容如下:

// 所有页面都依赖的基础库
import 'react';
import 'react-dom';
// 所有页面都使用的样式
import './base.css';

2、修改webpack配置,在entry中加入base,就完成了对新Chunk base的配置,相关修改如下:

module.exports = {
    entry: {
        base: './base.js'
    }
};

3、为了从common中提取出base也包含的部分,还需要配置一个CommonsChunkPlugin,相关代码如下:

new CommonsChunkPlugin({
    // 从common和base两个现成的Chunk中提取公共部分
    chunks: ['common', 'base'],
    // 将公共部分放到base中
    name: 'base'
})

由于common和base的公共部分就是base目前已经包含的部分,所以这样配置后common会变小,而base将保持不变。 构建后得到以下四个文件:

  • base.js:所有网页都依赖的基础库组成的代码
  • common.js:网页A,B都需要的但没在base,js文件中出现过的代码
  • a.js:网页A单独需要的代码
  • b.js:网页B单独需要的代码

需要在HTML中按照以下顺序引入以下文件,才能让网页正常运行:

<script src="base.js"></script>
<script src="common.js"></script>
<script src="a.js"></script>

--------------------------->这就完成了提取公共代码所需的所有步骤 采用以上的方法可能会出现common.js中没有代码的情况,因为去掉基础运行库后,很难再找到所有页面都会用上的模块。这种情况可以采用以下做法:

  • CommonsChunkPlugin提供了一个选项minChunks,表示文件要被提取出来时,需要在指定的Chunks中出现的最小次数。假如:minChunks=2, chunks=['a', 'b', 'c', 'd'],则任何一个文件只要在['a', 'b', 'c', 'd']中两个以上的Chunk中都出现过,这个文件就会被提取出来,minChunks越小,被提取到common.js的文件就会越多。
  • 根据各个页面之间的相关性选取其中的部分页面时,可用CommonsChunPlugin提取这部分被选出的页面的公共部分,而不是提取所有页面的公共部分,而且这样的操作可以叠加多次,这样做的效果会很好,但缺点是配置复杂,需要根据页面之间的关系去思考如何配置,该方法不通用。

分割代码以按需加载

为什么需要按需加载

网页承载的功能越来越多,采用单页应用座位前端架构的网站会面临网页所需要加载的代码量很大问题,许多功能都被集中到一个html里,会导致网页加载缓慢,交互卡顿,用户体验糟糕。 因为一次性加载所有功能对应的代码,但用户在每个阶段只能使用其中一部分功能, 所以当用户需要什么就只加载什么。

如何使用按需加载

采用原则

  • 将整个网站划分成一个个小功能,再按照每个功能的相关程度将他们分成几类
  • 将每类合并为一个Chunk,按需加载对应的Chunk
  • 不要按需加载用户首次打开网站时需要看到的画面所对应的功能,将其放到执行入口所在的Chunk中,以减少用户能感知的网页加载时间
  • 对于不依赖大量代码的功能点(如依赖Chart.js,依赖flv.js等功能点),可再次对其进行按需加载。

被分割出去的代码在用户操作到了或者即将操作对应的功能时再去加载。 (按需加载的代码也会耗时,所以可以预估用户接下来可能会进行的操作,并提前加载对应的代码,让用户感知不到网络加载)

用Webpack 实现按需加载

webpack内置大量的分割代码的功能去实现按需加载,举个例子:

  • 网页首次加载时只加载main.js文件,网页会展示一个按钮,在main.js文件中只包含监听按钮事件和加载按需加载的代码
  • 在按钮被单击时才去加载被分割出去的show.js文件,在加载成功后再执行show.js里的函数 假如main.js文件的内容如下:
window.document.getElementById('btn').addEventListener('click', function (){
    // 在按钮被单击后采取加载show.js文件,文件加载成功后执行文件导出的函数
    import(/*webpackChunkName: "show"*/ './show').then((show) => {
        show('Webpack');
    })
});

show.js文件内容如下:

module.exports = function (content) {
    window.alert('Hello ' + content);
}

很关键的一句:import(/*webpackChunkName: "show"*/ './show') webpack内置了对import(*)语句的支持,当webpack遇到了类似的语句时会这样处理:

  • 以show.js为入口重新生成一个Chunk
  • 当代码执行到import所在的语句时才去加载由Chunk对应的文件
  • import返回一个Promise,当文件加载成功时可以在Promise的then方法中获取show.js导出的内容

使用import()分割代码后,浏览器要支持PromiseAPI才能使代码正常运行(因为import()返回一个promise) /*webpackChunkName: "show"*/为动态生成的Chunk赋予一个名称,方便追踪和调试代码,不指定动态生成的Chunk的名称的话,默认名称是[id].js,/*webpackChunkName: "show"*/是webpack 3引入的新特性

为了正确输出在/*webpackChunkName: "show"*/中配置的ChunkName,还需要配置webpack

module.exports = {
    // js执行入口文件
    entry: {
        main: './main.js',
    },
    output: {
        // 为entry中配置生成的Chunk配置输出文件的名称
        filename: '[name].js',
        // 为动态加载的Chunk配置输出文件名称,没有这一行,分割出的代码文件名称为[id].js
        chunkFilename: '[name].js',
    }
}

按需加载与 ReactRouter

对采用ReactRouter的应用进行按需加载优化: (通过ReactRouter的应用在两个子页面之间切换和管理路由) 入口文件如下:main.js

import React, {PureComponent, createElement} from 'react';
import {render} from 'react-dom';
import {HashRouter, Route, Link} from 'react-router-dom';
import PageHome from './pages/home';

function getAsyncComponent(load) {
    return class AsyncComponent extends PureComponent {
        componentDidMount() {
            // 在高阶组件DidMount时才去执行网络加载步骤
            load().then(({default: component}) => {
                // 代码加载成功,获取了代码导出的值,调用setState,通知高阶组件重新渲染子组件
                this.setState({
                    component,
                })
            });
        }
        
        render() {
            const {component} = this.state || {};
            // component是React.Component类型,需要通过React.createElement生产一个组件实例
            return component ? createElement(component) : null;
        }
    }
}

function App() {
    return (
        <HashRouter>
            <div>
                <nav>
                    <Link to='/'>Home</Link> | <Link to='/about'>About</Link> | <Link to='/login'>Login</Link>
                </nav>
                <hr />
                <Route exact path='/' component={PageHome} />
                <Route path='/aboout' component={getAsyncComponent(
                    // 异步加载函数异步加载PageAbout组件
                    () => import(/* webpackChunkName: 'page-about' */ './pages/about')
                )}
                />
                <Route path='/login' component={getAsyncComponent(
                    // 异步加载函数异步加载PageAbout组件
                    () => import(/* webpackChunkName: 'page-login' */ './pages/login')
                )}
                />
            </div>
        </HashRouter>
    )
}
render(<App />, window.document.getElectById('app'));

getAsyncComponent配合ReactRouter按需加载组件。 以上代码需要通过Babel转换后才能在浏览器中正常运行,所以需要在Webpack中配置好对应的babel-loader,先将代码交给babel-loader处理,再提交给Webpack处理其中的import语句里

(这样会有一个报错,Babel不理解import语法,因为还没有加入到ECMAScript标准里,安装一个Babel插件,先安装Babel插件babel-plugin-syntax-dynamic-import,加入到.babelrc中)

{
    "presets": [
        "env",
        "react"
    ],
    "plugins": [
        "syntax-dynamic-import"
    ]
}

输出的三个文件:main.js:执行入口所在的代码块;page-about.js:用户访问/about才会加载的代码;page-login.js:用户访问/login时才会加载的代码块;