This article is participating in node.js advanced technology essay, click to see details.

Live – server believe that a lot of friends all not strange, should be used in the library, it is a development environment to support real-time refresh ability server, similar functionality and like HTTP server | – anywhere, etc. Why are we talking about this library? Because it’s so small… ugh… and fucking SAO!!

The core function is hot update, source code a total of 600 lines or so, the core implementation of only 100 lines, involved in the skills are very very much, such as the following 👇 these:

  • How to write Node scripts?
  • How are Node services built to support the middleware model?
  • How to set up Node static file hosting service?
  • How to intercept a Stream for resource injection?
  • How does cross-end support appeal to browsers (or other applications)?
  • Delayed initialization of one-to-one WS services?
  • How do I listen for changes in resource content?

If you are interested in the above content and listen to me continue, all dry goods, no idle talk. Let’s take a look at the basic usage of live-server:

This command can be used as a command line after the global installation, for example:

# terminal input
live-server
Copy the code

A service is started and a browser is automatically opened to access the current static resource. At the same time, monitor static file content changes in the current directory, and refresh the browser in real time.

Now that we know how to use it, let’s take a look at how it works, based on version 1.1.2. The first thing you can see from the bin field in package.json is that the entry file for the current script is live-server.js:

{
  "bin": {
    "live-server": "./live-server.js"}},Copy the code

Script entrancelive-server.jsThe implementation of the

The first code defines the script execution environment as Node:

#! /usr/bin/env node

var path = require('path');
var fs = require('fs');
var assign = require('object-assign');
var liveServer = require("./index");
Copy the code

This is followed by parsing the command parameters from the node command we entered. For example, we typed the following command:

# terminal input
live-server --port=3000 --host=http://localhost
Copy the code

The specific analytical logic is as follows:

var opts = {
	host: process.env.IP,
	port: process.env.PORT,
	open: true.mount: [].proxy: [].middleware: [].logLevel: 2};// Obtain the.live-server.json file in the system account root folder (equivalent to os.homedir())
var homeDir = process.env[(process.platform === 'win32')?'USERPROFILE' : 'HOME'];
var configPath = path.join(homeDir, '.live-server.json');

// If the file exists, read the json content of the file and merge the parameters
if (fs.existsSync(configPath)) {
	var userConfig = fs.readFileSync(configPath, 'utf8');
	assign(opts, JSON.parse(userConfig));
	if (opts.ignorePattern) opts.ignorePattern = new RegExp(opts.ignorePattern);
}

The first argument is node's execution context, the second argument is the address of the script to execute, and the third and subsequent arguments are command arguments */
for (var i = process.argv.length - 1; i >= 2; --i) {
  var arg = process.argv[i];
  // Parse the port number
  if (arg.indexOf("--port=") > -1) {
	var portString = arg.substring(7);
	var portNumber = parseInt(portString, 10);
	  if (portNumber === +portString) {
		opts.port = portNumber;
		process.argv.splice(i, 1); }}// Resolve the host address
  else if (arg.indexOf("--host=") > -1) {
    opts.host = arg.substring(7);
	process.argv.splice(i, 1);
  }
  // Omit other else if code
  // This section is the same as above, all the other parameters of the parse command
  / /...
}
Copy the code
  • Check whether the user is in the root directory.live-server.jsonFile, parses json content as the default configuration
  • fromprocess.argvRead all command arguments and merge them with the default arguments. The value is an array, and the first entry in the array isnodeThe second item is the address of the script to execute, and the following items are all subsequent parameters.
  • After getting the default parameters, the call beginsserverThe real implementation, and passing the parameters in, looks like this:
liveServer.start(opts);
Copy the code

The realization of the liveServer

The liveServer implementation is in index.js:

// Read the contents of injected. HTML
// The content is actually a piece of Websocket code, used to communicate with the service
var INJECTED_CODE = fs.readFileSync(path.join(__dirname, "injected.html"), "utf8");
Copy the code

