До чтения
previously:
Идеи дизайна
Когда вы вводите URL-адрес, URL-адрес может соответствовать ресурсу (файлу) на сервере или каталогу. Сервер So будет анализировать этот URL-адрес и делать разные вещи для разных ситуаций. Если URL-адрес соответствует файлу, сервер вернет файл. Если URL-адрес соответствует папке, сервер вернет список всех подфайлов/подпапок, содержащихся в этой папке. Это то, что в основном делает статический сервер.
Но реальная ситуация не так проста, URL-адрес, который мы получаем, может быть неправильным, а соответствующий файл или папка могут вообще не существовать. Или какие-то файлы и папки защищены системой и скрыты, и мы не хотим, чтобы клиент об этом знал. Следовательно, мы должны сделать несколько разных возвратов и подсказок для этих особых случаев.
Кроме того, прежде чем мы действительно вернем файл, нам нужно провести некоторые переговоры с клиентом. Нам нужно знать тип языка, метод кодирования и т. д., которые клиент может принять, чтобы выполнять различную обработку возврата для разных браузеров. Нам нужно сообщить клиенту некоторую дополнительную информацию о возвращаемом файле, чтобы клиент мог лучше получать данные: Нужно ли кэшировать файл и как? Был ли файл сжат и как его распаковать? так далее...
До сих пор у нас было предварительное понимание почти всего, что в основном делает статический сервер, Пойдем!
выполнить
каталог проекта
static-server/
|
| - bin/
| | - www # 批处理文件
|
|
| - src/
| | - App.js # main文件
| | - Config.js # 默认配置
|
|
·- package.json
конфигурационный файл
Чтобы запустить сервер, нам нужно знать номер порта, на котором был запущен сервер.
И после получения запроса пользователя нам нужнона нашем собственном сервереЧтобы найти ресурсы, поэтому нам нужно настроитьРабочий список.
let config = {
host:'localhost' //提示用
,port:8080 //服务器启动时候的默认端口号
,path:path.resolve(__dirname,'..','test-dir') //静态服务器启动时默认的工作目录
}
общая структура
Уведомление
- Это в функции события по умолчанию привязанный объект (здесь небольшой сервер), который изменен на большой объект сервера, так что метод под сервером может быть вызван в функции обратного вызова.
class Server(){
constructor(options){
/* === 合并配置参数 === */
this.config = Object.assign({},config,options)
}
start(){
/* === 启动http服务 === */
let server = http.createServer();
server.on('request',this.request.bind(this));
server.listen(this.config.port,()=>{
let url = `${this.config.host}:${this.config.port}`;
console.log(`server started at ${chalk.green(url)}`)
})
}
async request(req,res){
/* === 处理客户端请求,决定响应信息 === */
// try
//如果是文件夹 -> 显示子文件、文件夹列表
//如果是文件 -> sendFile()
// catch
//出错 -> sendError()
}
sendFile(){
//对要返回的文件进行预处理并发送文件
}
handleCache(){
//获取和设置缓存相关信息
}
getEncoding(){
//获取和设置编码相关信息
}
getStream(){
//获取和设置分块传输相关信息
}
sendError(){
//错误提示
}
}
module.exports = Server;
запрос обработка запросов
получить URLpathname
,а такжеАдрес локального рабочего корневого каталога сервераСшивание, возврат одногоfilename
используя имя файла иstat方法
Определить, является ли это файлом или папкой
-
Если это папка, использовать
readdir方法
Возвращает список в папке, оборачивая список в массив объектов Затем объедините руль, чтобы скомпилировать данные массива в шаблон и, наконец, вернуть шаблон клиенту. -
Если это файл, Передать req, res, statObj, путь к файлу
sendFile
, обрабатывается sendFile
async request(req,res){
let pathname = url.parse(req.url);
if(pathname == '/favicon.ico') return; //浏览器会自动向我们索取网站图标,这里没有准备,为了防止报错,返回即可
let filepath = path.join(this.config.root,pathname);
try{
let statObj = await stat(filepath);
if(statObj.isDirectory()){
let files = awaity readdir(filepath);
files.map(file=>{
name:file
,path:path.join(pathname,file)
});
// 让handlebar 拿着数去编译模板
let html = this.list({
title:pathname
,files
})
res.setHeader('Content-Type','text/html');
res.end(html);
}else{
this.sendFile(req,res,filepath,statObj);
}
}catch(e){
this.sendError(e,req,res);
}
}
[совет] Мы будем
request
методasync
так что мы можем писать асинхронный код так же, как мы пишем синхронный код
метод
sendFile
Задействует такие функции, как кэширование, кодирование, сегментированная передача и т. д.
sendFile(){
if(this.handleCache(req,res,filepath,statObj)) return; //如果走缓存,则直接返回。
res.setHeader('Content-type',mime.getType(filepath)+';charset=utf-8');
let encoding = this.getEncoding(req,res); //获取浏览器能接收的编码并选择一种
let rs = this.getStream(req,res,filepath,statObj); //支持断点续传
if(encoding){
rs.pipe(encoding).pipe(res);
}else{
rs.pipe(res);
}
}
handleCache
При обработке кеша следует отметить, что кеш делится на обязательный кеш и кеш сравнения, и приоритет обязательного кеша выше, чем приоритет относительного кеша.
То есть, когда принудительный кэш вступает в силу, он не перейдет в относительный кэш, и не будет инициировать запрос, такой как сервер.
Но раз принудительно промахнулся кеш, то кеш пойдет относительно, если文件标识
Если изменений нет, вступает в силу относительный кеш,
Клиент по-прежнему будет кэшировать данные для получения данных, поэтому принудительное кэширование и относительное кэширование не конфликтуют.
Принудительное кэширование и относительное кэширование используются вместе, чтобы снизить нагрузку на сервер и обеспечить актуальность запрошенных данных.
Кроме того, следует отметить, что если одновременно установлены два относительных идентификатора кэшируемых файлов, кэширование вступит в силу только тогда, когда ни один из них не изменился.
handleCache(req,res,filepath,statObj){
let ifModifiedSince = req.headers['if-modified-since']; //第一次请求是不会有的
let isNoneMatch = req.headers['is-none-match'];
res.setHeader('Cache-Control','private,max-age=30');
res.setHeader('Expires',new Date(Date.now()+30*1000).toGMTString()); //此时间必须为GMT
let etag = statObj.size;
let lastModified = statObj.ctime.toGMTString(); //此时间格式可配置
res.setHeader('Etag',etag);
res.setHeader('Last-Modified',lastModified);
if(isNoneMatch && isNoneMatch != etag) return false; //若是第一次请求已经返回false
if(ifModifiedSince && ifModifiedSince != lastModified) return false;
if(isNoneMatch || ifModifiedSince){
// 说明设置了isNoneMatch或则isModifiedSince且文件没有改变
res.writeHead(304);
res.end();
return true;
}esle{
return false;
}
}
Если вы хотите узнать больше о кэшировании в деталях, вы можете прочитать эту мою статью
getEncoding
Получите тип кодировки, который браузер может получить из заголовка запроса, и используйте обычное сопоставление для соответствия первому. Создайте соответствующий экземпляр zlib и верните его методу sendFile для кодирования при возврате файла.
getEncoding(req,res){
let acceptEncoding = req.headers['accept-encoding'];
if(/\bgzip\b/.test(acceptEncoding)){
res.setHeader('Content-Encoding','gzip');
return zlib.createGzip();
}else if(/\bdeflate\b/.test(acceptEncoding)){
res.setHeader('Content-Encoding','deflate');
return zlib.createDeflate();
}else{
return null;
}
}
getStream
Фрагментарная передача, в основном с использованием заголовка запросаreq.headers['range']
Чтобы подтвердить, где начинается и заканчивается файл, который нужно получить, но фактические данные получены черезfs.createReadStream
читать.
getStream(req,res,filepath,statObj){
let start = 0;
// let end = statObj.size - 1;
let end = statObj.size;
let range = req.headers['range'];
if(range){
let result = range.match(/bytes=(\d*)-(\d*)/); //不可能有小数,网络传输的最小单位为一个字节
if(result){
start = isNaN(result[1])?0:parseInt(result[1]);
// end = isNaN(result[2])?end:parseInt(result[2]) - 1;
end = isNaN(result[2])?end:parseInt(result[2]);
}
res.setHeader('Accept-Range','bytes');
res.setHeader('Content-Range',`bytes ${start}-${end}/${statObj.size}`)
res.statusCode = 206; //返回整个数据的一块
}
return fs.createReadStream(filepath,{
start:start-1,end:end-1
});
}
Упакован как инструмент командной строки
Мы можем ввести в командной строке, какnpm start
Запуск dev-сервера похож на настройку команды запуска для запуска нашего статического сервера.
#! /usr/bin/env node
// -d 静态文件根目录
// -o --host 主机
// -p --port 端口号
let yargs = require('yargs');
let Server = require('../src/app.js');
let argv = yargs.option('d',{
alias:'root'
,demand:'false' //是否必填
,default:process.cwd()
,type:'string'
,description:'静态文件根目录'
}).option('o',{
alias:'host'
,demand:'false' //是否必填
,default:'localhost'
,type:'string'
,description:'请配置监听的主机'
}).option('p',{
alias:'port'
,demand:'false' //是否必填
,default:8080
,type:'number'
,description:'请配置端口号'
})
//usage 命令格式
.usage('static-server [options]')
// example 用法实例
.example(
'static-server -d / -p 9090 -o localhost'
,'在本机9090的端口上监听客户端的请求'
)
.help('h').argv;
//argv = {d,root,o,host,p,port}
let server = new Server(argv);
server.start();
let os = require('os').platform();
let {exec} = require('child_process');
let url = `http://${argv.hostname}:${argv.port}`
if(argv.open){
if(os === 'win32'){
exec(`start ${url}`);
}else{
exec(`open ${url}`);
}
}
Что касается принципа, из-за ограниченности места, пожалуйста, обратите внимание на эту мою статью для более подробной информации.process.argv с инструментами командной строки
Скачивайте, устанавливайте и пользуйтесь
через нпм
npm i static-server-study
let static = require('static-server-study');
let server = new static({
port:9999
,root:process.cwd()
});
server.start();
через гитхаб
После клонирования выполните следующую команду
npm init
npm link
Затем мы можем использовать любой каталог в качестве рабочего каталога статического сервера.
Просто откройте окно командной строки в этом каталоге.static-server