一些開發紀錄(假設白名單之類的設定好了),程式碼部分要怎麼處理
- User 開啟網頁
- client 端向 server 請求 signature
- server 收到請求後,請求 weixin 提供 token、ticket 來產生 signature 回傳
- client 端收到 signature 後設定 weixin API
用來組簽名的 url 參數一定要用當下的 url (window.location.href)。
因為微信會在 url 加上它們的參數,這些參數要包含在 url 中,不然會失敗。
所以不要自己組自己的 url,又或者記得要把 search 也包含進去。
// 透過 scriptjs library 用在 react 中。達到 load 外部 script 的功能
import script from 'scriptjs';
const readyAsync = key => new Promise(resolve => script.ready(key, resolve));
const JWEIXIN = 'jweixin';
Promise.all([
readyAsync(JWEIXIN),
axios.get(`/api/weixinShareConfig`, { // 向 server 請求簽名
withCredentials: true,
params: {
url: window.location.href, // 簽名的參數需要 url,所以這邊帶回去
},
}),
Promise.resolve(window.location),
]).then(weixinShare);
script('//res.wx.qq.com/open/js/jweixin-1.4.0.js', JWEIXIN);
apiRouter.get('/weixinShareConfig', weixinShareConfigHandler);
//==================
const uuid = require('uuid/v4');
const makeWeixinSignature = require('../utils/makeWeixinSignature');
const findOrCreateReportMetadata = require('../utils/findOrCreateReportMetadata');
const appId = process.env.WEIXIN_APP_ID;
module.exports = async function weixinShareConfigHandler(req, res) {
try {
const nonceStr = uuid();
const timestamp = new Date().getTime();
const { url } = req.query;
const reportId = url.replace(/^.+\/reports\/(.+)\?token.+$/, '$1');
const [signature, { reportTitle, reportSentDate }] = await Promise.all([
makeWeixinSignature({ nonceStr, timestamp, url }), // 產生簽名
findOrCreateReportMetadata({
reportId,
apiHost: req.apiHost,
mobileToken: req.user[reportId].mobileToken,
}), // 另外準備一些需要用到的資訊回傳給前端(title、description)
]);
return res.json({
appId,
timestamp,
nonceStr,
signature,
reportTitle,
reportSentDate,
}); // 回給前端
} catch (error) {
// 有任何 error 都吐回給前端,讓前端開網頁 console 可以查看到到底產生簽名的過程怎麼失敗的
// miss appid etc...
return res.send({ error });
}
};
// 產生簽名
const { promisify } = require('util');
const axios = require('axios');
const crypto = require('crypto');
const client = require('../redisClient');
const getAsync = promisify(client.get).bind(client);
const appId = process.env.WEIXIN_APP_ID;
const secret = process.env.WEIXIN_APP_SECRET;
const NINETY_MINUTES = 5400;
const WEIXIN_ACCESS_TOKEN_ENDPOINT = 'https://api.weixin.qq.com/cgi-bin/token';
const WEIXIN_GET_TICKET_ENDPOINT =
'https://api.weixin.qq.com/cgi-bin/ticket/getticket';
async function fetchWexinAccessToken() {
// 用 appid secret 去拿 token
const { data } = await axios.get(WEIXIN_ACCESS_TOKEN_ENDPOINT, {
params: { grant_type: 'client_credential', appid: appId, secret },
});
if (data.errcode) {
throw data.errmsg;
}
return `${data.access_token}`;
}
async function findOrCreateWeixinAccessToken() {
// 確認 redis 有沒有 cache token
let weixinAccessToken = await getAsync('weixinAccessToken');
if (weixinAccessToken) {
return weixinAccessToken;
}
weixinAccessToken = await fetchWexinAccessToken(); // get token
// 存新拿到的 token 到 redis 做 cache
client.set('weixinAccessToken', weixinAccessToken, 'EX', NINETY_MINUTES);
return weixinAccessToken;
}
async function fetchWexinTicket() {
const weixinAccessToken = await findOrCreateWeixinAccessToken();
// 請求 ticket
const { data } = await axios.get(WEIXIN_GET_TICKET_ENDPOINT, {
params: { access_token: weixinAccessToken, type: 'jsapi' },
});
if (data.errcode) {
throw data.errmsg;
}
return `${data.ticket}`;
}
async function findOrCreateWexinTicket() {
// token 跟 ticket 都有時效性,官方文件說明為 2小時
// token 跟 ticket 都有 rate limitaion,所以做 cache 是必要的。
// 先確認 redis 有沒有存上次請求的 token
// 有的話就回傳
let weixinTicket = await getAsync('weixinTicket');
if (weixinTicket) {
return weixinTicket;
}
weixinTicket = await fetchWexinTicket(); // 產生 ticket
// 新請求的 ticket cache 在 redis 中。
client.set('weixinTicket', weixinTicket, 'EX', NINETY_MINUTES);
return weixinTicket;
}
module.exports = async function makeWeixinSignature({
nonceStr = 'AAAAAA',
timestamp,
url,
}) {
const weixinTicket = await findOrCreateWexinTicket();
// str 的排列順序是官方指定的,不能更動
const str = `jsapi_ticket=${weixinTicket}&noncestr=${nonceStr}×tamp=${timestamp}&url=${url}`;
// 用 sha1 產生簽名
const signature = crypto
.createHash('sha1')
.update(str)
.digest('hex');
return signature;
};
這隻是我開發該需求的要使用的資訊,其他人參考此文件時,不用理這隻檔案。
const { promisify } = require('util');
const axios = require('axios');
const client = require('../redisClient');
const getAsync = promisify(client.get).bind(client);
const TWO_WEEKS = 2 * 7 * 24 * 60 * 60;
const getReportConfigEndpint = ({ reportId, apiHost }) =>
`${apiHost}/api/v1/aaaaa/reports/${reportId}/config`;
async function fetchReportMetadata({ reportId, apiHost, mobileToken }) {
const { data } = await axios.get(
getReportConfigEndpint({ apiHost, reportId }),
{
headers: {
Authorization: mobileToken,
},
},
);
if (data.returnCode !== '50000') {
throw data.returnCode;
}
const { reportTitle, reportSentDate } = data;
return { reportTitle, reportSentDate };
}
module.exports = async function findOrCreateReportMetadata({
reportId,
apiHost,
mobileToken,
}) {
const metadataKey = `report-meta-${reportId}`;
const reportMetadataJSON = await getAsync(metadataKey);
if (reportMetadataJSON) {
return JSON.parse(reportMetadataJSON);
}
const reportMetadata = await fetchReportMetadata({
reportId,
apiHost,
mobileToken,
});
client.set(metadataKey, JSON.stringify(reportMetadata), 'EX', TWO_WEEKS);
return reportMetadata;
};
官方有說明,有舊的 API 要被淘汰。但其實都還可以。問題是新的 API 在某些裝置不 work。
所以兩套 API 我們都使用,並且測試起來 ios android desktop 都能 work。
2018 9月13號,有人的開發測試結果是
安卓适用于老接口,新接口不行
wx.onMenuShareAppMessage(eventConf)
wx.onMenuShareTimeline(eventConf)
iOS 适用于新接口,老接口不行
wx.updateAppMessageShareData(eventConf, success)
wx.updateTimelineShareData(eventConf, success)
请注意,原有的 wx.onMenuShareTimeline、wx.onMenuShareAppMessage、wx.onMenuShareQQ、wx.onMenuShareQZone 接口,即将废弃。请尽快迁移使用客户端6.7.2及JSSDK 1.4.0以上版本支持的 wx.updateAppMessageShareData、updateTimelineShareData 接口。
自定义“分享给朋友”及“分享到QQ”按钮的分享内容(1.4.0)updateAppMessageShareData
自定义“分享到朋友圈”及“分享到QQ空间”按钮的分享内容(1.4.0) updateTimelineShareData
获取“分享到朋友圈”按钮点击状态及自定义分享内容接口(即将废弃)onMenuShareTimeline
获取“分享给朋友”按钮点击状态及自定义分享内容接口(即将废弃)onMenuShareAppMessage
import { DateTime } from 'luxon';
function formatDate(reportSentDate) {
return DateTime.fromFormat(`${reportSentDate}`, 'yyyyLLddHHmm').toFormat(
'yyyy-LL-dd',
);
}
function getSearchParam(search, paramName) {
const params = search
.replace('?', '')
.split('&')
.reduce((accumulator, paramString) => {
const [key, value] = paramString.split('=');
return { ...accumulator, [key]: value };
}, {});
return params[paramName] || '';
}
export default function weixinShare([, { data }, location]) {
const { wx } = window;
if (!wx) {
return;
}
try {
if (data.error) {
throw data.error;
}
const {
appId,
timestamp,
nonceStr,
signature,
reportTitle,
reportSentDate,
} = data; // server 端組好傳回來給 client 的 data
wx.config({
appId,
timestamp,
nonceStr,
signature,
jsApiList: [
'onMenuShareTimeline',
'onMenuShareAppMessage',
'updateAppMessageShareData',
'updateTimelineShareData',
],
});
const { protocol, host, pathname, search } = location;
const reportId = pathname.replace(/\/reports\/(.+)$/, '$1');
const token = getSearchParam(search, 'token');
const shareConfig = {
title: reportTitle,
desc: formatDate(reportSentDate),
link: `${protocol}//${host}/reports/${reportId}?token=${token}`,
imgUrl: `${protocol}//${host}/api/thumbnail?reportId=${reportId}`,
};
wx.ready(() => {
wx.onMenuShareTimeline(shareConfig);
wx.onMenuShareAppMessage(shareConfig);
wx.updateTimelineShareData(shareConfig);
wx.updateAppMessageShareData(shareConfig);
});
} catch (errorCode) {
console.error(`getWeixinSignature fail: ${errorCode}`); // eslint-disable-line no-console
}
}
server 要放微信官方要求的驗證文件(txt檔)
設一個 router res 回驗證 key 就好了。
app.get('/MP_verify_by5xxxxxxxxK9UW.txt', (req, res) =>
res.send('by5xxxxxxxxK9UW'),
);
二次分享的意思是,我分享給A朋友,A朋友拿這份訊息再去分享給B朋友
微信為了避免濫轉發訊息,有做了阻擋二次分享的設計。它會在 url 中加入其他的參數做為它們的判斷。
這樣就會造成自訂分享的設定失效。
解決辦法就是 link
的參數每次都組新的,而不是直接抓 window.location.href
可以 google 微信 二次分享
多看看網路上怎麼處理這個問題
link: `${protocol}//${host}/reports/${reportId}?token=${token}`,
關於分享連結的圖片方面,有一些資訊與測試結果。
- 網路上流傳這個圖檔不能超過 32kb (但我測試到 50kb 都還可以,不過實務上我還是沒超過 32kb 為主)
- imgUrl 也要在白名單裡面(我跟我的 link 是同一個 do main、我沒測試過不同 domain 能不能 work)
- imgUrl 的 value 要帶有 protocal (http://... or https://)
- 檔案格式 png、jpg 可以、gif 不行。但 server 回傳 gif 時,header 設 Content-Type png、jpeg 的話,能 work (當然圖片就不是 gif 這樣會動的了)
這邊的程式範例是,先去找客戶的 logo 找到的話,就轉成 png (sharp library)回傳。 若沒有 logo 或者有任何問題的話,就回傳一張預設的圖片(static file)回去。
const path = require('path');
const httpProxy = require('http-proxy');
const sharp = require('sharp');
const defaultThumbnailPath = path.resolve(
process.cwd(),
'assets',
'images',
'icon.png',
);
const errorHandler = (err, req, res) => res.sendFile(defaultThumbnailPath);
const proxy = httpProxy.createProxyServer({});
proxy
.on('proxyRes', (proxyRes, req, res) => {
try {
if (proxyRes.statusCode !== 200) {
throw new Error(`Status code: ${proxyRes.statusCode}`);
}
const transformer = sharp()
.png()
.resize({
height: 150,
width: 150,
fit: 'contain',
background: { r: 0, g: 0, b: 0, alpha: 0 },
});
proxyRes.pipe(transformer).pipe(res);
} catch (error) {
errorHandler(error, req, res);
}
})
.on('error', errorHandler);
module.exports = function thumbnailHandler(req, res) {
const { reportId } = req.query;
req.url = `/api/v1/xxxx/reports/${reportId}/logo`;
proxy.web(req, res, {
target: req.apiHost,
selfHandleResponse: true,
});
};
desktop 的 browser 的核心是 IE...
所以程式碼要確保 IE11 能支援 (caniuse 確認一下)
除錯方面,因為沒法 console.log ,所以最快的話把懷疑的地方程式 try catch 起來,然後 alert 出錯誤訊息。這方法蠻快能知道錯誤的。