现在很多带有社交性质的应用都陆续的添加了显示地区的功能。为什么要公布属地呢?

其实主要目的还是为了净化网络空间,减少网暴的或者谣言的出现。

那么如何判断用户的地址呢?

答: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 会返回一个包含多个信息的对象(试用版只能看到国家、省份、城市),内容如下

image-20230119120718189

总体来说原理就是通过他们维护的 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})
  }
})

此时的效果如下,比之前在控制台中执行要舒服一些

image-20230119122226144

再提供一个接口供 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,有需要可以自行拉取


前端小白