The goddess of town building

Http-server-p 3000 is a service with port 3000, which can be accessed directly in the browser. If you want to implement such a tool yourself, what should you do? Don’t worry. Let me write it down

Write before we want to figure out what to do: first written in his bag, a service, return visit to port 3000, should show the public the list of directories, / index after HTML, you should show the index. The HTML content

  • Init a project first, download some packages, MIME (parse return header type), chalk (colorful output), debug
  • Create your own directory structure:

index.css

body{
    background: red
}
Copy the code

index.html

<! DOCTYPE html> <html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <link rel="stylesheet" href="/index.css">
</body>
</html>
Copy the code

config.js

let path = require('path'// Start the configuration item of the servicelet config = {
    hostname:'localhost',
    port:3000,
    dir:path.join(__dirname,'.. '.'public')
}
module.exports = config
Copy the code

Debug is used in app.js.

// set DEBUG=static:app (win32 // export DEBUG=static:app (ios

let config = require('./config')
let path = require('path')
let fs = require('fs')
let mime = require('mime')
let chalk = require('chalk')
let util = require('util')
let url = require('url')
let http = require('http')
let stat= util.promisify(fs.stat) //debug Specifies whether to print using the following parameterslet debug = require('debug') ('static:app') 
//console.log(chalk.green('hello'));
//debug('app') class Server {// First write a Server classconstructor(){
        this.config = config
    }
    handleRequest() {return (req,res)=>{
        }
    }
    start(){// start method on instancelet {port,hostname} = this.config
       letServer = http.createserver (this.handlerequest ()) // Start a service using HTTP and execute the handleRequest method in the callbacklet url = `http://${hostname}:${chalk.green(port)}` debug(url); server.listen(port, hostname); }}let server = new Server()
server.start()
Copy the code

Node executes app.js (before executing set DEBUG=static:app) to get the image below

  handleRequest() {returnAsync (req,res)=>{// Process pathlet {pathname} = url.parse(req.url,true// Because the pathname will be /index, this will point directly to disk C, add./ will become the currentlet  p = path.join(this.config.dir,'. '+pathname)
         
            try{
               let statObj = await stat(p)// Check whether the p path is correctif(statObj.isDirectory()){

                }else{// read res.setheader ('Content-Type',mime.getType(p)+'; charset=utf8')
                    fs.createReadStream(p).pipe(res)
                }
            }catch(e){
                res.statusCode = 404;
                res.end()
            }
        }
    }
Copy the code

By this time visit http://localhost:3000/index.html, can the page information

  • Now that the shelf is up, start writing. Since error reporting and presentation page information should be reused, separate them into two separate methods
SendFile (req,res,p){sendFile(req,res,p){'Content-Type',mime.getType(p)+'; charset=utf8')
        fs.createReadStream(p).pipe(res)
    }
    sendError(req,res,e){
        debug(util.inspect(e).toString())
        res.statusCode = 404;
        res.end()
    }
Copy the code
  • NPM install ejs; render (‘ file contents’, ‘variable parameters’); render (‘ file contents’,’ variables’)src/tmpl.ejs
<! DOCTYPE html> <! DOCTYPE html> <html> <head> <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="renderer" content="webkit">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <title>staticServer</title> </head> <body> <! - meet js is < % % > wrapped up, by value = - > < % dirs. The forEach (dir = > {% > < li > < a href ="<%=dir.path%>"><%=dir.name%></a></li>
    <% })%>
  </body>
</html>
Copy the code

That’s going to go in app.js

let ejs = require('ejs')
let tmpl = fs.readFileSync(path.join(__dirname,'tmpl.ejs'),'utf8')
let readDir = util.promisify(fs.readdir)// Method used to read the directoryCopy the code

Git templates are available here

And I’m going to attach TMPL to this app.js so if it’s a directory this code is going to look like this

 if(statObj.isdirectory ()){// Handlebal ejs underscore jadelet dirs = await readDir(p)
                    debug(dirs// Returns an array [index.css,index.html]dirs = dirs.map(dir => ({
                        path: path.join(pathname, dir),
                        name: dir
                    }))
                let content = ejs.render(this.tmpl,{dirs})
                    res.setHeader('Content-Type'.'text/html; charset=utf8')
                    res.end(content)
                }else{
                    this,this.sendFile(req,res,p)
                }
