一个非常适合nodejs初探者练手的全栈项目

7,883 阅读10分钟

写在前面:

这是算是一个前端萌新第一次涉及到前后端的全栈开发项目,可能涉及到的技术栈用的并不是深入,对许多中间件的总结所述必然会有所欠缺,但本文旨在浅析整个项目过程,同时去还原深化一些相关的概念。而且第一次写文章思路排版等不是很明确,有不对的地方欢迎各位大佬们指正!


  • 项目实质: 一个基于nodejs、express框架及mongoDB数据库搭建的简易博客系统

  • 效果实现 :主体分为前后页面,前台包括用户注册登录面板,文章内容的分页、分类展示;内容详情页有文章内容展示,底部有评论信息展示。后台管理页面包括管理首页、注册用户详细信息、文章分类管理页、文章分类添加页、所有文章信息页、添加文章页。实现对整站整站所有内容的增删改查。整站部分页面用bootstrap组件搭建,天然响应式,但是样式很一般。

  • 页面预览

    前台主页
    前台主页

    内容详情
    内容详情

    后台
    后台

    数据库
    数据库

  • 技术栈

    • nodeJs 搭建基本的后端环境
    • express 实现页面路由设计、页面渲染、后端数据处理、静态资源的托管等
    • mongoose nodejs后端与MongoDB数据库连接的桥梁,定义数据库表结构、构建表模型、通过操作表模型实现对数据库的增删改查。
    • ajax 实现用户注册、登录相关逻辑判断与验证、无刷新提交平论、获取评论
    • body-parser 用于处理前端post请求提交过来的数据
    • cookies 保持用户登录状态,作为中间变量传递给模板实现逻辑上的渲染
    • es6 模板字符串渲染评论,后端数据回馈的大面积promise操作
    • swig 模板渲染引擎,实现页面的引用、继承、代码的复用从而提高页面性能
  • 开发环境
    • webstorm 同时在这里推荐一下这个强大的IED集成开发环境,比如版本控制、依赖安装、初始化构建、代码提示等等,特别适合初学者用来开发前后端项目
    • mongoDB 这是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的,所以特别适合用来实践前后端项目
  • 源码地址github.com/formattedzz…
  • 获取方式: 初始化一个本地仓库,fork上面地址的库然后git下来。安装MongoDB数据库(最好也装一个可视化工具比较直观查看到数据源),在项目根目录下新建一个db文件夹作为本站数据库然后连接起来(具体做法可以参考mongoDB官方文档),最后运行入口文件app.js在浏览器输入localhost:8888就ok了。如果大家有问题欢迎到我个人博客联系我一起探讨。

项目解剖

文件结构如下

-project    
    db                    //数据库文件,存取整站页面上所有数据
    -models               //数据库模型,和schemas下的表结构一一对应
        user.js
        category.js
        content.js
    -schemas              //表结构,一个js文件对应一张表,定义每张表的数据结构
        users.js          
        categories.js
        contents.js
    node_modules
    -public               //静态资源存放区
        css
        img
        font
        -js
            jquery.js   
            bootstrap.js
            index.js
    -routers              //三个路由模块,分别处理不同的业务逻辑
        api.js            //api模块;负责处理前台页面登录注册及提交评论等
        main.js           //负责接收前台操作请求、渲染前台页面
        admin.js          //负责接收后台管理操作请求、渲染后台页面
    -views                //所有浏览请求后端返回的页面都从这里取
        mian
        -admin
            index.html
            layout.html
            view.html
    app.js                //入口文件,运行它就等于开启了我们的服务器
    package.json          //在这里可以查询你安装的中间件及其版本号

入口文件解析

/**
 * Created by Administrator on 2017/10/24.
 */
//加载express模块
var express = require("express");

//加载swig模块
var swig = require("swig");

//加载mongoose模块,这个中间件是nodejs与mongoDB数据库的桥梁
var mongoose = require("mongoose");

//加载用户表模型,模型从表结构构建出来,然后我们操作模型操作数据
var User = require("./models/user");

//加载kooies模块,用于在登录成功后再req中写入cookie,然后就可以再刷新或请求页面时
//将cookie变量传递给模板用于渲染验证
var Cookies = require('cookies');

//创建一个新的服务器,相当于httpcreateServer
var app = express();

//静态文件资源托管,js css img等,浏览器在解析页面是遇到的所有url都会发送请求给后端,
//我们不可能在后端给每个js、css或img的url都设置路由监听,这样以/public开头的请求都会被
//指引到public目录下去调取资源并返回
app.use("/public",express.static( __dirname+"/public"));

//定义应用使用的模板引擎,第一个参数:所要渲染模板文件的后缀,也是模板引擎的名称,第二个参数:渲染的方法
app.engine("html",swig.renderFile);
//定义模板文件存放的路径,第一个参数必须是views,这是模块内指定的解析字段,第二个参数为路径:./表示根目录
app.set("views","./views");
//注册使用模板引擎;第一个参数不能变,第二个参数和上面的html一致
app.set("view engine","html");
//设置完就可以直接在res中渲染html文件了:res.render("index.html",{要渲染的变量})
//第一个参数是相对于views文件夹的路径

//在开发过程中要取消模板缓存,便于调试,在模板页面有任何修改保存后浏览器就能同步更新了
swig.setDefaults({cache : false});

//var User = require("./models/user");

//加载bodyparser模块,用来解析前端post方式提交过来的数据
//详细文档:https://github.com/expressjs/body-parser
var bodyparser = require("body-parser");
app.use(bodyparser.urlencoded({extended:true}));


//app.use里的函数是一个通用接口,所有的页面的刷新及请求都会执行这个函数
app.use( function(req, res, next) {
    req.cookies = new Cookies(req, res);
//在req对象下建立一个cookie属性,在登录成功后就会被附上用户的信息,之后页面的刷新和
//请求的请求头里都会附带这个cookie发送给后端,且其会一直存在直到退出登录或关闭浏览器,
//当然也可以设置它的有效时间

    req.userInfo = {};
    if(req.cookies.get('userInfo')){
        var str1 = req.cookies.get('userInfo');
        req.userInfo=JSON.parse(str1);
        User.findById(req.userInfo._id).then(function(userInfodata){
            req.userInfo.isadmin = Boolean(userInfodata.isadmin);
        });
    }
    next();

} );


//分模块开发,便于代码管理,分为前台展示模块,后台管理模块及逻辑接口模块
app.use("/admin" ,require("./routers/admin"));
app.use("/" ,require("./routers/main"));
app.use("/api" ,require("./routers/api"));

//链接数据库,成功之后再开启端口监听
mongoose.connect("mongodb://localhost:27017/myBlog");
var db = mongoose.connection;
db.once("open", function () {
    console.log("Mongo Connected");
    app.listen(8888);
});
db.on("error", console.error.bind(console, "Mongoose Connection Error"));

在schemas/users.js中定义用户信息的数据结构:

var mongoose = require("mongoose");
module.exports = new mongoose.Schema({
    username: String,
    password: String,
    isadmin:{
        type:Boolean,
        default:false
    }
});

在models/user.js中构建用户表模型

var mongoose = require("mongoose");

var userschama = require("../schemas/users");

module.exports = mongoose.model("User",userschama);

这样我们在路由js文件中根据相对路径引入user.js用户表模型就能用mongoose的语法来对这张表的数据进行增删改查了,以后端登录验证代码为例:

router.post("/user/login",function(req ,res ){
    var username = req.body.username;
    //通过body-parser中间件post提交的数据都在req对象下的bodys属性中
    var password = req.body.password;

    if(username == ""||password==""){
        resdata.code=1;
        resdata.message="用户名和密码不能为空!";
        res.json(resdata);
        return;
    }
//User为引入的用户信息表模型,根据前端提交的数据作为第一个参数进行查询
    User.findOne({
        username:username,
        password:password
    },function(err,userinfo){
        if(err){
            console.log(err);
        }
        if(!userinfo){
            resdata.code = 2;
            resdata.message = "用户名或密码错误!";
            res.json(resdata);
            return false;
        }
        resdata.message = "登录成功!";
        resdata.userinfo={
            id:userinfo._id ,
            username:userinfo.username
        };
//登录成功后给cookie设置对象字符串
        req.cookies.set('userInfo', JSON.stringify({
            "_id": userinfo._id,
            "username": userinfo.username
        }));
        res.json(resdata);
    })

});

给渲染文件传递变量,模板就会根据变量来决定渲染那些部分

//渲染首页
router.get("/", function(req, res) {

    res.render('main/index', {
        userInfo:req.userInfo  
//req.userInfo在入口文件中根据cookie做过统一配置,里面包含当前用户是否为管理员的属性
    });

});

在页面模板中

{% if userInfo.isadmin %}
    <p>尊敬的管理员! <a href="/admin/"> 点击这里</a>进入管理页面</p>
{% else %}
    <p>你好,欢迎光临我的博客!</p>
{% endif %}

在文件目录中我们看到,前台页面写了三个,layout,index,view,分别是头部和侧边栏共用的布局模板,主页面及内容详情页。这里用到了模板的继承,后面会用到分页的引用,两者差不多,都是将共用的部分写到layout.html里面,然后在主页面:

layout.html:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
            <!--共用部分-->

        {% block content %}
        {% endblock %}

            <!--共用部分-->
    </head>
    <body>

    </body>
    </html>