I first read the contents of the injected. HTML file at the root of the library and assigned the contents to a variable for later use. The content of this file is a piece of websocket code stored. The function of this code is to inject the code when accessing resources such as HTML, which can be executed to connect and communicate with the service ws while the HTML file is running. Take a look at this injected. HTML first:

<! -- Code injected by live-server -->
<script type="text/javascript">
	/ / 
       
	if ('WebSocket' in window) {(function() {
			function refreshCSS() {
				var sheets = [].slice.call(document.getElementsByTagName("link"));
				var head = document.getElementsByTagName("head") [0];
				for (var i = 0; i < sheets.length; ++i) {
					var elem = sheets[i];
					head.removeChild(elem);
					var rel = elem.rel;
					if (elem.href && typeofrel ! ="string" || rel.length == 0 || rel.toLowerCase() == "stylesheet") {
						var url = elem.href.replace(/ (& | \? _cacheOverride=\d+/.' ');
						elem.href = url + (url.indexOf('? ') > =0 ? '&' : '? ') + '_cacheOverride=' + (new Date().valueOf()); } head.appendChild(elem); }}var protocol = window.location.protocol === 'http:' ? 'ws://' : 'wss://';
			var address = protocol + window.location.host + window.location.pathname + '/ws';
			var socket = new WebSocket(address);
			socket.onmessage = function(msg) {
				if (msg.data == 'reload') window.location.reload();
				else if (msg.data == 'refreshcss') refreshCSS();
			};
			console.log('Live reload enabled.'); }) (); }/ /]] >
</script>
Copy the code

As you can see, the content of this file is a javascript script that does the following:

  • According to theurlAddress generates the WS service address to connect to
  • Initialize thewsThe connection
  • Listening to thewsService push message:
    • reloadMessage refreshes the current page
    • refreshcssMessages do insensitive CSS refreshes

Insensitive CSS refreshes by traversing all the stylesheet link tags in the head TAB, removing them one by one, and then re-inserting them, generating a new timestamp field to remove caching effects.

Next comes the main implementation of the service:

var LiveServer = {
	server: null.watcher: null.logLevel: 2
};

LiveServer.start = function(options) {}

LiveServer.shutdown = function() {}

module.exports = LiveServer;
Copy the code

You define an object and add the start and shutdown methods. Let’s look at the implementation of start:

// Start the server according to the options parameter
LiveServer.start = function(options) {
    options = options || {};
	/ / host address
	var host = options.host || '0.0.0.0';
	/ / the port number
	varport = options.port ! = =undefined ? options.port : 8080; // 0 means random
	// The entry to the script, which is the entry to the resource server to start
	var root = options.root || process.cwd();
	
	// Other default parameter Settings
	/ /...
	
	// Initialize a connect service
	var app = connect();
	
	/ /... Omit other logic-independent code such as log logic
	
	// Some middleware is loaded, for example
	// Add middleware for cORS cross-domain processing
	if (cors) {
		app.use(require("cors") ({origin: true.// reflecting request origin
			credentials: true // allowing requests with credentials
		}));
	}
	
	// Load the static file hosting service middleware etc
	app.use(staticServerHandler)
	
	var server, protocol;
	// If the user has set the HTTPS configuration
	if(https ! = =null) {
		var httpsConfig = https;
		// If the HTTPS parameter is a string, it is used as the configuration file path
		// Then load the file content as the parameter configuration for the HTTPS request
		if (typeof https === "string") {
			httpsConfig = require(path.resolve(process.cwd(), https));
		}
		// Create an HTTPS server
		server = require(httpsModule).createServer(httpsConfig, app);
		protocol = "https";
	} else {
		// Otherwise, HTTP is used by default
		server = http.createServer(app);
		protocol = "http"; }}Copy the code
  • The default values of the various parameters are assigned first
  • throughconnectThe library instantiates a middleware service
  • loadingcorsMiddleware, static file hosting service middleware, etc
  • Select a value based on user parametershttp/httpsservice
  • http/httpsService loading middleware model

The core code is as follows:

// Initialize a connect middleware service
var app = connect();

// Loads a lot of middleware
app.use(mideware1);
app.use(mideware2);
app.use(mideware3);

