
从零开发一个钉钉机器人消息发送 npm 库:ddmessage-fruge365
前言
在日常开发中,我们经常需要将系统状态、错误信息、数据报告等通过钉钉机器人发送到群聊中。虽然钉钉提供了 Webhook API,但每次都要手写 HTTP 请求代码,还要处理跨域问题,非常繁琐。
于是我开发了 ddmessage-fruge365
这个 npm 库,让钉钉机器人消息发送变得简单易用。
项目背景
遇到的问题
- 重复代码:每个项目都要写一遍钉钉 API 调用代码
- 跨域限制:浏览器环境无法直接调用钉钉 API
- 类型安全:缺少 TypeScript 类型定义
- 消息格式:需要手动构造复杂的消息结构
解决方案
开发一个统一的 npm 库,提供:
- 简洁的 API 接口
- 完整的 TypeScript 支持
- 跨域代理解决方案
- 多种消息类型支持
技术选型
核心技术栈
- TypeScript:提供类型安全和更好的开发体验
- Axios:处理 HTTP 请求
- Rollup:构建 ES 模块版本
- Jest:单元测试框架
项目结构
ddmessage-fruge365/
├── src/
│ ├── index.ts # 主要逻辑
│ ├── types.ts # 类型定义
│ └── utils.ts # 工具函数
├── lib/ # 编译输出
├── package.json
├── tsconfig.json
└── rollup.config.js
核心功能实现
1. 基础类型定义
typescript
// src/types.ts
export interface DingTalkConfig {
accessToken: string; // 机器人 Access Token
baseUrl?: string; // API 基础地址
timeout?: number; // 请求超时时间
proxyPath?: string; // 代理路径
}
export interface TextMessage {
msgtype: 'text';
text: {
content: string;
};
at?: {
atMobiles?: string[];
atUserIds?: string[];
isAtAll?: boolean;
};
}
export interface DingTalkResponse {
errcode: number;
errmsg: string;
}
2. 核心类实现
typescript
// src/index.ts
export class DingTalkRobot {
private client: AxiosInstance;
private accessToken: string;
private proxyPath?: string;
constructor(config: DingTalkConfig) {
this.accessToken = config.accessToken;
this.proxyPath = config.proxyPath;
// 根据是否使用代理选择不同的 baseURL
const baseURL = this.proxyPath ? '' : (config.baseUrl || 'https://oapi.dingtalk.com');
this.client = axios.create({
baseURL,
timeout: config.timeout || 10000,
headers: {
'Content-Type': 'application/json',
},
});
}
async send(message: Message): Promise<DingTalkResponse> {
try {
let response;
if (this.proxyPath) {
// 使用代理时,直接请求代理路径
const url = `${this.proxyPath}?access_token=${this.accessToken}`;
response = await axios.post(url, message, {
timeout: 10000,
headers: { 'Content-Type': 'application/json' }
});
} else {
// 不使用代理时,使用配置的 client
const url = `/robot/send?access_token=${this.accessToken}`;
response = await this.client.post(url, message);
}
return response.data;
} catch (error: any) {
throw new Error(`发送消息失败: ${error.message}`);
}
}
async sendText(content: string, at?: AtOptions): Promise<DingTalkResponse> {
const message: TextMessage = {
msgtype: 'text',
text: { content },
...(at && { at }),
};
return this.send(message);
}
}
3. 跨域问题解决
这是项目的核心难点。浏览器环境下直接调用钉钉 API 会遇到跨域问题,需要通过代理服务器转发请求。
Vite 代理配置
javascript
// vite.config.js
export default {
server: {
proxy: {
'/dd-api': {
target: 'https://oapi.dingtalk.com/robot/send',
changeOrigin: true,
rewrite: (path) => path.replace('/dd-api', ''),
},
},
},
}
库中的处理逻辑
typescript
// 根据是否使用代理选择不同的请求方式
if (this.proxyPath) {
// 浏览器环境:使用代理路径
const url = `${this.proxyPath}?access_token=${this.accessToken}`;
response = await axios.post(url, message);
} else {
// Node.js 环境:直接调用钉钉 API
const url = `/robot/send?access_token=${this.accessToken}`;
response = await this.client.post(url, message);
}
构建配置
TypeScript 配置
json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./lib",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"declaration": true,
"sourceMap": true
}
}
Rollup 配置(ES 模块支持)
javascript
// rollup.config.js
const resolve = require('@rollup/plugin-node-resolve');
const commonjs = require('@rollup/plugin-commonjs');
const typescript = require('@rollup/plugin-typescript');
module.exports = {
input: 'src/index.ts',
output: {
file: 'lib/index.esm.js',
format: 'es'
},
plugins: [
resolve(),
commonjs(),
typescript({
tsconfig: './tsconfig.esm.json'
})
],
external: ['axios']
};
Package.json 配置
json
{
"main": "lib/index.js",
"module": "lib/index.esm.js",
"types": "lib/index.d.ts",
"exports": {
".": {
"import": "./lib/index.esm.js",
"require": "./lib/index.js"
}
}
}
使用示例
Node.js 环境
javascript
const DingTalkRobot = require('ddmessage-fruge365');
const robot = new DingTalkRobot({
accessToken: 'your-access-token'
});
// 发送文本消息
await robot.sendText('系统启动成功!');
// 发送 Markdown 消息
await robot.sendMarkdown('日报', `
## 今日工作总结
- 完成功能开发 ✅
- 修复 3 个 bug 🐛
- 代码审查通过 👍
`);
Vue 项目中使用
javascript
import DingTalkRobot from 'ddmessage-fruge365';
const robot = new DingTalkRobot({
accessToken: 'your-access-token',
proxyPath: '/dd-api' // 使用代理
});
// 发送消息
await robot.sendText('Hello from Vue!');
遇到的技术难点
1. 模块格式兼容性
需要同时支持 CommonJS 和 ES 模块:
解决方案:
- 使用 TypeScript 编译 CommonJS 版本
- 使用 Rollup 构建 ES 模块版本
- 在 package.json 中配置 exports 字段
2. 跨域代理逻辑
不同环境需要不同的请求方式:
解决方案:
- 通过
proxyPath
参数判断是否使用代理 - 代理环境直接使用 axios 请求代理路径
- 非代理环境使用配置的 axios 实例
3. TypeScript 类型安全
确保所有 API 都有完整的类型定义:
解决方案:
- 定义完整的接口类型
- 使用泛型约束参数类型
- 生成 .d.ts 声明文件
项目亮点
1. 开箱即用
javascript
// 一行代码发送消息
await robot.sendText('Hello World!');
2. 完整的 TypeScript 支持
typescript
interface DingTalkConfig {
accessToken: string;
proxyPath?: string;
timeout?: number;
}
3. 跨平台兼容
- Node.js 服务端
- Vue/React 前端项目
- 原生 JavaScript
4. 多种消息类型
- 文本消息(支持 @ 功能)
- Markdown 消息
- 链接消息
性能优化
1. 按需加载
javascript
// 只导入需要的功能
import { getCurrentIP } from 'ddmessage-fruge365/lib/utils';
2. 请求优化
- 配置合理的超时时间
- 复用 axios 实例
- 错误重试机制
3. 包体积优化
- 使用 Rollup 进行 Tree Shaking
- 外部化依赖(axios)
- 压缩输出代码
测试与发布
单元测试
javascript
// 测试消息发送功能
describe('DingTalkRobot', () => {
test('should send text message', async () => {
const robot = new DingTalkRobot({ accessToken: 'test' });
const result = await robot.sendText('test message');
expect(result.errcode).toBe(0);
});
});
发布流程
bash
# 1. 构建项目
npm run build
# 2. 运行测试
npm test
# 3. 发布到 npm
npm publish
总结
通过开发 ddmessage-fruge365
这个项目,我深入学习了:
- npm 包开发:从设计到发布的完整流程
- TypeScript 工程化:类型定义、编译配置、声明文件生成
- 跨域问题解决:代理配置、环境适配
- 模块化设计:CommonJS 和 ES 模块兼容
- 工具链配置:Rollup、Jest、TypeScript 集成
这个库目前已发布到 npm,欢迎大家使用和反馈!
相关链接
- GitHub:https://github.com/fruge365/DdMessage
- NPM:https://www.npmjs.com/package/ddmessage-fruge365
- 作者博客:https://fruge365.blog.csdn.net/
如果这篇文章对你有帮助,欢迎点赞收藏!有任何问题也可以在评论区交流讨论。