快速實(shí)現(xiàn)微信掃碼關(guān)注公眾號/用戶注冊并登陸

??上周二開晨會分配任務(wù)的時候,分配到了一個微信掃碼關(guān)注公眾號的需求。剛開始以為只要截個公眾號二維碼的圖,然后按照UI出的設(shè)計(jì)稿把二維碼放到指定位置,再加上一波加邊框加陰影的操作提交就完事了。所以當(dāng)部門大佬問多久能做完的時候,我毫不猶豫地說:小Case啦,兩天妥妥的!

??回到座位上本想著時間還早,先刷會微博。刷著刷著看到廣州東站的宜家要搬遷的消息,活動大減價全場5折起?。∪滩蛔↑c(diǎn)進(jìn)去宜家的網(wǎng)上商城,看了兩圈好想剁手買買買。這時,部門大佬站了起來,好像正往我這邊看。算了算了,先關(guān)注宜家公眾號干活去吧,拿出手機(jī)準(zhǔn)備掃碼的時候整個人都懵了...

??誒?誒誒?!早上的需求好像是要實(shí)現(xiàn)掃碼關(guān)注公眾號并登陸的,但瀏覽器怎么會知道我掃碼了,而且掃的還是登陸用的二維碼...這怎么跟想象的不一樣,我還打包票說兩天內(nèi)做完,真的完了完了。

??當(dāng)然,兩天后這掃碼的功能還是經(jīng)過測試按時提交,并在線上穩(wěn)定運(yùn)行了一周。如何才能快速實(shí)現(xiàn),并盡量少踩坑,我想,有些思路和和代碼寫下來,以后可能會用得著。

??項(xiàng)目主要以Nodejs進(jìn)行開發(fā),優(yōu)先選用Koa2 + ioredis等一些比較輕量級的模塊實(shí)現(xiàn),配合Alpine Linux制作Docker Image,最后得到的一個開箱即用的Docker鏡像也僅僅只有33M。

??瀏覽項(xiàng)目的完整代碼可以點(diǎn)擊這里github,如果對你有幫助歡迎Star。

先整理一下需求:

  • 登錄入口實(shí)現(xiàn)掃描二維碼關(guān)注公眾號并登錄網(wǎng)站。已關(guān)注的直接跳轉(zhuǎn)登陸,未關(guān)注的等待用戶關(guān)注再跳轉(zhuǎn);
  • 新的公眾平臺掃碼登錄機(jī)制代替原有的微信開放平臺的掃碼登錄;
  • 掃碼關(guān)注后需要根據(jù)情況返回不同的提示(歡迎)信息。

??但目前已上線的網(wǎng)站同時提供微信掃碼和手機(jī)/郵箱注冊登錄,新需求實(shí)際是想讓更多的用戶關(guān)注公眾號。完全按照需求上做的話就會變成強(qiáng)制用戶必須關(guān)注公眾號,否則無法完成登錄??紤]到市場上有不少同類型產(chǎn)品,這種強(qiáng)制的行為可能會導(dǎo)致用戶反感,從而選擇其他產(chǎn)品。

經(jīng)討論需求改為:

  • 保留現(xiàn)有的微信掃碼和手機(jī)/郵箱注冊登錄,完成注冊/登錄流程后,新增一個掃碼關(guān)注公眾號的頁面;
  • 用戶掃碼關(guān)注,關(guān)注后利用unionid機(jī)制綁定賬戶,讓手機(jī)/郵箱注冊的用戶以后可以直接微信掃碼登錄;
  • 點(diǎn)擊關(guān)注后,網(wǎng)站自動跳轉(zhuǎn)進(jìn)入控制臺,或點(diǎn)擊暫不關(guān)注直接跳轉(zhuǎn);
  • 掃碼關(guān)注后需要根據(jù)情況返回不同的提示(歡迎)信息。

實(shí)現(xiàn)思路和步驟:

  1. 實(shí)現(xiàn)一個與微信公眾號平臺交互的API,接收并處理公眾號推送的事件(關(guān)注、掃碼和文字消息等);
  2. 實(shí)現(xiàn)一個生成二維碼的API供瀏覽器調(diào)用,API可通過參數(shù)聲明需要返回的格式;
  3. 請求公眾平臺 →【生成帶參數(shù)的二維碼】接口生成帶有場景值的二維碼,生成成功后記錄到數(shù)據(jù)庫并返回;
  4. 瀏覽器獲取二維碼信息后輪詢二維碼的掃描狀態(tài),掃描成功后自動跳轉(zhuǎn);
  5. 用戶掃碼后,公眾平臺會向1實(shí)現(xiàn)的API推送事件,如果是關(guān)注就獲取用戶信息,然后記錄到數(shù)據(jù)庫。

