webpack从配置到跑路v3

区分不同环境

目前我们的webpack配置都定义在webpack.config.js中,我们可以通过 process.env.NODE_ENV 区分不同的环境,从而加载不同的配置。
创建多个配置文件:

  • webpack.base.js 公共配置
  • webpack.dev.js 开发环境的配置
  • webpack.prod.js 生产环境的配置

webpack-merge 专为webpack设计,提供了一个 merge 函数,用于连接数组、合并对象,根据不同的环境生成不同的配置文件。

npm i webpack-merge -D
//合并示例
merge({
    devtool: 'cheap-module-eval-source-map',
    module: { rules: [{a: 1}] },
    plugins: [1,2,3]
}, {
    devtool: 'none',
    mode: "production",
    module: { rules: [{a: 2}, {b: 1}] },
    plugins: [4,5,6],
});
--> 合并后:
{
    devtool: 'none',
    mode: "production",
    module: { rules: [{a: 1}, {a: 2}, {b: 1}] },
    plugins: [1,2,3,4,5,6]
}

创建配置文件的目录config,存放配置文件webpack.base.config.js(公共配置)、webpack.dev.config.js(开发环境)、webpack.prod.config.js(生产环境)

  • webpack.dev.config.js
    const merge = require('webpack-merge');
    const baseConfig = require('./webpack.base.config.js');
    module.exports = merge(baseConfig, {
        mode: 'development',
        devtool: "cheap-module-eval-source-map",
        devServer: {
            //...
        }
        //...
    })
    
  • package.json 启动时指定配置文件
    "scripts": {
        "dev": "cross-env NODE_ENV=development webpack-dev-server --config=webpack.config.dev.js",
        "build": "cross-env NODE_ENV=production webpack --config=webpack.config.prod.js"
    },
    

可以使用 merge 合并,也可以使用 merge.smart 合并。merge.smart 在合并loader时,会将同一匹配规则的进行合并。

定义环境变量

DefinePluginwebpack的一个内置插件,允许创建一个在编译时可以配置的全局常量。这可能会对开发模式和生产模式的构建允许不同的行为非常有用。
如果在预发布构建中执行日志记录,而不在线上构建中执行,则可以使用全局常量来决定是否记录日志。设置DefinePlugin,就可以忘记开发环境和生产环境构建的规则。

每个传进DefinePlugin的键值都是一个标志符,或者多个用 . 连接起来的标志符。

  • 如果值是一个字符串,它会被当作一个代码片段来使用;
  • 如果值不是字符串,它会被转化为字符串(包括函数);
  • 如果值是一个对象,正常对象定义即可;
  • 如果在一个 key 前面加了 typeof,它会被定义为 typeof 调用。
plugins: [
    new webpack.DefinePlugin({
        PRODUCTION: JSON.stringify(true),
        DEV: JSON.stringify('dev'),  // 字符串
        BROWSER_SUPPORTS_HTML5: true,
        FLAG: 'true',  // 这是个布尔类型
        TWO: '1+1',   // 这是个数值型
        'typeof window': JSON.stringify('object'),
        'process.env.firstName': JSON.stringify("hello webpack")
    })
]

注意:因为这个插件直接执行文本替换,给定的值必须包含字符串本身内的实际引号。通常有两种方式来达到这个效果,使用 '"xxxx"', 或者使用 JSON.stringify('xxxx')。

src/index.js 中使用这些变量

console.log(PRODUCTION)  // true
console.log(DEV)         // "dev"
console.log(BROWSER_SUPPORTS_HTML5)   // true
console.log(FLAG)         // true
console.log(TWO)         // 2
console.log(typeof window)         // "object"
console.log(process.env.firstName)    // "hello webpack"
if(PRODUCTION) {
   // ...
} else {
   // ...
}

webpack解决跨域

webpack可以通过配置代理 devServer.proxy 在前端解决跨域。

//webpack.config.js
module.exports = {
    //...
    devServer: {
        proxy: "http://xxx.yyy.zzz:4000"
    }
}

proxy 可以是一个字符串、数组、对象,有多种配置方式,适用各种场景。

模拟数据

模拟请求数据不再仅仅是后端的独家工作,前端一样可以!

  • 简单的数据模拟
// webpack.config.js
module.exports = {
    devServer: {
        before(app) {
            app.get('/user', (req, res) => {
                res.json({info: 'hello webpack'})
            })
        }
    }
}

// src/index.js
fetch("user")
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(err => console.log(err));
// npm run dev  --> {info: 'hello webpack'}
  • mocker-api
    mocker-apiREST API 创建模拟 API。在没有实际 REST API 服务器的情况下测试应用程序时,它会很有用。