index.html:

    {% extends "layout.html" %}

    {% block content %}

        <!--不同的部分-->

    {% endblock %}

用户信息的分页处理:

router.get("/user",function(req,res){
<!--page的值在分页按钮url的hash值中设定-->
    var page = Number(req.query.page||1);
    var limit = 8;
    var skip = (page-1)*limit;
    var total;
    var counts;
    User.count().then(function(count){
        total = Math.ceil(count/limit);
        page = Math.max(1,page);
        page = Math.min(page,total);
        counts = count;
    });
<!--limit为限制获取的条数,skip为跳过多少条选取-->
    User.find().limit(limit).skip(skip).then(function(users){

        res.render("admin/userindex",{
            userInfo:req.userInfo,
            users:users,
            page:page,
            total:total,
            counts:counts
        })
    });

});

<!--之后在模板中根据接收到的数据进行渲染:-->
<tr >
        <th>用户ID</th>
        <th>用户名</th>
        <th>密码</th>
        <th>是否为管理员</th>
    </tr>
    {% for user in users %}
    <tr>
        <td>{{user._id.toString()}}</td>
        <td>{{user.username}}</td>
        <td>{{user.password}}</td>
        <td>{{user.isadmin}}</td>
    </tr>
    {% endfor %}

文章或分类的添加、修改则在每篇文章或分类的链接hash值键入响应的ID,这样在get页面的时候,数据才能根据ID来提取响应的信息赋给新页面渲染。

router.get("/category/edit",function(req,res){
    var cateid = req.query.id||"";
    //获取ID,然后根据ID查询
    Category.find({id:cateid}).then(function(cateinfo){
        res.render("admin/categoryedit",{
            userInfo:req.userInfo ,
            name:cateinfo.name
        });
    });

});

router.post("/category/edit",function(req,res){
    var name =req.body.name||"";
    var id = req.query.id||"";

    if(name==""){
        res.render("admin/error",{userInfo:req.userInfo});
        return false;
    }else{
        Category.findOne({_id:id},function(err,info){
            if(err){
                console.log(err);
            }
            if(info){
                console.log(info);
                info.name = name;
                info.save();
                res.render("admin/success",{userInfo:req.userInfo});
            }

        });
    }
});

表字段的关联与引入,在文章表结构中,其作者跟分类都是与用户表和分类表关联的:

var mongoose = require("mongoose");

module.exports = new mongoose.Schema({
    title: String,
    category : {
        type:mongoose.Schema.Types.ObjectId,
        ref : "Category"
    },
    // 分类数据类型为对象id,关联了Category,Category为分类模型中定义的名字,必须一致
    composition:{
        type: String,
        default : ""
    },
    description :{
        type: String,
        default : ""
    },
    user:{
        type:mongoose.Schema.Types.ObjectId,
        ref : "User"
    },
    num:{
        type:Number,
        dafault:0
    },
    addtime:{
        type:Date,
        default: new Date()
    },
    comment:{
        type:Array,
        default:[]
    }
});
//get所有文章主页面中,用.populate方法就能相对应的值了
router.get("/content",function(req,res){
    Content.find().populate(["category","user"]).sort({_id:-1}).then(function(contents){
        //console.log(contents);
        res.render("admin/content",{
            userInfo:req.userInfo,
            contents:contents
        });
    });

});

踩坑指南

  1. 三个分模块处理的app.use一定要放在设置cookie的后面,如上,否则会导致cookie加载不上。
  2. 要深刻理解get和post的区别,get方式不能改变后端数据的任何数据,只能获取,在用ajax获取评论的时候,为了能在最上面显示最新的评论,在赋给resdata数据的时候变直接做了反转,此时两者存在引用关系,也就改变了原有的顺序,导致浏览器端报500错误,也是郁闷了好久
router.get('/pinglun', function(req, res) {
    var contentid = req.query.contentid || '';
    Content.findOne({
        _id: contentid
    }).then(function(content) {
        resdata.postdata = content;
        //resdata.data.comments.reverse();
        res.json(resdata);
    })
});

3.通用模块处理的时候不要忘了next()函数的执行,如:

//统一返回给前端的数据格式
var resdata;
router.use(function(req,res,next){
    resdata = {
        code:0,
        message:""
    };
    next();
});
  1. 在评论分页部分,是直接用ajax请求过来的数据在前端js中完成的,比如每页显示n条,当评论数小于n的时候,需要再对数组做进一步的处理
  2. index.html中的js应先引入jq然后bootstrap,这里js不多,所以当项目很大的时候我们就能体会到webpack等前端自动化构建工具的强大了
  • 项目收获 :初步熟悉了全栈项目的开发流程,加深前后端数据交互方面的概念,了解了一些中间件的特性,体会了es6语法特性的强大及严谨性。