基础篇
webpack 的基本概念和⽇常开发的实⽤技巧
一、webpack 与构建发展简史
初识 webpack:配置⽂件名称
webpack 默认配置⽂件:webpack.config.js
可以通过 webpack --config 指定配置⽂件
初识 webpack:webpack 配置组成
module.exports = {
entry: './src/index.js',// 1.打包的⼊⼝⽂件
output: './dist/main.js',//2.打包的输出
mode: 'production',// 3.环境
module: {
rules: [ // 4.Loader 配置
{ test: /\.txt$/, use: 'raw-loader' }
]
},
plugins: [ // 5.插件配置
new HtmlwebpackPlugin({
template: ''./src/index.html'
})
]
};
零配置 webpack包含哪些内容?
module.exports = {
entry: './src/index.js', // 1.指定默认的 entry 为: ./src/index.js
output: './dist/main.js',// 2.指定默认的 output 为: ./dist/main.js
mode: 'production', // 3
module: {
rules: [ // 4
{ test: /\.txt$/, use: 'raw-loader' }
]
},
plugins: [ // 5
new HtmlwebpackPlugin({
template: './src/index.html’
})
]
};
安装 webpack 和 webpack-cli
- npm install webpack webpack-cli --save-dev
- 检查是否安装成功:./node_modules/.bin/webpack -v
Webpack初体验:⼀个最简单的例⼦
通过 npm script 运⾏ webpack
{
"name": "hello-webpack",
"version": "1.0.0",
"description": "Hello webpack",
"main": "index.js",
"scripts": {
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC"
}
二、webpack 基础⽤法
核⼼概念之 Entry
- Entry ⽤来指定 webpack 的打包⼊⼝
理解依赖图的含义
Entry 的⽤法
单⼊⼝:entry 是⼀个字符串
module.exports = {
entry: './path/to/my/entry/file.js'
};
多⼊⼝:entry 是⼀个对象
module.exports = {
entry: {
app: './src/app.js',
adminApp: './src/adminApp.js'
}
};
核⼼概念之 Output
- Output ⽤来告诉 webpack 如何将编译后的⽂件输出到磁盘
Output 的⽤法:单⼊⼝配置
module.exports = {
entry: './path/to/my/entry/file.js'
output: {
filename: 'bundle.js’,
path: __dirname + '/dist'
}
};
Output 的⽤法:多⼊⼝配置
module.exports = {
entry: {
app: './src/app.js',
search: './src/search.js'
},
output: {
filename: '[name].js', // 通过占位符确保⽂件名称的唯⼀
path: __dirname + '/dist'
}
};
核⼼概念之 Loaders
webpack 开箱即用只支持 JS 和 JSON 两种文件类型,通过 Loaders 去支持其它文 件类型并且把它们转化成有效的模块,并且可以添加到依赖图中。
本身是一个函数,接受源文件作为参数,返回转换的结果。
常⻅的 Loaders 有哪些?
Loaders 的⽤法
const path = require('path');
module.exports = {
output: {
filename: 'bundle.js'
},
module: {
rules: [
{ test: /\.txt$/, use: 'raw-loader' } // test 指定匹配规则 ; use 指定使⽤的 loader 名称
]}
};
核⼼概念之 Plugins
插件⽤于 bundle ⽂件的优化,资源管理和环境变量注⼊
作⽤于整个构建过程
常⻅的 Plugins 有哪些?
Plugins 的⽤法
const path = require('path');
module.exports = {
output: {
filename: 'bundle.js'
},
plugins: [ // 放到 plugins 数组⾥
new HtmlWebpackPlugin({template: './src/index.html'})
]
};
核⼼概念之 Mode
Mode ⽤来指定当前的构建环境是:production、development 还是 none
设置 mode 可以使⽤ webpack 内置的函数,默认值为 production
Mode 的内置函数功能
核⼼概念之 资源解析
资源解析:解析 ES6
资源解析:增加ES6的babel preset配置
资源解析:解析 React JSX
资源解析:解析 CSS、解析 Less 和 SaSS
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
+ module: {
+ rules: [
+ {
+ test: /\.css$/,
+ use: [
+ 'style-loader', // style-loader 将样式通过 <style> 标签插⼊到 head 中
+ 'css-loader', // css-loader ⽤于加载 .css ⽂件,并且转换成 commonjs 对象
+ 'less-loader' // less-loader ⽤于将 less 转换成 css
+ ]
+ }
+ ]
+ }
};
资源解析:解析图⽚、解析字体
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
+ module: {
+ rules: [
+ {
+ test: /\.(png|svg|jpg|gif)$/,
+ use: [
+ 'file-loader', // file-loader ⽤于处理⽂件
+ ]
+ },
+ {
+ test: /\.(woff|woff2|eot|ttf|otf)$/,
+ use: [
+ 'file-loader'// file-loader 也可以⽤于处理字体
+ ]
+ }
+ ]
+ }
};
资源解析:使⽤ url-loader
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
+ module: {
+ rules: [
+ {
+ test: /\.(png|svg|jpg|gif)$/,
+ use: [{ + loader: 'url-loader’, // url-loader 也可以处理图⽚和字体
+ options: {
+ limit: 10240 // 可以设置较⼩资源⾃动 base64
+ }
+ }]
+ },
+ ]
+ }
};
webpack中的⽂件监听
⽂件监听是在发现源码发⽣变化时,⾃动重新构建出新的输出⽂件。
webpack 开启监听模式,有两种⽅式:
·启动 webpack 命令时,带上 --watch 参数
·在配置 webpack.config.js 中设置 watch: true
webpack中的⽂件监听使⽤
⽂件监听的原理分析
轮询判断⽂件的最后编辑时间是否变化
某个⽂件发⽣了变化,并不会⽴刻告诉监听者,⽽是先缓存起来,等 aggregateTimeout
module.export = {
//默认 false,也就是不开启
watch: true,
//只有开启监听模式时,watchOptions才有意义
wathcOptions: {
//默认为空,不监听的文件或者文件夹,支持正则匹配
ignored: /node_modules/,
//监听到变化发生后会等300ms再去执行,默认300ms
aggregateTimeout: 300,
//判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
poll: 1000
}
}
热更新:webpack-dev-server
热更新:使⽤ webpack-dev-middleware
热更新的原理分析
什么是⽂件指纹?
- 打包后输出的⽂件名的后缀
⽂件指纹如何⽣成
Hash:和整个项⽬的构建相关,只要项⽬⽂件有修改,整个项⽬构建的 hash 值就会更改
Chunkhash:和 webpack 打包的 chunk 有关,不同的 entry 会⽣成不同的 chunkhash 值
Contenthash:根据⽂件内容来定义 hash ,⽂件内容不变,则 contenthash 不变
JS 的⽂件指纹设置
CSS 的⽂件指纹设置
图⽚的⽂件指纹设置
代码压缩
JS ⽂件的压缩
内置了 uglifyjs-webpack-plugin
CSS ⽂件的压缩
html ⽂件的压缩
module.exports = {
entry: {
app: './src/app.js',
search: './src/search.js'
},
output: {
filename: '[name][chunkhash:8].js',
path: __dirname + '/dist'
},
plugins: [
+ new HtmlWebpackPlugin({ // 修改 html-webpack-plugin,设置压缩参数
+ 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
+ }
+})
]
};
三、webpack 进阶⽤法
当前构建时的问题
每次构建的时候不会清理⽬录,造成构建的输出⽬录 output ⽂件越来越多
通过 npm scripts 清理构建⽬录
rm -rf ./dist && webpack
rimraf ./dist && webpack
⾃动清理构建⽬录
避免构建前每次都需要⼿动删除 dist
使⽤ clean-webpack-plugin:默认会删除 output 指定的输出⽬录
module.exports = {
entry: {
app: './src/app.js',
search: './src/search.js'
},
output: {
filename: '[name][chunkhash:8].js',
path: __dirname + '/dist'
},
plugins: [
+ new CleanWebpackPlugin()
] };
CSS3 的属性为什么需要前缀?
举个例⼦
PostCSS 插件 autoprefixer ⾃动补⻬ CSS3 前缀
浏览器的分辨率
CSS 媒体查询实现响应式布局
// 缺陷:需要写多套适配样式代码
@media screen and (max-width: 980px) {
.header {
width: 900px;
}
}
@media screen and (max-width: 480px) {
.header {
height: 400px;
}
}
@media screen and (max-width: 350px) {
.header {
height: 300px;
}
}
rem 是什么?
W3C 对 rem 的定义: font-size of the root element
rem 和 px 的对⽐:
·rem 是相对单位
·px 是绝对单位
移动端 CSS px ⾃动转换成 rem
资源内联的意义
代码层⾯:
- ⻚⾯框架的初始化脚本
- 上报相关打点
- css 内联避免⻚⾯闪动
请求层⾯:减少 HTTP ⽹络请求数
- ⼩图⽚或者字体内联 (url-loader)
HTML 和 JS 内联
raw-loader 内联 html
<script>${require(' raw-loader!babel-loader!. /meta.html')}</script>
raw-loader 内联 JS
<script>${require('raw-loader!babel-loader!../node_modules/lib-flexible')}</script>
CSS 内联
// ⽅案⼀:借助 style-loader
// ⽅案⼆:html-inline-css-webpack-plugin
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: [
{
loader: 'style-loader',
options: {
insertAt: 'top', // 样式插入到 <head>
singleton: true, //将所有的style标签合并成一个
}
},
"css-loader",
"sass-loader"
],
},
]
},
};
多⻚⾯应⽤(MPA)概念
每⼀次⻚⾯跳转的时候,后台服务器都会给返回⼀个新的 html ⽂档, 这种类型的⽹站也就是多⻚⽹站,也叫做多⻚应⽤。
多⻚⾯打包基本思路
// 每个⻚⾯对应⼀个 entry,⼀个 html-webpack-plugin
// 缺点:每次新增或删除⻚⾯需要改 webpack 配置
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js ‘
}
};
多⻚⾯打包通⽤⽅案
// 动态获取 entry 和设置 html-webpack-plugin 数量
// 利⽤ glob.sync
- entry: glob.sync(path.join(__dirname, './src/*/index.js'))
module.exports = {
entry: {
index: './src/index/index.js',
search: './src/search/index.js ‘
}
};
使⽤ source map
作⽤:通过 source map 定位到源代码
- source map科普⽂:www.ruanyifeng.com/blog/2013/0…
开发环境开启,线上环境关闭
- 线上排查问题的时候可以将 sourcemap 上传到错误监控系统
source map 关键字
eval: 使⽤eval包裹模块代码
source map: 产⽣.map⽂件
cheap: 不包含列信息
inline: 将.map作为DataURI嵌⼊,不单独⽣成.map⽂件
module:包含loader的sourcemap
source map 类型
基础库分离
利⽤ SplitChunksPlugin 进⾏公共脚本分离
利⽤ SplitChunksPlugin 分离基础包
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
commons: { // test: 匹配出需要分离的包
test: /(react|react-dom)/,
name: 'vendors',
chunks: 'all'
}
}
}
}
};
利⽤ SplitChunksPlugin 分离⻚⾯公共⽂件
module.exports = {
optimization: {
splitChunks: {
minSize: 0,// minuSize: 分离的包体积的⼤⼩
cacheGroups: {
commons: {
name: 'commons',
chunks: 'all',
minChunks: 2 // minChunks: 设置最⼩引⽤次数为2次
}
}
}
}
}
tree shaking(摇树优化)
概念:1 个模块可能有多个⽅法,只要其中的某个⽅法使⽤到了,则整个⽂件都会被打到 bundle ⾥⾯去,tree shaking 就是只把⽤到的⽅法打⼊ bundle ,没⽤到的⽅法会在 uglify 阶段被擦除掉。
使⽤:
- webpack 默认⽀持,在 .babelrc ⾥设置 modules: false 即可
- production mode的情况下默认开启
要求:必须是 ES6 的语法,CJS 的⽅式不⽀持
DCE (Dead code elimination)
// 代码不会被执⾏,不可到达
// 代码执⾏的结果不会被⽤到
// 代码只会影响死变量(只写不读)
if (false) {
console.log('这段代码永远不会执行’);
}
Tree-shaking 原理
利⽤ ES6 模块的特点:
·只能作为模块顶层的语句出现
· import 的模块名只能是字符串常量
· import binding 是 immutable的
代码擦除: uglify 阶段删除⽆⽤代码
现象:构建后的代码存在⼤量闭包代码
会导致什么问题?
⼤量作⽤域包裹代码,导致体积增⼤(模块越多越明显)
运⾏代码时创建的函数作⽤域变多,内存开销变⼤
模块转换分析
结论:
被 webpack 转换后的模块会带上⼀层包裹
import 会被转换成 __webpack_require
进⼀步分析 webpack 的模块机制
scope hoisting 原理
原理:将所有模块的代码按照引⽤顺序放在⼀个函数作⽤域⾥,然后适当的重命名⼀ 些变量以防⽌变量名冲突
对⽐: 通过 scope hoisting 可以减少函数声明代码和内存开销
scope hoisting 使⽤
// webpack mode 为 production 默认开启
// 必须是 ES6 语法,CJS 不⽀持
module.exports = {
entry: {
app: './src/app.js',
search: './src/search.js'
},
output: {
filename: '[name][chunkhash:8].js',
path: __dirname + '/dist'
},
plugins: [
+ new webpack.optimize.ModuleConcatenationPlugin()
]
};
代码分割的意义
对于⼤的 Web 应⽤来讲,将所有的代码都放在⼀个⽂件中显然是不够有效的,特别是当你的
某些代码块是在某些特殊的时候才会被使⽤到。webpack 有⼀个功能就是将你的代码库分割成
chunks(语块),当代码运⾏到需要它们的时候再进⾏加载。
懒加载 JS 脚本的⽅式
CommonJS:require.ensure
ES6:动态 import(⽬前还没有原⽣⽀持,需要 babel 转换)
如何使⽤动态 import?
安装 babel 插件
npm install @babel/plugin-syntax-dynamic-import --save-dev
ES6:动态 import(⽬前还没有原⽣⽀持,需要 babel 转换)
{
"plugins": ["@babel/plugin-syntax-dynamic-import"],
...
}
代码分割的效果
ESLint 的必要性
⾏业⾥⾯优秀的 ESLint 规范实践
Airbnb: eslint-config-airbnb、 eslint-config-airbnb-base
腾讯:
-
alloyteam团队 eslint-config-alloy(github.com/AlloyTeam/e…)
-
ivweb 团队:eslint-config-ivweb(github.com/feflow/esli…)
制定团队的 ESLint 规范
ESLint 如何执⾏落地?
和 CI/CD 系统集成
和 webpack 集成
⽅案⼀:webpack 与 CI/CD 集成
本地开发阶段增加 precommit 钩⼦
安装 husky
npm install husky --save-dev
// 增加 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”
]
}
]
}
};
webpack 打包库和组件
webpack 除了可以⽤来打包应⽤,也可以⽤来打包 js 库 实现⼀个⼤整数加法库的打包
- 需要打包压缩版和⾮压缩版本
- ⽀持 AMD/CJS/ESM 模块引⼊
库的⽬录结构和打包要求
// 打包输出的库名称:
// ·未压缩版 large-number.js
// ·压缩版 large-number.min.js
+ |- /dist
+ |- large-number.js
+ |- large-number.min.js
+ |- webpack.config.js
+ |- package.json
+ |- index.js
+ |- /src
+ |- index.js
⽀持的使⽤⽅式
如何将库暴露出去?
如何指对 .min 压缩
// 通过 include 设置只压缩 min.js 结尾的⽂件
module.exports = {
mode: "none",
entry: {
"large-number": "./src/index.js",
"large-number.min": "./src/index.js"
},
output: {
filename: "[name].js",
library: "largeNumber",
libraryTarget: "umd"
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
include: /\.min\.js$/,
}),
],
}
};
设置⼊⼝⽂件
// package.json 的 main 字段为 index.js
if (process.env.NODE_ENV === "production") {
module.exports = require("./dist/large-number.min.js");
} else {
module.exports = require("./dist/large-number.js");
}
⻚⾯打开过程
服务端渲染 (SSR) 是什么?
渲染: HTML + CSS + JS + Data -> 渲染后的 HTML
服务端:
所有模板等资源都存储在服务端
内⽹机器拉取数据更快
⼀个 HTML 返回所有数据
浏览器和服务器交互流程
客户端渲染 vs 服务端渲染
总结:服务端渲染 (SSR) 的核⼼是减少请求
SSR 的优势
减少⽩屏时间
对于 SEO 友好
SSR 代码实现思路
服务端
- 使⽤ react-dom/server 的 renderToString ⽅法将React 组件渲染成字符串
- 服务端路由返回对应的模板
客户端
- 打包出针对服务端的组件
webpack ssr 打包存在的问题
浏览器的全局变量 (Node.js 中没有 document, window)
- 组件适配:将不兼容的组件根据打包环境进⾏适配
- 请求适配:将 fetch 或者 ajax 发送请求的写法改成 isomorphic-fetch 或者 axios
样式问题 (Node.js ⽆法解析 css)
- ⽅案⼀:服务端打包通过 ignore-loader 忽略掉 CSS 的解析
- ⽅案⼆:将 style-loader 替换成 isomorphic-style-loader
如何解决样式不显示的问题?
⾸屏数据如何处理?
当前构建时的⽇志显示
展示⼀⼤堆⽇志,很多并不需要开发者关注
统计信息 stats
如何优化命令⾏的构建⽇志
使⽤效果
如何判断构建是否成功?
在 CI/CD 的 pipline 或者发布系统需要知道当前构建状态
每次构建完成后输⼊ echo $? 获取错误码
构建异常和中断处理
webpack4 之前的版本构建失败不会抛出错误码 (error code)
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);
}
})
}
]