webpack从入门到放弃

3,726 阅读7分钟

webpack构建流程

从启动webpack构建到输出结果经历了一系列过程:

  1. 解析webpack.config.js配置参数,调用shell并追加命令行参数,通过 optimist将前两者参数整合成 options 对象传到了下一个流程的控制对象中
  2. 注册所有配置的插件,让插件监听webpack构建生命周期的事件节点,以做出对应的反应。
  3. 从配置的entry入口文件开始解析文件构建AST语法树,找出每个文件所依赖的文件,递归下去。
  4. 在解析文件递归的过程中根据文件类型和loader配置找出合适的loader用来对文件进行转换。
  5. 递归完后得到每个文件的最终结果,根据entry配置生成代码块chunk。
  6. 输出所有chunk到文件系统。

入口和上下文(entry and context)

entry

指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始

  • 单入口
entry: './src/app.js'
//等同于上面写法
entry: {
    main: './src/app.js'
}
  • 多入口
  entry: {
    app: './src/pages/app/index.js',
    list: './src/pages/list/index.js'
  }

这告诉我们 webpack 从 app.js 和 .js 开始创建依赖图(dependency graph)。这些依赖图是彼此完全分离、互相独立的。

多页面应用,入口文件动态获取,下面方法用于,获取动态键值对入口文件

const glob = require('glob') 
const path = require('path') 
const GLOB_FILE_PATH = './src/pages/**/index.js' 
const CUT_PATH = './src/pages/' 
exports.getEntries = function(argv){ 
    let paths = glob.sync(GLOB_FILE_PATH) 
    let entries = {} 
    for (let i = 0; i < paths.length; i++){ 
        let pathName = path.dirname(paths[i]).replace(new RegExp('^' + CUT_PATH), '') 
        entries[pathName] = paths[i] 
    } 
    return entries 
}

context

webpack 编译时的基础目录,entryloader 会相对于此目录查找文件

默认值为项目根目录,不建议修改

出口(output)

告诉 webpack 在哪里输出它所创建的 bundles

output: {
    path: path.join(__dirname, 'dist'),
    publicPath: '/',
    filename: 'js/[name].js',
    clean: true, // 在生成文件之前清空 output 目录
}

publicPath

用于指定打包后的文件需要加载的外部资源(如图片、js、css等)的根路径

默认值是一个空字符串 "",通常设置成"/"
静态资源最终访问路径 = output.publicPath + 资源loader或插件等配置路径

  • loader 输出图片文件配置
{ name: 'imgs/[name].[ext]' }
// 那么图片最终的访问路径为
output.publicPath('/') + 'imgs/[name].[ext]' = '/imgs/[name].[ext]'
  • plugin 提取css文件配置:
new ExtractTextPlugin('css/[name].[contenthash:10].css')
// html中加载css打包后代码
<link href="/css/app.9502b0c565.css" rel="stylesheet">
  • html中加载js打包后代码
<script type="text/javascript" src="/js/runtime.4ece365fd5.js"></script>

path

打包文件输出的目录

建议绝对路径;默认值为当前路径。
path 中用使用 [hash] 模板可用于版本回归

output: {
    path: path.resolve('./dist/[hash:8]/')
}

loader

loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)

注意:module.loaders 改为 module.rules;链式loader

  • webpack1旧语法
module: {
    loaders: [{
        test: /\.less$/,
        loader: "style!css!less"
    })
}
  • webpack新语法语法
module: {
    rules: [{
        test: /\.less$/,
        use: [
            "style-loader",
            "css-loader",
            "less-loader"
        ]
    }]
}

样式类

less-loader(依赖less)

npm i less less-loader --save-dev

less-loader加载less文件,less转移成
输出→CSS

sass-loader(依赖node-sass)

npm i node-sass sass-loader --save-dev

sass-loader加载sass/scss文件,node-sass转移成css
输出→CSS

postcss-loader 加载和转译 CSS 文件

npm i postcss-loader autoprefixer --save-dev

postcss是一个通过JS插件转换css的工具,提供JS API,开发者可以根据API接口,定制开发postcss插件 ,比较流行的工具有:

  • autoprefixer 补全浏览器厂商前缀
  • stylelint 代码检查工具
  • postcss-pxtorem 将px转换成rem单位

工作流:

  1. 把 CSS 解析成 JavaScript 可以操作的 抽象语法树(AST)结构
  2. 调用插件来处理 AST 并得到结果 输出→CSS

postcss.config配置如下:

module.exports = {
    plugins: {
        autoprefixer: {},
        "postcss-pxtorem": {
            rootValue: 16,
            propList: ['*'],
            minPixelValue: 1,
            exclude: (e) => {
                if (/src(\\|\/)excludeDirName(\\|\/)/.test(e)) {
                    return false
                }
                    return true
                },
            }
        }
    }
}

webpack4 请使用 postcss-loader v4;webpack5请使用最新版,并安装依赖postcss

css-loader

npm i css-loader --save-dev

作用:解析css后,将css代码转换成js模块,然后对css中 @import 和 url() 这样的外部资源进行处理。(就像 js 解析 import/require() 一样)

@import './style.css' => require('./style.css')
url(./image.png) => require('./image.png')

输出可以被执行的js代码字符串数组

启用 CSS 模块

module: {
    rules: [
      {
        test: /.css$/i,
        loader: "css-loader",
        options: {
          modules: {
              localIdentName:'[name]-[local]-[hash:base64:5]'
          }
        },
      },
    ],
  },
};

启用css模块的作用,就是在编译后产生唯一的class,避免和其他文件的class冲突。
下面是一个React组件

import style from './style.less'
export default ()=>{
    return( <div className={style.container}>主体内容</div> )
}

.container{
    font-family: "Helvetica Neue", Helvetica, Arial, "PingFang SC", "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
}

css-loader编译后的结果

.style-container-32JMV{
    font-family: "Helvetica Neue", Helvetica, Arial, "PingFang SC", "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
}

但在项目中也会遇到比如antd、swiper等第三方组件库,需要修改原样式的场景,CSS Module也提供了全局规则模式

:global(.container) {
  width: 100%;
}

配合less可以同时定义多个全局class

:global {
    .layout{
        margin: 0;
        padding: 0;
    }
    .container{
      width: 100%;
    }
}

style-loader 将模块的导出作为样式添加到 DOM 中

npm i style-loader --save-dev

把css通过style标签形式插入到DOM中,一般用在开发环境,生产环境会用mini-css-extract-plugin插件将css提取到独立文件中。

最终style loader配置

const getStyleLoaders = () =>
    [
      'style-loader',
      {
        loader: 'css-loader',
        options: {
          modules: {
            localIdentName: '[name]-[local]-[hash:base64:5]',
          },
        },
      },
      'postcss-loader',
      'sass-loader',
    ].filter(Boolean);

module: {
    rules: [
        {
          test: /\.(sc|c)ss$/,
          use: getStyleLoaders(),
        }
    ]
}

转换编译类

babel-loader

由于浏览器只能读懂ES5语法,需要babel将ES2015+语法编译为ES5语法

  1. 安装
    npm i @babel/core @babel/preset-env @babel/preset-react babel-loader --save-dev

  2. 用法

{
    test: /\.js$/,
    use: {
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
            cacheDirectory: true,
        }
    }
}

options

  • cacheDirectory:默认值为 false。当有设置时,指定的目录将用来缓存 loader 的执行结果
  • babelrc 默认true,设置false,.babelrc将不会启用 .babelrc
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false,
      }
    ],
    "@babel/typescript",
    "@babel/preset-react"
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 3,
        "helpers": true,
        "regenerator": true,
        "useESModules": true
      }
    ]
  ]
}

备注:@babel/preset-env已加入@babel/plugin-proposal-class-properties,不需要在plugins中额外引入

script-loader

在全局上下文(global context)执行一次 JS脚本,就像你在网页上通过<script>把它们引进来一样。

文件类

raw-loader 加载文件原始内容,比如.txt文件

file-loader 将文件打包导到输出目录