Copy the code
  • The following is the elaboration of the problem, a total of three directions:
  1. If the file is accessed, there should be caching,
  2. It’s a big file and it should be compressed,
  3. Scope of the request

The cache

The working principle of

First request:

1. The client initiates an HTTP GET request for a file. 2. The server processes the request and returns the file contents and corresponding headers, including the Etag (for example, 627-4D648041f6b80) (assuming that the server supports Etag generation and Etag is enabled) with the status code of 200.Copy the code

Second request (breakpoint continuation) :

1, the client initiates an HTTP GET request for a file and sends an if-range (the contents of this header are the Etag: 627-4d648041f6b80 returned by the server during the first request). 2. The server determines whether the received Etag matches the calculated Etag. If so, the status code of the response is 206. Otherwise, the status code is 200.Copy the code
 cache(req,res,statObj){
        //etag if-none-match
        //Last-Modified  if-modified-since
        //Cache-Control 
        //ifNoneMatch is generally the MD5 stamp of the content => ctime+sizelet ifNoneMatch = req.headers['if-none-match'] / /ifModifiedSince Time when the file was last modifiedlet ifModifiedSince = req.headers['if-modified-since']
        let since = statObj.ctime.toUTCString(); // Last modified time // represents a description of the server filelet etag = new Date(since).getTime()  +The '-'+statObj.size
        res.setHeader('Cache-Control'.'max-age=10') // Force cache res.setheader within 10 seconds'Etag',etag)
        res.setHeader('Last-Modified'// request header with // reaccess comparison, if equal, cacheif(ifNoneMatch ! == etag){return false
        }
        if(ifModifiedSince ! = since){return false
        }
        res.statusCode = 304
        res.end()
        return true

    }
Copy the code

Add this sentence in sendFile

/ / cacheif(this.cache(req,res,statObj)) return
Copy the code

So the screen to access index.html is

The compression

let zlib = require('zlib');
Copy the code

Compression method

   compress(req,res,statObj){// Compress accept-encoding :gzip,deflate,br // content-encoding :gziplet header = req.headers['accept-encoding']
       if(header){
          
        if(header.match(/\bgzip\b/)){
            res.setHeader('Content-Encoding'.'gzip') 
            return zlib.createGzip()
        }else if(header.match(/\bdeflate\b/)){
            res.setHeader('Content-Encoding'.'deflate') 
            return zlib.createDeflate()
        }else{
            return false// Compression is not supported}}else{
           return false}}Copy the code

sendFile

    sendFile(req,res,p,statObj) {/ / cacheif(this.cache(req,res,statObj)) return/ / compressionlet s = this.compress(req, res, p, statObj);
        console.log(s)
        res.setHeader('Content-Type',mime.getType(p)+'; charset=utf8')
         let rs = fs.createReadStream(p)
        ifRs.pipe (s).pipe(res)}else{rs.pipe(res)} // fs.createreadStream (p).pipe(res)}Copy the code

Check whether the compression success: visit http://localhost:3000/index.css

Scope of the request

range(req,res,statObj){// Range request header: Rang:bytes=1-100 // server accept-ranges :bytes // Content-ranges :1-100/totallet header = req.headers['range']
        //header =>bytes=1-100
        let start = 0;
        let end = statObj.size; // Size of the entire fileif(header){
            res.setHeader('Content-Range'.'bytes')
            res.setHeader('Accept-Ranges',`bytes ${start}-${end}/${statObj.size}`)
            let[,s,e] = header.match(/bytes=(\d*)-(\d*)/); start = s? parseInt(s):start end = e? parseInt(e):end }return{start,end:end-1}// Because start is 0}Copy the code

That’s the case with sendFile

  sendFile(req, res, p, statObj) {// Cache function comparison forceif (this.cache(req, res, statObj)) return; // Content-encoding :gzip res.setheader ()'Content-Type', mime.getType(p) + '; charset=utf8');
        let s = this.compress(req, res, p, statObj); // Range requestlet {start,end} = this.range(req,res,statObj);
        let rs = fs.createReadStream(p,{start,end})
        if (s) {
            rs.pipe(s).pipe(res);
        } else{ rs.pipe(res); }}Copy the code

On the command line tools perform curl -v — header “Range: bytes = 1-3” http://localhost:3000/index.html you can see the effect

You can see here

Git address

Just give it a thumbs up before you leave