node项目规范及重构升级

服务器

前言

继上次简单的创建node项目及部署node部署及简单的接口调用 (opens new window),但为了方便后面使用和学习,将node项目重构为更规范、更易维护。
主要是为了不让所有代码都在index.js里面,接下来让我们将项目改成下方展示的结构,主要是src文件。

# wechat-api-nodeServer
│
├── src/                # 源代码
│   ├── controllers/    # 控制器
│   ├── routes/         # 路由
│   └── utils/          # 工具类
│
├── package.json        # 项目依赖配置
├── .gitignore          # Git 忽略文件
└── README.md           # 项目说明文件
1
2
3
4
5
6
7
8
9
10

# 1、解释(controllers(控制器)、routes(路由)、utils(工具类))

1-1:controllers(控制器)
控制器用于处理具体的业务逻辑。例如,获取小程序的 token、处理用户登录等操作。
1-2:routes(路由)
路由用于定义那些URL路径触发那个控制器的函数,例如,像交通警察一样,决定请求走哪条路。
1-3:utils(工具类) 顾名思义这个文件夹存放一些公共工具函数,比如发起 HTTP 请求、格式化时间、处理签名验证等。

# 2、📁 结构的调整

# 老
├── index.js            # 项目入口,负责启动 Express 应用
├── package.json        # 项目依赖配置
├── .gitignore          # Git 忽略文件
└── README.md           # 项目说明文件
# 新
│
├── src/
│   ├── controllers/
│   │   └── wechatController.js      # 处理微信 token 获取逻辑
│   ├── routes/
│   │   └── wechatRoutes.js          # 配置路由
│   └── utils/
│       └── request.js               # 工具函数封装 HTTP 请求
│
├── index.js                         # 项目入口,负责启动 Express 应用
├── package.json                     # 项目依赖配置
└── .gitignore                       # Git 忽略文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 3、📄 修改 index.js内容的修改

import express from 'express';
import wechatRoutes from './src/routes/wechatRoutes.js';
require('dotenv').config(); # 加载.env

const app = express();
const PORT = process.env.PORT || 3000; # .env 用于存放 敏感配置或环境变量,比如微信的 appId、appSecret。你需要安装 dotenv 包来加载它。

# 允许跨域(本地调试)
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  next();
});

// 路由挂载
app.use('/api', wechatRoutes);

