iT邦幫忙

2021 iThome 鐵人賽

DAY 22
1
Modern Web

JavaScript Easy Go!系列 第 22

#22 IPAPAPI - IP as Picture API

今天來用 Cloudflare Workers 寫個有趣的東西吧!

你的 IP 是不是

很酷吧!

現在為你介紹... IP as Picture API,簡稱 IPAPAPI。

如何做到的?

還記得我們在 #20 Telegram Bot Webhook 訊息收發 中在介紹 wrnagler dev 時有提到原本給你的 Code 跑下去會回傳 Worker 收到的東西嗎?

其中也有包含到你的 IP 喔!

所以我們可以用 Worker 來動態生成包含請求來源 IP 的圖片,為了在 10ms 內完成圖片,我們使用 SVG 向量圖來直接操作文字圖內文字。

路由

在這個程式中,我們只需要處理兩條路由規則即可,其中一條是 favicon.ico,另一條則是請求圖片的規則:

// src/main.js
import { Router } from "itty-router";
import { response } from "./response";
import { create } from "./creator";
import colorFix from "./color_fix";

const router = Router();
// 也可以將請求指向其他地方的圖檔當作 favicon,但這裡其實沒什麼必要,所以直接回傳空字串
router.all("/favicon.ico", () => response({ data: "", status: 200 }));

// 對於其他所有請求,我們一律當作對圖片的請求
router.all("*", async (request) => {
    const { query, headers } = request;
    // 我們用 Cloudflare 加上的 CF-CONNECTING_IP 當作來源 IP
    const ip = headers.get("cf-connecting-ip") || headers.get("x-forwarded-for") || request.headers.get("x-real-ip");
    // 解構請求中的 Query String,就是問號後面的參數
    const config = {
        ip,
        font: (query.font || query.f || "baloo").trim().toLowerCase(),
        size: +(query.size || query.s || 20),
        width: +(query.width || query.w || 140),
        height: +(query.height || query.h || 60),
        radius: +(query.radius || query.r || 0),
        color: "#" + (query.color || query.c || "2E3440"),
        background: "#" + (query.background || query.b || "ECEFF4"),
    };
    // 修正錯誤的顏色
    config.color = colorFix(config.color);
    config.background = colorFix(config.background);

    // 在 console 顯示標準化後的生成設定
    console.log(JSON.stringify(config, null, 2));
    // 把設定丟到產生器去
    const product = create(config);
    // 用 response 處理 Header 之類的東西
    return response({ data: product });
});

async function main() {
    addEventListener("fetch", (event) => {
        event.respondWith(router.handle(event.request));
    });
}

// 丟給 index.js 去執行,個人習慣而已,當然也可以直接跑
export { main };

