静态服务器
HTTP 模块
它是Node.js借助了libuv、httParser等一些C/C++ 语言的库,才得以实现,最终暴露给我们一些非常好用的API
配置Webstorm
一些有用的工具
esno
- 是一个 CLI 命令(替代
node
),用于在包类型中无缝运行 TypeScript 和commonjs
ESMmodule
。
它由esbuild提供支持,因此速度非常快。
node-dev
- 当文件更新时自动重启的node(服务器默认是24h运行除非ctrl + c 中断,重新运行程序)
- 避免每次改完代码都要重新运行的麻烦
- 不宜在生产环境使用
ts-node
- 让node支持直接运行TypeScript代码
- 不宜在生产环境使用
ts-node-dev( 推荐 )
- 这个工具结合了上面两个工具
- 可以用TypeScript开发Node.js程序,且会自动重启
- 不宜在生产环境使用,但非常适合用来学习
安装ts-node-dev: yarn global add ts-node-dev
一些有用的工具2
WebStorm自动格式化
- Reformat Code 设置为Ctrl + L 或者其他键位
- 可以快速帮我们格式化 TS / JS 代码
WebStorm自动写代码
- Complete Current Statement:(自动补全剩余部分逻辑代码和格式化代码) 设为 Ctrl + Enter
- Show Context Actions(自动声明变量,自动引入包等) 设为 Alt + Enter
- 把冲突的键位改为其他快捷键
- 当然你可以设为其他键位
一些有用的工具3
WebStorm Git配置
- 把WebStorm的git路径设置为Cmder里的
- 示例:D:\user\Software\Cmder\vendor\git-for-windows\bin\git.exe
- 好处是统一管理统一配置
下图是我使用的 git bash
一些有用的工具4
curl
- GET请求:
curl -v url
- POST请求:
curl -v -d "name = user" url
- 设置请求头:
-H 'Content-Type: application/json'
- 设置动词:
-X PUT
- JSON请求:
curl -d '{"name": "ccc"}' -H 'Content-Type: application/json' url
- 后续会用到curl来构造请求
创建项目
步骤
yarn int -y
- 新建index.ts
- 使用命令行或者webstorm启动(
ts-node-dev index.ts
,在这之前你要安装tsyarn gloval add typescript
,然后tsc -init
初始化出一个tsconfig.json)
这样ts文件就可以使用node运行了
- 如果我们在ts文件中直接引入node的模块,会发现一个问题:它找不到node中的模块,我们需要安装一个依赖:
yarn add --dev @types/node 安装node所有模块的声明文件
当我们安装完成之后就会发现多了很多Node模块的ts文件
这样我们就可以在webstorm中开发Node.js的typescript的应用了
- 引入http模块(webstorm自动引入,快捷键 Alt + Enter)
- 用http创建server(webstorm自动命名,快捷键 Alt + Enter)
- 监听server的request事件(可以简写)
- server.listen(8888)开始监听8888端口
- 启动server的方式:
ts-node-dev index.ts
这样它就会一直监听本机的8888端口 - 使用curl -v http://localhost:8888 发请求
import * as http from "http";
const server = http.createServer();
// 监听server的request,如果服务器被人请求了
server.on('request', () => {
console.log('有人请求了')
});
// 监听端口
server.listen(8888);
但是,请求端在等服务端的响应,但是服务端什么都没响应,因此服务端在等新的请求和请求端都在等服务端的响应,就这样进入死循环了...
ctrl + c 断开请求端和响应端
import * as http from "http";
const server = http.createServer();
// 监听server的request,如果服务器被人请求了
server.on('request', (request,response) => {
console.log('有人请求了')
response.end('服务端相应内容')
});
// 监听端口
server.listen(8888);
这样请求端得到了服务端的响应内容
服务端也得到了三次请求
requst对象
server是个什么东西
http.Server类的实例
- 根据文档知道http.createServer的返回值的类型
- 所以server是http.Server类的实例
发现有我们经常使用的API
- 因此server拥有几个事件和方法
- 其中也就request事件和listen()方法目前能用上
继承net.Server类
-
根据文档知道http.Server继承了net.Server
-
因此server就拥有了几个事件和方法
-
其中也就error事件和address()方法目前能用上
-
这就是看文档的方法(看它有哪些属性、事件、方法)
能否根据请求的了路径不同,响应不同的内容呢?
我们得知道用户请求了什么路径
server.on('request', (request,response) => {
console.log('有人请求了')
response.end('服务端相应内容')
});
从名字上可以看出request
是请求相关的信息,response
是响应相关的信息,但是这样猜是没有根据的,凭什么说request
有这些东西
因此我们得知道request是由谁构造出来的对象,console.log(request.constructor)
我们可以看出:request
的构造者是IncomingMessage
,我们知道了构造它的类是不是就知道它有哪些属性?
我们可以通过文档去查这个类,或者控制台打印出这个类,或者单击进入
为什么明明request下没有那么多属性或方法,我们在webstorm中,通过request.
却不提示它拥有的属性或方法呢?
因为ts并不知道它是什么类型的对象,我们得告诉ts,这个request是IncomingMessage
对象
下面介绍第一个ts的语法: 加类型标注
server.on('request', (request:IncomingMessage,response) => {
...
});
这样在webstorm中通过request.
就可以展示所有request的属性方法,就算不看文档也知道怎么使用了
(webstorm小技巧:request.httpVersion.log
按下tab键就可以控制台打印)
这样我们就可以从request
中获取到请求路径了
console.log(request.httpVersion);
console.log(request.url);
那么同样的道理,response 是什么呢? 通过console.log(response.constructor);
把它的constructor
打印出来
也就是说response
的类型就是ServerResponse
同样的我们使用ts给response
加类型标注
server.on('request', (request:IncomingMessage,response:ServerResponse) => {
...
});
当我们添加标注后,webstorm就智能的帮我们引入了ServerResponse
模块,写代码太舒服了
通过response.
就可以获得所有response的属性方法了
至此,我们就知道了request
是由IncomingMessage
类构造出来的,response
是由ServerResponse
类构造出来的
用Node.js获取请求内容
get请求
request.method
获取请求动词request.url
获取请求路径(含查询参数)request.header
获取请求头- get请求一般没消息体/请求体
post请求
curl-v -d "name=user"
http://localhost:8888request.on('data',fn)
获取消息体request.on('end',fn)
拼接消息体
如果我们在请求的时候,加上-v
参数,请求完成后我们会得到请求内容
curl -v http://localhost:8888/xxx
其中:
- Headers有三个部分: Host、User-Agent、Accept,大于号
>
表示请求 - 小于号
<
表示响应
服务器中获取到了请求的内容
server.on('request', (request:IncomingMessage,response:ServerResponse) => {
console.log('request.method');
console.log(request.method);
console.log('request.url');
console.log(request.url);
console.log('request.headers');
console.log(request.headers);
response.end('服务端相应内容')
});
那么如果是一个post请求呢?
当curl 命令后加 -d
后就会自动的将get请求转成post请求
请求端: curl -v -d "name=LG" http://localhost:8888/xxx
服务端: 响应内容
但是我们拿不到post请求的请求体啊(name=LG
怎么没有获取到?)
Node.js如何获取到POST请求的请求体 / 消息体?
我们需要监听request的data,怎么知道要监听request的data呢?
看下文档 v12,知道post
的constructor是IncomingMessage
,而IncomingMessage
又继承自stream.readable,它是有data事件的,因此post上是有data事件的
那么这个data事件是什么意思呢?
当用户上传内容的时候,每上传一个字节或者一段内容就会触发data事件
假设用户要上传2M的内容,那么它是分几次上传?如果是一次,那么只要中间出了什么差池,那么2M的内容就全毁了
如果了解tcp协议就会知道: 每次上传的报文的大小是固定的,可能是几k的大小,这样就慢慢的上传。所以就会不停的触发data事件,因此我们要监听每一次的data事件,把每次data返回结果放到一个数组中
chunk
的意思就是一小块数据的意思
那么什么时候才结束呢?因此我们要监听上传结束事件,文档中有说明
当上传结束的时候就要把array中的数据链接起来
request.on('end', () => {
const body = Buffer.concat(array).toString();
console.log('body');
console.log(body)
response.end('服务端相应内容')
})
请求端:
响应端:
这种上传方式,就算上传1G的内容也能拿到,因为服务器在监听一小段一小段的data放到数组中然后把数组连起来,只要服务器内存够,就可以处理1G的文件
这就是Node.js获取post请求中请求体的过程:
import * as http from "http";
import {IncomingMessage, ServerResponse} from "http";
const server = http.createServer();
// 监听server的request,如果服务器被人请求了
server.on('request', (request: IncomingMessage, response: ServerResponse) => {
console.log(request.method);
console.log(request.url);
console.log(request.headers);
let array:any = []
request.on('data', (chunk) => {
array.push(chunk);
})
request.on('end', () => {
const body = Buffer.concat(array).toString();
console.log('body');
console.log(body)
response.end('服务端相应内容')
})
});
// 监听端口
server.listen(8888);
(request,response) 是啥
找类
Request
- 拥有headers、methods、url等属性
- 从stream.Readable类继承了 data/end/error事件
- 为什么不能直接拿到post请求的请求体,而是要获取一段一段的buffer,再去拼接呢? 跟TCP有关
Response
- 拥有getHeader/ setHeader / end / write等方法
- 拥有statusCode属性,可读可写
- 继承了Stream,目前用不上
response对象
修改状态码
目前,请求端发起请求后,response只能响应简单的'hi'
实际上我们还可以改很多东西,如:改掉200 / ok ,改为400
response.statusCode = 400;
当然我们也可以在浏览器中发起请求
import * as http from "http";
import {IncomingMessage, ServerResponse} from "http";
const server = http.createServer();
// 监听server的request,如果服务器被人请求了
server.on('request', (request: IncomingMessage, response: ServerResponse) => {
console.log(request.method);
console.log(request.url);
console.log(request.headers);
let array: any = []
request.on('data', (chunk) => {
array.push(chunk);
})
request.on('end', () => {
const body = Buffer.concat(array).toString();
console.log('body');
console.log(body)
response.statusCode = 400;
response.end('hi')
})
});
// 监听端口
server.listen(8888);
我们也可以把响应的code改为404,怎么才能像平时访问的404网页那样,"您访问的网页找不到了.."
那我们response.end('hi')
的时候就不要给'hi'
内容了,而是直接endresponse.end()
当浏览器发现,response只返回了404,没有返回任何内容,那么浏览器就会返回一个404的html网页
添加任意内容
我们可以再在response中加一些内容
response.setHeader('X-name', 'I am LG');
设置响应体
通过response.write('')
往响应体中写内容
request.on('end', () => {
const body = Buffer.concat(array).toString();
console.log('body');
console.log(body)
response.statusCode = 400;
response.setHeader('X-name', `I am LG`);
response.write('响应体内容1\n');
response.write('响应体内容2\n');
response.write('响应体内容3\n');
response.write('响应体内容4\n');
response.end()
})
因此,整个响应的内容都可以使用Node.js去控制
那么响应体中只能响应字符串或者Buffer吗?可以响应图片吗?我们可以在response.write('')
中响应一个二进制的图片
当然,响应头中要设置一下ContentType
request.on('end', () => {
const body = Buffer.concat(array).toString();
console.log('body');
console.log(body)
response.statusCode = 400;
response.setHeader('X-name', `I am LG`);
response.setHeader('Content-Type', `image/png`);
response.write(ImageData);
response.end()
})
这样用户访问首页,就可以得到一张图片了
完成目标1: 根据url返回不同的文件
如果访问的路径是/index.html
那么就返回一个静态文件,一般放在static / public文件夹中(这个文件夹中的代码都是运行在浏览器中,而不是运行在Node.js中)
这里我们在根目录下新建public文件夹,在它下面新建一个index.html、style.css、main.js
如果访问的路径是/index.html
就要读当前目录下的public/index.html
我们要借助Node.js中的path模块去处理路径拼接
Node.js中的全局变量__dirname
表示当前文件所在目录
import * as http from "http";
import {IncomingMessage, ServerResponse} from "http";
import * as fs from "fs";
import * as p from 'path';
const server = http.createServer();
const publicDir = p.resolve(__dirname,'public')
// 监听server的request,如果服务器被人请求了
server.on('request', (request: IncomingMessage, response: ServerResponse) => {
const {method, url, headers} = request
switch (url){
case '/index.html':
// data是Buffer,要转成string
fs.readFile(p.resolve(publicDir,'index.html'),(error,data)=>{
if(error) throw error;
response.end(data.toString())
})
break;
case '/style.css':
fs.readFile(p.resolve(publicDir,'style.css'),(error,data)=>{
if(error) throw error;
response.end(data.toString())
})
break;
case '/main.js':
fs.readFile(p.resolve(publicDir,'main.js'),(error,data)=>{
if(error) throw error;
response.end(data.toString())
})
break;
}
});
// 监听端口
server.listen(8888);
我们可以发现一个问题,我们要访问的内容都被当做html展示出来了,因此我们要告诉浏览器它是css还是js
我们需要告诉指定内容的类型是什么response.setHeader('Content-Type','text/css;charset=utf-8')
这样浏览器才知道这是css响应
同样的响应的JS文件,也要设置一下
response.setHeader('Content-Type','text/javascript;charset=utf-8')
接着让index.html引用style.css和main.js
虽然即使我们服务器,在响应的时候不说明响应文件的类型,浏览器发现index.html中使用了link标签自动识别出文件为css文件,使用script标签的文件为js文件
但是为了让写法更加完整,最好还是在响应头中说明文件的类型
import * as http from "http";
import {IncomingMessage, ServerResponse} from "http";
import * as fs from "fs";
import * as p from 'path';
const server = http.createServer();
const publicDir = p.resolve(__dirname, 'public')
// 监听server的request,如果服务器被人请求了
server.on('request', (request: IncomingMessage, response: ServerResponse) => {
const {method, url, headers} = request
switch (url) {
case '/index.html':
response.setHeader('Content-Type', 'text/html;charset=utf-8')
// data是Buffer,要转成string
fs.readFile(p.resolve(publicDir, 'index.html'), (error, data) => {
if (error) throw error;
response.end(data.toString())
})
break;
case '/style.css':
response.setHeader('Content-Type', 'text/css;charset=utf-8')
// data是Buffer,要转成string
fs.readFile(p.resolve(publicDir, 'style.css'), (error, data) => {
if (error) throw error;
response.end(data.toString())
})
break;
case '/main.js':
response.setHeader('Content-Type', 'text/javascript;charset=utf-8')
// data是Buffer,要转成string
fs.readFile(p.resolve(publicDir, 'main.js'), (error, data) => {
if (error) throw error;
response.end(data.toString())
})
break;
}
});
// 监听端口
server.listen(8888);
完成目标2: 处理查询参数
当我们在请求html的时候有时候要加一个参数,如:http://localhost:8888/index.html?q=1
我们不妨打印一下这个url
可以发现url竟然包含了?q=1
这时候我们就要借助Node.js中的url模块来解决这个问题,但是由于之前的代码中从request中引入了url,和这里的url模块重命了,所以要重新命名一下
const {method, url, headers} = request
改为
const {method, url:path, headers} = request
接下来,我们不妨看下结果url处理过的path会变成什么?
const object = url.parse(path as string)
console.log(object)
这样就把路径和查询参数单独摘出来了,我们就可以单独的去获取了!!
const {pathname,search} = url.parse(path as string)
这样查询字符串就不会之前代码中的switch case
中的逻辑了
这样我们就使用url.parse()
获取到了基本路径和查询参数
import * as http from "http";
import {IncomingMessage, ServerResponse} from "http";
import * as fs from "fs";
import * as p from 'path';
import * as url from "url";
const server = http.createServer();
const publicDir = p.resolve(__dirname, 'public')
// 监听server的request,如果服务器被人请求了
server.on('request', (request: IncomingMessage, response: ServerResponse) => {
const {method, url:path, headers} = request
const {pathname,search} = url.parse(path as string)
switch (pathname) {
case '/index.html':
response.setHeader('Content-Type', 'text/html;charset=utf-8')
// data是Buffer,要转成string
fs.readFile(p.resolve(publicDir, 'index.html'), (error, data) => {
if (error) throw error;
response.end(data.toString())
})
break;
case '/style.css':
response.setHeader('Content-Type', 'text/css;charset=utf-8')
// data是Buffer,要转成string
fs.readFile(p.resolve(publicDir, 'style.css'), (error, data) => {
if (error) throw error;
response.end(data.toString())
})
break;
case '/main.js':
response.setHeader('Content-Type', 'text/javascript;charset=utf-8')
// data是Buffer,要转成string
fs.readFile(p.resolve(publicDir, 'main.js'), (error, data) => {
if (error) throw error;
response.end(data.toString())
})
break;
}
});
// 监听端口
server.listen(8888);
完成目标3: 匹配任意文件
当前只能回去三个文件,如果当前在根目录下加一个叫jquery.js
文件,然后在index.html中引入这个js文件
我们在浏览器中访问http://localhost:8888/index.html?q=1
,我们会发现我们获取不到jquery.js
为什么呢?因为我们在响应中没有处理,那难道每加一个文件,都要作响应的处理吗?
我们希望如果引入了别的文件,我们不想改任何内容,会自动的匹配到哪个文件,我们该怎么办呢
我们观察一下之前的代码,其实很多内容都是一样
我们只需要从请求的路径中获得到文件名,然后用publicDir拼接文件名即可得到完整的请求路径
import * as http from "http";
import {IncomingMessage, ServerResponse} from "http";
import * as fs from "fs";
import * as p from 'path';
import * as url from "url";
const server = http.createServer();
const publicDir = p.resolve(__dirname, 'public')
// 监听server的request,如果服务器被人请求了
server.on('request', (request: IncomingMessage, response: ServerResponse) => {
const {method, url:path, headers} = request
const {pathname,search} = url.parse(path as string)
// response.setHeader('Content-Type', 'text/html;charset=utf-8')
// data是Buffer,要转成string
const fileName = (pathname as string ).substr(1)
fs.readFile(p.resolve(publicDir, fileName), (error, data) => {
if (error){
response.statusCode = 404
response.end()
}else{
response.end(data.toString())
}
})
});
// 监听端口
server.listen(8888);
我们发现所有文件都可以请求到了
即使我们在public
目录下新创建xxx的目录,再创建一个aa.html,在浏览器中也是可以访问到的
如果访问的路径/文件不存在
到此,就实现了一个静态服务器了,public中的文件只要访问的路径存在,就都可以访问到了
完成目标4:处理不存在的文件 -返回404页面
如果访问的路径不存在,我们打印error
接下来我们要处理400的错误和内部服务器的错误(如:权限问题),根据error.errno来判断错误类型
if (error.errno === -4058) {
response.statusCode = 404
console.log(p.resolve(publicDir, '404.html'))
fs.readFile(p.resolve(publicDir, '404.html'), (err, data) => {
response.end(data)
})
}else {
response.statusCode = 500
response.setHeader('Content-Type', 'text/html;charset=utf-8')
response.end('服务器繁忙,请稍后再试')
}
如果访问的路径不存在,就去读404.html
注意: 如果index中插入的图片请求不到,请在图片地址前加上/
,请在图片地址前加上/
,请在图片地址前加上/
!!!!!!!!!!!!!!!
对比下路径加/
和不加/
图片请求图片路径的区别:
当我们尝试访问首页的时候,会显示服务器繁忙?
因为此时,filename是空的,pathname是/
if(fileName === ''){ fileName = 'index.html' }
如果访问的路径是纯目录,那么也会报错,针对这个错误类型再加一个判断即可
if(error.errno === -4068){
response.statusCode = 403
response.setHeader('Content-Type', 'text/html;charset=utf-8')
response.end('无权查看纯目录了路径')
}
至此,处理不存在的文件,返回404页面的功能就全部实现了
import * as http from "http";
import {IncomingMessage, ServerResponse} from "http";
import * as fs from "fs";
import * as p from 'path';
import * as url from "url";
const server = http.createServer();
const publicDir = p.resolve(__dirname, 'public')
// 监听server的request,如果服务器被人请求了
server.on('request', (request: IncomingMessage, response: ServerResponse) => {
const {method, url: path, headers} = request
const {pathname, search} = url.parse(path as string)
// response.setHeader('Content-Type', 'text/html;charset=utf-8')
// data是Buffer,要转成string
let fileName = (pathname as string).substr(1)
if(fileName === ''){
fileName = 'index.html'
}
fs.readFile(p.resolve(publicDir, fileName), (error, data) => {
if (error) {
console.log(error)
if (error.errno === -4058) {
response.statusCode = 404
console.log(p.resolve(publicDir, '404.html'))
fs.readFile(p.resolve(publicDir, '404.html'), (err, data) => {
response.end(data)
})
}else if(error.errno === -4068){
response.statusCode = 403
response.setHeader('Content-Type', 'text/html;charset=utf-8')
response.end('无权查看纯目录了路径')
} else {
response.statusCode = 500
response.setHeader('Content-Type', 'text/html;charset=utf-8')
response.end('服务器繁忙,请稍后再试')
}
} else {
response.end(data)
}
})
});
// 监听端口
server.listen(8888);
完成目标5: 处理非GET请求
用的时候用户发起的并不是get请求,如:页面中有form表单
当单击post时候会发送post请求
我们应该阻止用户发送post请求,并提示用户不能使用post而不是返回404页面
我们应该对method进行过滤,静态服务器是不能接收post请求
if(method !== 'GET'){
// 表明服务器禁止了使用当前HTTP 方法的请求
response.statusCode = 405;
response.end();
return
}
至此,就完成了处理非GET请求的功能
完成目标6: 添加缓存选项
添加缓存选项,减少第二次请求相同路径的时间
我们可以借助Cache-Concol
let cacheAge = 3600 * 24 *365;
response.setHeader('Cache-Control',`public,max-age=${cacheAge}`)
可以看出除了首页不能缓存外,其他文件都缓存了,也就是第二次重复请求相同地址时,有些文件就直接从内存的缓存中读取了,缓存时间为一年,在这段时间内,不会再访问服务器,每次请求时间都是0秒
如果想更新缓存,就更改文件名
全部代码
import * as http from "http";
import {IncomingMessage, ServerResponse} from "http";
import * as fs from "fs";
import * as p from 'path';
import * as url from "url";
const server = http.createServer();
const publicDir = p.resolve(__dirname, 'public')
let cacheAge = 3600 * 24 *365;
// 监听server的request,如果服务器被人请求了
server.on('request', (request: IncomingMessage, response: ServerResponse) => {
const {method, url: path, headers} = request
const {pathname, search} = url.parse(path as string)
if(method !== 'GET'){
// 405表明服务器禁止了使用当前HTTP 方法的请求
response.statusCode = 405;
response.end();
return
}
// response.setHeader('Content-Type', 'text/html;charset=utf-8')
let fileName = (pathname as string).substr(1)
if(fileName === ''){
fileName = 'index.html'
}
fs.readFile(p.resolve(publicDir, fileName), (error, data) => {
if (error) {
console.log(error)
if (error.errno === -4058) {
// 404表示网页或文件未找到
response.statusCode = 404
console.log(p.resolve(publicDir, '404.html'))
fs.readFile(p.resolve(publicDir, '404.html'), (err, data) => {
response.end(data)
})
// 访问的的是纯目录路径,403表示服务器端有能力处理该请求,但是拒绝授权访问
}else if(error.errno === -4068){
response.statusCode = 403
response.setHeader('Content-Type', 'text/html;charset=utf-8')
response.end('无权查看纯目录了路径')
} else {
response.statusCode = 500
response.setHeader('Content-Type', 'text/html;charset=utf-8')
response.end('服务器繁忙,请稍后再试')
}
} else {
// 添加缓存
response.setHeader('Cache-Control',`public,max-age=${cacheAge}`)
// 返回文件内容
response.end(data)
}
})
});
// 监听端口
server.listen(8888);
对比业界优秀案例
对比业界优秀案例,发现他们提供了哪些功能,哪些功能我们是可以做的,完善我们自己的静态服务器
免费获得正版webstorm使用的的权限
不要浪费你的代码,持续维护三个月,可以换webStorm一年的使用权限
发布到npm
申请条件
- 你是开源项目的核心贡献者
- 你的项目是开源的(如MIT许可证)
- 你的项目是不盈利的
- 你的项目至少活跃了3个月(每个月至少提交一次代码)
- 你定义发布更新的版本
- 满足条件就点击申请按钮
填写材料
- 邮箱地址一定不要填错
- No. of required licenses 处填1
- 项目描述用Google翻译
- 正版JetBrains全家桶唾手可得,价值$649/ 年
转载自:https://juejin.cn/post/7248827279651176506