การ cache GET Request และการใช้งาน HttpClient ขั้นสูง ตอนที่ 5

เขียนเมื่อ 6 ปีก่อน โดย Ninenik Narkdee
httpclientmodule http cache httpclient interceptor angular

คำสั่ง การ กำหนด รูปแบบ ตัวอย่าง เทคนิค ลูกเล่น การประยุกต์ การใช้งาน เกี่ยวกับ httpclientmodule http cache httpclient interceptor angular

ดูแล้ว 6,571 ครั้ง


ในเนื้อหาตอนที่ 5 นี้ เราจะมาดู การใช้งาน HttpClient ขั้นสูง กันต่อ โดยจะเริ่มในเรื่องของการ caching
หรือการ cache ค่าที่ได้จากการ request และเพื่อจะให้เห็นภาพการทำงานชัดเจน เราจะขอกลับไปใช้งาน
เกี่ยวกับการ cache ใน ArticleModule จากบทความ
    ดึงข้อมูล แสดงรายการพร้อมแบ่งหน้า ด้วย HttpClient ใน Angular ตอนที่ 2 http://niik.in/853 
 
โดยทั่วไปแล้ว การ cache ข้อมูลจะนิยมใช้กับกรณีที่การ Request แบบ GET เพื่อดึงข้อมูลจาก backend service มาแสดง
หรือมาใช้งาน เบื้องต้นให้เราสร้างไฟล์ interceptor มาใช้งานใน ArticleModule ดังนี้
 
C:\projects\httpclient>ng g class article/article-http-interceptor
 
กำหนดโค้ดเบื้องต้นดังนี้
 
ไฟล์ article-http-interceptor.ts
 
import {Injectable} from '@angular/core';
import {HttpEvent, HttpInterceptor, 
        HttpHandler,
        HttpResponse,
        HttpRequest} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';     
import 'rxjs/add/operator/do';  

@Injectable()
export class ArticleHttpInterceptor implements HttpInterceptor {
    
   constructor(){}
    
   intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
     console.log('Interceptor request ran..');
     const started = Date.now();  // เวลาเริ่มต้นที่ request     
     return next.handle(req).do(event => {
        if (event instanceof HttpResponse) {
            // หาช่วงเวลาที่ใช้ในการรับส่งข้อมูล เวลาที่ response ส่งค่ากลับมา ลบด้วยเวลา เริ่มต้น
            const elapsed = Date.now() - started; 
            // แสดง url ที่ทำการ request ไป ว่าใช้เวลาไปกี่ มิลลิวินาที
            console.log(`Request for ${req.urlWithParams} took ${elapsed} ms.`);            
        }
      });
   }

}
 
 
ไฟล์ article.module.ts
 
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HTTP_INTERCEPTORS } from '@angular/common/http';

import { ArticleRoutingModule } from './article-routing.module';
import { ArticleComponent } from './article.component';
import { ArticleListComponent } from './article-list.component';
import { ArticleDetailComponent } from './article-detail.component';

import { ArticleHttpInterceptor } from './article-http-interceptor';

@NgModule({
  imports: [
    CommonModule,
    ArticleRoutingModule
  ],
  providers: [{
    provide: HTTP_INTERCEPTORS,
    useClass: ArticleHttpInterceptor,
    multi: true,
  }],    
  declarations: [ArticleComponent, ArticleListComponent, ArticleDetailComponent]
})
export class ArticleModule { }
 
 
จากนั้นทดสอบเรียกข้อมูลที่หน้า article ที่มีการ request แบบ get ดึงข้อมูลมาแสดง เราจะเห็นว่า มีการแทรกการทำงาน
ของ HttpClient เกิดขึ้น ข้างต้นเราเป็นเช็คว่า เวลาที่ใช้ในการ request และ response เป็นเท่าไหร่ แสดงดังรูป
 
 


 
 
สังเกตเพิ่มเติมที่ขีดเส้นใต้บรรทัดของไฟล์ใน console สีเขียวกับสีแดง จะเห็นมีการเรียกให้ interceptor ทั้งหมด
ทำงาน ตามที่ได้บอกในตอนที่แล้วว่า ถ้าเรามีการกำหนด interceptor หลายอัน ตัว interceptor ก็จะทำงานทั้งหมด
ตามลำดับการกำหนดหรือ import เข้าไป กล่าวคือ ใน RegisterModule เรามีการใช้งาน Interceptor ผ่านไฟล์
my-http-interceptor.ts และใน ArticleModule เราก็เพิ่งเพิ่ม interceptor ด้วยไฟล์ article-http-interceptor.ts
โดยไฟล์ทั้งสอง ตอนนี้เราให้ทำงานเหมือนกัน คือดูการใช้เวลาในการ request และ response และเนื่องจากในไฟล์
app.module.ts เรา import ArtcileModule กับ RegisterModule ตามลำดับดังนี้
 
ไฟล์ app.module.ts บางส่วน
 
  imports: [
    BrowserModule,
    FormsModule,
    HttpClientModule,
    ArticleModule,
    RegisterModule,
    AppRoutingModule
  ],
 
