preface

Recently, I changed the wechat sharing module of my own wechat public account from PHP to nodeJS version. Although this is a small function, I still choose egg framework, which can also be considered as preparation for further development of the public account in the future.

This article is just an introduction to the project and does not cover the principles of Egg. Please do not ask me why I do not use KOA directly.

First, build local environment of Egg

1. The introduction of an egg

Koa framework: A new Web framework based on the Node.js platform, built by the same people behind Express, which uses the same HTTP infrastructure as Express. The latest version, KOA2, is based on ES7 and has perfect support for Promise and Async.

Egg framework: Egg2. x uses KOA2.x as its base framework, is compatible with Koa2.x middleware, and supports Node.js 8 at the minimum

Egg2.x is described by a graph:

More here will not say, interested in children’s shoes please see eggjs.org/zh-cn/intro…

2. Install

npm i egg --save
npm i egg-bin --save-dev
Copy the code

3. Configure startup scripts

{
  "name": "egg-example"."scripts": {
    "dev": "egg-bin dev"}}Copy the code

2. Local directory structure

Create a local directory as follows:

Node ├ ─ ─ package. Json ├ ─ ─ app │ ├ ─ ─ the extend / / extension │ | ├ ─ ─ helper. Js │ ├ ─ ─ service / / service | ├ ─ ─ the controller / / controller | ├ ─ ─ │ ├─ ├─ ├─ garbage ├─ config ├─ config.default.js // The plug-inCopy the code

The above is only the structure of this case, the complete structure, refer to the official: eggjs.org/zh-cn/basic…

Configure the domain name and nginx reverse proxy

The egg server uses port 7001 by default, because we resolve the secondary domain name share.xxx.com to 7001, allowing the domain name to access the Egg interface directly.

Resolving secondary Domain name

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;}}Copy the code

Nginx reverse proxy

After the server restarts nginx, you can access our port 7001 via share.xxx.com!

Fourth, wechat sharing function logic

Wechat sharing is actually a relatively simple function. The difficulty is to understand how the token of wechat public account is converted into a signature. Here is a simple picture:

Permission refers to the secure domain name, which needs to be set up in the background of the public account (as shown below), and fill in your share link domain name.

When filling in the domain name, you need to put the verification file given by the public account into the root directory. When you click save, the public account server will request this file to verify whether the domain name is valid.

The interfaces and codes for obtaining tokens and tickets are described in details in the following sections.

5. Code development

1. Verify the interface

Secure domain name, need to put a text file in the root directory, the public account will try to open the file, and verify the key in it. Because egg does not have direct access to the root directory file, the validation interface is implemented using routing

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

/ / verification
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; }}Copy the code

2. GetTicket interface

Egg does the following for CSRF security:

  • Synchronizer Tokens
  • Double Cookie Defense
  • Custom Header

In the default CSRF configuration, the token is set in a Cookie. In AJAX requests, the token can be fetched from the Cookie and sent to the server in the query, body, or header.

In beforeSend, add the header x-csrf-token:

// Request a signature
var token = getCookie('csrfToken');
if(token){
    var url = location.href.split(The '#') [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: 'My Share'.desc: 'My text introduction, detailed'.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');
}
Copy the code

3. Wechat obtains tokens

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

    // The cache is invalid
    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: The ${JSON.stringify(config.wx)}--tokenResult: The ${JSON.stringify(result)}`));
            result = null; }}return result;
}
Copy the code

4. Obtain ticket by wechat

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

    // The cache is invalid
    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: The ${JSON.stringify(config.wx)}--jsapiResult: The ${JSON.stringify(jsapiResult)}`));
            result = null; }}return result;
}
Copy the code

5. Cache policy

The validity periods of oken and ticket are 7200 seconds and the request frequency is limited. Therefore, you are advised to cache these two strings locally on the server. Developers can store these two strings locally, in the database, or globally.

Take this project as an example, here is lazy, save directly in local TXT, don’t ask me why use file store ^_^, I won’t tell you is lazy install database.

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));
}
Copy the code

6. Generate a signature

The signature generation rules are as follows:

Fields participating in the signature include noncestr (random string), valid jsapi_ticket, timestamp (timestamp), url (the url of the current web page, excluding # and the rest). After sorting all parameters to be signed in alphabetical order (lexicographical order) according to the ASCII characters of field names, the URL is used to concatenate them into string string1. Note that all parameter names are lowercase characters. Sha1 encrypts string1, using the original field name and value, without URL escape.

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
};
Copy the code

6. Local debugging

It is recommended to use VS Code to debug the server code, which requires simple configuration and can be found in the egg documentation: eggjs.org/zh-cn/core/…

7. Deployment projects

Before formal deployment, developers configure middleware, templates, plug-ins, and startup configuration items as required.

1. Start the

Egg provides egg-scripts to enable start-stop of online environments.

npm i egg-scripts --save
Copy the code

Add the NPM scripts

{
  "scripts": {
    "start": "egg-scripts start --daemon --title=egg-server-showcase"."stop": "egg-scripts stop"}}Copy the code

Egg starts CPU count processes by default, which is good for performance.

2. Stop

egg-scripts stop [--title=egg-server]
Copy the code

3. Keep alive

Egg-cluster is built into the framework to start the Master process, so no additional configuration is required. The framework also supports management using PM2 for special needs:

8. Performance monitoring

We use egg’s official recommended Node.js performance platform (Alinode)

1. Install the Runtime

AliNode Runtime can replace Node.js Runtime directly

// Install the TNVM version management tool. https://github.com/aliyun-node/tnvm wget -O- https://raw.githubusercontent.com/aliyun-node/tnvm/master/install.sh | bash The source ~ / bashrc / / https://help.aliyun.com/knowledge_detail/60811.html, TNVM install alinode-v4.2.2 # Install alinode-v4.2.2 # install alinode-v4.2.2 # install alinode-v4.2.2 # install alinode-v4.2.2 # For quick access, no need to install AgenthubCopy the code

2. Install and configure egg-Alinode

npm i egg-alinode --save
Copy the code

Enable plug-in:

// config/plugin.js
exports.alinode = {
  enable: true.package: 'egg-alinode'};Copy the code

Configuration:

// config/config.default.js
exports.alinode = {
  // Obtain access parameters from 'Node.js performance platform'
  appid: '<YOUR_APPID>'.secret: '<YOUR_SECRET>'};Copy the code

3. Start the application

The official opening method used by Ali is to add the following code in the command line:

ENABLE_NODE_LOG=YES node demo.js
Copy the code

But in egg, it has its own startup, which is still:

npm start
Copy the code

The official explanation:

After a successful startup, visit your interface a few times, and after a while, you can see the data on the console.

Console address: node.console.aliyun.com

Common mistakes

1. An error occurs during TNVM installation

1.7.1 Git version is too old. Upgrade Git…

// Download git 2.21.1
wget https:/ / github.com/git/git/archive/v2.21.1.tar.gz
tar -zxvf v221.1..tar.gz

// Install dependencies
yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel gcc perl-ExtUtils-MakeMaker

// Delete older versions of git
yum remove git

// Go to the decompressed folder
cd git2.211.

/ / compile
make prefix=/usr/local/git all
make prefix=/usr/local/git install

// Environment variables
echo "export PATH=$PATH:/usr/local/git/bin" >> /etc/profile

// Let environment variables take effect
source /etc/profile
Copy the code

2. undefined reference to `libiconv’

cd /usr/local/src

// Download the new version of Libiconv
wget http:/ / ftp.gnu.org/pub/gnu/libiconv/libiconv-1.15.tar.gz
tar -zxvf libiconv1.15.tar.gz

/ / installation
cd libiconv1.15
./configure --prefix=/usr/local/libiconv && make && make install

// Create a link
ln -s /usr/local/lib/libiconv.so /usr/lib
ln -s /usr/local/lib/libiconv.so2. /usr/lib
Copy the code

Then repeat the 12 lines above to install Git

Git clone reported an SSL connect error

GitHub does not support the old encryption mode. Upgrade to CentOS 6.8 or upgrade SSH separately

yum update -y
yum update openssh
Copy the code

At this point, the whole article is over, and there are some other tutorials in the code section, so overall, it’s not that difficult stuff, let’s learn from each other and improve.