第五章 性能优化

207 阅读6分钟

性能优化概述

性能优化可以从三个方面入手:

  1. 构建性能
  2. 传输性能
  3. 运行性能

image.png

性能优化没有完美的解决方案,需要具体情况具体分析

构建性能

这里所说的构建性能,主要是指开发阶段的构建性能

优化构建性能即降低代码打包所带来的时间

传输性能

传输性能是指打包后的JS代码从服务器传输到浏览器所经历的时间

传输性能虽然主要受网络环境的影响,但也可以通过合理地构建工程结构来优化传输性能,例如:

  1. 总传输量

    所有需要传输的JS文件的内容之和就是总的传输量

    可以通过减少重复代码,来减少总传输量

  2. 文件数量

    当访问页面时,需要传输的JS文件的数量

    文件数量越多,浏览器发出的http请求就越多,传输性能就越差

  3. 浏览器缓存

    浏览器会缓存JS文件,被缓存的JS文件就不会再进行传输

运行性能

运行性能是指JS代码在浏览器端的运行速度,它主要取决于开发者编写的代码的质量

本章不对运行性能的优化进行讨论

减少模块解析

什么是模块解析

image.png

模块解析包括:分析源代码与抽象语法树的生成、依赖分析、模块内容转换

如果不对某个模块进行解析,则该模块经过loader处理后的代码就是最终打包结果中的代码(并且不会包含它所依赖的其他模块)

image.png

因此,不对某个模块进行解析能够缩短构建时间

如何让某个模块不进行解析

// webpack.config.js

module.exports = {
    module: {
        noParse: /xxx.js$/
    }
}

优化loader性能

限制loader的应用范围

优化loader性能可以从限制loader的应用范围方面入手

例如:babel-loader可以将ES6或更高版本的语法转换为ES5的语法,可是有些库本身就是ES5语法书写的,或者本身就是已经降级过后发布到npm上的,无需转换,使用babel-loader反而会带来不必要的构建时间

lodash就是这样的库,lodash是用ES3的语法编写的

通过module.rules.exclude或module.rules.include两个配置,可以“排除”或“仅包含”需要应用loader的场景

例如:

匹配JS文件,但lodash目录下JS文件不进行匹配

// webpack.config.js

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /lodash/,
                use: ["xxx-loader"]
            }
        ]
    }
}

匹配JS文件,但只匹配src目录下的JS文件

// webpack.config.js
module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                include: /src/,
                use: ["xxx-loader"]
            }
        ]
    }
}

可以将限制loader范围和noParse混合使用,进一步提高构建性能

缓存loader的结果

如果文件内容没有变化,则经过相同的loader转换后,得到的结果也不会变化

因此可以将loader的转换结果保存下来,对于内容没有变化的文件,让它们在以后的转换中直接使用保存的结果

cache-loader就可以实现这样的功能

// webpack.config.js
module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                use: ["cache-loader", ...loaders]
            }
        ]
    }
}

注意:cache-loader应该最后被应用,即需要放到第一位

默认情况下,cache-loader会将缓存内容放到cache-loader的默认目录中,开发者也可以指定缓存结果所在的目录

// webpack.config.js
module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                use: [
                    {
                        loader: "cache-loader",
                        options: {
                            cacheDirectory: "./cache"
                        }
                    },
                    ...loaders
                ]
            }
        ]
    }
}

思考:cache-loader明明是放在数组的最前面,为何却能够决定位于数组后面的loader是否运行

实际上,loader函数中可以包含一个方法:pitch

该方法会接收一个被转换文件的文件存放路径

该方法可以返回要转换的文件的源代码,也可以不返回

function loader(){
    ...;
}

loader.pitch = function (filePath){
    return sourceCode;
}

module.exports = loader;

一条匹配规则中的loader数组,loader的pitch运行顺序和数组元素的先后顺序相同(和loader函数的运行顺序相反)

当文件交给loader进行转换之前,会先将文件路径交给这些loader的pitch函数

若pitch函数什么也不返回,则会将filePath交付给use数组中位于后面的loader的pitch进行处理

若pitch返回了文件的源代码,则可以跳过后面的loader

image.png

为loader开启多线程

JS是一门单线程的语言,因此webpack本身的运行也是在一个线程上的

thread-loader可以开启一个线程池,线程池中包含适量的线程

thread-loader会把后续的loader放到线程池的线程中运行,以提高构建效率(位于前面的loader不管)

后续的loader是指use数组中位于thread-loader之后的loader,并不是指在thread-loader运行之后才开始运行的loader(即后续是指注册位置上的后面,而不是开始运行时间上的后面)

由于webpack是在一个线程上运行的,而后续的loader会放到新的线程中,所以后续的loader:

  • 不能使用 webpack api 生成文件
  • 无法使用自定义的 plugin api
  • 无法访问 webpack options

通常情况下,会将一些与不会使用到webpack的功能的loader放在thread-loader之后,例如babel-loader,babel本身就包含了转换代码所需的完整功能,因此无需使用到webpack提供的功能

在实际的开发中,最好还是先进行测试,来决定thread-loader放到什么位置

需要注意的是,开启和管理线程是需要消耗时间的,在小型项目中使用thread-loader反而会增加构建时间

热替换

热替换(Hot Module Replacement)并不能降低构建时间(可能还会稍微增加),但可以降低代码改动到效果呈现的时间

当使用webpack-dev-server时,从代码改动到效果呈现的过程如下:

image.png

而使用了热替换后,流程发生了如下变化:

image.png

由于热替换不会导致页面刷新,因此开发者在页面中输入的数据就不会因为页面刷新而消失,这也能提升开发体验和效率

使用

  1. 更改配置

    module.exports = {
        devServer: {
            hot: true 	// 开启HMR
        },
        plugins: [
            // 在webpack4中,只要设置了devServer.hot为true,将会自动应用该插件
            new webpack.HotModuleReplacementPlugin()
        ]
    }
    
  2. 让下面的代码得到执行

    if(module.hot){ 			// 是否开启了热更新
        module.hot.accept(); 	// 接受热更新
    }
    

    上面的这段代码是会被加入到打包结果中,并参与最终运行的

原理

开发服务器在启动时,会使用WebSocket与浏览器进行通信

在没有开启热替换的情况下,每当对代码进行重新打包,开发服务器就会通过WebSocket主动向浏览器发送一个更新通知,浏览器接收到该消息,就会调用location.reload(),导致页面刷新

当设置devServer.hot为true后,开发服务器就会向打包结果中模块函数的module参数对象中加入hot属性

不过仅仅如此,在重新打包后,浏览器还是会调用location.reload()来刷新页面

如果代码中运行了module.hot.accept(),将改变这一行为

在webpack打包的期间,HotModuleReplacementPlugin插件会向打包结果中注入一些代码

若没有调用module.hot.accept(),则浏览器收到更新通知后就会调用location.reload()

若调用了module.hot.accept(),则浏览器收到更新通知后就不会调用location.reload(),而是执行HotModuleReplacementPlugin注入的代码

HotModuleReplacementPlugin注入的代码会让浏览器向开发服务器请求哪部分内容发生了更新,服务器收到后把更新的部分响应给浏览器

之后,HotModuleReplacementPlugin注入的代码还会将原始代码中的相应部分替换为更新后的代码,并让入口模块重新运行,自此完成了热替换

样式热替换

样式也可以实现热替换,但要实现热替换就只能使用style-loader,而不能使用mini-css-extract-plugin

因为热替换发生时,HotModuleReplacementPlugin只是简单地重新运行代码

style-loader所生成的代码一重新运行,就会重新设置style元素中的样式

mini-css-extract-plugin是在构建期间通过nodejs生成一个新的css文件,因此它无法支持热替换

分包

分包就是将多个文件中重复出现的代码(特别是公共模块)提取出来,形成单独的文件

分包可以减少公共代码的重复出现,降低总体的打包体积,并且如果提取出来的是不经常改动的代码,则还可以进行浏览器缓存

分包可以分为手动分包和自动分包

一个包可以理解为一个bundle

手动分包