第一步,搭建Koa的環(huán)境并接入微信公眾平臺

??提供的源碼里包含刪減過的 Koa2和 koa-router的代碼,也可以使用原版的代碼。建議使用Nodejs10以上版本,特別是Nodejs12,換了新的HTTP解析器(llhttp)性能直接提高了一倍。

安裝依賴

package.json

"dependencies": {
    "debug": "^4.1.1",
    "got": "^9.6.0",
    "ioredis": "^4.10.0",
    "mime-types": "^2.1.24",
    "negotiator": "^0.6.2",
    "xml2js": "^0.4.19",
    "ylru": "^1.2.1"
 }

如果是直接用官方的 Koa,mime-types,negotiator,ylru都不用安裝

目錄結(jié)構(gòu)

dir.png

Koa APP的代碼結(jié)構(gòu)跟官方的栗子差不多,就直接看吧
app.js

const http = require('http')
const Koa = require('./vendor/koa2/application')
const XMLParser = require('./middlewares/XMLParser')
const router = require('./routes/wechat')
const app = new Koa()
?
app.use(XMLParser) // 解析xml的中間件,用于預(yù)處理微信公眾號推送的事件
?
app.use(router.routes())
app.use(router.allowedMethods())
?
http.createServer(app.callback()).listen(3000)

middlewares/XMLParser.js

const parseXML = require('xml2js').parseString
const debug = require('debug')('xml-parse')
?
const parse = (req, options = {}) => {
    return new Promise((resolve, reject) => {
        let xml = ''
        req.on('data', chunk => { xml += chunk.toString('utf-8') })
           .on('error', reject)
           .on('end', () => parseXML(xml, options, (err, res) => {
                if (err) reject(err)
                resolve(res)
        }))
    })
}
?
module.exports = async (ctx, next) => {
    // 這里先嘗試直接匹配,匹配失敗再到mime庫里查詢
    if (ctx.request.type === 'text/xml' || ctx.is('xml')) {
        try {
            ctx.request.body = await parse(ctx.req)
        } catch (e) {
            debug(e.message)
        }
    }
    await next()
}

routes/wechat.js

const Router = require('../vendor/koa-router')
const wechatController = require('../controllers/wechat')
?
?
const router = new Router({
    prefix: '/wechat'
})
?
// 測試號配置接口信息時需要校驗(yàn),但傳輸?shù)臄?shù)據(jù)跟推送消息一樣,所以放在同一個controller里處理
// conntroller的完整path是/wechat/event,這個后面配置測試號URL的時候會用到
router.get('/', ctx => ctx.body = 'hello wechat')
      .get('/event', wechatController)
      .post('/event', wechatController)
?
module.exports = router

先新建一個配置文件,與app.js同目錄
config.js
WXMP的信息暫時留空,到配置微信公眾平臺的時候再填寫

const CACHE = {
    host: 'localhost',
    port: 6379
}
?
// WeChat Media Platform
const WXMP = {
    appID: '',
    appSecret: '',
    token: ''
}
?
module.exports = {
    CACHE,
    WXMP
}

/controllers/wechat.js

const { WXMP } = require('../config');
const { SHA1 } = require('../utils/mUtils')
?
module.exports = async (ctx, next) => {
    const token = WXMP.token
    const { signature, nonce, timestamp, echostr } = ctx.query
?
    /**
    * https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421135319
    * 1)將token、timestamp、nonce三個參數(shù)進(jìn)行字典序排序
    * 2)將三個參數(shù)字符串拼接成一個字符串進(jìn)行sha1加密
    * 3)開發(fā)者獲得加密后的字符串可與signature對比,標(biāo)識該請求來源于微信
    */
    const str = [token, timestamp, nonce].sort().join('')
?
    const signVerified = SHA1(str) === signature
?
    if (!signVerified) {
        ctx.status = 404 // 可以不設(shè)為404,koa默認(rèn)的狀態(tài)值就是404
        return
    }
?
    if (ctx.method === 'GET') ctx.body = echostr
    else if (ctx.method === 'POST') {
        // 實(shí)現(xiàn)思路里的第一步
        // 推送的消息會以POST的方式進(jìn)到這里,暫時用不著,先放著
    }
}

