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又在国内重回巅峰了呢,虽然很难~~哈哈哈,各位加油!