手动分包的的基本原理:

  1. 先单独打包公共模块

    公共模块就是多个chunk中都会使用到的模块,通常为第三方库,例如jquery、lodash等

    对于同一个chunk中的多个模块使用了同一个公共模块,webpack在打包时只会往其js文件加入一次公共模块的代码,因此只有一个chunk的工程中是不需要对公共模块进行分包的

    一般只需要将体积较大的公共模块进行分包处理,因为对于小型的公共模块,如果将它单独称为一个文件,会导致页面运行时需要多进行一次请求,可能得不偿失

    公共模块在分包时需要单独形成一个打包结果文件,并且打包结果中还需要暴露一个全局变量

    每打包完一个公共模块,就需要为该模块生成一个对应的资源清单manifest(资源清单是一个.json文件),资源清单中记录了该模块的打包结果在输出目录中的路径,以及模块的打包结果中所暴露的变量的名称

    image.png

  2. 再正常根据入口模块进行打包

    webpack在根据入口模块进行打包时,如果发现导入的模块有对应的资源清单,则打包结果会变成下面的结构:

    假设入口文件中导入了jquery和lodash:

    // index.js
    var $ = require("jquery");
    var _ = require("lodash");
    

    假设jquery的打包结果中暴露的变量叫做jquery,lodash的打包结果中暴露的变量叫做lodash

    // 入口模块的打包结果main.js
    (function(modules){
        ...;
    })({
        // index.js文件的打包结果并没有变化
        "./src/index.js": function(module, exports, webpack_require){
            var $ = webpack_require("./node_modules/jquery/index.js")
            var _ = webpack_require("./node_modules/lodash/index.js")
        },
        // 由于jquery有资源清单,因此jquery的原始代码就不会出现在函数之中
        "./node_modules/jquery/index.js": function(module, exports, webpack_require){
            module.exports = jquery;
        },
        // 由于lodash有资源清单,因此lodash的原始代码就不会出现在函数之中
        "./node_modules/lodash/index.js": function(module, exports, webpack_require){
            module.exports = lodash;
        }
    })
    

    对于有资源清单的模块,webpack就不会将模块的原始代码加入到打包结果中,而是根据资源清单中提示的模块暴露的变量,让这些模块去导出该变量

    查找资源清单并不是打包时会发生的默认行为,是需要开发者通过插件进行配置的

打包公共模块

单独打包公共模块时所使用的配置往往和打包入口模块所用的配置不一样,因此通常需要为公共模块建立一个独立的配置文件,一般将该文件命名为webpack.dll.config.js

运行打包命令时输入下面的命令:

webpack --config webpack.dll.config.js

需要将一个公共模块作为一个chunk,因此entry中的属性数量会与公共模块的数量相等

通常将对公共模块的打包结果放到dll目录中

打包结果文件中所暴露全局变量的名称就是通过output.library来指定的

dll是由早期语言衍生出来的,全称为Dynamic Link Library(动态链接库),库中会暴露一些API,开发者可以引入这些库来使用这些API

生成资源清单需要使用一个插件 —— webpack.DllPlugin,该插件会为每一个chunk都生成对应的资源清单文件

该插件中需要传一个配置对象,对象中需要提供资源清单的输出位置的绝对路径(path属性),以及清单对应的打包结果所暴露的变量名称(name属性)

// webpack.dll.config.js
const webpack = require("webpack");
const path = require("path");

module.exports = {
    mode: "production",					// 第三方库的代码无需我们调试
    entry: {
        jquery: ["jquery"],				// 这里需要写为数组
        lodash: ["lodash"]
    },
    output: {
        filename: "dll/[name].js",
        library: "[name]"				// bundle暴露的全局变量名称就是chunk的名称
    },
    plugins: [
        new webpack.DllPlugin({
            path: path.resolve(__dirname, "dll", "[name].manifest.json"),
            name: "[name]"
        })
    ]
}

注意:

  • 当需要为打包结果生成资源清单时,entry中对应chunk的属性的值就必须设置为一个数组,这是webpack规定的,即使该chunk的入口文件只有一个
  • 资源清单不应该存放在输出目录中,因为资源清单是在之后对入口模块进行打包时需要使用到的,而不是打包结果运行时需要使用的
打包入口模块
  1. 重新设置clean-webpack-plugin

    clean-webpack-plugin默认会把输出目录中原本包含的所有文件都删除

    因此,如果打包时使用了插件clean-webpack-plugin,为了避免它把之前生成好的公共模块删除,需要做出以下配置:

    new CleanWebpackPlugin({
        // "**/*"表示清除所有目录下的所有文件
        // "!dll"表示不清除dll目录
        // "!dll/*"表示不清除dll目录下的所有文件
        // 后者覆盖前者
        cleanOnceBeforeBuildPatterns: ["**/*", "!dll", "!dll/*"]
    })
    

    目录和文件的匹配规则使用的是globbing patterns

  2. 使用DllReferencePlugin控制打包结果

    加入该插件后,在打包过程中webpack才会去检查对应的资源清单

    module.exports = {
        plugins:[
            new webpack.DllReferencePlugin({
                manifest: require("./dll/jquery.manifest.json")
            }),
            new webpack.DllReferencePlugin({
                manifest: require("./dll/lodash.manifest.json")
            })
        ]
    }
    
  3. 在打包结果的html中手动引入公共模块的打包结果

    <script src="./dll/jquery.js"></script>
    <script src="./dll/lodash.js"></script>
    

    可以直接在html-webpack-plugin所参考的页面中引入这两个资源,之后html-webpack-plugin会在这两个script元素的后面加入其他script

优缺点

优点:

  1. 极大提升自身模块的打包速度
  2. 极大地缩减了自身文件体积
  3. 有利于浏览器缓存公共代码

缺点:

  1. 使用步骤繁琐

  2. 如果被打包的公共模块中也包含了重复代码,则效果不太理想

    因为第三方库也是会依赖其他库的,也许所要打包的两个第三方库就依赖了同一个库,因此对这两个第三方库进行打包,由于它们是不同的chunk,因此打包结果中还是会出现重复代码

自动分包

不同于手动分包需要开发者进行大量的操作才能完成分包,自动分包仅需要开发者对webpack进行一些简单的配置即可

使用自动分包时,需要给webpack提供一个合理的分包策略,有了分包策略后,开发者甚至不需要特别安装插件,webpack就能够自动按照策略进行分包

实际上,webpack的分包还是使用插件来完成的,使用的插件叫做SplitChunksPlugin,但该插件在webpack4的版本中已经内置进webpack之内,因此无需开发者手动进行注册

开启自动分包后,若两个chunk中使用了同一个公共模块时,webpack就能够分析出这两个chunk内部所使用的公共模块,之后根据分包策略将公共模块从那两个chunk中提取出来,并让其形成一个(或多个)单独的chunk,最后根据该chunk就可以形成一个包含公共代码的独立的bundle

image.png

开启了自动分包后,由于webpack需要对chunk中的代码进行分析,因此反而会增加打包的时间

分包策略的基本配置

webpack提供了optimization配置项,用于配置一些优化信息,其中的splitChunks就是分包策略的配置

// webpack.config.js
module.exports = {
    optimization: {
        splitChunks: {}			// 分包策略
    }
}

分包策略中的配置都是有默认值的,但默认的分包策略不会对普通chunk进行分包,因此开发者在多个chunk中导入的公共模块默认不会单独形成一个chunk

splitChunks中可以设置如下的属性:

  1. chunks

    该配置项用于配置哪些chunk需要应用分包策略

    它有三个取值:

    ① "all":对于所有的chunk都应用分包策略

    ② "async":仅针对异步chunk应用分包策略,默认值

    ③ "initial":仅针对普通chunk应用分包策略

  2. maxSize

    该配置可以控制包的最大字节数

    如果一个包(包括被拆分出来的包)的体积超过了该值,则webpack会尽可能的将其分离成多个子包(分出来的子包的大小必须大于等于minSize)

    最小的包中只会包含一个模块,模块就无法再进行细分,如果一个完整的模块的体积超过了该值,webpack就无法做到再切割,因此,尽管使用了这个配置,也完全有可能出现某个包的大小超过maxSize

    maxSize的默认值为0,maxSize为0代表着正无穷

  3. automaticNameDelimiter

    用于设置分离出来的chunk的name的分隔符,默认值为“~”

  4. minChunks

    模块被多少个chunk使用时,才对其进行分包,默认值为1

  5. minSize

    该配置可以控制包的最小字节数

    若一个包所拆分出的子包的体积小于minSize,则不对包进行真正地拆分,因为这种拆分通常是得不偿失的

    minSize的默认值为30000,minSize为0就是0

