ฉันจะใช้ jQuery ในสคริปต์ Greasemonkey ใน Google Chrome ได้อย่างไร


157

ตามที่คุณบางคนอาจรู้ว่า Google Chrome ได้วางข้อ จำกัด ที่ร้ายแรงบางอย่างในสคริปต์ Greasemonkey

โครเมี่ยมไม่สนับสนุน@require, @resource, unsafeWindow, GM_registerMenuCommand, หรือGM_setValueGM_getValue

ฉันไม่สามารถหาวิธีรวมไลบรารี jQuery ในสคริปต์ Greasemonkey ภายใต้ Google Chrome ได้หากไม่จำเป็นต้องใช้

มีใครมีคำแนะนำในเรื่องนี้บ้างไหม?


19
เป็นที่น่าสังเกตว่า Google Chrome ที่มี Tampermonkey ได้รับการสนับสนุน@requireแล้วในตอนนี้ซึ่งเป็นวิธีที่ง่ายกว่าวิธีที่ตอบไว้
Steen Schütt

2
Tampermonkey ยังรองรับ unsafeWindow ซึ่งดีมากสำหรับหน้าเว็บที่มี jQuery อยู่แล้ว var $ = unsafeWindow.jQuery;
ทิมกู๊ดแมน

1
@requireใช้งานได้ดีบนไซต์ที่คุณไม่ต้องกังวลเกี่ยวกับการขัดแย้งกับหนึ่งในพันหรือล้านไลบรารี JS อื่น ๆ ที่ผูกกับ $ เมื่อโหลด อย่างไรก็ตามหากคุณกำลังเขียนสคริปต์สำหรับเว็บไซต์ที่ใช้ $ สำหรับอย่างอื่นหรือแย่กว่านั้นคือการเขียนสคริปต์เพื่อให้ทำงานบนทุก ๆ ไซต์ให้ใช้กลไกการโหลดที่ปลอดภัยเปรียบเทียบซึ่งอธิบายโดยtghwด้านล่าง
GDorn

คำตอบ:


192

จาก"เคล็ดลับสคริปต์ผู้ใช้: การใช้ jQuery - บล็อกของ Erik Vold"

// ==UserScript==
// @name         jQuery For Chrome (A Cross Browser Example)
// @namespace    jQueryForChromeExample
// @include      *
// @author       Erik Vergobbi Vold & Tyler G. Hicks-Wright
// @description  This userscript is meant to be an example on how to use jQuery in a userscript on Google Chrome.
// ==/UserScript==

// a function that loads jQuery and calls a callback function when jQuery has finished loading
function addJQuery(callback) {
  var script = document.createElement("script");
  script.setAttribute("src", "//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js");
  script.addEventListener('load', function() {
    var script = document.createElement("script");
    script.textContent = "window.jQ=jQuery.noConflict(true);(" + callback.toString() + ")();";
    document.body.appendChild(script);
  }, false);
  document.body.appendChild(script);
}

// the guts of this userscript
function main() {
  // Note, jQ replaces $ to avoid conflicts.
  alert("There are " + jQ('a').length + " links on this page.");
}

// load jQuery and execute the main function
addJQuery(main);

แทนที่จะเป็น 3 บรรทัดใน addEventListener สำหรับ 'load' จะไม่ "callback ();" เพียงแค่ทำงาน?
crdx

6
หน้าของฉันมี jQuery อยู่แล้ว แต่ดูเหมือนว่ายังจำเป็นต้องใช้รหัสด้านบนเพื่อใช้ jQuery ใน userscript อย่างไรก็ตามสอง jQuery รวมถึงอาจทำให้เกิดความขัดแย้งดังนั้นบรรทัดแรกของ main () ฟังก์ชั่นของคุณอาจต้อง jQuery.noConflict ();
slolife

2
ผมได้มีการปรับเปลี่ยนสายscript.textContent = "(" + callback.toString() + ")();";ไปscript.textContent = "jQuery.noConflict();(" + callback.toString() + ")();";ในแม่แบบสคริปต์ผู้ใช้ของตัวเองดังนั้นจะไม่มีความขัดแย้งที่น่าแปลกใจใด ๆ :)
RCE