// Initialize the HTTP service and use middleware
var server = http.createServer(app);
Copy the code

How is the static file hosting service implemented?

// Create middleware for static file hosting services
var staticServerHandler = staticServer(root);

// Load the static file hosting service middleware etc
app.use(staticServerHandler) // Custom static server
	.use(entryPoint(staticServerHandler, file))
	.use(serveIndex(root, { icons: true }));
Copy the code

The specific static file hosting service is implemented in staticServer:

// Static file hosting service
function staticServer(root) {
	var isFile = false;
	try { // For supporting mounting files instead of just directories
		// Check whether the specified path is a file
		isFile = fs.statSync(root).isFile();
	} catch (e) {
		if(e.code ! = ="ENOENT") throw e;
	}
	// Return a middleware
	return function(req, res, next) {
		// Only GET and HEAD requests are processed
		if(req.method ! = ="GET"&& req.method ! = ="HEAD") return next();
		// Get the path part after the domain name, for example x.com/abc/def get/ABC /def
		// If isFile is true, null
		var reqpath = isFile ? "" : url.parse(req.url).pathname;
		varhasNoOrigin = ! req.headers.origin;var injectCandidates = [ new RegExp("</body>"."i"), new RegExp("</svg>"), new RegExp("</head>"."i")];
		var injectTag = null;

		// Use the send library to return static resources as the result of HTTP requests
		send(req, reqpath, { root: root })
			.on('error', error)
			.on('directory', directory)
			.on('file', file)
			.on('stream', inject)
			.pipe(res);
	};
}
Copy the code
  • staticServerFunction is a create function that creates a middleware function
  • If you areGET | HEADRequests are called directlynext()Execute the next middleware
  • usingsendLibraries treat static resources ashttpThe request result is returned
    • Parameter 1 is the current request object
    • Parameter 2 is the requested resource path
    • Parameter 3 specifies that the relative path of the requested resource is the current script root path or the user can specify root
  • usingsendThe library listens for events
    • The directory handler function is called when requesting that the resource is a folder
    • The file handler function is called when requesting that the resource is a file
    • Inject processing logic is invoked when the requested flow starts. This is the most critical part, where the WS code is injected

The following describes how to process each send event:

  • folder

When accessing a file, concatenate/directly after and then redirect the resource

// When a directory is requested
function directory() {
	var pathname = url.parse(req.originalUrl).pathname;
	res.statusCode = 301;
	res.setHeader('Location', pathname + '/');
	res.end('Redirecting to ' + escape(pathname) + '/');
}
Copy the code
  • File handler function
// When a file is requested
function file(filepath /*, stat*/) {
	var x = path.extname(filepath).toLocaleLowerCase(), match,
			possibleExtensions = [ "".".html".".htm".".xhtml".".php".".svg" ];
	if (hasNoOrigin && (possibleExtensions.indexOf(x) > -1)) {
		// TODO: Sync file read here is not nice, but we need to determine if the html should be injected or not
		var contents = fs.readFileSync(filepath, "utf8");
		for (var i = 0; i < injectCandidates.length; ++i) {
			match = injectCandidates[i].exec(contents);
			if (match) {
				injectTag = match[0];
				break; }}}}Copy the code

Main processing logic is according to the request of the path to the file suffix, determine whether. HTML. | HTM. | XHTML document type, such as it is read from the file content, through regular find file contents does it include the < / body > | < / head > characters, such as If it does, the file is ready to inject WS code. Here we just mark the injectTag variable; the real injection is done in the stream event.

  • streamStream start event
// Inject the socket script into the read target file stream
function inject(stream) {
	if (injectTag) {
		// We need to modify the length given to browser
		var len = INJECTED_CODE.length + res.getHeader('Content-Length');
		res.setHeader('Content-Length', len);
        // Save a reference to the original pipe
		var originalPipe = stream.pipe;
        // Override the original pipe method
		stream.pipe = function(resp) {
            // Re-invoke the PIPE method and invoke the event-stream module to inject the contents of the stream
            // The injected content is part of the WebSocket communication
			originalPipe.call(stream, es.replace(new RegExp(injectTag, "i"), INJECTED_CODE + injectTag)).pipe(resp); }; }}Copy the code

