浏览器插件开发

34 阅读5分钟

1.需要一个manifest.json 文件

2.需要一个background.js 文件

3.需要一个popup.js 文件 popup.html 文件

4.其他页面 比如oss.html 和oss.js 文件, 以及utils/oss.js 请求方法

manifest.json:


{
  "manifest_version": 3,  
  "name": "OSS上传插件",
  "version": "1.0",
  "description": "【上传数据,调用接口,显示并复制结果】【自动查询Pushy资源包hash值】",
  "permissions": ["storage"],
  "host_permissions": [
    "https://******.cn/*",
    "https://******.seakoi.net/*", 告诉插件允许跨域的地址
    "https://at.alicdn.com/*"
  ],
  "action": {
    "default_popup": "popup.html"
  },
  "web_accessible_resources": [
    {
      "resources": [
        "assets/pushStyles.css",
        "panels/push/index.html",
        "panels/push/index.js"
      ],
      "matches": ["<all_urls>"]
    }
  ],
  "background": {
    "service_worker": "background.js",   // 写入请求的文件
    "type": "module"  // 允许使用inmort 语法导入
  }
}

background.js : 发送接收浏览器发送的请求,匹配后端接口请求,将后端请求结果作为浏览器请求结果并返回


import { login, fetchUploadFile } from "./api/oss.js";  // 后端请求方法
// 登录API地址
const LOGIN_API_URL = "https://update.react-native.cn/api/user/login";

// 硬编码的自动登录凭据
const AUTO_LOGIN_CREDENTIALS = {
  email: "service@seakoi.cn",
  pwd: "d69b63258378b191b0fdb7aa5a68a9e3",
};

/**
 * 检查登录状态
 * @returns {Promise<boolean>} 是否存在token
 */
async function checkLoginStatus() {
  const result = await chrome.storage.local.get(["token"]);
  return !!result.token;
}

/**
 * 获取存储的用户信息和token
 * @returns {Promise<{token: string, userInfo: object}>}
 */
async function getStoredData() {
  const result = await chrome.storage.local.get(["token", "userInfo"]);
  return {
    token: result.token || null,
    userInfo: result.userInfo || null,
  };
}

/**
 * 执行登录操作
 * @param {object} credentials 登录凭据 {email: string, pwd: string}
 * @returns {Promise<{success: boolean, data?: object, error?: string}>}
 */
async function performLogin(credentials) {
  try {
    if (!credentials || !credentials.email || !credentials.pwd) {
      throw new Error("缺少登录凭据");
    }

    const response = await fetch(LOGIN_API_URL, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
      },
      body: JSON.stringify({
        email: credentials.email,
        pwd: credentials.pwd,
      }),
      credentials: "omit",
    });

    if (!response.ok) {
      const errorText = await response.text().catch(() => "");
      console.error("登录API响应:", {
        status: response.status,
        statusText: response.statusText,
        headers: Object.fromEntries(response.headers.entries()),
        body: errorText,
      });
      throw new Error(
        `登录失败: ${response.status} ${errorText || response.statusText}`
      );
    }

    const data = await response.json();

    if (data.token && data.info) {
      // 保存token和用户信息
      await chrome.storage.local.set({
        token: data.token,
        userInfo: {
          email: data.info.email,
          name: data.info.name,
        },
      });

      return {
        success: true,
        data: {
          token: data.token,
          userInfo: {
            email: data.info.email,
            name: data.info.name,
          },
        },
      };
    } else {
      throw new Error("登录响应数据格式错误");
    }
  } catch (error) {
    console.error("登录错误:", error);
    return {
      success: false,
      error: error.message || "登录失败,请检查网络连接",
    };
  }
}

