1. Code
  2. JavaScript
  3. Node

使用 ExpressJS 構建完整的 MVC 網站

Scroll to top

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 的虛假公司的簡單網站。 這是完整設計的屏幕截圖:

sitesitesite

在本教程結束時,我們將擁有壹個完整的 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 控制器需要進行相當多的更改。 為了簡化任務,我決定將添加的記錄列表與添加 / 編輯它們的表單結合起來。 正如妳在下面的屏幕截圖中看到的那樣,頁面左側部分是為列表保留的,右側部分是為表單保留的。

control-panelcontrol-panelcontrol-panel

將所有內容放在壹個頁面上意味著我們必須關註呈現頁面的部分,或者更具體地說明我們發送到模板的數據。 這就是為什麽我創建了幾個組合的輔助函數,如下所示:

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