如何利用AOP+IOC思想解构前端项目开发

本文将经过 TypeClient 架构来阐述如何利用AOP+IOC思想来解构前端项目的开发。html

首先声明,AOP+IOC思想的理解须要有必定的编程架构基础。目前,这两大思想使用的场景,基本都在nodejs端,在前端的实践很是少。我本着提供一种新的项目解构思路的想法,而非推翻社区庞大的全家桶。你们看看就好,若是能给你提供更好的灵感,那么再好不过了,很是欢迎交流。前端

如下咱们将以 TypeClient 的 React 渲染引擎为例。vue

AOP

一种面向切面编程的思想。它在前端的表现是前端的装饰器,咱们能够经过装饰器来拦截函数执行前与执行后的自定义行为。node

AOP的主要做用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能一般包括日志统计、安全控制、异常处理等。把这些功能抽离出来以后, 再经过“动态织入”的方式掺入业务逻辑模块中。 AOP的好处首先是能够保持业务逻辑模块的纯净和高内聚性,其次是能够很方便地复用日志统计等功能模块。

以上是网络上对AOP的简单解释。那么实际代码也许是这样的react

@Controller()
class Demo {
  @Route() Page() {}
}
复制代码

但不少时候,咱们仅仅是将某个class下的函数看成一个储存数据的对象而已,而在肯定运行这个函数时候拿出数据作自定义处理。能够经过 reflect-metadata 来了解更多装饰器的做用。git

IOC

Angular难以被国内接受很大一部分缘由是它的理念太庞大,而其中的DI(dependency inject)在使用时候则更加让人迷糊。其实除了DI还有一种依赖注入的思想叫 IOC。它的表明库为 inversify。它在github上拥有6.7K的star数,在依赖注入的社区里,口碑很是好。咱们能够先经过这个库来了解下它对项目解构的好处。github

例子以下:ajax

@injectable()
class Demo {
  @inject(Service) private readonly service: Service;
  getCount() {
    return 1 + this.service.sum(2, 3);
  }
}
复制代码
固然,Service已经优先被注入到inversify的container内了,才能够经过 TypeClient 这样调用。

从新梳理前端项目运行时

通常地,前端项目会通过这样的运行过程。算法

  1. 经过监听hashchange或者popstate事件拦截浏览器行为。
  2. 设定当前得到的window.location 数据如何对应到一个组件。
  3. 组件如何渲染到页面。
  4. 当浏览器URL再次变化的时候,咱们如何对应到一个组件而且渲染。

这是社区的通用解决方案。固然,咱们不会再讲解如何设计这个模式。咱们将采用全新的设计模式来解构这个过程。npm

从新审视服务端路由体系

咱们聊的是前端的架构,为何会聊到服务端的架构体系?

那是由于,其实设计模式并不局限在后端或者前端,它应该是一种比较通用的方式来解决特定的问题。

那么也许有人会问,服务端的路由体系与前端并不一致,有何意义?

咱们以nodejs的http模块为例,其实它与前端有点相似的。http模块运行在一个进程中,经过http.createServer的参数回调函数来响应数据。咱们能够认为,前端的页面至关于一个进程,咱们经过监听相应模式下的事件来响应获得组件渲染到页面。

服务端多个Client发送请求到一个server端端口处理,为何不能类比到前端用户操做浏览器地址栏经过事件来获得响应入口呢?

答案是能够的。咱们称这种方式为 virtual server 即基于页面级的虚拟服务。

既然能够抽象称一种服务架构,那固然,咱们能够彻底像nodejs的服务化方案靠拢,咱们能够将前端的路由处理的如nodejs端常见的方式,更加符合咱们的意图和抽象。

history.route('/abc/:id(d+)', (ctx) => {
  const id = ctx.params.id;
  return <div>{id}</div>;
  // 或者: ctx.body = <div>{id}</div>; 这种更加能理解
})
复制代码

改造路由设计

