動態

詳情 返回 返回

聊聊中後台前端應用:上下文的那些事兒 - 動態 詳情

經過《聊聊中後台前端應用:模塊相關的一些事》和《聊聊中後台前端應用:業務中的組件體系》這兩篇文章的鋪墊,終於可以單獨寫一篇文章來專門講講「上下文」相關的事情了——

概念明晰

在進入正題之前,先試圖釐清與主題關係密切的幾個概念:狀態、狀態管理和上下文。

狀態

有時會聽到兩撥人在打嘴仗——有一撥人説:「前端都是狀態,沒有數據,是狀態驅動視圖而不是數據驅動視圖」。另一撥人反駁説:「狀態難道不是數據嗎?不是數據是啥?」——這兩撥人的説法都沒有錯,只不過是站在了不同的角度。

一般來説,「數據」是指存放在數據庫、文件系統中的持久數據,是「靜態」的、「持久」的,常被當作「數據源」的略稱來用;「狀態」則是保持在內存當中的瞬時數據,是「動態」的、「臨時」的,來源於通過 HTTP 請求或本地存儲讀取的數據以及終端用户在界面上的操作——然而它們都是數據。

也就是説,可以認為在經典的三層架構中數據層往上的分層中的數據都是「狀態」:

「表現-領域-數據」分層架構

對於前端來説,數據層通信的對象就是服務端和本地存儲。

狀態管理

「狀態管理」是什麼?顧名思義,就是對「狀態」的「管理」。雖然在前端圈兒內隨着 Redux 等的流行讓「狀態管理」成了熱詞,但它並不是什麼新鮮貨。

世上的任何事物都需要被管理,只不過當它還沒那麼複雜的時候,不需要作為一門學問或者説一套方法論拿出來供人們單獨討論。

由於前後端分離和單頁面應用的出現,使得前端的狀態複雜化,如何去有效地進行管理成為了問題,因此形成了現如今很多人去關注並討論「狀態管理」的局面。

我瞭解到的前端項目中,它們的狀態管理方案可以説是「兩極分化」——要麼分散在各個 UI 組件中,要麼集中到一個所謂的「全局 store」中——這兩種我都不太認可。

上下文

在《聊聊中後台前端應用:模塊相關的一些事》和《聊聊中後台前端應用:業務中的組件體系》這兩篇文章中都對「上下文」有所描述,簡單來説,它對於程序的作用就相當於幫助人去理解事物並做出相應反應的「語境」,畢竟它們的英文都是「context」。

在實際應用時,「上下文」很可能是一個帶有很多屬性及對其進行讀、寫操作的方法的對象。那些已經被暴露出來或沒有被暴露出來的變量,就是上文所説的「狀態」,而暴露出來的方法或函數就是對「狀態」進行管理用的——它們共同構成了「上下文」。

一個上下文可以用類的方式去實現:

class ValueContext {
  private value;

  constructor({ initialValue }) {
    this.value = initialValue;
  }

  public getValue() {
    return this.value;
  }

  public setValue(value) {
    return this.value = value;
  }
}

const context = new ValueContext({ initialValue: 'Hello, Ourai!' });

也可以用函數的方式:

function createValueContext({ initialValue }) {
  let value = initialValue;

  return {
    getValue: () => value,
    setValue: newValue => (value = newValue),
  };
}

const context = createValueContext({ initialValue: 'Hello, Ourai!' });

無論用哪種方式實現,無論實現的具體邏輯是什麼,對於上下文的消費者來説它就是一個 API——「上下文」是串起各部分邏輯,具有一定程度泛化的業務語義的接口。

組件與狀態

交互與狀態如影隨形,除了那些純展示用的 UI 組件,一般來説 UI 組件都會有與其關聯的狀態,只是維護的地方不同。

根據 UI 組件自身內部是否維護了狀態,可分為「無狀態組件」與「有狀態組件」。

那些純展示用的 UI 組件毋庸置疑都是「無狀態組件」,而有交互的 UI 組件若是將狀態維護外置,那就是「無狀態組件」,否則是「有狀態組件」——

<template>
  <input :value="value" @input="handleInput" />
</template>

<script lang="ts">
import { Vue, Component } from 'vue-property-decorator';

@Component
export default class StatefulInput extends Vue {
  private value: string = '';

  private handleInput(evt): void {
    this.value = evt.target.value;
  }
}
</script>

同樣是自定義的輸入框組件,上面的示例是有狀態組件,而下面的則是無狀態組件:

<template>
  <input :value="value" @input="handleInput" />
</template>

<script lang="ts">
import { Vue, Component, Prop } from 'vue-property-decorator';

@Component
export default class StatelessInput extends Vue {
  @Prop({ type: String, default: '' })
  private readonly value!: string;

  private handleInput(evt): void {
    this.$emit('input', evt.target.value);
  }
}
</script>

相較之下,有狀態組件在保證功能正常的情況下可以暴露更少的屬性和事件,但可複用性就降低了,無狀態組件正好相反。

控件通常會被設計為無狀態組件,尤其是交互簡單的和純展示的;為了保證基本功能可用,交互複雜的控件可能會被設計為有狀態組件。

在封裝部件時,大多數人的思路和封裝控件一樣,就會——

在相對大粒度的部件中,如果主要依賴屬性和事件進行通信的話,它們的數量很容易變得失控,並且內部的結構和邏輯也會被改得面目全非,維護起來十分困難和難受——這就變成了一坨翔💩!

歐雷《聊聊中後台前端應用:業務中的組件體系》

按照封裝控件的思路去封裝部件,很容易會讓屬性和事件的數量變得失控,或者內部各種邏輯膨脹,使部件變得十分臃腫——無論是哪種,都會加大維護成本。

