用wxt开发浏览器插件心得
wxt
是一个浏览器插件开发工具,详细信息参见https://github.com/wxt-dev/wxt
。
某教务系统平时成绩没有Excel导入功能,只能手工一个个输入,工作量相当繁重,本人开发了一个js小脚本完成任务,并可同时完成平时成绩及期末成绩导入。但针对非计算机专业人士,少数同事都不知如何打开并编辑js脚本,及如何进入浏览器调试工具、注入js并运行,完成输入任务。因此,开发浏览器插件很有必要。
生成工程
pnpm dlx wxt@latest init 工程名
设置权限
因为插件需要获取浏览器当前tab并注入及运行脚本,修改wxt.config.ts
文件:
import { defineConfig } from "wxt";
// See https://wxt.dev/api/config.html
export default defineConfig({
manifest: {
name: "xx学院教务系统成绩导入",
permissions: ["activeTab", "tabs", "scripting"],
},
});
脚本实现思路
单击插件图标,弹出一个界面,提供一个textarea方便从Excel贴学号、姓名、成绩,一个导入
按钮,单击后将js代码注入到浏览器活动标签页即教务系统成绩录入页面。但popup
页面和浏览器的其它页面是隔离的,因此需要特殊方法才能访问里面的元素。
获取用户输入
现在脚本和popup
在同一沙箱中,比较简单:
const scoresRaw = (
document.getElementById("txtScoresRaw") as HTMLInputElement | null
)?.value;
获取当前活动标签页
const tabs = await browser.tabs.query({
active: true,
windowId: browser.windows.WINDOW_ID_CURRENT,
});
if (tabs.length === 0) return;
if (typeof tabs[0].id !== "number") return;
待注入脚本
该脚本将在用户活动标签页中运行,必须采用特殊的方法才能正常运行。
function doImport(scoresRaw: string) {...}
注入过程
try {
await browser.scripting.executeScript({
target: {
tabId: tabs[0].id,
},
func: doImport,
args: [scoresRaw],
});
} catch (err) {
alert(`运行失败: ${err}`);
}
附popup
html源码
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>xx学院教务系统成绩导入</title>
<meta name="manifest.type" content="browser_action" />
<link rel="stylesheet" href="bulma/css/bulma.css" />
</head>
<body>
<div class="field">
<label class="label" for="txtScoresRaw">学生成绩:</label>
<div class="control">
<textarea id="txtScoresRaw" rows="20" cols="50" placeholder="请粘贴学生成绩: 学号、姓名、成绩,每行一条记录"></textarea>
</div>
</div>
<div class="field">
<div class="control">
<button id="btnImport" class="button is-primary" type="button">导入</button>
</div>
</div>
<script type="module" src="./main.ts"></script>
</body>
</html>
附popup
ts源码
export type {};
function doImport(scoresRaw: string) {
// 分数
const scores = {};
// 按行进行切分
const rows = scoresRaw.trim().split("\n");
for (const row of rows) {
const fields = row
.trim()
.split(/ |\t/)
.map((e) => e.trim())
.filter((e) => e !== "");
scores[fields[0]] = fields;
}
// 找到录入成绩对应的文档
let doc = document; // 默认为top文档,即当前页面所在的文档
for (let i = 0; i < frames.length; i++) {
const d = frames[i].document;
if (d.title === "教学记录-YETHAN以专教学信息服务平台") {
doc = d;
break;
}
}
// 填充到每个输入框
let ids = doc.getElementsByName("studentId"); // 平时成绩录入时学号
if (ids.length === 0) {
ids = doc.getElementsByName("student_id"); // 期末成绩录入时学号
}
const marks = doc.getElementsByName("mark");
for (let i = 0; i < marks.length; i++) {
const id = ids[i].value;
const row = scores[id];
if (row) {
// 检查是否有该学生记录
const score = row[2];
if (score) {
// 判断成绩是否存在
marks[i].value = score;
} else {
alert(`学号:${id} 学生无成绩!!!`);
}
} else {
alert(`学号:${id} 学生无记录!!!`);
}
}
}
async function inject() {
const scoresRaw = (
document.getElementById("txtScoresRaw") as HTMLInputElement | null
)?.value;
const tabs = await browser.tabs.query({
active: true,
windowId: browser.windows.WINDOW_ID_CURRENT,
});
if (tabs.length === 0) return;
if (typeof tabs[0].id !== "number") return;
try {
await browser.scripting.executeScript({
target: {
tabId: tabs[0].id,
},
func: doImport,
args: [scoresRaw],
});
} catch (err) {
alert(`运行失败: ${err}`);
}
}
const button = document.getElementById("btnImport");
button?.addEventListener("click", inject);
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话