一個很是適合nodejs初探者練手的全棧項目

寫在前面:

這是算是一個前端萌新第一次涉及到先後端的全棧開發項目,可能涉及到的技術棧用的並非深刻,對許多中間件的總結所述必然會有所欠缺,但本文旨在淺析整個項目過程,同時去還原深化一些相關的概念。並且第一次寫文章思路排版等不是很明確,有不對的地方歡迎各位大佬們指正!css


  • 項目實質: 一個基於nodejs、express框架及mongoDB數據庫搭建的簡易博客系統html

  • 效果實現 :主體分爲先後頁面,前臺包括用戶註冊登陸面板,文章內容的分頁、分類展現;內容詳情頁有文章內容展現,底部有評論信息展現。後臺管理頁面包括管理首頁、註冊用戶詳細信息、文章分類管理頁、文章分類添加頁、全部文章信息頁、添加文章頁。實現對整站整站全部內容的增刪改查。整站部分頁面用bootstrap組件搭建,自然響應式,可是樣式很通常。前端

  • 頁面預覽
    node

    前臺主頁
    前臺主頁

    內容詳情
    內容詳情

    後臺
    後臺

    數據庫
    數據庫

  • 技術棧jquery

    • 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的語法來對這張表的數據進行增刪改查了,之後端登陸驗證代碼爲例:webpack

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);
    })

});複製代碼

給渲染文件傳遞變量,模板就會根據變量來決定渲染那些部分git

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

    res.render('main/index', {
        userInfo:req.userInfo  
//req.userInfo在入口文件中根據cookie作過統一配置,裏面包含當前用戶是否爲管理員的屬性
    });

});複製代碼

在頁面模板中es6

{% if userInfo.isadmin %}
    <p>尊敬的管理員! <a href="/admin/"> 點擊這裏</a>進入管理頁面</p>
{% else %}
    <p>你好,歡迎光臨個人博客!</p>
{% endif %}複製代碼

在文件目錄中咱們看到,前臺頁面寫了三個,layout,index,view,分別是頭部和側邊欄共用的佈局模板,主頁面及內容詳情頁。這裏用到了模板的繼承,後面會用到分頁的引用,二者差很少,都是將共用的部分寫到layout.html裏面,而後在主頁面:github

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 %}複製代碼

用戶信息的分頁處理:web

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語法特性的強大及嚴謹性。
相關文章
相關標籤/搜索