使用Typescript編寫Redux+Reactjs應用程序

注:本文的原始資料和示例來自ServiceStackApps/typescript-redux ,根據個人實際狀況,作了一些調整,詳見文內說明,感謝原做者的無私分享。javascript

本文經過設置,運行和探索Javascript一些高級的技術棧:css

  • TypeScript - 具有類型的Javascript超集, 提供一些高級別的語法特性(注:真正的面向對象等)和部分ES5的支持
  • typings - 用於搜索和安裝TypeScript語法定義的包管理器
  • React - 簡單,高性能的javascript UI層框架,利用虛擬DOM和應答數據流
  • Redux - javascript 應用程序的狀態管理框架,很是適合和React搭配使用

提供開發大型javascript應用程序強大的基礎,並改進在Visual Studio中的開發體驗(注:事實上,並不是必定在Visual Studio中,其餘的編輯器也是能夠的)html

本文中涉及到的代碼可在此處查看:https://github.com/xuanye/typescript-redux-samplejava

<!--more-->node

安裝 TypeScript

若是你尚未從typescriptlang.org下載安裝最新版本的Typescript。Visual Studio的用戶能夠直接使用下面的連接快速安裝:react

本文已經默認你已經安裝了TypeScript v1.8 或更高的版本jquery

(注:原文中使用JSPM做爲nodejs的包管理器,本文中我仍然使用npm來代替,原文中使用system做爲模塊加載器,本文中用webpack代替)webpack

建立 一個 ASP.NET Web 項目(若是你的編輯器不是VS.NET,那就直接跳過到配置TypeScript)

雖然安裝了 TypeScript VS.NET 擴展提供了,一個新的 HTML Application with TypeScript 項目模板,可是你最好仍是經過建立一個 Empty ASP.NET Web Application 項目並配置項目支持Typescript -- 這比把它從Typescript轉換成 ASP.NET Web項目要方便的多。.git

新建項目模板

在接下來的界面 選擇 Empty 模板來建立一個空模板:es6

新建空網站

啓用 TypeScript

在項目的右鍵菜單Add > TypeScript File中添加一個 TypeScript File 文件就會自動配置的你Web項目 .csproj 文件,加上一些啓用TypeScript 支持必須的導入項:

新建Typescript文件

配置的時候會彈出對話框:

對話框

點擊 No 來跳過使用Nuget對話框來安裝Typing 定義文件,由於咱們後面會使用typings Package Manager 來代替它安裝定義文件.

配置 TypeScript

在項目中第一激活TypeScript須要配置一些選項。VS.NET 2015 能夠經過項目屬性中的Typescript Build配置節來配置TypeScript的編譯選項,這些信息將直接配置到VS的**.csproj**項目文件中,以下圖所示:

TypeScript Properties Page

不過咱們更傾向於使用tsconfig.json的一個文本文件來配置這些選項,並且這個配置文件能夠更好的適配到其餘的編輯器/IDE中,更利於知識的分享,減小一些沒必要要的問題。

在項目上右鍵Add > New Item 在打開的對話框中搜索 typescript,並選擇 TypeScript JSON Configuration File 文件模板 來添加tsconfig.json 到你的項目中:

add-tsconfig

這會添加一個基礎的tsconfig.json配置文件到你的項目中,這些配置會替換掉你以前在.csproj 項目文件中配置的變量

tsconfig for webpack, React and JSX

爲了更快的進入狀態,你能夠複製下面的配置信息並替換你的tsconfig.json 文件內容:

{

  "compilerOptions": {
    "noImplicitAny": false,
    "noEmitOnError": true,
    "removeComments": false,
    "module": "commonjs",
    "sourceMap": true,
    "target": "es5",
    "jsx": "react",
    "experimentalDecorators": true
  },
  "exclude": [
    "typings",
    "node_modules",
    "wwwroot"
  ]
}

和默認的 tsconfig.json 有所不一樣的是 :

  • target:es5 - 將編譯的javascript設置成es5版本
  • module:commonjs - 使用commonjs做爲模塊加載器(事實上無所謂,咱們最終使用webpack打包)
  • jsx:react - 將 .tsx 文件轉換成 React的 JavaScript 語法,而不是jsx語法。
  • experimentalDecorators:true -啓用ES7裝飾器語法支持,事實上這個語法規則尚未肯定,因此本文中棄用了
  • exclude:node_modules - 排除一些文件夾,不要去編譯這些文件夾下面的typescript代碼。

VS 2013 不支持 tsconfig.json 可是不打緊,咱們最終使用webpack打包代碼,而不是vs自己,因此。。你懂的

安裝 webpack

Webpack 是德國開發者 Tobias Koppers 開發的模塊加載器,它和傳統的commonjs和requirejs的不一樣之處,在於,它在運行時是不須要它自己的,js和其餘一些資源文件(css,圖片等)在運行以前就已經併合併到了一塊兒,而且它的不少插件讓你能夠在作不少預編譯的事情(好比本文中的將typescript編譯成es5版本的javascript)。

事實上,我並不是對它很熟悉,也只是參與了不少的資料,:) 你能夠從下面的這些連接獲取到一些有用的信息:

安裝 webpack自己很是方便,只要使用npm命令全局安裝就能夠了:

C:\proj> npm install webpack -g

等待執行完成便可。

初始化項目

在項目目錄下執行 npm init 爲項目建立一個package.json文件,以便咱們後續安裝一些相關的包到本地

C:\proj> npm init

配置webpack

使用webpack 打包typescript代碼,並編譯成javascript須要安裝一些插件,來安裝一下:

C:\proj> npm install ts-loader source-map-loader --save-dev

初始化項目的文件夾結構,之因此在這裏說,是由於咱們下面的配置文件會使用到對應的目錄地址,建成後的目錄結構如圖所示:

06-folder-list.png

其中 source 目錄用於存放Typescript源代碼文件(本例中爲了路徑引用方便,我將HTML文件也放到裏該目錄下,實際項目中不用這麼作) wwwroot/js 用於存放生成js文件和引用的第三方類庫(jquery,zepto等等) 本例中,我將reactjs的js文件放到wwwroot/js/lib目錄中,並在頁面上單獨引用。

完成後,在項目目錄添加一個webpack.config.js文件,該文件是webpake的配置文件,將如下代碼複製到文件中:

module.exports = {
    entry: "./source/index.ts",
    output: {
        filename: "./wwwroot/js/[name].js",
    },
    // Enable sourcemaps for debugging webpack's output.
    devtool: "source-map",
    resolve: {
        // Add '.ts' and '.tsx' as resolvable extensions.
        extensions: ["", ".webpack.js", ".web.js", ".ts", ".tsx", ".js"]
    },

    module: {
        loaders: [
            // All files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'.
            { test: /\.tsx?$/, loader: "ts-loader" }
        ],

        preLoaders: [
            // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
            { test: /\.js$/, loader: "source-map-loader" }
        ]
    },
    // When importing a module whose path matches one of the following, just
    // assume a corresponding global variable exists and use that instead.
    // This is important because it allows us to avoid bundling all of our
    // dependencies, which allows browsers to cache those libraries between builds.
    externals: {
        "react": "React",
        "react-dom": "ReactDOM"
    }
};

