เลเยอร์การเข้าถึงข้อมูลสากลสำหรับเว็บแอปพลิเคชัน
โดยทั่วไปบนเซิร์ฟเวอร์คุณเรียก API หรือฐานข้อมูลของคุณโดยตรงเพื่อดึงข้อมูลบางส่วน อย่างไรก็ตามในลูกค้าคุณไม่สามารถโทรหาบริการของคุณในลักษณะเดียวกัน (เช่นนโยบายข้ามโดเมน) จะต้องมีการร้องขอ XHR/FETCH ไปยังเซิร์ฟเวอร์ซึ่งได้รับการส่งต่อไปยังบริการของคุณ
การเขียนโค้ดต่างกันสำหรับสภาพแวดล้อมทั้งสองนั้นซ้ำซ้อนและเกิดข้อผิดพลาดได้ง่าย FetchR ให้เลเยอร์ที่เป็นนามธรรมเหนือการเรียกใช้บริการข้อมูลของคุณเพื่อให้คุณสามารถดึงข้อมูลโดยใช้ API เดียวกันบนเซิร์ฟเวอร์และฝั่งไคลเอ็นต์
npm install fetchr --save สำคัญ: เมื่ออยู่ในเบราว์เซอร์ Fetchr จะอาศัย API Fetch อย่างเต็มที่ หากคุณต้องการสนับสนุนเบราว์เซอร์เก่าคุณจะต้องติดตั้งโพลีฟิลเช่นกัน (เช่น https://github.com/github/fetch)
ทำตามขั้นตอนด้านล่างเพื่อตั้งค่า FetchR อย่างถูกต้อง สิ่งนี้จะถือว่าคุณใช้เฟรมเวิร์กด่วน
ทางฝั่งเซิร์ฟเวอร์ให้เพิ่ม Middleware FetchR ลงในแอพ Express ของคุณที่จุดสิ้นสุด API ที่กำหนดเอง
Middleware FetchR คาดว่าคุณจะใช้มิดเดิลแวร์ body-parser (หรือมิดเดิลแวร์ทางเลือกที่เติมเงิน req.body ) ก่อนที่คุณจะใช้มิดเดิลแวร์ FetchR
import express from 'express' ;
import Fetcher from 'fetchr' ;
import bodyParser from 'body-parser' ;
const app = express ( ) ;
// you need to use body-parser middleware before fetcher middleware
app . use ( bodyParser . json ( ) ) ;
app . use ( '/myCustomAPIEndpoint' , Fetcher . middleware ( ) ) ; ในด้าน xhrPath เอน
xhrPath เป็นคุณสมบัติกำหนดค่าที่เป็นตัวเลือกที่ช่วยให้คุณปรับแต่งจุดสิ้นสุดให้เป็นบริการของคุณค่าเริ่มต้นเป็น /api
import Fetcher from 'fetchr' ;
const fetcher = new Fetcher ( {
xhrPath : '/myCustomAPIEndpoint' ,
} ) ; คุณจะต้องลงทะเบียนบริการข้อมูลใด ๆ ที่คุณต้องการใช้ในแอปพลิเคชันของคุณ อินเทอร์เฟซสำหรับบริการของคุณจะเป็นวัตถุที่ต้องกำหนดคุณสมบัติ resource และการดำเนินการอย่างน้อยหนึ่ง CRUD คุณสมบัติ resource จะถูกใช้เมื่อคุณเรียกหนึ่งในการดำเนินการ CRUD
// app.js
import Fetcher from 'fetchr' ;
import myDataService from './dataService' ;
Fetcher . registerService ( myDataService ) ; // dataService.js
export default {
// resource is required
resource : 'data_service' ,
// at least one of the CRUD methods is required
read : async function ( { req , resource , params , config } ) {
return { data : 'foo' } ;
} ,
// other methods
// create: async function({ req, resource, params, body, config }) {},
// update: async function({ req, resource, params, body, config }) {},
// delete: async function({ req, resource, params, config }) {}
} ;บริการข้อมูลอาจต้องเข้าถึงคำขอแต่ละรายการเช่นเพื่อรับเซสชันการเข้าสู่ระบบปัจจุบันของผู้ใช้ ด้วยเหตุผลนี้ผู้ดึงข้อมูลจะต้องได้รับการสร้างอินสแตนซ์หนึ่งครั้งต่อคำขอ
บนเซิร์ฟเวอร์สิ่งนี้ต้องการให้ Fetcher เป็นอินสแตนซ์ต่อคำขอในมิดเดิลแวร์ด่วน บนไคลเอนต์ไซด์สิ่งนี้จะต้องเกิดขึ้นในการโหลดหน้าเว็บเท่านั้น
// app.js - server
import express from 'express' ;
import Fetcher from 'fetchr' ;
import myDataService from './dataService' ;
const app = express ( ) ;
// register the service
Fetcher . registerService ( myDataService ) ;
// register the middleware
app . use ( '/myCustomAPIEndpoint' , Fetcher . middleware ( ) ) ;
app . use ( function ( req , res , next ) {
// instantiated fetcher with access to req object
const fetcher = new Fetcher ( {
xhrPath : '/myCustomAPIEndpoint' , // xhrPath will be ignored on the serverside fetcher instantiation
req : req ,
} ) ;
// perform read call to get data
fetcher
. read ( 'data_service' )
. params ( { id : 42 } )
. then ( ( { data , meta } ) => {
// handle data returned from data fetcher in this callback
} )
. catch ( ( err ) => {
// handle error
} ) ;
} ) ; // app.js - client
import Fetcher from 'fetchr' ;
const fetcher = new Fetcher ( {
xhrPath : '/myCustomAPIEndpoint' , // xhrPath is REQUIRED on the clientside fetcher instantiation
} ) ;
fetcher
. read ( 'data_api_fetcher' )
. params ( { id : 42 } )
. then ( ( { data , meta } ) => {
// handle data returned from data fetcher in this callback
} )
. catch ( ( err ) => {
// handle errors
} ) ;
// for create you can use the body() method to pass data
fetcher
. create ( 'data_api_create' )
. body ( { some : 'data' } )
. then ( ( { data , meta } ) => {
// handle data returned from data fetcher in this callback
} )
. catch ( ( err ) => {
// handle errors
} ) ; ดูตัวอย่างง่ายๆ
บริการโทรบนไคลเอนต์อย่างโปร่งใสกลายเป็นคำขอดึงข้อมูล เป็นความคิดที่ดีที่จะตั้งค่าส่วนหัวแคชในการโทรหาทั่วไป คุณสามารถทำได้โดยการจัดหาพารามิเตอร์ที่สามในการโทรกลับของบริการ หากคุณต้องการดูว่าส่วนหัวถูกกำหนดโดยบริการที่คุณเพิ่งโทรมาเพียงตรวจสอบพารามิเตอร์ที่สามในการโทรกลับ
หมายเหตุ: หากคุณกำลังใช้สัญญาข้อมูลเมตาจะมีอยู่ในคุณสมบัติ meta ของค่าที่ได้รับการแก้ไข
// dataService.js
export default {
resource : 'data_service' ,
read : async function ( { req , resource , params , config } ) {
return {
data : 'response' , // business logic
meta : {
headers : {
'cache-control' : 'public, max-age=3600' ,
} ,
statusCode : 200 , // You can even provide a custom statusCode for the fetch response
} ,
} ;
} ,
} ; fetcher
. read ( 'data_service' )
. params ( { id : ### } )
. then ( ( { data , meta } ) {
// data will be 'response'
// meta will have the header and statusCode from above
} ) ; มีวิธีการอำนวยความสะดวกที่เรียกว่า fetcher.getServiceMeta บนอินสแตนซ์ FetchR วิธีนี้จะส่งคืนข้อมูลเมตาสำหรับการโทรทั้งหมดที่เกิดขึ้นในรูปแบบอาร์เรย์ ในเซิร์ฟเวอร์สิ่งนี้จะรวมการเรียกใช้บริการทั้งหมดสำหรับคำขอปัจจุบัน ในไคลเอนต์สิ่งนี้จะรวมการโทรบริการทั้งหมดสำหรับเซสชันปัจจุบัน
โดยปกติแล้วคุณจะยกตัวอย่างตัวเลือกเริ่มต้นด้วยตัวเลือกเริ่มต้นสำหรับเซสชันเบราว์เซอร์ทั้งหมด แต่อาจมีกรณีที่คุณต้องการอัปเดตตัวเลือกเหล่านี้ในภายหลังในเซสชันเดียวกัน
คุณสามารถทำได้ด้วยวิธีการ updateOptions :
// Start
const fetcher = new Fetcher ( {
xhrPath : '/myCustomAPIEndpoint' ,
xhrTimeout : 2000 ,
} ) ;
// Later, you may want to update the xhrTimeout
fetcher . updateOptions ( {
xhrTimeout : 4000 ,
} ) ; เมื่อเกิดข้อผิดพลาดในวิธีการ Fetchr CRUD ของคุณคุณควรโยนวัตถุข้อผิดพลาด วัตถุข้อผิดพลาดควรมี statusCode (ค่าเริ่มต้น 500) และคุณสมบัติ output ที่มีวัตถุ JSON serializable ซึ่งจะถูกส่งไปยังไคลเอนต์
export default {
resource : 'FooService' ,
read : async function create ( req , resource , params , configs ) {
const err = new Error ( 'it failed' ) ;
err . statusCode = 404 ;
err . output = { message : 'Not found' , more : 'meta data' } ;
err . meta = { foo : 'bar' } ;
throw err ;
} ,
} ;และในการโทรของคุณ:
fetcher
. read ( 'someData' )
. params ( { id : '42' } )
. catch ( ( err ) => {
// err instanceof FetchrError -> true
// err.message -> "Not found"
// err.meta -> { foo: 'bar' }
// err.name = 'FetchrError'
// err.output -> { message: "Not found", more: "meta data" }
// err.rawRequest -> { headers: {}, method: 'GET', url: '/api/someData' }
// err.reason -> BAD_HTTP_STATUS | BAD_JSON | TIMEOUT | ABORT | UNKNOWN
// err.statusCode -> 404
// err.timeout -> 3000
// err.url -> '/api/someData'
} ) ; วัตถุที่มีวิธี abort จะถูกส่งคืนเมื่อสร้างคำขอ FETCHR บนไคลเอนต์ สิ่งนี้มีประโยชน์หากคุณต้องการยกเลิกคำขอก่อนที่จะเสร็จสมบูรณ์
const req = fetcher
. read ( 'someData' )
. params ( { id : 42 } )
. catch ( ( err ) => {
// err.reason will be ABORT
} ) ;
req . abort ( ) ; xhrTimeout เป็นคุณสมบัติการกำหนดค่าที่เป็นตัวเลือกที่อนุญาตให้คุณตั้งค่าการหมดเวลา (ใน MS) สำหรับคำขอลูกค้าทั้งหมดเริ่มต้นเป็น 3000 บนลูกค้าไซด์ XHRPath และ XHRTIMEOUT จะถูกใช้สำหรับคำขอทั้งหมด บนเซิร์ฟเวอร์ไม่จำเป็นต้องใช้ xhrpath และ xhrtimeout และถูกละเว้น
import Fetcher from 'fetchr' ;
const fetcher = new Fetcher ( {
xhrPath : '/myCustomAPIEndpoint' ,
xhrTimeout : 4000 ,
} ) ; หากคุณต้องการตั้งค่าการหมดเวลาต่อคำขอคุณสามารถโทรหา clientConfig ด้วยคุณสมบัติ timeout :
fetcher
. read ( 'someData' )
. params ( { id : 42 } )
. clientConfig ( { timeout : 5000 } ) // wait 5 seconds for this request before timing out
. catch ( ( err ) => {
// err.reason will be TIMEOUT
} ) ; สำหรับบางแอปพลิเคชันอาจมีสถานการณ์ที่คุณต้องประมวลผลพารามิเตอร์บริการที่ส่งผ่านในคำขอก่อนที่จะส่งไปยังบริการจริง โดยทั่วไปคุณจะประมวลผลพวกเขาในบริการเอง อย่างไรก็ตามหากคุณต้องการดำเนินการประมวลผลผ่านบริการมากมาย (เช่นการฆ่าเชื้อเพื่อความปลอดภัย) คุณสามารถใช้ตัวเลือก paramsProcessor
paramsProcessor เป็นฟังก์ชั่นที่ส่งผ่านไปยังวิธีการ Fetcher.middleware มันผ่านข้อโต้แย้งสามข้อวัตถุคำขอวัตถุ ServiceInfo และวัตถุพารามิเตอร์บริการ ฟังก์ชั่น paramsProcessor สามารถแก้ไขพารามิเตอร์บริการได้หากจำเป็น
นี่คือตัวอย่าง:
/**
Using the app.js from above, you can modify the Fetcher.middleware
method to pass in the paramsProcessor function.
*/
app . use (
'/myCustomAPIEndpoint' ,
Fetcher . middleware ( {
paramsProcessor : function ( req , serviceInfo , params ) {
console . log ( serviceInfo . resource , serviceInfo . operation ) ;
return Object . assign ( { foo : 'fillDefaultValueForFoo' } , params ) ;
} ,
} ) ,
) ; สำหรับบางแอปพลิเคชันอาจมีสถานการณ์ที่คุณต้องแก้ไขการตอบสนองก่อนที่จะส่งผ่านไปยังลูกค้า โดยทั่วไปคุณจะใช้การดัดแปลงในบริการของคุณเอง อย่างไรก็ตามหากคุณต้องการแก้ไขการตอบสนองในบริการจำนวนมาก (เช่นเพิ่มข้อมูลการดีบัก) คุณสามารถใช้ตัวเลือก responseFormatter
responseFormatter เป็นฟังก์ชั่นที่ส่งผ่านไปยังวิธีการ Fetcher.middleware มันถูกส่งผ่านสามอาร์กิวเมนต์วัตถุคำขอวัตถุตอบสนองและวัตถุตอบกลับบริการ (เช่นข้อมูลที่ส่งคืนจากบริการของคุณ) ฟังก์ชั่น responseFormatter สามารถแก้ไขการตอบสนองของบริการเพื่อเพิ่มข้อมูลเพิ่มเติม
ดูตัวอย่างด้านล่าง:
/**
Using the app.js from above, you can modify the Fetcher.middleware
method to pass in the responseFormatter function.
*/
app . use (
'/myCustomAPIEndpoint' ,
Fetcher . middleware ( {
responseFormatter : function ( req , res , data ) {
data . debug = 'some debug information' ;
return data ;
} ,
} ) ,
) ; ตอนนี้เมื่อมีการร้องขอการตอบสนองของคุณจะมีคุณสมบัติ debug ที่เพิ่มไว้ด้านบน
FetchR ให้การสนับสนุน CORS โดยอนุญาตให้คุณผ่านโฮสต์ Origin Full Origin ไปยังตัวเลือก corsPath
ตัวอย่างเช่น:
import Fetcher from 'fetchr' ;
const fetcher = new Fetcher ( {
corsPath : 'http://www.foo.com' ,
xhrPath : '/fooProxy' ,
} ) ;
fetcher . read ( 'service' ) . params ( { foo : 1 } ) . clientConfig ( { cors : true } ) ; นอกจากนี้คุณยังสามารถปรับแต่งวิธีการสร้าง URL GET โดยผ่านคุณสมบัติ constructGetUri เมื่อคุณเรียกใช้การโทร read :
import qs from 'qs' ;
function customConstructGetUri ( uri , resource , params , config ) {
// this refers to the Fetcher object itself that this function is invoked with.
if ( config . cors ) {
return uri + '/' + resource + '?' + qs . stringify ( this . context ) ;
}
// Return `falsy` value will result in `fetcher` using its internal path construction instead.
}
import Fetcher from 'fetchr' ;
const fetcher = new Fetcher ( {
corsPath : 'http://www.foo.com' ,
xhrPath : '/fooProxy' ,
} ) ;
fetcher . read ( 'service' ) . params ( { foo : 1 } ) . clientConfig ( {
cors : true ,
constructGetUri : customConstructGetUri ,
} ) ; คุณสามารถปกป้องเส้นทางมิดเดิลแวร์ FetchR ของคุณจากการโจมตี CSRF โดยการเพิ่มมิดเดิลแวร์ไว้ข้างหน้า:
app.use('/myCustomAPIEndpoint', csrf(), Fetcher.middleware());
คุณสามารถใช้ https://github.com/expressjs/csurf สำหรับสิ่งนี้เป็นตัวอย่าง
ต่อไปคุณต้องตรวจสอบให้แน่ใจว่าโทเค็น CSRF กำลังถูกส่งไปพร้อมกับคำขอของเราเพื่อให้สามารถตรวจสอบได้ ในการทำเช่นนี้ให้ส่งโทเค็นเป็นคีย์ในตัว options.context อ็อบเจ็กต์ข้อความบนไคลเอนต์:
const fetcher = new Fetcher ( {
xhrPath : '/myCustomAPIEndpoint' , // xhrPath is REQUIRED on the clientside fetcher instantiation
context : {
// These context values are persisted with client calls as query params
_csrf : 'Ax89D94j' ,
} ,
} ) ; _csrf นี้จะถูกส่งในคำขอไคลเอนต์ทั้งหมดเป็นพารามิเตอร์แบบสอบถามเพื่อให้สามารถตรวจสอบได้บนเซิร์ฟเวอร์
เมื่อโทรหาบริการ Fetcher คุณสามารถผ่านวัตถุกำหนดค่าเพิ่มเติมได้
เมื่อการโทรนี้ทำจากไคลเอนต์วัตถุกำหนดค่าจะถูกใช้เพื่อตั้งค่าตัวเลือกคำขอบางอย่างและสามารถใช้เพื่อแทนที่ตัวเลือกเริ่มต้น:
//app.js - client
const config = {
timeout : 6000 , // Timeout (in ms) for each request
unsafeAllowRetry : false , // for POST requests, whether to allow retrying this post
} ;
fetcher . read ( 'service' ) . params ( { id : 1 } ) . clientConfig ( config ) ;สำหรับคำขอจากเซิร์ฟเวอร์วัตถุกำหนดค่าจะถูกส่งผ่านไปยังบริการที่ถูกเรียก
คุณสามารถตั้งค่า FETCHR เป็นคำขอที่ล้มเหลวโดยอัตโนมัติโดยการระบุการกำหนดค่า retry ใน Global หรือในการกำหนดค่าคำขอ:
// Globally
const fetchr = new Fetchr ( {
retry : { maxRetries : 2 } ,
} ) ;
// Per request
fetchr . read ( 'service' ) . clientConfig ( {
retry : { maxRetries : 1 } ,
} ) ; ด้วยการกำหนดค่าข้างต้น FETCHR จะลองอีกสองครั้งคำขอทั้งหมดที่ล้มเหลว แต่เพียงครั้งเดียวเมื่อโทร read('service')
คุณสามารถปรับแต่งวิธีการทำงานของกลไกการทำงานใหม่ได้อย่างไร นี่คือการตั้งค่าทั้งหมดและค่าเริ่มต้นของพวกเขา:
const fetchr = new Fetchr ( {
retry : {
maxRetries : 2 , // amount of retries after the first failed request
interval : 200 , // maximum interval between each request in ms (see note below)
statusCodes : [ 0 , 408 ] , // response status code that triggers a retry (see note below)
} ,
unsafeAllowRetry : false , // allow unsafe operations to be retried (see note below)
}ช่วงเวลา
ช่วงเวลาระหว่างการร้องขอแต่ละครั้งจะเคารพสูตรต่อไปนี้โดยอิงจาก backoff แบบเอ็กซ์โปเนนเชียลและกลยุทธ์การกระวนกระวายใจที่เผยแพร่ในโพสต์บล็อกสถาปัตยกรรม AWS นี้:
Math . random ( ) * Math . pow ( 2 , attempt ) * interval ; attempt คือจำนวนของความพยายามลองใหม่ในปัจจุบันเริ่มต้นจาก 0 ตาม interval เริ่มต้นสอดคล้องกับ 200ms
รหัสสถานะ
ด้วยเหตุผลทางประวัติศาสตร์ FETCHR จะลองตอบกลับ 408 ครั้งและไม่มีการตอบกลับเลย (ตัวอย่างเช่นข้อผิดพลาดเครือข่ายที่ระบุโดยรหัสสถานะ 0) อย่างไรก็ตามคุณอาจพบว่ามีประโยชน์ในการลองใหม่ในรหัสอื่น ๆ เช่นกัน (502, 503, 504 สามารถเป็นผู้สมัครที่ดีสำหรับการลองใหม่โดยอัตโนมัติ)
ความไม่ปลอดภัย
โดยค่าเริ่มต้น FetchR จะลอง read คำขอเท่านั้น สิ่งนี้ทำเพื่อเหตุผลด้านความปลอดภัย: การอ่านสองรายการจากฐานข้อมูลไม่เลวร้ายเท่ากับการสร้างรายการสองครั้ง แต่ถ้าแอปพลิเคชันหรือทรัพยากรของคุณไม่ต้องการการป้องกันประเภทนี้คุณสามารถอนุญาตให้ลองใหม่ได้โดยการตั้งค่า unsafeAllowRetry เป็น true และ FETCHR จะลองดำเนินการทั้งหมดอีกครั้ง
โดยค่าเริ่มต้น FETCHR จะผนวกค่าบริบททั้งหมดเข้ากับ URL คำขอเป็นพารามิเตอร์แบบสอบถาม contextPicker ช่วยให้คุณสามารถควบคุมตัวแปรบริบทที่ส่งเป็นพารามิเตอร์แบบสอบถามได้มากขึ้นขึ้นอยู่กับวิธีการร้องขอ ( GET หรือ POST ) สิ่งนี้มีประโยชน์เมื่อคุณต้องการ จำกัด จำนวนตัวแปรใน GET URL เพื่อไม่ให้แคชแคช
contextPicker เป็นไปตามรูปแบบเดียวกันกับพารามิเตอร์เพร predicate ใน lodash/pickBy ที่มีสองอาร์กิวเมนต์: (value, key)
const fetcher = new Fetcher ( {
context : {
// These context values are persisted with client calls as query params
_csrf : 'Ax89D94j' ,
device : 'desktop' ,
} ,
contextPicker : {
GET : function ( value , key ) {
// for example, if you don't enable CSRF protection for GET, you are able to ignore it with the url
if ( key === '_csrf' ) {
return false ;
}
return true ;
} ,
// for other method e.g., POST, if you don't define the picker, it will pick the entire context object
} ,
} ) ;
const fetcher = new Fetcher ( {
context : {
// These context values are persisted with client calls as query params
_csrf : 'Ax89D94j' ,
device : 'desktop' ,
} ,
contextPicker : {
GET : [ 'device' ] , // predicate can be an array of strings
} ,
} ) ; เมื่อโทรหาบริการ Fetcher คุณสามารถเพิ่มส่วนหัวคำขอที่กำหนดเอง
คำขอมีส่วนหัวที่กำหนดเองเมื่อคุณเพิ่มตัวเลือก headers ใน 'clientConfig'
const config = {
headers : {
'X-VERSION' : '1.0.0' ,
} ,
} ;
fetcher . read ( 'service' ) . params ( { id : 1 } ) . clientConfig ( config ) ; คำขอทั้งหมดมีส่วนหัวที่กำหนดเองเมื่อคุณเพิ่มตัวเลือก headers ในการสร้างอาร์กิวเมนต์ของ 'Fetcher'
import Fetcher from 'fetchr' ;
const fetcher = new Fetcher ( {
headers : {
'X-VERSION' : '1.0.0' ,
} ,
} ) ; ในการรวบรวมสถิติความสำเร็จ/ความล้มเหลว/ความล่าช้าของ Fetcher Service คุณสามารถกำหนดค่า statsCollector สำหรับ Fetchr ฟังก์ชั่น statsCollector จะถูกเรียกใช้ด้วยหนึ่ง Argumment: stats วัตถุ stats จะมีฟิลด์ต่อไปนี้:
create|read|update|delete import Fetcher from 'fetchr' ;
const fetcher = new Fetcher ( {
xhrPath : '/myCustomAPIEndpoint' ,
statsCollector : function ( stats ) {
// just console logging as a naive example. there is a lot more you can do here,
// like aggregating stats or filtering out stats you don't want to monitor
console . log (
'Request for resource' ,
stats . resource ,
'with' ,
stats . operation ,
'returned statusCode:' ,
stats . statusCode ,
' within' ,
stats . time ,
'ms' ,
) ;
} ,
} ) ; app . use (
'/myCustomAPIEndpoint' ,
Fetcher . middleware ( {
statsCollector : function ( stats ) {
// just console logging as a naive example. there is a lot more you can do here,
// like aggregating stats or filtering out stats you don't want to monitor
console . log (
'Request for resource' ,
stats . resource ,
'with' ,
stats . operation ,
'returned statusCode:' ,
stats . statusCode ,
' within' ,
stats . time ,
'ms' ,
) ;
} ,
} ) ,
) ; ซอฟต์แวร์นี้ใช้งานได้ฟรีภายใต้ Yahoo! ใบอนุญาต BSD Inc. ดูไฟล์ใบอนุญาตสำหรับข้อความใบอนุญาตและข้อมูลลิขสิทธิ์