Все, что вам нужно знать о реализации статического сервера Node.js

Node.js внешний интерфейс сервер Командная строка HTTP JavaScript

До чтения

previously:

Node и http: все в одном

Идеи дизайна

Когда вы вводите 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;
    }
}

Если вы хотите узнать больше о кэшировании в деталях, вы можете прочитать эту мою статью

304 и кеш

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