MEAN stack的点点滴滴

一个简单的关于MEAN.js的分享(入门教程)


总览

写在前面:
Angular自从出2.x版本后1.x就统称为Angularjs,2以及后面的版本称为Angular
另外Angularjs将在未来停止更新,本文基于Angularjs,所以前端部分的写法不用深究,其他内容换成Angular后也可以使用。

MEAN.js简介

MEAN是一个以JavaScript语言主导的web开发技术栈。是MongoDB、Express、AngularJS、Nodejs的首字母缩写的组合。前端部分为Google提供的开源框架AngularJS(2.0开始做了不兼容升级并改名为Angular,这里以AngularJS为例,因为我们实验室主导项目基于AngularJS的,还没完成升级。)后端部分用Nodejs自带的http模块开启一个服务器,并用Express框架提供的中间件设计完成后端逻辑。其中数据持久化使用了一种noSQL数据库MongoDB。

AngularJS是一个前端MVC框架,主要用于做单页应用(SPA,Single Page Application)。单页应用,顾名思义就是只有一个页面,里面所有的内容都是通过js动态请求数据并动态修改HTML的DOM实现的。

Nodejs是JavaScript的一个运行环境,由Google的V8引擎提供了js解析,让js可以脱离浏览器运行,并提供了一些系统api比如文件读写(fs模块),内置了一个http模块提供服务器支持(C++编写,效率保证)我们要使用它需要安装,网上可以找到安装程序,装完后可以通过node -v查看版本

Express是后端框架,作为Nodejs http模块的server中间件处理分发请求。

MongoDB是文档型数据库,内部数据为json结构,不用再进行对象的二次转换,使用很方便,效率也蛮高,比较适合数据变化较大,数据结构较复杂的网页。

以上只是极简单的介绍,详情可以自行参考各框架的官网~

以一个小项目为例

接下来以一个时间表小项目为例,记录下从0开始的编码过程。这个项目主要用于记录一周7天x上午下午晚上3个时间段的空闲时间(实验室要求每个人规划自己的空闲时间提交来实验室的时间)。核心就两个模块,时间表模块和用户模块。时间表就增改查,用户就登录注册。出于演示所有功能逻辑都只是一个雏形...
然后说一下我的环境,为了适应生产环境,我本地环境是node6.10.0,npm3.10.10至于npm是啥下文会讲。


后端

Nodejs建立服务器

首先提一下模块系统,我们自己开发的网页会用到很多别人写的库或者框架,要是全部以源码方式交流(git等方式团队合作)代码文件就很大,而且满大街找别人写好的东西也很累是不(笑),所以就有了模块系统也就是npm这个东西。在安装完nodejs后npm也一起装完了,npm就是一个nodejs的模块管理工具,官方一点就是包管理器。你需要下载一个什么东西直接npm install xxx就好了,是不是超级方便,执行完后当前目录下(当前目录就是你命令行执行那个命令的目录,“>”左边那个东西,windows下按win+r输入cmd就打开了命令行,默认在用户目录“C:\Users\你的用户名”)就会有一个node_modules文件夹,下载的所有库都会在那里面。那么要是团队合作怎么知道我装了什么库呢?一般人都能想到,写一个文件记录装的库名字和版本号不就行啦,这就是package.json文件。当然package.json文件记录了更多的信息,比如项目名字,作者,开源协议等稍后再细讲。

好了进入正题。win+r输入cmd进入命令行(一个黑框框),输入cd Desktop回车,工作路径切换到桌面(下文“执行xxx”就表示命令行输入命令后回车)执行mkdir timetable,桌面会新建一个timetable文件夹。执行cd timetablenpm init,然后会问我们好多问题,暂时全部保持默认一路回车(之后再改)。最后出现Is this ok? (yes)回车之后timetable文件夹就出现了package.json文件。我的文件长这样

1
2
3
4
5
6
7
8
9
10
11
{
"name": "timetable",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

当然刚刚那一堆命令也不用那么烦,你完全可以把上面这段代码复制,本地保存为package.json然后新建个timetable文件夹吧package.json扔里面,效果完全一样。

package.json是nodejs项目的模块配置文件,执行npm相关命令的时候都会读取或者写入这个文件,上文提到的把依赖的库名称和版本号记录下来也是在这个文件里面。
在timetable文件夹下新建www.js,编写如下代码

1
2
3
4
5
6
const http = require('http');
const server = http.createServer(function (req, res) {
res.write('hello nodejs');
res.end();
});
server.listen(8080);

回到命令行(希望你没有关,关了也没事,打开timetable文件夹,在不选中任何文件的情况下按住shift后按鼠标右键会发现右键菜单多了“在此处打开命令窗口”的选项,也可能是“在此处打开powershell窗口”…win10不太一样,点一下就打开在timetable目录的命令行了,当然你要通过cd切换我也没意见)执行node www,然后打开浏览器输入http://localhost:8080就可以看到输出了。这样就用nodejs很简单的建立了一个本地服务器,监听8080端口。

后端基本结构

这样虽然开启了了服务器,并且也可以成功收到请求了,但是若是所有的请求都在一个函数里面执行逻辑,比如判断请求参数调用不同函数,那将是一个灾难(虽然用一个map似乎也不是不可以,但是我们有更好的方案)。然后执行npm i --save express引入express框架(如果你的命令行正在执行node你可以选择ctrl+c终止或者另外再开一个命令行)。这里i就是install的缩写,--save就表示吧这个库连同版本号一起写入package.json文件中。下载完依赖后打开package.json,会发现多了

1
2
3
"dependencies": {
"express": "^4.16.2"
}

这就记录了这个项目依赖express这个框架(库),版本号为4.16.2。这样会安装最新版本,如果要装确定的版本,就在库名后面加上@版本号就行了比如npm i --save express@4.0.0(这个版本号瞎编的,实际中需要去官网看需要哪个版本),别人拿到项目后也不用一个个npm i xxx了可以直接运行npm i,npm会读取package.json文件里含有dependencies字段的内容并全部下载(除了dependencies,常用的还有devDependencies等,区别后面会讲)

好了我们继续,打开并修改www.js文件(加了相关注释)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 引入http模块
const http = require('http');
// 引入express
const express = require('express');
// 实例化express
const app = express();
// 用express的实例app创建一个服务
const server = http.createServer(app);
// 监听8080端口
server.listen(8080);
// 监听对根目录的get请求
app.get('/', function (req, res, next) {
res.send('hello express');
});

重启下服务器(ctrl+c终止后按↑就可以看到历史命令,找到node www后回车)每次对代码进行修改都要重启下服务器让内的代码进入内存(node会吧代码放到缓存,从缓存读取)然后刷新下浏览器,应该可以看到效果了,为了区分我把hello nodejs改成输出hello express。对比下两个www.js文件,其实就是`createServer`函数的参数用express的实例app代替了,而后面加了个app.get函数用于处理对“/”的get请求。至于get请求啥意思建议补一下http协议相关内容(找几篇博客看一下就好)

有了这个就方便多了,我们可以简单的往下加app.get或者post来处理不同的请求。www.js文件最后面加上

1
2
3
4
5
6
app.get('/user', function (req, res, next) {
res.send('user');
});
app.get('/timetable', function (req, res, next) {
res.send('timetable');
});

重启node服务后访问localhost:8080/userlocalhost:8080/timetable就可以看到效果了。然后是不是觉得重启每次改完代码都要重启下服务很烦?那就需要一些工具了。命令行执行npm i --save-dev supervisor。这回出现了个新东西--save-dev这个和上面出现过的--save是差不多的,只不过这样就把依赖写入package.json的devDependencies字段里面,表示这个依赖开发的时候才用得到,生产环境不需要。这样生产环境执行npm install --production就不会下载安装devDependencies下的东西。此时package.json文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"name": "timetable",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.16.2"
},
"devDependencies": {
"supervisor": "^0.12.0"
}
}

