开发小程序的朋友们随时都会听到一句话:“喂,快给我打一个xxx环境的预览码”,无论你正在干什么,都得赶紧地回一句:“稍等,这就给你打码……”
然后苦逼的你build了一个xxx环境的包,打开了微信开发者工具,点了一下预览,等了一下,预览码出来了,你复制丢给你的爸爸们。
终于有一天,你正在专心致志做一些不可描述的事情时,“喂,快给我打一个xxx环境的预览码”,这时你内心怒吼了一句:“老子不给你打码!你自己打去!”
于是就有了这个需求,要搞个东西让爸爸们自主打码,嗯,应该就是只有一个按钮,点一下就可以出现预览二维码的东西,意淫了一下应该是这样的:
没错!就这样干!
干大事就要从胡思乱想开始,现在来想想要搞成这个功能,需要做点什么准备工作吧。
找微信开发者工具的接口
最重要的事情莫过于看看微信开发者工具有没有给我们提供这样的接口让我们去操作,经过一番查阅文档我们会发现,果然有!
https://developers.weixin.qq.com/miniprogram/dev/devtools/http.html
会发现,文档给我们提供了两种方式的接口,命令行调用以及HTTP调用。有了接口,一切都好办了,无非就是调一下接口,拿到二维码,贴到页面上去而已嘛,很简单。
梳理开发流程
我们就把这个简单的事情,用流程图说明一下:
https://www.processon.com/view/link/5b1de951e4b06350d45ce355
所需技术
工欲善其事,必先利其器,我们要搞这个东西,还是先要把用到的技术整理一下。
好像没别的东西了,用到了再说吧。
为了省事,直接把前后端的东西放在一起。项目目录:
可以看到server这个目录下放的都是后端的东西。
server/index.js
先看看入口文件index.js,从这里我们可以知道后端要做两件事情,第一要能访问到前端build出来的静态资源,第二要能与前端通过HTTP接口进行交互。见代码:
const path =
require('path')
const
Koa
=
require('koa')
const koaStatic =
require('koa-static')
const bodyParser =
require('koa-bodyparser')
const router =
require('./router')
const app =
new
Koa()
const port =
9871
app.use(bodyParser())
// 处理静态资源 这里是前端build好之后的目录
app.use(koaStatic(
path.resolve(__dirname,
'../dist')
))
// 路由处理接口
app.use(router.routes()).use(router.allowedMethods())
// 监听端口
app.listen(9871)
console.log(`[demo] start-quick is starting at port ${port}`)
静态资源方面的话使用koa-static即可,重点是怎样给前端提供接口,这就要看路由了。
server/router/index.js
const
Router
=
require('koa-router')
// 业务逻辑
const wx =
require('../controller/wx')
const router =
new
Router({
// 接口前缀 比如open接口 请求路径就是/api/open
prefix:
'/api'
})
router.get('/open', wx.open)
.get('/login', wx.login)
.get('/preview', wx.preview)
.get('/build', wx.build)
module.exports = router
这里可以清晰看到,后端提供了四个接口,但具体每个接口的业务逻辑则封装在controller里的wx.js,如果以后还有别的业务逻辑,就在controller加相应的模块即可。
server/controller/wx.js
// 微信开发者工具接口调用逻辑
const
{open, login, preview, build}
=
require('../utli/wxToolApi')
// 处理成功失败返回格式的工具
const
{successBody, errorBody}
=
require('../utli')
class
WxController
{
/**
* 根据环境对mpvue项目进行打包
* @returns {Promise}
*/
static
async build (ctx)
{
// 前端传过来的get参数
const query = ctx.request.query
if
(!query ||
!query.env)
{
ctx.body = errorBody(,
'构建项目失败')
return
}
const
[err, data]
=
await build(query.env)
ctx.body = err ? errorBody(err,
'构建项目失败')
: successBody(data,
'构建项目成功')
}
/**
* 打开微信开发者工具
* @returns {Promise}
*/
static
async open (ctx)
{
const
[err, data]
=
await open()
ctx.body = err ? errorBody(err,
'打开微信开发者工具失败')
: successBody(data,
'打开微信开发者工具成功')
}
/**
* 登录微信开发者工具
* @returns {Promise}
*/
static
async login (ctx)
{
const
[err, data]
=
await login()
ctx.body = err ? errorBody(err,
'登录二维码返回失败')
: successBody(data,
'登录二维码返回成功')
}
/**
* 查看预览码
* @returns {Promise}
*/
static
async preview (ctx)
{
const
[err, data]
=
await preview()
ctx.body = err ? errorBody(err,
'预览二维码返回失败')
: successBody(data,
'预览二维码返回成功')
}
}
module.exports =
WxController
为了代码更加清晰,这里将具体操作微信开发者工具的接口逻辑抽到util/wxToolApi.js里去了,仅仅处理怎样以统一格式返回给前端。
util/wxToolApi.js
const
{promiseWrap, successBody, errorBody}
=
require('../utli')
const
{INSTALL_PATH, PROJECT_PATH, PORT_PATH, PORT_FILE_NAME, HOST}
=
require('../const')
const
{readFile}
=
require('../utli/nodeApi')
const shell =
require('shelljs')
const axios =
require('axios')
module.exports =
{
/**
* 根据环境对mpvue项目进行打包
* @param env [doc, pre, prd]
* @returns {*}
*/
build (env)
{
return promiseWrap(new
Promise((resolve, reject)
=>
{
// 进入项目目录
shell.cd(PROJECT_PATH)
// 执行打包命令
shell.exec(`npm run build:${env}`,
function
(code, stdout, stderr)
{
resolve(stdout)
})
}))
},
/**
* 打开微信开发者工具
* @returns {*}
*/
open ()
{
return promiseWrap(new
Promise((resolve, reject)
=>
{
// 进入项目目录
shell.cd(INSTALL_PATH)
// 执行微信开发者工具接口“命令行启动工具”
shell.exec(`cli -o ${PROJECT_PATH}`,
function
(code, stdout, stderr)
{
if
(stderr)
return reject(stderr)
resolve(stdout)
})
}))
},
/**
* 获取微信开发者工具端口号
* @returns {Promise<*>}
*/
async getPort ()
{
shell.cd(PORT_PATH)
// http 服务在工具启动后自动开启,HTTP 服务端口号在用户目录下记录,可通过检查用户目录、检查用户目录下是否有端口文件及尝试连接来判断工具是否安装/启动。
const
[err, data]
=
await readFile(PORT_FILE_NAME)
return err ? errorBody(err,
'读取端口号文件失败')
: successBody(data,
'读取端口号文件成功')
},
/**
* 微信开发者工具进行登录
* @returns {*}
*/
login ()
{
return promiseWrap(new
Promise(async
(resolve, reject)
=>
{
// 获取端口号
const portData =
await
module.exports.getPort()
if
(portData.code !==
0)
{
reject(portData)
return
}
const port = portData.data
axios.get(`http://${HOST}:${port}/login?format=base64`)
.then(res =>
{
resolve(res.data)
})
.catch(e =>
{
reject(e)
})
}))
},
/**
* 微信开发者工具获取预览码
* @returns {*}
*/
preview ()
{
return promiseWrap(new
Promise(async
(resolve, reject)
=>
{
const portData =
await
module.exports.getPort()
if
(portData.code !==
0)
{
reject(portData)
return
}
const port = portData.data
axios.get(`http://${HOST}:${port}/preview?format=base64&projectpath=${encodeURIComponent(PROJECT_PATH)}`)
.then(res =>
{
resolve(res.data)
})
.catch(e =>
{
reject(e)
})
}))
}
}
这里有一点需要注意,为什么只有open接口需要用命令行调用方式?那是因为HTTP调用方式必须加端口,比如open接口:
# 打开工具
http://127.0.0.1:端口号/open
# 打开/刷新项目
http://127.0.0.1:端口号/open?projectpath=项目全路径
如果你根本都没有打开微信开发者工具,在以下地方就会找不到端口:
端口号文件位置:
macOS :
~/Library/Application
Support/微信web开发者工具/Default/.ide
Windows
:
~/AppData/Local/微信web开发者工具/User
Data/Default/.ide
所以作为一个全自动化打码工具,怎么可能还要自己去手动打开微信开发者工具呢!
后端的东西基本就那么多,终于到前端了,前端十分简单,就不多说了:
<group< span="">
title="请选择环境"></group<>
<radio< span=""> :options="envOption"
v-model="env"></radio<>
<x-button< span="">
class="btn"
type="default" @click.native="handlePreviewProject">点击预览</x-button<>
<div< span="">
v-if="loginImg"
class="code"></div<>
请先登录
<img< span="">
class="code-img" :src="loginImg"
alt=""></img<>
<div< span="">
v-if="preImg"
class="code"
id="preImg"></div<>
预览二维码
<img< span="">
class="code-img" :src="`${base64Prefix}${preImg}`"
alt=""></img<>
<style< span="">
lang='less'></style<>
.btn {
width:
90%!important;
margin:
30px
auto
30px
auto;
}
.code {
display: flex;
align-items: center;
flex-direction: column;
.code-img {
width:
300px;
height:
300px;
}
}
这里有一个坑就是,login返回的base64是带了 data:image/jpeg;base64,
前缀的,所以可以直接放到img的src里,但是获取预览码的preview返回的却没有这个前缀!所以需要自己加上去,就是那个 base64Prefix:'data:image/png;base64,'
其实到这里已经基本实现了整个打码功能,但如果真的要可以用还有很多事情没做。