تبدیل توییت به عکس با استفاده از توابع بدون سرور Netlify


هدف نهایی این پست یا درواقع چیزی که در انتها خواهیم ساخت، یک سرویس تبدیل توییت به عکسه که احتمالا بتونه اشتراک‌گذاری توییت‌ها رو در شبکه‌های اجتماعی دیگه یا آرشیو کردن اون‌ها رو راحت‌تر کنه. اما، جذاب‌تر و مهم‌تر از نتیجه نهایی، نحوه ساخت این سرویسه که در ادامه به تفصیل توضیح داده خواهد شد.

خلاصه (TL;DR)
برای ساخت این سرویس از Puppeteer یک مرورگر وب بدون واسط (Headless Browser) برای رندر یک صفحه وب (المان Embed توییتر) و تبدیل اون به عکس استفاده می‌کنیم که داخل یک تابع بدون سرور (Serverless Function) اجرا می‌شه. در عین حال، به هیچ احراز هویت و API Key احتیاج نداریم و هیچ هزینه‌ای هم پرداخت نمی‌کنیم.

توی این پروژه تماما از جاوا اسکریپت (Javascript) استفاده می‌کنیم، سمت فرانت (Front-end) که البته خیلی وارد جزئیاتش نمی‌شم از فریم‌ورک Svelte، سمت بک (Back-end) هم از Nodejs برای توسعه یک تابع بدون سرور استفاده می‌کنیم که در نهایت روی Netlify دپلوی (مستقر [deploy]) میشه.

در ادامه، ابتدا کمی سرویس و معماری بدون‌سرور رو بررسی می‌کنیم، برنامه رو به‌صورت محلی (local) توسعه می‌دیم و تست می‌کنیم و در انتها روی سرویس Netlify مستقر (deploy) می‌کنیم.


شمای کلی پروژه
شمای کلی پروژه


قبل از ادامه و بررسی پروژه، می‌تونید نتیجه نهایی رو در tweet2img و کد‌ها رو در ریپازیتوری گیت‌هاب ببینید.


لازم به ذکره قسمت‌هایی از این آموزش از راهنمایی‌های این ویدئو از Wes Bos کمک گرفته شده:

https://youtu.be/A0Ww-SU7K5E




مقدمه

پردازش ابری (Cloud Computing)



معماری‌های مختلفی در طول سال‌ها برای توسعه سیستم‌های پردازشیِ ابری معرفی و ارائه شده‌اند که یکی از هدف‌های اون‌ها مثل بخش‌های دیگه محاسبات کامپیوتری، تقسیم و جدا کردن سیستم به بخش‌های مستقل و قابل مدیریت است که در نتیجه اون توسعه‌دهندگان وارد جزئیات کمتری برای استقرار (دپلوی [deploy]) سرویس‌های خودشون بشن. در نتیجه سرویس‌های ابری مختلفی با سطوح انتزاع مختلفی برای استفاده موجود هستند، کاربر می‌تواند پایین‌ترین سطح انتزاع را انتخاب کرده و جزئیات سرور مثل میزان فضای ذخیره‌سازی و مصرف حافظه را کنترل کند و یا تنها در سطح مدیریت‌شده‌تری قرار گرفته و تنها درگیر انتخاب سیستم‌عامل و نصب نرم‌افزار‌های مورد نیاز خود داخل یک ماشین مجازی (Virtual Machine) شود.

معماری‌های پردازش ابری و موارد مورد نیاز به رسیدگی
معماری‌های پردازش ابری و موارد مورد نیاز به رسیدگی


توابع بدون سرور (Serverless Functions)

xkcd 927
xkcd 927


