天天看點

單頁應用程式_單頁應用程式的類型驅動開發 為您的州設計一個好的類型的五個規則 (Five rules for designing a good type for your state) 結論 (Conclusion)

單頁應用程式

Type-driven-development is a programming style where you first define types and extract functions from those types. This style is closely connected to functional programming as they share the concept of functions being transformation from data of type

A

to data of type

B

and objects just storing data and not logic.

類型驅動開發是一種程式設計風格,您首先定義類型并從這些類型中提取函數。 這種風格與函數式程式設計緊密相關,因為它們共享函數的概念,即從

A

類型

A

資料轉換為

B

類型

A

資料,以及僅存儲資料而非邏輯的對象。

When working with modern libraries like React and Vue we have a state (of a certain type) that gets transformed into a DOM and receives updates when the user interacts with the generated DOM. Here the type of the application’s state is absolutely central to the inner workings of the application. Everything that happens is either a result of what the current value of the state or is a modification for this state.

當使用像React和Vue這樣的現代庫時,我們有一個狀态(某種類型),該狀态被轉換為DOM并在使用者與生成的DOM互動時接收更新。 在這裡,應用程式狀态的類型絕對是應用程式内部工作的核心。 發生的一切都是該狀态的目前值的結果,或者是對該狀态的修改。

When keeping this in mind it is logical that we design the architecture of our applications with a strong focus on this state, as everything else follows from it anyway. This leads us to the type-driven-development paradigm. Only how do we design a good type for our state? There is a lot written about this, but there is not a obviously single approach that is the way of doing it. Like with almost all architectural questions, the answers is heavily dependent on the context of the question. In this article I will present you five rules I think a good state for a single-page-application should adhere to.

牢記這一點是合乎邏輯的,我們在設計應用程式體系結構時要特别關注此狀态,因為其他所有情況都會随之而來。 這導緻我們進入類型驅動開發範式。 隻有我們如何為狀态設計一個好的類型? 有寫關于這個有很多,但沒有一個明顯的單一的辦法,是做它的方式。 與幾乎所有架構問題一樣,答案在很大程度上取決于問題的背景。 在本文中,我将向您介紹五個規則,我認為對于單頁應用程式應遵循一個良好的狀态。

為您的州設計一個好的類型的五個規則 (Five rules for designing a good type for your state)

1.每個執行個體都是有效執行個體(1. Every instance is a valid instance)

As its essence, a type definition is a description of which values are a valid instance of the type. Saying that a your state has to of a type

T

means that you put a restriction on which values are allowed to be used as a state. This concept should be used to restrict all possible instances of the state to ones you as a programmer expect. And on its turn the compiler is going to do it level best to verify that you actual did account for all possibilities and throw type errors if you didn’t.

本質上,類型定義是對哪些值是該類型的有效執行個體的描述。 說您的狀态必須為

T

類型,意味着您對允許将值用作狀态的條件施加了限制。 應該使用此概念将狀态的所有可能執行個體限制為程式員所期望的狀态執行個體。 反過來,編譯器将使其達到最佳狀态,以驗證您是否确實考慮了所有可能性,如果未考慮則抛出類型錯誤。

The type definition of the state should make it impossible to create types that cause crashes, are ambiguous to their meaning or are invalid in any other way.

狀态的類型定義應使其不可能建立導緻崩潰,含義不明确或以任何其他方式無效的類型。

The biggest cause of invalid states is data duplication. If data is saved multiple times in a state it is very easy for those to get out of sync and create states that you didn’t account for in your application. Take for example the state below for an overview of blog posts with an example instance:

無效狀态的最大原因是資料重複。 如果資料以某個狀态多次儲存,那麼很容易使這些資料不同步并建立您在應用程式中未考慮的狀态。 以下面的狀态為例,其中包含示例執行個體的部落格文章概述:

interface OverviewState {
  items: Blog[]
  loading: boolean
  error?: Error
}


