一 样式处理
01 CSS预处理
import './style.css'
多loader时,从后到前执行
{
rules: [
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2, // scss 里通过 @import 引用 scss问题
modules: true, // css 模块化
localIdentName: '[name]__[local]__[has:base64:5]' // 指定类名规则
sourceMap: true // 在浏览器的调试工具里查看源码
}
},
{
loader: 'scss-loader',
options: {
sourceMap: true // 在浏览器的调试工具里查看源码
}
},
postcss-loader
]
}
]
}
css-loader里的importLoaders 是用来编译 scss里@import scss文件的。
不然scss里@import scss文件会直接使用css-loader, 不会通过前面的'scss-loader' 'postcss-loader'编译。
postcss-loader 用来增加浏览器前缀,依赖autoprefixer, 需配置
postcss.config.js
module.exports={
plugins: [require('autoprefixer')]
}
02 以<link>方式加载css
使用mini-css-extract-plugin代替style-loader
module: {
rules:[
test:/\.less$/,
use:[
MiniCssExtractPlugin.loader,
'css-loader'
]
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[id].css'
})
]
03 CSS模块化
import './style.css' 这种方式引入的CSS会作用于全局,下面让css也具有模块特点
- css-loader中增加 modules: true
- localIdentName 指定css代码中的类名会如何编译 如:localIdentName: '[name][local][has:base64:5]' style.css
.title{
color: #999;
}
编译后为 .style__title__1CFy6 3. 引入方式改为
import style from './index.scss'
(<h1 class=`${styles.title}`>My Webpack app</h1>)
在css-loader增加modules选项,说明打开CSS Modules 支持
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true
}
}
]
}
Scope
默认情况下,CSS将所有类名暴露到全局作用域中。样式可在局部作用域中,避免全局作用域污染。 语法 :local(.className)可被用来在局部作用域中声明className,会以模块形式暴露出去。
:local(.className) { background: red; }
:local .className { color: green; }
:local(.className .subClass) { color: green; }
:local .className .subClass :global(.global-class-name) { color: blue; }
._23_aKvs-b8bW2Vg3fwHozO { background: red; }
._23_aKvs-b8bW2Vg3fwHozO { color: green; }
._23_aKvs-b8bW2Vg3fwHozO ._13LGdX8RMStbBE9w-t0gZ1 { color: green; }
._23_aKvs-b8bW2Vg3fwHozO ._13LGdX8RMStbBE9w-t0gZ1 .global-class-name { color: blue; }
04 css px自动转换成rem
使用px2rem-loader 页面渲染时计算根元素的font-size值 也可使用手淘的lib-flexible库 github.com/amfe/lib-fl…
npm i px2rem-loader -D
npm i lib-flexible -S
module.exports={
module:{
rules:[
test:/\.less$/,
use:[
'style-loader',
'css-loader',
'less-loader',
{
loader: "px2rem-loader",
options:{
remUnit: 75, // 1rem == 75px 对应750宽
remPrecision: 8 // rem小数位数
}
}
]
]
}
}
二 环境配置
在生产环境中我们关注的是如何让用户更快地加载资源,涉及如何压缩、添加环境变量优化打包、最大限度地利用缓存等。
01 拆分Development 和 Production
npm i -D webpack-merge build/ -webpack.base.js -webpack.dev.js -webpack.prod.js
package.json
{
"scripts": {
"dev": "webpack-dev-server --config ./build/webpack.dev.js",
"build": "webpack --config ./build/webpack.prod.js"
}
}
webpack.base.js
module.exports = {
entry: {
main: 'src/inde.js'
},
module: {
//...
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new CleanWebpackPlugin(['../dist'], {
root: path.resolve(__dirname, '../')
})
],
output: {
filename: '[name].js',
path: path.resolve(__dirname, '../dist')
}
}
webpack.prod.js
const merge = require('webpack-merge')
const baseConfig = require('./weboack.base.js')
const prodConfig = {
mode: 'production', // 自动添加生产环境配置项
devtool: 'cheap-module-source-map',
}
module.exports = merge(baseConfig, prodConfig)
webpack.dev.js
const merge = require('webpack-merge')
const baseConfig = require('./weboack.base.js')
const devConfig = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
devServer: {
contentBase: './dist',
open: true,
port: 8080,
hot: true
},
plugins:[
new wepback.HotModuleReplacementPlugin()
],
optimization: {
usedExports: true
}
}
module.exports = merge(baseConfig, devConfig)
02 环境变量
通常我们需要为production 和 development
cross-env+NODE_ENV
npm i -D cross-env
"script":{
"build":"cross-env NODE_ENV=production webpack --config webpack.config.js"
}
webpack.base.js使用环境变量
const isProduction = process.env.NODE_ENV === 'production';
module.exports = {
devtoo: isProduction ? null : 'source-map'
}
DefinePlugin
添加不同的环境变量,在webpack中可使用DefinePlugin进行设置
plugins: [
new webpack.DefinePlugin({
ENV: JSON.stringify('production')
})
]
app.js
document.write(ENV)
除了字符串类型的值外,还可设置五体类型的环境变量
new webpack.DefinePlugin({
ENV: JSON.stringify('production'),
IS_PRODUCTION: true,
ENV_ID: 120912098,
CONSTANTS: JSON.stringify({
TYPES: ['foo', 'bar']
})
})
DefinePlugin在替换环境变量时对字符串是完全替换,不添加JSON.stringify的话,在替换后会成为变量名,而非字符串。因此这里都要加上JSON.stringify
许多框架与库都采用“process.env.NODE_ENV”作为一个区别开发环境和生产环境的变量。 process.env是nodeJS用于存放当前进程环境变量的对象; 而NODE_ENV则可让开发者者指定当前的运行时环境,当它的值为production时即代表当前为生产环境,库可框架代码在打包时就可据此去掉一此开发环境的代码,如警告信息和日志等。
new webpack.DefinePlugin({
process.env.NODE_ENV: 'production'
})
如果启用了mode: production,则webpack已经设置好了process.env.NODE_ENV,不需要手动添加
三 生产环境配置
01 SourceMap
跟踪代码出错位置 source-map 会降底打包速度
module.exports = {
mode: 'development',
devtool: 'source-map'
}
对于CSS SCSS LESS来说,需要添加额外的source map配置项
{
loader: 'css-loader',
options: { sourceMap: true } // 在浏览器的调试工具里查看源码
},{
loader: 'scss-loader',
options: {sourceMap: true } // 在浏览器的调试工具里查看源码
}
eval 打包最快 inline-source-map 映射代码被加入到main.js cheap-inline-source-map 错误只提示到行,不提示第几个字符(只针对业务代码) cheap-module-source-map 同时跟踪第三方module代码
development 环境推荐 cheap-module-eval-source-map production 环境推荐 cheap-module-source-map
关于SourceMap文章
- segmentfault.com/a/119000000…
- www.html5rocks.com/en/tutorial…
- www.ruanyifeng.com/blog/2013/0…
- www.youtube.com/watch?v=NkV…
在将资源发布到线上环境前,我们通常会进行代码压缩,或者叫uglify
02 JS资源压缩
两个工具,一个是UglifyJS(webpack3),另一个terser-webpack-plugin(webpack4)。后者支持ES6+代码的压缩,更加面向未来。
在webpack3的话,开启压缩需要调用webpack.optimize.UglifyJsPlugin
plugins: [new webpack.optimize.UglifyJsPlugin()]
webpack4之后,这项配置被移到了config.optimization.minimize,如果开启了mode:production则不需要手动设置
optimization: {
minimize: true
}
terser-webpack-plugin插件支持自定义配置
| 配置项 | 类型 | 默认值 | 功能描述 |
|---|---|---|---|
| test | string/RegExp/Array<string/RegExp> | /.m?js(?.*)?$/i | terser的作用范围 |
| include | string/RegExp/Array<string/RegExp> | undefined | 包含目录 |
| exclude | string/RegExp/Array<string/RegExp> | undefined | 排除目录 |
| cache | Boolean/String | false | 是否开启缓存 |
| parallel | Boolean/String | false | 多进程压缩,强烈建议开启 |
| sourceMap | Boolean | false | 是否生成source map |
| terserOptions | Object | {...defalut} | terser压缩配置,如是否可对变量重命名,是否兼容IE8 |
03 压缩CSS
压缩CSS文件的前提是将样式提取出来,接着使用Plugin 压缩
webpack3 使用 mini-css-extract-plugin提取CSS
然后PurgecssPlugin压缩
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({
paths: glob.sync(`$(PATHS.src)/**/*`,{nodir: true})
})
]
}
}
webpack4 使用 splitChunks提取CSS
然后OptimizeCssAssetsWebpackPlugin压缩,这个插件本质上使用的是压缩器cssnano webpack.prod.js
const prodConfig = {
optimization: {
splitChunks: {
cacheGroups: {
styles: { // 单入口 单css
name: 'styles',
test: /\.css$/, // 只要是css文件就打包到styles中
chunks: 'all',
enforce: true // 忽略默认配置项
},
mainStyles: { // 多入口时,main文件下的css打包的main.css中
name: 'main',
test: (m,c,entry="main") => m.costructor.name ==='CssModule' && recursiveIssuer(m) === entry,
chunks: 'all',
enforce: true // 忽略默认配置项
},
adminStyles: { // 多入口时,main文件下的css打包的main.css中
name: 'admin',
test: (m,c,entry="admin") => m.costructor.name ==='CssModule' && recursiveIssuer(m) === entry,
chunks: 'all',
enforce: true // 忽略默认配置项
}
}
},
minimizer: [
new OptimizeCssAssetsWebpackPlugin({}) // 压缩css
]
}
}
04 多进程并行压缩
terser-webpack-plugin 开启parallel参数(webpack4默认推荐,支持ES6压缩)
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
parallel: 4 // CPU数量 可输入 false true
})
]
}
}
05 缓存
缓存指重复利用浏览器已经获取过的资源。
一个常用的方法是在每次打包工程中对资内容计算一次hash
output:{
filename: 'bundle@[chunkhash].js'
}
06 输出动态HTML
资源名改变意味着html引用的路径也要改变
plugins: [
new HtmlWebpackPlugin({
template: './index.html'
})
]
07 体积监控可分析
- VS Code插件Import Cost可帮助我们对引入模块的大小进行实时监测
- webpack-bundle-analyzer分析bundle构成
四 开发环境调优
01 webpack-dashboard
webpack每次构建结束后都会在控制台输出一些打包相关的信息,但是这些信息是以列表的形式展示的,有时会显得不够直观。webpack-dashboard就是更好地展示这些信息的 npm i webpck-dashboard webpack.prod.js
plugins: [
new DashboardPlugin()
]
package.json
"scripts":{
"dev": "webpack-dashboard -- webpack-dev-server"
}
02 速度分析 speed-measure-webpack-plugin
觉得webpack构建很慢但又不清楚如何下手优化吗?试试SMP。SMP可分析出webpack整个打包工程中在各个loader和plugin上耗费的时间,这将会有助于找出构建过程中的性能瓶颈。
SMP使用非常简单,只要用它的warp方法包裹在webpack的配置对象外面即可。 npm i speed-measure-webpack-plugin -D
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin")
const smp = new SpeedMeasurePlugin()
const webpackConfig = smp.wrap({
plugins: [
new MyPlugin(),
new MyOtherPlugin()
]
})
03 体积分析:使用webpack-bundle-analyzer
构建后会在8888端口展示大小
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
可分析依赖的第三方模块大小、业务组件大小
05 Hot Module Replacement
让代码在网页不刷新的前端下得到最新的改动,首先我们要确保项目是基于webpack-dev-server进行开发的。webpack本身的命令行不支持HMR
const webpack = require('webpack')
module.exports = {
devServer: {
hot: true,
hotOnly: true // 浏览器不刷新
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}
修改配置后需重启
css-loader vue react自带监听module更新重绘组件,但是使用偏门资源时,需要自己写重绘代码
import number from './nomber';
if(module.hot){
module.hot.accept('./number',()=>{
document.body.removeChild(document.getElementById('number'))
number()
})
}
06 WebpackDevServer
WebpackDevServer可看作一个服务者,它的主要工作就是接收浏览器的请求,然后返回资源。 当WebpackDevServer接收到浏览器的资源请求时,它会先进行URL地址检验。如果该地址是资源服务地址(publicPath),就会从webpack的打包结果中寻找该资源并返回给浏览器。反之,则直接读取硬盘文件。
两大职能:
- 令webpack进行模块打包,并处理打包结果的资源请求
- 作为普通的web server,处理静态资源文件请求
代码变动,自动编译 webpack.config.js
module.exports = {
devServer: {
contentBase: './dist',
publicPath: './dist',
open: true,
proxy: {
'/api':'http://localhost:3000'
}
}
}
package.json
"scripts": {
"watch": "webpack --watch",
"start": "webpack-dev-server"
}
使用WebpackDevServer 代理
业务中通常的ajax请求
axios.get('/react/api/header.json')
.then((res)=>{
console.log(res)
})
webpack.config.js
module.exports: {
devServer: {
contentBase: './dist',
open: true,
port: 8080,
hot: true,
hotOnly: true,
proxy: {
'/react/api': {
target: 'http://localhost:3000', // 开发环境代理
secure: false, // https转发
pathRewrite: {
'header.json': 'demo.json' // 后端完成前的demo数据
'^/api': '', // 以/api开头全部忽略
},
changeOrigin: true, // 突破origin限制
bypass: function(req, res, proxyOptions){ // 拦截
if(req.headers.accept.indexOf('html') !== -1){
console.log('Skipping proxy for browser request.')
return '/index.html' // 返回指定html
return false // 跳过代理
}
}
},
},
proxy:[{
context: ['./auth', './api'], // 多路径代理到同一个target
target: 'http://localhost:3000'
}],
proxy:{ // 代理 / 也就是根目录时需配置index为false或 ''
index: '',
'/':{ /* ... */}
},
}
}
自定义中间件做mock server
WebpackDevServer中有两个时间可插入自己实现的中间件,分别是devServer.before / devServer.after,即WebpackDevServer加载所有内部中间件之前和之后两个时机。
devServer: {
before(app, server){
app.get('/some/path', (req,res)=>{
res.json({custom: 'response'})
})
}
}
自定义中间件常被用来做mock server WebpackDevServer提供了自定义中间件的Hook,所以我们可实现简单自己的mock server 下面在devServer.before插入一个接口 /api/mock.json
devServer: {
port: 9000,
before(app, server){
app.get('/api/mock.json',(req, res)=>{
res.json({Hello: 'world')
})
}
}
启动dev server,访问http://localhost:9000/api/mock.json就可看到这个接口返回的数据
WebpackDevServer 解决单页应用路由问题
<BrowserRouter>
<div>
<Route path='/' exact component={Home} />
<Route path='/list' component={List}/>
</div>
</BrowserRouter>
wepback.config.js
module.exports ={
devServer: {
historyApiFallback: {
rewrites: [{
from: '/abc.html/',
to: '/list.html'
}]
},
historyApiFallback: true, //绑定main.js,解决路由跳转问题,等价于下面写法
historyApiFallback: {
rewrites: [{
from: /\*/,
to: '/index.html'
}]
},
}
}