前言
由于鄙人最近在做項目用到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上面的人撕逼的內容:
- Decorator is not supported!——撕逼1
- Easily Add an Babel Plugin——撕逼2
- why we don’t include them——撕逼3
- you don’t need decorators——撕逼
- the good time to have decorators now——撕逼4
大概內容這樣吧:
撕逼精選:
- 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
個人觀點
- 確實decorators是個語法糖,但是不可否認decorators可以把復雜的代碼變得清晰干凈。因此我是很喜歡decorator(畢竟我是古老個javaer)。
- create-react-app的作者說道,他很開心看到有支持decorators的plugin,但是認為不太穩定、不太標準而且有decorators的替代品,所以先暫且不進行這方面的優化。
- 盡管現在有官方支持了,但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