React App – Router and Redux

从这里进入第二阶段的学习,会创建一个新的财务软件项目。

客户端路由
传统的页面导航基于服务器,每个页面独立请求和由服务器提供。
新的方法是基于客户端,页面变化由路由表对应到组件和 JS 方法。
使用组件模拟页面导航的应用程序其实只有一个页面,运行更快。
根据用户的动作,JS 选择组件提供给页面,只刷新必要的部分。

重用代码
为了重用一些基本的代码和项目结构文件,复制 Todo App。
新的程序用来记录个人账户变化,可命名为 Budget App。
需要清空的文件夹包括:
src/components
src/playground
src/styles/components
需要更新以下文件:
src/styles/base/_base.scssbody中删除以下定义:

    background: $dark-blue;

src/styles/base/_settings.scss中保留以下定义:

$s-size: 1.2rem;
$m-size: 1.6rem;
$l-size: 3.2rem;
$xl-size: 4.8rem;
$screen-width: 45rem;

src/styles/styles.scss中保留以下定义:

@import './base/settings';
@import './base/base';

更新public/index.html文件中如下语句:

        <title>Budget App</title>

更新package.json文件中如下语句:

  "name": "budget-app",

更新src/app.js如下:

import React from 'react';
import ReactDOM from 'react-dom';
import 'normalize.css/normalize.css';
import './styles/styles.scss';

ReactDOM.render(<p>From Budget App</p>, 
    document.getElementById('app'));

重启服务器$ npm run dev-server
确认程序主页信息,控制台没有错误提示。
以上,清理工作完成,开始添加本项目内容。

安装 React Router 运行:
$ cnpm install react-router-dom@4.2.2 --save

为 Webpack 配置使用客户端路由。
配置webpack.config.js文件的devServer部分:

    devServer: {
        contentBase: path.join(__dirname,'public'),
        historyApiFallback: true
    }

以上增加了语句historyApiFallback: true
设置找不到指定页面时默认打开程序主页。

以下代码过程不符合正常或推荐开发流程。
应该是从框架到内容伴随测试的迭代开发。
这里只是记录下第一阶段最终的结果。

创建组件
创建src/components/DashboardPage.js作为主页:

import React from 'react';

const DashboardPage = () => (
    <div>
        Reserved for Dashboard
    </div>
);

export default DashboardPage;

以上,没有惊喜。

类似的,创建AddBudgetPage.js用于添加预算:

import React from 'react';

const AddBudgetPage = () => (
    <div>
        Reserved for Adding Budget Page
    </div>
);

export default AddBudgetPage;

创建HelpPage.js用于提供帮助信息:

import React from 'react';

const HelpPage = () => (
    <div>
        Reserved for Help Page
    </div>
);

export default HelpPage;

创建NotFoundPage.js用于404错误:

import React from 'react';
import { Link } from 'react-router-dom';

const NotFoundPage = () => (
    <div>
        404! - <Link to="/">Go Home</Link>
    </div>
);

export default NotFoundPage;

这里用了来自 React Router 的 Link 容器。
类似 HTML 的 可以创建超链接。
只是 Link 依托 React 框架,效率更高。

创建EditBudgetPage.js用于编辑个别预算:

import React from 'react';

const EditBudgetPage = (props) => {
    console.log(props);
    return (
        <div>
            Reserved for Editing Budget with ID: 
            {props.match.params.id}
        </div>
    );
};

export default EditBudgetPage;

注意以上代码在控制台记录了读入的属性。
并在返回文字中加入读入属性的一个变量。
该变量在后面的路由设置中会做定义。

最后创建Header.js用于插入到所有页面头部:

import React from 'react';
import { NavLink } from 'react-router-dom';

const Header = () => (
    <header>
        <h1>Budget App</h1>
        <NavLink to="/" activeClassName="is-active" exact={true}>Home</NavLink>
        <NavLink to="/create" activeClassName="is-active">Add</NavLink>
        <NavLink to="/help" activeClassName="is-active">Help</NavLink>
    </header>
);

export default Header;

注意, 是 HTML5 的容器,不是来自 React。
使用NavLink创建导航,后面的类会凸显当前页面链接。

以上所有组件会统一由路由文件链接和组合。
创建src/routers/AppRouter.js路由文件:

import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import DashboardPage from '../components/DashboardPage';
import AddBudgetPage from '../components/AddBudgetPage';
import EditBudgetPage from '../components/EditBudgetPage';
import HelpPage from '../components/HelpPage';
import NotFoundPage from '../components/NotFoundPage';
import Header from '../components/Header';

const AppRouter = () => (
    <BrowserRouter>
        <div>
            <Header />
            <Switch>
                <Route path="/" component={DashboardPage} exact={true} />
                <Route path="/create" component={AddBudgetPage} />
                <Route path="/edit/:id" component={EditBudgetPage} />
                <Route path="/help" component={HelpPage} />
                <Route component={NotFoundPage} />
            </Switch>
        </div>
    </BrowserRouter>
);

export default AppRouter;

以上导入组件,注意诸如 的用法。
会添加到所有可能的路由。
会根据路径设置匹配需要提供的组件。
exact参数保证路径匹配方式为绝对匹配。
其中:id的参数传入方式以及在属性中的结果。

最后将以上创建的路由对象提供给app.js

import React from 'react';
import ReactDOM from 'react-dom';
import AppRouter from './routers/AppRouter';
import 'normalize.css/normalize.css';
import './styles/styles.scss';

ReactDOM.render(<AppRouter />,
    document.getElementById('app'));

综上,组件通过路由配置来集合,并提供给程序。
这一部分了解路由的结构,组件和参数传导用法。

一个错误
这里记录一个奇怪的错误,关于 404 错误。
在设置/edit/:id的路由时,可能发现错误:
GET http://localhost:8080/edit/bundle.js net::ERR_ABORTED 404 (Not Found)
其实错误信息很明显,找bundle.js的位置不对。
但直到对比官方代码才发现原因在index.html里。
正确的代码是:

<script src="/bundle.js"></script>

而我的代码是:

<script src="bundle.js"></script>

没有明确bundle.js在根目录,程序找错了位置。

Redux
简单的说,Redux方便在不同组件之间传递状态。
根据经典模型,状态和属性需要依赖母子关系传递。
而Redux方便了不相关的组件之间相互传递信息。

为了初步了解Redux创建练习文件:
src/playground/PlayRedux.js
修改webpack.config.js配置提供以上文件:
entry: './src/playground/PlayRedux.js',
注意这里替代的是原程序入口app.js
可见Redux会从更底层控制React程序。

更新PlayRedux.js文件如下:

console.log('hello');

运行开发服务器:
$ npm run dev-server
到程序主页http://localhost:8080/
打开开发者工具控制台,确认提示信息。

安装Redux
$ cnpm install redux@3.7.2 --save

Redux的组成* Action是程序发给Store的信息,用dispatch方法发送

  • Reducer定义某个Action发生时State如何相应的变化
  • Store保存程序状态并提供访问和更新接口,注册监听

根据 Action 判断如何更新State,具有如下特点:

  • Reducer 是纯函数,即输入和输出都与外界无关。
  • Reducer 不会修改 State 或 Action

安装两个包,分别用于生成唯一 ID 和转换对象。
cnpm install uuid@3.1.0 --save
cnpm install babel-plugin-transform-object-rest-spread@6.23.0 --save
更新.babelrc配置文件,添加对以上插件的引用:

{
    "presets": ["env", "react"],
    "plugins": [
        "transform-class-properties",
        "transform-object-rest-spread"
    ]
}

生成唯一 ID 的包提供uuid()方法,很简单。
转换对象是指增加对ES6规范 … obj的支持。

用两个小程序示例,更新PlayRedux.js文件如下:

// *********************************
// 以下是只包含一个 Reducer 的简单例子
// *********************************

// 从 redux 导入创建库和整合 Reducer 的模块
import { createStore, combineReducers } from 'redux';
import uuid from 'uuid';

// 定义 Action,默认参数为空对象,默认对象属性值为 1
const increment = ({ incrementBy = 1 } = {}) => ({
    type: 'INCREMENT',
    incrementBy
});

const decrement = ({ decrementBy = 1 } = {}) => ({
    type: 'DECREMENT',
    decrementBy
});

const set = ({ count } = {}) => ({
    type: 'SET',
    count
});

const reset = () => ({
    type: 'RESET'
})

// 定义 reducer,为不同 Action 定义处理方法
const countReducer = (state = { count: 0 }, action) => {
    switch (action.type) {
        case 'INCREMENT':
            return {
                count: state.count + action.incrementBy
            };
        case 'DECREMENT':
            return {
                count: state.count - action.decrementBy
            };
        case 'RESET':
            return {
                count: 0
            };
        case 'SET':
            return {
                count: action.count
            };
        default:
            return state;
    }
};

// 创建 store 用来保存 state
// 注意创建方法需要使用参数 reducer
const store = createStore(countReducer);
console.log(store.getState());

// store 注册方法,每次 state 变化时触发控制台日志
// 返回的注销方法,在触发时注销对 state 变化的监控
const unsubscribe = store.subscribe(() => {
    console.log(store.getState());
});

// 使用默认值 1 和指定值 5 增加计数器
store.dispatch(increment());
store.dispatch(increment({incrementBy: 5}));

store.dispatch(decrement());
store.dispatch(decrement({decrementBy: 10}));

// 重置和设置计数器
store.dispatch(reset());
store.dispatch(set({count: 101}));

// 注销监控后运行减法,没有触发控制台日志
unsubscribe();
store.dispatch(decrement());

// *******************************
// 以下是包含两个 Reducer 的复杂例子
// *******************************

// 定义 Expsense 的 Action

// ADD_EXPENSE
// 新建支出,设置默认值,直接返回对象
const addExpense = ({
    description = '',
    note = '',
    amount = 0,
    createdAt = 0
} = {}) => ({
    type: 'ADD_EXPENSE',
    expense: {
        id: uuid(),
        description,
        note,
        amount,
        createdAt
    }
});

// REMOVE_EXPENSE
const removeExpense = ({ id } = {}) => ({
    type: 'REMOVE_EXPENSE',
    id
});

// EDIT_EXPENSE
const editExpense = (id, updates) => ({
    type: 'EDIT_EXPENSE',
    id,
    updates
});

// 定义 Filter 的 Action

// SET_TEXT-FILTER
const setTextFilter = (text = '') => ({
    type: 'SET_TEXT_FILTER',
    text
});

// SORT_BY_DATE
const sortByDate = () => ({
    type: 'SORT_BY_DATE'
});

// SORT_BY_AMOUNT
const sortByAmount = () => ({
    type: 'SORT_BY_AMOUNT'
});

// SET_START_DATE
const setStartDate = (date) => ({
    type: 'SET_START_DATE',
    date
});

// SET_END_DATE
const setEndDate = (date) => ({
    type: 'SET_END_DATE',
    date
});

// 定义 Expense 的默认状态
const eDefaultState = [];

const expensesReducer = ( state = eDefaultState, action) => {
    switch (action.type) {
        case 'ADD_EXPENSE':
            return [
                // 注意以下引用 state 对象的方法
                ...state,
                action.expense
            ];
        case 'REMOVE_EXPENSE':
            // 返回过滤掉指定 ID 的数组实现删除功能
            return state.filter(({ id }) => id !== action.id);
        case 'EDIT_EXPENSE':
            return state.map((expense) => {
                if (expense.id === action.id) {
                    return {
                        // 后面的属性值会覆盖前面同名的
                        ...expense,
                        ...action.updates
                    };
                } else {
                    return expense;
                }
            });
        default:
            return state;
    }
};

// 定义 Filter 的默认状态
const fDefaultState = {
    text: '',
    sortBy: 'date',
    startDate: undefined,
    endDate: undefined
};

const filtersReducer = ( state = fDefaultState, action) => {
    switch (action.type) {
        case 'SET_TEXT_FILTER':
            return {
                ...state,
                text: action.text
            };
        case 'SORT_BY_DATE':
            return {
                ...state,
                sortBy: 'date'
            };
        case 'SORT_BY_AMOUNT':
            return {
                ...state,
                sortBy: 'amount'
            }
        case 'SET_START_DATE':
            return {
                ...state,
                startDate: action.date
            };
        case 'SET_END_DATE':
            return {
                ...state,
                endDate: action.date
            };
        default:
            return state;
    }
};

// 返回经过过滤的 Expense,注意过滤和排序方法的定义
const getFilteredExpenses = (expenses, {text, sortBy, startDate, endDate}) => {
    return expenses.filter((expense) => {
        const startDateMatch = typeof startDate !== 'number' ||
            expense.createdAt >= startDate;
        const endDateMatch = typeof endDate !== 'number' ||
            expense.createdAt <= endDate;
        const textMatch = 
            expense.description.toLowerCase().includes(text.toLowerCase());
        return startDateMatch && endDateMatch && textMatch;
    }).sort((a, b) => {
        if (sortBy === 'date') {
            return a.createdAt > b.createdAt ? 1 : -1;
        } else if (sortBy === 'amount') {
            return a.amount > b.amount ? 1 : -1;
        } else {
            console.log('not sortable');
        };
    });
};

// 通过整合 Reducer 创建库
const expenseStore = createStore(
    combineReducers({
        expenses: expensesReducer,
        filters: filtersReducer,
    })
);

// 在每次状态有更新时控制台输出过滤后的 Expense
expenseStore.subscribe(() =>{
    const state = expenseStore.getState();
    const filteredExpenses = 
        getFilteredExpenses(state.expenses, state.filters);
    console.log(filteredExpenses);
});

// 创建三个 Expense
const one = expenseStore.dispatch(
    addExpense({description: 'rent', amount: 100}));
const two = expenseStore.dispatch(
    addExpense({description: 'coffee', amount: 200, createdAt: -1000}));
const three = expenseStore.dispatch(
    addExpense({description: 'rent', amount: 200, createdAt: 1000}));

// 删除和更新 Expense 的方式很简洁,具体可查看 Reducer 中的代码
expenseStore.dispatch(removeExpense({id: one.expense.id}));
expenseStore.dispatch(editExpense(two.expense.id, {amount: 500}));

// 定义并应用文字过滤器,之后清空过滤器对比结果
expenseStore.dispatch(setTextFilter('rent'));
expenseStore.dispatch(setTextFilter());

// 根据 Amount 和 Date 分别做排序,对比结果
expenseStore.dispatch(sortByAmount());
expenseStore.dispatch(sortByDate());

// 设置过滤器开始时间再清空,对比结果
expenseStore.dispatch(setStartDate(125));
expenseStore.dispatch(setStartDate());

// 设置过滤器结束时间再清空,对比结果
expenseStore.dispatch(setEndDate(125));
expenseStore.dispatch(setEndDate());

// 示例库的结构,包括一个 Expense 数组和一个过滤器对象
const demoState = {
    expense: [{
        id: 'anything',
        description: 'January Rent',
        note: 'Final payment',
        amount: 54500,
        createAt: 0
    }],
    filters: {
        text: 'rent',
        sortBy: 'amount', // date or amount
        startDate: undefined,
        endDate: undefined
    }
};

具体参考代码内的行注释,输出都在控制台。

React App – Modal and Styles

目前,随机选择的任务由浏览器默认警告弹出。
借助第三方库 React Modal 可以优化这个交互。

安装 React Modal:
$ cnpm install react-modal@2.2.2 --save
新建src/TaskModal.js如下:

import React from "react";
import Modal from "react-modal";
// 通过 props 传入选择的任务和关闭提示窗方法
const TaskModal = (props) => (
    <Modal
        // 根据是否由选择任务确定是否弹窗
        isOpen={!!props.selectedTask}
        // 在点击窗体外部时关闭窗体
        onRequestClose={props.onConfirmPick}
        // 为读屏软件提供的文字信息
        contentLabel="Selected Task"
    >
        <h3>Selected Task</h3>
        {props.selectedTask && <p>{props.selectedTask}</p>}
        <button onClick={props.onConfirmPick}>Okay</button>
    </Modal>
);

export default TaskModal;

可查阅文档了解 Modal 自带的属性配置项。
以上 Modal 优化了提示信息的界面和交互。

另外,注意直接返回组件并省略 return 的用法。
以上省略了 return 的方法可以进一步简化代码:
Header.js可以简化为如下:

import React from 'react';
const Header = (props) => (
    <div>
        <h1>{props.title}</h1>
        <h2>{props.subtitle}</h2>
    </div>
)
export default Header;

其他简单返回的 SFC 组件可做同样的优化:
• Action.js
• Tasks.js
• Task.js

SCSS 样式定义
SCSS 与 CSS 的关系类似 JSX 与 JS。
前者提供更方便简洁的语法,便于使用。
后者是最终的编译结果,为浏览器接受。

为了加载 CSS 和 Style,安装以下模块:
$ cnpm install style-loader@0.18.2 css-loader@0.28.4 --save

配置webpack.config.jsmodule部分:

    module: {
        rules: [{
            loader: 'babel-loader',
            test: /\.js$/,
            exclude: /node_modules/
        }, {
            test: /\.css$/,
            use: [
                'style-loader',
                'css-loader'
            ]
        }]
    },

以上定义了对 CSS 文件的处理方式。

src/app.js中添加对 CSS 文件的导入:

import React from 'react';
import ReactDOM from 'react-dom';
import TodoApp from './components/TodoApp';
import './styles/styles.css';

ReactDOM.render(<TodoApp />, document.getElementById('app'));

建立样式定义文件和专属文件夹:
src/styles/styles.css

/* 为测试目的,设置所有页面对象为红色 */
* {
    color: red;
}

完成以上配置后,重启开发服务器:
$ npm run dev-server
可以看到页面上所有文字变为红色。

下面添加处理 SCSS 的模块:
$ cnpm install sass-loader@6.0.6 node-sass@4.5.3 --save

再次更新webpack.config.jsmodule部分:

    module: {
        rules: [{
            loader: 'babel-loader',
            test: /\.js$/,
            exclude: /node_modules/
        }, {
            test: /\.scss$/,
            use: [
                'style-loader',
                'css-loader',
                'sass-loader'
            ]
        }]
    },

以上更新了对 SCSS 监控和使用sass-loader模块。

为测试新的变化,修改src/styles/styles.css
更新文件名为src/styles/styles.css和内容如下:

$brand-color: blue;
* {
    color: $brand-color;
}

注意 SCSS 对变量的支持,方便集中管理配色。

相应的,app.js导入的样式文件也需要更新:

import React from 'react';
import ReactDOM from 'react-dom';
import TodoApp from './components/TodoApp';
import './styles/styles.scss';

ReactDOM.render(<TodoApp />, document.getElementById('app'));

导入样式文件扩展名由 CSS 改为 SCSS 即可。

中断并重新运行 dev-server 确认字体颜色变化。

分离关注
与 JS 代码类似,SCSS 也应将代码按组件分离。
创建src/styles/base/_base.scss文件:

html {
    font-size: 62.5%
}

body {
    font-family: Arial, Helvetica, sans-serif;
    font-size: 1.6rem;
}

这里简单定义基本主页对象的样式。
注意文件名以下划线开始,表示为组件样式文件。

创建src/styles/components/_header.scss文件:

.header {
    background: #20222b;
    color: white;
    margin-bottom: 4.8rem;
    padding: 1.6rem 0;
}
// BEM Block Element Modifier
.header__title {
    font-size: 3.2rem;
    margin: 0;
}

.header__subtitle {
    color: #a5afd7;
    font-size: 1.6rem;
    font-weight: 500;
    margin: 0;
}

以上分别为标题容器,主副标题设置了类样式。
注意主副标题的类名称,使用了 BEM 规范。

修改src/components/Header.js应用 SCSS 类:

import React from 'react';
const Header = (props) => (
    <div className="header">
        <h1 className="header__title">{props.title}</h1>
        <h2 className="header__subtitle">{props.subtitle}</h2>
    </div>
)
export default Header;

注意 JSX 中用className类定义 CSS 类。

最后更新styles/styles.scss导入新建的 SCSS 文件:

@import './base/base';
@import './components/header';

注意导入组件 SCSS 文件,省略下划线和扩展名。

保存以上更新,查看主页确认样式效果已经应用。

样式兼容问题
不同系统或浏览器可能对样式有不同默认设置。
使用第三方库对 CSS 做一般化处理可修正该问题。
安装库:$ cnpm install normalize.css@7.0.0 --save
更新src/app.js导入一般化 CSS 文件:

import React from 'react';
import ReactDOM from 'react-dom';
import TodoApp from './components/TodoApp';
import 'normalize.css/normalize.css';
import './styles/styles.scss';

ReactDOM.render(<TodoApp />, document.getElementById('app'));

更新webpack.config.jsmodule部分:

    module: {
        rules: [{
            loader: 'babel-loader',
            test: /\.js$/,
            exclude: /node_modules/
        }, {
            test: /\.s?css$/,
            use: [
                'style-loader',
                'css-loader',
                'sass-loader'
            ]
        }]
    },

注意正则表达式/\.s?css$/兼容了 CSS 和 SCSS。
在 Chrome 浏览器上,这个改进明显的效果是:
所有 HTML 容器默认带的白边缩进消失了。

结构化样式定义
把所有的样式定义在一个 SCSS 文件中不便管理。
且 SCSS 允许使用变量统一管理反复使用的定义。
为此,创建src/styles/base/_settings.scss存放变量:

// Colors
$off-black: #20222b;
$off-white: #a5afd7;
$dark-blue: #333745;

// Spacing
$m-size: 1.6rem;
$l-size: 3.2rem;
$xl-size: 4.8rem;

并创建一个外部容器,统一管理组件的居中。
新建src/styles/components/_container.scss文件:

.container {
    max-width: 60rem;
    margin: 0 auto;
    padding: 0 $m-size;
}

注意这里已经应用了 settings 中定义的变量。
为使用 settings 和 container 更新styles.scss:

@import './base/settings';
@import './base/base';
@import './components/container';
@import './components/header';

注意 settings 需要最先导入。
接着更新其他 SCSS 文件使用 settings 中的变量。
更新_base.scss应用背景色和字体变量:

html {
    font-size: 62.5%
}

body {
    background: $dark-blue;
    font-family: Arial, Helvetica, sans-serif;
    font-size: $m-size;
}

类似的,更新_header.scss:

.header {
    background: $off-black;
    color: white;
    margin-bottom: $xl-size;
    padding: $m-size 0;
}

.header__title {
    font-size: $l-size;
    margin: 0;
}

.header__subtitle {
    color: $off-white;
    font-size: $m-size;
    font-weight: 500;
    margin: 0;
}

最后,更新 JS 文件,应用这里定义的容器类。
更新src/components/Header.js添加居中容器:

import React from 'react';

const Header = (props) => (
    <div className="header">
        <div className="container">
            <h1 className="header__title">{props.title}</h1>
            <h2 className="header__subtitle">{props.subtitle}</h2>
        </div>
    </div>
)

export default Header;

更新src/components/TodoApp.js中的返回部分:

        return (
            <div>
                <Header
                    title={title}
                    subtitle={subtitle}
                />
                <div className="container">
                    <Action
                        hasOptions={this.state.tasks.length > 0}
                        onHandlePick={this.onHandlePick}
                    />
                    <Tasks
                        tasks={this.state.tasks}
                        onRemoveTasks={this.onRemoveTasks}
                        onRemoveTask={this.onRemoveTask}
                    />
                    <AddTask
                        onAddTask={this.onAddTask}
                    />
                </div>
                <TaskModal
                    selectedTask={this.state.selectedTask}
                    onConfirmPick={this.onConfirmPick}
                />
            </div>
        );

将除了 Header 和 TaskModal 以外的部分居中。
以上应用了 SCSS 的功能并对样式定义做了结构化。

选取任务按钮
Todo App 的重要功能之一是从列表随机选取任务。
目前改功能通过一个默认按钮实现,这里优化它。

更新styles/base/_settings.scss

// Colors
$off-black: #20222b;
$off-white: #a5afd7;
$dark-blue: #333745;
$purple: #8357c5;

// Spacing
$s-size: 1.2rem;
$m-size: 1.6rem;
$l-size: 3.2rem;
$xl-size: 4.8rem;

以上添加了一个紫色和一个小号的定义。

创建styles/components/_button.scss:

// big button
.big-button {
    background: $purple;
    border: none;
    border-bottom: .6rem solid darken($purple, 10%);
    color: white;
    font-weight: bold;
    font-size: $l-size;
    margin-bottom: $xl-size;
    padding: 2.4rem;
    width: 100%;
}

.big-button:disabled {
    opacity: .5;
}

// normal button
.button {
    background: $purple;
    border: none;
    border-bottom: .3rem solid darken($purple, 10%);
    color: white;
    font-weight: 500;
    padding: $s-size;
}

// block element modifier
.button--link {
    background: none;
    border: none;
    color: $off-white;
    padding: 0;
}

以上分别定义了大小两种按钮的样式。
注意大按钮中使用的darken方法和disable状态。
小按钮中使用的 BEM 定义方法修改原设置。

更新styles/styles.scss加载_button.scss文件:

@import './base/settings';
@import './base/base';
@import './components/button';
@import './components/container';
@import './components/header';

以上增加了对新建按钮样式文件的导入。

更新styles/base/_base.js添加如下内容:

button {
    cursor: pointer;
}

button:disabled {
    cursor: default;
}

以上根据按钮可用与否调整了鼠标指针样式。

将新建的大按钮类应用到组件Action.js文件:

    <div>
        <button
            className="big-button"
            onClick={props.onHandlePick}
            disabled={!props.hasOptions}
        >
            Choose a task for me!
                </button>
    </div>

以上只是应用了大按钮的类定义。

将新建的小按钮类应用到AddTask.js文件:

            <div>
                {this.state.error && <p>{this.state.error}</p>}
                <form onSubmit={this.onAddTask}>
                    <input type="text" name="task" />
                    <button className="button">Add Task</button>
                </form>
            </div>

同样只是添加类的定义即可。

将新建的小按钮类应用到Tasks.js

        <button
            className="button button--link"
            onClick={props.onRemoveTasks}
        >
            Remove All
        </button>

Task.js文件:

            <button
                className="button button--link"
                onClick={(e) =>
                    props.onRemoveTask(props.task)}
            >
                remove
            </button>

同样只是为按钮添加了类的定义。
但是,注意 BEM 类紧跟原类的应用方式。

任务列表样式
继续为任务列表和提交任务部分设置样式。

更新src/styles/base/_settings.scss添加如下:

$blue: #3c4251;
$light-blue: #464b5e;

创建src/styles/component/_task.scss文件:

// tasks
.tasks {
    background: $light-blue;
    margin-bottom: $xl-size;
}

.tasks__message {
    color: $off-white;
    margin: 0;
    padding: $l-size;
    text-align: center;
    border-bottom: .1rem solid lighten($light-blue, 10%)
}

// tasks header
.tasks-header {
    background: $blue;
    color: $off-white;
    display: flex;
    justify-content: space-between;
    padding: $m-size;
}

.tasks-header__title {
    margin: 0;
}

注意任务列表标题中对象排列方向和间隔的定义。
方向使用横向排列,默认为纵向排列。
分布为空间填满间隔,即散列至容器边界。

更新src/styles/styles.scss添加如下语句:

@import './components/tasks';

更新src/components/TodoApp.js如下:

                    <div className="tasks">
                        <Tasks
                            tasks={this.state.tasks}
                            onRemoveTasks={this.onRemoveTasks}
                            onRemoveTask={this.onRemoveTask}
                        />
                        <AddTask
                            onAddTask={this.onAddTask}
                        />
                    </div>

以上只是新建一个容器,承载任务列表和添加任务对象。
并将新建的类应用到该容器,以便使样式定义生效。

更新src/components/tasks.js如下:

        <div className="tasks-header">
            <h3 className="tasks-header__title">
                {
                    props.tasks.length ?
                        `You have ${props.tasks.length} tasks:` :
                        'You do not have tasks!'
                }
            </h3>
            <button
                className="button button__link"
                onClick={props.onRemoveTasks}
            >
                Remove All
            </button>
        </div>
        {props.tasks.length === 0 && 
            <p className="tasks__message">
                Please add a task to get started!</p>}

以上将任务列表标题和清除列表的按钮放到一个容器。

单个任务的样式
继续更新单个任务记录和添加任务表单的样式。

新建src/styles/components/_task.scss文件:

.task {
    border-bottom: .1rem solid lighten($light-blue, 10%);
    display: flex;
    justify-content: space-between;
    padding: $l-size $m-size;
}

.list {
    margin: 0;
    padding-inline-start: 0;
}

.list-item {
    color: white;
    font-weight: 500;
    font-size: 2rem;
    margin: 0;
    margin-inline-start: $l-size;
    margin-inline-end: $s-size;
    word-break: break-all;
}

将其导入到styles.scss文件,这里略过。

更新src/components/Tasks.js如下:

        <ol className="list">
            {
                props.tasks.map((task) =>
                    <Task
                        key={Math.random()}
                        task={task}
                        onRemoveTask={props.onRemoveTask}
                    />
                )
            }
        </ol>

以上更新将列表的外层容器样式应用到任务序列表

更新src/components/Task.js如下:

    <div className="task">
        <li className="list-item">{props.task}</li>
        <button
            className="button button__link"
            onClick={(e) =>
                props.onRemoveTask(props.task)}
        >
            remove
        </button>
    </div>

以上更新将任务记录和其内层容器的样式应用到对象。

新建src/styles/components/_add-task.scss文件:

.error-message {
    color: $off-white;
    font-style: italic;
    margin: $m-size 0 0 0;
    padding: 0 $m-size;
}

.add-task {
    display: flex;
    padding: $m-size;
}

.add-task__input {
    background: $dark-blue;
    border: none;
    border-bottom: .3rem solid darken($dark-blue, 10%);
    color: $off-white;
    flex-grow: 1;
    margin-right: $s-size;
    padding: $s-size;
}

以上定义了错误提示,添加任务的表单及表单输入框样式。
表单按钮的样式已经在更早的时候定义和应用过了。
其中,flex-grow: 1;定义表单输入框占满剩余空间。

更新src/components/AddTask.js如下:

            <div>
                {this.state.error && 
                    <p className="error-message">{this.state.error}</p>}
                <form className="add-task" onSubmit={this.onAddTask}>
                    <input className="add-task__input" type="text" name="task" />
                    <button className="button">Add Task</button>
                </form>
            </div>

以上分别将错误信息,添加任务表单和表单输入框的样式应用。

随机任务弹窗
继续对随机选择的任务弹窗做样式优化。

新建src/styles/components/_modal.scss文件:

.ReactModalPortal > div {
    opacity: 0
}

.ReactModalPortal .ReactModal__Overlay {
    align-items: center;
    display: flex;
    justify-content: center;
    transition: opacity 200ms ease-in-out;
}

.ReactModalPortal .ReactModal__Overlay--after-open {
    opacity: 1;
}

.ReactModalPortal .ReactModal__Overlay--before-close {
    opacity: 0;
}

.modal {
    background: $light-blue;
    color: white;
    max-width: 30rem;
    outline: none;
    padding: $l-size;
    text-align: center;
}

.modal__title {
    margin: 0 0 $m-size 0;
}

.modal__body {
    margin: 0 0 $l-size 0;
    font-size: 2rem;
    font-weight: 300;
    word-break: break-all;
}

注意,除自定义类以上覆盖定义了多个 React Modal 类。
这些类名是预设的,在 React Modal 不同生命周期出现。
需要在开发者工具中查看和发现以上 React Modal 类。
这里的word-break: break-all;定义了对超长字符的截断。

更新src/components/TaskModal.js如下:

    <Modal
        isOpen={!!props.selectedTask}
        onRequestClose={props.onConfirmPick}
        contentLabel="Selected Task"
        closeTimeoutMS={200}
        className="modal"
    >
        <h3 className="modal__title">Selected Task</h3>
        {props.selectedTask && 
            <p className="modal__body">{props.selectedTask}</p>}
        <button className="button" onClick={props.onConfirmPick}>
            Okay
        </button>
    </Modal>

以上closeTimeoutMS={200}对关闭窗口的渐出效果有必要。

优化移动设备
对于小屏幕的手机,该 Todo App还需要进行优化。

更新public/index.html的头部,添加如下代码:

<meta name="viewport" content="width=device-width, initial-scale=1"> 

以上为移动设备扩展组件的占屏比例,已经优化了很多。
使用开发者工具的移动设备模拟,选择 iPhone 5可看到效果。

进一步的优化包括:
1. 为移动设备使用更小组件间隔,以节省屏幕空间;
2. 纵列任务输入框和提交按钮,并分别占满整行;

更新src/styles/base/_settings.scss文件添加:

$screen-width: 45rem;

以上定义了一个屏幕宽度的临界值,用于应用不同样式。

更新src/styles/components/_headers.scss添加:

@media (max-width: $screen-width) {
    .header {
        margin-bottom: $s-size;
    }
}

以上重定义了屏幕窄于45rem时的标题下部留白宽度。

更新src/styles/components/_button.scss添加:

@media (max-width: $screen-width) {
    .big-button {
        margin-bottom: $s-size;
    }
}

以上为随机选择任务的大按钮做了同样的留白宽度设置。

更新src/styles/components/_add-task.scss添加:

@media (max-width: $screen-width) {
    .add-task {
        flex-direction: column;
    }

    .add-task__input {
        margin-right: 0;
        margin-bottom: $s-size;
    }
}

以上为定义了窄屏时任务输入框和提交按钮的宽度及间隔。

网站缩略标记
准备标记文件并保存到public/images文件加下。
更新public/index.html文件,在头部添加如下语句:

<link rel="icon" type="image/png" href="/images/favicon.png" />

刷新网页就可以看到更新的标记了。

备注
实际上,调整样式是很细致的工作,需要逐步和反复操作。
最好每修改一个组件的一个样式就保存,确认效果是否合意。
不是写好一堆样式定义再应用类,而是先应用类再做调整。

React App – Webpack

分离关注
没有 Webpack,所有文件都在 Public 文件夹中。
包括,HTML 和所有来自第三方与自编程的 JS 文件。
对于复杂和庞大的项目,传统方式的管理成本很高。

将第三方模块的 JS 放入 node_modules文件夹。
将自编程的 JS 组件文件放入src文件夹。
将以上所有 JS 文件处理成一个bundle.js文件。
在 HTML 主页文件中只需插入一个 SCRIPT 引用。
以上就是 Webpack 做的工作,简化管理提升效率。

全局模块
程序运行依赖的,通过-g安装的模块为全局模块。
要尽量避免使用全局模块,有以下原因:

  1. 不便协作:程序本身没有全局模块的记录
  2. 不便管理:全局模块的版本信息没有记录。
  3. 不便使用:需要通过命令行运行相关命令。
    到目前未知,Todo App安装了两个全局模块:
  4. live-server: 提供Web服务以运行程序
  5. babel: 编译 JSX 文件到 React 标准文件
    卸载这两个全局模块:
    $ npm uninstall -g babel-cli live-server
    中国大陆地区用户可用 CNPM 命令。

重新安装以上两个模块到项目:
$ npm install live-server babel-cli@6.24.1 --save
同样,中国大陆用户可以用 CNPM 命令。
要运行这两个模块,编辑package.json如下:

{
  "name": "05_todo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "server": "live-server public/",
    "build": "babel src/todo.js --out-file=public/scripts/app.js --presets=env,react --watch" 
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "babel-cli": "^6.24.1",
    "babel-preset-env": "^1.5.2",
    "babel-preset-react": "^6.24.1",
    "live-server": "^1.2.1"
  }
}

注意以上scripts部分是手动添加的快捷脚本。
运行npm run <script_name>就等于后面的命令。
运行命令方便了,但要修改命令也在这里操作。

再回顾一下项目模块相对全局模块的好处:

  1. 定义了所有依赖模块,可用一条命令全部安装。
  2. 各模块版本定义清楚,避免了兼容性问题。
  3. 可定义脚本,更便于执行复杂或冗长的命令。

安装和配置 Webpack
安装$ npm install webpack@3.0.1 --save

创建本节练习文件src/module.js:

console.log('app is running!!');

这里为初步测试,只在控制台写下提示信息。

在项目根目录创建webpack.config.js配置文件:

const path = require('path');
// 可运行`node webpack.config.js`查看结果
// console.log(__dirname);
module.exports = {
    // 指定输入的 JS 文件,可使用相对路径
    entry: './src/module.js',
    // 指定输出的 JS 文件,必须用绝对路径
    output: {
        path: path.join(__dirname,'public'),
        filename: 'bundle.js'
    }
};

以上基础配置设置了输入输出文件及路径。

更新public/index.html文件如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Todo App</title>
    </head>
    <body>
        <div id="app"></div>
        <script src="bundle.js"></script>
    </body>
</html>

删除了 React、ReactDOM 和 app.js 的引用。
添加了 bundle.js 的引用。

更新package.jsonscripts部分:

  "scripts": {
    "server": "live-server public/",
    "build-babel": "babel src/todo.js --out-file=public/scripts/app.js --presets=env,react --watch",
    "build": "webpack --watch"
  },

运行npm run build触发webpack --watch命令。
到 live-server 的主页,查看控制台信息。
修改src/module.js中的提示信息,保存,确认。

以上操作完成了 webpack 的基本配置。

ES6导入导出
通过拆分大块程序到模块,可优化代码结构。
模块即独立JS文件,通过导入导出功能连接。
创建src/utils.js文件:

console.log('utils is running!');
// 模块内方法可批量导出
const square = (x) => x * x;
const add = (a, b) => a + b;
export { square, add };
// 也可以独立导出
export const isAdult = (age) => age >= 18;
export const canDrink = (age) => age >= 21;

更新src/module.js文件:

console.log('app is running!');
// 无论导出方式,导入方式是类似的
import { square, add, isAdult, canDrink } from './utils.js';
console.log(square(4));
console.log(add(100, 23));
console.log(isAdult(16));
console.log(canDrink(22));

安装和使用模块
$ cnpm install validator@8.0.0 --save
$ cnpm install react@16.0.0 react-dom@16.0.0 --save
$ cnpm install babel-core@6.25.0 babel-loader@7.1.1 --save
更新src/module.js文件:

console.log('app is running!');

import youguess, { square, add, isAdult, canDrink }
    from './utils.js';
console.log(square(4));
console.log(add(100, 23));
console.log(isAdult(16));
console.log(canDrink(22));
console.log(youguess(100, 56));

import validator from 'validator';
console.log(validator.isEmail('test'));
console.log(validator.isEmail('test@test.com'));
console.log(validator.isISBN('978-7-5217-0106-7'));

import React from 'react';
import ReactDOM from 'react-dom';
// 没有使用 Babel 时,只能直接用以下 React 方法
const template = React.createElement('p', {}, 'testing!');
ReacctDOM.render(template, document.getElementById('app'));

第三方模块的导入,不需指定相对路径。
对比bundle.js文件大小,可以发现:
即便安装了模块,不导入则不写入bundle.js

配置 Webpack 使用 Babel
更新webpack.config.js使用 Babel 模块:

const path = require('path');
module.exports = {
    entry: './src/module.js',
    output: {
        path: path.join(__dirname,'public'),
        filename: 'bundle.js'
    },
    module: {
        rules: [{
            // 指定使用的加载器
            loader: 'babel-loader',
            // 指定监控的文件类型 .js
            test: /\.js$/,
            // 从监控中排除部分文件
            exclude: /node_modules/
        }]
    }
};

注意以上添加的module部分对 Babel 的配置。
另外,需要创建指定preset.babelrc文件:

{
    "presets": ["env", "react"]
}

对比以上配置与一般 Babel 命令行的类似之处:
$ babel src/module.js --out-file=public/scripts/app.js --presets=env,react --watch"
现在可以更新src/module.js文件使用 JSX:

…
// 可以不在使用如下繁复的 React 方法
// const template = React.createElement('p', {}, 'testing!');
// Babel 会直接将如下简洁的 JSX 编译为标准 JS
const template = <p>JSX FROM WEBPACK!</p>;
ReactDOM.render(template, document.getElementById('app'));

重新运行npm run build,到程序主页确认结果。

拆分组件到文件
建立文件夹src/playground存放练习文件。
将目前src下所有文件放入以上文件夹。
建立文件夹src/components存放主程序组件。
新建src/app.js拷贝原todo.js中的代码。

AddTask组件
新建src/components/AddTask.js文件。
src/app.js剪切AddTask组件并更新如下:

// 在原代码基础上添加如下导入导出命令即可
import React from 'react';
// 组件体代码没有变化
…
export default AddTask;

更新src/app.js头部如下:

import React from 'react';
import ReactDOM from 'react-dom';
import AddTask from './components/AddTask';

更新webpack.config.js中的entry部分:

entry: './src/app.js',

编译程序:$ npm run build
到程序主页确认程序功能依然正常。

按照以上方法拆分如下组件:
• Header
• Action
• Tasks
• Task
• AddTask
注意,Task需要导入到Tasks组件中。
最后,TodoApp也可以抽取出来。
注意,如上操作中导入路径需要修改。

开发工具增强
Webpack 将整个程序被编译到bundle.js文件。
错误提示也只指向该文件,排错会变得困难。
为找到引发错误的原代码,更新 Wepback 配置。
这里使用 devtool 的 source map 功能:

// 添加在 module 部分的下面
devtool: 'cheap-module-eval-source-map'

重新运行$ npm run build,更新src/app.js:

import React from 'react';
import ReactDOM from 'react-dom';
import TodoApp from './components/TodoApp';
// 添加如下控制台信息,用于测试追溯源代码
console.log('source map');

ReactDOM.render(<TodoApp />, document.getElementById('app'));

使用 devtool 前,以上信息会追溯到bundle.js
使用 devtool 后,可以准确追溯到 app.js
注意,使用 devtool 会大量增加bundle.js体量。

安装 Webpack 的 Dev Server:
$ cnpm install webpack-dev-server@2.5.1 --save
更新webpack.config.js文件:

// 添加如下Dev Server配置信息
    devServer: {
        contentBase: path.join(__dirname,'public')
    }

更新package.jsonscripts部分如下:

  "scripts": {
    "server": "live-server public/",
    "build": "webpack",
    "dev-server": "webpack-dev-server"
  },

从现在起,不必再分别运行 Live Server 和 Webpack。
终止上面两个服务,运行$ npm run dev-server即可。

注意,Dev Server 生成的bundle.js文件保存在内存中。
本地 public 文件夹中的bundle.js文件可有可无。
运行$ npm run build可生成新的本地bundle.js文件。

Babel 的TCP插件
Transform Class Properties 是 Babel 提供的插件。
可以简化 ES6 组件的构造函数,安装:
$ cnpm install babel-plugin-transform-class-properties@6.24.1 --save
更新.babelrc配置文件:

{
    "presets": ["env", "react"],
    "plugins": ["transform-class-properties"]
}

简单的说,类属性省略了构造函数和绑定操作:
• 原本需要绑定的函数,转换成箭头函数即可
• 原本要在构造函数中定义的 State,可直接定义
例如,对于AddTask.js文件中的如下部分代码:

    constructor(props) {
        super(props);
        this.onAddTask = this.onAddTask.bind(this);
        this.state = {
            error: undefined
        }
    }

    onAddTask(e) {
        e.preventDefault();
        const task = e.target.elements.task.value.trim();
        const error = this.props.onAddTask(task);
        this.setState(() => ({ error }));
        if (!error) {
            e.target.elements.task.value = '';
        }
    }

可以更新为:

    state = {
        error: undefined
    }

    onAddTask = (e) => {
        e.preventDefault();
        const task = e.target.elements.task.value.trim();
        const error = this.props.onAddTask(task);
        this.setState(() => ({ error }));
        if (!error) {
            e.target.elements.task.value = '';
        }
    }

以上代码在逻辑、结构和体量上同时得到了简化。
类似的更新也可以部署到TodoApp.js中。

React App – SFC

无状态函数组件
Stateless Functional Component (SFC)
之前介绍的所谓组件,或叫容器,是基于 JS 的类。
传统组件支持 Props 和 State,也需要 Render 方法。
由于 Render 方法资源成本较高,没有 State 的可用SFC。

SFC 是基于方法的组件,不支持 State 但支持 Props。
Props 从外部导入数据,SFC 不主动求变,不需要 Render。
介绍 SFC 和默认 Props 设置,新建src/stateless.js:

// 最外层容器组件,包含其他组件,使用 SFC 创建
const Person = (props) => {
    return (
        <div>
            <h1>Stateless Functional Component</h1>
            <Student name="Student" />
            <Teacher name="Teacher" age={props.age}/>
        </div>
    )
}
// 设置默认 Props 值,没有指定 Props 时将加载
Person.defaultProps = {
    age: 30
}
// 同样使用 SFC 创建的子组件,注意其代码简洁度
const Student = (props) => {
    return (
        <div>
            <h2>Student</h2>
            <p>Name: {props.name}</p>
            {props.age && <p>Age: {props.age}</p>}
        </div>
    )
}
// 传统组件,注意 render 方法和 props 引用方式
class Teacher extends React.Component {
    render () {
        return (
            <div>
                <h2>Teacher</h2>
                <p>Name: {this.props.name}</p>
                <p>Age: {this.props.age}</p>
            </div>
        )
    }
}
// 如果 Person 的 age 没有设置,则会用默认的 Props
ReactDOM.render(
    <Person age={25}/>, 
    document.getElementById('app')
);

由于 SFC 相对简介,可以提高程序运行效率。
默认 Props 设置可以使程序更健壮和有弹性。

React开发者工具
Chrome 或 Firefox 都有专门的 React 开发工具。
搜索 React Developer Tools 就可以找到。
安装后,可以在开发者工具中看到 React 页。
其中提供类似 Elements 页的 React 对象信息。

一个彩蛋是$r变量,可在 Console 中查看。
在 React 开发者工具页选中一个 React 对象。
开发者工具会将其自动将其保存到$r变量。
在 Console 中输入$r可查看该对象的细节。

再次简化代码
查看之前 TODO LIST 程序中清空列表的方法:

    onRemoveTasks() {
        this.setState(() => {
            return {
                tasks: []
            };
        });
    }

仅仅为了清空一个状态值不需要如上许多行:

    onRemoveTasks() {
        this.setState(() => ({tasks: []}));
    }

做了以上简化后,代码简洁了许多。
注意,用箭头函数返回对象要包裹括号。
否则,标识对象的花括号会被当作函数体标识。

localStorage 和 JSON
JavaScript 提供基于 localStorage 模块的本地存储服务。
注意,该功能将所有数据转换为字符串类型保存。
在开发者工具的控制台可以执行以下命令:

> localStorage.setItem('age',20);
< undefined
> localStorage.getItem('age');
< "20"
> localStorage.removeItem('age');
< undefined
> localStorage.getItem('age');
< null

注意赋值为整形的 age 变量返回的是字符串类型数值。

对于对象类,JavaScript 提供基于 JSON 模块的转换功能。
JSON (JavaScript Object Notation) JavaScript 对象标记类型。
JSON.stringify() 方法将字符串表示的对象转为 JSON 对象。
JSON.parese() 方法将 JSON 对象转换为 JavaScript 对象。

> JSON.stringify({ age: 26 });
< "{"age":26}"
> const json = JSON.stringify({ age: 26});
< undefined
> json
< "{"age":26}"
> JSON.parse(json);
< {age: 26}age: 26__proto__: Object
> JSON.parse(json).age
< 26

注意返回的对象值为整型,而不再是字符串类型。
理解 JSON.stringify 和 JSON.parse 相配合的用法。

Todo App 更新
使用以上介绍的方法,更新src/todo.js如下:

// 无状态函数组件 Stateless Functionl Component
// 对于没有 state 的组件,可以简化其代码
class TodoApp extends React.Component {
    // 构造函数可引入外部导入的 props 值
    constructor(props) {
        // 继承父类的 props
        super(props);
        this.onRemoveTasks = this.onRemoveTasks.bind(this);
        // 绑定新建的删除单个任务的函数到 this
        this.onRemoveTask = this.onRemoveTask.bind(this);
        this.onHandlePick = this.onHandlePick.bind(this);
        this.onAddTask = this.onAddTask.bind(this);
        // 使用外部导入的 props 定义初始 state
        this.state = {
            tasks: props.tasks
        }
    }
    // 只有基于类的组件才支持生命周期函数
    // 组件加载后执行的生命周期函数
    componentDidMount() {
        // 用 try 和 catch 防止非法数据造成程序崩溃
        try {
            // 从本地存储获取字符串方式保存的 tasks 对象
            const json = localStorage.getItem('tasks');
            // 将字符串方式保存的对象转换为 JS 对象
            const tasks = JSON.parse(json);
            // 如果任务列表不为空,则更新到状态
            if (tasks) {
                // 状态名与状态值变量名相同可简化定义方法
                this.setState(() => ({ tasks }));
            }
        } catch (e) {
            console.log(e)
        }
    }
    componentDidUpdate(props, state) {
        // 只有组件更新成功添加了任务,才保存任务列表到本地存储
        if (state.tasks.length !== this.state.tasks.length) {
            // 将程序中保存的 JS 对象转换为 JSON 对象本地保存
            const json = JSON.stringify(this.state.tasks);
            localStorage.setItem('tasks', json);
        }
    }
    // 在组件卸载前运行的生命周期函数
    componentWillUnmount() {
        console.log('will unmount');
    }
    onRemoveTasks() {
        this.setState(() => ({ tasks: [] }));
    }
    // 添加删除单个任务的函数
    onRemoveTask(target) {
        this.setState((state) => ({
            // 使用 filter 方法返回任务列表中除指定任务之外的任务
            tasks: state.tasks.filter((task) => task !== target)
        }));
    }
    onHandlePick() {
        // Math.floor 方法取整
        const randomNum = Math.floor(
            // 以下方法计算随机 index 值
            Math.random() * this.state.tasks.length);
        const task = this.state.tasks[randomNum];
        alert(`Let's do ${task}`);
    }
    // 该方法在输入错误时返回错误信息,正确时更新任务列表
    onAddTask(task) {
        if (!task) {
            return 'Enter Valid Value!'
        // 如果指定 task 值可以查到有效 index 则证明输入重复
        } else if (this.state.tasks.indexOf(task) > -1) {
            return 'Task already exists!'
        } else {
            this.setState((state) =>
                // concat 方法添加数组元素并返回一个新数组
                ({ tasks: state.tasks.concat([task]) }));
        }
    }

    render() {
        const title = 'TODO LIST';
        const subtitle = 'Make a wise plan!';

        return (
            <div>
                <Header
                    title={title}
                    subtitle={subtitle}
                />
                <Action
                    hasOptions={this.state.tasks.length > 0}
                    onHandlePick={this.onHandlePick}
                />
                <Tasks
                    tasks={this.state.tasks}
                    onRemoveTasks={this.onRemoveTasks}
                    // 将删除单个任务的方法传入任务列表的 props
                    onRemoveTask={this.onRemoveTask}
                />
                <AddTask
                    onAddTask={this.onAddTask}
                />
            </div>
        );
    }
};
// 为程序外层容器设置默认的任务列表状态值
TodoApp.defaultProps = {
    tasks: []
}

// 优化为无状态函数组件,省略了 render 方法
const Header = (props) => {
    return (
        <div>
            <h1>{props.title}</h1>
            <h2>{props.subtitle}</h2>
        </div>
    );
}
// 优化为无状态函数组件,省略了 render 方法
const Action = (props) => {
    return (
        <div>
            <button
                onClick={props.onHandlePick}
                disabled={!props.hasOptions}
            >
                Choose a task for me!
                </button>
        </div>
    );
}
// 任务列表外层容器,包含任务数量提示,清除列表按钮和任务列表
const Tasks = (props) => {
    return (
        <div>
            <p>
                {
                    props.tasks.length ?
                        `You have ${props.tasks.length} tasks:` :
                        'You do not have tasks!'
                }
            </p>
            <button onClick={props.onRemoveTasks}>
                Remove All
            </button>
            <ol>
                {
                    props.tasks.map((task) =>
                        <Task
                            key={Math.random()}
                            task={task}
                            // 将删除单个任务方法传入单任务 props
                            onRemoveTask={props.onRemoveTask}
                        />
                    )
                }
            </ol>
        </div>
    );
}
// 优化为无状态函数组件,省略了 render 方法
const Task = (props) => {
    return (
        <div>
            <li>{props.task}
                <button
                    onClick={(e) => 
                        props.onRemoveTask(props.task)}
                >
                    remove
                </button>
            </li>
        </div>
    );
}
// 添加任务的容器,包含错误信息和提交新任务的表单
class AddTask extends React.Component {
    constructor(props) {
        super(props);
        // 注意这里对本地定义的添加任务方法做绑定处理
        this.onAddTask = this.onAddTask.bind(this);
        // 在 state 中定义一个默认为空的错误信息值
        this.state = {
            error: undefined
        }
    }

    onAddTask(e) {
        e.preventDefault();
        // 清理输入任务数据中可能存在的前后空格字符
        const task = e.target.elements.task.value.trim();
        // 处理添加任务的操作,返回可能存在的错误信息
        const error = this.props.onAddTask(task);
        // 将返回的可能错误信息放入状态值
        this.setState(() => ({ error }));
        // 在无错误,即添加任务成功时,清空表单输入框
        if (!error) {
            e.target.elements.task.value = '';
        }
    }

    render() {
        return (
            // 在错误信息存在时输出错误提示信息
            <div>
                {this.state.error && <p>{this.state.error}</p>}
                <form onSubmit={this.onAddTask}>
                    <input type="text" name="task" />
                    <button>Add Task</button>
                </form>
            </div>
        );
    }
}

ReactDOM.render(<TodoApp />, document.getElementById('app'));

以上,所有更新和重点部分已经做了行内注释。

Counter App 练习
作为对本节内容的练习,更新 Counter 程序如下:

class Counter extends React.Component {
    constructor(props) {
        super(props);
        this.onAddOne = this.onAddOne.bind(this);
        this.onMinusOne = this.onMinusOne.bind(this);
        this.onReset = this.onReset.bind(this);
        this.state = {
            count: 0
        }
    }

    componentDidMount() {
        // 从本地存储获取计数器值并转换为整型
        const text = localStorage.getItem('count');
        const value = parseInt(text, 10);
        // 如果本地存储数据非法,转换结果会为 NaN
        if (!isNaN(value)) {
            // 确认转换结果为有效整数后写入 State
            this.setState(() => ({count: value}));
        }
    }

    componentDidUpdate(props, state) {
        // 判断只有在计数器发生变化时触发本地存储作业
        if (state.count !== this.state.count) {
            // 这里主要防止由无效的 reset 操作触发的资源浪费
            localStorage.setItem('count', this.state.count);
        }
    }

    onAddOne() {
        this.setState((state) => ({count: state.count + 1}))
    }

    onMinusOne() {
        this.setState((state) => ({count: state.count - 1}));
    }

    onReset() {
        this.setState(() => ({count: 0}));
    }

    render() {
        return (
            <div>
                <h1>Count: {this.state.count}</h1>
                <button onClick={this.onAddOne}>+1</button>
                <button onClick={this.onMinusOne}>-1</button>
                <button onClick={this.onReset}>reset</button>
            </div>
        );
    }
}

ReactDOM.render(
    <Counter/>, 
    document.getElementById('app'));

以上主要应用了生命周期函数和本地存储方法。

React – Component

React 程序用 Component 管理页面组件。
组件之间存在从属关系,需要提前设计。
例如我们的作业列表应用结构如下:

<TodoApp />
    <Header />
    <Action />
    <Tasks />
        <Task />
    <AddTask />

Props 和 State
Props 和 State 是组件很重要的两个特性。
简单的说:
• 他们都是对象类型
• 都可以用于生成页面内容
• 其变化都会引发页面刷新
• 但 Props 从外部传入
• 而 State 由组件自身定义
由于传入方式的差别,Props 不可修改。

基于类的组件
组件实际是通过 JS 的类实现的。
为练习,创建src/class.js文件:

// class
class Person {
    // 构造函数可以定义默认参数值
    constructor(name = 'Anonymous', age = 0) {
        this.name = name;
        this.age = age;
    }
    getGreeting() {
        return `Hi. I am ${this.name}.`;
    }
    getDescription() {
        return `${this.name} is ${this.age} year(s) old.`;
    }
}
// 使用传入的参数创建对象
const p1 = new Person('Person', 20);
console.log(p1.getGreeting());
console.log(p1.getDescription());
// 使用默认的参数创建对象
const p2 = new Person();
console.log(p2.getGreeting());
console.log(p2.getDescription());

// sub class
class Student extends Person {
    // 子类的构造函数必须继承父类
    constructor(name, age, major) {
        super(name, age);
        this.major = major;
    }
    // 使用 !! 可转换其他类型为逻辑值
    hasMajor() {
        return !!this.major;
    }
    getDescription() {
        // 注意这里对父类方法的引用
        let desp = super.getDescription();
        if (this.hasMajor()) {
            desp += ` Their major is ${this.major}.`;
        }
        return desp;
    }
}

const s1 = new Student('Student', 20, 'Computer Science');
console.log(s1.hasMajor());
console.log(s1.getDescription());

const s2 = new Student();
console.log(s2.hasMajor());
console.log(s2.getDescription());

class Traveler extends Person {
    constructor(name, age, home) {
        super(name, age);
        this.home = home;
    }
    getGreeting() {
        let greeting = super.getGreeting();
        if (!!this.home) {
            greeting += ` I'm visiting from ${this.home}.`;
        }
        return greeting;
    }
}

const t1 = new Traveler('Traveler', 20, 'Beijing');
console.log(t1.getGreeting());

const t2 = new Traveler();
console.log(t2.getGreeting());

需要注意的内容已经注释在代码里。
以上程序只在开发者工具控制台有反馈。

运行 live server 和 babel 如下:
$ live-server public
$ babel src/class.js --out-file=public/scripts/app.js --presets=react -w
live server 加载 public 下的 index.html
babel 将 class.js 以 react 标准编译为 app.js

局部刷新
React 引入 Props 和 State 可提升程序性能。
当二者有变化时,React 只刷新发生的变化。
改进之前的 counter 和 toggle 程序如下。
新建src/counter-state.js:

class Counter extends React.Component {
    constructor(props) {
        super(props);
        // 注意需要用 bind 关联自定义方法到 this
        this.onAddOne = this.onAddOne.bind(this);
        this.onMinusOne = this.onMinusOne.bind(this);
        this.onReset = this.onReset.bind(this);
        // 注意在构造函数中定义 state 对象
        this.state = {
            count: 0
        }
    }

    onAddOne() {
        // 注意 setState 函数用法
        this.setState((state) => {
            // 这里返回的是新的 state
            return {
                count: state.count + 1
            };
        })
    }

    onMinusOne() {
        this.setState((state) => {
            return {
                count: state.count - 1
            };
        })
    }

    onReset() {
        this.setState(() => {
            return {
                count: 0
            };
        })
    }

    render() {
        return (
            <div>
                <h1>Count: {this.state.count}</h1>
                <button onClick={this.onAddOne}>+1</button>
                <button onClick={this.onMinusOne}>-1</button>
                <button onClick={this.onReset}>reset</button>
            </div>
        );
    }
}

ReactDOM.render(<Counter />, document.getElementById('app'));

提取自 state 的 count 会在变化时由 react 自动刷新。
新建src/toggle-state.js文件:

class Toggle extends React.Component {
    constructor(props) {
        super(props);
        this.onToggle = this.onToggle.bind(this);
        this.state = {
            visibility: false
        }
    }

    onToggle() {
        this.setState((state) => {
            return {
                visibility: !state.visibility
            };
        });
    }

    render() {
        return(
            <div>
                <h1>Visibility Toggle</h1>
                <button onClick={this.onToggle}>
                    {this.state.visibility ? 
                        'Hide Detail' : 'Show Detail'}
                </button>
                {this.state.visibility && 
                    <p>Here is the detail!</p>}
            </div>
        );
    }
}

ReactDOM.render(<Toggle />, 
    document.getElementById('app'));

以上优化后的 toggle 程序与 counter 类似。
注意其中对 state 定义、更新和应用。
验证以上程序需要重新运行以下 babel 命令:
$ babel src/counter-state.js --out-file=public/scripts/app.js --presets=react -w
$ babel src/toggle-state.js --out-file=public/scripts/app.js --presets=react -w

类似的,优化 TODO LIST 程序src/todo.js

// 最外层的组件容器,包含容器架构和大部分的方法定义
class TodoApp extends React.Component {
    constructor(props) {
        super(props);
        this.onRemoveTasks = this.onRemoveTasks.bind(this);
        this.onHandlePick = this.onHandlePick.bind(this);
        this.onAddTask = this.onAddTask.bind(this);
        this.state = {
            tasks: ['Task One', 'Task Two', 'Task Four']
        }
    }
    // 清空任务列表的方法
    onRemoveTasks() {
        this.setState(() => {
            return {
                tasks: []
            };
        });
    }
    // 随机选取任务的方法
    onHandlePick() {
        const randomNum = Math.floor(
            Math.random() * this.state.tasks.length);
        const task = this.state.tasks[randomNum];
        alert(`Let's do ${task}`);
    }
    // 添加任务到列表的方法
    onAddTask(task) {
        // 在输入值为空或重复时返回错误信息
        if (!task) {
            return 'Enter Valid Value!'
        } else if (this.state.tasks.indexOf(task) > -1) {
            return 'Task already exists!'
        } else {
            this.setState((state) => {
                return {
                    // concat 方法不改原数组返回新数组
                    tasks: state.tasks.concat([task])
                }
            });
        }
    }

    render () {
        const title = 'TODO LIST';
        const subtitle = 'Make a wise plan!';

        // 注意这里将变量、方法或 state 通过 props 传入组件
        return (
            <div>
                <Header title={title} subtitle={subtitle} />
                <Action
                    hasOptions={this.state.tasks.length > 0}
                    onHandlePick={this.onHandlePick}
                />
                <Tasks
                    tasks={this.state.tasks}
                    onRemoveTasks={this.onRemoveTasks}
                />
                <AddTask 
                    onAddTask={this.onAddTask}
                />
            </div>
        );
    }
};
// 头部只包含标题和副标题信息
class Header extends React.Component {
    render () {
        return (
            <div>
                <h1>{this.props.title}</h1>
                <h2>{this.props.subtitle}</h2>
            </div>
        );
    }
}
// 提供随机选择任务的按钮,在无任务时禁用
class Action extends React.Component {
    render () {
        return (
            <div>
                <button 
                    onClick={this.props.onHandlePick}
                    disabled={!this.props.hasOptions}
                >
                    Choose a task for me!
                </button>
            </div>
        );
    }
}
// 任务列表外层容器,包含任务数量提示,清除列表按钮和任务列表
class Tasks extends React.Component {
    render () {
        return (
            <div>
                <p>
                {
                    this.props.tasks.length ?
                    `You have ${this.props.tasks.length} tasks:` :
                    'You do not have tasks!'
                }
                </p>
                <button onClick={this.props.onRemoveTasks}>
                    Remove All
                </button>
                <ol>
                {
                    this.props.tasks.map(
                        (task) => <Task key={Math.random()} task={task} />
                    )
                }
                </ol>
            </div>
        );
    }
}
// 单个任务的子容器
class Task extends React.Component {
    render () {
        return (
            <div>
                <li>
                    {this.props.task}
                </li>
            </div>
        );
    }
}
// 添加任务的容器,包含错误信息和提交新任务的表单
class AddTask extends React.Component {
    constructor(props) {
        super(props);
        this.onAddTask = this.onAddTask.bind(this);
        this.state = {
            error: undefined
        }
    }

    onAddTask(e) {
        e.preventDefault();
        const task = e.target.elements.task.value.trim();
        const error = this.props.onAddTask(task);

        this.setState(() => {
            return {
                error: error
            };
        });
    }

    render () {
        return (
            <div>
                {this.state.error && <p>{this.state.error}</p>}
                <form onSubmit={this.onAddTask}>
                    <input type="text" name="task" />
                    <button>Add Task</button>
                </form>
            </div>
        );
    }
}

ReactDOM.render(<TodoApp />, document.getElementById('app'));

以上程序综合了之前介绍的所有技术应用。
这一部分主要介绍了 Props 和 State 的用法。

