这是我用于中型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.json 的main )。
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模块。