Jasper Ji

平常心

尝试用Rust已经有点时间了,之前主要的问题是卡在IndexedDB的使用上,最后的代码是这样。

spawn_local(async move {
        let (tx, rx) = oneshot::channel::<IdbDatabase>();
        let window = web_sys::window().unwrap();
        let idb_factory = window.indexed_db().unwrap().unwrap();

        let open_request = idb_factory
            .open_with_u32(String::from("todo").as_str(), 1)
            .unwrap();

        let on_upgradeneeded = Closure::once(move |event: &Event| {
            let target = event.target().expect("Event should have a target; qed");
            let req = target
                .dyn_ref::<IdbRequest>()
                .expect("Event target is IdbRequest; qed");

            let result = req
                .result()
                .expect("IndexedDB.onsuccess should have a valid result; qed");
            assert!(result.is_instance_of::<IdbDatabase>());
            let db = IdbDatabase::from(result);
            let store:IdbObjectStore = db.create_object_store(&String::from("user")).unwrap();
            let _index = store.create_index_with_str(&String::from("name"), &String::from("name")).expect("create_index_with_str error");

        });
        open_request.set_onupgradeneeded(Some(on_upgradeneeded.as_ref().unchecked_ref()));
        on_upgradeneeded.forget();

        let on_success = Closure::once(move |event: &Event| {
            // Extract database handle from the event
            let target = event.target().expect("Event should have a target; qed");
            let req = target
                .dyn_ref::<IdbRequest>()
                .expect("Event target is IdbRequest; qed");

            let result = req
                .result()
                .expect("IndexedDB.onsuccess should have a valid result; qed");
            assert!(result.is_instance_of::<IdbDatabase>());

            let db = IdbDatabase::from(result);
            let _ = tx.send(db);
        });
        open_request.set_onsuccess(Some(on_success.as_ref().unchecked_ref()));
        on_success.forget();

        let db = rx.await.unwrap();
        let transaction = db.transaction_with_str_and_mode(&String::from("user"), IdbTransactionMode::Readwrite).expect("transaction_with_str error");
        let store = transaction.object_store(&String::from("user")).expect("store error");

        let name = JsValue::from_str(_content_element.value().as_str());
        let add_request = store.add_with_key(&name, &JsValue::from("name")).expect("add error");

        let on_add_error = Closure::once(move |event: &Event| {
            console::log_1(&String::from("写入数据失败").into());
            console::log_1(&event.into());
        });
        add_request.set_onerror(Some(on_add_error.as_ref().unchecked_ref()));
        on_add_error.forget();

        let on_add_success = Closure::once(move |event: &Event| {
            console::log_1(&String::from("写入数据成功").into());
        });
        add_request.set_onsuccess(Some(on_add_success.as_ref().unchecked_ref()));
        on_add_success.forget();

        console::log_1(&String::from("do").into());
 });

因为IndexedDB连接成功是个异步事件,db 只能在成功事件中才能拿到,最初因为对IndexedDB的使用不熟悉,这玩意在很早以前看HTML5的时候就看到过,不过一直没有用过。我在成功事件中调用create_object_store,结果给报错了,实际上创建数据库只能在onupgradeneeded事件中处理。

let myDatabase = MyDatabase::new();
myDatabase.add(String::from("jasper", String::from("name")));

早期的想法是把数据库的操作封装起来,但是连接是异步的,上面的add操作时数据库可能还没有连接成功,db等于为空。可能是之前做iOS的缘故,我一直想着数据库的操作应该是个单例,不过Rust的单例好像不那么简单,一直报错,所以这个想法就先放放了。

早期的时候甚至不知道如何用Rust设置回调,最后发现了这个库kvdb_web,代码也是参考了indexed_db.rs中的方法。这个库并没有演示的例子,所以只是参考了他打开数据库的方法。里面有用到futures::channel,但是实际使用老是报错。后来我忘了Rust写前端,实际上是运行在Wasm虚拟机的环境下,Rust的多线程则是一般的操作系统的环境下了。实际上要在Wasm使用多线程,得用另外一种方式。实际上是用Rust的代码调用JS了,JS是单线程运行的。所以整个思路虽然是用Rust在写,但实际思想的话得跟着JS来走。我想在后面的代码中拿到db,有点类似JS中使用await的方式。最后用了两个库,wasm-bindgen-futures主要使用了spawn_local这个东西,另外还有futures-channel这个库。

