در مقاله پیشین ما ظاهر افزونه را طراحی و یک سری از قابلیتهای افزونه را معرفی کردیم. در این قسمت قصد داریم پردازش پس زمینه افزونه یعنی خواندن RSS و اعلام به روز آوری سایت را مورد بررسی قرار دهیم و یک سری قابلیت هایی که گوگل در اختیار ما قرار داده است.
خواندن RSS توسط APIهای گوگل
گوگل در تعدادی از زمینهها و سرویسهای خودش apiهایی را ارائه کرده است که یکی از آن ها خواندن فید است و ما از آن برای خواندن RSS یا اتم وب سایت کمک میگیریم. روند کار بدین صورت است که ابتدا ما بررسی میکنیم کاربر چه مقادیری را ثبت کرده است و افزونه قرار است چه بخش هایی از وب سایت را بررسی نماید. در این حین، صفحه پس زمینه شروع به کار کرده و در هر سیکل زمانی مشخص شده بررسی میکند که آخرین بار چه زمانی RSS به روز شده است. اگر از تاریخ قبلی بزرگتر باشد، پس سایت به روز شده است و تاریخ جدید را برای دفعات آینده جایگزین تاریخ قبلی کرده و یک پیام را به صورت نوتیفیکیشن جهت اعلام به روز رسانی جدید در آن بخش به کاربر نشان میدهد.
اجازه دهید کدها را کمی شکیلتر کنیم. من از فایل زیر که یک فایل جاوااسکریپتی است برای نگه داشتن مقادیر بهره میبرم تا اگر روزی خواستم یکی از آنها را تغییر دهم راحت باشم و در همه جا نیاز به تغییر مجدد نداشته نباشم. نام فایل را (const.js) به خاطر ثابت بودن آنها انتخاب کردهام.
//برای ذخیره مقادیر از ساختار نام و مقدار استفاده میکنیم که نامها را اینجا ثبت کرده ام
var Variables={
posts:"posts",
postsComments:"postsComments",
shares:"shares",
sharesComments:"sharesComments",
}
//برای ذخیره زمان آخرین تغییر سایت برای هر یک از مطالب به صورت جداگانه نیاز به یک ساختار نام و مقدار است که نامها را در اینجا ذخیره کرده ام
var DateContainer={
posts:"dtposts",
postsComments:"dtpostsComments",
shares:"dtshares",
sharesComments:"dtsharesComments",
interval:"interval"
}
//برای نمایش پیامها به کاربر
var Messages={
SettingsSaved:"تنظیمات ذخیره شد",
SiteUpdated:"سایت به روز شد",
PostsUpdated:"مطلب ارسالی جدید به سایت اضافه شد",
CommentsUpdated:"نظری جدیدی در مورد مطالب سایت ارسال شد",
SharesUpdated:"اشتراک جدید به سایت ارسال شد",
SharesCommentsUpdated:"نظری برای اشتراکهای سایت اضافه شد"
}
//لینکهای فید سایت
var Links={
postUrl:"https://www.dntips.ir/feeds/posts",
posts_commentsUrl:"https://www.dntips.ir/feeds/comments",
sharesUrl:"https://www.dntips.ir/feed/news",
shares_CommentsUrl:"https://www.dntips.ir/feed/newscomments"
}
//لینک صفحات سایت
var WebLinks={
Home:"https://www.dntips.ir",
postUrl:"https://www.dntips.ir/postsarchive",
posts_commentsUrl:"https://www.dntips.ir/commentsarchive",
sharesUrl:"https://www.dntips.ir/newsarchive",
shares_CommentsUrl:"https://www.dntips.ir/newsarchive/comments"
}
موقعی که اولین بار افزونه نصب میشود، باید مقادیر پیش فرضی وجود داشته باشند که یکی از آنها مربوط به مقدار سیکل زمانی است (هر چند وقت یکبار فید را چک کند) و دیگری ذخیره مقادیر پیش فرض رابط کاربری که قسمت پیشین درست کردیم؛ پروسه پس زمینه برای کار خود به آنها نیاز دارد و بعدی هم تاریخ نصب افزونه است برای اینکه تاریخ آخرین تغییر سایت را با آن مقایسه کند که البته با اولین به روزرسانی تاریخ فید جای آن را میگیرد. جهت انجام اینکار یک فایل init.js ایجاد کردهام که قرار است بعد از نصب افزونه، مقادیر پیش فرض بالا را ذخیره کنیم.
chrome.runtime شامل رویدادهایی چون onInstalled ، onStartup ، onSuspend و ... است که مربوطه به وضعیت اجرایی افزونه میشود. آنچه ما اضافه کردیم یک listener برای زمانی است که افزونه نصب شده است و در آن مقادیر پیش فرض ذخیره میشوند. اگر خوب دقت کنید میبینید که روش دخیره سازی ما در اینجا کمی متفاوت از مقاله پیشین هست و شاید پیش خودتان بگویید که احتمالا به دلیل زیباتر شدن کد اینگونه نوشته شده است ولی مهمترین دلیل این نوع نوشتار این است که متغیرهای بین {} آنچنان فرقی با خود string نمیکنند یعنی کد زیر:
chrome.storage.local.set('mykey':myvalue,....
با کد زیر برابر است:
chrome.storage.local.set(mykey:myvalue,...
پس اگر مقداری را داخل متغیر بگذاریم آن مقدار حساب نمیشود؛ بلکه کلید نام متغیر خواهد شد.
برای معرفی این دو فایل const.js و init.js به manifest.json میتوانید به صورت زیر عمل کنید:
در این حالت خود اکستنشن در زمان نصب یک فایل html درست کرده و این دو فایل js را در آن صدا میزند که البته خود ما هم میتوانیم اینکار را مستقیما انجام دهیم. مزیت اینکه ما خودمان مسقیما این کار را انجام دهیم این است که در صورتی که فایلهای js ما زیاد شوند، فایل manifest.jason زیادی شلوغ شده و شکل زشتی پیدا میکند و بهتر است این فایل را تا آنجا که میتوانیم خلاصه نگه داریم. البته روش بالا برای دو یا سه تا فایل js بسیار خوب است ولی اگر به فرض بشود 10 تا یا بیشتر بهتر است یک فایل جداگانه شود و من به همین علت فایل background.htm را درست کرده و به صورت زیر تعریف کردهام:
نکته:نمی توان در تعریف بک گراند هم فایل اسکریپت معرفی کرد و هم فایل html
لینکهای بالا به ترتیب معرفی ثابتها، لینک api گوگل که بعدا بررسی میشود، فایل init.js برای ذخیره مقادیر پیش فرض، فایل ominibox که در مقاله پیشین در مورد آن صحبت کردیم و فایل rssreader.js که جهت خواندن rss در پایینتر در موردش بحث میکنیم و فایل contextmenus که این را هم در مطلب پیشین توضیح دادیم.
جهت خواندن فید سایت ما از Google API استفاده میکنیم؛ اینکار دو دلیل دارد:
قبل از اینکه manifst به ورژن 2 برسد ما اجازه داشتیم کدهای جاوااسکریپت به صورت inline در فایلهای html بنویسیم و یا اینکه از منابع و آدرسهای خارجی استفاده کنیم برای مثال یک فایل jquery بر روی وب سایت jquery ؛ ولی از ورژن 2 به بعد، گوگل سیاست امنیت محتوا Content Security Policy را که سورس و سند اصلی آن در اینجا قرار دارد، به سیستم Extension خود افزود تا از حملاتی قبیل XSS و یا تغییر منبع راه دور به عنوان یک malware جلوگیری کند. پس ما از این به بعد نه اجازه داشتیم inline بنویسیم و نه اجازه داشتیم فایل jquery را از روی سرورهای سایت سازنده صدا بزنیم. پس برای حل این مشکل، ابتدا مثل همیشه یک فایل js را در فایل html معرفی میکردیم و برای حل مشکل دوم باید منابع را به صورت محلی استفاده میکردیم؛ یعنی فایل jquery را داخل دایرکتوری extension قرار میدادیم.
برای حل مشکل مشکل صدا زدن فایلهای راه دور ما از Relaxing the Default Policy استفاده میکنیم که به ما یک لیست سفید ارائه میکند و در این لیست سفید دو نکتهی مهم به چشم میخورد که یکی از آن این است که استفاده از آدرس هایی با پروتکل Https و آدرس لوکال local host/127.0.0.1 بلا مانع است و از آنجا که api گوگل یک آدرس Https است، میتوانیم به راحتی از API آن استفاده کنیم. فقط نیاز است تا خط زیر را به manifest.json اضافه کنیم تا این استثناء را برای ما در نظر بگیرد.
آدرسی که ما از گوگل درخواست کردیم فقط مختص خواندن فید نیست؛ تمامی apiهای جاوااسکریپتی در آن قرار دارند و ما تنها نیاز داریم قسمتی از آن لود شود. پس اولین خط از دستور بالا بارگذاری بخش مورد نیاز ما را به عهده دارد. در مورد این دستور این صفحه را مشاهده کنید.
در خط دوم ما تابع خودمان را به آن معرفی میکنیم تا وقتی که گوگل لودش تمام شد این تابع را اجرا کند تا قبل از لود ما از توابع آن استفاده نکنیم و خطای undefined دریافت نکنیم. تابعی که ما از آن خواستیم اجرا کند alarmManager نام دارد و قرار است یک آلارم و یک سیکل زمانی را ایجاد کرده و در هر دوره، فید را بخواند. کد تابع مدنظر به شرح زیر است:
function alarmManager()
{
chrome.storage.local.get(DateContainer.interval,function ( items) {
period_time==items[DateContainer.interval];
chrome.alarms.create('RssInterval', {periodInMinutes: period_time});
});
chrome.alarms.onAlarm.addListener(function (alarm) {
console.log(alarm);
if (alarm.name == 'RssInterval') {
var boolposts,boolpostsComments,boolshares,boolsharesComments;
chrome.storage.local.get([Variables.posts,Variables.postsComments,Variables.shares,Variables.sharesComments],function ( items) {
boolposts=items[Variables.posts];
boolpostsComments=items[Variables.postsComments];
boolshares=items[Variables.shares];
boolsharesComments=items[Variables.sharesComments];
chrome.storage.local.get([DateContainer.posts,DateContainer.postsComments,DateContainer.shares,DateContainer.sharesComments],function ( items) {
var Vposts=new Date(items[DateContainer.posts]);
var VpostsComments=new Date(items[DateContainer.postsComments]);
var Vshares=new Date(items[DateContainer.shares]);
var VsharesComments=new Date(items[DateContainer.sharesComments]);
if(boolposts){var result=RssReader(Links.postUrl,Vposts,DateContainer.posts,Messages.PostsUpdated);}
if(boolpostsComments){var result=RssReader(Links.posts_commentsUrl,VpostsComments,DateContainer.postsComments,Messages.CommentsUpdated); }
if(boolshares){var result=RssReader(Links.sharesUrl,Vshares,DateContainer.shares,Messages.SharesUpdated);}
if(boolsharesComments){var result=RssReader(Links.shares_CommentsUrl,VsharesComments,DateContainer.sharesComments,Messages.SharesCommentsUpdated);}
});
});
}
});
}
خطوط اول تابع alarmManager وظیفهی خواندن مقدار interval را که در init.js ذخیره کردهایم، دارند که به طور پیش فرض 10 ذخیره شده است تا تایمر یا آلارم خود را بر اساس آن بسازیم. در خط chrome.alarms.create یک آلارم با نام rssinterval میسازد و قرار است هر 10 دقیقه وظایفی که بر دوشش گذاشته میشود را اجرا کند (استفاده از api جهت دسترسی به آلارم نیاز به مجوز "alarms" دارد). وظایفش از طریق یک listener که بر روی رویداد chrome.alarms.onAlarm گذاشته شده است مشخص میشود. در خط بعدی مشخص میشود که این رویداد به خاطر چه آلارمی صدا زده شده است. البته از آنجا که ما یک آلارم داریم، نیاز چندانی به این کد نیست. ولی اگر پروژه شما حداقل دو آلارم داشته باشد نیاز است مشخص شود که کدام آلارم باعث صدا زدن این رویداد شده است. در مرحله بعد مشخص میکنیم که کاربر قصد بررسی چه قسمتهایی از سایت را داشته است و در تابع callback آن هم تاریخ آخرین تغییرات هر بخش را میخوانیم و در متغیری نگه داری میکنیم. هر کدام را جداگانه چک کرده و تابع RssReader را برای هر کدام صدا میزنیم. این تابع 4 پارامتر دارد:
آدرس فیدی که قرار است از روی آن بخواند
آخرین به روزسانی که از سایت داشته متعلق به چه تاریخی است.
نام کلید ذخیره سازی تاریخ آخرین تغییر سایت که اگر بررسی شد و مشخص شد سایت به روز شده است، تاریخ جدید را روی آن ذخیره کنیم.
در صورتی که سایت به روز شده باشد نیاز است پیامی را برای کاربر نمایش دهیم که این پیام را در اینجا قرار میدهیم.
کد تابع rssreader
function RssReader(URL,lastupdate,datecontainer,Message) {
var feed = new google.feeds.Feed(URL);
feed.setResultFormat(google.feeds.Feed.XML_FORMAT);
feed.load(function (result) {
if(result!=null)
{
var strRssUpdate = result.xmlDocument.firstChild.firstChild.childNodes[5].textContent;
var RssUpdate=new Date(strRssUpdate);
if(RssUpdate>lastupdate)
{
SaveDateAndShowMessage(datecontainer,strRssUpdate,Message)
}
}
});
}
در خط اول فید توسط گوگل خوانده میشود، در خط بعدی ما به گوگل میگوییم که فید خوانده شده را چگونه به ما تحویل دهد که ما قالب xml را خواسته ایم و در خط بعدی اطلاعات را در متغیری به اسم result قرار میدهد که در یک تابع برگشتی آن را در اختیار ما میگذارد. از آن جا که ما قرار است تگ lastBuildDate را بخوانیم که پنجمین تگ اولین گره در اولین گره به حساب میآید، خط زیر این دسترسی را برای ما فراهم میکند و چون تگ ما در یک مکان ثابت است با همین تکه کد، دسترسی مستقیمی به آن داریم:
var strRssUpdate = result.xmlDocument.firstChild.firstChild.childNodes[5].textContent;
مرحله بعد تاریخ را که در قالب رشتهای است، تبدیل به تاریخ کرده و با lastupdate یعنی آخرین تغییر قبلی مقایسه میکنیم و اگر تاریخ برگرفته از فید بزرگتر بود، یعنی سایت به روز شده است و تابع SaveDateAndShowMessage را صدا میزنیم که وظیفه ذخیره سازی تاریخ جدید و ایجاد notification را به عهده دارد و سه پارامتر کلید ذخیره سازی و مقدار آن و پیام را به آن پاس میکنیم.
خطوط اول مربوط به ذخیره تاریخ است و دومین نکته نحوهی ساخت نوتیفکیشن است. اجرای یک notification نیاز به مجوز "notifications " دارد که مجوز آن در manifest به شرح زیر است:
در خطوط بالا سایر مجوزهایی که در طول این دوره به کار اضافه شده است را هم میبینید.
برای ساخت نوتیفکیشن از کد chrome.notifications.create استفاده میکنیم که پارامتر اول آن کد یکتا یا همان ID جهت ساخت نوتیفیکیشن هست که میتوان خالی گذاشت و دومی تنظیمات ساخت آن است؛ از قبیل عنوان و آیکن و ... که در بالا به اسم options معرفی کرده ایم و در آگومان دوم آن را معرفی کرده ایم و آرگومان سوم هم یک تابع callback است که نوشتن آن اجباری است. options شامل عنوان، پیام، آیکن و نوع notification میباشد که در اینجا basic انتخاب کردهایم. برای دسترسی به دیگر خصوصیتهای options به اینجا و برای داشتن notificationهای زیباتر به عنوان rich notification به اینجا مراجعه کنید. برای اینکه این امکان باشد که کاربر با کلیک روی notification به سایت هدایت شود باید در تابع callback مربوط به notifications.create این کد اضافه گردد که در صورت کلیک یک تب جدید با آدرس سایت ساخته شود:
نکته مهم: پیشتر معرفی آیکن به صورت بالا کفایت میکرد ولی بعد از این باگ کد زیر هم باید جداگانه به manifest اضافه شود:
"web_accessible_resources": [
"icon.png"
]
خوب؛ کار افزونه تمام شده است ولی اجازه دهید این بار امکانات افزونه را بسط دهیم:
من میخواهم برای افزونه نیز قسمت تنظیمات داشته باشم. برای دسترسی به options میتوان از قسمت مدیریت افزونهها در مرورگر یا حتی با راست کلیک روی آیکن browser action عمل کرد. در اصل این قسمت برای تنظیمات افزونه است ولی ما به خاطر آموزش و هم اینکه افزونه ما UI خاصی نداشت تنظیمات را از طریق browser action پیاده سازی کردیم و گرنه در صورتی که افزونه شما شامل UI خاصی مثلا نمایش فید مطالب باشد، بهترین مکان تنظیمات، options است. برای تعریف options در manifest.json به روش زیر اقدام کنید:
"options_page": "popup.html"
همان صفحه popup را در این بخش نشان میدهم و اینبار یک کار اضافهتر دیگر که نیاز به آموزش ندارد اضافه کردن input با Type=number است که برای تغییر interval به کار میرود و نحوه ذخیره و بازیابی آن را در طول دوره یاد گرفته اید.
بعضی صفحات مانند بوک مارک و تاریخچه فعالیتها History و همینطور newtab را میتوانید جایگزین کنید. البته یک اکستنشن میتواند فقط یکی از صفحات را جایگزین کند. برای تعیین جایگزین در manifest اینگونه عمل میکنیم:
شاید فکر کنید کد بالا الان شامل مباحث ui و ... میشود و بعد به مرورگر اعمال خواهد شد؛ در صورتی که اینگونه نیست و نیاز دارد چند خط کدی نوشته شود. ولی مسئله اینست که کد بالا تنها صفحات html را پشتیبانی میکند و مستقیما نمیتواند فایل js را بخواند. پس صفحه بالا را ساخته و کد زیر را داخلش میگذاریم:
خط chrome.devtools.panels.create یک پنل یا همان تب را ساخته و در پارامترهای بالا به ترتیب عنوان، آیکن و صفحهای که باید در آن رندر شود را دریافت میکند و پس از ایجاد یک callback اجرا میشود. اطلاعات بیشتر
APIها
برای دیدن لیست کاملی از APIها میتوانید به مستندات آن رجوع کنید و این مورد را به یاد داشته باشید که ممکن است بعضی apiها در بعضی موارد پاسخ ندهند. به عنوان مثال در content scripts نمیتوانید به chrome.devtools.panels دسترسی داشته باشید یا اینکه در DeveloperTools دسترسی به DOM میسر نیست. پس این مورد را به خاطر داشته باشید. همچنین بعضی apiها از نسخهی خاصی به بعد اضافه شدهاند مثلا همین مثال قبلی devtools از نسخه 18 به بعد اضافه شده است و به این معنی است با خیال راحت میتوانید از آن استفاده کنید. یا آلارمها از نسخه 22 به بعد اضافه شدهاند. البته خوشبختانه امروزه با دسترسی آسانتر به اینترنت و آپدیت خودکار مرورگرها این مشکلات دیگر آن چنان رخ نمیدهند.
Messaging
همانطور که در بالا اشاره شد شما نمیتوانید بعضی از apiها را در بعضی جاها استفاده کنید. برای حل این مشکل میتوان از messaging استفاده کرد که دو نوع تبادلات پیغامی داریم:
One-Time Requests یا درخواستهای تک مرتبهای
Long-Lived Connections یا اتصالات بلند مدت یا مصر
درخواستهای تک مرتبه ای
این درخواستها همانطور که از نامش پیداست تنها یک مرتبه رخ میدهد؛ درخواست را ارسال کرده و منتظر پاسخ میماند. به عنوان مثال به کد زیر که در content script است دقت کنید:
کد بالا یک ارسال کننده پیام است. موقعی که سایتی باز میشود، یک کلید با مقدارش را ارسال میکند و کد زیر در background گوش میایستد تا اگر درخواستی آمد آن را دریافت کند:
اگر نیاز به یک کانال ارتباطی مصر و همیشگی دارید کدها را به شکل زیر تغییر دهید
contentscripts
var port = chrome.runtime.connect({name: "my-channel"});
port.postMessage({myProperty: "value"});
port.onMessage.addListener(function(msg) {
// do some stuff here
});
background
chrome.runtime.onConnect.addListener(function(port) {
if(port.name == "my-channel"){
port.onMessage.addListener(function(msg) {
// do some stuff here
});
}
});
نمونه کد
نمونه کدهایی که در سایت گوگل موجود هست میتوانند کمک بسیاری خوبی باشند ولی اینگونه که پیداست اکثر مثالها مربوط به نسخهی یک manifest است که دیگر توسط مرورگرها پشتیبانی نمیشوند و مشکلاتی چون اسکریپت inline و CSP که در بالا اشاره کردیم را دارند و گوگل کدها را به روز نکرده است.
دیباگ کردن و پک کردن فایلها برای تبدیل به فایل افزونه Debugging and packing
برای دیباگ کردن کدها میتوان از دو نمونه console.log و alert برای گرفتن خروجی استفاده کرد و همچنین ابزار Chrome Apps & Extensions Developer Tool هم نسبتا امکانات خوبی دارد که البته میتوان از آن برای پک کردن اکستنشن نهایی هم استفاده کرد. برای پک کردن روی گزینه pack کلیک کرده و در کادر باز شده گزینهی pack را بزنید. برای شما دو نوع فایل را در مسیر والد دایرکتوری extension نوشته شده درست خواهد کرد که یکی پسوند crx دارد که میشود همان فایل نهایی افزونه و دیگری هم پسوند pem دارد که یک کلید اختصاصی است و باید برای آپدیتهای آینده افزونه آن را نگاه دارید. در صورتی که افزونه را تغییر دادید و خواستید آن را به روز رسانی کنید موقعی که اولین گزینه pack را میزنید و صفحه باز میشود قبل از اینکه دومین گزینه pack را بزنید، از شما میخواهد اگر دارید عملیات به روز رسانی را انجام میدهید، کلید اختصاصی آن را وارد نمایید و بعد از آن گزینه pack را بزنید:
آپلود نهایی کار در Google web store
برای آپلود نهایی کار به google web store که در آن تمامی برنامهها و افزونههای کروم قرار دارند بروید. سمت راست آیکن تنظیمات را بزنید و گزینه developer dashboard را انتخاب کنید تا صفحهی آپلود کار برای شما باز شود. دایرکتوری محتویات اکستنشن را zip کرده و آپلود نمایید. توجه داشته باشید که محتویات و سورس خود را باید آپلود کنید نه فایل crx را. بعد از آپلود موفقیت آمیز، صفحهای ظاهر میشود که از شما آیکن افزونه را در اندازه 128 پیکسل میخواهد بعلاوه توضیحاتی در مورد افزونه، قیمت گذاری که به طور پیش فرض به صورت رایگان تنظیم شده است، لینک وب سایت مرتبط، لینک محل پرسش و پاسخ برای افزونه، اگر لینک یوتیوبی در مورد افزونه دارید، یک شات تصویری از افزونه و همینطور چند تصویر برای اسلایدشو سازی که در همان صفحه استاندارد آنها را توضیح میدهد و در نهایت گزینهی جالبتر هم اینکه اکستنشن شما برای چه مناطقی تهیه شده است که متاسفانه ایران را ندیدم که میتوان همه موارد را انتخاب کرد. به خصوص در مورد ایران که آی پیها هم صحیح نیست، انتخاب ایران چنان تاثیری ندارد و در نهایت گزینهی publish را میزنید که متاسفانه بعد از این صفحه درخواست میکند برای اولین بار باید 5 دلار آمریکا پرداخت شود که برای بسیاری از ما این گزینه ممکن نیست.
سورس پروژه را میتوانید از اینجا ببینید و خود افزونه را از اینجا دریافت کنید.