Read the instructions before

previously:

Node and HTTP: one and the same

Design ideas

When you enter a URL, it may correspond to a resource (file) on the server or to a directory. The So server will analyze this URL and do different things for different situations. If the URL corresponds to a file, the server returns the file. If the URL corresponds to a folder, the server returns a list of all the subfiles/subfolders contained in that folder. That’s what a static server is all about.

But the reality is not that simple. The URL we get may be wrong, the file or folder it corresponds to may not exist, or some files and folders are hidden by the system and we don’t want the client to know. Therefore, we need to make some different returns and hints for these special cases.

Again, we need to do some negotiation with the client before we actually return a file. We need to know what language types, encoding methods, and so on are acceptable to the client in order to do different return processing for different browsers. We need to tell the client some additional information about the returned file so that the client can better receive the data: does the file need to be cached and how? Is the file compressed and how should it be decompressed? And so on…

At this point, we’ve taken a look at almost everything a static server mainly does. Let’s Go!

implementation

Project directory

static-server/
|
| - bin/
|   | - www   Batch files
|      
|
| - src/
|   | - App.js    # main file
|   | - Config.js   # default configuration. | | - package, a jsonCopy the code

The configuration file

To start a server, we need to know the startup port number of the server

After receiving the user’s request, we need to search for resources on our own server, so we need to configure a working directory.

let config = {
    host:'localhost'Resolve (__dirname,'.. '.'test-dir') // Default working directory for static server startup}Copy the code

The overall framework

Pay attention to

  • The this in the event function, which by default refers to the bound object (in this case, the small server), is changed to the large object server to call the method under server in the callback function.
class Server(){constructor(options){constructor(options){=== */ this.config = object.assign ({},config,options)}start(){/* === start the HTTP service === */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 // if it is a folder -> display list of subfiles, folders // If it is a file -> sendFile() // catch // error -> sendError()}sendFile(){// Preprocess the file to be returned and send the file}handleCache(){// Get and set cache-related information}getEncoding(){// Get and set the encoding information}getStream(){// Get and set block transfer information}sendError{// error}} module.exports = Server;Copy the code

Request Processing

Get the PATHname of the URL, concatenate it with the server’s local working root address, and return a filename using the filename and stat methods to check whether it is a file or a folder

  • If it is a folder, use the readdir method to return the list under that folder, wrap the list as an array of objects, compile the array data with handleBar into a template, and return the template to the client

  • If it is a file, pass req, res, statObj, and Filepath to sendFile for processing

async request(req,res){
    let pathname = url.parse(req.url);
    if(pathname == '/favicon.ico') return; // The browser will automatically ask us for the website icon, which is not prepared here, in order to prevent errors, just returnlet filepath = path.join(this.config.root,pathname);
   	try{
        let statObj = await stat(filepath);
        if(statObj.isDirectory()){
            letfiles = awaity readdir(filepath); files.map(file=>{ name:file ,path:path.join(pathname,file) }); // Let handlebar compile the template with the numberslet 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); }}Copy the code

[tip] We make the Request method async so that we can write async just like synchronous code

methods

sendFile

It involves functions such as cache, encoding and segmented transmission

sendFile() {if(this.handleCache(req,res,filepath,statObj)) return; // If cache is removed, return directly. res.setHeader('Content-type',mime.getType(filepath)+'; charset=utf-8');
    letencoding = this.getEncoding(req,res); // Get the encoding that the browser can accept and select onelet rs = this.getStream(req,res,filepath,statObj); // Support breakpoint continuationif(encoding){
        rs.pipe(encoding).pipe(res);
    }else{ rs.pipe(res); }}Copy the code

handleCache

Note that the cache is divided into mandatory cache and comparison cache, and the priority of mandatory cache is higher than that of relative cache.

That is, when the cache force is in effect, it does not go against the cache and does not make requests to the server.

If the file identity has not changed, the relative cache takes effect.

The client is still going to cache data to fetch data, so mandatory caching and relative caching are not in conflict.

Mandatory caching, when used together with relative caching, can reduce the strain on the server while keeping the requested data up to date.

It is also important to note that if you set the file identity of two relative caches at the same time, the cache will take effect only if neither of them is changed.

handleCache(req,res,filepath,statObj){
    let ifModifiedSince = req.headers['if-modified-since']; // The first request will not happenlet isNoneMatch = req.headers['is-none-match'];
    res.setHeader('Cache-Control'.'private,max-age=30');
    res.setHeader('Expires',new Date(Date.now()+30*1000).toGMTString()); // The time must be GMTlet etag = statObj.size;
    let lastModified = statObj.ctime.toGMTString(); // This time format can be configured res.setheader ('Etag',etag);
    res.setHeader('Last-Modified',lastModified);
    
    if(isNoneMatch && isNoneMatch ! = etag)return false; // If the first request has been returnedfalse
    if(ifModifiedSince && ifModifiedSince ! = lastModified)return false;
    if(isNoneMatch || ifModifiedSince){// Description isNoneMatch or isModifiedSince is set and res.writehead (304) is not changed; res.end();return true;
    }esle{
    	return false; }}Copy the code

To learn more about caching, read my article

304 and cache

getEncoding

Take the encoding type that the browser can receive from the request header, use the regular match to match the first one, and create the corresponding zlib instance to return to the sendFile method for encoding when the file is returned.

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{
    	returnnull; }}Copy the code

getStream

Headers [‘range’] is used to determine where the file to be received starts and ends, whereas fs.createreadStream is used to read the data.

getStream(req,res,filepath,statObj){
    let start = 0;
    // let end = statObj.size - 1;
    let end = statObj.size;
    let range = req.headers['range'];
    if(range){
      letresult = range.match(/bytes=(\d*)-(\d*)/); // The smallest unit of network transmission is one byteif(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 the entire data block}return fs.createReadStream(filepath,{
      start:start-1,end:end-1
    });
  }
Copy the code

Wrap it as a command-line tool

We can customize a startup command to start our static server just like typing NPM start on the command line to start a dev-server.

#! /usr/bin/env node
// -dStatic file root directory // -o --host Host // -p --port Port numberlet yargs = require('yargs');
let Server = require('.. /src/app.js');
let argv = yargs.option('d', {alias:'root'
  ,demand:'false'// Mandatory,default:process.cwd(),type:'string'
  ,description:'Static file root'
}).option('o', {alias:'host'
  ,demand:'false'// Mandatory: default:'localhost'
  ,type:'string'
  ,description:'Please configure the host to listen to'
}).option('p', {alias:'port'
  ,demand:'false'// Mandatory: default:8080,type:'number'
  ,description:'Please configure the port number'}) //usage command format.usage('static-server [options]'// example:'static-server -d / -p 9090 -o localhost'
    ,'Listen for client requests on port of native 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}`); }}Copy the code

As for the principle, due to space limitations, please check out my article process.argv and command line tools for more details

Download, install, and use

Through the NPM

npm i static-server-study
Copy the code
let static = require('static-server-study');
let server = new static({
  port:9999
  ,root:process.cwd()
});
server.start();
Copy the code

Through a lot

I の making

After clone, run the following command

npm init
npm link
Copy the code

We can then treat any directory as a working directory for a static server by opening a command line window in that directory and typing static-server