PHP-MySQL-和-JavaScript-学习指南第六版-十-

42 阅读41分钟

PHP、MySQL 和 JavaScript 学习指南第六版(十)

原文:zh.annas-archive.org/md5/4aa97a1e8046991cb9f8d5f0f234943f

译者:飞龙

协议:CC BY-NC-SA 4.0

第二十七章:HTML5 音频与视频

互联网增长的最大驱动力之一是用户对音频和视频形式的多媒体需求无止境的追求。最初,带宽非常宝贵,几乎没有实时流媒体的概念,下载音频轨道甚至视频可能需要几分钟甚至几小时。

带宽的高昂成本和快速调制解调器的有限可用性推动了更快速、更有效率的压缩算法的发展,例如 MP3 音频和 MPEG 视频,但即使是那时,以合理的时间下载文件的唯一方法也是大幅降低它们的质量。

我早期的一个互联网项目是 1997 年英国的第一个由音乐管理机构授权的在线广播电台。实际上,它更像是一个播客(在这个术语被创造出来之前),因为我们每天制作一个半小时的节目,然后使用最初为电话系统开发的算法将其压缩到 8 位、11 千赫单声道,听起来像电话质量,甚至更差。尽管如此,我们迅速获得了成千上万的听众,他们会下载节目,并在弹出的浏览器窗口中使用插件的方式收听其中讨论的网站。

幸运的是,对于我们和所有发布多媒体内容的人来说,很快就能提供更高的音频和视频质量,但仍需用户下载并安装插件播放器。Flash 成为这些播放器中最受欢迎的,击败了竞争对手如 RealAudio,但由于经常导致浏览器崩溃并要求用户不断升级新版本,它声名狼藉。

因此,普遍认为前进的道路是制定一些支持在浏览器中直接处理多媒体的 web 标准。当然,像微软和谷歌这样的浏览器开发者对这些标准的看法不同,但是当风尘定息时,他们已经同意了所有浏览器应该原生播放的文件类型的一个子集,并将其引入了 HTML5 规范。

最后,只要你把音频和视频编码成几种不同格式,就可以将多媒体上传到 web 服务器,在网页中加入一些 HTML 标签,然后在任何主流桌面浏览器、智能手机或平板设备上播放媒体,用户无需下载插件或做任何其他更改。

关于编解码器

术语编解码器代表编码器/解码器。它描述了软件提供的编解码音频和视频等媒体的功能。在 HTML5 中,根据所使用的浏览器,有多组不同的编解码器可用。

与图形和其他传统网页内容不太相关的音频和视频方面的一个复杂情况是格式和编解码器的许可问题。许多格式和编解码器需付费使用,因为它们是由单个公司或公司联盟开发并选择了专有许可证。一些免费和开源的浏览器由于难以支付费用或者开发者基于原则反对专有许可证,因此不支持最流行的格式和编解码器。由于版权法在不同国家有所不同,并且许可证的执行较难,这些编解码器通常可以在网络上免费获取,但在您所在地可能在技术上是非法使用的。

以下是 HTML5 <audio>标签支持的编解码器(也适用于附加到 HTML5 视频的音频):

AAC

这种音频编解码器是专有的专利技术,通常使用*.aac*文件扩展名,称为高级音频编码。其 MIME 类型为audio/aac

FLAC

这种音频编解码器是由 Xiph.Org 基金会开发的自由无损音频编解码器,使用*.flac*扩展名,其 MIME 类型为audio/flac

MP3

这种音频编解码器称为 MPEG 音频层 3,已经存在多年。尽管这个术语经常(错误地)用来指代任何类型的数字音频,但它是一种专有的专利技术,使用*.mp3*扩展名。其 MIME 类型为audio/mpeg

PCM

这种音频编解码器称为脉冲编码调制,存储由模拟到数字转换器编码的完整数据,是音频 CD 上数据存储的格式。因为它不使用压缩,被称为无损编解码器,其文件通常比 AAC 或 MP3 文件大得多。通常使用*.wav*扩展名。其 MIME 类型为audio/wav,但也可能看到audio/wave

Vorbis

有时被称为 Ogg Vorbis,因为通常使用*.ogg*文件扩展名,这种音频编解码器没有专利限制,无需版税。其 MIME 类型为audio/ogg,或者 WebM 容器使用audio/webm

截至 2021 年中期,AAC、MP3、PCM 和 Vorbis 通常受到大多数操作系统和浏览器的支持(不包括微软已停止支持的 Internet Explorer),除了以下与 Safari 相关的异常情况:

Vorbis audio/ogg

MacOS 10.11 及更早版本的 Safari 需要 Xiph Quicktime。

Vorbis audio/webm

不支持 Safari。

FLAC audio/ogg

不支持 Safari(尽管audio/flac支持)。

因此,除非您真的有理由使用 Vorbis,否则现在通常安全地仅使用 AAC 或 MP3 进行压缩有损音频,FLAC 进行压缩无损音频,或 PCM 进行未压缩音频。

元素

为了适应各种平台,您可以使用多种编解码器记录或转换内容,然后在 <audio></audio> 标签内列出它们,例如 示例 27-1。嵌套的 <source> 标签包含您希望向浏览器提供的各种媒体。由于提供了 controls 属性,结果看起来像 图 27-1。

示例 27-1. 嵌入三种不同类型的音频文件
<audio controls>
  <source src='audio.m4a' type='audio/aac'>
  <source src='audio.mp3' type='audio/mp3'>
  <source src='audio.ogg' type='audio/ogg'>
</audio>

播放音频文件

图 27-1. 播放音频文件

在此示例中,我包含了三种不同的音频类型,因为这是完全可以接受的,如果您希望确保每个浏览器都能找到其首选格式而不仅仅是了解如何处理的一个格式。但是,您可以放弃 MP3 或 AAC 文件中的任何一个(但不能同时放弃),仍然可以确保示例在所有平台上播放。

<audio> 元素及其伴侣 <source> 标签支持多个属性:

autoplay

导致音频在准备就绪后立即开始播放

controls

导致显示控制面板

loop

设置音频循环播放

preload

导致音频在用户选择播放前即开始加载

src

指定音频文件的源位置

type

指定创建音频时使用的编解码器

如果您不向 <audio> 标签提供 controls 属性,并且也不使用 autoplay 属性,声音将不会播放,用户也无法点击播放按钮开始播放。这将使您不得不像 示例 27-2 中所示的那样,在 JavaScript 中提供此功能(需要额外的加粗显示的代码),以提供播放和暂停音频的功能,如 图 27-2 所示。

示例 27-2. 使用 JavaScript 播放音频
<!DOCTYPE html>
<html>
  <head>
    <title>Playing Audio with JavaScript</title>
    `<script` `src=``'OSC.js'``>``</script>`
  </head>
  <body>
    <audio `id=``'myaudio'`>
      <source src='audio.m4a' type='audio/aac'>
      <source src='audio.mp3' type='audio/mp3'>
      <source src='audio.ogg' type='audio/ogg'>
    </audio>

    `<button` `onclick=``'playaudio()'``>``Play Audio``</button>`
    `<button` `onclick=``'pauseaudio()'``>``Pause Audio``</button>`

    `<script``>`
      `function` `playaudio``(``)`
      `{`
        `O``(``'myaudio'``)``.``play``(``)`
      `}`
      `function` `pauseaudio``(``)`
      `{`
        `O``(``'myaudio'``)``.``pause``(``)`
      `}`
    </script>
  </body>
</html>

HTML5 音频可以通过 JavaScript 进行控制

图 27-2. HTML5 音频可以通过 JavaScript 进行控制

当用户点击按钮时,通过调用 myaudio 元素的 playpause 方法来实现。

<video> 元素

在 HTML5 中播放视频与音频非常类似;您只需使用 <video> 标签并为您提供的媒体提供 <source> 元素即可。 示例 27-3 展示了如何使用三种不同的视频编解码器类型执行此操作,如 图 27-3 所示。

示例 27-3. 播放 HTML5 视频
<video width='560' height='320' controls>
  <source src='movie.mp4'  type='video/mp4'>
  <source src='movie.webm' type='video/webm'>
  <source src='movie.ogv'  type='video/ogg'>
</video>

图 27-3. 播放 HTML5 视频

视频编解码器

与音频类似,多种视频编解码器可供选择,但不同浏览器的支持程度不同。这些编解码器有不同的容器格式,如下所示:

MP4

作为 MPEG-4 标准的一部分指定的许可受限、多媒体容器格式。其 MIME 类型为 video/mp4

Ogg

由 Xiph.Org Foundation 维护的免费开放容器格式。Ogg 格式的创建者表示其不受软件专利的限制。其 MIME 类型为 video/ogg

WebM

一种为 HTML5 视频设计的免版税、开放视频压缩格式。其 MIME 类型为 video/webm

这些可能包含以下视频编解码器之一:

H.264 & H.265

对最终用户免费播放专利、专有视频编解码器,但对编码和传输过程的所有部分可能会产生版税费用。H.265 在相同质量输出下支持几乎双倍的压缩比 H.264。

Theora

一种视频编解码器,不受专利的限制且在编码、传输和播放各个层面都不需要版税支付。

VP8

类似于 Theora 的视频编解码器,由谷歌拥有,并作为开源发布,因此免费使用。

VP9

与 VP8 类似但更强大,使用一半的比特率。

现在你可以确信几乎所有的现代浏览器都支持这些,但有例外,Theora video/ogg 不支持 iOS,而 macOS 早期版本需要 Xiph QuickTime。

因此,如果 iOS 是你的目标平台之一(通常是这样),你可能希望避开使用 Ogg,而可以在所有平台上安全地依赖 MP4 或 WebM,并暂时忘记其他格式。然而,在 Example 27-3 中,我展示了如果你愿意,可以添加所有三种主要视频类型,因为浏览器将选择它偏爱的格式。

<video> 元素和相应的 <source> 标签支持以下属性:

autoplay

导致视频在准备就绪后立即开始播放

controls

导致显示控制面板

height

指定视频显示的高度

loop

设置视频循环播放

muted

静音音频输出

poster

允许您选择一个图像,在视频播放时显示

preload

导致视频在用户选择播放之前开始加载

src

指定视频文件的源位置

type

指定创建视频时使用的编解码器

width

指定视频显示的宽度

如果您希望从 JavaScript 控制视频播放,可以使用如 示例 27-4 中所示的代码(需要额外的代码部分用粗体标出),其结果显示在 图 27-4 中。

