Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

tw93-pake

https://github.com/tw93/Pake
https://github.dev/tw93/Pake
https://deepwiki.com/tw93/Pake

  • 代码量比较小,项目思路很好;
$ tokei
===============================================================================
 Language            Files        Lines         Code     Comments       Blanks
===============================================================================
 Dockerfile              1           56           38           12            6
 JavaScript              8         1116          989           24          103
 JSON                    9          304          304            0            0
 Python                  1           35           33            0            2
 Rust                    9          532          460            3           69
 TOML                    2           51           40            6            5
 TypeScript             24         1079          877           50          152
-------------------------------------------------------------------------------
 Markdown                7         2043            0         1709          334
 |- BASH                 5           56           41            9            6
 |- Shell                2           68           62            4            2
 |- TypeScript           2           10           10            0            0
 (Total)                           2177          113         1722          342
===============================================================================
 Total                  61         5216         2741         1804          671
===============================================================================
  • 代码很值得一读,大致过程:
    1. 预设了一个 tauri 的空置项目;
    2. 写了一个 js 的 cli,读取配置,写入本地的 tauri 的配置 json 中 (关键参数是目标网页的 url);
    3. 通过 js cli 调用 shell 命令执行 tauri build.
    4. tauri build 过程中,关键逻辑:根据配置 json 中的 url,打开一个 webview 来渲染远程目标网页;app build 过程中也加载了一些插件来自定义 app;
  • 简单来讲就是一个:基于 tauri 实现的,套壳 webview 来加载 website 的客户端 app。 (和 ionic 的 capacitor 的https://capacitorjs.com/ 功能类似)

rust部分的代码量比较小,比较适合当rust的入门新手项目来读;js部分的cli也写的很好,值得借鉴;

关键代码

cli逻辑

dist/cli.js

  • 关键配置信息:windows.url:配置要打包的网页的url;

var windows = [
 {
  url: "https://weread.qq.com",
  url_type: "web",
  hide_title_bar: true,
  fullscreen: false,
  width: 1200,
  height: 780,
  resizable: true,
  always_on_top: false,
  dark_mode: false,
  activation_shortcut: "",
  disabled_web_shortcuts: false
 }
];

var pakeConf = {
 windows: windows,
 user_agent: user_agent,
 system_tray: system_tray,
 system_tray_path: system_tray_path,
 inject: inject,
 proxy_url: proxy_url
};
  • 将cli中的配置写入到预设项目的 json 中;
async function mergeConfig(url, options, tauriConf) {
    ...
    ...
    const configJsonPath = path.join(tauriConfigDirectory, 'tauri.conf.json');
    await fsExtra.outputJSON(configJsonPath, tauriConf2, { spaces: 4 });
}

  • 配置初始化后,执行 tauri build 命令,构建 app;
async buildAndCopy(url, target) {
    ...
    ...
    await shellExec(`cd "${npmDirectory}" && ${this.getBuildCommand()}`);
    ...
    ...
}

tauri部分

src-tauri/tauri.conf.json

  • tauri项目的核心配置文件;

src-tauri/pake.json

  • 自定义关键配置:
{
  "windows": [
    {
      "url": "https://weread.qq.com",
      "url_type": "web",
      "hide_title_bar": true,
      "fullscreen": false,
      "width": 1200,
      "height": 780,
      "resizable": true,
      "always_on_top": false,
      "dark_mode": false,
      "activation_shortcut": "",
      "disabled_web_shortcuts": false
    }
  ]
}

src-tauri/src/app/config.rs

  • 定义了一个配置结构体,主要是读取 pake.json 中的配置项, 然后给tauri项目使用;
#![allow(unused)]
fn main() {
#[derive(Debug, Serialize, Deserialize)]
pub struct WindowConfig {
    pub url: String,
    pub hide_title_bar: bool,
    pub fullscreen: bool,
    pub width: f64,
    pub height: f64,
    pub resizable: bool,
    pub url_type: String,
    pub always_on_top: bool,
    pub dark_mode: bool,
    pub disabled_web_shortcuts: bool,
    pub activation_shortcut: String,
}

...
...

#[derive(Debug, Serialize, Deserialize)]
pub struct PakeConfig {
    pub windows: Vec<WindowConfig>,
    pub user_agent: UserAgent,
    pub system_tray: FunctionON,
    pub system_tray_path: String,
    pub proxy_url: String,
}

}

src-tauri/src/lib.rs

  • tauri app 的入口函数,主要是初始化一些插件和配置项, 以及调用自定义的核心逻辑: set_window函数;
#![allow(unused)]
fn main() {
pub fn run_app() {
...
...
#[allow(deprecated)]
    tauri_app
        .plugin(window_state_plugin)
        .plugin(tauri_plugin_oauth::init())
        .plugin(tauri_plugin_http::init())
        .plugin(tauri_plugin_shell::init())
        .plugin(tauri_plugin_notification::init())
        .plugin(tauri_plugin_single_instance::init(|_, _, _| ()))
        .invoke_handler(tauri::generate_handler![
            download_file,
            download_file_by_binary,
            send_notification,
        ])
        .setup(move |app| {
            let window = set_window(app, &pake_config, &tauri_config);
            set_system_tray(app.app_handle(), show_system_tray).unwrap();
            set_global_shortcut(app.app_handle(), activation_shortcut).unwrap();
            // Prevent flickering on the first open.
            window.show().unwrap();
            Ok(())
        })
        .on_window_event(|_window, _event| {
            #[cfg(target_os = "macos")]
            if let tauri::WindowEvent::CloseRequested { api, .. } = _event {
                let window = _window.clone();
                tauri::async_runtime::spawn(async move {
                    if window.is_fullscreen().unwrap_or(false) {
                        window.set_fullscreen(false).unwrap();
                        tokio::time::sleep(Duration::from_millis(900)).await;
                    }
                    window.minimize().unwrap();
                    window.hide().unwrap();
                });
                api.prevent_close();
            }
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
...
}

}

src-tauri/src/app/window.rs

  • tauri app 新建webview窗口,加载远程网页或者本地网页,这一段是核心功能逻辑
#![allow(unused)]
fn main() {
pub fn set_window(app: &mut App, config: &PakeConfig, tauri_config: &Config) -> WebviewWindow {
...
...
let url = match window_config.url_type.as_str() {
        "web" => WebviewUrl::App(window_config.url.parse().unwrap()),
        "local" => WebviewUrl::App(PathBuf::from(&window_config.url)),
        _ => panic!("url type can only be web or local"),
    };

    let config_script = format!(
        "window.pakeConfig = {}",
        serde_json::to_string(&window_config).unwrap()
    );

    let mut window_builder = WebviewWindowBuilder::new(app, "pake", url)
        .title("")
        .visible(false)
        .user_agent(user_agent)
        .resizable(window_config.resizable)
        .fullscreen(window_config.fullscreen)
        .inner_size(window_config.width, window_config.height)
        .always_on_top(window_config.always_on_top)
        .disable_drag_drop_handler()
        .initialization_script(&config_script)
        .initialization_script(include_str!("../inject/component.js"))
        .initialization_script(include_str!("../inject/event.js"))
        .initialization_script(include_str!("../inject/style.js"))
        .initialization_script(include_str!("../inject/custom.js"));
...
...
}

}

src-tauri/src/app/invoke.rs

  • 这一段主要实现了一个tauri的插件,供tauri app使用,有点类似:移动app中,原生实现的js bridge给h5使用;
#![allow(unused)]
fn main() {
#[command]
pub async fn download_file(app: AppHandle, params: DownloadFileParams) -> Result<(), String> {
    let window: WebviewWindow = app.get_webview_window("pake").unwrap();
    show_toast(&window, &get_download_message(MessageType::Start));

    let output_path = app.path().download_dir().unwrap().join(params.filename);
    let file_path = check_file_or_append(output_path.to_str().unwrap());
    let client = ClientBuilder::new().build().unwrap();

    let response = client
        .execute(Request::new(
            Method::GET,
            Url::from_str(&params.url).unwrap(),
        ))
        .await;

    match response {
        Ok(res) => {
            let bytes = res.bytes().await.unwrap();

            let mut file = File::create(file_path).unwrap();
            file.write_all(&bytes).unwrap();
            show_toast(&window, &get_download_message(MessageType::Success));
            Ok(())
        }
        Err(e) => {
            show_toast(&window, &get_download_message(MessageType::Failure));
            Err(e.to_string())
        }
    }
}

}