AMP

amp-script

描述

在 Web Worker 中运行自定义 JavaScript。

 

必需的脚本

<script async custom-element="amp-script" src="https://cdn.ampproject.org/v0/amp-script-0.1.js"></script>

用法

amp-script 组件允许您运行自定义 JavaScript。为了保持 AMP 的性能保证,您的代码在 Web Worker 中运行,并适用某些限制。

虚拟 DOM

您的 JavaScript 可以访问页面上包裹在 <amp-script> 组件内的区域。 amp-script 将组件的子项复制到虚拟 DOM。您的代码可以将该虚拟 DOM 作为 document.body 访问。

例如,这个 <amp-script> 组件定义了一个由单个 <p> 组成的 DOM。

<amp-script src="http://example.com/my-script.js" width="300" height="100">
  <p>A single line of text</p>
</amp-script>

如果您的代码将一个元素追加到 document.body

// my-script.js
const p = document.createElement('p');
p.textContent = 'A second line of text';
document.body.appendChild(p);

新元素将放置在该 <p> 之后。

<amp-script src="http://example.com/my-script.js" width="300" height="100">
  <p>A single line of text</p>
  <p>A second line of text</p>
</amp-script>

加载 JavaScript

amp-script 元素可以通过两种方式加载 JavaScript

  • 远程加载,来自 URL
  • 本地加载,来自页面上的 <script> 元素

来自远程 URL

使用 src 属性从 URL 加载 JavaScript

<amp-script layout="container" src="https://example.com/hello-world.js">
  <button>Hello amp-script!</button>
</amp-script>

来自本地元素

您也可以将 JavaScript 内联包含在 script 标记中。您必须

  • amp-scriptscript 属性设置为本地 script 元素的 id
  • amp-script 中包含 target="amp-script"
  • script 中包含 type="text/plain"。这样,浏览器就不会执行您的脚本,从而允许 amp-script 控制它。
<!-- To use inline JavaScript, you must add a script hash to the document head. -->
<head>
  <meta
    name="amp-script-src"
    content="sha384-YCFs8k-ouELcBTgzKzNAujZFxygwiqimSqKK7JqeKaGNflwDxaC3g2toj7s_kxWG"
  />
</head>

...

<amp-script width="200" height="100" script="hello-world">
  <button>Hello amp-script!</button>
</amp-script>

<!-- Add [target="amp-script"] to the <script> element. -->
<script id="hello-world" type="text/plain" target="amp-script">
  const btn = document.querySelector('button');
  btn.addEventListener('click', () => {
    document.body.textContent = 'Hello World!';
  });
</script>

出于安全原因,具有 script 或跨域 src 属性的 amp-script 元素需要在 <meta name="amp-script-src" content="..."> 标记中包含一个脚本哈希值。此外,同源 src 文件必须具有 Content-Typeapplication/javascripttext/javascript

如果您的页面包含多个 amp-script 元素,每个元素都需要一个脚本哈希值,请将每个元素作为空格分隔的列表包含在单个 <meta name="amp-script-src" content="..."> 标记中(有关包含多个脚本哈希的示例,请参见 examples/amp-script/example.amp.html)。

它是如何工作的?

amp-scriptWeb Worker 中运行您的自定义 JavaScript。通常,Web Worker 无权访问 DOM。但是 amp-script 使您的代码可以访问虚拟 DOM。当您的 JavaScript 修改此虚拟 DOM 时,amp-script 将更新真实的 DOM。

在底层,amp-script 使用 @ampproject/worker-dom。有关设计细节,请参见 “意图实现”问题

功能

支持的 API

通常支持 DOM 元素及其属性,但有一些限制。例如,您的代码不能向 DOM 添加新的 <script><style> 标记。

amp-script 重新创建了许多常用的 DOM API,并使它们可供您的代码使用。这个“hello world”示例使用了 getElementById()addEventListener()createElement()textContentappendChild()

const button = document.getElementById('textarea');

button.addEventListener('click', () => {
  const h1 = document.createElement('h1');
  h1.textContent = 'Hello World!';
  document.body.appendChild(h1);
});

支持的 DOM API 包括

  • 元素获取器,如 getElementByName()getElementsByClassName()getElementsByTagName()childNodes()parentNode()lastChild()
  • 修改器,如 createTextNode()appendChild()insertBefore()removeChild()replaceChild()
  • 涉及事件的方法,如 addEventListener()removeEventListener()createEvent()
  • 属性和特性获取器,如 getAttribute()hasAttribute()
  • 事件属性,如 Event.targetEvent.typeEvent.bubbles
  • 元素属性,如 attributesidouterHTMLtextContentvalueclassListclassName
  • 还有更多。