1
@hippietrail ในmain()คุณสามารถใช้$.loadScript()แล้วให้ทุกอย่างทำงานเมื่อโหลดสคริปต์เสร็จสิ้นการโหลด jQueryUI
tghw

1
-1: ด้วยวิธีการนั้นรหัสในmainจะทำงานในบริบทของหน้าเป้าหมายซึ่งหมายความว่า (เหนือสิ่งอื่นใด) นโยบายการร้องขอข้ามไซต์ของหน้าเป้าหมายนั้นใช้ - (เช่นฉันต้อง reinject GM_xmlhttpRequestก่อนที่จะเห็นว่ามันทำ) ไม่ช่วยฉัน) jquery.min.jsในที่สุดฉันเพียงแค่คัดลอกวางโค้ดของ
Jean Hominal

43

ฉันได้เขียนฟังก์ชั่นบางอย่างตามสคริปต์ของ Erik Voldเพื่อช่วยให้ฉันเรียกใช้ฟังก์ชันรหัสและสคริปต์อื่น ๆ ในเอกสาร คุณสามารถใช้มันเพื่อโหลด jQuery ลงในหน้าแล้วเรียกใช้รหัสภายใต้windowขอบเขตทั่วโลก

ตัวอย่างการใช้งาน

// ==UserScript==
// @name           Example from http://stackoverflow.com/q/6834930
// @version        1.3
// @namespace      http://stackoverflow.com/q/6834930
// @description    An example, adding a border to a post on Stack Overflow.
// @include        http://stackoverflow.com/questions/2246901/*
// ==/UserScript==

var load,execute,loadAndExecute;load=function(a,b,c){var d;d=document.createElement("script"),d.setAttribute("src",a),b!=null&&d.addEventListener("load",b),c!=null&&d.addEventListener("error",c),document.body.appendChild(d);return d},execute=function(a){var b,c;typeof a=="function"?b="("+a+")();":b=a,c=document.createElement("script"),c.textContent=b,document.body.appendChild(c);return c},loadAndExecute=function(a,b){return load(a,function(){return execute(b)})};

loadAndExecute("//ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js", function() {
    $("#answer-6834930").css("border", ".5em solid black");
});

คุณสามารถคลิกที่นี่เพื่อติดตั้งหากคุณมั่นใจว่าฉันไม่ได้พยายามหลอกให้คุณติดตั้งสิ่งที่เป็นอันตรายและไม่มีใครแก้ไขโพสต์ของฉันเพื่อชี้ไปยังสิ่งอื่น โหลดหน้าซ้ำและคุณควรเห็นเส้นขอบรอบโพสต์ของฉัน

ฟังก์ชั่น

load(url, onLoad, onError)

โหลดสคริปต์urlลงในเอกสาร เลือกเรียกกลับอาจมีการจัดและonLoadonError

execute(functionOrCode)

แทรกฟังก์ชั่นหรือสตริงของรหัสลงในเอกสารและดำเนินการ ฟังก์ชั่นจะถูกแปลงเป็นซอร์สโค้ดก่อนที่จะแทรกดังนั้นจึงสูญเสียขอบเขต / การปิดปัจจุบันและทำงานภายใต้windowขอบเขตส่วนกลาง

loadAndExecute(url, functionOrCode)

ทางลัด; สิ่งนี้จะโหลดสคริปต์จากurlนั้นแทรกและดำเนินการfunctionOrCodeถ้าสำเร็จ

รหัส

function load(url, onLoad, onError) {
    e = document.createElement("script");
    e.setAttribute("src", url);

    if (onLoad != null) { e.addEventListener("load", onLoad); }
    if (onError != null) { e.addEventListener("error", onError); }

    document.body.appendChild(e);

    return e;
}

function execute(functionOrCode) {
    if (typeof functionOrCode === "function") {
        code = "(" + functionOrCode + ")();";
    } else {
        code = functionOrCode;
    }

    e = document.createElement("script");
    e.textContent = code;

    document.body.appendChild(e);

    return e;
}

function loadAndExecute(url, functionOrCode) {
    load(url, function() { execute(functionOrCode); });
}

@cyphunk ใช่มันเป็นสิ่งสำคัญสำหรับฉันที่จะบันทึกตัวละครเหล่านั้น ที่จริงแล้วฉันรู้สึกโง่ที่ออกจากโพสต์นี้ไป ฉันจะลบมัน
Jeremy Banks

