フロントエンド マイクロサービスの無制限の実践 | JD Cloud テクニカル チーム

I.はじめに

プロジェクトの発展に伴い、フロントエンド SPA アプリケーションの規模は増大し続け、業務コードが結合し、コンパイルが遅くなり、日常のメンテナンスが困難になってきました。同時に、フロントエンド技術の急速な発展により、機能拡張が困難になり、再構築コストが高く、安定性が低くなりました。そこで、フロントエンド マイクロサービスが登場しました。

フロントエンドマイクロサービスの利点

1. 制御可能な複雑さ: ビジネス モジュールは、過剰なコード サイズを回避し、複雑さを低く抑え、メンテナンスと開発の効率を促進するために分離されています。

2. 独立した展開: モジュールの展開により、モジュールの影響範囲が減り、単一モジュールのエラーが全体の状況に影響を与えなくなり、プロジェクトの安定性が向上します。

3. 柔軟なテクノロジーの選択: 将来のフロントエンド テクノロジー スタックも含め、市場のすべてのフロントエンド テクノロジー スタックを同じプロジェクトで使用できます。

4. スケーラビリティ、ダイナミックなビジネス拡大の可能性を向上させ、リソースの無駄を回避

マイクロフロントエンドサービスの仕組み

技術的な比較と選択:

選択 静的リソースのプリロード 子应用保活 iframe jsサンドボックス CSSサンドボックス アクセスコスト 住所
EMP × × × 低い https://github.com/efoxTeam/emp
乾君 × × ミッドロー https://qiankun.umijs.org/zh/
無限の ミッドロー https://wjie-micro.github.io/doc/
マイクロアプリ × × ミッドロー https://zeroing.jd.com/micro-app/

プロジェクトのさまざまなテクノロジーのサポートとプロジェクトへのアクセスのコストを比較することで、最終的には選択に境界線がなくなりました。

2. wjie の簡単な使用法 (例として vue フレームワークを使用したメイン アプリケーションを取り上げます)

メインアプリケーションは vue フレームワークで、wjie-vue を直接使用でき、react フレームワークは wjie-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. 動作モードの選択

Unbounded には、シングルトン モード、キープアライブ モード、再構築モードの 3 つの動作モードがあります。

(1)、キープアライブモード(長期保存ページ)

解釈: Vue のキープアライブの性質 (サブアプリケーションのインスタンスと Web コンポーネントは破棄されず、状態とルーティングも失われず、ホット Web コンポーネントのホットプラグのみが実行されます) と同様に、サブアプリケーションはライフサイクルを実行したくありません。変更やサブアプリケーションの切り替えをしたくない場合は、白い画面時間の間、キープアライブ モードを使用できます。メインアプリケーションにはサブアプリケーションの異なるページにジャンプするためのエントリが複数あり、サブアプリケーションのルーティングを変更できないためキープアライブモードは使用できません。

構成: メイン アプリケーションがサブアプリケーションをロードするときに、構成パラメータに 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 でパッケージ化されていない古いプロジェクトの場合、サブアプリケーションを切り替えるときに白い画面が表示される場合があります。白い画面の時間を減らすために、可能な限りキープアライブ モードを使用する必要があります。

3. モジュールをロードします (メインアプリケーション構成)

サブアプリケーション基本情報管理

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

4. サブアプリケーションの設定

無制限のプラグイン システムは主に、ウェアハウス コードの変更を避けるために、ユーザーが実行時にサブアプリケーション コードを変更できるようにすることを目的としています。

// 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. データ送信とメッセージ通信

データインタラクション

1. 小道具を通す

2.窓越しに着信回線を通って通信する

3. イベントバスを介して通信する

小道具

メイン アプリケーションはデータを通じてサブアプリケーションにパラメータを渡し、サブアプリケーションはメソッド メソッドを通じてメイン アプリケーションにパラメータを渡します。

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

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

メインアプリのiframe内で実行されるサブアプリを利用する

iframe と同様のパラメータの受け渡しと呼び出し

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

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

イベントバス

分散型通信ソリューション、便利。コンポーネント間の通信と同様

メインアプリケーション

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

6. ルーティング

Vue メイン アプリケーションを例に挙げると、サブアプリケーション A の名前は A、メイン アプリケーション A ページのパスは /pathA、サブアプリケーション B の名前は B、メイン アプリケーション B ページのパスは/パスBです

メイン アプリケーションは、統合された props を使用してジャンプ関数を渡します。

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

1. メインのアプリケーション履歴ルーティング

サブアプリケーション 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. メインアプリケーションのハッシュルーティング

サブアプリケーション 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 });
      }
  });

7. 導入

フロントエンドの単一ページの展開は、自動化やツールの変更に関係なく、パッケージ化された静的ファイルをサーバーの正しい場所に配置することになります。したがって、プロジェクトの独立したデプロイメントと混合デプロイメントがサポートされています。

著者: JD Logistics Zhang Yanyan Liu Haiding

コンテンツソース: JD Cloud 開発者コミュニティ

{{名前}}
{{名前}}

おすすめ

転載: my.oschina.net/u/4090830/blog/9008248