有关支持的 DOM API 的完整列表,请参见 API 兼容性表

querySelector() 支持简单的选择器 - 元素、id、类和属性。因此,document.querySelector('.class') 将起作用,但 document.querySelector('.class1 .class2') 将不起作用。查看代码 了解详情。

amp-script 支持常用的 Web API,如 FetchWebSocketslocalStoragesessionStorageCanvas。目前,尚未实现 History API,也没有实现 cookie。

amp-script 不支持整个 DOM API 或 Web API,因为这会使 amp-script 自己的 JavaScript 太大且速度太慢。如果您希望看到支持某个 API,请提交问题自己建议并贡献更改

有关显示 amp-script 使用的一组示例,请参见此处

框架和库

目前,像 jQuery 这样的库在没有修改的情况下无法与 amp-script 一起使用,因为它们使用不受支持的 DOM API。但是,@ampproject/worker-dom 的设计是为了支持流行的 JavaScript 框架使用的 API。amp-script 已经使用 ReactPreact 进行了测试。为了保持包的大小较小,我们建议使用 Preact。其他框架可能可以使用,但尚未经过彻底测试;如果您正在寻求支持,请提交问题或在此处进行贡献。

创建 AMP 元素

您可以使用 amp-script 向 DOM 添加 amp-imgamp-layout 组件。目前不支持其他 AMP 组件。如果您需要创建不同的 AMP 元素,请在 #25344 上投票并添加一个描述您的用例的评论。

引用 amp-state

amp-script 支持获取和设置 amp-state JSON。这使 amp-script 可以通过 amp-bind 绑定与页面上的其他 AMP 元素进行交互。

如果您在用户手势之后从 amp-script 调用 AMP.setState(),则绑定可能会导致 DOM 发生变异。否则,将设置状态,但不会发生绑定(类似于 amp-state 初始化)。有关更多信息,请参见关于用户手势的部分

AMP.setState() 的工作方式如 <amp-bind> 文档中所述,将对象文字合并到指定的状态中。此示例显示了它如何影响 DOM

<script id="myscript" type="text/plain" target="amp-script">
  const button = document.getElementsByTagName('button')[0];

  function changer() {
    AMP.setState({myText: "I have changed!"});
  }

  button.addEventListener('click', changer);
</script>

<amp-script layout="container" script="myscript">
  <p [text]="myText">Will I change?</p>
  <button>Change it!</button>
</amp-script>

AMP.getState() 是异步的,并返回一个 Promise。Promise 使用它传递的状态变量的字符串化值来解决。这些示例演示了它在不同类型下的用法

async function myFunction() {
  AMP.setState({'text': 'I am a string'});
  let text = await AMP.getState('text');

  AMP.setState({'number': 42});
  let number = Number(await AMP.getState('number'));

  AMP.setState({'obj': {'text': 'I am a string', 'number': 42}});
  let obj = JSON.parse(await AMP.getState('obj'));
}

这是另一个示例。amp-state 不支持其 src 属性中的 WebSocket URL,但是我们可以使用 amp-script 从 WebSocket 传递数据。

<amp-script width="1" height="1" script="webSocketDemo"> </amp-script>

<script type="text/plain" target="amp-script" id="webSocketDemo">
  const socket = new WebSocket('wss://websocket.example');
  socket.onmessage = event => {
    AMP.setState({socketData: event.data});
  };
</script>

要使用 AMP.setState(),您必须在文档头部中包含 amp-bind 扩展脚本。

请注意,如果其他内容更改了 <amp-script> 内的 DOM,则该更改不会传播到虚拟 DOM。同步过程是单向的。因此,最好避免使用如下代码

<amp-script layout="container" script="myscript">
  <p [text]="myText">Will I change?</p>
</amp-script>
<button on="tap:AMP.setState({myText: 'I changed'})">
  Change this and amp-script won't know
</button>

检索 <amp-list> 的数据

您可以导出一个函数以充当 <amp-list> 的数据源。导出的函数必须返回 JSON 或使用 JSON 解析的 Promise。

导出 API 在全局范围内可用,并具有以下签名

/**
 * @param {string} name the name to identify the function by.
 * @param {Function} function the function to export.
 */
function exportFunction(name, function) {}

限制

为了保持 AMP 的性能和布局稳定性保证,amp-script 施加了一些限制。

JavaScript 代码的大小

amp-script 对代码大小有标准

  • 每个内联脚本最多可以包含 10,000 个字节
  • 页面上的脚本总共可以包含最多 150,000 个字节
  • 在沙盒模式下运行的页面上的脚本总共可以包含最多 300,000 个字节

用户手势

在某些情况下,amp-script 不会应用由您的 JavaScript 代码触发的 DOM 更改,除非该代码是由用户手势(例如,按钮点击)触发的。这有助于防止内容布局偏移导致不良的用户体验。

对于尺寸不可更改的 amp-script 容器,规则的限制较少。当 layout 不是 container 且尺寸在 HTML 中指定时,情况就是如此。我们将此类容器称为“固定大小”。我们将尺寸可以更改的容器称为“可变大小”。

以下是一些固定大小容器的示例

<amp-script layout="fill" height="300" width="500" script="myscript"></amp-script>

<amp-script layout="fixed-height" height="300" script="myscript" ></amp-script>

以下是一些可变大小容器的示例

<amp-script layout="responsive" script="myscript"></amp-script>

<amp-script layout="fixed" height="300" script="myscript"></amp-script>

<amp-script layout="container" height="300" width="500" script="myscript"></amp-script>

允许的 DOM 更改如下

  • 固定大小容器中,你的代码可以随时进行任何更改。
  • 可变大小容器中,你的代码只能在用户手势之后进行更改。然后,它有 5 秒钟的时间进行更改。如果你的代码进行了一次或多次 fetch() 调用,则它可以继续进行更改,直到最后一次 fetch() 完成后的 5 秒钟为止。
固定大小容器可变大小容器
页面加载时更改随时允许不允许
用户事件后更改随时允许允许 5 秒钟 + fetch()

请参阅此处,了解有关 AMP 布局系统的更多信息。

计算脚本哈希值

由于在 amp-script 中运行的自定义 JS 不受常规 内容安全策略的约束,因此你需要添加脚本哈希值

  • 对于内联 JavaScript
  • 对于从跨域源加载的 JavaScript

将脚本哈希值包含在文档头部的 meta[name=amp-script-src] 元素中。你需要为每个 <amp-script> 组件使用的脚本提供一个哈希值。以下是生成哈希值的几种方法

  • 如果省略 <meta> 标记,AMP 将输出一个控制台错误,其中包含预期的哈希字符串。你可以复制此字符串来创建适当的 <meta> 标记。
  • AMP Optimizer node 模块 会生成此哈希值并自动插入 <meta> 标记。
  • 自己构建,使用以下步骤
  1. 计算脚本内容的 SHA384 哈希和。此总和应以十六进制表示。
  2. 对结果进行 base64url 编码。
  3. 在其前面加上 sha384-

以下是在 Node.js 中计算哈希值的方法

const crypto = require('crypto');
const hash = crypto.createHash('sha384');

function generateCSPHash(script) {
  const data = hash.update(script, 'utf8');
  return (
    'sha384-' +
    data
      .digest('base64')
      .replace(/=/g, '')
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
  );
}

@ampproject/toolbox-script-csp node 模块也会计算哈希值。

此示例展示了如何在 HTML 中使用脚本哈希值

<head>
  <!--
    A meta[name="amp-script-src"] element contains all script hashes for
    <amp-script> elements on the page, delimited by spaces.
  -->
  <meta
    name="amp-script-src"
    content="
      sha384-fake_hash_of_remote_js
      sha384-fake_hash_of_local_script
    "
  />
</head>
<body>
  <!--
    A "src" attribute with a cross-origin URL requires adding a script hash.

    If the hash of remote.js's contents is "fake_hash_of_remote_js",
    we'll add "sha384-fake_hash_of_remote_js" to the <meta> tag above.

-->
  <amp-script src="cross.origin/remote.js" layout="container"> </amp-script>

  <!--
    A "script" attribute also requires adding a script hash.

    If the hash of #myScript's text contents is "fake_hash_of_local_script",
    we'll add "sha384-fake_hash_of_local_script" to the <meta> tag above.
  -->
  <amp-script script="myScript" layout="container"> </amp-script>
  <script type="text/plain" target="amp-script" id="myScript">
    document.body.textContent += 'Hello world!';
  </script>
</body>

在开发期间,你可以通过将 data-ampdevmode 属性添加到 amp-script 元素或根 html 节点来禁用 JavaScript 大小和脚本哈希值要求。将此属性添加到根 html 节点将抑制页面上的所有验证错误。将其添加到 amp-script 元素将仅抑制有关大小和脚本哈希值的错误。

属性

src

用于执行远程脚本。

将在该 <amp-script> 的上下文中执行的 JS 文件的 URL。URL 的协议必须为 HTTPS。HTTP 响应的 Content-Type 必须为 application/javascripttext/javascript

