插件构建之plasma

246 阅读7分钟

过去一年,开发了两款插件并上架谷歌商店,在最初技术调研时原本想使用plasma,考虑插件包的体积与其他未知原因,最终我还是选择了webpack5搭建了一个基础的chrome插件,具体可参考之前写的一篇文章#放弃plasmo,webpack5搭建了一个chrome基础插件,因为原生的插件配置也非常简单。通常的插件api使用也踩了不少坑,但从新回顾,发现plasma解决了我当初插件业务开发中的很多问题,也真正做到了让开发者只关注业务本身就行。

初始化一个插件项目

按照官方教程,初始化一个插件非常简单,使用一下命令就行,按照指定命令提示,快速初始化了一个插件项目

pnpm create plasmo

我们看到初始化后的项目是下面这样的

1.png 我们发现这是一个原始的构建插件工程项目,当我们执行pnpm run dev时,会生成一个build文件夹,我们只需要打开chrome插件的开发者模式,添加这个build后的文件夹就行

2.png

此时我们加载完插件后,popup.html插件就是这样的

3.png

我们修改popup.tsx的任何一行代码时,此时会热更新到插件,无需重新加载插件,这是我之前使用webpack5构建插件未解决的问题,因为我们次修改后,需要build,重新加载,才能生效,这种体验有点糟糕。但是plasmo就完美解决插件热更新问题

调整项目文件夹名

我们看到初始化项目根目录的popup.tsx就是我们插件打开的popup页面,但是可以在根目录下新建一个popup文件夹

// popup/index.tsx
import { useState } from "react"

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

  return (
    <div>
      <h1>公众号:Web技术学苑</h1>
    </div>
  )
}

export default IndexPopup

3.png

在初始化后的文件夹,我们可以使用src来组织我们的插件,具体可以参考src,从扩展页面中可以发现,在插件中的一些页面可以组织成以下

# contents/index.tsx
# popup/index.tsx
# options/index.tsx
# newtab/index.tsx
# sidepanel/index.tsx
# devtools/index.tsx

我们发现popup页面的样式如何控制

css module

我们看下popup页面,我们引入的scss 对应的index.module.scss

.app {
  width: 360px;
  height: 600px;
}

popup

import { useState } from "react"
import style from "./index.module.scss"

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

  return (
    <div className={style["app"]}>
      <h1>公众号:Web技术学苑</h1>
    </div>
  )
}

export default IndexPopup

看下页面结果

5.png 我们会发现plasma天然支持css module,真正加载插件的时候,会把scss编译成css

contents

插件中,popup是插件的一个气泡页面,90%的插件都会有这个气泡,但是我们也会发现一些安装的插件会改变我们浏览器网页的内容,为什么会改变我们浏览网页的内容呢,真正影响的当前页面布局的是contents

如何在网站插入内容?我们知道插件的content.js是可以获取到当前网页的浏览器内容的,也就是说可以操作当前网页的dom,你可以理解成加载当前网页后,chrome插件给开发者开了一个黑盒,开发者只要用户安装了这个插件,我就可以改变当前页面的dom

index.module.scss

.app {
  width: 100%;
  text-align: center;
  padding: 10px 0;
  color:red;
}

// contents/index.tsx
import React, { memo } from "react"

import style from "./index.module.scss"

interface Props {}

const Index: React.FC<Props> = (props) => {
  const {} = props
  return <div className={style["app"]}>hello,欢迎关注公众号Web技术学苑</div>
}

export default memo(Index)

我们发现css没有作用,但是页面内容已经插入的当前网页的html中

6.png 我们首页会发现plasma会创建一个plasmo-csuiwebComponent,而且插入到html的根节点上,且样式不生效,那如何使得样式生效呢

导出默认getStyle

// contents/index.tsx
import type { PlasmoGetStyle } from "plasmo"
import React, { memo } from "react"
// 引入默认scss文件
import styleText from "data-text:./index.module.scss"
import style from "./index.module.scss"
// 引入默认样式
export const getStyle: PlasmoGetStyle = () => {
  const style = document.createElement("style")
  style.textContent = styleText
  return style
}
interface Props {}

const Index: React.FC<Props> = (props) => {
  const {} = props
  return <div className={style["app"]}>hello,欢迎关注公众号Web技术学苑</div>
}

export default memo(Index)

7.png

所以样式就生效了,我们发现在contents引入的cssmodule并不会像在popup一样,而是需要getStyle这样的接口,动态插入的style

如何在指定域名中生效,现在默认是所有网站都会生效,因此我想指定网址才生效呢,我们需要导出config即可,并配置matches指定域名,然后重新运行项目即可

// contents/index.tsx
export const config: PlasmoCSConfig = {
  matches: ["https://www.baidu.com/*"],
  all_frames: true
}
...

如何插入对应页面节点上

我们发现以上的webComponent是插入在html上的,在通常情况下,有可能实际业务中会遇到插入到页面的某个节点上,所以如何将content的内容插入到节点上

  • 主要是要导出getOverlayAnchor,然后绑定页面具体的节点
// contents/overlayAnchor.tsx
import type {
  PlasmoCSConfig,
  PlasmoCSUIProps,
  PlasmoGetOverlayAnchor
} from "plasmo"
import React from "react"

export const config: PlasmoCSConfig = {
  matches: ["https://www.baidu.com/*"],
  all_frames: true
}
export const getOverlayAnchor: PlasmoGetOverlayAnchor = () =>
  document.querySelector(".quickdelete-wrap")

const ContentForQuick = () => (
  <span
    style={{
      borderRadius: 4,
      background: "red",
      height: "44px",
      display: "flex",
      alignItems: "center"
    }}>
    公众号:Web技术学苑
  </span>
)

export default ContentForQuick

8.png 以上就是达到我想要的目标了,不过插入的内容依旧是webCompoent

options

通常来讲这可能是插件内部的设置页面,我们看下如何在popup中或者content中如何打开插件中内部的页面

// options/index.tsx
import React, { memo } from "react"
interface Props {}
const Set: React.FC<Props> = (props) => {
  const {} = props
  return <div>我是设置页面</div>
}

export default memo(Set)

我们在popup弹出窗口中打开

...
function IndexPopup() {
  const [data, setData] = useState("")
  const handleToOptionsPage = () => {
    chrome.runtime.openOptionsPage()
  }
  return (
    <div className={style["app"]}>
      <h1>公众号:Web技术学苑</h1>
      <div onClick={handleToOptionsPage}>go to option page</div>
    </div>
  )
}

9.png 因此options页面就在弹框页面中打开了一个新的页面

newtab页面

这个页面会默认覆盖你当前默认打开的tab页面,你只需要在根目录新建newtab/index.tsx即可

// newtab/index.tsx
import React, { memo } from "react"

interface Props {}

const TabsPge: React.FC<Props> = (props) => {
  const {} = props
  return <div>new tabs page</div>
}

export default memo(TabsPge)

当我们每新开一个tab时,默认就会插件的tab

10.png

tabs

我们插件内部也可以有很多内部的页面,因此,你可以在根目录新建一个tabs目录,然后新建一个about.tsx页面

// tabs/about.tsx
import React, { memo } from "react"
interface Props {}
const About: React.FC<Props> = (props) => {
  const {} = props
  return <div>about page</div>
}

export default memo(About)

我们可以在popup弹框页面打开一个页面

import { useState } from "react"
import style from "./index.module.scss"
function IndexPopup() {
  const [data, setData] = useState("")
  const handleToOptionsPage = () => {
    chrome.runtime.openOptionsPage()
  }
  const handleToNewTabPage = () => {
    chrome.tabs.create({
      url: `chrome-extension://${chrome.runtime.id}/newtab.html`
    })
  }
  const handleToAboutPage = () => {
    chrome.tabs.create({
      url: `chrome-extension://${chrome.runtime.id}/tabs/about.html`
    })
  }
  return (
    <div className={style["app"]}>
      <h1>公众号:Web技术学苑</h1>
      <div onClick={handleToOptionsPage}>go to option page</div>
      <div onClick={handleToNewTabPage}>go to tab page</div>
      <div onClick={handleToAboutPage}>go to about page</div>
    </div>
  )
}
export default IndexPopup

sidepanel

这是chrome插件的一个新方式,可以在当前窗口打开侧边栏方式打开插件内容

import React, { memo } from "react"

interface Props {}

const Index: React.FC<Props> = (props) => {
  const {} = props
  return <div>this is sidepanel</div>
}

export default memo(Index)

打开sidepanel,主要在background.js中打开sidepanel

// background.js
chrome.action.onClicked.addListener(() => 
{ 
   chrome.sidePanel.setOptions(
   {   path: "sidepanel.html",
       enabled: true,
       openPanel: true
    }).catch((error) => console.error("Failed to open side panel:", error)); 
 });

总结

  • 主要介绍了plasma构建插件的几个核心文件,比如background.jscontentsoptionstabs等插件页面
  • 如何在content.js中使用cssModule并插入相对指定节点
  • code example