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

一、前言

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

前端微服务优势

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

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

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

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

微前端服务结构

技术对比和选型:

选型静态资源预加载子应用保活iframejs沙箱css沙箱接入成本地址
EMP×××https://github.com/efoxTeam/emp
Qiankun××中低https://qiankun.umijs.org/zh/
无界中低https://wujie-micro.github.io/doc/
micro-app××中低https://zeroing.jd.com/micro-app/

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

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

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

主应用改造:

1// 引入无界,根据框架不同版本不同,引入不同的版本 2import { setupApp, bus, preloadApp, startApp } from 'wujie-vue2' 3 4// 设置子应用默认参数 5setupApp({ 6 name: '子应用id(唯一值)', 7 url: "子应用地址", 8 exec: true, 9 el: "容器", 10 sync: true 11}) 12 13// 预加载 14preloadApp({ name: "唯一id"}); 15 16// 启动子应用 17startApp({ name: "唯一id"});

子应用改造:

1、跨域

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

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

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

1// 本地配置 2server: { 3 host: '127.0.0.1', // 本地启动如果主子应用没处在同一个ip下,也存在跨域的问题,需要配置 4 headers: { 5 'Access-Control-Allow-Credentials': true, 6 'Access-Control-Allow-Origin': '*', // 如资源没有携带 cookie,需设置此属性 7 'Access-Control-Allow-Headers': 'X-Requested-With,Content-Type', 8 'Access-Control-Allow-Methods': '*' 9 } 10} 11 12// nginx 配置 13add_header Access-Control-Allow-Credentials true; 14add_header Access-Control-Allow-Origin "*"; 15add_header Access-Control-Allow-Headers 'X-Requested-With,Content-Type'; 16add_header Access-Control-Allow-Methods "*";

2、运行模式选择

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

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

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

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

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

(2)、单例模式

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

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

改造生命周期

1// window.__POWERED_BY_WUJIE__用来判断子应用是否在无界的环境中 2if (window.__POWERED_BY_WUJIE__) { 3 let instance; 4 // 将子应用的实例和路由进线创建和挂载 5 window.__WUJIE_MOUNT = () => { 6 const router = new VueRouter({ routes }); 7 instance = new Vue({ router, render: (h) => h(App) }).$mount("#app"); 8 }; 9 // 实例销毁 10 window.__WUJIE_UNMOUNT = () => { 11 instance.$destroy(); 12 }; 13} else { 14 // 子应用单独启动 15 new Vue({ router: new VueRouter({ routes }), render: (h) => h(App) }).$mount("#app"); 16} 17

(3)、重建模式

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

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

无生命周期改造

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

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

子应用基础信息管理

1// subList.js 数据可在配置页面动态配置 2const subList = [ 3 { 4 "name":"subVueApp1", 5 "exec":true,// false只会预加载子应用的资源,true时预执行子应用代码 6 "alive": true, 7 "show":true,// 是否引入 8 "url":{ 9 "pre":"http://xxx1-pre.com", 10 "gray":"http://xxx1-gray.com", 11 "prod":"http://xxx1.com" 12 } 13 }, 14 { 15 "name":"subVueApp2", 16 "exec":false,// false只会预加载子应用的资源,true时预执行子应用代码 17 "alive": false, 18 "show":true,// 是否引入 19 "url":{ 20 "pre":"http://xxx2-pre.com", 21 "gray":"http://xxx2-gray.com", 22 "prod":"http://xxx2.com" 23 } 24 } 25] 26export default subList;
<!---->
1// hostMap.js 2import subList from './subList' 3 4 const env = process.env.mode || 'pre' 5 6// 子应用map结构 7const subMap = {} 8const subArr = [] 9 10// 转换子应用 11export const hostMap = () => { 12 subList.forEach(v => { 13 const {url, ...other} = v 14 const info = { 15 ...other, 16 url: url[env] 17 } 18 subMap[v.name] = info 19 subArr.push(info) 20 }) 21 return subArr 22} 23 24// 获取子应用配置信息 25export const getSubMap = name => { 26 return subMap[name].show ? subMap[name] : {} 27}

子应用注册预加载和启动

1// setupApp.js 2import WujieVue from 'wujie-vue2'; 3import {hostMap} from './hostMap'; 4 5const { setupApp, preloadApp } = WujieVue 6 7const setUpApp = Vue => { 8 Vue.use(WujieVue) 9 hostMap().forEach(v => { 10 setupApp(v) 11 preloadApp(v.name) 12 }) 13} 14export default setUpApp; 15 16 17// main.js 18import Vue from 'vue' 19import setUpApp from'@/microConfig/setupApp' 20setUpApp(Vue)

配置公共函数

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

