Contents

typescript版每日一句插件

最近的任务是写一个每日一句的wechaty插件,目前的构思是先使用padplus token写出typescript版本的插件,之后再写python版本的,正好今天看到了很棒的参考项目,因此希望能迅速搭建一个简单的项目框架出来,再予以完善。

1. 项目环境的搭建

1.1 安装node

注意的是wechaty要求的node版本大于12,具体安装方法不做赘述。

1.2 安装typescript

1
2
npm install -g typescript
tsc -v

我安装的版本是Version 3.9.6

1.3 创建项目

1
npm init -y # 创建一个 package.json


使用wechaty社区统一的tsconfig

1
2
npm install eslint@^5.0.0
npm install --save-dev @chatie/tsconfig
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{
  "extends": "@chatie/tsconfig",
  "compilerOptions": {
    "outDir": "dist",
  },
  "exclude": [
    "node_modules/",
    "dist/",
    "tests/fixtures/",
  ],
  "include": [
    "app/**/*.ts",
    "bin/*.ts",
    "bot/**/*.ts",
    "examples/**/*.ts",
    "scripts/**/*.ts",
    "src/**/*.ts",
    "tests/**/*.spec.ts",
  ],
}

1.4 使用eslint规范代码

使用wechaty社区统一的eslint配置,其它项目的配置可以参考Using ESLint and Prettier in a TypeScript Project这篇文章

1
npm install --save-dev @chatie/eslint-config

会自动生成.eslintrc.js文件

const rules = {
}

module.exports = {
  extends: '@chatie',
  rules,
}

1.5 使用git-scripts

1
npm install --save-dev @chatie/git-scripts

使用npm pre-push可以实现git push前的代码标准化、包的自动升级等工作

1.6 检查版本

1
npm install --save-dev @chatie/semve

2. 语料的获取

使用axios进行异步的http请求,请求的结果为json格式,也就是javascript中的object,根据传入的参数列表提取出指定的值,返回为字符串列表。(待完成:根据json路径解析得到内容)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 根据url和参数制定爬取内容
async function getJsonData(url: string, params: string[]) {
  let result: string[] = [];
  let response: any = await axios.get(url);
  if (response.status == 200) {
    params.forEach((key) => {
      let data = response.data;
      if (key in data) {
        if (data[key] instanceof Array) {
          data[key] = data[key][0]; //返回第一个匹配的
        }
        result.push(data[key]);
      }
    });
  }
  return result;
}

3. 图片的生成

使用nodejs的gm库调用imagickMagick工具,对于爬取到的信息以及用户的头像、用户名还有当前的时间生成图片。

3.1 获取一对颜色

根据Cayman的建议,
获得一对随机颜色和其互补颜色,在此使用了randomcolor包来保证随机颜色的质量,使用16进制的位与运算生成其互补颜色

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
//生成互补随机颜色
const randomColor = require("randomcolor");
function generateColors(): [string, string] {
  let colors: [string, string] = ["", ""];
  colors[0] = randomColor({hue: 'random',luminosity: 'random'});
  console.log(" color is ", colors[0]);
  colors[1] =
    "#" +
    (
      "000000" +
      (0xffffff ^ parseInt(colors[0].replace("#", "0x"))).toString(16)
    ).slice(-6);
  return colors;
}

3.2 使用gm生成图片

gm可以在nodejs中调用GraphicsMagick或者ImageMagick,在使用之前需要安装依赖,我使用的是ImageMagick。

3.2.1 安装ImageMagick

Manjora
1
yay install imagemagick
MacOS
1
2
brew install imagemagick
brew install ghostscript

3.2.2 调用gm处理图片

安装好了之后就可以根据官方文档进行使用了,值得注意的是如果安装的是ImageMagick,引入gm包时要设置一下

1
gm = require('gm').subClass({imageMagick: true});

一个简单的图像处理函数如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//根据头像、用户名、爬取的文字生成图片,保存到指定位置
export async function generateImg(
  //   oriPath: string,
  savePath: string,
  avatarPath: string,
  userName: string,
  callback: ICallback
) {
  let colors: [string, string] = generateColors();
  gm("img/front.png")
    .background(colors[0]) //背景颜色
    .mosaic() // 合成图层
    .draw(`image over 800,150 100,100 "${avatarPath}" `) //叠加图片
    .fontSize(68) //字体大小
    .font("font/经典隶变简.ttf") //字体
    .fill(colors[1]) // 字体颜色
    .drawText(0, 400, userName, "Center") // 添加文字
    .quality(100) //质量最高
    .write(savePath, async function (err: any) { //进行保存,返回Promise
      if (!err) {
        console.log("finished");
        callback(savePath);
      } else {
        console.log("failed");
        callback('');
      }
    });
}

