JavaScript 调用 JSON API 实战

前言

最近很久都没有练习自己写 JS 脚本的能力了,刚好在我的博客中需要读取用户的IP等信息,本想着网上直接拿一个 API 调用就好,但是很多 API 不是数据收集参数不够多,就是只允许企业和付费用户调用,好不容意找到个很好的 API, 但是其数据返回的全是英文。最后艰难选择了最后一个全是英文的API, 自己写脚本好了。当然考虑的还不止这些,包括异常处理,sessionStorage 减少接口调用等等优化方法,逐步完善了这个脚本。

接下来我就分享一下写整个脚本的经过。

代码实现

获取 API

我选取的是 IPINFO 的 API, 该 API 需要注册,注册后在主页就可以拿到自己的token,如图所示:

* 为防止 API 被第三方滥用,请在 IP Info 相关设置中开启白名单。

该网站的免费用户每月可以获得 50,000 次免费的请求量,对于我这种小博客来说是足够了,需要更大的请求量也可以升级套餐,但是价格过高( $99/month )

调用 API

  1. 首先我们初始化 API 地址:

    // your_token 的位置填入自己的 token
    var apiUrl = 'https://ipinfo.io/json?token=your_token';
  2. 当我们接受 API 返回的 JSON 数据时用 fetch() 函数接收,正常情况下我们就可以直接使用数据了,但是若遇到特殊情况,比如说 API 调用失败,我们需要抛出异常并进行异常处理。

    代码实现:

    fetch(apiUrl)
    .then(response=>{
    if(!response.ok){
    // 抛出异常
    throw new Error('Can\'t Fetch API Url.');
    }
    return response.json();
    })
    .then(data=>{
    // 这里是使用数据
    writeTable(data);
    })
    .catch(error => {
    // 如果出现异常,我们将返回 null 以告知函数没有读取到数据
    writeTable(null);
    // 并在终端返回错误信息
    console.error('There was a problem with your fetch operation:', error);
    });
  3. 这样的代码看似很完美,但是我们简单测试一下就暴露了一个问题,每当我们访问一次这个页面时,该方法就被调用了一次,API 的使用次数就会减一,这让我们本不富裕的请求量更雪上加霜,那我们有什么方法可以优化一下呢?

    优化方法大致分为两种:

    • 后端存储数据
    • 前端 sessionStorage 或 localStorage

    由于我们是纯静态页面,故选择用第二种方法,那到底是用 sessionStorage 还是 localStorage?

    sessionStorage 的特点是当我们的数据存入 sessionStorage 后,会缓存在当前标签页中,当标签页被关闭时,数据随机销毁。 localStorage 的特点是数据会永久存储在浏览器的 cache 中,即使关闭标签页和窗口,数据也不会丢失。

    那么根据我们获取用户 IP 的这个需求来看,IP 的时效性很强,换一个 Wi-Fi 就会出现变化,所以我们应该选取 sessionStorage 临时存储信息。

    代码实现:

    // 从 sessionStorage 读取数据
    const storedFormData = sessionStorage.getItem('fromData');
    // 如果 sessionStorage 存在数据则优先使用 sessionStorage 的数据
    if (storedFormData) {
    const formData = JSON.parse(storedFormData);
    writeTable(formData);
    } else {
    fetch(apiUrl)
    .then(response=>{
    if(!response.ok){
    throw new Error('Can\'t Fetch API Url.');
    }
    return response.json();
    })
    .then(data=>{
    writeTable(data);
    // 当成功读取数据时,将数据存入 sessionStorage
    sessionStorage.setItem('fromData', JSON.stringify(data));
    })
    .catch(error => {
    writeTable(null);
    console.error('There was a problem with your fetch operation:', error);
    });
    }
  4. 我们离完美的代码又近了一步,但是有些朋友可能就会问:我用的 PJAX,无加载的情况下每个页面都会执行这个函数,但是又因为部分变量在别的页面根本不存在就会报错怎么办?

    事实上,JS BOM 中有个非常好用的方法 window.location.pathname, 该方法会返回当前页面的路径,那么我们只需要判断一下该页面是否为需要执行该 JS 脚本的页面就好了。

    代码实现:

    // 封装成一个函数
    function loadTableData(){
    // 读取当前页路径
    const currentPagePath = window.location.pathname;

    // 判断是否为当前页,是则执行
    if (currentPagePath === '/privacy/') {
    const storedFormData = sessionStorage.getItem('fromData');
    if (storedFormData) {
    const formData = JSON.parse(storedFormData);
    writeTable(formData);
    } else {
    fetch(apiUrl)
    .then(response=>{
    if(!response.ok){
    throw new Error('Can\'t Fetch API Url.');
    }
    return response.json();
    })
    .then(data=>{
    writeTable(data);
    sessionStorage.setItem('fromData', JSON.stringify(data));
    })
    .catch(error => {
    writeTable(null);
    console.error('There was a problem with your fetch operation:', error);
    });
    }
    }
    };
  5. 近乎完美!我们只要进行最后一步,在网页加载成功后执行该脚本就成功了!

    // 在刷新后执行
    $(()=>{loadTableData()});
    // 在 pjax 执行完成后执行,防止换页后数据丢失
    $(document).on('pjax:complete', ()=>{loadTableData()});

