关于 Node.js Web应用开发
文章目录
Node.js Web 框架演进
《狼书(卷2):Node.js Web应用开发》 第一章
Node.js http模块
最早的Connect
// 使用connect框架
var connect = require('connect')
var http = require('http')
var app = connect()
// 将开发中的可变部分抽象出来,通过app.use函数挂载到app对象上
// 中间件有顺序
// 中间价可分类:全局/局部
app.use('/', function foo(req, res, next){
res.end('Hello from connect3!\n');
})
app.use('/2', function foo(req, res, next){
res.end('Hello form connect2!\n');
})
// app.use(function(req, res){
// res.end('Hello from connect!\n');
// })
http.createServer(app).listen(3011)
// 打开localhost:3011
曾经扛鼎的Express
内置了路由,将路由方法和中间件结合在一起,最终绑定到了app对象
var express = require('express');
var app = express();
var http = require('http');
app.get('/', function(req, res){
res.send('hello world');
})
app.get('/2', function(req, res){
res.send('hello world2');
})
// 通过内置模块启用(简单易用,但定制功能上略差)
// app.listen(4001)
// 使用http模块的http.createServer(app):可以完成更多定制功能
var server = http.createServer(app);
server.listen(4002)
新兴的Koa
Koa和Express中间件写法不一样之处在于http.creatteServer()函数的参数不同,其他的部分几乎一模一样
路由中间件
- koa-generator koa-router
- koa-trie-router
const http = require('http')
const Koa = require('koa')
const app = new Koa()
// 响应所有请求
app.use(async (ctx, next) => {
// ctx.body = 'hello koa 2';
// 多URL处理
if (ctx.path == '/') {
ctx.body = 'hello koa'
}
else if (ctx.path == '/2') {
ctx.body = 'hello koa2'
}
else{
ctx.body = 'hello koa else'
}
});
app.listen(3005)
// 多URL不适用
// const server = http.createServer(app.callback())
// server.listen(3000)
const Koa = require('koa')
const Router = require('koa-trie-router')
let app = new Koa()
let router = new Router()
router
.use(function (ctx, next) {
console.log('* requests')
return next()
})
.get(function (ctx, next) {
console.log('GET requests')
return next()
})
.put('/foo', function (ctx) {
ctx.body = 'PUT /foo requests'
})
.put('/bar', function (ctx) {
ctx.body = 'POST /bar requests'
})
app.use(router.middleware())
app.listen(3333)
中间件的对比
中间件是Web框架的核心,无论是Express还是Koa的学习过程中,都应该重视它。写法和特性是相辅相成的,尤其是应用中常见的各种模块,绝大多数都是对中间件的封装。从http模块到中间件,这是一个非常好的演进路线。同样,从Express中间件进化到Koa中间价,也是非常重要的变革。
// Express中间件的经典写法
function (req, res, next) {
res.send('hello world')
}
// Koa中间件写法
async function (ctx, next) {
const start = new Date();
await nect();
const ms = new Date - start;
ctx.set('X-Response-Time', '$(ms)ms');
}
Express和Koa大比拼
Express的哲学是为HTTP服务器提供小的、健壮的工具,使之成为开箱即用的Web框架
Koa只提供中间件内核,不绑定任何中间件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3CefB6SO-1646576971871)(C:\Users\BZL\AppData\Roaming\Typora\typora-user-images\image-20210719144247522.png)]
const bros = require('./build/Release/broscpp.node')
const Koa = require('koa')
const Router = require("koa-router")
const os = require('os')
const port = 8088
let app = new Koa()
let router = new Router()
// 获取IP地址
function getIPv4() {
//同一接口可能有不止一个IP4v地址,所以用数组存
let ipv4s = [];
//获取网络接口列表对象
let interfaces = os.networkInterfaces();
Object.keys(interfaces).forEach(function(key) {
interfaces[key].forEach(function(item) {
//跳过IPv6 和 '127.0.0.1'
if ('IPv4' !== item.family || item.internal !== false) return;
//可用的ipv4s加入数组
ipv4s.push(item.address);
})
})
return ipv4s[0]; //返回一个可用的即可
}
let host = getIPv4();
router.get('/msg_get', async (ctx, next) => {
console.log("url value: " + ctx.request.querystring);
var querys_tring = ctx.request.query.msg;
bros.bros_talker(querys_tring);
ctx.body = { querys_tring };
});
app.use(router.routes())
.use(router.allowedMethods());
// app.listen(8088)
app.listen(port, () => {
console.log("server started. listen at: ", host, port);
})
Koa 的使用
Context
Koa将Node.js的Request和Response对象封装到Context对象中,所以称为一次对话的上下文,通过加工Context对象,就可以控制返回给用户的内容
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RIUvIQMY-1646576971872)(C:\Users\BZL\AppData\Roaming\Typora\typora-user-images\image-20210719174224060.png)]
Context对象内置了一些常用属性:
- context.state
- context.app
- context.cookies
- context.throw
常用属性和方法
ctx.request
-
ctx.request是Koa的Request对象,Koa的Request对象是在Node.js的请求对象之上的抽象,提供了很多对HTTP服务器开发有用的功能
-
ctx.request.url //获取请求URL
-
ctx.request.query //获取解析的查询字符串
-
ctx.request.querystring //获取原始查询字符串
-
主要功能是获取GET请求中的参数
-
POST请求的参数获取方式和GET请求不同。Koa没有封装获取POST请求参数的方法,因此需要解析Context中的原生Node.js请求对象req
- 可以通过koa-bodyparser等中间件来获取POST请求的参数
-
const Koa = require('koa'); const app = new Koa(); app.use(async (ctx) => { let postdata = ''; ctx.req.on('data', (data) => { postdata += data; }); ctx.req.on('end', () => { console.log(postdata); }); }); app.listen(3000); // curl 命令可以模拟POST请求 // curl -d "param1=value¶m2=value2" http://localhost:3000/
// 处理路由 const Koa = require('koa'); const app = new Koa(); app.use(async (ctx) => { if (ctx.request.method === 'POST'){ }else if (ctx.request.method === 'GET'){ if (ctx.request.path !== '/'){ ctx.response.type = 'html'; ctx.response.body = '<a href="/">Go To Index</a>' }else{ ctx.response.body = 'hello world'; } } }); app.listen(3000);
ctx.response
-
ctx.response.body // 用于设置返回给用户的响应主题
-
ctx.response.status // 设置请求状态,如200、404、500等
-
ctx.response.type // 设置响应的Content-Type(HTML、图片…)
-
const Koa = require('koa'); const app = new Koa(); app.use(async (ctx) => { ctx.response.status = 200; if(ctx.response.accepts('json')){ ctx.response.type = 'json'; ctx.response.body = {data: 'hello world'}; }else if(ctx.response.accepts('html')){ ctx.response.type = 'html'; ctx.response.body = '<p>hello world</p>'; }else{ ctx.response.type = 'text'; ctx.response.body = 'hello world'; } }); app.listen(3000); // ctx.response.is(types...) // ctx.response.redirect(url, [alt]),这个方法用于将状态码302重定向到URL,例如用户登录后自动重定向到网站的首页
ctx.state
- ctx.state是推荐的命名空间,用于通过中间件传递信息和前端视图。类似koa-views这些渲染View层的中间件也会默认把ctx.state里面的属性作为View的上下文传入
ctx.state.user = yield User.find(id);
,该代码是把user属性存放到ctx.state对象里,以便能够被另一个中间件读取
ctx.cookies
-
ctx.cookies用于获取和设置Cookie
-
ctx.cookies.get(name, [options]); ctx.cookies.set(name, value, [options]);
ctx.throw
-
ctx.throw用于抛出错误,把错误信息返回给用户
-
app.use(async (ctx) => { ctx.throw(500); })
把打印日志的部分单独抽象成一个logger函数
const logger = async function(ctx, next){
console.log(ctx.method, ctx.host + ctx.url)
// next() 会返回一个Promise对象,当前中间件位于next(0之后的代码会暂停执行,直到最后一个中间件执行完毕后,再自下而上一次执行每个中间件中next()之后的代码)
await next();
}
app.use(logger)
app.use(async function (ctx, next){
ctx.body = 'hello world'
})
如果想将多个中间件组合成一个单一的中间件,便于重用或导出,可以使用koa-compose,如下:
const compose = require('koa-compose');
async function middleware1(ctx, next){
// do sth.
await next();
};
async function middleware2(ctx, next){
// do sth.
await next();
};
async function middleware3(ctx, next){
// do sth.
await next();
};
const all = compose([middleware1, middleware2, middleware3]);
app.use(all);
几个常用的中间件
koa-bodyparser
- 该中间件可以把POST请求的参数解析到ctx.request.body中
const Koa = require('koa')
const app = new Koa()
const bodyParser = require('koa-bodyparser')
// 加入了html
app.use(bodyParser())
app.use(async (ctx) => {
if (ctx.url === '/' && ctx.method === 'GET') {
ctx.type = 'html';
let html = `
<h1>登录</h1>
<form method="POST" action="/">
<p>用户名</p>
<input name="userName" /><br/>
<p>密码</p>
<input name="password" type="password /><br/>
<button type="submit">submit</button>
</form>`
ctx.body = html;
}else if (ctx.url === '/' && ctx.method === 'POST'){
let postData = ctx.request.body
ctx.body = postData
}
})
app.listen(3000)
koa-router
- 简化了路由的写法
- 用该中间件改造上面的程序
const Koa = require('koa')
const app = new Koa()
const bodyParser = require('koa-bodyparser')
const Router = require('koa-router')
const router = new Router()
router.get('/', (ctx, next) => {
// 绘制登录页,省略
})
router.post('/', (ctx, next) => {
// 解析formData,省略
})
app.use(bodyParser())
.use(router.routes()) // 加载koa-router中间件
.use(router.allowedNethods()) // 对异常状态码的处理
app.listen(3000)
koa-static和koa-view
- 实际开发中,把HTML模板写在单独的文件中,引用单独的CSS样式及JavaScript文件
- koa-static是专门用于加载静态资源的中间件,通过它可以为页面请求加载CSS、JavaScript等静态资源
- koa-view用于加载HTML模板文件
const Koa = require('koa')
const views = require('koa-views')
const path = require('path')
const bodyParser = require('koa-bodyparser')
const static = require('koa-static')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()
app.use(views(__dirname + '/views'), { // 加载模板引擎
map: {html: 'ejs'}
})
app.use(static( // 加载静态资源
path.join(__dirname, '/static')
))
app.get('/', async(ctx, next) =>
await ctx.render('index') // 渲染模板
})
app.post('/', (ctx, next) => {
let postData = ctx.request.body
ctx.body = postData
})
app.use(bodyParser())
.use(router.routes())
.use(router.allowedMethods())
app.listen(3000)
REST(表现层状态转移)设计一般符合以下条件
- 程序或应用的事物都应该抽象为资源
- 每个资源对应唯一的URI
- 使用统一的接口对资源进行操作
- 对资源的各种操作不会改变资源标识
- 所有操作都是无状态的
如果一条路由请求在all方法和其它方法中同时命中,只有执行了await next(),这条路由才会在all方法和其它方法中都起作用
-
在项目中,all()方法一般用来设置请求头,如设置过期时间、CORS、跨域资源共享等
-
HTTP状态码主要包括1xx(消息)、2xx(成功)、3xx(重定向)、4xx(请求错误)、5xx和6xx(服务器错误)等6种不同类型。表4.1列举了常用的HTTP状态码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-paS8SV1W-1646576971873)(C:\Users\BZL\AppData\Roaming\Typora\typora-user-images\image-20210720112300734.png)]
- MVC模式在概念上强调Model、View和Controller的分离,模块间也遵循着由Controller进行消息处理、Model进行数据源处理、View进行数据显示的职责分离原则。因此,在实现上,MVC模式(如图5.1所示)的Framework通常会将MVC的3个部分分离实现。
Web应用开发
必然涉及两个方向
- 前端界面
- 后端服务
- 需要后端服务提供数据信息给前端界面进行展示
- 前端发起HTTP请求,以Ajax的方式调用后端提供的服务接口,后端接口接收到请求之后,进行相应的逻辑处理,并返回对应的数据给前端,然后由前端进行动态的HTML片段替换
模板引擎
-
模板引擎是Web应用中用来生成动态HTML的工具,负责将数据模型与HTML模板结合(模板渲染),生成最终的HTML
-
编写HTML模板的语法被称为模板语法,模板语法的表达能力和可扩展性决定了模板引擎的易用性
-
模板引擎可以让程序实现界面与数据、业务代码与逻辑代码的分离,大大提升了开发效率,良好的设计也使代码重用变得更加容易
- 置换型
- 实现简单,但效率低下
- 解释型
- 编译型
- 置换型
-
Nunjucks
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r1GMXlyq-1646576971874)(C:\Users\BZL\AppData\Roaming\Typora\typora-user-images\image-20210720171138548.png)]
ctx.set("Content-Type", "application/json")
ctx.body = JSON.stringify(json)
// 提取为中间件
module.exports = () => {
function render(json) {
this.set("Content-Type", "application/json");
this.body = JSON.stringify(json);
}
return async (ctx, next) => {
// send 函数的作用是将需要处理的json对象转化为字符串
ctx.send = render.bind(ctx);
await next();
}
}
为了是项目便于维护,建议将所有的中间件独立出来进行管理
通过应用MVC模式结合Koa框架来开发Web应用,使项目的层次更加清晰;业务之间相互解耦,易于扩展,能够提高团队的开发效率。
需要安装的插件/中间件
#sudo npm install koa-router -g
sudo npm install node-addon-api -g
sudo npm i koa-router -g
sudo npm i koa-compose -g
sudo npm i koa-bodyparser -g
sudo npm i koa-static -g
sudo npm i koa-views -g
# 监测项目中的所有文件,一旦发现文件改动,自动重启
# 启动 nodemon app.js
sudo npm i nodemon -g
sudo npm i koa-nunjucks-2 -g
sudo npm i koa-multer -g
如何查看端口占用情况
lsof -i:[端口]
bootstrap
Bootstrap,来自 Twitter,是目前最受欢迎的前端框架。Bootstrap 是基于 HTML、CSS、JAVASCRIPT 的,它简洁灵活,使得 Web 开发更加快捷。
布局参考:https://v3.bootcss.com/examples/dashboard/
JavaScript
- 声明一个对象,当做命名空间来使用
- 定义一个公共函数来获取指定id元素,减少代码量,提高代码复用率
- 给元素设置 id
- 正则是公认的难学但非常有用的工具
// 声明一个对象,当做命名空间来使用
var eg = {};
// 定义一个公共函数来获取指定id元素,减少代码量,提高代码复用率
eg.$ = function(id){
return document.getElementById(id);
}
// 调用
eg.regCheck = function(){
var uid = eg.$("userid");
var upwd = eg.$("userpwd");
}
先写好html结构基础,再配合CSS做出大致效果,最后用JavaScript加上各种动作
将JavaScript引用放在元素中的页面内容后面
对于getElementById( )、getElementsByClassName( )和getElementsByTagName( )这3个方法来说,只有getElementsByTagName( )这一个方法能够操作动态DOM
function showLog(str) {
// class id
if (!log) log = $("#log");
// 推数据
log.append("<li class='"+className+"'>"+str+"</li>");
if(log.children("li").length > 8) {
log.get(0).removeChild(log.children("li")[0]);
}
}
DOM
想要创建一个元素,需要以下4步:
① 创建元素节点:createElement()。
② 创建文本节点:createTextNode()。
③ 把文本节点插入元素节点:appendChild()。
④ 把组装好的元素插入到已有元素中:appendChild()。
插入元素 将创建的元素插入到HTML
JavaScript
中,插入元素,有两种方法:
-
appendChild()
-
// 语法: 插入到末尾 A.appendChild(B); // 为按钮添加点击事件 var oBtn = document.getElementById("btn"); oBtn.onclick = function() { // content }
-
-
insertBefore()
-
// 语法: 在ref之前插入B // A表示父元素,B表示新子元素,ref表示指定子元素 A.insertBefore(B, ref);
-
删除元素
A.emoveChild(B);
复制元素
// obj: 被复制的元素
// 参数为true:表示复制元素本身及复制该元素下的所有子元素
// 参数为false: 表示不复制子元素
obj.cloneNode(bool);
替换元素
A.replaceChild(new, old);
获取HTML属性值
对于HTML元素的属性值的获取,一般可以通过属性名来找到该属性对应的值(对象属性)
// 语法
// obj 是元素名,它是一个DOM对象(即使用getElementById( )、getElementsByTagName( )等方法获取的元素节点)
// attr是属性名,对于一个对象来说,可以通过点运算符(.)来获取它的属性值
obj.attr
对象方法
- getAttribute()
- setAttribute()
- removeAttribute()
- hasAttribute()
// 语法
obj.getAttribute("attr");
obj.setAttribute("attr", "值");
获取CSS属性值
在JavaScript
中,可以使用getComputedStyle()
方法来获取一个CSS属性的取值
// 语法
// attr表示CSS属性名,这里的属性名使用的是“小驼峰型”
getComputedStyle(obj).attr
设置CSS属性值
// 语法
obj.style.attr="值"
// 对于文本框
obj.style["attr"]
使用cssText属性来同时设置多个CSS属性
// 语法
obj.style.cssText="值"
innerHTML和innerText
事件
在JavaScript
中,一个事件包含3部分:
- 事件主角:是按钮?还是div元素?还是其他?
- 事件类型:是单击?还是移动?还是其他?
- 事件过程:这个事件都发生了些什么?
JavaScript
常见的事件有五种
事件操作是JavaScript
的核心
- 鼠标事件
- 键盘事件
- 表单事件
- 编辑事件
- 页面事件
鼠标事件
- onclick 鼠标单击事件
- onmouseover 鼠标移入事件
- onmouseout 鼠标移出事件
- onmousedown 鼠标按下事件
- onmouseup 鼠标松开事件
- onmousemove 鼠标移动事件
键盘事件
- onKeydown 键盘按下
- onKeyup 键盘松开
表单事件
- onfoucus和onblur 获取焦点/失去焦点触发的事件
- onselect
- onchange
- onsubmit
编辑事件
- oncopy
- onselectstart
- oncontextmenu
页面事件
- onload 表示文档加载完成后再执行的一个事件
- onbeforeunload
事件监听器
在JavaScript
中,如果要给元素添加一个事件,可采用以下两种方式
- 事件处理器
- 事件监听器
事件监听器
指的是使用addEventListener()
方法为一个元素添加事件,又称为“绑定事件”
// 语法
// obj是一个DOM对象
// type是一个字符串,指的是事件类型(如单击事件用click,这个事件类型不需要加“on”)
// fn是一个函数名或匿名函数
// false表示事件冒泡阶段调用
obj.addEventListener(type, fn, false)
// 使用事件监听器为同一个元素添加多个相同的事件
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8" />
<script>
window.onload = function () {
var oBtn = document.getElementById("btn");
oBtn.addEventListener("click", function () {
alert("第1次");
}, false);
oBtn.addEventListener("click", function () {
alert("第2次");
}, false);
oBtn.addEventListener("click", function () {
alert("第3次");
}, false);
}
</script>
</head>
<body>
<input id="btn" type="button" value="按钮"/>
</body>
</html>
解绑事件
在JavaScript
在,可以用removeEventListener()
方法为元素解绑某个事件,解绑事件与绑定事件是功能相反的操作
// 语法
// obj是一个DOM对象
// type是一个字符串,指的是事件类型(如单击事件用click,这个事件类型不需要加“on”)
// fn 必须是一个函数名,而不是一个函数
// false表示事件冒泡阶段调用
obj.addEventListener(type, fn, false)
event对象
当一个事件发生的时候,这个事件有关的详细信息都会临时保存到一个指定的地方,这个地方就是event对象。每一个事件,都有一个对应的event对象
event对象常用属性
属性 | 说明 |
---|---|
type | 事件类型 |
keyCode | 键码值 |
shiftKey | 是否按下 shift键 |
CtrlKey | 是否按下ctrl键 |
AltKey | 是否按下alt键 |
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8" />
<script>
window.onload = function () {
document.onkeydown = function (e) {
if (e.shiftKey || e.altKey || e.ctrlKey) {
alert("禁止使用shift、alt、ctrl键!")
}
}
}
</script>
</head>
<body>
<div>绿叶,给你初恋般的感觉~</div>
</body>
</html>
this
在事件操作中,可以这样理解:哪个DOM对象(元素节点)调用了this所在的函数,那么this指向的就是哪个DOM对象。
对话框
alert
一般只用于提示文字
confirm
不仅提示文字,还提供确认(返回true和false)
prompt
不仅会提示文字,还会返回一个字符串
定时器
setTimeout和clearTimeout
使用前者来“一次性”的调用函数,使用后者来取消执行前者
// code 可以是一段代码,可以是一个函数,也可以是一个函数名
setTimeout(code, time);
setInterval和clearInterval
重复性调用
// code 可以是一段代码,可以是一个函数,也可以是一个函数名
setInterval(code, time);