曹寅和断指觉禅师
去年的时候我曾到河北大名县的兴化寺探访,在挖掘兴化寺历史时,了解到了关于清初重建该寺的断指觉禅师的故事。在成书于康熙年间的《济宗世谱》一书里,看到断指觉禅师的弟子里,竟然有“曹寅子清居士”,这首先让我想到了《红楼梦》作者曹雪芹的祖父曹寅,“曹寅子清”是不是就是曹寅呢?
去年的时候我曾到河北大名县的兴化寺探访,在挖掘兴化寺历史时,了解到了关于清初重建该寺的断指觉禅师的故事。在成书于康熙年间的《济宗世谱》一书里,看到断指觉禅师的弟子里,竟然有“曹寅子清居士”,这首先让我想到了《红楼梦》作者曹雪芹的祖父曹寅,“曹寅子清”是不是就是曹寅呢?
为什么要购买一台Linux手机呢?安卓和iOS我都开发过,iOS稍微久一些。最早想到Linux手机是源于安卓和iOS这些平台都会限制开发语言,比如你想用Python开发个界面。很早的时候有在安卓手机上安装过Ubuntu,但那个界面完全是PC的桌面,体验比较差。最近直接触发我购买PinePhone Pro的理由是我想用GTK的Rust绑定来开发应用。
淘宝上可以购买,但是价格有点偏贵,需要4500左右。直接购买,官网目前不支持直接发内地,这家公司是从香港发货的,所以可以找家转运公司,发货到香港,再转运到内地。费用除了399美金的机子,还有官网发出的运费,因为是到香港,这个大概十几美金,再加上转运费近200,最后花费3000多些,比在淘宝购买要便宜不少。时间上,大概一周多点时间,就可以收到货。
手机的包装跟之前网上看的开箱视频一样,手机、贴膜、说明书(没有中文)、充电线(红色)。开机时需要将后盖打开,把电池接触膜给撕掉,这样就可以开机了。手机的外放音质感觉很干涩,确实不能iPhone之类的相比。因为没有多余手机卡,所以移动网络并没有测试。
这台手机最大的特点就是刷机友好,我一开始并没有打算刷机,应该是对Arch Linux不熟悉的缘故,所以预装系统下软件一直无法更新也无法安装新软件,比如我喜欢的Rust,甚至我都不知道如何进行远程连接,实际上现在认为都是不熟悉导致的。
安装新系统主要参考官网的说明Installation instructions,我选择使用内置卡安装系统,因为我的是Mac OS,所以在刷系统到eMMC有点不一样,主要参考Install Image Pinephone。系统刷到卡的速度有点慢,按照文档设置1M/s,系统大概6G多,结果刷了1个多小时才结束。文档说如果使用外置卡的话,可以用另一种技术,那个能快很多,但是手头没有多余的卡,加上内置卡128G的容量,觉得暂时就这样。
预装的Manjaro系统,桌面使用Plasma Mobile,可能是稍早些的系统版本,系统UI显示看起来不是很精细的感觉。Manjaro是基于Arch LInux构建的,使用Pacman来进行软件管理。因为平时只使用过CentOS和Ubuntu,所以一开始操作不是很顺利。系统自带的相机功能无法使用,系统支持横竖屏,但是也有小Bug,比如竖屏时有些应用会显示成横屏的,这时只需要把手机横过来,再竖屏就好了。
首先给我感觉界面很漂亮,不像Plasma Mobile这个界面会感觉有毛刺一般。但是我不太喜欢Phosh切入后台的操作,我更喜欢Plasma Mobile的方式。在虚拟键盘方面,虽然Phosh的更漂亮,但是Control等相关键设计过于小,使用时候很容易误点到下方的按键。刷机后,发现相机可用了,但是成像有问题。另外软件升级后发现整个系统的软件启动变慢了,暂未找到原因。系统预装的是火狐浏览器,但是网页似乎无法分辨屏幕大小,所以还是PC版的布局在显示。这点Plasma Mobile上的Angelfish浏览器更好一些。
Manjaro 虽然也是基于Arch Linux,但是Arch Linux还是跟Manjaro不太一样些,比入没有pacman-mirrors
这个命令。
Manjaro Phosh版使用了一段一点时间,发现自带的Firefox在播放音乐的时候很卡,尝试安装chromium试下,但是启动不了,网上有人说Arch Linux版可以,于是我安装了,但是依旧不行,不知道是不是跟我用Phosh有关系没有。更新了Firefox发现老报配置错误,无法使用。即使在没有更新之前,虽然可以使用,但貌似没有安装中文字体的缘故,中文显示不了。我尝试整体更新软件,虽然也设置了国内的源,但是依旧很慢。选择单独升级Phosh,结果重启后就是死循环了。为了不把电池耗尽,只能选择刷机了。
之前在网上转门调研Best distro for Pinephone Pro?,里面Arch Linux不少人推荐。但目前来看,虽然感觉App启动比Manjaro Phosh版的快一些,但是不够完整,使用性太差了。
没有图形界面?一直处于命令行的登录状态。
Debian的移动版,整体感觉比较稳定,应用启动也比之前的快。
这个是原来的Open webOS,安装后可以启动,但是很快就会进入死循环,界面确实延续了WebOS的风格,但是也发现相机之类的界面出现界面乱的问题,不过因为很快就死机了,所以基本也没有来得及体验。手机电池耗尽对于初次刷机的一定要注意,会让你误以为手机成可砖块。建议先不要刷这个系统,熟悉了再考虑。
这个是基于Alpine Linux的,PS:有点头大,刷一次机学习一种新的发行版操纵。
我安装的是这个,不过感觉很卡,应该不是使用了Gnome的缘故,因为远程连接也很卡,说明还是系统适配的问题。
这个系统是目前最可用的系统,可能是早期是由Ubuntu主导的,而且还发过手机,后来他们放弃了,现在由社区在驱动。首先这个系统支持中文输入,另外整个交互也非常的流畅。
这个系统是基于Sailfish OS的,但是无法正常启动一直处于光标闪烁中。
目前发现虚拟键盘不支持中文输入,键盘源上有一个Chinese选项,但打出来的依旧是英文。
电池管理这块似乎确实存在问题,耗电确实比一般的手机要快,放了一晚上就没有电了,所以后台可能有一些程序在运行,而对于iOS系统后台有严格管理的,所以我一般关机。
一次系统升级后,我都关机了,第二天再启动时发现无法启动,充电也不行,然后我就准备刷机,试了几次都不行。以为成砖头了,网上看了半天,最后才发现原来是电池耗尽问题(The battery is fully drained),一般手机电池耗尽了,重新充电就可以了,但是Pinephone Pro的好像有问题,稍微充电一开机,系统死循环,电量耗尽,然后就一直充不进去电,最后参考官方的文档终于启动了。
上面关于关机后都没有电的问题,后来发现是手机在充电时我关机的,拔掉USB后系统莫名的自动开机,而当时因为升级后出现了问题,直接无法开机,我也没有注意到,所以才有了关机后会耗电的认知。
signature from "Arch Linux ARM Build System builder@archlinuxarm.org" is unknown trust
安装软件时老是提示证书的问题,无法升级以及安装软件,最后参考了这篇文章,解决方式如下:
sudo rm -r /etc/pacman.d/gnupg
sudo pacman-key --init
sudo pacman-key --populate
sudo pacman-key --refresh-keys
sudo pacman -Sc
我安装了VLC播放器,界面完全是桌面的,显示分辨率看起来很低。但是可以使用,只不过界面显示不全了。这点看起来也有好处,那就这些应用可能只需要修改界面就可以了。自从升级这个后,第二天手机就启动不了,软件升级也是有风险。
这个台手机显然不是一台备用机,很多问题对于没有经验的新手来说是致命的,比如电池耗尽问题,让我差点以为手机成砖头了。Linux看似稳定,但是也是潜藏危机,每次升级很可能让系统无法正常运行,而我在如何恢复这块经验不足,每次都是重新安装,如果在生产环境这可能是灾难性的,意味着资料的丢失。
主要参考官方的这篇文章cross-compiling,如果是没有动态库的简单Rust程序,基本没有问题。这里我主要使用GTK4的Rust绑定库gtk4-rs,来开发Pinephone Pro的应用程序,这有GTK相关的动态库依赖,所以导致问题比较麻烦。这里先说明一下我对GTK开发完全不熟,另外也对交叉编译不是很懂。
主要是在Linux发行版Manjaro系统上编译的,理论上Ubuntu这些也应该可以的。交叉编译GTK的最大问题,主要是GTK相关动态库的问题。这里面需要了解一些编译链接以及动态库方面的知识,笔者一开始以为自己了解,结果被狠狠的上了一课。建议读一下《高级C/C++编译技术》这本书,补充相关的知识。
Pinephone Pro运行的程序属于ARM64的程序,交叉编译时需要安装aarch64-linux-gnu
。
sudo pacman -S aarch64-linux-gnu
安装完成后,会在系统usr目录下生成aarch64-linux-gnu
的目录,在编译时遇到找不到库时,我们就需要将找到的库放置在/usr/aarch64-linux-gnu/lib/
下。这里可以使用ld的命令验证是否找到了链接库。
sudo /aarch64-linux-gnu/bin/ld -lgtk-4 --verbose
添加--verbose
参数后可以打印出详细的查找路径,便于Debug,另外这个加上sudo
,是因为这边不加的话,即使有库也无法找到,但是加上后就可以。
重点提示下缺失库,一定要找对应平台的库,比如这里我主要找aarch64的,一开始我使用archlinuxarm,查找我需要的库,但发现有些库也没有的,一种是自己编译对应版本的库。另一种就是从已安装的手机系统中找对应的库,因为我之前在PinePhone Pro上有编译成功过同样的程序,所以系统上自然也有了这些依赖库。可以使用ldd查看程序的依赖库。
ldd gtk-rs
linux-vdso.so.1 (0x0000ffff806d8000)
libgobject-2.0.so.0 => /usr/lib/libgobject-2.0.so.0 (0x0000ffff80560000)
libgtk-4.so.1 => /usr/lib/libgtk-4.so.1 (0x0000ffff7fc00000)
libpango-1.0.so.0 => /usr/lib/libpango-1.0.so.0 (0x0000ffff7fb70000)
libgdk_pixbuf-2.0.so.0 => /usr/lib/libgdk_pixbuf-2.0.so.0 (0x0000ffff7fb10000)
libcairo-gobject.so.2 => /usr/lib/libcairo-gobject.so.2 (0x0000ffff7faf0000)
libcairo.so.2 => /usr/lib/libcairo.so.2 (0x0000ffff7f9a0000)
libgraphene-1.0.so.0 => /usr/lib/libgraphene-1.0.so.0 (0x0000ffff7f970000)
libgio-2.0.so.0 => /usr/lib/libgio-2.0.so.0 (0x0000ffff7f710000)
libglib-2.0.so.0 => /usr/lib/libglib-2.0.so.0 (0x0000ffff7f590000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x0000ffff7f560000)
libc.so.6 => /usr/lib/libc.so.6 (0x0000ffff7f3a0000)
libffi.so.8 => /usr/lib/libffi.so.8 (0x0000ffff7f380000)
/lib/ld-linux-aarch64.so.1 => /usr/lib/ld-linux-aarch64.so.1 (0x0000ffff8069f000)
libgmodule-2.0.so.0 => /usr/lib/libgmodule-2.0.so.0 (0x0000ffff7f360000)
libpangocairo-1.0.so.0 => /usr/lib/libpangocairo-1.0.so.0 (0x0000ffff7f340000)
libharfbuzz.so.0 => /usr/lib/libharfbuzz.so.0 (0x0000ffff7f210000)
libfribidi.so.0 => /usr/lib/libfribidi.so.0 (0x0000ffff7f1e0000)
libfontconfig.so.1 => /usr/lib/libfontconfig.so.1 (0x0000ffff7f180000)
...
这里会显示详细的库以及库的位置,然后我们就可以使用scp工具把库从手机端拷贝到电脑上。记得换成你自己的IP,比如:
scp manjaro@192.168.1.4:/usr/lib/libicudata.so.72 ./
解决了库的来源问题,还需要解决库的依赖问题,GTK库的依赖相当多,比如你找到了lgtk-4
,结果接着给报另外一堆库找不到。这是因为lgtk-4
本身又依赖了很多的动态库,这时我们可以使用readelf这个工具查看。
sudo readelf -d libgtk-4.so
标记 类型 名称/值
0x0000000000000001 (NEEDED) 共享库:[libgmodule-2.0.so.0]
0x0000000000000001 (NEEDED) 共享库:[libglib-2.0.so.0]
0x0000000000000001 (NEEDED) 共享库:[libgobject-2.0.so.0]
0x0000000000000001 (NEEDED) 共享库:[libgio-2.0.so.0]
0x0000000000000001 (NEEDED) 共享库:[libpangocairo-1.0.so.0]
0x0000000000000001 (NEEDED) 共享库:[libpango-1.0.so.0]
0x0000000000000001 (NEEDED) 共享库:[libharfbuzz.so.0]
0x0000000000000001 (NEEDED) 共享库:[libcairo.so.2]
0x0000000000000001 (NEEDED) 共享库:[libfribidi.so.0]
0x0000000000000001 (NEEDED) 共享库:[libcairo-gobject.so.2]
0x0000000000000001 (NEEDED) 共享库:[libfontconfig.so.1]
0x0000000000000001 (NEEDED) 共享库:[libgdk_pixbuf-2.0.so.0]
0x0000000000000001 (NEEDED) 共享库:[libepoxy.so.0]
0x0000000000000001 (NEEDED) 共享库:[libm.so.6]
0x0000000000000001 (NEEDED) 共享库:[libgraphene-1.0.so.0]
0x0000000000000001 (NEEDED) 共享库:[libXi.so.6]
0x0000000000000001 (NEEDED) 共享库:[libX11.so.6]
0x0000000000000001 (NEEDED) 共享库:[libpangoft2-1.0.so.0]
..
然后我们就从手机系统寻找对应的库,我的程序要主要依赖的是GTK,所以解决了库的问题后,程序就编译通过了,将编译好的成传输到手机后也可以顺利运行。
目前看来交叉编译Pinephone Pro的程序的主要问题是动态库的问题,因为GTK库的庞大,导致问题也变得有点复杂。一开始我在Mac上编译,但因为没有经验的缘故,一时间把这个单一的动态库缺失的问题想的过于复杂了。甚至更换到Ubuntu上编译,但是也失败了,因为并没有认识到问题的根本,完全是瞎猫碰上死老鼠的心态。后来又换成在Manjaro上编译,这里说句题外话,Manjaro的桌面版也是头一次用,但是整个系统对于只是开发发需求的我而言比Ubuntu这种重型的要轻便很多,另外这个系统真的是开发者友好的系统,命令行是非常的人性化。因为库很多,在一次复制过程中直接把系统的依赖库给覆盖掉了,导致系统无法启动,只能重新安装Manjaro系统。
依赖库太多,直接把折腾到临晨3点才搞定。现在看来应该整理使用脚本把所有的库依赖给处理下,比手动的要快吧。想到了之前开发iOS其实也是交叉编译,只不过苹果提供的工具完全让我们忽略了这些复杂性,另外当时记得很少使用动态库,一般是以静态库的形式。
距离上一篇写一个Yew版的UI库,已经一个多月了。一直在写Yew,有些认知已经变化了,总结一下。
结构式组件(Struct components),我更喜欢叫它类组件,以下是一个计数器的例子。
use yew::prelude::*;
pub enum Msg {
Add,
Sub
}
pub struct CounterPage {
counter: i32
}
impl Component for CounterPage {
type Message = Msg;
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
Self {
counter:0
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::Add=>{
self.counter += 1;
true
},
Msg::Sub=>{
self.counter -= 1;
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<div>
<h1>{ "Counter测试" }</h1>
<div>{"counter: "}{self.counter}</div>
<div>
<button onclick={ctx.link().callback(move|_|Msg::Add)}>{ "增加" }</button>
<button onclick={ctx.link().callback(move|_|Msg::Sub)}>{ "减少" }</button>
</div>
</div>
}
}
}
这种形式的重点是生命周期,上面的例子只使用了最常用的create
、update
、view
这三个方法。下面是所有的生命周期的方法:
pub trait BaseComponent: Sized + 'static {
/// The Component's Message.
type Message: 'static;
/// The Component's Properties.
type Properties: Properties;
/// Creates a component.
fn create(ctx: &Context<Self>) -> Self;
/// Updates component's internal state.
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool;
/// React to changes of component properties.
fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool;
/// Returns a component layout to be rendered.
fn view(&self, ctx: &Context<Self>) -> HtmlResult;
/// Notified after a layout is rendered.
fn rendered(&mut self, ctx: &Context<Self>, first_render: bool);
/// Notified before a component is destroyed.
fn destroy(&mut self, ctx: &Context<Self>);
/// Prepares the server-side state.
fn prepare_state(&self) -> Option<String>;
}
不过Yew的类组件在状态更新方面跟React不一样。比如React:
// 初始化状态
this.state = {
counter: 0
}
// 设置状态
this.setSatate({
counter: 1
})
Yew主要依赖于消息,每次要更新状态先发送消息,然后在update
方法里面处理消息并决定视图是否需要重新渲染。
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::Add=>{
self.counter += 1;
true
},
Msg::Sub=>{
self.counter -= 1;
true
}
}
}
这种模式实际上是受到了Elm这个语言的影响。这个是Elm官网上的Button例子。
module Main exposing (..)
-- Press buttons to increment and decrement a counter.
--
-- Read how it works:
-- https://guide.elm-lang.org/architecture/buttons.html
--
import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
-- MAIN
main =
Browser.sandbox { init = init, update = update, view = view }
-- MODEL
type alias Model = Int
init : Model
init =
0
-- UPDATE
type Msg
= Increment
| Decrement
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
model + 1
Decrement ->
model - 1
-- VIEW
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (String.fromInt model) ]
, button [ onClick Increment ] [ text "+" ]
]
这个受到React的函数式组件的启发,上面计数器的例子可以这样写:
#[function_component]
pub fn CounterPage() -> Html {
let counter = use_state(|| 0);
let add_callback = {
let counter_clone = counter.clone();
Callback::from(move |_| {
counter_clone.set(*counter_clone+1);
})
};
let sub_callback = {
let counter_clone = counter.clone();
Callback::from(move |_| {
counter_clone.set(*counter_clone-1);
})
};
html! {
<div>
<h1>{ "Counter测试" }</h1>
<div>{"counter: "}{*counter}</div>
<div>
<button onclick={add_callback}>{ "增加" }</button>
<button onclick={sub_callback}>{ "减少" }</button>
</div>
</div>
}
}
从形式上这个会比类组件更加的简洁。Yew函数式组件是一种完整构建组件的方法,主要是依赖Hooks,比如上面的use_state
,还有很多比如use_node_ref
、use_effect_with_deps
。如果你用过React的函数式组件,那么只要适当的参考下Yew的相关文档,很容易上手。不熟悉函数式组件的,建议阅读最新的React函数式文档,或者读一本书,比如我读的是《React学习手册第二版》。
Yew的官方文档这样描述:
You are currently reading about function components - the recommended way to write components when starting with Yew and when writing simple presentation logic.
There is a more advanced, but less accessible, way to write components - Struct components. They allow very detailed control, though you will not need that level of detail most of the time.
这给人感觉类组件更强大,我一开始使用类组件,但是后来因为用到了上下文消息的监听的问题,见这个例子contexts,里面的发布消息,是用函数式组件写的,当时没有反应过来如何用类组件写发布消息,后来发现类组件也很简单,这是后话。后面我就有意开始使用函数式组件,发现用函数式组件似乎实现起来也问题不大,所以后面的组件又都是函数式组件写的。
函数式组件一开始我觉得,也有跟类组件类似的生命周期的东西,虽然在组织逻辑复杂的代码方面似乎没有类组件方便,后来我慢慢的使用类似过程式的方式,把一些相似的逻辑拆分到方法里面。如下:
#[function_component]
pub fn YELTag(props: &YELTagProps) -> Html {
let on_click = {
let on_click = props.on_click.clone();
Callback::from(move |e| {
on_click.emit(e);
})
};
let on_click_clone = on_click.clone();
html! {
<span
class={get_span_classes(props)}
style={if props.color.is_empty() {"".to_string()} else {format!("background-color: {}", props.color)}}
onclick={on_click}
>
{props.children.clone()}
if props.closable {
<i class="el-tag__close el-icon-close" onclick={ on_click_clone }></i>
}
</span>
}
}
fn get_span_classes(props: &YELTagProps) -> Vec<String> {
let mut span_classes = vec!["el-tag".to_string()];
if let Some(t) = props.tag_type.clone() {
span_classes.push(format!("el-tag--{}", t));
}
if let Some(e) = props.effect.clone() {
span_classes.push(format!("el-tag--{}", e));
}
if let Some(s) = props.size.clone() {
span_classes.push(format!("el-tag--{}", s));
}
if props.hit {
span_classes.push("is-hit".to_string());
}
span_classes
}
把class
单独通过fn get_span_classes(props: &YELTagProps) -> Vec<String>
来处理,这样就可以避免函数组件内代码的膨胀。
但就在刚发布这篇文章后的第二天,我遇到了如下的问题。
#[function_component]
pub fn YELInputNumber(props: &YELInputNumberProps) -> Html {
let on_decrease = {
let value = props.value.clone();
Callback::from(move |e| {
let v = decrease(props, value);
// log!("v:", v);
})
};
let on_increase = { Callback::from(|e| {}) };
html! {
<div class={get_div_classes(props)}>
if props.controls {
<span
class="el-input-number__decrease"
role="button"
onclick={on_decrease}
>
<i class={format!("el-icon-{}", {
if get_controls_at_right(props) {
"arrow-down"
} else {
"minus"
}
})}></i>
</span>
<span
class="el-input-number__increase"
role="button"
onclick={on_increase}
>
<i class={format!("el-icon-{}", {
if get_controls_at_right(props) {
"arrow-down"
} else {
"plus"
}
})}></i>
</span>
}
<YELInput value={get_display_value(props)}/>
</div>
}
}
fn decrease(props: &YELInputNumberProps, val: f64) -> f64 {
let precision_factor = js_sys::Math::pow(10.0, get_num_precision(props));
let num = precision_factor * val - precision_factor * props.step as f64;
return to_precision(props, num, None);
}
fn to_precision(props: &YELInputNumberProps, num: f64, precision_option: Option<i32>) -> f64 {
let precision = if let Some(p) = precision_option {
p
} else {
get_num_precision(props) as i32
};
let a = js_sys::Math::round(num * js_sys::Math::pow(10.0, precision as f64));
let b = js_sys::Math::pow(10.0, precision as f64);
return a / b;
}
fn get_precision(val: f64) -> i32 {
let value_string = val.to_string();
let dot_position = value_string.find('.');
if let Some(p) = dot_position {
return (value_string.len() as i32) - (p as i32) - 1;
}
0
}
这段代码会报错,因为我在闭包中使用了props这个引用,这个引用不满足'Static
的这个要求。
| pub fn YELInputNumber(props: &YELInputNumberProps) -> Html {
| ----- - let's call the lifetime of this reference `'1`
| |
| `props` is a reference that is only valid in the function body
...
41 | / Callback::from(move |e| {
42 | | let v = decrease(props, value);
43 | | // log!("v:", v);
44 | | })
| | ^
| | |
| |__________`props` escapes the function body here
| argument requires that `'1` must outlive `'static`
出错的代码:
#[function_component]
pub fn YELInputNumber(props: &YELInputNumberProps) -> Html {
let on_decrease = {
let value = props.value.clone();
Callback::from(move |e| {
let v = decrease(props, value);
// log!("v:", v);
})
};
这之前使用闭包都是复制了props的值,没想道这次因为使用了props引用本身却出错了。因为decrease这个方法会调用其他的方法,也需要props中的参数。这个错误似乎无法解决,props这个参数我无法修改成'Static
的,因为这个是Yew自己提供的。一种就是我彻底把所有后续用到的参数复制出来,通过decrease传给下面的调用,即使decrease本身不需要这些参数,但这样一来反而增加了复杂性。但如果改成类组件的话,就不存在上面的问题,每个类组件可以包含props属性,在实例方法中可以调用。
通过以上这个问题,打破了我原来觉得函数式组件是一种完备方法的结论,即使我努力去尝试适应,但函数式组件在复杂逻辑代码中有它的缺陷,这终究是Rust本身的决定,也是框架的问题。我相信JS版本没有这样的问题,所以React中函数式组件的可以大行其道有它的道理,但是在Yew中却受到语言特性的限制。
类组件和函数式都可以使用,两者是不同思想,见上分析。虽然Yew的函数式组件看起来跟React的函数式组件很像,但还是有限制的,所以如果遇到问题,那么回到类组件的方式,至少可以解决问题。但为什么不直接使用类组件就得了,所以Yew团队也不知道怎么想的,创造了两种方式,但又不是很完美,反而增加了困扰,或许我根本不需要了解React。
我就是Vue的用户,更准确的是应该Vue 2用户,Vue 3还没有来得及学习。Element UI 也是基于Vue2的组件库。一开始我觉得这只是JS和Rust的不同,很多时候我都是用Vue的思维在学习使用Yew这个框架,但渐渐发现Yew与Vue在设计思想上有很大不同,本质上这实际上是React与Vue的不同,尤其是React的函数式组件思想。
在写Table这个组件时,JS版的Table中使用很多动态获取的东西,这种方式对于Rust而言就无法做到,首先是数组,一般都是固定类型,所以这里需要使用范型,范型问题不大。动态性,我想到了反射这样的东西,官方提供反射功能很有限,是一种编译时的反射。后来也找到了一些第三方的库,不过最后发现还是要结合范型,并不能动态的判断类型,进行值的转换。
另外比如Element UI组件的属性有些同时支持Number、String、Function,我一开始想到用范型,但是在实际使用中还需要动态的判断类型,这个Rust无法做到,目前这类属性的实现还没有好的方法。
使用Rust好处就是类型确定性,比如还是组件的属性,JS版使用的是字符串,这个实际上就有个验证的问题。但是如果用Rust写的话,可以直接使用枚举类型。
用Rust写前端,大部分问题实际上就跟你用C#或者Java写前端一样,只不过Rust对比后者而言少了更多的动态性,另外就是它的所有权法则,早期的时候,非常不习惯,顿不顿就被编译器暂停了。Rust项目编译慢,这个确实是个问题,我的电脑都是32G的8核的配置。
题外话:从前端的角度,TypeScript确实弥补了JavaScript在类型上的问题,这也是很多大项目转向了TypeScript。
去年的时候我就尝试用Yew写了一个记事本的应用,当时可以说是刚接触Yew,Rust也没写什么代码,好在那个应用在UI方面也比较简单。一年过去了,最近又开始折腾Yew这个框架,一开始只是想把网络请求以及图片显示等测试一下,后来觉得是不是可以写一个UI库试下,虽然也有几个在Yew推荐的UI库,但感觉审美不在一个线上。之前写系统后台管理端时,使用的是vue-element-admin这个框架,想着也写一个Yew版的吧,不过首先得把Element UI库给写出来,实际上Yew能不能大范围的使用还是跟类似Element UI这样的企业级库有关系。另一方面对于Yew,我也想通过写这个UI库,来进一步学习Yew这个框架,同时也可以练习Rust的使用。
这些组件的实现都是参考了Element UI的源码,样式部分基本原封不动的使用了Element UI的样式,Element UI是基于Vue的,现在等于把Vue换成了Yew,Javascript换成了Rust,实现逻辑上依旧使用Element UI的,但在具体实现上则是根据Yew框架和Rust的特点有所不同,尽可能在功能上保持跟Element UI一致。
一开始我选择了Button这个最常用的组件,这个组件相对来说比较简单。
use yew::prelude::*;
pub enum Msg {}
pub struct YewButton {
props:YewButtonProps
}
#[derive(Clone, PartialEq, Properties)]
pub struct YewButtonProps {
#[prop_or_default]
pub disabled: bool,
#[prop_or_default]
pub style: String,
pub title: AttrValue,
pub on_clicked: Callback<MouseEvent>,
#[prop_or_default]
pub loading:bool,
#[prop_or_default]
pub plain:bool,
// medium / small / mini
#[prop_or_default]
pub size:String
}
impl Component for YewButton {
type Message = Msg;
type Properties = YewButtonProps;
fn create(ctx: &Context<Self>) -> Self {
Self {
props:ctx.props().clone()
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let title = ctx.props().title.clone();
let disabled = ctx.props().disabled.clone();
let style = ctx.props().style.clone();
let loading = ctx.props().loading.clone();
let onclick = ctx.props().on_clicked.reform(move |event: MouseEvent| {
event.stop_propagation();
event.clone()
});
let mut classes = Vec::new();
classes.push(String::from("el-button"));
if !style.is_empty() {
let ss = format!("el-button--{}", style);
classes.push(ss);
}
if disabled {
classes.push(String::from("is-disabled"));
}
if self.props.plain {
classes.push(String::from("is-plain"));
}
// TODO 需要对字符串进行检查
if !self.props.size.is_empty() {
classes.push(format!("el-button--{}", self.props.size));
}
html! {
<button class={classes!(classes.clone())} {onclick} disabled={disabled.clone()} >
{title.clone()}
if loading {
<i class="el-icon-loading"></i>
}
</button>
}
}
}
这个里面代码目前没有优化,基本上时当时写的样子,应该是没有完全实现所有Element UI版的按钮功能,但核心功能是可以用的。
评分组件在以前iOS的时候也是自己写过,相对按钮组件,这个就相对复杂些了。主要问题是实现半星效果的功能时,发现Yew的鼠标事件不能穿透,一开始按照原版的实现来写,结果老是出错,后来才发现是这个不能穿透的问题。
if self.is_rate_disabled() {
return false;
}
if self.props.allow_half {
let element: Element = e.target_unchecked_into();
let mut target = None;
// 这段代码原本是Element UI的实现,但是Yew的鼠标移动事件并不能穿透,所以这段代码弃用
// if element.class_list().contains("el-rate__item") {
// target = element.query_selector(".el-rate__icon").unwrap();
// }
// if target.is_some()&& target.clone().unwrap().class_list().contains("el-rate__decimal") {
// target = target.clone().unwrap().parent_element();
// } else if element.class_list().contains("el-rate__decimal") {
// target = element.parent_element();
// }
if target.is_none() {
target = Some(element);
}
if target.is_some() {
let offset_x = e.offset_x()*2;
let client_width = target.clone().unwrap().client_width();
self.pointer_at_left_half = offset_x <= client_width;
self.current_value = if self.pointer_at_left_half {
(index+1) as f64 - 0.5
} else {
(index+1) as f64
};
}
} else {
self.current_value = (index+1) as f64;
}
self.hover_index = index+1;
true
原版的实现专门对穿透做了处理,既然不支持穿透实现反而简单了,这个应该是Yew自己的设置了。
完成了评分组件后,想到找个再复杂一点的,于是选中了颜色选择器这个组件,一看这个组件是由好几子组件组成的,就是他了。颜色是个比较专业的东西,原版专门有个Color模块来处理,一开始我打算用Rust重写Color模块,不过鉴于Javascript的动态类型,一开始写一个Hsv转RGB的时候就卡住了,于是就找了一些第三方的库,先把功能拼凑出来,这也是颜色选择器中没有使用原版的实现的一部分。另一个遇到的问题是一些参数面板原版是支持滑动来操作的,但是我试了一下还是有问题。于是只支持了点击设置值的操作。
另一个遇到的大问题是原版支持输入颜色的操作,但是我的输入组件还没有写好,看了写源码,这个组件还需要单独去写。于是目前的功能不支持输入。
颜色处理,最后使用了csscolorparser这个库,把原来零散的库给替换了。
最后这个组件功能我觉得能用,支持十六进制颜色以及RGB和RGBA的形式,实际上csscolorparser这个库支持的很全。目前存在的问题,原版使用了Vue特有的transition标签,我一开始以为这是HTML的,后来才发现。原版弹框时会有动画,而且也会把弹出框定位到颜色按钮的下方适当位置。目前这本版本暂时没有这样的效果。
Yew这个框架,我也尝试的看了下源码,这非常有帮助,因为这是文档有时无法学习到的。基本的运行原理以及生命周期之类的都有了一定的认识。另外通过这三个组件的练习,跟Vue的实现一对比,发现Vue还是包装的更深,更对开发者友好。Yew基本的功能实现是没有问题的,只是有些类似上面提到的transition这样的,可能就需要自己实现。
Rust方面,一方面我看了一些源码,这些源码使用了更深层次的Rust功能,比如宏、范型。另一方面,Rust写前端,毕竟运行环境不是操作系统,所以有些库也是特定平台的。比如数学模块,我一开始想着使用Rust自带之类的标准库,但最后还是使用js-sys的Math模块,实际证明这个很好用。
下一步,目前只写了三组件,总共有90个组件,完全是冰山一角,工作量之巨大,只能慢慢来。目前来看用Rust写前端,只是一种实验性的东西,毕竟Javascript的应用面很广,小程序,移动端,Rust更广阔的应该还是在系统应用这个层级,但面对C/C++这类系统级别的语言也是挑战,另外服务端又有各种语言占据。Rust的使用场景看似很广,但却不得不名对先占市场的问题,所以这也是我为什么要用Rust来写前端这个初衷,就是想用Rust。
最后有对Yew这个框架感兴趣的,可以关注下yew-lab这个项目,组件相关的代码都在里面。
王爽的《汇编语言》第四版,如果没有写过汇编,最好从头开始读。我一开始就以为自己有高级语言的经历,选择性的看书,结果发现还是没有理解汇编语言的核心。
我的电脑是Mac,用虚拟机安装了windows xp
系统,使用MASM 5.0
,下载的安装包里还有LINK
程序。IDE使用RadAsm,这个编辑器的高亮功能不错,不过不支持自动补全。
assume cs:codesg,ss:stack
stack segment
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
stack ends
codesg segment
start:
mov ax, stack
mov ss, ax
mov sp, 20h
mov cx,10h
mov bx,0
s:
add bx, 1
push bx
loop s
mov ax, 4c00h
int 21h
codesg ends
end start
在Debug
下我发现运行最后会报错,发现栈中多了一些数据,而且是我一旦设置栈段后,就会出现这些数据。我把书中的例子也试了下,发现也有这个问题,不过书中栈设置的比较大,所以运行不会出问题,而我这个刚好设置的就是我需要的数据大小。后来在汇编语言论坛,找到相关问题,意思是Debug
下会在栈中存储一些数据的缘故,所以栈的大小不能刚适合,这会有问题的。
最早是读了《Unix编程环境》,在书中有展示如何用Lex和Yacc制作一门编程语言,后来就买了《Lex与Yacc》,Flex、Bison分别是Lex、Yacc的现代版本,于是又买了《Flex与Bison》,《Flex与Bison》实际上是《Lex与Yacc》的续作,都是同样的作者。书虽然简单读了下,但里面的例子一直没有尝试的运行下。
电脑:macOS Monterey,12.5
Flex版本2.6.4,Bison版本3.8.2。Mac下直接使用brew安装,安装完成后记得根据提示把环境配置下。
brew install flex
brew install bison
GCC版本,我使用gcc -v
命令后,实际出现的是clang的版本。
gcc -v
Apple clang version 14.0.0 (clang-1400.0.29.102)
Target: x86_64-apple-darwin21.6.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
书中第一个例子类似unix下wc的例子。
%{
int chars = 0;
int words = 0;
int lines = 0;
%}
%%
[a-zA-Z]+ { words++; chars += strlen(yytext);}
\n { chars++; lines++; }
. { chars++; }
%%
int main(int argc, char** aggv) {
yylex();
printf("%8d%8d%8d\n", lines, words, chars);
return 0;
}
书中的例子main函数都没有写返回值,编译的时候回报warn了,我都加了返回参数。
flex fb1-1.l # 上面的例子保存为l后缀的文件
gcc lex.yy.c -ll # 这一步会升a.out文件
./a.out # 执行文件,输入一些字符,以control+d结束后,便会输出结果
这里需要注意的是原书中gcc lex.yy.cc -lfl
,结果一直报错。后来找到Mac应该使用-ll
替代-lfl
,参考Reddit上的这个问答:Does anyone know how to resolve this error when compiling Flex and Bison?
第二例子是一个计算器例子,结合了Flex和Bison
fb1-5.l
文件代码
%{
# include "fb1-5.tab.h"
%}
%%
"+" { return ADD; }
"-" { return SUB; }
"*" { return MUL; }
"/" { return DIV; }
"|" { return ABS; }
"(" { return OP; }
")" { return CP; }
[0-9]+ { yylval = atoll(yytext); return NUMBER; }
\n { return EOL; }
"//".*
[ \t] { /* ignore white space */ }
%%
fb1-5.y
文件代码,需要注意的是书中的例子类似factor default $$ = $1
,实际运行报错修改为factor { $$ = $1; }
格式。
/**/
%{
#include <stdio.h>
void yyerror(); // 新加
int main(); // 新加
int yylex(); // 新加
%}
%token NUMBER
%token ADD SUB MUL DIV ABS
%token OP CP
%token EOL
%%
calclist: /* 空规则 */
| calclist exp EOL { printf("= %d\n", $2); }
;
exp: factor { $$ = $1; }
| exp ADD factor { $$ = $1 + $3; }
| exp SUB factor { $$ = $1 - $3; }
;
factor: term { $$ = $1; }
| factor MUL term { $$ = $1 * $3; }
| factor DIV term { $$ = $1 / $3; }
;
term: NUMBER { $$ = $1; }
| ABS term { $$ = $2 >= 0? $2 : -$2; }
;
%%
void yyerror(char *s) {
fprintf(stderr, "error: %s\n", s);
}
int main(int argc, char **argv) {
yyparse();
return 0;
}
按一下顺序运行
bison -d fb1-5.y # 一定要加-d,加了以后会生成fb1-5.tab.h文件
flex fb1-5.l # 生成lex.yy.c文件
gcc fb1-5.tab.c lex.yy.c -ll # 这个也跟原书的命令不太一样,生成a.out文件
其实还遇到一些跟c有关系的编译问题,比如atoi
在c99
下不可用,需要使用atoll
。总的来说,问题不大,算是都运行起来了,毕竟原书是2009年出版的。最后难能可贵的是书中例子的ftp下载地址居然还可以用,代码下载。
书中也提到使用Flex和Bison,当然也可以手写。其实对于我这样的多年写代码的人,很多时候太关注实现了,往往失去了高层次的思考,使用Flex和Bison这样的工具是让你直接关注问题本身,比如语言的一些问题。
我用的是docker安装的,创建一个ftp的目录,使用命令。
1 | docker run -d -v 你自己的目录:/home/vsftpd \ |
感觉登录很慢,需要做下配置。
进入容器
1 | docker exec -it 你自己的容器的ID bash |
使用vi编辑配置文件,修改reverse_lookup_enable
字段为NO
1 | vi /etc/vsftpd/vsftpd.conf |
保存配置
1 | source /etc/vsftpd/vsftpd.conf |
删除里面全部的nameserve
,同样保存。
1 | vi /etc/resolv.conf |
可以显示目录单是却无法下载文件,测试文件很小,只有一两次偶然的情况下载成功了,搜索了半天发现都没有遇到跟我一样问题的。最后发现原来是没有正确设置防火墙端口的问题,使用的是阿里云,在安全组我只开通了21100
这个端口,实际上应该是一个范围21100-21110
,安全组的配置应该是21100/21110
。
目前使用Docker进行安装,15672
是网页端访问口,5672
是程序端访问接口。
1 | docker run -d --name my-rabbit -p 15672:15672 -p 5672:5672 -e RABBITMQ_DEFAULT_USER=username -e RABBITMQ_DEFAULT_PASS=password rabbitmq:3.9-management |
这个命令是后台运行,username
以及password
需要替换成你自己的。
Spring Boot启动后会自动连接RabbitMQ,这个在测试环境下可以设置不自动连接。
1 | spring: |
如果监听了没有创建的队列会报异常错误,可以用注解的时候自动创建队列。
1 | @RabbitListener(queuesToDeclare = @Queue("some-queue")) |
这个问题,偶然间我一不小心把RabbitMQ重启,当然没有消息,过了一会发现队列中的新数据并没有消费。最后重启了消费者的服务后,就正常了。所以这个问题,如果不及时发现的话,最终会导致队列出错的,在大项目的时候还是需要注意。
Python自带Http服务器,我一般是在共享文件时使用,直接在需要共享的目录下运行命令即可启动服务,不过有时候传输不是很稳定,重启一下就好了。
1 | python -m SimpleHTTPServer 8080 |
之前主要用Python 2.7的版本,随着2.7的终结加上macOS 12.3已经彻底移除了python 2.x的版本,只能使用3的版本了,Python 3的这个Http服务器命令似乎更简单一些。
1 | python3 -m http.server 8001 |
1 | #!/usr/bin/env python |