概念

什么是 single-spa?

single-spa 一个基于JavaScript的 微前端 框架,他可以用于构建可共存的微前端应用,每个前端应用都可以用自己的框架编写,完美支持 Vue React Angular。可以实现 服务注册 事件监听 子父组件通信 等功能。
用于 父项目 集成子项目使用

什么是 single-spa-vue ?

single-spa-vue 是提供给使用vue子项目使用的npm包。他可以快速和sigle-spa父项目集成,并提供了一些比较便携的api。
用于 子项目 使用

我们要实现的

  • vue-cli 与 single-spa 集成
  • 远程加载服务
  • manifest 自动加载需要的 JS
  • namespace 样式隔离
  • 兼容性问题解决

    父项目的处理

    初始化项目

    我们父项目和子项目都使用vue-cli进行集成。父项目为了美化,用ant-design-vue做前端框架。
    新建一个项目,名称叫 parent。我们为了方便,暂时不引入vuexeslint。记得,父项目的 vue-router 要开启history模式。
    Single-Spa   Vue Cli 微前端落地指南 (项目隔离远程加载,自动引入) - 图1
    接着我们安装ant-design-vuesingle-spa,然后启动项目。

    1. npm install ant-design-vue single-spa --save -d
    2. 复制代码

    父项目注册子项目路由

    我们注册一个子服务路由,只是注册, 不填写component字段。

    1. {
    2. path: '/vue',
    3. name: 'vue',
    4. }
    5. 复制代码

    搭建基础框架

    我们在父项目的入口 vue组件,简单地写一下我们的基础布局。左边为菜单栏,右边是布局栏。
    左边菜单栏内有一项vue列表项,vue 里面有2个路由。分别是子项目的 homeabout. 右侧内容栏内,增加一个id为 single-vue 的dom元素,这是我们稍后子项目要挂载的目标dom元素。
    Single-Spa   Vue Cli 微前端落地指南 (项目隔离远程加载,自动引入) - 图2

    1. <template>
    2. <a-layout id="components-layout-demo-custom-trigger">
    3. <a-layout-sider :trigger="null" collapsible v-model="collapsed">
    4. <div class="logo" />
    5. <a-menu theme="dark" mode="inline">
    6. <a-sub-menu key="1">
    7. <span slot="title">
    8. <a-icon type="user" />
    9. <span>Vue</span>
    10. </span>
    11. <a-menu-item key="1-1">
    12. <a href="/vue#">
    13. Home
    14. </a>
    15. </a-menu-item>
    16. <a-menu-item key="1-2">
    17. <a href="/vue#/about">
    18. About
    19. </a>
    20. </a-menu-item>
    21. </a-sub-menu>
    22. </a-menu>
    23. </a-layout-sider>
    24. <a-layout>
    25. <a-layout-header style="background: #fff; padding: 0" />
    26. <a-layout-content :style="{ margin: '24px 16px', padding: '24px', background: '#fff', minHeight: '280px' }">
    27. <div class="content">
    28. <!--这是右侧内容栏-->
    29. <div id="single-vue" class="single-spa-vue">
    30. <div id="vue"></div>
    31. </div>
    32. </div>
    33. </a-layout-content>
    34. </a-layout>
    35. </a-layout>
    36. </template>
    37. <script>
    38. export default {
    39. data() {
    40. return {
    41. collapsed: false,
    42. };
    43. }
    44. };
    45. </script>
    46. <style>
    47. #components-layout-demo-custom-trigger .trigger {
    48. font-size: 18px;
    49. line-height: 64px;
    50. padding: 0 24px;
    51. cursor: pointer;
    52. transition: color 0.3s;
    53. }
    54. #components-layout-demo-custom-trigger .trigger:hover {
    55. color: #1890ff;
    56. }
    57. #components-layout-demo-custom-trigger .logo {
    58. height: 32px;
    59. background: rgba(255, 255, 255, 0.2);
    60. margin: 16px;
    61. }
    62. </style>
    63. 复制代码

    注册子项目

    这里就是我们的重头戏:如何使用single-spa注册子项目。在注册之前,我们先了解一下2个api:
    singleSpa.registerApplication:这是注册子项目的方法。参数如下:

  • appName: 子项目名称

  • applicationOrLoadingFn: 子项目注册函数,用户需要返回 single-spa 的生命周期对象。后面我们会介绍single-spa的生命周期机制
  • activityFn: 回调函数入参 location 对象,可以写自定义匹配路由加载规则。