ดังนั้นลำดับการทำงานของ interceptor ของ ArticleModule จึงทำงานก่อน แล้วก็ interceptor ของ RegisterModule
ทำงานทีหลังตามลำดับการ import เข้ามา
    จะเห็นไฟล์ interceptor ทั้งสองไม่ได้มีความเกี่ยวเนื่องกัน แต่กลับทำงานถึงสองครั้ง ซึ่งถ้าการทำงานของไฟล์
ทั้งสองทำงานต่างกัน ก็จะมีผลกับค่าที่ได้จากการกำหนด interceptor วิธีแก้ปํญหา ให้ interceptor ของแต่ละส่วน
ทำงานตามหน้าทีหรือเงื่อนไขของตัวมันเอง คือเราต้องทำการตรวจสอบค่า url หรือ urlWithParams ว่าเป็นของไฟล์
ทีเราต้องการดังนี้ โดยในไฟล์ my-http-interceptor.ts เรามีการ request ไปที่ url 
http://localhost/demo/show_data.php เราก็จะเช็ค ก่อนว่า เป็นการเรียกไปที่ url ที่ลงท้ายด้วย show.data.php หรือไม่
ดังนี้
 
ไฟล์ my-http-interceptor.ts บางส่วน
 
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
	console.log(req.url);
	console.log(req.urlWithParams);
	if(req.url.endsWith('show_data.php')){                
		console.log('Interceptor ran..');
		const started = Date.now();  // เวลาเริ่มต้นที่ request          
		return next
		.handle(req)
		.do(event => {
			if (event instanceof HttpResponse) {
				// หาช่วงเวลาที่ใช้ในการรับส่งข้อมูล เวลาที่ response ส่งค่ากลับมา ลบด้วยเวลา เริ่มต้น
				const elapsed = Date.now() - started; 
				// แสดง url ที่ทำการ request ไป ว่าใช้เวลาไปกี่ มิลลิวินาที
				console.log(`Request for ${req.urlWithParams} took ${elapsed} ms.`);
			}
		});            

	}else{
		return next.handle(req);
	}       
}
 
การตรวจสอบด้วย req.url.endsWith() คือการหาว่าค่าจาก req.url ลงท้ายด้วย "show_data.php" หรือไม่ ถ้าใช้
ก็ให้เข้าไปทำงานเงื่อนไขการกำหนด interceptor ในไฟล์นี้ แต่ถ้าไม่ใช้ ก็ให้ส่งผ่านไป ทำงานที่ไฟล์อืนค่อถ้ามี
ไฟล์ในลำดับถัดไปรอรับการทำงานอยู่ 
    เชนเดียวกับไฟล์ article-http-interceptor.ts เราต้องกำหนดการทำงานในลักษณะคล้ายกันเข้าไป ดังนี้
 
ไฟล์ article-http-interceptor.ts บางส่วน
 
console.log(req.url);
console.log(req.urlWithParams);
// if(req.urlWithParams=="https://jsonplaceholder.typicode.com/posts"){      
if(req.url.endsWith('/posts')){     
	console.log('Interceptor request ran..');
	const started = Date.now();  // เวลาเริ่มต้นที่ request     
	return next.handle(req).do(event => {
		if (event instanceof HttpResponse) {
			// หาช่วงเวลาที่ใช้ในการรับส่งข้อมูล เวลาที่ response ส่งค่ากลับมา ลบด้วยเวลา เริ่มต้น
			const elapsed = Date.now() - started; 
			// แสดง url ที่ทำการ request ไป ว่าใช้เวลาไปกี่ มิลลิวินาที
			console.log(`Request for ${req.urlWithParams} took ${elapsed} ms.`);            
		}
	});
}else{
	return next.handle(req);
}
 
เนื่องจากไฟล์นี้เราเรียกไปยัง url https://jsonplaceholder.typicode.com/posts ดังนั้นเราจึงเช็ค ค่า string ท้าย
ของ url เป็น req.url.endsWith('/posts') นั่นหมายถึง ถ้ามีการเรียกไปยัง url ที่ลงท้ายด้วย /posts ก็ให้เข้ามาทำงาน
ในส่วน interceptor ของไฟล์นี้ แต่ถ้าไม่ใช่ ก็ให้ส่งผ่าน ไปทำงานของไฟล์อื่นถ้ามี
 
หลังจากกำหนดเงื่อนไขการทำงานของไฟล์ interceptor ทั้งสองไฟล์แล้ว เรามาลองทดสอบเรียกไปหน้า article
ใหม่อีกครั้งจะได้ผลลัพธ์ตามรูปดังนี้

 


 
 
จะเห็นว่ามีการเรียกไฟล์ interceptor ทั้งสองไฟล์เหมือนเดิมตามปกติ แต่ที่ต่างไป หลังจากเราใส่เงื่อนไข
ก็คือ ไฟล์ที่มีการเข้าไปทำงานตามเงื่อนไขก็จะเป็นไฟล์ article-http-interceptor.ts โดยสังเกตจากมีค่า result
ส่งกลับออกมา ทั้งนี้ก็เพราะ url ที่เรา request ไปเป็น url ที่ใช้ใน article และเข้าเงื่อนไขการทำงานที่เรากำหนด
ขึ้นมานั่นเอง
 
