海滨漫步( A Walk on Seaside )
请先
下载后,再开始本教程。
如果你选择自己安装 Seaside ( 从 SqueakMap) ,则会让你选择一个用户名和密码,这个用户名和密码将用于 Seaside 的配置程序。
一切就绪以后,你需要启动 Seaside 服务。 Seaside 提供了 WAKom 类作为 Web 服务器的接口。要启动一个服务器的实例,要在一个工作空间里执行以下代码:
WAKom startOn: 9090
Seaside 将会在 9090 端口监听需求。 你存储映象以后,无论何时再启动, Seaside 服务将会自动启动。
基本概念: 会话与组件
要测试 Seaside 是否在运行,请在浏览器中浏览 http://localhost:9090/seaside/counter 。这样会运行一个最小的 Seaside 应用:一个带有增加和减少链接的计数器。我们稍后就研究一下这个小应用,但现在,请试验一下它是否工作正常:点击 "++" 链接使数字增加, "--" 链接使数字减少。
在试验计数器的时候,请留心一下浏览器地址栏里的 URL 。 URL 有两个需求参数,这两个参数都告诉了我们 Seaside 运作的一些信息。第一个,是一个随机字母数字序列,就像 _s=ZXsNyDAuodddtmEG 这样,并在运行这个应用的过程中保持不变。这是代表当前用户会话的一个唯一键值。会话是一个 Seaside 应用的核心概念。大多数的 web 框架都提供一个会话对象,仅仅作为保持状态的一个空间,并集中于一个需求/响应的循环; Seaside 则不同,它把会话作为一个进程或者线程对待,从一个定义好的入口点开始会话,然后应用线性地往下处理,在给用户显示网页和等待用户输入时暂停。稍后我们在研究控制流的时候,再更深入的探索这个问题。
你可能注意到了,在页面底部有一个长的灰条,上面有个 "New Session" (新会话)的链接。这是一个 Seaside 工具条,它在开发时存在;我们将在稍后研究它的一些选项。 "New Session" 链接使得放弃当前会话并从开头启动一个新会话。你现在按一下,只有一件事发生了改变,就是计数器重置为 0, 当然会话 id 也改变了。
第二个参数(标识为 _k) 是一个稍短的,随机字母数字串。这个参数并不代表特定的动作,也不代表会话状态的编码;所有的状态都保存在服务器端,一个 Seaside 会话线性地进行,并不在行为之间跳来跳去。相反地,它保持对当前会话中需求的跟踪,并允许 Seaside 在整个应用中跟踪用户的进行。类似地,页面上每个链接或者表单域都有一个唯一的有序 id: 意味着返回到服务器上时,把每个元素与这数字 id 相关连。这在渲染书签时带来了非智能的甚至是无意义的缺点,但同时也带来了巨大的稳定性。
与命名页面或动作不同, Seaside 应用含有一些“组件”,它是对用户界面的状态和逻辑的模式化。一个会话启动的时候,就创建了一个组件实例,这个组件就进入了一个响应循环:它把自己显示给用户,然后等待输入。输入 (点击链接或者提交表单) 将引发一个组件可响应的方法,并且当方法完成时组件会再一次显示自己。这些触发的方法可能仅仅更新一下应用的或者当前组件的状态,或者它创建其他组件并把控制传递给其他组件,这后一个组件也将进入它自己的响应循环。
在 "counter" 应用中模型化的类是 WACounter, 我们现在就来深入探索一下它。
状态、行为、显示:组件的本质
每个组件有 3 个责任:维护 UI 状态,响应用户的输入,并把自己显示为 HTML 。我们来看一下 WACounter 类是如何来处理这些方面的。
状态
虽然一个应用的大多数状态都保存在商业对象或者数据库中,但是用户界面通常会有自己的状态。这也许包括,例如,一个表单域的当前值,或者要显示的是哪一个数据库记录,或者一个树视图的哪一个节点是展开的,等等。所有这些都保存在组成用户界面的组件的实例变量中。
在 WACounter 中,唯一需要关心的状态就是数字本身。当 WACounter 的实例在会话开始第一次创建时,它初始化其 'count' 实例变量为 0 。当你点击 "++" 和 "--" 链接时,你仅仅是在改变这个实例变量的值。研究这个的一个好途径就是点击状态栏中的 "Toggle Halos" 链接,它会产生一系列的调试图标。点击出现在页面上部的眼形图标,这将会出现一个基于 Web 的 WACounter 组件的检视器。你会看到带有一个数字实例变量的 WACounter 是从其超类 WAComponent 继承而来,你还会在下面看到 'count' 变量,它的当前值也会是你刚刚启动检视器的页面中计数器的值。
当你研究完时,可以关闭检视窗口。要让附加的图标消失,可以再次点击 "Toggle Halos" 。
行为
在计数器应用中有两种可能的用户行为, WACounter 对应每一种都有一个相应的方法来处理。点击 "++" 链接将会导致 #increase 方法的调用,这是一个非常简单的方法 - 下面就是它的全部:
increase
count := count + 1
这恰恰就是你所期待的:它使 'count' 的值增加了 1 。它返回时,响应循环继续,组件再次显示它时,就显示了新的数值。
为增加趣味,让我们来改一下。在眼睛 halo 图标的旁边有个扳手图标 - 点击它会启动一个功能强大pressing this will take you to a very functional web-based System Browser (courtesy of Lukas Renggli), open to the class of the current component. Find the #increase method and change it so that count increases by two instead of by one. Remember to hit Accept. Then close the browser and try the "++" link again.
Display
When Seaside needs to display a component, it sends that component the message #renderContentOn:, passing an instance of WAHtmlRenderer as the argument. This will be a familiar pattern to anyone who has implemented an applet in Java, or a new subclass of Morph in Squeak: the object is given a canvas, which it is then expected to use to display itself. In this case, rather than shapes and lines, the "canvas" knows how to render elements of HTML. The renderer works like a stream: each message to it will append some text or elements to the document being rendered. Here's WACounter's #renderContentOn: method:
renderContentOn: html
html heading: count.
html anchor
callback: [ self increase ];
with: '++'.
html space.
html anchor
callback: [ self decrease ];
with: '--'
Three different messages are sent to the renderer, each of them producing a different kind of HTML element. The first of these, #heading:, produces a simple section heading: if the current value of 'count' were 42, it would append 42 to the document. #space is also very simple, simply appending a non-breaking space character. #anchor is more interesting. It is called, obviously, to produce the "++" and "--" links that appear under the count. And the with: argument is clearly specifying a string to appear in the link. But what do these links point to? Where do they go?
The short answer is, don't worry about it. In Seaside, links don't have destinations, they have callbacks: each time you generate a link or a button, it is associated with a block. When that particular link or button is clicked, the block is evaluated. In this case, clicking on the "++" link will result, as you would expect, in a call to self increase.
Let's modify this method slightly: instead of links for increment and decrement, we'll use submit buttons. Find WACounter>>renderContentOn: in a Squeak browser, and change the code to this:
renderContentOn: html
html form: [
html heading: count.
html submitButton
callback: [ self increase ];
text: '++'.
html space.
html submitButton
callback: [ self decrease ];
text: '--' ]
The main thing we did was to change the calls to #anchor to submitButton. The two methods are used almost identically: it's very easy to switch back and forth between links and buttons as your interface requires. However, submit buttons will only work if they are inside a form. The #form: method is very simple, taking a singe block. This time, it's not a callback; instead, it's used structurally, to enclose the contents of the form. Using #form: this way simply ensures that everything ends up inside a pair of form tags. The same convention is used by the table methods, #table:, #tableRow:, and #tableData:.
The Response Loop
Now that you've been introduced to each of a component's responsibilities, it may be useful to return to how they're all related. Each component operates a response loop, displaying itself, waiting for input, processing the input, and then displaying itself again. In terms of the methods we've seen so far, it looks like this:
- An instance of WACounter is created; 'count' is initialized to 0.
- Seaside calls <tt>#renderContentOn:</tt> on the counter. This produces an HTML document displaying the current value of 'count', as well as some links with associated callbacks. This HTML is sent to the user.
- The user clicks on one of the links, invoking the associated callback, which in turn calls either <tt>#increase</tt> or <tt>#decrease</tt>. This sets 'count' to a new value. The action method then returns.
- The counter is rendered again, with its new state.
Looking Deeper: Interactions Between Components
To transfer control to another component, WAComponent provides the special method #call:. This method takes a component as a parameter, and will immediately begin that component's response loop, displaying it to the user. To test this, we can use the WAFormDialog component class - we'll modify #decrease to print a message if the user tries to go below zero:
decrease
count = 0
ifFalse: [count := count - 1]
ifTrue: [self call: (WAFormDialog new
addMessage: 'Let''s stay away from negatives.'; yourself)]
If 'count' is zero, this creates a new instance of WAFormDialog, gives it an informative message to display, and calls it. Now start a new session and try to hit the "--" link. The counter should disappear, and you should see the dialog instead, the message shown as a large heading.
Displaying a simple dialog like this is common enough that WAComponent provides an #inform: method for it, mimicking the default #inform: provided by Object. Try changing #decrease to use it, like so:
decrease
count = 0
ifFalse: [count := count - 1]
ifTrue: [self inform: 'Let''s stay away from negatives.']
You may notice that along with the message, the dialog has a button labelled
"OK". What happens when you press that? Well, the dialog goes away and the counter is displayed once more. Behind the scenes, the instance of WAFormDialog invokes a companion method to #call: named #answer, which causes control to return back to the calling component. In effect, calling another component is a simple subroutine call: if you like, you can think of #call: as pushing a new component onto the stack, and #answer as popping back to the old one.
In fact, no such stack is maintained. Instead, what #answer does is a little more complicated: it causes the original message send of #call: to return, and the program continues from that point. Since calling the dialog was the last statement of #decrease, all that happens is that decrease returns and the counter's response loop continues.
This is completely non-intuitive, and needs a lot of further explanation. Let me break down the sequence of exactly what happens:
- WACounter>>decrease makes a call to WAComponent>>inform:
- WAComponent>>inform: creates a new instance of WADialog, and passes it into WAComponent>>call:. Let's name this point in the sequence "the call point".
- The WAFormDialog begins its response loop, and displays itself to the user's browser.
- The user clicks the "OK" button. This causes WAFormDialog to invoke WAComponent>>answer.
- Now for the strange part. WAComponent>>answer never returns. That's because it makes a jump: control shifts back to the "call point", just after the send of <tt>#call:</tt>.
- WAComponent>>call: returns to WAComponent>>inform:.
- WAComponent>>inform: returns to WACounter>>decrease.
- WACounter>>decrease returns to the counter's response loop, and it is displayed.
The really cool thing about #call: is this: if a called component provides an argument to #answer:, that argument will be returned from #call:. In other words, calling a component can yield a result. This is much more powerful than simply pushing and popping components from a stack. For example, it makes it easy to implement #confirm:, which displays a question and returns "true" or "false" depending on what the user clicks, to go with #inform:. Try changing #decrease as follows:
decrease
count = 0
ifFalse: [count := count - 1]
ifTrue:
[(self confirm: 'Do you want to go negative?')
ifTrue: [self inform: 'Ok, let''s go negative!'.
count := -100]].
If you play with the counter now, you'll realize that within this one method there can be up to three page views: the confirmation dialog, the message dialog for "Ok, let's go negative", and finally back to the counter itself (for bonus points, try using the back button during this sequence and see what happens). This is a typical structure for Seaside applications: rather than having a series of closely coupled pages, each of which knows which pages come before and after it, each page will collect and return a single piece of information from the user, with the logic stringing them together all maintained in one place. The result can be stunningly resuable pieces of code.
Call and answer isn't the only way Seaside components get reused. You may not realize it, but there have been at least two Seaside components on your screen at all times. One of them is the WACounter - what's the other? The answer is, what you've actually been seeing this whole time is an instance of WACounter embedded inside another component, an instance of WAToolFrame, which renders the toolbar. This sort of embedding is very common in Seaside, and pages are often made up of many individual, nested components. The details of embedding components are beyond the scope of this tutorial, but for a simple example you might want to look at WAMultiCounter (http://localhost:9090/seaside/multi), which contains several independent WACounters. If you do, make sure to hit the "Toggle Halos" link and poke around.
Moving On: Starting Your Own Applications
Hopefully, by now you have some sense of what a Seaside application looks like. To start playing with your own, you may want to look at the configuration app at http://localhost:9090/seaside/config. This makes it easy to add a new, named application, and associate it with a component class of your choice. You'll need the username and password you picked at installation time to get in. If you write a new component and want it to show up as an option in the config app, make sure you implement #canBeRoot on the class side to return true.
Feel free to email comments to the Seaside mailing list: you can sign up at http://lists.squeakfoundation.org/listinfo/seaside. If you are looking for free hosting of Seaside applications check out www.seasidehosting.st.
<iframe style="border: 0px none ; margin: 0px; padding: 0px; overflow: hidden; width: 100%; height: 24px;" id="oakvoc_iframe_title"></iframe>
<iframe style="border: 0px none ; margin: 0px; padding: 0px; overflow: hidden; width: 100%; height: 328px;" id="oakvoc_iframe"></iframe>