singleSpa.start:这是启动函数。
我们新建一个 single-spa-config.js,并在main.js内引入。

  1. // main.js
  2. import Vue from 'vue'
  3. import App from './App.vue'
  4. import router from './router'
  5. import Ant from 'ant-design-vue';
  6. import './single-spa-config.js'
  7. import 'ant-design-vue/dist/antd.css';
  8. Vue.config.productionTip = false;
  9. Vue.use(Ant);
  10. new Vue({
  11. router,
  12. render: h => h(App)
  13. }).$mount('#app')
  14. 复制代码

single-spa-config.js:

  1. // single-spa-config.js
  2. import * as singleSpa from 'single-spa'; //导入single-spa
  3. /*
  4. * runScript:一个promise同步方法。可以代替创建一个script标签,然后加载服务
  5. * */
  6. const runScript = async (url) => {
  7. return new Promise((resolve, reject) => {
  8. const script = document.createElement('script');
  9. script.src = url;
  10. script.onload = resolve;
  11. script.onerror = reject;
  12. const firstScript = document.getElementsByTagName('script')[0];
  13. firstScript.parentNode.insertBefore(script, firstScript);
  14. });
  15. };
  16. singleSpa.registerApplication( //注册微前端服务
  17. 'singleDemo',
  18. async () => {
  19. await runScript('http://127.0.0.1:3000/js/chunk-vendors.js');
  20. await runScript('http://127.0.0.1:3000/js/app.js');
  21. return window.singleVue;
  22. },
  23. location => location.pathname.startsWith('/vue') // 配置微前端模块前缀
  24. );
  25. singleSpa.start(); // 启动
  26. 复制代码

与官方文档不同的是,我们这里使用了 远程加载。远程加载的原理,我们后面会单独写。
父项目就处理完毕了,接下来我们处理子项目。

子项目的处理

初始化项目

子项目的处理,比父项目就稍微复杂一些。
我们还是新建一个项目,叫做vue-child,使用 vue create vue-child 创建。子项目的创建过程,就随意了,这里我们忽略过程。
另外,我们需要安装一个叫做 single-spa-vue 的npm包。

  1. npm install single-spa-vue --save -d
  2. 复制代码

single-spa-vue

如果想注册为一个子项目,还需要 single-spa-vue 的包装。
main.js中引入 single-spa-vue,传入Vue对象和vue.js挂载参数,就可以实现注册。它会返回一个对象,里面有single-spa 需要的生命周期函数。使用export导出即可

  1. import singleSpaVue from "single-spa-vue";
  2. import Vue from 'vue'
  3. const vueOptions = {
  4. el: "#vue",
  5. router,
  6. store,
  7. render: h => h(App)
  8. };
  9. // singleSpaVue包装一个vue微前端服务对象
  10. const vueLifecycles = singleSpaVue({
  11. Vue,
  12. appOptions: vueOptions
  13. });
  14. // 导出生命周期对象
  15. export const bootstrap = vueLifecycles.bootstrap; // 启动时
  16. export const mount = vueLifecycles.mount; // 挂载时
  17. export const unmount = vueLifecycles.unmount; // 卸载时
  18. export default vueLifecycles;
  19. 复制代码

webpack的处理

只是导出了,还需要挂载到window
在项目目录下新建 vue.config.js, 修改我们的webpack配置。我们修改webpack output内的 librarylibraryTarget 字段。

  • output.library: 导出的对象名
  • output.libraryTarget: 导出后要挂载到哪里

同时,因为我们是远程调用,还需要设置 publicPath 字段为你的真实服务地址。否则加载子chunk时,会去当前浏览器域名的根路径寻找,有404问题。 因为我们本地的服务启动是localhost:3000,所以我们就设置 //localhost:3000

  1. module.exports = {
  2. publicPath: "//localhost:3000/",
  3. // css在所有环境下,都不单独打包为文件。这样是为了保证最小引入(只引入js)
  4. css: {
  5. extract: false
  6. },
  7. configureWebpack: {
  8. devtool: 'none', // 不打包sourcemap
  9. output: {
  10. library: "singleVue", // 导出名称
  11. libraryTarget: "window", //挂载目标
  12. }
  13. },
  14. devServer: {
  15. contentBase: './',
  16. compress: true,
  17. }
  18. };
  19. 复制代码