若是是以上的书写方式,那么也能够解决基本的问题,可是不符合咱们AOP+IOC的设计,书写的时候仍是比较繁琐的,同时也没有解构掉响应的逻辑。

咱们须要解决如下问题:

  1. 如何解析路由字符串规则?
  2. 如何利用这个规则快速匹配到对应的回调函数?

在服务端有不少解析路由规则的库,比较表明的是 path-to-regexp,它被使用在KOA等著名架构中。它的原理也就是将字符串正则化,使用当前传入的path来匹配相应的规则从而获得对应的回调函数来处理。可是这种作法有一些瑕疵,那就是正则匹配速度较慢,当处理队列最后一个规则被匹配的时候,全部规则都将被执行过,当路由过多时候性能较差,这一点能够参看我以前写的 koa-rapid-router超越koa-router性能的100多倍。还有一点瑕疵是,它的匹配方式是按照你编写顺序匹配的,因此它具备必定的顺序性,开发者要很是注意。好比:

http.get('/:id(d+)', () => console.log(1));
http.get('/1234', () => console.log(2));
复制代码

若是咱们访问/1234,那么它将打印出1,而非2

为了解决性能以及优化匹配过程的智能性,咱们能够参考 find-my-way 的路由设计体系。具体请看官本身看了,我不解析。总之,它是一种字符串索引式算法,可以快速而智能地匹配到咱们须要的路由。著名的 fastify 就是采用这个架构来达到高性能的。

TypeClient 的路由设计

咱们能够经过一些简单的装饰器就能快速定义咱们的路由,本质仍是采用find-my-way的路由设计原则。

import React from 'react';
import { Controller, Route, Context } from '@typeclient/core';
import { useReactiveState } from '@typeclient/react';
@Controller('/api')
export class DemoController {
  @Route('/test')
  TestPage(props: Reat.PropsWithoutRef<Context>) {
    const status = useReactiveState(() => props.status.value);
    return <div>Hello world! {status}</div>;
  }
}
// --------------------------
// 在index.ts中只要
app.setController(DemoController);
// 它就自动绑定了路由,同时页面进入路由 `/api/test` 的时候
// 就会显示文本 `Hello world! 200`。
复制代码
可见,TypeClient 经过 AOP 理念定义路由很是简单。

路由生命周期

当从一个页面跳转到另外一个页面的时候,前一个页面的生命周期也随即结束,因此,路由是具备生命周期的。再此,咱们将整个页面周期拆解以下:

  1. beforeCreate 页面开始加载
  2. created 页面加载完成
  3. beforeDestroy 页面即将销毁
  4. destroyed 页面已经销毁

为了表示这4个生命周期,咱们根据React的hooks特制了一个函数useContextEffect来处理路由生命周期的反作用。好比:

import React from 'react';
import { Controller, Route, Context } from '@typeclient/core';
import { useReactiveState } from '@typeclient/react';
@Controller('/api')
export class DemoController {
  @Route('/test')
  TestPage(props: Reat.PropsWithoutRef<Context>) {
    const status = useReactiveState(() => props.status.value);
    useContextEffect(() => {
      console.log('路由加载完成了');
      return () => console.log('路由被销毁了');
    })
    return <div>Hello world! {status}</div>;
  }
}
复制代码

其实它与useEffect或者useLayoutEffect有些相似。只不过咱们关注的是路由的生命周期,而react则关注组件的生命周期。

其实经过上面的props.status.value咱们能够猜想出,路由是有状态记录的,分别是100200还有500等等。咱们能够经过这样的数据来判断当前路由处于什么生命周期内,也能够经过骨架屏来渲染不一样的效果。

中间件设计

为了控制路由生命周期的运行,咱们设计了中间件模式,用来处理路由前置的行为,好比请求数据等等。中间件原则上采用与KOA一致的模式,这样能够大大兼容社区生态。

const middleware = async (ctx, next) => {
  // ctx.....
  await next();
}
复制代码

