Mobx-React的TodoMVC

前言

由于鄙人最近在做項目用到React做UI和Mobx做數據控制處理。所以我就記錄一下學習的東西。今天就分享一下自己做(抄)的一個小東西,那就是Todo List。

環境如下:

  • System: Ubuntu
  • node version: v8.0.0
  • npm version: 5.3.0
  • create-react-app version: 1.3.3

建立項目

我們用create-react-app my-app創建自己的項目,并進入自己的項目啦

create-react-app my-app
cd my-app

接下來安裝mobx、mobx-react和一些組件

npm install mobx-react mobx --save
npm install mobx-react-devtools --save-dev
npm install direcctor --save

npm install todomvc-app-css todomvc-common --save

然后就:

npm start

居然成功了!太神奇啦!還以為會失敗呢!

使用Decorator or not

首先呢,我們來看看github上面的人撕逼的內容:

大概內容這樣吧:

撕逼精選:

  • if you use MobX or a similar library, you don’t need decorators. They are just syntax sugar in this case.
  • Since we don’t currently use decorators, we don’t take it upon ourselves to provide a migration path if the standard becomes incompatible.
  • We’re happy to support decorators after the spec advances, and they are officially supported in Babel without the -legacy plugin. And MobX works fine without decorators.
  • babel seems to now be off the -legacy plugin for 7.0 babel-plugin-transform-decorators

個人觀點

  1. 確實decorators是個語法糖,但是不可否認decorators可以把復雜的代碼變得清晰干凈。因此我是很喜歡decorator(畢竟我是古老個javaer)。
  2. create-react-app的作者說道,他很開心看到有支持decorators的plugin,但是認為不太穩定、不太標準而且有decorators的替代品,所以先暫且不進行這方面的優化。
  3. 盡管現在有官方支持了,但create-react-app的還沒動靜。

綜上所述

我暫且不在create-react-app的項目里面不用decorator,但假如你很想用的話,我推薦你看這篇文章How to get decorators in create-react-app(親測可用)。

看完這個大家怎么想呢?無奈跟我一起做項目的朋友用了create-react-app建立項目,所以就出現這種尷尬的情況。


開始開發

1. Models

在src目錄下創建一個models的目錄,并創建TodoModels.js的文件。直接貼代碼:

import { extendObservable } from 'mobx';

export default class TodoModel {
    store;
    id;
    title;
    completed;

    constructor(store, id, title, completed) {
        this.store = store;
        this.id = id;
        extendObservable(this, {
            title: title,
            completed: completed
        });
    }

    toggle() {
        this.completed = !this.completed;
    }

    destroy() {
        this.store.todos.remove(this);
    }

    setTitle(title) {
        this.title = title;
    }

    toJS() {
        return {
            id: this.id,
            title: this.title,
            completed: this.completed
        };
    }

    static fromJS(store, object) {
        return new TodoModel(store, object.id, object.title, object.completed);
    }
}

這里這特別的地方就是constructor()方法里面的內容啦。里面使用了extendObservable(),它跟ES當中的Object.assign很相似,也是把對象復制給target。但不同的就是這個observable了。從字面上可以理解,給target對象分配一個observable的屬性變量。像代碼里面,我把title: title分配給this這個對象,并且title變成一個observable的變量。而一開始聲明的屬性變量都是為了讓大家清晰該類型有哪些屬性。當然你也可以不加啦。

2. store

目錄src/store

TodoStore.js

...
    constructor(props) {
        extendObservable(this, {
            todos: [],
            get activeTodoCount() {
                return this.todos.reduce(
                    (sum, todo) => sum + (todo.completed ? 0 : 1), 0
                )
            }
        });
    };

    subscribeServerToStore() {
        reaction(
            () => this.toJS(),
            todos => window.fetch && fetch('/api/todos', {
                method: 'post',
                body: JSON.stringify({ todos }),
                header: new Headers({ 'Content-Type': 'application/json'})
            })
        );
    }

    subscribeLocalstorageToStore() {
        reaction(
            () => this.toJS(),
            todos => localStorage.setItem('mobx-react-todomvc-todos', JSON.stringify({todos}))
        );
    }

...

這里特別的地方就是get activeTodoCount(){...}這個getter也變成一個computed的東西,還有reaction()用于產生副作用的東西,他第一個參數是產生數據的函數并且產生數據作為第二個函數輸入參數,第二個參數是就是side effect啦。要注意的是里面執行的副作用訪問的任何 observable 都不會被追蹤。看來這個函數不是我們想要的action啊(可能這個項目還沒有用到action)。遲點再抄一遍contactlist的demo吧。

3. component

目錄src/component

TodoItem.js

import React from 'react';
import PropTypes from 'prop-types';
import {observer} from 'mobx-react';
import { expr, extendObservable } from 'mobx';

const ESCAPE_KEY = 27;
const ENTER_KEY = 13;

class TodoItem extends React.Component {
    editText;

    constructor(props) {
        super(props);
        extendObservable(this, {
            editText: ""
        });
    }

    render() {
        const {viewStore, todo} = this.props;
        return (
            <li className={[
                    todo.completed ? "completed" : "",
                    expr(() => todo === viewStore.todoBeingEdited ? "editing" : "")
                ].join(" ")} >
                <div className="view">
                    <input
                        className="toggle"
                        type="checkbox"
                        checked={todo.completed}
                        onChange={this.handleToggle}
                    />
                    <label onDoubleClick={this.handleEdit} >
                        {todo.title}
                    </label>
                    <button className="destroy" onClick={this.handleDestroy} />
                </div>
                <input
                    ref="editField"
                    className="edit"
                    value={this.editText}
                    onBlur={this.handleSubmit}
                    onChange={this.handleChange}
                    onKeyDown={this.handleKeyDown}
                />
            </li>
        )
    }

    handleSubmit = (event) => {
        const val = this.editText.trim();
        if (val) {
            this.props.todo.setTitle(val);
            this.editText = val;
        } else {
            this.handleDestroy();
        }
        this.props.viewStore.todoBeingEdited = null;
    };

    handleDestroy = () => {
        this.props.todo.destroy();
        this.props.viewStore.todoBeingEdited = null;
    };

    handleEdit = () => {
        const todo = this.props.todo;
        this.props.viewStore.todoBeingEdited = todo;
        this.editText = todo.title;
    };

    handleKeyDown = (event) => {
        if (event.which === ESCAPE_KEY) {
            this.editText = this.props.todo.title;
            this.props.viewStore.todoBeingEdited = null;
        } else if (event.which === ENTER_KEY) {
            this.handleSubmit(event);
        }
    };

    handleChange = (event) => {
        this.editText = event.target.value;
    };

    handleToggle = () => {
        this.props.todo.toggle();
    };
}

TodoItem.propTypes = {
    todo: PropTypes.object.isRequired,
    viewStore: PropTypes.object.isRequired
};

export default observer(TodoItem);

這里又多了個observer的內容啦。observer函數可以用來將 React 組件轉變成響應式組件。這里所說的響應式是指數據響應,而不是頁面啊哈。簡單來說:所有渲染 observable 數據的組件。 像這里我們使用了viewStore和todo兩個有observable的變量,我們就必須使用observer否則在數據發生變化后,重新刷新組件。那有人說:我可以在最頂層定義observer啊,多么干凈利落啊。很遺憾這樣做是不行的。

當 observer 需要組合其它裝飾器或高階組件時,請確保 observer 是最深處(第一個應用)的裝飾器,否則它可能什么都不做。

TodoApp.js

class TodoApp extends React.Component {
    render() {
        const {todoStore, viewStore} = this.props;
        return (
            <div>
                <DevTool />
                <header className="header">
                    <h1>todos</h1>
                    <TodoEntry todoStore={todoStore} />
                    <TodoOverview todoStore={todoStore} viewStore={viewStore} />
                    <TodoFooter todoStore={todoStore} viewStore={viewStore} />
                </header>

            </div>
        )
    }

    componentDidMount() {
        var { Router } = require('director/build/director');
        var viewStore = this.props.viewStore;
        var router = Router({
            '/': function() { viewStore.todoFilter = ALL_TODOS; },
            '/active': function() { viewStore.todoFilter = ACTIVE_TODOS; },
            '/completed': function() { viewStore.todoFilter = COMPLETED_TODOS; }
        });
        router.init('/');
    }

...

export default observer(TodoApp);

這里我們通過路徑來修改viewStore.todoFilter,進而對數據響應處理和組件刷新。這里加入了一個很有趣的devTool,看起來挺酷炫的開發組件。


以上是我通過todo mvc學到的一些內容和感悟。希望大家喜歡。
源碼地址:https://github.com/Salon-sai/learning-mobx-react/tree/master/TodoList

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,762評論 25 708
  • afinalAfinal是一個android的ioc,orm框架 https://github.com/yangf...
    passiontim閱讀 15,486評論 2 45
  • 練習重點:自我檢查 1.觀察念頭、想法、情緒 大部分時候能夠覺察到念頭、想法和情緒,理性思考之后再處理,最近沒有出...
    EmmaJW閱讀 270評論 0 0
  • 行兮,走兮, 獨赴離亭。 倚秋風, 白柏零落兮。 斑駁雨跡, 云低傘平, 黃葉蕭蕭兮。 別路荒草, 萋萋遠兮。 步...
    馮識侜閱讀 357評論 2 4
  • 剛剛從小時念念不忘的“我想去桂林”的象鼻山歸來,也才剛剛回味完“黃山歸來不看岳”那種滿足情感……轉眼間,不虛...
    邱禮賢閱讀 407評論 0 1