# 准备

# 安装 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 是一个 Nodedev 工具,当 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"
  },
  // ..
}

在脚本中不需要指定 nodemonnode/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 请求的修改比较特殊,这里需要验证 namephone 字段,所以需要使用 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' 选项,表示验证规则在查询中执行,而不是在文档中执行。

更新于 阅读次数