react中使用构建缓存

3. 7. 2018: UPDATED to ReasonReact v0.4.2

3. 7. 2018:更新为ReasonReact v0.4.2

You may have heard of Reason before. It’s a syntax on top of OCaml that compiles to both readable JavaScript code and to native and bytecode as well.

您可能以前听说过理性 。 这是OCaml之上的语法,可编译为可读JavaScript代码以及本机代码和字节代码。

This means you could potentially write a single application using Reason syntax, and be able to run it in the browser, and on Android and iOS phones as well.

这意味着您可能会使用Reason语法编写单个应用程序 ,并能够在浏览器以及Android和iOS手机上运行它。

This is one of the reasons why Reason (ouch, pun) is becoming increasingly popular. This is especially true in the JavaScript community because of the syntax similarities.

这是原因(哎呀,双关语)变得越来越流行的原因之一。 由于语法相似,在JavaScript社区中尤其如此。

If you were a JavaScript developer before Reason came out and wanted to learn a functional programming (FP) language, you would have had to also learn a whole new syntax and set of rules. This might’ve discouraged many people.

如果您在Reason问世之前是JavaScript开发人员,并且想学习一种功能编程(FP)语言,那么您还必须学习一种全新的语法和规则集。 这可能使许多人灰心。

With Reason, you mainly need to understand the FP principles on which it’s based — such as immutability, currying, composition, and higher-order-functions.

使用Reason,您主要需要了解FP所基于的FP原理,例如不变性,currying,组成和高阶函数。

Before I discovered Reason, I was trying to use FP principles in JavaScript as much as I could. However, JavaScript is limited in this sense, since it’s not meant to be an FP language. To take advantage of these principles effectively, you need to use a bunch of libraries that create complicated abstractions which are hidden from you.

在我发现Reason之前,我试图尽可能多地在JavaScript中使用FP原理。 但是,JavaScript在这种意义上受到限制,因为它并不意味着是FP语言。 为了有效地利用这些原理,您需要使用一堆库来创建对您隐藏的复杂抽象。

Reason, on the other hand, opens the entire FP realm to all interested JavaScript developers. It provides us with an opportunity to use all those cool OCaml features using syntax we dearly know.

另一方面,Reason向所有感兴趣JavaScript开发人员开放了整个FP领域。 它为我们提供了使用我们熟知的语法来使用所有这些很酷的OCaml功能的机会。

Last but not least, we can write our React or React Native apps using Reason.

最后但并非最不重要的一点是,我们可以使用Reason编写React或React Native应用程序。

您为什么要尝试理性? (Why should you give Reason a try?)

I hope you’ll discover the answer for yourself by the time you’ve finished reading this post.

希望您在阅读完这篇文章后能自己找到答案。

As we go through the source code of the classic Tic Tac Toe game — written in Reason, using React — I’ll explain the core features of the language. You’ll see the benefits of the strong type system, immutability, pattern matching, functional composition using pipe, and so on. Unlike JavaScript, these features are intrinsic to Reason itself.

当我们浏览经典的Tic Tac Toe游戏的源代码时,使用React用Reason编写。我将解释该语言的核心功能。 您将看到强类型系统,不变性,模式匹配,使用管道的功能组合等优点。 与JavaScript不同,这些功能是Reason本身固有的。

热身 (Warming up)

Before getting your hands dirty, you need to install Reason on your machine following this guide.

在弄脏手之前,您需要按照本指南在计算机上安装Reason。

After that, you need to setup your app. To do this, you can either clone my repository containing the code of our app or you can setup your own project using ReasonScripts and code along.

之后,您需要设置您的应用程序。 为此,您可以克隆包含我们应用程序代码的存储库 ,也可以使用ReasonScripts和代码来设置自己的项目。

To view your app in the browser, you need to compile your Reason files to JavaScript ones first. The BuckleScript compiler will take care of that.

要在浏览器中查看您的应用,您需要先将Reason文件编译为JavaScript文件。 BuckleScript编译器将负责此工作。

In other words, when you run npm start (in the ReasonScripts project), your Reason code gets compiled to JavaScript. The result of the compilation is then rendered to the browser. You can see for yourself how readable the compiled code is by checking the lib folder inside your app.

换句话说,当您运行npm start (在ReasonScripts项目中)时,您的Reason代码将编译为JavaScript。 然后将编译结果呈现给浏览器。 通过检查应用程序内的lib文件夹,您可以自己查看编译后代码的可读性。

我们的第一个组成部分 (Our first component)

As we’ve already mentioned, our Tic Tac Toe app is written using ReasonReact library. This makes Reason approachable for JavaScript developers, and a lot of newcomers are coming from this community.

正如我们已经提到的,我们的Tic Tac Toe应用是使用ReasonReact库编写的。 这使得Reason对于JavaScript开发人员来说很容易上手,并且这个社区有很多新来者。

Our app has a classic component structure, like any other React app. We’ll go through the components top-down when talking about UI, and bottom-up when describing their logic.

我们的应用程序具有经典的组件结构,就像其他任何React应用程序一样。 在谈论UI时,我们将自上而下浏览组件,而在描述其逻辑时,则将自下而上浏览。

Let’s get started by taking a look at the top level App component.

让我们开始看看顶级的App组件。

let component = ReasonReact.statelessComponent("App");
let make = _children => {...component,render: _self =><div><div className="title">(ReasonReact.string("Tic Tac Toe"))</div><Game /></div>,
};

The component gets created when you call ReasonReact.statelessComponent and pass the name of the component to it. You don’t need any class keywords like in React, since Reason doesn’t have any whatsoever.

当您调用ReasonReact.statelessComponent并将该组件的名称传递给它时,将创建该组件。 您不需要像React中的任何类关键字,因为Reason没有任何内容。

The component is neither a class nor function — it’s a so-called record. record is one of Reason’s data structures, which is similar to the JavaScript object. Unlike the latter, however, record is immutable.

组件既不是类也不是函数,它既是所谓的record 。 record是Reason的数据结构之一,类似于JavaScript对象。 但是,与后者不同, record是不可变的。

Our new record component contains various default properties such as the initial state, lifecycle methods, and render. To adjust the component to our needs, we need to override some of these properties. We can do that inside the make function that returns our component.