??來到這里,先測試一下Web服務(wù)是否能正常跑起來。這里使用Postman直接發(fā)請求,也可以用瀏覽器訪問http://localhost:3000/wechat/

hello_wechat.png

接下來配置一下微信公眾平臺。

??線上的環(huán)境經(jīng)不起折騰,還是用公眾號測試號進(jìn)行調(diào)試吧,如果你是剛開始接觸微信公眾號開發(fā),推薦使用測試號。

掃碼登陸后會看到這樣一個界面:

mp-sandbox.png

??appID和appsecret是系統(tǒng)生成的,只需要填寫URL和Token。把a(bǔ)ppID和appsecret貼到之前創(chuàng)建的config.js中,token自己隨便輸入,保證兩個token一致即可。配置URL之前,我們需要一個域名和一個內(nèi)網(wǎng)穿透的環(huán)境。舊版本的微信web開發(fā)工具提供一個類似的方式,讓微信服務(wù)器可以向我們在內(nèi)網(wǎng)的機(jī)器推送消息,新版沒有這功能我們就自己百度一個吧。

??粗略比較了一下,發(fā)現(xiàn)續(xù)斷內(nèi)網(wǎng)穿透很大方,只要9.9就有兩條8M永久使用的隧道(只比KFC會員價的原味雞貴4毛,尊貴的VIP吃雞怎么還這么貴(╯‵□′)╯︵┻━┻)。注冊交了9.9入會費(fèi),裝上客戶端后進(jìn)行簡單配置就可以了,體驗(yàn)挺棒的。設(shè)置過程中發(fā)現(xiàn)他家支持好多系統(tǒng),像群暉、OpenWRT那些都有,還有樹莓派,看來吃塵多年的B+可以拿出來發(fā)揮余熱了。好像還有一些有趣的功能,可惜體驗(yàn)隧道不支持。不過只要9.9,還要啥自行車呢,果斷開干吧!

xd.png

??點(diǎn)擊保存稍等一會會得到一個外網(wǎng)訪問地址,類似http://sd8xxxxxxxxs.gzhttp.cn

tunnel.png

