写点什么

前端微服务无界实践 | 京东云技术团队

  • 2023-05-25
    北京
  • 本文字数:6295 字

    阅读完需:约 21 分钟

前端微服务无界实践 | 京东云技术团队

一、前言

随着项目的发展,前端 SPA 应用的规模不断加大、业务代码耦合、编译慢,导致日常的维护难度日益增加。同时前端技术的发展迅猛,导致功能扩展吃力,重构成本高,稳定性低。因此前端微服务应运而生。

前端微服务优势

1.复杂度可控: 业务模块解耦,避免代码过大,保持较低的复杂度,便于维护与开发效率。


2.独立部署: 模块部署,减少模块影响范围,单个模块发生错误,不影响全局,提升项目稳定性。


3.技术选型灵活: 在同一项目下可以使用市面上所有前端技术栈,也包括未来的前端技术栈。


4.扩展性,提升业务动态扩展的可能,避免资源浪费

微前端服务结构

技术对比和选型:


通过对比多种技术对项目的支持情况和项目接入的成本,我们最终选型无界。

二、wujie 简单用法(以主应用使用 vue 框架为例)

主应用是 vue 框架可直接使用 wujie-vue,react 框架可直接使用 wujie-react,先安装对应的插件哦

主应用改造:

// 引入无界,根据框架不同版本不同,引入不同的版本import { setupApp, bus, preloadApp, startApp } from 'wujie-vue2'
// 设置子应用默认参数setupApp({ name: '子应用id(唯一值)', url: "子应用地址", exec: true, el: "容器", sync: true})
// 预加载preloadApp({ name: "唯一id"});
// 启动子应用startApp({ name: "唯一id"});
复制代码

子应用改造:

1、跨域

子应用如果支持跨域,则不用修改


原因:存在请求子应用资源跨域


方案:因前端应用基本是前后端分离,使用 proxy 代理。只需配置在子应用配置允许跨域即可


// 本地配置server: {    host: '127.0.0.1', // 本地启动如果主子应用没处在同一个ip下,也存在跨域的问题,需要配置    headers: {        'Access-Control-Allow-Credentials': true,        'Access-Control-Allow-Origin': '*', // 如资源没有携带 cookie,需设置此属性        'Access-Control-Allow-Headers': 'X-Requested-With,Content-Type',        'Access-Control-Allow-Methods': '*'    }}
// nginx 配置add_header Access-Control-Allow-Credentials true;add_header Access-Control-Allow-Origin "*";add_header Access-Control-Allow-Headers 'X-Requested-With,Content-Type';add_header Access-Control-Allow-Methods "*";
复制代码

2、运行模式选择

无界有三种运行模式:单例模式、保活模式、重建模式



(1)、保活模式(长存页面)


释义:类似于 vue 的 keep-alive 性质(子应用实例和 webcomponent 不销毁,状态、路由都不丢失,只做热 webcomponent 的热插拔),子应用不想做生命周期改造,子应用切换又不想有白屏时间,可以采用保活模式。主应用上有多个入口跳转到子应用的不同页面,不能采用保活模式,因为无法改变子应用路由。


配置:只需要在主应用加载子应用的时候,配置参数添加 alive:true


效果:预加载+保活模式=页面数据请求和渲染提前完成,实现瞬间打开效果


(2)、单例模式


释义:子应用页面切走,会调用 window.__WUJIE_UNMOUNT 销毁子应用当前实例。子应用页面如果切换回来,会调用 window.__WUJIE_MOUNT 渲染子应用新的子应用实例。过程相当于:销毁当前应用实例 => 同步新路由 => 创建新应用实例


配置:只需要在主应用加载子应用的时候,配置参数添加 alive:false


改造生命周期


// window.__POWERED_BY_WUJIE__用来判断子应用是否在无界的环境中if (window.__POWERED_BY_WUJIE__) {  let instance;  // 将子应用的实例和路由进线创建和挂载  window.__WUJIE_MOUNT = () => {    const router = new VueRouter({ routes });    instance = new Vue({ router, render: (h) => h(App) }).$mount("#app");  };   // 实例销毁  window.__WUJIE_UNMOUNT = () => {    instance.$destroy();  };} else {  // 子应用单独启动  new Vue({ router: new VueRouter({ routes }), render: (h) => h(App) }).$mount("#app");}
复制代码


(3)、重建模式


释义:每次页面切换销毁子应用 webcomponent+js 的 iframe。


配置:只需要在主应用加载子应用的时候,配置参数添加 alive:false


无生命周期改造


备注:非 webpack 打包的老项目,子应用切换可能出现白屏,应尽可能使用保活模式降低白屏时间

三、加载模块(主应用配置)

子应用基础信息管理

// subList.js 数据可在配置页面动态配置const subList = [    {        "name":"subVueApp1",        "exec":true,// false只会预加载子应用的资源,true时预执行子应用代码        "alive": true,        "show":true,// 是否引入        "url":{            "pre":"http://xxx1-pre.com",            "gray":"http://xxx1-gray.com",            "prod":"http://xxx1.com"        }    },    {        "name":"subVueApp2",        "exec":false,// false只会预加载子应用的资源,true时预执行子应用代码        "alive": false,        "show":true,// 是否引入        "url":{            "pre":"http://xxx2-pre.com",            "gray":"http://xxx2-gray.com",            "prod":"http://xxx2.com"        }    }]export default subList;
复制代码