script

用于执行本地脚本。

script[type=text/plain][target=amp-script] 元素的 id,该元素的文本内容包含将在该 <amp-script> 的上下文中执行的 JS。

sandbox

对可能由此 <amp-script> 更改的 DOM 应用额外的限制。与 iframe[sandbox] 属性类似,属性的值可以为空以应用所有限制,也可以是空格分隔的标记以解除特定限制

  • allow-forms:允许创建和修改 表单元素。AMP 需要特殊处理,以防止用户输入产生未经授权的状态更改请求。有关更多详细信息,请参阅 amp-form 的安全注意事项

max-age

需要 script 属性。此属性是可选的,但如果指定了 script,则必须用于签名交换。

max-age 属性以秒为单位指定从签名交换 (SXG) 发布时起,允许本地脚本提供的最大生存期。AMP Packager 使用此值来计算 SXG expires 时间。

应仔细选择 max-age 的值

  • 较长的 max-age 会增加 SXG 降级的潜在安全影响。

  • 较短的 max-age 可能会阻止包含在具有最短 SXG 生存期的 AMP 缓存中。例如,Google AMP 缓存至少需要4 天(345600 秒)。请注意,由于 SXG 规范设置的最大值,目前没有理由选择长于 7 天(604800 秒)的 max-age

如果你不发布签名交换,则 max-age 不起作用。

nodom

可选的 nodom 属性优化了 <amp-script> 以用作数据层而不是 UI 层。它删除了 <amp-script> 进行 DOM 修改的能力,从而有利于更小的捆绑包大小,因此性能更好。它还会自动隐藏 <amp-script>,因此你可以省略高度和宽度属性。

sandboxed

不要与 sandbox 属性混淆。

如果设置,这将表示 worker-dom 应激活沙盒模式。在此模式下,Worker 位于其自己的跨域 iframe 中,从而创建强大的安全边界。它还会强制执行 nodom 模式。由于强大的安全边界,沙盒脚本不需要提供脚本哈希值。

通用属性

此元素包括扩展到 AMP 组件的常用属性

错误和警告

使用 amp-script 时,可能会遇到一些运行时错误。

内联脚本为 (...) 字节,超过 10,000 的限制。

没有内联脚本可以超过 10,000 字节。请参阅上面的JavaScript 代码大小

超出最大脚本总大小 (...)。

页面使用的所有非沙盒脚本的总和不能超过 150,000 字节。请参阅上面的JavaScript 代码大小

页面使用的所有沙盒脚本(请参阅沙盒模式)的总和不能超过 300,000 字节。请参阅上面的JavaScript 代码大小

未找到脚本哈希值。

对于本地脚本和跨域脚本,你需要添加一个脚本哈希值以确保安全。

(...) 必须在 meta[name="amp-script-src"] 中包含“sha384-(...)”

同样,你需要脚本哈希值。只需将此错误中的值复制到你的 <meta> 标记中即可。

在沙盒模式下禁用 JavaScript 脚本哈希值要求。

在开发模式下禁用 JavaScript 大小和脚本哈希值要求。

如果你的 <amp-script> 包括 data-ampdevmode 属性,AMP 将不会检查你的脚本哈希值或代码的大小。

阻止 (...) 尝试修改 (...)

为了避免不希望的内容布局偏移,amp-script 在某些条件下禁止 DOM 突变。请参阅上面的用户手势

由于非法突变,amp-script... 已终止

如果脚本尝试进行太多不允许的 DOM 更改,amp-script 可能会停止脚本,使其不会与 DOM 过于不同步。

由于最近缺少用户交互,AMP.setState 仅更新了页面状态,而没有重新评估绑定。

如果在用户交互之前修改可变大小容器中的状态变量,amp-script 将不会更新 DOM 以避免不希望的内容布局偏移。请参阅上面的引用 amp-state

除非你的 <amp-script> 包含属性 sandbox="allow-forms",否则无法更改表单元素 (...)。

此属性对于安全是必需的。请参阅上面的常用属性

已清理的节点:(...)

如果你的代码添加了一个不允许的元素(例如 <script><style> 或不受支持的 AMP 组件),amp-script 将会将其删除。

需要更多帮助?

你已阅读此文档十几次,但它并没有真正涵盖你所有的问题?也许其他人也有同样的感觉:在 Stack Overflow 上联系他们。

转到 Stack Overflow
发现错误或缺少功能?

AMP 项目强烈鼓励你的参与和贡献!我们希望你成为我们开源社区的长期参与者,但也欢迎对你特别感兴趣的问题做出一次性贡献。

转到 GitHub