關於webpack.config.js中的詳細說明,可參考官方的說明,其中externals配置節是用於排除,單獨引用的reactjs類庫,不打包進生成的文件,entry入口這裏是示例,在下面的章節會替換成實際的內容。

安裝 React

經過npm 安裝 react到本地,你能夠能夠手動到官網下載最新的版本,並複製到wwwroot/js/lib 目錄下:

C:\proj> npm install react --save

從 v0.14 開始 React 將dom操做分離到一個單獨的包中,咱們也來安裝一下:

C:\proj> npm install react-dom --save

手動將react庫 從node_modules 複製到 wwwroot/js/lib 目錄中,以下圖所示:

06-folder-react

咱們實際使用到的文件是react.min.jsreact-dom.min.js

安裝 typings - TypeScript definitions的管理器

爲了可以開啓Typescript的自動完成和類型檢查支持,咱們須要下載一些第三方類庫的類型定義文件,最好的方式是經過安裝typings 能夠經過npm來全局安裝它:

C:\proj> npm install typings -g

如今咱們能夠經過 typings 命令來安裝咱們須要的TypeScript 類型定義文件了。

Install React Type Definitions

C:\proj> typings install react --ambient --save

Install React DOM Type Definitions

C:\proj> typings install react-dom --ambient --save

The --ambient 標誌是讓 typings 在社區版本中查找 .d.ts TypeScript 定義文件,它們都在DefinitelyTyped --save 標誌是讓這些安裝的信息保存到typings.json配置文件中

安裝完成後你打開文件 typings/browser.d.ts 能夠看到:

/// <reference path="main\ambient\react-dom\react-dom.d.ts" />
/// <reference path="main\ambient\react\react.d.ts" />

在其餘文件中使用這些類型定義文件,只須要引用typings/browser.d.ts文件便可:

/// <reference path='../typings/browser.d.ts'/>

開始 用 TypeScript 編碼了

太棒了! 到這裏咱們終於有了一個能夠工做的Typescript開發環境的,能夠開始編寫TypeScript 和 React代碼,並看看它們是否正常工做,接下來的代碼按照咱們以前的約定,在./source目錄添加你的代碼,好了,咱們從一個最簡單的React 示例開始吧:

Example 1 - HelloWorld

在第一個示例中,咱們要編寫一個最簡單能正常運行的應用,氣死就是 Helloworldsource 目錄下新建 example01/ 文件夾,並添加第一個 TypeScript 文件 :

app.tsx

/// <reference path='../../typings/browser.d.ts'/>

import * as React from "react";
import * as ReactDOM from "react-dom";

class HelloWorld extends React.Component<any, any> {
    render() {
        return <div>Hello, World!</div>;
    }
}
ReactDOM.render(<HelloWorld/>, document.getElementById("content"));

這裏咱們來一塊兒看一看,這個代碼是怎麼運行的:

先來看看第一行代碼:

/// <reference path='../../typings/browser.d.ts'/>

使用了 Reference 標籤來引用全部的以前經過typings安裝的 Definitely Typed 文件

看一下 import 語句:

import * as React from 'react';
import * as ReactDOM from 'react-dom';

導入以前使用 npm 命令安裝的Javascript模塊 (注:Typescript中可使用第三方Javascript庫,可是必須提供類型定義文件,沒有的話須要寫一個), * 號表示導入整個模塊,若是你但願只導入一個模塊的話 ,你能夠這麼寫:

import { render } from 'react-dom';

惟一的例外是在 .tsx文件中,必須導入 React模塊,不然在使用JSX代碼塊時會發生編譯錯誤:

return <div>Hello, World!</div>; //compile error: Cannot find name React

下面的代碼是建立一個組件(component)繼承至 React的Component<TProps,TState>基類:

class HelloWorld extends React.Component<any, any> {

當咱們的組件(Components) 不包含任何特定的屬性( property)和狀態(state)時,咱們可使用 any 類型來忽略一些特殊的類型 When Components doesn't have any properties or state they can use any to ignore specifying types.

咱們以前在TypeScript配置中已經啓用了jsx語法支持,咱們能夠在**.tsx** 使用jsx語法了(注:和配置沒啥關係,配置只是用來編譯生成代碼的,tsx天生就是支持jsx語法的)

render() {
        return <div>Hello, World!</div>;
    }

最後一行是一個標準的React代碼,它意思是在#content DOM 節點中輸出咱們的 HelloWorld 組件(component)的實例:

ReactDOM.render(<HelloWorld/>, document.getElementById("content"));

如今,全部剩下的就是創建一個HTML頁面來容納咱們的剛剛編寫的組件啦(component):

index.html

<!DOCTYPE html>
<html>
<head>
    <title>TypeScript + React + Redux</title>

</head>
<body>
    <h1>Example 2</h1>
    <div id="content"></div>
    <script src="../../wwwroot/js/lib/react/react.min.js"></script>
    <script src="../../wwwroot/js/lib/react/react-dom.min.js"></script>
    <script src="../../wwwroot/js/sample02.js"></script>
</body>
</html>

index.html 是一個在ASP.NET中預約義的默認文檔,用來讓咱們能夠瀏覽以前編寫的組件效果,它被安排在各個示例文件夾中,像我以前說的那樣,並非非要放在這裏,只是爲了更好的組織url。上述的 index.html在目錄/example01/中。

首先,咱們必須引入 react.min.jsreact-dom.min.js 文件,以前有談到過,webpack.config.js配置中設置react自己不被打包,而是單獨引用。一些通用的第三方類庫,爲了更好的使用CDN和緩存,可使用單獨引用的方式,固然也能夠打包在一塊兒,哪一種方式要看實際的狀況。

<script src="../../wwwroot/js/lib/react/react.min.js"></script>
    <script src="../../wwwroot/js/lib/react/react-dom.min.js"></script>

同時咱們引入了一個 sample02.js的文件,這有點奇怪,由於這個文件咱們並無建立,它這時確實也並不存在

<script src="../../wwwroot/js/sample02.js"></script>

這就是咱們接下來要處理的問題,以前咱們講到我會使用 webpack 來 代替默認使用 VS.NET 2015 做爲 Typescript 的編譯器,sample02.js文件實際上是 webpack 自動生成的文件。這個時候它不存在,是由於咱們尚未配置好它,讓咱們從新打開webpack.config.js文件,看看裏面的內容:

module.exports = {
   entry: {
        sample01: "./source/sample01/app.tsx" //將示例1的app.tsx文件做爲入口文件
    },
    output: {
        filename: "./wwwroot/js/[name].js",
    },
    // Enable sourcemaps for debugging webpack's output.
    devtool: "source-map",
    resolve: {
        // Add '.ts' and '.tsx' as resolvable extensions.
        extensions: ["", ".webpack.js", ".web.js", ".ts", ".tsx", ".js"]
    },

    module: {
        loaders: [
            // All files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'.
            { test: /\.tsx?$/, loader: "ts-loader" }
        ],

        preLoaders: [
            // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
            { test: /\.js$/, loader: "source-map-loader" }
        ]
    },
    // When importing a module whose path matches one of the following, just
    // assume a corresponding global variable exists and use that instead.
    // This is important because it allows us to avoid bundling all of our
    // dependencies, which allows browsers to cache those libraries between builds.
    externals: {
        "react": "React",
        "react-dom": "ReactDOM"
    }
};

和以前的內容相比,只修改了 entry 配置節,將 ./source/sample01/app.tsx 做爲一個入口文件,並命名爲 sample01, 而輸出的目錄則是 ./wwwroot/js/ 而且以入口的名字做爲文件名 [name].js,因此在 index.html 我引入的文件 ../../wwwroot/js/sample02.js

entry: {
        sample01: "./source/sample01/app.tsx" //將示例1的app.tsx文件做爲入口文件
    },
    output: {
        filename: "./wwwroot/js/[name].js",
    },

回到 index.html 文件中

最後添加一個 <div/> 空標籤元素,並設置 idcontent 用來輸出React組件。

<div id="content"></div>

如今全部的工做作完後,咱們打開瀏覽器直接訪問/example01/來查看效果了 -- 哈哈,咱們第一個可運行的React應用!

此處輸入圖片的描述

Demo:/typescript-react/example01/

提示: 我使用了 VS.NET 2015 做爲開發工具,因此自帶httpserver ,若是你並非用VS.NET 2015 ,那麼能夠任意的 http server 工具來查看示例。

如使用node 的 http-server包 全局安裝:

C:\proj> npm install http-server -g

而後在項目的根目錄運行:

C:\proj> http-server

打開瀏覽器 查看 :http://localhost:8080/source/sample01/index.html

Example 2 - 模塊化 HelloWorld

在第二個示例中,咱們將嘗試經過移動<HelloWorld />的實現到獨立的文件中來模塊化咱們的應用:

helloworld.tsx

import * as React from "react";

export class HelloWorld extends React.Component<any, any> {
    render() {
        return <div>Hello world!It's from Helloword Component.</div>;
    }
}

爲了讓HelloWorld組件在外部能夠被調用,咱們須要使用 export 關鍵字。咱們一樣可使用 default 關鍵字來定義一個默認導出default export),讓使用者導入的時候更加方便,並能夠重命名稱它們喜歡的名字,而後咱們須要移除在app.tsx中的HelloWorld實現,並用import 新組件的方式代替它:

app.tsx

/ <reference path='../../typings/browser.d.ts'/>

import * as React from "react";
import * as ReactDOM from "react-dom";

import {HelloWorld} from "./helloworld";

ReactDOM.render(<HelloWorld/>, document.getElementById("content"));

若是咱們使用默認導出default export),那麼導入的部分就是這樣的:

import  HelloWorld  from './HelloWorld';

這個示例的改動很是小,咱們來看一下,咱們的程序是否還能正常運行。

注:這裏要注意咱們仍需在webpack.config.js中添加 entry ,後續的示例再也不重複了

entry: {
        sample01: "./source/sample01/app.tsx",
        sample02: "./source/sample02/app.tsx"
    },
    ...

[]

Demo: /typescript-redux/example02/

Example 3 - 建立一個有狀態的組件

如今咱們已是Helloworld界的大師了,應該升級下咱們的遊戲規則,建立一些更高級的有狀態的組件了,畢竟不能100級了,還在新手村。

咱們要作的第一件偉大的事情就是計數器,是的,咱們把示例中的 helloword 文件修改文件名爲 counter 並把內容修改以下:

counter.tsx

import * as React from "react";


export default class Counter extends React.Component<any, any> {

   constructor(props, context) {
        super(props, context);
        this.state = { counter: 0 };
    }
    render() {
        return (
            <div>
                <p>
                    <label>Counter: </label><b>#{this.state.counter}</b>
                </p>
                <button onClick={e => this.incr(1) }>INCREMENT</button>
                <span style={{ padding: "0 5px" }} />
                <button onClick={e => this.incr(-1) }>DECREMENT</button>
            </div>
        );
    }

    incr(by:number) {
        this.setState({ counter: this.state.counter + by });
    }
}

好像沒什麼驚喜,咱們在頁面中添加了一個計數器,經過按鈕 increment / decrement 來改變它的值, 實際使用的是React內置的setState()方法:

Demo: /typescript-redux/example03/

使用 Redux

使用 setState() 是在組件中改變狀態的老辦法了,如今比較流行的是使用 Redux,在使用以前,咱們須要安裝一下:

C:\proj> npm install redux --save

一樣也要安裝它的定義文件 Type Definitions:

C:\proj> typings install redux --ambient --save

Example 4 - 使用 Redux 改造計數器

若是你對Redux 還不太熟悉,如今是開始的時候,下面是一些相關的問題(不過下面的網站在天朝基本都打不開):

這裏推薦兩個中文在線文檔吧,雖然也常常打不開:

Redux 是 JavaScript 狀態容器,提供可預測化的狀態管理。

可讓你構建一致化的應用,運行於不一樣的環境(客戶端、服務器、原生應用),而且易於測試。不只於此,它還提供 超爽的開發體驗,好比有一個時間旅行調試器能夠編輯後實時預覽。

Redux 除了和 React 一塊兒用外,還支持其它界面庫。 它體小精悍(只有2kB)且沒有任何依賴。

如今咱們知道Redux 是什麼了,讓咱們開始改造咱們的計數器:

counter.tsx

import * as React from 'react';
import { createStore } from 'redux';

let store = createStore(
    (state, action) => {
        switch (action.type) {
            case 'INCR':
                return { counter: state.counter + action.by };
            default:
                return state;
        }
    },
    { counter: 0 });

export default class Counter extends React.Component<any, any> {
    private unsubscribe: Function;
    componentDidMount() {
        this.unsubscribe = store.subscribe(() => this.forceUpdate());
    }
    componentWillUnmount() {
        this.unsubscribe();
    }
    render() {
        return (
            <div>
                <p>
                    <label>Counter: </label><b>#{store.getState().counter}</b>
                </p>
                <button onClick={e => store.dispatch({ type:'INCR', by: 1 }) }>INCREMENT</button>
                <span style={{ padding: "0 5px" }} />
                <button onClick={e => store.dispatch({ type:'INCR', by: -1 }) }>DECREMENT</button>
            </div>
        );
    }
}