18

ใช้ jQuery โดยไม่ต้องกลัวของความขัดแย้งjQuery.noConflict(true)โดยการเรียก ชอบมาก

function GM_main ($) {
    alert ('jQuery is installed with no conflicts! The version is: ' + $.fn.jquery);
}

add_jQuery (GM_main, "1.7.2");

function add_jQuery (callbackFn, jqVersion) {
    jqVersion       = jqVersion || "1.7.2";
    var D           = document;
    var targ        = D.getElementsByTagName ('head')[0] || D.body || D.documentElement;
    var scriptNode  = D.createElement ('script');
    scriptNode.src  = 'http://ajax.googleapis.com/ajax/libs/jquery/'
                    + jqVersion
                    + '/jquery.min.js'
                    ;
    scriptNode.addEventListener ("load", function () {
        var scriptNode          = D.createElement ("script");
        scriptNode.textContent  =
            'var gm_jQuery  = jQuery.noConflict (true);\n'
            + '(' + callbackFn.toString () + ')(gm_jQuery);'
        ;
        targ.appendChild (scriptNode);
    }, false);
    targ.appendChild (scriptNode);
}


แต่สำหรับสคริปต์ข้ามเบราว์เซอร์ทำไมไม่ใช้ประโยชน์จากสำเนา jQuery ที่ดีรวดเร็วและของท้องถิ่นเมื่อคุณทำได้

ข้อมูลต่อไปนี้ทำงานเป็นผู้ใช้ Chrome และสคริปต์ Greasemonkey และใช้@requireสำเนา jQuery ในเครื่องที่ดีหากแพลตฟอร์มรองรับ

// ==UserScript==
// @name     _Smart, cross-browser jquery-using script
// @include  http://YOUR_SERVER.COM/YOUR_PATH/*
// @require  http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js
// @grant    GM_info
// ==/UserScript==

function GM_main ($) {
    alert ('jQuery is installed with no conflicts! The version is: ' + $.fn.jquery);
}

if (typeof jQuery === "function") {
    console.log ("Running with local copy of jQuery!");
    GM_main (jQuery);
}
else {
    console.log ("fetching jQuery from some 3rd-party server.");
    add_jQuery (GM_main, "1.7.2");
}

function add_jQuery (callbackFn, jqVersion) {
    var jqVersion   = jqVersion || "1.7.2";
    var D           = document;
    var targ        = D.getElementsByTagName ('head')[0] || D.body || D.documentElement;
    var scriptNode  = D.createElement ('script');
    scriptNode.src  = 'http://ajax.googleapis.com/ajax/libs/jquery/'
                    + jqVersion
                    + '/jquery.min.js'
                    ;
    scriptNode.addEventListener ("load", function () {
        var scriptNode          = D.createElement ("script");
        scriptNode.textContent  =
            'var gm_jQuery  = jQuery.noConflict (true);\n'
            + '(' + callbackFn.toString () + ')(gm_jQuery);'
        ;
        targ.appendChild (scriptNode);
    }, false);
    targ.appendChild (scriptNode);
}

3
+1 ที่น่าประทับใจจากคำตอบเหล่านี้ทั้งหมดของคุณเป็นคำตอบเดียวที่บอกว่าใช้ @require คุณยังสามารถใช้$ = unsafeWindow.jQueryหากหน้านั้นมี jQuery (ฉันทดสอบเฉพาะใน Tampermonkey)
AMK

Caveat: IE บางเวอร์ชันไม่มี console.log () ยกเว้นว่าคุณได้ติดตั้งเครื่องมือ dev แล้วดังนั้นสคริปต์จะหยุดทำงาน โดยปกติแล้วคุณจะค้นพบสิ่งนี้เพียงครั้งเดียวที่คุณปล่อยสคริปต์ของคุณนอกเหนือจากนักพัฒนาซอฟต์แวร์และ QA โฟล์ค
แยกวิเคราะห์

1
@ แยกวิเคราะห์เฟส IE ก็สวยมาก NA ที่นี่ ครั้งสุดท้ายที่ฉันตรวจสอบ IE ยังคงไม่สนับสนุน userscripts ดี (¿เลย?) มีการเปลี่ยนแปลงกับ IE 10 หรือไม่?
เบอร์ทรัมอดัมส์

