- output
- plugin
- loaders
- 解析css
- 解析图片和字体文件
- 文件监听
- 热更新
- 文件指纹策略
- 代码压缩
- 自动清理构建目录产物
- 自动补齐css3属性的前缀
- 移动端css px自动转换成rem
- 资源内联
- 多页面应用(MPA)
- 使用 source map
- 提取公共资源
- tree shaking (摇树优化)
- scope hoisting
- 代码分割和动态 import
- ESLint
- 服务端渲染(SSR)
- 统计信息 stats
- 如何判断构建是否成功?
- 构建配置管理可选方案
- 使用ESLint 规范构建脚本
- 冒烟测试(smoke testing)
- 单元测试与测试覆盖率
- 持续集成的作用
- webpack构建速度和体积优化策略
- webpack打包原理
- web商城的性能优化策略
output
用来告诉 webpack 如何将编译后的文件输出到磁盘
plugin
用于整个构建过程,可以理解为loader不能解决的都可以用plugins解决
常见的plugins
| 名称 | 描述 |
|---|---|
| CommonsChunkPlugin | 将chunks相同的模块代码提取成公共js |
| CleanWebpackPlugin | 清理构建目录 |
| ExtractTextWebpackPlugin | 将CSS从bundle文件里提取成一个独立的CSS文件 |
| CopyWebpackPlugin | 将文件或文件夹拷贝到构建的输出目录 |
| HtmlWebpackPlugin | 创建html文件去承载输出的bundle |
| UglifyjsWebpackPlugin | 压缩js |
| ZipWebpackPlugin | 将打包出的资源生成一个zip包 |
插件的运行环境
插件没有像loader那样的独立运行环境,只能在webpack里面运行
插件的基本结构
class MyPlugin { // 插件名称apply(compiler){ // 插件上的applycompiler.hooks.done.tap('MyPlugin', { // 插件hookstats/*stats is passed as argument when done hook is tapped*/} => {console.log('Hello World!') // 插件处理逻辑})}}// 使用plugins: [new MyPlugin()]
搭建插件的运行环境
const path = require('path')const DemoPlugin = require('./plugins/demo-plugin.js')const PATHS = {lib: path.join(__dirname,'app', 'shake.js'),build: path.join(__dirname, 'build')}module.exports = {entry: {lib: PATHS.lib},output: {path: PATHS.build,filename: '[name].js'},plugins: [new DemoPlugin()]}
插件中如何获取传递的参数?
通过插件的构造函数进行获取
module.exports = {constructor(options) {this.options = options}apply() {console.log('apply', this.options)}}
插件的错误处理
- 参数校验阶段可以直接throw的方式抛出
- 通过compilation对象的warnings 和 errors接收
compilation.warinings.push('warning')compilation.errors.push('error')
通过Compilation进行文件写入
Compilation上的assets可以用于文件写入
- 可以将zip资源包设置到compilation.assets对象上
文件写入需要使用webpack-sources
const {RawSource} = require('webpack-sources')module.exports = class DemoPlugin {constructor(options) {this.options = options}apply(compiler) {const {name} = this.optionscompiler.plugin('emit', (compilation, cb) => {compilation.assets[name] = new RawSource('demo')cb()})}}
Node.js里面将文件压缩为zip包
使用jszip
var zip = new JSZip();zip.file("Hello.txt", "Hello World\n");var img = zip.folder("images");img.file("smile.gif", imgData, {base64: true});zip.generateAsync({type:"blob"}).then(function(content) {// see FileSaver.jssaveAs(content, "example.zip");});
Compiler上负责文件生成的hooks
Hooks是emit,是一个异步的hook(AsyncSeriesHook)
emit生成文件阶段,读取的是compilation.assets对象的值
- 可以将zip资源包设置到compilation.assets对象上
loaders
常见的loaders
| 名称 | 描述 |
|---|---|
| babel-loader | 转换es6,es7等js新特性语法 |
| css-loader | 支持.css文件的加载和解析 |
| less-loader | 将less文件转换为css |
| ts-loader | 将ts转换为js |
| file-loader | 进行图片、字体等的打包 |
| raw-loader | 将文件以字符串的形式导入 |
| thread-loader | 多进程打包js和css |
loader-runner
定义: loader-runner 允许在不安装webpack的情况下运行loader
作用:
- 作为 webpack的依赖,webpack中使用它执行loader
- 进行loader的开发和调试
loader-utils
loader异常处理
- loader内直接通过throw抛出
- 通过this.callback传递错误
this.callback(err: Error || null,content: string | Buffer,sourceMap?:SourceMap,meta?: any)
loader异步处理
通过this.async来返回 一个异步函数
缓存条件: loader的结果在相同的输入下有确定的输出
- 有依赖的loader无法使用缓存
如何进行文件输出
通过this.emitFile进行文件写入雪碧图
spritesmith解析css
css-loader用于加载.css文件,并且转换成commonjs对象style-loader将样式通过<style>标签插入到head中
解析图片和字体文件
file-loader : 用于解析文件,也可用于处理字体文件url-loader : 也可处理图片和字体,可以设置较小资源自动 base64
那么 **file-loader** 和 **url-loader** 的区别是什么呢?url-loader 更加灵活,它可以把小文件转换为 base64 格式的 URL ,从而减少网络请求次数url-loader 封装了 file-loader , url-loader 不依赖于 file-loader ,即使用 url-loader 时,只需要安装 url-loader 即可,不需要安装 file-loader ,因为 url-loader 内置了 file-loader 。通过上面的介绍,我们可以看到, url-loader 工作分两种情况:1.文件大小小于 limit 参数, url-loader 将会把文件转为 DataURL ;2.文件大小大于 limit , url-loader 会调用 file-loader 进行处理,参数也会直接传给 file-loader 。因此我们只需要安装 url-loader 即可。
文件监听
文件监听是在发现源码发生变化时,自动重新构建出新的输出文件
两种方式:
- 启动
webpack命令时,带上--watch参数 ,缺点:浏览器不是自动刷新 - 在配置
webpack.config.js中设置watch: true文件监听的原理分析
轮询判断文件的最后编辑时间是否变化
某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等aggregateTimeoutmodule.export = {watch: true, // 默认falsewatchOptions: { // 只有开启监听模式时,watchOptions才有意义ignored: /node_modules/, // 默认为空,不监听的文件或文件夹,支持正则匹配aggregateTimeout:300, // 监听到变化发生后会等300ms再去执行,默认是300mspoll: 1000 // 判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒询问1000次}}
热更新
- webpack-dev-server
WDS 不刷新浏览器
WDS 不输出文件,而是放在内存(因此构建速度会更快)中
使用 HotModuleReplacementPlugin 插件
/*package.json*/"script": {"dev": "webpack-dev-server --open"}
/*webpack.config.js*/const webpack = require('webpack')module.export = {devServer: {contentBase: './dist',hot: true},plugins: [new webpack.HotModuleReplacementPlugin()]}
- webpack-dev-middleware
WDM 将 webpack 输出的文件传输给服务器,适用于灵活的定制场景
const express = require('express')const webpack = require('webpack')const webpackDevMiddleware = require('webpack-dev-middleware')const app = express()const config = require('./webpack.config.js')const compiler = webpack(config)app.use(webpackDevMiddleware(compiler,{publicPath: config.output.publicPath}))app.listen(3000, function () {console.log('example app listen on port 3000!\n')})
热更新的原理分析
webpack compile: 将js编译成Bundle
HMR Server:将热更新的文件输出给HRM Runtime
Bundle server:提供文件在浏览器的访问
HRM Runtime:会被注入到浏览器,更新文件的变化
bundle.js: 构建输出的文件
文件指纹策略
打包后输出的文件名的后缀,eg: index_65577538.js 65577538就是文件指纹
文件指纹如何生成?
hash: 和整个项目的构建有关,只要项目文件有修改,整个项目构建的hash值就会更改
每次只要有一个文件修改,hash值就会改变
chunkhash: 和webpack打包的的chunk有关,不同的entry会生成不同的chunkhash值
有一个文件变化并不影响其他文件的变化
contenthash:根据文件内容来定义hash,文件内容不变,则contenthash不变
一个页面既有 js 和 css ,对于 js 我们用 chunkhash ,如果 css 也用 chunkhash 那么即使 css内容不变的情况下,打包发布后 css 的 chunkhash 也已经变了,这个不是我们想要的。所以 css可以用 contenthash 来处理。
js的文件指纹设置
设置outout 的filename,使用[chunkhash]
output:{filename: '[name][chunkhash:8].js',path: __dirname + '/dist'}
css的文件指纹设置
安装插件 mini-css-extract-plugin
设置MiniCssExtractPlugin 的 filename,这个插件是将css单独抽离出来成为一份css文件,使用[contenthash]
注意:MiniCssExtrackPlugin和style-loader这两个是不能一起用的,因为style-loader是将css插入head里面,而MiniCssExtrackPlugin是将css抽离出文件,所以这两个是互斥的。
module: {rules: [{test: /\.css$/,use: [MiniCssExtrackPlugin.loader,'css-loader']}]},plugins: [new MiniCssExtractPlugin({filename: '[name][contenthash:8].css'})]
图片的文件指纹设置
设置 file-loader 的 name,使用[hash]
| 点位符名称 | 含义 |
|---|---|
| [ext] | 资源后缀名 |
| [name] | 文件名称 |
| [path] | 文件的相对路径 |
| [folder] | 文件所在的文件夹 |
| [contenthash] | 文件的内容hash,默认是md5生成 |
| [hash] | 文件的内容hash,默认是md5生成 |
| [emoji] | 一个随机的指代文件内容的emoj |
module:{rules: {test: /\.(png|svg|jpg|gif|jpeg)$/,use: [{loader: 'file-loader',options: {name: 'img/[name][hash:8].[ext]'}}]}}
代码压缩
html压缩
修改 html-webpack-plugin ,设置压缩参数(通常一个页面对应一个HtmlWebpackPlugin)
plugins: [new HtmlWebpackPlugin({template: path.join(__dirname, 'src/search.html'),filename: 'search.html',chunks: ['search'],inject: true,minify: {html5: true,collapseWhitespace: true,preserveLineBreaks: false,minifyCss: true,minifyJs: true,removeComments: false}})]
css压缩
使用 optimize-css-assets-webpack-plugin ,同时使用 cssnano
plugins: [new OptimizeCssAssetsPlugin({assetNameRegExp: /\.css$/g,cssProcessor: require('cssnano')})]
js压缩
在webpack4内置了 uglifyjs-webpack-plugin ,所以打包出来的是自动压缩了的
自动清理构建目录产物
构建时的问题,每次构建的时候不会清理目录,造成构建的输出目录 output 文件起来越多
- 手动构建
rm -rf ./dist && webpack rimraf ./dist && webpack
解决办法:
自动清理构建目录,避免构建前每次都需要手动删除 dist
使用 clean-webpack-plugin ,默认会删除 output 指定的输出目录
plugins: [new CleanWebpackPlugin()]
遇到的错误:
2019-08-17 16:00
webpack版本是:4.39.2
clean-webpack-plugin版本是:3.0.0
配置是:
const CleanWebpackPlugin = require('clean-webpack-plugin')...plugins: [...new CleanWebpackPlugin()]
打包时报 CleanWebpackPlugin is not a constructor 错误
正确的做法是:当然这是目前最新的语法,但不是一成不变的
const {CleanWebpackPlugin} = require('clean-webpack-plugin')...plugins: [...new CleanWebpackPlugin()]
自动补齐css3属性的前缀
postCSS插件 autoprefixer 自动补齐css3前缀,不用在头部引用 postcss-loader
安装: postcss-loader , autoprefixer
module: {rules: [{test: /\.less$/,use: ['style-loader','css-loader','less-loader',{loader: 'postcss-loader',options: {plugins: () => [require('autoprefixer')({browsers: ['last 2 version', '> 1%', 'ios 7'] // 浏览器要兼容的版本,各浏览器最新的两个版本,浏览器使用的人数(>1%),以及要兼容ios7})]}}]}]}
构建时会有如下提示:
所以可以有两种解决方案:
1.在package.json中加入下面的代码
"browserslist": [
"last 2 version",
"> 1%",
"ios 7",
"maintained node versions",
"not dead"
]
- 新建
.browserslistrc文件,写入如下内容:
# Browsers that we support
last 2 version
> 1%
ios 7
maintained node versions
not dead
so,上面的代码改成了:
module: {
rules: [
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'less-loader',
{
loader: 'postcss-loader',
options: {
plugins: () => [
require('autoprefixer')()
]
}
}
]
}
]
}
移动端css px自动转换成rem
css媒体查询实现响应式布局 缺陷:需要写多套适配样式代码
使用 px2rem-loader
页面渲染时计算根元素的 font-size 值
可以使用手淘的 lib-flexible库
module: {
rules: [
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'less-loader',
{
loader: 'px2rem-loader',
options: {
remUnit: 75,
remPrecision: 8
}
}
]
}
]
}
资源内联
什么是资源内联? 资源内联就是代码像 js,css ,图片,字体文件如何内联到 html 中去。
意义:
代码层面:
- 页面框架的初始化脚本
- 上报相关打点,完成js、css初始化
- css 内联避免页面闪动
请求层面:
- 减少HTTP网络请求数
- 小图片或字体内联(url-loader)
html 和 js 内联
raw-loader 内联html
<head>
${require('raw-loader!./meta.html')}
<title>hello world</title>
</head>
raw-loader 内联 js
<script>${require('raw-loader!babel-loader!../node_modules/lib-flexible/flexible.js')}</script>
注意:
raw-loader 的版本要0.5.1
内联 css 两种方法:
- 借助 style-loader
- html-inline-css-webpack-plugin
module: {
rules: [
{
test: /\.less$/,
use: [
{
loader: 'style-loader',
options: {
insertAt: 'top', // 样式插入到 <head>
singleton: true // 将所有的sytle 标签合并成一个
}
},
'css-loader',
'less-loader'
]
}
]
}
多页面应用(MPA)
概念: 每一次页面跳转的时候,后台服务器都会返回一个新的 html 文档,这种类型的网站也就是多页网站,也叫多页应用。
多页面打包基本思路
每一个页面对应一个 entry ,一个 html-webpack-plugin
缺点:每次新增或删除页面需要改 webpack 配置
解决方案:
动态获取 entry 和设置 html-webpack-plugin 数量
利用 glob.sync
- entry: glob.sync(path.join(__dirname, ‘./src/*/index.js’))
使用 source map
作用:通过 source map 定位到源代码
开发环境开启,线上环境关闭
- 线上排查问题可以将 sourcemap 上传到错误监控系统
source map 关键字
| eval | 使用 eval 包裹模块代码 |
|---|---|
| source map | 产生 .map 文件 |
| cheap | 不包含列信息 |
| inline | 将 .map 作为 DataURI 嵌入,不单独生成 .map 文件 |
| module | 包含 loader 的 sourcemap |
source map 类型
| devtool | 首次构建 | 二次构建 | 是否适合生产环境 | 可以定位的代码 |
|---|---|---|---|---|
| none | +++ | +++ | Y | 最终输出的代码 |
| eval | +++ | +++ | N | webpack生成的代码(一个个的模块) |
| cheap-eval-source-map | + | ++ | N | 经过loader转换后的代码(只能看到行) |
| cheap-module-eval-source-map | o | ++ | N | 源代码(只能看到行) |
| eval-source-map | — | + | N | 源代码 |
| cheap-source-map | + | o | Y | 经过loader转换后的代码(只能看到行) |
| cheap-module-source-map | o | - | Y | 源代码(只能看到行) |
| inline-cheap-source-map | + | o | N | 经过loader转换后的代码(只能看到行) |
| inline-cheap-module-source-map | o | - | N | 源代码(只能看到行) |
| source-map | — | — | Y | 源代码 |
| inline-source-map | — | — | N | 源代码 |
| hidden-source-map | — | — | Y | 源代码 |
提取公共资源
基础库分离
- 思路: 将 react、 react-dom 基础包通过 cdn 引入,不打入 bundle 中
- 方法: 使用 html-webpack-externals-plugin
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin')
...
plugins: [
new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: 'https://unpkg.com/react@16/umd/react.development.js',
global: 'React'
},
{
module: 'react-dom',
entry: 'https://unpkg.com/react-dom@16/umd/react-dom.development.js',
global: 'ReactDOM'
}
]
})
]
相应的将对应的 .html 文件添加 cdn 链接
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
利用 SplitChunksPlugin 进行公共脚本分离
- webpack4 内置的,替代 CommonsChunkPlugin 插件
- chunks 参数说明:
- async 异步引入的库进行分离(默认)
- initial 同步引入的库进行分离
- all 所有引入的库进行分离(推荐)
module.exports= {
optimization: {
splitChunks: {
chunks: 'async',
minSize: 30000, // 包最小的大小
maxSize: 0,
minChunks: 1, // 某个包最少使用的次数
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
}
}
}
}
}
利用 SplitChunksPlugin 分离基础包
module.exports= {
optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /(react|react-dom)/, // 匹配出需要分离的包
name: 'vendors',
chunks: 'all'
}
}
}
}
}
利用 SplitChunksPlugin 分离页面公共文件
module.exports= {
optimization: {
splitChunks: {
minSize: 0, // 分离的包体积的大小
cacheGroups: {
commons: {
name: 'commons',
chunks: 'all',
minChunks: 2 // 设置最小引用次数为2次
}
}
}
}
}
tree shaking (摇树优化)
概念: 一个模块可能有多个方法,只要其中的某个方法使用到了,则整个文件都会被打到 bundle 里面去,tree shaking 就是只把用到的方法打入 bundle ,没用到的方法会在 uglify 阶段被擦除掉
使用: webpack 默认支持,在 .babelrc 里面设置 modules:false 即可 mode: ‘production’情况下默认开启 要求: 必须是ES6的语法,CJS的方式不支持
原理:
利用ES6模块的特点:
- 只能作为模块顶层的语句出现
- import 的模块名只能是字符串常量
- import binding 是 immutable的
无用的css 如何删除?
- purifycss遍历代码,识别已经用到的css class
- uncss: htmlf需要通过jsdom加载, 所有的样式通过postcss解析,通过dodument.querySelector来识别在html 文件里面不存在的选择器
在webpack中如何使用purifycss
使用 purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用
const path = require('path')
const glob = require('glob')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const PurgecssPlugin = require('purgecss-webpack-plugin')
const PATHS = {
src: path.join(__dirname, 'src')
}
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css'
}),
new PurgecssPlugin({
path: glob.sync(`${PATHS.src}/**/*`, {nodir: true})
})
]
}
scope hoisting
现象:构建后的代码存在大量的闭包代码
后果:大量函数闭包包裹代码,导致体积增大(模块越多越明显)
运行代码时创建的函数作用域变多,内存开销变大
原理: 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量防止变量名冲突
对比: 通过 scope hoisting 可以减少函数声明代码和内存开销
使用:
webpack4 mode 为 production 默认开启,必须是ES6 语法 ,CJS不支持
module.export = {
plugins: [
// webpack3
new webpack.optimize.ModuleConcatenationPlugin()
]
}
代码分割和动态 import
代码分割的意义
对于大型的 web 应用来讲,将所有的代码都放在一个文件中显然是不够有效的,特别是当你的某些代码块是在某些特殊场景才会用到。webpack 有一个功能是将代码库分割成 chunks (语块),当代码运行到需要的时候再进行加载。
适用场景:
- 抽离相同代码到一个共享块
- 脚本懒加载,使得初始下载的代码更小
懒加载 JS 脚本方式
commonJS: require.ensure
ES6 :动态 import (目前还没有原生支持,需要 babel 转换)
如何使用动态 import
安装 babel 插件: @babel/plugin-syntax-dynamic-import -D
// .babelrc
"plugins": ["@babel/plugin-syntax-dynamic-import"]
ESLint
行业优秀的ESLint 规范实践(https://github.com/airbnb/javascript)
Airbnb: eslint-config-airbnb, eslint-config-airbnb-base
腾讯:
- alloyteam团队: eslint-config-alloy(https://github.com/AlloyTeam/eslint-config-alloy)
ivweb团队: eslint-config-ivweb(https://github.com/feflow/eslint-config-ivweb)
制定团队的ESLint 规范
不重复造轮子,基于eslint:recommend配置并改进
- 能够帮助发现代码错误的规则,全部开启
-
eslint 如何执行落地?
CI/CD系统集成
-
CI/CD 集成-本地开发阶段增加 precommit 钩子
安装 husky
npm i husky -D- 增加 npm script,通过 lint-staged 增量检查修改的文件
"scripts": { "precommit": "lint-staged" }, "lint-staged": { "linters": { "*.{js,scss}": ["eslint --fix", "git add"] } }webpack与ESLint集成
使用eslint-loader,构建时检查js规范module.exports= { module: { rules: [ test: /\.js$/, exclude: /node_modules/, use: [ "babel-loader", "eslint-loader" ] ] } }
安装eslint, eslint-plugin-import, eslint-plugin-react, eslint-plugin-react-hooks, and eslint-plugin-jsx-a11y -D
添加.eslintrc.js
module.exports = {
"parser": "babel-eslint",
"extends": "airbnb",
"parserOptions": {
"ecmaVersion": 6
},
"plugins": ["react"],
"rules": {
"strict": 0,
"keyword-spacing": 0,
"prefer-rest-params": 0,
"react/jsx-filename-extension": 0,
"react/jsx-wrap-multilines": 0,
"react/jsx-indent": 0,
"react/jsx-tag-spacing": 0,
"jsx-a11y/click-events-have-key-events": 0,
"jsx-a11y/no-noninteractive-element-interactions": 0,
"react/self-closing-comp": 0,
"react/jsx-closing-tag-location": 0,
"import/no-unresolved":0,
"import/newline-after-import": 0,
"import/order": 0,
"indent": ["error", 4],
"semi": ["error", "never"],
"object-shorthand": 0,
"import/no-extraneous-dependencies": 0,
"comma-dangle": [2, "never"],
"arrow-parens": 0,
"array-callback-return": 0,
"global-require": 0,
"eol-last": 0,
"func-names": 0,
"no-console": 0,
"no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 0 }],
},
"env": {
"browser": true,
"node": true
}
}
发现构建依旧报错,可能是eslint-loader版本的问题,总是报缺少一些包,类似如下的错,先采用了蠢办法,缺啥补啥
服务端渲染(SSR)
渲染: html + css + js + data -> 渲染后的 html
服务端:
- 所有模板等资源都存储在服务端
- 内网机器拉取数据更快
- 一个html返回所有数据
客户端渲染 vs 服务端渲染
| 客户端渲染 | 服务端渲染 | |
|---|---|---|
| 请求 | 多个请求(HTML, 数据等) | 1 个请求 |
| 加载过程 | HTML&数据串行加载 | 1个请求返回HTML&数据 |
| 渲染 | 前端渲染 | 服务端渲染 |
| 可交互 | 图片等静态资源加载完成,JS逻辑执行完成可交互 |
总结: 服务端渲染(SSR)的核心是减少请求
优势:
- 减少白屏时间
- 对SEO友好
SSR代码实现思路
服务端
- 使用 react-dom/server 的 renderString 方法将 React 组件渲染成字符串
- 服务端路由返回对应模板
客户端
- 打包出针对服务端的组件
统计信息 stats
如何判断构建是否成功?
构建异常和中断处理
webpack4之前的版本构建失败不会抛出错误码
Node.js 中的 process.exit 规范:
- 0 表示成功完成,回调函数中,err 为 null
非 0 表示执行失败,回调函数中,err 为 null , err.code 就是传给 exit 的数字。
如何主动捕获并处理构建错误?
compiler 在每次构建结束后会触发 done 这个 hook
- process.exit 主动处理构建报错
plugins: [
function () {
this.hooks.done.tap('done', (stats) => {
if (stats.compilation.errors && stats.compilation.errors.length && process.argv.indexOf('--watch') === -1) {
console.log('build error')
process.exit(1)
}
})
}
]
构建配置管理可选方案
- 通过多个配置文件管理不同环境的构建, webpack —config 参数进行控制
- 将构建配置设计成一个库,比如: hjs-webpack, Neutrino, webpack-blocks
- 抽成一个工具进行管理, 比如:create-react-app, kyt, nwb
- 将所有的配置放在一个文件,通过 —env 参数控制分支选择
构建配置包设计
- 通过多个配置文件管理不同环境的 webpack 配置
- 基础配置: webpack.base.js
- 开发环境: webpack.dev.js
- 生产环境: webpack.prod.js
SSR环境: webpack.ssr.js
……
- 抽离成一个npm 包统一管理
- 规范: Git commit 日志,README, ESLint规范,Semver 规范
- 质量:冒烟测试,单元测试,测试覆盖率和CI
通过 webpack-merge组合配置
合并配置: module.exports = merge(baseConfig, devConfig)> merge = require('webpack-merge') ... > merge( ...{a: [1], b: 5, c: 20}, ...{a: [2], b: 10, d: 421} ...) {a: [1, 2], b: 10, c: 20, d: 421}
功能模块设计
目录结构设计

使用ESLint 规范构建脚本
使用 eslint-config-airbnb-base
eslint —fix 可以自动处理空格
module.exports = {
parser: 'babel-eslint',
extends: 'airbnb-base',
env: {
browser: true,
node: true
}
}
冒烟测试(smoke testing)
冒烟测试是指对提交测试的软件在进行详细深入的测试之前而进行的预测试,这种预测试的主要目的是暴露导致软件需重新发布的基本功能失效等严重问题。
执行
- 构建是否成功
- 每次构建完成build目录是否有内容输出
- 是否有js,css 等静态资源文件
- 是否有html文件
判断构建是否成功
```javascript const path = require(‘path’) const webpack = require(‘webpack’) const rimraf = require(‘rimraf’) const Mocha = require(‘mocha’)
const Mocha = new Mocha({ timeout: ‘10000ms’ })
process.chdir(__dirname)
rimraf(‘./dist’, () => { const prodConfig = require(‘../../lib/webpack.prod.js’) webpack(prodConfig, (err, stats) => { if (err) { console.error(err) return }
console.log(stats.toString({
color: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}))
console.log(`\n Compiler sucess ,begin`)
}) })
<a name="UgKHd"></a>
## 判断基本功能是否正常
编写 mocha 测试用例
- 是否有js, css 等静态资源文件
- 是否有HTML文件
```javascript
const glob = require('glob-all')
describe('checking generated file exists', function() {
it ('should generate html files', function (done) {
const files = glob.sync(
[
'./dist/index.html',
'./dist/search.html'
]
)
if (files.length > 0) {
done()
} else {
throw new Error('No html files found')
}
})
it('should generate js & css files', function (done) {
const files = glob.sync(
[
'./dist/index_*.js',
'./dist/search_*.js',
'./dist/index_*.css',
'./dist/search_*.css'
]
)
if (files.length > 0) {
done()
} else {
throw new Error('No files found')
}
})
})
单元测试与测试覆盖率
- 单纯的测试框架,需要断言库(mocha,ava)
- chai
- should.js
- expect
- better-assert
- 集成框架,开箱即用(Jasmine,Jest)
- 极简API
编写单元测试用例
技术选型: Mocha + Chai
测试代码: describe, it, expect
测试命令: mocha add.test.js ```javascript // add.test.js const expect = require(‘chai’).expect const add require(‘../src/add’)
describe(‘use expect: src/add.js’, () => { it(‘add(1, 2) === 3’, () => { expect(add(1, 2).to.equal(3)) }) })
<a name="XYoQs"></a>
## 单元测试接入
1. 安装 mocha + chai
`npm i mocha chai -D`
2. 新建 test 目录, 并增加 xxx.test.js 测试文件
2. 在package.json 中的 scripts 字段增加test
```json
"scripts": {
"test": "node_modules/mocha/bin/_mocha"
}
- 执行测试命令
npm run test
持续集成的作用
优点:
- 快速发现错误
- 防止分支大幅偏离主干
核心措施是,代码集成到主干之前,必须通过自动化测试。 只要有一个测试用例失败,就不能集成。
接入Travis CI
- https://travis-ci.org/ 使用github 账号登录
- 在https://travis-ci.org/account/repositories为项目开启
- 项目根目录下新增 .travis.yml
travis.yml文件内容
install 安装项目依赖
script 运行测试用例 ```yaml language: node_js # 语言
sudo: false # 是否需要sudo权限
cache: # 是否开启缓存 apt: true, directories:
- node_modules
node_js: stable # 设置相应的版本
install:
- npm install -D # 安装构建器依赖
- cd ./test/template-project
- npm install -D # 安装模板项目依赖
script:
- npm test
<a name="JTPG9"></a>
# 发布到 npm
<a name="BMSNM"></a>
## 添加用户
npm adduser
<a name="pdrx6"></a>
## 升级版本
1. 升级补丁版本号:npm version patch
1. 升级小版本号:npm version minor
1. 升级大版本号:npm version major
<a name="BD7c1"></a>
### 发布版本
npm publish
<a name="7wOec"></a>
# Git 规范和Changelog 生成
良好的Git commit规范优势:
- 加快Code Review 的流程
- 根据Git Commit的元数据生成Changelog
- 后续维护者可以知道Feature 被修改的原因
技术方案<br />
<a name="h08Iv"></a>
## 提交格式要求
```markdown
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
格式说明:
type代表某次提交的类型,比如是修复一个bug还是增加一个新的feature,所有的type类型如下:
- feat: 新增feature
- fix: 修复bug
- docs: 仅仅修改了文档,比如:README, CHANGLOG, CONTRIBUTE等等
- style: 仅仅修改了空格、格式缩进、逗号等等,不改变代码逻辑
- refactor: 代码重构,没有加新功能或者修复bug
- perf: 优化相关,比如提升性能、体验
- test: 测试用例,包括单元测试、集成测试等
- chore: 改变构建流程、或增加依赖库、工具等
- revert: 回滚到上一个版本
本地开发阶段增加precommit钩子
安装 husky
npm i husky -D通过commitmsg钩子校验信息
"scripts": { "commitmsg": "validate-commit-msg", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s-r 0" }, "devDependencies": { "validate-commit-msg": "^2.11.1", "conventional-changelog-cli": "^1.2.0", "husky": "^0.13.1" }开源项目版本信息案例
软件的版本通常由三位组成,比如:X.Y.Z
- 版本是严格递增的,此处是:16.2.0 -> 16.3.0 ->16.3.1
- 在发布重要版本时,可以发布alpha, rc等先行版本
- alpha和 rc等修饰版本的关键字后面可以带上次数和meta信息
遵守semver规范的优势
- 避免出现循环依赖
-
语义化版本(Semantic Versioning)规范格式
主版本号: 当你做了不兼容的API修改
- 次版本号: 当你做了向下兼容的功能性新增
-
先行版本号
先行版本号可以作为发布正式版本之前的版本,格式是在修订版本号后面上一个连接号(-),再加上一连串以点(.)分割的标识符,标识符可以由英文、数字和连接号([0-9A-Za-z-])组成
alpha: 是内部测试版,一般不向外发布,会有很多bug。一般只有测试人员使用
- beta:也是测试版,这个阶段的版本会一直加入新的功能,在Alpha版本之后推出
- rc:Release Candidate系统平台上就是发行候选版本。RC版本不会再加入新功能,主要着重于除错。
webpack构建速度和体积优化策略
初级分析:使用webpack 内置的stats
方法一:stats 构建的统计信息
package.json中使用stats
"scripts": {
"build": "webpack --env production --json > stats.json"
}
方法二: node.js中使用
const webpack = require('webpack')
const config = require('./webpack.config.js')('production')
webpack(config, (err, stats) => {
if (err) {
return console.log(err)
}
if (stats.hasErrors()) {
return console.log(stats.toString("errors-only"))
}
console.log('stats')
})
速度分析:使用speed-measure-webpack-plugin
const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasureWebpackPlugin()
const webpackConfig = smp.wrap({
plugins: [
new MyPlugin(),
new MyOtherPlugin()
]
})
// 可以看到每个loader和插件执行耗时
体积分析: 使用webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
// 构建完成后会在8888端口展示大小
使用高版本的webpack和nodejs
使用 webpack4优化原因
- V8带来的优化(for of 替代 forEach, map和set 替代 Object, includes替代indexOf)
- 默认使用更快的md4 hash算法
- webpack AST可以直接从loader传递给AST,减少解析时间
-
多进程 / 多实例构建
资源并行解析可选方案thread-loader
parallel-webpack
- HappyPacks
HappyPack
原理: 每次webpack解析一个模块,HappyPack会将它及它的依赖分配给worker线程中exports.plugins = [ new HappyPack({ id: 'jsx', threads: 4, loaders: ['babel-label'] }), new HappyPack({ id: 'styles', threads: 2, loaders: ['style-loader', 'css-loader', 'less-loader'] }) ]使用 thread-loader解析资源
原理: 每次webpack解析一个模块,thread-loader会将它及它的依赖分配给worker线程中module.exports = smp.wrap({ entry: entry, output: { path: path.join(__dirname, 'dist'), filename: '[name]_[chunkhash:8].js' }, mode: 'production', module: { rules: [ { test: /\.js$/, use: [ { loader: 'thread-loader', options: { workers: 3 } }, 'babel-loader' ] } ] } })
多进程并行压缩代码
方法一: 使用 webpack-parallel-uglify-plugin 插件
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')
module.exports = {
plugins: [
new ParalleUglifyPlugin({
uglifyJS: {
output: {
beautify: false,
comments: false
},
compress: {
warning: false,
drop_console: true,
collapse_vars: true,
reduce_vars: true
}
}
})
]
}
方法二:uglifyjs-webpack-plugin开启parallel参数
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
plugins: [
new UglifyJsPlugin({
uglifyOptions: {
warnings: false,
parse: {},
compress: {},
mangle: true,
output: null,
toplevel: false,
nameCache: null,
ie8: false,
keep_fnames: false
},
parallel: true
})
]
}
方法三: terser-webpack-plugin开启parallel参数(推荐)
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
parallel: 4
})
]
}
}
分包:设置Externals
思路: 将react 、react-dom基础包通过cdn引入,不打入bundle中
方法:使用 html-webpack-externals-plugin(上面已经用过了)
进一步分包:预编译资源模块
思路: 将react, react-dom, redux, react-redux基础包和业务基础包打包成一个文件
方法: 使用 DLLPlugin进行分包, DllReferencePlugin 对 mainifest.json引用
第一步: 使用DllPlugin进行分包
const path = require(path)
const webpack = require('webpack')
module.exports = {
conext: process.cwd,
resolve: {
extensions: ['.js', '.jsx', '.json', '.less', '.css'],
modules: [__dirname, 'node_modules']
},
entry: {
library: [
'react',
'react-dom',
'redux',
'react-redux'
]
},
// 注意:这个library一定要与webpack.DllPlugin配置中的name完全一样
output: {
filename: '[name]_[hash:8].dll.js',
path: path.resolve(__dirname, './build/library'),
library: '[name]_[hash:8]'
},
plugins: [
new webpack.DllPlugin({
name: '[name]_[hash:8]',
path: './build/library/[name].json'
})
]
}
第二步: 使用DllReferencePlugin引用manifest.json
// 在webpack.config.js中引入
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
manifest: require('./build/library/[name].json')
})
]
}
页面引用效果(需要手动引入,否则找不到)
<html>
<body>
<div>...</div>
<script src='/build/library/library_xxxxxxxx.dll.js'></script>
</body>
</html>
缓存
目的: 提升二次构建速度
缓存思路:
- babel-loader开启缓存
- terser-webpack-plugin开启缓存
使用cache-loader或hard-source-webpack-plugin
缩小构建目标
目的:尽可能的少构建模块,比如babel-loader不解析node_modules
module.exports = { module: { rules: [ { test: /\.js$/, loader: 'happypack/loader', exclude: 'node_modules' } ] } }减少文件搜索范围
优化resolve.modules配置(减少模块搜索层级)
优化resolve.mainFields配置
优化resolve.extensions配置
合理使用aliasmodule.exports = { resolve: { alias: { react: path.resolve(__dirname, './node_modules/react/dist/react.min.js') }, modules: [path.resolve(__dirname, 'node_modules')], extensions: ['.js'], mainFields: ['main'] } }图片压缩
要求: 基于node库的imagemin或tinypng API
使用: 配置 image-webpack-loaderrules: [{ test: /\.(png|svg|jpg|jpeg|gif|blob)$/, use: [{ loader: 'file-loader', options: { name: `${filename}img/[name]${hash}.[ext]` } },{ loader: 'image-webpack-loader', options: { mozjpeg: { progressive: true, quality: 65 }, optipng: { enabled: false }, pngquant: { quality: '65-90', speed: 4 }, gifsicle: { interlaced: false }, webp: { quality: 75 } } }] }]Imagemin优点分析
有很多定制选项
- 可以引入更多第三方优化插件,例如: pngquart
-
Imagemin的压缩原理
pngquant: 是一款PNG压缩器,通过将图像转换为具有alpha通道(通常比24/32位PNG文件小60-80%)的更高效的8位PNG格式 ,可显著减少文件大小
- pngcrush: 其主要目的是通过尝试不同的压缩级别和PNG过滤方法来降低PNG IDAT数据流的大小
- optipng: 其设计灵感来自于pngcrush。 optipng可将图像文件重新压缩为更小尺寸,而不会丢失任何信息
- tinypng: 也是将24位PNG文件转化为更小有索引的8位图片,同时所有非必要的metadata也会被剥离掉
使用动态Polyfill服务
构建体积优化:动态Polyfill
| 方案 | 优点 | 缺点 | 是否采用 |
|---|---|---|---|
| babel-polyfill | React16官方推荐 | 1. 包体积200k+,难以单独抽离Map,Set 1. 项目里react是单独引用的cdn,如果要单独构建一份在react前加载 |
否 |
| babel-plugin-transfrom-runtime | 只能polyfill用到的类或方法,相对体积较小 | 不能polyfill原型上的方法,不适用业务项目的复杂开发环境 | 否 |
| 自己写map,set的polyfill | 定制化高体积小 | 1. 重复造轮子,容易在日后年久失修成为坑 1. 即使体积小,依然所有用户都要加载 |
否 |
| polyfill-service | 只给用户返回需要的polyfill,社区维护 | 国内部分奇葩浏览器UA可能无法识别(但可以降级返回所需全部polyfill) | 是 |
polyfill service原理
识别 user Agent, 下发不同的Polyfill
webpack打包原理
启动分析过程
查找webpack入口文件
在命令行运行 npm run dev 或 npm run build 命令之后,npm会让命令工具进入node_modules.bin目录查找是否存在webpack.sh或者webpack.cmd文件,如果存在,就执行,不存在就抛出错误
实际上的入口文件是: node_modules\webpack\bin\webpack.js
分析webpack的入口文件:webpack.js
process.exitCode = 0 // 1. 正常执行返回
const runCommand = (command,args) => {...} // 2. 运行某个命令
const isInstalled = packageName => {...} // 3. 判断某个包是否安装
const CLIs = [...] // 4. webpack 可用的CLI:webpack-cli和webpack-command
const installedClis = CLIs.filter(cli => cli.installed) // 5. 判断是否两个cli是否安装
if (installedClis.length === 0) {...} // 6. 根据安装数量进行处理
else if (installedClis.length === 1) {...}
else {...}
启动后的结果
webpack最终找到webpack-cli(webpack-command)这个npm包,并且执行CLI
webpack-cli源码阅读
webpack-cli 做的事情
- 引入yargs, 对命令行进行定制
- 分析命令行参数,对各个参数进行转换,组成编译配置项
- 引用webpack,根据配置项进行编译和构建
从NON_COMPILATION_CMD分析出不需要编译的命令
```javascript const { NON_COMPILATION_ARGS } = require(“./utils/constants”);
const NON_COMPILATION_CMD = process.argv.find(arg => { if (arg === “serve”) { global.process.argv = global.process.argv.filter(a => a !== “serve”); process.argv = global.process.argv; } return NON_COMPILATION_ARGS.find(a => a === arg); });
if (NON_COMPILATION_CMD) { return require(“./utils/prompt-command”)(NON_COMPILATION_CMD, …process.argv); }
<a name="5B0m9"></a>
### NON_COMPILATION_ARGS的内容
```javascript
const NON_COMPILATION_ARGS = [
"init", //创建一份webpack配置文件
"migrate", //进行webpack版本迁移
"serve", //运行webpack-serve
"generate-loader", //生成webpack loader代码
"generate-plugin", //生成webpack plugin代码
"info" //返回与本地环境相关的一点信息
// “add” //往webpack配置文件中增加属性
// “remove” //往webpack配置文件中删除属性
]
webpack-cli 使用args分析
参数分组config/config-args.js,将命令划分为9类
- Config options: 配置相关参数(文件名称、运行环境等 )
- Basic options: 基础参数(entry设置 、debug设置、 watch设置、devtool设置 )
- Module options: 模块参数,给loader设置扩展
- Output options: 输出参数(输出路径、输出文件名称)
- Advanced options 高级用法(记录设置、缓存设置、监听频率、bail等)
- Resolving options: 解析参数(alias和解析文件后缀设置)
- Optimizing options: 优化参数
- Stats options: 统计参数
- options:通用参数(帮助命令,版本信息等)
webpack-cli执行结果
webpack-cli 对配置文件和命令行参数进行转换最终生成配置选项参数options
最终会根据配置参数实例化webpack对象,然后执行构建流程webpack的本质
可以将其理解是一种基于事件流的编程范例,一系列的插件运行。Tapable是什么 ?
Tapable是一个类似于Nodde.js的EventEmitter的库,主要是控制钩子函数的发布与订阅,控制着webpack的插件系统,
Tapable库暴露了很多Hook(钩子)类,为插件提供挂载的钩子const { Tapable, SyncHook, //同步钩子 SyncBailHook, //同步熔断钩子 SyncWaterfallHook, //同步流水钩子 SyncLoopHook, //同步循环钩子 AsyncParallelHook, //异步并发钩子 AsyncSeriesHook, //异步串行钩子 AsyncSeriesBailHook, //异步串行熔断钩子 AsyncSeriesWaterfallHook //异步串行熔流水钩子 } = require("tapable")Tapable hooks类型
| type | function |
|---|---|
| Hook | 所有钩子的后缀 |
| Waterfall | 同步方法,但它会传值给下一个函数 |
| Bail | 熔断:当函数有任何返回值,就会在当前执行函数停止 |
| Loop | 监听函数返回true表示继续循环,返回undefined表示结束循环 |
| Sync | 同步方法 |
| AsyncSeries | 异步串行钩子 |
| AsyncParallel | 异步并行执行钩子 |
使用new hook新建钩子
Tapable 暴露出来的都是类方法,new一个类方法获得我们需要的钩子
class接受数组参数options,非必传。类方法会根据传参,接受同样数量的参数
const hook1 new SyncHook([‘arg1’, ‘arg2’, ‘arg3’])
使用钩子的绑定与执行
提供同步&异步绑定钩子的方法,并且他们都有绑定事件和执行事件对应的方法
| Async* | Sync* |
|---|---|
| 绑定:tapAsync/tabPromise/tap | 绑定:tap |
| 执行:callAsync/promise | 执行:call |
基本用法示例
const hook = new SyncHook(['arg1, arg2, arg3'])
// 绑定事件到webpack事件流
hook.tap('hook1', (arg1, arg2, arg3) => {
console.log(arg1, arg2, arg3)
})
// 执行绑定事件
hook.call(1, 2, 3)
实际例子演示
定义一个Car方法,在内部hooks上新建钩子,分别是同步钩子accelerate(接受一个参数)、brake,异步钩子calculateRoutes
使用钩子对应的绑定和执行方法
calculateRoutes使用tapPromise可以返回一个promise对象
动手实现一个简易的webpack
可以将ES6语法转换成ES5语法
- 通过babylon生成 AST
- 通过bable-core将AST重新生成源码
可以分析模块之间的依赖关系
- 通过babel-traverse的ImportDeclaration方法获取依赖属性
web商城的性能优化策略
- 渲染优化
- 首页,列表页,详情页采用SSR或Native渲染
- 个人中心页预渲染
- 弱网优化
- 使用离线包、PWA等离线缓存技术
- Webview优化
- 打开Webview的同时并行地加载页面数据
相关代码webpack