这时候就可以使用supervisor www代替node www了,supervisor会自动监听文件,有改动就自动重启。然后我们发现supervisor这个单词好长,教练,有没有简单的命令?当然有。package.json有一个scripts字段,目前里面只有"test": "echo \"Error: no test specified\" && exit 1",我们加上"start": "supervisor www",,现在package.json的scripts变成了这样

1
2
3
4
"scripts": {
"start": "supervisor www",
"test": "echo \"Error: no test specified\" && exit 1"
},

然后就可以用npm run start代替supervisor www命令了,然后npm对start做了简化可以直接写npm start(具体关于这部分内容可以看这里),好了我们执行npm start

继续。随着项目变大,代码全部写在www文件会导致文件越来越大。是时候做一波代码分离了。首先吧app和www分离,后续代码基本不会修改www里面的内容了,就是app内容的修改(多写几个app.get等…)好,首先新建个app.js并把www.js里面关于app的代码分离出去,然后新建个bin文件夹,表示可执行目录,并把www.js文件拖进去,之后所有可执行文件都会放在bin目录(有些项目会有定时任务啥的要单独执行)...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// app.js
// 引入express
const express = require('express');
// 实例化express
const app = express();

app.get('/', function (req, res, next) {
res.send('hello express');
});
app.get('/user', function (req, res, next) {
res.send('user');
});
app.get('/timetable', function (req, res, next) {
res.send('timetable');
});

module.exports = app;
1
2
3
4
5
6
7
8
9
10
// bin/www.js
// 引入http模块
const http = require('http');
// 引入app模块
const app = require('../app');

// 用express的实例app创建一个服务
const server = http.createServer(app);
// 监听8080端口
server.listen(8080);

解释一下,这里把app相关的代码分离到了app.js,www.js的app就引入(因为app在父目录所以是`../app.js`)就好了。然后因为要提供给www文件引入所以app文件最后一行`module.exports = app;`就表示把app模块导出(这里是CommonJS规范的模块语法)。
然后我们对bin/www.js文件做最后的改动,把端口号定义成常量然后server绑定了listening和error事件(用于报告错误,具体事件代码我是直接复制IDEA生成的express项目代码的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// bin/www.js
// 引入http模块
const http = require('http');
// 引入app模块
const app = require('../app');
// 定义端口常量
const PORT = 8080;
// 用express的实例app创建一个服务
const server = http.createServer(app);
// 监听8080端口
server.listen(PORT);
// 绑定成功监听端口事件
server.on('listening', () => {
console.log('connect to port ' + PORT);
});
// 绑定错误事件
server.on('error', (error) => {
if (error.syscall !== 'listen') {
throw error;
}

const bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;

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;
}
});

然后我们可以不用管www文件了。把重点放到app文件,首先就是那几个app.get。之前演示已经知道了这个是绑定路由,根据前端请求的路径执行不同的函数实现后端数据处理逻辑。那么当项目越来越大以后,这样子的函数就会越来越多,都放到app.js显然不是很合适,是时候做一波路由分离了(这个名词我瞎编的0.0)

首先新建一个routes文件夹里面再新建index.jsuser.jstimetable.js。然后把app.get相关代码转移过去。那么问题来了,怎么转移?之前把app从www转移是因为app完全是分离的代码,www里面的app可以导入app并且没有别的操作了。然而我们这里要把app.get转移出去的话index.js文件里面的app的确也可以导入,但是在routes/index.js文件写好的代码怎么导入到app.js文件呢?直接require么?那不是循环依赖了(routes/index.js require app,app.js又require routes/index.js)一个可行的方案是routes/index.js导出一个可执行函数并接受app作为参数,在app.js里面直接require并执行。示例代码

1
2
3
4
// app.js
app.get('/', function (req, res, next) {
res.send('hello express');
});

改成这样

1
2
// app.js
require('./routes/index')(app);
1
2
3
4
5
6
// routes/index.js
module.exports = function (app) {
app.get('/', function (req, res, next) {
res.send('hello express');
});
};

这么做的缺点这是把app整个根模块暴露给了项目的子模块,就有可能造成app模块的滥用导致影响别的代码,或者说,增强了模块的耦合。
不过在express4.x开始引入了一个Router,于是分离路由有了新方式:

1
2
3
4
// app.js
app.get('/', function (req, res, next) {
res.send('hello express');
});

改成这样

1
2
3
// app.js
const index = require('./routes/index');
app.use('/', index);
1
2
3
4
5
6
7
8
9
// routes/index.js
const express = require('express');
const router = express.Router();

router.get('/', function (req, res) {
res.send('hello express');
});

module.exports = router;

这两种方式没什么本质的区别,app本质上也只是一个router而已,这里用了express.Router挂载路由,后面的get啊post啊都加到router上面,独立了一个路由的实例,具体有什么好处可以查看这篇文章浅谈 Express 4.0 Router 模块。这里有个地方需要注意的,现在无论app.get还是app.use还是router.get第一个参数都是“/”,看不出啥问题,但是要是换成“/user”就有区别了。下文细讲
完整的分离后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// app.js
// 引入express
const express = require('express');
// 实例化express
const app = express();
// 引入别的路由模块
const index = require('./routes/index');
const user = require('./routes/user');
const timetable = require('./routes/timetable');

app.use('/', index);
app.use('/user', user);
app.use('/timetable', timetable);

module.exports = app;
1
2
3
4
5
6
7
8
9
// routes/index.js
const express = require('express');
const router = express.Router();

router.get('/', function (req, res, next) {
res.send('hello express');
});

module.exports = router;
1
2
3
4
5
6
7
8
9
// routes/timetable.js
const express = require('express');
const router = express.Router();

router.get('/', function (req, res, next) {
res.send('timetable');
});

module.exports = router;
1
2
3
4
5
6
7
8
9
// routes/user.js
const express = require('express');
const router = express.Router();

router.get('/', function (req, res, next) {
res.send('user');
});

module.exports = router;

首先会发现index.jstimetable.jsuser.js三个文件内部的get第一个参数都是“/”而不是“/user”啥的。看app.js用了app.use。就是使用中间件,具体这个啥意思目前可以理解为对第一个参数的路由的请求都由第二个参数的模块处理,比如app.use('/user',user)表示对/user路径的请求都会由user模块处理,那么/user的请求就是user模块的/对应的函数处理。
在routes/user.js加一个路由:

1
2
3
4
5
6
7
8
9
10
11
12
// routes/user.js
const express = require('express');
const router = express.Router();

router.get('/', function (req, res, next) {
res.send('user');
});
router.get('/user', function (req, res, next) {
res.send('user2');
});

module.exports = router;

它本身在app.js就挂在到/user下了,内部再router.get(‘/user’),所以对应到路由就是访问localhost:8080/user/user才能看到user2的输出。另外可以看到后面的function有三个参数,req,res,next。分别对应请求体,相应体和下一个函数。这里先解释下next这个参数
修改routes/user.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// routes/user.js
const express = require('express');
const router = express.Router();

router.get('/', function (req, res, next) {
req.user = 'aaa';
res.user = 'bbb';
next();
});
router.get('/', function (req, res, next) {
res.send('user name ' + req.user + res.user);
});

module.exports = router;

我们写了两个router.get(‘/‘)的函数,路由会先找到第一个执行后再找到第二个继续执行,所以访问localhost:8080/user就会显示user name aaabbb
这里第一个/就可以理解为一种预处理。可以试试吧第一个/的函数的next()删了看看会发生什么,或者吧next()改成res.send('user')看看页面显示啥。

