320 lines
8.2 KiB
Markdown
320 lines
8.2 KiB
Markdown
# 微信分享 SDK
|
||
|
||
[](https://www.npmjs.com/package/wxsdk-pure)
|
||
[](https://github.com/yourusername/wxsdk-pure/blob/main/LICENSE)
|
||
|
||
一个轻量级、健壮的微信分享 SDK 封装,支持朋友圈分享、好友分享和微信开放标签功能。
|
||
|
||
## 功能特性
|
||
|
||
- 🚀 **一键初始化**:简化微信分享配置流程
|
||
- 🔒 **安全可靠**:完善的错误处理和参数校验
|
||
- ⚡ **智能加载**:避免重复加载微信 SDK
|
||
- ⏱️ **超时控制**:防止加载卡死
|
||
- 🔄 **Promise API**:支持 async/await 调用
|
||
- 📱 **微信开放标签**:支持最新开放标签功能
|
||
|
||
## 安装
|
||
|
||
```bash
|
||
npm install wxsdk-pure
|
||
```
|
||
```javascript
|
||
import wxSDK from 'wxsdk-pure';
|
||
```
|
||
或者浏览器直接引用
|
||
```html
|
||
<script src='./dist/wxsdk-pure.js'></script>
|
||
```
|
||
|
||
## 使用方法
|
||
|
||
### 基本使用
|
||
```javascript
|
||
wxSDK({
|
||
apiUrl: 'https://your-api.com/wx-signature',
|
||
title: ['朋友圈标题', '好友分享标题'],
|
||
desc: '分享描述内容',
|
||
shareIcon: [
|
||
'https://example.com/timeline-icon.jpg',
|
||
'https://example.com/message-icon.jpg'
|
||
],
|
||
debug: true
|
||
})
|
||
.then(wx => {
|
||
console.log('微信 SDK 初始化成功');
|
||
})
|
||
.catch(err => {
|
||
console.error('初始化失败:', err);
|
||
});
|
||
```
|
||
|
||
### 高级配置
|
||
|
||
```javascript
|
||
wxSDK({
|
||
apiUrl: 'https://your-api.com/wx-signature',
|
||
sdk: 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js',
|
||
title: '默认标题',
|
||
desc: '分享描述内容',
|
||
shareLinks: [
|
||
'https://your-domain.com/timeline-url',
|
||
'https://your-domain.com/message-url'
|
||
],
|
||
jsApiList: ['chooseImage', 'previewImage'],
|
||
openTagList: ['wx-open-launch-weapp'],
|
||
timeout: 10000,
|
||
callback: {
|
||
ready: () => console.log('分享配置就绪'),
|
||
success: (type) => console.log(`${type} 分享配置成功`),
|
||
error: (err) => console.error('分享配置失败', err)
|
||
}
|
||
});
|
||
```
|
||
|
||
## 配置选项
|
||
|
||
| 参数 | 类型 | 默认值 | 必填 | 描述 |
|
||
|-----|-----|-----|----|--------|
|
||
| `apiUrl` | string | - | 是 | 后端签名接口地址 |
|
||
| `sdk` | string | `https://res.wx.qq.com/open/js/jweixin-1.6.0.js` | 否 | 微信 SDK URL |
|
||
| `title` | string \| string[] | `['分享至朋友圈', '分享至好友']` | 否 | 分享标题 |
|
||
| `desc` | string | `'万事皆虚,万物皆允'` | 否 | 分享描述 |
|
||
| `shareIcon` | string \| string[] | 网站图标 | 否 | 分享图标 URL(如果没有使用该属性将自动抓取页面头部类型为icon的资源作为分享图) |
|
||
| `shareLinks` | string \| string[] | 当前页面 URL | 否 | 分享链接 |
|
||
| `debug` | boolean | `false` | 否 | 启用调试模式 |
|
||
| `jsApiList` | string[] | `[]` | 否 | 微信 JS API 列表 |
|
||
| `openTagList` | string[] | `[]` | 否 | 微信开放标签列表 |
|
||
| `timeout` | number | `5000` | 否 | SDK 加载超时时间(ms) |
|
||
| `callback.ready` | function | - | 否 | SDK 就绪回调 |
|
||
| `callback.success` | function | - | 否 | 分享成功回调 |
|
||
| `callback.error` | function | - | 否 | 错误回调 |
|
||
|
||
## 回调函数
|
||
|
||
### `callback.ready()`
|
||
当微信 SDK 初始化完成且分享配置就绪时触发
|
||
|
||
### `callback.success(type)`
|
||
- `type`: `'timeline'` 或 `'message'`
|
||
当朋友圈或好友分享配置成功时触发
|
||
|
||
### `callback.error(error)`
|
||
当发生错误时触发,参数为错误对象
|
||
|
||
## 注意事项
|
||
|
||
1. **跨域问题**:确保后端签名接口支持 CORS 或使用代理
|
||
2. **URL 一致性**:签名 URL 必须与当前页面 URL 完全一致(不含 hash)
|
||
3. **HTTPS**:微信要求所有页面必须使用 HTTPS
|
||
4. **开放标签**:使用开放标签需在微信公众平台申请
|
||
5. **图标大小**:微信分享图标建议尺寸 200×200 像素
|
||
|
||
## 后端接口要求
|
||
|
||
SDK 需要调用后端接口获取微信签名,接口应返回以下格式的 JSON 数据:
|
||
|
||
```json
|
||
{
|
||
"appId": "YOUR_WECHAT_APPID",
|
||
"timestamp": 1620000000,
|
||
"nonceStr": "RANDOM_STRING",
|
||
"signature": "WEIXIN_SIGNATURE"
|
||
}
|
||
```
|
||
|
||
## 后端 Express 实现
|
||
|
||
### 环境要求
|
||
|
||
1. Node.js 14+
|
||
2. Express 4.x
|
||
3. Axios 1.x
|
||
|
||
### 安装依赖
|
||
|
||
```bash
|
||
npm install express axios
|
||
```
|
||
|
||
### 具体实现
|
||
|
||
```javascript
|
||
const express = require('express');
|
||
const axios = require('axios');
|
||
const crypto = require('crypto');
|
||
const router = express.Router();
|
||
|
||
// 缓存 access_token 和 ticket
|
||
let cacheToken = null;
|
||
let cacheTicket = null;
|
||
let tokenExpireTime = 0;
|
||
let ticketExpireTime = 0;
|
||
|
||
// 微信分享签名接口
|
||
router.get('/wxShare', async (req, res) => {
|
||
// 微信公众号配置
|
||
const appId = 'appId';
|
||
const appsecret = 'appsecret';
|
||
|
||
// 获取前端传递的当前页面 URL
|
||
const { url } = req.query;
|
||
|
||
// 验证参数
|
||
if (!url) {
|
||
return res.status(400).json({
|
||
code: 400,
|
||
message: 'URL参数不能为空'
|
||
});
|
||
}
|
||
|
||
try {
|
||
// 检查并刷新 access_token
|
||
const now = Math.floor(Date.now() / 1000);
|
||
if (!cacheToken || now >= tokenExpireTime) {
|
||
const tokenResponse = await axios.get(
|
||
`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appsecret}`
|
||
);
|
||
|
||
if (tokenResponse.data.errcode) {
|
||
throw new Error(`获取access_token失败: ${tokenResponse.data.errmsg}`);
|
||
}
|
||
|
||
cacheToken = tokenResponse.data.access_token;
|
||
// 提前10分钟过期
|
||
tokenExpireTime = now + tokenResponse.data.expires_in - 600;
|
||
}
|
||
|
||
// 检查并刷新 ticket
|
||
if (!cacheTicket || now >= ticketExpireTime) {
|
||
const ticketResponse = await axios.get(
|
||
`https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${cacheToken}&type=jsapi`
|
||
);
|
||
|
||
if (ticketResponse.data.errcode !== 0) {
|
||
throw new Error(`获取ticket失败: ${ticketResponse.data.errmsg}`);
|
||
}
|
||
|
||
cacheTicket = ticketResponse.data.ticket;
|
||
// 提前10分钟过期
|
||
ticketExpireTime = now + ticketResponse.data.expires_in - 600;
|
||
}
|
||
|
||
// 生成签名
|
||
const nonceStr = generateNonceStr(16);
|
||
const timestamp = Math.floor(Date.now() / 1000);
|
||
|
||
const signature = generateSignature({
|
||
jsapi_ticket: cacheTicket,
|
||
noncestr: nonceStr,
|
||
timestamp,
|
||
url
|
||
});
|
||
|
||
res.json({
|
||
appId,
|
||
nonceStr,
|
||
timestamp,
|
||
signature
|
||
});
|
||
|
||
} catch (err) {
|
||
console.error('微信分享签名错误:', err);
|
||
|
||
// 重置缓存
|
||
cacheToken = null;
|
||
cacheTicket = null;
|
||
|
||
res.status(500).json({
|
||
code: 500,
|
||
message: '服务器内部错误',
|
||
error: err.message
|
||
});
|
||
}
|
||
});
|
||
|
||
// 生成随机字符串
|
||
function generateNonceStr(length = 16) {
|
||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||
let nonceStr = '';
|
||
|
||
for (let i = 0; i < length; i++) {
|
||
nonceStr += chars.charAt(Math.floor(Math.random() * chars.length));
|
||
}
|
||
|
||
return nonceStr;
|
||
}
|
||
|
||
// 生成签名
|
||
function generateSignature(params) {
|
||
const string = Object.keys(params)
|
||
.sort()
|
||
.map(key => `${key}=${params[key]}`)
|
||
.join('&');
|
||
|
||
return crypto.createHash('sha1').update(string).digest('hex');
|
||
}
|
||
|
||
module.exports = router;
|
||
```
|
||
|
||
### 错误响应
|
||
|
||
```json
|
||
{
|
||
"code": 400,
|
||
"message": "URL参数不能为空"
|
||
}
|
||
```
|
||
|
||
```json
|
||
{
|
||
"code": 500,
|
||
"message": "服务器内部错误",
|
||
"error": "获取access_token失败: invalid appid"
|
||
}
|
||
```
|
||
|
||
### 启动服务
|
||
|
||
在 `app.js` 中集成路由:
|
||
|
||
```javascript
|
||
const express = require('express');
|
||
const wxShareRouter = require('./routes/wxShare');
|
||
|
||
const app = express();
|
||
|
||
// 中间件
|
||
app.use(express.json());
|
||
app.use(express.urlencoded({ extended: true }));
|
||
|
||
// 路由
|
||
app.use('/api', wxShareRouter);
|
||
|
||
// 错误处理
|
||
app.use((err, req, res, next) => {
|
||
console.error(err.stack);
|
||
res.status(500).send('服务器错误');
|
||
});
|
||
|
||
// 启动服务器
|
||
const PORT = process.env.PORT || 3000;
|
||
app.listen(PORT, () => {
|
||
console.log(`服务器运行在端口 ${PORT}`);
|
||
});
|
||
```
|
||
|
||
## 常见问题
|
||
|
||
### 1. 签名无效 (invalid signature)
|
||
|
||
可能原因:
|
||
- URL 不一致(前端传入的 URL 必须与当前页面完全一致)
|
||
- 时间戳不一致(确保服务器时间准确)
|
||
- 签名算法错误(严格按照微信文档实现)
|
||
|
||
解决方案:
|
||
- 验证前端传入的 URL 是否去除 hash 部分
|
||
- 检查服务器时间是否同步
|
||
- 使用微信官方签名校验工具验证 |