Dojo 中間件進階

翻譯自:https://github.com/dojo/framework/blob/master/docs/en/middleware/supplemental.md

中間件基本原理

Dojo 提供了渲染中間件的概念,以幫助銜接響應式、函數部件與底層的命令式 DOM 結構。

如果部件能夠訪問 DOM 信息,某些 web 應用程序需求就更容易實現。常見的例子有:

  • 響應式 UI 不與特定的設備類型綁定,而是根據可用的頁面區域改變元素的大小。
  • 僅當某些元素在用戶可視區域可見時,才延遲加載需要的數據——例如無限滾動列表。
  • 引導元素獲取焦點,並在用戶變更焦點後進行響應。

但是,中間件並非必須與 DOM 綁定;這個概念還適合部件的渲染生命週期等更常用的情況。此類需求的常見示例如下:

  • 如果獲取數據的開銷大,則在渲染間緩存數據
  • 根據特定條件暫停和恢復部件的渲染;在所需信息不可用時,避免無用的渲染
  • 將函數部件標記爲無效,以便 Dojo 可以重新渲染部件

一箇中間件組件一般公開的某些功能與部件渲染的 DOM 元素有關;大多是部件的根節點。中間件系統爲部件在瀏覽器中的展示和交互提供了更高級的控制,並且允許部件以一致的方式使用幾個新興的 Web 標準。

如果部件在其底層的 DOM 元素存在之前訪問中間件的某些屬性,則返回合理的默認值。還有一些中間件可以暫停部件的渲染,直到滿足某些條件。使用這些中間件,部件能避免不必要的渲染,直到所需的信息可用爲止,然後 Dojo 將在數據可用時獲取中間件的正確屬性值,自動重新渲染受影響的部件。

創建中間件

中間件是使用 @dojo/framework/core/vdom 中的 create() 工廠方法定義的。這與創建函數部件的過程類似,但是中間件工廠返回的並不是 VDOM 節點,而是允許訪問中間件功能集的 API。簡單的中間件只需要一個函數調用來實現它們的需求,也可以直接返回一個函數,而不需要將中間件包裝在一個對象中。

下面介紹一箇中間件組件,它有一個簡單的 get()set() API:

src/middleware/myMiddleware.ts

import { create } from '@dojo/framework/core/vdom';

const factory = create();

export const myMiddleware = factory(() => {
    return {
        get() {},
        set() {}
    };
});

export default myMiddleware;

使用中間件

中間件主要用在函數部件中,但也可以通過組合形成其他中間件,以實現更復雜的需求。這兩種情況下,任何用到的中間件都會作爲屬性傳給 create() 方法,然後通過部件或中間件工廠實現函數中的 middleware 參數使用這些中間件。

例如,在部件中使用上面的 myMiddleware 中間件:

src/widgets/MiddlewareConsumerWidget.tsx

import { create, tsx } from '@dojo/framework/core/vdom';
import myMiddleware from '../middleware/myMiddleware';

const render = create({ myMiddleware });
export const MiddlewareConsumerWidget = render(({ middleware: { myMiddleware } }) => {
    myMiddleware.set();
    return <div>{`Middleware value: ${myMiddleware.get()}`}</div>;
});

export default MiddlewareConsumerWidget;

組合中間件

以下示例演示了用中間件組合出新的中間件,以實現更有用的需求:

  • 在本地緩存中取一個值
  • 如果緩存未命中,則從外部獲取值
  • 在等待外部的值返回時,暫停使用該中間件的部件的進一步渲染
  • 一旦外部的值可以通過本地緩存訪問,就恢復渲染並讓使用的部件失效,以重新渲染這些部件

src/middleware/ValueCachingMiddleware.ts

import { create, defer, invalidator } from '@dojo/framework/core/vdom';
import { cache } from '@dojo/framework/core/middleware/cache';

const factory = create({ defer, cache });

