0%

npm ls

npm prune 清理无关package

npm i —prefix

issue: npm ERR! Error: EPERM: operation not permitted, rename

use ‘npm cache clean’
npm install fails on Windows: “Error: EPERM: operation not permitted, rename” #10826

疑杀毒软件问题

npx

npx是npm的命令,创建React App时使用了如下命令

1
npx create-react-app my-app

npm 用于包管理(安装、卸载、调用已安装的包blabla),npx在此基础上提高使用包的体验,实际上,调用上述命令时,npm依次查找create-react-app的依赖,无法找到则从网络安装,随后调用创建项目,并在包命令执行结束后删除。

用软链接共享node_modules

须知node_modules使用Portable的方式管理依赖,规避了依赖树上的版本冲突,见 知乎:每个项目文件夹下都需要有node_modules吗?

1
mklink /d D:\project\B\node_modules D:\project\A\node_modules

1
npm --registry https://registry.npm.taobao.org install 

设置

1
npm config set registry https://registry.npm.taobao.org

pnpm

为什么现在我更推荐 pnpm 而不是 npm/yarn?
mindset

Bilibili npm yarn pnpm的区别

npm 早期版本 递归安装依赖 会产生重复下载 依赖包深度嵌套的问题 现代版本中引入了扁平化管理 遗留问题:串行安装速度慢 每个项目都安装庞大的node_modules

yarn 并行安装 因不适用项目下node_modules 很多代码提示/补全工具可能失明

pnpm 提供极致的磁盘空间效率 特定的依赖包版本在磁盘上只会保存一份(pnpm store); 硬链接 + 软连接

lint

静态代码检查(static code verify),运行ng lint,根据项目目录下的tslint.json所配置的规则,检查诸如命名,空行,triple-equals等书写规范,在命令行输出违反规范的位置。
加参数—fix可自动修复绝大多数的检查错误

Unfortunately,tslint已于2019宣布停止维护,并迁移至typescript-eslint,见TSLint in 2019
关于从TSLint到typescript-eslint,参考Migrate the repo to ESLint, 实际上只需

1
npx tslint-to-eslint-config

对于Angular,关于迁移,Angular团队提出了关于性能以及与现有工具链一致性的要求,见issue#13732, 目前有angular-eslint plugin支持10.1及以上版本,以实现从tslint到eslint的迁移
1
2
3
4
5
##Step 1 - Add relevant dependencies
ng add @angular-eslint/schematics
##Step 2 - Run the convert-tslint-to-eslint schematic on a project
ng g @angular-eslint/schematics:convert-tslint-to-eslint {{YOUR_PROJECT_NAME_GOES_HERE}}
##Step 3 - Remove root TSLint configuration and use only ESLint

Karma

Karma, 业(佛教观念,个人因果的集合)Karma是测试JavaScript代码而生的自动化测试管理工具,可监控文件的变化,自动执行测试。

1
2
3
4
5
"karma": "^5.0.2",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~2.1.0",
"karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.4.2",

Jasmine (Jasminum 茉莉)

Angular CLI 会下载并安装试用 Jasmine 测试框架 测试 Angular 应用

X.spec.ts文件用于Jasmine做单元测试
Jasmine

单元测试

单元测试(英語:Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。 程序单元是应用的最小可测试部件。 在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

单元测试是为了测试代码逻辑,每个‘单元’在用例场景下是否能返回期望的结果,仅此而已
栗子
假设为UserService.ts设计单元测试,须知

1
2
3
4
5
6
7
export class UserService{
constructor(private commonHTTP:CommonHTTPService){ }

getUserByID(id:string):Observable<any>{
return commonHTTP.get(id)
}
}

UserService.spec.ts
1
2
3
4
5
6
7
8
9
10
describe('UserService', ()=>{
it('getUserByID return stubbed value from a spy', ()=>{
const commonHTTPSpy = jasmine.createSpyObj('CommonHTTPService', ['get']);
const stubValue = 'stub value';
commonHTTPSpy.get.and.returnValue(stubValue);

const userService = new UserService(commonHTTPSpy);
expect(userService.getUserByID(1)).toBe(stubValue,'service returned stub value')
})
});

上例待测

e2e

ng e2e builds and serves app, then runs end-to-end test with Protractor(端对端测试工具,protractor原意是量角器,Angular是角).

配置使用Headless Chrome

Angular.cn: 为在 Chrome 中运行 CI 测试而配置 CLI
karma.conf.js

1
2
3
4
5
6
7
browsers: ['ChromeHeadlessCI'],
customLaunchers: {
ChromeHeadlessCI: {
base: 'ChromeHeadless',
flags: ['--no-sandbox']
}
},

e2e/protractor.conf.js
1
2
3
4
5
6
7
8
9
10
const config = require('./protractor.conf').config;

config.capabilities = {
browserName: 'chrome',
chromeOptions: {
args: ['--headless', '--no-sandbox']
}
};

exports.config = config;

code coverage

1
ng test --code-coverage

或-cc输出代码覆盖率报告,其他参数见Angular Docs

Issues:Uncaught NetworkError: Failed to execute ‘send’ on ‘XMLHttpRequest’: Failed to load ‘ng:///XXXComponent/%C9%B5fac.js’.

使用—source-map=false避免test fail见StackOverflow:Angular tests failing with Failed to execute ‘send’ on ‘XMLHttpRequest’

服务端渲染(Server Side Render, SSR)

  • 通过搜索引擎优化(SEO)来帮助网络爬虫。

    根据之前写简单爬虫的经历,以模仿使用者浏览静态页面并匹配目标内容为原理的爬虫,难以识别模块化的angular页面资源,SSR方式可以让每个 URL 返回的都是一个完全渲染好的页面。

  • 迅速显示出第一个支持首次内容绘制(FCP)的页面

    据说如果页面加载超过了三秒钟,那么 53% 的移动网站会被放弃

    着陆页(landing pages),看起来就和完整的应用一样。 这些着陆页是纯 HTML,并且即使 JavaScript 被禁用了也能显示。 这些页面不会处理浏览器事件,不过它们可以用 routerLink 在这个网站中导航。

  • 提升在手机和低功耗设备上的性能

    在执行JavaScript存在困难的设备上,可以显示出静态页面,而不是直接crush。。。大概是有机会说“抱歉,blabla”

@nguniversal/express-engine 模板

1
npm add @nguniversal/express-engine --clientProject QQsNgApp

于是原angular项目变为anguar应用与express server混合项目
新增

1
2
3
4
5
./server.ts
./tsconfig.server.json
./webpack.server.config.js
./src/main.server.ts
./src/app/app.server.module.ts

篡改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
angular.json:
+ products.QQsNgApp.architect.serve={...}

main.ts:
document.addEventListener('DOMContentLoaded', () => {
bootstrap();
});

package.json:
scripts:
+ "compile:server": "webpack --config webpack.server.config.js --progress --colors",
+ "serve:ssr": "node dist/server",
+ "build:ssr": "npm run build:client-and-server-bundles && npm run compile:server",
+ "build:client-and-server-bundles": "ng build --prod && ng run csd-ams-front:server:production --bundleDependencies all"
...

服务端渲染无法调用windows对象,继而document, localstorage等对象会报undefined

停止和退出

app.quit未完全结束进程

在exe执行中未现问题

Cannot find module ‘./src/….’
Github Issue
将文件pattern添加到electron-builder的打包规则中

另外,require import的路径均不区分大小写,configure.ts被活生生编译并打包为Configure.js以及Configure.map.js
import编译成require,找不到configure

无边透明窗体显示时出现闪烁 Issue#10069

该问题是由于webview plugin无法设置透明背景造成的,在BrowserWindow show过程中显示了webview的白色背景,目前的workaround可以延迟页面内容的显示

1
2
3
4
5
6
7
function showBrowserWindow() {
win.setOpacity(0);
win.show();
setTimeout(() => {
win.setOpacity(1);
}, 50);
}

rebuild fail

1
2
gyp ERR! clean error
gyp ERR! stack Error: EPERM: operation not permitted, unlink 'D:\projxxx\node_modules\ref\build\Release\binding.node'

往往是项目文件正在使用中(正在参与其他进程的编译或执行)

oauth2

早先本机应用程序使用嵌入的用户代理(嵌入的web view)进行OAuth授权请求,这种方法有很多缺点,包括主机应用程序
能够复制用户凭据和Cookie,以及需要在每个应用程序中从头进行身份验证的用户。IETF RFC 8252
使用浏览器被认为更加安全且容易保留认证状态

2026-07 修订:这条仍然成立。本机应用包括 Electron,应优先使用系统浏览器 + Authorization Code + PKCE,而不是在 BrowserWindow / webview 中嵌入登录页。旧示例中的 response_type=id_token token 属于 implicit/hybrid 风格,今天不建议作为新实现。

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
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| User Device |
| |
| +--------------------------+ | (5) Authorization +---------------+
| | | | Code | |
| | Client App |---------------------->| Token |
| | |<----------------------| Endpoint |
| +--------------------------+ | (6) Access Token, | |
| | ^ | Refresh Token +---------------+
| | | |
| | | |
| | (1) | (4) |
| | Authorizat- | Authoriza- |
| | ion Request | tion Code |
| | | |
| | | |
| v | |
| +---------------------------+ | (2) Authorization +---------------+
| | | | Request | |
| | Browser |--------------------->| Authorization |
| | |<---------------------| Endpoint |
| +---------------------------+ | (3) Authorization | |
| | Code +---------------+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+


对于 electron,渲染界面提供入口 signin,点击后调用默认浏览器打开登录页,authenticate 通过后,重定向过程会将授权码返回到 electron。这个“返回”过程可以使用自定义协议实现,也可以使用 loopback localhost callback。

重定向过程会将授权码或直接将access token返回到electron

2026-07 修订:新实现应让浏览器只返回 authorization code,Electron 客户端再用 PKCE 的 code_verifier 换 token。不要让 access token 出现在 URL fragment 中,也不要在 renderer 中长期保存 token。

推荐流程:

  1. Electron 主进程生成 code_verifiercode_challengestatenonce
  2. 使用 shell.openExternal(authUrl) 打开系统浏览器。
  3. 通过自定义协议或 localhost callback 收到 codestate
  4. 校验 state,用 code_verifier 请求 token。
  5. token 存储放在系统凭据管理器、加密存储或主进程内存中,renderer 只拿业务所需的最小状态。

renderUI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
signinWithADB2C(){
const adconfig={
clientid:'3c744214-bf78-4f92-8173-49863ae8f24b',
authority:'https://qqstudio.b2clogin.com/qqstudio.onmicrosoft.com/B2C_1_basic_sign_up_and_sign_in',
redirectUri:'http://localhost:9990/index.html',
scopes:'3c744214-bf78-4f92-8173-49863ae8f24b openid'
}
// make up / assemble authority URL
const authorityURL = `${adconfig.authority}/oauth2/v2.0/authorize?client_id=${
adconfig.clientid
}&redirect_uri=${
encodeURI(adconfig.redirectUri)
}&scope=${
encodeURI(adconfig.scopes)
}&response_type=id_token%20token&nonce=defaultNonce&prompt=login`;
// call main process open URL with default browser
// meanwhile launch a http server
this.ipcService.send('openinbroweser', authorityURL);
this.loading = true;
// listen Logged in message
this.ipcService.on('access_token', msg => {
// TODO process user info
});
}

2026-07 修订:上面代码仅作历史记录。新代码应改为 response_type=code,并添加 code_challengecode_challenge_method=S256、随机 statenonce

main 主线程中launch一个http server,负责提供Redirect URI的页面,页面自执行request请求,请求亦由http server响应

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
....
ipcMain.on('openinbroweser', (event, args) => {
log('info', 'ipcmain event:openinbroweser');
if (args) {
const { shell } = require('electron');
shell.openExternal(args);
} else {
log('error', 'invalid website', args);
}
});

// launch a http server
var static = require('node-static');
var file = new static.Server(`${__dirname}/public`);
http.createServer(function (request, response) {
if(request.url==='/index.html'){
request.addListener('end', function () {
file.serve(request, response)
}).resume();
}else{
const reg = new RegExp("t=([^&]*)");
const res= request.url.match(reg);
const token = res[1];
console.log('t=',token)
win.webContents.send('access_token',token)
response.write("success"); //close default browser
}
response.end(); //end the response
}).listen(9990)

redirect page (public/index.html)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<html>
<body>
<p>serve by csportal</p>
<script>
(() => {
var reg = new RegExp("#access_token=([^&]*)");
var res = location.href.match(reg);
var token = unescape(res[1]);
fetch(`http://localhost:3000?t=${token}`)
.then(function (response) {
return response.json();
})
.catch(error => console.error('Error:', error))
.then(response => {console.log('Success:', response);
window.close();
});
})();
</script>
</body>
</html>

Caution! 需考虑到浏览器将token传递给client的过程,都有被第三方恶意应用占用URL Scheme或者localhost端口截取Access Token的风险。在有”显式”授权流程的方式中,浏览器传递授权码,由client凭授权码换取token,同样无法杜绝第三方拦截。
了解使用带有PKCE(Proof Key for Code Exchange)支持的授权码模式

2026-07 新增:Electron OAuth 安全清单

  • state 必须随机且单次使用,callback 必须校验。
  • localhost callback 使用随机端口,并尽量只监听 127.0.0.1
  • 自定义协议要考虑被其他应用抢注的风险。
  • 不在 renderer、localStorage、日志、URL 中暴露 token。
  • 打开外部链接前校验协议和 host,避免被恶意 URL 利用。

NSIS(Nullsoft Scriptable Install System)

Macros are used to insert code at compile time, depending on defines and using the values of the defines. The macro’s commands are inserted at compile time. This allows you to write general code only once and use it a lot of times but with few changes

如上所述,宏的作用是“insert”代码,和通常的编程语言中的#define是一样的。insert的锚点标识是!insertmacro

宏指令

1
2
3
4
5
6
7
!macro Hello
DetailPrint "Hello world"
!macroend

Section Test
!insertmacro Hello
SectionEnd

含参宏

1
2
3
4
5
6
7
8
9
!macro Hello What
DetailPrint "Hello ${What}"
!macroend

Section Test
!insertmacro Hello "World"
!insertmacro Hello "Tree"
!insertmacro Hello "Flower"
SectionEnd

node-ffi (Node.js Foreign Function Interface)

install:

1
npm i ffi

如果install的ffi有问题,可以拉source下来编译
compile:
1
2
3
4
npm i -g node-gyp
git clone git://github.com/node-ffi/node-ffi.git
cd node-ffi
node-gyp rebuild

Node FFI Tutorial

node-ffi 停止更新当前不支持最新版本node.js,事实上 经QQs实践基于node10的electron6无法与之集成构建,Github上有替代方案 node-ffi-napi

ws: a Node.js WebSocket library

Version npm
Linux Build
Windows Build
Coverage Status

ws is a simple to use, blazing fast, and thoroughly tested WebSocket client and
server implementation.

Passes the quite extensive Autobahn test suite: server,
client.

Note: This module does not work in the browser. The client in the docs is a
reference to a back end with the role of a client in the WebSocket
communication. Browser clients must use the native
WebSocket
object(QQs: browser client should call the Websocket constructor provided by browser, see the example at bottom of this article). To make the same code work seamlessly on Node.js and the browser, you
can use one of the many wrappers available on npm, like
isomorphic-ws.

Table of Contents

Protocol support

  • HyBi drafts 07-12 (Use the option protocolVersion: 8)
  • HyBi drafts 13-17 (Current default, alternatively option
    protocolVersion: 13)

Installing

1
npm install ws

Opt-in for performance and spec compliance

There are 2 optional modules that can be installed along side with the ws
module. These modules are binary addons which improve certain operations.
Prebuilt binaries are available for the most popular platforms so you don’t
necessarily need to have a C++ compiler installed on your machine.

  • npm install --save-optional bufferutil: Allows to efficiently perform
    operations such as masking and unmasking the data payload of the WebSocket
    frames.
  • npm install --save-optional utf-8-validate: Allows to efficiently check if a
    message contains valid UTF-8 as required by the spec.

API docs

See /doc/ws.md for Node.js-like documentation of ws classes and
utility functions.

WebSocket compression

ws supports the permessage-deflate extension which enables
the client and server to negotiate a compression algorithm and its parameters,
and then selectively apply it to the data payloads of each WebSocket message.

The extension is disabled by default on the server and enabled by default on the
client. It adds a significant overhead in terms of performance and memory
consumption so we suggest to enable it only if it is really needed.

Note that Node.js has a variety of issues with high-performance compression,
where increased concurrency, especially on Linux, can lead to catastrophic
memory fragmentation
and slow performance. If you intend to use
permessage-deflate in production, it is worthwhile to set up a test
representative of your workload and ensure Node.js/zlib will handle it with
acceptable performance and memory usage.

Tuning of permessage-deflate can be done via the options defined below. You can
also use zlibDeflateOptions and zlibInflateOptions, which is passed directly
into the creation of [raw deflate/inflate streams][node-zlib-deflaterawdocs].

See [the docs][ws-server-options] for more options.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const WebSocket = require('ws');

const wss = new WebSocket.Server({
port: 8080,
perMessageDeflate: {
zlibDeflateOptions: {
// See zlib defaults.
chunkSize: 1024,
memLevel: 7,
level: 3
},
zlibInflateOptions: {
chunkSize: 10 * 1024
},
// Other options settable:
clientNoContextTakeover: true, // Defaults to negotiated value.
serverNoContextTakeover: true, // Defaults to negotiated value.
serverMaxWindowBits: 10, // Defaults to negotiated value.
// Below options specified as default values.
concurrencyLimit: 10, // Limits zlib concurrency for perf.
threshold: 1024 // Size (in bytes) below which messages
// should not be compressed.
}
});

The client will only use the extension if it is supported and enabled on the
server. To always disable the extension on the client set the
perMessageDeflate option to false.

1
2
3
4
5
const WebSocket = require('ws');

const ws = new WebSocket('ws://www.host.com/path', {
perMessageDeflate: false
});

Usage examples

Sending and receiving text data

1
2
3
4
5
6
7
8
9
10
11
const WebSocket = require('ws');

const ws = new WebSocket('ws://www.host.com/path');

ws.on('open', function open() {
ws.send('something');
});

ws.on('message', function incoming(data) {
console.log(data);
});

Sending binary data

1
2
3
4
5
6
7
8
9
10
11
12
13
const WebSocket = require('ws');

const ws = new WebSocket('ws://www.host.com/path');

ws.on('open', function open() {
const array = new Float32Array(5);

for (var i = 0; i < array.length; ++i) {
array[i] = i / 2;
}

ws.send(array);
});

Simple server

1
2
3
4
5
6
7
8
9
10
11
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', function connection(ws) {
ws.on('message', function incoming(message) {
console.log('received: %s', message);
});

ws.send('something');
});

External HTTP/S server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const fs = require('fs');
const https = require('https');
const WebSocket = require('ws');

const server = https.createServer({
cert: fs.readFileSync('/path/to/cert.pem'),
key: fs.readFileSync('/path/to/key.pem')
});
const wss = new WebSocket.Server({ server });

wss.on('connection', function connection(ws) {
ws.on('message', function incoming(message) {
console.log('received: %s', message);
});

ws.send('something');
});

server.listen(8080);

Multiple servers sharing a single HTTP/S server

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
33
const http = require('http');
const WebSocket = require('ws');
const url = require('url');

const server = http.createServer();
const wss1 = new WebSocket.Server({ noServer: true });
const wss2 = new WebSocket.Server({ noServer: true });

wss1.on('connection', function connection(ws) {
// ...
});

wss2.on('connection', function connection(ws) {
// ...
});

server.on('upgrade', function upgrade(request, socket, head) {
const pathname = url.parse(request.url).pathname;

if (pathname === '/foo') {
wss1.handleUpgrade(request, socket, head, function done(ws) {
wss1.emit('connection', ws, request);
});
} else if (pathname === '/bar') {
wss2.handleUpgrade(request, socket, head, function done(ws) {
wss2.emit('connection', ws, request);
});
} else {
socket.destroy();
}
});

server.listen(8080);

Client authentication

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
const http = require('http');
const WebSocket = require('ws');

const server = http.createServer();
const wss = new WebSocket.Server({ noServer: true });

wss.on('connection', function connection(ws, request, client) {
ws.on('message', function message(msg) {
console.log(`Received message ${msg} from user ${client}`);
});
});

server.on('upgrade', function upgrade(request, socket, head) {
authenticate(request, (err, client) => {
if (err || !client) {
socket.destroy();
return;
}

wss.handleUpgrade(request, socket, head, function done(ws) {
wss.emit('connection', ws, request, client);
});
});
});

server.listen(8080);

Also see the provided example using express-session.

Server broadcast

A client WebSocket broadcasting to all connected WebSocket clients, including
itself.

1
2
3
4
5
6
7
8
9
10
11
12
13
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', function connection(ws) {
ws.on('message', function incoming(data) {
wss.clients.forEach(function each(client) {
if (client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
});
});

A client WebSocket broadcasting to every other connected WebSocket clients,
excluding itself.

1
2
3
4
5
6
7
8
9
10
11
12
13
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', function connection(ws) {
ws.on('message', function incoming(data) {
wss.clients.forEach(function each(client) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
});
});

echo.websocket.org demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const WebSocket = require('ws');

const ws = new WebSocket('wss://echo.websocket.org/', {
origin: 'https://websocket.org'
});

ws.on('open', function open() {
console.log('connected');
ws.send(Date.now());
});

ws.on('close', function close() {
console.log('disconnected');
});

ws.on('message', function incoming(data) {
console.log(`Roundtrip time: ${Date.now() - data} ms`);

setTimeout(function timeout() {
ws.send(Date.now());
}, 500);
});

Use the Node.js streams API

1
2
3
4
5
6
7
8
9
10
const WebSocket = require('ws');

const ws = new WebSocket('wss://echo.websocket.org/', {
origin: 'https://websocket.org'
});

const duplex = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' });

duplex.pipe(process.stdout);
process.stdin.pipe(duplex);

Other examples

For a full example with a browser client communicating with a ws server, see the
examples folder.

Otherwise, see the test cases.

FAQ

How to get the IP address of the client?

The remote IP address can be obtained from the raw socket.

1
2
3
4
5
6
7
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', function connection(ws, req) {
const ip = req.connection.remoteAddress;
});

When the server runs behind a proxy like NGINX, the de-facto standard is to use
the X-Forwarded-For header.

1
2
3
wss.on('connection', function connection(ws, req) {
const ip = req.headers['x-forwarded-for'].split(/\s*,\s*/)[0];
});

How to detect and close broken connections?

Sometimes the link between the server and the client can be interrupted in a way
that keeps both the server and the client unaware of the broken state of the
connection (e.g. when pulling the cord).

In these cases ping messages can be used as a means to verify that the remote
endpoint is still responsive.

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
const WebSocket = require('ws');

function noop() {}

function heartbeat() {
this.isAlive = true;
}

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', function connection(ws) {
ws.isAlive = true;
ws.on('pong', heartbeat);
});

const interval = setInterval(function ping() {
wss.clients.forEach(function each(ws) {
if (ws.isAlive === false) return ws.terminate();

ws.isAlive = false;
ws.ping(noop);
});
}, 30000);

wss.on('close', function close() {
clearInterval(interval);
});

Pong messages are automatically sent in response to ping messages as required by
the spec.

Just like the server example above your clients might as well lose connection
without knowing it. You might want to add a ping listener on your clients to
prevent that. A simple implementation would be:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const WebSocket = require('ws');

function heartbeat() {
clearTimeout(this.pingTimeout);

// Use `WebSocket#terminate()`, which immediately destroys the connection,
// instead of `WebSocket#close()`, which waits for the close timer.
// Delay should be equal to the interval at which your server
// sends out pings plus a conservative assumption of the latency.
this.pingTimeout = setTimeout(() => {
this.terminate();
}, 30000 + 1000);
}

const client = new WebSocket('wss://echo.websocket.org/');

client.on('open', heartbeat);
client.on('ping', heartbeat);
client.on('close', function clear() {
clearTimeout(this.pingTimeout);
});

How to connect via a proxy?

Use a custom http.Agent implementation like https-proxy-agent or
socks-proxy-agent.

Changelog

We’re using the GitHub releases for changelog entries.

License

MIT

[node-zlib-deflaterawdocs]:
https://nodejs.org/api/zlib.html#zlib_zlib_createdeflateraw_options

[ws-server-options]:
https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback

Browser Client

1
2
3
4
5
6
7
8
9
10
11
12
// Create WebSocket connection.
const socket = new WebSocket('ws://localhost:8080');

// Connection opened
socket.addEventListener('open', function (event) {
socket.send('Hello Server!');
});

// Listen for messages
socket.addEventListener('message', function (event) {
console.log('Message from server ', event.data);
});

Branch

  • master
  • dev
  • feature/xxxx
  • release/1.x.x
  • hotfix/1.x.x

    Tag

    hotfix 合并到master 以及 dev

    1
    2
    git checkout master
    git merge --no-ff hotfix/1.x.x

    关于QA的讨论

    曾认为feature代码在开发确认功能,过静态检查、单元测试、code review后合并至dev,即feature所在的生命周期由开发团队维护,从dev做release交QA进行全量测试
    实际上在本团队,feature的DOD(defination of done)规定在合并到dev前需要通过QA测试,对合并后再发的’版本’也肯定要测的。
    对于本团队的实践,是否存在QA过早介入,测试是否重复的问题,fcc大佬如是说:多数情况下new feature的业务/逻辑上的确认只能人工进行,这里的人是QA的人是合理的,该‘确认’过程不包含在code review(主要针对编码质量)中,若无QA的确认,则存在合并后被QA拒绝的风险,而拆解合并后的代码可能代价颇高。
    另:了解gitlab

  • master 只合并

  • dev 拉feature 完成合并到dev
  • dev 拉release 测试后合并到 master 和 dev

为什么不直接测dev?拉release相当于锁定dev状态 此时dev可以接收合并 防止dev遭合并破坏时影响release进程 同时支持多release版本并行

  • release 有bug 在当前分支修改后 需合并到 dev
  • 线上紧急修复 master分支拉hotfix 修复后合并到master dev