开发提效 - 用好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~🫡

相关推荐
陈辛chenxin16 小时前
软件测试大赛Web测试赛道工程化ai提示词大全
前端·可用性测试·测试覆盖率
沿着路走到底16 小时前
python 判断与循环
java·前端·python
Code知行合壹16 小时前
AJAX和Promise
前端·ajax
大菠萝学姐16 小时前
基于springboot的旅游攻略网站设计与实现
前端·javascript·vue.js·spring boot·后端·spring·旅游
心随雨下16 小时前
TypeScript中extends与implements的区别
前端·javascript·typescript
摇滚侠16 小时前
Vue 项目实战《尚医通》,底部组件拆分与静态搭建,笔记05
前端·vue.js·笔记·vue
双向3316 小时前
CANN训练营实战指南:从算子分析到核函数定义的完整开发流程
前端
caleb_52016 小时前
vue cli的介绍
前端·javascript·vue.js
Swift社区16 小时前
如何监测 Vue + GeoScene 项目中浏览器内存变化并优化性能
前端·javascript·vue.js
WYiQIU16 小时前
大厂前端岗重复率极高的场景面试原题解析
前端·javascript·vue.js·react.js·面试·状态模式