บร็อค - จุดที่ดีฉันไม่ได้ถือเมทริกซ์ความเข้ากันไม่ได้ของ IE ในหัวของฉัน ดังนั้นไม่สามารถใช้ได้โดยตรงกับ userscripts (แม้ว่าจะปรากฏว่าบางคนพยายามที่จะใช้โซลูชัน IE) แต่เป็น gotcha ทั่วไป
แยกวิเคราะห์เฟส

15

หากหน้านั้นมี jQuery อยู่แล้วเพียงทำตามเทมเพลตนี้:

// ==UserScript==
// @name          My Script
// @namespace     my-script
// @description   Blah
// @version       1.0
// @include       http://site.com/*
// @author        Me
// ==/UserScript==

var main = function () {

    // use $ or jQuery here, however the page is using it

};

// Inject our main script
var script = document.createElement('script');
script.type = "text/javascript";
script.textContent = '(' + main.toString() + ')();';
document.body.appendChild(script);

ฉันไม่คิดว่ามันจะใช้งานได้เพราะ userscript ไม่มีสิทธิ์เข้าถึงหน้าต่างเอกสาร?
Christoph

@Christoph มันใช้งานได้ฉันมีและฉันยังคงใช้userscriptโดยใช้วิธีการนี้
Mottie

1
นี่คือการฉีดสคริปต์ greasemonkey ลงในหน้า ดังนั้นอาจผ่านการป้องกันบางอย่างที่ greasemonkey มี
Thymine

1
@Thymine ฉันได้สังเกตเห็นว่าวิธีการนี้จะฉีด userscript ไปยังหน้าที่ไม่พึงประสงค์ ผมเคยมีการตัดส่วนที่ฉีดในคำสั่งที่ตรวจสอบif window.location
Mottie

12

วิธีง่ายๆคือการใช้requiredคำหลัก:

// @require     http://code.jquery.com/jquery-latest.js

มันได้รับการสนับสนุนโดยการใช้งานส่วนขยายเท่านั้น
user2284570

@ user2284570 รองรับทุกส่วนขยาย userscript ที่ฉันสามารถหาได้จากเบราว์เซอร์ใดก็ได้
แมตต์เอ็ม

@MattM ฉันหมายความว่าคุณไม่จำเป็นต้องติดตั้งส่วนขยายบน Opera และ Chrome เพื่อเรียกใช้ userscripts
user2284570

7

มีวิธีที่ง่ายมากในการหลีกเลี่ยงรวมถึงสำเนา jQuery สำหรับสคริปต์ Chrome ทั้งหมดเมื่อสคริปต์เหล่านั้นไม่ได้ใช้คุณสมบัติพิเศษ (GM_ * ฟังก์ชั่น ฯลฯ ) ...

เพียงแค่แทรกสคริปต์ลงในหน้า DOM และดำเนินการ! ส่วนที่ดีที่สุดคือเทคนิคนี้ใช้งานได้ดีบน Firefox + Greasemonkey ดังนั้นคุณสามารถใช้สคริปต์เดียวกันสำหรับทั้งสอง:

var script = document.createElement("script");
script.type = "text/javascript";
script.textContent = "(" + threadComments.toString() + ")(jQuery)";
document.body.appendChild(script);

