用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}`);
}

popuphtml源码

<!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>

popupts源码

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);
posted @   卓能文  阅读(412)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示