一、开发场景
在中后台前端开发的过程中,有个这样的场景:本地开发环境通过代理的方式联调测试环境的接口;为了能拿到测试环境接口的数据,需要把测试环境登录态(Cookie)拷贝过来种到本地的开发环境中。
二、现有方案
- 手动种植Cookie,
document.cookie='uploadticket=NhXfYIYXjmdcZNqped2Pfzc4ay8kvGWLFCoeWigy5Qs=;expires=Tue, 10 Mar 2022 10:03:55 GMT' - 浏览器插件种植Cookie,ModHeader等插件。
三、不足之处
现有的两个方案都是可以达到相应的目的,不足之处是需要人为的去操作登录然后拷贝相应的Cookie,同时还有记忆成本,整个过程下来还是比较无趣而烦恼的。
四、根本方式
- 实现原理:在
webpack-dev-server的proxy配置中增加对应配置,在转发请求的时候增加测试环境的登录态。 - 代理配置示例如下:onProxyReq配置项未在文档中提及,通过proxy的类型提示发现,在http-proxy-middleware 模块中有相应的使用方式介绍。
import onProxyReq from '../scripts/on-proxy-req';
export default {
'/app/api': {
target: 'http://test.xxx.com/',
changeOrigin: true,
pathRewrite: {},
onProxyReq: onProxyReq
}
};
五、整体方案
- 请求测试环境登录接口,获取登录态(Cookie),把登录态信息写入到本地文件。
const request = require('umi-request').default;
const cookieFilePath = require('./cookie-file-path');
const utils = require('./utils');
const fs = require('fs/promises');
try {
const json = require(cookieFilePath);
if (json.expires > Date.now()) {
// console.log('Cookie信息未失效');
process.exit();
}
} catch (err) { }
request.post('http://test.xxx.com/signinSpa', {
data: {
username: 'admin',
password: 'admin'
},
requestType: 'form',
getResponse: true,
timeout: 3e3,
}).then(({ response }) => {
// 文件内容
const data = {
cookies: {},
expires: Date.now() + 3 * 24 * 60 * 60 * 1000
};
response.headers.forEach((value, key) => {
if (typeof key == 'string' && key.toLowerCase() == 'set-cookie') {
const cookie = utils.parseSetCookie(value);
if (Object.keys(cookie).length > 0) {
data.cookies[cookie.name] = cookie.value;
}
}
});
fs.writeFile(
cookieFilePath,
JSON.stringify(data, null, '\t')
).then(() => {
console.log('文件写入成功');
}).catch(() => {
console.error('文件写入失败');
});
}).catch(err => {
console.error(err);
});
- 从文件中读取测试环境登录态信息,然后把登录态信息添加到代理请求的头里。
const cookieFilePath = require('./cookie-file-path');
const utils = require('./utils');
let json = {};
try {
json = require(cookieFilePath);
} catch (err) { }
// 看这里
function onProxyReq(proxyReq) {
if (Object.keys(json).length > 0) {
let cookies = proxyReq.getHeader('Cookie');
cookies = typeof cookies == 'string' ? utils.parseCookie(cookies) : {};
cookies = Object.assign({}, cookies, json.cookies);
cookies = utils.stringifyCookieObj(cookies);
proxyReq.setHeader('Cookie', cookies);
}
}
module.exports = onProxyReq;
- 实现自动化处理:
在npm scripts中增加前置命令,然后再开启本地开发。
{
"scripts": {
"start": "node ./scripts/build-cookie-file.js && cross-env BABEL_CACHE=none UMI_ENV=local umi dev",
}
}
代理配置文件中导入对应的方法。
import onProxyReq from '../scripts/on-proxy-req';
export default {
'/app/api': {
target: 'http://test.xxx.com/',
changeOrigin: true,
pathRewrite: {},
onProxyReq: onProxyReq
}
};
- 开发的时候直接运行
npm run start就可以了,再也不用关心测试环境登录态了。
六、后续方案(不再使用文件方式存放cookie)
// user-login-status.js
const axios = require('axios').default;
const utils = require('./utils');
const _url = '';
const _username = '';
const _password = '';
const _config = { headers: { 'Content-Type': 'multipart/form-data' } };
function fetchUserLoginStatus(url, data, config) {
url = url || _url;
data = Object.assign({ username: _username, password: _password, }, data);
config = Object.assign({}, _config, config);
return axios.post(url, data, config).then((res) => {
const data = res.data || {};
const headers = res.headers;
if (data.code == 200 && Array.isArray(headers['set-cookie'])) {
const arr = headers['set-cookie'];
for (let i = 0; i < arr.length; i++) {
const cookie = utils.parseSetCookie(arr[i]);
if (cookie.name === 'appliedwebsid') {
const obj = {};
obj[cookie.name] = cookie.value;
return obj;
};
}
}
return null;
}).catch((err) => {
console.error(err);
});
}
let loginStatus;
function getLoginStatus() {
return loginStatus;
}
function setLoginStatus(url, data, config) {
return fetchUserLoginStatus(url, data, config).then(cookie => {
loginStatus = cookie;
});
}
setLoginStatus();
module.exports = {
getLoginStatus,
setLoginStatus,
};
// proxy-event-handler.js
const utils = require('./utils.js');
const ULS = require('./user-login-status.js');
const zlib = require('zlib');
const concat = require('concat-stream');
const debounce = require('lodash/debounce.js')
function onProxyReq(proxyReq) {
if (ULS.getLoginStatus()) {
let cookies = proxyReq.getHeader('Cookie');
cookies = typeof cookies == 'string' ? utils.parseCookie(cookies) : {};
cookies = Object.assign({}, cookies, ULS.getLoginStatus());
cookies = utils.stringifyCookieObj(cookies);
proxyReq.setHeader('Cookie', cookies);
}
}
const refreshLoginStatus = debounce(
ULS.setLoginStatus,
1e4,
{ trailing: false, leading: true }
);
// TODO 如何优雅的防频呢
function checkLoginStatus(data) {
try {
data = JSON.parse(data.toString());
if (data && data.code == '41002') {
refreshLoginStatus();
}
} catch (err) {
console.log('检查系统登录态时异常', err);
}
}
function onProxyRes(proxyRes) {
const contentType = proxyRes.headers['content-type'];
const contentEncoding = proxyRes.headers['content-encoding'];
if (typeof contentType === 'string' && contentType.indexOf('application/json') > -1) {
if (contentEncoding === 'gzip'
|| contentEncoding === 'compress'
|| contentEncoding === 'deflate'
) {
proxyRes.pipe(zlib.createUnzip()).pipe(concat(checkLoginStatus));
} else {
proxyRes.pipe(concat(checkLoginStatus));
}
}
}
module.exports = {
onProxyReq: onProxyReq,
onProxyRes: onProxyRes,
};