0%

使用Git Parameter简化GitFlow工作方式的持续集成

  • 安装Git Parameter Plugin
  • 设置 Jenkins Job ‘this project is parameterized’
  • 设置变量BRANCH_NAME,变量类型为Branch or Tag, 默认为master
  • 设置 Source Code Management - Branches to build 为 $BRANCH_NAME

完成后可使用该项目的build with Parameters功能,即选择特定分支进行构建

日常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
git config --global user.email "QQs@xxxx.xxx"
git config --global user.name "QQs"
git init
git remote add origin git@xxxx.xxx:projectX.git
// 添加远程主机命名为origin
git branch -r
// 查看远程分支
git pull origin dev:develop
// 拉取origin上的dev分支 命名为develop
git add .
git commit -m "blablabla"
git push origin dev
// 将修改推送到origin上的dev分支上
// clone and rename
git clone git@xxxxxx YourFolderName

clone指定分支

1
git clone -b release git@xxxx.xxx:projectX.git

创建分支

1
git checkout -b bug/fixXXXissue

忽略本地修改拉取远程分支

1
2
git fetch --all
git reset --hard origin/master

可以认为git pull是git fetch和git merge两个步骤的结合

git pull <远程主机名> <远程分支名>:<本地分支名>

//取回远程主机某个分支的更新,再与本地的指定分支合并。

1
2
3
4
5
6
7
8
git fetch origin master:temp 
//在本地新建一个temp分支,并将远程origin仓库的master分支代码下载到本地temp分支
git diff tmp
//来比较本地代码与刚刚从远程下载下来的代码的区别
git merge tmp
//合并temp分支到本地的master分支
git branch -d temp
//删除temp分支

回退

1
git reset --hard cd8462xxxx

即将HEAD游标指向到之前的一次commit

git clean -fxd

-f —force
-x 移除包括被gitignore忽略的文件 支持-e参数(if have)
-X 移除所有git ignore的文件 用于从头开始重建一切
-d 递归并移除untrack目录

部分检出

1
2
git config core.sparsecheckout true
echo /db/* >> .git/info/sparse-checkout

reset revert

1
2
3
4
git reset --soft <commit> // 重置到<commit>版本,修改内容放入缓存区(unstage)
git reset --hard <commit> // 重置到<commit>版本,修改内容永久删除

git revert <commit-last> .. <commit-somewhere> // 提交一个记录来撤销所罗列出的<commit>

rebase

1
2
3
4
git checkout feature/new_featueXXX
git rebase develop
// resolve merge conflict
git rebase --continue
1
git rebase -i <start-commit> <end-commit> //(start-commit, end-commit] 前开后闭区间,合并到当前分支,默认 end-commit 为当前 HEAD

把 develop rebase 进 feature/new_featueXXX 分支,develop为上游(Upstream), checkout 的new_featueXXX 分支为Currnet Branch.

每将一次develop的commit rebase进feature 都合并为一个中间版本commit,然后 git rebase —continue。实际中,rebase过程中可能产生冲突,如果两条分支都含有多次commit,且修改内容相互渗透,产生很多冲突,continue时是个中间版本 很难保证复合变基的逻辑吧 那将使这种”规范”失去意义 索性直接merge算了

rebase的取消
rebase完成后没有与该操作对应的commit记录,即不改变前后commit的个数(只调整顺序)但是git本身是有所有操作的记录的,因此任何操作都可以回退, 使用git reflog显示这些记录 并选择标记头进行回退

1
2
3
git reflog
---显示action历史---
git reset --hard HEAD{10}

git rebase 的撤销

包括git reset到之前的版本,此时HEAD会指向到旧版本,较新的commit不在git log中可见了,可以通过git reflog查看 tip: git reflog —date=iso查看操作时间
找到commit的SHA号码 git reset到它即可

git cherry-pick

1
2
git checkout develop
git cherry-pick f2ef69d 9839b06

例如f2ef69d 9839b06是release上刚修好的bug,可以使用上述命令将两处修改直接复制到develop分支

repository 迁移

1
2
3
4
5
git clone --bare git@old-repo.git 
cd old-repo
git remote add bitbucket git@bitbucket-repo.git
git push --all bitbucket
git push --tags bitbucket

stale branches 和 remote tracking branch

remote tracking branch是一个引用(reference),表示是远端分支的状态,不应被修改

stale branch是远端已经移除的remote tracking branchStackOverflow:What is a “stale” git branch?

git log

退出日志文本是按q,同vim

查看所有分支对当前目录的修改,并显示所修改文件:
git log —stat —graph —all

stage

git status
git add/rm
git reset ./temp.txt
git checkout —

tag

标签tag用于标记一个commit

1
git tag -a v1.0.3 -m "bump version to v1.0.3"

使用git tag命令查看所有标签,使用git checkout检出指定标签版本
移除尚未推送到远端的标签:
1
git tag -d v1.0.3

git blame

submodule

为项目添加子模块

1
git submodule add https://example.com/demo/lib1 lib1

关联了子模块的项目含有.gitmoudles文件 形如
1
2
3
4
5
6
7
8
9
[submodule "lib1"]
path = lib1
url = https://example.com/demo/lib1
[submodule "lib2"]
path = lib2
url = https://example.com/demo/lib2
[submodule "lib3"]
path = lib3
url = https://example.com/demo/lib3

拉取项目后源码中不包含这些子项目 使用git submodule命令更新
1
2
3
git submodule init lib1 lib2 #init命令可以按需初始化 lib1 lib2写入项目config
git submodule update


大型项目递归拉取submodule
1
git submodule update --init --recursive

ssh协议改https

1
2
3
git remote add temp_remote_name https://xxxxxxx.git
git remote remove origin
git remote rename temp_remote_name origin

git worktree

1
2
3
4
git worktree add -b feature-render ..\project-feature-render
git worktree list

git worktree remove ..\project-feature-render

git worktree 区别于 git clone 共享git对象数据库 比git clone要快

worktree目录中看不到完整的git log 仿佛是新的仓库 不能checkout其他分支 但是可以rebase

使用pnpm避免node_modules重复

git flow

feat 增加新的业务功能

fix 修复业务问题 / BUG

perf 优化性能

style 更改代码风格,不影响运行结果

refactor 重构代码

revert 撤销更改

test 测试相关,不涉及业务代码的更改

docs 文档和注释相关

chore 更新依赖 / 修改脚手架配置等琐事

workflow 工作流改进

ci 持续集成相关

types 类型定义文件更改

troubleshooting

error: object file .git/objects/61/9151e2619bc36c3c4f5f0c86432b2ca651706d is empty fatal: loose object 619151e2619bc36c3c4f5f0c86432b2ca651706d (stored in .git/objects/61/9151e2619bc36c3c4f5f0c86432b2ca651706d) is corrupt

尝试用下列方法修复

1
2
3
# 删除.git/objects/*/目录下的空文件
git fsck --full
git gc --auto

git fsck命令用于检查文件有效性和连贯性
git gc 清理不必要的文件并优化本地存储库

Postman
postform-data-auth
postform-data
Node.js Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const express = require('express');
const app = express();
var multer = require('multer');
var upload = multer();

// Parse URL-encoded bodies (as sent by HTML forms)
app.use(express.urlencoded());

// for parsing multipart/form-data
app.use(upload.array());
app.use(express.static('public'));
app.post('/', function(request, response){
console.log(request.body);
response.json({res:'ok',data:request.body})
});

const listener = app.listen(process.env.PORT || 3000, function () {
console.log('Your app is listening on port ' + listener.address().port);
});

Node.js client
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
const http = require('http')
const FormData = require('form-data')

function btoa(str) {
return Buffer.from(str).toString('base64')
}
const formData = new FormData();
formData.append('username', 'QQs');
formData.append('password', btoa('*****'));
const authorizationData = 'Basic ' + btoa('authorizationID:authorizationPassword');
const options = {
hostname: 'localhost',
port: 3000,
path: '',
method: 'POST',
headers: formData.getHeaders(),
auth: authorizationData
};
const req = http.request(options, (res) => {
res.setEncoding('utf8');
res.on('data', (chunk) => {
console.log(`BODY: ${chunk}`);
});
res.on('end', () => {
console.log('No more data in response.');
});
});
req.on('error', (e) => {
console.error(`problem with request: ${e.message}`);
});
formData.pipe(req);

Angular httpclient
1
2
3
4
5
6
7
8
9
10
11
12
13
const formData = new FormData();
formData.append('username', 'QQs');
formData.append('password', btoa('*****'));
const postUrl = 'http://loacalhost:3000/';
const authorizationData = 'Basic ' + btoa('authorizationID:authorizationPassword');
const headers = new HttpHeaders({
'Authorization': authorizationData
});
return this.http.post(postUrl, postData, { headers: headers }).pipe(
map(response => {

})
)

html form
1
2
3
4
5
<form action="http://authorizationID:authorizationPassword@localhost:3000/" method="post" enctype="multipart/form-data">
<input type="text" name="username" id="username">
<input type="text" name="password" id="password">
<input type="submit" value="Post">
</form>

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 直接触发危险硬件动作。
参考