1. قبل از شروع
استفاده از ابزارهای پشتیبان بدون سرور مانند Cloud Firestore و Cloud Functions بسیار آسان است، اما آزمایش آنها دشوار است. مجموعه شبیه ساز محلی Firebase به شما امکان می دهد نسخه های محلی این سرویس ها را بر روی دستگاه توسعه خود اجرا کنید تا بتوانید سریع و ایمن برنامه خود را توسعه دهید.
پیش نیازها
- یک ویرایشگر ساده مانند Visual Studio Code، Atom یا Sublime Text
- Node.js 10.0.0 یا بالاتر (برای نصب Node.js، از nvm استفاده کنید ، برای بررسی نسخه خود،
node --version
را اجرا کنید) - جاوا 7 یا بالاتر (برای نصب جاوا از این دستورالعمل ها استفاده کنید ، برای بررسی نسخه خود،
java -version
را اجرا کنید)
کاری که خواهی کرد
در این کد لبه، یک برنامه خرید آنلاین ساده را اجرا و اشکال زدایی می کنید که توسط چندین سرویس Firebase پشتیبانی می شود:
- Cloud Firestore: پایگاه داده NoSQL بدون سرور و مقیاس پذیر جهانی با قابلیت های بلادرنگ.
- توابع ابری : یک کد پشتیبان بدون سرور که در پاسخ به رویدادها یا درخواستهای HTTP اجرا میشود.
- Firebase Authentication : یک سرویس احراز هویت مدیریت شده که با سایر محصولات Firebase ادغام می شود.
- میزبانی Firebase : میزبانی سریع و ایمن برای برنامه های وب.
برای فعال کردن توسعه محلی، برنامه را به مجموعه شبیه ساز متصل خواهید کرد.
همچنین یاد خواهید گرفت که چگونه:
- نحوه اتصال برنامه خود به Emulator Suite و نحوه اتصال شبیه سازهای مختلف.
- قوانین امنیتی Firebase چگونه کار می کند و چگونه قوانین امنیتی Firestore را در برابر یک شبیه ساز محلی آزمایش کنید.
- نحوه نوشتن یک تابع Firebase که توسط رویدادهای Firestore راه اندازی می شود و نحوه نوشتن تست های یکپارچه سازی که در مقابل مجموعه شبیه ساز اجرا می شود.
2. راه اندازی کنید
کد منبع را دریافت کنید
در این لبه کد، شما با نسخه ای از نمونه فروشگاه آتش شروع می کنید که تقریباً کامل شده است، بنابراین اولین کاری که باید انجام دهید این است که کد منبع را شبیه سازی کنید:
$ git clone https://github.com/firebase/emulators-codelab.git
سپس به دایرکتوری Codelab بروید، جایی که برای باقیمانده این Codelab کار خواهید کرد:
$ cd emulators-codelab/codelab-initial-state
حالا وابستگی ها را نصب کنید تا بتوانید کد را اجرا کنید. اگر اتصال اینترنت شما کندتر است، ممکن است یک یا دو دقیقه طول بکشد:
# Move into the functions directory
$ cd functions
# Install dependencies
$ npm install
# Move back into the previous directory
$ cd ../
Firebase CLI را دریافت کنید
مجموعه Emulator بخشی از Firebase CLI (رابط خط فرمان) است که می تواند با دستور زیر بر روی دستگاه شما نصب شود:
$ npm install -g firebase-tools
سپس، تأیید کنید که آخرین نسخه CLI را دارید. این کد لبه باید با نسخه 9.0.0 یا بالاتر کار کند، اما نسخه های بعدی دارای رفع اشکال بیشتری هستند.
$ firebase --version 9.6.0
به پروژه Firebase خود متصل شوید
اگر پروژه Firebase ندارید، در کنسول Firebase ، یک پروژه Firebase جدید ایجاد کنید. شناسه پروژه ای را که انتخاب می کنید یادداشت کنید، بعداً به آن نیاز خواهید داشت.
اکنون باید این کد را به پروژه Firebase شما متصل کنیم. ابتدا دستور زیر را برای ورود به Firebase CLI اجرا کنید:
$ firebase login
سپس دستور زیر را برای ایجاد نام مستعار پروژه اجرا کنید. شناسه پروژه Firebase خود را جایگزین $YOUR_PROJECT_ID
کنید.
$ firebase use $YOUR_PROJECT_ID
اکنون شما آماده اجرای برنامه هستید!
3. شبیه سازها را اجرا کنید
در این بخش، برنامه را به صورت محلی اجرا خواهید کرد. این بدان معناست که زمان راهاندازی Emulator Suite فرا رسیده است.
شبیه سازها را راه اندازی کنید
از داخل پوشه منبع کد لبه، دستور زیر را برای راه اندازی شبیه سازها اجرا کنید:
$ firebase emulators:start --import=./seed
شما باید برخی از خروجی ها را مانند این ببینید:
$ firebase emulators:start --import=./seed i emulators: Starting emulators: auth, functions, firestore, hosting ⚠ functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: database, pubsub i firestore: Importing data from /Users/samstern/Projects/emulators-codelab/codelab-initial-state/seed/firestore_export/firestore_export.overall_export_metadata i firestore: Firestore Emulator logging to firestore-debug.log i hosting: Serving hosting files from: public ✔ hosting: Local server: http://127.0.0.1:5000 i ui: Emulator UI logging to ui-debug.log i functions: Watching "/Users/samstern/Projects/emulators-codelab/codelab-initial-state/functions" for Cloud Functions... ✔ functions[calculateCart]: firestore function initialized. ┌─────────────────────────────────────────────────────────────┐ │ ✔ All emulators ready! It is now safe to connect your app. │ │ i View Emulator UI at http://127.0.0.1:4000 │ └─────────────────────────────────────────────────────────────┘ ┌────────────────┬────────────────┬─────────────────────────────────┐ │ Emulator │ Host:Port │ View in Emulator UI │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Functions │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Firestore │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Hosting │ 127.0.0.1:5000 │ n/a │ └────────────────┴────────────────┴─────────────────────────────────┘ Emulator Hub running at 127.0.0.1:4400 Other reserved ports: 4500 Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.
هنگامی که پیام شروع به کار همه شبیه سازها را مشاهده کردید، برنامه آماده استفاده است.
برنامه وب را به شبیه سازها وصل کنید
بر اساس جدول موجود در گزارشها، میبینیم که شبیهساز Cloud Firestore در پورت 8080
گوش میدهد و شبیهساز Authentication در پورت 9099
گوش میدهد.
┌────────────────┬────────────────┬─────────────────────────────────┐ │ Emulator │ Host:Port │ View in Emulator UI │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Functions │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Firestore │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Hosting │ 127.0.0.1:5000 │ n/a │ └────────────────┴────────────────┴─────────────────────────────────┘
بیایید کد ظاهری شما را به جای تولید به شبیه ساز متصل کنیم. فایل public/js/homepage.js
را باز کنید و تابع onDocumentReady
را پیدا کنید. می بینیم که کد به نمونه های استاندارد Firestore و Auth دسترسی دارد:
public/js/homepage.js
const auth = firebaseApp.auth();
const db = firebaseApp.firestore();
بیایید اشیاء db
و auth
را برای اشاره به شبیه سازهای محلی به روز کنیم:
public/js/homepage.js
const auth = firebaseApp.auth();
const db = firebaseApp.firestore();
// ADD THESE LINES
if (location.hostname === "127.0.0.1") {
console.log("127.0.0.1 detected!");
auth.useEmulator("http://127.0.0.1:9099");
db.useEmulator("127.0.0.1", 8080);
}
اکنون هنگامی که برنامه در دستگاه محلی شما اجرا می شود (که توسط شبیه ساز میزبانی سرویس ارائه می شود)، مشتری Firestore نیز به جای پایگاه داده تولید، به شبیه ساز محلی اشاره می کند.
EmulatorUI را باز کنید
در مرورگر وب خود، به http://127.0.0.1:4000/ بروید. باید رابط کاربری Emulator Suite را ببینید.
برای مشاهده UI برای شبیه ساز Firestore کلیک کنید. مجموعه items
قبلاً حاوی داده هایی است زیرا داده های وارد شده با پرچم --import
است.
4. برنامه را اجرا کنید
برنامه را باز کنید
در مرورگر وب خود، به http://127.0.0.1:5000 بروید و باید فروشگاه Fire را ببینید که به صورت محلی روی دستگاه شما اجرا می شود!
از برنامه استفاده کنید
یک مورد را در صفحه اصلی انتخاب کنید و روی افزودن به سبد خرید کلیک کنید. متأسفانه با خطای زیر مواجه خواهید شد:
بیایید آن باگ را برطرف کنیم! از آنجایی که همه چیز در شبیه سازها اجرا می شود، می توانیم آزمایش کنیم و نگران تأثیرگذاری بر داده های واقعی نباشیم.
5. برنامه را اشکال زدایی کنید
اشکال را پیدا کنید
خوب، اجازه دهید به کنسول توسعه دهنده کروم نگاه کنیم. برای مشاهده خطا در کنسول، Control+Shift+J
(ویندوز، لینوکس، سیستم عامل کروم) یا Command+Option+J
(Mac) را فشار دهید:
به نظر می رسد در روش addToCart
خطایی وجود داشته است، بیایید نگاهی به آن بیندازیم. از کجا میخواهیم به چیزی به نام uid
در آن متد دسترسی پیدا کنیم و چرا null
است؟ در حال حاضر روش در public/js/homepage.js
به این شکل است:
public/js/homepage.js
addToCart(id, itemData) {
console.log("addToCart", id, JSON.stringify(itemData));
return this.db
.collection("carts")
.doc(this.auth.currentUser.uid)
.collection("items")
.doc(id)
.set(itemData);
}
آها! ما وارد برنامه نشده ایم. با توجه به اسناد احراز هویت Firebase ، زمانی که ما وارد سیستم نشدهایم، auth.currentUser
null
است. بیایید یک چک برای آن اضافه کنیم:
public/js/homepage.js
addToCart(id, itemData) {
// ADD THESE LINES
if (this.auth.currentUser === null) {
this.showError("You must be signed in!");
return;
}
// ...
}
برنامه را تست کنید
اکنون صفحه را بازخوانی کنید و سپس روی افزودن به سبد خرید کلیک کنید. این بار باید خطای بهتری دریافت کنید:
اما اگر در نوار ابزار بالا گزینه Sign In را بزنید و سپس دوباره Add to Cart را بزنید، می بینید که سبد خرید به روز شده است.
با این حال، به نظر می رسد که اعداد به هیچ وجه صحیح نیستند:
نگران نباشید، به زودی آن باگ را برطرف خواهیم کرد. ابتدا، بیایید عمیقاً به آنچه که هنگام افزودن یک کالا به سبد خرید خود رخ داد، بپردازیم.
6. توابع محلی باعث می شود
کلیک کردن روی افزودن به سبد خرید زنجیره ای از رویدادها را آغاز می کند که شامل چندین شبیه ساز است. در گزارشهای Firebase CLI، پس از افزودن یک مورد به سبد خرید، باید چیزی شبیه پیامهای زیر را مشاهده کنید:
i functions: Beginning execution of "calculateCart" i functions: Finished "calculateCart" in ~1s
چهار رویداد کلیدی برای تولید آن گزارشها و بهروزرسانی رابط کاربری که مشاهده کردید رخ داد:
1) Firestore Write - Client
یک سند جدید به مجموعه Firestore /carts/{cartId}/items/{itemId}/
اضافه شد. می توانید این کد را در تابع addToCart
در داخل public/js/homepage.js
ببینید:
public/js/homepage.js
addToCart(id, itemData) {
// ...
console.log("addToCart", id, JSON.stringify(itemData));
return this.db
.collection("carts")
.doc(this.auth.currentUser.uid)
.collection("items")
.doc(id)
.set(itemData);
}
2) عملکرد ابر فعال شد
Cloud Function calculateCart
به هر رویداد نوشتنی (ایجاد، بهروزرسانی یا حذف) که برای موارد سبد خرید با استفاده از ماشه onWrite
رخ میدهد گوش میدهد، که میتوانید آن را در functions/index.js
ببینید:
functions/index.js
exports.calculateCart = functions.firestore
.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
try {
let totalPrice = 125.98;
let itemCount = 8;
const cartRef = db.collection("carts").doc(context.params.cartId);
await cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
}
);
3) Firestore Write - Admin
تابع calculateCart
همه موارد موجود در سبد خرید را می خواند و کل مقدار و قیمت را جمع می کند، سپس سند "سبد" را با مجموعات جدید به روز می کند (به cartRef.update(...)
بالا مراجعه کنید).
4) Firestore Read - Client
وب سایت برای دریافت به روز رسانی در مورد تغییرات در سبد خرید مشترک است. همانطور که در public/js/homepage.js
میبینید، پس از نوشتن مجموعهای جدید و بهروزرسانی رابط کاربری توسط Cloud Function، بهروزرسانی بیدرنگ دریافت میکند:
public/js/homepage.js
this.cartUnsub = cartRef.onSnapshot(cart => {
// The cart document was changed, update the UI
// ...
});
خلاصه
کارت خوب بود! شما فقط یک برنامه کاملاً محلی راه اندازی کرده اید که از سه شبیه ساز مختلف Firebase برای آزمایش کاملاً محلی استفاده می کند.
اما صبر کنید، چیزهای بیشتری وجود دارد! در بخش بعدی یاد خواهید گرفت:
- نحوه نوشتن تست های واحد که از شبیه سازهای Firebase استفاده می کنند.
- چگونه از شبیه سازهای Firebase برای اشکال زدایی قوانین امنیتی خود استفاده کنید.
7. قوانین امنیتی متناسب با برنامه خود ایجاد کنید
برنامه وب ما دادهها را میخواند و مینویسد، اما تاکنون واقعاً نگران امنیت نبودهایم. Cloud Firestore از سیستمی به نام "قوانین امنیتی" برای اعلام اینکه چه کسی به خواندن و نوشتن داده ها دسترسی دارد استفاده می کند. مجموعه Emulator یک راه عالی برای نمونه سازی اولیه این قوانین است.
در ویرایشگر، فایل emulators-codelab/codelab-initial-state/firestore.rules
را باز کنید. خواهید دید که ما سه بخش اصلی در قوانین خود داریم:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// User's cart metadata
match /carts/{cartID} {
// TODO: Change these! Anyone can read or write.
allow read, write: if true;
}
// Items inside the user's cart
match /carts/{cartID}/items/{itemID} {
// TODO: Change these! Anyone can read or write.
allow read, write: if true;
}
// All items available in the store. Users can read
// items but never write them.
match /items/{itemID} {
allow read: if true;
}
}
}
در حال حاضر هر کسی می تواند داده ها را در پایگاه داده ما بخواند و بنویسد! ما میخواهیم مطمئن شویم که فقط عملیات معتبر انجام میشود و هیچ اطلاعات حساسی را درز نمیکنیم.
در طول این کد، با پیروی از اصل حداقل امتیاز، همه اسناد را قفل می کنیم و به تدریج دسترسی را اضافه می کنیم تا زمانی که همه کاربران تمام دسترسی های مورد نیاز خود را داشته باشند، اما نه بیشتر. بیایید دو قانون اول را به روز کنیم تا با تنظیم شرط روی false
، دسترسی را رد کنیم:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// User's cart metadata
match /carts/{cartID} {
// UPDATE THIS LINE
allow read, write: if false;
}
// Items inside the user's cart
match /carts/{cartID}/items/{itemID} {
// UPDATE THIS LINE
allow read, write: if false;
}
// All items available in the store. Users can read
// items but never write them.
match /items/{itemID} {
allow read: if true;
}
}
}
8. شبیه سازها و تست ها را اجرا کنید
شبیه سازها را راه اندازی کنید
در خط فرمان، مطمئن شوید که در emulators-codelab/codelab-initial-state/
هستید. ممکن است همچنان شبیه سازهای مراحل قبلی در حال اجرا باشند. اگر نه، شبیه سازها را دوباره راه اندازی کنید:
$ firebase emulators:start --import=./seed
هنگامی که شبیه سازها اجرا می شوند، می توانید آزمایشات محلی را بر روی آنها اجرا کنید.
تست ها را اجرا کنید
در خط فرمان در یک تب ترمینال جدید از دایرکتوری emulators-codelab/codelab-initial-state/
ابتدا به دایرکتوری توابع بروید (ما برای باقیمانده از Codelab اینجا خواهیم ماند):
$ cd functions
اکنون تست های موکا را در پوشه توابع اجرا کنید و به بالای خروجی بروید:
# Run the tests $ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping carts 1) can be created and updated by the cart owner 2) can be read only by the cart owner shopping cart items 3) can be read only by the cart owner 4) can be added only by the cart owner adding an item to the cart recalculates the cart total. - should sum the cost of their items 0 passing (364ms) 1 pending 4 failing
در حال حاضر چهار شکست داریم. همانطور که فایل قوانین را میسازید، میتوانید پیشرفت را با تماشای آزمونهای بیشتری اندازهگیری کنید.
9. دسترسی ایمن به سبد خرید
دو شکست اول تست های "سبد خرید" هستند که آزمایش می کنند:
- کاربران فقط می توانند سبد خرید خود را ایجاد و به روز کنند
- کاربران فقط می توانند سبد خرید خود را بخوانند
functions/test.js
it('can be created and updated by the cart owner', async () => {
// Alice can create her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").set({
ownerUID: "alice",
total: 0
}));
// Bob can't create Alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart").set({
ownerUID: "alice",
total: 0
}));
// Alice can update her own cart with a new total
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").update({
total: 1
}));
// Bob can't update Alice's cart with a new total
await firebase.assertFails(bobDb.doc("carts/alicesCart").update({
total: 1
}));
});
it("can be read only by the cart owner", async () => {
// Setup: Create Alice's cart as admin
await admin.doc("carts/alicesCart").set({
ownerUID: "alice",
total: 0
});
// Alice can read her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").get());
// Bob can't read Alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart").get());
});
بیایید این آزمون ها را پاس کنیم. در ویرایشگر، فایل قوانین امنیتی، firestore.rules
باز کنید و عبارات موجود در match /carts/{cartID}
را به روز کنید:
firestore.قوانین
rules_version = '2';
service cloud.firestore {
// UPDATE THESE LINES
match /carts/{cartID} {
allow create: if request.auth.uid == request.resource.data.ownerUID;
allow read, update, delete: if request.auth.uid == resource.data.ownerUID;
}
// ...
}
}
این قوانین اکنون فقط به صاحب سبد خرید اجازه خواندن و نوشتن را می دهد.
برای تأیید داده های ورودی و تأیید اعتبار کاربر، از دو شی استفاده می کنیم که در زمینه هر قانون در دسترس هستند:
- شی
request
حاوی داده ها و فراداده های مربوط به عملیاتی است که در حال تلاش است. - اگر یک پروژه Firebase از احراز هویت Firebase استفاده می کند، شی
request.auth
کاربری را که درخواست می کند توصیف می کند.
10. تست دسترسی به سبد خرید
هر زمان که firestore.rules
ذخیره شود، مجموعه Emulator به طور خودکار قوانین را به روز می کند. میتوانید تأیید کنید که شبیهساز قوانین بهروز شده را با نگاه کردن به برگهای که شبیهساز را اجرا میکند، برای پیغام Rules updated
:
تست ها را دوباره اجرا کنید و بررسی کنید که دو تست اول در حال حاضر موفق باشند:
$ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping carts ✓ can be created and updated by the cart owner (195ms) ✓ can be read only by the cart owner (136ms) shopping cart items 1) can be read only by the cart owner 2) can be added only by the cart owner adding an item to the cart recalculates the cart total. - should sum the cost of their items 2 passing (482ms) 1 pending 2 failing
آفرین! اکنون دسترسی به سبد خرید را ایمن کرده اید. بیایید به سراغ آزمون مردود بعدی برویم.
11. جریان «افزودن به سبد خرید» را در رابط کاربری بررسی کنید
در حال حاضر، اگرچه صاحبان سبد خرید در سبد خرید خود می خوانند و می نویسند، اما نمی توانند موارد جداگانه را در سبد خرید خود بخوانند یا بنویسند. دلیل آن این است که در حالی که مالکان به سند سبد خرید دسترسی دارند، به زیر مجموعه اقلام سبد خرید دسترسی ندارند.
این یک وضعیت خراب برای کاربران است.
به رابط کاربری وب که در http://127.0.0.1:5000,
در حال اجرا است برگردید و سعی کنید چیزی به سبد خرید خود اضافه کنید. شما یک خطای Permission Denied
دریافت می کنید که از کنسول اشکال زدایی قابل مشاهده است، زیرا ما هنوز به کاربران اجازه دسترسی به اسناد ایجاد شده در زیر مجموعه items
را نداده ایم.
12. اجازه دسترسی به اقلام سبد خرید را بدهید
این دو تست تأیید میکنند که کاربران فقط میتوانند مواردی را به سبد خرید خود اضافه کنند یا مواردی را از آن بخوانند:
it("can be read only by the cart owner", async () => {
// Alice can read items in her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart/items/milk").get());
// Bob can't read items in alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart/items/milk").get())
});
it("can be added only by the cart owner", async () => {
// Alice can add an item to her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart/items/lemon").set({
name: "lemon",
price: 0.99
}));
// Bob can't add an item to alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart/items/lemon").set({
name: "lemon",
price: 0.99
}));
});
بنابراین میتوانیم قاعدهای بنویسیم که در صورتی که کاربر فعلی همان UID مالک را در سند سبد خرید داشته باشد، اجازه دسترسی را میدهد. از آنجایی که نیازی به تعیین قوانین مختلف برای create, update, delete
نیست، میتوانید از یک قانون write
استفاده کنید که برای همه درخواستهایی که دادهها را تغییر میدهند اعمال میشود.
قانون مدارک موجود در زیر مجموعه اقلام را به روز کنید. get
در شرطی خواندن یک مقدار از Firestore است – در این مورد، ownerUID
در سند سبد خرید.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ...
// UPDATE THESE LINES
match /carts/{cartID}/items/{itemID} {
allow read, write: if get(/databases/$(database)/documents/carts/$(cartID)).data.ownerUID == request.auth.uid;
}
// ...
}
}
13. تست دسترسی به سبد خرید
حالا می توانیم تست را دوباره اجرا کنیم. به بالای خروجی بروید و بررسی کنید که تست های بیشتری قبول شده باشند:
$ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping carts ✓ can be created and updated by the cart owner (195ms) ✓ can be read only by the cart owner (136ms) shopping cart items ✓ can be read only by the cart owner (111ms) ✓ can be added only by the cart owner adding an item to the cart recalculates the cart total. - should sum the cost of their items 4 passing (401ms) 1 pending
خوب! اکنون همه آزمون های ما قبول می شوند. ما یک آزمایش معلق داریم، اما در چند مرحله به آن خواهیم رسید.
14. دوباره جریان "افزودن به سبد خرید" را بررسی کنید
به صفحه جلویی وب ( http://127.0.0.1:5000 ) بازگردید و یک مورد را به سبد خرید اضافه کنید. این یک گام مهم برای تأیید این است که آزمایشها و قوانین ما با عملکرد مورد نیاز مشتری مطابقت دارند. (به یاد داشته باشید که آخرین باری که ما امتحان کردیم، کاربران رابط کاربری نتوانستند مواردی را به سبد خرید خود اضافه کنند!)
هنگامی که firestore.rules
ذخیره می شود، کلاینت به طور خودکار قوانین را دوباره بارگیری می کند. بنابراین، سعی کنید چیزی به سبد خرید اضافه کنید.
خلاصه
کارت خوب بود! شما به تازگی امنیت برنامه خود را بهبود بخشیده اید، یک گام اساسی برای آماده سازی آن برای تولید! اگر این یک برنامه تولیدی بود، میتوانیم این آزمایشها را به خط لوله ادغام مداوم خود اضافه کنیم. این به ما اطمینان می دهد که اطلاعات سبد خرید ما این کنترل های دسترسی را خواهند داشت، حتی اگر دیگران قوانین را تغییر دهند.
اما صبر کنید، چیزهای بیشتری وجود دارد!
اگر ادامه دهید یاد خواهید گرفت:
- نحوه نوشتن تابعی که توسط یک رویداد Firestore راه اندازی شده است
- نحوه ایجاد تست هایی که روی چندین شبیه ساز کار می کنند
15. تست های Cloud Functions را تنظیم کنید
تا کنون ما بر روی قسمت جلویی برنامه وب خود و قوانین امنیتی Firestore تمرکز کردهایم. اما این برنامه همچنین از Cloud Function ها برای به روز نگه داشتن سبد خرید کاربر استفاده می کند، بنابراین ما می خواهیم آن کد را نیز آزمایش کنیم.
مجموعه Emulator آزمایش عملکردهای Cloud را بسیار آسان می کند، حتی عملکردهایی که از Cloud Firestore و سایر خدمات استفاده می کنند.
در ویرایشگر، فایل emulators-codelab/codelab-initial-state/functions/test.js
باز کنید و به آخرین آزمایش موجود در فایل بروید. در حال حاضر، به عنوان معلق علامت گذاری شده است:
// REMOVE .skip FROM THIS LINE
describe.skip("adding an item to the cart recalculates the cart total. ", () => {
// ...
it("should sum the cost of their items", async () => {
...
});
});
برای فعال کردن تست، .skip
حذف کنید، به این صورت است:
describe("adding an item to the cart recalculates the cart total. ", () => {
// ...
it("should sum the cost of their items", async () => {
...
});
});
سپس، متغیر REAL_FIREBASE_PROJECT_ID
را در بالای فایل پیدا کنید و آن را به ID پروژه Firebase واقعی خود تغییر دهید.
// CHANGE THIS LINE
const REAL_FIREBASE_PROJECT_ID = "changeme";
اگر ID پروژه خود را فراموش کرده اید، می توانید ID پروژه Firebase خود را در تنظیمات پروژه در کنسول Firebase پیدا کنید:
16. در تست های توابع قدم بزنید
از آنجایی که این تست تعامل بین Cloud Firestore و Cloud Functions را تایید میکند، نسبت به تستهای کدهای قبلی، تنظیمات بیشتری را شامل میشود. بیایید از طریق این آزمون قدم برداریم و ایده ای از آنچه انتظار دارد به دست آوریم.
یک سبد خرید ایجاد کنید
توابع ابری در یک محیط سرور قابل اعتماد اجرا می شوند و می توانند از احراز هویت حساب سرویس استفاده شده توسط Admin SDK استفاده کنند. ابتدا، یک برنامه را با استفاده از initializeAdminApp
به جای initializeApp
مقداردهی اولیه می کنید. سپس، یک DocumentReference برای سبد خریدی که مواردی را به آن اضافه می کنیم ایجاد می کنید و سبد را مقداردهی اولیه می کنید:
it("should sum the cost of their items", async () => {
const db = firebase
.initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
.firestore();
// Setup: Initialize cart
const aliceCartRef = db.doc("carts/alice")
await aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });
...
});
عملکرد را فعال کنید
سپس، اسناد را به زیر مجموعه items
سند سبد خرید اضافه کنید تا عملکرد را فعال کنید. دو مورد اضافه کنید تا مطمئن شوید که در حال آزمایش اضافه کردنی هستید که در تابع اتفاق می افتد.
it("should sum the cost of their items", async () => {
const db = firebase
.initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
.firestore();
// Setup: Initialize cart
const aliceCartRef = db.doc("carts/alice")
await aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });
// Trigger calculateCart by adding items to the cart
const aliceItemsRef = aliceCartRef.collection("items");
await aliceItemsRef.doc("doc1").set({name: "nectarine", price: 2.99});
await aliceItemsRef.doc("doc2").set({ name: "grapefruit", price: 6.99 });
...
});
});
انتظارات آزمون را تنظیم کنید
از onSnapshot()
برای ثبت شنونده برای هرگونه تغییر در سند سبد خرید استفاده کنید. onSnapshot()
تابعی را برمی گرداند که می توانید برای لغو ثبت شنونده آن را فراخوانی کنید.
برای این تست، دو مورد اضافه کنید که مجموعا 9.98 دلار قیمت دارند. سپس، بررسی کنید که آیا سبد خرید itemCount
مورد انتظار و totalPrice
دارد یا خیر. اگر چنین است، پس تابع کار خود را انجام داد.
it("should sum the cost of their items", (done) => {
const db = firebase
.initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
.firestore();
// Setup: Initialize cart
const aliceCartRef = db.doc("carts/alice")
aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });
// Trigger calculateCart by adding items to the cart
const aliceItemsRef = aliceCartRef.collection("items");
aliceItemsRef.doc("doc1").set({name: "nectarine", price: 2.99});
aliceItemsRef.doc("doc2").set({ name: "grapefruit", price: 6.99 });
// Listen for every update to the cart. Every time an item is added to
// the cart's subcollection of items, the function updates `totalPrice`
// and `itemCount` attributes on the cart.
// Returns a function that can be called to unsubscribe the listener.
await new Promise((resolve) => {
const unsubscribe = aliceCartRef.onSnapshot(snap => {
// If the function worked, these will be cart's final attributes.
const expectedCount = 2;
const expectedTotal = 9.98;
// When the `itemCount`and `totalPrice` match the expectations for the
// two items added, the promise resolves, and the test passes.
if (snap.data().itemCount === expectedCount && snap.data().totalPrice == expectedTotal) {
// Call the function returned by `onSnapshot` to unsubscribe from updates
unsubscribe();
resolve();
};
});
});
});
});
17. تست ها را اجرا کنید
ممکن است همچنان شبیهسازهای آزمایشهای قبلی را داشته باشید. اگر نه، شبیه سازها را راه اندازی کنید. از خط فرمان، اجرا کنید
$ firebase emulators:start --import=./seed
یک برگه ترمینال جدید باز کنید (امولاتورها را در حال اجرا بگذارید) و به فهرست توابع بروید. ممکن است هنوز این مورد را از آزمایش قوانین امنیتی باز داشته باشید.
$ cd functions
اکنون تست های واحد را اجرا کنید، باید 5 تست را مشاهده کنید:
$ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping cart creation ✓ can be created by the cart owner (82ms) shopping cart reads, updates, and deletes ✓ cart can be read by the cart owner (42ms) shopping cart items ✓ items can be read by the cart owner (40ms) ✓ items can be added by the cart owner adding an item to the cart recalculates the cart total. 1) should sum the cost of their items 4 passing (2s) 1 failing
اگر به خرابی خاص نگاه کنید، به نظر میرسد که خطای مهلت زمانی است. این به این دلیل است که آزمایش منتظر است تا عملکرد به درستی به روز شود، اما هرگز انجام نمی شود. اکنون، ما آماده هستیم تا تابع را بنویسیم تا تست را برآورده کنیم.
18. یک تابع بنویسید
برای رفع این تست، باید تابع را در functions/index.js
بهروزرسانی کنید. اگرچه برخی از این تابع نوشته شده است، اما کامل نیست. عملکرد در حال حاضر به این صورت است:
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
let totalPrice = 125.98;
let itemCount = 8;
try {
const cartRef = db.collection("carts").doc(context.params.cartId);
await cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
});
این تابع به درستی مرجع سبد خرید را تنظیم می کند، اما سپس به جای محاسبه مقادیر totalPrice
و itemCount
، آنها را به مقادیر سخت کد شده به روز می کند.
واکشی و تکرار از طریق
زیر مجموعه items
یک ثابت جدید، itemsSnap
راهاندازی کنید تا زیر مجموعه items
باشد. سپس، تمام اسناد موجود در مجموعه را تکرار کنید.
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
try {
let totalPrice = 125.98;
let itemCount = 8;
const cartRef = db.collection("carts").doc(context.params.cartId);
// ADD LINES FROM HERE
const itemsSnap = await cartRef.collection("items").get();
itemsSnap.docs.forEach(item => {
const itemData = item.data();
})
// TO HERE
return cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
});
مجموع قیمت و تعداد آیتم را محاسبه کنید
ابتدا، اجازه دهید مقادیر totalPrice
و itemCount
را صفر کنیم.
سپس، منطق را به بلوک تکرار خود اضافه کنید. ابتدا بررسی کنید که کالا دارای قیمت باشد. اگر مورد مقدار مشخصی ندارد، آن را به طور پیشفرض روی 1
بگذارید. سپس، مقدار را به مجموع در حال اجرا itemCount
اضافه کنید. در نهایت، قیمت کالا را ضرب در مقدار به مجموع در حال اجرا totalPrice
اضافه کنید:
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
try {
// CHANGE THESE LINES
let totalPrice = 0;
let itemCount = 0;
const cartRef = db.collection("carts").doc(context.params.cartId);
const itemsSnap = await cartRef.collection("items").get();
itemsSnap.docs.forEach(item => {
const itemData = item.data();
// ADD LINES FROM HERE
if (itemData.price) {
// If not specified, the quantity is 1
const quantity = itemData.quantity ? itemData.quantity : 1;
itemCount += quantity;
totalPrice += (itemData.price * quantity);
}
// TO HERE
})
await cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
});
همچنین میتوانید برای کمک به اشکالزدایی وضعیتهای موفقیت آمیز و خطا، ورود به سیستم را اضافه کنید:
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
let totalPrice = 0;
let itemCount = 0;
try {
const cartRef = db.collection("carts").doc(context.params.cartId);
const itemsSnap = await cartRef.collection("items").get();
itemsSnap.docs.forEach(item => {
const itemData = item.data();
if (itemData.price) {
// If not specified, the quantity is 1
const quantity = (itemData.quantity) ? itemData.quantity : 1;
itemCount += quantity;
totalPrice += (itemData.price * quantity);
}
});
await cartRef.update({
totalPrice,
itemCount
});
// OPTIONAL LOGGING HERE
console.log("Cart total successfully recalculated: ", totalPrice);
} catch(err) {
// OPTIONAL LOGGING HERE
console.warn("update error", err);
}
});
19. تکرار تست ها
در خط فرمان، مطمئن شوید که شبیه سازها هنوز در حال اجرا هستند و تست ها را دوباره اجرا کنید. شما نیازی به راه اندازی مجدد شبیه سازها ندارید زیرا آنها به طور خودکار تغییرات عملکردها را دریافت می کنند. شما باید تمام تست ها را قبول کنید:
$ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping cart creation ✓ can be created by the cart owner (306ms) shopping cart reads, updates, and deletes ✓ cart can be read by the cart owner (59ms) shopping cart items ✓ items can be read by the cart owner ✓ items can be added by the cart owner adding an item to the cart recalculates the cart total. ✓ should sum the cost of their items (800ms) 5 passing (1s)
آفرین!
20. آن را با استفاده از رابط کاربری Storefront امتحان کنید
برای تست نهایی، به برنامه وب ( http://127.0.0.1:5000/ ) برگردید و یک مورد را به سبد خرید اضافه کنید.
تأیید کنید که سبد خرید با مجموع صحیح به روز می شود. خارق العاده!
خلاصه
شما از طریق یک مورد آزمایشی پیچیده بین Cloud Functions for Firebase و Cloud Firestore قدم زدهاید. شما یک Cloud Function برای قبولی در آزمون نوشتید. شما همچنین تأیید کردید که عملکرد جدید در UI کار می کند! شما همه این کارها را به صورت محلی انجام دادید و شبیه سازها را روی دستگاه خود اجرا کردید.
شما همچنین یک سرویس گیرنده وب ایجاد کرده اید که در برابر شبیه سازهای محلی اجرا می شود، قوانین امنیتی را برای محافظت از داده ها تنظیم کرده اید، و قوانین امنیتی را با استفاده از شبیه سازهای محلی آزمایش کرده اید.