精华内容
下载资源
问答
  • 刚把掘金热文章收藏评论分析思路发出去后,就收到很多掘金好友喜欢和阅读,这也让我更有信心把整个实现过程一步一步记录下来,让有兴趣前端童鞋也可以熟悉前后端。虽然整个功能简单,但也算实现了整个过程,...

    关于本篇文章的思路介绍在: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.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一份也是可以的,哈哈~~

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

    另外欢迎大家来我的博客做客:小K博客:www.xkboke.com/

    我的博客也会不定期的分享一些前端难题和自己工作时遇到的问题

    我的微信

    如果打赏一下我也是不介意的哈 ?


    展开全文
  • 大家都知道,时间最近的评论会被加载到上面,这就好比张三先评论了一个666,第二天李四评论了一个777,那么李四的评论肯定是展示在上面的。 那么具体怎么实现呢?那有人说了,保存在数据库中按时间更新不就行...

    前言

    其实我们生活中有很多栈的例子。今天记录这一篇文章主要还是用我生活中的实际例子来解决问题。
    公司有个项目,需要做一个评论功能。大家都知道,时间最近的评论会被加载到最上面,这就好比张三先评论了一个666,第二天李四评论了一个777,那么李四的评论肯定是展示在最上面的。
    那么具体怎么去实现呢?那有人说了,保存在数据库中按时间更新不就行了吗。这样也确实可以,不过,我们可以用更简单的做法来实现。
    因为这个例子类似于我们栈的操作——先进后出的原则。
    在公司做项目的时候,我也是用的自定义的一个栈去搞定了。

    准备工作

    • 工具:idea+jdk8
    • 技术要求:Java基础语法

    编码环节

    首先,我们得先确定下来,用什么数据来模拟栈的操作。由于是一个一个的元素放入栈里面,我们可以考虑用数组来实现。
    在这里插入图片描述
    以上是Java官方文档中的栈定义,我们也只需要实现三个方法:判断是否为空、移除栈顶对象、添加元素到栈的尾部

    所以我们事先得定义一个数组:

    Objects[] arr;
    

    数组定义好了之后呢,想想,我们怎么去获取到栈尾部或者栈首的元素呢?还记得数组的索引吗?可以用索引来假设为栈的指针。
    所以,我们还得定义好栈的元素个数和栈的默认长度以及默认的指针:

    private int stackLength = 4; // 数组的默认长度
    
    private int size; // 记住栈容器的元素个数
    
    private int index = -1; // 操作数组下标位置的指针
    

    为什么这儿指向的是-1呢?我们知道,数组的第一个元素是索引为0,那么-1的意思就是不指向任何元素。待会儿我们在用的时候再去指向他。

    然后,我们还得定义出数组的初始化。以及初始化的长度。参考官方文档的写法,当栈的长度满了之后我们就对栈长度进行1.5倍的扩容。我们就单独提取出一个方法来放置;

    /**
     * 数组初始化或者以1.5倍容量对数组扩容
     */
    private void capacity() {
        // 数组初始化
        if (this.arr == null) {
            this.arr = new Object[this.stackLength];
        }
        // 以1.5倍对数组扩容
        if (this.size - (this.stackLength - 1) >= 0) { // 如果当前数组的元素个数大于了当前数组的最后一个索引值
            this.stackLength = this.stackLength + (this.stackLength >> 1); // 位运算,让长度变成原来的1/2
            this.arr = Arrays.copyOf(this.arr, this.stackLength); // 复制一个新的数组,用新开辟的长度
        }
    }
    

    push方法

    如何给栈添加元素?我们要考虑的地方:指针向右移动一位,也就是说指针要+1。其次,添加完元素之后,栈元素的长度发生了变化,size+1 。

    public E push(E item){
        // 先初始化数组
        this.capacity();
        // 添加元素
        this.arr[++index] = item;
        // 记录元素个数加一
        this.size++;
        return item;
    }
    

    pop方法

    pop方法主要是用来移除栈顶的元素。
    先分析一下思路:我们要用index去指向栈顶的元素,该怎么去指定?
    删除之后,对应的size长度该怎么去改变?
    我们知道,当元素添加了之后,index会跟着改变,那么就好比我们添加了三个元素,此时的index应该就是指向的2。那就好办了。
    当移除的时候,我们只需要让index–来操作就能解决问题;看代码:

    /**
     * 获取栈顶元素
     *
     * @return
     */
    public E pop() {
        // 如果栈容器中没有元素则抛出异常
        if (this.index == -1) {
            throw new EmptyStackException();
        }
        // 记录元素个数
        this.size--;
        // 返回栈顶元素
        System.out.println("删除元素之前的当前下标:"+index);
        return (E) this.arr[index--];
    }
    

    empty方法

    判断栈是否为空,这很简单。直接判断当前的size是不是0就能解决:

    public boolean empty(){
    	return this.index==0?true:false;
    }
    

    全部代码

    package com.zxy;
    
    import java.util.Arrays;
    import java.util.EmptyStackException;
    
    /**
     * @Author Zxy
     * @Date 2021/2/2 20:24
     * @Version 1.0
     * 演示栈容器的使用
     */
    public class MyStack<E> {
    
        private Object[] arr; // 存放元素的物理结构
    
        private int stackLength = 4; // 数组的默认长度
    
        private int size; // 记住栈容器的元素个数
    
        private int index = -1; // 操作数组下标位置的指针
    
        /**
         * 判断栈容器是否为空
         */
        public boolean empty() {
            return this.size == 0 ? true : false;
        }
    
        /**
         * 获取栈顶元素
         *
         * @return
         */
        public E pop() {
            // 如果栈容器中没有元素则抛出异常
            if (this.index == -1) {
                throw new EmptyStackException();
            }
            // 记录元素个数
            this.size--;
            // 返回栈顶元素
            System.out.println("删除元素之前的当前下标:"+index);
            return (E) this.arr[index--];
        }
    
    
        /**
         * 向栈顶添加元素
         *
         * @param item
         * @return
         */
        public E push(E item) {
            // 初始化数组
            this.capacity();
            // 向数组中添加元素
            System.out.println("添加元素之前的下标:"+index);
            this.arr[++index] = item;
            System.out.println("添加元素之后的下标:"+index);
            // 记录元素个数
            this.size++;
            return item;
        }
    
        /**
         * 数组初始化或者以1.5倍容量对数组扩容
         */
        private void capacity() {
            // 数组初始化
            if (this.arr == null) {
                this.arr = new Object[this.stackLength];
            }
            // 以1.5倍对数组扩容
            if (this.size - (this.stackLength - 1) >= 0) { // 如果当前数组的元素个数大于了当前数组的最后一个索引值
                this.stackLength = this.stackLength + (this.stackLength >> 1); // 位运算,让长度变成原来的1/2
                this.arr = Arrays.copyOf(this.arr, this.stackLength); // 复制一个新的数组,用新开辟的长度
            }
        }
    
        public static void main(String[] args) {
            MyStack<String> stack = new MyStack<>();
            stack.push("a");
            stack.push("b");
            stack.push("c");
            System.out.println(stack.size);
    
            System.out.println("当前栈顶元素:"+stack.pop());
            /*System.out.println(stack.pop());
            System.out.println(stack.pop());*/
        }
    }
    
    

    以上就是我的一点小总结,如果哪儿不对,请指正。
    谢谢你长得这么好看还来看我的文章。

    展开全文
  • 也可以更好地提供评论功能,所以我也想试试用 <code>Github Issues</code> 来作为博客数据源。 <p>API在这:<a href="https://developer.github.com/v3/issues/">...
  • list_comments()(wordpress读取评论数据函数:wp_list_comments()),默认情况下最新的评论会展示在评论的最后,但是比较友好的方式是将最新的评论展示的评论列表的顶部,那这个功能怎么实现呢,其实很简单,我们进入...
  • 功能描述很简单,但是怎么实现呢?不卖关子了,要想实现这个功能,就需要对ios消息传递链和消息响应链有比较深入了解,在这里就不做太多描述,有想了解可以在评论里留言。 那么还是我一贯做法,贴代码: ...

    之前在银行做开发时有做过这么个功能:当用户5分钟内不操作手机时要弹出手势密码锁。银行嘛,安全性考虑的是最多的。

    功能描述很简单,但是怎么实现呢?不卖关子了,要想实现这个功能,就需要对ios的消息传递链和消息响应链有比较深入的了解,在这里就不做太多的描述,有想了解的可以在评论里留言。

    那么还是我的一贯做法,贴代码:

    首先是.h文件,并没有太多东西:

    #import <UIKit/UIKit.h>


    @interface DHFBaseWindow : UIWindow


    @end


    然后是.m文件:

    //可以用来判断屏幕是否有被触摸

    #import "DHFBaseWindow.h"

    #import "DHFModel_UserInfoManager.h"

    @implementation DHFBaseWindow


    - (void)sendEvent:(UIEvent *)event{

        [super sendEvent:event];

    }


    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{

        UIView * view = [super hitTest:point withEvent:event];

        [DHFModel_UserInfoManager sharedManager].mUserLastTouchView = view;

    //    NSLog(@"%@",view.debugDescription);

        return view;

    }


    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{

        return [super pointInside:point withEvent:event];

    }

    @end


    想要知道DHFModel_UserInfoManager做了什么?其实很简单 都是一些用户的信息,头像啊、用户名啊、还有就是最后点击的视图(或者说最后响应用户点击的视图)mUserLastTouchView  

    那么问题来了,这个类并没有实现5分钟不操作弹出密码键盘啊!当然 这篇文章只是提供思路,并且是为了解释上篇3Dtouch功能。

    要想实现几分钟不操作那个功能其实很简单:你可以在- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event里添加一个timer 如果调用就会重置timer

    就这样!

    展开全文
  • 微信发送空白朋友圈教程今天闲着没事,用微信发了条空白朋友圈,效果如下:也能这样发空白朋友圈,效果如下:不少朋友评论问我怎么做到?答案很简单,利用神秘代码,就能实现!如何获得这段朋友圈空白神秘代...

    今天玩机最前线跟大家分享的技巧是,微信发送空白朋友圈。新关注的用户,记得星标「玩机最前线」!

    如何在微信发送空白朋友圈呢?方法很简单,只要多多关注我们公众号玩机最前线,就能学到好多有趣的实用技巧啦!

    99402056-361b-eb11-8da9-e4434bdf6706.jpeg

    微信发送空白朋友圈教程

    今天闲着没事,用微信发了条空白朋友圈,效果如下:

    9a402056-361b-eb11-8da9-e4434bdf6706.png

    也能这样发空白朋友圈,效果如下:

    9b402056-361b-eb11-8da9-e4434bdf6706.png

    不少朋友评论问我怎么做到的?答案很简单,利用神秘代码,就能实现!

    如何获得这段朋友圈空白神秘代码呢?你只需要在公众号「玩机最前线」

    99402056-361b-eb11-8da9-e4434bdf6706.jpeg

    安卓用户回复关键字「35」,苹果用户回复关键字「36」,别弄错了,即可获得朋友圈空白神秘代码:

    9d402056-361b-eb11-8da9-e4434bdf6706.jpeg

    复制后,粘贴到朋友圈,像平时一样发送文字朋友圈即可。如果你想要这条空白朋友圈长一点,就粘贴后换行,多粘贴几遍就OK了。

    暂时不知道什么时候会失效,记得早点用!

    9e402056-361b-eb11-8da9-e4434bdf6706.png

    (本教程测试机型:iPhone XR)

    (本教程微信版本:7.0.4)

    总结

    想吸引大家的注意力,不妨考虑发条空白朋友圈晒下!但仔细想了下,这条空白朋友圈,除了拿来装逼之外,一无是处,说不定还会被人认为你是沙雕。。。。。。

    9f402056-361b-eb11-8da9-e4434bdf6706.jpeg

    a0402056-361b-eb11-8da9-e4434bdf6706.gif

    更多好玩技巧

    目前,我们的手机真伪查查功能如下,点击下面文字立即体验

    竖排文字签名生成

    上标电话号码生成器

    下标电话号码生成器

    下划线昵称生成器

    模糊昵称生成器

    上标昵称生成器(需英文)

    彩色昵称生成器(需英文,安卓)

    翅膀昵称生成器(支持多种样式)

    冒烟昵称生成器

    手机真伪查查

    温馨提醒:如何第一时间找到「手机真伪查查」小程序,点击左上角「...」,再选择「添加到我的小程序」就OK啦!

    点击下方小程序

    即可体验

    展开全文
  • 微信发送空白朋友圈教程今天闲着没事,用微信发了条空白朋友圈,效果如下:也能这样发空白朋友圈,效果如下:不少朋友评论问我怎么做到?答案很简单,利用神秘代码,就能实现!如何获得这段朋友圈空白神秘代...
  • 功能点 站内搜索、栏目管理、视频播放(完全模仿优酷视频页面)、焦点图、静态页面生成(新浪、搜狐等大型网站普遍采用技术)、文章管理、无刷新评论评论的无刷新分页、敏感词过滤、用户管理、友情链接管理、...
  • 前言最近看到一个案例,初看时候对产品经理说很简单,是一个长页面,底图上有按钮,按了之后有素材动效,这对于前端来说是基础的功能。后来定睛一看发现,还有微信阅读数和评论,发现事情并不简单。整个交互层...
  • Hooks 方式去实现这个功能。 <pre><code>jsx function Counter() { const [count, setCount] = React.useState(0) return ( <div> Count: {count} <button onclick="{()"> set...
  • 一个WEB APP开发问题

    2016-04-08 22:27:15
    有一门课要做一个WEB项目,我们想做一个简单的。功能是一所大学课程的打分网站。学生注册登录自己的账户后,查看课程,...各个层怎么开发,最好有简单的例子,最简单的就行,比如登录。另外web api可以选择java语言吗。
  • 5. 高可扩展性,用户通过简单的学习后,可以自定义配置门户、流程应用、内容管理应用 更多的产品介绍、使用说明、下载、在线体验、API及讨论请移步至http://www.o2oa.net/ 官方网站: 开源主页 : ...
  • 功能点 站内搜索、栏目管理、视频播放(完全模仿优酷视频页面)、焦点图、静态页面生成(新浪、搜狐等大型网站普遍采用技术)、文章管理、无刷新评论评论的无刷新分页、敏感词过滤、用户管理、友情链接管理、...
  • 正如我在某次“还我旧版”运动中听到声音,“不管怎么改版,只要友邻们还在就好”,改版是豆瓣不断良好发展必经之路,但这句话中对友邻珍重又令我感受到了豆瓣宝贵特质。 豆瓣作为一个工具价值可以通过...
  • 【Matlab】二维图堆叠为三维图

    千次阅读 2018-12-04 21:54:15
    最简单的方法是使用Origin,Origin也可以很容易实现上述的功能,绘图教程:http://www.cappchem.com/Article2014/1108.html。 但是,目前笔者在Matlab中尚未找到现成的相关的绘图方法供我们使用,如果看到这里的...
  • 移动应用中最简单的手势,就是touch手势,而这也是应用中最常使用的手势,类比web开发中的事件,就好比web开发中的click。在web开发中,浏览器内部实现了click事件,我们可以...
  • 经常看到贴吧或者是在评论区看到有人将电影中片段转换成gif动画,感觉挺酷,于是便自己尝试用Photoshop(以下简称PS)做了起来,其实操作起来还是很简单,接下来用图解形式介绍给大家怎么用PS实现。废话不多说,...
  • 一个最简单的 JavaBeanMaker 一些非常有用的JAVA常用方法 以下这段servlet代码变为jsp大致的写法是 异常处理优劣观 用 Java 保存位图文件 用 Java 保存位图文件 用 javabean 来实现 MySQL 的分页显示 用 JSP ...
  • 从零开始写项目第五篇【评论功能、备忘录】 从零开始写项目终极【维护网站、修复Bug】 从零开始写项目【总结】 带你搭一个SpringBoot+SpringData JPADemo 【极简版】SpringBoot+SpringData JPA 管理系统 :jack_o...
  • 生命周期钩子是一些简单的函数,这些函数会在Angular应用组件特定生命周期被调用。生命周期钩子在Angular 1.5版本被引入,通常与.component()方法一起使用,并在接下来的几个版本中演变,并...
  • 将对应信息显示出来,并且提供加入到购物车的功能。商品详细信息下方显示其他用户留言,并且已经登录用户可以对这件商品进行评论,商品发布者可以在下方查看留言,并且与有意者打成交易意向。商品发布者可以在...
  • 完美学校网站系统全站源代码学校网站模板下载

    千次下载 热门讨论 2011-01-10 12:23:08
    系统前台的栏目、菜单、功能入口等全部实现后台控制,用户只需在后台进行简单的设置即可制作出适合于自身学校的网站系统。 我们将推出更多的学校机构网站,学校网站管理系统,做中国最好,易用,安全的学校上网,学校信息...
  • 13.2 一个简单的string模板 293 13.2.1 定义一个模板 294 13.2.2 模板实例化 295 13.2.3 模板参数 296 13.2.4 类型等价 296 13.2.5 类型检查 297 13.3 函数模板 298 13.3.1 函数模板的参数 299 13.3.2 函数...
  • 13.2 一个简单的string模板 293 13.2.1 定义一个模板 294 13.2.2 模板实例化 295 13.2.3 模板参数 296 13.2.4 类型等价 296 13.2.5 类型检查 297 13.3 函数模板 298 13.3.1 函数模板的参数 299 13.3.2 函数...
  • 11.4.2 一个简单的基于函数的索引例子 414 11.4.3 只对部分行建立索引 422 11.4.4 实现有选择的唯一性 424 11.4.5 关于ORA-01743的警告 424 11.4.6 基于函数的索引小结 425 11.5 应用域索引 -1 11.6 关于索引...
  • C++程序设计语言(特别版)--源代码

    热门讨论 2012-04-23 07:33:51
    13.2 一个简单的string模板 293 13.2.1 定义一个模板 294 13.2.2 模板实例化 295 13.2.3 模板参数 296 13.2.4 类型等价 296 13.2.5 类型检查 297 13.3 函数模板 298 13.3.1 函数模板的参数 299 13.3.2 函数...
  • 系统前台的栏目、菜单、功能入口等全部实现后台控制,用户只需在后台进行简单的设置即可制作出适合于自身学校的网站系统。 我们将推出更多的学校机构网站,学校网站管理系统,做中国最好,易用,安全的学校上网,学校...
  • 现后台控制,用户只需在后台进行简单的设置即可制作出适合于自身学校的网站系统。 我们将推出更多的学校机构网站,学校网站管理系统,做中国最好,易用,安全的学校上网,学校信息化网站系统, 强大学校网站系统全站源...
  • 软件工程教程

    热门讨论 2012-07-06 23:10:29
    本项目从Android声音处理入手,实现音乐功能,根据用户兴趣,提高用户参与度。 问:有什么应用价值? 答: 本项目是一个能提高用户参与和娱乐程序项目,具有一定使用价值。 追求结果--钢琴练奏师 1.2 ...

空空如也

空空如也

1 2 3
收藏数 46
精华内容 18
关键字:

最简单的评论功能怎么实现