页面编写
antd表单的使用
要使用antd,先下载antd
cnpm install antd --save
首先看一个表单的例子,在src下新建一个test文件夹,新建TextAntdForm.jsx,内容如下(先不管看得懂看不懂,后面解释)
import React from 'react'import {Form, Input, Button} from 'antd'function TextAntdForm(props) {const {getFieldDecorator, validateFields} = props.formconst handleSubmit = (event) => {event.preventDefault();validateFields((error, values) => {if (!error) {console.log(values);}})}return(<div style={{width: "300px", margin: "100px auto", fontFamily: "Consolas, '楷体'"}}><Form onSubmit={handleSubmit}><Form.Item label="用户名">{getFieldDecorator('title', {rules: [{required: true,message: '请输入用户名',}],})(<Input size="large" placeholder="请输入用户名"/>)}</Form.Item><Form.Item label="密码">{getFieldDecorator('password', {rules: [{required: true,message: '请输入密码',}],})(<Input.Password size="large" placeholder="请输入密码"/>)}</Form.Item><Form.Item><Button size="large" type="primary" htmlType="submit">提交</Button></Form.Item></Form></div>)}export default Form.create()(TextAntdForm)
修改src/index.js渲染该组件(记得引入antd/dits/antd.css,否则antd组件没有样式)
import React from 'react'import ReactDOM from 'react-dom';// import router from './router'import TestAntdForm from './test/TestAntdForm'import 'antd/dist/antd.css'import './common.css';// ReactDOM.render(router(), document.getElementById("root"));ReactDOM.render(<TestAntdForm />, document.getElementById("root"));

