您好,登錄后才能下訂單哦!
0、前言
當一個公司有多個開發(fā)團隊時,我們可能會遇到這樣一些問題:
1.技術(shù)選項雜亂,大家各玩各
2.業(yè)務重復度高,各種通用api,登錄注銷,權(quán)限管理都需要重復實現(xiàn)(甚至一個團隊都需要重復實現(xiàn))
3.業(yè)務壁壘,業(yè)務之間的互通變得比較麻煩
4.部署方式復雜,多個域名(或IP地址)訪問,給用戶造成較大的記憶難度
5.多套系統(tǒng),風格難以統(tǒng)一
6.等等...
當然,解決方式有不少。以下就來講解下我們這邊的一種解決方案。
1、思路
Angualr
Angular(注:非AngularJS) 是流行的前端 MVVM 框架之一,配合 TypeScript,非常適合用來做后臺管理系統(tǒng)。由于我們曾今的一套 Angularjs 開發(fā)框架,我們繼續(xù)選擇 Angular 來進行實現(xiàn),并盡可能的兼容 AngularJS 的模塊。
SPA
選 SPA 還是多頁?多余 Mvvm 來說,多頁并不是標配。而且多頁開發(fā)中,我們勢必會關(guān)注更多的內(nèi)容,包括通用header,footer,而不僅僅是頁面的核心內(nèi)容。
模塊化
為什么要模塊化呢?當有多個團隊開發(fā)時(或者項目較大時),我們希望各個團隊開發(fā)出來的東西都是 模塊(不僅限于JS模塊),這樣可以讓我們獨立發(fā)布、更新、刪除模塊,也能讓我們的關(guān)注點集中在特定模塊下,提高開發(fā)效率和可維護性。
平臺化
我們需要有一個運行平臺(Website站點),允許在里面運行指定的模塊。這樣就可以實現(xiàn)單一入口,也容易實現(xiàn)通用邏輯,模塊共享機制等等。
兼容 AngularJS 模塊
在考慮將框架切換到 Angular 時,我們無可避免的會遇到如何兼容當前已有模塊的問題。大致可選的方案如下:
1.參考 AngualrJS -> Angular 官方升級指南,一步步將模塊切換為 Angular 的實現(xiàn)。(工作量大,需要開發(fā)團隊調(diào)整很多東西)
2.iframe嵌入,會有一定的體驗差異,但對開發(fā)團隊來說,基本無縫升級,也不需要做什么改動。(無疑,我們選擇了這套方案)
模塊打包
我們需要將單個的模塊打包為資源包,進行更新。這樣才能做到模塊獨立發(fā)布,及時生效。
CSS沖突
在大型 SPA 中,CSS沖突是很大的一個問題。我們期望通過技術(shù)手段,能夠根據(jù)當前使用的模塊,加載和卸載CSS。
跨頁面共享數(shù)據(jù)
由于涉及到iframe兼容舊有模塊,我們無可避免,需要考慮跨窗口的頁面共享。
公共模塊
當一個團隊的模塊較多時,就會有一些公共的東西被抽取出來,這個過程,框架是無法知道的,所以這個時候,我們就需要考慮支持公共模塊。(模塊之間也有依賴關(guān)系)
3、實現(xiàn)
基于以上的一些思考,我們首先需要實現(xiàn)一個基礎(chǔ)的平臺網(wǎng)站,這個沒什么難度,直接用 Angular 實現(xiàn)即可。有了這一套東西,我們的登錄注銷,基本的菜單權(quán)限管理,也就實現(xiàn)了。
在這個基礎(chǔ)之上,我們也能實現(xiàn)公共服務、公共組件了(封裝一系列常用的玩意)。
如何模塊化?如何打包?
注意:此模塊并非Angular本身的模塊。 我們通過約定,在 modules/ 下的每一個目錄都是一個業(yè)務模塊。一個業(yè)務模塊一般會包含,靜態(tài)資源、CSS以及JS。根據(jù)這個思路,我們的打包策略就是:遍歷 modules/ 的所有目錄,對每一個目錄進行單獨打包(webpack多entry打包+CSS抽?。?,另外使用 gulp 來處理相關(guān)的靜態(tài)資源(在我看來,gulp才是構(gòu)建工具,webpack是打包工具,所以混合使用,物盡其用)。
一般來說,webpack 會把所有相關(guān)依賴打包在一起,A、B 模塊都依賴了 @angular/core 識別會重復打包,而且框架中,也已經(jīng)打包了 @angular 相關(guān)組件。這個時候,常規(guī)的打包配置就不太合適了。那該如何做呢?
考慮到 Angular 也提供了 CDN 版本,所以我們將 Angular 的組件通過文件合并,作為全局全量訪問,如 ng.core、ng.common 等。
既然這樣,那我們打包的時候,就可以利用 webpack 的 externals 功能,把相關(guān)依賴替換為全局變量。
externals: [{ 'rxjs': 'Rx', '@angular/common': 'ng.common', '@angular/compiler': 'ng.compiler', '@angular/core': 'ng.core', '@angular/http': 'ng.http', '@angular/platform-browser': 'ng.platformBrowser', '@angular/platform-browser-dynamic': 'ng.platformBrowserDynamic', '@angular/router': 'ng.router', '@angular/forms': 'ng.forms', '@angular/animations': 'ng.animations' }
這樣處理之后,我們打包后的文件,也就不會有 Angular 框架代碼了。
注:這個對引入資源的方式也有一定要求,就不能直接引入內(nèi)層資源了。
如何動態(tài)加載模塊
打包完成之后,這個時候就要考慮平臺如何加載這些模塊了(發(fā)布過程就不說了,放到指定位置即可)。
什么時候決定加載模塊呢?其實是訪問特定路由的時候,所以我們的頂級路由,會使用Promise方法來實現(xiàn),如下:
const loadModule = (moduleName) => { return () => { return ModuleLoaderService.load(moduleName); }; }; const dynamicRoutes = []; modules.forEach(item => { dynamicRoutes.push({ path: item.path, canActivate: [AuthGuard], canActivateChild: [AuthGuard], loadChildren: loadModule(item.module) }); }); const appRoutes: Routes = [{ path: 'login', component: LoginComponent }, { path: 'logout', component: LogoutComponent }, { path: '', component: LayoutComponent, canActivate: [AuthGuard], children: [ { path: '', component: HomeComponent }, ...dynamicRoutes, { path: '**', component: NotFoundComponent }, ] }];
我們把每個模塊,按照 umd 的格式進行打包。然后再需要使用該模塊的時候,使用動態(tài)構(gòu)建 script 來運行腳本。
load(moduleName, isDepModule = false): Promise<any> { let module = window['xxx'][moduleName]; if (module) { return Promise.resolve(module); } return new Promise((resolve, reject) => { let path = `${root}${moduleName}/app.js?rnd=${Math.random()}`; this._loadCss(moduleName); this.http.get(path) .toPromise() .then(res => { let code = res.text(); this._DomEval(code); return window['xxx'][moduleName]; }) .then(mod => { window['xxx'][moduleName] = mod; let AppModule = mod.AppModule; // route change will call useModuleStyles function. // this.useModuleStyles(moduleName, isDepModule); resolve(AppModule); }) .catch(err => { console.error('Load module failed: ', err); resolve(EmptyModule); }); }); } // 取自jQuery _DomEval(code, doc?) { doc = doc || document; let script = doc.createElement('script'); script.text = code; doc.head.appendChild(script).parentNode.removeChild(script); }
CSS的動態(tài)加載相對比較簡單,代碼如下:
_loadCss(moduleName: string): void { let cssPath = `${root}${moduleName}/app.css?rnd=${Math.random()}`; let link = document.createElement('link'); link.setAttribute('rel', 'stylesheet'); link.setAttribute('href', cssPath); link.setAttribute('class', `xxx-module-style ${moduleName}`); document.querySelector('head').appendChild(link); }
為了能夠在模塊切換時卸載,還需要提供一個方法,供路由切換時使用:
useModuleStyles(moduleName: string): void { let xxxModuleStyles = [].slice.apply(document.querySelectorAll('.xxx-module-style')); let moduleDeps = this._getModuleAndDeps(moduleName); moduleDeps.push(moduleName); xxxModuleStyles.forEach(link => { let disabled = true; for (let i = moduleDeps.length - 1; i >= 0; i--) { if (link.className.indexOf(moduleDeps[i]) >= 0) { disabled = false; moduleDeps.splice(i, 1); break; } } link.disabled = disabled; }); }
公共模塊依賴
為了處理模塊依賴,我們可以借鑒 AMD規(guī)范 以及使用 requirejs 作為加載器。當前在我的實現(xiàn)里,是自定義了一套加載器,后期應該會切換到 AMD 規(guī)范上去。
如何兼容 AngularJS
模塊?
為了兼容 AngularJS 的模塊,我們引入了 iframe, iframe會先加載一套曾今的 AngularJS 宿主,然后再這個宿主中,運行 AngularJS 模塊。為了實現(xiàn)通信,我們需要兩套平臺程序中,都引入一個基于 postMessage 實現(xiàn)的跨窗口通信庫(因為默認跨域,所以用postMessage實現(xiàn)),有了它之后,我們就可以很方便的兩邊通信了。
AOT編譯
按照 Angular 官方的 Aot 編譯流程即可。
多Tab頁
在后臺系統(tǒng)中,多Tab頁是比較常用了。但是多Tab頁,在單頁中使用,會有一定的性能風險,這個依據(jù)實際的情況,進行使用。實現(xiàn)多Tab頁的核心就是如何動態(tài)加載組件以及如何獲取到要加載的組件。
多Tab頁面,實際就是一個 Tabset 組件,只是在 tab-item 的實現(xiàn)稍顯特別一些,相關(guān)動態(tài)加載的源碼:
@ViewChild('dynamicComponentContainer', { read: ViewContainerRef }) dynamicComponentContainer: ViewContainerRef; constructor( private elementRef: ElementRef, private renderer: Renderer2, private tabset: TabsetComponent, private resolver: ComponentFactoryResolver, private parentContexts: ChildrenOutletContexts ) { } public destroy() { let el = this.elementRef.nativeElement as HTMLElement; // tslint:disable-next-line:no-unused-expression el.parentNode && (el.parentNode.removeChild(el)); } private loadComponent(component: any) { let context = this.parentContexts.getContext(PRIMARY_OUTLET); let injector = ReflectiveInjector.fromResolvedProviders([], this.dynamicComponentContainer.injector); const resolver = context.resolver || this.resolver; let factory = resolver.resolveComponentFactory(component); // let componentIns = factory.create(injector); // this.dynamicComponentContainer.insert(componentIns.hostView); this.dynamicComponentContainer.createComponent(factory); }
注意:要考慮組件卸載方法,如 destroy()
為了獲取到當前要渲染的組件,我們可以借用路由來抓取:
this.router.events.subscribe(evt => { if (evt instanceof NavigationEnd) { let pageComponent; let pageName; try { let nextRoute = this.route.children[0].children[0]; pageName = this.location.path(); pageComponent = nextRoute.component; } catch (e) { pageName = '$$notfound'; pageComponent = NotFoundComponent; } let idx = this.pageList.length + 1; if (!this.pageList.find(x => x.name === pageName)) { this.pageList.push({ header: `頁面${idx}`, comp: pageComponent, name: pageName, closable: true }); } setTimeout(() => { this.selectedPage = pageName; }); } });
3、總結(jié)
以上就是大概的實現(xiàn)思路以及部分相關(guān)的細節(jié)。其他細節(jié)就需要根據(jù)實際的情況,進行酌情處理。
該思路并不僅限于 Angular 框架,使用 Vue、React 也可以做到類似的效果。同時,這套東西也比較適合中小企業(yè)的后臺平臺(不一定非要多團隊,一個團隊按模塊開發(fā)也是不錯的)。
如需要了解更多細節(jié),可以參考:ngx-modular-platform,能給個 star 就更好了。
本文github地址
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。