centos + nodejs + egg2.x 開發微信分享功能

前言

近期把自用的微信公衆號微信分享模塊從 php 修改成 nodejs 的版本,雖然這是一個很小的功能,但仍然選擇了 egg 框架,也算是爲將來繼續開發公衆號,作點擴展的準備。javascript

本文章僅爲項目介紹,不涉及 egg 的原理,請不要問我爲啥不直接用koa。php

1、 egg本地環境搭建

1. egg簡介

koa框架:基於 Node.js 平臺的新的 web 框架,由 Express 幕後的原班人馬打造,它與Express使用同一套http基礎庫。最新的koa2,是基於ES7開發的,完美支持了promise及async。html

egg框架:egg2.x 以Koa2.x 做爲其基礎框架,兼容Koa 2.x 的中間件,最低支持 Node.js 8java

經過一張圖來描述 egg2.x :node

egg2.x

更多的這裏就不說了,有興趣的童鞋請看 eggjs.org/zh-cn/intro…jquery

2. 安裝

npm i egg --save
npm i egg-bin --save-dev
複製代碼

3. 配置啓動scripts

{
  "name": "egg-example",
  "scripts": {
    "dev": "egg-bin dev"
  }
}
複製代碼

2、本地目錄結構

建立本地目錄以下:nginx

node
    ├── package.json
    ├── app
    │   ├── extend                                        // 擴展
    │   |   ├── helper.js
    │   ├── service                                       // 服務
    |   ├── controller                                    // 控制器
    |   ├── public                                        // 靜態資源路徑
    │   ├── middleware                                    // 中間件
    │   └── router.js                                    // 路由
    └── config
        ├── config.default.js                            // 配置
        └── plugin.js                                    // 插件
複製代碼

以上僅是本案例中的結構,完整結構,參考官方:eggjs.org/zh-cn/basic…git

三 、配置域名及nginx反向代理

egg 服務端默認採用的是 7001 端口,由於咱們將二級域名: share.xxx.com 解析到 7001 上,實現域名直接訪問 egg 接口。github

二級域名

解析二級域名web

server {
    listen 80;
    server_name  share.xxx.com;
    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;
        proxy_pass      http://127.0.0.1:7001;
    }
}
複製代碼

nginx 反向代理

服務器從新啓動 nginx 後,便可經過 share.xxx.com 訪問到咱們的 7001 端口了!

4、微信分享功能邏輯

微信分享實際上是一個比較簡單的功能,難點也就是了解微信公衆號的token如何轉換成簽名,這裏簡單畫了一個圖:

分享

權限,指的是安全域名,須要到公衆號後臺進行設置(以下圖),並填寫你的分享連接域名。

公衆號設置

填寫域名時,須要將公衆號給的驗證文件放到根目錄,在你點擊保存時,公衆號服務器將會去請求本文件,驗證該域名是否有效。

js 安全域名

具體的獲取 token 及 ticket 接口及代碼,會在下面詳細講解。

5、代碼開發

1. 驗證接口

安全域名,須要在根目錄放一個文本文件,公衆號會嘗試打開該文件,並驗證其中的key。因爲 egg 沒法直接訪問根目錄文件,所以使用路由來實現驗證接口

// 路由
router.get('/MP_verify_ysZJMVdQxMoU8v35.txt', controller.check.index);

// 驗證
class CheckController extends Controller {
    async index() {
        let cache = await this.ctx.helper.readFile(path.join(this.config.baseDir, 'app/MP_verify_ysZJMVdQxMoU8v35.txt'));
        this.ctx.body = cache;
    }
}
複製代碼

2. getTicket接口

egg 針對 csrf 安全作了如下幾種處理:

  • Synchronizer Tokens
  • Double Cookie Defense
  • Custom Header

在 CSRF 默認配置下,token 會被設置在 Cookie 中,在 AJAX 請求的時候,能夠從 Cookie 中取到 token,放置到 query、body 或者 header 中發送給服務端。

以 jquery 爲例, 在 beforeSend 中,增長 header 項 x-csrf-token:

// 請求籤名
var token = getCookie('csrfToken');
if(token){
    var url = location.href.split('#')[0];
    var host = location.origin;
    $.ajax({
        url: host + "/getTicket",
        type: 'post',
        data: {
            url: encodeURIComponent(url)
        },
        beforeSend: function (request) {
            request.setRequestHeader("x-csrf-token", token);
        },
        success: function (res) {
            if(res.code === 0){
                wx.config({
                    debug: true,
                    appId: res.data.appId,
                    timestamp: res.data.timestamp,
                    nonceStr: res.data.nonceStr,
                    signature: res.data.signature,
                    jsApiList: [
                        'updateTimelineShareData',
                        'updateAppMessageShareData'
                    ]
                }); 
                wx.ready(function () {
                    var shareData = {
                        title: '個人分享',
                        desc: '個人文字介紹,詳細的',
                        link: host,
                        imgUrl: host + "/public/images/icon.jpg"
                    };
                    wx.updateTimelineShareData(shareData);
                    wx.updateAppMessageShareData(shareData);
                });
                wx.error(function (res) {
                    console.log(res.errMsg);
                });
            }else{
                console.log(res);
            }
            
        }
    });
}else{
    alert('invalid csrf token');
}
複製代碼

3. 微信獲取token

async getToken(ctx, config){
    let timestamp = new Date().valueOf();
    let cache = await this.ctx.service.fileService.read('token');
    let result = cache;

    // 緩存失效
    if (!cache || cache.expires_in < timestamp || cache.app_id !== config.wx.appId) {
        result = await ctx.curl(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${config.wx.appId}&secret=${config.wx.secret}`, {
            dataType: 'json'
        });
        if (this.ctx.helper.checkResponse(result)) {
            result = {
                access_token: result.data.access_token,
                expires_in: timestamp + result.data.expires_in * 1000,
                app_id: config.wx.appId
            };
            this.ctx.service.fileService.write('token', result);               
        } else {
            this.ctx.service.fileService.write('token', '');
            this.ctx.logger.error(new Error(`${timestamp}--wxconfig: ${JSON.stringify(config.wx)}--tokenResult: ${JSON.stringify(result)}`));
            result = null;
        }
    }

    return result;
}
複製代碼

4. 微信獲取ticket

async getTicket(ctx, config, res){
    let timestamp = new Date().valueOf();
    let cache = await this.ctx.service.fileService.read('ticket');
    let result = cache;

    // 緩存失效
    if (!cache || cache.expires_in < timestamp || cache.app_id !== config.wx.appId) {
        result = await ctx.curl(`https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${res.access_token}&type=jsapi`, {
            dataType: 'json'
        });
        if (this.ctx.helper.checkResponse(result)) {
            result = {
                ticket: result.data.ticket,
                expires_in: timestamp + result.data.expires_in * 1000,
                app_id: config.wx.appId
            };
            this.ctx.service.fileService.write('ticket', result); 
        } else {
            this.ctx.service.fileService.write('ticket', '');
            this.ctx.logger.error(new Error(`${timestamp}--wxconfig: ${JSON.stringify(config.wx)}--jsapiResult: ${JSON.stringify(jsapiResult)}`));
            result = null;
        }
    }

    return result;
}
複製代碼

5. 緩存策略

oken 及 ticket 的有效期均爲 7200 秒,且有必定的請求頻率限制,所以推薦在服務器本地緩存這兩個串,開發者能夠自行選擇存儲在本地、數據庫、全局中。

以本項目爲例,這裏偷懶了下,直接存在本地txt中,別問我爲啥用文件存儲 ^_^,我不會告訴你是懶的裝數據庫。

async read(type) {
    let src = type === 'token' ? this.tokenFile : this.ticketFile;
    let data = await this.ctx.helper.readFile(src);
    data = JSON.parse(data);
    return data;
}

async write(type, data) {
    let src = type === 'token' ? this.tokenFile : this.ticketFile;
    await this.ctx.helper.writeFile(src, JSON.stringify(data));
}
複製代碼

6. 生成簽名

簽名生成規則以下:

參與簽名的字段包括noncestr(隨機字符串), 有效的jsapi_ticket,timestamp(時間戳),url(當前網頁的URL,不包含#及其後面部分) 。對全部待簽名參數按照字段名的ASCII 碼從小到大排序(字典序)後,使用URL拼接成字符串string1,這裏須要注意的是全部參數名均爲小寫字符。對string1做sha1加密,字段名和字段值都採用原始值,不進行URL 轉義。

const uuidv1 = require('uuid/v1');
const noncestr = uuidv1();
const timestamp = Math.round(new Date().valueOf() / 1000);
const string1 = `jsapi_ticket=${jsapi_ticket}&noncestr=${noncestr}&timestamp=${timestamp}&url=${url}`;
const crypto = require('crypto');
const hash = crypto.createHash('sha1');
hash.update(string1);
const signature = hash.digest('hex');
return {
    nonceStr: noncestr,
    timestamp,
    signature,
    appId: appId,
    jsapi_ticket,
    url,
    string1
};
複製代碼

6、本地調試

這裏推薦使用 vs code 來調試服務端代碼,須要簡單進行配置,參考 egg 官方文檔: eggjs.org/zh-cn/core/…

7、部署項目

在正式部署前,開發者根據須要自行配置中間件、模板、插件、啓動配置項等。

1. 啓動

egg 提供了 egg-scripts 來支持線上環境的啓停。

npm i egg-scripts --save
複製代碼

添加 npm scripts

{
  "scripts": {
    "start": "egg-scripts start --daemon --title=egg-server-showcase",
    "stop": "egg-scripts stop"
  }
}
複製代碼

egg 默認會開啓 cpu 數量的進程,性能方面仍是不錯的。

2. 中止

egg-scripts stop [--title=egg-server]
複製代碼

3. 保活

框架內置了 egg-cluster 來啓動 Master 進程,所以不須要作額外配置便可。若有特殊需求,框架也支持使用 pm2 來作管理:

8、性能監控

咱們使用 egg 官方推薦的 Node.js 性能平臺(alinode)

1. 安裝 Runtime

AliNode Runtime 能夠直接替換掉 Node.js Runtime

// 安裝版本管理工具 tnvm,安裝過程出錯參考:https://github.com/aliyun-node/tnvm
wget -O- https://raw.githubusercontent.com/aliyun-node/tnvm/master/install.sh | bash
source ~/.bashrc

// https://help.aliyun.com/knowledge_detail/60811.html,這裏有node版本對應的alinode版本
tnvm install alinode-v4.2.2 # 安裝須要的版本
tnvm use alinode-v4.2.2 # 使用須要的版本

// 因爲egg官方封裝了egg-alinode 來快速接入,無需安裝 agenthub
複製代碼

2. 安裝配置 egg-alinode

npm i egg-alinode --save
複製代碼

開啓插件:

// config/plugin.js
exports.alinode = {
  enable: true,
  package: 'egg-alinode',
};
複製代碼

配置:

// config/config.default.js
exports.alinode = {
  // 從 `Node.js 性能平臺` 獲取對應的接入參數
  appid: '<YOUR_APPID>',
  secret: '<YOUR_SECRET>',
};
複製代碼

3. 啓動應用

阿里官方使用的開啓方式,是在命令行加入如下代碼:

ENABLE_NODE_LOG=YES node demo.js
複製代碼

但在egg中,有本身的啓動方式,仍然是:

npm start
複製代碼

官方解釋是:

解釋

成功啓動後,訪問幾回你的接口,稍等一會,便可在控制檯看到數據了。

成功

控制檯地址: node.console.aliyun.com

常見錯誤

1. 安裝 tnvm 遇到報錯

懷疑是 1.7.1 git 版本太老形成,升級 git...

// 下載 git 2.21.1 版本
wget https://github.com/git/git/archive/v2.21.1.tar.gz
tar -zxvf v2.21.1.tar.gz

// 安裝依賴
yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel gcc perl-ExtUtils-MakeMaker

// 刪除老版本git
yum remove git

// 進入解壓後的文件夾
cd git-2.21.1

// 編譯
make prefix=/usr/local/git all
make prefix=/usr/local/git install

// 環境變量
echo "export PATH=$PATH:/usr/local/git/bin" >> /etc/profile

// 讓環境變量生效
source /etc/profile
複製代碼

2. undefined reference to `libiconv'

cd /usr/local/src

// 下載新版的libiconv
wget http://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.15.tar.gz
tar -zxvf libiconv-1.15.tar.gz

// 安裝
cd libiconv-1.15
./configure --prefix=/usr/local/libiconv && make && make install

// 建立連接
ln -s /usr/local/lib/libiconv.so /usr/lib
ln -s /usr/local/lib/libiconv.so.2 /usr/lib
複製代碼

而後重複上面安裝git過程的12行以後便可

3. git clone 報錯 SSL connect error

GitHub 前幾天開始不支持老的加密方式,升級到 CentOS 6.8 或者單獨升級SSH,如下命令2選1

yum update -y
yum update openssh
複製代碼

至此,整篇文章所有結束,這裏的代碼部分也有參照一些別的教程,因此整體來講,並非難度多大的東西,你們互相學習互相進步吧。

相關文章
相關標籤/搜索