nodejs开发微信h5分享

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'
});

就讲到这里,有问题给我留言,谢谢大家支持!

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。