现在可以读取和写入 NFC 标签。
什么是网络 NFC?
NFC 指的是近距离无线通信,这是一种在 13.56 MHz 运行的短程无线技术,可在不到 10 厘米的距离和高达 424 kbit/s 的传输速率之间实现设备之间的通信。
借助 Web NFC,网站可以在 NFC 标签靠近用户设备(通常为 5-10 厘米、2-4 英寸)时读取和写入 NFC 标签。当前范围仅限于 NFC 数据交换格式 (NDEF),这是一种轻量级二进制消息格式,适用于不同的标签格式。
![手机开启 NFC 标签以交换数据](https://developer.chrome.google.cn/static/docs/capabilities/nfc/image/phone-powering-an-nfc-ta-6042b0ba379e8.png?authuser=9&hl=sl)
建议的用例
Web NFC 仅限于 NDEF,因为读取和写入 NDEF 数据的安全属性更容易量化。不支持低级别的 I/O 操作(例如 ISO-DEP、NFC-A/B、NFC-F)、点对点通信模式和基于主机的卡模拟 (HCE)。
可能会使用网络 NFC 的网站示例包括:
- 当用户将设备靠近展览附近的 NFC 卡时,博物馆和美术馆可显示有关显示屏的更多信息。
- 产品目录管理网站可以通过读取或写入某个容器上的 NFC 标签来更新有关其内容的信息。
- 会议网站可以使用它在活动期间扫描 NFC 徽章,并确保它们处于锁定状态,以防止上面写入的信息进一步更改。
- 网站可以使用它来共享设备或服务预配场景所需的初始密钥,以及在操作模式下部署配置数据。
![手机扫描多个 NFC 标签](https://developer.chrome.google.cn/static/docs/capabilities/nfc/image/phone-scanning-several-nf-b3de890312af6.png?authuser=9&hl=sl)
当前状态
步骤 | 状态 |
---|---|
1. 创建铺垫消息 | 完成 |
2. 创建规范的初始草稿 | 完成 |
3. 收集反馈并不断改进设计 | 完成 |
4. 源试用 | 完成 |
5. 启动 | 完成 |
使用网络 NFC
功能检测
硬件功能检测与您通常熟悉的功能不同。如果存在 NDEFReader
,表示浏览器支持 Web NFC,但不表示所需的硬件是否存在。特别是,如果硬件缺失,某些调用返回的 promise 将拒绝。在描述 NDEFReader
时,我会提供详细信息。
if ('NDEFReader' in window) { /* Scan and write NFC tags */ }
术语
NFC 标签是一种被动 NFC 设备,也就是说,当有主动 NFC 设备(如手机)在附近时,即利用磁感应进行供电。NFC 标签有多种形式和形式,例如贴纸、信用卡、手臂式手腕等。
![透明 NFC 标签的照片](https://developer.chrome.google.cn/static/docs/capabilities/nfc/image/photo-a-transparent-nfc-c2377ef8acaaa.jpg?authuser=9&hl=sl)
NDEFReader
对象是 Web NFC 中的入口点,可提供用于准备在靠近 NDEF 标签时执行的读取和/或写入操作的功能。NDEFReader
中的 NDEF
代表 NFC 数据交换格式,一种由 NFC Forum 标准化的轻量级二进制消息格式。
NDEFReader
对象用于对来自 NFC 标签的传入 NDEF 消息执行操作,以及将 NDEF 消息写入范围内的 NFC 标签。
支持 NDEF 的 NFC 标签就像便利贴。任何人都可以读取该文件;除非该文件处于只读状态,否则任何人都可以对其进行写入。它包含一条封装了一条或多条 NDEF 记录的 NDEF 消息。每条 NDEF 记录都是一个二进制结构,包含数据载荷和关联的类型信息。网络 NFC 支持以下 NFC Forum 标准化记录类型:空、文本、网址、智能海报、MIME 类型、绝对网址、外部类型、未知和本地类型。
![NDEF 消息示意图](https://developer.chrome.google.cn/static/docs/capabilities/nfc/image/diagram-an-ndef-message-5fdf1496614dd.png?authuser=9&hl=sl)
扫描 NFC 标签
如需扫描 NFC 标签,请先实例化一个新的 NDEFReader
对象。调用 scan()
会返回 promise。如果先前未授予访问权限,系统可能会提示用户。如果满足以下条件,promise 将解析:
- 它只是为了响应用户手势(如触摸手势或点击鼠标)而调用。
- 用户已允许网站与 NFC 设备互动。
- 用户的手机支持 NFC。
- 用户已在手机上启用 NFC。
promise 解析后,可通过事件监听器订阅 reading
事件,获取传入的 NDEF 消息。您还应订阅 readingerror
事件,以便在附近有不兼容的 NFC 标签时收到通知。
const ndef = new NDEFReader();
ndef.scan().then(() => {
console.log("Scan started successfully.");
ndef.onreadingerror = () => {
console.log("Cannot read data from the NFC tag. Try another one?");
};
ndef.onreading = event => {
console.log("NDEF message read.");
};
}).catch(error => {
console.log(`Error! Scan failed to start: ${error}.`);
});
当该 NFC 标签在附近时,会触发 NDEFReadingEvent
事件。它包含它独有的两个属性:
serialNumber
表示设备的序列号(例如 00-11-22-33-44-55-66);如果没有可用的序列号,则返回空字符串。message
表示 NFC 标签中存储的 NDEF 消息。
如需读取 NDEF 消息的内容,请循环遍历 message.records
,并根据其 recordType
适当处理其 data
成员。data
成员作为 DataView
提供,因为它允许处理以 UTF-16 编码的数据情况。
ndef.onreading = event => {
const message = event.message;
for (const record of message.records) {
console.log("Record type: " + record.recordType);
console.log("MIME type: " + record.mediaType);
console.log("Record id: " + record.id);
switch (record.recordType) {
case "text":
// TODO: Read text record with record data, lang, and encoding.
break;
case "url":
// TODO: Read URL record with record data.
break;
default:
// TODO: Handle other records with record data.
}
}
};
写入 NFC 标签
如需写入 NFC 标签,请先实例化一个新的 NDEFReader
对象。调用 write()
会返回 promise。如果先前未授予访问权限,系统可能会提示用户。此时,NDEF 消息已“准备”完毕,如果以下条件全部满足,promise 便会解析:
- 它只是为了响应用户手势(如触摸手势或点击鼠标)而调用。
- 用户已允许网站与 NFC 设备互动。
- 用户的手机支持 NFC。
- 用户已在手机上启用 NFC。
- 用户已点按 NFC 标签,且 NDEF 消息已成功写入。
如需将文本写入 NFC 标签,请将字符串传递给 write()
方法。
const ndef = new NDEFReader();
ndef.write(
"Hello World"
).then(() => {
console.log("Message written.");
}).catch(error => {
console.log(`Write failed :-( try again: ${error}.`);
});
如需将网址记录写入 NFC 标签,请将表示 NDEF 消息的字典传递给 write()
。在以下示例中,NDEF 消息是具有 records
键的字典。其值是一个记录数组,在本示例中,该网址记录定义为一个对象,其中 recordType
键设置为 "url"
,data
键设置为网址字符串。
const ndef = new NDEFReader();
ndef.write({
records: [{ recordType: "url", data: "https://w3c.github.io/web-nfc/" }]
}).then(() => {
console.log("Message written.");
}).catch(error => {
console.log(`Write failed :-( try again: ${error}.`);
});
您还可以将多条记录写入一个 NFC 标签。
const ndef = new NDEFReader();
ndef.write({ records: [
{ recordType: "url", data: "https://w3c.github.io/web-nfc/" },
{ recordType: "url", data: "https://web.dev/nfc/" }
]}).then(() => {
console.log("Message written.");
}).catch(error => {
console.log(`Write failed :-( try again: ${error}.`);
});
如果 NFC 标签包含不应被覆盖的 NDEF 消息,请在传递给 write()
方法的选项中将 overwrite
属性设置为 false
。在这种情况下,如果 NDEF 消息已存储在 NFC 标签中,返回的 promise 将拒绝。
const ndef = new NDEFReader();
ndef.write("Writing data on an empty NFC tag is fun!", { overwrite: false })
.then(() => {
console.log("Message written.");
}).catch(error => {
console.log(`Write failed :-( try again: ${error}.`);
});
将 NFC 标签设为只读
为防止恶意用户覆盖 NFC 标签的内容,您可以将 NFC 标签设为永久只读。此操作为单向过程,无法撤销。将 NFC 标签设为只读后,便无法再对其进行写入。
如需将 NFC 标签设为只读,请先实例化一个新的 NDEFReader
对象。调用 makeReadOnly()
会返回 promise。如果先前未授予访问权限,系统可能会提示用户。如果所有条件均满足,promise 便会解析:
- 它只是为了响应用户手势(如触摸手势或点击鼠标)而调用。
- 用户已允许网站与 NFC 设备互动。
- 用户的手机支持 NFC。
- 用户已在手机上启用 NFC。
- 用户已点按 NFC 标签,且 NFC 标签已成功设为只读。
const ndef = new NDEFReader();
ndef.makeReadOnly()
.then(() => {
console.log("NFC tag has been made permanently read-only.");
}).catch(error => {
console.log(`Operation failed: ${error}`);
});
下面是在写入 NFC 标签后将其设为永久只读的方法。
const ndef = new NDEFReader();
try {
await ndef.write("Hello world");
console.log("Message written.");
await ndef.makeReadOnly();
console.log("NFC tag has been made permanently read-only after writing to it.");
} catch (error) {
console.log(`Operation failed: ${error}`);
}
由于 makeReadOnly()
在 Android 的 Chrome 100 或更高版本中可用,因此请通过以下各项检查此功能是否受支持:
if ("NDEFReader" in window && "makeReadOnly" in NDEFReader.prototype) {
// makeReadOnly() is supported.
}
安全与权限
Chrome 团队根据控制对强大网络平台功能的访问权限中定义的核心原则(包括用户控制、透明度和工效学设计)设计和实现了 Web NFC。
由于 NFC 扩展了恶意网站可能获取的信息域,因此限制 NFC 的可用性,以便最大限度地了解和控制 NFC 的使用。
![网站上网络 NFC 提示的屏幕截图](https://developer.chrome.google.cn/static/docs/capabilities/nfc/image/screenshot-a-web-nfc-pro-054c47951e735.png?authuser=9&hl=sl)
网络 NFC 仅适用于顶级框架和安全浏览上下文(仅限 HTTPS)。在处理用户手势(例如按钮点击)时,源站必须先请求 "nfc"
权限。如果先前未授予访问权限,则 NDEFReader
scan()
、write()
和 makeReadOnly()
方法会触发用户提示。
document.querySelector("#scanButton").onclick = async () => {
const ndef = new NDEFReader();
// Prompt user to allow website to interact with NFC devices.
await ndef.scan();
ndef.onreading = event => {
// TODO: Handle incoming NDEF messages.
};
};
用户发起的权限提示与现实世界中通过目标 NFC 标签移动设备的物理移动相结合,可以反映其他文件和设备访问 API 中的选择器模式。
为了执行扫描或写入,当用户用其设备轻触 NFC 标签时,网页必须保持可见状态。浏览器使用触感反馈来指示点按操作。如果显示屏关闭或设备锁定,则无法访问 NFC 无线装置。对于不可见网页,接收和推送 NFC 内容会暂停,并在网页再次可见时恢复。
借助 Page Visibility API,可以跟踪文档公开范围的变化。
document.onvisibilitychange = event => {
if (document.hidden) {
// All NFC operations are automatically suspended when document is hidden.
} else {
// All NFC operations are resumed, if needed.
}
};
实战宝典
下面是一些可帮助您上手的代码示例。
检查权限
Permissions API 允许检查是否已授予 "nfc"
权限。此示例展示了如何在没有用户互动的情况下扫描 NFC 标签(如果之前已授予访问权限),否则显示一个按钮。请注意,该机制适用于写入 NFC 标签,因为它在后台使用相同的权限。
const ndef = new NDEFReader();
async function startScanning() {
await ndef.scan();
ndef.onreading = event => {
/* handle NDEF messages */
};
}
const nfcPermissionStatus = await navigator.permissions.query({ name: "nfc" });
if (nfcPermissionStatus.state === "granted") {
// NFC access was previously granted, so we can start NFC scanning now.
startScanning();
} else {
// Show a "scan" button.
document.querySelector("#scanButton").style.display = "block";
document.querySelector("#scanButton").onclick = event => {
// Prompt user to allow UA to send and receive info when they tap NFC devices.
startScanning();
};
}
取消 NFC 操作
使用 AbortController
基元可以轻松地取消 NFC 操作。以下示例展示了如何通过 NDEFReader scan()
、makeReadOnly()
和 write()
方法的选项传递 AbortController
的 signal
,并同时中止这两项 NFC 操作。
const abortController = new AbortController();
abortController.signal.onabort = event => {
// All NFC operations have been aborted.
};
const ndef = new NDEFReader();
await ndef.scan({ signal: abortController.signal });
await ndef.write("Hello world", { signal: abortController.signal });
await ndef.makeReadOnly({ signal: abortController.signal });
document.querySelector("#abortButton").onclick = event => {
abortController.abort();
};
写入后读取
结合使用 write()
和 scan()
与 AbortController
基元,可在向 NFC 标签写入消息后对其进行读取。以下示例展示了如何将短信写入 NFC 标签并读取 NFC 标签中的新消息。三秒后停止扫描。
// Waiting for user to tap NFC tag to write to it...
const ndef = new NDEFReader();
await ndef.write("Hello world");
// Success! Message has been written.
// Now scanning for 3 seconds...
const abortController = new AbortController();
await ndef.scan({ signal: abortController.signal });
const message = await new Promise((resolve) => {
ndef.onreading = (event) => resolve(event.message);
});
// Success! Message has been read.
await new Promise((r) => setTimeout(r, 3000));
abortController.abort();
// Scanning is now stopped.
读取和写入文本记录
文本记录 data
可通过使用记录 encoding
属性实例化的 TextDecoder
进行解码。请注意,文本记录的语言可通过其 lang
属性获取。
function readTextRecord(record) {
console.assert(record.recordType === "text");
const textDecoder = new TextDecoder(record.encoding);
console.log(`Text: ${textDecoder.decode(record.data)} (${record.lang})`);
}
如需写入简单的文本记录,请将字符串传递给 NDEFReader write()
方法。
const ndef = new NDEFReader();
await ndef.write("Hello World");
默认情况下,文本记录采用 UTF-8 编码,并采用当前文档的语言,但您可以使用用于创建自定义 NDEF 记录的完整语法来指定这两个属性(encoding
和 lang
)。
function a2utf16(string) {
let result = new Uint16Array(string.length);
for (let i = 0; i < string.length; i++) {
result[i] = string.codePointAt(i);
}
return result;
}
const textRecord = {
recordType: "text",
lang: "fr",
encoding: "utf-16",
data: a2utf16("Bonjour, François !")
};
const ndef = new NDEFReader();
await ndef.write({ records: [textRecord] });
读取和写入网址记录
使用 TextDecoder
解码记录的 data
。
function readUrlRecord(record) {
console.assert(record.recordType === "url");
const textDecoder = new TextDecoder();
console.log(`URL: ${textDecoder.decode(record.data)}`);
}
如需写入网址记录,请将 NDEF 消息字典传递给 NDEFReader write()
方法。NDEF 消息中包含的网址记录被定义为一个对象,将 recordType
键设置为 "url"
,将 data
键设置为网址字符串。
const urlRecord = {
recordType: "url",
data:"https://w3c.github.io/web-nfc/"
};
const ndef = new NDEFReader();
await ndef.write({ records: [urlRecord] });
读取和写入 MIME 类型记录
MIME 类型记录的 mediaType
属性表示 NDEF 记录载荷的 MIME 类型,以便正确解码 data
。例如,使用 JSON.parse
解码 JSON 文本,使用 Image 元素解码图片数据。
function readMimeRecord(record) {
console.assert(record.recordType === "mime");
if (record.mediaType === "application/json") {
const textDecoder = new TextDecoder();
console.log(`JSON: ${JSON.parse(decoder.decode(record.data))}`);
}
else if (record.mediaType.startsWith('image/')) {
const blob = new Blob([record.data], { type: record.mediaType });
const img = new Image();
img.src = URL.createObjectURL(blob);
document.body.appendChild(img);
}
else {
// TODO: Handle other MIME types.
}
}
如需写入 MIME 类型记录,请将 NDEF 消息字典传递给 NDEFReader write()
方法。NDEF 消息中包含的 MIME 类型记录被定义为一个对象,该对象的 recordType
键设置为 "mime"
,mediaType
键设置为内容的实际 MIME 类型,data
键设置为一个对象,该对象可以是 ArrayBuffer
,也可以是 ArrayBuffer
的视图(例如 Uint8Array
、DataView
)。
const encoder = new TextEncoder();
const data = {
firstname: "François",
lastname: "Beaufort"
};
const jsonRecord = {
recordType: "mime",
mediaType: "application/json",
data: encoder.encode(JSON.stringify(data))
};
const imageRecord = {
recordType: "mime",
mediaType: "image/png",
data: await (await fetch("icon1.png")).arrayBuffer()
};
const ndef = new NDEFReader();
await ndef.write({ records: [jsonRecord, imageRecord] });
读取和写入绝对网址记录
可以通过简单的 TextDecoder
对绝对网址记录 data
进行解码。
function readAbsoluteUrlRecord(record) {
console.assert(record.recordType === "absolute-url");
const textDecoder = new TextDecoder();
console.log(`Absolute URL: ${textDecoder.decode(record.data)}`);
}
如需写入绝对网址记录,请将 NDEF 消息字典传递给 NDEFReader write()
方法。NDEF 消息中包含的绝对网址记录定义为一个对象,其 recordType
键设置为 "absolute-url"
,data
键设置为网址字符串。
const absoluteUrlRecord = {
recordType: "absolute-url",
data:"https://w3c.github.io/web-nfc/"
};
const ndef = new NDEFReader();
await ndef.write({ records: [absoluteUrlRecord] });
读取和写入智能海报记录
智能海报记录(用于杂志广告、传单、广告牌等)会将某些网页内容描述为包含 NDEF 消息作为载荷的 NDEF 记录。调用 record.toRecords()
以将 data
转换为智能海报记录中包含的记录列表。它应该包含网址记录、标题的文本记录、图片的 MIME 类型记录以及一些自定义本地类型记录(如 ":t"
、":act"
和 ":s"
),分别用于智能海报记录的类型、操作和大小。
本地类型记录仅在所在的 NDEF 记录的本地上下文中是唯一的。当类型的含义在包含的记录的本地上下文之外无关紧要时,以及存储空间用量具有硬性限制时,请使用这些变量。在 Web NFC 中,本地类型记录名称始终以 :
开头(例如 ":t"
、":s"
、":act"
)。例如,这是为了区分文本记录和本地类型文本记录。
function readSmartPosterRecord(smartPosterRecord) {
console.assert(record.recordType === "smart-poster");
let action, text, url;
for (const record of smartPosterRecord.toRecords()) {
if (record.recordType == "text") {
const decoder = new TextDecoder(record.encoding);
text = decoder.decode(record.data);
} else if (record.recordType == "url") {
const decoder = new TextDecoder();
url = decoder.decode(record.data);
} else if (record.recordType == ":act") {
action = record.data.getUint8(0);
} else {
// TODO: Handle other type of records such as `:t`, `:s`.
}
}
switch (action) {
case 0:
// Do the action
break;
case 1:
// Save for later
break;
case 2:
// Open for editing
break;
}
}
如需写入智能海报记录,请将 NDEF 消息传递给 NDEFReader write()
方法。NDEF 消息中包含的智能海报记录定义为一个对象,其中 recordType
键设置为 "smart-poster"
,data
键设置为一个对象,再次表示智能海报记录中包含的 NDEF 消息。
const encoder = new TextEncoder();
const smartPosterRecord = {
recordType: "smart-poster",
data: {
records: [
{
recordType: "url", // URL record for smart poster content
data: "https://my.org/content/19911"
},
{
recordType: "text", // title record for smart poster content
data: "Funny dance"
},
{
recordType: ":t", // type record, a local type to smart poster
data: encoder.encode("image/gif") // MIME type of smart poster content
},
{
recordType: ":s", // size record, a local type to smart poster
data: new Uint32Array([4096]) // byte size of smart poster content
},
{
recordType: ":act", // action record, a local type to smart poster
// do the action, in this case open in the browser
data: new Uint8Array([0])
},
{
recordType: "mime", // icon record, a MIME type record
mediaType: "image/png",
data: await (await fetch("icon1.png")).arrayBuffer()
},
{
recordType: "mime", // another icon record
mediaType: "image/jpg",
data: await (await fetch("icon2.jpg")).arrayBuffer()
}
]
}
};
const ndef = new NDEFReader();
await ndef.write({ records: [smartPosterRecord] });
读取和写入外部类型记录
如需创建应用定义的记录,请使用外部类型记录。这些文件可能包含 NDEF 消息,作为可通过 toRecords()
访问的载荷。其名称包含签发组织的域名、一个冒号,以及长度至少为一个字符的类型名称,例如 "example.com:foo"
。
function readExternalTypeRecord(externalTypeRecord) {
for (const record of externalTypeRecord.toRecords()) {
if (record.recordType == "text") {
const decoder = new TextDecoder(record.encoding);
console.log(`Text: ${textDecoder.decode(record.data)} (${record.lang})`);
} else if (record.recordType == "url") {
const decoder = new TextDecoder();
console.log(`URL: ${decoder.decode(record.data)}`);
} else {
// TODO: Handle other type of records.
}
}
}
如需写入外部类型记录,请将 NDEF 消息字典传递给 NDEFReader write()
方法。NDEF 消息中包含的外部类型记录被定义为一个对象,其中 recordType
键设置为外部类型的名称,data
键设置为一个对象,该对象表示外部类型记录中包含的 NDEF 消息。请注意,data
键也可以是 ArrayBuffer
,或者提供对 ArrayBuffer
的视图(例如 Uint8Array
和 DataView
)。
const externalTypeRecord = {
recordType: "example.game:a",
data: {
records: [
{
recordType: "url",
data: "https://example.game/42"
},
{
recordType: "text",
data: "Game context given here"
},
{
recordType: "mime",
mediaType: "image/png",
data: await (await fetch("image.png")).arrayBuffer()
}
]
}
};
const ndef = new NDEFReader();
ndef.write({ records: [externalTypeRecord] });
读取和写入空记录
空记录没有载荷。
如需写入空记录,请将 NDEF 消息字典传递给 NDEFReader write()
方法。NDEF 消息中包含的空记录定义为一个对象,将 recordType
键设置为 "empty"
。
const emptyRecord = {
recordType: "empty"
};
const ndef = new NDEFReader();
await ndef.write({ records: [emptyRecord] });
浏览器支持
Chrome 89 中的 Android 设备支持网络 NFC。
开发提示
在开始使用网络 NFC 功能时,我希望能够提前了解以下信息:
- 在 Web NFC 正常工作之前,Android 会在操作系统级别处理 NFC 标签。
- 您可以在 material.io 上找到 NFC 图标。
- 使用 NDEF 记录
id
,在需要时轻松识别记录。 - 支持 NDEF 的未格式化 NFC 标签包含一条空类型的记录。
- 写入 Android 应用记录非常简单,如下所示。
const encoder = new TextEncoder();
const aarRecord = {
recordType: "android.com:pkg",
data: encoder.encode("com.example.myapp")
};
const ndef = new NDEFReader();
await ndef.write({ records: [aarRecord] });
样本歌曲
试用官方示例,并查看一些精彩的 Web NFC 演示:
反馈
网络 NFC 社区小组和 Chrome 团队非常期待听到您对网络 NFC 的看法和体验。
向我们介绍 API 设计
是否存在 API 无法正常运行的问题?或者,您是否需要缺少一些方法或属性来实现您的想法?
在 Web NFC GitHub 代码库中提交规范问题,或将您的想法添加到现有问题中。
报告实施方面的问题
您是否发现了 Chrome 实现方面的错误?或者实现方式是否不同于规范?
在 https://new.crbug.com 上提交 bug。请务必提供尽可能多的详细信息,提供重现 bug 的简单说明,并将组件设置为 Blink>NFC
。Glitch 非常适合用于快速轻松地分享重现的视频。
表达支持
打算使用 Web NFC 吗?您的公开支持有助于 Chrome 团队确定功能的优先级,并向其他浏览器供应商展示支持这些功能的重要性。
请使用 # 标签 #WebNFC
向 @ChromiumDev 发送一条推文,并告诉我们您使用该产品的位置和方式。
实用链接
- 规格
- 网页 NFC 演示 | 网页 NFC 演示来源
- 跟踪 bug
- ChromeStatus.com 条目
- Blink 组件:
Blink>NFC
致谢
非常感谢 Intel 员工实现 Web NFC。Google Chrome 有赖于提交者社区的紧密协作,以便推进 Chromium 项目。并非每位 Chromium 提交者都是 Google 员工,这些贡献者值得特别表彰!