掘金插件

掘金插件 - 赞文章分类功能

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         掘金插件
// @namespace    https://github.com/zhukunpenglinyutong/Juejin-Plugin
// @version      1.0
// @description  掘金插件 - 赞文章分类功能
// @author       朱昆鹏
// @match        *://juejin.im/user/*
// @require https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js
// @grant        none
// ==/UserScript==

let globalData = null; // 全局数据

// 实现一个简单的路由监听(监听跳转是否是主页)
window.addEventListener('load', function (e) {
    var reg = /https:\/\/juejin.im\/user/;
    if (reg.test(e.target.URL)) init(); // 如果进入主页,触发
})


// ============================================================================================
// ===================== 初始化部分:创建 点赞文章分类,等待点击事件触发 ==============================
// ============================================================================================

function init() {
    //(异步是因为有时候掘金网站加载太慢)
    setTimeout(() => {
        createMoreItem(); // 创建 点赞文章分类 div
        eventBus(); // 点击事件监听(并执行拿取数据 和 创建展示DOM的方法)
    }, 500)
}

// 创建 点赞文章分类 div
function createMoreItem() {
    let selectItems = document.querySelectorAll('.nav-item.not-in-scroll-mode .more-panel .more-item')
    let newA = selectItems[1].cloneNode() // 赋值节点
    newA.attributes.removeNamedItem('href') // 删除掉href跳转,因为就当个触发器使用
    newA.id = 'jSort'
    newA.innerText = '文章分类'
    selectItems[1].parentNode.appendChild(newA)
}

// 管理事件的函数
function eventBus() {
    let jSort = document.getElementById('jSort')
    jSort.addEventListener('click', async event => {
        let dialogId = document.getElementById('dialogId')

        if (dialogId) { // 已经点击过分类查看了,只是隐藏到了,这里避免重复拿取,浪费性能
          dialogId.style.display = 'inline';
        } else {
          let dataReq = new DataReq()
          let userId = location.pathname.match(/\w{24}/)[0]
          let res = await dataReq.getData(userId) // 获取数据
          createDOM(res) // 创建展示数据的DOM层
        }

    })
}



// ============================================================================================
// =================== 数据处理部分:逻辑处理部分(请求数据 && 处理数据) =============================
// ============================================================================================

// axios配置
let instance = axios.create({
    baseURL: 'https://user-like-wrapper-ms.juejin.im/v1/user/',
    timeout: 30000,
    headers: { "X-Juejin-Src": "web" }
});

// 数据请求类
class DataReq {

    constructor () {
    }

    /**
     * 请求数据(对外开放)
     * @param {String} userId
     */
    getData(userId) {

        console.log('userID', userId)

        return new Promise (async (resolve, reject) => {
            // 因为现成的掘金接口,一次只能拿到30个数据,如果点赞很多文章的话,我们需要拼接很多次请求才行
            // 思考一:等一个请求返回后,再把page的值增加,直到返回数组为空,不再进行请求,这样是个串行的
            // ✅ 思考二:在发送请求之前,先发送一个 page=0&pageSize=0 这时会返回你有多少点赞的文章数量,根据数量这个就能获取到需要发送的次数,这个是并行的

            let that = this

            // 根据返回的数组,建立对应数量的axios请求
            let totalArr = await this.getTotal(userId)
            let aixosArr = totalArr.map(item => instance.get(item))


            axios.all(aixosArr)
                .then(axios.spread( function () {
                let arr = []
                for (let i = 0; i < arguments.length; i++) {
                    arr.push(arguments[i].data.d.entryList)
                }
                let resData = that.dataHandle(arr.flat())
                resolve(resData) // 经过处理类处理后的数据
            }));
        })

    }


    /**
     * 请求page=0&pageSize=0 结果用于生成多次axios请求(原因是掘金接口一次拿不到全部点赞数据,上限是30)
     * (内部使用)
     * @param { String } 用户ID
     * @return { Array } ['请求地址一', '', ...]
     */
    getTotal(userId) {
        return new Promise((resolve, reject) => {

            instance.get(`${userId}/like/entry?page=0&pageSize=0`)
                .then(res => {

                let len = Math.ceil(res.data.d.total/30),
                    arr = [];

                for (let i = 0; i < len; i++) {
                    arr.push(`${userId}/like/entry?page=${i}&pageSize=30`)
                }

                resolve(arr)
            })
                .catch(e => {
                reject(e)
            })

        })
    }


    /**
     * 数据处理函数(内部使用)(此处代码写的不好qwq)
     */
    dataHandle (data) {
        let countedNames = data
        .map( item => item.tags.map(jtem => jtem.title ) )
        .flat(Infinity)
        .reduce((allNames, name) => {
            if (name in allNames) {
                allNames[name]++;
            } else {
                allNames[name] = 1;
            }
            return allNames;
        }, {})
        // ['Vue.js', 'Vue.js', '...', ...] ===> [ { name: 'Vue.js', num: 6}, {'Jquery': 4}, {} ]

        let resArr = []
        for (let prop in countedNames) {
            resArr.push({
                name: prop,
                num: countedNames[prop],
                data: []
            })
        }

        resArr.sort((a, b) =>  b.num - a.num); // 按照数量排序

        // 第二部分:
        // 获取 所有值
        let aaa = data.map( item => {
            return {
                title: item.title,
                originalUrl: item.originalUrl,
                username: item.user.username,
                objectId: `https://juejin.im/user/${item.user.objectId}`,
                tags: item.tags.map( j => j.title )
            }
        })


        // 整成想要的格式
        resArr.forEach( item => {
            aaa.forEach( jtem => {
                jtem.tags.some( j => j === item.name) ? item.data.push(jtem) : ''
            })
        })

        return resArr
    }

}