我们执行 vue-cli-service serve --port 3000后,就可以看到一直等待的界面了~
Single-Spa   Vue Cli 微前端落地指南 (项目隔离远程加载,自动引入) - 图3
其中,左侧可以切换子项目中的路由。右侧联网加载。
这样,我们的第一版就大功告成了。接下来,我们做进一步优化和分享

样式隔离

样式隔离这块,我们使用postcss的一个插件:postcss-selector-namespace。 他会把你项目里的所有css都会添加一个类名前缀。这样就可以实现命名空间隔离
首先,我们先安装这个插件:npm install postcss-selector-namespace --save -d
项目目录下新建 postcss.config.js,使用插件:

  1. // postcss.config.js
  2. module.exports = {
  3. plugins: {
  4. // postcss-selector-namespace: 给所有css添加统一前缀,然后父项目添加命名空间
  5. 'postcss-selector-namespace': {
  6. namespace(css) {
  7. // element-ui的样式不需要添加命名空间
  8. if (css.includes('element-variables.scss')) return '';
  9. return '.single-spa-vue' // 返回要添加的类名
  10. }
  11. },
  12. }
  13. }
  14. 复制代码

在父项目要挂载的区块,添加我们的命名空间。结束
Single-Spa   Vue Cli 微前端落地指南 (项目隔离远程加载,自动引入) - 图4

独立运行

大家可能会发现,我们的子服务现在是无法独立运行的,现在我们改造为可以独立 + 集成双模式运行。
single-spa 有个属性,叫做 window.singleSpaNavigate。如果为true,代表就是single-spa模式。如果false,就可以独立渲染。
我们改造一下子项目的main.js

  1. // main.js
  2. const vueOptions = {
  3. el: "#vue",
  4. router,
  5. render: h => h(App)
  6. };
  7. /**** 添加这里 ****/
  8. if (!window.singleSpaNavigate) { // 如果不是single-spa模式
  9. delete vueOptions.el;
  10. new Vue(vueOptions).$mount('#vue');
  11. }
  12. /**** 结束 ****/
  13. // singleSpaVue包装一个vue微前端服务对象
  14. const vueLifecycles = singleSpaVue({
  15. Vue,
  16. appOptions: vueOptions
  17. });
  18. 复制代码

这样,我们就可以独立访问子服务的 index.html 。不要忘记在public/index.html里面添加命名空间,否则会丢失样式。

  1. <div class="single-spa-vue">
  2. <div id="app"></div>
  3. </div>
  4. 复制代码

需要了解的知识点

远程加载

在这里,我们的远程加载使用的是async await构建一个同步执行任务。
创建一个script标签,等script加载后,返回script加载到window上面的对象。

  1. /*
  2. * runScript:一个promise同步方法。可以代替创建一个script标签,然后加载服务
  3. * */
  4. const runScript = async (url) => {
  5. return new Promise((resolve, reject) => {
  6. const script = document.createElement('script');
  7. script.src = url;
  8. script.onload = resolve;
  9. script.onerror = reject;
  10. const firstScript = document.getElementsByTagName('script')[0];
  11. firstScript.parentNode.insertBefore(script, firstScript);
  12. });
  13. };
  14. 复制代码

vue 和 react/angular 挂载的区别

Vue 2.x的dom挂载,采取的是 覆盖Dom挂载 的方式。例如,组件要挂载到#app上,那么它会用组件覆盖掉#app元素。
但是React/Angular不同,它们的挂载方式是在目标挂载元素的内部添加元素,而不是直接覆盖掉。 例如组件要挂载到#app上,那么他会在#app内部挂载组件,#app还存在。
这样就造成了一个问题,当我从 vue子项目 => react项目 => vue子项目时,就会找不到要挂载的dom元素,从而抛出错误。
解决这个问题的方案是,让 vue项目组件的根元素类名/ID名和要挂载的元素一致 就可以。
例如我们要挂载到 #app 这个dom上,那么我们子项目内部的app.vue,最顶部的dom元素id名也应该叫 #app

  1. <template>
  2. <div id="app">
  3. <div id="nav">
  4. <router-link to="/">Home</router-link> |
  5. <router-link to="/about">About</router-link>
  6. </div>
  7. <router-view/>
  8. </div>
  9. </template>
  10. 复制代码

manifest 自动加载 bundle和chunk.vendor