React – Setup to Hello

准备工作,安装如下工具:
• Visual Studio Code 网页下载安装
○ Path Intellisense 插件
○ Babel ES6/ES7 插件
• Node.js 网页下载安装
• Yarn 运行$ npm install -g yarn

准备项目文件夹:
$ cd ~
$ mkdir Workspace
$ cd Workspace
$ mkdir todo_app
$ cd todo_app

准备基本项目文件:
public/index.html

<!DOCTYPE html>
<html>

    <head>
        <meta charset="UTF-8">
        <title>Todo App</title>
    </head>

    <body>
        This is my HTML file!
    </body>

</html>

以上是最基本的 index.html 主页:
• HTML 文档类型声明
• 头部指定字符集和标题
• 页面主体包含一串文字

准备基本服务器模块:
使用live-server提供基本 Web 服务。
$ yarn global add live-server
$ npm install -g live-server
$ live-server -v
$ live-server 1.2.1
使用yarnnpm中任一方式安装。
最后,查看版本号确认安装完成。

中国大陆用户可用 CNPM 代替 NPM:
npm install -g cnpm --registry=https://registry.npm.taobao.org
运行以上安装后,用 CNPM 命令如下:
$ npm install -g live-server
CNPM 使用国内服务器,安装更顺利。

测试服务器功能:
$ live-server public
以上命令指定 public 为 Web 服务目录。
直接使用其中的 index.html 生成页面。

导入 React 和 ReactDom 模块:
更新 index.html 文件的 body 部分如下:

<body>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="/scripts/app.js"></script>
</body>

同时创建 public/scripts/app.js 文件。

console.log('App.js is running!');

保存 index.html, 在 Web 页打开开发者工具。
切换到开发者工具的 Console 页面:
• 确认日志显示 App.js is running!
• 输入 React 和 ReactDom 确认模块加载成功。

理解 Babel 的必要性
更新 index.html 文件的 body 部分如下:

<body>
<div id="app"></div>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="/scripts/app.js"></script>
</body>

以上更新创建了一个 idappdiv 容器。
接着,更新 app.js 文件如下并保存:

console.log('App.js is running!');
// JSX - JavaScript XML
var templte = <h1 id="someid">This is JSX from app.js!</h1>;
var appRoot = document.getElementById('app');
ReactDOM.render(template, appRoot);

以上将 template 内容放到 appRoot 指定容器。
然而 Web 页面会有报错,无法识别 template 内容。

为了转换 JSX 到浏览器兼容代码,可到如下网站:
中国大陆用户专享:https://babel.docschina.org/
输入以下 JS 代码:

var templte = <h1 id="someid">This is JSX from app.js!</h1>;

得到浏览器兼容 JS 代码如下:

var templte = /*#__PURE__*/React.createElement(
"h1", {id: "someid"}, "This is JSX from app.js!");

实际上,JSX 总要被转换成浏览器可识别的 JS 才行。
以后会安装 Babel 模块来转换,这里先拷贝兼容代码。

更新 app.js 文件如下并保存:

console.log('App.js is running!');
// JSX - JavaScript XML
var templte = /*#__PURE__*/React.createElement(
    "h1", {id: "someid"}, "This is JSX from app.js!");
var appRoot = document.getElementById('app');
ReactDOM.render(template, appRoot);

现在到 Web 页面,确认程序已经可以正确运行。

安装和使用 Babel 模块
运行如下命令安装和确认 Babel 模块:
$ cnpm install -g babel-cli@6.24.1
$ babel --version
6.24.1 (babel-core 6.26.3)
以上安装了全局通用的 Babel 命令行工具。
下面安装程序专属的 Babel 模块:
首先初始化程序,创建 package.json 文件。
$ cnpm init
安装 Babel 预设环境和 React 模块:
$ cnpm install babel-preset-react@6.24.1 babel-preset-env@1.5.2 --save
注意需要 --save 参数更新 package.json 文件。
而安装完成后,项目文件有如下变化:
package.json 中多了 dependencies
• 项目文件夹中多了 node_modules
后者包含了大量的次级依赖库文件。

要使用 Babel,创建 JSX 源码 src/app.js

console.log('App.js is running!');
// JSX - JavaScript XML
var template = <h1>This is JSX from app.js!</h1>
var appRoot = document.getElementById('app');
ReactDOM.render(template, appRoot);

运行 Babel 命令:
$ babel src/app.js --out-file=public/scripts/app.js --presets=react
以上命令用 React 预设编辑 JSX 到 JS。
注意对源 JSX 文件和目标 JS 文件的指定。
查看编辑后的 JS 文件,与之前手动创建的相同。
为避免有更新时反复编译 JSX,可添加 -w 参数。
注意为了实时监控更新,终端需要保持运行状态。

更多 JSX 的细节

console.log('App.js is running!');

// JSX - JavaScript XML
var title = 'Todo App';
var para = 'There is some detail:';
var items = ['Item One', 'Item Two', 'Item Three']
var myObj = {
  title: title,
  para: para,
  items: items,
  showItems: false,
}
function getPara(thePara) {
  if (thePara) {
    return thePara
  } else {
    return 'Reserved for a paragraph...';
  }
}
function getItems(theItems) {
  if (true) {
    return (
      <ol>
        <li>{theItems[0]}</li>
        <li>{theItems[1]}</li>
        <li>{theItems[2]}</li>
      </ol>
    )
  } else {
    return undefined;
  }
}
var template = (
  <div>
    <h1>
      {myObj.title ?
      'Welcome to '+ myObj.title.toUpperCase():
      'Welcome!'
    }
    </h1>
    <p>{getPara(myObj.para)}</p>
    {myObj.showItems && getItems(myObj.items)}
  </div>
);

var appRoot = document.getElementById('app');
ReactDOM.render(template, appRoot);

以上最终返回的是 template,因此从他看起:
• 可直接返回 HTML 标签
• JSX 语句用 {} 标注
• 支持 boolean ? of_true : of_false 语法
• 可配合 {} 呼叫自定义方法
• 支持 boolean && statement 条件返回
• 自定义方法支持复杂条件语句

ES6 标准
作为新的 JS 标准,ES6 支持一些新的功能:
• 使用 let 和 const 代替 var 定义变量
• 使用 => 代替 function 定义方法
这里,重命名 src 下的 app.jsjsx.js
记得终止 Babel 的后台程序,重新运行:
$ babel src/es6.js --out-file=public/scripts/app.js --presets=react -w
新建 src/es6.js 文件用于这一部分的演示。

var nameVar = 'Ling';
nameVar = 'Ring';
console.log('nameVar', nameVar);
var nameVar = 'Sing';
console.log('nameVar', nameVar);

let nameLet = 'Ling';
nameLet = 'Ring';
console.log('nameLet', nameLet);
// let nameLet = 'Sing';

const nameConst = 'Ling';
console.log('nameCosnt', nameConst);
// nameConst = 'Ring';

function callPet() {
    const petName = 'Dudu';
    return petName;
}
const myPet = callPet();
// console.log(petName);
console.log(myPet);


let fullName = 'First Middle Last';
if (fullName) {
    var firstName = fullName.split(' ')[0];
    let middleName = fullName.split(' ')[1];
    const lastName = fullName.split(' ')[2];
    console.log(firstName + ' ' + middleName + ' ' + lastName)
}
console.log(firstName);
// Uncaught ReferenceError: middleName is not defined
// console.log(middleName);
// Uncaught ReferenceError: lastName is not defined
// console.log(lastName);

注意,以上注释掉的语句是会引发错误的部分。
局部变量和全局变量的区别这里不做解释。
注意 ES6 推荐使用 let 和 const 代替 var:
• 使用 var 声明的变量可以重复声明
• 使用 let 或 const 声明的变量不允许重复声明
• 使用 let 声明的变量可以重新赋值
• 使用 const 声明的变量不可以重新赋值
• 可以使用 const 的时候优先使用 const
○ 保护声明的变量不被意外修改

继续在src/es6.js中添加代码,测试ES6功能:

// es5 function
const squareEs5 = function (x) {
    return x * x;
};
console.log('square func with es5',squareEs5(8));

// still es5 function
function squareStillEs5(x) {
    return x * x;
};
console.log('square func with es5',squareStillEs5(8));

// es6 function
const squareEs6 = (x) => {
    return x * x;
}
console.log('square func with es6',squareEs6(8));

// es6 function
const squareStillEs6 = (x) => x * x;
console.log('square func with es6',squareStillEs6(8));

// practice
const fullname = "Bill Gates";
function firstNameEs5(x) {
    return x.split(' ')[0];
}
console.log(firstNameEs5(fullname));
const firstNameEs6 = (x) => {
    return x.split(' ')[0];
}
console.log(firstNameEs6(fullname));
const firstNameStillEs6 = (x) => x.split(' ')[0];
console.log(firstNameStillEs6(fullname));

const addEs5 = function(x, y) {
    console.log(arguments)
    return x + y;
}
console.log(addEs5(1, 2, 3));

const addEs6 = (x, y) => {
    // arguments is not defined
    // console.log(arguments)
    return x + y;
}
console.log(addEs6(4, 5, 6));

const user = {
    name: 'Ling',
    cities: ['Beijing', 'Shanghai', 'Shenzhen'],
    // following es5 syntax works
    // action: function() {
    // following es6 syntax does not work
    // action: ()=> {
    action() {
        console.log(this.name);
        console.log(this.cities);
        this.cities.forEach(function(city) {
            console.log(this.name + ' lived in ' + city);
        })
        this.cities.map((city) => {
            console.log(this.name + ' visited ' + city);
        })
    }
}
user.action();

// practice
const multiplier = {
    numbers: [1, 2, 3, 4],
    by: 3,
    multiply() {
        return this.numbers.map((num) => num * this.by);
    }
}
console.log(multiplier.multiply());

以上代码中,注意:
• ES5 的两种函数定义方法
• ES6 的=>函数定义方法
=>函数不支持arguments
forEachmap的用法
• 对象内函数的创建方法

现在回过头来再更新src/jsx.js
保持原来的代码,添加如下部分:

// for template two
let count = 0;
const id = 'buttonId';
const addOne = () => {
  console.log('add one')
};
const minusOne = () => {
  console.log('minus one')
};
const reset = () => {
  console.log('reset')
};
const templateTwo = (
  <div>
    <h1 id={id} className="counter">
      Count: {count}
    </h1>
    <button onClick={addOne}>+1</button>
    <button onClick={minusOne}>-1</button>
    <button onClick={reset}>Reset</button>
  </div>
);

const appRoot = document.getElementById('app');
ReactDOM.render(templateTwo, appRoot);

以上代码创建了templateTwo并放到主页上。
其中包含一个计数器,注意idclass用法。
加一,减一和重置的三个函数目前只占位。
要看到结果,需要重新运行babel命令:
$ babel src/jsx.js --out-file=public/scripts/app.js --presets=react -w

要使三个按钮工作,需要再次更新代码:

// for template two
let count = 0;
const id = 'buttonId';
const addOne = () => {
  count++;
  renderCounter();
};
const minusOne = () => {
  count--;
  renderCounter();
};
const reset = () => {
  count = 0;
  renderCounter();
};
const appRoot = document.getElementById('app');
const renderCounter = () => {
  const templateTwo = (
    <div>
      <h1 id={id} className="counter">
        Count: {count}
      </h1>
      <button onClick={addOne}>+1</button>
      <button onClick={minusOne}>-1</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
  ReactDOM.render(templateTwo, appRoot);
}
renderCounter();

注意,以上三个按钮的函数都做了操作:
• 更新计数器值
• 重新render计数器
作为初始render,程序末尾也需render一次。
反复运行ReactDOM.render页面才会更新。

表单和提交
在之前src/jsx.js列表的基础上新增表单。
这里创建src/form.js文件:

// 创建 app 对象,包括标题和任务
const app = {
    title: 'TODO LIST',
    subtitle: 'Make a wise plan!',
    tasks: []
};

// 定义表单提交方法
const onFormSubmit = (e) => {
    // console.log(e);
    // 禁用提交表单内容到 URL
   // 可注释掉下一句看提交后的 URL
    e.preventDefault();
    // 获得表单提交事件的提交值
    const task = e.target.elements.task.value;
    // 判断是否提交值为空,空值不执行
    if (task) {
        // 将提交值添加到 tasks 数组
        app.tasks.push(task);
        // 提交后清空表单
        e.target.elements.task.value = '';
    }
    // 重新加载模板到页面
    renderTemplate();
};

// 定义清空 tasks 数组的方法
const onRemoveAll = () => {
    app.tasks = [];
    renderTemplate();
};

// 定义随机选择任务的方法
const onMakeDecision = () => {
    const randomNum = Math.floor(Math.random() * app.tasks.length);
    const task = app.tasks[randomNum];
    alert("Let's do " + task + ' !');
};

// 从页面获得 ID 为 app 的 HTML 对象
const appRoot = document.getElementById('app');

// 定义生成模板并刷新到页面的方法
const renderTemplate = () => {
    // 定义模板
    const template = (
        <div>
            <h1>{app.title}</h1>
            // 在副标题存在时添加到 <p> 并显示
            {app.subtitle && <p>{app.subtitle}</p>}
            // 根据 tasks 数组内容有无显示不同内容
            <p>{app.tasks.length > 0 ? 
                'You have ' + app.tasks.length + ' Tasks:' : 
                'Great! No Tasks!'}</p>
            // 创建一个随机选择任务的按钮
            <button 
                // 在任务列表为空时禁用该按钮
                disabled={app.tasks.length === 0}
                onClick={onMakeDecision}
            >
                Choose one for me!
            </button>
            // 创建一个清空 tasks 数组的按钮
            <button onClick={onRemoveAll}>Remove All</button>
            // 任务列表,将 tasks 数组映射到 <li>
            <ol>
                {
                    app.tasks.map((task) => {
                        return <li key={Math.random()}>{task}</li>
                    })
                }
            </ol>
            // 创建一个可提交新建任务的表单
            <form onSubmit={onFormSubmit}>
                <input type="text" name="task"/>
                <button>Add Task</button>
            </form>
        </div>
    );
    // 刷新页面
    ReactDOM.render(template, appRoot);
};

// 在页面中显示初始模板
renderTemplate();

由于注释较多,为方便查看放在了代码中间。
以上每次刷新模板为全内容刷新,只为演示。
实际上,React 会选择性刷新有更新的部分。

收尾,创建一个翻转小程序src/toggle.js

let visible = false;
// 定义翻转方法
const toggle = () => {
    // 翻转可见条件
    visible = !visible;
    // 重新加载页面
    render();
}

const render = () => {
    const template = (
        <div>
            <h1>Visibility Toggle</h1>
            // 点击按钮时翻转可见条件
            <button onClick={toggle}>
                // 根据可见条件显示按钮文字
                {visible ? 'Hide Detail' : 'Show Detail'}
            </button>
            // 根据可见条件显示文字
            {visible && <p>Here is the detail!</p>}
        </div>
    );
    ReactDOM.render(
        template, 
        document.getElementById('app')
    );
};

render();

备注
以上程序运行时需要打开两个命令行窗口。
一个运行live-server public的 Web 服务。
另一个做 Babel 编译,可 -w 监视运行,如:
$ babel src/toggle.js --out-file=public/scripts/app.js --presets=react -w

React App – Transition

本部分是学习动画和样式控制的干燥内容。
代码本身只有技术意义,不构成项目。

如下位置下载模板,解压缩到工作空间:
https://github.com/ferlobo1985/react_fullstack/tree/3_transitions_template
安装依赖 $ npm install
安装 Transition 模块 $ npm install react-transition-group --save
运行服务器 $ npm start

动态效果
更新 src/components/Transition.js

import React, { Component } from 'react';
import '../css/App.css';

class TransitionComp extends Component{
    state = {
        show: true
    }

    toggle = () => {
        this.setState({
            show: !this.state.show
        })
    }

    render(){
        return(
            <div>
                {
                    this.state.show ?
                    <div style={{
                        background:'red', 
                        height:'100px'}}/>
                    : null
                }
                <div className="showDiv"
                    onClick={this.toggle}>
                    Show or Hide
                </div>
            </div>

        )
    }
}

export default TransitionComp;

以上创建了一个可显隐的红色背景框。

动画效果
现在使用 Transition 模块实现动画功能:

import React, { Component } from 'react';
import Transition from 'react-transition-group/Transition';
import '../css/App.css';

class TransitionComp extends Component{
    state = {
        show: true
    }

    toggle = () => {
        this.setState({
            show: !this.state.show
        })
    }

    render(){
        return(
            <div>
                <Transition
                    in={this.state.show}
                    timeout={2000}
                    mountOnEnter
                    unmountOnExit
                >
                    {state=>
                        <div style={{
                            background:'red',
                            height:'100px',
                            transition:'all 2s ease',
                            opacity: state==='entered'? 1:0
                        }}>
                            {state}
                        </div>
                    }           
                </Transition>
                <div className="showDiv"
                    onClick={this.toggle}>
                    Show or Hide
                </div>
            </div>
        )
    }
}

export default TransitionComp;

注意以上 Transition 组件的属性设置,其中:

  • in 设置动画进入的标志,这里为 show 状态
  • timeout 为动画时长,要与 style 中设置一致
  • mountOnEnter 表示动画进入时加载对象
  • unmountOnExit 表示动画退出时卸载对象

组件 Transition 需要配合对象的 Style 设置。
这里主要是 transition 和 opacity 配合使用。
其中 transition 时间要与前者 timeout 一致。

类的动画
以上使用行内样式动画,这里介绍 CSS 类。
更新 transition.js 中 Transition 部分:

                <Transition
                    in={this.state.show}
                    timeout={{
                        enter: 5000,
                        exit: 2000
                    }}
                    enter={true}
                    exit={true}
                    onEnter={(node)=>{
                        console.log(node)
                    }}
                    onExit={(node)=>{
                        console.log(node)
                    }}
                >
                    {state=>
                        <div className={`square square-${state}`}>
                            {`square square-${state}`}
                        </div>
                    }
                </Transition>

为了看到完整动画效果,移除了 mount 相关属性。
对 timeout 也做了更详细的设置,进入和退出不同。
开关 enter 和 exit 可控制是否开启”过程中”状态。
同时,也可以用 onEnter 和 onExit 触发定义动作。
另外,CSS 类名称使用动画 state 信息动态生成。
简化对象样式定义为 CSS 类定义,更新 App.css

.square {
  background: red;
  height: 100px;
  opacity: 1;
  transition: all 2s ease;
}

.square-entering {
  background: orange;
  opacity: 1;
  transform: translateX(0%)
}

.square-entered {
  background: greenyellow;
}

.square-exiting {
  background: blue;
}

.square-exited {
  background: purple;
  opacity: 0;
  transform: translateX(-100%)
}

以上 CSS 类定义了每个动画状态样式。
translateX 定义了 X 轴方向动画效果。

PropTypes
定义模板,用它来检测属性是否合规。
首先Ctrl + C终止服务器的运行,安装组件:
$ npm install prop-types --save
创建如下用户模板文件:
src/components/user_template.js

import React from 'react';
import PropTypes from 'prop-types';

const UserTemplate = (props) => {
    // console.log(props)
    return (
        <div>
            UserTemplate
        </div>
    );
};

UserTemplate.propTypes = {
    firstname: PropTypes.string,
    lastname: PropTypes.string,
    age: PropTypes.number,
    //actions: PropTypes.array,
    actions: PropTypes.arrayOf(PropTypes.string),
    active: PropTypes.bool,
    message: PropTypes.func,
    car: PropTypes.object,
    language: PropTypes.oneOf(['English','Spanish']),
    userid: PropTypes.oneOfType([
        PropTypes.string,
        PropTypes.number
    ]).isRequired,
    type: PropTypes.string.isRequired,
    custom:function(props,propName,ComponentName){
        console.log(props,propName,ComponentName);
        if(props[propName] === 'have') {
            console.log('have custom!')
        } else {
            return new Error(`The value of ${propName} should be "have"`)
        }
    }
}

export default UserTemplate;

与以上模板文件相配合,创建如下用户实例:
src/components/user.js

import React, { Component } from 'react';
import UserTemplate from './user_template';

class User extends Component {

    state = {
        firstname: 'Firstname',
        lastname: 'Lastname',
        age: 25,
        actions: ['run','jump'],
        active: false,
        message(){console.log('hey')},
        car:{brand:"Toyota",model:"Carolla"},
        language: 'Japanese',
        userid: '20200001', // '20200001'
        custom: 'hav',
        color: 'red'
    }

    changeColor(){
        // this.setState({
        //     color:'blue'
        // })
        this.refs.inColor.style.color = 'blue'
    }

    render() {
        const style = {
            color: this.state.color
        }
        return (
            <div>
                <h4 style={style} ref="inColor">
                    User ID: {this.state.userid}
                </h4>
                <div onClick={()=>this.changeColor()}>
                    Change Color
                </div>
                <UserTemplate {...this.state}/>
            </div>
        );
    }
}

export default User;

以上用户实例定义的状态可在模板中检查。
包括数据类型、指定值、必须项、自定义等。
在 App.js 中创建 User 的 Link 和 Route。

除了使用模板检查数据类型,还用了 refs
注意,使用refs与修改状态得到的效果相同。
React Native 推荐使用修改状态,不推荐refs

React App – Router and LC

这部分学习多页面之间的路径导航配置和组件的生命周期。

创建程序
$ mkdir 02_router
$ cd 02_router
$ create-react-app .
$ npm install react-router-dom –save
删除 index.js 和 index.html 外 public 和 src 中其他文件。
其中,index.js 的文件内容也需要清空,并更新为如下:

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

 const App = () => {
   return (
     <div>Home</div>
   )
 }

 ReactDOM.render(
  <App/>,
  document.querySelector('#root')
)

以上只是创建了组件的占位符和基本结构代码。

组件结构
创建组件目录src\components ,添加如下三个组件文件:
home.js posts.js profiles.js 使用如下的初始代码:

import React from 'react';

const Profiles = () => {
    return (
        <div>
            Profiles
        </div>
    )
}

export default Profiles;

以上是 Profile 的代码,Home 和 Posts 修改相应文字。

加入导航
更新 index.js 文件,导入 router 模块和其他组件:

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route } from 'react-router-dom';

// COMPONENTS
import Home from './components/home';
import Posts from './components/posts';
import Profile from './components/profile';

 const App = () => {
   return (
     <BrowserRouter>
      <div>
        <header>
          Header
        </header>
        <Route path="/" exact component={Home}/>
        <Route path="/posts" component={Posts}/>
        <Route path="/profile" component={Profile}/>
      </div>
     </BrowserRouter>
   )
 }

 ReactDOM.render(
  <App/>,
  document.querySelector('#root')
)

组件 BrowserRouter 和 Route 分别作容器和导航之用。
类似 return 方法,容器只能包含一个 HTML tag 对象。
导航组件 exact 属性强制路径匹配,可删掉查看效果。
在导航容器中加入的 header 组件会出现在所有页面上。

除了 BrowserRouter 还有 HashRouterMemoryRouter
前者在 URL 中添加 # 标记,后者则除根路径不显示具体路径。
可以直接替换以上代码中的 BrowserRouter 查看结果。

最后,在浏览器地址栏输入以下地址查看结果:

  • http://localhost:3000/
  • http://localhost:3000/posts
  • http://localhost:3000/profile

路径选择
以上 Route 代码中,添加 exact 是为实现精确的路径匹配。
否则 path="/posts" 也匹配 path="/",组件将重复显示。
这里可用的解决方案除了 exact 属性还有 Switch 组件:

…
import { BrowserRouter, Route, 
  NavLink, Switch } from 'react-router-dom';
…
        <Switch>
          <Route path="/posts/:id/:username" component={PostItem}/>
          <Route path="/profile" component={Profile}/>
          <Route path="/posts" component={Posts}/>
          <Route path="/" component={Home}/>
        </Switch>
…

注意 Switch 组件中的 Route 顺序,精细靠前模糊靠后。
因为 Switch 只返回第一个匹配的 Route,选项顺序重要。

创建链接
通过 react-router-dom 的 Link 模块,可轻松创建链接。
更新 index.js 中的如下语句:

…
import { BrowserRouter, Route, 
  Link } from 'react-router-dom';
…
        <header>
          <Link to="/">Home</Link><br/>
          <Link to="/posts">Posts</Link><br/>
          <Link to={{
            pathname:'/profile',
            hash: '#thehash',
            search: '?profile=true'
          }}>Profile</Link><br/>
          <hr/>
        </header>
…

Link 的语法很简单,有趣的是可添加 URL 参数。
分别打开链接查看结果,注意 profile 页面路径的参数。

除了 Link 还有 NavLink 可用,提供了更多功能,如:

          <NavLink 
            to="/posts"
            activeStyle={{color:'red'}}
            activeClassName="selected"
          >Posts</NavLink><br/>

以上为到 Posts 的链接设定选中时的 CSS 类和颜色样式。

动态参数
一般 URL 路径中存在 id 等动态参数,这里演示如下。
创建 post_item.js 组件文件,用于单个日志:

import React from 'react';

const PostItem = (props) => {
    console.log(props);
    let params = props.match.params;
    return (
        <div>
            {params.id} - {params.username}
        </div>
    )
}

export default PostItem;

以上接收来自 index 的 props,显示 id 和 username
控制台日志显示了 props 中传递的可用参数信息。

更新 index.js 代码的如下有关 PostItem 的部分:

…
// COMPONENTS
import Home from './components/home';
import Posts from './components/posts';
import Profile from './components/profile';
import PostItem from './components/post_item';
…
        </header>
        <Route path="/" exact component={Home}/>
        <Route path="/posts" exact component={Posts}/>
        <Route path="/posts/:id/:username" component={PostItem}/>
        <Route path="/profile" component={Profile}/>
      </div>

注意 Router 中对 id 和 username 的声明传递了 props

作为从 props 提取路径的另一个样例,更新 profile.js

import React from 'react';
import { Link } from 'react-router-dom';

const Profile = (props) => {
    return (
        <div>
            <Link to={{pathname: `${props.match.url}/posts`}}>
                take me to /profile/posts
            </Link>
        </div>
    )
}

export default Profile;

以上代码调用了 props 中的部分信息构造了一个 URL 链接。
其中 props.match.url 中保存的就是当前 Profile 页面地址。
构造的 /profile/posts 地址并不存在,点击也没有反应。

重定向页
继续上面的话题,有时候可能需要对页面做重定向处理。
例如,将上节中无效的 URL 地址重定向到有效的地址。
更新 index.js

…
import { BrowserRouter, Route, NavLink, 
  Switch, Redirect } from 'react-router-dom';
…
        <Switch>
          <Redirect from="/profile/posts" to="/posts"/>
          <Route path="/posts/:id/:username" exact component={PostItem}/>
          <Route path="/profile" component={Profile}/>
          <Route path="/posts" component={Posts}/>
          <Route path="/" exact component={Home}/>
          <Route render={()=> <h3> Page not found - 404</h3>}/>
          {/* <Route component={notFound}/> */}
        </Switch>
…

以上更新导入了 Redirect 模块功能,在 Swtich 中添加:
从无效 /profile/posts 到有效 /posts 的重定向
此外,所有根目录以上的无效 URL 会导航到 404 页面。
而更友好的备选方式是创建一个 notFound 组件。

注意,以上更新重新为根页面添加了 exact 属性。
其他没有添加 exact 属性的无效页面将默认匹配成功。

  • 也就是说,/asdf 将触发 404 页面。
  • /posts/asdf 将不会触发 404 页面。

另一种重定向的方式是直接修改 props 中的 history 值。
更新 profile.js,实现从 Profile 到 Home 的默认跳转:

import React from 'react';
import { Link } from 'react-router-dom';

const Profile = (props) => {
    //console.log(props);
    const redir = () => {
        props.history.push('/')
    }
    return (
        <div>
            <Link to={{pathname: `${props.match.url}/posts`}}>
                take me to /profile/posts
            </Link>
            {redir()}
        </div>
    )
}
export default Profile;

以上定义的 redir() 方法将 ‘/’ 添加到 props 的 history
该 Profile 页面打开时,redir() 方法自动定向页面到 /

生命周期
创建 lifecycle.js 组件初始文件:

import React, { Component } from 'react';

class Life extends Component {
    render() {
        return (
            <div>
                <h3>Life Cycles</h3>
            </div>
        )
    }
}

export default Life;

更新 index.js 文件,导入并建立 Life 的链接和导航:

// COMPONENTS
import Home from './components/home';
import Posts from './components/posts';
import Profile from './components/profile';
import PostItem from './components/post_item';
import Life from './components/lifecycle';

 const App = () => {
   return (
     <BrowserRouter>
      <div>
        <header>
          <NavLink to="/">Home</NavLink><br/>
          <NavLink 
            to="/posts"
            activeStyle={{color:'red'}}
            activeClassName="selected"
          >Posts</NavLink><br/>
          <NavLink to={{
            pathname:'/profile',
          }}>Profile</NavLink><br/>
          <NavLink to="/life">Life</NavLink>
          <hr/>
        </header>
        <Switch>
          <Redirect from="/profile/posts" to="/posts"/>
          <Route path="/posts/:id/:username" exact component={PostItem}/>
          <Route path="/profile" component={Profile}/>
          <Route path="/posts" component={Posts}/>
          <Route path="/life" component={Life}/>
          <Route path="/" exact component={Home}/>
          <Route render={()=> <h3> Page not found - 404</h3>}/>
          {/* <Route component={notFound}/> */}
        </Switch>
      </div>
     </BrowserRouter>
   )
 }

注意其中添加的有关 Life 的 import,NavLink 和 Route 语句。

更新 lifecycle.js 文件:

import React, { Component } from 'react';

class Life extends Component {
    // 1st step: get default props

    // 2nd step: get default state
    state = {
        title: 'Life Cycle'
    }

    // 3rd step: before render
    UNSAFE_componentWillMount() {
        console.log('before render');
        // document.querySelector('h3').style.color = 'red'
    }

    UNSAFE_componentWillUpdate() {
        console.log('before update')
    }

    componentDidUpdate() {
        console.log('after update')
    }

    shouldComponentUpdate(nexProps, nextState) {
        console.log(this.state)
        console.log(nextState);
        if(nextState.title === 'something else') {
            return false
        }
        return true;
    }

    UNSAFE_componentWillReceiveProps() {
        console.log('before receive props')
    }

    componentWillUnmount() {
        console.log('component unmount')
    }

    // 4th step: render jsx
    render() {
        console.log('RENDER')
        return (
            <div>
                <h3>{this.state.title}</h3>
                <div onClick={
                    // ()=> this.setState({title:'something else'})
                    ()=> this.setState({title:'title is changed'})
                }>Click to change the title!</div>
            </div>
        )
    }

    // 5th step: after render
    componentDidMount() {
        console.log('after render')
        document.querySelector('h3').style.color = 'red'
    }
}

export default Life;

注意,其中注释部分的说明,前后顺序不影响实际执行顺序。
第三步中的样式修改不可用,因为发生在 HTML render 之前。
有些方法将由于安全问题在未来被弃用,名称为 UNSAFE_xxx

关于更新和接收属性的方法都只以当前组件内部为作用域。
也就是说组件获得初始状态和属性不会触发相关方法。
点击 Safe 链接测试 UNSAFE_componentWillReceiveProps()
点击其他链接测试 componentWillUnmount()

遗留问题
如前述,UNSAFE_componentWillUpdate() 会在触发时更新组件。
触发条件有两种,在发生更新或从当前页重新加载当前组件时。
后一种情况实际没有发生任何组件内容更新,是一个问题。

这里可以更新 UNSAFE_componentWillUpdate() 方法:

    shouldComponentUpdate(nexProps, nextState) {
        if(nextState.title === this.state.title) {
            return false
        }
        return true;
    }

以上判断新旧属性是否有变化,有变化才更新组件内容。
但如果需要判断的属性过多也会带来问题…
所以 React 提供了 PureComponent 来解决这个问题。
导入时选用 PureComponent 而非 Component 即可。

用 React 的 Pure

条件渲染
有时需要按照条件生成组件,JSX 支持 value? case_t : case_f
类似以上 lifecycle.js,创建 conditional.js及其路径和导航。
更新 index.js 部分略过,conditional.js 使用如下内容:

import React, { Component } from 'react';

const showIt = (value) => {
    return ( value ?
        <div>Hello, it's true!</div>
        :
        <div>Sorry, it's false!</div>
    )
}

class Conditional extends Component {
    state = {
        value: true
    }

    render() {
        return (
            <div>
                <div>{showIt(this.state.value)}</div>
                <div onClick={
                    // ()=> this.setState({title:'something else'})
                    ()=> this.setState({value: !this.state.value})
                    }>Click to toggle the value!</div>
            </div>
        )
    }
}

export default Conditional;

以上设定一个逻辑值状态,根据值返回不同刷新组件内容。
使用前一节中介绍的类似方法,点击切换其值和组件内容。

返回数组
为了了解可以返回的数组是怎样,更新 posts.js 如下:

import React from 'react';
import { Link } from 'react-router-dom';

const Posts = () => {
    const data = [
        {id: '1', name: 'Post 1'},
        {id: '2', name: 'Post 2'},
        {id: '3', name: 'Post 3'},
    ]

    const list = data.map(item => {
        return (
            <span key={item.id}>
                <Link to={item.id}>{item.name}</Link><br/>
            </span>
        )
    })

    return [
        <div key="aList">
            {list}
        </div>,
        <div key="item1">Item 1</div>,
        <div key="item2">Item 2</div>
    ]
}

export default Posts;

以上返回一个数组,且第一项内嵌映射生成的数组。

高阶组件
全称 High Order Component,输入和输出组件。
船舰 Card 以嵌入其他组件,文件 src/hoc/card. is
这里需要重复嵌入的仅仅是一种灰色背景的格式。

import React from 'react';

const Card = (props) => {
    const style = {
        background: 'lightgrey'
    }

    return (
        <div style={style}>
            {props.children}
        </div>
    )
}

export default Card;

以上对传入的 children 属性添加灰色背景。
更新 Posts,为其返回数组应用 Card 组件:

import React from 'react';
import { Link } from 'react-router-dom';

import Card from '../hoc/card';

const Posts = () => {
…
    return [
        <Card key="aList">
            {list}
        </Card>,
        <div key="item1">Item 1</div>,
        <div key="item2">Item 2</div>
    ]
}

export default Posts;

如上,返回的第一个数组对象有灰色背景。

再看一个用 HOC 实现用户认证的场景。
创建文件 src/hoc/auth. is

import React from 'react';

const Auth = (props) => {
    const pass = 'P@ssw0rd';

    return ( 
        pass !=='P@ssw0rd' ?
        <h3>Not authorizded</h3>
        : props.children
    )
}

export default Auth;

以上模拟认证过程,对比定义变量和预设值。
相同则显示组件子属性,否则警告未认证。

这次把它用到 Home 组件中:

import React from 'react';

import Auth from '../hoc/auth';

const Home = () => {
    return (
        <Auth>
            Home
        </Auth>
    )
}

export default Home;

以上导入和应用自不待言。

再看一个例子,创建 src/component/user.js

import React from 'react';

import User from '../hoc/user';

const User1 = (props) => {
    console.log('User1:', props)
    return (
        <div>
            User 1
        </div>
    )
}

const User2 = (props) => {
    console.log('User2:', props)
    return (
        <div>
            User 2
        </div>
    )
}

export default User(User1, User2, 'Hello', 'Bonjour');

index.js 导入和创建导航与路径的操作略过。
以上创建两个用户连同两个参数传给 HOC对象。

以下为 src/hoc/user.js

import React from 'react';

const User = (User1, User2, arg1, arg2) => {
    return (props) => (
        <div>
            {arg1} <User1 {...props}/>
            {arg2} <User2 {...props} />
        </div>
    )
}

export default User;

以上读入两个用户和参数,按指定方式返回。
注意在用户对象中指定 props 以保留属性。

为什么需要做以上看似多余的 HOC 操作。
为了实现 Separate Of Concern (SOC)

安装错误
运行 npm install 安装库,可能遇到错误:
npm ERR! code ETIMEDOUT
解决办法是删除可能存在的代理:
npm config delete proxy
npm config delete https-proxy

The complete React Fullstack course – React basics

2017年,为了完成作业,硬着头皮学了 React 和 React Native 的两套课程。
并没有弄清楚一些具体概念,过去了这些年再重新来看,好像更明白了一点。
这个课程也是录制于 2017 年,以下是第一部分的笔记,关于 React 基础。

最终程序是一个简单的新闻检索页面,根据输入的关键字动态检索新闻标题。
程序本身没有特别的价值,过程中可以复习有关 React 和 ES6 的基础知识。

创建程序
$ npm install -g create-react-app
$ create-react-app my-app
Or
$ mkdir my-app
$ cd my-app
$ create-react-app .
从模板创建程序
$ npx create-react-app my-app –template [template-name]
选择包管理器
$ npx create-react-app my-app –use-npm

运行程序
$ cd my-app
$ npm start
可选的其他运行方式
$ npx create-react-app my-app // npm 5.2+
$ npm init react-app my-app // npm 6+
$ yarn create react-app my-app // Yarn 0.25+
运行模式
$ npm start // dev mode
$ yarn start // dev mode
$ npm test // test mode
$ yarn test // test mode
$ npm run build // bundle in prod mode
$ yarn build // build for prod

打包到生产模式后,所有 JS 源码会封装到 bundle.js 文件中,由 Webpack 和 Barbel 完成。
项目目录结构中,node_modules 包含所有依赖的库文件,描述在 package.json 文件中。

以下文件名不可修改:
• public/index.html Web 程序页面模板
• src/index.js Web 程序的 JS 代码入口
除了以上两个文件外,其他文件名称可以修改。
只有 public 文件夹中的文件才可以被 public/index.html 访问和使用。

由于有 JSX 的帮助,以下语句的结果相同:

const App =() => {
  return <h1 className='title'>Hello World!</h1>
}
const App =() => {
  return React.createElement('h1',{className:'title'},'Hello World!');
}

注意,在 JS 中使用 className 来标识 HTML tag 的类。

为了加快重建,只有 src 文件夹中的文件才会由 Webpack 处理。
任何保存在其他位置的 JS 或 CSS 文件都会被 Webpack 忽略。
创建生产环境包时,src 以外的顶级文件夹都会被忽略,可用作项目文档之用。

如果本地安装了 Git 且当前项目不是另一个项目的子集,
那么系统会默认为当前项目创建一个新的 Git 库并添加顶级 .git 文件夹。

架构设计
React 程序在架构上存在组件间的母子关系,即组件是需要嵌套在一起的。
这里要创建的新闻列表样例程序将包含两个顶级组件:Header 和 News。
其中 News 组件下包含多个 News Item,这里存在三个组件的两级关系。

每个组件都会有独立的源码文件,存放在 src 下的 components 文件夹中。

函数组件
对于没有自定义方法的组件,可以使用 const 创建并 export 即可,样例如下:

import React from 'react';

const Header = () => {
    return <h1>This is the imported Header</h1>
}

export default Header;

在其他需要使用该组件的地方,import 该组件并引用即可,样例如下:

import React from 'react';
import ReactDom from 'react-dom';
import Header from './components/header';

const App =() => {
  return (
    <div>
      <Header />
    </div>
  )
}

ReactDom.render(<App/>, document.querySelector('#root'));

如上,定义的组件属于 functional 函数类型组件,以后再介绍类型组件。

动态数据
对于可以从系统获取的动态数据 JSX 使用花括号标识,例如:

import React from 'react';

const nowDate = new Date();
const currentYear = nowDate.getFullYear();
// JS has the month starts from 0
const currentMonth = nowDate.getMonth() + 1;
const currentDate = nowDate.getDate();

const Header = () => {
    return <p>{currentYear}/{currentMonth}/{currentDate}</p>
}

export default Header;

另一个样例:

import React from 'react';

const user = {
    firstName: 'Test',
    lastName: 'User',
    age: 20
}

const Header = () => {
    return <p>{user.firstName} {user.lastName} is {user.age} years old</p>
}

export default Header;

类型组件
函数组件根本上是一个函数,可以接受参数并处理动态数据,但只能独立工作。
如果希望组件之间有所互动,需要使用更为复杂的 class 类型组件,注意:
1. 类型组件使用 class 定义,并需要继承 react.componnet
2. 类型组件的内容需要使用 render() 方法生成,不可以直接 return
基于之前的样例,一个简单的类型组件样例如下:

import React, { Component } from 'react';

class Header extends Component {
    render() {
        return (
            <header>
                <div>Logo</div>
                <input type="text"/>
            </header>
        )
    }
}

export default Header;

添加样式
使用 CSS 可以美化程序界面,例如在 index.heml 页面中引用特定的字体。
当然,也可以在组件文件中定义 styles 以行内的方式添加样式定义。
但是,以上都不是推荐的最佳操作,最好定义外部 CSS 文件并在组件中引用。
创建 src/css 文件夹,添加 styles.css 文件:

body {
    margin: 0
}

header {
    background: #03a9f4;
    text-align: center
}

.logo {
    color: #fff;
    font-size: 40px;
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    text-align: 'Center'
}

header input {
    font-size: 20px;
    margin: 20px 0
}

在 header 组件中引用该外部 CSS 并为相关 tag 添加类标识:

import React, { Component } from 'react';
import '../css/styles.css';

class Header extends Component {
    render() {
        return (
            <header>
                <div className="logo">Logo</div>
                <input type="text" onChange={this.inputChangeHandler}/>
            </header>
        )
    }
}

export default Header;

事件处理
正如传统的 HTML 组件可以处理事件,JSX 通过自身方法实现类似功能。
例如以下示例中的鼠标点击或内容更改等事件,都在控制台有所反馈:

class Header extends Component {
    inputChangeHandler(event){
        console.log(event.target.value);
    }
    render() {
        return (
            <header>
                <div 
                    className="logo"
                    onClick={()=>console.log("clicked!")}
                >Logo</div>
                <input type="text" onChange={this.inputChangeHandler}/>
            </header>
        )
    }
}

构造函数
类的继承中,如果需要更新构造函数,而不是重写,需要添加 super()
为测试和说明以上,可以创建如下 public/play.js 文件:

class Car {
    constructor() {
        this.wheels = 4;
        this.type = 'Hatchback'
    }

    alertWheels() {
        console.log(4);
    }
}

class Toyota extends Car {
    constructor() {
        super();
        this.brand = 'Toyota';
    }
}

const car = new Car();
car.alertWheels();

const toyota = new Toyota();
console.log(toyota.brand);
console.log(toyota.type);

并在 index.html 的 body 中引用:

…
    <script src="play.js"></script>
  </body>
</html>

组件状态
首先要明确的是,函数组件没有状态属性,只有类型组件才拥有状态属性。
状态是一种对象,其特殊之处在于状态变化时系统会自动重新加载组件。
注意,这里不是刷新整个页面,而只是更新涉及组件的 render() 函数。

这里先看一种用 bind 的旧规范,作为范例,修改之前的 Header 组件如下:

class Header extends Component {
    state = {
        keywords: 'Hello'
    }

    inputChangeHandler(event){
        this.setState({
            keywords: event.target.value
        })
    }

    render() {
        console.log(this.state.keywords);
        return (
            <header>
                <div>Logo</div>
                <input 
                    type="text" 
                    onChange={this.inputChangeHandler.bind(this)}
                />
            </header>
        )
    }
}

以上范例定义了状态,并在输入框内容变更时更新到控制台日志。
注意 input 组件的 onChange 属性里使用了 bind(this) 方法。
以上方法将类的 this 指针带入自定义的函数,已被新规格弃用:

class Header extends Component {
    state = {
        keywords: 'Hello'
    }

    inputChangeHandler = (event) => {
        this.setState({
            keywords: event.target.value
        })
    }

    render() {
        console.log(this.state.keywords);
        return (
            <header>
                <div>Logo</div>
                <input 
                    type="text" 
                    onChange={this.inputChangeHandler}
                />
            </header>
        )
    }
}

注意新规格在自定义函数时使用新的 function = (e) => {} 结构。

再有,对于自定义类的扩展类,可能需要如下的构造方法:

class Toyota extends Car {
    constructor(props) {
        super(props);
        this.state = {
            keywords: 'Hello'
        }
    }
…

使用如上构造方法并将状态定义置入其中,是为更好的继承类属性。

属性传递
组件状态保存于内存且易丢失,属性可相对持久的保存并再组建间传递。
属性的介绍有点冗长,但因为其重要性也是值得的,准备 db.json 文件:

[
    {"id": 1,"title": "title 1","feed": "feed 1"},
    {"id": 2,"title": "title 2","feed": "feed 2"},
    {"id": 3,"title": "title 3","feed": "feed 3"},
    {"id": 4,"title": "title 4","feed": "feed 4"}
]

保存以上文件在 src 根目录,更新 index.js 文件如下:

import React, {Component} from 'react';
import ReactDom from 'react-dom';
import Header from './components/header';
import NewsList from './components/news_list';
import JSON from './db.json';

class App extends Component {
  state = {
    news: JSON,
  }
  render() {
    //console.log(JSON);
    //console.log(this.state.news);
    return (
      <div>
        <Header />
        <NewsList news={this.state.news}/>
      </div>
    )
  }
}

ReactDom.render(<App/>, document.querySelector('#root'));

导入 db.json 文件后,可用 console.log(JSON); 测试导入结果。
添加 state 后,可用 console.log(this.state.news); 测试状态更新结果。
为 NewsList 组件添加 news={this.state.news} 即完成属性添加和传递。

编辑 news_list.js 文件,用函数方式接收 props,在控制台日志确认。

import React from 'react';

const NewsList = (props) => {
    console.log(props);
    return (
        <div>News List</div>
    )
}

export default NewsList;

再次修改 news_list.js 文件,使用 map 方法遍历对象:

import React from 'react';

const NewsList = (props) => {
    //console.log(props);
    const items = props.news.map((item)=>{
        return(
            <div>
                <h3>{item.title}</h3>
                <div>
                    {item.feed}
                </div>
            </div>
        )
    })
    return (
        <div>{items}</div>
    )
}

export default NewsList;

此时已经可以在 App 页面确认新闻列表。

一次性遍历所有新闻对象不利于程序扩展和性能。
这里进一步分解新闻组件,分解到独立对象 news_list_item.js 文件。
首先,更新新闻列表文件:

import React from 'react';
import NewsItem from './news_list_item';

const NewsList = (props) => {
    //console.log(props);
    const items = props.news.map((item)=>{
        return(
            <NewsItem key={item.id} item={item}/>
        )
    })
    return (
        <div>{items}</div>
    )
}

export default NewsList;

导入独立新闻对象,并传递 key 和 item 属性。
编辑独立新闻对象文件,处理传入的 item 属性:

import React from 'react';

const NewsItem = (props) => {
    //console.log(props);
    return(
        <div>
            <h3>{props.item.title}</h3>
            <div>
                {props.item.feed}
            </div>
        </div>
    )
}

export default NewsItem;

类似 import {Component} from ‘react’ 的操作,以上代码可重构:

import React from 'react';

const NewsItem = ({item}) => {
    //console.log(item);
    return(
        <div>
            <h3>{item.title}</h3>
            <div>
                {item.feed}
            </div>
        </div>
    )
}

export default NewsItem;

子属性
从母组件到子组件,有一个可以隐含传送的属性,叫做 children。
更新 news_list.js 中的 NewsItem 组件代码如下:

import React from 'react';
import NewsItem from './news_list_item';

const NewsList = (props) => {
    //console.log(props);
    const items = props.news.map((item)=>{
        return(
            <NewsItem key={item.id} item={item}>
                Make a good use of the Children props!
            </NewsItem>
        )
    })
    return (
        <div>{items}</div>
    )
}

export default NewsList;

注意 NewsItem 组件由原来的自关闭更新为带有内容的状态。
在 news_list_item.js 文件中可以调用隐含传入的 children 属性:

import React from 'react';

const NewsItem = ({item,children}) => {
    console.log(item);
    return(
        <div>
            <h3>{item.title}</h3>
            <div>
                {item.feed}
            </div>
            <div>
                {children}
            </div>
        </div>
    )
}

export default NewsItem;

页面中可以查看到每个新闻对象都被添加了该子属性。

行内样式
一般来说,不推荐使用行内样式定义方式,会造成样式定义分散不易管理。
但如果个别组件需要动态更新样式,行内样式也可用,更新 header.js:

import React, { Component } from 'react';
import '../css/styles.css';

class Header extends Component {
    constructor(props) {
        super(props);
        this.state = {
            keywords: 'Hello'
        }
    }

    inputChangeHandler = (event) => {
        this.setState({
            keywords: event.target.value
        })
    }

    render() {
        const style = {
            background: 'red'
        }
        if(this.state.keywords !== '') {
            style.background = 'blue'
        } else {
            style.background = 'red'
        }
        return (
            <header style={style}>
                …
            </header>
        )
    }
}

export default Header;

以上定义了内部 style 根据输入状况修改背景颜色,应用到 header 组件。

一种更有趣的方式是更新状态,并根据状态更新样式:

import React, { Component } from 'react';
import '../css/styles.css';

class Header extends Component {
    constructor(props) {
        super(props);
        this.state = {
            active: false,
            keywords: 'Hello'
        }
    }

    inputChangeHandler = (event) => {
        this.setState({
            active: event.target.value === '' ? false : true,
            keywords: event.target.value
        })
    }

    render() {
        return (
            <header style={{background:`${this.state.active ? 'red':'blue'}`}}>
                …
            </header>
        )
    }
}

export default Header;

以上添加了组件状态,根据输入更新状态,根据状态设定背景颜色。

进一步优化,动态更新状态到组件类定义,可抽取样式到外部 CSS:

import React, { Component } from 'react';
import '../css/styles.css';

class Header extends Component {
    constructor(props) {
        super(props);
        this.state = {
            active: 'non-active',
            keywords: 'Hello'
        }
    }

    inputChangeHandler = (event) => {
        this.setState({
            active: event.target.value === '' ? 'non-active' : 'active',
            keywords: event.target.value
        })
    }

    render() {
        return (
            <header className={this.state.active}>
                …
            </header>
        )
    }
}

export default Header;

以上根据输入框内容动态更新类名称,可配合外部 CSS 更新样式。

样式架构
借助第三方提供的库,可以更省力的实现动态样式,这里用 glamor。
首先恢复 header.js 文件到初始状态:

import React, { Component } from 'react';
import '../css/styles.css';

class Header extends Component {
    constructor(props) {
        super(props);
        this.state = {
            keywords: 'Hello'
        }
    }

    inputChangeHandler = (event) => {
        this.setState({
            keywords: event.target.value
        })
    }

    render() {
        return (
            <header >
                <div className="logo">Logo</div>
                <input 
                    type="text" 
                    onChange={this.inputChangeHandler}
                />
            </header>
        )
    }
}

export default Header;

首先,用 Ctrl + C 终止 React 服务器,运行如下命令安装 glamor:
$ npm install glamor –save
接着,更新 news_list_item.js 文件,导入和使用 glamor 的 css 模块:

import React from 'react';
import {css} from 'glamor';

const NewsItem = ({item,children}) => {
    let news_item = css({
        padding: '20px',
        boxSizing: 'border-box',
        borderBottom: '1px solid grey',
        ':hover': {
            color: 'red',
            background: 'white'
        },
        '@media(max-width: 500px)': {
            color: 'blue'
        }

    })

    let item_grey = css({
        background: 'lightgrey'
    })

    return(
        //<div {...news_item} {...item_grey}>
        <div className={`${news_item} ${item_grey}`}>
            <h3>{item.title}</h3>
            <div>
                {item.feed}
            </div>
            <div>
                {children}
            </div>
        </div>
    )
}

export default NewsItem;

这里定义了两个 css 的类,其中有 :hover 和 @media 的特殊用法。
最后,注意如下两种代码结果是相同的,都使用了 ES6 规范:

添加检索
目标程序,以 Header 文本框中输入的内容为关键字检索新闻标题。
更新 header.js 文件如下,简化类型组件为函数组件,用属性传递关键字:

import React from 'react';
import '../css/styles.css';

const Header = (props) => {
    return (
        <header>
            <div className="logo">Logo</div>
            <input 
                type="text" 
                onChange={props.keywords}
            />
        </header>
    )
}

export default Header;

注意,以上更新将输入框的 onChange 变化结果放入 keywords 属性。

更新 index.js 文件,确认 header 的 keywords 属性可传递回 index:

import React, {Component} from 'react';
import ReactDom from 'react-dom';
import Header from './components/header';
import NewsList from './components/news_list';
import JSON from './db.json';

class App extends Component {
  state = {
    news: JSON,
  }

  getKeyword = (event) => {
    console.log(event.target.value);
  }

  render() {
    return (
      <div>
        <Header keywords={this.getKeyword}/>
        <NewsList news={this.state.news}/>
      </div>
    )
  }
}

ReactDom.render(<App/>, document.querySelector('#root'));

以上更新在 header 上定义 keywords 属性,传递到 getKeyword 方法。
后者获取 onChange 事件对象,并在控制台日志中输出该事件的目标值。

在控制台确认可以拿到输入的关键词后,再次更新 index.js 添加检索功能:

import React, {Component} from 'react';
import ReactDom from 'react-dom';
import Header from './components/header';
import NewsList from './components/news_list';
import JSON from './db.json';

class App extends Component {
  state = {
    news: JSON,
    filtered: []
  }

  getKeyword = (event) => {
    let filtered = this.state.news.filter((item)=>{
      return item.title.indexOf(event.target.value) > -1
    });
    this.setState({
      filtered
    })
  }

  render() {
    let nFiltered = this.state.filtered;
    let nWhole = this.state.news;
    return (
      <div>
        <Header keywords={this.getKeyword}/>
        <NewsList 
          news={nFiltered.length === 0 ? nWhole : nFiltered}
        />
      </div>
    )
  }
}

ReactDom.render(<App/>, document.querySelector('#root'));

以上更新首先添加了新的 filtered 状态,保存筛选后的新闻。
接着,在 render 方法中添加两个保存筛选前后新闻的变量。
在 NewsList 组件中根据有无筛选结果传递全部或筛选的新闻。

最有趣的是 getKeyword 方法的更新,直接使用了 filter 方法。
类似 map 方法,它遍历新闻列表对象并检索标题中的关键词。
检索命中时索引值大于等于 0,返回新闻对象到 filtered 数组。
最后更新组件的 filtered 状态,注意原始的代码应该如下:
this.setState({ filtered: filtered})
前一个 filtered 时状态变量,后一个是返回的过滤有新闻数组。
由于 JS ES6 规范所允许的简化,变成 this.setState({ filtered})

Think Python – Ch6

许多函数做打印内容到屏幕或移动乌龟绘图等,本身并不返回任何值,本章节介绍返回值的函数,先举例如下:

$ python3
>>> def area(radius):
...   area = math.pi*radius**2
...   return area
... 
>>> import math
>>> area(5)
78.53981633974483

以上样本函数返回的是一个临时变量,也可以直接返回计算该临时变量的语句。条件语句中可以有多个返回命令,但只要有一个执行过,程序即退出,其他没有执行的代码就成为了死码:

>>> def area(radius):
...   if radius <= 0:
...     print('radius cannot be 0 or negative')
...     return -1
...   else:
...     return math.pi*radius**2
... 
>>> area(-1)
radius cannot be 0 or negative
-1
>>> area(5)
78.53981633974483

注意以上改进的函数需要包括所有情况,如果条件设定为radius < 0则会丢失对输入为0的处理,得到None的返回值。

所谓增量开发就是从简单的模型开始,逐步完善程序的细节功能,以减少开发和调试的难度。如计算平面上两点的距离,在确定输入和输出后即可写出如下初始代码:

>>> def distance(x1,y1,x2,y2):
...   return 0.0
... 
>>> distance(1,2,3,4)
0.0

之后可以容易计算出两点在两个平面维度上的距离:

>>> def distance(x1,y1,x2,y2):
...   dx = abs(x2 - x1)
...   dy = abs(y2 - y1)
...   print('dx is:', dx)
...   print('dy is:', dy)
...   return 0.0
... 
>>> distance(1,2,3,4)
dx is: 2
dy is: 2
0.0
>>> distance(4,3,2,1)
dx is: 2
dy is: 2
0.0

最后利用直角三角形边长关系计算出两点的距离:

>>> def distance(x1,y1,x2,y2):
...   dx = abs(x2 - x1)
...   dy = abs(y2 - y1)
...   return math.sqrt(dx**2 + dy**2)
... 
>>> distance(1,2,3,4)
2.8284271247461903

在开发中段加入的打印语句只是为了确认临时变量,在最终函数的结果中不会出现,类似的其实也包括计算直线距离时用到的abs()函数,这样在开发过程中临时加入但不会出现在最终程序中的语句叫做支架(Scaffolding)。

总结增量开发的流程如下:

  • 从一个可以运行得初始程序写起,即便这个程序只是个框架,什么也不做;
  • 逐渐增加程序的功能,使用中间变量和语句帮助确认开发过程和结果正确;
  • 重复上一步,直到实现预期功能,移除支架变量和语句,但保持程序可读;

函数也可以返回逻辑值,用来做判断或在被调用后定义其他函数的逻辑,举例如下:

>>> def is_divisible(x,y):
...   if x%y == 0:
...     return True
...   else:
...     return False
... 
>>> is_divisible(4,3)
False
>>> is_divisible(4,2)
True
>>> def is_divisible(x,y):
...   return x%y == 0
... 
>>> is_divisible(4,3)
False
>>> is_divisible(4,2)
True
>>> def check_divisible(x,y):
...   if is_divisible(x,y):
...     print('is divisible!')
...   else:
...     print('not divisible!')
... 
>>> check_divisible(4,3)
not divisible!
>>> check_divisible(4,2)
is divisible!

以上定义了一个函数,检测输入的两个数是否可以整除,中段为该函数做了简化操作,最后在其他函数中调用该函数输出更易读的结果。类似的,可以写出如下函数:

>>> def is_between(x,y,z):
...   return x<=y and y<=z
... 
>>> is_between(2,3,4)
True
>>> is_between(2,3,2)
False

作为复习,再看如下递归函数示例,尝试理解其中的数学意义:

>>> def factorial(n):
...   if n == 0:
...     return 1
...   else:
...     return n*factorial(n-1)
... 
>>> factorial(3)
6

递归函数可以构成复杂的内部逻辑,如果不是绝对必要,可以测试函数可用性并忽略具体的执行逻辑与代码细节,合理和正确的使用已有的函数和库即可。例如斐波那契数列:

>>> def fibonacci(n):
...   if n == 0:
...     return 0
...   elif n == 1:
...     return 1
...   else:
...     return fibonacci(n-1)+fibonacci(n-2)
... 
>>> fibonacci(3)
2
>>> fibonacci(4)
3
>>> fibonacci(5)
5
>>> fibonacci(6)
8

以上程序的栈图在大数情况下呈几何倍数增长,要分析和了解具体执行过程细节有困难,理解并相信程序内部的逻辑即可。

有些时候需要对输入的参数做类型检测,可以用isinstance()函数实现:

>>> def factorial(n):
...   if not isinstance(n, int):
...     print('integer please')
...     return -1
...   elif n < 0:
...     print('positive please')
...     return -1
...   elif n == 0:
...     return 1
...   else:
...     return n*factorial(n-1)
... 
>>> factorial(1.5)
integer please
-1
>>> factorial(-3)
positive please
-1
>>> factorial(0)
1
>>> factorial(3)
6

作为调试技巧之一,可以检查函数读入的参数,返回的结果,及调用的值等三个关键处:

>>> def factorial(n):
...   space = ' '*2*n
...   print(space, 'factorial', n)
...   if n == 0:
...     print(space, 'returning 1')
...     return 1
...   else:
...     result = n*factorial(n-1)
...     print(space, 'returning', result)
...     return result
... 
>>> factorial(3)
       factorial 3
     factorial 2
   factorial 1
 factorial 0
 returning 1
   returning 1
     returning 2
       returning 6
6

以上函数中的space变量定义和打印语句是支架组件,只做调试用。类似还有如下:

>>> def fibonacci(n):
...   space = ' '*2*n
...   print(space,'fibonacci',n)
...   if n == 0:
...     print(space,'returning',0)
...     return 0
...   if n == 1:
...     print(space,'returning',1)
...     return 1
...   else:
...     print(space,'fibonacci',n-1,'+','fibonacci',n-2)
...     return fibonacci(n-1)+fibonacci(n-2)
... 
>>> fibonacci(3)
       fibonacci 3
       fibonacci 2 + fibonacci 1
     fibonacci 2
     fibonacci 1 + fibonacci 0
   fibonacci 1
   returning 1
 fibonacci 0
 returning 0
   fibonacci 1
   returning 1
2

对于以上带调试语句的斐波那契数列函数,目前输入的是最小值3,输入6就可以看到会有多少次递归,由于输出内容过多,这里就不再演示。

看似简单的函数可以组合成较为复杂的函数,看如下函数的执行及栈图:

>>> def a(x,y):
...   x += 1
...   return x * y
... 
>>> def b(z):
...   prod = a(z, z)
...   print(z, prod)
...   return prod
... 
>>> def c(x,y,z):
...   total = x + y + z
...   square = b(total)**2
...   return square
... 
>>> x = 1
>>> y = x + 1
>>> print(c(x, y+3, x+y))
9 90
8100

栈图如下:

  • main: [x->1, y->5, z->3]
  • c: [total->9, square->b(9)**2] [square<-8100]
  • b: [z->9, prod->a(9,9), print->[9, prod], return->prod] [prod<-90]
  • a: [x->10, return->90]

练习,Ackermann函数(http://en.wikipedia.org/wiki/Ackermann_function):

>>> def ack(m,n):
...   if (not isinstance(m,int)) or (not isinstance(n,int)):
...     print('integer please')
...     return -1
...   elif m < 0 or n < 0:
...     print('positive please')
...     return -1
...   elif m == 0:
...     return n + 1
...   elif m > 0 and n == 0:
...     return ack(m-1,1)
...   elif m > 0 and n > 0:
...     return ack(m-1,ack(m,n-1))
...   else:
...     print('sth wrong')
...     return -1
... 
>>> ack(3,4)
125

练习,检测输入的字符串是否顺序和倒序相同:

>>> def is_palindrome(word):
...   if len(word)==1 or len(word)==0:
...     print('is a palindrome')
...   elif first(word)!=last(word):
...     print('is not a palindrome')
...   else:
...     is_palindrome(middle(word))
... 
>>> 
>>> is_palindrome('12321')
is a palindrome
>>> is_palindrome('12345')
is not a palindrome
>>> is_palindrome('')
is a palindrome
>>> is_palindrome('11')
is a palindrome
>>> is_palindrome(' ')
is a palindrome

继续,检查输入的两个数是否存在幂与底数关系:

>>> def is_power(a,b):
...   if a==1 or a==b==0:
...     return True
...   elif (a==0 and b!=0) or (a!=0 and b==0):
...     return False
...   elif is_divisible(a,b) and is_power(a/b,b):
...     return True
...   else:
...     return False
... 
>>> is_power(8,2)
True
>>> is_power(9,2)
False
>>> is_power(0,0)
True
>>> is_power(0,2)
False
>>> is_power(9,0)
False

以上程序中,关键是判断ab的整除关系以及a/bb之间的幂与底数关系,后者使用递归调用函数自身完成,而且判断递归终止的基本情景也很重要。

最后,写一个函数计算最大公约数(Greatest Common Divisor):

>>> def gcd(a,b):
...   if b == 0:
...     return a
...   else:
...     r = a%b
...     return gcd(b,r)
... 
>>> gcd(0,28)
28
>>> gcd(28,0)
28
>>> gcd(0,0)
0
>>> gcd(21,28)
7

与之前的程序一样,关键是找到合适的算法,即设定gcd(a,0)结果为a,而且gcd(a,b)gcd(b,a%b)同样都可以计算出最大公约数。如果可以知道如何计算,实现计算本身

关于带返回值的函数和递归的复习就到这里,下一部分介绍迭代。听起来艰深,其实就是反复执行一段语句,之前有用for或递归实现了语句的循环调用,还会有哪些新的东西呢?