Chèn mã vào ngữ cảnh trang bằng cách sử dụng tập lệnh nội dung


480

Tôi đang tìm hiểu cách tạo tiện ích mở rộng Chrome. Tôi mới bắt đầu phát triển một cái để bắt các sự kiện trên YouTube. Tôi muốn sử dụng nó với trình phát flash YouTube (sau này tôi sẽ cố gắng làm cho nó tương thích với HTML5).

manifest.json:

{
    "name": "MyExtension",
    "version": "1.0",
    "description": "Gotta catch Youtube events!",
    "permissions": ["tabs", "http://*/*"],
    "content_scripts" : [{
        "matches" : [ "www.youtube.com/*"],
        "js" : ["myScript.js"]
    }]
}

myScript.js:

function state() { console.log("State Changed!"); }
var player = document.getElementById("movie_player");
player.addEventListener("onStateChange", "state");
console.log("Started!");

Vấn đề là giao diện điều khiển cho tôi "Bắt đầu!" , nhưng không có "Nhà nước thay đổi!" khi tôi phát / tạm dừng video YouTube.

Khi mã này được đặt trong bàn điều khiển, nó đã hoạt động. Tôi đang làm gì sai?


14
hãy thử xóa các trích dẫn xung quanh tên hàm của bạn:player.addEventListener("onStateChange", state);
Eduardo

2
Điều đáng chú ý là khi viết các trận đấu, đừng quên bao gồm https://hoặc http://, điều này www.youtube.com/*sẽ không cho phép bạn đóng gói tiện ích mở rộng và sẽ gây ra lỗi phân tách lược đồ bị thiếu
Nilay Vishwakarma

Câu trả lời:


874

Các kịch bản nội dung được thực thi trong môi trường "thế giới biệt lập" . Bạn phải tiêm state()phương pháp của bạn vào chính trang đó.

Khi bạn muốn sử dụng một trong các chrome.*API trong tập lệnh, bạn phải triển khai một trình xử lý sự kiện đặc biệt, như được mô tả trong câu trả lời này: Tiện ích mở rộng của Chrome - lấy thông điệp gốc của Gmail .

Mặt khác, nếu bạn không phải sử dụng chrome.*API, tôi thực sự khuyên bạn nên tiêm tất cả mã JS của mình vào trang thông qua việc thêm <script>thẻ:

Mục lục

  • Phương pháp 1: Tiêm một tệp khác
  • Phương pháp 2: Tiêm mã nhúng
  • Phương pháp 2b: Sử dụng hàm
  • Phương pháp 3: Sử dụng một sự kiện nội tuyến
  • Giá trị động trong mã được tiêm

Phương pháp 1: Tiêm một tệp khác

Đây là phương pháp dễ nhất / tốt nhất khi bạn có nhiều mã. Bao gồm mã JS thực tế của bạn trong một tệp trong phần mở rộng của bạn, giả sử script.js. Sau đó, hãy để tập lệnh nội dung của bạn như sau (giải thích tại đây: Ứng dụng Google Chome Lối tắt Short Javascript tùy chỉnh ):

var s = document.createElement('script');
// TODO: add "script.js" to web_accessible_resources in manifest.json
s.src = chrome.runtime.getURL('script.js');
s.onload = function() {
    this.remove();
};
(document.head || document.documentElement).appendChild(s);

Lưu ý: Nếu bạn sử dụng phương pháp này, script.jstệp được chèn phải được thêm vào "web_accessible_resources"phần ( ví dụ ). Nếu bạn không làm như vậy, Chrome sẽ từ chối tải tập lệnh của bạn và hiển thị lỗi sau trong bảng điều khiển:

Từ chối tải phần mở rộng chrome: // [EXTENSIONID] /script.js. Tài nguyên phải được liệt kê trong khóa tệp kê khai web_accessible_resource để được tải bởi các trang bên ngoài tiện ích mở rộng.

Phương pháp 2: Tiêm mã nhúng

Phương pháp này hữu ích khi bạn muốn chạy nhanh một đoạn mã nhỏ. (Xem thêm: Làm cách nào để tắt các phím nóng facebook với tiện ích mở rộng Chrome? ).

var actualCode = `// Code here.
// If you want to use a variable, use $ and curly braces.
// For example, to use a fixed random number:
var someFixedRandomValue = ${ Math.random() };
// NOTE: Do not insert unsafe variables in this way, see below
// at "Dynamic values in the injected code"
`;

