Plasmo Framework:次世代的浏览器插件开发框架

发表于 2年以前  | 总阅读数:3356 次

分享目标

  • 有可能你生活、工作中很多场景可以通过开发一个浏览器插件来解决,但是你不知道可以通过它来解决,也不了解浏览器插件的能力上限,所以你就不会想到去实现,或者使用插件去实现,本文希望从 “广度”、“深度” 两个方面着手,帮助你全面的认识浏览器插件,并在合适的时候能够想起它、使用它,提高工作效率!
  • 分享业务的开发实践,因为 Plasmo 等框架对插件开发做了一层抽象,那么不太合理的抽象可能会导致一些 “抽象泄露”,随之带来开发的 “坑”,本文希望将这些坑也发掘出来,同时希望能够思考,沉淀,希望形成可复用的知识,帮助后来者能够更快,更进一步完成插件开发的目标。

缘起

最近团队在做的业务需要重度使用浏览器插件,所以有必要对浏览器插件进行全面的调研与实践,以了解其上限,并考虑将浏览器插件的开发与现有的 Web 工程化开发流程进行结合,提高开发的效率与幸福感,于是遇到了 Plasmo Framework -- 一个开发浏览器插件的工程化框架,本文将尝试介绍关于插件、插件开发、基于 Plasmo 的插件开发以及业务实践等相关内容。

阅读本文,你将学习到:

1 . 浏览器插件的 Why/What/How 等原理性的内容

2 . 了解传统浏览器插件的开发流程

3 . 了解 Plasmo Framework 的原理

4 . 了解 Plasmo Framework 框架引入之后的浏览器插件的开发流程

5 . 了解插件开发过程中的业务实践

6 . 更近一步,教你开发(可能是)人生中第一个插件:)

关于浏览器插件

注意:正文以 Chrome 插件为例进行讲解。

为什么需要浏览器插件?

早期的浏览器厂商有一个愿景,希望基于浏览器打造一个 Browser OS,浏览网页是 OS 的一类应用,使用 Browser 的扩展 API,构建更多的应用,或管理浏览网页的体验,或提供多个网页、应用之间进行交流的桥梁,也成为了浏览器厂商巩固自己地位,在激烈的浏览器大战中取得胜出的关键筹码。

想象一下今天的建筑在微信 OS 上的小程序、支付宝小程序,任何一个应用当集聚一定流量之后都希望用各种各样的 “手段” 留住用户,让用户高频次的打开自家应用,这就是为什么浏览器除了提供网页浏览体验之外,还希望提供个性化的 “浏览器插件” 这样的应用,就是希望浏览器插件可以成为一种新的 “Desktop App”(桌面端应用):

1 . 通过浏览器的流量积累

2 . 提供 “浏览器插件” 的 “应用” 方式

3 . 吸引开发者构建应用与提供原始 Web 开发技术栈的支持

4 . 为开发者提供应用分发的渠道 “Chrome Web Store”

5 . 提供浏览器插件可以触达用户的入口,用户可以方便的消费浏览器插件

6 . 各行各业浏览器插件涌现,与日益增长的用户形成良性的消费反馈循环

7 . 建立浏览器这一巨头应用的竞争壁垒

Chrome Web Store

image.png

浏览器插件消费入口

image.png

插件实际消费的效果(为页面注入脚本、UI)

当然浏览器巨头的竞争,方便的是我们消费者,我们现在可以享受到各行各业的插件应用带来的效率与生产力的提升,甚至借助插件还可以获取到很多整合类的消息与咨询,扩充了我们的视野。

什么是浏览器插件?

一句话解释:满足用户打造个性化的浏览器体验的一系列 “应用”,这些应用基于 Web 开发技术栈开发,可调用一系列插件独有的扩展 API,运行在安全的沙箱环境,开发出来之后可以上架到 Chrome Web Store,在浏览器侧边栏的插件栏进行消费。

image.png

浏览器插件、网页、以及两者之间的关系架构图

其中图中提到的 Background Script、Popup/Option/Override Page、Content Script 与 Web Page 图示如下。

image.png

Background Script

image.png

Content Script

image.png

Web Page

Popup Page

image.png

Option Page

Override Page

浏览器插件能干什么?

比较通俗一点:

  • 浏览器页面能做的,插件都能做

  • 因为插件的 Content Script 与浏览器页面共享 DOM,所以比如渲染页面(HTML/CSS)、执行脚本(JavaScript)、操作浏览器的 DOM、或通过操作 DOM 注入 JS 脚本操作 BOM 等 API

  • 浏览器页面不能做的,插件也能做

  • 插件 Web 侧的 Content Script、以及插件的 Background Script、Popup/Option/Override Page 等可以调用浏览器为插件提供的独有的 Extension API,如 runtimetabscookiedevtools 等,所以插件可以管理多 Tab、定制开发者调试工具(Devtools)、定制浏览器页面右键点击菜单栏、定制书签、定制搜索栏、定制新开 Tab 页面等

image.png

Tab 管理

image.png

右键菜单栏

image.png

Devtools

image.png

搜索栏

定制新 Tab

参考 Chrome 插件官方提供的例子,可以对插件可以做的事情进行一个大致的归类,主要展示一些高频使用场景。

参考:https://github.com/GoogleChrome/chrome-extensions-samples

