컨텐츠 스크립트를 사용하여 페이지 컨텍스트에 코드 삽입


480

Chrome 확장 프로그램을 만드는 방법을 배우고 있습니다. 방금 YouTube 이벤트를 잡기 위해 하나를 개발하기 시작했습니다. YouTube 플래시 플레이어와 함께 사용하고 싶습니다 (나중에 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!");

문제는 콘솔이 "시작되었습니다!"라는 메시지를 표시 한다는 것 입니다. 하지만 '상태가 변경되었습니다!' 는 없습니다 . YouTube 동영상을 재생 / 일시 중지 할 때

이 코드를 콘솔에 넣으면 작동했습니다. 내가 무엇을 잘못하고 있지?


14
함수 이름 주위의 따옴표를 제거하십시오.player.addEventListener("onStateChange", state);
Eduardo

2
이 경기를 작성할 때 포함하는 것을 잊지 않는 것이 또한 주목할 만하다 https://이나 http://, 이것은 www.youtube.com/*당신이 확장 팩시키지 않을 것임을 및 던질 것이다 누락 계획 구분 오류
Nilay하기 Vishwakarma

답변:


874

컨텐츠 스크립트는 "격리 된 세계"환경 에서 실행됩니다 . state()메소드를 페이지 자체 에 주입해야 합니다.

chrome.*스크립트 에서 API 중 하나를 사용하려면 다음 답변에 설명 된대로 특수한 이벤트 핸들러를 구현해야합니다. Chrome 확장 프로그램-Gmail의 원래 메시지 검색 .

그렇지 않으면 chrome.*API 를 사용할 필요가 없으면 <script>태그 를 추가하여 페이지에 모든 JS 코드를 삽입하는 것이 좋습니다 .

목차

  • 방법 1 : 다른 파일 삽입
  • 방법 2 : 임베디드 코드 삽입
  • 방법 2b : 함수 사용
  • 방법 3 : 인라인 이벤트 사용
  • 삽입 된 코드의 동적 값

방법 1 : 다른 파일 삽입

이것은 코드가 많을 때 가장 쉽고 가장 좋은 방법입니다. 확장명 내의 파일에 실제 JS 코드를 포함하십시오 (예 :) script.js. 그런 다음 콘텐츠 스크립트를 다음과 같이하십시오 ( Google Chome“애플리케이션 바로 가기”사용자 정의 Javascript 여기에 설명 )

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);

참고 :이 방법을 사용하는 경우 주입 된 script.js파일을 "web_accessible_resources"섹션에 추가해야합니다 ( ). 그렇지 않으면 Chrome에서 스크립트로드 를 거부 하고 콘솔에 다음 오류가 표시됩니다.

chrome-extension : // [EXTENSIONID] /script.js의로드 거부 확장 프로그램 외부의 페이지에서로드하려면 리소스가 web_accessible_resources 매니페스트 키에 나열되어 있어야합니다.

방법 2 : 임베디드 코드 삽입

