关于本篇文章的思路介绍在:juejin.im/post/5a1293…,css
看代码以前能够先看一下实现的效果;html
刚把掘金最热文章收藏评论分析的思路发出去后,就收到不少掘金好友的喜欢和阅读,这也让我更有信心把整个实现过程一步一步记录下来,让有兴趣的前端童鞋也能够熟悉先后端。虽然整个功能简单,但也算实现了整个过程,但愿能帮助前端的童鞋梳理一下本身的思路;之前也曾有过不少疑惑,数据库怎么和后端链接?后端怎么去数据库取数据?数据怎么以json的格式返回?前端怎么使用接口?我能够本身写接口吗?前端怎么解析接口?vue不用脚手架怎么使用?jquery和vue怎么组合?bootstrap怎么和vue结合在一块儿?前端
这一些问题或许对大佬和老鸟们都很是简单,或许不屑一提,可是对入门不久,或者对想了解先后端怎么一气贯通的人来讲,这却很难理解,很容易就跑偏了,或者没有勇气往下学习下去,由于学的越多,会发现牵扯的知识点愈来愈多,知识点越多,感受本身会的越少,而后就有恐惧感,最后不了了之了;vue
首先我声明一下,这个教程是帮助有兴趣的童鞋进行梳理或者说辅助,代码没有过高深,也没有太优化,用简单的方式作出来,就是学习交流一下,但愿大佬们不要看了之后一脸嫌弃 /(ㄒoㄒ)/~~node
确保你已经安装了nodejs 和 mogodb, 若是你是MAC系统,那么能够看一下这个教程帮你快速搭建环境,相信你能搞定的:blog.csdn.net/byc233518/a…jquery
接下来请用npm初始一个项目juejinSpider,步骤我就不细说了,教程一大堆,不要使用脚手架一类的工具,就是简单的 npm init ;android
初始完成之后开始梳理相关的目录结构,这里放一张个人项目结构,获取没有那么标准,可是对这个项目够用就能够了:ios
一、mongodb配置git
mongodb文件夹存放mongodb相关的配置文件:github
dbconfig.js
是数据库链接文件,juejinSchema.js
是从新构建的Schema结构,model.js
是对数据库的curd操做;
二、node_modules不用说了,是npm安装的依赖,这里咱们使用的有express,mongoose,mongodb,superagent
三、server目录中存放的是后端接口处理及爬虫执行文件:
app.js
是后端服务的主文件,监听5000端口的请求,与请求相关的conctroller我放在了单独的conctroller文件中,因为使用的router比较少就没有抽象出来;spider.js
负责采集掘金接口信息并进行处理存储到mongodb;
四、view文件夹主要是前端视图目录:
lib文件夹存放的主要是使用到的一些库文件,主要有:jquery,vue,axios,ecahrts,bootstrap,masonry(瀑布流插件),imagesloaded(图片加载插件);每一个的用途将会在下文说起;
js文件夹下的main.js是主要的js文件,前端的页面渲染和接口请求都在此实现,index.html就不用说了,主要的视图文件,能够直接打开,不须要打包工具,由于我不想搞得太复杂。
package.json就不用说了,npm init生成的文件,里面有你所须要的依赖;
首先是mongodb的相关配置dbconfig.js
,主要代码以下:
这里主要配置了mongodb的连接地址,以及链接状态,个人mongodb的默认端口是27017,另外新建一个数据库,在这里我命名为juejin,建库的相关操做,请参考:blog.csdn.net/byc233518/a…;另外建议下载一个mongodb可视化工具mongobooster;
var mongoose = require('mongoose'),
DB_URL = 'mongodb://localhost:27017/juejin';
var db = mongoose.connect(DB_URL,{useMongoClient:true});
//链接成功
mongoose.connection.on('connected',function () {
console.log("Mongoose connection open to "+DB_URL);
});
//链接异常
mongoose.connection.on('error',function (err) {
console.log("Mongoose connection erro "+err);
});
//链接断开
mongoose.connection.on('disconnected',function () {
console.log("Mongoose connection disconnected ");
});
module.exports = mongoose;
复制代码
接下来就开始设计Schema结构,由于从掘金接口获取的数据有大量的无用数据(反正对我来讲无用,我只要几个数据😂),主要代码以下juejinSchema.js
:这里为了防止数据重复采集,我设置了文章原始连接为惟一值,可是采集过程当中发现,仍是有空值存在,不过好在不影响总体采集,这里暂时尚未优化;
var mongoose = require('./dbconfig.js'), // 引入mongodb配置文件
Schema = mongoose.Schema;
// 构造Schema
var JuejinSchema = new Schema({
author:String, //做者
category:{ //类别
id:String, //类别ID,由于爬取的时候发现,九大类别在发送请求的时候是发送的id号
name:String, //名称
title:String
},
collectionCount:Number, //收藏数
commentsCount:Number, //评论数
viewsCount:Number, //浏览数
title:String, //文章标题
summaryInfo:String, //文章摘要
originalUrl:{type:String,unique: true}, // 文章原始连接
screenshot:String // 缩略图
});
module.exports = mongoose.model('juejin',JuejinSchema);复制代码
紧接着就是数据库相关的curd相关操做,这里我把它单独抽取出来放在model.js
文件里,这里虽然还能够进一步的抽象出来一个dao文件,可是因为项目并非很大因此这里就在一个文件里实现,这里我仅实现了数据的插入和查询操做,并无对删除和更新进行具体实现;其中插入操做主要面对爬虫获取数据并写入数据库,查询主要面对前端显示相关内容,主要实现代码以下:
var Juejin = require('./juejinSchema.js'); //引入Schema 文件
//数据插入
function insert(conditions,callback) {
conditions = conditions || {};
Juejin.create(conditions,callback)
}
//数据查询
function find(conditions,callback) {
conditions = conditions || {};
Juejin.find(conditions,callback);
}
//数据更新
function update(conditions,update) {
Juejin.update(conditions,update,function (err,res) {
if(err) console.log('Error' + err);
else console.log('Res:' + res);
})
}
//数据删除
function del(conditions) {
Juejin.remove(conditions,function (err,res) {
if(err) console.log('Error' + err);
else console.log('Res:' + res);
})
}
module.exports = {
find:find,
del:del,
update:update,
insert:insert
};复制代码
而后开始‘爬虫’主要文件的编写,初始的时候我只考虑到了在后台手动执行每次爬取活动,并无想到前台出发爬取数据的操做;后来感受所有都采用自动采集的方式比较好,就从新构建了spider.js
文件;这个方法目前主要就是响应前端的请求并进行采集数据并插入数据中;
var superagent = require('superagent');//引入superagent 插件
var model = require('../mongodb/model.js');// 引入mongodb 的model
//爬取掘金热文主要函数,接收参数sort: 须要爬取的类别 callback:爬取完成后的回调
spider = function (sort, callback) {
var limit = 100;//限制爬取的数据为100条,多余100条掘金就不给回应了
// 每一个种类所对应的id值,在发送请求的时候须要
var categroyList = [
{
"id": "5562b410e4b00c57d9b94a92",
"name": "android"
},
{
"id": "5562b415e4b00c57d9b94ac8",
"name": "前端"
},
{
"id": "5562b405e4b00c57d9b94a41",
"name": "iOS"
},
{
"id": "569cbe0460b23e90721dff38",
"name": "产品"
},
{
"id": "5562b41de4b00c57d9b94b0f",
"name": "设计"
},
{
"id": "5562b422e4b00c57d9b94b53",
"name": "工具资源"
},
{
"id": "5562b428e4b00c57d9b94b9d",
"name": "阅读"
},
{
"id": "5562b419e4b00c57d9b94ae2",
"name": "后端"
},
{
"id": "57be7c18128fe1005fa902de",
"name": "人工智能"
}
];
for (var i = 0; i < categroyList.length; i++) {//根据type值取出对应的id值
if (categroyList[i].name === sort) {
var id = categroyList[i].id;
break;
}
}
//请求连接
var URL = 'https://timeline-merger-ms.juejin.im/v1/get_entry_by_hot?src=web&limit=' + limit + '&category=' + id;
superagent
.get(URL)
//请求结束后的操做
.end(function (err, res) {
if (err) {
return err;
}
//解析请求后获得的body数据
var result = res.body;
insertTomongoDB(result, callback);
});
};
//数据写入mongodb
insertTomongoDB = function (val, callback) {
//获取body中相关的主要数据,为entrylist数组
var data = val.d.entrylist;
//建立一个插入数据库的数组
var insertList = [];
for (var i = 0; i < data.length; i++) {
var insert = {
author: data[i].author,
category: {
id: data[i].category.id,
name: data[i].category.name,
title: data[i].category.title
},
collectionCount: data[i].collectionCount,
commentsCount: data[i].commentsCount,
viewsCount: data[i].viewsCount,
title: data[i].title,
summaryInfo: data[i].summaryInfo,
originalUrl: data[i].originalUrl,
screenshot: data[i].screenshot
};
insertList.push(insert)
}
model.insert(insertList, callback); // 插入操做
};
module.exports = {
spiders: spider
};
复制代码
好的,获取数据的部分已经完成,开始构建后端接口app.js
(不要觉得顺序反了,由于最初我只作了获取数据部分,为了测试是否可以正常插入数据到mongodb),这里我采用的是express框架,监听5000端口的请求,目前只写了两个接口,都是get请求,一个负责查询数据,一个负责触发采集数据操做;这里我把主要的控制器放在了单独的文件controller.js
中。
var express = require('express');//引入express
var app = express(); // 构造一个实例
var $ = require('./controllers/controllers.js'); //引入controller
//设置跨域访问
app.all('*', function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
res.header("X-Powered-By",' 3.2.1');
res.header("Content-Type", "application/json;charset=utf-8");
next();
});
// 数据获取接口,须要得到类别
app.get('/api/getListByCategory',$.list);
// 数据采集接口,须要得到类别
app.get('/api/sendSpiderByCategory',$.send);
//监听5000端口
var server = app.listen(5000, function () {
var host = server.address().address;
var port = server.address().port;
console.log('Example app listening at http://%s:%s', host, port);
});复制代码
后端的主要接口已经写好,那么开始写主要的控制器controller.js
,控制器中目前也只有两个实现方法,一个是获取数据列表的方法,一个是触发采集数据的方法;这里须要引入model.js
和spider.js
;一个是为了实现查询数据,一个是为了触发采集数据操做;主要代码以下:
var model = require('../../mongodb/model.js'); // 引入model文件
var spider = require('../spider');//引入spider文件
//获取文章列表
list = function (req,res,next) {
var param = req.query.sort; //解析get请求所携带的参数sort
model.find({'category.name':param},function (err,doc) {
if(err){
res.end(err);
return
}
//这里直接返回数据库返回的数据,我并无进行其余封装,因此返回的是一个数组,后续会考虑统一标准
res.end(JSON.stringify(doc));
});
};
//根据类型选择爬取的内容
send = function (req,res,next) {
var param = req.query.sort;//解析get请求所携带的参数sort
//触发采集程序运行,并返回数据插入操做的结果
spider.spiders(param,function (err,doc) {
if(err){
res.end(JSON.stringify(err));
return
}
//若是数据插入成功,返回ok
res.end(JSON.stringify({msg:'ok'}));
});
};
module.exports = {
list:list,
send:send
};复制代码
此时你能够运行app.js,在命令行或者webstorm中直接run,看到控制台出现这样的状况即表示成功,此时你能够再浏览器中输入: http://localhost:5000/api/getListByCategory?sort=Android 这时若是你数据库没有数据可能会报错,个人显示以下
此时后端以及数据库相关的工做已经完成,接下来就是前端的工做了,前端我选择了vue+bootstrap进行快速构建页面,vue和boostrap的优势我就不用说了,大佬们早已经剖析的体无完肤(就差肢解了,额,有点血腥,别怪我,最近在看Rick and Morty(A站有资源),虽然很血腥暴力,可是有不少话能让人深入反思,准备二刷了,一遍不过瘾。。。跑题了,仍是回来继续写);说到哪里了,对,讲到使用的vue和bootstrap,这里我没有使用vue的脚手架,由于感受没有必要,我只是引用其中的一部分,不须要大动牛刀;boostrap的引用也不用说了,由于使用到http请求,那我就想干脆把axios也拉过来一块练练吧,直接引用,很少说;由于看到有些问题说vue怎么和jquery一块使用,那我就继续把jquery拿来使用一番,反正不要钱(对,不要钱的,随便用),由于牵扯到图标的使用,那么就把echarts 也勾引过来吧,关于echarts的使用,能够看一下官方文档,那里已经有了很详细的解释,我就不展开了,这里我使用的是折线图,参考连接:echarts.baidu.com/demo.html#l…;不要感受折线图,柱状图,雷达图,还有各类图很难,其实跟着官方文档一点一点配置很简单的,只要你有数据,什么样的图表分析你均可以作出来;好吧很少说直接上view的index.html
代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>掘金历史最热收藏排行榜</title>
<link rel="stylesheet" href="./lib/bootstrap.min.css">
</head>
<body>
<div id="main">
<div class="container">
<div class="row panel">
<div class="col-sm-12 page-header">
<h2 class="text-center">掘金{{sort}}历史最热收藏排行榜前一百名</h2>
<h4 class="text-center">收藏,评论,浏览量折线分析</h4>
</div>
</div>
<div class="row " id="menu" >
<div class="col-sm-7 col-sm-offset-3">
<ul class="nav nav-pills">
<li role="presentation" class="active"><a href="#" v-on:click="getData('Android')">Android</a></li>
<li role="presentation" class="active"><a href="#" v-on:click="getData('前端')">前端</a></li>
<li role="presentation" class="active"><a href="#" v-on:click="getData('iOS')">iOS</a></li>
<li role="presentation" class="active"><a href="#" v-on:click="getData('产品')">产品</a></li>
<li role="presentation" class="active"><a href="#" v-on:click="getData('设计')">设计</a></li>
<li role="presentation" class="active"><a href="#" v-on:click="getData('工具资源')">工具资源</a></li>
<li role="presentation" class="active"><a href="#" v-on:click="getData('阅读')">阅读</a></li>
<li role="presentation" class="active"><a href="#" v-on:click="getData('后端')">后端</a></li>
<li role="presentation" class="active"><a href="#" v-on:click="getData('人工智能')">人工智能</a></li>
</ul>
</div>
</div>
<div class="row spider" >
<div class="col-sm-12" >
<h4 class="text-center">数据不存在???!!别担忧,咱们开始采集,仅限前100条数据,快选择你要采集的数据</h4>
</div>
<div class="col-sm-7 col-sm-offset-3">
<ul class="nav nav-pills">
<li role="presentation" class="active"><a href="#" v-on:click="spiderData('android')">Android</a></li>
<li role="presentation" class="active"><a href="#" v-on:click="spiderData('前端')">前端</a></li>
<li role="presentation" class="active"><a href="#" v-on:click="spiderData('iOS')">iOS</a></li>
<li role="presentation" class="active"><a href="#" v-on:click="spiderData('产品')">产品</a></li>
<li role="presentation" class="active"><a href="#" v-on:click="spiderData('设计')">设计</a></li>
<li role="presentation" class="active"><a href="#" v-on:click="spiderData('工具资源')">工具资源</a></li>
<li role="presentation" class="active"><a href="#" v-on:click="spiderData('阅读')">阅读</a></li>
<li role="presentation" class="active"><a href="#" v-on:click="spiderData('后端')">后端</a></li>
<li role="presentation" class="active"><a href="#" v-on:click="spiderData('人工智能')">人工智能</a></li>
</ul>
</div>
</div>
<div class="row">
<div id="line" style="width: 1200px;height: 600px"></div>
</div>
<div style="height: 30px"></div>
<div class="row" id="masonry">
<div class="col-sm-6 col-md-4 box" v-for="item in articleList">
<div class="thumbnail">
<img :src="item.screenshot" alt="">
<div class="caption">
<h3><a :href="item.originalUrl">{{item.title}}</a></h3>
<p>{{item.summaryInfo}}</p>
</div>
</div>
</div>
</div>
<div><a v-on:click="goTop()" href="#">回到顶部</a></div>
</div>
</div>
<script src="./lib/jquery.min.js"></script>
<script src="./lib/bootstrap.min.js"></script>
<script src="./lib/vue.js"></script>
<script src="./lib/axios.min.js"></script>
<script src="./lib/echarts.min.js"></script>
<script src="./lib/masonry-docs.min.js"></script>
<script src="./lib/imagesloaded.pkgd.min.js"></script>
<script src="./js/main.js"></script>
</body>
</html>复制代码
其实你们会发现我多引用了masonry-docs.min.js
和imagesloaded.pkgd.min.js
两个文件,这两个文件主要的做用是让文章以瀑布流的方式显示,同时因为图片没有加载的时候可能会产生重叠,因此我引入了imagesLoade来判断图片是否正常加载,若是正常加载后再进行瀑布流显示;bootstrap使用的样式为:v3.bootcss.com/components/… ;全部的引入文件能够从个人git上拉取:传送门
最后是前端页面逻辑的实现,主要在main.js
中,这里掺杂了vue,jquery语法,有代码洁癖的人不要激动哈,我只是想两个同时用一下,并无违反vue的初衷,主要代码以下:
$(document).ready(function () {
//建立一个vue实例
var vm = new Vue({
el: '#main',
data: {
articleList: [],
sort: '前端'
},
//初始挂载的时候就发送请求,默认请求前端数据
mounted() {
"use strict";
this.getData('前端');
$('.spider').css('display', 'none');
},
methods: {
//初始化折线图图表
initChart: function (obj) {
//主要配置
var options = {
//折线图标题
title: {
text: '掘金历史最热'
},
//提示组件框,坐标轴触发,主要在柱状图,折线图等会使用类目轴的图表中使用。
tooltip: {
trigger: 'axis'
},
//图例的类型
legend: {
data: ['收藏数', '评论数', '查看数']
},
//直角坐标系内绘图网格,距离容器上下左右的距离
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true //grid 区域是否包含坐标轴的刻度标签。
},
//工具栏。内置有导出图片,数据视图,动态类型切换,数据区域缩放,重置五个工具。
toolbox: {
feature: {
saveAsImage: {}//这里使用导出图片
}
},
//dataZoom 组件 用于区域缩放,从而能自由关注细节的数据信息,或者概览数据总体,或者去除离群点的影响
dataZoom: {
show: true,
realtime: true,
start: 0,
end: 10,//咱们数据范围显示为10篇文章的数据
},
//直角坐标系 grid 中的 x 轴
xAxis: {
type: 'category', // 类目轴
boundaryGap: false, //坐标轴两边留白策略,类目轴和非类目轴的设置和表现不同。
data: obj.title, //类目数据,即文章标题
axisLabel: { // X轴标签显示为8个字为一行,防止文字重叠
interval: 0,
formatter: function (value) {
var ret = "";//拼接加\n返回的类目项
var maxLength = 8;//每项显示文字个数
var valLength = value.length;//X轴类目项的文字个数
var rowN = Math.ceil(valLength / maxLength); //类目项须要换行的行数
if (rowN > 1)//若是类目项的文字大于3,
{
for (var i = 0; i < rowN; i++) {
var temp = "";//每次截取的字符串
var start = i * maxLength;//开始截取的位置
var end = start + maxLength;//结束截取的位置
//这里也能够加一个是不是最后一行的判断,可是不加也没有影响,那就不加吧
temp = value.substring(start, end) + "\n";
ret += temp; //凭借最终的字符串
}
return ret;
}
else {
return value;
}
}
}
},
//Y轴类别,这里创建了两个Y轴,由于数据量差异过大
yAxis: [{
type: 'value',
name: '收藏与评论'
}, {
type: 'value',
name: '浏览数'
}],
//数据来源
series: [
{
name: '收藏数',
type: 'line',
// stack:'总量',
data: obj.collect
},
{
name: '评论数',
type: 'line',
// stack:'总量',
data: obj.comment
},
{
name: '浏览数',
yAxisIndex: 1,
type: 'line',
// stack:'总量',
data: obj.view
}
]
};
var ele = document.getElementById('line');//获取渲染图表的节点
var myChart = echarts.init(ele);//初始化一个图表实例
myChart.setOption(options);//给这个实例设置配置文件
},
//获取文章数据,须要接收参数
getData: function (val) {
var self = this;
self.sort = val;
//使用axios进行请求
axios.get('http://localhost:5000/api/getListByCategory?sort=' + self.sort)
.then(function (response) {
var data = response.data;
if (data.length <= 0) {
$('.spider').css('display', 'block');
$('#menu').css('display', 'none');
alert('数据库中不存在数据,请进行采集后查询');
}
self.articleList = data;
var arryCollect = [],
arryComment = [],
arryView = [],
arryTitle = [];
for (var i = 0; i < data.length; i++) {
arryCollect.push(data[i].collectionCount);
arryComment.push(data[i].commentsCount);
arryView.push(data[i].viewsCount);
arryTitle.push(data[i].title)
}
var obj = {
collect: arryCollect,
comment: arryComment,
view: arryView,
title: arryTitle
};
console.log(obj);
self.initChart(obj);
self.loadInfo();
});
},
//加载瀑布流文章显示
loadInfo: function () {
var $container = $('#masonry');
$container.imagesLoaded(function () {
setTimeout(function () {
$container.masonry({
itemSelector: '.box'
});
}, 1000)
})
},
//爬取数据,根据参数
spiderData: function (val) {
var self = this;
//使用axios进行请求
axios.get('http://localhost:5000/api/sendSpiderByCategory?sort=' + val)
.then(function (response) {
if (response.data.msg === 'ok') {
$('.spider').css('display', 'none');
$('#menu').css('display', 'block');
alert('数据采集成功');
self.getData(val);
}
})
},
// goTop:function () {
// this.click(function (e) {
// e.preventDefault();
// $(document.body).animate({scrollTop: 0}, 800);
// });
// }
}
});
});
复制代码
到此为止,整个项目算是基本完成了,你能够直接打开index.html进行查看,初始是没有数据的,会提醒你进行采集数据,采集完成后会提示成功,而后刷新页面就会发现数据已经有了;(此时app.js须要在后台运行,不要关闭)
若是你在整个搭建过程当中出现问题的话能够给我留言,或者直接添加个人微信,但愿能和你们相互交流,我不是大佬,或许不能解决你所提出的问题,但咱们能够讨论一下;但愿经过这篇文章可以帮助想一我的搭建先后端的童鞋,虽然简单,但先后端以及数据库都用到了;就像全部的代码都是从"hello world"开始同样,一旦你完成了“hello world”,后面你就能够无限的扩展了;
整个git项目今天我又从新修改了一遍,添加了更多的注释,方便更多的童鞋可以理解,项目地址github.com/gengchen528… ,喜欢有兴趣的不妨来个star,不要吝啬哈,fork一份也是能够的,哈哈~~
本文纯手打,但愿尊重一下个人成果,如要转载请联系我,谢谢
另外欢迎你们来个人博客作客:小K博客:www.xkboke.com/
个人博客也会不按期的分享一些前端难题和本身工做时遇到的问题
个人微信
若是打赏一下我也是不介意的哈 😆