个人实践之Express后端构建

环境配置

1. 初始化项目

首先,在一个新目录中初始化一个 Node.js 项目:

1
2
3
mkdir express-backend
cd express-backend
npm init -y

这会创建一个基本的 package.json 文件,后续可以通过这个文件管理项目的依赖。

2. 安装依赖(好多不认识的)

安装 Express 框架以及一些常见的开发工具和中间件:

1
2
3
npm install express
npm install dotenv morgan cors body-parser
npm install --save-dev nodemon
  • express: 核心的 Web 框架。
  • dotenv: 用于管理环境变量,存储敏感信息(如数据库连接字符串、API 密钥等)。
  • morgan: HTTP 请求日志中间件,可以用来记录请求日志,方便调试和监控。
  • cors: 解决跨域问题的中间件。
  • body-parser: 用于解析请求体中的 JSON 数据(对于 POST 请求等非常常见)。
  • nodemon: 开发环境中用于自动重启服务的工具。
  • (后面我确认不需要了会进行修改)

3. 项目结构设计(估计不会使用这个设计)

一般来说,可以按以下结构组织 Express 项目:

1
2
3
4
5
6
7
8
9
10
11
12
13
express-backend/
├── node_modules/
├── src/
│ ├── controllers/ # 控制器,用于处理业务逻辑
│ ├── middleware/ # 中间件
│ ├── models/ # 数据模型(如果使用数据库)
│ ├── routes/ # 路由
│ ├── config/ # 配置文件(如数据库连接、API 密钥等)
│ └── server.js # 入口文件
├── .env # 环境变量文件
├── .gitignore # 忽略文件配置
├── package.json
└── README.md
  • src/controllers:处理具体的业务逻辑。
  • src/middleware:放置自定义中间件,例如身份验证中间件、日志中间件等。
  • src/routes:定义 API 路由,调用控制器。
  • src/models:数据库模型(如使用 ORM,Mongoose 或 Sequelize 等)。
  • src/config:存放项目的配置文件,如数据库连接、第三方 API 密钥等。

4. 创建基础的 Express 应用

src/server.js 中创建一个基本的 Express 服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
javascript// src/server.js
const express = require('express');
const cors = require('cors');
const morgan = require('morgan');
const bodyParser = require('body-parser');
require('dotenv').config(); // 加载 .env 文件中的环境变量

const app = express();
const port = process.env.PORT || 3000; // 读取环境变量中的端口,默认 3000

// 中间件配置
app.use(cors()); // 解决跨域问题
app.use(morgan('dev')); // 请求日志
app.use(bodyParser.json()); // 解析 JSON 请求体

// 路由配置
app.get('/', (req, res) => {
res.send('Hello, Express!');
});

// 启动服务器
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});

(补充)主要区别:CommonJS 与 ES Modules

  1. **requireimport**:

    • require 是 CommonJS 模块系统的导入方式,它是 Node.js 默认的模块系统,直到 Node.js v12 之前。
    • import 是 ES Modules 的导入方式,这种方式更符合现代 JavaScript 标准,并且在前端 JavaScript(例如浏览器中)中也广泛使用。
  2. 导出

    • CommonJS使用module.exportsexports来导出模块。例如:javascriptmodule.exports = { myFunction };
    • ES Modules使用exportexport default来导出模块。例如:javascriptexport default myFunction;
  3. 同步与异步

    • require 是同步加载模块的,因此在代码执行时会直接读取模块。
    • import 是异步加载的,虽然在大多数情况下,JavaScript 引擎会自动处理它,但与 require 的行为稍有不同。
  4. 文件扩展名

    • 使用 require 时,你通常不需要加 .js 后缀,但在 import 中,通常需要加上 .js 后缀,除非它是一个标准模块(例如 express)。

    • 例如:

      1
      import something from './module.js';  // 必须加上 .js 扩展名

为什么选择 ES Modules(import)?

  1. 现代化标准import/export 是现代 JavaScript 标准,符合 ECMAScript 规范。
  2. 更好的静态分析:与 require 相比,import 语句更容易进行静态分析,这使得工具(如打包器、代码压缩器等)能够更好地优化代码。
  3. 跨平台一致性:前端 JavaScript 使用 import,这样你的 Node.js 后端与前端代码的模块化系统保持一致。

5. 使用环境变量管理敏感信息

在根目录下创建 .env 文件来存储环境变量(如数据库连接字符串、API 密钥等):

1
bashtouch .env

.env 文件中添加环境变量:

1
2
3
4
PORT=3000
DB_HOST=localhost
DB_USER=myuser
DB_PASSWORD=mypassword

在代码中通过 dotenv 加载环境变量:

1
2
3
require('dotenv').config();

