萬物皆可 Serverless 之使用 SCF+COS 給將來寫封信

或許你有用過或者據說過《給將來寫封信》,這是由全知工坊開發的一款免費應用,你能夠在此刻給本身或他人寫下一封信,而後選擇在將來的某一天寄出,想必那時收到信的人看着這封來自過往的信時必定會十分感動吧。javascript

本文來自 Serverless 社區用戶「乂乂又又」供稿

此次我就帶你們一塊兒來使用無服務器雲函數 SCF 和對象存儲 COS,快速開發一個屬於本身的「給將來寫封信」應用。html

效果展現

寫下一封信,而後投遞:前端

寫下一封信,而後投遞

一封來自好久之前的信:java

一封來自好久之前的信

寫給將來的本身python

寫給將來的本身

你也能夠訪問letter.idoo.top/letter來親自體驗一下(僅供測試之用,不保證服務一直可用)git

操做步驟

第一步:新建 python 雲函數

參見我以前的系列文章《萬物皆可 Serverless 之使用 SCF+COS 快速開發全棧應用》github

第二步:編寫雲函數

Life is short, show me the code.

老規矩,直接上代碼express

import json
import datetime
import random
from email.mime.text import MIMEText
from email.header import Header
import smtplib

# 是否開啓本地debug模式
debug = False

# 騰訊雲對象存儲依賴
if debug:
    from qcloud_cos import CosConfig
    from qcloud_cos import CosS3Client
    from qcloud_cos import CosServiceError
    from qcloud_cos import CosClientError
else:
    from qcloud_cos_v5 import CosConfig
    from qcloud_cos_v5 import CosS3Client
    from qcloud_cos_v5 import CosServiceError
    from qcloud_cos_v5 import CosClientError

# 配置存儲桶
appid = '66666666666'
secret_id = 'xxxxxxxxxxxxxxx'
secret_key = 'xxxxxxxxxxxxxxx'
region = 'ap-chongqing'
bucket = 'name'+'-'+appid

#配置發件郵箱
mail_host = "smtp.163.com"
mail_user = "xxxxxxxxxx@163.com"
mail_pass = "xxxxxxxxxxxxxx"
mail_port = 465

# 對象存儲實例
config = CosConfig(Secret_id=secret_id, Secret_key=secret_key, Region=region)
client = CosS3Client(config)

#smtp郵箱實例
smtpObj = smtplib.SMTP_SSL(mail_host, mail_port)

# cos 文件讀寫
def cosRead(key):
    try:
        response = client.get_object(Bucket=bucket, Key=key)
        txtBytes = response['Body'].get_raw_stream()
        return txtBytes.read().decode()
    except CosServiceError as e:
        return ""


def cosWrite(key, txt):
    try:
        response = client.put_object(
            Bucket=bucket,
            Body=txt.encode(encoding="utf-8"),
            Key=key,
        )
        return True
    except CosServiceError as e:
        return False

#獲取全部信件
def getletters():
    letterMap = {}
    letterTxt = cosRead('letters.txt')  # 讀取數據
    if len(letterTxt) > 0:
        letterMap = json.loads(letterTxt)
    return letterMap

#添加信件
def addletter(date, email, letter):
    letterMap = getletters()
    if len(letterMap) > 0:
        letterMap[date+'_'+randomKey()] = email+'|'+letter
    return cosWrite('letters.txt', json.dumps(letterMap, ensure_ascii=False)) if len(letterMap) > 0 else False

#刪除信件
def delletter(letter):
    letterMap = getletters()
    if len(letterMap) > 0:
        letterMap.pop(letter)
    return cosWrite('letters.txt', json.dumps(letterMap, ensure_ascii=False)) if len(letterMap) > 0 else False


# 獲取今日日期
def today():
    return datetime.datetime.now().strftime("%Y-%m-%d")

# 判斷信件是否到期
def checkDate(t):
    return t[0:10] == today()

# 根據時間生成uuid
def randomKey():
    return ''.join(random.sample('zyxwvutsrqponmlkjihgfedcba0123456789', 6))