前后端分离

前面写了那么多所谓的路由都是发送一串字符串,那我需要发送一个页面什么的怎么办呢?首先我们可以先尝试下
修改index.js代码

1
2
3
4
5
6
7
8
9
// routes/index.js
const express = require('express');
const router = express.Router();

router.get('/', function (req, res, next) {
res.send('<h1 style="color: red">hello express</h1>');
});

module.exports = router;

访问http://localhost:8080看看页面输出了什么?是不是h1标签的大小以及样式都应用上了?其实我们发送的不是一个严格的html文件,但是浏览器当做html解析了。那我们要发送好看的首页带css样式再加点js效果那不是字符串拼接要累死人了?别慌express帮我们搞定了,而且只需要一点点代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// app.js
// 引入path
const path = require('path');
// 引入express
const express = require('express');
// 实例化express
const app = express();
// 引入别的路由模块
const index = require('./routes/index');
const user = require('./routes/user');
const timetable = require('./routes/timetable');

// 设置静态资源路径,express自动处理get请求
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', index);
app.use('/user', user);
app.use('/timetable', timetable);

module.exports = app;

就加了两行const path = require('path');app.use(express.static(path.join(__dirname, 'public')));。然后根目录下新建public文件夹,里面再新建一个index.html,写入点简单的代码
public/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>timetable</title>
<style>
h1 {
color: red;
}
</style>
</head>
<body>
<h1>Time Table</h1>
<script>
const h1 = document.querySelector('h1');
console.log(h1);
h1.innerHTML += '123';
</script>
</body>
</html>

打开浏览器http://localhost:8080就可以看到效果啦。好了,解释下上面加的那些代码的意义,html文件就不解释了。。。主要是app.js里面加的const path = require('path');app.use(express.static(path.join(__dirname, 'public')));第一句就是引入path依赖,它是nodejs自带的一个模块,会自动处理文件路径拼接的符号问题。比如用字符串拼接’a/‘+’/b就是’a//b’这个路径就会出错,但是path.join(‘a/‘, ‘/b’)结果就是’a\b’。(可以执行下routes/test.js看看输出结果)。这里顺便解释了后面那一句path.join(__dirname, 'public')的意思,就是把__dirnamepublic两个路径拼起来,至于__dirname就是项目路径名,nodejs全局定义的一个变量。然后继续往外解释express.static(path.join(__dirname, 'public'))。外面的express.static就是设置静态文件路径,最后用app.use引入到项目,这句话整个的意思就是把public目录下的文件都设置成静态文件,所有的请求都先去public下找一遍,并且吧对/目录的请求绑定为public/index.html。所以加了这一句,nodejs对于网页发出的所有请求都会先去public目录找一遍,找到就返回那个静态文件,没找到就进入下一步逻辑。

举个例子,在public目录下新建一个123.txt,里面随便打点东西比如我打了“123”。打开浏览器,输入localhost:8080/123.txt我们发出了一个对123.txt的请求,可以看到浏览器显示了这个文件的内容。所以,对于浏览器的请求,node就会在app文件里面绑定的路由(app.use或者app.get app.post)一个个找下来,找到了就执行相关逻辑(或者直接返回静态文件)没找到就出错啦。所以我们在app.js最后加上两个错误处理的函数(放在app.js的最后一行的上面,虽然放最后也无所谓,但是这样美观一点0v0)

1
2
3
app.use(function (req, res, next) {
res.send('404 NOT FOUND!')
});

这个就是处理所有的路由都没有找到的情况,这里使用了use没有第一个参数,直接就是一个function,表示所有的请求都会经过它。访问一个不存在的路由就可以看到效果,比如localhost:8080/aaa就可以看到页面显示“404 NOT FOUND!”,因为前面所有路由都没有匹配的话就会走到最后一个app.use(就是之前刚加的那个。)
这个和app.use(express.static(path.join(__dirname, 'public')));是一样的,所有的请求都会经过那个中间件,而express.static()最后返回的也是一个类似于function (req, res, next)这样的函数。所以我们可以了解到,express的前后端分离核心就是这一句,把public目录设置成静态文件目录导致浏览器所有请求会优先去public文件夹下寻找对应的文件,找到就返回。那么public目录就是前端的目录,前端使用html文件编写,可以直接返回并由浏览器渲染。


前端

前端基本结构

前面已经讲了public目录就是静态文件目录也就可以理解为前端目录,并且已经新建了两个文件作为例子(index.html123.txt)。现在我们把文件结构建完整。在public文件夹下面新建文件夹css,js,img,lib,font,(这里我为了让GitHub不忽略他所以某些文件夹加了个.gitkeep文件,因为GitHub会忽略空文件夹,虽然理论上里面放个什么文件都可以,但是大家还是约定成.gitkeep文件。)然后随便写点东西,先把一开始建立的index.html文件的css部分和js部分分离。在css文件夹下新建index.css,js文件夹下新建index.js。

1
2
3
4
/* public/css/index.css */
h1 {
color: red;
}
1
2
3
4
// public/js/index.js
const h1 = document.querySelector('h1');
console.log(h1);
h1.innerHTML += '123';

分离css和js后的html文件public/index.html

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>timetable</title>
<link rel="stylesheet" href="css/index.css">
</head>
<body>
<h1>Time Table</h1>
<script src="js/index.js"></script>
</body>
</html>

这里我们可以看到<link rel="stylesheet" href="css/index.css">这个link标签的href直接引入了相对路径下的css。因为这个请求发送实质是一个对 localhost:8080/css/index.css的GET请求,所以服务端直接处理并返回了静态文件。由此也可以进一步确定public目录下的所有文件就是一个前端的工程项目。后端会自动发送相应请求文件。

AngularJS:MVC

前端结构建好了就进入开发了,前端框架选择的是AngularJS,先引入文件(因为后续要使用webpack所以这里我们采用网络上的文件引入,所以确保自己联网。)
这里我们找了个AngularJS1.6.6版本的CDN引入。(CDN这里可以简单理解为别人存在网络上的文件我们可以直接用)。修改index.html文件(注:以下操作全部在public文件夹下,所以提到文件名都不说具体目录了,全部都在public或者它的子目录下的文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>timetable</title>
<link rel="stylesheet" href="css/index.css">
</head>
<body ng-app="app">
<h1>Time Table</h1>
<div>
<input type="text" placeholder="双向绑定测试" ng-model="name">{{name}}
</div>
<script src="https://cdn.bootcss.com/angular.js/1.6.6/angular.js"></script>
<script src="js/index.js"></script>
</body>
</html>

修改index.js文件

1
angular.module('app', [])

这里不会讲太多AngularJS的概念(可以参考官网或者哪天我有空了再写相关文章…)只会说一些我们实验室的开发流程。

首先AngularJS就是一个库,和JQuery一样的方式引入就好了,这里是script标签的src引入了一个网上的库。然后我们在body标签里面加了一个ng-app="app",然后js的代码angular.module('app', [])就申明了一个模块,angular.module这个函数接受两个参数,第一个是模块名,第二个是依赖名,第二个参数啥意思后面会介绍,现在因为我们没有别的依赖所以就让第二个参数为空数组。第一个参数的模块名就是对应页面上的ng-app的值,告诉AngularJS这个是一个模块名字叫做app(ng-app的等号后面的值),所以页面上的ng-app后面的值要和js里面的angular.module的第一个参数一样,不然会出错。这里就叫app了。因为angular主要是单页应用的框架,所以只会有一个主html文件(就是包含head body等标签的html),别的代码都是以html片段的形式存在,在angular需要时会动态加载进来替换掉页面上的部分DOM结构。

解释完了我们写的代码,可以打开浏览器看看效果。我们发现在input框里输入的内容都会在后面出现。这里就是双向绑定的概念了,回到代码,发现我们在input框加入了ng-model="name",然后后面又加了{{name}}ng-model的意思是把输入框的值和后面那个变量进行绑定{{}}的意思是把变量进行输出。这样input的内容有修改的话就可以实时在内存中变化了。同理要是内存中的变量变化了也会在input中显示(比如js代码修改了变量值)说说不解释概念还是废话了一堆..

刚刚提到内存中的变量,可能对这个概念有点懵逼,现在我们加上controller,就会对这个概念有点了解了。
修改index.html的一部分代码(就是div 标签上加了一个ng-controller="appCtrl"

1
2
3
<div ng-controller="appCtrl">
<input type="text" placeholder="双向绑定测试" ng-model="name">{{name}}
</div>

修改index.js,定义一个controller(就是那个.controller),名字为appCtrl(第一个参数,是个字符串),第二个参数就是这个controller的内容(是个函数)。

1
2
3
4
5
angular.module('app', [])
.controller('appCtrl', function ($scope) {
$scope.name = 123;
})
;

ng-controller就是一个控制器,控制器是什么概念呢?简单理解下就是和页面绑定在一起的数据,比如页面里面的文字,输入框里面的内容,甚至是一个按钮的点击事件对应(绑定)的函数。为什么要叫控制器呢,因为这里的代码可以控制那些数据,并且可以和页面上的内容双向绑定。这样我们要得到页面上的数据就不用再通过获取结点,在获取内容的方式了,可以直接获取内存中的数据,也可以直接修改内存中的数据修改页面显示。比如上述代码就是通过修改内存中的name来修改页面中的name,所以打开localhost:8080/可以看到input输入框里面已经有一个123了。至于controller里面的$scope是啥?emmmmmm…继续简单理解下就是包含页面所有数据的一个对象,页面的所有变量的双向绑定的值都在这个$scope里面。

一句话总结controller:控制页面的数据。另外,页面的数据全部在$scope里面,$scope里面的东西在页面上也可以直接用。

为了加深理解,我们加个按钮显示数据。

1
2
3
4
<div ng-controller="appCtrl">
<input type="text" placeholder="双向绑定测试" ng-model="name">{{name}}
<button ng-click="show()">show</button>
</div>
1
2
3
4
5
6
7
8
angular.module('app', [])
.controller('appCtrl', function ($scope) {
$scope.name = 123;
$scope.show = function () {
console.log($scope.name);
};
})
;

可以看到,在html里面我们在input下面加了一个button,并在标签里面加上了ng-click="show()"这个ng-click就是angular的事件绑定的写法,至于这个show()自然就是绑定在click时间的函数啦,show这个函数在controller里面就是$scope.show,之前说了,页面的所有内容都是在$scope下的。然后我们在controller里面给它赋值了一个函数,函数log出$scope.name,现在打开控制台,点击按钮,就能看到控制台输出了123,我们修改input的内容,再按一下按钮,会发现控制台输出了我们填写的内容,这就是双向绑定的直观概念,它的好处也就显而易见了,对于表单啊什么的直接绑定到$scope里面了,点击按钮就可以发送请求给后端了,不用麻烦的一个个获取。

我们试试看吧

1
2
3
4
5
<div ng-controller="appCtrl">
<input type="text" placeholder="用户名" ng-model="user.name"><br/>
<input type="password" placeholder="密码" ng-model="user.pwd"><br/>
<button ng-click="submit()">提交</button>
</div>
1
2
3
4
5
6
7
angular.module('app', [])
.controller('appCtrl', function ($scope) {
$scope.submit = function () {
console.log($scope.user);
};
})
;

我们修改了设置了controller的那个div里面的内容,加了两个input用于输入用户名和密码,ng-mode我们绑定到了user的两个属性,button的click绑到了submit函数。注意,我们ng-model不是name和pwd,而是user.name和user.pwd,这样controller里面直接$scope.user就是包含了两个属性的对象,非常方便。在另外,页面的数据会自动存放在controller里面,我们不用在controller里面重新定义一遍就可以直接用(比如这里就是console.log($scope.user);我们在外面并没有也不需要定义$scope.user)现在看看效果。打开http://localhost:8080随便输入什么,点击提交,就可以看到控制台输出了user对象。当然我们也可以初始化一下,设置默认值,这个在调试阶段特别好用:

1
2
3
4
5
6
7
8
9
10
11
angular.module('app', [])
.controller('appCtrl', function ($scope) {
$scope.user = {
name: 123,
pwd: 123
};
$scope.submit = function () {
console.log($scope.user);
};
})
;

刷新一下浏览器,可以看到两个输入框已经有内容了,在调试时就不用反复输入了。

好了,我们已经可以在控制台看到数据打印出来了,那么怎么发送到后端呢?这里就要用ajax了,ajax就是一个发送异步请求的东西而已,具体不详细讲,只说怎么用(具体内容可看我写的有关angular的内容,有空写的话。。。)在angular里面,提供了一个$http的服务用语发送请求。$http和$scope一样,都是一个angular提供的服务,需要作为controller的参数才能使用。

1
2
3
4
5
6
7
8
9
10
11
12
angular.module('app', [])
.controller('appCtrl', function ($scope, $http) {
$scope.user = {
name: 123,
pwd: 123
};
$scope.submit = function () {
console.log($scope.user);
$http.post('http://localhost:8080/user/login',$scope.user);
};
})
;

首先可以看到我们在controller的参数里面加入了$http,然后可以看到在submit函数里面加了一行$http.post('http://localhost:8080/user/login',$scope.user);。这一行表示向localhost:8080/user/login这个路径发送了一个post请求,请求的参数是$scope.user。好了,我们要在这里插入一点后端的内容。前端已经吧输入发过去了,后端怎么获取呢?还记得之前后端的router.get么,它接受get请求,事实上我们在浏览器上输入一个url就是一个get请求。现在要一个post,那么我们就加上对post的处理。
修改根目录的routes文件夹下的user.js

1
2
3
4
5
6
7
8
9
// routes/user.js
const express = require('express');
const router = express.Router();

router.post('/login',function (req, res, next) {
console.log('login');
});

module.exports = router;

这里我们先输出一个login字符串来看看是不是请求到了这个接口(所谓的接口就是一个uri,比如这里就是http://localhost:8080/user/login)。点击submit,然后打开运行着npm start的那个命令行,可以看到在黑框框下输出了一个字符串“login”,说明我们成功请求到这个接口了,那么问题就来了,我们怎么获取到传过来的参数呢?express有很好的中间件机制,当然是使用别人提供的中间件啦!这里我们用body-parser这个中间件,它可以预先帮我解析请求中的请求体,并把post请求的参数设置到req.body里面。首先要安装,在命令行多按几次ctrl+c终止那个那个npm start的命令,然后执行npm i --save body-parser,安装完后去修改根目录下的app.js,吧依赖引入进来,并用app.use把body-parser配置好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// app.js
// 引入path
const path = require('path');
// 引入express
const express = require('express');
// 实例化express
const app = express();
// 引入body-parser
const bodyParser = require('body-parser');
// 引入别的路由模块
const index = require('./routes/index');
const user = require('./routes/user');
const timetable = require('./routes/timetable');

// 解析json格式的post请求体,给req添加body属性
app.use(bodyParser.json());
// 中间件只解析urlencoded 请求体,并返回,只支持UTF-8编号文本
// extend
// ture->使用queryString库(默认) false->使用qs库。
app.use(bodyParser.urlencoded({ extended: false }));

// 设置静态资源路径,express自动处理get请求
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', index);
app.use('/user', user);
app.use('/timetable', timetable);

app.use(function (req, res, next) {
res.send('404 NOT FOUND!')
});

module.exports = app;

其实我们就加了这3行

1
2
3
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

第一行引入依赖没啥问题,后两行用app.use说明所有的请求都会经过他们,和设置静态页面一样的用法。至于后两行use里面啥意思,上面的文件也做了详细说明,这里,具体可以自行搜索相关资料进一步学习,这里我们暂时就知道怎么用就好了。
然后我们的req.body里面应该已经有内容了。修改routes/user.js,把之前的login字符串改为req.body

1
2
3
4
5
6
7
8
const express = require('express');
const router = express.Router();

router.post('/login',function (req, res, next) {
console.log(req.body);
});

module.exports = router;

好,继续运行npm start,打开浏览器按submit。在打开命令行的时候我们可以看到输出的不是login了,而是{ name: 123, pwd: 123 }这个对象,前端已经成功到后端了,那么后端怎么发送信息给前端呢?超简单res.send啊!不不不,我们的意思是,前端怎么收到呢?angular的$http早就帮我们搞定了这些。
routes/user.js

1
2
3
4
5
6
7
8
9
const express = require('express');
const router = express.Router();

router.post('/login',function (req, res, next) {
console.log(req.body);
res.send('登陆成功!');
});

module.exports = router;

public/js/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
angular.module('app', [])
.controller('appCtrl', function ($scope, $http) {
$scope.user = {
name: 123,
pwd: 123
};
$scope.submit = function () {
console.log($scope.user);
$http.post('http://localhost:8080/user/login',$scope.user)
.then(function (res) {
console.log(res);
})
};
})
;

后端部分就是加了一个res.send发送了一个字符串,前端部分可以看到我们在刚刚那个post下加了一个then,这里$http.post返回了一个promise,具体promise是啥可以百度也可以看我别的文章(哇,我都挖了几个坑了。。。慢慢填。。。)then接受两个参数,都是函数,第一个函数是返回成功时的情况,第二个函数就是返回错误是的情况。我们暂时先不考虑错误情况,直接then接受一个函数,然后把res作为参数并打印出来,可以看到控制台输出了好多东西。嘛。。。其实也就是一个对象啦,我们关注data这个字段就好了,可以看到data这个字段里面的东西就是后端返回的值。
装个逼用一下es6的解构赋值(也不算啥新东西啦,es8都要出来了。。。)

1
2
3
4
$http.post('http://localhost:8080/user/login',$scope.user)
.then(function (res) {
console.log(res);
})

改为

1
2
3
4
$http.post('http://localhost:8080/user/login', $scope.user)
.then(function ({data}) {
console.log(data);
})

再刷新浏览器点击提交,就可以看到控制台输出后端返回的字符串啦,是不是很简单~

随着逻辑越来越复杂,在controller里面既要控制页面的数据,又要和后端交互处理请求相应,那代码就会越来越庞大,是事实再分离一波了!这时候就可以手写service了。service字面意思就是服务,就是提供一些通用的服务供controller调用,其实之前用到的$scope,$http都是服务,只不过它们是angular提供的服务,我们也可以自己定义服务。比如定义一个UserService服务,把和用户相关的比如登录注册重置密码的逻辑都放在这里供controller调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// public/js/index.js
angular.module('app', [])
.controller('appCtrl', function ($scope, UserService) {
$scope.user = {
name: 123,
pwd: 123
};
$scope.submit = function () {
console.log($scope.user);
UserService.login($scope.user);
};
})
.service('UserService', function ($http) {
this.login = function (user) {
$http.post('http://localhost:8080/user/login', user)
.then(function ({data}) {
console.log(data);
})
}
})
;

这样虽然controller和service分开了,但是都放在一个文件还是觉得很乱,那我们进一步分离,首先删了public/js/index.js。因为我们要把它拆开。
在public文件夹下新建app.js作为主要入口,然后在public/js下新建controller.js和service.js
public/app.js

1
2
3
4
5
angular.module('app', [
'app.controller',
'app.service'
])
;

public/js/controller.js

1
2
3
4
5
6
7
8
9
10
11
12
angular.module('app.controller', [])
.controller('appCtrl', function ($scope, UserService) {
$scope.user = {
name: 123,
pwd: 123
};
$scope.submit = function () {
console.log($scope.user);
UserService.login($scope.user);
};
})
;

public/js/service.js

1
2
3
4
5
6
7
8
9
10
angular.module('app.service', [])
.service('UserService', function ($http) {
this.login = function (user) {
$http.post('http://localhost:8080/user/login', user)
.then(function ({data}) {
console.log(data);
})
}
})
;

好了,分完了,就是把controller,service简单拆了一下,另外controller.js第一行的module名字取为“app.controller”,service的module名字取为“app.service”。然后在app.js里面的module的第二个参数去进行“依赖注入”,就是把module名字放进数组里就好啦。
最后还有一步!把这些js引入到html里面(把index的引入删除在加上新加的三个文件的引入)

1
2
3
4
<script src="https://cdn.bootcss.com/angular.js/1.6.6/angular.js"></script>
<script src="js/controller.js"></script>
<script src="js/service.js"></script>
<script src="app.js"></script>

好了运行http://localhost:8080看看效果吧(应该没什么效果,和原来一样的界面)

webpack的加入

可以看到,之前我们只是简单做了个代码分离,就要在index.html多加入三个script标签,以后万一要加上别的库文件又是一大堆script标签,然后要加上css的话继续一堆link。。。另外,每次写完代码都要刷新一遍网页也很烦,后端部分我们已经用supervisor自动重启服务器来减轻负担了,前端有没有自动刷新浏览器什么的工具啊?必须是有的,而且可以一起吧上面两个问题全部解决!

webpack就是这么一个工具,它会从一个(或多个)js文件开始,一次查找所有的依赖,并引入进来打包成一个js文件。它强大的加载器以及插件可以做到找到css依赖甚至编译less,scss,ts等语言并引入。还有一个叫做webpack-dev-server的东西可以实现热加载(就是自动刷新浏览器,甚至不刷新浏览器就可以修改页面)。它的工作全部都写入一个配置文件,并根据配置文件的内容,实现一定功能,比如最基础的查找js依赖,配置完加载器后可以加载css文件,编译less,scss,配置devServer后可以热更新,配置html插件后可以吧标签自动插入到html文件里面。

听得那么多都心动了吧?那我们开始吧!

既然要用webpack,当然得先安装(下载)喽,就是一句npm i的事,但是我们要装的不只是webpack,还有一些别的东西(因为webpack要加载html等需要别的配置,所以需要相关加载器或者插件等)这里我们就一次性下完啦。。。
npm i --save-dev webpack webpack-dev-server html-webpack-plugin open-browser-webpack-plugin
npm i --save-dev babel babel-core babel-loader babel-plugin-istanbul babel-preset-es2015
npm i --save-dev css-loader less-loader postcss-loader sass-loader style-loader
npm i --save-dev file-loader html-loader istanbul-instrumenter-loader url-loader
为了美观点我这里分成了4条,当然也可以写到一句里面。。第一条是webpack和热加载的服务器webpack-dev-server,后面两个是webpack的两个插件,第一个用于自动在html里面插入标签(当然它有更牛逼的功能,这里我们就用来插入标签了,就是打包后的script标签就是靠它加入到html文件的)第二个是自动打开浏览器的插件。第二条是js相关的编译加载器,第三条是css相关,最后一条是别的杂七杂八的加载器。。。因为webpack本身只是处理js中的模块依赖问题的,有了这些加载器后可以加载别的css等文件(还可以编译es6,编译less等)

好了装完后怎么让它们合作起来呢?根目录下新建一个webpack.config.js的文件(这是webpack的默认配置文件名,要是换别的名字需要在启动webpack时指定。用这个名就不用另外指定了)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const OpenBrowserPlugin = require('open-browser-webpack-plugin');
const webpack = require('webpack');

// 定义了一些路径
const APP_PATH = path.resolve(__dirname, 'public/app.js');
const BUILD_PATH = path.resolve(__dirname, 'build');

module.exports = {
// 入口,分为app 入口和提取插件库的入口
entry: {
app: APP_PATH,
},

// 输出文件路径和名字
output: {
path: BUILD_PATH,
filename: 'bundle.js'
},

plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
inject: 'body',
minify: false
}),

//自动启动浏览器
new OpenBrowserPlugin({url: 'http://localhost:8081'})
],

//dev 服务器
devServer: {
historyApiFallback: true,
hot: true,
inline: true,
progress: true,
contentBase: "./build", // dev server的根路径
port: 8081
},

// 加载器
module: {
"loaders": [
{
test: /\.html$/,
loader: 'html-loader'
},
{
test: /\.css$/,
loader: 'style-loader!css-loader'
},
{
test: /\.less/,
loader: 'style-loader!css-loader!less-loader'
},
{
test: /\.(png)|(jpg)|(gif)|(woff)|(svg)|(eot)|(ttf)$/,
use: [
{
loader: "url-loader",
options: {
limit: 50000, //小于50K的 都打包
name: "[hash:8].[name].[ext]",
publicPath: "img/", //替换CSS引用的图片路径 可以替换成爱拍云上的路径
outputPath: "../img/" //生成之后存放的路径
}
}
]
},
{
test: /\.js?$/,
loader: 'babel-loader',
include: APP_PATH,
query: {
presets: ['es2015']
}
}
]
},
devtool: 'source-map'
};

我知道被甩一脸代码你的内心是拒绝的,emmm…毕竟webpack有很多东西,详细信息需要参考官网文档,这里只给出了一些简单的配置例子而已(我也掌握的不多,仅仅是能用的程度ORZ…)之前已经提到了webpack是依赖处理工具,所以开头配置就是entry字段,entry表示依赖的起点,表示js的根文件,也是它查找依赖的起点。然后有entry就有output,这就是编译完后的代码存放的位置,另外webpack通过插件和模块加载器来扩展功能,比如我们加了HtmlWebpackPlugin插件配置,就表示吧生成的代码作为资源引用插入到对应html的body标签,OpenBrowserPlugin插件就表示以默认浏览器自动打开对应url。关于这些插件的用法每个都有相关的官方文档可供参考,这些不是webpack自带的,需要开发者自行查找相关插件(一般你遇到的诡异的需求都有别人给你做好的插件可以用)。devServer字段就是配置webpack-dev-server的地方,后面的module就是模块的加载器了,比如webpack不认识html文件就需要用HTML加载器引入。这里就是简单提一下webpack算是抛砖引玉,要深入学习还是要去啃webpack官方文档啊~另外这里就是单纯的够用,比如现在babel有些更新也没怎么关注…一切使用以官方文档为准。

好了,配置文件都写完了是不是迫不及待要试一下?别急,有些地方也得改一改。首先当然是html文件删除所有的script,link标签。
public/index.html就只剩下下面这坨光秃秃的代码了,看上去清爽了一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>timetable</title>
</head>
<body ng-app="app">
<h1>Time Table</h1>
<div ng-controller="appCtrl">
<input type="text" placeholder="用户名" ng-model="user.name"><br/>
<input type="password" placeholder="密码" ng-model="user.pwd"><br/>
<button ng-click="submit()">提交</button>
</div>
</body>
</html>

至于我们删除部分的代码之前webpack配置的插件会吧打包后的代码自动引入的~
下一步改造js,既然app.js定义了app模块和界面绑定,它显然就是起点啦

1
2
3
4
5
6
7
8
9
10
11
12
// public/app.js
import angular from 'angular';
import controller from './js/controller';
import service from './js/service';

angular.module('app', [
controller,
service
])
;

import './css/index.css';

emmmmm….似乎我们把angular的scrip也删了,然后还忘记装了?那装一下吧。。。npm i --save angular。然后可以看到我们原来直接写在数组里的是字符串,现在是引入的内容。至于这个import只是一个es6的模块语法,表示从对应的文件引入某个变量或者函数。既然引入了,那对应的文件是不是要导出?没错,继续改造controller和service文件

1
2
3
4
5
6
7
8
9
10
11
12
13
//public/js/controller.js
export default angular.module('app.controller', [])
.controller('appCtrl', function ($scope, UserService) {
$scope.user = {
name: 123,
pwd: 123
};
$scope.submit = function () {
console.log($scope.user);
UserService.login($scope.user);
};
})
.name;
1
2
3
4
5
6
7
8
9
10
11
//public/js/service.js
export default angular.module('app.service', [])
.service('UserService', function ($http) {
this.login = function (user) {
$http.post('http://localhost:8080/user/login', user)
.then(function ({data}) {
console.log(data);
})
}
})
.name;

controller和service的修改完全一样就是加个导出而已,至于controller和service用到的angular,已经在app导入过了,你想再导入一遍也没啥问题,因为webpack会判断发现已经有了就不会重复导入。导出的语法就是一个export,至于default,就是默认导出,这个每个文件只能有一个,别人导入的时候就可以不用管有什么无脑导入就行了。至于更多的es6模块部分的内容建议阅读相关博客深入了解。(敲黑板!!es6是必修课!!)
最后一步,启动命令行!当然先加个别名,可以少打点东西也好记,怎么加之前已经提过了,就是在package.json的scripts字段加一条
"dev": "webpack-dev-server --history-api-fallback --hot --inline --progress --content-base ./build/ --port 8081"
因为之前装了好多依赖,所以现在package.json文件是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
{
"name": "timetable",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "supervisor bin/www",
"dev": "webpack-dev-server --history-api-fallback --hot --inline --progress --content-base ./build/",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"angular": "^1.6.6",
"body-parser": "^1.18.2",
"express": "^4.16.2"
},
"devDependencies": {
"babel": "^6.23.0",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-plugin-istanbul": "^4.1.5",
"babel-preset-es2015": "^6.24.1",
"css-loader": "^0.28.7",
"file-loader": "^1.1.5",
"html-loader": "^0.5.1",
"html-webpack-plugin": "^2.30.1",
"istanbul-instrumenter-loader": "^3.0.0",
"less-loader": "^4.0.5",
"open-browser-webpack-plugin": "0.0.5",
"postcss-loader": "^2.0.8",
"sass-loader": "^6.0.6",
"style-loader": "^0.19.0",
"supervisor": "^0.12.0",
"url-loader": "^0.6.2",
"webpack": "^3.8.1",
"webpack-dev-server": "^2.9.4"
}
}

恩,终于结束了。。。转换的过程是痛苦的,结果是美好的,现在打开命令行进入项目根目录输入npm run dev,可以看到浏览器自动打开了,界面什么的和之前一样,我们可以随便修改点css代码看看效果,比如把颜色改为蓝色,然后回到浏览器就可以看到自动更新了。然而当我们点击提交却出错了,他说我们跨域了。跨域是因为webpack-dev-server自己开了一个服务器在8081端口,我们额后端是8080端口,所以就被拒绝了,我们只要在后端加几行就好了。
打开根目录下的app.js文件,在这几行的上面

1
2
3
4
// app.js
app.use('/', index);
app.use('/user', user);
app.use('/timetable', timetable);

