关于微服务:前端微服务无界实践-京东云技术团队

35次阅读

共计 8739 个字符,预计需要花费 22 分钟才能阅读完成。

一、前言

随着我的项目的倒退,前端 SPA 利用的规模一直加大、业务代码耦合、编译慢,导致日常的保护难度日益减少。同时前端技术的倒退迅猛,导致性能扩大吃力,重构老本高,稳定性低。因而前端微服务应运而生。

前端微服务劣势

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

2. 独立部署: 模块部署, 缩小模块影响范畴,单个模块产生谬误, 不影响全局,晋升我的项目稳定性。

3. 技术选型灵便: 在同一我的项目下能够应用市面上所有前端技术栈, 也包含将来的前端技术栈。

4. 扩展性,晋升业务动静扩大的可能,防止资源节约

微前端服务构造

技术比照和选型:

选型 动态资源预加载 子利用保活 iframe js 沙箱 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,先装置对应的插件哦

主利用革新:

// 引入无界,依据框架不同版本不同,引入不同的版本
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.js
import 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.js
import 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.js
import Vue from 'vue'
import setUpApp from'@/microConfig/setupApp'
setUpApp(Vue)

配置公共函数

全子利用共享的生命周期函数,可用于执行多个子利用间雷同的逻辑操作函数独特解决

// lifecycle.js
const 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.js
import 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.js
const 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.js
import 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-vue
import 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 开启路由同步能力


// 子利用 A
function 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});
      }
  });

七、部署

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

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

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

正文完
 0