0%

capture full size screenshot

  1. F12
  2. Ctrl + Shift + P
  3. type in capture full size screenshot

dom process tips

FreeCodeCamp:你知道 Chrome 自带的开发者工具有这些功能吗

  • $$(‘.className’) chrome自带元素选择器
  • 将页面作为文本进行编辑

    1
    document.body.contentEditable = true;
  • 获取事件以及监听事件

  • console.time()

    1
    2
    3
    4
    console.time('heavy process')
    console.timeLog('heavy process', 'step1 finished')
    console.timeLog('heavy process', 'step2 finished')
    console.timeEnd('heavy process')
  • console.table(array)

  • console.assert(exp,’description’) 断言exp返回false时输出字符串

使用chrome 模拟加载较慢网速

  1. F12
  2. Network Tab
  3. Change “Online” to “Slow 3G”

页面元素断点

第三方UI控件会动态添加元素事件,使用css的hover,focus无法触发,把光标放上去触发又无法查看code,设置element的breakpoint可以在指定元素被修改时break,无论查看js逻辑还是元素的样式变化都很方便

headless chrome

知乎:Headless Chrome入门 即不显示浏览器界面而在命令行运行Chrome功能,该模式主要用于自动化测试工具#### 响应式
使用 Chrome DevTools 中的 Device Mode 模拟移动设备

performance

current version: 6.5.2

关于响应式编程

知乎:响应式编程

Observable 的发布和订阅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Create simple observable that emits three values
const myObservable = of(1, 2, 3); // or from([1,2,3])

// Create observer object
const myObserver = {
next: x => console.log('Observer got a next value: ' + x),
error: err => console.error('Observer got an error: ' + err),
complete: () => console.log('Observer got a complete notification'),
};

// Execute with the observer object
myObservable.subscribe(myObserver);
// Logs:
// Observer got a next value: 1
// Observer got a next value: 2
// Observer got a next value: 3
// Observer got a complete notification

of(…items) from(iterable)是自带常用产生Observable对象的工具

Observable 可观察对象

在http请求中,可观察对象不是new构造的,而是由httpclient的get或post方法返回一个Observable\. 私以为是成功调用接口后就发布一个结果,这个结果可以用管道加工

参考 Observable 的操作符

1
2
3
4
5
6
7
8
9
10
11
getDict(dictname): Observable<DictItem[]> {
return this.http.get(`dict/${dictname}`).pipe(
map((res: HttpResponse) => {
if (res.status === 'ok') {
return res.data;
} else {
return [];
}
}),
);
}

获取这个结果就需要订阅(subscribe)这个Observable,这个Observable是匿名的,每次获取它需要调用函数来返回

QQs:为什么httpclient方法不需要取消订阅

据说这些方法被实现为只next一次

Subject

A Subject is a special type of Observable which shares a single execution path among observers.

被比喻成广播者

1
2
3
4
5
6
7
const subject = new Subject();

subject.subscribe(log('s1 subject'));
subject.subscribe(log('s2 subject'));

subject.next('r');
subject.next('x');

同步数据转Observable

场景:组件粒度小,在视图中多次实例化,或者重复初始化,为防止频繁调用后台接口应加入数据缓存,
基础数据缓存实现为,第一次调用,从httpclient获取接口数据的Observable对象,并用管道处理加入缓存,之后将缓存数据取出转为Observable对象
basicdata.service.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Observable, of, from } from 'rxjs';
import { map } from 'rxjs/operators';

users: User[];
getUserList(): Observable<User[]> {
if (this.users) {
return of(this.users);
} else {
return this.http.post(`user/search`, { isvalid: 1 }).pipe(
map((res) => {
this.users = res as User[];
return this.users;
})
);
}
}

app.component.ts
1
2
3
4
this.basicData.getUserList().subscribe(list => {
this.userlist = list;
this.getUserName();
});

Promise转Observable

起初为了用async await编码以及利用链式调用,很多异步操作封装成了Promise,Promise转Observable用from方法转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Observable, of, from } from 'rxjs';
import { map } from 'rxjs/operators';

getCurrentUserToken(): Observable<any> {
if (this.currentUser) {
return of(this.currentUser);
} else {
return from(this.ipcService.call('currentuser')).pipe(
map(arg => {
if (arg) {
return typeof arg === 'string' ? JSON.parse(arg) : arg;
} else {
return null;
}
})
);
}
}

Observable的链式调用

操作符

操作符实在太多了

实现一个乘10的operator

1
2
3
4
5
6
7
8
9
function multiplyByTen(input: Observable<any>): Observable<any> {
return Rx.Observable.create(observer => {
input.subscribe({
next: (v) => observer.next(10 * v),
error: (err) => observer.error(err),
complete: () => observer.complete()
});
});
}

创建操作符 of from interval等:

from

转化Promise对象、类数组对象、迭代器对象转化为 Observables
将数组转化为 Observable

1
2
3
4
5
6
var array = [10, 20, 30];
var result = Rx.Observable.from(array);
result.subscribe(x => console.log(x));

// 结果如下:
// 10 20 30