var script = document.createElement('script');
script.textContent = actualCode;
(document.head||document.documentElement).appendChild(script);
script.remove();

Lưu ý: nghĩa đen mẫu chỉ được hỗ trợ trong Chrome 41 trở lên. Nếu bạn muốn tiện ích mở rộng hoạt động trong Chrome 40-, hãy sử dụng:

var actualCode = ['/* Code here. Example: */' + 'alert(0);',
                  '// Beware! This array have to be joined',
                  '// using a newline. Otherwise, missing semicolons',
                  '// or single-line comments (//) will mess up your',
                  '// code ----->'].join('\n');

Phương pháp 2b: Sử dụng hàm

Đối với một đoạn mã lớn, trích dẫn chuỗi là không khả thi. Thay vì sử dụng một mảng, một hàm có thể được sử dụng và xâu chuỗi:

var actualCode = '(' + function() {
    // All code is executed in a local scope.
    // For example, the following does NOT overwrite the global `alert` method
    var alert = null;
    // To overwrite a global variable, prefix `window`:
    window.alert = null;
} + ')();';
var script = document.createElement('script');
script.textContent = actualCode;
(document.head||document.documentElement).appendChild(script);
script.remove();

Phương thức này hoạt động, bởi vì +toán tử trên chuỗi và hàm chuyển đổi tất cả các đối tượng thành chuỗi. Nếu bạn có ý định sử dụng mã nhiều lần, bạn nên tạo một hàm để tránh lặp lại mã. Một triển khai có thể trông giống như:

function injectScript(func) {
    var actualCode = '(' + func + ')();'
    ...
}
injectScript(function() {
   alert("Injected script");
});

Lưu ý: Vì chức năng được tuần tự hóa, phạm vi ban đầu và tất cả các thuộc tính ràng buộc sẽ bị mất!

var scriptToInject = function() {
    console.log(typeof scriptToInject);
};
injectScript(scriptToInject);
// Console output:  "undefined"

Phương pháp 3: Sử dụng một sự kiện nội tuyến

Đôi khi, bạn muốn chạy một số mã ngay lập tức, ví dụ để chạy một số mã trước khi <head>phần tử được tạo. Điều này có thể được thực hiện bằng cách chèn một <script>thẻ với textContent(xem phương pháp 2 / 2b).

Một cách khác, nhưng không được khuyến nghị là sử dụng các sự kiện nội tuyến. Không khuyến khích vì nếu trang xác định chính sách Bảo mật Nội dung cấm các tập lệnh nội tuyến, thì trình lắng nghe sự kiện nội tuyến sẽ bị chặn. Các tập lệnh nội tuyến được chèn bởi phần mở rộng, mặt khác, vẫn chạy. Nếu bạn vẫn muốn sử dụng các sự kiện nội tuyến, đây là cách:

var actualCode = '// Some code example \n' + 
                 'console.log(document.documentElement.outerHTML);';

document.documentElement.setAttribute('onreset', actualCode);
document.documentElement.dispatchEvent(new CustomEvent('reset'));
document.documentElement.removeAttribute('onreset');

Lưu ý: Phương pháp này giả định rằng không có người nghe sự kiện toàn cầu nào khác xử lý resetsự kiện này. Nếu có, bạn cũng có thể chọn một trong những sự kiện toàn cầu khác. Chỉ cần mở bảng điều khiển JavaScript (F12), nhập document.documentElement.onvà chọn các sự kiện có sẵn.

Giá trị động trong mã được tiêm

Đôi khi, bạn cần truyền một biến tùy ý cho hàm được chèn. Ví dụ:

var GREETING = "Hi, I'm ";
var NAME = "Rob";
var scriptToInject = function() {
    alert(GREETING + NAME);
};

Để tiêm mã này, bạn cần truyền các biến làm đối số cho hàm ẩn danh. Hãy chắc chắn để thực hiện nó một cách chính xác! Những điều sau đây sẽ không hoạt động:

var scriptToInject = function (GREETING, NAME) { ... };
var actualCode = '(' + scriptToInject + ')(' + GREETING + ',' + NAME + ')';
// The previous will work for numbers and booleans, but not strings.
// To see why, have a look at the resulting string:
var actualCode = "(function(GREETING, NAME) {...})(Hi, I'm ,Rob)";
//                                                 ^^^^^^^^ ^^^ No string literals!

Giải pháp là sử dụng JSON.stringifytrước khi truyền đối số. Thí dụ:

var actualCode = '(' + function(greeting, name) { ...
} + ')(' + JSON.stringify(GREETING) + ',' + JSON.stringify(NAME) + ')';

Nếu bạn có nhiều biến, đáng để sử dụng JSON.stringifymột lần, để cải thiện khả năng đọc, như sau:

...
} + ')(' + JSON.stringify([arg1, arg2, arg3, arg4]) + ')';

81
Câu trả lời này nên là một phần của tài liệu chính thức. Tài liệu chính thức nên giao hàng với cách được đề xuất -> 3 cách để làm điều tương tự ... Sai?
Mars Robertson

7
@ Qantas94Heavy CSP của tiện ích mở rộng không ảnh hưởng đến tập lệnh nội dung. Chỉ CSP của trang là có liên quan. Phương pháp 1 có thể bị chặn bằng cách sử dụng một lệnh script-srcloại trừ nguồn gốc của tiện ích mở rộng, phương pháp 2 có thể bị chặn bằng cách sử dụng CSP loại trừ "không an toàn nội tuyến" `.
Rob W

3
Có người hỏi tại sao tôi xóa thẻ script bằng cách sử dụng script.parentNode.removeChild(script);. Lý do của tôi để làm điều đó là vì tôi thích dọn dẹp mớ hỗn độn của mình. Khi một tập lệnh nội tuyến được chèn vào tài liệu, nó sẽ được thực thi ngay lập tức và <script>thẻ có thể được gỡ bỏ một cách an toàn.
Rob W

9
Phương pháp khác: sử dụng location.href = "javascript: alert('yeah')";bất cứ nơi nào trong tập lệnh nội dung của bạn. Nó dễ dàng hơn cho các đoạn mã ngắn và cũng có thể truy cập các đối tượng JS của trang.
Métoule

3
@ChrisP Cẩn thận với việc sử dụng javascript:. Mã trải dài trên nhiều dòng có thể không hoạt động như mong đợi. Một dòng nhận xét ( //) sẽ cắt bớt phần còn lại, vì vậy điều này sẽ thất bại : location.href = 'javascript:// Do something <newline> alert(0);';. Điều này có thể được tránh bằng cách đảm bảo rằng bạn sử dụng các bình luận nhiều dòng. Một điều cần cẩn thận là kết quả của biểu thức sẽ bị hủy. javascript:window.x = 'some variable';sẽ khiến tài liệu không tải và được thay thế bằng cụm từ 'một số biến'. Nếu được sử dụng đúng cách, nó thực sự là một sự thay thế hấp dẫn <script>.
Cướp W

61

Điều duy nhất còn thiếu ẩn khỏi câu trả lời tuyệt vời của Rob W là cách giao tiếp giữa tập lệnh trang được chèn và tập lệnh nội dung.

Ở phía bên nhận (tập lệnh nội dung của bạn hoặc tập lệnh trang được chèn) thêm trình lắng nghe sự kiện:

document.addEventListener('yourCustomEvent', function (e) {
  var data = e.detail;
  console.log('received', data);
});

Về phía người khởi tạo (tập lệnh nội dung hoặc tập lệnh trang được chèn) gửi sự kiện:

var data = {
  allowedTypes: 'those supported by structured cloning, see the list below',
  inShort: 'no DOM elements or classes/functions',
};

document.dispatchEvent(new CustomEvent('yourCustomEvent', { detail: data }));

Ghi chú:

  • Tin nhắn DOM sử dụng thuật toán nhân bản có cấu trúc, chỉ có thể truyền một số loại dữ liệu ngoài các giá trị nguyên thủy. Nó không thể gửi các thể hiện lớp hoặc hàm hoặc các thành phần DOM.
  • Trong Firefox, để gửi một đối tượng (không phải là giá trị nguyên thủy) từ tập lệnh nội dung đến ngữ cảnh trang, bạn phải sao chép nó một cách rõ ràng vào mục tiêu bằng cách sử dụng cloneInto(một hàm tích hợp), nếu không, nó sẽ thất bại với lỗi vi phạm bảo mật .

    document.dispatchEvent(new CustomEvent('yourCustomEvent', {
      detail: cloneInto(data, document.defaultView),
    }));

Tôi thực sự đã liên kết với mã và giải thích ở dòng thứ hai trong câu trả lời của mình, với stackoverflow.com/questions/9602022/ .
Rob W

1
Bạn có tham chiếu cho phương pháp được cập nhật của mình không (ví dụ: báo cáo lỗi hoặc trường hợp kiểm tra?) Trình CustomEventxây dựng thay thế document.createEventAPI không dùng nữa .
Rob W

Đối với tôi 'ClarkEvent (CustomEvent mới ...' đã hoạt động. Tôi có Chrome 33. Ngoài ra, nó không hoạt động trước đây vì tôi đã viết addEventListener sau khi tiêm mã js.
jscripter

Cẩn thận hơn về những gì bạn truyền vào như là tham số thứ 2 của bạn cho hàm CustomEventtạo. Tôi đã trải qua 2 thất bại rất khó hiểu: 1. chỉ cần đặt các trích dẫn đơn lẻ xung quanh 'chi tiết' một cách ngẫu nhiên đã tạo ra giá trị nullkhi nhận được bởi người nghe Content Script của tôi. 2. Quan trọng hơn, vì một số lý do tôi đã phải JSON.parse(JSON.stringify(myData))hoặc nếu không nó cũng sẽ trở thành null. Với điều này, tôi thấy rằng tuyên bố của nhà phát triển Chromium sau đây - rằng thuật toán "bản sao có cấu trúc" được sử dụng tự động - không đúng. bug.chromium.org/p/chromium/issues/detail?id=260378#c18
jdunk

Tôi nghĩ cách chính thức là sử dụng window.postMessage: developer.chrome.com/extensions/ trên
Enrique

9

Tôi cũng đã phải đối mặt với vấn đề đặt hàng các tập lệnh được tải, đã được giải quyết thông qua việc tải tập lệnh liên tiếp. Tải được dựa trên câu trả lời Rob W của .

function scriptFromFile(file) {
    var script = document.createElement("script");
    script.src = chrome.extension.getURL(file);
    return script;
}

function scriptFromSource(source) {
    var script = document.createElement("script");
    script.textContent = source;
    return script;
}

function inject(scripts) {
    if (scripts.length === 0)
        return;
    var otherScripts = scripts.slice(1);
    var script = scripts[0];
    var onload = function() {
        script.parentNode.removeChild(script);
        inject(otherScripts);
    };
    if (script.src != "") {
        script.onload = onload;
        document.head.appendChild(script);
    } else {
        document.head.appendChild(script);
        onload();
    }
}

Ví dụ về việc sử dụng sẽ là:

var formulaImageUrl = chrome.extension.getURL("formula.png");
var codeImageUrl = chrome.extension.getURL("code.png");

inject([
    scriptFromSource("var formulaImageUrl = '" + formulaImageUrl + "';"),
    scriptFromSource("var codeImageUrl = '" + codeImageUrl + "';"),
    scriptFromFile("EqEditor/eq_editor-lite-17.js"),
    scriptFromFile("EqEditor/eq_config.js"),
    scriptFromFile("highlight/highlight.pack.js"),
    scriptFromFile("injected.js")
]);

Trên thực tế, tôi là người mới đối với JS, vì vậy hãy thoải mái ping tôi đến những cách tốt hơn.


3
Cách chèn tập lệnh này không hay, vì bạn đang làm ô nhiễm không gian tên của trang web. Nếu trang web sử dụng một biến được gọi là formulaImageUrlhoặc codeImageUrl, thì bạn đang hủy hiệu quả chức năng của trang. Nếu bạn muốn chuyển một biến vào trang web, tôi khuyên bạn nên đính kèm dữ liệu vào phần tử tập lệnh ( e.g. script.dataset.formulaImageUrl = formulaImageUrl;) và sử dụng ví dụ (function() { var dataset = document.currentScript.dataset; alert(dataset.formulaImageUrl;) })();trong tập lệnh để truy cập dữ liệu.
Cướp W

@RobW cảm ơn bạn đã lưu ý, mặc dù đó là về mẫu. Bạn có thể vui lòng làm rõ, tại sao tôi nên sử dụng IIFE thay vì chỉ nhận dataset?
Dmitry Ginzburg

4
document.currentScriptchỉ trỏ đến thẻ script trong khi nó đang thực thi. Nếu bạn muốn truy cập thẻ script và / hoặc thuộc tính / thuộc tính của nó (ví dụ dataset), thì bạn cần lưu trữ nó trong một biến. Chúng ta cần một IIFE để có được một bao đóng để lưu trữ biến này mà không gây ô nhiễm không gian tên toàn cầu.
Cướp W

@RobW xuất sắc! Nhưng chúng ta không thể sử dụng một số tên biến, hầu như không giao nhau với tên hiện có. Có phải nó không phải là thành ngữ hay chúng ta có thể có một số vấn đề khác với nó?
Dmitry Ginzburg

2
Bạn có thể, nhưng chi phí sử dụng IIFE là không đáng kể, vì vậy tôi không thấy lý do nào để thích ô nhiễm không gian tên hơn IIFE. Tôi đánh giá chắc chắn rằng tôi sẽ không phá vỡ trang web của người khác theo một cách nào đó và khả năng sử dụng tên biến ngắn. Một ưu điểm khác của việc sử dụng IIFE là bạn có thể thoát tập lệnh sớm hơn nếu muốn ( return;).
Cướp

6

trong tập lệnh Nội dung, tôi thêm thẻ tập lệnh vào phần đầu liên kết trình xử lý 'onmessage', bên trong trình xử lý tôi sử dụng, eval để thực thi mã. Trong kịch bản nội dung gian hàng tôi cũng sử dụng trình xử lý onmessage, vì vậy tôi có được giao tiếp hai chiều. Tài liệu Chrome

//Content Script

var pmsgUrl = chrome.extension.getURL('pmListener.js');
$("head").first().append("<script src='"+pmsgUrl+"' type='text/javascript'></script>");


//Listening to messages from DOM
window.addEventListener("message", function(event) {
  console.log('CS :: message in from DOM', event);
  if(event.data.hasOwnProperty('cmdClient')) {
    var obj = JSON.parse(event.data.cmdClient);
    DoSomthingInContentScript(obj);
 }
});

pmListener.js là một trình nghe url tin nhắn bài viết

//pmListener.js

//Listen to messages from Content Script and Execute Them
window.addEventListener("message", function (msg) {
  console.log("im in REAL DOM");
  if (msg.data.cmnd) {
    eval(msg.data.cmnd);
  }
});

console.log("injected To Real Dom");

Bằng cách này, tôi có thể có giao tiếp 2 chiều giữa CS với Real Dom. Ví dụ, nó rất hữu ích nếu bạn cần nghe các sự kiện webscoket hoặc bất kỳ biến trong bộ nhớ hoặc sự kiện nào.


1

Bạn có thể sử dụng chức năng tiện ích mà tôi đã tạo cho mục đích chạy mã trong ngữ cảnh trang và nhận lại giá trị được trả về.

Điều này được thực hiện bằng cách tuần tự hóa một chức năng thành một chuỗi và đưa nó vào trang web.

Tiện ích có sẵn ở đây trên GitHub .

Ví dụ sử dụng -



// Some code that exists only in the page context -
window.someProperty = 'property';
function someFunction(name = 'test') {
    return new Promise(res => setTimeout(()=>res('resolved ' + name), 1200));
}
/////////////////

// Content script examples -

await runInPageContext(() => someProperty); // returns 'property'

await runInPageContext(() => someFunction()); // returns 'resolved test'

await runInPageContext(async (name) => someFunction(name), 'with name' ); // 'resolved with name'

await runInPageContext(async (...args) => someFunction(...args), 'with spread operator and rest parameters' ); // returns 'resolved with spread operator and rest parameters'

await runInPageContext({
    func: (name) => someFunction(name),
    args: ['with params object'],
    doc: document,
    timeout: 10000
} ); // returns 'resolved with params object'


0

Nếu bạn muốn tiêm hàm thuần túy, thay vì văn bản, bạn có thể sử dụng phương pháp này:

function inject(){
    document.body.style.backgroundColor = 'blue';
}

// this includes the function as text and the barentheses make it run itself.
var actualCode = "("+inject+")()"; 

document.documentElement.setAttribute('onreset', actualCode);
document.documentElement.dispatchEvent(new CustomEvent('reset'));
document.documentElement.removeAttribute('onreset');

Và bạn có thể truyền tham số (không may là không có đối tượng và mảng nào có thể được xâu chuỗi) cho các hàm. Thêm nó vào baretheses, như vậy:

function inject(color){
    document.body.style.backgroundColor = color;
}

// this includes the function as text and the barentheses make it run itself.
var color = 'yellow';
var actualCode = "("+inject+")("+color+")"; 


Điều này khá thú vị ... nhưng phiên bản thứ hai, với một biến về màu sắc, không hoạt động đối với tôi ... Tôi nhận được 'không nhận ra' và mã ném lỗi ... không xem đó là một biến.
11teenth
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.