开发提效 - 用好Chrome插件

用上自己写的Chrome插件是一种什么感觉,想想都逼格很高,我来教你!

背景

先说这个需求的产生。之前的业务开发中,有一个场景是,因为每个账号的权限和数据不同,需要不停的切换账号去查看不同的数据,但在localhost环境下登录成功后,却会跳转到qa环境,需要很笨的打开浏览器控制台去复制qa环境的cookie去替换localhost环境的cookie。😰

同样的操作做多了之后意识到一定有优化的空间。最开始想到的是在配置文件里将localhost也同qa一样做特化,让它做正确的跳转。实践之后,确实解决了登录后错误跳转的问题。但需要反复登录、登出的问题依然在,所以,还得想! 🤔️

最终解决方案是用Chrome插件。

开整

因为功能很简单,所以代码也很简单。Chrome插件开发需要的知识也很简单,前端三件套就行。

准备工作

首先在Chrome浏览器的扩展程序中加载已解压的扩展程序

1、在浏览器中打开chrome://extensions/

2、点击加载已解压的扩展程序,选择本地准备好的code目录;

3、加载完成。

开始code

先看一下插件code目录结构,由 前端三件套 + icon + manifest.json 组成。

manifest.json文件是Chrome插件的配置文件,下面是基本配置属性:

  • permissions:插件可以使用的权限,本案例需要cookiestabs。全部权限在这【👉🔗👈】
  • icons:各尺寸的icons,icons推荐png,不支持svg。
  • action:引入html。

代码:

js 复制代码
{
  "manifest_version": 3,
  "name": "一键登录",
  "description": "自动填充有效token到cookie,仅供内部使用",
  "version": "1.0.0",
  "permissions": [
    "cookies",
    "tabs"
  ],
  "icons": {
    "16": "images/icon-16.png",
    "32": "images/icon-32.png",
    "48": "images/icon-48.png",
    "128": "images/icon-128.png"
  },
  "action": {
    "default_popup": "popup.html"
  }
}

好了,接下来都是操作三件套了(代码比较简单,搞清楚思路以及所需要的功能即可)。

popup.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>一键登录</title>
    <link rel="stylesheet" href="popup.css">
</head>

<body>
    <div class="login-container">
        <h2>一键登录</h2>
        <div class="login-item" id="inputLoginItem">
            <input class="uid-input" id="uidInput" placeholder="输入uid登录" />
            <button class="login-button" id="loginButton">一键登录</button>
        </div>
        <div class="tips">
            当前页面的 host 必须为以下几种才有效:
            <div class="tips-iems">
                <p> - *.qa.com</p>
                <p> - localhost(只能登录qa环境)</p>
            </div>
        </div>
    </div>
</body>

<script src="popup.js"></script>
</html>

popup.js,考虑的点稍多,一步步来讲

1、配置不同环境的变量(localhost、qa)

js 复制代码
  // 环境Map
  const envMap = {
    localhost: {
      url: 'http://localhost',
      domain: 'localhost',
      apiPrefix: 'https://api.qa.com',
      envName: 'QA',
    },
    qa: {
      url: 'https://www.qa.com',
      domain: 'qa.com',
      apiPrefix: 'https://api.qa.com',
      defaultUserId: 100001,
      envName: 'QA',
    },
  };

2、获取环境变量

js 复制代码
  // 环境名
  const env = await getEnv();
  // API前缀
  const API_PREFIX = envMap[env]?.apiPrefix;
  // 当前环境的URL
  const BASE_URL = envMap[env]?.url;
  // 当前环境的域名
  const DOMAIN = envMap[env]?.domain;
  // 当前环境默认设置的用户ID
  const DEFAULT_USER_ID = envMap[env]?.defaultUserId;
  // 当前环境的名字
  const envName = envMap[env]?.envName;
  
  // 获取当前环境
  async function getEnv() {
    let env;
    const result = await chrome.tabs.query({
      active: true,
      currentWindow: true,
    });
    const url = result[0].url.split('?')[0];
    const envKeys = Object.keys(envMap);
    envKeys.forEach((item) => {
      if (url.includes(envMap[item].domain)) {
        env = item;
      }
    });
    return env;
  }

3、为dom绑定事件