建立一個 Redux Store

引用redux模塊的 createStore方法,並建立一個Redux store ,並傳遞默認的state:

import { createStore } from 'redux';

let store = createStore(
    (state, action) => {
        switch (action.type) {
            case 'INCR':
                return { counter: state.counter + action.by };
            default:
                return state;
        }
    },
    { counter: 0 });

由於咱們的計數器只有一個Action ,咱們的reducer(Redux中的專有名詞,即處理Action的函數)的實現就比較簡單 - 返回更新的計數器對象

另一件咱們須要知道的關於Redux的事情是Redux是獨立於React的,並不像 setState() 那樣內置在其中的。React並不知道何時你的Redux Store中的State發生了變化--實際上是須要知道的,由於你的組件要知道何時須要重繪。由於這個,咱們須要註冊一個監聽器來觀察 store的state變化來強制觸發組件的重繪:

private unsubscribe: Function;

    componentDidMount() {
        this.unsubscribe = store.subscribe(() => this.forceUpdate());
    }
    componentWillUnmount() {
        this.unsubscribe();
    }

咱們還須要將修改組件經過store.getState() 方法讀取它的state信息,並修改以前的內置方式setState()方法爲觸發一個Action來修改咱們應用的state 。

render() {
        return (
            <div>
                <p>
                    <label>Counter: </label><b>#{store.getState().counter}</b>
                </p>
                <button onClick={e => store.dispatch({ type:'INCR', by: 1 }) }>INCREMENT</button>
                <span style={{ padding: "0 5px" }} />
                <button onClick={e => store.dispatch({ type:'INCR', by: -1 }) }>DECREMENT</button>
            </div>
        );
    }

如今咱們的計數器已經"Redux化"了,從新運行一下示例,並看看和以前的效果是否一致?

Demo: /typescript-redux/example04/

安裝 React Redux

在上一個示例中,咱們在Counter模塊中建立了 Redux store 來幫助咱們優化代碼。由於你的應用應該只有一個Store,因此這不是一個正確使用它的方式 (關於這個原則,你須要參看Redux的相關文檔),咱們使用Redux的React幫助庫來幫咱們改善這種狀況。

事實上,當咱們結合Redux和React的時候,咱們必須安裝的一個包就是react-redux,它一樣能夠經過 npm 方式安裝

C:\proj> npm install react-redux --save

和大多數流行的類庫同樣,它也已經有了類型定義文件了,一塊兒來安裝一下吧:

C:\proj> typings install react-redux --ambient --save

Example 5 - 使用 Provider 注入store到紙容器的上下文(context)中

在這個示例中,咱們將 Redux store 移動到上一層的app.tsx 文件中,就像這樣

app.tsx

import * as React from "react";
import * as ReactDOM from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";

import Counter from "./counter";

let store = createStore(
    (state, action) => {
        switch (action.type) {
            case 'INCR':
                return { counter: state.counter + action.by };
            default:
                return state;
        }
    },
    { counter: 0 });

ReactDOM.render(
    <Provider store={store}>
        <Counter />
    </Provider>,
    document.getElementById("content"));

爲了傳遞store到咱們的組件中,咱們使用了React's child context 特性 , 在 react-redux 封裝了 <Provider/>組件 ,咱們直接使用就能夠了。

爲了讓React知道 咱們但願把store注入到咱們的 Counter 組件中,咱們還須要定義個靜態的contextTypes 屬性 制定context中須要的內容:

counter.tsx

import * as React from 'react';

export default class Counter extends React.Component<any, any> {
    context: any;
    static contextTypes = {
        store: React.PropTypes.object
    }
    private unsubscribe: Function;
    componentDidMount() {
        this.unsubscribe = this.context.store.subscribe(() => this.forceUpdate());
    }
    componentWillUnmount() {
        this.unsubscribe();
    }
    render() {
        return (
            <div>
                <p>
                    <label>Counter: </label><b>#{this.context.store.getState().counter}</b>
                </p>
                <button onClick={e => this.context.store.dispatch({ type:'INCR', by: 1 }) }>INCREMENT</button>
                <span style={{ padding: "0 5px" }} />
                <button onClick={e => this.context.store.dispatch({ type:'INCR', by: -1 }) }>DECREMENT</button>
            </div>
        );
    }
}

改動對頁面沒什麼影響,咱們的程序應該仍是能夠正常運行:

Demo: /typescript-redux/example05/

Example 6 - 使用 connect() 建立無狀態的組件

咱們已經編寫了一些示例程序,如今哦咱們回過頭來從新看看一下。在上一個例子中,咱們看到咱們能夠經過 Provider 組件來傳遞 state到咱們的子組件,react-redux 一樣也提供了一些其餘的方式。

Redux的 connect() 函數返回一個更高級別的組件,它可讓組件變得無狀態(stateless), 經過將state和callback函數映射到組件的屬性上(properties)以下降組件和Redux Store的耦合度:

counter.tsx

import * as React from 'react';
import { connect } from 'react-redux';

class Counter extends React.Component<any, any> {
    render() {
        return (
            <div>
                <p>
                    <label>Counter: </label>
                    <b>#{this.props.counter}</b>
                </p>
                <button onClick={e => this.props.incr() }>INCREMENT</button>
                <span style={{ padding: "0 5px" }} />
                <button onClick={e => this.props.decr() }>DECREMENT</button>
            </div>
        );
    }
}

const mapStateToProps = (state) => state;

const mapDispatchToProps = (dispatch) => ({
    incr: () => {
        dispatch({ type: 'INCR', by: 1 });
    },
    decr: () => {
        dispatch({ type: 'INCR', by: -1 });
    }
});

export default connect(mapStateToProps, mapDispatchToProps)(Counter);

爲了達到這個效果,咱們經過傳遞一個 mapStateToProps 的函數 ,這個函數返回一個對象,這個對象包含組件所須要的全部狀態(state)。 咱們的組件仍然須要更新狀態,因此還須要傳遞一個 mapDispatchToProps 的函數,這個函數經過調用,將組織須要傳遞到Redux action的參數,並觸發對應在store中註冊的Reduce。

Redux的 connect() 會將上述函數組合到一個更高一級的組件中,並訂閱Redux store的變化,經過更新state來改變組件的屬性並重繪(實際上的子組件) Counter 組件

這些修改對頁面來講仍然是透明的,你能夠打開並從新試試它的功能

Demo: /typescript-redux/example06/

安裝 es6-shim

原文中的這個章節是爲了合併對象,安裝es6-shim,並使用其中的 Object.assign() 方法,我從 Object.assign()這裏複製了Polyfill以下,而沒有使用 es6-shim ,以下代碼所示:

if (typeof Object.assign != 'function') {
  (function () {
    Object.assign = function (target) {
      'use strict';
      if (target === undefined || target === null) {
        throw new TypeError('Cannot convert undefined or null to object');
      }

      var output = Object(target);
      for (var index = 1; index < arguments.length; index++) {
        var source = arguments[index];
        if (source !== undefined && source !== null) {
          for (var nextKey in source) {
            if (source.hasOwnProperty(nextKey)) {
              output[nextKey] = source[nextKey];
            }
          }
        }
      }
      return output;
    };
  })();
}

Example 7 - Shape Creator

咱們的下一個例子 咱們將擴展Redux建立一個更大,更高級的真實的應用程序,經過這個例子進一步探索它的好處。由於這個世界不須要另一個TodoMVC應用了,因此我計劃建立另一個形狀生成應用代替,它提供更多的視角去觀察狀態的變化。

Counter.tsx

咱們將開始建立經過計數器(Counter)來控制控件的寬度和高度,爲了達到這效果,須要重構一下咱們的 Counter 組件,定義個field的屬性來肯定應該修改哪一個狀態(width/height),讓其變得更加可複用。另外再增長一個 step 的屬性來控制變化的尺度。

由於咱們要發送多個Action,因此咱們要適修改一下咱們的Action Type名字,這裏咱們使用{Type}_{Event}格式來重命名它們,因此計數器的Action變成了COUNTER_CHANGE

import * as React from 'react';
import { connect } from 'react-redux';

class Counter extends React.Component<any, any> {
    render() {
        var field = this.props.field, step = this.props.step || 1;
        return (
            <div>
                <p>
                    <label>{field}: </label>
                    <b>{this.props.counter}</b>
                </p>
                <button style={{width:30, margin:2}} onClick={e => this.props.decr(field, step)}>-</button>
                <button style={{width:30, margin:2}} onClick={e => this.props.incr(field, step)}>+</button>
            </div>
        );
    }
}

const mapStateToProps = (state, props) => ({ counter: state[props.field] || 0 });

const mapDispatchToProps = (dispatch) => ({
    incr: (field, step) => {
        dispatch({ type: 'COUNTER_CHANGE', field, by: step });
    },
    decr: (field, step) => {
        dispatch({ type: 'COUNTER_CHANGE', field, by: -1 * step });
    }
});

export default connect(mapStateToProps, mapDispatchToProps)(Counter);

如今它變得能夠複用了,咱們能夠建立多個實例來控制咱們形狀的Width和Height了:

<Counter field="width" step={10} />
<Counter field="height" step={10} />

它看上去就像這樣:

colorpicker.tsx

下一個組件咱們須要一個控制顏色的組件,range INPUT 控件很是適合做爲基礎顏色調節器的空間,相似一個滑動條(這個控件在IE的老版本上沒有辦法識別,請你們不要用IE看),同時須要一個顯示顏色的區域,惟一不尋常的事情是須要一個計算顏色亮度的函數用來區分是否顯示白色或者黑色的前臺文本 。

而且<ColorPicker /> 是一個單純的React控件,對Redux沒有任何的依賴,因此稍後咱們須要把它包裝進一個更高級別的組件中:

import * as React from 'react';

export class NumberPicker extends React.Component<any, any> {
    render() {
        return (
            <p>
                <input type="range" value={this.props.value.toString() } min="0" max="255"
                    onChange={e => this.handleChange(e) } />
                <label> {this.props.name}: </label>
                <b>{ this.props.value }</b>
            </p>
        );
    }
    handleChange(event) {
        const e = event.target as HTMLInputElement;
        this.props.onChange(parseInt(e.value));
    }
}

export class ColorPicker extends React.Component<any, any> {
    render() {
        const color = this.props.color;
        const rgb = hexToRgb(color);
        const textColor = isDark(color) ? '#fff' : '#000';

        return (
            <div>
                <NumberPicker name="Red" value={rgb.r} onChange={n => this.updateRed(n)} />
                <NumberPicker name="Green" value={rgb.g} onChange={n => this.updateGreen(n) } />
                <NumberPicker name="Blue" value={rgb.b} onChange={n => this.updateBlue(n) } />
                <div style={{
                    background: color, width: "100%", height: 40, lineHeight: "40px",
                    textAlign: "center", color: textColor
                }}>
                    {color}
                </div>
            </div>
        );
    }
    updateRed(n: number) {
        const rgb = hexToRgb(this.props.color);
        this.changeColor(rgbToHex(n, rgb.g, rgb.b));
    }
    updateGreen(n: number) {
        const rgb = hexToRgb(this.props.color);
        this.changeColor(rgbToHex(rgb.r, n, rgb.b));
    }
    updateBlue(n: number) {
        const rgb = hexToRgb(this.props.color);
        this.changeColor(rgbToHex(rgb.r, rgb.g, n));
    }
    changeColor(color: string) {
        this.props.onChange(color);
    }
}

const componentToHex = (c) => {
    const hex = c.toString(16);
    return hex.length == 1 ? "0" + hex : hex;
};

const rgbToHex = (r, g, b) => "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);

const hexToRgb = (hex: string): { r: number; g: number; b: number; } => {
    const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
    } : null;
};

const luminance = (color: string) => {
    const rgb = hexToRgb(color);
    return 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b;
};

export const isDark = (color: string) => luminance(color) < 100;

在界面上看起來像這樣:

shapemaker.tsx

這就是咱們的Shape 生成器,引入了更多的狀態,咱們將關注 topleft 的位置來肯定他在預覽區域顯示的位置,同時控制它的顏色和大小,同時也很是重要的是咱們加了一個 Add Shape 的按鈕,把Shape添加到咱們的預覽區域中,詳細的代碼以下: :

import * as React from "react";
import { connect } from "react-redux";
import { isDark } from "./colorpicker";

class ShapeMaker extends React.Component<any, any> {
    constructor(props?, context?) {
        super(props, context);
        this.state = { top: props.top, left: props.left };
    }
    render() {
        var width = this.props.width, height = this.props.height, background = this.props.color;
        const color = isDark(background) ? '#fff' : '#000';
        return (
            <div>
                <p>
                    <label>size: </label>
                    <b>{height}x{width}</b>
                </p>
                <div style={{ height, width, background, color, lineHeight: height + "px", margin: "auto" }}>
                    ({this.state.top}, {this.state.left})
                </div>
                <div>
                    <p>
                        <label>position: </label>
                        <input style={{ width: 30 }} defaultValue={this.props.top} onChange={e => this.handleTop(e) } />
                        <span>, </span>
                        <input style={{ width: 30 }} defaultValue={this.props.left} onChange={e => this.handleLeft(e) } />
                    </p>

                    <button onClick={e => this.props.addShape(background, height, width, this.state.top, this.state.left) }>
                        Add Shape
                    </button>
                </div>
            </div>
        );
    }
    handleTop(e) {
        var top = parseInt(e.target.value);
        if (!isNaN(top))
            this.setState({ top });
    }
    handleLeft(e) {
        var left = parseInt(e.target.value);
        if (!isNaN(left))
            this.setState({ left });
    }
}

export default connect(
    (state) => ({
        width: state.width, height: state.height, color: state.color,
        top: state.nextShapeId * 10, left: state.nextShapeId * 10
    }),
    (dispatch) => ({
        addShape: (color, height, width, top, left) => {
            dispatch({ type: 'SHAPE_ADD', height, width, color, top, left });
        }
    })
)(ShapeMaker);

在界面上看起來像這樣:

基於消息的Actions

有趣的是,儘管這個組件的改動部分貌似不少,不過它僅僅觸發一個單獨的SHAPE_ADD Action。 咱們開始看到一些使用Redux方式的好處,像強制分離咱們功能背後的粗粒度API ,去除和DOM的關係。 這樣只要能操做Store就能操做應用程序的的具體功能,這都要感謝基於消息的設計。 由於它們只是基本的javascript對象,咱們能夠很是輕鬆的建立並序列化100個 SHAPE_ADD actions,而且把它們存儲到localStorage等以便咱們後續重置,甚至通知到別人,再在其本地重現過程。

ShapeViewer.tsx

如今我已經有了建立一個Shape的全部部分,咱們還須要一個組件去顯示他們,ShapeViewer 經過輸出一個 DIV顯示添加Shape的大小,顏色和未知。

import * as React from "react";
import { connect } from "react-redux";
import { isDark } from "./colorpicker";

class ShapeMaker extends React.Component<any, any> {
    constructor(props?, context?) {
        super(props, context);
        this.state = { top: props.top, left: props.left };
    }
    render() {
        var width = this.props.width, height = this.props.height, background = this.props.color;
        const color = isDark(background) ? '#fff' : '#000';
        return (
            <div>
                <p>
                    <label>size: </label>
                    <b>{height}x{width}</b>
                </p>
                <div style={{ height, width, background, color, lineHeight: height + "px", margin: "auto" }}>
                    ({this.state.top}, {this.state.left})
                </div>
                <div>
                    <p>
                        <label>position: </label>
                        <input style={{ width: 30 }} defaultValue={this.props.top} onChange={e => this.handleTop(e) } />
                        <span>, </span>
                        <input style={{ width: 30 }} defaultValue={this.props.left} onChange={e => this.handleLeft(e) } />
                    </p>

                    <button onClick={e => this.props.addShape(background, height, width, this.state.top, this.state.left) }>
                        Add Shape
                    </button>
                </div>
            </div>
        );
    }
    handleTop(e) {
        var top = parseInt(e.target.value);
        if (!isNaN(top))
            this.setState({ top });
    }
    handleLeft(e) {
        var left = parseInt(e.target.value);
        if (!isNaN(left))
            this.setState({ left });
    }
}

export default connect(
    (state) => ({
        width: state.width, height: state.height, color: state.color,
        top: state.nextShapeId * 10, left: state.nextShapeId * 10
    }),
    (dispatch) => ({
        addShape: (color, height, width, top, left) => {
            dispatch({ type: 'SHAPE_ADD', height, width, color, top, left });
        }
    })
)(ShapeMaker);

當添加一個Shape,ShapeViewer就會一個空DIV容器中繪製它。

拖拽 shapes 來產生Actions

爲了更好的查看全部的Shape,ShapeViewer 包括了支持拖拽來更新shape的位置的功能,這也是一個快速產生大量Action的一個方法,能夠快速的可視化並播放一系列state的變化

爲了簡單起見,使用了mousemove事件而不是drag drop 的api云云

ActionPlayer.tsx

如今咱們已經實現了咱們App的全部功能,能夠開始Redux的特別功能

replayActions

若是你已經正確的完成咱們的應用,我理論上能夠經過重置Redux的Store到它的默認狀態,而後再次觸發以前的每個Action來重現咱們整個應用的會話。這就是 replayActions 要實現的功能。每一個Action咱們提供10毫秒*第幾個的間隔來播放。

import * as React from "react";

export default class ActionPlayer extends React.Component<any, any> {
    private unsubscribe: Function;
    componentDidMount() {
        this.unsubscribe = this.props.store.subscribe(() => this.forceUpdate());
    }
    componentWillUnmount() {
        this.unsubscribe();
    }
    render() {
        return (
            <div>
                <button onClick={e => this.replayActions() }>replay</button>
                <p>
                    <b>{this.props.actions.length}</b> actions
                </p>
                <button onClick={e => this.undoAction() }>undo</button> <span></span>
                <button onClick={e => this.resetState() }>clear</button>
            </div>
        );
    }
    resetState() {
        this.props.store.dispatch({ type: "LOAD", state: this.props.defaultState });
        this.props.actions.length = 0;
    }
    replayActions() {
        const snapshot = this.props.actions.slice(0);
        this.resetState();

        snapshot.forEach((action, i) =>
            setTimeout(() => this.props.store.dispatch(action), 10 * i));
    }
    undoAction() {
        const snapshot = this.props.actions.slice(0, this.props.actions.length - 1);
        this.resetState();
        snapshot.forEach(action => this.props.store.dispatch(action));
    }
}

ActionPlayer 也顯示有多少個Action被觸發了:

resetState

清空咱們的應用回到最原始的狀態不能再簡單了,就只有從新加載defaultState並清除保存的actions。

undoAction

由於咱們的應用捕獲了全部的Action,撤銷的動做,咱們是經過將以前的全部Action從新再執行一遍,但不包括最後一個的方式來實現。這看上去效率很低,不過Javascript的VM 性能還不錯,因此看上去也還好--就像咱們真的實現了撤銷同樣:)

app.tsx

在實現了全部的模塊以後,剩下的一件事情就是咱們父容器了,用於掛載全部的組件和Redux Reducer函數,實現全部action的狀態轉換

Application Reducers

經過switch狀態來處理每一個action的type屬性是實現reducer函數的經典方式。不能用對象展開方法,使用Object.assign() 也許是最好的方式了,可是它是ES6的特性,並不是全部的瀏覽器都支持。爲此咱們以前有介紹安裝工具包或者使用polyfill來實現,在本文的示例中使用了polyfill。 咱們在reducer中處理state的變化,使用 Object.assign() 新建了一個副本。但不能這樣使用 Object.assign(state, { visibilityFilter: action.filter }),由於它會改變第一個參數的值。你必須把第一個參數設置爲空對象{}

咱們看到咱們不須要一個特殊的Redux函數去作這個事情,咱們經過在action消息中傳遞一個簡單的參數,就能輕易地讓咱們的reducer函數返回指望的state。

import * as React from "react";
import * as ReactDOM from "react-dom";
import { createStore } from "redux";
import { Provider, connect } from "react-redux";

import Counter from "./counter";
import ActionPlayer from "./actionplayer";
import ShapeMaker from "./shapemaker";
import ShapeViewer from "./shapeviewer";
import { ColorPicker } from "./colorpicker";

import "./objectassign";

var actions = [];
var defaultState = { nextShapeId: 0, width: 100, height: 100, color: "#000000", shapes: [] };

let store = createStore(
    (state, action) => {
        actions.push(action);
        let shape;
        switch (action.type) {
            case "COUNTER_CHANGE":
                return Object.assign({}, state, { [action.field]: state[action.field] + action.by });
            case "COLOR_CHANGE":
                return Object.assign({}, state, { color: action.color });
            case "SHAPE_ADD":
                const id = state.nextShapeId;
                shape = Object.assign({}, { id: id }, action);
                delete shape["type"];
                return Object.assign({}, state, { nextShapeId: id + 1, shapes: [...state.shapes, shape] });
            case "SHAPE_CHANGE":
                shape = Object.assign({}, state.shapes.filter(x => x.id === action.id)[0],
                    { top: action.top, left: action.left });
                return Object.assign({}, state,
                    { shapes: [...state.shapes.filter(x => x.id !== action.id), shape] });
            case "LOAD":
                return action.state;
            default:
                return state;
        }
    },
    defaultState);

class ColorWrapperBase extends React.Component<any, any> {
    render() {
        return <ColorPicker color={this.props.color} onChange={this.props.setColor} />;
    }
}

const ColorWrapper = connect(
    (state) => ({ color: state.color }),
    (dispatch) => ({ setColor: (color) => dispatch({ type: 'COLOR_CHANGE', color }) })
)(ColorWrapperBase);

ReactDOM.render(
    <Provider store={store}>
        <table>
            <tbody>
                <tr>
                    <td style={{ width: 220 }}>
                        <Counter field="width" step={10} />
                        <Counter field="height" step={10} />
                        <ColorWrapper />
                    </td>
                    <td style={{ verticalAlign: "top", textAlign: "center", width: 500 }}>
                        <h2>Preview</h2>
                        <ShapeMaker />
                    </td>
                    <td style={{ verticalAlign: 'bottom' }}>
                        <ActionPlayer store={store} actions={actions} defaultState={defaultState} />
                    </td>
                </tr>
                <tr>
                    <td colSpan={3}>
                        <h2 style={{ margin: 5, textAlign: 'center' }}>Shapes</h2>
                        <ShapeViewer />
                    </td>
                </tr>
            </tbody>
        </table>
    </Provider>,
    document.getElementById("content"));

如今咱們能夠看到一個可工做的Shape生成器了,這是它的全貌:

Demo: /typescript-redux/example07/

有一點須要指出咱們的頂級App只會繪製一次,由於它不包含在任何一個父組件,也沒有調用setState()來改變state並觸發重繪。因此咱們須要包裝一下咱們的ColorPicker進一個Redux-aware ColorWrapper ,而且映射咱們的Redux state到它的組件屬性中,一樣把onChange 轉會成觸發適當的Redux action

重構 Reducers

原文中重構Reducers是使用了Typescript的一些高級特性,如裝飾器等,可是我自己是拒絕這麼作的,畢竟裝飾器仍是實驗性的特性,未來會發生變化, 這裏的重構只是從新組織一下代碼,並將原來的一堆switch拆分到不一樣的reducer函數中:

let store = createStore(
    (state, action) => {
        actions.push(action);
        let shape;
        switch (action.type) {
            case "COUNTER_CHANGE":
                return Object.assign({}, state, { [action.field]: state[action.field] + action.by });
            case "COLOR_CHANGE":
                return Object.assign({}, state, { color: action.color });
            case "SHAPE_ADD":
                const id = state.nextShapeId;
                shape = Object.assign({}, { id: id }, action);
                delete shape["type"];
                return Object.assign({}, state, { nextShapeId: id + 1, shapes: [...state.shapes, shape] });
            case "SHAPE_CHANGE":
                shape = Object.assign({}, state.shapes.filter(x => x.id === action.id)[0],
                    { top: action.top, left: action.left });
                return Object.assign({}, state,
                    { shapes: [...state.shapes.filter(x => x.id !== action.id), shape] });
            case "LOAD":
                return action.state;
            default:
                return state;
        }
    },
    defaultState);

這裏沒有使用Redux內置的 combineReducers來幫助咱們模塊化Reduxer, 仍是使用一種字典的方式來組織咱們的函數,這樣作我相信更加可閱讀性和可擴展性更高。(事實上本例中使用combineReducers 會帶來一些額外的問題,由於combineReducers中的Reducer分管state中的不一樣分支而互不影響,而此例中有些模塊須要交互方式可能致使代碼必定的冗餘)

app.tsx:

import reducers from './reducers';
...

let store = createStore(
    (state, action) => {
        var reducer = reducers[action.type];
        var nextState = reducer != null
            ? reducer(state, action)
            : state;

        if (action.type !== 'LOAD')
            history.add(action, nextState);

        return nextState;
    },
    defaultState);

reducers.ts

reducers 模塊就是返回一個action.type爲key的字典對象,內容則是它們對應的處理函數 :

import "./objectassign";

import { addShape, changeShape } from './reducers/shapeReducers';

const changeCounter = (state, action) =>
    Object.assign({}, state, { [action.field]: state[action.field] + action.by });

const changeColor = (state, action) =>
    Object.assign({}, state, { color: action.color });

export default {
    COUNTER_CHANGE: changeCounter,
    COLOR_CHANGE: changeColor,
    SHAPE_ADD: addShape,
    SHAPE_CHANGE: changeShape,
    LOAD: (state, action) => action.state
};

使用命名函數讓代碼更具可讀性而且讓你獨立地開發和測試每一個reducer的實現。 同時也講幾個相關的reducer封裝進單獨的模塊中,就像這樣:

shapeReducers.ts

import "../objectassign";

export const addShape = (state, action) => {
    var id = state.nextShapeId;
    var shape = Object.assign({}, { id: id }, action);
    delete shape['type'];
    return Object.assign({}, state, { nextShapeId: id + 1, shapes: [...state.shapes, shape] });
};

export const changeShape = (state, action) => {
    var shape = Object.assign({}, state.shapes.filter(x => x.id === action.id)[0],
        { top: action.top, left: action.left });
    return Object.assign({}, state, { shapes: [...state.shapes.filter(x => x.id !== action.id), shape] });
};

重構 Redux 的組件

這裏還有一些事情須要咱們去改進Redux-connected的組件,這些組件使用了connect()來建立咱們更高級別的(實際上是子類)Redux-connected 組件:

class ColorWrapperBase extends React.Component<any, any> {
    render() {
        return <ColorPicker color={this.props.color} onChange={this.props.setColor} />;
    }
}

