關於本篇文章的思路介紹在:juejin.im/post/5a1293…,
看程式碼之前可以先看一下實現的效果;
剛把掘金最熱文章收藏評論分析的思路發出去後,就收到很多掘金好友的喜歡和閱讀,這也讓我更有信心把整個實現過程一步一步記錄下來,讓有興趣的前端童鞋也可以熟悉前後端。雖然整個功能簡單,但也算實現了整個過程,希望能幫助前端的童鞋梳理一下自己的思路;以前也曾有過很多疑惑,資料庫怎麼和後端連線?後端怎麼去資料庫取資料?資料怎麼以json的格式返回?前端怎麼使用介面?我可以自己寫介面嗎?前端怎麼解析介面?vue不用腳手架怎麼使用?jquery和vue怎麼組合?bootstrap怎麼和vue結合在一起?
這一些問題或許對大佬和老鳥們都非常簡單,或許不屑一提,但是對入門不久,或者對想了解前後端怎麼一氣貫通的人來說,這卻很難理解,很容易就跑偏了,或者沒有勇氣往下學習下去,因為學的越多,會發現牽扯的知識點越來越多,知識點越多,感覺自己會的越少,然後就有恐懼感,最後不了了之了;
首先我宣告一下,這個教程是幫助有興趣的童鞋進行梳理或者說輔助,程式碼沒有太高深,也沒有太優化,用簡單的方式做出來,就是學習交流一下,希望大佬們不要看了以後一臉嫌棄 /(ㄒoㄒ)/~~
開工第一步
確保你已經安裝了nodejs 和 mogodb, 如果你是MAC系統,那麼可以看一下這個教程幫你快速搭建環境,相信你能搞定的:blog.csdn.net/byc233518/a…
框架搭建
接下來請用npm初始一個專案juejinSpider,步驟我就不細說了,教程一大堆,不要使用腳手架一類的工具,就是簡單的 npm init ;
初始完成以後開始梳理相關的目錄結構,這裡放一張我的專案結構,獲取沒有那麼標準,但是對這個專案夠用就可以了:
檔案目錄解析
1、mongodb配置
mongodb資料夾存放mongodb相關的配置檔案:
dbconfig.js
是資料庫連線檔案,juejinSchema.js
是重新構建的Schema結構,model.js
是對資料庫的curd操作;
2、node_modules不用說了,是npm安裝的依賴,這裡我們使用的有express,mongoose,mongodb,superagent
3、server目錄中存放的是後端介面處理及爬蟲執行檔案:
app.js
是後端服務的主檔案,監聽5000埠的請求,與請求相關的conctroller我放在了單獨的conctroller檔案中,由於使用的router比較少就沒有抽象出來;spider.js
負責採集掘金介面資訊並進行處理儲存到mongodb;
4、view資料夾主要是前端檢視目錄:
lib資料夾存放的主要是使用到的一些庫檔案,主要有:jquery,vue,axios,ecahrts,bootstrap,masonry(瀑布流外掛),imagesloaded(圖片載入外掛);每個的用途將會在下文提及;
js資料夾下的main.js是主要的js檔案,前端的頁面渲染和介面請求都在此實現,index.html就不用說了,主要的檢視檔案,可以直接開啟,不需要打包工具,因為我不想搞得太複雜。
package.json就不用說了,npm init生成的檔案,裡面有你所需要的依賴;
編碼動工
一、mongodb配置檔案
首先是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設計
接下來就開始設計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操作
緊接著就是資料庫相關的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檔案編寫
然後開始‘爬蟲’主要檔案的編寫,初始的時候我只考慮到了在後臺手動執行每次爬取活動,並沒有想到前臺出發爬取資料的操作;後來感覺全部都採用自動採集的方式比較好,就重新構建了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/
我的部落格也會不定期的分享一些前端難題和自己工作時遇到的問題
我的微信
如果打賞一下我也是不介意的哈 ?