mongodb+express+vue代码实现掘金最热文章收藏评论分析

2,639 阅读9分钟

关于本篇文章的思路介绍在:juejin.cn/post/684490…

看代码之前可以先看一下实现的效果;

刚把掘金最热文章收藏评论分析的思路发出去后,就收到很多掘金好友的喜欢和阅读,这也让我更有信心把整个实现过程一步一步记录下来,让有兴趣的前端童鞋也可以熟悉前后端。虽然整个功能简单,但也算实现了整个过程,希望能帮助前端的童鞋梳理一下自己的思路;以前也曾有过很多疑惑,数据库怎么和后端连接?后端怎么去数据库取数据?数据怎么以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.jsspider.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.jsimagesloaded.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一份也是可以的,哈哈~~

本文纯手打,希望尊重一下我的成果,如要转载请联系我,谢谢