Native node writes a static resource server

myanywhere

Make a simple castration version of anywhere static resource server with native node to improve understanding of node and http.

Relevant knowledge

  • es6 and es7 syntax

  • http-related network knowledge

    • Response Header
    • Cache correlation
    • Compression correlation
  • path module

    • path.join stitching path
    • path.relative
    • path.basename
    • path.extname
  • http module

  • fs module

    • fs.stat function

      Use the fs.stat function to get stats to get parameters for a file or folder

      • stats.isFile determines whether it is a folder
    • fs.createReadStream(filePath).pipe(res)

      File readable streams for more efficient reading

    • fs.readdir

    • ...

  • promisify

    • async await

1. Implement reading files or folders

const http= require('http')
const conf = require('./config/defaultConfig')
const path = require('path')
const fs = require('fs')

const server = http.createServer((req, res) => {
  const filePath = path.join(conf.root, req.url)
  // http://nodejs.cn/api/fs.html#fs_class_fs_stats
  fs.stat(filePath, (err, stats) => {
    if (err) {
      res.statusCode = 404
      res.setHeader('Content-text', 'text/plain')
      res.end(`${filePath} is not a directoru or file`)
    }
    // If it is a file
    if (stats.isFile()) {
      res.statusCode = 200
      res.setHeader('Content-text', 'text/plain')
      fs.createReadStream(filePath).pipe(res)
    } else if (stats.isDirectory()) {
      fs.readdir(filePath, (err, files) => {
        res.statusCode = 200
        res.setHeader('Content-text', 'text/plain')
        res.end(files.join(','))
      })
    }
  })
})

server.listen(conf.port, conf.hostname, () => {
  const addr = `http:${conf.hostname}:${conf.port}`
  console.info(`run at ${addr}`)
})

2. async await asynchronous modification

To avoid multilevel callbacks, we use jsasync and await to transform our code

router.js

Pull logic-related code out of app.js into router.js and develop in modules

const fs = require('fs')
const promisify = require('util').promisify
const stat = promisify(fs.stat)
const readdir = promisify(fs.readdir)

module.exports = async function (req, res, filePath) {
  try {
    const stats = await stat(filePath)
    if (stats.isFile()) {
      res.statusCode = 200
      res.setHeader('Content-text', 'text/plain')
      fs.createReadStream(filePath).pipe(res)
    } else if (stats.isDirectory()) {
      const files = await readdir(filePath)
      res.statusCode = 200
      res.setHeader('Content-text', 'text/plain')
      res.end(files.join(','))
    }
  } catch (error) {
    res.statusCode = 404
    res.setHeader('Content-text', 'text/plain')
    res.end(`${filePath} is not a directoru or file`)
  }
}

app.js

const http= require('http')
const conf = require('./config/defaultConfig')
const path = require('path')
const route = require('./help/router')

const server = http.createServer((req, res) => {
  const filePath = path.join(conf.root, req.url)
  route(req, res, filePath)
})

server.listen(conf.port, conf.hostname, () => {
  const addr = `http:${conf.hostname}:${conf.port}`
  console.info(`run at ${addr}`)
})

3. Perfect clickable

The work above already allows us to see the folder directory on the page, but it is text and not clickable

Rendering with handlebars

  • Reference handlebars

    const Handlebars = require('handlebars')
    
  • Create template 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>{{title}}</title>
      <style>
        body {
          margin: 10px
        }
        a {
          display: block;
          margin-bottom: 10px;
          font-weight: 600;
        }
      </style>
    </head>
    <body>
      {{#each files}}
        <a href="{{../dir}}/{{file}}">{{file}}</a>
      {{/each}}
    </body>
    </html>
    
    

    ‚Äč

  • router.js configuration

    Use absolute paths when referencing

    const tplPath = path.join(__dirname, '../template/dir.html')
    const source = fs.readFileSync(tplPath, 'utf8')
    const template = Handlebars.compile(source)
    
  • Create data

    ....
    module.exports = async function (req, res, filePath) {
      try {
       ...
        } else if (stats.isDirectory()) {
          const files = await readdir(filePath)
          res.statusCode = 200
          res.setHeader('Content-text', 'text/html')
          const dir = path.relative(config.root, filePath)
          const data = {
            // The path.basename() method returns the last part of a path
            title: path.basename(filePath),
            // path.relative('/data/orandea/test/aaa',      '/data/orandea/impl/bbb');
            // Return:'. /. /impl/bbb'
            dir: dir ? `/${dir}` : '',
            files
          }
          console.info(files)
          res.end(template(data))
        }
      } catch (error) {
    ...
      }
    }
    
    

4. mime

New mime.js file

const path = require('path')

const mimeTypes = {
    ....
}
module.exports = (filePath) => {
  let ext = path.extname(filePath).toLowerCase()

  if (!ext) {
    ext = filePath
  }

  return mimeTypes[ext] || mimeTypes['.txt']
}

mine.js returns the corresponding mime based on the file suffix name

5. Compressed Pages Optimize Performance

stream compression for read

Add compress item in defaultConfig.js

module.exports = {
  // The process.cwd() path can change as the execution path changes
  // The process cwd() method returns the directory where the Node.js process is currently working.
  root: process.cwd(),
  hostname: '127.0.0.1',
  port: 9527,
  compress: /\.(html|js|css|md)/
}

Write compression processing compress

const {createGzip, createDeflate} = require('zlib')
module.exports = (rs, req, res) => {
  const acceptEncoding = req.headers['accept-encoding']
  if (!acceptEncoding || !acceptEncoding.match(/\b(gzip|deflate)\b/)) {
    return
  } else if (acceptEncoding.match(/\bgzip\b/)) {
    res.setHeader('Content-Encoding', 'gzip')
    return rs.pipe(createGzip())
  } else if (acceptEncoding.match(/\bdeflate\b/)) {
    res.setHeader('Content-Encoding', 'deflate')
    return rs.pipe(createGzip())
  }
}

/*
 match() Method can retrieve a specified value within a string or find a match for one or more regular expressions.
 This method is similar to indexOf() and lastIndexOf(), but it returns the specified value, not the position of the string.
 */

Changes to read files in router.js

...
let rs = fs.createReadStream(filePath)
if (filePath.match(config.compress)) {
    rs = compress(rs, req, res)
  }
rs.pipe(res)

After compressing the file results, the compression rate can be up to 70%

6. Processing the cache

Approximate Caching Principles

User Request Local Cache--no-->Request Resources-->Negotiate Cache Return Response

User requests local cache--yes-->Determine if the swap is valid--valid-->local cache--invalid-->Negotiate cache return response

Cache header

  • expires old is not used now
  • Cache-Control relative to last request time
  • If-Modified-Since / Last-Modified
  • If-None-Match / ETag

cache.js

const {cache} = require('../config/defaultConfig')
function refreshRes(stats, res) {
  const { maxAge, expires, cacheControl, lastModified, etag } = cache
  if (expires) {
    res.setHeader('Expores', (new Date(Date.now() + maxAge * 1000)).toUTCString())
  }
  if (cacheControl) {
    res.setHeader('Cache-Control', `public, max-age=${maxAge}`)
  }
  if (lastModified) {
    res.setHeader('Last-Modified', stats.mtime.toUTCString())
  }
  if (etag) {
    res.setHeader('ETag', `${stats.size}-${stats.mtime.toUTCString()}`)
  }
}

module.exports = function isFresh(stats, req, res) {
  refreshRes(stats, res)

  const lastModified = req.headers['if-modified-since']
  const etag = req.headers['if-none-match']

  if (!lastModified && !etag) {
    return false
  }
  if (lastModified && lastModified !== res.getHeader('Last-Modified')) {
    return false
  }
  if (etag && res.getHeader('ETag').indexOf(etag) ) {
    return false
  }
  return true
}


router.js

// If the file is fresh and unchanged, set the response header to return directly
if (isFresh(stats, req, res)) {
      res.statusCode = 304
      res.end()
      return
    }

7. Open browser automatically

Write openUrl.js

const {exec} = require('child_process')

module.exports = url => {
  switch (process.platform) {
  case 'darwin':
    exec(`open ${url}`)
    break

  case 'win32':
    exec(`start ${url}`)
  }
}

Only Windows and mac systems are supported

Use in app.js

server.listen(conf.port, conf.hostname, () => {
  const addr = `http:${conf.hostname}:${conf.port}`
  console.info(`run at ${addr}`)
  openUrl(addr)
})

summary

domo is not difficult, but it involves a lot of bits of knowledge, a better understanding of the underlying node, and a sense of its power in handling network requests. In addition, the new syntax of es6 and es7 is very strong, so you have to do more work in the future.

Tags: Handlebars encoding network IE

Posted on Tue, 21 Apr 2020 15:01:05 -0400 by SunsetKnight