缓存组

splitChunks.cacheGroups中的一个个属性就是一个个缓存组

每个缓存组提供了一套独立的分包策略,webpack进行自动分包时所使用的分包策略实际上是缓存组的,而splitChunks中的基本配置只是为缓存组提供可参考的配置项:缓存组中有的配置会使用自身的;否则使用全局配置的

缓存组也包含全局配置中不存在的配置,如test,priority等

每一个缓存组都有自己的优先级,在分包的过程中,webpack会按照缓存组的优先级从高到低依次对这些缓存组进行应用,直至将所有缓存组都尝试应用一遍

默认情况下,webpack提供了两个缓存组:

// webpack.config.js
module.exports = {
    optimization:{
        splitChunks: {
            // 全局策略
            ...,
            // 缓存组
            cacheGroups: {
                vendors: {
                    // 匹配node_modules目录下的包
                    test: /[\\/]node_modules[\\/]/,
                    // 缓存组优先级,默认值为0
                    priority: -10
                },
                default: {
                    minChunks: 2,
                    priority: -20,
                    // 重用已经分离出去的chunk,避免分离出已经存在的chunk
                    reuseExistingChunk: true
                }
            }
        }
    }
}

这两个缓存组始终存在,即使你设置cacheGroups为一个空对象

依照某个缓存组拆分出来的chunk,chunk的name属性的前缀就是该缓存组的属性名称,如:vendors~chunkName1~chunkName2

default缓存组的test属性没有进行设置,则默认会匹配所有模块

也可以利用缓存组将公共的样式进行抽离

// webpack.config.js
module.exports = {
    optimization: {
        splitChunks: {
            chunks: "all",
            cacheGroups: {
                styles: {
                    test: /\.css$/, 	// 匹配样式模块
                    minChunks: 2 		// 覆盖默认的最小chunk引用数
                }
            }
        }
    },
    module: {
        rules: [{
            test: /\.css$/,
            use: [MiniCssExtractPlugin.loader, "css-loader"]
        }]
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: "[name].[hash:5].css",
            // chunkFilename用于配置被拆分出去的css文件的文件名
            chunkFilename: "common.[hash:5].css"
        })
    ]
}

由于需要将样式进行抽离,也就是单独形成css文件,因此需要使用到mini-css-extract-plugin插件

自动分包的大致原理

webpack在打包时,webpack会读取每个chunk的模块记录,并根据分包策略,找到这些模块记录中的满足策略要求的模块,这些模块就是需要拆分出来的公共模块

然后,webpack会根据分包策略生成一个新的chunk,该chunk是以这些公共模块作为入口文件的

最后,webpack把拆分出去的模块的代码从原始包中移除

将公共模块提取出来之后,公共模块中的代码和原始模块中的代码都需要进行一些变化

对于公共模块,公共模块的代码会往全局对象上注入一个数组属性(名为webpackJsonp),数组元素就是公共模块中的代码

对于原始模块,当它需要执行公共模块的代码时,就需要访问全局对象的数组属性,来获取到公共模块中的代码并执行

代码压缩

分包无法处理单个模块体积过大的场景

如何将单个模块的体积减少,就需要使用代码压缩

代码压缩就是将模块内部的一些无用代码删除,例如:未被使用过的函数,未被使用过的变量等,还可以通过修改标识符的长度来减少模块代码体积

通过修改标识符的字符长度,可以减少模块体积,还可以将模块中的代码进行丑化,提高破解代码的成本,这个过程称之为混淆

一般仅在生产环境下对代码进行压缩

目前主流的代码压缩工具有两个:UglifyJs和Terser

UglifyJs是一个传统的代码压缩工具,已存在多年,曾经是前端应用的必备工具,但由于它不支持ES6语法,所以目前的流行度已有所下降

Terser是一个新兴的代码压缩工具,它支持ES6+语法,因此被很多构建工具内置使用

terser官网:terser.org/

webpack中就内置了Terser,如果工程是在生产环境下进行打包的,webpack内部其实就会使用Terser对代码进行压缩

如果不想让webpack使用Terser进行压缩或还想添加其他代码压缩工具,则需要手动配置:

// webpack.config.js
var TerserPlugin = require('terser-webpack-plugin');
var OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
    mode: "production",
    optimization: {
        // 何时需要使用压缩
    	minimize: true,
        // 用什么插件进行压缩
        minimizer: [
            new TersetPlugin(),					// 对js代码进行压缩的插件
		    new OptimizeCSSAssetsPlugin()		// 对css代码进行压缩的插件
        ]
 	}
}

一旦设置了optimization.minimize为true,则无论是在生产环境下还是在开发环境下,都会对代码进行压缩

默认情况下是没有该配置的,表示仅在生产环境下对代码进行压缩

optimization.minimizer用于指示webpack用哪些插件对代码进行压缩

如果不对minimizer进行手动设置,则该配置项中默认就包含了terser-webpack-plugin

如果开发者手动设置了minimizer,则就需要手动加入terser-webpack-plugin才能应用该插件

tree shaking

代码压缩可以移除模块内部的无效代码,tree shaking则可以移除模块之间的无效代码

一个模块可能会导出很多内容,但这些内容并不是全部都会被其他模块所使用到,tree shaking就是移除哪些没有被使用到的导出内容,不让它们出现在对应的打包结果文件中

在webpack中,只要是在生产环境下进行的打包,就会自动启用tree shaking

原理

webpack在打包时,会根据入口模块分析出依赖关系

当解析到一个模块时,webpack会对模块中的ES Module的静态导入语句进行分析,分析出该模块会使用到另一个模块的哪些导出内容

之所以webpack会选择ES Module的静态导入语句进行分析,是因为其具有以下几个特点:

  1. 静态导入语句只能为顶级代码,并且通常会放在模块的顶部
  2. import的模块名只能是字符串常量
  3. import绑定的变量是不可变的

这些特点都有利于webpack在不运行代码的情况下也能准确地分析出依赖关系,从而进行更准确地tree shaking

尽管如此,由于JS语言的灵活性以及webpack还没那么智能,所以webpack仍无法完全做到将所有不会使用到的导出内容都给移除,因此webpack所坚持的原则是:“在保证代码能够正常运行的前提下,尽可能地对代码tree shaking”

所以对于不太确定是否会被使用到的导出内容,webpack会选择对其保留,以避免代码的运行出现问题

另外,webpack对使用CommonJS导出的内容是完全无法tree shaking的,webpack会简单地将CommonJS导出的所有内容都加入到打包结果中

该问题在webpack5中得到了解决

基于此,开发者就可以通过限制自己所使用的导入导出语句来提高webpack在tree shaking时的准确度:

  • 使用ES Module进行导入导出,而不要使用CommonJS进行导入导出

  • 导出时,不要一次性将所有内容导出为一个整体(例如对象或数组),而是将内容分别地进行导出

    因为webpack很难分析出模块在导入该整体时使用到了整体中的哪些元素

    // 不利于tree shaking
    export const obj = {
        a: 10,
        b: 20
    };
    
    // 有利于tree shaking
    export const a = 10;
    export const b = 20;
    

当依赖全部分析完成后,webpack会标记出那些肯定没有被使用过的导出语句为dead code,然后将这些代码交付给代码压缩工具进行删除

关于tree shaking的更详细介绍参见:tsejx.github.io/webpack-gui…

懒加载

页面在一开始运行时可能并不需要加载所有的js代码,而是在需要时再去加载必要的js代码,这就是懒加载

在webpack工程的源代码中只能通过import()来实现懒加载

