跳到内容

d

验证与 ESLint

我们通常希望对存储在应用程序数据库中的数据应用一些约束。我们的应用程序不应接受 content 属性缺失或为空的笔记。现在笔记的有效性在路由处理函数中检查:

app.post('/api/notes', (request, response) => {
  const body = request.body
  if (!body.content) {    return response.status(400).json({ error: 'content missing' })  }
  // ...
})

如果笔记没有 content 属性,我们就以 400 bad request 的状态码响应请求。

在数据存储到数据库之前验证数据格式的一种更智能的方法是使用 Mongoose 提供的验证功能。

我们可以为模式中的每个字段定义特定的验证规则:

const noteSchema = new mongoose.Schema({
  content: {    type: String,    minLength: 5,    required: true  },  important: Boolean
})

现在 content 字段要求至少五个字符长,并且被设为 required,意味着字段不能缺失。我们没有对 important 字段添加任何约束,所以它在模式中的定义没有改变。

minLength 和 required 验证器是 Mongoose 提供的内置的验证器。如果内置的验证器全都无法满足我们的需求,我们还可以用 Mongoose 的自定义验证器功能创建新的验证器。

如果我们试图在数据库中存储一个违反了某些约束的对象,操作将会抛出异常。让我们更改创建新笔记的处理函数,来将任何可能的异常传递给错误处理中间件:

app.post('/api/notes', (request, response, next) => {  const body = request.body

  const note = new Note({
    content: body.content,
    important: body.important || false,
  })

  note.save()
    .then(savedNote => {
      response.json(savedNote)
    })
    .catch(error => next(error))})

让我们扩展错误处理函数来处理这些验证错误:

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

当对象验证失败时,我们返回以下 Mongoose 的默认错误消息:

postman显示错误消息

将数据库后端部署到生产环境

该应用程序应该能在 Fly.io/Render 上按原样工作。由于到目前为止我们只对后端进行了修改,所以我们不需要构建前端的新生产版本。

dotenv 中定义的环境变量只会在后端不处于生产模式,即不在 Fly.io 或 Render 中时使用。

对于生产环境,我们需要在托管我们应用的服务中设置数据库 URL。

在 Fly.io 中,可以通过 fly secrets set 命令来完成:

fly secrets set MONGODB_URI='mongodb+srv://fullstack:thepasswordishere@cluster0.a5qfl.mongodb.net/noteApp?retryWrites=true&w=majority'

在开发应用的过程中,很可能会出现一些失败的情况。例如,当我第一次部署带有数据库的应用时,一个笔记都没有看到:

浏览器显示没有出现任何笔记

浏览器控制台的网络标签页显示并未成功获取笔记,请求只是在 pending 状态下停留了很长时间,最后以 502 状态码失败。

必须始终打开浏览器控制台!

同时,持续关注服务端日志也非常重要。当我用 fly logs 打开日志时,问题就显而易见了:

fly.io服务器日志显示连接到未定义

数据库 URL 是 undefined ,所以是忘记执行 fly secrets set MONGODB_URI 命令了。

你还需要在 MongoDB Atlas 中将 fly.io 应用的 IP 地址添加到白名单中。否则 MongoDB 会拒绝连接。

遗憾的是,fly.io 不会给你的应用提供一个专门的 IPv4 地址,所以你需要在 MongoDB Atlas 中允许所有的 IP 地址。

在使用 Render 时,可以通过在仪表板中定义适当的环境变量来提供数据库 URL:

render仪表板显示MONGODB_URI环境变量

Render 仪表板显示服务端日志:

render仪表板上有箭头指向在端口10000上运行的服务器

你可以在这个 GitHub 仓库的 part3-6 分支中找到我们当前应用的完整代码。

lint

在我们进入下一章节之前,我们来介绍一个重要的工具,叫做 lint。维基百科对 lint 的描述如下:

一般来说,lint 或 linter 是任何检测并标记编程语言中的错误,包括样式错误的工具。术语“lint 类行为”有时用于标记语言的可疑用法的过程。lint 类工具通常对源代码进行静态分析。

对于编译型静态类型语言,比如 Java,NetBeans 等 IDE 可以指出代码中的错误,甚至不止是编译错误。像 checkstyle 这样用于执行静态分析的附加工具,可以用来扩展 IDE 的能力,来指出与样式相关的问题,如缩进。

在 JavaScript 领域,目前领头的静态分析工具(又称“linting”)是ESlint。

