<dfn id="hx5t3"><strike id="hx5t3"><em id="hx5t3"></em></strike></dfn>

    <thead id="hx5t3"></thead><nobr id="hx5t3"><font id="hx5t3"><rp id="hx5t3"></rp></font></nobr>

    <listing id="hx5t3"></listing>

    <var id="hx5t3"></var>
    <big id="hx5t3"></big>

      
      

      <output id="hx5t3"><ruby id="hx5t3"></ruby></output>
      <menuitem id="hx5t3"><dfn id="hx5t3"></dfn></menuitem>

      <big id="hx5t3"></big>

        帶你入門前端工程(十一):微前端

        譚光志

        什么是微服務?先看看維基百科的定義:

        微服務(英語:Microservices)是一種軟件架構風格,它是以專注于單一責任與功能的小型功能區塊 (Small Building Blocks) 為基礎,利用模塊化的方式組合出復雜的大型應用程序,各功能區塊使用與語言無關 (Language-Independent/Language agnostic)的API集相互通信。

        換句話說,就是將一個大型、復雜的應用分解成幾個服務,每個服務就像是一個組件,組合起來一起構建成整個應用。

        想象一下,一個上百個功能、數十萬行代碼的應用維護起來是個什么場景?

        1. 牽一發而動全身,僅僅修改一處代碼,就需要重新部署整個應用。經常有“修改一分鐘,編譯半小時”的情況發生。
        2. 代碼模塊錯綜復雜,互相依賴。更改一處地方的代碼,往往會影響到應用的其他功能。

        如果使用微服務來重構整個應用有什么好處?

        一個應用分解成多個服務,每個服務獨自服務內部的功能。例如原來的應用有 abcd 四個頁面,現在分解成兩個服務,第一個服務有 ab 兩個頁面,第二個服務有 cd 兩個頁面,組合在一起就和原來的應用一樣。

        當應用其中一個服務出故障時,其他服務仍可以正常訪問。例如第一個服務出故障了, ab 頁面將無法訪問,但 cd 頁面仍能正常訪問。

        好處:不同的服務獨立運行,服務與服務之間解耦。我們可以把服務理解成組件,就像本小書第 3 章《前端組件化》中所說的一樣。每個服務可以獨自管理,修改一個服務不影響整體應用的運行,只影響該服務提供的功能。

        另外在開發時也可以快速的添加、刪除功能。例如電商網站,在不同的節假日時推出的活動頁面,活動過后馬上就可以刪掉。

        難點:不容易確認服務的邊界。當一個應用功能太多時,往往多個功能點之間的關聯會比較深。因而就很難確定這一個功能應該歸屬于哪個服務。

        PS:微前端就是微服務在前端的應用,也就是前端微服務。

        微服務實踐

        現在我們將使用微前端框架 qiankun 來構建一個微前端應用。之所以選用 qiankun 框架,是因為它有以下幾個優點:

        • 技術棧無關,任何技術棧的應用都能接入。
        • 樣式隔離,子應用之間的樣式互不干擾。
        • 子應用的 JavaScript 作用域互相隔離。
        • 資源預加載,在瀏覽器空閑時間預加載未打開的微應用資源,加速微應用打開速度。

        樣式隔離

        樣式隔離的原理是:每次切換子應用時,都會加載該子應用對應的 css 文件。同時會把原先的子應用樣式文件移除掉,這樣就達到了樣式隔離的效果。

        我們可以自己模擬一下這個效果:

        <!-- index.html -->
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>Document</title>
            <link rel="stylesheet" href="index.css">
        <body>
            <div>移除樣式文件后將不會變色</div>
        </body>
        </html>
        /* index.css */
        body {
            color: red;
        }

        現在我們加一段 JavaScript 代碼,在加載完樣式文件后再將樣式文件移除掉:

        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>Document</title>
            <link rel="stylesheet" href="index.css">
        <body>
            <div>移除樣式文件后將不會變色</div>
            <script>
                setTimeout(() => {
                    const link = document.querySelector('link')
                    link.parentNode.removeChild(link)
                }, 3000)
            </script>
        </body>
        </html>

        這時再打開頁面看一下,可以發現 3 秒后字體樣式就沒有了。

        JavaScript 作用域隔離

        主應用在切換子應用之前會記錄當前的全局狀態,然后在切出子應用之后恢復全局狀態。假設當前的全局狀態如下所示:

        const global = { a: 1 }

        在進入子應用之后,無論全局狀態如何變化,將來切出子應用時都會恢復到原先的全局狀態:

        // global
        { a: 1 }

        官方還提供了一張圖來幫助我們理解這個機制:

        好了,現在我們來創建一個微前端應用吧。這個微前端應用由三部分組成:

        • main:主應用,使用 vue-cli 創建。
        • vue:子應用,使用 vue-cli 創建。
        • react: 子應用,使用的 react 16 版本。

        對應的目錄如下:

        -main
        -vue
        -react

        創建主應用

        我們使用 vue-cli 創建主應用(然后執行 npm i qiankun 安裝 qiankun 框架):

        vue create main

        如果主應用只是起到一個基座的作用,即只用于切換子應用。那可以不需要安裝 vue-router 和 vuex。

        改造 App.vue 文件

        主應用必須提供一個能夠安裝子應用的元素,所以我們需要將 App.vue 文件改造一下:

        <template>
            <div class="mainapp">
                <!-- 標題欄 -->
                <header class="mainapp-header">
                    <h1>QianKun</h1>
                </header>
                <div class="mainapp-main">
                    <!-- 側邊欄 -->
                    <ul class="mainapp-sidemenu">
                        <li @click="push('/vue')">Vue</li>
                        <li @click="push('/react')">React</li>
                    </ul>
                    <!-- 子應用  -->
                    <main class="subapp-container">
                        <h4 v-if="loading" class="subapp-loading">Loading...</h4>
                        <div id="subapp-viewport"></div>
                    </main>
                </div>
            </div>
        </template>
        
        <script>
        export default {
            name: 'App',
            props: {
                loading: Boolean,
            },
            methods: {
                push(subapp) { history.pushState(null, subapp, subapp) }
            }
        }
        </script>

        可以看到我們用于安裝子應用的元素為 #subapp-viewport,另外還有切換子應用的功能:

        <!-- 側邊欄 -->
        <ul class="mainapp-sidemenu">
            <li @click="push('/vue')">Vue</li>
            <li @click="push('/react')">React</li>
        </ul>

        改造 main.js

        根據 qiankun 文檔說明,需要使用 registerMicroApps()start() 方法注冊子應用及啟動主應用:

        import { registerMicroApps, start } from 'qiankun';
        registerMicroApps([
          {
            name: 'react app', // app name registered
            entry: '//localhost:7100',
            container: '#yourContainer',
            activeRule: '/yourActiveRule',
          },
          {
            name: 'vue app',
            entry: { scripts: ['//localhost:7100/main.js'] },
            container: '#yourContainer2',
            activeRule: '/yourActiveRule2',
          },
        ]);
        start();

        所以現在需要將 main.js 文件改造一下:

        import Vue from 'vue'
        import App from './App'
        import { registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start, initGlobalState } from 'qiankun'
        
        let app = null
        
        function render({ loading }) {
            if (!app) {
                app = new Vue({
                    el: '#app',
                    data() {
                        return {
                            loading,
                        }
                    },
                    render(h) {
                        return h(App, {
                            props: {
                                loading: this.loading
                            }
                        })
                    }
                });
            } else {
                app.loading = loading
            }
        }
        
        /**
         * Step1 初始化應用(可選)
         */
        render({ loading: true })
        
        const loader = (loading) => render({ loading })
        
        /**
         * Step2 注冊子應用
         */
        
        registerMicroApps(
            [
                {
                    name: 'vue', // 子應用名稱
                    entry: '//localhost:8001', // 子應用入口地址
                    container: '#subapp-viewport',
                    loader,
                    activeRule: '/vue', // 子應用觸發路由
                },
                {
                    name: 'react',
                    entry: '//localhost:8002',
                    container: '#subapp-viewport',
                    loader,
                    activeRule: '/react',
                },
            ],
            // 子應用生命周期事件
            {
                beforeLoad: [
                    app => {
                        console.log('[LifeCycle] before load %c%s', 'color: green', app.name)
                    },
                ],
                beforeMount: [
                    app => {
                        console.log('[LifeCycle] before mount %c%s', 'color: green', app.name)
                    },
                ],
                afterUnmount: [
                    app => {
                        console.log('[LifeCycle] after unmount %c%s', 'color: green', app.name)
                    },
                ],
            },
        )
        
        // 定義全局狀態,可以在主應用、子應用中使用
        const { onGlobalStateChange, setGlobalState } = initGlobalState({
            user: 'qiankun',
        })
        
        // 監聽全局狀態變化
        onGlobalStateChange((value, prev) => console.log('[onGlobalStateChange - master]:', value, prev))
        
        // 設置全局狀態
        setGlobalState({
            ignore: 'master',
            user: {
                name: 'master',
            },
        })
        
        /**
         * Step3 設置默認進入的子應用
         */
        setDefaultMountApp('/vue')
        
        /**
         * Step4 啟動應用
         */
        start()
        
        runAfterFirstMounted(() => {
            console.log('[MainApp] first app mounted')
        })
        

        這里有幾個注意事項要注意一下:

        1. 子應用的名稱 name 必須和子應用下的 package.json 文件中的 name 一樣。
        2. 每個子應用都有一個 loader() 方法,這是為了應對用戶直接從子應用路由進入頁面的情況而設的。進入子頁面時判斷一下是否加載了主應用,沒有則加載,有則跳過。
        3. 為了防止在切換子應用時顯示空白頁面,應該提供一個 loading 配置。
        4. 設置子應用的入口地址時,直接填入子應用的訪問地址。

        更改訪問端口

        vue-cli 的默認訪問端口一般為 8080,為了和子應用保持一致,需要將主應用端口改為 8000(子應用分別為 8001、8002)。創建 vue.config.js 文件,將訪問端口改為 8000:

        module.exports = {
            devServer: {
                port: 8000,
            }
        }

        至此,主應用就已經改造完了。

        創建子應用

        子應用不需要引入 qiankun 依賴,只需要暴露出幾個生命周期函數就可以:

        1. bootstrap,子應用首次啟動時觸發。
        2. mount,子應用每次啟動時都會觸發。
        3. unmount,子應用切換/卸載時觸發。

        現在將子應用的 main.js 文件改造一下:

        import Vue from 'vue'
        import VueRouter from 'vue-router'
        import App from './App.vue'
        import routes from './router'
        import store from './store'
        
        Vue.config.productionTip = false
        
        let router = null
        let instance = null
        
        function render(props = {}) {
            const { container } = props
            router = new VueRouter({
                // hash 模式不需要下面兩行
                base: window.__POWERED_BY_QIANKUN__ ? '/vue' : '/',
                mode: 'history',
                routes,
            })
        
            instance = new Vue({
                router,
                store,
                render: h => h(App),
            }).$mount(container ? container.querySelector('#app') : '#app')
        }
        
        if (window.__POWERED_BY_QIANKUN__) {
            // eslint-disable-next-line no-undef
            __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
        } else {
            render()
        }
        
        function storeTest(props) {
            props.onGlobalStateChange &&
                props.onGlobalStateChange(
                    (value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev),
                    true,
                )
            props.setGlobalState &&
                props.setGlobalState({
                    ignore: props.name,
                    user: {
                        name: props.name,
                    },
                })
        }
        
        export async function bootstrap() {
            console.log('[vue] vue app bootstraped')
        }
        
        export async function mount(props) {
            console.log('[vue] props from main framework', props)
            storeTest(props)
            render(props)
        }
        
        export async function unmount() {
            instance.$destroy()
            instance.$el.innerHTML = ''
            instance = null
            router = null
        }

        可以看到在文件的最后暴露出了 bootstrap mount unmount 三個生命周期函數。另外在掛載子應用時還需要注意一下,子應用是在主應用下運行還是自己獨立運行:container ? container.querySelector('#app') : '#app'。

        配置打包項

        根據 qiankun 文檔提示,需要對子應用的打包配置項作如下更改:

        const packageName = require('./package.json').name;
        module.exports = {
          output: {
            library: `${packageName}-[name]`,
            libraryTarget: 'umd',
            jsonpFunction: `webpackJsonp_${packageName}`,
          },
        };

        所以現在我們還需要在子應用目錄下創建 vue.config.js 文件,輸入以下代碼:

        // vue.config.js
        const { name } = require('./package.json')
        
        module.exports = {
            configureWebpack: {
                output: {
                    // 把子應用打包成 umd 庫格式
                    library: `${name}-[name]`,
                    libraryTarget: 'umd',
                    jsonpFunction: `webpackJsonp_${name}`
                }
            },
            devServer: {
                port: 8001,
                headers: {
                    'Access-Control-Allow-Origin': '*'
                }
            }
        }

        vue.config.js 文件有幾個注意事項:

        1. 主應用、子應用運行在不同端口下,所以需要設置跨域頭 'Access-Control-Allow-Origin': '*'。
        2. 由于在主應用配置了 vue 子應用需要運行在 8001 端口下,所以也需要在 devServer 里更改端口。

        另外一個子應用 react 的改造方法和 vue 是一樣的,所以在此不再贅述。

        部署

        我們將使用 express 來部署項目,除了需要在子應用設置跨域外,沒什么需要特別注意的地方。

        主應用服務器文件 main-server.js

        const fs = require('fs')
        const express = require('express')
        const app = express()
        const port = 8000
        
        app.use(express.static('main-static'))
        
        app.get('*', (req, res) => {
            fs.readFile('./main-static/index.html', 'utf-8', (err, html) => {
                res.send(html)
            })
        })
        
        app.listen(port, () => {
            console.log(`main app listening at http://localhost:${port}`)
        })

        vue 子應用服務器文件 vue-server.js

        const fs = require('fs')
        const express = require('express')
        const app = express()
        const cors = require('cors')
        const port = 8001
        
        // 設置跨域
        app.use(cors())
        app.use(express.static('vue-static'))
        
        app.get('*', (req, res) => {
            fs.readFile('./vue-static/index.html', 'utf-8', (err, html) => {
                res.send(html)
            })
        })
        
        app.listen(port, () => {
            console.log(`vue app listening at http://localhost:${port}`)
        })

        react 子應用服務器文件 react-server.js

        const fs = require('fs')
        const express = require('express')
        const app = express()
        const cors = require('cors')
        const port = 8002
        
        // 設置跨域
        app.use(cors())
        app.use(express.static('react-static'))
        
        app.get('*', (req, res) => {
            fs.readFile('./react-static/index.html', 'utf-8', (err, html) => {
                res.send(html)
            })
        })
        
        app.listen(port, () => {
            console.log(`react app listening at http://localhost:${port}`)
        })

        另外需要將這三個應用打包后的文件分別放到 main-static、vue-static、react-static 目錄下。然后分別執行命令 node main-server.js、node vue-server.js、node react-server.js 即可查看部署后的頁面?,F在這個項目目錄如下:

        -main
        -main-static // main 主應用靜態文件目錄
        -react
        -react-static // react 子應用靜態文件目錄
        -vue
        -vue-static // vue 子應用靜態文件目錄
        -main-server.js // main 主應用服務器
        -vue-server.js // vue 子應用服務器
        -react-server.js // react 子應用服務器

        我已經將這個微前端應用的代碼上傳到了 github,建議將項目克隆下來配合本章一起閱讀,效果更好。下面放一下 DEMO 的運行效果圖:

        小結

        對于大型應用的開發和維護,使用微前端能讓我們變得更加輕松。不過如果是小應用,建議還是單獨建一個項目開發。畢竟微前端也有額外的開發、維護成本。

        參考資料

        帶你入門前端工程 全文目錄:

        1. 技術選型:如何進行技術選型?
        2. 統一規范:如何制訂規范并利用工具保證規范被嚴格執行?
        3. 前端組件化:什么是模塊化、組件化?
        4. 測試:如何寫單元測試和 E2E(端到端) 測試?
        5. 構建工具:構建工具有哪些?都有哪些功能和優勢?
        6. 自動化部署:如何利用 Jenkins、Github Actions 自動化部署項目?
        7. 前端監控:講解前端監控原理及如何利用 sentry 對項目實行監控。
        8. 性能優化(一):如何檢測網站性能?有哪些實用的性能優化規則?
        9. 性能優化(二):如何檢測網站性能?有哪些實用的性能優化規則?
        10. 重構:為什么做重構?重構有哪些手法?
        11. 微服務:微服務是什么?如何搭建微服務項目?
        12. Severless:Severless 是什么?如何使用 Severless?
        閱讀 3.6k
        5.6k 聲望
        10.3k 粉絲
        0 條評論
        你知道嗎?

        5.6k 聲望
        10.3k 粉絲
        宣傳欄
        一本到在线是免费观看_亚洲2020天天堂在线观看_国产欧美亚洲精品第一页_最好看的2018中文字幕