使用 ExpressJS 構建完整的 MVC 網站
Chinese (Traditional) (中文(繁體)) translation by Fuhuan (you can also view the original English article)
在本文中,我們將構建壹個包含前端客戶端的完整網站,以及用於管理網站內容的控制面板。 正如妳可能猜到的,應用程序的最終工作版本包含許多不同的文件。 我按照開發過程壹步壹步地編寫本教程,但是我沒有包含每個文件,因為這會使這篇文章變得非常冗長乏味。 但是,源代碼可以在 GitHub 上找到,我強烈建議妳看壹下。
介紹
Express 是 Node 的最佳框架之壹。 它有強大的支持和壹些有用的功能。 那裏有很多很棒的文章,涵蓋了所有的基礎知識。 但是,這次我想深入挖掘並分享我創建完整網站的工作流程。 通常,本文不僅適用於 Express,還適用於與 Node 開發人員可用的其他壹些優秀工具結合使用。
我假設妳熟悉 Nodejs,將它安裝在妳的系統上,並且妳可能已經使用它構建了壹些應用程序。
核心是連接。 這是壹個中間件框架,它帶有很多有用的東西。 如果妳想知道究竟什麽是中間件,這裏有壹個簡單的例子:
1 |
var connect = require('connect'),
|
2 |
http = require('http');
|
3 |
|
4 |
var app = connect() |
5 |
.use(function(req, res, next) {
|
6 |
console.log("That's my first middleware");
|
7 |
next(); |
8 |
}) |
9 |
.use(function(req, res, next) {
|
10 |
console.log("That's my second middleware");
|
11 |
next(); |
12 |
}) |
13 |
.use(function(req, res, next) {
|
14 |
console.log("end");
|
15 |
res.end("hello world");
|
16 |
}); |
17 |
|
18 |
http.createServer(app).listen(3000); |
中間件基本上是壹個接受請求和響應對象以及下壹個功能的函數。 每個中間件都可以決定使用響應對象進行響應,或者通過調用下壹個回調將流傳遞給下壹個函數。 在上面的示例中,如果在第二個中間件中刪除 next()方法調用,則 hello world 字符串將永遠不會發送到瀏覽器。 壹般來說,這就是 Express 的工作方式。 有壹些預定義的中間件,當然,這可以為妳節省大量時間。 例如,Body 解析器解析請求主體並支持 application / json,application / x-www-form-urlencoded 和 multipart / form-data。 或 Cookie 解析器,它解析 cookie 頭並使用由 cookie 名稱鍵入的對象填充 req.cookies。
Express 實際上包裝了 Connect 並在其周圍添加了壹些新功能。 例如,路由邏輯,這使得流程更加順暢。 以下是處理 GET 請求的示例:
1 |
app.get('/hello.txt', function(req, res){
|
2 |
var body = 'Hello World'; |
3 |
res.setHeader('Content-Type', 'text/plain');
|
4 |
res.setHeader('Content-Length', body.length);
|
5 |
res.end(body); |
6 |
}); |
建立
設置 Express 有兩種方法。 第壹個是將它放在妳的 package.json 文件中並運行 npm install(有壹個笑話,npm 意味著沒問題的人 :))。
1 |
{
|
2 |
"name": "MyWebSite", |
3 |
"description": "My website", |
4 |
"version": "0.0.1", |
5 |
"dependencies": {
|
6 |
"express": "3.x" |
7 |
} |
8 |
} |
框架的代碼將放在 node_modules 中,妳將能夠創建它的實例。 但是,我更喜歡使用命令行工具的替代選項。 只需使用 npm install -g express 全局安裝 Express 。 通過這樣做,妳現在擁有了壹個全新的 CLI 工具。 例如,如果妳運行:
1 |
express --sessions --css less --hogan app |
Express 將創建壹個應用程序框架,其中包含壹些已為妳配置的內容。 以下是 express(1)命令的用法選項:
1 |
Usage: express [options]
|
2 |
Options: |
3 |
-h, --help output usage information |
4 |
-V, --version output the version number |
5 |
-s, --sessions add session support |
6 |
-e, --ejs add ejs engine support (defaults to jade) |
7 |
-J, --jshtml add jshtml engine support (defaults to jade) |
8 |
-H, --hogan add hogan.js engine support |
9 |
-c, --css add stylesheet support (less|stylus) (defaults to plain css) |
10 |
-f, --force force on non-empty directory |
正如妳所看到的,只有幾個選項,但對我來說它們已經足夠了。 通常我使用較少的 CSS 預處理器和 hogan 作為模板引擎。 在這個例子中,我們還需要會話支持,所以 --sessions 參數解決了這個問題。 上面的命令完成後,我們的項目如下所示:
1 |
/public |
2 |
/images |
3 |
/javascripts |
4 |
/stylesheets |
5 |
/routes |
6 |
/index.js |
7 |
/user.js |
8 |
/views |
9 |
/index.hjs |
10 |
/app.js |
11 |
/package.json |
如果妳查看 package.json 文件,妳將看到我們需要的所有依賴項都添加到此處。 雖然它們尚未安裝。 為此,只需運行 npm install,然後會彈出 node_modules 文件夾。
我意識到上述方法並不總是合適的。 妳可能希望將路由處理程序放在另壹個目錄或類似的東西中。 但是,正如妳將在接下來的幾章中看到的那樣,我將對已經生成的結構進行更改,這很容易做到。 因此,妳應該將 express(1)命令視為樣板生成器。
敏捷開發
在本教程中,我設計了壹個名為 FastDelivery 的虛假公司的簡單網站。 這是完整設計的屏幕截圖:



在本教程結束時,我們將擁有壹個完整的 Web 應用程序,其中包含壹個可用的控制面板。 我們的想法是在不同的限制區域內管理網站的每個部分。 布局是在 Photoshop 中創建的,並切成 CSS(less)和 HTML(hogan)文件。 現在,我不打算介紹切片過程,因為它不是本文的主題,但如果妳對此有任何疑問,請不要猶豫。 切片後,我們有以下文件和 app 結構:
1 |
/public |
2 |
/images (there are several images exported from Photoshop) |
3 |
/javascripts |
4 |
/stylesheets |
5 |
/home.less |
6 |
/inner.less |
7 |
/style.css |
8 |
/style.less (imports home.less and inner.less) |
9 |
/routes |
10 |
/index.js |
11 |
/views |
12 |
/index.hjs (home page) |
13 |
/inner.hjs (template for every other page of the site) |
14 |
/app.js |
15 |
/package.json |
以下是我們要管理的網站元素列表:
- 主頁(中間的橫幅 - 標題和文字)
- 博客(添加,刪除和編輯文章)
- 服務頁面
- 職業頁面
- 聯系頁面
組態
在開始真正的實施之前,我們必須做壹些事情。 配置設置就是其中之壹。 讓我們假設我們的小站點應該部署到三個不同的位置 - 本地服務器,登臺服務器和生產服務器。 當然,每個環境的設置都不同,我們應該實現壹個足夠靈活的機制。 如妳所知,每個節點腳本都作為控制臺程序運行。 因此,我們可以輕松發送將定義當前環境的命令行參數。 我將該部分包裝在壹個單獨的模塊中,以便稍後為其編寫測試。 這是 /config/index.js 文件:
1 |
var config = {
|
2 |
local: {
|
3 |
mode: 'local', |
4 |
port: 3000 |
5 |
}, |
6 |
staging: {
|
7 |
mode: 'staging', |
8 |
port: 4000 |
9 |
}, |
10 |
production: {
|
11 |
mode: 'production', |
12 |
port: 5000 |
13 |
} |
14 |
} |
15 |
module.exports = function(mode) {
|
16 |
return config[mode || process.argv[2] || 'local'] || config.local; |
17 |
} |
目前只有兩種設置 - 模式和端口。 正如妳可能猜到的,應用程序為不同的服務器使用不同的端口。 這就是為什麽我們必須在 app.js 中更新站點的入口點。
1 |
... |
2 |
var config = require('./config')();
|
3 |
... |
4 |
http.createServer(app).listen(config.port, function(){
|
5 |
console.log('Express server listening on port ' + config.port);
|
6 |
}); |
要在配置之間切換,只需在最後添加環境。 例如:
1 |
node app.js staging |
會產生:
1 |
Express server listening on port 4000 |
現在我們將所有設置放在壹個地方,並且它們易於管理。
測試
我是 TDD 的忠實粉絲。 我將嘗試涵蓋本文中使用的所有基類。 當然,對絕對壹切進行測試會使寫作時間過長,但總的來說,這就是在創建自己的應用程序時應該如何進行。 我最喜歡的測試框架之壹是茉莉花。 當然它在 npm 註冊表中可用:
1 |
npm install -g jasmine-node |
讓我們創建壹個測試目錄來保存我們的測試。 我們要檢查的第壹件事是配置設置 spec 文件必須以.spec.js 結尾,因此該文件應該被稱為 config.spec.js。
1 |
describe("Configuration setup", function() {
|
2 |
it("should load local configurations", function(next) {
|
3 |
var config = require('../config')();
|
4 |
expect(config.mode).toBe('local');
|
5 |
next(); |
6 |
}); |
7 |
it("should load staging configurations", function(next) {
|
8 |
var config = require('../config')('staging');
|
9 |
expect(config.mode).toBe('staging');
|
10 |
next(); |
11 |
}); |
12 |
it("should load production configurations", function(next) {
|
13 |
var config = require('../config')('production');
|
14 |
expect(config.mode).toBe('production');
|
15 |
next(); |
16 |
}); |
17 |
}); |
運行 jasmine-node ./tests 妳應該看到以下內容:
1 |
Finished in 0.008 seconds
|
2 |
3 tests, 6 assertions, 0 failures, 0 skipped |
這壹次,我首先編寫了實現,然後編寫了第二個測試。 這並不是 TDD 的做事方式,但在接下來的幾章中我會做相反的事情。
我強烈建議花大量時間編寫測試。 沒有比完全測試的應用程序更好的了。
幾年前,我意識到壹些非常重要的東西,它可以幫助妳制作更好的程序。 每次開始編寫新類,新模塊或新邏輯時,請問自己:
我該怎麽測試呢?
我該怎麽測試呢?這個問題的答案將幫助妳更有效地編寫代碼,創建更好的 API,並將所有內容放入分離良好的塊中。 妳不能為意大利代碼編寫測試。 例如,在上面的配置文件中(/config/index.js) 我添加了在模塊的構造函數中發送模式的可能性。 妳可能想知道,當主要想法是從命令行參數獲取模式時,為什麽要這樣做? 這很簡單...... 因為我需要測試它。 讓我們假設壹個月後我需要檢查生產配置中的某些內容,但是節點腳本是使用 staging 參數運行的。 沒有那麽小的改進,我將無法做出這種改變。 之前的壹個小步驟現在實際上可以防止將來出現問題。
數據庫
由於我們正在構建壹個動態網站,我們需要壹個數據庫來存儲我們的數據。 我選擇使用 mongodb 作為本教程。 Mongo 是壹個 NoSQL 文檔數據庫。 可以在此處找到安裝說明,因為我是 Windows 用戶,所以我遵循 Windows 安裝。 完成安裝後,運行 MongoDB 守護程序,默認情況下偵聽端口 27017。 因此,理論上,我們應該能夠連接到此端口並與 mongodb 服務器通信。 要從節點腳本執行此操作,我們需要壹個 mongodb 模塊 / 驅動程序。 如果妳下載了本教程的源文件,則該模塊已添加到 package.json 文件中。 如果沒有,只需將 “mongodb”:“1.3.10” 添加到妳的依賴項並運行 npm install。
接下來,我們將編寫壹個測試,檢查是否有 mongodb 服務器在運行。 /tests/mongodb.spec.js 文件:
1 |
describe("MongoDB", function() {
|
2 |
it("is there a server running", function(next) {
|
3 |
var MongoClient = require('mongodb').MongoClient;
|
4 |
MongoClient.connect('mongodb://127.0.0.1:27017/fastdelivery', function(err, db) {
|
5 |
expect(err).toBe(null); |
6 |
next(); |
7 |
}); |
8 |
}); |
9 |
}); |
mongodb 客戶端的.connect 方法中的回調接收 db 對象。 我們稍後將使用它來管理我們的數據,這意味著我們需要在模型中訪問它。 每當我們必須向數據庫發出請求時,創建壹個新的 MongoClient 對象並不是壹個好主意。 這就是為什麽我在 connect 函數的回調中移動了快速服務器的運行:
1 |
MongoClient.connect('mongodb://127.0.0.1:27017/fastdelivery', function(err, db) {
|
2 |
if(err) {
|
3 |
console.log('Sorry, there is no mongo db server running.');
|
4 |
} else {
|
5 |
var attachDB = function(req, res, next) {
|
6 |
req.db = db; |
7 |
next(); |
8 |
}; |
9 |
http.createServer(app).listen(config.port, function(){
|
10 |
console.log('Express server listening on port ' + config.port);
|
11 |
}); |
12 |
} |
13 |
}); |
更好的是,由於我們有配置設置,最好將 mongodb 主機和端口放在那裏,然後將連接 URL 更改為:
1 |
'mongodb://' + config.mongo.host + ':' + config.mongo.port + '/fastdelivery' |
密切關註中間件:attachDB,我在調用 http.createServer 函數之前添加了它。 由於這壹點添加,我們將填充請求對象的.db 屬性。 好消息是我們可以在路徑定義期間附加多個功能。 例如:
1 |
app.get('/', attachDB, function(req, res, next) {
|
2 |
... |
3 |
}) |
因此,Express 會事先調用 attachDB 來到達我們的路由處理程序。 壹旦發生這種情況,請求對象將具有.db 屬性,我們可以使用它來訪問數據庫。
MVC
我們都知道 MVC 模式。 問題是這如何適用於 Express。 或多或少,這是壹個解釋問題。 在接下來的幾章中,我將創建模塊,它們充當模型,視圖和控制器。
模型
該模型將處理我們的應用程序中的數據。 它應該可以訪問 MongoClient 返回的 db 對象。 我們的模型還應該有壹個擴展它的方法,因為我們可能想要創建不同類型的模型。 例如,我們可能需要 BlogModel 或 ContactsModel。 所以我們需要編寫壹個新的規範:/tests/base.model.spec.js,以便測試這兩個模型的功能。 請記住,通過在開始編寫實現之前定義這些功能,我們可以保證我們的模塊將只執行我們希望它執行的操作。
1 |
var Model = require("../models/Base"),
|
2 |
dbMockup = {};
|
3 |
describe("Models", function() {
|
4 |
it("should create a new model", function(next) {
|
5 |
var model = new Model(dbMockup); |
6 |
expect(model.db).toBeDefined(); |
7 |
expect(model.extend).toBeDefined(); |
8 |
next(); |
9 |
}); |
10 |
it("should be extendable", function(next) {
|
11 |
var model = new Model(dbMockup); |
12 |
var OtherTypeOfModel = model.extend({
|
13 |
myCustomModelMethod: function() { }
|
14 |
}); |
15 |
var model2 = new OtherTypeOfModel(dbMockup); |
16 |
expect(model2.db).toBeDefined(); |
17 |
expect(model2.myCustomModelMethod).toBeDefined(); |
18 |
next(); |
19 |
}) |
20 |
}); |
我決定傳遞壹個模型對象,而不是壹個真正的 db 對象。 那是因為稍後,我可能想要測試壹些具體的東西,這取決於來自數據庫的信息。 手動定義這些數據會容易得多。
extend 方法的實現有點棘手,因為我們必須更改 module.exports 的原型,但仍保留原始構造函數。 值得慶幸的是,我們已經編寫了壹個很好的測試,證明我們的代碼可以工作。 通過上述內容的版本如下所示:
1 |
module.exports = function(db) {
|
2 |
this.db = db; |
3 |
}; |
4 |
module.exports.prototype = {
|
5 |
extend: function(properties) {
|
6 |
var Child = module.exports; |
7 |
Child.prototype = module.exports.prototype; |
8 |
for(var key in properties) {
|
9 |
Child.prototype[key] = properties[key]; |
10 |
} |
11 |
return Child; |
12 |
}, |
13 |
setDB: function(db) {
|
14 |
this.db = db; |
15 |
}, |
16 |
collection: function() {
|
17 |
if(this._collection) return this._collection; |
18 |
return this._collection = this.db.collection('fastdelivery-content');
|
19 |
} |
20 |
} |
這裏有兩種輔助方法。 db 對象的 setter 和數據庫集合的 getter 。
視圖
視圖將向屏幕呈現信息。 本質上,視圖是壹個向瀏覽器發送響應的類。 Express 提供了壹種簡短的方法:
1 |
res.render('index', { title: 'Express' });
|
響應對象是壹個包裝器,它有壹個很好的 API,使我們的生活更輕松。 但是,我更願意創建壹個封裝此功能的模塊。 默認視圖目錄將更改為模板,並將創建壹個新模板,它將承載基本視圖類。 這壹小改變現在需要另壹個改變。 我們應該通知 Express 我們的模板文件現在放在另壹個目錄中:
1 |
app.set('views', __dirname + '/templates');
|
首先,我將定義我需要的內容,編寫測試,然後編寫實現。 我們需要壹個符合以下規則的模塊:
- 其構造函數應該接收響應對象和模板名稱。
- 它應該有壹個接受數據對象的 render 方法。
- 它應該是可擴展的。
妳可能想知道我為什麽要擴展 View 類。 是不是只是調用 response.render 方法? 實際上,在某些情況下,妳需要發送不同的標頭或以某種方式操縱響應對象。 例如,提供 JSON 數據:
1 |
var data = {"developer": "Krasimir Tsonev"};
|
2 |
response.contentType('application/json');
|
3 |
response.send(JSON.stringify(data)); |
不要每次都這樣做,最好有壹個 HTMLView 類和壹個 JSONView 類。 甚至是用於將 XML 數據發送到瀏覽器的 XMLView 類。 如果妳建立壹個大型網站,那就更好了,包裝這些功能,而不是壹遍又壹遍地復制粘貼相同的代碼。
這是 /views/Base.js 的規範:
1 |
var View = require("../views/Base");
|
2 |
describe("Base view", function() {
|
3 |
it("create and render new view", function(next) {
|
4 |
var responseMockup = {
|
5 |
render: function(template, data) {
|
6 |
expect(data.myProperty).toBe('value');
|
7 |
expect(template).toBe('template-file');
|
8 |
next(); |
9 |
} |
10 |
} |
11 |
var v = new View(responseMockup, 'template-file'); |
12 |
v.render({myProperty: 'value'});
|
13 |
}); |
14 |
it("should be extendable", function(next) {
|
15 |
var v = new View(); |
16 |
var OtherView = v.extend({
|
17 |
render: function(data) {
|
18 |
expect(data.prop).toBe('yes');
|
19 |
next(); |
20 |
} |
21 |
}); |
22 |
var otherViewInstance = new OtherView(); |
23 |
expect(otherViewInstance.render).toBeDefined(); |
24 |
otherViewInstance.render({prop: 'yes'});
|
25 |
}); |
26 |
}); |
為了測試渲染,我不得不創建壹個模型。 在這種情況下,我創建了壹個模仿 Express 的響應對象的對象。 在測試的第二部分中,我創建了另壹個 View 類,它繼承了基類並應用了自定義 render 方法。 這是 /views/Base.js 類。
1 |
module.exports = function(response, template) {
|
2 |
this.response = response; |
3 |
this.template = template; |
4 |
}; |
5 |
module.exports.prototype = {
|
6 |
extend: function(properties) {
|
7 |
var Child = module.exports; |
8 |
Child.prototype = module.exports.prototype; |
9 |
for(var key in properties) {
|
10 |
Child.prototype[key] = properties[key]; |
11 |
} |
12 |
return Child; |
13 |
}, |
14 |
render: function(data) {
|
15 |
if(this.response && this.template) {
|
16 |
this.response.render(this.template, data); |
17 |
} |
18 |
} |
19 |
} |
現在我們在 tests 目錄中有三個規範,如果妳運行 jasmine-node ./tests,結果應該是:
1 |
Finished in 0.009 seconds
|
2 |
7 tests, 18 assertions, 0 failures, 0 skipped |
調節器
還記得路線以及它們的定義方式嗎?
1 |
app.get('/', routes.index);
|
在 “/” 後途徑,其在上面的例子中,實際上是控制器。 它只是壹個接受請求,響應和下壹個的中間件功能。
1 |
exports.index = function(req, res, next) {
|
2 |
res.render('index', { title: 'Express' });
|
3 |
}; |
以上是妳的控制器應該在 Express 的上下文中的外觀。 該快遞(1)命令行工具創建壹個新的目錄路徑,但在我們的情況下,更好地為它被命名為控制器,所以我改變,以反映這壹命名方案。
因為我們不只是構建壹個非常小的應用程序,所以如果我們創建壹個基類,我們可以擴展它將是明智的。 如果我們需要將某種功能傳遞給所有控制器,那麽這個基類將是最佳選擇。 再次,我將首先編寫測試,所以讓我們定義我們需要的東西:
- 它應該有壹個 extend 方法,它接受壹個對象並返回壹個新的子實例
- 子實例應該有壹個 run 方法,這是舊的中間件函數
- 應該有壹個 name 屬性,用於標識控制器
- 我們應該能夠基於類創建獨立的對象
所以現在只是壹些事情,但我們可能會在以後添加更多功能。 測試看起來像這樣:
1 |
var BaseController = require("../controllers/Base");
|
2 |
describe("Base controller", function() {
|
3 |
it("should have a method extend which returns a child instance", function(next) {
|
4 |
expect(BaseController.extend).toBeDefined(); |
5 |
var child = BaseController.extend({ name: "my child controller" });
|
6 |
expect(child.run).toBeDefined(); |
7 |
expect(child.name).toBe("my child controller");
|
8 |
next(); |
9 |
}); |
10 |
it("should be able to create different childs", function(next) {
|
11 |
var childA = BaseController.extend({ name: "child A", customProperty: 'value' });
|
12 |
var childB = BaseController.extend({ name: "child B" });
|
13 |
expect(childA.name).not.toBe(childB.name); |
14 |
expect(childB.customProperty).not.toBeDefined(); |
15 |
next(); |
16 |
}); |
17 |
}); |
這是 /controllers/Base.js 的實現:
1 |
var _ = require("underscore");
|
2 |
module.exports = {
|
3 |
name: "base", |
4 |
extend: function(child) {
|
5 |
return _.extend({}, this, child);
|
6 |
}, |
7 |
run: function(req, res, next) {
|
8 |
|
9 |
} |
10 |
} |
當然,每個子類都應該定義自己的 run 方法以及它自己的邏輯。
FastDelivery 網站
好的,我們為 MVC 架構提供了壹套很好的類,我們已經用測試覆蓋了新創建的模塊。 現在我們準備繼續使用我們的假公司 FastDelivery 的網站。 讓我們假設該網站有兩個部分 - 前端和管理面板。 前端將用於向最終用戶顯示數據庫中寫入的信息。 管理面板將用於管理該數據。 讓我們從我們的管理(控制)面板開始。
控制面板
讓我們首先創建壹個簡單的控制器,它將作為管理頁面。 /controllers/Admin.js 文件:
1 |
var BaseController = require("./Base"),
|
2 |
View = require("../views/Base");
|
3 |
module.exports = BaseController.extend({
|
4 |
name: "Admin", |
5 |
run: function(req, res, next) {
|
6 |
var v = new View(res, 'admin'); |
7 |
v.render({
|
8 |
title: 'Administration', |
9 |
content: 'Welcome to the control panel' |
10 |
}); |
11 |
} |
12 |
}); |
通過為控制器和視圖使用預先編寫的基類,我們可以輕松地為控制面板創建入口點 該視圖類接受壹個模板文件的名稱。 根據上面的代碼,該文件應該被稱為 admin.hjs,並且應該放在 / templates 中。 內容看起來像這樣:
1 |
<!DOCTYPE html>
|
2 |
<html>
|
3 |
<head>
|
4 |
<title>{{ title }}</title> |
5 |
<link rel='stylesheet' href='/stylesheets/style.css' /> |
6 |
</head>
|
7 |
<body>
|
8 |
<div class="container"> |
9 |
<h1>{{ content }}</h1> |
10 |
</div>
|
11 |
</body>
|
12 |
</html>
|
(為了使本教程保持相當簡短且易於閱讀的格式,我不打算展示每個視圖模板。 我強烈建議妳從 GitHub 下載源代碼。)
現在要使控制器可見,我們必須在 app.js 中添加壹個路徑:
1 |
var Admin = require('./controllers/Admin');
|
2 |
... |
3 |
var attachDB = function(req, res, next) {
|
4 |
req.db = db; |
5 |
next(); |
6 |
}; |
7 |
... |
8 |
app.all('/admin*', attachDB, function(req, res, next) {
|
9 |
Admin.run(req, res, next); |
10 |
}); |
請註意,我們不是直接將 Admin.run 方法作為中間件發送。 那是因為我們想要保持上下文。 如果我們這樣做:
1 |
app.all('/admin*', Admin.run);
|
單詞本中管理員將指向別的東西。
保護管理面板
每個以 / admin 開頭的頁面都應受到保護。 為實現這壹目標,我們將使用 Express 的中間件:Sessions。 它只是將壹個對象附加到名為 session 的請求上。 我們現在應該更改管理控制器以執行另外兩項操作:
- 它應檢查是否有可用的會話。 如果沒有,則顯示登錄表單。
- 它應接受登錄表單發送的數據,並在用戶名和密碼匹配時授權用戶。
這是壹個我們可以用來完成這個的小輔助函數:
1 |
authorize: function(req) {
|
2 |
return ( |
3 |
req.session && |
4 |
req.session.fastdelivery && |
5 |
req.session.fastdelivery === true |
6 |
) || ( |
7 |
req.body && |
8 |
req.body.username === this.username && |
9 |
req.body.password === this.password |
10 |
); |
11 |
} |
首先,我們有壹個聲明嘗試通過會話對象識別用戶。 其次,我們檢查表格是否已提交。 如果是這樣,表單中的數據在 request.body 對象中可用,該對象由 bodyParser 中間件填充。 然後我們只檢查用戶名和密碼是否匹配。
現在這裏是控制器的 run 方法,它使用我們的新助手。 我們檢查用戶是否被授權,顯示控制面板本身,否則我們顯示登錄頁面:
1 |
run: function(req, res, next) {
|
2 |
if(this.authorize(req)) {
|
3 |
req.session.fastdelivery = true; |
4 |
req.session.save(function(err) {
|
5 |
var v = new View(res, 'admin'); |
6 |
v.render({
|
7 |
title: 'Administration', |
8 |
content: 'Welcome to the control panel' |
9 |
}); |
10 |
}); |
11 |
} else {
|
12 |
var v = new View(res, 'admin-login'); |
13 |
v.render({
|
14 |
title: 'Please login' |
15 |
}); |
16 |
} |
17 |
} |
管理內容
正如我在本文開頭所指出的那樣,我們有很多東西需要管理。 為簡化過程,我們將所有數據保存在壹個集合中。 每條記錄都有標題,文字,圖片和類型屬性。 該類型屬性將決定記錄的所有者。 例如,“聯系人” 頁面只需要壹個類型為 “聯系人” 的記錄,而 “博客” 頁面則需要更多記錄。 因此,我們需要三個新頁面來添加,編輯和顯示記錄。 在我們開始創建新模板,樣式和將新內容放入控制器之前,我們應該編寫我們的模型類,它位於 MongoDB 服務器和我們的應用程序之間,當然還提供了壹個有意義的 API。
1 |
// /models/ContentModel.js |
2 |
|
3 |
var Model = require("./Base"),
|
4 |
crypto = require("crypto"),
|
5 |
model = new Model(); |
6 |
var ContentModel = model.extend({
|
7 |
insert: function(data, callback) {
|
8 |
data.ID = crypto.randomBytes(20).toString('hex');
|
9 |
this.collection().insert(data, {}, callback || function(){ });
|
10 |
}, |
11 |
update: function(data, callback) {
|
12 |
this.collection().update({ID: data.ID}, data, {}, callback || function(){ });
|
13 |
}, |
14 |
getlist: function(callback, query) {
|
15 |
this.collection().find(query || {}).toArray(callback);
|
16 |
}, |
17 |
remove: function(ID, callback) {
|
18 |
this.collection().findAndModify({ID: ID}, [], {}, {remove: true}, callback);
|
19 |
} |
20 |
}); |
21 |
module.exports = ContentModel; |
該模型負責為每條記錄生成唯壹的 ID。 我們將需要它以便稍後更新信息。
如果我們想為 “聯系人” 頁面添加新記錄,我們可以簡單地使用:
1 |
var model = new (require("../models/ContentModel"));
|
2 |
model.insert({
|
3 |
title: "Contacts", |
4 |
text: "...", |
5 |
type: "contacts" |
6 |
}); |
因此,我們有壹個很好的 API 來管理我們的 mongodb 集合中的數據。 現在我們準備編寫用於使用此功能的 UI。 對於此部分,Admin 控制器需要進行相當多的更改。 為了簡化任務,我決定將添加的記錄列表與添加 / 編輯它們的表單結合起來。 正如妳在下面的屏幕截圖中看到的那樣,頁面左側部分是為列表保留的,右側部分是為表單保留的。