{
    test: /\.(gif|png|jpe?g|svg)$/,
    use: [{
        loader: 'file-loader',
        options:{
            name: 'static/img/[name].[ext]?[hash]',
        }
    }]
}

默认输出到output的根目录下,name为32为hash值

url-loader 功能类似file-loader

对file-loader的扩展,可以设置小图片转换base64图片

{
    test: /\.(png|jpg|gif)$/,
        use: [
        {
            loader: 'url-loader',
            options: {
                limit:10240,
                name: 'static/img/[name].[ext]?[hash]'
            }
        }
    ]
}

webpack5内置静态资源构建能力

{
    test: /\.(gif|png|jpe?g|svg)(\?.*)?$/,
    type: 'asset',
    generator: {
        filename: 'static/img/[name].[ext]?[hash]',
    },
    parser: {
        dataUrlCondition: {
            maxSize: 10 * 1024,
        },
    },
}

type取值说明:

  • asset/source — 功能相当于 raw-loader
  • asset/inline — 功能相当于 url-loader,若想要设置编码规则,可以在 generator 中设置 dataUrlCondition
  • asset/resource —功能相当于 file-loader
  • asset — 默认会根据文件大小来选择使用哪种类型,当文件小于 8 KB 的时候会使用 asset/inline,否则会使用 asset/resource,也可手动进行阈值的设定

清理和测试类

mocha-loader 使用 mocha 测试(浏览器/NodeJS)

eslint-loader 使用 ESLint 清理代码

jshint-loader 使用 JSHint 清理代码

模版类

html-loader 解决html里加载图片问题

minimize:true 压缩html文件

handlebars-loader 加载handlebars文件并编译为html文件

handlebars-template-loader 解决handlebars图片路径问题

markup-inline-loader

将内联的 SVG/MathML 文件转换为 HTML。在应用于图标字体,或将 CSS 动画应用于 SVG 时非常有用

{
    test: /\.html$/,
    use: [
      'html-loader',
      'markup-inline-loader'
    ]
}

html中使用:
<img markup-inline src="./_images/camera.svg" />
<img data-markup-inline src="./_images/camera.svg" />

框架类

vue-loader 加载和转译 Vue 组件

angular2-template-loader 加载和转译 Angular 组件

插件

loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量

html-webpack-plugin(生成html文件)

  • 为html文件中引入的外部资源如script、link,动态添加每次compile后的hash,防止引用缓存的外部文件问题
  • 可以生成创建html入口文件,比如单页面可以生成一个html文件入口,配置N个html-webpack-plugin可以生成N个页面入口
const HtmlWebpackPlugin = require('html-webpack-plugin');
    plugins: [
        new htmlWebpackPlugin(options),
        new htmlWebpackPlugin(options)
    ]
名称类型默认值描述
title{String}``用于生成的HTML文档的标题
filename{String}'index.html'生成html的文件名
template{String}``模版路径及文件名(路径相对与output.context)
inject{Boolean|String}truetrue或'body':所有JavaScript资源将被放置在正文元素的底部。
'head':将脚本放置在head元素中
false:不会将脚本放进html中
favicon{String}``将给定的图标路径添加到输出HTML
minify{Boolean|Object}true缩小输出html html-minifier
hash{Boolean}falsetrue:将webpack所有包含的脚本和CSS文件附加一个独特的编译哈希。这对缓存清除非常有用
cache{Boolean}true仅在文件被更改时才发出文件

mini-css-extract-plugin(提取css文件)

webpack 把所有的资源都当成了一个模块, CSS,Image, JS 字体文件资源, 都打包到一个 bundle.js 文件中,它将css模块代码提单独取到css文件中

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  module: {
    rules: [
      {
        test: /.css$/i,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
  plugins: [new MiniCssExtractPlugin()],
};

webpack4使用extract-text-webpack-plugin

terser-webpack-plugin(压缩js文件)

webpack5内置了terser-webpack-plugin,如果需要配置或webpack4,需要安装

const TerserPlugin = require('terser-webpack-plugin');
optimization: {
    minimize: true,
    minimizer: [new TerserPlugin({
        terserOptions: {
            compress: {
                comparisons: false,
                drop_console: true, //丢掉console
                inline: 2,
            },
            output: {
                comments: false,
            }
        }
    })]
}
名称类型默认值描述
test{RegExp|Array<RegExp>}/.m?js(?.*)?$/i匹配文件
parallel{Boolean|Number}true使用多进程并行运行来提高构建速度
terserOptions{Object}defaultminify options

webpack4项目可能用的uglifyjs-webpack-plugin插件压缩JS

copy-webpack-plugin(拷贝静态资源)

const CopyPlugin = require("copy-webpack-plugin")
CopyWebpackPlugin(
    patterns: [
        {
            from: 'src/static',
            to: 'static',
        },
    ])

from 定义要拷贝的源目录.
to 定义要拷贝的目标目录,默认output.path
filter 忽略拷贝指定的文件 可以用模糊匹配.

clean-webpack-plugin 打包前清理构建目录

默认会删除output目录下的文件,但目录本身不会被删除;此功能已经被webpack 5.20.0+内置了,详见上面的output设置

使用方法和常用的配置项

const { CleanWebpackPlugin } = require('clean-webpack-plugin');
plugins: [new CleanWebpackPlugin({
    verbose:true, //打开日志,默认关闭
    cleanStaleWebpackAssets: true //默认删除未使用的资源
})]

compression-webpack-plugin gzip压缩

http gzip解压过程

  1. 浏览器请求资源时会自带accept-encoding请求头,告诉服务器支持的压缩编码类型
  2. 服务器开启gzip配置=>接收客户端资源文件请求,服务器通过gzip,来对response进行编码(如果有gzip文件,则直接使用gzip文件,减少服务器压力),编码后header中有content-encoding:gzip,发送给浏览器
  3. 浏览器接到response后,根据content-encoding:gzip来对response进行解码,之后进行页面渲染。

模块热替换方案-HotModuleReplacementPlugin(HMR)

  1. 讲模块热替换前,先说下热更新。热更新就是当代码变更后页面自动刷新,代码如下:
devServer: {
    hot: true // 激活服务器的HMR
}

配置很简单,就是有个缺点,页面会刷新,所有资源都需要重新加载,所以就有了热替换。

  1. 热替换是在不刷新页面的前提下,替换模块内容;基本原理就是webpack-dev-server通过websocket把更新后的代码推送到浏览器,通过 module.hot.accept 注册的回调被执行

新增如下配置

// webpack.config.js
new webpack.HotModuleReplacementPlugin()

项目入口文件

import App from './App';
if (module.hot) {
    module.hot.accept('./App', () => {
        ReactDOM.render( <App />, document.getElementById('root') );
    });
}

webpack内置的HMR方案解决了页面刷新的问题,但仍然存在内部状态问题,不利于开发环境调试。

缺点:如果用react或者vue,刷新后组件内的状态会变成初始值,对React来说,在HMR做的是重新引入root 组件,然后重新渲染;因为HMR是对根组件的热替换,所以对于根组件和它的子组件的state都会被丢失,但存储于redux store中的状态仍会可以保持

  1. 方案1:还以React来说,引入react-hot-loader来解决上面遗留的问题。

使用react-hot-loader后,可以删除webpack.HotModuleReplacementPlugin插件

// App.jsx
import { hot } from 'react-hot-loader/root'
export default hot(App)
// .babelrc
{
  "plugins": ["react-hot-loader/babel"]
}
// package.json
{
  "dependencies": {
    "react-hot-loader": "^4.13.0",
    "@hot-loader/react-dom": "^17.0.1"
  }
}
// webpack.config.js
{
  resolve: {
    alias: {
      'react-dom': '@hot-loader/react-dom' //解决React 16.6+ 新特性的热替换问题
    }
  }
}
  1. 方案2:react-refresh@pmmmwh/react-refresh-webpack-plugin react官方模块热替换(HMR)方案
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
{
    module: {
                {
                    test: /\.tsx?$/,
                    loader: 'babel-loader',
                    options: {
                        plugins: [isEnvDevelopment && require.resolve('react-refresh/babel')].filter(Boolean),
                    }
                }
            },
    plugins:[isEnvDevelopment && new ReactRefreshPlugin(),]
}

要求:react-dom@16.9+

webpack自带插件

webpack.DefinePlugin 配置全局常量

项目中定义常量以区分环境(是常量,在项目中不可以重新赋值),并非挂在在window上,但在所有模块中都可以被访问。一般会配合cross-env一起使用

cross-env(跨平台设置node环境变量)

什么是node的环境变量?process是存在于nodejs中的一个全局变量,process.env就是项目所在运行环境的一些信息。
环境变量又有什么用呢?最常见的一个场景就是,webpack打包时,会创建一个node服务环境,通过设置环境变量,给服务打上一个标签,用来标注当前的运行环境

// package.json
"scripts": {
    "dev": "cross-env NODE_ENV_MARK=dev webpack-dev-server --config config/start.js",
}
//开发环境 dev.env.js
module.exports = {
    NODE_ENV: '"development"',
    prefix: '"//devapi.abc.com"',
};
const env = require(`../env/${process.env.NODE_ENV_MARK}.env`)
new webpack.DefinePlugin({
    'process.env': env
})

webpack.ProgressPlugin 进度插件

webpack.optimize.CommonsChunkPlugin 提取chunks之间共享的通用模块

webpack将多个模块打包之后的代码集合称为chunk。
此插件已在webpack4及更高版本中移除,如需拆包,请参考文章中优化模块的SplitChunksPlugin

通过将公共模块拆出来,最终合成的文件能够在最开始的时候加载一次,利用缓存、按需加载提高页面的加载速度

module.exports = {
    entry: {
        main1: '/src/main1.js',
        main2: '/src/main2.js',
        jquery:["jquery"],
        vue:["vue"]
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: ["common",'jquery','vue'],//对应于上面的entry的key
            minChunks:2
        })
    ]
};

打包后jquery和vue会生成独立chunk,main1和main2中的公共业务模块会打包到common.js中;
minChunks为infinity时,公共业务模块会分别打包到main1.js和main2.js中

其他

webpack-dev-server

  • 开发环境用于调试报错信息等,生成一个开发用的服务器,在文件有变化的时候自动给我们打包,然后刷新页面
  • 它还有个模块热替换的功能 .. 就是它可以只替换有变化的地方 .. 不需要刷新整个页面 ...

speed-measure-webpack-plugin 构建速度分析

webpack-bundle-analyzer 构建体积分析

优化

优化分两大类:构建速度和构建输出大小

下面是构建速度类:

1.更新webpack版本(构建时间)

webpack5 较于 webpack4,新增了持久化硬盘缓存、改进缓存算法提升构建性能,更好的tree shaking和代码生成逻辑优化产物大小。

2.缓存(构建时间)

  • 持久化硬盘缓存
cache: { 
    type: 'filesystem', // 使用文件缓存 
},

dll❎:首次将公共库打包成单独的文件,此后只需打包业务代码,减少打包时间,webpack4+已经官方内置了相关功能

  • 开启 babel-loader 缓存 babel编译过程很耗时,好在babel-loader提供缓存编译结果选项,在重启webpack时不需要创新编译而是复用缓存结果减少编译流程。babel-loader缓存机制默认是关闭的,打开的配置如下:
{
    test: /\.js$/,
    loader: 'babel-loader',
    options: {
        cacheDirectory: true,
    },
}

3.缩小loader搜索范围(构建时间)

{
    test: /\.js$/,
    loader: 'babel-loader',
    include: path.resolve(__dirname, 'src'),
}

4.优化resolve 更快的解析(构建时间)

  • 配置解析模块时搜索的目录 类似import redux from 'redux'这样既非相对又非绝对路径的写法时,会查找当前目录下的node_modules目录,默认的配置会采用向上递归搜索的方式去寻找node_modules,但通常项目里只有一个node_modules在根目录
module.exports = {
    resolve: {
        modules: [
            'node_modules',
            path.resolve(__dirname, 'src'),
        ]
    }
};

下面是优化构建输出大小

5.tree-shaking(打包体积)

借助es6模块的静态性的特点(在编译时就确定模块间的依赖关系),来删掉export但是没有import过的东西

