网易项目webpack配置解析¶
统计信息:字数 16390 阅读33分钟
create time 2020-01-01
last modify time 2024-04-12
课程 webpack 版本是4
学习目标:自己可以看懂90%的配置文件,并自定义plugin和loader
01 环境与目录¶
环境分类:开发、测试、生产
开发环境:增加开发服务器操作
测试环境:测试环境和生产环境很接近
生产环境:增加 tree-shaking devtool(source-map)操作(压缩操作)
不同模式下对应不同的文件:
- 开发环境下 npm run dev => dev.config.js;
- 生产环境下 npm run build => prod.config.js。
实际操作时,有一个 base.config.js 是基础默认配置,不同环境都会执行,运行时会执行多个脚本。
npm run build
实际执行了什么操作?
node build.js
# 内部脚本:使用 webpack 打包(webpack.pro.js)进行打包
# webpack.base.js 与 webpack.pro.js 合并
# 从 config 中拿出 index.js 和 pro.env 中的环境变量
package.json
{
"scripts": {
"start": "npm run dev",
"test": "npm run unit && npm run e2e",
"lint": "eslint --ext .js src test/unit test/e2e/specs",
"build": "node build/build.js",
"build-online": "node build/build.js online",
"css": "sass --watch --scss --no-cache --unix-newlines src:src -t compressed",
}
}
Build.js (这是网易项目中自定义的build脚本)
'use strict';
// 执行检查版本函数
require('./check-versions')();
// 设置node环境是生产环境
process.env.NODE_ENV = 'production';
// 默认的创建环境是空
process.env.BUILD_MODE = '';
// 判断传参:如果传参是在线模式,那么把创建环境设置为在线(把terminal中的参数变成全局变量使用)
if (!!process.argv[2] && process.argv[2] === 'online') {
process.env.BUILD_MODE = 'online';
}
// 下面是基本的第三方库
const ora = require('ora');
const rm = require('rimraf');
const path = require('path');
const chalk = require('chalk');
const webpack = require('webpack');
const config = require('../config');
const webpackDllConfig = require('./webpack.dll.config');
// 开始加载 loading 动画
const snipper = ora('building for production...');
snipper.start();
// dll 打包函数
function buildDll() {
return new Promise((resolve, reject) => {
// 使用webpack开始编译webpack 本身是一个方法 webpack(config);
// 第一个参数是配置对象,第二个参数是回调函数
webpack(webpackDllConfig, (err, stats) => {
// 编译结束后,停止loading
spinner.stop();
// 抛出编译的错误
if (err) throw err;
// 控制台输出编译的结果(配置)
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false, // if ts-loader, it is true
chunk: false,
chunkModules: false,
}) + '\n\n');
// 如果编译成功,但是有错误,那么显示错误
if (stats.hasErrors()) {
console.log(chalk.red('Build failed with errors.\n'));
// Promise 抛出拒绝
reject();
// 退出进程
process.exit();
}
// 如果编译成功,提示成功文本
console.log(chalk.cyan('Build complete.\n'));
console.log(chalk.yellow('Tip: build files are meant to be served over an HTTP server.\n Opening index.html over file:// will not work.\n'));
resolve();
});
});
}
// build 打包函数
// 不同的 build 使用不同配置文件,其他配置类似(打印日志)
function buildProject(config) {
return new Promise((resolve, reject) => {
webpack(config, (err, stats) => {
spinner.stop();
if (err) throw err;
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false,
}) + '\n\n');
});
// 处理异常基本相同
if (stats.hasErrors()) {
console.log(chalk.red('Build failed with errors.\n'));
reject();
process.exit(1);
}
console.log(chalk.cyan('Build complete.\n'));
resolve();
});
}
// 脚本开始运行
// 01 删除默认的dist目录(清空打包环境)
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err;
// 02 判断模式,开始编译
// 在线模式
if (process.env.BUILD_MODE === 'online') {
// 首先编译DLL,执行上面的函数
buildDll().then(() => {
// 引入生产环境配置
return require('./webpack.prod.conf');
}).then((config) => {
// 使用生产环境配置编译项目
return buildProject(config);
});
}
// 测试模式(测试环境配置)
else {
buildDll().then(() => {
return require('./webpack.test.conf');
}).then((config) => {
return buildProject(config);
});
}
});
使用第三方插件创建的build.js 也差不多,@vue/cli 创建的build脚本。
在命令行中输入 webpack config.js 和执行 node build.js 并在JS文件中使用的效果是一样的。
代码中,webpack 本质上是一个方法。
const webpack = require('webpack');
webpack(config);
打包过程是异步的,所以先进行DLL打包,然后再引入生产环境配置,进行下一步打包。
build 脚本:导入配置文件,调用webpack打包方法进行打包
build.js
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) {
throw err;
}
webpack(webpackConfig, (err, stats) => {
if (err) {
throw err;
}
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false,
}) + '\n\n');
if (stats.hasError()) {
console.log(chalk.red('Build failed with errors'));
process.exit(1);
}
console.log(chalk.cyan('Build complete.'));
});
});
prod 脚本中,有一个webpack-merge 方法,可以合并多个脚本
配置的本质就是一个对象,merge就是合并多个对象
webpack.prod.conf.js
const path = require('path');
const utils = require('utils');
const webpack = require('webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const merge = require('webpack-merge');
const config = require('../config');
const baseWebpackConfig = require('./webpack.base.conf');
const env = require('../config/prod.env');
// 如果构建模式不是Online构建,那么设置环境变量的发布环境,为测试环境
// merge 方法用于合成配置文件(基本配置和build配置文件)
const webpackConfig = merge(baseWebpackConfig, {
mode: 'production',
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true,
usePostCSS: true,
})
},
devtool: false,
output: {
path: config.build-assetsRoot,
filename: utils.aeestsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
optimization: {
runtimeChunk: {
name: 'manifest'
},
splitChunk: {
//
}
}
})
02 常用 loader 和插件¶
常用插件 plugins,下面依次介绍
- webpack.DefinePlugin 在打包阶段定义全局变量
- webpack.HashedModuledsPlugin 保持 module.id 稳定
- webpack.NoEmitOnErrorsPlugin 屏蔽错误
- webpack.providePlugin 提供插件库
- copy-webpack-plugin 可以帮助拷贝内容
全部的插件官方文档:https://v4.webpack.docschina.org/plugins/
webpack.DefinePlugin¶
指定当前的环境变量(打包阶段定义全局变量)
可以使用 webpack --env production 通过命令行的形式传参,或者使用这个对象指定当前的环境变量是开发环境还是生产环境,在业务代码中获取到当前的环境变量。
// --env process.env 无法在业务代码中拿到(所以要初始化定义环境,把用户输入的环境放在node中)
plugins: [
// 定义环境(测试环境还是生产环境,不需要每次指定,--env 比较麻烦)
new webpack.DefinePlugin({
'process.env': env,
}),
]
prod.env.js 简化版
'use strict';
module.exports = {
NODE_ENV: '"production"',
publish_env: '"online"'
};
webpack.DllReferencePlugin¶
plugins: [
// DllReferencePlugin 将打包输出的内容 映射关系放置到项目中,在打包的时候,忽略这些文件
new webpack.DllReferencePlugin({
context: path.join(__dirname, '..'),
manifest: require('./vendor-manifest.json')
}),
]
MiniCssExtractPlugin¶
官方推荐使用mini-css-extract-plugin插件来打包css文件(从css文件中提取css代码到单独的文件中,对css代码进行代码压缩等)。
plugins: [
// extract css into its own file
new MiniCssExtractPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css')
}),
]
ModuleConcatenationPlugin¶
过去 webpack 打包时的一个取舍是将 bundle 中各个模块单独打包成闭包。这些打包函数使你的 JavaScript 在浏览器中处理的更慢。相比之下,一些工具像 Closure Compiler 和 RollupJS 可以提升(hoist)或者预编译所有模块到一个闭包中,提升你的代码在浏览器中的执行速度。
plugins: [
// enable scope hoisting
new webpack.optimize.ModuleConcatenationPlugin(),
]
webpack.HashedModuleIdsPlugin¶
保持模块的 module.id 稳定
该插件会根据模块的相对路径生成一个四位数的hash作为模块id, 建议用于生产环境。
如何判断一个文件是新的还是旧的(浏览器读取新文件,还是读取缓存文件),就根据文件后面的hash值判断。所以webpack打包输出的文件中就增加了哈希值。
const webpackConfig = merge(baseWebpackConfig, {
mode: 'production',
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true,
usePostCSS: true,
})
},
devtool: false,
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js'),
},
plugins: [
// keep module.id stable when vender modules does not change
// 如果 static 路径下面的第三方库文件没有改变,那么不需要重新打包这部分代码
new webpack.HashedModuleIdsPlugin(),
],
});
webpack.NoEmitOnErrorsPlugin¶
如果代码出现问题,webpack 默认不会继续编译,显示错误。
这个插件可以继续编译并让浏览器显示(操作更友好)。
plugins: [
// webpack.NoEmitOnErrorsPlugin 屏蔽错误
new webpack.NoEmitOnErrorsPlugin(),
],
webpack.providePlugin 提供第三方库¶
如果我们在全局中使用某些库,例如jquery,可以使用这个插件
base.conf.js
对于 axios jquery 等通用组件,每个组件都需要 import,可以只用这个插件。直接在这里定义,不需要在不同组件中全局定义,定以后可以打包到环境中。
plugins: [
new webpack.ProvidePlugin({
Regular: 'Regular',
$: 'jquery',
axios: 'axios',
}),
...utils.htmlPlugin(),
new HappyPack({
id: 'happybabel',
loader: ['babel-loader?cacheDirectory=true'],
threadPool: happyThreadPool,
}),
]
copy-webpack-plugin 可以帮助拷贝内容¶
这个插件不是自带的,需要安装
const CopyWebpackPlugin = require('copy-webpack-plugin');
plugins: [
// copy-webpack-plugin 可以帮助拷贝内容
// 直接把一部分 static 的代码拷贝到打包后的目录中(不需要手动 mv 命令)
// webpack 只会处理打包的模块,例如static中有100张图片,可以使用这个插件
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.dev.assetsSubDirectory,
ignore: ['.*'],
}
])
],
03 优化打包的内容¶
DLL优化¶
plugins: [
new webpack.DllReferencePlugin({
manifest: require("./dll/vender-manifest.json")
})
]
什么是DLL优化?我们需要用第三方库,不会修改第三方库的内容,每次webpack打包会处理第三方库代码。既然第三方库代码不变,我们可以先把第三方库代码处理了,放在一边,然后下一次打包不需要再次处理这部分代码,直接使用。
import $ from 'jquery';
import _ from 'lodash';
webpack.dll.js
const webpack = require('webpack');
module.exports = {
entry: {
vender:['jquery', 'lodash']
},
output: {
path: __dirname + '/dll',
filename: '[name].dll.js',
library: '[name]_library'
},
plugins: [
new webpack.DllPlugin({
path: __dirname + '/dll/[name]-manifest.json',
name: '[name]_library'
})
]
};
bash 打包第三方库
webpack --config webpack.dll.js
输出 vender.dll.js
然后在webpack配置文件中增加这个文件
plugins: [
new webpack.DllReferencePlugin({
manifest: require("./dll/vender-manifest.js")
})
]
使用 webpack 继续打包项目代码
HappyPack¶
const happyPack = require('happypack');
// 配置连接池,容量等于CPU的个数(适合多核CPU并行打包)
const happyThreadPool = HappyPack.ThreadPool({
size: os.cpus().length
});
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'happypack/loader?id=happybabel'
}
]
}
]
},
plugins: [
new HappyPack({
// 这里的ID和上面的ID必须相同,否则报错
id: 'happybabel',
loaders: ['babel-loader?cacheDirectory=true'],
threadPool: happyThreadPool,
}),
// 支持其他类型文件的编译
new HappyPack({
// 这里的ID和上面的ID必须相同,否则报错
id: 'happybabel',
loaders: ['scss-loader?cacheDirectory=true'],
threadPool: happyThreadPool,
})
]
如果项目较小,打包编译的时间反而更多
因为这里使用多进程,调用进程也消耗时间
所以文件组件较少时,使用 Happypack 可能增加打包时间。
如果大型项目,那么使用这个可以节省很多时间。
这是一个第三方库,需要单独安装到dev中
04 webpack 自定义插件¶
项目中打包简化:可变性配置:通过编写响应的操作函数;
Myloader.js 自定义增加插件(使用正则替换代码中的字符,类似于AST,抽象语法树,类似中间件的语法)开发的时候,我们使用 static 中的图片,生产环境中需要使用 www.baidu.com 中的图片,所以可以自定义一个插件替换开发环境中的变量。
module.exports = function(context) {
context.replace('bind', 'on');
return context;
}
使用
const require('./myplugin');
module: {
rules: [
{
test: /\.js$/,
// loader: 'babel-loader'
use: [
{ loader: 'babel-loader' },
{ loader: './myloader.js' },
]
}
]
}
index.js 插件就是监听 webpack 的生命周期函数,并在合适的时候处理代码
const fs = require('fs');
const path = require('path');
module.exports = a;
function a () {
}
a.prototype.apply = function(compiler) {
compiler.hooks.done.tap('changeStitic', function(compilation) {
let context = compiler.options.context;
let publickPath = path.resolve(context, 'dist');
compilation.toJson().assets.forEach((ast) => {
const filePath = path.resolve(publickPath, ast.name);
fs.readFile(filepath, function(err, file) {
var newcontext = file.toString().replace('./static', 'www.baidu.com');
fs.writeFile(filePath, newcontext, function() {})
});
})
})
}
make 周期需要处理很多编译的配置,新手不好做,done 周期直接操作编译后的文件,相对简单
总结¶
如果对模块内容批量进行处理:loader 是首选方案;
如果要加入特殊的功能:可以自定义增加插件 plugin;
loader 是对某一类文件进行处理(css-loader sass-loader)
plugin 是监听到 webpack 的某个过程(make)执行的一个操作(webpack插件系统的生命周期)