一、目标
使 业务文件 通过 解析引擎 转换成能够 供Koa进行页面渲染 的 产物文件
二、解析引擎的作用
- 解析编译
- 依赖分析
- 编译
- 输出
- 模块分包
- 模块分析
- 模块拆分
- 输出
- 压缩优化与分流
三、分步实现
1. 完成 Webpack 5 基础打包配置
1.1 目录结构
在原有/app
文件夹中新增webpack
文件夹,并添加文件使结构如下
lua
/app
|----原有其他文件夹...
|----/webpack
|----build.js
|----/confg
|----webpack.base.js
1.2 Webpack 相关配置
build.js
配置内容
javascript
const webpack = require('webpack');
const webBaseConfig = require('./config/webpack.base.js');
console.log('\nbuilding... \n');
webpack(webBaseConfig, (err, stats) => {
if (err) {
throw err;
}
process.stdout.write(`${stats.toString({
colors: true, // 在控制台输出色彩信息
modules: false, // 不显示每个模块的打包信息
children: false, // 不显示子模块的打包信息
chunks: false, // 不显示每个代码块的信息
chunkModules: true // 显示代码块中模块的信息
})}\n`)
})
webpack.base.js
配置内容
javascript
const path = require('path');
const webpack = require('webpack')
const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');
/**
* webpack 基础配置
*/
module.exports = {
// 入口配置
entry: {
'entry.page1': './app/pages/page1/entry.page1.js',
'entry.page2': './app/pages/page2/entry.page2.js'
},
// 模块解析配置(决定了要加载解析哪些模块以及用什么方式去解释)
module: {
rules: [
{
test: /\.vue$/,
use: {
loader: 'vue-loader'
}
},
{
test: /\.js$/,
include: [
// 只对业务代码进行 babel,加快 webpack 打包速度
path.resolve(process.cwd(), './app/pages')
],
use: {
loader: 'babel-loader'
}
},
{
test: /\.(png|jpe?g|gif)(\?.+)?$/,
use: {
loader: 'url-loader',
options: {
limit: 300,
esModule: false
}
}
},
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
},
{
test: /\.less$/,
use: [ 'style-loader', 'css-loader', 'less-loader' ]
},
{
test: /\.(eot|svg|ttf|woff|woff2)(\?\S*)?$/,
use: 'file-loader'
}
]
},
// 产物输出路径
output: {
filename: 'js/[name]_[chunkhash:8].bundle.js',
path: path.join(process.cwd(), './app/public/dist/prod'),
publicPath: '/dist/prod',
crossOriginLoading: 'anonymous'
},
// 配置模块解析的具体行为(定义 webpack 在打包时,如何找到并解析具体模块的路径)
resolve: {
extensions: ['.js', '.vue', '.less', '.css'],
alias: {
}
},
// 配置 webpack 插件
plugins: [
// 处理 .vue 文件,这个插件是必须的
// 它的职能是将你定义过的其他规则复制并应用到 .vue 文件里
// 例如,如果只有一条匹配规则 /\.js$/ 的规则,那么它会应用到 .vue 文件中的 <script> 板块中
new VueLoaderPlugin(),
// 把第三方库暴露到 window context 下
new webpack.ProvidePlugin({
Vue: 'vue'
}),
// 定义全局常量
new webpack.DefinePlugin({
__VUE_OPTIONS_API__: 'true', // 支持 vue 解析 optionsApi
__VUE_PROD_DEVTOOLS: 'false', // 禁用 Vue 调试工具
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false' // 禁用生产环境显示 "水合" 信息
}),
// 构造最终渲染的页面模版
new HtmlWebpackPlugin({
// 产物 (最终模版) 输出路径
filename: path.resolve(process.cwd(), './app/public/dist/', 'entry.page1.tpl'),
// 指定要使用的模版文件
template: path.resolve(process.cwd(), './app/view/entry.tpl'),
// 要注入的代码块
chunks: [ 'entry.page1']
}),
new HtmlWebpackPlugin({
// 产物 (最终模版) 输出路径
filename: path.resolve(process.cwd(), './app/public/dist/', 'entry.page2.tpl'),
// 指定要使用的模版文件
template: path.resolve(process.cwd(), './app/view/entry.tpl'),
// 要注入的代码块
chunks: [ 'entry.page2']
})
],
// 配置打包输出优化(代码分割、模块合并、缓存、TreeShaing、压缩等优化策略)
optimization: {}
}
1.3 测试文件与模板文件配置
新增一些文件,用于后续验证打包效果
app/pages/page1/entry.page1.js
javascript
import { createApp } from 'vue';
import page1 from './page1.vue';
const app = createApp(page1);
app.mount('#root')
app/pages/page1/page1.vue
html
<template>
<h1>page1</h1>
<input v-model="content" />
</template>
<script setup>
import { ref } from 'vue';
const content = ref('');
console.log('page1 init')
</script>
<style lang="less" scoped>
h1{
color: red;
}
</style>
app/pages/page2/entry.page2.js
javascript
import { createApp } from 'vue';
import page2 from './page2.vue';
const app = createApp(page2);
app.mount('#root')
app/pages/page2/page2.vue
html
<template>
<h1>page2</h1>
<input v-model="content" />
</template>
<script setup>
import { ref } from 'vue';
const content = ref('');
console.log('page2 init')
</script>
<style lang="less" scoped>
h1{
color: blue;
}
</style>
app/view/entry.tpl
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{ name }}</title>
<link href="/static/normalize.css" rel="stylesheet">
<link href="/static/icon.png" rel="icon" type="image/x-icon">
</head>
<body style="color: red">
<div id="root"></div>
<input id="env" value="{{ env }}" style="display: none">
<input id="options" value="{{ options }}" style="display: none">
</body>
<script type="text/javascript">
try {
window.env = document.getElementById('env').value;
const options = document.getElementById('options').value;
window.options = JSON.parse(options);
} catch (e) {
console.error(e)
}
</script>
</html>
1.4 验证结果
bash
node ./app/webpack/build.js
运行上述命令后,能够在 app/public/dist
文件看到打包后的产物
1.5 Controller 修改
由于我们将用于给 Koa 进行渲染的文件放在了 app/public/dist
文件夹中,所以我们需要修改之前的 app/controller/view.js
,对渲染路径进行修改
javascript
await ctx.render(`output${sep}entry.${ctx.params.page}`, {
修改为
javascript
await ctx.render(`dist${sep}entry.${ctx.params.page}`, {
2. Webpack 打包优化
2.1 实现动态构造
在基础配置中,pligun
的位置我们使用了多个 new HtmlWebpackPlugin
指定最终渲染的页面模版,但是这不利于后续更改与维护,所以我们将要采用动态构造的方式来提高可维护性。
在 app/webpack/config/webpack.base.js
引入所需新依赖
javascript
const glob = require("glob");
在 引入依赖后、webpack 基础配置前 实现动态构造
javascript
...原有引入依赖代码不变...
// 动态构造 pageEntries htmlWebpackPluginList
const pageEntries = {};
const htmlWebpackPluginList = [];
// 获取 app/pages 目录下所有入口文件 (entry.xxx.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],
})
);
});
module.exports = { ...已有内容不变... }
动态构造会将多个 HtmlWebpackPlugin
合并为一个 HtmlWebpackPluginList
,我们需要修改代码,将原有的多个 HtmlWebpackPlugin
替换为 HtmlWebpackPluginList
webpack.base.js
文件内,将 plugins 中的
javascript
new HtmlWebpackPlugin({
// 产物 (最终模版) 输出路径
filename: path.resolve(process.cwd(), './app/public/dist/', 'entry.page1.tpl'),
// 指定要使用的模版文件
template: path.resolve(process.cwd(), './app/view/entry.tpl'),
// 要注入的代码块
chunks: [ 'entry.page1']
}),
new HtmlWebpackPlugin({
// 产物 (最终模版) 输出路径
filename: path.resolve(process.cwd(), './app/public/dist/', 'entry.page2.tpl'),
// 指定要使用的模版文件
template: path.resolve(process.cwd(), './app/view/entry.tpl'),
// 要注入的代码块
chunks: [ 'entry.page2']
})
替换为
javascript
...htmlWebpackPluginList,
至此,我们成功完成了动态构造的配置
2.2 打包输出优化配置
除了一些基础配置之外,我们还可以在 optimization
自定义一些打包优化规则
javascript
// 配置打包输出优化(代码分割、模块合并、缓存、TreeShaing、压缩等优化策略)
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[\\/]/,
name: 'vendor', // 模块名称
priority: 20, // 优先级,数字越大,优先级越高
enforce: true, // 强制执行
reuseExistingChunk: true, // 复用已有的公共 chunk
},
common: { // 公共模块
name: 'common', // 模块名称
minChunks: 2, // 被 2 处引用即被归为公共模块
minSize: 1, // 最小分割文件大小 (1 byte)
priority: 10, // 优先级
reuseExistingChunk: true, // 复用已有的公共 chunk
}
}
}
},
2.3 其他配置
在 webpack.base.js
的 resolve.alias
中,我们可以定义一些变量以便后续在 require
时能够更便捷
javascript
alias: {
$pages: path.resolve(process.cwd(), "./app/pages"),
$common: path.resolve(process.cwd(), "./app/common"),
$widgets: path.resolve(process.cwd(), "./app/widgets"),
$store: path.resolve(process.cwd(), "./app/store"),
}
3. Webpack 环境分流配置
未完待续
elpis 源于 抖音"哲玄前端"《大前端全栈实践》