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(`$ 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:$:$` console.info(`run at $`) })
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(`$ 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:$:$` console.info(`run at $`) })
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> <style> body { margin: 10px } a { display: block; margin-bottom: 10px; font-weight: 600; } </style> </head> <body> {{#each files}} <a href="{{../dir}}/{}">{}</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 ? `/$` : '', 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 = 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 = 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=$`) } if (lastModified) { res.setHeader('Last-Modified', stats.mtime.toUTCString()) } if (etag) { res.setHeader('ETag', `$-$`) } } 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 = require('child_process') module.exports = url => { switch (process.platform) { case 'darwin': exec(`open $`) break case 'win32': exec(`start $`) } }
Only Windows and mac systems are supported
Use in app.js
server.listen(conf.port, conf.hostname, () => { const addr = `http:$:$` console.info(`run at $`) 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.