<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ù)驅(qū)動的前端架構(gòu)

          共 9201字,需瀏覽 19分鐘

           ·

          2021-05-16 02:26

          作者:徐飛
          https://zhuanlan.zhihu.com/p/370499228

          在廣義的前端領(lǐng)域,模型驅(qū)動視圖已經(jīng)不是什么新鮮話題了,“低代碼”和“搭建”也炙手可熱,而這些概念都是以增強應(yīng)用系統(tǒng)的可配置性為前提的。在這個大前提下,建立元數(shù)據(jù)驅(qū)動的前端架構(gòu)就變得很重要了。

          本次分享的目標是希望從零開始,初步建立一個小小的元數(shù)據(jù)驅(qū)動的原型系統(tǒng)(暫時只包括前端部分),并以此介紹這套系統(tǒng)與業(yè)務(wù)領(lǐng)域的可能結(jié)合方式。

          模型驅(qū)動的視圖

          從最簡單的結(jié)構(gòu)來看,一個模型驅(qū)動的視圖體系包含以下要素:

          • 1. 模型

            •     1. 定義狀態(tài)結(jié)構(gòu)

                2. 定義動作


          • 2. 視圖

            •     1. 訂閱狀態(tài)

                  2. 觸發(fā)動作


          這是很簡單的一種渲染模式,可以適用于所有的場景(暫且忽略性能之類的情況)。

          舉例來說,我們嘗試把狀態(tài)與渲染分離:

          type BooleanProps = {
          value: boolean;
          onChange: (v: boolean) => void;
          };

          // 狀態(tài)的持有者
          const Boolean = (props: PropsWithChildren<BooleanProps>) => {
          const { value, onChange, children } = props;

          const context: DataContextValue = {
          value,
          onChange,
          };

          return (
          <DataContext.Provider value={context}>{children}</DataContext.Provider>
          );
          };

          // 僅渲染和觸發(fā)變更
          const Checkbox = () => {
          const { value, onChange } = useContext(DataContext);

          return (
          <input
          type="checkbox"
          checked={value}
          onChange={(e) => onChange(e.currentTarget.checked)}
          />
          );
          };

          // 兩者的組合
          const Demo = () => {
          const [value, onChange] = useState(false);

          return (
          <Boolean value={value} onChange={onChange}>
          <Checkbox />
          </Boolean>
          );
          };


          在這個例子中,Boolean 組件持有狀態(tài),而下層的 Checkbox 只負責消費這個狀態(tài),或者觸發(fā)上層傳入的修改狀態(tài)的動作。


          進而,可以造出更加泛化的數(shù)據(jù)表達形態(tài):

          type DataProps<T> = {
          value: T,
          onChange: (v: T) => void
          }

          // 狀態(tài)的持有者
          const Data = <T>(props: PropsWithChildren<DataProps<T>>) => {
          const { value, onChange, children } = props

          const context: DataContextValue = {
          value,
          onChange
          }

          return <DataContext.Provider value={context}>{children}</DataContext.Provider>
          }

          const Demo2 = () => {
          const [value1, onChange1] = useState(false)
          const [value2, onChange2] = useState('hello')

          return (
          <>
          <Data value={value1} onChange={onChange1}>
          <Checkbox />
          </Data>
          <Data value={value2} onChange={onChange2}>
          <Input />
          </Data>
          </>
          )
          }

          到這里,我們可以注意到,在同一個數(shù)據(jù)上下文之下,可以擁有若干個共享該數(shù)據(jù)的純渲染組件,也有機會在不影響整體結(jié)構(gòu)的情況下,把 Checkbox 換成與之等價的其他交互,比如 Switch,并不會影響業(yè)務(wù)的表達。甚至我們在 Data 下面添加任意的布局組件,也不會產(chǎn)生額外的改動。

          之前的結(jié)構(gòu)中,我們對于狀態(tài)的操作方式還是非常簡單的,只有讀寫兩種操作,還可以使用 useReducer 進一步拓展,支持添加更多的自定義動作響應(yīng):

          const Demo = () => {
          // reducer 可以是外部注冊的
          const [state, dispatch] = useReducer(reducer, initialCount, init);

          const context: DataContextValue = {
          state,
          dispatch,
          };

          return (
          <DataContext.Provider value={context}>{children}</DataContext.Provider>
          );
          };

          在這個時候,下層渲染組件的能力包括:

          • 1. 消費狀態(tài)

          • 2. 觸發(fā)外層提供的動作來改變狀態(tài)

          更極端一點,這里的各種動作都可以是在外部注冊的,這樣,可以把動作的實現(xiàn)外置,放在某些類似 serverless 的體系中去支撐。

          并且,我們發(fā)現(xiàn),渲染部分仍然是很輕量的,而且可以很容易有跨平臺實現(xiàn)。

          對元數(shù)據(jù)的初步認知

          以上的例子仍然太過簡單了,我們逐步去看一些更加復(fù)雜的,比如表格和表單的狀態(tài)結(jié)構(gòu):

          表格:

          const Table = () => {
          // 表頭信息
          // 行記錄信息
          };

          表單:

          const Form = () => {
          // 字段信息
          // 字段值信息
          };

          如果是按照之前的理念來實現(xiàn),我們當然也可以把這些信息全部糅合到狀態(tài)里,類似這樣:

          const Foo = () => {
          const [state, setState] = useState({
          fields: [],
          records: [],
          });

          return <Table fields={state.fields} state={state.records} />;
          };

          表單也是類似這樣的:

          const Foo = () => {
          const [state, setState] = useState({
          fields: [],
          record: {},
          });

          // 假定我們有一個叫做 Form 的組件,內(nèi)部展開這些字段和數(shù)據(jù)
          return <Form fields={state.fields} state={state.record} />;
          };

          這里的 fields 就是一種沒有經(jīng)過抽象的元數(shù)據(jù),我們可以考慮對這些代碼進行一種初步抽象,把字段信息隔離出去:

          type FieldsProviderProps = {
          fields: Field[];
          };

          const FieldsProvider = (props: PropsWithChildren<FieldsProviderProps>) => {
          const { fields } = props;

          const context: FieldContextValue = {
          fields,
          };

          return (
          <FieldContext.Provider context={context}>{children}</FieldContext.Provider>
          );
          };

          const Demo = () => {
          const fields = []; // 字段定義
          const [state, setState] = useState([]);

          return (
          <FieldsProvider fields={fields} state={state}>
          <Table />
          <FormList />
          </FieldsProvider>
          );
          };

          經(jīng)過這樣的抽象過程,我們把一些獨立于數(shù)據(jù)狀態(tài)的描述信息抽取出去,單獨處理了。最下層的組件仍然職責很單一,只是與之前相比,多了使用一些配置信息的權(quán)利。

          類似這種字段配置,就是一種元數(shù)據(jù)。它實際上是另外一個層面的類型信息,可以攜帶對業(yè)務(wù)模型的定義。

          使用 Schema 描述數(shù)據(jù)結(jié)構(gòu)

          剛才的示例促使我們進行思考:在很多時候,我們需要運行時獲取模型結(jié)構(gòu)定義的詳細信息。如果我們始終擁有這種信息,會導(dǎo)致編程過程變得不一樣嗎?

          比如說,當我們試圖表達一個任務(wù)實體的時候:

          type Task = {
          title: string;
          completed: boolean;
          };

          它可以分解為最原子的數(shù)據(jù)類型的組合,而每種類型又可以使用一個描述數(shù)據(jù)來約束,據(jù)此,我們嘗試描述各種常見數(shù)據(jù)類型的結(jié)構(gòu):

          type BooleanSchema = {
          type: "boolean";
          default?: boolean;
          };

          type StringSchema = {
          type: "string";
          default?: string;
          };

          type NumberSchema = {
          type: "number";
          default: number;
          };

          type ObjectSchema = {
          type: "object";
          properties: Record<string, Schema>;
          default?: Object;
          };

          type ArraySchema = {
          type: "array";
          items: Schema;
          default?: [];
          };

          type Schema =
          | BooleanSchema
          | NumberSchema
          | StringSchema
          | ObjectSchema
          | ArraySchema;

          上面的這些類型定義很簡陋,但是可以初步描述數(shù)據(jù)的基本形態(tài)。在此之上,可以更進一步,直接把業(yè)務(wù)的領(lǐng)域模型表達出來,比如,把前面示例中的 Task,可以換成這樣的方式來描述:

          const taskSchema = {
          type: "object",
          properties: {
          title: {
          type: "string",
          },
          completed: {
          type: "boolean",
          },
          },
          };

          這樣,我們可以重構(gòu)剛才的代碼結(jié)構(gòu),變成下面這種形狀:

          const Demo = () => {
          return (
          <SchemaProvider schema={schema}>
          <Table />
          <FormList />
          </SchemaProvider>
          );
          };

          在 SchemaProvider 中,我們可以從定義中取出當前類型的初始值,甚至可以自動生成一個校驗函數(shù),以驗證給定數(shù)據(jù)是否符合自身描述的規(guī)則。

          從 Schema 到 TypeScript 類型

          至此,我們已經(jīng)可以給一個承載狀態(tài)的組件添加相應(yīng)的 schema,但是,需要注意到,它對 TypeScript 的支持很不友好,schema 跟 value 沒有建立比較好的關(guān)聯(lián)。

          設(shè)想有如下代碼:

          <Data schema={taskSchema} value={{}} />

          在這個地方,當我們填寫了 schema,然后為 value 傳入數(shù)據(jù)的時候,它們并未產(chǎn)生關(guān)聯(lián),簡單來說,在 DataProps 定義的時候,如果不建立 schema 與 value 之間的關(guān)聯(lián),至少需要兩個泛型參數(shù):

          type DataProps<T1 extends Schema, T2> = {
          schema: T1;
          value: T2;
          };

          在 T1 和 T2 之間,很明顯 T1 的結(jié)構(gòu)更可靠,那么,我們就考慮把類型定義變成下面這樣,讓 value 變成 schema 的一種類型運算:

          type DataProps<T extends Schema> = {
          schema: T;
          value: ValueOf<T>;
          };

          這樣,我們就得實現(xiàn) ValueOf 這么一個類型操作了,不難得出類似以下的代碼:

          type ValueOfBoolean<T extends BooleanSchema> = boolean;
          type ValueOfNumber<T extends NumberSchema> = number;
          type ValueOfString<T extends StringSchema> = string;
          type ValueOfObject<T extends ObjectSchema> = {
          [K in keyof T["properties"]]: ValueOf<T["properties"][K]>;
          };
          type ValueOfArray<T extends ArraySchema> = Array<ValueOf<T["items"]>>;

          type ValueOf<T extends Schema> = T extends BooleanSchema
          ? ValueOfBoolean<T>
          : T extends NumberSchema
          ? ValueOfNumber<T>
          : T extends StringSchema
          ? ValueOfString<T>
          : T extends ObjectSchema
          ? ValueOfObject<T>
          : T extends ArraySchema
          ? ValueOfArray<T>
          : unknown;

          這時候,再看看剛才的數(shù)據(jù)類型:

          const Demo = () => {
          return (
          <Data
          schema={{
          type: "object",
          properties: {
          title: {
          type: "string",
          },
          completed: {
          type: "boolean",
          },
          },
          }}
          value={{ title: "" }}
          />
          );
          };

          就能夠?qū)崟r校驗出 value 結(jié)構(gòu)的錯誤了。

          語義化的數(shù)據(jù)展開

          建立了完整的 schema 結(jié)構(gòu)之后,我們再回頭去看表格和表單,就會發(fā)現(xiàn)比較簡單了。

          我們會發(fā)現(xiàn),它們其實是兩種迭代模式,一種是對象迭代為字段,一種是列表迭代為列表項。如果在迭代過程中擁有字段這類信息,那么,整個迭代過程都是可以抽象的。

          比如這里是簡單的字段迭代的過程:

          type ObjectIteratorProps<T extends ObjectSchema> = {
          schema: T;
          value: ValueOf<T>;
          onChange: (v: ValueOf<T>) => void;
          };

          const ObjectIterator = <T extends ObjectSchema>(
          props: PropsWithChildren<ObjectIteratorProps<T>>
          ) => {
          const { schema, value, onChange, children } = props;

          return Object.keys(schema.properties).map((key) => {
          const fieldSchema = schema.properties[key];
          const fieldValue = value[key];
          const fieldOnChange = (v) => {
          onChange({
          ...value,
          key: v,
          });
          };

          return (
          <Field key={key} value={fieldValue} onChange={fieldOnChange}>
          {children}
          </Field>
          );
          });
          };

          在使用的時候,可以:

          const Demo = () => {
          const [value, onChange] = useState < ValueOf<taskSchema>();
          return (
          <ObjectIterator
          schema={taskSchema}
          value={value}
          onChange={onChange}
          ></ObjectIterator>
          );
          };

          類似,ListIterator 也可以很容易表達出來。這樣,我們之前碰到的表格表單,或者類似的形態(tài),就有了比較統(tǒng)一的抽象方式了。

          更夸張一些,我們還可以對常見的數(shù)據(jù)結(jié)構(gòu)都實現(xiàn)一遍這樣的組件,而且內(nèi)部可以做很多優(yōu)化,比如虛擬滾動之類的,這樣,就減輕了渲染組件的負擔。

          基于類型的等價交互

          在業(yè)務(wù)中,我們常常看到若干種交互形態(tài),其內(nèi)在的數(shù)據(jù)結(jié)構(gòu)完全一致。在之前的示例中,已經(jīng)簡單看到一些了。

          在軟件架構(gòu)中,一個很重要的過程是在抽象的基礎(chǔ)上合并同類項。回到剛才的場景,我們會發(fā)現(xiàn),對字段的描述,實際上是很通用的,這部分信息很大程度上并非來自前端,而是業(yè)務(wù)建模的一個體現(xiàn)。

          這就是說,只要存在能夠表達這種業(yè)務(wù)模型的最低交互,它在業(yè)務(wù)上就是可用的,只是不一定友好。然后,在不修改其他代碼的情況下,替換為表達能力等價,但是交互更友好的渲染器,就可以提升這部分的體驗。

          舉例來說,假設(shè)我們有一個下象棋的游戲,已知規(guī)則,但是暫時還沒時間寫棋盤和棋子,能不能在表單和表格里面下棋呢?

          下面展示一個 demo,一個可以在表單中下的象棋游戲,篇幅所限,暫不放出代碼,在現(xiàn)場有過演示。

          從這里我們就可以認識到,棋盤和表單,盡管形態(tài)差異非常大,實際上是等價的。推而廣之,我們甚至可以用表單表達一切業(yè)務(wù)。

          小結(jié)

          理想狀態(tài)下,應(yīng)用架構(gòu)可以劃分以下兩個部分:

          • 1. 業(yè)務(wù):領(lǐng)域模型

          • 2. 基礎(chǔ)設(shè)施:框架與服務(wù)

          在這種狀態(tài)下,我們期望:

          業(yè)務(wù)專家盡可能不需要去關(guān)注具體實現(xiàn),而通過某種方式描述和表達業(yè)務(wù)細節(jié),這就是業(yè)務(wù)建模。

          比如說,當我們做業(yè)務(wù)建模的時候,并不需要去額外關(guān)心:

          • 1. 使用什么數(shù)據(jù)庫存儲數(shù)據(jù)

          • 2. 使用什么服務(wù)端開發(fā)框架

          • 3. 使用什么 Web 或者客戶端開發(fā)框架

          而是側(cè)重于描述:

          • 1. 當前是什么業(yè)務(wù)?

          • 2. 有哪些領(lǐng)域模型?

          • 3. 關(guān)聯(lián)關(guān)系如何?

          • 4. 支持什么操作?

          • 5. 有什么校驗邏輯?

          • 6. 權(quán)限如何分配?

          然后,盡可能把技術(shù)設(shè)施變成一個底層實現(xiàn)多樣化的業(yè)務(wù)解釋引擎,再去具體組合業(yè)務(wù)。

          在以上的探討中,我們已經(jīng)努力去做了以下事項:

          • 1. 建立了簡單的領(lǐng)域模型解釋層

          • 2. 建立了可替換的等價交互體系

          • 3. 實現(xiàn)了常見數(shù)據(jù)結(jié)構(gòu)的展開機制

          • 4. 把包含“邏輯”的部分盡可能隔離出去

          在此基礎(chǔ)上,前端部分成為了對領(lǐng)域模型的解釋引擎,視圖的組合與布局都不再影響業(yè)務(wù)正確性。沿著這個角度思考,我們可以看到更多的可能性,比如:

          <DataSource schema={model}>
          <Query />
          <Table />
          </DataSource>

          更語義化地表達:數(shù)據(jù)源、查詢、請求、異常 等概念,并且定義它們的組合方式。

          而更大的體系,則是前后端一體化,整個都是業(yè)務(wù)領(lǐng)域的解釋引擎,元數(shù)據(jù)從存儲、到傳輸、再到呈現(xiàn),一直伴隨整個應(yīng)用的生命周期。

          這個時候,我們發(fā)現(xiàn),一個完整的“配置化”的業(yè)務(wù)軟件系統(tǒng),就擁有了完整的表達鏈路了。

          注:本文主要是為了說明基于元數(shù)據(jù)思考的方式,本身的實現(xiàn)很簡陋,也并不代表需要這樣完全從底層建立應(yīng)用架構(gòu),在一些環(huán)節(jié),社區(qū)早已存在很多相關(guān)庫可以使用了。

          愛心三連擊

          1.看到這里了就點個在看支持下吧,你的在看是我創(chuàng)作的動力。

          2.關(guān)注公眾號腦洞前端,獲取更多前端硬核文章!加個星標,不錯過每一條成長的機會。

          3.如果你覺得本文的內(nèi)容對你有幫助,就幫我轉(zhuǎn)發(fā)一下吧。

          瀏覽 71
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  日产欧美在线 | 美女被操视频91 | 亚洲小说区图片区都市 | 肏逼福利影城 | 波多野结衣免费不卡视频 |