h5网页传播非常广泛,分享是必不可少的,但是分享需要涉及后端开发,很多前端开发都很难动手,实际上非常简单,下面我借助nodejs进行开发。
准备工作:
一、 申请公众号
二、 公众号设置,有三个地方:
image.png
(1) 配置js安全域名,使前端有权限访问。
在公众号设置中的,功能设置,设置JS接口安全域名,需要验证文件文MP_verify_ad0JFhhF79gbQaxZ.txt,下载放进根目录即可。
(2)配置ip白名单,使服务器可以访问。
在基本配置中找到白名单配置,如图:
image.png
(3) 配置调用接口权限,能开启的都开就行。
基本的都配置好了,接下来开始写服务端代码,很简单,记得安装依赖,我是用cnpm:
cnpm install request sha1
nodejs的代码:
const request = require('request');
const sha1 = require('sha1');
// 这些配置记得替换你的appid和secret
let config = {
appID: "wxafasfa123131",// 微信公众号appID
appSecret: "fasdfasfa", //微信公众号里有appSecret
getAccessTokenUrl: 'https://api.weixin.qq.com/cgi-bin/token',
getJsapiTicketUrl: 'https://api.weixin.qq.com/cgi-bin/ticket/getticket'
};
// 为了应对缓存压力,不要每次刷新token,访问量高会带来很大问题
// 因为获取access_token的接口,一天最多调用2000次,每次有效期是两个小时
let CACHE = {
ticket: '',
ticketTimeout: 0,
ticketTime: 0,
accessToken: '',
accessTokenTimeout: 0,
accessTokenTime: 0
};
/**
* 微信分享
*/
class WxShare {
constructor() {
this.refreshAccessToken().then(res => {
this.refreshJsapiTicket(res);
}).catch(e => {
console.log(e);
});
}
/**
* 刷新access_token
*/
refreshAccessToken() {
return new Promise((resolve, reject) => {
const tokenUrl = `${config.getAccessTokenUrl}?grant_type=client_credential&appid=${config.appID}&secret=${config.appSecret}`;
request(tokenUrl, (error, response, body) => {
console.error('refreshAccessToken', body);
if (typeof body === 'string') {
try {
body = JSON.parse(body);
} catch (e) {
body = {
errcode: '-1000',
body
};
}
}
if (body && (!body.errcode || body.errcode == 0)) {
CACHE.accessToken = body.access_token;
CACHE.accessTokenTimeout = body.expires_in * 500;
CACHE.accessTokenTime = new Date();
resolve(CACHE.accessToken);
} else if (body) {
reject(body.errmsg);
} else {
reject('未知异常');
}
});
});
}
/**
* 刷新ticket
* @param {*} access_token
* @param {*} callback
*/
refreshJsapiTicket(access_token) { // Jsapi_ticket
return new Promise((resolve, reject) => {
let ticketUrl = `${config.getJsapiTicketUrl}?access_token=${access_token}&type=jsapi`;
request(ticketUrl, function (err, response, content) {
content = JSON.parse(content);
console.error('refreshJsapiTicket', content);
if (content && (content.errcode == 0 || !content.errcode)) {
CACHE.ticket = content.ticket;
CACHE.ticketTimeout = content.expires_in * 500;
CACHE.accessTokenTime = new Date();
resolve(CACHE.ticket); // ticket
} else if (content) {
reject(content.errmsg);
} else {
reject('未知异常');
}
})
});
};
async getShareConfig(url) { // 获取access_token
let access_token = CACHE.accessToken;
let ticket = CACHE.ticket;
if (!access_token || (new Date() - CACHE.accessTokenTime > CACHE.accessTokenTimeout)) {
access_token = await this.refreshAccessToken();
ticket = await this.refreshJsapiTicket(access_token);
}
let nonceStr = this.createNonceStr();
let timestamp = this.createTimestamp()
let signature = this.createSign({
jsapi_ticket: ticket,
nonceStr, timestamp, url
});
return {
appID: config.appID,
access_token,
ticket,
timestamp,
nonceStr,
signature
};
};
/**
* 随机字符串
*/
createNonceStr() {
return Math.random().toString(36).substr(2, 15);
};
/**
* 时间戳
*/
createTimestamp() {
return parseInt(new Date().getTime() / 1000).toString();
};
/**
* 拼接字符串
* @param {*} args
*/
rawString(args) {
var keys = Object.keys(args);
keys = keys.sort()
var newArgs = {};
keys.forEach(function (key) {
newArgs[key.toLowerCase()] = args[key];
});
var string = '';
for (var k in newArgs) {
string += '&' + k + '=' + newArgs[k];
}
string = string.substr(1);
return string;
};
/**
* 新的
* @param {*} config
*/
createSign(config) {
let _this = this;
var ret = {
jsapi_ticket: config.jsapi_ticket,
nonceStr: config.nonceStr,
timestamp: config.timestamp,
url: config.url
};
let url = ret.url;
let index = url.indexOf('#');
let res = Object.assign({}, ret, {
url: index > -1 ? url.substr(0, index) : url
});
var string = _this.rawString(res);
var shaObjs = sha1(string);
return shaObjs;
}
}
module.exports = WxShare;
使用方式:
// 理论上你只需要实例化一次
const wxShare = new WxShare();
// 获取分享的配置
// 返回的数据 {appID, access_token, ticket, timestamp, nonceStr, signature}
// 理论上你只需要返回appID, timestamp, nonceStr, signature
// 为了好测试 就直接返回所有的方便验证
const res = await wxShare.getShareConfig(url);
// 假如你是使用koa,你可以这样做路由
router.post('/api/get-share-config', async (ctx, next) => {
try {
const { url } = ctx.request.body;
const config = await wxShare.getShareConfig(url);
ctx.body = {data: config, code: 200}
} catch (err) {
ctx.body = {
errorMsg: JSON.stringify(err),
code: 400
};
}
});
后端已经到此结束了,你应该可以调试出你的加密数据了,接口可以返回了。
当前还需要前端代码,我这边一并提供了吧,节省大家时间:
Utils.js 文件
export const FUN_APP_SIGN = "SceApp";
export const WEIXIN_APP_SIGN = 'microMessenger';
export const DINGDING_APP_SIGN = 'dingtalk';
export default class Utils {
/**
*
* @param {string} str 处理的字符串
* @param {boolean} isStart 是否从首字符开始
* @returns {string} 下划线转化为驼峰命名规格 an_apple 返回anApple/anApple
*/
static transToUppercase(str, isStart = false) {
if (isStart) {
let $0 = str.charAt(0);
if ((/[a-z]/i).test($0)) {
str = str.replace($0, $0.toUpperCase());
}
}
return str.replace(/_(\w)/g, function ($0, $1) {
if ((/[a-z]/i).test($1)) {
return $1.toUpperCase();
} else {
return $0;
}
});
}
/**
* 检测客户端机型和环境
* @returns {{isFunApp: boolean, isWx: boolean, isIos: boolean, isAndroid: boolean}}
*/
static getClient() {
let result, userAgent;
result = {
isFunApp: false,
isWx: false,
isIos: false,
isAndroid: false,
isDingDing: false
};
userAgent = window.navigator.userAgent.toLowerCase();
// 是否为公司app中
if (userAgent.indexOf(FUN_APP_SIGN.toLocaleLowerCase()) > -1) {
result.isFunApp = true;
} else {
// 判断是否为微信
if (userAgent.indexOf(WEIXIN_APP_SIGN.toLocaleLowerCase()) > -1) {
result.isWx = true;
}
// 判断是否为微信
if (userAgent.indexOf(DINGDING_APP_SIGN.toLocaleLowerCase()) > -1) {
result.isDingDing = true;
}
}
// 判断系统
if (userAgent.indexOf('android') > -1 || userAgent.indexOf('linux') > -1) {
//安卓手机
result.isAndroid = true;
} else if (userAgent.indexOf('iphone') > -1) {
//苹果手机
result.isIos = true;
} else if (userAgent.indexOf('windows phone') > -1) {
//winphone手机
result.isWindowsPhone = true;
}
return result;
}
static transToUnderline(str) {
return str.replace(/([A-Z])/g, "_$1").toLowerCase();
}
static getStyle(t) {
return t.currentStyle ? t.currentStyle : getComputedStyle(t);
}
/**
*
* @param str 去除str的首位空格
* @returns {*}
*/
static trim(str) {
return str.replace(/^\s+|\s+$/g, '');
}
}
wx-share文件
import Utils from "../../common/utils";
/**
* 微信分享 使用方法:
* let wxShare = WxShare.getInstance();
* wxShare.share({title, desc, link, imgUrl});
*
* 注:这是一个单例 不能够直接实例,可能出现异常
* 如果出现任何异常查看微信官方文档,比对代码
* https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115
*/
export class WxShare {
static sharePlugin = null;
/**
*
* @param shareData
* @returns {WxShare}
*/
static getInstance() {
let sharePlugin = WxShare.sharePlugin;
if (!sharePlugin) {
sharePlugin = WxShare.sharePlugin = new WxShare();
}
return sharePlugin;
}
// 标记位置,防止位置失效
location = null;
// 加载微信脚本的url
jsUri = `${window.location.protocol}//res.wx.qq.com/open/js/jweixin-1.6.0.js`;
// 加密数据 ios中不需要重新请求 android需要重新请求
wxEncryptData = null;
// 分享的内容
shareData = null;
// 是否可以分享了
isSharing = false;
isReady = false;
isNeedShare = false;
/**
* 分享的构造函数
* @param shareData {title, desc, link, imgUrl}
*/
constructor() {
this.init().catch();
}
/**
* 初始化异常 主要调用一次js
* 设置权限,然后触发ready,标记isReady
* @returns {Promise<boolean>}
*/
async init() {
if (!Utils.getClient().isWx) {
return false;
}
let wx = window.wx;
if (!wx) {
wx = await this.loadJs();
wx.ready(this.onReady.bind(this));
await this.setWxAccess();
}
}
onReady() {
this.isReady = true;
if (this.isNeedShare) {
this.isNeedShare = false;
this.share(this.shareData).catch();
}
}
/**
* 分享的时机非常关键,主要看目前的isReady是否为true
*
* false:标记为需要分享,isNeedShare为true
* ready根据isNeedShare标记自动发布分享
*
* true:有可能页面地址通过history改变
* 此时必须重新获取权限,否则调用api会加密失败
*
* 略微差异:
* ios中history的改变不需要重新请求后台的加密签名,android需要
* 两者都需要重新进行签名配置,因为url改变了
*
* @param title
* @param desc
* @param link
* @param imgUrl
* @returns {Promise<void>}
*/
async share({ title, desc, link, imgUrl }) {
let shareData = WxShare.ctrlShareContent({ title, desc, link, imgUrl });
this.shareData = shareData;
if (this.isSharing) {
return;
}
if (this.isReady) {
// url地址改变 重新配置签名
if (window.location.href !== this.location) {
this.isSharing = true;
// 安卓需要向后台请求
if (Utils.getClient().isAndroid) {
this.wxEncryptData = null;
}
await this.setWxAccess();
this.isSharing = false;
}
this.shareFriend(this.shareData);
this.shareCircle(this.shareData);
} else {
this.isNeedShare = true;
}
}
static ctrlShareContent(config) {
// config.title = config.title || "一个品控很严的公寓品牌";
// config.link = config.link || window.location.href;
// config.desc = config.desc || "我猜你会喜欢这里的设计";
// config.imgUrl = config.imgUrl || "http://sce-funlive-01.oss-cn-shanghai.aliyuncs.com/hotel/1545961988406040e3fd9-c766-4fb1-a2f8-ce4ad66e19b7?x-oss-process=image/resize,w_256"
return config;
}
// config信息验证失败会执行error函数,如签名过期导致验证失败,
// 具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,
// 对于SPA可以在这里更新签名。
onError(res) {
console.log(res);
}
/**
* 获取后端签名加密
* @returns {Promise<*>}
*/
async getWxEncrypt() {
this.location = window.location.href;
try {
let res = await fetch('/api/get-share-config', {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=utf-8' },
body: JSON.stringify({
url: this.location
})
}).then(response => response.json())
.then(data => {
return data;
});
console.log(res);
return res;
} catch (e) { }
}
/**
* 设置调用js权限
*/
async setWxAccess() {
let data = this.wxEncryptData;
if (!data) {
data = this.wxEncryptData = await this.getWxEncrypt();
}
// 配置config
window.wx.config({
debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: data.appID, // 必填,公众号的唯一标识
timestamp: data.timestamp, // 必填,生成签名的时间戳
nonceStr: data.nonceStr, // 必填,生成签名的随机串
signature: data.signature,// 必填,签名
jsApiList: ["updateAppMessageShareData", "updateTimelineShareData", "onMenuShareTimeline", "onMenuShareAppMessage"] // 必填,需要使用的JS接口列表
// jsApiList: ["updateAppMessageShareData", "updateTimelineShareData"] // 必填,需要使用的JS接口列表
});
}
// 自定义“分享给朋友”及“分享到QQ”按钮的分享内容(1.4.0)
shareFriend({ title, desc, link, imgUrl }) {
let wx = window.wx;
if (wx.onMenuShareAppMessage) {
wx.onMenuShareAppMessage({
title, desc, link, imgUrl,
success: (res) => { },
fail: (res) => this.onError(res)
});
} else {
wx.updateAppMessageShareData({
title, desc, link, imgUrl,
success: (res) => { },
fail: (res) => this.onError(res)
});
}
}
// 自定义“分享到朋友圈”及“分享到QQ空间”按钮的分享内容(1.4.0)
shareCircle({ title, link, imgUrl }) {
let wx = window.wx;
if (wx.updateTimelineShareData) {
wx.onMenuShareTimeline({
title, link, imgUrl,
success: (res) => { },
fail: (res) => this.onError(res)
});
} else {
wx.updateTimelineShareData({
title, link, imgUrl,
success: (res) => { },
fail: (res) => this.onError(res)
});
}
}
loadJs() {
return new Promise((resolve, reject) => {
if (window.wx) {
return resolve(window.wx);
}
let script = document.createElement("script");
script.type = "text/javascript";
script.src = this.jsUri;
window.document.getElementsByTagName('head')[0].appendChild(script);
script.onload = () => {
resolve(window.wx);
};
script.onerror = reject;
});
}
}
// 使用方式:
WxShare.getInstance().share({
title: 'pccold',
desc: '2020 幸福新声',
link: `${window.location.protocol}//${window.location.host}/`,
imgUrl: 'http://sce-funworld-public.oss-cn-shanghai.aliyuncs.com/66/assets/summary/bg2.png'
});
就讲到这里,有问题给我留言,谢谢大家支持!