本文将通过 TypeClient 架构来论述如何利用AOP+IOC思维来解构前端我的项目的开发。

首先申明,AOP+IOC思维的了解须要有肯定的编程架构根底。目前,这两大思维应用的场景,根本都在nodejs端,在前端的实际非常少。我本着提供一种新的我的项目解构思路的想法,而非颠覆社区宏大的全家桶。大家看看就好,如果能给你提供更好的灵感,那么再好不过了,十分欢送交换。

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

AOP

一种面向切面编程的思维。它在前端的体现是前端的装璜器,咱们能够通过装璜器来拦挡函数执行前与执行后的自定义行为。

AOP的次要作用是把一些跟外围业务逻辑模块无关的性能抽离进去,这些跟业务逻辑无关的性能通常包含日志统计、安全控制、异样解决等。把这些性能抽离进去之后, 再通过“动静织入”的形式掺入业务逻辑模块中。 AOP的益处首先是能够放弃业务逻辑模块的污浊和高内聚性,其次是能够很不便地复用日志统计等功能模块。

以上是网络上对AOP的简略解释。那么理论代码兴许是这样的

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

但很多时候,咱们仅仅是将某个class下的函数当作一个贮存数据的对象而已,而在确定运行这个函数时候拿出数据做自定义解决。能够通过 reflect-metadata 来理解更多装璜器的作用。

IOC

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

例子如下:

@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再次变动的时候,咱们如何对应到一个组件并且渲染。

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

从新扫视服务端路由体系

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

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

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

咱们以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.tsimport { 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.tsximport { 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.tsimport { 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.tsimport 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.tsexport * 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都能够独立存在,使得我的项目内容迁徙变得非常容易。

  • 框架: github.com/flowxjs/Typ…
  • 我的项目模板: github.com/flowxjs/Typ…
  • 简略的最佳实际: github.com/flowxjs/Typ…

大家能够通过以上的两个例子来理解开发模式。

总结

新的开发理念并不是让你摒弃掉传统的开发方式和社区,而且提供更好的思路。当然,这种思路的好与坏,各有各的了解。然而我还是想申明下,我明天仅仅是提供一种新的思路,大家看看就好,喜爱的给个star。非常感谢!