四时宝库

程序员的知识宝库

细聊Single-Spa + Vue Cli 微前端落地指南「实践」

作者:王圣松

转发链接:https://juejin.im/post/5dfd8a0c6fb9a0165f490004

概念

什么是 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。我们为了方便,暂时不引入vuex和eslint。记得,父项目的 vue-router 要开启history模式。



接着我们安装ant-design-vue 和 single-spa,然后启动项目。

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

父项目注册子项目路由

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

  {
    path: '/vue',
    name: 'vue',
  }
复制代码

搭建基础框架

我们在父项目的入口 vue组件,简单地写一下我们的基础布局。左边为菜单栏,右边是布局栏。

左边菜单栏内有一项vue列表项,vue 里面有2个路由。分别是子项目的 home 和 about. 右侧内容栏内,增加一个id为 single-vue 的dom元素,这是我们稍后子项目要挂载的目标dom元素。



<template>
  <a-layout id="components-layout-demo-custom-trigger">
    <a-layout-sider :trigger="null" collapsible v-model="collapsed">
      <div class="logo" />
      <a-menu theme="dark" mode="inline">
        <a-sub-menu key="1">
          <span slot="title">
            <a-icon type="user" />
            <span>Vue</span>
          </span>
          <a-menu-item key="1-1">
            <a href="/vue#">
              Home
            </a>
          </a-menu-item>
          <a-menu-item key="1-2">
            <a href="/vue#/about">
              About
            </a>
          </a-menu-item>
        </a-sub-menu>
      </a-menu>
    </a-layout-sider>
    <a-layout>
      <a-layout-header style="background: #fff; padding: 0" />
      <a-layout-content :style="{ margin: '24px 16px', padding: '24px', background: '#fff', minHeight: '280px' }">
        <div class="content">
          <!--这是右侧内容栏-->
          <div id="single-vue" class="single-spa-vue">
            <div id="vue"></div>
          </div>
        </div>
      </a-layout-content>
    </a-layout>
  </a-layout>
</template>
<script>
  export default {
    data() {
      return {
        collapsed: false,
      };
    }
  };
</script>
<style>
  #components-layout-demo-custom-trigger .trigger {
    font-size: 18px;
    line-height: 64px;
    padding: 0 24px;
    cursor: pointer;
    transition: color 0.3s;
  }

  #components-layout-demo-custom-trigger .trigger:hover {
    color: #1890ff;
  }

  #components-layout-demo-custom-trigger .logo {
    height: 32px;
    background: rgba(255, 255, 255, 0.2);
    margin: 16px;
  }
</style>
复制代码

注册子项目

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

singleSpa.registerApplication:这是注册子项目的方法。参数如下:

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

singleSpa.start:这是启动函数。

我们新建一个 single-spa-config.js,并在main.js内引入。

// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import Ant from 'ant-design-vue';
import './single-spa-config.js'
import 'ant-design-vue/dist/antd.css';
Vue.config.productionTip = false;

Vue.use(Ant);

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')
复制代码

single-spa-config.js:

// single-spa-config.js
import * as singleSpa from 'single-spa'; //导入single-spa
/*
* runScript:一个promise同步方法。可以代替创建一个script标签,然后加载服务
* */
const runScript = async (url) => {
    return new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = url;
        script.onload = resolve;
        script.onerror = reject;
        const firstScript = document.getElementsByTagName('script')[0];
        firstScript.parentNode.insertBefore(script, firstScript);
    });
};

singleSpa.registerApplication( //注册微前端服务
    'singleDemo', 
    async () => {
        await runScript('http://127.0.0.1:3000/js/chunk-vendors.js');
        await runScript('http://127.0.0.1:3000/js/app.js');
        return window.singleVue;
    },
    location => location.pathname.startsWith('/vue') // 配置微前端模块前缀
);

singleSpa.start(); // 启动
复制代码

与官方文档不同的是,我们这里使用了 远程加载。远程加载的原理,我们后面会单独写。

父项目就处理完毕了,接下来我们处理子项目。

子项目的处理

初始化项目

子项目的处理,比父项目就稍微复杂一些。

我们还是新建一个项目,叫做vue-child,使用 vue create vue-child 创建。子项目的创建过程,就随意了,这里我们忽略过程。

另外,我们需要安装一个叫做 single-spa-vue 的npm包。

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

single-spa-vue

如果想注册为一个子项目,还需要 single-spa-vue 的包装。

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

import singleSpaVue from "single-spa-vue";
import Vue from 'vue'

const vueOptions = {
    el: "#vue",
    router,
    store,
    render: h => h(App)
};