当webpack对模块进行编译时,发现使用import()导入了某个模块,就会为该模块单独形成一个chunk(该chunk称为异步chunk),因此也会单独形成一个打包结果

import()语句在编译过程中也会被webpack进行替换,替换后的函数在实际执行时会返回一个promise

当浏览器执行到该函数时,才会去请求对应的js文件,而在js文件的请求完成之前,promise的状态都为pending,文件读取完成并执行后,promise状态变为fulfilled,其相关数据就是该js文件所导出的所有内容所聚合成的一个对象(类似于import * as obj中的obj)

在具体实现上,html页面中引入的入口js文件会往window对象上注入一个叫做webpackJsonp的数组,数组中会记录入口js文件所会执行到的其它模块的js代码,当运行到import()语句时,浏览器就会向开发服务器发出一个js文件请求,该js文件中的代码就会向webpackJsonp中push异步模块的模块代码,之后入口模块就能够通过webpackJsonp执行到这些代码了

在源代码中,可以在import()的参数前面书写特殊形式的注释来指定生成出来的chunk的name

import(/* webpackChunkName: "abc" */"./test");

和require()一样,import()导入的模块是动态依赖,webpack没有办法对动态依赖进行tree shaking

require()是同步的动态依赖,import()是异步的动态依赖

可以使用一些技巧间接实现对目标模块tree shaking

例如:可以设置一个中间的js文件,这个中间的js文件中使用静态的ES Module的导入方式导入原本应该被import()的目标模块,然后中间js文件再将导入结果导出,而之前本应该import()目标模块的模块,转而import()中间的js文件,由于中间js文件中使用的是静态导入方式导入的目标模块,因此中间js文件中未使用到的目标模块的导出结果就不会进入到打包结果中,这就实现了对目标模块的tree shaking

gzip

gzip是一种压缩文件的算法

本节所介绍的压缩并非之前的“代码压缩”,它并不是通过缩短文件中的字符内容或删除文件中的换行空格等进行的压缩,而是另一种压缩手段

B/S结构中的压缩传输

浏览器如果支持某个或某些压缩算法,并希望服务器响应过来压缩后的文件内容,则在请求服务器时,会附带一个accept-encoding请求头,该请求头中记录了浏览器所支持的所有压缩算法

服务器收到请求后,检查accept-encoding请求头,如果服务器也支持某种压缩手段,则服务器在收到请求后,就可以对要响应的文件内容进行压缩处理,并将经过压缩后的内容响应给浏览器,同时会附带一个content-encoding响应头,该响应头中记录着响应体中的内容是通过什么压缩手段得来的

浏览器收到响应后,查看到content-encoding响应头,就将响应体进行解压缩,以得到原本的内容

image.png

优点:传输效率可以得到提升

缺点:服务器对内容进行压缩需要耗费时间,客户端对响应内容进行解压也需要耗费时间

使用webpack进行预压缩

压缩原本就是浏览器与服务器之间的事情,但webpack可以在打包时对结果进行预压缩,而最终得到的就是压缩后的打包结果

之后,服务器就可以不需要对打包内容进行压缩,而是可以直接响应打包内容给浏览器

image.png

使用compression-webpack-plugin插件就可以对打包结果进行预压缩:

// webpack.config.js
var CompressionWebpackPlugin = require("compression-webpack-plugin");

module.exports = {
    mode: "production",					// 通常在生产环境下对打包结果进行压缩
    plugins: [
        new CompressionWebpackPlugin({
            test: /\.js$/,				// 针对哪些文件进行预压缩
 			filename: "[name].gzip",	// 设置压缩后的文件的文件名称
            minRatio: 0.3	// 压缩后的体积小于等于压缩前的体积的0.3倍时才真正进行压缩
        })
    ]
}

打包结果中有多少个js文件,该插件就会根据这些js文件生成多少个对应的压缩后的文件,插件默认使用的是gzip压缩算法

考虑到服务器并不知道浏览器所支持哪些压缩手段,如果插件所使用的压缩算法是浏览器不支持的,则服务器还是需要对原始文件内容进行响应,因此插件只是往打包结果中增加压缩后的文件,并不会将原始文件删除