이 방법은 작은 코드를 빠르게 실행하려는 경우에 유용합니다. (또한 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();

참고 : 템플릿 리터럴 은 Chrome 41 이상에서만 지원됩니다. 확장 프로그램이 Chrome 40에서 작동하게하려면 다음을 사용하십시오.

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');

방법 2b : 함수 사용

큰 코드 덩어리의 경우 문자열을 인용하는 것이 가능하지 않습니다. 배열을 사용하는 대신 함수를 사용하고 문자열화할 수 있습니다.

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();

+문자열과 함수 의 연산자는 모든 객체를 문자열로 변환 하기 때문에이 방법이 작동 합니다. 코드를 두 번 이상 사용하려는 경우 코드 반복을 피하는 함수를 작성하는 것이 좋습니다. 구현은 다음과 같습니다.

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

참고 : 함수가 직렬화되므로 원래 범위와 모든 바인딩 된 속성이 손실됩니다!

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

방법 3 : 인라인 이벤트 사용

경우에 따라 <head>요소를 만들기 전에 일부 코드를 실행하는 등 일부 코드를 즉시 실행하려는 경우가 있습니다 . <script>태그를 삽입하여 수행 할 수 있습니다 textContent(방법 2 / 2b 참조).

대안 이지만 권장되지않지만 인라인 이벤트를 사용하는 것입니다. 페이지가 인라인 스크립트를 금지하는 컨텐츠 보안 정책을 정의하면 인라인 이벤트 리스너가 차단되므로 권장하지 않습니다. 반면, 확장에 의해 주입 된 인라인 스크립트는 여전히 실행됩니다. 인라인 이벤트를 계속 사용하려면 다음과 같이하십시오.

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');

참고 :이 메소드는 이벤트를 처리하는 다른 글로벌 이벤트 리스너가 없다고 가정합니다 reset. 있는 경우 다른 글로벌 이벤트 중 하나를 선택할 수도 있습니다. JavaScript 콘솔 (F12)을 열고을 입력 document.documentElement.on한 후 사용 가능한 이벤트를 선택하십시오.

삽입 된 코드의 동적 값

간혹 주입 된 함수에 임의의 변수를 전달해야합니다. 예를 들면 다음과 같습니다.

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

이 코드를 삽입하려면 변수를 인수로 익명 함수에 전달해야합니다. 올바르게 구현하십시오! 다음은 작동 하지 않습니다 .

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!

해결책은 JSON.stringify인수를 전달하기 전에 사용 하는 것입니다. 예:

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

변수가 많은 경우 JSON.stringify다음과 같이 가독성을 높이기 위해 한 번만 사용하는 것이 좋습니다 .

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

81
이 답변은 공식 문서의 일부 여야합니다. 공식 문서는 권장 된 방법으로 제공해야합니다.
화성 로버트슨

7
@ Qantas94Heavy 확장의 CSP는 컨텐츠 스크립트에 영향을 미치지 않습니다 . 페이지의 CSP 만 관련이 있습니다. 방법 1은 script-src확장의 원점을 배제 하는 지시문을 사용하여 차단 될 수 있고 , 방법 2는 "안전하지 않은 인라인"을 제외하는 CSP를 사용하여 차단 될 수 있습니다.
Rob W

3
누군가을 사용하여 스크립트 태그를 제거하는 이유를 물었습니다 script.parentNode.removeChild(script);. 내가하는 이유는 내가 엉망을 정리하고 싶어하기 때문입니다. 인라인 스크립트가 문서에 삽입되면 즉시 실행되며 <script>태그를 안전하게 제거 할 수 있습니다.
Rob W

9
다른 방법 : location.href = "javascript: alert('yeah')";컨텐츠 스크립트의 어느 곳에서나 사용 하십시오. 짧은 코드 스 니펫이 더 쉽고 페이지의 JS 객체에 액세스 할 수도 있습니다.
Métoule

3
@ChrisP주의해서 사용하십시오 javascript:. 여러 줄에 걸친 코드는 예상대로 작동하지 않을 수 있습니다. 줄 주석 ( //)은 나머지를 자르므로 실패 location.href = 'javascript:// Do something <newline> alert(0);';합니다.. 여러 줄 주석을 사용하면이를 피할 수 있습니다. 주의해야 할 또 다른 사항은 표현식의 결과가 공백이어야한다는 것입니다. javascript:window.x = 'some variable';문서가 언로드되고 '일부 변수'문구로 대체됩니다. 올바르게 사용하면 실제로 매력적인 대안 <script>입니다.
Rob W

61

유일한 것 잃어버린 Rob W의 탁월한 답변에 숨겨진 것은 삽입 된 페이지 스크립트와 컨텐츠 스크립트 사이의 통신 방법입니다.

수신 측 (콘텐츠 스크립트 또는 삽입 된 페이지 스크립트)에서 이벤트 리스너를 추가하십시오.

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

이니시에이터 측 (콘텐츠 스크립트 또는 삽입 된 페이지 스크립트)에서 이벤트를 보내십시오.

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 }));

노트:

  • DOM 메시징은 구조적 복제 알고리즘을 사용하는데,이 알고리즘 은 기본 값 외에 일부 데이터 유형 만 전송할 수 있습니다 . 클래스 인스턴스, 함수 또는 DOM 요소를 보낼 수 없습니다.
  • Firefox에서 컨텐츠 스크립트의 객체 (예 : 기본 값이 아님)를 페이지 컨텍스트로 보내려면 cloneInto(내장 함수)를 사용하여 명시 적으로 대상에 오브젝트를 복제해야합니다 . 그렇지 않으면 보안 위반 오류로 실패합니다. .

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

실제로 답변의 두 번째 줄에있는 코드 및 설명에 stackoverflow.com/questions/9602022/…에 연결되었습니다 .
Rob W

1
업데이트 된 메소드 (예 : 버그 보고서 또는 테스트 사례)에 대한 참조가 있습니까? CustomEvent생성자가 더 이상 사용되지 않는 document.createEventAPI를 대체합니다 .
Rob W

나를 위해 'dispatchEvent (new CustomEvent ...'가 작동했습니다. Chrome 33이 있습니다. 또한 js 코드를 삽입 한 후 addEventListener를 작성했기 때문에 이전에는 작동하지 않았습니다.
jscripter