4. 异步与回调

4.1 回调函数

在编写这个插件的过程中,有很多需要等待一定时间来完成的步骤,比如图片的下载保存等,如果不进行设置的话一些过程是异步执行的,如果想在这个过程结束后进行下一个过程,可以使用回调函数,比如说在图片生成完成之后进行发送的函数可以这么写:

调用时候传入一个匿名函数作为回调函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
generateImg(path,avatarPath,name, async(sendImg: string) => {
  if (sendImg.length > 0 ) {
    try{
      const imgFile = FileBox.fromFile(sendImg);
      if (room) {
        await room.say(imgFile);
      } else {
        await contact.say(imgFile);
      }
    } catch(e){
      console.log("error is " + e);
    }
  }
});

generateImg函数则在判断文件写入成功后执行回调函数

4.2 promise

以上的代码可读性较差,可以使用Promise进行改写,MDN对于Promise的解释是:

Promise对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象。

Promise有以下三种状态,初始状态为pending

  • pending: 初始状态,既不是成功,也不是失败状态。
  • fulfilled: 意味着操作成功完成。
  • rejected: 意味着操作失败。

当状态转换到fulfilled或者rejected时,说明异步过程已经结束,可以调用then方法绑定的处理方法进行后续处理。
基本语法如下:

1
2

new Promise( function(resolve, reject) {...} /* executor */  );

Promise 对象是由关键字 new 及其构造函数来创建的。该构造函数会把一个叫做“处理器函数”(executor function)的函数作为它的参数。
这个“处理器函数”接受两个函数: resolve 和 reject 作为其参数。
executor 内部通常会执行一些异步操作,当异步任务顺利完成且返回结果值时,会调用 resolve 函数,promise状态改成fulfilled;
而当异步任务失败且返回失败原因(通常是一个错误对象)时,会调用reject 函数,该promise 状态为rejected。
Promise不论成功或失败都会调用then方法,可以在then方法中对于返回值进行判断,从而进行不同的后续步骤,catch只有当 promise 失败时才会调用。

异步下载文件的函数定义如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 下载文件到本地
export async function downloadFile(url: string, localPath: string) {
  if (fs.existsSync(localPath)) {
    console.log("file existed: ", localPath);
    return new Promise((resolve: any) => {
      resolve("success");
    });
  } else {
    const response = await axios({
      url,
      method: "GET",
      responseType: "stream",
    });
    let writer = fs.createWriteStream(localPath);
    response.data.pipe(writer);
    return new Promise((resolve: any, reject: any) => {
      writer.on("finish", resolve("success"));
      writer.on("error", reject);
    });
  }
}

4.3 await

await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成。若 Promise 正常处理(fulfilled),其回调的resolve函数参数作为 await 表达式的值,继续执行 async function。
若 Promise 处理异常(rejected),await 表达式会把 Promise 的异常原因抛出。

之前定义的函数,调用时的语句如下:

1
await downloadFile(contact.payload.avatar, avatarPath);

5.总结

最终效果如下:

总的过程收获很大,接触了新的不太熟悉的语言,VScode有让我真香了,然而断点调试我还是不太会用,用的webstorm进行。接下来想:

  • 继续完善更多的功能
  • 写python版本
  • 打包成nodejs包
  • 使用github进行自动构建

参考

  1. 使用 VS Code 搭建 TypeScript 开发环境 https://www.cnblogs.com/mybilibili/p/11601970.html
  2. 使用Typescript编写和发布npm包 https://www.jianshu.com/p/8fa2c50720e4
  3. 基于typescript发布npm包的流程 https://segmentfault.com/a/1190000019930922
  4. https://typescript-eslint.io/
  5. Wonderful Wechaty devops toolset https://wechaty.github.io/2020/06/20/wonderful-wechaty-devops-tools/
  6. Using ESLint and Prettier in a TypeScript Project https://www.robertcooper.me/using-eslint-and-prettier-in-a-typescript-project
  7. 使用 TypeScript 编写爬虫 https://zhuanlan.zhihu.com/p/74374510
  8. randomColor 项目地址 https://github.com/davidmerfield/RandomColor
  9. gm项目地址 https://github.com/aheckmann/gm
  10. Typescript 异步编程范式 Await/Async Deferred/Promise https://blog.csdn.net/cfarmerreally/article/details/79935077
  11. Promise - JavaScript | MDN https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise
  12. 使用 Promise https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Using_promises
  13. https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/await
  14. https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/async_function