Jasper Ji

平常心

主要参考官方的这篇文章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>
        }
    }
}

这种形式的重点是生命周期,上面的例子只使用了最常用的createupdateview这三个方法。下面是所有的生命周期的方法:

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_refuse_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的用户,更准确的是应该Vue 2用户,Vue 3还没有来得及学习。Element UI 也是基于Vue2的组件库。一开始我觉得这只是JS和Rust的不同,很多时候我都是用Vue的思维在学习使用Yew这个框架,但渐渐发现Yew与Vue在设计思想上有很大不同,本质上这实际上是React与Vue的不同,尤其是React的函数式组件思想。

Rust vs JavaScript

动态性

在写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 按钮

一开始我选择了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版的按钮功能,但核心功能是可以用的。

Rate 评分

评分组件在以前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自己的设置了。

ColorPicker 颜色选择器

完成了评分组件后,想到找个再复杂一点的,于是选中了颜色选择器这个组件,一看这个组件是由好几子组件组成的,就是他了。颜色是个比较专业的东西,原版专门有个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

WC例子

书中第一个例子类似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有关系的编译问题,比如atoic99下不可用,需要使用atoll。总的来说,问题不大,算是都运行起来了,毕竟原书是2009年出版的。最后难能可贵的是书中例子的ftp下载地址居然还可以用,代码下载

书中也提到使用Flex和Bison,当然也可以手写。其实对于我这样的多年写代码的人,很多时候太关注实现了,往往失去了高层次的思考,使用Flex和Bison这样的工具是让你直接关注问题本身,比如语言的一些问题。

我用的是docker安装的,创建一个ftp的目录,使用命令。

docker run -d -v 你自己的目录:/home/vsftpd \
-p 20:20 -p 21:21 -p 21100-21110:21100-21110 \
-e FTP_USER=用户名 -e FTP_PASS=密码 \
-e PASV_ADDRESS=主机IP -e PASV_MIN_PORT=21100 -e PASV_MAX_PORT=21110 \
--name vsftpd --restart=always fauria/vsftpd

连接慢

感觉登录很慢,需要做下配置。

进入容器

docker exec -it 你自己的容器的ID bash 

使用vi编辑配置文件,修改reverse_lookup_enable字段为NO

vi /etc/vsftpd/vsftpd.conf 

保存配置

source /etc/vsftpd/vsftpd.conf

删除里面全部的nameserve,同样保存。

vi /etc/resolv.conf
source /etc/resolv.conf

无法下载

可以显示目录单是却无法下载文件,测试文件很小,只有一两次偶然的情况下载成功了,搜索了半天发现都没有遇到跟我一样问题的。最后发现原来是没有正确设置防火墙端口的问题,使用的是阿里云,在安全组我只开通了21100这个端口,实际上应该是一个范围21100-21110,安全组的配置应该是21100/21110

参考

fauria/vsftpd

安装

目前使用Docker进行安装,15672是网页端访问口,5672是程序端访问接口。

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,这个在测试环境下可以设置不自动连接。

spring:
  rabbitmq:
    listener:
      direct:
        auto-startup: false # 测试环境下屏蔽掉RabbitMQ的自动重连
      simple:
        auto-startup: false # 测试环境下屏蔽掉RabbitMQ的自动重连

自动创建队列

如果监听了没有创建的队列会报异常错误,可以用注解的时候自动创建队列。

@RabbitListener(queuesToDeclare = @Queue("some-queue"))
public void someMethod() {
  // TODO Some Things
}

队列不消费

这个问题,偶然间我一不小心把RabbitMQ重启,当然没有消息,过了一会发现队列中的新数据并没有消费。最后重启了消费者的服务后,就正常了。所以这个问题,如果不及时发现的话,最终会导致队列出错的,在大项目的时候还是需要注意。

Python自带Http服务器,我一般是在共享文件时使用,直接在需要共享的目录下运行命令即可启动服务,不过有时候传输不是很稳定,重启一下就好了。

Python2 版本

python -m SimpleHTTPServer 8080

Python3 版本

之前主要用Python 2.7的版本,随着2.7的终结加上macOS 12.3已经彻底移除了python 2.x的版本,只能使用3的版本了,Python 3的这个Http服务器命令似乎更简单一些。

