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 源于 抖音"哲玄前端"《大前端全栈实践》

相关推荐
Spider_Man2 小时前
从 “不会迭代” 到 “面试加分”:JS 迭代器现场教学
前端·javascript·面试
我的写法有点潮2 小时前
最全Scss语法,赶紧收藏起来吧
前端·css
小高0072 小时前
🧙‍♂️ 老司机私藏清单:从“记事本”到“旗舰 IDE”,我只装了这 12 个插件
前端·javascript·vue.js
Mo_jon3 小时前
css 遮盖滚动条,鼠标移上显示
前端·css
EveryPossible3 小时前
终止异步操作
前端·javascript·vue.js
Stringzhua3 小时前
setup函数相关【3】
前端·javascript·vue.js
neon12043 小时前
解决Vue Canvas组件在高DPR屏幕上的绘制偏移和区域缩放问题
前端·javascript·vue.js·canva可画
Sammyyyyy4 小时前
Node.js 做 Web 后端优势为什么这么大?
开发语言·前端·javascript·后端·node.js·servbay
妮妮喔妮4 小时前
Webpack 有哪些特性?构建速度?如何优化?
前端·webpack·node.js