1// lifecycle.js 2const lifecycles = { 3 beforeLoad: (appWindow) => console.log(`${appWindow.__WUJIE.id} beforeLoad 生命周期`), 4 beforeMount: (appWindow) => console.log(`${appWindow.__WUJIE.id} beforeMount 生命周期`), 5 afterMount: (appWindow) => console.log(`${appWindow.__WUJIE.id} afterMount 生命周期`), 6 beforeUnmount: (appWindow) => console.log(`${appWindow.__WUJIE.id} beforeUnmount 生命周期`), 7 afterUnmount: (appWindow) => console.log(`${appWindow.__WUJIE.id} afterUnmount 生命周期`), 8 activated: (appWindow) => console.log(`${appWindow.__WUJIE.id} activated 生命周期`), 9 deactivated: (appWindow) => console.log(`${appWindow.__WUJIE.id} deactivated 生命周期`), 10 loadError: (url, e) => console.log(`${url} 加载失败`, e), 11}; 12 13export default lifecycles; 14 15 16// subCommon.js 17// 跳转到主应用指定页面 18const toJumpMasterApp = (location, query) => { 19 20 this.$router.replace(location, query); 21 const url = new URL(window.location.href); 22 url.search = query 23 // 手动的挂载url查询参数 24 window.history.replaceState(null, '', url.href); 25} 26// 跳转到子应用的页面 27const toJumpSubApp = (appName, query) => { 28 this.$router.push({path: appName}, query) 29} 30export default { 31 toJumpMasterApp, 32 toJumpSubApp 33} 34 35 36// setupApp.js 37import lifecycles from './lifecycles'; 38import subCommon from './subCommon'; 39const setUpApp = Vue => { 40 .... 41 hostMap().forEach(v => { 42 setupApp({ 43 ...v, 44 ...lifecycles, 45 props: subCommon 46 }) 47 preloadApp(v.name) 48 }) 49}

主应用加载子应用页面

1// 子应用页面加载 2// app1.vue 3<template> 4 <WujieVue 5 :key="update" 6 width="100%" 7 height="100%" 8 :name="name" 9 :url="appUrl" 10 :sync="subVueApp1Info.sync" 11 :alive="subVueApp1Info.alive" 12 :props="{ data: dataProps ,method:{propsMethod}}" 13 ></WujieVue> 14</template> 15 16<script> 17import wujieVue from "wujie-vue2"; 18import {getSubMap} from '../../hostMap'; 19const name = 'subVueApp1' 20export default { 21 data() { 22 return { 23 dataProps: [], 24 subVueApp1Info: getSubMap(name) 25 } 26 }, 27 computed: { 28 appUrl() { 29 // return getSubMap('subVueApp1').url 30 return this.subVueApp1Info.url + this.$route.params.path 31 } 32 }, 33 watch: { 34 // 如果子应用是保活模式,可以采用通信的方式告知路由变化 35 "$route.params.path": { 36 handler: function () { 37 wujieVue.bus.$emit("vue-router-change", `/${this.$route.params.path}`); 38 }, 39 immediate: true, 40 }, 41 }, 42 methods: { 43 propsMethod() {} 44 } 45} 46</script>

四、子应用配置

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

1// plugins.js 2const plugins = { 3 'subVueApp1': [{ 4 htmlLoader:code => { 5 return code; 6 }, 7 cssAfterLoaders: [ 8 // 在加载html所有样式之后添加一个外联样式 9 { src:'https://xxx/xxx.css' }, 10 // 在加载html所有样式之后添加一个内联样式 11 { content:'img{height: 300px}' } 12 ], 13 jsAfterLoaders: [ 14 { src:'http://xxx/xxx.js' }, 15 // 插入一个内联脚本本 16 { content:` 17 window.$wujie.bus.$on('routeChange', path => { 18 console.log(path, window, self, global, location) 19 })` 20 }, 21 // 执行一个回调 22 { 23 callback(appWindow) { 24 console.log(appWindow.__WUJIE.id); 25 } 26 } 27 ] 28 }], 29 'subVueApp2': [{ 30 htmlLoader: code=> { 31 return code; 32 } 33 }] 34}; 35export default plugins;
<!---->
1// setupApp.js 2import plugins from './plugins'; 3const setUpApp = Vue => { 4 ...... 5 hostMap().forEach(v => { 6 setupApp({ 7 ...v, 8 plugins: plugins[element.name] 9 }) 10 ...... 11 }) 12}

五、数据传输和消息通信

数据交互方式

1,通过props进行传

2、通过window进线传达

3,通过事件bus进行传达

props

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

1// 主应用 2<WujieVue name="xxx" url="xxx" :props="{ data: xxx, methods: xxx }"></WujieVue> 3 4// 子应用 5const props = window.$wujie?.props; // {data: xxx, methods: xxx}

window

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

类似iframe的传参和调用

1// 主应用获取子应用的全局变量数据 2window.document.querySelector("iframe[name=子应用id]").contentWindow.xxx; 3 4//子应用获取主应用的全局变量数据 5window.parent.xxx;

eventBus

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

主应用

1// 使用 wujie-vue 2import WujieVue from"wujie-vue"; 3const{ bus }= WujieVue; 4 5// 主应用监听事件 6bus.$on("事件名字",function(arg1,arg2, ...){}); 7// 主应用发送事件 8bus.$emit("事件名字", arg1, arg2,...); 9// 主应用取消事件监听 10bus.$off("事件名字",function(arg1,arg2, ...){});

子应用

1// 子应用监听事件 2window.$wujie?.bus.$on("事件名字",function(arg1,arg2, ...){}); 3// 子应用发送事件 4window.$wujie?.bus.$emit("事件名字", arg1, arg2,...); 5// 子应用取消事件监听 6window.$wujie?.bus.$off("事件名字",function(arg1,arg2, ...){});

规范主子应用传递规则

规则:子应用名+事件名

主应用向子应用传参

1// 主应用传参 2bus.$emit('matser', options) // 主应用向所有子应用传参 3bus.$emit('vite:getOptions', options) // 主应用向指定子应用传参 4 5//子应用监听主应用事件 6window?.$wujie?.bus.$on("master", (options) => { 7 console.log(options) 8}); 9//子应用监听主应用特定通知子应用事件 10window?.$wujie?.bus.$on("vite:getOptions", (options) => { 11 console.log(options) 12});

六、路由

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

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

1jump (location) { 2 this.$router.push(location); 3}

1、主应用history路由

子应用 B 为非保活应用

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

1function handleJump(){ 2 window.$wujie?.props.jump({ path:"/pathB"}); 3}

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

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

子应用 B 为保活应用

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

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

1// 子应用 A 点击跳转处理函数 2function handleJump() { 3 window.$wujie?.bus.$emit("routeChange", "/test"); 4} 5 6// 子应用 B 监听并跳转 7window.$wujie?.bus.$on("routeChange", (path) => this.$router.push({ path }));

2、主应用hash路由

子应用 B 为非保活应用

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

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

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

1主应用 2jump(location,query){ 3 // 跳转到主应用B页面 4 this.$router.push(location); 5 const url=new URL(window.location.href); 6 url.search=query 7 // 手动的挂载url查询参数 8 window.history.replaceState(null,"",url.href); 9} 10 11// 子应用 B 开启路由同步能力 12 13 14// 子应用A 15function handleJump() { 16 window.$wujie?.props.jump({ path: "/pathB" } , `?B=${window.encodeURIComponent("/test")}`}); 17}

子应用 B 为保活应用

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

1// bus.js 2// 在 xxx-sub 路由下子应用将激活路由同步给主应用,主应用跳转对应路由高亮菜单栏 3 bus.$on('sub-route-change', (name, path) => { 4 const mainName = `${name}-sub`; 5 const mainPath = `/${name}-sub${path}`; 6 const currentName = router.currentRoute.name; 7 const currentPath = router.currentRoute.path; 8 if (mainName === currentName && mainPath !== currentPath) { 9 router.push({ path: mainPath }); 10 } 11 });

七、部署

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

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

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

点赞
收藏

评论区

加载中...

相关推荐

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

AssemblyScript 入门指南[每日前端夜话0xEB]

每日前端夜话0xEB每日前端夜话,陪你聊前端。每天晚上18:00准时推送。正文共:2459 字预计阅读时间:10分钟作者:DannyGuo翻译:疯狂的技术宅来源:logrocket!(https://oscimg.oschina.net/oscnet/b880277c594152a503

Node.js 12中的ES模块[每日前端夜话0x9E]

每日前端夜话0x9E每日前端夜话,陪你聊前端。每天晚上18:00准时推送。正文共:2552字预计阅读时间:10 分钟作者:BrianDeSousa翻译:疯狂的技术宅来源:logrocket!(https://oscimg.oschina.net/oscnet/2ccaf94cecd3

【京东开源项目】微前端框架MicroApp 1.0正式发布

MicroApp是由京东前端团队推出的一款微前端框架,它从组件化的思维,基于类WebComponent进行微前端的渲染,旨在降低上手难度、提升工作效率。MicroApp无关技术栈,也不和业务绑定,可以用于任何前端框架。

微前端无界机制浅析 | 京东物流技术团队

简介随着项目的发展,前端SPA应用的规模不断加大、业务代码耦合、编译慢,导致日常的维护难度日益增加。同时前端技术的发展迅猛,导致功能扩展吃力,重构成本高,稳定性低。为了能够将前端模块解耦,通过相关技术调研,最终选择了无界微前端框架作为物流客服系统解耦支持。

微前端父子应用及兄弟应用间组件或方法共享方案

作者:京东物流刘微微背景我们的很多web应用在持续迭代中功能越来越复杂,参与的人员、团队不断增多,导致项目出现难以维护的问题,这种情况PC端尤其常见,微前端为我们提供了一种高效管理复杂应用的方案。但是在使用微前端的过程中,通常会有一些公共方法或公共组件,本