React 项目实战

2020-03-06 language web

准备环境

在 React 社区提供了很多脚手架,这里使用官方推荐的 create-react-app 工具。

----- 安装脚手架
# npm install -g create-react-app

----- 生成并运行项目
$ create-react-app foobar
$ cd foobar
$ npm start

----- 其它常见操作
$ npm run build
$ npm test
$ npm run eject

目录结构如下。

pages/  页面

服务端可以采用 JSON Server 模拟,

创建用户页面

创建一个用来添加用户的页面,保存在 src/pages/UserAdd.js 文件中。

import React from 'react';

class UserAdd extends React.Component {
    render() {
        return (
            <div>User add page.</div>
        );
    }
}
export default UserAdd;

通过 npm install --save-dev react-router-dom 安装路由功能,这里使用的是 5.X 版本,使用其提供的路由组件来控制当前路由下页面应该渲染的组件,详细可以参考 React Router 中的介绍,修改后的 src/index.js 文件如下。

import React from "react";
import ReactDOM from 'react-dom';
import {
    BrowserRouter as Router,
    Switch,
    Route,
    Link
} from "react-router-dom";

import UserAdd from './pages/UserAdd';

function Home() {
    return (
        <div>
            <header>
                <h1>Welcome</h1>
            </header>
            <nav>
                <ul>
                    <li><Link to="/user/add">Add User</Link></li>
                </ul>
            </nav>
        </div>
    );
}

ReactDOM.render(
    <React.StrictMode>
        <Router>
            <div>
                <Switch>
                    <Route path="/" exact><Home /></Route>
                    <Route path="/user/add"><UserAdd /></Route>
                </Switch>
            </div>
        </Router>
    </React.StrictMode>,
    document.getElementById('root')
);

点击可以直接看到具体的页面跳转。

发送 POST 请求

接着继续修改添加用户界面,也就是向上述搭建的 http://localhost:3000/user 服务端发送 POST 请求。

import React from 'react';

class UserAdd extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            name: '',
            age: 0,
            gender: 'male'
        };
    }

    handleValueChange(field, value, type='string') {
        if(type === 'number'){
            value = + value;
        }
        this.setState({
            [field]: value
        });
    }

    handleSubmit(e){
        e.preventDefault();
        console.log(JSON.stringify(this.state));

        const {name, age, gender} = this.state;
        fetch('http://localhost:4040/user', {
            method: 'post',
            body: JSON.stringify({name, age, gender}),
            headers: {'Content-Type': 'application/json'}
        })
        .then((res) => res.json())
        .then((res) => {
            console.log(res);
            if (res.id) {
                console.log('Add user successfully.');
                this.setState({name: '', age: 0, gender: ''});
            } else {
                console.log('Add user failed.');
            }
        })
        .catch((err) => console.error(err));
    }

    render() {
        return (
            <div>
                <header>User add page.</header>
                <main>
                    <form onSubmit={(e) => this.handleSubmit(e)}>
                        <label>UserName:</label>
                        <input type="text" value={this.state.name}
                            onChange={(e) => this.handleValueChange('name', e.target.value)} />
                        <br />
                        <label>Age:</label>
                        <input type="number" value={this.state.age || ''}
                            onChange={(e) => this.handleValueChange('age', e.target.value, 'number')} />
                        <br />
                        <label>Gender:</label>
                        <select value={this.state.gender}
                            onChange={(e) => this.handleValueChange('gender', e.target.value)}>
                            <option value="">Choose</option>
                            <option value="male">Male</option>
                            <option value="female">Female</option>
                        </select>
                        <br />
                        <br />
                        <input type="submit" value="Submit" />
                    </form>
                </main>
            </div>
        );
    }
}
export default UserAdd;

创建表单时,因为 React 是单向数据流,对于绑定值就需要通过一个 onChange 方法来更新值。在提交表单时,可以用 ajax、表单提交、fetch 方法,其中,表单提交会导致页面跳转,不建议使用,这里使用较为先进的 fetch 方法。