经过AOP 咱们能够轻松引用这个中间件,达到页面加载完毕状态前的数据处理。

import React from 'react';
import { Controller, Route, Context, useMiddleware } from '@typeclient/core';
import { useReactiveState } from '@typeclient/react';
@Controller('/api')
export class DemoController {
  @Route('/test')
  @useMiddleware(middleware)
  TestPage(props: Reat.PropsWithoutRef<Context>) {
    const status = useReactiveState(() => props.status.value);
    useContextEffect(() => {
      console.log('路由加载完成了');
      return () => console.log('路由被销毁了');
    })
    return <div>Hello world! {status}</div>;
  }
}
复制代码

设计周期状态管理 - ContextStore

不得不说这个是一个亮点。为何要设计这样一个模式呢?主要是为了解决在中间件过程当中对数据的操做可以及时响应到页面。由于中间件执行与react页面渲染是同步的,因此咱们设计这样的模式有利于数据的周期化。

咱们采用了很是黑科技的方案解决这个问题:@vue/reactity

对,就是它。

咱们在react中嵌入了VUE3最新的响应式系统,让咱们开发快速更新数据,而放弃掉dispatch过程。固然,这对中间件更新数据是及其有力的。

这里 我很是感谢 sl1673495 给到的黑科技思路让咱们的设计可以完美兼容react。

咱们经过@State(callback)来定义ContextStore的初始化数据,经过useContextState或者useReactiveState跟踪数据变化而且响应到React页面中。

来看一个例子:

import React from 'react';
import { Controller, Route, Context, useMiddleware, State } from '@typeclient/core';
import { useReactiveState } from '@typeclient/react';
@Controller('/api')
export class DemoController {
  @Route('/test')
  @useMiddleware(middleware)
  @State(createState)
  TestPage(props: Reat.PropsWithoutRef<Context>) {
    const status = useReactiveState(() => props.status.value);
    const count = useReactiveState(() => props.state.count);
    const click = useCallback(() => ctx.state.count++, [ctx.state.count]);
    useContextEffect(() => {
      console.log('路由加载完成了');
      return () => console.log('路由被销毁了');
    })
    return <div onClick={click}>Hello world! {status} - {count}</div>;
  }
}

function createState() {
  return {
    count: 0,
  }
}
复制代码

你能够看到不断点击,数据不断变化。这种操做方式极大简化了咱们数据的变化写法,同时也能够与vue3响应式能力看齐,弥补react数据操做复杂度的短板。

除了在周期中使用这个黑科技,其实它也是能够独立使用的,好比在任意位置定义:

// test.ts
import { reactive } from '@vue/reactity';

export const data = reactive({
  count: 0,
})
复制代码

咱们能够在任意组件中使用

import React, { useCallback } from 'react';
import { useReactiveState } from '@typeclient/react-effect';
import { data } from './test';

function TestComponent() {
  const count = useReactiveState(() => data.count);
  const onClick = useCallback(() => data.count++, [data.count]);
  return <div onClick={onClick}>{count}</div>
}
复制代码

利用IOC思想解构项目

以上的讲解都没有设计IOC方面,那么下面将讲解IOC的使用。

Controller 服务解构

咱们先编写一个Service文件

import { Service } from '@typeclient/core';

@Service()
export class MathService {
  sum(a: number, b: number) {
    return a + b;
  }
}
复制代码

而后咱们能够在以前的Controller中直接调用:

import React from 'react';
import { Controller, Route, Context, useMiddleware, State } from '@typeclient/core';
import { useReactiveState } from '@typeclient/react';
import { MathService } from './service.ts';
@Controller('/api')
export class DemoController {
  @inject(MathService) private readonly MathService: MathService;

