一步一步踩坑实现微信小程序自动预览

4,511 阅读8分钟

在最近的工作中,主要做的是C端的小程序,在开发过程中倒是还好,一到改BUG的时候,可能就会进入保存,编译,自动预览的循环了,虽然微信已经提供了快简介自动预览,但是总归是一个手动的过程,要是能监听文件变化自动预览不是更好吗?

查看文档

于是我就去看了下文档,发现微信已经提供了这个API,可以用命令行调用,也可以用HTTP调用

一开始本着配置少一点的原则,我选择了HTTP调用,因为命令行调用需要配置微信开发者工具的安装目录,而HTTP调用可以通过微信指定的路径获取端口号(实践证明,too young, too simple, 认真你就输了,HTTP调用的问题太多,所以我后来又改成了命令行调用)。

然后就去官方所指示的ide文件夹找端口号了,结果并没有发现那个文件。怎么回事呢?一顿搜索之后,才知道目前的版本需要如下才能开启http服务: 微信开发者工具 -> 查看所有项目 -> 设置 -> 安全,里面有个服务端口,选择开启以后就能找到ide文件,进而得到端口号进行后续操作了。

前置工作

  1. commander,主要是从命令行读取参数,实现动态配置,方便调用。
  2. chalk 控制台输出格式多样化。
  3. axios 用于发起http请求。
  4. package.json中的bin字段,用于实现自定义命令。
  5. 路径区分, process.cwd()执行命令时所在的目录,__dirname执行文件所在的目录。 导入配置文件主要是用前者。
  6. 使用 fs.watch进行目录和文件的监听,在文件变动后调用自动预览

读取端口号

文档中提到 端口号文件位置:

macOS : ~/Library/Application Support/微信开发者工具/Default/.ide Windows : ~/AppData/Local/微信开发者工具/User Data/Default/.ide

既然路径有了,那么就好说了,只要获取到用户目录,然后再拼接上不同平台的后续路径,那么通过读取.ide文件就可以得到端口号了

代码如下:

const fs = require("fs");
const os = require("os");
const isWin = os.platform() === `win32`;

function getHttpPort() {
  const home = os.homedir();
  const suffix = isWin
    ? `/AppData/Local/微信开发者工具/User Data/Default/.ide`
    : `/Library/Application Support/微信开发者工具/Default/.ide`;
  const idePath = home + suffix;

  const port = fs.readFileSync(idePath, { encoding: "utf8" });

  return port;
}

打开工具或指定项目

HTTP调用

实例:

# 打开工具
http://127.0.0.1:端口号/open
# 打开/刷新项目
http://127.0.0.1:端口号/open?projectpath=项目全路径

先在浏览器中直接进行访问,很好,你会发现微信开发者工具并没有打开,再仔细看了下文档,用法肯定没错的,总共就端口号和项目路径两个变量,怎么可能会出错呢?被某厂坑久了,就知道有问题是必然的。

算了,打不开就打不开吧,影响并不是很大,反正平时开发的时候,开发者工具都是打开的。这个问题呢,我猜测是因为工具每次打开端口号都会变化,而读取的端口号是之前的,所以就没用了。

命令行调用

-o, --open [projectpath]: 打开工具,如果不带 projectpath,只是打开工具。如果带 project path,则打开路径中的项目,每次执行都会自动编译刷新,并且自动打开模拟器和调试器。projectpath 不能是相对路径。项目路径中必须含正确格式的 project.config.json 且其中有 appid 和 projectname 字段。

调用:

cli -o /Users/username/demo

这次的调用完全没有问题的。

自动预览

HTTP调用

接口定义:

URL:/autopreview

HTTP 方法:GET

URL 参数 必填 说明
projectpath 指定路径中的项目。如项目已打开,自动刷新项目。如项目未创建,自动创建并自动预览项目
infooutput 指定后,会将本次自动预览的额外信息以 json 格式输出至指定路径,如代码包大小、分包大小信息。
compilecondition 指定自定义编译条件,值为 json 字符串,条件可指定两个字段,pathName 表示打开的页面,不填表示首页,query 表示页面参数