const port = process.env.PORT || 3000;

6. 设置开发与生产环境

  • 使用 nodemon 来自动重启服务器,这对于开发阶段非常有用。可以在 package.json 中配置 dev 脚本:
1
2
3
4
json"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js"
}
  • 在生产环境中,通常使用 pm2 来管理进程,它能确保应用在生产中稳定运行,并提供负载均衡、日志管理等功能。安装 pm2
1
npm install pm2 --save-dev

配置生产环境时可以在 .env 文件中加入不同的变量,或者使用外部的环境变量进行配置。

(补充)JavaScript 的 Build 脚本

在 JavaScript 中,build 脚本通常用于将源代码编译、打包或压缩成可用于生产环境的文件。通常使用工具如 webpack, rollup, esbuildparcel 来进行打包和构建。

假设你在 JavaScript 项目中,tsc 主要用于 TypeScript 构建。如果是纯 JavaScript 项目,build 脚本通常会使用以下工具之一:

1. **使用 webpack**:

Webpack 是最常用的 JavaScript 构建工具,通常用于打包和压缩 JavaScript 代码。

  • 安装依赖:

    1
    npm install --save-dev webpack webpack-cli
  • 配置 webpack.config.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const path = require('path');

    module.exports = {
    entry: './src/index.js', // 入口文件
    output: {
    filename: 'bundle.js', // 输出文件
    path: path.resolve(__dirname, 'dist'),
    },
    mode: 'production', // 使用生产模式
    };
  • package.json 中配置 build 脚本:

    1
    2
    3
    "scripts": {
    "build": "webpack"
    }

7.配置git

运行git init命令,创建创建.gitignore文件,配置一些基本信息

1
2
3
node_modules
dist
.env

代码演示

(因为在另外一个文件《全栈开发之路》里面有简单提到了,所以这里只是列举一些JavaScript和typescript的区别)

  • javascript代码中无类型声明
  • import文件必须加上 .js 扩展名

(补充:)

已经使用了 body-parser 来处理 JSON 数据 (bodyParser.json())。因此,**不需要再额外添加 json()urlencoded()**,因为 body-parser 库提供了这些功能。

  1. json()urlencoded() 是 Express 4.x 版本自带的中间件,主要用于解析请求体中的数据:
    • **app.use(json())**:解析传入请求的 JSON 数据。如果你发送的是 JSON 格式的数据(例如通过 fetchaxios 发送 JSON),这个中间件会自动解析请求体中的 JSON 数据,并把它放到 req.body 中。
    • **app.use(urlencoded({ extended: false }))**:用于解析 URL 编码的表单数据(application/x-www-form-urlencoded)。如果你从表单提交数据(通过 POST 请求),这个中间件会解析数据并将其放到 req.body 中。extended: false 表示你只能通过简单的字符串和数组传递数据,而不能嵌套对象。
  2. **你使用了 body-parser**:body-parser 是一个专门处理请求体的中间件,它提供了 json()urlencoded() 方法来解析不同格式的请求体。你已经在代码中使用了 bodyParser.json(),它就实现了 app.use(json()) 的功能。
  3. 是否需要添加 json()urlencoded()
    • **如果你已经使用 body-parser**,那么不需要再使用 Express 自带的 json()urlencoded()
    • body-parser 在 Express 4.x 版本之后已经被集成为 Express 的一个独立库,你可以继续使用它,或者直接使用 Express 自带的中间件(如果不依赖于 body-parser)进行解析。

你的代码已经包含了 body-parser 的配置,实际上已经覆盖了 json()urlencoded() 的功能。因此,不需要再添加这两个中间件,你的代码已经处理了 JSON 数据的解析。如果你需要处理 application/x-www-form-urlencoded 的数据,可以使用 bodyParser.urlencoded({ extended: false }),或者继续使用 app.use(urlencoded({ extended: false }))(如果没有使用 body-parser)。


建立数据库以及使用ORM工具

目前应该使用的是prisma(因为之前使用的是drizzle,所以不想照搬)和postgresql(因为Neon提供了Free的数据库,可以很好地拿来测试)

prisma的官方文档写得挺详细的了,这里就不多做解释(不过我怕类型检查没有用Typescript,希望别被什么折磨到了)