写入数据

在我们拿到数据后,要进行数据利用,如上文所示,我们调用了 writeTable() 方法使用了数据,现在我们就要对 writeTable() 进行实现。

  1. 首先我们简单的写入下数据,JSON的结构直接对应代码结构,比如我们初始化一组数据:

    const jsonData = {
    "ip": "14.198.50.29",
    "data": {
    "hostname": "014198050029.ctinets.com",
    }
    };

    那么取出 hostname 的方法为:

    console.log(jsonData.data.hostname);

    接下来我们简单实现下 API 的读取:

    function writeTable(data){
    $('#userAgentCountry').html(data.country);
    $('#userAgentIp').html(data.ip);
    $('#userAgentRegion').html(data.region);
    $('#userAgentCity').html(data.city);
    $('#userAgentIsp').html(data.org);

    // 我们直接用内置的方法获取到用户的 UA 信息
    $('#userAgentDevice').html(navigator.userAgent);
    }
  2. 我们把抛出异常时的情况进行处理

    function writeTable(data){
    if (data === null){
    $('#userAgentCountry').html('无法获取信息');
    $('#userAgentIp').html('无法获取信息');
    $('#userAgentRegion').html('无法获取信息');
    $('#userAgentCity').html('无法获取信息');
    $('#userAgentIsp').html('无法获取信息');
    $('#userAgentDevice').html(navigator.userAgent);
    } else {
    $('#userAgentCountry').html(data.country);
    $('#userAgentIp').html(data.ip);
    $('#userAgentRegion').html(data.region);
    $('#userAgentCity').html(data.city);
    $('#userAgentIsp').html(data.org);
    $('#userAgentDevice').html(navigator.userAgent);
    }
    }
  3. 但是我们执行了如此多的 DOM 选择器,为了节省内存,我们初始化他们并直接调用初始化好的选择器

    function writeTable(data){
    var $userAgentIp = $('#userAgentIp'),
    $userAgentCountry = $('#userAgentCountry'),
    $userAgentRegion = $('#userAgentRegion'),
    $userAgentCity = $('#userAgentCity'),
    $userAgentIsp = $('#userAgentIsp'),
    $userAgentDevice = $('#userAgentDevice');
    if (data === null){
    $userAgentIp.html('无法获取信息');
    $userAgentCountry.html('无法获取信息');
    $userAgentRegion.html('无法获取信息');
    $userAgentCity.html('无法获取信息');
    $userAgentIsp.html('无法获取信息');
    $userAgentDevice.html(navigator.userAgent);
    } else {
    $userAgentIp.html(data.ip);
    $userAgentCountry.html(data.country);
    $userAgentRegion.html(data.region);
    $userAgentCity.html(data.city);
    $userAgentIsp.html(data.org);
    $userAgentDevice.html(navigator.userAgent);
    }
    }

格式化 ISO 国家代码