表单验证

也就是验证表单当前字段是否合法,如果有效则显示数据,否则显示错误状态,为了方便管理,修改组件的状态信息。

import React from 'react';

class UserAdd extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            form: {
                name: {
                    valid: false,
                    value: '',
                    error: ''
                },
                age: {
                    valid: false,
                    value: 0,
                    error: ''
                },
                gender: {
                    valid: false,
                    value: '',
                    error: ''
                }
            }
        };
    }

    handleValueChange(field, value, type='string') {
        if(type === 'number'){
            value = + value;
        }

        const {form} = this.state;
        const newObj = {value, valid: true, error: ''};

        switch(field) {
            case 'name': {
                if (value.length >= 5) {
                    newObj.error = 'Length of username should less than 4 bytes.';
                    newObj.valid = false;
                } else if (value.length === 0) {
                    newObj.error = 'Please input the username.';
                    newObj.valid = false;
                }
                break;
            }
            case 'age': {
                if (value > 100 || value <= 0 || !value) {
                    newObj.error = 'Age should between 1 and 100.';
                    newObj.valid = false;
                }
                break;
            }
            case 'gender': {
                if (!value) {
                    newObj.error = 'Please choose the gender.';
                    newObj.valid = false;
                }
                break;
            }
            default: {
                console.log("Invalid field");
                return;
            }
        }

        this.setState({
            form: {
                ...form,
                [field]: newObj
            }
        });
    }

    handleSubmit(e){
        e.preventDefault();
        console.log(JSON.stringify(this.state));

        const {form: {name, age, gender}} = this.state;
        if (!name.valid || !age.valid || !gender.valid) {
            console.log('Please input the correct message.');
            return;
        }

        fetch('http://localhost:4040/user', {
            method: 'post',
            body: JSON.stringify({
                name: name.value,
                age: age.value,
                gender: gender.value
            }),
            headers: {'Content-Type': 'application/json'}
        })
        .then((res) => res.json())
        .then((res) => {
            console.log(res);
            if (res.id) {
                console.log('Add user successfully.');
                this.setState({name: '', age: 0, gender: ''});
            } else {
                console.log('Add user failed.');
            }
        })
        .catch((err) => console.error(err));
    }

    render() {
        const {form: {name, age, gender}} = this.state;

        return (
            <div>
                <header>User add page.</header>
                <main>
                     <form onSubmit={(e) => this.handleSubmit(e)}>
                         <label>UserName:</label>
                         <input type="text" value={name.value}
                             onChange={(e) => this.handleValueChange('name', e.target.value)} />
                         {!name.valid && <span>{name.error}</span>}
                         <br />
                         <label>Age:</label>
                         <input type="number" value={age.value || ''}
                             onChange={(e) => this.handleValueChange('age', e.target.value, 'number')} />
                         {!age.valid && <span>{age.error}</span>}
                         <br />
                         <label>Gender:</label>
                         <select value={gender.value}
                             onChange={(e) => this.handleValueChange('gender', e.target.value)}>
                             <option value="">Choose</option>
                             <option value="male">Male</option>
                             <option value="female">Female</option>
                         </select>
                         {!gender.valid && <span>{gender.error}</span>}
                         <br />
                         <br />
                         <input type="submit" value="Submit" />
                     </form>
                </main>
            </div>
        );
    }
}
export default UserAdd;

高级组件

所谓的高阶组件就是以一个组件作为入参,并返回组件的函数,例如,表单提交无非就是默认值、校验规则、错误信息,那么就可以将其封装为一个通用的组件。

创建一个 src/utils/formProvider.js 文件,作为高级组件。

import React from 'react';

function formProvider (fields) {
    return function(Comp) {
        const initialFormState = {};
        for(const key in fields){
            initialFormState[key] = {
                value: fields[key].defaultValue,
                error: ''
            };
        }
    
        class FormComponent extends React.Component {
            constructor(props) {
                super(props);
                this.state = {
                     form: initialFormState,
                     formValid: false
                };
                this.handleValueChange = this.handleValueChange.bind(this);
            }
            
            handleValueChange(fieldName, value){
                const { form } = this.state;
                const newFieldState = {value, valid: true, error: ''};
                const fieldRules = fields[fieldName].rules;
                
                for(let i = 0; i < fieldRules.length; i++){
                    const {pattern, error} = fieldRules[i];
                    let valid = false;
                    if(typeof pattern === 'function') {
                        valid = pattern(value);
                    } else {
                        valid = pattern.test(value);
                    }
            
                    if(!valid){
                        newFieldState.valid = false;
                        newFieldState.error = error;
                        break;
                    }
                }
            
                const newForm = {...form, [fieldName]: newFieldState};
                const formValid = Object.values(newForm).every(f => f.valid);
                this.setState({
                    form: newForm,
                    formValid
                });
            }
            
            render(){
                const { form, formValid } = this.state;
                return (
                    <Comp
                        {...this.props}
                        form={form}
                        formValid={formValid}
                        onFormChange={this.handleValueChange}
                    />
                );
            }
        }
        return FormComponent;
    }
}
export default formProvider;

对应的 src/pages/UserAdd.js 文件也要作相应的修改。

import React from 'react';
import formProvider from '../utils/formProvider';

class UserAdd extends React.Component {
    handleSubmit(e){
        e.preventDefault();
        const { form: { name, age, gender }, formValid} = this.props;
    
        if (!formValid) {
            console.log('Please input the correct message.');
            return;
        }
    
        fetch('http://localhost:4040/user', {
            method: 'post',
            body: JSON.stringify({
                name: name.value,
                age: age.value,
                gender: gender.value
            }),
            headers: {'Content-Type': 'application/json'}
        })
        .then((res) => res.json())
        .then((res) => {
            console.log(res);
            if (res.id) {
                console.log('Add user successfully.');
                this.setState({name: '', age: 0, gender: ''});
            } else {
                console.log('Add user failed.');
            }
        })
        .catch((err) => console.error(err));
    }
    
    render() {
        const {form: {name, age, gender}, onFormChange} = this.props;
    
        return (
            <div>
                <header>User add page.</header>
                <main>
                <form onSubmit={(e) => this.handleSubmit(e)}>
                    <label>UserName:</label>
                    <input type="text" value={name.value}
                            onChange={(e) => onFormChange('name', e.target.value)} />
                    {!name.valid && <span>{name.error}</span>}
                    <br />
                    <label>Age:</label>
                    <input type="number" value={age.value || ''}
                            onChange={(e) => onFormChange('age', e.target.value, 'number')} />
                    {!age.valid && <span>{age.error}</span>}
                    <br />
                    <label>Gender:</label>
                    <select value={gender.value}
                            onChange={(e) => onFormChange('gender', e.target.value)}>
                        <option value="">Choose</option>
                        <option value="male">Male</option>
                        <option value="female">Female</option>
                    </select>
                    {!gender.valid && <span>{gender.error}</span>}
                    <br />
                    <br />
                    <input type="submit" value="Submit" />
                </form>
                </main>
            </div>
        );
    }
}
    
UserAdd = formProvider({
    name: {
        defaultValue: '',
        rules: [{
            pattern: function(value) {
                return value.length > 0;
            },
            error: 'Please input username'
        }, {
            pattern: /^.{1,4}$/,
            error: 'Length of username should less than 4 bytes.'
        }]
    },
    age: {
        defaultValue: 0,
        rules: [{
            pattern: function(value) {
                return value >= 1 && value <= 100;
            },
            error: 'Age should between 1 and 100.'
        }]
    },
    gender: {
        defaultValue: '',
        rules: [{
            pattern: function(value) {
                return !!value;
            },
            error: 'Please choose the gender.'
        }]
    }
})(UserAdd)

export default UserAdd;

表单控件组件

UserAdd.jsrender() 函数中有很多的重复代码,每个表单控件都包含 label、控件元素、控制显示的 span 元素。所以,可以封装一个 FormItem 组件,新建 src/components/FormItem.js 文件。

import React from 'react';
 
class FormItem extends React.Component {
    render () {
        const {label, children, valid, error} = this.props;
        return (
            <div>
                <label>{label}</label>
                {children}
                {!valid && <span>{error}</span>}
            </div>
        );
    }
}
export default FormItem;

对应的表单渲染修改为如下。

<form onSubmit={(e) => this.handleSubmit(e)}>
    <FormItem label="UserName:" valid={name.valid} error={name.error}>
        <input type="text" value={name.value}
            onChange={(e) => onFormChange('name', e.target.value)} />
    </FormItem>
    
    <FormItem label="Age:" valid={age.valid} error={age.error}>
        <input type="number" value={age.value || ''}
            onChange={(e) => onFormChange('age', e.target.value, 'number')} />
    </FormItem>
    
    <FormItem label="Gender:" valid={age.valid} error={age.error}>
        <select value={gender.value}
            onChange={(e) => onFormChange('gender', e.target.value)}>
            <option value="">Choose</option>
            <option value="male">Male</option>
            <option value="female">Female</option>
        </select>
    </FormItem>
    
    <br />
    <input type="submit" value="Submit" />
</form>

用户列表

新建 src/pages/UserList.js 文件。

import React from 'react';

class UserList extends React.Component {
    constructor (props) {
        super(props);
        this.state = {
            userList: []
        };
    }

    componentDidMount() {
        fetch('http://localhost:4040/user')
        .then(res => res.json())
        .then(res => {
            this.setState({
                userList: res
            });
        });
    }

    render() {
        const { userList } = this.state;
        return (
            <div>
                <header><h1>User List</h1></header>
                <main>
                    <table>
                        <thead>
                            <tr>
                            <th>UID</th>
                            <th>UserName</th>
                            <th>Gender</th>
                            <th>Age</th>
                            </tr>
                        </thead>
                    <tbody>
                    {
                        userList.map((user) => {
                            return (
                                <tr key={user.id}>
                                    <td>{user.id}</td>
                                    <td>{user.name}</td>
                                    <td>{user.gender}</td>
                                    <td>{user.age}</td>
                                </tr>
                            );
                        })
                    }
                    </tbody>
                    </table>
                </main>
            </div>
        );
    }
}
export default UserList;

如果在添加完用户之后跳转到列表,可以通过 this.context.router.push('/user/list'); 执行。

布局组件

目前页面大部分都是有相同的 Header 以及 Footer ,只是中间部分不同,所以,完全可以用组件化来解决,新建 src/layouts/HomeLayout.js 文件如下。

import React from 'react';
 
class HomeLayout extends React.Component {
    render() {
        const { title, children } = this.props;
        return (
            <div>
                <header>
                    <h1>
                        {title}
                    </h1>
                </header>
                <main>
                    {children}
                </main>
            </div>
        );
    }
}
export default HomeLayout;

然后可以类似如下方式使用。

<HomeLayout title="Welcome">
    <div>... ...</div>
</HomeLayout>

其中的 <div>... ...</div> 内容就会作为 props.children 传入到布局组件中,然后可以修改主页面、用户添加页面、用户列表页面。

添加 Antd

通过 npm install antd --save 安装核心,并通过 npm install --save @ant-design/icons 安装 Icon 库,如果要打包整个项目会非常大,所以,通过 npm install babel-plugin-import --develop 安装,做到按需加载。最后,在根目录下创建 .roadhogrc 文件,配置文件修改如下。

{
  "extraBabelPlugins": [
    ["import", {
      "libraryName": "antd",
      "libraryDirectory": "lib",
      "style": "css"
    }]
  ]
}