涉及微信開發的技術人員總會面對一些「對接」工做,每當作好一個產品賣給對方的時候,都須要程序員介入進行一些配置。例如:html
使用「微信JS-SDK」的應用,咱們須要添加微信公衆號「JS接口安全域名」。前端
爲了解決微信頁面安全提示,咱們須要添加微信公衆號「業務域名」。vue
爲了在小程序中使用WebView頁面,咱們須要添加微信小程序「業務域名」。git
以上三種狀況都不是簡單的將域名填入到微信管理後臺,而是須要下載一個txt文件,保存到服務器根目錄,可以被微信服務器直接訪問,才能正常保存域名。程序員
若是隻須要對接一個或幾個應用,打開Nginx配置,以下添加:github
location /YGCSYilWJs.txt {
default_type text/html;
return 200 '78362e6cae6a33ec4609840be35b399b';
}
複製代碼
假若有幾十個甚至幾百個項目須要接入😂。web
讓咱們花20分鐘完全解決這個問題。redis
進行域名泛解析:*.abc.com
-> 服務器,反向代理根目錄下.txt
結尾的請求。順便配置一下通配符SSL證書(網上有免費版本)。數據庫
location ~* ^/+?\w+\.txt$ {
proxy_http_version 1.1;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://127.0.0.1:8362$request_uri;
proxy_redirect off;
}
複製代碼
建立一個新項目,yarn add koa
(或許你須要一個腳手架)。json
正如上面所說,咱們須要攔截根目錄下以.txt
結尾的請求。所以咱們添加koa路由模塊koa-router
。爲了處理API中的數據,還須要koa-body
模塊。咱們使用Sequelize
做爲ORM,用Redis
做爲緩存。
在入口文件(例如main.js
)中引入Koa及KoaBody(爲了方便閱讀,此處爲關鍵代碼,並不是完整代碼,下同)。
import Koa from 'koa'
import KoaBody from 'koa-body'
const app = new Koa()
app.proxy = true
var server = require('http').createServer(app.callback())
app
.use((ctx, next) => { // 解決跨域
ctx.set('Access-Control-Allow-Origin', '*')
ctx.set('Access-Control-Allow-Headers', 'Authorization, DNT, User-Agent, Keep-Alive, Origin, X-Requested-With, Content-Type, Accept, x-clientid')
ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS')
if (ctx.method === 'OPTIONS') {
ctx.status = 200
ctx.body = ''
}
return next()
})
.use(KoaBody({
multipart: true, // 開啓對multipart/form-data的支持
strict: false, // 取消嚴格模式,parse GET, HEAD, DELETE requests
formidable: { // 設置上傳參數
uploadDir: path.join(__dirname, '../assets/uploads/tmpfile')
},
jsonLimit: '10mb', // application/json 限制,default 1mb 1mb
formLimit: '10mb', // multipart/form-data 限制,default 56kb
textLimit: '10mb' // application/x-www-urlencoded 限制,default 56kb
}))
server.listen(8362) // 監聽的端口號
複製代碼
這裏沒有引用koa-router
,由於使用傳統的koa-router
方式,閱讀起來不夠直觀,所以咱們進行一個簡單改造——文件即路由(PS:容易閱讀的數據能大幅提升生產力,API若是須要便於閱讀,則須要引入swagger等工具,爲了方便,咱們改造爲文件即路由,一眼掃過去的樹形數據就是咱們的各個API及其結構)。
文件目錄對應到API路徑是這樣的:
|-- controller
|-- :file.txt.js // 對應API:/xxx.txt ,其中ctx.params('file')表明文件名
|-- index.html
|-- index.js // 對應API: /
|-- api
|-- index.js // 對應API: /api 或 /api/
|-- weixin-js-sdk
|-- add.js // 對應API: /api/weixin-js-sdk/add
|-- check.js // 對應API: /api/weixin-js-sdk/check
|-- get.js // 對應API: /api/weixin-js-sdk/get
複製代碼
這樣一來,API接口的結構一目瞭然(自然的樹形圖),並且維護controller
文件自己便可,無需同步維護一次路由表。
文件即路由的具體改造方法(參考自:github.com/dominicbarn…):
import inject, { client } from './inject'
import flatten from 'array-flatten'
import path from 'path'
import Router from 'koa-router'
import eachModule from 'each-module'
var debug = require('debug')('koa-file-router')
const methods = [
'get',
'post',
'put',
'head',
'delete',
'options',
'data'
]
export const redisClient = client
export default (dir, options) => {
if (!options) options = {}
debug('initializing with options: %j', options)
var router = new Router()
return mount(router, discover(dir))
}
function discover (dir) {
var resources = {
params: [],
routes: []
}
debug('searching %s for resources', dir)
eachModule(dir, function (id, resource, file) {
// console.log(id)
// console.log(file)
if (id.startsWith('_params')) {
var name = path.basename(file, '.js')
debug('found param %s in %s', name, file)
resources.params.push({
name: name,
handler: resource.default
})
} else {
methods.concat('all').forEach(function (method) {
if (method in resource.default) {
var url = path2url(id)
debug('found route %s %s in %s', method.toUpperCase(), url, file)
resources.routes.push({
name: resource.name,
url: url,
method: method,
handler: resource.default[method]
})
}
})
}
})
resources.routes.sort(sorter)
return resources
}
function mount (router, resources) {
resources.params.forEach(function (param) {
debug('mounting param %s', param.name)
router.param(param.name, param.handler)
})
let binds = {}
resources.routes.forEach(function (route) {
debug('mounting route %s %s', route.method.toUpperCase(), route.url)
if (route.method === 'data') {
binds = route.handler()
}
})
resources.routes.forEach(function (route) {
debug('mounting route %s %s', route.method.toUpperCase(), route.url)
// console.log('mounting route %s %s', route.method.toUpperCase(), route.url)
if (route.method !== 'data') {
route.handler = route.handler.bind(Object.assign(binds, inject))
let args = flatten([route.url, route.handler])
if (route.method === 'get' && route.name) args.unshift(route.name)
router[route.method].apply(router, args)
// router[route.method](route.url, route.handler)
}
})
// console.log(router)
return router
}
function path2url (id) {
var parts = id.split(path.sep)
var base = parts[parts.length - 1]
if (base === 'index') parts.pop()
return '/' + parts.join('/')
}
function sorter (a, b) {
var a1 = a.url.split('/').slice(1)
var b1 = b.url.split('/').slice(1)
var len = Math.max(a1.length, b1.length)
for (var x = 0; x < len; x += 1) {
// same path, try next one
if (a1[x] === b1[x]) continue
// url params always pushed back
if (a1[x] && a1[x].startsWith(':')) return 1
if (b1[x] && b1[x].startsWith(':')) return -1
// normal comparison
return a1[x] < b1[x] ? -1 : 1
}
}
複製代碼
在代碼的第一行引入一個inject.js
文件,這個文件主要是注入一些數據到對應的函數中,模擬前端vue的寫法:
import utils from './lib/utils'
import { Redis } from './config'
import redis from 'redis'
import { promisify } from 'util'
import plugin from './plugins'
import fs from 'fs'
import path from 'path'
import Sequelize from 'sequelize'
import jwt from 'jsonwebtoken'
const publicKey = fs.readFileSync(path.join(__dirname, '../publicKey.pub'))
export const client = redis.createClient(Redis)
const getAsync = promisify(client.get).bind(client)
const modelsDir = path.join(__dirname, './models')
const sequelize = require(modelsDir).default.sequelize
const models = sequelize.models
export default {
$plugin: plugin,
$utils: utils,
Model: Sequelize,
model (val) {
return models[val]
},
/** * send success data */
success (data, status = 1, msg) {
return {
status,
msg,
result: data
}
},
/** * send fail data */
fail (data, status = 10000, msg) {
return {
status,
msg,
result: data
}
},
/** * 經過Redis進行緩存 */
cache: {
set (key, val, ex) {
if (ex) {
client.set(key, val, 'PX', ex)
} else {
client.set(key, val)
}
},
get (key) {
return getAsync(key)
}
},
/** * 深拷貝對象、數組 * @param {[type]} source 原始對象或數組 * @return {[type]} 深拷貝後的對象或數組 */
deepCopy (o) {
if (o === null) {
return null
} else if (Array.isArray(o)) {
if (o.length === 0) {
return []
}
let n = []
for (let i = 0; i < o.length; i++) {
n.push(this.deepCopy(o[i]))
}
return n
} else if (typeof o === 'object') {
let z = {}
for (let m in o) {
z[m] = this.deepCopy(o[m])
}
return z
} else {
return o
}
},
async updateToken (userGUID, expiresIn = '365d') {
const userInfo = await models['user'].findOne({
where: {
userGUID
}
})
models['userLog'].create({
userGUID: userInfo.userGUID,
type: 'update'
})
const token = jwt.sign({
userInfo
}, publicKey, {
expiresIn
})
return token
},
decodeToken (token) {
return jwt.verify(token.substr(7), publicKey)
}
}
複製代碼
以上文件所實現的效果是這樣的:
// 示例Controller文件
export default {
async get (ctx, next) { // get則是GET請求,post則爲POST請求,其他同理
this.Model // inject.js文件中注入到this裏面的Sequelize對象
this.model('abc') // 獲取對應的model對象,abc即爲sequelize.define('abc'...
this.cache.set('key', 'value') // 設置緩存,例子中用Redis做爲緩存
this.cache.get('key') //獲取緩存
ctx.body = this.success('ok') // 同理,由injec.js注入
next()
}
}
複製代碼
看到上面的寫法,是否是有一種在寫vue的感受?
爲了獲取參數更爲方便,咱們進行一些優化。添加一個controller中間件:
const defaultOptions = {}
export default (options, app) => {
options = Object.assign({}, defaultOptions, options)
return (ctx, next) => {
ctx.post = function (name, value) {
return name ? this.request.body[name] : this.request.body
}
ctx.file = function (name, value) {
return name ? ctx.request.body.files[name] : ctx.request.body.files
}
ctx.put = ctx.post
ctx.get = function (name, value) {
return name ? this.request.query[name] : this.request.query
}
ctx.params = function (name, value) {
return name ? this.params[name] : this.params
}
return next()
}
}
複製代碼
這樣一來,咱們在controller文件中的操做是這個效果:
// 示例Controller文件, 文件路徑對應API路徑
export default {
async get (ctx, next) { // GET請求
ctx.get() // 獲取全部URL參數
ctx.get('key') // 獲取名稱爲'key'的參數值
ctx.params('xxx') // 若是controller文件名爲「:」開頭的變量,例如:xxx.js,則此處獲取xxx的值,例如文件名爲「:file.txt.js」,請求地址是「ok.txt」,則ctx.params('file')的值爲「ok」
ctx.body = this.success('ok')
next()
},
async post (ctx, next) { // POST請求
// 在POST請求中,除了GET請求的參數獲取方法,還能夠用:
ctx.post() // 用法同ctx.get()
ctx.file() // 上傳的文件
ctx.body = this.success('ok')
next()
},
async put (ctx, next) { // PUT請求
// 在PUT請求中,除了有GET和POST的參數獲取方法,還有ctx.put做爲ctx.post的別名
ctx.body = this.success('ok')
next()
},
async delete (ctx, next) { // DELETE請求
// 參數獲取方法同post
ctx.body = this.success('ok')
next()
},
async ...
}
複製代碼
固然,光有以上用法還不夠,還得加上一些快速操做數據庫的魔法(基於Sequelize
)。
添加auto-migrations模塊,在gulp
中監控models
文件夾中的變化,若是添加或者修改了model,則自動將model同步到數據庫做爲數據表(相似Django,試驗用法,請勿用於生產環境)。具體配置參考源碼。
在models文件夾中新建文件weixinFileIdent.js
,輸入一下信息,保存後則數據庫中將自動出現weixinFileIdent
表。
export default (sequelize, DataTypes) => {
const M = sequelize.define('weixinFileIdent', {
id: {
type: DataTypes.BIGINT.UNSIGNED,
allowNull: false,
primaryKey: true,
autoIncrement: true
},
name: {
type: DataTypes.STRING(255),
allowNull: false
},
content: {
type: DataTypes.STRING(255),
allowNull: true
}
})
M.associate = function (models) {
// associations can be defined here
}
return M
}
複製代碼
在controller文件夾中添加:file.txt.js
文件。
export default {
async get (ctx, next) {
const fileContent = await this.model('weixinFileIdent').findOne({
where: {
name: ctx.params('file')
},
order: [
['updatedAt', 'DESC']
]
})
ctx.set('Content-Type', 'text/plain; charset=utf-8')
if (fileContent) {
ctx.body = fileContent.content
next()
} else {
ctx.body = ''
next()
}
}
}
複製代碼
在用戶訪問https://域名/XXX.txt
文件的時候,讀取數據庫中保存的文件內容,以文本的方式返回。這樣一來,微信的驗證服務器方會經過對該域名的驗證。
同理,咱們須要一個添加數據的接口controller/api/check.js
(僅供參考)。
export default {
async get (ctx, next) {
await this.model('weixinFileIdent').create({ // 僅演示邏輯,沒有驗證是否成功,是否重複等。
name: ctx.get('name'),
content: ctx.get('content')
})
ctx.body = {
status: 1
}
next()
}
}
複製代碼
當咱們訪問https://域名/api/weixin-js-sdk/check?name=XXX&content=abc
的時候,將插入一條數據到數據庫中,記錄從微信後臺下載的文件內容,當訪問https://域名/XXX.txt
的時候,將返回文件內容(此方法通用於文初提到的三種場景)。
按照一樣的方法,咱們實現多用戶JS-SDK配置信息返回。
定義模型(C+S保存後自動創表):
export default (sequelize, DataTypes) => {
const M = sequelize.define('weixinJSSDKKey', {
id: {
type: DataTypes.BIGINT.UNSIGNED,
allowNull: false,
primaryKey: true,
autoIncrement: true
},
domain: {
type: DataTypes.STRING(255),
allowNull: false
},
APPID: {
type: DataTypes.STRING(255),
allowNull: false
},
APPSECRET: {
type: DataTypes.STRING(255),
allowNull: true
}
})
M.associate = function (models) {
// associations can be defined here
}
return M
}
複製代碼
controller/api/weixin-js-sdk/add.js
文件:
export default {
async get (ctx, next) {
await this.model('weixinJSSDKKey').create({
domain: ctx.get('domain'),
APPID: ctx.get('APPID'),
APPSECRET: ctx.get('APPSECRET')
})
ctx.body = {
status: 1,
msg: '添加成功'
}
next()
}
}
複製代碼
同理,當咱們訪問https://域名/api/weixin-js-sdk/add?domain=XXX&APPID=XXX&APPSECRET=XXX
的時候,將插入一條數據到數據庫中,記錄該用戶的二級域名前綴,公衆號的APPID
和APPSECRET
。
controller/api/weixin-js-sdk/get.js
文件,依賴於co-wechat-api
模塊。
const WechatAPI = require('co-wechat-api')
export default {
async get (ctx, next) {
const weixinJSSDKKey = this.model('weixinJSSDKKey')
const domain = ctx.header.host.substring(0, ctx.header.host.indexOf('.abc.com')) // 根域名
const result = await weixinJSSDKKey.findOne({
where: {
domain
},
order: [
['updatedAt', 'DESC']
]
})
if (result) {
const APPID = result.APPID
const APPSECRET = result.APPSECRET
if (ctx.query.url) {
const api = new WechatAPI(APPID, APPSECRET, async () => {
// 傳入一個獲取全局token的方法
let txt = null
try {
txt = await this.cache.get('weixin_' + APPID)
} catch (err) {
console.log(err)
txt = '{"accessToken":"x","expireTime":1520244812873}'
}
return txt ? JSON.parse(txt) : null
}, (token) => {
// 請將token存儲到全局,跨進程、跨機器級別的全局,好比寫到數據庫、redis等
// 這樣才能在cluster模式及多機狀況下使用,如下爲寫入到文件的示例
this.cache.set('weixin_' + APPID, JSON.stringify(token))
})
var param = {
debug: false,
jsApiList: [ // 須要用到的API列表
'checkJsApi',
'onMenuShareTimeline',
'onMenuShareAppMessage',
'onMenuShareQQ',
'onMenuShareWeibo',
'hideMenuItems',
'showMenuItems',
'hideAllNonBaseMenuItem',
'showAllNonBaseMenuItem',
'translateVoice',
'startRecord',
'stopRecord',
'onRecordEnd',
'playVoice',
'pauseVoice',
'stopVoice',
'uploadVoice',
'downloadVoice',
'chooseImage',
'previewImage',
'uploadImage',
'downloadImage',
'getNetworkType',
'openLocation',
'getLocation',
'hideOptionMenu',
'showOptionMenu',
'closeWindow',
'scanQRCode',
'chooseWXPay',
'openProductSpecificView',
'addCard',
'chooseCard',
'openCard'
],
url: decodeURIComponent(ctx.query.url)
}
ctx.body = {
status: 1,
result: await api.getJsConfig(param)
}
next()
} else {
ctx.body = {
status: 10000,
err: '未知參數'
}
next()
}
} else {
ctx.body = ''
next()
}
}
}
複製代碼
JS-SDK配置數據獲取地址:https://域名/api/weixin-js-sdk/get?APPID=XXX&url=XXX
,第二個XXX爲當前頁面的地址。通常爲encodeURIComponent(window.location.origin) + '/'
,若是當前頁面存在'/#/xxx',則爲encodeURIComponent(window.location.origin) + '/#/'
。
返回的數據中的result即爲wx.config(result)
。
最後,咱們再寫一個簡單的html頁面做爲引導(http://localhost:8362/?name=xxx):
完整示例源代碼:github.com/yi-ge/weixi…
原創內容。文章來源:www.wyr.me/post/592