背景
前端项目在本地调试时都难以跳过登录环节,如果每次都要通过登录去调试就太麻烦了,有没有什么方法避免调试时还需要跳往登录页登录呢?文本梳理出下面三种方案供大家参考:
Cookie复制
Cookie
复制是一种最简单粗暴的纯手工方案,步骤如下:
- 先在测试环境进行登录,在控制台中拿到
Cookie
中的令牌 - 本地启动前端项目,打开控制台
Cookie
存储,将刚刚拿到的令牌复制粘贴到该域名下的Cookie
存储中,然后刷新页面
这种纯手工的方案也是挺麻烦的,每次也需要去Cookie
中找令牌,然后复制,还会出现复制粘贴错误内容。既然手工复制不好,那我们能不能自动化复制呢?答案是可以的,谷歌拓展插件可以做到这一点
谷歌拓展插件
一个应用(扩展)其实是压缩在一起的一组文件,包括HTML、CSS、JavaScript
脚本、图片文件及其它任何需要的文件。 应用(扩展)本质上来说就是web
页面。
文件
每个应用(扩展)都应该包含下面的文件:
- 一个
manifest
文件 - 一个或多个
HTML
文件(除非这个应用是一个皮肤) - 可选的一个或多个
JavaScript
文件 - 可选的任何需要的其他文件,例如图片
在开发应用(扩展)时,需要把这些文件都放到同一个目录下。发布应用(扩展)时,这个目录全部打包到一个应用(扩展)名是 .crx
的压缩文件中。如果使用谷歌浏览器应用开放平台或Chrome Developer Dashboard
上传应用(扩展),可以自动生成 .crx
文件。
弹窗的架构
背景页面并不是应用(扩展)中唯一的页面。例如,一个browser action
可以包含一个弹窗(popup)
,而弹窗就是用html
页面实现的。
应用(扩展)里面的HTML
页面可以互相访问各自DOM
树中的全部元素,或者互相调用其中的函数。
下图显示了一个browser action
的弹窗的架构。弹窗的内容是由HTML
文件(popup.html
)定义的web
页面。它不必复制背景页面(background.html
)里的代码,因为它可以直接调用背景页面中的函数。
复制Cookie
思路如下:
- 获取地址栏地址域名下所有
Cookie
- 将
Cookie
写入调试地址域名下 - 打开调试页面
涉及到的API:
chrome.cookies.getAll(object details, function callback)
从一个cookie
存储获取与给定信息匹配的所有cookies
。所获取cookies
将按照最长路径优先排序。当多个cookies
有相同长度路径,最早创建的cookie
在前面。chrome.cookies.set(object details)
用给定数据设置一个cookie
。如果相同的cookie
存在,它们可能会被覆盖。chrome.tabs.getSelected(integer windowId, function callback)
获取特定窗口指定的标签。
项目结构
debug
├── iamges
└── icon.png
├── manifest.json
├── popup.css
├── popup.html
└── popup.js
配置文件
{
"manifest_version": 2,
"name": "debug",
"description": "前端项目调试工具",
"version": "1.0.0",
"icons": {
"16": "/images/icon16.png",
"32": "/images/icon32.png",
"48": "/images/icon48.png",
"128": "/images/icon128.png"
},
"permissions": [
"cookies", // 访问Cookie的权限
"tabs", // 访问标签的权限
"http://*/*",
"https://*/*",
"storage"
],
"browser_action": {
"default_icon": {
"16": "/images/icon16.png",
"32": "/images/icon32.png",
"48": "/images/icon48.png",
"128": "/images/icon128.png"
},
"default_popup": "popup.html"
}
}
弹窗页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./popup.css">
<title>Document</title>
</head>
<body>
<input type="text" name="redirect_url" id="redirect_url">
<button id="debug_btn">debug</button>
<script src="./popup.js"></script>
</body>
</html>
弹窗脚本
/** 按钮DOM */
const debug_btn = document.getElementById('debug_btn');
/** 重定向 */
const redirectTo = (url) => {
window.open(url);
}
/** 获取地址栏 */
const getUrl = () => {
return new Promise((resolve) => {
chrome.tabs.getSelected(null, resolve)
})
}
/** 获取Cookie */
const getCookie = () => {
return new Promise(async (resolve) => {
const tab = await getUrl();
// alert('tab' + JSON.stringify(tab))
chrome.cookies.getAll({ url: tab.url }, resolve)
})
}
/** 设置Cookie */
const setCookie = () => {
return new Promise(async () => {
const cookies = await getCookie();
// alert('cookie' + JSON.stringify(cookie))
const url = document.getElementById('redirect_url').value;
const url_reg = /^https?:\/\/*\/*/;
if (url_reg.test(url)) {
cookies.forEach((cookie) => {
const { name, value, path, secure, expirationDate, storeId } = cookie;
chrome.cookies.set({ url, name, value, path, secure, expirationDate, storeId, domain: 'localhost' });
})
redirectTo(url);
} else {
alert('输入url不正确!')
}
})
}
/** 监听点击事件 */
debug_btn.addEventListener('click', async () => {
setCookie();
})
添加到拓展程序中
- 打开谷歌浏览器拓展程序,路径:菜单->更多工具->拓展程序。或者通过
chrome://extensions
链接打开 - 单击“加载已解压的拓展程序”,选择我们搭建的拓展目录
效果
代理服务
思路:在浏览器与本地前端项目服务之间搭建一个代理服务,代理服务主要帮助我们完成登录拿到令牌,转发浏览器的请求,向本地前端项目服务拉取静态资源(比如静态页面、样式文件等),和向资源服务器获取受保护资源
实践
首先搭建以下四个服务:
- 代理服务
localhost:2000(proxy)
SSO
服务localhost:8888(SSO)
- 本地前端服务
localhost:3304(fontend)
- 资源服务
localhost:8080(backend)
代理服务proxy
代理服务主要有以下几个职责:
- 启动代理服务时向
SSO
服务登录获取令牌,并存在服务内存中 - 向本地服务拉取静态资源文件,比如
index.html
、图片、样式文件等 - 转发获取受保护资源的请求
调试配置文件:
module.exports = {
user: {
username: 'ddxyq',
password: '123456',
},
proxy: [
{
path: ['/api'],
target: 'http://localhost:8080', // 资源服务
},
],
html: 'http://localhost:3304', // 前端本地服务
frontPage: true,
};
具体实现:
const express = require('express');
const request = require('request');
const config = require('./config/debug');
const app = express();
const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
/** 管理cookie */
var cookie = null;
/** 请求转发url,判断是获取静态资源页面还是获取受保护资源 */
const getProxy = (path) => {
const { proxy, html } = config;
const t = proxy.filter(p => p.path.some((s) => path.includes(s)));
return t.length ? t[0].target + path : html + path;
}
/** 请求转发 */
app.all('*', async (req, res) => {
const options = {
url: getProxy(req.path),
headers: {
Cookie: cookie,
'Content-Type': req.headers['content-type'] || req.headers['Content-Type'],
'User-Agent': userAgent,
},
body: req.body,
};
const result = request(options)
if (result instanceof Promise) {
const { result: r } = await result;
res.send(r);
return;
}
result.pipe(res);
return;
})
/** 自动登录 */
const login = () => {
const options = {
method: 'POST',
url: 'http://localhost:8888/passport/login',
headers: {
'Content-Type': 'application/json',
'User-Agent': userAgent,
},
json: true,
body: config.user,
};
request(options, (error, res, body) => {
if (error) throw new Error(error);
if (body === 'OK') {
const c = res.headers['set-cookie'];
request({
method: 'GET',
url: 'http://localhost:3304',
headers: {
Cookie: c,
'User-Agent': userAgent,
},
}, (e, r, b) => {
if (!!b) {
cookie = c;
console.log('登录成功!');
}
})
}
})
}
login();
app.listen(process.env.PORT, () => {
console.log(`Example app listening at http://localhost:${process.env.PORT}`)
})
SSO服务
SSO服务主要是校验用户登录信息后下发令牌
const express = require('express');
const bodyParser=require('body-parser');
const jwt = require('jsonwebtoken');
const cors = require("cors");
const app = express();
const SECRET = 'SDKHASUDFADFUASHDVAB';
/** 中间件 */
app.use(cors());
app.use(bodyParser.json({ extended: false }));
/** 登录下发token */
app.post('/passport/login', (req, res) => {
const { username, password } = req.body;
console.log('username, password',username, password)
if (username && password) {
const payload = { username };
const token = jwt.sign(payload, SECRET, { expiresIn: '1day' });
/** 设置Cookie */
res.cookie('security_context', token, { domain: 'localhost', path: '/', httponly: true });
res.send(200);
} else {
res.json(500, { error: '账户密码不正确!' });
}
})
app.listen(process.env.PORT, () => {
console.log(`Example app listening at http://localhost:${process.env.PORT}`)
})
本地前端服务(fontend)
我们用create-react-app脚手架搭建一个react应用,并在应用中请求受保护资源
测试组件代码:
import React, { useEffect, useState } from 'react';
import logo from './logo.svg';
import './App.css';
import axios from 'axios';
function App() {
const [data, setData] = useState([]);
useEffect(() => {
axios.get(`/api/quotestore/123456`).then((res: any) => {
const { code, data } = res.data;
if (code === 200) {
setData(data);
}
})
}, []);
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
{
data.map((d: any) => (
<div>
<span>{d.storeName}</span>
</div>
))
}
</header>
</div>
);
}
export default App;
资源服务(backend)
资源服务也就是我们的后端服务,处理前端请求响应数据
校验令牌中间件
const jwt = require('jsonwebtoken');
const secret = 'SDKHASUDFADFUASHDVAB';
const tokenMiddleWare = (req, res, next) => {
const security_context = req.cookies['security_context'];
jwt.verify(security_context, secret, (error, decoded) => {
if (error) {
console.log(error.message)
res.redirect(302, 'http://localhost:7777/login');
}
console.log(decoded)
next();
})
}
module.exports = tokenMiddleWare
主要代码:
const express = require('express');
const cors = require('cors');
const bodyParser=require('body-parser');
const cookieParser = require('cookie-parser');
const tokenMiddleWare = require('./tokenMiddleWare');
const app = express();
/** 中间件 */
app.use(cors());
app.use(bodyParser.json({ extended: false }));
app.use(cookieParser());
app.get('/api/quotestore/:id', tokenMiddleWare, (req, res) => {
const { id } = req.params;
console.log(`id: ${id}, ${req.cookies['security_context']}`);
if (!!id) {
return res.json({
code: 200,
data: [
{
storeName: 'ddxyq',
storeId: 'nsifghe'
},
{
storeName: 'kkzjx',
storeId: 'etyjdsdrf'
},
{
storeName: 'hlhjx',
storeId: 'eryeryjadf'
},
],
errorMessage: null
})
} else {
res.json({
code: 500,
data: null,
errorMessage: 'id不存在'
})
}
})
app.listen(process.env.PORT, () => {
console.log(`Example app listening at http://localhost:${process.env.PORT}`)
})
通过代理服务(localhost:2000
)向SSO
服务(localhost:8888
)请求登录拿到令牌后,可以访问访问前端本地服务(localhost:3304
)页面,并向资源服务(localhost:8080
)请求受保护资源
最后,代理服务可以封装成一个命令行工具库,发布到公司的仓库中,就可以帮助前端同学优雅的调试了~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。