所有由罗佳(博主)发布的文章

拦截微软电脑管家的安装或执行

首先不要指望通过调整windows自己的设置来禁止这个流氓行为,微软已经沉迷于给用户喂屎,不会给你关闭的选项的(甚至你反复删除之后它依然会反复自动安装)。

所以这里用的是杀毒软件拦截,比如通过在火绒添加以下自定义规则即可拦截其安装或运行。

这里用通配符匹配微软电脑管家的程序目录名称,并且禁止该目录下一切的创建和修改以及执行行为,只保留删除操作,这样即使它已经被以各种奇怪的方式装进了你的电脑里,也可以确保它里面的程序不会被执行。

如果要问我为什么要对这个软件这么恨之入骨,我只能说对于未经许可偷偷安装的软件(简称流氓软件)就要用这样的态度对待它。我也不是没有尝试过打开它看看,但打开之后发现它确实就是一坨屎,没有任何实际的功能,完全就是个其它程序的启动器,但是它却还起了好几个webview来浪费我们的内存,这不值得。

 

[Zig]使用 std.Thread.Pool 线程池

我发现Zig的官方文档里似乎没有写这个`std.Thread.Pool`的具体用例,所以写一篇笔记给大家参考一下。

这个Zig标准库中的线程池主要用到两个部分,一个是线程池本身的结构体`std.Thread.Pool`,一个是用于等待任务结束的`std.Thread.WaitGroup`(等待组)结构体。等待组的作用是把一组相关的任务放在一起,之后可以使用特定方法等待它们全部完成。

流程:

  • 在线程池初始化好了之后在创建线程任务时给pool.spawnWg方法传入指定的WaitGroup和要执行的函数以及参数
  • 分配任务时线程池就会开始执行任务
  • →随时使用`pool.waitAndWork`方法阻塞当前线程,等待WaitGroup中所有任务完成
  • ↘或者在任何时候使用`wait_group.isDone()`方法检查任务是否全部完成,如果没有完成可以让主线程去做其他事情。

以下直接写一个示例,内容依然是在我前面的笔记中已经出现了好几次的多线程数据竞争示例。

const std = @import("std");
const debug = std.debug;
const Pool = std.Thread.Pool;
const Thread = std.Thread;
const WaitGroup = std.Thread.WaitGroup;

const print = debug.print;

const NUM_THREADS = 2;

var sum: u64 = 0;//让几个线程分别把这个sum自增,完成后sum应当由于非加锁自增导致其值为一个随机数
pub fn sumTask(idx: usize) void {
    const tid = Thread.getCurrentId(); // 获取线程真实的id
    print("Thread {} (idx:{}) : starting...\n", .{ tid, idx });
    var timer = std.time.Timer.start() catch unreachable; //获取一个计时器,用于之后计算线程执行时间
    const y = ∑
    for (0..50000000) |_| {
        y.* += 1;
    }
    print("Thread {} (idx:{}) : done in {}ms.\n", .{ tid, idx, timer.read() / 1000000 });
}
test "threadpool" {
    const tid = Thread.getCurrentId(); // 获取线程真实的id
    print("Main thread {} start\n", .{tid});
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    var thread_pool: Pool = undefined;
    try thread_pool.init(.{ //初始化线程池
        .allocator = allocator,
        .n_jobs = NUM_THREADS, //线程数
    });
    defer thread_pool.deinit();
    var wait_group: WaitGroup = undefined;
    wait_group.reset(); //初始化等待组
    //分配10个任务,线程池内的空闲线程会分别完成这些任务
    for (0..10) |idx| {
        thread_pool.spawnWg(
            &wait_group,//传入等待组
            sumTask,//传入任务函数
            .{idx},//任务函数的参数元组
        );
    }
    thread_pool.waitAndWork(&wait_group); //等待以上分配的任务全部完成
    print("Main: sum = {}\n", .{sum});
    print("Main: program completed. Exiting.\n", .{});
}

需要注意的是,如果在分配完任务后立刻使用`thread_pool.waitAndWork`等待任务完成(像上面的示例中一样,也就是在任务没有全部完成时就开始等待),你会发现虽然设置的线程数是2,理论上任务应该两个两个一起执行,但实际上却是3个任务一起执行的,因为调用这个等待方法的线程也执行了任务。我当时也疑惑了一会儿,然后想明白了,既然等待线程已经等在那里了,那么与其干等不如一起把任务做完,这样的逻辑确实是合理的,但这样实际并行执行的线程就会多一个,这是要注意的地方。

另一个要注意的是,目前的线程池实现中每一个线程启动之后就会开始从任务队列的开头取任务,但添加任务的时候也是添加到队列的开头的,所以虽然不严格保证顺序(因为你后续还可以继续添加任务),但任务队列大致上是后进先出的,所以不要连续不断地添加任务,至少要等线程池任务清空了才可以继续添加新的任务。(我觉得这样不太合理)

配置VSCode CodeGeeX插件连接本地语言模型的提示词

CodeGeeX插件支持连接本地语言模型服务来提供补全功能,可以避免发送出去的源码泄露的风险。

我使用LM Studio起了一个服务端,然后配置插件去连接,虽然连接可以正常连上,但是补全代码时完全不会返回正确的内容,要么就是复读,要么就是连着发送的原始内容一起返回回来。

然后我发现了,“参数配置”里的两个“使用默认提示词”选项,并不是使用插件自带的默认提示词的意思,而是使用服务端配置的提示词的意思,所以如果服务端没有配置系统提示词的话,那么补全的时候就会乱答。

但即使我把“使用默认提示词”取消勾选,使用插件设置里自带的提示词,依然会进行错误的补全,然后我认为是不是这个插件提供的提示词其实是不对的,于是我改了一下,把代码补全的提示词改为如下内容:

你正在执行编辑器的文本或代码内容补全任务,编辑器将按以下结构向你提供用户正在编辑的文件内容:
"###LANGUAGE:当前文件所用的语言
###MODE:BLOCK
<|code_suffix|>光标之后的内容<|code_prefix|>光标之前的内容<|code_middle|>"
你需要预测在"光标之前的内容"和"光标之后的内容"之间应该插入的内容。
你只要输出你预测要插入的内容,不要附加其它任何内容。

然后再进行补全,这下补全的内容终于正常了。

额外提示,提示词模板需要按照下面的设置,否则也无法正确生成结果:

Before System:     <|system|>\n
After System:      \n
Before User:       <|user|>\n
After User:        \n
Before Assistant:  <|assistant|>\n
After Assistant:   \n

[WebAssembly]使用Zig语言制作wasm模块

“前几篇文章你不是还在写C++的wasm笔记,为什么突然开始使用Zig了?”

实际上由于C++古老的语言特性导致有的时候必须把声明和定义分开来写非常繁琐,而且在Emscripten环境下要添加第三方的库比较麻烦,所以我想改到一个自带包管理的语言。于是就首先捡起以前学到一半被劝退的Rust,这个国庆我花了几天时间把rust的教程看完了,但我又一次被这个反人类语言极致的复杂性劝退了,也就有了上一篇文章的内容。

在群友的推荐下我发现Zig这个类C的语言好像还行,于是抱着试一试的心态来学习一下,也就有了这篇笔记。不过不代表我之后就会用这个语言写项目,还有待观察。

这篇笔记的主要内容不是怎么编写Zig语言,而是如何把一个Zig项目编译成wasm模块,因此需要先有一点点的zig基础才能看懂。现在zig的文档和社区生态还不是很完善,且有的东西还在变化,有的东西是查不到或者很难查到的,我花了一些时间摸索才搞明白一些要点,这就是写这篇笔记的原因。

环境准备

如果还没有zig环境,可以用scoop快速部署,只需执行两个脚本即可

安装scoop

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression

用scoop安装最新版的zig,我写这篇笔记的时候zig版本是0.14.0-dev.1798,部分特性有可能在以后改变。

scoop bucket add versions
scoop install versions/zig-dev

以上即可完成zig环境的安装。顺带一提,使用scoop update zig-dev可以更新zig-dev的版本。

简单的示例

最简单快速的方式就是创建一个zig文件,然后使用命令把它编译成wasm文件。

main.zig 文件(由于代码高亮插件没有支持这个语言,而且以下示例看着挺像rust的,所以先用rust高亮来凑活一下)

const std = @import("std");

pub fn main() !void {
    const poi: u16 = 666;

    std.debug.print("poi: {}\n", .{poi});
}

然后使用命令编译

zig build-exe main.zig -target wasm32-wasi

此时目录下就会多出一个main.wasm和一个main.wasm.o文件(以后不同版本可能有所区别),和Emscripten不同的是它不会帮你生成一个胶水js文件供你直接加载。

这里我们使用的是wasi接口的wasm,如果你的运行环境是wasmtime的话,那么就可以直接执行这个wasm文件了。

生成的结果使用nodejs也可以运行,但要在nodejs中运行它,我们得自己为加载wasm写一些代码,还好也不多:

start.js 文件

const { readFile } = require('node:fs/promises');
const { WASI } = require('node:wasi');
const { argv, env } = require('node:process');
const { join } = require('node:path');

const wasi = new WASI({
	version: 'preview1',//该项必须定义,见文档:
	// https://nodejs.org/docs/latest-v20.x/api/wasi.html#new-wasioptions
	args: argv,//该项不是必须的,用于把node运行参数传给wasm程序
	env,//该项不是必须的,用传递环境变量
	
	//其它参数也见上面的文档链接
	//注意这是一个较新的特性,不同nodejs版本的接口可能会有区别,注意选择对应的文档版本
});

(async () => {
	const wasm = await WebAssembly.compile(
		await readFile(join(__dirname, 'main.wasm')),
	);
	const instance = await WebAssembly.instantiate(wasm, wasi.getImportObject());
	wasi.start(instance);
})();

使用nodejs执行以上js文件将得到输出结果:poi: 666

继续阅读[WebAssembly]使用Zig语言制作wasm模块

在SMB中创建符号链接

我直接使用命令在SMB卷上创建一个测试的链接,命令和返回的信息如下(注意该命令的/d选项需要使用管理员权限运行,否则会直接返回Access is denied,详见底部注意事项):

C:\Users\luojia> mklink /d "\\smb创建目标路径" "\\smb上被链接的目录"
You do not have sufficient privilege to perform this operation.

我已经使用管理员身份运行了,依然提示权限不足,那么就说明并不是我本机的权限不足,而是host端的权限问题了。进到SMB宿主机系统里,看了看共享权限和文件夹权限都已经全部给足了,问题也不在这里。

刚刚我查了一下,发现其实并不是SMB协议上没有能力创建符号链接,而是对于这个操作还有一个额外的选项对其进行权限控制。

修改方法很简单:

  1. 在“运行”里打开 secpol.msc
  2. 进入“本地策略”→“用户权限分配
  3. 双击“创建符号链接”会跳出一个属性框,默认应该有个管理员用户组,点击下面的“添加用户或组”添加你的SMB账户,或者如果你不介意的话也可以直接添加“Everyone”用户。
  4. 点击确定之后再到自己本机上测试,已经可以在SMB上创建符号链接了。

到这里应该已经完成了链接的创建,如果还有问题就接着看下文。

 

这里是一些mklink相关的注意事项和额外说明:

  • mklink的 /d 选项(需要管理员权限)创建的是符号链接,对象可以是目录或文件,它可以在本地使用绝对或相对路径创建链接。
  • mklink的 /j 选项创建的是重解析点(也叫目录连接点,仅用于目录),必须使用绝对路径。这个选项是无法在SMB上创建的,会提示 Local NTFS volumes are required to complete the operation.
  • mklink的 /H 选项可以创建同一个存储设备上文件的“硬链接”,即在硬盘上指向的是同一个文件,但你可以在不同的路径创建它的分身。这个选项不可以对目录使用,因为目录本身就是没有文件实体的。
  • 如果要创建SMB上的符号链接,不可以使用映射的驱动器路径,必须使用分享的UNC路径,如 “\\luojia-pc\samba分享\文件或目录”这样的形式,如果使用映射的驱动器路径,会提示“The system cannot find the path specified”或“系统找不到指定路径”。
  • Powershell里无法使用mklink命令,要先输入cmd回车切到cmd或者直接打开cmd才能用。Powershell有另一个New-Item命令可以用于创建符号链接。

如果你在创建符号链接的时候还遇到类似被禁用的提示,比如“The symbolic link cannot be followed because its type is disabled”,可以使用以下命令查看一下策略设定:

fsutil behavior query SymlinkEvaluation

我这里已经设置过了,所以显示的是

Local to local symbolic links are enabled.
Local to remote symbolic links are enabled.
Remote to local symbolic links are enabled.
Remote to remote symbolic links are enabled.

但默认并不是全部启用的,如果你不关心每个条目的意义,可以使用以下命令把它们全部开启

fsutil behavior set SymlinkEvaluation L2L:1
fsutil behavior set SymlinkEvaluation L2R:1
fsutil behavior set SymlinkEvaluation R2L:1
fsutil behavior set SymlinkEvaluation R2R:1

这些设置具体的含义可以参考微软官方文档:https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/fsutil-behavior

 

[WebAssembly]初学笔记 Pthreads多线程

如果有看不明白的地方请先看前置说明文章

对于有高性能要求的可并发任务来说,在程序一开始设计的时候就要把多线程协作考虑进去,因此介绍完了一些emscripten的基本操作之后,我立刻就来到了多线程的章节。

Emscripten提供了两种实现多线程的方式:

  • POSIX Threads (Pthreads) API (以下简称pthreads)(此工具中标准库的std::thread也基于pthreads实现)
  • Wasm Workers API (以下简称wasmworkers)(在这里继续再批判一次官方这破API文档竟然不写每个函数的用法,其它东西扯了一堆)

我参阅了一下wasmworkers的文档,以上两种API基本能实现相同的功能,区别在于它的wasmworkers是直接根据js原生worker原本的API实现的,因此输出的编译结果会比较小,而pthreads实现了完整的pthreads api,所以输出的编译结果会比较大。由于考虑程序的可移植性,所以这里我只学习使用pthreads实现多线程,如果对pthreads不熟悉的话可以看一看这个Rookie-Note上的学习文档简单了解一下,还是挺简单的,我也刚学会才来写了这篇笔记。

本篇笔记不会对pthreads api如何使用进行全面的说明,因为内容比较多且不是专属于wasm的内容,在代码编写上也和普通的 C pthread程序没有什么区别,可以看我上面贴的菜鸟笔记链接去学习,这里主要就说明一下emscripten中pthread的实现方式和举个例子,顺便贴上emscripten的pthread指导链接

首先要注意

  • 使用以上两种多线程API的时候都不可以和 `-sSINGLE_FILE` 编译参数搭配使用,也就是使用多线程就无法把输出文件全部打包到一个js文件里了。
  • 要使用pthreads需要浏览器开启SharedArrayBuffer支持(在目前的浏览器中由于存在安全原因默认是禁用的),在nodejs中可以直接使用。wasmworkers我没有测试,但应该也是一样的。
  • 编译参数:要使用pthreads库,需要在编译时添加 `-pthread` 参数。
  • 由于标准库中的std::thread在emscripten中也是基于pthread实现的,因此如果你比较熟悉标准库的`std::thread`也可以直接使用它,但要记得添加 `-pthread` 编译参数。

继续阅读[WebAssembly]初学笔记 Pthreads多线程

[WebAssembly]初学笔记 使用Embind在Javascript与C++之间交互

如果有看不明白的地方请先看前置说明文章

Embind是emscripten提供的又一种在js和c++之间的交互方案,其提供更加丰富的交互方式,不止是前面的笔记中介绍的那种简单的函数调用。

Embind库API参考文档地址:https://emscripten.org/docs/api_reference/bind.h.html

官方指导文档地址:https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html,下面简单概括一下这个库包含的功能。

1. 在js环境中绑定c++中的(绑定指的是把另一个语言中的概念映射为当前语言中类似的概念):

2. 在c++环境中:使用`val`类操作js中的任意对象。 继续阅读[WebAssembly]初学笔记 使用Embind在Javascript与C++之间交互

[WebAssembly]初学笔记 在C++中嵌入Javascript代码

如果有看不明白的地方请先看前置说明文章

前一篇笔记写了如何在js中调用c++的函数,要在c++里执行js代码也有几种方法,另外和从js到c++的交互只能调用函数和操作内存不同,c++里可以编写完整的js代码并获得结果。

这里依然先附上官方的参考:https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html


使用emscripten_run_script直接执行代码

这是emscripten提供的执行js代码的方法,此方法无法获取js代码的运行结果(类似于返回值),可以获取返回值的版本在下文。这个方案的内部实现方式是把这个代码字符串扔外面的js环境用eval()来执行。

在test.cpp中编写如下代码:

#include <emscripten.h>

int main() {
	emscripten_run_script(
		"console.log('岂因祸福避趋之');\n"
		"console.log('6');");
	return 0;
}

然后执行命令编译:emcc -sMODULARIZE=1 -sASSERTIONS -sEXPORT_ES6 -o dist/test.mjs test.cpp

继续阅读[WebAssembly]初学笔记 在C++中嵌入Javascript代码

[WebAssembly]初学笔记 从Javascript调用C++函数

如果有看不明白的地方请先看前置说明文章

要在js中使用c++编写的函数或者在c++中运行js代码,则需要有一些方法打通这两个环境,官方已经写了一个列表列举出了所有的方法,可以参考:https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html

在此我会对部分方法简单进行举例。

上一篇笔记说到如果不需要自动执行main,可以不写main函数,此时我们需要使用wasm中编写的函数就需要手动设置需要导出的函数。


函数导出

要从C++中导出函数给js环境使用,有两种方法:一种不用改动代码,需要在编译参数上进行指定,另一种需要在代码中添加宏来指定。

这两种方法的使用场景我认为是这样:如果有一份源码不是你写的,或者说就是引用别处的一个库,那么我们去修改其源码肯定不便于后期维护,因此在编译参数中指定导出的方法最稳妥,但如果要编译的源码就是你专门为了这个wasm项目写的,那么使用宏指定可以让编译参数更加简洁,在代码中也可以更清晰地看出导出了哪些函数。

方法一:需要在编译时添加参数指明要导出的函数名称:

-sEXPORTED_FUNCTIONS=_func1,_func2,_func3,其中func1,func2,func3是在c++中你定义的函数名称,作为导出函数时,需要在其名称前面添加一个下划线,然后这些函数就会被添加给wasm的实例对象。

现在将test.cpp的内容修改为下:

#include <iostream>
extern "C" { // 由于C++导出方法会对方法进行重命名,这是用来指定以C形式导出方法

//定义一个无返回值函数,参数为一个int
void poi1(const int val) {
	std::cout << "poi1: " << val << std::endl;//输出val的值
}
//定义一个返回整数的函数,参数为两个int
int poi2(const int val, const int val2) {
	std::cout << "poi2: " << val << " " << val2 << std::endl;//输出两个val的值
	return 114514;
}
//定义一个无返回值函数,参数为一个bool
void boolTest(bool val) {
	std::cout << "bool: " << val << std::endl;
}

}

然后执行命令编译:emcc -sMODULARIZE=1 -sASSERTIONS -sEXPORTED_FUNCTIONS=_poi1,_poi2,_boolTest -o dist/test.mjs test.cpp

继续阅读[WebAssembly]初学笔记 从Javascript调用C++函数

[WebAssembly]初学笔记 安装环境和尝试编译

以前也尝试过学WASM,但由于当时没有应用场景所以安装完环境就弃坑了,这次来好好学习一下用法。这篇笔记是边学边写的,如果有错误或者需要补充的地方请留言。

WASM通常需要使用其他高级语言编写后编译为wasm二进制文件交给运行时去运行,但如果你的头够铁,也可以尝试手写wasm指令,或者通过wasm的文本格式理解它的底层运行原理。

本笔记在Windows平台上使用C++编写源码编译为wasm,所以需要安装emscripten工具包。除了C++以外,Rust也是一个推荐选项,而其它语言目前还没有非常完备的支持。

安装Emscripten

这是用于把C/C++编译到wasm的工具包。

Emscripten的项目地址在:https://github.com/emscripten-core/emscripten

文档地址在:https://emscripten.org/index.html

安装方式官方手册在:https://emscripten.org/docs/getting_started/downloads.html,这里我简单介绍一下,但还以官方手册为准,我不会同步更新。

首先把emscripten仓库克隆到任意位置,然后进入该目录

git clone --depth 1 https://github.com/emscripten-core/emsdk.git

cd emsdk

然后依次执行以下命令进行安装

emsdk.bat install latest
emsdk.bat activate latest --permanent

上面的` –permanent`参数是为了让emsdk的命令在全局可用,否则你需要在每个命令环境中都执行一次activate。执行完了之后可以关闭当前的命令窗口,然后找个地方开始准备写我们的代码了。 继续阅读[WebAssembly]初学笔记 安装环境和尝试编译

[MySQL]JSON数组取对称差集函数

写了个函数,做个记录。作用是扣掉两个数组相同的部分,只保留不同的部分,也就是对称差集。

CREATE FUNCTION `array_diff`(`arr1` json, `arr2` json) RETURNS json
    NO SQL
    DETERMINISTIC
BEGIN
	RETURN (
		WITH 
		A AS (SELECT el FROM JSON_TABLE(arr1, '$[*]' COLUMNS(el INT PATH '$')) T),
		B AS (SELECT el FROM JSON_TABLE(arr2, '$[*]' COLUMNS(el INT PATH '$')) T)
		SELECT JSON_ARRAYAGG(el) FROM (
			SELECT el FROM A WHERE A.el NOT IN (SELECT el FROM B)
			UNION
			SELECT el FROM B WHERE B.el NOT IN (SELECT el FROM A)
		) AS T
	);
END

调用

select array_diff('[1,2,3]','[2,3,4]')

结果

[1, 4]

 

网页视频截图、录制GIF、调整时间脚本

源码地址在:https://github.com/JiaJiaJiang/jia-webvideo-tools

我也写了一个油猴脚本来引用打包好的项目输出文件:https://gist.github.com/JiaJiaJiang/736f5a90b55b815a0e9eb6463dd09061,需要注意,引用的文件位于GitHub,所以需要过墙才可以正常加载。

主要功能均通过Alt+Shift+右键调出的工具条操作,鼠标悬浮在各个项目上会有说明。其它快捷功能见脚本描述或者Github的readme。

截图效果:

[SQLite]导入自己写的扩展出现”SqliteError: 找不到指定的程序”

自己写了个sqlite c++扩展,然后编译之后载入出现了标题上的错误,如果是英文的话应该是”SqliteError: The specified module could not be found”。

出现这个错误并不是sqlite没找到对应的dll文件,如果是没找到文件的话会提示“找不到指定的模块”。

这个情况是sqlite载入dll之后找不到程序入口,但我在 load_extension 和 c++ 代码中的入口名称明明就是一致的,于是我用 `dumpbin /exports dll文件` 查看dll到底导出了什么入口,发现导出了这么个东西:

_Z22我的入口名P7sqlite3PPcPK20sqlite3_api_routines

我把这一串名字写到 load_extension 中之后终于可以正确导入了,但我肯定不能让它一直生成这样不确定的名字,所以开始找找办法让它只导出我指定的名字。

然后发现这是我用了c++的原因,要解决dll导出的入口名和代码里写的不一致的问题,只要在 `__declspec(dllexport)` 前面加上 `extern “C” ` 即可,完整的是

extern "C" __declspec(dllexport)

这样改完再编译,使用dumpbin查看导出的函数名就已经正常了。

让docker构建nodejs应用时使用npm缓存加速安装

我的node应用在原本的部署脚本下,每次部署都要十几分钟,而我的项目又不适合多阶段构建,于是想了各种办法让它使用镜像layer缓存一部分安装过程,但是多多少少都有点问题。

看来想要不出问题的话还是要在部署的时候让安装过程完整跑一次,这样要缩短部署时间就只能尽量利用npm缓存。

第一步:挂个外部缓存

我本来以为dockerfile构建过程中是没法像容器那样绑定一个volume来让过程文件持久化的,但是昨天我才发现其实是有的,就是要更新一下docker,这是Buildkit特性的一部分,这个特性在18.09版本之后的docker才有,总之尽量把docker升级到最新版就可以用了。

使用方法很简单,如下Dockerfile:

FROM node:18.18.1-buster-slim
RUN apt update &&\
	apt install -y git openssh-client python3 curl
COPY ./deploy /deploy
COPY ./app/ /app
RUN --mount=type=cache,target=/root/.npm \   #在RUN命令的开头这样写来挂一个缓存目录,这个mount参数需要在每个要使用此缓存的命令里都写一遍
	sh /deploy/install.sh   #然后执行你的安装脚本
WORKDIR "/deploy"
ENTRYPOINT ["/bin/sh"]
CMD [ "./start.sh" ]
EXPOSE 80

这样在这个dockerfile构建过程中就会把构建容器的`/root/.npm`映射到通用的docker缓存里,而且我测试下来如果在其它镜像里也挂载同样的缓存,那么其它镜像构建的时候也可以使用该缓存,但我其它镜像的FROM镜像都是相同的,不知道如果来源镜像变了是否会影响缓存挂载。

注意:~/.npm 是linux下npm默认的缓存目录,但默认缓存目录是可以更改的,如果你的项目里修改了npm的默认缓存目录地址,那么这里也要一起改。或者如果你的平台比较特殊,npm的默认目录本来就不在这里,那么也要进行对应的修改。

除了挂载缓存(type=cache)以外,还可以绑定外部文件或目录(type=bind),相关资料在这里https://docs.docker.com/build/guide/mounts/,不过这个文档有点迷惑,没写source和target哪个是里面哪个是外面的,我在source里面写外部路径它给我报文件不存在,然后我就直接用cache了,没再继续尝试。

如果你觉得构建过程缓存的文件有问题,或者单纯想清除这些缓存,可以根据这里的方法,执行以下命令:

docker builder prune --filter type=exec.cachemount

如果没有效果,可以尝试去掉–filter及其参数。

第二步:让npm优先使用缓存(可选)

npm在安装依赖时,即使本地有缓存,也会向服务器发起请求检查每个本地的缓存有没有过期,这个过程也很漫长。其实一般即使缓存过期了也问题不大,因为正常来说同一个版本号的包其内容是不会变的,所以可以让npm优先使用本地缓存,跳过检查其在线状态,这样可以大幅减少安装时间。

方法很简单,只要给安装命令加个`–prefer-offline`参数:

npm i --prefer-offline
#也可以再加个 --verbose 参数确认是否真的使用了缓存
npm i --prefer-offline --verbose

这样折腾完了之后,应用的后续部署时间在依赖没有改变的情况下从原来的十几分钟缩短到了一分多钟,堪称火箭级加速,总算解决了一个困扰我一年多的问题。