خواندن 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.addListener(function(details) {
var now=String(new Date());
var params={};
params[Variables.posts]=true;
params[Variables.postsComments]=false;
params[Variables.shares]=false;
params[Variables.sharesComments]=false;
params[DateContainer.interval]=1;
params[DateContainer.posts]=now;
params[DateContainer.postsComments]=now;
params[DateContainer.shares]=now;
params[DateContainer.sharesComments]=now;
chrome.storage.local.set(params, function() {
if(chrome.runtime.lastError)
{
/* error */
console.log(chrome.runtime.lastError.message);
return;
}
});
});
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 میتوانید به صورت زیر عمل کنید:
"background": {
"scripts": ["const.js","init.js"]
}
در این حالت خود اکستنشن در زمان نصب یک فایل html درست کرده و این دو فایل js را در آن صدا میزند که البته خود ما هم میتوانیم اینکار را مستقیما انجام دهیم. مزیت اینکه ما خودمان مسقیما این کار را انجام دهیم این است که در صورتی که فایلهای js ما زیاد شوند، فایل manifest.jason زیادی شلوغ شده و شکل زشتی پیدا میکند و بهتر است این فایل را تا آنجا که میتوانیم خلاصه نگه داریم. البته روش بالا برای دو یا سه تا فایل js بسیار خوب است ولی اگر به فرض بشود 10 تا یا بیشتر بهتر است یک فایل جداگانه شود و من به همین علت فایل background.htm را درست کرده و به صورت زیر تعریف کردهام:
"background": {
"page": "background.htm"
}
background.htm
<html>
<head>
<script type="text/javascript" src="const.js"></script>
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
<script type="text/javascript" src="init.js"></script>
<script type="text/javascript" src="omnibox.js"></script>
<script type="text/javascript" src="rssreader.js"></script>
<script type="text/javascript" src="contextmenus.js"></script>
</head>
<body>
</body>
</html>
لینکهای بالا به ترتیب معرفی ثابتها، لینک api گوگل که بعدا بررسی میشود، فایل init.js برای ذخیره مقادیر پیش فرض، فایل ominibox که در مقاله پیشین در مورد آن صحبت کردیم و فایل rssreader.js که جهت خواندن rss در پایینتر در موردش بحث میکنیم و فایل contextmenus که این را هم در مطلب پیشین توضیح دادیم.
جهت خواندن فید سایت ما از Google API استفاده میکنیم؛ اینکار دو دلیل دارد:
- کدنویسی راحتتر و خلاصهتر برای خواندن RSS
- استفاده اجباری از یک پروکسی به خاطر Content Security Policy و حتی CORS
قبل از اینکه 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 اضافه کنیم تا این استثناء را برای ما در نظر بگیرد.
"content_security_policy": "script-src 'self' https://*.google.com; object-src 'self'"
در اینجا استفاده از هر نوع subdomain در سایت گوگل بلامانع اعلام میشود.
بنابراین آدرس زیر به background.htm اضافه میشود:
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
استفاده از این Api در rssreader.js فایل rssreader.js را به background.htm اضافه میکنیم و در آن کد زیر را مینویسیم:
google.load("feeds", "1");
google.setOnLoadCallback(alarmManager);
آدرسی که ما از گوگل درخواست کردیم فقط مختص خواندن فید نیست؛ تمامی 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 را به عهده دارد و سه پارامتر کلید ذخیره سازی و مقدار آن و پیام را به آن پاس میکنیم.
کد تابع SaveDateAndShowMesage
function SaveDateAndShowMessage(DateField,DateValue,Message)
{
var params={
}
params[DateField]=DateValue;
chrome.storage.local.set( params,function(){
var options={
type: "basic",
title: Messages.SiteUpdated,
message: Message,
iconUrl: "icon.png"
}
chrome.notifications.create("",options,function(){
chrome.notifications.onClicked.addListener(function(){
chrome.tabs.create({'url': WebLinks.Home}, function(tab) {
});
});
});
});
}
خطوط اول مربوط به ذخیره تاریخ است و دومین نکته نحوهی ساخت نوتیفکیشن است. اجرای یک notification نیاز به مجوز "notifications " دارد که مجوز آن در manifest به شرح زیر است:
"permissions": [
"storage",
"tabs",
"alarms",
"notifications"
]
در خطوط بالا سایر مجوزهایی که در طول این دوره به کار اضافه شده است را هم میبینید.
برای ساخت نوتیفکیشن از کد chrome.notifications.create استفاده میکنیم که پارامتر اول آن کد یکتا یا همان ID جهت ساخت نوتیفیکیشن هست که میتوان خالی گذاشت و دومی تنظیمات ساخت آن است؛ از قبیل عنوان و آیکن و ... که در بالا به اسم options معرفی کرده ایم و در آگومان دوم آن را معرفی کرده ایم و آرگومان سوم هم یک تابع callback است که نوشتن آن اجباری است. options شامل عنوان، پیام، آیکن و نوع notification میباشد که در اینجا basic انتخاب کردهایم. برای دسترسی به دیگر خصوصیتهای options به
اینجا و برای داشتن notificationهای زیباتر به عنوان rich notification به
اینجا مراجعه کنید. برای اینکه این امکان باشد که کاربر با کلیک روی notification به سایت هدایت شود باید در تابع callback مربوط به notifications.create این کد اضافه گردد که در صورت کلیک یک تب جدید با آدرس سایت ساخته شود:
chrome.notifications.create("",options,function(){
chrome.notifications.onClicked.addListener(function(){
chrome.tabs.create({'url': WebLinks.Home}, function(tab) {
});});
});
نکته مهم: پیشتر معرفی آیکن به صورت بالا کفایت میکرد ولی بعد از
این باگ کد زیر هم باید جداگانه به 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 اینگونه عمل میکنیم:
"chrome_url_overrides": {
"newtab": "newtab.htm"
}
ایجاد یک تب اختصاصی در Developer Tools
تکه کدی که باید manifest اضافه شود:
"devtools_page": "devtools.htm"
شاید فکر کنید کد بالا الان شامل مباحث ui و ... میشود و بعد به مرورگر اعمال خواهد شد؛ در صورتی که اینگونه نیست و نیاز دارد چند خط کدی نوشته شود. ولی مسئله اینست که کد بالا تنها صفحات html را پشتیبانی میکند و مستقیما نمیتواند فایل js را بخواند. پس صفحه بالا را ساخته و کد زیر را داخلش میگذاریم:
<script src="devtools.js"></script>
فایل devtools.js هم شامل کد زیر میشود:
chrome.devtools.panels.create(
"Dotnettips Updater Tools",
"icon.png",
"devtoolsui.htm",
function(panel) {
}
);
خط 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 یا اتصالات بلند مدت یا مصر
درخواستهای تک مرتبه ای