示例 27-4. 从 JavaScript 控制视频播放
<!DOCTYPE html>
<html>
  <head>
    <title>Playing Video with JavaScript</title>
    `<script` `src=``'OSC.js'``>``</script>`
  </head>
  <body>
    <video `id=``'myvideo'` width='560' height='320'>
      <source src='movie.mp4'  type='video/mp4'>
      <source src='movie.webm' type='video/webm'>
      <source src='movie.ogv'  type='video/ogg'>
    </video><br>

    `<button` `onclick=``'playvideo()'``>``Play Video``</button>`
    `<button` `onclick=``'pausevideo()'``>``Pause Video``</button>`

    `<script``>`
      `function` `playvideo``(``)`
      `{`
        `O``(``'myvideo'``)``.``play``(``)`
      `}`
      `function` `pausevideo``(``)`
      `{`
        `O``(``'myvideo'``)``.``pause``(``)`
      `}`
    </script>
  </body>
</html>

此代码与从 JavaScript 控制音频的方式非常相似。只需调用 myvideo 对象的 play 和/或 pause 方法即可播放和暂停视频。

图 27-4. JavaScript 正在用于控制视频

使用本章中的信息,您将能够在几乎所有浏览器和平台上嵌入任何您喜欢的音频和视频,而不用担心用户是否能够播放。

在接下来的章节中,我将演示许多其他 HTML5 功能的使用,包括地理位置和本地存储。

问题

  1. 用于将音频和视频插入到 HTML5 文档中的两个 HTML 元素标签是哪两个?

  2. 为了确保在所有主要平台上能够播放,您应该提供哪两种压缩的有损音频编解码器(或在其中选择)?

  3. 要调用哪些方法来播放和暂停 HTML5 媒体播放?

  4. FLAC 是哪种格式类型?

  5. 为了确保在所有主要平台上能够播放,您应该选择哪两种视频编解码器(或在其中选择)?

查看 “第二十七章答案” 中有关这些问题的答案。

第二十八章:其他 HTML5 功能

在 HTML5 的最后一章中,我将解释如何使用地理位置和本地存储,并演示如何在浏览器中进行拖放操作,以及如何设置和使用 Web Workers 并利用跨文档消息传递。

严格来说,大多数这些特性(如 HTML5 的大部分)实际上并不是 HTML 的扩展,因为您是通过 JavaScript 而不是 HTML 标记访问它们。它们只是被浏览器开发人员采纳并被赋予便捷的 HTML5 统称。

这意味着,您需要彻底理解本书中的 JavaScript 教程,才能正确使用它们。话虽如此,一旦掌握了它们,您会想知道在没有这些强大新功能之前,您是如何生活的。

地理位置和 GPS 服务

全球定位系统(GPS)服务由多颗围绕地球轨道的卫星组成,它们的位置非常精确。当启用 GPS 的设备连接到这些卫星时,来自各个卫星的信号到达时间的不同,使得设备能够相当精确地确定自己的位置;因为光速(因而是无线电波)是已知的恒定值,信号从卫星到 GPS 设备的传输时间表明卫星的距离。

通过记录来自不同卫星的信号到达时间,这些卫星在任何时候的轨道位置都已精确确定,一个简单的三角测量计算就能让设备相对于卫星的位置精确到几米甚至更少。

许多移动设备,如手机和平板电脑,配备有 GPS 芯片并能提供此信息。但有些设备没有,有些设备已关闭,还有些设备可能在室内使用,被屏蔽了 GPS 卫星信号,因此无法接收任何信号。在这些情况下,可能会使用其他技术来尝试确定设备的位置。

警告

我还要提醒您考虑地理位置信息的隐私影响,特别是如果坐标作为应用程序功能的一部分传输回服务器时。任何具有地理位置功能的应用程序都应有明确的隐私政策。哦,顺便说一句,技术上地理位置信息实际上并不在 HTML5 标准中。事实上,它是由 W3C/WHATWG 定义的独立功能,但大多数人将其视为 HTML5 的一部分。

其他定位方法

如果您的设备具有手机硬件但没有 GPS 芯片,它可以尝试通过检查从各种通信塔接收到的信号的时序来三角测量其位置(这些塔可以通信,并且它们的位置非常精确)。如果有几座塔,这几乎可以达到 GPS 定位的精度。但如果只有一座塔,信号强度可以用来确定围绕塔周围的粗略半径,形成的圆表示您可能位于的区域。这可能会将您的位置定位在离实际位置一英里或两英里的范围内,甚至到几十米内。

如果那样还失败了,可能会有 WiFi 接入点的位置在您设备的范围内是已知的,由于所有接入点都有称为媒体访问控制(MAC)地址的唯一标识地址,可以得到您位置的一个合理良好的近似值,也许可以缩小到一两条街道。这就是 Google 街景车辆一直在收集的信息类型(其中一些信息由于可能侵犯数据隐私权而被要求丢弃)。

如果这些方法都失败了,您可以查询并使用您的设备使用的 Internet Protocol(IP)地址作为粗略位置指示器。但通常情况下,这只能提供您的互联网提供商拥有的一个主要交换机的位置,该位置可能有数十甚至数百英里之遥。但至少,您的 IP 地址(通常)可以缩小您所在国家的范围,有时甚至可以缩小到您所在地区。

注意

媒体公司通常使用 IP 地址来限制其内容的播放地域。但是,可以简单地设置使用转发 IP 地址的代理服务器(位于阻止外部访问的领土内),直接将内容通过封锁传递到“外国”浏览器。代理服务器也经常用于伪装用户的真实 IP 地址或绕过审查限制,并可以在 WiFi 热点上共享给许多用户使用。因此,如果通过 IP 地址定位某人,您不能完全确定已经准确识别了正确的位置,甚至国家,应将此信息视为仅作最佳猜测。

地理位置和 HTML5

在第二十五章中,我简要介绍了 HTML5 地理位置。现在是时候深入了解它,再次显示在示例 28-1 中。

示例 28-1. 显示您当前位置的地图
<!DOCTYPE html>
<html>
  <head>
    <title>Geolocation Example</title>
  </head>
  <body>
    <script>
      if (typeof navigator.geolocation == 'undefined')
         alert("Geolocation not supported.")
      else
        navigator.geolocation.getCurrentPosition(granted, denied)

      function granted(position)
      {
        var lat = position.coords.latitude
        var lon = position.coords.longitude

        alert("Permission Granted. You are at location:\n\n"
          + lat + ", " + lon +
          "\n\nClick 'OK' to load Google Maps with your location")

        window.location.replace("https://www.google.com/maps/@"
          + lat + "," + lon + ",8z")
      }

      function denied(error)
      {
        var message

        switch(error.code)
        {
          case 1: message = 'Permission Denied'; break;
          case 2: message = 'Position Unavailable'; break;
          case 3: message = 'Operation Timed Out'; break;
          case 4: message = 'Unknown Error'; break;
        }

        alert("Geolocation Error: " + message)
      }
    </script>
  </body>
</html>

让我们浏览这段代码,看看它是如何工作的,从显示标题的<head>部分开始。文档的<body>完全由 JavaScript 组成,立即开始查询navigator.geolocation属性。如果返回的值是undefined,则浏览器不支持地理位置,并弹出错误警告窗口。

否则,我们调用属性的getCurrentPosition方法,并向其传递两个函数的名称:granteddenied(请记住,通过仅传递函数名称,我们传递的是实际的函数代码,而不是调用函数的结果,如果函数名称附带括号,则会是后者):

navigator.geolocation.getCurrentPosition(granted, denied)

这些函数稍后在脚本中出现,用于处理提供用户位置数据的两种可能性:granteddenied

函数granted首先被调用,仅当可以访问数据时才会进入。如果可以访问,变量latlong将被赋予浏览器中地理位置例程返回的值。

然后会弹出一个警告窗口,显示有关用户当前位置的详细信息。当用户点击“确定”时,警告窗口将关闭,并且当前网页将被一个指向 Google 地图的网页替换。它通过使用地理位置调用返回的纬度和经度来传递,使用 8 作为缩放设置。您可以通过将8z的值更改为另一个数值后跟一个z,在window.location.replace调用的末尾设置不同的缩放级别。

通过调用window.location.replace来实现地图的显示。结果看起来像图 28-1。

图 28-1. 显示用户位置的交互地图

如果权限被拒绝(或存在其他问题),则由denied函数显示错误消息,并弹出自己的警告窗口通知用户错误信息:

switch(error.code)
{
  case 1: message = 'Permission Denied'; break;
  case 2: message = 'Position Unavailable'; break;
  case 3: message = 'Operation Timed Out'; break;
  case 4: message = 'Unknown Error'; break;
}

alert("Geolocation Error: " + message)

当浏览器从主机请求地理位置数据时,它将提示用户进行授权。用户可以选择授权或拒绝。拒绝授权会导致权限被拒绝状态,如果用户授权但主机系统无法确定其位置,则会显示位置不可用,而超时则发生在用户授权并且主机尝试获取其位置但请求超时的情况。

还有另一种错误情况,即某些平台和浏览器组合允许用户在不授予或拒绝权限的情况下关闭权限请求对话框。这会导致应用程序在等待回调时“挂起”。

在本书的早期版本中,我曾调用 Google Maps API 在网页内直接嵌入地图,但现在该服务需要您自行申请唯一的 API 密钥,并且超过一定使用量可能会产生费用。这就是为什么现在的示例仅生成一个 Google 地图链接。如果您希望在您的网页和应用中嵌入 Google 地图,所有必要的信息都在网站上。当然,还有许多其他的地图选项,如Bing 地图OpenStreetMap,它们都有可以访问的 API。

本地存储

Cookie 是现代互联网的重要组成部分,因为它们使网站能够在每个用户的机器上保存小段信息,用于跟踪目的。这并不像听起来那么可怕,因为大多数跟踪是帮助网民保存用户名和密码,使他们能够保持登录状态到诸如 Twitter、Facebook 等社交网络的网站上。

Cookie 还可以在本地保存你访问网站的偏好设置(而不是将这些设置存储在网站的服务器上),或者可以用来在电子商务网站上构建订单时跟踪购物车。

但是,它们也可以更积极地用于跟踪你经常访问的网站,并获得你的兴趣画像,以更有效地定向广告。这就是为什么欧洲联盟现在“要求事先知情同意,以便存储或访问用户终端设备上存储的信息”。

但是,作为一个网页开发者,想象一下在用户设备上保存数据会有多么有用,尤其是如果你的计算服务器和硬盘空间预算有限。例如,你可以创建浏览器内的网页应用程序和服务,用于编辑文字处理文档、电子表格和图像,将所有这些数据保存在用户的计算机上,尽可能降低服务器采购预算。

