【玩转掘金】 我的赞,我的💗,都给了谁,有你吗?

3,378 阅读3分钟

这是我参与8月更文挑战的第18天,活动详情查看:8月更文挑战

前言

玩转掘金 系列,对掘金扩展一些有意思的功能,比如:

某天进入个人主页一看,自己已经给别人点赞过了,我滴个神。
作为新人,互相支持一下,也算正常,我的这些💗,都给了谁呢,追寻真像,自己动手实现一个吧。

效果演示

详情列表是可以点击进入文章页面的
stars.gif

源码地址

源码地址:JJMyStarAndC

后端服务采用nodejs编写,你需要安装对应的安装包。

实现思路

可行的思路:

  1. Chrome插件 + 游猴脚本
  2. 静态页面 + nginx代理
  3. 静态页面 + 服务转发
  4. ......

本文的思路是 静态页面 + nodejs自定义服务,
纯属为了好玩额外用了 Server Sent Events , 简称SSE,服务器端单边推送,其作用是服务端获取数据后,单边推动给客服端。

有人可能会说,咋不用 socket.io, 我这里额外说一下, socket.io号称socket中的jQuery, 这个意思,大家懂了吧,而且还要配套其服务端库一起使用,讨厌!!

更好的实现方案:透传即可
express + http-proxy-middleware

实现细节

数据获取

一切均围绕数据展开,没有数据,别画饼,别谈理想。

对于Star获取,一个接口足以!

请求地址: api.juejin.cn/interact_ap…
请求方式: post
请求参数:
其有没有隐藏的limit参数选项呢? 大家可以尝试一波!

 {
    cursor: `${cursor}`,  // 起始查询位置
    item_type: 2,    // 文章点赞,对应有沸点点赞
    sort_type: 2,   // 排序,有近导员
    user_id: uid, // 用户id
  }

返回结果:

{
   has_moretrue,
   data: [{
       author_user_info:{
           user_id"3465271329953806", // 用户ID
           user_name"小魔童哪吒" // 用户名
       }, 
       article_info: {
           article_id: "6996484371305725965"  // 文章ID
           title"k8s 学习二"  // 文章标题
       }
   }]
}

请求的时候, cursor参数非常重要,表示指针位置,从哪继续往后走。
返回结果里面,很重要的字段has_more, 表示还没有数据,如果有,继续获取。

掘金很多的接口,起初没做限制的,当初我记得某些接口可以传入limit参数为1000,也可以,后来做了改进,值得赞一波。

其循环获取核心代码:

 while (res.has_more) {
    data.cursor = `${cursor}`
    
    res = (await axios.default.post(url, data, {
      headers
    })).data;
    
    cursor += 10;
    await delay(undefined, 16).run();  // 暂停16ms, 故意的
  }

我们说了,这里是服务端获取了数据,还没推动到前端。

数据推送

nodejs端使用SSE,也很简单,我这里就没有使用三方库了。

  1. request传了uid,rid两个参数 uid表示用户ID, rid表示requestId
  2. getStars 表示开始查询
  3. SSE的核心就在于Content-TypeConnection
    'Content-Type': 'text/event-stream'申明类型
    'Connection': 'keep-alive'表示不关闭连接
  4. 因为这里也属于一个请求,所以我们借助事件中心来派发事件给请求,请求再回写数据
app.get('/sseStream', function (request, response) {
  response.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive'
  });

  const { uid, rid } = request.query;
  console.log("uid:", uid);

  // 查询用户的点赞
  getStars(uid, rid);

  // 产线可别这么用
  eventsCenter.removeAllListeners("ssePush");
  // 事件中心
  eventsCenter.on("ssePush", function (event, data) {
    // console.log("push message to clients");
    response.write("event: " + String(event) + "\n" + "data: " + JSON.stringify(data) + "\n\n");

  });
});

那事件是哪派发出来的,就是数据获取那里派发的,我补上代码, 这里多了两种外的事件messageTotal, messageEnd, 一个是总数消息,有个是表示请求结束的消息。

async function getStars(uid, rid) {
  let cursor = 0
  const data = {
    cursor: `${cursor}`,
    item_type: 2,
    sort_type: 2,
    user_id: uid,
  }
  let res = {
    has_more: true
  };

  while (res.has_more) {
    data.cursor = `${cursor}`

    res = (await axios.default.post(url, data, {
      headers
    })).data;

    console.log("res:", data, res)

    eventsCenter.emit("ssePush", "messageTotal", {
      uid,
      rid,
      count: res.count
    });
    eventsCenter.emit("ssePush", "message", {
      uid,
      rid,
      datas: (res.data || []).map(d => ({
        user_id: d.author_user_info.user_id,
        user_name: d.author_user_info.user_name,
        title: d.article_info.title
      }))
    });
    cursor += 10;
    await delay(undefined, 16).run();
  }
  eventsCenter.emit("ssePush", "messageEnd", {
    uid,
    rid
  })
}

前端数据获取

前台对应监听事件就好了, 就这么简单。

    const source = new EventSource(`/sseStream?uid=${uid}&rid=${rid}`);

    // 收到新数据
    source.addEventListener('message', function (e) {
        let data = JSON.parse(e.data)
        // 不是需要的数据
        if (data.uid != uid || data.rid != rid) {
            return;
        }

        listArr.push(...data.datas);
        // console.log("listArr:", listArr);
        renderList(listArr);
        gotStarsEl.innerHTML = listArr.length;

    }, false)

    // 收到总数据消息
    source.addEventListener('messageTotal', function (e) {
        let data = JSON.parse(e.data)

        // 不是需要的数据
        if (data.uid != uid || data.rid != rid) {
            return;
        }
        totalStarsEl.innerHTML = data.count;
    }, false)

    // 统计完毕
    source.addEventListener('messageEnd', function (e) {
        let data = JSON.parse(e.data)
        console.log("meesage", data);
    }, false)

统计和分组

  1. 统计: 以用户ID为key,没有则新建,有则修改基数。
  2. 对keys进行map转数组,然后sort。

就这么简单!

    const statObj = list.reduce((obj, cur) => {
        if (hasOwnProperty.call(obj, cur.user_id)) {
            obj[cur.user_id].count += 1;
            obj[cur.user_id].items.push(cur);
        } else {
            obj[cur.user_id] = {
                items: [cur],
                count: 1,
                ...cur
            };
        }
        return obj;
    }, {});

    // 分组
    const groupList = Object
        .keys(statObj)
        .map(k => statObj[k])  //分组
        .sort((a, b) => a.count > b.count ? -1 : 1);  // 排序

更多的实现细节,请移步源码。

写在最后

3-5分钟,500-1000字,有所得,而不为所累,如果你觉得不错,你的一赞一评就是我前行的最大动力。

技术交流群请到 这里来。 或者添加我的微信 dirge-cloud,一起学习。