背景
公司内部希望有一个可以快速开发项目的工具,不用每次都使用基础的脚手架进行初始化再根据项目需求进行定制改动,开源仓库又与公司业务有不少差距,所以决定在公司内部几个典型项目的基础上维护几个通用的示例项目方便开发同学快速进入开发。
解决方案
之前使用electron开发过一些小工具但是安装包体积太大了,所以这次选用tauri来开发。主要功能就是将公司内几个典型仓库内置在工具中,并支持开发者对项目进行快速克隆,完成新项目的初始化操作。
这里我们主要实现如下几个功能:
- 展示模板仓库列表
- 克隆模板仓库
- 记住本机工作目录
- 预览模板仓库 UI 界面
- 单独清除仓库的
.git
文件夹 - 删除本地仓库
开发准备
本次开发环境为 win10
系统,需要安装Rust
及Microsoft Visual Studio C
生成工具。
实现方法
为了方便开发,这里使用element-plus
帮助构建页面。
项目初始化
首先使用create-tauri-app
创建新 Tauri
项目,按照初始化之后的提示执行pnpm install
安装依赖,然后再启动项目。
rust后端代码修改
配置部分
tauri
应用配置(这部分比较重要,一定要准确配置好应用所需接口权限)修改。
修改src-tauritauri.conf.json
文件,在tauri->allowlist
增加如下接口权限配置:
"os": {
"all": true
},
"http":{
"all": true,
"request": true,
"scope":[
"http://**",
"https://**"
]
},
"shell": {
"all": false,
"open": true,
"execute": true,
"scope": [
{
"name": "run-git",
"cmd": "git",
"args": true
}
]
},
"dialog": {
"all": false,
"open": true
},
"path": {
"all": true
}
其中shell
为rust
调用shell
克隆仓库使用,os
、dialog
、path
为需要使用到的系统API
,http
是我们在页面上发送请求所用。
rust
部分
然后修改 src-taurisrcmain.rs
文件,追加如下核心方法:
1.检查文件夹是否存在:
代码语言:rust复制#[tauri::command]
fn check_folder_exists(folder: &str) -> String {
println!("folder: {}", folder);
let path = Path::new(folder);
if path.exists() && path.is_dir() {
return '1'.to_string();
}
return '0'.to_string();
}
2.打开文件夹:
代码语言:rust复制#[tauri::command]
fn open_directory(path: &str) {
let output = match std::env::consts::OS {
"windows" => Command::new("cmd")
.args(&["/C", &format!("start {}", path)])
.output()
.expect("Failed to start"),
"linux" | "macos" => Command::new("open")
.arg(path)
.output()
.expect("Failed to start"),
_ => panic!("Unsupported OS"),
};
if output.status.success() {
println!("Directory opened successfully.");
} else {
println!("Failed to open directory.");
}
}
3.删除文件夹:
代码语言:rust复制#[tauri::command]
fn del_directory(path: &str) -> String {
let dir_path = Path::new(path);
if dir_path.exists() && dir_path.is_dir() {
fs::remove_dir_all(dir_path).expect("无法删除目录");
println!("目录已删除");
return '1'.to_string();
} else {
println!("目录不存在");
return '0'.to_string();
}
}
以上命令需要如下依赖:
代码语言:rust复制use std::env;
use std::fs;
use std::path::Path;
use std::process::Command;
最后在main
函数中注册tauri
命令:
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
greet,
check_folder_exists,
open_directory,
del_directory
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
到此,rust
后端部分就完成了。
前端代码修改
1.集成element-plus
。
首先安装如下几个依赖:element-plus
、less
、less-loader
。
修改src
目录下的main.js
,引入element-plus
:
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
const app = createApp(App);
app.use(ElementPlus);
2.定义外部接口。
仓库列表我们使用一段json,为了方便直接把json放在外部存储上,然后在接口中请求返回。
在srcrequestindex.js
中追加如下代码(没有request文件夹可自己新建):
import { http } from "@tauri-apps/api";
async function getTemplates() {
return await http.fetch(
"https://存储服务/templates.json",
{
method: "get",
responseType: 2,
}
);
}
export { getTemplates };
参考返回数据示例:
代码语言:json复制{
name: "xxx管理系统",
thumbs: [
{
picUrl: 'https://xxx/1.png',
pageName: '登录'
},
{
picUrl: 'https://xxx/2.png',
pageName: '首页'
},
{
picUrl: 'https://xxx/3.png',
pageName: '系统管理'
}
],
git: 'http://xxx.git',
},
这里主要返回了项目名、缩略图数组(用于展示项目的几个典型页面UI)、仓库地址。
3.修改页面。
修改创建项目时自带的 srccomponentsGreet.vue
页面。
引入一些必要依赖:
代码语言:js复制import { invoke } from "@tauri-apps/api/tauri";
import { Command } from '@tauri-apps/api/shell';
import { open as openDialog } from '@tauri-apps/api/dialog';
import { appDataDir } from '@tauri-apps/api/path';
import { type } from '@tauri-apps/api/os';
追加如下函数(介绍几个关键方法):
代码语言:js复制// 选择本地工作目录
async function chooseDir() {
const dir_path = await openDialog({
directory: true,
multiple: false,
defaultPath: await appDataDir(),
});
dir.value = dir_path;
localStorage.setItem('local_dir', dir_path);
}
// 克隆仓库
async function do_clone(params) {
const command = new Command('run-git', params);
command.on('close', data => {
console.log(`command finished with code ${data.code} and signal ${data.signal}`)
cloneLoading.value = false;
ElMessage({
message: '仓库克隆成功',
type: 'success',
})
let clonedProjects = JSON.parse(localStorage.getItem('clonedProjects') || '{}');
clonedProjects[params[1]] = params[2];
localStorage.setItem('clonedProjects', JSON.stringify(clonedProjects));
});
command.on('error', error => {
console.error(`command error: "${error}"`)
cloneLoading.value = false;
});
command.stdout.on('data', line => console.log(`command stdout: "${line}"`));
command.stderr.on('data', line => console.log(`command stderr: "${line}"`));
cloneLoading.value = true;
const child = await command.spawn();
console.log('pid:', child.pid);
}
// 克隆仓库或者打开本地仓库目录
async function open_clone_repo(doc, repoIndex) {
if (isCloned(doc)) {
openCloned(doc)
return
}
if (dir.value == '') {
ElMessage({
message: '请选择工作目录',
type: 'error',
})
return
}
let repo_name = ''
if (projectName.value == '') {
repo_name = doc.substring(doc.lastIndexOf('/') 1).split('.')[0];
}
curRepoIndex.value = repoIndex;
let spliter = '\'
// mac系统
const osType = await type();
if (osType.indexOf('Darwin') != -1) {
spliter = '/'
}
const clone_dir = projectName.value == '' ? dir.value spliter repo_name : dir.value spliter projectName.value;
console.log('local_dir:', clone_dir)
const exists = await invoke("check_folder_exists", { folder: clone_dir });
if ( exists) {
ElMessage({
message: '本地已存在该目录,请重新输入',
type: 'error',
})
return
}
do_clone(['clone', doc, clone_dir])
}
// 删除本地目录
async function del_local(url) {
let clonedProjects = JSON.parse(localStorage.getItem('clonedProjects') || '{}');
if (clonedProjects[url]) {
console.log('del dir:', clonedProjects[url])
ElMessageBox.alert('确定删除吗', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
closeOnClickModal: true,
showCancelButton: true,
showClose: false,
callback: async (action) => {
if (action === 'confirm') {
const status = await invoke("del_directory", { path: clonedProjects[url] })
if ( status) {
ElMessage({
type: 'success',
message: `操作成功`,
})
delete clonedProjects[url];
localStorage.setItem('clonedProjects', JSON.stringify(clonedProjects));
const projectsTemp = projects.value
projects.value = []
projects.value = projectsTemp
} else {
ElMessage({
type: 'error',
message: `操作失败`,
})
}
}
},
})
}
}
// 判断仓库是不是在本地被克隆过
function isCloned(url) {
let clonedProjects = JSON.parse(localStorage.getItem('clonedProjects') || '{}');
if (clonedProjects[url]) {
return true
}
return false
}
4.关键方法的调用按钮:
代码语言:html复制<div>
<el-button :loading="cloneLoading && proIndex == curRepoIndex" type="primary"
@click="open_clone_repo(project.git, proIndex)">{{ isCloned(project.git) ? '打开目录' : '克隆仓库' }}</el-button>
</div>
<el-dropdown class="del-dir" v-if="isCloned(project.git)" split-button type="primary"
@click="del_local(project.git)" @command="handleCommand($event, project.git)">
删除目录
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="clear_git">清除.git文件夹</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
然后一个简单的仓库管理工具就初具雏形了,执行pnpm tauri dev
来测试下。
下次开发项目就可以直接使用这个工具,浏览项目的UI截图,挑选模板仓库中适合需求的项目进行克隆来进行开发了。
参考资料
- tauri 预先准备
- Visual Studio 2022 生成工具
- demo source