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()); // 加入 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);
});
});

findByIdAndDeletefindByIdAndUpdate 等同理

最好在每个 .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 配置:

npx eslint --init

该配置保存在 .eslintrc.js 文件中,.eslintignore 文件保存忽略的文件

创建单独的脚本进行 linting

"lint": "eslint ."

测试 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'); // the actual Express application
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": "node --test"

测试文件在 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 ,我们无需在测试中导入关键字,如 describetestexpect

测试是否渲染了笔记内容:

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 组件中分离出来,进入它自己的存储

存储器的状态通过动作改变,可以自定义一个动作:

{
type: 'INCREMENT';
}

动作对应用状态的影响通过 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 访问