python3 -m http.server 8001

跨域配置

#!/usr/bin/env python
try:
    # Python 3
    from http.server import HTTPServer, SimpleHTTPRequestHandler, test as test_orig
    import sys
    def test (*args):
        test_orig(*args, port=int(sys.argv[1]) if len(sys.argv) > 1 else 8000)
except ImportError: # Python 2
    from BaseHTTPServer import HTTPServer, test
    from SimpleHTTPServer import SimpleHTTPRequestHandler

class CORSRequestHandler (SimpleHTTPRequestHandler):
    def end_headers (self):
        self.send_header('Access-Control-Allow-Origin', '*')
        SimpleHTTPRequestHandler.end_headers(self)

if __name__ == '__main__':
    test(CORSRequestHandler, HTTPServer)

早前为了了解如净禅师,整理过《如净和尚语录》的简体拼音版,当然有段时间也试图寻找更多关于禅师的资料,不过都不成体系。后来得益于CBETA Online系统提供的线上众多禅师的语录资料,才有以下的研究。

天童寺

如净禅师是什么时间入住天童寺的呢?单从如净禅师在天童寺上堂的记录来看不好推断。这里我主要根据“浙翁遗书”来判断,据《如净和尚语录·明州天童景德寺语录》记载:

浙翁遗书至。上堂。八月十八钱塘潮。浙翁声价泼天高。尽教四海弄潮手。彻底穷渊辊一遭。重拣择不辞劳。要透龙门继凤毛。忽然收卷还源去。万古曹溪风怒号。

如净禅师离开净慈寺后石田法薰禅师成为了下一任住持,据《石田法薰禅师语录·临安府净慈报恩光孝禅寺语录》记载:

浙翁遗书至。上堂。千五百人善知识。不念吾宗正岑寂。五峰趯倒浪翻空。大地山河俱失色。金风体露。叶落归根。只堪惆怅不堪陈。

二人都收到浙翁禅师圆寂的通知,浙翁禅师全称浙翁如琰(yǎn),曾经住持天童寺,所以在天童寺住持的如净禅师,在语录中有一篇《为浙翁入祖堂》,据《如净和尚语录·小佛事》记载:

为浙翁入祖堂

昔从太白凌霄去。今自凌霄太白来。不堕去来生死路。

展真云:看堂堂面目笑咍咍。且道,笑向阿谁?

以真指祖云:大家元是主中主,惯入驴胎与马胎。

石田法薰在入住净慈寺之前是在建康府太平兴国禅寺,在《建康府太平兴国禅寺语录》中明确的写到“师于嘉定十六年四月二日入院”,堂语录最后有明确时间的是嘉定十七年端午,再一转就是已经在净慈寺了,我一开始根据如净禅师关于浙翁遗书至,明确写道是八月份,所以一开始推断石田法薰禅师是嘉定十七年入住的净慈寺,但根据石田禅师语录中行状,有点出入,据《石田法薰禅师语录·行状》记载:

庙堂精选择。乃以师补处。宝庆元。有旨迁南山净慈。端平二。复有旨。迁北山灵隐。两山居各十年。

所以石田法薰禅师是宝庆元年入住净慈寺,再根据石田法薰在净慈寺的语录判断收到浙翁遗书的时间是宝庆元年的八月份。实际上浙翁禅师,确实圆寂于宝庆元年。再看如净禅师收到浙翁遗书的时间,基本在天童寺语录的后面,根据天童寺上堂语录,最后推断如净禅师是在嘉定十七年下半年去的天童寺。正是在这一年,住持天童寺的无际了派圆寂,而在净慈寺的如净禅师也收到了通知,据《如净和尚语录·再住净慈禅寺语录》记载:

派和尚遗书至。上堂。万派朝宗一派收。扬清激浊几经秋。忽然到底都干却。露柱灯笼笑不休。且道。笑箇什么。下座同诣灵凡。羞法供养。

正是因为无际了派禅师的圆寂,所以才有了如净禅师奉旨从净慈寺迁天童寺的后话。

圆寂时间

关于如净禅师的圆寂时间,有多种说法,一说在绍定元年,但我研究发现时间不对。如净禅师圆寂后,当时住阿育王寺的无准师范收到了如净禅师的遗书,据《无准师范禅师语录·住庆元府阿育王山广利禅寺语录》记载:

前住天童净和尚遗书至。上堂。太白峰前收阵脚。鉴湖归唱村田乐。无端调转入新丰。谁知错处非常错。直得洞水逆流。乳峰倒卓。石女攒眉。木人泪落。此曲如今谁共闻。越山无际天无垠。

阿育王寺离天童寺很近,所以这个应该是第一时间就收到的。这个记录在《无准师范禅师语录·住庆元府阿育王山广利禅寺语录》的前段,是刚入住没有多久就收到的,所以我一直觉得只要知道无准师范是什么时间入住的阿育王寺就可以推出如净禅师圆寂的年份了,后来在《径山无准和尚入内引对升座语录》找到了相关的线索,据出自《径山无准和尚入内引对升座语录》记载:

绍定六年七月十五日。

皇帝御修政殿引见师领本寺僧众山寿毕。提举都知太尉张延庆引见祝皇帝寿。

师捧香云。根同天地。秀发山川。薰霭宝炉。瑞腾沙界。恭为祝延皇帝陛下圣躳万岁。万岁。万万岁。指七金山为寿山。巍巍不动。以四大海为福海。渺渺无穷。长居北极之尊。永作中天之主。金枝玉叶。育秀腾芳。八表归仁。万邦入贡。

都知太尉云。请长老敷演。

乃奏云(臣)僧(师范)。一介庸衲。生于西蜀。浪游湖海。今四十年。于道无闻。每切自愧。夙生何幸。两蒙睿旨。扫洒庆元育王。而至双径。自去年八月领职。与四方衲子朝夕禅诵。仰报圣恩。

另据《后村先生大全集·径山佛鉴禅师》记载:“绍定壬辰秋,奉诏住径山”,结合二者可知无准师范是绍定五年秋离开时阿育王寺到径山寺住持。语录虽然没有明确的纪年,但是包含一系节日类的记载,有周期性。根据无准师范在阿育王山广利禅寺的语录反复推断发现,收到如净禅师遗书是宝庆三年,时间应该是宝庆三年七月十四到九月九日之间,大概可以推断如净禅师应该是这个时段圆寂的。

另一个证据是“天基节”这个节日是宋理宗时才有的,是宋理宗的生日正月五日,这个是在宋宁宗嘉定十七年十一月设置的。无准师范住阿育王寺语录中刚好有三个跟此节日有关的记载,宋理宗的第一个年号是宝庆,宝庆只有三年,所以依次推算无准师范是在宝庆三年的上半年到的阿育王寺。

语录考证

现存的两本语录《如净和尚语录》、《天童山景德寺如净禅师续语录》。《如净和尚语录》应该是刊行版本,在后世资料都引用的是这个版本里面的内容。《天童山景德寺如净禅师续语录》有人认为是后人伪造,所以暂且不论。这里主要研究下语录发行的几个时间点,按时间顺序排序。

狮子吼无畏说。百兽闻之皆脑裂。天衣举似箇中人。迈古超今离途辙。

绍定戊子中秋 天衣住山比丘文蔚谨跋

绍定戊子即绍定元年,也就是宝庆三年的下一年,因为宝庆只有三年。

净禅师得无师句。用逸格机。娄至德已前。青叶髻之后。突出无面目底。糙暴生狞。通身是眼。要看是录。予保。渠未梦见此老脚跟下汗臭气在。

绍定改元开炉日 灵隐高原祖泉敬跋

绍定改元即绍定元年,根据宋代的《禅苑清规》记载开炉日为农历十月一日,闭炉为农历二月一日,大概就是类似现代的烧暖气吧,所以就是绍定元年十月一日的时候,灵隐寺的高原祖泉题写了跋。

绍定二禩岁在己丑桐柏散吏吕潇 敬书

这个是如净禅师语录序的题款,实际即绍定二年,虽然没有写几月几日,但应该是在同年的六月初伏之前。

岁次己丑六月初伏日。小师广宗募刻板。临安府灵隐景德禅寺住持祖泉挍勘焉。

这个是绍定二年,也是己丑年,六月初伏日刻板的。

现在的语录是日本藏版本,但是语录至少到明末时,国内还保留有其语录,据明末清初僧净行的《远门禅师摘欺说》记载:

我长翁净祖六坐道塲,有语录在大中老师处,时山翁和尚增修天童志,乃取以参入山乘,其稿付山僧订刻板藏。

但是国内的语录后来应该是遗失了。发现国内引用语录的部分,主要是在天童寺住持期间的语录,没有其他寺庙的语录。

雪窦足庵

如净禅师圆寂前自认为是雪窦足庵的弟子,但他是何时参礼的?据《如净和尚语录·住建康府清凉寺语录》记载:

师于嘉定三年十月初五日。于华藏褒忠禅寺。受请入寺。

搜索了下华藏褒忠禅寺基本没有相关的资料,大致推测是现在无锡华藏寺。从嘉定三年到宝庆三年,出世住持丛林十八年时间,另据《如净和尚语录·偈颂》记载:

师六坐道场未禀承众或是请师云待我涅槃堂里拈出果临终拈香云

如净行脚四十余年。首到乳峰。失脚堕于陷穽。此香今不免。拈出钝置我前住雪窦足庵大和尚。

并书辞世颂云。

六十六年罪犯。弥天打箇𨁝跳。活陷黄泉。咦。从来生死不相干。

圆寂时六十六岁,即宝庆三年的时候,根据古人过年即增一岁,推断禅师出生于绍兴三十二年(1162年),四十九岁的时候入住清凉寺,开始了主持生涯。据宋代楼钥的《攻愧集·雪窦足庵禅师塔铭》记载,足庵禅师是绍熙二年圆寂,在雪窦山八年时间,即淳熙十一年至绍熙二年。淳熙十一年,如净禅师二十三岁,到绍熙二年如净禅师是三十岁,所以如净禅师跟随足庵禅师的大致时间段也在这个范围。大致推算从三十岁开始到四十九岁,十八年间应该是在各大丛林参学时间。

如净与道元

按道元的记载他是宝庆元年五月一日见的如净禅师,这时如净禅师在天童寺已经半年多了,到宝庆三年如净禅师圆寂,实际上道元禅师跟如净禅师学法也就两年时间。看起来时间不长,但或许在参学的过程中道元禅师其实已有自己的答案,只是需要一个验证而已,刚好在对的时间遇到了如净禅师。

道元创建日本的曹洞宗,提倡只管打坐,不参话头,但如净禅师语录看不出如净禅师有这样的观点,据《如净和尚语录·明州天童景德寺语录》记载:

上堂。心念分飞。如何措手。赵州狗子佛性无。只箇无字铁扫帚。扫处纷飞多。纷飞多处扫。转扫转多。扫不得处拼命扫。昼夜竖起脊梁。勇猛切莫放倒。忽然扫破太虚空。万别千差尽豁通。

再看元代智彻禅师《禅宗决疑集·禅林静虑门》一节。

此举丛林纲纪坐禅寂静一节。古来佛法兴隆丛林茂盛。天龙协佑施主归崇。受用现成常住丰厚。处处安禅着众。人人慕道精修。或三五百之多僧。或一二千之众士。东西两序执事营为。内外一如铺心若地。箇箇如因识果。人人见道明心。三德六味总无亏。四事七珍皆具足。所以僧堂中学般若菩萨。十指不点水。百事不干怀。粥饭之余专心在道。上根利器者。不离单位坐究一乘。昼夜惺惺端持正观。后来各人有大发明成大法器收因结果。向丛林中为大宗匠。开大炉鞴煆炼学人。做工夫处。先举上床一种。威仪事在精诚。须要跏趺端坐。眼端鼻鼻端脐。牙关紧咬拳头紧捏。待喘息已定。举箇话头。僧问赵州。狗子还有佛性也无。州云无。不用动口动舌。默默参究以悟为期。此是丛林中坐禅仪式样子。众所共知。

坐禅时,参赵州狗子佛性无这一公案,是当时禅门普遍的一种方式,所以如净禅师也并没有出其左右。是在北宋的《禅苑清规·坐禅仪》中,并没有参公案的方式,所以道元更多的应该是吸收这个比较多一点,据《禅苑清规·坐禅仪》记载:

尽学般若菩萨。先当起大悲心。发弘誓愿。精修三昧。誓度众生。不为一身独求解脱尔。乃放舍诸像。休息万事。身心一如。动静无间。量其饮食不多不少。调其睡眠不节不恣。欲坐禅时。于闲静处厚敷坐物。宽系衣带。令威仪齐整。然后结跏趺坐。先以右足安左䏶上。左足安右䏶上。或半跏趺坐亦可。但以左足压右足而已。次以右手安左手上。左掌安右掌上。以两手大拇指面相拄。徐徐举身前欠。复左右摇振。乃正身端坐。不得左倾右侧前躬后仰。令腰脊头项骨节相拄状如浮屠。又不得耸身太过。令人气急不安。要令耳与肩对。鼻与脐对。舌拄上腭唇齿相着。目须微开免致昏睡。若得禅定。其力最胜。古有习定高僧坐常开目。向法云圆通禅师亦诃人闭目坐禅。以谓黑山鬼窟。盖有深旨。达者知焉。身相既定。气息既调。然后宽放脐腹。一切善恶都莫思量。念起即觉。觉之即失。久久忘缘。自成一片。此坐禅之要术也。

如净禅师塔

目前语录中缺少记录如净禅师的塔铭或者行实,所以如净禅师塔于何处并没有明确的记载。清康熙年间的《续指月录》记载,如净禅师圆寂后塔于天童,但是据天童寺志记载,万历间发洪水,很多遗迹都已毁,所以作者这么写也缺乏依据。日本的《贞和集》,是一本收录国内禅师言语的合集,记载有楫翁者曾礼如净禅师塔,据《贞和集·卷一·礼塔》记载:

净和尚塔

楫翁

杜鹃啼血绿阴交,三远萝龛恨转淆。紫癜宸台绝车迹,月明金凤宿龙巢。

楫翁者,并不见于现存的资料中,除此之外没有看到其他人礼塔的记录存世。据现在天童寺的崇和师父的介绍,如净禅师塔应该在南谷庵,但是万历年间的洪水早已把那里摧毁,后来在那一块建设了密云圆悟的塔院,虽然日本方面曾多次来找塔,但都没有结果,我在想既然是洪水,或许塔之类或许早已被冲到其他地方,或许淹埋与泥土之中吧,当然这是猜想。

如今杭州净慈寺后有如净禅师塔,据现在天童寺的崇和师父的介绍,此塔是清末日本人修建,并没有说明明确的依据,实际上净慈寺后山的很多宋塔,在明代就已经毁掉了。我查过嘉庆版的《净慈寺志》并未有如净禅师塔的记录,甚至在主持中都没有把如净禅师列入,虽然禅师两度主持净慈寺,但不清楚为什么没有记载,可见如净禅师被遗忘已经很久了。

文献记载

以下整理了其他文献关于如净禅师的记载:

无文道璨禅师,据《无文道璨禅师语录·䟦天童净和尚寿无量墨迹》记载:

䟦天童净和尚寿无量墨迹

无量拳头能杀而不能活。天童拳头能活而不能杀。闲云亲中二老之毒。山河大地草木丛林至今忍痛未已也。虗空霹未尝不殷然天地间。雅维那于展卷处忽然轰入髑髅。政恐不及掩耳。

这位禅师是南宋宝佑之后,可见那会如净禅师禅师的墨迹还是有留世的。实际后来发现无文道灿禅师还为如净禅师起棺,据《无文道璨禅师语录》记载:

南庵主起棺

见了天童便跺跟。佛来有口不能吞。莓苔绿遍门前路。坐看春风四十年。无禅道可论。无佛法可传。拾薪樵子无可寻之迹。衔花飞鸟无可见之缘。折脚铛中烂煑乾坤清气。长柄杓内舀干沧海根源。了生死去来之如幻。观涅槃寂灭之现前。回首鉴湖青山未老。笑携藜杖白首言蔙。这箇又是某人可见之踪迹。设若放阔步于藕丝孔中。入正定于微尘影里。诸人又向什处与此老相见。阎浮树在海南边。

据《如净和尚语录·住建康府清凉寺语录》记载:

璨禅客至上堂。金刚宝剑入红炉,煆出杨岐三脚驴。

禅师与如净禅师曾有交集,时间在如净禅师住健康府清凉寺,

