關於本篇文章的思路介紹在: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/
個人博客也會不按期的分享一些前端難題和本身工做時遇到的問題
個人微信
若是打賞一下我也是不介意的哈 😆