概述
全网最详细的谷歌插件开发小册👏🏻,之前写谷歌插件的时候绕了一圈网上的教程,没有发现比较好的文档教程,索性根据官方文档梳理一遍,避免后面学习的同学继续踩坑!!!
目录
概述 在Chrome Web Stor e发布和更新插件插件的版本管理与错误处理 插件的安全性和隐私保护 Chrome插件的调试技巧 使用automated testing进行插件测试 使用DevTools进行性能分析 插件中的消息传递 使用tabs API 使用bookmarks API 使用notifications API 使用webRequest API 更多API storage API alarms API contextMenus API 插件权限系统 内容安全策略(CSP) 插件生命周期 事件系统 浏览器事件 网络事件 用户事件 Manifest文件 背景脚本(Background Scripts) 内容脚本(Content Scripts) 弹出页面(Popup Pages) 选项页面(Options Pages) 目录 Chrome插件 插件的基本构成 插件生命周期与事件系统 插件权限系统与内容安全策略(CSP) 深入了解Chrome API 与网页内容进行交互 保存用户设置和数据 实现插件的国际化 插件测试与调试 插件发布与维护 参考资料
Chrome插件
Chrome插件,或称为扩展,是一种专为Google Chrome浏览器设计的软件应用程序。插件可以让开发者向浏览器中添加新的特性或功能,或者对现有的功能进行增强或改变。从改变浏览器的视觉主题,到阻止广告,从管理密码到加强网络安全,Chrome插件的使用场景几乎无所不包,给用户带来高度定制化的浏览体验。
插件运行在Chrome浏览器的沙盒环境中,这意味着它们在浏览器中执行,但不会影响到计算机的其他部分。这种设计保证了浏览器的稳定性和用户的安全。
插件的开发主要依赖于Web技术,包括HTML、CSS和JavaScript,因此,任何有Web开发经验的人都能够相对容易地开始Chrome插件的开发。然而,由于Chrome插件需要与浏览器紧密交互,因此还需要学习一些特定的知识,比如Chrome的API、插件的生命周期、消息传递机制等。
插件的基本构成
Manifest文件
Manifest文件是一个插件的元数据,它告诉Chrome插件的名称、描述、版本、权限以及其他插件需要的属性。下面是一个基本的manifest.json文件:
{
"manifest_version": 2,
"name": "My First Extension",
"description": "This is a sample Chrome Extension",
"version": "1.0",
"icons": {
"48": "icon.png",
"128": "icon_large.png"
},
"permissions": [
"activeTab",
"storage",
"https://*.mywebsite.com/*"
],
"background": {
"scripts": ["background.js"],
"persistent": false
},
"content_scripts": [
{
"matches": ["https://*.google.com/*"],
"js": ["content.js"],
"run_at": "document_end"
}
],
"browser_action": {
"default_icon": "icon.png",
"default_popup": "popup.html",
"default_title": "Open the popup"
},
"options_page": "options.html",
"web_accessible_resources": [
"script.js",
"style.css"
]
}
上面的manifest文件声明了一个插件的基本信息,并指定了插件的背景脚本、弹出窗口以及权限。
背景脚本(Background Scripts)
背景脚本是插件的主要工作区域,它可以监听浏览器事件,执行长时间运行的任务,或者管理插件的其他部分。下面是一个基本的背景脚本:
// background.js
chrome.browserAction.onClicked.addListener(function(tab) {
chrome.tabs.executeScript({
code: 'document.body.style.backgroundColor="green"'
});
});
这个脚本监听浏览器动作的点击事件,然后在当前标签页执行一个简单的脚本,将页面背景设为绿色。
内容脚本(Content Scripts)
内容脚本是插入到网页中的脚本,它们可以直接访问和修改网页的DOM。下面是一个内容脚本的例子,它在页面加载完成后,将所有的链接变为红色:
// content.js
window.onload = function() {
var links = document.getElementsByTagName('a');
for(var i = 0; i < links.length; i++) {
links[i].style.color = 'red';
}
};
弹出页面(Popup Pages)
弹出页面是插件的用户界面,它们在用户点击插件图标时显示。下面是一个弹出页面的HTML代码,它显示一个简单的欢迎消息:
<!-- popup.html -->
<!DOCTYPE html>
<html>
<body>
<h1>Welcome to My Extension!</h1>
</body>
</html>
选项页面(Options Pages)
选项页面是插件的设置页面,用户可以通过它来自定义插件的行为。下面是一个选项页面的例子:
<!-- options.html -->
<!DOCTYPE html>
<html>
<body>
<h1>Extension Options</h1>
<label>Background color: <input type="text" id="bgColor"></label>
<button id="save">Save</button>
<script src="options.js"></script>
</body>
</html>
在上面的选项页面中,用户可以输入一个颜色,然后点击保存按钮来改变插件的背景颜色。这需要配合一个options.js脚本来实现:
document.getElementById('save').onclick = function() {
var color = document.getElementById('bgColor').value;
chrome.storage.sync
.set({backgroundColor: color});
};
插件生命周期与事件系统
插件的生命周期是指从用户安装或更新插件,到用户卸载插件的过程。在这个过程中,插件可以响应各种浏览器或用户事件,执行相应的操作。
插件生命周期
插件的生命周期主要包含以下阶段:
安装或更新:用户第一次安装插件,或者插件有新的版本可供更新时,浏览器会加载并初始化插件。此时,插件可以在background脚本中监听 chrome.runtime.onInstalled
事件,执行初始化操作。
chrome.runtime.onInstalled.addListener(function(details) {
if (details.reason == "install") {
console.log("This is a first install!");
} else if (details.reason == "update") {
console.log("Updated from " + details.previousVersion + " to " + chrome.runtime.getManifest().version + "!");
}
});
启动:用户打开浏览器时,插件会被启动。插件可以在这个阶段初始化数据,设置默认状态等。
chrome.runtime.onStartup.addListener(function() {
console.log("Browser started, initialize plugin data.");
});
运行:插件被启动后,就进入了运行阶段。在这个阶段,插件可以响应用户操作,监听和处理浏览器事件,提供各种功能。
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
if (changeInfo.status == 'complete' && tab.active) {
console.log("Active tab updated, let's do something!");
}
});
停止:用户关闭浏览器时,插件会被停止。插件可以监听 chrome.runtime.onSuspend
事件,保存数据,清理资源等。
chrome.runtime.onSuspend.addListener(function() {
console.log("Browser is about to close, save plugin data.");
});
卸载:用户从浏览器中卸载插件时,插件的生命周期就结束了。插件可以监听 chrome.runtime.onInstalled
事件的uninstall
原因,执行卸载操作。
chrome.runtime.setUninstallURL("https://your_website.com/uninstall", function() {
console.log("Uninstall URL has been set");
});
事件系统
Chrome插件的事件系统允许插件响应各种浏览器和用户事件。以下是一些常见的事件:
浏览器事件
浏览器启动事件: 你可以通过监听 chrome.runtime.onStartup
事件来知道浏览器启动。
chrome.runtime.onStartup.addListener(function() {
console.log("Browser has been started.");
});
浏览器关闭事件: Chrome没有提供浏览器关闭的直接事件,但是可以使用 chrome.windows.onRemoved
事件在最后一个浏览器窗口关闭时执行操作。
chrome.windows.onRemoved.addListener(function(windowId) {
chrome.windows.getAll({}, function(windows) {
if(windows.length == 0) {
console.log("Browser is closing, the last window was closed.");
}
});
});
打开新窗口事件: 你可以通过监听 chrome.windows.onCreated
事件来知道新窗口打开。
chrome.windows.onCreated.addListener(function() {
console.log("New window opened.");
});
关闭窗口事件: 你可以通过监听 chrome.windows.onRemoved
事件来知道窗口关闭。
chrome.windows.onRemoved.addListener(function(windowId) {
console.log("Window with id " + windowId + " was closed.");
});
切换标签页事件: 你可以通过监听 chrome.tabs.onActivated
事件来知道标签页切换。
chrome.tabs.onActivated.addListener(function(activeInfo) {
console.log("Tab with id " + activeInfo.tabId + " is now active.");
});
网络事件
请求发送事件: 你可以通过监听 chrome.webRequest.onBeforeRequest
事件来知道请求发送。
chrome.webRequest.onBeforeRequest.addListener(
function(details) {
console.log("Request is about to be sent: ", details);
},
{urls: ["<all_urls>"]}
);
响应接收事件: 你可以通过监听 chrome.webRequest.onCompleted
事件来知道响应接收。
chrome.webRequest.onCompleted.addListener(
function(details) {
console.log("Request completed: ", details);
},
{urls: ["<all_urls>"]}
);
连接错误事件: 你可以通过监听 chrome.webRequest.onErrorOccurred
事件来知道连接错误。
chrome.webRequest.onErrorOccurred.addListener(
function(details) {
console.log("Error occurred while making request: ", details);
},
{urls: ["<all_urls>"]}
);
用户事件
点击插件图标事件: 你可以通过监听 chrome.browserAction.onClicked
事件来知道插件图标被点击。
chrome.browserAction.onClicked.addListener(function(tab) {
console.log("Plugin icon clicked in tab with id " + tab.id + ".");
});
选择插件菜单事件: 首先需要创建菜单,然后通过监听 chrome.contextMenus.onClicked
事件来知道插件菜单被选择。
chrome.contextMenus.create({
id: "sampleContextMenu",
title: "Sample Context Menu",
contexts: ["page"]
});
chrome.contextMenus.onClicked.addListener(function(info, tab) {
if (info.menuItemId == "sampleContextMenu") {
console.log("Sample Context Menu clicked.");
}
});
使用快捷键事件: 快捷键需要在 manifest.json
文件中定义,然后通过监听chrome.commands.onCommand
事件来知道快捷键被使用。
chrome.commands.onCommand.addListener(function(command) {
console.log('User triggered command: ' + command);
});
插件权限系统与内容安全策略(CSP)
Chrome插件需要在manifest文件中声明其所需的权限,如访问浏览历史,修改网页内容等。此外,为了保护用户,Chrome还实施了内容安全策略(CSP),限制插件可以从哪些源加载资源。
插件权限系统
插件需要的权限需要在 manifest.json
文件中的 "permissions"
部分进行声明。例如,如果一个插件需要访问用户的浏览历史,那么它需要添加 "history"
权限:
{
"name": "My extension",
...
"permissions": [
"history"
],
...
}
权限的种类很多,不同的权限对应插件可以访问的API和资源。更多权限可以在 Chrome 扩展官方文档 中查询。
内容安全策略(CSP)
内容安全策略(Content Security Policy,简称CSP)是一种安全标准,用于预防跨站脚本攻击(XSS)和数据注入攻击。默认的CSP策略禁止扩展加载远程JavaScript或CSS,只允许从扩展包内部加载。也就是说,你不能直接在你的HTML文件中引用一个外部的JS或CSS文件,所有的JS和CSS都应该以文件的形式包含在扩展包中。
如果你需要修改CSP策略,例如你需要从特定的远程服务器加载脚本或样式,你可以在 manifest.json
文件中使用 "content_security_policy"
来声明新的策略。例如:
{
"name": "My extension",
...
"content_security_policy": "script-src 'self' https://example.com; object-src 'self'",
...
}
以上策略表示插件允许从扩展包自身('self')和https://example.com加载脚本,只允许从扩展包自身加载插件.
注意,修改CSP策略应谨慎处理,因为过于宽松的CSP策略可能会带来安全风险。在某些情况下,如果可能,最好使用插件的后台脚本(background script)来加载和处理远程数据,而不是直接在前台页面(例如弹出页面或选项页面)加载远程脚本或样式。
更多关于CSP的信息,可以参考 Chrome 插件开发官方文档 。
深入了解Chrome API
在开发Chrome插件时,我们会频繁地使用Chrome API。这些API提供了许多功能,例如操作标签页,访问和修改用户书签,发送桌面通知,拦截和修改网络请求等。接下来,我们将详细介绍其中的一部分。
插件中的消息传递
在插件中,我们通常需要在不同的脚本之间进行通信,例如在background脚本和content脚本之间,或者在popup脚本和background脚本之间。Chrome提供了chrome.runtime.sendMessage
和chrome.runtime.onMessage
API,用于在插件的不同组件之间发送和接收消息。
以下是一个在content脚本中发送消息,并在background脚本中接收消息的示例:
// content script
chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
console.log(response.farewell);
});
// background script
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if (request.greeting == "hello")
sendResponse({farewell: "goodbye"});
});
使用tabs API
chrome.tabs
API 允许插件操作浏览器的标签页,例如创建新的标签页,关闭标签页,切换标签页,修改标签页的URL等。以下是一个创建新标签页的示例:
chrome.tabs.create({url: "http://www.example.com"});
使用bookmarks API
chrome.bookmarks
API 允许插件操作用户的书签,例如创建书签,删除书签,搜索书签等。以下是一个创建书签的示例:
chrome.bookmarks.create({
'parentId': '1',
'title': 'Extension bookmarks',
'url': 'http://www.example.com'
});
使用notifications API
chrome.notifications
API 允许插件发送桌面通知。以下是一个发送通知的示例:
chrome.notifications.create({
type: 'basic',
iconUrl: 'icon.png',
title: 'Notification title',
message: 'Notification message'
});
使用webRequest API
chrome.webRequest
API 允许插件监控和修改网络请求。例如,以下代码监听所有的网络请求,并在控制台中打印请求的URL:
chrome.webRequest.onBeforeRequest.addListener(
function(details) { console.log(details.url); },
{urls: ["<all_urls>"]},
["blocking"]
);
更多API
除了上述提到的API,Chrome还提供了许多其他功能强大的API,可以满足不同插件的需求。
storage API
用于在插件中存储和读取数据。
// 保存数据
chrome.storage.sync.set({ key: value }, function() {
console.log("Data saved.");
});
// 读取数据
chrome.storage.sync.get("key", function(result) {
console.log("Data retrieved: ", result.key);
});
alarms API
用于创建和管理定时任务。
// 创建定时任务
chrome.alarms.create("alarmName", { delayInMinutes: 10 });
// 监听定时任务触发事件
chrome.alarms.onAlarm.addListener(function(alarm) {
console.log("Alarm triggered: ", alarm);
});
contextMenus API
用于在浏览器上下文菜单中添加自定义菜单项。
// 创建上下文菜单
chrome.contextMenus.create({
id: "menuItemId",
title: "Menu Item",
contexts: ["page"]
});
// 监听菜单项点击事件
chrome.contextMenus.onClicked.addListener(function(info, tab) {
console.log("Menu item clicked: ", info.menuItemId);
});
更多详见谷歌插件API
与网页内容进行交互
与网页内容进行交互是Chrome插件开发中常见的需求,可以通过内容脚本和消息传递来实现。下面是如何与网页内容进行交互的示例:
// Content script
// 监听来自插件的消息
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
if (message.action === "changeColor") {
// 修改网页元素的颜色
document.body.style.backgroundColor = message.color;
sendResponse({ message: "Color changed!" });
}
});
// 向插件发送消息
chrome.runtime.sendMessage({ action: "getData" }, function(response) {
console.log("Data received from plugin: ", response.data);
});
// Background script
// 向内容脚本发送消息
chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, { action: "changeColor", color: "#FF0000" }, function(response) {
console.log("Response from content script: ", response.message);
});
});
// 监听来自内容脚本的消息
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
if (message.action === "getData") {
// 获取数据并发送给内容脚本
sendResponse({ data: "Some data from plugin" });
}
});
通过以上代码,插件可以向内容脚本发送消息,并与网页内容进行交互,例如修改网页元素的样式、获取网页中的数据等。
保存用户设置和数据
插件可以使用Chrome的存储API(storage API)来保存用户设置和数据。下面是使用storage API保存和读取数据的示例:
// 保存数据
chrome.storage.sync.set({ key: value }, function() {
console.log("Data saved.");
});
// 读取数据
chrome.storage.sync.get("key", function(result) {
console.log("Data retrieved: ", result.key);
});
以上代码使用 chrome.storage.sync
来保存和读取数据,数据会与用户的Chrome账号关联,可在不同设备间同步。
实现插件的国际化
插件的国际化是为了让插件能够支持多语言环境,并提供不同语言的翻译。以下是如何实现插件的国际化的示例:
在插件的根目录下创建一个名为
_locales
的文件夹。在
_locales
文件夹中创建一个子文件夹,以语言代码命名(例如en
表示英语,zh_CN
表示简体中文)。在每个语言代码的文件夹中,创建一个
messages.json
文件,用于存储对应语言的翻译。例如,在
en
文件夹中的messages.json
可以包含如下内容:{
"pluginTitle": {
"message": "My Plugin"
},
"greeting": {
"message": "Hello, world!"
}
}
4. 在插件的代码中使用 `chrome.i18n.getMessage` 方法来获取对应语言的翻译。
```javascript
// 获取插件标题的翻译
var pluginTitle = chrome.i18n.getMessage("pluginTitle");
// 获取问候语的翻译
var greeting = chrome.i18n.getMessage("greeting");
如果当前语言环境没有对应的翻译,将会使用默认的翻译(默认翻译可以在 messages.json
中指定)。
通过以上步骤,插件可以根据用户的语言设置自动加载对应的翻译文件,实现国际化功能。
请注意,在 manifest.json
文件中的 "default_locale"
字段中指定插件的默认语言。
{
"name": "My Plugin",
...
"default_locale": "en",
...
}
插件测试与调试
在开发Chrome插件时,测试和调试是非常重要的步骤,以确保插件的功能正常并且符合预期。下面是关于插件测试和调试的详细内容。
Chrome插件的调试技巧
调试是开发过程中解决问题和改进插件的关键步骤之一。以下是一些常用的Chrome插件调试技巧:
使用 console.log()
打印调试信息:在开发过程中,可以使用console.log()
在开发者工具的控制台中输出信息,以便查看变量的值、代码的执行流程等。
console.log("Variable value:", variable);
使用 debugger
关键字设置断点:在代码中插入debugger
关键字可以在开发者工具中设置断点,当代码执行到此处时会暂停执行,以便进行逐行调试。
function myFunction() {
// ...
debugger;
// ...
}
使用 chrome.runtime.onInstalled
事件:可以在插件安装或更新时,监听chrome.runtime.onInstalled
事件来执行一些调试任务,如初始化数据、显示调试信息等。
chrome.runtime.onInstalled.addListener(function(details) {
if (details.reason == "install") {
console.log("Extension installed.");
} else if (details.reason == "update") {
console.log("Extension updated.");
}
});
使用Chrome开发者工具:Chrome提供了强大的开发者工具,包括元素检查、网络监控、性能分析等功能,可以帮助调试和优化插件的开发过程。
这些调试技巧可以帮助开发者定位问题、追踪代码执行,以及优化插件的开发过程。
使用automated testing进行插件测试
自动化测试(automated testing)是一种在开发过程中自动执行测试用例的方法,可以提高测试效率并确保插件的功能和稳定性。
在Chrome插件开发中,可以使用一些测试框架和工具来进行自动化测试,例如:
Jasmine:是一个流行的JavaScript测试框架,可以用于编写和执行单元测试、集成测试等。
describe("MyPlugin", function() {
it("should do something", function() {
expect(myPlugin.doSomething()).toBe(true);
});
it("should handle async operations", function(done) {
myPlugin.doAsyncOperation(function(result) {
expect(result).toBe(true);
done();
});
});
});
Selenium WebDriver:是一个用于自动化浏览器操作的工具,可以模拟用户在浏览器中的交互行为,用于编写端到端(end-to-end)测试。
const { Builder, By, Key } = require("selenium-webdriver");
async function myTest() {
let driver = await new Builder().forBrowser("chrome").build();
try {
await driver.get("https://www.example.com");
await driver.findElement(By.name("q")).sendKeys("test", Key.ENTER);
let title = await driver.getTitle();
console.log("Page title:", title);
} finally {
await driver.quit();
}
}
myTest();
使用自动化测试可以帮助开发者验证插件的功能和性能,降低人为错误的发生,并提高开发效率。
使用DevTools进行性能分析
Chrome开发者工具提供了强大的性能分析功能,可以帮助开发者找出插件中的性能瓶颈和优化点。以下是使用DevTools进行性能分析的步骤:
打开Chrome开发者工具(快捷键:F12)。
切换到"Performance"选项卡。
点击"Record"按钮开始录制性能数据。
进行一些操作,以触发插件的功能。
停止录制,分析性能数据。
在性能分析结果中,可以查看函数的执行时间、内存使用情况、页面加载时间等信息,以便找出性能瓶颈和进行优化。
性能分析可以帮助开发者优化插件的执行效率和资源占用,提高插件的性能和用户体验。
插件发布与维护
在开发完成后,将插件发布到Chrome Web Store是使其可供用户下载和安装的关键步骤。同时,对于已发布的插件,维护和管理也是非常重要的。以下是关于插件发布与维护的详细内容。
在Chrome Web Store发布和更新插件
将插件发布到Chrome Web Store可以让用户轻松找到、安装和使用插件。以下是发布和更新插件的步骤:
登录到Chrome开发者控制台。
创建一个新的开发者账号或使用现有的账号。
在开发者控制台中,选择"开发者中心"并点击"新增项目"按钮。
提供插件的基本信息,包括名称、描述、图标等。
上传插件的压缩文件(
.zip
格式),其中包含了插件的所有文件和资源。填写其他必要的信息,如分类、语言、隐私政策等。
验证插件的所有权限,并确保插件符合Chrome Web Store的规定和政策。
提交插件进行审核。审核过程可能需要几天时间。
一旦插件审核通过,它将在Chrome Web Store上可见,用户可以进行安装。
对于更新插件,可以通过上传新版本的压缩文件并更新插件的信息。
在发布和更新插件时,需要确保插件的完整性、安全性,并遵守Chrome Web Store的规定和政策。
插件的版本管理与错误处理
在插件的开发和维护过程中,版本管理和错误处理是非常重要的。以下是一些常见的版本管理和错误处理技巧:
版本管理:对于插件的每个发布版本,应使用语义化版本号(Semantic Versioning)来管理。可以在插件的 manifest.json
文件中指定版本号,并确保每个版本的更新都有明确的变更记录。
{
"manifest_version": 2,
"version": "1.0.0",
...
}
错误处理:在插件中使用适当的错误处理机制可以提高插件的健壮性。可以使用 try...catch
语句来捕获并处理可能出现的错误,以及使用console.error()
方法将错误信息输出到控制台。
try {
// 代码块可能抛出错误的部分
} catch (error) {
console.error("An error occurred:", error);
}
通过合理的版本管理和有效的错误处理,可以更好地维护和管理插件,及时修复bug并提供更好的用户体验。
插件的安全性和隐私保护
在插件开发过程中,确保插件的安全性和保护用户隐私是至关重要的。以下是一些常见的安全性和隐私保护措施:
权限管理:在 manifest.json
文件中,只授予插件所需的最小权限。不要授予过多的权限,以避免滥用用户的隐私。
{
"manifest_version": 2,
"permissions": [
"storage",
"tabs"
],
...
}
数据保护:在处理用户数据时,采取适当的安全措施,如数据加密、安全传输等,以防止数据泄露或被恶意使用。
更新检查:定期检查插件的更新并及时应用,以修复安全漏洞和错误,并提供用户所需的新功能。
安全审查:在开发过程中,进行安全审查,包括代码审查和漏洞扫描,以确保插件没有潜在的安全问题。
隐私政策:如果插件收集用户个人信息,应提供清晰的隐私政策,并遵守相关的数据保护法律和规定。
遵循这些安全性和隐私保护措施,可以增强插件的安全性,保护用户的隐私,并提升用户对插件的信任。
参考资料
Chrome开发者文档 - Chrome官方提供的开发者文档,包含了全面的插件开发指南和API参考。
Chrome插件教程 - Chrome官方提供的插件开发入门教程,逐步介绍如何创建和发布插件。
Chrome插件示例 - Chrome官方提供的插件示例代码,涵盖了各种功能和用例,可以作为参考和学习的资源。
Chrome插件开发者社区 - Chrome插件开发者社区的论坛,可以在这里提问、讨论和交流插件开发的相关话题。
Chrome插件开发工具 - Chrome开发者工具的文档,介绍了如何使用开发者工具进行调试和性能分析。
Chrome Web Store开发者文档 - Chrome Web Store的开发者文档,提供了发布和管理插件的指南和说明。
Chrome插件安全性和隐私保护指南 - Chrome官方提供的插件安全性和隐私保护的指南,介绍了如何确保插件的安全性和保护用户隐私。
转载于:https://mp.weixin.qq.com/s/q5TaoNkW2aeDyHV9BQxqhg