// hostMap.jsimport subList from './subList'
const env = process.env.mode || 'pre'
// 子应用map结构const subMap = {}const subArr = []
// 转换子应用export const hostMap = () => { subList.forEach(v => { const {url, ...other} = v const info = { ...other, url: url[env] } subMap[v.name] = info subArr.push(info) }) return subArr}
// 获取子应用配置信息export const getSubMap = name => { return subMap[name].show ? subMap[name] : {}}
复制代码

子应用注册预加载和启动

// setupApp.jsimport WujieVue from 'wujie-vue2';import {hostMap} from './hostMap';
const { setupApp, preloadApp } = WujieVue
const setUpApp = Vue => { Vue.use(WujieVue) hostMap().forEach(v => { setupApp(v) preloadApp(v.name) })}export default setUpApp;

// main.jsimport Vue from 'vue'import setUpApp from'@/microConfig/setupApp'setUpApp(Vue)
复制代码

配置公共函数

全子应用共享的生命周期函数,可用于执行多个子应用间相同的逻辑操作函数共同处理


// lifecycle.jsconst lifecycles = {  beforeLoad: (appWindow) => console.log(`${appWindow.__WUJIE.id} beforeLoad 生命周期`),  beforeMount: (appWindow) => console.log(`${appWindow.__WUJIE.id} beforeMount 生命周期`),  afterMount: (appWindow) => console.log(`${appWindow.__WUJIE.id} afterMount 生命周期`),  beforeUnmount: (appWindow) => console.log(`${appWindow.__WUJIE.id} beforeUnmount 生命周期`),  afterUnmount: (appWindow) => console.log(`${appWindow.__WUJIE.id} afterUnmount 生命周期`),  activated: (appWindow) => console.log(`${appWindow.__WUJIE.id} activated 生命周期`),  deactivated: (appWindow) => console.log(`${appWindow.__WUJIE.id} deactivated 生命周期`),  loadError: (url, e) => console.log(`${url} 加载失败`, e),};
export default lifecycles;

// subCommon.js// 跳转到主应用指定页面const toJumpMasterApp = (location, query) => { this.$router.replace(location, query); const url = new URL(window.location.href); url.search = query // 手动的挂载url查询参数 window.history.replaceState(null, '', url.href);}// 跳转到子应用的页面const toJumpSubApp = (appName, query) => { this.$router.push({path: appName}, query)}export default { toJumpMasterApp, toJumpSubApp }

// setupApp.jsimport lifecycles from './lifecycles';import subCommon from './subCommon';const setUpApp = Vue => { .... hostMap().forEach(v => { setupApp({ ...v, ...lifecycles, props: subCommon }) preloadApp(v.name) })}
复制代码

主应用加载子应用页面