让我们将 ESLint 添加为后端的开发依赖项。开发依赖项是只在开发应用的过程中需要的工具。比如,和测试有关的工具就是开发依赖项。当应用以生产模式运行时,就不需要开发依赖项了。

使用以下命令将 ESlint 作为开发依赖项安装到后端项目中:

npm install eslint @eslint/js --save-dev

package.json 文件的内容会这么变化:

{
  //...
  "dependencies": {
    "dotenv": "^16.4.7",
    "express": "^5.1.0",
    "mongoose": "^8.11.0"
  },
  "devDependencies": {    "@eslint/js": "^9.22.0",    "eslint": "^9.22.0"  }
}

该命令在文件中添加了 devDependencies 一节,并在其中添加了 eslint 和 @eslint/js,同时在 node_modules 目录中安装了所需的库。

之后我们可以用以下命令初始化默认的 ESlint 配置:

npx eslint --init

我们要回答所有的问题:

ESlint初始化的终端输出

配置将会保存在 eslint.config.mjs 文件中。

格式化配置文件

让我们重设配置文件 eslint.config.mjs 的格式,将它从当前的格式改为:

import globals from 'globals'

export default [
  {
    files: ['**/*.js'],
    languageOptions: {
      sourceType: 'commonjs',
      globals: { ...globals.node },
      ecmaVersion: 'latest',
    },
  },
]

到目前为止,ESLint 配置文件在 files 选项中定义了 ["**/*.js"],告诉 ESLint 要检查项目目录中的所有 JavaScript 文件。languageOptions 属性指定 ESLint 应支持的语言特性相关的选项,其中我们将 sourceType 设为“commonjs”。这表示我们项目中的 JavaScript 代码使用 CommonJS 模块系统,从而使 ESLint 能以相应的方法分析代码。

globals 属性指定预定义的全局变量。这里使用的展开运算符告诉 ESLint 包含 globals.node 设定中定义的所有全局变量,比如 process。对于浏览器端代码,我们会在这里定义 globals.browser,来允许浏览器特有的全局变量,比如 window 和 document。

最后,ecmaVersion 属性设为“latest”。这将 ECMAScript 版本设为最新可用版本,意味着 ESLint 能理解并正确检查最新的 JavaScript 语法和特性。

我们希望同时使用 ESLint 推荐的和自定义的设置。之前安装的 @eslint/js 包提供了 ESLint 的预定义配置。我们可以在配置文件中导入并启用:

import globals from 'globals'
import js from '@eslint/js'// ...

export default [
  js.configs.recommended,  {
    // ...
  },
]

我们已将 js.configs.recommended 添加到配置数组的最上面,这确保 ESLint 的推荐设置会在我们自定义的选项之前先应用。

让我们继续构建配置文件。安装一个定义了一套与代码样式相关的规则的插件:

npm install --save-dev @stylistic/eslint-plugin-js

导入并启用插件,并添加这四条代码样式规则:

import globals from 'globals'
import js from '@eslint/js'
import stylisticJs from '@stylistic/eslint-plugin-js'
export default [
  {
    // ...
    plugins: {       '@stylistic/js': stylisticJs,    },    rules: {       '@stylistic/js/indent': ['error', 2],      '@stylistic/js/linebreak-style': ['error', 'unix'],      '@stylistic/js/quotes': ['error', 'single'],      '@stylistic/js/semi': ['error', 'never'],    },   },
]

plugins 属性提供了一种可以通过添加自定义规则、配置以及其他核心 ESLint 库中没有的功能来扩展 ESLint 功能的方法。我们已安装并启用了 @stylistic/eslint-plugin-js,它为 ESLint 添加了 JavaScript 样式的规则。此外,还添加了关于缩进、换行、引号和分号的规则,这四条规则都是在 Eslint styles plugin 中定义的。

Windows 用户注意事项:样式规则中将换行样式设为了 unix。建议无论使用什么操作系统都使用 Unix 样式的换行符(\n),这样文件可以兼容大多数现代操作系统,并且在多人处理同一文件时更方便协作。如果使用 Windows 样式的换行符,ESLint 会产生如下错误:Expected linebreaks to be 'LF' but found 'CRLF'。遇到这种情况时,按照这份指南将 Visual Studio Code 配置为使用 Unix 样式换行。

运行 linter

要检查和验证某个文件,比如 index.js,可以使用以下命令:

npx eslint index.js

