Elips - webpack 工程化实现
概述
当开发一个具有规模的程序,你将遇到非常多的非业务问题,这些问题包括:执行效率、兼容性、代码的可维护性可扩展性、团队协作、测试等等等等,我们将这些问题称之为工程问题。工程问题与业务无关,但它深刻的影响到开发进度,如果没有一个好的工具解决这些问题,将使得开发进度变得极其缓慢,同时也让开发者陷入技术的泥潭。
在开发中,我们往往会遇到如下问题:
开发时态,devtime
:
- 模块划分越细越好
- 支持多种模块化标准
- 支持npm或其他包管理器下载的模块
- 能够解决其他工程化的问题
运行时态,runtime
:
- 文件越少越好
- 文件体积越小越好
- 代码内容越乱越好
- 所有浏览器都要兼容
- 能够解决其他运行时的问题,主要是执行效率问题
为了解决上述问题,这时候我们就需要一个工具,这个工具能够让开发者专心的在开发时态写代码,然后利用这个工具将开发时态编写的代码转换为运行时态需要的东西,这个工具就叫做构建工具。
通过构建工具,我们就可以省去繁杂的步骤,直接一条指令就能将开发环境的项目构建为生产环境的项目代码,之后要做的就是部署上线即可。
webpack如何配置?
如果不知道webpack 如何配置,一般可以按照这个顺序入手
- 入口
- 出口
- loader
- plugin
- 其他处理
1. 入口
webpack 从哪里开始解析文件
js
// 动态构造 pageEntries htmlWebpackPluginList
const pageEntries = {};
const htmlWebpackPluginList = [];
// 获取 app/pages 目录下所有入口文件 (entry.xx.js)
const entryList = path.resolve(process.cwd(), "./app/pages/**/entry.*.js");
glob.sync(entryList).forEach((file) => {
const entryName = path.basename(file, ".js");
// 构造 entry
pageEntries[entryName] = file;
// 构造最终渲染的页面
htmlWebpackPluginList.push(
// html-webpack-plugin 辅助注入 bundle 到 tpl 文件中
new HtmlWebpackPlugin({
// 产物(最终模板)输出路径
filename: path.resolve(
process.cwd(),
"./app/public/dist/",
`${entryName}.tpl`
),
// 指定要使用的模板
template: path.resolve(process.cwd(), "./app/view/entry.tpl"),
chunks: [entryName], // 引入的 chunk
})
);
});
module.exports = {
// 入口配置
entry: pageEntries,
// ...
}
2. 出口
webpack 打包结果输出设置
js
module.export = {
// ...
output: {
filename: "js/[name]_[chunkhash:8].bundle.js", // 输出文件名称
path: path.join(process.cwd(), "./app/public/dist/prod"), // 输出路径
publicPath: "/dist/prod", // 公共路径
crossOriginLoading: "anonymous", // 跨域加载
},
// ...
}
3. loader
webpack 在打包时会将 js 转换为抽象语法树进行处理,但 webpack 在遇到其他资源文件可能会遇到不认识的产物(CSS、图片等),这个时候我们需要使用 loader 在将这些文件处理成 webpack 认识的内容。loader 发生在生成抽象语法树之前。
js
module.export = {
// ...
module: {
rules: [
{
test: /.vue$/, // 匹配 vue
use: "vue-loader",
},
{
test: /.m?jsx?$/, // 匹配 js
include: path.resolve(process.cwd(), "./app/pages"), // 只对业务代码进行 bebel, 加快 webpack 的打包速度
use: "swc-loader",
},
{
// webpack5 添加 4 种新的模块类型,来替换所有这些 loader
test: /.(png|jpe?g|gif|webp|avif)(?.*)?$/i,
type: "asset", // webpack5 通用资源处理模块 默认 8kb 以下转换为 base64
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 10kb 以下转换为 base64
},
},
// generator: {
// filename: "img/[name].[contenthash:6][ext]", // 文件输出目录
// },
},
{
test: /.(eot|svg|ttf|woff|woff2)(?\S*)?$/i, // 其他文件
type: "asset/resource", // 默认会导出单独文件
},
{
// css 处理
oneOf: [
{
test: /.css$/, // 匹配 css
use: getStyleLoaders(),
},
{
test: /.less$/, // 匹配 less
use: getStyleLoaders("less-loader"),
},
],
},
],
},
// ...
}
4. plugin
loader的功能定位是转换代码,而一些其他的操作难以使用 loader 完成,比如:
- 当 webpack 生成文件时,顺便多生成一个说明描述文件
- 当 webpack 编译启动时,控制台输出一句话表示 webpack 启动了
- 当xxxx时,xxxx
这种类似的功能需要把功能嵌入到 webpack 的编译流程中,而这种事情的实现是依托于plugin的
js
module.export = {
// ...
// 配置 webpack 插件
plugins: [
// 处理 vue 文件,这个插件是必须的
// 他的功能是将你定义的过的其他规则复制并应用到 .vue 文件中
// 例如, /.js$/ 会应用到 .vue 文件 中的 <script> 块
new VueLoaderPlugin(),
// 把第三方库暴露到 window context 中
new webpack.ProvidePlugin({
Vue: "vue",
axios: "axios",
_: "lodash",
}),
// 定义全局常量
new webpack.DefinePlugin({
__VUE_OPTIONS_API__: true, // 开启 vue options API
__VUE_PROD_DEVTOOLS__: false, // 禁用 vue 调试工具
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, // 禁用生产环境显示 "水合" 信息
}),
...htmlWebpackPluginList,
],
// ...
}
5. 其他处理
分包
js
module.export = {
// ...
optimization: {
/**
* 把 js 文件打包成3种类型
* 1. vendor: 第三方 lib 库, 基本不会改动,除非依赖版本升级
* 2. common: 业务组件代码公共部分抽取出来,改动较少
* 3. entry.{page}: 不同页面 entry 里的业务组件代码差异部分,会经常改动
* 目的:把改动和引用频率不一样的 js 区分出来,以达到更好利用浏览器缓存的效果
*/
splitChunks: {
chunks: "all", // 对同步和异步模块都进行分割
maxAsyncRequests: 10, // 每次异步加载最大并行请求数量
maxInitialRequests: 10, // 入口点最大并行请求数
cacheGroups: {
vendor: {
// 第三方依赖库
test: /[\/]node_modules[\/]/, // 对 node_modules 目录下的文件单独打包
name: "vendor", // 分组名称
priority: 20, // 优先级 数字越大优先级越高
enforce: true, // 强制执行
reuseExistingChunk: true, // 复用已经存在的 chunk
},
common: {
// 公共模块
minChunks: 2, // 最小引用次数
name: "common", // 分组名称
priority: 10, // 优先级
reuseExistingChunk: true, // 复用已经存在的 chunk
minSize: 30000, // 分包达到多少字节后分包
},
},
},
// 将 webpack 运行时生产的代码打包到 runtime.js
runtimeChunk: true,
},
// ...
}
热更新
采用 express 中间件方式实现,先是在 webpack 配置文件中针对开发环境对入口等配置进行修改,加入热更新相关配置,然后在启动文件中创建 express 服务,引用
webpack-dev-middleware
和webpack-hot-middleware
中间件,分别实现监控文件改动和热更新功能。
js
// webpack.dev.js
// 基类配置
const baseConfig = require("./webpack.base.js");
// dev-server
const DEV_SERVER_CONFIG = {
host: "127.0.0.1",
port: 9002,
HMR_PATH: "__webpack_hmr", // 官方规定
TIMEOUT: 20000,
};
const clientPath = `http://${DEV_SERVER_CONFIG.host}:${DEV_SERVER_CONFIG.port}`;
// 开发阶段的 entry 配置需要加入 hmr
Object.keys(baseConfig.entry).forEach((v) => {
// 第三方包不作为 hmr 入口
if (v !== "vendor") {
baseConfig.entry[v] = [
// 主入口文件
baseConfig.entry[v],
// hmr 配置更新入口, 官方指定的 hrm 路径
`webpack-hot-middleware/client?path=${clientPath}/${DEV_SERVER_CONFIG.HMR_PATH}&timeout=${DEV_SERVER_CONFIG.TIMEOUT}&reload=true`,
];
}
});
// 开发环境 webpack 配置
const webpackConfig = merge.smart(baseConfig, {
// 指定开发环境
mode: "development",
// source-map 开发工具,呈现代码的映射工具
devtool: "eval-cheap-module-source-map",
// 开发环境的 output 配置
output: {
filename: "js/[name].bundle.js", // 输出文件名称 开发环境不用计算 hash 提升构建速度
path: path.resolve(process.cwd(), "./app/public/dist/dev"), // 输出文件存储路径
publicPath: `${clientPath}/public/dist/dev`, // 外部资源公共路径
globalObject: "this",
},
// 开发阶段插件
plugins: [
new webpack.HotModuleReplacementPlugin({
// 用于实现热模块替换
// 模块热替换允许在应用程序运行时替换模块
// 极大的提高了开发过程中的效率, 因为能够在应用程序运行时替换模块
//! 热替换并不能降低构建时间(可能还会稍微增加),但可以降低代码改动到效果呈现的时间
multiStep: false,
}),
],
});
js
// dev.js
// 本地开发启动 devServer
const express = require("express");
const path = require("path");
const webpack = require("webpack");
const consoler = require("consoler");
const devMiddleware = require("webpack-dev-middleware");
const hotMiddleware = require("webpack-hot-middleware");
// 从 webpack.dev.js 获取 webpack 配置 和 dev-server 配置
const { webpackConfig, DEV_SERVER_CONFIG } = require("./config/webpack.dev.js");
// 创建 express 服务
const app = express();
const compiler = webpack(webpackConfig);
// 指定静态文件目录
app.use(express.static(path.join(__dirname, "../public/dist")));
// 引用 devMiddleware 中间件 (监控文件改动)
app.use(
devMiddleware(compiler, {
// 落地文件
writeToDisk: (filePath) => filePath.endsWith(".tpl"),
// 资源路径
publicPath: webpackConfig.output.publicPath,
// headers 配置
headers: {
"Access-Control-Allow-Origin": "*", // 白名单
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS", // 允许请求方法
"Access-Control-Allow-Headers":
"X-Requested-With, content-type, Authorization", // 允许请求头
},
stats: {
colors: true, // 在控制台输出色彩信息
},
})
);
// 引用 hotMiddleware 中间件 (实现热更新)
app.use(
hotMiddleware(compiler, {
path: `/${DEV_SERVER_CONFIG.HMR_PATH}`,
log: () => {},
})
);
consoler.info("请等待webpack初次构建完成提示...");
// 启动 dev-server
const port = DEV_SERVER_CONFIG.port;
app.listen(port, () => {
console.log(`-- [start] dev server running at http://localhost:${port} --`);
});
总结
通过 哲玄课堂《大前端全栈实现》 学习,对工程化和构建工具有了进一步的理解。