single-spa 基于vue的实践初版

579 阅读7分钟

仓库地址:gitee.com/larntin/spa…

语雀笔记地址:www.yuque.com/zuiyu-qwofk…

背景

因XX需求,需要深入了解学习前端微服务,经过网文的安利,最终选择了 single-spa 这个框架。

将前端微服务 single-spa 使用的相关技术,组织成了一个系列文章,方便自己今后快速回忆,也方便和众多有需要人士共同交流。

研习路径

  1. 复习了所有相关技术之后需要实践一下,于是找到官方基于vue的demo,阅读、运行、调试源码;
  2. 动手从零开始按照官方vue-demo的架构搭建一套,目的:
    1. 体会框架的筋骨脉络;
    2. 遇到的各种问题,框架是如何去解决的;
    3. 先按照demo的技术路线固化实现,再提炼、总结、升华;

工程结构图

建立工程

  1. npm 全局安装工具 lerna,我安装的是版本4;

    1. npm install -g lerna
  2. npm 全局安装工具 create-single-spa(初版就用官方给的脚手架,以后再定制webpack配置文件);

    1. npm install --global create-single-spa
    2. create-single-spa 可以建立的类型是三种:
      1. create-single-spa --moduleType root-config
      2. create-single-spa --moduleType app-parcel
      3. create-single-spa --moduleType util-module
  3. npm 全局安装 @vue/cli工具,当前我的版本是 @vue/cli@4.5.15

  4. 使用 lerna init 工程之后,在 packages 目录下建立 root-config 工程;

    1. 运行命令: create-single-spa root-config --moduleType root-config
    2. 过程里面交互输入:orgName,npmClient,typescript及其他
  5. 多工程dev端口规划:

    1. root-config项目使用:9000
    2. navbar的vue的导航框架使用:9001,然后是 app1:9002,app2:9003
  6. 多工程nginx端口规划:

    1. root-config 9100
    2. navbar:9101 app1:9102 app2:9103
  7. 在 packages 目录下建立 navbar 的 vue 工程

    1. vue create navbar
  8. 然后再spa-demo根目录执行命令 lerna bootstrap

    1. 发现 root-config 中的 prepare 脚本 husky install 报错,就先屏蔽了
  9. 在 lerna 根目录的 package.json 中添加自定义命令:

    1. --scope 后面带的应该是 packages 目录下工程的名字,而不是目录的名字
    2. 通过lerna启动的 packages 的子工程竟然是独立 vscode 实例的,关闭 vscode 都不可以停止服务

关键库的版本

@vue/cli:  4.5.15   // 这个框架又决定了以下的版本号
    "@vue/cli-service": "~4.5.0",
    "core-js": "^3.6.5",
    "vue": "^2.6.11",
    "webpack": "^4.46.0",
    // 无 webpack-cli
    "webpack-dev-server": "^3.11.3",
create-single-spa@4.1.2  // 这个框架又决定了以下的版本号
    "webpack": "^5.51.1",
    "webpack-cli": "^4.8.0",
    "webpack-dev-server": "^4.7.0",

这就导致了 root-config 工程和子工程实际依赖的webpack、webpack-dev-server版本不一致,查看对应文档的时候需谨慎。

root-config的改造

  1. 在html这个唯一入口添加所有的js文件,都放到 public目录下,带有版本号和sourcemap文件方便调试;
  2. 添加 .editorconfig 和 .prettierrc文件,使得代码格式化自动补全风格一致;

navbar-后台管理系统框架工程基于@vue/cli的改造

后管系统包含,用户登录、注册、忘记密码已经进入系统之后的,菜单管理和用户信息、消息系统主体框架。然后,根据登录用户的权限获取菜单和消息,用户点击菜单,中间的业务区域就渲染成不同的业务页面,也就是各个子系统中的功能。

安装@vue/cli插件依赖: vue-cli-plugin-single-spa

npm i -D vue-cli-plugin-single-spa

目前版本是:3.1.2

注意:

  1. 以 vue-cli-plugin开发的是 @vue/cli规范中的社区插件;
  2. 在执行vue-cli-service命令的时候,他会自动解析package.json中的依赖,自动加载@vue/cli的plugins,所以不需要在vue.config.js中显示的加载;
    1. 具体规范查看:cli.vuejs.org/zh/guide/pl…

vue-cli-plugin-single-spa@3.1.2的问题

// 3.1.2 的源码如下,注释是我自己加的
const SystemJSPublicPathWebpackPlugin = require("systemjs-webpack-interop/SystemJSPublicPathWebpackPlugin");
const StandaloneSingleSpaPlugin = require("standalone-single-spa-webpack-plugin");

