页面编写

antd表单的使用

要使用antd,先下载antd

  1. cnpm install antd --save

首先看一个表单的例子,在src下新建一个test文件夹,新建TextAntdForm.jsx,内容如下(先不管看得懂看不懂,后面解释)

  1. import React from 'react'
  2. import {Form, Input, Button} from 'antd'
  3. function TextAntdForm(props) {
  4. const {getFieldDecorator, validateFields} = props.form
  5. const handleSubmit = (event) => {
  6. event.preventDefault();
  7. validateFields((error, values) => {
  8. if (!error) {
  9. console.log(values);
  10. }
  11. })
  12. }
  13. return(
  14. <div style={{width: "300px", margin: "100px auto", fontFamily: "Consolas, '楷体'"}}>
  15. <Form onSubmit={handleSubmit}>
  16. <Form.Item label="用户名">
  17. {getFieldDecorator('title', {
  18. rules: [{
  19. required: true,
  20. message: '请输入用户名',
  21. }],
  22. })(
  23. <Input size="large" placeholder="请输入用户名"/>
  24. )}
  25. </Form.Item>
  26. <Form.Item label="密码">
  27. {getFieldDecorator('password', {
  28. rules: [{
  29. required: true,
  30. message: '请输入密码',
  31. }],
  32. })(
  33. <Input.Password size="large" placeholder="请输入密码"/>
  34. )}
  35. </Form.Item>
  36. <Form.Item>
  37. <Button size="large" type="primary" htmlType="submit">提交</Button>
  38. </Form.Item>
  39. </Form>
  40. </div>
  41. )
  42. }
  43. export default Form.create()(TextAntdForm)

修改src/index.js渲染该组件(记得引入antd/dits/antd.css,否则antd组件没有样式)

  1. import React from 'react'
  2. import ReactDOM from 'react-dom';
  3. // import router from './router'
  4. import TestAntdForm from './test/TestAntdForm'
  5. import 'antd/dist/antd.css'
  6. import './common.css';
  7. // ReactDOM.render(router(), document.getElementById("root"));
  8. ReactDOM.render(<TestAntdForm />, document.getElementById("root"));

页面编写 - 图1
现在来解释上面的代码,首先看最后一行

  1. Form.create()(TextAntdForm)

还记得高阶组件吗,Form.create()就是一个高阶组件,他会向组件的props中注入form,form提供了一些API,这里使用了两个:

  • getFieldDecorator:用于和表单进行双向绑定
  1. getFieldDecorator('title', {
  2. rules: [{
  3. required: true,
  4. message: '请输入用户名',
  5. }],
  6. })(
  7. <Input size="large" placeholder="请输入用户名"/>
  8. )

上面将Input与表单项进行了绑定,getFieldDecorator接收两个参数,第一个参数是id,根据它可以获取输入控件的值或者设置输入控件的值,是必填项;第二个参数是options,里面可以有很多属性,这里使用了rules,定义了校验的规则,required表示是否必填,message表示未填时显示的消息文字。

  • validateFields:校验

里面接受一个回调函数,回调函数接收两个参数,第一个参数为error,当不满足校验规则时error的值非空,第二个参数是values,会将绑定表单的值以对象的形式传给values,键就是在getFieldDecorator传入的id。

上面在提交表单后,会调用Form的onSubmit回调函数,在回调函数,我们对数据进行了校验,如果没有问题的话,我们可以将输入表单的键值对以对象的形式获取到values,并打印出来
页面编写 - 图2

页面数据

我们将数据设置为一个数组datas,它的格式如下

  1. datas = [
  2. {title: , brief: , isTop: , content: }
  3. {title: , brief: , isTop: , content: }
  4. ]

Home组件根据datas展示数据,Edit和Display组件根据id和datas获取要展示的数据,由于多个组件都要用到数据,所以这里使用useContext和useReducer来分发数据。

在src下新建Provider.jsx,用来提供state和dispatch,内容如下

  1. import React, {useReducer} from 'react'
  2. export const Context = React.createContext();
  3. const reducer = (state, action) => {
  4. const tempDatas = state.datas
  5. // 按道理case后面跟的都是常量,我这里为了简单
  6. switch(action.type) {
  7. case 'insertData':
  8. tempDatas[tempDatas.length] = action.data;
  9. return {...state, datas: tempDatas}
  10. case 'updateData':
  11. tempDatas[action.id] = action.data
  12. return {...state, datas: tempDatas}
  13. case 'deleteData':
  14. tempDatas.splice(action.id, 1)
  15. return {...state, datas: tempDatas}
  16. case 'changeOperation':
  17. return {...state, operation: action.operation}
  18. default:
  19. return state
  20. }
  21. }
  22. const initState = {
  23. // 随便写的数据用来实验 随后会删掉
  24. datas: [{title: 'aaa', isTop: true, brief: 'hahah'}, content: '<p>123</p>'],
  25. // 用来判断是添加文章还是编辑文章
  26. operation: 'ADD'
  27. }
  28. function Provider(props) {
  29. const [state, dispatch] = useReducer(reducer, initState)
  30. const {children} = props
  31. return (
  32. <Context.Provider value={{state, dispatch}}>
  33. {children}
  34. </Context.Provider>
  35. )
  36. }
  37. export default Provider

更改router.js,在最上面加上Provider

  1. import Provider from './Provider'
  2. // 上面没有变化,除了import Provider,故此省略
  3. const router = () => {
  4. return (
  5. <Provider>
  6. <Router history={history}>
  7. <Switch>
  8. {routes.map((item, id) => {
  9. return RouteItem({ key: id, ...item, history: history })
  10. })}
  11. </Switch>
  12. </Router>
  13. </Provider>
  14. );
  15. };
  16. export default router

Home

观察Home页面
页面编写 - 图3
发现Home是由这一个个Item组成,Item中的数据正是datas数组中每一个元素的内容,在Home中新建文件夹Item,并在Item中新建index.jsx和index.module.css。index.jsx:

  1. import React from 'react'
  2. import styles from './index.module.css'
  3. import {Modal} from 'antd'
  4. const {confirm} = Modal
  5. function Item(props) {
  6. const {
  7. data,
  8. login,
  9. handleDelete,
  10. index,
  11. handleToEdit,
  12. handleToDisplay
  13. } = props;
  14. const toDisplay = (event) => {
  15. event.preventDefault();
  16. handleToDisplay(index)
  17. }
  18. const toEdit = (event) => {
  19. event.preventDefault();
  20. handleToEdit(index)
  21. }
  22. const toDelete = (event) => {
  23. event.preventDefault();
  24. confirm({
  25. title: `你确定要删除${data.title}`,
  26. content: '删除后内容不可恢复',
  27. onOk() {
  28. handleDelete(index)
  29. },
  30. onCancel() {
  31. },
  32. });
  33. }
  34. return (
  35. <div className={styles.item}>
  36. <h2><a href={`/display/${data.id}`} onClick={toDisplay}>{data.title}</a></h2>
  37. <hr />
  38. <div className={styles.abstract}>
  39. {data.brief}
  40. </div>
  41. <div className={styles.readmore}>
  42. <a href={`/display/${data.id}`} onClick={toDisplay}>阅读更多</a>
  43. </div>
  44. {/* 当登录失显示编辑本文和删除本文 */}
  45. {login ?
  46. <div className={styles.edit}>
  47. <a href={`/edit/${data.id}`} onClick={toEdit}>编辑本文</a>
  48. </div> : ""}
  49. {login ?
  50. <div className={styles.delete}>
  51. <a href="/delete" onClick={toDelete}>删除本文</a>
  52. </div>: ""}
  53. {/* 当isTop为1时显示置顶图标 */}
  54. {data.isTop ?
  55. <div className={styles.isTop}>
  56. <svg viewBox="0 0 1024 1024">
  57. <path d="M0 0h1024v1024z" fill="#7ED321"></path>
  58. <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>
  59. </svg>
  60. </div> : ""}
  61. </div>
  62. )
  63. }
  64. export default Item

注意到Item为展示组件,只负责数据的展示,而不负责数据的处理、获取,Item的数据、数据的操作都是从props中获取的,这些操作都由Item的容器组件Home来完成。

我们会将login保存在sessionStorage,login是一个布尔值,保存了是否登录的信息,true表示登录,false表示未登录,根据是否登录,决定是否将删除和编辑的操作暴露出来。同时我们也将根据data的isTop是否为true来显示是否置顶的svg图样(该图样来自CSDN的置顶图样)。

index.module.css

  1. .item {
  2. width: 100%;
  3. height: 150px;
  4. background-color: white;
  5. margin-bottom: 40px;
  6. border-radius: 4px;
  7. padding-left: 25px;
  8. padding-right: 20px;
  9. padding-top: 20px;
  10. position: relative;
  11. box-shadow:0px 0px 6px 6px #FFF;
  12. }
  13. .item a {
  14. text-decoration: none;
  15. color: #40759b;
  16. }
  17. .item a:hover {
  18. text-decoration: underline;
  19. }
  20. .abstract {
  21. width: 100%;
  22. padding-top: 30px;
  23. }
  24. .readmore {
  25. position: absolute;
  26. font-size: 14px;
  27. bottom: 10px;
  28. right: 10px;
  29. }
  30. .edit {
  31. position: absolute;
  32. font-size: 14px;
  33. bottom: 10px;
  34. right: 75px;
  35. }
  36. .delete {
  37. position: absolute;
  38. font-size: 14px;
  39. bottom: 10px;
  40. right: 140px;
  41. }
  42. .isTop {
  43. position: absolute;
  44. width: 50px;
  45. top: 0;
  46. right: 0;
  47. }

现在进入Home组件,修改Home/index.jsx如下

  1. import React, { useContext } from 'react'
  2. import BasicLayout from './../../layouts/BasicLayout'
  3. import Item from './components/Item'
  4. import {withRouter} from 'react-router-dom'
  5. import {Context} from './../../router'
  6. import { message } from 'antd'
  7. function Home(props) {
  8. const {history} = props
  9. // 使用useContext获取Provider提供的数据
  10. const {state, dispatch} = useContext(Context);
  11. const login = sessionStorage.getItem("login");
  12. const datas = state.datas;
  13. const handleDelete = (id) => {
  14. dispatch({type: "deleteData", id});
  15. message.success('删除成功');
  16. }
  17. const handleToEdit = (id) => {
  18. dispatch({type: "changeOperation", operation: "EDIT"})
  19. history.push(`/edit/${id}`);
  20. }
  21. const handleToDisplay = (id) => {
  22. history.push(`display/${id}`);
  23. }
  24. return (
  25. <div>
  26. <BasicLayout>
  27. {datas.map((data, index) => {
  28. return <Item
  29. index={index}
  30. data={data}
  31. key={index}
  32. login={login}
  33. handleDelete={handleDelete}
  34. handleToEdit={handleToEdit}
  35. handleToDisplay={handleToDisplay}
  36. />
  37. })}
  38. </BasicLayout>
  39. </div>
  40. )
  41. }
  42. export default withRouter(Home)

现在启动项目(npm start),观察到页面如下
页面编写 - 图4
说明Home页面已经成功了(由于在sessionStorage中没有login,所以删除本文和编辑本文均显示不出来,当点击阅读更多时,会跳转到Display的页面)。

Display

我们来观察Display的页面
页面编写 - 图5
发现Display页面有一个背景为白色的内容区和一个按钮,这个按钮根据是否有登录来决定是否暴露出来,所以我们在Display中新建一个components文件夹,在里面新建一个Content文件夹,在Content文件夹中新建index.jsx和index.module.css。首先Content是一个展示组件,所以它的数据全部都由Display提供,所有的数据操作也由Display传入回调函数进行处理。

index.jsx如下

  1. import React from 'react'
  2. import { Button } from 'antd'
  3. import styles from './index.module.css'
  4. function Content(props) {
  5. const {login, htmlContent, handleToEdit} = props
  6. const toEdit = () => {
  7. handleToEdit();
  8. }
  9. return(
  10. <div className={styles.display}>
  11. <div className="braft-output-content" style={{minHeight: "425px", backgroundColor: "#FFF", padding: "50px 25px", fontSize: "16px", maxWidth: "850px"}} dangerouslySetInnerHTML={{__html: htmlContent}} >
  12. </div>
  13. {login &&
  14. <div className={styles.edit}>
  15. <Button type="primary" onClick={toEdit}>编辑文章</Button>
  16. </div>
  17. }
  18. </div>
  19. )
  20. }
  21. export default Content

想必上面的代码还是比较容易理解的,index.module.css的内容如下

  1. .display {
  2. position: relative;
  3. }
  4. .display ul, .display ol {
  5. padding-left: 30px;
  6. }
  7. .edit {
  8. position: absolute;
  9. top: 10px;
  10. right: 10px;
  11. }

所以Display中的内容如下

  1. import React, {useContext} from 'react'
  2. import BasicLayout from './../../layouts/BasicLayout'
  3. import {withRouter} from 'react-router-dom'
  4. import Content from './components/Content'
  5. import {Context} from './../../Provider'
  6. import BraftEditor from 'braft-editor'
  7. function Display(props) {
  8. const {history} = props
  9. // 获取传过来的id
  10. const index = Number(history.location.pathname.split("/")[2])
  11. const {state, dispatch} = useContext(Context)
  12. const htmlContent = BraftEditor.createEditorState(state.datas[index].content).toHTML()
  13. const login = sessionStorage.getItem("login")
  14. const handleToEdit = () => {
  15. dispatch({type: "changeOperation", operation: "EDIT"});
  16. history.push(`/edit/${index}`);
  17. }
  18. return (
  19. <BasicLayout>
  20. <Content
  21. htmlContent={htmlContent}
  22. login = {login}
  23. handleToEdit = {handleToEdit}
  24. />
  25. </BasicLayout>
  26. )
  27. }
  28. export default withRouter(Display)

至此Display页面设计完毕。

Edit

在写Edit页面之前,来改造一下RichText组件,我们要将RichText做成展示组件,所有的数据都由Edit提供,所有的数据处理也由Edit处理,修改如下

  1. import React from 'react'
  2. import BraftEditor from 'braft-editor'
  3. import 'braft-editor/dist/index.css'
  4. function RichText(props) {
  5. const {value, onChange} = props
  6. const handleEditorChange = (editorState) => {
  7. onChange(editorState)
  8. }
  9. return (
  10. <BraftEditor
  11. value={value}
  12. onChange={handleEditorChange}
  13. />
  14. )
  15. }
  16. export default RichText

