在我们的工作过程中,每当需要排查问题、跑冒烟用例、看测试环境的效果时,经常需要在浏览器环境中切换登录账号,另外,在开发的过程中,也需要在编辑器 VS Code 里切换代理登录的账号。
以政采云的业务开发为例:访问测试、预发等不同环境要切账号,切换不同角色身份、不同地理区划、甚至查看有特殊数据时也要切账号……这让我们的工作中充斥了大量的输入账号密码的无效时间,也需要我们额外维护账号文档,非常苦恼。
关于在 VS Code 编辑器里快捷切换账号的工具,我们已经有同学设计开发过,在后续的文章中会向大家展示。
本文将讲述一下如何在浏览器环境,扩展 Chrome 浏览器原有的“记住密码”功能,实现快捷登录、隔离账号信息以及备注标签等方便使用的功能,同时分享给测试、后端、产品等其他的伙伴,提高大家的效率,希望这次探索能给更多的人带来启发。
主要演示一下插件的位置,其中,删除和置顶是常见功能,就不在这里演示了
传送门编写在 popup/index.html
目录下,用于提供快捷进入不同环境登录页的入口,用颜色清晰地区别开测试、预发等环境,以及记录辅助系统鲁班的地址。
既然是代替用户进行浏览器页面的登录,我们当然可以选择 Chrome Extension (扩展程序)(https://developer.chrome.com/docs/extensions/) 来解决这一难题。
扩展是基于 Web 技术构建的,例如 HTML、JavaScript 和 CSS。它们在单独的沙盒执行环境中运行,并与 Chrome 浏览器交互。
扩展允许您通过使用 API 修改浏览器行为和访问 Web 内容来“扩展”浏览器。
Chrome 浏览器将会识别包含 manifest.json
文件的目录为扩展文件,所以我们可以开发一个 Chrome Extension 项目来解决这一问题。
本次 Chrome 插件选用 React 框架开发,其他开发者也可以根据自己的偏好进行技术选型。
第一版本的插件能力暂时不接入后端,数据都存在本地。
优点
缺点
由于原政采云登录页面是用内部基于 AntD 开发的组件库,为了保持视觉风格的统一,我选择了继续使用我们内部的组件库,每个团队也可以根据自己情况选择自己的组件库,或者开源的组件库,如 ant design,element ui 等。
既然可以访问 Web 内容,那么最简便的操作就是不用触发任何其他的按钮打开弹层,直接 识别登录页面,在原有登录页面的空白处中 插入我们的组件 DOM 元素,就可以实现最便捷的操作。我们得到一个登录账号列表,不必透出密码,根据我们自己打的标签判断当前需要登录的账号,一键登录,代替手动操作。
我们建一个空项目,配置必要的 .babelrc 、.gitignore、webpack.config.js 文件,使得文件可以支持 Babel、Git、Webpack 的正常使用,安装 Less 以及相关的 loader 方便我们的开发,目录结构大致如下:
.
├── README.md
├── package-lock.json
├── package.json
├── src
│ ├── assets # 存放扩展程序的标志图片
│ ├── contentScript # 对 Web 文件的操作
│ ├── manifest.json # Chrome Extension 的清单文件
│ └── popup # 用于存放弹出层
└── webpack.config.js
这里是用来配置扩展程序的基础信息的文件
{
"name": "Account Saver",
"description" : "zcy 账号管理小精灵~",
"version": "1.0",
"manifest_version": 2,
"icons": {
"16": "./assets/icon.png",
"48": "./assets/icon.png",
"96": "./assets/icon.png",
"128": "./assets/icon.png"
},
"browser_action": {
"default_icon": "./assets/icon.png", // 插件加载在浏览器右上角时的图标
"default_title": "账号管理小精灵~", // hover 图标的提示文字
"default_popup":"/popup.html" // 默认点击图标时弹出的浮层
},
"permissions": [
"tabs",
"activeTab",
"storage",
"notifications"
],
"background": {
"persistent": false,
"scripts": ["./background.js"]
},
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"content_scripts": [
{
"matches": [
"http://*/*",
"https://*/*"
],
"js": [ // content script 文件
"/popupListener.js"
],
"run_at": "document_idle"
}
]
}
如下代码配置 webpack ,可以帮助我们编译打包 HTML、JavaScript 和 Less 编写的样式文件,打包静态资源,执行npm run build
获得打包好的 dist 文件,就可以分享到团队中了。
const path = require('path');
const webpack = require('webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
context: path.resolve(__dirname, './src'),
entry: {
popup: './popup/index.js',
background: './background/index.js',
popupListener: './contentScript/popupListener.js',
},
output: {
path: path.resolve(__dirname, './dist'),
publicPath: '/',
filename: '[name].js',
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'less-loader'],
},
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
babelrc: false,
presets: [
// 添加 preset-react 识别 react 代码
require.resolve('@babel/preset-react'),
require.resolve('@babel/preset-env'),
{
plugins: ['@babel/plugin-proposal-class-properties'],
},
],
cacheDirectory: true,
},
},
},
],
},
plugins: [
new HtmlWebpackPlugin({
title: 'popup',
template: './popup/index.html',
inject: true,
chunks: ['popup'],
filename: 'popup.html',
}),
new webpack.HotModuleReplacementPlugin(),
new CleanWebpackPlugin(['./dist/', './zip/']),
new CopyWebpackPlugin([
{ from: 'assets', to: 'assets' },
{ from: 'manifest.json', to: 'manifest.json', flatten: true },
]),
],
};
Content Scripts 是运行在Web页面的上下文的 JavaScript 文件。通过标准的 DOM,Content Scripts 可以操作(读取并修改)浏览器当前访问的Web页面的内容,并将信息传递给父扩展。
在此我们通过原生 JavaScript 的 createElement()
和 append()
方法向 body 中追加元素,插入浮层。
const { domain } = document;
const isZcy = domain.indexOf('zcy') !== -1;
const userDom = document.getElementsByName('username')[0];
if (isZcy && userDom) {
// 域名为政采云域名,且存在 name = username 的元素(输入框)时,在页面左侧插入一个浮层
const body = document.getElementsByTagName('body')[0];
const panelWrapper = document.createElement('div');
ReactDOM.render(<AccountPanel />, panelWrapper);
body.append(panelWrapper);
}
Event()
event = new Event(typeArg, eventInit);
// typeArg 是DOMString 类型,表示所创建事件的名称。
// eventInit 可选,接受以下字段:
// bubbles 是否支持冒泡,cancelable:能否被取消,composed:事件是否会触发shadow DOM(阴影DOM)根节点之外的事件监听器
target.dispatchEvent(event)
const usernameDom = document.getElementById('username');
const passwordDom = document.getElementById('password');
const { accountList } = this.state;
const { username, password } = accountList.find((item) => item.username === handleUsername);
// 未来可能会废弃的写法
// const evt = document.createEvent('HTMLEvents');
// evt.initEvent('input', true, true);
// ie 不支持
const evt = new Event('input', { bubbles: true });
// 将值填入 dom 输入框里
usernameDom.value = username;
usernameDom.dispatchEvent(evt);
passwordDom.value = password;
passwordDom.dispatchEvent(evt);
// 模拟用户点击登录按钮
const loginBtn = document.getElementsByClassName('login-btn')[0];
loginBtn.click();
即使 Webpack 配置了热更新,插件打包出来的 JavaScript 代码更新后也是不能热加载的,我们可以访问 chrome://extensions/
点击下图中的小按钮重新加载,或者安装 Extensions Reloader (https://chrome.google.com/webstore/detail/extensions-reloader/fimgfedafeadlieiabdeeaodndnlbhid?hl=zh-CN) 插件,点击按钮进行重新加载。
Chrome 允许安装 Chrome 应用市场和本地文件两种来源的扩展文件。访问 chrome://extensions/,打开 开发者模式,点击 加载已解压的扩展程序,就可以选中我们本地的文件了,Edge 等浏览器也可以用。
设计方向:对插件的使用者增加登录功能,登录通过 域账号-密码-业务小组 圈定一个范围,同一个 业务小组共享 测试账号、绑定的业务标签、业务小组关联的应用。前端本地开发时,项目获得的账号通过当前应用所属的业务小组拉取。
Chrome Developers (https://developer.chrome.com/docs/extensions/mv3/getstarted/)