序言
我在《全棧開(kāi)發(fā)之道》一書(shū)中,講述了多個(gè)MEAN 全棧的應(yīng)用實(shí)例,近期,不斷有讀者詢問(wèn),書(shū)中講述的 AngularJS 很容易理解, 那么如何創(chuàng)建基于 Angular 6 的 MEAN 全棧的增刪改查呢?
當(dāng)然,這里是有一定差別的,不過(guò),只要有了之前的AngularJS基礎(chǔ),便可平滑過(guò)渡到 Angular 6。
本章仍然通過(guò)國(guó)外經(jīng)典案例來(lái)學(xué)習(xí),原文如下:
MEAN Stack (Angular 5) CRUD Web Application Example
工程源碼下載地址: https://github.com/didinj/mean-stack-angular5-crud
代碼與實(shí)例講解
(1)創(chuàng)建一個(gè) Angular 6 工程,并運(yùn)行成功。 驗(yàn)證你的開(kāi)發(fā)環(huán)境是OK的。 具體過(guò)程不再贅述。
ng new mean-angular5
(2) Replace Web Server with Express.js
創(chuàng)建 Express 工程,NG1.x 時(shí)代, 直接通過(guò) Express generator 命令就可以創(chuàng)建,有了 NG5后,沒(méi)有自動(dòng)創(chuàng)建 MEAN 的命令了。 只有通過(guò)載入 Express 的方式完成。
在工程所在路徑下,執(zhí)行以下命令,把需要的模塊加載進(jìn)來(lái):
npm install --save express body-parser morgan body-parser serve-favicon
在工程根目錄下,創(chuàng)建bin 文件夾,并在bin下創(chuàng)建www 文件,如下
mkdir bin
touch bin/www
在bin/www 文件中,添加以下代碼:
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('mean-app:server');
var http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}
在工程根目錄下,創(chuàng)建一個(gè)新的文件 app.js
touch app.js
把以下代碼添加到 app.js 文件中:
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var bodyParser = require('body-parser');
var book = require('./routes/book');
var app = express();
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({'extended':'false'}));
app.use(express.static(path.join(__dirname, 'dist')));
app.use('/books', express.static(path.join(__dirname, 'dist')));
app.use('/book', book);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
創(chuàng)建路由文件
在根目錄下,創(chuàng)建路由文件:
mkdir routes
touch routes/book.js
在 routes/book.js 文件中,添加以下代碼:
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.send('Express RESTful API');
});
module.exports = router;
注意了, 一個(gè)完整的Express 工程已經(jīng)形成了
我們不再運(yùn)行 ng serve -o , 而是進(jìn)入 npm start 時(shí)代。這就是典型的 MEAN 工程的節(jié)奏!
npm start
運(yùn)行結(jié)果如下。與之前最大的差別是, 網(wǎng)絡(luò)請(qǐng)求的地址已經(jīng)變?yōu)椋?http://localhost:3000 端口號(hào)不再是 4200 了。

特別注意
當(dāng)運(yùn)行 ng serve -o 時(shí), 在瀏覽器地址欄輸入: htttp://localhost:4200 ,也同樣可以出現(xiàn)之前默認(rèn)的 Angular頁(yè)面。
加入express 框架后, 解決了后端路由問(wèn)題。
行文至此,有必要指出: Angular 自身帶有路由, 而 Express 也是解決路由。 既然 Angular 自身有路由,那么,為什么還要用到 Express 呢?
你可以這樣理解: Angular是前端框架,Angular 所攜帶的路由是為了解決前端的路由,所謂前端路由,就是頁(yè)面之間的跳轉(zhuǎn),通過(guò)它,解決了單頁(yè)面問(wèn)題。 前端路由并不請(qǐng)求后臺(tái)服務(wù)器,只是在頁(yè)面之間來(lái)回跳轉(zhuǎn)。
而 Express 路由則不然,它解決的是訪問(wèn)后臺(tái)服務(wù)器的路由。
如果僅僅是學(xué)習(xí)Angular,永遠(yuǎn)是停留在前端上,它無(wú)法解決全棧的問(wèn)題。
全棧 = Angular + express + node.js + MongoDB。
通過(guò)前面的代碼,我們?cè)谝?express的同時(shí),也引入了 mongoDB,借助express,對(duì)數(shù)據(jù)庫(kù)的訪問(wèn),變得如此簡(jiǎn)單!
不信,看下路由就清楚了。
在 npm start 啟動(dòng)后, 瀏覽器地址欄輸入: http://localhost:3000/book , 此時(shí)出現(xiàn):

