Angular在国内使用的人并不像国外那么多,基本都是外企在用,但其框架的思想却仍可以为我们所借鉴,在某些问题没有思路的时候可以参考ng相关的处理,ng处理方式和思维确实比较超前,但也因此而曲高和寡。本文旨在通过ng全家桶项目(前端Angular10 + 后端NestJS7)的实践来总结对于ng架构中一些亮点的关注与思考,Angular和Nest在前后端框架的处理上同出一脉,对比起来更有借鉴意义。
[目录结构]
[目录描述]
整个前端项目是基于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;
}
}
[目录结构]
[目录描述]
后端项目是基于nestjs框架的大型后台项目配置,api模块主要是对外输出的接口,auth、filters、guard、interceptors、middlewares、pipes等是对于需要的模块进行统一的收集处理,main.ts是主入口文件,用于启动及相关配置等,app.module.ts是用来收集所有模块的导入,ng基于模块的方式可以起到非常好的隔离效果
[实践分享]
typeorm数据库连接:使用navicat连接需要先创建数据库,否则无法连接
bull消息队列:nestjs的消息队列使用的是bull.js这个库,其实现了一个调度的消息队列机制
微服务:nestjs默认使用的基础的类似java的spring框架,并未采用微服务,如需使用,需要完全重构
首先,对于没有用过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原理及使用方法介绍,我这里就不展开所有的源码,整体的核心大框架如下:
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的区别,这里就不做表述了,其整体的核心结构大致如下:
这里主要在对依赖注入的实现做一个简单的理解分享,其思路是一脉相承的,对于理解后端理念的依赖注入有很好的理解,这也正是后端前端化的一个体现,也是最早的MVC框架向后来的MVVM框架过度的一个历史过程,依赖注入方式对于最早的前端框架还是有纪念意义的,但是对于ng全家桶来说,这算是其基本哲学的一个基本面
Angular
先来看一下ng是如何实现injector的,这里重点在于使用了抽象类来重载不同函数的使用,对于provider循环依赖的处理,利用了一个Map数据结构来区分不同的Provider
// 抽象类
export abstract class Injector {
// get方法重载的使用
abstract get(
token: Type|InjectionToken|AbstractType, 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;
constructor(
providers: StaticProvider[],
parent: Injector = Injector.NULL,
source: string|null = null
) {
this.parent = parent;
this.source = source;
const records = this._records = new Map();
records.set(
Injector,
{token: Injector, fn: IDENT, deps: EMPTY, value: this, useNew: false}
);
records.set(
INJECTOR,
{token: INJECTOR, fn: IDENT, deps: EMPTY, value: this, useNew: false}
);
this.scope = recursivelyProcessProviders(records, providers);
}
get(token: Type|InjectionToken, 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 = [], 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,
provider: StaticProvider): string|null
{
let scope: string|null = null;
// 根据不同情况处理一些错误
...
return scope;
}
// 解析Token的函数
function resolveToken(
token: any,
record: Record|undefined|null,
records: Map, 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 | 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,
moduleRef: Module,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
) {
...
}
// 记载控制器
public async loadController(
wrapper: InstanceWrapper,
moduleRef: Module,
contextId = STATIC_CONTEXT,
) {
...
}
public async loadInjectable(
wrapper: InstanceWrapper,
moduleRef: Module,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
) {
const injectables = moduleRef.injectables;
await this.loadInstance(
wrapper,
injectables,
moduleRef,
contextId,
inquirer,
);
}
// 加载Provider
public async loadProvider(
wrapper: InstanceWrapper,
moduleRef: Module,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
) {
const providers = moduleRef.providers;
await this.loadInstance(
wrapper,
providers,
moduleRef,
contextId,
inquirer,
);
await this.loadEnhancersPerContext(wrapper, contextId, wrapper);
}
public loadPrototype(
{ name }: InstanceWrapper,
collection: Map>,
contextId = STATIC_CONTEXT,
) {
...
}
// 解析继承父类的参数
public async resolveConstructorParams(
wrapper: InstanceWrapper,
moduleRef: Module,
inject: InjectorDependency[],
callback: (args: unknown[]) => void,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
parentInquirer?: InstanceWrapper,
) {
...
}
// 反射继承父类的参数
public reflectConstructorParams(
type: Type
): any[]
{
...
}
// 反射功能参数
public reflectOptionalParams(
type: Type
): any[]
{
...
}
// 反射自己的参数
public reflectSelfParams(
type: Type
): any[]
{
...
}
// 解析单个参数
public async resolveSingleParam
(
wrapper: InstanceWrapper,
param: Type | 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(
moduleRef,
isFunction(token) ? (token as Type).name : token,
dependencyContext,
wrapper,
contextId,
inquirer,
keyOrIndex,
);
}
// 解析参数的token
public resolveParamToken(
wrapper: InstanceWrapper,
param: Type | 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又在国内重回巅峰了呢,虽然很难~~哈哈哈,各位加油!
仓库地址,欢迎star:
前端仓库地址
后端仓库地址