A Node static resource server that includes file caching, transport compression, EJS template engine, MIME type matching, and other functions. It uses Node’s built-in modules to access resources through links.

Create an HTTP Server

The HTTP module of Node provides the HTTP server and client interface, used by require(‘ HTTP ‘).

Start by creating a simple HTTP server. Set the following parameters:

// server/config.js
module.exports = {
  root: process.cwd(),
  host: '127.0.0.1'.port: '8877'
}
Copy the code

The process.cwd() method returns the current working directory of the Node.js process.

The Node server calls http.createserver () every time it receives an HTTP request. Each time it receives a request, it parses the request header as part of a new request and triggers the callback functions with the new Request and Respond objects. Create a simple HTTP service with a default response status of 200:

// server/http.js
const http = require('http')
const path = require('path')

const config = require('./config')

const server = http.createServer((request, response) = > {
  let filePath = path.join(config.root, request.url)
  response.statusCode = 200
  response.setHeader('content-type'.'text/html')
  response.write(`<html><body><h1>Hello World! </h1><p>${filePath}</p></body></html>`)
  response.end()
})

server.listen(config.port, config.host, () => {
  const addr = `http://${config.host}:${config.port}`
  console.info(`server started at ${addr}`)})Copy the code

The address of a client requesting a static resource can be obtained via request.url, and then the path module is used to concatenate the path of the resource.

$node server/http.js will display this path to any address after accessing http://127.0.0.1:8877/ :

You need to restart the server for update every time you modify the response content of the server. It is recommended that you update the supervisor plug-in that restarts automatically and start the server using the Supervisor.

$ npm install supervisor -D
$ supervisor server/http.js
Copy the code

2. Use FS to read resource files

Our goal is to build a static resource server that we want to retrieve when accessing a resource file or directory. To do this, use Node’s built-in FS module to read the static resource file.

Use fs.stat() to read file status information, determine whether a file is a directory or stats.isfile () in the callback, and use fs.readdir() to read the file name in the directory

// server/route.js
const fs = require('fs')

module.exports = function (request, response, filePath){
  fs.stat(filePath, (err, stats) => {
    if (err) {
      response.statusCode = 404
      response.setHeader('content-type'.'text/plain')
      response.end(`${filePath} is not a file`)
      return;
    }
    if (stats.isFile()) {
      response.statusCode = 200
      response.setHeader('content-type'.'text/plain')
      fs.createReadStream(filePath).pipe(response)
    } 
    else if (stats.isDirectory()) {
      fs.readdir(filePath, (err, files) => {
        response.statusCode = 200
        response.setHeader('content-type'.'text/plain')
        response.end(files.join(', '))})}})}Copy the code

Where fs.createreadstream () reads the file stream, and pipe() reads the file into memory in segments, optimizing for high concurrency.

Modify the previous HTTP server to introduce the newly created route.js function as a response function:

// server/http.js
const http = require('http')
const path = require('path')

const config = require('./config')
const route = require('./route')

const server = http.createServer((request, response) = > {
  let filePath = path.join(config.root, request.url)
  route(request, response, filePath)
})

server.listen(config.port, config.host, () => {
  const addr = `http://${config.host}:${config.port}`
  console.info(`server started at ${addr}`)})Copy the code

$node server/http.js

If it is a file, print it directly:

Mature static resource server Anywhere, in-depth understanding of nodeJS author written.

Util. Promisify Optimize FS asynchronously

We notice that both fs.stat() and fs.readdir() have callback callbacks. Instead of a hell callback, we use Node util.promisify() as a chained operation.

Util.promisify () simply returns a Promise instance to facilitate asynchronous operations, and can be used with async/await to modify the fs operation code in route.js:

// server/route.js
const fs = require('fs')
const util = require('util')

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

