我如何构建Express应用程序

212 阅读5分钟

TL;DR-探索示例库

这是我用于中型Node后端的典型结构。对于小规模的后端,我可能只是把所有东西都放在一个文件里,而且我可能不会去理会构建工具。

package.json

让我们从package.json 开始。 这里是相关的部分。

{
  "main": "index.js",
  "engines": {
    "node": "12.18.2"
  },
  "dependencies": {
    "express": "^4.17.1",
    "express-async-errors": "^3.1.1",
    "loglevel": "^1.6.8"
  },
  "devDependencies": {
    "@babel/cli": "^7.10.4",
    "@babel/core": "^7.10.4",
    "@babel/preset-env": "^7.10.4",
    "@babel/register": "^7.10.4",
    "nodemon": "^2.0.4"
  },
  "scripts": {
    "start": "node .",
    "build": "babel --delete-dir-on-start --out-dir dist --copy-files --ignore \"**/__tests__/**,**/__mocks__/**\" --no-copy-ignored src"
  }
}

这是我们的服务器的条目。所以当我们在这个目录下运行node . ,这就是将被运行的文件。

引擎

这向我们使用的工具表明我们打算让项目在哪个版本的node上运行。

依赖性

express 是给定的(有很多替代品,如果你使用其中之一,那很好,无论如何,你可能仍然能够从这篇博文中得到一些东西)。对于我的每一个Express.js应用,我也使用 ,因为它允许我使用 来编写我的中间件,这对我来说基本上是必需的。更不容易出错,因为它确保任何异步错误都会被传播到你的错误处理中间件。express-async-errors async/await

我个人喜欢loglevel ,还有很多其他的日志工具,但loglevel 是一个好的开始。

devDependencies

我用Babel编译我所有的东西。这使得我们可以使用在我们的环境中还不太支持的语法(主要是ESModules),以及方便的插件,如babel-plugin-macros.因此所有的@babel 包。

  • @babel/core 是babel的核心依赖。其他东西都需要它。
  • @babel/cli 是用于 脚本,将我们的源代码编译成Node可以运行的输出代码。build
  • @babel/preset-env 使得包含所有典型的语言插件和转换变得非常容易,我们将需要为我们正在构建的环境。
  • @babel/register 是在开发过程中使用的。

如果你使用TypeScript,那么你可能还想添加@babel/preset-typescript

我还使用nodemon ,用于观察模式(当文件被改变时重新启动服务器)。

脚本

start 脚本简单地运行node . ,它将运行main 文件(我们已将其设置为index.js )。

build 脚本把src 目录下的所有文件(简称 "源文件")用babel编译到dist 目录下(简称分发文件)。下面是对所有选项的解释。

  • --delete-dir-on-start 确保我们不会在两次编译之间有旧的文件挂在那里。
  • --out-dir dist 表示我们希望将编译后的文件保存在哪里
  • --copy-files 表示未编译的文件应该被复制(例如对 文件很有用)。.json
  • --ignore \"**/__tests__/**,**/__mocks__/**\" 是必要的,这样我们就不必费力地编译任何与测试有关的文件,因为我们在生产中也不需要这些文件。
  • --no-copy-ignored 因为我们不编译被忽略的文件,我们想表明我们也不想复制它们(所以这将禁用被忽略的文件的 )。--copy-files

如果你使用TypeScript,确保在build 脚本中加入--extensions ".ts,.tsx,.js"

.babelrc.js

下面是.babelrc.js 的样子。

const pkg = require('./package.json')

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          node: pkg.engines.node,
        },
      },
    ],
  ],
}

这很简单。我们把所有的代码编译成我们的package.json 中指定的engines.node 值所支持的JavaScript语法版本。

如果我们使用TypeScript(推荐给团队使用),那么我们也会包括@babel/preset-typescript

index.js

这是我们的模块的入口文件(这是来自package.jsonmain )。

if (process.env.NODE_ENV === 'production') {
  require('./dist')
} else {
  require('nodemon')({script: 'dev.js'})
}

当我们在生产中运行我们的应用程序时,它是在一个服务器上运行的,该服务器已被配置为将NODE_ENV 环境变量设置为'production' 。因此,在生产中,我们的index.js 的设置方式,它将以我们代码的编译版本启动服务器。

然而,在本地运行该项目时,我们将需要nodemon ,并将选项{script: 'dev.js'} ,这将告诉nodemon运行dev.js 脚本,并在我们做出改变时重新运行它。这将改善我们在对服务器进行修改时的反馈循环。关于nodemon ,还有很多选项,有人向我提到node-dev是另一个值得研究的好项目,所以你也可以看看。

dev.js

这个很简单。

require('@babel/register')
require('./src')