??接著把外網(wǎng)訪問地址+之前定義的path(http://sd8xxxxxxxxs.gzhttp.cn/wechat/event)填寫到測試號接口配置的URL中,然后點(diǎn)擊提交。這時我們已經(jīng)成功接入到微信公眾平臺了。

第二步,實(shí)現(xiàn)一個生成二維碼的API并完善與微信公眾號平臺交互的API

??開始第二步之前,先來了解一下創(chuàng)建二維碼及用戶掃碼后公眾號給web服務(wù)推送消息的流程:

sequence.png

??首先在utils/wechat/文件夾中新建一個helper.js,負(fù)責(zé)提供公眾號配置(用于下面創(chuàng)建wechat對象)和get/set access_token的兩個方法。

utils/wechat/helper.js

const { WXMP } = require('../../config')
const { redis } = require('../dbHelper')
?
const config = {
    MP: {
        appID: WXMP.appID,
        appSecret: WXMP.appSecret,
        token: WXMP.token,
        getAccessToken: async () => {
            let token = await redis.get('access_token')
            return token
        },
        saveAccessToken: async (data = {}) => {
            await redis.set('access_token', data.access_token ,'EX', data.expires_in)
        }
    }
}
?
module.exports = {
    ...config
}

??在utils/wechat/文件夾中新建一個wxmp.js,定義一個Wechat類

...
const api = {
    accessToken: 'token?grant_type=client_credential',
    user: {
        info: 'user/info?',
    },
    QRCodeTicket: 'qrcode/create?',
    QRCode: 'showqrcode?'
}
?
class Wechat {
    constructor (opts) {
        // 這里的opts傳入的是上面定義的config
        ...
        this.fetchAccessToken(true)
    }
    // 獲取access_token
    async fetchAccessToken (init = false) {
        let token = await this.getAccessToken()
?
        if (!token) {
            token = await this.updateAccessToken()
            await this.saveAccessToken(token)
            token = token.access_token
        }
        return token
    }
?
    async updateAccessToken () {
        const url = api.accessToken + '&appid=' + this.appID + '&secret=' + this.appSecret
        return await got(url)
    }
?
    // 提供一個統(tǒng)一操作的入口,第一個參數(shù)傳入操作函數(shù)名就可以拿到對應(yīng)的配置
    async handle (operation, ...args) {
        const token = await this.fetchAccessToken()
        if (!token) return null
?
        const options = this[operation](token, ...args)
        let res = await wxGot(options)

        return res
    }
?
    // 獲取用戶信息
    getUserInfo (token, openID, lang) {
        const url = `${api.user.info}access_token=${token}&openid=${openID}&lang=${lang || 'zh_CN'}`
?
        return { url: url }
    }
?
    // 申請二維碼Ticket
    getQRCodeTicket (token, sceneStr, timeout) {
        return {
            url: `${api.QRCodeTicket}access_token=${token}`,
            method: 'post',
            body: {
                "expire_seconds": timeout || 60,
                "action_name": "QR_STR_SCENE", // 臨時二維碼
                "action_info": {
                "scene": {
                    "scene_str": sceneStr
                }
            }
        }
    }
}
?
module.exports = Wechat

??我們繼續(xù)回到剛才的/routes/wechat.js,增加 “+” 標(biāo)識的代碼(新增獲取二維碼的路由)

const Router = require('../vendor/koa-router')
const wechatController = require('../controllers/wechat')
+ const { createQRCodeMB } = require('../controllers/wechat')
?
const router = new Router({
    prefix: '/wechat'
})
?
// 測試號配置接口信息時需要校驗(yàn),但傳輸?shù)臄?shù)據(jù)跟推送消息一樣,所以放在同一個controller里處理
// conntroller的完整path是/wechat/event,這個后面配置測試號URL的時候會用到
router.get('/', ctx => ctx.body = 'hello wechat')
      .get('/event', wechatController)
      .post('/event', wechatController)
+     .get('/qrcode', createQRCodeMB)
?
module.exports = router

然后打開/controllers/wechat.js,公眾號推送事件類型可以參考這里

const { WXMP } = require('../config');
const { SHA1, fmtNormalXML, streamToBuffer, createTimestamp } = require('../utils/mUtils')
const { redis } = require('../utils/dbHelper')
const Wechat = require('../utils/wechat/wxmp')
const MPConfig = require('../utils/wechat/helper').MP
const got = require('got')
const qr = require('../vendor/qr')
const fs = require('fs')
const pathResolve = require('path').resolve
?
const MP = new Wechat(MPConfig)
?
module.exports = async (ctx, next) => {
    ...
    if (ctx.method === 'GET') ctx.body = echostr
    else if (ctx.method === 'POST') {
        // 把數(shù)組形態(tài)的xmlObject轉(zhuǎn)換可讀性更高的結(jié)構(gòu)
        const message = fmtNormalXML(ctx.request.body.xml)
?        const msgType = message.MsgType
        const msgEvent = message.Event
        const userID = message.FromUserName
        let eventKey = message.EventKey
        let body = null
?
        if (msgType === 'event') {
            switch (msgEvent) {
                // 關(guān)注&取關(guān)
                case 'subscribe':
                case 'unsubscribe':
                    body = await subscribe(message)
                    break
                // 關(guān)注后掃碼
                case 'SCAN':
                    body = '掃碼成功'
                    break
            }

            if (!!eventKey) {
                // 有場景值(掃了我們生成的二維碼)
                let user = await MP.handle('getUserInfo', userID)
                let userInfo = `${user.nickname}(${user.sex ? '男' : '女'}, ${user.province}${user.city})`
                if (eventKey.slice(0, 8) === 'qrscene_') {
                    // 掃碼并關(guān)注
                    // 關(guān)注就創(chuàng)建帳號的話可以在這里把用戶信息寫入數(shù)據(jù)庫完成用戶注冊
                    eventKey = eventKey.slice(8)
                    console.log(userInfo + '掃碼并關(guān)注了公眾號')
                } else {
                    // 已關(guān)注
                    console.log(userInfo + '掃碼進(jìn)入了公眾號')
                }
?
                // 更新掃碼記錄,供瀏覽器掃碼狀態(tài)輪詢
                await redis.pipeline()
                           .hset(eventKey, 'unionID', user.unionid || '') // 僅unionid機(jī)制下有效
                           .hset(eventKey, 'openID', user.openid)
                           .exec()
            }
        }
    }
}
?
async function subscribe (message) {
    let userID = message.FromUserName
    if (message.Event === 'subscribe') {
        return '感謝您的關(guān)注'
    } else {
        // 用戶取消關(guān)注后我們不能再通過微信的接口拿到用戶信息,
        // 如果要記錄用戶信息,需要從我們自己的用戶記錄里獲取該信息。
        // 所以建議創(chuàng)建用戶時除了unionid,最好把openid也保存起來。
        console.log(userID + '取關(guān)了')
    }
}
?
const templetData = fs.readFileSync(pathResolve(__dirname, '../vendor/qrcode-templet.html'))
?
// 創(chuàng)建二維碼
async function createQRCodeMB (ctx, next) {
    let userID = ctx.query.userID
    let type = +ctx.query.type
    let errno = 0
    let responseDate = {}
    let id = createTimestamp()
?
    let res = await MP.handle('getQRCodeTicket', id)
?
    if (res === null) errno = 1
    else {
        responseDate = {
            expiresIn: res.expire_seconds,
            id
        }
?
        let imgBuffer = await streamToBuffer(qr.image(res.url))
        let imgSrc = imgBuffer.toString('base64')
?
        if (type === 1) {
            // 返回圖片
            ctx.body = `<img src="data:image/png;base64,${imgSrc}" />`
        } else if (type === 2) {
            // 返回一個自帶查詢狀態(tài)和跳轉(zhuǎn)的網(wǎng)頁
            let templetValue = `
                <script>var imgSrc='${imgSrc}',id='${responseDate.id}',
                timeout=${responseDate.expiresIn},width=100,height=100</script>`
?
            ctx.body = templetValue + templetData.toString('utf-8')
        } else {
            // 返回圖片內(nèi)容
            responseDate.imgSrc = imgSrc
        }
     }
?
    if (!ctx.body) {
        ctx.body = {
            errno,
            ...responseDate
        }
    }
}
?
module.exports.createQRCodeMB = createQRCodeMB

??到這里應(yīng)該是可以接收到公眾號推送的掃碼事件和生成二維碼。

??保存后我們先測試一下,首先不帶參數(shù)訪問http://localhost:3000/wechat/qrcode

normal.png

??接著嘗試獲取二維碼圖片(使用參數(shù)type=1)并使用微信掃描二維碼:

scan.png

??首次掃描二維碼會提示關(guān)注,點(diǎn)擊關(guān)注后數(shù)據(jù)庫就會更新,控制臺也會打印出類似 “XXX掃碼并關(guān)注了公眾號“ 的日志。但這時候公眾號里應(yīng)該會提示 ”該公眾號提供的服務(wù)出現(xiàn)故障,請稍后再試“ 的提示,因?yàn)槌绦虿]有把提示信息正確得返回。下一步我們需要格式化返回的信息(即ctx.body的內(nèi)容)。

