全栈领域模型框架elpis(二)工程化

1. elpis 为什么需要工程化?

elpis致力于打造企业级应用框架,支持多系统切换。elpis 的渲染模式:每个独立站点的入口由 SSR 渲染分发,而每个入口渲染的页面又是一个独立的 SPA 应用。

工程化在 elpis 中的首要作用就是:根据不同的入口文件,生成不同的页面模板,并指定页面模板需要注入的代码块

其次,我们想要将代码提交到生成环境中运行,还需要代码进行兼容性处理、分包、压缩、打包分析等等,以保证生产环境代码的正常运行。再者,对于前端的代码的修改,每次修改后都需要重启服务器过于麻烦,因此对于开发环境,我们还需要代码的热更新功能,实现代码修改后的实时热更新,提升本地开发效率和体验。

2. elpis 如何做工程化?

2.1 SSR 入口文件的处理

对于SSR的入口文件,如果我们使用的是 Vue 框架,则我们需要创建一个 Vue-APP;如果我们使用的是 React,那么我们需要创建一个 React-APP;但是我们的目的打造的是一个多入口的应用,如果每个入口都重复的写一遍创建 APP 应用的代码,就显得很繁琐。

于是,我们可以对于 Vue 框架设计一个 boot-vue.js 的公共方法,用于创建 Vue 的 APP应用(React同理)。再通过配置项的形式给到不同的 APP 可以创建不同的 APP 实例,它们可以拥有不同的路由、不同的三方库等等。

我们以创建 Vue 的 APP 为例,创建一个 boot.js 方法用于创建 Vue-APP:

js 复制代码
import { createApp } from 'vue';
import ElementPlus from 'element-plus';
import 'element-plus/theme-chalk/index.css';
import 'element-plus/theme-chalk/dark/css-vars.css';
import pinia from '@store';
import { createRouter, createWebHistory } from 'vue-router';

/**
 * 应用主入口 启动多个入口
 * @param {Object} pageComponent 页面入口组件
 * @param {Object} options 配置项
 *  - {Array} routes 路由配置
 *  - {Array} libs 需要加载的库
 */
export default (pageComponent, options) => {
  const { routes, libs } = options || {};

  const app = createApp(pageComponent);
  
  app.use(ElementPlus); // 注册 UI 组件库

  // 加载第三方库
  if (libs && libs?.length > 0) {
    try {
      for (let i = 0; i < libs.length; i++) {
        app.use(libs[i]);
      }
    } catch (error) {
      console.error(`加载第三方库失败: ${error}`);
    }
  }

  app.use(pinia); // 注册 pinia

  // 页面路由
  if (routes && routes?.length > 0) {
    const router = createRouter({
      history: createWebHistory(), // 统一使用 history 模式
      routes,
    });
    app.use(router);
    router.isReady().then(() => {
      app.mount('#root');
    })
  } else {
    app.mount('#root');
  }
};

不同的应用页面入口,直接调用 boot.js 即可创建 APP 应用。

2.2 Vue单文件的处理

因为使用到了 Vue 框架单文件,因此我们需要给工程加上处理 Vue 单文件的能力。

首先,以面向对象的编程思维,我们新建一个 webpack.common.js 文件,将开发环境和生产环境都需要的配置配置在此,而开发环境的入口文件 webpack.dev.js、生产环境的入口文件 webpack.prod.js 会先继承 webpack.common.js 文件配置,在新增各种环境所需的配置即可(这里使用的是 webpack,当然使用 vite 等其他构建工具也行)。

对于 Vue,我们需要使用解析 Vue 对应的 vue-loader 和 插件 VueLoaderPlugin。

js 复制代码
const { VueLoaderPlugin } = require('vue-loader');
module.exports = {
    module: {
            rules: [
              {
                test: /\.vue$/,
                use: [ 'vue-loader' ]
              },
              // ...
          ]
    },
    plugins: [
        new VueLoaderPlugin(),
        new webpack.DefinePlugin({
         __VUE_OPTIONS_API__: 'true', // 支持 vue 解析 optionApi
          __VUE_PROD_DEVTOOLS__: 'false', // 禁用 Vue 调试工具
          __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false' // 禁用 水合 信息
        }),
    ]
}

2.3 入口文件的处理

约定:我们的入口文件以 entry.xxx.js 的命名形式存在的。