두 번째 매개 변수로 CustomEvent생성자에 전달하는 내용에 특히주의하십시오 . 나는 두 가지 매우 혼란스러운 좌절을 경험했다 : 1. 단순히 'detail'주위에 작은 따옴표를 두는 null것은 내 콘텐츠 스크립트의 청취자가받을 때 당황하게 가치를 만들었다 . 2. 더 중요한 것은, 어떤 이유로 든 내가해야했던 JSON.parse(JSON.stringify(myData))null입니다. 이를 감안할 때 다음과 같은 Chromium 개발자의 주장에 따르면 "구조적 클론"알고리즘이 자동으로 사용된다는 것은 사실이 아닙니다. bugs.chromium.org/p/chromium/issues/detail?id=260378#c18
jdunk

공식적인 방법은 window.postMessage : developer.chrome.com/extensions/…
Enrique

9

또한로드 된 스크립트의 순서 문제에 직면했습니다.로드 된 스크립트는 순차적으로로드 된 스크립트를 통해 해결되었습니다. 로딩은 Rob W의 답변을 기반으로 합니다.

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();
    }
}

사용 예는 다음과 같습니다.

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")
]);

사실, 나는 JS에 익숙하지 않으므로 더 나은 방법으로 나를 핑 (ping) 할 수 있습니다.


3
웹 페이지의 네임 스페이스를 오염시키기 때문에 스크립트를 삽입하는이 방법은 좋지 않습니다. 웹 페이지에서 formulaImageUrl또는 라는 변수를 사용하면 페이지 codeImageUrl의 기능을 효과적으로 파괴하는 것입니다. 변수를 웹 페이지에 전달하려면 데이터를 스크립트 요소 ( e.g. script.dataset.formulaImageUrl = formulaImageUrl;) 에 첨부하고 스크립트 (function() { var dataset = document.currentScript.dataset; alert(dataset.formulaImageUrl;) })();에서 예를 들어 데이터에 액세스하는 것이 좋습니다.
Rob W

@RobW 샘플에 대한 자세한 내용은 참고해 주셔서 감사합니다. 왜 내가 얻는 대신 IIFE를 사용해야하는지 명확히 해 주 dataset시겠습니까?
Dmitry Ginzburg

4
document.currentScript스크립트 태그가 실행되는 동안 만 가리 킵니다. 스크립트 태그 및 / 또는 해당 속성 / 속성 (예 :)에 액세스하려면 dataset변수에 저장해야합니다. 글로벌 네임 스페이스를 오염시키지 않고이 변수를 저장하기 위해 클로저를 얻으려면 IIFE가 필요합니다.
Rob W

@RobW 우수! 그러나 기존 변수와 거의 교차하지 않는 변수 이름 만 사용할 수는 없습니다. 그것은 단지 비 관념적입니까, 아니면 다른 문제가 있습니까?
Dmitry Ginzburg

2
IIFE를 사용하는 데 드는 비용은 무시할만한 수준이므로 IIFE보다 네임 스페이스 오염을 선호하는 이유는 없습니다. 나는 다른 방법으로 웹 페이지 깨뜨리지 않을 것이며 짧은 변수 이름을 사용할 수 있다고 확신합니다 . IIFE를 사용하면 얻을 수있는 또 다른 장점은 원하는 경우 스크립트를 더 일찍 종료 할 수 있다는 것입니다 ( return;).
Rob W

6

Content script에서는 'onmessage'핸들러를 바인딩하는 헤드에 스크립트 태그를 추가합니다. 부스 콘텐츠 스크립트에서 onmessage 핸들러도 사용하므로 양방향 통신이 가능합니다. 크롬 문서

//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는 게시물 메시지 URL 리스너입니다.

//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");

이렇게하면 CS와 Real Dom간에 양방향 통신이 가능합니다. 예를 들어 webscoket 이벤트를 수신해야하거나 메모리 변수 또는 이벤트를 수신해야하는 경우 매우 유용합니다.


1

페이지 컨텍스트에서 코드를 실행하고 반환 된 값을 가져 오기 위해 만든 유틸리티 함수를 사용할 수 있습니다.

이것은 함수를 문자열로 직렬화하고 웹 페이지에 주입하여 수행됩니다.

이 유틸리티는 GitHub에서 사용할 수 있습니다 .

사용 예-



// 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

텍스트 대신 순수한 기능을 주입하려면 다음 방법을 사용할 수 있습니다.

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');

그리고 매개 변수를 전달할 수 있습니다 (불행히도 개체와 배열을 문자열로 지정할 수 없습니다). 다음과 같이 맨더에 추가하십시오.

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+")"; 


이것은 꽤 멋지지만 ... 색상 변수가있는 두 번째 버전은 나를 위해 작동하지 않습니다 ... 나는 '인식되지 않습니다'라는 오류가 발생하고 코드는 오류를 발생시킵니다 ... 변수로 보지 않습니다.
11teenth
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.