插件用途 使用的 API 插件地址
书签管理 - bookmarks.create- bookmarks.getTree- bookmarks.remove- bookmarks.update- tabs.create- ... Github 地址[1]
浏览器页面信息管理 - browserAction.onClicked- browserAction.setIcon- runtime.onInstalled- storage.StorageArea.get- storage.StorageArea.set- ... Github 地址:1. 动态改 Favicon[2]2. 页面背景颜色[3]3. 添加右键菜单栏[4]4. 注入脚本[5]
浏览器 Tab 管理 - extension.getURL- tabs.create- tabs.update- ... Github 地址:1. Tab 折叠[6]2. 新 Tab 展示页面重载[7]
浏览历史管理 - history.deleteAll- history.deleteUrl- history.search- ... Github 地址[8]:1. 浏览器历史页面重载[9]
快捷键管理 - commands.onCommand- ... Github 地址[10]
网络管理 - browserAction.onClicked- cookies.getAll- cookies.onChanged- cookies.remove- ... Github 地址:1. 处理 Cookie[11]2. 处理 HTTP Headers[12]
调试管理 - browserAction.onClicked- debugger.attach- debugger.detach- debugger.onEvent- ... Github 地址:1. 处理 JS 执行、暂停[13]
开发者工具栏管理 - devtools.panels.ElementsPanel.createSidebarPane- devtools.panels.ElementsPanel.onSelectionChanged- ... Github 地址:1. 操作 Element 面板信息[14]
通知管理 - notifications.create- notifications.onButtonClicked- ... Github 地址[15]
搜索栏管理 - omnibox.onInputEntered- tabs.create- omnibox.onInputChanged- omnibox.onInputEntered- ... Github 地址:1. 处理 OmniBox[16]

举例几个可能和我们研发相关的插件:

插件介绍 使用 API 图示 插件地址
展示代码 Diff - clipboard[17]- fileSystem[18]- storage[19] Github 地址[20]
读取本地文件系统 - fileSystem[21] storage[22] Github 地址[23]
Github OAuth - identity[24] Github 地址[25]
- 展示编辑器,进行代码编辑- 代码编辑器 - chrome.fileSystem[26]- Runtime[27]- Window[28] - Github 地址[29]- 代码编辑器[30]
图片裁剪 - fileSystem[31]- storage[32] Github 地址[33]
进行各种浏览器通知 - Notification API documentation[34] Github 地址[35]

传统插件开发流程

上面我们提到插件分为两块:

  • 插件域:整个浏览器生命周期只会存在一份

  • Popup/Option/Override Page:基于 Web 技术栈 HTML/CSS/JavaScript,可以调用插件的部分 API

  • Background Script:基于 JavaScript,运行在 Service Worker 中,可以调用插件的全部 API

  • 属于插件,但存在 Web 页面的独立域:和页面相关,一个页面的生命周期可以注入一到多个 Content Script

  • Content Script:基于 JavaScript 语法,与主页面共享 DOM,可以调用插件的部分 API

image.png

所以一个传统的浏览器的插件开发流程如下:

image.png

上述的开发流程就和我们还没有引入前端工程化时期的开发流程很像,主要就是如下几个流程:

1 . 编写原生的 HTML/CSS/JavaScript,然后调用浏览器提供的插件 API 完成业务逻辑,通过 Git 管理开发代码

2 . 把在不同的环境使用不同的环境变量标志、调用不同的接口、获取不同的数据

3 . 因为没法使用 Node.js、没有包管理的概念,所以基本上只能手工测试,或者引入一些 UMD 的包测试框架进行测试

4 . 将插件目录文件夹打包发给用户进行验收测试,为了保持隐私,这里可能需要对代码进行一轮混淆

5 . 测试没问题,发布 PPE 进行测试,可以通过 CI/CD 来进行持续集成、交付

6 . PPE 没问题,发布线上进行测试,可以通过 CI/CD 来进行持续集成、交付

7 . 有问题进行回滚、Hotfix 等

传统插件开发 Quick Start

一份极简的插件开发代码如下:

  • 包含 manifest.json ,作为插件的索引文件,在其中索引 background.jspopup.html、以及一些 ICON 资源等,类似 Node.js 的 package.json 的文件
  • 包含 background.js ,运行在 Service Worker 中,调用插件的 API,监听定时器做定时喝水的通知提醒,然后监听通知按钮的点击进行续时
  • 包含 popup.htmlpopup.js 用于打开页面,设置定时,有随机定时、15 min、30 min 和取消定时,设定定时之后会进行 插件的 Icon Badge 为 ON 打开计时
  • 调用 chrome.notifications API 会在系统通知栏展示通知结果。

目录结构如下:

.
├── background.js
├── drink_water128.png
├── drink_water16.png
├── drink_water32.png
├── drink_water48.png
├── manifest.json
├── popup.html
├── popup.js
└── stay_hydrated.png

该插件的地址参见:https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/examples/water_alarm_notification

该插件的代码如下:使用纯 HTML/CSS/JavaScript 开发

manifest.json

{
  "name": "Drink Water Event Popup",
  "description": "Demonstrates usage and features of the event page by reminding user to drink water",
  "version": "1.0",
  "manifest_version": 3,
  "permissions": [
    "alarms",
    "notifications",
    "storage"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_title": "Drink Water Event",
    "default_popup": "popup.html"
  },
  "icons": {
    "16": "drink_water16.png",
    "32": "drink_water32.png",
    "48": "drink_water48.png",
    "128": "drink_water128.png"
  }
}

background.js

'use strict';

chrome.alarms.onAlarm.addListener(() => {
  chrome.action.setBadgeText({ text: '' });
  chrome.notifications.create({
    type: 'basic',
    iconUrl: 'stay_hydrated.png',
    title: 'Time to Hydrate',
    message: 'Everyday I'm Guzzlin'!',
    buttons: [
      { title: 'Keep it Flowing.' }
    ],
    priority: 0
  });
});

chrome.notifications.onButtonClicked.addListener(async () => {
  const item = await chrome.storage.sync.get(['minutes']);
  chrome.action.setBadgeText({ text: 'ON' });
  chrome.alarms.create({ delayInMinutes: item.minutes });
});

popup.html

