前言

每当看到好的文章或者好的视频,底下总有那么一些长得好看,说话有好听的人才。没有文化的我只能默默的留下不争气的“卧槽,牛逼!

同样是腰间盘,为何汝如此突出。同样九年义务教育,为何汝如此优秀。痛定思痛,我决定要向他们学习。于是秉着收藏即学会的原则,我要用 React & Antd & Vite 快速做一个 chrome 扩展程序(常用名:插件) – 语录收藏。大体的功能是可以选中一段话,右键可以保存到个人语录中。点击输入框会将所有保存的语录弹窗显示,可供我们快捷输入。点击插件会随机显示一段心灵鸡汤,可以随时补充语录。

安装

yarn create vite my-extension --template react-ts

我们先创建一个 react-ts 的工程

yarn add antd

接着安装完 antd之后,我们就可以开始下面的工作了

改造多页面配置

根据 Vite 官方文档 把它改造成一个多页面的工程。多页面的 vite.config.ts 如下:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path, { resolve } from "path";
import makeManifest from "./utils/plugins/make-manifest";
import customDynamicImport from './utils/plugins/custom-dynamic-import';
import vitePluginImp from 'vite-plugin-imp'const root = resolve(__dirname, "src");
const pagesDir = resolve(root, "pages");
const assetsDir = resolve(root, "assets");
const publicDir = resolve(__dirname, "public");
const outDir = resolve(__dirname, "dist");const isDev = process.env.__DEV__ === "true";// https://vitejs.dev/config/
export default defineConfig({resolve: {alias: {"@src": root,"@assets": assetsDir,"@pages": pagesDir,},},plugins: [react(), makeManifest(), customDynamicImport(),// 按需加载配置vitePluginImp({libList: [{libName: "antd",style: (name) => `antd/es/${name}/style`,},],}),],publicDir,build: {outDir,sourcemap: isDev,rollupOptions: {input: {popup: resolve(pagesDir, "popup", "index.html"),options: resolve(pagesDir, "options", "index.html"),background: resolve(pagesDir, "background", "index.ts"),// content 需要在 manifest 中指定 css 资源content: resolve(pagesDir, "content", "index.ts"),contentStyle: resolve(pagesDir, "content", "style.less"),},output: {entryFileNames: "src/pages/[name]/index.js",chunkFileNames: isDev? "assets/js/[name].js": "assets/js/[name].[hash].js",assetFileNames: (assetInfo: {name: string | undefined;source: string | Uint8Array;type: 'asset';}) => {const { dir, name: _name } = path.parse(assetInfo.name || '');const assetFolder = getLastElement(dir.split("/"));const name = assetFolder + firstUpperCase(_name);return `assets/[ext]/${name}.chunk.[ext]`;},},},},css: {preprocessorOptions: {less: {javascriptEnabled: true,modifyVars: {'@primary-color': '#1e80ff',  // 设置 antd 主题色},},}},
});function getLastElement<T>(array: ArrayLike<T>): T {const length = array.length;const lastIndex = length - 1;return array[lastIndex];
}function firstUpperCase(str: string) {const firstAlphabet = new RegExp(/( |^)[a-z]/, "g");return str.toLowerCase().replace(firstAlphabet, (L) => L.toUpperCase());
}

这里有几个点需要注意下:

  • 通过 vite-plugin-imp 按需加载 antd
  • contentStyle 为 Content script 内容脚本(下文会介绍)指定的样式,需要单独指定
  • make-manifest、custom-dynamic-import 这两个自定义插件分别是为了处理 manifest 和 content 动态导入

Nodemon 自动更新

为了方便我们快捷开发,我们安装一下 nodemon 自动更新

yarn global add nodemon
或者
yarn add nodemon --dev

添加 nodemon.json 配置文件

{"env": {"__DEV__": "true"},"watch": ["src", "utils", "vite.config.ts"],"ext": "tsx,css,scss,html,ts","ignore": ["src/**/*.spec.ts"],"exec": "node_modules/.bin/vite build"
}

至此,脚手架相关配置搞定了,简简单单,接下来我们要开始插件相关的工作了。

组成结构

首先简单了解一下插件的整体结构,因 V2 版本即将过期,我们直接用 V3 的版本。

插件的组成结构取决于它的功能,但是所有扩展都必须有一个 manifest 的清单。以下是插件包含的所有模块:

  • Manifest:向浏览器提供关于插件的信息,例如可能使用的功能和图标、执行脚本文件等重要的文件。
  • Service worker:插件事件处理程序,包含了浏览器事件的监听器。可以访问所有的 Chrome api:实现跨域请求、网页截屏、弹出 chrome 通知消息等功能,前提是要在 manifest.json 中声明了所需的权限。
  • Toolbar icon:浏览器工具栏上显示的插件图标。用户可以单击图标与一个使用弹出框进行交互。
  • UI elements:用户交互的元素,包括:上面说的点击图标和弹窗、右键菜单、地址栏搜索选择插件、快捷键唤起等,甚至还可以在页面中插入自定义组件。
  • Content script:内容脚本允许插件将逻辑注入页面,以读取和修改其内容。 内容脚本可以在已加载到浏览器中的页面上下文中执行的 JavaScript,例如上面说的在页面中插入自定义组件。
  • Options page:顾名思义,就是插件的配置页面,可以对插件进行一些配置操作。

Manifest

官方要求的是 manifest.json 文件,这里先用 js 来代替,编译阶段再转换文件格式。本次开发的配置如下:

import packageJson from "../package.json";
import { ManifestType } from "@src/manifest-type";const manifest: ManifestType = {manifest_version: 3,name: packageJson.name,version: packageJson.version,  // 当前插件版本description: packageJson.description,icons: { // 不同尺寸使用场景不同,"16": "icon16.png","32": "icon32.png","48": "icon48.png","128": "icon128.png"},background: { service_worker: "src/pages/background/index.js" },action: {default_popup: "src/pages/popup/index.html",default_icon: {"16": "icon16.png","32": "icon32.png","48": "icon48.png","128": "icon128.png"}},content_scripts: [{matches: ["<all_urls>"],js: ["src/pages/content/index.js"],// content 样式需要特殊指定,若使用 antd,需要另外添加 antd 部分样式// css: ["assets/css/contentStyle.chunk.css"],},],options_page: "src/pages/options/index.html",web_accessible_resources: [{resources: ["assets/js/*.js","assets/css/*.css",],matches: ["*://*/*"],},],permissions: [   // 操作 chrome 的权限"storage","activeTab","scripting","contextMenus","notifications",],
};export default manifest;

UI elements

上面说到 UI 交互大多数就是,点击插件图标弹窗、右键菜单、在页面中插入自定义组件等等。没错,小孩子才做选择,这几种我全要。

Content

因 chrome 插件不支持在 content scripts 中使用 module,这里我们使用动态引入。

// src/pages/content/index.ts/*** @description* chrome 插件不支持在 content scripts 中使用 module*/
import("./components/Content");
// src/pages/content/components/Content/index.tsximport { createRoot } from "react-dom/client";
import App from "@src/pages/content/components/Content/app";const root = document.createElement("div");
root.id = "content-view-root";
document.body.append(root);createRoot(root).render(<App />);

以下逻辑简单概括起来就是:

1.监听用户聚焦输入框,从 storage 中获取语录集数据,创建一个选择框提供用户选择快捷输入。
2.监听用户选中文本,调用 sendMessage 向 service worker 发送请求,缓存选中内容。右键可以加入选中内容。

// src/pages/content/components/Content/app.tsximport { useEffect, useRef, useState } from "react";
import { createPopper } from '@popperjs/core/lib/popper-lite.js';
import preventOverflow from '@popperjs/core/lib/modifiers/preventOverflow.js';export default function App() {const focusTargetRef = useRef<any>();const toolTargetRef = useRef<any>();const [sentences, setSentences] = useState([]);useEffect(() => {// capture: true 聚焦事件不会冒泡,但是可以在捕获阶段触发document.body.addEventListener('focus', handleFocus, true)document.body.addEventListener('blur', handleBlur, true)// 监听文字选中document.addEventListener("selectionchange", handleSelectionChange)return () => {document.body.removeEventListener('focus', handleFocus, true)document.body.removeEventListener('blur', handleBlur, true)document.removeEventListener("selectionchange", handleSelectionChange)}}, []);function handleFocus(event: any) {// 只有可编辑元素才弹窗const target = event?.targetif(target?.isContentEditable || target?.tagName === 'INPUT' || target?.tagName === 'TEXTAREA') {chrome.storage.sync.get("sentences", ({ sentences }) => {if (!sentences || !sentences?.length) {return}setSentences(sentences)});focusTargetRef.current = targetconst popperInstance = createPopper(target as HTMLElement, toolTargetRef.current, {// 省略配置});toolTargetRef.current.setAttribute('data-show', '');popperInstance.update();}}function handleBlur() {setTimeout(() => {toolTargetRef.current.removeAttribute('data-show');}, 300)}function handleSelectionChange() {// 获取选中文本chrome?.runtime?.sendMessage({ action: 'add', data: document.getSelection()?.toString() });}function handleInput(info: string) {const target = focusTargetRef?.currentif(target?.isContentEditable) {focusTargetRef.current.innerText = info} else if(target?.tagName === 'INPUT' || target?.tagName === 'TEXTAREA') {focusTargetRef.current.value = info}// 自定义触发输入事件const event = new Event('input', { bubbles: false, cancelable: false })focusTargetRef.current.dispatchEvent(event);}return (<><div id="tooltip" ref={toolTargetRef}>{ sentences?.map((sentence, index) => (<div className='sentence-item' key={index} onClick={() => handleInput(sentence)}>{sentence}</div>)) }</div></>);
}

Popup

Popup 页面是用户点击插件图标以后出现的弹窗,我们定一个 html 模版页面。

<!-- src/pages/popup/index.html --><!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8" /><link rel="icon" type="image/svg+xml" href="/react.svg" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Popup</title></head><body><div id="root"></div><script type="module" src="./index.tsx"></script></body>
</html>

接着引入组件相关页面。

// src/pages/popup/index.tsximport React from "react";
import { createRoot } from "react-dom/client";
import Popup from "@pages/popup/Popup";
import "@pages/popup/index.less";function init() {const root = document.querySelector("#root");if (!root) {throw new Error("Can not find root");}createRoot(root).render(<Popup />);
}init();

Popup 组件页面功能也比较简单,主要调用接口获取数据展示、刷新、加入语录集等等。

// src/pages/popup/Popup.tsximport React, { useEffect, useState } from "react";
import "@pages/popup/Popup.less";
import { Card } from 'antd';
import { CopyOutlined, ReloadOutlined, FileAddOutlined } from '@ant-design/icons';
import { CopyToClipboard } from 'react-copy-to-clipboard'const Popup = () => {const [sentence, setSentence] = useState('');useEffect(() => {getSentence()}, []);async function getSentence() {const res = await fetch('https://api.oick.cn/dutang/api.php', {method: 'GET'})const nextSentence = await res.json()setSentence(nextSentence)}const handleAdd = () => {chrome.storage.sync.get("sentences", ({ sentences = [] }) => {chrome.storage.sync.set({ sentences: [sentence, ...sentences] });});}const handleReload = () => {getSentence()}return (<div className="popup-container"><Card style={{ width: 300 }}actions={[<FileAddOutlined key='add' onClick={handleAdd}/>,<CopyToClipboard key="copy" text={sentence}><CopyOutlined/></CopyToClipboard>,<ReloadOutlined key="reload" onClick={handleReload}/>,]}><p style={{ minHeight: 60 }}>{ sentence }</p></Card></div>);
};export default Popup;

Options

Optinos 页面与 Popup 页面的代码类似就不再赘述,此页面可以通过右键插件图标 – 选择“选项”进入,页面支持复制、编辑、删除、新增等等,UI 展示如下:

Service worker

这部分我们主要用到了插件的 contextMenus 右键菜单、storage 存储数据、notifications 通知等功能,这些功能我们都需要在 Manifest 里配置。

// src/pages/background/index.tslet selectedSentence = ''chrome.runtime.onInstalled.addListener(() => {// 右键菜单管理chrome.contextMenus.create({"id": "0","type" : "normal","title" : "新增语录",contexts: ['selection'],});
});chrome.contextMenus.onClicked.addListener(() => {addSentence()
} )function addSentence() {chrome.storage.sync.get("sentences", ({ sentences = [] }) => {chrome.storage.sync.set({ sentences: [selectedSentence, ...sentences] });showNotification()});
}chrome.runtime.onMessage.addListener((request) => {const { data, action } = request;if (action === 'add') {selectedSentence = data}});function showNotification() {chrome.notifications.create({type: 'basic',iconUrl: './images/icon.png',title: '',message: '操作成功',priority: 0,});
}

加载与调试

加载本地插件

存放清单文件的目录可以在开发者模式下添加为插件,操作步骤如下:

1.浏览器输入 chrome://extensions 可以打开插件管理页面* 另外,可以点击右上角插件管理的图标,在弹窗菜单底部选择管理扩展程序。* 另外,还可以点击右上角设置按钮,在弹窗菜单中选择更多工具–扩展程序。
2.通过点击开发者模式旁边的开关来启用开发人员模式。
3.最后点击左上角加载已解压的扩展程序选择编译好的目录即可成功加载。

调试模式

我们修改了代码以后,nodemon 自动更新以后,我们需要再 对于 background 的调试,可以在插件管理页面上点击 Service Worker

模块间通信

组件的 backgroundpopupcontent 三者之间关系图如下:

  • content script 与 service worker / popup
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {sendResponse(data);
});const getCurrentTab = async () => {let queryOptions = {active: true, currentWindow: true};let [tab] = await chrome.tabs.query(queryOptions);return tab;
};await chrome.tabs.sendMessage(tab.id, data);
  • popup 与 service worker
chrome.runtime.sendMessage(data, (response) => {console.log(response)
})chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {sendResponse(data);
});

打包发布

发布脚本主要做了以下几个操作:

  • 版本号更新* 使用 node-semver 升级版本号* 使用 sed 命令修改文件,node fs 也行
  • 构建完了之后要压缩整个 dist,必须要压缩才能发布到 Chrome Extension Store
// build.mjs#!/usr/bin/env zxconst semverInc = require('semver/functions/inc')
const packageJson = require('./package.json');// let {version} = await fs.readJson('./package.json')console.log(chalk.yellow.bold(`Current verion: ${packageJson.version}`)
)let types = ['major', 'minor', 'patch']
let type = await question(chalk.cyan('Release type? Press Tab twice for suggestion \n'),{choices: types,}
)
let version = ''
if (type !== '' || types.includes(type)) {version = semverInc(packageJson.version, type)console.log(chalk.green.bold(`Release verion? ${version}`))// 使用 sed 命令修改 version,用 node fs 修改也行$`sed -i '' s/${packageJson.version}/${version}/g package.json`
} else {await $`exit 1`
}// 构建
await $`tsc && vite build`// git
await $`git add .`
await $`git commit -m 'Update version to ${version}'`
await $`git tag v${version}`
await $`git push origin refs/tags/v${version}`
await $`git push origin HEAD:refs/for/master`// 压缩
await $`zip -q -r bundle.zip ./dist`
  • Chrome 应用商店 - 开发者协议 开发者需要交纳 5美元,才可以发布代码到 Chrome Extension Store

