Debugging Node applications

调试Node应用
将数据打印到控制台是一种可靠的方法,而且总是值得一试。
使用如下命令启动应用

  1. node --inspect index.js

在Chrome浏览器点击绿色按钮
image.png
当有问题发生时,质疑一切,stop and fix

MongoDB

MongoDB是文档数据库
阅读材料:集合文档
互联网上充满了Mongo数据库服务,推荐使用MongoDB Atlas
image.png
登录之后,要求创建一个集群, 选择AWS, Frankfurt, 耐心等待集群创建完成
image.png
点击Database Access, 创建一个用户,允许读写数据库
image.png
点击Network Access, 允许所有IP访问
image.png
回到Databases, 点击connect
image.png
image.png
该视图显示MongoDB URI,这是我们将添加到应用的 MongoDB 客户端库的数据库地址。

官方的MongoDB NodeJs驱动用起来很麻烦,我们使用更高级的Mongoose库

安装Mongoose

  1. npm install mongoose

让我们创建mongo.js 文件,来创建一个实践应用:

  1. const mongoose = require('mongoose')
  2. if (process.argv.length < 3) {
  3. console.log('Please provide the password as an argument: node mongo.js <password>');
  4. process.exit(1)
  5. }
  6. const password = process.argv[2]
  7. // 注意,这里的fullstack是上文中创建的用户名
  8. const url =
  9. `mongodb+srv://fullstack:${password}@cluster0.5r7sa.mongodb.net/myFirstDatabase?retryWrites=true&w=majority`
  10. mongoose.connect(url, { useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false, useCreateIndex: true })
  11. const noteSchema = new mongoose.Schema({
  12. content: String,
  13. date: Date,
  14. important: Boolean
  15. })
  16. const Note = mongoose.model('Note', noteSchema)
  17. const note = new Note({
  18. content: 'HTML is Easy',
  19. date: new Date(),
  20. important: true,
  21. })
  22. note.save().then(result => {
  23. console.log('note saved!')
  24. mongoose.connection.close()
  25. })

当使用命令_**node mongo.js password**_运行代码时,Mongo 将向数据库添加一个新文档。
注意,这里的password是前文中为数据库用户(fullstack)创建的密码,不是MongoDB Atlas的密码。如果密码里有特殊字符,需要URL encode that password, 编码工具:https://www.urlencoder.org/
运行时报错: MongooseError: Operation notes.insertOne() buffering timed out after 10000ms
image.png
解决办法:把DNS改成 8.8.8.8
image.png
执行成功后,可以看到数据写入了myFirstDatabase数据库
image.png

我们可以更改URI中的数据库名,比如改成note-app, 重新执行代码,则会生成一个新库,myFirstDatabase库可以手动删掉
image.png

Schema

  1. const noteSchema = new mongoose.Schema({
  2. content: String,
  3. date: Date,
  4. important: Boolean,
  5. })
  6. const Note = mongoose.model('Note', noteSchema)

上述代码定义了在mongoDB中存储数据的模式(schema)
在 Note 模型定义中,第一个 “Note”参数是模型的单数名。 集合的名称将是小写的复数 notes,因为Mongoose 约定是当模式以单数(例如Note)引用集合时自动将其命名为复数(例如notes)。

creating and saving objects

  1. const note = new Note({
  2. content: 'HTML is Easy',
  3. date: new Date(),
  4. important: true,
  5. })
  6. note.save().then(result => {
  7. console.log('note saved!')
  8. mongoose.connection.close()
  9. })

上述代码创建并保存了一条数据
必须要有mongoose.connection.close(),否则操作不会完成

Fetching objects from the database

  1. Note.find({}).then(result => {
  2. result.forEach(note => {
  3. console.log(note);
  4. })
  5. mongoose.connection.close()
  6. })

使用**find**方法搜索数据,条件为{},搜索出所有数据
指定条件搜索:

  1. Note.find({important: true}).then(result => {...})

Exercise 3.12