<!DOCTYPE html>
<html>
  <head>
    <title>Water Popup</title>
    <style>
      body {
        text-align: center;
      }

      #hydrateImage {
        width: 100px;
        margin: 5px;
      }

      button {
        margin: 5px;
        outline: none;
      }

      button:hover {
        outline: #80DEEA dotted thick;
      }
    </style>
    <!--
      - JavaScript and HTML must be in separate files
     -->
  </head>
  <body>
      <img src='./stay_hydrated.png' id='hydrateImage'>
      <!-- An Alarm delay of less than the minimum 1 minute will fire
      in approximately 1 minute increments if released -->
      <button id="sampleMinute" value="1">Sample minute</button>
      <button id="min15" value="15">15 Minutes</button>
      <button id="min30" value="30">30 Minutes</button>
      <button id="cancelAlarm">Cancel Alarm</button>
   <script src="popup.js"></script>
  </body>
</html>

popup.js

'use strict';

function setAlarm(event) {
  let minutes = parseFloat(event.target.value);
  chrome.action.setBadgeText({text: 'ON'});
  chrome.alarms.create({delayInMinutes: minutes});
  chrome.storage.sync.set({minutes: minutes});
  window.close();
}

function clearAlarm() {
  chrome.action.setBadgeText({text: ''});
  chrome.alarms.clearAll();
  window.close();
}

//An Alarm delay of less than the minimum 1 minute will fire
// in approximately 1 minute increments if released
document.getElementById('sampleMinute').addEventListener('click', setAlarm);
document.getElementById('min15').addEventListener('click', setAlarm);
document.getElementById('min30').addEventListener('click', setAlarm);
document.getElementById('cancelAlarm').addEventListener('click', clearAlarm);

关于 Plasmo Framework

工程化插件开发流程

可以看到上述传统插件的开发流程,基本上使用原生的 HTML/CSS/JavaScript 技术栈,然后手工分发源码等方式完成包的部署,当然可以引入 CI/CD 来进行部署发布等。

上述流程对于在极佳 DX 的前端工程化工具链的熏陶中的我们来说肯定是不符合我们现代化 Web 工程化开发的诉求的,我们期望的流程可能是如下这样的:

image.png

我们希望:

1 . 能够有脚手架,一键初始化项目,开启项目开发服务器(热更新),构建可部署产物,提供如 Init/Dev/Build 等命令

2 . 能够在使用主流前端框架、语言和 UI 库等,如 React、Redux、TypeScript、Tailwind、Ant Deisgn/Semi Design/Arco Design 等

3 . 内置最佳开发实践,一个插件开发的生命周期与各个模块能够以灵活、可扩展的方式提供出来

4 . 提供各种样例、开源代码库、有友好的开发者社区可以答疑解惑等等

5 . 能与现代 Web 工程化开发对齐,方便的整合进现有的 CI/CD 流程中

当然熟练掌握 Webpack/Parcel/Vite 的同学可能可以方便的搭建出上述的框架出来,而我们也是在权衡调研之后,发现了一个几乎解决了上述所有述求的开发框架:Plasmo Framework[36],甚至提供了比我们预期还要多得多的好用特性。

框架原理

Plasmo 是基于 Parcel 封装的一套脚手架,脚手架吸收了插件开发的最佳实践,并结合了现代 Web 前端工程化开发的最佳实践。

参考 Parcel:https://parceljs.org/recipes/web-extension/

Plasmo 的项目地址为:https://github.com/PlasmoHQ/plasmo

项目目录结构如下:

.
├── cli // CLI、脚手架
│   ├── create-plasmo
│   │   └── src
│   └── plasmo
│       ├── i18n
│       ├── src
│       │   ├── commands
│       │   └── features
│       │       ├── extension-devtools
│       │       ├── extra
│       │       ├── helpers
│       │       └── manifest-factory
│       └── templates // 支持各种模板的渲染,如 React、Svelte、Vue3
│           └── static
│               ├── react17
│               ├── react18
│               ├── svelte3
│               └── vue3
├── examples // 例子
│   ├── with-ant-design
│   │   └── assets
│   ├── with-background
│   │   └── assets
├── extensions
│   ├── mice
│   │   ├── assets
│   │   ├── contents
│   │   ├── core
│   │   └── docs
│   └── world-edit
│       ├── assets
│       └── core
├── packages // 公共子包
│   ├── config
│   │   └── ts
│   ├── constants
│   │   └── manifest
│   ├── gcp-refresh-token
│   │   └── src
│   │       └── __snapshots__
│   ├── init // init 命令对应的执行逻辑
│   │   └── templates
│   │       └── assets
│   ├── parcel-bundler
│   │   └── src
│   ├── parcel-config
│   ├── parcel-namer-manifest
│   │   └── src
│   ├── parcel-packager
│   │   └── src
│   ├── parcel-resolver
│   │   └── src
│   ├── parcel-runtime
│   │   └── src
│   ├── parcel-transformer-inject-env
│   │   └── src
│   ├── parcel-transformer-manifest
│   │   ├── runtime
│   │   └── src
│   ├── parcel-transformer-svelte3
│   │   └── src
│   ├── parcel-transformer-vue3
│   │   └── src
│   ├── permission-ui
│   │   └── src
│   ├── prettier-plugin-sort-imports
│   │   └── src
│   │       ├── natural-sort
│   │       └── utils
│   ├── puro
│   │   └── src
│   ├── rps
│   │   └── src
│   │       └── core
│   ├── storage // 包装的 Chrome Storage 的包,提供 React Hooks 版本
│   │   └── src
│   ├── use-hashed-state
│   │   └── src
│   └── utils
└── templates
    └── qtt
        └── src
            └── __snapshots__

Plasmo 使用 Turborepo 来管理 Monrepo 项目,包管理使用 Pnpm。

Turborepo[37] 是一个为基于 JS/TS 的 Monrepo 设计高性能的构建系统。

其原理设计架构图如下:

image.png

对应在 Plasmo Framework 背景下的插件开发流程如下:

image.png

工程化插件开发 Quick Start