希叟绍昙禅师,这位禅师是南宋淳佑年间人,据《希叟绍昙禅师广录》记载:

䟦天童净和尚墨迹。诸老䟦后

太白死句中有活句。诸老活句中有死句。死活向上有事在。拟议寻思。吴元济不待夜入蔡州城。已被擒捉了也。具透关眼者。切忌扫雪求迹。年月日。

虚堂和尚,据《虚堂和尚语录·行状》记载:

道过金山。掩室和尚。一见甚器重。

这里提到虚堂禅师有见过掩室和尚,而如净禅师在首住净慈寺语录中有记录到这位禅师,据《如净和尚语录·临安府净慈禅寺语录》记载:

谢掩室和尚。上堂。掩室摩竭国。老胡豁开顶门。杜口毗耶城。净名败缺话柄。提上古两端公案。发今朝一段威光。所以宾主历然。江湖有在。还知么。不是诗人不献诗。春风吹作鹧鸪词。

这位掩室和尚即掩室善开禅师,松源崇岳禅师的法嗣。如净禅师也曾在松源崇岳处参学,据《虚堂和尚语录·行状》记载:

由是回浙到净慈。见净和尚。净问云。尔还知所生父母通身红烂。在荆棘林中么。师云。好事不在匆忙。净随后打一拳。师展两手云。且缓缓。

掩室和尚当时在金山,所以再联系到虚堂禅师行状中的“净和尚”应该就是当时的如净禅师,

月江正印禅师,一位元代禅师,曾经在元顺帝元统元年入住阿育王寺,而且归在佛祖赞章节中,可见对于禅师的认可。据《月江正印禅师语录·佛祖赞》记载:

天童净和尚

两头白牯眉毛竖。三面狸奴鼻孔凹。一只皮靴能剔脱。月明金凤宿龙巢。

如净禅师虽然也曾两次入住净慈寺,但是清代编撰的《净慈寺志》却没有关于如净禅师的记录,同样是清代的《天童寺志》直接把禅师归入到元代。这些都说明元代之后,如净禅师逐渐被人们遗忘,或许当时的资料也是不全。

灵隐高原

语录作跋的是高原禅师,虽不清楚禅师与如净禅师的关系,但也可见一般。实际上这确实是一位了不起的禅师,在当时也是很有威望的。

高原禅师与无准禅师的关系又非同一般,曾经住梨洲的时候,无准禅师也是一块同行的。后来高原禅师圆寂后无准禅师也收到了讣音,可见关系之一般。

灵隐高原和尚讣音至。上堂。来无所从。南高峰。北高峰。去无所至。东㵎水。西㵎水。幻泡忽灭。证得乌龟成白鳖。清风未已。须信高原元不死。既不死。且道在什么处。拈起拄杖云。见么。见么。卓一下。云。认着依前不相似。

出自《无准师范禅师语录·住庆元府阿育王山广利禅寺语录》

《率庵梵琮禅师语录》中有一个偈语,是送高原从台州去灵隐的。

寄台州瑞岩高原禅师住灵隐

水出在高原。源深到冷泉。饮者秃却舌。嗅者鼻孔穿。口鼻两俱丧。妙用绝正偏。侧耳与招手。听猿同呼猿。藤萝影里石磊磊。双㵎合流波涟涟。南来北来脚下过。欲知冷暖待驴年。

如净禅师在建康府清凉寺后的下一个住锡的就是台州瑞岩,推测应该是接的高原禅师位置,再大胆的猜想一下,或许就是高原禅师的推荐了。

《北磵居简禅师语录·常州顯慶禪寺語錄》写到收到了高原禅师的訃音,可见关系一般,而这位禅师晚年入住了净慈寺。

高原和尚讣至。上堂。二月十一。人从北山来。报高原死也。似者般僧。只患多不患少。哑。更覔一箇。如星中月。砂里金。簸土扬尘无处寻。

《西岩了慧禅师语录·行状》中写道西岩了慧禅师早年跟随过高原禅师。

