# 准备
# 安装 node
# 初始化
创建一个新的模板
npm init |
进入到 json
文件,对 scripts
进行改动
"scripts": { | |
"start": "node index.js", // 添加 | |
"test": "echo \"Error: no test specified\" && exit 1" | |
}, |
在根目录创建 index.js
文件
console.log('Hello World!'); |
可以直接用 Node
运行 node index.js
也可以作为一个 npm
脚本运行 npm start
# Express
将 Express
安装
npm install express |
# nodemon
nodemon
是一个 Node
的 dev
工具,当 Node
代码发生变化时, nodemon
会自动重启 Node
服务,无需手动重启。
将 nodemon
定义为一个 开发依赖项 来安装它。
npm install --save-dev nodemon |
我们可以像这样用 nodemon 启动我们的应用。
node_modules/.bin/nodemon index.js |
这个命令很长,所以在 package.json
文件去添加一个专门的 npm
脚本
{ | |
// .. | |
"scripts": { | |
"start": "node index.js", | |
"dev": "nodemon index.js", // 添加 | |
"test": "echo \"Error: no test specified\" && exit 1" | |
}, | |
// .. | |
} |
在脚本中不需要指定 nodemon
的 node/modules/.bin/nodemon
路径,因为 _npm 自动知道从该目录中搜索该文件。
我们现在可以用命令在开发模式下启动服务器。
npm run dev |
示例代码
const express = require('express') // 这是 express 的引用 | |
const app = express() // 创建一个 express 实例 | |
let notes = [ | |
{ | |
id: 1, | |
content: "HTML is easy", | |
important: true | |
}, | |
{ | |
id: 2, | |
content: "Browser can execute only JavaScript", | |
important: false | |
}, | |
{ | |
id: 3, | |
content: "GET and POST are the most important methods of HTTP protocol", | |
important: true | |
} | |
] | |
app.use(express.json()) // 添加 json 解析中间件 | |
app.get('/', (request, response) => { // 添加一个路由 | |
response.send('<h1>Hello World!</h1>') // 响应一个字符串 | |
}) | |
app.get('/api/notes', (request, response) => { // 在 /api/notes 路径下响应一个 json 数据 | |
response.json(notes) | |
}) | |
const generateId = () => { // 生成 id | |
const maxId = notes.length > 0 | |
? Math.max(...notes.map(n => n.id)) | |
: 0 | |
return maxId + 1 | |
} | |
app.post('/api/notes', (request, response) => { // 在 api/notes 路径下 post 请求后响应一个 json 数据 | |
const body = request.body | |
if (!body.content) { | |
return response.status(400).json({ | |
error: 'content missing' | |
}) | |
} | |
const note = { | |
content: body.content, | |
important: body.important || false, | |
id: generateId(), | |
} | |
notes = notes.concat(note) | |
response.json(note) | |
}) | |
app.get('/api/notes/:id', (request, response) => { // 在 /api/notes/:id 路径下响应一个 json 数据 | |
const id = Number(request.params.id) | |
const note = notes.find(note => note.id === id) | |
if (note) { | |
response.json(note) | |
} else { | |
console.log('x') | |
response.status(404).end() | |
} | |
}) | |
app.delete('/api/notes/:id', (request, response) => { // 在 /api/notes/:id 路径下对 delete 请求进行响应 | |
const id = Number(request.params.id) | |
notes = notes.filter(note => note.id !== id) | |
response.status(204).end() | |
}) | |
const PORT = 3001 | |
app.listen(PORT, () => { | |
console.log(`Server running on port ${PORT}`) | |
}) |
# 跨域资源共享
当我们直接用前端访问后端时,出现了以下问题
Access to XMLHttpRequest at 'http://localhost:3001/api/notes' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
这是存在一个 CORS
的问题
在我们的环境中,问题在于,默认情况下,在浏览器中运行的应用的 JavaScript 代码只能与同一来源的服务器通信。
因为服务器在 localhost 3001端口
,而前端在 localhost 5173端口
没有共同的起源
可以使用 CORS
中间件解决这个问题
在后端中安装 cors
包
npm install cors |
并在 index.js
中取中间件得以使用
const cors = require('cors'); | |
app.use(cors()); |
# 引入 mongoDB 数据库
在 MongoDB Atlas
中点击 connect
获得到 MongoDB URI
看起来是类似这种
mongodb+srv://fullstack:thepasswordishere@cluster0.o1opl.mongodb.net/?retryWrites=true&w=majority
安装 Mongoose
npm install mongoose |
在根目录中创建一个 models
文件夹,假设在 models
文件夹中创建一个 person.js
文件,内容如下:
const mongoose = require('mongoose'); // 导入 mongoose | |
mongoose.set('strictQuery', false); // 避免 warning | |
const url = process.env.MONGODB_URI; // 从环境变量中获取 url | |
console.log('connecting to '); | |
mongoose.connect(url) // 连接数据库 | |
.then(result => { | |
console.log('connected to MongoDB'); | |
}) | |
.catch((error) => { | |
console.log('error connecting to MongoDB: ', error.message); | |
}) | |
const personSchema = new mongoose.Schema({ // 定义 schema | |
name: String, | |
number: String, | |
}) | |
personSchema.set('toJSON', { // 设置 toJSON 方法 | |
transform: (document, returnedObject) => { | |
returnedObject.id = returnedObject._id.toString(); | |
delete returnedObject._id; | |
delete returnedObject.__v; | |
} | |
}) | |
module.exports = mongoose.model('Person', noteSchema); // 导出 model |
这里需要定义环境变量的值
使用 dotenv
库,安装
npm install dotenv |
在根目录下创建一个 .env
文件,里面定义环境变量的值,比如:
MONGODB_URI=mongodb+srv://fullstack:thepasswordishere@cluster0.o1opl.mongodb.net/?retryWrites=true&w=majority
PORT=3001
这里的 fullstack
是数据库的用户名, thepasswordishere
是数据库的密码, cluster0
是数据库的名称, o1opl
是数据库的 ID, 3001
是端口号,可以根据自己的需求修改。
当然!这些里面有一些机密信息,我们需要将 .env
文件添加到 gitignore
文件中,以避免将这些信息泄漏给其他人。
在 .env
文件中定义的环境变量可以通过表达式 require('dotenv').config()
来引入。就可以像引用普通环境变量一样在代码中引用它们,使用 process.env.VARIABLE_NAME
来获取值。
后端仓库代码如下:
require('dotenv').config() // 引用 .env 文件 | |
const express = require('express') // 这是 express 的引用 | |
const app = express() // 创建一个 express 实例 | |
const Person = require('./models/person') // 引入 Person 模型 | |
const cors = require('cors') // 引入 cors | |
app.use(express.json()) // 添加 json 解析中间件 | |
app.use(cors()); // 添加 cors 中间件 | |
const requestLogger = (request, response, next) => { // 请求日志中间件 | |
console.log('Method:', request.method); | |
console.log('Path:', request.path); | |
console.log('Body:', request.body); | |
console.log('---'); | |
next(); | |
} | |
app.use(requestLogger); | |
app.get('/api/persons', (request, response) => { | |
Person.find({}).then(persons => { | |
response.json(persons); | |
}) | |
}) | |
app.get('/api/persons/:id', (request, response, next) => { | |
Person.findById(request.params.id) | |
.then(person => { | |
if(person) { // 如果有数据 | |
response.json(person); | |
} else { | |
response.status(404).end(); | |
} | |
}) | |
.catch(error => next(error)) | |
}) | |
app.delete('/api/persons/:id', (request, response) => { // 响应删除 | |
// const id = Number(request.params.id); | |
// persons = persons.filter(person => person.id !== id); | |
// response.status(204).end(); | |
Person.findByIdAndDelete(request.params.id) | |
.then(result => { | |
response.status(204).end(); | |
}) | |
.catch(error => next(error)); | |
}) | |
app.post('/api/persons', (request, response) => { | |
const body = request.body; | |
console.log(body); | |
if(!body.name || !body.phone) { // 验证请求体 | |
return response.status(400).json({ | |
error: 'content missing' | |
}) | |
} | |
const person = new Person({ | |
name: body.name, | |
phone: body.phone, | |
}) | |
person.save().then(savedPerson => { | |
response.json(savedPerson); | |
}) | |
}) | |
app.put('/api/persons/:id', (request, response, next) => { | |
const body = request.body; | |
console.log() | |
const person = { | |
name: body.name, | |
phone: body.phone, | |
} | |
Person.findByIdAndUpdate(request.params.id, person, {new: true}) | |
.then(updatePerson => { | |
response.json(updatePerson); | |
}) | |
.catch(error => next(error)) | |
}) | |
const unknownEndpoint = (request, response) => { | |
console.log('Unknown endpoint'); | |
response.status(404).send({error: 'unknown endpoint'}) | |
} | |
app.use(unknownEndpoint); | |
const errorHandler = (error, request, response, next) => { // 错误处理中间件 | |
console.log(error.message); | |
if(error.name === 'CastError') { // 判断 CastError | |
return response.status(400).send({error: 'malformatted id'}); | |
} | |
next(error); // 继续执行下一个中间件 | |
} | |
app.use(errorHandler); // 当任何以前的中间件或路由处理器抛出错误或调用 next (error) 时,Express 会跳过其他中间件并直接进入第一个错误处理中间件 | |
const PORT = process.env.PORT || 3001; | |
app.listen(PORT, () => { | |
console.log(`Server running on port ${PORT}`) | |
}) |
相对应的前端代码就是使用 axios
对后端进行请求,后端就是处理这些请求
注意!!!前后端的路径要一致
要注意中间件的使用顺序
不要把 express.json()
中间件放在 requestLogger
中间件之后,因为 express.json()
中间件会解析请求体,而 requestLogger
中间件会打印请求体,如果 express.json()
中间件在 requestLogger
中间件之后,会导致请求体被解析成空对象,从而导致 requestLogger
中间件打印空对象,而不是请求体。
错误判断的中间件使用要在路径匹配的中间件之后,否则可能会提前捕获到错误
像这里的 errorHandler
中间件,它应该在路径匹配的中间件之后,否则可能会提前捕获到错误。
# 为模式中的每个字段定义特定的验证规则
如下,将 phone
字段设置为至少 8 个字符的 String
类型,并且第一部分 2-3 个数字,第二部分 7-8 个数字,两个部分通过 - 连接
const personSchema = new mongoose.Schema({ // 定义 schema | |
name: { // 设置验证规则 | |
type: String, // 字符串 | |
minLength: 3, // 至少三个字符 | |
required: true // 设置为必填 | |
}, | |
phone: { | |
type: String, // 字符串 | |
minLength: 8, // 至少 8 个字符 | |
required: true, // 设置为必填 | |
validate: { // 验证规则 | |
validator: function(v) { | |
return /^\d{2,3}-\d{7,8}$/.test(v); // 正则表达式验证 | |
}, | |
message: props => `${props.value} is not a valid phone` // 错误提示 | |
} | |
}, | |
}) |
对应的 post
请求也要修改
app.post('/api/persons', (request, response, next) => { | |
const body = request.body; | |
console.log(body); | |
if(!body.name || !body.phone) { // 验证请求体 | |
return response.status(400).json({ | |
error: 'content missing' | |
}) | |
} | |
const person = new Person({ | |
name: body.name, | |
phone: body.phone, | |
}) | |
person.save().then(savedPerson => { | |
response.json(savedPerson); | |
}).catch(error => next(error)) // 添加错误处理 | |
}) |
对于 put
请求的修改比较特殊,这里需要验证 name
和 phone
字段,所以需要使用 runValidators
选项,并且 context
选项的值为 query
,表示验证规则在查询中执行。
app.put('/api/persons/:id', (request, response, next) => { | |
const { name, phone } = request.body; | |
Person.findByIdAndUpdate( | |
request.params.id, | |
{name, phone}, | |
{new: true, runValidators: true, context: 'query'}) | |
.then(updatePerson => { | |
response.json(updatePerson); | |
}) | |
.catch(error => next(error)) | |
}) |
并在错误处理中间件中添加错误判断
const errorHandler = (error, request, response, next) => { // 错误处理中间件 | |
console.log(error.message); | |
if(error.name === 'CastError') { // 判断 CastError | |
return response.status(400).send({error: 'malformatted id'}); | |
} else if(error.name === 'ValidationError') { // 判断 ValidationError | |
return response.status(400).json({error: error.message}); | |
} | |
next(error); // 继续执行下一个中间件 | |
} |
# 为什么 POST 不需要额外设置而 PUT 需要
对于 POST 请求通常是在创建一个新的文档,Mongoose 会自动应用在 Schema 定义的所有验证器来确保数据的有效性。
对于 PUT 请求通常修改数据库中的现有记录,而不是先加载文档到内存再保存。默认情况下 findByIdAndUpdate
不会运行模式验证器,所以需要使用 runValidators
选项来启用验证。设置 {new: true}
选项,表示返回更新后的文档,而不是更新前的文档。选择 context: 'query'
选项,表示验证规则在查询中执行,而不是在文档中执行。