经过 vue 相关的 loader 和 插件的处理后,可以将 vue 文件打包成 js 文件,我们还需要根据不同的入口页面模板,给它们注入不同的代码块(我们的应用可能有很多很多的入口文件,如果每个入口文件都在 webpack.common.js 文件中单独声明且指定它们的代码块,那么这个配置文件就会显得特别臃肿。从工程化的角度,这里我们采用统一处理)。

js 复制代码
// 所有页面的入口
const pageEntries = {};
// 所有页面的 html 插件配置
const htmlWebpackPluginList = [];

// 获取 app/pages 目录下所有的入口文件(entry.xxx.js)
const entryFileList = path.resolve(process.cwd(), './app/pages/**/entry.*.js');
glob.sync(entryFileList).forEach(file => {
  const entryFileName = path.basename(file, '.js');
  pageEntries[entryFileName] = file;
  htmlWebpackPluginList.push(new HtmlWebpackPlugin({
    template: path.resolve(process.cwd(), './app/views/entry.tpl'),
    filename: path.resolve(process.cwd(), './app/public/dist/', `${entryFileName}.tpl`),
    chunks: [ entryFileName ]
  }))
})

module.exports ={
    plugins: [
        ...htmlWebpackPluginList
    ]
}

2.4 样式文件的处理

处理 less、css 等样式文件,需要使用到对应的 loader(注意:loader 的执行顺序是从右到左)。

js 复制代码
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /\.less$/,
        use: ['style-loader', 'css-loader', 'less-loader'],
      },

2.5 处理图片等其他资源

webpack5 以及内置处理图片、字体文件等资源的功能,这里就不再赘述。

2.6 生产环境处理

考虑到项目庞大之后,打包的速度会很慢,打包后的代码体积会很大。所有我们针对这些问题需要做出对应的配置。

优化打包速度,以使用 thread-loader 为示例:

js 复制代码
 const ThreadLoaderConfig = {
    workers: OS.cpus().length,
    workerNodeArgs: ['--max-old-space-size=1024'],
    workerParallelJobs: 50,
}
 {
    loader: 'thread-loader', // 小项目没有必要使用,启动进程也需要时间的
    options: {
      ...ThreadLoaderConfig
    }
  },

优化单个文件打包体积过大,在 vue-router 上,我们使用动态导入的策略,加上 webpack 的分包策略,对 js 进行分包:

js 复制代码
// 动态导入
component: () => import('./complex-view/schema-view/schema-view.vue')
// 分包
splitChunks: {
  chunks: 'all',
  maxAsyncRequests: 10,
  maxInitialRequests: 10,
  cacheGroups: {
    vendor: { // 第三方模块
      test: /[\\/]node_modules[\\/]/,
      name: 'vendor',
      priority: 20,
      enforce: true,
      reuseExistingChunk: true,
    },
    common: {
      test: /[\\/]widgets|common[\\/]/,
      name: 'common',
      minChunks: 2,
      minSize: 1,
      priority: 10,
      reuseExistingChunk: true,
    },
  }
    },

实现 js 文件的动态导入,减小首屏加载的资源体积。

2.7 开发环境搭建

使用 express 搭建本地服务器,结合 webpack 的中间件,监听文件的改动,当文件改动时,通知浏览器更新页面局部内容,达到实时热更新的效果。使用到的 webpack 中间件有 webpack-dev-middleware、webpack-hot-middleware,以及 HMR 插件 HotModuleReplacementPlugin。具体配置细节需要查看 webpack 官方文档。

js 复制代码
// webpack.dev.js
const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const webpackCommonConfig = require('./webpack.common');

const DEV_SERVER_CONFIG = {
  HOST: '127.0.0.1',
  PORT: 2024,
  HMR_PATH: '__webpack_hmr',
  TIMEOUT: 20000
}

Object.keys(webpackCommonConfig.entry).forEach(v => {
  if (v !== 'vendor') {
    const { HOST, PORT, HMR_PATH, TIMEOUT } = DEV_SERVER_CONFIG;
    webpackCommonConfig.entry[v] = [
      webpackCommonConfig.entry[v],
      `webpack-hot-middleware/client?path=http://${HOST}:${PORT}/${HMR_PATH}?timeout=${TIMEOUT}&reload=true`
    ]
  }
})