新增一個生成模板的文件/utils/tmpl.js
格式化給公眾號返回的消息,這里只簡單使用util.format來格式化消息。

const util = require('util')
?
const msgTemplet = `
<xml>
    <ToUserName><![CDATA[%s]]></ToUserName>
    <FromUserName><![CDATA[%s]]></FromUserName>
    <CreateTime>%d</CreateTime>
    <MsgType><![CDATA[%s]]></MsgType>
    $msgBody$
</xml>
`
?
const textMsg = `<Content><![CDATA[%s]]></Content>`
const imageMsg = `<Image><MediaId><![CDATA[%s]]></MediaId></Image>`
?
module.exports = (ctx, originMsg) => {
    let type = (ctx && ctx.type) || 'text'
    let msgTmpl = util.format(msgTemplet,
        originMsg.FromUserName,
        originMsg.ToUserName,
        Math.floor(new Date().getTime() / 1000),
        type
    )
?
    let body = ''
?
    switch (type) {
        case 'text':
            body = util.format(textMsg, ctx)
            break
    case 'image':
        break
    default:
        body = util.format(textMsg, '操作無效')
    }
?
    return msgTmpl.replace(/\$msgBody\$/, body)
}

接著我們在controllers/wechat.js增加一下 ”+“ 標(biāo)記的代碼

const { WXMP } = require('../config');
const { SHA1, fmtNormalXML, streamToBuffer, createTimestamp } = require('../utils/mUtils')
+ const { tmpl } = require('../utils/wechat')
const { redis } = require('../utils/dbHelper')
const Wechat = require('../utils/wechat/wxmp')
...
module.exports = async (ctx, next) => {
    const token = WXMP.token
    const { signature, nonce, timestamp, echostr } = ctx.query
?
    const str = [token, timestamp, nonce].sort().join('')
    ...
                // 更新掃碼記錄,供瀏覽器掃碼狀態(tài)輪詢
                await redis.pipeline()
                           .hset(eventKey, 'unionID', user.unionid || '') // 僅unionid機(jī)制下有效
                           .hset(eventKey, 'openID', user.openid)
                           .exec()
            }
        }
