作者:胡琦 | 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。
伪需求分析
- 最好的爱情就是我知 TA 冷暖,我懂 TA 心意 —— 定时天气预报外加心灵鸡汤;
- 最好的陪伴就是安心地和 TA 一起倒数最重要的日子 —— 倒计时提醒;
- 最好的心情就是 TA 每天第一次睁开眼睛看到的是我的问候,夕阳下在我的晚安声中进入梦乡 —— 早安晚安问候;
- 当然最重要的是学习了解一下新鲜事物,比如 Deno、比如 Serverless。
实现构想
- 缘起于大佬的创意和代码实现,所以代码不用考虑太多,照搬就行;
- 邮箱服务直接 Github 搜一波,现在的年轻人不讲武德,什么源码等等通通一股脑丢到 Github,我也想康康;
- 再想下代码实现,涉及到日期时间计算、邮件发送,是不是得找个巨佬的肩垫垫脚?插件拿过来就是刚!
- 怎么部署呢?稍微对比了一下,就鹅厂云了,好像几个月前就支持 Deno 部署了,应该比较成熟。
- 最后, 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 云函数大概的流程:
- 首先明确一点,腾讯云云函数支持 Deno 应用部署,所以我们通过新建云函数来部署 Deno;
- 在新建云函数的时候,我们先选择模板函数-Deno 创建,主要是因为我们需要官方模板提供的代码和 deno 以及 bootrap 这两个命令工具;然后不用修改,直接把模板代码下载到本地,等下我们把大的文件如 deno 放到云函数的「层」里面;
- 将代码(环境)下载到本地,把大的文件作为层上传(其实把名为 deno 的文件单独作为层就够了,占了 50 多 M,也可以把一些 Deno 的依赖包再放到层了),然后把剩余的文件上传到函数代码;
- 需要注意一点,云函数要有返回才能算调用成功(尽管调用失败也能执行入口函数,但是一直是超时的报错),经排查,加上官方模板中关于 event 触发的一系列代码就能正常调用了,
- 一个很实用的地方是环境变量,云函数函数配置中设置的环境变量键值对,在代码中能通过
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 应用的最佳实践!