私以為,部件內部儘量不要有交互邏輯和業務邏輯,也儘可能不去維護任何狀態——業務的狀態與邏輯上升至上下文中,交互邏輯下沉到控件中,展現狀態由業務狀態計算出來——部件中理論上只有業務與交互/展現間的轉換邏輯。

理想狀況下,部件中的各種依賴也是通過上下文獲取的,而不是自己從哪個位置 import 進來的。

説白了,在前端應用這個「有機體」中,控件和部件是具體產生功能的「組織」和「器官」,而上下文則是為它們傳遞信息、輸送養分的「神經」和「血管」。

上下文概要

構成上下文的基本元素除了上文説過的要維護的狀態及對其進行讀、寫操作的方法/函數之外,大多還會有讓上下文內外數據保持一致的基本是基於觀察者模式實現的同步機制。

在本系列文章所闡述的體系中,上下文大概分為三類:應用上下文、模塊上下文和數據上下文。

「應用上下文」是作用於整個應用的上下文,如果運行時只有一個應用,那麼可以把它視為是「全局」的上下文;倘若運行時中存在多個應用,那就是每個應用對應一個應用上下文。

應用上下文中維護的是單一應用範圍內共享的狀態,如路由、主題、國際化等配置信息,和用户的基本信息與權限等。

「模塊上下文」主要用來維護以「模塊」為中心的狀態,像指定模塊所依賴的其他模塊的資源和它提供給其他模塊的可用資源這類依賴信息,以及該模塊的模型、視圖、服務端動作(通過 HTTP 請求與服務端通信的函數)等元數據。

「數據上下文」則是前端應用中使數據流動起來的主力,稍後展開説。

數據上下文

在繼續往下説「數據上下文」之前,首先要理解在《聊聊中後台前端應用:業務中的組件體系》中提到的「從數據的視角看前端」。

「數據上下文」又細分為「視圖上下文」和「搜索上下文」,根據整個體系的複雜程度,它們可分別再往下劃分出「字段上下文」和「過濾器上下文」。實際上,可以認為「搜索上下文」是為了收集列表數據過濾條件而特化了的「(對象)視圖上下文」。

「值」的抽象

各種「數據上下文」的共同特點是對「值」的操作,因此可以圍繞着「值」進行一些抽象——

根據用途,有三種「值」的狀態——一直處於活動狀態的「當前值」,用 value 來表示;在初始化與重置時用來賦值的「初始值」和「默認值」,分別用 initialValuedefaultValue 來表示。

在不同的具體數據上下文中「當前值」的含義會有所差別。比如,在對象視圖上下文中它是指隨着用户操作而變化的字段的鍵值集合,而在列表視圖上下文中則是已選中的記錄。

在實際應用中,「初始值」與「默認值」的主要區別在於優先級不同,「初始值」大於「默認值」,即在沒有「初始值」時才會用「默認值」。

與「值」相關的操作基本只有 4 個,分別是對「當前值」進行讀與寫的 getValue()setValue(),將「當前值」向外/上傳遞的 submit() 以及恢復「當前值」到「初始值」或「默認值」的 reset()

相應地,有 4 個「事件」供外界在不同時機進行數據同步用——代表數據已經準備好了的 ready 事件;「當前值」的每次變更都會觸發 change 事件;調用 submit()reset() 時會觸發對應的 submitreset 事件。

關於 ready 事件,有一個使用場景是:網頁加載完成後,列表數據要等過濾條件收集好了再發請求獲取,但過濾條件又得先從 URL 的查詢參數中恢復——這就需要列表視圖上下文去監聽搜索上下文的 ready 事件,進而去發請求獲取數據。

「值」的校驗

為了保障數據的安全及純淨,在處理數據時先進行合法性或者説有效性校驗是基本操作,因此在調用 setValue() 時其內部會先校驗一波。

對「值」的校驗實際上就是按照優先級執行一下各個約束條件。這裏隱含了一個信息——約束是可以顯式定義並且可擴展的。

「值」的約束可分為來自數據類型和數據結構的自然性約束,以及源於模型關係與業務規則等的非自然性約束。這裏的「自然」與否是單純從數據的特性層面來説。

總結

提起「應用」這個詞,很多人的第一反應是:「這個東西好重、好龐大啊!」它在他們腦中的形象就像壓在孫悟空身上的五指山一樣的巨石。

而一個更好的視角是,把「應用」看作是「接口」與「實現」的組合。更準確地説,可能是「流水線」與「物料」——將不易變的、關係與規則相對固定的東西泛化並接口化,它們之間相互連接形成「流水線」;把易變的部分作為在「流水線」上流轉的「物料」存在。

就像在《聊聊中後台前端應用:業務中的組件體系》的最後,我説——

理想情況下,最終會發現——除了業務邏輯,好像其餘部分幾乎都是接口(interface)——具體實現可以任意移除,隨意替換!

歐雷《聊聊中後台前端應用:業務中的組件體系》

如果不作進一步説明,上面的描述也許會有些讓人摸不着頭腦——

在一箇中後台前端應用中,最易變的是業務邏輯和 UI 設計,最不易變的是「值」的自然性約束、「視圖」與「字段」間的內在關係、控件的屬性和事件等。

構建一個體系,將易變的部分弄成作為「物料」存在的元數據或配置,互相連通的上下文就成為了「流水線」。


本文其他閲讀地址:個人網站|微信公眾號

user avatar ichu 頭像 jiaolvdekaixinguo 頭像 barrior 頭像 solvep 頭像 elegantdevil 頭像 axiaoxin_blog 頭像 chumendeshijie_68fa2aee8a3d5 頭像 fengkuangdejinju 頭像
點贊 8 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.