1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        微前端x重構(gòu)實(shí)踐落地總結(jié)

        共 7701字,需瀏覽 16分鐘

         ·

        2022-01-14 18:54

        前言

        大家好,我是海怪。最近換到了新部門,在做智能平臺(tái)相關(guān)的內(nèi)容。我接到的第一個(gè)任務(wù)就是把以前前端的項(xiàng)目重構(gòu)一次。

        說(shuō)是重構(gòu),不如說(shuō)是重寫一遍。因?yàn)樵瓉?lái)的項(xiàng)目是 ant-design-vue + vue 全家桶,要切換成 ant-design + ant-design-pro + react 全家桶

        更讓人頭疼的是,產(chǎn)品經(jīng)理并不會(huì)讓我們有大把大把時(shí)間專門搞重構(gòu),我們要邊重構(gòu)邊做需求。在這樣的挑戰(zhàn)下,我想到了微前端解決方案,下面就跟大家分享這次 微前端在重構(gòu)上的落地實(shí)踐吧。

        這次實(shí)踐我簡(jiǎn)化了一下,放在 Github 上,大家可以自行 clone 來(lái)玩玩。

        技術(shù)棧

        首先,來(lái)講講技術(shù)棧,老項(xiàng)目主要用了下面的技術(shù):

        • 框架
          • Vue
          • vuex
          • vue-router
        • 樣式
          • scss
        • UI
          • ant-design-vue
          • ant-design-pro for vue
        • 腳手架
          • vue-cli

        新項(xiàng)目需要用到的技術(shù)有:

        • 框架
          • React
          • redux + redux-toolkit
          • react-router
        • 新式
          • less
        • UI
          • react-design-react
          • react-design-pro for react
        • 腳手架
          • 團(tuán)隊(duì)內(nèi)部自創(chuàng)腳手架

        可以看到兩個(gè)項(xiàng)目除了業(yè)務(wù)之外,幾乎沒(méi)什么交集了。

        微前端策略

        老項(xiàng)目作為主應(yīng)用,通過(guò) qiankun 去加載新項(xiàng)目(子應(yīng)用)里的頁(yè)面。

        • 當(dāng)沒(méi)有需求時(shí),在新項(xiàng)目(子應(yīng)用)重寫頁(yè)面,重寫完了之后,在老項(xiàng)目(主應(yīng)用)中加載新項(xiàng)目的頁(yè)面,下掉老項(xiàng)目的頁(yè)面
        • 當(dāng)有需求時(shí),也是在新項(xiàng)目(子應(yīng)用)重寫面面再做對(duì)應(yīng)需求(向產(chǎn)品要多點(diǎn)時(shí)間),重寫完了之后,在老項(xiàng)目(主應(yīng)用)中加載新項(xiàng)目的頁(yè)面

        這樣一來(lái)就可以避免 “我要一整個(gè)月都做重構(gòu)” 的局面,而是可以做到一個(gè)頁(yè)面一個(gè)頁(yè)地慢慢遷移。最終等所有頁(yè)面都在新項(xiàng)目寫好之后,直接把老項(xiàng)目下掉,新項(xiàng)目就可以從幕后站出來(lái)了。相當(dāng)于從重寫的第一天開始,老項(xiàng)目就成替身了。

        如果只看上面畫的架構(gòu)圖,會(huì)覺得:啊,不就引入一個(gè) qiankun 就完事了么?實(shí)際上還有很細(xì)節(jié)和問(wèn)題需要注意的。

        升級(jí)版架構(gòu)

        上圖的架構(gòu)有一個(gè)問(wèn)題就是,當(dāng)每次點(diǎn)擊側(cè)邊欄的 MenuItem 時(shí),都會(huì)加載一次微應(yīng)用的子頁(yè)面,也即:

        微應(yīng)用子頁(yè)面之間的切換,其實(shí)就是在微應(yīng)用里路由切換嘛,大可不需要通過(guò)重新加載一次微應(yīng)用來(lái)做微應(yīng)用子頁(yè)面的切換。

        所以,我想了一個(gè)辦法:我在 旁邊放了一個(gè)組件 Container。進(jìn)入主應(yīng)用后,這個(gè)組件先直接把微應(yīng)用整個(gè)都加載了。

        <a-layout>
        ??
        ??<a-layout-content>
        ????
        ????<micro-app-container>micro-app-container>
        ????
        ????<router-view/>
        ??a-layout-content>
        a-layout>

        當(dāng)展示老頁(yè)面時(shí),把這個(gè) Container 高度設(shè)為 0,要展示新頁(yè)面時(shí),再把 Container 高度自動(dòng)撐開。

        //?micro-app-container

        <template>
        <div?class="container"?:style="{?height:?visible???'100%'?:?0?}">
        ??<div?id="micro-app-container">div>
        div>
        template>

        <script>
        import?{?registerMicroApps,?start?}?from?'qiankun'

        export?default?{
        ??name:?"Container",
        ??props:?{
        ????visible:?{
        ??????type:?Boolean,
        ??????defaultValue:?false,
        ????}
        ??},
        ??mounted()?{
        ????registerMicroApps([
        ??????{
        ????????name:?'microReactApp',
        ????????entry:?'//localhost:3000',
        ????????container:?'#micro-app-container',
        ????????activeRule:?'/#/micro-react-app',
        ??????},
        ????])
        ????start()
        ??},
        }
        script>

        這樣一來(lái),當(dāng)進(jìn)入老項(xiàng)目時(shí),這個(gè) Container 自動(dòng)被 mounted 后就會(huì)地去加載子應(yīng)用了。當(dāng)在切換新頁(yè)面時(shí),本質(zhì)上是在子應(yīng)用里做路由切換,而不是從 A 應(yīng)用切換到 B 應(yīng)用了。

        子應(yīng)用的布局

        由于新的項(xiàng)目(子應(yīng)用)里的頁(yè)面要供給老項(xiàng)目(主應(yīng)用)來(lái)使用的,所以子應(yīng)用也應(yīng)該有兩套布局:

        第一套標(biāo)準(zhǔn)的管理后臺(tái)布局,有 Sider,Header 還有 Content,另一套側(cè)作為子應(yīng)用時(shí),只展示 Content 部分的布局。

        //?單獨(dú)運(yùn)行時(shí)的布局
        export?const?StandaloneLayout:?FC?=?()?=>?{
        ??return?(
        ????<AntLayout?className={styles.layout}>
        ??????<Sider/>
        ??????<AntLayout>
        ????????<Header?/>
        ????????<Content?/>
        ??????AntLayout>

        ????AntLayout>
        ??)
        }

        //?作為子應(yīng)用時(shí)的布局
        export?const?MicroAppLayout?=?()?=>?{
        ??return?(
        ????<Content?/>
        ??)
        }
        單獨(dú)運(yùn)行時(shí)的布局
        作為微應(yīng)用時(shí)的布局

        最后通過(guò) window.__POWERED_BY_QIANKUN__ 就可以切換不同的布局了。

        import?{?StandaloneLayout,?MicroAppLayout?}?from?"./components/Layout";

        const?Layout?=?window.__POWERED_BY_QIANKUN__???MicroAppLayout?:?StandaloneLayout;

        function?App()?{
        ??return?(
        ????<Layout/>
        ??);
        }

        樣式?jīng)_突

        qiankun 是默認(rèn)開啟 JS 隔離(沙箱),關(guān)閉 CSS 樣式隔離的。為什么這么做呢?因?yàn)?CSS 的隔離是不能無(wú)腦做去做的,下面來(lái)講講這方面的問(wèn)題。

        qiankun 一共提供了兩種 CSS 隔離方法(沙箱):嚴(yán)格沙箱 以及 實(shí)驗(yàn)性沙箱。

        嚴(yán)格沙箱

        開啟代碼:

        start({
        ??sandbox:?{
        ????strictStyleIsolation:?true,
        ??}
        })

        嚴(yán)格沙箱主要通過(guò) ShadowDOM 來(lái)實(shí)現(xiàn) CSS 樣式隔離,效果是當(dāng)子應(yīng)用被掛在到 ShadowDOM 上,主子應(yīng)用的樣式 完完全全 地被隔離,無(wú)法互相影響。你說(shuō):這不是很好么?No No No。

        這種沙箱的優(yōu)點(diǎn)也成為了它自己的缺點(diǎn):除了樣式的硬隔離,DOM 元素也直接硬隔離了,導(dǎo)致子應(yīng)用的一些 Modal、PopoverDrawer 組件會(huì)因?yàn)檎也坏街鲬?yīng)用的 body 而丟失,甚至跑到整個(gè)屏幕之外。

        還記得我剛說(shuō)主應(yīng)用和子應(yīng)用都用了 ant-design 么?ant-design 的 Modal、PopoverDrawer 的實(shí)現(xiàn)方式就是要掛在到 document.body 上的,這么一隔離,它們一掛在整個(gè)元素起飛了。

        實(shí)驗(yàn)性沙箱

        開啟代碼:

        start({
        ??sandbox:?{
        ????experimentalStyleIsolation:?true,
        ??}
        })

        這種沙箱實(shí)現(xiàn)方式就是給子應(yīng)用的樣式加后綴標(biāo)簽,有點(diǎn)像 Vue 里的 scoped,通過(guò)名字來(lái)做樣式 “軟隔離”,比如像這樣:

        其實(shí)這種方式已經(jīng)很好地做了樣式隔離,但是主應(yīng)用里經(jīng)常有人喜歡寫 !important 來(lái)覆蓋 ant-design 的組件原樣式:

        .ant-xxx?{
        ???color:?white:?!important;
        }

        !importnant 的優(yōu)先級(jí)是最高的,如果微應(yīng)用也用了這個(gè) .ant-xxx 類,就很容易被主應(yīng)用的樣式影響了。所以在加載微應(yīng)用時(shí),還需要處理 ant-design 之間的樣式?jīng)_突問(wèn)題。

        ant-design 樣式?jīng)_突

        ant-design 提供了一個(gè)非常好的類名前綴功能:用 prefixCls 來(lái)做樣式隔離,我自然也用上了:

        //?自定義前綴
        const?prefixCls?=?'cmsAnt';

        //?設(shè)置?Modal、Message、Notification?rootPrefixCls
        ConfigProvider.config({
        ??prefixCls,
        })

        //?渲染
        function?render(props:?any)?{
        ??const?{?container,?state,?commit,?dispatch?}?=?props;

        ??const?value?=?{?state,?commit,?dispatch?};

        ??const?root?=?(
        ????<ConfigProvider?prefixCls={prefixCls}>
        ??????<HashRouter?basename={basename}>
        ????????<MicroAppContext.Provider?value={value}>
        ??????????<App?/>
        ????????MicroAppContext.Provider>

        ??????HashRouter>
        ????ConfigProvider>
        ??);

        ??ReactDOM.render(root,?container
        ??????container.querySelector('#root')
        ????:?document.querySelector('#root'));
        }
        @ant-prefix: cmsAnt; // 引入來(lái)改變?nèi)肿兞恐?br>

        但是不知道為什么,在 less 文件中改了 ant-prefix 變量后,ant-design-pro 的樣式還是老樣子,有的組件樣式改變了,有的沒(méi)變化。

        最后,我是通過(guò) less-loadermodifyVars 在打包時(shí)來(lái)更新全局的 ant-prefix less 變量才搞定的:

        var?webpackConfig?=?{
        ??test:?/.(less)$/,
        ??use:?[
        ????...
        ????{
        ??????loader:?'less-loader',
        ??????options:?{
        ????????lessOptions:?{
        ??????????modifyVars:?{
        ????????????'ant-prefix':?'cmsAnt'
        ??????????},
        ??????????sourceMap:?true,
        ??????????javascriptEnabled:?true,
        ????????}
        ??????}
        ????}
        ??]
        }

        具體 Issue 看 Issue: ant-design 改了 prefixCls 后 ant-design-pro 不生效。

        主子應(yīng)用狀態(tài)管理

        老項(xiàng)目(主應(yīng)用)用到了 vuex 全局狀態(tài)管理,所以新項(xiàng)目頁(yè)面(子應(yīng)用)里有時(shí)需要更改主應(yīng)用里的狀態(tài),這里我用了 qiankun 的 globalState 來(lái)處理。

        首先在 Container 里創(chuàng)建了 globalActions,再監(jiān)聽 vuex 狀態(tài)變更,每次變更都通知子應(yīng)用,同時(shí)把 vuex 的 commitdispatch 函數(shù)傳給子應(yīng)用:

        import?{initGlobalState,?registerMicroApps,?start}?from?'qiankun'

        const?globalActions?=?initGlobalState({
        ??state:?{},
        ??commit:?null,
        ??dispatch:?null,
        });

        export?default?{
        ??name:?"Container",
        ??props:?{
        ????visible:?{
        ??????type:?Boolean,
        ??????defaultValue:?false,
        ????}
        ??},
        ??mounted()?{
        ????const?{?dispatch,?commit,?state?}?=?this.$store;
        ????registerMicroApps([
        ??????{
        ????????name:?'microReactApp',
        ????????entry:?'//localhost:3000',
        ????????container:?'#micro-app-container',
        ????????activeRule:?'/#/micro-react-app',
        ????????//?初始化時(shí)就傳入主應(yīng)用的狀態(tài)和?commit,?dispatch
        ????????props:?{
        ??????????state,
        ??????????dispatch,
        ??????????commit,
        ????????}
        ??????},
        ????])
        ????
        ????start()
        ????
        ????//?vuex?的?store?變更后再次傳入主應(yīng)用的狀態(tài)和?commit,?dispatch
        ????this.$store.watch((state)?=>?{
        ??????console.log('state',?state);
        ??????globalActions.setGlobalState({
        ????????state,
        ????????commit,
        ????????dispatch
        ??????});
        ????})
        ??},
        }

        子應(yīng)用里接收主應(yīng)用傳來(lái)的 state,commit 以及 dispatch 函數(shù),同時(shí)新起一個(gè) Context,把這些東西都放到 MicroAppContext 里。(Redux 因?yàn)椴恢С执娣藕瘮?shù)這種 nonserializable 的值,所以只能先存到 Context 里)

        //?渲染
        function?render(props:?any)?{
        ??const?{?container,?state,?commit,?dispatch?}?=?props;

        ??const?value?=?{?state,?commit,?dispatch?};

        ??const?root?=?(
        ????<HashRouter?basename={basename}>
        ??????<MicroAppContext.Provider?value={value}>
        ????????<App?/>
        ??????MicroAppContext.Provider>

        ????HashRouter>
        ??);

        ??ReactDOM.render(root,?container
        ??????container.querySelector('#root')
        ????:?document.querySelector('#root'));
        }

        //?mount?時(shí)監(jiān)聽?globalState,只要一改再次渲染?App
        export?async?function?mount(props:?any)?{
        ??console.log('[micro-react-app]?mount',?props);
        ??props.onGlobalStateChange((state:?any)?=>?{
        ????console.log('[micro-react-app]?vuex?狀態(tài)更新')
        ????render(state);
        ??})
        ??render(props);
        }

        這樣一來(lái),子應(yīng)用也可以通過(guò) commit,和 dispatch 來(lái)更改主應(yīng)用的值了。

        const?OrderList:?FC?=?()?=>?{
        ??const?{?state,?commit?}?=?useContext(MicroAppContext);

        ??return?(
        ????<div>
        ??????<h1?className="title">【微應(yīng)用】訂單列表h1>


        ??????<div>
        ????????<p>主應(yīng)用的?Counter:?{state.counter}p>
        ????????<Button?type="primary"?onClick={()?=>?commit('increment')}>【微應(yīng)用】+1Button>
        ????????<Button?danger?onClick={()?=>?commit('decrement')}>【微應(yīng)用】-1Button>
        ??????div>
        ????div>
        ??)
        }

        當(dāng)然了,這樣的實(shí)踐也是我自己 “發(fā)明” 的,不知道這是不是一個(gè)好的實(shí)踐,我只能說(shuō)這樣能 Work。

        全局變量報(bào)錯(cuò)

        另一個(gè)問(wèn)題就是當(dāng)子應(yīng)用隱式使用全局變量時(shí),import-html-entry 執(zhí)行 JS 時(shí)會(huì)直接爆炸。比如微應(yīng)用有如下

        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

          <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            free性中国hd国语露脸 | 国产日皮视频 | 男人添女人下面全程高潮黄黄 | 韩国三级黄色大片 | 成人性爱视频免费网站 | 国产极品粉嫩馒头在线 | 亚洲国产私拍精品国模在线观看 | 91香蕉视频黄 | 1000部啪啪毛片免费看 | 青娱乐亚洲精品视频 |