很久很久没有提笔写东西了,也意味着很久很久没有瞎折腾 Copy 大法了。我是谁?我是谁并不重要,江湖肯定没有 Copy 攻城狮的传说,不过,也许这是一篇真情露出的踩坑文。以前,听说过“If I have seen further,it is by standing on the shoulders of giants.”,而此刻我正站在Ryan Dahl乂乂又又的肩膀上,体验万物皆可 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 文档,几经波折之后,具有辣眼睛的新鲜代码出来了:

/*
* 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. 首先明确一点,腾讯云云函数和云开发 CloudBase 都支持 Deno 应用部署,经过摸索,我认为当前的这个 bot 更适合云函数,所以我们通过新建云函数来部署 Deno;
  2. 在新建云函数的时候,我们先选择模板函数-Deno 创建,主要是因为我们需要官方模板提供的代码和 deno 以及 bootrap 这两个命令工具;然后不用修改,直接把模板代码下载到本地,等下我们把大的文件如 deno 放到云函数的层里面,因为这里有个“巨坑”--官方模板代码及命令行工具总大小超过了云函数在线编辑模式所要求的 10M,所以不支持在线编辑(好比自动给我们生成了环境和代码,但是没法直接修改);
  3. 鉴于这个“巨坑”,我们想到的办法是将代码(环境)下载到本地,把大的文件作为上传(其实把名为 deno 的文件单独作为层就够了,占了 50 多 M,也可以把一些 Deno 的依赖包再放到层了),然后把剩余的文件上传到函数代码基本就能避坑;
  4. 还有一个“坑”,是我技术不到家,不了解云函数的相关知识,好像是这个云函数要有返回才能算调用成功(尽管调用失败也能执行入口函数,但是一直是超时的报错),经排查,加上官方模板中关于 event 触发的一系列代码就能正常调用了,
  5. 另外一个感觉很实用的地方就是环境变量,云函数函数配置中设置的环境变量键值对,在代码中能通过Deno.env.toObject()捕获到;当然测试事件中的传参在官方模板提供的代码中也能捕获到,这样就做到了简单的可配置,改下环境变量或者输出的事件参数,我就能给其他“铝盆友”发送暖心的邮件了,甚至还可以一次配置 10 个“铝盆友”,同时发送邮件,“爱拼才会赢”!

没图说个

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

“怼”腾讯工程师

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

// 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:

配置铝盆友

不过,最终遇到时区的问题,只能暂时放一放了:

时区问题

希望各位大佬能解答一下!

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


huqi
833 声望15 粉丝

我以为的就是我以为的?