前言
近期把自用的微信公众号微信分享模块从 php 修改为 nodejs 的版本,虽然这是一个很小的功能,但仍然选择了 egg 框架,也算是为未来继续开发公众号,做点扩展的准备。
本文章仅为项目介绍,不涉及 egg 的原理,请不要问我为啥不直接用koa。
一、 egg本地环境搭建
1. egg简介
koa框架:基于 Node.js 平台的新的 web 框架,由 Express 幕后的原班人马打造,它与Express使用同一套http基础库。最新的koa2,是基于ES7开发的,完美支持了promise及async。
egg框架:egg2.x 以Koa2.x 作为其基础框架,兼容Koa 2.x 的中间件,最低支持 Node.js 8
通过一张图来描述 egg2.x :
更多的这里就不说了,有兴趣的童鞋请看 eggjs.org/zh-cn/intro…
2. 安装
npm i egg --save
npm i egg-bin --save-dev
复制代码
3. 配置启动scripts
{
"name": "egg-example",
"scripts": {
"dev": "egg-bin dev"
}
}
复制代码
二、本地目录结构
创建本地目录如下:
node
├── package.json
├── app
│ ├── extend // 扩展
│ | ├── helper.js
│ ├── service // 服务
| ├── controller // 控制器
| ├── public // 静态资源路径
│ ├── middleware // 中间件
│ └── router.js // 路由
└── config
├── config.default.js // 配置
└── plugin.js // 插件
复制代码
以上仅是本案例中的结构,完整结构,参考官方:eggjs.org/zh-cn/basic…
三 、配置域名及nginx反向代理
egg 服务端默认采用的是 7001 端口,因为我们将二级域名: share.xxx.com 解析到 7001 上,实现域名直接访问 egg 接口。
解析二级域名
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 端口了!
四、微信分享功能逻辑
微信分享其实是一个比较简单的功能,难点也就是了解微信公众号的token如何转换成签名,这里简单画了一个图:
权限,指的是安全域名,需要到公众号后台进行设置(如下图),并填写你的分享链接域名。
填写域名时,需要将公众号给的验证文件放到根目录,在你点击保存时,公众号服务器将会去请求本文件,验证该域名是否有效。
具体的获取 token 及 ticket 接口及代码,会在下面详细讲解。
五、代码开发
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}×tamp=${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
};
复制代码
六、本地调试
这里推荐使用 vs code 来调试服务端代码,需要简单进行配置,参考 egg 官方文档: eggjs.org/zh-cn/core/…
七、部署项目
在正式部署前,开发者根据需要自行配置中间件、模板、插件、启动配置项等。
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 来做管理:
八、性能监控
我们使用 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
复制代码
至此,整篇文章全部结束,这里的代码部分也有参照一些别的教程,所以总体来说,并不是难度多大的东西,大家互相学习互相进步吧。