// ============================================================================================
// ========================= DOM生成层,根据数据生成对应的弹出层和交互  =============================
// ============================================================================================

const createDOM = (res) => {
    globalData = res

    // 响应式监听属性,响应点击标签时候的视图切换
    let observeData = {
        select: '' // 当前选中的分类名称
    }

    renderBasic() // 渲染弹出框基本结构
    defineReactive(observeData, 'select', observeData.select) // 监听响应式
    observeData.select = res[0].name // 初始化,渲染第一个分类中的数据
    createDOMEventBus(observeData) // 启动事件监听
}

// 渲染弹出框基本结构
function renderBasic() {

    let dialogMianItem = '' // 根据数据,生成底部导航栏结构
    globalData.forEach( item => {
      dialogMianItem += `<div class="dialog-mian-item" style="position: relative;margin-right: 25px;">
                <button class="dialogBtn" style="padding: 9px 15px;font-size: 12px;border-radius: 3px;min-width: 100px;">${item.name}</button>
                <sup style="position: absolute;top: 0;right: 10px;transform: translateY(-50%) translateX(100%);background-color: #f56c6c;border-radius: 10px;color: #fff;display: inline-block;font-size: 12px;height: 18px;line-height: 18px;padding: 0 6px;text-align: center;white-space: nowrap;">
                  ${item.num}
                </sup>
              </div>`
    })


    // 创建弹出层
    let div = document.createElement('div')
    div.id = 'dialogId'
    // 模板中展示的内容
    div.innerHTML = `
      <div class="dialog"
        style="margin: 15vh auto 50px;width: 60vw;height: 600px;background: #fff;opacity: 1;border-radius: 2px;box-shadow: 0 1px 3px rgba(0,0,0,.3);box-sizing: border-box;">
        <div class="dialog-header" style="padding: 20px 20px 10px;line-height: 24px;font-size: 18px;color: #303133;margin-bottom: 15px;display: flex;justify-content: space-between;">
          <div>文章分类 <input style="margin-left: 10px;" disabled value="搜索功能8.1日开放" /> </div>
          <div id="dialog-close" style="cursor: pointer;">X</div>
        </div>
        <div class="dialog-mian">
          <div class="dialog-mian" style="padding: 0 22px;">
            <div class="dialog-mian-lang" style="display: flex;justify-content: space-between;padding-top: 12px;overflow-y: auto;">${dialogMianItem}</div>
            <div class="dialog-content" style="margin-top: 10px;height: 430px;overflow-y: auto;">

            </div>
          </div>
        </div>
      </div>
    `
    let body = document.querySelector('body')
    div.style = `position: fixed;height: 100vh;width: 100vw;top: 0;left: 0;z-index: 1000;`
    body.appendChild(div)
}

// 生成详细文章列表
function createDialogContent(selectName) {

  let randerData = globalData.filter( item => item.name === selectName)[0].data
  let dialogContent = document.querySelector('.dialog-content')

  let dialogContentText = ''; // 具体一行行内容
  // 当前点击展示的内容
  randerData.forEach(item => {
      let aStr = ''
      item.tags.forEach( tag => {
        aStr += `<a href="javascript:;">${tag} · </a>`
      })
      dialogContentText += `
                <div class="dialog-content_item" style="margin: 18px 0px;border-bottom: 1px solid rgba(178,186,194,.15);">
                   <div class="meta-row">
                      <a href="${item.objectId}">${item.username}</a> · ${aStr}
                   </div>
                   <div class="title" style="margin-top: 6px;"><a href="${item.originalUrl}" style="font-size: 16px;color: #2E3135;">${item.title}</a></div>
                </div>`
  })

  dialogContent.innerHTML = dialogContentText
}

// 事件总线
function createDOMEventBus (observeData) {
    // 点击切换事件(采用事件冒泡,进行捕获)
    let btns = document.querySelector('.dialog-mian-lang')
    btns.addEventListener('click', function (e) {
      console.log(e)
      if (e.target.nodeName === 'BUTTON') observeData.select = e.target.textContent;
    })

    // 点击关闭按钮
    let dialogClose = document.getElementById('dialog-close')
    let dialogId = document.getElementById('dialogId')
    dialogClose.addEventListener('click', function () {
        console.log('触发')
       dialogId.style.display = 'none'
    })
}


// ============================================================================================
// ========================== 实现简单数据监听,用来简化DOM操作(朱昆鹏)  ===========================
// ============================================================================================

// 数据劫持
function defineReactive (obj, key, value) {
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,

        get () {
           return value
        },

        set (newValue) {
           value = newValue
           createDialogContent(value) // 更新视图 (生成详细文章列表)
        }
    })
}