تابع به‌عنوان سرویس ([Function As A Service [FAAS) دسته‌بندی‌ای از سرویس‌های پردازش ابری‌ست که اپلیکیشن‌های ساخته شده توسط این مدل، از معماری بدون سرور استفاده می‌کنند. بدون سرور (Serverless) یک معماری و مدل اجرا (Execution Model) پردازش ابری است که در اون ارائه دهنده سرویس ابری (Cloud Provider) سرور را مدیریت کرده و به‌صورت پویا (dynamically) منابع مورد نیاز سرور را اختصاص میده. بنابراین توسعه دهنده می‌تواند تنها روی توسعه کد خودش تمرکز بکنه و ارائه دهنده سرویس باقی جزئیات را مدیریت می‌کنه.

بدون سرور به این معنی نیست که هیچ سروری اجرای کد‌ها رو برعهده نداره -که عملا منطقی نیست- اما به این منظوره که پیچیدگی‌های ایجاد و نگهداری سرورها حذف شده و تنها ماهیت و کد لازم برای اجرای سرویس نیازمند پیاده‌سازی‌ست.

از دیگر مزیت‌های این مدل اجرا، کاهش هزینه‌های استفرار و ارائه سرویس‌ها و همچنین استفاده بهینه از منابع سروره، چون که کاربر هزینه‌ای برای زمان‌هایی که سرور بی‌کار (idle) است پرداخت نمی‌کنه.

ارائه‌دهنده‌های اصلی سرویس‌های ابری
ارائه‌دهنده‌های اصلی سرویس‌های ابری


تقریبا تمامی ارائه‌ دهندگان اصلی سرویس‌های ابری سرویس‌های FAAS ارائه می‌دهند؛ در اکثر اون‌ها برای استفاده از این مدل اجرا نیاز است تا اپلیکیشن باندل (bundle) شده و در قالب یک فایل Zip برای اجرا روی سرویس قرار گیرد.

در این پروژه از سرویس Netlify برای دپلوی برنامه استفاده می‌کنیم که سرویس تابع بدون سرور اون (Netlify Functions) از سرویس AWS Lambda آمازون استفاده می‌کنه. همچنین، امکاناتی برای تست لوکال (محلی [local]) توابع -بدون سرور- و باندل کردن، دپلوی و ورژن‌بندی اون‌ها در اختیار میذاره که به‌شدت توسعه پروژه رو ساده می‌کنه.

صورت کلی یک تابع بدون سرور سرویس Netlify به‌صورت زیره:

// functions/foo.js
exports . handler = async ( event , context ) => {
    return {
        statusCode : 200 , 
        body : “ We are now split 
    }; 
}

این تابع هروقت ریکوئست‌ای (درخواست [request]) به آدرس مشخص شده اون (اینجا: img/) ارسال بشه، اجرا می‌شه و در نهایت هر مقداری برگردانده (return) شود به‌عنوان جواب ریکوئست ارسال می‌شه.

در پلن رایگان، Netlify نهایتا ۱۰ ثانیه فرصت اجرا به هر تابع می‌دهد.


ساخت خودکار تصویر


پروژه Ronin از 100r.co
پروژه Ronin از 100r.co


برای ساخت سرویس تبدیل توییت به عکسمون، نیاز داریم تا بتونیم به‌صورت برنامه‌نویسی شده یک تصویر بسازیم؛ برای این کار روش‌های مختلفی وجود داره که بیشتر محدود به زبان انگلیسی (به دلیل عدم پشتیبانی از فونت فارسی)، کُند و یا بدون امکانات شخصی‌سازی هستن. همچنین روش‌هایی نظیر استفاده از Context API با وجود قدرت و سرعت زیاد، به دلیل تفاوت محتوی توییت‌ها، مشخص نبودن طول توییت -نه نهایت طول- و همینطور تصاویر، پیش‌نمایش لینک‌ها و توییت‌های ضمیمه شده آن، مشخص‌نیست و نیاز به محاسبه نسبتا پیچیده داره، رسیدن به نتیجه مطلوب اصلا راحت نیست.

یکی از روش‌هایی که مدتی‌ست مرسوم شده و به نسبت بسیار ساده است، استفاده از قدرت بی‌نظیر HTML و CSS برای ساخت المان‌های دیداری و سپس تبدیلش به تصویره؛ استفاده از این روش، قدرت شخصی سازی بی‌نظیری به ما میده و در عین حال سریع و قابل اتکاست.

چیدمان تصویر با استفاده از Puppeteer

برای اینکه تصویر را با استفاده از تکنولوژی‌های وب بسازیم، نیاز به ابزاری داریم تا بتونه عملیات رندر (چیدمان [Render]) صفحه وب متشکل از HTML و CSS مورد نظر رو انجام بده و توانایی ارائه خروجی تصویر هم داشته باشه. برای اینکار از یک مرورگر وب بدون واسط (Headless Browser) به نام Puppeteer استفاده می‌کنیم که امروزه بسیار پر استفاده است.

مرورگر وب بدون واسط (Headless Browser)
یک مرورگر بدون واسطه، یک مرورگر وب بدون رابط گرافیکی و با قابلیت کنترل بالا و خودکار است که امکان پردازش و رندر یک صفحه وب همانند یک مرورگر عادی را در محیطی قابل برنامه‌ریزی و مدیریت شده می‌دهد.

دو نمونه از ابزارهای کنترل و کار با این مرورگر‌ها PhantomJS و Puppeteer هستند که هر دو از موتور V8 کرومیوم استفاده می‌کنند. همینطور مرورگر کروم نصب شده روی سیستم نیز می‌تواند در حالت بدون واسط اجرا شود.

‏Puppeteer این امکان رو میده تا داخل کد جاوا اسکریپت خودمون یک مرورگر کروم اجرا کرده، صفحه مورد نظر رو داخل اون رندر کرده و از اون یک اسکرین‌شات (Screenshot) ذخیره کنیم.



آماده سازی

همون‌طور که بالاتر گفته شد، در این پروژه از nodejs استفاده می‌کنیم. اگر اون رو نصب ندارید می‌تونید از سایت اصلی Nodejs و یا با استفاده از مدیر بسته (package manger) سیستم‌عاملتون دانلود و نصب کنید.

اگر اطمینان ندارید، ورژن LTS را نصب کنید، همچنین پیشنهاد می‌کنم از یک مدیر نسخه (version manager) برای nodejs مثل nvm یا n استفاده کنید:
اگر از لینوکس استفاده می‌کنید و node رو نصب ندارید، انتخاب خوبیه که از اول با نصب یکی از این دو اقدام به نصبشون بکنید. برای مثال با این دستور زیر n رو نصب کنید: curl -L git.io/n-install | bash

پس از نصب با اجزای دستورات زیر تو ترمینال از درستی نصبشون مطمئن بشید:

node -v
npm -v

همچنین برای این پروژه لازم است در Netlify ثبت‌نام کرده و یک تیم بسازید: سایت Netlify


ساخت پروژه و مفاهیم اولیه

برای شروع توی یک دایرکتوری جدید یک پروژه ایجاد کنید. از اونجایی که در این پروژه از فریمورک svelte استفاده می‌کنم،‌ پروژه خودم (با نام tweet2img) رو از template پیش‌فرض خودشون با استفاده از degit که یک ابزار scaffolding هست می‌سازم:

npx degit sveltejs/template tweet2img


توجه کنید که هیچ لزومی به استفاده از svelte نیست و پروژه خودتون رو به‌هر صورتی که تمایل دارید بسازید.

با وارد کردن این دستور، پروژه‌ای با ساختار تصویر زیر ساخته می‌شه:

ساخت پروژه
ساخت پروژه


حالا، وارد دایرکتوری پروژه شده و با استفاده از دستور زیر پکیج‌های مورد نیاز پروژه (dependencies) رو نصب می‌کنیم:

npm i
همچنین می‌تونیم با استفاده از دستور npm start پروژه رو اجرا و مشاهده کنیم.


آماده‌سازی netlify-cli

برای اینکه ‌بتونیم توابع بدون سرور خودمون رو به‌صورت لوکال تست کنیم و در نهایت مستقیم ‌اون‌ها رو منتشر کنیم، نیاز به ابزاری داریم تا بتونه مشابه محیط اجرای نهایی (پس از انتشار) اون‌ها رو اجرا بکنه. برای اینکار Netlify ابزاری تحت ترمینال به نام netlify-cli دارد. این پکیج رو به‌صورت عمومی (global) روی سیستم نصب می‌کنیم:

npm i -g netlify-cli

با اجرای این دستور netlify-cli به‌صورت عمومی روی سیستم نصب می‌شود و از هر دایرکتوری‌ای می‌تونیم اون رو اجرا کنیم. در اولین استفاده لازم است با استفاده از دستور زیر احراز هویت کنیم:

netlify login

با اجرای این دستور اگر در netlify لاگین باشین صفحه‌ای مشابه تصویر زیر باز می‌شود، روی دکمه Authorize کلیک کنید:


احراز هویت در netlify
احراز هویت در netlify


پس احراز هویت، netlify اطلاعات مربوط به کاربر فعلی را در آدرس زیر ذخیره می‌کند:

~/.netlify/config.json
برای مطالعه بیشتر در مورد احراز هویت و تغییر توکن و موارد دیگه این قسمت از مستندات رو بخونید.


اجرای اولیه پروژه

حالا می‌توانیم وارد دایرکتوری پروژه شده و با استفاده از دستور زیر پروژه رو به‌صورت لوکال اجرا کنیم:

netlify dev

با اجرای این دستور، netlify-cli به‌صورت خودکار نوع پروژه و دستور لازم برای اجرای اون رو تشخیص داده و پروژه رو روی پورت 8888: اجرا می‌کنه.

لازم به ذکره که netlify پروژه رو با استفاده از دستور npm start روی پورت پیش‌فرض دستور که اینجا 5000: هست اجرا می‌کنه و بعد یک پروکسی به اون پروژه توی پورت 8888: ایجاد می‌کنه تا با اینکار آدرس‌های مربوط به توابع بدون سرور رو مدیریت کنه؛ همینطور بتونه تغییرات فرانت پروژه و تغییرات توابع رو به‌صورت مجزا اجرا و اعمال کنه.

حال با مشاهده آدرس localhost:8888 توی مرورگر می‌تونیم نمایی اولیه از پروژه داشته باشیم:


اجرای پروژه اولیه
اجرای پروژه اولیه


تنظیمات Netlify

برای شخصی‌سازی و تنظیم Netlify از یک فایل TOML استفاده می‌کنیم. برای اینکار در root پروژه یک فایل با نام netlify.toml بسازید:


ساخت فایل تنظیمات Netlify
ساخت فایل تنظیمات Netlify


سپس کدهای زیر را داخل فایل کپی کنید. با قرار دادن این کدها، موارد زیر را برای زمان ساخت پروژه (مشخص شده توسط [build]) مشخص می‌کنیم:

  1. دایرکتوری اصلی پروژه که لازم است serve بشود دایرکتوری public است.
  2. توابع بدون سرور ما در دایرکتوری api قرار خواهند گرفت.
  3. برای ساخت پروژه لازم است از دستور npm run build استفاده شود.
[build]
  publish = "public"
  functions = "api"
  command = "npm run build"
اگر برخلاف این پروژه از هیچ فرم‌ورکی استفاده نمی‌کردین و نیازی به هیچ دستوری برای ساخت (build) پروژه نبود مقدار اون رو برابر # قرار بدین.


ساخت اولین تابع بدون سرور

ساختار توابع بدون سرور به این صورته که هر دایرکتوری داخل دایرکتوری‌ای که برای توابع بدون سرور تنظیم کردیم (اینجا api/.) یک endpoint به آدرس اسم اون دایرکتوری خواهد ساخت که اسکریپت js با همون اسم رو اجرا می‌کنه.

برای مثال با ساختن دایرکتوری و اسکریپت زیر:

./api/random/random.js

یک endpoint به آدرس زیر خواهیم داشت که نتیجه اجرای اسکریپت random.js رو با هر ریکوئست پاسخ میده.

/.netlify/functions/random

مشابه توضیح بالا دایرکتوری و فایل random رو در root پروژه می‌سازیم:


ساخت api با نام random
ساخت api با نام random


هدف این تابع این است تا با هر ریکوئست یک عدد تصادفی (random) به عنوان ریسپانس برگرداند. این تابع رو به‌صورت زیر می‌نویسیم:

// api/random/random.js
exports.handler = async (event, context) => {
    return {
        statusCode: 200,
        body: Math.random().toString(),
    };
};

حال می‌توانیم با وارد کردن آدرس زیر در مرورگر نتیجه اجرای این دستور رو مشاهده کنیم:

http://localhost:8888/.netlify/functions/random

ایجاد اعداد تصادفی با استفاده از تابع بدون سرور random
ایجاد اعداد تصادفی با استفاده از تابع بدون سرور random



دریافت پارامتر‌ها و متد درخواست

به ورودی‌های تابعی که در کد خودمون export کردیم دقت کنید. این ورودی‌ها اطلاعات لازم درباره درخواست ارسال شده و محیط اجرای تابع رو در اختیار ما می‌ذارن. شکل کلی اون‌ها به‌صورت زیره:

exports.handler = async (event, context, callback) => {}

پارامتر callback برای ارسال response استفاده می‌شده که ما در مثال قبل تنها با return کرد یک object اینکار رو انجام دادیم. بجای return کردن میشه از تابع callback به‌صورت زیر استفاده کرد:

exports.handler = function(event, context, callback) {
    callback(null, { // null indicates no error
        statusCode: 200,
        body: "Hello, World"
    });
}

ورودی event، اطلاعات زیر رو در اختیار ما می‌ذاره:

// whats inside event parameter:

{
    "path": "Path parameter",
    "httpMethod": "Incoming request's method name"
    "headers": {Incoming request headers}
    "queryStringParameters": {query string parameters }
    "body": "A JSON string of the request payload."
    "isBase64Encoded": "A boolean flag to indicate if the applicable request payload is Base64-encode"
}

بنابراین با استفاده از event.httpMethod می‌تونیم نوع درخواست ارسال شده رو کنترل کنیم و برای مثال تنها به‌ درخواست‌های POST پاسخ بدیم و با استفاده از event.queryStringParameters می‌تونیم پارامتر‌های ارسال شده توی URL رو به‌صورت یک Object در اختیار داشته باشیم.

برای نمونه اسکریپت greet رو در آدرس زیر می‌سازیم:

./api/greet/greet.js

و کد زیر رو توی اون قرار بدید:

exports.handler = async (event, context) => {
    const { name } = event.queryStringParameters;
    return {
        statusCode: 200,
        body: `Hello, ${name || "world"}!`,
    };
};
تغییرات داخل فایل توابع به‌صورت آنی بعد از ذخیره شدن فایل، اعمال می‌شن اما با ساخت تابع جدید نیاز به اجرای دوباره دستور netlify dev است. بنابراین دستور را دوباره اجرا کنید تا endpoint این تابع ساخته شود.

حال با وارد کردن آدرس زیر و قرار دادن یک اسم در انتها می‌تونید پیامی حاوی اون اسم رو برگردونید:

http://localhost:8888/.netlify/functions/greet?name=

اگر اسمی فرستاده نشود بجای آن world قرار می‌‌گیرد.
اجرای تابع greet
اجرای تابع greet


تغییر آدرس توابع

اجرای توابع با همچین آدرس‌هایی نه قشنگه و نه راحت. برای همین لازم داریم درخواست‌ها رو از یک آدرس ساده‌تر redirect کنیم به این آدرس. برای مثال مشخص کنیم که هر ریکوئست که به random/ رسید اون رو به netlify/functions/random./ پاس بده:

/random  -->    /.netlify/functions/random

برای اینکار Netlify با استفاده از یک فایل اجازه مشخص کردن ریدایرکت‌ها رو به ما میده:

داخل دایرکتوری public یک فایل به نام redirects_ می‌سازیم و داخل اون در هر سطر مشخص می‌کنیم با دریافت ریکوئست به هر آدرس، به کدام آدرس و با کدام status code ریدایرکت شویم:

این موارد رو با space از هم جدا کرده و در هر سطر یک مورد را وارد می‌کنیم.
/random /.netlify/functions/random 200
/greet /.netlify/functions/greet 200


حال با وارد کردن آدرس‌های زیر می‌تونیم توابع رو اجرا کنیم:

http://localhost:8888/random

http://localhost:8888/greet


نصب پکیج و استفاده در تابع بدون سرور

همانطور که در مقدمه پست اشاره شد، سرویس‌های ارائه دهنده سرویس FAAs،‌ توابع بدون سرور رو به‌صورت یک فایل bundle شده به فرمت zip دریافت و اجرا می‌کنند. در اینجا عملیات باندل کردن و آماده سازی توابع رو netlify-cli با استفاده از webpack به‌صورت خودکار انجام میده.

به‌صورت دقیق‌تر، از ورژن ۲.۷ عملیات باندل کردن به‌صورت خودکار انجام می‌شه و قبل از اون باید از ابزارهایی مثل netlify-lambda استفاده می‌شد.
در اینجا هم اگر می‌خواهید از import بجای require استفاده کنید نیاز است تا با استفاده از netlify-lambda و تنظیم اون تابع رو build کنید.

بنابراین می‌تونیم بدون اعمال تنظیمات خاصی از پکیج‌های شخص‌ثالث (third-part packages) در پروژه خودمون استفاده کنیم.

برای تست این مورد یک تابع جدید با نام quote در آدرس زیر بسازید:

./api/quote/quote.js

و ترمینال را در این آدرس باز کرده و با استفاده از دستور زیر یک پکیج Initialize کنید:

npm init -y

با اجرای این دستور فایل package.json در دایکتوری تابع ساخته شده و می‌تونیم پکیج‌هایی که لازم داریم رو نصب کنیم.

در این تابع می‌خوام از جملات master Yoda استفاده کنم. برای همین پکیج yodaquotes رو نصب می‌کنم:

npm i yodaquotes

بعد از نصب این پکیج، می‌تونم از اون توی تابع خودم استفاده کنم:

const yodaQuotes = require("yodaquotes");

exports.handler = async (event, context) => ({
    statusCode: 200,
    body: yodaQuotes(),
});
مجددا بعد از ساخت این تابع دستور netlify dev رو ری‌استارت کنید و مقادیر لازم برای ری‌دایرکت کردن تابع رو در فایل redirects_ قرار بدین.

الان با وارد کردن آدرس زیر می‌تونیم از دستورات یودا بهره‌مند بشیم:

دریافت یک quote از Yoda
دریافت یک quote از Yoda





بالاخره

تبدیل توییت به عکس با استفاده از puppeteer

توابعی که در قسمت‌های قبل درست کردیم صرفا جنبه آموزشی داشتن تا ببینیم چطوری توابع بدون سرور بسازیم و از اون‌ها استفاده کنیم. اما بالاخره رسیدیم به هدف اصلی این پست، که تبدیل توییت به عکسه.


همونطور که قبلا اشاره شد برای تبدیل توییت به عکس از puppeteer استفاده می‌کنیم. برای این کار المان embed یک توییت رو با استفاده از Oembed API توییتر دریافت می‌کنیم.

اگر سعی کرده باشید از twitter یک API key بگیرید احتمالا می‌دونید که پروسه راحت و سریعی نیست و چندین مرحله پرسش و پاسخ رو طی می‌کنید که ممکنه تا چند روز طول می‌کشه. اما ما اینجا از Oembed API استفاده می‌کنیم که یک API عمومی و رایگان هست.

این API به این صورت عمل می‌کنه که با دریافت آدرس یک توییت، اطلاعات لازم برای embed کردن توییت شامل یک کد HTML مربوط به اون رو ارائه میده:

نتیجه Oembed API توییتر
نتیجه Oembed API توییتر


اگر این کد HTML خروجی‌ای مشابه تصویر زیر رندر می‌کنه:

المان embed یک توییت از Fermat's Library
المان embed یک توییت از Fermat's Library


ما از این المان استفاده کرده و با استفاده از puppeteer از اون یک اسکرین‌شات می‌گیریم.


ایجاد و راه‌اندازی پروژه

برای شروع مشابه آخرین مثال قسمت قبل، یک تابع جدید (مثلا img) می‌سازیم و یک پکیج initialize می‌کنیم و پکیج‌های لازم رو نصب می‌کنیم که در ادامه بررسی می‌شن:

cd api/img
npm init -y
npm i chrome-aws-lambda puppeteer-core node-fetch waait

در اینجا دلیل اینکه چرا خود puppeteer رو نصب نکرده و puppeteer-core رو نصب کردیم به این خاطره که با نصب puppeteer، همراه خودش یک مرورگر کروم رو هم نصب می‌کنه که ما احتیاجی نداریم. به عنوان مرورگر ما از chrome-aws-lambda استفاده می‌کنیم که یک نسخه باینری از مرورگر کروم است که برای اجرا در محیط‌های aws lambda آمازون و cloud functions گوگل بهینه و آماده شده.

همونطور که قبلا اشاره شد، Netlify از سرویس‌های aws آمازون استفاده می‌کنه.

پکیج‌های node-fetch و waait پکیج‌های دلخواهی هستند که احتیاجی بهشون نداریم اما برای راحتی کار ازشون استفاده کردم. می‌تونید این پکیج‌هارو نصب نکرده یا از پکیج‌های مشابهشون استفاده کنید.


کد اولیه اسکرین‌شات

برای اسکرین‌شات گرفتن، از کد زیر از Leigh Halliday استفاده کردم که نحوه استفاده از کروم رو در اجرای لوکال و هنگام اجرا روی سرور تفکیک کرده:

https://gist.github.com/mhsattarian/c420dddc9390d641ed962ce4ec2d7c36

در این کد توی تابع getOptions با پارامتر isDev مشخص می‌شود که درحال اجرای کد در لوکال هستیم و یا روی سرور که بر اساس اون مروگر کروم نصب شده روی سیستم (در حالت بدون واسطه [headless] و مخفی [incognito]) و یا chrome-aws-lambda به‌عنوان مرورگر مورد استفاده انتخاب می‌شود.

تابع getScreenshot هم وظیفه اجرای مرورگر (تابع launch)، باز کردن صفحه به آدرس مورد نظر (تابع goto و پارامتر url) و اسکرین شات گرفتن از آن (تابع screenshot) را بر عهده دارد.

همینطور، با استفاده از تابع setViewPort مشخص می‌کنیم که document در هنگام لود صفحه چه اندازه‌ای باشه و از آرگومان deviceScaleFactor برای این استفاده کردیم که اندازه document بزرگتر باشه و تصاویر با کیفیت‌تری در هنگام اسکرین‌شات گرفتن خروجی بگیریم.


اعمال تغییرات

۱. از اونجایی که ما می‌خواهیم المان Embed توییت به‌صورت HTML رو استفاده کنیم و همچنین نه از کل صفحه که از یک المان خاص احتیاج‌داریم اسکرین‌شات گرفته بشه، کد رو به‌صورت زیر تغییر میدیم:

https://gist.github.com/mhsattarian/8f380c851b2eaaae11b299f330f66670

در اینجا بجای استفاده از متد page.goto از تکه کد زیر برای رندر یک تکه کد HTML استفاده کردیم:

بالاتر توضیح داده‌شد که این تکه HTML قرار است از سرویس Oembed توییتر دریافت شود.
page.setContent(html, 
    { waitUntil: ["networkidle0", "domcontentloaded"], }
);

که در این کد مشخص کردیم مقدار متغیر html رو رندر کرده و تا هنگام لود تمامی المان‌ها صبر کن.

۲. تغییر دیگر، استفاده از متد page.evaluate برای اعمال یک اسکریپت جاوا اسکریپت درون صفحه رندر شده است. با استفاده از این تابع می‌توانیم یک اسکریپت دلخواه را درون صفحه اجرا کرده و خروجی‌ای از آن دریافت کنیم. در اینجا از این تابع برای محاسبه مختصات باکس المان embed با کمک متد getBoundingClientRect استفاده کردیم تا بعدا از اون برای مشخص کردن محدوده مورد نظر برای اسکرین‌شات گرفتن استفاده کنیم.

نکته جانبی
توییتر پیش از این از web components در المان خود استفاده می‌کرد و المان‌هایی که نیاز به استایل‌دهی آن‌ها داشتیم، در shadow root قرار می‌گرفتند. استایل دهی به این المان‌ها از طریق css به راحتی ممکن نبود و به همین دلیل می‌بایست که از متد element.shadowRoot برای دریافت المان‌های صفحه استفاده می‌کردیم.
اما حدود یک ماه پیش با تغییر المان embed خودشون و استفاده از iframe تا حدودی این کار -در روش استفاده ما- راحت‌تر شده.


ساخت تابع بدون سرور و نحوه استفاده

الان که نحوه اسکرین‌شات رو بررسی کردیم می‌تونیم تابع بدون سرور خودمون رو بنویسیم. برای اینکار فایل اسکریپتی که نوشتیم (api/img/img.js/.) رو باز می‌کنیم و محتوی کد بالا و سپس کد زیر رو وارد می‌کنیم:

https://gist.github.com/mhsattarian/447f6d0105e6652f344a01879e16e083
  • آدرس توییت مورد نظر رو از پارامتر url دریافت کرده (خط ۶) و با استفاده از Oembed API توییتر کد HTML المان رو دریافت می‌کنیم (خط ۸).
  • سپس مقدار Environment Variable ای با نام CHROME رو می‌خونیم تا با استفاده از اون مشخص کنیم آیا در اجرای محلی هستیم یا اجرای روی سرور (خط ۱۳).

برای اینکه این مقدار در اجراهای لوکال مشخص شود، در فایل package.json به صورت زیر اسکریپتی برای اجرای پروژه می‌نویسیم:

نوشتن اسکریپت برای شروع پروژه
نوشتن اسکریپت برای شروع پروژه

همینطور پکیج cross-env رو هم به‌صورت زیر نصب می‌کنیم:

npm i cross-env


  • با استفاده از تابع getScreenshot ای که نوشتیم نتیجه اسکرین‌شات رو به‌صورت بافر (Buffer) دریافت می‌کنیم (خط ۱۴).
  • در نهایت مثل قبل یک Object را return می‌کنیم که به عنوان body بافر عکس که تبدیل به Base64 شده را قرار میدیم. همینطور با پراپرتی isBase64Encoded مشخص می‌کنیم که مقدار body یک فایل به فرمت base64 است (خط ۱۶).

نتیجه اجرای تمامی این مراحل، تابعی است که با دریافت آدرس یک توییت در پارامتر url اش می‌تونه اون رو تبدیل به تصویر کرده و خروجی رو برگردونه. برای مثال آدرس یک توییت رو در انتهای آدرس زیر قرار بدین:

http://localhost:8888/img?url=


نتیجه تبدیل توییت به عکس
نتیجه تبدیل توییت به عکس


در انتها، برای انتشار و استقرار سایتتون می‌تونید از دستور زیر استفاده کنید:

netlify deploy --prod

با استفاده از این دستور، پروژه شما build شده و با نامی که انتخاب کردید منتشر خواهد شد.


خلاصه

تولید عکس با استفاده از تکنولوژی‌های وب انعطاف‌پذیری زیادی به ما میده در عین‌حال به‌شدت راحت و سریعه. پروژه‌های زیادی از همین روش در دنیای وب برای تولید تصویر، thumbnail و داده‌های مشابه استفاده می‌کنند. همینطور سرویس‌هایی مانند HCTI هستند که API برای اتوماتیک کردن همین پروسه در اختیار می‌گذارن.

توابع بدون سرور هم امکان پیاده‌سازی سرویس‌های سمت بک رو -بیشتر مواقع- در راحت‌ترین شرایط فراهم می‌کنن که سرویس‌های بسیاری بجز Netlify برای استفاده رایگان از این مدل اجرا مثل Vercel و Begin وجود دارن.

امیدوارم جالب بوده باشه براتون.



دیگر منابع

  • تصویر پس‌زمینه: کاور از editorX و فونت از صابر راستی‌کردار
  • نمودار ابتدای پست، ساخته شده به کمک cloudcraft.co
  • مقایسه معماری‌های پردازش ابری، ساخته شده به کمک excalidraw
  • تصویر سازی بخش «توابع بدون سرور»، از xkcd
  • نمودار ارائه‌دهندگان سرویس‌های ابری از وبلاگ maxkelsen
  • تصویر بخش «ساخت خودکار تصاویر» از پروژه Ronin از 100r.co
  • تصویر بخش «چیدمان تصویر با استفاده از Puppeteer» از buddy.works
  • پاراگراف اول توضیح «مرورگر وب بدون واسط» از ویکی‌پدیا
  • توییت نمایش داده‌شده در ابتدای بخش «بالاخره، تبدیل توییت به عکس...» از Fermats Library
  • توییت تبدیل شده به عکس در تصویر انتهایی از Sam Altman