背景:前两天线上出现了训练营详情无法跳转的问题,定位之后是由于前端在调接口时传参有问题,导致的接口返回参数不对导致的。
原因分析:
问题接口:http://o2o.dailyyoga.com.cn/620/yogao2school/session/马赛克?session_id=759&dy=1&app[]=1&app[]=1&version[]=7.14.0.0&version[]=7.14.0.0&sid[]=fbdaf97d954665a879a5ce56b012f7c9&sid[]=fbdaf97d954665a879a5ce56b012f7c9&uid[]=117261794&uid[]=117261794&visitor_uid[]=117261794&visitor_uid[]=117261794&time[]=1587375765&time[]=1587375765&timezone[]=8&timezone[]=8&channels[]=200001&channels[]=200001&type[]=2&type[]=2&deviceId[]=eff881a9ef0239c36b855fb5bce1023fbe57d561&deviceId[]=eff881a9ef0239c36b855fb5bce1023fbe57d561&sign[]=08456ae9ab2a23a759f7ecab86431843&sign[]=9840a13d9798daca44d91ad60eee938e
可以看到接口里面拼接的uid、sid等参数后面多了一个[],这个问题是怎么出现的呢?
首先,h5这边请求接口时的逻辑如下:
通过请求库axios来发起请求,其中query为该次请求需要拼接到接口后面的参数。
query的值是由客户端拼接到h5链接后面的参数解析之后得到,由于7.15版本之前ios在训练营详情页面对应的参数拼接了两遍的原因(抓到的链接:https://node.dailyyoga.com.cn/o2_detail/?session_id=760&dy=1&app=1&app=1&version=7.13.1.0&version=7.13.1.0&sid=b4f6ddcd51a206c189386cf6643ad686&sid=b4f6ddcd51a206c189386cf6643ad686&uid=89560032&uid=89560032&visitor_uid=89560032&visitor_uid=89560032&time=1587378408&time=1587378408&timezone=8&timezone=8&channels=200001&channels=200001&type=2&type=2&deviceId=eff881a9ef0239c36b855fb5bce1023fbe57d561&deviceId=eff881a9ef0239c36b855fb5bce1023fbe57d561&sign=e3c4c2f9bdfd15715485365bc8e9387f&sign=0faa69df1cbfa94d1a861942ba2ae7b0),导致这边query解析出来里面有些key值对应成了数组,就像这样:
{
session_id: '738',
dy: '1',
app: [1, 1],
uid: ['89560032', '89560032'],
sid: ['b4f6ddcd51a206c189386cf6643ad686', 'b4f6ddcd51a206c189386cf6643ad686'],
version: ['7.13.1.0', '7.13.1.0']
}
这个时候请求库在往url上拼接对应的参数的时候出现了问题,下面是请求库把参数拼接到url后面时的源码,这段代码生成了上面说的有问题的url请求链接:
/**
* Build a URL by appending params to the end
*
* @param {string} url The base of the url (e.g., http://www.google.com)
* @param {object} [params] The params to be appended
* @returns {string} The formatted url
*/
module.exports = function buildURL(url, params, paramsSerializer) {
/*eslint no-param-reassign:0*/
if (!params) {
return url;
}
var serializedParams;
if (paramsSerializer) {
serializedParams = paramsSerializer(params);
} else if (utils.isURLSearchParams(params)) {
serializedParams = params.toString();
} else {
var parts = [];
utils.forEach(params, function serialize(val, key) {
if (val === null || typeof val === 'undefined') {
return;
}
if (utils.isArray(val)) {
key = key + '[]'; // 这里是重点,参数里面如果有数组,对应的key后面就会加上[]
} else {
val = [val];
}
utils.forEach(val, function parseValue(v) {
if (utils.isDate(v)) {
v = v.toISOString();
} else if (utils.isObject(v)) {
v = JSON.stringify(v);
}
parts.push(encode(key) + '=' + encode(v));
});
});
serializedParams = parts.join('&');
}
if (serializedParams) {
var hashmarkIndex = url.indexOf('#');
if (hashmarkIndex !== -1) {
url = url.slice(0, hashmarkIndex);
}
url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams;
}
return url;
};
然后,再来看看为啥这个query会被解析成这样子的。
训练营详情页面通过服务端渲染(vue框架集成的nuxt.js)重构,nuxt中提供了一个方法,用来在页面加载出来前异步获取数据。
async asyncData({ params, query, error, $axios, req, res })
其中query参数是nuxt自己提供的,跟vue获取query的方法一样(这是vue处理query的源码地址:https://github.com/vuejs/vue-router/blob/dev/src/util/query.js),下面是其中一段核心代码:
function parseQuery (query: string): Dictionary {
const res = {}
query = query.trim().replace(/^(\?|#|&)/, '')
if (!query) {
return res
}
query.split('&').forEach(param => {
const parts = param.replace(/\+/g, ' ').split('=')
const key = decode(parts.shift())
const val = parts.length > 0 ? decode(parts.join('=')) : null
if (res[key] === undefined) {
res[key] = val
} else if (Array.isArray(res[key])) {
res[key].push(val) // 如果有多于两个以上相同的key的参数,会继续往数组里面push
} else {
res[key] = [res[key], val] // 如果对象里面已经有了这个key值,是这个时候还有一个
// 相同的key值,此时该key对应的value就会变成数组形式
}
})
return res
}
训练营项目重构前,解析链接里面的参数是我们自己写的方法,对链接中有相同的参数会做去重处理,重复的key会取链接中最后一个的值。
DY.getUrlQueryObj = function (url) {
url = url == null ? window.location.href : url;
var search = url.substring(url.lastIndexOf("?") + 1);
var obj = {};
var reg = /([^?&=]+)=([^?&=]*)/g;
search.replace(reg, function (rs, $1, $2) {
var name = decodeURIComponent($1);
var val = decodeURIComponent($2);
obj[name] = val;
return rs;
});
return obj;
};
解决办法:
重构的项目中,在使用集成好的query之前,先做一遍去重,再使用。
let queryNew = {};
let keyLength = Object.keys(query);
for(let i = 0; i < keyLength.length; i++) {
// 处理链接里面参数重复拼接 去重
let isArray = query[keyLength[i]] instanceof Array;
if(isArray && query[keyLength[i]].length > 1) {
queryNew[keyLength[i]] = query[keyLength[i]][0];
} else {
queryNew[keyLength[i]] = query[keyLength[i]];
}
}
总结:框架在给我们的开发带来便利的同时,也埋下了一些我们无法预估的坑,后面的开发过程中还是需要多去关注一些框架底层的实现机制,多看源码,不断学习,不断提升。