第七课 网易项目webpack配置解析¶
统计信息:字数 17870 阅读36分钟
webpack版本是4;目标是:自己可以看懂90%的配置文件,并自定义plugin和loader
Webpack 不同等级的使用者:
初级:可以通过CLI搭建基本的webpack并打包项目
中级:可以更改一部分配置文件(loader plugin)
高级:使用webpack解决项目的问题
2024年备注:webpack5 已经使用在生产项目,注意更新知识体系
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
{
"author": "xxx",
"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 打包函数
// webpack 本身是一个方法 webpack(config);
function buildDll() {
return new Promise((resolve, reject) => {
// 使用webpack开始编译,第一个是配置对象
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();
});
});
}
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();
});
}
// 删除默认的dist目录
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err;
// 如果是在线模式
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脚本也很简单
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 config = require('../config');
const merge = require('webpack-merge');
const baseWebpackConfig = require('./webpack.base.conf');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const env = require('../config/prod.env');
// 如果构建模式不是Online构建,那么设置环境变量的发布环境,为测试环境
const webpackConfig = merge(baseWebpackConfig, {
// merge 方法用于合成配置文件(基本配置和build配置文件)
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 帮助手动拷贝内容(未打包的字体图标或者图片)
webpack.DefinePlugin¶
指定当前的环境变量(打包阶段定义全局变量)
可以使用 webpack --env production 通过命令行的形式传参,或者使用这个对象指定当前的环境变量是开发环境还是生产环境,在业务代码中获取到当前的环境变量。
// --env process.env 无法在业务代码中拿到(所以要初始化定义环境,把用户输入的环境放在node中)
plugins: [
new webpack.DefinePlugin({
'process.env': env,
}),
// 定义环境(测试环境还是生产环境,不需要每次指定,--env 比较麻烦)
new webpack.DllReferencePlugin({
context: path.join(__dirname, '..'),
manifest: require('./vendor-manifest.json')
}),
// extract css into its own file
new MiniCssExtractPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css')
}),
// keep module.id stable when vender modules does not change
new webpack.HashedModuleIdsPlugin(),
// webpack 只会把处理的模块进入打包结果。
// enable scope hoisting
new webpack.optimize.ModuleConcatenationPlugin(),
]
prod.env.js 简化版
'use strict';
module.exports = {
NODE_ENV: '"production"',
publish_env: '"online"'
};
练习
plugins: [
new webpack.DefinePlugin({
'process.env': env
}),
new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false
}
},
sourceMap: config.build.productionSourceMap,
parallel: true
}),
]
插件都放在 plugins 数组中,创建一个插件的实例
webpack.HashedModuleIdsPlugin¶
保持模块的 module.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 屏蔽错误
new webpack.NoEmitOnErrorsPlugin(),
],
});
webpack.NoEmitOnErrorsPlugin¶
这个插件在上面已经使用了。如果代码出现问题,webpack 默认不会继续编译,显示错误。这个插件可以继续编译并让浏览器显示(操作更友好)。
webpack.providePlugin¶
如果我们在全局中使用某些库,例如jquery,可以使用这个插件
base.conf.js
对于 axios jquery 等通用组件,每个组件都需要import,可以只用这个插件。直接在这里定义,不需要在不同组件中全局定义,定以后可以打包到环境中(React等是否可以这样使用?)
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中
可以使用dtable这样的大项目测试一下编译的时间
03 webpack 中常见问题¶
如果对模块内容进行处理:loader 是首选方案;
如果要加入特殊的功能:可以自定义增加插件 plugin;
项目中打包简化:可变性配置:通过编写响应的操作函数;
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 是对某一类文件进行处理(css-loader sass-loader)
plugin 是监听到 webpack 的某个过程(make)执行的一个操作(webpack插件系统的生命周期)
环境和目录¶
这部分和 01 有重复
- 开发环境(dev)webpack.dev.conf.js
- 测试环境(test)Test.js 打包测试文件,而不是打包业务代码
- 生产环境(prod)
对应不同的配置文件
基本配置文件:webpack.base.conf.js (主要是loaders) vue-loader, babel-loader, url-loader(handle image file jpg), url-loader(handle meida file mp4)
在不同环境中,把基本配置和特定环境的配置项目 merge 成一个配置文件。
例如:生产环境下执行下面的操作
node build.js
JS内部使用 webpack.base.conf.js and webpack.prod.conf.js 合并文件,从 config 中拿出 index.js and pro.env 中的环境变量,然后进行生产环境下面的打包。
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 webpackConfig = require('./webpack.prod.conf.js');
const spinner = ora('building starting');
spinner.start();
// delete dist dir
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) {
throw err;
}
// start webpack
webpack(webpackConfig, (err, stats) => {
spinner.stop();
if (err) {
throw err;
}
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false, // ts-loader set it true
chunks: false,
chunkModules: false
}) + '\n\n');
if (stats.hasErrors()) {
console.log(chalk.red('build with error.\n'));
process.exit(1);
}
console.log(chalk.cyan('build complete.\n'));
});
});