// 监听来自popup的消息(监听浏览器发送过来的请求)
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  (async () => {
    try {
      console.log("收到的消息1111", message.body);
      switch (message.action) {
        case "uploadUrlChrome":
          const res1 = await fetchUploadFile(message.body);
          console.log("收到的消息3333", message);
          sendResponse({ success: true, data: res1 }); (将后端请求结果作为浏览器请求结果返回,给到浏览器发送请求的方法)
        case "marktingLogin":
          const res = await login(message.body);
          console.log("收到的消息2222", message);
          sendResponse({ success: true, data: res });
          break;
        case "checkStatus":
          const isLoggedIn = await checkLoginStatus();
          sendResponse({ success: true, isLoggedIn });
          break;

        case "autoLogin":
          // 自动登录,使用硬编码凭据
          const autoLoginResult = await performLogin(AUTO_LOGIN_CREDENTIALS);
          sendResponse(autoLoginResult);
          break;

        case "login":
          // 手动登录,使用传入的凭据
          const loginResult = await performLogin(message.credentials);
          sendResponse(loginResult);
          break;

        case "getUserInfo":
          const storedData = await getStoredData();
          sendResponse({
            success: true,
            token: storedData.token,
            userInfo: storedData.userInfo,
          });
          break;

        default:
          sendResponse({ success: false, error: "未知的操作" });
      }
    } catch (error) {
      console.error("消息处理错误:", error);
      sendResponse({ success: false, error: error.message });
    }
  })();

  // 返回true表示异步响应
  return true;
});

popop.js (结合了所有的其他页面)


// Tab 配置
const tabsData = [
  { id: "oss", title: "OSS上传", folder: "panels/oss" },
  { id: "other", title: "其他面板", folder: "panels/other" },
  { id: "push", title: "Push登录状态", folder: "panels/push" },
];

const tabsContainer = document.getElementById("tabsContainer");
const contentsContainer = document.getElementById("contentsContainer");

// 动态生成 Tab 和内容区域
tabsData.forEach((tab, index) => {
  const tabBtn = document.createElement("div");
  tabBtn.className = "tab" + (index === 0 ? " active" : "");
  tabBtn.dataset.tab = tab.id;
  tabBtn.innerText = tab.title;
  tabsContainer.appendChild(tabBtn);

  const contentDiv = document.createElement("div");
  contentDiv.className = "tab-content" + (index === 0 ? " active" : "");
  contentDiv.id = tab.id;

  // 加载 HTML
  fetch(`${tab.folder}/index.html`)
    .then((res) => res.text())
    .then((html) => {
      contentDiv.innerHTML = html;

      // 加载对应 JS
      const script = document.createElement("script");
      script.src = `${tab.folder}/index.js`;
      script.type = "module";
      contentDiv.appendChild(script);
    })
    .catch((err) => (contentDiv.innerHTML = "<p>加载失败</p>"));

  contentsContainer.appendChild(contentDiv);
});

// Tab 切换
function setupTabs() {
  const tabs = document.querySelectorAll(".tab");
  const contents = document.querySelectorAll(".tab-content");

  tabs.forEach((tab) => {
    tab.addEventListener("click", () => {
      const targetId = tab.dataset.tab;
      tabs.forEach((t) => t.classList.remove("active"));
      tab.classList.add("active");

      contents.forEach((c) => c.classList.remove("active"));
      const targetContent = document.getElementById(targetId);
      if (targetContent) targetContent.classList.add("active");
    });
  });
}
setupTabs();

utils/oss.js请求方法:


import request from "../api/request.js";

// 这里封装了一个统一的浏览器请求方法
export async function apiRequest({
  action = "marktingLogin",
  method = "GET",
  url,
  body = null,
}) {
  return new Promise((resolve, reject) => {
    chrome.runtime.sendMessage(
      {
        action,
        method,
        url,
        body,
      },
      (response) => {
        if (!response) {
          reject(new Error("No response from background"));
          return;
        }

        if (response.success) {
          resolve(response.data);
        } else {
          reject(new Error(response.error || "Request failed"));
        }
      }
    );
  });
}
// 这里定义了一个浏览器请求的方法,用于在浏览器请求监听响应位置处匹配后端请求(在background.js) 中。
/**
 * @param {*} body 登录参数
 * @returns {Promise<void>}
 * @description 封装的浏览器内部方法,用于加载用户信息
 */