js 复制代码
  // 登陆按钮
  const loginButton = document.getElementById('loginButton');
  // uid输入框
  const uidInput = document.getElementById('uidInput');

  // 点击事件
  loginButton.onclick = async function () {
    // 检查输入的uid是否合法
    if (!uidInput.value || isNaN(Number(uidInput.value))) {
      alert('请输入正确的uid');
      return null;
    }

    // 拿到此uid返回的结果
    const { token, user } = await fetchToken(uidInput.value, this);

    // 如果用户存在,修改浏览器cookie
    if (user) {
      await setToken(token, user);
    }
  };

  // 添加键盘事件,当按下回车键时,触发登陆按钮的点击事件
  uidInput.onkeydown = function (e) {
    if (e.code === 'Enter') {
      loginButton.click();
    }
  };
  
  // 获取uid token
  async function fetchToken(uid, currentButton = null, isDefault = false) {
    if (currentButton) {
      currentButton.innerHTML = '登录中...';
      currentButton.style.opacity = 0.8;
      currentButton.disabled = true;
    }

    // 返回uid对应的token
    const result = await request({
      url: `${API_PREFIX}user/getToken?userId=${uid}`,
    });

    if (currentButton) {
      currentButton.innerHTML = '一键登录';
      currentButton.removeAttribute('style');
      currentButton.disabled = false;
    }

    if (!result) {
      alert('用户不存在');
      return {};
    }

    return { token: result.token, user: uid };
  }
  
  // 设置cookie
  async function setToken(token, user) {
    const env = await getEnv();
    if (!env || !user) return;
    const { url, domain } = envMap[env];
    chrome.cookies.set(
      {
        url,
        name: 'token',
        value: token,
        domain: domain,
      },
      function () {
        chrome.tabs.query({ active: true, currentWindow: true }, async function (arrayOfTabs) {
          const enable = await isEnableHost();
          if (enable) {
            const redirectUrl = new URL(arrayOfTabs[0].url).searchParams.get('path');
            if (redirectUrl) {
              chrome.tabs.update(arrayOfTabs[0].id, {
                url: redirectUrl,
              });
            } else {
              chrome.tabs.reload(arrayOfTabs[0].id);
            }
            window.close();
          }
        });
      },
    );
  }
  
  // 当前host是否启用
  async function isEnableHost() {
    const env = await getEnv();
    if (!env) return false;
    const arrayOfTabs = await chrome.tabs.query({
      active: true,
      currentWindow: true,
    });
    const currentUrl = arrayOfTabs[0].url.split('?')[0];
    return currentUrl.includes(DOMAIN);
  }

4、初始化init

js 复制代码
  // 初始化,不是可用环境的域名时,禁用登陆按钮;否则show出当前环境的名字,并显示历史登陆uid记录
  async function init() {
    if (!env) {
      inputLoginButton.disabled = !(await isEnableHost());
      uidInput.setAttribute('disabled', 'disabled');
      return;
    }
    envNameElement.innerText = `- 当前为 ${envName} 环境`;
    await renderHistory();
  }

popup.js完整代码(一个js文件全部梭哈🤣):