module.exports = async function (request, response, filePath) {
  try {
    const stats = await stat(filePath)
    if (stats.isFile()) {
      response.statusCode = 200
      response.setHeader('content-type'.'text/plain')
      fs.createReadStream(filePath).pipe(response)
    }
    else if (stats.isDirectory()) {
      const files = await readdir(filePath)
      response.statusCode = 200
      response.setHeader('content-type'.'text/plain')
      response.end(files.join(', '))}}catch (err) {
    console.error(err)
    response.statusCode = 404
    response.setHeader('content-type'.'text/plain')
    response.end(`${filePath} is not a file`)}}Copy the code

Because both fs.stat() and fs.readdir() can return error, try-catch is used.

Note that an asynchronous callback returns an async operation with await. An async callback returns a promise without await, and await must be used inside async.

Add EJS template engine

From the above example, enter the file path manually and then return the resource file. Now optimize the example by turning the file directory into an HTML A link that returns to the file resource.

Using Response.write () to insert HTML tags in the first example is obviously unfriendly. At this point, use a template engine to concatenate HTML.

There are many common template engines, ejS, Jade, Handlebars, here using EJS:

npm i ejs
Copy the code

Create a new template SRC /template/index.ejs, similar to the HTML file:


      
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Node Server</title>
</head>
<body>
<% files.forEach(function(name) {% >
  <a href=".. /<%= dir %>/<%= name %>"> <% = name% ></a><br>
<%}) % >
</body>
</html>
Copy the code

Modify route.js again, add ejS template and ejs.render(), pass files, dir, etc in file directory code:

// server/route.js
const fs = require('fs')
const util = require('util')
const path = require('path')
const ejs = require('ejs')
const config = require('./config')
// Asynchronous optimization
const stat = util.promisify(fs.stat)
const readdir = util.promisify(fs.readdir)
// Import templates
const tplPath = path.join(__dirname,'.. /src/template/index.ejs')
const sourse = fs.readFileSync(tplPath) // 读出来的是buffer

module.exports = async function (request, response, filePath) {
  try {
    const stats = await stat(filePath)
    if (stats.isFile()) {
      response.statusCode = 200...}else if (stats.isDirectory()) {
      const files = await readdir(filePath)
      response.statusCode = 200
      response.setHeader('content-type'.'text/html')
      // response.end(files.join(','))

      const dir = path.relative(config.root, filePath) // Relative to the root directory
      const data = {
        files,
        dir: dir ? `${dir}` : ' ' // path.relative may return an empty string ()
      }

      const template = ejs.render(sourse.toString(),data)
      response.end(template)
    }
  } catch (err) {
    response.statusCode = 404...}}Copy the code

Restart $node server/http.js to see the link to the file directory:

Match the MIME type of the file

Static resources include images, CSS, JS, JSON, HTML, etc. The content-type of the response header is set to text/plain after checking stats.isfile (), but various files have different Mime types.

We start by matching the MIME type of the file with its suffix:

// server/mime.js
const path = require('path')
const mimeTypes = {
  'js': 'application/x-javascript'.'html': 'text/html'.'css': 'text/css'.'txt': "text/plain"
}

module.exports = (filePath) = > {
  let ext = path.extname(filePath)
    .split('. ').pop().toLowerCase() // Take the extension

  if(! ext) {// If there is no extension, such as file
    ext = filePath
  }
  return mimeTypes[ext] || mimeTypes['txt']}Copy the code

Match the MIME Type of the file and set the response header with response.setheader (‘ content-type ‘, ‘XXX’) :

// server/route.js
const mime = require('./mime')...if (stats.isFile()) {
      const mimeType = mime(filePath)
      response.statusCode = 200
      response.setHeader('Content-Type', mimeType)
      fs.createReadStream(filePath).pipe(response)
    }
Copy the code

Run the server to access a file and see that the content-Type has been changed:

Six, file transfer compression

Note that the request header contains Accept — Encoding: gzip, deflate, which tells the server about the compression mode supported by the client. The response header uses content-encoding to indicate the compression mode of the file.

The built-in Zlib module of Node supports file compression. Previously the file was read using fs.createreadstream (), so the compression is on the ReadStream file stream. Examples of gzip, Deflate compression:

// server/compress.js
const  zlib = require('zlib')

