برنامه نویسی ۹۰۱ بازدید

در این راهنما یک اپلیکیشن ساده می‌سازیم که کارت‌هایی دارای تصویر را به صورت ناهمگام در یک اپلیکیشن انگولار از سرور اکسپرس Node.js واکشی کرده و تنها پس از این که کاربر تا انتهای صفحه اسکرول کرد، بقیه موارد را بارگذاری می‌کند. به این ترتیب با امکان Lazy Loading در انگولار آشنا خواهیم شد.

Lazy loading در انگولار

پیش‌نیازها

  • آشنایی با HTML ،CSS و جاوا اسکریپت
  • آشنایی با انگولار یا یک فریمورک دیگر جاوا اسکریپت
  • درکی مقدماتی از درخواست‌های HTTP
  • دانش مقدماتی از Node.js

Lazy Loading چیست؟

«بارگذاری کند» (Lazy Loading) یا «بارگذاری بنا به تقاضا» (on-demand loading) روشی برای بارگذاری محتوا در یک وب‌سایت پس از درخواست کاربر برای دیدن آن‌ها محسوب می‌شود.

Lazy loading در انگولار
زبانه network کروم در زمان اسکرول کردن روی یک صفحه

در مثال فوق می‌بینید که کارت‌های اضافی تنها پس از اینکه کاربر شروع به اسکرول کردن بکند، بارگذاری می‌شوند.

چرا باید از Lazy Loading استفاده کنیم؟

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

شروع

ما قصد داریم کار را با ایجاد یک پوشه در دایرکتوری پروژه آغاز کنیم. ترمینال خود را باز کرده و دستور زیر را در آن وارد نمایید:

mkdir server

چنان که احتمالاً حدس می‌زنید، سرور اکسپرس در پوشه server اجرا خواهد شد، در حالی که اپلیکیشن انگولار ما در پوشه client ساخته می‌شود که متعاقباً از طریق CLI آن را ایجاد خواهیم کرد.

کار روی سرور

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

npm init -y

برای اجرای سرور به Express نیاز دارید:

npm i express

و Nodemon برای بارگذاری خودکار پس از ایجاد تغییرات در کد مورد نیاز است:

npm i nodemon –D

فایل package.json را در کد ادیتور باز کنید و اسکریپت‌های زیر را به آن اضافه کنید:

"scripts": {
  "start": "node server",
   "dev": "nodemon server"
},

یک فایل جدید به نام server.js ایجاد کرده و کد زیر را در آن قرار دهید:

const express = require('express');

const app = express();

//set headers
app.use(function (req, res, next) {
    res.header('Access-Control-Allow-Origin', '*');
    res.header(
        'Access-Control-Allow-Headers',
        'Origin, X-Requested-With, Content-Type, Accept'
    );
    next();
});

// items API Routes
app.use('/api/items', require('./routes/api/items'));

const PORT = process.env.PORT || 5000;

app.listen(PORT, () => console.log(`Server started on port ${PORT}`));

فایل Server.js به فایل items.js در مسیر api/items/ سرویس می‌دهد که هنوز ایجاد نکرده‌ایم و از این رو آن را همراه با پوشه‌های مورد نیاز برای شبیه‌سازی فایل داده ایجاد می‌کنیم. به این منظور دایرکتوری‌ها و فایل‌های زیر را ایجاد کنید:

/server
  server.js
  /data
    items_list.js
  /routes
    /api
      items.js

برای شبیه‌سازی داده‌ها فایل زیر را دانلود کنید:

https://github.com/railaru/angular-lazy-load/blob/master/server/data/item_list.js

یک نقطه انتهایی API ساده با کارکرد صفحه‌بندی در فایل items.js ایجاد می‌کنیم:

const express = require('express');
const router = express.Router();
const items = require('../../data/item_list');

// Get All items
router.get('/', (req, res) => res.json(items));

// Paginate Items
router.get('/page/:page_number/amount/:page_amount', (req, res) => {
  const page = parseInt(req.params.page_number) - 1;
  const pageAmount = parseInt(req.params.page_amount);
  const found = items.slice(page * pageAmount, (page + 1) * pageAmount);

  if (found) {
    setTimeout(() => {
      res.json(found);
    }, 1000)
  } else {
    res.status(400).json({ msg: `No items with the specified parameters` });
  }
});

module.exports = router;

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

npm run dev

اپلیکیشن Postman (+) را باز کنید و یک درخواست تست GET به نقطه انتهایی در فایل items.js ارسال کنید. زمانی که مقادیر page/2 و amount/2 را به عنوان پارامتر برای نقطه انتهایی API ارسال کنیم، مقاله‌های 3 و 4 بارگذاری می‌شوند.

Lazy loading در انگولار
لطفاً برای نمایش در اندازه بزرگتر روی تصویر کلیک کنید.

کار روی کلاینت

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

تولید کد آماده

مطمئن شوید که Angular CLI (+) نصب شده و کد آماده‌ای برای client ایجاد کنید:

ng new client

SCSS را به عنوان «پیش پردازشگر» (preprocessor) خود انتخاب کنید. ما به یک سرویس انگولار و برخی کامپوننت‌ها برای اپلیکیشن خود نیاز داریم و از این رو از CLI برای تولید آن‌ها استفاده می‌کنیم. درون پوشه client/ یک سرویس می‌سازیم که با API بک‌اند که در بخش قبلی ساختیم، صحبت می‌کند:

ng g s api

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

ng g c grid && ng g c card && ng g c card-shimmer

در ادامه باید یک اینترفیس ایجاد کنیم که کد فرانت‌اند را از نظر نوع بررسی می‌کند. یک فایل به نام item.interface.ts ایجاد کنید و کد زیر را به آن اضافه کنید:

export default class ItemInterface {

  id: number;
  title: string;
  type: string;
  img: string;
  imgLarge: string;
  description: string;
  text: string;
  tags: string[];

  constructor(id: number, title: string, type: string, img: string, imgLarge: string, description: string, text: string, tags: string[]) {

    this.id = id;
    this.title = title;
    this.type = type;
    this.img = img;
    this.imgLarge = imgLarge;
    this.description = description;
    this.text = text;
    this.tags = tags;
  }
}

2 فایل آخر که نیاز داریم، برای استایل‌بندی استفاده می‌شوند. بنابراین فایل‌های ‎_typograhpy.scss و ‎_animations.scss را بسازید:

فایل ‎_typography.scss

@import url('https://fonts.googleapis.com/css?family=Roboto:100,100i,300,300i,400,400i,500,500i,700,700i,900,900i');

body{
  margin: 0;
  font-family: 'Roboto', sans-serif;
}

a{
  text-decoration: none;
}

فایل ‎_animations.scss

.spinner-container {
  display: flex;
  margin-top: 50px;
}

.spinner {
  width: 2.5rem;
  height: 2.5rem;
  border-top-color: #4285f4;
  border-left-color: #4285f4;

  animation: spinner 400ms linear infinite;
  border-bottom-color: transparent;
  border-right-color: transparent;
  border-style: solid;
  border-width: 2px;
  border-radius: 50%;
  margin: auto;
}

@keyframes spinner {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.shine {
  $shine-opacity: 5%;
  background: darken(#f6f7f8, $shine-opacity)  linear-gradient(
      to right,
      darken(#f6f7f8, $shine-opacity) 0%,
      darken(#edeef1, $shine-opacity) 20%,
      darken(#f6f7f8, $shine-opacity) 40%,
      darken(#f6f7f8, $shine-opacity) 100%
  ) no-repeat;
  background-size: 800px 400px;
  display: inline-block;
  position: relative;

  animation-duration: 1s;
  animation-fill-mode: forwards;
  animation-iteration-count: infinite;
  animation-name: placeholder-shimmer;
  animation-timing-function: linear;
}

@keyframes placeholder-shimmer {
  0% {
    background-position: -468px 0;
  }
  100% {
    background-position: 468px 0;
  }
}

پس از ایجاد فایل‌های ‎.scss باید مطمئن شوید که آن‌ها را در style.scss ایمپورت کرده‌اید. در نهایت باید اپلیکیشن فرانت‌اند خود را کمی سازمان‌یافته‌تر بکنیم. کد آماده را سازماندهی می‌کنیم و در ساختار زیر قرار می‌دهیم:

/client/src/app/
  /components
    /containers
      /grid
    /presentationals
      /card
      /card-shimmer
  /services
    api.service.ts
    api.service.spec.ts
  /interfaces
    item.interface.ts
  /style
    _animations.scss
    _typography.scss

پیاده‌سازی کارکرد فرانت‌اند

اکنون که کد آماده برای اپلیکیشن انگولار تولید شده است، می‌توانیم شروع به اتصال آن به API اکسپرس کرده و آیتم‌های صفحه‌بندی شده را عرضه کنیم:

http://localhost:5000/api/items/page/1/amount/12

کد زیر را به فایل api.service.ts اضافه کنید:

import { Injectable } from '@angular/core';

import { HttpClient } from '@angular/common/http';

import { Observable } from 'rxjs';

import ItemInterface from '../interfaces/item.interface';

@Injectable({
  providedIn: 'root'
})

export class ApiService {

  private pageNr = 1;

  constructor(private http: HttpClient) { }

  fetchItems(): Observable<ItemInterface[]> {
    return this.http.get<ItemInterface[]>(`http://localhost:5000/api/items/page/${this.pageNr}/amount/8`);
  }

  paginatePage(): void {
    this.pageNr ++;
  }
}
  • متد ()fetchItems یک observable با نوع ItemInterface[]‎ بازمی‌گرداند. ما قادر هستیم در مقادیری که این متد از سرور می‌گیرد مشترک (Subscribe) شویم.
  • متد ()paginatePage صرفاً شماره صفحه‌ها را برای URL درخواست API افزایش می‌دهد به طوری که هر بار که فراخوانی شود، می‌توانیم آیتم‌های جدیدی از سرور بگیریم و آن‌ها را در زمان اسکرول صفحه از سوی کاربر به وی نشان دهیم.

اینک داده‌ها را در اپلیکیشن انگولار داریم، اما برای نمایش آن به کاربر باید به سرویس API انگولار وصل شویم و مقادیر دریافتی را به «کامپوننت‌های ارائه‌ای» (presentational components) ارسال کنیم. کد زیر را به کامپوننت grid اضافه کنید:

import { Component, OnInit } from '@angular/core';
import { ApiService } from '../../../services/api.service';

import ItemInterface from '../../../interfaces/item.interface';

@Component({
  selector: 'app-grid',
  templateUrl: './grid.component.html',
  styleUrls: ['./grid.component.scss']
})
export class GridComponent implements OnInit {

  constructor(private apiService: ApiService) {}

  cards: ItemInterface[] = [];
  isLoading = false;
  loadedAll = false;
  isFirstLoad = true;

  ngOnInit(): void {

    this.getCards();
    this.handleScroll();
  }

  getCards(): void {

    this.isLoading = true;
    this.apiService.fetchItems().subscribe(res => {
        if (res.length) {
          this.cards.push(...res);
        } else {
          this.loadedAll = true;
        }
        this.isLoading = false;
        this.isFirstLoad = false;
    });
  }

  handleScroll(): void {

    window.onscroll = () => this.detectBottom();
  }

  detectBottom(): void {

      if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
        if (!this.loadedAll) {
          this.apiService.paginatePage();
          this.getCards();
        }
      }
  }
}

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

نکته: مطمئن شوید که HttpClientModule را به imports و ApiService را به ارائه‌دهنده درون app.module.ts اضافه کرده‌اید.

لی‌آوت زیر را درون grid.component.html اضافه کنید:

<div class="grid" *ngIf="isFirstLoad">
  <app-card-shimmer *ngFor="let cardShimmer of [0, 1, 2, 3, 4, 5, 6, 7]"></app-card-shimmer>
</div>

<ng-container *ngIf="!isFirstLoad">
  <div class="grid">
    <app-card
      *ngFor="let card of cards"
      [type]="card.type"
      [title]="card.title"
      [img]="card.img"
      [description]="card.description"
      [tags]="card.tags">
    </app-card>
  </div>
  <div *ngIf="isLoading" class="spinner-container">
    <span class="spinner"></span>
  </div>
</ng-container>

یک شبکه واکنش‌گرای ساده را با استفاده از CSS grid می‌سازیم:

.grid {
  display: grid;
  @media (min-width: 768px) {
    grid-template-columns: 1fr 1fr;
  }
  @media (min-width: 1024px) {
    grid-template-columns: 1fr 1fr 1fr 1fr;
  }
  grid-gap: 20px;
}

کامپوننت Card باید مقادیر ارسالی از کامپوننت grid را دریافت کند و از این رو آن‌ها را از طریق دکوراتور Input@ انگولار ارسال می‌کنیم:

فایل card.component.ts

import {Component, Input, OnInit} from '@angular/core';

@Component({
  selector: 'app-card',
  templateUrl: './card.component.html',
  styleUrls: ['./card.component.scss']
})
export class CardComponent implements OnInit {

  constructor() { }

  @Input() type: string;
  @Input() title: string;
  @Input() img: string;
  @Input() description: string;
  @Input() tags: string[];

  ngOnInit() {
  }

}

فایل card.component.html

<a href='#' class="card">
  <img src="{{img}}" alt="" class="card__img">
  <div class="card__content">
    <div class="card__type">
      {{type}}
    </div>
    <div class="card__title">
      {{title}}
    </div>
    <div class="card__description">
      {{description}}
    </div>
  </div>
</a>

فایل card.component.scss

.card{
  $hover-transition: .4s;
  box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
  transition: $hover-transition;
  display: block;
  position: relative;
  &__img {
    width: 100%;
    height: 140px;
    object-fit: cover;
    object-position: center;
  }
  &__content {
    padding: 29px 35px 35px;
  }
  &__type {
    font-size: 12px;
    letter-spacing: 0.3px;
    color: #9aa0a6;
    font-weight: 700;
    text-transform: uppercase;
  }
  &__title {
    margin-top: 10px;
    color: #3c4043;
    font-size: 18px;
    font-weight: 500;
    letter-spacing: normal;
    line-height: 26px;
    transition: $hover-transition;
  }
  &__description {
    margin-top: 15px;
    padding-bottom: 20px;
    color: #5f6368;
    font-size: 14px;
    letter-spacing: normal;
    line-height: 22px;
  }
  &:hover, &:focus {
    transition: $hover-transition;
    box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
    .card__title {
      transition: $hover-transition;
      color: #4285f4;
    }
  }
}

در ادامه مقداری استایل‌بندی «سوسوزدن» (shimmer) به کارت‌ها می‌دهیم تا بتوانیم به جای صفحه خالی پیش از بارگذاری اولیه محتوا، آن‌ها را ببینیم:

فایل card-shimmer.component.html

<div class='article-card-shimmer'>
  <div class="shimmer shine article-card-shimmer__img"></div>
  <div class='article-card-shimmer__text'>
    <div class="shimmer shine article-card-shimmer__category"></div>
    <div class="shimmer shine article-card-shimmer__title"></div>
    <div class="shimmer shine article-card-shimmer__details"></div>
  </div>
</div>

فایل card-shimmer.component.scss

.article-card-shimmer {
  &__img {
    width: 100%;
    height: 140px;
  }
  &__text {
    margin: 0 35px;
    margin-top: 30px;
    display: flex;
    flex-flow: column nowrap;
  }
  &__category {
    width: 30%;
    height: 12px;
  }
  &__title {
    width: 60%;
    height: 18px;
    margin-top: 15px;
  }
  &__details {
    margin-top: 30px;
    width: 90%;
    height: 100px;
  }
}

کد آماده را از فایل app.component.html حذف می‌کنیم و به کامپوننت grid که درون یک کانتینر قرار دارد می‌آوریم:

فایل app.component.html

<div class="container">
  <app-grid></app-grid>
</div>

فایل app.component.scss

.container {
  max-width: 1200px;
  padding: 0 25px 50px;
  margin: 50px auto 0;
}

اینک یک اپلیکیشن عالی با قابلیت بارگذاری کُند (lazy loading) داریم. اطمینان حاصل کنید که سرور در حال اجرا است و دستور زیر را درون پوشه client/ اجرا کنید تا اپلیکیشن انگولار اجرا شود:

ng serve –o

Lazy loading در انگولار

بدین ترتیب به پایان این مقاله می‌رسیم.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۲ نفر
آیا این مطلب برای شما مفید بود؟
شما قبلا رای داده‌اید!
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.

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