前端架構概覽
思考:我們有什麼,我們缺什麼?
前端架構分為很多部分,在每個不同的項目裏都會有各自的特點。所以,當我們想優化一個大型項目的時候,可以從一個概覽圖來入手分析,比如下圖:
從我自己的項目特點來分析,我們的基礎設施比較完備,一些公共的基礎服務都可以嘗試接入,唯獨業務代碼異常混亂。
原因:由於業務迭代頻繁,接手的人多,導致組件規範不好、公共方法沒有抽離。而且各個業務之間代碼耦合性很強,看似沒關聯的業務,內部代碼之間卻互相調用。長此以往,這個項目必定難以維護,新的需求迭代只會越做越慢,最後無人願意接手。
那我的切入點就先從這代碼結構開始,vuejs框架既然這麼自由,那麼我們就能創造更多的屬於自己業務特點的東西。
改造一,面向服務設計
前端如何有服務?
定義
所謂服務,是指軟件功能的獨立單元,其設計意圖是完成特定的任務。其中包含了執行完整、離散的業務功能所需的代碼和數據集成,並且可以遠程訪問、進行交互或獨立更新。
以往痛點
前端的公共代碼往往最多放在一個公共目錄,甚至有些還分散在不同業務頁面裏,不斷的被複制、粘貼,導致代碼複用率很低。並且沒有版本可以追蹤修改歷史,導致產生潛在的全局影響。
借鑑點
Chrome團隊的面向服務架構設計(SOA)。
原來的各種模塊會被重構成獨立的服務(Service),每個服務(Service)都可以在獨立的進程中運行,訪問服務(Service)必須使用定義好的接口,通過 IPC 來通信,從而構建一個更內聚、鬆耦合、易於維護和擴展的系統,更好實現 Chrome 簡單、穩定、高速、安全的目標!
示意圖:
結合點
前端的獨立模塊可以用npm包來實現,每個npm包負責一個獨立的公共功能,自帶版本管理和獨立更新。package.json定義出口文件index.js,其中定義此模塊提供的功能接口和代碼文件。接口調用通過export、import方式實現,可以全局或按需加載。
示意圖:
改造效果
目錄結構:
調用方式:
import { ServiceStorage } from 'hc-services'
ServiceStorage.setItem('userId', userId)
改造二,業務模塊化
前端如何有模塊?
定義
模塊化是指解決一個複雜問題時自頂向下逐層把系統劃分成若干模塊的過程。每個模塊完成一個特定的子功能,所有的模塊按某種方式組裝起來,成為一個整體,完成整個系統所要求的功能。
以往痛點
前端的業務代碼很容易形成低內聚、高耦合的穿插形式,難以維護,可讀性差。加上vuejs選項式開發導致邏輯關注點的碎片化,以及mixin導致的方法名、變量名在子組件中的混淆。隨着項目規模變大,幾年之後就會出現難以維護,無人敢碰,只能重構的局面。
借鑑點
Symfony的bundle系統(一套可複用的PHP組件)。
bundle類似於其他軟件中的插件,但卻更好。關鍵區別在於:Symfony中的每一樣東西都是bundle,包括框架核心功能,以及你編寫的程序代碼。bundle是Symfony體系中的一等公民。一個bundle,就是一組結構化的文件,存於一個“用於實現某個獨立功能”的目錄中。每個目錄都包含着關乎那個功能的所有東西,包括php文件,模板,css,js文件,tests,以及其他。
示意圖:
結合點
基於vue的multi-page模式,每個page是一個完全獨立的業務模塊(App),獨立編譯部署。每個業務模塊中再細分子業務模塊subApp,每個subApp自帶vuejs框架的store、router、mixins等關乎這個業務的所有東西。編譯時將subApp等子模塊組裝到一起,從而實現一個獨立的業務功能。這樣就能實現業務代碼的解耦,讓各個子模塊功能更內聚,更易於維護和擴展。
示意圖:
改造效果
subApp的接口文件index.js:
// index.js,subApp的出口文件
import { HcSubApp } from 'hc-micro-pages';
import store from './store';
import routes from './routes';
import { jump2IsvPage, jump2CreateCard } from './libs/utils';
const subApp = new HcSubApp();
subApp.store = store; // vuex數據註冊
subApp.routes = routes; // vue router路由註冊
subApp.routePrefix = 'inquiry'; // 命名空間隔離
subApp.storeModule = 'inquiry'; // 命名空間隔離
// 輸出subApp對象,供融合使用
export default subApp;
// 輸出對外接口,供其他subApp調用
export { jump2IsvPage, jump2CreateCard };
subApp的熱插拔註冊:
/**
* SubApp 註冊
* 這裏實現的是SubApp的熱插拔
*/
import SubAppInquiry from './inquiry';
import SubAppCollect from './collect';
import SubAppInoculate from './inoculate';
import SubAppDoctor from './doctor';
export default {
SubAppInquiry,
SubAppCollect,
SubAppInoculate,
SubAppDoctor,
};
subApps的聚合
import { HcBaseApp, HcSubAppComposer } from 'hc-micro-pages';
import App from './App.vue';
// 引入subApps
import subApps from './subApps';
// 融合所有subApps
const composer = new HcSubAppComposer(subApps);
composer.install();
// 創建聚合App實例
class MyApp extends HcBaseApp {
beforeStart() {} // 鈎子
afterStart() {} // 鈎子
}
const app = new MyApp({
router: composer.router, // 引入聚合數據
store: composer.store, // 引入聚合數據
App,
});
// 創建Vue實例並掛在dom節點
app.start();
改造三,多包管理模式
為什麼還涉及到多包?公共服務包 + 業務模塊 = ?
新問題
npm包的形式並不適合修改頻繁的項目,總要發佈新版本,會降低開發效率。而且包的代碼還要和業務代碼分離,同時操作兩個項目去寫同一個業務感覺很分裂,也不利於調試。同時業務模塊也缺少一個模塊該有的特性:版本、修改歷史、發版的tag。這樣對灰度發佈、版本回滾、問題追溯都不友好。
借鑑點
lerna,名字來源於希臘神話九頭蛇海德拉(Lernaean Hydra)。 形象的説明它是一個用來管理多包項目的工具(monorepos),多包指的是一個項目內包含多個git、npm包。已經採用lerna管理的庫有很多,比如:Babel、React、Jest、Taro等。
結合點
將服務包和業務模塊合併成一個項目,服務包放到packages目錄,業務模塊放到pages目錄,每個子目錄都初始化成npm包形式,用於版本管理。
並且搭配yarn的workspace特性,每個包、模塊都有自己的版本依賴。
當用lerna初始化項目時,它會將對每個包、模塊的引用改成軟鏈接的形式(如果沒有指定版本;或者指定的版本與本地包的版本一致也會形成軟連接),這樣每次修改都會對其他包實時生效,大大提高開發、調試效率。一旦開發、調試完成,就可以指定具體的包版本並且發佈到npm源,以保證代碼穩定。
而且打包部署時,可以對每個包、模塊根據版本打一個tag(例如:hc-utils@1.0.1),並生成changelog文件,方便代碼回滾和追溯問題。
改造效果
lerna配置
{
"useWorkspaces": true,
"version": "independent",
"npmClient": "yarn",
"packages": [
"src/*",
"src/packages/*",
"src/pages/*",
],
"command": {
"publish": {
"allowBranch": [
"master"
],
"message": "chore(release): publish packages",
"conventionalCommits": true
}
}
}
lerna下的目錄結構
發版前打tag
最終結果及未來
未來的拓展在哪裏?
最終效果,邏輯圖:
缺點
包的權限問題
由於是monorepos項目,意味着你可以修改所有代碼,在實際開發中就會帶來一些麻煩。比如某些成員不小心修改了其他包的代碼,而我們對這些修改可能檢測不到。
好在我們在發版時會對所有包在master分支上進行打tag,如果發現當前MR的需求並不涉及某些改動的包,那我們可以lerna diff一下,看看是不是錯誤代碼。
展望
subApp子模塊不應該僅僅侷限於子頁面範疇,它就像頁面的積木,應該提供更多的能力,只不過基於當前的業務特點,首要解決了頁面多而雜的問題。
其他的可能性就是提供邏輯功能的能力,比如用户登錄模塊(監聽token過期自動攔截路由跳轉)。還有頁面局部功能塊的能力,比如廣告位(根據位置id,請求數據後自動渲染)。