app.listen(PORT, () => {
  console.log(`🚀 Server is running at http://localhost:${PORT}`);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

3-1:在项目根目录创建.env文件

APP_ID = your_wechat_app_id
APP_SECRET = your_wechat_app_secret
PORT = 3000
1
2
3

3-2:安装 dotenv

# dotenv 就是一个帮助我们从 .env 文件中读取这些环境变量的工具。
npm install dotenv
# 为了让 Node.js 可以安全地读取这些环境变量了。
1
2
3

3-3:在 index.js 中加载 .env

require('dotenv').config(); # 加载.env
1

然后就可以使用变量了,如index.js里:const PORT = process.env.PORT || 3000;

# 4、📄 增加 src/routes/wechatRoutes.js

import express from 'express';
import { getWeChatToken } from '../controllers/wechatController.js';

const router = express.Router();

router.get('/get-token', getWeChatToken); # GET /api/get-token?appId=xxx&appSecret=xxx

export default router;
1
2
3
4
5
6
7
8

# 5、📄 增加 src/controllers/wechatController.js

import { requestGet } from '../utils/request.js';

export async function getWeChatToken(req, res) {
  const { appId, appSecret } = req.query;

  if (!appId || !appSecret) {
    return res.status(400).json({ error: '缺少 appId 或 appSecret 参数' });
  }

  try {
    const url = 'https://api.weixin.qq.com/cgi-bin/token';
    const response = await requestGet(url, {
      grant_type: 'client_credential',
      appid: appId,
      secret: appSecret,
    });

    res.json(response.data);
  } catch (error) {
    res.status(500).json({ error: '请求失败', detail: error.message });
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 5、📄 增加 src/utils/request.js

import axios from 'axios';

export function requestGet(url, params = {}) {
  return axios.get(url, { params });
}
1
2
3
4
5

# 5、📄 修改 package.json

{
  "name": "wechat-api-nodeServer",
  "version": "1.0.0",
  "type": "module", # type:module,因为咱们上面咱们采用import引入
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "axios": "^1.6.0",
    "express": "^4.18.2"
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 6、📄 修改 .gitignore

node_modules/
.env
1
2

# 7、启动方式不变

node index.js 或 npm run start
# 或者用 pm2,pm2安装参考上一篇文章
pm2 restart index 或 pm2 restart all重启所有服务 或 pm2 restart <id> 启动单个
1
2
3

# 补充知识点

不使用 "type": "module"(改用 require
如果你不想在 package.json 中加 "type": "module",你可以用传统的 CommonJS 语法(require)。 把 package.json 中删掉 "type": "module"
然后把所有的 import / export 改成 require / module.exports:
✅ 修改所有文件如下: 📄 index.js

# import express from 'express';
const express = require('express'); # 重点

# import wechatRoutes from './src/routes/wechatRoutes.js';
const wechatRoutes = require('./src/routes/wechatRoutes'); # 重点

require('dotenv').config(); # 加载 .env 配置

const app = express();
const PORT = process.env.PORT || 3000;

app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  next();
});

app.use('/api', wechatRoutes);

app.listen(PORT, () => {
  console.log(`🚀 Server running at http://localhost:${PORT}`);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

📄 src/routes/wechatRoutes.js

# import express from 'express';
const express = require('express'); # 重点
# import { getWeChatToken } from '../controllers/wechatController.js';
const { getWeChatToken } = require('../controllers/wechatController'); # 重点

const router = express.Router();

router.get('/get-token', getWeChatToken); # GET /api/get-token?appId=xxx&appSecret=xxx

# export default router;
module.exports = router; # 重点
1
2
3
4
5
6
7
8
9
10
11

📄 src/controllers/wechatController.js

# import { requestGet } from '../utils/request.js';
const { requestGet } = require('../utils/request'); # 重点

async function getWeChatToken(req, res) { # 不要默认导出了
  const { appId, appSecret } = req.query;

  if (!appId || !appSecret) {
    return res.status(400).json({ error: '缺少 appId 或 appSecret 参数' });
  }

  try {
    const url = 'https://api.weixin.qq.com/cgi-bin/token';
    const response = await requestGet(url, {
      grant_type: 'client_credential',
      appid: appId,
      secret: appSecret,
    });

    res.json(response.data);
  } catch (error) {
    res.status(500).json({ error: '请求失败', detail: error.message });
  }
}
# 重点 导出
module.exports = {
  getWeChatToken,
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

📄 src/utils/request.js

# import axios from 'axios';
const axios = require('axios');

function requestGet(url, params = {}) { # 不要默认导出了
  return axios.get(url, { params });
}

# 重点 导出
module.exports = {
  requestGet,
};
1
2
3
4
5
6
7
8
9
10
11

# 增加接口 小程序:异步验证校验多媒体文件接口 /wxa/media check async

📁 1. src/controllers/wechatController.js 增加新方法

const { requestPost } = require('../utils/request');

async function mediaCheckAsync(req, res) {
  const { accessToken, mediaUrl, mediaType } = req.body;

  if (!accessToken || !mediaUrl || !mediaType) {
    return res.status(400).json({ error: '缺少必要参数 accessToken、mediaUrl 或 mediaType' });
  }

  try {
    const url = `https://api.weixin.qq.com/wxa/media_check_async?access_token=${accessToken}`;
    const payload = {
      media_url: mediaUrl,
      media_type: parseInt(mediaType),
    };

    const response = await requestPost(url, payload);

    res.json(response.data);
  } catch (error) {
    res.status(500).json({ error: '媒体校验请求失败', detail: error.message });
  }
}

module.exports = {
  getWeChatToken,
  mediaCheckAsync, # 记得导出、记得导出、记得导出
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

📁 2. src/routes/wechatRoutes.js 添加新路由

const express = require('express');
const { getWeChatToken, mediaCheckAsync } = require('../controllers/wechatController');

const router = express.Router();

router.get('/token', getWeChatToken);
router.post('/media-check', mediaCheckAsync); # POST /api/media-check

module.exports = router;
1
2
3
4
5
6
7
8
9

📁 3. src/utils/request.js 添加 requestPost 方法

const axios = require('axios');

function requestGet(url, params = {}) {
  return axios.get(url, { params });
}

function requestPost(url, data = {}) {
  return axios.post(url, data);
}

module.exports = {
  requestGet,
  requestPost,
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

✅ 4. 添加中间件支持 req.body(如果你还没加) 修改 index.js

# app.use(express.json());  # 加这一句,才能解析 JSON body 请求,如下:
const express = require('express'); # 重点
const wechatRoutes = require('./src/routes/wechatRoutes'); # 重点

require('dotenv').config(); # 加载 .env 配置

const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json()); # 加这一句,才能解析 JSON body 请求

app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
  next();
});

app.use('/api', wechatRoutes);

app.listen(PORT, () => {
  console.log(`🚀 Server running at http://localhost:${PORT}`);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 小程序调用

🔸 1. 获取 token 示例(GET 请求)

wx.request({
  url: 'https://你的业务域名/api/token',
  method: 'GET',
  data: {
    appid: '你的appid',
    secret: '你的secret',
  },
  success(res) {
    console.log('获取 token 成功', res.data);
  },
  fail(err) {
    console.error('获取 token 失败', err);
  }
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

🔸 2. 异步校验媒体(POST 请求)

wx.request({
  url: 'https://你的业务域名/api/media-check',
  method: 'POST',
  header: {
    'Content-Type': 'application/json',
  },
  data: {
    accessToken: '你获取到的token',
    mediaUrl: 'https://example.com/1.jpg',
    mediaType: 2,
  },
  success(res) {
    console.log('媒体校验结果:', res.data);
  },
  fail(err) {
    console.error('校验失败', err);
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 总结:

至此就把项目结构调整完成啦!赶紧去尝试把。此次咱们主要学习了controllers(控制层)、routes(路由)、utils(封装工具类)、也复习一遍 模块化,主要形式CommonJS (node采用的方式)、 ES Modules (ESM)(JavaScript 的官方标准模块系统),Node v12开始通过在package.json 中增加type:module来启用ESM模式。

# 补充-小程序获取token有效时间是7200秒(2个小时)

目前access_token的有效期通过返回的expires_in来传达,目前是7200秒之内的值。如何没有超过7200可以直接使用缓存里面的老token,无需再次调用。
修改

const { requestGet } = require('../utils/request'); # 重点
# 用对象按 appId 缓存 token 和过期时间
const tokenCache = {};

async function getWeChatToken(req, res) { # 不要默认导出了
  const { appId, appSecret } = req.query;

  if (!appId || !appSecret) {
    return res.status(400).json({ error: '缺少 appId 或 appSecret 参数' });
  }

  const now = Date.now();
  const cached = tokenCache[appId];
  
  # 如果已缓存并未过期,返回缓存的 token
  if (cached && cached.token && cached.expireTime > now) {
    return res.json({
      access_token: cached.token,
      expires_in: Math.floor((cached.expireTime - now) / 1000),
      cached: true
    });
  }

  try {
    const url = 'https://api.weixin.qq.com/cgi-bin/token';
    const response = await requestGet(url, {
      grant_type: 'client_credential',
      appid: appId,
      secret: appSecret,
    });
    
    const token = result.data.access_token;
    const expireTime = now + result.data.expires_in * 1000;
    
     # 缓存 token
    tokenCache[appId] = {
      token,
      expireTime
    };
    
    res.json(response.data);
  } catch (error) {
    res.status(500).json({ error: '请求失败', detail: error.message });
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
Last Updated: 2025/3/17 16:31:21