module.exports = (readStream, request, response) = > {
  const acceptEncoding = request.headers['accept-encoding']
  
  if(! acceptEncoding || ! acceptEncoding.match(/\b(gzip|deflate)\b/)) {
    return readStream
  }
  else if (acceptEncoding.match(/\bgzip\b/)) {
    response.setHeader("Content-Encoding".'gzip')
    return readStream.pipe(zlib.createGzip())
  }
  else if (acceptEncoding.match(/\bdeflate\b/)) {
    response.setHeader("Content-Encoding".'deflate')
    return readStream.pipe(zlib.createDeflate())
  }
}
Copy the code

Modify the route.js file read code:

// server/route.js
const compress = require('./compress')...if (stats.isFile()) {
      const mimeType = mime(filePath)
      response.statusCode = 200
      response.setHeader('Content-Type', mimeType)
      
      // fs.createReadStream(filePath).pipe(response)
+     let readStream = fs.createReadStream(filePath)
+     if(filePath.match(config.compress)) { / / regular matching: / \. | | js | CSS md (HTML) /
        readStream = compress(readStream,request, response)
      }
      readStream.pipe(response)
    }
Copy the code

Running server, you can see that not only the response header has been added to the compression flag, but also the resource size of 3K has been compressed to 1K, the effect is obvious:

7. Resource cache

All of the above Node services are first requested by the browser or no cache state, so if the browser/client requests the resource, an important front-end optimization point is to cache the resource on the client side. Caches include strong caches and negotiated caches:

Strongly cached fields in the Request Header are Expires and cache-Control. If within the validity period, the cache resource is directly loaded, and the status code is displayed as 200.

The negotiation cached fields in the Request Header are:

  • If-modified-since (Respond Header last-modified)
  • If-none — Match (Respond Header Etag)

If the negotiation succeeds, the 304 status code is returned, the expiration time is updated, and the browser local resources are loaded. Otherwise, the server resource file is returned.

First configure the default cache field:

// server/config.js
module.exports = {
  root: process.cwd(),
  host: '127.0.0.1'.port: '8877'.compress: /\.(html|js|css|md)/.cache: {
    maxAge: 2.expires: true.cacheControl: true.lastModified: true.etag: true}}Copy the code

Create a new server/cache.js and set the response header:

const config = require('./config')
function refreshRes (stats, response) {
  const {maxAge, expires, cacheControl, lastModified, etag} = config.cache;

  if (expires) {
    response.setHeader('Expires', (new Date(Date.now() + maxAge * 1000)).toUTCString());
  }
  if (cacheControl) {
    response.setHeader('Cache-Control', `public, max-age=${maxAge}`);
  }
  if (lastModified) {
    response.setHeader('Last-Modified', stats.mtime.toUTCString());
  }
  if (etag) {
    response.setHeader('ETag', `${stats.size}-${stats.mtime.toUTCString()}`); }} module.exports = // mtime needs to be converted to a string, otherwise an error is reported in Windowsfunction isFresh (stats, request, response) {
  refreshRes(stats, response);

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

  if(! lastModified && ! etag) {return false;
  }
  if(lastModified && lastModified ! == response.getHeader('Last-Modified')) {
    return false;
  }
  if(etag && etag ! == response.getHeader('ETag')) {
    return false;
  }
  return true;
};
Copy the code

Finally, modify route.js

// server/route.js
+ const isCache = require('./cache')

   if (stats.isFile()) {
      const mimeType = mime(filePath)
      response.setHeader('Content-Type', mimeType)

+     if (isCache(stats, request, response)) {
        response.statusCode = 304;
        response.end();
        return;
      }
      
      response.statusCode = 200
      // fs.createReadStream(filePath).pipe(response)
      let readStream = fs.createReadStream(filePath)
      if(filePath.match(config.compress)) {
        readStream = compress(readStream,request, response)
      }
      readStream.pipe(response)
    }
Copy the code

Restart the Node server, access a file, and Respond when the first request succeeds. Respond Header Returns the cache time:

Request Header sends the negotiation Request field:


That’s a simple Node static resource server. Clone the project on my github NodeStaticServer