The processing logic is mainly done by overriding the PIPE method and then replacing the contents of the stream read, replacing the character with the WS code + to be injected, and updating the Content-Length value in the response header returned by the RES to the replaced Content Length.

The service listens and opens the browser

// Handle successful server
server.addListener('listening'.function(/*e*/) {
	LiveServer.server = server;

	var address = server.address();
	// @see https://www.cnblogs.com/wenwei-blog/p/12114184.html
	var serveHost = address.address === "0.0.0.0" ? "127.0.0.1" : address.address;
	var openHost = host === "0.0.0.0" ? "127.0.0.1" : host;

	var serveURL = protocol + ': / /' + serveHost + ':' + address.port;
	// The url service address of the opened application
	var openURL = protocol + ': / /' + openHost + ':' + address.port;

    // Omit the log section
    / /...


	// Launch browser
	// Use the open library to invoke the application to open the path
	// The user is not individually set to invoke the browser
	if(openPath ! = =null) {
		if (typeof openPath === "object") {
			openPath.forEach(function(p) {
				open(openURL + p, {app: browser});
			});
		} else {
			open(openURL + openPath, {app: browser}); }}});// Setup server to listen at port
// Listen for port number and host
server.listen(port, host);
Copy the code
  • throughlisteningEvent listenershttp/httpsService started successfully
  • Concatenate the address of the resource to be reached, that isopenPath
  • usingopenLibraries call up applications, and by default, browsers
  • Listen for port numbers andhostTo start running the service

Page resource and service communication connections

As we know from the above analysis, we inject WS code when the HTTP request for the stream resource returns, and the WS code automatically attempts to initiate a connection with our server service. The server’s handshake event is triggered, so we can initialize the WS service during the handshake and establish a one-to-one connection between the client and WS.

The one-to-one connection is established mainly for the convenience of communication and data isolation, and the WS service is initialized only when the client initiates the connection, because some resources will not inject the WS service, so there is no need for the connection.

The one-to-one connection is implemented through faye- webSocket.

// WebSocket
var clients = [];
// Listen for handshake events. Each socket connection corresponds to a socket service
// Use the Faye - webSocket library to implement one-to-one connections
server.addListener('upgrade'.function(request, socket, head) {
	var ws = new WebSocket(request, socket, head);
	// After successful WS initialization, a message is sent to the connected WS client
	// This message is not used by the client
	ws.onopen = function() {
	    ws.send('connected');
	};

    // Remove the cached instance when listening to the client shutdown
	ws.onclose = function() {
		clients = clients.filter(function (x) {
			returnx ! == ws; }); };// Cache the client instance
	clients.push(ws);
});
Copy the code

How do I listen for changes in resource content

After the ws connection is established between the client and the server, we need to listen for changes to the static resource content and notify the client resource to refresh after the changes:

// Setup file watcher
LiveServer.watcher = chokidar.watch(watchPaths, {
	ignored: ignored,
	ignoreInitial: true
});

// Resource change handler
function handleChange(changePath) {
	var cssChange = path.extname(changePath) === ".css" && !noCssInject;

	clients.forEach(function(ws) {
		if (ws) {
			ws.send(cssChange ? 'refreshcss' : 'reload'); }}); }// Listen for related change events
LiveServer.watcher
	.on("change", handleChange)
	.on("add", handleChange)
	.on("unlink", handleChange)
	.on("addDir", handleChange)
	.on("unlinkDir", handleChange)
	.on("error".function (err) {
		console.log("ERROR:".red, err);
	});
Copy the code
  • usingchokidarThe library listens for changes to file contents
  • If the file content changes, check whether the CSS file is changed
    • CSS changes, WS happensrefreshcssThe message
    • Otherwise WS sendsreloadThe message
  • The client responds differently to the message,reloadOr refresh without feelingcss

Close the service

