随着项目的发展,前端SPA应用的规模不断加大、业务代码耦合、编译慢,导致日常的维护难度日益增加。同时前端技术的发展迅猛,导致功能扩展吃力,重构成本高,稳定性低。因此前端微服务应运而生。
1.复杂度可控: 业务模块解耦,避免代码过大,保持较低的复杂度,便于维护与开发效率。
2.独立部署: 模块部署,减少模块影响范围,单个模块发生错误,不影响全局,提升项目稳定性。
3.技术选型灵活: 在同一项目下可以使用市面上所有前端技术栈,也包括未来的前端技术栈。
4.扩展性,提升业务动态扩展的可能,避免资源浪费

通过对比多种技术对项目的支持情况和项目接入的成本,我们最终选型无界。
主应用是vue框架可直接使用wujie-vue,react框架可直接使用wujie-react,先安装对应的插件哦
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // 引入无界,根据框架不同版本不同,引入不同的版本import { setupApp, bus, preloadApp, startApp } from 'wujie-vue2'// 设置子应用默认参数setupApp({ name: '子应用id(唯一值)', url: "子应用地址", exec: true, el: "容器", sync: true})// 预加载preloadApp({ name: "唯一id"});// 启动子应用startApp({ name: "唯一id"}); |
子应用如果支持跨域,则不用修改
原因:存在请求子应用资源跨域
方案:因前端应用基本是前后端分离,使用proxy代理。只需配置在子应用配置允许跨域即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 本地配置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 "*"; |
无界有三种运行模式:单例模式、保活模式、重建模式