export const ValueCachingMiddleware = factory(({ middleware: { defer, cache, invalidator }}) => {
    get(key: string) {
        const cachedValue = cache.get(key);
        if (cachedValue) {
            return cachedValue;
        }
        // Cache miss: fetch the value somehow through a promise
        const promise = fetchExternalValue(value);
        // Pause further widget rendering
        defer.pause();
        promise.then((result) => {
            // Cache the value for subsequent renderings
            cache.set(key, result);
            // Resume widget rendering once the value is available
            defer.resume();
            // Invalidate the widget for a re-render
            invalidator();
        });
        return null;
    }
});

export default ValueCachingMiddleware;

傳入中間件屬性

由於中間件是通過 create() 工具函數定義的,因此爲中間件指定屬性接口的方式,與爲函數部件指定屬性接口的方式相同。主要的區別是中間件屬性會被添加到所有消費者部件的屬性接口中。這意味着屬性值是在實例化部件時設置的,而不是在部件使用中間件時。在整個組合層次結構中,屬性被看作是隻讀的,因此中間件不能修改屬性值。

下面是具有屬性接口的中間件示例:

src/middleware/middlewareWithProperties.tsx

import { create } from '@dojo/framework/core/vdom';

const factory = create().properties<{ conditional?: boolean }>();

export const middlewareWithProperties = factory(({ properties }) => {
    return {
        getConditionalState() {
            return properties().conditional ? 'Conditional is true' : 'Conditional is false';
        }
    };
});

export default middlewareWithProperties;

在部件中使用中間件及其屬性:

src/widgets/MiddlewarePropertiesWidget.tsx

import { create, tsx } from '@dojo/framework/core/vdom';
import middlewareWithProperties from '../middleware/middlewareWithProperties';

const render = create({ middlewareWithProperties });
export const MiddlewarePropertiesWidget = render(({ properties, middleware: { middlewareWithProperties } }) => {
    return (
        <virtual>
            <div>{`Middleware property value: ${properties().conditional}`}</div>
            <div>{`Middleware property usage: ${middlewareWithProperties.getConditionalState()}`}</div>
        </virtual>
    );
});

export default MiddlewarePropertiesWidget;

然後,當創建 MiddlewarePropertiesWidget 實例時,指定中間件的 conditional 屬性值,例如:

src/main.tsx

import renderer, { tsx } from '@dojo/framework/core/vdom';
import MiddlewarePropertiesWidget from './widgets/MiddlewarePropertiesWidget';

const r = renderer(() => <MiddlewarePropertiesWidget conditional={true} />);
r.mount();

可用的中間件

Dojo 提供了多種可選的中間件,當部件需要實現特定需求時,可以包含這些中間件。

cache

提供了一個簡單的、部件內的緩存,可以在部件的多次渲染間保留少量數據。

API:

import cache from '@dojo/framework/core/middleware/cache';
  • cache.get&lt;T = any&gt;(key: any): T | null
    • 根據指定的 key 獲取當前緩存值,如果緩存未命中則返回 null
  • cache.set&lt;T = any&gt;(key: any, value: T)
    • 將提供的 value 存儲在緩存中,並與指定的 key 關聯。
  • cache.clear()
    • 清除當前在部件本地緩存中存儲的所有值。

icache

組合了 cacheinvalidator 中間件功能,以提供一個緩存,支持延遲值的解析,並在值可用時自動讓部件失效。

API:

import icache from '@dojo/framework/core/middleware/icache';
  • icache.getOrSet&lt;T = any&gt;(key: any, value: any): T | undefined
    • 如果存在的話,則返回根據 key 獲取的值,否則就將 key 值設置爲 value。在這兩種情況下,如果緩存值尚未解析,則返回 undefined
  • icache.get&lt;T = any&gt;(key: any): T | undefined
    • 根據 key 獲取緩存值,如果未設置值或者該值處在掛起狀態,則返回 undefined
  • icache.set(key: any, value: any)
    • 將提供的 value 設置給指定的 key。如果 value 是一個函數,則將調用它以獲取要緩存的實際值。如果函數返回的是 promise,則會先緩存一個“pending”值,直到解析出最終的值。在所有場景中,一旦一個值可用並存儲到緩存中,該部件將被標記爲無效,這樣就可以使用最終的值重新渲染。
  • clear()
    • 清除當前在部件本地緩存中存儲的所有值。