我们的新record组件包含各种默认属性,例如初始状态,生命周期方法和呈现。 要根据我们的需求调整组件,我们需要覆盖其中一些属性。 我们可以在返回组件的make函数中执行此操作。

Since the record is immutable, we can’t override its properties by mutation. Instead, we need to return a new record. To do this, we need to spread our component and redefine the properties we want to change. This is very similar to the JavaScript object spread operator.

由于record是不可变的,因此我们无法通过更改覆盖其属性。 相反,我们需要返回一个新record 。 为此,我们需要扩展组件并重新定义我们要更改的属性。 这与JavaScript对象传播运算符非常相似。

Since the App is a pretty simple component, we want to override only the default render method so we can render our elements to the screen. The render method takes a single self argument that gives us access to the state and reducers, as we’ll see later.

由于该App是一个非常简单的组件,因此我们只想覆盖默认的render方法,以便可以将元素渲染到屏幕上。 render方法采用单个self参数,使我们可以访问状态和reducer,我们将在后面看到。

Since ReasonReact supports JSX, our render function can return JSX elements. The uncapitalized element will be recognized as a DOM element — div. The capitalized element will be recognized as a component — Game.

由于ReasonReact支持JSX ,因此我们的render函数可以返回JSX元素。 没有大写的元素将被识别为DOM元素— div 。 大写的元素将被识别为组件Game

Due to Reason’s strong type system, you can’t simply pass a string to an element in order to display it, as you can in classic React.

由于Reason强大的类型系统,您不能像经典React那样简单地将字符串传递给元素以显示它。

Instead, you need to pass such string into a ReasonReact.string helper function that’ll convert it into reactElement which can be rendered.

相反,您需要将这样的字符串传递给ReasonReact.string帮助器函数,该函数会将其转换为可以呈现的reactElement

Since this is a little bit verbose, and we’ll use this helper quite often, let’s store it in a toString variable. In Reason, you can use only the let keyword to do that.

由于这有点冗长,并且我们将经常使用此帮助器,因此将其存储在toString变量中。 在理性中,您只能使用let关键字来做到这一点。

let toString = ReasonReact.string;

Before moving any further, let’s talk a bit about the make function’s arguments. Since we are not passing any props to the App component, it takes only the default children argument.

在继续之前,让我们先讨论一下make函数的参数。 由于我们没有将任何道具传递给App组件,因此它仅采用默认的children参数。

However, we are not using it. We can make this explicit by writing an underscore before it. If we haven’t done this, the compiler would give us a warning that the argument is not being used. We are doing the same with the self argument in the render method.

但是,我们没有使用它。 我们可以通过在其前面加一个下划线来使之明确。 如果我们还没有这样做,编译器会警告我们该参数没有被使用。 我们在render方法中使用self参数做同样的事情。

Understandable error and warning messages are another cool feature that’ll improve your developer experience, compared to JavaScript.

与JavaScript相比,可理解的错误和警告消息是另一个很酷的功能,它将改善您的开发人员体验。

设置变体类型 (Setting up variant types)

Before diving into the application itself, we’ll define our types first.

在深入研究应用程序本身之前,我们将首先定义类型。

Reason is a statically typed language. This means it evaluates the types of our values during the compilation time. In other words, you don’t need to run your app to check if your types are correct. This also means that your editor can provide you with useful editing support.

原因是一种静态类型的语言。 这意味着它将在编译期间评估我们的值的类型。 换句话说,您无需运行应用程序即可检查类型是否正确。 这也意味着您的编辑器可以为您提供有用的编辑支持 。

However, having a type system doesn’t mean you need to explicitly define types for all the values. If you decide not to, Reason will figure out (infer) the types for you.

但是,拥有类型系统并不意味着您需要为所有值明确定义类型。 如果您决定不这样做,Reason将为您找出(推断)类型。

We’ll take advantage of the type system to define the types that we’ll use throughout our app. This will force us to think about the structure of our app before coding it and we’ll get a code documentation as a bonus.

我们将利用类型系统来定义将在整个应用程序中使用的类型。 这将迫使我们在对应用程序进行编码之前考虑一下应用程序的结构,并且还会获得一份代码文档作为奖励。

If you’ve had any experience with TypeScript or Flow, Reason types will look familiar. However, unlike these two libraries, you don’t need any previous configuration at all (I’m looking at you Typescript). Types are available out of the box.

如果您有使用TypeScript或Flow的经验,Reason类型将很熟悉。 但是,与这两个库不同,您根本不需要任何先前的配置(我在看您的Typescript)。 开箱即用。

In Reason, we can distinguish between types and variant types (in short variants). Types are for example bool, string, and int. On the other hand, variants are more complex. Think of them as of enumerable sets of values—or more precisely, constructors. Variants can be processed via pattern matching, as we’ll see later.

在Reason中,我们可以区分类型和变体类型 (简称为变体)。 类型是例如boolstringint 。 另一方面,变体更加复杂。 将它们视为可枚举的值集,或更准确地说,是构造函数。 可以通过模式匹配来处理变体,我们将在后面看到。

type player =| Cross| Circle;type field =| Empty| Marked(player);

Here we define player and field variants. When defining a variant, you need to use a type keyword.

在这里,我们定义playerfield 变体 。 定义变体时,需要使用type关键字。

Since we are building a Tic Tac Toe game, we’ll need two players. So, the player type will have two possible constructors — Cross and Circle.

由于我们正在构建Tic Tac Toe游戏,因此我们需要两名玩家。 因此, player类型将具有两个可能的构造函数CrossCircle

If we think about the playing board, we know that each field type can have two possible constructors — either Empty or Marked by one of the players.

如果我们考虑游戏板,我们知道每种field类型都可以有两个可能的构造函数- Empty或由一个玩家Marked的。

If you take a look at the Marked constructor, you can see that we are using it as a data structure. We use a variant to hold another piece of data. In our case, we pass it the player variant. This behavior is pretty powerful since it enables us to combine different variants and types together to create more complex types.

如果看一下Marked构造函数,可以看到我们正在将其用作数据结构。 我们使用一个变体来保存另一段数据。 在我们的案例中,我们将其传递给player变量。 这种行为非常强大,因为它使我们能够将不同的变体和类型组合在一起以创建更复杂的类型。

So, we’ve got the field variant. However, we need to define the whole playing board which consists of rows of fields.

因此,我们有了field变量。 但是,我们需要定义由多个字段组成的整个游戏板。

