今天将今日掘友分油猴插件使用rollup+vue重写了。
具体的步骤我这里列一下:
新建workspace
git init
npm init
拆分文件
ProgressBar.js
今日掘友分进度组件。
// src/ProgressBar.js
import { getGrowthTaskList } from './utils/api';
const TaskDetailPopup = {
props: {
task: {
type: Array,
default() {
return [];
},
},
},
render(h) {
const { icon, title, score, done, limit } = this.task;
return h('div', { staticClass: 'task-detail-popup' }, [
h('div', { staticClass: 'task-icon' }, [
h('img', { attrs: { src: icon, alt: title } }),
]),
h('div', { staticClass: 'task-info' }, [
h('span', {}, title),
h('span', {}, `掘友分+${score},已完成${done}/${limit}`),
]),
]);
},
};
export default {
name: 'ProgressBar',
data() {
return {
// 今日已获掘友分
today_jscore: 0,
// 有完成次数的任务
limitTasks: [],
// 头部区域是否展示
headerVisible: true,
};
},
render(h) {
return h(
'div',
{ staticClass: 'pl-progressbar', class: { top: !this.headerVisible } },
[
// 进度条
h(
'div',
{ staticClass: 'pl-progressbar-container' },
this.limitTasks.map(task => {
const { done, limit, maxScore } = task;
// 任务进度
const taskProgressItems = [];
for (let i = 0; i < limit; i++) {
taskProgressItems.push(
h('div', {
staticClass: 'task-progress-item',
class: { 'item-done': i < done },
})
);
}
return h(
'div',
{
staticClass: 'task-item',
class: {
'all-finished': done === limit,
'not-start': done === 0,
},
// 按比例占据宽度
style: {
'flex-grow': maxScore,
},
on: {
// 点击任务跳转去完成
click() {
window.open(task.web_jump_url);
},
},
},
[
h(
'div',
{ staticClass: 'progress-item-wrapper' },
taskProgressItems
),
h(TaskDetailPopup, { props: { task } }),
]
);
})
),
// 总分
h(
'div',
{ staticClass: 'pl-progressbar-totalscore' },
`今日掘友分 ${this.today_jscore}`
),
]
);
},
mounted() {
this.getGrowthTaskList();
this.observeHeader();
},
methods: {
// 需要监听头部的展示与隐藏
observeHeader() {
const header = document.querySelector('header.main-header');
const observer = new MutationObserver(mutationsList => {
const [record] = mutationsList;
if (record.attributeName !== 'class') return;
this.headerVisible = header.classList.contains('visible');
});
observer.observe(header, {
attributes: true,
childList: false,
subtree: false,
});
},
getGrowthTaskList() {
getGrowthTaskList().then(data => {
Object.assign(this, data);
});
},
},
};
index.scss
组件样式以及对原页面的样式修改。
// src/index.scss
// 覆盖原有样式 === begin
// 取消header下部的阴影
.main-header {
box-shadow: none !important;
}
// 用户页面header下方出现list-header的sticky元素
.list-block .list-header.sticky {
top: calc(5rem + 12px) !important;
&.top {
top: 12px !important;
}
}
// 评论通知页 /notification
.view-nav {
top: calc(5rem + 12px) !important;
}
// 覆盖原有样式 === end
.pl-progressbar {
display: flex;
justify-content: space-between;
padding: 1px;
font-size: 12px;
height: 10px;
color: var(--juejin-brand-1-normal);
background: var(--juejin-navigation);
position: fixed;
top: 5rem;
left: 0;
right: 0;
z-index: 1249;
transition: transform .2s;
&.top {
transform: translate3d(0,-5rem,0);
}
&-container {
flex-grow: 1;
display: flex;
}
&-totalscore {
align-self: center;
padding: 0 20px;
}
}
body[data-theme="dark"] .pl-progressbar {
background: var(--juejin-navigation);
}
.task-item {
overflow: hidden;
display: flex;
cursor: pointer;
.progress-item-wrapper {
position: relative;
flex: 1;
display: flex;
&::before {
content: '';
border: 5px solid var(--juejin-brand-1-normal);
border-left: 3px solid var(--juejin-navigation);
position: absolute;
z-index: 1;
}
&::after {
content: '';
border: solid 5px var(--juejin-navigation);
border-left: 3px solid transparent;
position: absolute;
right: -5px;
}
}
.task-detail-popup {
position: absolute;
top: 12px;
display: none;
align-items: center;
background: var(--juejin-navigation);
padding: 5px 10px;
border-radius: 5px;
border: solid 1px;
z-index: 999;
.task-icon {
width: 40px;
height: 40px;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
}
}
.task-info {
margin-left: 10px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
}
&.all-finished {
.progress-item-wrapper::after {
border-left-color: var(--juejin-brand-1-normal);
}
}
&.not-start {
.progress-item-wrapper::before {
border-top-color: transparent;
border-bottom-color: transparent;
border-right-color: transparent;
}
}
&:hover {
.progress-item-wrapper {
opacity: .5;
}
.task-detail-popup {
display: flex;
}
}
.task-progress-item {
flex: 1;
background: #f2f3f5;
position: relative;
&.item-done {
background: var(--juejin-brand-1-normal);
}
&+.task-progress-item {
margin-left: 1px;
}
}
}
banner.txt
将油猴插件需要的头部注释拆到该文件中。
// ==UserScript==
// @name 今日掘友分
// @namespace http://tampermonkey.net/
// @version 0.1.1
// @description try to take over the world!
// @author 奇幻心灵
// @match https://juejin.cn/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=juejin.cn
// @connect juejin.cn
// @grant GM_xmlhttpRequest
// @require https://cdn.jsdelivr.net/npm/vue@2.6.14
// ==/UserScript==
api.js
封装油猴GM_xmlhttpRequestapi,以及页面中的请求。
const post = (url, data = {}) => {
const headers = {
'Content-Type': 'application/json;charset=UTF-8',
};
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url,
headers,
responseType: 'json',
data: JSON.stringify(data),
onload: xhr => resolve(xhr.response),
onerror: err => reject(err),
});
});
};
const getUrlPath = relativePath => `https://api.juejin.cn${relativePath}`;
// 获取成长任务列表
export function getGrowthTaskList() {
const url = getUrlPath('/growth_api/v1/user_growth/task_list');
return post(url, {
growth_type: 1,
}).then(rsp => {
const { err_no, data } = rsp;
if (err_no !== 0) return;
const { growth_tasks, today_jscore } = data;
// 有完成次数的任务
const limitTasks = [];
Object.values(growth_tasks).forEach(tasks => {
tasks.forEach(task => {
const { limit, score } = task;
// -1表示无限制,任务可以完成任意次数
if (limit !== -1) {
task.maxScore = limit * score;
limitTasks.push(task);
}
});
});
// 排序:优先展示单任务分值较大的任务,已完成的任务往后排
limitTasks.sort((a, b) => {
if (b.done === b.limit && a.done !== a.limit) return -1;
if (a.done === a.limit && b.done !== b.limit) return 1;
if (a.score !== b.score) return b.score - a.score;
return b.maxScore - a.maxScore;
});
return { today_jscore, limitTasks };
});
}
- main.js
入口文件,使用
rollup打包的入口。
import Vue from 'vue';
import ProgressBar from './ProgressBar';
import './index.scss';
const el = document.createElement('div');
const mainHeaderBox = document.querySelector('.main-header-box');
mainHeaderBox.appendChild(el);
new Vue({ el, render: h => h(ProgressBar) }).$mount();
写了一个banner插件
由于rollup-plugin-banner不满足要求,新写了一个。
// plugins/banner/index.js
const path = require('path');
const fs = require('fs');
export default function (opts) {
const { file, encoding = 'utf-8' } = opts;
return {
name: 'banner',
renderChunk(code) {
const filepath = path.resolve(file);
const exists = fs.existsSync(filepath);
if (!exists) return;
const content = fs.readFileSync(filepath, encoding);
return content + '\r\n\r\n' + code;
},
};
}
rollup配置
import banner from './plugins/banner/index';
import sass from 'rollup-plugin-sass';
const path = require('path');
export default {
input: 'src/main.js',
output: {
file: 'bundle.js',
format: 'iife', // 导出自执行函数
globals: {
vue: 'Vue',
},
},
external: ['vue'],
plugins: [
banner({ file: path.join(__dirname, 'src/banner.txt') }),
sass({ insert: true }),
],
};