从用户的角度来看,想象一下本地加载文档比通过网络加载快多少,尤其是在慢速连接下。此外,如果你知道一个网站没有存储你文档的副本,那么安全性会更高。当然,你永远不能保证一个网站或网页应用程序是完全安全的,你也不应该使用可以联网的软件(或硬件)处理高度敏感的文档。但对于像家庭照片这样的最小私密文档,你可能更愿意使用本地保存而不是保存文件到外部服务器的网页应用程序。

使用本地存储

使用 Cookie 作为本地存储的最大问题在于,每个 Cookie 最多只能保存 4 KB 的数据。Cookie 还需要在每次页面重新加载时来回传递。并且,除非你的服务器使用传输层安全性(TLS)加密(SSL 的更安全的后继者),否则每次传输 Cookie 时都会明文传输。

但是,有了 HTML5,你可以访问一个更大的本地存储空间(通常每个域名在 5 MB 到 10 MB 之间,具体取决于浏览器),这些数据在页面加载之间和网站访问之间持久存在(甚至在关闭计算机电源并重新开启后仍然存在)。此外,本地存储数据不会在每次页面加载时发送到服务器,并且可以由用户清除,因此你通常希望在服务器上保留数据;否则用户可能会发现他们的数据消失而感到不满,即使是他们自己清除了数据。

本地存储数据通过键/值对处理。键是为引用数据分配的名称,值可以保存任何类型的数据,但保存为字符串。所有数据都是唯一的,限定在当前域内,出于安全原因,由具有不同域的网站创建的任何本地存储都与当前本地存储分开,并且不能被存储数据的域之外的任何域访问。

localStorage 对象

您可以通过查询其类型来测试是否可以通过localStorage对象访问本地存储,如下所示:

if (typeof localStorage == 'undefined')
{
  // Local storage is not available—tell the user and quit.
  // Or maybe offer to save data on the web server instead?
}

如果要处理本地存储不可用的情况,这将取决于您打算使用它的方式,因此您放置在if语句内的代码将由您决定。

一旦确定本地存储可用,您就可以开始使用localStorage对象的setItemgetItem方法,如下所示:

localStorage.setItem('loc', 'USA')
localStorage.setItem('lan', 'English')

要稍后检索此数据,请将键传递给getItem方法,如下所示:

loc = localStorage.getItem('loc')
lan = localStorage.getItem('lan')

与保存和读取 cookie 不同,您可以在任何时候调用这些方法,而不仅仅是在 Web 服务器发送任何标头之前。保存的值将保留在本地存储中,直到以以下方式擦除:

localStorage.removeItem('loc')
localStorage.removeItem('lan')

或者,您可以通过调用clear方法彻底清除当前域的本地存储,如下所示:

localStorage.clear()

示例 28-2 将前述示例组合到一个单独的文档中,显示当前两个键的值在弹出的警告消息中,最初将为null。然后将键和值保存到本地存储中,检索并重新显示,这次分配了值。最后,删除键,然后再次尝试检索这些值,但返回的值再次为null。图 28-2 显示这三个警告消息中的第二个。

示例 28-2. 获取、设置和删除本地存储数据
<!DOCTYPE html>
<html>
  <head>
    <title>Local Storage</title>
  </head>
  <body>
    <script>
      if (typeof localStorage == 'undefined')
      {
        alert("Local storage is not available")
      }
      else
      {
        loc = localStorage.getItem('loc')
        lan = localStorage.getItem('lan')
        alert("The current values of 'loc' and 'lan' are\n\n" +
          loc + " / " + lan + "\n\nClick OK to assign values")

        localStorage.setItem('loc', 'USA')
        localStorage.setItem('lan', 'English')
        loc = localStorage.getItem('loc')
        lan = localStorage.getItem('lan')
        alert("The current values of 'loc' and 'lan' are\n\n" +
          loc + " / " + lan +  "\n\nClick OK to clear values")

        localStorage.removeItem('loc')
        localStorage.removeItem('lan')
        loc = localStorage.getItem('loc')
        lan = localStorage.getItem('lan')
        alert("The current values of 'loc' and 'lan' are\n\n" +
          loc + " / " + lan)
      }
    </script>
  </body>
</html>

图 28-2. 从本地存储中读取两个键及其值
注意

在本地存储中,您可以包含几乎任何和所有数据,并且可以包含多个键/值对,最多达到您域的可用存储限制。

Web Workers

Web workers 运行后台作业,适用于需要长时间计算且不应阻止用户进行其他操作的情况。要使用 web worker,您可以创建 JavaScript 代码段,这些代码将在后台运行。这段代码不必像某些异步系统中的作业那样设置和监控中断。相反,每当它有报告的内容时,您的后台进程通过事件与主 JavaScript 进行通信。

这意味着 JavaScript 解释器决定如何最有效地分配时间片,并且您的代码只需在需要传递信息时与后台任务通信。

示例 28-3 展示了如何设置 Web Worker 在后台执行重复任务,本例中是计算质数。

示例 28-3. 设置和与 Web Worker 通信
<!DOCTYPE html>
<html>
  <head>
    <title>Web Workers</title>
    <script src='OSC.js'></script>
  </head>
  <body>
    Current highest prime number:
    <span id='result'>0</span>

    <script>
      if (!!window.Worker)
      {
        var worker = new Worker('worker.js')

        worker.onmessage = function (event)
        {
          O('result').innerText = event.data;
        }
      }
      else
      {
        alert("Web workers not supported")
      }
    </script>
  </body>
</html>

本例首先在 ID 为 result<span> 元素中创建一个元素,其中将放置来自 Web Worker 的输出。然后,在 <script> 部分,通过 !! 连续 not 运算符测试 window.Worker。如果不是 true,则在 else 部分显示消息,提示我们 Web Worker 不可用。

否则,程序通过调用 Worker 创建一个新的 worker 对象,并将文件名 worker.js 传递给它。然后,新的 worker 对象的 onmessage 事件附加到一个匿名函数,该函数将由 worker.js 传递的任何消息放入先前创建的 <span> 元素的 innerText 属性中。

Web Worker 本身保存在文件 worker.js 中,其内容显示在 示例 28-4 中。

示例 28-4. worker.js Web Worker
var n = 1

search: while (true)
{
  n += 1

  for (var i = 2; i <= Math.sqrt(n); i += 1)
  {
    if (n % i == 0) continue search
  }

  postMessage(n)
}

该文件将值 1 赋给变量 n。然后,它连续循环,递增 n 并通过测试从 1n 的平方根的所有值,看它们是否能够完全除尽 n,没有余数。如果找到因子,continue 命令立即停止暴力攻击,因为该数字不是质数,并从下一个更高的 n 值开始处理。

但是,如果测试了所有可能的因子并且没有导致余数为零,n 必须是质数,因此其值被传递给 postMessage,它将消息发送回设置此 Web Worker 的对象的 onmessage 事件。

结果如下所示:

Current highest prime number: 30477191

要停止 Web Worker 的运行,可以调用 worker 对象的 terminate 方法,如下所示:

worker.terminate()
注意

如果您希望停止运行此特定示例,可以在浏览器的地址栏中输入以下内容:

javascript:worker.terminate()

还请注意,由于 Chrome 处理安全性的方式,您不能在文件系统上使用 Web Worker,只能从 Web 服务器(或在类似 AMPPS 的开发服务器上 localhost 运行文件,详见 第 2 章)。

Web Worker 确实有一些安全限制需要注意:

  • Web Worker 在其自己独立的 JavaScript 上下文中运行,并且无法直接访问任何其他执行上下文中的内容,包括主 JavaScript 线程或其他 Web Worker。

  • Web Worker 上下文之间的通信通过 web messaging (postMessage) 来完成。

  • 因为 Web Workers 无法访问主 JavaScript 上下文,它们无法修改 DOM。Web Workers 可用的唯一 DOM 方法包括 atobbtoaclearIntervalclearTimeoutdumpsetIntervalsetTimeout

  • Web Workers 受同源策略的限制,因此不能从与原始脚本不同源的位置加载 Web Worker,除非通过跨站方法。

拖放

通过设置 ondragstartondragoverondrop 事件的事件处理程序,你可以轻松支持在网页上拖放对象,例如 示例 28-5。

示例 28-5. 拖放对象
<!DOCTYPE HTML>
<html>
  <head>
    <title>Drag and Drop</title>
    <script src='OSC.js'></script>
    <style>
      #dest {
        background:lightblue;
        border    :1px solid #444;
        width     :320px;
        height    :100px;
        padding   :10px;
      }
    </style>
  </head>
  <body>
    <div id='dest' ondrop='drop(event)' ondragover='allow(event)'></div><br>
    Drag the image below into the above element<br><br>

    <img id='source1' src='image1.png' draggable='true' ondragstart='drag(event)'>
    <img id='source2' src='image2.png' draggable='true' ondragstart='drag(event)'>
    <img id='source3' src='image3.png' draggable='true' ondragstart='drag(event)'>

    <script>
      function allow(event)
      {
        event.preventDefault()
      }

      function drag(event)
      {
        event.dataTransfer.setData('image/png', event.target.id)
      }

      function drop(event)
      {
        event.preventDefault()
        var data=event.dataTransfer.getData('image/png')
        event.target.appendChild(O(data))
      }
    </script>
  </body>
</html>

在设置了 HTML、提供了标题并加载了 OSC.js 文件之后,该文档对 ID 为 dest<div> 元素进行样式设置,为其设置了背景颜色、边框、固定尺寸和填充。

然后,<body> 部分创建了 <div> 元素,并将事件处理程序函数 dropallow 附加到 <div>ondropondragover 事件上。之后是一些文本,然后是三个将其 draggable 属性设置为 true 的图像。每个图像的 ondragstart 事件附加了 drag 函数。

<script> 部分,allow 事件处理程序函数简单地阻止了拖动的默认操作(即不允许它),而 drag 事件处理程序函数调用了事件的 dataTransfer 对象的 setData 方法,传递了 MIME 类型 image/png 和事件的 target.id(即被拖动的对象)。dataTransfer 对象在拖放操作期间保存被拖动的数据。

最后,drop 事件处理程序函数还拦截其默认操作,以便允许拖放,然后从 dataTransfer 对象中获取被拖动对象的内容,传递对象的 MIME 类型。然后通过其 appendChild 方法将放置的数据附加到目标(即 dest <div>)。

如果你自己尝试这个例子,你可以将图像拖放到 <div> 元素中,它们将保留在那里,如 图 28-3 所示。这些图像只能拖放到附加了 dropallow 事件处理程序的元素中,而不能放置到其他位置。

图 28-3. 已拖放两个图像

你还可以附加其他事件,如 ondragenter(当拖动操作进入元素时运行)、ondragleave(当离开元素时运行)和 ondragend(当拖动操作结束时运行),例如可以在这些操作期间修改光标。

跨文档消息

您在前面稍早已经看到消息的使用情况,在 Web Worker 部分。但是由于它并非讨论的核心主题,而且消息仅发布到同一文档,所以我没有详细说明。但出于明显的安全原因,跨文档消息确实需要谨慎使用,因此如果您计划使用它,您需要完全了解其工作原理。

在 HTML5 之前,浏览器开发人员禁止跨文档脚本,但除了阻止潜在的攻击站点外,这还阻止了合法页面之间的通信。任何形式的文档交互通常必须通过 Ajax 和第三方 Web 服务器进行,这很麻烦且难以建立和维护。

但是现在,Web 消息传递允许脚本通过使用一些合理的安全限制在这些边界之间进行交互,以防止恶意黑客攻击。它通过使用postMessage方法实现,允许从一个域向另一个域发送纯文本消息,始终在单个浏览器内进行。

这要求 JavaScript 首先获取接收文档的window对象,从而允许消息直接发布到与发送文档直接相关的各种其他窗口、框架或 iframe。接收的消息事件具有以下属性:

数据

接收到的消息

来源

发件人文档的来源,包括方案、主机名和端口

发送文档的源窗口

发送消息的代码只需一条指令,其中您传递要发送的消息以及应用于其的域,如示例 28-6 中所示。

示例 28-6. 将 Web 消息发送到 iframe
<!DOCTYPE HTML>
<html>
  <head>
    <title>Web Messaging (a)</title>
    <script src='OSC.js'></script>
  </head>
  <body>
    <iframe id='frame' src='07.html' width='360' height='75'></iframe>

    <script>
      count = 1

      setInterval(function()
      {
        O('frame').contentWindow.postMessage('Message ' + count++, '*')
      }, 1000)
    </script>
  </body>
</html>

在这里,通常使用OSC.js文件来拉取O函数,然后创建一个 ID 为frame<iframe>元素,加载示例 28-7 中的内容。然后,在<script>部分中,将变量count初始化为1,并设置每秒重复一次的间隔,以发布字符串'Message '(使用postMessage方法)以及当前值count,然后递增。postMessage调用附加到iframe对象的contentWindow属性,而不是iframe对象本身。这很重要,因为 Web 消息要求将帖子发布到窗口,而不是窗口内的对象。

示例 28-7. 从另一个文档接收消息
<!DOCTYPE HTML>
<html>
  <head>
    <title>Web Messaging (b)</title>
    <style>
      #output {
        font-family:"Courier New";
        white-space:pre;
      }
    </style>
    <script src='OSC.js'></script>
  </head>
  <body>
    <div id='output'>Received messages will display here</div>

    <script>
      window.onmessage = function(event)
      {
        O('output').innerHTML =
          '<b>Origin:</b> ' + event.origin + '<br>' +
          '<b>Source:</b> ' + event.source + '<br>' +
          '<b>Data:</b>   ' + event.data
      }
    </script>
  </body>
</html>

这个例子设置了一些样式,使输出更清晰,然后创建了一个 ID 为 output<div> 元素,在其中接收到的消息内容将被放置。 <script> 部分包含一个附加到窗口 onmessage 事件的单个匿名函数。在此函数中,显示了 event.originevent.sourceevent.data 属性值,如 Figure 28-4 所示。

图 28-4. iframe 到目前为止已接收到 29 条消息

Web 消息传递只能跨域工作,因此您不能通过从文件系统加载文件来测试它;您必须使用 web 服务器(例如在 Chapter 2 建议的 AMPPS 堆栈)。正如您从 Figure 28-4 可以看到的那样,来源是 *http://localhost*,因为这些示例正在本地开发服务器上运行。源是 window 对象,当前消息的值是 Message 29

要自己运行这个程序,只需将 06.html 加载到浏览器中,使用 localhost:// 而不是文件系统,它将与 07.html 进行通信,而无需您加载它,因为它被插入到一个 iframe 中。

目前,Example 28-6 完全不安全,因为传递给 postMessage 的域值是通配符 *

O('frame').contentWindow.postMessage('Message ' + count++, `'*'`)

要将消息仅定向到源自特定域的文档,您可以更改此参数。在当前情况下,值为 http://localhost 将确保只有从本地服务器加载的文档会接收到任何消息:

O('frame').contentWindow.postMessage('Message ' + count++, `'http://localhost'`)

同样地,作为它目前的状态,监听程序显示接收到的任何和所有消息。这也不是非常安全的状态,因为浏览器中还存在恶意文档,它们可能试图发送消息,而不经意的文档中的监听器代码可能会访问这些消息。因此,您可以通过使用 if 语句限制监听器对消息的反应,就像这样:

window.onmessage = function(event)
{
  `if` `(``event``.``origin``)` `==` `'http://localhost'``)`
  `{`
    O('output').innerHTML =
      '<b>Origin:</b> ' + event.origin + '<br>' +
      '<b>Source:</b> ' + event.source + '<br>' +
      '<b>Data:</b>   ' + event.data
  `}`
}
警告

如果您始终使用站点的正确域,您的网络消息通信将更加安全。但是,请注意,由于消息是明文发送的,某些浏览器或浏览器插件可能存在不安全因素,这可能使此类通信不安全。因此,提高安全性的一种方法是为所有网络消息使用加密方案,并考虑引入双向通信协议以验证每条消息的真实性。

通常情况下,您不会向用户警告 originsource 值,并且仅将它们用于安全检查。然而,这些例子显示这些值,以帮助您实验网络消息并了解正在发生的事情。除了使用 iframe 外,弹出窗口和其他标签页中的文档也可以使用此方法相互通信。

其他 HTML5 标签

多个其他新的 HTML5 标签正在主要浏览器中得到采纳,包括<article><aside><details><figcaption><figure><footer><header><hgroup><mark><menuitem><meter><nav><output><progress><rp><rt><ruby><section><summary><time><wbr>。您可以从eastmanreference.com获取更多关于这些和所有其他 HTML5 标签的信息。

这就是您对 HTML5 的介绍。现在您拥有了多个强大的新功能,可以用来创建更加动态和引人入胜的网站。在最后一章中,我将向您展示如何将本书中的所有不同技术结合起来,创建一个迷你社交网络站点。

问题

  1. 您调用哪个方法来请求 Web 浏览器的地理位置数据?

  2. 如何确定浏览器是否支持本地存储?

  3. 您可以调用哪个方法来清除当前域的所有本地存储数据?

  4. Web 工作者与主程序进行通信的最佳方式是什么?

  5. 如何停止 Web 工作者的运行?

  6. 要支持拖放操作,如何防止默认操作禁止这些事件的拖放?

  7. 如何使跨文档消息传递更安全?

请参阅“第二十八章答案”位于附录 A 获取这些问题的答案。

第二十九章:统筹全局

现在你已经读到了本书的末尾,在动态网页编程的各种方法、原理和应用的道路上迈出了第一个里程碑,我想给你留下一个真实的例子,让你能够深入理解。事实上,它是一个例子的集合,因为我组建了一个简单的社交网络项目,包括所有你期望在这样一个站点上看到的主要功能,更重要的是,这样一个 Web 应用程序。

在各种文件中,有 MySQL 表的创建和数据库访问、CSS、文件包含、会话控制、DOM 访问、异步调用、事件和错误处理、文件上传、图像处理、HTML5 画布等示例。

每个示例文件都是完整的、独立的,但与其他文件一起工作,构建一个完全可用的社交网络站点,甚至包括一个可以修改以完全改变项目外观和感觉的样式表。由于体积小巧,最终产品特别适用于智能手机或平板电脑等移动平台,但同样可以在全尺寸桌面计算机上运行良好。

并且你会发现,通过充分利用 jQuery 和 jQuery Mobile 的强大功能,代码运行速度快,易于使用,适应所有环境,并且外观优美。作为练习,你可能希望进一步调整代码,也许包括某种方式使用 React。

话虽如此,我已尽可能简化这段代码,以便易于理解。因此,它有很大的改进空间,例如通过存储哈希(不可逆单向函数的固定长度输出)来增强安全性,而不是未加密的密码,并且更顺畅地处理登录和注销之间的过渡——但让我们将这些留给读者作为传说中的练习,尤其是本章末尾没有问题。(嗯,就一个!)

我将这段代码的任何部分留给你,你认为可以使用并扩展为自己的目的。也许你甚至希望在这些文件的基础上创建一个属于自己的社交网络站点。

设计社交网络应用程序

在编写任何代码之前,我坐下来并想出了几个我认为对这样一个应用程序至关重要的事物。这些包括以下内容:

  • 注册流程

  • 登录表单

  • 注销设施

  • 会话控制

  • 用户配置文件及其上传的缩略图

  • 成员目录

  • 添加成员作为好友

  • 成员之间的公共和私人消息传递

  • 项目样式化

我决定将项目命名为Robin's Nest;如果你使用这段代码,你需要在index.phpheader.php文件中修改名称和标志。

在网站上

本章中的所有示例都可以在我的 GitHub 存储库中找到,您可以下载一个存档文件,应将其解压到计算机上适当的位置。

特别值得注意的是,对于本章而言,在ZIP文件中,您将找到一个名为robinsnest的文件夹,其中包含所有以下示例所需的正确文件名。这意味着您可以轻松地将它们全部复制到您的 Web 开发文件夹中以进行尝试。

functions.php

让我们直接进入项目,从示例 29-1,functions.php开始,这是主要函数的包含文件。这个文件不仅仅包含函数,因为我已经在这里添加了数据库登录详细信息,而不是使用另一个单独的文件。代码的前四行定义了要使用的数据库的主机和名称,以及用户名和密码。

默认情况下,在这个文件中,MySQL 用户名设置为robinsnest,程序使用的数据库也叫做robinsnest。第八章 提供了关于如何创建新用户和/或数据库的详细说明,但是简要回顾一下,首先通过输入 MySQL 命令提示符并键入以下命令来创建一个名为robinsnest的新数据库:

CREATE DATABASE robinsnest;

然后,您可以像这样创建一个名为robinsnest的用户,该用户可以访问此数据库:

CREATE USER 'robinsnest'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON robinsnest.* TO 'robinsnest'@'localhost';

显然,您应该为此用户使用比password更安全的密码,但为简单起见,这些示例中使用的是该密码——如果您在生产站点上使用此代码,请确保更改密码。

函数

该项目使用了五个主要函数:

createTable

检查表是否已存在,如果不存在则创建

queryMysql

向 MySQL 发出查询,如果失败则输出错误消息

destroySession

销毁 PHP 会话并清除其数据以注销用户

sanitizeString

从用户输入中删除潜在恶意代码或标签

showProfile

如果用户有图像和“关于我”消息,则显示它们

到目前为止,所有这些行为对您来说应该是显而易见的,除了showProfile可能是个例外,它查找名称为*<user.jpg>(其中*是当前用户的用户名)的图像,如果找到,则显示它。它还显示用户保存的任何“关于我”文本。

我已确保所有需要的函数都有错误处理,以便捕获您可能引入的任何排版或其他错误并生成错误消息。但是,如果您在生产服务器上使用任何此代码,您将需要提供自己的错误处理程序以使代码更加用户友好。

所以,输入示例 29-1,将其保存为functions.php(或从伴随网站下载),然后您将准备好进入下一节。

示例 29-1. functions.php
<?php // Example 01: functions.php
  $host = 'localhost';    // Change as necessary
  $data = 'robinsnest';   // Change as necessary
  $user = 'robinsnest';   // Change as necessary
  $pass = 'password';     // Change as necessary
  $chrs = 'utf8mb4';
  $attr = "mysql:host=$host;dbname=$data;charset=$chrs";
  $opts =
  [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
  ];

  try
  {
    $pdo = new PDO($attr, $user, $pass, $opts);
  }
  catch (\PDOException $e)
  {
    throw new \PDOException($e->getMessage(), (int)$e->getCode());
  }

  function createTable($name, $query)
  {
    queryMysql("CREATE TABLE IF NOT EXISTS $name($query)");
    echo "Table '$name' created or already exists.<br>";
  }

  function queryMysql($query)
  {
    global $pdo;
    return $pdo->query($query);
  }

  function destroySession()
  {
    $_SESSION=array();

    if (session_id() != "" || isset($_COOKIE[session_name()]))
      setcookie(session_name(), '', time()-2592000, '/');

    session_destroy();
  }

  function sanitizeString($var)
  {
    global $pdo;
    $var = strip_tags($var);
    $var = htmlentities($var);
    if (get_magic_quotes_gpc())
      $var = stripslashes($var);
    $result = $pdo->quote($var);          // This adds single quotes
    return str_replace("'", "", $result); // So now remove them
  }

  function showProfile($user)
  {
    if (file_exists("$user.jpg"))
      echo "<img src='$user.jpg' style='float:left;'>";

    $result = $pdo->query("SELECT * FROM profiles WHERE user='$user'");

    while ($row = $result->fetch())
    {
      die(stripslashes($row['text']) . "<br style='clear:left;'><br>");
    }

    echo "<p>Nothing to see here, yet</p><br>";
  }
?>

注意

如果您曾阅读本书的早期版本,在这些示例中使用了旧的mysql扩展名,后来改为mysqli,您现在将看到我再次转向到目前为止最好的解决方案,即PDO

要使用PDO引用 MySQL 数据库,必须在queryMysqlsanitizeString函数中应用global关键字,以允许它们使用$PDO的值。

头文件

为了保持项目的统一性,项目的每个页面都需要访问相同的功能集。因此,我将这些内容放在了header.php(示例 29-2)中。这个文件实际上是其他文件所包含的文件。它包含了functions.php。这意味着每个文件只需要一次require_once

header.php从调用session_start函数开始。正如您在第十三章中回忆的那样,这将设置一个会话,它将记住我们希望跨不同 PHP 文件存储的某些值。换句话说,它代表用户对站点的访问,并且如果用户在一段时间内忽略站点,则会超时。

会话开始后,程序输出所需的 HTML 来设置每个网页,包括加载样式表和各种所需的 JavaScript 库。在此之后,包含函数文件(functions.php)并将默认字符串“Welcome Guest”分配给$userstr

在此之后,随机字符串值被分配给变量$randstr,在整个应用程序中用于追加到 URL 上,以便每个加载的页面看起来对 jQuery 滑动界面都是唯一的。如果没有这样做,看起来不应该改变到 jQuery 的页面将从其缓存中获取,这将导致尽可能好的性能。在具有静态信息的一组页面上,这是可以接受的,但这是一个动态应用程序,页面信息随时会变化,因此我们必须确保每个新的页面请求来自服务器而不是缓存。

接下来,代码检查会话变量user当前是否被赋值。如果是,表示用户已经登录,所以变量$loggedin被设置为TRUE,并且用户名从会话变量user中检索到 PHP 变量$user中,并相应地更新了$userstr。如果用户尚未登录,则$loggedin被设置为FALSE

接下来,一些 HTML 输出欢迎用户(如果尚未登录则为访客),并输出 jQuery Mobile 页面头部和内容部分所需的<div>元素。

在此之后,根据$loggedin的值,if块将显示两套菜单中的一套。非登录集合仅提供主页、注册和登录选项,而登录版本则提供对应用程序功能的完全访问权限。按钮使用 jQuery Mobile 标记进行样式设置,例如data-role='button'用于将元素显示为按钮,data-inline='true'用于内联显示元素(类似于<span>元素),data-transition="slide"用于在单击时使新页面滑入视图中,详见第二十三章。

在这些 URL 中,您将注意到r=$randstr的使用,如前所述,以确保从服务器获取每个页面,而不是从 jQuery 的缓存中获取。

此文件应用的附加样式位于文件styles.css(详见示例 29-13,本章末详述)中。

示例 29-2. header.php
<?php // Example 02: header.php
  session_start();

echo <<<_INIT
<!DOCTYPE html>
<html>
 <head>
 <meta charset='utf-8'>
    <meta name='viewport' content='width=device-width, initial-scale=1'> 
    <link rel='stylesheet' href='jquery.mobile-1.4.5.min.css'>
    <link rel='stylesheet' href='styles.css'>
    <script src='javascript.js'></script>
    <script src='jquery-2.2.4.min.js'></script>
    <script src='jquery.mobile-1.4.5.min.js'></script>

_INIT;

  require_once 'functions.php';

  $userstr = 'Welcome Guest';
  $randstr = substr(md5(rand()), 0, 7);

  if (isset($_SESSION['user']))
  {
    $user     = $_SESSION['user'];
    $loggedin = TRUE;
    $userstr  = "Logged in as: $user";
  }
  else $loggedin = FALSE;

echo <<<_MAIN
    <title>Robin's Nest: $userstr</title>
  </head>
  <body>
    <div data-role='page'>
      <div data-role='header'>
        <div id='logo'
          class='center'>R<img id='robin' src='robin.gif'>bin's Nest</div>
        <div class='username'>$userstr</div>
      </div>
      <div data-role='content'>

_MAIN;

  if ($loggedin)
  {
echo <<<_LOGGEDIN
        <div class='center'>
          <a data-role='button' data-inline='true' data-icon='home'
            data-transition="slide" href='members.php?view=$user&r=$randstr'>Home</a>
          <a data-role='button' data-inline='true' data-icon='user'
            data-transition="slide" href='members.php?r=$randstr'>Members</a>
          <a data-role='button' data-inline='true' data-icon='heart'
            data-transition="slide" href='friends.php?r=$randstr'>Friends</a><br>
          <a data-role='button' data-inline='true' data-icon='mail'
            data-transition="slide" href='messages.php?r=$randstr'>Messages</a>
          <a data-role='button' data-inline='true' data-icon='edit'
            data-transition="slide" href='profile.php?r=$randstr'>Edit Profile</a>
          <a data-role='button' data-inline='true' data-icon='action'
            data-transition="slide" href='logout.php?r=$randstr'>Log out</a>
        </div>

_LOGGEDIN;
  }
  else
  {
echo <<<_GUEST
        <div class='center'>
          <a data-role='button' data-inline='true' data-icon='home'
            data-transition='slide' href='index.php?r=$randstr''>Home</a>
          <a data-role='button' data-inline='true' data-icon='plus'
            data-transition="slide" href='signup.php?r=$randstr''>Sign Up</a>
          <a data-role='button' data-inline='true' data-icon='check'
            data-transition="slide" href='login.php?r=$randstr''>Log In</a>
        </div>
        <p class='info'>(You must be logged in to use this app)</p>

_GUEST;
  }
?>

setup.php

随着编写的这对包含文件,现在是时候设置它们将使用的 MySQL 表格了。我们可以使用示例 29-3,setup.php来实现这一点,您应该在调用任何其他文件之前在浏览器中输入并加载它,否则将会出现大量的 MySQL 错误。

创建的表格简洁明了,具有以下名称和列:

members

用户名 user(已索引)、密码 pass

messages

ID id(已索引)、作者 auth(已索引)、接收者 recip、消息类型 pm、消息 message

friends

用户名 user(已索引)、朋友的用户名 friend

profiles

用户名 user(已索引)、“关于我” text

由于createTable函数首先检查表是否已经存在,因此可以安全地多次调用此程序而不会生成任何错误。

如果您选择扩展此项目,则很可能需要向这些表格添加更多列。如果是这样,请记住在重新创建表格之前可能需要发出 MySQL 的DROP TABLE命令。

示例 29-3. setup.php
<!DOCTYPE html> <!-- Example 03: setup.php -->
<html>
  <head>
    <title>Setting up database</title>
  </head>
  <body>
    <h3>Setting up...</h3>

<?php
 require_once 'functions.php';

 createTable('members',
 'user VARCHAR(16),
 pass VARCHAR(16),
 INDEX(user(6))');

 createTable('messages',
 'id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
 auth VARCHAR(16),
 recip VARCHAR(16),
 pm CHAR(1),
 time INT UNSIGNED,
 message VARCHAR(4096),
 INDEX(auth(6)),
 INDEX(recip(6))');

 createTable('friends',
 'user VARCHAR(16),
 friend VARCHAR(16),
 INDEX(user(6)),
 INDEX(friend(6))');

 createTable('profiles',
 'user VARCHAR(16),
 text VARCHAR(4096),
 INDEX(user(6))');
?>

    <br>...done.
  </body>
</html>
警告

要使此示例有效,您必须首先确保已创建变量$data中指定的数据库,详情请参见示例 29-1,并且已授予给定名称的用户(在$user中)及其密码(在$pass中)访问权限。

index.php

此文件虽然微不足道,但却是必不可少的,以便为项目提供一个首页。它所做的只是显示一个简单的欢迎消息。在完成的应用程序中,这将是您向用户推销站点优点以鼓励注册的地方。

顺便说一句,由于我们已经设置好了所有 MySQL 表格并创建了包含的文件,您现在可以加载示例 29-4,index.php,到您的浏览器中,以便首次查看新应用程序。它应该看起来像图 29-1。

示例 29-4. index.php
<?php // Example 04: index.php
  session_start();
  require_once 'header.php';

  echo "<div class='center'>Welcome to Robin's Nest,";

  if ($loggedin) echo " $user, you are logged in";
  else           echo ' please sign up or log in';

  echo <<<_END
      </div><br>
    </div>
    <div data-role="footer">
      <h4>Web App from <i><a href='https://github.com/RobinNixon/lpmj6'
      target='_blank'>Learning PHP MySQL & JavaScript</a></i></h4>
    </div>
  </body>
</html>
_END;
?>

图 29-1. 应用程序的主页

signup.php

现在我们需要一个模块来让用户加入我们的新社交网络,那就是示例 29-5,signup.php。这是一个略长一些的程序,但你之前已经看过其所有部分。

让我们从 HTML 的末尾块开始。这是一个简单的表单,允许输入用户名和密码。但请注意使用了带有idused的空<div>。这将是程序中异步调用的目的地,用于检查所需用户名是否可用。完整的工作原理请参见第十八章。

检查用户名是否可用

现在回到程序的起点,你会看到一段以函数checkUser开头的 JavaScript 代码块。当焦点从表单的username字段移除时,这段代码会被 JavaScript 的onBlur事件调用。首先,它会将我提到的带有idused<div>的内容设置为非空格&nbsp;,以清除它(如果之前有值的话)。

接下来会向checkuser.php发送请求,该程序报告user中的用户名是否可用。异步调用的返回结果(使用 jQuery 执行),一个友好的消息,然后放置在used<div>中。

在 JavaScript 部分之后是一些 PHP 代码,你应该能从第十七章关于表单验证的讨论中识别出来。本节还使用sanitizeString函数,在查找数据库中的用户名之前移除潜在恶意字符,如果用户名尚未使用,则插入新的用户名$user和密码$pass

登录

成功注册后,用户将被提示登录。此时更流畅的响应可能是自动登录新创建的用户,但因为我不想过度复杂化代码,所以保持了注册和登录模块的分离。不过,如果你想要的话,很容易实现这一点。

此文件使用 CSS 类fieldname来排列表单字段,使它们在列下面整齐对齐。加载到浏览器中时(并与稍后显示的checkuser.php配合使用),该程序将看起来像图 29-2,你可以看到异步调用已经确认用户名Robin可用。如果希望密码字段只显示星号,请将其类型从text更改为password

记住,在运行任何其他 PHP 程序文件之前,你必须先运行setup.php

警告

在生产服务器上,我不建议像我这样将用户密码明文存储,出于空间和简易性的考虑才这样做。相反,你应该对其进行盐值处理并存储为单向哈希字符串。有关如何执行此操作的详细信息,请参阅第十三章。

图 29-2. 注册页面
示例 29-5. signup.php
<?php // Example 05: signup.php
  require_once 'header.php';

echo <<<_END
  <script>
    function checkUser(user)
    {
      if (user.value == '')
      {
        $('#used').html('&nbsp;')
        return
      }

      $.post
      (
        'checkuser.php',
        { user : user.value },
        function(data)
        {
          $('#used').html(data)
        }
      )
    }
  </script>  
_END;

  $error = $user = $pass = "";
  if (isset($_SESSION['user'])) destroySession();

  if (isset($_POST['user']))
  {
    $user = sanitizeString($_POST['user']);
    $pass = sanitizeString($_POST['pass']);

    if ($user == "" || $pass == "")
      $error = 'Not all fields were entered<br><br>';
    else
    {
      $result = queryMysql("SELECT * FROM members WHERE user='$user'");

      if ($result->rowCount())
        $error = 'That username already exists<br><br>';
      else
      {
        queryMysql("INSERT INTO members VALUES('$user', '$pass')");
        die('<h4>Account created</h4>Please Log in.</div></body></html>');
      }
    }
  }

echo <<<_END
      <form method='post' action='signup.php?r=$randstr'>$error
      <div data-role='fieldcontain'>
        <label></label>
        Please enter your details to sign up
      </div>
      <div data-role='fieldcontain'>
        <label>Username</label>
        <input type='text' maxlength='16' name='user' value='$user'
          onBlur='checkUser(this)'>
        <label></label><div id='used'>&nbsp;</div>
      </div>
      <div data-role='fieldcontain'>
        <label>Password</label>
        <input type='text' maxlength='16' name='pass' value='$pass'>
      </div>
      <div data-role='fieldcontain'>
        <label></label>
        <input data-transition='slide' type='submit' value='Sign Up'>
      </div>
    </div>
  </body>
</html>
_END;
?>

checkuser.php

配合signup.php,这里是示例 29-6,checkuser.php,它在数据库中查找用户名并返回指示它是否已经被使用的字符串。因为它依赖于sanitizeStringqueryMysql函数,程序首先包含文件functions.php

然后,如果$_POST变量user有值,函数将在数据库中查找它,并根据它是否作为用户名存在输出“对不起,用户名*user已被使用”或“用户名user*可用”。只需检查函数调用返回的值$result->rowCount即可,如果未找到名称,则返回0,如果找到,则返回1

HTML 实体&#x2718;&#x2714;也用于在字符串前面加上红叉或绿勾,并且根据styles.css中定义的taken类显示红色或available类显示绿色,本章后面将显示。

示例 29-6. checkuser.php
<?php // Example 06: checkuser.php
  require_once 'functions.php';

  if (isset($_POST['user']))
  {
    $user   = sanitizeString($_POST['user']);
    $result = queryMysql("SELECT * FROM members WHERE user='$user'");

    if ($result->rowCount())
      echo  "<span class='taken'>&nbsp;&#x2718; " .
            "The username '$user' is taken</span>";
    else
      echo "<span class='available'>&nbsp;&#x2714; " .
           "The username '$user' is available</span>";
  }
?>

login.php

现在用户可以在网站上注册了,示例 29-7,login.php提供了让他们登录所需的代码。像注册页面一样,它具有简单的 HTML 表单和一些基本的错误检查,还使用sanitizeString在查询 MySQL 数据库之前。

在这里需要注意的主要是,一旦验证用户名和密码成功,会话变量userpass将分别被赋予用户名和密码的值。只要当前会话保持活动状态,这些变量将被项目中的所有程序访问,从而允许它们自动为已登录用户提供访问权限。

如果成功登录,您可能会对die函数的使用感兴趣。这是因为它将echoexit命令合并在一起,从而节省了一行代码。为了样式化,这个文件(像大多数文件一样)将类main应用于内容,以使其从左侧边缘缩进。

当您在浏览器中调用此程序时,它应该看起来像图 29-3。注意在此处使用了password的输入类型,以用星号掩盖密码,防止任何人看到用户的密码。

示例 29-7. login.php
 <?php // Example 07: login.php
  require_once 'header.php';
  $error = $user = $pass = "";

  if (isset($_POST['user']))
  {
    $user = sanitizeString($_POST['user']);
    $pass = sanitizeString($_POST['pass']);

    if ($user == "" || $pass == "")
      $error = 'Not all fields were entered';
    else
    {
      $result = queryMySQL("SELECT user,pass FROM members
        WHERE user='$user' AND pass='$pass'");

      if ($result->rowCount() == 0)
      {
        $error = "Invalid login attempt";
      }
      else
      {
        $_SESSION['user'] = $user;
        $_SESSION['pass'] = $pass;
        die("<div class='center'>You are now logged in. Please
             <a data-transition='slide'
               href='members.php?view=$user&r=$randstr'>click here</a>
               to continue.</div></div></body></html>");
      }
    }
  }

echo <<<_END
      <form method='post' action='login.php?r=$randstr'>
        <div data-role='fieldcontain'>
          <label></label>
          <span class='error'>$error</span>
        </div>
        <div data-role='fieldcontain'>
          <label></label>
          Please enter your details to log in
        </div>
        <div data-role='fieldcontain'>
          <label>Username</label>
          <input type='text' maxlength='16' name='user' value='$user'>
        </div>
        <div data-role='fieldcontain'>
          <label>Password</label>
          <input type='password' maxlength='16' name='pass' value='$pass'>
        </div>
        <div data-role='fieldcontain'>
          <label></label>
          <input data-transition='slide' type='submit' value='Login'>
        </div>
      </form>
    </div>
  </body>
</html>
_END;
?>

图 29-3. 登录页面

profile.php

在注册并登录后,新用户可能希望做的第一件事之一是创建个人资料,这可以通过示例 29-8,profile.php完成。我认为您会在这里找到一些有趣的代码,比如用于上传、调整大小和锐化图像的例程。

让我们从代码末尾的主要 HTML 开始。这与您刚刚看到的表单类似,但这次有一个参数enctype='multipart/form-data'。这使我们能够同时发送多种类型的数据,允许上传图片以及一些文本。还有一个file类型的输入,它创建一个“浏览”按钮,用户可以单击以选择要上传的文件。

当表单提交时,程序开始时执行的代码。它首先确保用户已登录,然后才允许程序执行继续。只有在这样做后才显示页面标题。

注意

如第二十三章所述,由于 jQuery Mobile 使用异步通信的方式,使用 HTML 上传文件是不可能的,除非您通过向<form>元素添加属性data-ajax='false'来禁用该功能。这将允许 HTML 文件上传正常进行,但您将失去执行页面更改动画的能力。

添加“关于我”的文本

接下来,检查$_POST变量text,看看是否有文本发布到程序中。如果有,将对其进行过滤,并将所有长的空白序列(包括换行符和回车符)替换为单个空格。此函数包含了双重安全检查,确保用户实际存在于数据库中,并且在将此文本插入数据库中成为用户的“关于我”详情之前,没有任何黑客尝试能够成功。

如果没有发布文本,则查询数据库以查看是否已存在任何文本,以便预填充用户编辑的<textarea>

添加个人资料图片

接下来,我们转到检查$_FILES系统变量是否已上传图像的部分。如果是,则创建一个名为$saveto的字符串变量,基于用户的用户名后跟扩展名*.jpg*。例如,名为Jill的用户将导致$saveto具有值Jill.jpg。这是保存上传图像以供用户个人资料使用的文件。

接下来,检查上传的图像类型,并仅在它是*.jpeg*、.png或*.gif*图像时接受。成功后,使用一个imagecreatefrom函数之一将上传的图像填充到变量$src中,根据上传的图像类型。现在,该图像以 PHP 可处理的原始格式存在。如果图像不是允许的类型,则将标志$typeok设置为FALSE,防止处理图像上传代码的最后部分。

处理图像

首先,我们使用以下语句将图像的尺寸存储在$w$h中,这是将数组中的值快速分配给单独变量的一种方法:

list($w, $h) = getimagesize($saveto);

然后,使用变量 $max 的值(设置为 100),计算新的尺寸,以确保新图像的宽高比与原图相同,但各维度均不超过 100 像素。这将为变量 $tw$th 赋予所需的新值。如果需要更小或更大的缩略图,只需相应地更改变量 $max 的值即可。

接下来,调用 imagecreatetruecolor 函数创建一个新的空画布 $tw 宽、$th 高的 $tmp。然后使用 imagecopyresampled 函数将图像从 $src 重新取样到新的 $tmp。有时重新取样图像会导致略微模糊的副本,因此下一段代码使用 imageconvolution 函数稍微清晰化图像。

最后,将图像保存为 .jpeg 文件,保存位置由变量 $saveto 定义,然后使用 imagedestroy 函数从内存中移除原始图像和调整大小后的图像画布,释放所使用的内存。

显示当前个人资料

最后但并非最不重要的是,为了在编辑之前让用户查看当前的个人资料,将在输出表单 HTML 之前调用 functions.php 中的 showProfile 函数。如果尚未存在个人资料,则不会显示任何内容。

当显示个人资料图像时,会应用 CSS 样式,为其添加边框、阴影,并在其右侧添加边距,以将个人资料文本与图像分隔开。将 示例 29-8 加载到浏览器中的结果显示在 图 29-4 中,您可以看到 <textarea> 已经预填充了“关于我”的文本。

示例 29-8. profile.php
<?php // Example 08: profile.php
  require_once 'header.php';

  if (!$loggedin) die("</div></body></html>");

  echo "<h3>Your Profile</h3>";

  $result = queryMysql("SELECT * FROM profiles WHERE user='$user'");

  if (isset($_POST['text']))
  {
    $text = sanitizeString($_POST['text']);
    $text = preg_replace('/\s\s+/', ' ', $text);

    if ($result->rowCount())
         queryMysql("UPDATE profiles SET text='$text' where user='$user'");
    else queryMysql("INSERT INTO profiles VALUES('$user', '$text')");
  }
  else
  {
    if ($result->rowCount())
    {
      $row  = $result->fetch();
      $text = stripslashes($row['text']);
    }
    else $text = "";
  }

  $text = stripslashes(preg_replace('/\s\s+/', ' ', $text));

  if (isset($_FILES['image']['name']))
  {
    $saveto = "$user.jpg";
    move_uploaded_file($_FILES['image']['tmp_name'], $saveto);
    $typeok = TRUE;

    switch($_FILES['image']['type'])
    {
      case "image/gif":   $src = imagecreatefromgif($saveto); break;
      case "image/jpeg"// Both regular and progressive jpegs
      case "image/pjpeg": $src = imagecreatefromjpeg($saveto); break;
      case "image/png":   $src = imagecreatefrompng($saveto); break;
      default:            $typeok = FALSE; break;
    }

    if ($typeok)
    {
      list($w, $h) = getimagesize($saveto);

      $max = 100;
      $tw  = $w;
      $th  = $h;

      if ($w > $h && $max < $w)
      {
        $th = $max / $w * $h;
        $tw = $max;
      }
      elseif ($h > $w && $max < $h)
      {
        $tw = $max / $h * $w;
        $th = $max;
      }
      elseif ($max < $w)
      {
        $tw = $th = $max;
      }

      $tmp = imagecreatetruecolor($tw, $th);
      imagecopyresampled($tmp, $src, 0, 0, 0, 0, $tw, $th, $w, $h);
      imageconvolution($tmp, array(array(-1, -1, -1),
        array(-1, 16, -1), array(-1, -1, -1)), 8, 0);
      imagejpeg($tmp, $saveto);
      imagedestroy($tmp);
      imagedestroy($src);
    }
  }

  showProfile($user);

echo <<<_END
      <form data-ajax='false' method='post'
        action='profile.php?r=$randstr' enctype='multipart/form-data'>
      <h3>Enter or edit your details and/or upload an image</h3>
      <textarea name='text'>$text</textarea><br>
      Image: <input type='file' name='image' size='14'>
      <input type='submit' value='Save Profile'>
      </form>
    </div><br>
  </body>
</html>
_END;
?>

图 29-4. 编辑用户配置文件

members.php

使用 示例 29-9,members.php,您的用户将能够找到其他成员,并选择将其添加为好友(如果他们已经是好友,则删除)。此程序有两种模式。第一种列出所有成员及其与您的关系,第二种显示用户的个人资料。

查看用户个人资料

后续代码是为了处理后一种模式,首先检测从 $_GET 数组中获取的变量 view 是否存在。如果存在,则用户想要查看某人的个人资料,因此程序将使用 showProfile 函数执行该操作,并提供一些链接到用户的朋友和消息。

添加和删除好友

在此之后,将测试两个 $_GET 变量 addremove。如果其中一个变量有值,它将是要添加或删除的用户的用户名。我们通过在 MySQL 的 friends 表中查找用户来实现这一点,并插入或从表中删除该用户名。

当然,每个提交的变量都首先通过 sanitizeString 函数进行处理,以确保在与 MySQL 一起使用时安全。

列出所有成员

代码的最后部分发出 SQL 查询来列出所有用户名。代码在输出页面标题之前将返回的数字放入变量$num中。

然后,for循环遍历每个成员,获取其详细信息,然后在friends表中查找他们,看他们是被用户关注还是正在关注用户。如果某人既是粉丝又是关注者,则被归类为共同朋友。

当用户关注另一个成员时,变量$t1为非零值;当另一个成员关注用户时,变量$t2为非零值。根据这些值,在每个用户名后显示文本,显示与当前用户的关系(如果有的话)。

还显示图标以展示这些关系。双向箭头表示用户是共同朋友,左箭头表示用户正在关注另一个成员,右箭头表示另一个成员正在关注用户。

最后,根据用户是否在关注另一个成员,提供链接以添加或取消该成员作为朋友。

当在浏览器中调用示例 29-9 时,显示效果类似于图 29-5。请注意用户被邀请“关注”一个未关注的成员,但如果成员已经在关注用户,则会提供一个“回礼”链接以回报友谊。如果用户已经在关注另一个成员,则用户可以选择“取消”来结束关注。

图 29-5. 使用成员模块
注意

在生产服务器上,可能会有数千甚至数十万的用户,因此您将大幅修改此程序,以支持搜索“关于我”文本、分页输出等。

示例 29-9. members.php
<?php // Example 09: members.php
  require_once 'header.php';

  if (!$loggedin) die("</div></body></html>");

  if (isset($_GET['view']))
  {
    $view = sanitizeString($_GET['view']);

    if ($view == $user) $name = "Your";
    else                $name = "$view's";

    echo "<h3>$name Profile</h3>";
    showProfile($view);
    echo "<a data-role='button' data-transition='slide'
          href='messages.php?view=$view&r=$randstr'>View $name messages</a>";
    die("</div></body></html>");
  }

  if (isset($_GET['add']))
  {
    $add = sanitizeString($_GET['add']);

    $result = queryMysql("SELECT * FROM friends
      WHERE user='$add' AND friend='$user'");
    if (!$result->rowCount)
      queryMysql("INSERT INTO friends VALUES ('$add', '$user')");
  }
  elseif (isset($_GET['remove']))
  {
    $remove = sanitizeString($_GET['remove']);
    queryMysql("DELETE FROM friends
      WHERE user='$remove' AND friend='$user'");
  }

  $result = queryMysql("SELECT user FROM members ORDER BY user");
  $num    = $result->rowCount();

  while ($row = $result->fetch())
  {
    if ($row['user'] == $user) continue;

    echo "<li><a data-transition='slide' href='members.php?view=" .
      $row['user'] . "&$randstr'>" . $row['user'] . "</a>";
    $follow = "follow";

    $result1 = queryMysql("SELECT * FROM friends WHERE
      user='" . $row['user'] . "' AND friend='$user'");
    $t1      = $result1->rowCount();

    $result1 = queryMysql("SELECT * FROM friends WHERE
      user='$user' AND friend='" . $row['user'] . "'");
    $t2      = $result1->rowCount();

    if (($t1 + $t2) > 1) echo " &harr; is a mutual friend";
    elseif ($t1)         echo " &larr; you are following";
    elseif ($t2)       { echo " &rarr; is following you";
                         $follow = "recip"; }

    if (!$t1) echo " [<a data-transition='slide'
      href='members.php?add=" . $row['user'] . "&r=$randstr'>$follow</a>]";
    else      echo " [<a data-transition='slide'
      href='members.php?remove=" . $row['user'] . "&r=$randstr'>drop</a>]";
  }

?>
    </ul></div>
  </body>
</html>

friends.php

显示用户的朋友和粉丝的模块是示例 29-10,friends.php。这个程序像members.php一样查询friends表,但只针对单个用户。然后显示所有该用户的共同朋友和粉丝,以及他们正在关注的人。

所有粉丝都保存在名为$followers的数组中,所有正在关注的人都放在名为$following的数组中。然后使用一段整洁的代码来提取所有既是粉丝又被用户关注的人,如下所示:

$mutual = array_intersect($followers, $following);

函数array_intersect提取两个数组中共同的所有成员,并返回一个新数组,其中仅包含这些人。然后可以使用array_diff函数来分别处理$followers$following数组,只保留不是共同朋友的人,如下所示:

$followers = array_diff($followers, $mutual);
$following = array_diff($following, $mutual);

这导致数组$mutual仅包含共同的朋友,$followers仅包含粉丝(没有共同朋友),$following仅包含正在关注的人(没有共同朋友)。

现在我们掌握了这些数组,分别显示每个类别的成员就变得非常简单,如 Figure 29-6 所示。PHP 的sizeof函数返回数组中元素的数量;在这里,我仅在大小非零时触发代码(即当该类型的朋友存在时)。请注意,通过在相关位置使用变量$name1$name2$name3,代码可以知道您正在查看自己的朋友列表,并使用YourYou are而不仅仅显示用户名。如果希望在此屏幕上显示用户的个人资料信息,则可以取消注释该行。

Example 29-10. friends.php
<?php // Example 10: friends.php
  require_once 'header.php';

  if (!$loggedin) die("</div></body></html>");

  if (isset($_GET['view'])) $view = sanitizeString($_GET['view']);
  else                      $view = $user;

  if ($view == $user)
  {
    $name1 = $name2 = "Your";
    $name3 =          "You are";
  }
  else
  {
    $name1 = "<a data-transition='slide'
              href='members.php?view=$view&r=$randstr'>$view</a>'s";
    $name2 = "$view's";
    $name3 = "$view is";
  }

  // Uncomment this line if you wish the user’s profile to show here
  // showProfile($view);

  $followers = array();
  $following = array();

  $result = queryMysql("SELECT * FROM friends WHERE user='$view'");

  while ($row = $result->fetch())
  {
    $followers[$j] = $row['friend'];
  }

  $result = queryMysql("SELECT * FROM friends WHERE friend='$view'");

  while ($row = $result->fetch())
  {
    $following[$j] = $row['user'];
  }

  $mutual    = array_intersect($followers, $following);
  $followers = array_diff($followers, $mutual);
  $following = array_diff($following, $mutual);
  $friends   = FALSE;

  echo "<br>";

  if (sizeof($mutual))
  {
    echo "<span class='subhead'>$name2 mutual friends</span><ul>";
    foreach($mutual as $friend)
      echo "<li><a data-transition='slide'
            href='members.php?view=$friend&r=$randstr'>$friend</a>";
    echo "</ul>";
    $friends = TRUE;
  }

  if (sizeof($followers))
  {
    echo "<span class='subhead'>$name2 followers</span><ul>";
    foreach($followers as $friend)
      echo "<li><a data-transition='slide'
            href='members.php?view=$friend&r=$randstr'>$friend</a>";
    echo "</ul>";
    $friends = TRUE;
  }

  if (sizeof($following))
  {
    echo "<span class='subhead'>$name3 following</span><ul>";
    foreach($following as $friend)
      echo "<li><a data-transition='slide'
            href='members.php?view=$friend&r=$randstr'>$friend</a>";
    echo "</ul>";
    $friends = TRUE;
  }

  if (!$friends) echo "<br>You don't have any friends yet.";
?>
    </div><br>
  </body>
</html>

Figure 29-6. 显示用户的朋友和追随者

messages.php

主要模块的最后一个是 Example 29-11,messages.php。程序首先检查变量text中是否有消息发布。如果有,它将被插入到messages表中。同时,pm的值也被存储。这表示消息是私有还是公共的。0代表公共消息,1代表私有消息。

接下来,显示用户的个人资料以及输入消息的表单,还有用于选择私有或公共消息的单选按钮。之后,根据它们是私有还是公共消息,显示所有的消息。如果是公共消息,所有用户都可以看到,但私有消息只有发件人和收件人可以看到。所有这些都通过对 MySQL 数据库的一些查询来处理。另外,当消息是私有时,它会以whispered开头并显示为斜体。

最后,程序显示几个链接,用于刷新消息(以防其他用户同时发布消息)和查看用户的朋友。再次使用变量$name1$name2的技巧,使得在查看自己的个人资料时,显示为Your而不是用户名。

Example 29-11. messages.php
<?php // Example 11: messages.php
  require_once 'header.php';

  if (!$loggedin) die("</div></body></html>");

  if (isset($_GET['view'])) $view = sanitizeString($_GET['view']);
  else                      $view = $user;

  if (isset($_POST['text']))
  {
    $text = sanitizeString($_POST['text']);

    if ($text != "")
    {
      $pm   = substr(sanitizeString($_POST['pm']),0,1);
      $time = time();
      queryMysql("INSERT INTO messages VALUES(NULL, '$user',
        '$view', '$pm', $time, '$text')");
    }
  }

  if ($view != "")
  {
    if ($view == $user) $name1 = $name2 = "Your";
    else
    {
      $name1 = "<a href='members.php?view=$view&r=$randstr'>$view</a>'s";
      $name2 = "$view's";
    }

    echo "<h3>$name1 Messages</h3>";
    showProfile($view);

    echo <<<_END
      <form method='post' action='messages.php?view=$view&r=$randstr'>
        <fieldset data-role="controlgroup" data-type="horizontal">
          <legend>Type here to leave a message</legend>
          <input type='radio' name='pm' id='public' value='0' checked='checked'>
          <label for="public">Public</label>
          <input type='radio' name='pm' id='private' value='1'>
          <label for="private">Private</label>
        </fieldset>
      <textarea name='text'></textarea>
      <input data-transition='slide' type='submit' value='Post Message'>
    </form><br>
_END;

    date_default_timezone_set('UTC');

    if (isset($_GET['erase']))
    {
      $erase = sanitizeString($_GET['erase']);
      queryMysql("DELETE FROM messages WHERE id='$erase' AND recip='$user'");
    }

    $query  = "SELECT * FROM messages WHERE recip='$view' ORDER BY time DESC";
    $result = queryMysql($query);

    while ($row = $result->fetch())
    {
      if ($row['pm'] == 0 || $row['auth'] == $user || $row['recip'] == $user)
      {
        echo date('M jS \'y g:ia:', $row['time']);
        echo " <a href='messages.php?view=" . $row['auth'] .
             "&r=$randstr'>" . $row['auth']. "</a> ";

        if ($row['pm'] == 0)
          echo "wrote: &quot;" . $row['message'] . "&quot; ";
        else
          echo "whispered: <span class='whisper'>&quot;" .
            $row['message']. "&quot;</span> ";

        if ($row['recip'] == $user)
          echo "[<a href='messages.php?view=$view" .
               "&erase=" . $row['id'] . "&r=$randstr'>erase</a>]";

        echo "<br>";
      }
    }
  }

  if (!$num)
    echo "<br><span class='info'>No messages yet</span><br><br>";

  echo "<br><a data-role='button'
        href='messages.php?view=$view&r=$randstr'>Refresh messages</a>";
?>

    </div><br>
  </body>
</html>

您可以在浏览器中查看此程序的结果,如 Figure 29-7 所示。请注意,用户查看自己的消息时,提供了链接以删除不想保留的任何消息。还要注意,jQuery Mobile 如何实现对选择发送私有或公共消息的单选按钮的样式化。这在 Chapter 23 中有详细说明。

Figure 29-7. The messaging module

logout.php

我们社交网络食谱的最后一步是示例 29-12 的logout.php,这是一个登出页面,用于关闭会话并删除任何相关数据和 cookie。调用此程序的结果显示在图 29-8 中,用户现在被要求点击一个链接,该链接将带他们到未登录的主页,并从屏幕顶部删除登录链接。当然,您也可以编写 JavaScript 或 PHP 重定向来执行此操作(如果您希望保持登出的整洁状态,则这可能是个好主意)。

图 29-8. 登出页面
示例 29-12. logout.php
<?php // Example 12: logout.php
  require_once 'header.php';

  if (isset($_SESSION['user']))
  {
    destroySession();
    echo "<br><div class='center'>You have been logged out. Please
         <a data-transition='slide'
           href='index.php?r=$randstr'>click here</a>
           to refresh the screen.</div>";
  }
  else echo "<div class='center'>You cannot log out because
             you are not logged in</div>";
?>
    </div>
  </body>
</html>

styles.css

此项目使用的样式表显示在示例 29-13 中。以下是几组声明:

*

使用通用选择器为项目设置默认字体系列和大小。

body

设置项目窗口的宽度,水平居中,指定背景颜色,并添加边框。

html

设置 HTML 部分的背景颜色。

img

给所有图像添加边框、阴影和右侧边距。

.username

居中用户名,并选择字体系列、大小、颜色、背景和填充以显示它。

.info

此类用于显示重要信息。它设置了背景和前景文本颜色,应用了边框和填充,并缩进了使用它的元素。

.center

此类用于居中<div>元素的内容。

.subhead

此类强调文本的各个部分。

.taken.available.error.whisper

这些声明设置用于显示不同类型信息的颜色和字体样式。

#logo

将 logo 文本样式设为 HTML5 以外的浏览器中使用时的备用,以及当画布 logo 无法创建时。

#robin

对页面标题中的 robin 图像进行对齐。

#used

确保由checkuser.php异步调用填充的元素,如果用户名已被使用,则不会与上面的字段太接近。

示例 29-13. styles.css
* {
  font-family:verdana,sans-serif;
  font-size  :14pt;
}

body {
  width     :700px;
  margin    :20px auto;
  background:#f8f8f8;
  border    :1px solid #888;
}

html {
  background:#fff
}

img {
  border            :1px solid black;
  margin-right      :15px;
  -moz-box-shadow   :2px 2px 2px #888;
  -webkit-box-shadow:2px 2px 2px #888;
  box-shadow        :2px 2px 2px #888;
}

.username {
  text-align :center;
  background :#eb8;
  color      :#40d;
  font-family:helvetica;
  font-size  :20pt;
  padding    :4px;
}

.info {
  font-style :italic;
  margin     :40px 0px;
  text-align :center;
}

.center {
  text-align:center;
}

.subhead {
  font-weight:bold;
}

.taken, .error {
  color:red;
}

.available {
  color:green;
}

.whisper {
  font-style:italic;
  color     :#006600;
}

#logo {
  font-family:Georgia;
  font-weight:bold;
  font-style :italic;
  font-size  :97px;
  color      :red;
  }

#robin {
  position          :relative;
  border            :0px;
  margin-left       :-6px;
  margin-right      :0px;
  top               :17px;
  -moz-box-shadow   :0px 0px 0px;
  -webkit-box-shadow:0px 0px 0px;
  box-shadow        :0px 0px 0px;
}

#used {
  margin-top:50px;
}

javascript.js

最后,还有 JavaScript 文件(见示例 29-14),其中包含本书中始终使用的OSC函数。

示例 29-14. javascript.js
function O(i)
{
  return typeof i == 'object' ? i : document.getElementById(i)
}

function S(i)
{
  return O(i).style
}

function C(i)
{
  return document.getElementsByClassName(i)
}

如众所周知,这就是全部。如果您基于此代码或本书中的其他示例编写了任何内容,或者从中获得了其他收获,我很高兴能够提供帮助,并感谢您阅读本书。

问题

  1. 你喜欢从这本书中学到了什么?

查看“第二十九章答案”,在附录 A 中找到这个问题的答案。