现在很多带有社交性质的应用都陆续的添加了显示地区的功能。为什么要公布属地呢?
其实主要目的还是为了净化网络空间,减少网暴的或者谣言的出现。
那么如何判断用户的地址呢?
答:IP。
不同的IP 都有不同的属地,这些具体的属地信息一般由运营商来负责维护,也有部分组织或者个人收集维护了IP 对应地理位置的数据库供开发者付费或者免费使用(因为维护一个如此庞大的的数据库会耗费比较大的精力,所以大部分产品都是收费的)。
本文演示所用的 ipdb 是 ipip.net 提供的试用版,试用版比起付费版会少很多功能,我们只用来学习使用方式足够了(商用一定要选择付费版本,否则可能会律师函警告⚠️)。
由于 ipip 的 SDK 文档几乎是没有,所以我记录得详细一点 ipdb 文件和代码示例已经上传到文末 github,有需要可以去下载试用。
核心使用
因为我是个切图仔,所以我选择用 nodejs,我们需要需要下载 ipip 对应的nodejs SDK(全部语言 SDK可以去 github 自行查找)。
$ npm install ipip-ipdb 复制代码
然后编写 JS 脚本来解析,github 的文档基本没有,我在测试文件中发现的这几个 API
import { fileURLToPath } from 'node:url' import { dirname, resolve } from 'node:path' import ipdb from 'ipip-ipdb' const __dirname = dirname(fileURLToPath(import.meta.url)) const client = new ipdb.City(resolve(__dirname, '../db/ipipfree.ipdb')) function parseIP(ip) { console.log(client.findMap(ip, 'CN')) console.log(client.findInfo(ip, 'CN')) } 复制代码
ipip-ipdb 最核心的 API 是一个 City 构造函数,可以创建一个实例。该实例有两个方法,findMap 和 findInfo,findMap 会返回国家、省份、城市的一个 Map 结构,findInfo 会返回一个包含多个信息的对象(试用版只能看到国家、省份、城市),内容如下
总体来说原理就是通过他们维护的 ipdb 文件来查询 ip 的地理位置之间的映射关系,因为 IP 会发生变动,所以维护起来比较麻烦,而且试用版的更新频率也不确定,想要正常商用此功能还是要付费订阅比较好。
锦上添花
核心内容到上面就结束了,后面基础内容较多,可以顺便复习一下 koa 的使用。
解析 IP 的工作一般是由后端来完成,所以我们需要搭建一个服务端程序。因为只演示这一个小功能,我们就不去使用那些花里胡哨的框架了,就用最简洁的 koa(或者你想用express等其他的都可以),页面渲染也直接使用模板渲染。
安装 koa 用到的中间件
$ npm install koa @koa/router koa-views ejs 复制代码
准备工作完成之后开始上才艺。还是经典的三段式,在根目录下新建 server.js
import Koa from 'koa' import Router from '@koa/router' const app = new Koa() const router = new Router() app.use(router.routes()) app.listen(4000, () => { console.log('IP 查询服务启动: 4000'); }) 复制代码
然后编写路由,并通过ejs模板引擎进行渲染
import views from 'koa-views' import { fileURLToPath } from 'node:url' import { dirname } from 'node:path' const __dirname = dirname(fileURLToPath(import.meta.url)) app.use(views(__dirname + '/views', { extension: 'ejs' })) router.get('/', async (ctx) => { await ctx.render('index', {ip: ''}) // 参数用于渲染内容,默认填入空 }) 复制代码
模板引擎加载完成下一步开始编写模板,在 views 目录下新建index.ejs 文件并添加如下代码(省略了 style代码),逻辑控制部分的变量都是通过findInfo方法解析 IP 之后填入的数据。
<body> <script> window.onload = () => { // 加载完成时添加事件绑定 const search = document.getElementById("search"); search.addEventListener("keydown", (e) => { if(e.code === 'Enter') { location.search = '?ip=' + search.value } }) } </script> <div class="container"> <input id="search" class="search" placeholder="请输入 IP 地址" value="<%- ip %>" /> <% if(locals.msg) { %> <p class="error-msg"><%- msg %> </p> <% } else if(locals.res) { %> <ul> <% if(res.countryName) { %> <li><%- res.countryName %> </li> <% } %> <% if(res.regionName) { %> <li><%- res.regionName %> </li> <% } %> <% if(res.cityName) { %> <li><%- res.cityName %> </li> <% } %> </ul> <% } %> </div> </body> 复制代码
此时我们的渲染还没有传入数据,再回来完善一下这部分逻辑。执行过程大致为:判断 IP 是否传入,没有传入渲染空页面,有 IP 的话进行正则校验,格式错误报错,否则渲染解析后的数据。
router.get('/', async (ctx) => { const { ip } = ctx.request.query if (!ip) { // 没有 ip 渲染空页面 await ctx.render('index', {ip: ''}) return } if (!isIP(ip)) { // 不合法 IP 报错 await ctx.render('index', { ip, msg: 'IP格式错误' }) } else { // 检索省市区 const res = parseIP(ip) // 返回带数据的页面 await ctx.render('index', {ip, res}) } }) 复制代码
此时的效果如下,比之前在控制台中执行要舒服一些
再提供一个接口供 ajax 请求使用,直接返回解析的数据即可
router.get('/json', async (ctx) => { const { ip } = ctx.request.query if (!ip) { ctx.status = 400 ctx.body = errorFormat(40001, 'IP 不能为空') return } if (!isIP(ip)) { ctx.status = 400 ctx.body = errorFormat(40002, 'IP 格式错误') } else { const res = parseIP(ip) ctx.body = responseFormat(res) } }) 复制代码
代码已经上传 github,有需要可以自行拉取