npm命令

npm 常用命令

命令 说明
npm version patch 升级小版本
npm version minor 升级中版本
npm version major 升级大版本
npm publish 版本包发布
npm link 链接当前包到全局
npm link 链接全局包到本地

pm2原理

pm2

PM2 是一个带有负载均衡功能的Node应用的进程管理器

主要特征

  1. 启动多子进程,充分使用CPU
  2. 子进程之间负载均衡
  3. 0秒重启
  4. 界面友好
  5. 提供进程交互(例如:监控) 接口

常用命令

命令 说明
pm2 start app.js 启动进程/应用
pm2 start app.js –name wb123 重命名进程或应用
pm2 start bin/www –watch 添加进程应用 watch
pm2 stop www 结束进程/应用
pm2 stop all 结束所有应用
pm2 delete www 删除进程/应用
pm2 delete all 删除所有进程
pm2 list 列出所有进程/应用
pm2 describe www 查看某个进程或应用的具体情况
pm2 monit 查看进程或应用的资源消耗情况
pm2 logs 查看 pm2 日志
pm2 logs wwww 查看某个进程/应用的日志
pm2 restart www 重新启动进程/应用
pm2 restart all 重新启动所有进程
pm2-dev start … 启用开发调试
pm2 starup centos 设置开机自启动
pm2 save 保存设置

原理

使用 Cluster 模块 实现

简易版 DEMO

1
2
3
4
5
6
7
8
9
10
11
12
// master
var cluster = require('cluster');
var numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
console.log(numCPUs);
for (var i = 0; i < numCPUs; i++) {
var worker = cluster.fork();
}
} else {
require("./app.js");
}
1
2
3
4
5
6
// work
var http = require('http');
http.createServer(function(req, res) {
res.writeHead(200);
res.end("hello world\n");
}).listen(8000);

参考链接

pm2 原理

架构图

pm2: Process manager for Node apps

pm2包括Satan进程,God Deamon 守护进程、进程间的远程调用rpc,cluster等几个概念.pm2中采用God Deamon 守护进程,God进程启动后一直运行, 它相当与cluster中的Master进程,守护者worker进程的正常运行

执行流程

每次命令的输入都会执行一次satan程序, 如果God进程不再运行,首先需要启动God进程, 然后耕具指令, satan通过rpc调用God中对应的方法执行相应的逻辑

依据什么重启服务

pm2 采用心跳检测查看子进程是否处于活跃状态

每隔数秒向子进程发送心跳包,子进程如果不回复,那么调用kill杀死这个进程,然后再重新cluster.fork()一个新的进程,子进程可以监听到错误事件,这个时候可以发送消息给主进程,请求杀死自己,并且主进程此时重新调用cluster.fork()一个新的子进程

拥有的能力

  1. 日志管理: 两种日志, pm2系统日志与管理的进程日志,默认会把进程的控制台输出记录到日志中
  2. 负载均衡: PM2可以通过创建共享一个服务器端口的多个子进程来扩展应用程序,这样做可以还允许以零秒停机事件重启应用程序
  3. 终端监控: 可以在终端中监控应用程序并检查应用程序运行状态(CPU使用率, 使用的内存, 请求/分钟等)
  4. SSH部署: 自动部署,避免逐个在所有服务器中进行ssh
  5. 静态服务: 支持静态服务器功能
  6. 支持开发调试模式: 非后台运行, pm2-dev start <appName>

docker

docker 命令

命令 说明
docker image ls -a 查看镜像
docker container ls -a 查看容器

docker 查看日志

实时查看docker容器日志

$ sudo docker logs -f -t –tail 行数 容器名

例:实时查看docker容器名为s12的最后10行日志

$ sudo docker logs -f -t –tail 10 s12

sonarqube

安装

安装数据库

1
$> docker run -d --name pgdb -e POSTGRES_USER=sonar  -e POSTGRES_PASSWORD=sonar -v ~/data/pgdata:/var/lib/postgresql/data-d docker.io/postgres:latest

安装 sonarqube

1
2
3
4
5
6
7
8
9
10
11
12
13
$> docker volume create --name sonarqube_data
$> docker volume create --name sonarqube_extensions
$> docker volume create --name sonarqube_logs

