1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        教你如何構(gòu)建自己的依賴注入工具

        共 33498字,需瀏覽 67分鐘

         ·

        2023-09-01 02:17

        閱讀前準(zhǔn)備

        在閱讀這篇文檔之前,你可以先了解一下這些知識(shí),方便跟上文檔里面的內(nèi)容:

        • 概念知識(shí):控制反轉(zhuǎn)、依賴注入、依賴倒置;
        • 技術(shù)知識(shí):裝飾器 Decorator、反射 Reflect;
        • TSyringe 的定義:Token、Provider https://github.com/microsoft/tsyringe#injection-token 。

        本篇文章所有實(shí)現(xiàn)的實(shí)踐都寫到了 codesandbox 里面,感興趣可以點(diǎn)進(jìn)去看看源碼 https://codesandbox.io/s/di-playground-oz2j9。

        什么是依賴注入

        簡(jiǎn)單的例子

        我們這里來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的例子來(lái)解釋什么是依賴注入:學(xué)生從家里駕駛交通工具去上學(xué)。

              
              class Transportation {
          drive() {
            console.log('driving by transportation')
          }
        }

        class Student {
          transportation = new Transportation()
          gotoSchool() {
            console.log('go to school by')
            this.transportation.drive()
          }
        }

        那么在現(xiàn)實(shí)的生活中,比較遠(yuǎn)的學(xué)生會(huì)選擇開車去上學(xué),近的的學(xué)生選擇騎自行車去上學(xué),那么上面的代碼我們可能會(huì)繼續(xù)抽象,寫成下面的樣子:

              
              class Car extends Transportation {
          drive() {
            console.log('driving by car')
          }
        }

        class FarStudent extends Student {
          transportation = new Car()
        }

        這樣的確滿足了比較遠(yuǎn)的學(xué)生駕車去上學(xué)的需求,但是這里有一個(gè)問題,學(xué)生也有自己的具體選擇和偏好,有的人喜歡寶馬、有的人喜歡特斯拉。我們可以為了解決這樣的問題繼續(xù)使用繼承的方式,繼續(xù)抽象,得到喜歡寶馬的有錢學(xué)生、喜歡特斯拉的有錢學(xué)生。

        大家估計(jì)也會(huì)覺得這樣寫代碼完全不可行,耦合度太高,每個(gè)類型的學(xué)生在抽象的時(shí)候都直接和一個(gè)具體的交通工具綁定在一起。學(xué)生擁有的交通工具并不是這個(gè)學(xué)生創(chuàng)建出來(lái)決定的,而是根據(jù)他的家庭狀況、喜好確定他使用什么樣子的交通工具去上學(xué);甚至家里可能有很多的車,每天看心情開車去上學(xué)。

        那么為了降低耦合性,根據(jù)具體的狀態(tài)和條件進(jìn)行依賴的創(chuàng)建,就要說到下面的模式了。

        控制反轉(zhuǎn)

        控制反轉(zhuǎn)(Inversion of Control,縮寫為 IoC)是一種設(shè)計(jì)原則,通過反轉(zhuǎn)程序邏輯來(lái)降低代碼之間的耦合性。

        控制反轉(zhuǎn)容器(IoC 容器)是某一種具體的工具或者框架,用來(lái)執(zhí)行從內(nèi)部程序反轉(zhuǎn)出來(lái)的代碼邏輯,從而提高代碼的復(fù)用性和可讀性。我們常常用到的 DI 工具,就扮演了 IoC 容器的角色,連接著所有的對(duì)象和其依賴。

        51cb95331acb4b12f7f3b13505864d29.webp

        參考 Martin Fowler 關(guān)于控制反轉(zhuǎn)和依賴注入的文章 https://martinfowler.com/articles/injection.html

        依賴注入

        依賴注入是控制反轉(zhuǎn)的一種具體的實(shí)現(xiàn),通過放棄程序內(nèi)部對(duì)象生命創(chuàng)建的控制,由外部去創(chuàng)建并注入依賴的對(duì)象。

        依賴注入的方法主要是以下四種:

        • 基于接口。實(shí)現(xiàn)特定接口以供外部容器注入所依賴類型的對(duì)象。
        • 基于 set 方法。實(shí)現(xiàn)特定屬性的 public set 方法,來(lái)讓外部容器調(diào)用傳入所依賴類型的對(duì)象。
        • 基于構(gòu)造函數(shù)。實(shí)現(xiàn)特定參數(shù)的構(gòu)造函數(shù),在新建對(duì)象時(shí)傳入所依賴類型的對(duì)象。
        • 基于注解,在私有變量前加類似 “@Inject” 的注解,讓工具或者框架能夠分析依賴,自動(dòng)注入依賴。

        前兩種方法不會(huì)在前端常用的 DI 工具中用到,這里主要介紹后面兩種。

        如果從構(gòu)造函數(shù)傳遞,就可以寫成這樣:

              
              class Student {
          constructor(transportation: Transportation) {
            this.transportation = transportation
          }

          gotoSchool() {
            console.log('go to school by')
            this.transportation.drive()
          }
        }

        class Car extends Transportation {
          drive() {
            console.log('driving by car')
          }
        }

        const car = new Car()
        const student = new Student(car)
        student.gotoSchool()

        在沒有工具的情況情況下,基于構(gòu)造函數(shù)的定義方式是可以手寫出來(lái)的,只不過這里的寫法雖然是屬于依賴注入了,但是過多繁瑣的手動(dòng)實(shí)例化會(huì)是研發(fā)人員的噩夢(mèng);特別是 Car 對(duì)象本身可能會(huì)依賴著不同的輪胎、不同的發(fā)動(dòng)機(jī)的實(shí)例。

        依賴注入的工具

        依賴注入的的工具是 IoC 容器的一種,通過自動(dòng)分析依賴,然后在工具內(nèi)完成了本來(lái)手動(dòng)執(zhí)行的對(duì)象實(shí)例化的過程。

              
              @Injectable()
        class Student {
          constructor(transportation: Transportation) {
            this.transportation = transportation
          }

          gotoSchool() {
            console.log('go to school by')
            this.transportation.drive()
          }
        }

        const injector = new Injector()
        const student = injector.create(Student)
        student.gotoSchool()

        如果是使用注解的方式,就可以寫成這樣:

              
              @Injectable()
        class Student {
          @Inject()
          private transportation: Transportation

          gotoSchool() {
            console.log('go to school by')
            this.transportation.drive()
          }
        }

        const injector = new Injector()
        const student = injector.create(Student)
        student.gotoSchool()

        兩者的區(qū)別在于對(duì)工具的依賴性,從構(gòu)造函數(shù)定義的 class 即使用手動(dòng)創(chuàng)建仍然能正常運(yùn)行,但是以注解的方式定義的 class 就只能通過工具進(jìn)行創(chuàng)建,無(wú)法通過手動(dòng)創(chuàng)建。

        依賴倒置

        軟件設(shè)計(jì)模式六大原則之一,依賴倒置原則,英文縮寫 DIP,全稱 Dependence Inversion Principle。

        高層模塊不應(yīng)該依賴低層模塊,兩者都應(yīng)該依賴其抽象;抽象不應(yīng)該依賴細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴抽象。

        在擁有 loC 容器的場(chǎng)景下,對(duì)象創(chuàng)建的控制不在我們手上,而是在工具或者框架內(nèi)部創(chuàng)建,一個(gè)學(xué)生在上學(xué)時(shí)開的是寶馬還是特斯拉,是由運(yùn)行環(huán)境決定的。而在 JS 的實(shí)際運(yùn)行環(huán)境中,遵循的更是鴨子模型,不管是不是交通工具,只要能開,什么車都可以。

        所以我們可以把代碼改成下面這樣,依賴的是一個(gè)抽象,而不是某個(gè)具體的實(shí)現(xiàn)。

              
              // src/transportations/car.ts
        class Car {
           drive() {
             console.log('driving by car')
           }
        }

        // src/students/student.ts
        interface Drivable {
           drive(): void
        }

        class Student {
          constructor(transportation: Drivable) {
            this.transportation = transportation
          }

          gotoSchool() {
            console.log('go to school by')
            this.transportation.drive()
          }
        }

        為什么依賴抽象而不依賴實(shí)現(xiàn)那么重要呢?在復(fù)雜的架構(gòu)里面,合理的抽象能夠幫助我們?cè)诒3趾?jiǎn)潔,在領(lǐng)域邊界內(nèi)提高內(nèi)聚,不同的邊界降低耦合,指導(dǎo)項(xiàng)目進(jìn)行合理的目錄結(jié)構(gòu)劃分。在實(shí)現(xiàn) SSR 和 CSR 的復(fù)合能力的時(shí)候,在客戶端運(yùn)行的時(shí)候,需要通過 HTTP 去請(qǐng)求數(shù)據(jù),而在服務(wù)端,我們只需要直接調(diào)用 DB 或者 RPC 就能夠獲取數(shù)據(jù)。

        我們可以抽象一下請(qǐng)求數(shù)據(jù)的對(duì)象,定義一個(gè)抽象的 Service,在客戶端和服務(wù)端分別實(shí)現(xiàn)同樣的函數(shù),去請(qǐng)求數(shù)據(jù):

              
              interface IUserService {
          getUserInfo(): Promise<{ name: string }>
        }

        class Page {
          constructor(userService: IUserService) {
            this. userService = userService
          }

          async render() {
            const user = await this.userService. getUserInfo()
            return `<h1> My name is ${user.name}. </h1>`
          }
        }

        在網(wǎng)頁(yè)上這么用:

              
              class WebUserService implements IUserService {
          async getUserInfo() {
            return fetch('/api/users/me')
          }
        }

        const userService = new WebUserService()
        const page = new Page(userService)

        page.render().then((html) => {
          document.body.innerHTML = html
        })

        在服務(wù)端這么用:

              
              class ServerUserService implements IUserService {
          async getUserInfo() {
            return db.query('...')
          }
        }

        class HomeController {
          async renderHome() {
            const userService = new ServerUserService()
            const page = new Page(userService)
            ctx.body = await page.render()
          }
        }

        可測(cè)試性

        除了實(shí)現(xiàn)了軟件工程里面最重要的高內(nèi)聚低耦合,依賴注入還能夠提高代碼的可測(cè)試性。

        一般的測(cè)試我們可能會(huì)這樣寫:

              
              class Car extends Transportation {
          drive() {
            console.log('driving by car')
          }
        }

        class Student {
          this.transportation = new Car()

          gotoSchool() {
            console.log('go to school by')
            this.transportation.drive()
          }
        }

        it('goto school successfully'async () => {
          const driveStub = sinon.sub(Car.prototype, 'drive').resolve()
          const student = new Student()
          student. gotoSchool()
          expect(driveStub.callCount).toBe(1)
        })

        這樣的單元測(cè)試雖然能夠正常運(yùn)行,但是由于 Stub 的函數(shù)是入侵在 prototype 上面,這是一個(gè)全局的副作用影響,會(huì)讓其他單元測(cè)試如果運(yùn)行到這里受到影響。如果在測(cè)試結(jié)束的時(shí)候,清除了 sinon 的副作用,倒是不會(huì)影響串行的單元測(cè)試,但是就無(wú)法進(jìn)行并行測(cè)試了。而使用了依賴注入的方法,就不會(huì)有這些問題了。

              
              class Student {
          constructor(transportation: Transportation) {
            this.transportation = transportation
          }

          gotoSchool() {
            console.log('go to school by')
            this.transportation.drive()
          }
        }

        it('goto school successfully'async () => {
          const driveFn = sinon.fake()
          const student = new Student({
            { drive: driveFn },
          })
          student.gotoSchool()
          expect(driveFn.callCount).toBe(1)
        })

        循環(huán)依賴

        在擁有循環(huán)依賴的情況下,一般我們無(wú)法將對(duì)象創(chuàng)建出來(lái),比如下面的這兩個(gè)類定義。雖然從邏輯上面需要避免這樣的情況發(fā)生,但是很難說代碼寫的過程中不會(huì)完全用到這樣的情況。

              
              export class Foo {
          constructor(public bar: Bar) {}
        }

        export class Bar {
          constructor(public foo: Foo) {}
        }

        有了 DI 之后,通過 IoC 的創(chuàng)建控制反轉(zhuǎn),一開始創(chuàng)建對(duì)象的時(shí)候不會(huì)真正創(chuàng)建實(shí)例,而是給一個(gè) proxy 對(duì)象,當(dāng)這個(gè)對(duì)象真正被使用的時(shí)候才會(huì)創(chuàng)建實(shí)例,然后解決循環(huán)依賴的問題。至于為什么這里的 Lazy 裝飾器是一定要存在的,等到我們后面實(shí)現(xiàn)的時(shí)候再解釋。

              
              @Injectable()
        export class Foo {
          constructor(public @Lazy(() => Bar) bar: Bar) {}
        }

        @Injectable()
        export class Bar {
          constructor(public @Lazy(() => Foo) foo: Foo) {}
        }

        一些缺點(diǎn)

        當(dāng)然,使用 DI 工具也不是完全沒有壞處,比較明顯的壞處包括:

        • 無(wú)法控制的生命周期,因?yàn)閷?duì)象的實(shí)例化在 IoC 里面,所以對(duì)象什么時(shí)候創(chuàng)建出來(lái)并不完全由當(dāng)前程序說了算。所以這要求我們?cè)谑褂霉ぞ呋蛘呖蚣苤?,需要非常了解其中的原理,最好是讀過里面的源碼。
        • 當(dāng)依賴出錯(cuò)的時(shí)候,比較難定位到是哪個(gè)在出錯(cuò)。因?yàn)橐蕾囀亲⑷脒M(jìn)來(lái)的,所以當(dāng)依賴出錯(cuò)的時(shí)候只能通過經(jīng)驗(yàn)去分析,或者現(xiàn)場(chǎng) debug 住,一點(diǎn)點(diǎn)進(jìn)行深入調(diào)試,才能知道是哪個(gè)地方的內(nèi)容出錯(cuò)了。這對(duì) Debug 能力,或者項(xiàng)目的整體把控能力要求很高。
        • 代碼無(wú)法連貫閱讀。如果是依賴實(shí)現(xiàn),你從入口一直往下,就能看到整個(gè)代碼執(zhí)行樹;而如果是依賴抽象,具體的實(shí)現(xiàn)和實(shí)現(xiàn)之間的連接關(guān)系是分開的,通常需要文檔才能夠看到項(xiàng)目全貌。

        社區(qū)的工具

        從 github 的 DI 分類可以查看到一些流行的 DI 工具 https://github.com/topics/dependency-injection?l=typescript

        InversifyJS(https://github.com/inversify/InversifyJS): 能力強(qiáng)大的,依賴注入的工具,比較嚴(yán)格的依賴抽象的執(zhí)行方式;雖然嚴(yán)格的申明很好,但是寫起來(lái)很重復(fù)和啰嗦。

        TSyringe(https://github.com/microsoft/tsyringe): 簡(jiǎn)單易用,繼承自 angular 的抽象定義,比較值得學(xué)習(xí)。

        實(shí)現(xiàn)基本的能力

        為了實(shí)現(xiàn)基本的 DI 工具的能力,去接管對(duì)象創(chuàng)建,實(shí)現(xiàn)依賴倒置和依賴注入,我們主要實(shí)現(xiàn)三個(gè)能力:

        • 依賴分析:為了能夠創(chuàng)建對(duì)象,需要讓工具知道有那些依賴。
        • 注冊(cè)創(chuàng)建器:為了支持不同類型的實(shí)例創(chuàng)建方式,支持直接依賴、支持抽象依賴、支持工廠創(chuàng)建;不同的上下文注冊(cè)不同的實(shí)現(xiàn)。
        • 創(chuàng)建實(shí)例:利用創(chuàng)建器將實(shí)例創(chuàng)建出來(lái),支持單例模式,多例模式。

        假定我們的最終形態(tài)是這樣的執(zhí)行代碼,如果要想要最終的結(jié)果的話,可以點(diǎn)擊在線編碼的鏈接 https://codesandbox.io/s/di-playground-oz2j9 。

              
              @Injectable()
        class Transportation {
          drive() {
            console.log('driving by transportation')
          }
        }

        @Injectable()
        class Student {
          constructor(
            private transportation: Transportation,
          
        ) {}

          gotoSchool() {
            console.log('go to school by')
            this.transportation.drive()
          }
        }

        const container = new Container()
        const student = container.resolve(Student)
        student.gotoSchool()

        依賴分析

        為了能夠讓 DI 工具能夠進(jìn)行依賴分析,需要開啟 TS 的裝飾器功能,以及裝飾器的元數(shù)據(jù)功能。

              
              {
          "compilerOptions": {
            "experimentalDecorators"true,
            "emitDecoratorMetadata"true
          }
        }

        Decorator

        那么首先讓我們來(lái)看一下構(gòu)造函數(shù)的依賴是怎么分析出來(lái)的。開啟了裝飾器和元數(shù)據(jù)的功能之后,前面的代碼嘗試在 TS 的 playground 進(jìn)行一次編譯,能夠看到運(yùn)行的 JS 代碼是這樣的。

        8eeec3c0774fb69154e0ee6fbfacb3a8.webp

        能夠注意到比較關(guān)鍵的代碼定義是這樣的:

              
              Student = __decorate([
          Injectable(),
          __metadata("design:paramtypes", [Transportation])
        ], Student);

        仔細(xì)去閱讀 __decorate 函數(shù)的邏輯的話,實(shí)際上就是一個(gè)高階函數(shù),為了倒序執(zhí)行 ClassDecorator 和 Metadata 的 Decorator,翻譯一下上面的代碼,就等于:

              
              Student = __metadata("design:paramtypes", [Transportation])(Student)
        Student = Injectable()(Student)

        然后我們?cè)僮屑?xì)閱讀 __metadata 函數(shù)的邏輯,執(zhí)行的是 Reflect 的函數(shù),就等于代碼:

              
              Student = Reflect.metadata("design:paramtypes", [Transportation])(Student)
        Student = Injectable()(Student)

        反射

        前面的代碼結(jié)果,我們暫時(shí)可以不管第一行,來(lái)閱讀一下第二行的含義,這里正是我們需要的依賴分析的能力。Reflect.metadata 是一個(gè)高階函數(shù),返回的是一個(gè) decorator 函數(shù),執(zhí)行后將數(shù)據(jù)定義在構(gòu)造函數(shù)上,可以通過 getMetadata 從這個(gè)構(gòu)造函數(shù)或者其繼承者都能找到定義的數(shù)據(jù)。

        b1be2a5b35fabdbdf821849e06806b74.webp

        比如上面的反射,我們能夠通過下面的方式拿到定義的數(shù)據(jù):

              
              const args = Reflect.getMetadata("design:paramtypes", Student)
        expect(args).toEqual([Transportation])

        反射元數(shù)據(jù)的提案:https://rbuckton.github.io/reflect-metadata/#syntax

        開啟了 emitDecoratorMetadata 之后,被裝飾的地方,TS 會(huì)在編譯的時(shí)候自動(dòng)填充三種元數(shù)據(jù):

        • design:type 當(dāng)前屬性的類型元數(shù)據(jù),出現(xiàn)在 PropertyDecorator 和 MethodDecorator;
        • design:paramtypes 入?yún)⒌脑獢?shù)據(jù),出現(xiàn)在 ClassDecorator 和 MethodDecorator;
        • design:returntype 返回類型元數(shù)據(jù),出現(xiàn)在 MethodDecorator。
        2a6a6c34f59ac5d583fdb5054db83b16.webp

        標(biāo)記依賴

        為了讓 DI 工具能夠收集并存儲(chǔ)依賴,我們需要在 Injectable 中,將依賴的構(gòu)造函數(shù)解析出來(lái),然后也通過反射定義構(gòu)造函數(shù)的方式,將數(shù)據(jù)描述通過一個(gè) Symbol 值記錄在反射中。

              
              const DESIGN_TYPE_NAME = {
          DesignType"design:type",
          ParamType"design:paramtypes",
          ReturnType"design:returntype"
        };

        const DECORATOR_KEY = {
          InjectableSymbol.for("Injectable"),
        };

        export function Injectable<T>() {
          return (target: new (...args: any[]) => T) => {
            const deps = Reflect.getMetadata(DESIGN_TYPE_NAME.ParamType, target) || [];
            const injectableOpts = { deps };
            Reflect.defineMetadata(DECORATOR_KEY.Injectable, injectableOpts, target);
          };
        }

        這樣做有兩個(gè)目的:

        • 通過內(nèi)部的 Symbol 標(biāo)記配置數(shù)據(jù),表面這個(gè)構(gòu)造函數(shù)是經(jīng)過 Injectable 裝飾的,可以被 IoC 創(chuàng)建出來(lái)。
        • 采集并組裝配置數(shù)據(jù),定義在構(gòu)造函數(shù)中,包括依賴的數(shù)據(jù),后面可能會(huì)用到的比如單例多例的配置數(shù)據(jù)。

        定義容器

        有了裝飾器在反射中定義的數(shù)據(jù),可以就可以創(chuàng)建 IoC 中最重要的 Container 部分,我們就實(shí)現(xiàn)一個(gè) resolve 函數(shù),將實(shí)例自動(dòng)創(chuàng)建出來(lái):

              
              const DECORATOR_KEY = {
          Injectable: Symbol.for("Injectable"),
        };

        const ERROR_MSG = {
          NO_INJECTABLE: "Constructor should be wrapped with decorator Injectable.",
        }

        export class ContainerV1 {
          resolve<T>(target: ConstructorOf<T>): T {
            const injectableOpts = this.parseInjectableOpts(target);
            const args = injectableOpts.deps.map((dep) => this.resolve(dep));
            return new target(...args);
          }

          private parseInjectableOpts(target: ConstructorOf<any>): InjectableOpts {
            const ret = Reflect.getOwnMetadata(DECORATOR_KEY.Injectable, target);
            if (!ret) {
              throw new Error(ERROR_MSG.NO_INJECTABLE);
            }
            return ret;
          }
        }

        主要的核心邏輯是下面幾個(gè):

        • 從構(gòu)造函數(shù)中解析 Injectable 裝飾器通過反射定義的數(shù)據(jù),如果沒有的話,就拋出錯(cuò)誤;稍微要注意一下的是,由于反射數(shù)據(jù)具備繼承性,所以這里只能用 getOwnMetadata 取當(dāng)前目標(biāo)的反射數(shù)據(jù),保證當(dāng)前目標(biāo)一定是被裝飾過的。
        • 然后通過依賴再遞歸創(chuàng)建出依賴的實(shí)例,得到構(gòu)造函數(shù)的入?yún)⒘斜怼?/li>
        • 最后通過實(shí)例化構(gòu)造函數(shù),得到我們要的結(jié)果。

        創(chuàng)建實(shí)例

        到這里最基本的創(chuàng)建對(duì)象的功能就實(shí)現(xiàn)好了,下面這樣的代碼終于能夠正常運(yùn)行了。

              
              @Injectable()
        class Transportation {
          drive() {
            console.log('driving by transportation')
          }
        }

        @Injectable()
        class Student {
          constructor(
            private transportation: Transportation,
          
        ) {}

          gotoSchool() {
            console.log('go to school by')
            this.transportation.drive()
          }
        }

        const container = new Container()
        const student = container.resolve(Student)
        student.gotoSchool()

        也可以通過訪問 codesandbox,左邊選擇 ContainerV1 的模式,看到這樣的結(jié)果。

        ac881470ab3fb39bacb0825ff10f2bba.webp 依賴抽象

        那么基本的 IoC 我們就完成了,但是接下來(lái)我們要改變一下需求,希望能夠在運(yùn)行時(shí)候?qū)⒔煌üぞ咛鎿Q成想要的任何工具,而 Student 的依賴仍然應(yīng)該是一個(gè)可以開的交通工具。

        接下來(lái)我們分兩步實(shí)現(xiàn):

        • 實(shí)例替換:運(yùn)行時(shí)把 Transportation 替換成 Bicycle。
        • 依賴抽象:把 Transportation 從 class 變成 Interface。

        在實(shí)現(xiàn)實(shí)例替換和依賴抽象的能力前,我們先得定義清楚依賴和依賴實(shí)現(xiàn)的關(guān)系,讓 IoC 能夠知道創(chuàng)建哪個(gè)實(shí)例去注入依賴,那就得先說一下 Token 和 Provier。

        Token

        作為依賴的唯一標(biāo)記,可以是 String、Symbol、Constructor、或者 TokenFactory。在沒有依賴抽象的情況下,其實(shí)就是不同的 Constructor 之間直接依賴;String 和 Symbol 是我們?cè)谝蕾嚦橄笾髸?huì)使用到的依賴 ID;而 TokenFactory 是實(shí)在想要進(jìn)行文件循環(huán)引用的時(shí)候,用來(lái)進(jìn)行解析依賴的方案。

        我們可以先不管 TokenFactory, 其他的定義部分 Token 并不需要單獨(dú)實(shí)現(xiàn),只是一個(gè)類型定義:

              
              export type Token<T = any> = string | symbol | ConstructorOf<T>;

        Provider

        注冊(cè)到容器里面的和 Token 形成對(duì)應(yīng)關(guān)系的實(shí)例創(chuàng)建定義,然后 IoC 在拿到 Token 之后,能夠通過 Provider 創(chuàng)建出正確的實(shí)例對(duì)象。再細(xì)分一下,Provider 又可以分成三個(gè)類型:

        • ClassProvider
        • ValueProvider
        • FactoryProvider

        ClassProvider

        使用構(gòu)造函數(shù)來(lái)進(jìn)行實(shí)例化的定義,一般我們前面實(shí)現(xiàn)的簡(jiǎn)單版本的例子,其實(shí)就是這個(gè)模式的簡(jiǎn)化版;再稍微改一下,就很容易實(shí)現(xiàn)這個(gè)版本,并且實(shí)現(xiàn)了 ClassProvider 之后,我們就能夠通過注冊(cè) Provider 的方式去替換前面例子中的交通工具了。

              
              interface ClassProvider<T = any> {
          token: Token<T>
          useClass: ConstructorOf<T>
        }

        ValueProvider

        ValueProvider 在全局已經(jīng)擁有一個(gè)唯一實(shí)現(xiàn),但是在內(nèi)部卻定義了抽象依賴的情況下非常好用。舉個(gè)簡(jiǎn)單的例子,在進(jìn)行簡(jiǎn)潔架構(gòu)的模式下,我們要求核心的代碼邏輯是和上下文無(wú)關(guān)的,那么前端如果想要使用瀏覽器環(huán)境中的全局對(duì)象的時(shí)候,需要進(jìn)行抽象定義,然后把這個(gè)對(duì)象通過 ValueProvider 傳遞進(jìn)去。

              
              interface ClassProvider<T = any> {
          token: Token<T>
          useValue: T
        }

        FactoryProvider

        這個(gè) Provider 會(huì)有一個(gè)工廠函數(shù),然后去創(chuàng)建實(shí)例,當(dāng)我們需要使用工廠模式的時(shí)候,就會(huì)非常有用。

              
              interface FactoryProvider<T = any> {
          token: Token<T>;
          useFactory(c: ContainerInterface): T;
        }

        實(shí)現(xiàn)注冊(cè)和創(chuàng)建

        定義了 Token 和 Provider 之后,我們就可以通過他們實(shí)現(xiàn)一個(gè)注冊(cè)函數(shù),并將 Provider 和創(chuàng)建連接起來(lái)。邏輯也比較簡(jiǎn)單,重點(diǎn)就兩個(gè):

        • 使用 Map 形成 Token 和 Provider 的映射關(guān)系,同時(shí)對(duì) Provider 的實(shí)現(xiàn)進(jìn)行去重,后注冊(cè)的覆蓋前面的。TSyringe 可以進(jìn)行多次注冊(cè),如果構(gòu)造函數(shù)依賴的是一個(gè)示例數(shù)組的話,就會(huì)依次對(duì)每次的 Provider 都創(chuàng)建一個(gè)實(shí)例;不同這種情況實(shí)際上用得很少,并且會(huì)讓 Provider 實(shí)現(xiàn)的復(fù)雜增加很高,感興趣的同學(xué)可以去研究它的這部分實(shí)現(xiàn)和定義方式。
        • 通過解析不同類型的 Provider,然后去做不同的依賴的創(chuàng)建。
              
              export class ContainerV2 implements ContainerInterface {
          private providerMap = new Map<Token, Provider>();

          resolve<T>(token: Token<T>): T {
            const provider = this.providerMap.get(token);
            if (provider) {
              if (ProviderAssertion.isClassProvider(provider)) {
                return this.resolveClassProvider(provider);
              } else if (ProviderAssertion.isValueProvider(provider)) {
                return this.resolveValueProvider(provider);
              } else {
                return this.resolveFactoryProvider(provider);
              }
            }

            return this.resolveClassProvider({
              token,
              useClass: token
            });
          }

          register(...providers: Provider[]) {
            providers.forEach((p) => {
              this.providerMap.set(p.token, p);
            });
          }
         }

        實(shí)例替換

        實(shí)現(xiàn)了支持 Provider 注冊(cè)的函數(shù)之后,我們就可以通過定義 Transportation 的 Provider 的方式,去替換學(xué)生上學(xué)時(shí)候的交通工具了。

              
              const container = new ContainerV2();
        container.register({
          token: Transportation,
          useClass: Bicycle
        });

        const student = container.resolve(Student);
        return student.gotoSchool();

        于是我們?cè)?codesandbox 就能夠看到下面的效果,終于可以騎車去上學(xué)了。

        cbc4f3277f2b59c2935d87066818a43f.webp

        工廠模式

        我們實(shí)現(xiàn)了依賴的替換,在實(shí)現(xiàn)依賴抽象之前,我們先插入一個(gè)新的需求,因?yàn)槠綍r(shí)騎車上學(xué)實(shí)在是太辛苦了,所以周末路況比較好,希望能夠開車上學(xué)。通過工廠模式,我們就能夠使用下面的方式進(jìn)行實(shí)現(xiàn):

              
              const container = new ContainerV2();
        container.register({
          token: Transportation,
          useFactory(c) => {
            if (weekday > 5) {
              return c.resolve(Car);
            } else {
              return c.resolve(Bicycle);
            }
          }
        });

        const student = container.resolve(Student);
        return student.gotoSchool();

        這里是簡(jiǎn)單的工廠模式介紹,TSyringe 和 InversifyJS 都有工廠模式的創(chuàng)建函數(shù),這是比較推薦的方式;同時(shí)大家也可以在其他的的 DI 工具設(shè)計(jì)里面,有一些工具會(huì)把工廠函數(shù)的判斷放到 class 申明的地方。

        這樣不是不可以,單個(gè)實(shí)現(xiàn)單個(gè)作用的時(shí)候?qū)懫饋?lái)會(huì)更簡(jiǎn)單,但是這里就要說到我們引入 DI 的目的,為了解耦。工廠函數(shù)的邏輯判斷其實(shí)是業(yè)務(wù)邏輯的一部分,本身不屬于具體的實(shí)現(xiàn)所歸屬的領(lǐng)域;并且當(dāng)實(shí)現(xiàn)被多個(gè)工廠邏輯中使用的時(shí)候,這個(gè)地方的邏輯就會(huì)變得很奇怪。

        定義抽象

        那么做完實(shí)例替換之后,我們來(lái)看看怎么讓 Transportation 變成一個(gè)抽象,而不是一個(gè)具體的實(shí)現(xiàn)對(duì)象。那么首先第一步,就是需要把 Student 的依賴從具體的實(shí)現(xiàn)邏輯,變成一個(gè)抽象邏輯。

        我們需要的是一個(gè)交通工具抽象,一個(gè)可以開的交通工具,自行車、摩托車、小汽車都可以;只要可以開,什么車都可以。然后再創(chuàng)建一個(gè)新的學(xué)生 class 繼承一下舊的對(duì)象,用于區(qū)分和對(duì)比。

              
              interface ITransportation {
          drive(): string
        }

        @Injectable({ muiltple: true })
        export class StudentWithAbstraction extends Student {
          constructor(protected transportation: ITransportation) {
            super(transportation);
          }
        }

        如果這樣寫的話,會(huì)發(fā)現(xiàn)依賴解析出來(lái)會(huì)是錯(cuò)誤的;因?yàn)樵?TS 編譯的時(shí)候,interface 是一個(gè)類型,運(yùn)行時(shí)就會(huì)變成類型所對(duì)應(yīng)的構(gòu)造對(duì)象,無(wú)法正確解析依賴。

        079390e501e87eb909ee5a6139e2ed33.webp

        所以這里除了定義一個(gè)抽象類型,同時(shí)我們還需要為這個(gè)抽象類型定義一個(gè)唯一標(biāo)記,也就是 Token 里面的 string 或者 symbol。我們一般會(huì)選擇 symbol,這樣全局唯一的值。這里可以利用 TS 同名在值和類型的多重定義,當(dāng)作值和當(dāng)作類型讓 TS 自己去分析。

              
              const ITransportation = Symbol.for('ITransportation')
        interface ITransportation {
          drive(): string
        }

        @Injectable({ muiltple: true })
        export class StudentWithAbstraction extends Student {
          constructor(
            protected @Inject(ITransportation) transportation: ITransportation,
          
        ) {
            super(transportation);
          }
        }

        替換抽象依賴

        注意到的是,除了定義了抽象依賴的 Token 值,我們還需要加一個(gè)額外的裝飾器,讓這個(gè)標(biāo)記構(gòu)造函數(shù)的入?yún)⒁蕾嚕o它一個(gè) Token 標(biāo)記。

              
              function Inject(token: Token{
          return (target: ConstructorOf<any>, key: string | symbol, index: number) => {
            if (!Reflect.hasOwnMetadata(DECORATOR_KEY.Inject, target)) {
              const tokenMap = new Map([[key, token]]);
              Reflect.defineMetadata(DECORATOR_KEY.Inject, tokenMap, target);
            } else {
              const tokenMap: Map<number, Token> = Reflect.getOwnMetadata(
                DECORATOR_KEY.Inject,
                target
              );
              tokenMap.set(index, token);
            }
          };
        }

        同時(shí)在 Injectable 中的邏輯也需要改一下,把相應(yīng)位置的依賴替換掉。

              
              export function Injectable<T>(opts: InjectableDecoratorOpts = {}{
          return (target: new (...args: any[]) => T) => {
            const deps = Reflect.getMetadata(DESIGN_TYPE_NAME.ParamType, target) || [];
            const tokenMap: Map<number, Token> = Reflect.getOwnMetadata(
              DECORATOR_KEY.Inject,
              target
            );
            if (tokenMap) {
              for (const [index, token] of tokenMap.entries()) {
                deps[index] = token;
              }
            }

            const injectableOpts = {
              ...opts,
              deps
            };
            Reflect.defineMetadata(DECORATOR_KEY.Injectable, injectableOpts, target);
          };
        }

        注冊(cè)抽象的 Provider

        到這里還剩下最后的一步,注入 Token 對(duì)應(yīng)的 Provider 就可以使用了,我們只需要更改一下之前的 FactoryProvider 的 Token 定義,然后就達(dá)到了我們的目標(biāo)了。

              
              const ITransportation = Symbol.for('ITransportation')
        interface ITransportation {
          drive(): string
        }

        const container = new ContainerV2();
        container.register({
          token: ITransportation,
          useFactory(c) => {
            if (weekday > 5) {
              return c.resolve(Car);
            } else {
              return c.resolve(Bicycle);
            }
          }
        });
        const student = container.resolve(StudentWithAbstraction);
        return student.gotoSchool();
        實(shí)現(xiàn)惰性創(chuàng)建

        前面我們已經(jīng)實(shí)現(xiàn)了基于構(gòu)造函數(shù)的依賴注入的方式,這種方式很好,不影響構(gòu)造函數(shù)正常的使用。但是這樣有一個(gè)問題是,依賴樹上面所有的對(duì)象實(shí)例都會(huì)在根對(duì)象被創(chuàng)建出來(lái)的時(shí)候,全部創(chuàng)建出來(lái)。這樣子會(huì)有一些浪費(fèi),那些沒有被使用到的實(shí)例原本是可以不創(chuàng)建出來(lái)的。

        為了保證被創(chuàng)建的實(shí)例都是被使用的,那么我們選擇使用時(shí)創(chuàng)建實(shí)例,而不是初始化根對(duì)象的時(shí)候。

        定義使用方式

        在這里我們需要更改一下 Inject 函數(shù),使其能夠同時(shí)支持構(gòu)造函數(shù)的入?yún)⒀b飾和 Property 的裝飾。

              
              const ITransportation = Symbol.for('ITransportation')
        interface ITransportation {
          drive(): string
        }

        @Injectable()
        class Student {
          @Inject(ITransportation)
          private transportation: ITransportation

          gotoSchool() {
            console.log('go to school by')
            this.transportation.drive()
          }
        }

        const container = new Container()
        const student = container.resolve(Student)
        student.gotoSchool()

        屬性裝飾器

        我們結(jié)合 TS 編譯的結(jié)果和類型定義,來(lái)看看 ParameterDecorator 和 PropertyDecorator 的特征。

        下面是 .d.ts 中的描述

        20d57135ccad6a2cbdabd1adcb98179a.webp

        下面是編譯的結(jié)果

        a7f7f4d57bda51dcc956ccba3217ec9c.webp

        可以看到的是有以下幾個(gè)區(qū)別:

        • 入?yún)€(gè)數(shù)是不一樣的,ParameterDecorator 因?yàn)闀?huì)有第幾個(gè)參數(shù)的數(shù)據(jù)。
        • 描述對(duì)象是不一樣的,構(gòu)造函數(shù)的 ParameterDecorator 描述的是構(gòu)造函數(shù);而 PropertyDecorator 描述的是構(gòu)造函數(shù)的 Prototype。

        于是通過識(shí)別標(biāo)記,然后返回 property 的描述文件,在 Prototype 上面添加了對(duì)應(yīng)屬性的 getter 函數(shù),實(shí)現(xiàn)了使用時(shí)進(jìn)行對(duì)象創(chuàng)建的邏輯。

              
              function decorateProperty(_1: object, _2: string | symbol, token: Token{
          const valueKey = Symbol.for("PropertyValue");
          const ret: PropertyDescriptor = {
            get(thisany) {
              if (!this.hasOwnProperty(valueKey)) {
                const container: IContainer = this[REFLECT_KEY.Container];
                const instance = container.resolve(token);
                this[valueKey] = instance;
              }

              return this[valueKey];
            }
          };
          return ret;
        }

        export function Inject(token: Token): any {
          return (
            target: ConstructorOf<any> | object,
            key: string | symbol,
            index?: number
          ) => {
            if (typeof index !== "number" || typeof target === "object") {
              return decorateProperty(target, key, token);
            } else {
              return decorateConstructorParameter(target, index, token);
            }
          };
        }

        這里可以稍微注意一點(diǎn)的是,TS 本身的描述的設(shè)計(jì)里面是不推薦返回 PropertyDescriptor 去更改屬性的定義,但是實(shí)際上在標(biāo)準(zhǔn)和 TS 的實(shí)現(xiàn)里面,他其實(shí)是做了這個(gè)事情的,所以這里未來(lái)也許會(huì)發(fā)生變化。

        循環(huán)依賴

        做完惰性創(chuàng)建,我們來(lái)說一個(gè)有一點(diǎn)點(diǎn)關(guān)系的問題,循環(huán)依賴。一般來(lái)說,我們應(yīng)該從邏輯中避免循環(huán)依賴,但是如果不得不使用的時(shí)候,還是需要提供解決方案來(lái)解決循環(huán)依賴。

        比如這樣一個(gè)例子:

              
              @Injectable()
        class Son {
          @Inject()
          father: Father

          name = 'Thrall'

          getDescription() {
            return `I am ${this.name}, son of ${this.father.name}.`
          }
        }

        @Injectable()
        class Father {
         @Inject()
          son: Son

          name = 'Durotan'

          getDescription() {
            return `I am ${this.name}, my son is ${this.son.name}.`
          }
        }

        const container = new Container()
        const father = container.resolve(Father)
        console.log(father. getDescription())

        為什么會(huì)出問題

        出問題的原因是因?yàn)檠b飾器的運(yùn)行時(shí)機(jī)。構(gòu)造函數(shù)裝飾器的目的是描述構(gòu)造函數(shù),也就是當(dāng)構(gòu)造函數(shù)被申明出來(lái)之后,緊接著就會(huì)運(yùn)行裝飾器的邏輯,而此時(shí)它的依賴還沒有被申明出來(lái),取到的值還是 undefined。

        30b145d7a657dd9b41650db57fb4a607.webp

        文件循環(huán)

        除了文件內(nèi)循環(huán),還有文件之間的循環(huán),比如下面的這個(gè)例子。

        1f68ed75e50cce5df9bf95cb3d2f419c.webp

        會(huì)發(fā)生下面的事情:

        • Father 文件被 Node 讀??;
        • Father 文件在 Node 會(huì)初始化一個(gè) module 注冊(cè)在總的 modules 里面;但是 exports 還是一個(gè)空對(duì)象,等待執(zhí)行賦值;
        • Father 文件開始執(zhí)行第一行,引用 Son 的結(jié)果;
        • 開始讀取 Son 文件;
        • Son 文件在 Node 會(huì)初始化一個(gè) module 注冊(cè)在總的 modules 里面;但是 exports 還是一個(gè)空對(duì)象,等待執(zhí)行賦值;
        • Son 文件執(zhí)行第一行,引用 Father 的結(jié)果,然后讀取到 Father 注冊(cè)的空 module;
        • Son 開始申明構(gòu)造函數(shù);然后讀取 Father 的構(gòu)造函數(shù),但是此時(shí)是 undefined,執(zhí)行裝飾器邏輯;
        • Son 的 Module 賦值 exports 并結(jié)束執(zhí)行;
        • Father 讀取到 Son 的構(gòu)造函數(shù)之后,開始申明構(gòu)造函數(shù);正確讀取 Son 的構(gòu)造函數(shù)執(zhí)行裝飾器邏輯。

        打破循環(huán)

        當(dāng)發(fā)生循環(huán)依賴的時(shí)候,第一個(gè)思路應(yīng)該是打破循環(huán);讓依賴變成沒有循環(huán)的抽象邏輯,打破執(zhí)行之間的先后問題。

              
              export const IFather = Symbol.for("IFather");
        export const ISon = Symbol.for("ISon");

        export interface IPerson {
          name: string;
        }

        @Injectable()
        export class FatherWithAbstraction {
          @Inject(ISon)
          son!: IPerson;

          name = "Durotan";
          getDescription() {
            return `I am ${this.name}, my son is ${this.son.name}.`;
          }
        }

        @Injectable()
        export class SonWithAbstraction {
          @Inject(IFather)
          father!: IPerson;

          name = "Thrall";
          getDescription() {
            return `I am ${this.name}, son of ${this.father.name}.`;
          }
        }

        const container = new ContainerV2(
         { token: IFather, useClass: FatherWithAbstraction },
         { token: ISon, useClass: SonWithAbstraction }
        );
        const father = container.resolve(FatherWithAbstraction);
        const son = container.resolve(SonWithAbstraction);
        console.log(father.getDescription())
        console.log(son.getDescription())

        通過定義公共的 Person 抽象,讓 getDescription 函數(shù)能夠正常執(zhí)行;通過提供 ISon 和 IFather 的 Provider,提供了各自依賴的具體實(shí)現(xiàn),邏輯代碼就能夠正常運(yùn)行了。

        惰性依賴

        除了依賴抽象以外,如果實(shí)在是需要進(jìn)行循環(huán)依賴,我們?nèi)匀荒軌蛲ㄟ^技術(shù)手段解決這個(gè)問題,那就是讓依賴的解析在構(gòu)造函數(shù)定義之后能夠執(zhí)行,而不是和構(gòu)造函數(shù)申明時(shí)執(zhí)行。此時(shí)只需要一個(gè)簡(jiǎn)單的手段,使用函數(shù)執(zhí)行,這就是我們前面說到的 Lazy 邏輯了。

        因?yàn)?JS 的作用域內(nèi)變量提升,在函數(shù)中是能持有變量引用的,只要保證函數(shù)在執(zhí)行的時(shí)候,變量已經(jīng)賦值過了,就能夠正確解析依賴了。

              
              @Injectable()
        class Son {
          @LazyInject(() => Father)
          father: Father

          name = 'Thrall'

          getDescription() {
            return `I am ${this.name}, son of ${this.father.name}.`
          }
        }

        @Injectable()
        class Father {
          @LazyInject(() => Son)
          son: Son

          name = 'Durotan'

          getDescription() {
            return `I am ${this.name}, my son is ${this.son.name}.`
          }
        }

        const container = new Container()
        const father = container.resolve(Father)
        console.log(father. getDescription())

        TokenFactory

        我們需要做的,是增加一個(gè)新的 Token 解析方式,能夠使用函數(shù)動(dòng)態(tài)獲取依賴。

              
              interface TokenFactory<T = any> {
          getToken(): Token<T>;
        }

        然后增加一個(gè) LazyInject 的裝飾器,并兼容這個(gè)邏輯。

              
              export function LazyInject(tokenFn: () => Token): any {
          return (
            target: ConstructorOf<any> | object,
            key: string | symbol,
            index?: number
          ) => {
            if (typeof index !== "number" || typeof target === "object") {
              return decorateProperty(target, key, { getToken: tokenFn });
            } else {
              return decorateConstructorParameter(target, index, { getToken: tokenFn });
            }
          };
        }

        最后在 Container 中兼容一下這個(gè)邏輯,寫一個(gè) V3 的版本 Container。

              
              export class ContainerV3 extends ContainerV2 implements IContainer {
          resolve<T>(tokenOrFactory: Token<T> | TokenFactory<T>): T {
            const token =
              typeof tokenOrFactory === "object"
                ? tokenOrFactory.getToken()
                : tokenOrFactory;

            return super.resolve(token);
          }
        }

        最后看一下使用效果:

              
              const container = new ContainerV3();
        const father = container.resolve(FatherWithLazy);
        const son = container.resolve(SonWithLazy);
        father.getDescription();
        son.getDescription();
        8ea321a54bfe744e51a6dadc226d4fa8.webp 最后

        到這里基本上實(shí)現(xiàn)了一個(gè)基本可用的 DI 工具,稍微回顧一下我們的內(nèi)容:

        • 使用反射和 TS 的裝飾器邏輯,我們實(shí)現(xiàn)了依賴的解析和對(duì)象創(chuàng)建;
        • 通過 Provider 定義,實(shí)現(xiàn)了實(shí)例替換、依賴抽象、工廠模式;
        • 通過使用 PropertyDecorator 定義 getter 函數(shù),我們實(shí)現(xiàn)了惰性創(chuàng)建;
        • 通過 TokenFactory 動(dòng)態(tài)獲取依賴,我們解決了循環(huán)依賴。
        瀏覽 86
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            大尺度擦边短剧 | 超碰逼逼网 | 忘羡情趣用品羞耻play文 | 国产ts在线观看 | 大乳护士喂奶三级hd | 成人免费h无码网站在线观看 | AV网站在线播放 | 男生操女生的视频网站 | 东京热av影院 | 精品一区国产 |