// singleSpaVue包装一个vue微前端服务对象
const vueLifecycles = singleSpaVue({
    Vue,
    appOptions: vueOptions
});

// 导出生命周期对象
export const bootstrap = vueLifecycles.bootstrap; // 启动时
export const mount = vueLifecycles.mount; // 挂载时
export const unmount = vueLifecycles.unmount; // 卸载时

export default vueLifecycles;

复制代码

webpack的处理

只是导出了,还需要挂载到window。

在项目目录下新建 vue.config.js, 修改我们的webpack配置。我们修改webpack output内的 library 和 libraryTarget 字段。

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

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

module.exports = {
    publicPath: "//localhost:3000/",
    // css在所有环境下,都不单独打包为文件。这样是为了保证最小引入(只引入js)
    css: {
        extract: false
    },
    configureWebpack: {
        devtool: 'none', // 不打包sourcemap
        output: {
            library: "singleVue", // 导出名称
            libraryTarget: "window", //挂载目标
        }
    },
    devServer: {
        contentBase: './',
        compress: true,
    }
};

复制代码

我们执行 vue-cli-service serve --port 3000后,就可以看到一直等待的界面了~

其中,左侧可以切换子项目中的路由。右侧联网加载。

这样,我们的第一版就大功告成了。接下来,我们做进一步优化和分享

样式隔离

样式隔离这块,我们使用postcss的一个插件:postcss-selector-namespace。 他会把你项目里的所有css都会添加一个类名前缀。这样就可以实现命名空间隔离

首先,我们先安装这个插件:npm install postcss-selector-namespace --save -d

项目目录下新建 postcss.config.js,使用插件:

// postcss.config.js

module.exports = {
  plugins: {
    // postcss-selector-namespace: 给所有css添加统一前缀,然后父项目添加命名空间
    'postcss-selector-namespace': {
      namespace(css) {
        // element-ui的样式不需要添加命名空间
        if (css.includes('element-variables.scss')) return '';
        return '.single-spa-vue' // 返回要添加的类名
      }
    },
  }
}
复制代码

在父项目要挂载的区块,添加我们的命名空间。结束

独立运行

大家可能会发现,我们的子服务现在是无法独立运行的,现在我们改造为可以独立 + 集成双模式运行。

single-spa 有个属性,叫做 window.singleSpaNavigate。如果为true,代表就是single-spa模式。如果false,就可以独立渲染。

我们改造一下子项目的main.js :

// main.js
const vueOptions = {
  el: "#vue",
  router,
  render: h => h(App)
};

/**** 添加这里 ****/
if (!window.singleSpaNavigate) { // 如果不是single-spa模式
  delete vueOptions.el;
  new Vue(vueOptions).$mount('#vue');
}
/**** 结束 ****/

// singleSpaVue包装一个vue微前端服务对象
const vueLifecycles = singleSpaVue({
  Vue,
  appOptions: vueOptions
});
复制代码

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

<div class="single-spa-vue">
    <div id="app"></div>
</div>
复制代码

需要了解的知识点

远程加载

在这里,我们的远程加载使用的是async await构建一个同步执行任务。

创建一个script标签,等script加载后,返回script加载到window上面的对象。

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

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。

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>
  </div>
</template>
复制代码

manifest 自动加载 bundle和chunk.vendor

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

singleSpa.registerApplication(
    'singleVue',
    async () => {
        await runScript('http://127.0.0.1:3000/js/chunk-vendors.js'); // 写死的文件列表
        await runScript('http://127.0.0.1:3000/js/app.js');
        return window.singleVue;
    },
    location => location.pathname.startsWith('/vue') 
);

复制代码

我们的实现思路,就是让子项目使用 stats-webpack-plugin 插件,每次打包后都输出一个 只包含重要信息的manifest.json文件。父项目先ajax 请求 这个json文件,从中读取出需要加载的js目录,然后同步加载。

stats-webpack-plugin

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

{
  "errors": [],
  "warnings": [],
  "version": "4.41.4",
  "hash": "d0601ce74a7b9821751e",
  "publicPath": "//localhost:3000/",
  "outputPath": "/Users/janlay/juejin-single/vue-chlid/dist",
  "entrypoints": { // 只使用这个字段
    "app": {
      "chunks": [
        "chunk-vendors",
        "app"
      ],
      "assets": [
        "js/chunk-vendors.75fba470.js",
        "js/app.3249afbe.js"
      ],
      "children": {},
      "childAssets": {}
    }
    ... ...
  }
复制代码

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

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

在vue.config.js中使用:

{
    configureWebpack: {
        devtool: 'none',
        output: {
            library: "singleVue",
            libraryTarget: "window",
        },
        /**** 添加开头 ****/
        plugins: [
            new StatsPlugin('manifest.json', {
                chunkModules: false,
                entrypoints: true,
                source: false,
                chunks: false,
                modules: false,
                assets: false,
                children: false,
                exclude: [/node_modules/]
            }),
        ]
        /**** 添加结尾 ****/
    }
}
复制代码

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

地址:https://www.webpackjs.com/configuration/stats/

父项目改造

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

/*
* getManifest:远程加载manifest.json 文件,解析需要加载的js
* url: manifest.json 链接
* bundle:entry名称
* */
const getManifest = (url, bundle) => new Promise(async (resolve) => {
    const { data } = await axios.get(url);
    const { entrypoints, publicPath } = data;
    const assets = entrypoints[bundle].assets;
    for (let i = 0; i < assets.length; i++) {
        await runScript(publicPath + assets[i]).then(() => {
            if (i === assets.length - 1) {
                resolve()
            }
        })
    }
});
复制代码

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

async () => {
    let singleVue = null;
    await getManifest('http://127.0.0.1:3000/manifest.json', 'app').then(() => {
        singleVue = window.singleVue;
    });
    return singleVue;
},
复制代码

其实,如果要做一个 微前端管理平台,也是靠这个实现。

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

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

地址:https://github.com/YataoZhang/my-single-spa/issues/4

展望

通信

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

js沙箱实现状态管理

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

推荐Vue学习资料文章:

通俗易懂的Vue异步更新策略及 nextTick 原理

通俗易懂的Vue响应式原理以及依赖收集

原生JS +Vue实现框选功能

Vue.js轮播库热门精选

一文带你搞懂vue/react应用中实现ssr(服务端渲染)

Vue+CSS3 实现图片滑块效果

教你Vue3 Compiler 优化细节,如何手写高性能渲染函数(上)

教你Vue3 Compiler 优化细节,如何手写高性能渲染函数(下)

vue实现一个6个输入框的验证码输入组件

一用惊人的Vue实践技巧「值得推荐」

Vue常见的面试知识点汇总(上)「附答案」

Vue常见的面试知识点汇总(下)「附答案」

Kbone原理详解与小程序技术选型

为什么我不再用Vue,改用React?

让Jenkins自动部署你的Vue项目「实践」

20个免费的设计资源 UI套件背景图标CSS框架

Deno将停止使用TypeScript,并公布五项具体理由

前端骨架屏都是如何生成的

Vue原来可以这样写开发效率杠杠的

用vue简单写一个音乐播放组件「附源码」

为什么Vue3.0不再使用defineProperty实现数据监听?

「干货」学会这些Vue小技巧,可以早点下班和女神约会

探索 Vue-Multiselect

细品30张脑图带你从零开始学Vue

Vue后台项目中遇到的技术难点以及解决方案

手把手教你Electron + Vue实战教程(五)

手把手教你Electron + Vue实战教程(四)

手把手教你Electron + Vue实战教程(三)

手把手教你Electron + Vue实战教程(二)

手把手教你Electron + Vue实战教程(一)

收集22种开源Vue模板和主题框架「干货」

如何写出优秀后台管理系统?11个经典模版拿去不谢「干货」

手把手教你实现一个Vue自定义指令懒加载

基于 Vue 和高德地图实现地图组件「实践」

一个由 Vue 作者尤雨溪开发的 web 开发工具—vite

是什么让我爱上了Vue.js

1.1万字深入细品Vue3.0源码响应式系统笔记「上」

1.1万字深入细品Vue3.0源码响应式系统笔记「下」

「实践」Vue 数据更新7 种情况汇总及延伸解决总结

尤大大细说Vue3 的诞生之路「译」

提高10倍打包速度工具Snowpack 2.0正式发布,再也不需要打包器

大厂Code Review总结Vue开发规范经验「值得学习」

Vue3 插件开发详解尝鲜版「值得收藏」

带你五步学会Vue SSR

记一次Vue3.0技术干货分享会

Vue 3.x 如何有惊无险地快速入门「进阶篇」

「干货」微信支付前后端流程整理(Vue+Node)

带你了解 vue-next(Vue 3.0)之 炉火纯青「实践」

「干货」Vue+高德地图实现页面点击绘制多边形及多边形切割拆分

「干货」Vue+Element前端导入导出Excel

「实践」Deno bytes 模块全解析

细品pdf.js实践解决含水印、电子签章问题「Vue篇」

基于vue + element的后台管理系统解决方案

Vue仿蘑菇街商城项目(vue+koa+mongodb)

基于 electron-vue 开发的音乐播放器「实践」

「实践」Vue项目中标配编辑器插件Vue-Quill-Editor

基于 Vue 技术栈的微前端方案实践

消息队列助你成为高薪 Node.js 工程师

Node.js 中的 stream 模块详解

「干货」Deno TCP Echo Server 是怎么运行的?

「干货」了不起的 Deno 实战教程

「干货」通俗易懂的Deno 入门教程

Deno 正式发布,彻底弄明白和 node 的区别

「实践」基于Apify+node+react/vue搭建一个有点意思的爬虫平台

「实践」深入对比 Vue 3.0 Composition API 和 React Hooks

前端网红框架的插件机制全梳理(axios、koa、redux、vuex)

深入Vue 必学高阶组件 HOC「进阶篇」

深入学习Vue的data、computed、watch来实现最精简响应式系统

10个实例小练习,快速入门熟练 Vue3 核心新特性(一)

10个实例小练习,快速入门熟练 Vue3 核心新特性(二)

教你部署搭建一个Vue-cli4+Webpack移动端框架「实践」

2020前端就业Vue框架篇「实践」

详解Vue3中 router 带来了哪些变化?

Vue项目部署及性能优化指导篇「实践」

Vue高性能渲染大数据Tree组件「实践」

尤大大细品VuePress搭建技术网站与个人博客「实践」

10个Vue开发技巧「实践」

是什么导致尤大大选择放弃Webpack?【vite 原理解析】

带你了解 vue-next(Vue 3.0)之 小试牛刀【实践】

带你了解 vue-next(Vue 3.0)之 初入茅庐【实践】

实践Vue 3.0做JSX(TSX)风格的组件开发

一篇文章教你并列比较React.js和Vue.js的语法【实践】

手拉手带你开启Vue3世界的鬼斧神工【实践】

深入浅出通过vue-cli3构建一个SSR应用程序【实践】

怎样为你的 Vue.js 单页应用提速

聊聊昨晚尤雨溪现场针对Vue3.0 Beta版本新特性知识点汇总

【新消息】Vue 3.0 Beta 版本发布,你还学的动么?

Vue真是太好了 壹万多字的Vue知识点 超详细!

Vue + Koa从零打造一个H5页面可视化编辑器——Quark-h5

深入浅出Vue3 跟着尤雨溪学 TypeScript 之 Ref 【实践】

手把手教你深入浅出vue-cli3升级vue-cli4的方法

Vue 3.0 Beta 和React 开发者分别杠上了

手把手教你用vue drag chart 实现一个可以拖动 / 缩放的图表组件

Vue3 尝鲜

总结Vue组件的通信

Vue 开源项目 TOP45

2020 年,Vue 受欢迎程度是否会超过 React?

尤雨溪:Vue 3.0的设计原则

使用vue实现HTML页面生成图片

实现全栈收银系统(Node+Vue)(上)

实现全栈收银系统(Node+Vue)(下)

vue引入原生高德地图

Vue合理配置WebSocket并实现群聊

多年vue项目实战经验汇总

vue之将echart封装为组件

基于 Vue 的两层吸顶踩坑总结

Vue插件总结【前端开发必备】

Vue 开发必须知道的 36 个技巧【近1W字】

构建大型 Vue.js 项目的10条建议

深入理解vue中的slot与slot-scope

手把手教你Vue解析pdf(base64)转图片【实践】

使用vue+node搭建前端异常监控系统

推荐 8 个漂亮的 vue.js 进度条组件

基于Vue实现拖拽升级(九宫格拖拽)

手摸手,带你用vue撸后台 系列二(登录权限篇)

手摸手,带你用vue撸后台 系列三(实战篇)

前端框架用vue还是react?清晰对比两者差异

Vue组件间通信几种方式,你用哪种?【实践】

浅析 React / Vue 跨端渲染原理与实现

10个Vue开发技巧助力成为更好的工程师

手把手教你Vue之父子组件间通信实践讲解【props、$ref 、$emit】

1W字长文+多图,带你了解vue的双向数据绑定源码实现

深入浅出Vue3 的响应式和以前的区别到底在哪里?【实践】

干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)

基于Vue/VueRouter/Vuex/Axios登录路由和接口级拦截原理与实现

手把手教你D3.js 实现数据可视化极速上手到Vue应用

吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【上】

吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【中】

吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【下】

Vue3.0权限管理实现流程【实践】

后台管理系统,前端Vue根据角色动态设置菜单栏和路由


作者:王圣松
转发链接:https://juejin.im/post/5dfd8a0c6fb9a0165f490004

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言
    友情链接