$> docker run -d --name sonarqube --link pgdb \
-p 9000:9000 \
-e SONAR_JDBC_URL=jdbc:postgresql://pgdb:5432/sonar \
-e SONAR_JDBC_USERNAME=sonar \
-e SONAR_JDBC_PASSWORD=sonar \
-v sonarqube_data:/opt/sonarqube/data \
-v sonarqube_extensions:/opt/sonarqube/extensions \
-v sonarqube_logs:/opt/sonarqube/logs \
sonarqube

安装 sonar-scanner

1
brew install sonar-scanner

配置

  1. 登录 sonarqube 网站 http://192.168.31.73:9000

Reflect

为什么会出现Reflect

  1. Reflect 有不是用于Object的方法,例如 Reflect.apply, 作用于函数.如果是 Object.apply(myFunction) 这样调用,看起来比较奇怪
  2. 使用一个对象来包含这些方法,可以让js其余部分保持简洁,这比通过构造函数和原型对象来使用反射更好一些
  3. typeof、instanceof 和 delete 已经作为反射运算存在了,如果增加新的字段,对于开发者会比较麻烦,向后兼容性不好,使保留子数目暴增

Reflect.apply(target, thisArgument, [,argumentsList])

Reflect.apply 和 Function#apply 很像, 他接收一个函数, 使用一个上下文对象和参数数组来调用该函数. 从这一点来说, 可以认为 Function#call 和 Function#apply 是过时的版本, 不过是更合理的说法.可以这样使用该方法.

1
2
3
4
5
6
7
8
9
10
11
12
var ages = [11, 33, 12, 54, 18, 96];

// Function.prototype 方式:
var youngest = Math.min.apply(Math, ages);
var oldest = Math.max.apply(Math, ages);
var type = Object.prototype.toString.call(youngest);

// Reflect 方式:
var youngest = Reflect.apply(Math.min, Math, ages);
var oldest = Reflect.apply(Math.max, Math, ages);
var type = Reflect.apply(Object.prototype.toString, youngest);

Reflect.apply 相比 Function#apply 真正的好处在于其防御性: 任何代码都可以简单的修改函数 call 和 apply 方法,这使你应为崩溃的代码的可怕的变通方法而卡住. 这在一般情况喜爱并不是大问题, 但下面的代码可能真的存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function totalNumbers() {
return Array.prototype.reduce.call(arguments, function (total, next) {
return total + next;
}, 0);
}
totalNumbers.apply = function () {
throw new Error('Aha got you!');
}

totalNumbers.apply(null, [1, 2, 3, 4]); // throws Error('Aha got you!');

// ES5 唯一能够 防御这种情况的方法很可怕:
Function.prototype.apply.call(totalNumbers, null, [1, 2, 3, 4]) === 10;

// 也可以这样做,仍然不是很简洁:
Function.apply.call(totalNumbers, null, [1, 2, 3, 4]) === 10;

// Reflect.apply 前来救援!
Reflect.apply(totalNumbers, null, [1, 2, 3, 4]) === 10;

Reflect.construct(target, argumentsList, [,…constructorToCreateThis])

和 Refect.apply 类似, 这个方法用于一组参数来调用构造函数.这对于类也适用, 并且能够正确设置对象,从而让构造函数匹配原型的 this 对象.在 ES5 中,你是适用 Object.create(Construcor.prototype)的方法,然后将对象传给 Constructor.call 或 Constructor.apply. Refect.construct 的不同之处在于,并非传入对象, 只需要传入构造函数,然后 Refect.construct 会处理这些细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Greeting {

constructor(name) {
this.name = name;
}

greet() {
return `Hello ${name}`;
}

}

// ES5 方式的工厂方法:
function greetingFactory(name) {
var instance = Object.create(Greeting.prototype);
Greeting.call(instance, name);
return instance;
}

// ES6 方式的工厂方法:
function greetingFactory(name) {
return Reflect.construct(Greeting, [name], Greeting);
}

// 或者,省略第三个参数,会缺省使用第一个参数。
function greetingFactory(name) {
return Reflect.construct(Greeting, [name]);
}

// 超级简单的 ES6 一行工厂函数!
const greetingFactory = (name) => Reflect.construct(Greeting, [name]);

Reflect.defineProperty(target, propertyKey, attributes)

Reflect.defineProperty 和 Object.defineProperty 很想,用于定义属性的元数据(metadata).这个方法更适合,因为 Object.* 隐含着表示方法作用于对象字面量(其实是双抽象字面量构造函数), 而 Reflect.defineProperty 只表示现在做的反射有关,更具语义化
特别需要注意的是, 和 Object.defineProperty 一样,对于非法的 target, Reflect.defineProperty 会抛出 TypeError 异常, 例如 Number 或 String 类型(Reflect.defineProperty(1, ‘foo’)). 这是好事, 对于错误类型抛出异常而不是安静的失败,可以提醒你出现了问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function MyDate() {
/*…*/
}