这个接口请求以后,倒是可以用,但是坑也不小。

  1. infooutput 这个参数我写了,却并没有生成相应的文件。
  2. compilecondition 这个参数其实是我从预览那个接口凑过来的,官方文档只有2个参数。本来我也没想到这个参数的,但是没办法,我们的项目首页是pages/home/index,自动预览以后可能是没找到pages/index/index这种默认的首页,然后就随便跳了一个?

文档总是缺三少四的,这里一块那里一块,稍微看漏一点这个功能可能就出不来了,说到底看文档就是得仔细。

命令行调用

自动预览必须处于登录状态,如果没有登录,会提示需先登录。

--auto-preview <project_root>: 自动预览代码,project_root 指定项目根路径。

--auto-preview-info-output <path>: 指定后,会将本次预览的额外信息以 json 格式输出至指定路径,如代码包大小、分包大小信息。


官方文档只介绍了这两个参数,还是像之前一样,从预览那个的调用凑一下吧。

--compile-condition '<json>': 指定自定义编译条件,json 条件可指定两个字段,pathName 表示打开的页面,不填表示首页,query 表示页面参数

用命令行测试:

cli --auto-preview /Users/username/demo --compile-condition {\"pathName\": \"pages/home/index\",\"query\":\"a=1\"}

大坑

  1. 命令行调用的时候编译条件应该是调用JSON.stringify(obj).replace(/\"/g, `\\"`)所获得的字符串,注意要把"换成\",且前后不需要单引号,文档里面那种调用方式会报错,不信的小伙伴可以自己尝试。
  2. 查询参数query只能有一个参数,如果用&链接多个参数的话,提示信息是成功,但是手机并没有自动预览,害得我还纳闷了好久。这个问题必须得给官方反应一下,太影响使用了。

node脚本编写思路

辅助函数

// 将D:\\www\\soft转换成D:/www/soft这种形式
// msg为路径不存在时的报错信息
function getPath(projectpath, msg) {
  if (!projectpath) {
    throw new Error(msg);
  }
  return projectpath.split(path.sep).join(`/`);
}

// 获取打包路径
function getDist(config = {}) {
  const { projectpath } = config;
  return (
    config.dist ||
    (/dist\/?$/.test(projectpath) ? projectpath : (projectpath || "") + `/dist`)
  );
}

// 将exec转为promise类型的函数,方便使用async await
const promisify = require("util").promisify;
let { exec } = require("child_process");
exec = promisify(exec);

这里介绍一下exec

child_process.exec(command[, options][, callback])
参数 类型 说明
command string The command to run, with space-separated arguments. (需要运行的命令,参数用空格分开)
options object { cwd: "子进程工作目录,默认为null", }
其他参数我没怎么用过
callback 回调函数: (error: Error, stdout: string | Buffer, stderr: string | Buffer)

spawn和exec的却别在于,spawn的输出是实时的,而exec是执行完之后统一返回。前者还得监听事件略显麻烦,所以我选择了exec。

open.js

async function open({ projectpath, cli, }) {
  return new Promise(async (resolve, reject) => {
    log();
    log(chalk.green(`打开开发者工具中...`));
    const result = await exec(
      `cli -o ${projectpath}`,
      {
        cwd: cli
      }
    );

    const isSuccess = result.stdout;
    log(isSuccess ? chalk.green(`打开成功`) : chalk.red(`打开失败`));
    isSuccess ? resolve() : reject();
  })
}

preview.js

async function preview(config) {
  await open(config);

  const port = getHttpPort();
  const { dist, projectpath, time, compile, cli } = config;

  log();
  log(chalk.blue(`开始监听文件变动`));
  log(`路径参数: `, compile);

  fs.watch(
    dist,
    debounce(async (evt, filename) => {
      log();
      console.log(`${filename} ${evt}`);

      log(chalk.green(`自动预览重启中...`));
      log(
        `执行命令: cli --auto-preview ${projectpath} --compile-condition ${compile}`
      );
      const result = await exec(
        `cli --auto-preview ${projectpath} --compile-condition ${compile}`,
        {
          cwd: cli
        }
      );

      const isSuccess = result.stdout;
      log(isSuccess ? chalk.green(`自动预览成功`) : chalk.red(`自动预览失败`));
    }, time || 1000)
  );
}

lib/index.js

const fn = {
  preview,
  upload,
};

function run(config, type) {
  if (!fn[type]) {
    throw new Error(`type参数不合法,请确保为preview, upload的一种`);
  }

  log(chalk.green(`本次启动类型为: ${type}`));
  fn[type](config);
}

config参数从命令行参数中读取文件获得,type参数为命令行参数。

bin/index.js

#! /usr/bin/env node
const run = require("../lib/index");
const program = require("commander");
const fs = require("fs");
const path = require("path");
const { getPath, getHttpPort, getDist } = require("../util/index");

program
  .option("-c, --config <type>", "config file", "auto.js")
  .option("-t, --type <type>", "auto type, etc: preview, upload", "preview")
  .parse(process.argv);

// 获取执行命令时所在的目录,拼接上配置文件目录,使用require(ConfigFile)即可获得相关配置
const CD = process.cwd();
const Config = program.config;
const ConfigFile = path.join(CD, Config);

const defaultCompile = {
  pathName: `pages/home/index`
};

// 判断配置文件不存在,则直接报错
if (!fs.existsSync(ConfigFile)) {
  throw new Error(`[ERROR]: ${Config} not found in ${CD}`);
} else {
  start();
}

function start() {
  let config = require(ConfigFile);
  config = { ...config };
  config.projectpath = getPath(
    config.projectpath,
    `配置文件中projectPath字段必须有值`
  );
  const projectpath = config.projectpath;
  config.dist = getDist(config);
  config.compile = JSON.stringify(config.compile || defaultCompile).replace(
    /\"/g,
    `\\"`
  );
  // 这里可写可不写,打开工具后会进行再次获取
  config.port = getHttpPort();

  run(config, program.type);
}