建议为 linting 创建一个专门的 npm 脚本:

{
  // ...
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js",
    // ...
    "lint": "eslint ."  },
  // ...
}

现在,npm run lint 命令将检查项目中的每个文件。

当运行命令时,dist 目录中的文件也会被检查。我们不希望这种情况发生,我们可以通过添加一个对象,并在其 ignores 属性声明一个我们要忽略的目录和文件的数组来实现这一点。

// ...
export default [
  js.configs.recommended,
  {
    files: ['**/*.js'],
    // ...
  },
  {     ignores: ['dist/**'],   },]

这会使得整个 dist 目录都不被 ESlint 检查。

lint 对我们的代码有很多意见:

ESlint错误的终端输出

相比于从命令行执行 linter,更好的替代方案是给编辑器配置 eslint 插件,来持续不断地运行 linter。使用这个插件后,你将立即在代码中看到错误。你可以在这里找到更多关于 Visual Studio ESLint 插件的信息。

VS Code ESlint 插件会用红线划出违反样式的地方:

VScode ESLint插件显示错误的截图

这会让错误很容易发现,从而立即修复。

添加更多样式规则

ESlint 有大量易于使用的规则,只要编辑 eslint.config.mjs 文件即可使用。

让我们添加 eqeqeq 规则,如果相等不是用三等号===检查的,eqeqeq 规则就会警告我们。将该规则添加到配置文件的 rules 字段下。

export default [
  // ...
  rules: {
    // ...
   eqeqeq: 'error',  },
  // ...
]

让我们顺便对规则进行一些其他更改。

让我们阻止行尾的不必要的尾随空格,要求大括号前后始终有一个空格,并且也要求箭头函数的函数参数中一致使用空格。

export default [
  // ...
  rules: {
    // ...
    eqeqeq: 'error',
    'no-trailing-spaces': 'error',    'object-curly-spacing': ['error', 'always'],    'arrow-spacing': ['error', { before: true, after: true }],  },
]

我们的默认配置使用了这里预定义的一串规则:

// ...

export default [
  js.configs.recommended,
  // ...
]

这里包括一条警告 console.log 命令的规则,而我们不想使用这条规则。禁用一条规则可以通过在配置文件中定义其“value”为 0 或 off 来完成。让我们在此期间对 no-console 规则这样做。

[
  {
    // ...
    rules: {
      // ...
      eqeqeq: 'error',
      'no-trailing-spaces': 'error',
      'object-curly-spacing': ['error', 'always'],
      'arrow-spacing': ['error', { before: true, after: true }],
      'no-console': 'off',    },
  },
]

禁用 no-console 规则使我们能够使用 console.log 语句而不使 ESLint 将其标记为问题。在开发期间需要调试代码时这尤其有用。下面是包含迄今为止我们所做的所有修改的完整配置文件:

import globals from 'globals'
import js from '@eslint/js'
import stylisticJs from '@stylistic/eslint-plugin-js'

export default [
  js.configs.recommended,
  {
    files: ['**/*.js'],
    languageOptions: {
      sourceType: 'commonjs',
      globals: { ...globals.node },
      ecmaVersion: 'latest',
    },
    plugins: {
      '@stylistic/js': stylisticJs,
    },
    rules: {
      '@stylistic/js/indent': ['error', 2],
      '@stylistic/js/linebreak-style': ['error', 'unix'],
      '@stylistic/js/quotes': ['error', 'single'],
      '@stylistic/js/semi': ['error', 'never'],
      eqeqeq: 'error',
      'no-trailing-spaces': 'error',
      'object-curly-spacing': ['error', 'always'],
      'arrow-spacing': ['error', { before: true, after: true }],
      'no-console': 'off',
    },
  },
  {
    ignores: ['dist/**'],
  },
]

注 当你对 eslint.config.mjs 文件进行更改时,推荐从命令行运行 linter。这将验证配置文件的格式是否正确:

npm run lint 在终端的输出

如果你的配置文件中有什么错误,lint 插件可能会表现得相当不稳定。

许多公司都会定义编码标准,并通过 ESlint 配置文件在整个组织中强制实施。不推荐反复重新发明轮子,采用别人项目中现成的配置是个明智的选择。最近,许多项目都采用了 Airbnb 的Javascript 样式指南,并使用了 Airbnb 的 ESlint 配置。

你可以在这个 GitHub 仓库的 part3-7 分支中找到我们当前应用程序的全部代码。