export const loadUser = async (body) => {
  console.log("加载用户信息");
  try {
    const data = await apiRequest({
      method: "post",
      url: "/api/base/login",
      body,
    });

    console.log("用户信息:", data);
  } catch (err) {
    console.error("请求失败:", err?.message);
  }
};

export const uploadUrlChrome = async (body) => {
  console.log("加载用户信息");
  try {
    const data = await apiRequest({
      method: "post",
      action: "uploadUrlChrome",
      body,
    });

    console.log("用户信息:", data);
    return data;
  } catch (err) {
    console.error("请求失败:", err?.message);
  }
};

/** 后端接口请求统一 */
/**
 *
 * @param {*} credentials 登录接口请求参数
 * @returns 返回登录信息
 */
export const login = async (credentials) => {
  try {
    const res = await request.post("/api/base/login", credentials);
    return res;
  } catch (err) {
    console.error("登录失败:", err.message);
    throw err; // 可选择继续抛出
  }
};

export const getFont = async ({ url }) => {
  try {
    const res = await request.get(
      "/font",
      { url }, // ✅ 只作为 params 第二个参数是params 参数
      { responseType: "blob" } // ✅ 正确的位置
    );
    return res;
  } catch (err) {
    console.error("请求阿里font失败", err.message);
    throw err;
  }
};

export const uploadFont = async ({ data }) => {
  try {
    const res = await request.post(
      "/api/fileUploadAndDownload/upload?fileType=system",
      { file: data }, // 第二个参数认为是post 的body 参数
      {
        headers: {
          "Content-Type": "multipart/form-data",
          // 注意:某些情况下,'Content-Type'不需要手动设置,因为FormData会自动处理
        },
      }
    );
    return res;
  } catch (err) {
    console.error("请求阿里font失败", err.message);
    throw err; // 可选择继续抛出
  }
};

export const fetchUploadFile = async (data) => {
  if (!data?.url) return;
  try {
    console.log(data, "dsadfadffsf");
    const font = await getFont({ url: data?.url });
    // 步骤2: 准备上传的数据
    const formData = new FormData();
    // 假设后台接口期望的字段名为'file',并且你已经有了一个合适的文件名
    console.log(font, "fdsfsffffffffffff11111111");
    const fileName = "downloaded_font.css"; // 替换为实际文件名和扩展名
    formData.append("file", font, fileName);
    const uploadResult = await uploadFont({ data: formData });
    console.log(formData, font, uploadResult, "sadfasdfsafs22222222222");
    if (uploadResult?.code === 0) {
      return uploadResult?.data?.file?.url ?? "";
    }
    throw new Error("上传接口报错");
    // 假设后台返回了JSON,其中包含新文件的路径
    // if (uploadResponse.status === 200) {
    //     const newFilePath = uploadResponse.data.newFilePath; // 替换为你的实际返回字段名
    //     console.log(newFilePath, 'sadfasdfsafs')
    //     return newFilePath;
    // } else {
    //     throw new Error(`Failed to upload file: ${uploadResponse.statusText}`);
    // }
  } catch (error) {
    console.error("Error processing file:", error);
    throw new Error(error); // 可以选择重新抛出错误以便上层处理
  }
};

oss.html || oss.js


# **在oss.js 中 我们可以通过点击,触发浏览器请求的方法,匹配到后端接口后,执行后端请求。最后通过浏览器请求接口最后返回到是后端请求的结果。**

