信息管理系统实训
本次课是以 Nodejs 为基础的前、后端项目课程。
一、项目开发准备
1.1 博客系统简介
博客是一种通常由个人管理、不定期张贴新文章的网站。开发博客系统,本质上就是通过开发动态 Web 网站,在互联网上建立个人博客的一整套系统。
1.2 项目需求分析
-
需求分析原因和方式
-
项目中,需求驱动开发,即开发人员需要以需求为目标来实现业务逻辑。
-
企业中,借助产品原型图分析需求;需求分析后,前端按照产品原型图开发前端页面,后端开发对应的业务及响应处理。
-
-
需求分析的内容
-
页面及页面上的业务逻辑
-
归纳业务逻辑并划分模块
-
-
项目主要页面及模块划分
-
后台:
-管理首页
-文章管理
-个人中心
-
前台:
-注册页面-登录页面
-博客首页
-博客详情
-
1.3 项目开发模式
- 开发模式:前、后端分离
- 前端技术选型与框架:HTML5、CSS3、JavaScript、JQuery(JS的类库-Ajax技术)、Bootstrap(响应式页面布局框架)
- 后端技术选型与框架:Nodejs、Express(Node.js服务器框架)、xtpl模板引擎、Session技术
- 数据的持久化:MySQL
1.4 需要安装的软件
- VScode
在 D 盘或 E 盘新建自己名字命名的项目练习文件夹,并将 home 文件夹粘贴到练习文件夹中 ,在VSCode 中打开。
- Nodejs
Nodejs 开源且跨平台,在 Windows 平台,Nodejs 就是一个以 .msi
结尾的安装程序,分为 32位 和 64位 两个版本,双击运行根据引导程序完成安装即可。
打开 cmd 或 powershell 或 VScode 中的终端,执行如下命令,出现版本则为安装成功( npm 是 node.js 的第三方模块仓库上传下载工具)。
>node -v;
>npm -v;
注意 ! ! ! 所有的开发软件都不要安装在中文目录下,否则有可能出现意想不到的错误或兼容问题。
- Mysql和Navicat
- Navicat是操作Mysql的客户端软件
- 推荐Mysql版本5,如果安装版本8,使用Navicat访问Mysql时需要进行配置(版本8使用了新的加密方式)
二、认识 Nodejs
Node.js® 是一个基于 Chrome V8 引擎 的 JavaScript 运行时。
1.1 宿主环境
以网景的 Netscape Navigator 内置的 JavaScript 1.1 为蓝本,由ECMA-262定义的 ECMAScript 是一种脚本语言标准,它对该脚本语言的语法、类型、语句、操作符等做出明确规定。
JavaScript 是由 ECMAScript、DOM、BOM 三部分组成,浏览器实现了 ECMAScript 标准,使其能够运行于浏览器,我们称浏览器为 ECMAScript 的宿主环境。可以说 JavaScript 是运行在客户端的。
而 Node.js 则是运行在服务端的,即 Nodejs 是可以运行 ECMAScript 的宿主环境。Node.js 是一个事件驱动I/O服务端 JavaScript 环境,基于 Google 的 V8 引擎并加以优化,执行速度快,性能好。
1.2 体验
Node.js 程序是以 .js
结尾的文件,在 VSCode 中创建 demo1.js
console.log("Hello World!");
打开 VSCode 终端或 cmd 命令行工具,执行 demo1.js
>node path/demo1.js
注:Nodejs 和 JavaScript 都是基于 ECMAScript 语言规范的,二者的语法具有一致性,显著的区别是 Node.js 中没有 DOM 和 BOM,因此无法使用 window 或 document 相关语法。
# 语法错误
alert('Hello Nodejs!');
document.write('你好,世界!');
注意:创建文件名或者文件夹的名字一定一定一定要避免使用 node!
二、模块
2.1 什么是模块
Node.js 在 ECMAScript 的基础上扩展并封装了许多高级特性,如文件访问、网络访问等,使 Node.js 成为一个很好的 Web 开发平台。
基于 Node.js 这个平台将 Web 开发常用的一些功能进行封装,称为模块。模块是 Node.js 使代码易于重用的一种组织和包装方式。
Node.js 的模块有系统模块、第三方模块和用户模块。开发人员可以使用 Node.js 自带的系统模块,还可以使用来自 npm 仓库的第三方模块,同时还可以封装自己的模块发布到 npm 仓库中与他人分享。
导入模块的方法:使用require函数
2.2 系统模块
在安装 Node.js 时一些模块自动被安装,这些模块大多由 Node.js 官方维护,称这类模块为系统(核心)模块,如 fs 、path 、os 、http 等。
如 fs 模块是 Node.js 中用于操作文件读写的模块。使用 fs 模块创建文件并写入内容的代码如下,:
// demo2.js,导入系统模块 fs , const 用于定义常量,推荐使用常量来接收系统模块
const fs = require('fs');
// 文件操作,其中写文件writeFile方法体现了Node.js的编码风格,即都可以(必须)有一个回调函数作为参数
//参数1:文件路径/文件名
//参数2:文件中写入的内容
//参数3:回调函数,作用是查看执行的结果,第一个参数返回错误信息,为 null 则成功执行
fs.writeFile('./demo2.html', '<h1>hello world!</h1>', function(err,data) {
//console.log(err);
if(!err) {
console.log('文件创建成功!');
}
});
Node.js中文API文档:http://nodejs.cn/api
2.3 用户模块
用户模块是相对于系统模块而言的,由开发者根据逻辑需要自行封装完成,以单个或多个独立文件构成。Nodejs 中的模块大多以 .js
或 .json
文件形式存在。
实际上,所有的.js文件都被视为模块!
.js
形式存在的模块:
// demo3.js
function sayHi(name) {
console.log('Hi~'+name);
}
//模块封装后,外部要调用内部的属性和方法,必须要【暴露】出去!!!(相当于Java中的public)
module.exports.abc = sayHi;
- 外部模块调用:
//demo4.js
//导入 demo3.js
const demo = require('./demo3');
demo.abc('小明');
- 以
.json
形式存在的模块:
{
"name": "小明",
"age": 18,
"gender": "男"
}
2.4 第三方模块
Nodejs 有着庞大的社区支持,全世界优秀的开发人员封装了大量实用的模块,托管在开源平台上供大家随意下载使用。https://www.npmjs.com
为了方便大家上传、下载开源模块,社区还提供了专门在平台上传和下载的工具 npm
,并随着 Node.js 一起被安装了。
下载模块分为本地安装和全局安装:
- 本地安装
>npm install mysql
执行上述命令后,将会在 npmjs.org 平台下载 mysql 模块,然后在执行命令所在的目录创建 node_modules
目录,mysql 模块会被下载到 node_modules
目录中。需要注意的是,本地安装的模块只能在当前项目中使用.
- 全局安装
>npm install nodemon -g
执行上述命令后,将会在 npmjs.org 平台下载 nodemon 模块,然后nodemon 模块默认会被下载到 C 盘的用户下的 node_modules 目录中。全局安装的模块所有项目都可以使用。
执行 npm config list
可查看全局安装的默认目录 。
tip:通常项目中用到的功能模块使用本地安装,开发中使用的工具模块使用全局安装。
- 本地卸载
>npm uninstall mysql
执行上述命令后会将本地安装的mysql模块从 node_modules
目录中删除。
- 全局卸载
>npm uninstall nodemon -g
执行上述命令后会将全局安装的nodemon模块从 node_modules
目录中删除。
2.5 描述文件
若干模块组合起来放在统一目录中时推荐创建 package.json
文件,该文件中记录模块的名称、版本、依赖等信息,并可以修改。package.json
文件即可以手动创建,也能通过命令快速创建。
>npm init
# 或加参数-y,跳过引导设置,使用默认设置
>npm init -y
-
name
标记模块的名称,名称中不允许包含大写字母 -
version
标记模块的版本号 -
main
模块入口,默认为index.js
-
script
自定义脚本命令,以npm run 命令名称
形式调用,npm run start
时,可以省略run
-
dependencies
记录所有被依赖的模块,安装模块时添加--save
或-S
模块会被记录在package.json
中
>npm install mysql --save
devDependencies
记录开发阶段所有被依赖的模块,安装模块时添加--save-dev
或-D
>npm install gulp --save-dev
tip:有了package.json
后,即使网站代码中没有 node_modules 文件夹,可以使用命令:
>npm install
由 npm 通过读取dependencies
,自动下载依赖的所有模块。
2.6 加载机制
在 Nodejs 中安装和导入不同类型的模块时需要了解 Nodejs 模块的加载机制:
-
安装第三方模块时,并不一定会在执行命令的位置保存下载的模块代码
-
先看执行命令的位置是否有 node_modules ,有则保存在当前的 node_modules 中;
-
没有则依次向父级目录查找,直到找到 node_modules ;
-
所有祖先级都未找到(顶级为盘符),则在执行命令的位置自动创建 node_modules (开发项目时要求在这个位置)。
-
-
用 require 导入模块时,会按照一定的顺序查找模块
- 模块路径只有模块名时,则优先认为是系统模块,则先去查找【系统模块】,再去执行命令所在目录中的
node_modules
中查找,然后依次向父级目录中的node_modules
中查找。
// 系统模块能找到 const http = require('http'); // 系统模块找不到,然后去 node_modules 中查找 const mysql = require('mysql');
- 如果是路径,即以
./
、../
开头的相对路径或以盘符开头的绝对路径查找模块,则严格按路径加载模块。
// 参照当前文件位置查找 user.js 模块 const user = require('./user'); // 当前文件的上一级查找 cart.js 模块 const cart = require('../cart');
- 模块路径只有模块名时,则优先认为是系统模块,则先去查找【系统模块】,再去执行命令所在目录中的
注意:加载模块可以省略文件后缀。
结论:第三方模块由于全被放到了 node_modules 中,所以一般不使用路径导入;用户自己编写的功能模块推荐使用相对路径或者绝对路径。
三、HTTP服务
3.1 HTTP协议
网站架构中的浏览器与服务器之间遵守的是 HTTP 协议,该协议规定了浏览器与服务器交互的准则,其中最核心部分是约束了浏览器要以何种方式发起请求,服务器应该以何种方式进行响应。
传统的网站架构一般都要依赖某一个独立的服务器,如 Apache、Tomcat、Nginx 等,实际上就是基于 HTTP 协议开发出来的,已经将 HTTP 协议所规定的大部分内容实现。
而 Node.js 的 Web 服务略有不同,是完全自创建服务器。Nodejs 中提供了专门的系统模块 http
创建 HTTP 服务,它暴露了有许多与 HTTP 协议相关的方法,然后由开发者自主完成 HTTP 中请求与响应的操作。
3.2 请求和响应
HTTP中请求和响应的数据报文格式:
- 请求----浏览器
HTTP 规定任何请求都可以由请求行、请求头、请求主体 3 部分构成:
- 请求行由请求地址和请求方法构成
- 请求头由多组键值对构成,大部分由浏览器自动添加
- 请求主体实际为请求时所携带的数据
- 响应----服务器
HTTP 规定任何请求都可以由 状态行、响应头、响应主体 3 部分构成:
- 状态行由状态码和状态信息构成,如 200 OK 、404 Not Found 等
- 响应头由多组键值对构成,如 Content-Type
- 响应主体实际为服务器交给浏览器的内容
以 Node.js 创建 HTTP 服务器的过程为例:
新建 demo5.js ,首先要创建服务。导入 http 系统模块后,调用模块内部的方法 createServer
,便获得了 HTTP 服务实例,借助实例监听 3000 端口。
其次Nodejs 的 http
模块实例在监听到浏览器的请求后,可以指定状态行、响应主体以及部分响应主体。创建demo5.js,输入以下代码:
// demo5.js
// 导入 http 系统模块
const http = require('http');
// 创建 http 服务器实例
const app = http.createServer();
// 监听 3000 端口
app.listen(3000);
// 响应浏览器的请求,请求request简写成req,应答response简写成res
app.on('request', function (req, res) {
// 状态行及响应头,告诉浏览器信息的类型和编码
res.writeHead(200, {
'Content-Type': 'text/html; charset=UTF-8'
})
// 响应体
res.write('<h1>你好,世界!</h1>');
//res.write('<h1>你好,中国!</h1>');
// 结束本次响应
res.end();
})
代码测试:
- 在终端运行
>node demo5.js
- 在浏览器中用地址 127.0.0.1:3000(推荐)或 localhost:3000(推荐)或本机IP地址访问运行的HTTP服务器。
注意:凡是修改过 node.js 服务代码后,必须重新启动服务才能生效! 快捷键 crtl+c 停止当前服务。
3.2 响应网站资源
网站是由多种类型的资源构成的,其中包括HTML文件和CSS文件、JS文件、图片、音频、视频等称为静态资源的文件,实际上这些文件都是通过 Node.js 系统模块 fs
读取后以响应体的方式返回给浏览器的,读取网页文件的核心代码如下(读取图像文件的代码类似):
//demo6.js
// 在demo5.js基础上添加导入 fs 系统模块
const fs = require('fs');
// 响应浏览器的请求
app.on('request', function (req, res) {
// 当遇到浏览器请求某文件时,使用 fs 模块进行读取,回调函数获取读取的结果
fs.readFile('./demo2.html', 'utf-8',function (err, data) {
//data为读取的内容
//console.log(data);
// 失败时响应结果
if(err) return res.end('读取失败!');
// 成功时响应结果
res.end(data);
})
return;
})
实际使用中是通过querystring
系统模块解析请求路径来获得各文件存在的路径,通过判断再利用 fs
模块对文件进行读取。
这是底层的服务器代码!!!作为了解Web服务器的运行机制即可。
四、Express
Express 是基于 Nodejs 的专门用于网站开发的服务器框架。使用 Express 框架开发网站会更加高效。
4.1 安装和启动
从技术层面看 Express 是第三方的模块,因此学习使用 Express 的前提是通过 npm
安装 Express 至本地,如下所示:
# 安装 Express
>npm install express
Express 会被下载并保存在 node_modules 目录,接下来我们导入 Express 并借助它创建网站。
//demo7.js
// 1. 导入 Express 模块
const express = require('express');
// 2. 创建服务器实例
const app = express();
// 3. 监听端口
app.listen(3000);
4.2 路由
路由是本质上是请求地址( URL 路径)与程序之间的映射关系,服务端能针对 URL 地址的变化,而编写不同的程序逻辑。如当 URL 地址为 / 时,显示index.html;当 URL 地址为 /login 时,显示 login.html ;当 URL 地址为 /register时,显示 register.html 等。
Express实现了对路由的支持,只需要按照它的语法进行配置即可。 从 Express 中路由的语法层面来看,路由分别由请求方法、路径、回调函数 3 部分组成,代码如下所示:
// demo7.js中继续
// 设计路由
app.get('/', function (req, res) {
//send方法是Express封装的响应方法
res.send('<h1>home</h1>');
})
app.get('/login', function (req, res) {
res.send('<h1>登录</h1>');
})
app.get('/register', function (req, res) {
res.send('<h1>注册</h1>');
})
如上代码的含义是当用户在浏览器地址栏中输入IP地址(域名):端口后的地址为 /
则响应的内容为“首页”;输入的是 /login
则响应的内容为 “登录”;输入的是 /register
则响应的内容为 “注册”。
注意:一个路由中只允许响应一次结果!这是HTTP协议决定的,否则会出现错误。
4.3 静态资源
设计登录页面的路由,响应登录页面 login.html ,代码如下:
// demo7.js 进一步完善
//导入系统模块fs
const fs = require('fs');
app.get('/login', function (req, res) {
fs.readFile('./home/login.html', 'utf-8', function (err, data) {
// 错误处理
if(err) return res.send('<h3>404 Not Found!</h3>');
// 成功读取
res.send(data);
})
})
页面中的静态资源需要进一步响应。
Express 中专门内置了处理静态资源的解决方案,极大的简化了静态资源的处理:
方法一:分别指定,如下代码所示:
//用中间件处理静态资源
app.use(express.static('./home/css'));
app.use(express.static('./home/js'));
app.use(express.static('./home/images'));
......
方法二:推荐,将 home 文件夹中的所有静态资源(css、js、images等文件或文件夹)放在同一个目录 public 中,如下代码所示:
//4.用中间件处理静态资源
app.use(express.static('./home/public'));
// 设计路由
app.get('/login', function (req, res) {
fs.readFile('./home/login.html','utf-8',function (err, data) {
// 错误处理
if(err) return res.send('<h3>404 Not Found!</h3>');
// 成功读取
res.send(data);
})
})
上述代码中 express.static
的参数为实际的静态资源的存放目录。
4.4 模板引擎
模板引擎是创建动态网站的重要组成部分,借助于模板引擎开发者能够非常高效的动态生成 HTML 标签。Express 中并未内置模板引擎,需要开发者自行选择合适的模板引擎。
- xtpl 模板引擎的安装和配置
本次课以阿里的 xtpl
模板引擎做为学习目标。模板引擎也是基于 Nodejs 开发的第三方模块,因此也需要借助 npm
下载,
# 安装模板引擎,同时安装两个模块
$ npm install xtpl xtemplate
安装模板引擎后需要在 Express 中进行配置:
// demo8.js
//1.导入Express
const express = require('express');
//2.创建http服务器
const app = express();
//3.监听3000端口
app.listen(3000);
//4.用中间件处理静态资源
app.use(express.static('./home/public'));
// 5. 配置模板引擎
app.set('view engine', 'xtpl');
// 配置模板目录
app.set('views', './home/');
// 6. 设计路由
app.get('/', function (req, res) {
res.render('login');
})
使用 xtpl 模板引擎后,需要将原本的 .html
文件后缀改为 .xtpl
,并且 xtpl
模板引擎在 Express 中无需要导入即可正常运行!!!
tip: .html 改为 .xtpl 后,VSCode没有语法高亮,点击左下角的“纯文本”按钮,设置xtpl与html关联。
请完善登录路由
/login
和注册页路由/register
响应页面的代码。
- 模板布局
又称为模板继承,模板继承是为了重用模板中的公共内容,作用是将网站的公共部分(如顶部导航、底部等)提取出来,称为母版,其他页面继承这个公共的母版,而不需要重复书写,同时便于后期的代码维护。
例如:公共的母版为 layout.xtpl,其中{ { {block (‘body’)}}}是占位符,为其他页面填充的不同的内容占位。
其他页面,首先用extend命令继承母版,然后将不同的内容写在 { {#block (‘body’)}} 和 { {/block}} 之内,这里的 body 是标识符。
-
home 文件夹下新建 layout.xtpl,粘贴 index.xtpl 内容,将
<div class="body"> </div>
的内容切掉,以{ { {block (‘body’)}}}替换(挖了一个坑,3对花括号)。 -
在 login.xtpl 中,只留
<div class="body"> </div>
的非公共部分内容,其他内容切掉,继承母版 { {extend (’./layout’)}} ,非公共部分内容用 { {#block (‘body’)}} 和 { {/block}} 包围(填坑)。
请使用模板继承修改模板
index.xtpl
和register.xtpl
。
- 模板变量
渲染模板时可以向模板传递变量,即模板可以接收变量,称为模板变量。利用模板变量(数据)生成动态页面。
例如通过定义一个对象数组,动态生成导航栏目项
//demo9.js ,在demo8.js的基础上添加数组
//定义导航栏目数据
let navs = [
{
text: 'home', link: '/' },
{
text: '关于我们', link: '/about' },
{
text: '加入我们', link: '/join' },
]
//设计路由
app.get('/', function (req, res) {
// 第2个参数为对象类型,该对象数据会被传给模板内部
res.render('index', {
navs });
})
注意:前台home文件夹中的模板渲染时都需要传这个对象数据!
在 layout.xtpl 中接收 navs ,通过遍历数组,生成导航项
<!-- 导航 -->
<nav>
{
{
#each (navs)}}
<a href="{
{this.link}}">{
{
this.text}}</a>
{
{
/each}}
</nav>
xtpl模板引擎文档资料地址: https://github.com/xtemplate/xtemplate/blob/master/docs/syntax-cn.md
4.5 中间件
所谓的中间件就是Express在处理请求和响应时自动被执行的函数。也就是说中间件其实只是一个函数,其特殊之处在于在请求和响应过程中自动被执行!!!
- 中间件的定义和挂载
中间件在 Express 中,必须使用 use 方法应用(挂载)后方可生效。每个中间件函数都可以接收 3 个参数,分别为 req
、res
和 next
,其中 req
和 res
分别用于处理请求及响应,next
是一个函数,必须调用该函数才能继续下一个中间件的执行。如下代码所示:
// 在demo9.js中定义两个中间件
// 中间件是一个特殊的函数
function md1(req, res, next) {
console.log('你没看错,我就是中间件!');
// 继续执行下一个中间件
next();
}
function md2(req, res, next) {
console.log('我是下一个中间件!');
next();
}
// 应用(挂载)中间件
app.use(md1);
app.use(md2);
// 设计路由
app.get('/', function (req, res) {
res.render('index', {
navs });
})
Express 的中间件机制具有以下明显特征:
-
所有被挂载的中间件函数内部都可以处理请求和响应
-
中间件的数量没有限制,且要依照挂载的先后顺序依次执行
-
上一个中间件处理请求或响应的结果,会被传递到下一个中间件当中,就像车间流水线上一道道的工序一样!!!
- 中间件的作用
中间件实际上是为 Express 功能的封装提供了一种机制,只能用于封装与请求与响应相关的逻辑。通常我们使用别人封装好的中间件为自己的逻辑服务,很少自己去封装。
当使用了某个中间件后,一般会在 req 或 res 中增加了某个属性或方法,也就是将代码逻辑的封装或处理的结果写到 req 或 res 中,交给下一个中间件使用。例如服务端在接收模板以POST方式提交的数据时,需要用到body-parser
中间件解析数据。
前面用到的处理静态资源的中间件是Express中内置的唯一的一个中间件!!!
app.use(express.static('./public'));
4.5 数据解析
网站开发以 HTTP 协议为基础,浏览器发起请求的同时可以携带一些必要的数据(参数),做为服务器必须要有能力获取这些数据(参数)。这里以登录页面为例,这里需要修改表单的action属性值设置为接收的路由,input控件需要设置name属性。
<form action="http://localhost:3000/send">
<ul>
<li>
<label for="">邮 箱:</label>
<input type="text" name="email">
</li>
<li>
<label for="">密 码:</label>
<input type="password" name="password">
</li>
<li>
<label for=""></label>
<input type="submit" value="登 录">
<a href="javascript:;">忘记密码?</a>
</li>
</ul>
</form>
在 Express 中根据请求方式的不同,获取参数的方法也不一样:
- GET 方式,是表单的默认请求方式,直接通过
res.query
获取get
方式提交的数据。在复制 demo8.js 的代码到 demo10.js ,增加登录页面提交数据给服务器的路由:
// demo10.js , 在 demo8.js 的基础上添加路由
// 用户以 get 方式提交的数据
app.get('/send', function (req, res) {
// 直接通过 req.query 即可获得 get 方式提交的数据
console.log(req.query);
res.send('get方式~');
})
-
POST 方式,即 form 的 method 属性值为 POST 时
<form action="/send" method="POST">
需要借助中间件才可以获得 post
方式提交的数据。body-parser
是专门用于解析 post
方式提交的数据的第三方中间件,挂载该中间件后通过 req.body
获取 post
方式提交的数据。
安装 body-parser
中间件:
>npm install body-parser --save
在服务器实例中挂载中间件,增加如下代码:
// demo10.js ,增加以POSt方式接收数据的路由
// 导入 body-parser 中间件
const bodyParser = require('body-parser');
// 挂载中间件
app.use(bodyParser.urlencoded({
extended: false }))
app.post('/send', function (req, res) {
// 获得 post 方式提交的数据
console.log(req.body);
res.send('post方式~');
})
注意:如果以 post
方式发起请求时,Express 中必须使用 app.post
定义路由,如果浏览器以 get
方式发起请求,则使用 app.get
定义路由。
五、MySQL
MySQL 是一款开源的数据库软件,Node.js 能够支持多种数据库软件,本项目使用MySQL实现数据的持久化。Node.js.通过 mysql
第三方模块实现对 MySQL 数据库的访问及操作。
注:使用 MySQL 的前提是已经安装了 MySQL 服务器软件并启动,通过计算机管理中的服务查看。Node.js 只是对 MySQL 服务器进行访问及操作而已!
准备工作:使用 Navicat 访问 MySQL 数据库:新建MySQL连接,连接名自定义,用户名使用默认,密码123456;在连接下新建数据库blog,在blog上右击执行“运行SQL文件”,浏览到文件blog.sql,点击开始,关闭后在“表”上右击执行“刷新”,新建 users 和 posts 两个表。
5.1 安装
本地安装 mysql
,–save 参数将依赖记录到 package.json 中。
# 安装 mysql
$ npm install mysql --save
5.2 建立连接
mysql 模块内部封装了对MySQL数据库的操作逻辑,公开了其中的一些属性和方法供开发者使用。登录MySQL数据库的过程是:
// db.js
// 导入 mysql 模块
const mysql = require('mysql');
// 建立到 MySQL 服务器的连接
const db = mysql.createConnection({
host: 'localhost',
user: 'root',
password: '123456',
database: 'blog'
})
调用 mysql
模块的方法 createConnection
并传入 MySQL 数据库服务器相关的信息,如用户名、密码、地址、数据库等,这样便创建了 Nodejs 与 MySQL 服务器的连接,为后续 MySQL 的访问操作做好铺垫。
5.3 访问
当 Nodejs 与 MySQL 建立好连接后,便可以通过 SQL
语句实现对 MySQL 数据库的访问及操作。
// 通过 query 方法执行 SQL 语句并返回结果
db.query('SELECT * FROM students', function (err, rows) {
// 查询的结果
console.log(rows);
});
六、数据入库
以注册页面为例
6.1 设计路由
除了加载页面的路由,还需要定义接收数据的路由,同时页面上表单的action属性值可以用“ /路由 ”
app.get('/register', function (req, res) {
res.render('register', {
navs });
})
app.post('/add', function (req, res) {
//console.log(req.body);
//将接收到的数据写入MySQL数据库中
res.send("OK~");
})
<form action="/add" method="POST">
6.2 数据入库
6.3 路径问题
-
基于文件系统的路径
Nodejs 中的路径都是以磁盘中目录为参考依据的,通常使用相对路径。这时的路径与浏览器地址栏无关,因此一切正常。
-
基于网络的路径
以往写静态网页时也是以磁盘目录为参考依据的,也使用相对路径,用本地浏览器查看时,由于浏览器地址栏中的路径与磁盘路径完全一致,因此一切也正常。
但是当用服务器地址进行访问时,会出现静态资源加载不上的现象,原因是这时网页中的路径始终是以浏览器地址栏中的路径做为参考,导致浏览器无法获取静态资源。
- 浏览器地址栏中的路径包含路由,而路由是自由定义的,并不以磁盘中文件的位置关系为参考
- 网页中应该使用绝对路径,如“ http://localhost:3000/css/main.css ",这里可以使用 / ,它代表了“ http://localhost:3000 ”,推荐简写成“ /css/main.css ",express 在找静态资源时自动拼接上了public。
tip:静态资源都放在public文件夹,或将所在目录设置为静态资源目录。
小结:网页中使用网络路径,Node.js的服务器程序中使用文件路径。
6.4 路由中间件
通常一个网站中的路由较多,都设置在一个文件中时导致代码可读性差、不易于管理。
Express中将通过 app.get() 定义的路由称为顶级路由 top-level,同时提供了子路由,子路由继承了顶级路由的功能和特性,但子路由无法独立运行,需要以中间件的形式将其挂载。
// demo12.js,注意端口是3001
const express = require('express');
const app = express();
app.listen(3001);
const router = express.Router();
router.get('/', function (req,res) {
res.send('我是子路由!');
})
app.use(router);
这样,我们就可以将多个路由以子路由的形式拆分写在不同的js文件中,在主程序中进行挂载。即创建子路由可以将具有关联性的路由进行封装,方便网站项目代码的开发管理。
// home.js
const express = require('express');
const router = express.Router();
router.get('/', function (req, res) {
res.send('home');
})
router.get('/login', function (req, res) {
res.send('登录');
})
router.get('/register', function (req, res) {
res.send('注册');
})
module.exports = router;
上述代码封装实现了前台的3个子路由,需要将它挂载到顶级路由之上才能运行,如下代码所示:
// demo13.js
const express = require('express');
const app = express();
app.listen(3001);
const home = require('./home');
app.use(home);
路由中间件是 Express 借助中间件机制对路由进行的二次封装,目的是提供更加便利的网站开发方案。
七、MVC
7.1 MVC概念
MVC(模型-视图-控制器)是广泛使用的一种软件设计模式,M指业务模型(数据),V指用户界面(网页),C指控制器(程序),即接受用户的输入并调用model和view去完成用户的需求。MVC实现的目标是将软件用户界面和业务逻辑分离,以使代码可扩展性、可复用性、可维护性、灵活性加强。
7.2 目录调整
一般情况下,网站前台和后台分别是两个独立网站,分别安装express ,分别用不同的端口,单独去开发,共用一个数据库。
我们这里开发为一个网站,这样做的好处是共用一个端口,用不同的路由区分。
因此,需要调整目录结构,按MVC模式新建 models 、views 、public和routes 文件夹,将原 admin 和 home 下的文件分别调整到 views 和 public 文件夹中,推荐文件夹内部分别存放前、后台文件,方便对其进行分类管理。
这里我们在 views 中分别新建 admin 和 home 文件夹,分别放入前台和后台的HTML文件,将文件后缀全部改为 .xtpl 模板文件; public 文件夹存放静态资源,由于前、后台的静态资源有相同文件夹,就不再分类了。
八、博客项目
简单博客项目实战,巩固 Nodejs 相关技术,提高综合运用能力。
8.1 静态部署
- 安装模块,在终端运行命令
>npm init -y
>npm install express xtpl xtemplate mysql body-parser
- 新建 app.js
const express = require('express');
const app = express();
app.listen(3000);
app.use(express.static('public'));
app.set('view engine', 'xtpl');
app.set('views', './views')
app.get('/', function (req, res) {
res.render('home/index');
})
app.get('/admin', function (req, res) {
res.render('admin/index');
})
app.get('/admin/settings', function (req,res) {
res.render('admin/settings');
})
注意:加载页面时,会出现静态资源加载不了的现象,需要将模块文件中静态资源的路径由相对路径改为以/
开始的绝对路径,即去掉最前面的.
。
8.2 后台公共母版
- 新建母版 layout.xtpl
- 在 views/admin 下新建 layout.xtpl,粘贴 index.xtpl 内容,将
<div class="body index"> </div>
的内容切掉,以{ { {block (‘body’)}}}替换(挖了一个坑)。 - 在 index.xtpl 中,只留
<div class="body index"> </div>
的非公共部分内容,其他内容切掉,继承母版 { {extend (’./layout’)}} ,非公共部分内容用 { {#block (‘body’)}} 和 { {/block}} 包围(填坑)。 - 其它页面以相同方法处理
- 母版优化
layout.xtpl 中最下面的一段JS代码只在首页中才有用,其他页面都不用,这样导致访问其他页面都会加载这段代码,可以再挖一个坑
- 在 layout.xtpl 中,剪切最下面
<script> </script>
的内容,以{ { {block (‘script’)}}}替换(又挖了一个坑)。 - 在 index.xtpl 中,将剪切的代码粘贴在最后,用{ {#block (‘script’)}} 和 { {/block}} 包围(填坑)。
结论:有了公共母版,页面静态资源的路径问题就不用一个页面一个页面的进行修改,可以统一处理了
8.3 路由文件
在 routes 中分别新建 admin.js 和 home.js 文件,分别为后台和前台的子路由文件。
- home.js 设置前台子路由
//home.js
const express = require('express');
const router = express.Router();
router.get('/', function (req, res) {
res.render('home/index');
})
module.exports = router;
- admin.js 设置后台子路由
//admin.js
const express = require('express');
const router = express.Router();
router.get('/', function (req, res) {
res.render('admin/index');
})
router.get('/settings', function (req,res) {
res.render('admin/settings');
})
module.exports = router;
- app.js 中挂载子路由
const express = require('express');
const app = express();
app.listen(3000);
//导入与前后台相关的子路由
const home = require('./routes/home');
const admin = require('./routes/admin');
app.use(express.static('public'));
app.set('view engine', 'xtpl');
app.set('views', './views')
//挂载子路由
app.use(home);
//可以加参数:指定为某一路径开头的路由,才挂载子路由
app.use('/admin',admin);
8.4 数据模型
在 models 中创建数据库操作的 js 文件,原则是以表为单位,将数据库操作拆分为多个模块。分别新建 db.js 、posts.js 和 users.js 文件。
- db.js 连接数据库模块
// 导入 mysql 模块
const mysql = require('mysql');
// 建立连接
const db = mysql.createConnection({
host: 'localhost',
user: 'root',
password: '123456',
database: 'blog'
})
//封装结果导出,成为独立的模块
module.exports = db;
- posts.js 表操作模块,可以先写好模型处理的规范,逻辑在用的时候再补充
//针对posts表进行增删改查操作
const db = require('./db');
module.exports.select = function () {
}
module.exports.insert = function () {
}
module.exports.updata = function () {
}
module.exports.delete = function () {
}
- users.js 表操作模块
//针对users表进行增删改查操作
const db = require('./db');
module.exports.select = function () {
}
module.exports.insert = function () {
}
module.exports.updata = function () {
}
module.exports.delete = function () {
}
8.5 用户登录
登录和注册是后台的功能模块,但是是在前台网站进行的,模板、路由和模型都在前台相关代码中进行设计。
-
登录处理流程
-
登录页面-view(前端)
- HTML:将 index.xtpl 页面静态资源的路径改为绝对路径,并为表单控件定义 id 和 name 属性,name 的值应和数据库中用户表字段一致,以方便与用户表中数据的处理:
<form action="" id="loginForm">
<ul>
<li>
<label for="">邮 箱:</label>
<input name="email" type="text">
</li>
<li>
<label for="">密 码:</label>
<input name="pass" type="password">
</li>
<li>
<label for=""></label>
<input type="submit" value="登 录">
<a href="javascript:;">忘记密码?</a>
</li>
</ul>
</form>
- JS:在 index.xtpl 中添加提交表单的jQuery代码,使用 jQuery 中的 ajax 发起请求,将用户填写的邮箱和密码发送服务端的路由,并根据验证的结果进行下一步的操作,验证成功则跳转到后台首页,失败则弹出警示框:
<script src="/assets/jquery/jquery.min.js"></script>
<script>
//监听表单的提交事件,使用Ajax
$('#loginForm').on('submit',function(){
//获取表单中用户填写的数据
let data = $(this).serialize();
//使用 ajax 将用户登录信息发送到服务端
$.ajax({
url:'/login',
type:'post',
data: data,
success:function(info){
//console.log(info);
// 根据后端的验证结果,进行下一步操作
if(info.code===10000){
return location.href='/admin';
}
alert('登录失败');
}
})
//阻止表单的默认提交,不刷新页面
return false;
})
</script>
- 路由-control(接口)
- 在app.js 中导入 body-parserk 中间件并挂载,用于接收 post 方式提交的数据
// app.js
const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({
extend: false }));
- home.js :添加页面路由和接收数据的路由,将接收的前端数据交给模型进行校验,并将验证结果响应给前端:
// home.js
const express = require('express');
const router = express.Router();
//导入数据库模型
const users = require('../models/users');
//首页
router.get('/', function (req, res) {
res.render('home/index');
})
//登录页面
router.get('/login', function (req, res) {
res.render('home/login');
})
//定义了一个接收数据的新路由(接口)
router.post('/login', function (req, res) {
//console.log(req.body);
let email = req.body.email;
let pass = req.body.pass;
//调用模型
users.auth(email, pass, function (err, row) {
//登录成功
if (!err && row.code === 10000) {
//return res.send(row.msg);
//响应给浏览器的数据格式为 json
return res.json({
code: 10000,
msg:'登录成功!'
});
}
//登录失败
res.send(row.msg);
})
})
module.exports = router;
- 数据模型-model(后端)
- db.js :用户表中存的密码是经过 MD5 加密后的密文,在进行密码校验时,需要将页面提交的密码进行加密,下载 md5 模块,在db.js 中导入,所有模型均可使用:
>npm install md5 --save
//db.js
//导入 md5 模块,用 md5 加密
const md5 = require('md5');
// db 上附加了一个函数md5
db.md5 = md5;
- users.js:执行SQL语句,验证用户表中是否存在与路由调用模型时传递的用户名和密码是否一致,并将校验结果返回给路由:
//针对users表进行增删改查操作
const db = require('./db');
module.exports.auth = function (email, pass, cb) {
//1.查看有无该用户
let sql = 'select email,pass,id,name,avater from users where email=?';
//2.该用户数据库中的密码与用户提交的密码是否一致
db.query(sql, email, function (err, rows) {
//语句执行失败
if (err) return cb(err);
//是否存在用户
if (rows.length > 0 && rows[0].pass === db.md5(pass)) {
return cb(null, {
code:10000, msg: '登录成功!', result:rows[0]});
}
cb(null, {
code:10001, msg: '查无此人!' });
})
}
8.6 session机制
上面我们实现了登录页面,用户正确登录后可进入后台首页,但是由于后台程序并没有记录用户的登录状态,如果使用/admin
仍可以访问后台。
HTTP 是一个无状态的协议,即服务器与客户端之间会话(请求和响应)过程中,服务器不会记录客户端的访问信息,这给网站开发带来了一些不利的影响,如登录状态、用户身份验证都无法实现。
cookie 和 session 是典型的解决用户登录问题的解决方案。
- cookie
cookie 是浏览器存储数据的一种方式,它能够以特定的文本格式存储少量的数据。
// 设置 cookie
<script>
document.cookie = 'name=小明; expires=' + new Date('2020-10-01');
// 读取 cookie
console.log(document.cookie);
</script>
cookie 默认有效时间至关闭浏览器结束会话时,通过 expires
可以指定 cookie 的失效时间,当浏览器发起任何请求时所有已存储的 cookie
都会被以请求头的方式自动发送到服务端。当cookie
的值由 session
来指定时,就可以记录用户的特征信息了。
- session
session 是服务器端存储数据的一种方式,一般情况下 session 配合 cookie 使用,它们两者的关系类似钥匙和锁的关系。
在不同类型的语言中 session 的存储方式并不相同,Express 常使用中间件 express-session
中间件支持 session 的存储。
- 安装
express-session
模块
$ npm install express-session --save
- 挂载中间件
session 是全站都要用到的信息,通常在顶级路由中导入模块并挂载。
// app.js
// 导入 express-session 中间件
const session = require('express-session');
// 挂载中间件
app.use(session({
secret: 'xaiu',
resave: false,
saveUninitialized: true ,
cookie: {
maxAge: 3600 * 1000 * 24 * 7
}
}));
当挂载 express-session
后,通过 req.session
实现对 session
的操作,如在后台子路由中通过 req.session 查看 session 信息:
// routes/admin.js
router.get('/', function (req, res) {
console.log(req.session);
res.render('admin/index');
})
这时控制台输出 session 信息。
注意:默认情况下 express-session
将 session 存储在内存当中,每次重启 Node 服务都会导致 session
丢失。
- 响应头
当挂载 session
中间件后,Node 服务端第一次响应时会将自动设置的响应头 Set-Cookie
发给浏览器 ,指示浏览器在本地设置一个 cookie,等到下次请求时浏览器则自动将这个 cookie
通过请求头 Cookie
发送给服务端。
- 在 session 中写入数据
可以将用户访问时的特征信息写入session 中,使后台程序记录了用户的状态,弥补了 HTTP 无状态的缺陷。
// home.js
//调用模型
users.auth(email, pass, function (err, row) {
//登录成功
if (!err && row.code === 10000) {
//return res.send(row.msg);
//通过session记录用户登录成功的状态
req.session.loginfo = row.result;
//响应给浏览器的数据格式为 json
return res.json({
code: 10000,
msg:'登录成功!'
});
}
//登录失败
res.send(row.msg);
})
- 登录状态判断
通过自定义的中间件可以对 session 中记录的用户登录状态信息进行判断,从而实现对用户的访问控制:
//app.js ,在挂载 session 中间件之后
//只为 /admin 路由单独指定中间件
app.use('/admin',function (req, res, next) {
// 未登录跳转到登录页
if (!req.session.loginfo) {
return res.redirect('./login');
}
next();
})
在用户登录和购物车等应用场景中都需要使用这种机制。
- 全局模板数据
app.locals
中定义的属性值,在任意模板可以直接使用,提供了全局数据支持。
可以将 session 中存储的用户信息定义为全局模板数据,实现在页面动态数据的显示:
- app.js :
//app.js
app.use('/admin',function (req, res, next) {
// 未登录跳转到登录页
if (!req.session.loginfo) {
return res.redirect('./login');
}
//将用户登录信息定义为全局模板数据
app.locals.userinfo = req.session.loginfo;
next();
})
- layout.xtpl :在后台模板中显示用户名和头像
<!-- 个人资料 -->
<div class="profile">
<!-- 头像 -->
<div class="avatar img-circle">
{
{#if (userinfo.avatar)}}
<img src="{
{userinfo.avatar}}">
{
{else}}
<img src="/images/default.png">
{
{/if}}
</div>
<h4>{
{userinfo.name}}</h4>
</div>
8.8 退出登录
判断用户是否登录是依据是否有对应的 session 值,退出登录即废掉 session 值即可。
在后台点击“退出”登录,发出请求,在由 Node 销毁session即可。
- admin下的 layout.xtpl :找到 “退出” 标签位置,将链接地址改为
/logout
:
<li>
<a href="/logout">
<i class="fa fa-sign-out"></i>
退出
</a>
</li>
- 在 home.js 增加路由
/logout
// home.js
//退出登录
router.get('/logout', function (req, res) {
//将用户的登录状态 session 清空即可
req.session.loginfo = null;
//跳转到前台首页
res.redirect('/') ;
})
8.9 前台公共母版
- base.xtpl
-
在 home 下新建 base.xtpl ,粘贴 index.xtpl 的所在内容,将
<div class="body"> </div>
的内容切掉,以{ { {block (‘body’)}}} 标记(第一个坑);由于 login.xtpl 中有 JS代码,在最下面的</body>
前再以{ { {block (‘script’)}}}占位(第二个坑),将 login.xtpl 中引入 jquery 的 script 标记粘贴在前面,引入 jquery 成为公共部分。 -
index.xtpl ,只留
<div class="body"> </div>
的内容,其他内容切掉,继承母版 { {extend (’./base’)}} ,主体内容用 { {#block (‘body’)}} 和 { {/block}} 包围 。 -
login.xtpl ,只留
<div class="body"> </div>
和<script></script>
的内容,其他内容切掉,继承母版 { {extend (’./base’)}} ,主体部分用 { {#block (‘body’)}} 和 { {/block}} 包围 ;JS 部分用{ {#block (‘script’)}} 和 { {/block}} 包围 。 -
register.xtpl ,进行相同处理
-
将 base.xtpl 模板中的链接的地址改为路由地址
<!-- 导航 --> <nav> <a href="/">Home</a> <a href="/about">关于我们</a> <a href="/join">加入我们</a> </nav> <!-- 注册/登录 --> <div class="extra"> <a href="/register">注册</a> <a href="/login">登录</a> </div>
- layout.xtpl
-
嵌套母版:除了注册和登录页面,其他页面都有相同的侧边栏,新建 layout.xtpl 继承自 base.xtpl,在 { {#block (‘body’)}} { {/block}} 之间粘贴 index.xtpl 的主体
<div class="body"> </div>
的内容,将侧边栏中的<section></section>
切掉,以 { { {block (‘main’)}}} 占位 -
index.xtpl,继承母版改为 { {extend (’./layout’)}} ,只留
<section></section>
部分用 { {#block (‘main’)}} 和 { {/block}} 包围 。