将一个无限的迭代器(来自于 generator)转化为 Observable。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* generateDoubles(seed) {
var i = seed;
while (true) {
yield i;
i = 2 * i; // double it
}
}

var iterator = generateDoubles(3);
var result = Rx.Observable.from(iterator).take(10);
result.subscribe(x => console.log(x));

// Results in the following:
// 3 6 12 24 48 96 192 384 768 1536

转化操作符 map mapTo merge mergeMap等:

map类似于Array.prototype.map投射函数应用于每个值;mapTo相当于忽略实际订阅接受结果,替换为指定值;

merge将多个订阅捋直成一个订阅;mergeMap将投射函数应用于每个值,并将多个订阅捋直(啥?不懂)

take
filter
tap

Perform a side effect for every emission on the source Observable, but return an Observable that is identical to the source.对源可观察对象的每个‘发射’应用一个副作用,但仍然返回与源相同的可观察对象

1
2
3
4
tap<T>(nextOrObserver?: NextObserver<T> | ErrorObserver<T> | CompletionObserver<T> | (
(x: T) => void),
error?: (e: any) => void,
complete?: () => void): MonoTypeOperatorFunction<T>

参数可以是可观察对象或回调方法
常见于附上一个log操作
1
2
3
4
5
6
7
getHeroes(): Observable<Hero[]> {
return this.http.get<Hero[]>(this.heroesUrl)
.pipe(
tap(heroes => this.log(`fetched heroes`)),
catchError(this.handleError('getHeroes'))
) as Observable<Hero[]>;
}

BehaviorSubject

Subject 的作用是实现 Observable 的多播。由于其 Observable execution 是在多个订阅者之间共享的,所以它可以确保每个订阅者接收到的数据绝对相等。不仅使用 Subject 可以实现多播,RxJS 还提供了一些 Subject 的变体以应对不同场景,那就是:BehaviorSubject、ReplaySubject 以及 AsyncSubject。

BehaviorSubject 的特性就是它会存储“当前”的值。这意味着你始终可以直接拿到 BehaviorSubject 最后一次发出的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const subject = new Rx.BehaviorSubject(Math.random());

// 订阅者 A
subject.subscribe((data) => {
console.log('Subscriber A:', data);
});

subject.next(Math.random());

// 订阅者 B
subject.subscribe((data) => {
console.log('Subscriber B:', data);
});

subject.next(Math.random());

toPromise deprecated

Rxjs v8版本后toPromise方法弃用, 原因基于Observable与Promise前后返回值不一致的issue
RxJS heads up: toPromise is being deprecated

曾常使用的

1
2
3
4
5
public async loadCategories() {
this.categories = await this.inventoryService
.getCategories()
.toPromise()
}

变更为
1
2
3
4
5
6
import { lastValueFrom } from 'rxjs';
...
public async loadCategories() {
const categories$ = this.inventoryService.getCategories();
this.categories = await lastValueFrom(categories$);
}

rxjs 6中被废弃的toPromise

The lastValueFrom is almost exactly the same as toPromise() meaning that it will resolve with the last value that has arrived when the Observable completes, but with the difference in behavior when Observable completes without emitting a single value. When Observable completes without emitting, toPromise() will successfully resolve with undefined (thus the return type change), while the lastValueFrom will reject with the EmptyError. Thus, the return type of the lastValueFrom is Promise, just like toPromise() had in RxJS 6.lastValueFrom几乎与toPromise() 一个意思,在Observable complete的时候,lastValueFrom会带着Observable最后一个产生的值resolve,但是不同之处在于Observable不带值complete的情况下。
当Observable不产生值并且complete时,toPromise方法会成功resolve为undefined(也就是返回类型改变了,不再是T),但是lastValueFrom会带着EmptyErrorreject。因此,就如同在RxJS 6里一样,它的返回类型是Promise

However, you might want to take the first value as it arrives without waiting an Observable to complete, thus you can use firstValueFrom. The firstValueFrom will resolve a Promise with the first value that was emitted from the Observable and will immediately unsubscribe to retain resources. The firstValueFrom will also reject with an EmptyError if the Observable completes with no values emitted.然而,你可能不等Observable complete,想要使用其第一个生产的值,因此可以用firstValueFrom。firstValueFrom会将Observable第一个生产的值resolve并且立刻unsubscribe保存该值。firstValueFrom也会在Observable没有值产生而complete的情况下带着 EmptyErrorreject。

风险一 用户数据库泄露导致用户密码泄露

风险二 网站管理人员、数据库管理员获取用户密码

如何安全的存储密码
要点如下

  • 加密(对称加密)存储无法防止风险二
  • 通用的以hash(单向散列算法如md5)获取的 digest(摘要)无法还原完整的原数据,但是存在rainbow table的隐患

rainbow table: 多数人所使用的密码为常见的组合,攻击者可以将所有密码的常见组合进行单向哈希,得到一个摘要组合, 然后与数据库中的摘要进行比对即可获得对应的密码(QQs理解的是,这里的密码或许与用户密码并不完全一致,但是可以通过网站的摘要核对)。这个摘要组合也被称为rainbow table。在当今计算机处理能力下,这种破译方式是可行而且可取的。