js 复制代码
(async function () {
  // 环境Map
  const envMap = {
    localhost: {
      url: 'http://localhost',
      domain: 'localhost',
      apiPrefix: 'https://api.qa.com',
      envName: 'QA',
    },
    qa: {
      url: 'https://www.qa.com',
      domain: 'qa.com',
      apiPrefix: 'https://api.qa.com',
      defaultUserId: 100001,
      envName: 'QA',
    },
  };

  // 环境名
  const env = await getEnv();
  // API前缀
  const API_PREFIX = envMap[env]?.apiPrefix;
  // 当前环境的URL
  const BASE_URL = envMap[env]?.url;
  // 当前环境的域名
  const DOMAIN = envMap[env]?.domain;
  // 当前环境默认设置的用户ID
  const DEFAULT_USER_ID = envMap[env]?.defaultUserId;
  // 当前环境的名字
  const envName = envMap[env]?.envName;

  // 获取当前环境
  async function getEnv() {
    let env;
    const result = await chrome.tabs.query({
      active: true,
      currentWindow: true,
    });
    const url = result[0].url.split('?')[0];
    const envKeys = Object.keys(envMap);
    envKeys.forEach((item) => {
      if (url.includes(envMap[item].domain)) {
        env = item;
      }
    });
    return env;
  }

  // 登陆按钮
  const loginButton = document.getElementById('loginButton');
  // uid输入框
  const uidInput = document.getElementById('uidInput');

  // 点击事件(一套走完的基本流程)
  loginButton.onclick = async function () {
    // 1、检查输入的uid是否合法
    if (!uidInput.value || isNaN(Number(uidInput.value))) {
      alert('请输入正确的uid');
      return null;
    }

    // 2、拿到此uid返回的结果
    const { token, user } = await fetchToken(uidInput.value, this);

    // 3、如果用户存在,修改浏览器cookie,并刷新页面
    if (token && user) {
      await setToken(token, user);
    }
  };
  
  // =================== 基本思路结束 ===================

  // 添加键盘事件,当按下回车键时,触发登陆按钮的点击事件
  uidInput.onkeydown = function (e) {
    if (e.code === 'Enter') {
      loginButton.click();
    }
  };

  // 下面是点击流程中需要用到的工具函数
  // 获取 uid token
  async function fetchToken(uid, currentButton = null, isDefault = false) {
    if (currentButton) {
      currentButton.innerHTML = '登录中...';
      currentButton.style.opacity = 0.8;
      currentButton.disabled = true;
    }
    // 返回uid对应的token
    const result = await request({
      url: `${API_PREFIX}user/getToken?userId=${uid}`,
    });
    if (currentButton) {
      currentButton.innerHTML = '一键登录';
      currentButton.removeAttribute('style');
      currentButton.disabled = false;
    }
    if (!result) {
      alert('用户不存在');
      return {};
    }
    return { token: result.token, user: uid };
  }

  // 设置cookie
  async function setToken(token, user) {
    const env = await getEnv();
    if (!env || !user) return;
    const { url, domain } = envMap[env];
    chrome.cookies.set(
      {
        url,
        name: 'token',
        value: token,
        domain: domain,
      },
      function () {
        chrome.tabs.query({ active: true, currentWindow: true }, async function (arrayOfTabs) {
          const enable = await isEnableHost();
          if (enable) {
            const redirectUrl = new URL(arrayOfTabs[0].url).searchParams.get('path');
            if (redirectUrl) {
              chrome.tabs.update(arrayOfTabs[0].id, {
                url: redirectUrl,
              });
            } else {
              chrome.tabs.reload(arrayOfTabs[0].id);
            }
            window.close();
          }
        });
      },
    );
  }

  // 当前host是否启用
  async function isEnableHost() {
    const env = await getEnv();
    if (!env) return false;
    const arrayOfTabs = await chrome.tabs.query({
      active: true,
      currentWindow: true,
    });
    const currentUrl = arrayOfTabs[0].url.split('?')[0];
    return currentUrl.includes(DOMAIN);
  }
  
  // 请求函数
  function request({url, headers, method = "get"}) {
    return new Promise((resolve, reject) => {
        fetch(url, {
            method,
            headers
        })
            .then(function (response) {
                response
                    .json()
                    .then(function (res) {
                        resolve(res.data);
                    })
                    .catch(function (error) {
                        console.error("Fetch error:" + error);
                        alert("请求失败");
                        reject(error);
                    });
            })
            .catch(function (error) {
                console.error("Fetch error:" + error);
                alert("请求失败");
                reject(error);
            });
    })
  }
  // =========== 工具函数end ===========

  // 初始化,不是可用环境的域名时,禁用登陆按钮;否则show出当前环境的名字
  async function init() {
    if (!env) {
      inputLoginButton.disabled = !(await isEnableHost());
      uidInput.setAttribute('disabled', 'disabled');
      return;
    }
    envNameElement.innerText = `- 当前为 ${envName} 环境`;
    await renderHistory();
  }

  init();
})();

还可以添加缓存功能(需要给storage权限),这样就可以记录之前登录过的账号,可以优化的地方还有很多。

总结

看完这个例子后,有没有发现其实就是在操作dom和浏览器常见API,Chrome插件没有想的那么难,快快定制你的专属插件吧 (^-^)

over~🫡

相关推荐
程序猿小D17 分钟前
第二百六十七节 JPA教程 - JPA查询AND条件示例
java·开发语言·前端·数据库·windows·python·jpa
奔跑吧邓邓子1 小时前
npm包管理深度探索:从基础到进阶全面教程!
前端·npm·node.js
前端李易安1 小时前
ajax的原理,使用场景以及如何实现
前端·ajax·okhttp
风清扬_jd2 小时前
Chromium 添加书签功能浅析c++
c++·chrome
汪子熙2 小时前
Angular 服务器端应用 ng-state tag 的作用介绍
前端·javascript·angular.js
Envyᥫᩣ2 小时前
《ASP.NET Web Forms 实现视频点赞功能的完整示例》
前端·asp.net·音视频·视频点赞
Мартин.6 小时前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
昨天;明天。今天。7 小时前
案例-表白墙简单实现
前端·javascript·css
数云界7 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd7 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome