微前端由ThoughtWorks 2016年提出,將后端微服務(wù)的理念應(yīng)用于瀏覽器端,即將 Web 應(yīng)用由單一的單體應(yīng)用轉(zhuǎn)變?yōu)槎鄠€(gè)小型前端應(yīng)用聚合為一的應(yīng)用。

美團(tuán)已經(jīng)是一家擁有幾萬人規(guī)模的大型互聯(lián)網(wǎng)公司,提升整體效率至關(guān)" />

国产成人精品无码青草_亚洲国产美女精品久久久久∴_欧美人与鲁交大毛片免费_国产果冻豆传媒麻婆精东

15158846557 在線咨詢 在線咨詢
15158846557 在線咨詢
所在位置: 首頁 > 營銷資訊 > 網(wǎng)站運(yùn)營 > 用微前端的方式搭建類單頁應(yīng)用

用微前端的方式搭建類單頁應(yīng)用

時(shí)間:2022-08-12 16:57:02 | 來源:網(wǎng)站運(yùn)營

時(shí)間:2022-08-12 16:57:02 來源:網(wǎng)站運(yùn)營

前言




微前端由ThoughtWorks 2016年提出,將后端微服務(wù)的理念應(yīng)用于瀏覽器端,即將 Web 應(yīng)用由單一的單體應(yīng)用轉(zhuǎn)變?yōu)槎鄠€(gè)小型前端應(yīng)用聚合為一的應(yīng)用。

美團(tuán)已經(jīng)是一家擁有幾萬人規(guī)模的大型互聯(lián)網(wǎng)公司,提升整體效率至關(guān)重要,這需要很多內(nèi)部和外部的管理系統(tǒng)來支撐。由于這些系統(tǒng)之間存在大量的連通和交互訴求,因此我們希望能夠按照用戶和使用場景將這些系統(tǒng)匯總成一個(gè)或者幾個(gè)綜合的系統(tǒng)。

我們把這種由多個(gè)微前端聚合出來的單頁應(yīng)用叫做“類單頁應(yīng)用”,美團(tuán)HR系統(tǒng)就是基于這種設(shè)計(jì)實(shí)現(xiàn)的。美團(tuán)HR系統(tǒng)是由30多個(gè)微前端應(yīng)用聚合而成,包含1000多個(gè)頁面,300多個(gè)導(dǎo)航菜單項(xiàng)。對用戶來說,HR系統(tǒng)是一個(gè)單頁應(yīng)用,整個(gè)交互過程非常順暢;對開發(fā)者同學(xué)來說,各個(gè)應(yīng)用均可獨(dú)立開發(fā)、獨(dú)立測試、獨(dú)立發(fā)布,大大提高了開發(fā)效率。

接下來,本文將為大家介紹“微前端構(gòu)建類單頁應(yīng)用”在美團(tuán)HR系統(tǒng)中的一些實(shí)踐。同時(shí)也分享一些我們的思考和經(jīng)驗(yàn),希望能夠?qū)Υ蠹矣兴鶈l(fā)。

HR系統(tǒng)的微前端設(shè)計(jì)

因?yàn)槊缊F(tuán)的HR系統(tǒng)所涉及項(xiàng)目比較多,目前由三個(gè)團(tuán)隊(duì)來負(fù)責(zé)。其中:OA團(tuán)隊(duì)負(fù)責(zé)考勤、合同、流程等功能,HR團(tuán)隊(duì)負(fù)責(zé)入職、轉(zhuǎn)正、調(diào)崗、離職等功能,上海團(tuán)隊(duì)負(fù)責(zé)績效、招聘等功能。這種團(tuán)隊(duì)和功能的劃分模式,使得每個(gè)系統(tǒng)都是相對獨(dú)立的,擁有獨(dú)立的域名、獨(dú)立的UI設(shè)計(jì)、獨(dú)立的技術(shù)棧。但是,這樣會帶來開發(fā)團(tuán)隊(duì)之間職責(zé)劃分不清、用戶體驗(yàn)效果差等問題,所以就迫切需要把HR系統(tǒng)轉(zhuǎn)變成只有一個(gè)域名和一套展示風(fēng)格的系統(tǒng)。

為了滿足公司業(yè)務(wù)發(fā)展的要求,我們做了一個(gè)HR的門戶頁面,把各個(gè)子系統(tǒng)的入口做了鏈接歸攏。然而我們發(fā)現(xiàn)HR門戶的意義非常小,用戶跳轉(zhuǎn)兩次之后,又完全不知道跳到哪里去了。因此我們通過將HR系統(tǒng)整合為一個(gè)應(yīng)用的方式,來解決以上問題。

一般而言,“類單頁應(yīng)用”的實(shí)現(xiàn)方式主要有兩種:

  1. iframe嵌入
  2. 微前端合并類單頁應(yīng)用
其中,iframe嵌入方式是比較容易實(shí)現(xiàn)的,但在實(shí)踐的過程中帶來了如下問題:

考慮到這些問題,iframe嵌入并不能滿足我們的業(yè)務(wù)訴求,所以我們開始用微前端的方式來搭建HR系統(tǒng)。

在這個(gè)微前端的方案里,有幾個(gè)我們必須要解決的問題:

  1. 一個(gè)前端需要對應(yīng)多個(gè)后端
  2. 提供一套應(yīng)用注冊機(jī)制,完成應(yīng)用的無縫整合
  3. 構(gòu)建時(shí)集成應(yīng)用和應(yīng)用獨(dú)立發(fā)布部署
只有解決了以上問題,我們的集成才是有效且真正可落地的,接下來詳細(xì)講解一下這幾個(gè)問題的實(shí)現(xiàn)思路。

一個(gè)前端對應(yīng)多個(gè)后端

HR系統(tǒng)最終線上運(yùn)行的是一個(gè)單頁應(yīng)用,而項(xiàng)目開發(fā)中要求應(yīng)用獨(dú)立,因此我們新建了一個(gè)入口項(xiàng)目,用于整合各個(gè)應(yīng)用。在我們的實(shí)踐中,把這個(gè)項(xiàng)目叫做“Portal項(xiàng)目”或“主項(xiàng)目”,業(yè)務(wù)應(yīng)用叫做“子項(xiàng)目”,整個(gè)項(xiàng)目結(jié)構(gòu)圖如下所示:

“Portal項(xiàng)目”是比較特殊的,在開發(fā)階段是一個(gè)容器,不包含任何業(yè)務(wù),除了提供“子項(xiàng)目”注冊、合并功能外,還可以提供一些系統(tǒng)級公共支持,例如:

“子項(xiàng)目”對外輸出不需要入口HTML頁面,只需要輸出的資源文件即可,資源文件包括js、css、fonts和imgs等。

HR系統(tǒng)在線上運(yùn)行了一個(gè)前端服務(wù)(Node Server),這個(gè)Server用于響應(yīng)用戶登錄、鑒權(quán)、資源的請求。HR系統(tǒng)的數(shù)據(jù)請求并沒有經(jīng)過前端服務(wù)做透傳,而是被Nginx轉(zhuǎn)發(fā)到后端Server上,具體交互如下圖所示:

轉(zhuǎn)發(fā)規(guī)則上限制數(shù)據(jù)請求格式必須是 系統(tǒng)名+Api做前綴 這樣保障了各個(gè)系統(tǒng)之間的請求可以完全隔離。

其中,Nginx的配置示例如下:

server { listen 80; server_name xxx.xx.com; location /project/api/ { set $upstream_name "server.project"; proxy_pass http://$upstream_name; } ... location / { set $upstream_name "web.portal"; proxy_pass http://$upstream_name; }}我們將用戶的統(tǒng)一登錄和認(rèn)證問題交給了SSO,所有的項(xiàng)目的后端Server都要接入SSO校驗(yàn)登錄狀態(tài),從而保障業(yè)務(wù)系統(tǒng)間用戶安全認(rèn)證的一致性。

在項(xiàng)目結(jié)構(gòu)確定以后,應(yīng)用如何進(jìn)行合并呢?因此,我們開始制定了一套應(yīng)用注冊機(jī)制。

應(yīng)用注冊機(jī)制

“Portal項(xiàng)目”提供注冊的接口,“子項(xiàng)目”進(jìn)行注冊,最終聚合成一個(gè)單頁應(yīng)用。在整套機(jī)制中,比較核心的部分是路由注冊機(jī)制,“子項(xiàng)目”的路由應(yīng)該由自己控制,而整個(gè)系統(tǒng)的導(dǎo)航是“Portal項(xiàng)目”提供的。

路由注冊

路由的控制由三部分組成:權(quán)限菜單樹、導(dǎo)航和路由樹,“Portal項(xiàng)目”中封裝一個(gè)組件App,根據(jù)菜單樹和路由樹生成整個(gè)頁面。路由掛載到DOM樹上的代碼如下:

let Router = <Router fetchMenu = {fetchMenuHandle} routes = {routes} app = {App} history = {history} >ReactDOM.render(Router,document.querySelector("#app"));Router是在react-router的基礎(chǔ)上做了一層封裝,通過menu和routes最后生成一個(gè)如下所示的路由樹:

<Router> <Route path="/" component={App}> <Route path="/namespace/xx" component={About} /> <Route path="inbox" component={Inbox}> <Route path="messages/:id" component={Message} /> </Route> </Route> </Router>具體注冊使用了全局的window.app.routes,“Portal項(xiàng)目”從window.app.routes獲取路由,“子項(xiàng)目”把自己需要注冊的路由添加到window.app.routes中,子項(xiàng)目的注冊如下:

let app = window.app = window.app || {}; app.routes = (app.routes || []).concat([{ code:'attendance-record', path: '/attendance-record', component: wrapper(() => async(require('./nodes/attendance-record'), 'kaoqin')),}]);路由合并的同時(shí)也把具體的功能做了引用關(guān)聯(lián),再到構(gòu)建時(shí)就可以把所有的功能與路由管理起來。項(xiàng)目的作用域要怎么控制呢?我們要求“子項(xiàng)目”間是彼此隔離,要避免樣式污染,要做獨(dú)立的數(shù)據(jù)流管理,我們用項(xiàng)目作用域的方式來解決這些問題。

項(xiàng)目作用域控制

在路由控制的時(shí)候我們提到了 window.app,我們也是通過這個(gè)全局App來做項(xiàng)目作用域的控制。window.app包含了如下幾部分:

let app = window.app || {};app = { require:function(request){...}, define:function(name,context,index){...}, routes:[...], init:function(namespace,reducers){...} };window.app主要功能:

子項(xiàng)目完整的注冊,如下所示:

import reducers from './redux/kaoqin-reducer';let app = window.app = window.app || {}; app.routes = (app.routes || []).concat([{ code:'attendance-record', path: '/attendance-record', component: wrapper(() => async(require('./nodes/attendance-record'), 'kaoqin')), // ... 其他路由}]);function wrapper(loadComponent) { let React = null; let Component = null; let Wrapped = props => ( <div className="namespace-kaoqin"> <Component {...props} /> </div> ); return async () => { await window.app.init('namespace-kaoqin',reducers); React = require('react'); Component = await loadComponent(); return Wrapped; };}其中做了這幾件事情:

  1. 把路由添加到window.app中
  2. 業(yè)務(wù)第一次功能被調(diào)用的時(shí)候執(zhí)行 window.app.init(namespace,reducers),注冊項(xiàng)目作用域和數(shù)據(jù)流的reducers
  3. 對業(yè)務(wù)功能的掛載節(jié)點(diǎn)包裝一個(gè)根節(jié)點(diǎn):Component掛載在classNamenamespace-kaoqindiv下面
這樣就完成了“子項(xiàng)目”的注冊,“子項(xiàng)目”的對外輸出是一個(gè)入口文件和一系列的資源文件,這些文件由webpack構(gòu)建生成。

CSS作用域方面,使用webpack在構(gòu)建階段為業(yè)務(wù)的所有CSS都加上自己的作用域,構(gòu)建配置如下:

//webpack打包部分,在postcss插件中 添加namespace的控制config.postcss.push(postcss.plugin('namespace', () => css => css.walkRules(rule => { if (rule.parent && rule.parent.type === 'atrule' && rule.parent.name !== 'media') return; rule.selectors = rule.selectors.map(s => `.namespace-kaoqin ${s === 'body' ? '' : s}`); })));CSS處理用到postcss-loader,postcss-loader用到postcss,我們添加postcss的處理插件,為每一個(gè)CSS選擇器都添加名為.namespace-kaoqin的根選擇器,最后打包出來的CSS,如下所示:

.namespace-kaoqin .attendance-record { height: 100%; position: relative}.namespace-kaoqin .attendance-record .attendance-record-content { font-size: 14px; height: 100%; overflow: auto; padding: 0 20px}... CSS樣式問題解決之后,接下來看一下,Portal提供的init做了哪些工作。

let inited = false;let ModalContainer = null;app.init = async function (namespace,reducers) { if (!inited) { inited = true; let block = await new Promise(resolve => { require.ensure([], function (require) { app.define('block', require.context('block', true, /^/.//(?!dev)([^//]|//(?!demo))+/.jsx?$/)); resolve(require('block')); }, 'common'); }); ModalContainer = document.createElement('div'); document.body.appendChild(mtfv3ModalContainer); let { Modal} = block; Modal.getContainer = () => ModalContainer; } ModalContainer.setAttribute('class', `${namespace}`); mountReducers(namepace,reducers)};init方法主要做了兩件事情:

  1. 掛載“子項(xiàng)目”的reducers,把“子項(xiàng)目”的數(shù)據(jù)流掛載了redux上
  2. “子項(xiàng)目”的彈出窗全部掛載在一個(gè)全局的div上,并為這個(gè)div添加對應(yīng)的項(xiàng)目作用域,配合“子項(xiàng)目”構(gòu)建的CSS,確保彈出框樣式正確
上述代碼中還看到了app.define的用法,它主要是用來處理JS公共庫的控制,例如我們用到的組件庫Block,期望每個(gè)“子項(xiàng)目”的版本都是統(tǒng)一的。因此我們需要解決JS公共庫版本統(tǒng)一的問題。

JS公共庫版本統(tǒng)一

為了不侵入“子項(xiàng)目”,我們采用構(gòu)建過程中替換的方式來做,“Portal項(xiàng)目”把公共庫引入進(jìn)來,重新定義,然后通過window.app.require的方式引用,在編譯“子項(xiàng)目”的時(shí)候,把引用公共庫的代碼從require('react')全部替換為window.app.require('react'),這樣就可以將JS公共庫的版本都交給“Portal項(xiàng)目”來控制了。

define 的代碼和示例如下:

/*** 重新定義包* @param name 引用的包名,例如 react* @param context 資源引用器 實(shí)際上是 webpackContext(是一個(gè)方法,來引用資源文件)* @param index 定義的包的入口文件*/app.define = function (name, context, index) { let keys = context.keys(); for (let key of keys) { let parts = (name + key.slice(1)).split('/'); let dir = this.modules; for (let i = 0; i < parts.length - 1; i++) { let part = parts[i]; if (!dir.hasOwnProperty(part)) { dir[part] = {}; } dir = dir[part]; } dir[parts[parts.length - 1]] = context.bind(context, key); } if (index != null) { this.modules[name]['index.js'] = this.modules[name][index]; }};//定義app的react //定義一個(gè)react資源庫:把原來react根目錄和lib目錄下的.js全部獲取到,綁定到新定義的react中,并指定react.js作為入口文件app.define('react', require.context('react', true, /^.//(lib//)?[^//]+/.js$/), 'react.js');app.define('react-dom', require.context('react-dom', true, /^.//index/.js$/));“子項(xiàng)目”的構(gòu)建,使用webpack的externals(外部擴(kuò)展)來對引用進(jìn)行替換:

/** * 對一些公共包的引用做處理 通過webpack的externals(外部擴(kuò)展)來解決 */const libs = ['react', 'react-dom', "block"];module.exports = function (context, request, callback) { if (libs.indexOf(request.split('/', 1)[0]) !== -1) { //如果文件的require路徑中包含libs中的 替換為 window.app.require('${request}'); //var在這兒是聲明的意思 callback(null, `var window.app.require('${request}')`); } else { callback(); }};這樣項(xiàng)目的注冊就完成了,還有一些需要“子項(xiàng)目”自己改造的地方,例如本地啟動(dòng)需要把“Portal項(xiàng)目”的導(dǎo)航加載進(jìn)來,需要做mock數(shù)據(jù)等等。

項(xiàng)目的注冊完成了,我們?nèi)绾伟l(fā)布部署呢?

構(gòu)建后集成和獨(dú)立部署

在HR系統(tǒng)的整合過程中,開發(fā)階段對“子項(xiàng)目”是“零侵入”,而在發(fā)布階段,我們也希望如此。

我們的部署過程,大概如下:

第一步:在發(fā)布機(jī)上,獲取代碼、安裝依賴、執(zhí)行構(gòu)建;
第二步:把構(gòu)建的結(jié)果上傳到服務(wù)器;
第三步:在服務(wù)器執(zhí)行 node index.js 把服務(wù)啟動(dòng)起來。

“Portal項(xiàng)目”構(gòu)建之后的文件結(jié)構(gòu)如下:

“子項(xiàng)目”構(gòu)建后的文件結(jié)構(gòu)如下:

線上運(yùn)行的文件結(jié)構(gòu)如下:

把“子項(xiàng)目”的構(gòu)建文件上傳到服務(wù)器對應(yīng)的“子項(xiàng)目”文件目錄下,然后對“子項(xiàng)目”的資源文件進(jìn)行集成合并,生成.dist目錄中的文件,提供給用戶線上訪問使用。

每次發(fā)布,我們主要做以下三件事情:

  1. 發(fā)布最新的靜態(tài)資源文件
  2. 重新生成entry-xx.js和index.html(更新入口引用)
  3. 重啟前端服務(wù)
如果是純靜態(tài)服務(wù),完全可以做到熱部署,動(dòng)態(tài)更新一下引用關(guān)系即可,不需要重啟服務(wù)。因?yàn)槲覀冊贜ode服務(wù)層做了一些公共服務(wù),所以選擇了重啟服務(wù),我們使用了公司的基礎(chǔ)服務(wù)和PM2來實(shí)現(xiàn)熱啟動(dòng)。

對于歷史文件,我們需要做版本控制,以保障之前的訪問能夠正常運(yùn)行。此外,為了保證服務(wù)的高可用性,我們上線了4臺機(jī)器,分別在兩個(gè)機(jī)房進(jìn)行部署,最終來提高HR系統(tǒng)的容錯(cuò)性。

總結(jié)

以上就是我們使用React技術(shù)棧和微前端方式搭建的“類單頁應(yīng)用”HR業(yè)務(wù)系統(tǒng),回顧一下這個(gè)技術(shù)方案,整個(gè)框架流程如下圖所示:




在產(chǎn)品層面上,“微前端類單頁應(yīng)用”打破了獨(dú)立項(xiàng)目的概念,我們可以根據(jù)用戶的需求自由組裝我們的頁面應(yīng)用,例如:我們可以在HR門戶上把考勤、請假、OA審批、財(cái)務(wù)報(bào)銷等高頻功能放在一起。甚至可以讓用戶自己定制功能,讓用戶真的感受到我們是一個(gè)系統(tǒng)。

“微前端構(gòu)建類單頁應(yīng)用”方案是基于React技術(shù)棧開發(fā),如果把路由管理機(jī)制和注冊機(jī)制抽離出來作為一個(gè)公共的庫,就可以在webpack的基礎(chǔ)上封裝成一個(gè)業(yè)務(wù)無關(guān)性的通用方案,而且使用起來非常的友好。

截止目前,HR系統(tǒng)已經(jīng)穩(wěn)定運(yùn)行了1年多的時(shí)間,我們總結(jié)了以下三個(gè)優(yōu)點(diǎn):

  1. 單頁應(yīng)用的體驗(yàn)比較好,按需加載,交互流暢
  2. 項(xiàng)目微前端化,業(yè)務(wù)解耦,穩(wěn)定性有保障,項(xiàng)目的粒度易控制
  3. 項(xiàng)目的健壯性比較好,項(xiàng)目注冊僅僅增加了入口文件的大小,30多個(gè)項(xiàng)目目前只有12K

作者簡介

賈召,2014年加入美團(tuán),先后主導(dǎo)了OA、HR、財(cái)務(wù)等企業(yè)項(xiàng)目的前端搭建,自主研發(fā)React組件庫Block,在Block的基礎(chǔ)上統(tǒng)一了整個(gè)企業(yè)平臺的前端技術(shù)棧,致力于提高研發(fā)團(tuán)隊(duì)的工作效率。

如果想跟作者零距離交流,歡迎加入美團(tuán)點(diǎn)評前端技術(shù)俱樂部,請加美美的微信(微信號:MTDPtech01),回復(fù):Web,美美就會自動(dòng)拉你進(jìn)群。

---------- END ----------

也許你還想看

WMRouter:美團(tuán)外賣Android開源路由框架

Flutter的原理及美團(tuán)的實(shí)踐

Jenkins的Pipeline腳本在美團(tuán)餐飲SaaS中的實(shí)踐

http://weixin.qq.com/r/9HVSSg3EOFBHrUkp9yDm (二維碼自動(dòng)識別)



關(guān)鍵詞:端的,方式

74
73
25
news

版權(quán)所有? 億企邦 1997-2025 保留一切法律許可權(quán)利。

為了最佳展示效果,本站不支持IE9及以下版本的瀏覽器,建議您使用谷歌Chrome瀏覽器。 點(diǎn)擊下載Chrome瀏覽器
關(guān)閉