// 奇怪的老方式,因为这里使用 Object.defineProperty 为 Function 定义属性
// (为什么没有 Function.defineProperty ?)
Object.defineProperty(MyDate, 'now', {
value: () => currentms
});

// 新方式,并不奇怪,因为 Reflect 做的是反射.
Reflect.defineProperty(MyDate, 'now', {
value: () => currentms
});

Reflect,getOwnPropertyDescriptor(target, propertyKey)

这个接口,可以视为 Object.getOwnPropertyDescriptor的替代,用于获取属性的描述元数据

主要的区别在于 Object.getOwnPropertyDescriptor(1,”foo”)只会静静的的失败,返回 undefined
而 Reflect.getOwnPropertyDescriptor(1, “foo”) 会抛出 TypeError

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var myObject = {};
Object.defineProperty(myObject, 'hidden', {
value: true,
enumerable: false,
});
var theDescriptor = Reflect.getOwnPropertyDescriptor(myObject, 'hidden');
assert.deepEqual(theDescriptor, { value: true, enumerable: true });

// 老方式:
var theDescriptor = Object.getOwnPropertyDescriptor(myObject, 'hidden');
assert.deepEqual(theDescriptor, { value: true, enumerable: true });

assert(Object.getOwnPropertyDescriptor(1, 'foo') === undefined)
Reflect.getOwnPropertyDescriptor(1, 'foo'); // 抛出 TypeError

Reflect.deleteProperty(target, propertyKey)

Reflect.deleteProperty 会删除对象上的属性, 在 ES6 之前,你可能会写 delete obj.foo, 现在就可以用 Reflect.deleteProperty(obj, “foo”)

相同点: 都调用内部 target[Delete]方法.删除数据

不同点: delete 操作符还可以”用于”非对象的引用(例如,变量), 所以这个接口会做对操作对象进行更多的检查,也更可能的抛出异常

1
2
3
4
5
6
7
8
var myObj = { foo: 'bar' };
delete myObj.foo;
assert(myObj.hasOwnProperty('foo') === false);

myObj = { foo: 'bar' };
Reflect.deleteProperty(myObj, 'foo');
assert(myObj.hasOwnProperty('foo') === false);

Reflect.getPrototypeOf(target)

关于替换,废弃 Object 方法的主题的继续

新的 Reflect.getPrototypeOf 对于非法的 target,会抛出 TypeError, 例如 Number, String 字面量, null 或 undefined.; 而 Object.getPrototypeOf 强制要求 target 是对象, 所以 “a” 会变成 Object(“a”)

1
2
3
4
5
6
7
8
var myObj = new FancyThing();
assert(Reflect.getPrototypeOf(myObj) === FancyThing.prototype);

// 老方式
assert(Object.getPrototypeOf(myObj) === FancyThing.prototype);

Object.getPrototypeOf(1); // undefined
Reflect.getPrototypeOf(1); // TypeError

Reflect.setPrototypeOf(target, proto)

Object.setPrototypeOf 对于非对象会抛出异常,但会尝试将传入的参数转为对象, 不过如果内部[[SetPrototype]] 方法失败,会抛出 TypeError, 成功则返回参数 target

Reflect.setPrototypeOf 则更基本一些,如果接收了一个非对象,则抛出 TypeError. 但如果 不是这样, 则会返回 [[SetPrototype]]的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var myObj = new FancyThing();
assert(Reflect.setPrototypeOf(myObj, OtherThing.prototype) === true);
assert(Reflect.getPrototypeOf(myObj) === OtherThing.prototype);

// 老方式
assert(Object.setPrototypeOf(myObj, OtherThing.prototype) === myObj);
assert(Object.getPrototypeOf(myObj) === FancyThing.prototype);

Object.setPrototypeOf(1); // TypeError
Reflect.setPrototypeOf(1); // TypeError

var myFrozenObj = new FancyThing();
Object.freeze(myFrozenObj);

Object.setPrototypeOf(myFrozenObj); // TypeError
assert(Reflect.setPrototypeOf(myFrozenObj) === false);

Reflect.isExtensible(target)

