Seaside 简明指南

标签: pharo ; smalltalk ; seaside ;


学习 Seaside

Seaside 并不像乍看起来的那么难,只是有些特殊。你只需要学习几个基本的类就能使用它的大部分功能,所以就有了这一个快速教程。

要开始使用,你需要理解 WAComponentWARenderCanvasWATaskWASession。你还需要明白 Seaside 是一个框架,而不是一个 API,因此你需要通过子类化和扩展这些核心类来与之协作。

WAComponent

WAComponent 是你在 Seaside 中主要会打交道的类。一个组件同时代表了“页面”和“用户控件”的概念。如果你是从其他框架转过来的,可以认为“页面”和“组件”是不可区分的。

为了能够在配置界面(/seaside/config)中将一个组件配置为应用的根,你需要在组件的类侧重写 canBeRoot 方法。

FooComponent class >> canBeRoot
   ^ true

这样会在配置编辑器的根组件下拉菜单中显示该组件作为一个可选项。

一个更直接且我推荐的方法是通过在你的根类上创建一个 initialize 方法来程序化地创建你的站点,如下所示...

FooComponent class >> initialize
    "self initialize"
    | app |
    app := self registerAsApplication: #foo.
    app libraries add: SULibrary.
    app preferenceAt: #sessionClass put: FooSession

这会在 /seaside/foo 路径下设置一个调度器,并包含 Scriptaculous 库 (SULibrary) 和一个自定义会话类,该会话类可能包含诸如当前用户或当前数据库会话等信息。然后你可以高亮显示注释 "self initialize" 并运行它来创建你的站点。这种方法还有一个额外的优势,即当你将包加载到任何新的镜像中时,它会自动设置你的站点,并且允许你按需程序化地重新创建你的站点。当升级到更新版本的 Seaside 时,这一点非常有用,因为有时需要重新创建站点。

渲染

Seaside 可以渲染两种类型的内容:一种是视图,通过在 WAComponent 子类中重写的 renderContentOn: 方法来实现;另一种是非 UI 对象,通过重写的 renderOn: 方法来实现。

renderContentOn:renderOn: 都是框架方法,你需要重写它们以便框架能够调用。不要试图自己调用这些方法来渲染对象,也不要随便在一个对象上添加 renderContentOn: 方法并认为它会起作用,实际上不会。renderContentOn: 仅在 WAComponent 的子类中有效。

视图

对于喜欢模型-视图-控制器(MVC)风格的人来说,你会希望将所有的渲染代码保留在代表视图的 WAComponent 子类中。要在 Seaside 中创建一个视图,你需要继承 WAComponent,重写 renderContentOn: 方法,并开始使用传递给 renderContentOn: 方法的渲染画布(由框架在组件渲染时传入)来编写 HTML。

renderContentOn: html
    html div: 'Hello World'

WAComponent 拥有一个名为 children 的集合,其中包含其他的 WAComponent 实例。这构成了用户界面的控件树。一个组件可以由自身以及可选的嵌套子组件或模型构建而成,允许组件组合。这也是人们开始遇到问题并遇到令人头疼的“处理回调时未找到组件”错误的地方。你必须通过重写的 children 访问器返回所有可见的子组件集合。

children
    ^ { header. currentBody. footer }

renderContentOn: html
    html div id: #header; with: [html render: header].
    html div id: #body; with: [html render: currentBody].
    html div id: #footer; with: [html render: footer]

模型

对于那些只想让自己的模型自行渲染并且不需要同一模型的多个视觉表示的人来说,可以跳过 WAComponent,直接在模型中重写 #renderOn: 方法。这样,模型就可以直接参与到渲染过程中,而无需作为组件的一部分。但是需要注意的是,这种方式适用于简单的渲染逻辑,对于复杂的用户界面或者需要多个视图的情况,还是建议使用 WAComponent 来管理视图逻辑。

渲染错误

人们经常忽视这一点并做错,从而导致各种奇怪的错误。你必须知道如何正确地渲染子组件,不要这样做...

renderContentOn: html
    foo renderOn: html

也不要这样做...

renderContentOn: html
    foo renderContentOn: html

无论对象是什么类型的,渲染另一个对象只有一种正确的方式...

renderContentOn: html
    html render: foo

在项目级别创建一次 WAComponent 的子类,并使用这个自定义组件作为超类来编写该项目中的其余组件是一个好主意。这样做为你提供了一个地方来提升你想在整个项目中应用的组件类型的事物,并全局覆盖默认值,比如使用不同的渲染画布。

使用 call: 和 answer:

在传统的 Web 框架中,你通过构建用户提交的表单或包含参数的锚标签及服务器端重定向来在页面之间进行导航,这些参数可以从请求中被另一页面解析。这意味着任何页面都可能成为入口点,而且传递给它的任何参数都不能被信任,必须合理地进行验证和解析,因为用户可以通过编辑浏览器中的 URL 来修改这些参数。这也意味着你只能传递那些可以放入 URL 中的简单参数,如字符串和数字。

Seaside 的工作方式不同。在 Seaside 中,组件是真正的对象,它们可以互相调用 call:answer: 方法,这允许你在它们之间传递任何对象作为参数或结果。这两种方法都是设计用于回调阶段使用的,也就是你的控制器方法中。例如,你可以在结果列表中点击一个链接来编辑某个对象...

editFoo: aFoo
    self call: (FooEditor on: aFoo)

renderContentOn: html
    foos do: [:each | 
        html div class: #fooRow; with:
            [html anchor 
                callback:[ self editFoo: each ]; 
                text: 'Edit'.
             html text: foo description ]]

一个组件在渲染阶段绝不能调用或回复另一个组件。请注意,在方法 editFoo: 中传递给 renderContentOn: 锚点的 call: 是作为一个回调块传递的,在渲染期间不会被调用,只有当用户点击锚点时才会被调用。

这是一个简单的案例,更复杂的情况可能包括某种工作流,其中组件会返回结果。

orderFoo: aFoo
    | customer address order |
    customer := (self call: CustomerForm new) ifNil: [ ^self ].
    address := (self call: AddressForm new) ifNil: [ ^self ].
    order := (self call: (FooOrder foo: aFoo customer: customer shipTo: address)) ifNil: [ ^self ].
    self sendEmailConfirmationFor: order

在每种情况下,每个被调用的组件都会使用 #answer: 来返回一个结果,或者如果用户按下取消按钮,则返回 nil。整个多页面订单处理流程的工作流在这里得到了体现,每个组件的结果会被后续步骤使用,或者如果用户取消了操作,则会中断流程。

通过将回调直接附加到用户动作上,你无需担心如何在 URL 中表示状态或在工作流的后续步骤中解析和验证输入以及重新从数据库中获取模型。Seaside 应用中的每个页面都不是有效的入口点,因此用户不能随意篡改 URL 来导航到你不希望他们看到的页面。

WARenderCanvas

WARenderCanvas 包含了用于创建 HTML 的 API。目前它是默认的画布,但你可以覆盖它以返回你自己定制的子类,这样做是为了通过添加新方法来抽象出你应用中频繁执行的常见操作。例如,你可能不喜欢默认的日期输入实现,或许你需要一个时区感知的日期输入来实现精确的时间安排。你可以编写自己的日期标签,并在自定义画布中覆盖 dateInput 方法,以返回你应用程序特定版本的 dateInput。你可以根据需求进行自定义,扩展框架的画布,使其能够使用应用程序的语言。

rendererClass
    ^ MyCoolRenderCanvas

WARenderCanvas 是理解如何在 renderContentOn: 中编写代码的一个起点。当你感到困惑时,只需查看画布并找到你想要的方法,看看它创建了哪个标签对象,然后查看标签类以了解哪些属性对它是有效的。

我几乎所有的关于 Seaside 的知识都是通过这种方法学到的。文档固然好,但并不总是可用,实际上,你真的可以通过直接查看画布提供的标签类入口点,利用简单的构建模式轻松找到你需要的东西。

WATask

WATaskWAComponent 的一个特殊子类,用于执行工作流。WATask 的使用方式与组件类似,只不过你需要重写 go 方法而不是 renderContentOn:,并且必须调用另一个组件,因为任务本身没有用户界面,因此不是一个可渲染的对象。

任务本质上协调其他组件的显示,通过调用、显示组件并从组件获取答案,你可以编写非常简单和优雅的代码来决定下一步的操作。

当一个组件调用另一个组件时,被调用的组件会在用户界面上取代调用者的位置。如果调用者是另一个组件的子组件,那么被调用者似乎会取代它在复合组件中的位置。所有组件仍然存在,调用者只是在被调用者返回结果之前不会显示。这使得你可以很容易地设置一个父组件来协调其子组件的显示,当涉及到复杂的流程时,这一点非常有用。

WASession

WASession 是可选的,严格来说你不必子类化它并使用它,但如果有一些数据你希望在整个当前会话的作用域内全局可用,创建一个带有这些数据访问器的自定义会话类通常会很方便。会话作为一个上下文对象,可供你所有的组件使用。这是放置数据库连接、某些配置数据或当前用户的好地方。Seaside 会话本质上是单线程的,因此除非你显式地启动了多线程操作,否则你不必担心并发或锁定问题。