// 子应用页面加载// app1.vue<template>    <WujieVue        :key="update"        width="100%"        height="100%"        :name="name"        :url="appUrl"        :sync="subVueApp1Info.sync"         :alive="subVueApp1Info.alive"         :props="{ data: dataProps ,method:{propsMethod}}"    ></WujieVue></template>
<script>import wujieVue from "wujie-vue2";import {getSubMap} from '../../hostMap';const name = 'subVueApp1'export default { data() { return { dataProps: [], subVueApp1Info: getSubMap(name) } }, computed: { appUrl() { // return getSubMap('subVueApp1').url return this.subVueApp1Info.url + this.$route.params.path } }, watch: { // 如果子应用是保活模式,可以采用通信的方式告知路由变化 "$route.params.path": { handler: function () { wujieVue.bus.$emit("vue-router-change", `/${this.$route.params.path}`); }, immediate: true, }, }, methods: { propsMethod() {} }}</script>
复制代码

四、子应用配置

无界的插件体系主要是方便用户在运行时去修改子应用代码从而避免去改动仓库代码



// plugins.jsconst plugins = {  'subVueApp1': [{    htmlLoader:code => {      return code;    },    cssAfterLoaders: [      // 在加载html所有样式之后添加一个外联样式      { src:'https://xxx/xxx.css' },      // 在加载html所有样式之后添加一个内联样式      { content:'img{height: 300px}' }    ],    jsAfterLoaders: [      { src:'http://xxx/xxx.js' },      // 插入一个内联脚本本      { content:`          window.$wujie.bus.$on('routeChange', path => {          console.log(path, window, self, global, location)          })`      },      // 执行一个回调      {        callback(appWindow) {          console.log(appWindow.__WUJIE.id);        }      }    ]  }],  'subVueApp2': [{    htmlLoader: code=> {      return code;    }  }]};export default plugins;
复制代码


// setupApp.jsimport plugins from './plugins';const setUpApp = Vue => {    ......    hostMap().forEach(v => {        setupApp({            ...v,            plugins: plugins[element.name]        })        ......    })}
复制代码

五、数据传输和消息通信

数据交互方式

1,通过 props 进行传


2、通过 window 进线传达


3,通过事件 bus 进行传达

props

主应用通过 data 传参给子应用, 子应用通过 methods 方法传参给主应用


// 主应用<WujieVue name="xxx" url="xxx" :props="{ data: xxx, methods: xxx }"></WujieVue>
// 子应用const props = window.$wujie?.props; // {data: xxx, methods: xxx}
复制代码

window

利用子应用运行在主应用的 iframe


类似 iframe 的传参和调用


// 主应用获取子应用的全局变量数据window.document.querySelector("iframe[name=子应用id]").contentWindow.xxx;
//子应用获取主应用的全局变量数据window.parent.xxx;
复制代码

eventBus

去中心化的通信方案,方便。类似于组件间的通信


主应用


// 使用 wujie-vueimport WujieVue from"wujie-vue";const{ bus }= WujieVue;
// 主应用监听事件bus.$on("事件名字",function(arg1,arg2, ...){});// 主应用发送事件bus.$emit("事件名字", arg1, arg2,...);// 主应用取消事件监听bus.$off("事件名字",function(arg1,arg2, ...){});
复制代码


子应用


// 子应用监听事件window.$wujie?.bus.$on("事件名字",function(arg1,arg2, ...){});// 子应用发送事件window.$wujie?.bus.$emit("事件名字", arg1, arg2,...);// 子应用取消事件监听window.$wujie?.bus.$off("事件名字",function(arg1,arg2, ...){});
复制代码

规范主子应用传递规则

规则:子应用名+事件名


主应用向子应用传参


// 主应用传参bus.$emit('matser', options) // 主应用向所有子应用传参bus.$emit('vite:getOptions', options) // 主应用向指定子应用传参
//子应用监听主应用事件window?.$wujie?.bus.$on("master", (options) => { console.log(options)});//子应用监听主应用特定通知子应用事件window?.$wujie?.bus.$on("vite:getOptions", (options) => { console.log(options)});
复制代码

六、路由

以 vue 主应用为例,子应用 A 的 name 为 A, 主应用 A 页面的路径为/pathA,子应用 B 的 name 为 B,主应用 B 页面的路径为/pathB 为例


主应用统一 props 传入跳转函数


jump (location) {  this.$router.push(location);}
复制代码

1、主应用 history 路由

子应用 B 为非保活应用

1、子应用 A 只能跳转到子应用 B 的主应用的默认路由


function handleJump(){   window.$wujie?.props.jump({ path:"/pathB"});}
复制代码


2、子应用 A 只能跳转到子应用 B 应用的指定路由(非默认路由)


// 子应用A点击跳转处理函数, 子应用B需开启路由同步function handleJump(){    window.$wujie?.props.jump({ path:"/pathB", query:{ B:"/test"}});}
复制代码

子应用 B 为保活应用

子应用 A 只能跳转到子应用 B 的主应用的路由


可写入主应用的插件中,主应用插件根据不同的应用,引入不同方法


// 子应用 A 点击跳转处理函数function handleJump() {  window.$wujie?.bus.$emit("routeChange", "/test");}
// 子应用 B 监听并跳转window.$wujie?.bus.$on("routeChange", (path) => this.$router.push({ path }));
复制代码

2、主应用 hash 路由

子应用 B 为非保活应用

1、子应用 A 只能跳转到子应用 B 的主应用的默认路由


同子应用 B 为非保活应用,子应用 A 跳转到子应用 B 的主应用的默认路由


2、子应用 A 只能跳转到子应用 B 应用的指定路由(非默认路由)


主应用jump(location,query){     // 跳转到主应用B页面    this.$router.push(location);     const url=new URL(window.location.href);    url.search=query    // 手动的挂载url查询参数    window.history.replaceState(null,"",url.href);}
// 子应用 B 开启路由同步能力

// 子应用Afunction handleJump() { window.$wujie?.props.jump({ path: "/pathB" } , `?B=${window.encodeURIComponent("/test")}`});}
复制代码

子应用 B 为保活应用

同子应用 B 为保活应用,子应用 A 跳转到子应用 B 路由


// bus.js// 在 xxx-sub 路由下子应用将激活路由同步给主应用,主应用跳转对应路由高亮菜单栏  bus.$on('sub-route-change', (name, path) => {      const mainName = `${name}-sub`;      const mainPath = `/${name}-sub${path}`;      const currentName = router.currentRoute.name;      const currentPath = router.currentRoute.path;    if (mainName === currentName && mainPath !== currentPath) {        router.push({ path: mainPath });      }  });
复制代码

七、部署

前端单页面的部署,不管怎么自动化,工具怎么变. 都是把打包好的静态文件,放到服务器的正确位置下。所以支持项目的独立部署和混合部署。


作者:京东物流 张燕燕 刘海鼎

内容来源:京东云开发者社区

发布于: 刚刚阅读数: 3
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
前端微服务无界实践 | 京东云技术团队_微服务_京东科技开发者_InfoQ写作社区