// Disable watcher resource content monitoring and disable server services
LiveServer.shutdown = function() {
	var watcher = LiveServer.watcher;
	if (watcher) {
		watcher.close();
	}
	var server = LiveServer.server;
	if (server)
		server.close();
};

// The shutdown method is raised on the server's error event
server.addListener('error'.function(e) {
	if (e.code === 'EADDRINUSE') {
		var serveURL = protocol + ': / /' + host + ':' + port;
		console.log('%s is already in use. Trying another port.'.yellow, serveURL);
		setTimeout(function() {
			server.listen(0, host);
		}, 1000);
	} else {
		console.error(e.toString().red); LiveServer.shutdown(); }});Copy the code

The core implementation of the library’s functionality

Briefly extract the most important core implementation of the library, basically 100 lines of code as follows:

const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
const open = require('open');
const send = require('send');
const eventStream = require('event-stream');
const fayeWebsocket = require('faye-websocket');
const chokidar = require('chokidar');

const config = {
  host: 'http://127.0.0.1'.port: 3000.root: process.cwd(),
}

const injectContent = fs.readFileSync('./injected.html');

const server = http.createServer((req, res) = > {
  let isInject = false;
  const reqPath = url.parse(req.url).pathname;

  function handleFile(filepath) {
    const ext = path.extname(filepath).toLocaleLowerCase();
    const targetFiles = possibleExtensions = [ ' '.'.html'.'.htm'.'.xhtml' ];
    const fileContent = fs.readFileSync(filepath, 'utf8');
    if(! req.headers.origin && targetFiles.includes(ext)) {const regexp = /<\/body>/g;
      if (regexp.exec(fileContent)) {
        isInject = true; }}}function inject(stream) {
    if(! isInject)return;

    // We need to modify the length given to browser
    const len = injectContent.length + res.getHeader('Content-Length');
    // Save a reference to the original pipe
    const originalPipe = stream.pipe;

    res.setHeader('Content-Length', len);
    // Override the original pipe method
    stream.pipe = function(resp) {
      // Re-invoke the PIPE method and invoke the event-stream module to inject the contents of the stream
      // The injected content is part of the WebSocket communication
      originalPipe.call(stream, eventStream.replace(/<\/body>/g, injectContent + '</body>')).pipe(resp);
    };
  }

  send(req, reqPath, {
    root: config.root,
  }).on('stream', inject)
    .pipe(res);

});

let clients = [];
server.addListener('upgrade'.(request, socket, head) = > {
  const ws = new fayeWebsocket(request, socket, head);
  ws.onopen = function() {
    ws.send('connected');
  };
  ws.onmessage = function(e) {
    console.log('receive:', e.data);
    ws.send(e.data)
  }
  ws.onclose = function() {
    clients = clients.filter(function (x) {
      returnx ! == ws; }); }; clients.push(ws); }); server.listen(3000.() = > {
  const openPath = `${config.host}:${config.port}`;
  console.log(`[live-server] server is running at: ${openPath}`);

  // After the service is successfully started, open the browser
  open(openPath, {
    app: null}); });const wathcer = chokidar.watch([config.root], { ignoreInitial: true });

wathcer.on('change', handleChange)
  .on('add', handleChange)
  .on('unlink', handleChange)
  .on('addDir', handleChange)
  .on('unlinkDir', handleChange)
  .on('error'.(err) = > {});

function handleChange(changePath) {
  console.log('file change');
  // Check whether the CSS file content has changed
  const cssChange = path.extname(changePath) === '.css';
  clients.forEach(function(ws) {
    if (ws) {
      ws.send(cssChange ? 'refreshcss' : 'reload'); }}); }Copy the code

conclusion

The library source code implementation, I hope partners can quickly master the following points, interested can also continue to rely on the library behind further exploration:

  • nodeThe format of the script
  • http/httpsThe service is created and passedconnectThe library supports the middleware model
  • Static resource services are created using the SEND library and the stream content is modified through the Event-Straem library
  • Use the Open library to open a browser or other application
  • Use Chokidar to listen for file content changes
  • The WS service is initialized and one-to-one connections are established with faye- WebSocket at client connection time