export const ColorWrapper = connect(
    (state) => ({ color: state.color }),
    (dispatch) => ({ setColor: (color) => dispatch({ type: "COLOR_CHANGE", color }) })
)(ColorWrapperBase);

注:原文中做者說不喜歡經過這種方式(離開組件類聲明,去改變組件的行爲)來改變原來組件的實現,而後再接下來的章節中,敘述若是經過Typescript的裝飾器功能來改造,可是我暫時並不一樣意這種觀點,在Typescript中裝飾器自己是比較晦澀的語法糖,並且還只是實驗性的功能特性,在咱們尚未很是熟練掌握的狀況下,仍是應該謹慎處理,並且我還以爲使用connect()的方式,並無什麼不妥。若是你對裝飾器的部分很是感興趣,能夠去原文看看怎麼實現的。

帶有語法綁定的方法

在React Apps中 PureRenderMixin經過檢查和監聽組件的props或state的變動來阻止一些沒必要要的重繪,這樣能有效地改善應用程序的性能。

順便說一句,Redux connect()的方法自動就可以判斷是否須要更新組件,經過映射對象關聯比較和判斷狀態是否發生了變化。

經過fat arrow syntax(箭頭函數語法))你能夠輕鬆的綁定javascript中的this對象到函數中,並且不須要考慮怎麼綁定:

export class NumberPicker extends React.Component<any, any> {
    render() {
        return (
            <p>
                <input type="range" value={this.props.value.toString()} min="0" max="255"
                    onChange={e => this.handleChange(e)} /> //new function created
                <label> {this.props.name}: </label>
                <b>{ this.props.value }</b>
            </p>
        );
    }
    handleChange(event) {
        const e = event.target as HTMLInputElement;
        this.props.onChange(parseInt(e.value));
    }
}

這樣寫的問題是相似在循環中聲明函數同樣,一旦屬性無效後,箭頭函數的聲明就會被從新執行一次,這樣就會有些性能方面的問題,一個簡單地方式來改善這個問題,只須要這樣定義你的箭頭函數:

export class NumberPicker extends React.Component<INumberProps, any> {
    render() {
        return (
            <p>
                <input type="range" value={this.props.value.toString()} min="0" max="255"
                    onChange={this.handleChange} /> //uses same function
                <label> {this.props.name}: </label>
                <b>{this.props.value}</b>
            </p>
        );
    }
    handleChange = (event) => { //fat arrow syntax
        const e = event.target as HTMLInputElement;
        this.props.onChange(parseInt(e.value));
    }
}

Example 8 - 經過狀態快照實現時間旅行

在這個實例中咱們講經過一個應用State快照更加完整實現替換ActionPlayer。經過使用state咱們能夠實現更加豐富的歷史功能,包括回退,前進,和指定某個時間點的導航,就像咱們控制進度條同樣進行一次『實現旅行』體驗模擬後退和前進

爲了讓歷史狀態管理更加可服用一點,它被封裝成一個類似的API,包括基本的操做包括導航、重設,添加如今的狀態等等:

app.tsx

var history = {
    states: [],
    stateIndex: 0,
    reset() {
        this.states = [];
        this.stateIndex = -1;
    },
    prev() { return this.states[--this.stateIndex]; },
    next() { return this.states[++this.stateIndex]; },
    goTo(index) { return this.states[this.stateIndex=index]; },
    canPrev() { return this.stateIndex <= 0; },
    canNext() { return this.stateIndex >= this.states.length - 1; },
    pushState(nextState) {
        this.states.push(nextState);
        this.stateIndex = this.states.length - 1;
    }
};

let store = createStore(
    (state, action) => {
        var reducer = reducers[action.type];
        var nextState = reducer != null
            ? reducer(state, action)
            : state;

        if (action.type !== 'LOAD')
         {
             history.pushState(nextState);
             console.log(history);
         }

        return nextState;
    },
    defaultState);

History.tsx

經過保存和重置整個狀態快照來實現咱們的歷史控制器,這很是的直接了當,基本上經過觸發 LOAD action 來銷燬以前保存的全部state,(注:這裏沒有像原文中使用裝飾器,而是傳統的方式,這組件有個問題,就是自己的數據並非state的部分,可是又須要再state變化的時候,同步刷新,因此須要訂閱store的變化來強制刷新):

import * as React from 'react';


// History 自己的數據不禁 State 管理,可是又要在State變化的時候重繪
export default class History extends React.Component<any, any> {
    context: any;
    static contextTypes = {
        store: React.PropTypes.object
    }
    private unsubscribe: Function;
    componentDidMount() {
        this.unsubscribe = this.context.store.subscribe(() => this.forceUpdate());
    }
    componentWillUnmount() {
        this.unsubscribe();
    }

    render() {
        return (
            <div>
                <button onClick={this.replayStates}>replay</button>
                <span> </span>
                <button onClick={this.resetState}>clear</button>
                <p>
                    <b>{this.props.history.states.length}</b> states
                </p>
                <button onClick={this.prevState} disabled={this.props.history.canPrev()}>prev</button>
                <span> </span>
                <button onClick={this.nextState} disabled={this.props.history.canNext()}>next</button>
                <p>
                    <b>{this.props.history.stateIndex + 1}</b> position
                </p>
                <input type="range" min="0" max={this.props.history.states.length - 1}
                    disabled={this.props.history.states.length === 0}
                    value={this.props.history.stateIndex} onChange={this.goToState} />
            </div>
        );
    }
    resetState = () => {
        this.context.store.dispatch({ type: 'LOAD', state: this.props.defaultState });
        this.props.history.reset();
    }
    replayStates = () => {
        this.props.history.states.forEach((state, i) =>
            setTimeout(() => this.context.store.dispatch({ type: 'LOAD', state }), 10 * i));
    }
    prevState = () => {
        this.context.store.dispatch({ type: 'LOAD', state: this.props.history.prev() });
    }
    nextState = () => {
        this.context.store.dispatch({ type: 'LOAD', state: this.props.history.next() });
    }
    goToState = (event) => {
        const e = event.target as HTMLInputElement;
        this.context.store.dispatch({ type: 'LOAD', state: this.props.history.goTo(parseInt(e.value)) });
    }
}

如今咱們的實例已經支持豐富的歷史功能了:)

Demo: /typescript-redux/example08/

改進撤銷功能

若是正在給你的Redux應用程序添加撤銷/重作功能,你確定指望可以回退獨立組件的變化的部分,而不是整個應用的state,很是幸運 這篇文章:redux docs have you covered 利用 Elm的undo-redo package編寫了一個撤銷的實例。

注:原文中,做者還提供了一個多個客戶端交互的示例程序,由於涉及到服務端的編碼,主要是StackService自己(.NET的一個webapi框架)的實現,因此省略了,後續有空我再編寫一個Nodejs實現的部分吧,若是機會的話。。我相信你懂的 ,或者能夠先看下原文吧。

相關文章
相關標籤/搜索