结束语

仓库地址 至此一个简单的浏览器插件就开发完成了,大伙有什么疑问的话可以留言相互探讨一下。

最后

最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

前端基于excljs导出xlsx时图片资源的处理及踩坑实录相关推荐

  1. EasyExcel导出xlsx时,某一列的数据为空

    问题:EasyExcel导出xlsx时,某一列的数据为空 问题来源:字段的命名在读取时,出现了问题 这个字段,我们可以debug一下, 有的时候会变成bid ,这个时候就会导致导出字段为空 解决办法: ...

  2. JAVA发布栅格图层_基于 WebGL实现自定义栅格图层踩坑实录

    以下内容转载自totoro的文章<WebGL-Y轴翻转踩坑实录> 作者:totoro 链接:blog.totoroxiao.com/webgl-flipY- 来源:blog.totorox ...

  3. springCloud项目不能向EurekaServer 注册多个EurekaClient时(端口不一致)方法及踩坑经历

    spring cloud 问题说明:springCloud项目不能向EurekaServer 注册多个EurekaClient时(端口不一致)方法及踩坑经历: 前提--->已经能够通过Eurek ...

  4. 基于xdocreport导出复杂word文档,专业避坑指南

    如果你要先问我为什么要导出word?那么请你走开,你个杠精! 在完成这个功能时花费了大量的时间查阅资料,发现能满足导出复杂word文档的工具只有xdocreport,如果有其他的工具欢迎分享.废话不多 ...

  5. 荣耀8 基于官方8.0系统 刷xposed,面具 trwp踩坑

    1.首先刷入recovery要换一个支持8.0 的recovery,然后是刷入命令也变化了, 2.华为助手刷机会释放一个刷机包到电脑某个目录,这个文件以后有用的. trwp开启mtp模式 之后拷贝一个 ...

  6. 前端自动化测试之多浏览器兼容测试平台F2etest全面踩坑记录

    PPT更详尽:F2etest兼容性平台&UIrecorder脚本录制回放 本文参考:http://shaofan.org/f2etest/,https://www.jianshu.com/p/ ...

  7. python连接plc_Python与PLC踩坑实录:成功解决西门子 PLC S7-200_SMART与PC连接时不能同时用Python的snap7包和step7软件同时连接...

    解决西门子 PLC S7-200_SMART与PC连接时不能同时用Python的snap7包和step7软件同时连接 问题描述 在与西门子 PLC(型号S7-200_SMART)进行Python编程操 ...

  8. 服务器导出表格无法打开php,phpSpreadsheet导出xlsx无法打开的解决办法

    PhpSpreadsheet是什么?一句话解释就是:一个用纯 PHP 来实现读取.写入电子表格文件(xls\xlsx等)的PHP库. 为什么不用PHPExcel? 也就是一句话:PHPExcel太故老 ...

  9. SpringBoot+Vue+Itext实现前端请求文件流的方式导出PDF时在指定位置添加照片

    场景 SpringBoot+Vue+Itext实现前端请求文件流的方式下载PDF: SpringBoot+Vue+Itext实现前端请求文件流的方式下载PDF_BADAO_LIUMANG_QIZHI的 ...

最新文章

  1. 如何定位并优化慢查询Sql
  2. au加载默认的输入和输出设备失败_Mac OS X的音频输入输出时如何调整音量
  3. 推荐一些网站给大家[转]
  4. gb2312 requests乱码_不要相信requests返回的text
  5. java创建具体时间点_java单例饿汉模式对象创建时间点疑问
  6. WebPart的Web部件页部署时发生错误--小窍门
  7. OC开发笔记之第二篇
  8. oracle .ctl 是什么文件_Oracle误删dual表怎么办?这里教你怎么恢复
  9. 如何提高VFP应用软件的路径适应性
  10. Dede 删除文档同时文章中的图片的方法
  11. Veeam创建复制任务Replication Job
  12. DCTDAO将于3月27日在TrustSwap发行代币DCTD
  13. 在外面旅游,手机用电怎么解决?
  14. OpenResty Codis集群缓存系统
  15. 使用Redis实现高并发分布式序列号生成服务
  16. mvc 调试 f12 浏览器闪退
  17. 日常一记(11)--word公式输入任意矩阵
  18. python mock server_五、python MOCK SERVER
  19. wed是什么意思在计算机应用基础中,卡西欧wed什么意思
  20. 成功帮我拿3家大厂offer(阿里、美团、虾皮),这份Java面试宝典,简直神了

热门文章

  1. Excel制作动态图表
  2. R 两组样本t检验 wilcoxon检验、卡方、fisher精确检验
  3. 联想笔记本屏幕扩展快捷键用不了
  4. linux抓包查对方的mac地址,1.根据MAC地址抓包
  5. python3中使用pip3错误syn_python-pip3错误-'_NamespacePath'对象没有属性'sort'
  6. 自建服务器和购买云服务器的过程总结
  7. 有云说 | 直播火爆的真正原因是什么?
  8. 网易新闻iOS版使用的18个开源组件
  9. c#上位机plc通讯读位
  10. 弗洛伊德 精神分析学理论