theme

允許部件渲染時爲 CSS 樣式類設置主題,並且允許爲應用程序設置主題以及確定當前設置的主題,如果有設置的話。

Dojo 的樣式和主題參考指南中有詳細說明。

API:

import theme from '@dojo/framework/core/middleware/theme';
  • theme.classes&lt;T extends ClassNames&gt;(css: T): T
    • 爲部件傳入一個或多個 CSS 類名,然後接收根據當前設置的主題而修改後的名字,以便在返回部件的虛擬節點時使用。
  • theme.set(css: Theme)
    • 允許應用程序設置指定的主題。
  • theme.get(): Theme | undefined
    • 返回當前設置的主題,如果沒有設置主題則返回 undefined。通常在應用程序的根部件中使用。

i18n

允許在渲染部件時,將消息文本本地化,也允許應用程序進行區域設置,以及獲取當前設置的區域,如果有設置的話。

Dojo 的國際化參考指南中有詳細說明。

API:

import i18n from '@dojo/framework/core/middleware/i18n';
  • i18n.localize&lt;T extends Messages&gt;(bundle: Bundle&lt;T&gt;, useDefaults = false): LocalizedMessages&lt;T&gt;
    • 從指定的 bundle 中返回根據當前設置的區域而本地化的一組消息。useDefaults 用於控制當前區域對應的值不可用時,是否返回來自默認語言的消息。默認值爲 false,在這種情況下返回的是空值,而不是默認語言的消息。
  • i18n.set(localeData?: LocaleData)
    • 允許應用程序設置指定的區域。
  • i18n.get()
    • 返回當前設置的區域,如果沒有設置區域則返回 undefined。通常在應用程序的根部件中使用。

dimensions

提供部件底層節點的各種大小和位置信息。

API:

import dimensions from '@dojo/framework/core/middleware/dimensions';
  • dimensions.get(key: string | number): Readonly&lt;DimensionResults&gt;
    • 返回部件中由節點的 key 屬性標識的 DOM 元素的尺寸信息。如果當前部件中不存在此節點(尚未渲染或指定的 key 無效),則返回的值都是 0

返回的 DimensionResults 包含以下屬性,這些屬性映射到指定 DOM 元素的相關屬性:

Property Source
client.left node.clientLeft
client.top node.clientTop
client.width node.clientWidth
client.height node.clientHeight
position.bottom node.getBoundingClientRect().bottom
position.left node.getBoundingClientRect().left
position.right node.getBoundingClientRect().right
position.top node.getBoundingClientRect().top
size.width node.getBoundingClientRect().width
size.height node.getBoundingClientRect().height
scroll.left node.scrollLeft
scroll.top node.scrollTop
scroll.height node.scrollHeight
scroll.width node.scrollWidth
offset.left node.offsetLeft
offset.top node.offsetTop
offset.width node.offsetWidth
offset.height node.offsetHeight

intersection

使用 Intersection Observer API 提供關於節點在特定可視區域是否可見等信息。

因爲 Intersection Observer API 是一個新興的 Web 標準,因此在不支持此 API 的瀏覽器中運行應用程序時,框架會自動確保底層的 API 可用。注意,Dojo 6 版本不支持 Intersection Observer API v2

API:

import intersection from '@dojo/framework/core/middleware/intersection';
  • intersection.get(key: string | number, options: IntersectionGetOptions = {}): IntersectionResult
    • 返回部件中由節點的 key 屬性標識的 DOM 元素的交叉(intersection)信息。如果當前部件中不存在此節點(尚未渲染或指定的 key 無效),會返回一個結果,表示無交叉。

option 參數允許對如何計算交叉做更多控制。可用字段與 intersection observer API options 相同。

IntersectionResult 屬性:

屬性 類型 說明
intersectionRatio number 與根元素的可視區域相交的元素邊界框的比率,從 0.01.0,默認的根元素是瀏覽器的可視區域,除非通過 options.root 元素指定了一個元素。
isIntersecting boolean 值爲 true 時表示目標元素與根元素的可視區域交叉(表示過渡到了交叉狀態)。值爲 false 時表示從交叉過渡到了不交叉。

