美文网首页
NG全家桶全栈项目实践总结

NG全家桶全栈项目实践总结

作者: 维李设论 | 来源:发表于2020-11-04 00:33 被阅读0次
    后端 | NG全家桶全栈项目实践总结.png

    前言

    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全家桶来说,这算是其基本哲学的一个基本面

    <b>Angular</b>

    先来看一下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;
    }
    

    <b>Nest</b>

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

    参考

    相关文章

      网友评论

          本文标题:NG全家桶全栈项目实践总结

          本文链接:https://www.haomeiwen.com/subject/uujwvktx.html