@babel/presets-env 默认会将ES Module转换化为CommonJS形式,从而导致Webpack的tree-shaking特性失效,设置false后会禁用模块化语句的转化,将Module的语法交给Webpack处理

"presets": [
    [
      "@babel/preset-env",
      {
        "modules": false
      }
    ]
]
  • purgecss-webpack-plugin 对css tree shaking

6.文件压缩(打包体积)

  • 压缩css css-minimizer-webpack-plugin使用cssnano优化和压缩css,支持缓存、并发模式下运行,css的热替换也很好的支持。webpack5不推荐使用optimize-css-assets-webpack-plugin
optimization: {
    minimizer: [
      new CssMinimizerPlugin({
        parallel: true, //默认使用多线程运行,os.cpus().length - 1
      }),
    ],
  },
  • 压缩JS
new TerserPlugin({
    terserOptions: {
        compress: {
            comparisons: false,
            drop_console: true, //丢掉console
            inline: 2,
        },
        output: {
            comments: false,
        }
    }
})
  • imagemin-webpack-plugin压缩图片

7.chunk拆分(打包体积)

  • 对三方库和公共代码拆分 splitChunks
  • 运行时文件 runtimeChunk

runtime:模块代码的链接,打包完后,浏览器加载入口文件后,webpack会有一个启动程序,控制模块的创建和执行

异步模块的创建步骤:

  1. 创建一个Promise对象,使用installedChunks记录下其resolve和reject,便于后面获取资源后切换上下文,控制.then()的执行实际
  2. installedChunks记录一家在和加载中的chunk
  3. 创建script标签,发起异步请求
  • webpack特性import() 按需加载
optimization: {
    splitChunks: {
        chunks: 'all',
        minChunks: 2,
        cacheGroups: {
            dll: {
                chunks: 'all',
                test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
                name: 'dll',
                priority: 100,
                enforce: true,/* 为此缓存组创建块时,告诉webpack忽略minSize,minChunks,maxAsyncRequests,maxInitialRequests选项。*/
                reuseExistingChunk: true,
            },
            commons: {
                name: 'commons',
                minChunks: 2,
                chunks: 'all',
                reuseExistingChunk: true,
            },
        },
    },
    runtimeChunk: true,
}

另外还有文件名生成hash值,利用浏览器缓存减少文件加载

output: {
    filename: `static/js/[name]${isEnvProduction ? '.[contenthash:8]' : ''}.js`,
},

8.其他

  • 使用 alias resolve.alias 配置路径映射。 发布到npm的库大多数都包含两个目录,一个是放着cmd模块化的lib目录,一个是把所有文件合成一个文件的dist目录,多数的入口文件是指向lib里面下的。
    默认情况下webpack会去读lib目录下的入口文件再去递归加载其它依赖的文件这个过程很耗时,alias配置可以让webpack直接使用dist目录的整体文件减少文件递归解析。配置如下:
module.exports = {
  resolve: {
    alias: {
      'moment': 'moment/min/moment.min.js',
      'react': 'react/dist/react.js',
      'react-dom': 'react-dom/dist/react-dom.js'
    }
  }
};
  • 使用 noParse module.noParse 配置哪些文件可以脱离webpack的解析。 有些库是自成一体不依赖其他库的没有使用模块化的,比如jquey、momentjs、chart.js,要使用它们必须整体全部引入。
    webpack是模块化打包工具完全没有必要去解析这些文件的依赖,因为它们都不依赖其它文件体积也很庞大,要忽略它们配置如下:
module.exports = {
  module: {
    noParse: /node_modules\/(jquey|moment|chart\.js)/
  }
};
  • webpack4的一些优化

由于webpack4没有启用多线程构建,可以借助happypack这个启用多线程

优化总结:

  • 在加快构建时间方面,包括升级webpack、配置cache,可大大加快二次构建速度。
  • 在减小打包体积方面,包括压缩代码、分离重复代码、Tree Shaking,可最大幅度减小打包体积。
  • 在加快加载速度方面,按需加载、浏览器缓存、CDN 效果都很显著。