在线彩虹表

明文 摘要(32bit MD5)
111111 96e79218965eb72c92a549dd5a330112
123456 E10ADC3949BA59ABBE56E057F20F883E
000000 670b14728ad9902aecba32e22fa4f6bd
  • Salted Hash 改进的单项hash算法,明文密码混入“随机因素“,然后进行单向哈希后存储,其结果是rainbow table因salt不同而不同

    QQs按:做到这个份上,应该足够安全了,然而据说随着显卡并行计算能力的发展,存在破解出rainbow table的可能,我想可能是暴力破解salt。因此有了Salted Hash的各种改进方案

  • 慢哈希 如bcrypt,迭代执行多次hash运算,现实是增加了计算时间,使得暴力破解实际不可行

如设置 bcrypt 计算一次需要 0.5 秒,遍历 6 位的简单密码,需要的时间为:((26 * 2 + 10)^6) / 2 秒,约 900 年。

bcrypt

文章略老,基于以上原理,单向hash算法有了更多的发展

docker build

Dockerfile是构建docker的脚本,docker Hub很多应用容器以Dockerfile方式分享。

1
docker build -t tomcat8.5.51 .

命令格式 docker build [option] [url]

  • 选项-t tomcat8.5.51将build完成的镜像命名为tomcat8.5.51
  • . 是当前目录,即执行当前目录下的Dockerfile

执行完成后可在 docker images中查到tomcat8.5.51

docker compose

Compose 定位是 「定义和运行多个 Docker 容器的应用(Defining and running multi-container Docker applications)」

我们知道使用一个 Dockerfile 模板文件,可以让用户很方便的定义一个单独的应用容器。然而,在日常工作中,经常会碰到需要多个容器相互配合来完成某项任务的情况。例如,要实现一个 Web 项目,除了 Web 服务容器本身,往往还需要再加上后端的数据库服务容器,甚至还包括负载均衡容器等。

Compose 恰好满足了这样的需求。它允许用户通过一个单独的 docker-compose.yml 模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)。

安装docker-compose

1
2
3
4
$ sudo curl -L "https://github.com/docker/compose/releases/download/v2.17.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

$ sudo chmod +x /usr/local/bin/docker-compose
$ sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose

菜鸟教程

使用docker为某项开发封装开发环境(存目)

Node.js 官方文档 C++ Addon

基于Node.js c++ Addons机制对接动态链接库,以实现现有Node.js接口外的底层访问

2026-07 修订:Electron 原生模块的核心风险不是“能不能写 C++”,而是 ABI(Application Binary Interface)、Node.js/Electron 版本、平台工具链、签名和分发。新代码优先使用 N-API / node-addon-api,尽量避免直接绑定 V8/NAN;已有历史包才考虑 nanffi-napiref-* 这类兼容成本较高的方案。

“Chromium和Node.js大部分的代码都是用c++实现的,所以理所当然地也可以用C++为它们开发插件。” ——— <给electron做c开发的那些坑>

node-gyp

GYP is short for ‘Generate Your Projects’,顾名思义,GYP工具用于生成在相应平台上的项目,如在windows平台上生成Visual Studio解决方案(.sln), Mac下则是XCode项目配置以及Scons工具。

node-gyp is a cross-platform command-line tool written in Node.js for compiling native addon modules for Node.js. It contains a fork of the gyp project that was previously used by the Chromium team, extended to support the development of Node.js native addons.

node-gyp 用于编译nodejs原生addon模块的,跨平台的命令行工具,fork了Chromium team使用的gyp项目,该项目用户开发Node.js C++插件

1
2
npm install -g node-gyp
npm install --global --production windows-build-tools

2026-07 修订windows-build-tools 属于旧时代经验,不建议作为新项目默认方案。Windows 上优先安装 Visual Studio Build Tools 的 “Desktop development with C++” 工作负载,并使用当前 node-gyp 文档要求的 Python 和 MSVC 工具链。

Caution!安装过程中出现 issue#147 Hangs on Python is already installed, not installing again. 原因是VisualStudio有进程占用了相关工具链,结束VS进程并重新安装windows-build-tools即可

当前版本node-gyp使用 vs2017 build tools而不支持2019版本,下文中记述了workaround

2026-07 修订:这条只适用于当年的 node-gyp/Node/Electron 组合。现在应按当前 Node.js 与 node-gyp 支持矩阵选择 VS Build Tools 版本,通常不再需要 VS2017 shim。遇到问题先确认 node -vnpm -vnode-gyp -v、Electron 版本、Python 路径和 MSVC 工具链。

python依赖

支持v2.7, v3.5, v3.6, or v3.7 v3.8 (QQs已实践2.7,3.8)

2026-07 修订:Python 2.7 已不应进入新项目工具链。现代 node-gyp 使用 Python 3,具体最低版本以当前 node-gyp 官方文档为准。

如果安装了多个版本 应使用下述命令注明python路径

1
node-gyp <command> --python /path/to/executable/python

抑或修改npm调用设置
1
npm config set python /path/to/executable/python

否则会调用默认版本(环境变量Path中指向的版本)

binding.gyp

1
2
3
4
5
6
7
8
{
"targets": [
{
"target_name": "hello",
"sources": [ "src/hello.cc" ]
}
]
}

关于gyp配置,但凡要比hello world走的远,都需要阅读下列文档

2026-07 修订:包名已经从旧的 electron-rebuild 迁移到 scoped 包 @electron/rebuild。很多历史文章仍写 electron-rebuild,新项目优先使用 @electron/rebuild

1
npm i -D @electron/rebuild

package.json

1
2
3
4
"scripts": {
...
"rebuild": "electron-rebuild -f -w yourmodule"
}

2026-07 新增:如果使用 Electron Forge,也可以通过 Forge 的相关插件/生命周期集成 rebuild。关键是保证 native addon 针对 Electron 的 Node ABI 编译,而不是针对系统 Node.js 编译。

node-pre-gyp

node-pre-gyp makes it easy to publish and install Node.js C++ addons from binaries.

node-pre-gyp stands between npm and node-gyp and offers a cross-platform method of binary deployment.

Features :

  • 使用node-pre-gyp命令行工具安装依赖二进制C++模块(install your package’s C++ module from a binary)
  • 使用node-pre-gyp模块动态引入js模块 require(‘node-pre-gyp’).find
  • 其他开发命令如 test, publish (详见—help)

配置package.json

  • 依赖 + node-pre-gyp
  • 开发依赖 + aws-sdk
  • install命令脚本 node-pre-gyp install —fallback-to-build
  • 声明需要的二进制模块
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    "dependencies"  : {
    "node-pre-gyp": "0.6.x"
    },
    "devDependencies": {
    "aws-sdk": "2.x"
    }
    "scripts": {
    "install": "node-pre-gyp install --fallback-to-build"
    },
    "binary": {
    "module_name": "your_module",
    "module_path": "./lib/binding/",
    "host": "https://your_module.s3-us-west-1.amazonaws.com"
    }
    这就是为什么英国人的项目使用npm install —build-from-source来打包addon的原理

导学列表:

  • 编译一个hello QQs C++ Addon的实践
  • 可以交互的manager插件
  • 为什么CSActivation可以用 npm install —build-from-source 打包插件

参考 Electron-利用DLL实现不可能

编译方法

官方example

  • nan: C++-based abstraction between Node and direct V8 APIs. 历史项目常见,但新项目不优先。
  • napi: C-based API guaranteeing ABI stability across different node versions as well as JavaScript engines.
  • node-addon-api: header-only C++ wrapper classes which simplify the use of the C-based N-API.

2026-07 修订:新项目优先 node-addon-api / N-API,因为它能降低 Node.js/Electron 版本升级时的 ABI 重编译压力。直接使用 V8 或 NAN 更容易被运行时升级影响。

文件结构

hello.cc

binding.gyp

package.json

编译后的二进制插件的文件扩展名是 .node

所有的 Node.js 插件必须导出一个如下模式的初始化函数:

1
2
void Initialize(Local<Object> exports);
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

使用 node-gyp 构建插件时,使用宏 NODE_GYP_MODULE_NAME 作为 NODE_MODULE() 的第一个参数将确保会将最终二进制文件的名称传给 NODE_MODULE()。

1
node-gyp configure

为当前平台生成相应的项目构建文件。 这会在 build/ 目录下生成一个 Makefile 文件(Unix 平台)或 vcxproj 文件(Windows平台)。

1
node-gyp build

生成编译后的 *.node 的文件。 它会被放进 build/Release/ 目录。

关于VS2019 找不到C++ build tool的问题

私以为比较好的workaround是创建一个用2017替代2019的shim,详见node-gyp issue#1663

经实践可行

2026-07 修订:该 workaround 仅保留作历史排障记录。新环境应优先安装当前 VS Build Tools、确认 npm config get msvs_version、Python 路径和 node-gyp 版本,不建议为新项目伪装旧版 Visual Studio。

关于rebuild ffi error的问题

electron-rebuild issue#308

需求:使用Crypt32加密(编码/解码)

1
npm i ffi-napi ref ref-struct

后面两个包是映射C语言类型的接口

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
34
35
36
37
38
39
40
41
42
43
44
45
46
const fs = require("fs");
const ref = require("ref");
const ffi = require("ffi-napi");
const Struct = require("ref-struct");

const DATA_BLOB = Struct({
cbData: ref.types.uint32,
pbData: ref.refType(ref.types.byte)
});
const PDATA_BLOB = new ref.refType(DATA_BLOB);
const Crypto = new ffi.Library('Crypt32', {
"CryptUnprotectData": ['bool', [PDATA_BLOB, 'string', 'string', 'void *', 'string', 'int', PDATA_BLOB]],
"CryptProtectData" : ['bool', [PDATA_BLOB, 'string', 'string', 'void *', 'string', 'int', PDATA_BLOB]]
});

function encrypt(plaintext) {
let buf = Buffer.from(plaintext, 'utf16le');
let dataBlobInput = new DATA_BLOB();
dataBlobInput.pbData = buf;
dataBlobInput.cbData = buf.length;
let dataBlobOutput = ref.alloc(DATA_BLOB);
let result = Crypto.CryptProtectData(dataBlobInput.ref(), null, null, null, null, 0, dataBlobOutput);
let outputDeref = dataBlobOutput.deref();
let ciphertext = ref.reinterpret(outputDeref.pbData, outputDeref.cbData, 0);
return ciphertext.toString('base64');
};

function decrypt(ciphertext) {
let buf = Buffer.from(ciphertext, 'base64');
let dataBlobInput = new DATA_BLOB();
dataBlobInput.pbData = buf;
dataBlobInput.cbData = buf.length;
let dataBlobOutput = ref.alloc(DATA_BLOB);
let result = Crypto.CryptUnprotectData(dataBlobInput.ref(), null, null, null, null, 0, dataBlobOutput);
let outputDeref = dataBlobOutput.deref();
let plaintext = ref.reinterpret(outputDeref.pbData, outputDeref.cbData, 0);
return plaintext.toString('utf16le');
};

let text = "有死之荣,无生之耻";
let ciphertext = encrypt(text);
let plaintext = decrypt(ciphertext);

console.log("text:", text);
console.log("ciphertext:", ciphertext);
console.log("plaintext:", plaintext);

以上代码白嫖自Github node-ffi Issue#355 comments from Wackerberg 侵删

2026-07 现代最佳实践:Electron 原生能力的正确做法

2026-07 新增:现在做 Electron + C/C++ 原生能力,不建议从 ffi-napi 或直接 V8/NAN 开始。优先级应是:先找现成 Node/Electron API,其次封装本地服务/CLI,再用 N-API / node-addon-api 写稳定 ABI 的 addon。只有在快速验证 DLL 调用、历史包袱或小范围内部工具中,才考虑 ffi-napi

方案选择
场景 推荐方案 原因
调系统能力,如文件、加密、通知、托盘 Electron / Node.js / OS API 少一层 native addon,维护成本最低
调已有 exe / Python / C++ 程序 主进程 child_process 或本地服务 进程隔离更清楚,崩溃不拖垮 Electron
调稳定 C/C++ SDK 或算法库 N-API + node-addon-api ABI 更稳定,适合长期维护
只想快速验证 DLL 函数 ffi-napi 快,但类型、内存、兼容性风险较高
高性能图像处理 / 设备 SDK / 加密模块 独立 native addon 或本地 daemon 便于测试、签名、崩溃恢复和权限隔离
推荐项目结构
1
2
3
4
5
6
7
8
9
10
11
my-electron-app/
package.json
src/
main/
preload/
renderer/
native/
device-addon/
package.json
binding.gyp
src/addon.cc

native/device-addon 最好作为独立 npm package 管理,原因是:

  • 可以单独跑 C++ 单元测试和 node-gyp rebuild
  • 可以独立声明平台工具链、头文件、动态库和 postinstall。
  • Electron 主项目只依赖它,不把业务 UI 和 native 编译问题揉在一起。
最小 N-API / node-addon-api 示例

安装依赖:

1
2
npm i node-addon-api
npm i -D node-gyp

binding.gyp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"targets": [
{
"target_name": "device_addon",
"sources": ["src/addon.cc"],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")"
],
"dependencies": [
"<!(node -p \"require('node-addon-api').gyp\")"
],
"defines": ["NAPI_DISABLE_CPP_EXCEPTIONS"]
}
]
}

src/addon.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <napi.h>

Napi::String GetVersion(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
return Napi::String::New(env, "device-addon/0.1.0");
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("getVersion", Napi::Function::New(env, GetVersion));
return exports;
}

NODE_API_MODULE(device_addon, Init)

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"name": "device-addon",
"version": "0.1.0",
"main": "index.js",
"gypfile": true,
"scripts": {
"build": "node-gyp rebuild"
},
"dependencies": {
"node-addon-api": "^8.0.0"
},
"devDependencies": {
"node-gyp": "^11.0.0"
}
}

index.js

1
2
3
const path = require('path')

module.exports = require(path.join(__dirname, 'build/Release/device_addon.node'))
Electron 中的正确集成方式

原生模块应由主进程加载,渲染进程通过 preload 暴露最小 API:

1
2
3
4
5
6
7
// main.js
const { ipcMain } = require('electron')
const deviceAddon = require('device-addon')

ipcMain.handle('device:get-version', () => {
return deviceAddon.getVersion()
})
1
2
3
4
5
6
// preload.js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('deviceAPI', {
getVersion: () => ipcRenderer.invoke('device:get-version')
})
1
2
// renderer.js
const version = await window.deviceAPI.getVersion()

关键点:不要在 renderer 中直接 require('.node'),也不要把任意 native 方法透传给页面。主进程负责权限、参数校验、错误处理和崩溃隔离。

Electron ABI 重编译

Electron 自带 Node.js,native addon 不能只按系统 node 编译。安装或升级 Electron 后,需要针对 Electron 版本重编译:

1
2
npm i -D @electron/rebuild
npx electron-rebuild

如果使用 Electron Forge,Forge 官方流程会在打包时自动处理 native module rebuild;复杂项目仍建议显式确认构建日志中 native addon 是针对 Electron ABI 编译的。

排查顺序:

  1. 确认 process.versions.electronprocess.versions.modules
  2. 删除 native 包的 build/ 后重新 rebuild。
  3. 确认 Python 3、Visual Studio Build Tools / Xcode / build-essential 已安装。
  4. 确认 .node 文件被 electron-builder / Forge 打进最终产物。
  5. 如果依赖外部 .dll / .so / .dylib,确认运行时搜索路径和签名。
工业/机器人软件里的建议边界
  • 设备 SDK、相机 SDK、运动控制卡 SDK:优先封装在主进程或独立本地服务,不让 UI 直接碰指针、句柄、线程。
  • 图像处理算法:高频大数据量可用 native addon;低频批处理可先用 Python/CLI 服务验证。
  • 长时间采集或控制:建议 native 层只做薄封装,状态机、日志、错误恢复、权限控制放在可测试的应用层。
  • 安全相关能力:所有命令都要有白名单、参数校验、超时、取消、审计日志,避免 renderer 一条 IPC 直接触发危险硬件动作。
参考

ElementUI, a Vue 2.0 based component library

Caution! 限定的引入顺序:

UI.css —> template —> vue.js —> UI.js —> vue controller

其中 template 和 vue.js 交换没有影响

响应式栅格布局

基础栅格,基于24划分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<el-row>
<el-col :span="6">
<div class="grid-content bg-purple"></div>
</el-col>
<el-col :span="6">
<div class="grid-content bg-purple-light"></div>
</el-col>
<el-col :span="6">
<div class="grid-content bg-purple"></div>
</el-col>
<el-col :span="6">
<div class="grid-content bg-purple-light"></div>
</el-col>
</el-row>

间隔( :gutter )

栅格之间的水平间距
1
2
3
<el-row :gutter="20">
...
</el-col>

偏移( :offset )
1
<el-col :span="6" :offset="6"><div class="grid-content"></div></el-col>

弹性盒子( flex )
1
2
3
<el-row type="flex" justify="center" align="center">
...
</el-row>

水平值:start, center, end, space-between, space-around

垂直值:top, middle, bottom

响应式( Responsive )

1
2
3
<el-col :xs="4" :sm="6" :md="8" :lg="9" :xl="11">
<div class="grid-content"></div>
</el-col>

隐藏

  • hidden-xs-only - 当视口在 xs 尺寸时隐藏
  • hidden-sm-only - 当视口在 sm 尺寸时隐藏
  • hidden-sm-and-down - 当视口在 sm 及以下尺寸时隐藏
  • hidden-sm-and-up - 当视口在 sm 及以上尺寸时隐藏

    Form表单

    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
    34
    35
    <div id="app">
    <el-form ref="form" :model="formData" :rules="formValidators" label-width="80px">
    <el-form-item label="title" prop="title">
    <el-input v-model="formData.title"></el-input>
    </el-form-item>
    <el-form-item label="Date">
    <el-col :span="11">
    <el-date-picker type="date" placeholder="Pick a date" v-model="formData.date1" style="width: 100%;">
    </el-date-picker>
    </el-col>
    <el-col class="line" :span="2">-</el-col>
    <el-col :span="11">
    <el-time-picker placeholder="Pick a time" v-model="formData.date2" style="width: 100%;">
    </el-time-picker>
    </el-col>
    </el-form-item>
    <el-form-item label="Subscribe">
    <el-switch v-model="formData.isSubscribe"></el-switch>
    </el-form-item>
    <el-form-item label="TAG">
    <el-checkbox-group v-model="formData.tags">
    <el-checkbox label="Activity" name="type"></el-checkbox>
    <el-checkbox label="Bonus" name="type"></el-checkbox>
    <el-checkbox label="Online" name="type"></el-checkbox>
    </el-checkbox-group>
    </el-form-item>
    <el-form-item label="Comments">
    <el-input type="textarea" v-model="formData.Comments"></el-input>
    </el-form-item>
    <el-form-item>
    <el-button type="primary" @click="onSubmit">Create</el-button>
    <el-button>Cancel</el-button>
    </el-form-item>
    </el-form>
    </div>
    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
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    var Main = {
    data() {
    return {
    formData: {
    title: '',
    date1: '',
    date2: '',
    isSubscribe: false,
    tags: [],
    Comments: ''
    },
    formValidators: {
    title: [{
    required: true,
    message: '请输入标题',
    trigger: 'blur'
    },
    {
    min: 3,
    max: 5,
    message: '长度在 3 到 5 个字符',
    trigger: 'blur'
    }
    ]
    }
    }
    },
    methods: {
    onSubmit(event) {
    this.$refs[formName].validate((valid) => {
    if (valid) {
    console.log(event);
    console.log(this.formData)
    } else {
    console.log('validate fail');
    return false;
    }
    });

    }
    }
    }
    var Ctor = Vue.extend(Main)
    new Ctor().$mount('#app')
    以上几乎就是官网的example,官方文档未给出submit事件触发form action的表单提交方式,api文档中未提及action属性。有资料表示存在“ this.$refs.formName.submit ”(QQs未验证)

    这种方式也造成了做表单校验时,要在提交方法中调用form.validate,form.validate又是个异步操作,后续动作(一般是http post)要放在其回调函数中

    而在我大Angular的响应式表单中,ngSubmit监听FormGroup当前值,在校验不通过时将禁用type=”submit”提交按钮