const ExampleState: OverviewState = {
  items: [
    {title: 'Lorem ipsum'}, 
    {title: 'Lorem ipsum'}
  ],
  loading: true,
  error: new Error('failed to parse data')
}
           

What is your application going to do when it has to render

ExampleState

? Show the results in

items

? Or is it going to show a loader because

loading

is

true

? Or show an error message because we have an

Error

in our nullable

error

field? This isn’t a good type because it is not clear in what state our application actually is. We have some bits of data stored in there, but it is ambiguous to what has happened and what needs to happen. A way to fix this type would be to use an union type:

您的應用程式必須呈現

ExampleState

時将做什麼? 在

items

顯示結果? 還是因為

loading

true

來顯示加載程式? 或顯示錯誤消息,因為我們有一個

Error

在我們為空的

error

領域? 這不是一個好的類型,因為尚不清楚我們的應用程式實際處于什麼狀态。 我們在其中存儲了一些資料,但是它與發生的事情和需要發生的事情模棱兩可。 修複此類型的一種方法是使用聯合類型:

interface OverviewState {
  items:  
    | "loading" 
    | Blog[]
    | Error
}
           

In this state it is always clear in what state we are and every state makes sense and is to be expected. Either we are loading, we are done loading and have data or we have an error. There is no more ambiguous data and data duplication that can lead to invalid states. And the beautiful thing is that TypeScript wont let us access the items unless we have checked that we aren't in a loading or error state. In this way types help us to account for every possible state.

在這種狀态下,總是很清楚我們處于什麼狀态,每個狀态都有意義并且可以預期。 我們正在加載,已經完成加載并且有資料,或者有錯誤。 沒有更多的模棱兩可的資料和重複資料會導緻無效狀态。 美麗的是,除非我們檢查自己是否未處于加載或錯誤狀态,否則TypeScript不會允許我們通路這些項目。 通過這種方式,類型可以幫助我們解釋每個可能的狀态。

2.每個動作都有React (2. Every action has a reaction)

also applies outside Newton's apple tree. Everything that happens in your application should lead to a valid state. For example, data parsing isn’t something that your designer thought about when designing the application. Meanwhile parsing is an action that can lead to a parsing error that has to be dealt with. The type-driven way of doing this is adding an error state to the type for the state. And because this is now a possible valid instance of the state we will be forced to deal with it in the render layer by the type checker.

也适用于牛頓的蘋果樹以外。 應用程式中發生的所有事情都應導緻一個有效狀态。 例如,資料解析不是設計人員在設計應用程式時所考慮的。 同時,解析是一種可能導緻必須解決的解析錯誤的操作。 這種類型驅動的方式是将錯誤狀态添加到該狀态的類型。 而且由于這現在是狀态的可能有效執行個體,是以類型檢查器将迫使我們在渲染層中處理該狀态。

The previous rule stated that there shouldn’t be too many possible instances of your state, this one state that there shouldn’t be to few.

上一條規則規定,您的狀态不應有太多可能的執行個體,而該狀态不應有太多的執行個體。

If we again look to a state for an overview page then one could (very naively) wonder why we need more than just an array of blogs. After all that is everything the interface of the application shows.

如果我們再次将狀态放在概述頁面的狀态,則可能(非常幼稚)想知道為什麼我們需要的不僅僅是一系列部落格。 畢竟,這是應用程式界面顯示的所有内容。

interface OverviewState {
  items?: Blog[]
}
           

And you could properly work with this. Show a loader as long as the array is not there, render the teasers when it is populated and if the loading has resulted in an empty array there probably was an error. But is this a descriptive state? Can a new developer on your team tell the empty array that there was an error? Can you still understand this in 2 years? The answer is most likely ‘no’. This is because the events of starting the loading and an error happening don’t have any clear state deriving from them. Instead they abuse magical instances of the array to be able to create a state that the type checker will accept. Using an union as shown before does create a clear state for ‘loading’, ‘loaded’ (both with and without items) and ‘error’.