现在我们来看一下Edit页面的结构
页面编写 - 图6
我们使用antd的表单来做成这件事情,在前面已经介绍过antd表单的使用,所以这里不多加介绍,直接上代码

  1. import React, {useEffect, useContext} from 'react'
  2. import RichText from './components/RichText'
  3. import { withRouter } from 'react-router-dom'
  4. import { Form, Input, Button, message, Checkbox } from 'antd'
  5. import BasicLayout from './../../layouts/BasicLayout'
  6. import BraftEditor from 'braft-editor'
  7. import {Context} from './../../Provider'
  8. function Edit(props) {
  9. const {history} = props
  10. const FormItem = Form.Item;
  11. const { getFieldDecorator, validateFieldsAndScroll } = props.form;
  12. const {state, dispatch} = useContext(Context)
  13. useEffect(() => {
  14. // 当组件加载后 如果是编辑文章 根据id获取数据 然后显示
  15. // 如果是添加操作,则不加载数据 直接显示空白内容
  16. if(state.operation === 'EDIT') {
  17. const index = Number(history.location.pathname.split("/")[2])
  18. const data = state.datas[index]
  19. // setFieldsValue为表单设置内容
  20. props.form.setFieldsValue({
  21. ...data,
  22. content: BraftEditor.createEditorState(data.content)
  23. })
  24. }
  25. }, [])
  26. const handleSubmit = (event) => {
  27. event.preventDefault();
  28. validateFieldsAndScroll((err, values) => {
  29. if(!err) {
  30. // 如果是通过添加按钮进来的 那么拿到数据保存 然后跳转到home页面
  31. if(state.operation === 'ADD') {
  32. dispatch({type: 'insertData', data: {
  33. ...values,
  34. content: values.content.toRAW()
  35. }});
  36. message.success("添加成功");
  37. history.push("/home");
  38. //如果是编辑文章进来的,更新数据 然后跳转到home
  39. } else if (state.operation === "EDIT") {
  40. const id = Number(history.location.pathname.split("/")[2]);
  41. dispatch({type: "updateData", id, data: {
  42. ...values,
  43. content: values.content.toRAW(),
  44. }});
  45. message.success("更新成功");
  46. history.push("/home");
  47. }
  48. }
  49. })
  50. }
  51. return (
  52. <BasicLayout>
  53. <div>
  54. <Form onSubmit={handleSubmit}>
  55. <FormItem labelAlign="left" label="文章标题">
  56. {getFieldDecorator('title', {
  57. rules: [{
  58. required: true,
  59. message: '请输入标题',
  60. }],
  61. })(
  62. <Input size="large" placeholder="请输入标题"/>
  63. )}
  64. </FormItem>
  65. <FormItem size="large" label="文章摘要">
  66. {getFieldDecorator('brief', {
  67. rules: [{
  68. required: true,
  69. message: '请输入摘要',
  70. }],
  71. })(
  72. <Input.TextArea style={{fontSize: "16px"}} placeholder="请输入摘要"/>
  73. )}
  74. </FormItem>
  75. <FormItem>
  76. {getFieldDecorator('isTop', {
  77. valuePropName: 'checked',
  78. })(
  79. <Checkbox>
  80. 是否置顶
  81. </Checkbox>,
  82. )}
  83. </FormItem>
  84. <FormItem label="文章正文">
  85. {getFieldDecorator('content', {
  86. validateTrigger: 'onBlur',
  87. rules: [{
  88. required: true,
  89. message: "请输入正文"
  90. }],
  91. })(
  92. <RichText />
  93. )}
  94. </FormItem>
  95. <FormItem>
  96. <Button size="large" type="primary" htmlType="submit">提交</Button>
  97. </FormItem>
  98. </Form>
  99. </div>
  100. </BasicLayout>
  101. )
  102. }
  103. // 为Edit注入form
  104. export default withRouter(Form.create()(Edit))

上面的代码虽然有点长,但是都是比较容易理解的。注意,虽然我们没有为RichText传入value和onChange,但是由于RichText和表单项进行了双向绑定,所以表单会注入value和onChange。

Login

Login页面应该是最简单的,只要用我们在前面antd表单示例里面的表单就可以完成,所以直接上代码如下

  1. import React from 'react'
  2. import {withRouter} from 'react-router-dom'
  3. import {Form, Button, Input, message } from 'antd'
  4. import LoginLayout from './../../layouts/LoginLayout'
  5. function Login(props) {
  6. const { form, history } = props;
  7. const FormItem = Form.Item;
  8. const {getFieldDecorator, validateFields} = form
  9. const handleSubmit = (event) => {
  10. event.preventDefault();
  11. validateFields((err, values) => {
  12. if (!err) {
  13. if (values.adminId === "123" && values.password === "123") {
  14. sessionStorage.setItem("login", true);
  15. message.success("登录成功");
  16. history.push("/home")
  17. } else {
  18. message.error("用户名或密码错误")
  19. }
  20. }
  21. })
  22. }
  23. return (
  24. <LoginLayout>
  25. <Form onSubmit={handleSubmit}>
  26. <FormItem labelAlign="left" label="用户名">
  27. {getFieldDecorator('adminId', {
  28. rules: [{
  29. required: true,
  30. message: '请输入用户名',
  31. }],
  32. })(
  33. <Input size="large" placeholder="请输入用户名"/>
  34. )}
  35. </FormItem>
  36. <FormItem size="large" label="密码">
  37. {getFieldDecorator('password', {
  38. rules: [{
  39. required: true,
  40. message: '请输入密码',
  41. }],
  42. })(
  43. <Input.Password size="large" placeholder="请输入密码"/>
  44. )}
  45. </FormItem>
  46. <FormItem>
  47. <Button size="large" type="primary" htmlType="submit">提交</Button>
  48. </FormItem>
  49. </Form>
  50. </LoginLayout>
  51. )
  52. }
  53. export default withRouter(Form.create()(Login))