如上例所示,表单验证需添加表单的:rules属性,表单项的prop属性(注意在label,值同name),提交方法中调用form.validate

Table

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<el-table :data="tableData" border style="width: 100%">
<el-table-column fixed prop="date" label="日期" width="150">
</el-table-column>
<el-table-column prop="name" label="姓名" width="120">
</el-table-column>
<el-table-column prop="address" label="地址" width="300">
</el-table-column>
<el-table-column prop="zip" label="邮编" width="120">
</el-table-column>
<el-table-column fixed="right" label="操作" width="100">
<template slot-scope="scope">
<el-button @click="handleClick(scope.row)" type="text" size="small">查看</el-button>
<el-button type="text" size="small">编辑</el-button>
</template>
</el-table-column>
</el-table>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var Main = {
data() {
return {
tableData: [{
date: '2016-05-02',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄',
zip: 200333
}, {
date: '2016-05-04',
name: '王大虎',
address: '上海市普陀区金沙江路 1517 弄',
zip: 200333
}]
}
},
methods: {
handleClick(row) {
console.log(row);
}
}
}
var Ctor = Vue.extend(Main)
new Ctor().$mount('#app')

比较有意思的是这个el-table-column label可以设置 多级表头

模板与绑定

1
2
3
4
5
6
7
<span>Message: {{ msg }}</span>
<span v-html="rawHtml"></span>
<div v-bind:id="dynamicId"></div>
<img v-bind:src="url" />
<p v-if="seen">现在你看到我了</p>
<a v-on:click="doSomething">...</a>
<li v-for="item in items">...</li>

@click is short for v-on:click

方法的括号()不是必须的

支持动态属性名和事件名称

1
2
<a v-bind:[attributeName]="url"> ... </a>
<a v-on:[eventName]="doSomething"> ... </a>

Vue实例

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<template>
<div id="container" >
<span>{{ title }}</span>
<span v-once>{{ title }} 没变吧</span>
<span v-if="flag">now shown</span>
<p>
<a v-on:click="handleClick">
<img v-bind:src="url" :class="[flag? 'imgSize1':'imgSize2']" />
</a>
</p>
<p>

</p>
</div>
</template>
<script>
import * as THREE from 'three'
export default {
name:'viewPort',
props:{
//target
},
data(){
return{
title:"titleXXX",
flag:true,
items:["item1","item2","item3"],
url: require('@/assets/images/threelogo.png')
}
},
//
created(){

},
mounted(){
this.initScene()
},
methods:{
initScene(){
// 创建3D场景对象Scene
const scene = new THREE.Scene();
console.log(scene);

},
handleClick() {
this.flag = !this.flag;
}
}
}
</script>

<style scoped>
#container {
margin: 0;
width: 100%;
height: 100vh;
background: #fff;
}

.imgSize1 {
width: 100px;
height: 100px;
}

.imgSize2 {
width: 50px;
height: 50px;
}
</style>

当一个 Vue 实例被创建时,它将 data 对象中的所有的属性加入到 Vue 的响应式系统中。当这些属性的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。

常用实例方法:

1
2
3
4
5
vm.$data === data // => true
vm.$el === document.getElementById('example') // => true
vm.$watch('a', function (newValue, oldValue) {
// 这个回调将在 `vm.a` 改变后调用
})

生命周期钩子
生命周期
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
new Vue({
data: {
a: 1
},
created: function () {
// `this` 指向 vm 实例
console.log('a is: ' + this.a)
}
mounted: function () {
console.log('a is: ' + this.a)
}
beforeDestroy:function(){

}
})

内联处理器方法
1
2
3
4
5
6
7
8
new Vue({
data: {

},
methods:{
// TODO functions
}
})

官网

爬行起步方式

Caution! 限定的引入顺序:
template —> vue.js —> vue controller
其中 template 和 vue.js 交换没有影响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<html>
<head>
<meta charset="UTF-8">
<title>Hi vue</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
{{ message }}
</div>
<script type="text/javascript">
var app = new Vue({
el: '#app',
data: {
message: 'here here'
}
})
</script>
</body>
</html>

常规起步方式

1
2
3
npm install -g @vue/cli

vue create vueproj

阅读 vue-cli指引

原力起飞方式

webpack 打包 vue 基础

多模块项目

vue-multi-module

vue渐进式

  • 完整版
  • 编译器 用以将模板字符串编译成js渲染函数
  • 运行时 不含编译器 用以创建vue实例 处理虚拟dom
  • UMD 通过\引入html的完整版本

vue对比其他框架

