webpack构建工具
重要通知
webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。
基本概况
webpack-dev-server:https://github.com/webpack/webpack-dev-server
安装配置
> pnpm install --save-dev webpack
> webpack -v
> webpack # webpack指令默认执行 webpack --config webpack.config.js
package.json
{
"scripts": {
"dev": "",
"serve": "",
"build": "",
"test": "",
"lint": "",
}
}
配置结构文件
webpack.config.js
const path = require('path');
module.exports = {
/*
* 入口(entry)
*/
// 单个入口
entry: '',
// 对象语法
entry: {},
/*
* 出口(output)
*/
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
// 多个入口起点
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
filename: '[name]-[chunkhash].js'
},
// 混用语法,从输出的 bundle 中排除依赖
externals: [
{
jquery: 'jQuery',
react: 'react',
lodash: {
commonjs: 'lodash',
amd: 'lodash',
root: '_', // 指向全局变量
},
subtract: {
root: ['math', 'subtract'],
},
},
// 函数
function ({ context, request }, callback) {
if (/^yourregex$/.test(request)) {
return callback(null, 'commonjs ' + request);
}
callback();
},
// 正则表达式
/^(jquery|\$)$/i,
],
/*
* 转换器(loader)
*/
module: {},
/*
* 插件(plugins)
*/
plugins: [],
/*
* 开发中 Server(devServer)
*/
devServer: {
contentBase: false,
port: 8080,
proxy: {
"/api": {
target: "http://localhost:8080",
pathRewrite: {"^/api" : ""}
}
}
}
}
webpack.dev.config.js
const { merge } = require("webpack-merge");
module.exports = {
mode: 'development',
}
webpack.prod.config.js
const { merge } = require("webpack-merge");
module.exports = {
mode: 'production',
}
webpack.dll.config.js
webpack.vendor.config.js
公共变量与函数
// 开发环境
if (process.env.NODE_ENV === 'development') {
//
}
// 生产环境
if (process.env.NODE_ENV === 'production') {
//
}
动态链接库
模块功能loader与plugin
typescript模块
安装:pnpm install --save-dev typescript ts-loader
module.exports = {
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ]
},
}
业务功能实现示例
CSS片段与文件
- 抽离提取CSS并独立为CSS文件
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
}
]
},
plugins: [
new ExtractTextPlugin("styles.css")
]
}
- 自动补全前缀autoprefixer
package.json
{
"browserslist": [
"> 1%",
"last 5 versions",
"last 10 Chrome versions",
"last 5 Firefox versions",
"Safari >= 6",
"ie > 8"
]
}
postcss.config.js
module.exports = {
plugins: [
require('autoprefixer')
]
}
js片段与文件
- 编译ES6
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)|path.resolve(__dirname, "src")/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
}
- 分割chunk
const webpack = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new webpack.optimize.AggressiveSplittingPlugin({
minSize: 20, // 字节,分割点。默认:30720
maxSize: 30, // 字节,每个文件最大字节。默认:51200
chunkOverhead: 0, // 默认:0
entryChunkMultiplicator: 1, // 默认:1
}),
// 展示出打包后的各个bundle所依赖的模块
new BundleAnalyzerPlugin()
]
}
- 压缩代码
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
module: {
rules: [
]
},
plugins: [
//ES6需要编译,否则会压缩异常报错
new UglifyJsPlugin({
test: /\.js($|\?)/i
})
]
}
- 拆分 bundles:DLLPlugin 和 DLLReferencePlugin 用某种方法实现了拆分 bundles,同时还大大提升了构建的速度
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('../static/vendor-mainfest.json') // 指向这个json
}),
new webpack.DllPlugin({
path: path.join(__dirname, '../static/', '[name]-mainfest.json'), // 描述依赖对应关系的json文件
name: '[name]_library',
context: __dirname // 执行的上下文环境,对之后DllReferencePlugin有用
})
]
}
- 通过对 entry 文件进行处理,业务逻辑将等待所依赖的 externals 文件加载完成后再开始执行
const WaitExternalPlugin = require('wait-external-webpack-plugin');
module.exports = {
externals: {},
plugins: [
new WaitExternalPlugin({
test: /\.js$/, // 正则匹配需要处理的 entry,默认对所有 entry 进行处理
}),
]
}
HTML片段与文件
- 单页面
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new htmlWebpackPlugin({
filename: 'index.html',
title: "WebPack打包机制", //生成的HTML文档的标题
template: path.join(__dirname, '/public/index.html'), //模板HTML
inject: 'body',
favicon: path.join(__dirname, '../resource/images/logo.png'),
xhtml: true,
base: '',
meta: {},
minify: {
caseSensitive: false, //是否大小写敏感
removeEmptyAttributes: true, //去除空属性
collapseWhitespace: true, //去除空格
removeComments: true, //去掉注释
hash: true, //添加HASH值,避免缓存副作用
cache: true, //内容变化, 重新生成文件名
removeComments: true, // 移除注释
collapseWhitespace: true, // 移除空白格
minifyCSS: true, // 压缩html内嵌样式
minifyJS: true, // 压缩html内嵌js
}
});
]
}
- 多页面
const glob = require('glob');
const HtmlWebpackPlugin = require('html-webpack-plugin');
function htmlWebpackConfigs () {
var configs = []
glob.sync('./src/apps/**/*.html').forEach(htmlPath => {
const chunk = htmlPath.split('./src/apps/')[1].split('/index.html')[0]
const filename = chunk + '.html'
const htmlConf = {
filename: filename,
template: htmlPath,
chunks: ['vendors', chunk]
}
configs.push(new HtmlWebpackPlugin(htmlConf))
})
return configs
}
module.exports = {
module: {
rules: [
]
},
plugins: [
...htmlWebpackConfigs()
]
}
文件目录
const TransferWebpackPlugin = require('transfer-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
plugins: [
// 从指定目录复制其下的所有文件与目录到指定目录
new TransferWebpackPlugin(patterns: array, [basePath: string]),
// 清除文件:例如在构建生产环境代码之前,清除dist目录
new CleanWebpackPlugin(),
// 拷贝资源,包括子级目录的所有文件与文件夹
new CopyWebpackPlugin([
{
from: './document',
to: './'
}
]),
]
}
- 合并文件
const merge = require('webpack-merge');
const config = {};
const common = {};
merge(config, common);
图片模块
- 压缩图片
const ImageminPlugin = require('imagemin-webpack-plugin');
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 8192 //文件大小(单位 byte)低于指定的限制时,可以返回一个 DataURL
}
}
]
}
]
},
plugins: [
new ImageminPlugin({
test: /\.(jpe?g|png|gif|svg)$/i
})
]
}
模板编译
- jade与pug
module.exports = {
module: {
rules: [
// pnpm i -D jade jade-loader
{
test: /\.jade$/,
loader: "jade"
},
// pnpm i -D pug pug-loader pug-filters
{
test: /\.pug$/,
loader: 'pug'
}
]
},
}
变量管理
const webpack = require('webpack');
module.exports = {
plugins: [
// 创建一个在编译时可以配置的全局常量
new webpack.DefinePlugin({
PRODUCTION: JSON.stringify(true),
VERSION: JSON.stringify("5fa3b9"),
})
]
}
Tree Shaking
通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如 import 和 export。
- 实现原理
- 在解析编译代码的过程中,通过维护一份模块依赖关系图数据,标记中代码中需要导入的模块与被引用的模块,然后使用TerserWebpackPlugin插件实现代码删除。

HMR模块热替换
HMR(Hot Module Replacement),即模块热替换,在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面。
- 实现机制
- 保留在完全重新加载页面时丢失的应用程序状态。
- 只更新变更内容,以节省宝贵的开发时间。
- 调整样式更加快速 - 几乎相当于在浏览器调试器中更改样式。
- 工作原理
- 应用程序
- 应用程序代码要求 HMR runtime 检查更新。
- HMR runtime(异步)下载更新,然后通知应用程序代码。
- 应用程序代码要求 HMR runtime 应用更新。
- HMR runtime(同步)应用更新。
- 编译器:除了普通资源,编译器(compiler)需要发出 "update",以允许更新之前的版本到新的版本。
- 更新后的 manifest(JSON):manifest 包括新的编译 hash 和所有的待更新 chunk 目录。每个更新 chunk 都含有对应于此 chunk 的全部更新模块(或一个 flag 用于表明此模块要被移除)的代码。
- 一个或多个更新后的 chunk (JavaScript):编译器确保模块 ID 和 chunk ID 在这些构建之间保持一致。通常将这些 ID 存储在内存中(例如,使用 webpack-dev-server 时),但是也可能将它们存储在一个 JSON 文件中。
- 模块:HMR 是可选功能,只会影响包含 HMR 代码的模块。举个例子,通过 style-loader 为 style 样式追加补丁。为了运行追加补丁,style-loader 实现了 HMR 接口;当它通过 HMR 接收到更新,它会使用新的样式替换旧的样式。
- HMR Runtime:对于模块系统的 runtime,附加的代码被发送到 parents 和 children 跟踪模块。在管理方面,runtime 支持两个方法 check 和 apply。
- check 发送 HTTP 请求来更新 manifest。如果请求失败,说明没有可用更新。如果请求成功,待更新 chunk 会和当前加载过的 chunk 进行比较。对每个加载过的 chunk,会下载相对应的待更新 chunk。当所有待更新 chunk 完成下载,就会准备切换到 ready 状态。
- apply 方法将所有被更新模块标记为无效。对于每个无效模块,都需要在模块中有一个更新处理函数(update handler),或者在它的父级模块们中有更新处理函数。否则,无效标记冒泡,并也使父级无效。每个冒泡继续,直到到达应用程序入口起点,或者到达带有更新处理函数的模块(以最先到达为准,冒泡停止)。如果它从入口起点开始冒泡,则此过程失败。
- 所有无效模块都被(通过 dispose 处理函数)处理和解除加载。然后更新当前 hash,并且调用所有 "accept" 处理函数。runtime 切换回闲置状态(idle state),一切照常继续。
- 应用程序
Loader实现原理
loader 是导出为一个函数的 node 模块。该函数在 loader 转换资源的时候调用,一般loader用于处理非JS的其他资源,例如less、sass、image、css等。
- 工具依赖包
- loader-utils:提供了许多有用的工具
- schema-utils:配合 loader-utils,用于保证 loader 选项,进行与 JSON Schema 结构一致的校验
- 代码实现
loeader文件:exmaple.js
const { urlToRequest, getHashDigest, interpolateName, isUrlRequest } = require('loader-utils');
const { validate, ValidationError } = require('schema-utils');
module.exports = function(source) {
const content = source.replace("console.log('", "console.log('\/n 日志:");
return content;
}
- 配置示例
module.exports = {
rules: [
// 自定义loader
{
test: /\.js$/,
use:[{
loader:path.resolve(path.resolve(__dirname, '../loaders/'), "example")
}],
exclude: /node_modules/
},
]
}
plugin插件原理
插件向第三方开发者提供了 webpack 引擎中完整的能力。使用阶段式的构建回调,开发者可以引入它们自己的行为到 webpack 构建流程中。
- 一个plugin插件的构成
- 一个 JavaScript 命名函数。
- 在插件函数的 prototype 上定义一个 apply 方法。
- 指定一个绑定到 webpack 自身的事件钩子。
- 处理 webpack 内部实例的特定数据。
- 功能完成后调用 webpack 提供的回调。
- compiler对象:代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
- compilation对象:代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。
// 一个 JavaScript 命名函数
function exampleWebpackPlugin(options) {
console.log('插件');
};
// 在插件函数的 prototype 上定义一个 `apply` 方法。
exampleWebpackPlugin.prototype.apply = function(compiler) {
// 同步
compiler.hooks.compile.tap('exampleWebpackPlugin', (compilation) => {
console.log('compiler');
});
// 异步
compiler.hooks.emit.tapAsync('exampleWebpackPlugin', (compilation, callback) => {
// 添加文件资源,插入到 webpack 构建中
compilation.assets['Readme.md'] = {
source: function() {
return '指引说明'
},
size: function() {
return 21;
}
};
//功能完成后调用 webpack 提供的回调。
callback();
})
};
module.exports = exampleWebpackPlugin;
缓存机制
构建性能提升
本指南包含一些改进构建/编译性能的实用技巧,无论你正在 development 或构建 production,以下做法应该帮助到你达到最佳。
- Loaders配置优化:合理评估loaders的引入,以及对loader配置字段include的充分使用。
{
test: /\.js$/,
include: path.resolve(__dirname, "src"),
loader: "babel-loader"
}
- 解析优化
- 尽量减少 resolve.modules, resolve.extensions, resolve.mainFiles, resolve.descriptionFiles 中类目的数量,因为他们会增加文件系统调用的次数。
- 如果你不使用 symlinks ,可以设置 resolve.symlinks: false (例如 npm link 或者 yarn link)。
- 如果你使用自定义解析 plugins ,并且没有指定 context 信息,可以设置 resolve.cacheWithContext: false。
- Dlls单独编译
- chunks代码优化
- 使用 更少/更小 的库。
- 在多页面应用程序中使用 SplitChunksPlugin。
splitChunks: {
chunks: "async",
minSize: 30000,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
- 在多页面应用程序中以 async 模式使用 SplitChunksPlugin。
- 移除不使用的代码。
- 只编译你当前正在开发部分的代码。
- Worker Pool
thread-loader 可以将非常消耗资源的 loaders 转存到 worker pool 中。
- 持久化缓存
使用 cache-loader 启用持久化缓存。使用 package.json 中的 "postinstall" 清除缓存目录。
- chunks代码优化
tapable
Tapable 包暴露了许多 Hook 类,可以用来为插件创建钩子。
代码示例
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
第三方脚手架工具
create-react-app
vue-cli
自定义脚手架
const program = require('commander'); // 解析命令;
const chalk = require('chalk'); // 命令行界面输出美颜
const fs = require('fs-extra'); // fs的拓展;
const shell = require('shelljs'); // 重新包装了 child_process;
const inquirer = require('inquirer'); // 交互式问答;
const ora = require('ora'); // 输出样式美化;
const ejs = require('ejs'); // 模版引擎;
const download = require('download-git-repo') //用于下载远程仓库至本地 支持GitHub、GitLab、Bitbucket
const program = require('commander') //命令行处理工具
const exists = require('fs').existsSync //node自带的fs模块下的existsSync方法,用于检测路径是否存在。(会阻塞)
const path = require('path') //node自带的path模块,用于拼接路径
const ora = require('ora') //用于命令行上的加载效果
const home = require('user-home') //用于获取用户的根目录
const tildify = require('tildify') //将绝对路径转换成带波浪符的路径
const chalk = require('chalk')// 用于高亮终端打印出的信息
const inquirer = require('inquirer') //用于命令行与开发者交互
const rm = require('rimraf').sync // 相当于UNIX的“rm -rf”命令
const logger = require('../lib/logger') //自定义工具-用于日志打印
const generate = require('../lib/generate') //自定义工具-用于基于模板构建项目
const checkVersion = require('../lib/check-version') //自定义工具-用于检测vue-cli版本的工具
const warnings = require('../lib/warnings') //自定义工具-用于模板的警告
const localPath = require('../lib/local-path') //自定义工具-用于路径的处理
const isLocalPath = localPath.isLocalPath //判断是否是本地路径
const getTemplatePath = localPath.getTemplatePath //获取本地模板的绝对路径