mocker-api 的安装与使用

npm i mocker-api -D
  • 创建 mock/mocker.js
module.exports = {
    'GET /user': {name: 'webpack4'},
    'POST /login/account': (req, res) => {
        const { password, username } = req.body
        if (password === '888888' && username === 'admin') {
            return res.send({
                status: 'ok',
                code: 0,
                token: 'sdfsdfsdfdsf',
                data: { id: 1, name: 'webpack4' }
            })
        } else {
            return res.send({ status: 'error', code: 403 })
        }
    }
}
  • webpack.config.js
const apiMocker = require('mocker-api');
module.export = {
    //...
    devServer: {
        before(app){
            apiMocker(app, path.resolve('./mock/mocker.js'))
        }
    }
}
  • src/index.js
// get
fetch("user")
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(err => console.log(err));
// post
fetch("/login/account", {
    method: "POST",
    headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        username: "admin", password: "888888"
    })
})
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(err => console.log(err));
  • npm run dev 查看控制台

配置优化

量化

有时候我们以为的优化可能是负优化,希望能有一个量化的指标可以看出前后对比。
speed-measure-webpack-plugin插件可以做速度分析,测量各个插件和loader所花费的时间;

npm i -D speed-measure-webpack-plugin

// webpack.config.js
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
    // ... webpack配置
});

输出信息:

量化指标.png

exclude/include

exclude/include 配置loader时使用,确保loader尽可能少的处理文件。

rules: [
    {
        test: /\.js[x]?$/,
        use: ['babel-loader'],
        include: [path.resolve(__dirname, 'src')]
    }
]

exclude(排除)的优先级高于include(仅包含),在include/exclude中使用绝对路径的数组。
结合量化指标,可以查看加上include/exclude和不加的构建速度。

cache-loader

对于一些性能开销比较大的loader,可以使用cache-loader,将结果缓存到磁盘中,默认保存在node_modueles/.cache/cache-loader目录下。
cache-loader的配置很简单,只需要放在其他loader之前即可;

npm i cache-loader -D

// webpack.config.js
rules: [
    {
        test: /\.jsx?$/,
        use: ['cache-loader', 'babel-loader']
    }
]

如果只打算给babel-loader配置cache的话,可以不用cache-loader,给babel-loader增加选项cacheDirectory

use: {
    loader: 'babel-loader',
    options: {
        cacheDirectory: true,  // 开启缓存
        // ...
    }
}
// 或者
loader: 'babel-loader?cacheDirectory=true'

cacheDirectory 默认为false。当有设置时,指定的目录将用来缓存loader的执行结果。之后的Webpack构建将会尝试读取缓存,来避免在每次执行时可能产生的、高性能消耗的Babel重新编译过程。设置true时,使用默认缓存目录node_modules/.cache/babel-loader

happyPack

在webpack构建过程中,有大量jscss、图片、字体等文件需要loader解析和处理,这些转换操作还不能并发处理文件,而是一个个文件进行处理;随着文件越来越多,构建速度必然会变慢。
happyPack的基本原理是,将转换任务分解到多个子进程中去并行处理,子进程处理完成后把结果发送到主进程中,从而减少总的构建时间;

npm i happypack -D

// webpack.config.js
const happyPack = require('happypack');
module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: 'happypack/loader?id=js'
            },
            {
                test: /\.css$/,
                use: 'happypack/loader?id=css',
                include: [
                    path.resolve(__dirname, 'src'),
                    path.resolve(__dirname, 'static')
                ]
            }
        ],
    },
    plugins: [
        new HappyPack({
            id: 'js',  // 和rules中的id=js对应,标识Happypack处理哪类文件
            use: ['babel-loader'] //必须是数组,指定所使用的处理器Loader
            // loaders: ['babel-loader']
        }),
        new HappyPack({
            id: 'css',
            use: ['style-loader', 'css-loader','postcss-loader'],
        }),
    ]
}

happypack默认开启 CPU核数 - 1 个进程,当然也可以threadsHappypack

注意:postcss-loader配置在Happypack中,必须要在项目中创建postcss.config.js,否则会抛出错误: Error: No PostCSS Config found

module.exports = {
    plugins: [
        require('autoprefixer')()
    ]
}

thread-loader

除了使用Happypack外,也可以使用thread-loader,把thread-loader放置在其他loader之前,那么后面的loader就会在一个单独的worker池中运行。
worker(worker pool)中运行的loader是受到限制的,比如:

  • 这些loader不能产生新的文件;
  • 这些loader不能使用定制的loader API
  • 这些loader无法获取webpack的选项设置。
npm i thread-loader -D