当传递3个参数时插入,传递一个参数时查询

  1. const mongoose = require('mongoose')
  2. if (process.argv.length < 3) {
  3. console.log('Please provide the password as an argument: node mongo.js <password>')
  4. process.exit(1)
  5. }
  6. const password = process.argv[2]
  7. const url =
  8. `mongodb+srv://fullstack:${password}@cluster0.5r7sa.mongodb.net/phonebook-app?retryWrites=true&w=majority`
  9. mongoose.connect(url, { useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false })
  10. const personSchema = new mongoose.Schema({
  11. name: String,
  12. number: String,
  13. })
  14. const Person = mongoose.model('Person', personSchema)
  15. if (process.argv.length === 5) {
  16. const name = process.argv[3]
  17. const number = process.argv[4]
  18. const person = new Person({
  19. name,
  20. number,
  21. })
  22. person.save().then(result => {
  23. console.log(`add ${name} number ${number} to phonebook`)
  24. mongoose.connection.close()
  25. })
  26. } else if (process.argv.length === 3) {
  27. Person.find({}).then(result => {
  28. console.log("phonebook:")
  29. result.forEach(person => {
  30. console.log(`${person.name} ${person.number}`)
  31. })
  32. mongoose.connection.close()
  33. })
  34. }

Backend connected to a database

后端连接到数据库
将mongoose的定义放到index.js
注意:这里的密码是明文写的,不要把有密码的文件上传到github

  1. const mongoose = require('mongoose')
  2. const url =
  3. `mongodb+srv://fullstack:你的密码@cluster0.5r7sa.mongodb.net/note-app?retryWrites=true&w=majority`
  4. mongoose.connect(url, { useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false, useCreateIndex: true })
  5. const noteSchema = new mongoose.Schema({
  6. content: String,
  7. date: Date,
  8. important: Boolean,
  9. })
  10. const Note = mongoose.model('Note', noteSchema)

修改get方法

  1. app.get('/api/notes', (request, response) => {
  2. Note.find({}).then(notes => {
  3. response.json(notes)
  4. })
  5. })

添加代码,修改schema的toJSON方法,使返回结果有id,不要_id和__v
image.png

  1. noteSchema.set('toJSON', {
  2. transform: (document, returnedObject) => {
  3. returnedObject.id = returnedObject._id.toString()
  4. delete returnedObject._id
  5. delete returnedObject.__v
  6. }
  7. })

尽管 Mongoose 对象的 id 属性看起来像一个字符串,但实际上它是一个对象。 为了安全起见,我们定义的 toJSON 方法将其转换为字符串。 以免将来造成不必要的麻烦。

Database configuration into its own module

数据库配置分离到单独的模块
创建models目录, 新建note.js

  1. const mongoose = require('mongoose')
  2. const url = process.env.MONGODB_URI
  3. // `mongodb+srv://fullstack:密码@cluster0.5r7sa.mongodb.net/note-app?retryWrites=true&w=majority`
  4. console.log('connecting to', url)
  5. mongoose.connect(url, { useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false, useCreateIndex: true })
  6. .then(result => {
  7. console.log('connected to MongoDB');
  8. })
  9. .catch((error) => {
  10. console.log('error connecting to MongoDB: ', error.message);
  11. })
  12. const noteSchema = new mongoose.Schema({
  13. content: String,
  14. date: Date,
  15. important: Boolean,
  16. })
  17. noteSchema.set('toJSON', {
  18. transform: (document, returnedObject) => {
  19. returnedObject.id = returnedObject._id.toString()
  20. delete returnedObject._id
  21. delete returnedObject.__v
  22. }
  23. })
  24. // const Note = mongoose.model('Note', noteSchema)
  25. module.exports = mongoose.model('Note', noteSchema)

注意看,nodeJS的module和ES6不同,使用module.exports变量,将Note赋值给它
导入模块的方法:

  1. const Note = require('./models/note')

将数据库的地址硬编码到代码中并不是一个好主意,因此数据库的地址通过MONGODB_URI 环境变量传递给应用。

  1. const url = process.env.MONGODB_URI

有很多方法可以定义环境变量的值。 一种方法是在应用启动时定义它:

  1. MONGODB_URI=address_here npm run dev

一个更复杂的方法是使用dotenv ,使用如下命令安装库:

  1. npm install dotenv