?
+       ctx.type = 'application/xml'
+       ctx.body = tmpl(body || ctx.body, message)
    }
}
?
async function subscribe (message) {
    let userID = message.FromUserName
    if (message.Event === 'subscribe') {
        return '感謝您的關(guān)注'
    } else {
        console.log(userID + '取關(guān)了')
    }
}

??保存后再獲取一次二維碼并掃描,微信上就能正確顯示提示信息了:

message.png

第三步,瀏覽器增加掃碼狀態(tài)輪詢

??這塊跟業(yè)務(wù)代碼關(guān)系比較密切,所以不做詳細(xì)介紹。共通點(diǎn)就是通過二維碼返回id獲取unionid(openid)的記錄,然后按需處理,最后以cookies或其他方式更新登錄狀態(tài)。

輪詢的代碼可以參考vendor/qrcode-templet.html

async function waitToSubscribe(id, timeout) {
    let countdown = Math.ceil(timeout / 3);
    return new Promise((resolve, reject) => {
        const loop = async function() {
            let res = await ky.default.get("/wechat/check", {
                searchParams: { id }
            }).json();

            if (!res) return;
            if (res.errno === 0) resolve("subscribe");
            else if (res.errno === 2) reject("timeout");
            else if (countdown-- > 0) self.QRCodeTimer = setTimeout(loop, 3000);
        };
        loop();
    });
};

(async () => {
    try {
        await waitToSubscribe(id, timeout);
        window.location.href = "/wechat/";
    } catch (e) {
        history.go(0);
    }
})();

我們可以嘗試獲取集成好獲取狀態(tài)的二維碼網(wǎng)頁(使用參數(shù)type=2,實(shí)際使用時可以用iframe嵌套):

auto.png

總結(jié):

??到這里,我們已經(jīng)實(shí)現(xiàn)了:

1.  與微信公眾號平臺交互的API,能夠接收并處理公眾號推送的事件;
2.  生成二維碼的API,并能分別以三種常用方式返回二維碼;
3.  掃描二維碼后,微信上能正常顯示服務(wù)返回的提示信息,并成功記錄在數(shù)據(jù)庫中;
4.  當(dāng)瀏覽器輪詢二維碼的掃描狀態(tài)并獲取到掃描結(jié)果后,自動跳轉(zhuǎn)。

??以上幾乎包含了公眾號開發(fā)的完整流程,其他的功能可以參照公眾號開發(fā)文檔上的說明按需增加。這里有一點(diǎn)需要注意的,文中提到的unionid機(jī)制需要以公司身份申請正式的公眾號和微信開放平臺,并在開放平臺上完成公眾號綁定。同一個用戶在已綁定公眾號、小程序、網(wǎng)站應(yīng)用等程序里會使用同一個unionid來確定用戶的唯一性。

??像公眾號網(wǎng)頁授權(quán)、開放平臺的網(wǎng)站應(yīng)用授權(quán)(類似京東的掃碼登錄)和小程序的開發(fā),等有空的時候再更新。碼了這么多字,差點(diǎn)忘了要去宜家掃貨,廣告上說促銷商品數(shù)量有限,萬一賣完了豈不是錯過了幾個億::>_<::,周末要找個時間過去看看才行。

最后附上Dockerfile 和源碼地址

??預(yù)先拷貝文件到/build目錄,便于生成更小的Docker Image

cp -rf vendor docker/build/vendor
cp -rf utils docker/build/utils
cp -rf routes docker/build/routes
cp -rf middlewares docker/build/middlewares
cp -rf controllers docker/build/controllers
cp app.js docker/build/app.js
cp config.js docker/build/config.js
FROM alpine
COPY package.json /var/www/wechat-mp/
WORKDIR /var/www/wechat-mp
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
 && apk add nodejs npm \
 && npm install --production --registry=https://registry.npm.taobao.org \
 && npm cache clean -f \
 && rm package-lock.json \
 && apk del npm \
 && rm -rf ~/.npm \
 && rm -rf /var/cache/apk/* \
 && rm -rf /root/.cache \
 && rm -rf /tmp/*
COPY build/ /var/www/wechat-mp/
CMD node app.js</pre>

github: https://github.com/lym0r9/wechat-mp

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容