將所有內容放在壹個頁面上意味著我們必須關註呈現頁面的部分,或者更具體地說明我們發送到模板的數據。 這就是為什麽我創建了幾個組合的輔助函數,如下所示:
1 |
var self = this; |
2 |
... |
3 |
var v = new View(res, 'admin'); |
4 |
self.del(req, function() {
|
5 |
self.form(req, res, function(formMarkup) {
|
6 |
self.list(function(listMarkup) {
|
7 |
v.render({
|
8 |
title: 'Administration', |
9 |
content: 'Welcome to the control panel', |
10 |
list: listMarkup, |
11 |
form: formMarkup |
12 |
}); |
13 |
}); |
14 |
}); |
15 |
}); |
它看起來有點難看,但它可以按我的意願工作。 第壹個幫助器是 del 方法,它檢查當前的 GET 參數,如果找到 action = delete&id = [記錄的 id],它將從集合中刪除數據。 第二個函數稱為表單,它主要負責顯示頁面右側的表單。 它檢查表單是否已提交並正確更新或在數據庫中創建記錄。 最後,list 方法獲取信息並準備壹個 HTML 表,稍後將其發送到模板。 這三個助手的實現可以在本教程的源代碼中找到。
在這裏,我決定向妳展示處理文件上傳的功能:
1 |
handleFileUpload: function(req) {
|
2 |
if(!req.files || !req.files.picture || !req.files.picture.name) {
|
3 |
return req.body.currentPicture || ''; |
4 |
} |
5 |
var data = fs.readFileSync(req.files.picture.path); |
6 |
var fileName = req.files.picture.name; |
7 |
var uid = crypto.randomBytes(10).toString('hex');
|
8 |
var dir = __dirname + "/../public/uploads/" + uid; |
9 |
fs.mkdirSync(dir, '0777'); |
10 |
fs.writeFileSync(dir + "/" + fileName, data); |
11 |
return '/uploads/' + uid + "/" + fileName; |
12 |
} |
如果提交了文件,請求對象的節點腳本.files 屬性將填充數據。 在我們的例子中,我們有以下 HTML 元素:
1 |
<input type="file" name="picture" /> |
這意味著我們可以通過 req.files.picture 訪問提交的數據。 在上面的代碼片段中,req.files.picture.path 用於獲取文件的原始內容。 稍後,相同的數據將寫入新創建的目錄中,最後會返回正確的 URL。 所有這些操作都是同步的,但使用 readFileSync,mkdirSync 和 writeFileSync 的異步版本是壹個好習慣。
前端
努力工作現已完成。 管理面板正在運行,我們有壹個 ContentModel 類,它允許我們訪問存儲在數據庫中的信息。 我們現在要做的是編寫前端控制器並將它們綁定到保存的內容。
這是主頁的控制器 - /controllers/Home.js
1 |
module.exports = BaseController.extend({
|
2 |
name: "Home", |
3 |
content: null, |
4 |
run: function(req, res, next) {
|
5 |
model.setDB(req.db); |
6 |
var self = this; |
7 |
this.getContent(function() {
|
8 |
var v = new View(res, 'home'); |
9 |
v.render(self.content); |
10 |
}) |
11 |
}, |
12 |
getContent: function(callback) {
|
13 |
var self = this; |
14 |
this.content = {};
|
15 |
model.getlist(function(err, records) {
|
16 |
... storing data to content object |
17 |
model.getlist(function(err, records) {
|
18 |
... storing data to content object |
19 |
callback(); |
20 |
}, { type: 'blog' });
|
21 |
}, { type: 'home' });
|
22 |
} |
23 |
}); |
主頁需要壹個具有主頁類型的記錄和四個具有博客類型的記錄。 控制器完成後,我們只需要在 app.js 中添加壹個路徑:
1 |
app.all('/', attachDB, function(req, res, next) {
|
2 |
Home.run(req, res, next); |
3 |
}); |
同樣,我們將 db 對象附加到請求。 幾乎與管理面板中使用的工作流程相同。
我們的前端(客戶端)的其他頁面幾乎完全相同,因為它們都有壹個控制器,它通過使用模型類獲取數據,當然還有定義的路徑。 有兩個有趣的情況我想更詳細地解釋壹下。 第壹個與博客頁面相關。 它應該能夠顯示所有文章,但也只能呈現壹個。 所以,我們必須註冊兩條路線:
1 |
app.all('/blog/:id', attachDB, function(req, res, next) {
|
2 |
Blog.runArticle(req, res, next); |
3 |
}); |
4 |
app.all('/blog', attachDB, function(req, res, next) {
|
5 |
Blog.run(req, res, next); |
6 |
}); |
它們都使用相同的控制器:Blog,但調用不同的運行方法。 註意 / blog /:id 字符串。 這條路線將匹配像 URL / 博客 / 4e3455635b4a6f6dccfaa1e50ee71f1cde75222b 和長哈希將可 req.params.id。 換句話說,我們能夠定義動態參數。 在我們的例子中,這是記錄的 ID。 獲得此信息後,我們就可以為每篇文章創建壹個唯壹的頁面。
第二個有趣的部分是我如何構建服務,職業和聯系人頁面。 很明顯,他們只使用數據庫中的壹條記錄。 如果我們必須為每個頁面創建壹個不同的控制器,那麽我們必須復制 / 粘貼相同的代碼,只需更改類型字段。 有壹種更好的方法來實現這壹點,只有壹個控制器,它接受其 run 方法中的類型。 以下是路線:
1 |
app.all('/services', attachDB, function(req, res, next) {
|
2 |
Page.run('services', req, res, next);
|
3 |
}); |
4 |
app.all('/careers', attachDB, function(req, res, next) {
|
5 |
Page.run('careers', req, res, next);
|
6 |
}); |
7 |
app.all('/contacts', attachDB, function(req, res, next) {
|
8 |
Page.run('contacts', req, res, next);
|
9 |
}); |
控制器看起來像這樣:
1 |
module.exports = BaseController.extend({
|
2 |
name: "Page", |
3 |
content: null, |
4 |
run: function(type, req, res, next) {
|
5 |
model.setDB(req.db); |
6 |
var self = this; |
7 |
this.getContent(type, function() {
|
8 |
var v = new View(res, 'inner'); |
9 |
v.render(self.content); |
10 |
}); |
11 |
}, |
12 |
getContent: function(type, callback) {
|
13 |
var self = this; |
14 |
this.content = {}
|
15 |
model.getlist(function(err, records) {
|
16 |
if(records.length > 0) {
|
17 |
self.content = records[0]; |
18 |
} |
19 |
callback(); |
20 |
}, { type: type });
|
21 |
} |
22 |
}); |
部署
部署基於 Express 的網站實際上與部署任何其他 Node.js 應用程序相同:
- 這些文件放在服務器上。
- 應該停止節點進程(如果它正在運行)。
- 壹個 NPM 安裝命令應以安裝新的依賴(如果有的話)來運行。
- 然後應該再次運行主腳本。
請記住,Node 仍然相當年輕,所以並非所有內容都可以按預期工作,但總是會有改進。 例如,永遠保證妳的 Nodejs 程序將連續運行。 妳可以通過發出以下命令來執行此操作:
1 |
forever start yourapp.js |
這也是我在服務器上使用的內容。 這是壹個不錯的小工具,但它解決了壹個大問題。 如果妳只使用節點 yourapp.js 運行妳的應用程序,壹旦妳的腳本意外退出,服務器就會關閉。 只需重新啟動應用程序。
現在我不是系統管理員,但我想分享我將節點應用程序與 Apache 或 Nginx 集成的經驗,因為我認為這在某種程度上是開發工作流程的壹部分。
如妳所知,Apache 通常在端口 80 上運行,這意味著如果妳打開 http:// localhost 或 http:// localhost:80,妳將看到 Apache 服務器提供的頁面,並且很可能妳的節點腳本正在偵聽不同的港口。 因此,妳需要添加壹個接受請求的虛擬主機並將它們發送到正確的端口。 例如,假設我想在 expresscompletewebsite.dev 地址下的本地 Apache 服務器上托管我們剛剛構建的站點。 我們要做的第壹件事是將我們的域添加到 hosts 文件中。
1 |
127.0.0.1 expresscompletewebsite.dev |
之後,我們必須編輯 Apache 配置目錄下的 httpd-vhosts.conf 文件並添加
1 |
# expresscompletewebsite.dev |
2 |
<VirtualHost *:80> |
3 |
ServerName expresscompletewebsite.dev |
4 |
ServerAlias www.expresscompletewebsite.dev |
5 |
ProxyRequests off |
6 |
<Proxy *> |
7 |
Order deny,allow |
8 |
Allow from all |
9 |
</Proxy> |
10 |
<Location /> |
11 |
ProxyPass https://localhost:3000/ |
12 |
ProxyPassReverse http://localhost:3000/ |
13 |
</Location> |
14 |
</VirtualHost> |
服務器仍然接受端口 80 上的請求,但是將它們轉發到節點正在偵聽的端口 3000。
Nginx 設置要容易得多,說實話,它是托管基於 Nodejs 的應用程序的更好選擇。 妳仍然需要在 hosts 文件中添加域名。 之後,只需在 Nginx 安裝下的 / sites-enabled 目錄中創建壹個新文件。 該文件的內容如下所示:
1 |
server {
|
2 |
listen 80; |
3 |
server_name expresscompletewebsite.dev |
4 |
location / {
|
5 |
proxy_pass http://127.0.0.1:3000; |
6 |
proxy_set_header Host $http_host; |
7 |
} |
8 |
} |
請記住,妳無法使用上述主機設置同時運行 Apache 和 Nginx。 這是因為它們都需要端口 80. 此外,如果妳計劃在生產環境中使用上述代碼段,妳可能需要對更好的服務器配置進行壹些額外的研究。 正如我所說,我不是這方面的專家。
結論
Express 是壹個很棒的框架,它為妳提供了壹個開始構建應用程序的良好起點。 正如妳所看到的,關於如何擴展它以及妳將使用它構建的內容,這是壹個選擇問題。 它通過使用壹些很棒的中間件簡化了枯燥的任務,並為開發人員留下了有趣的部分。
源代碼
我們構建的此示例站點的源代碼可在 GitHub 上獲得 - https://github.com/tutsplus/build-complete-website-expressjs。 可以隨意保存並修改它。 以下是運行網站的步驟。
- 下載源代碼
- 轉到 app 目錄
- 運行 npm install
- 運行 mongodb 守護程序
- 運行節點 app.js