spawn_local(async {
	let (tx, rx) = oneshot::channel::<i32>();
	// 省略中间代码
	let on_success = Closure::once(move |event: &Event| {
                // Extract database handle from the event
                let target = event.target().expect("Event should have a target; qed");
                let req = target
                    .dyn_ref::<IdbRequest>()
                    .expect("Event target is IdbRequest; qed");

                let result = req
                    .result()
                    .expect("IndexedDB.onsuccess should have a valid result; qed");
                assert!(result.is_instance_of::<IdbDatabase>());

                let db = IdbDatabase::from(result);
                let _ = tx.send(db);
        });
    open_request.set_onsuccess(Some(on_success.as_ref().unchecked_ref()));
    on_success.forget();
   
    // 等于这块回一直阻塞,直到拿个值。
    let db = rx.await.unwrap();
    // 省略剩余代码
})

futures-channel这个库的用法跟Rust自带那个有点类似,不过只用这个是可以工作的。后面的问题基本上是IndexedDB的用法问题,最后终于可以把数据成功插入了,虽然再次插入数据时会有写入失败的问题,不过这个已经是后面的事了。

当我费了很多周折,成功写入时,也就是写这篇文章时候,突然间发现可以直接用下面的方式写,如果只是要写入数据的话。

let window = web_sys::window().unwrap();
let idb_factory = window.indexed_db().unwrap().unwrap();

let open_request = idb_factory
    .open_with_u32(String::from("todo").as_str(), 1)
    .unwrap();

let on_upgradeneeded = Closure::once(move |event: &Event| {
    let target = event.target().expect("Event should have a target; qed");
    let req = target
        .dyn_ref::<IdbRequest>()
        .expect("Event target is IdbRequest; qed");

    let result = req
        .result()
        .expect("IndexedDB.onsuccess should have a valid result; qed");
    assert!(result.is_instance_of::<IdbDatabase>());
    let db = IdbDatabase::from(result);
    let store: IdbObjectStore = db.create_object_store(&String::from("user")).unwrap();
    let _index = store
        .create_index_with_str(&String::from("name"), &String::from("name"))
        .expect("create_index_with_str error");
});
open_request.set_onupgradeneeded(Some(on_upgradeneeded.as_ref().unchecked_ref()));
on_upgradeneeded.forget();

let on_success = Closure::once(move |event: &Event| {
    // Extract database handle from the event
    let target = event.target().expect("Event should have a target; qed");
    let req = target
        .dyn_ref::<IdbRequest>()
        .expect("Event target is IdbRequest; qed");

    let result = req
        .result()
        .expect("IndexedDB.onsuccess should have a valid result; qed");
    assert!(result.is_instance_of::<IdbDatabase>());

    let db = IdbDatabase::from(result);
    let transaction = db
        .transaction_with_str_and_mode(&String::from("user"), IdbTransactionMode::Readwrite)
        .expect("transaction_with_str error");
    let store = transaction
        .object_store(&String::from("user"))
        .expect("store error");

    let name = JsValue::from_str(_content_element.value().as_str());
    let add_request = store
        .add_with_key(&name, &JsValue::from("name"))
        .expect("add error");

    let on_add_error = Closure::once(move |event: &Event| {
        console::log_1(&String::from("写入数据失败").into());
        console::log_1(&event.into());
    });
    add_request.set_onerror(Some(on_add_error.as_ref().unchecked_ref()));
    on_add_error.forget();

    let on_add_success = Closure::once(move |event: &Event| {
        console::log_1(&String::from("写入数据成功").into());
    });
    add_request.set_onsuccess(Some(on_add_success.as_ref().unchecked_ref()));
    on_add_success.forget();
});
open_request.set_onsuccess(Some(on_success.as_ref().unchecked_ref()));
on_success.forget();

总结

至此算是把IndexedDB的问题解决了,如果想把操作封装起来,那么可以用前面的方式。只是简单的操作的话,后面的代码也是可以的。到目前为止,基本上还没有写网页的东西,Yew的东西,也就只是搭了个架子时有用到,并没有深入的使用。后面得需要好好的深入下,Yew这个框架跟传统的还不太一样,用trunk serve启动后,实际会有一个WebSocket在前后端之间通讯。

再谈谈Rust学习的东西,实际上《Rust权威指南》这本书我只是看了个大概,而目前的实验项目中并没有使用Rust去写很多逻辑的代码,更多的是调用。用Rust写前端这个方式,起步会比一般的难点,主要还是牵涉的东西不只是Rust的问题,还有Wasm以及JS相关的东西,最重要的还是要明白他的运行环境是浏览器中Wasm虚拟机。不过后续如果有大量成熟的库,可以解决这些基础问题的话,只写网页的话,难度会小些。

虽然项目整体进展不大,也走了一些弯路,不过却也让我思考到了更多的东西。现在感觉Rust是未来全栈程序员必备的语言,这门语言是非常考验你是不是一个合格的程序员,因为他既有很底层的东西,比如内存概念,也有很现代的语法,但前提是你需要都懂,对于那些不是科班出身又没有好好学习计算机相关的东西人来说,这个确实有难度。

Python平时主要是处理数据用,在Mac上使用很方便,最近想把一个程序让下面人用,他们是Window环境,Python他们并不熟悉,所以直接把源文件丢过去,他们又得折腾一番。我就想到打包成exe,其实很早就有这样的想法,但一直没有做过。用的是PyInstaller这个库,还想着这么简单,结果一打包,发现并不是Window下的exe,原来是我理解的错误,以为可以直接打包成Window上执行的exe了,实际上PyInstaller只是在不同平台上将python打包成对应平台的应用了。难道我又得找台Windows,环境弄好,再打包一个?真麻烦,想着可以交叉编译吗?查了下PyInstaller以前好像可以,后来就去掉了。之前用过点Go语言,就是可以交叉编译的,一时间我尽有想用Go重写那个功能的想法,但是一对比还是Python方便。最后找了一个通过docker进行打包的方案docker-pyinstaller

直接这个命令就可以打包了,具体可以参考文档。

docker run -v "$(pwd):/src/" cdrx/pyinstaller-windows

但是有个问题,打包出来的应用一执行,提示一个依赖的库,没有找到。不对啊,文档中明明说把依赖写在requirements.txt文件里,我也写了的。

文档中的这段话,看着没有其他的操作,也没有相关的示例代码。

If the src folder has a requirements.txt file, the packages will be installed into the environment before PyInstaller runs.

折腾了好几个小时,最后找到了一篇日文的文章,看完后豁然开朗,原来是写法的问题,我对docker的一些东西不够熟吧。

docker run --rm -v "$(pwd):/src/" cdrx/pyinstaller-windows -c \
  "pip install -r requirements.txt && \
  pyinstaller main.py --onedir --onefile --clean && \
  mv dist/main.exe main.exe && \
  rm -rf __pycache__/ build/ dist/ main.spec"

最后打包后的exe就没有依赖缺失的问题了。不过打包成exe后,我最终还是改了下python的代码,比如os.path.dirname(__file__)是无法使用了,另外打开文件时提示UnicodeDecodeError: 'gbk' codec can't decode byte的错误,需要加encoding='utf-8'

参考

Docker環境のPyInstallerでキレイにExe化する

最近在看WebAssembly的东西,觉得Rust写基于WebAssembly的Web应用好像是不错的选择。另外Rust这门语言,很早就听说的了,后来也买了《Rust权威指南》这本书,只是简单的翻了翻,并没有做过什么项目。于是就起了个备忘录的简单项目。

Yew

有点类似React,用起来还是比较顺手了。目前版本迭代还是相当的快,我一开始用的0.19.0的版本,最后因为一个库的关系,直接选择了最新的版本,版本之间还时存在兼容问题。我打包后有2M,相比传统的还是比较大了,不过可能是是没有优化的结果。在代码里面写Html,主要的问题是格式化的问题,vscode里面一格式化代码后,html的代码就乱了。

问题

Yew本身提供的文档还可以,有问题看看文档也能解决。主要的问题是wey_sys的一些库的使用,比如我想使用IndexedDB这个东西,其实JS版的文档很好看懂,Rust版的就有点不知所措,主要还是我对Rust了解不够,再加上这些库并没有特别全的文档,所以折腾半天。比如设置IndexedDB回调的问题,参数显示js_sys::Function,我想着那就构造个Function出来,结果翻遍文档没有看到如何构造的例子,最后解决方案是闭包转Function的方式,一开始也是报错,提示闭包已久被销毁了,后来发现官方的例子有点问题,最后修复了。

另外因为要在几个页面间共享一些数据,想着写个单例吧,发现不那么简单,找了个lazy_static这样库,提示*mut u8` cannot be shared between threads safely这样的错误,最终还是停了下来。

总结

Rust的学习曲线比较陡,主要是很多语法不同于传统的C体系,所以感觉原来的经验无法施展,而且沿着经验的思路反而会比较绕,发现还是得按Rust的方式解决问题。

上次去省图偶然看到了《WebAssembly 实战》一书,之前就有关注到,于是就借了回来,刚好趁清明假期看看。之前有写过一篇《初试WebAssembly》,本身Rust语言不熟,虽然我买了本《Rust权威指南》,但一直没有什么项目可以练手,基本上没有进展。《WebAssembly 实战》一书是基于C/C++的,相对来说读起来障碍不大。实际我也跑了一个例子试了下,当然如果真拿C/C++来写Web应用的话,估计会很奔溃了。接着我又看了好几个关于WebAssembly的视频演讲,发现有了新的想法。

推出WebAssembly这个东西主要是因为现有的优化已经无法满足性能的进一步提升了,语言肯定不能大改了,JS这么用的广。记得早些年使用Flash的时候,那会是在嵌入式上跑AS2,性能的问题真的是让我当时无语。后来又推出了AS3性能又大幅的提升,但是很长一段时间,我们公司的引擎没有升级,所以我基本上还是用AS2。虽然现在的Web开发没有像当年那样,毕竟开放的环境还是不一样。回到正题,如果单纯的写WebAssembly的话,好像也没有这个必要,而且据说和JS的调用开销不小,所以到底什么时候用,是个问题。反过来想要是哪天整个的Web可以大部分都用WebAssembly来写的话,那就好了,不过目前还不能直接操作Dom,这是一个很大的问题,理论和JS是相互调用的,我看到Rust有个web_sys的库,可以操作dom,不过我想应该也是自己实现的吧。目前看到几个Rust的例子,是直接用WebAssembly来写整个的Web应用的,比如Yew这个Rust的框架允许你构建基于WebAssembly的应用。另外微软的Blazor,允许使用C#来编写基于WebAssembly的单页应用。还有Go的Vugu。

所以如果和JS混着用,那么实际上是把WebAssembly作为一个计算密集型的一个解决方案吧,实际开发中大部分的Web应用都是些轻客户端,主要还是界面相关的操作了,其实大部分情况下很难想到用WebAssembly了,这也是我最早尝试时的感想。但是如果未来解决了类似调DOM的成本比较高的的问题,那么有没有可能Web语言变成Rust为主,为什么是Rust呢?首先WebAssembly本身没有垃圾回收,内存是手动处理了。Rust呢?标榜的是一个没有垃圾回收的安全语言,与C/C++相比实际上是语言层面上处理内存管理的问题。这么一想Rust与WebAssembly真是绝配,当然其他的语言也可以编译成WebAssembly,但对于垃圾回收的语言,是比要实现一个垃圾回收的运行时环境,这样性能上就会有折扣。

总结

想来想去,最终落到Rust这个语言上了,或者说WebAssembly本身的设计这种在底层改变的东西无疑为未来提供了各种改变的可能性,即使不是Rust也许会是其他的语言。C/C++用户可以让现有的库移植到Web上使用,另外Web应用一定是现在这样的轻客户端的类型吗?WebAssembly的引入确实提供另一种可能。另外一定要用JS来写Web应用吗?过去是没有办法,但是底层的改变让未来可以使用统一基于WebAssembly的语言来写Web成为了可能,而不像过去所有新的语言实际上都得编译成JS。基于WebAssembly的框架,有微软这样的大厂,也有基于Go的,某种程度这是一场大厂利好的事,但坏处是当有各种的语言可以写Web时,会不会有点分裂,毕竟只学一个JS,大家共用资源,而用不同的语言有学习成本。

本站14年建立的,那会Octopress确实流行,好多的技术大拿都用,可惜当时只是建了个站,就没有写东西,后来想写东西了,发现大家都不用Octopress了,有人说慢,其实我的电脑上还行。一开始我也没有想着换,Octopress的问题是作者已经不更新了,所以就有一堆的升级的问题,总之对于程序员而言也无所谓,改改还是可以用的,就这样一直坚持到写这篇文章的晚上。

我比较在意代码高亮,上次就把Octopress的代码高亮给改了,不过不完美。其实博客很大程度是我的一些技术的记录了,想想这个,就一时兴起尝试的迁移吧。虽然静态博客框架很多,不过自己最近3年一直有写JS,另外Hexo确实很多人用。这次的迁移可以说相当的顺利。Hexo的文档非常不错,基本上照着来网站就好了。原来想着在Github再建一个项目,后来发现不需要,直接把我旧的网站一覆盖就可以了。

使用的主题是NexT,我比较喜欢简洁的,这个主题的文档也非常不错,很快就搞定了,看着焕然一新的博客,Octopress已成过去时了。

0%