// webpack.config.js
rules: [
    {
        test: /\.jsx?$/,
        // babel-loader耗时比较长,所以给它配置 thread-loader
        use: ['thread-loader', 'babel-loader']
    }
]

开启JS多进程压缩

虽然很多webpack优化的文章上会提及多进程压缩的优化,不管是webpack-parallel-uglify-plugin或是uglifyjs-webpack-plugin配置parallel。其实没必要单独安装这些插件,它们并不会让Webpack构建速度提升。
当前Webpack默认使用的是terser-webpack-pluginuglifyjs-webpack-plugin不支持ES6语法),默认就开启了多进程和缓存。构建时,项目中可以看到terser的缓存文件node_modules/.cache/terser-webpack-plugin

HardSourceWebpackPlugin

HardSourceWebpackPlugin为模块提供中间缓存,缓存默认存放路径是node_modules/.cache/hard-source
配置hard-source-webpack-plugin,首次构建时间没有太大变化,但是第二次开始,构建时间大约可以节约80%

npm i hard-source-webpack-plugin -D

// webpack.config.js
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
module.exports = {
    //...
    plugins: [
        new HardSourceWebpackPlugin()
    ]
}

HardSourceWebpackPlugin文档 中列出了一些可能会遇到的问题以及如何解决,例如热更新失效,或者某些配置不生效等。

noParse

如果一些第三方模块如jquery 、lodash 没有AMD/CommonJS规范版本,可以使用noParse来标识这个模块,这样Webpack会引入这些模块,但是不进行转化和解析,从而提升Webpack的构建性能。简而言之,忽略大型的library以提高构建性能。

noParse属性的值是一个正则表达式或是一个function

//webpack.config.js
module.exports = {
    //...
    module: {
        noParse: /jquery|lodash/
    }
}

如果使用到了不需要解析的第三方依赖,那么配置 noParse 可以起到一定的优化作用。

IgnorePlugin

webpack的内置插件,作用是忽略第三方包指定的目录。
比如 moment v2.24.0会将所有本地化内容和核心功能一起打包,那么就可以使用IgnorePlugin在打包时忽略本地化内容。

//webpack.config.js
module.exports = {
    //...
    plugins: [
        // 忽略 moment 下的 ./locale 目录
        new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
    ]
}

在使用时,如果需要指定语言,那么要手动引入语言包

import moment from 'moment';
import 'moment/locale/zh-cn'; // 手动引入中文语言

index.js中只引入moment,打包出来的bundle大小为263KB;配置了IgnorePlugin,单独引入moment/locale/zh-cn,构建的包大小为55KB

externals

我们可以将一些JS文件存储在CDN上(减少webpack打包出来的js体积),在index.html中通过<script>标签引入。

<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>

我们希望在使用时仍然可以通过 import 的方式去引用(import $ from 'jquery'),并且希望webpack不会对其进行打包,那么配置externals就好了。

//webpack.config.js
module.exports = {
    //...
    externals: {
        // jquery通过script引入之后,全局中即有了 jQuery 变量
        'jquery': 'jQuery'
    }
}

DllPlugin

如果将所有的JS文件都打包成一个JS文件,必然会导致最终生成的JS文件很大,这个时候就需要考虑拆分bundles
DllPluginDLLReferencePlugin 可以实现拆分 bundles,并且可以大大提升构建速度,而且它们都是webpack的内置模块。
DllPlugin 可以把复用性较高的第三方模块打包到动态链接库中,在不升级这些库的情况下(版本号没变),动态库不需要重新打包,每次构建只重新打包业务代码,而不再把所有代码都重新构建一次。
新建一个webpack配置文件webpack.config.dll.js,专门用于编译动态链接库,比如将reactreact-dom单独打包成一个动态链接库

//webpack.config.dll.js
const webpack = require('webpack');
const path = require('path');

module.exports = {
    entry: {
        react: ['react', 'react-dom']
    },
    mode: 'production',
    output: {
        filename: '[name].dll.[hash:6].js',
        path: path.resolve(__dirname, 'dist', 'dll'),
        library: '[name]_dll'  // 暴露给外部使用
        //libraryTarget  指定如何暴露内容,缺省时就是 var
    },
    plugins: [
        new webpack.DllPlugin({
            //name和library一致
            name: '[name]_dll', 
             // manifest.json 的生成路径
            path: path.resolve(__dirname, 'dist', 'dll', 'manifest.json')
        })
    ]
}

package.jsonscripts 中增加:

"scripts": {
    // ...
    "build:dll": "webpack --config webpack.config.dll.js"
},

执行npm run build:all,生成的dist目录:

dist
└── dll
    ├── manifest.json
    └── react.dll.9dcd9d.js

之所以将动态链接库单独放在 dll 目录下,主要是为了使用 CleanWebpackPlugin 更为方便的过滤掉动态链接库。
manifest.json 用于让 DLLReferencePlugin 映射到相关依赖上。

配置 webpack.config.js

//webpack.config.js
const webpack = require('webpack');
const path = require('path');
module.exports = {
    //...
    devServer: {
        contentBase: path.resolve(__dirname, 'dist')
    },
    plugins: [
        new webpack.DllReferencePlugin({
            manifest: path.resolve(__dirname, 'dist', 'dll', 'manifest.json')
        }),
        new CleanWebpackPlugin({
             // 不删除dll目录
            cleanOnceBeforeBuildPatterns: ['**/*', '!dll', '!dll/**']
        }),
        //...
    ]
}

使用 npm run build 构建,可以看到bundle.js的体积大大减少,构建速度也明显加快。
修改 index.html,手动引入react.dll.js

<script src="/dll/react.dll.9dcd9d.js"></script>

抽离公共代码

如果多个页面引入了一些公共模块,那么可以把这些公共模块抽离出来,单独打包。公共代码只需要下载一次就缓存起来,避免重复下载。
抽离公共代码对于对于单页面应用和多页应用在配置上没什么区别,都是配置在optimization.splitChunks中。

//webpack.config.js
module.exports = {
    optimization: {
        splitChunks: {  //分割代码块
            cacheGroups: {
                vendor: {
                    // 第三方依赖,如 lodash
                    priority: 1,  //设置优先级,首先抽离第三方模块
                    name: 'vendor',
                    test: /node_modules/,
                    chunks: 'initial',
                    minSize: 0,
                    minChunks: 1 //最少引入了1次
                },
                //缓存组
                common: {
                    // 公共模块,比如 utils.js
                    chunks: 'initial',
                    name: 'common',
                    minSize: 100,  //大小超过100个字节
                    minChunks: 3  //最少引入了3次
                }
            }
        }
    }
}

如果打包出来的bundle.js体积过大,则可以将一些依赖打包成动态链接库,然后将剩下的一些第三方依赖拆出来。
optimization.runtimeChunk 的作用是将包含chunk映射关系的列表从main.js中抽离出来,在配置splitChunk时,还应该配置runtimeChunk

optimization: {
    runtimeChunk: {
        name: 'manifest'
    },
    splitChunks: {
        // ...
    }
}

最终构建出来的文件中会生成一个 manifest.js

webpack-bundle-analyzer

在做webpack构建优化时,vendor的体积超过1Mreactreact-dom 已经打包成了DLL
所以需要借助分析插件 webpack-bundle-analyzer 查看下哪些包的体积较大

npm install webpack-bundle-analyzer -D

//webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
    //....
    plugins: [
        //...
        new BundleAnalyzerPlugin(),
    ]
}

执行 npm run build 构建,会默认打开 http://127.0.0.1:8888/,可以看到各个包的体积

analyzer

进一步对 vendor 进行拆分,将 vendor 拆分成了4个(使用 splitChunks 进行拆分即可)

// webpack.config.js
    optimization: {
        concatenateModules: false,
        // ...
        splitChunks: {//分割代码块
            maxInitialRequests:6, //默认是5
            cacheGroups: {
                vendor: {
                    //第三方依赖
                    priority: 1,
                    name: 'vendor',
                    test: /node_modules/,
                    chunks: 'initial',
                    minSize: 100,
                    minChunks: 1 //重复引入了几次
                },
                'lottie-web': {
                    name: "lottie-web", // 单独将 react-lottie 拆包
                    priority: 5, // 权重需大于`vendor`
                    test: /[\/]node_modules[\/]lottie-web[\/]/,
                    chunks: 'initial',
                    minSize: 100,
                    minChunks: 1 //重复引入了几次
                },
                // ...
            }
        },
    },
// 重新构建
npm run build

webpack自身的优化

  • tree-shaking 如果使用ES6import语法,那么在生产环境下,会自动移除没有使用到的代码;
    //math.js
    const add = (a, b) => {
        console.log('aaaaaa')
        return a + b;
    }
    const minus = (a, b) => {
        console.log('bbbbbb')
        return a - b;
    }
    export { add, minus }
    
    //index.js
    import {add, minus} from './math';
    add(2,3);
    
    构建的最终代码里,minus 函数不会被打包进去。
  • scope hosting 作用域提升
    变量提升,可以减少一些变量声明。在生产环境下,默认开启。
    另外,大家测试的时候注意一下,speed-measure-webpack-pluginHotModuleReplacementPlugin 不能同时使用,否则会报错。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343