序言
我在《全栈开发之道》一书中,讲述了多个MEAN 全栈的应用实例,近期,不断有读者询问,书中讲述的 AngularJS 很容易理解, 那么如何创建基于 Angular 6 的 MEAN 全栈的增删改查呢?
当然,这里是有一定差别的,不过,只要有了之前的AngularJS基础,便可平滑过渡到 Angular 6。
本章仍然通过国外经典案例来学习,原文如下:
MEAN Stack (Angular 5) CRUD Web Application Example
工程源码下载地址: https://github.com/didinj/mean-stack-angular5-crud
代码与实例讲解
(1)创建一个 Angular 6 工程,并运行成功。 验证你的开发环境是OK的。 具体过程不再赘述。
ng new mean-angular5
(2) Replace Web Server with Express.js
创建 Express 工程,NG1.x 时代, 直接通过 Express generator 命令就可以创建,有了 NG5后,没有自动创建 MEAN 的命令了。 只有通过载入 Express 的方式完成。
在工程所在路径下,执行以下命令,把需要的模块加载进来:
npm install --save express body-parser morgan body-parser serve-favicon
在工程根目录下,创建bin 文件夹,并在bin下创建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);
}
在工程根目录下,创建一个新的文件 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;
创建路由文件
在根目录下,创建路由文件:
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;
注意了, 一个完整的Express 工程已经形成了
我们不再运行 ng serve -o , 而是进入 npm start 时代。这就是典型的 MEAN 工程的节奏!
npm start
运行结果如下。与之前最大的差别是, 网络请求的地址已经变为: http://localhost:3000 端口号不再是 4200 了。
特别注意
当运行 ng serve -o 时, 在浏览器地址栏输入: htttp://localhost:4200 ,也同样可以出现之前默认的 Angular页面。
加入express 框架后, 解决了后端路由问题。
行文至此,有必要指出: Angular 自身带有路由, 而 Express 也是解决路由。 既然 Angular 自身有路由,那么,为什么还要用到 Express 呢?
你可以这样理解: Angular是前端框架,Angular 所携带的路由是为了解决前端的路由,所谓前端路由,就是页面之间的跳转,通过它,解决了单页面问题。 前端路由并不请求后台服务器,只是在页面之间来回跳转。
而 Express 路由则不然,它解决的是访问后台服务器的路由。
如果仅仅是学习Angular,永远是停留在前端上,它无法解决全栈的问题。
全栈 = Angular + express + node.js + MongoDB。
通过前面的代码,我们在引入 express的同时,也引入了 mongoDB,借助express,对数据库的访问,变得如此简单!
不信,看下路由就清楚了。
在 npm start 启动后, 浏览器地址栏输入: http://localhost:3000/book , 此时出现:
配置 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));
单独开启一个终端窗口, 开启数据库:
sudo mongod
此时,在另一个窗口再次运行 npm start ,这时,会出现
connection successful
说明:
如果你使用内置的mongoose ,会出现以下信息:
(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 ,并将它注册为 mongoose promise library 的原因。
Create Mongoose.js Model
在工程根目录下,
mkdir models
创建一个 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 文件用来创建 mongodb 的collection。
而 routes/book.js 文件用来管理路由, 接下来,开始后台访问的路由配置。
在 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;
再来看下效果:
npm start
此时,在浏览器输入:
http://localhost:3000/book 时, 后台返回的数据是一个空数组: [ ] , 这说明,工作正常。毕竟还没有在数据库添加内容。
我们完全可以在终端窗口测试后台的响应,而不用切换到浏览器上。
具体来说,另起一个终端窗口
curl -i -H "Accept: application/json" localhost:3000/book
我们在测试 CRUD 的操作, 如果后台返回以下响应数据,说明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
说明,配置数据库的方式有两种:
(1)图形化操作数据库的工具: 比如: 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
正常情况下,后台返回以下数据:
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
运行结果:
Create Angular 5 Component for Displaying Book Detail
Create Angular 5 Component for Add New Book
运行结果:
add
update 、 delete、 edit 都好用, 如图
特别注意
NG6 实现了 前端与后台的分离,前端(Angular) 本身是一个应用服务, 而后台(node.js) 也是一个服务。 所以,在启动时,应该启动三个服务:
- sudo mongod (启动数据库服务器)
- npm start (启动 Angular 应用)
运行时,必须用 3000 端口,这是 app.js 确定的端口。 此时, Angular 默认的端口 4200 已经不再起作用了。
小结
这个案例,很好地诠释了“路由”的概念:前端路由和后台路由。 单独起一篇来写吧
运行工程遇到的问题
从 github 上下载一个 angular 工程,该怎么运行它呢?
前提: 先启动 mongoDB 数据库
sudo mongod
运行应用程序,如下:
第一步:
npm install
第二步:
npm start
这时候,出现报错很正常, 一个个解决呗。
Cannot find module '@angular-devkit/core'
module 找不见,怎么办? 安装呗。 那么,为什么会出现这种情况呢? 原因是: package.json 工程配置文件中没有这个文件,而编译时,需要这个文件。
npm install @angular-devkit/core --save-dev
这时候,再执行 npm start ,就可以了。
如果还是报错,就得更新 @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 的运行
编译成功后, 在浏览器地址栏输入:
http://localhost:3000
运行结果如下: