单页应用程序_单页应用程序的类型驱动开发

单页应用程序

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显示结果? 还是因为loadingtrue来显示加载程序? 或显示错误消息,因为我们有一个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的新MapSet类型之类的库。

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类型,它是对ErrorSuccess的抽象。 这种类型专用于它的整个范例,称为铁路编程

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

单页应用程序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值