ESLint

ESLint是一个针对JS的代码风格检查工具,当不满足其要求的风格时,会给予警告或错误

注意:ESLint仅仅是对代码风格进行检查,ESLint提示错误不代表代码不能够正常运行

官网:eslint.org/

中文网:eslint.nodejs.cn/

安装ESLint:

npm i -D eslint

创建配置文件:

eslint的配置文件叫做.eslintrc.*,需要将配置文件创建在工程的根目录中

.eslintrc.*可以是:.eslintrc.json、.eslintre.js、.eslintre.yaml

可以手动创建配置文件,也可以使用eslint库提供的命令创建配置文件:

eslint --init

也可以不创建eslint的配置文件,但此时就需要在package.json中添加的eslintConfig字段,并在该字段中添加具体的eslint配置

ESLint通常配合代码编辑器(例如vscode)使用:

在vscode中安装ESLint扩展,该扩展会根据全局或工程中的eslint库以及eslint配置文件对工程中的JS代码自动进行检查

如果不希望对某些js文件进行代码风格检查,则需要在.eslintignore文件中进行设置,例如:

dist/**/*.js
node_modules

基本配置

  • env

    该配置项是一个对象,用于指示代码的运行环境

    对象中可以有以下两个属性:

    ① browser:代码是否在浏览器环境中运行

    ② es6:是否启用ES6的全局API,例如Promise

    // .eslintrc.json
    {
        "env": {
            "brower": true,
            "es6": true
        }
    }
    
  • parserOptions

    该配置项是一个对象,用于指定eslint要对哪些语法进行支持

    ecmaVersion:支持的ES语法版本

    sourceType:

    ​ script:传统脚本

    ​ module:模块化脚本

  • parser

    eslint的工作原理是先将代码进行解析,然后按照规则对解析结果进行分析

    eslint 默认使用Espree作为解析器,该配置就可以指定eslint的解析器

  • globals

    设置允许使用的全局变量

    // .eslintrc.json
    {
        "globals": {
            "var1": "readonly",				// 只允许对全局变量var1进行读操作
            "var2": "writable"				// 允许对全局变量var2进行读和写操作
        }
    }
    

    eslint支持注释形式的配置,可以在具体的js代码文件中使用下面的注释也可以完成相同的配置:

    /* global var1: readonly, var2: writable */
    
  • rules

    该配置代表着eslint的规则集合,rules中的一个属性就是一条规则,每条规则影响某个方面的代码风格

    每条规则都可以有下面几个取值:

    ① "off" 或 0 或 false:关闭该规则的检查

    ② "warn" 或 1 或 true:验证不通过时提示警告

    ③ "error" 或 2:验证不通过时提示错误

    例如:

    // .eslintrc.json
    
    {
        "rules": {
            "eqeqeq": "warn",
            "curly": "error"
        }
    }
    

    规则除了可以配置在配置文件中,也可以以注释的形式存在于具体的js代码文件中:

    /* eslint eqeqeq: "off", curly: "error" */
    
  • extends

    eslint中除了可以使用官方定义的一些规则外,也可以自定义规则

    在大部分情况下,我们无需耗费精力在定义规则上,因为已经有其他人已经定义好了很多规则,我们直接使用他们定义好的规则即可

    该配置就是用于使用第三方定义的规则

    目前较为流行的就是Airbnb公司所开源的规则,具体使用方式如下:

    安装规则库:

    npm i -D eslint-config-airbnb
    

    使用规则库:

    // .eslintrc.json
    
    {
    	"extends": "airbnb"
    }
    

bundle analyzer

bundle analyer是一个plugin

bundle analyer可以将模块在打包结果中的体积占比以图形化的方式显示到页面中,方便开发者进行分析和优化

安装bundle analyzer:

npm i -D webpack-bundle-analyzer

在webpack中应用bundle analyzer:

// webpack.config.js
var WebpackBundleAnalyzer = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;

module.exports = {
    plugins: [
        new WebpackBundleAnalyzer()
    ]
}