十九薙发。灯授以般舟念佛三昧。非其志也。辤往成都讲席。习性宗经论。俄叹曰。义学岂究竟法哉。染指足矣。去谒坏庵照于昭觉。一见心许法器。趣其南询。乃束包出三峡。由湖湘而至江浙。见浙翁琰于径山。闻高原泉孤硬径直。往依之。同枯寂甘如饴。泉迁台之瑞岩。令师与俱。泉问。山河大地。是有是无。拟开口。即喝出。以偈呈。即曰没交涉。偶侍次。令书龙门三自省。白杨示众语。泉阅之笑曰。写字与做言何尽得。争奈没交涉何。师愤悱莫伸。泉曰。吾方便娄矣。汝自不顾。盖缘法不在此。其往见雪窦乎。时主雪窦席者。佛鉴无准范也。

“孤硬径直”或许就是对这位禅师最好的表述吧,不知道当时有没有语录还是后来遗失的缘故,只有个别零散的一些语录夹杂在其他资料里面,而且在如净禅师圆寂后绍定二年二月份的时候高原禅师也圆寂了,而绍定元年的十月一日的时候禅师才为《如净和尚语录》作了跋。

因为之前看过《启示录:打造用户喜爱的产品》,看到这本时,我当时想着是否要入手,看了下目录似乎跟《启示录》不一样,但当时并入手,后来看一些豆瓣的评价,与前作相比评分不高,主要是集中在章节多,内容不够深入,所以更不想入手了。最近的一些事让意识到产品的重要性,于是在京东的试读上,看了点开头,提到了坎贝尔这个人,这个其实硅谷很有名的被誉为硅谷教练的人物,之前在《乔布斯传》中有,但我没有想到坎贝尔对于谷歌、脸书这样的公司也是影响,这个是后话,总之我入手《启示录2:打造优秀的产品团队》这本书,不到300页的,大部分在上班路上的地铁上看到,最后这点在家看完了。

内容

先说说这个书名吧,《启示录:打造用户喜爱的产品》,原书名是《Empowered: Ordinary People, Extraordinary Products》,如果直译的应该是《赋能:普通人,非凡产品》,但可能是为了凑前作《启示录》的热度,起了这么一个名字。

书中倡导自主型团队,我感觉的这是一种过于理想的团队,或许这应该是创业团队的理想状态。大家志同道合,相互贡献自己的知识,为一个共同的产品愿景努力。有书中的团队,也让想到过去或者现在所在团队的一些问题。

总结

面向老板型团队

几年前我所在的一个公司就是这样现状,老板很有钱,决定超着互联网的方向玩一把,当时正值移动应用兴起的2014年,大家都在做APP,当时的开发团队其实比较全,客户端有iOS、安卓,服务端、前端、测试。我当时负责iOS,安卓的人当时比我们iOS多些。现在回想起来,那会技术团队人员配置最全,设计团队只是按项目配的人并没有设计负责人,所有人向当时的一位类似项目经理的人汇报。现在回想起来,当时是没有产品经理的,如果真的要找一位,那可能就是并不是常来但总是不断提需求的老板吧。后来的一些项目也设置过产品的岗位,但这名产品是项目经理转过来的,并不专业,完全是他们设计他们的,我们拿到了就得开发,而我们觉得这个功能有问题,那会总感觉我们想把产品做好,但总是做不好的怪圈。

这样的现象,我把称为面向老板型团队,当时没有产品经理是一个最大问题,老板自己想着如何开发产品,然后将这种需求直接给开发团队,并没有转换成合理的产品需求,如果老板比较强势的话,即使大家觉得有问题,在一次次沟通无效后,最终在变成了妥协,而后的团队的一些人就转变成面向老板的倾向,不再关心产品本身,最终这样的团队注定成功不了,事实也是这样。

面向业务型团队

后来我去了一家传统公司,当时招技术,本想着技术可以对这个业务有一定的赋能。但慢慢的发现陷入了书里提到的功能性团队的境地,而我更愿意称为面向业务型团队。传统公司的问题可能更严重,尤其是中小型的,这类企业的大都是通过贷款来做业务,所以每一分钱都需要有产出,所以这类公司很难在产品上有预先的投入,所以我们技术这边往往是平时不太忙,一旦有新的业务来了,我们就得立马开发业务相对应的功能,这几乎注定每次都是草草开始,只能先做一些功能让业务先用。后来我渐渐发现除了技术团队的投入不足外,我们最大问题就是没有产品,没有产品愿景,有的只是不停变化的业务。

0%