如何利用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。很是感謝!

相關文章
相關標籤/搜索