這樣您就可以正确地工作了。 隻要不存在數組,就顯示一個加載器,在填充數組時渲染提示,如果加載導緻一個空數組,則可能是錯誤。 但這是描述性的狀态嗎? 團隊中的新開發人員可以告訴空數組有錯誤嗎? 您兩年後還能了解嗎? 答案很可能是“否”。 這是因為開始加載的事件和發生錯誤的事件并沒有從中得出任何清晰的狀态。 相反,它們濫用數組的魔術執行個體以能夠建立類型檢查器将接受的狀态。 如前所示,使用聯合會為“正在加載”,“已加載”(包含和不包含項目)和“錯誤”建立明确的狀态。

3.渲染時不解析 (3. No parsing while rendering)

The data in the state should be stored in the format the application uses. An extreme example to illustrate this: let's say we have a date field that gets loaded from an API. In the JSON response of the API the date is represent as a string. Somewhere in you application you are going to parse this to a

Date

object so you can use the date formatting in the render function. If you save your string in the state and create the

Date

in the render function itself then it becomes possible to store the string

‘foo’

in the date field and have it count as a valid instance (after all,

‘foo’

is a valid string). This clearly violates the first rule we talked about as we allow an invalid instance of our state to exists.

狀态下的資料應以應用程式使用的格式存儲。 一個極端的例子來說明這一點:假設我們有一個從API加載的日期字段。 在API的JSON響應中,日期表示為字元串。 您将在應用程式中的某個位置将其解析為

Date

對象,以便可以在render函數中使用日期格式。 如果将字元串儲存為狀态并在render函數本身中建立

Date

,則可以在日期字段中存儲字元串

'foo'

并将其計為有效執行個體(畢竟

'foo'

是有效的串)。 這顯然違反了我們談論的第一個規則,因為我們允許存在一個無效的狀态執行個體。

To extends on this: if you use moment.js in your application for formatting dates than it would make sense to use the

Moment

type in your state instead of

Date

and parse them around on every render. The same applies to using libraries like immutable.js or JavaScripts’ new

Map

and

Set

types.

對此進行擴充:如果您在應用程式中使用moment.js設定日期格式,則在狀态中使用

Moment

類型而不是

Date

并在每個渲染中對其進行解析是有意義的。 這同樣适用于使用像immutable.js或JavaScripts的新

Map

Set

類型之類的庫。

The types the state uses to store data should be the ones your application is going to use and be a proper data structure for the data they represent.

狀态用于存儲資料的類型應該是您的應用程式将要使用的類型,并且應該是它們表示的資料的适當資料結構。

This also means that the state of your application shouldn’t be linked to whatever models your backend uses. Quite often backend data will contain more info than needed or have unneeded levels of nesting. Especially API’s from ERP’s and CMSes can give you some bizarre data structures as their internal data model are highly influenced by their need to be configurable. Below you find an example of how Drupal represents date fields in its API. Of course you wouldn’t want this in your state, but either a

Date

object or an

Option<Date>

for optional dates. Validating that the array isn’t empty and that

value

contains a valid data string should happen just once: in the parsing layer with the API call.

這也意味着您的應用程式狀态不應連結到後端使用的任何模型。 後端資料經常會包含比所需更多的資訊,或者具有不必要的嵌套級别。 特别是來自ERP和CMS的API可以為您提供一些奇怪的資料結構,因為它們的内部資料模型受到可配置性需求的極大影響。 在下面,您可以找到有關Drupal如何在其API中表示日期字段的示例。 當然,您不希望它處于您的狀态,但是可以使用

Date

對象或

Option<Date>

作為可選日期。 驗證數組不為空并且該

value

包含有效資料字元串應該隻發生一次:在具有API調用的解析層中。