# api網關回復消息格式化
def apiReply(reply, html=False, code=200):
    htmlStr = r'''<!DOCTYPE html>
<html lang="zh-cn">

<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <title>給將來的本身寫封信</title>
    <style>
        html,
        body {
            padding: 0px;
            margin: 0px;
            height: 100vh;
        }

        .main {
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
        }

        .main_phone {
            display: flex;
            flex-direction: column;
            justify-content: start;
            align-items: center;
        }
    </style>
</head>

<body id='body'>
    <div class="main" style="width: 80vw;">
        <div style="height: 5vh;"></div>
        <div id='letter_top'>
            <p style="text-align: center;">開始寫信</p>
            <wired-textarea id="letter" style="height: 320px;width: 300px;" placeholder="此刻平靜地寫下一封信,給將來的本身一份溫暖..." elevation="6" rows="14"></wired-textarea>
        </div>
        <div style="display: flex;align-items: center;justify-content: center;">
            <div id='letter_left'>
                <p style="text-align: center;">開始寫信</p>
                <wired-textarea id="letter" style="height: 320px;width: 300px;" placeholder="此刻平靜地寫下一封信,給將來的本身一份溫暖..." elevation="6" rows="14"></wired-textarea>
            </div>
            <div style="width: 16px;"></div>
            <div>
                <p style="text-align: center;">送信日期</p>
                <wired-calendar id="calendar"></wired-calendar>
            </div>
        </div>
        <wired-divider style="margin: 16px 0;"></wired-divider>
        <p id="hitokoto"></p>
        <div>
            <wired-input id="email" placeholder="收件郵箱"></wired-input>
            <wired-button onclick="send()">投遞</wired-button>
        </div>
        <div style="height: 5vh;"></div>
    </div>
    <script>
        let datex = '';
        let myEmail = document.getElementById('email');
        let myLetter = document.getElementById('letter');
        let myCalendar = document.getElementById('calendar');

        let width =
            window.innerWidth ||
            document.documentElement.clientWidth ||
            document.body.clientWidth

        let height =
            window.innerHeight ||
            document.documentElement.clientHeight ||
            document.body.clientHeight

        let pc = width >= height

        let today = new Date();
        let info = today.toString().split(' ');
        let selected = `${info[1]} ${today.getDate()}, ${today.getFullYear()}`;

        document.getElementById('body').classList.add(pc ? 'main' : 'main_phone');
        document.getElementById('letter_left').style.display = pc ? 'block' : 'none';
        document.getElementById('letter_top').style.display = pc ? 'none' : 'block';
        myCalendar.setAttribute("selected", selected);
        myCalendar.addEventListener('selected', () => {
            let selectedObject = myCalendar.value;
            let date = new Date(new Date().setDate(selectedObject.date.getDate()));
            datex = date.toISOString().substr(0, 10);
        });

        function send() {
            if (datex.length < 1 || myEmail.value.length < 1 || myLetter.value.length < 1) {
                alert('信件內容、送信日期或投遞郵箱不能爲空');
                return;
            }
            fetch(window.location.href, {
                    method: 'POST',
                    body: JSON.stringify({
                        date: datex,
                        email: myEmail.value,
                        letter: myLetter.value
                    })
                }).then(res => res.json())
                .catch(error => console.error('Error:', error))
                .then(response => alert(response.ok ? '添加成功:)' : '添加失敗:('));
        }
    </script>
    <script src="https://v1.hitokoto.cn/?encode=js&select=%23hitokoto" defer></script>
    <script src="https://unpkg.com/wired-elements@2.0.5/lib/wired-elements-bundled.js "></script>
</body>

</html>'''
    return {
        "isBase64Encoded": False,
        "statusCode": code,
        "headers": {'Content-Type': 'text/html' if html else 'application/json', "Access-Control-Allow-Origin": "*"},
        "body": htmlStr if html else json.dumps(reply, ensure_ascii=False)
    }

#登錄郵箱
def loginEmail():
    try:
        smtpObj.login(mail_user, mail_pass)
        return True
    except smtplib.SMTPException as e:
        print(e)
        return False

#發送郵件
def sendEmail(letter):
    temp=letter.split('|')
    receivers = [temp[0]]
    message = MIMEText(temp[1], 'plain', 'utf-8')
    message['From'] = mail_user
    message['To'] = temp[0]
    message['Subject'] = '一封來自好久之前的信'
    try:
        smtpObj.sendmail(mail_user, receivers, message.as_string())
        print("send email success")
        return True
    except smtplib.SMTPException as e:
        print("Error: send email fail")
        return False

#天天定時檢查須要發送的信件
def check_send_letters():
    loginEmail()
    letters = getletters()
    for date in letters.keys():
        if checkDate(date):
            sendEmail(letters[date])


def main_handler(event, context):
    if 'Time' in event.keys():  # 來自定時觸發器
        check_send_letters()
        return
    if 'httpMethod' in event.keys():  # 來自api網關觸發器
        if event['httpMethod'] == 'GET':
            return apiReply('', html=True)  # 返回網頁
        if event['httpMethod'] == 'POST':  # 添加信件
            body = json.loads(event['body'])
            flag = addletter(body['date'], body['email'], body['letter'])
            return apiReply({
                'ok': True if flag else False,
                'message': '添加成功' if flag else '添加失敗'
            })
    return apiReply('', html=True)

沒錯,這就是前面展現的網頁應用的所有源碼了,使用雲函數 SCF 構建一個完整的先後端的全棧應用就是這麼簡單。json

代碼可能有點長,其實也沒多少知識點,下面我們再一塊兒捋一下這個雲函數 ~後端

import json
import datetime
import random
from email.mime.text import MIMEText
from email.header import Header
import smtplib

# 是否開啓本地debug模式
debug = False

# 騰訊雲對象存儲依賴
if debug:
    from qcloud_cos import CosConfig
    from qcloud_cos import CosS3Client
    from qcloud_cos import CosServiceError
    from qcloud_cos import CosClientError
else:
    from qcloud_cos_v5 import CosConfig
    from qcloud_cos_v5 import CosS3Client
    from qcloud_cos_v5 import CosServiceError
    from qcloud_cos_v5 import CosClientError

首先是依賴的導入,這裏主要導入了 python 自帶的 email 模塊和騰訊雲對象存儲 SDK,來實現信件的發送和後端存儲需求。

這裏須要注意一點,在騰訊雲的雲函數在線運行環境中,已經安裝了 qcloud\_cos\_v5 對象存儲 SDK,而我在本地環境安裝的對象存儲 SDK 是 qcloud\_cos,爲了方便本地調試,這裏我設置了一個 debug 開關,來動態導入 qcloud\_cos 依賴,這一點我在以前的系列文章《萬物皆可Serverless之使用SCF+COS快速開發全棧應用》中有講到。

# 配置存儲桶
appid = '66666666666'
secret_id = 'xxxxxxxxxxxxxxx'
secret_key = 'xxxxxxxxxxxxxxx'
region = 'ap-chongqing'
bucket = 'name'+'-'+appid

#配置發件郵箱
mail_host = "smtp.163.com"
mail_user = "xxxxxxxxxx@163.com"
mail_pass = "xxxxxxxxxxxxxx"
mail_port = 465

而後配置一下本身的郵箱信息和騰訊雲對象存儲桶信息

配置完成以後,咱們再來看一下雲函數的入口函數 main_handler(event, context)

def main_handler(event, context):
    if 'Time' in event.keys():  # 來自定時觸發器
        check_send_letters()
        return
    if 'httpMethod' in event.keys():  # 來自api網關觸發器
        if event['httpMethod'] == 'GET':
            return apiReply('', html=True)  # 返回網頁
        if event['httpMethod'] == 'POST':  # 添加信件
            body = json.loads(event['body'])
            flag = addletter(body['date'], body['email'], body['letter'])
            return apiReply({
                'ok': True if flag else False,
                'message': '添加成功' if flag else '添加失敗'
            })
    return apiReply('', html=True)

這裏咱們根據event的keys裏有無'Time'來判斷雲函數是不是經過定時器來觸發的,

這一點我在以前的系列文章《萬物皆可 Serverless 之使用 SCF+COS 快速開發全棧應用》中有講到。

#天天定時檢查須要發送的信件
def check_send_letters():
    loginEmail()
    letters = getletters()
    for date in letters.keys():
        if checkDate(date):
            sendEmail(letters[date])

檢查雲函數是不是經過定時器觸發,是由於在後面咱們會給這個雲函數添加定時觸發器來天天定時檢查須要發送的信件。

這裏的 check\_send\_letters 函數的做用就是登陸咱們的郵箱並讀取在 cos 中的全部信件,而後逐封檢查信件的發信日期,若是信件發信日期與當前的日期相符,就會向指定的郵箱發送信件,完成在指定日期投放信件的功能。

if event['httpMethod'] == 'GET':
    return apiReply('', html=True)  # 返回網頁
if event['httpMethod'] == 'POST':  # 添加信件
    body = json.loads(event['body'])
    flag = addletter(body['date'], body['email'], body['letter'])
    return apiReply({
         'ok': True if flag else False,
         'message': '添加成功' if flag else '添加失敗'
    })

若是咱們的雲函數是經過 api 網關觸發的話,就判斷一下 http 請求的方法是 GET 仍是 POST

<!DOCTYPE html>
<html lang="zh-cn">

<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <title>給將來的本身寫封信</title>
    <style>
        html,
        body {
            padding: 0px;
            margin: 0px;
            height: 100vh;
        }

        .main {
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
        }

        .main_phone {
            display: flex;
            flex-direction: column;
            justify-content: start;
            align-items: center;
        }
    </style>
</head>

<body id='body'>
    <div class="main" style="width: 80vw;">
        <div style="height: 5vh;"></div>
        <div id='letter_top'>
            <p style="text-align: center;">開始寫信</p>
            <wired-textarea id="letter" style="height: 320px;width: 300px;" placeholder="此刻平靜地寫下一封信,給將來的本身一份溫暖..." elevation="6" rows="14"></wired-textarea>
        </div>
        <div style="display: flex;align-items: center;justify-content: center;">
            <div id='letter_left'>
                <p style="text-align: center;">開始寫信</p>
                <wired-textarea id="letter" style="height: 320px;width: 300px;" placeholder="此刻平靜地寫下一封信,給將來的本身一份溫暖..." elevation="6" rows="14"></wired-textarea>
            </div>
            <div style="width: 16px;"></div>
            <div>
                <p style="text-align: center;">送信日期</p>
                <wired-calendar id="calendar"></wired-calendar>
            </div>
        </div>
        <wired-divider style="margin: 16px 0;"></wired-divider>
        <p id="hitokoto"></p>
        <div>
            <wired-input id="email" placeholder="收件郵箱"></wired-input>
            <wired-button onclick="send()">投遞</wired-button>
        </div>
        <div style="height: 5vh;"></div>
    </div>
    <script>
        let datex = '';
        let myEmail = document.getElementById('email');
        let myLetter = document.getElementById('letter');
        let myCalendar = document.getElementById('calendar');

        let width =
            window.innerWidth ||
            document.documentElement.clientWidth ||
            document.body.clientWidth

        let height =
            window.innerHeight ||
            document.documentElement.clientHeight ||
            document.body.clientHeight

        let pc = width >= height

        let today = new Date();
        let info = today.toString().split(' ');
        let selected = `${info[1]} ${today.getDate()}, ${today.getFullYear()}`;

        document.getElementById('body').classList.add(pc ? 'main' : 'main_phone');
        document.getElementById('letter_left').style.display = pc ? 'block' : 'none';
        document.getElementById('letter_top').style.display = pc ? 'none' : 'block';
        myCalendar.setAttribute("selected", selected);
        myCalendar.addEventListener('selected', () => {
            let selectedObject = myCalendar.value;
            let date = new Date(new Date().setDate(selectedObject.date.getDate()));
            datex = date.toISOString().substr(0, 10);
        });

        function send() {
            if (datex.length < 1 || myEmail.value.length < 1 || myLetter.value.length < 1) {
                alert('信件內容、送信日期或投遞郵箱不能爲空');
                return;
            }
            fetch(window.location.href, {
                    method: 'POST',
                    body: JSON.stringify({
                        date: datex,
                        email: myEmail.value,
                        letter: myLetter.value
                    })
                }).then(res => res.json())
                .catch(error => console.error('Error:', error))
                .then(response => alert(response.ok ? '添加成功:)' : '添加失敗:('));
        }
    </script>
    <script src="https://v1.hitokoto.cn/?encode=js&select=%23hitokoto" defer></script>
    <script src="https://unpkg.com/wired-elements@2.0.5/lib/wired-elements-bundled.js "></script>
</body>

</html>

若是是 GET 請求就返回上面的前端網頁,也就是文章開頭第一張圖,再來瞅一眼

雲函數返回的前端網頁

再來看下前端網頁的發信過程

function send() {
    if (datex.length < 1 || myEmail.value.length < 1 || myLetter.value.length < 1) {
        alert('信件內容、送信日期或投遞郵箱不能爲空');
        return;
    }
    fetch(window.location.href, {
            method: 'POST',
            body: JSON.stringify({
                date: datex,
                email: myEmail.value,
                letter: myLetter.value
            })
        }).then(res => res.json())
        .catch(error => console.error('Error:', error))
        .then(response => alert(response.ok ? '添加成功:)' : '添加失敗:('));
}

這裏咱們是向當前網頁地址,也是雲函數的 api 網關地址 POST 了一個包含全部信件信息的 json 字符串

if event['httpMethod'] == 'POST':  # 添加信件
    body = json.loads(event['body'])
    flag = addletter(body['date'], body['email'], body['letter'])
    return apiReply({
         'ok': True if flag else False,
         'message': '添加成功' if flag else '添加失敗'
    })

回到雲函數後端,咱們在收到 POST 請求以後,在 event 裏拿到 POST 的請求體,並從新將 json 字符串轉成 map 對象,以後將 body 傳給 addletter 函數,將信件信息保存到 cos 裏,而後向網頁前端回覆信件是否添加成功。

這樣整個應用的先後端只用一個雲函數就都實現了,是否是很酸爽呀( •̀ ω •́ )y~

第三步:配置雲函數觸發器

找到本地雲函數文件夾下面的 template.yaml 配置文件

Resources:
  default:
    Type: 'TencentCloud::Serverless::Namespace'
    letter:
      Properties:
        CodeUri: ./
        Type: Event
        Environment:
          Variables:
        Description: 給將來寫封信雲函數
        Handler: index.main_handler
        MemorySize: 64
        Timeout: 3
        Runtime: Python3.6
        Events:
          timer:
            Type: Timer
            Properties:
              CronExpression: '0 0 8 * * * *'
              Enable: True
          letter_apigw:
            Type: APIGW
            Properties:
              StageName: release
              ServiceId:
              HttpMethod: ANY
      Type: 'TencentCloud::Serverless::Function'

這裏主要配置了一下雲函數的名稱,timer 觸發器和 api 網關觸發器,能夠自行設置。

第四步:上線發佈雲函數,api 網關啓用響應集成

參見我以前的系列文章《萬物皆可 Serverless 之使用 SCF+COS 快速開發全棧應用》

第五步:綁定備案域名

若是你有備案域名而且想給 api 網關自定義域名的話,可參考我以前的系列文章《萬物皆可Serverless之免費搭建本身的不限速大容量雲盤(5TB)》

寫在最後

OK,沒啥問題的話如今你應該已經成功上線了本身的給將來寫封信網頁應用,或許你也能夠趁熱打鐵,試着靜下心來認真的給將來的本身留幾句話呢?

以上,咱們能夠總結出把一個簡單的網頁應用先後端都在一個雲函數裏來實現是徹底沒有問題的,並且極大縮短了咱們應用開發的時間,很是的方便,還省去了購買配置和維護服務器的費用,讓開發者能夠真正將精力放到業務自己的開發上,這就是 Serverless 最大的魅力!

Serverless Framework 30 天試用計劃

咱們誠邀您來體驗最便捷的 Serverless 開發和部署方式。在試用期內,相關聯的產品及服務均提供免費資源和專業的技術支持,幫助您的業務快速、便捷地實現 Serverless!

詳情可查閱: Serverless Framework 試用計劃

One More Thing

3 秒你能作什麼?喝一口水,看一封郵件,仍是 —— 部署一個完整的 Serverless 應用?

複製連接至 PC 瀏覽器訪問: https://serverless.cloud.tenc...

3 秒極速部署,當即體驗史上最快的 Serverless HTTP 實戰開發!

傳送門:

歡迎訪問:Serverless 中文網,您能夠在 最佳實踐 裏體驗更多關於 Serverless 應用的開發!


推薦閱讀: 《Serverless 架構:從原理、設計到項目實戰》
相關文章
相關標籤/搜索