resize

允許部件使用 ResizeObserver 響應 DOM 節點的 resize 事件,並且在調整大小時提供節點新大小的更新信息。使用這個中間件是創建適配各種視窗大小的響應式應用程序的有效方法。

因爲 Resize Observer 是一個新興的 Web 標準,因此在不支持此 API 的瀏覽器中運行應用程序時,框架會自動確保底層的 API 可用。

API:

import resize from '@dojo/framework/core/middleware/resize';
  • resize.get(key: string | number): DOMRectReadOnly | null
    • 返回部件中由節點的 key 屬性標識的 DOM 元素的尺寸信息。如果當前部件中不存在此節點(尚未渲染或指定的 key 無效),則返回 null。返回的對象是一個標準的 DOMRectReadOnly 結構。

breakpoint

允許部件確定一個指定的寬度斷點,該斷點與其中一個虛擬節點的當前寬度匹配。此中間件在創建能夠適配各種顯示寬度的部件時非常有用,比如在移動端和桌面分辨率下同時使用的部件。

resize 中間件組合使用,以獲取元素的寬度,並在調整寬度時自動讓部件失效。

注意: 如果沒有設置自定義的寬度斷點,Dojo 將默認使用以下集合:

  • SM: 0
  • MD: 576
  • LG: 768
  • XL: 960

API:

import breakpoint from '@dojo/framework/core/middleware/breakpoint';
interface Breakpoints {
    [index: string]: number;
}
  • breakpoint.get(key: string | number, breakpoints: Breakpoints = defaultBreakpoints)
    • 依據節點的當前寬度,返回與部件中指定的輸出節點(由 key 標識)匹配的斷點。可以通過 breakpoints 參數設置自定義的斷點。返回的值是一個包含 breakpoint 屬性的對象,它標識出了匹配的斷點名稱,以及一個 contentRect 屬性,它包含的值與 resize.get(key) 返回的值相同。

當要在很多位置使用同一個斷點集時,該集合只需定義一次,而不必在每一次調用 breakpoint.get() 時傳入此集合。應用程序可以通過以下方式使用適當的默認值定義自己的自定義斷點中間件:

src/middleware/myCustomBreakpoint.ts

import { createBreakpointMiddleware } from '@dojo/framework/core/middleware/breakpoint';

const myCustomBreakpoint = createBreakpointMiddleware({ Narrow: 0, Wide: 500 });

export default myCustomBreakpoint;

store

當使用 Dojo store 組件時,部件能訪問外部的狀態。

Dojo Store 參考指南中有詳細說明。

API:

import store from '@dojo/framework/core/middleware/store';
  • store.get&lt;U = any&gt;(path: Path&lt;S, U&gt;): U
    • 根據指定的 path 從 store 中獲取值。當關聯的值更改後,組合部件也會失效並重新渲染。
  • store.path(path: any, ...segments: any): StatePaths&lt;S&gt;
    • 返回從指定的根路徑開始,並附加了多個片段之後的 store 路徑。
  • store.at&lt;U = any&gt;(path: Path&lt;S, U[]&gt;, index: number)
    • 當訪問存儲的數組值時,返回數字索引指向的值的 store 路徑
  • store.executor&lt;T extends Process&lt;any, any&gt;&gt;(process: T): ReturnType&lt;T&gt;
    • 在組合部件的 store 中執行給定的 process 並返回結果。

focus

組合使用 VDOM focus 原生方法 ,允許部件檢查和控制輸出的 DOM 間的焦點。

API:

import focus from '@dojo/framework/core/middleware/focus';
  • focus.shouldFocus(): boolean
    • 如果應在當前渲染週期中指定焦點,則返回 true。將只返回一次 true,後續調用將返回 false,直到再次調用 focus.focus()。這個函數通常作爲 focus 屬性值傳給指定的 VDOM 節點,允許部件指出焦點應該應用到哪裏。
  • focus.focus()
    • 能夠被調用,以指示部件或者一個子部件在下一次渲染週期時獲取焦點。這個函數通常傳給輸出的 VDOM 節點的 onfocus 事件處理函數,允許部件響應用戶驅動的焦點變更事件。
  • focus.isFocused(key: string | number): boolean
    • 如果部件中,指定的 key 標識的 VDOM 節點當前獲取焦點,則返回 true。如果相關的 VDOM 節點沒有焦點或者部件中不存在此 VDOM 節點,則返回 false

