The preface
Writing a static service provides a deeper understanding of the HTTP modules in Node, which is what we originally intended. I’m sure you’ve all used http-Server, so we’re going to implement something similar here. Function as follows
- After launching our module, type localhost:3000 to open the file in our public directory (index.html is open by default).
- The debug plug-in is mainly used to output some logs on the command line. We only use basic functions, so there are no difficulties. It doesn’t poke here
- The chalk plugin is likely to be used, which makes the command line output logs colorful and beautiful
The preparatory work
Our catalog is deconstructed as follows
- Start our service and automatically open public/index.html
- Bin /www.js is the configuration we will use to start the service from the command line later
- Public is our static directory
- App. Js master file
- Config.js configuration file
- Mppl.html is the template we compiled using EJS, which we will cover later
Start with the simplest config.js
let path = require('path');
let config = {
hostname:'127.0.0.1', // Default host port:3000, // default port dir:path.join(__dirname,' '.'public') // Default open directory (absolute path)}; module.exports = config;Copy the code
The above code can understand, let’s start to write our main file
The core code app.js
1. Introduce the required dependency packages
let http = require('http');
let url = require('url');
let path = require('path');
let util = require('util');
let fs = require('fs');
let zlib = require('zlib');
let mime = require('mime'); // Get the content typelet debug = require('debug') (The '*'); // Printout controls output based on environment variableslet chalk = require('chalk'); / / a piece of chalklet ejs = require('ejs'); // Template engine //let config = require('./config');
let stat= util.promisify(fs.stat); //promise the fs.stat methodlet readdir = util.promisify(fs.readdir);
let template = fs.readFileSync(path.join(__dirname,'tmpl.html'),'utf8'); // Read the ejS template fileCopy the code
- Mime parses the file to give you the content type, usage
- Ejs rendering engine, we use the simplest function, will not be able to understand
2. Enable the HTTP service
/* Run conditions specify host name * specify port number to start * Specify directory to run */let config = require('./config'); // Import the configuration file class Server {// declaration classconstructor() { this.config = config; } handleRequest(req,res){make sure this is an instance}start(){// The method to start the servicelet server =http.createServer(this.handleRequest.bind(this));
let{hostname,port} = this.config; Server.listen (port,hostname); debug(`http://${hostname}:${port}Start ') // Prints}} // to start a serviceletserver = new Server(); server.start(); // Call the start methodCopy the code
As of now, the simple service is enabled, so let’s test the effect
Perfect. The console prints out the content,
3. Implement the handleRequest method, which handles the request logic
List what we’re going to do
- Parse the pathname of the URL
- The path is spliced with the default path (G:// cpG-server /public)
- Check whether it’s file or folder or 404
let stat= util.promisify(fs.stat); Stat async handleRequest(req,res){// Make sure this is an instancelet {pathname} = url.parse(req.url,true); // Get the path to the URLletp = path.join(this.config.dir,pathname); // public: G:/ cpp-server /public :// cpp-server /public/index. HTML //let statObj=await stat(p); }catch (e) {this.senderror (req,res,e)}}Copy the code
Try catch is used to catch errors. When the file does not exist, call sendError and implement the error handling method first
File does not exist logic, sendError()
sendError(req,res,e){ debug(util.inspect(e)); // The util module provides the method res.statuscode = 404; res.end('Not Found');
}
Copy the code
Now that I’ve written so much, let’s test the error file to see if I can print an error
Test perfect, at this point we should determine whether to open a file or directory, and give the corresponding method, let’s start the directory rendering method
Ejs render directory list
- Declare a template template and mount it to the instance
let template = fs.readFileSync(path.join(__dirname,'tmpl.html'),'utf8'); // Read the ejS template file class Server{constructor(){this.template = template // mount to instance}}Copy the code
- If it is a directory, render an HTML page to show the directory structure
if(statObj.isdirectory ()){// To list the contents of the directory, clicklet dirs= await readdir(p); //public directory structure =>[index.html,style.css]dirs =dirs.map(dir=>{
return {
filename:dir,
path:path.join(pathname,dir)
}
});
//dirsIs to render data / / format is as follows [{filename: index. HTML, path:'/index.html'},{{filename:style.css,path:' '/style.css}}]
let str =ejs.render(this.template,{dirs}); // ejS render method // console.log(STR); res.setHeader('Content-Type'.'text/html; charset=utf-8');
res.end(str);
}
Copy the code
- Let’s take a look at the mppl. HTML template, and the EJS usage, and we’re only using the simplest one, so we should be able to understand it, right
<! DOCTYPE html> <html lang="en">
<head>
<meta charset="UTF-8"> <title> title </title> </head> <body> // loopdirs<% dirs.map(item=>{%> <li><a href="<%=item.path%>"><%=item.filename%></a></li>
<%})%>
</body>
</html>
Copy the code
Render directory structure, we’ve written it, let’s test it and see if it works
So far, no bugs, the next implementation if it is a file, directly render the content of the file
6. File rendering method, this.sendFile() method
sendFile(req,res,p,statObj){
res.setHeader('Content-Type', mime.getType(p) + '; charset=utf-8'); fs.createReadStream(p).pipe(res); // pipe to writable stream}Copy the code
Function has been achieved pull, do not believe we test
- The functionality is already there, but we asked for three more features
- 1. Check whether caching is supported
- 2. Check whether compression is supported
- 3. Check whether range requests are supported
6.1 Adding the Cache Function
- Modify the sendFile() method to add three functions
sendFile(req,res,p,statObj){// check whether there is a cacheif(this.cache(req,res,p,statObj)){// If there is a cache res.statusCode = 304; res.end();return} //2. Check whether compression is supported.... // check whether there is a range request.... }Copy the code
- Cache () Cache method
There are two types of caching, mandatory caching and negotiated caching
- Enforce caching of server catch-control and Expires
- Negotiate last-Modified and Etag for the cache server
- Negotiation Cache client if-modified-since if-none-match matches the server
- Post baidu cache deconstruction for you to see
I think you get the idea after looking at this picture. Let’s start writing the cache method
cache(req,res,p,statObj){// Implement caching /* Force caching server cache-control Expires negotiation Caching server Last-Modified Etag negotiation caching clientif-modified-since if-none-match etag ctime + file size last-Modified ctime Mandatory cache */ res.setheader ('Cache-Control'.'no-cache');
res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toGMTString()); // Resend the request after 10 secondslet etag = statObj.ctime.toGMTString() + statObj.size; // File modification time and file sizelet lastModified = statObj.ctime.toGMTString(); // File modification time res.setheader ('Etag', etag);
res.setHeader('Last-Modified', lastModified);
let ifNoneMatch = req.headers['if-none-match'];
let ifModifiedSince = req.headers['if-modified-since'];
if(etag ! =ifNoneMatch) {// Not equal, do not cachereturn false;
}
if(lastModified ! =ifModifiedSince) {// Same logicreturn false;
}
return true; // if not, go to cache}Copy the code
Now that the caching function is done, let’s test whether the header set is added
We’ve already implemented caching
6.2 Enabling compression
- Zlib provides compression in Node
gzip(req,res,p,statObj){// Client accept-encoding: gzip, deflate, br // Server content-encoding: gziplet encoding = req.headers['accept-encoding']; // Get the received compression format of the request headerif (encoding) {
if (encoding.match(/\bgzip\b/)) {
res.setHeader('Content-Encoding'.'gzip')
returnzlib.createGzip(); // Return a gzip stream}else if (encoding.match(/\bdeflate\b/)) {
res.setHeader('content-encoding'.'deflate');
returnzlib.createDeflate(); // Return createDeflate compressed stream}else {
return false; // Compression is not supported otherwise}}else {
return false; // Compression is not supported otherwise}}Copy the code
- Modify the sendFile() method
sendFile(req,res,p,statObj){// check whether there is a cacheif(this.cache(req,res,p,statObj)){// If there is a cache res.statusCode = 304; res.end();return} //2, check whether compression res.setheader () is supported"Content-Type",mime.getType(p)+"; charset=utf8");
let compress =this.gzip(req,res,p,statObj);
if(compress){// Checks for compression. It returns a compressed streamreturn fs.createReadStream(p).pipe(compress).pipe(res);
}else{// No compression is supportedreturnFs.createreadstream (p).pipe(res)} //3, check whether there is a range request.... }Copy the code
Use the 1.txt file to test
So far, so good, but there’s one last feature left, which implements scope requests
6.3 Implement the range request function
- Range:bytes=0-3
- Accept-range :bytes Content-range :bytes 0-3/ XXX Content-Length: XXX
Since there may be compression and scope requests at the same time, let’s change the previous code slightly
sendFile(req,res,p,statObj){// 1. Check whether there is a cache.... //2, check whether compression is supported and add range request res.setheader ("Content-Type",mime.getType(p)+"; charset=utf8");
let compress =this.gzip(req,res,p,statObj);
let {start,end} = this.range(req,res,p,statObj); // Deconstruct the start and end positionsif(compress){// Checks for compression. It returns a compressed streamreturn fs.createReadStream(p,{start,end}).pipe(compress).pipe(res);
}else{
// res.setHeader("Content-Type",mime.getType(p)+"; charset=utf8");
return fs.createReadStream(p,{start,end}).pipe(res)
}
}
Copy the code
- Range () The method of the request
range(req, res, statObj, p) {// Client Range:bytes=0-3 // Server accept-range :bytes Content-range :bytes 0-3/8777let range = req.headers['range']; // If there are scope requestsif (range) {
let[, start, end] = range.match(/(\d*)-(\d*)/); Start = start? Number(start) : 0; //start sets the default value end = end? Number(end) :statObj.size - 1; //end Sets the default value res.statuscode = 206; // Request res.setheader ('Accept-Ranges'."bytes");
res.setHeader('Content-Length',end-start+1);
res.setHeader('Content-Range',`bytes ${start}-${end}/${statObj.size}`);
return {start,end};
}else {
return {start:0, end:statObj.size}; }}Copy the code
Curl = curl = curl = curl = curl = curl
- 1. The content of TXT 123456789. We only want the first four characters
The test is perfect. Next we want to implement cGP-server, automatically open the browser, open the directory. We need to reference a module called yargs
7. Yargs module is configured with command line input
- Yargs configuration usage ‘
- Here we only use the most basic usage, a look to understand, see the official website for details
7.1 Functions of NPM Link
- The NPM link command links an NPM package anywhere to the global execution environment so that it can be run directly from the command line anywhere.
7.2 Modifying the Package. json File
7.3 Configuration at www.js
7.3.1 We export the main app.js file to www.js for use
7.3.2 Modifying the www.js file
#! /usr/bin/env node // The bin/www.js command is executed
const yargs = require('yargs');
let argv = yargs.option('port',{// basic usage of yargsalias: 'p', // alias default: 3000, // Default value description:'this is port'// demand:false// Is it necessary to}).option('hostname', {alias: 'h',
default: 'localhost',
description:'this is hostname',
demand:false
}).option('dir', {alias: 'd',
default: process.cwd(),
description:'this is cwd',
demand:false
}).usage('cgp-server [options]').argv; // Start the servicelet Server = require('.. /src/app.js'); new Server(argv).start(); // Check whether the platform is Windows or MAClet platform = require('os').platform(); // Start the child processlet {exec} = require('child_process'); Win32 / / win systemif(platform==="win32") {exec(`start http://${argv.hostname}:${argv.port}`)}else {
exec(`open http://${argv.hostname}:${argv.port}`)}Copy the code
- A brief introduction to yargs usage. Then we type CPP-server –help to see the effect
yargs.option('port',{// basic usage of yargsalias: 'p', // alias default: 3000, // Default value description:'this is port'// demand:false// Must})Copy the code
- To explain the process, start the service, then determine the system, and then automatically open the browser based on the platform
Let’s test it and see if it works
At the end
If you can see here, it’s really not easy, click a “like” and then go, share the source code with you