一、 Web框架的功能
HTTP协议 – 万维网的基石
curl-s-v 网址
- -s是silent,用于隐藏进度条
- -v 是 verbose,用于打印全部header
- -L 是重定向
- -o nul 是为了隐藏HTML文本,内容太多不方便展示
-
- 开头是注释
- >开头是HTTP请求
- < 开头是HTTP响应
备注
- “>”号开头的部分是请求
- “<”号开头的部分是响应
- 最后是响应体
举例
curl -s -v http://www.baidu,com
请求和响应
请求
分为四部分
- 请求行
- 请求头
- 回车
- 请求体/消息体
备注
- 如果请求体的内容为JSON
- 那么请求头就要有
Content-Type:application/json
- 这一规范可以在MDN查看
如下所示
响应
分为四部分
- 状态行
- 响应头
- 回车
- 响应体/消息体
备注
- 如果响应体的内容为JSON
- 那么响应头就要有
Content-Type:application.json
HTTP的复杂性
- HTTP复杂就复杂在他有很多请求头
- 每个请求头或响应头功能各不相同,我们需要记住
Web框架
功能
- 更方便地处理HTTP请求与响应
- 更方便的连接数据库、Redis
- 更方便的路由
- 其他: HTML模板
理念
- web框架的主流思想是MVC
- Model处理数据相关逻辑
- View处理视图相关逻辑,前后分离之后,view不重要(后端基本不管,交给前端去做)
- Controller负责其他逻辑
架构示意
备注
- 比如爬虫的时候,爬百度或某个网站的搜索结果,这样就是使用你的代码去请求其他的web服务器
- 我们的web服务器可能大部分时间都是在处理HTTP请求
二、Hello Express
处理HTTP请求与响应
最简单的封装
- 将请求封装为
[['get','/xxx'],{请求头},'请求体']
- 将响应封装为
[status,{请求头},'响应体']
Node.js的封装
- 封装在http模块中;额
- 使用
request对象( Incoming Message的实例) 读取请求
- 使用
response对象 (ServerResponse的实例) 设置响应
Express的封装
- 封装级别高一点点,只需理解 Express 的编程模型即可
- 中文文档
express-demo-1
创建项目
- 创建目录,用WebStorm或者VSCode打开
yarn init -y;git init
- 添加
.gitignore
文件 - 提交到git,推送至Github
开始学习express
CRM学习法: Copy – Run – Modify
备注
- 这是一个比较“古老的”Node.js的web框架,Node.js从v5版本已经不使用
--save
将包安装到依赖里
安装express
yarn add express 或者 npm i express
- 上面两个命令二选一,不要混用
创建app.js
- 内容Copy自文档中的Hello World 部分
- Run 一下app.js,命令为
node app.js
- 打开
http://localhost:3000
或者使用curl http://localhost:3000
- 最好 MOdify 几处代码,比如改内容、路径和端口等
如果修改了代码,在命令行中是需要使用Ctrl + C
中断,再重新运行node app.js
,如果嫌麻烦,就使用node -dev app.js
,如果没有安装node -dev
就安装一下,这样修改完代码,就不用每次要重新运行文件了
const express = require('express')
const app = express()
const port = 8000
app.get('/xxx', (req, res) => {
res.send('你好,Express!')
})
app.listen(port, () => {
console.log(`现在监听的端口是 ${port}`)
})
用CRM学到了什么?
app = express()
- 这个app 应该是核心
app.get('/xxx',(req,res)=>{})
用于对GET /xxx
路径的请求做出响应,res.end()
可以响应数据app.listen(3000,fn)
开启端口监听
三、Hello TypeScript
使用TypeScript
准备工作
yarn global add typescript ts-node ts-node-dev
全局安装工具yarn add @types/express
安装类型支持- 新建
app2.ts
将之前app.js中的代码粘贴过来 tsc --init
,tsc是我们安装typescript后自带的一个命令,运行完就会创建一个tsconfig.json
的文件- 修改
tsconfig
的target
(使用ES6) 和nolmplicitAny
(没有隐式的any,意思是不允许默认有any,如果想加自己手动加,用any你就不要用TS) - 将require改为 import (import才是导入导出的规范写法)
备注
express中完全没有TS的类型支持
安装好了类型支持后,我们发现在@types
文件件下就有了express
如果不想一直结束的时候打分号,就要在webstorm中做如下配置,永远使用分号和单引号,然后使用快捷键ctrl + L
格式化一下代码
运行
ts-node-dev app2.ts
(加上-dev
修改完代码,支持自动刷新)
不管是ts-node
还是ts-node-dev
都不能用于生产环境,只适合用于开发环境!!生产环境还是提前要把TS变成JS!!
四、Express的app对象是什么
使用IDE查看类型
- 使用VSCode或WebStorm可以查看app对象的类型(使用TS写代码的好处就是可以通过单击,知道对象继承自哪些类)
- 类型为Express接口
- Express extends Application
- Application extends EventEmitter,IRuter… …(IRuter是最重要的!!)
- 其中IRuter 包含了 get/post/put等方法
- 有了TypeScript,都不用看文档了
看下IRouter下有哪些方法:
从上图可以看出IRouter中有很多我们需要的方法
如果要看源代码,需要去express/application.js下面看
有了TS的类型声明之后,写代码就会有提示,如下图提示第二个参数就是一个RequestHandler,白色高亮部分就是当前参数的类型
如果我们不知道RequestHandler是什么也没关系,我们可以通过单击post找到RequestHandler再单击RequestHandler,去看它的类型
我们发现它是一个函数,有三个参数,分别是req,res,next
我们怎么知道req,res,next它们是什么类型呢,将鼠标悬上去
我们发现,req是一个Request对象
怎么知道req下有哪些方法呢?
我们在webstorm中写入express.request
,单击它去找它继承了哪些类,有哪些方法即可
避免重复创建
目前我们的代码实现了一个app.js和一个app2.ts,以后我们在新建目录的时候,会重复上面的事情,要创建tsconfig,package.json,gitignore和入口文件app.js或者app2.ts
为了避免每次新建项目,都要重复创建
我们不如新建一个仓库,将它放到仓库中,因为这些代码我们之前已经保存在一个仓库了,现在像想把它保存到另外一个仓库(本地一个仓库上传到远程的2个仓库中)
git add .
git commit -m 'xxx'
- github中新建一个仓库
- 复制git开头的路径
git remote add starter git@github.com:keepBlank/express-starter.git
git push starter main
- 修改并提交
README
- 以后如果有什么是每个项目要做的,就添加到这个仓库中来
- 弄好了之后,记得删除掉这个远程仓库(不能同时关联2个远程仓库)
git remote remove starter
这就是一个开源项目了
五、Express脚手架
一键搞定项目目录
express-generator
安装
npm install -g express-generator
如果报错尝试
npm cache clean --force
使用
express -h
查看帮助express --view-ejs .
注意有一个点 (使用ejs后端模板引擎)- 这句话用于创建文件, 点表示当前目录
- 由于它会覆盖文件,所以要重新安装@types/express
CRM学习法
为了防止每次修改代码都要重启服务,修改下package.json,使用node-dev
运行文件
yarn install; yarn start
- 分析app.js,主要API为
app.set
和app.use
- app.set用于改配置,app.use用于使用中间件
- 记得提交可运行代码,防止后面改出问题
六、用TypeScript开发Express引用
改写
- 把app.js代码复制到app2.ts
yarn add @types/node --dev
这样才能使用requireyarn add @types/express --dev
再安装下express的类型声明文件- 你也可以用
import
代替require - 你也可以用
const
代替var - 需
RequestHandler
和ErrorRequestHandler
断言 - 将
bin/www
中的入口改为 app.ts - 修改package.json中的脚本。将
node
改为ts-node
,因为node不认识ts的语法,所以不能使用node dev ./bin/www
,而是改为ts-node-dev ./bin/www
- 最后运行,
yarn start
备注
在webstorm中使用Ctrl + R
匹配大小写,匹配全词,将var
替换为const
为什么TS不知道下面参数的类型呢?我们不是已经下载了类型声明文件了吗
因为use接收不同类型的函数,它既可以接收三个参数的函数又可以使用4个参数的函数,因此它不知道当前的use的类型
我们可以使用as
关键字,这个语法叫断言
目前只有app文件是ts,其他js文件能不能改成ts?
当然可以只要将js的后缀改为ts,消除代码中的警告和错误即可
改成TS并不难,就是得知道类型是什么,方法就是单击对象,跳转到ts类型声明文件中,看它是什么类型,再使用as加上断言即可
疑问
- 为什么ts-node 可以运行JS
- 答:本来就可以啊,只是添加了对TS的支持
- 注意:不要在生产环境这样用
七、app.use 与 Express 编程模型
app.use()
use是使用的意思,它到底使用了什么?
理解app.use
创建新目录 express-demo-2
- 使用express-starter-1
- 尝试使用
req.url
和res.send
- 多次使用会怎样?会报错,为什么?(不能两次以上send)第一个请求完成之后,要使用
next()
,并且响应只能响应一次,即只能写一次response.end()
- 改成
res.write
,还记得流吗
使用浏览器发请求,只能得到一个响应(这是浏览器的问题,加上res.end
就显示正常了)
使用curl
发送请求收到了两个响应
- 为什么不会关闭呢?告诉Node.js我们写完了,加上
res.end()
试试,这样浏览器就不会一直在等服务端了 next
什么时候可以省略?只有最后一个可以省略next()
,第一个第二个不能省,因为省了就走不到下一个去了,最后一个没有下一个,因此可以不用写next()
const express = require('express');
const {request} = require('express');
const app = express();
app.use((request, response, next) => {
console.log(request.url);
response.write('Hi,I am Server Client')
next()
});
app.use((request, response, next) => {
console.log(2);
response.write('Hi,I am Server Client 2')
next()
});
app.use((request, response, next) => {
response.end()
});
app.listen(3000,()=>{
console.log('正在监听3000端口');
});
备注
有点像顺序执行的感觉,第一个执行完,就调用next()
,执行第二个,执行完,再调用next()
,最后执行response.end()
表示数据写完了,结束了
最后一个,虽然没必须要再加next()
,但是习惯上还是加上,也避免webstorm警告报错
express的编程模型
理解并记住下面的图,就明白了Expressd
的核心了
图中的fn就是下图中的函数
八、中间件与路由
fn就是中间件,因为它是被插入到启动和结束中间的物件
除了我们自己写的中间件外,还提供了现成的中间件
上图中,每个app.use()
里面的函数调用返回的结果都是函数
那为什么每个函数都要调用一下再返回新的函数,而不是直接使用新的函数呢?
因为使用函数调用的方式,方便我们传选项,如
app.use(cookieParser({...}));
一般来说,都是给我们一个函数,我们调用一下这个函数,然后这个参数会根据我们传的参数,返回新的函数,这个新的函数就是中间件fn
优点
由于Express拥有上面所说的编程模型,所以它有以下的优点:
模块化
- 这种模型使得每个功能都能通过一个函数实现,每个函数就相当于一个模块
- 然后通过app.use 将这个函数整合起来
- 如果把函数放到文件或发布到npm,它就变成了单独的包或者说模块了,这样也就实现了模块化
比如:我们现在实现一个独立的模块logger:“任何人访问的时候,就打印它的访问路径”
// logger.js
const logger = function (request,response,next){
console.log(request.url);
next()
}
module.exports = logger
// app.js
const logger = require('./logger.js');
app.use(logger);
运行node-dev app.js
那如果现在想在返回的路径前加上其他的前缀呢?
此时,logger就不应该是一个函数了,而是返回这个函数的函数,是它在调用的时候支持传参
// logger.js
const logger = prefix => {
return (request,response,next)=>{
console.log(`${prefix}:${request.url}`);
next()
}
}
module.exports = logger
// app.js
const logger = require('./logger.js');
const fn = logger('dev')
app.use(logger);
运行node-dev app.js
函数fn就是我们返回的新的函数,新的函数会在log的时候,在路径前加上dev
其中
const fn = logger('dev')
app.use(logger);
可以简写成app.use(logger('dev'))
这样logger的功能就完全的独立于代码主逻辑之外
这就是中间件的好处,只要我们把它放在中间,它就自动的去做它自己该做的事,我们无需知道它具体是怎么做到的,我们只需要知道它的功能,使用的时候给它传个参数就可以了
现在我们也能看明白了之前看到的代码是什么意思了
以 express-demo-1举例
app.use(logger('dev'))
logger('dev')
会返回一个函数- 这个函数会在每次请求到达的时候,打印出信息
- 我们根本就不用去了解它是怎么做到的
- 我们也可以很快做出类似的模块
路由
假如现在想获取用户在输入三个不同的路径的时候,得到三个不同的字符串
webstorm的代码模板功能
如果觉得每次都要写下面这段代码
app.use((request,response,next)=>{
})
我们可以借助webstorm中的模板功能,将常用的重复的代码,添加到模板中
在设置中搜索live template
其中$END$
的意思就是光标在这个位置自动的停住
当我们在webstorm中敲app.use
的时候就会自动的补全代码
使用app.use如何实现路由
格式:
app.use((request,response,next)=>{
if(request.path === '/' && request.method === 'get' ){
response.write('home')
}
next()
})
示例代码
app.use((request,response,next)=>{
if(request.path === '/' && request.method === 'get' ){
response.send('根目录')
}
next()
})
app.use((request,response,next)=>{
if(request.path === '/xxx' && request.method === 'get'){
response.send('这是xxx')
}
next()
})
app.use((request,response,next)=>{
if(request.path === '/yyy' && request.method === 'get'){
response.send('这是yyy')
}
next()
})
备注
- url是包括
?xxx
后面的查询字符串的,如果不想要?
后面的查询字符串应该使用path
- 每个app.use()都相当于独立的模块
- Express的模型有一个非常好的好处就是,我们可以把上面三个模块中国的每个模块单独的拎出来,分别放到
index.js
或xxx.js
或yyy.js
中 - 所以与其说Express是一个Web框架,不如说它是一个专注于做中间件的框架,它使得我们可以把所有的功能都以中间件的形式插入到app中
更方便的写法
app.use('/xxx',fn)
,当然路径也可以改为正则app.get('/xxx',fn)
app.post('/xxx',fn)
app.route('/xxx').all(f1).get(f2).post(f3)
- 这些都是API糖
备注
app.route('/xxx').all(f1).get(f2).post(f3)
解释下这句话的意思.all(fn)
的意思是不管请求的方法使用的是get
还是post
都会执行函数fn.all
后面的.get
和.post
它们之间是或的关系,如果是get请求就走get的逻辑,如果是post请求就走post的逻辑,如果都不是就走all的逻辑
九、错误处理
为什么要错误处理?有的时候在中间的时候就想要它停下来,如上图所示,在执行完fn1和fn2后发现它不是一个登录用户,那我们就不能给它展示页面内容了,我们应该提示他去另外一个地方或者说这个用户不是一个管理员我们应该显示一个错误页面,你没有权限
也就是说我们需要在中间中断,不要再执行后面的了
如何让它中断呢?next()就可以做到
app.use((request,response,next)=>{
response.write('1')
next()
})
app.use((request,response,next)=>{
response.write('2')
if(true){
next('Not Login')
}else{
next()
}
})
app.use((request,response,next)=>{
response.write('3')
next()
})
app.use((error,request,response,next)=>{
response.write(error)
response.end()
next()
})
备注
next('Not Login')
中如果传了参数,就表示当前出了错误,就不要走后面的其他的函数了,直接走到错误处理函数- 那么它就会进入带error的四个参数的回调函数中
next()能传参数吗?
- 可以看文档,也可以看Typescript定义
- 推荐后者
在webstorm中通过点击use进行跳转,一层层查找到了next函数中的err,其中的?表示可有可无,类型是any,返回值是空。因此我们可以在error中传字符串next('Not Login')
,字符串也可以当error
即next()接收一个任意类型的错误,返回值为空
next(error)
- 会直接进入
errorHandler
,不执行后面的中间件 errorHandler
的默认实现 见文档
有时候想看对应的中文文档,文档的下方一般有支持的语言。但是一般情况下建议看英文的文档,因为中文的文档有时候翻译的不全或者翻译的不准确
用官方的代码,实现以下错误
app.use((request,response,next)=>{
console.log('1');
next()
})
app.use((request,response,next)=>{
console.log('2');
if(true){
next('Not Login')
}else{
next()
}
})
app.use((request,response,next)=>{
console.log('3');
next()
})
app.use((error,request,response,next)=>{
if (response.headersSent) {
return next(error)
}
response.status(500)
response.send('未登录')
})
如何定义errorHandler
- 还是看文档,文档说一般在最后定义
- app.use((err,req,res,next)=>{}),定义错误中间件的时候,我们只需要在use的时候,多加一个err即可
- 可以定义多个这样的中间件
备注: 错误处理中间件
同普通的中间件一样,只不过它是专门用来处理错误的
多个错误处理程序就像中间件一样,一字排开,逐个执行,先执行第一个,再执行第二个…。这就是为什么错误处理程序中要有next,因为你可以对错误进行多次处理。
注意在使用多个error处理程序时候,要错误放进next中传给下一个错误处理程序
next(error)
,否则后面的错误处理程序就不知道错误是什么
app.use((request,response,next)=>{
console.log('1');
next()
})
app.use((request,response,next)=>{
console.log('2');
if(true){
next('Not Login')
}else{
next()
}
})
app.use((request,response,next)=>{
console.log('3');
next()
})
app.use((error,request,response,next)=>{
console.log(error);
next(error)
})
let count = 0
app.use((error,request,response,next)=>{
count += 1
console.log(`目前有${count}个错误`);
next(error)
})
每次刷新页面发起请求,count + 1
这就是多个错误处理的写法,一定要将错误传给后面next(error)
,否则后面的错误处理不知道,就像800米接力跑一样,这里的error相当于接力棒,我们得把接力棒传给下一位
next(‘route’)
这是一个特殊的参数
- 见源码中的next函数的定义
- 很少用到
- 可以看看文档中的例子
- 主要要学会如何看源码
一般什么时候去看源码?
如果是为了看源码而看源码就是一个非常低效的方法
当我们在遇到某一个具体的问题的时候,就会产生应用场景,这时候如果带着问题去查文档,效果会好很多。遇到问题就去翻源码,长此以往,源码就渐渐熟悉了
查源码的方法就是,一个个去找,使用ctrl + f
模糊搜索关键字,查找自己想查找的方法,看源代码是如何定义的
文档中是这么说的