const datefieldDrupal = [
  {
    value: "2020-07-10T08:01:40+00:00", 
    format: "Y-m-d\TH:i:sP"
  }
]
           

4.贊成工會(4. Favor the union)

Unions allow us to create types that list different possible scenarios of which only one can exist at a time. This is very useful when modelling a process with multiple possible outcomes. We already have partially seen this in a previous example. Union types are a real beast when used correctly in type-driven-development. For example, an API call results in either a success with a value, a not-found error, an unauthorized error or a generic server error:

聯合允許我們建立列出不同可能場景的類型,這些場景一次隻能存在一個。 在對具有多個可能結果的流程進行模組化時,這非常有用。 在前面的示例中,我們已經部分看到了這一點。 在類型驅動開發中正确使用聯合類型時,它是真正的野獸。 例如,API調用會導緻帶有值的成功,未找到的錯誤,未經授權的錯誤或通用伺服器錯誤:

type ApiResult<a> = 
  | { kind: 'success', value: a }
  | { kind: 'not-found' }
  | { kind: 'unauthorized' }
  | { kind: 'error', error?: Error }
           

Using a reusable container type like this in your project will help to create better states. To start off it isn’t anymore possible to forget a bit of error handling on one API call somewhere in your application. This should reduce the amount of stupid little bugs around the unhappy paths. Another benefit is that it is directly clear to everybody that

ApiResult<Blog[]>

stores data that gets loaded via an API.

在項目中使用像這樣的可重用容器類型将有助于建立更好的狀态。 首先,不可能再在應用程式中某個地方的某個API調用上忘記一些錯誤處理了。 這樣可以減少不愉快的道路周圍愚蠢的小蟲子的數量。 另一個好處是,每個人都可以清楚地知道

ApiResult<Blog[]>

存儲通過API加載的資料。

Union types are great in situations where an object type would allow invalid instances.

在對象類型允許無效執行個體的情況下,聯合類型非常有用。

Union types can also be used to create some very generic container types that help greatly when modeling data. The first one is the

Result

type, an abstraction on something that is either an

Error

or an

Success

. This type has an entire paradigm dedicated to it, called railroad oriented programming.

聯合類型還可以用于建立一些非常通用的容器類型,這些容器類型在對資料模組化時有很大幫助。 第一個是

Result

類型,它是對

Error

Success

的抽象。 這種類型專用于它的整個範例,稱為鐵路程式設計。

type Result<a, e> =
  | { kind: 'success', value: a }
  | { kind: 'error', error: e }
  
const success = <a,e>(a:a): Result<a, e> => ({ kind: 'success', value: a })
const error = <a, e>(e:e): Result<a, e> => ({ kind: 'error', error: e })
           

One of the great things about modeling something as a

Result

of two types is that it is directly clear that we are looking at the result of something that tried to make an

a

but could have failed with the error that is described by the

e

.

關于将某物模組化為兩種類型的

Result

的一個偉大的事情是,很明顯,我們正在檢視試圖生成

a

但可能由于

e

所描述的錯誤而失敗的事物的結果。

Another type in this category is the

Option

type. An

Option

models the fact that the data that it contains could be absent. This is comparable to making a type nullable, only options have similar helper functions as results.

此類别中的另一種類型是“

Option

類型。 一個

Option

對以下事實進行模組化:它所包含的資料可能不存在。 這相當于使類型為可為空,隻有選項具有與結果類似的輔助函數。

type Option<a> =
  | { kind: 'some', value: a }
  | { kind: 'none' }
  
const some = <a>(a:a): Option<a> => ({ kind: 'some', value: a })
const none = <a>(): Option<a> => ({ kind: 'none' })
           

While many more exist, with just these generic containers at our disposal we can build more resilient software that is easier to reason about.

盡管還有更多的容器存在,但隻有這些通用容器可供我們使用,我們才能建構更具彈性的軟體,進而更易于推理。