(1)、保活模式(长存页面)
释义:类似于vue的keep-alive性质(子应用实例和webcomponent不销毁,状态、路由都不丢失,只做热webcomponent的热插拔),子应用不想做生命周期改造,子应用切换又不想有白屏时间,可以采用保活模式。主应用上有多个入口跳转到子应用的不同页面,不能采用保活模式,因为无法改变子应用路由。
配置:只需要在主应用加载子应用的时候,配置参数添加alive:true
效果:预加载+保活模式=页面数据请求和渲染提前完成,实现瞬间打开效果
(2)、单例模式
释义:子应用页面切走,会调用window.__WUJIE_UNMOUNT销毁子应用当前实例。子应用页面如果切换回来,会调用window.__WUJIE_MOUNT渲染子应用新的子应用实例。过程相当于:销毁当前应用实例 => 同步新路由 => 创建新应用实例
配置:只需要在主应用加载子应用的时候,配置参数添加alive:false
改造生命周期
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 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打包的老项目,子应用切换可能出现白屏,应尽可能使用保活模式降低白屏时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // subList.js 数据可在配置页面动态配置const subList = [ { "name":"subVueApp1", "exec":true,// false只会预加载子应用的资源,true时预执行子应用代码 "alive": true, "show":true,// 是否引入 "url":{ "pre":"ae1K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4S2^5P5o6q4Q4x3X3c8H3M7X3g2Q4x3X3g2U0L8$3@1`.", "gray":"aaaK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4S2^5P5o6q4Q4x3X3c8Y4M7X3q4&6i4K6u0W2j5$3!0E0", "prod":"a9dK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4S2^5P5o6q4Q4x3X3g2U0L8$3@1`." } }, { "name":"subVueApp2", "exec":false,// false只会预加载子应用的资源,true时预执行子应用代码 "alive": false, "show":true,// 是否引入 "url":{ "pre":"e4cK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4S2^5P5o6u0Q4x3X3c8H3M7X3g2Q4x3X3g2U0L8$3@1`.", "gray":"5bcK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4S2^5P5o6u0Q4x3X3c8Y4M7X3q4&6i4K6u0W2j5$3!0E0", "prod":"aeeK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4S2^5P5o6u0Q4x3X3g2U0L8$3@1`." } }]export default subList; |
<!---->
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // 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] : {}} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // 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) |
全子应用共享的生命周期函数,可用于执行多个子应用间相同的逻辑操作函数共同处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | // 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) })} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | // 子应用页面加载// 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> |
无界的插件体系主要是方便用户在运行时去修改子应用代码从而避免去改动仓库代码

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | // plugins.jsconst plugins = { 'subVueApp1': [{ htmlLoader:code => { return code; }, cssAfterLoaders: [ // 在加载html所有样式之后添加一个外联样式 { src:'0c0K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6^5P5s2S2Q4x3V1k6^5P5s2S2Q4x3X3g2U0M7%4y4Q4x3U0M7`. }, // 在加载html所有样式之后添加一个内联样式 { content:'img{height: 300px}' } ], jsAfterLoaders: [ { src:'6abK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4S2^5P5q4)9J5c8Y4S2^5P5q4)9J5k6h3A6K6i4K6t1%4 }, // 插入一个内联脚本本 { 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; |
<!---->
1 2 3 4 5 6 7 8 9 10 11 12 | // setupApp.jsimport plugins from './plugins';const setUpApp = Vue => { ...... hostMap().forEach(v => { setupApp({ ...v, plugins: plugins[element.name] }) ...... })} |
1,通过props进行传
2、通过window进线传达
3,通过事件bus进行传达
主应用通过data传参给子应用, 子应用通过methods方法传参给主应用
1 2 3 4 5 | // 主应用<WujieVue name="xxx" url="xxx" :props="{ data: xxx, methods: xxx }"></WujieVue>// 子应用const props = window.$wujie?.props; // {data: xxx, methods: xxx} |
利用子应用运行在主应用的iframe
类似iframe的传参和调用
1 2 3 4 5 | // 主应用获取子应用的全局变量数据window.document.querySelector("iframe[name=子应用id]").contentWindow.xxx;//子应用获取主应用的全局变量数据window.parent.xxx; |
去中心化的通信方案,方便。类似于组件间的通信
主应用
1 2 3 4 5 6 7 8 9 10 | // 使用 wujie-vueimport WujieVue from"wujie-vue";const{ bus }= WujieVue;// 主应用监听事件bus.$on("事件名字",function(arg1,arg2, ...){});// 主应用发送事件bus.$emit("事件名字", arg1, arg2,...);// 主应用取消事件监听bus.$off("事件名字",function(arg1,arg2, ...){}); |
子应用
1 2 3 4 5 6 | // 子应用监听事件window.$wujie?.bus.$on("事件名字",function(arg1,arg2, ...){});// 子应用发送事件window.$wujie?.bus.$emit("事件名字", arg1, arg2,...);// 子应用取消事件监听window.$wujie?.bus.$off("事件名字",function(arg1,arg2, ...){}); |
规则:子应用名+事件名
主应用向子应用传参
1 2 3 4 5 6 7 8 9 10 11 12 | // 主应用传参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传入跳转函数
1 2 3 | jump (location) { this.$router.push(location);} |
1、子应用A 只能跳转到子应用 B 的主应用的默认路由
1 2 3 | function handleJump(){ window.$wujie?.props.jump({ path:"/pathB"});} |
2、子应用A 只能跳转到子应用B 应用的指定路由(非默认路由)
1 2 3 4 | // 子应用A点击跳转处理函数, 子应用B需开启路由同步function handleJump(){ window.$wujie?.props.jump({ path:"/pathB", query:{ B:"/test"}});} |
子应用A 只能跳转到子应用 B 的主应用的路由
可写入主应用的插件中,主应用插件根据不同的应用,引入不同方法
1 2 3 4 5 6 7 | // 子应用 A 点击跳转处理函数function handleJump() { window.$wujie?.bus.$emit("routeChange", "/test");}// 子应用 B 监听并跳转window.$wujie?.bus.$on("routeChange", (path) => this.$router.push({ path })); |
1、子应用A 只能跳转到子应用 B 的主应用的默认路由
同子应用B为非保活应用,子应用A跳转到子应用 B 的主应用的默认路由
2、子应用A 只能跳转到子应用B 应用的指定路由(非默认路由)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 主应用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为保活应用,子应用A跳转到子应用 B 路由
1 2 3 4 5 6 7 8 9 10 11 | // 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 }); } }); |
前端单页面的部署,不管怎么自动化,工具怎么变. 都是把打包好的静态文件,放到服务器的正确位置下。所以支持项目的独立部署和混合部署。
作者:京东物流 张燕燕 刘海鼎
内容来源:京东云开发者社区