function threadComments($) {
    // taken from kip's http://userscripts-mirror.org/scripts/review/62163
    var goodletters = Array('\u00c0','\u00c1','\u00c2','\u00c3','\u00c4','\u00c5','\u00c6','\u00c7'
                             ,'\u00c8','\u00c9','\u00ca','\u00cb','\u00cc','\u00cd','\u00ce','\u00cf'
                                      ,'\u00d1','\u00d2','\u00d3','\u00d4','\u00d5','\u00d6'         
                             ,'\u00d8','\u00d9','\u00da','\u00db','\u00dc','\u00dd'                  
                             ,'\u00e0','\u00e1','\u00e2','\u00e3','\u00e4','\u00e5','\u00e6','\u00e7'
                             ,'\u00e8','\u00e9','\u00ea','\u00eb','\u00ec','\u00ed','\u00ee','\u00ef'
                                      ,'\u00f1','\u00f2','\u00f3','\u00f4','\u00f5','\u00f6'         
                             ,'\u00f8','\u00f9','\u00fa','\u00fb','\u00fc','\u00fd'         ,'\u00ff').join('');

    // from Benjamin Dumke's http://userscripts-mirror.org/scripts/review/68252
    function goodify(s)
      {
         good = new RegExp("^[" + goodletters + "\\w]{3}");
         bad = new RegExp("[^" + goodletters + "\\w]");
         original = s;
         while (s.length >3 && !s.match(good)) {
            s = s.replace(bad, "");
            }
         if (!s.match(good))
         {
           // failed, so we might as well use the original
           s = original;
         }
         return s;
      }  

    in_reply_to = {};


    function who(c, other_way) {


        if (other_way)
        {
            // this is closer to the real @-reply heuristics
            m = /@(\S+)/.exec(c);
        }
        else
        {
            m = /@([^ .:!?,()[\]{}]+)/.exec(c);
        }
        if (!m) {return}
        if (other_way) {return goodify(m[1]).toLowerCase().slice(0,3);}
        else {return m[1].toLowerCase().slice(0,3);}
    }

    function matcher(user, other_way) {
        if (other_way)
        {
            return function () {
                return goodify($(this).find(".comment-user").text()).toLowerCase().slice(0,3) == user
                }
        }
        else
        {
            return function () {
                return $(this).find(".comment-user").text().toLowerCase().slice(0,3) == user
                }
        }
    }

    function replyfilter(id) {
        return function() {
            return in_reply_to[$(this).attr("id")] == id;
        }
    }

    function find_reference() {
        comment_text = $(this).find(".comment-text").text();
        if (who(comment_text))
        {
            fil = matcher(who(comment_text));
            all = $(this).prevAll("tr.comment").filter(fil);
            if (all.length == 0)
            {
                // no name matched, let's try harder
                fil = matcher(who(comment_text, true), true);
                all = $(this).prevAll("tr.comment").filter(fil);
                if (all.length == 0) {return}
            }
            reference_id = all.eq(0).attr("id");
            in_reply_to[$(this).attr("id")] = reference_id;
        }
    }


    // How far may comments be indented?
    // Note that MAX_NESTING = 3 means there are
    // up to *four* levels (including top-level)
    MAX_NESTING = 3

    // How many pixels of indentation per level?
    INDENT = 30

    function indenter(parent) {

        for (var i = MAX_NESTING; i > 0; i--)
        {
            if (parent.hasClass("threading-" + (i-1)) || (i == MAX_NESTING && parent.hasClass("threading-" + i)))
            {
                return function() {
                    $(this).addClass("threading-" + i).find(".comment-text").css({"padding-left": INDENT*i});
                }
            }
        }

        return function() {
            $(this).addClass("threading-1").find(".comment-text").css({"padding-left": INDENT});
        }

    }

    function do_threading(){
        id = $(this).attr("id");
        replies = $(this).nextAll("tr.comment").filter(replyfilter(id));
        ind = indenter($(this));
        replies.each(ind);
        replies.insertAfter(this);
    }

    function go() {
        $("tr.comment").each(find_reference);
        $("tr.comment").each(do_threading);
    }

    $.ajaxSetup({complete: go});
    go();
}

(ถูกขโมยโดยไม่มีเจตนาจาก Shog9 บน meta.stackoverflow เนื่องจากเขาไม่ได้ย้ายที่นี่และฉันต้องลบเมตาโพสต์ .. )


4

นอกจากนี้คุณสามารถแพ็คสคริปต์ด้วย jQuery ไปยังส่วนขยายของ Chrome ดูคริปเนื้อหาของ Google Chrome

ส่วนขยายของ Chrome ซึ่งต่างจากสคริปต์ Greasemonkey สามารถอัปเดตตัวเองได้โดยอัตโนมัติ


2
ใช่มันจะง่ายขึ้น แต่ตอนนี้ฉันชอบที่จะรักษาสคริปต์ของฉันผ่าน userscripts.org และไม่สร้างความซ้ำซ้อนกับที่เก็บส่วนขยายของ Google
Alekc

4
และมีค่าใช้จ่าย $ 5 ในการอัปโหลดไปยัง Google Web Store
Camilo Martin

4