5.提取事實應該很容易 (5. Extracting facts should be easy)

Last but not least, we should remember to keep our code (as) simple (as possible). Luckily great type definitions allow the code around it to be simple. As an example, let’s look at this state for a login form:

最後但并非最不重要的一點,我們應該記住保持代碼(盡可能)簡單。 幸運的是,出色的類型定義使圍繞它的代碼很簡單。 例如,讓我們看一下登入表單的這種狀态:

interface FieldWithError<a> {
  value: a
  error: Option<string>
}


interface LoginFormState {
  username: FieldWithError<string>
  password: FieldWithError<string>
}
           

In our login form we have a field for the password and for the username. Both fields can have a validation error attached to them that gets set in the submit and cleared when typing in the field that has the error. Those things are all simple to implement with this state. But we also do have one extra requirement: you can only press submit when all errors are cleared. This requires us to extract from the state whether there are any errors. For this we can write a function like this:

在我們的登入表單中,我們有一個密碼和使用者名字段。 這兩個字段都可以附加一個驗證錯誤,該驗證錯誤會在送出中設定,并在輸入包含錯誤的字段時清除。 這些事情在這種狀态下都易于實作。 但是我們也有一個額外的要求:隻有清除所有錯誤後,您才能按送出。 這就要求我們從狀态中提取是否有任何錯誤。 為此,我們可以編寫如下函數:

const hasErrors = (s: LoginFormState): boolean => 
  s.username.error.kind == 'none' && s.password.error.kind == 'none'
           

While this function works perfectly fine it is not ideal. If someone adds a field to the form it is not unlikely they will forget to update the function. Alternatively we can use the following state, which satisfies all our requirements, but uses a

Map

to collect all the errors:

盡管此功能運作良好,但并不理想。 如果有人在表單中添加字段,則他們不太可能會忘記更新功能。 另外,我們可以使用以下狀态滿足所有要求,但使用

Map

收集所有錯誤:

interface LoginFormState {
  username: string
  password: string
  errors: Map<'username' | 'password', string>
}


const hasErrors = (s: LoginFormState): boolean => !s.errors.isEmpty()
           

This state contains the exact same data, is equally clear and well designed But it suits this situation better because it makes it easier to extract the facts we need. A task like clearing all the errors is also easier, you just set the

errors

map to a new empty map.

該狀态包含完全相同的資料,同樣清晰且設計良好,但是它更适合這種情況,因為它使提取所需的事實更加容易。 清除所有錯誤等任務也更加容易,您隻需将

errors

映射設定為新的空映射即可。

A good type satisfies all previous rules and will serve as a solid backbone to your application. A great type will allow your codebase to be as simple as possible.

好的類型可以滿足所有以前的規則,并且可以作為您應用程式的堅實基礎。 很棒的類型将使您的代碼庫盡可能簡單。

This is less of a rule and more of a reminder, but we should always consider how our types are going to be used when designing them. Especially when talking about the type definition of the state of your application because this is the backbone of you codebase. Everything will be influenced by how easy or hard it is to use your type, so put some effort into making it as easy as possible without sacrificing quality.

這不是一個規則,而是一個提醒,但是在設計它們時,我們應該始終考慮如何使用我們的類型。 尤其是在談論應用程式狀态的類型定義時,因為這是代碼庫的基礎。 一切都會受到使用類型的難易程度的影響,是以請在不犧牲品質的情況下盡最大努力使它變得簡單。

結論 (Conclusion)

That were the five rules for designing great states. What do you think? Do you agree with them or are there other things you would put in there? I hope you learned something from reading this article.

那是設計偉大國家的五個規則。 你怎麼看? 您是否同意他們的意見,或者還有其他事情要提出? 希望您從閱讀本文中學到了一些東西。

翻譯自: https://medium.com/hoppinger/type-driven-development-for-single-page-applications-bf8ee98d48e2

單頁應用程式