  @Route('/test')
  @useMiddleware(middleware)
  @State(createState)
  TestPage(props: Reat.PropsWithoutRef<Context>) {
    const status = useReactiveState(() => props.status.value);
    const count = useReactiveState(() => props.state.count);
    const click = useCallback(() => ctx.state.count++, [ctx.state.count]);
    const value = this.MathService.sum(count, status);
    useContextEffect(() => {
      console.log('路由加载完成了');
      return () => console.log('路由被销毁了');
    })
    return <div onClick={click}>Hello world! {status} + {count} = {value}</div>;
  }
}

function createState() {
  return {
    count: 0,
  }
}
复制代码

你能够看到数据的不断变化。

Component 解构

咱们为react的组件创造了一种新的组件模式,称IOCComponent。它是一种具有IOC能力的组件,咱们经过useComponent的hooks来调用。

import React from 'react';
import { Component, ComponentTransform } from '@typeclient/react';
import { MathService } from './service.ts';

@Component()
export class DemoComponent implements ComponentTransform {
  @inject(MathService) private readonly MathService: MathService;

  render(props: React.PropsWithoutRef<{ a: number, b: number }>) {
    const value = this.MathService.sum(props.a, props.b);
    return <div>{value}</div>
  }
}
复制代码

而后在任意组件中调用

import React from 'react';
import { Controller, Route, Context, useMiddleware, State } from '@typeclient/core';
import { useReactiveState } from '@typeclient/react';
import { MathService } from './service.ts';
import { DemoComponent } from './component';
@Controller('/api')
export class DemoController {
  @inject(MathService) private readonly MathService: MathService;
  @inject(DemoComponent) private readonly DemoComponent: DemoComponent;

  @Route('/test')
  @useMiddleware(middleware)
  @State(createState)
  TestPage(props: Reat.PropsWithoutRef<Context>) {
    const status = useReactiveState(() => props.status.value);
    const count = useReactiveState(() => props.state.count);
    const click = useCallback(() => ctx.state.count++, [ctx.state.count]);
    const value = this.MathService.sum(count, status);
    const Demo = useComponent(this.DemoComponent);
    useContextEffect(() => {
      console.log('路由加载完成了');
      return () => console.log('路由被销毁了');
    })
    return <div onClick={click}>
      Hello world! {status} + {count} = {value} 
      <Demo a={count} b={value} />
    </div>;
  }
}

function createState() {
  return {
    count: 0,
  }
}
复制代码

Middleware 解构

咱们彻底能够抛弃掉传统的中间件写法,而采用能加解构化的中间件写法:

import { Context } from '@typeclient/core';
import { Middleware, MiddlewareTransform } from '@typeclient/react';
import { MathService } from './service';

@Middleware()
export class DemoMiddleware implements MiddlewareTransform {
  @inject(MathService) private readonly MathService: MathService;

  async use(ctx: Context, next: Function) {
    ctx.a = this.MathService.sum(1, 2);
    await next();
  }
}
复制代码

为react新增Slot插槽概念

它支持Slot插槽模式,咱们能够经过useSlot得到Provider与Consumer。它是一种经过消息传送节点片断的模式。

const { Provider, Consumer } = useSlot(ctx.app);
<Provider name="foo">provider data</Provider>
<Consumer name="foo">placeholder</Consumer>
复制代码

而后编写一个IOCComponent或者传统组件。

// template.tsx
import { useSlot } from '@typeclient/react';
@Component()
class uxx implements ComponentTransform {
  render(props: any) {
    const { Consumer } = useSlot(props.ctx);
    return <div>
      <h2>title</h2>
      <Consumer name="foo" />
      {props.children}
    </div>
  }
}
复制代码

最后在Controller上调用

import { inject } from 'inversify';
import { Route, Controller } from '@typeclient/core';
import { useSlot } from '@typeclient/react';
import { uxx } from './template.tsx';
@Controller()
@Template(uxx)
class router {
  @inject(ttt) private readonly ttt: ttt;
  @Route('/test')
  test() {
    const { Provider } = useSlot(props.ctx);
    return <div>
      child ...
      <Provider name="foo">
        this is foo slot
      </Provider>
    </div>
  }
}
复制代码

