一、建立待办事项清单

Hi there!

在这本书中,我们将用 JavaScript 构建一些非常有趣的应用。 JavaScript 已经从一种简单的脚本语言,在浏览器中用于表单验证,发展成为一种强大的编程语言,几乎在任何地方都可以使用。 看看这些用例:

  • 想要设置一个服务器来处理数以百万计的请求和大量的 I/O 操作? 你有 Node.js,它的单线程非阻塞 I/O 模型可以轻松处理高负载。 使用 Node.js 框架在服务器上编写 JavaScript,如Expresssail
  • 想要构建一个大规模的 web 应用? 这是一个激动人心的时刻是一个前端开发人员,因为许多新的 JavaScript 框架,如,角 2,Vue.js,等等,可以加快您的开发过程,很容易构建大型应用。 想要开发一个移动应用? 选择*React NativeNativeScript,你便能够基于 JavaScript 编写的单一代码库,创造出真正适用于 iOS 和 Android 的原生手机应用。 不够吗? 使用PhoneGapIonic使用 HTML、CSS、JavaScript 简单创建一个移动应用。 就像一个 web 应用! 想要构建桌面应用? 使用Electron来构建一个跨平台的本地桌面应用,使用 HTML, CSS,当然还有 JavaScript。 JavaScript 在构建虚拟现实(VR)和增强现实(AR)应用中也发挥着重要作用。 React VRA-Frame用于构建 WebVR 体验,Argon.jsAR.js用于添加 AR 到您的 web 应用。**

JavaScript 也在快速发展。 通过引入ECMAScript 2015(ES6**),大量的新增走进语言简化开发人员大量的工作,向他们提供功能,以前只能使用打印稿和 CoffeeScript。 甚至,JavaScript 的新规范(ES7 及以上)中也加入了新特性。 对于 JavaScript 开发人员来说,这是一个激动人心的时刻,本书旨在为您打下坚实的基础,以便您将来能够适应前面提到的任何 JavaScript 平台/框架。

本章主要针对了解 HTML、CSS、JavaScript 等基本概念,但尚未掌握 ES6、Node 等新主题的读者。 本章将涵盖以下主题:

  • 文档对象模型(DOM)操作和事件监听器
  • 介绍和实际使用 JavaScript 的 ES6 实现
  • 使用 Node 和 npm 进行前端开发
  • 使用 Babel 将 ES6 转换为 ES5
  • 使用 npm 脚本设置自动开发服务器

如果您对这些主题感到满意,可以跳到下一章,在那里我们将处理一些高级工具和概念。

系统需求

JavaScript 是网络语言。 因此,你可以在任何系统中使用 web 浏览器和文本编辑器来构建 web 应用。 但我们确实需要一些工具来构建现代复杂的 web 应用。 为了获得更好的开发体验,建议使用至少 4gb RAM 的 Linux 或 Windows 机器或 Mac 机器。 在开始之前,您可能需要在系统中设置以下一些应用。

文本编辑器

首先,您需要一个 javascript 友好的文本编辑器。 在编写代码时,文本编辑器非常重要。 根据它们提供的特性,您可以节省数小时的开发时间。 有一些非常好的文本编辑器提供了优秀的语言支持。 我们将在本书中使用 JavaScript,所以我建议使用这些开源 JavaScript 友好的文本编辑器:

你也可以尝试 Sublime Text:https://www.sublimetext.com/,这是一个很棒的文本编辑器,但与前面提到的不同的是,Sublime Text 是商业的,你需要付费才能继续使用。 还有一个商业产品 WebStorm:https://www.jetbrains.com/webstorm/,这是一个为 JavaScript 开发的成熟的集成开发环境(IDE)。 它附带了用于调试和与 JavaScript 框架集成的各种工具。 你也许想试一试。

我建议在本书的项目中使用Visual Studio Code(VSCode)。

node . js

这里有另一个重要的工具,我们将在本书中使用,Node.js。 Node.js 是一个建立在 Chrome V8 引擎上的 JavaScript 运行时。 它允许您在浏览器之外运行 JavaScript。 Node.js 变得非常流行,因为它可以让你在服务器上运行 JavaScript,而且非常快,这要归功于它的非阻塞 I/O 方法。 Node.js 的另一个优秀优点是它有助于创建命令行工具,这些工具可以用于各种用途,比如自动化、代码搭建等等,我们将在本书中使用其中的许多工具。 在写这本书的时候,最新的Long Term Support(LTS)Node.js 版本是 6.10.2。 我将在本书中使用这个版本。 您可以在阅读本书时安装最新的 LTS 版本。

对于 Windows 用户

在 Windows 上安装很简单; 只需下载并安装最新的 LTS 版本:https://nodejs.org/en/

对于 Linux 用户

最简单的方法是按照https://nodejs.org/en/download/package-manager/提供的说明,通过包管理器安装最新的 LTS 版本。

Mac 用户

使用自制程序安装 Node.js:

安装 Node.js 后,在终端(Windows 用户的命令提示符)中运行node -v,检查是否安装正确。 这将打印您安装的节点的当前版本。

谷歌 Chrome

最后,在你的系统中安装最新版本的谷歌 Chrome:https://www.google.com/chrome/。 你可以使用火狐或其他浏览器,但我将使用 Chrome,所以如果你使用 Chrome,它将更容易跟上。

现在我们已经在系统中安装了所有必要的工具,让我们开始构建我们的第一个应用!

ToDo 列表应用

让我们看看我们将要构建的应用:

我们将构建这个简单的待办事项列表应用,它允许我们创建一个任务列表,将它们标记为已完成,并从列表中删除任务。

让我们使用本书代码文件中第 1 章的起始代码开始。 启动器代码将包含三个文件:index.htmlscripts.jsstyles.css。 在浏览器中打开index.html文件,可以看到待办事项列表应用的基本设计,如上面的截图所示。

JavaScript 文件将是空的,我们将在其中编写创建应用的脚本。 让我们看一下 HTML 文件。 在<head>部分,引用了styles.css文件和 BootstrapCDN,在<body>标签的末尾,jQuery 和 Bootstrap 的 JS 文件和我们的scripts.js文件一起被包含:

  • Bootstrap 是一个 UI 开发框架,帮助我们更快地构建响应式 HTML 设计。 Bootstrap 附带了一组 JavaScript 代码,需要 jQuery 来运行。

  • jQuery 是一个 JavaScript 库,它简化了用于 DOM 遍历、DOM 操作、事件处理等的 JavaScript 函数。

Bootstrap and jQuery are widely used together for building web applications. In this book, we will be focusing more on using JavaScript. Hence, both of them will not be covered in detail. However, you can take a look at w3school's website for learning Bootstrap: https://www.w3schools.com/bootstrap/default.asp and jQuery: https://www.w3schools.com/jquery/default.asp in detail.

在我们的 HTML 文件中,包含 last 的 CSS 文件中的样式将覆盖上一个文件中的样式。 因此,如果我们计划重写框架的任何默认 CSS 属性,那么在默认框架的 CSS 文件(在我们的例子中是 Bootstrap)之后包含我们自己的 CSS 文件是一个很好的实践。 在本章中,我们不必担心 CSS,因为我们不会在本章中编辑 Bootstrap 的默认样式。 我们只需要专注于我们的 JS 文件。 JavaScript 文件必须按照初始代码中给定的顺序包含:

<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script src="scripts.js"></script>

我们首先包含了 jQuery 代码,之后会包含 Bootstrap JS 文件。 这是因为 Bootstrap 的 JS 文件需要 jQuery 来运行。 如果我们先包含 Bootstrap JS,它会在控制台上打印一个错误,说 Bootstrap 需要 jQuery 来运行。 尝试将 Bootstrap 代码移到 jQuery 代码之上,打开浏览器的控制台。 谷歌 Chrome,Ctrl + Shift+J 在 Windows 或 Linux 上【5】和命令+【显示】选择+在 Mac。你会收到一个错误类似这样:

**

因此,我们目前是通过按照正确的顺序包括 JS 文件来管理依赖项的。 然而,在较大的项目中,这可能非常困难。 在下一章中,我们将看到管理 JS 文件的更好方法。 现在,让我们继续构建应用。

我们的 HTML 文件的主体分为两个部分:

  • 导航栏
  • 容器

我们通常使用导航条来添加链接到我们的 web 应用的不同部分。由于我们在这个应用中只处理单个页面,我们将只在导航栏中包含页面标题。

I have included many classes to the HTML elements, such as navbar, navbar-inverse, navbar-fixed-topcontainercol-md-2, col-xs-2, and so on. They are used for styling the elements using Bootstrap. We'll discuss them in later chapters. For now, let's focus only on the functionality part.

Chrome DevTools

在主体部分中,我们有一个输入字段,其中有一个按钮用于添加新任务,还有一个无序列表用于列出任务。 无序列表将有一个复选框来标记任务已完成,并有一个删除图标将任务从列表中删除。 您可能会注意到,列表中的第一项使用划线行标记为完成。 如果你使用 Chrome DevTools 检查元素,你会注意到它有一个额外的类complete,它使用 CSS 在文本上添加了一个划线行,这是在我们的styles.css文件中定义的。

要使用 Chrome DevTools 检查一个元素,右键单击该元素并选择 inspect。 你也可以点击Ctrl+转变+C在 Windows 或 Linux,或命令【显示】+转变+C【病人】在 Mac 上,然后,将光标停留在元素看到它的细节。 您还可以直接编辑元素的 HTML 或 CSS,以查看页面上反映的更改。 从列表中第一项的div中删除完整的类。 你会看到全打线消失了。 直接在 DevTools 中所做的更改是临时的,当页面刷新时将被清除。 查看以下图片的工具列表,以检查一个元素的 Chrome 浏览器:**

  • A:从右键菜单中查看元素
  • B:点击光标图标,将光标悬停在该元素上,选择不同的元素
  • C:直接编辑页面的 HTML
  • D:直接编辑与元素相关的 CSS

Chrome DevTools 的另一个不错的功能是,你可以在 JavaScript 代码的任何地方写debugger,谷歌 Chrome 将暂停脚本的执行,在点debugger被调用。 一旦执行暂停,您可以将鼠标悬停在 sources 选项卡中的源代码上,它将在弹出窗口中显示变量中包含的值。 您还可以在控制台选项卡中输入变量的名称来查看它的值。

这是谷歌 Chrome 调试器的截图:

您可以自由探索 Chrome 开发工具的不同部分,以了解更多关于它为开发人员提供的工具。

开始使用 ES6

现在您已经对开发人员工具有了一个很好的了解,让我们开始编码部分。 您应该已经熟悉了 JavaScript ES5 语法。 那么,让我们在本章中探索使用 ES6 语法的 JavaScript。 ES6 (ECMAScript 2015)是 ECMAScript 语言规范的第 6 个主要版本。 JavaScript 是 ECMAScript 语言规范的一个实现。

At the time of writing this book, ES8 is the latest release of JavaScript language. However, for simplicity and ease of understanding, this book only focuses on ES6. You can always learn about the latest features introduced in ES7 and beyond on the Internet easily once you grasp the knowledge of ES6.

在撰写本书时,所有现代浏览器都支持 ES6 的大部分特性。 然而,较老的浏览器不了解新的 JavaScript 语法,因此会抛出错误。 为了解决这样的向后兼容性问题,我们必须在部署应用之前将 ES6 代码转换为 ES5。让我们在本章的末尾了解一下。 最新版本的 Chrome 支持 ES6; 所以,现在,我们将直接使用 ES6 语法创建 ToDo List。

我将详细解释新的 ES6 语法。 如果你发现理解正常的 JavaScript 语法和数据类型有困难,请参考下面 w3schools 页面的相关章节:https://www.w3schools.com/js/default.asp。

在文本编辑器中打开scripts.js文件。 首先,我们将创建一个包含 ToDo List 应用方法的类,是的! 类是 ES6 中新增的 JavaScript。 使用 JavaScript 中的类创建对象很简单。 它允许我们将代码组织为模块。 在脚本文件中使用以下代码创建一个名为ToDoClass的类,并刷新浏览器:

class ToDoClass {
  constructor() {
    alert('Hello World!');
  }
}
window.addEventListener("load", function() {
  var toDo = new ToDoClass();
});

你的浏览器现在会抛出一个警告,说“Hello World!” 这是代码的作用。 首先,window.addEventListener将附加一个事件监听器到窗口,并等待窗口完成加载所有需要的资源。 一旦它被加载,就会触发load事件,调用事件监听器的回调函数,该函数初始化ToDoClass并将其赋值给变量toDo。 当ToDoClass被初始化时,它会自动调用构造函数,构造函数会创建一个警告,提示“Hello World!” 我们可以进一步修改代码以利用 ES6。 在window.addEventListener部分,你可以重写为:

let toDo;
window.addEventListener("load", () => {
  toDo = new ToDoClass();
});

首先,我们用新的箭头函数() => {}替换匿名回调函数function () {}。 其次,我们用let而不是var来定义变量。

箭头功能

箭头函数是 JavaScript 中定义函数的一种更简洁的方式,它们只是继承了父对象的this,而不是绑定自己的。 我们将很快看到更多关于this绑定的内容。 让我们看看如何使用新的语法。 考虑以下功能:

let a = function(x) {
}
let b = function(x, y) {
}

等价的箭头函数可以写成:

let a = x => {}
let b = (x,y) => {}

当我们必须向函数传递唯一的参数时,你可以看到()是可选的。

有时,我们只是在函数的一行中返回一个值,例如:

let sum = function(x, y) {
  return x + y;
}

如果我们想在箭头函数中以单行形式直接返回一个值,可以直接忽略return关键字和{}花括号,将其写成:

let sum = (x, y) => x+y;

就是这样! 自动返回xy的和。 但是,只有当您希望立即在单行中返回值时,才能使用此方法。

让,var, const

接下来,我们有let关键字。 ES6 有两个用于声明变量的新关键字:letconstletvar的区别在于使用它们声明的变量的作用域。 变量声明的范围内使用var函数定义和全球如果里面没有定义任何函数,而let的范围仅限于在封闭块中声明和全球如果里面没有定义任何封闭块。 看看下面的代码:

var toDo;
window.addEventListener("load", () => {
  var toDo = new ToDoClass();
});

如果你意外地在代码的某个地方重新声明toDo,如下所示,你的类对象将被覆盖:

var toDo = "some value";

这种行为令人困惑,并且很难为大型应用维护变量。 因此,ES6 中引入了let。 它只在声明变量的区域内限制变量的作用域。 在 ES6 中,鼓励使用let而不是var来声明变量。 看看下面的代码:

let toDo;
window.addEventListener("load", () => {
 toDo = new ToDoClass();
});

现在,即使你不小心在代码的其他地方重新声明了toDo,JavaScript 也会抛出一个错误,使你免于运行时异常。 封闭块是位于两个花括号{}之间的代码块,花括号可能属于函数,也可能不属于函数。

我们需要一个toDo变量在整个应用中都可以访问。 因此,我们在事件监听器之上声明toDo,并将其赋值给回调函数中的类对象。 这样,整个页面都可以访问toDo变量。

let is very useful for defining variables in for loops. You can create a for loop such that for(let i=0; i<3; i++) {} and the scope of the variable i will only be within the for loop. You can easily use the same variable name in other places of your code.

让我们来看看其他关键词constconst的工作原理与let相同,只是使用const声明的变量不能被更改(重分配)。 因此,const用于常量。 但是,不能重新分配整个常量,但可以更改其属性。 例如:

const a = 5;
a = 7; // this will not work
const b = {
  a: 1,
  b: 2
};
b = { a: 2, b: 2 }; // this will not work
b.a = 2; // this will work since only a property of b is changed

While writing code in ES6, always use const to declare your variables. Use let only when you need to perform any changes (reassignments) to the variable and completely avoid using var.

对象包含类变量和函数作为对象的属性和方法。 如果你想清楚 JavaScript 中对象是如何构造的,请参见:https://www.w3schools.com/js/js_objects.asp

从数据加载任务

我们在应用中要做的第一件事是从一组数据动态加载任务。 让我们声明一个类变量,它包含任务的数据以及预填充任务所需的方法。 ES6 没有提供声明类变量的直接方法。 我们需要使用构造函数声明变量。 我们还需要一个函数来将任务加载到 HTML 元素中。 因此,我们将创建一个loadTasks()方法:

class ToDoClass {
  constructor() {
    this.tasks = [
        {task: 'Go to Dentist', isComplete: false},
         {task: 'Do Gardening', isComplete: true},
         {task: 'Renew Library Account', isComplete: false},
    ];
    this.loadTasks();
  }

  loadTasks() {
  }
}

task 变量在构造函数内部声明为this.tasks,这意味着 task 变量属于this(ToDoClass)。 变量是一个对象数组,其中包含任务详细信息及其完成状态。 第二项任务即将完成。 现在,我们需要为数据生成 HTML 代码。 我们将从 HTML 中重用<li>元素的代码来动态生成一个任务:

 <li class="list-group-item checkbox">
  <div class="row">
    <div class="col-md-1 col-xs-1 col-lg-1 col-sm-1 checkbox">
     <label><input type="checkbox" value="" class="" checked></label>
    </div>
    <div class="col-md-10 col-xs-10 col-lg-10 col-sm-10 task-text complete">
      First item
    </div>
     <div class="col-md-1 col-xs-1 col-lg-1 col-sm-1 delete-icon-area">
      <a class="" href="/"><i class="delete-icon glyphicon glyphicon-trash"></i></a>
     </div>
   </div>
 </li>

In JavaScript, an instance of a class is called the class object or simply object. The class objects are structured similarly to JSON objects in key-value pairs. The functions associated with a class object are called its methods and the variables/values associated with a class object are called its properties.

模板文字

传统上,在 JavaScript 中,我们使用+操作符连接字符串。 然而,如果我们想连接多行字符串,那么我们必须使用转义代码\来转义新行,例如:

let a = '<div> \
    <li>' + myVariable+ '</li> \
</div>'

当我们必须编写包含大量 HTML 的字符串时,这可能会非常令人困惑。 在本例中,我们可以使用 ES6 模板字符串。 模板字符串是由反引号包围的字符串,而不是单引号' '。 通过使用这个,我们可以以更简单的方式创建多行字符串:

let a = `
<div>
   <li> ${myVariable} </li>
</div>
`

可以看到,我们可以用类似的方式创建 DOM 元素; 我们在 HTML 中输入它们,而不用担心空格或多行。 因为模板字符串中出现的任何格式,如制表符或新行,都会直接记录在变量中。 我们可以使用${}在字符串中声明变量。 因此,在本例中,我们需要为每个任务生成一个项目列表。 首先,我们将创建一个函数来循环遍历数组并生成 HTML。 在我们的loadTasks()方法中,编写以下代码:

loadTasks() {
  let tasksHtml = this.tasks.reduce((html, task, index) => html +=  
  this.generateTaskHtml(task, index), '');
  document.getElementById('taskList').innerHTML = tasksHtml;
 }

之后,在ToDoClass中创建一个generateTaskHtml()函数,代码如下:

generateTaskHtml(task, index) {
 return `
  <li class="list-group-item checkbox">
   <div class="row">
    <div class="col-md-1 col-xs-1 col-lg-1 col-sm-1 checkbox">
     <label><input id="toggleTaskStatus" type="checkbox"  
     onchange="toDo.toggleTaskStatus(${index})" value="" class="" 
     ${task.isComplete?'checked':''}></label>
    </div>
    <div class="col-md-10 col-xs-10 col-lg-10 col-sm-10 task-text ${task.isComplete?'complete':''}">
     ${task.task}
   </div>
   <div class="col-md-1 col-xs-1 col-lg-1 col-sm-1 delete-icon-area">
     <a class="" href="/" onClick="toDo.deleteTask(event, ${index})"><i 
     id="deleteTask" data-id="${index}" class="delete-icon glyphicon 
     glyphicon-trash"></i></a>
    </div>
   </div>
  </li>
`;
}

现在,刷新页面,哇! 我们的应用装载了来自我们的tasks变量的任务。 乍一看,这应该是一大堆代码,但让我们逐行研究一下。

In case the changes aren't reflected when you refresh the page, it's because Chrome has cached the JavaScript files and is not retrieving the latest one. To make it retrieve the latest code, you will have to do a hard reload by pressing Ctrl+Shift+R on Windows or Linux and command+Shift+R on Mac.

loadTasks()函数中,我们声明了一个变量tasksHtml,其值由tasks变量的数组reduce()方法的回调函数返回。 JavaScript 中的每个数组对象都有一些与之关联的方法。 reduce是 JS 数组的一种方法,它将函数从左到右应用于数组的每个元素,并将这些值应用于累加器,这样数组就会缩减为一个值,然后返回最终的值。 reduce方法接受两个参数; 第一个是回调函数,它应用于数组的每个元素,第二个是累加器的初始值。 让我们看看普通 ES5 语法中的函数:

let tasksHtml = this.tasks.reduce(function(html, task, index, tasks) { 
  return html += this.generateTaskHtml(task, index)
}.bind(this), '');
  • 第一个参数是回调函数,它的四个参数是:html,这是我们的累加器,task,这是 task 数组中的一个元素,index,给出迭代中数组元素的当前索引,还有tasks, 它包含应用 reduce 方法的整个数组(在我们的用例中,我们不需要回调函数中的整个数组,因此在我们的代码中忽略了第四个参数)。
  • 第二个参数是可选的,它包含累加器的初始值。 在我们的例子中,初始的 HTML 字符串是一个空字符串''
  • 此外,请注意,我们必须使用this(这是我们的类)对象bind回调函数,以便在回调函数中可以访问ToDoClass的方法和变量。 这是因为,否则,每个函数都将定义自己的this对象,父函数的this对象将在该函数中不可访问。

回调函数首先取空的html字符串(累加器),然后将其与ToDoClassgenerateTaskHtml()方法返回的值连接起来,ToDoClass方法的参数是数组的第一个元素及其下标。 当然,返回值应该是一个字符串,否则将抛出一个错误。 然后,使用更新后的累加器值对数组中的每个元素重复该操作,最终在迭代结束时返回该累加器值。 最终的缩减值包含将任务填充为字符串的整个 HTML 代码。

通过应用 ES6 箭头函数,整个操作可以在一行内完成,如下所示:

let tasksHtml = this.tasks.reduce((html, task, index) => html += this.generateTaskHtml(task, index), '');

并不是那么简单! 因为我们只是在一行中返回值,所以可以忽略{}花括号和return关键字。 此外,箭头函数不定义自己的this对象; 他们只是继承了父母的目标。 所以我们也可以忽略.bind(this)方法。 现在,我们使用箭头函数使代码更清晰,更易于理解。

在我们进入loadTasks()方法的下一行之前,让我们看看generateTaskHtml()方法的工作原理。 该函数接受两个参数——任务数据中的数组元素任务及其索引,并返回一个包含填充任务的 HTML 代码的字符串。 注意,我们在复选框的代码中包含了变量:

<input id="toggleTaskStatus" type="checkbox" onchange="toDo.toggleTaskStatus(${index})" value="" class="" ${task.isComplete?'checked':''}>

它表示“当复选框的状态发生变化时”,调用toDo对象的toggleTaskStatus()方法,该方法使用的是被改变的任务的索引。 我们还没有定义toggleTaskStatus()方法,所以当你点击网站上的复选框,它将抛出一个错误,在 Chrome 的控制台和浏览器窗口没有什么特别的事情发生。 此外,我们还添加了一个条件操作符()?:,用于在任务状态完成时返回输入标记的 checked 属性。 如果任务已经完成,这对于使用预先选中的复选框呈现列表非常有用。

同样,我们包括${task.isComplete?'complete':''}``div包含任务文本这一个额外的类添加到任务,如果任务完成,和 CSS 已经写在styles.css申请该类文本呈现透油线。

最后,在锚标记中,我们包含了onClick="toDo.deleteTask(event, ${index})"来调用toDo对象的deleteTask()方法,参数是点击事件本身和任务的索引。 我们还没有定义deleteTask()方法,所以点击删除图标将带您到文件系统的根目录!

onclick and onchange are some of HTML attributes that are used to call JavaScript functions when the specified event occurs on the parent element on which the attributes are defined. Since these attributes belong to HTML, they are case insensitive.

现在,让我们看一下loadTasks()方法的第二行:

document.getElementById('taskList').innerHTML = tasksHtml;

我们只是用我们新生成的字符串tasksHTML替换了 IDtaskList的 DOM 元素的 HTML 代码。 现在,待办事项列表被填充了。 是时候定义toDo对象的两个新方法了,我们在生成的 HTML 代码中包含了这两个方法。

管理任务状态

ToDoClass内部,包含两个新方法:

 toggleTaskStatus(index) {
  this.tasks[index].isComplete = !this.tasks[index].isComplete;
   this.loadTasks();
 }
 deleteTask(event, taskIndex) {
   event.preventDefault();
   this.tasks.splice(taskIndex, 1);
   this.loadTasks();
 }

第一种方法,toggleTaskStatus(),用来标记任务已完成或未完成。 当以任务的索引作为参数单击复选框(onChange)时调用:

  • 使用任务的索引,我们将任务的isComplete状态赋值为不使用(!)操作符的当前状态的否定。 因此,可以在此函数中切换任务的完成状态。
  • 一旦用新数据更新了tasks变量,就调用this.loadTasks(),用更新后的值重新呈现所有任务。

第二种方法,deleteTask(),用于从列表中删除任务。 目前,单击删除图标将带您到文件系统的根目录。 但是,在导航到文件系统的根目录之前,会调用toDo.deleteTask(),参数是点击event和任务的index:

  • 第一个参数event包含了整个事件对象,包含了关于刚刚发生的点击事件的各种属性和方法(尝试console.log(event)内的deleteTask()函数查看 Chrome 控制台的所有细节)。
  • 为了防止任何默认操作(打开 URL)只发生一次,我们单击删除图标(<a>标签)。 首先,我们需要指定event.preventDefault()
  • 然后,我们需要从tasks变量中删除数组的 task 元素。 为此,我们使用了splice()方法,该方法从指定的索引中删除数组中指定数量的元素。 在我们的例子中,从需要删除的任务的索引中,只删除一个元素。 这将从tasks变量中删除要删除的任务。
  • 调用this.loadTasks()以使用更新的值重新渲染所有任务。

刷新页面(如果需要,硬加载),以查看当前应用如何使用新代码。 您现在可以将任务标记为已完成,并可以从列表中删除任务。

向列表中添加新任务

现在,我们可以选择切换任务状态和删除任务。 但是我们需要在列表中添加更多的任务。 为此,我们需要使用 HTML 文件中提供的文本框来允许用户输入新任务。 第一步将添加onclick属性到添加任务<button>:

<button class="btn btn-primary" onclick="toDo.addTaskClick()">Add</button>

现在,每次点击按钮都会调用toDo对象的addTaskClick()方法,该方法尚未定义。 所以,让我们在ToDoClass中定义它:

addTaskClick() {
  let target = document.getElementById('addTask');
  this.addTask(target.value);
  target.value = ""
}
addTask(task) {
  let newTask = {
   task,
   isComplete: false,
  };
  let parentDiv = document.getElementById('addTask').parentElement;
  if(task === '') {
   parentDiv.classList.add('has-error');
  } else {
   parentDiv.classList.remove('has-error');
   this.tasks.push(newTask);
   this.loadTasks();
  }
}

重新加载 Chrome 并尝试通过单击 Add 按钮添加一个新任务。 如果一切正常,您应该会看到一个新任务被添加到列表中。 此外,当您单击 Add 按钮而不在输入字段中输入任何内容时,它将用红色边框突出显示输入字段,指示用户应该在输入字段中输入文本。

See how I have divided our add task operation across two functions? I did a similar thing for the loadTask() function. In programming, it is a best practice to organize all the tasks into smaller, more generic functions, which will allow you to reuse those functions in the future.

让我们看看addTaskClick()方法是如何工作的:

  • addTaskClick()函数没有任何请求参数。 首先,要读取新任务的文本,我们获取 ID 为addTask<input>元素,其中包含任务所需的文本。 使用document.getElementById('addTask'),将其赋值给target变量。 现在,target变量包含了<input>元素的所有属性和方法,可以读取和修改(尝试console.log(target)查看变量中包含的所有细节)。
  • value属性包含必需的文本。 因此,我们将target.value传递给addTask()函数,该函数负责向列表中添加新任务。
  • 最后,通过将target.value设置为空字符串'',我们将输入字段重置为空状态。

这是 click 事件的事件处理部分。 让我们看看任务是如何在addTask()方法中被添加到列表中的。 变量task包含新任务的文本:

  • 理想情况下,该函数的第一步是构造定义我们任务的 JSON 数据:
let newTask = {
  task: task,
  isComplete: false
}
  • 这是另一个 ES6 特性对象的文字属性值简写; 我们可以简单地写{task},而不是在 JSON 对象中写{task: task}。 变量名将成为键,存储在变量中的值将成为值。 如果变量未定义,将抛出一个错误。
  • 我们还需要创建另一个变量parentDiv来存储目标<input>元素的父元素<div>的对象。 它很有用,因为当任务是一个空字符串时,我们可以将has-error类添加到父元素parentDiv.classList.add('has-error')中,Bootstrap 的 CSS 会将<input>元素渲染为红色边框。 这是我们指示用户在单击 Add 按钮之前需要输入文本的方式。
  • 然而,如果输入文本不是空的,我们应该从父元素中删除has-error类,以确保不向用户显示红色边框,然后简单地将newTask变量推入类的tasks变量。 同样,我们需要再次调用loadTasks(),以便渲染新任务。

按 Enter 键添加任务

这是添加任务的一种方式,但有些用户更喜欢直接按Enter键添加任务。 为此,让我们使用事件监听器来检测<input>元素中的Enter键。 我们也可以使用元素<input>onchange属性,但是让我们尝试一下事件监听器。 将事件监听器添加到类的最佳方法是在构造函数中调用它们,以便在类初始化时设置事件监听器。

因此,在我们的类中,创建一个新函数addEventListeners()并在构造函数中调用它。 我们将在这个函数中添加事件监听器:

constructor() {
  ...
  this.addEventListeners();
}
 addEventListeners() {
  document.getElementById('addTask').addEventListener('keypress', event => {
     if(event.keyCode === 13) {
       this.addTask(event.target.value);
       event.target.value = '';
     }
   });
 }

这是它! 重新加载 Chrome,输入文本,并按输入。 这将向列表中添加任务,就像 add 按钮的工作方式一样。 让我们来看看新的事件监听器:

  • 对于在 ID 为addTask<input>元素中发生的每一次按键,我们使用event对象作为参数运行回调函数。
  • 此事件对象包含按下的键的键码。 对于,输入键,键码为 13。 如果键码等于 13,我们只需调用this.addTask()函数,并将任务的文本event.target.value作为参数。
  • 现在,addTask()函数处理将任务添加到列表中。 我们可以简单地将<input>重置为空字符串。 这是将每个操作组织成函数的一大优点。 我们可以简单地在任何需要的地方重用这些函数。

在浏览器中持久化数据

现在,功能方面,我们的待办事项列表已经准备好了。 然而,在刷新页面时,数据将消失。 让我们看看如何在浏览器中持久化数据。 通常,web 应用与服务器端 api 连接,动态加载数据。 这里,我们不考虑服务器端实现。 因此,我们需要寻找一种在浏览器中存储数据的替代方法。 在浏览器中存储数据有三种方法。 它们是:

  • cookie:一个cookie是服务器存储在客户端(浏览器)上的一个有过期日期的小信息。 它对于从客户机读取信息(如登录令牌、用户首选项等)非常有用。 cookie 主要用于服务器端,可以存储在 cookie 中的数据量被限制为 4093 字节。 在 JavaScript 中,可以使用document.cookie对象来管理 cookie。
  • localStorage:HTML5 的localStorage存储信息没有截止日期,即使在关闭和打开网页后数据也会持续存在。 每个域提供 5mb 的存储空间。
  • sessionStorage:sessionStoragelocalStorage等效,只是数据仅在每个会话(用户正在处理的当前选项卡)有效。 当网站关闭时,数据过期。

对于我们的用例,localStorage是持久化任务数据的最佳选择。 localStorage以键值对的形式存储数据,而值必须是字符串。 让我们看看实现部分。 在构造函数内部,不要直接将值赋给this.tasks,而是将其改为:

constructor() {
  this.tasks = JSON.parse(localStorage.getItem('TASKS'));
   if(!this.tasks) {
    this.tasks = [
       {task: 'Go to Dentist', isComplete: false},
       {task: 'Do Gardening', isComplete: true},
       {task: 'Renew Library Account', isComplete: false},
    ];
  } 
... 
}

我们将把任务保存在localStorage中,作为一个以'TASKS'为键的字符串。 所以当用户第一次打开网站时,我们需要用'TASKS'键来检查localStorage中是否有数据。 如果没有数据,则返回null,这表示用户是第一次访问该网站。 我们需要使用JSON.parse()将从localStorage检索到的数据从字符串转换为对象:

  • 如果在localStorage中没有数据(用户第一次访问网站),我们将使用tasks变量为他们预填充一些数据。 添加代码以在应用中持久化任务数据的最佳位置是loadTasks()函数,因为每次对tasks进行更改时都会调用该函数。 在loadTasks()函数中,添加一行:
 localStorage.setItem('TASKS', JSON.stringify(this.tasks));
  • 这将把我们的tasks变量转换为字符串,并将其存储在localStorage中。 现在,您可以添加任务和刷新页面,数据将被持久化到浏览器中。
  • 如果您想为开发目的清空localStorage,可以使用localStorage.removeItem('TASKS')删除键,或者使用localStorage.clear()完全删除localStorage中存储的所有数据。

Everything in JavaScript has an inherent Boolean value, which can be called truthy or falsy. The following values are always falsy - null, "" (empty string), false, 0 (zero), NaN (not a number), and undefined. Other values are considered truthy. Hence, they can be directly used in conditional statements like how we used if(!this.tasks) {} in our code.

现在我们的应用已经完成,您可以删除index.html文件中的<ul>元素的内容。 现在将直接从 JavaScript 代码填充内容。 否则,当页面加载或刷新时,您将看到默认的 HTML 代码在页面中闪烁。 这是因为我们的 JavaScript 代码只有在所有的资源加载完成后才会执行,原因如下:

window.addEventListener("load", function() {
  toDo = new ToDoClass();
});

如果一切顺利,那么恭喜你! 您已经成功构建了第一个 JavaScript 应用,并且了解了 JavaScript 的新 ES6 特性。 哦,等一下! 看来我们忘了一些重要的东西!

All the storage options discussed here are unencrypted and, hence, should not be used for storing sensitive information, such as password, API keys, authentication tokens, and so on.

与旧浏览器的兼容性

虽然 ES6 可以兼容几乎所有的现代浏览器,但仍有许多用户使用较老版本的 Internet Explorer 或 Firefox。 那么,我们如何让我们的应用为他们工作呢? ES6 的好处是,它所有的新特性都可以使用 ES5 规范实现。 这意味着我们可以很容易地将代码转换为 ES5,它可以在所有现代浏览器上工作。 为此,我们将使用 Babel:https://babeljs.io/作为 ES6 到 ES5 转换的编译器。

还记得在本章开始时,我们是如何在系统中安装 Node.js 的吗? 好了,终于可以用了。 在开始将代码编译为 ES5 之前,我们需要了解 Node 和 npm。

node . js 和 npm

Node.js 是一个建立在 Chrome V8 引擎上的 JavaScript 运行时。 它允许开发人员在浏览器之外运行 JavaScript。 由于 Node.js 的非阻塞 I/O 模型,它被广泛用于构建数据密集型的实时应用。 你可以用 JavaScript 为你的 web 应用构建后台,就像 PHP、Ruby 或其他服务器端语言一样。

Node.js 的一个巨大优势是,它允许您将代码组织成模块。 模块是用于执行特定功能的一组代码。 到目前为止,我们已经在浏览器的<script>标签中一个接一个地包含了 JavaScript 代码。 但是在 Node.js 中,我们可以通过创建对模块的引用来简单地调用代码中的依赖项。 例如,如果我们需要 jQuery,我们可以简单地写如下:

const $ = require('jquery');

或者,我们可以这样写:

import $ from 'jquery';

jQuery 模块将包含在我们的代码中。 jQuery 的所有属性和方法都可以在$对象中访问。 $的作用域将只在它被调用的文件内。 因此,在每个文件中,我们可以单独指定依赖项,并且在编译期间将所有依赖项捆绑在一起。

但是等等! 为了包含jquery,我们需要下载包含所需模块的jquery包,并将其保存在一个文件夹中。 然后,我们需要将包含模块的文件夹中的文件引用赋给$。 随着项目的发展,我们将添加许多包,并在代码中引用模块。 那么,我们要如何管理所有的包呢? 好的,我们有一个很好的小工具,与 Node.js 一起安装,称为Node Package Manager(npm):

  • 对于 Linux 和 Mac 用户,npm 类似于这些:apt-getyumdnfHomebrew
  • 对于 Windows 用户,您可能还不熟悉包管理的概念。 假设你需要 jQuery。 但是您不知道运行 jQuery 需要哪些依赖项。 这就是包管理器发挥作用的地方。 你可以简单地运行一个命令来安装一个包(npm install jquery)。 包管理器将读取目标包的所有依赖项,并安装目标及其依赖项。 它还管理一个文件来跟踪已安装的包。 这用于在将来轻松地卸载包。

Even though Node.js allows require/import of modules directly into the code, browsers do not support require or import functionality to directly import a module. But there are many tools available that can easily mimic this functionality so that we can use import/require inside our browsers. We'll use them for our project in the next chapter.

npm 维护一个package.json文件来存储关于包的信息,比如它的名称、脚本、依赖项、开发依赖项、存储库、作者、许可等等。 包是一个包含一个或多个文件夹或文件的文件夹,其根文件夹中有一个package.json文件。 在 npm 中有成千上万的开放源码包。 访问https://www.npmjs.com/探索可用的套餐。 包可以是服务器端或浏览器端使用的模块,也可以是用于执行各种操作的命令行工具。

NPM 包可以安装在本地(每个项目)或全局(整个系统)。 我们可以使用不同的标志来指定如何安装它,如下所示:

  • 如果我们想要全局安装一个包,我们应该使用--global-g标志。
  • 如果该包应该安装在特定项目的本地,请使用--save-S标志。
  • 如果该包应该安装在本地,并且只用于开发目的,请使用--save-dev-D标志。
  • 如果你运行没有任何标志的npm install <package-name>,它将在本地安装包,但不会更新package.json文件。 不建议安装带有-S-D标志的软件包。

让我们使用 npm 安装一个命令行工具,名为http-server:https://www.npmjs.com/package/http-server。 它是一个简单的工具,可以用来在http-server上提供静态文件,就像在 Apache 或 Nginx 中提供文件一样。 这对于测试和开发我们的 web 应用很有用,因为我们可以看到应用通过 web 服务器提供服务时的行为。

**如果命令行工具只供我们使用而不供任何其他开发人员使用,则建议全局安装它们。 在我们的例子中,我们将只使用http-server包。 让我们全局安装它。 打开终端/命令提示符并运行以下命令:

npm install -g http-server

If you are using Linux, some times you might face errors such as permission denied or unable to access file, and so on. Try running the same command as administrator (prefixed with sudo) for installing the package globally.

安装完成后,在终端中导航到我们的 ToDo List 应用的根文件夹,并运行以下命令:

http-server

你将收到两个 url,服务器将开始运行,如下所示:

  • 要在本地设备上查看待办事项列表应用,请在浏览器中打开以127开头的 URL
  • 要在连接到本地网络的不同设备上查看待办事项列表应用,请在设备的浏览器上打开以192开头的 URL

每次打开应用,http-server都会在终端中打印所服务的文件。 有各种各样的选项可用http-server,如-p标志,它可以用来改变默认的端口号8080(尝试http-server -p 8085)。 访问 http-server:https://www.npmjs.com/package/http-servernpm 页面,获取所有可用选项的文档。 现在我们已经对npm包有了大致的了解,让我们安装 Babel 来将我们的 ES6 代码转换为 ES5。****

We will be using Terminals a lot in our upcoming chapters. If you are using VSCode, it has an inbuilt terminal, which can be opened by pressing Ctrl+` on Mac, Linux, and Windows. It also supports opening multiple terminal sessions at the same time. This can save you lot time on switching between windows.

使用 Node 和 Babel 设置我们的开发环境

Babel 是一个 JavaScript 编译器,用于将 JavaScript 代码从 ES6+转换为正常的 ES5 规范。 让我们在项目中设置 Babel,以便它能自动编译我们的代码。

在设置了 Babel 之后,我们的项目中会有两个不同的 JS 文件。 一个是 ES6,我们用它来开发我们的应用,另一个是编译好的 ES5 代码,它将被浏览器使用。 因此,我们需要在项目根目录下创建两个不同的文件夹,分别是srcdist。 将scripts.js文件移动到src目录下。 我们将使用 Babel 从src目录编译脚本,并将结果存储在dist目录中。 因此,在index.html中,将scripts.js的引用更改为<script src="dist/scripts.js"></script>,这样浏览器就会始终读取编译后的代码:

  1. 要使用 npm,我们需要在项目的根目录中创建package.json。 在终端中导航到项目根目录并输入:
npm init
  1. 首先,它会询问项目的名称,输入名称。 对于其他问题,要么输入一些值,要么按,输入接受默认值。 这些值将被填充到package.json文件中,稍后可以更改。

  2. 让我们通过在终端中运行以下命令来安装我们的开发依赖项:

npm install -D http-server babel-cli babel-preset-es2015 concurrently
  1. 这个命令将创建一个node_modules文件夹并在其中安装软件包。 现在,你的package.json文件将在其devDependencies参数中包含上述的包,你当前的文件夹结构应该是:
.
├── dist
├── index.html
├── node_modules
├── package.json
├── src
└── styles.css

If you are using git or any other version control system in your project, add node_modules and the dist folder to .gitignore or a similar file. These folders need not be committed to version control and must be generated when needed.

是时候编写脚本来编译代码了。 在package.json文件中,有一个名为scripts的参数。 默认情况下,它将是以下内容:

 "scripts": {
   "test": "echo \"Error: no test specified\" && exit 1"
 },

test是 npm 的默认命令之一。 当您在终端中运行npm test时,它会自动在终端中执行 test 键值内的脚本。 顾名思义,test用于执行自动化测试用例。 其他默认命令有startstoprestartshrinkwrap等。 在使用 Node.js 开发服务器端应用时,这些命令对于运行脚本非常有用。

然而,在前端开发期间,我们可能需要更多的命令,比如默认命令。 npm还允许我们创建命令来执行任意脚本。 然而,与默认命令(如npm start)不同,我们不能通过运行npm <command-name>来执行自己的命令; 我们必须在航站楼执行npm run <command-name>

我们将设置 npm 脚本,以便运行npm run build将为我们的应用生成一个使用编译后的 ES5 代码的工作构建,运行npm run watch将启动一个开发服务器,我们将用于开发。

将脚本部分的内容改为:

"scripts": {
  "watch": "babel src -d dist --presets=es2015 -ws",
  "build": "rm -rf dist && babel src -d dist --presets=es2015",
  "serve": "http-server"
},

嗯,看起来有很多脚本! 让我们一个一个地看一看。

首先,让我们看看watch脚本:

  • 这个脚本的作用是开始babel在观看模式下,每一次我们在 ES6 代码做任何改变在src目录,它将自动 transpiled ES5 代码在dist目录连同源地图,这是用于调试编译后的代码。 手表模式将继续在终端中进行,直到执行结束(按Ctrl+C)。

  • 从项目的根目录在终端中执行npm run watch。 您可以看到 Babel 已经开始编译代码,并且将在dist文件夹中创建一个新的scripts.js文件。

  • scripts.js文件将以 ES5 格式包含我们的代码。 在 Chrome 中打开index.html,你应该看到我们的应用正常运行。

下面是它的工作原理。 尝试在终端中直接运行babel src -d dist --presets=es2015 -ws。 它将抛出一个错误,说明babel未安装(错误消息可能根据您的操作系统而有所不同)。 这是因为我们还没有在全球安装巴别塔。 我们只在项目中安装了它。 因此,当我们运行npm run watch时,npm 将在项目的node_modules文件夹中寻找 Babel 的二进制文件,并使用这些二进制文件执行命令。

删除dist目录,在package.json"babel": "babel src -d dist"中新建一个脚本。 我们将使用这个脚本来学习巴别塔是如何工作的:

  • 该脚本告诉 Babel编译src目录中的所有 JS 文件,并将生成的文件保存在dist目录中。 如果dist目录不存在,将创建该目录。 这里,使用了-d标志来告诉 Babel 它需要在整个目录内编译文件。
  • 在终端上运行npm run babel,在dist目录下打开新的scripts.js文件。 好吧,文件被编译了,但不幸的是,结果也是 ES6 语法,所以新的scripts.js文件是我们的原始文件的精确拷贝!
  • 我们的目标是将代码编译为 ES5。 为此,我们需要指导 Babel 在编译期间使用一些预置。 看看我们的npm install命令,我们已经为此安装了一个名为babel-preset-es2015的包。
  • 在我们的 Babel 脚本中,添加选项--presets=es2015并再次执行npm run babel。 这一次,代码将被编译成 ES5 语法。
  • 在浏览器中打开我们的应用,在构造函数中添加debugger,然后重新加载。 我们有一个新问题; 源代码现在将包含 ES5 语法中的代码,这使得调试原始代码更加困难。
  • 为此,我们需要使用-s标志启用源映射,该标志创建了.map文件,该文件用于将编译后的代码映射回原始源代码。 同时,使用-w旗将巴别塔置于手表模式。

现在我们的脚本将与在watch命令中使用的脚本相同。 使用调试器重新加载应用,您可以看到源代码将包含我们的原始代码,即使它使用的是已编译的源代码。

如果运行一个单独的命令也可以使用http-server启动我们的开发服务器,那不是很好吗? 我们不能使用&&连接同时运行的两个命令。 因为&&将在第一个命令完成后执行第二个命令。

为此,我们安装了另一个名为concurrently的包。 它用于同时执行多个命令。 concurrently的用法如下:

concurrently "command1" "command2" 

当我们执行npm run watch时,我们需要同时运行当前的watch脚本和serve脚本。 将watch脚本更改为以下内容:

"watch": "concurrently \"npm run serve\" \"babel src -d dist --presets=es2015 -ws\"",

再次尝试运行npm run watch。 现在,您有了一个功能齐全的开发环境,它将自动提供文件,并在您更改 JS 代码时编译代码。

航运的代码

一旦开发完成,如果您使用版本控制,为了发布代码,请将node_modulesdist文件夹添加到忽略列表中。 否则,发送没有node_modulesdist文件夹的代码。 其他开发人员可以简单地运行npm install来安装依赖项,并在需要时读取package.json文件中的脚本来构建项目。

我们的npm run build命令将删除项目文件夹中的dist文件夹,并使用最新的 JS 代码创建一个新的dist文件夹。

总结

恭喜你! 你已经用新的 ES6 语法构建了第一个 JavaScript 应用。 在本章中,您已经学习了以下概念:

  • JavaScript 中的 DOM 操作和事件监听器
  • ECMAScript 2015 (ES6)的 JavaScript 语法
  • Chrome 开发工具
  • Node 和 npm 的工作原理
  • 使用 Babel 将 ES6 代码转换为 ES5 代码

在当前的 npm 设置中,我们只是简单地创建了一个编译脚本来将代码转换为 ES5。 还有许多其他工具可用于自动化更多任务,如缩小、检测、图像压缩等。 在下一章中,我们将使用一个名为 Webpack 的工具。****