接下來,我們來說說 colorFix 處理的問題。
我希望 API 能接受 Hex Color (#XXXXXX) 以及 CSS Color String (就是像 red、blue 這種),但上面那樣寫會有個問題:當使用者輸入 royalblue 時,會變成 #royalblue,這可不是顏色啊!
所以我們用 colorFix 去「修好」這些壞掉的顏色。

colorFix

colorFix 會從一個顏色列表中找是否有符合的「真的」顏色,如果有就把「#」拿掉。

// src/colorFix.js
// 可解析顏色列表
const COLOR_LIST = [
    "transparent",
    "aliceblue",
    ...
    "yellowgreen",
];

function colorFix(color) {
    const real = color.replace(/^#/, "");
    if (COLOR_LIST.includes(real)) return real;
    return color;
}

export default colorFix;

creator

creator.js 就是整個程式的核心,負責生成 SVG 檔案。

// src/creator.js
import fonts from "./fonts";

// 解構傳進來的各項設定
function create({ ip, font, size, width, height, radius, color, background }) {
    // 對於字體做檢查,如果沒有指定字體,則用預設的 baloo
    let fontCss = fonts[font],
        fontFamily = font;
    if (!fontCss || !fontFamily) {
        fontCss = fonts["baloo"];
        fontFamily = "baloo";
    }
    // 我們的「產品」
    let product = "";
    // 首先包含 SVG 的定義東西,要不然瀏覽器會沒辦法解析喔
    product += `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" fill="${background}">`;
    // SVG 中是可以用 CSS 規則的,所以甚至可以用動畫喔
    product += `<style>
        ${fontCss}
        svg {
            border-radius: ${radius}px;
        }
        #ip {
            font-family: ${fontFamily};
            font-size: ${size}px;
            fill: ${color};
            text-anchor: middle;
            dominant-baseline: middle;
        }
    </style>`;
    // 下面這個 rect 是背景喔
    product += `<rect x="0" y="0" width="${width}" height="${height}" rx="${radius}" ry="${radius}" fill="${background}"/>`;
    // 這行則是 IP 的文字
    product += `<text id="ip" x="${width / 2}" y="${height / 2}">${ip}</text>`;
    // 記得結尾
    product += "</svg>";
    return product;
}

export { create };

這樣幾乎都完成了,除了最累的字體。

Fonts!!

IPAPAPI 總共提供 19 種字體,都是從 Google Fonts 上來的,使用 OFL 授權。

這邊有個麻煩的事,我們不能直接用 CSS 規則 @import Google Fonts,必須使用 inline base64 放到檔案內。

還記得之前說過,Cloudflare Workers 只支援大小 1 MB 吧,那怎麼塞那麼多字體?
答案是把字體中不用的字大部分都刪掉!畢竟我們基本上只有數字 + . + :

然後把每一個字型檔轉成 base64 放到 JavaScript 檔案內:

// src/fonts/baloo.js
const baloo = `
@font-face { 
    font-family: "baloo";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url(data:application/octet-stream;base64,d09GMgABAAAAAEGQABIAAAAA5HQ...FAAA=);
}`;

export default baloo;

然後丟到 src/fonts/index.js 去:

// src/fonts/index.js
import baloo from "./baloo";
import roboto from "./roboto";
...
import ruthie from "./ruthie";

const fonts = {
    baloo,
    roboto,
    ...
    ruthie,
};

export default fonts;

累死人了,應該寫個自動化程式。 XD

然後就完成啦!

它現在住這:Full Counter: IP as Picture API
前面寫個 Full Counter 的原因是因為它的概念就是把你丟過來的 IP 一字不差的丟回去。

使用方法與文件:README


每日鐵人賽熱門 Top 10 (1005)

以 10/05 20:00 ~ 10/06 20:00 文章觀看數增加值排名

  1. +132 Day-1 開始玩懷舊遊戲機的事前準備導覽篇
    • 作者: rei0
    • 系列:在新世代裡復活吧!我的童年懷舊遊戲機們!
  2. +131 [DAY-22] 填補知識缺口 尋找導師 持續學習
    • 作者: flipsyde
    • 系列:帶腦去上班 & No Rules Rules
  3. +123 [Day 28] Gitea - 如何自簽憑證與Nginx注意
    • 作者: rainforest
    • 系列:Dev's Ops 啟程
  4. +115 [Day 3] SRE - Log寫好一點,對團隊好一些
    • 作者: rainforest
    • 系列:Dev's Ops 啟程
  5. +115 [Day 5] SRE - 發動事件左移之術,預視未來的機制
    • 作者: rainforest
    • 系列:Dev's Ops 啟程
  6. +114 [Day 4] SRE - 保持精簡的監控
    • 作者: rainforest
    • 系列:Dev's Ops 啟程
  7. +114 [Day 11] SRE - 事後檢討,拜託拜託讓我吸個經驗值
    • 作者: rainforest
    • 系列:Dev's Ops 啟程
  8. +113 [Day 10] SRE - ON-CALL
    • 作者: rainforest
    • 系列:Dev's Ops 啟程
  9. +113 Day 21: Informix(2)
    • 作者: 阿瑜
    • 系列:FRIENDS
  10. +112 Proxmox VE 網路進階設定 (Bridge、LACP、VLAN)
    • 作者: Jason Cheng (節省哥)
    • 系列:突破困境:企業開源虛擬化管理平台

Bogay 學長,今天榜單有好多 SRE 喔!


上一篇
#21 讓 Automation 與 Chat Bot 連動
下一篇
#23 3 分鐘建立 Node.js 網頁伺服器
系列文
JavaScript Easy Go!31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言