你能看到的结构以下:

<div>
  <h2>title</h2>
  this is foo slot
  <div>child ...</div>
</div>
复制代码

解构项目的原则

咱们能够经过对IOC服务与Middleware还有组件进行不一样纬度的解构,封装成统一的npm包上传到私有仓库中供公司内部开发使用。

类型

  1. IOCComponent + IOCService
  2. IOCMiddleware + IOCService
  3. IOCMiddlewware
  4. IOCService

原则

  1. 通用化
  2. 内聚合
  3. 易扩展

遵循这种原则的化可使公司的业务代码或者组件具备高度的复用性,并且经过AOP可以很清楚直观的表现代码即文档的魅力。

通用化

即保证所封装的逻辑、代码或者组件具体高度的通用化特性,对于不太通用的不必封装。好比说,公司内部统一的导航头,导航头有可能被用到任意项目中作统一化,那么就很是适合封装为组件型模块。

内聚性

通用的组件须要获得统一的数据,那么能够经过IOCComponent + IOCService + IOCMiddleware的形式将其包装,在使用的适合只须要关注导入这个组件便可。仍是举例通用导航头。好比导航头须要下拉一个团队列表,那么,咱们能够这样定义这个组件:

一个service文件:

// service.ts
import { Service } from '@typeclient/core';
@Service()
export class NavService {
  getTeams() {
    // ... 这里能够是ajax请求的结果
    return [
      {
        name: 'Team 1',
        id: 1,
      },
      {
        name: 'Team 2',
        id: 1,
      }
    ]
  }

  goTeam(id: number) {
    // ...
    console.log(id);
  }
}
复制代码

组件:

// component.ts
import React, { useEffect, setState } from 'react';
import { Component, ComponentTransform } from '@typeclient/react';
import { NavService } from './service';

@Component()
export class NavBar implements ComponentTransform {
  @inject(NavService) private readonly NavService: NavService;
  render() {
    const [teams, setTeams] = setState<ReturnType<NavService['getTeams']>>([]);
    useEffect(() => this.NavService.getTeams().then(data => setTeams(data)), []);
    return <ul>
      {
        teams.map(team => <li onClick={() => this.NavService.goTeam(team.id)}>{team.name}</li>)
      }
    </ul>
  }
}
复制代码

咱们将这个模块定义为@fe/navbar,同时导出这个个对象:

// @fe/navbar/index.ts
export * from './component';
复制代码

在任意的IOC组件中就能够这样调用

import React from 'react';
import { Component, ComponentTransform, useComponent } from '@typeclient/react';
import { NavBar } from '@fe/navbar';

@Component()
export class DEMO implements ComponentTransform {
  @inject(NavBar) private readonly NavBar: NavBar;
  render() {
    const NavBar = useComponent(this.NavBar);
    return <NavBar />
  }
}
复制代码

你能够发现只要加载这个组件,至关于请求数据都自动被载入了,这就很是有区别与普通的组件模式,它能够是一种业务型的组件解构方案。很是实用。

易扩展

主要是让咱们对于设计这个通用型的代码或者组件时候保持搞扩展性,好比说,巧用SLOT插槽原理,咱们能够预留一些空间给插槽,方便这个组件被使用不一样位置的代码所传送而且替换掉原位置内容,这个的好处须要开发者自行体会。

演示

咱们提供了一个demo来表现它的能力,并且能够从代码中看到如何解构整个项目。咱们的每一个Controller均可以独立存在,使得项目内容迁移变得很是容易。

你们能够经过以上的两个例子来了解开发模式。

总结

新的开发理念并非让你摒弃掉传统的开发方式和社区,并且提供更好的思路。固然,这种思路的好与坏,各有各的理解。可是我仍是想声明下,我今天仅仅是提供一种新的思路,你们看看就好,喜欢的给个star。很是感谢!

相关文章
相关标签/搜索