กลับมาที่เรื่องการ cache ข้อมูลของเรากันต่อ การ cache ข้อมูลส่วนใหญ่แล้ว ก็จะเกี่ยวกับการบันทึกข้อมูล ไม่ว่า
จะเป็นการบันทึกในหน่วยความจำ เป็นไฟล์ หรืออื่นๆ โดย angular ให้แนวทางสำหรับการสร้างคำส่ง หรือ service 
ในการจัดการ cache ตามโครงสร้างนามธรรมดังนี้
 
abstract class HttpCache {
    abstract get(req: HttpRequest<any>): HttpResponse<any>|null;
    abstract put(req: HttpRequest<any>, resp: HttpResponse<any>): void;
}
 
service สำหรับใช้งาน cache ก็ควรจะมีการไปดึงค่าจาก cache มาใช้เช่นกำหนดเป็นคำสั่ง get() โดยคืนค่าเป็น 
HttpResponse หรือเป็น null กรณีไม่มีค่า 
และการนำค่าไปบันทึกเช่นกำหนดด้วยคำสั่ง put() โดยไม่ต้องคืนค่าใดๆ 
 
ในที่นี้เราจะยังไม่ลงในส่วนของการสร้าง service สำหรับการ cache และยังไม่ได้มีการทำการบันทึกข้อมูลใดๆ 
แต่จะใช้แนวทางการ cache อย่างง่าย เพื่อให้เห็นแนวทางการทำงานดังนี้
 
ไฟล์ article-http-interceptor.ts
 
import {Injectable} from '@angular/core';
import {HttpEvent, HttpInterceptor, 
        HttpHandler,
        HttpResponse,
        HttpRequest} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';     
import 'rxjs/add/operator/do';  


@Injectable()
export class ArticleHttpInterceptor implements HttpInterceptor {
   public cacheData = {};

   constructor(){}

   intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        
        if(req.url.endsWith('/posts')){    
            console.log("request ran..")            
            if(req.method !== 'GET') {
                return next.handle(req);
            }
            let url = req.url;

            const cachedResponse = this.cacheData[url] || null;
            if (cachedResponse) {
                console.log("use cache");
                return Observable.of(cachedResponse);
            }      

            return next.handle(req).do(event => {
                if (event instanceof HttpResponse) {
                    console.log("response ran..")   
                    this.cacheData[req.url] = event;
                }
            });
        }else{
            return next.handle(req);
        }        
 
   }

}
 
รูปแบบการ cache อย่างง่ายเบื้องต้น เราสร้างตัวแปร Object มา 1 ตัว ชื่อ cacheData ไว้สำหรับเก็บข้อมูล
cache ซึ่งเป็นค่า HttpResponse ที่ได้จาก backend service ในครั้งแรก จากนั้นเมื่อมีการเรียกดูข้อมูลค่านี้
อีกครั้ง ก็จะใช้จากค่าที่ทำการ cache แทน โดยไม่ให้เข้าไปทำการดึงข้อมูลจาก backend service ที่ฝั่ง
server อีก แบบนี้เป็นต้น
    อย่างไรก็ตาม ถ้ามีอัพเดทการใช้งานรูปแบบการ cache ที่ซับซ้อน เช่นการบันทึกด้วย indexDB , webSQL หรือ
แม้แต่ localStorage ที่สามารถนำมาใช้ได้แล้ว จะนำเสนอเป็นบทความต่อๆ ไป
 
ทดสอบความแตกต่างของข้อมูลในหน้า article ของ demo 1 (ไม่มี cache) และ demo 2 (มี cache) จะเห็น
ความแตกต่างของการโหลดข้อมูล 
    สำหรับเนื้อหาของการใช้งาน HttpClient ขั้นสูงในหัวข้อที่เหลือจะนำเสนอต่อไป รอติดตาม
 





กด Like หรือ Share เป็นกำลังใจ ให้มีบทความใหม่ๆ เรื่อยๆ น่ะครับ



อ่านต่อที่บทความ









เนื้อหาที่เกี่ยวข้อง









URL สำหรับอ้างอิง





คำแนะนำ และการใช้งาน

สมาชิก กรุณา ล็อกอินเข้าระบบ เพื่อตั้งคำถามใหม่ หรือ ตอบคำถาม สมาชิกใหม่ สมัครสมาชิกได้ที่ สมัครสมาชิก


  • ถาม-ตอบ กรุณา ล็อกอินเข้าระบบ
  • เปลี่ยน


    ( หรือ เข้าใช้งานผ่าน Social Login )







เว็บไซต์ของเราให้บริการเนื้อหาบทความสำหรับนักพัฒนา โดยพึ่งพารายได้เล็กน้อยจากการแสดงโฆษณา โปรดสนับสนุนเว็บไซต์ของเราด้วยการปิดการใช้งานตัวปิดกั้นโฆษณา (Disable Ads Blocker) ขอบคุณครับ