const webpackDevConfig = merge.smart(webpackCommonConfig, {
  mode: 'development',
  // 开发环境打包产物输出
  output: {
    path: path.resolve(process.cwd(), './app/public/dist/dev/'), // dev 输出文件路径
    publicPath: `http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/public/dist/dev/`, // 服务器-外部资源的路径
    filename: 'js/[name]_[chunkhash:8].bundle.js',
    globalObject: 'this',
  },
  plugins: [
    // 应用运行时,可以热模块替换
    new webpack.HotModuleReplacementPlugin({
      multiStep: false,
    }),
  ],
  devtool: 'cheap-module-source-map'
})

module.exports = {
  webpackDevConfig,
  DEV_SERVER_CONFIG
};

本地开发环境启动入口文件,如下:

js 复制代码
// dev.js 本地开发环境启动文件
const path = require('path');
const express = require('express');
const webpack = require('webpack');
const WebpackDevMiddleware = require('webpack-dev-middleware');
const WebpackHotMiddleware = require('webpack-hot-middleware');

const { webpackDevConfig, DEV_SERVER_CONFIG } = require('./config/webpack.dev');

const app = express(); // 初始化 express 服务

const compiler = webpack(webpackDevConfig);

// 指定 静态文件目录
app.use(express.static(path.join(__dirname, '../public/dist')))

// 监控文件改动
app.use(WebpackDevMiddleware(compiler, {
  writeToDisk: (filePath) => filePath.endsWith('.tpl'),
  publicPath: webpackDevConfig.output.publicPath,
  // headers
  headers: {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, HEAD, DELETE, OPTIONS',
    'Access-Control-Allow-Headers': 'X-Request-With, Content-Type, Authorization',
  },
  stats: {
    colors: true,
  }
}))

// 热更新,通知浏览器
app.use(WebpackHotMiddleware(compiler, {
  path: `/${DEV_SERVER_CONFIG.HMR_PATH}`,
  log: () => {}
}))

console.log('=== 等待 webpack 初次构建完成... ===');

const { PORT } = DEV_SERVER_CONFIG;

app.listen(PORT, () => {
  console.log(`dev server listening on port: ${PORT}`);
})

2.8 增加启动脚本

区分本地开发环境启动脚本、生产环境打包脚本

json 复制代码
"build:dev": "node --max_old_space_size=4096 ./app/webpack/dev.js",
"build:prod": "node ./app/webpack/prod.js"

3. elpis工程化总结

为了能够集成【node服务 => SSR => SPA】的能力我们处理了这些问题:

  1. 应用程序的创建方法封装
  2. Vue 文件的处理
  3. 页面模板与代码块的绑定
  4. 样式处理
  5. 资源处理
  6. 生产环境处理
  7. 开发环境搭建

这些处理只是冰山一角,还有很多需要处理的问题,如:代码兼容性处理(css兼容性处理、js兼容性处理等等),这里就不再赘述。

阶段二完结,阶段三的文字飞奔中...(欢迎各位佬交流)

相关推荐
Huooya3 分钟前
springboot的外部配置加载顺序
spring boot·面试·架构
---yx89897811 分钟前
数字人系统源码---v10技术五大底层架构链路全局开发思路
算法·架构·数字人·数字人源码·数字人系统
苏苏码不动了15 分钟前
Android MVC、MVP、MVVM三种架构的介绍和使用。
android·架构·mvc
pyliumy1 小时前
在基于Arm架构的华为鲲鹏服务器上,针对openEuler 20.03 LTS操作系统, 安装Ansible 和MySQL
服务器·架构·ansible
恒星漫游者1 小时前
Redis集群 vs 云数据库:中小电商的缓存方案选择
架构
旧厂街小江3 小时前
LeetCode 第63题:不同路径 II
算法·程序员·架构
不修×蝙蝠3 小时前
SpringBoot(一)--搭建架构5种方法
java·spring boot·架构·配置·搭建
森焱森4 小时前
AArch64架构及其编译器
linux·c语言·单片机·架构
黑金IT4 小时前
深入理解人脸特征向量及图片转换方法与开发架构
算法·架构
Lei活在当下4 小时前
【尚未完成】【Android架构底层逻辑拆解】Google官方项目NowInAndroid研究(4)数据层的设计和实现之data
架构