加上这几行

1
2
3
4
5
6
7
8
9
// app访问预处理中间件
// 设置跨域请求许可以及返回内容的数据格式
app.use(function (req, res, next) {
res.setHeader("Access-Control-Allow-Origin", "*"); //允许哪些url可以跨域请求到本域
res.setHeader("Access-Control-Allow-Methods", "GET,POST"); //允许的请求方法,一般是GET,POST,PUT,DELETE,OPTIONS
res.setHeader("Access-Control-Allow-Headers", "x-requested-with,content-type,Token"); //允许哪些请求头可以跨域

next();
});

然后在点击提交试试,应该就好了,这是nodejs常用的解决跨域的方式。


数据库

MongoDB简介

前后端都连上了,该到数据库了。前端发过来的用户名和密码,我们需要存到数据库,一个注册的功能通常都是这样,前端发送用户输入的数据到后端,后端存入数据库中,这样下次这个用户只需要登录就好,并且与他相关的数据也会保存在一块。

常用的数据库是关系数据库,常见的Oracle,SQL Server还有MySQL等都是关系数据库。但是面对互联网日益爆增的数据量,关系数据库逐渐显得力不从心,于是出现了一类新型的数据库。我们称为NoSQL,Not Only SQL。非关系型数据库。非关系数据库又有好多种,MongoDB属于其中的文档型数据库。它的优点是数据结构要求不严格,表结构可变,不需要像关系型数据库一样需要预先定义表结构(这句来自百度百科)。而且存储结构就是json的结构,非常适合与js协作。

MongoDB的安装不多说了就是一个数据库引擎而已。主要是启动会有点东西需要配置。而每次手动启动也很烦一般就把它设置为一个服务了然后开机自启就行。这里引用菜鸟教程的配置方式

管理员模式打开命令行窗口
创建目录,执行下面的语句来创建数据库和日志文件的目录
mkdir c:\data\db
mkdir c:\data\log
创建配置文件
创建一个配置文件。该文件必须设置 systemLog.path 参数,包括一些附加的配置选项更好。
例如,创建一个配置文件位于 C:\mongodb\mongod.cfg,其中指定 systemLog.path 和 storage.dbPath。具体配置内容如下:

1
2
3
4
5
systemLog:
destination: file
path: c:\data\log\mongod.log
storage:
dbPath: c:\data\db

通过执行mongod.exe,使用–install选项来安装服务,使用–config选项来指定之前创建的配置文件。
“C:\mongodb\bin\mongod.exe” –config “C:\mongodb\mongod.cfg” –install
启动MongoDB服务
net start MongoDB
关闭MongoDB服务
net stop MongoDB
移除MongoDB服务
“C:\mongodb\bin\mongod.exe” –remove

闲每次开启服务麻烦可以去windows的“服务”里设置开机自启动

MongoDB可视化工具

所谓可视化工具也可以称为数据库管理工具,就是可以直接在界面上以用户友好的方式查看数据库的内容。MongoDB的话我常用的是mongobooster和Robomongo,mongobooster支持数据库的json文件导入导出,但是界面上个人认为Robomongo更友好一点,所以我一般做简单的查询修改都是用Robomongo。至于这两个软件就自行寻找安装包安装啦,就是两个普通软件而已…mongobooster是收费的,不过免费部分的功能也够用了(反正我只是用来导入导出数据。。。)

Robmongo打开后要创建连接,链接本地的数据库服务后才可以操作里面的内容,点击create,保持默认点击save就好了,你愿意的话save之前改一下name啥的也行,(我改成了localhost。。)另外MongoDB默认是没有密码的,所以安全方面要另外设置,这里只是入门就保持默认了,做项目要用MongoDB一定要设置密码哦。然后连上后里面只有system和test,这是默认的数据库别理它。。

mongoose库介绍

前面配置了数据库和可视化软件,现在就要正式码代码啦。道理我都懂,所以我要怎么用js来操作数据库呢?emmmmm,在后端程序和数据库引擎之前我们需要一个桥梁,这就是数据库驱动,所以我们需要安装一个库来帮我们,这里我选的是mongoose这个库。执行npm i --save mongoose安装mongoose库。装完后我们需要创建一个链接,连上后就可以用js代码操作数据库了。链接很简单,无非就是端口号啊数据库名啊账号密码啊之类的对上就好了,因为默认没有账号密码所以就需要端口号和数据库名,这里我们选择time_table作为名字。然后我们总不能操作一次重新写一段连接数据库的代码吧?所以根目录下新建一个config文件夹里面新建config.js,写上以下内容

1
2
3
4
5
6
7
8
9
//config/config.js
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/time_table');

const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', console.log.bind(console, 'connection success:'));

exports.mongoose = mongoose;

这些代码均可以从mongoose的文档里找到,代码很简单,导入mongoose库后创建一个链接然后导出mongoose。我们之前不是说后端已经拿到数据了么,在routes/user.js里。我们只是简单的输出了一下前端的数据,现在我们要真正把它保存到数据库里面。以下的代码也从文档中可以找到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//routes/user.js
const express = require('express');
const router = express.Router();

//导入已经配置好数据库连接的mongoose库
const mongoose = require('../config/config').mongoose;

//定义User这个"表",noSQL术语叫做集合(collection),调用mongoose.model并传入第一个参数是表名,第二个是表的结构
//其中第二个参数作为表的结构使用mongoose.Schema定义,具体内容官网都有详细的文档。
const userSchema = mongoose.Schema({
name: String,
pwd: String,
});
const User = mongoose.model('User', userSchema);

router.post('/login',function (req, res, next) {
console.log(req.body);
//根据定义的model新建一个对象并保存,第二个参数是保存成功后的回调函数
const user = new User(req.body);
user.save(function (err, user) {
if (err) return console.error(err);
console.log(user);
res.send('登陆成功!');
});
});

module.exports = router;

可以看到我们对routes/user.js做了点手脚,首先导入配置好的mongoose库,然后新建了一个User的结构,另外提一句,mongoose.model第一个参数的复数形式才是真正的表名。前端数据发过来后我们用发过来的数据new了一个user并保存了。现在去页面上点击提交可以看到命令行里面输出了

1
2
{ name: 123, pwd: 123 }
{ name: '123', pwd: '123', _id: 5a8d7d85148af21d2ca5baa7, __v: 0 }

第一个自然就是前端传过来的数据,第二个就是保存的数据,MongoDB自动生成了_id_v两个字段_id自然是唯一的值用于区分每个对象,这个值大家应该都不一样,至于这个值怎么产生的可以参考这个,总之你知道_id可以用于唯一区分数据库里一条数据就好了。_v是版本号,具体用法可以看官方文档。然后我们打开Robmongo可以看到里面多了”time_table”这个,如果没有的话右键连接名(就是侧栏最上面那个,名字取决于你创建时的名字,我的是localhost,默认是New Connection)找到第二个Refresh,刷新完就有了。然后可以看到下面有Collections,Functions,Users。展开Collections,可以看到users,双击就能看到里面的数据了,可以看到这样的内容

1
2
3
4
5
6
7
/* 1 */
{
"_id" : ObjectId("5a8d7a29f54eab2accaca062"),
"name" : "123",
"pwd" : "123",
"__v" : 0
}

去网页上多点几下提交再打开Robmongo就变成这样了(没变化的话按f5刷新)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 1 */
{
"_id" : ObjectId("5a8d7a29f54eab2accaca062"),
"name" : "123",
"pwd" : "123",
"__v" : 0
}

/* 2 */
{
"_id" : ObjectId("5a8d7d85148af21d2ca5baa7"),
"name" : "123",
"pwd" : "123",
"__v" : 0
}

