Angular在國內使用的人並不像國外那麼多,基本都是外企在用,但其框架的思想卻仍能夠爲咱們所借鑑,在某些問題沒有思路的時候能夠參考ng相關的處理,ng處理方式和思惟確實比較超前,但也所以而曲高和寡。本文旨在經過ng全家桶項目(前端Angular10 + 後端NestJS7)的實踐來總結對於ng架構中一些亮點的關注與思考,Angular和Nest在先後端框架的處理上同出一脈,對比起來更有借鑑意義。css
[目錄結構]html
src前端
appvue
loginjava
mainnode
[目錄描述]react
整個前端項目是基於angular腳手架生成的,其基本目錄結構是在src的app下進行相關組件和頁面的模塊開發,main.ts和index.html是整個單頁應用的主入口,根目錄下angular.json用於配置相關的打包編譯等環境配置參數c++
[實踐分享]git
This likely means that the library (@angular/common/http) which declares HttpClientModule has not been processed correctly by ngcc, or is not compatible with Angular Ivy. Check if a newer version of the library is available, and update if so. Also consider checking with the library's authors to see if the library is expected to be compatible with Ivy.
[(ngModal)]
指令,必須在module中引入FormsModuleimport { FormsModule } from '@angular/forms'; @NgModule({ imports: [ FormsModule ] })
.app-message { width: 300px; height: 40px; background: #fff; transition: all 2s none; border: 1px solid #ececec; border-radius: 4px; position: fixed; left: 50%; top: 10px; margin-left: -150px; text-align: center; &-show { animation: a 3s ease-out forwards; animation-direction: alternate; animation-iteration-count: 1; @keyframes a { 0% {opacity: 1;} 100% {opacity: 0;} } } &-hide { opacity: 0; } }
[目錄結構]angularjs
src
api
article
audio
user
dto
auth
filters
exception
guard
interceptors
middlewares
logger
pipes
[目錄描述]
後端項目是基於nestjs框架的大型後臺項目配置,api模塊主要是對外輸出的接口,auth、filters、guard、interceptors、middlewares、pipes等是對於須要的模塊進行統一的收集處理,main.ts是主入口文件,用於啓動及相關配置等,app.module.ts是用來收集全部模塊的導入,ng基於模塊的方式能夠起到很是好的隔離效果
[實踐分享]
首先,對於沒有用過ng的同窗科普一下,angular其實分爲兩個大版本,一個是angular1.x的,也就是ng1,也就是如今還有的angularjs,另外一個版本是ng2之後的版本,ng2以後被谷歌收購後,徹底重寫了框架,惟一和1.x相通的估計也就剩那幾個思想還在了:模塊化、依賴注入、雙向綁定、MVC,對於1.x感興趣的同窗能夠去看Vue的1.x的版本,基本算是簡化版的ng1.x,Vue2以後就和後來的ng分道揚鑣了,vue2主要是以發佈訂閱來替代依賴注入的思路,扯遠了...(ps: 想看ng1版本的能夠看這個地址,竟然還有更新... angularjs官方倉庫),這裏分析的主要是Ng10,ng8以後除了引入Ivy(Ivy架構官方介紹)這個編譯渲染器以外,其實改動不大,主要就是在優化以及廢除和新建一些api等等。Ng的源碼很龐大,goggle自研了一個bazel自動化構建工具,ng天然也是靠這個構建的,對bazel感興趣的同窗,能夠看這個Google軟件構建工具Bazel原理及使用方法介紹,我這裏就不展開全部的源碼,總體的核心大框架以下:
packages
complier (ps: 不展開了,這個編譯部分作的很優秀,本篇講不完,回頭寫編譯器部分專門說吧,尤爲是Ivy那個,後續react的fiber以及vue3的最新的compiler部分都有其影響)
src
core
src
di
reflection
view
nestjs是nodejs的web應用的一個大的集成,它最初是基於express封裝的一個後端框架,後來將服務端各類理念都使用js實現了一下,雖然不能和成熟的服務端語言框架如java等進行媲美,可是服務端所須要的東西基本都具有了,對於有需求想要使用js來開發後端的同窗是個不錯的選擇,我的認爲簡單的bff,好比想本身模擬的開發個後臺接收請求,選擇node直接寫或者使用express、koa就能夠,對於有必定的中間層給前端處理,能夠選用阿里的egg,對於如何基於egg構建中間層,能夠看看這篇文章如何爲團隊定製本身的 Node.js 框架?(基於 EggJS),對於大型的服務端,尤爲是前端是以ng爲主棧的,能夠優先考慮使用nestjs;其次對於io較多而計算較少的(js自己的特質),或者服務端須要與c++配合的,大型服務端應用也可使用nest。nest默認是不採用微服務的形式的,nest將不一樣的平臺封在了不一樣的platform下,這裏只分析普通的以express爲platform的形式,對於喜歡微服務的同窗,能夠對比和java的springcloud的區別,這裏就不作表述了,其總體的核心結構大體以下:
packages
core
injector
services
這裏主要在對依賴注入的實現作一個簡單的理解分享,其思路是一脈相承的,對於理解後端理念的依賴注入有很好的理解,這也正是後端前端化的一個體現,也是最先的MVC框架向後來的MVVM框架過分的一個歷史過程,依賴注入方式對於最先的前端框架仍是有記念意義的,可是對於ng全家桶來講,這算是其基本哲學的一個基本面
Angular
先來看一下ng是如何實現injector的,這裏重點在於使用了抽象類來重載不一樣函數的使用,對於provider循環依賴的處理,利用了一個Map數據結構來區分不一樣的Provider
// 抽象類 export abstract class Injector { // get方法重載的使用 abstract get<T>( token: Type<T>|InjectionToken<T>|AbstractType<T>, notFoundValue?: T, flags?: InjectFlags ): T; abstract get( token: any, notFoundValue?: any ): any; // create方法重載的使用 static create( providers: StaticProvider[], parent?: Injector ): Injector; static create( options: { providers: StaticProvider[], parent?: Injector, name?: string } ): Injector; static create( options: StaticProvider[]|{providers: StaticProvider[], parent?: Injector, name?: string}, parent?: Injector ): Injector { if (Array.isArray(options)) { return INJECTOR_IMPL(options, parent, ''); } else { return INJECTOR_IMPL(options.providers, options.parent, options.name || ''); } } static __NG_ELEMENT_ID__ = -1; } // 記錄判斷prodiver的數據結構,這裏使用interface來承載 interface Record { fn: Function; useNew: boolean; deps: DependencyRecord[]; value: any; } interface DependencyRecord { token: any; options: number; } // 實現抽象類 export class StaticInjector implements Injector { readonly parent: Injector; readonly source: string|null; readonly scope: string|null; private _records: Map<any, Record|null>; constructor( providers: StaticProvider[], parent: Injector = Injector.NULL, source: string|null = null ) { this.parent = parent; this.source = source; const records = this._records = new Map<any, Record>(); records.set( Injector, <Record>{token: Injector, fn: IDENT, deps: EMPTY, value: this, useNew: false} ); records.set( INJECTOR, <Record>{token: INJECTOR, fn: IDENT, deps: EMPTY, value: this, useNew: false} ); this.scope = recursivelyProcessProviders(records, providers); } get<T>(token: Type<T>|InjectionToken<T>, notFoundValue?: T, flags?: InjectFlags): T; get(token: any, notFoundValue?: any): any; get(token: any, notFoundValue?: any, flags: InjectFlags = InjectFlags.Default): any { const records = this._records; // record的緩存隊列 let record = records.get(token); // 利用record避免循環提供的問題 if (record === undefined) { // This means we have never seen this record, see if it is tree shakable provider. const injectableDef = getInjectableDef(token); if (injectableDef) { const providedIn = injectableDef && injectableDef.providedIn; if (providedIn === 'any' || providedIn != null && providedIn === this.scope) { records.set( token, record = resolveProvider( {provide: token, useFactory: injectableDef.factory, deps: EMPTY})); } } if (record === undefined) { // Set record to null to make sure that we don't go through expensive lookup above again. records.set(token, null); } } let lastInjector = setCurrentInjector(this); try { return tryResolveToken(token, record, records, this.parent, notFoundValue, flags); } catch (e) { return catchInjectorError(e, token, 'StaticInjectorError', this.source); } finally { setCurrentInjector(lastInjector); } } toString() { const tokens = <string[]>[], records = this._records; records.forEach((v, token) => tokens.push(stringify(token))); return `StaticInjector[${tokens.join(', ')}]`; } } // 解析Provider的函數 function resolveProvider( provider: SupportedProvider): Record { const deps = computeDeps(provider); let fn: Function = IDENT; let value: any = EMPTY; let useNew: boolean = false; let provide = resolveForwardRef(provider.provide); // 一些錯誤處理 ... return {deps, fn, useNew, value}; } // 處理循環依賴的問題 function recursivelyProcessProviders( records: Map<any, Record>, provider: StaticProvider): string|null { let scope: string|null = null; // 根據不一樣狀況處理一些錯誤 ... return scope; } // 解析Token的函數 function resolveToken( token: any, record: Record|undefined|null, records: Map<any, Record|null>, parent: Injector, notFoundValue: any, flags: InjectFlags ): any { let value; ... return value; } // 計算依賴函數 function computeDeps( provider: StaticProvider): DependencyRecord[] { let deps: DependencyRecord[] = EMPTY; const providerDeps: any[] = (provider as ExistingProvider & StaticClassProvider & ConstructorProvider).deps; if (providerDeps && providerDeps.length) { deps = []; for (let i = 0; i < providerDeps.length; i++) { let options = OptionFlags.Default; let token = resolveForwardRef(providerDeps[i]); if (Array.isArray(token)) { for (let j = 0, annotations = token; j < annotations.length; j++) { const annotation = annotations[j]; if (annotation instanceof Optional || annotation == Optional) { options = options | OptionFlags.Optional; } else if (annotation instanceof SkipSelf || annotation == SkipSelf) { options = options & ~OptionFlags.CheckSelf; } else if (annotation instanceof Self || annotation == Self) { options = options & ~OptionFlags.CheckParent; } else if (annotation instanceof Inject) { token = (annotation as Inject).token; } else { token = resolveForwardRef(annotation); } } } deps.push({token, options}); } } ... return deps; }
Nest
再來看一下,nest的實現,不一樣於ng的實現,nest是利用參數和繼承父類參數來肯定整個的循環依賴關係的,其沒有使用重載來實現,但都對循環依賴作了處理,其基本思路是一致的。
export type InjectorDependency = Type<any> | Function | string | symbol; export interface PropertyDependency { key: string; name: InjectorDependency; isOptional?: boolean; instance?: any; } export interface InjectorDependencyContext { key?: string | symbol; name?: string | symbol; index?: number; dependencies?: InjectorDependency[]; } export class Injector { // 加載中間件 基於express的load方式 public async loadMiddleware( wrapper: InstanceWrapper, collection: Map<string, InstanceWrapper>, moduleRef: Module, contextId = STATIC_CONTEXT, inquirer?: InstanceWrapper, ) { ... } // 記載控制器 public async loadController( wrapper: InstanceWrapper<Controller>, moduleRef: Module, contextId = STATIC_CONTEXT, ) { ... } public async loadInjectable<T = any>( wrapper: InstanceWrapper<T>, moduleRef: Module, contextId = STATIC_CONTEXT, inquirer?: InstanceWrapper, ) { const injectables = moduleRef.injectables; await this.loadInstance<T>( wrapper, injectables, moduleRef, contextId, inquirer, ); } // 加載Provider public async loadProvider( wrapper: InstanceWrapper<Injectable>, moduleRef: Module, contextId = STATIC_CONTEXT, inquirer?: InstanceWrapper, ) { const providers = moduleRef.providers; await this.loadInstance<Injectable>( wrapper, providers, moduleRef, contextId, inquirer, ); await this.loadEnhancersPerContext(wrapper, contextId, wrapper); } public loadPrototype<T>( { name }: InstanceWrapper<T>, collection: Map<string, InstanceWrapper<T>>, contextId = STATIC_CONTEXT, ) { ... } // 解析繼承父類的參數 public async resolveConstructorParams<T>( wrapper: InstanceWrapper<T>, moduleRef: Module, inject: InjectorDependency[], callback: (args: unknown[]) => void, contextId = STATIC_CONTEXT, inquirer?: InstanceWrapper, parentInquirer?: InstanceWrapper, ) { ... } // 反射繼承父類的參數 public reflectConstructorParams<T>( type: Type<T> ): any[] { ... } // 反射功能參數 public reflectOptionalParams<T>( type: Type<T> ): any[] { ... } // 反射本身的參數 public reflectSelfParams<T>( type: Type<T> ): any[] { ... } // 解析單個參數 public async resolveSingleParam<T> ( wrapper: InstanceWrapper<T>, param: Type<any> | string | symbol | any, dependencyContext: InjectorDependencyContext, moduleRef: Module, contextId = STATIC_CONTEXT, inquirer?: InstanceWrapper, keyOrIndex?: string | number, ) { if (isUndefined(param)) { throw new UndefinedDependencyException( wrapper.name, dependencyContext, moduleRef, ); } const token = this.resolveParamToken(wrapper, param); return this.resolveComponentInstance<T>( moduleRef, isFunction(token) ? (token as Type<any>).name : token, dependencyContext, wrapper, contextId, inquirer, keyOrIndex, ); } // 解析參數的token public resolveParamToken<T>( wrapper: InstanceWrapper<T>, param: Type<any> | string | symbol | any, ) { if (!param.forwardRef) { return param; } wrapper.forwardRef = true; return param.forwardRef(); } }
總結:從nest和ng對injector的實現能夠看出,雖然都是注射器的實現,可是因爲呈現方式的不一樣,於是在實現方式上也會有所不一樣,對於ts而言,選用interface仍是抽象類,確實能夠借鑑java的模式思路,對於習慣js的咱們來講,對於整個數據類型的擴展(如:抽象類、接口)等是須要向後端借鑑的。總體來講,對於依賴注入的實現最關鍵的就是在於處理provider的整個依賴問題,這二者都是採用token的方式來區分對待究竟是屬於哪個provider,而後對於特殊的相關依賴循環的問題作對應的處理
ng整個生態體系在國內應用的並不廣,但並不妨礙其做爲前端理念的擴展先行者的這樣一個角色,我的認爲其在隔離性以及系統性方面都是要優於vue和react的,於是對於目前比較流行的微前端框架(ps: 對於ng的微前端應用,能夠參考這篇文章【第1789期】使用 Angular 打造微前端架構的 ToB 企業級應用),我的以爲在沙箱隔離等系統融合方面確實能夠借鑑一下ng的某些思路,或許正是因爲這個緣由,它纔是三大框架中最早上ts的,也有可能整個ng的開發者更像是傳統的軟件工程師,對於整個開發要作到定義數據、定義模型、系統設計等等,對於大型項目而言,這樣確實會減小不少因bug而須要重複修改的時間,可是對於小型項目,我的認爲仍是vue更合適。雖然對於國內,ng基本已經屬於明日黃花了,可是它的一些理念及設計思路確實仍是值得借鑑的,在這個內卷的時代,各大應用都在向着高級化、大型化發展,說不定哪天ng又在國內重回巔峯了呢,雖然很難~~哈哈哈,各位加油!