Skip to content

Latest commit

 

History

History
393 lines (341 loc) · 12.7 KB

2019-01-30:微信分享接口API.md

File metadata and controls

393 lines (341 loc) · 12.7 KB

微信分享解口API

開發時的版本為 jweixin-1.4.0.js

一些開發紀錄(假設白名單之類的設定好了),程式碼部分要怎麼處理

執行步驟

  1. User 開啟網頁
  2. client 端向 server 請求 signature
  3. server 收到請求後,請求 weixin 提供 token、ticket 來產生 signature 回傳
  4. client 端收到 signature 後設定 weixin API

2. client 端向 server 請求 signature

用來組簽名的 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);

3. server 收到請求後,請求 weixin 提供 token、ticket 來產生 signature 回傳

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}&timestamp=${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;
};

4. client 端收到 signature 後設定 weixin API

官方有說明,有舊的 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}`,

imgUrl

關於分享連結的圖片方面,有一些資訊與測試結果。

  • 網路上流傳這個圖檔不能超過 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 微信桌機版

desktop 的 browser 的核心是 IE...
所以程式碼要確保 IE11 能支援 (caniuse 確認一下)
除錯方面,因為沒法 console.log ,所以最快的話把懷疑的地方程式 try catch 起來,然後 alert 出錯誤訊息。這方法蠻快能知道錯誤的。