在上面父项目加载子项目的代码中,我们可以看到。我们要注册一个子服务,需要一次性加载2个JS文件。如果需要加载的JS更多,甚至生产环境的 bundle 有唯一hash, 那我们还能写死文件名和列表吗?

  1. singleSpa.registerApplication(
  2. 'singleVue',
  3. async () => {
  4. await runScript('http://127.0.0.1:3000/js/chunk-vendors.js'); // 写死的文件列表
  5. await runScript('http://127.0.0.1:3000/js/app.js');
  6. return window.singleVue;
  7. },
  8. location => location.pathname.startsWith('/vue')
  9. );
  10. 复制代码

我们的实现思路,就是让子项目使用 stats-webpack-plugin 插件,每次打包后都输出一个 只包含重要信息的manifest.json文件。父项目先ajax 请求 这个json文件,从中读取出需要加载的js目录,然后同步加载。
Single-Spa   Vue Cli 微前端落地指南 (项目隔离远程加载,自动引入) - 图5

stats-webpack-plugin

这里就不得不提到这个webpack plugin了。它可以在你每次打包结束后,都生成一个manifest.json 文件,里面存放着本次打包的 public_path bundle list chunk list 文件大小依赖等等信息。

  1. {
  2. "errors": [],
  3. "warnings": [],
  4. "version": "4.41.4",
  5. "hash": "d0601ce74a7b9821751e",
  6. "publicPath": "//localhost:3000/",
  7. "outputPath": "/Users/janlay/juejin-single/vue-chlid/dist",
  8. "entrypoints": { // 只使用这个字段
  9. "app": {
  10. "chunks": [
  11. "chunk-vendors",
  12. "app"
  13. ],
  14. "assets": [
  15. "js/chunk-vendors.75fba470.js",
  16. "js/app.3249afbe.js"
  17. ],
  18. "children": {},
  19. "childAssets": {}
  20. }
  21. ... ...
  22. }
  23. 复制代码

我们切换到子项目的目录,安装这个webpack插件:

  1. npm install stats-webpack-plugin --save -d
  2. 复制代码

vue.config.js中使用:

  1. {
  2. configureWebpack: {
  3. devtool: 'none',
  4. output: {
  5. library: "singleVue",
  6. libraryTarget: "window",
  7. },
  8. /**** 添加开头 ****/
  9. plugins: [
  10. new StatsPlugin('manifest.json', {
  11. chunkModules: false,
  12. entrypoints: true,
  13. source: false,
  14. chunks: false,
  15. modules: false,
  16. assets: false,
  17. children: false,
  18. exclude: [/node_modules/]
  19. }),
  20. ]
  21. /**** 添加结尾 ****/
  22. }
  23. }
  24. 复制代码

具体的配置项,可以访问 webpack 中文文档 - configuration - stats 查阅

父项目改造

当然,父项目中的单runScript已经无法支持使用了,写个getManifest方法,处理一下。

  1. /*
  2. * getManifest:远程加载manifest.json 文件,解析需要加载的js
  3. * url: manifest.json 链接
  4. * bundle:entry名称
  5. * */
  6. const getManifest = (url, bundle) => new Promise(async (resolve) => {
  7. const { data } = await axios.get(url);
  8. const { entrypoints, publicPath } = data;
  9. const assets = entrypoints[bundle].assets;
  10. for (let i = 0; i < assets.length; i++) {
  11. await runScript(publicPath + assets[i]).then(() => {
  12. if (i === assets.length - 1) {
  13. resolve()
  14. }
  15. })
  16. }
  17. });
  18. 复制代码

我们首先ajax到 manifest.json 文件,解构出里面的 entrypoints publicPath字段,遍历出真实的js路径,然后按照顺序加载。

  1. async () => {
  2. let singleVue = null;
  3. await getManifest('http://127.0.0.1:3000/manifest.json', 'app').then(() => {
  4. singleVue = window.singleVue;
  5. });
  6. return singleVue;
  7. },
  8. 复制代码

其实,如果要做一个 微前端管理平台,也是靠这个实现。
Single-Spa   Vue Cli 微前端落地指南 (项目隔离远程加载,自动引入) - 图6

single-spa 的生命周期和实现原理

这里推荐一位dalao的原理分享:链接

展望

通信

可以使用发布订阅模式实现,也可以实现一个类似于vuex的状态管理

js沙箱实现状态管理

这个可以使用proxy 进行监听,切换保存,切入还原状态

代码仓库 & 最后

本文代码仓库:码云

作者:王圣松
链接:https://juejin.im/post/5dfd8a0c6fb9a0165f490004
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处