因为默认的国家代码为二位英文码,故我们需要将其转换成中文。

function countryCodeToName(countryCode) {

// ISO 国家代码和国家名称的映射
var countryNames = {
"AF": "阿富汗",
"AX": "奥兰群岛",
"AL": "阿尔巴尼亚",
//...
"ZW": "津巴布韦"
};

// 返回对应国家代码的国家名称,如果不存在则返回国家代码
return countryNames[countryCode] || countryCode;
}

然后在 writeTable() 方法中调用就可以啦

function writeTable(data){
var $userAgentIp = $('#userAgentIp'),
$userAgentCountry = $('#userAgentCountry'),
$userAgentRegion = $('#userAgentRegion'),
$userAgentCity = $('#userAgentCity'),
$userAgentIsp = $('#userAgentIsp'),
$userAgentDevice = $('#userAgentDevice');
if (data === null){
$userAgentIp.html('无法获取信息');
$userAgentCountry.html('无法获取信息');
$userAgentRegion.html('无法获取信息');
$userAgentCity.html('无法获取信息');
$userAgentIsp.html('无法获取信息');
$userAgentDevice.html(navigator.userAgent);
} else {
countryName = countryCodeToName(data.country);
$userAgentIp.html(data.ip);
$userAgentCountry.html(countryName);
$userAgentRegion.html(data.region);
$userAgentCity.html(data.city);
$userAgentIsp.html(data.org);
$userAgentDevice.html(navigator.userAgent);
}
}

完整代码展示

var apiUrl = 'https://ipinfo.io/json?token=your_token';

function loadTableData(){
const currentPagePath = window.location.pathname;
if (currentPagePath === '/privacy/') {
const storedFormData = sessionStorage.getItem('tableData');
if (storedFormData) {
const formData = JSON.parse(storedFormData);
writeTable(formData);
} else {
fetch(apiUrl)
.then(response=>{
if(!response.ok){
throw new Error('Can\'t Fetch API Url.');
}
return response.json();
})
.then(data=>{
writeTable(data);
sessionStorage.setItem('tableData', JSON.stringify(data));
})
.catch(error => {
writeTable(null);
console.error('There was a problem with your fetch operation:', error);
});
}
}
};

$(()=>{loadTableData()});
$(document).on('pjax:complete', ()=>{loadTableData()});

function writeTable(data){
var $userAgentIp = $('#userAgentIp'),
$userAgentCountry = $('#userAgentCountry'),
$userAgentRegion = $('#userAgentRegion'),
$userAgentCity = $('#userAgentCity'),
$userAgentIsp = $('#userAgentIsp'),
$userAgentDevice = $('#userAgentDevice');
if (data === null){
$userAgentIp.html('无法获取信息');
$userAgentCountry.html('无法获取信息');
$userAgentRegion.html('无法获取信息');
$userAgentCity.html('无法获取信息');
$userAgentIsp.html('无法获取信息');
$userAgentDevice.html(navigator.userAgent);
} else {
countryName = countryCodeToName(data.country);
$userAgentIp.html(data.ip);
$userAgentCountry.html(countryName);
$userAgentRegion.html(data.region);
$userAgentCity.html(data.city);
$userAgentIsp.html(data.org);
$userAgentDevice.html(navigator.userAgent);
}
}

function countryCodeToName(countryCode) {

// ISO 国家代码和国家名称的映射
var countryNames = {
"AF": "阿富汗",
"AX": "奥兰群岛",
"AL": "阿尔巴尼亚",
//...
"ZW": "津巴布韦"
};

// 返回对应国家代码的国家名称,如果不存在则返回国家代码
return countryNames[countryCode] || countryCode;
}

附录:ISO 国家代码中文对照函数