在项目根目录新建.env文件,写入下列信息
注意不可以使用反引号```, 只能用单引号

  1. MONGODB_URI='mongodb+srv://fullstack:密码@cluster0.5r7sa.mongodb.net/note-app?retryWrites=true&w=majority'
  2. PORT=3001

将.env添加到.gitignore文件,以免私密信息上传到github
使用require('dotenv').config()命令来使用 .env 文件中定义的环境变量

  1. require('dotenv').config()
  2. const express = require('express')
  3. const app = express()
  4. const Note = require('./models/note')
  5. // ..
  6. const PORT = process.env.PORT
  7. app.listen(PORT, () => {
  8. console.log(`Server running on port ${PORT}`)
  9. })

Using database in route handlers

更改后端的其余部分,使其使用数据库
post创建新的便签:

  1. app.post('/api/notes', (request, response) => {
  2. const body = request.body
  3. if (body.content === undefined) {
  4. return response.status(400).json({ error: 'content missing' })
  5. }
  6. const note = new Note({
  7. content: body.content,
  8. important: body.important || false,
  9. date: new Date(),
  10. })
  11. note.save().then(savedNote => {
  12. response.json(savedNote)
  13. })
  14. })

使用mongoose的**findById**方法查找指定id

  1. app.get('/api/notes/:id', (request, response) => {
  2. Note.findById(request.params.id).then(note => {
  3. response.json(note)
  4. })
  5. })

Verifying frontend and backend integration

当后端扩展时,最好先用 浏览器,Postman 或者 VS Code REST 客户端 来测试后端。
当后端测试没问题时,再联合前端一起测试。

Error handling

当请求一个不存在的ID时,服务器响应404。
如果id的格式与mongoDB要求的格式不匹配的话,promise会rejected, 所以添加catch方法,在控制台log出error, 如果 id 的格式不正确,那么我们将在 catch 块中定义的错误处理程序中结束。 适合这种情况的状态代码是 400 Bad Request,表明服务器无法理解请求。 客户端不应该在没有修改的情况下重复请求

  1. app.get('/api/notes/:id', (request, response) => {
  2. Note.findById(request.params.id)
  3. .then(note => {
  4. if (note) {
  5. response.json(note)
  6. } else {
  7. response.status(404).end()
  8. }
  9. })
  10. .catch(error => {
  11. console.log(error)
  12. response.status(400).send({error: 'malformatted id'})
  13. })
  14. })

Moving error handling into middleware

将错误处理移入中间件
在某些情况下,最好在单个位置实现所有错误处理,可用使用next函数向下传递error
next函数必须以参数形式传入,这样才能将错误传给中间件

  1. app.get('/api/notes/:id', (request, response, next) => {
  2. Note.findById(request.params.id)
  3. .then(note => {
  4. if (note) {
  5. response.json(note)
  6. } else {
  7. response.status(404).end()
  8. }
  9. })
  10. .catch(error => next(error))
  11. })

error handler是一种错误处理中间件,将下列错误处理程序放在最后

  1. const errorHandler = (error, request, response, next) => {
  2. console.log(error.message)
  3. if (error.name === 'CastError' && error.kind === 'ObjectId') {
  4. return response.status(400).send({ error: 'malformatted id' })
  5. }
  6. next(error)
  7. }
  8. app.use(errorHandler)

The order of middleware loading

中间件加载顺序
中间件的执行顺序与通过 app.use 函数加载到 express 中的顺序相同

  1. app.use(express.static('build'))
  2. app.use(express.json())
  3. app.use(requestLogger)
  4. app.post('/api/notes', (request, response) => {
  5. const body = request.body
  6. // ...
  7. })
  8. const unknownEndpoint = (request, response) => {
  9. response.status(404).send({ error: 'unknown endpoint' })
  10. }
  11. // handler of requests with unknown endpoint
  12. app.use(unknownEndpoint)
  13. const errorHandler = (error, request, response, next) => {
  14. // ...
  15. }
  16. // handler of requests with result to errors
  17. app.use(errorHandler)

Other operations

删除便签

使用**findByIdAndRemove**删除便签

  1. app.delete('/api/notes/:id', (request, response, next) => {
  2. Note.findByIdAndRemove(request.params.id)
  3. .then(result => {
  4. response.status(204).end()
  5. })
  6. .catch(error => next(error))
  7. })

更新便签

使用**findByIdAndUpdate**更新便签

  1. app.put('/api/notes/:id', (request, response, next) => {
  2. const body = request.body
  3. const note = {
  4. content: body.content,
  5. important: body.important,
  6. }
  7. Note.findByIdAndUpdate(request.params.id, note, { new: true })
  8. .then(updatedNote => {
  9. response.json(updatedNote)
  10. })
  11. .catch(error => next(error))
  12. })

注意,findByIdAndUpdate 方法接收一个常规的 JavaScript 对象作为参数,而不是用 Note 构造函数创建的新便笺对象。
{new: true}参数是可选的, 表示返回的updatedNote是更新后的数据,如果没有这个参数,返回的是更新前的旧数据