分包加载
什么是分包
webpack是一个“模块打包机”,但不意味着每次打包都要把所有模块都打包成一个bundle,有时候需要把一些代码分离出来打到另外的文件中,这就是“分包”。
为什么需要分包
分包有两个好处
1. 分包可以减少代码体积,加快资源加载速度
对于有多个入口的页面,或者有异步加载模块的页面,不同的入口模块、不同的异步模块之间可能有共享的代码,如果每个入口和每个异步模块都将这些共享代码打包进去,可能导致代码体积变大,页面在加载文件的速度相应降低。如果我们将共享代码抽离成一个单独的文件,就可以解决这个问题。
2. 分包可以更好地利用缓存
我们项目代码可以分为业务代码和第三方代码,如果总是把业务代码和第三方打包到一起,当业务代码变更时候,浏览器每次都要更新整个bundle,但是第三方代码并未变更,其实只需要加载变更的业务代码。
如果我们把第三方代码抽离到一个独立的bundle,并给这个bundle名加根据内容计算的hash,就可以很好滴利用强缓存,即使业务代码变化,只要第三方代码不变,就会使用第三方代码的本地的缓存版本,而不需要发起网络请求。
同理,不仅第三方代码可以分包,一些业务中不常变的代码(如通用组件、工具方法等)也可以通过分包来更好地利用缓存。
简而言之,可以把不常变化的代码分包,在项目迭代后用户只需要加载变更的代码,而不变的分包的代码从缓存中加载,提升了资源加载速度。
分包的考量因素
设计分包策略主要考虑3个因素:
- 要被分割出的代码块的最小尺寸
- 分割的代码块需要被引用的最小次数
- 一个入口模块或一个异步加载模块的最大并行请求数量
被引用次数
如果一个模块被引用的太少(比如只有一个其他模块引用它),对于利用缓存和减小包体积不一定有太大好处,反而增加了请求数量。
另外,我们希望将不常变的模块提取出来,那么我们如何判断一个模块是否经常变化呢?如果我们确定某些模块是不常变的,那可以直接将其分割打包出来。对于我们不确定的模块,我们可以根据模块被引用的次数来进行估计,我们认为,一个模块如果被更多的入口模块或者异步模块引用,那它就应该是通用性更强的模块,就更可能是不常变的。因此,具体操作时,我们需要设定一个被引用次数下限,大于等于这个下限时候,才需要将模块打包。
最小尺寸
虽然我们希望将不常变得模块分离出来,但是并不意味着所有的不常变的模块都分割出来是一个好的选择。如果一个模块体积很小,那么多出的请求也是一个很大的负担。因此我们要考虑模块的体积这个因素。只有体积大到一定程度时,我们才给其被分割的资格。
入口模块和异步模块的最大并行请求数
代码被分割后,在运行时是如何被加载的呢?比如从一个入口模块为起点打包,有多个被分割的模块,那么加载这个入口模块后,webpack运行时代码会起作用,会发起http请求将分割代码请求到浏览器端。 之前提到,分割代码的好处是可以在迭代后减小需要请求的资源量。但是如果我们过分分割代码,也会引来别的问题,那就是,会造成一个入口模块或这个异步模块加载后再加载分割代码的并行请求数太大,会影响web app的性能。
因此我们需要控制入口模块和异步模块的最大并行请求数,我们需要给入口模块和异步模块设定并行请求数上限,在打包过程中,若是分割打包模块到达这个上限,就会不进行分割打包。
那么我们希望在达到这个上限后,哪些被打包,哪些不被打包呢?这就需要我们配置分割打包的优先级。
如何使用webpack分包
webpack配置中的optimization.splitChunks用来配置分包。
我们先来看默认配置
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async',
minSize: 20000,
minRemainingSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
其中主要的配置项如下:
- chunk
- minSize
- maxSize
- minChunks
- maxInitialRequests
- maxAsyncRequests
- cacheGroups
- test
- priority
- name
1. cacheGroups缓存组
首先介绍缓存组,因为缓存组是比较核心的一个概念。
从开发者的角度,在配置分割打包时,我们需要告诉webpack,我们希望将什么样的模块打包,即希望将满足什么条件的模块打包。我们通过一系列配置项来描述这些条件。如果我们想打多个包,那么我们就需要告诉webpack每个包都会将什么样的模块打包进去,每个打包的条件配置项就是一个缓存组。
简单地说,一个缓存组就是一个规则,符合规则的模块都将打包为一个chunk。
从webpack角度,webpack在遍历整个依赖图时候,会依次判断这个模块符合哪个缓存组,判断是否符合缓存组就是判断是否符合缓存组定义的配置项,如chunk、minSize、maxInitialRequests等条件。如果一个模块符合多个缓存组的规则,就在多个缓存组中选取优先级高的。
一个切片一定对应一个缓存组,一个缓存组不一定对应一个切片,因为如果没有模块符合缓存组规则,就不会生成该缓存组对应的切片。
缓存组继承splitChunks中的配置项,cacheGroups选项同级的选项都会被cacheGroup继承和覆盖,它们就是为了给cacheGroup提供默认值的,如果cacheGroup不定义相应的选项,默认就用它们的值。
test是一个正则,用来匹配模块路径,匹配到的模块有机会加入这个缓存组。
name是一个字符串,表示缓存组输出时候的文件名。
priority是一个数字,表示优先级,当一个模块符合多个缓存组时候,通过比较优先级来判断应该让模块加入哪个缓存组,webpack默认的缓存组(defaultVendors和default)优先级为负数,为自定义缓存组让步。
默认有defaultVendors和default两个缓存组,可以通过设置为false禁止它们。
2. chunk
chunks表示依赖被打包模块的模块的类型,该属性有三个可能的值 all,async和initial。比如,如果配置项为initial,则webpack在做代码分割提取公共模块时,只会考虑从入口模块引用的模块中提取,而不考虑从异步模块引用的模块中提取。
3. minSize
最小切片,用于控制可以被提取的模块的最小尺寸,如果满足该缓存组其他条件的所有模块打包体积小于该选项的值,则不会被打包。
4. minChunks
表示被打包模块需要被依赖的最小次数。
5. maxInitialRequests和maxAsyncRequests
这两个参数用来配置入口模块最大并发请求数和异步模块最大并发请求数
实践
使用webpack默认的配置就可以满足大部分需求,有些情况我们希望自己定义一些选项
可以通过自定义cacheGroup来指定某些不常变的目录下的模块分包(如通用组件、工具函数模块等)。
如果是网站使用http1,maxInitialRequests和maxAsyncRequests配小一些,比如5。
chunk可以配置为”all”,让入口chunk共享的代码也可以参与分包。
主要配置用法
webpack核心概念之entry
1. 基本概念
- 模块打包机
- 依赖图的概念
2. entry的配置方法
entry字段可以传字符串、数组、对象
- 单入口,单出口
// 简写,会打包成“main.js”
const config = {
entry: './path/to/my/entry/file.js'
};
// 对象,会打包成“bundle.js”
const config = {
entry: {
bundle: './path/to/my/entry/file.js'
}
};
- 多入口,单出口
// 数组,不同入口的模块依赖集 会打包到一起
const config = {
entry: [
'./jquery.js',
'./components.js',
'./path/to/my/entry/file.js'
]
};
- 多入口,多出口
const config = {
entry: {
app: './src/app.js',
vendors: './src/vendors.js'
}
};
webpack核心概念之output
output用来控制webpack如何将文件写入磁盘
output配置项参数
- filename
文件名
// 指定文件名
output: {
filename: 'bundle.js',
}
// 或者利用占位符指定名称,并添加hash
output: {
filename: '[name].[hash:7].js',
}
- path
输出路径
output: {
filename: '[name].js',
path: __dirname + '/dist'
}
- publicPath
静态资源发布地址,设置该参数后,webpack的loaders和plugins处理模块后,将会在模块引用路径上加上这个前缀
12. webpack核心概念之loaders
webpack支持不同的模块规范,ES6 module(动态import需要babel插件支持)、CMD、AMD
webpack本身只支持js和json两种,其他类型需要loader支持
loader支持相应类型的文件,并且把它们转化成有效的模块,并且可能产生额外的文件
每个loader是个函数,接收源文件作为参数,返回转换结果供下一个loader使用
loader使用方法
- 配置
- 内联
- cli
常见loader
- babel-loader
- css-loader
- style-loader
- file-loader、url-loader
- raw-loader
- less-loader、stylus-loader
loader配置方法
- rules下配置各种loader
- test表示文件匹配正则
- use表示loader,可以传字符串,也可以传对象(给loader传参数)和数组(多个loader)
// 配置loader
module.exports = {
module: {
rules: [
{
test: /\.txt$/,
use: 'raw-loader'
}
]
}
};
// 配置多个loader
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
};
// 给loader传参
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
module: true
}
}
]
}
]
}
};
webpack核心概念之plugins
plugin目的在于解决loaders无法实现的其他事
plugin作用于构建的整个生命周期
plugin是一个类,配置时候需要实例化,并根据需要传入参数
常见plugin
- html-webpack-plugin
- clean-webpack-plugin
- mini-css-extract-plugin
- copy-webpack-plugin
plugin用法
const config = {
entry: './path/to/my/entry/file.js',
output: {
filename: 'my-first-webpack.bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
use: 'babel-loader'
}
]
},
plugins: [
new webpack.optimize.UglifyJsPlugin(),
new HtmlWebpackPlugin({template: './src/index.html'})
]
};
文件指纹策略:chunkhash、contenthash和hash
文件指纹即webpack打包输出的文件的标识,文件标识会拼接在输出的文件后缀,这样允许我们进行非覆盖发布策略,并对文件使用强缓存策略
文件指纹的分类
- hash,和整个项目的构建相关,只要项目文件有修改,整个项目的hash值就会更改
- chunkhash,和webpack打包的chunk有关,不同的entry会生成不同的chunkhash值
- contenthash,根据文件内容来定义hash,文件内容不变,则contenthash不变
上述3中都是webpack打包配置中的占位符
最佳实践
- output.filename,使用chunkhash
output: {
filename: '[name].[chunkhash:8].js'
}
- MiniCssExtractPlugin的filename,使用[contenthash](这样,如果css内容没变而js变化了,也不会导致css文件的指纹变化)
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash:8].css'
})
]
- file-loader的name,使用[hash]或者[contenthash],这里的[hash]和[contenthash]效果一样,都是文件本身的md5
{
test: /\.(png|jpg|gif)$/,
loader: 'file-loader',
options: {
name: 'img/[name][hash:8].[ext]'
}
}
hash是md5计算的,默认32位
HTML、CSS和JavaScript代码压缩
1. js压缩
webpack内置了uglifyjs-webpack-plugin,如果mode是’production’默认开启,development关闭
如果需要配置参数,可以引入这个插件并手动配置
2. css压缩
使用optimize-css-assets-webpack-plugin,同时使用cssnano
plugins: [
new OptimizeCSSAssetsPlugin({
assetNameRegExt: /\.css$/g,
cssProcessor: require('cssnano')
})
]
3. HTML压缩
使用html-webpack-plugin,设置minify参数
plugins: [
new HTMLWebpackPlugin({
template: './index.html',
filename: 'index.html',
minify: {
html5: true,
collapseWhitespace: true,
minifyCSS: true,
minifyJS: true,
removeComments: false
}
})
]
工程化
单元测试和覆盖率测试
市面上主要的单测工具
- mocha、ava
单纯的测试框架,需要断言库- chai
- should.js
- expect
- better-assert
- Jasmine、Jest
集成框架,开箱即用
持续集成和Travis CI
持续集成核心措施是在代码集成到主干之前,必须通过自动化测试,只要有一个测试用例失败就不能集成
接入 Travis CI
- https://travis-ci.org 使用github账号登录
- 在https://travis-ci.org/count/repositories为项目开启
- 项目根目录下新增 .travis.yml
.travis.yml文件内容
install安装项目依赖
script运行测试用例
做完上面的工作后,每次我们提交代码后github会触发钩子让travis执行,Travis根据配置文件执行测试脚本,失败的话将阻止合并。
git commit 规范和changelog生成
良好的git commit 规范优势
- 加快codereview 流程
- 根据commit记录生成changelog
- 后续维护者可以知道feature修改原因
具体方案
- 统一团队git commit日志规范便于后续代码review和版本发布
- 使用angular的git commit日志作为规范
- 提交类型限制为feat、fix、refactor、style、docs、chore、pref、test等
- 提交信息分两部分 标题、主体内容
- 日志提交友好提示
- commitizen工具
- 不符合日志要求的commit拒绝提交的保障机制
- 使用validate-commit.msg工具
- 同时在客户端、gitlab server hook做
- 统一changelog文档信息生成
- 使用conventionnal-changelog-cli工具
语义化版本(semantic versioning)规范格式
语义化版本的优势
- 语义性
- 避免循环依赖和依赖冲突
semantic versioning规范是github提出来的
规范:
- 主版本号 不兼容的api修改
- 次版本号 有向下兼容的功能性新增
- 修订号 做了向下兼容的问题修正
- 测试版本号和3位版本号用”-“连接,测试版本号递进使用.1、.2的形式
- alpha 内测
- beta 外部小范围测试
- rc 公测
示例
16.1.0-alpha
16.1.0-alpha.1
16.1.0-beta
16.1.0-rc
16.1.0
动态加载
require.ensure()和import() 使用区分
使用webpack构建项目时候,有两种方法可以实现动态加载:require.ensure
和import()
require.ensure
<font style="color:rgb(77, 77, 77);">require.ensure</font>
是webpack提供的用于动态加载的API,目前不推荐使用,可以用动态import代替。
require.ensure(
dependencies:String [],
callback:function(require),
errorCallback?:function(error),
chunkName:String
);
<font style="color:rgb(77, 77, 77);">require.ensure()</font>
接受四个参数:
- 第一个参数的依赖关系是一个数组,代表了当前需要进来的模块的一些依赖。
- 第二个参数回调就是一个回调函数其中需要注意的是,这个回调函数有一个参数要求,通过这个要求就可以在回调函数内动态引入其他模块值得注意的是,虽然这个要求是回调函数的参数,理论上可以换其他名称,但是实际上是不能换的,否则的的的的WebPack就无法静态分析的时候处理它。
- 第三个参数errorCallback比较好理解,就是处理错误的回调。
- 第四个参数chunkName则是指定打包的组块名称。
const MyComponent = r => require.ensure([], () => r(require('./MyComponent')), 'MyComponent');
new Promise(MyComponent)
.then(res => {
// MyComponent
console.log('MyComponent: ', res);
});
import()
import进行动态加载是ECMAScript的新提案,虽然还没有被ECMA委员会批准,但是webpack已经开始用了。
import('./asyncModule').then(({default}) => console.log('module: ', default));
import接收一个字符串参数,代表模块路径,返回一个promise,异步加载成功后,这个promise会resolve一个对象,对象的default属性就是模块。