原文:Realtime Web Apps
协议:CC BY-NC-SA 4.0
六、设计应用
因为这是一本关于开发而不是设计的书,所以这一章会很短。它讨论了一些 web 应用特有的设计注意事项。此外,我们将谈一谈如何确保在 Photoshop 中制作的设计能够很好地翻译到 web 上。
注意本章假设你已经接触过 Adobe Photoshop,对它的界面相当熟悉,而且——也许是最重要的——你关心设计。您将能够在另一个程序(如 GIMP)中创建相同的作品,但以下步骤不会正确匹配。
警告如果你是一名铁杆开发者,正考虑跳过这一章,那么除了带你浏览应用的设计,它还提供了一些调整 Photoshop 设置的技巧,以确保编码后的设计看起来像 Photoshop 中的布局。如果您在团队中工作并做任何前端工作,当您的创意团队想知道为什么字体看起来略有不同或其他微小的布局不一致时,这可能会省去您一些头痛。
设定设计目标
对于任何设计项目来说,从明确的目标开始是很重要的。这有助于防止设计偏离轨道或与应用的意图相冲突。
因为这种设计是针对需要在标准 web 浏览器和手持设备上工作的应用,所以这种设计的目标如下:
- 在绝对必要的情况下需要尽可能少的图像
- 保持设计简洁,只关注所需的内容
- 使用大型用户界面(UI)元素,使应用易于在触摸屏上使用
如果设计坚持这三个目标,那么从桌面到移动的过渡将会很容易,使用简单,导航也非常容易。
定义调色板
接下来,你需要为应用选择一个调色板。这完全是一个主观的决定,但是所选择的颜色应该符合一定的准则:
- 背景颜色和文本颜色应该有很高的对比度,以确保易于阅读
- 从易读性的角度来看,较亮的背景对眼睛来说更容易一些
- 包含强调色来吸引对重要元素(如链接和按钮)的注意通常是个好主意
对于这个应用,让我们使用简单的地球色调加上明亮的橙色强调色(见图 6-1 )。保持中性色可以让眼睛看起来更舒服,防止颜色冲突,这会让网站上的文字更难阅读。本书中不会正确显示强调色,因为它是灰度的。要正确查看强调色,请将 Photoshop 中的拾色器设置为图中所示的十六进制值。
图 6-1。为应用选择的调色板浅色将用于背景,深色用于文本
选择字体
最近对@font-face
的广泛支持为设计师们打开了一个全新的可能性世界。基于网络的应用不再局限于一小部分网络安全字体;相反,交互设计师现在只受到他们想象力的限制(还有 OFL 许可 1 )。
利用你新获得的自由,你现在需要为应用选择字体。像调色板一样,这完全是一个主观的决定,但是有一些通用的指导原则可以帮助选择稳定的字体:
- 标题可以使用比网站上其他内容更有趣的字体。
- 正文应该使用简单易读的字体,以确保阅读起来不难。
- 设计一致性很重要。不要过分使用不同的字体;坚持一个或两个一起工作很好的。
- 如果选择的字体非常独特,文档中使用的其他字体应该更加端庄,以防止冲突。
- 额外提示:有疑问时,使用 Helvetica。 2
为了创造视觉趣味,我们应用中的标题将采用古铜色黑色, 3 ,这是一种非常独特的字体,在广告的广告狂人时代大量使用。它经典而大胆,看起来不傻,这使得它非常适合大字体。
由于库珀·布莱克的圆形衬线,紧凑的字距(字符之间的间距)增加了视觉趣味。通过将字母挤在一起,我们获得了简洁的视觉效果。
相比之下,该网站的其余文字将设置为新闻哥特式 4 ,就字体而言,这明显不那么直言不讳。它的线条简洁有力,非常易读——即使是小字体——但不会像库珀·布莱克那样引人注目。
为了提高可读性,字距应该向外调整一点,让字符“呼吸”它还与标题字体形成了很好的对比。
这两种字体很好地相互补充,将形成一个有凝聚力的演示文稿,而不会与互联网上其他使用传统 web 安全字体(如 Arial 和 Georgia)的网站融为一体。
当我们将它们放在一个样本中时,最终的字体选择看起来非常好(见图 6-2 )。
图 6-2。所选字体的样本
字体渲染:PHOTOSHOP vs . @ FONT-FACE
没有网络字体经验的设计师经常抱怨说,他们看起来和在 Photoshop 中不一样了。发生这种情况是因为 Photoshop 使用的抗锯齿与大多数 web 浏览器不同,这意味着字体的渲染会稍有不同。大多数人不会注意到这种差异,但这种差异足以让一个刚刚花了一周时间把自己的设计做得恰到好处的创意型人才感到心痛(见图 6-3 )。
图 6-3。在浏览器(上图)和 Photoshop(下图)中渲染的新闻哥特风格
幸运的是,Photoshop 提供了更改布局中字体抗锯齿的选项。其中一个选项——强——与大多数浏览器使用的抗锯齿非常相似,它消除了 Photoshop 布局与其在 Web 上的外观之间的明显差异(参见图 6-4 )。
图 6-4。使用强抗锯齿类型,字体匹配更紧密
要更改 Photoshop 中的抗锯齿模式,请选择文本图层(或图层;这可以批量更改而不影响其他设置)并打开“字符”面板。在右下角,有一个下拉菜单,可能默认选择了平滑。改成 Strong 就万事俱备了(见图 6-5 )。
图 6-5。在 Photoshop 中更改文字的抗锯齿
设计常见的站点元素
准备好你的颜色和字体,你就可以开始设计了。在 Photoshop 中,创建一个宽1024px
高840px
的新文档。开始设置背景为浅色,#FBF7E7
。
创建标题
设计的第一个元素是标题,它将与设计的主体分开,以深色#1F1B0C
作为背景色。
-
1.对于页眉,选择矩形工具,画一个宽
1024px
高240px
的矩形。将其与文档的顶部和左侧对齐。 -
2.接下来,选择水平文字工具,并在页面上绘制一个包含主要标题的文本区域。打开字符面板,进行如下设置:
-
字体:古柏黑
-
尺寸:110 磅
-
跟踪:-80
-
颜色:#fbf 7 和 7
-
抗锯齿:强
-
3.添加应用的标题,实时 Q & A ,并将其置于标题中央。
-
4.要添加副标题,请使用水平文字工具在标题下绘制另一个文本区域。在“字符”面板中,将设置更改为以下内容:
-
字体:新闻哥特式光
-
尺寸:18 磅
-
跟踪:100
-
颜色:#fbf 7 和 7
-
抗锯齿:强
-
5.添加副标题,课堂、演示和会议的实时反馈系统。
保存到目前为止的工作;你现在已经得到了应用的标题(见图 6-6 )。
图 6-6。设计割台的工作进展
创建页脚
接下来,让我们添加网站页脚。再画一个1024px
宽、50px
高的矩形(就像你为标题画的一样),并将其与底部和左侧对齐;这将是页脚的背景。
-
1.使用水平文字工具,以页脚背景为中心绘制一个文本区域;然后将“字符”面板中的设置更改为以下内容:
-
字体:新闻哥特式光
-
尺寸:13 磅
-
跟踪:25
-
颜色:#fbf 7 和 7
-
抗锯齿:强
-
2.添加版权信息, 2013 杰森·伦斯托夫&菲尔·莱格特;然后按下
Tab
并选择右对齐选项。制表符允许版权信息左对齐,而制表符后的文本右对齐。现在添加其余的页脚文本:的一部分实时 Web 应用:用 HTML5 WebSocket,PHP,和 jQuery 。获取图书|源代码(在 GitHub 上)。
*** 3.对于链接 Get the Book and Source Code(在 GitHub 上),依次选择每一个并将字体设置为 News Gothic Medium,打开下划线,将颜色设置为你的强调色,#E06F00
。**
**再次保存;页脚准备就绪(参见图 6-7 )。
图 6-7。应用的页脚
表单元素
现在您已经有了应用的基本包装,您可以开始整合用户界面的外观和感觉了。因为这个应用完全是关于交互的,它严重依赖于表单在与会者和演示者之间发送信息。
因为这个应用将依赖于它的表单元素来获得大部分美感,所以让我们从设计输入和按钮样式开始,我们可以在应用的所有视图上使用它们。
文本和电子邮件输入
文本输入将用于这个应用上几乎所有的交互,所以它们是一个逻辑起点。我倾向于将所有元素保存在 Photoshop 的不同图层上,然后将它们分组到文件夹中。随意使用任何适合你的组织方法。
-
1. Playing off the roundness of our headline font, grab the rounded rectangle tool and set the border radius to
6px
. Draw a rectangle430px
wide by40px
tall and make it white (#FFFFFF
). This will serve as the base for the input (see Figure 6-8).图 6-8。带 6px 圆角的白色矩形
-
2. Next, we need to make it look a little more like an input. To start, let’s give it a border. Bring up the Layer Style panel by clicking the Layer Style (fx) button at the bottom of your layers panel while the rectangle layer is active (see Figure 6-9).
图 6-9。Photoshop 中的图层样式按钮
-
3. After the Layer Style button is clicked, several options will be listed. Click Stroke and the Effects dialog will open.
提示隐藏目标路径——当你选择图层时 Photoshop 显示的矩形轮廓(见图 6-9 的例子)——以便更好地了解你编辑效果时的样子。要隐藏,按下
Command + Shift + H
或点击查看显示并取消选中目标路径。 -
4. In the Effects dialog, give the input a 2px stroke and place it outside the shape. Use the dark color for this. The rest of the settings should remain at their default values (see Figure 6-10).
图 6-10。添加了笔画的输入,加上所有的设置
-
5. Next, add an inner shadow to the input, which will give it some dimension. Click the Inner Shadow check box below Stroke in the Layer Style panel, which brings up new settings (if it doesn’t, click the actual label next to the check box). Set the shadow color to dark (#1F1B0C), the angle to 135º, distance to 2px, and the size to 14px. Change the blending mode to Normal as well, and drop the opacity to 30%, which gives you an input-looking rectangle (see Figure 6-11).
图 6-11。对输入矩形应用阴影的设置
提示将混合模式设置为正常可以确保阴影很好地转换为 HTML/CSS。虽然混合模式正在设计中 5 (希望很快),但 CSS3 目前还不支持,因此应该暂时避免在网页布局中使用。
提示使用 135 的阴影,这样在 CSS 中 X 和 Y 的偏移量是相同的。这通常看起来很好,并防止 Photoshop 中的阴影和 Web 上的阴影不一致。
-
6.最后,输入需要标签的样式,这将让用户知道她应该在字段中输入什么。这将是输入右上角的一个小文本字段,设置如下:
-
字体:新闻哥特式粗体
-
跟踪:25
-
颜色:#1F1B0C
-
抗锯齿:强
-
7. Use the text Tell us your name (so attendees know who you are). Align it with the input, and your input is complete (see Figure 6-12).
图 6-12。输入的标签完成了样式化
提交按钮
如果表单不能被提交,那么它们对我们没有太大的帮助,所以我们需要一个提交按钮来配合输入。
-
1.就像文字输入一样,使用圆角矩形工具,边框半径设置为
6px
。创建一个宽310px
高54px
的盒子,设置为深色(#1F1B0C
)。这将是你的按钮的基础。 -
2.接下来,使用具有以下设置的文本工具:
-
字体:古柏黑
-
尺寸:30 磅
-
跟踪:-50
-
颜色:#fbf 7 和 7
-
抗锯齿:强
-
3.使用文本创建您的房间并将文本置于提交按钮的中央。
-
4. To give it a little dimension, open the Layer Style panel and add a drop shadow using the dark color (
#1F1B0C
) at 30% opacity, angled at 135º, and set at a size of10px
(see Figure 6-13).图 6-13。带有细微阴影的提交按钮
设计主视图
现在,网站的基本元素已经设计好了,您可以开始将它们组装到主页设计中。
此页面为用户提供了两个选项:他们可以作为演示者创建文件室或加入现有文件室。为了简单起见,除了页眉和页脚,这两个选项应该是页面上仅有的内容。
创建房间表单
首先放置创建创建房间表单所需的输入,它允许即将成为演示者的人为 Q & A:
- 名字
- 电子邮件
- 会话名称
因为这一页上有两个表单,所以只使用左半部分的可用空间。用你的标签描述得更详细一点,向用户解释为什么她需要提供这些信息。不要只是列出他们接受的数据,而是使用以下听起来更人性化的标签:
- 告诉我们您的姓名(以便与会者知道您是谁)。
- 告诉我们您的电子邮件地址(以便与会者可以与您联系)。
- 你的疗程叫什么?
在这些输入的下面,添加一个带有文本 Create Your Room 的 submit 按钮(在上一节中您已经准备好了)。这就完成了想要创建新房间的演示者的表格(参见图 6-14 )。
图 6-14。主页视图上演示者的表单
但是,这个表单仍然缺少一些东西。让我们添加一个标题和一段描述性文字,以便清楚地说明这个表格的用途。首先在表单上方添加一个标题,设置如下:
- 字体:古柏黑
- 尺寸:48 磅
- 跟踪:-50
- 颜色:#1F1B0C
- 抗锯齿:强
简单的用文字呈现?对于标题,它向用户提出了一个清晰的问题,应该很快引导他到正确的形式。通过添加和提交按钮相同的投影来增加一点尺寸:颜色#1F1B0C,透明度 30%,135,大小 10px。
提示在图层面板中要复制样式的图层上右键单击(或按住 Control 键并单击),可以快速轻松地将图层样式复制到其他元素;然后从上下文菜单中选择复制图层样式。复制完成后,右击你想要应用样式的图层;然后从上下文菜单中选择粘贴图层样式,样式将被应用。当挑剔的客户希望看到设计中每个元素的多种变化时,这将为您节省时间。
接下来,在标题下添加一段文字,内容为:创建一个房间开始问答环节。使用以下设置:
- 字体:新闻哥特式罗马
- 尺寸:24 磅
- 跟踪:25
- 颜色:#1F1B0C
将标题居中并复制到表格上方,现在你应该有一个创建新房间的完成表格(见图 6-15 )。
图 6-15。完成家庭视图的创建房间表单
加入房间表单
为了保持设计的一致性,加入一个房间的形式将在风格上与创建一个房间的形式相同;只有输入和复制会改变。
为了最大限度地降低与会者的准入门槛,加入房间所需的唯一信息是房间号。
- 通过将 create-a-room 表单复制到页面的右半部分,然后删除三个输入中的两个,快速启动该表单。
- 把标题改成参加?上面的内容是“使用 ID 加入房间”。
- 输入的标签应该变成,房间的 ID 是什么?
- 最后,提交按钮的文本应该是“加入这个房间”。
- 完成这些修改后,主页视图应该包含两个表单(见图 6-16 )。
图 6-16。已完成的首页视图
提示现在你已经完成了主视图,将其保存为图层构图,在此状态下创建文档的快照(参见图 6-17 )。这允许您隐藏所有主视图层,并开始在问题视图上工作,而不会丢失任何布局。如果你曾经不得不保存一个 PSD 的多个版本,或者因为覆盖了部分布局而丢失了半天的工作,layer comps 将会是你新的最好的朋友。
图 6-17。图层复合面板允许在设计的不同状态之间快速切换,消除了对多个文件或大量重复图层的需求
设计房间视图
房间视图借用了主视图中的一些元素,这将稍微简化这个过程,但它也有多个状态:一个版本供与会者使用,一个版本供演示者使用,一个“关闭的”房间外观供演示者结束其会话后使用。
设计与会者视图
与会者视图有三个不同的部分:
- 提问的形式
- 房间信息(他们在哪里,谁负责)
- 这些问题
为了组织这些信息,您将使用两栏布局,其中左栏更大,并突出重要信息(其中 important 表示最直接有用;在这种情况下,提问表单和问题本身),右栏将包含不太重要的信息—会议和发言人的名称,与会者可能在加入之前就知道—并且会更小。
提问式表单将位于主栏的顶部,不会偏离标准的表单元素设计。
房间信息将遵循库珀黑色标题和新闻哥特式正文的标准;唯一不同的是,它会更小,标题使用 30 磅文本,正文使用 18 磅文本。
这些问题不同于你目前设计的任何问题,所以它们需要更多的思考。需要显示的信息如下:
- 问题本身
- 该问题获得的投票数
- 允许与会者为该问题投票的按钮
问题将使用 News Gothic 以 24 磅显示,这对于在任何屏幕尺寸上阅读都是一个很好的尺寸,即使设备稍微远一点,例如放在桌子上的电话。文本将位于问题布局的右侧。
计票将在库珀黑色 24 点,将坐在最左边的问题。
在计数和问题之间有一个大按钮,用来对问题进行投票表决。用椭圆工具画一个直径为 60px 的圆,颜色为深色,# 1F1B0C 然后使用自定义形状工具在其中心绘制一个浅色箭头#FBF7E7。给黑色圆圈添加和提交按钮一样的阴影,现在你就有了一个投票按钮。
在第一个问题下面添加第二个问题,以确保设计可以处理多个问题。问题之间的微妙分割线——一个 2px 乘 500px 的深色矩形,填充不透明度为 10%,带有来自提交按钮的阴影——完成了与会者页面视图(参见图 6-18 )。
图 6-18。与会者的房间视图
设计封闭房间视图
封闭房间视图与标准与会者视图非常相似,但有两个明显的例外:
- 问问题的形式已经被一个关于房间被关闭的通知所取代。
- 问题已经被淡化为 60%的不透明度(投票按钮被淡化为 15%的不透明度),以使这个房间在视觉上明显是关闭的。
除了这两个变化,封闭的房间视图几乎没有变化(见图 6-19 )。
图 6-19。封闭房间视图
设计演示者视图
最后,该应用需要一个演示者视图。这与与会者视图非常相似,只是有一些例外:
- 提问形式被删除了,因为演示者不需要问自己问题。
- 房间的链接会显示出来,因此演示者可以轻松地与她选择的任何人共享房间。
- 有一个关闭房间的按钮。
- 投票按钮被移除,取而代之的是一个标记问题已回答的按钮,尽管是在问题的右边而不是左边。
房间链接和关闭按钮都使用了我们已经设计的标准表单元素,但是回答按钮需要一些新的设计:
-
使用圆角矩形工具,用深色#1F1B0C 画一个 72px 的正方形。接下来,按住 command 键单击图层面板中的形状,这将在形状周围绘制一个选取框选区。
-
选择矩形选框工具并按住 Alt 然后单击并拖动选框的左侧,从上到下重叠 12px。当您释放单击时,选取框现在应该是 60px 宽 72px 高,矩形的左侧未被选中。
-
With the marquee still on the shape, make sure the shape layer is selected in the Layers panel; then click the Add layer mask button at the bottom of the Layers panel (it is next to the Layer Styles button) to create a mask. This gives the shape the appearance of having the upper- and lower-right corners rounded, but the left side squared (see Figure 6-20).
图 6-20。从左到右,圆角矩形,矩形周围的选取框选择,从左侧取消选择的 12px,以及最终的遮罩形状
-
对此形状应用与提交按钮相同的阴影;然后使用选中复选框的自定义形状工具,以浅色#FBF7E7 绘制一个复选框,以深色形状为中心。这给了你一个回答按钮。
-
Position the room link and end this session buttons in the right-hand column, and the presenter view is good to go (see Figure 6-21).
图 6-21。演示者视图
更小的屏幕布局(以及为什么不在这里设计它们)
在上一章中,我们花了很多时间讨论一个移动友好的 web 应用相对于一套本地应用的优势。那么为什么现在不设计手机版呢?
主要原因是移动设计本质上太不稳定,不适合传统设计。这需要更多的动手操作,尝试看看的方法,这在 Photoshop 中会花费很长时间。 6
由于这种特殊布局的简单性,另一个不做手机专用布局的原因是为了节省时间。如果在 CSS 中进行修改很容易,那么最好把时间花在编码上。不需要加倍努力。
摘要
在本章中,您应用了前几章的所有规划,并为应用创建了一个设计。因为你是有组织的,你提前考虑过,你的 PSD 包含了可以用 CSS 非常接近地复制的样式和字体。
在下一章——终于——你将开始编写这个应用。更具体地说,您将构建前端,包括高级 CSS,使这种设计适应任何屏幕大小。
1
2
3
T2T4http://www.fontpalace.com/font-details/News+Gothic+BT/
5
6Adobe 似乎也知道这一点,因为他们正在开发一种新工具来解决这个问题:http://html.adobe.com/edge/reflow/
。**
七、创建 HTML 和 CSS 标记
现在设计已经准备好了,您可以开始编码了。在这一章中,你将把你创建的 PSD 转换成一个 web 可用的 HTML 和 CSS 布局。您还将实现 CSS3 媒体查询,以确保布局在平板电脑和手持设备大小的屏幕上看起来不错。
从基础开始:设置 HTML5 文档
在写一行代码之前,让我们提醒自己正在创建的设计(见图 7-1 )。
图 7-1。应用外观的提示
现在,您可以在项目的根文件夹中创建一个新的 HTML 文档。将其命名为index.html,并插入doctype
和其他必需的元素:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Realtime Q&A</title>
</head>
<body>
</body>
</html>
准备好字体
接下来,你需要设计的字体。因为 Cooper Black 和 News Gothic 不是 Tahoma 和 Georgia 等传统的 web 安全字体,所以您需要使用 CSS 的@font-face
功能来加载它们。
然而,Cooper Black 和 News Gothic 并没有在开放字体许可下发布, 1 ,这意味着简单地在网站上使用@font-face
字体是不合法的。但幸运的是,由于 Fonts.com 等公司的出现,这不是一个问题,这些公司(收取合理的费用)将处理字体嵌入的法律问题,让我们继续进行设计。
对于这个应用,在 Fonts.com 创建一个免费帐户,在“管理 Web 字体”部分创建一个新项目,并将 Cooper Black Regular 和 News Gothic No. 2 系列字体(搜索“News Gothic”)添加到一个项目中(参见图 7-2 )。将您的开发域(即使是本地的)添加到项目中;那就发表吧。使用 JavaScript 嵌入选项,因为这是免费帐户唯一可以使用的选项。
图 7-2。Fonts.com 项目详情
将新的<script>
标签添加到<head>
部分的index.html
中:
<head>
<meta charset="utf-8" />
<title>Realtime Q&A</title>
<!--Fonts via fonts.com-->
<script type="text/javascript" src="[`fast.fonts.com/jsapi/a09d5d16-57fd-447d-a0f6-73443033d2de.js"></script`](http://fast.fonts.com/jsapi/a09d5d16-57fd-447d-a0f6-73443033d2de.js"></script)>
</head>
接下来,在应用的根目录下创建一个名为assets
的子目录,并在其中创建另一个名为styles
的子目录。在里面,创建一个名为main.css
的新样式表。
在main.css
里面,添加一个注释块,上面有来自 Fonts.com 的font-family
规则,以备后用:
/**
* Styles for the Realtime Q&A app
*/
/*
fonts.com font-family rules.
Cooper Black
font-family:'Cooper Black W01';
News Gothic:
light: font-family:'NewsGothicNo.2W01-Light 710401';
light-italic: font-family:'NewsGothicNo.2W01-Light 710404';
roman: font-family:'NewsGothicNo.2W01-Roman';
italic: font-family:'News Gothic No.2 W01 It';
demi: font-family:'NewsGothicNo.2W01-Mediu 710407';
demi-italic: font-family:'NewsGothicNo.2W01-Mediu 710410';
bold: font-family:'NewsGothicNo.2W01-Bold';
bold-italic: font-family:'NewsGothicNo.2W01-BoldI';
*/
在index.html
的<head>
部分的 Fonts.com 脚本标签下包含新的样式表:
<head>
<meta charset="utf-8" />
<title>Realtime Q&A</title>
<!-- Fonts via fonts.com -->
<script type="text/javascript" src="[`fast.fonts.com/jsapi/a09d5d16-57fd-447d-a0f6-73443033d2de.js">
</script`](http://fast.fonts.com/jsapi/a09d5d16-57fd-447d-a0f6-73443033d2de.js"></script)>
<!--Main site styles-->
<link rel="stylesheet" href="./assets/styles/main.css" />
</head>
@FONT-FACE 和 SAAS
虽然看起来它仅仅存在了一两年,但实际上它早在 1998 年就随着 CSS2 规范被引入了。然而,由于缺乏支持、浏览器不兼容问题,以及最重要的法律问题,它直到最近才被真正使用。
使用@font-face
创建一个指向字体的指针,它允许浏览器下载并使用它在用户机器上呈现字体。这对设计来说很棒,但是它为任何人简单地非法下载字体打开了大门。可以预见,铅字铸造厂对此并不满意。
然而,近年来,在提高网络字体的安全性方面取得了长足的进步。除了 web 字体的新格式,如嵌入式开放类型(EOT)和 web 开放字体格式(WOFF),提供 Web 字体嵌入软件作为服务的公司如雨后春笋般涌现(SaaS)。
使用 SaaS 有很多好处,但只有一个缺点(那根本不是缺点)。主要优势如下:
- 您无需购买即可使用该字体。
- 你不必再为了保护你自己和你的客户而在模糊的法律水域中航行。
- 跨浏览器字体嵌入的有限性已经为您处理好了。
使用 SaaS 进行字体嵌入的唯一“缺点”是它不是免费的。但是如果你看看这些数字,使用 SaaS 实际上是一笔巨大的交易。例如,流行的 Futura 字体可以从 MyFonts 3 以每种风格 24.95 美元或 445.50 美元购买全家(每月限 10,000 次浏览量)的网络字体购买。
*Fonts.com4每月收费 10 美元来嵌入整个 Futura 系列(以及其库中的任何其他字体),每月浏览量高达 25 万次。
当我们做数学计算时,这意味着如果唯一嵌入的字体是 Futura 系列,那么使用 SaaS 将需要超过 3 年半的时间来购买网络字体许可。
考虑到你的页面浏览量是它的 25 倍,而且你可能会在这项服务中使用不止一种字体,很明显 SaaS 是一笔大买卖。
网络字体 SaaS 选项
对于这个应用,我们使用 Fonts.com 加载库珀黑色和新闻哥特式到我们的设计。有许多选项可供选择,但以下是一些最受欢迎的选项:
http://fontdeck.com/
http://fonts.com/
https://typekit.com/
如果你选择使用 Fonts.com 以外的软件,你仍然可以顺利完成本书中的应用,但是请注意font-family
名称可能会有所不同。
注意Fonts.com 免费账户要求在你的应用中放置一个横幅,JavaScript include 会自动做到这一点。Fontdeck 的免费帐户将允许您开发本书中的应用,而不会增加任何成本。他们只在一个网站被“激活”时收费,他们认为当超过 20 个唯一的 IP 地址需要访问该网站时才收费。
公共元素
像大多数其他应用一样,这个应用有一些不随页面变化的通用元素。让我们从发展这些开始。
标题标记
这个应用将提供的每个视图都有顶部的标题。这在结构上很简单:它是一个盒子,里面有应用标题和标语。
使用 HTML5 <header>
元素,将标题标记添加到index.html
的<body>
部分:
<body>
<header>
<h1>Realtime Q&A</h1>
<p class="tagline">
A live feedback system for classes, presentations, and conferences.
</p><!--/.tagline-->
</header>
</body>
提示在带有类或 id 的元素的末尾使用注释有助于提高可扫描性,尤其是在处理嵌套了相同标签的嵌套元素或布局时,比如<div>
元素。这些注释完全是可选的。
如果你在浏览器中查看,它在语义上是有意义的(见图 7-3 )。这应该是应用的一个次要目标:如果所有的样式都被撕掉了,它还清晰可辨吗?
图 7-3。未样式化的标题标记
页脚标记
与页眉类似,页脚在语义上也很简单。它分解成一个盒子,里面有法律副本和几个链接。因为这样的复制在列表中是有意义的,所以让我们使用一个无序列表在<footer>
元素中显示它。将粗体代码添加到index.html
: 的正文中
<body>
<header>
<h1>Realtime Q&A</h1>
<p class="tagline">
A live feedback system for classes, presentations, and conferences.
</p><!--/.tagline-->
</header>
<footer>
<ul>
<li class="copyright">
© 2013 Jason Lengstorf & Phil Leggetter
</li><!--/.copyright-->
<li>
Part of <em>Realtime Web Apps: HTML5 Websockets, Pusher, and the
Web’s Next Big Thing</em>.
</li>
<li>
<a href="[`amzn.to/XKcBbG">Get`](http://amzn.to/XKcBbG">Get)the Book</a> |
<a href="[`cptr.me/UkMSmn">Source`](http://cptr.me/UkMSmn">Source)Code (on GitHub)</a>
</li>
</ul>
</footer>
</body>
在浏览器中重新加载index.html
。页脚信息以逻辑方式显示(参见图 7-4 )。
图 7-4。添加了无样式的页脚标记
款式
标记就绪后,就可以开始设计元素的样式了。从基础开始,添加字体规则。每个元素都将在em
s 中设置其字体大小,以便在稍后媒体查询时更加灵活。诀窍是将主体font-size
设置为px
值,对所有其他元素使用相对大小。如果你以后需要增加或减少字体大小,你需要做的就是调整主体设置,整个设计会相应地调整。
以设计为指导,为页眉和页脚中的每个元素设置颜色、大小和字母间距。使用粗体或斜体字体时,不要忘记将字体重置为正常样式和粗细;如果你忘记了,浏览器会将自己的粗体或斜体应用到已经粗体或斜体的文本中,这在大多数情况下看起来很糟糕。当你完成了这些,CSS 将看起来如下:
/**
* Styles for the Realtime Q&A app
*/
/*
fonts.com font-family rules.
Cooper Black
font-family:'Cooper Black W01';
News Gothic:
light: font-family:'NewsGothicNo.2W01-Light 710401';
light-italic: font-family:'NewsGothicNo.2W01-Light 710404';
roman: font-family:'NewsGothicNo.2W01-Roman';
italic: font-family:'News Gothic No.2 W01 It';
demi: font-family:'NewsGothicNo.2W01-Mediu 710407';
demi-italic: font-family:'NewsGothicNo.2W01-Mediu 710410';
bold: font-family:'NewsGothicNo.2W01-Bold';
bold-italic: font-family:'NewsGothicNo.2W01-BoldI';
*/
/* Basic Font Styles
----------------------------------------------------------------------------*/
body {
font: 18px/24px 'NewsGothicNo.2W01-Roman';
color: #1f1b0c;
letter-spacing: .06em;
}
h1 {
font-family: 'Cooper Black W01';
font-weight: normal;
}
h1 {
margin: 0;
color: #fbf7e7;
font-size: 6em;
line-height: 1em;
letter-spacing: -.1em;
}
.tagline {
font-family: 'NewsGothicNo.2W01-Light 710401';
font-size: 1.1em;
line-height: 1em;
color: #fbf7e7;
letter-spacing: .12em;
}
a {
font-family: 'NewsGothicNo.2W01-Mediu 710407';
color: #e06f00;
text-decoration: none;
}
a:active,a:hover,a:focus {
text-decoration: underline;
outline: none;
}
footer li {
font-family: 'NewsGothicNo.2W01-Light 710401';
font-size: .75em;
line-height: 1em;
letter-spacing: .04em;
color: #fbf7e7;
}
footer li em {
font-family: 'NewsGothicNo.2W01-Light 710404';
font-style: normal;
}
注意现在为h1
设置了两个规则。这是有意的,因为在设计的后期,会有其他元素添加到第一个规则中,这些元素不应该从第二个规则集中接收样式。
如果此时将index.html
加载到浏览器中,在添加深色背景之前,它看起来不会正确(参见图 7-5 )。
图 7-5。仅应用字体样式规则的标记
要纠正这一点,现在您需要添加布局规则,以给出背景颜色、对齐方式和其他框模型规则。添加新的布局代码以完成页眉和页脚样式:
/* Layout
----------------------------------------------------------------------------*/
html { background-color: #fbf7e7; }
body { margin: 0; }
header,footer {
-webkit-box-shadow: 0 0 10px rgba(31, 27, 12, .3);
box-shadow: 0 0 10px rgba(31, 27, 12, .3);
}
header,footer {
overflow: hidden;
background: #1f1b0c;
margin: 0;
padding: 1em;
text-align: center;
}
header {
margin-bottom: 3em;
padding: 3em 0 2em;
}
header h1,header p {
width: 960px;
margin: 0 auto;
padding: 0;
}
header h1 { margin-bottom: .25em; }
footer { margin-top: 6em; }
footer ul {
overflow: hidden;
width: 960px;
margin: 0 auto;
padding: 0;
}
footer li {
float: right;
margin-left: 1em;
list-style: none;
}
footer li.copyright { float: left; margin-left: 0; }
这段代码的大部分都是不言自明的,但是有几条规则值得注意。方框阴影仅使用-webkit-
前缀。这是因为所有其他浏览器现在都支持标准的框阴影规则(甚至是 IE9),所以不再需要包含规则的前缀版本来使其工作。事实上,-webkit-
前缀只需要添加对 Safari、iOS 和 Android 旧版本的支持。因为旧版本的 iOS 和 Android 并不少见,所以保持这个规则是个好主意。
box-shadow
规则还使用了从 CSS3 开始新增的rgba
。这允许您设定颜色的 alpha(或不透明度)。使用的 RGB 值与站点的深色相匹配。
最后,一个解决“clearfixes”需求的技巧:将包含元素设置为overflow: hidden
,它将增长到包含浮动元素。这适用于所有的浏览器,所以这是一个消除大量非语义元素的非常简单的技巧。这个技巧用在页脚元素上,强制<footer>
元素在浮动的<li>
元素周围正确地应用填充。
重新加载浏览器,查看完整的页眉和页脚(参见图 7-6 )。
图 7-6。样式化的页眉和页脚
使页眉和页脚响应迅速
完成页眉和页脚的最后一步是添加媒体查询,这将调整它们在平板电脑和手持设备上的显示。
您不需要做任何花哨的事情,比如检查设备方向或像素密度,而是简单地根据视口宽度调整布局。这种方法的原因是,在较小的屏幕上不需要发生任何花哨的事情;布局只需要稍微调整一下,以适应所提供的屏幕空间。
为了简单起见,将仅对布局进行两项调整:宽度在960px
以下的设备(平板电脑)和宽度在768px
以下的设备(手持设备)。
对于标题,需要改变的只是元素的字体大小和最大宽度。更改填充有助于防止布局看起来过于宽敞。
页脚需要同样的调整。此外,它还需要关闭列表元素的浮动,以便它们可以居中。
将媒体添加到main.css
的底部,以调整较小屏幕的页眉和页脚:
/* Media queries
----------------------------------------------------------------------------*/
@media screen and (max-width: 960px)
{
header h1,header p { width: 760px; }
header { padding: .75em 0 1.2em; }
footer { margin-top: 4em; padding: 0;}
footer ul { width: 740px; }
footer li,footer li.copyright { float: none; margin: .75em 0;}
}
@media screen and (max-width: 768px)
{
header h1,header p {
width: 90%;
min-width: 300px;
}
header { padding-bottom: .75em; }
header h1 { font-size: 2.4em; }
footer { margin-top: 3em; }
footer ul { width: 300px; }
}
在浏览器中重新加载文件。乍一看,似乎什么都没有改变,但是如果你调整浏览器窗口的大小,你会看到元素根据不同的屏幕尺寸进行了调整(见图 7-7 )。
图 7-7。不同屏幕尺寸的页眉和页脚
开发主页视图
应用的框架准备好了,你现在可以从主页开始插入各个视图了。
编写标记
主页由两个表单组成,所以让我们从两个表单中较简单的一个开始,了解基本情况。
“加入房间”表单有一个标题、一个简短的广告词、一个带标签的输入和一个提交按钮。将以下标记添加到index.html, in between the <header> and <footer> tags,
以创建该表单:
<section>
<form id="attending">
<h2>Attending?</h2>
<p>Join a room using its ID.</p>
<label>
What is the room's ID?
<input type="text" name="room_id" />
</label>
<input type="submit" value="Join This Room" />
</form><!--/#attending-->
</section>
接下来,使用相同格式生成“创建房间”表单,它的标记方式几乎相同。将这个<form>
元素直接添加到我们刚刚添加的前一个“attending”表单元素之后:
<form id="presenting">
<h2>Presenting?</h2>
<p>Create a room to start your Q&A session.</p>
<label>
Tell us your name (so attendees know who you are).
<input type="text" name="presenter-name" />
</label>
<label>
Tell us your email (so attendees can get in touch with you).
<input type="email" name="presenter-email" />
</label>
<label>
What is your session called?
<input type="text" name="session-name" />
</label>
<input type="submit" value="Create Your Room" />
</form><!--/#presenting-->
这段代码没有什么值得注意的,除了使用type="email"
作为演示者的电子邮件输入。保存它并重新加载浏览器;你现在有了主视图的两种形式,尽管没有样式化(见图 7-8 )。
图 7-8。主视图的所有标记都已就位,但需要样式化
实现 CSS
更新现有的包含字体系列和字体粗细的h1
规则,使字体系列和字体粗细也适用于h2
标签。还要为二级标题添加附加规则;一般段落;以及表单、输入和标签:
/* Basic Font Styles
----------------------------------------------------------------------------*/
/* Update */
h1, h2 {
font-family: 'Cooper Black W01';
font-weight: normal;
}
/* Unchanged */ h1 { margin: 0; color: #fbf7e7; font-size: 6em; line-height: 1em; letter-spacing: -.1em;}/* New rules */
h2 { text-shadow: 0 0 10px rgba(31, 27, 12, .3); }
h2 {
margin: 0 0 .5em;
font-size: 2.75em;
line-height: 1em;
letter-spacing: -.08em;
}
p {
text-align: center;
}
form p {
margin: 0 0 1em;
padding: 0;
font-size: 1.375em;
}
label {
font-family: 'NewsGothicNo.2W01-Bold';
font-size: .75em;
line-height: 1.25em;
letter-spacing: .04em;
}
input {
font-family: 'Cooper Black W01';
color: #fbf7e7;
background-color: #1f1b0c;
border-radius: 6px;
border: none;
font-size: 1.75em;
line-height: 1em;
letter-spacing: -.08em;
text-shadow: 0 0 10px rgba(31, 27, 12, .3);
}
label input {
font-family: 'NewsGothicNo.2W01-Light 710401';
font-size: 1.75em;
letter-spacing: .08em;
color: #1f1b0c;
background-color: #fff;
}/* Existing rules e.g. .tagline */
保存后,主视图应有适当的字体(见图 7-9 )。
图 7-9。应用了字体样式的主视图,但没有布局规则
布局规则相当简单:“加入房间”表单向右浮动,“创建房间”表单向左浮动,标签和输入需要垂直堆叠。通过在main.css
: 的“布局”部分的第一个footer
规则之前插入如下所示的代码来实现这些规则
section {
width: 960px;
margin: 0 auto;
overflow: hidden;
}
form {
float: left;
width: 460px;
text-align: center;
}
form#attending { float: right; }
label {
display: block;
width: 430px;
margin: 0 auto 1em;
text-indent: 2px;
text-align: left;
}
input {
margin: .25em 0 0;
padding: .375em .875em;
}
label input {
display: block;
width: 400px;
margin: 0;
padding: .375em 15px;
border: 2px solid #1f1b0c;
-webkit-border-radius: 6px;
border-radius: 6px;
-webkit-box-shadow: inset 2px 2px 14px rgba(31, 27, 12, .3);
box-shadow: inset 2px 2px 14px rgba(31, 27, 12, .3);
}
保存这些更改并在浏览器中重新加载页面。此时,主视图已接近完成(见图 7-10 )。
图 7-10。主视图现在看起来像实体模型
为活动和悬停的表单元素创建样式
因为表单是交互式的,所以当用户悬停、单击和切换表单元素时,为他们提供视觉反馈是一个好主意。为此,您需要为输入的:active
、:hover
和:focus
状态添加样式。
使用高亮颜色#E06F00
,改变活动输入的边框颜色以指示用户当前关注的位置,并使提交按钮在活动或悬停时变为橙色。包括以下代码来实现这一点:
/* Highlights
----------------------------------------------------------------------------*/
input:active,input:hover,input:focus {
background-color: #e06f00;
outline: none;
}
input::-moz-focus-inner { border: 0; }
label input:active,label input:focus {
border-color: #e06f00;
background-color: #fff;
outline: none;
}
label input:hover { background-color: #fff; }
这段代码覆盖了浏览器的默认行为,并用一个自定义突出显示来替换它。值得注意的是input::-moz-focus-inner
规则;这解决了 Firefox 中的一个问题,当它处于活动状态时,会导致输入在输入内部出现一个小虚线。
警告如果你覆盖了默认的浏览器样式,确保用你自己的样式替换它们。使用键盘浏览网页的用户依靠:focus
和:active
状态来查看光标当前停留的位置,因此完全移除这些状态将对用户体验产生负面影响。
覆盖样式后,在浏览器中重新加载页面,并使用 Tab 键在表单中导航。现在,当输入处于活动状态时,它们会以橙色突出显示(参见图 7-11 ),当它们有焦点或悬停在上面时,提交按钮会变成橙色(参见图 7-12 )。
图 7-11。具有焦点的文本输入现在用橙色突出显示
图 7-12。当鼠标悬停或聚焦时,提交按钮变成橙色
添加媒体查询
媒体对主页的查询仍然相当简单。在平板电脑上,如果表格缩小了一点,你仍然可以把它们并排放在一起,而在手持设备上,它们应该一个叠一个。
“加入房间”表单应该放在最上面,因为它更短,也更容易被使用(参加的人可能比出席的人多)。
以下代码为较小的视口添加了额外的规则:
@media screen and (max-width: 960px)
{
header h1,header p { width: 760px; }
header { padding: .75em 0 1.2em; }
section { width: 740px; }
p { margin: 0 0 2em; padding: 0; }
header>p,section>p { font-size: .875em; }
form { width: 340px; padding: 0 8px; }
form p { font-size: 1em; }
label { width: 100%; }
input { font-size: 1.5em; }
label input { width: 91%; }
footer { margin-top: 4em; padding: 0;}
footer ul { width: 740px; }
footer li,footer li.copyright { float: none; margin: .75em 0;}
}
@media screen and (max-width: 768px)
{
header h1,header p,section {
width: 90%;
min-width: 300px;
}
header { padding-bottom: .75em; }
header h1 { font-size: 2.4em; }
form,form#attending {
float: none;
width: 90%;
margin: 0 auto 3em;
}
form p { font-size: .75em; }
label input { width: 88%; font-size: 1.6em; }
footer { margin-top: 3em; }
footer ul { width: 300px; }
}
保存并重新加载页面;然后改变浏览器大小,查看布局是否适应(参见图 7-13 )。
图 7-13。主视图,包含响应性布局规则
为与会者开发活动房间视图
下一步是为活动房间视图创建标记,因为它将被与会者看到。这是他们提问和投票的地方。
编写标记
首先,抓取在index.html
中使用的相同的页眉和页脚标记,并将其保存到一个名为attendee-active.html
: 的新文件中
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Realtime Q&A</title>
<!-- Fonts via fonts.com -->
<script type="text/javascript" src="[`fast.fonts.com/jsapi/a09d5d16-57fd-447d-a0f6-73443033d2de.js"></script`](http://fast.fonts.com/jsapi/a09d5d16-57fd-447d-a0f6-73443033d2de.js"></script)>
<!-- Main site styles -->
<link rel="stylesheet" href="./assets/styles/main.css" />
</head>
<body>
<header>
<h1>Realtime Q&A</h1>
<p class="tagline">
A live feedback system for classes, presentations, and conferences.
</p><!--/.tagline-->
</header>
<section>
</section>
<footer>
<ul>
<li class="copyright">
© 2013 Jason Lengstorf & Phil Leggetter
</li><!--/.copyright-->
<li>
Part of <em>Realtime Web Apps: HTML5 Websockets, Pusher, and the
Web’s Next Big Thing</em>.
</li>
<li>
<a href="http://amzn.to/XKcBbG">Getthe Book</a> |
<a href="http://cptr.me/UkMSmn">SourceCode (on GitHub)</a>
</li>
</ul>
</footer>
</body>
</html>
该视图的内容分为三个部分:
- 标题,包含房间名称和演示者信息
- “提出问题”表格
- 问题列表
前两个标记元素并不奇怪,所以让我们先把它们去掉。在<section>
中添加两个附加元素:
<section>
<header>
<h2>Realtime Web Apps & the Mobile Internet</h2>
<p>
Presented by Jason Lengstorf
(<a href="mailto:jason@lengstorf.com">email</a>)
</p>
</header>
<form id="ask-a-question">
<label>
If you have a question and you don't see it below, ask it here.
<input type="text" name="new-question" tabindex="1" />
</label>
<input type="submit" value="Ask" tabindex="2" />
</form><!--/#ask-a-question-->
</section>
使用 HTML5 的数据属性
这些问题将以一个无序列表的形式出现,但是有一个变化:您将使用 HTML5 data-
属性,而不是为投票计数创建一个额外的元素。
这有两个目的:
- jQuery 内置了对访问该属性的支持
- CSS 可以使用这个属性来生成对文档不重要的内容
注意“不重要”在前面的陈述中是指投票计数对于所显示的信息并不重要。出于这个原因,它被排除在标记之外,而是由 CSS 显示,从而保持标记的整洁和语义。
将问题无序列表标记添加到现有<section>
的底部:
<ul id="questions">
<li id="question-1"
data-count="27">
**<p>**
**What is the best way to implement realtime features today?**
**</p>**
**<form class="vote">**
**<input value="I also have this question."**
**type="submit" />**
**</form>**
**</li>**`<!--`**/#question-1**`-->`
**<li id="question-2"**
**data-count="14">**
**<p>**
**Does this work on browsers that don't support the**
**WebSockets API?**
**</p>**
**<form class="vote">**
**<input value="I also have this question."**
**type="submit" />**
**</form>**
**</li>**`<!--`**/#question-2**`-->`
**</ul>**`<!--`**/#questions**`-->`
标记就绪后,在浏览器中加载
attendee-active.html`。看起来不太好,但所有的部分都在适当的位置(见图 7-14 )。
图 7-14。未样式化的活动与会者视图
实现 CSS
首先,通过在main.css
: 的“基本字体样式”部分的第一个页脚规则之前插入以下代码,添加问题和标题的字体样式。
section header h2 {
font-size: 1.5em;
line-height: 1.125em;
letter-spacing: -.06em;
}
#questions li {
font-size: 1.33em;
letter-spacing: .1em;
}
接下来,您需要添加基本的布局规则。有很多,所以在main.css
中创建一个新的部分,紧接在现有的“布局”部分之后:
/* Questions View
----------------------------------------------------------------------------*/
section header {
background: transparent;
float: right;
width: 340px;
margin: 0;
padding: 0;
box-shadow: none;
overflow: visible;
}
section header h2 {
margin: 0 0 .5em;
text-align: left;
}
section header p {
width: auto;
margin: 0;
text-align: left;
}
form#ask-a-question,#questions {
width: 596px;
margin: 0;
padding: 0;
overflow: hidden;
}
#questions { padding-bottom: 1em; }
#ask-a-question label,#ask-a-question>input { float: left; }
#ask-a-question label { width: 460px; }
#ask-a-question label input {
width: 430px;
height: 1.7em;
margin: 0;
padding-left: 15px;
padding-right: 15px;
}
#ask-a-question input {
height: 1.55em;
margin: 0.5em 0 0 0.5em;
padding: 0.1em 0.75em;
}
#questions li {
position: relative;
list-style: none;
margin: 0;
padding: 1em 0 1em;
overflow: hidden;
-webkit-box-shadow: 0 12px 16px -16px rgba(31, 27, 12, .3),
0 -12px 16px -16px rgba(31, 27, 12, .3);
box-shadow: 0 12px 16px -16px rgba(31, 27, 12, .3),
0 -12px 16px -16px rgba(31, 27, 12, .3);
}
#questions p {
float: right;
width: 77%;
margin: .75em 0;
padding: 0;
text-align: left;
}
#questions .vote {
position: relative;
display: block;
width: 76px;
height: 76px;
margin: 0 0 0 2em;
padding: 0;
overflow: hidden;
}
这些规则都是相当基本的;他们解决了<header>
元素与文本颜色相同的问题,将各部分浮动到适当的位置,并在适当的地方添加边距和间距,以使元素正确布局(参见图 7-15 )。
图 7-15。问题页面的部分完整布局
切片 PSD 和创建精灵
布局基本完成后,我们需要将按钮的图像放入网站。这将需要切片 PSD 并创建一个 sprite,这是一个由较小的图像组成的大图像。精灵用于减少请求的数量和网站的整体下载量。
在与会者问题视图中打开 Photoshop 中的 PSD 并抓取切片工具。在“向上投票”按钮周围画一个 76 px 乘 76 px 的切片。确保它水平和垂直居中。
在第二个向上投票按钮,进入黑色圆形的图层样式,用我们的高亮颜色#E06F00
添加一个颜色叠加。这将是按钮的悬停和活动状态。
在按钮顶部以 76 px 乘 76 px 绘制另一个切片(参见图 7-16 )。
图 7-16。PSD 中的按钮带有切片
存储为 Web 所用(command + option + shift + S 或“文件”菜单中的“存储为 Web 所用…”)。将两个按钮片段保存为 PNG 文件。您可以随意命名它们,因为您将在下一步将它们组合成不同的图像。
为了准备演示者视图,通过绘制 78 px 宽 88 px 高的切片,对回答按钮及其悬停状态做同样的事情(参见图 7-17 )。
图 7-17。PSD 中的接听按钮带有切片
接下来,创建一个 154 像素宽,176 像素高的新 PSD。将背景设置为浅色#FBF7E7
。
将您刚刚创建的四个 png 放入文档,将“关闭”投票按钮放在左上角,“打开”投票按钮放在左下角,“关闭”回答按钮放在右上角,“打开”回答按钮放在右下角(参见图 7-18 )。
图 7-18。Photoshop 中的精灵
最后一步是将这张图片保存到网上。这一次,将其保存为 JPG,并在不明显损失质量的情况下尽可能降低质量。在该图像的情况下,70
的质量设置是好的。
要保存该图像以供项目使用,请在assets
文件夹中创建一个名为images
的新子目录;然后将此图像保存为sprite.jpg
。
回到main.css
,让我们使用新的精灵来设计投票按钮。将以下代码添加到“问题视图”部分的底部:
#questions .vote input[type=submit] {
margin: 0;
width: 100%;
height: 100%;
cursor: pointer;
text-indent: -9999px;
background: url(../img/sprite.jpg) left top no-repeat;
-webkit-box-shadow: none;
box-shadow: none;
}
#questions .vote input:active,
#questions .vote input:hover,
#questions .vote input:focus {
background-position: left bottom;
}
这段代码将输入设置为像一个小的查看窗口一样工作,只显示 sprite 的一部分。在悬停时,或者当用户切换到输入时,精灵移动,在输入的查看窗口中显示其自身的不同部分。这节省了加载“over”图像的额外 HTTP 请求,从而改善了加载时间和整体用户体验。
在浏览器中保存并重新加载文档。输入现在看起来像你设计的按钮,悬停或点击按钮会使其以橙色高亮显示(见图 7-19 )。
图 7-19。样式化按钮,包括其高亮显示状态
使用:在之前
这个视图的最后一步是从data-count
属性中检索投票数。这是使用:before
伪元素和content
规则完成的,它允许您传入一个要在伪元素中显示的属性名。使用data-count
属性,将其向左移动,并使用以下代码将其垂直放置在中心位置(添加到main.css)
: 中“问题视图”部分的底部)
#questions li:before {
content: attr(data-count);
position: absolute;
left: 0;
top: 50%;
margin-top: -.5em;
}
保存并重新加载;现在票数显示在每个问题的左侧(见图 7-20 )。
图 7-20。投票数显示在问题的左侧
最后,您需要使字体与设计相匹配,因此在main.css
的“基本字体样式”部分,为已经应用于h1
和h2
的字体规则添加一个用于伪元素的选择器:
h1,h2,#questions li:before {
font-family: 'Cooper Black W01';
font-weight: normal;
}
现在,如果重新加载,字体看起来是正确的(见图 7-21 )。
图 7-21。风格化的投票计数
添加媒体查询
较小屏幕的布局相当简单。在平板电脑上,房间描述移动到布局的顶部,表单和问题位于下方,形成一列布局。
在手持设备上,一列布局仍然存在,投票计数移到投票按钮上方,以节省更多的水平空间。
更新main.css
中的“媒体查询”部分,以匹配粗体显示的代码,从而实施更改:
@media screen and (max-width: 960px)
{
header h1,header p { width: 760px; }
header { padding: .75em 0 1.2em; }
section header {
float: none;
width: 680px;
margin: 0 auto 1.5em;
overflow: hidden;
}
p { margin: 0 0 2em; padding: 0; }
header>p,section>p { font-size: .875em; }
form { width: 340px; padding: 0 8px; }
form#ask-a-question { float: none; width: 680px; margin: 0 auto 1em; }
form p { font-size: 1em; }
label { width: 100%; }
#ask-a-question label { width: 80%; }
input { font-size: 1.5em; }
#ask-a-question input { font-size: 1.75em; }
label input,#ask-a-question label input { width: 91%; }
#questions { margin: 0 auto; }
footer { margin-top: 4em; padding: 0;}
footer ul { width: 740px; }
footer li,footer li.copyright { float: none; margin: .75em 0;}
}
@media screen and (max-width: 768px)
{
header h1,header p {
width: 90%;
min-width: 300px;
}
section,section header,form#ask-a-question,#questions { width: 300px; }
header { padding-bottom: .75em; }
header h1 { font-size: 2.4em; }
form,form#attending,form#ask-a-question {
float: none;
width: 90%;
margin: 0 auto 3em;
}
form#ask-a-question { overflow: visible; }
form p { font-size: .75em; }
label input { width: 88%; font-size: 1.6em; }
#ask-a-question label { width: 270px; }
#ask-a-question label input { width: 87%; }
#ask-a-question input {
float: none;
margin: 0 auto;
}
#questions li { font-size: 1.125em; line-height: 1.125em; }
#questions li:before { top: 20px; left: 24px; margin-top: 0; }
#questions .vote { margin: 20px 0 0;}
#questions p { width: 210px; margin: 0; }
footer { margin-top: 3em; }
footer ul { width: 300px; }
}
保存、重新加载和调整浏览器窗口的大小。布局现在可以响应了(见图 7-22 )。
图 7-22。以多种视窗尺寸查看问题
为与会者开发封闭房间视图
下一步是为被演示者关闭的房间创建视图。与会者将不再能够投票,将会有一个通知让他们知道会议室已经关闭,他们可以通过电子邮件联系演示者。
在 app 的根目录下新建一个名为attendee-closed.html
的文件;这是保存该步骤标记的地方。
争取尽可能少的新加价
因为编写更少的代码总是更好,所以对标记所做的唯一更改是用封闭房间通知替换“提问”表单,并在问题列表中添加一个“封闭”类。将attendee-active.html
的内容复制到新的attendee-closed.html
文件中,将“提问”表单替换为关闭通知标记,并添加class="closed"
属性,如下图所示:
<h3>This session has ended.</h3>
<p>
If you have a question that wasn't answered, please
<a href="mailto:jason@copterlabs.com">email the presenter</a>.
</p>
<ul id="questions" class="closed">
<!—leave existing elements here -->
</ul><!--/#questions-->
保存后,将文档加载到浏览器中,显示文档几乎准备就绪,无需更改(见图 7-23 )。
图 7-23。未应用新样式的关闭视图
添加样式
调整标题和正文样式,更新两个现有规则以包括h3
,添加一个新的h3
规则,并修改p
,如下面main.css
: 的“基本字体样式”部分所示
h1,h2,h3,#questions li:before {
font-family: 'Cooper Black W01';
font-weight: normal;
}
h2,h3 { text-shadow: 0 0 10px rgba(31, 27, 12, .3); }
h3 {
margin: 0 365px .75em 0;
font-size: 1.875em;
line-height: 1em;
letter-spacing: -.08em;
text-align: center;
}
p {
text-align: center;
margin: 0 365px 2em 0;
padding: 0 6em;
}
接下来,通过将问题的不透明度降低到 0.2,使问题变得明显不再是交互式的。从按钮上移除活动和悬停状态,以确保它看起来不可点击。将#questions.closed
规则添加到main.css
: 中“问题视图”部分的末尾
#questions.closed { opacity: .4; }
#questions.closed .vote { opacity: .2; }
#questions.closed .vote input:active,
#questions.closed .vote input:hover,
#questions.closed .vote input:focus {
background-position: left top;
cursor: default;
}
重新加载您的浏览器;您会看到问题逐渐消失,“投票”按钮显示为禁用状态(图 7-24 )。
图 7-24。已完成的封闭房间视图
媒体的询问呢?
因为您将更改保持得如此简单,所以不需要对媒体查询进行更新;它开箱后就能正常工作。
为演示者开发会议室视图
转移到后端功能之前的最后一步是为 presenter 视图创建标记。除了以下三点之外,该视图与与会者视图完全相同:
- 没有“提出问题”的形式
- 有一个“回答”表单和按钮,而不是“投票”按钮
- 在房间信息下面有一个表单,有一个到房间的链接和一个关闭表单的按钮
返工现有标记
创建一个名为presenter.html
的新文件,并将attendee-active.html
的内容复制到其中。
接下来,删除问题上方的“提问”表单标记,并删除每个问题的“投票”表单,代之以“回答”表单和按钮。在<header>
标签中添加一个新表单,其中包含房间信息,该信息具有一个禁用的文本输入,其值为房间的统一资源指示器(URI ),并添加一个提交按钮,其副本为“Close This Room ”,这将允许演示者结束会话。
总而言之,变化很少。唯一的区别是在<section>
元素中:
<section>
<header>
<h2>Realtime Web Apps & the Mobile Internet</h2>
<p>
Presented by Jason Lengstorf
(<a href="mailto:jason@lengstorf.com" tabindex="100">email</a>)
</p>
<form id="close-this-room">
<label>
Link to your room.
<input type="text" name="room-url"
value="[`realtime.local/room/1234`](http://realtime.local/room/1234)"
disabled />
</label>
<input type="submit" value="Close This Room" />
</form><!--/#close-this-room-->
</header>
<ul id="questions" class="presenter">
<li id="question-1"
data-count="27">
<form class="answer">
<input type="submit" value="Answer this question." />
</form>
<p>
What is the best way to implement realtime features today?
</p>
</li>
<li id="question-2"
data-count="14">
<form class="answer">
<input type="submit" value="Answer this question." />
</form>
<p>
Does this work on browsers that don't support the
WebSockets API?
</p>
</li>
</ul><!--/#questions-->
</section>
在浏览器中查看此文件;它需要一点调整,但已经很接近了(见图 7-25 )。
图 7-25。无样式演示者视图
更新 CSS
正如你在图 7-25 中看到的,唯一需要调整的是“回答”按钮和侧边栏中的表单。“回答”按钮使用 sprite,类似于“投票”按钮,浮动在问题的右侧。侧边栏中的表单只需要稍微窄一点,以适应可用的空间。
更新现有的#questions .vote
和#questions .vote input[type=submit]
规则,使其也适用于演示者视图,并在main.css
: 中的“问题视图”部分的底部添加一些特定的回答和结束规则
/* Updated rules */
#questions .vote,#questions .answer {
position: relative;
display: block;
width: 76px;
height: 76px;
margin: 0 0 0 2em;
padding: 0;
overflow: hidden;
}
#questions .vote input[type=submit],
#questions .answer input[type=submit] {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
cursor: pointer;
text-indent: -9999px;
background: url(../img/sprite.jpg) left top no-repeat;
-webkit-box-shadow: none;
box-shadow: none;
}
/* new rules */#questions .answer {
float: right;
width: 78px;
height: 88px;
margin: 0;
}
#questions .answer input[type=submit] { background-position: right top; }
#questions .answer input:active,
#questions .answer input:hover,
#questions .answer input:focus {
background-position: right bottom;
}
#close-this-room { width: 340px; margin: 2em 0 0; }
#close-this-room label input { width: 305px; }
重新加载您的浏览器以查看正确样式的演示者视图(参见图 7-26 )。
图 7-26。带有“回答”按钮活动状态的风格化演示者视图
更新媒体查询
presenter 视图的媒体查询与 attendee 视图的类似,只是不是将投票计数移动到左侧按钮的上方,而是移动到右侧计数的下方。
除此之外,“关闭这个房间”表单被重排以适应平板电脑和手持设备上的单列布局。
更新现有规则并将新规则添加到main.css
的“媒体查询”部分,如下所示:
@media screen and (max-width: 960px)
{
section header,#close-this-room {
float: none;
width: 680px;
margin: 0 auto 1.5em;
overflow: hidden;
}
#close-this-room { margin: 1em auto 0; }
#close-this-room label { width: 59%; float: left;}
#close-this-room label input { width: 88%; margin: 0; }
#close-this-room input { float: left; margin: .6em 0 0; }
}
@media screen and (max-width: 768px)
{
form,form#attending,form#ask-a-question,#close-this-room {
float: none;
width: 90%;
margin: 0 auto 3em;
}
#ask-a-question label,#close-this-room label { width: 270px; }
#ask-a-question input,#close-this-room input {
float: none;
margin: 0 auto;
}
/* New close rules */
#close-this-room { margin: 1em auto 0; }
#questions.presenter li:before {
top: auto;
right: 24px;
bottom: 6px;
left: auto;
z-index: 10;
}
}
在浏览器中保存并重新加载页面。你现在有了一个响应的演示者视图(见图 7-27 )。
图 7-27。演示者视图,包括平板电脑和手持设备布局
摘要
此时,您已经有了应用的 HTML 和 CSS 模板,并且您已经准备好开始使用 PHP 和 MySQL 开发后端。
仍然有一些东西需要 CSS 样式,比如点击后的“投票”按钮,或者回答后的问题,但是你将在下一章开发该功能时添加这些样式。
在下一章中,您将构建服务器端脚本和数据库来使应用实际工作。
1
2
3
4`*
八、构建后端:第一部分
到目前为止,在 app 开发过程中,大部分的努力都是针对网站的前端。在本章中,您将构建驱动前端的后端脚本。
滚动你自己的简单 MVC 框架
在构建应用之前,你的首要任务是为它构建一个框架。正如我们之前所确定的,模型-视图控制器(MVC)框架将是这个特定构建中的最佳选择。
在本节中,您将从头开始构建一个 MVC 框架。这是一项复杂的任务,所以我们分两章来完成。在第一章中,你将建立主页的框架和它背后的一些逻辑,在第九章中,我们将填充 MVC 结构并完成应用。
确定目录结构
一个好的应用从适当的组织开始。在 web 根文件夹中,创建两个文件夹并添加子文件夹,如下所示:
-
assets
-
images
-
scripts
-
styles
-
system
-
config
-
controllers
-
core
-
inc
-
lib
-
models
-
views
在我们构建应用时,我们将介绍这些文件夹的用途;现在,请放心,这个应用将组织良好。
为所有请求设置路由
您的框架需要的第一件事是一个将请求路由到适当位置的脚本。该脚本将初始化任何必要的配置变量,加载任何额外的必需脚本,并确定用户通过适当的控制器发送请求的意图(通过 URL)。
设置配置变量
因为这个应用的每个安装可能会有不同的配置,你将建立一个配置文件。在/system/config
中,创建一个名为config.inc.php
的新文件,并插入以下内容:
<?php
/**
* A sample configuration file
*
* The variables below need to be filled out with environment specific data.
*
* @author Jason Lengstorf <jason@lengstorf.com>
* @author Phil Leggetter <phil@leggetter.co.uk>
*/
// Set up an array for constants
$_C = array();
//-----------------------------------------------------------------------------
// Converts the constants array into actual constants
//-----------------------------------------------------------------------------
foreach ($_C as $constant=>$value) {
define($constant, $value);
}
到目前为止,这实际上并没有创建任何配置变量,但是它建立了一个结构来实现这一点。所有配置变量都将被添加到$_C
数组中,该数组通过脚本底部的一个foreach
循环运行,将每个变量定义为一个常量。
注意配置变量被定义为常量是因为 1)它们是不可变的,这意味着它们在执行过程中不能被更改;2)它们需要在全局范围内,以便对函数和类可用。
结构就绪后,开始添加配置变量。在本章中,我们需要存储以下特定于应用的数据:
- 应用的时区
- 数据库配置信息
- 是否显示调试信息
在config.inc.php
中,添加粗体显示的代码,将配置变量声明为常量:
<?php
/**
* A sample configuration file
*
* The variables below need to be filled out with environment specific data.
*
* @author Jason Lengstorf <jason@lengstorf.com>
* @author Phil Leggetter <phil@leggetter.co.uk>
*/
// Set up an array for constants
$_C = array();
//-----------------------------------------------------------------------------
// General configuration options
//-----------------------------------------------------------------------------
$_C['APP_TIMEZONE'] = 'US/Pacific';
//-----------------------------------------------------------------------------
// Database credentials
//-----------------------------------------------------------------------------
$_C['DB_HOST'] = 'localhost';
$_C['DB_NAME'] = '';
$_C['DB_USER'] = '';
$_C['DB_PASS'] = '';
//-----------------------------------------------------------------------------
// Enable debug mode (strict error reporting)
//-----------------------------------------------------------------------------
$_C['DEBUG'] = TRUE;
//-----------------------------------------------------------------------------
// Converts the constants array into actual constants
//-----------------------------------------------------------------------------
foreach ($_C as $constant=>$value) {
define($constant, $value);
}
为每个变量添加正确的值后,保存此文件;您可以稍后在编辑器中关闭它,因为在本章中您不需要再次编辑该文件。
注意不要忘记使用您的开发数据库凭证更新DB_HOST
、DB_NAME
、DB_USER
和DB_PASS
值。如果没有它们,当您在本章后面构建数据库连接脚本时,应用将抛出一个致命错误。
正在初始化应用
应用的首要任务是设置环境变量和全局设置。这将让应用知道文件在哪里,如何连接到数据库,是否显示调试信息,以及其他步骤,使事情在引擎盖下顺利运行。
在 web 根文件夹中,创建一个名为index.php
的新文件。在里面,首先设置应用的基础:
<?php
/**
* The initialization script for the app
*
* @author Jason Lengstorf <jason@lengstorf.com>
* @author Phil Leggetter <phil@leggetter.co.uk>
*/
//-----------------------------------------------------------------------------
// Initializes environment variables
//-----------------------------------------------------------------------------
// Server path to this app (i.e. /var/www/vhosts/realtime/httpdocs/realtime)
define('APP_PATH', dirname(__FILE__));
// App folder, relative from web root (i.e. /realtime)
define('APP_FOLDER', dirname($_SERVER['SCRIPT_NAME']));
// URL path to the app (i.e.http://example.org/realtime/)
define(
'APP_URI',
remove_unwanted_slashes('http://' . $_SERVER['SERVER_NAME'] . APP_FOLDER . '/')
);
// Server path to the system folder (for includes)
define('SYS_PATH', APP_PATH . '/system');
APP_PATH
是一个常量,它将存储应用在服务器上的绝对路径。这是用于 PHP 包含。
另一方面,APP_FOLDER
存储来自 web 根的相对路径。它将用于相对链接或 CSS 包含和资产路径。为了避免从子目录运行应用的问题,包含了一个尾随斜线。由于调用了remove_unwanted_slashes()
,我们可以轻松地做到这一点。
APP_URI
是 app 的实际 URI。例如,如果应用位于www.example.org
的网络根目录下,APP_URI
将包含http://www.example.org/;
,如果应用位于名为 realtime 的子目录中,APP_URI
将存储http://www.example.org/realtime/
。
注 APP_URI
部分是用一个叫做remove_unwanted_slashes()
的函数确定的,这个函数还没有定义。这将在下一节中添加。
最后,SYS_PATH
包含系统文件的路径(这是 MVC 框架的大部分)。
在应用常量下面,您现在可以设置会话,包括配置变量,为应用设置适当的错误报告级别,以及设置应用时区。将粗体显示的代码添加到index.php
:
<?php
/**
* The initialization script for the app
*
* @author Jason Lengstorf <jason@lengstorf.com>
* @author Phil Leggetter <phil@leggetter.co.uk>
*/
//-----------------------------------------------------------------------------
// Initializes environment variables
//-----------------------------------------------------------------------------
// Server path to this app (i.e. /var/www/vhosts/realtime/httpdocs/realtime)
define('APP_PATH', dirname(__FILE__));
// App folder, relative from web root (i.e. /realtime)
define('APP_FOLDER', dirname($_SERVER['SCRIPT_NAME']));
// URL path to the app (i.e.http://example.org/realtime)
define(
'APP_URI',
remove_unwanted_slashes('http://' . $_SERVER['SERVER_NAME'] . APP_FOLDER)
);
// Server path to the system folder (for includes)
define('SYS_PATH', APP_PATH . '/system');
// Relative path to the form processing script (i.e. /realtime/process.php)
define('FORM_ACTION', remove_unwanted_slashes(APP_FOLDER . '/process.php'));
//-----------------------------------------------------------------------------
// Initializes the app
//-----------------------------------------------------------------------------
// Starts the session
if (!isset($_SESSION)) {
session_start();
}
// Loads the configuration variables
require_once SYS_PATH . '/config/config.inc.php';
// Turns on error reporting if in debug mode
if (DEBUG===TRUE) {
ini_set('display_errors', 1);
error_reporting(E_ALL^E_STRICT);
} else {
ini_set('display_errors', 0);
error_reporting(0);
}
// Sets the timezone to avoid a notice
date_default_timezone_set(APP_TIMEZONE);
因为这个应用将利用会话在页面加载之间传递数据,所以如果没有设置$_SESSION
超级全局,脚本将调用session_start()
。
然后,在加载配置变量之后,脚本检查DEBUG
的值,并且——如果它被设置为TRUE
,在开发期间应该总是这样——打开严格的错误报告;否则,会抑制错误。
最后,因为 PHP 会抛出一个没有它的通知,所以使用APP_TIMEZONE
变量来设置时区。
设置实用功能
为了避免混淆路由的逻辑,复杂的操作应该封装在函数中。幸运的是,本节没有太多复杂的操作,所以我们只需要创建四个实用函数:
- 解析 URI 并将其各部分作为数组返回的函数
- 使用 URI 部件确定控制器名称的函数
- 一种功能,用于防止 URI 中除其协议以外的任何部分出现双斜线
- 一个自动加载器,将检查我们的应用中的类,并包括它们(或提供一个有用的错误消息,如果请求的类不存在)
解析 URI
因为我们希望我们的应用有漂亮的 URIs,而不是笨拙的查询字符串,我们需要一种方法来确定 URI 的哪些部分是用于配置的,哪些只是 URI 的一部分。
注意因为这个应用可能不总是安装在 URI 的根目录下,所以解析脚本需要将 URI 与应用的位置进行比较,并只返回URI 中不引用应用在服务器上的位置的部分。
*例如,如果应用安装在http://www.example.org/
,房间 ID 1234 的 URI 将是http://www.example.org/room/1234
。然而,如果应用安装在名为realtime
的子目录中,则房间 ID 1234 的 URI 将是http://www.example.org/realtime/room/1234
。
在这两种情况下,我们只希望通过parse_uri()
函数返回“房间”和“1234”。
然后,URI 中与位置无关的部分将被以正斜杠分开,并存储为供应用使用的数组,应用将使用它们来确定要显示的视图(以及稍后将介绍的其他一些内容)。
在index.php
的底部,使用粗体显示的代码添加 URI 解析函数:
//-----------------------------------------------------------------------------
// Initializes the app
//-----------------------------------------------------------------------------
// Starts the session
if (!isset($_SESSION)) {
session_start();
}
// Loads the configuration variables
require_once SYS_PATH . '/config/config.inc.php';
// Turns on error reporting if in debug mode
if (DEBUG===TRUE) {
ini_set('display_errors', 1);
error_reporting(E_ALL^E_STRICT);
} else {
ini_set('display_errors', 0);
error_reporting(0);
}
// Sets the timezone to avoid a notice
date_default_timezone_set(APP_TIMEZONE);
//-----------------------------------------------------------------------------
// Function declarations
//-----------------------------------------------------------------------------
/**
* Breaks the URI into an array at the slashes
*
* @return array The broken up URI
*/
function parse_uri( )
{
// Removes any subfolders in which the app is installed
$real_uri = preg_replace(
'∼^'.APP_FOLDER.'∼',
'',
$_SERVER['REQUEST_URI'],
1
);
$uri_array = explode('/', $real_uri);
// If the first element is empty, get rid of it
if (empty($uri_array[0])) {
array_shift($uri_array);
}
// If the last element is empty, get rid of it
if (empty($uri_array[count($uri_array)-1])) {
array_pop($uri_array);
}
return $uri_array;
}
parse_uri()
函数首先使用preg_replace()
从请求的 URI 中删除APP_FOLDER
,只留下告诉应用用户请求什么的位。然后,该函数使用explode()
在正斜杠处分割 URI,然后在返回数组之前检查数组开头和结尾的空元素。
获取控制器名称
接下来,您需要找出合适的控制器名称来加载给定的 URI 部件。为此,将以下粗体代码添加到index.php
:
//-----------------------------------------------------------------------------
// Function declarations
//-----------------------------------------------------------------------------
/**
* Breaks the URI into an array at the slashes
*
* @return array The broken up URI
*/
function parse_uri( )
{
// Removes any subfolders in which the app is installed
$real_uri = preg_replace(
'∼^'.APP_FOLDER.'∼',
'',
$_SERVER['REQUEST_URI'],
1
);
$uri_array = explode('/', $real_uri);
// If the first element is empty, get rid of it
if (empty($uri_array[0])) {
array_shift($uri_array);
}
// If the last element is empty, get rid of it
if (empty($uri_array[count($uri_array)-1])) {
array_pop($uri_array);
}
return $uri_array;
}
/**
* Determines the controller name using the first element of the URI array
*
* @param $uri_array array The broken up URI
* @return string The controller classname
*/
function get_controller_classname( &$uri_array )
{
$controller = array_shift($uri_array);
return ucfirst($controller);
}
这个函数很简单:通过引用将 URI 传递给函数*,将第一个元素加载到变量$controller
中,然后将第一个字母大写并返回该值。*
注意使用&符号(&
)通过引用传递变量意味着在函数内部执行的操作不仅会影响传递给它的数据,还会影响调用该函数的范围。
这意味着http://example.com/room/1234/
的 URI 将被解析为具有以下结构的数组:
array(2) {
[0]=>
string(4) "room"
[1]=>
string(4) "1234"
}
数组的第一个元素“room”将被隔离、大写,然后返回,给我们这个get_controller_classname()
函数的返回值:
Room
注我们将在本章的稍后部分讨论如何使用这个返回值。
避免不必要的斜线
每当你处理 URIs 时,URI 部分总有可能有前导或尾随斜线。当这些部分结合在一起时,就会产生问题。
例如,站点的 URI 可能存储在一个变量中,如下所示:
$site_uri = 'http://www.example.org/';
如果一个链接被设置为相对于 web 根目录,它可能会被声明如下:
$services_link = '/services/';
现在假设您的应用需要提供一个可以从任何网页访问的服务页面链接。您的第一反应可能是:
$services_uri = $site_uri . $services_link;
但是,该变量将具有以下值:
http://www.example.org//services/
第二个双斜线是一个问题,当你的 URI 组件从不同的位置被抓取时($_SERVER
超全局,特定于应用的配置,等等。)不太可能会出现不想要的双斜线情况。
因此,值得编写一个函数来检测并删除给定 URI 中任何不需要的双斜线。但是,因为协议——意思是“http://”部分——有两个斜线,所以需要特别注意不要破坏 URI。
为此,将以下粗体代码添加到 index.php 中:
//-----------------------------------------------------------------------------
// Function declarations
//-----------------------------------------------------------------------------
/**
* Breaks the URI into an array at the slashes
*
* @return array The broken up URI
*/
function parse_uri( )
{
// Removes any subfolders in which the app is installed
$real_uri = preg_replace(
'∼^'.APP_FOLDER.'∼',
'',
$_SERVER['REQUEST_URI'],
1
);
$uri_array = explode('/', $real_uri);
// If the first element is empty, get rid of it
if (empty($uri_array[0])) {
array_shift($uri_array);
}
// If the last element is empty, get rid of it
if (empty($uri_array[count($uri_array)-1])) {
array_pop($uri_array);
}
return $uri_array;
}
/**
* Determines the controller name using the first element of the URI array
*
* @param $uri_array array The broken up URI
* @return string The controller classname
*/
function get_controller_classname( &$uri_array )
{
$controller = array_shift($uri_array);
return ucfirst($controller);
}
/**
* Removes unwanted double slashes (except in the protocol)
*
* @param $dirty_path string The path to check for unwanted slashes
* @return string The cleaned path
*/
function remove_unwanted_slashes( $dirty_path )
{
return preg_replace('∼(?<!:)//∼', '/', $dirty_path);
}
使用preg_replace()
,该函数检查前面没有冒号(:
)的双斜线(//
)的出现,并用单斜线(/
)替换它们。
因为正则表达式看 1 会有点毛,所以我们把这个一点一点分解一下:
-
∼
—开始分隔符;这只是告诉函数一个正则表达式模式开始了 -
(?<!:)
—所谓的“负面回顾”,有三个主要组成部分: -
括号—定义后视
-
?<!
—实际的 look back,字面意思是告诉正则表达式,“在匹配的字符之前查看该字符” -
:
—我们不想匹配的表情或性格;在这种情况下,它是两个斜杠前面的冒号,这表明它是协议,不应该被替换 -
//
—要查找的字符;在这种情况下,双斜线 -
∼
—结束分隔符;这告诉函数正则表达式模式结束了
继续前面的 URI 例子,双斜线问题是通过运行组合的 URI 部分到remove_unwanted_slashes()
来解决的:
$services_uri = remove_unwanted_slashes($site_uri . $services_link);
这在$services_uri
中存储了一个适当的 URI:
http://www.example.org/services/
自动加载类
最后,为了避免加载大量未使用的 PHP 类,您需要一个自动加载器,只在需要访问文件时才抓取它们。
这是通过创建一个函数在所有存储类的地方进行搜索来实现的;然后使用spl_autoload_register()
将该功能注册为自动加载器。
注意__autoload()
函数曾经是标准的,但是 PHP 现在推荐使用spl_autoload_register()
,因为它有更好的灵活性和性能。
首先,让我们参考应用的文件夹结构,并确定在构建应用时可能保存类文件的所有位置。
该函数将加载三个类类型:
- 控制器将存储在
system/controllers/
中 - 模型存储在
system/models/
中 - 核心文件将存储在
system/core/
中(我们将在后面详细讨论这些文件)
有了可能位置的列表,该函数将遍历每个位置,并查看该类是否存在;如果是,它将加载该类并返回TRUE
;如果没有,它抛出一个Exception
,声明该类不存在。
添加以下粗体代码来实现这一点。别忘了调用 app 初始化块中的 spl_autoload_register()
!
<?php
/**
* The initialization script for the app
*
* @author Jason Lengstorf <jason@lengstorf.com>
* @author Phil Leggetter <phil@leggetter.co.uk>
*/
//-----------------------------------------------------------------------------
// Initializes environment variables
//-----------------------------------------------------------------------------
// Server path to this app (i.e. /var/www/vhosts/realtime/httpdocs/realtime)
define('APP_PATH', dirname(__FILE__));
// App folder, relative from web root (i.e. /realtime)
define('APP_FOLDER', dirname($_SERVER['SCRIPT_NAME']));
// URI path to the app (i.e.http://example.org/realtime)
define(
'APP_URI',
remove_unwanted_slashes('http://' . $_SERVER['SERVER_NAME'] . APP_FOLDER)
);
// Server path to the system folder (for includes)
define('SYS_PATH', APP_PATH . '/system');
// Relative path to the form processing script (i.e. /realtime/process.php)
define('FORM_ACTION', remove_unwanted_slashes(APP_FOLDER . '/process.php'));
//-----------------------------------------------------------------------------
// Initializes the app
//-----------------------------------------------------------------------------
// Starts the session
if (!isset($_SESSION)) {
session_start();
}
// Loads the configuration variables
require_once SYS_PATH . '/config/config.inc.php';
// Turns on error reporting if in debug mode
if (DEBUG===TRUE) {
ini_set('display_errors', 1);
error_reporting(E_ALL^E_STRICT);
} else {
ini_set('display_errors', 0);
error_reporting(0);
}
// Sets the timezone to avoid a notice
date_default_timezone_set(APP_TIMEZONE);
// Registers class_loader() as the autoload function
spl_autoload_register('class_autoloader');
//-----------------------------------------------------------------------------
// Function declarations
//-----------------------------------------------------------------------------
/**
* Breaks the URI into an array at the slashes
*
* @return array The broken up URI
*/
function parse_uri( )
{
// Removes any subfolders in which the app is installed
$real_uri = preg_replace(
'∼^'.APP_FOLDER.'∼',
'',
$_SERVER['REQUEST_URI'],
1
);
$uri_array = explode('/', $real_uri);
// If the first element is empty, get rid of it
if (empty($uri_array[0])) {
array_shift($uri_array);
}
// If the last element is empty, get rid of it
if (empty($uri_array[count($uri_array)-1])) {
array_pop($uri_array);
}
return $uri_array;
}
/**
* Determines the controller name using the first element of the URI array
*
* @param $uri_array array The broken up URI
* @return string The controller classname
*/
function get_controller_classname( &$uri_array )
{
$controller = array_shift($uri_array);
return ucfirst($controller);
}
/**
* Removes unwanted slashes (except in the protocol)
*
* @param $dirty_path string The path to check for unwanted slashes
* @return string The cleaned path
*/
function remove_unwanted_slashes( $dirty_path )
{
return preg_replace('∼(?<!:)//∼', '/', $dirty_path);
}
/**
* Autoloads classes as they are instantiated
*
* @param $class_name string The name of the class to be loaded
* @return bool Returns TRUE on success (Exception on failure)
*/
function class_autoloader( $class_name )
{
$fname = strtolower($class_name);
// Defines all of the valid places a class file could be stored
$possible_locations = array(
SYS_PATH . '/models/class.' . $fname . '.inc.php',
SYS_PATH . '/controllers/class.' . $fname . '.inc.php',
SYS_PATH . '/core/class.' . $fname . '.inc.php',
);
// Loops through the location array and checks for a file to load
foreach ($possible_locations as $loc) {
if (file_exists($loc)) {
require_once $loc;
return TRUE;
}
}
// Fails because a valid class wasn't found
throw new Exception("Class $class_name wasn't found.");
}
完成路由
创建了实用程序函数并准备好自动加载器来加载所请求的控制器后,路由脚本的最后一步是实际处理请求控制器的 URI 组件,并最终将正确的视图发送给用户。
加载控制器
在index.php
中,在初始化块和函数声明之间添加以下代码:
// Registers class_loader() as the autoload function
spl_autoload_register('class_autoloader');
//-----------------------------------------------------------------------------
// Loads and processes view data
//-----------------------------------------------------------------------------
// Parses the URI
$uri_array = parse_uri();
$class_name = get_controller_classname($uri_array);
$options = $uri_array;
// Sets a default view if nothing is passed in the URI (i.e. on the home page)
if (empty($class_name)) {
$class_name = 'Home';
}
// Tries to initialize the requested view, or else throws a 404 error
try {
$controller = new $class_name($options);
} catch (Exception $e) {
$options[1] = $e->getMessage();
$controller = new Error($options);
}
//-----------------------------------------------------------------------------
// Function declarations
//-----------------------------------------------------------------------------
使用效用函数,URI 被分解并存储在$uri_array
中。然后它被传递给get_controller_classname()
,?? 将控制器的类名存储在$class_name
中。剩余的 URI 组件储存在$options
中以备后用。
接下来,检查$class_name
以确保它不为空;如果是,则提供默认的类名“Home”。
最后,使用一个try...catch
块,实例化一个请求类型的新控制器对象,将$options
作为参数传递给构造函数。如果出现任何问题,就会创建一个新的Error
对象来显示错误消息。
注意你将在本章的后面构建Error
类。
输出视图
加载控制器后,除了输出标记之外,没有什么要做的了。在index.php
中,添加以下粗体代码:
//-----------------------------------------------------------------------------
// Loads and processes view data
//-----------------------------------------------------------------------------
// Parses the URI
$uri_array = parse_uri();
$class_name = get_controller_classname($uri_array);
$options = $uri_array;
// Sets a default view if nothing is passed in the URI (i.e. on the home page)
if (empty($class_name)) {
$class_name = 'Home';
}
// Tries to initialize the requested view, or else throws a 404 error
try {
$controller = new $class_name($options);
} catch (Exception $e) {
$options[1] = $e->getMessage();
$controller = new Error($options);
}
//-----------------------------------------------------------------------------
// Outputs the view
//-----------------------------------------------------------------------------
// Includes the header, requested view, and footer markup
require_once SYS_PATH . '/inc/header.inc.php';
$controller->output_view();
require_once SYS_PATH . '/inc/footer.inc.php';
//-----------------------------------------------------------------------------
// Function declarations
//-----------------------------------------------------------------------------
页眉和页脚标记——您将在本章稍后创建——是简单的包含,夹在它们之间的是对控制器对象的output_view()
方法的调用,该方法为请求的视图输出格式化的标记。
添加 URI 重写本
让路由运行的最后一步是添加.htaccess
文件,该文件将通过路由引导所有请求(除非直接请求文件或子目录)。
在应用的根目录下,创建一个名为.htaccess
的新文件,并插入以下代码:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.php [L]
</IfModule>
您可能需要更新RewriteBase
的值,以匹配应用的路径。一些服务器设置将在没有设置RewriteBase
的情况下运行,因此如果您遇到错误,请先尝试删除它,然后将其设置为您的应用的路径。
注意如果你是一名 WordPress 开发者,你可能会认出这段代码。这和 WordPress 网站上使用的重写规则是一样的。
设置核心类
因为应用中使用的每个控制器、视图和模型都有基线功能,所以在构建它们之前,你需要创建抽象类来容纳公共方法,并声明需要包含在扩展它们的所有类中的任何方法和/或属性存根。
什么是抽象类?
定义为 抽象的类不能被实例化,任何包含至少一个抽象方法的类也必须是抽象的。定义为抽象的方法只是声明方法的签名,它们不能定义实现。
从抽象类继承时,所有在父类声明中标记为抽象的方法必须由子类定义。]
—摘自 PHP 手册关于类抽象的词条 2
这意味着,抽象类允许开发人员创建充当其他类的模板的类,同时还提供公共功能。因为抽象类不能被直接实例化,所以可以创建方法“存根”,这要求子类声明那些方法并为它们提供功能。
举个简单的例子,一个抽象类可以定义一个人。所有人都睡觉和喝酒,所以抽象类应该定义那些方法。因为睡眠对所有人来说几乎都是一样的,这可以在课堂上定义。然而,不是所有的人都喝同样的东西;该动作应该被定义为要在子类中声明的存根。
下面是这个简单的例子在真实代码中的样子:
abstract class Person
{
public $rest = 0,
$drinks = array();
public function sleep( )
{
++$rest;
}
abstract public function drink( );
}
class Jason extends Person
{
private $_wishes = array(
'a little bit taller',
'a baller',
'a girl who looks good',
'a rabbit in a hat',
'a bat',
'a \'64 Impala',
);
public function drink( )
{
$this->drinks[] = 'coffee';
}
public function wish( )
{
$wish_index = mt_rand(0, count($this->_wishes)-1);
return $this->_wishes[$wish_index];
}
}
class Phil extends Person
{
public function drink( )
{
$this->drinks[] = 'tea';
}
}
Person
类设置了通用的属性,以及一个所有扩展该类的类(人)将共享的sleep()
方法。
然后它定义了一个抽象方法drink()
。因为所有的人都喝,但不是所有的人都喝同样的饮料,这个方法需要被要求,但没有被定义;这就是抽象类的强大之处。
现在,当定义了Jason
和Phil
类时,基础就已经奠定了——不需要额外的代码来允许休眠——所以完成这些类的代码非常简单。
同样值得注意的是,扩展抽象类的类不受所提供的方法和方法存根的限制;可以根据需要声明附加的属性和方法。例如,Jason
类包含一个私有属性——$_wishes
——和一个名为wish()
的附加方法。 3
创建抽象控制器类
Controller
类非常简单:它需要检查所需的数据是否被传入,并为生成页面标题和解析视图定义方法存根。
创建一个名为class.controller.inc.php
的新文件,并保存在system/core/
子目录中。在内部,粘贴以下代码:
<?php
/**
* An abstract class that lays the groundwork for all controllers
*
* @author Jason Lengstorf <jason@lengstorf.com>
* @author Phil Leggetter <phil@leggetter.co.uk>
*/
abstract class Controller
{
public $actions = array(),
$model;
protected static $nonce = NULL;
/**
* Initializes the view
*
* @param $options array Options for the view
* @return void
*/
public function __construct( $options )
{
if (!is_array($options)) {
throw new Exception("No options were supplied for the room.");
}
}
/**
* Generates a nonce that helps prevent XSS and duplicate submissions
*
* @return string The generated nonce
*/
protected function generate_nonce( )
{
// TODO: Add the actual nonce generation script
return "tempnonce";
}
/**
* Performs basic input sanitization on a given string
*
* @param $dirty string The string to be sanitized
* @return string The sanitized string
*/
protected function sanitize( $dirty )
{
return htmlentities(strip_tags($dirty), ENT_QUOTES);
}
/**
* Sets the title for the view
*
* @return string The text to be used in the <title> tag
*/
abstract public function get_title( );
/**
* Loads and outputs the view's markup
*
* @return void
*/
abstract public function output_view( );
}
__construct()
方法检查中的选项数组(将是由index.php
中的parse_uri()
提取的数组),如果没有提供选项,则抛出Exception
。
目前,generate_nonce()
方法只是返回一个临时字符串。在本章后面的“处理表单提交”一节中,您将再次用到这个方法。
sanitize()
方法做一些非常基本的输入净化,这应该总是在用户提供的数据上执行。
在本章的后面,你将构建一个扩展这个类的控制器,但是首先你需要一个处理视图的方法(否则output_view()
方法会崩溃)。
创建视图类
为了让显示输出,你需要一个View
类。本质上,这个类将加载由Controller
类请求的视图并返回它们。但是,因为显示的数据会因房间而异,View
类也需要将数据插入视图;这将需要一个简单的设置器实现,我们将在稍后讨论。
在system/core/
子目录中,创建一个名为class.view.inc.php
的新文件。在内部,添加以下代码:
<?php
/**
* Parses template files with loaded data to output HTML markup
*
* @author Jason Lengstorf <jason@lengstorf.com>
* @author Phil Leggetter <phil@leggetter.co.uk>
*/
class View
{
protected $view,
$vars = array();
/**
* Initializes the view
*
* @param $view array The view slug
* @return void
*/
public function __construct( $view=NULL ) {
if (!$view) {
throw new Exception("No view slug was supplied.");
}
$this->view = $view;
}
/**
* Stores data for the view into an array
*
* @param $key string The variable name
* @param $var string The variable value
* @return void
*/
public function __set( $key, $var ) {
$this->vars[$key] = $var;
}
/**
* Loads and parses the selected template using the provided data
*
* @param $print boolean Whether the markup should be output directly
* @return mixed A string of markup if $print is TRUE or void
*/
public function render( $print=TRUE ) {
// Converts the array of view variables to individual variables
extract($this->vars);
// Checks to make sure the requested view exists
$view_filepath = SYS_PATH . '/views/' . $this->view . '.inc.php';
if (!file_exists($view_filepath)) {
throw new Exception("That view file doesn't exist.");
}
// Turns on output buffering if markup should be returned, not printed
if (!$print) {
ob_start();
}
require $view_filepath;
// Returns the markup if requested
if (!$print) {
return ob_get_clean();
}
}
}
注意你可能已经注意到View
类不是抽象的。这是因为它的功能不需要在这个应用中扩展。
从顶部开始,这个类定义了两个属性:$view
,存储要加载的视图的名称;和$vars
,这是一个特定于视图的键值对数组,用于定制输出。
__construct()
方法检查视图段或标识视图的字符串,并将其存储在对象中以备后用,如果没有提供,则抛出Exception
。
__set()
方法是另一个神奇的方法(我们将在下一节详细讨论),类似于__construct()
,它允许将数据作为属性存储在对象中,即使属性没有明确定义。这为在视图中添加输出变量创建了一个快捷方式。
最后,render()
函数使用函数extract()
将所有定制属性存储到变量中,检查有效的视图文件,并根据$print
标志打印或返回视图的标记。
为什么 SETTERS 有用
像 PHP 中所有其他神奇的方法一样,__set()
方法实际上并没有那么神奇;它只是提供了一条捷径来做一些原本会很麻烦的事情。
例如,我们的各种视图将不会共享相同的输出变量:一个房间将有一个标题和演讲者,而问题视图将有问题文本和投票数。
虽然您可以在各自的视图中显式声明每个属性,但是如果将来添加了额外的属性,就会增加维护的麻烦。
或者,您可以使用一个专用属性来保存每个视图的自定义变量数组:
<?php
class RWA_Example
{
public $vars = array();
}
$test = new RWA_Example;
// Sets custom variables
$test->vars['foo'] = 'bar';
$test->vars['bat'] = 'baz';
// Gets custom variables
echo $test->vars['foo'];
echo $test->vars['bat'];
这是一个完全可以接受的解决方案,但是输入起来有点笨拙。
使用 magic setter 方法通过提供一种快捷方式简化了这个过程:简单地设置属性,就像它们被显式声明一样,然后使用__set()
将它们放入一个数组中。
为了检索定制变量,还有另一个神奇的方法叫做__get()
。
下面是上一个使用 getters 和 setters 的例子:
<?php
class RWA_Example
{
protected $magic = array();
public function __set( $key, $val )
{
$this->magic[$key] = $val;
}
public function __get( $key )
{
return $this->magic[$key];
}
}
$test = new RWA_Example;
// Sets custom properties
$test->foo = 'bar';
$test->bat = 'baz';
// Gets custom properties
echo $test->foo;
echo $test->bat;
这极大地提高了代码的可读性,降低了输入错误的风险,从而为动态地向对象添加属性创建了一个有效的快捷方式。
创建抽象模型类
需要的最后一个核心类是Model
类,这是三个类中最简单的一个。对于这个应用,所有的Model
类需要做的就是创建一个数据库连接。
在system/core/
子目录中,创建一个名为class.model.inc.php
的新文件,并插入以下代码:
<?php
/**
* Creates a set of generic database interaction methods
*
* @author Jason Lengstorf <jason@lengstorf.com>
* @author Phil Leggetter <phil@leggetter.co.uk>
*/
abstract class Model
{
public static $db;
/**
* Creates a PDO connection to MySQL
*
* @return boolean Returns TRUE on success (dies on failure)
*/
public function __construct( ) {
$dsn = 'mysql:dbname=' . DB_NAME . ';host=' . DB_HOST;
try {
self::$db = new PDO($dsn, DB_USER, DB_PASS);
} catch (PDOExeption $e) {
die("Couldn't connect to the database.");
}
return TRUE;
}
}
__construct()
方法试图使用存储在system/config/config.inc.php
中的值创建一个新的 MySQL 连接,如果连接失败,将抛出一个Exception
。
注意我们使用 PHP 数据对象(PDO) 4 进行数据库访问,因为它提供了一个简单的接口,如果使用得当,几乎不可能使用 SQL 注入。
添加页眉和页脚标记
在实际构建应用页面之前的最后一步是将页眉和页脚标记添加到应用中以供通用。
从最简单的文件开始,在system/inc/
中创建一个名为footer.inc.php
的新文件,并插入您在第七章中构建的页脚标记:
<footer>
<ul>
<li class="copyright">
© 2013 Jason Lengstorf & Phil Leggetter
</li><!--/.copyright-->
<li>
Part of <em>Realtime Web Apps: With HTML5 WebSocket, PHP,
and jQuery</em>.
</li>
<li>
<a href="http://amzn.to/XKcBbG">Get the Book</a> |
<a href="http://cptr.me/UkMSmn">SourceCode (on GitHub)</a>
</li>
</ul>
</footer>
</body>
</html>
页脚标记中没有什么值得注意的;然而,头部引入了我们第一个需要可变数据的标记。让我们从创建标记开始,然后在下一节处理变量的设置。
创建一个名为header.inc.php
的新文件,并保存在system/inc/
中,其中包含以下标记(变量以粗体显示):
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title><?php echo $title; ?></title>
<!-- Fonts via fonts.com -->
<script type="text/javascript"
src="http://fast.fonts.com/jsapi/6a912a6e-163c-4c8b-afe0-e3d22ffab02e.js"></script>
<!-- Main site styles -->
<link rel="stylesheet" href="<?php echo $css_path; ?>" />
</head>
<body>
<header>
<h1>Realtime Q&A</h1>
<p class="tagline">
A live feedback system for classes, presentations, and conferences.
</p><!--/.tagline-->
</header>
该标记设置 HTML5 文档类型和基本元数据,使用变量设置页面标题,然后加载字体样式表(来自 Fonts.com)。站点样式表的位置存储在一个变量中,因为它的位置需要相对于应用的安装来确定,以避免破坏 URI。
因为应用在每个页面的顶部共享通用的标题标记,所以这也包括在内。
为标题设置变量
为了设置标题的变量,再次打开index.php
并添加以下代码,以粗体显示:
//-----------------------------------------------------------------------------
// Outputs the view
//-----------------------------------------------------------------------------
// Loads the <title> tag value for the header markup
$title = $controller->get_title();
// Sets the path to the app stylesheet for the header markup
$dirty_path = APP_URI . '/assets/styles/main.css';
$css_path = remove_unwanted_slashes($dirty_path);
// Includes the header, requested view, and footer markup
require_once SYS_PATH . '/inc/header.inc.php';
$controller->output_view();
require_once SYS_PATH . '/inc/footer.inc.php';
//-----------------------------------------------------------------------------
// Function declarations
//-----------------------------------------------------------------------------
使用控制器对象的get_title()
方法设置第一个变量$title
。
接下来,使用APP_URI
常量和样式表的路径生成样式表路径,在存储到$css_path
用于输出之前,检查样式表的双斜线。
这个样式表路径变量是不是大材小用了?
乍一看,生成$css_path
变量的步骤似乎是不必要的,URI 可以很容易地硬编码到标题标记中。毕竟档案一直在assets/styles/main.css
吧?
因为我们使用 URI 重写,所以我们不能使用相对路径(即href="./assets/styles/main.css"
)。
使用绝对 URI,比如href="/assets/styles/main.css"
,只要应用安装在服务器的根目录下就可以了。
然而,如果应用安装在子目录中,绝对 URI 需要编辑,以包括子目录路径,我们不希望每次安装都需要编辑标题标记。
因此,将两行代码放在一起确定样式表的绝对路径是避免大麻烦的一件小事。
将样式表和资产复制到正确的目录中
现在头部标记已经就位并引用了样式表,您应该将第七章中的main.css
复制到assets/styles/
文件夹中。你也应该复制图像精灵到assets/img/
。
注意为了让事情向前发展,本章将不再提及或打印 CSS 标记和资产。别忘了把第七章的main.css
抄过来,不然 app 跟后面的截图不匹配。
建立主页
路由创建完毕。核心类都写好了。通用标记已经准备就绪。剩下要做的就是创建应用的第一个实际页面。
首先从最简单的页面开始,让我们构建主页。
创建家庭控制器
首先,在system/controllers/
中创建一个名为class.home.inc.php
的新文件。在内部,添加以下代码:
<?php
/**
* Generates output for the Home view
*
* @author Jason Lengstorf <jason@lengstorf.com>
* @author Phil Leggetter <phil@leggetter.co.uk>
*/
class Home extends Controller
{
/**
* Overrides the parent constructor to avoid an error
*
* @return bool TRUE
*/
public function __construct( )
{
return TRUE;
}
/**
* Generates the title of the page
*
* @return string The title of the page
*/
public function get_title( )
{
return 'Realtime Q&A';
}
/**
* Loads and outputs the view's markup
*
* @return void
*/
public function output_view( )
{
$view = new View('home');
$view->render();
}
}
正如我们在构建抽象的Controller
类时讨论的那样,Home
类扩展了Controller
。
首先—因为主页不接受任何参数—构造函数被覆盖以避免Exception
。
然后声明了get_title()
,它简单地返回了在<title>
标签中使用的应用的名称。
最后,output_view()
方法创建了一个View
类的新实例,并调用它的render()
方法来输出标记。
接下来,让我们创建主页标记并为输出生成任何必要的变量。
创建主视图
主页的标记将保存在一个名为home.inc.php
的文件中,该文件应该在system/views/
中创建并保存。在里面,添加你在第七章中创建的 home 标记(需要由变量设置的部分已经加粗):
<section>
<form id="attending" method="post"
action=" <?php echo $join_action; ?>">
<h2>Attending?</h2>
<p>Join a room using its ID.</p>
<label>
What is the room's ID?
<input type="text" name="room_id" />
</label>
<input type="submit" value="Join This Room" />
<input type="hidden" name="nonce"
value=" <?php echo $nonce; ?>" />
</form><!--/#attending-->
<form id="presenting" method="post"
action=" <?php echo $create_action; ?>">
<h2>Presenting?</h2>
<p>Create a room to start your Q&A session.</p>
<label>
Tell us your name (so attendees know who you are).
<input type="text" name="presenter-name" />
</label>
<label>
Tell us your email (so attendees can get in touch with you).
<input type="email" name="presenter-email" />
</label>
<label>
What is your session called?
<input type="text" name="session-name" />
</label>
<input type="submit" value="Create Your Room" />
<input type="hidden" name="nonce"
value=" <?php echo $nonce; ?>" />
</form><!--/#presenting-->
</section>
这个视图需要三个变量:
$join_action
:允许用户加入现有房间的表单动作$nonce
:一个安全令牌,防止表单被欺诈或重复提交$create_action
:允许用户创建新房间的表单动作
两个表单中都使用了$nonce
,因为没有办法同时提交两个表单(即使有,这个应用也不支持这种行为)。
生成输出变量
要创建输出主视图的变量,请返回主控制器(class.home.inc.php
)并添加以下粗体代码:
/**
* Loads and outputs the view's markup
*
* @return void
*/
public function output_view( )
{
$view = new View('home');
$view->nonce = $this->generate_nonce();
// Action URIs for form submissions
$view->join_action = APP_URI . 'room/join';
$view->create_action = APP_URI . 'room/create';
$view->render();
}
使用您在View
类中创建的 setter,添加变量就像在View
对象中声明新属性一样简单。
注意记住,这些属性被View
class’ render()
方法转换成独立的变量,所以无论您为属性选择什么名称,都是视图可用的变量名(也就是说,$view->nonce
作为$nonce
对视图可用)。
让应用第一次旋转
随着主视图的完成,你的应用终于可以在浏览器中加载了。在你的浏览器中导航到应用——本书假设应用安装在http://rwa.local/
——你会看到在第七章中设计的主页(见图 8-1 )。
图 8-1。主视图,由应用生成
添加错误处理程序
接下来,应用需要一个错误处理程序。当前,如果访问了不工作的 URI(如http://rwa.loca/not-real/
),会显示一条难看的“未发现异常”错误信息(见图 8-2 )。
图 8-2。不工作的 URIs 导致显示难看的错误
如果出现问题,应用已经在尝试加载Error
类,所以您需要做的就是构建控制器和视图来捕捉错误,并以易读的方式显示它们。
创建错误控制器
首先,创建一个名为class.error.inc.php
的新文件,并将其保存在system/controllers/
中。在内部放置以下代码:
<?php
/**
* Processes output for the Room view
*
* @author Jason Lengstorf <jason@lengstorf.com>
* @author Phil Leggetter <phil@leggetter.co.uk>
*/
class Error extends Controller
{
private $_message = NULL;
/**
* Initializes the view
*
* @param $options array Options for the view
* @return void
*/
public function __construct( $options )
{
if (isset($options[1])) {
$this->_message = $options[1];
}
}
/**
* Generates the title of the page
*
* @return string The title of the page
*/
public function get_title( )
{
return 'Something went wrong.';
}
/**
* Loads and outputs the view's markup
*
* @return void
*/
public function output_view( )
{
$view = new View('error');
$view->message = $this->_message;
$view->home_link = APP_URI;
$view->render();
}
}
Error 类有一点不同,它声明了一个私有属性$_message
,该属性将存储Exception
类的错误消息。
构造函数将提供的错误消息存储在$_message
中(如果提供了一个的话),并且get_title()
返回一个针对<title>
标签的通用错误消息。
view()
方法只是将错误消息和主页 URI 添加到视图对象中,以便在标记中使用;然后呈现输出。
创建错误视图
为了显示Error
控制器的结果,在system/views/
中创建一个名为error.inc.php
的新文件,并添加以下标记:
<section id="error">
<h2>
I’m sorry, Dave.<br />
I’m afraid I can’t do that.
</h2>
<p>
Sorry, but something went wrong. Maybe the error message below
will help.
</p>
<p><code> <?php echo $message; ?></code></p>
<p>
<a href=" <?php echo $home_link; ?>">← go back to the home page</a>
</p>
</section>
$message
变量输出提供给Exception
的实际信息。“返回主页”链接使用$home_link
链接到主页。
添加特定于错误的样式
错误页面需要对样式表进行一些小的调整,以便正确显示,所以打开assets/styles/main.css
并在媒体查询上方添加以下内容:
/* Error Styling
----------------------------------------------------------------------------*/
section#error { text-align: center; }
section#error p { margin: 0 auto 2em; }
测试错误页面
为了验证一切正常,访问浏览器中的断开链接(http://rwa.local/not-real/
)以查看错误页面(参见图 8-3 )。
图 8-3。错误页面现在更容易阅读了
建立数据库
在我们可以在应用中走得更远之前,需要建立数据库。剩下的两个控制器——问题和房间——都存储数据,因此需要模型。
我们已经在第五章中讨论了如何构建数据库,所以我们将直接进入这里的代码。在 phpMyAdmin 中,终端,或者你喜欢的执行 MySQL 查询的方法,运行下面的命令:
CREATE TABLE IF NOT EXISTS 'presenters' (
'id' int(11) NOT NULL AUTO_INCREMENT,
'name' varchar(255) COLLATE utf8_unicode_ci NOT NULL,
'email' varchar(255) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY ('id'),
UNIQUE KEY 'email' ('email')
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
CREATE TABLE IF NOT EXISTS 'questions' (
'id' int(11) NOT NULL AUTO_INCREMENT,
'room_id' int(11) NOT NULL,
'question' text COLLATE utf8_unicode_ci NOT NULL,
'is_answered' tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY ('id'),
KEY 'room_id' ('room_id')
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
CREATE TABLE IF NOT EXISTS 'question_votes' (
'question_id' int(11) NOT NULL,
'vote_count' int(11) NOT NULL,
PRIMARY KEY ('question_id')
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
CREATE TABLE IF NOT EXISTS 'rooms' (
'id' int(11) NOT NULL AUTO_INCREMENT,
'name' varchar(255) COLLATE utf8_unicode_ci NOT NULL,
'is_active' tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY ('id')
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
CREATE TABLE IF NOT EXISTS 'room_owners' (
'room_id' int(11) NOT NULL,
'presenter_id' int(11) NOT NULL,
KEY 'room_id' ('room_id','presenter_id')
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
这段代码创建了应用运行所需的数据库表。如果您在 phpMyAdmin 中查看数据库,您将看到新创建的表(参见图 8-4 )。
图 8-4。在 phpMyAdmin 中查看的数据库表
处理表单提交
任何应用的主要组成部分之一是它接受表单提交的方式。为了最大限度地提高应用的效率,有必要花些时间来计划如何组织、清理、处理和存储所有表单提交。
规划表单提交工作流程
首先,我们需要建立表单提交工作流:
- 用户向适当的控制器提交一个表单,这由表单的
action
属性决定。 - 控制器识别表单提交并检查有效的动作。
- 如果找到了有效的动作,则使用 nonce 检查提交的合法性。
- 有效的提交被发送到操作指定的方法。
- handler 方法处理数据,将其交给模型进行存储,并返回一个布尔标志来指示成功或失败。
- 用户被重定向到正确的页面(或者,在许多情况下,页面只是用新数据更新)。
有了这个工作流,您现在可以开始充实表单提交过程的不同步骤了。
设置并检查有效动作
在本章的前面,你设置了抽象的Controller
类,它有一个名为$actions
的属性。这个属性将被Room
和Question
控制器用来定义一组动作和它们相应的方法。
每个控制器都有自己独特的动作,所以数组需要在Controller
类的构造函数中声明。带有动作的示例控制器可能如下所示:
class Example extends Controller
{
public function __construct( $options )
{
parent::__construct($options);
$this->model = new Example_Model;
$this->actions = array(
'action-one' => 'say_foo',
);
if (array_key_exists($options[0], $this->actions)) {
$this->handle_form_submission($options[0]);
exit;
}else {
// If we get here, no valid form was submitted...
}
}
/* get_title() and output_view() would go here */
}
上面重要的部分被加粗了。现在,忽略new Example_Model
位;我们会在几页后讨论这个问题。
$actions
数组被设置为一个键-值对,其中键是动作的名称(由提交 URI 触发),值是处理表单的方法的名称。
构造函数添加了一个if...else
检查来查看是否到达了有效的表单提交 URI。如果是这样,它将触发尚未编写的handle_form_submission()
方法。
要触发一个动作,表单需要提交给一个 URI,它有类名、一个正斜杠,然后是动作:
<form action="http://rwa.local/example/action-one">...</form>
处理动作的方法也需要添加到这个类中,但是我们将在本节的稍后部分讨论这一点。
防止重复或欺诈提交
为了防止错误的、重复的或欺诈性的表单提交,您需要实现一个nonce——或nnumber usedonce——以确保每个表单提交都来自一个有效的表单并且是第一次提交。
创建随机数
要创建一个 nonce,您需要做的就是为每个用户在每次页面加载时生成一个随机字符串。然后,这个随机数被添加为当前视图中加载的任何表单的隐藏表单字段,并存储在用户的会话中。
打开system/core/class.controller.inc.php
并将以下粗体代码添加到generate_nonce()
方法中:
protected function generate_nonce( )
{
// Checks for an existing nonce before creating a new one
if (empty(self::$nonce)) {
self::$nonce = base64_encode(uniqid(NULL, TRUE));
$_SESSION['nonce'] = self::$nonce;
}
return self::$nonce;
}
该方法首先检查$nonce
是否为空,因为 app 中经常会显示多个表单;如果第一个表单的 nonce 被覆盖,它就不能成功提交,这会破坏应用。
如果 nonce 没有设置,则通过生成一个uniqid()
然后用base64_encode()
对其进行编码来生成一个新的 nonce。这既作为静态属性存储在对象中(因此所有基于Controller
的类在其视图中使用相同的 nonce ),也存储在$_SESSION
超全局中,以便在提交后验证 nonce。
检查随机数
提交表单时,首先需要检查的是通过表单提交的随机数是否与会话中存储的随机数相匹配。如果它们不匹配,就说明有问题,提交的内容不应该被处理。
要检查 nonce,用下面的粗体代码向system/core/class.controller.inc.php
添加一个名为check_nonce()
的新方法:
protected function generate_nonce( )
{
// Checks for an existing nonce before creating a new one
if (empty(self::$nonce)) {
self::$nonce = base64_encode(uniqid(NULL, TRUE));
$_SESSION['nonce'] = self::$nonce;
}
return self::$nonce;
}
/**
* Checks for a valid nonce
*
* @return bool TRUE if the nonce is valid; otherwise FALSE
*/
protected function check_nonce( )
{
if (
isset($_SESSION['nonce']) && !empty($_SESSION['nonce'])
&& isset($_POST['nonce']) && !empty($_POST['nonce'])
&& $_SESSION['nonce']===$_POST['nonce']
) {
$_SESSION['nonce'] = NULL;
return TRUE;
} else {
return FALSE;
}
}
/**
* Performs basic input sanitization on a given string
*
* @param $dirty string The string to be sanitized
* @return string The sanitized string
*/
protected function sanitize( $dirty )
{
return htmlentities(strip_tags($dirty), ENT_QUOTES);
}
该方法检查三个标准:
- 随机数存储在会话中
- 随机数是和表单一起提交的
- 会话和表单中的随机数是相同的
如果所有三个条件都满足,nonce 将从会话中删除(因此表单无法再次成功提交),并返回 Boolean TRUE
以表示 nonce 检查成功。
编写表单处理方法
要实际处理表单提交,您需要三种方法:
- 第一个将检查 nonce,执行 action handler 方法,并将用户重定向到正确的位置,等待成功或失败。
- 第二个是前面提到的动作处理程序,它实际上处理提交的表单数据。
- 第三种是模型方法,它从动作处理程序获取处理过的数据,并相应地操作数据库。
添加主窗体处理方法
第一个方法将驻留在system/core/class.controller.inc.php
中,称为handle_form_submission()
。它接受一个参数:动作。
将以下粗体代码添加到Controller
类中:
protected function check_nonce( )
{
if (
isset($_SESSION['nonce']) && !empty($_SESSION['nonce'])
&& isset($_POST['nonce']) && !empty($_POST['nonce'])
&& $_SESSION['nonce']===$_POST['nonce']
) {
$_SESSION['nonce'] = NULL;
return TRUE;
} else {
return FALSE;
}
}
/**
* Handles form submissions
*
* @param $action string The form action being performed
* @return void
*/
protected function handle_form_submission( $action )
{
if ($this->check_nonce()) {
// Calls the method specified by the action
$output = $this->{$this->actions[$action]}();
if (is_array($output) && isset($output['room_id'])) {
$room_id = $output['room_id'];
} else {
throw new Exception('Form submission failed.');
}
header('Location: ' . APP_URI . 'room/' . $room_id);
exit;
} else {
throw new Exception('Invalid nonce.');
}
}
/**
* Performs basic input sanitization on a given string
*
* @param $dirty string The string to be sanitized
* @return string The sanitized string
*/
protected function sanitize( $dirty )
{
return htmlentities(strip_tags($dirty), ENT_QUOTES);
}
该方法从验证随机数开始;然后,它调用操作处理程序方法,并将输出存储在一个变量中。对于这个应用,每个操作都将返回一个房间 ID,因此该方法检查以确保返回了一个房间 ID。使用房间 ID,用户然后被重定向到她应该查看的房间。
向控制器添加动作方法
为了实际处理提交的表单数据,需要向输出表单的Controller
类添加一个新方法。使用我们之前的Example
类,定义的两个动作需要将名为say_foo()
和say_bar()
的方法添加到Example
类中,这里用粗体显示:
class Example extends Controller
{
public function __construct( $options )
{
parent::__construct($options);
$this->actions = array(
'action-one' => 'say_foo',
);
if (array_key_exists($options[0], $this->actions)) {
$this->handle_form_submission($options[0]);
exit;
} else {
// If we get here, no form was submitted...
}
}
/* get_title() and output_view() would go here */
protected function say_foo( )
{
$room_id = $this->sanitize($_POST['room_id']);
$sayer_id = $this->sanitize($_POST['sayer_id']);
echo 'Foo!';
return $this->model->update_foo_count($room_id, $sayer_id);
}
}
首先,这个方法获取从提交的表单传递来的任何数据,对其进行清理,并将其存储在一个变量中。尽管这个方法只是一个例子,但是$room_id
被传递来跟随应用中存在的动作处理程序。
接下来,动作处理程序执行请求的动作:在本例中,输出字符串“Foo!”对着屏幕。
最后,它执行模型方法并返回结果。
向模型类添加方法
表单处理链的最后一步是添加一个模型方法。但首先,我们需要一个模型类。
幸运的是,这个应用将使事情变得简单,所以模型类简单地扩展了Model
类,这使它们能够访问 PDO 驱动的数据库连接,然后声明所需的方法。不需要额外的设置。
继续我们的Example
类,我们需要创建前面实例化的Example_Model
类。该类需要的唯一方法是由动作处理程序方法调用的update_foo_count()
方法,该方法只需要增加已经发生的“foo”的计数,并返回一个数据数组。
这看起来是这样的:
class Example_Model extends Model
{
public function vote_question( $room_id, $sayer_id )
{
// Increments the vote count for the question
$sql = "UPDATE sayings
SET foo_count = foo_count+1
WHERE sayer_id = :sayer_id";
$stmt = self::$db->prepare($sql);
$stmt->bindParam(':sayer_id', $sayer_id, PDO::PARAM_INT);
$stmt->execute();
$stmt->closeCursor();
return array(
'room_id' => $room_id,
'sayer_id' => $sayer_id,
);
}
}
这是将在此应用中构建的所有模型的基本模式。该方法创建一个 SQL 语句,使用 PDO 准备它,然后执行所需的任何数据库操作(在本例中是更新)。
执行查询后,将返回一个包含相关数据的数组。
注意该应用中的模型严重依赖于预处理语句,这比标准的 SQL 查询安全得多。要完成本书中的模型,你不需要知道比上一个例子中显示的更多的东西,但是如果你需要重温一下 PDO,请访问位于http://php.net/pdo
的 PHP 手册。
摘要
这一章很难懂。至此,您已经成功地为您的应用构建了一个基于 MVC 原则的框架。
然而,在后端完成之前,您还有一段路要走。在下一章中,您将为房间和问题数据类型构建控制器、视图和模型。
1 RegEx 复杂到足以配得上它自己的书,这本书已经在http://www.regular-expressions.info/
方便地在线编译了
2
3 菲尔不愿意。他计划。
4*