收尾

在这里还有一个小地方没有处理,那就是Header,里面的a标签的点击事件没有处理,并且我们希望在登录的情况下显示”写博客”,以及在登录的情况下显示”退出登录”,在未登录的情况下显示”登录”,所以修改Header如下(由于要用到history,所以要在Layout里给Header传入history,但是Layout也没有history,所以要在Home, Edit, Display, Login中给用到的Layout传入history,这里的代码就不贴出了,想必这样的事情对现在的你应该已经很简单了)

  1. import React, {useContext} from 'react'
  2. import styles from './index.module.css'
  3. import {Context} from './../../Provider'
  4. function Header(props) {
  5. const {history} = props;
  6. const {dispatch} = useContext(Context);
  7. const login = sessionStorage.getItem("login");
  8. const toEdit = (e) => {
  9. e.preventDefault();
  10. dispatch({type: "changeOperation", operation: "ADD"});
  11. history.push("/edit");
  12. }
  13. const toHome = (e) => {
  14. e.preventDefault()
  15. history.push("/home")
  16. }
  17. const logout = (e) => {
  18. e.preventDefault()
  19. sessionStorage.removeItem("login");
  20. history.push("/login");
  21. }
  22. const login_ = (e) => {
  23. e.preventDefault()
  24. history.push("/login");
  25. }
  26. return (
  27. <div className={styles.header}>
  28. <div className={styles.nav}>
  29. <ul>
  30. <li><a href="/home" onClick={toHome}>首页</a></li>
  31. {login && <li><a href="/edit" onClick={toEdit}>写博客</a></li>}
  32. {login ? <li><a href="/login" onClick={logout}>退出登录</a></li> : <li><a href="/login" onClick={login_}>登录</a></li>}
  33. </ul>
  34. </div>
  35. <div className={styles.desc}>
  36. Coder
  37. </div>
  38. </div>
  39. )
  40. }
  41. export default Header

接下来就是数据的持久化,我们希望将数据能够保存到localStorage,这样当页面刷新,关闭页面、浏览器,关机数据都能够保存。修改Provider.jsx

  1. import React, {useReducer} from 'react'
  2. export const Context = React.createContext();
  3. const saveData = (datas) => {
  4. localStorage.setItem("datas",JSON.stringify(datas) || [])
  5. }
  6. const loadData = () => {
  7. // 如果datas没有内容,则为空数组,因为后面用到datas.map,放止报错
  8. return JSON.parse(localStorage.getItem("datas")) || []
  9. }
  10. const reducer = (state, action) => {
  11. const tempDatas = state.datas
  12. // 每次对数据进行操作后都保存数据
  13. switch(action.type) {
  14. case 'insertData':
  15. tempDatas[tempDatas.length] = action.data;
  16. saveData(tempDatas)
  17. return {...state, datas: tempDatas}
  18. case 'updateData':
  19. tempDatas[action.id] = action.data
  20. saveData(tempDatas)
  21. return {...state, datas: tempDatas}
  22. case 'deleteData':
  23. tempDatas.splice(action.id, 1)
  24. saveData(tempDatas)
  25. return {...state, datas: tempDatas}
  26. case 'changeOperation':
  27. return {...state, operation: action.operation}
  28. default:
  29. return state
  30. }
  31. }
  32. const initState = {
  33. // 初始化从localStorage中读取数据
  34. datas: loadData(),
  35. operation: 'ADD'
  36. }
  37. function Provider(props) {
  38. const [state, dispatch] = useReducer(reducer, initState)
  39. const {children} = props
  40. return (
  41. <Context.Provider value={{state, dispatch}}>
  42. {children}
  43. </Context.Provider>
  44. )
  45. }
  46. export default Provider