教程:FullStackOpen2022/Part 4

优化应用结构

对于较小的应用,结构并不重要。可一旦应用开始增大,就必须建立某种结构,并将应用的不同职责分离到单独的模块中,使开发应用更加容易。

优化后的目录结构:

├── index.js
├── app.js
├── build
│   └── ...
├── controllers
│   └── blogs.js
├── models
│   └── blog.js
├── package-lock.json
├── package.json
├── utils
│   ├── config.js
│   ├── logger.js
│   └── middleware.js
  • index.js:用于启动应用。
  • app.js:实际的应用。
  • controllers/blogs.js:路由处理程序。
  • models/blog.js:为 blogs 定义 Mongoose schema。
  • utils/logger.js:用于控制台的打印输出,告诉应用的运行状态。
  • utils/config.js:环境变量的处理,应用的其他部分可以通过导入配置模块来访问环境变量。
  • utils/middleware.js:自定义中间件模块。

以 bloglist 应用为例,将该应用重构。

初始 index.js:

const http = require('http')
const express = require('express')
const app = express()
const cors = require('cors')
const mongoose = require('mongoose')

const blogSchema = new mongoose.Schema({
  title: String,
  author: String,
  url: String,
  likes: Number
})

const Blog = mongoose.model('Blog', blogSchema)

const mongoUrl = 'mongodb://localhost/bloglist'
mongoose.connect(mongoUrl)

app.use(cors())
app.use(express.json())

app.get('/api/blogs', (request, response) => {
  Blog
    .find({})
    .then(blogs => {
      response.json(blogs)
    })
})

app.post('/api/blogs', (request, response) => {
  const blog = new Blog(request.body)

  blog
    .save()
    .then(result => {
      response.status(201).json(result)
    })
})

const PORT = 3003
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`)
})

models/blog.js

将建立到数据库连接的责任交给 app.js 模块,models 目录下的 blog.js 只为 blogs 定义 Mongoose schema。

const mongoose = require("mongoose");

const blogSchema = new mongoose.Schema({
  title: String,
  author: String,
  url: String,
  likes: Number,
});

blogSchema.set("toJSON", {
  transform: (document, returnedObject) => {
    returnedObject.id = returnedObject._id.toString();
    delete returnedObject._id;
    delete returnedObject.__v;
  },
});

module.exports = mongoose.model("Blog", blogSchema);

controller/blogs.js

路由实际上是一个中间件,可用于在某个位置定义“相关路由” ,通常放置在单独的模块中。

路由的事件处理程序通常称为controllers。所有与 blog 相关的路由现在都在controllers 目录下的blogs.js 模块中定义。

controllers/blogs.js:

const blogsRouter = require("express").Router();
const Blog = require("../models/blog");

blogsRouter.get("/", (request, response) => {
  Blog.find({})
    .then((blogs) => {
      response.json(blogs);
    })
    .catch((error) => next(error));
});

blogsRouter.post("/", (request, response) => {
  const blog = new Blog(request.body);

  blog
    .save()
    .then((result) => {
      response.status(201).json(result);
    })
    .catch((error) => next(error));
});

module.exports = blogsRouter;

在文件的开始创建了一个新的 Router 对象。该模块将路由导出,所有消费者可用。

blogsRouter 对象必须只定义路由的相对部分,即空路径 / 或仅仅定义参数 /:id

app.js 对路由对象使用use方法,按如下方式使用:

const blogsRouter = require('./controllers/blogs')
app.use('/api/blogs', blogsRouter)

utils

config.js

专门环境变量的处理,应用的其他部分可以通过导入配置模块来访问环境变量。

require('dotenv').config()

const PORT = process.env.PORT
const MONGODB_URI = process.env.MONGODB_URI

module.exports = {
  MONGODB_URI,
  PORT
}

logger.js

info 用于打印正常的日志消息,error 用于所有错误消息。

const info = (...params) => {
  console.log(...params)
}

const error = (...params) => {
  console.error(...params)
}

module.exports = {
  info, error
}

middleware.js

自定义中间件。

const logger = require('./logger')

const requestLogger = (request, response, next) => {
  logger.info('Method:', request.method)
  logger.info('Path:  ', request.path)
  logger.info('Body:  ', request.body)
  logger.info('---')
  next()
}

const unknownEndpoint = (request, response) => {
  response.status(404).send({ error: 'unknown endpoint' })
}

const errorHandler = (error, request, response, next) => {
  logger.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)
}

module.exports = {
  requestLogger,
  unknownEndpoint,
  errorHandler
}

app.js

进行重构后的 app.js:

const config = require("./utils/config");
const express = require("express");
const app = express();
const cors = require("cors");
const blogsRouter = require("./controllers/blogs");
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)
  .then(() => {
    logger.info("connected to MongoDB");
  })
  .catch((error) => {
    logger.error("error connecting to MongoDB:", error.message);
  });

app.use(cors());
app.use(express.static("build"));
app.use(express.json());
app.use(middleware.requestLogger);

app.use("/api/blogs", blogsRouter);

app.use(middleware.unknownEndpoint);
app.use(middleware.errorHandler);

module.exports = app;

index.js

index.js 文件只从 app.js 文件导入实际的应用,然后启动应用。

index.js 简化后如下:

const app = require('./app')
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}`)
})