【前置条件】

确保安装了 Node.js、Pnpm、Git:

  • Node 安装参见 https://nodejs.org/
  • Pnpm 安装参见 https://pnpm.io/
  • Git 安装参见:https://git-scm.com/

【项目初始化】

在 CLI 中执行如下命令创建一个 Plasmo 插件项目,默认为 React 模板:

pnpm create plasmo

可以看到生成的目录如下:

.
├── README.md
├── assets
│   └── icon512.png
├── node_modules
├── package.json
├── pnpm-lock.yaml
├── popup.tsx
└── tsconfig.json

【项目执行与应用效果查看】

进入项目,执行 pnpm dev 命令,开启开发服务器:

cd hello-world
pnpm dev

接着打开浏览器插件管理页面:chrome://extensions,选择 build 产物,即可完成插件的安装与使用:

确保打开开发者模式、点击加载已解压的扩展程序、选择 build/chrome-mv3-dev 插件包。

image.png

加载成功之后就可以在管理面板看到对应的插件:

image.png

然后在浏览器右上角插件消费栏进行插件消费:

image.png

【项目与代码分析】

可以看到这个插件打开了一个 Popup 页面,展示了标题、输入框和按钮,按钮点击可以跳转 Plasmo 的文档页。

而插件相关的 名称等元信息、需要特殊指定的 manifest 内容等则是在 package.json 中管理,后续 dev/build 时会自动提取这些元信息,写入到 manifest.json 中:

{
  "name": "hello-world",
  "displayName": "Hello world",
  "version": "0.0.0",
  "description": "A basic Plasmo extension.",
  "author": "",
  "packageManager": "pnpm@6.23.6",
  "scripts": {
    "dev": "plasmo dev",
    "build": "plasmo build"
  },
  "dependencies": {
    "plasmo": "0.52.4",
    "react": "18.2.0",
    "react-dom": "18.2.0"
  },
  "devDependencies": {
    "@plasmohq/prettier-plugin-sort-imports": "1.2.0",
    "@types/chrome": "0.0.193",
    "@types/node": "18.6.4",
    "@types/react": "18.0.17",
    "@types/react-dom": "18.0.6",
    "prettier": "2.7.1",
    "typescript": "4.7.4"
  },
  "manifest": {
    "host_permissions": [
      "https://*/*"
    ]
  }
}

以下为写入到 build/chrome-mv3-dev/manifest.json 中的内容:

{
  "icons": {
    "16": "icon16.bee5274e.png",
    "48": "icon48.71d7523e.png",
    "128": "icon128.a87b0594.png"
  },
  "manifest_version": 3,
  "action": {
    "default_icon": {
      "16": "icon16.bee5274e.png",
      "48": "icon48.71d7523e.png"
    },
    "default_popup": "popup.f4f22924.html"
  },
  "version": "0.0.0",
  "name": "Hello world",
  "description": "A basic Plasmo extension.",
  "author": "",
  "permissions": [],
  "host_permissions": ["https://*/*"],
  "content_security_policy": {
    "extension_pages": "script-src 'self' http://localhost;object-src 'self';"
  },
  "web_accessible_resources": [
    { "matches": ["<all_urls>"], "resources": ["__parcel_hmr_proxy__"] }
  ]
}

插件的 icon 自动识别 assets/icon512.png 图片,然后在 dev/build 时进行处理,生成 16/48/128 三类格式,适配不同的使用场景。

项目中各种模块,如 popup 等则使用 React TypeScript 开发 UI、使用 TypeScript 撰写脚本,可以按照正常的 Web 工程化开发的方式进行文件、资源的导入使用。

以下是 popup.tsx 的内容,和我们平时撰写的组件使用一模一样:

import { useState } from "react"

function IndexPopup() {
  const [data, setData] = useState("")

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        padding: 16
      }}>
      <h2>
        Welcome to your{" "}
        <a href="https://www.plasmo.com" target="_blank">
          Plasmo
        </a>{" "}
        Extension!
      </h2>
      <input onChange={(e) => setData(e.target.value)} value={data} />
      <a href="https://docs.plasmo.com" target="_blank">
        View Docs
      </a>
    </div>
  )
}

export default IndexPopup

我们只需要遵守 Plasmo 内建的文件命名、位置放置的规范、文件导入的规范,在 dev/build 时即可识别对应的文件,生成 .plasmo 文件夹,然后将这些内容丢给 Parcel 进行构建,就可以生成符合预期、且可以在浏览器中执行的插件 build 产物。

同时 Plasmo 提供了开发服务器与热更新,享受到改代码就可以实时获取效果的便利。

当我们的插件开发完成,就可以打包进行上架,只需要执行如下命令:

pnpm build --zip // 默认打 mv3 的包
pnpm build --target=firefox-mv2 --zip // 打兼容 Firefox mv2 的包

然后将插件发布到各家应用商店即可:

  • Chrome Webstore: https://developer.chrome.com/docs/webstore/publish/
  • Edge Add-on: https://docs.microsoft.com/en-us/microsoft-edge/extensions-chromium/publish/publish-extension
  • Mozilla Firefox Add-on: https://extensionworkshop.com/documentation/publish/

当然 Plasmo 还提供了 BPP、通过 Github Action 的 CI 工作流持续部署插件到各个商店,详情可以参考作者的文档:https://blog.plasmo.com/p/ext101-tut-0。

业务实践

使用 React + TypeScript 开发 UI[38]

参考:https://blog.plasmo.com/p/content-scripts-ui

在插件开发世界里有两种类型的 UI:

1 . Extension Page UI:存在于插件作用域下的 Web 页面,如 Popup Page UI、Option Page UI、Override Page UI 等

2 . Extension Injected UI:注入到某个 Web 页面下的 UI,如我们常见的在 Web 页面里面进行划词翻译的插件,你选中一个单词,弹出对应的 “释义” 框,这个“释义” 框就是常见的 Extension Injected UI,在插件里的概念也叫 Content Script UI

