# 准备

# 安装 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' 选项,表示验证规则在查询中执行,而不是在文档中执行。

更新于 阅读次数 次