วิธีที่ง่ายกว่า: ตัด + วางเนื้อหาของ jquery.min.js ไว้ด้านบนของสคริปต์ผู้ใช้ของคุณ เสร็จสิ้น

ฉันพบปัญหาต่าง ๆ กับคำตอบที่แนะนำ โซลูชัน addJQuery () ทำงานบนหน้าส่วนใหญ่ แต่มีข้อบกพร่องหลายอย่าง หากคุณพบปัญหาให้คัดลอก + วางเนื้อหา jquery ลงในสคริปต์ของคุณ


ใช่ฉันคิดว่าสิ่งนี้เหมาะสมที่สุดเพราะแม้แต่คนเดียวก็สามารถสร้างสคริปต์สร้างเล็กน้อยที่สร้างรุ่นโครมโดยทำสิ่งที่คุณแนะนำที่นี่
dkinzer

2

ฉันสงสัยว่าคุณไม่สามารถเชื่อใจdocument.defaultView.jQueryในสคริปต์ GM ของคุณได้หรือไม่:

if (document.defaultView.jQuery) {
  jQueryLoaded(document.defaultView.jQuery);
} else {
  var jq = document.createElement('script');
  jq.src = 'http://jquery.com/src/jquery-latest.js';
  jq.type = 'text/javascript';
  document.getElementsByTagName('head')[0].appendChild(jq);
  (function() { 
    if (document.defaultView.jQuery) jQueryLoaded(document.defaultView.jQuery);
    else setTimeout(arguments.callee, 100);
  })();
}

function jQueryLoaded($) {
  console.dir($);
}

1

อีกวิธีหนึ่งคือการแก้ไขสคริปต์ของคุณเพื่อโหลด jQuery ด้วยตนเอง ตัวอย่างจากhttp://joanpiedra.com/jquery/greasemonkey/ :

// Add jQuery
var GM_JQ = document.createElement('script');
GM_JQ.src = 'http://jquery.com/src/jquery-latest.js';
GM_JQ.type = 'text/javascript';
document.getElementsByTagName('head')[0].appendChild(GM_JQ);

// Check if jQuery's loaded
function GM_wait() {
    if(typeof unsafeWindow.jQuery == 'undefined') { window.setTimeout(GM_wait,100); }
else { $ = unsafeWindow.jQuery; letsJQuery(); }
}
GM_wait();

// All your GM code must be inside this function
function letsJQuery() {
    alert($); // check if the dollar (jquery) function works
}

แก้ไข: DRATS! หลังจากการทดสอบจะปรากฏรหัสนี้ไม่ทำงานเนื่องจาก Google Chrome เรียกใช้ userscripts / extensions ในขอบเขต / กระบวนการแยกต่างหากจากหน้าเว็บจริง คุณสามารถดาวน์โหลดรหัส jQuery โดยใช้ XmlhttpRequest จากนั้น Eval แต่คุณต้องโฮสต์รหัสบนเซิร์ฟเวอร์ที่อนุญาตให้ใช้ทรัพยากรร่วมกันข้ามแหล่งกำเนิดโดยใช้Access-Control-Allow-Origin: *ส่วนหัว น่าเสียดายที่ไม่มี CDN ปัจจุบันที่มี jQuery รองรับสิ่งนี้


-1

ส่วนขยายที่สมบูรณ์แบบสำหรับการฝัง jQuery ลงใน Chrome Console ทำได้ง่ายอย่างที่คิด ส่วนขยายนี้ยังไม่ระบุหาก jQuery ถูกฝังไว้ในหน้าแล้ว

ส่วนขยายนี้ใช้เพื่อฝัง jQuery ในหน้าใด ๆ ที่คุณต้องการ อนุญาตให้ใช้ jQuery ในคอนโซลเชลล์ (คุณสามารถเรียกใช้คอนโซล Chrome ด้วย "Ctrl + Shift + j")

หากต้องการฝัง jQuery ลงในแท็บที่เลือกให้คลิกที่ปุ่มส่วนขยาย

LINK to extension: https://chrome.google.com/extensions/detail/gbmifchmngifmadobkcpijhhldeeelkc


นั่นไม่ใช่คำตอบจริงๆ และทำไมฉันต้องการโหลด jQuery หากฉันไม่ต้องการ
Vik
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.