<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          微前端架構(gòu)的幾種技術(shù)選型

          共 20057字,需瀏覽 41分鐘

           ·

          2022-07-16 10:10

          背景

          隨著SPA大規(guī)模的應(yīng)用,緊接著就帶來一個(gè)新問題:一個(gè)規(guī)模化應(yīng)用需要拆分。

          一方面功能快速增加導(dǎo)致打包時(shí)間成比例上升,而緊急發(fā)布時(shí)要求是越短越好,這是矛盾的。另一方面當(dāng)一個(gè)代碼庫集成了所有功能時(shí),日常協(xié)作絕對是非常困難的。而且最近十多年,前端技術(shù)的發(fā)展是非常快的,每隔兩年就是一個(gè)時(shí)代,導(dǎo)致同志們必須升級(jí)項(xiàng)目甚至于換一個(gè)框架。但如果大家想在一個(gè)規(guī)模化應(yīng)用中一個(gè)版本做好這件事,基本上是不可能的。

          最早的解決方案是采用iframe的方法,根據(jù)功能主要模塊拆分規(guī)模化應(yīng)用,子應(yīng)用之間使用跳轉(zhuǎn)。但這個(gè)方案最大問題是導(dǎo)致頁面重新加載和白屏。

          那有什么好的解決方案呢?微前端這樣具有跨應(yīng)用的解決方案在此背景下應(yīng)運(yùn)而生了!

          微前端的概念

          微前端是什么:微前端是一種類似于微服務(wù)的架構(gòu),是一種由獨(dú)立交付的多個(gè)前端應(yīng)用組成整體的架構(gòu)風(fēng)格,將前端應(yīng)用分解成一些更小、更簡單的能夠獨(dú)立開發(fā)、測試、部署的應(yīng)用,而在用戶看來仍然是內(nèi)聚的單個(gè)產(chǎn)品。有一個(gè)基座應(yīng)用(主應(yīng)用),來管理各個(gè)子應(yīng)用的加載和卸載。

          f135ab0912746bd6.png

          所以微前端不是指具體的庫,不是指具體的框架,不是指具體的工具,而是一種理想與架構(gòu)模式。

          微前端的核心三大原則就是:獨(dú)立運(yùn)行、獨(dú)立部署、獨(dú)立開發(fā)

          微前端的優(yōu)勢

          采用微前端架構(gòu)的好處就是,將這些小型應(yīng)用融合為一個(gè)完整的應(yīng)用,或者將原本運(yùn)行已久、沒有關(guān)聯(lián)的幾個(gè)應(yīng)用融合為一個(gè)應(yīng)用可以將多個(gè)項(xiàng)目融合為一,又可以減少項(xiàng)目之間的耦合,提升項(xiàng)目擴(kuò)展性。

          實(shí)現(xiàn)微前端的幾種方式

          • single-spaqiankun
          • 基于WebComponent的micro-app
          • webpack5實(shí)現(xiàn)的Module Federation

          微前端框架的分類

          Single-spa

          single-spa是一個(gè)很好的微前端基礎(chǔ)框架,而qiankun框架就是基于single-spa來實(shí)現(xiàn)的,在single-spa的基礎(chǔ)上做了一層封裝,也解決了single-spa的一些缺陷。

          首先我們先來了解該如何使用single-spa來完成微前端的搭建。

          single-spa.jpg

          Single-spa實(shí)現(xiàn)原理

          首先在基座應(yīng)用中注冊所有App的路由,single-spa保存各子應(yīng)用的路由映射關(guān)系,充當(dāng)微前端控制器Controler,。URL響應(yīng)時(shí),匹配子應(yīng)用路由并加載渲染子應(yīng)用。上圖便是對single-spa完整的描述。

          有了理論基礎(chǔ),接下來,我們來看看代碼層面時(shí)如何使用的。

          以下以Vue工程為例基座構(gòu)建single-spa,在Vue工程入口文件main.js完成基座的配置。

          基座配置

          //main.js
          import Vue from 'vue'
          import App from './App.vue'
          import router from './router'
          import { registerApplication, start } from 'single-spa'

          Vue.config.productionTip = false

          const mountApp = (url) => {
            return new Promise((resolve, reject) => {
              const script = document.createElement('script')
              script.src = url

              script.onload = resolve
              script.onerror = reject

              // 通過插入script標(biāo)簽的方式掛載子應(yīng)用
              const firstScript = document.getElementsByTagName('script')[0]
              // 掛載子應(yīng)用
              firstScript.parentNode.insertBefore(script, firstScript)
            })
          }

          const loadApp = (appRouter, appName) => {

            // 遠(yuǎn)程加載子應(yīng)用
            return async () => {
              //手動(dòng)掛載子應(yīng)用
              await mountApp(appRouter + '/js/chunk-vendors.js')
              await mountApp(appRouter + '/js/app.js')
              // 獲取子應(yīng)用生命周期函數(shù)
              return window[appName]
            }
          }

          // 子應(yīng)用列表
          const appList = [
            {
              // 子應(yīng)用名稱
              name'app1',
              // 掛載子應(yīng)用
              app: loadApp('http://localhost:8083''app1'),
              // 匹配該子路由的條件
              activeWhenlocation => location.pathname.startsWith('/app1'),
              // 傳遞給子應(yīng)用的對象
              customProps: {}
            },
            {
              name'app2',
              app: loadApp('http://localhost:8082''app2'),
              activeWhenlocation => location.pathname.startsWith('/app2'),
              customProps: {}
            }
          ]

          // 注冊子應(yīng)用
          appList.map(item => {
            registerApplication(item)
          })
           
          // 注冊路由并啟動(dòng)基座
          new Vue({
            router,
            mounted() {
              start()
            },
            renderh => h(App)
          }).$mount('#app')


          復(fù)制代碼

          構(gòu)建基座的核心是:配置子應(yīng)用信息,通過registerApplication注冊子應(yīng)用,在基座工程掛載階段start啟動(dòng)基座。

          子應(yīng)用配置

          import Vue from 'vue'
          import App from './App.vue'
          import router from './router'
          import singleSpaVue from 'single-spa-vue'

          Vue.config.productionTip = false

          const appOptions = {
            el'#microApp',
            router,
            renderh => h(App)
          }

          // 支持應(yīng)用獨(dú)立運(yùn)行、部署,不依賴于基座應(yīng)用
          // 如果不是微應(yīng)用環(huán)境,即啟動(dòng)自身掛載的方式
          if (!process.env.isMicro) {
            delete appOptions.el
            new Vue(appOptions).$mount('#app')
          }
          // 基于基座應(yīng)用,導(dǎo)出生命周期函數(shù)
          const appLifecycle = singleSpaVue({
            Vue,
            appOptions
          })

          // 拋出子應(yīng)用生命周期
          // 啟動(dòng)生命周期函數(shù)
          export const bootstrap = (props)  => {
            console.log('app2 bootstrap')
            return appLifecycle.bootstrap(() => { })
          }
          // 掛載生命周期函數(shù)
          export const mount = (props) => {
            console.log('app2 mount')
            return appLifecycle.mount(() => { })
          }
          // 卸載生命周期函數(shù)
          export const unmount = (props) => {
            console.log('app2 unmount')
            return appLifecycle.unmount(() => { })
          }

          復(fù)制代碼

          配置子應(yīng)用為umd打包方式

          //vue.config.js
          const package = require('./package.json')
          module.exports = {
            // 告訴子應(yīng)用在這個(gè)地址加載靜態(tài)資源,否則會(huì)去基座應(yīng)用的域名下加載
            publicPath'//localhost:8082',
            // 開發(fā)服務(wù)器
            devServer: {
              port8082
            },
            configureWebpack: {
              // 導(dǎo)出umd格式的包,在全局對象上掛載屬性package.name,基座應(yīng)用需要通過這個(gè)
              // 全局對象獲取一些信息,比如子應(yīng)用導(dǎo)出的生命周期函數(shù)
              output: {
                // library的值在所有子應(yīng)用中需要唯一
                library: package.name,
                libraryTarget'umd'
              }
            }

          復(fù)制代碼

          配置子應(yīng)用環(huán)境變量

          // .env.micro 
          NODE_ENV=development
          VUE_APP_BASE_URL=/app2
          isMicro=true
          復(fù)制代碼

          子應(yīng)用配置的核心是用singleSpaVue生成子路由配置后,必須要拋出其生命周期函數(shù)

          用以上方式便可輕松實(shí)現(xiàn)一個(gè)簡單的微前端應(yīng)用了。

          那么我們有single-spa這種微前端解決方案,為什么還需要qiankun呢?

          相比于single-spaqiankun他解決了JS沙盒環(huán)境,不需要我們自己去進(jìn)行處理。在single-spa的開發(fā)過程中,我們需要自己手動(dòng)的去寫調(diào)用子應(yīng)用JS的方法(如上面的 createScript方法),而qiankun不需要,乾坤只需要你傳入響應(yīng)的apps的配置即可,會(huì)幫助我們?nèi)ゼ虞d。

          Qiankun

          Qiankun的優(yōu)勢

          • 基于 single-spa[1] 封裝,提供了更加開箱即用的 API。

          • 技術(shù)棧無關(guān),任意技術(shù)棧的應(yīng)用均可 使用/接入,不論是 React/Vue/Angular/JQuery 還是其他等框架。

          • HTML Entry 接入方式,讓你接入微應(yīng)用像使用 iframe 一樣簡單。

          • 樣式隔離,確保微應(yīng)用之間樣式互相不干擾。

          • JS 沙箱,確保微應(yīng)用之間 全局變量/事件 不沖突。

          • 資源預(yù)加載,在瀏覽器空閑時(shí)間預(yù)加載未打開的微應(yīng)用資源,加速微應(yīng)用打開速度。

          基座配置

          import { registerMicroApps, start } from 'qiankun';


          registerMicroApps([
            {
              name: 'reactApp',
              entry: '//localhost:3000',
              container: '#container',
              activeRule: '/app-react',
            },
            {
              name: 'vueApp',
              entry: '//localhost:8080',
              container: '#container',
              activeRule: '/app-vue',
            },
            {
              name: 'angularApp',
              entry: '//localhost:4200',
              container: '#container',
              activeRule: '/app-angular',
            },
          ]);
          // 啟動(dòng) qiankun
          start();
          復(fù)制代碼

          子應(yīng)用配置

          以 create react app 生成的 react 16 項(xiàng)目為例,搭配 react-router-dom 5.x。

          1.在 src 目錄新增 public-path.js,解決子應(yīng)用掛載時(shí),訪問靜態(tài)資源沖突

            if (window.__POWERED_BY_QIANKUN__) {
              __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
            }
          復(fù)制代碼

          2.設(shè)置 history 模式路由的 base

            <BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/app-react' : '/'}>
          復(fù)制代碼

          3.入口文件 index.js 修改,為了避免根 id #root 與其他的 DOM 沖突,需要限制查找范圍。

            import './public-path';
            import React from 'react';
            import ReactDOM from 'react-dom';
            import App from './App';


            function render(props) {
              const { container } = props;
              ReactDOM.render(<App />, container ? container.querySelector('#root') : 
              document.querySelector('#root'));
            }


            if (!window.__POWERED_BY_QIANKUN__) {
              render({});
            }


            export async function bootstrap() {
              console.log('[react16] react app bootstraped');
            }


            export async function mount(props) {
              console.log('[react16] props from main framework', props);
              render(props);
            }


            export async function unmount(props) {
              const { container } = props;
              ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') :  
              document.querySelector('#root'));
            }
          復(fù)制代碼

          4.修改 webpack 配置

          安裝插件 @rescripts/cli,當(dāng)然也可以選擇其他的插件,例如 react-app-rewired

          npm i -D @rescripts/cli
          復(fù)制代碼

          根目錄新增 .rescriptsrc.js

          const { name } = require('./package');


          module.exports = {
            webpack: (config) => {
              config.output.library = `${name}-[name]`;
              config.output.libraryTarget = 'umd';
              config.output.jsonpFunction = `webpackJsonp_${name}`;
              config.output.globalObject = 'window';


              return config;
            },


            devServer: (_) => {
              const config = _;


              config.headers = {
                'Access-Control-Allow-Origin''*',
              };
              config.historyApiFallback = true;
              config.hot = false;
              config.watchContentBase = false;
              config.liveReload = false;


              return config;
            },
          };
          復(fù)制代碼

          以上對Qiankun的使用可以看出,與single-spa使用過程很相似。不同的是,Qiankun的使用過程更簡便了。一些內(nèi)置的操作交由給Qiankun內(nèi)部實(shí)現(xiàn)。這是一種IOC思想的實(shí)現(xiàn),我們只管面向容器化開發(fā),其他操作交給Qiankun框架管理。

          Micro-app

          micro-app并沒有沿襲single-spa的思路,而是借鑒了WebComponent的思想,通過CustomElement結(jié)合自定義的ShadowDom,將微前端封裝成一個(gè)類WebComponent組件,從而實(shí)現(xiàn)微前端的組件化渲染。并且由于自定義ShadowDom的隔離特性,micro-app不需要像single-spaqiankun一樣要求子應(yīng)用修改渲染邏輯并暴露出方法,也不需要修改webpack配置,是目前市面上接入微前端成本最低的方案。

          WebComponent的概念

          **WebComponent**[2]是HTML5提供的一套自定義元素的接口,**WebComponent**[3]是一套不同的技術(shù),允許您創(chuàng)建可重用的定制元素(它們的功能封裝在您的代碼之外)并且在您的 web 應(yīng)用中使用它們。以上是MDN社區(qū)對WebComponent的解釋。

          • Custom elements(自定義元素): 一組 JavaScript API,允許您定義 custom elements 及其行為,然后可以在您的用戶界面中按照需要使用它們。
          • Shadow DOM(影子 DOM) :一組 JavaScript API,用于將封裝的“影子”DOM 樹附加到元素(與主文檔 DOM 分開呈現(xiàn))并控制其關(guān)聯(lián)的功能。通過這種方式,您可以保持元素的功能私有,這樣它們就可以被腳本化和樣式化,而不用擔(dān)心與文檔的其他部分發(fā)生沖突。
          • HTML templates(HTML 模板):  <template> 和 <slot> 元素使您可以編寫不在呈現(xiàn)頁面中顯示的標(biāo)記模板。然后它們可以作為自定義元素結(jié)構(gòu)的基礎(chǔ)被多次重用。

          接下來用一個(gè)小例子更快來理解WebComponent的概念。

          一個(gè)存在組件內(nèi)交互的WebComponent

          // 基于HTMLElement自定義組件元素
          class CounterElement extends HTMLElement {

            // 在構(gòu)造器中生成shadow節(jié)點(diǎn)
            constructor() {
              super();

              this.counter = 0;

              // 打開影子節(jié)點(diǎn)
              // 影子節(jié)點(diǎn)是為了隔離外部元素的影響
              const shadowRoot = this.attachShadow({ mode'open' });

              // 定義組件內(nèi)嵌樣式
              const styles = `
                    #counter-increment {
                        width: 60px;
                        height: 30px;
                        margin: 20px;
                        background: none;
                        border: 1px solid black;
                    }
                `
          ;

              // 定義組件HTMl結(jié)構(gòu)
              shadowRoot.innerHTML = `
                    <style>${styles}</style>
                    <h3>Counter</h3>
                    <slot name='counter-content'>Button</slot>
                    <span id='counter-value'>; 0 </span>;
                    <button id='counter-increment'> + </button>
                `
          ;

              // 獲取+號(hào)按鈕及數(shù)值內(nèi)容
              this.incrementButton = this.shadowRoot.querySelector('#counter-increment');
              this.counterValue = this.shadowRoot.querySelector('#counter-value');

              // 實(shí)現(xiàn)點(diǎn)擊組件內(nèi)事件驅(qū)動(dòng)
              this.incrementButton.addEventListener("click"this.decrement.bind(this));

            }

            increment() {
              this.counter++
              this.updateValue();
            }

            // 替換counter節(jié)點(diǎn)內(nèi)容,達(dá)到更新數(shù)值的效果
            updateValue() {
              this.counterValue.innerHTML = this.counter;
            }
          }

          // 在真實(shí)dom上,生成自定義組件元素
          customElements.define('counter-element', CounterElement);

          復(fù)制代碼

          有了對WebComponent的理解,接下來,我們更明白了Micro-app的優(yōu)勢。

          micro-app的優(yōu)勢

          d879637b4bb34253.png
          • 使用簡單

            我們將所有功能都封裝到一個(gè)類WebComponent組件中,從而實(shí)現(xiàn)在基座應(yīng)用中嵌入一行代碼即可渲染一個(gè)微前端應(yīng)用。

            同時(shí)micro-app還提供了js沙箱樣式隔離元素隔離預(yù)加載數(shù)據(jù)通信靜態(tài)資源補(bǔ)全等一系列完善的功能。

          • 零依賴

            micro-app沒有任何依賴,這賦予它小巧的體積和更高的擴(kuò)展性。

          • 兼容所有框架

            為了保證各個(gè)業(yè)務(wù)之間獨(dú)立開發(fā)、獨(dú)立部署的能力,micro-app做了諸多兼容,在任何技術(shù)框架中都可以正常運(yùn)行。

          基座的簡易配置

          基座存在預(yù)加載子應(yīng)用、父子應(yīng)用通信、公共文件共享等等


          // index.js
          import React from "react"
          import ReactDOM from "react-dom"
          import App from './App'
          import microApp from '@micro-zoe/micro-app'

          const appName = 'my-app'

          // 預(yù)加載
          microApp.preFetch([
            { name: appName, url'xxx' }
          ])

          // 基座向子應(yīng)用數(shù)據(jù)通信
          microApp.setData(appName, { type'新的數(shù)據(jù)' })
          // 獲取指定子應(yīng)用數(shù)據(jù)
          const childData = microApp.getData(appName)

          microApp.start({
            // 公共文件共享
            globalAssets: {
              js: ['js地址1''js地址2', ...], // js地址
              css: ['css地址1''css地址2', ...], // css地址
            }
          })
          復(fù)制代碼

          分配一個(gè)路由給子應(yīng)用

          // router.js
          import { BrowserRouter, Switch, Route } from 'react-router-dom'

          export default function AppRoute ({
            return (
              <BrowserRouter>
                <Switch>
                  <Route path='/'>
                    <micro-app name='app1' url='http://localhost:3000/' baseroute='/'></micro-app>
                  </Route>
                </Switch>
              </BrowserRouter>

            )
          }

          復(fù)制代碼

          子應(yīng)用的簡易配置

          // index.js
          import React from "react"
          import ReactDOM from "react-dom"
          import App from './App'
          import microApp from '@micro-zoe/micro-app'

          const appName = 'my-app'

          // 子應(yīng)用運(yùn)行時(shí),切換靜態(tài)資源訪問路徑
          if (window.__MICRO_APP_ENVIRONMENT__) {
            __webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__
          }

          // 基子應(yīng)用向基座發(fā)送數(shù)據(jù)
          // dispatch只接受對象作為參數(shù)
          window.microApp.dispatch({ type'子應(yīng)用發(fā)送的數(shù)據(jù)' })
          // 獲取基座數(shù)據(jù)
          const data = window.microApp.getData() // 返回基座下發(fā)的data數(shù)據(jù)

          //性能優(yōu)化,umd模式
          // 如果子應(yīng)用渲染和卸載不頻繁,那么使用默認(rèn)模式即可,如果子應(yīng)用渲染和卸載非常頻繁建議使用umd模式
          // 將渲染操作放入 mount 函數(shù) -- 必填
          export function mount({
            ReactDOM.render(<App />document.getElementById("root"))
          }

          // 將卸載操作放入 unmount 函數(shù) -- 必填
          export function unmount({
            ReactDOM.unmountComponentAtNode(document.getElementById("root"))
          }

          // 微前端環(huán)境下,注冊mount和unmount方法
          if (window.__MICRO_APP_ENVIRONMENT__) {
            window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount }
          else {
            // 非微前端環(huán)境直接渲染
            mount()
          }

          復(fù)制代碼

          設(shè)置子應(yīng)用路由

          import { BrowserRouter, Switch, Route } from 'react-router-dom'

          export default function AppRoute ({
            return (
              // 設(shè)置基礎(chǔ)路由,子應(yīng)用可以通過window.__MICRO_APP_BASE_ROUTE__獲取基座下發(fā)的baseroute,
              // 如果沒有設(shè)置baseroute屬性,則此值默認(rèn)為空字符串
              <BrowserRouter basename={window.__MICRO_APP_BASE_ROUTE__ || '/'}>
                ...
              </BrowserRouter>

            )
          }

          復(fù)制代碼

          以上便是Micro-app的用法

          Module Federation

          Module Federation是Webpack5提出的概念,module federation用來解決多個(gè)應(yīng)用之間代碼共享的問題,讓我們更加優(yōu)雅的實(shí)現(xiàn)跨應(yīng)用的代碼共享。

          MF想做的事和微前端想解決的問題是類似的,把一個(gè)應(yīng)用進(jìn)行拆分成多個(gè)應(yīng)用,每個(gè)應(yīng)用可獨(dú)立開發(fā),獨(dú)立部署,一個(gè)應(yīng)用可以動(dòng)態(tài)加載并運(yùn)行另一個(gè)應(yīng)用的代碼,并實(shí)現(xiàn)應(yīng)用之間的依賴共享。

          為了實(shí)現(xiàn)這樣的功能, MF在設(shè)計(jì)上提出了這幾個(gè)核心概念。

          Container

          一個(gè)被 ModuleFederationPlugin 打包出來的模塊被稱為 Container。通俗點(diǎn)講就是,如果我們的一個(gè)應(yīng)用使用了 ModuleFederationPlugin 構(gòu)建,那么它就成為一個(gè) Container,它可以加載其他的 Container,可以被其他的 Container 所加載。

          Host&Remote

          從消費(fèi)者和生產(chǎn)者的角度看 ContainerContainer 又可被稱作 Host 或 Remote

          Host:消費(fèi)方,它動(dòng)態(tài)加載并運(yùn)行其他 Container 的代碼。

          Remote:提供方,它暴露屬性(如組件、方法等)供 Host 使用

          可以知道,這里的 Host 和 Remote 是相對的,因?yàn)?一個(gè) Container 既可以作為 Host,也可以作為 Remote

          Shared

          一個(gè) Container 可以 Shared 它的依賴(如 react、react-dom)給其他 Container 使用,也就是共享依賴。

          微信圖片_20220626184254.png
          微信圖片_20220626184305.png

          以上是webpack5與之前版本的模塊管理對比圖

          微應(yīng)用配置

          通過webpack5的配置達(dá)成微應(yīng)用的效果

          // 配置webpack.config.js
          const { ModuleFederationPlugin } = require("webpack").container;
          new ModuleFederationPlugin({
            name"appA",
           //出口文件
            filename"remoteEntry.js",
           //暴露可訪問的組件
            exposes: {
              "./input""./src/input",
            },
           //或者其他模塊的組件
           //如果把這一模塊當(dāng)作基座模塊的話,
           //這里應(yīng)該配置其他子應(yīng)用模塊的入口文件
            remotes: {
              appB"appB@http://localhost:3002/remoteEntry.js",
            },
           //共享依賴,其他模塊不需要再次下載,便可使用
            shared: ['react''react-dom'],
          })

          復(fù)制代碼

          以上便是我對微應(yīng)用架構(gòu)的理解,以及微應(yīng)用架構(gòu)技術(shù)的演變過程。不難看出,這些技術(shù)的演變都朝著易用性和可拓展性的方向演進(jìn)。其中技術(shù)也有其時(shí)代的局限性,不過思想和技術(shù)總是在不斷進(jìn)步的。這幾類技術(shù)選型都有其優(yōu)缺點(diǎn),各有千秋,我們可以根據(jù)不同的需要選擇不同的技術(shù)來構(gòu)建應(yīng)用。

          下列是本文寫作時(shí)的參考資料:

          single-spa: zh-hans.single-spa.js.org/docs/gettin…[4]

          qiankun: qiankun.umijs.org/zh/guide[5]

          WebComponent: developer.mozilla.org/zh-CN/docs/…[6]

          micro-app: cangdu.org/micro-app/d…[7]

          參考資料

          [1]

          https://github.com/CanopyTax/single-spa

          [2]

          https://developer.mozilla.org/zh-CN/docs/Web/Web_Components#%E4%BE%8B%E5%AD%90

          [3]

          https://developer.mozilla.org/zh-CN/docs/Web/Web_Components#%E4%BE%8B%E5%AD%90

          [4]

          https://zh-hans.single-spa.js.org/docs/getting-started-overview

          [5]

          https://qiankun.umijs.org/zh/guide

          [6]

          https://developer.mozilla.org/zh-CN/docs/Web/Web_Components

          [7]

          http://cangdu.org/micro-app/docs.html#/


          關(guān)于本文

          作者:花小白

          https://juejin.cn/post/7113503219904430111

          最后


          歡迎關(guān)注【前端瓶子君】??ヽ(°▽°)ノ?
          回復(fù)「算法」,加入前端編程源碼算法群,每日一道面試題(工作日),第二天瓶子君都會(huì)很認(rèn)真的解答喲!
          回復(fù)「交流」,吹吹水、聊聊技術(shù)、吐吐槽!
          回復(fù)「閱讀」,每日刷刷高質(zhì)量好文!
          如果這篇文章對你有幫助,在看」是最大的支持
           》》面試官也在看的算法資料《《
          “在看和轉(zhuǎn)發(fā)”就是最大的支持


          瀏覽 57
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  污污污啪啪啪 | 午夜成人性爱免费视频 | 99在线成人精品视频 | 久久精品夜色噜噜亚洲A∨ | 老外玩csgo中国的妹子视频 |