Serverless + Deno!极速上线女友嘘寒问暖 Bot

2020-12-11 10:40:19 浏览数 (1)

作者:胡琦 | GitHub:hu-qi/deno-serverless 很久很久没有提笔写东西了,也意味着很久很久没有瞎折腾 Copy 大法了。我是谁?我是谁并不重要,江湖肯定没有 Copy 攻城狮的传说,不过,也许这是一篇真情露出的踩坑文。以前,听说过「If I have seen further,it is by standing on the shoulders of giants.」,而此刻我正站在 Ryan Dahl[1]乂乂又又[2]的肩膀上,体验万物皆可 Serverless 的 Serverless Deno ,从零到一开(kao)发(bei)然并卵的铝盆友彩虹屁 bot。

伪需求分析

  1. 最好的爱情就是我知 TA 冷暖,我懂 TA 心意 —— 定时天气预报外加心灵鸡汤;
  2. 最好的陪伴就是安心地和 TA 一起倒数最重要的日子 —— 倒计时提醒;
  3. 最好的心情就是 TA 每天第一次睁开眼睛看到的是我的问候,夕阳下在我的晚安声中进入梦乡 —— 早安晚安问候;
  4. 当然最重要的是学习了解一下新鲜事物,比如 Deno、比如 Serverless。

实现构想

  1. 缘起于大佬的创意和代码实现,所以代码不用考虑太多,照搬就行;
  2. 邮箱服务直接 Github 搜一波,现在的年轻人不讲武德,什么源码等等通通一股脑丢到 Github,我也想康康;
  3. 再想下代码实现,涉及到日期时间计算、邮件发送,是不是得找个巨佬的肩垫垫脚?插件拿过来就是刚!
  4. 怎么部署呢?稍微对比了一下,就鹅厂云了,好像几个月前就支持 Deno 部署了,应该比较成熟。
  5. 最后, Just Do IT!

热气腾腾

看小标题是不是猜到什么恶心的东西了?是的,正是在下!本大狮,历经九九八十一分钟(实际折腾了一宿,主要卡在 Serverless 部分了),翻阅了多处 API 文档,几经波折之后,新鲜代码出来了:

代码语言:javascript复制
/*
* Copyer huqi
* https://github.com/hu-qi
*/
import * as log from "https://deno.land/std@0.79.0/log/mod.ts";
import { SmtpClient } from "https://deno.land/x/smtp/mod.ts";
import {
  differenceInDays,
  format,
} from "https://deno.land/x/date_fns@v2.15.0/index.js";
import { zhCN } from "https://deno.land/x/date_fns@v2.15.0/locale/index.js";
import "https://deno.land/x/dotenv/load.ts";

// 很随意的入参,来自.env
const {
  SEND_EMAIL,
  PASSWORD,
  RECV_EMAIL,
  NAME_GIRL,
  CITY,
  CUTDOWNDATE,
  CUTDOWNTHINGS,
} = Deno.env.toObject();

// 很随意的API,来自掘金
const URL = {
  weather: `http://wthrcdn.etouch.cn/weather_mini?city=${CITY}`,
  soup: "https://www.iowen.cn/jitang/api/",
  pi: "https://chp.shadiao.app/api.php",
};

// 先配置下邮箱服务,管他行不行
const client = new SmtpClient();

const connectConfig: any = {
  hostname: "smtp.163.com",
  port: 25,
  username: SEND_EMAIL,
  password: PASSWORD,
};

// 姑且认为返回的都是结构数据
async function _html(url: string): Promise<string> {
  return await (await fetch(url)).text();
}

// 目标城市的天气
async function getWeather(url: string) {
  let data = await _html(url);
  if (data.indexOf("OK") > -1) {
    let _data = JSON.parse(data).data;
    const { ganmao, wendu, forecast } = _data;
    const weather = forecast[0].type;
    return `天气:${weather} 当前温度:${wendu}
          ${ganmao}`;
  } else {
    return "亲爱的,今天天气真奇妙!";
  }
}

// 倒计时
function getTime() {
  const today = format(new Date(), "PPPP", { locale: zhCN });
  const days = differenceInDays(new Date(CUTDOWNDATE), new Date());

  return `今天是 ${today} ${CUTDOWNTHINGS}倒计时:${days}天`;
}

// 心灵鸡汤
async function getSoup(url: string) {
  let data = await _html(url);
  if (data.indexOf("数据获取成功") > -1) {
    let _data = JSON.parse(data).data;
    const { content } = _data.content;
    return content;
  } else {
    return `高考在昨天,${CUTDOWNTHINGS}在明天,今天没有什么事儿!`;
  }
}

// 彩虹?屁?
async function getPi(url: string) {
  let data = await _html(url);
  return data.length > 3 ? data : "你上辈子一定是碳酸饮料吧,为什么我一看到你就开心的冒泡";
}

// 早安
async function morning() {
  return `
          <p>${getTime()}</p>
          <p>${await getSoup(URL.soup)} </p>
          <p>${await getWeather(URL.weather)} </p>
          <p>${await getPi(URL.pi)}</p>
      `;
}

// 晚安
async function ngiht() {
  return `
          <p>${await getSoup(URL.soup)} </p>
          <p>${await getPi(URL.pi)} </p>
          <p>晚安,${NAME_GIRL}同学,今天你也是最棒的,继续加油鸭!</p>
      `;
}

// 日期插件有点屌
function getTimeX() {
  // 返回 “上午” 或者 “下午”
  return format(new Date(), "aaaa", { locale: zhCN });
}

// 入口函数
async function main_handler() {
  // 邮件正文
  const content = getTimeX() === "上午" ? await morning() : await ngiht();
  // 邮件标题
  const greeting = getTimeX() === "上午"
    ? `早安, ${NAME_GIRL}`
    : `晚安,${NAME_GIRL} `;

  // "及时关注可能会发生的错误"
  try {
    await client.connect(connectConfig);
    await client.send({
      from: SEND_EMAIL,
      to: RECV_EMAIL,
      subject: greeting,
      content: content,
    });
    await client.close();
    log.info("send email success");
  } catch (error) {
    // "现在开始执行B计划",
    // "与其关心程序的异常,不如多关注下身边的女孩子吧"
    log.error(error);
    log.info("Error: send email fail");
  }
  log.info(content);
  return content;
}

// 立即执行(宫刑?)
main_handler();

不得不感叹 Deno 的生态真牛掰,想用什么插件就有什么插件,刚好满足了上边这么多需求。像这个日期库,十分丰富,无论是日期格式化、国际化还是日期常用的函数等等,考虑得很周到,像这么好用的插件,Copy 攻城狮就别学了,我是学不会的,这辈子都不可能学会的。

冰封万里

第一个夜晚叫初夜,第一场雪叫初雪,新闻上说这几天全国很多地方迎来了初雪,我在广州也感受到了阵阵寒意,昨晚感觉像露宿街头,冬风呼呼地吹,似乎在嘲笑我弱不经吹的技术,啪啪啪地扇了我一整宿……还好,经过腾讯云工程师的指点,我如梦初醒,终于走出了“千里冰封,万里雪飘”,迎来了部署成功的喜悦。

先说说部署 Deno 云函数大概的流程:

  1. 首先明确一点,腾讯云云函数支持 Deno 应用部署,所以我们通过新建云函数来部署 Deno;
  2. 在新建云函数的时候,我们先选择模板函数-Deno 创建,主要是因为我们需要官方模板提供的代码和 deno 以及 bootrap 这两个命令工具;然后不用修改,直接把模板代码下载到本地,等下我们把大的文件如 deno 放到云函数的「层」里面;
  3. 将代码(环境)下载到本地,把大的文件作为上传(其实把名为 deno 的文件单独作为层就够了,占了 50 多 M,也可以把一些 Deno 的依赖包再放到层了),然后把剩余的文件上传到函数代码
  4. 需要注意一点,云函数要有返回才能算调用成功(尽管调用失败也能执行入口函数,但是一直是超时的报错),经排查,加上官方模板中关于 event 触发的一系列代码就能正常调用了,
  5. 一个很实用的地方是环境变量,云函数函数配置中设置的环境变量键值对,在代码中能通过Deno.env.toObject()捕获到;当然测试事件中的传参在官方模板提供的代码中也能捕获到,这样就做到了简单的可配置,改下环境变量或者输出的事件参数,我就能给其他“铝盆友”发送暖心的邮件了,甚至还可以一次配置 10 个“铝盆友”,同时发送邮件,“爱拼才会赢”!

