<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          設(shè)計(jì)一個(gè)插件架構(gòu),我是如何思考的

          共 13419字,需瀏覽 27分鐘

           ·

          2021-05-27 20:37

          點(diǎn)擊上方 前端瓶子君,關(guān)注公眾號(hào)

          回復(fù)算法,加入前端編程面試算法每日一題群

           

          來(lái)源:ES2049

          https://juejin.cn/post/6962102171923906596

          引子

          大家可能不知道,鄙人之前人送外號(hào)“過(guò)度設(shè)計(jì)”。作為一個(gè)自信的研發(fā)人員,我總是希望我開(kāi)發(fā)的系統(tǒng)可以解決之后所有的問(wèn)題,用一套抽象可以覆蓋之后所有的擴(kuò)展場(chǎng)景。當(dāng)然最終往往能夠證明我的愚昧與思慮不足。先知曾說(shuō)過(guò)“當(dāng)一個(gè)東西什么都可以做時(shí),他往往什么都做不了”。過(guò)度的抽象,過(guò)度的開(kāi)放性,往往讓接觸他的人無(wú)所適從。講到這里你可能以為我要開(kāi)始講過(guò)度設(shè)計(jì)這個(gè)主題了,但其實(shí)不然,我只是想以這個(gè)話題作為引子,和大家討論一下關(guān)于設(shè)計(jì)一個(gè)插件架構(gòu)我是如何思考的。

          image.png
          image.png

          為什么需要插件

          我們的軟件系統(tǒng)往往是要面向持續(xù)性的迭代的,在開(kāi)發(fā)之初很難把所有需要支持的功能都想清楚,有時(shí)候還需要借助社區(qū)的力量去持續(xù)生產(chǎn)新的功能點(diǎn),或者優(yōu)化已有的功能。這就需要我們的軟件系統(tǒng)具備一定的可擴(kuò)展性。插件模式就是我們常常選用的方法。

          事實(shí)上,現(xiàn)存的大量軟件系統(tǒng)或工具都是使用插件方式來(lái)實(shí)現(xiàn)可擴(kuò)展性的。比如大家最熟悉的小可愛(ài)——VSCode,其插件擁有量已經(jīng)超越了他的前輩 Atom,發(fā)布到市場(chǎng)中的數(shù)量目前是 24894 個(gè)。這些插件幫助我們定制編輯器的外觀或行為,增加額外功能,支持更多語(yǔ)法類型,大大提升了開(kāi)發(fā)效率,同時(shí)也不斷拓展著自身的用戶群體。又或者是我們熟知的瀏覽器 Chrome,其核心競(jìng)爭(zhēng)力之一也是豐富的插件市場(chǎng),使其不論是對(duì)開(kāi)發(fā)者還是普通使用者都已成為了不可獲取的一個(gè)工具。另外還有 Webpack、Nginx 等等各種工具,這邊就不一一贅述了。

          根據(jù)目前各個(gè)系統(tǒng)的插件設(shè)計(jì),總結(jié)下來(lái),我們創(chuàng)造插件主要是幫助我們解決以下兩種類型的問(wèn)題:

          • 為系統(tǒng)提供全新的能力
          • 對(duì)系統(tǒng)現(xiàn)有能力進(jìn)行定制

          同時(shí),在解決上面這類問(wèn)題的時(shí)候做到:

          • 插件代碼與系統(tǒng)代碼在工程上解耦,可以獨(dú)立開(kāi)發(fā),并對(duì)開(kāi)發(fā)者隔離框架內(nèi)部邏輯的復(fù)雜度
          • 可動(dòng)態(tài)化引入與配置

          并且進(jìn)一步地可以實(shí)現(xiàn):

          • 通過(guò)對(duì)多個(gè)單一職責(zé)的插件進(jìn)行組合,可以實(shí)現(xiàn)多種復(fù)雜邏輯,實(shí)現(xiàn)邏輯在復(fù)雜場(chǎng)景中的復(fù)用

          這里提到的不管是提供新能力,還是進(jìn)行能力定制,都既可以針對(duì)系統(tǒng)開(kāi)發(fā)者本身,也可以針對(duì)三方開(kāi)發(fā)者。

          結(jié)合上面的特征,我們嘗試簡(jiǎn)單描述一下插件是什么吧。插件一般是可獨(dú)立完成某個(gè)或一系列功能的模塊。一個(gè)插件是否引入一定不會(huì)影響系統(tǒng)原本的正常運(yùn)行(除非他和另一個(gè)插件存在依賴關(guān)系)。插件在運(yùn)行時(shí)被引入系統(tǒng),由系統(tǒng)控制調(diào)度。一個(gè)系統(tǒng)可以存在復(fù)數(shù)個(gè)插件,這些插件可通過(guò)系統(tǒng)預(yù)定的方式進(jìn)行組合。

          怎么實(shí)現(xiàn)插件模式

          插件模式本質(zhì)是一種設(shè)計(jì)思想,并沒(méi)有一個(gè)一成不變或者是萬(wàn)金油的實(shí)現(xiàn)。但我們經(jīng)過(guò)長(zhǎng)期的代碼實(shí)踐,其實(shí)已經(jīng)可以總結(jié)出一套方法論來(lái)指導(dǎo)插件體系的實(shí)現(xiàn),并且其中的一些實(shí)現(xiàn)細(xì)節(jié)是存在社區(qū)認(rèn)可度比較高的“最佳實(shí)踐”的。本文在攥寫過(guò)程中也參考研讀了社區(qū)比較有名的一些項(xiàng)目的插件模式設(shè)計(jì),包括但不僅限于 Koa、Webpack、Babel 等。

          1. 解決問(wèn)題前首先要定義問(wèn)題

          實(shí)現(xiàn)一套插件模式的第一步,永遠(yuǎn)都是先定義出你需要插件化來(lái)幫助你解決的問(wèn)題是什么。這往往是具體問(wèn)題具體分析的,并總是需要你對(duì)當(dāng)前系統(tǒng)的能力做一定程度的抽象。比如 Babel,他的核心功能是將一種語(yǔ)言的代碼轉(zhuǎn)化為另一種語(yǔ)言的代碼,他面臨的問(wèn)題就是,他無(wú)法在設(shè)計(jì)時(shí)就窮舉語(yǔ)法類型,也不了解應(yīng)該如何去轉(zhuǎn)換一種新的語(yǔ)法,因此需要提供相應(yīng)的擴(kuò)展方式。為此,他將自己的整體流程抽象成了 parse、transform、generate 三個(gè)步驟,并主要面向 parse 和 transform 提供了插件方式做擴(kuò)展性支持。在 parse 這層,他核心要解決的問(wèn)題是怎么去做分詞,怎么去做詞義語(yǔ)法的理解。在 transform 這層要做的則是,針對(duì)特定的語(yǔ)法樹(shù)結(jié)構(gòu),應(yīng)該如何轉(zhuǎn)換成已知的語(yǔ)法樹(shù)結(jié)構(gòu)。

          很明顯,babel 他很清楚地定義了 parse 和 transform 兩層的插件要完成的事情。當(dāng)然也有人可能會(huì)說(shuō),為什么我一定要定義清楚問(wèn)題呢,插件體系本來(lái)就是為未來(lái)的不確定性服務(wù)的。這樣的說(shuō)法對(duì),也不對(duì)。計(jì)算機(jī)程序永遠(yuǎn)是面向確定性的,我們需要有明確的輸入格式,明確的輸出格式,明確的可以依賴的能力。解決問(wèn)題一定是在已知的一個(gè)框架內(nèi)的。這就引出了定義問(wèn)題的一門藝術(shù)——如何賦予不確定以確定性,在不確定中尋找確定。說(shuō)人話,就是“抽象”,這也是為什么最開(kāi)始我會(huì)以過(guò)度設(shè)計(jì)作為引子。

          我在進(jìn)行問(wèn)題定義的時(shí)候,最常使用的是樣本分析法,這種方法并非捷徑,但總歸是有點(diǎn)效的。樣本分析法,就是先著眼于整理已知待解決的問(wèn)題,將這些問(wèn)題作為樣本嘗試分類和提取共性,從而形成一套抽象模式。然后再通過(guò)一些不確定但可能未來(lái)待解決的問(wèn)題來(lái)測(cè)試,是否存在無(wú)法套用的情況。光說(shuō)無(wú)用,下面我們還是以 babel 來(lái)舉個(gè)栗子,當(dāng)然 babel 的抽象設(shè)計(jì)其實(shí)本質(zhì)就是有理論支撐的,在有現(xiàn)有理論已經(jīng)為你做好抽象時(shí),還是盡量用現(xiàn)成的就好啦。

          image.png

          Babel 主要解決的問(wèn)題是把新語(yǔ)法的代碼在不改變邏輯的情況下如何轉(zhuǎn)換成舊語(yǔ)法的代碼,簡(jiǎn)單來(lái)說(shuō)就是 code => code 的一個(gè)問(wèn)題。但是需要轉(zhuǎn)什么,怎么轉(zhuǎn),這些是會(huì)隨著語(yǔ)法規(guī)范不斷更新變化的,因此需要使用插件模式來(lái)提升其未來(lái)可拓展性。我們當(dāng)下要解決的問(wèn)題也許是如何轉(zhuǎn)換 es6 新語(yǔ)法的內(nèi)容,以及 JSX 這種框架定制的 DSL。我們當(dāng)然可以簡(jiǎn)單地串聯(lián)一系列的正則處理,但是你會(huì)發(fā)現(xiàn)每一個(gè)插件都會(huì)有大量重復(fù)的識(shí)別分析類邏輯,不但加大了運(yùn)行開(kāi)銷,同時(shí)也很難避免互相影響導(dǎo)致的問(wèn)題。Babel 選擇了把解析與轉(zhuǎn)換兩個(gè)動(dòng)作拆開(kāi)來(lái),分別使用插件來(lái)實(shí)現(xiàn)。解析的插件要解決的問(wèn)題是如何解析代碼,把 Code 轉(zhuǎn)化為 AST。這個(gè)問(wèn)題對(duì)于不同的語(yǔ)言又可以拆解為相同的兩個(gè)事情,如何分詞,以及如何做詞義解析。當(dāng)然詞義解析還能是如何構(gòu)筑上下文、如何產(chǎn)出 AST 節(jié)點(diǎn)等等,就不再細(xì)分了。最終形成的就是下圖這樣的模式,插件專注解決這幾個(gè)細(xì)分問(wèn)題。轉(zhuǎn)換這邊的,則可分為如何查找固定 AST 節(jié)點(diǎn),以及如何轉(zhuǎn)換,最終形成了 Visitor 模式,這里就不再詳細(xì)說(shuō)了。那么我們?cè)偎伎家幌?,如果未?lái) ES7、8、9(相對(duì)于設(shè)計(jì)場(chǎng)景的未來(lái))等新語(yǔ)法出爐時(shí),是不是依然可以使用這樣的模式去解決問(wèn)題呢?看起來(lái)是可行的。

          image.png

          這就是前面所說(shuō)的在不確定中尋找確定性,盡可能減少系統(tǒng)本身所面臨的不確定,通過(guò)拆解問(wèn)題去限定問(wèn)題。

          那么定義清楚問(wèn)題,我們大概就完成了 1/3 的工作了,下面就是要正式開(kāi)始思考如何設(shè)計(jì)了。

          2. 插件架構(gòu)設(shè)計(jì)繞不開(kāi)的幾大要素

          插件模式的設(shè)計(jì),可以簡(jiǎn)單也可以復(fù)雜,我們不能指望一套插件模式適合所有的場(chǎng)景,如果真的可以的話,我也不用寫這篇文章了,給大家甩一個(gè) npm 地址就完事了。這也是為什么在設(shè)計(jì)之前我們一定要先定義清楚問(wèn)題。具體選擇什么方式實(shí)現(xiàn),一定是根據(jù)具體解決的問(wèn)題權(quán)衡得出的。不過(guò)呢,這事終歸還是有跡可循,有法可依的。

          當(dāng)正式開(kāi)始設(shè)計(jì)我們的插件架構(gòu)時(shí),我們所要思考的問(wèn)題往往離不開(kāi)以下幾點(diǎn)。整個(gè)設(shè)計(jì)過(guò)程其實(shí)就是為每一點(diǎn)選擇合適的方案,最后形成一套插件體系。這幾點(diǎn)分別是:

          • 如何注入、配置、初始化插件
          • 插件如何影響系統(tǒng)
          • 插件輸入輸出的含義與可以使用的能力
          • 復(fù)數(shù)個(gè)插件之間的關(guān)系是怎么樣的

          下面就針對(duì)每個(gè)點(diǎn)詳細(xì)解釋一下

          如何注入、配置、初始化插件

          注入、配置、初始化其實(shí)是幾個(gè)分開(kāi)的事情。但都同屬于 Before 的事情,所以就放在一起講了。

          先來(lái)講一講注入,其實(shí)本質(zhì)上就是如何讓系統(tǒng)感知到插件的存在。注入的方式一般可以分為 聲明式 和 編程式。聲明式就是通過(guò)配置信息,告訴系統(tǒng)應(yīng)該去哪里去取什么插件,系統(tǒng)運(yùn)行時(shí)會(huì)按照約定與配置去加載對(duì)應(yīng)的插件。類似 Babel,可以通過(guò)在配置文件中填寫插件名稱,運(yùn)行時(shí)就會(huì)去 modules 目錄下去查找對(duì)應(yīng)的插件并加載。編程式的就是系統(tǒng)提供某種注冊(cè) API,開(kāi)發(fā)者通過(guò)將插件傳入 API 中來(lái)完成注冊(cè)。兩種對(duì)比的話,聲明式主要適合自己?jiǎn)为?dú)啟動(dòng)不用接入另一個(gè)軟件系統(tǒng)的場(chǎng)景,這種情況一般使用編程式進(jìn)行定制的話成本會(huì)比較高,但是相對(duì)的,對(duì)于插件命名和發(fā)布渠道都會(huì)有一些限制。編程式則適合于需要在開(kāi)發(fā)中被引入一個(gè)外部系統(tǒng)的情況。當(dāng)然也可以兩種方式都進(jìn)行支持。

          然后是插件配置,配置的主要目的是實(shí)現(xiàn)插件的可定制,因?yàn)橐粋€(gè)插件在不同使用場(chǎng)景下,可能對(duì)于其行為需要做一些微調(diào),這時(shí)候如果每個(gè)場(chǎng)景都去做一個(gè)單獨(dú)的插件那就有點(diǎn)小題大作了。配置信息一般在注入時(shí)一起傳入,很少會(huì)支持注入后再進(jìn)行重新配置。配置如何生效其實(shí)也和插件初始化的有點(diǎn)關(guān)聯(lián),初始化這事可以分為方式和時(shí)機(jī)兩個(gè)細(xì)節(jié)來(lái)講,我們先講講方式。常見(jiàn)的方式我大概列舉兩種。一種是工廠模式,一個(gè)插件暴露出來(lái)的是一個(gè)工廠函數(shù),由調(diào)用者或者插件架構(gòu)來(lái)將提供配置信息傳入,生成插件實(shí)例。另一種是運(yùn)行時(shí)傳入,插件架構(gòu)在調(diào)度插件時(shí)會(huì)通過(guò)約定的上下文把配置信息給到插件。工廠模式咱們繼續(xù)拿 babel 來(lái)舉例吧。

          function declare<
              O extends Record<stringany>,
              R extends babel.PluginObj = babel.PluginObj
          >(
              builder: (api: BabelAPI, options: O, dirname: string) => R,
          ): (api: object, options: O | null | undefined, dirname: string) => R
          ;
          復(fù)制代碼

          上面代碼中的 builder 呢就是我們說(shuō)到的工廠函數(shù)了,他最終將產(chǎn)出一個(gè) Plugin 實(shí)例。builder 通過(guò) options 獲取到配置信息,并且這里設(shè)計(jì)上還支持通過(guò) api 設(shè)置一些運(yùn)行環(huán)境信息,不過(guò)這并不是必須的,所以不細(xì)說(shuō)了。簡(jiǎn)化一下就是:

          type TPluginFactory<OPTIONS, PLUGIN> = (options: OPTIONS) => PLUGIN;
          復(fù)制代碼

          所以初始化呢,自然也可以是通過(guò)調(diào)用工廠函數(shù)初始化、初始化完成后再注入、不需要初始化三種。一般我們不選擇初始化完成后再注入,因?yàn)榻怦畹脑V求,我們盡量在插件中只做聲明。是否使用工廠模式則看插件是否需要初始化這一步驟。大部分情況下,如果你決定不好,還是推薦優(yōu)先選擇工廠模式,可以應(yīng)對(duì)后面更多復(fù)雜場(chǎng)景。初始化的時(shí)機(jī)也可以分為注入即初始化、統(tǒng)一初始化、運(yùn)行時(shí)才初始化。很多情況下 注入即初始化、統(tǒng)一初始化 可以結(jié)合使用,具體的區(qū)分我嘗試通過(guò)一張表格來(lái)對(duì)應(yīng)說(shuō)明:


          注入即初始化統(tǒng)一初始化運(yùn)行時(shí)才初始化
          是否是純邏輯型都可以使用
          是否需要預(yù)掛載或修改系統(tǒng)
          不是
          插件初始化是否有相互依賴關(guān)系不是不是
          插件初始化是否有性能開(kāi)銷都可以使用
          不是

          另外還有個(gè)問(wèn)題也在這里提一下,在一些系統(tǒng)中,我們可能依賴許多插件組合來(lái)完成一件復(fù)雜的事情,為了屏蔽單獨(dú)引入并配置插件的復(fù)雜性,我們還會(huì)提供一種 Preset 的概念,去打包多個(gè)插件及其配置。使用者只需要引入 Preset 即可,不用關(guān)心里面有哪些插件。例如 Babel 在支持 react 語(yǔ)法時(shí),其實(shí)要引入 syntax-jsx transform-react-jsx transform-react-display-name transform-react-pure-annotationsd 等多個(gè)插件,最終給到的是 preset-react 這樣一個(gè)包。

          插件如何影響系統(tǒng)

          插件對(duì)系統(tǒng)的影響我們可以總結(jié)為三方面:行為、交互、展示。單獨(dú)一個(gè)插件可能只涉及其中一點(diǎn)。根據(jù)具體場(chǎng)景,有些方面也不必去影響,比如一個(gè)邏輯引擎類型的系統(tǒng),就大概率不需要展示這塊的東西啦。

          VSCode 插件大致覆蓋了這三個(gè),所以我們可以拿一個(gè)簡(jiǎn)單的插件來(lái)看下。這里我們選擇了 Clock in status bar 這個(gè)插件,這個(gè)插件的功能很簡(jiǎn)單,就是在狀態(tài)欄加一個(gè)時(shí)鐘,或者你可以在編輯內(nèi)容內(nèi)快速插入當(dāng)前時(shí)間。

          image.png

          整個(gè)項(xiàng)目里最主要的是下面這些內(nèi)容:

          image.png

          在 package.json 中,通過(guò)擴(kuò)展的 contributes 字段為插件注冊(cè)了一個(gè)命令,和一個(gè)配置菜單。

          "main""./extension"// 入口文件地址
          "contributes": {
            "commands": [{
              "command""clock.insertDateTime",
              "title""Clock: Insert date and time"
            }],
            "configuration": {
              "type""object",
              "title""Clock configuration",
              "properties": {
                "clock.dateFormat": {
                  "type""string",
                  "default""hh:MM TT",
                  "description""Clock: Date format according to https://github.com/felixge/node-dateformat"
                }
              }
            }
          },
          復(fù)制代碼

          在入口文件 extension.js 中則通過(guò)系統(tǒng)暴露的 API 創(chuàng)建了狀態(tài)欄的 UI,并注冊(cè)了命令的具體行為。

          'use strict';

          // The module 'vscode' contains the VS Code extensibility API
          // Import the module and reference it with the alias vscode in your code below
          const
            clockService = require('./clockservice'),
            ClockStatusBarItem = require('./clockstatusbaritem'),
            vscode = require('vscode');

          // this method is called when your extension is activated
          // your extension is activated the very first time the command is executed
          function activate(context{
            // Use the console to output diagnostic information (console.log) and errors (console.error)
            // This line of code will only be executed once when your extension is activated

            // The command has been defined in the package.json file
            // Now provide the implementation of the command with  registerCommand
            // The commandId parameter must match the command field in package.json
            context.subscriptions.push(new ClockStatusBarItem());

            context.subscriptions.push(vscode.commands.registerTextEditorCommand('clock.insertDateTime', (textEditor, edit) => {
              textEditor.selections.forEach(selection => {
                const
                  start = selection.start,
                  end = selection.end;

                if (start.line === end.line && start.character === end.character) {
                  edit.insert(start, clockService());
                } else {
                  edit.replace(selection, clockService());
                }
              });
            }));
          }

          exports.activate = activate;

          // this method is called when your extension is deactivated
          function deactivate() {
          }

          exports.deactivate = deactivate;
          復(fù)制代碼

          上述這個(gè)例子有點(diǎn)大塊兒,有點(diǎn)稍顯粗糙。那么總結(jié)下來(lái)我們看一下,在最開(kāi)始我們提到的三個(gè)方面分別是如何體現(xiàn)的。

          • UI:我們通過(guò)系統(tǒng) API 創(chuàng)建了一個(gè)狀態(tài)欄組件。我們通過(guò)配置信息構(gòu)建了一個(gè) 配置頁(yè)。
          • 交互:我們通過(guò)注冊(cè)命令,增加了一項(xiàng)指令交互。
          • 邏輯:我們新增了一項(xiàng)插入當(dāng)前時(shí)間的能力邏輯。

          所以我們?cè)谠O(shè)計(jì)一個(gè)插件架構(gòu)時(shí)呢,也主要就從這三方面是否會(huì)被影響考慮即可。那么插件又怎么去影響系統(tǒng)呢,這個(gè)過(guò)程的前提是插件與系統(tǒng)間建立一份契約,約定好對(duì)接的方式。這份契約可以包含文件結(jié)構(gòu)、配置格式、API 簽名。還是結(jié)合 VSCode 的例子來(lái)看看:

          • 文件結(jié)構(gòu):沿用了 NPM 的傳統(tǒng),約定了目錄下 package.json 承載元信息。
          • 配置格式:約定了 main 的配置路徑作為代碼入口,私有字段 contributes 聲明命令與配置。
          • API 簽名:約定了擴(kuò)展必須提供 activate 和 deactivate 兩個(gè)接口。并提供了 vscode 下各項(xiàng) API 來(lái)完成注冊(cè)。

          UI 和 交互的定制邏輯,本質(zhì)上依賴系統(tǒng)本身的實(shí)現(xiàn)方式。這里重點(diǎn)講一下一般通過(guò)哪些模式,去調(diào)用插件中的邏輯。

          直接調(diào)用

          這個(gè)模式很直白,就是在系統(tǒng)的自身邏輯中,根據(jù)需要去調(diào)用注冊(cè)的插件中約定的 API,有時(shí)候插件本身就只是一個(gè) API。比如上面例子中的 activate 和 deactivate 兩個(gè)接口。這種模式很常見(jiàn),但調(diào)用處可能會(huì)關(guān)注比較多的插件處理相關(guān)邏輯。

          鉤子機(jī)制(事件機(jī)制)

          系統(tǒng)定義一系列事件,插件將自己的邏輯掛載在事件監(jiān)聽(tīng)上,系統(tǒng)通過(guò)觸發(fā)事件進(jìn)行調(diào)度。上面例子中的 clock.insertDateTime 命令也可以算是這類,是一個(gè)命令觸發(fā)事件。在這個(gè)機(jī)制上,webpack 是一個(gè)比較明顯的例子,我們來(lái)看一個(gè)簡(jiǎn)單的 webpack 插件:

          // 一個(gè) JavaScript 命名函數(shù)。
          function MyExampleWebpackPlugin() {

          };

          // 在插件函數(shù)的 prototype 上定義一個(gè) `apply` 方法。
          MyExampleWebpackPlugin.prototype.apply = function(compiler{
            // 指定一個(gè)掛載到 webpack 自身的事件鉤子。
            compiler.plugin('webpacksEventHook'function(compilation /* 處理 webpack 內(nèi)部實(shí)例的特定數(shù)據(jù)。*/, callback{
              console.log("This is an example plugin!!!");

              // 功能完成后調(diào)用 webpack 提供的回調(diào)。
              callback();
            });
          };
          復(fù)制代碼

          這里的插件就將“在 console 打印 This is an example plugin!!!”這一行為注冊(cè)到了 webpacksEventHook 這個(gè)鉤子上,每當(dāng)這個(gè)鉤子被觸發(fā)時(shí),會(huì)調(diào)用一次這個(gè)邏輯。這種模式比較常見(jiàn),webpack 也專門做了一份封裝服務(wù) github.com/webpack/tap… 通過(guò)定義了多種不同調(diào)度邏輯的鉤子,你可以在任何系統(tǒng)中植入這款模式,并能滿足你不同的調(diào)度需求(調(diào)度模式我們?cè)谙乱徊糠种性敿?xì)講述)。

          const {
           SyncHook,
           SyncBailHook,
           SyncWaterfallHook,
           SyncLoopHook,
           AsyncParallelHook,
           AsyncParallelBailHook,
           AsyncSeriesHook,
           AsyncSeriesBailHook,
           AsyncSeriesWaterfallHook
          } = require("tapable");
          復(fù)制代碼
          image.png

          鉤子機(jī)制適合注入點(diǎn)多,松耦合需求高的插件場(chǎng)景,能夠減少整個(gè)系統(tǒng)中插件調(diào)度的復(fù)雜度。成本就是額外引了一套鉤子機(jī)制了,不算高的成本,但也不是必要的。

          使用者調(diào)度機(jī)制

          這種模式本質(zhì)就是將插件提供的能力,統(tǒng)一作為系統(tǒng)的額外能力對(duì)外透出,最后又系統(tǒng)的開(kāi)發(fā)使用者決定什么時(shí)候調(diào)用。例如 JQuery 的插件會(huì)注冊(cè) fn 中的額外行為,或者是 Egg 的插件可以向上下文中注冊(cè)額外的接口能力等。這種模式我個(gè)人認(rèn)為比較適合又需要定制更多對(duì)外能力,又需要對(duì)能力的出口做收口的場(chǎng)景。如果你希望用戶通過(guò)統(tǒng)一的模式調(diào)用你的能力,那大可嘗試一下。你可以嘗試使用新的 Proxy 特性來(lái)實(shí)現(xiàn)這種模式。

          不管是系統(tǒng)對(duì)插件的調(diào)用還是插件調(diào)用系統(tǒng)的能力,我們都是需要一個(gè)確定的輸入輸出信息的,這也是我們上面 API 簽名所覆蓋到的信息。我們會(huì)在下一部分專門講一講。

          插件輸入輸出的含義與可以使用的能力

          插件與系統(tǒng)間最重要的契約就是 API 簽名,這涉及了可以使用哪些 API,以及這些 API 的輸入輸出是什么。

          可以使用的能力

          是指插件的邏輯可以使用的公共工具,或者可以通過(guò)一些方式獲取或影響系統(tǒng)本身的狀態(tài)。能力的注入我們常使用的方式是參數(shù)、上下文對(duì)象或者工廠函數(shù)閉包。

          提供的能力類型主要有下面四種:

          • 純工具:不影響系統(tǒng)狀態(tài)
          • 獲取當(dāng)前系統(tǒng)狀態(tài)
          • 修改當(dāng)前系統(tǒng)狀態(tài)
          • API 形式注入功能:例如注冊(cè) UI,注冊(cè)事件等

          對(duì)于需要提供哪些能力,一般的建議是根據(jù)插件需要完成的工作,提供最小夠用范圍內(nèi)的能力,盡量減少插件破壞系統(tǒng)的可能性。在部分場(chǎng)景下,如果不能通過(guò) API 有效控制影響范圍,可以考慮為插件創(chuàng)造沙箱環(huán)境,比如插件內(nèi)可能會(huì)調(diào)用 global 的接口等。

          輸入輸出

          當(dāng)我們的插件是處在我們系統(tǒng)一個(gè)特定的處理邏輯流程中的(常見(jiàn)于直接調(diào)用機(jī)制或鉤子機(jī)制),我們的插件重點(diǎn)關(guān)注的就是輸入與輸出。此時(shí)的輸入與輸出一定是由邏輯流程本身所處的邏輯來(lái)決定的。輸入輸出的結(jié)構(gòu)需要與插件的職責(zé)強(qiáng)關(guān)聯(lián),盡量保證可序列化能力(為了防止過(guò)度膨脹以及本身的易讀性),并根據(jù)調(diào)度模式有額外的限制條件(下面會(huì)講)。如果你的插件輸入輸出過(guò)于復(fù)雜,可能要反思一下抽象是否過(guò)于粗粒度了。

          另外還需要對(duì)插件邏輯保證異常捕捉,防止對(duì)系統(tǒng)本身的破壞。

          還是 Babel Parser 那個(gè)例子。

          {
            parseExprAtom(refExpressionErrors: ?ExpressionErrors): N.Expression;
            getTokenFromCode(code: number): void// 內(nèi)部再調(diào)用 finishToken 來(lái)影響邏輯
            updateContext(prevType: TokenType): void// 內(nèi)部通過(guò)修改 this.state 來(lái)改變上下文信息
          }
          復(fù)制代碼

          意料之中的輸入,堅(jiān)信不疑的輸出

          復(fù)數(shù)個(gè)插件之間的關(guān)系是怎么樣的

          Each plugin should only do a small amount of work, so you can connect them like building blocks. You may need to combine a bunch of them to get the desired result.

          這里我們討論的是,在同一個(gè)擴(kuò)展點(diǎn)上注入的插件,應(yīng)該以什么形式做組合。常見(jiàn)的形式如下:

          覆蓋式

          只執(zhí)行最新注冊(cè)的邏輯,跳過(guò)原始邏輯

          image.png

          管道式

          輸入輸出相互銜接,一般輸入輸出是同一個(gè)數(shù)據(jù)類型。

          image.png

          洋蔥圈式

          在管道式的基礎(chǔ)上,如果系統(tǒng)核心邏輯處于中間,插件同時(shí)關(guān)注進(jìn)與出的邏輯,則可以使用洋蔥圈模型。

          image.png

          這里也可以參考 koa 中的中間件調(diào)度模式 github.com/koajs/compo…

          const middleware = async (...params, next) => {
            // before
            await next();
            // after
          };
          復(fù)制代碼

          集散式

          集散式就是每一個(gè)插件都會(huì)執(zhí)行,如果有輸出則最終將結(jié)果進(jìn)行合并。這里的前提是存在方案,可以對(duì)執(zhí)行結(jié)果進(jìn)行 merge。

          image.png

          另外調(diào)度還可以分為 同步 和 異步 兩個(gè)方式,主要看插件邏輯是否包含異步行為。同步的實(shí)現(xiàn)會(huì)簡(jiǎn)單一點(diǎn),不過(guò)如果你不能確定,那也可以考慮先把異步的一起考慮進(jìn)來(lái)。類似 www.npmjs.com/package/neo… 這樣的工具可以很好地幫助你。如果你使用了 tapble,那里面已經(jīng)有相應(yīng)的定義。

          另外還需要注意的細(xì)節(jié)是:

          • 順序是先注冊(cè)先執(zhí)行,還是反過(guò)來(lái),需要給到明確的解釋或一致的認(rèn)知。
          • 同一個(gè)插件重復(fù)注冊(cè)了該怎么處理。

          總結(jié)

          當(dāng)你跟著這篇文章的思路,把這些問(wèn)題都思考清楚之后,想必你的腦海中一定已經(jīng)有了一個(gè)插件架構(gòu)的雛形了。剩下的可能是結(jié)合具體問(wèn)題,再通過(guò)一些設(shè)計(jì)模式去優(yōu)化開(kāi)發(fā)者的體驗(yàn)了。個(gè)人認(rèn)為設(shè)計(jì)一個(gè)插件架構(gòu),是一定逃不開(kāi)針對(duì)這些問(wèn)題的思考的,而且只有去真正關(guān)注這些問(wèn)題,才能避開(kāi)炫技、過(guò)度設(shè)計(jì)等面向未來(lái)開(kāi)發(fā)時(shí)時(shí)常會(huì)犯的錯(cuò)誤。當(dāng)然可能還差一些東西,一些推薦的實(shí)現(xiàn)方式也可能會(huì)過(guò)時(shí),這些就歡迎大家?guī)兔χ刚病?/p>

          作者:ES2049 / armslave00

          文章可隨意轉(zhuǎn)載,但請(qǐng)保留此原文鏈接。

          非常歡迎有激情的你加入 ES2049 Studio,簡(jiǎn)歷請(qǐng)發(fā)送至 [email protected] 。

          最后

          歡迎關(guān)注【前端瓶子君】??ヽ(°▽°)ノ?
          回復(fù)「算法」,加入前端編程源碼算法群,每日一道面試題(工作日),第二天瓶子君都會(huì)很認(rèn)真的解答喲!
          回復(fù)「交流」,吹吹水、聊聊技術(shù)、吐吐槽!
          回復(fù)「閱讀」,每日刷刷高質(zhì)量好文!
          如果這篇文章對(duì)你有幫助,在看」是最大的支持
          》》面試官也在看的算法資料《《
          “在看和轉(zhuǎn)發(fā)”就是最大的支持


          瀏覽 34
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  免费看黄秘 片视频 | 国产视频123区 | 中文字幕日产A片在线看 | 日韩国产精品一级毛片在线 | 久热这里只有精品10 |