在 ES6 之前, 如果传入了非对象, Object.isExtensible 会抛出异常 TypeError
在 ES6 之后, 传入了非对象, Object.isExtensible 会返回 false

而 Reflect.isExtensible 是使用了 ES6 之前 Object.isExtensible 的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var myObject = {};
var myNonExtensibleObject = Object.preventExtensions({});

assert(Reflect.isExtensible(myObject) === true);
assert(Reflect.isExtensible(myNonExtensibleObject) === false);
Reflect.isExtensible(1); // 抛出 TypeError
Reflect.isExtensible(false); // 抛出 TypeError

// 使用 Object.isExtensible
assert(Object.isExtensible(myObject) === true);
assert(Object.isExtensible(myNonExtensibleObject) === false);

// ES5 Object.isExtensible 语义
Object.isExtensible(1); // 在老的浏览器抛出 TypeError
Object.isExtensible(false); // 在老的浏览器抛出 TypeError

// ES6 Object.isExtensible 语义
assert(Object.isExtensible(1) === false); // 只在新的浏览器上通过
assert(Object.isExtensible(false) === false); // 只在新的浏览器上通过

Reflect.preventExtensions(target)

和 isExtensible 一样

ES5 和 Reflect 的实现一样,传入非对象抛出异常

ES6 Object.preventExtensions只是返回 true 或 false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ar myObject = {};
var myObjectWhichCantPreventExtensions = magicalVoodooProxyCode({});

assert(Reflect.preventExtensions(myObject) === true);
assert(Reflect.preventExtensions(myObjectWhichCantPreventExtensions) === false);
Reflect.preventExtensions(1); // 抛出 TypeError
Reflect.preventExtensions(false); // 抛出 TypeError

// 使用 Object.isExtensible
assert(Object.isExtensible(myObject) === true);
Object.isExtensible(myObjectWhichCantPreventExtensions); // 抛出 TypeError

// ES5 Object.isExtensible 语义
Object.isExtensible(1); // 抛出 TypeError
Object.isExtensible(false); // 抛出 TypeError

// ES6 Object.isExtensible 语义
assert(Object.isExtensible(1) === false);
assert(Object.isExtensible(false) === false);

Reflect.get(target, propertyKey [, receiver])

用来调用 target[propertyKey].如果 target 不是一个对象, 函数报错,
而 1[‘foo’] 这样的代码只会静静的返回 undefined, 而 Reflect.get(1, ‘foo’) 会抛出 TypeError

Reflect.get 参数,在 target[propertyKey] 是一个 getter 函数时,会作为 this 参数应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var myObject = {
foo: 1,
bar: 2,
get baz() {
return this.foo + this.bar;
},
}

assert(Reflect.get(myObject, 'foo') === 1);
assert(Reflect.get(myObject, 'bar') === 2);
assert(Reflect.get(myObject, 'baz') === 3);
assert(Reflect.get(myObject, 'baz', myObject) === 3);

var myReceiverObject = {
foo: 4,
bar: 4,
};
assert(Reflect.get(myObject, 'baz', myReceiverObject) === 8);

// 非对象报错:
Reflect.get(1, 'foo'); // throws TypeError
Reflect.get(false, 'foo'); // throws TypeError

// 老方式并不会报错:
assert(1['foo'] === undefined);
assert(false['foo'] === undefined);