常见的 Extenion Page UI 举例如下:

Extension Page UI 的在下面三类中一个插件只会有一个

Popup Page UI

Option Page UI

Override Page UI

常见的 Extension Injected UI 举例如下:

Content Scripts 和主页面共享 DOM,Extension Injected UI 每个页面都会有一个,如果设置了对此页面注入的话

Tango Extension

Loom Extension

Omni Extension

常规的上述 Page 开发流程如下:

  • Page UI

  • 创建 HTML 文件,设定一个根节点,如 id="root" 用于渲染 Virtual DOM

  • 创建一段 JS/TS 逻辑,用于处理挂载到 id="root" 节点的逻辑

  • 编写待挂载组件的逻辑,使用 TS(X)/Vue/Svelte 模板语法编写

  • 设置打包工具,如 Webpack、Vite、Parcel 等将写的组件逻辑编译为单一 JS 文件

  • 在 HTML 文件里面创建一个 script tag,然后指向上诉打包的文件

  • 创建 manifest.json 文件,将对应的入口指向 HTML 文件

  • 传统:写 HTML,引入 JavaScript 与 CSS,只能支持浏览器支持的的写法

  • 使用编译工具:

  • Injected UI

  • 创建 shadow DOM 挂载的容器元素

  • 在容器中创建一个 shadow DOM 的根节点

  • 将 shadowDOM 根节点注入到主页面 Body 下

  • 在 shadowDOM 根节点下创建一个 container 元素用于挂载 Virtual DOM(vDOM)

  • 将 vDOM 挂载到 container 元素下

  • 编写待挂载组件的逻辑,使用 TS(X)/Vue/Svelte 模板语法编写,将根组件渲染到此 container 元素下

  • 设置打包工具,如 Webpack、Vite、Parcel 等将写的组件逻辑编译为单一 JS 文件

  • 设置 manifest.json 文件的 content_scripts 数组字段,将这个文件添加进去

  • 创建 Content Script,注入到主页面

  • 通过共享的 DOM,创建 UI 元素

  • 添加 CSS 样式

  • 设置一些操作响应逻辑

  • 为了防止样式泄露,可能需要将内容创建在 iframe 或者 Shadow DOM[39] 里

  • 传统:

  • 使用编译工具:

上诉流程繁冗且复杂,Plasmo 为你做了一层抽象,使得你无需做任何的创建元素、挂载、编译等流程,只需要在 Plasmo 工程下创建对应的 .tsx 文件,编写 React + TypeScript 的逻辑即可。

针对 Extension Page UI,如 popup.tsxoptions.tsx ,然后在文件中 export default 对应的组件,Plasmo 会帮你自动完成上述 “使用编译工具” 所需的全流程步骤:

// popup.tsx

function Popup() {
  return <div>hello, plasmo popup</div>;
}

export default Popup;

针对 Injected UI,只需要创建 content.tsx 文件,或者注入多份时(contents/<name>.tsx),然后在文件中 export default 对应的组件,Plasmo 会帮你自动完成上述 “使用编译工具” 所需的全流程步骤:

// contents.tsx
function Content() {
  return <div>hello, plasmo content</div>;
}

export default Content;

如果你只是想注入脚本,那么命名不需要下 x ,只需要 content.ts 即可。

上述的 Extension Page UI 和 Extension Injected UI 都属于静态的方式,即经过 Plasmo 编译之后,会在 manifest.json 对应的字段声明,然后引入编译后的这些文件。

经过 build 之后,会形成如下目录结构:

.
├── background.f44a92a3.js
├── common.49dcdc31.css
├── content.96c90f8e.js
├── manifest.json
├── popup.a51b985f.css
├── popup.c0bbeb4e.js
├── popup.f4f22924.html

对应的 manifest.json 如下:

// manifest.json
{
  "action": {
    // popup.tsx => popup.f4f22924.html
    "default_popup": "popup.f4f22924.html"
  },
  // ...
  "background": {
    "service_worker": "background.f44a92a3.js",
    "type": "module"
  },
  "content_scripts": [
  // content.tsx => content.96c90f8e.js/common.49dcdc31.css
    {
      "matches": ["<all_urls>"],
      "js": ["content.96c90f8e.js"],
      "css": ["common.49dcdc31.css"],
      "run_at": "document_end"
    }
  ],
}

Runtime Injected UI:动态注入 Content Script UI

参考代码:

  • https://github.com/PlasmoHQ/examples/tree/main/with-content-scripts-ui
  • https://github.com/PlasmoHQ/plasmo/blob/main/cli/plasmo/templates/static/react18/content-script-ui-mount.tsx

在上一小节我们也提到了,声明的 contents.tsx 在编译之后实际上是声明在 manifest.json 中,以静态注入的方式注入到主页面中,可以选择 document_startdocument_enddocument_idle 逻辑,但是这就有一个限制,如果当我们的页面已经加载完成,度过了上述的三个阶段之后,我们才安装插件,此时就无法完成 Content Script UI 的注入。

为了解决这个问题,我们有必要重拾一下上述提到的 Content Script UI 的注入过程:

1 . 创建 shadow DOM 挂载的容器元素

2 . 在容器中创建一个 shadow DOM 的根节点

3 . 将 shadowDOM 根节点注入到主页面 Body 下

4 . 在 shadowDOM 根节点下创建一个 container 元素用于挂载 Virtual DOM(vDOM)

5 . 将 vDOM 挂载到 container 元素下

6 . 编写待挂载组件的逻辑,使用 TS(X)/Vue/Svelte 模板语法编写,将根组件渲染到此 container 元素下

7 . 设置打包工具,如 Webpack、Vite、Parcel 等将写的组件逻辑编译为单一 JS 文件

