Angular CLI Behind the Scenes, Part two
Last time, I walked through the Angular
CLI tool ng
and its relationship to npm
and webpack
. When I
left off, I noted that if you run ng serve --open
, a browser is opened (or a tab in an
existing browser is open) to render your code and if you make changes, those changes are automatically
reflected in the browser. What's actually going on here?
Angular's ng serve
is a wrapper over webpack-dev-server
, which you can use
and run independently of Angular. You can install it easily via npm
:
$ npm install webpack
$ npm install webpack-cli
$ npm install webpack-dev-server
(Yes, believe it or not, webpack itself isn't installed transitively, although 259 other packages are!). Most people go ahead and install these globally, but in this post I'll keep things local. The simplest webpack "site" is just a simple src/index.js like:
$ cat src/index.js
console.log("simplest thing I can think of");
And a corresponding index.html:
$ cat index.html
<script src="main.js"></script>
main.js? What's that? By default, webpack looks in a source directory named
src
and produces a bundled version named main.js
. That's all configurable,
of course, but I won't bother changing things around here.
Now, you can run the webpack-dev-server
:
$ node_modules/.bin/webpack-dev-server
By default (again), this will start up a web server on port 8080. If you navigate to it in Chrome with devtools open, you'll see two familiar/expected GET requests:
GET http://localhost:8080/
GET http://localhost:8080/main.js
As expected, and two other less familiar requests:
GET ws://localhost:8080/sockjs-node/029/sklgh3l2/websocket
GET http://localhost:8080/sockjs-node/info?t=1546968058570
The first is a web socket request - this allows client-side code to exchange arbitrary, non-HTTP data with a server. If you've been working with Javascript for long, you're probably familiar with the XMLHttpRequest "AJAX" means of client/server communication. WebSockets are similar, but rather than being strictly request/response, they're designed to just transfer arbitrary bytes back and forth between a Javascript application and the server.
Digging a little deeper, it's interesting to look into the behavior of websockets. Web Sockets are supported by Node.JS as well as all modern browsers, so it's easy enough to set up a test web socket:
$ npm install ws
$ cat echo.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({
port:8099
});
wss.on('connection', function connection(ws) {
ws.on('message', function incoming(message) {
console.log(message);
ws.send(message);
});
ws.send('ready');
});
var conn = new WebSocket('ws://localhost:8099');
conn.send('abc123');
Chrome's developer tools give you an overview of what's going on here, but it can be a little bit muddled to piece together exactly what happens when. A "web" socket is, of course, a regular TCP socket with additional protocol support above it. Actually the first thing the client does when the socket is opened is to send an ordinary HTTP request:
GET / HTTP/1.1
Host: localhost:8099
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
Upgrade: websocket
Origin: file://
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Sec-WebSocket-Key: dRBa5g3AFGKYmWQbi9rdRg==
Sec-WebSocket-Extenstions: permessage-deflate; client_max_window_bits
To which the server replies with:
HTTP/1.1 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 9M164RanLF04Uf8bFMq3I9R5qSf=
Origin: file://
So far, this doesn't seem very "lightweight" compared to an XMLHttpRequest - however, once the protocol switch has taken place, the overhead goes way down. Notice on line 13 of listing 1, I have the server posting the string "ready". This is transmitted as soon as the WebSocket protocol takes over and consists of a two-byte header, a "masking key" and a payload. The payload is the original data, byte-for- byte, XOR-ed with the masking key. This is done to prevent replay attacks, not to secure the traffic - it can be easily decoded by anybody with a packet sniffer. If you need security, upgrade to wss.
Once you understand what's going on, you can make sense of Chrome's Network tab view - it displays the request and response headers that make up the HTTP portion of the exchange in the left-most tab, followed by Frames in the second tab. When the server sends data to the browser, a red arrow is displayed; when the client sends data back to the server a green arrow is displayed.
The final GET request that pops up is the sockjs-node/info
request which returns a
JSON response like:
{"websocket":true,"origins":["*:*"],"cookie_needed":false,"entropy":3718846906}
It's pretty clear what this is for - just describing to the infrastructure what it needs to do.
The webpack-dev-server takes advantage of node.js's filesystem support to monitor changes to files
under the src
directory and update the held-open web socket whenever any of the files
change. Now, with the websocket held open on the client side, if you make a change to the underlying
source code (anything that webpack considers part of its bundle), you see a flurry of activity in
the network tab. First, the websocket reports that the hash has changed, and then the whole page
is reloaded at the behest of webpack-dev-server's client.js applyReload
function:
function applyReload(rootWindow, intervalId) {
clearInterval(intervalId);
log.info('[WDS] App updated. Reloading...');
rootWindow.location.reload();
}