var countryNames = {
"AF": "阿富汗",
"AX": "奥兰群岛",
"AL": "阿尔巴尼亚",
"DZ": "阿尔及利亚",
"AS": "美属萨摩亚",
"AD": "安道尔",
"AO": "安哥拉",
"AI": "安圭拉",
"AQ": "南极洲",
"AG": "安提瓜和巴布达",
"AR": "阿根廷",
"AM": "亚美尼亚",
"AW": "阿鲁巴",
"AU": "澳大利亚",
"AT": "奥地利",
"AZ": "阿塞拜疆",
"BS": "巴哈马",
"BH": "巴林",
"BD": "孟加拉国",
"BB": "巴巴多斯",
"BY": "白俄罗斯",
"BE": "比利时",
"BZ": "伯利兹",
"BJ": "贝宁",
"BM": "百慕大",
"BT": "不丹",
"BO": "玻利维亚",
"BA": "波斯尼亚和黑塞哥维那",
"BW": "博茨瓦纳",
"BV": "布维岛",
"BR": "巴西",
"IO": "英属印度洋领地",
"BN": "文莱",
"BG": "保加利亚",
"BF": "布基纳法索",
"BI": "布隆迪",
"KH": "柬埔寨",
"CM": "喀麦隆",
"CA": "加拿大",
"CV": "佛得角",
"KY": "开曼群岛",
"CF": "中非共和国",
"TD": "乍得",
"CL": "智利",
"CN": "中国",
"CX": "圣诞岛",
"CC": "科科斯(基林)群岛",
"CO": "哥伦比亚",
"KM": "科摩罗",
"CG": "刚果(布)",
"CD": "刚果(金)",
"CK": "库克群岛",
"CR": "哥斯达黎加",
"CI": "科特迪瓦",
"HR": "克罗地亚",
"CU": "古巴",
"CY": "塞浦路斯",
"CZ": "捷克共和国",
"DK": "丹麦",
"DJ": "吉布提",
"DM": "多米尼克",
"DO": "多米尼加共和国",
"EC": "厄瓜多尔",
"EG": "埃及",
"SV": "萨尔瓦多",
"GQ": "赤道几内亚",
"ER": "厄立特里亚",
"EE": "爱沙尼亚",
"ET": "埃塞俄比亚",
"FK": "福克兰群岛(马尔维纳斯)",
"FO": "法罗群岛",
"FJ": "斐济",
"FI": "芬兰",
"FR": "法国",
"GF": "法属圭亚那",
"PF": "法属波利尼西亚",
"TF": "法属南部领地",
"GA": "加蓬",
"GM": "冈比亚",
"GE": "格鲁吉亚",
"DE": "德国",
"GH": "加纳",
"GI": "直布罗陀",
"GR": "希腊",
"GL": "格陵兰",
"GD": "格林纳达",
"GP": "瓜德罗普",
"GU": "关岛",
"GT": "危地马拉",
"GG": "格恩西岛",
"GN": "几内亚",
"GW": "几内亚比绍",
"GY": "圭亚那",
"HT": "海地",
"HM": "赫德岛和麦克唐纳群岛",
"VA": "梵蒂冈",
"HN": "洪都拉斯",
"HK": "中国香港特别行政区",
"HU": "匈牙利",
"IS": "冰岛",
"IN": "印度",
"ID": "印度尼西亚",
"IR": "伊朗",
"IQ": "伊拉克",
"IE": "爱尔兰",
"IM": "马恩岛",
"IL": "以色列",
"IT": "意大利",
"JM": "牙买加",
"JP": "日本",
"JE": "泽西岛",
"JO": "约旦",
"KZ": "哈萨克斯坦",
"KE": "肯尼亚",
"KI": "基里巴斯",
"KP": "朝鲜",
"KR": "韩国",
"KW": "科威特",
"KG": "吉尔吉斯斯坦",
"LA": "老挝",
"LV": "拉脱维亚",
"LB": "黎巴嫩",
"LS": "莱索托",
"LR": "利比里亚",
"LY": "利比亚",
"LI": "列支敦士登",
"LT": "立陶宛",
"LU": "卢森堡",
"MO": "中国澳门特别行政区",
"MK": "马其顿",
"MG": "马达加斯加",
"MW": "马拉维",
"MY": "马来西亚",
"MV": "马尔代夫",
"ML": "马里",
"MT": "马耳他",
"MH": "马绍尔群岛",
"MQ": "马提尼克",
"MR": "毛里塔尼亚",
"MU": "毛里求斯",
"YT": "马约特",
"MX": "墨西哥",
"FM": "密克罗尼西亚",
"MD": "摩尔多瓦",
"MC": "摩纳哥",
"MN": "蒙古",
"ME": "黑山",
"MS": "蒙特塞拉特",
"MA": "摩洛哥",
"MZ": "莫桑比克",
"MM": "缅甸",
"NA": "纳米比亚",
"NR": "瑙鲁",
"NP": "尼泊尔",
"NL": "荷兰",
"AN": "荷属安的列斯",
"NC": "新喀里多尼亚",
"NZ": "新西兰",
"NI": "尼加拉瓜",
"NE": "尼日尔",
"NG": "尼日利亚",
"NU": "纽埃",
"NF": "诺福克岛",
"MP": "北马里亚纳群岛",
"NO": "挪威",
"OM": "阿曼",
"PK": "巴基斯坦",
"PW": "帕劳",
"PS": "巴勒斯坦",
"PA": "巴拿马",
"PG": "巴布亚新几内亚",
"PY": "巴拉圭",
"PE": "秘鲁",
"PH": "菲律宾",
"PN": "皮特凯恩群岛",
"PL": "波兰",
"PT": "葡萄牙",
"PR": "波多黎各",
"QA": "卡塔尔",
"RE": "留尼汪",
"RO": "罗马尼亚",
"RU": "俄罗斯",
"RW": "卢旺达",
"SH": "圣赫勒拿",
"KN": "圣基茨和尼维斯",
"LC": "圣卢西亚",
"PM": "圣皮埃尔和密克隆",
"VC": "圣文森特和格林纳丁斯",
"WS": "萨摩亚",
"SM": "圣马力诺",
"ST": "圣多美和普林西比",
"SA": "沙特阿拉伯",
"SN": "塞内加尔",
"RS": "塞尔维亚",
"SC": "塞舌尔",
"SL": "塞拉利昂",
"SG": "新加坡",
"SK": "斯洛伐克",
"SI": "斯洛文尼亚",
"SB": "所罗门群岛",
"SO": "索马里",
"ZA": "南非",
"GS": "南乔治亚岛和南桑威奇群岛",
"ES": "西班牙",
"LK": "斯里兰卡",
"SD": "苏丹",
"SR": "苏里南",
"SJ": "斯瓦尔巴特和扬马延",
"SZ": "斯威士兰",
"SE": "瑞典",
"CH": "瑞士",
"SY": "叙利亚",
"TW": "中国台湾",
"TJ": "塔吉克斯坦",
"TZ": "坦桑尼亚",
"TH": "泰国",
"TL": "东帝汶",
"TG": "多哥",
"TK": "托克劳",
"TO": "汤加",
"TT": "特立尼达和多巴哥",
"TN": "突尼斯",
"TR": "土耳其",
"TM": "土库曼斯坦",
"TC": "特克斯和凯科斯群岛",
"TV": "图瓦卢",
"UG": "乌干达",
"UA": "乌克兰",
"AE": "阿联酋",
"GB": "英国",
"US": "美国",
"UM": "美国本土外小岛屿",
"UY": "乌拉圭",
"UZ": "乌兹别克斯坦",
"VU": "瓦努阿图",
"VE": "委内瑞拉",
"VN": "越南",
"VG": "英属维尔京群岛",
"VI": "美属维尔京群岛",
"WF": "瓦利斯和富图纳",
"EH": "西撒哈拉",
"YE": "也门",
"ZM": "赞比亚",
"ZW": "津巴布韦"
};

Troubleshooting

  1. 注意拼写错误,我把 form 拼成 from 了(乐
  2. fetch() 方法仅允许 WEB API,无法直接将自己初始化的 JSON 变量传进去
  3. 确定好 sessionStorage 的逻辑顺序,先存再取
  4. token 一定要 设置域名白名单

结语

多位大厨正在努力烹饪,真是一场酣畅淋漓的 API 调用啊(不是