寫在前面:
這是算是一個前端萌新第一次涉及到前後端的全棧開發專案,可能涉及到的技術棧用的並不是深入,對許多中介軟體的總結所述必然會有所欠缺,但本文旨在淺析整個專案過程,同時去還原深化一些相關的概念。而且第一次寫文章思路排版等不是很明確,有不對的地方歡迎各位大佬們指正!
專案實質: 一個基於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
});
});
});複製程式碼
踩坑指南
- 三個分模組處理的app.use一定要放在設定cookie的後面,如上,否則會導致cookie載入不上。
- 要深刻理解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();
});複製程式碼
- 在評論分頁部分,是直接用ajax請求過來的資料在前端js中完成的,比如每頁顯示n條,當評論數小於n的時候,需要再對陣列做進一步的處理
- index.html中的js應先引入jq然後bootstrap,這裡js不多,所以當專案很大的時候我們就能體會到webpack等前端自動化構建工具的強大了
- 專案收穫 :初步熟悉了全棧專案的開發流程,加深前後端資料互動方面的概念,瞭解了一些中介軟體的特性,體會了es6語法特性的強大及嚴謹性。