现在来解释上面的代码,首先看最后一行
Form.create()(TextAntdForm)
还记得高阶组件吗,Form.create()就是一个高阶组件,他会向组件的props中注入form,form提供了一些API,这里使用了两个:
- getFieldDecorator:用于和表单进行双向绑定
getFieldDecorator('title', {rules: [{required: true,message: '请输入用户名',}],})(<Input size="large" placeholder="请输入用户名"/>)
上面将Input与表单项进行了绑定,getFieldDecorator接收两个参数,第一个参数是id,根据它可以获取输入控件的值或者设置输入控件的值,是必填项;第二个参数是options,里面可以有很多属性,这里使用了rules,定义了校验的规则,required表示是否必填,message表示未填时显示的消息文字。
- validateFields:校验
里面接受一个回调函数,回调函数接收两个参数,第一个参数为error,当不满足校验规则时error的值非空,第二个参数是values,会将绑定表单的值以对象的形式传给values,键就是在getFieldDecorator传入的id。
上面在提交表单后,会调用Form的onSubmit回调函数,在回调函数,我们对数据进行了校验,如果没有问题的话,我们可以将输入表单的键值对以对象的形式获取到values,并打印出来
页面数据
我们将数据设置为一个数组datas,它的格式如下
datas = [{title: , brief: , isTop: , content: }{title: , brief: , isTop: , content: }]
Home组件根据datas展示数据,Edit和Display组件根据id和datas获取要展示的数据,由于多个组件都要用到数据,所以这里使用useContext和useReducer来分发数据。
在src下新建Provider.jsx,用来提供state和dispatch,内容如下
import React, {useReducer} from 'react'export const Context = React.createContext();const reducer = (state, action) => {const tempDatas = state.datas// 按道理case后面跟的都是常量,我这里为了简单switch(action.type) {case 'insertData':tempDatas[tempDatas.length] = action.data;return {...state, datas: tempDatas}case 'updateData':tempDatas[action.id] = action.datareturn {...state, datas: tempDatas}case 'deleteData':tempDatas.splice(action.id, 1)return {...state, datas: tempDatas}case 'changeOperation':return {...state, operation: action.operation}default:return state}}const initState = {// 随便写的数据用来实验 随后会删掉datas: [{title: 'aaa', isTop: true, brief: 'hahah'}, content: '<p>123</p>'],// 用来判断是添加文章还是编辑文章operation: 'ADD'}function Provider(props) {const [state, dispatch] = useReducer(reducer, initState)const {children} = propsreturn (<Context.Provider value={{state, dispatch}}>{children}</Context.Provider>)}export default Provider
更改router.js,在最上面加上Provider
import Provider from './Provider'// 上面没有变化,除了import Provider,故此省略const router = () => {return (<Provider><Router history={history}><Switch>{routes.map((item, id) => {return RouteItem({ key: id, ...item, history: history })})}</Switch></Router></Provider>);};export default router
Home
观察Home页面
发现Home是由这一个个Item组成,Item中的数据正是datas数组中每一个元素的内容,在Home中新建文件夹Item,并在Item中新建index.jsx和index.module.css。index.jsx:
import React from 'react'import styles from './index.module.css'import {Modal} from 'antd'const {confirm} = Modalfunction Item(props) {const {data,login,handleDelete,index,handleToEdit,handleToDisplay} = props;const toDisplay = (event) => {event.preventDefault();handleToDisplay(index)}const toEdit = (event) => {event.preventDefault();handleToEdit(index)}const toDelete = (event) => {event.preventDefault();confirm({title: `你确定要删除${data.title}`,content: '删除后内容不可恢复',onOk() {handleDelete(index)},onCancel() {},});}return (<div className={styles.item}><h2><a href={`/display/${data.id}`} onClick={toDisplay}>{data.title}</a></h2><hr /><div className={styles.abstract}>{data.brief}</div><div className={styles.readmore}><a href={`/display/${data.id}`} onClick={toDisplay}>阅读更多</a></div>{/* 当登录失显示编辑本文和删除本文 */}{login ?<div className={styles.edit}><a href={`/edit/${data.id}`} onClick={toEdit}>编辑本文</a></div> : ""}{login ?<div className={styles.delete}><a href="/delete" onClick={toDelete}>删除本文</a></div>: ""}{/* 当isTop为1时显示置顶图标 */}{data.isTop ?<div className={styles.isTop}><svg viewBox="0 0 1024 1024"><path d="M0 0h1024v1024z" fill="#7ED321"></path><path d="M571.733333 157.866667l17.066667-12.8-83.2-83.2L552.533333 14.933333l183.466667 183.466667-46.933333 46.933333-81.066667-81.066666-17.066667 12.8 100.266667 100.266666-14.933333 14.933334-102.4-102.4c-6.4 4.266667-10.666667 8.533333-17.066667 10.666666l72.533333 72.533334-110.933333 110.933333 36.266667 36.266667-14.933334 14.933333L313.6 209.066667l14.933333-14.933334 36.266667 36.266667 110.933333-110.933333 61.866667 61.866666c6.4-4.266667 10.666667-8.533333 17.066667-10.666666l-96-96 14.933333-14.933334 98.133333 98.133334z m-72.533333 209.066666l17.066667-17.066666-117.333334-117.333334-17.066666 17.066667 117.333333 117.333333z m27.733333-29.866666l14.933334-14.933334L426.666667 204.8l-14.933334 14.933333 115.2 117.333334z m27.733334-27.733334l17.066666-14.933333-117.333333-117.333333-17.066667 14.933333 117.333334 117.333333z m27.733333-25.6l14.933333-14.933333L482.133333 149.333333l-14.933333 14.933334 115.2 119.466666z m10.666667-202.666666L554.666667 44.8l-21.333334 21.333333 38.4 38.4 21.333334-23.466666z m57.6 57.6l-40.533334-40.533334-21.333333 21.333334 40.533333 40.533333 21.333334-21.333333zM704 192l-38.4-38.4-21.333333 21.333333L682.666667 213.333333l21.333333-21.333333zM571.733333 471.466667l12.8-21.333334c8.533333 10.666667 17.066667 19.2 25.6 27.733334 6.4 6.4 12.8 6.4 21.333334-2.133334l172.8-172.8-38.4-38.4 17.066666-17.066666 87.466667 87.466666-17.066667 17.066667-29.866666-29.866667-177.066667 177.066667c-14.933333 14.933333-29.866667 14.933333-44.8 0l-29.866667-27.733333z m302.933334 21.333333l-44.8 44.8c-27.733333 25.6-55.466667 40.533333-83.2 44.8-27.733333 2.133333-59.733333-6.4-96-25.6l6.4-25.6c34.133333 19.2 64 27.733333 87.466666 25.6 23.466667-4.266667 46.933333-14.933333 68.266667-36.266667l44.8-44.8 17.066667 17.066667z m132.266666-21.333333l-17.066666 19.2-55.466667-55.466667c-10.666667 8.533333-19.2 17.066667-29.866667 23.466667l51.2 51.2-119.466666 119.466666-17.066667-17.066666 102.4-102.4-76.8-76.8-104.533333 100.266666-17.066667-17.066666 121.6-121.6 42.666667 42.666666c10.666667-6.4 19.2-14.933333 29.866666-23.466666L861.866667 362.666667l17.066666-17.066667 128 125.866667zM802.133333 682.666667h-25.6c2.133333-25.6 2.133333-55.466667-2.133333-89.6h23.466667c4.266667 34.133333 4.266667 64 4.266666 89.6z" fill="#FFFFFF"></path></svg></div> : ""}</div>)}export default Item
注意到Item为展示组件,只负责数据的展示,而不负责数据的处理、获取,Item的数据、数据的操作都是从props中获取的,这些操作都由Item的容器组件Home来完成。
我们会将login保存在sessionStorage,login是一个布尔值,保存了是否登录的信息,true表示登录,false表示未登录,根据是否登录,决定是否将删除和编辑的操作暴露出来。同时我们也将根据data的isTop是否为true来显示是否置顶的svg图样(该图样来自CSDN的置顶图样)。
index.module.css
.item {width: 100%;height: 150px;background-color: white;margin-bottom: 40px;border-radius: 4px;padding-left: 25px;padding-right: 20px;padding-top: 20px;position: relative;box-shadow:0px 0px 6px 6px #FFF;}.item a {text-decoration: none;color: #40759b;}.item a:hover {text-decoration: underline;}.abstract {width: 100%;padding-top: 30px;}.readmore {position: absolute;font-size: 14px;bottom: 10px;right: 10px;}.edit {position: absolute;font-size: 14px;bottom: 10px;right: 75px;}.delete {position: absolute;font-size: 14px;bottom: 10px;right: 140px;}.isTop {position: absolute;width: 50px;top: 0;right: 0;}
现在进入Home组件,修改Home/index.jsx如下
import React, { useContext } from 'react'import BasicLayout from './../../layouts/BasicLayout'import Item from './components/Item'import {withRouter} from 'react-router-dom'import {Context} from './../../router'import { message } from 'antd'function Home(props) {const {history} = props// 使用useContext获取Provider提供的数据const {state, dispatch} = useContext(Context);const login = sessionStorage.getItem("login");const datas = state.datas;const handleDelete = (id) => {dispatch({type: "deleteData", id});message.success('删除成功');}const handleToEdit = (id) => {dispatch({type: "changeOperation", operation: "EDIT"})history.push(`/edit/${id}`);}const handleToDisplay = (id) => {history.push(`display/${id}`);}return (<div><BasicLayout>{datas.map((data, index) => {return <Itemindex={index}data={data}key={index}login={login}handleDelete={handleDelete}handleToEdit={handleToEdit}handleToDisplay={handleToDisplay}/>})}</BasicLayout></div>)}export default withRouter(Home)
现在启动项目(npm start),观察到页面如下
说明Home页面已经成功了(由于在sessionStorage中没有login,所以删除本文和编辑本文均显示不出来,当点击阅读更多时,会跳转到Display的页面)。
Display
我们来观察Display的页面
发现Display页面有一个背景为白色的内容区和一个按钮,这个按钮根据是否有登录来决定是否暴露出来,所以我们在Display中新建一个components文件夹,在里面新建一个Content文件夹,在Content文件夹中新建index.jsx和index.module.css。首先Content是一个展示组件,所以它的数据全部都由Display提供,所有的数据操作也由Display传入回调函数进行处理。
index.jsx如下
import React from 'react'import { Button } from 'antd'import styles from './index.module.css'function Content(props) {const {login, htmlContent, handleToEdit} = propsconst toEdit = () => {handleToEdit();}return(<div className={styles.display}><div className="braft-output-content" style={{minHeight: "425px", backgroundColor: "#FFF", padding: "50px 25px", fontSize: "16px", maxWidth: "850px"}} dangerouslySetInnerHTML={{__html: htmlContent}} ></div>{login &&<div className={styles.edit}><Button type="primary" onClick={toEdit}>编辑文章</Button></div>}</div>)}export default Content
想必上面的代码还是比较容易理解的,index.module.css的内容如下
.display {position: relative;}.display ul, .display ol {padding-left: 30px;}.edit {position: absolute;top: 10px;right: 10px;}
所以Display中的内容如下
import React, {useContext} from 'react'import BasicLayout from './../../layouts/BasicLayout'import {withRouter} from 'react-router-dom'import Content from './components/Content'import {Context} from './../../Provider'import BraftEditor from 'braft-editor'function Display(props) {const {history} = props// 获取传过来的idconst index = Number(history.location.pathname.split("/")[2])const {state, dispatch} = useContext(Context)const htmlContent = BraftEditor.createEditorState(state.datas[index].content).toHTML()const login = sessionStorage.getItem("login")const handleToEdit = () => {dispatch({type: "changeOperation", operation: "EDIT"});history.push(`/edit/${index}`);}return (<BasicLayout><ContenthtmlContent={htmlContent}login = {login}handleToEdit = {handleToEdit}/></BasicLayout>)}export default withRouter(Display)
至此Display页面设计完毕。
Edit
在写Edit页面之前,来改造一下RichText组件,我们要将RichText做成展示组件,所有的数据都由Edit提供,所有的数据处理也由Edit处理,修改如下
import React from 'react'import BraftEditor from 'braft-editor'import 'braft-editor/dist/index.css'function RichText(props) {const {value, onChange} = propsconst handleEditorChange = (editorState) => {onChange(editorState)}return (<BraftEditorvalue={value}onChange={handleEditorChange}/>)}export default RichText
现在我们来看一下Edit页面的结构
我们使用antd的表单来做成这件事情,在前面已经介绍过antd表单的使用,所以这里不多加介绍,直接上代码
import React, {useEffect, useContext} from 'react'import RichText from './components/RichText'import { withRouter } from 'react-router-dom'import { Form, Input, Button, message, Checkbox } from 'antd'import BasicLayout from './../../layouts/BasicLayout'import BraftEditor from 'braft-editor'import {Context} from './../../Provider'function Edit(props) {const {history} = propsconst FormItem = Form.Item;const { getFieldDecorator, validateFieldsAndScroll } = props.form;const {state, dispatch} = useContext(Context)useEffect(() => {// 当组件加载后 如果是编辑文章 根据id获取数据 然后显示// 如果是添加操作,则不加载数据 直接显示空白内容if(state.operation === 'EDIT') {const index = Number(history.location.pathname.split("/")[2])const data = state.datas[index]// setFieldsValue为表单设置内容props.form.setFieldsValue({...data,content: BraftEditor.createEditorState(data.content)})}}, [])const handleSubmit = (event) => {event.preventDefault();validateFieldsAndScroll((err, values) => {if(!err) {// 如果是通过添加按钮进来的 那么拿到数据保存 然后跳转到home页面if(state.operation === 'ADD') {dispatch({type: 'insertData', data: {...values,content: values.content.toRAW()}});message.success("添加成功");history.push("/home");//如果是编辑文章进来的,更新数据 然后跳转到home} else if (state.operation === "EDIT") {const id = Number(history.location.pathname.split("/")[2]);dispatch({type: "updateData", id, data: {...values,content: values.content.toRAW(),}});message.success("更新成功");history.push("/home");}}})}return (<BasicLayout><div><Form onSubmit={handleSubmit}><FormItem labelAlign="left" label="文章标题">{getFieldDecorator('title', {rules: [{required: true,message: '请输入标题',}],})(<Input size="large" placeholder="请输入标题"/>)}</FormItem><FormItem size="large" label="文章摘要">{getFieldDecorator('brief', {rules: [{required: true,message: '请输入摘要',}],})(<Input.TextArea style={{fontSize: "16px"}} placeholder="请输入摘要"/>)}</FormItem><FormItem>{getFieldDecorator('isTop', {valuePropName: 'checked',})(<Checkbox>是否置顶</Checkbox>,)}</FormItem><FormItem label="文章正文">{getFieldDecorator('content', {validateTrigger: 'onBlur',rules: [{required: true,message: "请输入正文"}],})(<RichText />)}</FormItem><FormItem><Button size="large" type="primary" htmlType="submit">提交</Button></FormItem></Form></div></BasicLayout>)}// 为Edit注入formexport default withRouter(Form.create()(Edit))
上面的代码虽然有点长,但是都是比较容易理解的。注意,虽然我们没有为RichText传入value和onChange,但是由于RichText和表单项进行了双向绑定,所以表单会注入value和onChange。
Login
Login页面应该是最简单的,只要用我们在前面antd表单示例里面的表单就可以完成,所以直接上代码如下
import React from 'react'import {withRouter} from 'react-router-dom'import {Form, Button, Input, message } from 'antd'import LoginLayout from './../../layouts/LoginLayout'function Login(props) {const { form, history } = props;const FormItem = Form.Item;const {getFieldDecorator, validateFields} = formconst handleSubmit = (event) => {event.preventDefault();validateFields((err, values) => {if (!err) {if (values.adminId === "123" && values.password === "123") {sessionStorage.setItem("login", true);message.success("登录成功");history.push("/home")} else {message.error("用户名或密码错误")}}})}return (<LoginLayout><Form onSubmit={handleSubmit}><FormItem labelAlign="left" label="用户名">{getFieldDecorator('adminId', {rules: [{required: true,message: '请输入用户名',}],})(<Input size="large" placeholder="请输入用户名"/>)}</FormItem><FormItem size="large" label="密码">{getFieldDecorator('password', {rules: [{required: true,message: '请输入密码',}],})(<Input.Password size="large" placeholder="请输入密码"/>)}</FormItem><FormItem><Button size="large" type="primary" htmlType="submit">提交</Button></FormItem></Form></LoginLayout>)}export default withRouter(Form.create()(Login))
收尾
在这里还有一个小地方没有处理,那就是Header,里面的a标签的点击事件没有处理,并且我们希望在登录的情况下显示”写博客”,以及在登录的情况下显示”退出登录”,在未登录的情况下显示”登录”,所以修改Header如下(由于要用到history,所以要在Layout里给Header传入history,但是Layout也没有history,所以要在Home, Edit, Display, Login中给用到的Layout传入history,这里的代码就不贴出了,想必这样的事情对现在的你应该已经很简单了)
import React, {useContext} from 'react'import styles from './index.module.css'import {Context} from './../../Provider'function Header(props) {const {history} = props;const {dispatch} = useContext(Context);const login = sessionStorage.getItem("login");const toEdit = (e) => {e.preventDefault();dispatch({type: "changeOperation", operation: "ADD"});history.push("/edit");}const toHome = (e) => {e.preventDefault()history.push("/home")}const logout = (e) => {e.preventDefault()sessionStorage.removeItem("login");history.push("/login");}const login_ = (e) => {e.preventDefault()history.push("/login");}return (<div className={styles.header}><div className={styles.nav}><ul><li><a href="/home" onClick={toHome}>首页</a></li>{login && <li><a href="/edit" onClick={toEdit}>写博客</a></li>}{login ? <li><a href="/login" onClick={logout}>退出登录</a></li> : <li><a href="/login" onClick={login_}>登录</a></li>}</ul></div><div className={styles.desc}>Coder</div></div>)}export default Header
接下来就是数据的持久化,我们希望将数据能够保存到localStorage,这样当页面刷新,关闭页面、浏览器,关机数据都能够保存。修改Provider.jsx
import React, {useReducer} from 'react'export const Context = React.createContext();const saveData = (datas) => {localStorage.setItem("datas",JSON.stringify(datas) || [])}const loadData = () => {// 如果datas没有内容,则为空数组,因为后面用到datas.map,放止报错return JSON.parse(localStorage.getItem("datas")) || []}const reducer = (state, action) => {const tempDatas = state.datas// 每次对数据进行操作后都保存数据switch(action.type) {case 'insertData':tempDatas[tempDatas.length] = action.data;saveData(tempDatas)return {...state, datas: tempDatas}case 'updateData':tempDatas[action.id] = action.datasaveData(tempDatas)return {...state, datas: tempDatas}case 'deleteData':tempDatas.splice(action.id, 1)saveData(tempDatas)return {...state, datas: tempDatas}case 'changeOperation':return {...state, operation: action.operation}default:return state}}const initState = {// 初始化从localStorage中读取数据datas: loadData(),operation: 'ADD'}function Provider(props) {const [state, dispatch] = useReducer(reducer, initState)const {children} = propsreturn (<Context.Provider value={{state, dispatch}}>{children}</Context.Provider>)}export default Provider
