前言
Angular 在国内应用的人并不像国外那么多,根本都是外企在用,但其框架的思维却仍能够为咱们所借鉴,在某些问题没有思路的时候能够参考 ng 相干的解决,ng 解决形式和思维的确比拟超前,但也因而而曲高和寡。本文旨在通过 ng 全家桶我的项目(前端 Angular10 + 后端 NestJS7)的实际来总结对于 ng 架构中一些亮点的关注与思考,Angular 和 Nest 在前后端框架的解决上同出一脉,比照起来更有借鉴意义。
目录
- 前端我的项目实际
- 后端我的项目实际
- 源码简析
案例
前端我的项目实际
[目录构造]
-
src
-
app
-
login
- login.component.html
- login.component.scss
- login.component.spec.ts
- login.component.ts
-
main
- main.component.html
- main.component.scss
- main.component.spec.ts
- main.component.ts
- app.component.html
- app.component.scss
- app.component.spec.ts
- app.component.ts
- app.module.ts
- app.service.ts
- user.ts
-
- main.ts
- index.html
-
- angular.json
[目录形容]
整个前端我的项目是基于 angular 脚手架生成的,其根本目录构造是在 src 的 app 下进行相干组件和页面的模块开发,main.ts 和 index.html 是整个单页利用的主入口,根目录下 angular.json 用于配置相干的打包编译等环境配置参数
[实际分享]
- 脚手架版本问题:对于 ng10.1.x 以上的版本其脚手架版本与库版本有出入,会导致引入 HttpModule 及其他局部模块时报错,须要将 @angular/compiler 这个库进行相干版本的降级
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.
- 输入框双向数据绑定有效问题:对于 forms 表单的想应用
[(ngModal)]
指令,必须在 module 中引入 FormsModule
import {FormsModule} from '@angular/forms';
@NgModule({
imports: [FormsModule]
})
- 组件库应用有问题:目前 ng 的组件库次要有 官网 MATERIAL 组件库、NG-ZORRO 组件库、NG-NEST 组件库,但这几个次要的组件库都在 10 以下或 10.0 版本,会有局部 bug,因此本次未引入组件库,间接应用 html 和 scss 来优化页面
- Animation 引入问题:同样的跟版本有关系,想应用 ng 自带的 animation 动画库须要配合对应的版本,在做提示框隐没时本想应用 ng 的动画,起初改用了 animation 间接写
.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;}
}
后端我的项目踩坑实际
[目录构造]
-
src
-
api
-
article
- article.controller.ts
- article.module.ts
-
audio
- audio.controller.ts
- audio.module.ts
- audio.processor.ts
-
user
-
dto
- create-user.dto.ts
- get-user.dto.ts
- update-user.dto.ts
- user.controller.ts
- user.entity.ts
- user.interface.ts
- user.module.ts
- user.service.ts
-
-
-
auth
- auth.controller.ts
- auth.service.ts
- constants.ts
- jwt.strategy.ts
-
filters
-
exception
- http-exception.filter.ts
-
-
guard
- roles.guard.ts
-
interceptors
- cache.interceptor.ts
- exception.interceptor.ts
- logger.interceptor.ts
- timeout.interceptor.ts
- transform.interceptor.ts
-
middlewares
-
logger
- logger.middleware.ts
- logger.ts
-
-
pipes
- parse-int.pipe.ts
- validate.pipe.ts
- main.ts
- app.module.ts
-
- nest-cli.json
[目录形容]
后端我的项目是基于 nestjs 框架的大型后盾我的项目配置,api 模块次要是对外输入的接口,auth、filters、guard、interceptors、middlewares、pipes 等是对于须要的模块进行对立的收集解决,main.ts 是主入口文件,用于启动及相干配置等,app.module.ts 是用来收集所有模块的导入,ng 基于模块的形式能够起到十分好的隔离成果
[实际分享]
- typeorm 数据库连贯:应用 navicat 连贯须要先创立数据库,否则无奈连贯
- bull 音讯队列:nestjs 的音讯队列应用的是 bull.js 这个库,其实现了一个调度的音讯队列机制
- 微服务:nestjs 默认应用的根底的相似 java 的 spring 框架,并未采纳微服务,如需应用,须要齐全重构
源码简析
Angular
首先,对于没有用过 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
- aot
- css_parser
- expression_parser
- jit
- ml_parser
- template_parser
-
-
core
-
src
-
di
- injector.ts
-
reflection
- reflector.ts
-
view
- provider.ts
- render3 (ps: Ivy 的渲染局部)
-
-
-
Nest
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
- injector.ts
- module.ts
-
services
- reflector.service.ts
-
- platform-express
-
源码
这里次要在对依赖注入的实现做一个简略的了解分享,其思路是一脉相承的,对于了解后端理念的依赖注入有很好的了解,这也正是后端前端化的一个体现,也是最早的 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 又在国内重回巅峰了呢,尽管很难~~ 哈哈哈,各位加油!
参考
- Angular 源码
- Angular 中文文档
- Nest 源码
- Nest 中文文档
- AngularJs 和 angular 的比照
- Angular 9.1.0 在我的项目门路蕴含软链接时编译某些包时报错,9.1.1 已修改
- angularjs 源码剖析
- 如何为团队定制本人的 Node.js 框架?(基于 EggJS)
- 【第 1789 期】应用 Angular 打造微前端架构的 ToB 企业级利用