iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 27
2
Modern Web

30 天 Progressive Web App 學習筆記系列 第 27

Day 27 - 30 天 Progressive Web App 學習筆記 - To-Do List 實作 PWA - 完成 React + Redux 專案架構與功能

接下來 3 篇文章的目標是以 React + Redux + Webpack 的技術來完成 To-Do List 功能與 PWA,而內容會假設讀者已經熟悉 React, Redux 及 Webpack,會把重點放在如何實作,不會逐步講解基礎觀念。

  1. 環境建置
    • 基本介紹(Intro/Install/Run)
    • 執行 React 專案前置作業
    • 打造可撰寫 JSX/ES6/ES7 的環境
    • Webpack 設定
  2. 建立檔案架構與功能
    • 規劃 React + Redux 架構
    • 顯示待辦事項清單
    • 新增/修改/刪除待辦事項清單
  3. 實作 PWA
    • 建立 Web App Manifest File
    • 新增 Service Worker

本篇為第 2 篇: 建立檔案架構與功能


規劃 React + Redux 架構

  • assets
    • images 放置網站的圖片資源
  • boot.js 入口檔案
  • src
    • Component
      • Header.jsx
      • Input.jsx
      • TodoList.jsx
      • TodoListItem.jsx
    • Container
      • Main.jsx
      • InputContainer.jsx
      • TodoListContainer.jsx
    • Pages
      • App.js
      • Home.js
    • Reducers
      • index.js
      • todolist.js
    • Store
      • configureStore.js
    • Router
      • routing.js
    • main.css

針對檔案架構個別說明如下:

Pages

  • App.js

App.js 代表整個 Application,屬於應用程式最外層的元件,可以把固定的版面放這裡,此範例放置 Header 元件

import React, { Component } from 'react';
import Header from '../Component/Header.jsx';

export default class App extends Component {
    render() {
        return (
            <div>
                { /* 應用程式最外層的元件  */ }
                { /* 把固定的版面放這裡  */ }
				<Header />
                { this.props.children }
            </div>
        );
    }
}
  • Home.js

Home.js 用來放置主要內容,這裡放 Main 元件

import React, { Component } from 'react';
import { connect } from 'react-redux';
import Main from '../Container/Main.jsx';

class Home extends Component {
    render() {
        return (
            <div>
                <Main />
            </div>
        );
    }
}

export default (Home);


Container

  • Main.jsx

Main.jsx 包括 InputContainer 和 TodoListContainer.jsx

import React, { Component } from 'react';
import Input from './InputContainer.jsx';
import TodoListContainer from './TodoListContainer.jsx';

export default class Main extends Component {
    render() {
        return (
            <div>
                <Input />
                <TodoListContainer />
            </div>
        );
    }
}

  • InputContainer.jsx

InputContainer 的程式碼傳入 Input 元件所需的 action creator 為 addTodoList,用來處理『新增待辦事項』的方法

import { connect } from 'react-redux';
import Input from '../Component/Input.jsx';
import { addTodoList } from '../Reducers/todolist.js';

export default connect(null, {
    addTodoList
})(Input);
  • TodoListContainer.jsx

TodoListContainer 的程式碼傳入 TodoList 元件所需的 store(state.todolist) 和 action creator,store 包括 id、是否完成狀態、待辦事項文字; action creator 則有三個方法,分別是 getTodoList (取得待辦事項)、toggleTodoList (修改待辦事項)、delTodoList (刪除待辦事項)

import TodoList from '../Component/TodoList.jsx';
import { connect } from 'react-redux';
import { getTodoList, toggleTodoList, delTodoList } from '../Reducers/todolist.js';

export default connect(state => ({
    todos: state.todolist
}), {
    getTodoList,
    toggleTodoList,
    delTodoList
})(TodoList);

Component

  • Header.jsx 放置 LOGO 和顯示未完成的個數

import React, { Component } from 'react';
import { connect } from 'react-redux';

class Header extends Component {
    render () {
        const { unfinishedCount } = this.props;
        return (
            <header className="header">
                <img
                    className="logo"
                    src="./assets/images/logo_todo.png"
                    alt=""
                />
                <div className="unfinished">
                    <span className="count">{ unfinishedCount }</span>
                    <span>個未完成</span>
                </div>
            </header>
        );
    }
}

export default connect(state => {
    return {
        unfinishedCount: state.todolist
                        .filter((item)=>!item.isComplete).length
    }
})(Header);

  • Input.jsx 輸入 To-Do List 內容

import React, { Component } from 'react';

export default class Input extends Component {
    constructor (props) {
        super(props);
        this.state = {
            value: ''
        }
    }

    render() {
        const { addTodoList } = this.props;
        return (
            <div id="todoInput" className="input-content">
                <img
                    className="add"
                    src="./assets/images/ic_add.png"
                    alt=""
                />
                <input
                    className="input"
                    placeholder="What need to be done?"
                    value={ this.state.value }
                    type="text"
                    onChange={
                        (event) => {
                            this.setState({
                                value: event.target.value
                            });
                        }
                    }
                    onKeyDown={
                        (event) => {
                            if (event.keyCode === 13) {
                                console.log('Enter');
                                addTodoList(this.state.value);
                                this.setState({value: ''})
                            }
                        }
                    }
                />
            </div>
        );
    }
}

  • TodoList.jsx 顯示 To-Do List 內容(ul)