特性 Vue.js React Angular 2+
核心特点 渐进式框架,易学易用 虚拟DOM,组件化 完整的MVC框架
学习曲线 平缓,只需HTML/CSS/JS基础 中等,需掌握JSX和ES2015 陡峭,需掌握TypeScript和大量概念
性能优化 自动依赖追踪,精确更新 需手动使用PureComponent或shouldComponentUpdate 自动优化,但体积较大
模板语法 支持HTML模板,也支持JSX 强制使用JSX 使用类似HTML的模板
CSS处理 单文件组件中的scoped样式 CSS-in-JS方案(如styled-components) 支持组件样式,但不如Vue灵活
生态系统 官方维护路由和状态管理 丰富的第三方库,生态系统庞大 官方完整解决方案
向下扩展 简单,可通过CDN直接使用 需要构建系统,不适合简单项目 不适合简单项目
向上扩展 良好,官方提供CLI脚手架 强大,但配置较复杂 非常强大,适合大型企业应用
TypeScript支持 良好,官方提供类型声明 优秀,TS是React的一部分 必须使用TypeScript开发
项目体积 小(gzip后约30KB) 中等 较大(gzip后约65KB)
灵活性 高,不强制代码组织方式 中等,组件灵活但生态约束 低,有严格的代码组织规范
浏览器兼容性 IE9+ 现代浏览器 现代浏览器
响应式系统 基于依赖追踪的观察系统 基于状态管理和虚拟DOM 基于Zone.js和变更检测
原生渲染 通过Weex或NativeScript-Vue React Native Ionic或NativeScript
数据流 单向数据流 单向数据流(通过状态管理) 单向数据流
  • 易用性:Vue > React > Angular
    • Vue学习曲线最平缓,React需要掌握JSX,Angular最复杂
  • 灵活性:Vue > React > Angular
    • Vue不强制代码组织方式,React组件灵活但生态约束,Angular有严格规范
  • 性能:Vue ≈ React > Angular
    • Vue和React性能接近且优秀,Angular体积较大但性能也不错
  • 生态系统:React > Angular > Vue
    • React生态系统最庞大,Angular提供完整官方解决方案,Vue官方维护核心库
  • 适用场景
    • Vue:中小型项目,需要快速开发,团队技术栈多样
    • React:大型项目,需要丰富生态系统,团队熟悉JS
    • Angular:大型企业应用,需要完整解决方案,团队熟悉TypeScript

从Angular、React到Vue

  • 单文件组件 single file component
  • 相似的指令绑定
  • 直接赋值属性 无需setState
  • Composition API类React Hooks
  • props emit类似Angular装饰器风格

从vue2到vue3

RazorEngine: Open source templating engine based on Microsoft’s Razor parsing engine.

Razor文件类型

Razor可以在vb.net和C#中使用。分别对应了两种文件类型,.vbhtml和.cshtml

Razor的标识符

@字符被定义为Razor服务器代码块的标识符,表示是服务器代码。web form中使用<%%>中写服务器代码一个道理。

1
2
3
4
5
@{
string userName= "邓星林";
} <!-- 块级作用域 -->
<span>@userName</span>
<span>@DateTime.Now.ToString("yyyy-MM-hh")</span>

HTML

@Href(“~/“)//表示网站的根目录

@Html.Raw(Module.Content) 输出HTML

Razor标识混合HTML

1
2
3
4
5
6
7
8
9
@{
// html标签
<div></div>
var str = "abc";
// 下面会输出:this is a mail:dxl0321@qq.com, this is var: abc,this is mail@str,this is @;
@: this is a mail:dxl0321@qq.com, this is var: @str,this is mail@str,this is @@;
// 下面输出abc
@str
}

a.在作用域内如果是以html标签开始则视为文本输出

b.如果要输出符号@,则使用@@

c.如果要输出非html标签和非Razor语句的代码,则用@:,他的作用是相当于在处于html下面编写一样了,如在@:后面可以加上@就是表示Razor语句的变量

注释

1
2
3
4
5
6
7
@{
@*
多行注释
多行注释
*@
var i = 10; @* asdfasf *@
}

Layout 视图片段

插入指定路径定义的布局视图模板

1
2
3
4
5
6
7
@{
ViewData["Title"] = "分享优惠券管理";
Layout = "~/Views/Shared/_Layoutv2.cshtml";
}
<div class="main">
...
</div>

插入Page

1
2
3
<div class="area">
@RenderPage("/b.cshtml")
</div>

Section

Helper

helper就是可以定义可重复使用的帮助器方法,不仅可以在同一个页面不同地方使用,还可以在不同的页面使用。

1
2
3
4
5
6
7
8
9
10
@helper sum(int a,int b)
{
var result=a+b;
  @result

}
<div >
<p>2+3=@sum(2,3)</p>
<p>5+9=@sum(5,9)</p>
</div>

自带类型转换

  • AsInt()
  • IsInt()
  • AsBool()
  • IsBool()
  • AsFloat()
  • IsFloat()
  • AsDecimal()
  • IsDecimal()
  • AsDateTime()
  • IsDateTime()
  • ToString()