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>

        【工程化】從微服務(wù)到微前端:淺談微前端的設(shè)計(jì)思想

        共 13679字,需瀏覽 28分鐘

         ·

        2021-07-12 11:01

        記得作為實(shí)習(xí)生剛到公司的第一天就被什么monorepo、微前端等名詞搞得一頭霧水,經(jīng)過一段時間的學(xué)習(xí)終于摸清了一點(diǎn)門道,那么今天就來和大家聊一下我對微前端設(shè)計(jì)的看法和理解。

        1、引入:什么是微服務(wù)?

        微服務(wù)是近幾年在互聯(lián)網(wǎng)業(yè)界內(nèi)非常?? 的一個詞,在俺們大學(xué)沸點(diǎn)工作室Java組也已經(jīng)有Spring Cloud微服務(wù)的實(shí)踐先例,那我們作為前端的角度,該怎么理解微服務(wù)呢?

        可以看看下面這個?? :

        一個系統(tǒng)有PC Web端、手機(jī)H5、和后臺管理系統(tǒng),那么整個系統(tǒng)的結(jié)構(gòu)大概就像這樣:

        image.png

        這樣會造成什么問題嘞?

        • 單體服務(wù)端項(xiàng)目過大,不利于快速上手打包編譯;
        • 不同系統(tǒng)會有相同的功能點(diǎn),導(dǎo)致產(chǎn)生大量重復(fù)的無意義的接口;
        • 數(shù)據(jù)庫設(shè)計(jì)復(fù)雜。

        那么微服務(wù)又是怎么解決的嘞?

        核心就是就是將系統(tǒng)拆分成不同的服務(wù),通過網(wǎng)關(guān)和controller來進(jìn)行簡單的控制和調(diào)用,各服務(wù)分而治之、互不影響。

        我們現(xiàn)在再看一哈新的項(xiàng)目結(jié)構(gòu):

        image.png

        通過服務(wù)的拆分后,我們的系統(tǒng)是不是更加清晰了?? ,那么問題來了?

        這和我們本次的主題,微前端有什么關(guān)系嗎

        前端的微前端思想其實(shí)同樣來自于此:通過拆分服務(wù),實(shí)現(xiàn)邏輯的解耦。

        2、前端微服務(wù)設(shè)計(jì)

        2.1 為什么前端需要微服務(wù)?

        當(dāng)我們create一個新項(xiàng)目后,想必各位都有以下體會:

        寫項(xiàng)目的第一天:打包 20s

        寫項(xiàng)目的一周:打包 1min

        寫項(xiàng)目的一個月:打包 5min

        之前體驗(yàn)過公司老項(xiàng)目,代碼量非常大,可讀性不高,打包需要10+分鐘。

        隨著項(xiàng)目體量的增加,一個巨大的單體應(yīng)用是難以維護(hù)的,從而導(dǎo)致:開發(fā)效率低、上線困難等一系列問題。

        2.2 微前端的應(yīng)用場景

        對于一個管理系統(tǒng),它的頁面通常是長這個樣子的:

        image.png

        側(cè)邊欄的每一個tab,下面可能還有若干的二級節(jié)點(diǎn)甚至是三級節(jié)點(diǎn),久而久之,這樣的一個管理系統(tǒng),終究也會像前面提到的服務(wù)端一樣,難以維護(hù)。

        如果我們用微前端該如何設(shè)計(jì)呢?

        每一個tab就是一個子應(yīng)用,有自己的狀態(tài);自己的作用域;并且單獨(dú)打包發(fā)布。在全局層面只需要用一個主應(yīng)用(master)就可以實(shí)現(xiàn)管理和控制。

        一句話來講就是:應(yīng)用分發(fā)路由->路由分發(fā)應(yīng)用。

        2.3 早期微前端思路——iFrame

        Why not iframe ?

        對于路由分發(fā)應(yīng)用這件事:我們只需要通過iFrame就可以實(shí)現(xiàn)了,當(dāng)點(diǎn)擊不同的tab時,view區(qū)域展示的是iFrame組件,根據(jù)路由動態(tài)的改變iframe的src屬性,那不是so easy?

        它的好處有哪些?

        • 自帶樣式
        • 沙盒機(jī)制(環(huán)境隔離)
        • 前端之間可以相互獨(dú)立運(yùn)行

        那我們?yōu)槭裁礇]有使用iFrame做微前端呢?

        • CSS問題(視窗大小不同步)
        • 子應(yīng)用通信(使用postMessage并不友好)
        • 組件不能共享
        • 使用創(chuàng)建 iframe,可能會對性能或者內(nèi)存造成影響

        微前端的設(shè)計(jì)構(gòu)思:不僅能繼承iframe的優(yōu)點(diǎn),又可以解決它的不足。

        3、微前端核心邏輯

        3.1 子應(yīng)用加載(Loader)

        先來看看微前端的流程:

        image.png

        我們可以達(dá)成的共識是:需要先加載基座(master),再把選擇權(quán)交給主應(yīng)用,由主應(yīng)用根據(jù)注冊過的子應(yīng)用來抉擇加載誰,當(dāng)子應(yīng)用加載成功后,再由vue-router或react-router來根據(jù)路由渲染組件。

        3.1.1 注冊

        如果精簡代碼邏輯,在基座中實(shí)際上只需要做三件事:

        // 假設(shè)我們的微前端框架叫hailuo

        import Hailuo from './lib/index';



        // 1. 聲明子應(yīng)用

        const routers = [

            {

                path'http://localhost:8081',

                activeWhen'/subapp1'

            },

            {

                path'http://localhost:8082',

                activeWhen'/subapp2'

            }

        ];



        // 2. 注冊子應(yīng)用

        Hailuo.registerApps(routers);



        // 3. 運(yùn)行微前端

        Hailuo.run();

        注冊非常好理解,用一個數(shù)組維護(hù)所有已經(jīng)注冊了的子應(yīng)用:

            registerApps(routers: Router[]) {

                (routers || []).forEach((r) => {

                    this.Apps.push({

                        entry: r.path,

                        activeRule(location) => (location.href.indexOf(r.activeWhen) !== -1)

                    });

                });

            }

        3.1.2 攔截

        我們需要通過攔截注冊路由事件以保證主/子應(yīng)用的邏輯處理時機(jī)。

        import Hailuo from ".";



        // 需要攔截的實(shí)踐

        const EVENTS_NAME = ['hashchange''popstate'];

        // 實(shí)踐收集

        const EVENTS_STACKS = {

            hashchange: [],

            popstate: []

        };



        // 基座切換路由后的邏輯

        const handleUrlRoute = (...args) => {

            // 加載對應(yīng)的子應(yīng)用

            Hailuo.loadApp();

            // 執(zhí)行子應(yīng)用路由的方法

            callAllEventListeners(...args);

        };



        export const patch = () => {

            // 1. 先保證基座的事件監(jiān)聽路由的變化

            window.addEventListener('hashchange', handleUrlRoute);

            window.addEventListener('popstate', handleUrlRoute);



            // 2. 重寫addEventListener和removeEventListener

            // 當(dāng)遇到路由事件后:收集到stack中

            // 如果是其他事件:執(zhí)行original事件監(jiān)聽方法

            const originalAddEventListener = window.addEventListener;

            const originalRemoveEventListener = window.removeEventListener;



            window.addEventListener = (name, handler) => {

                if(name && EVENTS_NAME.includes(name) && typeof handler === "function") {

                    EVENTS_STACKS[name].indexOf(handler) === -1 && EVENTS_STACKS[name].push(handler);

                    return;

                }

                return originalAddEventListener.call(this, name, handler);

            };



            window.removeEventListener = (name, handler) => {

                if(name && EVENTS_NAME.includes(name) && typeof handler === "function") {

                    EVENTS_STACKS[name].indexOf(handler) === -1 && 

                    (EVENTS_STACKS[name] = EVENTS_STACKS[name].filter((fn) => (fn !== handler)));

                    return;

                } 

                return originalRemoveEventListener.call(this, name, handler);

            };



            // 手動給pushState和replaceState添加上監(jiān)聽路由變化的能力

            // 有點(diǎn)像vue2中數(shù)組的變異方法

            const createPopStateEvent = (state: any, name: string) => {

                const evt = new PopStateEvent("popstate", { state });

                evt['trigger'] = name;

                return evt;

            };



            const patchUpdateState = (updateState: (data: any, title: string, url?: string)=>voidname: string) => {

                return function({

                    const before = window.location.href;

                    updateState.apply(thisarguments);

                    const after = window.location.href;

                    if(before !== after) {

                        handleUrlRoute(createPopStateEvent(window.history.state, name));

                    }

                };

            }



            window.history.pushState = patchUpdateState(

                window.history.pushState,

                "pushState"

            );

            window.history.replaceState = patchUpdateState(

                window.history.replaceState,

                "replaceState"

            );

        }

        3.1.3 加載

        通過路由可以匹配到符合的子應(yīng)用后,那么該如何將它加載到頁面呢?

        我們知道SPA的html文件只是一個空模板,實(shí)質(zhì)是通過js驅(qū)動的頁面渲染,那么我們把某一個頁面的js文件,全都剪切到另一個html的<script>標(biāo)簽中執(zhí)行,就實(shí)現(xiàn)了A頁面加載B的頁面。

            async loadApp() {

                // 加載對應(yīng)的子應(yīng)用

                const shouldMountApp = this.Apps.filter(this.isActive);

                const app = shouldMountApp[0];

                const subapp = document.getElementById('submodule');

                await fetchUrl(app.entry)

                // 將html渲染到主應(yīng)用里

                .then((text) => {

                    subapp.innerHTML = text;

                });

                // 執(zhí)行 fetch到的js

                const res = await fetchScripts(subapp, app.entry);

                if(res.length) {

                    execScript(res.reduce((t, c) => (t+c), ''));

                } 

            }

        Better實(shí)踐 ——html-entry

        它是一個加載并處理html、js、css的庫。

        它不是去加載一個個的js、css資源,而是去加載微應(yīng)用的入口html。

        • 第一步 :發(fā)送請求,獲取子應(yīng)用入口HTML。
        • 第二步 :處理該html文檔,去掉html、head標(biāo)簽,處理靜態(tài)資源。
        • 第三步 :處理sourceMap;處理js沙箱;找到入口js。
        • 第四步 :獲取子應(yīng)用provider內(nèi)容

        同時,約束了子應(yīng)用提供加載和銷毀函數(shù)(這個結(jié)構(gòu)是不是很眼熟):

        export function provider({ dom, basename, globalData }{



            return {

                render() {

                    ReactDOM.render(

                        <App basename={basename} globalData={globalData} />,

                        dom ? dom.querySelector('#root') : document.querySelector('#root')

                    );

                },

                destroy({ dom }) {

                    if (dom) {

                        ReactDOM.unmountComponentAtNode(dom);

                    }

                },

            };

        }

        3.2 沙箱(Sandbox)

        沙箱是什么:你可以理解為對作用域的一種比喻,在一個沙箱內(nèi),我的任何操作不會對外界產(chǎn)生影響。

        Why we need sandbox?

        當(dāng)我們集成了很多子應(yīng)用到一起后,勢必會出現(xiàn)沖突,如全局變量沖突、樣式?jīng)_突,這些沖突可能會導(dǎo)致應(yīng)用樣式異常,甚至功能不可用。所以想讓微前端達(dá)到生產(chǎn)可用的程度,讓每個子應(yīng)用之間達(dá)到一定程度隔離的沙箱機(jī)制是必不可少的。

        實(shí)現(xiàn)沙箱,最重要的是:控制沙箱的開啟和關(guān)閉。

        3.2.1 快照沙箱

        原理就是運(yùn)行在某一環(huán)境A時,打一個快照,當(dāng)從別的環(huán)境B切換回來的時候,我們通過這個快照就可以立即恢復(fù)之前環(huán)境A時的情況,比如:

        // 切換到環(huán)境A

        window.a = 2;



        // 切換到環(huán)境B

        window.a = 3;



        // 切換到環(huán)境A

        console.log(a);    // 2

        實(shí)現(xiàn)思路,我們假設(shè)有Sandbox這個類:

        class Sandbox {

            private original;

            private mutated;

            sandBoxActive: () => void;

            sandBoxDeactivate: () => void;

        }
        const sandbox = new Sandbox();

        const code = "...";

        sandbox.activate();

        execScript(code);

        sandbox.sandBoxDeactivate();

        來理一下這個邏輯:

        1. 在sandBoxActive的時候,把變量存到original里;
        2. 在sandBoxDeactivate的時候,把當(dāng)前變量和original對比,不同的存到mutated(保存了快照),然后把變量的狀態(tài)恢復(fù)到original;
        3. 當(dāng)該沙箱再次觸發(fā)sandBoxActive,就可以把mutated的變量恢復(fù)到window上,實(shí)現(xiàn)沙箱的切換。

        3.2.2 VM沙箱

        類似于node中的vm模塊(可在 V8 虛擬機(jī)上下文中編譯和運(yùn)行代碼):http://nodejs.cn/api/vm.html#vm_vm_executing_javascript

        快照沙箱的缺點(diǎn)是無法同時支持多個實(shí)例。 但是vm沙箱利用proxy就可以解決這個問題。

        class SandBox {

            execScript(code: string) {

                const varBox = {};

                const fakeWindow = new Proxy(window, {

                    get(target, key) {

                        return varBox[key] || window[key];

                    },

                    set(target, key, value) {

                        varBox[key] = value;

                        return true;

                    }

                })

                const fn = new Function('window', code);

                fn(fakeWindow);

            }

        }



        export default SandBox;



        // 實(shí)現(xiàn)了隔離

        const sandbox = new Sandbox();

        sandbox.execScript(code);



        const sandbox2 = new Sandbox();

        sandbox2.execScript(code2);



        // map

        varBox = {

            'aWindow''...',

            'bWindow''...'

        }

        我們把各個子應(yīng)用的window放到map中,通過proxy代理,當(dāng)訪問時,直接就是訪問到的各個子應(yīng)用的window對象;如果沒有,比如使用window.addEventListener,就會去真正的window中尋找。

        3.2.3 CSS沙箱

        • 前提:webpack在構(gòu)建的時候,最終是通過appendChild去添加style標(biāo)簽到html里的

        解決方案:劫持appendChild,增加namespace。

        ?? 謝謝支持

        以上便是本次分享的全部內(nèi)容,希望對你有所幫助^_^

        喜歡的話別忘了 分享、點(diǎn)贊、收藏 三連哦~。

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

        手機(jī)掃一掃分享

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

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        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>
            15—17女人毛片 chinese高潮videos | A级免费电影 | 美女撒尿不遮挡免费 | 成人电影在线观看网址 | 国产自产才c区 | 婷婷色六月天 | 毛片强奷女兵 | 十八禁美女| 色婷婷AV一区二区三区软件 | 精品视频一区二区三区四区乐趣播 |