一、http常用状态码
// 1xx
100 # 服务器接收到一部分,正在等待其余部分。
101 # websocket协议
// 2xx 成功
200 # 成功
204 # 请求成功但是没有返回具体内容
206 # 分片传输
// 3xx 重定向、缓存
301 # 永久重定向
302 # 临时重定向
304 # 缓存
// 4xx 客户端错误
400 # 参数错误,服务器解析不了参数
401 # 用户未登陆,且没有权限
403 # 访问被禁止,用户已登陆但是没有权限
404 # Not Found
405 # 客户端请求方法,服务端不支持
416 # 请求范围无效 Range: bytes 5-
// 5xx 服务端错误
500 # 服务器内部错误
502 # 网关错误
504 # 网关超时
505 # http版本不支持
二、简单请求和复杂请求
复杂请求或者跨域请求,都会发送options预检请求。
什么样的请求是复杂请求呢?
- get和post默认是简单请求,如果自定义了header信息,这就会变成复杂请求。
- 除get和post请求之外的请求都是复杂请求。
三、http静态服务
├─http-server
│ ├─bin
│ │ ├─config # 命令行配置文件
│ │ └─www # 配置命令行,启动静态服务入口
│ ├─src
│ | ├─index.js # 启动静态服务
| | ├─util.js # 工具函数
│ | └─template.html # 展示文件夹列表的页面
| ├─package.json # 配置启动服务命令及依赖
1)配置启动命令
// package.json
"bin": {
"fs": "./bin/www.js",
"file-server": "./bin/www.js"
}
// 生成软链
npm link
2)配置命令行参数
// bin/config.js
// 命令行描述配置
const options = {
'port': {
option: '-p, --port <n>', // 根据commander 的 option('')
default: 8080,
usage: 'fs --port 3000',
description: 'set fs port'
},
'gzip': {
option: '-g, --gzip <n>',
default: 1,
usage: 'fs --gzip 0', // 禁用压缩
description: 'set fs gzip'
},
'cache': {
option: '-c, --cache <n>',
default: 1,
usage: 'fs --cache 0', // 禁用缓存
description: 'set fs gzip'
},
'directory': {
option: '-d, --directory <d>',
default: process.cwd(),
usage: 'fs --directory d:', // 禁用缓存
description: 'set fs directory'
}
}
module.exports = options;
3)命令行配置,静态服务入口
// bin/www.js
#! /usr/bin/env node
// 命令行的帮助文档
const program = require('commander');
const options = require('./config');
program.name('fs')
program.usage('[options]')
// 解析 当前运行进程传递的参数
const examples = new Set();
const defaultMapping = {};
Object.entries(options).forEach(([key, value]) => {
// 示例配置
examples.add(value.usage)
// 默认命令行配置
defaultMapping[key] = value.default;
// commander命令
program.option(value.option, value.description)
})
// 命令行执行--help打印的命令列表
program.on('--help', function () {
console.log('\nExamples:')
examples.forEach(item => {
console.log(` ${item}`)
})
})
// 获取用户输入参数
program.parse(process.argv);
let userArgs = program.opts();
// 合并最终的参数 需要启动一个服务
let serverOptions = Object.assign(defaultMapping, userArgs);
// 启动一个服务
const Server = require('../src/index');
let server = new Server(serverOptions);
server.start();
4) ejs模板
// src/template.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<%dirs.forEach(dir=>{%>
<li><a href="<%=dir.url%>"><%=dir.name%></a></li>
<%})%>
</body>
5) 工具函数,用来获取ip
// src/util.js
const os =require('os');
// 获取ip
function getIp(){
let interfaces = os.networkInterfaces();
interfaces = Object.values(interfaces).reduce((memo,current)=>{
return memo.concat(current);
},[]);
let ip = interfaces.find(item=>{
return item.family === 'IPv4' && item.cidr.startsWith('172')
});
return ip
}
this.getIp = getIp
6)读取配置,启动服务,展示页面
const http = require('http');
const url = require('url');
const path = require('path');
const util = require('./util');
const fs = require('fs').promises
const chalk = require('chalk');
const mime = require('mime');
const { createReadStream, readFileSync } = require('fs');
// ejs解析模板
const ejs = require('ejs');
const template = readFileSync(path.resolve(__dirname, 'template.html'), 'utf8');
class Server {
constructor(serverOptions) {
this.port = serverOptions.port;
this.directory = serverOptions.directory;
this.cache = serverOptions.cache;
this.gzip = serverOptions.gzip
this.handleRequest = this.handleRequest.bind(this);
this.template = template;
}
async handleRequest(req, res) {
// 1. 解析路径 => /src
let { pathname } = url.parse(req.url);
// 2. 文件命名是中文,解析会出问题,需要decode一下
pathname = decodeURIComponent(pathname)
// 3. 完整路径,用户不传directory,则默认cwd()的目录
// C:\Users\Desktop\http-server\http-server\src
let requestFile = path.join(this.directory, pathname);
try {
let statObj = await fs.stat(requestFile);
// 如果是文件夹
if (statObj.isDirectory()) {
// 读取文件夹下的文件
// [ 'index.js', 'template.html', 'util.js' ]
const dirs = await fs.readdir(requestFile);
// 根据数据和模板渲染内容
let fileContent = await ejs.render(this.template, {
dirs: dirs.map((dir) => ({
name: dir,
url: path.join(pathname, dir)
}))
});
res.setHeader('Content-Type', 'text/html;charset=utf-8');
// 将模板替换后的文件内容,返回给浏览器进行渲染
res.end(fileContent);
// 如果是文件,则发送文件
} else {
this.sendFile(req, res, requestFile)
}
} catch (e) {
console.log(e)
this.sendError(req, res, e);
}
}
// 根据路径边读编写,输出给浏览器解析
sendFile(req, res, requestFile) {
// 返回文件,需要给浏览器提供内容类型 和 内容的编码格式,浏览器才能识别出来去解析
res.setHeader('Content-Type', mime.getType(requestFile) + ';charset=utf-8');
// 将文件读取出来并且返回
// 如果不结束,浏览器相当于没有接受完毕
createReadStream(requestFile).pipe(res); // 流. ws.write() ws.write() ws.end()
}
// 访问文件地址不存在
sendError(req, res, e) {
res.statusCode = 404;
res.end(`Not Found`)
}
start() {
// 启动服务监听错误信息,如果端口占用 累加1
const server = http.createServer(this.handleRequest);
server.listen(this.port, () => { // 订阅方法 监听成功后会触发
console.log(chalk.yellow('Starting up http-server, serving ./'))
console.log(chalk.yellow('Available on:'));
console.log(`http://` + util.getIp().address + `:${chalk.green(this.port)}`);
console.log(`http://127.0.0.1:${chalk.green(this.port)}`);
});
// 如果报错是EADDRINUSE,端口占用,则+1监听新端口
server.on('error', (err) => {
if (err.errno === 'EADDRINUSE') {
server.listen(++this.port)
}
})
}
}
module.exports = Server;
接下来,我们调一调,看一看:
cd http-server
fs --port 3000