目錄
todolist項目:
todo,待辦;
人機交互,實質就是增刪改查;
前端MVC框架、後端MVC框架;
注:
開發環境用:npm + webpack + babel;
部署是另一套東西;
國際化:
中、英兩版;
複雜的網頁只作字符串轉換並不合適;
antd中的國際化;
瀑布式開發設計(傳統,需求調研,各種設計,時間週期長);
敏捷開發(快速迭代);
異步提交;部分刷新;事件通知機制;重繪DOM樹;
antd:
ant design,螞蟻金服開源的React UI庫;
https://ant.design/index-cn
https://ant.design/docs/react/introduce-cn
用戶提交,需要表單控件,對於React來說也是一個組件,但這些組件需要用戶看到,爲了美觀引入antd;
$ npm i antd --save
……
added 68 packages and updated 1 package in 44.219s
需求分析:
分層:
視圖層,負責顯示數據,每一個React組件一個xxx.js文件;
服務層,負責業務數據處理邏輯,命名爲xxxService.js文件;
Model層,負責數據,用Local Storage代替;
文件均在項目根下的./src/裏;
注:
服務層要與視圖層劃分清楚,這比較麻煩;
階段1:
服務層實現:
./src/service.js
JS不支持靜態變量,但支持靜態方法;
但在babel下可用static定義靜態變量;
import store from 'store';
export default class TodoService { //與組件無關,是類,大駝峯
static NAMESPACE = 'todo::'; //前綴,查看https://www.zhihu.com/的Application中Local Storage,相同類型的用同一個前綴,易於區分;看業務情況分段,一般最多2段即可,否則代碼複雜
todos = []; //用數組存儲待辦事宜,是在內存中,刷新網頁後會清空,所以必須存在Local Storage中
create(title) { //提供create方法,創建todo,把數據存儲到Local Storage上(Local Storage當作是後臺數據庫,持久化用),同時插入到todos數組中;title從browser端來,是用戶提交的輸入,在視圖層提供用戶提交的文本框
console.log('service');
const todo = {
key: TodoService.NAMESPACE + (new Date()).valueOf(), //毫秒時間戳
title: title,
completed: false
};
this.todos.push(todo);
store.set(todo.key, todo);
return todo;
}
}
注:
前端的Model層相對簡單,不用ORM工具,而是用browser的Local Storage模擬的存儲;
後端的Model層要用ORM框架;
視圖層實現,輸入框處理:
<Input .../>,輸入框;
https://ant.design/components/input-cn/
輸入框,能夠讓用戶提交信息,使用接收回車鍵即提交信息(另,也可點一按鍵即提交,這是錦上添花),即鍵盤按下就是一個事件(與處理鼠標點擊一樣),在<Input/>中;
輸入框的“回車”和“按鈕”是一個事件,增加按鈕是錦上添花;
import { Input } from 'antd';
ReactDOM.render(<Input placeholder="Basic usage" />, mountNode);
addonBefore,前綴標籤;
addonAfter,後置標籤;
placeholder,佔位詞;
onPressEnter,表示光標在輸入框中,按下了回車鍵觸發;
size="small",小輸入框,large大輸入框;
./src/Create.js
import React from 'react';
import { Input,Card } from 'antd'; //組合使用Input和Card組件
// import { Input } from 'antd';
import 'antd/lib/input/style'; //樣式導入
// ReactDOM.render(<Input placeholder="Basic usage" />, mountNode);
// import { Card } from 'antd';
import 'antd/lib/card/style';
// ReactDOM.render(
// <Card
// title="Card title"
// extra={<a href="#">More</a>}
// style={{ width: 300 }}
// >
// <p>Card content</p>
// <p>Card content</p>
// <p>Card content</p>
// </Card>,
// mountNode);
// export default class Create extends React.Component { //一定要導入React否則有異常
// render() {
// return (
// <Card title='請輸入待辦事宜' style={{ width: 300 }}>
// <Input placeholder="Basic usage" onPressEnter={this.props.onCreate} />
// </Card>
// );
// }
// }
export default props => //缺省導出無狀態組件,這樣寫就是讓不在這裏面定義業務的方法
<Card title='請輸入待辦事宜' style={{ width: 300 }}>
<Input placeholder="Basic usage" onPressEnter={props.onCreate}/> //等價於onPressEnter={(event) => {props.onCreate(event)}};測試時可用onPressEnter={(...args) => console.log('args is ', args)}看打印出什麼,就能決定參數怎麼寫
{/* <Input placeholder="Basic usage" onPressEnter={(event) => {props.onCreate(event)}}/> */} //onPressEnter本質就是要關聯一個單個參數的函數就行了,相當於綁定一個函數對象;onPressEnter實現的是一個函數定義,這個函數定義是一個參數,這個參數就是event對象,該箭頭函數本質就是調用props.onCreate(title)函數,這個箭頭函數只是作轉發,完全可省略,用上面那種寫法
</Card>
注:
./src/index.js
import React from 'react';
import ReactDom from 'react-dom';
import Create from './Create';
import TodoService from './service';
class Root extends React.Component {
constructor(props) {
super(props);
this.service = new TodoService(); //將業務的TodoService實例化,作爲Root實例的屬性
}
// handleCreate(...args) { //測試用,看打印的信息,可知參數如何寫;MDN裏html幫助,W3CSchool中的幫助
// console.log('Root handlerCreate');
// console.log(this);
// console.log('args is ', args);
// }
handleCreate(event) { //該函數處理Input中獲取到的用戶的輸入,及拿到數據後調用業務層service.js中相應的方法處理
console.log('Root handleCreate');
console.log(this);
console.log('event is ', event.target, event.target.value);
this.service.create(event.target.value); //數據持久化,拿到用戶輸入的數據,之後的處理(不應該在Create.js組件裏寫類似這樣的方法,數據交給index.js的Root組件處理,這就需要組件間數據共享,使用props),應調用service.js中TodoService類的create方法,構造器中有實例化該類
}
render() {
return (
<div>
<Create onCreate={this.handleCreate.bind(this)}/> //自定義組件屬性onCreate,其它名字也可,約定這樣寫,用於注入數據處理函數到Create組件的props中;此處要綁定Root的this,否則handleCreate中打印的是Create元素的this
</div>
)
}
}
ReactDom.render(<Root />, document.getElementById('root'));
注:
若手動將Local Storage中的數據清掉,數組中仍有,這兩處不會同步;
若再次刷新網頁,內存數組中的會清掉;
階段2,列表顯示、更改todo的完成狀態:
增加的todo待辦事宜,得顯示出來;
CheckBox,多選框,用來選中(完成)和取消(未完成);
onChange,選中|取消時觸發回調函數;
checked,是否選中,如在Todo.js中<Checkbox checked={props.todo.completed} .../>;
https://ant.design/components/checkbox-cn/
Grid柵格系統:
佈局方案,ant desigh和bootstrap很像,都使用一套柵格系統,使用24柵格,即每一個內部都能切分爲24份;
柵格卡片:
https://ant.design/components/card-cn/
例:
m.values()和m.keys()是迭代器(惰性求值),而array中的forEach()是立即返回;
m = new Map();
m.set(1,'a');
m.set(2,'b');
m.set(3,'c');
console.log(m);
let t = m.forEach((value,key) => console.log(key, '==', value)); //forEach方法遍歷後,沒有返回值
console.log(t);
t = [...m.values()].map(item => item + 1); //map方法遍歷後,有返回值
console.log(t);
k = [...m.keys()].map(item => item + 1);
console.log(k);
輸出:
Map { 1 => 'a', 2 => 'b', 3 => 'c' }
1 '==' 'a'
2 '==' 'b'
3 '==' 'c'
undefined
[ 'a1', 'b1', 'c1' ]
[ 2, 3, 4 ]
./src/Todo.js,構建todo組件:
import React from 'react';
import { Checkbox, Card, Col, Row } from 'antd';
import 'antd/lib/checkbox/style';
import 'antd/lib/Card/style';
import 'antd/lib/Col/style';
import 'antd/lib/Row/style';
// import { Checkbox } from 'antd';
// function onChange(e) {
// console.log(`checked = ${e.target.checked}`);
// }
// ReactDOM.render(
// <Checkbox onChange={onChange}>Checkbox</Checkbox>,
// mountNode);
// import { Card, Col, Row } from 'antd';
// ReactDOM.render(
// <div style={{ background: '#ECECEC', padding: '30px' }}>
// <Row gutter={16}>
// <Col span={8}>
// <Card title="Card title" bordered={false}>Card content</Card>
// </Col>
// <Col span={8}>
// <Card title="Card title" bordered={false}>Card content</Card>
// </Col>
// <Col span={8}>
// <Card title="Card title" bordered={false}>Card content</Card>
// </Col>
// </Row>
// </div>,
// mountNode);
export default props => (
<Card style={{ width: 300 }}>
<Row>
<Col span={4}>
<Checkbox checked={props.todo.completed} onChange={event => props.onChange(props.todo.key, event.target.checked)} /> //若沒有checked屬性,刷新頁面後,完成狀態的待辦事宜不會選中,但Local Storage中的completed是true
</Col>
<Col span={20}>
{props.todo.title}
</Col>
</Row>
</Card>
)
./src/index.js
import React from 'react';
import ReactDom from 'react-dom';
import Create from './Create';
import TodoService from './service';
import Todo from './Todo';
class Root extends React.Component {
constructor(props) {
super(props);
this.service = new TodoService();
this.state = ({todos: this.service.todos}) //若無此句,新增title後,Root的state沒變化,頁面不會刷新導致看不到新增的,而在Local Storage中是有的
}
// handleCreate(...args) {
// console.log('Root handlerCreate');
// console.log(this);
// console.log('args is ', args);
// }
handleCreate(event) {
// console.log('Root handleCreate');
// console.log(this);
// console.log('event is ', event.target, event.target.value);
this.service.create(event.target.value);
this.setState({todos: this.service.todos});
}
handleCheckedChange(key, checked) { //增加事件響應函數,handleCheckedChange(event),event.target.checked=false|true;
console.log('handleCheckedChange', key, checked);
this.service.setTodoState(key, checked);
this.setState({todos: this.service.todos});
}
render() {
return (
<div>
<Create onCreate={this.handleCreate.bind(this)}/>
<br />
{/* {this.service.todos.map(
item => <Todo todo={item} key={item.key} onChange={this.handleCheckedChange.bind(this)}/>)
} */} //迭代所有todos元素,返回一個個React組件;如下圖,要求迭代的元素有唯一的key,此處加上key={item.key}
{
[...this.service.todos.values()].map(
item => <Todo key={item.key} todo={item} onChange={this.handleCheckedChange.bind(this)} />
)
}
</div>
);
}
}
ReactDom.render(<Root />, document.getElementById('root'));
在index.js的<Todo />組件中,加key={item.key}
./service.js
import store from 'store';
export default class TodoService {
constructor() {
// super();
this.load(); //刷新頁面後,從Local Storage裝載數據,否則刷新頁面後沒有數據
}
load() {
store.each((value,key) => {
if (key.startsWith(TodoService.NAMESPACE)) //前綴即表示業務,防止與其它業務衝突,只過濾指定的業務
// this.todos.push(value);
this.todos.set(key, value);
});
console.log(this.todos);
}
static NAMESPACE = 'todo::';
// todos = [];
todos = new Map(); //遍歷數組找到key匹配的,爲高效使用ES6提供的Map類型
create(title) {
// console.log('service');
const todo = { //todo有3個屬性,key、title、completed,completed表示完成狀態
key: TodoService.NAMESPACE + (new Date()).valueOf(),
title: title,
completed: false
};
// this.todos.push(todo);
this.todos.set(todo.key, todo); //存儲todo
store.set(todo.key, todo); //持久化todo
return todo;
}
setTodoState(key, checked) {
let todo = this.todos.get(key);
if (todo) {
todo.completed = checked;
store.set(key, todo); //同步Local Storage
}
}
}
注:
階段3,項目目錄調整:
./src/component/{Create.js,Todo.js,TodoApp.js,Filter.js} #渲染的事情歸TodoApp負責,並管理所有的狀態;Create負責顯示文本框,接收用戶的輸入;Todo負責每一條待辦事宜的顯示;Filter負責狀態的切換
./src/service/service.js #負責業務的處理,爲了簡單,把數據處理也放在這裏
./src/index.js
./src/index.js
import React from 'react';
import ReactDom from 'react-dom';
import TodoApp from './component/TodoApp';
ReactDom.render(<TodoApp />, document.getElementById('root'));
./src/component/TodoApp.js
import React from 'react';
import Create from './Create';
import TodoService from '../service/service';
import Todo from './Todo';
export default class Root extends React.Component {
……
}
階段4,數據過濾:
過濾什麼狀態的待辦事宜,有3種,未完成、完成、全部;
使用antd的select:
https://ant.design/components/select-cn/
./src/component/Filter.js
import React from 'react';
import {Select, Card, Row, Col} from 'antd';
import 'antd/lib/select/style';
import 'antd/lib/card/style';
import 'antd/lib/row/style';
import 'antd/lib/col/style';
// import { Select } from 'antd';
// const Option = Select.Option;
// function handleChange(value) {
// console.log(`selected ${value}`);
// }
// ReactDOM.render(
// <div>
// <Select defaultValue="lucy" style={{ width: 120 }} onChange={handleChange}>
// <Option value="jack">Jack</Option>
// <Option value="lucy">Lucy</Option>
// <Option value="disabled" disabled>Disabled</Option>
// <Option value="Yiminghe">yiminghe</Option>
// </Select>
// <Select defaultValue="lucy" style={{ width: 120 }} disabled>
// <Option value="lucy">Lucy</Option>
// </Select>
// </div>,
// mountNode);
const Option = Select.Option;
export default props => (
<Card style={{ width: 300 }}>
<Row>
<Col span="4"></Col>
<Col span="20">
<Select style={{ width: 120 }} defaultValue="uncompleted" onChange={value => props.onChange(value)}>
<Option value="all">所有</Option>
<Option value="completed">完成</Option>
<Option value="uncompleted">未完成</Option>
</Select>
</Col>
</Row>
</Card>
)
./src/component/TodoApp.js
import React from 'react';
import Create from './Create';
import TodoService from '../service/service';
import Todo from './Todo';
import Filter from './Filter';
export default class Root extends React.Component {
constructor(props) {
super(props);
this.service = new TodoService();
this.state = ({todos: this.service.todos, filter: 'uncompleted'})
} //在state的屬性中加入filter
// handleCreate(...args) {
// console.log('Root handlerCreate');
// console.log(this);
// console.log('args is ', args);
// }
handleCreate(event) {
// console.log('Root handleCreate');
// console.log(this);
// console.log('event is ', event, event.target, event.target.value);
this.service.create(event.target.value);
this.setState({todos: this.service.todos});
}
handleCheckedChange(key, checked) { //handleCheckedChange(event),event.target.checked=false|true
console.log('handleCheckedChange', key, checked);
this.service.setTodoState(key, checked);
this.setState({todos: this.service.todos});
}
handleFilterChange(value) { //增加事件處理函數
// console.log('~~~~~~~', args);
// console.log(this);
console.log(value);
this.setState({filter: value});;
}
render() {
return (
<div>
<Create onCreate={this.handleCreate.bind(this)}/>
<Filter onChange={this.handleFilterChange.bind(this)}/>
<br />
{/* {this.service.todos.map(
item => <Todo todo={item} key={item.key} onChange={this.handleCheckedChange.bind(this)}/>)
} */}
{/* {
[...this.service.todos.values()].map(
item => <Todo key={item.key} todo={item} onChange={this.handleCheckedChange.bind(this)} />
)
} */}
{
[...this.service.todos.values()].filter(
item => {
let fs = this.state.filter;
if(fs === 'all') {
return true;
} else if(fs === 'completed') {
// if(item.completed === true)
// return true;
// else
// return false;
return item.completed === true;
} else if(fs === 'uncompleted') {
// if(item.completed === false)
// return true;
// else
// return false;
return item.completed === false;
}
}
).map(
item => <Todo key={item.key} todo={item} onChange={this.handleCheckedChange.bind(this)} />
)
} //迭代待辦事宜時,加入數組的filter函數過濾
</div>
);
}
}