一、在webpack中使用ESLint
ESLint如何落地?
- 和CI/CD系统集成
- 和webpack集成
webpack与ESLint的集成
使用eslint-loader,构建时检查JS规范。
下面我们操作一下,采用 eslint-config-airbnb,这个eslint-config-airbnb有很多依赖
第一步,安装依赖
npm install eslint eslint-plugin-import eslint-plugin-react eslint-plugin-jsx-a11y -D
npm install eslint-loader -D
第二步,在webpack.prod.js中将该loader加入
第三步:增加一个配置文件 .eslintrc.js
注:关于这个配置文件,可以是.eslintrc.json,可以是.eslintrc.js,可以是.eslintrc
这里我们采用.eslintrc.js,既然是js文件,我们就要采用module.exports的方式。
- parser配置项是我们要用的解析器,我们使用babel-eslint,所以要安装babel-eslint
- extends配置项用于继承,如果要继承多个,要用数组,我们继承了airbnb,所以按照eslint-config-airbnb
- rules配置用于修改eslint规则,自定义,比如你觉得官方哪些规则不满意,你可以在这里改,可不要。
- env配置指定启用的环境,比如设置node为true,你使用node的一些全局变量就不会报错
npm install babel-eslint eslint-config-airbnb -D
module.exports = {
"parser": "babel-eslint",
"extends": "airbnb",
"env": {
"browser": true,
"node": true
},
"rules": {
"semi": "error"
}
}
我们执行一下npm run build
它会检测每个文件,然后依次列出每个文件不符合eslint规范的地方,比如:
比如我们看到上面有个提示是需要两个空格,却发现了四个。那是因为我故意的,第九行。
假如我们团队就是希望用四个空格,怎么办?修改规则,怎么修改?如下图修改,在eslint官网,用户指南,规则,选择indent,缩进。
我们只要修改.eslintrc.js中的rules规则即可。
module.exports = {
"parser": "babel-eslint",
"extends": "airbnb",
"env": {
"browser": true,
"node": true
},
"rules": {
"indent": ["error", 4] // 注意这里
}
}
再比如,我们不希望在代码每条语句后面出现分号。这么配置:
"rules": {
"semi": "never"
}
二、webpack打包库和组件
怎么用webpack打包一个组件或者基础库,其实打包用rollup最好,很纯粹,也更简单,但是webpack也是很强大的,今天讲的是webpack。
2.1 问题抛出
实现一个大整数加法库的打包
(1)需要打包压缩版和非压缩版
(2)支持AMD/CJS/ESM模块引入,
import * as largeNumber from 'large-number'
largeNumber.add('999', '1')
支持CJS:
const largeNumber = require('large-number')
largeNumber.add('999', '1')
支持AMD:
require(['large-number'], function(large-number) {
largeNumber.add('999', '1')
})
支持
<script src="https://unpkg.com/large-number"></script>
<script>
largeNumber.add('999', '1')
</script>
// 如何将库暴露出去?
module.export = {
mode: "production",
entry: {
"large-number": "./src/index.js",
"large-number.min": "./src/index.js"
},
output: {
filename: "[name].js",
library: "largeNumber",
libraryExport: "default",
libraryTarget: "umd"
}
}
2.2 举例
自己写一个库,用webpack打包并发布到npm,然后用于项目
第一步,创建目录large-number
mkdir large-number
cd large-number
npm init -y
npm i webpack webpack-cli -D
第二步,创建large-number/src/index.js
export default function add(a, b) {
let i = a.length - 1
let j = b.length - 1
let carry = 0
let ret = ''
while (i >= 0 || j >= 0) {
let x = 0
let y = 0
let sum
if (i >= 0) {
x = a[i] - '0'
i --
}
if (j >= 0) {
y = b[j] - '0'
j --
}
sum = x + y + carry
if (sum >= 10) {
carry = 1
sum -= 10
} else {
carry = 0
}
ret = sum + ret
}
if (carry) {
ret = carry + ret
}
return ret
}
第三步,创建large-number/webpack.config.js
第四步,package.json中添加
打包:npm run build
第五步,修正:安装terser-webpack-plugin
显然,mode是不可以为production的,因为这样打包会被压缩,所以,我们需要将mode设置为none,然后通过其他配置去决定哪些压缩哪些不压缩。
这里我们采用一个插件:terser-webpack-plugin,这个插件通过配置来压缩large-number.min.js,而不压缩large-number.js,注意terser-webpack-plugin的也是webpack4.x自带的内置插件,它的内核就是uglifyjs-webpack-plugin.
npm install terser-webpack-plugin -D
webpack.config.js
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
'large-number': './src/index.js',
'large-number.min': './src/index.js'
},
output: {
filename: '[name].js', // name是占位符: large-number.js&large-number.min.js
library: 'largeNumber', // 打包出来的库的名字
libraryTarget: 'umd', // umd 可以amd,cjs,esm ,script标签引用
libraryExport: 'default' // 不设置default,引用库的时候麻烦,要largeNumber.default
},
// 优化
optimization: {
minimize: true,
minimizer: [
new TerserWebpackPlugin({
include: /\.min\.js$/ // 只针对min.js进行压缩
})
]
}
}
npm: package.json中的main属性
简而言之,此属性定义了当我们引用依赖时的文件地址。
平时开发中基本用不到,只有我们在引用或者开发某个依赖包的时候才派上用场。不使用main属性的话我们可能需要这样写引用:require(“some-module/dist/app.js”),如果我们在main属性中指定了dist/app.js的话,我们就可以直接引用依赖就可以了:require(“some-module”)
第六步,新建 large-number/index.js
if (process.env.NODE_ENV === 'production') {
module.exports = require('./dist/large-number.min.js')
} else {
module.exports = require('./dist/large-number.js')
}
同时,在package.json中设置如下:
第七步,登录 npm login
第八步,发布
npm publish执行的时候,触发钩子prepublish
记住,如果自己写的库升级了,那么一定要在package.json的version配置里改动版本号,然后再次npm publish
第九步,切换到业务项目中去用这个库
npm install large-number-guoyu -S
项目中引用
三、webpack之DLLPlugin和DLLReferencePlugin
3.1 什么是DLL
DLL(Dynamic Link Library)文件为动态链接库文件,在Windows中,许多应用程序并不是一个完整的可执行文件,它们被分割成一些相对独立的动态链接库,即DLL文件,放置于系统中。当我们执行某一个程序时,相应的DLL文件就会被调用。
举个例子:很多产品都用到螺丝,但是工厂在生产不同产品时,不需要每次连带着把螺丝也生产出来,因为螺丝可以单独生产,并给多种产品使用。在这里螺丝的作用就可以理解为是dll。
3.2 为什么要使用DLL
通常来说,我们的代码都可以至少简单区分成业务代码和第三方库。如果不做处理,每次构建时都需要把所有的代码重新构建一次,耗费大量的时间。然后大部分情况下,很多第三方库的代码并不会发生变更(除非是版本升级),这时就可以用到dll:把复用性较高的第三方模块打包到动态链接库中,在不升级这些库的情况下,动态库不需要重新打包,每次构建只重新打包业务代码。
还是上面的例子:把每次构建,当做是生产产品的过程,我们把生产螺丝的过程先提取出来,之后我们不管调整产品的功能或者设计(对应于业务代码变更),都不必重复生产螺丝(第三方模块不需要重复打包);除非是产品要使用新型号的螺丝(第三方模块需要升级),才需要去重新生产新的螺丝,然后接下来又可以专注于调整产品本身。
3.3 使用步骤
3.3.1 创建webpack.dll.js
根目录下创建一个webpack.dll.js文件
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
// 第三方库
library: [
'react',
'react-dom'
]
},
output: {
// 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称,
filename: '[name]_[hash:8].dll.js',
path: path.join(__dirname, 'build/library'),
// library必须和后面dllplugin中的name一致 后面会说明
library: '[name]_dll_[hash:8]'
},
plugins: [
new webpack.DllPlugin({
// 动态链接库的全局变量名称,需要和 output.library 中保持一致
// 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
name: '[name]_dll_[hash:8]',
// 描述动态链接库的 manifest.json 文件输出时的文件名称
path: path.join(__dirname, 'build/library/[name].manifest.json')
})
]
}
3.3.2 在项目中使用(引用)打好的DLL文件
在webpack.prod.js中引用上面打好的dll文件
const webpack = require('webpack')
new webpack.DllReferencePlugin({
manifest: require('./build/library/library.manifest.json')
})
由此可见,是否做基础包的分离,对构建效率影响很大
3.3.3 add-asset-html-webpack-plugin将dll.js文件引入到html文件中
npm i add-asset-html-webpack-plugin --save
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
new AddAssetHtmlWebpackPlugin({
filepath: path.join(__dirname, './build/library/library_fa48c66e.dll.js')
})
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
const files = fs.readdirSync(path.resolve(__dirname, './build/library'));
files.forEach(file => {
if(/.*\.dll.js/.test(file)) {
//将打包好的dll文件挂载到html中
plugins.push(new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, './build/library', file)
}))
}
if(/.*\.manifest.json/.test(file)) {
//分析第三方模块是否已经在dll文件里,如果里面有就不用再node_modules在分析打包了
plugins.push(new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, './build/library', file)
}))
}
})
四、多进程/多线程 打包
4.1 基本原理和背景
在webpack3的时代,最活跃的多线程打包方案是 HappyPack,后来由于作者的原因,不再维护,到了webpack4.x时代,官方自带了thread-loader,可以解决。
HappyPack会创建一个线程池,每个模块以及该模块的依赖都会获得worker线程。线程会处理各自的模块,处理完成之后,再通过自己的一个通讯机制,返回给主线程,完成整个构建过程。
thread-loader其实原理和HappyPack差不多,将打包任务划分成多个(node)进程,把模块依次分给这些线程,一起完成打包任务。
4.2 怎么使用tread-loader
第一步:安装 thread-loader
npm install thread-loader -D
第二步:配置webpack,在rules下配置
{
test: /\.js$/,
use: [
{
loader: 'thread-loader',
options: {
workers: 3 // 采用3个线程
}
},
'babel-loader',
'eslint-loader'
]
}
4.3 多进程并行压缩
压缩有多种方式,这里推荐一种方法: terser-webpack-plugin 开启 parallel 参数。(注:parallel 是平行的,同时发生的 意思)
安装:
npm install terser-webpack-plugin --save-dev
使用:
下面parallel设置是4,如果不设置,那么默认就是2倍CPU数量减去1.
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
optimization: {
minimizer: [new TerserPlugin({
parallel: 4
})],
},
}
parallel
Type: Boolean|Number Default: true
Use multi-process parallel running to improve the build speed. Default number of concurrent runs: os.cpus().length - 1.
通过某实例对比,将上述parallel: false的时候,打包时间为111秒。如果将parallel: true的时候,打包时间为79秒,效果很明显.
五、利用缓存提升二次构建速度
5.1 使用babel-loader插件的缓存
在使用缓存前
如何使用?
use: [
{
loader: 'thread-loader',
options: {
workers: 3
}
},
'babel-loader?cacheDirectory=true'
]
然后: npm run build
可以发现,node_modules多了一个 .cache/babel-loader 的文件夹
再次执行npm run build
5.2 利用terser-webpack-plugin的缓存
npm install terser-webpack-plugin
const TerserPlugin = require('terser-webpack-plugin')
optimization: {
minimizer: [
new TerserPlugin({
parallel: 4,
cache: true
})
]
}
执行 npm run build
第一次执行约3.3秒,再次执行,1.2秒
5.3 利用hard-source-webpack-plugin
npm install hard-source-webpack-plugin -S
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
plugins: [new HardSourceWebpackPlugin()]
执行 npm run build
最终时间降低到0.47秒