type row = list(field);
type board = list(row);

Each row is a list of fields and the playing board is composed of a list of rows.

rowfield s的列表,游戏boardrow s的列表组成。

The list is one of Reason’s data structures—similar to the JavaScript array. The difference is, it’s immutable. Reason also has an array as a mutable fixed-length list. We’ll come back to these structures later.

list是Reason的数据结构之一,类似于JavaScript数组。 不同之处在于,它是不变的。 原因也有一个array作为可变的固定长度列表。 稍后我们将回到这些结构。

type gameState = | Playing(player)| Winner(player)| Draw;

Another variant we need to define is a gameState. The game can have three possible states. One of the players can be Playing, be a Winner, or we can have a Draw.

我们需要定义的另一个变体是gameState 。 游戏可以具有三种可能的状态。 player可以是Playing Winner ,或是Winner ,或者我们可以开Draw

Now, we have all the types we need to compose the state of our game.

现在,我们拥有构成游戏状态所需的所有类型。

type state = {board,gameState,
};

Our component’s state is a record composed of the board and the gameState.

我们组件的状态是由boardgameState组成的record

Before moving any further, I’d like to talk about modules. In Reason, files are modules. For example, we stored all our variants inside SharedTypes.re file. This code gets automatically wrapped inside the module like this:

在进一步介绍之前,我想谈谈模块。 实际上,文件是模块。 例如,我们将所有变体存储在SharedTypes.re文件中。 这段代码会自动包装在模块中,如下所示:

module SharedTypes {/* variant types code */
}

If we wanted to access this module in a different file, we don’t need any import keyword. We can easily access our modules anywhere in our app using the dot notation — for example SharedTypes.gameState.