如何在 JavaScript 项目中使用 Prisma:

  1. 安装 Prisma 和数据库客户端 首先,你需要在项目中安装 Prisma 及其数据库客户端(如 MySQL、PostgreSQL 等):

    1
    npm install prisma @prisma/client
  2. 初始化 Prisma 配置 然后,使用以下命令初始化 Prisma:

    1
    npx prisma init

    这会生成一个 prisma 文件夹,包含 schema.prisma 文件,这个文件定义了数据库模型。

  3. 配置 schema.prismaschema.prisma 文件中,你需要配置数据库连接信息,以及定义数据模型。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    datasource db {
    provider = "postgresql" // 或者使用其他数据库
    url = env("DATABASE_URL")
    }

    generator client {
    provider = "prisma-client-js" // 选择 JavaScript 客户端
    }

    model User {
    id Int @id @default(autoincrement())
    name String
    email String @unique
    }
  4. 生成 Prisma 客户端 使用以下命令生成 Prisma 客户端代码:

    1
    npx prisma generate

(补充我的疑惑:)

npx prisma generatenpx prisma db push 是两个不同的命令,它们的功能不一样,且在使用 Prisma 时有不同的目的和作用。

npx prisma generatenpx prisma db push 的区别:

  1. **npx prisma generate**:
    • 功能:该命令的主要作用是 生成 Prisma Client。它根据 schema.prisma 文件中定义的数据模型自动生成客户端代码,用于在应用中与数据库进行交互。
    • 何时使用:每当你更改了 schema.prisma 文件中的模型(如添加、修改或删除数据模型)时,都需要运行 npx prisma generate 来更新 Prisma Client,以使客户端代码与最新的模型同步。
    • 注意:它不涉及数据库结构的更改,仅负责生成客户端代码。
  2. **npx prisma db push**:
    • 功能:该命令的作用是将 schema.prisma 文件中的数据模型推送到数据库。它会基于 schema.prisma 中的模型定义,自动在数据库中创建或更新表、字段等结构。
    • 何时使用:当你对 schema.prisma 文件中的数据模型进行修改时,如果你希望这些修改反映到实际的数据库中,可以运行 npx prisma db push。它会同步数据库结构,无需手动编写迁移脚本。
    • 注意:它只会同步数据库结构,不涉及 Prisma Client 的更新。也就是说,db push 不会生成 Prisma Client,它只更新数据库。

需要先运行哪个命令?

  • 需要先运行 npx prisma generate 吗?:如果你只是修改了数据库模型(即修改了 schema.prisma),并且希望生成相应的 Prisma Client 代码,**你应该先运行 npx prisma generate**,这样你的应用程序就可以使用最新的 Prisma Client 来与数据库交互。
  • 需要先运行 npx prisma db push 吗?:如果你修改了 schema.prisma 中的模型,并希望将这些更改同步到数据库中(例如创建或更新数据库表和字段),你可以运行 npx prisma db push。它会确保数据库结构与 schema.prisma 一致。

总结

  • npx prisma generate 用于生成 Prisma Client,使你能够在应用中通过自动生成的查询方法操作数据库。
  • npx prisma db push 用于将 schema.prisma 中的模型推送到数据库,更新数据库结构。

一般的使用顺序是:先修改 schema.prisma,然后运行 npx prisma db push 更新数据库结构,最后运行 npx prisma generate 生成与之匹配的 Prisma Client。如果只是修改代码而不需要同步数据库结构,可以只运行 npx prisma generate


(补充:)

console.dir(allUsers, { depth: null }) 是 JavaScript 中用于打印对象的一个方法,它与常见的 console.log() 类似,但具有一些不同之处,特别是在打印复杂或嵌套的对象时。

解释:

  • console.dir():用于打印 JavaScript 对象的内容,提供比 console.log() 更详细的信息,特别适合查看深度嵌套的对象。它显示的内容通常比 console.log() 更易于查看。
  • { depth: null }:这是一个选项,指定了打印的对象的嵌套深度。
    • depth: null 表示没有深度限制,即会递归打印对象的所有层级,直到对象的每一层都被完全展开。
    • 如果你使用其他数字(比如 { depth: 2 }),它将限制打印的嵌套深度到指定的层数。超过这个深度的嵌套结构将不会被完全展开。

在你的代码中:

1
2
3
4
5
6
7
const allUsers = await prisma.user.findMany({
include: {
posts: true,
profile: true,
},
});
console.dir(allUsers, { depth: null });
  • prisma.user.findMany() 查询了所有用户,并且通过 include 选项包括了 postsprofile 相关的关联数据。
  • console.dir(allUsers, { depth: null }) 会打印出 allUsers 数组的内容。如果 allUsers 是一个包含多个用户对象的数组,而每个用户对象可能还包含嵌套的 postsprofile 数据,这时使用 console.dir() 可以完整地查看这些数据,包括所有层级的嵌套内容。

为什么使用 console.dir() 而不是 console.log()

  • console.dir() 在处理复杂的对象时,能更方便地显示嵌套的结构。例如,如果你要查看 allUsers 中每个用户的 postsprofile 数据,console.dir() 会展开并显示这些关联数据。