有图有真相

为了填这些“坑”,我差点跟鹅厂的工程师怼上了,还好不是大佬的 bug,不然我也不讲武德,在大佬的倾情讲解和耐心解答下,我也只能耗子尾汁,悻悻离去!幸好有云平台的工单系统,还能和各个大厂的工程师进行“攻城狮和工程师的交流”。

“怼”腾讯工程师

为了避坑,我去掉了最后那行立即执行的函数,加入了官方模板中的如下代码,看样纸是捕获触发函数参数的:

代码语言:javascript复制
// do initialize
const scf_host: string | undefined = Deno.env.get("SCF_RUNTIME_API");

const scf_port: string | undefined = Deno.env.get("SCF_RUNTIME_API_PORT");

const func_name: string | undefined = Deno.env.get("_HANDLER");

const ready_url = `http://${scf_host}:${scf_port}/runtime/init/ready`;

const event_url = `http://${scf_host}:${scf_port}/runtime/invocation/next`;

const response_url =
  `http://${scf_host}:${scf_port}/runtime/invocation/response`;

const error_url = `http://${scf_host}:${scf_port}/runtime/invocation/error`;

// post ready -- finish initialization
console.log(`post ${ready_url}`);

postData(ready_url, { msg: "deno ready" }).then((data) => {
  console.log(`Initialize finish`);
});

async function processEvent(evt='') {
  if (evt.length === 0) {
    postData(error_url, {msg: "error handling event"}).then(data => {
      console.log(`Error response: ${data}`);
    });
  } else {
    postData(response_url, {msg:`finish process event`}).then(data => {
      console.log(`invoke response: ${data}`);
    });
  }
}

// Example POST method implementation:
async function postData(url = '', data = {}) {
  // Default options are marked with *
  const response = await fetch(url, {
    method: 'POST', // *GET, POST, PUT, DELETE, etc.
    body: JSON.stringify(data) // body data type must match "Content-Type" header
  });
  return response.text(); // parses JSON response into native JavaScript objects
}

while (true) {
  // get event
  // 立即执行改判si缓
  const responseEmail = await main_handler();
  const response = await fetch(event_url);
  response.text().then(function(text) {
    console.log(`get event: ${text}`);
    processEvent(text);
  });
}

值得提一下官方模板提供的文件,请看截图,罪大恶极的就是这个deno文件,50 多 M 大小导致无法友好地修改在线代码:

Deno 云函数模板

此次部署能得以成功,这里处理得当时第一步,我的理解是大文件如 NodeJS 的 node_modules 之类的文件有必要放到里,理论上 Deno 的依赖包也是同理,好在 Deno 依赖比较轻量。

其次,根据官方文档“层中的文件将会添加到 /opt 目录中,此目录在函数执行期间可访问”,我们将启动文件稍作修改:

此外,就是我们的“铝盆友”配置啦,入参随心所欲了,看您想怎么用就怎么定义,完事了代码里接一下就 OK:

配置铝盆友

最后附上源码,欢迎指教: hu-qi/deno-serverless

参考资料:

[1]

Ryan Dahl: https://github.com/ry

[2]

乂乂又又: https://juejin.cn/user/2418581313189326

One More Thing

立即体验腾讯云 Serverless Demo,获取 Serverless 新用户礼包,请在 PC 端访问: serverless.cloud.tencent.com/start?c=wx

欢迎进入千人 QQ 群 (537539545) 交流!

  • GitHub: github.com/serverless
  • 官网: cloud.tencent.com/product/serverless-catalog

没看过瘾?点击「阅读原文」进入 Serverless 中文网,体验更多 Serverless 应用的最佳实践!

0 人点赞