简单的静态资源服务器实现
初始版本0x00
import path, { extname } from 'path';
import http from 'http';
import url from 'url';
const documentRoot = 'C:\\Code\\exp';
const server = http.createServer((req, res) => {
const visitPath = path.join(documentRoot, decodeURI(req.url || ''));
fs.stat(visitPath, (err, stats) => {
if (err) {
res.statusCode = 500;
res.end(err.message);
return;
}
if (stats.isDirectory()) {
fs.readdir(visitPath, (err, fileList) => {
if (err) {
res.statusCode = 500;
res.end(err.message);
return;
}
res.setHeader('Content-Type', 'text/html; charset=utf8');
res.write(`
${req.url} 有 ${fileList.length} 个文件
`);
res.end();
});
}
else {
const extName = path.extname(visitPath);
if (['.gif', '.jpg', '.png'].find(x => extName.toLowerCase() === x)) {
res.setHeader('Content-Type', 'image/' + extName.slice(1));
}
else {
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(path.basename(visitPath))}"`);
}
res.statusCode = 200;
fs.createReadStream(visitPath).pipe(res);
}
});
});
server.listen(9527);
console.log('Server is listening in http://127.0.0.1:9527');
- 目的在于搭建静态服务器类似功能的服务器,如拥有文件进入回退、文件下载、图片预览等功能。
- 代码实现
- 用
http.createServer
创建一个http服务器,用于处理浏览器的请求。 -
server.listen(9527)
指定http服务器监听9527端口,并处理该端口接收到的request -
documentRoot
指定用户访问到的服务器的静态资源入口 -
path.join()
接口文档,是智能的将你传入的参数url拼接起来成为合法的url地址。decodeURI(req.url)
目的是拿到用户请求的url,并且考虑到有中文的情况将其decodeURI
- fs.stat()文档包含文件的信息,是否是文件夹,等。
- 检测访问地址是否在根目录(documentRoot)存在。存在则检测是否是文件夹,如果是文件夹就返回一个html文档,列表循环出所有文件。
- fs.readdir 读取文件夹内容 文档
- 如果不是文件夹,那么获取req.url的扩展名
path.extname(visitPath)
,检测是否是图片,如果是,那么就将response头添加内容标记Content-Type: image/png
,如果不是,则response头添加标记Content-Disponsition
文档,表示文件以附件的方式下载到本地。
fs.createReadStream(visitPath).pipe(res)
,visitPath
是当前文件的路径,基于这个文件创建读取流,通过pipe管道的方式去输出到一个写入流res。这里涉及到一个流的概念。一般我们通过res去写入流是调用res.write
等方法。其实本质上也是指定了一个流。这里的做法是,我通过某个文件创建一个读取流,那么这个流就是文件本身的所有数据。管道pipe
也就是指定输入和输出流。这里指定了写入流就是res。所以会表现为客户端的就是文件内容本身。
- 用
到此,一个实现了基本功能的静态服务器就搭建完成了。但是也注意到有不少可以优化的地方
- 社区是否存在基于http server的优秀实现?
- 什么是中间件,中间件的好处,你的功能,社区有优秀的实践吗?
- callback hell 回调地狱的问题
- node开发中,sync和async的优劣?为什么要存在异步和同步的代码?
版本0x01: 社区优秀的server实现,express、koa...
import fs from 'fs';
import path, { extname } from 'path';
import http from 'http';
import express from 'express';
const documentRoot = 'C:\\Code\\exp';
const app = express();
app.use((req, res) => {
const visitPath = path.join(documentRoot, decodeURI(req.url || ''));
fs.stat(visitPath, (err, stats) => {
if (err) {
res.statusCode = 500;
res.end(err.message);
return;
}
if (stats.isDirectory()) {
fs.readdir(visitPath, (err, fileList) => {
if (err) {
res.statusCode = 500;
res.end(err.message);
return;
}
res.setHeader('Content-Type', 'text/html; charset=utf8');
res.write(`
${req.url} 有 ${fileList.length} 个文件
`);
res.end();
});
}
else {
const extName = path.extname(visitPath);
if (['.gif', '.jpg', '.png'].find(x => extName.toLowerCase() === x)) {
res.setHeader('Content-Type', 'image/' + extName.slice(1));
}
else {
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(path.basename(visitPath))}"`);
}
res.statusCode = 200;
fs.createReadStream(visitPath).pipe(res);
}
});
})
app.listen(9527);
console.log('Server is listening in http://127.0.0.1:9527');
- 眼尖的伙伴注意到了,这里的代码,只是换成了
app.use
app.listen
这些express
提供的API,其实我们点进去express的源码就可以发现,其实req、res是继承 http.IncomingMessage 和 http.ServerResponse。这就表示app.use
是基于http来实现的。所以,我们之前写的代码直接拿来用,也是可以运行滴。
版本0x02:什么是中间件,中间件的好处,你的功能,社区有优秀的实践吗?
import fs from 'fs';
import path, { extname } from 'path';
import http from 'http';
import express from 'express';
import serveIndex from 'serve-index';
const documentRoot = 'C:\\Code\\exp';
const app = express();
app.use('/static', express.static(documentRoot));
app.use('/static', serveIndex(documentRoot));
app.use('/file', (req, res) => {
// 访问 /file/document 相当于要访问根目录下的 /document
const requestPath = req.path;
const visitPath = path.join(documentRoot, requestPath.replace(/^\/file/, ''));
fs.stat(visitPath, (err, stats) => {
if (err) {
res.statusCode = 500;
res.end(err.message);
return;
}
if (stats.isDirectory()) {
fs.readdir(visitPath, (err, fileList) => {
if (err) {
res.statusCode = 500;
res.end(err.message);
return;
}
res.json({ code: 0, message: 'ok', data: { fileList } });
});
}
else {
const extName = path.extname(visitPath);
if (['.gif', '.jpg', '.png'].find(x => extName.toLowerCase() === x)) {
res.setHeader('Content-Type', 'image/' + extName.slice(1));
}
else {
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(path.basename(visitPath))}"`);
}
res.statusCode = 200;
fs.createReadStream(visitPath).pipe(res);
}
});
});
app.use((req, res) => {
res.send('It works');
});
app.listen(9527);
console.log('Server is listening in http://127.0.0.1:9527');
- 什么是中间件?
中间件(英语:Middleware),又译中间件、中介层,是一类提供系统软件和应用软件之间连接、便于软件各部件之间的沟通的软件,应用软件可以借助中间件在不同的技术架构之间共享信息与资源。中间件位于客户机服务器的操作系统之上,管理着计算资源和网络通信。
中间件在现代信息技术应用框架如Web服务、面向服务的体系结构等中应用比较广泛,如数据库、Apache的Tomcat,IBM公司的WebSphere,BEA公司的WebLogic应用服务器,东方通的Tong系列中间件等都属于中间件。
严格来讲,中间件技术已经不局限于应用服务器、数据库服务器。围绕中间件,Apache组织、IBM、Oracle(BEA)、微软各自发展出了较为完整的软件产品体系。(Microsoft Servers微软公司的服务器产品)。
个人浅显的理解:在node应用中:http通信过程存在相互的reqest
和response
。那么中间件的作用就是接受requset,实现相应的处理,然后再输出你的期望输出。假如: 我们有个用作身份认证的中间件。这个中间件检测请求头是否存在某个特征值,如果该特征值合法,那么我正常返回数据,如果特征值不合法或者不存在,那么我可以拒绝这个请求。这就是简单的一个中间件的作用。实际上中间件可以做的东西很多,社区也有很多优秀的实现。开发的过程不妨去查看社区是否有star多的实现。不必再造轮子。当然为了学习或贡献,你可以自己自己编写中间件。
- 社区的优秀实现
app.use('/static', express.static(documentRoot));
app.use('/static', serveIndex(documentRoot));
上文的代码中这两行就是express已经为你实现好的中间件,当浏览器命中/static这个url的时候,表示客户端想要访问静态资源。这时候,我们定义了这个中间件提供静态资源服务去正确响应请求。express.static()
serveIndex()
分别是静态文件访问中间件和文件index中间件。其实也就是版本0x00
实现的功能。代码中的
const extName = path.extname(visitPath);
if (['.gif', '.jpg', '.png'].find(x => extName.toLowerCase() === x)) {
res.setHeader('Content-Type', 'image/' + extName.slice(1));
}
else {
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(path.basename(visitPath))}"`);
}
res.statusCode = 200;
fs.createReadStream(visitPath).pipe(res);
这一块功能,其实express.static已经实现了,这里是重复的,后面版本会将其删除。
版本0x03: callBackHell
const fs = require('fs');
const path = require('path');
const documentRoot = 'C:\\Code\\exp';
const express = require('express');
const pify = require('pify');
const app = express();
// 处理静态资源请求
app.use('/static', express.static(documentRoot));
// 处理目录读取
app.use('/file', async(req, res) => {
// 访问 /file/document 相当于要访问根目录下的 /document
const requestPath = req.path;
const visitPath = path.join(documentRoot, requestPath.replace(/^\/file/, ''));
try {
const stats = await pify(fs.stat)(visitPath)
if (stats.isDirectory()) {
const { err, fileList } = await pify(fs.readdir)(visitPath)
res.json({ code: 0, message: 'ok', data: { fileList } });
} else {
res.json({ code: 9001, message: 'Not a directory' });
}
} catch (err) {
res.status(500).end(err.massage)
return
}
});
app.use('/sync', (req, res) => {
const start = +new Date();
while(+new Date() - start < 10000) {}
res.send('finish');
});
app.use('/async', (req, res) => {
setTimeout(() => res.send('finish'), 10000);
});
// 直出前端视图
app.use((req, res) => {
res.send('It works!!!');
});
app.listen(9527);
console.log('Server is listening in http://127.0.0.1:9527');
- 关于
commonJS
和ES Module
的问题。这里的变更是因为我用了hotnode
热重载的一个包,但是这个包不支持ES module
语法。所以我将其改成了commonJS
的写法。但是社区其实有更好用的热重载包,对ES Module
语法也是支持的。ts-node-dev
.推荐使用,还可以支持调试npx ts-node-dev --inspect -- server.ts
- 同步和异步的问题
app.use('/sync', (req, res) => {
const start = +new Date();
while(+new Date() - start < 10000) {}
res.send('finish');
});
app.use('/async', (req, res) => {
setTimeout(() => res.send('finish'), 10000);
});
这段代码其实是非相关的,但是这里我留着是觉得这里很重要。node是单线程异步IO的。所以,我们写node的代码的时候,为了利用node的高性能,我们实际上IO操作等是一定要用异步操作的。接下来说说为什么,代码里面的两个例子。由于node是单线程的,所以当出现阻塞的时候,那么就不能马上处理新的请求。/sync是同步执行的。会阻塞10s钟,/async是异步执行的,setTimeout也是10s的等待,但是不会阻塞,原因是node会先将它丢到一个等待队列,等node的执行栈空了才进行回调。这个等待过程node是可以继续处理请求的。此处代码可以做个简单的小实验:启动浏览器A(客户端A)先去调用'localhost:9527/sync', 再去启动浏览器B(客户端B) 调用静态资源服务(localhost:9527/static/本地存在的一个文件如‘我的头像.png’)。这个是否你会发现无论是客户端A和客户端B都是在等待。因为客户端A阻塞了请求。所以,编写IO操作、文件操作等时间成本较长的操作,一定要是异步执行。而如果是先调用/async再去静态资源请求‘我的头像.png’,你会发现,服务端马上就响应并返回该图片。
- callback hell问题:Promise
Promise就是避免回调地狱而产生的。
try {
const stats = await pify(fs.stat)(visitPath)
if (stats.isDirectory()) {
const { err, fileList } = await pify(fs.readdir)(visitPath)
res.json({ code: 0, message: 'ok', data: { fileList } });
} else {
res.json({ code: 9001, message: 'Not a directory' });
}
} catch (err) {
res.status(500).end(err.massage)
return
}
pify
是个让node的异步操作Promise化的一个库。这样我们就可以实现编写同步代码,享受异步执行带来的高性能。
看着更简洁,更易读。
- 整片代码可以看出来已经拆分了几个中间件。分别是/static静态文件访问、/file用作api数据返回、其他地址直出前端视图。
下一篇文章将讲述:如何从空文件夹一步步搭建前端项目(我这里是react + ts),如何将自己搭建的服务器用于自己的前端项目。
PS:技术博客的编写经验不足,加上个人的水平有限,如果有错误可以指出一起共同探讨!请大家多多指教~