65.9K
CodeProject 正在变化。 阅读更多。
Home

JavaScript 前端 Web 应用教程 第一部分:七步构建一个最小化应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (52投票s)

2014年4月2日

CPOL

21分钟阅读

viewsIcon

361703

downloadIcon

5595

学习如何以最少的精力使用纯 JavaScript 构建前端 Web 应用程序。不要使用任何(第三方)框架或库,例如 jQuery 或 Angular,它们会创建黑盒依赖项和开销,并阻止您学习如何自己动手。

引言

本文摘自书籍 使用纯 JavaScript 构建前端 Web 应用,该书可作为开放获取在线书籍。它展示了如何以最少的精力构建前端应用程序,不使用任何(第三方)框架或库。虽然库和框架可能有助于提高生产力,但它们也创建了黑盒依赖项和开销,并且它们阻止您学习如何自己动手。

如果您想先了解其工作原理和外观,可以从我们的服务器运行本文讨论的最小应用程序

前端 Web 应用程序可以由任何 Web 服务器提供,但它在用户的计算机设备(智能手机、平板电脑或笔记本电脑)上执行,而不是在远程 Web 服务器上执行,如以下架构图所示。

通常,但并非总是如此,前端 Web 应用程序是单用户应用程序,不与其他用户共享。

本教程中讨论的 JavaScript 前端数据管理应用程序的最小版本仅包含完整应用程序所需整体功能的最小值。它只处理一种对象类型(Book)并支持四种标准数据管理操作(Create/Read/Update/Delete),但它需要通过 CSS 规则来美化用户界面,并添加应用程序整体功能的其他重要部分

  • 第 2 部分:处理响应式 (HTML5) 约束验证

  • 第 3 部分:处理枚举

  • 第 4 部分:管理单向关联,将作者和出版商分配给书籍

  • 第 5 部分:管理双向关联,也将书籍分配给作者和出版商

  • 第 6 部分:处理类层次结构中的子类型(继承)关系

背景

本节简要讨论 HTML 和 JavaScript 的一些元素,假设读者已经熟悉基本的编程概念并具有一些编程经验,例如在 PHP、Java 或 C# 中。

HTML

我们采用符号方程

HTML = HTML5 = XHTML5

声明当我们只说“HTML”或“HTML5”时,我们实际上指的是 XHTML5,因为我们更喜欢 XML 文档的清晰语法,而不是 HTML5 也允许的自由和令人困惑的 HTML4 风格语法。

JavaScript 对象

JavaScript 对象与经典的 OO/UML 对象不同。特别是,它们不必实例化一个类。它们可以以方法槽的形式拥有自己的(实例级)方法,因此它们不仅拥有(普通)属性槽,还拥有方法槽。此外,它们还可以拥有键值槽。因此,它们可以有三种不同类型的槽,而经典对象(在 UML 中称为“实例规范”)只有属性槽。

JavaScript 对象可以通过许多不同的方式用于不同的目的。以下是 JavaScript 对象的五种不同用例或可能含义

  1. 记录是一组属性槽,例如:

    var myRecord = { firstName:"Tom", lastName:"Smith", age:26} 
  2. 映射(或“哈希映射”或“关联数组”)是一组键值槽。它支持根据查找,例如:

    var numeral2number = { "one":1, "two":2, "three":3}

    它将数字值 1 与键“one”关联,2 与“two”关联,等等。键不必是有效的 JavaScript 标识符,它可以是任何类型的字符串 (例如,它可能包含空格)。

  3. 无类型对象不实例化类。它可能具有属性槽和方法槽,例如:

    var person1 = {  
      lastName: "Smith",  
      firstName: "Tom",
      getInitials: function () {
        return this.firstName.charAt(0) + this.lastName.charAt(0); 
      }  
    };
  4. 命名空间可以以一个由全局对象变量引用的无类型对象的形式定义,该变量的名称表示命名空间前缀。例如,以下对象变量提供了基于模型-视图-控制器 (MVC) 架构范式的应用程序的主要命名空间,其中我们有三个子命名空间对应于 MVC 应用程序的三个部分

    var myApp = { model:{}, view:{}, ctrl:{} };
  5. 通过表达式创建实例化 JavaScript 构造函数 C 定义的类的类型化对象 o

    var o = new C(...)

    这种类型化对象的类型/类可以通过内省表达式检索

    o.constructor.name  // returns "C" 

映射

映射通过一个特殊的循环来处理,我们使用预定义函数 Object.keys(a) 遍历映射的所有键,该函数返回映射 a 的所有键的数组。例如:

var i=0, key="";
for (i=0; i < Object.keys( numeral2number).length; i++) {
  key = Object.keys( numeral2number)[i];
  alert('The numeral '+ key +' denotes the number '+ numeral2number[key]);
}

对于向映射添加新元素,我们只需创建一个新的键值条目,如下所示

numeral2number["thirty two"] = 32; 

对于从映射删除元素,我们可以使用预定义的 JavaScript delete 运算符,如下所示

delete numeral2number["thirty two"];

JavaScript 支持三种数据结构

这三种数据结构(或复杂数据类型)是

  1. 如上所述,记录是特殊的 JS 对象,
  2. 如上所述,映射也是特殊的 JS 对象,
  3. 数组列表,它们是称为“数组”的特殊 JS 对象,但由于它们是动态的,它们更像是数组列表(如 Java 所定义)。

定义和实例化类

一个类可以分两步定义。首先,定义构造函数,它定义类的属性并将参数值分配给它们

function Person( first, last) {
  this.firstName = first; 
  this.lastName = last; 
}

接下来,将类的实例级方法定义为构造函数原型对象属性的函数槽

Person.prototype.getInitials = function () {
  return this.firstName.charAt(0) + this.lastName.charAt(0); 
}

最后,类级(“静态”)方法可以定义为构造函数的函数槽,如下所示

Person.checkName = function (n) {
  ... 
}

通过对构造函数应用 new 运算符来创建类的实例

var pers1 = new Person("Tom","Smith");

使用“点符号”在 Person 对象 pers1 上调用 getInitials 方法

alert("The initials of the person are: " + pers1.getInitials()); 

编写应用程序

我们示例应用程序的目的是管理书籍信息。也就是说,我们处理单一对象类型:Book,如下图所示。

这样的信息管理应用程序需要什么?应用程序必须支持四种标准用例

  1. 创建:输入要添加到受管图书集合中的图书数据。

  2. 读取:显示受管图书集合中所有图书的列表。

  3. 更新图书的数据。

  4. 删除一本书籍记录。

为了借助计算机键盘和屏幕输入数据,我们可以使用 HTML 表单,它为 Web 应用程序提供了用户界面技术。

为了维护数据对象集合,我们需要一种存储技术,可以将数据对象以持久记录的形式保存在辅助存储设备上,例如硬盘或固态硬盘。现代 Web 浏览器提供两种此类技术:较简单的一种称为本地存储,功能更强大的一种称为IndexDB。对于我们的最小示例应用程序,我们使用本地存储。

第 1 步 - 设置文件夹结构

在第一步中,我们为应用程序设置文件夹结构。我们为应用程序选择一个名称,例如“Public Library”,以及一个相应的(可能缩写的)应用程序文件夹名称,例如“publicLibrary”。然后我们在计算机磁盘上创建此文件夹,并为 JavaScript 源代码文件创建一个子文件夹“src”。在此文件夹中,我们按照软件应用程序架构的模型-视图-控制器范例创建子文件夹“model”、“view”和“ctrl”。最后,我们为应用程序的起始页创建一个 index.html 文件,如下所述。因此,我们最终得到以下文件夹结构

publicLibrary
  src
    ctrl
    model
    view
  index.html

应用程序的起始页加载 Book.js 模型类文件,并提供一个菜单,用于选择由相应页面(例如 createBook.html)执行的 CRUD 数据管理操作,或借助 Book.createTestData() 过程创建测试数据,或使用 Book.clearData() 清除所有数据

最小应用程序的起始页 index.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Minimal JS front-end App Example</title>
  <script src="src/model/Book.js"></script>
</head>
<body>
  <h1>Public Library</h1>
  <h2>An Example of a Minimal JavaScript front-end App</h2>
  <p>This app supports the following operations:</p>
  <menu>
    <li><a href="listBooks.html"><button type="button">List all books</button></a></li>
    <li><a href="createBook.html"><button type="button">Add a new book</button></a></li>
    <li><a href="updateBook.html"><button type="button">Update a book</button></a></li>
    <li><a href="deleteBook.html"><button type="button">Delete a book</button></a></li>
    <li><button type="button" onclick="Book.clearData()">Clear database</button></li>
    <li><button type="button" onclick="Book.createTestData()">Create test data</button></li>
  </menu>
</body>
</html> 

第 2 步 - 编写模型代码

在第二步中,我们将模型类的代码写入一个特定的 JavaScript 文件中。在上面显示的信息设计模型中,只有一个类,代表对象类型 Book。因此,在 src/model 文件夹中,我们创建一个 Book.js 文件,该文件最初包含以下代码

function Book( slots) {
  this.isbn = slots.isbn;
  this.title = slots.title;
  this.year = slots.year;
};

模型类 Book 被编码为具有单个 slots 参数的 JavaScript 构造函数,该参数应该是一个记录对象,具有 isbntitleyear 属性,分别代表 Book 类的 ISBN、标题和年份属性的值。因此,在构造函数中,每当创建新对象作为此类的实例时,slots 属性的值都会分配给相应的属性。

除了以构造函数的形式定义模型类之外,我们还在 Book.js 文件中定义了以下各项

  1. 一个类级属性 Book.instances,表示应用程序以映射形式管理的所有 Book 实例的集合。

  2. 一个类级方法 Book.loadAll,用于从持久数据存储中加载所有受管 Book 实例。

  3. 一个类级方法 Book.saveAll,用于将所有受管 Book 实例保存到持久数据存储中。

  4. 一个类级方法 Book.add,用于创建和存储新的 Book 记录。

  5. 一个类级方法 Book.update,用于更新现有 Book 记录。

  6. 一个类级方法 Book.destroy,用于删除 Book 实例。

  7. 一个类级方法 Book.createTestData,用于创建一些示例图书记录作为测试数据。

  8. 一个类级方法 Book.clearData,用于清除图书数据存储。

1. 表示所有 Book 实例的集合

为了表示应用程序管理的所有 Book 实例的集合,我们以下列方式定义并初始化类级属性 Book.instances

Book.instances = {}; 

因此,最初我们的藏书是空的。实际上,它被定义为一个空对象,因为我们希望以映射(一组键值槽,也称为“哈希映射”)的形式表示它,其中 ISBN 是访问相应图书对象(作为与键关联的值)的键。我们可以将这种映射的结构可视化为查找表,如表 1 所示。

表 1:表示图书集合的映射
006251587X { isbn:"006251587X," title:"Weaving the Web", year:2000 }
0465026567 { isbn:"0465026567," title:"Gödel, Escher, Bach", year:1999 }
0465030793 { isbn:"0465030793," title:"I Am A Strange Loop", year:2008 }

请注意,此映射的值是与表行对应的简单记录对象。因此,我们也可以在简单表中表示它们,如表 2 所示。

表 2:表示为表的图书对象集合
ISBN 标题 年份
006251587X 织网 2000
0465026567 哥德尔、埃舍尔、巴赫 1999
0465030793 我是一个奇怪的循环 2008

2. 加载所有 Book 实例

对于持久数据存储,我们使用本地存储,它是现代网络浏览器支持的 HTML5 JavaScript API。从本地存储加载图书记录涉及三个步骤

  1. 借助赋值语句从本地存储中检索已存储为带有键“bookTable”的大字符串的图书表

    bookTableString = localStorage["bookTable"];

    此检索在下面程序清单的第 5 行执行。

  2. 借助内置函数 JSON.parse 将图书表字符串转换为包含图书行作为元素的相应映射 bookTable

    bookTable = JSON.parse( bookTableString); 

    此转换在下面程序清单的第 11 行执行,称为反序列化。

  3. 借助在 Book 类中定义为“static”(类级)方法的 convertRow2Obj 过程,将 bookTable 的每一行(表示一个无类型记录对象)转换为存储为 Book.instances 映射元素中相应 Book 类型的对象

    Book.convertRow2Obj = function (bookRow) {
      var book = new Book( bookRow);
      return book;
    };

这是该过程的完整代码

Book.loadAll = function () {
  var i=0, key="", keys=[], bookTableString="", bookTable={};  
  try {
    if (localStorage["bookTable"]) {
      bookTableString = localStorage["bookTable"];
    }
  } catch (e) {
    alert("Error when reading from Local Storage\n" + e);
  }
  if (bookTableString) {
    bookTable = JSON.parse( bookTableString);
    keys = Object.keys( bookTable);
    console.log( keys.length +" books loaded.");
    for (i=0; i < keys.length; i++) {
      key = keys[i];
      Book.instances[key] = Book.convertRow2Obj( bookTable[key]);
    }
  }
};

请注意,由于像 localStorage["bookTable"] 这样的输入操作可能会失败,我们将其放在 try-catch 块中,这样当输入操作失败时,我们就可以跟进错误消息。

3. 保存所有 Book 实例

将主内存中的 Book.instances 集合中的所有图书对象保存到辅助内存中的本地存储涉及两个步骤

  1. 借助预定义的 JavaScript 函数 JSON.stringify 将映射 Book.instances 转换为 string

    bookTableString = JSON.stringify( Book.instances);

    此转换称为序列化。

  2. 将生成的 string 作为键“bookTable”的值写入本地存储

    localStorage["bookTable"] = bookTableString;

这两个步骤在以下程序清单的第 5 行和第 6 行执行

Book.saveAll = function () {
  var bookTableString="", error=false,
      nmrOfBooks = Object.keys( Book.instances).length;  
  try {
    bookTableString = JSON.stringify( Book.instances);
    localStorage["bookTable"] = bookTableString;
  } catch (e) {
    alert("Error when writing to Local Storage\n" + e);
    error = true;
  }
  if (!error) console.log( nmrOfBooks + " books saved.");
};

4. 创建新的 Book 实例

Book.add 过程负责创建一个新的 Book 实例并将其添加到 Book.instances 集合中

Book.add = function (slots) {
  var book = new Book( slots);
  Book.instances[slots.isbn] = book;
  console.log("Book " + slots.isbn + " created!");
};

5. 更新现有 Book 实例

为了更新现有的 Book 实例,我们首先从 Book.instances 中检索它,然后重新分配那些已更改值的属性

Book.update = function (slots) {
  var book = Book.instances[slots.isbn];
  var year = parseInt( slots.year);
  if (book.title !== slots.title) { book.title = slots.title;}
  if (book.year !== year) { book.year = year;}
  console.log("Book " + slots.isbn + " modified!");
};

请注意,对于数字属性(如 year),我们必须确保将相应输入参数 (y) 的值(通常通过 HTML 表单从用户输入中获取)从 String 转换为 Number,使用 parseIntparseFloat 这两个类型转换函数之一。

6. 删除现有 Book 实例

通过首先测试映射是否具有给定键的元素(第 2 行),然后应用 JavaScript 内置的 delete 运算符,从 Book.instances 集合中删除一个 Book 实例:该运算符从对象中删除一个槽,或者在我们的示例中,从映射中删除一个元素

Book.destroy = function (isbn) {
  if (Book.instances[isbn]) {
    console.log("Book " + isbn + " deleted");
    delete Book.instances[isbn];
  } else {
    console.log("There is no book with ISBN " + isbn + " in the database!");
  }
}; 

7. 创建测试数据

为了能够测试我们的代码,我们可以创建一些测试数据并将其保存在我们的本地存储数据库中。我们可以使用以下过程来完成此操作

Book.createTestData = function () {
  Book.instances["006251587X"] = new Book({isbn:"006251587X", title:"Weaving the Web", year:2000});
  Book.instances["0465026567"] = new Book({isbn:"0465026567", title:"Gödel, Escher, Bach", year:1999});
  Book.instances["0465030793"] = new Book({isbn:"0465030793", title:"I Am A Strange Loop", year:2008});
  Book.saveAll();
};

8. 清除所有数据

以下过程清除本地存储中的所有数据

Book.clearData = function () {
  if (confirm("Do you really want to delete all book data?")) {
    localStorage["bookTable"] = "{}";
  }
};

第 3 步 - 初始化应用程序

我们通过定义其命名空间和 MVC 子命名空间来初始化应用程序。命名空间是软件工程和许多编程语言(包括 Java 和 PHP)中的一个重要概念,它们提供对命名空间的特定支持,有助于将相关代码分组并避免名称冲突。由于 JavaScript 中没有对命名空间的特定支持,我们为此目的使用特殊对象(我们可以称之为“命名空间对象”)。首先,我们为我们的应用程序定义一个根命名空间(对象),然后我们定义三个子命名空间,每个子命名空间对应应用程序代码的三个部分之一:模型、视图和控制器。在我们的示例应用程序中,我们可以使用以下代码来完成此操作

var pl = { model:{}, view:{}, ctrl:{} }; 

在这里,主命名空间定义为 pl,代表“Public Library”,其三个子命名空间 modelviewctrl 最初是空对象。我们将此代码放在 ctrl 文件夹中的单独文件 initialize.js 中,因为此类命名空间定义属于应用程序代码的控制器部分。

第 4 步 - 实现列表对象用例

此用例对应于四个基本数据管理用例(创建-读取-更新-删除,CRUD)中的“读取”。

此用例的用户界面由包含用于显示图书对象的 HTML 表的以下 HTML 页面提供。对于我们的示例应用程序,此页面将命名为 listBooks.html(在主文件夹 publicLibrary 中),并将包含以下 HTML 代码

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Minimal JS front-end App Example</title>
  <script src="src/ctrl/initialize.js"></script>
  <script src="src/model/Book.js"></script>
  <script src="src/view/listBooks.js"></script>
  <script>
   window.addEventListener( "load", pl.view.listBooks.setupUserInterface);
  </script>
</head>
<body>
  <h1>Public Library: List all books</h1>
  <table id="books">
    <thead><tr><th>ISBN</th><th>Title</th><th>Year</th></tr></thead>
    <tbody></tbody>
  </table>
  <nav><a href="index.html">Back to main menu</a></nav>
</body>
</html>

请注意,此 HTML 文件加载了三个 JavaScript 文件:控制器文件 src/ctrl/initialize.js、模型文件 src/model/Book.js 和视图文件 src/view/listBooks.js。前两个文件包含用于初始化应用程序和模型类 Book 的代码,如上所述,第三个文件(代表“列出图书”操作的 UI 代码)现在正在开发中。实际上,对于此操作,我们只需要一个用于设置数据管理上下文和 UI 的过程,称为 setupUserInterface

 pl.view.listBooks = {
  setupUserInterface: function () {
    var tableBodyEl = document.querySelector("table#books>tbody");
    var i=0, keys=[], key="", row={};
    // load all book objects
    Book.loadAll();
    keys = Object.keys( Book.instances);
    // for each book, create a table row with a cell for each attribute
    for (i=0; i < keys.length; i++) {
      key = keys[i];
      row = tableBodyEl.insertRow();
      row.insertCell(-1).textContent = Book.instances[key].isbn;      
      row.insertCell(-1).textContent = Book.instances[key].title;  
      row.insertCell(-1).textContent = Book.instances[key].year;
    }
  }
};

此过程的简单逻辑包括两个步骤

  1. 从持久数据存储中读取所有对象的集合(第 6 行)。
  2. 在屏幕上的 HTML 表中将每个对象显示为一行(从第 9 行开始的循环中)。

更具体地说,setupUserInterface 过程首先通过调用 Book.loadAll() 从本地存储检索到的相应行创建图书对象,然后遍历 Book.instances 映射的所有键值槽(其中每个值都表示一个 book 对象)来创建视图表。在此循环的每个步骤中,使用 JavaScript DOM 操作 insertRow() 在表体元素中创建新行,然后使用 DOM 操作 insertCell() 在此行中创建三个单元格:第一个用于图书对象的 isbn 属性值,第二和第三个用于其 titleyear 属性值。insertRowinsertCell 都必须使用参数 -1 调用,以确保新元素附加到行和单元格列表中。

第 5 步 - 实现创建对象用例

对于带有用户输入的数据管理操作(例如“创建对象”操作),需要一个带有 HTML 表单的 HTML 页面作为用户界面。该表单为 Book 类的每个属性都有一个表单字段。对于我们的示例应用程序,此页面将命名为 createBook.html(在 app 文件夹 publicLibrary 中),并将包含以下 HTML 代码

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Minimal JS front-end App Example</title>    
  <script src="src/ctrl/initialize.js"></script>
  <script src="src/model/Book.js"></script>
  <script src="src/view/createBook.js"></script>
  <script>
   window.addEventListener("load", pl.view.createBook.setupUserInterface);
  </script>
</head>
<body>
  <h1>Public Library: Create a new book record</h1>
  <form id="Book">
    <p><label>ISBN: <input name="isbn" /></label></p>
    <p><label>Title: <input name="title" /></label></p>
    <p><label>Year: <input name="year" /></label></p>
    <p><button type="button" name="commit">Save</button></p>
  </form>
  <nav><a href="index.html">Back to main menu</a></nav>
</body>
</html>

视图代码文件 src/view/createBook.js 包含两个过程

  1. setupUserInterface 负责从持久数据存储中检索所有对象的集合,并在保存按钮上设置事件处理程序 (handleSaveButtonClickEvent),以通过保存用户输入数据来处理点击按钮事件;

  2. handleSaveButtonClickEvent 从表单字段中读取用户输入数据,然后通过调用 Book.saveRow 过程保存此数据。

pl.view.createBook = {
  setupUserInterface: function () {
    var saveButton = document.forms['Book'].commit;
    // load all book objects
    Book.loadAll();
    // Set an event handler for the save/submit button
    saveButton.addEventListener("click", 
        pl.view.createBook.handleSaveButtonClickEvent);
    window.addEventListener("beforeunload", function () {
        Book.saveAll(); 
    });
  },
  handleSaveButtonClickEvent: function () {
    var formEl = document.forms['Book'];
    var slots = { isbn: formEl.isbn.value, 
        title: formEl.title.value, 
        year: formEl.year.value};
    Book.add( slots);
    formEl.reset();
  }
};

第 6 步 - 实现更新对象用例

同样,我们有一个用户界面页面(updateBook.html)和一个视图代码文件(src/view/updateBook.js)。“更新对象”操作的 UI HTML 表单有一个选择字段用于选择要更新的图书,以及一个用于 Book 类的每个属性的表单字段。但是,标准标识符属性(ISBN)的表单字段是只读的,因为我们不允许更改现有对象的标准标识符。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Minimal JS front-end App Example</title>
  <script src="src/ctrl/initialize.js"></script>
  <script src="src/model/Book.js"></script>
  <script src="src/view/updateBook.js"></script>
  <script>
   window.addEventListener("load", pl.view.updateBook.setupUserInterface);
  </script>
</head>
<body>
  <h1>Public Library: Update a book record</h1>
  <form id="Book">
    <p>
      <label>Select book: 
        <select name="selectBook"><option value=""> --- </option></select>
      </label>
    </p>
    <p><label>ISBN: <input name="isbn" readonly="readonly" /></label></p>
    <p><label>Title: <input name="title" /></label></p>
    <p><label>Year: <input name="year" /></label></p>
    <p><button type="button" name="commit">Save Changes</button></p>
  </form>
  <nav><a href="index.html">Back to main menu</a></nav>
</body>
</html>

setupUserInterface 过程现在必须通过从持久数据存储中检索所有图书对象的集合来设置选择字段,以填充 select 元素的选项列表

pl.view.updateBook = {
  setupUserInterface: function () {
    var formEl = document.forms['Book'],
        saveButton = formEl.commit,
        selectBookEl = formEl.selectBook;
    var i=0, key="", keys=[], book=null, optionEl=null;
    // load all book objects
    Book.loadAll();
    // populate the selection list with books
    keys = Object.keys( Book.instances);
    for (i=0; i < keys.length; i++) {
      key = keys[i];
      book = Book.instances[key];
      optionEl = document.createElement("option");
      optionEl.text = book.title;
      optionEl.value = book.isbn;
      selectBookEl.add( optionEl, null);
    }
    // when a book is selected, populate the form with the book data
    selectBookEl.addEventListener("change", function () {
        var book=null, key = selectBookEl.value;
        if (key) {
          book = Book.instances[key];
          formEl.isbn.value = book.isbn;
          formEl.title.value = book.title;
          formEl.year.value = book.year;
        } else {
          formEl.isbn.value = "";
          formEl.title.value = "";
          formEl.year.value = "";
        }
    });
    saveButton.addEventListener("click", 
        pl.view.updateBook.handleUpdateButtonClickEvent);
    window.addEventListener("beforeunload", function () {
        Book.saveAll(); 
    });
  },
  // save updated data
  handleUpdateButtonClickEvent: function () {
    var formEl = document.forms['Book'];
    var slots = { isbn: formEl.isbn.value, 
        title: formEl.title.value, 
        year: formEl.year.value
    };
    Book.update( slots);
    formEl.reset();
  }
}; 

第 7 步 - 实现删除对象用例

对于“删除对象”用例,UI 表单只有一个选择字段,用于选择要删除的图书

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Minimal JS front-end App Example</title>
  <script src="src/ctrl/initialize.js"></script>
  <script src="src/model/Book.js"></script>
  <script src="src/view/deleteBook.js"></script>
  <script>
   window.addEventListener("load", pl.view.deleteBook.setupUserInterface);
  </script>
</head>
<body>
  <h1>Public Library: Delete a book record</h1>
  <form id="Book">
    <p>
      <label>Select book: 
        <select name="selectBook"><option value=""> --- </option></select>
      </label>
    </p>
    <p><button type="button" name="commit">Delete</button></p>
  </form>
  <nav><a href="index.html">Back to main menu</a></nav>
</body>
</html>

src/view/deleteBook.js 中的视图代码包含以下两个过程

pl.view.deleteBook = {
  setupUserInterface: function () {
    var deleteButton = document.forms['Book'].commit;
    var selectEl = document.forms['Book'].selectBook;
    var i=0, key="", keys=[], book=null, optionEl=null;
    // load all book objects
    Book.loadAll();
    keys = Object.keys( Book.instances);
    // populate the selection list with books
    for (i=0; i < keys.length; i++) {
      key = keys[i];
      book = Book.instances[key];
      optionEl = document.createElement("option");
      optionEl.text = book.title;
      optionEl.value = book.isbn;
      selectEl.add( optionEl, null);
    }
    deleteButton.addEventListener("click", 
        pl.view.deleteBook.handleDeleteButtonClickEvent);
    window.addEventListener("beforeunload", function () {
        Book.saveAll(); 
    });
  },
  handleDeleteButtonClickEvent: function () {
    var selectEl = document.forms['Book'].selectBook;
    var isbn = selectEl.value;
    if (isbn) {
      Book.destroy( isbn);
      selectEl.remove( selectEl.selectedIndex);
    }
  }
};

运行应用程序并获取代码

您可以从我们的服务器运行最小应用程序,并在web-engineering.info上找到更多关于网络工程的资源,包括开放获取书籍。

可能的变体和扩展

使用 IndexDB 而非 LocalStorage

除了使用本地存储 API,还可以使用 IndexDB API 来本地存储应用程序数据。使用本地存储,您只有一个数据库(您可能需要与其他来自同一域的应用程序共享),并且不支持数据库表(我们在方法中解决了这个限制)。使用IndexedDB,您可以为您的应用程序设置一个特定的数据库,并且可以定义数据库表,称为“对象存储”,它们可以有索引,用于借助索引属性而不是标准标识符属性访问记录。此外,由于IndexedDB支持更大的数据库,其访问方法是异步的,并且只能在数据库事务的上下文中调用。

或者,为了在(REST)web API 的帮助下远程存储应用程序数据,可以使用后端解决方案组件或云存储服务。远程存储方法允许管理更大的数据库并支持多用户应用程序。

使用 <time> 元素表达日期/时间信息

假设我们的 Book 模型类有一个额外的属性 publicationDate,其值必须包含在 HTML 表和表单中。虽然日期/时间信息项必须以人类可读的字符串形式(最好是根据用户浏览器设置的本地化形式)显示在网页上,但以这种形式存储日期/时间值并不是一个好主意。相反,我们使用预定义的 JavaScript 类 Date 的实例来表示和存储日期/时间值。在这种形式下,预定义的函数 toISOString()toLocaleDateString() 可用于将 Date 值转换为 ISO 标准日期/时间字符串(形式为“2015-01-27”)或本地化日期/时间字符串(如“27.1.2015”)。请注意,为简单起见,我们省略了日期/时间字符串的时间部分。

总之,日期/时间值以三种不同的形式表达

  1. 内部,用于存储和计算,作为 Date 值。

  2. 内部,用于注释本地化日期/时间字符串,或外部,用于以标准形式显示日期/时间值,作为 ISO 标准日期/时间字符串,例如,借助 toISOString()

  3. 外部,用于以本地化形式显示日期/时间值,作为本地化日期/时间字符串,例如,借助 toLocaleDateString()

当日期/时间值要包含在网页中时,我们可以使用 <time> 元素,它允许显示人类可读的表示(通常是本地化的日期/时间字符串),并用日期/时间值的标准(机器可读)形式进行注释。

我们用以下网页示例来说明 <time> 元素的使用,该网页包含两个 <time> 元素:一个用于显示固定日期,另一个(最初为空)元素用于显示今天日期,该日期通过 JavaScript 函数计算。在这两种情况下,我们都使用 datetime 属性来用相应的机器可读表示注释显示的人类可读日期。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
 <meta charset="UTF-8" />
 <title>Using the HTML5 Time Element</title>
 <script src="assignDate.js"></script>
 <script>window.addEventListener("load", assignDate);</script>
</head>
<body>
 <h1>HTML5 Time Element</h1>
 <p>HTML 2.0 was published on <time datetime="1995-11-24">November 24, 1995</time>.</p>
 <p>Today is <time id="today" datetime=""></time>.</p>
</body>
</html> 

此网页加载并执行以下 JavaScript 函数,用于将今天的日期计算为 Date 值,并将其 ISO 标准表示和本地化表示分配给 <time> 元素

function assignDate() {
  var dateEl = document.getElementById("today");
  var today = new Date();
  dateEl.textContent = today.toLocaleDateString();
  dateEl.setAttribute("datetime", today.toISOString());
}

注意事项

此应用程序的代码应通过以下方式扩展

  • 为用户界面页面添加一些CSS 样式
  • 添加约束验证

我们将在后续教程 JavaScript 前端 Web 应用教程第 2 部分:添加约束验证 中展示如何实现。

这个示例应用的代码的另一个问题是,每个类的数据存储管理方法(如 addupdate 等)都需要样板代码。虽然写几次这样的代码对于学习应用开发是有益的,但当你从事实际项目时,你不会想一遍又一遍地重复编写。在另一篇文章 使用 mODELcLASSjs 的声明式和响应式约束验证 中,我们提出了一种将这些方法以通用形式实现的方法,以便它们可以重用于应用程序的所有类。

实践项目

请随时在下面的 CodeProject 评论区提出关于这些项目的任何问题。

项目 1 - 开发一个用于管理电影数据的 JavaScript 前端应用程序

该应用程序的目的是管理电影信息。与教程中讨论的图书数据管理应用程序一样,您可以做出简化假设,即所有数据都可以保存在主内存中。因此,在应用程序启动时,电影数据从持久数据存储中读取。当用户退出应用程序时,数据必须保存到持久数据存储中,该存储应像教程中一样使用 JavaScript 的本地存储 API 实现。

该应用程序只处理一种对象类型:Movie,如下图所示。在教程的后续部分中,您将通过添加完整性约束、演员和导演作为进一步的模型类以及它们之间的关联来扩展这个简单的应用程序。

请注意,在该项目的大部分内容中,您可以遵循甚至复制图书数据管理应用程序的代码,不同之处在于现在有一个范围为 Date 的属性,因此您必须在列表对象表中使用 HTML <time> 元素,如上面“可能的变体和扩展”一节中所述。

要开发该应用程序,只需遵循教程中描述的七个步骤序列

  1. 第 1 步 - 设置文件夹结构

  2. 第 2 步 - 编写模型代码

  3. 第 3 步 - 初始化应用程序

  4. 第 4 步 - 实现列表对象用例

  5. 第 5 步 - 实现创建对象用例

  6. 第 6 步 - 实现更新对象用例

  7. 第 7 步 - 实现删除对象用例

项目 2 - 使用 IndexedDB 管理持久数据

通过用更强大的 IndexedDB API 替换使用 Local Storage API 进行持久数据存储,改进您在项目 1 中开发的应用程序。

历史

  • 2014年4月2日:创建第一个版本
  • 2014年4月3日:修正了错别字,添加了链接,添加了新的小节“HTML”和新的章节“要点”
  • 2014年4月11日:改进了表格格式,更新了代码,解决了文章与代码之间的不匹配问题
  • 2014年5月12日:更新了超链接
  • 2014年10月6日:更新了超链接,上传了图表图像,添加了最后一节“注意事项”
  • 2015年1月7日:添加了“JavaScript 支持三种数据结构”一节,更正了超链接。
  • 2015年1月28日:扩展了“可能的变化和扩展”一节(并将其标题更改为“可能的变化和扩展”),增加了两个小节:1)使用 IndexDB 而不是 LocalStorage,以及 2)使用 <time> 元素表达日期/时间信息。改进了预告片。
  • 2015年2月15日:重构了 for 循环中计数器变量的错误声明,例如 (var i=0;...)
  • 2015年6月17日:新增“实践项目”一节
  • 2015年12月1日:添加了前端应用程序架构图
© . All rights reserved.