配置 mongoose
npm install --save mongoose bluebird
在 app.js 文件中添加以下代碼:
var mongoose = require('mongoose');
mongoose.Promise = require('bluebird');
mongoose.connect('mongodb://localhost/mean-angular5', { useMongoClient: true, promiseLibrary: require('bluebird') })
.then(() => console.log('connection successful'))
.catch((err) => console.error(err));
單獨(dú)開(kāi)啟一個(gè)終端窗口, 開(kāi)啟數(shù)據(jù)庫(kù):
sudo mongod
此時(shí),在另一個(gè)窗口再次運(yùn)行 npm start ,這時(shí),會(huì)出現(xiàn)
connection successful
說(shuō)明:
如果你使用內(nèi)置的mongoose ,會(huì)出現(xiàn)以下信息:
(node:42758) DeprecationWarning: Mongoose: mpromise (mongoose's default promise library) is deprecated, plug in your own promise library instead: http://mongoosejs.com/docs/promises.html
這就是為什么添加 bluebird ,并將它注冊(cè)為 mongoose promise library 的原因。
Create Mongoose.js Model
在工程根目錄下,
mkdir models
創(chuàng)建一個(gè) collection, 命名為 Book
touch models/Book.js
在 Book.js 文件中,添加以下代碼:
var mongoose = require('mongoose');
var BookSchema = new mongoose.Schema({
isbn: String,
title: String,
author: String,
description: String,
published_year: String,
publisher: String,
updated_date: { type: Date, default: Date.now },
});
module.exports = mongoose.model('Book', BookSchema);
注意: model/book.js 文件用來(lái)創(chuàng)建 mongodb 的collection。
而 routes/book.js 文件用來(lái)管理路由, 接下來(lái),開(kāi)始后臺(tái)訪問(wèn)的路由配置。
在 routes/book.js ,添加代碼如下:
var express = require('express');
var router = express.Router();
var mongoose = require('mongoose');
var Book = require('../models/Book.js');
/* GET ALL BOOKS */
router.get('/', function(req, res, next) {
Book.find(function (err, products) {
if (err) return next(err);
res.json(products);
});
});
/* GET SINGLE BOOK BY ID */
router.get('/:id', function(req, res, next) {
Book.findById(req.params.id, function (err, post) {
if (err) return next(err);
res.json(post);
});
});
/* SAVE BOOK */
router.post('/', function(req, res, next) {
Book.create(req.body, function (err, post) {
if (err) return next(err);
res.json(post);
});
});
/* UPDATE BOOK */
router.put('/:id', function(req, res, next) {
Book.findByIdAndUpdate(req.params.id, req.body, function (err, post) {
if (err) return next(err);
res.json(post);
});
});
/* DELETE BOOK */
router.delete('/:id', function(req, res, next) {
Book.findByIdAndRemove(req.params.id, req.body, function (err, post) {
if (err) return next(err);
res.json(post);
});
});
module.exports = router;
再來(lái)看下效果:
npm start
此時(shí),在瀏覽器輸入:
http://localhost:3000/book 時(shí), 后臺(tái)返回的數(shù)據(jù)是一個(gè)空數(shù)組: [ ] , 這說(shuō)明,工作正常。畢竟還沒(méi)有在數(shù)據(jù)庫(kù)添加內(nèi)容。

我們完全可以在終端窗口測(cè)試后臺(tái)的響應(yīng),而不用切換到瀏覽器上。
具體來(lái)說(shuō),另起一個(gè)終端窗口
curl -i -H "Accept: application/json" localhost:3000/book
我們?cè)跍y(cè)試 CRUD 的操作, 如果后臺(tái)返回以下響應(yīng)數(shù)據(jù),說(shuō)明REST API 工作正常。
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 2
ETag: W/"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w"
Date: Fri, 10 Nov 2017 23:53:52 GMT
Connection: keep-alive
說(shuō)明,配置數(shù)據(jù)庫(kù)的方式有兩種:
(1)圖形化操作數(shù)據(jù)庫(kù)的工具: 比如: Robomongo
(2) 終端指令方式
這里以終端指令方式為例:
curl -i -X POST -H "Content-Type: application/json" -d '{ "isbn":"123442123, 97885654453443","title":"Learn how to build modern web application with MEAN stack","author": "Didin J.","description":"The comprehensive step by step tutorial on how to build MEAN (MongoDB, Express.js, Angular 5 and Node.js) stack web application from scratch","published_year":"2017","publisher":"Djamware.com" }' localhost:3000/book
正常情況下,后臺(tái)返回以下數(shù)據(jù):
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 415
ETag: W/"19f-sb+GoLr+sWYpk964su4Cw9hiKhc"
Date: Sun, 15 Apr 2018 10:39:32 GMT
Connection: keep-alive{"_id":"5ad32be4e2d8f11563dd70ad","isbn":"123442123, 97885654453443","title":"Learn how to build modern web application with MEAN stack","author":"Didin J.","description":"The comprehensive step by step tutorial on how to build MEAN (MongoDB, Express.js, Angular 5 and Node.js) stack web application from scratch","published_year":"2017","publisher":"Djamware.com","updated_date":"2018-04-15T10:39:32.822Z","__v":0}
Create Angular 5 Component for Displaying Book List
Create Angular 5 Routes to Book Component
運(yùn)行結(jié)果:

Create Angular 5 Component for Displaying Book Detail
Create Angular 5 Component for Add New Book
運(yùn)行結(jié)果:

add

update 、 delete、 edit 都好用, 如圖

特別注意
NG6 實(shí)現(xiàn)了 前端與后臺(tái)的分離,前端(Angular) 本身是一個(gè)應(yīng)用服務(wù), 而后臺(tái)(node.js) 也是一個(gè)服務(wù)。 所以,在啟動(dòng)時(shí),應(yīng)該啟動(dòng)三個(gè)服務(wù):
- sudo mongod (啟動(dòng)數(shù)據(jù)庫(kù)服務(wù)器)
- npm start (啟動(dòng) Angular 應(yīng)用)
運(yùn)行時(shí),必須用 3000 端口,這是 app.js 確定的端口。 此時(shí), Angular 默認(rèn)的端口 4200 已經(jīng)不再起作用了。
小結(jié)
這個(gè)案例,很好地詮釋了“路由”的概念:前端路由和后臺(tái)路由。 單獨(dú)起一篇來(lái)寫(xiě)吧
運(yùn)行工程遇到的問(wèn)題
從 github 上下載一個(gè) angular 工程,該怎么運(yùn)行它呢?
前提: 先啟動(dòng) mongoDB 數(shù)據(jù)庫(kù)
sudo mongod
運(yùn)行應(yīng)用程序,如下:
第一步:
npm install
第二步:
npm start
這時(shí)候,出現(xiàn)報(bào)錯(cuò)很正常, 一個(gè)個(gè)解決唄。
Cannot find module '@angular-devkit/core'
module 找不見(jiàn),怎么辦? 安裝唄。 那么,為什么會(huì)出現(xiàn)這種情況呢? 原因是: package.json 工程配置文件中沒(méi)有這個(gè)文件,而編譯時(shí),需要這個(gè)文件。
npm install @angular-devkit/core --save-dev
這時(shí)候,再執(zhí)行 npm start ,就可以了。
如果還是報(bào)錯(cuò),就得更新 @angular/cli 版本了。如下:
Step1: Edit your package.json changing the line
@angular/cli": "1.6.4"
to
@angular/cli": "^1.6.4"
Step2:
npm update -g @angular/cli
Step3:
npm install --save-dev @angular/cli@latest
Angular APP 的運(yùn)行
編譯成功后, 在瀏覽器地址欄輸入:
運(yùn)行結(jié)果如下:
