《起跑吧,Opa》 -- 中译本 第五章 使用HTML和CSS来创建UI

来源:互联网 发布:软件之家官网 编辑:程序博客网 时间:2024/06/05 16:52


第五章 使用HTML和CSS来创建UI


本章我们将专注于构建用户接口。在Opa中,构建用户接口的第一步就是创建表示层。

HTML标志
在表示层Opa使用前沿标准HTML5和CSS3来描绘UI。前述章节你已小接触了一下下Opa如何处理HTML,现在开始,扩大接触范围吧。

标签和属性
让我们回想一下早期我们在Opa中编写的HTML代码,还记得吧,我们是在某一个函数中编写的,就像下面这样:

 1   function sample_page() { 2     <header> 3       <h3>HTML in Opa</h3> 4     </header> 5     <article> 6       <div class="container"> 7         <p>Learning by examples.</p> 8       </div> 9     </article>10   }

现在,让我们牢记以下Opa中编写HTML代码的规则:

  • 关闭标签的名称是可以忽略的,如<tag>...</tag>可以被简写为<tag>...</>。

  • 如果属性值中无特殊符号(比如只包含字母、数字和下划线)那么属性值两边的引号是可以忽略不写的。

  • 如果一定要写引号,请写成双引号(如attr="...")而不是单引号(如attr='...')。(译者注:经与Opa开发成员确认,最新的1.2版本的Opa中attr=""和attr=''已经没有分别了)

使用上面前2条规则可以将代码重写成如下格式:

 1   function sample_page() { 2     <header> 3       <h3>HTML in Opa</> 4     </> 5     <article> 6       <div class=container> 7         <p>Learning by examples.</> 8       </> 9     </>10   }


嵌入机制
我们已经编写过了静态HTML代码,那如果需要动态生成HTML代码呢?这时我们会用到Opa中的"嵌入"机制。
如果你碰巧懂点JavaScript(别担心不懂也没事儿),如下类似代码应该不会陌生:
 x + " + " + y + " = " + (x+y);
Opa中我们会采用更加简介优雅的写法,如下:
 "{x} + {y} = {x+y}";
瞧,更简介吧?而且看起来也顺眼多了。
字符串符号""中花括号{…}里面是Opa表达式,Opa将表达式中变量名指向的变量值取出并执行整个表达式最后将将表达式执行结果与其它字符一起转换为字符串。
这种机制简单易懂,而且安全(译者注:JavaScript那种动不动就少写或多写一个引号导致运行错误的惨痛经历有木有?)。
嵌入机制不仅仅可用于字符串,也可用于HTML代码中。因此,你可以像下面那样重写sample_page函数使其具备一些通用性:
 1   function gen_page(header, class, content) { 2     <header> 3       <h3>{header}</h3> 4     </header> 5     <article> 6       <div class={class}> 7         <p>{content}</p> 8       </div> 9     </article>10   }

Opa内部会推断(还记得前面章节提到过的在不显式指定类型时Opa会自动推断类型的特性吧)出该函数具有如下类型:
 function xhtml gen_page(xhtml header, string class, xhtml content) { ... }
在Opa中HTML的类型就是xhtml,不管是HTML的头部还是正文都是这个类型。

现在你可以在调用sample_page 函数的同时给它传递参数了:
 function sample_page() {
   gen_page(<>HTML in Opa</>, "container", <>Learning by examples.</>)
 }
空白标签<>...</>在Opa中用于区分标准字符和HTML字符串,比如,<>string</>是HTML字符串而“string” 指的是标准字符串。

译者注:
上面的调用实例[gen_page(<>HTML in Opa</>, "container", <>Learning by examples.</>)]其实会发生如下类似编译错误:
>Type Conflict
> (6:37-6:45)         string
>  (17:0-17:6)         list(string)
>  
>  The second argument of function gen_page should be of type
>   string
>  instead of
>    list(string)
意思是类型不匹配。Opa期望得到一个list(string)类型而我们传入的是string类型。
书是说错了,那怎么修改呢?
咨询了Opa开发人员后得到的解释如下:
Opa中HTML标签属性的类型跟属性本身有关。比如,onclick属性类型为Function.t(事件类型),id属性类型为string,而class属性的类型是list(string)。
举例来说:
<调用语句>
 gen_page(?)
<被调用语句>
 function gen_page(class) { <div class={class}></div> }
如果使用attr={value}这种形式,value这个变量被直接传递给属性,那么它的类型要求为list类型,如gen_page(["myclass"])。
如果使用attr="{value}"这种形式,value这个变量会被自动转换成半角空格分割的list类型所以不会出错。
关于list类型,最早得到第七章才会接触了。


<>...</>这种空标签的形式用来表示这是一段HTML格式的字符串。

动态生成HTML有神马好处呢?你看哈,你做了一个生成HTML页面的函数,那么在你的应用中随时可以重用该函数啦(一行函数调用代码搞定),而且函数根据传入参数不同,可以动态生成不同HTML页面,这在使用Opa进行UI描绘时是很值得推荐的。

事件处理
现在你知道了如何编写静态HTML以及如何动态生成HTML。你可以根据特定条件(比如:数据库状态)来动态改变生成的网页。本章节中你将学习如何根据用户发生的动作来动态生成网页。

在HTML中你可以通过事件处理器来达到这个目的。你可以把事件处理器看做为了响应某些事件而执行的特定的代码。

一个事件处理器就是在用户界面被某些用户活动触发的一个函数。典型的事件处理器有:用户在页面鼠标点击某个元素(点击事件),用户按下回车键(新行事件),用户移动鼠标(鼠标移动事件),页面加载(页面准备事件)。
在Opa中事件处理器的类型为Dom.event 初始值为空(void)。
通过“Dom.event”关键字你可以在Opa的在线API文档中搜索到更多关于事件处理器的信息。

让我们来研究一下下面的函数:
1   function page() {2     <p onclick={clicked}>Click me!</p>3   }

onclick属性定义了一个针对点击事件的处理器,当用户在页面相应元素上按下鼠标左键时该事件被触发。属性值是一个{clicked} 的嵌入,不过有趣的是这里的嵌入不是基本变量而是一个函数,这意味着,当该事件被触发时有一个函数会被调用。该函数的定义方式如下:
 function void clicked(Dom.event event)  {
 ...
 }
该函数只有一个参数Dom.event,这个参数包含被触发事件的相关特定信息, 该函数没有返回值。

void表示没有值,通常当一个函数不需要返回值时我们就使用void。

每个事件处理器的定义都是相同的:Dom.event 参数和void返回值。

如果处理器不需要从事件参数中获取任何信息,自然在事件处理中不会使用该事件参数(这不奇怪,事件处理并不一定非得获取点什么,有时仅仅是事件被触发就好),Opa编译器会给出一个警告(译者注:类似Unused variable event这样的警告)。这其实蛮有用的,从某种角度来说函数参数没有被使用是应该警告一下,备不住有啥问题发生不是么?

不过若想避免这种让你不爽的警告,倒是有一个方法。你可以将参数名以下划线开始,此时连参数的类型声明也可以省了,如下:
 function clicked(_event) { ... }
或者更加简洁的写法:
 function clicked(_) { ... }

在变量/参数名称前加下划线意味着它们的值将被弃之不用。在事件处理器中此种用法并不新鲜,后续章节“模式匹配”中也会频繁使用这种处理方式。

请注意在事件处理器中使用匿名函数是极好的,如果决定不使用事件参数,那么可以这么写:
1   function page() {2     <p onclick={function(_) { ... }}>Click me!</>3   }

事件处理器中具体实现通常会包括一些DOM处理,接下来我们就会讨论DOM处理。

DOM处理
文档对象模型(DOM) 用于描述HTML文档的树型结构,想要改变HTML文档的内容可通过修改其对应DOM模型即可。

Opa提供了大量针对DOM处理的操作指令,最常用的莫过于改变DOM元素的值了,如下:
 #id = content
上述代码中id是一个DOM元素标志符,想要给之赋予的content是一个XHTML表达式。该行代码通过将一段XHTML表达式内容赋给代表DOM元素的标志符,从而达到改变元素内容的效果。
还有其它两种方式:
 #id += content
 #id =+ content
第一种方式将content内容前置于id元素内容之前,第二种方式将content内容后置于id内容之后。

除此之外也有其它的DOM操作函数,下面列出一些常用的函数:

  • Dom.fresh_id()生成一个新的DOM ID,该ID在本地页面是唯一的。当动态创建HTML组件比如根据数据库内容创建表格时该函数非常有用,注意该函数不是密码安全的(译者注:不是密码安全的意思是生成的密码是具有确定性的意即不是随机的)。

  • string Dom.get_content(dom)获取DOM元素的内容,该DOM元素通常是一个输入元素。

  • void Dom.set_content(string content, dom dom) 设置DOM元素的内容。

  • void Dom.give_focus(dom dom) 将焦点设置到指定的DOM元素。

  • void Dom.show(dom dom) 和 void Dom.hide(dom dom) 表示/隐藏元素。


范例:猜(数字)游戏

为了举例说明事件处理器和DOM处理的用法,我们编写下面的小游戏代码:

1. 计算机在1和10之间选择一个数字x。
2. 用户(在心里)猜测计算机选择的是什么数字。

3. 用户点击页面显示数字看是否与自己猜想的一样。

主函数如下:

1   function page() {2     <h1>Guess what is the number between 1 and 10 I'm thinking of?</h1>3     <div id=#response onclick={show_number}>Click to find out!</div>4   }

最后一行的点击事件处理器中调用了下面的函数:
1   function show_number(_) {2     #response = <>I was thinking of {1 + Random.int(10)}</>3   }

Random.int(x) 是标准库函数,它在0和指定数字之间(包括0但不包括指定数字)随机生成一个数字。因此表达式1 + Random.int(10) 的结果位于1和10之间。
所以,当用户点击页面时看到的数字其实是计算机实时生成的,请注意顺序上用户先想好了一个数字然后点击鼠标然后计算机给出生成的数字,这跟我们理解的“猜数字游戏”的顺序是相反的,当然了至少现阶段计算机不可能读取人类思维,所以呢我们这个示范性的小游戏就是那么个意思,凑合着用吧。
完整函数代码示例如下:
1   function show_number(_) {2     #response = <>I was thinking of {1 + Random.int(10)}</>3   }4   function page() {5     <h1>Guess what is the number between 1 and 10 I'm thinking of?</h1>6     <div id=#response onclick={show_number}>Click to find out!</div>7   }8   Server.start(Server.http, { title: "Guess", ~page })

在正式讨论wiki应用的界面之前,让我们给用户多次选择同时在每次选择错误时给予明显的提示信息(译者注:中国有个游戏很类似,甲想一个数,乙猜,甲根据乙猜的数字和自己所想数字对比然后说大了或小了或正好,然后轮到丙来猜……)。首先修改UI代码如下:
 <h1>Guess what is the number I'm thinking of</>
 <input id=#guess/>
 <span onclick={show}>Check</>
 <div id=#message/>
将#response 改为 #message以便显示提示信息,添加一个输入域以便用户输入猜想的数字。在前例中数字只能猜一次,现在我们让猜测可以继续好多次。 为了实现这个功能在函数开头你需要创建一个“秘密”值(译者注:即正确数字):
 secret = 1 + Random.int(10);
接着你需要修改show函数以计算出正确的提示信息:
 message =
   if (guess==secret) { <span class="success">Congrats! < /span> }
   else if (guess<secret) { <>More than this</> }
   else { <>Less than this</> };
注意我们加入了"喝彩"功能,当用户输入正确数字时显示特殊的"成功"文字。
将"秘密"值作为第一个参数传递给show函数:
1   function show(secret, _) {2     ...3   }4   function page() {5     ...6     <span onclick={show(secret, _)}>Check</>7     ...8   }

最后,你需要在show函数中读取用户输入的数字并且显示对应的提示信息,代码类似下面这样:
1   function show(secret, _) {2     guess = Dom.get_value(#guess);3     message = ...4     #message = message;5   }

而实际上编译时会报类型错误,这是因为用户输入的是字符串而而期望的是一个数字,可以通过转型来解决这个问题,如下:
 guess = String.to_int(Dom.get_value(#guess));
以下是全部完整代码(译者注:原文此处完全写错了,写成了上一段代码的拷贝版,下面是译者整合的代码):
 1   Server.start(Server.http, { title: "Guess", ~page }) 2  3   function page() { 4     secret = 1 + Random.int(10) 5     <h1>Guess what is the number I'm thinking of</> 6     <input id=#guess/> 7     <span onclick={show(secret, _)}>Check</> 8     <div id=#message/> 9   }10 11   function show(secret, _) {12     guess = String.to_int(Dom.get_value(#guess));13     message =14       if (guess==secret) { <span class="success">Congrats! < /span> }15       else if (guess<secret) { <>More than this</> }16       else { <>Less than this</> };17   18     #message = message;19   }


wiki界面(HTML)
现在我们将正式编写wiki应用的用户界面部分,本节中你会编写一个display函数,该函数拥有一个topic参数,该函数将实现构造页面的功能。我们假设你还记得下列我们在第四章编写的数据存储函数:
• load(topic)通过wiki主题取得wiki内容
• save(topic, source) 存储wiki主题和wiki内容
页面设计思路是页面有两种模式:
• 编辑模式,用户可以编辑页面内容 (使用Markdown语法)
• 显示模式,页面只显示wiki信息但不能编辑
本例中页面将拥有这两种模式,但同一时刻只有一种模式被呈现给用户而另外一种模式会被隐藏。

display函数代码如下:
 1   function display(topic) { 2     content = render(load_data(topic)); 3     xhtml = 4       <header> 5         <h3>OpaWiki</h3> 6       </header> 7       <article> 8         <h1>About {topic}</h1> 9         <div id=show_container>10           <small><strong>Tip:</strong> Double-click on the content to start editing it.</small>11           <section id=content_show ondblclick={function(_) { edit(topic) }}>{content}</section>12         </div>13         <div id=edit_container hidden>14           <small><strong>Tip:</strong> Click outside of the content box to save changes.</small>15           <textarea id=content_edit rows=30 onblur={function(_) { save(topic) }}/>16         </div>17        </article>;18     Resource.page("About {topic}", xhtml);19   }

首先我们通过调用load_data(topic)函数取得wiki主题对应的wiki内容,并将该内容(load_data函数返回值)作为参数传给render函数,后者可将wiki内容中的Markdown语法转换成HTML呈现。

Markdown
我们需要写一个渲染函数,该函数将Markdown代码渲染成能够被浏览器解析的HTML代码。

Markdown是一门轻量级的标识语言,适用于富格式用户输入。 通过其官方项目网站http://dillinger.io你可以学习更多的语法。

跟其它强大的项目一样,Opa提供一个易于使用的库来处理Markdown。
Opa也提供了Markdown语法的处理类库,只需要添加一行导入语句,就可以开始处理Markdown语法了。
 import stdlib.tools.markdown
后续章节我们会学习更多关于导入和包的知识,现在,只需知道,这么干之后就可以使用Markdown语法了,范例如下:
 function xhtml xhtml_of_string(Markdown.options options, string source)
通过传入参数(Markdown代码和渲染选项)我们可以得到处理过的HTML代码。如果不传入渲染选项也可以使用默认的渲染选项,如下:
 function render(markdown) {
   Markdown.xhtml_of_string(Markdown.default_options, markdown);
 }

动态更新页面
让我们来看看编辑(edit)和保存(save)函数。

调用编辑函数以将页面模式从表示切换到编辑:
1   function edit(topic) {2     Dom.set_value(#content_edit, load_data(topic));3     Dom.hide(#show_container);4     Dom.show(#edit_container);5     Dom.give_focus(#content_edit);6   }

第一行,我们通过load_data函数将topic对应的Markdown代码取出,赋值给编辑框,接下来我们隐藏了表示框显示编辑框,最后将焦点移动到编辑框。

类似滴,保存(save)函数将页面模式从编辑切换到表示并且保存用户所做的数据变更:
1   function save(topic) {2     content = Dom.get_value(#content_edit);3     save_data(topic, content);4     #content_show = render(content);5     Dom.hide(#edit_container);6     Dom.show(#show_container);7   }

首先我们从编辑框取出用户输入的Markdown代码,然后将之保存,然后通过render函数将其进行渲染并赋值给表示框,最后隐藏编辑框显示表示框。

译者注:此处WIKI页面的代码较多,为了能有个感性认识,整理可直接运行的代码如下:
 1   import stdlib.tools.markdown 2  3   database wiki { 4     map(string, string) /page 5     /page[_] = "This page is empty. Double-click to edit." 6   } 7  8   Server.start(Server.http, 9     {10       title: "hello",11       page: function() {12         display("How to learn Opa language quickly?")13       }14     }15   )16 17   function display(topic) {18     content = render(load_data(topic));19     <header>20       <h3>OpaWiki</h3>21     </header>22     <article>23       <h1>About {topic}</h1>24       <div id=show_container>25         <small><strong>Tip:</strong> Double-click on the content to start editing it.</small>26         <section id=content_show ondblclick={function(_) { edit(topic) }}>{content}</section>27       </div>28       <div id=edit_container hidden>29         <small><strong>Tip:</strong> Click outside of the content box to save changes.</small>30         <textarea id=content_edit rows=30 onblur={function(_) { save(topic) }}/>31       </div>32     </article>;33   }34 35   function save_data(topic, source) {36     /wiki/page[topic] <- source;37   }38 39   function load_data(topic) {40     /wiki/page[topic];41   }42 43   function render(markdown) {44     Markdown.xhtml_of_string(Markdown.default_options, markdown);45   }46 47   function edit(topic) {48     Dom.set_value(#content_edit, load_data(topic));49     Dom.hide(#show_container);50     Dom.show(#edit_container);51     Dom.give_focus(#content_edit);52   }53 54   function save(topic) {55     content = Dom.get_value(#content_edit);56     save_data(topic, content);57     #content_show = render(content);58     Dom.hide(#edit_container);59     Dom.show(#show_container);60   }


添加样式(CSS)
CSS是HTML的好伙伴,其全称是层叠样式表(Cascading Style Sheets)。既然HTML用来描述web页面的内容和结构,CSS则专注于表现层面的语义(如:表现形式和格式化处理)。CSS相关知识可以帮助你美化页面表现,虽然后续会介绍一种无需手动添加CSS代码即可达到页面美化效果的称之为Bootstrap的技术,掌握基本的CSS用法依然是我们竭力建议的。

在Opa中可以有三种方式操作CSS:

  • 使用常用的style属性

  • 使用Opa的CSS数据类型和特殊语法

  • 使用外部样式表单

除了讨论这三种CSS处理方式外,本节我们还会讨论如何给咱们的wiki应用添加样式。

精确样式属性
下面介绍Opa中第一种处理CSS的方法,使用通用的style属性,如下:

1   function page() {2     <p style="color: white; background: blue; padding: 10px;">Click me</>3   }

虽然该方法可用,但Opa并不推荐。首先,CSS的目的是为了分离数据和数据表示,其次,真的真的建议在独立于HTML之外创建完整的外部CSS文件。创建单独的CSS文件的方法后续会介绍。

不过,有时候根据业务功能需要动态处理CSS,下面让我们来看看如何处理这种情况。

强大的Opa样式
Opa针对HTML提供了数据类型和特殊的语法,同样,针对CSS也有一套,将普通的CSS语法用大括号{}括起来,比如:
1   function page() {2     style = css { color: white; background: blue; padding: 10px;}3     <p style={style}>Click me</>4   }


也可以这么写:
1 function page() {2<p style={css { color: yellow; background: blue; padding: 10px;}}>Click me</>3}
本书编写之时Opa中style属性使用CSS的方式只支持CSS3的子集,如果你写的CSS没错但就是编译不成功,哦,那说明你用的那部分CSS尚未被支持,这种情况下请选择其它另外CSS使用方式。
这种在Opa代码内部嵌入CSS的用法还有其它的好处,其中之一就是这种情况下CSS被当做一种数据类型然后可以被作为参数进行传递:
1   function paragraph(style, content) {2     <p style={style}>{content}</>3   }

该函数中参数类型是什么呢?我们称之为CSS属性类型,定义如下:
 function xhtml paragraph(css_properties style, xhtml content)
另一个好处是我们甚至可以参数化单个CSS属性(而不像上面那样将所有的CSS属性全部传递),如下:
1   function xhtml block(Css.size width, Css.size height, xhtml content) {2     style = css { width: {width}; height: {height} }3     <div style={style}>{content}</>4   }

看吧,我们可以使用Opa提供的Css.size类型传递单个的CSS定义。
类似滴,字体,颜色,背景色诸如此类的单个CSS属性定义也可单独使用。

外部CSS
在第三章你学习到了如何在Opa中嵌入资源到Opa的服务器中,这些资源其实可以被有规则滴组织在外部CSS文件中,在Opa中引用这个外部CSS文件即可达到效果。
1   Server.start(Server.http,2     [ {resources: @static_resource_directory("resources")},3       {register: {css:["/resources/css/style.css"]} },4       ...5     ]6   )

register字段的css属性包含外部可用的CSS清单的URL地址,一旦被注册,这些外部CSS文件可以被应用中所有页面使用。当然,也可以使用Resource.styled_page函数导入仅供单个页面使用 的样式,如下:
1   function page_with_style(body) {2     Resource.styled_page("This is a page with style",3     ["resources/custom_style.css"], body)4   }


给wiki应用加上样式
对于我们的wiki应用来说,只需简单地导resources/style.css文件即可:
1   Server.start(Server.http,2     [ {resources: @static_include_directory("resources")},3       {register: [{doctype: {html5}}, {css: ["/resources/style.css"]}]},4       {dispatch: start}5     ]6   );

好了截止此刻,你应该拥有了一个完整的可运行的wiki应用了,编译之:
 $ opa wiki.opa
运行之:
 $ ./wiki.js
 Http serving on http://thistle:8080

喏,服务器启动了。打开浏览器输入地址http://localhost:8080你将看到如下界面(其实本节示例代码没有给完整,是那么个意思吧):


图 5-1

Bootstrap: 好看易用的样式
一般来说web应用的样式由专业的web设计师来做,但在一些小项目或项目的早期阶段,不会引入专业的web设计师,此时,你可以使用一个称之为Bootstrap的前端框架来方便地进行样式设计工作。

Bootstrap是Twitter提供的一个开源项目,旨在为网站的Javascript和HTML提供高质量可响应的CSS设计。使用预定义库和适当的标记组合,开发人员可以获得专业的外观设计并且这是免费的哦。详细介绍Bootstrap超出了本书的范围,不过其官方网站(译者注:中文官网在此http://www.bootcss.com/)是绝好的学习来源。Opa开发者强烈推荐使用Bootstrap。

在Opa中导入Bootstrap很简单,如下一步搞定:
 import stdlib.themes.bootstrap
这将导入Opa所能支持的最高版本的Bootstrap版本,要想指定Bootstrap版本可如下导入:
 import stdlib.themes.bootstrap.v2.1.0
现在你就可以在Opa中毫无困难地使用Bootstrap了。
下面让我们通过在wiki应用中使用Bootstrap来举例说明其强大性。
只需要将display函数中的xhtml替换成下面的Bootstrap风格代码即可:
 1   function display(topic) { 2     content = render(load_data(topic)); 3     <div class="navbar navbar-fixed-top"> 4       <div class=navbar-inner> 5         <div class=container> 6           <a class=brand href="#">Opa Wiki</a> 7         </div> 8       </div> 9     </div>10     <div class=container>11       <h1>About {topic}</>12       <div id=show_container>13         <span class="badge badge-info">Tip</span>14         <small>Double-click on the content to start editing it. </small>15         <div class="well well-small" id=content_show ondblclick={function(_) { edit(topic) }}> {content}</div>16       </div>17       <div id=edit_container hidden>18         <span class="badge badge-info">Tip</span>19         <small>Click outside of the content box to confirm the changes.</small>20         <textarea id=content_edit rows=30 onblur={function(_) { save(topic) }} />21       </div>22     </div>23   }

上面的代码隐藏了编辑容器,在页面元素间添加了一点点空隙,并且让编辑框充满你的屏幕,效果如下:

图5-2

更多内容请关注博客专栏:
http://blog.csdn.net/qq_27056755 

0 0