import { loadUser, uploadUrlChrome } from "../../api/oss.js";
console.log("OSS 面板 JS 加载完成");
// 模拟登录状态键 (在浏览器插件中,通常是存储Token或其他凭证)
const LOGIN_STATE_KEY = "oss_is_logged_in";
const loginView = document.getElementById("loginView");
const ossUploadView = document.getElementById("ossUploadView");
const loginSubmitBtn = document.getElementById("loginSubmitBtn");
const logoutBtn = document.getElementById("logoutBtn");
const loginUsernameInput = document.getElementById("loginUsername");
const loginPasswordInput = document.getElementById("loginPassword");

/**
 * 切换显示的视图(登录面板或上传面板)
 */
const renderView = () => {
  // 检查 localStorage 中的状态
  const isLoggedIn = localStorage.getItem(LOGIN_STATE_KEY) === "true";

  if (isLoggedIn) {
    // 已登录,显示 OSS 上传面板
    loginView.classList.add("view-hidden");
    ossUploadView.classList.remove("view-hidden");
    console.log("当前视图: OSS 上传面板");
  } else {
    // 未登录,显示登录面板
    loginView.classList.remove("view-hidden");
    ossUploadView.classList.add("view-hidden");
    console.log("当前视图: 登录面板");
  }
};

/**
 * 模拟登录过程
 */
const handleLogin = async () => {
  const username = loginUsernameInput.value.trim();
  const password = loginPasswordInput.value;

  // if (!username || !password) {
  //   alert("请输入用户名和密码!");
  //   return;
  // }
  const loginObject = {
    checkoutSecret: true,
    deviceUniqueId: "",
    internalDeviceId: "",
    loginAppVersion: "",
    loginDeviceType: "Mac/Chrome",
    loginIsApp: 0,
    openCaptcha: false,
    password: "123456",
    username: "13345678953",
  };

  // --- 实际项目中,这里应发起API请求进行身份验证 ---
  console.log(`尝试使用 ${username} 登录...`);
  await loadUser({
    ...loginObject,
    username: "17688710001",
    password: "123456",
  });
  // 简化处理:假设任何输入都登录成功
  const success = true;

  if (success) {
    // 1. 更新状态到本地存储
    localStorage.setItem(LOGIN_STATE_KEY, "true");

    // 2. 切换视图
    renderView();

    // 3. 清空密码字段
    loginPasswordInput.value = "";

    alert(`欢迎回来,${username}!`);
  } else {
    alert("登录失败,请检查账号和密码。");
  }
};

/**
 * 退出登录
 */
const handleLogout = () => {
  // 1. 清除本地存储状态
  localStorage.removeItem(LOGIN_STATE_KEY);

  // 2. 切换视图
  renderView();

  alert("您已安全退出。");
};

// --- 事件监听器绑定 ---

// 1. 登录按钮点击事件
if (loginSubmitBtn) {
  // 使用箭头函数作为回调
  loginSubmitBtn.addEventListener("click", handleLogin);
}

// 2. 退出按钮点击事件
if (logoutBtn) {
  // 使用箭头函数作为回调
  logoutBtn.addEventListener("click", handleLogout);
}

// 3. 页面加载完毕后,立刻检查登录状态并渲染视图
renderView();

// --- OSS 上传面板功能 ---

const uploadBtn = document.getElementById("uploadBtn");
if (uploadBtn) {
  // 使用箭头函数作为回调
  uploadBtn.addEventListener("click", async () => {
    // const url = document.getElementById("inputUrl").value;
    const url = "https://at.alicdn.com/t/c/font_4997520_o6cupx6t3v.js";
    if (url) {
      document.getElementById("newUrl").value = `正在上传 ${url} ...`;
      const resultUrl = await uploadUrlChrome({ url }); // 触发浏览器请求方法
      // 在这里实现您的 OSS 上传逻辑, resultUrl 就是调用浏览器请求方法,获取到了后端接口数据。
      console.log("开始上传:", url);
      document.getElementById("newUrl").value = resultUrl;
    } else {
      document.getElementById("newUrl").value = `请输入有效的地址!`;
    }
  });
}