准备环境
在 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.js
的 render()
函数中有很多的重复代码,每个表单控件都包含 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"
}]
]
}