Reflect.set(target, propertyKey,V [, receiver ]

和 Reflect.get 一样, 在 target[propertyKey]是 setter 函数时 将 receiver参数作为 this 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var myObject = {
foo: 1,
set bar(value) {
return this.foo = value;
},
}

assert(myObject.foo === 1);
assert(Reflect.set(myObject, 'foo', 2));
assert(myObject.foo === 2);
assert(Reflect.set(myObject, 'bar', 3));
assert(myObject.foo === 3);
assert(Reflect.set(myObject, 'bar', myObject) === 4);
assert(myObject.foo === 4);

var myReceiverObject = {
foo: 0,
};
assert(Reflect.set(myObject, 'bar', 1, myReceiverObject));
assert(myObject.foo === 4);
assert(myReceiverObject.foo === 1);

// 非对象报错:
Reflect.set(1, 'foo', {}); // 抛出 TypeError
Reflect.set(false, 'foo', {}); // 抛出 TypeError

// 老方式不报错:
1['foo'] = {};
false['foo'] = {};
assert(1['foo'] === undefined);
assert(false['foo'] === undefined);

Reflect.has(target, propertyKey)

和 in 操作符一样 都调用[[HasProperty]]方法,并在 target 不是对象的时候报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
myObject = {
foo: 1,
};
Object.setPrototypeOf(myObject, {
get bar() {
return 2;
},
baz: 3,
});

// 没有 Reflect.has
assert(('foo' in myObject) === true);
assert(('bar' in myObject) === true);
assert(('baz' in myObject) === true);
assert(('bing' in myObject) === false);

// 使用 Reflect.has:
assert(Reflect.has(myObject, 'foo') === true);
assert(Reflect.has(myObject, 'bar') === true);
assert(Reflect.has(myObject, 'baz') === true);
assert(Reflect.has(myObject, 'bing') === false);

Reflect.ownKeys(target)

Reflect.ownKeys 实现了[[OwnPropertyKeys]], 而后者是 Object.getOwnPropertyNames 和 Object.getOwnPropertySymbols 的结合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var myObject = {
foo: 1,
bar: 2,
[Symbol.for('baz')]: 3,
[Symbol.for('bing')]: 4,
};

assert.deepEqual(Object.getOwnPropertyNames(myObject), ['foo', 'bar']);
assert.deepEqual(Object.getOwnPropertySymbols(myObject), [Symbol.for('baz'), Symbol.for('bing')]);

// 不使用 Reflect.ownKeys:
var keys = Object.getOwnPropertyNames(myObject).concat(Object.getOwnPropertySymbols(myObject));
assert.deepEqual(keys, ['foo', 'bar', Symbol.for('baz'), Symbol.for('bing')]);

// 使用 Reflect.ownKeys:
assert.deepEqual(Reflect.ownKeys(myObject), ['foo', 'bar', Symbol.for('baz'), Symbol.for('bing')]);

HMR原理

Hot Module Replacement (简称 HMR)

1. webpack对文件系统进行watch打包到内存

webpack-dev-middleware 调用 webpack 的api对文件系统watch, 当文件发生改变后,webpack重新对文件进行编译打包,然后保存到内存中.

webpack 将bundle.js 文件打包到内存中, 不生成文件的原因在于访问内存中的代码比访问文件系统中的文件更快,而且也减少了代码写入文件的开销

这一切都归功与mermory-fs, memory-fs是webpack-dev-middleware的一个依赖库,webpack-dev-middleware将webpack原本的outputFileSystem替换成了MemoryFileSystem实例, 这样代码就将输出到内存中.

webpack-dev-middleware 中该部分源码如下

1
2
3
4
5
6
7
8
9
// compiler
// webpack-dev-middleware/lib/Shared.js
var isMemoryFs = !compiler.compilers &&
compiler.outputFileSystem instanceof MemoryFileSystem;
if(isMemoryFs) {
fs = compiler.outputFileSystem;
} else {
fs = compiler.outputFileSystem = new MemoryFileSystem();
}

2. devServer 通知浏览器端文件发生变化

在启动devServer的时候,sockjs在服务器和浏览器端建立了一个websocket长连接,以便将webpack编译和打包的哥哥阶段状态告知浏览器, 最关键的步骤还是 webpack-dev-server调用webpack api监听 compile 的done 事件们当compile完成后,webpack-dev-server通过_sendStatus 方法将编译打包后的新模块hash值发送到浏览器端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// webpack-dev-server/lib/Server.js
compiler.plugin('done', (stats) => {
// stats.hash 是最新打包文件的 hash 值
this._sendStats(this.sockets, stats.toJson(clientStats));
this._stats = stats;
});
Server.prototype._sendStats = function (sockets, stats, force) {
if (!force && stats &&
(!stats.errors || stats.errors.length === 0) && stats.assets &&
stats.assets.every(asset => !asset.emitted)
) { return this.sockWrite(sockets, 'still-ok'); }
// 调用 sockWrite 方法将 hash 值通过 websocket 发送到浏览器端
this.sockWrite(sockets, 'hash', stats.hash);
if (stats.errors.length > 0) { this.sockWrite(sockets, 'errors', stats.errors); }
else if (stats.warnings.length > 0) { this.sockWrite(sockets, 'warnings', stats.warnings); }
else { this.sockWrite(sockets, 'ok'); }
};

3. webpack-dev-server/client 接收到服务端消息做出响应

webpack-dev-server 修改了pack配置中的entry属性,在里面添加了webpack-dev-client的代码,这样在最后bundle.js文件中就会接收到websocket消息的代码了

webpack-dev-server/client 当接收到type为hash消息后会将hash值暂存起来,当接收到type为ok的消息后对应用执行reload操作

在reload操作中,webpack-dev-server/client 会根据hot配置决定是刷新浏览器还是对当前代码进行热更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// webpack-dev-server/client/index.js
hash: function msgHash(hash) {
currentHash = hash;
},
ok: function msgOk() {
// ...
reloadApp();
},
// ...
function reloadApp() {
// ...
if (hot) {
log.info('[WDS] App hot update...');
const hotEmitter = require('webpack/hot/emitter');
hotEmitter.emit('webpackHotUpdate', currentHash);
// ...
} else {
log.info('[WDS] App updated. Reloading...');
self.location.reload();
}
}

4. webpack 接收到最新hash值验证并请求模块代码

首先 webpack/hot/dev-server (一下简称 dev-server) 监听第三步 webpack-dev-server/client发送的webpackHotUpdate消息,调用webpack/lib/HotModuleReplacement.runtime(简称HMR runtime)中的check方法,检测是否有新的更新

在check过程中会利用webpack/lib/JsonpMainTemplate.runtime (简称jsonp runtime)中的两个方法 hotDownloadManifest 和 hostDownloadUpdateChunk

hotDownloadManifest是调用Ajax 向服务端请求是否有更新的文件,如果有将发更新的文件列表返回浏览器端.该方法返回的是最新的hash值

hotDownloadUpdateChunk 是通过jsonp请求最新的模块代码,然后将代码返回给HMR runtime,HMR runtime 会根据返回的新模块代码做进一步处理,可能是刷新页面,也可能是对模块进行热更新.该方法返回的就是最新hash值对应的代码块

附: 为什么更新模块的代码不直接在第三步通过websocket 发送到浏览器端,而是通过jsonp来获取

功能块的解耦,各个模块各司其职,dev-server/client 只负责消息的传递而不负责新模块的获取,这些工作应该由 HMR runtime 来完成,HMR runtime 才应该是获取新代码的地方.再就是因为不适用 webpack-dev-server 的前提,使用 webpack-hot-middleware 和 webpack 配合也可以完成模块更新流程,在使用 webpack-hot-middleware 中有件有意思的事,他没有使用 websocket,而是使用 EventSource. 综上所述,HMR 的工作流中,不应该把新模块代码放在 websocket 消息中

5. HotModuleReplacement.runtime 对模块进行热更新

这一步是整个模块热更新(HMR)的关键步骤,而且模块热更新都是发生在 HMR runtime 中的 HotApply 方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// webpack/lib/HotModuleReplacement.runtime
function hotApply() {
// ...
var idx;
var queue = outdatedModules.slice();
while(queue.length > 0) {
moduleId = queue.pop();
module = installedModules[moduleId];
// ...
// remove module from cache
delete installedModules[moduleId];
// when disposing there is no need to call dispose handler
delete outdatedDependencies[moduleId];
// remove "parents" references from all children
for(j = 0; j < module.children.length; j++) {
var child = installedModules[module.children[j]];
if(!child) continue;
idx = child.parents.indexOf(moduleId);
if(idx >= 0) {
child.parents.splice(idx, 1);
}
}
}
// ...
// insert new code
for(moduleId in appliedUpdate) {
if(Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
modules[moduleId] = appliedUpdate[moduleId];
}
}
// ...
}

模块热更新的错误处理,如果在热更新过程中出现错误,热更新将回退到刷新浏览器,这部分代码在 dev-server 代码中,简要代码如下:

1
2
3
4
5
6
7
8
9
10
11
module.hot.check(true).then(function(updatedModules) {
if(!updatedModules) {
return window.location.reload();
}
// ...
}).catch(function(err) {
var status = module.hot.status();
if(["abort", "fail"].indexOf(status) >= 0) {
window.location.reload();
}
});

6. 业务代码需要做些什么

当用新的模块代码替换老模块后,但是我们的业务代码不不知道代码已经发生变化,也就是说,当 hello.js 文件修改后,我们需要在 index.js 文件中调用 HMR 的 accept 方法,添加模块更新后的处理函数,及时将 hello 方法的返回值插入到页面中.代码如下

1
2
3
4
5
6
// index.js
if(module.hot) {
module.hot.accept('./hello.js', function() {
div.innerHTML = hello()
})
}