无论你使用什么样的会话对象,都可以通过 session 访问器在每个组件中获得。在任何组件内部,你可以说 self session 来访问你的会话对象。

WASession 也是你可以找到其他最终想要访问的事物的地方,比如当前请求。你可以像这样读取查询字符串的值...

self fieldsAt: #someKey

WASession 也是 redirectTo: 方法所在的地方,这是一个你可能会需要用到的功能。

self session redirectTo: 'http://www.google.com'

危险,威利·罗宾森

  • Seaside 使用线程局部变量来存储当前会话,因此如果在处理过程中你通过 fork 分支一个块来在另一个线程上执行一些工作,最好在调用时将该块所需的数据传递给它,因为一旦在另一个线程上启动,它将不再能够访问当前会话。

  • 除非你是受虐狂,否则不要尝试在后台线程上创建 UI 组件,因为 Seaside 在实例化 WAComponent 子类时期望能够访问当前会话。

  • 不要在渲染方法中放置工作流逻辑或改变组件的状态,渲染方法应该能够被多次调用而不影响组件或改变其状态。如果用户刷新页面,你的组件将以当前状态再次被渲染,所以在渲染期间不要随意更改状态。简单的逻辑,比如决定是否渲染某内容或在一个循环中渲染内容是可以的,但不要在渲染时更改实例变量值或调用其他组件。

  • 不要直接从其他组件的渲染方法中调用组件,确保所有的 call: 都位于 callback: 块中,不知为何这一点常让新手栽跟头。

  • 当一个页面post回来时,任何表单数据都会被用来更新组件的状态,这发生在任何回调处理之前。你不需要直接访问请求,只需使用 Seaside 已经为你更新过的实例变量即可。

  • 使用 on:of: 快捷方式将表单控件直接绑定到组件或组件模型上的访问器,这可以节省大量输入,并且适用于大多数控件,同时有助于将命令代码分解到单独的方法中,而不是内联在某个渲染方法中,这样更容易找到或重用。这仅仅是为控件附加回调块的一种快捷方式,在你还在摸索的时候,它有助于快速原型设计。

  • 如果一个组件有子控件,你必须重写 children,并返回一个包含所有当前子控件的集合,否则你会遇到问题。你必须在父组件的 initialize 方法中创建这些子控件,或者在回调期间安全地更改组件状态时创建。不要在渲染阶段懒加载创建子控件。

  • 如果子控件列表是动态的,或者你希望浏览器的后退按钮能够真正使服务器端的状态回退到与用户浏览器缓存中的状态相匹配,务必重写 states 并返回一个需要回溯的对象集合,可能包括 self

就这些?

不,关于 Seaside 还有很多东西需要学习,我遗漏了很多内容,但这些都是基础,应该能让你迅速上手并提高效率。不久之后,你应该会理解为什么 Seaside 是革命性的;这可能不是你最后一个使用的 Web 框架,但它确实可能是你最后一个需要的 Web 框架。15 年过去了,我们现在有了像 React 这样的框架,它们只不过是 Seaside 组件模型的一个蹩脚模仿而已。

简单的登录过程和组件示例

我通过调用 inform: 稍微作弊了一下,inform: 是一个内置的通用对话框,用于向用户显示消息并获取确认。

SeasideLoginTask class>>initialize
    "sets up app at http://localhost/seaside/loginSample"
    self registerAsApplication: #loginSample

SeasideLoginTask>>go
    | user |
    user := self call: SeasideLogin new.
    user ifNil: [self inform: 'Unknown user or password, please try again!']
         ifNotNil: 
            [self inform: 'Congratulations, you are in!'.
            self session redirectTo: 'http://onsmalltalk.com']

以及

SeasideLogin>>userName
    ^ userName

SeasideLogin>>userName: aName
    userName := aName

SeasideLogin>>password
    ^ password

SeasideLogin>>password: aPassword
    password := aPassword

SeasideLogin>>login
    (userName = 'seaside' and: [password = 'rocks']) 
        ifTrue: [ self answer ]
        ifFalse: [ self answer: nil ]

SeasideLogin>>renderContentOn: html 
    html form: 
        [html heading level3; with: 'User Name:'.
        html textInput on: #userName of: self.
        html heading level3; with: 'Password:'.
        html passwordInput on: #password of: self.
        html break; submitButton on: #login of: self]

原文:http://onsmalltalk.com/terse-guide-to-seaside