至此,该功能已经大致完成了,我已经上传到了npm,可以直接进行使用。

wx-auto

微信开发者工具,HTTP调用,自动预览,提高工作效率

局部安装

npm i wx-auto -D 或者 yarn add wx-auto -D

全局安装

npm i wx-auto -g 

使用方法

wxauto
或者
wxauto -t preview -c auto.js

参数(均有默认值)

-t, --type 类型,目前支持preview和upload
-c, --config 配置文件名,默认值为auto.js

文件路径为相对路径,相对于执行命令时所在的路径

配置文件

{
  cli: "D:/soft/微信web开发者工具", // cli文件所在的目录
  projectpath: `D:/www/react/heywoof-app-frontend`, // 项目地址
  compile: {
    pathName: `pages/scene/index`, // 自动预览的页面路径
    query: `activityId=5d45050569515b000c5b740a` // 查询参数,微信目前有BUG,只能识别一个参数
  },
  build: `yarn build-test:weapp`, // 上传之前需要执行的命令
  upload: {
    version: "1.0.1",
    desc: "测试自动上传,不要乱动"
  }
};

此外还包含了自动打包上传的功能,配置之后执行wxauto -t upload -c auto.js,上传完毕后就会自动打开微信公众平台登录的网站,个人感觉还是挺实用的。

自动化测试

在查看文档的时候,我发现微信官方推出了一个多端统一开发工具——kbone,这个是基于vue的,就是配置略显麻烦。

除此之外,微信小程序现在已经支持自动化测试了,感兴趣的小伙伴可以自行尝试。注意:该功能需要最新版本支持,一定要符合文档所说的版本,我简单尝试了一下,自动化是可以实现的,更具体的测试就得看工作需要了。