一、前言
平时在搜索文章时总会有很多的干扰信息影响我们问题的查找,于是,将我比较常用的CSDN、稀土掘金、简书等网站汇总到这个工具中。每次进入程序时默认获取对应网址前几个文章,仅显示标题和详情信息,如果有自己想看的文章,点击标题即可直接跳转到对应的文章详情,也可以根据自己的需求输入关键字查找对应的文章。这样的好处是过滤掉其他无关信息,以下实现代码是根据网址的调用接口开发的,可能会因为网址的变动而导致调用失败。
二、设计思路
- 在要查询的网站,按F12打开控制台,查找对应接口的名称及调用参数
- nodejs+express编写后端服务,根据接口参数要求写接口函数
- vue3+electron搭建前端页面,对接自己搭建的nodejs服务
- nodejs调用接口处理响应数据,然后将处理后的数据返回给前端页面展示
三、运行效果
四、代码实现
1. 渲染进程
// api.js
import axios from "axios"
const URL = process.env.NODE_ENV === 'production'?"http://127.0.0.1:3050": 'api'
function juejinList(param) {
return new Promise((reslove, reject) => {
axios.post(URL + "/juejinlist", param).then((res) => {
reslove(res)
})
})
}
function juejinSearch(param) {
return new Promise((reslove, reject) => {
axios.get(params(URL + "/juejinsearch", param)).then((res) => {
reslove(res)
})
})
}
function csdnList() {
return new Promise((reslove, reject) => {
axios.get(URL + "/csdnlist").then((res) => {
reslove(res)
})
})
}
function csdnSearch(param) {
return new Promise((reslove, reject) => {
axios.get(params(URL + "/csdnsearch", param)).then((res) => {
reslove(res)
})
})
}
function jianshuList(param) {
return new Promise((reslove, reject) => {
axios.get(params(URL + "/jianshulist", param)).then((res) => {
reslove(res)
})
})
}
function jianshuSearch(param) {
return new Promise((reslove, reject) => {
axios.get(params(URL + "/jianshusearch", param)).then((res) => {
reslove(res)
})
})
}
function params(url, params) {
if (params) { // 有参数时拼接
return params ? url + '?' + Object.keys(params)
.filter(key => params[key] || params[key] === 0)
.map(key => `${key}=${params[key]}`)
.toString().replace(/,/g, '&') :
url
} else { // 没参数时获取
let obj = {};
url.match(/(\w+)=(\w+)/g).forEach(item => {
Object.assign(obj, {
[item.split('=')[0]]: item.split('=')[1]
})
})
return obj
}
}
export default {
juejinList,
juejinSearch,
csdnList,
csdnSearch,
jianshuList,
jianshuSearch
}
<!-- index.vue -->
<div class="container">
<div class="silder"> <!-- 侧边栏 -->
<div v-for="(item,index) in data.list" :key="index">
<el-link
type="primary"
:underline="false"
style="font-size: 14px; margin-top: 10px"
:style="item.state ? 'font-weight: bold;' : ''"
@click="handleChose(index)"
>{{ item.name }}</el-link>
</div>
</div>
<div class="content"> <!-- 滚动栏 -->
<div class="head">
<el-input
style="width: 150px;margin-right:10px;"
v-model="data.keyword"
placeholder="请输入关键字"
size="small"
:disabled="data.disabled"
clearable
@clear="handleChose(data.index)"
></el-input>
<el-button size="small" :disabled="data.disabled" @click="search">搜索</el-button>
</div>
<el-scrollbar class="main_content" :height="`${data.height}px`" @scroll="scroll">
<div id="content" v-loading.fullscreen.lock="fullscreenLoading">
<div v-for="(item,index) in data.contentList" :key="index">
<el-collapse v-model="activeNames">
<el-collapse-item :name="index">
<template #title>
<el-link
class="link"
:underline="false"
@click="handleUrl(item.url)"
v-html="item.title"
></el-link>
</template>
<div style="margin-right: 10px" v-html="item.describe"></div>
<div v-if="item.time">{{ item.time }}</div>
<div v-if="item.tag" class="label">
<div v-for="(val, key) in item.tag" :key="key">
<el-tag style="margin:5px 10px 0 0;">{{ val }}</el-tag>
</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
</el-scrollbar>
</div>
</div>
// index.js
import api from "../api";
import { ipcRenderer } from "electron";
import { onMounted, reactive, ref } from "vue";
const data = reactive({
list: [
{name: "稀土掘金",state: false},
{name: "CSDN",state: false},
{name: "简书",state: false}
],
contentList: [], // 主内容列表
curPlatform: "", // 当前平台
index: 0, // 平台索引
height: 500,
keyword: "",
disabled: false
});
var param = {
juejin: {
id_type: 2,
sort_type: 300,
cate_id: "6809637767543259144",
cursor: "0",
limit: 20,
search: {
query: "",
cursor: 0
}
},
csdn: {
search: {
q: "",
t: "all",
p: 1
}
},
jianshu: {
page: 1,
type_id: 31,
count: 10,
search: {
q: "",
page: 1,
type: "note",
order_by: "default"
}
}
};
const fullscreenLoading = ref(false)
onMounted(() => {
data.list[data.index].state = true;
data.curPlatform = data.list[data.index].name;
data.height = window.innerHeight - 90;
window.onresize = function() {
data.height = window.innerHeight - 90;
};
getjuejinList();
});
// 平台切换
function handleChose(index) {
data.list.forEach(item => {
item.state = false;
});
data.list[index].state = true;
data.curPlatform = data.list[index].name;
data.index = index;
switch (data.curPlatform) {
case "稀土掘金":
data.disabled = false
param.juejin.cursor = "0";
data.contentList = [];
getjuejinList();
break;
case "CSDN":
data.disabled = false
data.contentList = [];
getcsdnList();
break;
case "简书":
data.disabled = false
param.jianshu.page = 1;
data.contentList = [];
getjianshuList();
break;
}
}
// 滚动加载
function scroll(e) {
let num = document.querySelector("#content").clientHeight - document.querySelector(".main_content").clientHeight;
if (Math.floor(e.scrollTop - num) == 44) {
switch (data.curPlatform) {
case "稀土掘金":
if (data.keyword) {
param.juejin.search.cursor += 20;
juejinSearch();
} else {
param.juejin.cursor = "0";
getjuejinList();
}
break;
case "CSDN":
if (data.keyword) {
param.csdn.search.p += 1;
csdnSearch();
} else {
getcsdnList();
}
break;
case "简书":
if (data.keyword) {
param.jianshu.search.page += 1;
jianSearch();
} else {
getjianshuList();
}
break;
}
}
}
function handleUrl(url) {
ipcRenderer.send("openUrl", url);
}
function getjuejinList() {
fullscreenLoading.value = true
api.juejinList(param.juejin).then(res => {
data.contentList = [...data.contentList, ...res.data.list];
param.juejin.cursor = res.data.param;
fullscreenLoading.value = false
});
}
function juejinSearch() {
fullscreenLoading.value = true
api.juejinSearch(param.juejin.search).then(res => {
data.contentList = [...data.contentList, ...res.data.list];
fullscreenLoading.value = false
});
}
function getcsdnList() {
fullscreenLoading.value = true
api.csdnList().then(res => {
data.contentList = [...data.contentList, ...res.data.list];
fullscreenLoading.value = false
});
}
function csdnSearch() {
fullscreenLoading.value = true
api.csdnSearch(param.csdn.search).then(res => {
data.contentList = [...data.contentList, ...res.data.list];
fullscreenLoading.value = false
});
}
function getjianshuList() {
fullscreenLoading.value = true
api.jianshuList(param.jianshu).then(res => {
data.contentList = [...data.contentList, ...res.data.list];
fullscreenLoading.value = false
});
}
function jianSearch() {
fullscreenLoading.value = true
api.jianshuSearch(param.jianshu.search).then(res => {
data.contentList = [...data.contentList, ...res.data.list];
fullscreenLoading.value = false
});
}
function search() {
switch (data.curPlatform) {
case "稀土掘金":
param.juejin.search.cursor = 0;
param.juejin.search.query = data.keyword;
data.contentList = [];
juejinSearch();
break;
case "CSDN":
param.csdn.search.p = 1;
param.csdn.search.q = data.keyword;
data.contentList = [];
csdnSearch();
break;
case "简书":
param.jianshu.search.page = 1;
param.jianshu.search.q = data.keyword;
data.contentList = [];
jianSearch();
break;
}
}
/* index.css */
.container {
display: flex;
}
.silder {
border-right: 1px solid #dddddd;
width: 10%;
}
.content {
width: 90%;
}
.head {
margin: 0 10px;
padding: 10px 0;
display: flex;
align-items: center;
}
.main_content {
margin-left: 10px;
padding-right: 10px;
}
.link {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.label {
display: flex;
}
2. 主进程
// service.js
import express from 'express'
import axios from 'axios'
import dayjs from 'dayjs'
function params(url, params) {
if (params) { // 有参数时拼接
return params ? url + '?' + Object.keys(params)
.filter(key => params[key] || params[key] === 0)
.map(key => `${key}=${params[key]}`)
.toString().replace(/,/g, '&') :
url
} else { // 没参数时获取
let obj = {};
url.match(/(\w+)=(\w+)/g).forEach(item => {
Object.assign(obj, {
[item.split('=')[0]]: item.split('=')[1]
})
})
return obj
}
}
export default function initService() {
const app = express()
const port = 3050
// 获取掘金最新列表
app.post("/juejinlist", (req, res) => {
let body = ''
req.on('data', (thunk) => {
body += thunk
})
req.on('end', () => {
axios.post(params("https://api.juejin.cn/recommend_api/v1/article/recommend_cate_feed", {
aid: 2608,
uuid: "7236633682860246562",
spider: 0
}), JSON.parse(body)).then((result) => {
console.log("yes")
let list = result.data.data.map((item) => {
return {
id: item.article_id, // 文章id
url: "https://juejin.cn/post/" + item.article_id, // 文章链接
title: item.article_info.title, // 文章标题
describe: item.article_info.brief_content, // 文章描述
time: dayjs.unix(item.article_info.mtime).format('YYYY-MM-DD HH:mm:ss'), // 发布时间
tag: item.tags.map(value => value.tag_name) // 文章标签
}
})
res.send({
list,
param: result.data.cursor
})
}).catch((err) => {
// console.log(err)
})
})
})
// 关键字查询
app.get("/juejinsearch", (req, res) => {
axios.get(params("https://api.juejin.cn/search_api/v1/search", {
aid: 2608,
uuid: "7236633682860246562",
spider: 0,
id_type: 0,
limit: 20,
search_type: 0,
sort_type: 0,
version: 1,
...params(req.url)
})).then((result) => {
console.log("yes")
let list = result.data.data.map((item) => {
return {
id: item.result_model.article_id, // 文章id
url: "https://juejin.cn/post/" + item.result_model.article_id, // 文章链接
title: item.result_model.article_info.title, // 文章标题
describe: item.result_model.article_info.brief_content, // 文章描述
time: dayjs.unix(item.result_model.article_info.mtime).format('YYYY-MM-DD HH:mm:ss'), // 发布时间
tag: item.result_model.tags.map(value => value.tag_name) // 文章标签
}
})
res.send({
list,
param: ""
})
}).catch((err) => {
// console.log(err)
})
})
// 获取csdn最新列表
app.get("/csdnlist", (req, res) => {
axios.get(params("https://cms-api.csdn.net/v1/web_home/select_content", {
componentIds: "www-recomend-community",
cate1: "web"
})).then((result) => {
console.log("yes")
let list = result.data.data["www-recomend-community"].info.map((item) => {
return {
id: item.extend.product_id, // 文章id
url: item.extend.url, // 文章链接
title: item.extend.title, // 文章标题
describe: item.extend.desc, // 文章描述
time: item.extend.created_at, // 发布时间
tag: item.extend.csdnTag // 文章标签
}
})
res.send({
list,
param: ""
})
}).catch((err) => {
// console.log(err)
})
})
// 关键字查询
app.get("/csdnsearch", (req, res) => {
axios.get(params("https://so.csdn.net/api/v3/search", {
...params(req.url)
})).then((result) => {
console.log("yes")
let list = result.data.result_vos.map((item) => {
return {
id: item.id, // 文章id
url: item.url, // 文章链接
title: item.title, // 文章标题
describe: item.description, // 文章描述
time: item.created_at, // 发布时间
tag: item.search_tag // 文章标签
}
})
res.send({
list,
param: ""
})
}).catch((err) => {
// console.log(err)
})
})
// 获取简书最新列表
app.get("/jianshulist", (req, res) => {
axios.get(params("https://www.jianshu.com/programmers", params(req.url))).then((result) => {
console.log("yes")
let list = result.data.map((item) => {
return {
id: item.id, // 文章id
url: "https://www.jianshu.com/p/" + item.slug, // 文章链接
title: item.title, // 文章标题
describe: item.desc, // 文章描述
time: "", // 发布时间
tag: "" // 文章标签
}
})
res.send({
list,
param: ""
})
}).catch((err) => {
// console.log(err)
})
})
// 关键字查询
app.get("/jianshusearch", (req, res) => {
axios.post(params("https://www.jianshu.com/search/do", params(req.url)),{},{
headers: {
"Accept": "*/*"
}
}).then((result) => {
console.log("yes")
let list = result.data.entries.map((item) => {
return {
id: item.id, // 文章id
url: "https://www.jianshu.com/p/" + item.slug, // 文章链接
title: item.title, // 文章标题
describe: item.content, // 文章描述
time: dayjs(item.first_shared_at).format('YYYY-MM-DD HH:mm:ss'), // 发布时间
tag: "" // 文章标签
}
})
res.send({
list,
param: ""
})
}).catch((err) => {
// console.log(err)
})
})
app.listen(port, () => {
console.log(`http://127.0.0.1:${port}`)
})
}
在本地配置代理路径
// vue.config.js
devServer: {
proxy: {
'/api': {
target: "http://127.0.0.1:3050",
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
// index.js
import {
ipcMain,
shell
} from 'electron'
import initService from './service'
ipcMain.on("openUrl", (e, url) => {
shell.openExternal(url)
})
initService()
五、结尾
目前项目只实现了文章搜索和关键字查询的功能,大家可以根据自己的想法扩充查询的范围,也可以自定义开发些的筛选功能以满足自己的需求。