Web app 基础
web 开发第一原则:始终打开控制台
可以在控制台查看网络信息
传统 web 应用:获取 html,获取 css,获取 JavaScript
html 是树状的结构(DOM )
可以从控制台操作 DOM
接下来是 AJAX Asynchronous JavaScript and XML 时代
React 入门
React 简介
先安装 Node.js
创建应用:npm create vite@latest part1 -- --template react
const App = ( ) => ( <div > <p > Hello world</p > </div > );
.jsx 文件看起来返回的是 HTML 标记,实际上底层转化为了 JavaScript
注意 JSX 的每个标签都必须关闭,即写成 <br />
使用 props
向组件传递数据
React 组件名称必须大写
React 组件内容需要包含一个根元素,故返回值的最外层通常要加上 <div></div>
或 <></>
组件状态,事件处理
可以在组件内部定义助手函数
结构可用于参数传递时,如
const Hello = ({ name, age } ) => {};
状态钩子的用法:
import { useState } from 'react' ;const App = ( ) => { const [counter, setCounter] = useState (0 ); setTimeout (() => setCounter (counter + 1 ), 1000 ); return <div > {counter}</div > ; };export default App ;
事件处理:注意要求的事件处理器是对函数的引用
const App = ( ) => { const [counter, setCounter] = useState (0 ); const handleClick = ( ) => { console .log ('clicked' ); }; return ( <div > <div > {counter}</div > <button onClick ={handleClick} > plus</button > </div > ); };
深入 React 应用调试
复杂状态:可以使用对象或数组作为状态
const App = ( ) => { const [clicks, setClicks] = useState ({ left : 0 , right : 0 }); const handleLeftClick = ( ) => setClicks ({ ...clicks, left : clicks.left + 1 }); const handleRightClick = ( ) => setClicks ({ ...clicks, right : clicks.right + 1 }); return ( <div > {clicks.left} <button onClick ={handleLeftClick} > left</button > <button onClick ={handleRightClick} > right</button > {clicks.right} </div > ); };
useState
函数只能 直接在 component 中定义
与服务端通信
从渲染集合到模块学习
可以使用 map
函数从数组中生成渲染对象,注意要有 key 属性
notes.map ((note ) => <li key ={note.id} > {note.content}</li > );
表单
输入框处理:
const App = (props ) => { const [newNote, setNewNote] = useState ('a new note...' ); const handleNoteChange = (event ) => { console .log (event.target .value ); setNewNote (event.target .value ); }; return ( <div > <form onSubmit ={addNote} > <input value ={newNote} onChange ={handleNoteChange} /> <button type ='submit' > save</button > </form > </div > ); };
从服务器获取数据
使用 json-server ,安装为开发依赖项:
npm install json-server --save-dev
在 package.json
文件的 scripts
部分做一个小小的补充:
"server" : "json-server -p3001 --watch db.json"
数据存储在 db.json
中
使用 axios 库来代替浏览器和服务器之间的通信
effect hook 可以对函数组件进行副作用
useEffect (() => { console .log ('effect' ); axios.get ('http://localhost:3001/notes' ).then ((response ) => { console .log ('promise fulfilled' ); setNotes (response.data ); }); }, []);
useEffect
第二个参数用于指定效果的运行频率,这里使用 []
表示只在第一次渲染时运行
在服务端将数据 Alert 出来
json-server 比较接近 RESTful 的 API
在 REST 中,把单个数据对象称作资源,可以通过 note/3
找到一个单独的笔记,其中 3
表示该笔记的 id
json-server 的所有数据用 json 格式发送
向服务器发送数据:
addNote = (event ) => { event.preventDefault (); const noteObject = { content : newNote, date : new Date (), important : Math .random () > 0.5 , }; axios.post ('http://localhost:3001/notes' , noteObject).then ((response ) => { setNotes (notes.concat (response.data )); setNewNote ('' ); }); };
使用 HTTP PUT 请求来替换整个笔记:
const toggleImportanceOf = (id ) => { const url = `http://localhost:3001/notes/${id} ` ; const note = notes.find ((n ) => n.id === id); const changedNote = { ...note, important : !note.important }; axios.put (url, changedNote).then ((response ) => { setNotes (notes.map ((note ) => (note.id !== id ? note : response.data ))); }); };
将其分离成一个单独的模块:
import axios from 'axios' ;const baseUrl = 'http://localhost:3001/notes' ;const getAll = ( ) => { const request = axios.get (baseUrl); return request.then ((response ) => response.data ); };const create = (newObject ) => { const request = axios.post (baseUrl, newObject); return request.then ((response ) => response.data ); };const update = (id, newObject ) => { const request = axios.put (`${baseUrl} /${id} ` , newObject); return request.then ((response ) => response.data ); };export default { getAll, create, update };
直接调用:
const App = ( ) => { useEffect (() => { noteService.getAll ().then ((initialNotes ) => { setNotes (initialNotes); }); }, []); const toggleImportanceOf = (id ) => { const note = notes.find ((n ) => n.id === id); const changedNote = { ...note, important : !note.important }; noteService.update (id, changedNote).then ((returnedNote ) => { setNotes (notes.map ((note ) => (note.id !== id ? note : returnedNote))); }); }; const addNote = (event ) => { event.preventDefault (); const noteObject = { content : newNote, date : new Date ().toISOString (), important : Math .random () > 0.5 , }; noteService.create (noteObject).then ((returnedNote ) => { setNotes (notes.concat (returnedNote)); setNewNote ('' ); }); }; };
给 React 应用加点样式
除了定义另外一个 css 文件并引入的方法外,还支持内联样式
const Footer = ( ) => { const footerStyle = { color : 'green' , fontStyle : 'italic' , fontSize : 16 , }; return ( <div style ={footerStyle} > <em > Note app, Department of Computer Science, University of Helsinki 2022 </em > </div > ); };
用 NodeJS 和 Express 写服务端程序
Node.js 与 Express
可以在 package.json 文件中写脚本,并 npm run 脚本名
运行
"scripts" : { "start" : "node index.js" , "test" : "echo \"Error: no test specified\" && exit 1" } ,
这里使用 express 库的接口:
const express = require ('express' )const app = express ()let notes = [...] app.get ('/' , (request, response ) => { response.send ('<h1>Hello World!</h1>' ) }) app.get ('/api/notes' , (request, response ) => { response.json (notes) })const PORT = 3001 app.listen (PORT , () => { console .log (`Server running on port ${PORT} ` ) })
使用 nodemon 可以在代码修改后自动重启服务器
npm install --save-dev nodemon
REST API:
URL
动作
功能
notes/10
GET
获取一个资源
notes
GET
获取集合中所有的资源
notes
POST
基于 request data 创建新的资源
notes/10
DELETE
移除该资源
notes/10
PUT
用 request data 替代该资源
notes/10
PATCH
用 request data 替代该资源的一部分
获取单独的资源:
app.get ('/api/notes/:id' , (request, response ) => { const id = Number (request.params .id ); const note = notes.find ((note ) => note.id === id); response.json (note); });
删除资源:
app.delete ('/api/notes/:id' , (request, response ) => { const id = Number (request.params .id ); notes = notes.filter ((note ) => note.id !== id); response.status (204 ).end (); });
vscode 可以装 REST client 插件测试
接收数据要加入 json 解释器:
app.use (express.json ()); const generateId = ( ) => { const maxId = notes.length > 0 ? Math .max (...notes.map ((n ) => n.id )) : 0 ; return maxId + 1 ; }; app.post ('/api/notes' , (request, response ) => { const body = request.body ; if (!body.content ) return response.status (400 ).json ({ error : 'content missing' }); const note = { content : body.content , important : body.important || false , date : new Date (), id : generateId (), }; notes = notes.concat (note); response.json (note); });
注意 REST 测试时要指明数据类型:
POST http://localhost:3001/api/notes/ HTTP/1.1 content-type : application/json{ "name" : "sample" , "time" : "Wed, 21 Oct 2015 18:27:50 GMT" }
json-parser 是一个中间件 ,可以用来处理 request 和 response 的函数
把应用部署到网上
因为一些跨域请求默认是被禁止的,故可以使用 cors 中间件来运行其他来源的请求
const cors = require ('cors' ); app.use (cors ());
部署后端
部署前端:
"scripts" : { "build:ui" : "rm -rf build && cd ../part2-notes/ && npm run build && cp -r build ../notes-backend" , "deploy" : "git push heroku main" , "deploy:full" : "npm run build:ui && git add . && git commit -m uibuild && npm run deploy" , "logs:prod" : "heroku logs --tail" }
因为前端修改为了相对路径(前端和后端部署在了一起),故无法在本地调试前端,可以在本地添加一个代理:
"proxy" : "http://localhost:3001"
将数据存入 MongoDB
使用的数据库为 MongoDB,常用的服务提供商是 MongoDB Atlas
使用 mongoose 库,其提供了一个更高级别的 API
const mongoose = require ('mongoose' );const password = process.argv [2 ];const url = `mongodb+srv://fullstack:${password} @cluster0.o1opl.mongodb.net/myFirstDatabase?retryWrites=true&w=majority` ; mongoose.connect (url);const noteSchema = new mongoose.Schema ({ content : String , date : Date , important : Boolean , });const Note = mongoose.model ('Note' , noteSchema); const note = new Note ({ content : 'HTML is Easy' , date : new Date (), important : true , }); note.save ().then ((result ) => { console .log ('note saved!' ); mongoose.connection .close (); });Note .find ({}).then ((result ) => { result.forEach ((note ) => { console .log (note); }); mongoose.connection .close (); });
可以修改模式的 toJSON
方法:
noteSchema.set ('toJSON' , { transform : (document , returnedObject ) => { returnedObject.id = returnedObject._id .toString (); delete returnedObject._id ; delete returnedObject.__v ; }, });
一般使用环境变量,安装 dotenv 库
在 .env 文件中加入
MONGODB_URI=mongodb+srv://fullstack:<password>@cluster0.o1opl.mongodb.net/noteApp?retryWrites=true&w=majority PORT=3001
该文件的信息的使用方法:
require ('dotenv' ).config ();const PORT = process.env .PORT ; app.listen (PORT , () => { console .log (`Server running on port ${PORT} ` ); });
mongoose 有 findById
方法,获取一个单独的笔记方法如下
app.get ('/api/notes/:id' , (request, response ) => { Note .findById (request.params .id ).then ((note ) => { response.json (note); }); });
findByIdAndDelete
和 findByIdAndUpdate
等同理
最好在每个 .then
块之后使用 .catch
捕捉异常:
.catch (error => { console .log (error) response.status (400 ).send ({ error : 'malformatted id' }) })
可以进一步把错误处理程序封装到一个中间件:
app.get ('/api/notes/:id' , (request, response, next ) => { Note .findById (request.params .id ) .then () .catch ((error ) => next (error)); });
我们的错误处理程序如下:
const errorHandler = (error, request, response, next ) => { console .error (error.message ); if (error.name === 'CastError' ) return response.status (400 ).send ({ error : 'malformatted id' }); next (error); }; app.use (errorHandler);
ESLint 与代码检查
可以使用 Mongoose 中的验证功能来检查输入格式是否正确:
const noteSchema = new mongoose.Schema ({ content : { type : String , minLength : 5 , required : true , }, date : { type : Date , required : true , }, important : Boolean , });
中间件可以添加一些信息:
const errorHandler = (error, request, response, next ) => { console .error (error.message ); if (error.name === 'CastError' ) return response.status (400 ).send ({ error : 'malformatted id' }); else if (error.name === 'ValidationError' ) return response.status (400 ).json ({ error : error.message }); next (error); };
注意 findOneAndUpdate
不会自动验证,可以显示指定:
Note .findByIdAndUpdate (request.params .id , request.body , { new : true , runValidators : true , context : 'query' , });
ESlint 是 JavaScript 的静态分析工具
初始化一个默认的 ESlint 配置:
该配置保存在 .eslintrc.js
文件中,.eslintignore
文件保存忽略的文件
创建单独的脚本进行 linting
测试 Express 服务端程序, 以及用户管理
从后端结构到测试入门
项目的结构一般如下:
├── index.js ├── app.js ├── build │ └── ... ├── controllers │ └── notes.js ├── models │ └── note.js ├── package-lock.json ├── package.json ├── utils │ ├── config.js │ ├── logger.js │ └── middleware.js
utils/logger.js
文件中存放打印模块:
utils/logger.js const info = (...params ) => { console .log (...params); };const error = (...params ) => { console .error (...params); };module .exports = { info, error };
utils/config.js
文件中处理环境变量:
utils/config.js require ('dotenv' ).config ();const PORT = process.env .PORT ;const MONGODB_URI = process.env .MONGODB_URI ;module .exports = { MONGODB_URI , PORT };
utils/middleware.js
模块中是自定义的中间件:
utils/middleware.js const logger = require ('./logger' )const requestLogger = (request, response, next ) => {...}const unknownEndpoint = (request, response ) => { response.status (404 ).send ({ error : 'unknown endpoint' }) }const errorHandler = (error, request, response, next ) => {...}module .exports = { requestLogger, unknownEndpoint, errorHandler }
与 notes
相关的路由现在都在 controllers/notes.js
模块中:
controllers/notes.js const notesRouter = require ('express' ).Router ()const Note = require ('../models/note' ) notesRouter.get ('/' , (request, response ) => {...}) notesRouter.get ('/:id' , (request, response, next ) => {...}) notesRouter.post ('/' , (request, response, next ) => {...}) notesRouter.delete ('/:id' , (request, response, next ) => {...}) notesRouter.put ('/:id' , (request, response, next ) => {...})module .exports = notesRouter
这些路由对象相当于是中间件,注意 /api/notes/:id
被缩短为了 /:id
models/note.js
文件定义了 Mongoose 模式:
models/note.js const mongoose = require ('mongoose' )const noteSchema = new mongoose.Schema ({...}) noteSchema.set ('toJSON' , {...})module .exports = mongoose.model ('Note' , noteSchema)
index.js
文件用于启动应用:
index.js const app = require ('./app' ); const http = require ('http' );const config = require ('./utils/config' );const logger = require ('./utils/logger' );const server = http.createServer (app); server.listen (config.PORT , () => { logger.info (`Server running on port ${config.PORT} ` ); });
app.js
使用到了大量的中间件:
app.js const config = require ('./utils/config' );const express = require ('express' );const app = express ();const cors = require ('cors' );const notesRouter = require ('./controllers/notes' );const middleware = require ('./utils/middleware' );const logger = require ('./utils/logger' );const mongoose = require ('mongoose' ); logger.info ('connecting to' , config.MONGODB_URI ); mongoose.connect (config.MONGODB_URI ); app.use (cors ()); app.use (express.static ('build' )); app.use (express.json ()); app.use (middleware.requestLogger ); app.use ('/api/notes' , notesRouter); app.use (middleware.unknownEndpoint ); app.use (middleware.errorHandler );module .exports = app;
utils/for_testing.js
存放自动化测试文件,如
utils/for_testing.js const reverse = (string ) => { return string.split ('' ).reverse ().join ('' ); };const average = (array ) => { const reducer = (sum, item ) => { return sum + item; }; return array.reduce (reducer, 0 ) / array.length ; };module .exports = { reverse, average };
这里使用 node 的内置测试库 node:test
定义 npm test:
package.json
测试文件在 test/reverse.test.js
中:
test/reverse.test.js const { test } = require ('node:test' );const assert = require ('node:assert' );const reverse = require ('../utils/for_testing' ).reverse ;test ('reverse of a' , () => { const result = reverse ('a' ); assert.strictEqual (result, 'a' ); });
测试后端应用
使用 cross-env 包来实现跨平台兼容,并使用 NODE_ENV
环境变量来定义应用的执行模式:
package.json "scripts" : { "start" : "cross-env NODE_ENV=production node index.js" , "dev" : "cross-env NODE_ENV=development nodemon index.js" , "test" : "cross-env NODE_ENV=test jest --verbose --runInBand" , } ,
为测试设置单独的数据库:
const MONGODB_URI = process.env .NODE_ENV === 'test' ? process.env .TEST_MONGODB_URI : process.env .MONGODB_URI ;
TEST_MONGODB_URI=mongodb+srv://fullstack:<password>@cluster0.o1opl.mongodb.net/testNoteApp?retryWrites=true &w=majority
使用 supertest 包测试 API
const mongoose = require ('mongoose' );const supertest = require ('supertest' );const app = require ('../app' );const api = supertest(app);test ('notes are returned as json' , async () => { await api .get ('/api/notes' ) .expect (200 ) .expect ('Content-Type' , /application\/json/ ); });afterAll (() => { mongoose.connection .close (); });
expect(response.body).toHaveLength(2)
和 expect(response.body[0].content).toBe('HTML is easy')
等方便比较内容
类似的,还有 beforeEach()
函数在所有测试开始之前执行
可以使用 express-async-errors 来避免代码中大量出现的 try...catch...
块
用户管理
Mongo 不是关系型数据库,故本身不支持 join 操作,只能自定义模式
const userSchema = new mongoose.Schema ({ username : String , name : String , passwordHash : String , notes : [ { type : mongoose.Schema .Types .ObjectId , ref : 'Note' , }, ], });
另一边类似的
const noteSchema = new mongoose.Schema ({ content : { type : String , required : true , minlength : 5 , }, date : Date , important : Boolean , user : { type : mongoose.Schema .Types .ObjectId , ref : 'User' , }, });
安装 bcrypt 软件包来生成密码散列:
const saltRounds = 10 ;const passwordHash = await bcrypt.hash (password, saltRounds);
使用 mongoose-unique-validator 包来实现唯一性检查
Mongoose 库实现了集合之间的连接
const users = await User .find ({}).populate ('notes' );
密钥认证
在后台实现基于令牌的认证的支持
安装 jsonwebtoken 库
登录功能的代码放在 controllers/login.js
文件中
const jwt = require ('jsonwebtoken' );const bcrypt = require ('bcrypt' );const loginRouter = require ('express' ).Router ();const User = require ('../models/user' ); loginRouter.post ('/' , async (request, response) => { const { username, password } = request.body ; const user = await User .findOne ({ username }); const passwordCorrect = user === null ? false : await bcrypt.compare (password, user.passwordHash ); if (!(user && passwordCorrect)) return response.status (401 ).json ({ error : 'invalid username or password' }); const userForToken = { username : user.username , id : user._id }; const token = jwt.sign ( userForToken, process.env .SECRET , { expiresIn : 60 * 60 } ); response .status (200 ) .send ({ token, username : user.username , name : user.name }); });module .exports = loginRouter;
将令牌从浏览器发送到服务器要使用授权头,有几种认证方案,这里使用 Bearer 方案,检验的方法是:
const jwt = require ('jsonwebtoken' )const getTokenFrom = request => { const authorization = request.get ('authorization' ) if (authorization && authorization.toLowerCase ().startsWith ('bearer ' )) { return authorization.substring (7 ) return null } notesRouter.post ('/' , async (request, response) => { const body = request.body const token = getTokenFrom (request) const decodedToken = jwt.verify (token, process.env .SECRET ) if (!decodedToken.id ) { return response.status (401 ).json ({ error : 'token missing or invalid' }) } const user = await User .findById (decodedToken.id ) })
发送要加上头:
Authorization : Bearer eyJhbGciOiJIUzI1NiIsInR5c2VybmFtZSI6Im1sdXVra2FpIiwiaW
测试 React 应用
完成前台的登录功能
有条件地渲染登录窗口和笔记窗口:
{ user === null && loginForm (); } { user !== null && noteForm (); }
登录成功的用户信息储存在 token 中,故 noteService 模块中应添加
let token = null ;const setToken = (newToken ) => { token = `bearer ${newToken} ` ; };const create = async (newObject ) => { const config = { headers : { Authorization : token } }; const response = await axios.post (baseUrl, newObject, config); return response.data ; };
每次刷新后用户信息就消失了,可以通过将登录信息保存到本地存储中解决:
window .localStorage .setItem ('loggedNoteappUser' , JSON .stringify (user));
使用效果钩子 在进入页面时查找本地存储
useEffect (() => { const loggedUserJSON = window .localStorage .getItem ('loggedNoteappUser' ); if (loggedUserJSON) { const user = JSON .parse (loggedUserJSON); setUser (user); noteService.setToken (user.token ); } }, []);
props.children 与 proptypes
一个可以展开和折叠的组件:
import { useState } from 'react' ;const Togglable = (props ) => { const [visible, setVisible] = useState (false ); const hideWhenVisible = { display : visible ? 'none' : '' }; const showWhenVisible = { display : visible ? '' : 'none' }; const toggleVisibility = ( ) => { setVisible (!visible); }; return ( <div > <div style ={hideWhenVisible} > <button onClick ={toggleVisibility} > {props.buttonLabel}</button > </div > <div style ={showWhenVisible} > {props.children} <button onClick ={toggleVisibility} > cancel</button > </div > </div > ); };export default Togglable ;
如果想要在 Togglable 组件之外访问 visible 变量,可以使用 React 的 ref 机制:
import { useState, useEffect, useRef } from 'react' ;const noteFormRef = useRef ();const noteForm = ( ) => ( <Togglable buttonLabel ='new note' ref ={noteFormRef} > <NoteForm createNote ={addNote} /> </Togglable > );
Togglable 组件中:
import { useState, forwardRef, useImperativeHandle } from 'react' ;const Togglable = forwardRef ((props, ref ) => { useImperativeHandle (ref, () => { return { toggleVisibility }; }); });
通过 noteFormRef.current.toggleVisibility()
隐藏表单
可以使用 prop-types 包定义组件的预期和要求,如:
import PropTypes from 'prop-types' const Togglable = React .forwardRef ((props, ref ) => {...})Togglable .propTypes = { buttonLabel : PropTypes .string .isRequired }
测试 React 应用
这里使用 Vitest 测试,安装一些有用的测试库:
npm install --save-vitest vitest jsdom npm install --save-dev @testing-library/react @testing-library/jest-dom
配置测试脚本:
"scripts" : { "test" : "vitest run" }
创建 testSetup.js
的文件,用于重置模拟浏览器的 jsdom:
testSetup.js import { afterEach } from 'vitest' ;import { cleanup } from '@testing-library/react' ;import '@testing-library/jest-dom/vitest' ;afterEach (() => { cleanup (); });
将 vite.config.js
文件扩展如下:
vite.config.js export default defineConfig ({ test : { environment : 'jsdom' , globals : true , setupFiles : './testSetup.js' , }, });
通过设置 globals: true
,我们无需在测试中导入关键字,如 describe
、test
和 expect
。
测试是否渲染了笔记内容:
import { render, screen } from '@testing-library/react' ;import Note from './Note' ;test ('renders content' , () => { const note = { content : 'Component testing is done with react-testing-library' , important : true , }; render (<Note note ={note} /> ); const element = screen.getByText ( 'Component testing is done with react-testing-library' ); expect (element).toBeDefined (); });
screen.debug()
命令可以将一个组件的 HTML 打印到终端
在处理事件时,使用 user-event 库:
npm install --save-dev @testing-library/user-event
事件处理程序是一个用 Jest 定义的 mock 函数
const mockHandler = vi.fn ();
一个 session 被启动以与渲染的组件进行交互。
const user = userEvent.setup ();
await user.click (button);const button = screen.getByText ('make not important' );
端到端测试
前面讲的都是单元测试,实际上也可以进行整体测试
使用 Playwright
describe ('Note app' , function ( ) { it ('front page can be opened' , function ( ) { cy.visit ('http://localhost:3000' ); cy.contains ('Notes' ); cy.contains ( 'Note app, Department of Computer Science, University of Helsinki 2022' ); }); });
向窗体中输入信息:
describe ('Note app' , function ( ) { it ('user can log in' , function ( ) { cy.contains ('login' ).click (); cy.get ('#username' ).type ('mluukkai' ); cy.get ('#password' ).type ('salainen' ); cy.get ('#login-button' ).click (); cy.contains ('Matti Luukkainen logged in' ); }); });
利用 Redux 进行状态管理
Flux 架构与 Redux
在 Redux 中,状态被完全从 React 组件中分离出来,进入它自己的存储
存储器的状态通过动作改变,可以自定义一个动作:
动作对应用状态的影响通过 reducer 实现,如:
const counterReducer = (state = 0 , action ) => { switch (action.type ) { case 'INCREMENT' : return state + 1 ; default : return state; } };
reducer 作为创建存储的一个参数:
const store = createStore (counterReducer);
操作则使用 dispatch()
方法,如:
store.dispatch ({ type : 'INCREMENT' });console .log (store.getState ());
注意 reducer 必须是一个纯函数
让应用能够访问存储的方法:
import { Provider } from 'react-redux' ;ReactDOM .createRoot (document .getElementById ('root' )).render ( <Provider store ={store} > <App /> </Provider > );
现在使用 useDispatch()
分派动作,相应的,使用 useSelector
获取数据:
import { useSelector, useDispatch } from 'react-redux' ;const App = ( ) => { const dispatch = useDispatch (); const toggleImportance = (id ) => { dispatch (toggleImportanceOf (id)); }; const notes = useSelector ((state ) => state); };
再来点 reducers
可以使用 Redux Toolkit 组合两个 reducer:
const store = configureStore ({ reducer : { notes : noteReducer, filter : filterReducer, }, });
用 createSlice 函数创建 reducer 和相应的动作创建器
const noteSlice = createSlice ({ name : 'notes' , initialState, reducers : { createNote (state, action ) { const content = action.payload ; state.push ({ content, important : false , id : generateId (), }); }, }, });
注意到 state
可变了
在 Redux 应用中与后端通信
使用 Redux Thunk 库来实现动作
export const initializeNotes = ( ) => { return async (dispatch) => { const notes = await noteService.getAll (); dispatch (setNotes (notes)); }; };
React Query,useReducer 和 context
用 React Query 存储并管理从服务器检索的数据
将这个库中的函数传递给整个应用:
import { QueryClient , QueryClientProvider } from 'react-query' ;const queryClient = new QueryClient ();ReactDOM .createRoot (document .getElementById ('root' )).render ( <QueryClientProvider client ={queryClient} > <App /> </QueryClientProvider > );
获取方法:
const result = useQuery ('notes' , () => axios.get ('http://localhost:3001/notes' ).then ((res ) => res.data ) );
使用 mutation 创建新笔记并在本地插入:
const App = ( ) => { const newNoteMutation = useMutation (createNote, { onSuccess : (newNote ) => { const notes = queryClient.getQueryData ('notes' ); queryClient.setQueryData ('notes' , notes.concat (newNote)); }, }); };
useReducer
提供了为应用创建状态的机制:
const [counter, counterDispatch] = useReducer (counterReducer, 0 );
使用 createContext()
创建 context,这样就不需要层层传递给各组件了:
<CounterContext .Provider value={[counter, counterDispatch]}> ... </CounterContext .Provider >
其他组件可以使用 useContext
访问