import React, { Component } from 'react';
import TodoListItem from './TodoListItem.jsx';

export default class TodoList extends Component {
    componentDidMount() {
        this.props.getTodoList();
    }
    render() {
        const { todos, toggleTodoList, delTodoList } = this.props;
        return (
            <ul id="todoList">
            {
                todos.map((item, index) => {
                    return (
                        <TodoListItem
                            key={`todolist_${index}`}
                            id={ item.id }
                            desc={ item.desc }
                            isComplete={ item.isComplete }
                            toggleTodoList={ toggleTodoList }
                            delTodoList={ delTodoList }
                        />
                    )
                })
            }
            </ul>
        );
    }
}

  • TodoListItem.jsx 顯示 To-Do List 項目(li)

import React, { Component } from 'react';

export default class TodoListItem extends Component {
	constructor(props) {
		super(props);
        this.modifyList = this.modifyList.bind(this);
	}

    modifyList() {
        const { isComplete, id, desc } = this.props;

        this.props.toggleTodoList({
            id,
            desc,
            isComplete: !isComplete
        });
    }

	render() {
		const { isComplete, id, desc, delTodoList } = this.props;
		return (
			<li className="list">
                <a
                	className={ isComplete ? 'finish' : 'unfinished' }
                	onClick={ () =>{this.modifyList();} }
                >
                </a>
                <p
                    className="desc"
                    onClick={ () =>{this.modifyList();} }
                >
                    { desc }
                </p>
                <a
                    className="del"
                    onClick={ () => { delTodoList(id); } }
                >
                </a>
            </li>
		);
	}
}

Reducers

Reducers 則用來存放資料,搭配 action type、action creator 處理資料,再用 Reducers/index.js 整合 Reducers,此範例只有一個 Reducer (todolist.js)。

先前文章已使用 Vanilla JS 完成 To-Do List 功能,複習一下之前實作 Vanilla JS 的相關文章:

這次則是要改成 React + Redux 實作功能。

todolist 的 Reducer 方法如下,用來處理 todolist 資料:

export default function todolist (state = initialState, action) {
    switch (action.type) {
    case RECEIVE_TODO:
        return action.data;
    case ADD_TODO:
        state.push(action.payload);
        return [...state];
    case TOGGLE_TODO:
        state.forEach((item, index) => {
            if (item.id === action.payload.id) {
                state[index] = action.payload;
            }
        })
        return [...state];
    case DEL_TODO:
        return state.filter(item => item.id !== action.id)
    default:
        return state;
    }
}

action creator 如下:

export function receiveTodoList (data) {
    return {
        type: RECEIVE_TODO,
        data: data
    };
}

export function getTodoList() {
    return function(dispatch) {
        return axios.get(`${URL}todolist`)
        .then(res => {
            dispatch(receiveTodoList(res.data));
        });
    }
}
export function receiveAddTodoList (payload) {
    return {
        type: ADD_TODO,
        payload: payload
    };
}

export function addTodoList (value) {
    return function(dispatch) {
        return axios({
            method: 'post',
            url: `${URL}todolist`,
            headers: {
                'Content-Type': 'application/json'
            },
            data: {
                isComplete: false,
                desc: value
            }
        })
        .then(res => {
            dispatch(receiveAddTodoList(res.data));
        })
        .catch(error => { console.log(error) })
    }
}
export function toggleTodoList (payload) {
    return function(dispatch) {
        return axios({
            method: 'put',
            url: `${URL}todolist/${payload.id}`,
            headers: {
                'Content-Type': 'application/json'
            },
            data: payload
        })
        .then(res => {
            dispatch({type: TOGGLE_TODO, payload: payload});
        })
    }
}
export function delTodoList (id) {
    return function(dispatch) {
        return axios({
            method: 'delete',
            url: `${URL}todolist/${id}`,
            headers: {
                'Content-Type': 'application/json'
            }
        })
        .then(res => {
            dispatch({type: DEL_TODO, id: id});
        })
    }
}

補充說明: 元件為什麼要拆分為 ContainerComponent ?

可以參考 Container ComponentsPresentational and Container Components 這 2 篇文章,當然目前示範的 To-Do List 是小範例,但如果專案越來越龐大,為了重用性、可維護性等原因,建議拆分 ContainerComponent ,由 Container 處理資料,讓 Component 專心顯示內容。


本日小結

此篇文章針對『建立檔案架構與功能』做說明,從規劃專案的架構開始,新增各種元件去顯示畫面,接著建立 Redux 的 Store 、 Reducer 和 Action creator 完成待辦事項的功能,而下一篇將會介紹『實作 PWA』。


本人小小筆記,如有錯誤或需要改進的部分,歡迎給予回饋。
我將會用最快的速度修正,m(_ _)m。謝謝

上一篇
Day 26 - 30 天 Progressive Web App 學習筆記 - To-Do List 實作 PWA - React + Redux + Webpack 環境建置
下一篇
Day 28 - 30 天 Progressive Web App 學習筆記 - To-Do List 搭配 React + Redux 實作 PWA
系列文
30 天 Progressive Web App 學習筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言