/* 3 */
{
"_id" : ObjectId("5a8d840e148af21d2ca5baa8"),
"name" : "123",
"pwd" : "123",
"__v" : 0
}

可以看到我们每次点一下提交就执行了一次保存,又因为主键id是自动生成的所以name和pwd重复它也会保存,如果我们要让name不能重复,那这些逻辑都需要自己代码实现,比如先查询一下数据库看看name有没有被占用,没有的话就保存,有的话就返回一个错误信息。我们可以对login接口做如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
router.post('/login', function (req, res, next) {
console.log(req.body);
User.findOne({'name': req.body.name}, function (err, user) {
if (err) {
console.log(err);
res.send('数据库错误');
return;
}
if (user) {
res.send('用户已存在');
} else {
//根据定义的model新建一个对象并保存,第二个参数是保存成功后的回调函数
const user = new User(req.body);
user.save(function (err, user) {
if (err) return console.error(err);
console.log(user);
res.send('登陆成功!');
});
}
});
});

User.findOne就是去users这个collection里面查找一个,找到或者没找到都会返回,找到了后回调函数的第二个参数就是user。没找到第二个参数就是null。所以如果user不是null就返回用户已存在,否者就保存。可以去网页上试验一下,点击提交可以看到控制台输出了用户已存在,把name改成1234再点提交就输出登录成功了。emmm…仔细想想这怎么会是登录呢,这明明应该是注册的逻辑嘛~那我们就做简单的修改,把它变成register就好了~上面的/login改成/register,在新建一个login的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
router.post('/login', function (req, res, next) {
User.findOne(req.body, function (err, user) {
if (err) {
console.log(err);
res.send('数据库错误');
return;
}
if (user) {
res.send('登陆成功!');
} else {
res.send('账号或密码错误!');
}
});
});

router.post('/register', function (req, res, next) {
console.log(req.body);
User.findOne({'name': req.body.name}, function (err, user) {
if (err) {
console.log(err);
res.send('数据库错误');
return;
}
if (user) {
res.send('用户已存在');
} else {
//根据定义的model新建一个对象并保存,第二个参数是保存成功后的回调函数
const user = new User(req.body);
user.save(function (err, user) {
if (err) return console.error(err);
console.log(user);
res.send('登陆成功!');
});
}
});
});

这样才像样嘛,注册才需要写入数据库,登录只要判断有没有这儿数据就行了,有就说登陆成功没有就说账号或者密码错误。关注login接口的这一行User.findOne(req.body, function (err, user) {...})第一个参数直接就是req.body。因为我们定义的数据结构和前端传过来的一样所以就这么写,完整的写法是这样的:User.findOne({name:req.body.name, pwd:req.body.pwd}, function (err, user) {...})(在这里也是多此一举的写法。。。)。findOne的第一个参数就是查找条件,只有完全满足才行。api文档看这里

改完后端那顺便前端也改了呗~
public/index.html

1
2
3
4
5
6
<div ng-controller="appCtrl">
<input type="text" placeholder="用户名" ng-model="user.name"><br/>
<input type="password" placeholder="密码" ng-model="user.pwd"><br/>
<button ng-click="login()">登录</button>
<button ng-click="register()">注册</button>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//public/js/controller.js
export default angular.module('app.controller', [])
.controller('appCtrl', function ($scope, UserService) {
$scope.user = {
name: 123,
pwd: 123
};
$scope.login = function () {
UserService.login($scope.user);
};
$scope.register = function () {
UserService.register($scope.user);
};
})
.name;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//public/js/service.js
export default angular.module('app.service', [])
.service('UserService', function ($http) {
this.login = function (user) {
$http.post('http://localhost:8080/user/login', user)
.then(function ({data}) {
console.log(data);
})
};
this.register = function (user) {
$http.post('http://localhost:8080/user/register', user)
.then(function ({data}) {
console.log(data);
})
}
})
.name;

改动很简单,就是原来提交的按钮改成了两个按钮,一个登陆一个注册。对应的controller,service也改成了登录,注册的逻辑。当然,真实应用的注册应该不止这两个字段,登录和注册的内容应该也是不一样的。这里就一切从简了。

细心的童鞋应该发现了,routes/user.js里面除了一个登陆一个注册还有一些诡异的代码,就是定义数据结构啊之类的。如下

1
2
3
4
5
6
7
8
9
10
//导入已经连接好数据库的mongoose库
const mongoose = require('../config/config').mongoose;

//定义User这个"表",noSQL术语叫做集合(collection),调用mongoose.model并传入第一个参数是表名,第二个是表的结构
//其中第二个参数作为表的结构使用mongoose.Schema定义,具体内容官网都有详细的文档。
const userSchema = mongoose.Schema({
name: String,
pwd: String,
});
const User = mongoose.model('User', userSchema);

这些代码定义了users集合的数据结构,并且User这个模型(model)也是和数据库直接绑定的,就是说我们要通过这个User来操纵users这个集合。所以别的地方要用users里面的数据就要获取到User的实例,那么我们就要吧User导出,但是这样是不合理的,因为这个文件只是一个路由,User相关的定义应该放到别的地方统一定义。所以在根目录下新建model文件夹,并新建User.js吧模型定义的相关代码移出去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//model/User.js
//导入已经连接好数据库的mongoose库
const mongoose = require('../config/config').mongoose;

//定义User这个"表",noSQL术语叫做集合(collection),调用mongoose.model并传入第一个参数是model名,第二个是表的结构
//其中第二个参数作为表的结构使用mongoose.Schema定义,具体内容官网都有详细的文档。(比如数据类型之类的)
//第三个参数是集合名,省略的话默认会使用第一个参数的全小写的复数形式(所以这个例子写不写都一样)
const userSchema = mongoose.Schema({
name: String,
pwd: String,
});
const User = mongoose.model('User', userSchema, 'users');
// const User = mongoose.model('User', userSchema); 和上面一行效果一样
// const User = mongoose.model('User', userSchema, 'user'); 这么写集合名就是user了

exports.User = User;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//routes/user.js
const express = require('express');
const router = express.Router();

const User = require('../model/User').User;

router.post('/login', function (req, res, next) {
User.findOne(req.body, function (err, user) {
if (err) {
console.log(err);
res.send('数据库错误');
return;
}
if (user) {
res.send('登陆成功!');
} else {
res.send('账号或密码错误!');
}
});
});

router.post('/register', function (req, res, next) {
console.log(req.body);
User.findOne({'name': req.body.name}, function (err, user) {
if (err) {
console.log(err);
res.send('数据库错误');
return;
}
if (user) {
res.send('用户已存在');
} else {
//根据定义的model新建一个对象并保存,第二个参数是保存成功后的回调函数
const user = new User(req.body);
user.save(function (err, user) {
if (err) return console.error(err);
console.log(user);
res.send('登陆成功!');
});
}
});
});

module.exports = router;

到这里,前端后端数据库都连上了,代码也很清晰了。可以说我们的基础部分已经完成了。整个教程也到了尾声,本来打算介绍下测试,但是考虑到Angularjs即将被弃用就不打算再打一遍了,而Angular的cli配了一个开箱即用的测试框架,所以这部分内容以后写Angular相关的时候再详细介绍吧。至于开头说的time table这个项目,我是之前打过一遍的,功能也基本完整(还有测试相关代码)。这次写教程又根据文章节奏打了下,所以就到最简单的登录注册。并用commit记录内容。有心学习的同鞋可以去github的commit记录上一步步对照内容看下来。


项目代码

  1. 根据上面内容写的代码 timetable
  2. 文初说的完整的项目代码 timetable_fkq