8 . 设置 manifest.json 文件的 content_scripts 数组字段,将这个文件添加进去

因为目前 Plasmo 只针对静态的 Content Script UI 给了 Out-of-box 的方案,针对动态注入时,就需要手动实现这套方案,剖析 Plasmo 源码,参照上述的过程,我们可以实现动态注入。

参照 plasmo 提供的渲染模板:content-script-ui-mount.tsx

// @ts-nocheck
// prettier-sort-ignore
import React from "react"

import * as RawMount from "__plasmo_mount_content_script__"
import { createRoot } from "react-dom/client"

// Escape parcel's static analyzer
const Mount = RawMount

const MountContainer = () => {
  // ...
  return (
    <div
      id="plasmo-mount-container"
      style={{
        display: "flex",
        position: "relative",
        top,
        left
      }}>
      <RawMount.default />
    </div>
  )
}

async function createShadowContainer() {
  const container = document.createElement("div")

  container.id = "plasmo-shadow-container"

  container.style.cssText = `
    z-index: 1;
    position: absolute;
  `

  const shadowHost = document.createElement("div")

  if (typeof Mount.getShadowHostId === "function") {
    shadowHost.id = await Mount.getShadowHostId()
  }

  const shadowRoot = shadowHost.attachShadow({ mode: "open" })
  document.body.insertAdjacentElement("beforebegin", shadowHost)

  if (typeof Mount.getStyle === "function") {
    shadowRoot.appendChild(await Mount.getStyle())
  }

  shadowRoot.appendChild(container)
  return container
}

window.addEventListener("load", async () => {
  const rootContainer =
    typeof Mount.getRootContainer === "function"
      ? await Mount.getRootContainer()
      : await createShadowContainer()

  const root = createRoot(rootContainer)

  root.render(<MountContainer />)
})

上述模板代码核心内容拆解如下:

  • __plasmo_mount_content_script__ 即为 Content Script UI 的入口文件,在我们静态注入背景下,其为 contents.tsx 或者 contents/<name>.tsx
  • 在静态注入的背景下,在页面 load 事件触发时,将 Content Script UI 渲染到 rootContainer

认识到这一点之后,我们如果想要在运行时注入 Content Script UI,那么只需要改动上述的逻辑即可。

src/injected 文件夹下创建 renderContent.tsx 文件,将上述内容复制进去,然后修改对应的逻辑:

// 待渲染的 Content Script UI 脚本
import * as RawMount from "../contents/content.tsx"
import { createRoot } from "react-dom/client"

// ...

// 将只在 load 事件触发时执行改为可以在注入时动态调用
async function renderContent() {
  const rootContainer =
    typeof Mount.getRootContainer === 'function'
      ? await Mount.getRootContainer()
      : await createShadowContainer();

  const root = createRoot(rootContainer);

  root.render(<MountContainer />);
}

renderContent();

接着我们在 background.ts 里面监听页面 Tab 的激活,在 Tab 激活且已经加载完成的情况下,动态注入 Content Script UI:

import injectedContent from 'url:./injected/renderContent.tsx';

chrome.tabs.onActivated.addListener(activeInfo => {
  const { tabId } = activeInfo;

  chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
    if (
      !tabs?.[0]?.url?.includes('chrome://') &&
      tabs?.[0]?.status === 'complete'
    ) {
      // 这里因为 background 与插件的文件目录在一个目录,所以不需要加 chrome-extensions://url 协议。
      const arr = injectedContent.split('/');
      const res = arr[arr.length - 1];

      chrome.scripting.executeScript({
        target: { tabId: tabs[0]?.id as number },
        files: [res.split('?')[0]],
      });
    }
  });
});

上述在获取到 injectedContent 的路径时还需要经过处理,这里是因为 background 与插件的文件目录在一个目录,所以不需要加 chrome-extensions://xxx 等前缀,只需要类似下面这样的高亮的这一块。

chrome-extension://gjfldhahgbflogekgjigjncfelbdecik/rewrite-ws.8250290b.js?1661074291141

Plasmo 非常智能的一点就是,当我们以非常规后缀名进行文件导入时,会自动将文件及其依赖编译成为单一的 JS 文件,然后插入进来。

如我们在上面的:

import injectedContent from 'url:./injected/renderContent.tsx';

则会将 renderContent.tsx 对应的文件入口,将其所有的依赖树进行分析、打包,成为单一可在浏览器中运行的代码形态。

这里潜在的问题就是,如果有多份类似的 contents.tsx ,如 contents/<name>.tsx ,且每份文件里面是通过 import xx 语句进行导入资源或依赖,Plasmo 依赖的 Parcel 会对每份资源进行一次构建,即可能多份 contents/<name>.tsx 依赖的同一资源会产生多份产出物。

常见的如 contents/a.tsx 里面引用了一个 assets/logo.svgcontents/b.tsx 里面也引用了一个 assets/logo.svg ,那么最终产物里会有两个 assets/logo.svg ,且会生成不同的 hash 名称,如 assets/logo.sasassa.svg

我们将在下一节讨论如何解决这个问题。

Plasmo 支持对 Injected UI 提供各种维度的定制:

  • getMountPoint
  • getStyle
  • 修改 shadow-container 的样式
  • getShadowHostId
  • getRootContainer

Injected UI 挂载到主页面的结构如下:

<div>  <!-- getShadowHostId 改这个元素的 id --> 
  #shadow-root (open)
  <style></style>  <!-- getStyle 与修改 shadow-container 都在这里处理 --> 
  <!-- -->
  <div id="plasmo-shadow-container" style="z-index: 1; position: absolute;">
     <!-- getMountPoint 都在这里处理--> 
    <div id="plasmo-mount-container" style="display: flex; position: relative; top: 0px; left: 0px;"></div>
  </div>
</div>

其中 getRootContainer 则是自己完全重写挂载 Injected UI 的逻辑,如是否需要创建 Shadow DOM、是否使用 getStyle、是否使用 getShadowHostId 等逻辑:

async function createShadowContainer() {
  const container = document.createElement('div');

  container.id = 'plasmo-shadow-container';

  container.style.cssText = `
    z-index: 1;
    position: absolute;
  `;

  const shadowHost = document.createElement('div');

  if (typeof Mount.getShadowHostId === 'function') {
    shadowHost.id = await Mount.getShadowHostId();
  }

  const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
  document.body.insertAdjacentElement('beforebegin', shadowHost);

  if (typeof Mount.getStyle === 'function') {
    shadowRoot.appendChild(await Mount.getStyle());
  }

  shadowRoot.appendChild(container);
  return container;
}

async function renderContent() {
  const rootContainer =
    typeof Mount.getRootContainer === 'function'
      ? await Mount . getRootContainer () 
      : await createShadowContainer();

  const root = createRoot(rootContainer);

  root.render(<MountContainer />);
}

Plasmo 的文件路径那些事

在 Plasmo 的运行时下,为我们提供了几种资源使用形式[40]:

  • ~
  • url:
  • data-base64:
  • data-text:
  • chrome.runtime.getURL

【~】[41]

当在源代码模块之外使用时,或在 data-base64data-texturlscheme 使用场景下时,~ 总是表示项目的根目录,也就是 package.json 所存在的那个目录,通常被使用在如下场景:

  • package.json 对应的 manifest 字段中写 ~rulesets/test.json 时,代表 /rulesets/test.json
// package.json
{
  "manifest": {
    "action": {
        "default_icon": {
          "16": "~rulesets/icon16.png",
        },
        "default_popup": "popup.f4f22924.html"
      },
  }
}
  • 使用在 data-base64:~assets/image.png 中时,代表 /assets/image.png
  • 使用在 url:~src/code.js 中时,代表 /src/code.js

~ 用于在一份源代码,如 tstsx 文件中,导入另外一份源代码,(tstsx 文件),它代表两层含义:

  • 如果是默认设置,~ 代表项目根目录
  • 如果设置了 src 目录配置,~ 代表 src 目录,所以 ~core/code-module.tsx 实际上是 /src/core/code-module.tsx 目录

url:

url:scheme 用于从 web-accessible resources 加载资源,例子如下:

import myJavascriptFile from "url:./path/to/my/file/something.js"

上述 something.js 会被编译,然后自动加到 manifest.json 对应的 web_accessible_resources 字段中。

这里着重标出了会被编译,是一把双刃剑,在大部分场景下,我们也需要文件不被编译也能使用,这会在 chrome.runtime.getURL 提到。

【data-base64: 】[42]

将资源通过 base64 的方式内联在源代码里:

import someCoolImage from "data-base64:~assets/some-cool-image.png"

<img src={someCoolImage} alt="Some pretty cool image" />

【data-text:】[43]

以普通文本的方式加载内容,如加载 CSS 样式:

import cssText from "data-text:~/contents/plasmo-overlay.css"

export const getStyle = () => {
  const style = document.createElement("style")
  style.textContent = cssText
  return style
}

如果导入的是 .scss.less 等,Plasmo 会对内容进行编译,编译成普通的 CSS 使用。

【chrome.runtime.getURL】[44]

package.jsonmanifest 字段声明的资源会自动被复制到 Build 目录,并且不会被编译,在代码里可以通过 chrome.runtime.getURL 对这些资源进行引用:

// package.json
{
  "manifest": {
    "web_accessible_resources": [
      {
        "resources": [
          "~raw.js",
          "assets/pic*.png",
          "resources/test.json"
        ],
        "matches": [
          "<all_urls>"
        ]
      }
    ]
  }
}

上述的资源会被构建到插件里:

  • raw.js 是存在于 package.json 所在的项目目录
  • 在项目根目录下任何匹配 assets/pic*.png 的文件
  • 相对项目根目录下的 resources/test.json 文件

除此之外,Plasmo 还支持从 node_modules 导入的文件:

// package.json
{
  "manifest": {
    "web_accessible_resources": [
      {
        "resources": [
          "~raw.js",
          // ...
          "@inboxsdk/core/pageWorld.js",
          "@inboxsdk/core/background.js"
        ],
        "matches": [
          "<all_urls>"
        ]
      }
    ]
  }
}

上述 node_moudles 下的文件也会被打包到插件 Build 目录下,可以通过 chrome.runtime.getURL 引用,且文件不会被编译。

运行时将 Content Script 注入到 Main World

参考:https://docs.plasmo.com/workflows/content-scripts#injecting-into-the-main-world

Content Script 实际是运行和主页面脚本隔离的环境里,与主页面脚本共享 DOM,但是不共享作用域,比如在 Content Script 修改 window 对象是不生效的,也无法获取到主页面脚本的 window 对象,然而 Chrome 提供了 chrome.scripting.executeScript 来给主页面脚本注入 Content Scripts,使得注入的脚本可以操作 window

 chrome.scripting.executeScript(
    {
      target: {
        tabId // the tab you want to inject into
      },
      world: "MAIN", // MAIN to access the window object
      func: windowChanger // function to inject
      // or
      files: ['contents/app.js']
    },
    () => {
      console.log("Background script got callback after injection")
    }
  )
}

上述 Runtime Injected UI 中我们在注入 Content Script UI 时没有指明 world: MAIN 代表注入在 Content Script 仍然与主页面脚本隔离:

import injectedContent from 'url:./injected/renderContent.tsx';

chrome.tabs.onActivated.addListener(activeInfo => {
  const { tabId } = activeInfo;

  chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
    if (
      !tabs?.[0]?.url?.includes('chrome://') &&
      tabs?.[0]?.status === 'complete'
    ) {
      // 这里因为 background 与插件的文件目录在一个目录,所以不需要加 chrome-extensions://url 协议。
      const arr = injectedContent.split('/');
      const res = arr[arr.length - 1];

      chrome.scripting.executeScript({
        target: { tabId: tabs[0]?.id as number },
        files: [res.split('?')[0]],
      });
    }
  });
});

运行时处理 Action Button

需要处理这段逻辑是因为我们的插件期望是在 Popup Page UI 里面点击开始录制之后,如果没有结束录制,那么此时再次点击插件的 Action 按钮期望是变成处理暂停与继续录制的功能,而并非继续打开 Popup Page UI。

参考 chrome.action.setPopup/openPopup,发现插件不支持动态设置点击 Action Button 是打开 Popup Page UI 或不打开 Popup Page UI,所以无法运行时设置 Popup Page UI 的显影。

一个可行的思路,参考 Tango:

image.png

其实 Action Button 并没有设置 Popup Page UI,而是作为一个控制按钮:

1 . 如果此时不处于录制中,点击 Action Button 就触发录制界面的 Content Script UI

2 . 如果此时处于录制中,点击 Action Button 就处理暂停逻辑

// background.ts

chrome.action.onClicked.addListener(tab => {
  if (isRecording) { // open 录制页面 }
  else { // 处理暂停逻辑 }
});

除此之外,还可以设置 BadgeBackground、BadgeText、Icon、Title、Popup。

开发实践心得

经过阶段性业务实践,目前有一定的积极性可以评估 Plasmo 适合作为插件开发长期演进方案。

但同时需要了解到,当你遇到问题时,考虑如下几种解决方案。

遇事不决:

  • 多看 Chrome 插件的 API 文档[45]
  • 多看 Plasmo 的官方文档[46]
  • 多看 Chrome 插件的例子[47]
  • 多看 Plasmo 的例子[48]
  • 去 Plasmo 的 Discord 社区[49]提问

当然,必要时可以啃一下 Plasmo Framework 的源码[50] :

参考资料

[1]Github 地址: https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/mv2-archive/api/bookmarks/basic

[2]动态改 Favicon: https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/mv2-archive/api/browserAction/set_icon_path

[3]页面背景颜色: https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/mv2-archive/api/browserAction/make_page_red

[4]添加右键菜单栏: https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/mv2-archive/api/contextMenus/global_context_search

[5]注入脚本: https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/mv2-archive/api/browserAction/print

[6]Tab 折叠: https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/mv2-archive/api/default_command_override

[7]新 Tab 展示页面重载: https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/mv2-archive/api/override/blank_ntp

[8]Github 地址: https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/mv2-archive/api/browsingData/basic

[9]浏览器历史页面重载: https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/mv2-archive/api/history/historyOverride

[10]Github 地址: https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/mv2-archive/api/commands

[11]处理 Cookie: https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/mv2-archive/api/cookies

[12]处理 HTTP Headers: https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/mv2-archive/api/debugger/live-headers

[13]处理 JS 执行、暂停: https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/mv2-archive/api/debugger/pause-resume

[14]操作 Element 面板信息: https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/mv2-archive/api/devtools/panels/chrome-query

[15]Github 地址: https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/mv2-archive/api/notifications

[16]处理 OmniBox: https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/mv2-archive/api/omnibox/newtab_search

[17]clipboard: https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/apps#_feature_clipboard

[18]fileSystem: https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/apps#_feature_fileSystem

[19]storage: https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/apps#_feature_storage

[20]Github 地址: https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/apps/samples/diff

[21]fileSystem: https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/apps#_feature_fileSystem

[22]storage: https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/apps#_feature_storage

[23]Github 地址: https://github.com/GoogleChrome/chrome-extensions-samples/tree/master/apps/samples/filesystem-access

[24]identity: https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/apps#_feature_identity

[25]Github 地址: https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/apps/samples/github-auth

[26]chrome.fileSystem: http://developer.chrome.com/apps/fileSystem.html

[27]Runtime: http://developer.chrome.com/apps/app.runtime.html

[28]Window: http://developer.chrome.com/apps/app.window.html

[29]Github 地址: https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/apps/samples/mini-code-edit

[30]代码编辑器: https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/apps/samples/text-editor

[31]fileSystem: https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/apps#_feature_fileSystem

[32]storage: https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/apps#_feature_storage

[33]Github 地址: https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/apps/samples/image-edit

[34]Notification API documentation: http://developer.chrome.com/apps/notifications.html

[35]Github 地址: https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/apps/samples/rich-notifications

[36]Plasmo Framework: https://www.plasmo.com/

[37]Turborepo: https://turborepo.org/

[38]使用 React + TypeScript 开发 UI: https://blog.plasmo.com/p/content-scripts-ui

[39]Shadow DOM: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM

[40]资源使用形式: https://docs.plasmo.com/workflows/faq#tilde-import-resolution

[41]【~】: https://docs.plasmo.com/workflows/faq#tilde-import-resolution

[42]【data-base64: 】: https://docs.plasmo.com/workflows/assets#importing-image-assets-inline

[43]【data-text:】: https://docs.plasmo.com/workflows/content-scripts-ui#getstyle

[44]【chrome.runtime.getURL】: https://github.com/PlasmoHQ/examples/tree/main/with-web-accessible-resources

[45]API 文档: https://developer.chrome.com/docs/extensions/reference/

[46]官方文档: https://docs.plasmo.com/

[47]例子: https://github.com/GoogleChrome/chrome-extensions-samples

[48]例子: https://github.com/PlasmoHQ/examples/tree/0cdf4d3608b574fffe6e662dfe1e2325ef109d0d

[49]Discord 社区: https://discord.com/invite/8rrxVYYtfd

[50]Plasmo Framework 的源码: https://github.com/PlasmoHQ/plasmo

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/VKpHtRVIJh669SLz2nUJuQ

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237236次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8071次阅读
 目录