module.exports = (api, options) => {
  options.css.extract = false;

  const packageJsonPath = api.resolve("package.json");
  const { name } = require(packageJsonPath);
  if (!name) {
    throw Error(
      `vue-cli-plugin-single-spa: could not determine package name -- change your package json name field`
    );
  }

  api.chainWebpack((webpackConfig) => {
    webpackConfig.devServer
      .headers({
        "Access-Control-Allow-Origin": "*",
      })
      .set("disableHostCheck", true);

    webpackConfig.optimization.delete("splitChunks");

    webpackConfig.output.libraryTarget("umd");

    webpackConfig.output.devtoolNamespace(name);

    webpackConfig.set("devtool", "sourcemap");

    webpackConfig
      .plugin("SystemJSPublicPathWebpackPlugin")
      .use(SystemJSPublicPathWebpackPlugin, [
        {
          rootDirectoryLevel: 2,
          systemjsModuleName: name,
        },
      ]);

    webpackConfig
      .plugin("StandaloneSingleSpaPlugin")
      .use(StandaloneSingleSpaPlugin, [
        {
          appOrParcelName: name,
          disabled: process.env.STANDALONE_SINGLE_SPA !== "true",
        },
      ]);

    webpackConfig.output.set("jsonpFunction", `webpackJsonp__${name}`);

    webpackConfig.externals(["single-spa"]);
  });
};
  // 链式操作修改 webpack 配置
  chainWebpack: (config) => {
    if (process.env.VUE_APP_ANALYZER === "true") {
      config.plugin("webpack-bundle-analyzer").use(BundleAnalyzerPlugin);
    }

    config.devServer
      .headers({
        "Access-Control-Allow-Origin": "*",
      })
      .set("disableHostCheck", true);

    config.optimization.delete("splitChunks");

    config.output.libraryTarget("umd");

    config.output.set("jsonpFunction", `webpackJsonp__${projectName}`);

    // 配合 devtool:sourcemap 选项, 让多个工程有独立的调试命名空间,不至于 sourcemap 中地址冲突,无法调试
    config.output.devtoolNamespace(projectName);
    config.set("devtool", "sourcemap");

    // config.output["filename"] = "[name].js";
    // config.output["chunkFilename"] = "[name].js";

    // config.module
    //   .rule("images")
    //   .use("url-loader")
    //   .tap((options) => {
    //     // 修改它的选项...
    //     options.fallback.options.name = "js/" + options.fallback.options.name;
    //     return options;
    //   });
  },

安装 single-spa-vue库

npm install -S single-spa-vue

目前版本是:2.5.1

安装 systemjs-webpack-interop

npm install -S systemjs-webpack-interop

目前版本是:2.3.7

抽取公共库放到externals

@vue/cli中自定义webpack的必要项

configureWebpack选项

  1. 如果这个值是一个对象,则会通过 webpack-merge 合并到最终的配置中;
  2. 如果这个值是一个函数,则会接收被解析的配置作为参数。该函数既可以修改配置并不返回任何东西,也可以返回一个被克隆或合并过的配置版本;

chainWebpack

是一个函数,会接收一个基于 webpack-chain 的 ChainableConfig 实例。允许对内部的 webpack 配置进行更细粒度的修改。

外置vue的基础依赖

此处应该查看webpack v4 的外部扩展: v4.webpack.docschina.org/configurati…

我们把基础的 vue、vue-router、vuex抽取出来,放到SPAs的公共html这个唯一入口中去

configureWebpack: {
  externals: ["vue", "vue-router", "vuex", /^@sdemo/.+/],
},

vue app入口的改造

  1. 在root-config的唯一入口html中,没有vue工程以往的

    标签;

    1. 运行成功之后,会看到类似这样的标签
  2. 在src目录下添加文件set-public-path.js,并在main.js的第一行引入这个文件

import { setPublicPath } from "systemjs-webpack-interop";
setPublicPath("@sdemo/navbar", 2);
  1. 修改vue根app的通常启动方式
// vue 2 app 通常创建方式
// new Vue({
//   render: (h) => h(App),
// }).$mount("#app");

const vueLifecycles = singleSpaVue({
  Vue,
  appOptions: {
    render(h) {
      return h(App, { props: { githubLink: this.githubLink } });
    },
    router,
  },
});

export const bootstrap = vueLifecycles.bootstrap;
export const mount = vueLifecycles.mount;
export const unmount = vueLifecycles.unmount;
  1. 在控制台中,可能会看到请求的库是dev版本的并且是从cdn.jsdelivr.net网站下载的
    1. 可以看一下 www.yuque.com/zuiyu-qwofk…

构建品分析插件

npm i -D webpack-bundle-analyzer@4.4.2

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}

排除了vue、vue-router、vuex之后,使用分析命令分析navbar的构成,发现最大的包是 vue.runtime.esm.js

vue.js 和 vue.runtime.js 的区别和使用方法

  1. vue.js(完整版)
    1. 拥有全部的组件(如 compiler)
  2. vue.runtime.js(非完整版)
    1. 没有 compiler

总结

经过一些列的折腾之后,总结如下:

  1. 前端微服务需要的系统工程的知识,涉及到源码工程管理(lerna)、公共组件提取(element-ui、Antd),工程切分;
  2. 使用SystemJS加载AMD格式的包;
  3. 将通过@vue/cli 构建出来的工程修改为:
    1. 所有公共组件 externals 出去,放到 root-config 唯一口的 html 中加载
    2. 引入 vue-cli-plugin-singel-vue,和 single-spa-vue;
    3. 改造入口并导出 single-app的生命周期。
  4. 改造单独项目就是重中之重了
    1. 可以考虑修改为,本地开发、独立部署运行模式,有自己的html入口,独立app运行;
    2. 可以考虑,开发时期引入微服务框架的开发模式,这个开发模式根据自己的业务,带入基础的页面框架,以方便本地调试;
  5. root-config目前只是一个入口项目,包含html、SystemJS的基础配置,没有实质性的内容;navbar是导航窗口。可以考虑改造:
    1. 将微服务的统领配置移入navbar,改名为root-nav(portal),意义就是后管系统的面门系统,应该包含用户注册、用户登录、消息系统、后管框架一级系统菜单及业务绘制区域;
    2. event pub/sub对象在用户登录、消息系统登录之后注入到每一个 app 入口;