@babel/register 设置了babel来 "即时 "编译我们的文件,也就是说,当需要这些文件时,Babel会在Node有机会运行之前首先编译该文件。然后,require('./src') 会要求我们的src/index.js 文件,这才是事情真正开始发生的地方。

src/index.js

这个文件非常简单。

import logger from 'loglevel'
import {startServer} from './start'

logger.setLevel('info')

startServer()

它所做的就是配置记录器和启动服务器。我见过的大多数项目实际上都是在src/index.js 文件中启动服务器的,但我更喜欢把启动服务器的逻辑放在一个函数中,因为这样更容易进行测试。

src/start.js

好了,这里是事情真正开始 "表达 "的地方了。对于这个问题,我将在代码注释中解释。

import express from 'express'

// this is all it takes to enable async/await for express middleware
import 'express-async-errors'

import logger from 'loglevel'

// all the routes for my app are retrieved from the src/routes/index.js module
import {getRoutes} from './routes'

function startServer({port = process.env.PORT} = {}) {
  const app = express()

  // I mount my entire app to the /api route (or you could just do "/" if you want)
  app.use('/api', getRoutes())

  // add the generic error handler just in case errors are missed by middleware
  app.use(errorMiddleware)

  // I prefer dealing with promises. It makes testing easier, among other things.
  // So this block of code allows me to start the express app and resolve the
  // promise with the express server
  return new Promise(resolve => {
    const server = app.listen(port, () => {
      logger.info(`Listening on port ${server.address().port}`)

      // this block of code turns `server.close` into a promise API
      const originalClose = server.close.bind(server)
      server.close = () => {
        return new Promise(resolveClose => {
          originalClose(resolveClose)
        })
      }

      // this ensures that we properly close the server when the program exists
      setupCloseOnExit(server)

      // resolve the whole promise with the express server
      resolve(server)
    })
  })
}

// here's our generic error handler for situations where we didn't handle
// errors properly
function errorMiddleware(error, req, res, next) {
  if (res.headersSent) {
    next(error)
  } else {
    logger.error(error)
    res.status(500)
    res.json({
      message: error.message,
      // we only add a `stack` property in non-production environments
      ...(process.env.NODE_ENV === 'production' ? null : {stack: error.stack}),
    })
  }
}

// ensures we close the server in the event of an error.
function setupCloseOnExit(server) {
  // thank you stack overflow
  // https://stackoverflow.com/a/14032965/971592
  async function exitHandler(options = {}) {
    await server
      .close()
      .then(() => {
        logger.info('Server successfully closed')
      })
      .catch(e => {
        logger.warn('Something went wrong closing the server', e.stack)
      })

    if (options.exit) process.exit()
  }

  // do something when app is closing
  process.on('exit', exitHandler)

  // catches ctrl+c event
  process.on('SIGINT', exitHandler.bind(null, {exit: true}))

  // catches "kill pid" (for example: nodemon restart)
  process.on('SIGUSR1', exitHandler.bind(null, {exit: true}))
  process.on('SIGUSR2', exitHandler.bind(null, {exit: true}))

  // catches uncaught exceptions
  process.on('uncaughtException', exitHandler.bind(null, {exit: true}))
}

export {startServer}

这样做可以使测试更容易。例如,一个集成测试可以简单地这样做。

import {startServer} from '../start'

let server, baseURL
beforeAll(async () => {
  server = await startServer()
  baseURL = `http://localhost:${server.address().port}/api`
})

afterAll(() => server.close())

// make requests to the baseURL

如果这听起来很有趣,那么让我在TestingJavaScript.com上教你🏆

src/routes/index.js

这里是我的应用程序的所有路由的集合地。

import express from 'express'
// any other routes imports would go here
import {getMathRoutes} from './math'

function getRoutes() {
  // create a router for all the routes of our app
  const router = express.Router()

  router.use('/math', getMathRoutes())
  // any additional routes would go here

  return router
}

export {getRoutes}

src/routes/math.js

这只是一些路由/中间件/表达式控制器的一个臆造的例子。

import express from 'express'

// A function to get the routes.
// That way all the route definitions are in one place which I like.
// This is the only thing that's exported
function getMathRoutes() {
  const router = express.Router()
  router.get('/add', add)
  router.get('/subtract', subtract)
  return router
}

// all the controller and utility functions here:
async function add(req, res) {
  const sum = Number(req.query.a) + Number(req.query.c)
  res.send(sum.toString())
}

async function subtract(req, res) {
  const difference = Number(req.query.a) - Number(req.query.b)
  res.send(difference.toString())
}

export {getMathRoutes}

结论

就这样了。希望这很有趣,也很有用!如果你想了解这些东西的测试方面,不要错过TestingJavaScript.com上的Test Node.js Backends模块。