如果要在其他文件中访问此模块,则不需要任何import关键字。 我们可以使用点符号(例如SharedTypes.gameState轻松地在应用程序中的任何位置访问模块。

Since we are using our variants quite often, we can make it more concise by writing open SharedTypes at the top of the file in which we want to access our module. This allows us to drop the dot notation since we can use our module in the scope of our file.

由于我们经常使用变体,因此可以通过在要访问模块的文件顶部编写open SharedTypes使其更加简洁。 因为我们可以在文件范围内使用模块,所以这使我们可以删除点符号。

建立状态 (Establishing state)

Since we know how the state of our app will look, we can start building the game itself.

由于我们知道应用程序的状态,因此我们可以开始构建游戏本身。

We’ve seen that our App component renders the Game component. This is the place where all the fun starts. I’ll walk you through the code step-by-step.

我们已经看到,我们的App组件呈现了Game组件。 这是所有乐趣开始的地方。 我将逐步指导您完成代码。

The App was a stateless component, similar to the functional component in React. On the other hand, the Game is a stateful one which means it can contain state and reducers. Reducers in Reason are based on the same principles as those you know from Redux. You call an action, and the reducer will catch it and update the state accordingly.

App是一个无状态组件,类似于React中的功能组件。 另一方面, Game是有状态的,这意味着它可以包含状态和约简。 理性中的reducers基于与Redux相同的原理。 您调用一个动作,减速器将捕获该动作并相应地更新状态。

To see what’s going on in the Game component, let’s inspect the make function (the code is shortened).

要查看Game组件中发生了什么,让我们检查一下make函数(缩短了代码)。

let component = ReasonReact.reducerComponent("Game");let make = _children => {...component,initialState: () => initialState,reducer: (action: action, state: state) => ...,render: ({state, send}) => ...,
};

In the App component, we’ve overridden only the render method. Here, we are overriding reducer and initialState properties as well. We’ll talk about reducers later.

App组件中,我们仅覆盖了render方法。 在这里,我们也覆盖了reducerinitialState属性。 稍后我们将讨论减速器。

initialState is a function that (surprisingly) returns the initial state which we stored in a variable.

initialState是一个函数(令人惊讶地)返回我们存储在变量中的初始状态的函数。

let initialState = {board: [[Empty, Empty, Empty],[Empty, Empty, Empty],[Empty, Empty, Empty],],gameState: Playing(Cross),
};

If you scroll up a little bit and check our state type, you’ll see that the initialState has the same structure. It’s composed of the board that consists of rows of fields. At the beginning of the game all fields are Empty.

如果向上滚动一点并检查我们的state类型,您会看到initialState具有相同的结构。 它由包含field s的rowboard组成。 在游戏开始时,所有字段均为Empty

However, their status may change as the game goes on. Another part of the state is the gameState which is initially set to theCross player who plays first.

但是,它们的状态可能会随着游戏的进行而改变。 状态的另一部分是gameState ,它最初设置为首先玩的Cross玩家。

渲染板 (Rendering board)

Let’s take a look at the render method of our Game component.

让我们看一下Game组件的render方法。

render: ({state, send}) =><div className="game"><BoardstateonRestart=(_evt => send(Restart))onMark=(id => send(ClickSquare(id)))/></div>,

We already knew that it receives the self argument. Here, we use destructuring to access the state and the send function. This works just like in JavaScript.

我们已经知道它接受了self论证。 在这里,我们使用解构来访问statesend功能。 就像在JavaScript中一样。

The render method returns the Board component and passes it the state and two state handlers as props. The first one takes care of the app restart and the second one fires when the field gets marked by a player.

render方法返回Board组件,并将state和两个状态处理程序作为道具传递给它。 第一个负责应用的重启,第二个负责在玩家标记该字段时触发。

You might’ve noticed that we aren’t writing state=state when passing the state prop. In Reason, if we are not changing the prop’s name, we can pass prop using this simplified syntax.

您可能已经注意到,通过state道具时,我们不是在写state=state 。 因此,如果我们不更改道具名称,则可以使用此简化语法传递道具。

Now, we can take a look at the Board component. I’ve omitted most of the render method for the time being.

现在,我们来看一下Board组件。 我暂时省略了大多数render方法。

let component = ReasonReact.statelessComponent("Board");let make = (~state: state, ~onMark, ~onRestart, _children) => {...component,render: _ =><div className="game-board">/* ... */</div>,
};

The Board is a stateless component. As you might’ve noticed, the make function now takes several arguments. These are the props we’ve passed from the Game parent component.

Board是无国籍的组成部分。 您可能已经注意到, make函数现在带有几个参数。 这些是我们从Game父组件传递的道具。

The ~ symbol means that the argument is labeled. When calling a function with such an argument, we need to explicitly write the name of the argument when calling this function (component). And that’s what we did when we passed the props to it in the Game component.

~符号表示该参数已标记。 当使用这样的参数调用函数时,我们需要在调用该函数(组件)时显式地编写参数名称。 这就是我们在Game组件中将道具传递给它时所做的。

You might’ve also noticed that we are doing another thing with one of the arguments — ~state:state. In the previous section, we defined our state type. Here, we are telling the compiler that the structure of this argument should be same as of the state type. You might know this pattern from Flow.

您可能还注意到,我们正在使用其中一个参数~state:state做另一件事。 在上一节中,我们定义了state类型。 在这里,我们告诉编译器此参数的结构应与state类型相同。 您可能从Flow知道这种模式。

Let’s come back to the render method of the Board component.

让我们回到Board组件的render方法。

Since we are dealing with lists there, we’ll talk about them a little bit more now, before inspecting the rest of the render method.

由于我们在这里处理列表,因此在检查其余render方法之前,我们现在将稍微讨论它们。

游览I:列表和数组 (Excursion I: list and array)

In Reason, we have two data structures resembling JavaScript arrays — list and array. The list is immutable and resizable, whereas the array is mutable and has a fixed length. We are using a list due to its flexibility and efficiency which really shines when we use it recursively.

在原因,我们有两个数据结构类似于JavaScript数组- listarray 。 该list是不可变的并且可调整大小,而array是可变的,并且具有固定的长度。 我们使用list因为它的灵活性和效率,当我们递归使用它时,它确实很出色。

To map a list, you can use List.map method that receives two arguments—a function and a list. The function takes an element from the list and maps it. This works pretty much like the JavaScript Array.map. Here’s a simple example:

要映射list ,可以使用List.map方法,该方法接收两个参数-一个函数和一个list 。 该函数从list获取一个元素并进行映射。 这非常类似于JavaScript Array.map 。 这是一个简单的例子:

let numbers = [1, 5, 8, 9, 15];
let increasedNumbers = List.map((num) => num + 2, numbers);
Js.log(increasedNumbers);  /* [3,[7,[10,[11,[17,0]]]]] */

What? You’re saying that the printed result looks weird? This is because the lists in Reason are linked.

什么? 您是说打印结果看起来很奇怪? 这是因为Reason中的列表是链接的 。

Printing lists in your code can be confusing. Fortunately, you can convert it into an array using the Array.of_list method.

在代码中打印列表可能会造成混淆。 幸运的是,您可以使用Array.of_list方法将其转换为array

Js.log(Array.of_list(increasedNumbers));  /* [3,7,10,11,17] */

Let’s come back to our app and remind ourselves how our state looks.

让我们回到我们的应用程序,提醒自己我们的state

let initialState = {board: [[Empty, Empty, Empty],[Empty, Empty, Empty],[Empty, Empty, Empty],],gameState: Playing(Cross),
};

Inside the Board’s render method we first map over board which is composed of a list of rows. So, by mapping over it, we’ll gain access to the rows. Then, we render the BoardRow component.

内部审计委员会的render方法,我们首先映射在board它是由行的列表中。 因此,通过对其进行映射,我们将可以访问row s。 然后,我们渲染BoardRow组件。

let component = ReasonReact.statelessComponent("Board");let make = (~state: state, ~onMark, ~onRestart, _children) => {...component,render: _ =><div className="game-board">( ReasonReact.array(Array.of_list(List.mapi((index: int, row: row) =><BoardRowkey=(string_of_int(index))gameState=state.gameStaterowonMarkindex/>,state.board,),),))/* ... */

We are using the List.mapi method, which provides us with an index argument that we need to uniquely define our ids.

我们正在使用List.mapi方法,该方法为我们提供了一个index参数,我们需要用它来唯一地定义ID。

When mapping the list to the JSX elements, we need to do two additional things.

list映射到JSX元素时,我们需要做另外两件事。

First, we need to convert it to an array using Array.of_list. Secondly, we need to convert the result to the reactElement using ReasonReact.array, since we (as already mentioned) can’t simply pass the string to the JSX element like in React.

首先,我们需要使用Array.of_list将其转换为array 。 其次,我们需要将结果转换到reactElement使用ReasonReact.array ,因为我们(已经提到)不能串简单地传递到JSX元素像React。

To get to the field values, we need to map over each row as well. We are doing this inside the BoardRow component. Here, each element from the row is then mapped to the Square component.

为了获得字段值,我们还需要映射每row 。 我们在BoardRow组件中执行此BoardRow 。 在此,该row每个元素都将映射到Square组件。

let component = ReasonReact.statelessComponent("BoardRow");let make = (~gameState: gameState, ~row: row, ~onMark, ~index: int, _children) => {...component,render: (_) =><div className="board-row">(ReasonReact.array(Array.of_list(List.mapi((ind: int, value: field) => {let id = string_of_int(index) ++ string_of_int(ind);<Squarekey=idvalueonMark=(() => onMark(id))gameState/>;},row,),),))</div>,
};

Using these two mappings, our board gets rendered. You’ll agree with me that the readability of this code isn’t so good because of all the function wrappings.

使用这两个映射,可以渲染我们的电路板。 您会同意我的观点,由于所有函数包装,该代码的可读性不是很好。

To improve it, we can use the pipe operator which takes our list data and pipes it through our functions. Here’s the second mapping example — this time using pipe.

为了改善它,我们可以使用pipe运算符,该运算符获取list数据并通过函数将其管道传输。 这是第二个映射示例-这次使用pipe

let component = ReasonReact.statelessComponent("BoardRow");let make = (~gameState: gameState, ~row: row, ~onMark, ~index: int, _children) => {...component,render: (_) =><div className="board-row">(row|> List.mapi((ind: int, value: field) => {let id = string_of_int(index) ++ string_of_int(ind<Square key=idvalueonMark=(() => onMark(id))gameState/>;})|> Array.of_list|> ReasonReact.array)</div>,
};

This makes our code much more readable, don’t you think? First, we take the row and pass it to the mapping method. Then, we convert our result to an array. Finally, we convert it to the reactElement.

这使我们的代码更具可读性,您不觉得吗? 首先,我们将该row传递给映射方法。 然后,将结果转换为array 。 最后,我们将其转换为reactElement

By mapping our board, we are rendering a bunch of Square components to the screen and by doing so, we are creating the whole playing board.

通过映射我们的棋盘,我们在屏幕上渲染了一堆Square组件,并以此创建了整个游戏棋盘。

We’re passing a couple of props to the Square. Since we want our id to be unique, we create it by combining indices from both mappings. We are also passing down the value which contains the field type that can be either Empty or Marked.

我们要把一些道具传递给Square 。 因为我们希望id是唯一的,所以我们通过组合两个映射的索引来创建它。 我们还向下传递了包含可以为EmptyMarkedfield类型的value

Finally, we pass a gameState and the onMark handler which will get invoked when a particular Square is clicked.

最后,我们传递一个gameStateonMark处理程序,当单击特定Square时将调用它们。

输入栏位 (Entering fields)

let component = ReasonReact.statelessComponent("Square");let make = (~value: field, ~gameState: gameState, ~onMark, _children) => {...component,render: _self =><buttonclassName=(getClass(gameState, value))disabled=(gameState |> isFinished |> Js.Boolean.to_js_boolean)onClick=(_evt => onMark())>(value |> toValue |> toString)</button>,
};

The Square component renders a button and passes it some props. We are using a couple of helper functions here, but I won’t talk about all of them in detail. You can find them all in the repo.

Square组件呈现一个按钮,并向其传递一些道具。 我们在这里使用了几个辅助函数,但是我不会详细讨论它们。 你可以找到所有的回购 。

The button’s class is calculated using the getClass helper function which turns the square green when one of the players wins. When this happens, all the Squares will be disabled as well.

使用getClass辅助函数计算按钮的类,当其中一位玩家获胜时,该函数将正方形变为绿色。 发生这种情况时,所有Square也将被禁用。

To render the button’s value, we use two helpers.

为了呈现按钮的value ,我们使用了两个帮助器。

let toValue = (field: field) =>switch (field) {| Marked(Cross) => "X"| Marked(Circle) => "O"| Empty => ""
};

toValue will convert the field type to the string using pattern matching. We’ll talk about pattern matching later. For now, you need to know that we are matching the field data to our three patterns. So, the result would be X, O, or an empty string. Then, we use toString to convert it to the reactElement.

toValue将使用模式匹配将field类型转换为字符串。 稍后我们将讨论模式匹配。 现在,您需要知道我们正在将field数据与我们的三种模式进行匹配。 因此,结果将是XO或空字符串。 然后,我们使用toString将其转换为reactElement

Phew. We’ve just rendered the game board. Let’s quickly recap how we did it.

ew 我们刚刚渲染了游戏板。 让我们快速回顾一下我们是如何做到的。

Our top-level App component renders the Game component which holds the game state and passes it down along with the handlers to the Board component.

我们的顶级App组件呈现了Game组件,该组件保留游戏状态并将其与处理程序一起传递给Board组件。

The Board then takes the board state prop and maps the rows to the BoardRow component which maps the rows to the Square components. Each Square has an onClick handler that will fill it with a square or a circle.

然后, Board获取董事会状态道具并将行映射到BoardRow组件,后者将行映射到Square组件。 每个Square都有一个onClick处理程序,该处理程序将用正方形或圆形填充它。

使它已经做某事! (Make it do something already!)

Let’s take a look at how our logic controlling the game works.

让我们看看控制游戏的逻辑是如何工作的。

Since we have a board, we can allow a player to click on any square. When this happens, the onClick handler is fired and the onMark handler is called.

由于我们有一块木板,因此我们可以允许玩家单击任何正方形。 发生这种情况时,将触发onClick处理程序并调用onMark处理程序。

/* Square component */
<buttonclassName=(getClass(gameState, value))disabled=(gameState |> isFinished |> Js.Boolean.to_js_boolean)onClick=(_evt => onMark())>(value |> toValue |> toString)
</button>

The onMark handler got passed from the BoardRow component, but it was originally defined in the Game component that takes care of the state.

onMark处理程序是从BoardRow组件传递BoardRow ,但它最初是在负责状态的Game组件中定义的。

/* Game component */
render: ({state, send}) =><div className="game"><BoardstateonRestart=(_evt => send(Restart))onMark=(id => send(ClickSquare(id)))/></div>,

We can see that the onMark prop is a ClickSquare reducer, which means we are using it to update the state (as in Redux). The onRestart handler works similarly.

我们可以看到onMark道具是一个ClickSquare器,这意味着我们正在使用它来更新状态(如Redux中一样)。 onRestart处理程序的工作方式与此类似。

Notice that we are passing square’s unique id to the onMark handler inside the BoardRow component.

请注意,我们是路过广场的独特idonMark内部处理BoardRow组件。

/* BoardRow component */
(row|> List.mapi((ind: int, value: field) => {let id = string_of_int(index) ++ string_of_int(ind<Square key=idvalueonMark=(() => onMark(id))gameState/>;})|> Array.of_list|> ReasonReact.array
)

Before taking a look at our reducers in detail, we need to define actions to which our reducers will respond.

在详细研究我们的减速器之前,我们需要定义减速器将响应的动作。

type action =| ClickSquare(string)| Restart;

As with the global variant types, this forces us to think about our logic before we start implementing it. We define two action variants. ClickSquare takes one argument that will have a type of astring.

与全局变量类型一样,这迫使我们在开始执行逻辑之前先考虑一下自己的逻辑。 我们定义了两个动作变体。 ClickSquare接受一个参数,该参数将具有string类型。

Now, let’s take a look at our reducers.

现在,让我们看一下减速器。

let updateBoard = (board: board, gameState: gameState, id) =>board|> List.mapi((ind: int, row: row) =>row|> List.mapi((index: int, value: field) =>string_of_int(ind) ++ string_of_int(index) === id ?switch (gameState, value) {| (_, Marked(_)) => value| (Playing(player), Empty) => Marked(player)| (_, Empty) => Empty} :value));reducer: (action: action, state: state) =>switch (action) {| Restart => ReasonReact.Update(initialState)| ClickSquare((id: string)) =>let updatedBoard = updateBoard(state.board, state.gameState, id);ReasonReact.Update({board: updatedBoard,gameState:checkGameState3x3(updatedBoard, state.board, state.gameState),});},

The ClickSquare reducer takes an id of the particular Square. As we’ve seen, we are passing in the BoardRow component. Then, our reducer calculates a new state.

ClickSquare采用特定Squareid 。 如我们所见,我们传入了BoardRow组件。 然后,我们的减速器计算一个新状态。

For the board state update, we’ll call the updateBoard function. It uses the same mapping logic we used in the Board and BoardRow component. Inside of it, we map over the state.board to get the rows and then map over the rows to get the field values.

对于board状态更新,我们将调用updateBoard函数。 它使用与BoardBoardRow组件相同的映射逻辑。 在其内部,我们在state.board进行映射以获取行,然后在行上进行映射以获取字段值。

Since the id of each square is a composition of ids from both mappings, we’ll use it to find the field which the player clicked. When we find it, we’ll use the pattern matching to determine what to do with it. Otherwise, we’ll leave the square’s value unmodified.

由于id每平方米的IDS是来自两个映射组成,我们将用它来查找该用户点击了该领域。 找到它后,我们将使用模式匹配来确定如何处理它。 否则,我们将不修改平方的value

游览II:模式匹配 (Excursion II: pattern matching)

We use the pattern matching to process our data. We define patterns which we’ll match against our data. When exercising the pattern matching in Reason, we use a switch statement.

我们使用模式匹配来处理我们的数据。 我们定义将与数据匹配的模式 。 在Reason中执行模式匹配时,我们使用switch语句。

switch (state.gameState, value) {| (_, Marked(_)) => value| (Playing(player), Empty) => Marked(player)| (_, Empty) => Empty
}

In our case, we are using a tuple to represent our data. Tuples are data structures that separate data with commas. Our tuple contains the gameState and the value (containing the field type).

在我们的例子中,我们使用一个元组来表示我们的数据 。 元组是用逗号分隔数据的数据结构。 我们的tuple包含gameStatevalue (包含field类型)。

Then we define multiple patterns that we’ll match against our data. The first match determines the result of the entire pattern matching.

然后,我们定义将与数据匹配的多个模式 。 第一个匹配确定整个模式匹配的结果。

By writing an underscore inside the pattern, we are telling the compiler that we don’t care what the particular value is. In other words, we want to have a match every time.

通过在模式内部写下划线,我们告诉编译器我们不在乎特定的值是什么。 换句话说,我们希望每次都有一场比赛。

For example, the first pattern is matched when the value is Marked by any player. So, we don’t care about the gameState and we don’t care about the player type either.

例如,当value被任何玩家Marked时,第一个模式将匹配。 因此,我们不在乎gameState ,也不在乎玩家类型。

When this pattern is matched, the result is the original value. This pattern prevents players from overriding already marked Squares.

匹配此模式后,结果为原始value 。 这种模式可防止玩家覆盖已经标记的Squares

The second pattern addresses the situation when any player is playing, and the field is Empty. Here, we use the player type in the pattern and then again in the result. We are basically saying that we don’t care about which player is playing (Circle or Cross) but we still want to mark the square according to the player that is actually playing.

第二种模式解决了任何玩家正在玩并且该字段为Empty 。 在这里,我们在模式中使用player类型,然后在结果中再次使用。 基本上,我们说的是我们不在乎哪个玩家在玩( CircleCross ),但我们仍要根据实际玩的玩家来标记正方形。

The last pattern acts as the default one. If the first or the second pattern isn’t matched, the third will always match. Here, we don’t care about the gameState.

最后一个模式用作默认模式。 如果第一个或第二个模式不匹配,则第三个将始终匹配。 在这里,我们不在乎gameState

However, since we’re checking for the Playing game state in the previous pattern, we are now checking for the Draw or Winner gameState type. If this is the case, we’ll leave the field Empty. This default scenario prevents players from continuing to play when the game is over.

但是,由于我们要检查先前模式中的“ Playing游戏状态,因此现在要检查“ Draw或“ Winner gameState类型。 如果是这种情况,我们将保留字段Empty 。 此默认方案可防止玩家在游戏结束后继续玩游戏。

A cool thing about pattern matching in Reason is that the compiler will warn you if you haven’t covered all the possible pattern matches. This will save you a lot of trouble, because you’ll always know if you’ve covered all the possible scenarios. So, if the compiler is not giving you any warnings, your pattern matching will never fail.

原因中关于模式匹配的一个很酷的事情是,如果您没有涵盖所有可能的模式匹配,编译器会警告您。 这将为您省去很多麻烦,因为您将始终知道是否已涵盖所有可能的情况。 因此,如果编译器未向您发出任何警告,则模式匹配将永远不会失败。

When the pattern matching is finished, the particular field gets updated. When all the mappings are done, we get a new board state and store it as the updatedBoard. We can then update the component’s state by calling ReasonReact.Update.

模式匹配完成后,特定字段将更新。 完成所有映射后,我们将获得一个新的板状态并将其存储为updatedBoard 。 然后,我们可以通过调用ReasonReact.Update来更新组件的状态。

ReasonReact.Update({board: updatedBoard,gameState:checkGameState3x3(updatedBoard, state.board, state.gameState),

We update the board state using the result of the pattern matching. When updating the gameState, we call the checkGameState3x3 helper which calculates the state of the game for us.

我们使用模式匹配的结果来更新board状态。 在更新gameState ,我们调用checkGameState3x3帮助程序,该帮助程序为我们计算游戏状态。

我们有赢家吗? (Do we have a winner?)

Let’s take a look what the checkGameState3x3 does.

让我们看一下checkGameState3x3功能。

First, we need to define all the possible combinations of winning fields (for the 3x3 board) and store them as winningCombs. We also have to define the winningRows type.

首先,我们需要定义所有可能的获胜字段组合(对于3x3板),并将它们存储为winningCombs 。 我们还必须定义winningRows类型。

type winningRows = list(list(int));let winningCombs = [[0, 1, 2],[3, 4, 5],[6, 7, 8],[0, 3, 6],  [1, 4, 7],[2, 5, 8],[0, 4, 8],[2, 4, 6],
];

We passed this list to the checkGameState function as the first argument.

我们将此列表作为第一个参数传递给checkGameState函数。

let checkGameState3x3 = checkGameState(winningCombs);

By doing this, we are taking advantage of the currying principle. When we pass the winningCombs to the checkGameState function, we get back a new function waiting for the rest of the arguments to be passed. We store this new function as the checkGameState3x3.

通过这样做,我们利用了流通原则。 当我们将winningCombs传递给checkGameState函数时,我们将返回一个新函数,等待传递其余参数。 我们将此新功能存储为checkGameState3x3

This behavior is really helpful, since we are able to configure the checkGameState function depending on the width and height of the board.

这种行为确实很有帮助,因为我们能够根据棋盘的宽度和高度来配置checkGameState函数。

Let’s see what’s going on inside the checkGameState function.

让我们看看checkGameState函数内部发生了checkGameState

let checkGameState =(winningRows: winningRows,updatedBoard: board,oldBoard: board,gameState: gameState,) =>oldBoard == updatedBoard ?gameState :{let flattenBoard = List.flatten(updatedBoard);let rec check = (rest: winningRows) => {let head = List.hd(rest);let tail = List.tl(rest);switch (getWinner(flattenBoard, head),gameEnded(flattenBoard),tail,) {| (Cross, _, _) => Winner(Cross)| (Circle, _, _) => Winner(Circle)| (_, true, []) => Draw| (_, false, []) => whosPlaying(gameState)| _ => check(tail)};};check(winningRows);
};

First, we check if the board state is different from the previous one. If that’s not the case, we’ll return the unchanged gameState. Otherwise, we’ll calculate the new game state.

首先,我们检查板状态是否与前一个状态不同。 如果不是这种情况,我们将返回未更改的gameState 。 否则,我们将计算新的游戏状态。

计算新状态 (Calculating new states)

We start determining our new game state by converting the board part of the state, which consists of a list of rows, to a simple list using List.flatten. The flattened result will have this kind of structure:

我们开始通过使用List.flatten将状态的board部分转换为简单list来确定新游戏状态,该board部分由行list List.flatten 。 展平的结果将具有以下结构:

[Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty]

Back in the function, we define a check function that receives a single rest argument that has type of winningRows . The rec keyword before its definition means that it can be invoked recursively. However, for the recursive function calls, we need recursive data as well. Fortunately, the list is a recursive data structure.

回到该函数中,我们定义了一个check函数,该函数接收一个类型为winningRows的单个rest参数。 定义前的rec关键字意味着可以递归调用它。 但是,对于递归函数调用,我们还需要递归数据。 幸运的是,该list是递归数据结构。

We’ve already learned that lists in Reason are linked. This feature enables us to iterate through lists using recursion easily.

我们已经了解到Reason中的列表是链接的。 此功能使我们可以轻松地使用递归遍历列表 。

At the bottom of the checkGameState, we call the check function for the first time and pass it the winningCombs list. Inside the function, we extract the first element from the list and store it as the head. The rest of the list gets stored as the tail.

checkGameState的底部,我们第一次调用check函数,并将其传递给winningCombs列表。 在函数内部,我们从list提取第一个元素并将其存储为headlist的其余部分将作为tail存储。

After that, we use the pattern matching again. We already know how it works, so I won’t go into detail. But it’s worth checking how we define our data and patterns.

之后,我们再次使用模式匹配。 我们已经知道它是如何工作的,所以我将不做详细介绍。 但是值得检查一下我们如何定义数据和模式。

type winner =| Cross| Circle| NoOne;switch (getWinner(flattenBoard, head),gameEnded(flattenBoard),tail,
) { ...

Inside the switch statement, we use a tuple again to represent our data. Our tuple contains three elements—winner type as a result of the getWinner function, boolean as a result of the gameEnded function, and remaining list elements (tail).

switch语句内部,我们再次使用tuple表示数据。 我们的tuple包含三个元素得主类型作为结果getWinner功能,布尔作为结果gameEnded功能,和剩余的list元素( tail )。

Before going any further, let’s talk a bit about these two helper functions.

在继续之前,让我们先谈谈这两个辅助函数。

We’ll take a look inside the getWinner function first.

我们首先来看一下getWinner函数。

let getWinner = (flattenBoard, coords) =>switch (List.nth(flattenBoard, List.nth(coords, 0)),List.nth(flattenBoard, List.nth(coords, 1)),List.nth(flattenBoard, List.nth(coords, 2)),) {| (Marked(Cross), Marked(Cross), Marked(Cross)) => Cross| (Marked(Circle), Marked(Circle), Marked(Circle)) => Circle| (_, _, _) => NoOne};

When we call the check recursive function for the first time, the head will be the first element of the winningRows, that is [0, 1, 2] which is a list. We pass head to the getWinner function as the coords argument along with the flattenBoard.

当我们第一次调用check递归函数时, head将是winningRows的第一个元素,即[0, 1, 2] ,这是一个list 。 我们通过headgetWinner函数作为coords与一起说法flattenBoard

Again, we use the pattern matching with the tuple. Inside the tuple, we use the List.nth method to access the equivalent positions of the coords coordinates in the flattened board list. The List.nth function takes a list and a number and returns the list’s element to that position.

同样,我们使用与tuple匹配的模式。 在tuple内部,我们使用List.nth方法访问List.nth list coords的等效位置。 List.nth函数获取一个list和一个数字,然后将列表的元素返回到该位置。

So, our tuple consists of the three winning coordinates of our board that we’ve accessed using List.nth.

因此,我们的tuple由我们使用List.nth访问的董事会的三个获胜坐标List.nth

Now, we can match our tuple data against the patterns. The first two patterns check if all three fields are marked by the same player. If they are, we’ll return the winner — Cross or Circle. Otherwise, we’ll return NoOne.

现在,我们可以将tuple数据与模式匹配。 前两个模式检查所有三个字段是否都由同一玩家标记。 如果是,我们将退还赢家CrossCircle 。 否则,我们将返回NoOne

Let’s see what’s going on inside the gameEnded function. It checks if all the fields are Marked and returns a boolean.

让我们看看gameEnded函数内部发生了gameEnded 。 它检查是否所有字段都已Marked并返回布尔值。

let gameEnded = board =>List.for_all(field => field == Marked(Circle) || field == Marked(Cross),board,);

Since we know what values can be returned from our helper functions, let’s come back to our check function.

由于我们知道可以从辅助函数中返回什么值,因此让我们回到check函数。

switch (getWinner(flattenBoard, head),gameEnded(flattenBoard),tail,) {| (Cross, _, _) => Winner(Cross)| (Circle, _, _) => Winner(Circle)| (_, true, []) => Draw| (_, false, []) => whosPlaying(gameState)| _ => check(tail)};

Our pattern matching can now determine if the game ended in a win or draw. If these cases are not matched, we’ll move to the following case. If it’s matched, the game will continue and the whosPlaying function will be called, and the other player will take a turn.

现在,我们的模式匹配可以确定游戏是以赢还是平局结束。 如果这些情况不匹配,我们将移至以下情况。 如果匹配,游戏将继续并调用whosPlaying函数,另一位玩家将回合。

let whosPlaying = (gameState: gameState) =>switch (gameState) {| Playing(Cross) => Playing(Circle)| _ => Playing(Cross)};

Otherwise, we’ll call the check function recursively with a new combination of winning fields.

否则,我们将使用获胜字段的新组合递归调用check函数。

That’s it. Now you know how our code controlling the game logic works.

而已。 现在您知道了我们控制游戏逻辑的代码如何工作。

那是所有人! (That’s all folks!)

I hope this post helped you to understand the core features of this promising and still-developing language. However, to fully appreciate the power of this new syntax on top of OCaml, you need to start building your own stuff. Now you’re ready to do that.

我希望这篇文章可以帮助您了解这种有前途且仍在发展中的语言的核心功能。 但是,要充分了解OCaml之上这种新语法的功能,您需要开始构建自己的东西。 现在您准备好了。

Good luck!

祝好运!

If you liked this article, give it a few claps. I would greatly appreciate it and more people will be able to see this post as well.

如果您喜欢这篇文章,请给她一些鼓掌 我将不胜感激,更多的人也将能够看到这篇文章。

This post was originally published on my blog.

该帖子最初发布在我的博客上。

If you have any questions, criticism, observations, or tips for improvement, feel free to write a comment below or reach me via Twitter.

如果您有任何疑问,批评,意见或改进技巧,请随时在下面写评论或通过Twitter与我联系。

翻译自: https://www.freecodecamp.org/news/learn-reasonml-by-building-tic-tac-toe-in-react-334203dd513c/

react中使用构建缓存

react中使用构建缓存_通过在React中构建Tic Tac Toe来学习ReasonML相关推荐

  1. react中使用构建缓存_如何在React中构建热图

    react中使用构建缓存 Heat maps are a great way of visualizing correlations among two data sets.  With colors ...

  2. react中使用构建缓存_完整的React课程:如何使用React构建聊天室应用

    react中使用构建缓存 In this video course, you'll learn React by building a chat room app. 在本视频课程中,您将通过构建聊天室 ...

  3. react中使用构建缓存_如何使用React构建Chatbot

    react中使用构建缓存 My philosophy is simple. To become good at something, you need to do it a lot. 我的哲学很简单. ...

  4. .Net 6.0中的新增特性_.Net 6.0中的新增功能

    .Net 6.0中的新增特性_.Net 6.0中的新增功能 一..Net 6 介绍 .NET 6 作为 LTS 长期支持版本,.NET 6 将会获得 3 年的技术支持. .NET 6 是首个原生支持 ...

  5. css如何保留空格,HTML/CSS中的空格处理_如何保留页面中的空格

    html中的空格的规则 在html中内容中的多个空格一般会被视为一个,连续的多个空格符被自动合并了.同时内容前后的空格也会被清除, 如下: fly63 com 显示效果为: fly63 com 备注: ...

  6. amp jsp空格 nps_HTML/CSS中的空格处理\_如何保留页面中的空格【转】

    HTML/CSS中的空格处理\_如何保留页面中的空格[转] HTML中的空格的规则 在html中内容中的多个空格一般会被视为一个,连续的多个空格符被自动合并了.同时内容前后的空格也会被清除, 如下: ...

  7. python游戏代码运行不了_无法使我的tic tac toe游戏在python中正确运行

    转不到"玩家1"的原因是你的支票中缺少一个空格.你也没有正确地检查一个玩家何时获胜,这就是为什么你会有这种奇怪的行为.你需要检查每个位置,而不仅仅是最后一个.我还添加了对用户输入的 ...

  8. react中使用构建缓存_通过构建海滩度假胜地网站,了解如何使用React,Contentful和Netlify...

    react中使用构建缓存 In this full course from John Smilga you will learn React by building a beach resort we ...

  9. react中使用构建缓存_使用React和Netlify从头开始构建电子商务网站

    react中使用构建缓存 In this step-by-step, 6-hour tutorial from Coding Addict, you will learn to build an e- ...

最新文章

  1. linux下安装sniffit
  2. 前端开发工程师做些什么?
  3. 继 承(面向对象特征之二)
  4. (RabbitMQ) Java Client API Guide
  5. Linux ISATAP配置
  6. SAP系统管理员的工作
  7. 基于图像的三维重建与基于三维点云数据的曲面拟合
  8. 构建高性能.NET应用之配置高可用IIS服务器-第三篇 IIS中三个核心组件的讲解(上)...
  9. 常见面试算法题汇总(Android面试)
  10. Matlab线型,颜色及标记的自定义
  11. 如何用linux系统进行远程控制windows服务器
  12. Freyja2版本对分库分表的处理方式
  13. 别着急抢iPhone 13了!拍照有马赛克,苹果确认部分iPhone13存在bug
  14. 进了小公司的应届程序员如何翻身进入大公司?
  15. 什么是云桌面?企业为什么要关心云桌面?
  16. 关于红帽RHCE考试的那些事儿
  17. JVM cpu过高排查
  18. python乳腺癌细胞挖掘
  19. 小草小草快点长大。。
  20. 操作系统真象还原实验记录之实验十一:实现中断处理(二)

热门文章

  1. 三面美团Java岗,面试竟然被这31道Java基础题难倒了
  2. 大话数据结构——算法
  3. C++ qsort() 函数调用时实参与形参不兼容的问题解决
  4. Java 微信公众号导出所有粉丝(openId)
  5. uva 247(floyd传递闭包)
  6. 解析xml的4种方法详解
  7. 【Ubuntu14】Nginx+PHP5+Mysql记录
  8. I.Mx6 使用串口连接PSAM卡的注意事项
  9. sliverlight 开发FAQ
  10. Windows上PostgreSQL安装配置教程