Focus 委託示例

下面展示一個例子,在部件層次結構內和輸出的 VNode 之間委託和控制焦點:

src/widgets/FocusableWidget.tsx

import { create, tsx } from '@dojo/framework/core/vdom';
import focus from '@dojo/framework/core/middleware/focus';
import icache from '@dojo/framework/core/middleware/icache';

/*
    The input's `onfocus()` event handler is assigned to a method passed in
    from a parent widget, via the child's create().properties<MyPropertiesInterface>
    API, allowing user-driven focus changes to propagate back into the application.
*/
const childFactory = create({ focus }).properties<{ onfocus: () => void }>();

const FocusInputChild = childFactory(function FocusInputChild({ middleware: { focus }, properties }) {
    const { onfocus } = properties();
    return <input onfocus={onfocus} focus={focus.shouldFocus} />;
});

const factory = create({ focus, icache });

export default factory(function FocusableWidget({ middleware: { focus, icache } }) {
    const keyWithFocus = icache.get('key-with-focus') || 0;

    const childCount = 5;
    function focusPreviousChild() {
        let newKeyToFocus = (icache.get('key-with-focus') || 0) - 1;
        if (newKeyToFocus < 0) {
            newKeyToFocus = childCount - 1;
        }
        icache.set('key-with-focus', newKeyToFocus);
        focus.focus();
    }
    function focusNextChild() {
        let newKeyToFocus = (icache.get('key-with-focus') || 0) + 1;
        if (newKeyToFocus >= childCount) {
            newKeyToFocus = 0;
        }
        icache.set('key-with-focus', newKeyToFocus);
        focus.focus();
    }
    function focusChild(key: number) {
        icache.set('key-with-focus', key);
        focus.focus();
    }

    return (
        <div>
            <button onclick={focusPreviousChild}>Previous</button>
            <button onclick={focusNextChild}>Next</button>
            <FocusInputChild
                key="0"
                onfocus={() => focusChild(0)}
                focus={keyWithFocus == 0 ? focus.shouldFocus : undefined}
            />
            <FocusInputChild
                key="1"
                onfocus={() => focusChild(1)}
                focus={keyWithFocus == 1 ? focus.shouldFocus : undefined}
            />
            <FocusInputChild
                key="2"
                onfocus={() => focusChild(2)}
                focus={keyWithFocus == 2 ? focus.shouldFocus : undefined}
            />
            <FocusInputChild
                key="3"
                onfocus={() => focusChild(3)}
                focus={keyWithFocus == 3 ? focus.shouldFocus : undefined}
            />
            <FocusInputChild
                key="4"
                onfocus={() => focusChild(4)}
                focus={keyWithFocus == 4 ? focus.shouldFocus : undefined}
            />
        </div>
    );
});

injector

允許從 Dojo 註冊表中獲取注入器(injector),然後將其分配給失效的回調函數。

注意: 注入器和註冊表是高階概念,在編寫 Dojo 應用程序時通常用不到。它們主要由框架使用,以實現更高級的面向用戶的功能,如 Dojo store

API:

import injector from '@dojo/framework/core/middleware/injector';
  • injector.subscribe(label: RegistryLabel, callback: Function = invalidator)
    • 爲註冊表 label 指定的注入器(如果存在的話)訂閱給定的 callback 失效函數。如果未指定 callback,則默認使用 invalidator 中間件,以便當注入器使其數據可用時,將當前部件標記爲失效並重新渲染。
  • injector.get&lt;T&gt;(label: RegistryLabel): T | null
    • 獲取當前與給定的註冊表 label 關聯的註冊器,如果註冊器不存在則返回 null

block

