前言 相信前端工程师应该很熟悉一个概念:微前端开发。通过将应用拆分为多个模块,各个模块独立开发测试,大大提升了开发灵活性,也降低了模块间的相互影响。
Tauri 的 sidecar 功能其实就是一种类似微前端的方案,通过 sidecar 功能,我们可以直接执行命令行程序并获取其输出,从而实现功能模块的拆分开发。
假设我们现在要通过 Tauri 开发一个视频剪辑软件,使用开源方案 FFmpeg ,由于其本体是基于 c
实现的源代码,我们没办法直接使用,但是我们能找到编译好的可执行文件 FFmpeg Builds binaries ,允许我们通过命令行的形式直接对视频进行剪辑操作。此时我们就可以使用 Tauri 实现图形化界面,sidecar 功能实现对 FFmpeg
的调用,完成视频剪辑软件的基本开发,比如这样 ffmpegGUI 。
当然这个过程并不是一帆风顺而十分美好的,让我们一起来试试。
简单上手 第一个命令行程序 创建命令行程序的方法有很多,相信大家也都有各自习惯的流程,我们简单地以 Visual Studio + c#
为例来进行我们的开发。我们首先创建一个基于 .NET Framework 的命令行程序,Visual Studio 会帮助我们自动生成工程文件。
还记得程序员上手第一要义吗?Hello, World!
我们简单地添加一行打印然后编译。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;namespace ConsoleApp { internal class Program { static void Main (string [] args ) { + Console.WriteLine("Hello, World!" ); } } }
编译后生成的可执行文件即 .exe 文件我们可以在 ConsoleApp\bin\Debug
目录下找到,在命令行直接运行我们可以发现一切正常,程序可用,接下来就是让我们通过 sidecar 的方式在 Tauri 程序中调用我们的命令行程序。
Sidecar 的实现 根据官方文档 Embedding External Binaries ,我们将 ConsoleApp.exe
复制到 src-tauri
目录下,然后修改我们的 tauri.conf.json
如下,注意不加 .exe
尾缀!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { ... "tauri" : { "allowlist" : { "all" : false , "shell" : { "all" : false , "open" : true , "sidecar" : true , "scope" : [ { "name" : "ConsoleApp" , "sidecar" : true } ] } } , "bundle" : { ... "externalBin" : [ "ConsoleApp" ] , ... } , } }
踩坑:程序命名规范 此时我们直接执行 pnpm tauri dev
看看我们的 ConsoleApp 有没有被正确的进行打包。
然后发现,啪,报错了:
原来我们的命令行程序命名还得符合规范,必须得是 *-x86_64-pc-windows-msvc.exe
,重新命名完后我们可以发现在目录 src-tauri\target\debug
下成功出现了我们的 ConsoleApp.exe
,证明打包成功,我们可以尝试调用了。
Sidecar 的调用 我们首先要对 Greet.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 <script setup lang="ts"> import { ref } from "vue"; import { invoke } from "@tauri-apps/api/tauri"; + import { Command } from '@tauri-apps/api/shell' const greetMsg = ref(""); const name = ref(""); + const sidecarMsg = ref(""); async function greet() { // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command greetMsg.value = await invoke("greet", { name: name.value }); + const command = Command.sidecar('ConsoleApp') + const output = await command.execute(); + sidecarMsg.value = output.stdout; } </script> <template> <div class="card"> <input id="greet-input" v-model="name" placeholder="Enter a name..." /> <button type="button" @click="greet()">Greet</button> </div> <p>{{ greetMsg }}</p> + <p>{{ sidecarMsg }}</p> </template>
现在当我们按下 Greet 按钮时,如图所示,除了会执行原本的 greet
命令,还会通过 sidecar 功能执行我们的命令行程序,并获得其输出。
实战操作 添加相关库 对于实际可用的命令行程序来说,肯定还要能够添加对参数的识别和一定的功能。对于基于 c# 的命令行程序,推荐使用 Command Line Parser 作为参数识别工具,让我们来简单添加一下。
首先右键项目,然后点击 Manage NuGet Packages
在 Browse 里搜索 CommandLineParser
,第一个应该就是,点击右边下载按钮即可
改造命令行程序 假设现在我们需要这样一个功能,获取文件创建时间和修改时间,用不同参数加以区分,我们对原来的程序进行如下改造
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 using System;using System.IO;using System.Collections.Generic;using CommandLine;using System.Text;namespace ConsoleApp { internal class Program { public class Options { [Option('t', "test" , Required = false, HelpText = "Trigger test actions." ) ] public bool IsTest { get ; set ; } [Option('c', "create_time" , Required = false, HelpText = "Query file create time" ) ] public bool IsQueryCreateTime { get ; set ; } [Option('m', "modify_time" , Required = false, HelpText = "Query file modify time" ) ] public bool IsQueryModifyTime { get ; set ; } [Value(1, HelpText = "Target file lists." ) ] public IEnumerable<string > FileList { get ; set ; } } static void TestActions () { var currentTime = DateTime.Now.ToString("HH:mm:ss" ); Console.WriteLine(currentTime + " Message from stdout" ); Console.Error.WriteLine(currentTime + " Message from stderr" ); } static void Main (string [] args ) { Console.OutputEncoding = Encoding.UTF8; Parser.Default.ParseArguments<Options>(args) .WithParsed<Options>(o => { if (o.IsTest) { TestActions(); } foreach (var item in o.FileList) { if (!File.Exists(item)) continue ; FileInfo _file = new FileInfo(item); if (o.IsQueryCreateTime) { Console.WriteLine(_file.CreationTime.ToString()); } else if (o.IsQueryModifyTime) { Console.WriteLine(_file.LastWriteTime.ToString()); } } }); } } }
编译后简单测试通过,准备更新我们的 sidecar 实现
更新 sidecar 实现 需要注意的是,这个时候除了本来的 .exe 文件,我们还有 CommandLine.dll
同样需要打包整合
在复制后我们对 tauri.conf.json
进行修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { ... "tauri" : { "allowlist" : { ... "scope" : [ { "name" : "ConsoleApp" , "sidecar" : true , "args" : true } ] } } , "bundle" : { ... "resources" : [ "CommandLine.dll" ] , ... } , }
对于 Greet.vue
我们只需要改一处地方
1 2 - const command = Command.sidecar('ConsoleApp') + const command = Command.sidecar('ConsoleApp', ["-t"])
踩坑:Not specifying resource map 此时我们尝试按下 Greet 按钮执行命令行程序会发现报错信息
乍一看好像是格式问题,但是在我们将命令行程序的输出固定为 UTF8 格式时,仍然是相同的报错,说明问题与我们的格式无关。那会是什么问题呢?难道是程序没有正确打包?
可我们看 src-tauri\target\debug
目录下 .dll 文件和 .exe 文件都是存在的,直接执行也都没有问题,那是哪里出问题了?莫不是个 bug?
于是我们有了这个 issue: [bug] Always get Uncaught (in promise) invalid utf-8 sequence of 1 bytes from index 2
with sidecar 。
就结果而言,这确实是个bug:虽然看上去我们需要的资源都被打包进去了,但是他们并不能正确的找到彼此。解决方法当然也很简单:1. 等修复,相关的 pr feat: allow specifying a resource map, closes 现在还没被通过,pass。2. 指定 dll 文件的导入,这样相当于所有相关资源我们都需要指定明确的路径,有点麻烦。3. 把所有相关的 dll 都打包到 exe里,懒人懒办法。
对于我们来说,实现 3 是最简单的,甚至可以说是一步解决:因为我们只需要下载安装一个库 Costura.Fody
然后再次编译,得到如下输出目录,可见 dll 已经被打包到了我们的 exe 程序中
让我们用最新的程序替换之前的程序,再次尝试执行会发现,一切都没有问题了
注意事项 Costura.Fody 实际是将 ConsoleApp\bin\Debug or Release
目录下的所有 .dll 文件打包进 .exe,所以如果有第三方或者说自己写的 .dll,不要忘记添加 reference
并设置为 copy if newer
。
再进一步 当我们完成上述实践后,对如何基于 sidecar 开发应该已经有了一个初步的认识。踩坑当然不一定只踩了两个坑嘛,虽然这个坑可能很多人暂时是遇不到的:套娃。
事情是这样的,我已经为在开发的程序写好了最底层的 dll,为了方便的实现一部分功能,里面会通过 powershell 的实例执行一部分操作,类似这样
1 2 3 4 5 6 7 8 9 10 11 12 13 private void UnpinFolderFromQuickAccess (string path ) { using (var runspace = RunspaceFactory.CreateRunspace()) { runspace.Open(); var ps = PowerShell.Create(); var removeScript = $"((New-Object -ComObject shell.application).Namespace(\"shell:::{{679f85cb-0220-4080-b29b-5540cc05aab6}}\").Items() | Where-Object {{ $_.Path -EQ \"{path} \" }}).InvokeVerb(\"unpinfromhome\")" ; ps.AddScript(removeScript); ps.Invoke(); } }
截止到我们的 exe,程序执行一切正常,但当我们通过 sidecar 调用时,我们会发现程序会卡死在 ps.Invoke()
,加了 try...catch...
后才能保证不影响程序的执行。
结语 Sidecar 无疑是个特别重要的功能,但目前来看也确实存在一些不完善之处,如果再遇到坑的话我也会及时更新。
希望我收获的这些经验能对你有所帮助,下个坑见!