webpack整合笔记再探

832 阅读6分钟

一 样式处理

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也具有模块特点

  1. css-loader中增加 modules: true
  2. 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文章

在将资源发布到线上环境前,我们通常会进行代码压缩,或者叫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'
            }]   
        },
       
    }
}