Unbounded practice of front-end microservices | JD Cloud technical team

I. Introduction

With the development of the project, the scale of the front-end SPA application continues to increase, the business code is coupled, and the compilation is slow, resulting in increasing difficulty in daily maintenance. At the same time, the rapid development of front-end technology has led to difficult function expansion, high reconstruction costs, and low stability. Therefore, front-end microservices came into being.

Advantages of front-end microservices

1. Controllable complexity: Business modules are decoupled to avoid excessive code size, keep complexity low, and facilitate maintenance and development efficiency.

2. Independent deployment: module deployment, reducing the scope of module influence, errors in a single module will not affect the overall situation, and improve project stability.

3. Flexible technology selection: All front-end technology stacks on the market can be used under the same project, including future front-end technology stacks.

4. Scalability, improving the possibility of dynamic business expansion and avoiding waste of resources

Micro frontend service structure

Technical comparison and selection:

selection Static resource preloading Child application maintenance iframe js sandbox css sandbox access cost address
EMP × × × Low https://github.com/efoxTeam/emp
Qiankun × × mid Lo https://qiankun.umijs.org/zh/
unbounded mid Lo https://wujie-micro.github.io/doc/
micro-app × × mid Lo https://zeroing.jd.com/micro-app/

By comparing the support of various technologies for the project and the cost of project access, we finally have no boundaries in the selection.

2. Simple usage of wujie (take the main application using the vue framework as an example)

The main application is the vue framework, which can directly use wujie-vue, and the react framework can directly use wujie-react, first install the corresponding plug-in

Main app retrofit:

// 引入无界,根据框架不同版本不同,引入不同的版本
import { setupApp, bus, preloadApp, startApp } from 'wujie-vue2'

// 设置子应用默认参数
setupApp({
    name: '子应用id(唯一值)',
    url: "子应用地址",
    exec: true,
    el: "容器",
    sync: true
})

// 预加载
preloadApp({ name: "唯一id"});

// 启动子应用
startApp({ name: "唯一id"});

Sub-application transformation:

1. Cross domain

If the sub-application supports cross-domain, there is no need to modify it

Reason: There is a cross-domain request for sub-application resources

Solution: Since the front-end application is basically separated from the front-end and the front-end, proxy proxy is used. Just configure the sub-application configuration to allow cross-domain

// 本地配置
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. Operation mode selection

Unbounded has three operating modes: singleton mode, keep-alive mode, reconstruction mode

(1), keep-alive mode (long-term storage page)

Interpretation: Similar to the keep-alive nature of Vue (sub-application instances and webcomponents are not destroyed, state and routing are not lost, only hot-plugging of hot webcomponents is done), sub-applications do not want to do life cycle modification, and sub-application switching does not want to have During the white screen time, you can use the keep-alive mode. There are multiple entries in the main application to jump to different pages of the sub-application, and the keep-alive mode cannot be used because the routing of the sub-application cannot be changed.

Configuration: You only need to add alive: true to the configuration parameter when the main application loads the sub-application

Effect: preloading + keep-alive mode = page data request and rendering are completed in advance, achieving instant opening effect

(2), singleton mode

Interpretation: When the sub-application page is switched away, window.__WUJIE_UNMOUNT will be called to destroy the current instance of the sub-application. If the sub-application page is switched back, it will call window.__WUJIE_MOUNT to render a new sub-application instance of the sub-application. The process is equivalent to: destroy the current application instance => synchronize the new route => create a new application instance

Configuration: You only need to add alive: false to the configuration parameter when the main application loads the sub-application

Retrofit life cycle

// 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), reconstruction mode

Interpretation: Every page switching destroys the iframe of the sub-application webcomponent+js.

Configuration: You only need to add alive: false to the configuration parameter when the main application loads the sub-application

No life cycle modification

Remarks: For old projects that are not packaged by webpack, a white screen may appear when switching between sub-applications. You should use the keep-alive mode as much as possible to reduce the white screen time

3. Load the module (main application configuration)

Sub-application basic information management

// 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] : {}
}

Sub-app registration preload and launch

// 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)

Configure public functions

The life cycle function shared by all sub-applications can be used to perform common processing of the same logical operation function among multiple sub-applications

// 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)
    })
}

The main application loads the sub-application page

// 子应用页面加载
// 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>

4. Sub-application configuration

The unbounded plug-in system is mainly to facilitate users to modify the sub-application code at runtime so as to avoid changing the warehouse code

// 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]
        })
        ......
    })
}

5. Data transmission and message communication

data interaction

1. Pass through props

2. Through the window incoming line to communicate

3. Communicate through the event bus

props

The main application passes parameters to the sub-application through data, and the sub-application passes parameters to the main application through the methods method

// 主应用
<WujieVue name="xxx" url="xxx" :props="{ data: xxx, methods: xxx }"></WujieVue>

// 子应用
const props = window.$wujie?.props; // {data: xxx, methods: xxx}

window

Utilize a sub-app running in an iframe of the main app

Parameter passing and calling similar to iframe

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

//子应用获取主应用的全局变量数据
window.parent.xxx;

eventBus

Decentralized communication solution, convenient. Similar to communication between components

main application

// 使用 wujie-vue
import WujieVue from"wujie-vue";
const{ bus }= WujieVue;

// 主应用监听事件
bus.$on("事件名字",function(arg1,arg2, ...){});
// 主应用发送事件
bus.$emit("事件名字", arg1, arg2,...);
// 主应用取消事件监听
bus.$off("事件名字",function(arg1,arg2, ...){});

sub application

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

Standardize master-sub-application transfer rules

Rules: sub-application name + event name

The main application passes parameters to the sub-application

// 主应用传参
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)
});

6. Routing

Take the Vue main application as an example, the name of sub-application A is A, the path of the main application A page is /pathA, the name of sub-application B is B, and the path of the main application B page is /pathB

The main application uses unified props to pass in the jump function

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

1. Main application history routing

Sub-application B is a non-keep-alive application

1. Sub-application A can only jump to the default route of the main application of sub-application B

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

2. Sub-application A can only jump to the specified route of sub-application B (non-default route)

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

Sub-application B is a keep-alive application

Sub-application A can only jump to the route of the main application of sub-application B

It can be written into the plug-in of the main application, and the main application plug-in introduces different methods according to different applications

// 子应用 A 点击跳转处理函数
function handleJump() {
  window.$wujie?.bus.$emit("routeChange", "/test");
}

// 子应用 B 监听并跳转
window.$wujie?.bus.$on("routeChange", (path) => this.$router.push({ path }));

2. Main application hash routing

Sub-application B is a non-keep-alive application

1. Sub-application A can only jump to the default route of the main application of sub-application B

With sub-application B as a non-keep-alive application, sub-application A jumps to the default route of the main application of sub-application B

2. Sub-application A can only jump to the specified route of sub-application B (non-default route)

主应用
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")}`});
}

Sub-application B is a keep-alive application

The same sub-application B is a keep-alive application, and sub-application A jumps to sub-application B routing

// 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 });
      }
  });

7. Deployment

The deployment of the front-end single page, no matter how automated or how the tools change, is to put the packaged static files in the correct location of the server. Therefore, independent deployment and mixed deployment of projects are supported.

Author: JD Logistics Zhang Yanyan Liu Haiding

Content source: JD Cloud developer community

{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/4090830/blog/9008248