在構建時,允許部件在 Node.js 中執行稱爲 blocks 的模塊。通常用於構建時渲染。

構建(build)參考指南中有詳細說明。

API:

import block from '@dojo/framework/core/middleware/block';
  • block&lt;T extends (...args: any[]) =&gt; any&gt;(module: T)
    • 執行指定的 block 模塊,並返回執行結果

核心渲染中間件

@dojo/framework/core/vdom 模塊中包含基礎中間件,大多數 Dojo 應用程序都會用到。這些主要用於構建其他自定義中間件(框架提供的附加中間件就是由他們構成的),但在一般的部件開發中也偶爾會用到。

invalidator

這是最重要的中間件,在部件的失效生命週期中設置了一個鉤子。調用了 invaludator() 後,會將要渲染的部件排列到下一次的渲染計劃中。

API:

import invalidator from '@dojo/framework/core/vdom';
  • invalidator()
    • 將使用的部件標記爲無效,需要重新渲染。

node

支持通過節點的 key,訪問部件底層的 DOM 節點。當被請求的 DOM 節點是有效的,但還不可用時,Dojo 就立刻重新渲染部件,直到 DOM 節點變爲可用。

API:

import node from '@dojo/framework/core/vdom';
  • node.get(key: string | number): HTMLElement | null
    • 根據節點的 key 屬性,返回部件中指定的 DOM 元素。如果當前部件中不存在指定的 DOM 元素,則返回 null

diffProperty

通過爲指定的屬性註冊自己的 diff 函數,以允許部件對差異檢測進行細粒度控制。當嘗試重新渲染部件時,框架將調用該函數,以確定是否發生了變化,從而需要進行完全的重新渲染。如果在部件的屬性集中沒有檢測到差異,將跳過更新,並且現有的所有 DOM 節點都保持原樣。

編寫自定義的 diff 函數時,通常需要與 invalidator 中間件組合使用,以便需要更新部件的 DOM 節點時,將當前部件標記爲無效。

注意: 在組合部件或中間件的生命週期中,只能爲指定的屬性註冊一個 diff 函數,後續的調用將被忽略。渲染引擎有一套默認算法,該算法對對象和數組進行 shallow 對比,忽略函數,而對其他所有屬性進行相等檢查。爲屬性設置了自定義的 diff 函數後,將會覆蓋 Dojo 默認的差異檢測策略。

API:

import diffProperty from '@dojo/framework/core/vdom';
  • diffProperty(propertyName: string, diff: (current: any, next: any) =&gt; void)
    • 註冊指定的 diff 函數,該函數用於確定部件的 propertyName 屬性的 currentnext 值之間是否存在差異。

destroy

指定一個在部件銷燬時調用的函數,可以銷燬佔用的任何資源。

注意: 每一個組合的部件或中間件只能調用一次 destroy(),之後再調用會被忽略。對於需要在移除部件時有條件地添加執行句柄的高級方案,應該註冊一個可以跟蹤並迭代地銷燬所有必要資源的銷燬函數。

API:

import destroy from '@dojo/framework/core/vdom';
  • destroy(destroyFunction: () =&gt; void)
    • 設置當前部件銷燬時調用的 destroyFunction。設置的函數將覆蓋之前爲部件設置的任一銷燬函數。

getRegistry

通過處理器接口(handler interface),支持訪問部件自身的 Registry 實例,如果需要的話,也可以訪問應用程序一級的 Registry

注意: Registry 是一個高階概念,在編寫 Dojo 應用程序時通常用不到。它主要在框架內部使用,以實現更高階的面向用戶的功能,如 Dojo store

API:

import getRegistry from '@dojo/framework/core/vdom';
  • getRegistry(): RegistryHandler | null
    • 返回當前部件的 RegistryHandler,如果部件未完全初始化,則返回 null

defer

允許部件暫定和恢復渲染邏輯;在特定條件滿足之前短路部件的渲染時也很有用。

API:

import defer from '@dojo/framework/core/vdom';
  • defer.pause()
    • 暫停當前部件的進一步渲染,直到標記爲恢復。
  • defer.resume()
    • 恢復部件的渲染。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章