Elpis 开发框架搭建第二期 - Webpack5 实现工程化建设

一、目标

使 业务文件 通过 解析引擎 转换成能够 供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.jsresolve.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 源于 抖音"哲玄前端"《大前端全栈实践》

相关推荐
wearegogog1235 小时前
基于 MATLAB 的卡尔曼滤波器实现,用于消除噪声并估算信号
前端·算法·matlab
Drawing stars5 小时前
JAVA后端 前端 大模型应用 学习路线
java·前端·学习
品克缤5 小时前
Element UI MessageBox 增加第三个按钮(DOM Hack 方案)
前端·javascript·vue.js
小二·5 小时前
Python Web 开发进阶实战:性能压测与调优 —— Locust + Prometheus + Grafana 构建高并发可观测系统
前端·python·prometheus
小沐°5 小时前
vue-设置不同环境的打包和运行
前端·javascript·vue.js
qq_419854056 小时前
CSS动效
前端·javascript·css
烛阴6 小时前
3D字体TextGeometry
前端·webgl·three.js
桜吹雪6 小时前
markstream-vue实战踩坑笔记
前端
C_心欲无痕6 小时前
nginx - 实现域名跳转的几种方式
运维·前端·nginx
花哥码天下6 小时前
恢复网站console.log的脚本
前端·javascript·vue.js