一個非常適合nodejs初探者練手的全棧專案

藥不能停呀發表於2017-10-30

寫在前面:

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


  • 專案實質: 一個基於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語法特性的強大及嚴謹性。

相關文章