Web开发权威指南笔记(二)

2023-11-11

书:Web开发权威指南,[美] Chris Aquino, Todd Gandee著。为2nd实战项目CoffeeRun练习以及代码整理。全为个人借鉴本书产出,若需要转载请联系通知我,请尊重原创,谢谢~

整理了大概5天了,内容比较多(很多重点都整理在代码中的注释了),如果读者耐心观看一定可以和我一样收获很多的喲,我们一起加油~

为美食车搭建一个管理咖啡订单的应用,名字叫做CoffeeRun。

最终成果展示

第八章 模块、对象和方法

模块

创建三个不同的JavaScript文件,分别对应UI、内部逻辑和服务端交换逻辑

模块模式

立即调用的函数表达式(Immediately Invoked Function Expression, IIFE)。CoffeeRun中可能会有很多名称相同的函数,如果把他们都卸载全局命名空间中,它们会互相覆盖。因此,应该在函数内声明它们,这能避免它们被外界的代码访问或者覆盖。

例子

function initializeEvents() { //该方法会把所有步骤联系起来,从而具有交互性。首先获取缩略图数组,然后遍历整个数组,给其中每个元素添加点击事件处理程序
    'use strict';
    var thumbnails = getThumbnailsArray(); //将结果(缩略图数组)赋值给一个行为thumbnails的变量
    thumbnails.forEach(addThumbClickHandler); //不是一个好办法,不过在目前这样做不会引发错误,addThumbClickHandler函数只需要在foreach函数调用它时传递他的信息(thumbnails数组中的一个元素)
    addKeyPressHandler();
}
initializeEvents();
(function () { //该方法会把所有步骤联系起来,从而具有交互性。首先获取缩略图数组,然后遍历整个数组,给其中每个元素添加点击事件处理程序
    'use strict';
    var thumbnails = getThumbnailsArray(); //将结果(缩略图数组)赋值给一个行为thumbnails的变量
    thumbnails.forEach(addThumbClickHandler); //不是一个好办法,不过在目前这样做不会引发错误,addThumbClickHandler函数只需要在foreach函数调用它时传递他的信息(thumbnails数组中的一个元素)
    addKeyPressHandler();
})();

代码展示

 index.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>coffeerun</title>
    </head>
    <body>
        <script src="scripts/datastore.js" charset="UTF-8"></script>
        <script src="scripts/truck.js" charset="UTF-8"></script>
        <script src="scripts/main.js" charset="UTF-8"></script>
    </body>
</html>

datastore.js

(function (window) { //用基本的IIFE模式编写模块,我们可以利用函数作用域为大段代码创建命名空间
    'use strict';
    var App = window.App || {}; //本地变量App。如果在window上存在App属性,那么就将它赋值到本地App中;否则引用一个用{}表示的新的空对象
    /*最重要的模块DataStore,它可以存储数据、根据查询需求提供相应的数据,并且按照指令删除不必要的数据。*/
    function DataStore() { //首字母大写,这是JavaScript中约定的构造函数的命名方式
        this.data = {}; //创建和定制一个新的对象
    }
​
    DataStore.prototype.add = function (key, val) { //所有通过构造函数出创建的实例都可以访问其属性和方法的共享仓库:构造函数的prototype属性。创建实例使用new关键字调用构造函数。new关键字不仅仅创建并返回实例,还在实例和构造函数的prototype属性间创建了一个特别的链接
        this.data[key] = val;
    }
​
    DataStore.prototype.get = function (key) { //该方法接受参数key, 并在相应实例的data属性中查找键值
        return this.data[key];
    }
​
    DataStore.prototype.getAll = function() { //但getall会直接返回data属性的的引用
        return this.data;
    }
​
    DataStore.prototype.remove = function (key) { //当调用remove方法时,delete运算符会将一个键值对从对象上删除
        delete this.data[key];
    }
​
    App.DataStore = DataStore; //将DataStore绑定到App对象上
    window.App = App; //将新修改的App赋值到全局App属性上
})(window);

truck.js

(function (window) {
    'use strict';
    var App = window.App || {};
​
    function Truck(truckId, db) { //Truck模块,负责提供用千管理美食车的功能,比如创建、交付订单,打印等待中的订单列表
        this.truckId = truckId;
        this.db = db;
    }
​
    /*在createOrder函数中输出消息到控制台,然后使用db属性的add方法来存储订单信息。*/
    Truck.prototype.createOrder = function (order) {
        console.log('Adding order for ' + order.emailAddress);
        //无须在这个文件里指定App.DataStore的命名空间或者在模块内提起DataStore的构造函数。只要一个对象拥有和Datastore相同的方法,它就可以被Truck使用,而Truck并不关心它们是怎么实现的。
        this.db.add(order.emailAddress, order);
    };
​
    /*Truck从数据库中删除相应的订单*/
    Truck.prototype.deliverOrder = function (customerId) {
        console.log('Delivering order for ' + customerId);
        //deliverOrder也只调用this.db的remove方法,而无须知道remove方法如何运行。
        this.db.remove(customerId);
    };
​
    /*接受一个由客户邮箱地址组成的数组,遍历数组,然后使用console.log打印出订单信息。*/
    Truck.prototype.printOrders = function () {
        var customerIdArray = Object.keys(this.db.getAll()); //调用this.db.getAll来获取所有订单的键值对,然后将它们传入Object.keys, 从而获得一个仅包含键名的数组,再将这个数组赋值给变量customerldArray。
        console.log('Truck #' + this.truckId + ' has pending orders:');
        customerIdArray.forEach(function (id) { //在遍历这个数组时,将一个回调函数传给forEach。在回调函数内,尝试通过一个过(客户的邮箱地址)来获取订单信息。
            console.log(this.db.get(id));
        }.bind(this));//在forEach的回调函数外,this指代Truck实例。在forEach括号内部的匿名函数后马上调用.bind(this)方法,从而将修改过的匿名函数传给forEach。这个修改过的函数的所有者是Truck实例。
        // 最后遍历这个数组,为数组里的每个函数调用一次回调函数。
    };
​
    App.Truck = Truck;
    window.App = App;
​
})(window);

main.js

(function (window) {
    'use strict';
    var App = window.App;
    var Truck = App.Truck;
    var DataStore = App.DataStore;
    var myTruck = new Truck('ncc-1701', new DataStore());
    window.myTruck = myTruck; //为了便于交互,需要在main.js中将Truck暴露到全局命名空间。
})(window);

第九章 Bootstrap简介

添加Bootstrap

twitter-bootstrap - Libraries - cdnjs - The #1 free and open source CDN built to make life easier for developers:确保得到的是bootstrap.min.css的链接(如图9-2所示), 而不是其字体或者主题的链接。复制链接之后, 打开index.html添加一个<link>标签, 把该标签的href屈性设置为复制的链接地址。(为了适应版面, 不得不将href属性换行, 但是你应该把它写成一行。)

<head>
    <meta charset="UTF-8">
    <title>coffeerun</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.6.1/css/bootstrap.min.css">
</head>

Bootstrap文档

CSS · Bootstrap (getbootstrap.com):有关如何组织HTML的示例

代码展示

 index.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>coffeerun</title>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.6.1/css/bootstrap.min.css">
    </head>
    <body class="container">
        <header>
            <h1>CoffeeRun</h1>
        </header>
        <section><!--section可以对其他标记按逻辑进行分组。这里的<section>把表单的UI给框起来了-->
            <div class="panel panel-default"> <!--panel panel-default触发样式改变的Bootstrap类-->
                <div class="panel-body"> <!--panel-body触发样式改变的Bootstrap类-->
                    <form data-coffee-order="form"> <!--使用数据属性以便于Javascript DOM元素。-->
                        <div class="form-group"><!--form-control是Bootstrap定义的另一个类,它能为表单元素提供布局和排版样式。-->
                            <label for="coffeeOrder">Coffee Order</label><!--<label>标签可以大大增强表单元素的可用性。我们为<label>标签设置for属性来标记对应表单元素,for属性的值与要标记的表单元素的id属性相匹配。当一个<label>链接到了一个表单元素时,可以点击页面上的<label>文本,这样就会使其链接的表单元素处于激活状态。-->
                            <input class="form-control" name="coffee" id="coffeeOrder" autofocus> <!--autofocus:我们希望用户在页面加载完成之后无须点击, 就能立即在其中输入文本。-->
                        </div>
                        <div class="form-group">
                            <label for="emailInput">Email</label>
                            <input class="form-control" type="email" name="emailAddress" id="emailInput" autofocus value="" placeholder="dr@who.com"><!--placeholder属性:在文本字段中输入建议-->
                        </div>
                        <!--提供单选按钮。指定他们咖啡饮品的杯型:小杯、大杯、超大杯中选择一种。-->
                        <div class="radio"><!--将这三个radio的name属性设置为相同的值(size)。这会告诉浏览器,一次只能选择(或"选中")其中的一个。-->
                            <label>
                                <input type="radio" name="size" value="short">
                                Short
                            </label>
                        </div>
                        <div class="radio">
                            <label>
                                <input type="radio" name="size" value="tall" checked><!--为大杯单选按钮添加一个名为checked的布尔属性,它与autofocus的工作方式一样:当存在时,属性值为true;当不存在时,则为false。-->
                                Tall
                            </label>
                        </div>
                        <div class="radio">
                            <label>
                                <input type="radio" name="size" value="grande">
                                Grande
                            </label>
                        </div>
                        <!--添加下拉菜单。提供几种不同的口味以供选择。默认情况下不添加任何口味。-->
                        <div class="form-group">
                            <label for="flavorShot">Flavor Shot</label>
                            <select id="flavorShot" class="form-control" name="flavor"><!--每个<option>元素提供一个可能的值-->
                                <option value="">None</option><!--而<select>元素指定name值。默认情况下是第一个<option>元素被选中。如果想自动选择其他的选项(而不是第一个), 可以为将要选择的option元素添加一个selected的布尔属性。-->
                                <option value="caramel">Caramel</option>
                                <option value="almond">Almond</option>
                                <option value="mocha">Mocha</option>
                            </select>
                        </div>
                        <!--添加范围滑块。用户为他们的咖啡浓度选择一个介千0~100之间的值。-->
                        <div class="form-group">
                            <label for="strengthLevel">Caffeine Rating</label>
                            <input name="strength" id="strengthLevel" type="range" value="30"><!--创建一个范围滑块。 <input>和<label>元素应该关联起来,并包含在类名为form-group的<div>中。为了方便客户使用,将默认值设定为30。-->
                        </div>
                        <!--添加提交按钮和重置按钮-->
                        <button type="submit" class="btn btn-default">Submit</button>
                        <button type="reset" class="btn btn-default">Reset</button>
                    </form>
                </div>
            </div>
        </section>
        <script src="scripts/datastore.js" charset="UTF-8"></script>
        <script src="scripts/truck.js" charset="UTF-8"></script>
        <script src="scripts/main.js" charset="UTF-8"></script>
    </body>
</html>

第十章 使用JavaScript处理表单

jQuery简介

jQuery文档:Query库由 John Resig于 2006年创建,是最受欢迎的通用开源JavaScript库之一。此外,它为DOM操作、 元素创建、 服务器通信和事件处理提供了快速便捷的方法。

jquery - Libraries - cdnjs - The #1 free and open source CDN built to make life easier for developers:找到2.1.4版本并复制其地址。(可能有更高版本,但是在CoffeeRun中应使用2.1.4版本来避免兼容性问题。

</div>
</section>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js" charset="UTF-8"></script>
<script src="scripts/formhandler.js" charset="UTF-8"></script>
<script src="scripts/datastore.js" charset="UTF-8"></script>
<script src="scripts/truck.js" charset="UTF-8"></script>
<script src="scripts/main.js" charset="UTF-8"></script>

Bootstrap的模态行为

有关如何包含和触发Bootstrap的模态行为, 请参阅getbootstrap.corn/javascript上的文档。(需要添加一个<script>标签来引用cdnjs.com上Bootstrap的JavaScript。)

代码展示

 index.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>coffeerun</title>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.6.1/css/bootstrap.min.css">
    </head>
    <body class="container">
        <header>
            <h1>CoffeeRun</h1>
        </header>
        <section><!--section可以对其他标记按逻辑进行分组。这里的<section>把表单的UI给框起来了-->
            <div class="panel panel-default"> <!--panel panel-default触发样式改变的Bootstrap类-->
                <div class="panel-body"> <!--panel-body触发样式改变的Bootstrap类-->
                    <form data-coffee-order="form"> <!--使用数据属性以便于Javascript DOM元素。-->
                        <div class="form-group"><!--form-control是Bootstrap定义的另一个类,它能为表单元素提供布局和排版样式。-->
                            <label for="coffeeOrder">Coffee Order</label><!--<label>标签可以大大增强表单元素的可用性。我们为<label>标签设置for属性来标记对应表单元素,for属性的值与要标记的表单元素的id属性相匹配。当一个<label>链接到了一个表单元素时,可以点击页面上的<label>文本,这样就会使其链接的表单元素处于激活状态。-->
                            <input class="form-control" name="coffee" id="coffeeOrder" autofocus> <!--autofocus:我们希望用户在页面加载完成之后无须点击, 就能立即在其中输入文本。-->
                        </div>
                        <div class="form-group">
                            <label for="emailInput">Email</label>
                            <input class="form-control" type="email" name="emailAddress" id="emailInput" autofocus value="" placeholder="dr@who.com"><!--placeholder属性:在文本字段中输入建议-->
                        </div>
                        <!--提供单选按钮。指定他们咖啡饮品的杯型:小杯、大杯、超大杯中选择一种。-->
                        <div class="radio"><!--将这三个radio的name属性设置为相同的值(size)。这会告诉浏览器,一次只能选择(或"选中")其中的一个。-->
                            <label>
                                <input type="radio" name="size" value="short">
                                Short
                            </label>
                        </div>
                        <div class="radio">
                            <label>
                                <input type="radio" name="size" value="tall" checked><!--为大杯单选按钮添加一个名为checked的布尔属性,它与autofocus的工作方式一样:当存在时,属性值为true;当不存在时,则为false。-->
                                Tall
                            </label>
                        </div>
                        <div class="radio">
                            <label>
                                <input type="radio" name="size" value="grande">
                                Grande
                            </label>
                        </div>
                        <!--添加下拉菜单。提供几种不同的口味以供选择。默认情况下不添加任何口味。-->
                        <div class="form-group">
                            <label for="flavorShot">Flavor Shot</label>
                            <select id="flavorShot" class="form-control" name="flavor"><!--每个<option>元素提供一个可能的值-->
                                <option value="">None</option><!--而<select>元素指定name值。默认情况下是第一个<option>元素被选中。如果想自动选择其他的选项(而不是第一个), 可以为将要选择的option元素添加一个selected的布尔属性。-->
                                <option value="caramel">Caramel</option>
                                <option value="almond">Almond</option>
                                <option value="mocha">Mocha</option>
                            </select>
                        </div>
                        <!--添加范围滑块。用户为他们的咖啡浓度选择一个介千0~100之间的值。-->
                        <div class="form-group">
                            <label for="strengthLevel">Caffeine Rating</label>
                            <input name="strength" id="strengthLevel" type="range" value="30"><!--创建一个范围滑块。 <input>和<label>元素应该关联起来,并包含在类名为form-group的<div>中。为了方便客户使用,将默认值设定为30。-->
                        </div>
                        <!--添加提交按钮和重置按钮-->
                        <button type="submit" class="btn btn-default">Submit</button>
                        <button type="reset" class="btn btn-default">Reset</button>
                    </form>
                </div>
            </div>
        </section>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js" charset="UTF-8"></script><!--当我们添加Query<script>标签时,它创建了一个名为jQuery的函数,以及一个指向该函数的名为$的变量。-->
        <script src="scripts/formhandler.js" charset="UTF-8"></script>
        <script src="scripts/datastore.js" charset="UTF-8"></script>
        <script src="scripts/truck.js" charset="UTF-8"></script>
        <script src="scripts/main.js" charset="UTF-8"></script>
    </body>
</html>

formhandler.js

(function (window) { /*FormHandler将使用IIFE封装代码,并将一个构造函数附加到window.App属性*/
    'use strict';
    var App = window.App || {}; //window.App不存在,将一个空对象字面值赋值给它。声明一个FormHandler构造函数,并将其导出到window.App属性。
    var $ = window.jQuery;//FormHandler将会像App一样导入jQuery,这样做是为了指明模块是在使用在别处定义的代码。这是有助于团队成员互相协调和日后维护的最佳做法。
    function FormHandler(selector) { //FormHandler模块应该可以与任何一个<form>元素配合使用。为了实现这一点,可以给FormHandler构造函数传递一selector,而这个selector匹配index.html中<form>元素。
        if(!selector) { //如果未传入selector, 则抛出Error。
            throw new Error('No selector provided');//Error是一种内置类型,可以明确地表示代码中出现了意外的值或条件。但目前你使用的Error实例只是在控制台打印出消息。
        }

        this.$formElement = $(selector); //带$前缀的变量表示这个变量是通过jQuery选择出来的元素。常见书写惯例。
        /*确保元素选择器成功地从DOM检索到了一个元素。*/
        if (this.$formElement.length === 0){ //如果未查找到任何元素,jQuery将会返回空——如果选择器没有匹配到任何元素,它不会抛出一个异常。因此需要手动检查一下,因为FormHandler没有元素是不能工作的。jQuery封装集合的长度可以告诉我们有多少个匹配元素。
            throw new Error('Could not find element with selector: ' + selector);
        }
    }

    FormHandler.prototype.addSubmitHandler = function (fn) { //函数参数fn目的:对象传递到Truck实例的createOrder方法。
        console.log('Setting submit handler for form');
        this.$formElement.on('submit', function (event) { //on方法接受一个事件名称 并在事件被触发时执行回调。回调函数应该接受该事件的事件对象。
           event.preventDefault(); //调用event.preventDefault是为了确保用户提交表单时不会离开CoffeeRun页面。

            var data = {};
            $(this).serializeArray().forEach(function (item) {//在提交处理程序的回调中, this对象是对form元素的引用。jQuery提供了一个便捷的方法 (serializeArray)从表单获取值。为了使用serializeArray, 需要使用jQuery包装表单。调用$(this)会返回一个包装对象,这个包装对象可以使用serializeArray方法。
                data[item.name] = item.value;
                console.log(item.name + ' is ' + item.value); //使用相应对象的name和value在data上创建一个新属性。
            });
            console.log(data);
            fn(data); //在formhandler.js的提交处理程序回调中调用fn, 并把包含用户输入的数据对象传递给fn。
            this.reset();//如果提交表单之后, 旧数据能够被清除,那么用户便可以立即输入下一个订单
            this.elements[0].focus();//可以通过表单的elements属性轻松获得各个表单字段。elements是表单字段数组,可以从0开始索引并引用。为了让表单的特定字段被聚焦, 可以调用它的focus方法。(给咖啡订单添加的autofocus属性只会在网页首次加载时有效。)
        });
    }

    App.FormHandler = FormHandler;
    window.App = App;
})(window);

main.js

(function (window) {
    'use strict';
    var FORM_SELECTOR = '[data-coffee-order="form"]';
    var App = window.App;
    var Truck = App.Truck;
    var DataStore = App.DataStore;
    var FormHandler = App.FormHandler;
    var myTruck = new Truck('ncc-1701', new DataStore());
    window.myTruck = myTruck; //为了便于交互,需要在main.js中将Truck暴露到全局命名空间。
    var formHandler = new FormHandler(FORM_SELECTOR);//FORM_SELECTOR作为参数调用FormHandler构造函数,这样可以确保FormHandler实例与选择器选出的DOM元素绑定在一起。 再将这个实例赋值给一个名为formHandler的新变量。
    formHandler.addSubmitHandler(myTruck.createOrder.bind(myTruck));//myTruck.createOrder的所有者绑定为 myTruck, 然后再把这个函数传给formHandler.addSubmitHandler。
    console.log(formHandler);
})(window);

第十一章 从数据到DOM

代码展示

 点击Pending Orders的复选框1后

 index.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>coffeerun</title>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.6.1/css/bootstrap.min.css">
    </head>
    <body class="container">
        <header>
            <h1>CoffeeRun</h1>
        </header>
        <section><!--section可以对其他标记按逻辑进行分组。这里的<section>把表单的UI给框起来了-->
            <div class="panel panel-default"> <!--panel panel-default触发样式改变的Bootstrap类-->
                <div class="panel-body"> <!--panel-body触发样式改变的Bootstrap类-->
                    <form data-coffee-order="form"> <!--使用数据属性以便于Javascript DOM元素。-->
                        <div class="form-group"><!--form-control是Bootstrap定义的另一个类,它能为表单元素提供布局和排版样式。-->
                            <label for="coffeeOrder">Coffee Order</label><!--<label>标签可以大大增强表单元素的可用性。我们为<label>标签设置for属性来标记对应表单元素,for属性的值与要标记的表单元素的id属性相匹配。当一个<label>链接到了一个表单元素时,可以点击页面上的<label>文本,这样就会使其链接的表单元素处于激活状态。-->
                            <input class="form-control" name="coffee" id="coffeeOrder" autofocus> <!--autofocus:我们希望用户在页面加载完成之后无须点击, 就能立即在其中输入文本。-->
                        </div>
                        <div class="form-group">
                            <label for="emailInput">Email</label>
                            <input class="form-control" type="email" name="emailAddress" id="emailInput" autofocus value="" placeholder="dr@who.com"><!--placeholder属性:在文本字段中输入建议-->
                        </div>
                        <!--提供单选按钮。指定他们咖啡饮品的杯型:小杯、大杯、超大杯中选择一种。-->
                        <div class="radio"><!--将这三个radio的name属性设置为相同的值(size)。这会告诉浏览器,一次只能选择(或"选中")其中的一个。-->
                            <label>
                                <input type="radio" name="size" value="short">
                                Short
                            </label>
                        </div>
                        <div class="radio">
                            <label>
                                <input type="radio" name="size" value="tall" checked><!--为大杯单选按钮添加一个名为checked的布尔属性,它与autofocus的工作方式一样:当存在时,属性值为true;当不存在时,则为false。-->
                                Tall
                            </label>
                        </div>
                        <div class="radio">
                            <label>
                                <input type="radio" name="size" value="grande">
                                Grande
                            </label>
                        </div>
                        <!--添加下拉菜单。提供几种不同的口味以供选择。默认情况下不添加任何口味。-->
                        <div class="form-group">
                            <label for="flavorShot">Flavor Shot</label>
                            <select id="flavorShot" class="form-control" name="flavor"><!--每个<option>元素提供一个可能的值-->
                                <option value="">None</option><!--而<select>元素指定name值。默认情况下是第一个<option>元素被选中。如果想自动选择其他的选项(而不是第一个), 可以为将要选择的option元素添加一个selected的布尔属性。-->
                                <option value="caramel">Caramel</option>
                                <option value="almond">Almond</option>
                                <option value="mocha">Mocha</option>
                            </select>
                        </div>
                        <!--添加范围滑块。用户为他们的咖啡浓度选择一个介千0~100之间的值。-->
                        <div class="form-group">
                            <label for="strengthLevel">Caffeine Rating</label>
                            <input name="strength" id="strengthLevel" type="range" value="30"><!--创建一个范围滑块。 <input>和<label>元素应该关联起来,并包含在类名为form-group的<div>中。为了方便客户使用,将默认值设定为30。-->
                        </div>
                        <!--添加提交按钮和重置按钮-->
                        <button type="submit" class="btn btn-default">Submit</button>
                        <button type="reset" class="btn btn-default">Reset</button>
                    </form>
                </div>
            </div>
            <div class="panel panel-default">
                <div class="panel-body">
                    <h4>Pending Orders:</h4>
                    <div data-coffee-order="checklist"><!--使用<div>标签来展现Bootstrap的样式。 清单的主体部分是 [data-coffee•order=" checklist"]元素。在Javascript创建单个咖啡订单后,它将作为在DOM上展示清单项的目标元素。-->
​
                    </div>
                </div>
            </div>
        </section>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js" charset="UTF-8"></script><!--当我们添加Query<script>标签时,它创建了一个名为jQuery的函数,以及一个指向该函数的名为$的变量。-->
        <script src="scripts/checklist.js" charset="UTF-8"></script>
        <script src="scripts/formhandler.js" charset="UTF-8"></script>
        <script src="scripts/datastore.js" charset="UTF-8"></script>
        <script src="scripts/truck.js" charset="UTF-8"></script>
        <script src="scripts/main.js" charset="UTF-8"></script>
    </body>
</html>

checklist.js

(function (window){
    'use strict';
    /*导入App命名空间和jQuery, 并将它们赋值给一个局部变量*/
    var App = window.App || {};
    var $ = window.jQuery;
    /*接下来为Checklist创建一个构造函数, 记住要为构造函数传递一个selector参数,并且这个selector参数要至少能够匹配DOM中的一个元素。*/
    function CheckList(selector) {
        if (!selector) {
            throw new Error('No selector provided');
        }
        this.$element = $(selector);
        if (this.$element.length === 0) {
            throw new Error('Could not find element with selector: ' + selector);
        }
    }
    /*向Checklist添加一个名为addClickHandler原型方法,该方法的工作方式与FormHandler 的addSubmitHandler相同。换言之, 它将进行以下操作。(1)接受一个函数参数。(2)注册事件处理程序回调。(3)在事件处理程序回调中调用第一步中的函数参数。*/
    CheckList.prototype.addClickHandler = function (fn) {
        this.$element.on('click', 'input', function (event) {//使用this.$element.on注册回调事件处理程序时,要把click作为事件名称,但同时也要传入一个过滤选择器作为第二个参数。过滤选择器告知事件处理程序当且仅当事件是由<input>元素触发时才执行回调函数。这种模式被称为事件委托模式,它的工作原理是因为click和keypress等事件都会通过 DOM传播,这就意味着它们的祖先元素也会接收到事件。
            var email = event.target.value;
            this.removeRow(email);
            fn(email);
        }.bind(this));//不同的地方是,它将监听一个点击事件并将回调绑定到Checklist实例上。
    };
    //在这个新方法中,调用Row构造函数,并向其传入coffeeOrder参数,从而创建一个新的Row实例;再把新创建的实例赋值给变扯rowElement; 然后, 将rowElement的$element属性(它包含了DOM子树)附加到Checklist实例的$element属性(它是对清单项容器的引用)上。
    CheckList.prototype.addRow = function (coffeeOrder) {
        //移除匹配相应邮箱地址的已有行。每个客户只能拥有一个公开订单。因为我们使用简单的键值对存储数据,所以同 一邮箱地址对应订单中的后者会覆盖前者。
        this.removeRow(coffeeOrder.emailAddress);
        //使用咖啡订单信息创建一个新的Row实例
        var rowElement = new Row(coffeeOrder);
        //把新的Row实例的$element属性添加到清单中
        this.$element.append(rowElement.$element);
    }
    //在checklist.js中添加removeRow方法,并指定一个emailAddress参数。使用实例的$element属性来检索它的所有后代元素, 其中后代元素的value属性与邮箱参数要相匹配。接着,对检索到的元素调用closest方法,检索该元素的data-coffee-order属性为"checkbox"的祖先。最后,对该祖先执行remove方法。(代码中有些新语法,之后将对其进行解释。)
    CheckList.prototype.removeRow = function (email) {
        this.$element
            .find('[value="' + email + '"]')
            .closest('[data-coffee-order="checkbox"]')
            .remove();//请注意,this.$element.find只是在一个范围内查找,而不是搜索整个DOM——它只会搜索this.$element的后代元素。
    };
​
    /*Checklist模块需要3个方法来完成其工作:第1个负责创建一个清单项, 这个清单项包括了复选框和描述文本。可以将清单项视为table中的一行。第2个方法会从table中移除一行。第3个方法会为单击事件添加一个监听器,从而让代码知道何时需要移除一行。*/
    function Row(coffeeOrder) { //Row构造函数创建所有用于表示单个咖啡订单的DOM元素,包括复选框和描述文本,可以使用它构建带复选框的DOM子树来表示每个咖啡订单。不导出到App命名空间,它只会被Checklist.prototype内部的方法使用。coffeeOrder的参数:与传递给Truck.prototype.createOrder的数据是相同的。
        var $div = $('<div></div>', {
            'data-coffee-order' : 'checkbox',
            'class': 'checkbox' //'class'在单引号中是因为class是一个JavaScript保留字,因此需要使用单引号来防止浏览器以JavaScript形式对其进行解析(如果不使用单引号也会导致语法错误)。
        });//第一个参数为DOM元素的HTML标签。第二个参数为Query应该添加到<div>上的属性对象,对象的键值对会被转化为新元素的属性。它返回的结果是一个由jQuery创建的DOM元素, 我们将它赋值给一个名为$div的新变量。这并不是一个实例变量(也就是说,它只是$div而不是this.$div)
        var $label = $('<label></label>'); //不使用对象参数,因为它不需要额外的属性。
        var $checkbox = $('<input></input>', {
            type: 'checkbox',
            value: coffeeOrder.emailAddress
        });//调用$函数并为其传递一个<input>HTML标签,从而为复选框创建一个<input>元素。将第二个参数的type指定为checkbox, value指定为客户的邮箱地址。因为这些属性名称都没有使用特殊字符,所以不必将它们放在单引号之中。
        //复选框旁边显示的描述文本
        var description = coffeeOrder.size + ' ';
        if (coffeeOrder.flavor) {
            description += coffeeOrder.flavor + ' ';
        }
        description += coffeeOrder.coffee + ', ';
        description += ' (' + coffeeOrder.emailAddress + ')';
        description += ' [' + coffeeOrder.strength + 'x]';
        /*(1)把$checkbox追加到$label中。(2)把description追加到$label中。(3)把$label追加到$div中。在checklist.js中使用jQuery的append方法将元素连接在一起。此方法接受DOM元素或者jQuery封装集合作为参数,将其添加为调用者的子元素。*/
        $label.append($checkbox);
        $label.append(description);
        $div.append($label);
        //构造函数永远不应该有return语句。(当对构造函数使用new时,JavaScript会自动返回一个值。)但是可以把子树赋值给this.$element, 并将其当作实例的属性来访问。(选择此名称只是为了遵循其他构造函数的约定,它本身没有任何特殊的含义。)
        this.$element = $div;
    }
​
    /*在 IIFE的最后,将Checklist构造函数导出为App命名空间的一部分。*/
    App.CheckList = CheckList;
    window.App = App;
})(window);

main.js

(function (window) {
    'use strict';
    var FORM_SELECTOR = '[data-coffee-order="form"]';
    //匹配整个清单区域的[ data-coffee-order=" checklist"]选择器赋值到个变量上。
    var CHECKLIST_SELECTOR = '[data-coffee-order="checklist"]';
    var App = window.App;
    var Truck = App.Truck;
    var DataStore = App.DataStore;
    var FormHandler = App.FormHandler;
    var CheckList = App.CheckList;//把Checklist模块从APP命名空间导入到本地变批CHECKLIST_SELECTOR
    var myTruck = new Truck('ncc-1701', new DataStore());
    window.myTruck = myTruck; //为了便于交互,需要在main.js中将Truck暴露到全局命名空间。
    var checkList = new CheckList(CHECKLIST_SELECTOR);
    checkList.addClickHandler(myTruck.deliverOrder.bind(myTruck));//向checklist. addClickHandler传递一个绑定版的deliverOrder
    var formHandler = new FormHandler(FORM_SELECTOR);//FORM_SELECTOR作为参数调用FormHandler构造函数,这样可以确保FormHandler实例与选择器选出的DOM元素绑定在一起。 再将这个实例赋值给一个名为formHandler的新变量。
    /*删除此语句formHandler.addSubmitHandler(myTruck.createOrder.bind(myTruck));意思是myTruck.createOrder的所有者绑定为 myTruck, 然后再把这个函数传给formHandler.addSubmitHandler。*/
    //使用call与使用bind设置this值的方法类似。两者的区别是, bind返回一个新版本的函数或者方法, 但并不会立即执行它;而call实际上会调用返回的函数或者方法, 并允许将this设置为传入的第一个参数。(如果需要将其他的参数也传递到函数中, 只需要将额外参数添加到参数列表中即可。) call会运行函数体,并返回函数的返回值。
    formHandler.addSubmitHandler(function (data) {
        myTruck.createOrder.call(myTruck, data);
        checkList.addRow.call(checkList, data);
    });
    console.log(formHandler);
})(window);

第十二章 表单校验

正则表达式

RegExp(正则表达式) - JavaScript | MDN (mozilla.org):要了解更多关千正则表达式的知识,可以查看。

延展阅读: Webshim库

Release 1.16.0 · aFarkas/webshim · GitHub:注意!!!苹果的Safari浏览器并不支持约束校验API。如果我们需要使应用支持Safari,则需要使用一个库或者使用polyfill来模拟浏览器没有实现的API。Webshim是一个能在Safari中提供自定义约束功能的库,可以从此网站下载它。 解压档案如图放置,相关配置在代码展示也有提及~

代码展示

代码展示

 

 index.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>coffeerun</title>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.6.1/css/bootstrap.min.css">
        <style>
            form :focus:required:invalid {
                border-color: #a94442;
            }/*这会修改表单中拥有伪类:invalid的字段的边框颜色。 这个伪类会在表单进行有效性检测时, 由浏览器自动添加。*/
        </style>
    </head>
    <body class="container">
        <header>
            <h1>CoffeeRun</h1>
        </header>
        <section><!--section可以对其他标记按逻辑进行分组。这里的<section>把表单的UI给框起来了-->
            <div class="panel panel-default"> <!--panel panel-default触发样式改变的Bootstrap类-->
                <div class="panel-body"> <!--panel-body触发样式改变的Bootstrap类-->
                    <form data-coffee-order="form"> <!--使用数据属性以便于Javascript DOM元素。-->
                        <div class="form-group"><!--form-control是Bootstrap定义的另一个类,它能为表单元素提供布局和排版样式。-->
                            <label for="coffeeOrder">Coffee Order</label><!--<label>标签可以大大增强表单元素的可用性。我们为<label>标签设置for属性来标记对应表单元素,for属性的值与要标记的表单元素的id属性相匹配。当一个<label>链接到了一个表单元素时,可以点击页面上的<label>文本,这样就会使其链接的表单元素处于激活状态。-->
                            <input class="form-control" name="coffee" id="coffeeOrder" autofocus required pattern="[a-zA-Z\s]+" placeholder="只接受包含字母或空格的值"> <!--autofocus:我们希望用户在页面加载完成之后无须点击, 就能立即在其中输入文本。订单字段是必填的。正则表达式是:[a-zA-Z\s]+这句正则表达式的含义是:接受包含字母或空格的值-->
                        </div>
                        <div class="form-group">
                            <label for="emailInput">Email</label>
                            <input class="form-control" type="email" name="emailAddress" id="emailInput" autofocus value="" placeholder="dr@bignerdranch.com,只接受@bignerdranch.com域名的邮箱" required><!--placeholder属性:在文本字段中输入建议,邮箱地址字段是必填的-->
                        </div>
                        <!--提供单选按钮。指定他们咖啡饮品的杯型:小杯、大杯、超大杯中选择一种。-->
                        <div class="radio"><!--将这三个radio的name属性设置为相同的值(size)。这会告诉浏览器,一次只能选择(或"选中")其中的一个。-->
                            <label>
                                <input type="radio" name="size" value="short">
                                Short
                            </label>
                        </div>
                        <div class="radio">
                            <label>
                                <input type="radio" name="size" value="tall" checked><!--为大杯单选按钮添加一个名为checked的布尔属性,它与autofocus的工作方式一样:当存在时,属性值为true;当不存在时,则为false。-->
                                Tall
                            </label>
                        </div>
                        <div class="radio">
                            <label>
                                <input type="radio" name="size" value="grande">
                                Grande
                            </label>
                        </div>
                        <!--添加下拉菜单。提供几种不同的口味以供选择。默认情况下不添加任何口味。-->
                        <div class="form-group">
                            <label for="flavorShot">Flavor Shot</label>
                            <select id="flavorShot" class="form-control" name="flavor"><!--每个<option>元素提供一个可能的值-->
                                <option value="">None</option><!--而<select>元素指定name值。默认情况下是第一个<option>元素被选中。如果想自动选择其他的选项(而不是第一个), 可以为将要选择的option元素添加一个selected的布尔属性。-->
                                <option value="caramel">Caramel</option>
                                <option value="almond">Almond</option>
                                <option value="mocha">Mocha</option>
                            </select>
                        </div>
                        <!--添加范围滑块。用户为他们的咖啡浓度选择一个介千0~100之间的值。-->
                        <div class="form-group">
                            <label for="strengthLevel">Caffeine Rating</label>
                            <input name="strength" id="strengthLevel" type="range" value="30"><!--创建一个范围滑块。 <input>和<label>元素应该关联起来,并包含在类名为form-group的<div>中。为了方便客户使用,将默认值设定为30。-->
                        </div>
                        <!--添加提交按钮和重置按钮-->
                        <button type="submit" class="btn btn-default">Submit</button>
                        <button type="reset" class="btn btn-default">Reset</button>
                    </form>
                </div>
            </div>
            <div class="panel panel-default">
                <div class="panel-body">
                    <h4>Pending Orders:</h4>
                    <div data-coffee-order="checklist"><!--使用<div>标签来展现Bootstrap的样式。 清单的主体部分是 [data-coffee•order=" checklist"]元素。在Javascript创建单个咖啡订单后,它将作为在DOM上展示清单项的目标元素。-->
​
                    </div>
                </div>
            </div>
        </section>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js" charset="UTF-8"></script><!--当我们添加Query<script>标签时,它创建了一个名为jQuery的函数,以及一个指向该函数的名为$的变量。-->
        <script src="webshim/polyfiller.js" charset="UTF-8"></script>
        <script src="scripts/validation.js" charset="UTF-8"></script>
        <script src="scripts/checklist.js" charset="UTF-8"></script>
        <script src="scripts/formhandler.js" charset="UTF-8"></script>
        <script src="scripts/datastore.js" charset="UTF-8"></script>
        <script src="scripts/truck.js" charset="UTF-8"></script>
        <script src="scripts/main.js" charset="UTF-8"></script>
    </body>
</html>

validation.js

(function (window) {
    'use strict';
    var App = window.App || {};
​
    var Validation = {
        isCompanyEmail: function (email) {
            return /.+@bignerdranch\.com$/.test(email);//将字符串放笠在正斜杠(//)之间可以构成正则表达式。在斜杠之间我们指定了一个字符串必须由一个或者多个字符(.+)和@bignerdranch.com组成。同时,还使用了反斜杠表示bignerdranch.com中的句点应被视为字面值。(一般来说,正则表达式中的句点可以用来匹配任意字符。)结尾的$表示字符串必须以@bignerdranch.com结尾;换句话说,在这之后不能有任何字符。正则表达式的对象拥有一个test方法。把字符串传给test方法,它会返回一个布尔值——true表示字符串符合正则表达式的校验规则,而false表示不符合。
        }
    };
​
    //并将该Validation变量暴露到App命名空间。
    App.Validation = Validation;
    window.App = App;
​
})(window);

main.js

(function (window) {
    'use strict';
    var FORM_SELECTOR = '[data-coffee-order="form"]';
    //匹配整个清单区域的[ data-coffee-order=" checklist"]选择器赋值到个变量上。
    var CHECKLIST_SELECTOR = '[data-coffee-order="checklist"]';
    var App = window.App;
    var Truck = App.Truck;
    var DataStore = App.DataStore;
    var FormHandler = App.FormHandler;
    var Validation = App.Validation;//从App命名空间引入Validation, 并将其保存到本地变量中。
    var CheckList = App.CheckList;//把Checklist模块从APP命名空间导入到本地变批CHECKLIST_SELECTOR
    var webshim = window.webshim;
    var myTruck = new Truck('ncc-1701', new DataStore());
    window.myTruck = myTruck; //为了便于交互,需要在main.js中将Truck暴露到全局命名空间。
    var checkList = new CheckList(CHECKLIST_SELECTOR);
    checkList.addClickHandler(myTruck.deliverOrder.bind(myTruck));//向checklist. addClickHandler传递一个绑定版的deliverOrder
    var formHandler = new FormHandler(FORM_SELECTOR);//FORM_SELECTOR作为参数调用FormHandler构造函数,这样可以确保FormHandler实例与选择器选出的DOM元素绑定在一起。 再将这个实例赋值给一个名为formHandler的新变量。
    /*删除此语句formHandler.addSubmitHandler(myTruck.createOrder.bind(myTruck));意思是myTruck.createOrder的所有者绑定为 myTruck, 然后再把这个函数传给formHandler.addSubmitHandler。*/
    //使用call与使用bind设置this值的方法类似。两者的区别是, bind返回一个新版本的函数或者方法, 但并不会立即执行它;而call实际上会调用返回的函数或者方法, 并允许将this设置为传入的第一个参数。(如果需要将其他的参数也传递到函数中, 只需要将额外参数添加到参数列表中即可。) call会运行函数体,并返回函数的返回值。
    formHandler.addSubmitHandler(function (data) {
        myTruck.createOrder.call(myTruck, data);
        checkList.addRow.call(checkList, data);
    });
    formHandler.addInputHandler(Validation.isCompanyEmail);
    webshim.polyfill('forms forms-ext');
    webshim.setOptions('forms', {addValidators: true, lazyCustomMessages: true});
})(window);

formhandler.js

(function (window) { /*FormHandler将使用IIFE封装代码,并将一个构造函数附加到window.App属性*/
    'use strict';
    var App = window.App || {}; //window.App不存在,将一个空对象字面值赋值给它。声明一个FormHandler构造函数,并将其导出到window.App属性。
    var $ = window.jQuery;//FormHandler将会像App一样导入jQuery,这样做是为了指明模块是在使用在别处定义的代码。这是有助于团队成员互相协调和日后维护的最佳做法。
    function FormHandler(selector) { //FormHandler模块应该可以与任何一个<form>元素配合使用。为了实现这一点,可以给FormHandler构造函数传递一selector,而这个selector匹配index.html中<form>元素。
        if(!selector) { //如果未传入selector, 则抛出Error。
            throw new Error('No selector provided');//Error是一种内置类型,可以明确地表示代码中出现了意外的值或条件。但目前你使用的Error实例只是在控制台打印出消息。
        }
​
        this.$formElement = $(selector); //带$前缀的变量表示这个变量是通过jQuery选择出来的元素。常见书写惯例。
        /*确保元素选择器成功地从DOM检索到了一个元素。*/
        if (this.$formElement.length === 0){ //如果未查找到任何元素,jQuery将会返回空——如果选择器没有匹配到任何元素,它不会抛出一个异常。因此需要手动检查一下,因为FormHandler没有元素是不能工作的。jQuery封装集合的长度可以告诉我们有多少个匹配元素。
            throw new Error('Could not find element with selector: ' + selector);
        }
    }
​
    FormHandler.prototype.addSubmitHandler = function (fn) { //函数参数fn目的:对象传递到Truck实例的createOrder方法。
        console.log('Setting submit handler for form');
        this.$formElement.on('submit', function (event) { //on方法接受一个事件名称 并在事件被触发时执行回调。回调函数应该接受该事件的事件对象。
           event.preventDefault(); //调用event.preventDefault是为了确保用户提交表单时不会离开CoffeeRun页面。
            var data = {};
            $(this).serializeArray().forEach(function (item) {//在提交处理程序的回调中, this对象是对form元素的引用。jQuery提供了一个便捷的方法 (serializeArray)从表单获取值。为了使用serializeArray, 需要使用jQuery包装表单。调用$(this)会返回一个包装对象,这个包装对象可以使用serializeArray方法。
                data[item.name] = item.value;
                console.log(item.name + ' is ' + item.value); //使用相应对象的name和value在data上创建一个新属性。
            });
            console.log(data);
            fn(data); //在formhandler.js的提交处理程序回调中调用fn, 并把包含用户输入的数据对象传递给fn。
            this.reset();//如果提交表单之后, 旧数据能够被清除,那么用户便可以立即输入下一个订单
            this.elements[0].focus();//可以通过表单的elements属性轻松获得各个表单字段。elements是表单字段数组,可以从0开始索引并引用。为了让表单的特定字段被聚焦, 可以调用它的focus方法。(给咖啡订单添加的autofocus属性只会在网页首次加载时有效。)
        });
    };
    //因此只能在用户输入的时候执行校验程序,添加addinputHandler原型方法,它会在表单上绑定input件监听器。和addSubmitHandler一样, 它接受一个函数实参
    FormHandler.prototype.addInputHandler = function (fn) {
        console.log('Setting input handler for form');
        //使用jQuery的on方法绑定input事件的监听器, 确保使用事件委托模式过滤掉了除[name="emailAddress"]字段外的其他字段触发的事件。
        this.$formElement.on('input', '[name="emailAddress"]', function (event) {
            //在事件处理程序中, 从event.target对象中提取出邮箱地址字段的值。 接着执行addinputHandler的函数参数fn,并且将邮箱地址作为参数传入。
            var emailAddress = event.target.value;
            var message = '';
            //如果fn(emailAddress)返回true, 清除该字段的有效性提示, 否则将警告信息分配给 message变量,并将message设为有效性提示
            //Safari用户注意,webshim需要留意-在使用setCustomValidity的时候,必须使用jQuery包裹对象。需要将event.target对象包裹起来。
            if (fn(emailAddress)) {
                $(event.target).setCustomValidity('');
            }
            else {
                message = emailAddress + ' is not an authorized email address!后缀应该要@bignerdranch.com';
                $(event.target).setCustomValidity(message);
            }
        })
    }
​
    App.FormHandler = FormHandler;
    window.App = App;
})(window);

第十三章 Ajax

Ajax

  • Ajax是通过JavaScript与远端服务器通信的技术。

  • JavaScript可以在无须刷新页面的情况下,利用服务器返回的数据更新页面,这大大改善了Web应用的体验。

  • 最初,术语Ajax是asynchronous JavaScript and XML (异步JavaScript和XML)的缩写。

  • 但是现在无论是使用什么技术实现的,只要是符合这种风格的异步数据通信方式,都统称为Ajax。(异步通信意味着应用在发送请求后就可以执行其他任务,无需等待服务器响应。)

  • 如今,Ajax是在 后台发送和接受数据的标准机制。

XMLHttpRequest对象

  • Ajax 的核心是XMLHttpRequest API 。

  • 通过它能够在不刷新页面的情况下向服务器发送请求。这一切都在后台进行。

  • 通过XMLHttpRequest对象,可以监听请求、响应周期中的任何阶段,这和在DOM对象上监听事件大同小异。

  • 可以检查XMLHttpRequest的属性来获得现在的请求和响应周期的状态。

    • 其中response和status是两个十分有用的属性,它们会在改变发生的时候即时更新。

    • response 属性包括了服务器返回的数据(如HTML、XML、JSON等)

    • status是用千展示HTTP响应是否成功的数字代码,它们也被称作HTTP状态码。

    • 状态码按照范围分组,每个范围 都有自己的基本含义。 例如,200-299的状态码表示成功,而500-599的状态码表示服务器错误。 这些范围通常会被称作".xx"或者"3xx"状态。

状态码 状态文本 描述
200 OK 请求成功
400 错误请求 服务器不能理解该请求
404 找不到 找不到对应资源, 通常是因为文件或路径名不正确
500 服务器内部错误 服务器遇到了一个错误, 例如在服务器的代码中有一个未处理的异常
503 服务器不可用 服务器不能处理该请求, 通常是因为服务器过载或者若机

RESTful Web服务

  • CoffeeRun服务器提供了RESTful Web服务。

    • "REST"表示具象状态传输"(representational state transfer)

    • 这是一种基于HTTP操作(GET、POST、PUT、DELETE)和用于标识服务器上资源的URL的Web服务风格。

  • 通常,URL路径(服务器名称后的部分)会指代事物的集合(如/coffeeorders) 或者通过ID 标识个别事物(如/coffeeorders/[customer email])。

  • URL的不同会影响HTTP操作的结果。例如:

    • 使用集合类的URL时,GET请求会检索集合中所有物品的列表;

    • 如果是使用个别事物的URL,GET请求会检索该事物的所有细节信息。

URL路径 GET POST PUT DELETE
/coffeeorders 列举所有记录 创建一个记录 - 删除所有记录
/coffeeorders/a@b.com 得到该记录 - 更新记录 删除该记录

jQuery的$.ajax方法

jQuery.ajax() | jQuery API Documentation:Ajax的请求有许多选项,此网站可以帮助了解更多

代码展示

 remotedatastore.js

/*RemoteDataStore 模块,代表应用和服务器交流。每一个RemoteDataStore方法都接受一个函数实参,它会在接受服务响应后执行。*/
(function (window) {
    'use strict';
    /*引入App命名空间和jQuery*/
    var App = window.App || {};
    var $ = window.jQuery;
​
    /*创建IIFE模块和名为RemoteDataStore的构造函数。构造函数接受一个参数作为远端服务器的URL。如果这个参数并没有传递进来,构造函数会抛出一个错误。*/
    function RemoteDataStore(url) {
        if (!url) {
            throw new Error('No remote URL supplied.');
        }
        this.serverUrl = url;
    }
​
    /*为RemoteDataStore添加一个原型方法。和DataStore的add方法一样,它有key和val两个形参。*/
    RemoteDataStore.prototype.add = function (key, val) {
        $.post(this.serverUrl, val, function (serverResponse) {
            console.log(serverResponse);
        });//$.post方法只需要两个信息:服务器的URL和要带上的数据。和许多jQuery的方法一样,$.post接受一个可选的参数。传递一个回调函数作为第三个参数。当从服务器得到响应后,这个函数会被执行,并将数据传入其中。
        //请注意 ,我们并没有使用key参数。添加它是为了保待add方法在RemoteDataStore和DataStore中的一致性——它们都将咖啡订单信息作为第二个参数。
    };
    /*接下来 要使用jQuery的$.get方法。和$.post一样, 要对其传入服务器的URL。 我们不需要传入数据, 因为我们是要检索信息而非保存信息。除此之外,还要传入回调函数,这样就能在接收到服务端响应后进行调用。*/
    RemoteDataStore.prototype.getAll = function (cb) {//如果给getAll传递个函数实参cb,就可以在$.get的回调函数内部调用这个参数,也就能访问函数参数和服务器响应了。
        $.get(this.serverUrl, function (serverResponse) {
            console.log(serverResponse);
            cb(serverResponse); //getAll方法获得了远端服务器的所有订单数据,并且将数据传递给了它获得的回调函数cb。
        });
    };
​
    /*根据用户的邮箱地址获取单个咖啡订单。 和getAll样,它接受 函数实参,同样用于传递获得的咖啡订单。*/
    RemoteDataStore.prototype.get = function (get, cb) {
        $.get(this.serverurl + '/' + key, function (serverResponse) {
            console.log(serverResponse);
            cb(serverResponse);
        })
    }
​
    /*jQuery并没有提供一个通过Ajax发送DELETE请求的方便操作,因此我们使用$.ajax方法。($.get和$.post本质上都是调用了$.ajax方法并且注明了使用GET或者POST请求。)添加remove的原型方法。在其中调用$.ajax方法,传入两个参数。第一个参数是一个咖啡订单的URL, 由服务器URL斜杠和键名(邮箱地址 )组成;第二个参数是一
一个包括了Ajax请求的选项或设置的对象 。type设置为DELETE。*/
    RemoteDataStore.prototype.remove = function (key) {
        $.ajax(this.serverUrl + '/' + key, {
            type: 'DELETE'
        });
    };
​
​
    /*将RemoteDataStore暴露到App命名空间上。*/
    App.RemoteDataStore = RemoteDataStore;
    window.App = App;
})(window);

index.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>coffeerun</title>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.6.1/css/bootstrap.min.css">
        <style>
            form :focus:required:invalid {
                border-color: #a94442;
            }/*这会修改表单中拥有伪类:invalid的字段的边框颜色。 这个伪类会在表单进行有效性检测时, 由浏览器自动添加。*/
        </style>
    </head>
    <body class="container">
        <header>
            <h1>CoffeeRun</h1>
        </header>
        <section><!--section可以对其他标记按逻辑进行分组。这里的<section>把表单的UI给框起来了-->
            <div class="panel panel-default"> <!--panel panel-default触发样式改变的Bootstrap类-->
                <div class="panel-body"> <!--panel-body触发样式改变的Bootstrap类-->
                    <form data-coffee-order="form"> <!--使用数据属性以便于Javascript DOM元素。-->
                        <div class="form-group"><!--form-control是Bootstrap定义的另一个类,它能为表单元素提供布局和排版样式。-->
                            <label for="coffeeOrder">Coffee Order</label><!--<label>标签可以大大增强表单元素的可用性。我们为<label>标签设置for属性来标记对应表单元素,for属性的值与要标记的表单元素的id属性相匹配。当一个<label>链接到了一个表单元素时,可以点击页面上的<label>文本,这样就会使其链接的表单元素处于激活状态。-->
                            <input class="form-control" name="coffee" id="coffeeOrder" autofocus required pattern="[a-zA-Z\s]+" placeholder="只接受包含字母或空格的值"> <!--autofocus:我们希望用户在页面加载完成之后无须点击, 就能立即在其中输入文本。订单字段是必填的。正则表达式是:[a-zA-Z\s]+这句正则表达式的含义是:接受包含字母或空格的值-->
                        </div>
                        <div class="form-group">
                            <label for="emailInput">Email</label>
                            <input class="form-control" type="email" name="emailAddress" id="emailInput" autofocus value="" placeholder="dr@bignerdranch.com,只接受@bignerdranch.com域名的邮箱" required><!--placeholder属性:在文本字段中输入建议,邮箱地址字段是必填的-->
                        </div>
                        <!--提供单选按钮。指定他们咖啡饮品的杯型:小杯、大杯、超大杯中选择一种。-->
                        <div class="radio"><!--将这三个radio的name属性设置为相同的值(size)。这会告诉浏览器,一次只能选择(或"选中")其中的一个。-->
                            <label>
                                <input type="radio" name="size" value="short">
                                Short
                            </label>
                        </div>
                        <div class="radio">
                            <label>
                                <input type="radio" name="size" value="tall" checked><!--为大杯单选按钮添加一个名为checked的布尔属性,它与autofocus的工作方式一样:当存在时,属性值为true;当不存在时,则为false。-->
                                Tall
                            </label>
                        </div>
                        <div class="radio">
                            <label>
                                <input type="radio" name="size" value="grande">
                                Grande
                            </label>
                        </div>
                        <!--添加下拉菜单。提供几种不同的口味以供选择。默认情况下不添加任何口味。-->
                        <div class="form-group">
                            <label for="flavorShot">Flavor Shot</label>
                            <select id="flavorShot" class="form-control" name="flavor"><!--每个<option>元素提供一个可能的值-->
                                <option value="">None</option><!--而<select>元素指定name值。默认情况下是第一个<option>元素被选中。如果想自动选择其他的选项(而不是第一个), 可以为将要选择的option元素添加一个selected的布尔属性。-->
                                <option value="caramel">Caramel</option>
                                <option value="almond">Almond</option>
                                <option value="mocha">Mocha</option>
                            </select>
                        </div>
                        <!--添加范围滑块。用户为他们的咖啡浓度选择一个介千0~100之间的值。-->
                        <div class="form-group">
                            <label for="strengthLevel">Caffeine Rating</label>
                            <input name="strength" id="strengthLevel" type="range" value="30"><!--创建一个范围滑块。 <input>和<label>元素应该关联起来,并包含在类名为form-group的<div>中。为了方便客户使用,将默认值设定为30。-->
                        </div>
                        <!--添加提交按钮和重置按钮-->
                        <button type="submit" class="btn btn-default">Submit</button>
                        <button type="reset" class="btn btn-default">Reset</button>
                    </form>
                </div>
            </div>
            <div class="panel panel-default">
                <div class="panel-body">
                    <h4>Pending Orders:</h4>
                    <div data-coffee-order="checklist"><!--使用<div>标签来展现Bootstrap的样式。 清单的主体部分是 [data-coffee•order=" checklist"]元素。在Javascript创建单个咖啡订单后,它将作为在DOM上展示清单项的目标元素。-->
​
                    </div>
                </div>
            </div>
        </section>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js" charset="UTF-8"></script><!--当我们添加Query<script>标签时,它创建了一个名为jQuery的函数,以及一个指向该函数的名为$的变量。-->
        <script src="webshim/polyfiller.js" charset="UTF-8"></script>
        <script src="scripts/validation.js" charset="UTF-8"></script>
        <script src="scripts/checklist.js" charset="UTF-8"></script>
        <script src="scripts/formhandler.js" charset="UTF-8"></script>
        <script src="scripts/remotedatastore.js" charset="UTF-8"></script>
        <script src="scripts/datastore.js" charset="UTF-8"></script>
        <script src="scripts/truck.js" charset="UTF-8"></script>
        <script src="scripts/main.js" charset="UTF-8"></script>
    </body>
</html>

main.js

(function (window) {
    'use strict';
    var FORM_SELECTOR = '[data-coffee-order="form"]';
    //匹配整个清单区域的[ data-coffee-order=" checklist"]选择器赋值到个变量上。
    var CHECKLIST_SELECTOR = '[data-coffee-order="checklist"]';
    var SERVER_URL = 'http://coffeerun-v2-rest-api.herokuapp.com/api/coffeeorders';
    var App = window.App;
    var Truck = App.Truck;
    var DataStore = App.DataStore;
    var RemoteDataStore = App.RemoteDataStore;
    var FormHandler = App.FormHandler;
    var Validation = App.Validation;//从App命名空间引入Validation, 并将其保存到本地变量中。
    var CheckList = App.CheckList;//把Checklist模块从APP命名空间导入到本地变批CHECKLIST_SELECTOR
    var webshim = window.webshim;
    var remoteDS = new RemoteDataStore(SERVER_URL);
    // var myTruck = new Truck('ncc-1701', new DataStore());最后,将remoteDS,而不是DataStore实例,传入Truck构造函数。 因为DataStore和一RemoteDataStore拥有相同的方法,并且接受(几乎)一致的参数,这种变化将无缝工作。
    var myTruck = new Truck('ncc-1701', remoteDS);
    window.myTruck = myTruck; //为了便于交互,需要在main.js中将Truck暴露到全局命名空间。
    var checkList = new CheckList(CHECKLIST_SELECTOR);
    checkList.addClickHandler(myTruck.deliverOrder.bind(myTruck));//向checklist. addClickHandler传递一个绑定版的deliverOrder
    var formHandler = new FormHandler(FORM_SELECTOR);//FORM_SELECTOR作为参数调用FormHandler构造函数,这样可以确保FormHandler实例与选择器选出的DOM元素绑定在一起。 再将这个实例赋值给一个名为formHandler的新变量。
    /*删除此语句formHandler.addSubmitHandler(myTruck.createOrder.bind(myTruck));意思是myTruck.createOrder的所有者绑定为 myTruck, 然后再把这个函数传给formHandler.addSubmitHandler。*/
    //使用call与使用bind设置this值的方法类似。两者的区别是, bind返回一个新版本的函数或者方法, 但并不会立即执行它;而call实际上会调用返回的函数或者方法, 并允许将this设置为传入的第一个参数。(如果需要将其他的参数也传递到函数中, 只需要将额外参数添加到参数列表中即可。) call会运行函数体,并返回函数的返回值。
    formHandler.addSubmitHandler(function (data) {
        myTruck.createOrder.call(myTruck, data);
        checkList.addRow.call(checkList, data);
    });
    formHandler.addInputHandler(Validation.isCompanyEmail);
    webshim.polyfill('forms forms-ext');
    webshim.setOptions('forms', {addValidators: true, lazyCustomMessages: true});
})(window);

第十四章 Deferred和Promise

Promise和Deferred

Promise

  • Promise提供了一种管理复杂异步操作的方法。

  • Promise能够不再依赖回调函数, 通过返回Promise对象, 一步步解耦模块。

  • Promise对象有三种状态: pending、 fulfilled和rejected (如图 14-2所示)。

  • 每个Promise对象都有一个then方法,它会在Promise状态变为fulfilled时被触发。

    • 我们可以调用then并且传入一个回调函数。 当Promise的状态变为fulfilled时, 回调函数就会被执行, 同时会收到Promise异步操作后所取得的数据。

    • 可以链式调用多个then函数。相比于编写函数再让其接受数据并调用回调函数,返回一个Promise对象以利用then作为链式调用更为妥当。

Deferred

  • 先从jQuery的Deferred对象开始, 它和Promise十分相似。

  • jQuery的$.ajax方法(包括$.post和$.get方法)会返回一个Deferred对象。

  • Deferred对象会根据自身的两种状态——fulfilled和rejected——分别调用对应函数。

  • 从更新RemoteDataStore 模块开始,这样子它就能返回助Query的Ajax方法生成的Deferred。

  • 接着,修改其他模块,让它们在Deferred上注册回调函数。

代码展示

对于所有的代码有大幅度的修改,相关修改在以下提及,没修改的没有提及~

使用RemoteDataStore(main.js中做修改如下):刷新页面能够获取数据,并将之前添加的订单显示与Pending Orders,右下角truck.js打印的就是每个订单对象。

// var myTruck = new Truck('ncc-1701', new DataStore());
// 将remoteDS,而不是DataStore实例,传入Truck构造函数。 因为DataStore和一RemoteDataStore拥有相同的方法,并且接受(几乎)一致的参数,这种变化将无缝工作。
var myTruck = new Truck('ncc-1701', remoteDS);

 使用DataStore(main.js中做修改如下):经过在DataStore中的实现Promise的相关代码修改,便能能够正常运作

var myTruck = new Truck('ncc-1701', new DataStore());
// 将remoteDS,而不是DataStore实例,传入Truck构造函数。 因为DataStore和一RemoteDataStore拥有相同的方法,并且接受(几乎)一致的参数,这种变化将无缝工作。
// var myTruck = new Truck('ncc-1701', remoteDS);

 remotedatastore.js

/*RemoteDataStore 模块,代表应用和服务器交流。每一个RemoteDataStore方法都接受一个函数实参,它会在接受服务响应后执行。*/
(function (window) {
    'use strict';
    /*引入App命名空间和jQuery*/
    var App = window.App || {};
    var $ = window.jQuery;
​
    /*创建IIFE模块和名为RemoteDataStore的构造函数。构造函数接受一个参数作为远端服务器的URL。如果这个参数并没有传递进来,构造函数会抛出一个错误。*/
    function RemoteDataStore(url) {
        if (!url) {
            throw new Error('No remote URL supplied.');
        }
        this.serverUrl = url;
    }
​
    /*为RemoteDataStore添加一个原型方法。和DataStore的add方法一样,它有key和val两个形参。*/
    RemoteDataStore.prototype.add = function (key, val) {
        return $.post(this.serverUrl, val, function (serverResponse) { //开始使用jQuery的$.ajax方法返回的Deferred对象
            console.log(serverResponse);
        });//$.post方法只需要两个信息:服务器的URL和要带上的数据。和许多jQuery的方法一样,$.post接受一个可选的参数。传递一个回调函数作为第三个参数。当从服务器得到响应后,这个函数会被执行,并将数据传入其中。
        //请注意 ,我们并没有使用key参数。添加它是为了保待add方法在RemoteDataStore和DataStore中的一致性——它们都将咖啡订单信息作为第二个参数。
    };
    /*接下来 要使用jQuery的$.get方法。和$.post一样, 要对其传入服务器的URL。 我们不需要传入数据, 因为我们是要检索信息而非保存信息。除此之外,还要传入回调函数,这样就能在接收到服务端响应后进行调用。*/
    RemoteDataStore.prototype.getAll = function (cb) {//如果给getAll传递个函数实参cb,就可以在$.get的回调函数内部调用这个参数,也就能访问函数参数和服务器响应了。
        return $.get(this.serverUrl, function (serverResponse) { //开始使用jQuery的$.ajax方法返回的Deferred对象
            if (cb) { //因为现在返回jQuery的Ajax方法生成的Deferred对象,所以getAll并不一定要接受回调函数。为了确认是否有回调函数, 需要在执行cb前添加订语句进行判断。
                console.log(serverResponse);
                cb(serverResponse); //getAll方法获得了远端服务器的所有订单数据,并且将数据传递给了它获得的回调函数cb。
            }
        });
    };
​
    /*根据用户的邮箱地址获取单个咖啡订单。 和getAll样,它接受 函数实参,同样用于传递获得的咖啡订单。*/
    RemoteDataStore.prototype.get = function (get, cb) {
        return $.get(this.serverurl + '/' + key, function (serverResponse) { //开始使用jQuery的$.ajax方法返回的Deferred对象
            if(cb) { //因为现在返回jQuery的Ajax方法生成的Deferred对象,所以get并不一定要接受回调函数。为了确认是否有回调函数, 需要在执行cb前添加订语句进行判断。
                console.log(serverResponse);
                cb(serverResponse);
            }
        })
    }
​
    /*jQuery并没有提供一个通过Ajax发送DELETE请求的方便操作,因此我们使用$.ajax方法。($.get和$.post本质上都是调用了$.ajax方法并且注明了使用GET或者POST请求。)添加remove的原型方法。在其中调用$.ajax方法,传入两个参数。第一个参数是一个咖啡订单的URL, 由服务器URL斜杠和键名(邮箱地址 )组成;第二个参数是一
一个包括了Ajax请求的选项或设置的对象 。type设置为DELETE。*/
    RemoteDataStore.prototype.remove = function (key) {
        return $.ajax(this.serverUrl + '/' + key, { //开始使用jQuery的$.ajax方法返回的Deferred对象
            type: 'DELETE'
        });
    };
​
​
    /*将RemoteDataStore暴露到App命名空间上。*/
    App.RemoteDataStore = RemoteDataStore;
    window.App = App;
})(window);

main.js

(function (window) {
    'use strict';
    var FORM_SELECTOR = '[data-coffee-order="form"]';
    //匹配整个清单区域的[ data-coffee-order=" checklist"]选择器赋值到个变量上。
    var CHECKLIST_SELECTOR = '[data-coffee-order="checklist"]';
    var SERVER_URL = 'http://coffeerun-v2-rest-api.herokuapp.com/api/coffeeorders';
    var App = window.App;
    var Truck = App.Truck;
    var DataStore = App.DataStore;
    var RemoteDataStore = App.RemoteDataStore;
    var FormHandler = App.FormHandler;
    var Validation = App.Validation;//从App命名空间引入Validation, 并将其保存到本地变量中。
    var CheckList = App.CheckList;//把Checklist模块从APP命名空间导入到本地变批CHECKLIST_SELECTOR
    var webshim = window.webshim;
    var remoteDS = new RemoteDataStore(SERVER_URL);
    var myTruck = new Truck('ncc-1701', new DataStore());
    // 将remoteDS,而不是DataStore实例,传入Truck构造函数。 因为DataStore和一RemoteDataStore拥有相同的方法,并且接受(几乎)一致的参数,这种变化将无缝工作。
    // var myTruck = new Truck('ncc-1701', remoteDS);
    window.myTruck = myTruck; //为了便于交互,需要在main.js中将Truck暴露到全局命名空间。
    var checkList = new CheckList(CHECKLIST_SELECTOR);
    checkList.addClickHandler(myTruck.deliverOrder.bind(myTruck));//向checklist. addClickHandler传递一个绑定版的deliverOrder
    var formHandler = new FormHandler(FORM_SELECTOR);//FORM_SELECTOR作为参数调用FormHandler构造函数,这样可以确保FormHandler实例与选择器选出的DOM元素绑定在一起。 再将这个实例赋值给一个名为formHandler的新变量。
    /*删除此语句formHandler.addSubmitHandler(myTruck.createOrder.bind(myTruck));意思是myTruck.createOrder的所有者绑定为 myTruck, 然后再把这个函数传给formHandler.addSubmitHandler。*/
    //使用call与使用bind设置this值的方法类似。两者的区别是, bind返回一个新版本的函数或者方法, 但并不会立即执行它;而call实际上会调用返回的函数或者方法, 并允许将this设置为传入的第一个参数。(如果需要将其他的参数也传递到函数中, 只需要将额外参数添加到参数列表中即可。) call会运行函数体,并返回函数的返回值。
    formHandler.addSubmitHandler(function (data) {//我们只希望在Ajax请求成功的时候才这么做;换言之,我们只想在Deferred 状态变为fulfilled的时候才如此执行。那怎么知道Deferred何时会变为fulfilled呢? addSubmitHandler的函数实参会返回一个Deferred, 你可以在addSubmitHandler内部的Deferred后添加一个.then。
        return myTruck.createOrder.call(myTruck, data) //回调函数中添加return关键字返回Deferred。
            .then(function () {
                checkList.addRow.call(checkList, data);
            });//更新formHandler.addSubmitHandler的回调函数。在createOrder后添加.then。传入一个执行checklist.addRow的回调函数。
    });
    formHandler.addInputHandler(Validation.isCompanyEmail);
    myTruck.printOrders(checkList.addRow.bind(checkList)); //执行printOrders, 然后传入checklist.addRow。记住,要确保addRow绑定在Checklist的实例上。
    webshim.polyfill('forms forms-ext');
    webshim.setOptions('forms', {addValidators: true, lazyCustomMessages: true});
})(window);

formhandler.js

(function (window) { /*FormHandler将使用IIFE封装代码,并将一个构造函数附加到window.App属性*/
    'use strict';
    var App = window.App || {}; //window.App不存在,将一个空对象字面值赋值给它。声明一个FormHandler构造函数,并将其导出到window.App属性。
    var $ = window.jQuery;//FormHandler将会像App一样导入jQuery,这样做是为了指明模块是在使用在别处定义的代码。这是有助于团队成员互相协调和日后维护的最佳做法。
    function FormHandler(selector) { //FormHandler模块应该可以与任何一个<form>元素配合使用。为了实现这一点,可以给FormHandler构造函数传递一selector,而这个selector匹配index.html中<form>元素。
        if(!selector) { //如果未传入selector, 则抛出Error。
            throw new Error('No selector provided');//Error是一种内置类型,可以明确地表示代码中出现了意外的值或条件。但目前你使用的Error实例只是在控制台打印出消息。
        }
​
        this.$formElement = $(selector); //带$前缀的变量表示这个变量是通过jQuery选择出来的元素。常见书写惯例。
        /*确保元素选择器成功地从DOM检索到了一个元素。*/
        if (this.$formElement.length === 0){ //如果未查找到任何元素,jQuery将会返回空——如果选择器没有匹配到任何元素,它不会抛出一个异常。因此需要手动检查一下,因为FormHandler没有元素是不能工作的。jQuery封装集合的长度可以告诉我们有多少个匹配元素。
            throw new Error('Could not find element with selector: ' + selector);
        }
    }
​
    FormHandler.prototype.addSubmitHandler = function (fn) { //函数参数fn目的:对象传递到Truck实例的createOrder方法。
        console.log('Setting submit handler for form');
        this.$formElement.on('submit', function (event) { //on方法接受一个事件名称 并在事件被触发时执行回调。回调函数应该接受该事件的事件对象。
           event.preventDefault(); //调用event.preventDefault是为了确保用户提交表单时不会离开CoffeeRun页面。
            var data = {};
            $(this).serializeArray().forEach(function (item) {//在提交处理程序的回调中, this对象是对form元素的引用。jQuery提供了一个便捷的方法 (serializeArray)从表单获取值。为了使用serializeArray, 需要使用jQuery包装表单。调用$(this)会返回一个包装对象,这个包装对象可以使用serializeArray方法。
                data[item.name] = item.value;
                console.log(item.name + ' is ' + item.value); //使用相应对象的name和value在data上创建一个新属性。
            });
            console.log(data);
            fn(data).then(function (){//在formhandler.js的提交处理程序回调中调用fn, 并把包含用户输入的数据对象传递给fn。
                this.reset();//如果提交表单之后, 旧数据能够被清除,那么用户便可以立即输入下一个订单
                this.elements[0].focus();//可以通过表单的elements属性轻松获得各个表单字段。elements是表单字段数组,可以从0开始索引并引用。为了让表单的特定字段被聚焦, 可以调用它的focus方法。(给咖啡订单添加的autofocus属性只会在网页首次加载时有效。)
            }.bind(this));// 因为现在匿名函数会返回一个Deferred,所以可以在其后添加一个.then。使用.then注册一个回调函数用于重置表单并聚焦到第一个元素。当使用.then注册回调函数时,它会有一个新的作用域,因此需要在匿名函数上使用.bind为FormHandler实例设置它的this的值。
        });
    };
    //因此只能在用户输入的时候执行校验程序,添加addinputHandler原型方法,它会在表单上绑定input件监听器。和addSubmitHandler一样, 它接受一个函数实参
    FormHandler.prototype.addInputHandler = function (fn) {
        console.log('Setting input handler for form');
        //使用jQuery的on方法绑定input事件的监听器, 确保使用事件委托模式过滤掉了除[name="emailAddress"]字段外的其他字段触发的事件。
        this.$formElement.on('input', '[name="emailAddress"]', function (event) {
            //在事件处理程序中, 从event.target对象中提取出邮箱地址字段的值。 接着执行addinputHandler的函数参数fn,并且将邮箱地址作为参数传入。
            var emailAddress = event.target.value;
            var message = '';
            //如果fn(emailAddress)返回true, 清除该字段的有效性提示, 否则将警告信息分配给 message变量,并将message设为有效性提示
            //Safari用户注意,webshim需要留意-在使用setCustomValidity的时候,必须使用jQuery包裹对象。需要将event.target对象包裹起来。
            if (fn(emailAddress)) {
                $(event.target).setCustomValidity('');
            }
            else {
                message = emailAddress + ' is not an authorized email address!后缀应该要@bignerdranch.com';
                $(event.target).setCustomValidity(message);
            }
        })
    }
​
    App.FormHandler = FormHandler;
    window.App = App;
})(window);

checklist.js

(function (window){
    'use strict';
    /*导入App命名空间和jQuery, 并将它们赋值给一个局部变量*/
    var App = window.App || {};
    var $ = window.jQuery;
    /*接下来为Checklist创建一个构造函数, 记住要为构造函数传递一个selector参数,并且这个selector参数要至少能够匹配DOM中的一个元素。*/
    function CheckList(selector) {
        if (!selector) {
            throw new Error('No selector provided');
        }
        this.$element = $(selector);
        if (this.$element.length === 0) {
            throw new Error('Could not find element with selector: ' + selector);
        }
    }
    /*向Checklist添加一个名为addClickHandler原型方法,该方法的工作方式与FormHandler 的addSubmitHandler相同。换言之, 它将进行以下操作。(1)接受一个函数参数。(2)注册事件处理程序回调。(3)在事件处理程序回调中调用第一步中的函数参数。*/
    CheckList.prototype.addClickHandler = function (fn) {
        this.$element.on('click', 'input', function (event) {//使用this.$element.on注册回调事件处理程序时,要把click作为事件名称,但同时也要传入一个过滤选择器作为第二个参数。过滤选择器告知事件处理程序当且仅当事件是由<input>元素触发时才执行回调函数。这种模式被称为事件委托模式,它的工作原理是因为click和keypress等事件都会通过 DOM传播,这就意味着它们的祖先元素也会接收到事件。
            var email = event.target.value;
            // this.removeRow(email);
            fn(email).then(function () { //我们也只希望在Truck.prototype.deliverOrder成功时才从清单中删除选项。因此在此添加.then。
                this.removeRow(email);
            }.bind(this)); //记住,要通过.bind为匿名函数绑定。
        }.bind(this));//不同的地方是,它将监听一个点击事件并将回调绑定到Checklist实例上。
    };
    //在这个新方法中,调用Row构造函数,并向其传入coffeeOrder参数,从而创建一个新的Row实例;再把新创建的实例赋值给变扯rowElement; 然后, 将rowElement的$element属性(它包含了DOM子树)附加到Checklist实例的$element属性(它是对清单项容器的引用)上。
    CheckList.prototype.addRow = function (coffeeOrder) {
        //移除匹配相应邮箱地址的已有行。每个客户只能拥有一个公开订单。因为我们使用简单的键值对存储数据,所以同 一邮箱地址对应订单中的后者会覆盖前者。
        this.removeRow(coffeeOrder.emailAddress);
        //使用咖啡订单信息创建一个新的Row实例
        var rowElement = new Row(coffeeOrder);
        //把新的Row实例的$element属性添加到清单中
        this.$element.append(rowElement.$element);
    }
    //在checklist.js中添加removeRow方法,并指定一个emailAddress参数。使用实例的$element属性来检索它的所有后代元素, 其中后代元素的value属性与邮箱参数要相匹配。接着,对检索到的元素调用closest方法,检索该元素的data-coffee-order属性为"checkbox"的祖先。最后,对该祖先执行remove方法。(代码中有些新语法,之后将对其进行解释。)
    CheckList.prototype.removeRow = function (email) {
        this.$element
            .find('[value="' + email + '"]')
            .closest('[data-coffee-order="checkbox"]')
            .remove();//请注意,this.$element.find只是在一个范围内查找,而不是搜索整个DOM——它只会搜索this.$element的后代元素。
    };
​
    /*Checklist模块需要3个方法来完成其工作:第1个负责创建一个清单项, 这个清单项包括了复选框和描述文本。可以将清单项视为table中的一行。第2个方法会从table中移除一行。第3个方法会为单击事件添加一个监听器,从而让代码知道何时需要移除一行。*/
    function Row(coffeeOrder) { //Row构造函数创建所有用于表示单个咖啡订单的DOM元素,包括复选框和描述文本,可以使用它构建带复选框的DOM子树来表示每个咖啡订单。不导出到App命名空间,它只会被Checklist.prototype内部的方法使用。coffeeOrder的参数:与传递给Truck.prototype.createOrder的数据是相同的。
        var $div = $('<div></div>', {
            'data-coffee-order' : 'checkbox',
            'class': 'checkbox' //'class'在单引号中是因为class是一个JavaScript保留字,因此需要使用单引号来防止浏览器以JavaScript形式对其进行解析(如果不使用单引号也会导致语法错误)。
        });//第一个参数为DOM元素的HTML标签。第二个参数为Query应该添加到<div>上的属性对象,对象的键值对会被转化为新元素的属性。它返回的结果是一个由jQuery创建的DOM元素, 我们将它赋值给一个名为$div的新变量。这并不是一个实例变量(也就是说,它只是$div而不是this.$div)
        var $label = $('<label></label>'); //不使用对象参数,因为它不需要额外的属性。
        var $checkbox = $('<input></input>', {
            type: 'checkbox',
            value: coffeeOrder.emailAddress
        });//调用$函数并为其传递一个<input>HTML标签,从而为复选框创建一个<input>元素。将第二个参数的type指定为checkbox, value指定为客户的邮箱地址。因为这些属性名称都没有使用特殊字符,所以不必将它们放在单引号之中。
        //复选框旁边显示的描述文本
        var description = coffeeOrder.size + ' ';
        if (coffeeOrder.flavor) {
            description += coffeeOrder.flavor + ' ';
        }
        description += coffeeOrder.coffee + ', ';
        description += ' (' + coffeeOrder.emailAddress + ')';
        description += ' [' + coffeeOrder.strength + 'x]';
        /*(1)把$checkbox追加到$label中。(2)把description追加到$label中。(3)把$label追加到$div中。在checklist.js中使用jQuery的append方法将元素连接在一起。此方法接受DOM元素或者jQuery封装集合作为参数,将其添加为调用者的子元素。*/
        $label.append($checkbox);
        $label.append(description);
        $div.append($label);
        //构造函数永远不应该有return语句。(当对构造函数使用new时,JavaScript会自动返回一个值。)但是可以把子树赋值给this.$element, 并将其当作实例的属性来访问。(选择此名称只是为了遵循其他构造函数的约定,它本身没有任何特殊的含义。)
        this.$element = $div;
    }
​
    /*在 IIFE的最后,将Checklist构造函数导出为App命名空间的一部分。*/
    App.CheckList = CheckList;
    window.App = App;
})(window);

truck.js

(function (window) {
    'use strict';
    var App = window.App || {};
​
    function Truck(truckId, db) { //Truck模块,负责提供用千管理美食车的功能,比如创建、交付订单,打印等待中的订单列表
        this.truckId = truckId;
        this.db = db;
    }
​
    /*Truck现在会将RemoteDataStore生成的Deferred返回。在使用Promise和 Deferred时,最好的做法就是将它们返回, 这能让调用createO rder或者deliverOrder的对象所注册的回调函数在异步操作完成后被调用。*/
    /*在createOrder函数中输出消息到控制台,然后使用db属性的add方法来存储订单信息。*/
    Truck.prototype.createOrder = function (order) {
        console.log('Adding order for ' + order.emailAddress);
        //无须在这个文件里指定App.DataStore的命名空间或者在模块内提起DataStore的构造函数。只要一个对象拥有和Datastore相同的方法,它就可以被Truck使用,而Truck并不关心它们是怎么实现的。
        return this.db.add(order.emailAddress, order); //既然RemoteDataStore的方法会返回Deferred, 我们就也需要更新Truck方法。首先,处理createOrder和deliverOrder。
    };
​
    /*Truck从数据库中删除相应的订单*/
    Truck.prototype.deliverOrder = function (customerId) {
        console.log('Delivering order for ' + customerId);
        //deliverOrder也只调用this.db的remove方法,而无须知道remove方法如何运行。
        return this.db.remove(customerId); //既然RemoteDataStore的方法会返回Deferred, 我们就也需要更新Truck方法。首先,处理createOrder和deliverOrder。
    };
​
    /*接受一个由客户邮箱地址组成的数组,遍历数组,然后使用console.log打印出订单信息。*/
    Truck.prototype.printOrders = function (printFn) { //printOrders应该接受一个可选的函数实参, 所以我们需要判断到底是否有函数被传入。如果有,则执行。在执行的时候,传入目前的所有订单orders[id]。
        return this.db.getAll() //将代码更新一下,返回this.db.getAll,调用this.db.getAll来获取所有订单的键值对
            .then(function (orders) { //在其后调用.then。在.then中传入一个匿名函数,并且用.bind进行this绑定。
                /*我们的匿名函数期望接受包含服务器返回的所有咖啡订单信息的对象。抽取对象中的所有键名,并且将它们分配给变拭customerIdArray。*/
                var customerIdArray = Object.keys(orders); //然后将orders传入Object.keys, 从而获得一个仅包含键名的数组,再将这个数组赋值给变量customerldArray。
                console.log('Truck #' + this.truckId + ' has pending orders:');
                customerIdArray.forEach(function (id) { //在遍历这个数组时,将一个回调函数传给forEach。在回调函数内,尝试通过一个过(客户的邮箱地址)来获取订单信息。
                    console.log(orders[id]); //同样,改变console.log语句,让它不再调用this.db.get(id), 而是使用包含所有咖啡订单信息的orders对象。我们不应该在打印每个订单的时候都进行Ajax请求。
                    if (printFn) {
                        printFn(orders[id]);
                    }
                }.bind(this));//在forEach的回调函数外,this指代Truck实例。在forEach括号内部的匿名函数后马上调用.bind(this)方法,从而将修改过的匿名函数传给forEach。这个修改过的函数的所有者是Truck实例。
                // 最后遍历这个数组,为数组里的每个函数调用一次回调函数。
            }.bind(this));
​
    };
​
    App.Truck = Truck;
    window.App = App;
​
})(window);

datastore.js

(function (window) { //用基本的IIFE模式编写模块,我们可以利用函数作用域为大段代码创建命名空间
    'use strict';
    var App = window.App || {}; //本地变量App。如果在window上存在App属性,那么就将它赋值到本地App中;否则引用一个用{}表示的新的空对象
    var Promise = window.Promise; //创建一个Promise变量,然后赋予它window.Promise。尽管这不是必须的, 但将全局空间里所需的部分引入到我们自己的模块中是一个挺好的做法。
​
    /*最重要的模块DataStore,它可以存储数据、根据查询需求提供相应的数据,并且按照指令删除不必要的数据。*/
    function DataStore() { //首字母大写,这是JavaScript中约定的构造函数的命名方式
        this.data = {}; //创建和定制一个新的对象
    }
​
    /*创建一个名为promiseResolvedWith的辅助函数来生成一个Promise更方便。*/
    /*promiseResolvedWith是我们编写add方法时所使用的Promise代码的可复用结构。它接受一个名为value的参数,然后创建一个名为promise的变量,并且分配一个Promise实例。它将一个带有resolve和reject两个形参的匿名函数传入Promise的构造函数里。在匿名函数内,执行resolve并将value值传入。不需要在promiseResolvedW江h中将函数实参的this进行绑定, 因为在其内部并没有引用到this。将其余方法更新为使用promiseResolvedWith。*/
    function promiseResolvedWith(value) {
        /*创建一个promise变量,并分配一个Promise实例。确保在最后返回这个实例。*/
        var promise = new Promise(function(resolve, reject) {//当Promise运行的时候,它会执行匿名函数并传入两个值: resolve和reject。执行resolve函数会将Promise的状态变为fulfilled, 而执行reject函数会将其状态变为rejected。
            resolve(value);
        });
        return promise;
    }
​
    DataStore.prototype.add = function (key, val) { //所有通过构造函数出创建的实例都可以访问其属性和方法的共享仓库:构造函数的prototype属性。创建实例使用new关键字调用构造函数。new关键字不仅仅创建并返回实例,还在实例和构造函数的prototype属性间创建了一个特别的链接
        this.data[key] = val;
        return promiseResolvedWith(null); //为什么使用null作为实参呢?往DataStore添加值并不会产生一个值,所以不需要返回数值。当不需要返回数值时,应该使用null明确表示。(也可以使用resolve(val)让接下来的函数可以获取最新存储的数值, 但这对于CoffeeRun没有必要,因此在本例中就不引入了。)
    }
​
    /*在get和getAll中将先前版本的值传入promiseResolvedWith,而在remove方法中传入null。*/
    DataStore.prototype.get = function (key) { //该方法接受参数key, 并在相应实例的data属性中查找键值
        return promiseResolvedWith(this.data[key]);
    }
​
    DataStore.prototype.getAll = function() { //但getall会直接返回data属性的的引用
        return promiseResolvedWith(this.data);
    }
​
    DataStore.prototype.remove = function (key) { //当调用remove方法时,delete运算符会将一个键值对从对象上删除
        delete this.data[key];
        return promiseResolvedWith(null);
    }
​
    App.DataStore = DataStore; //将DataStore绑定到App对象上
    window.App = App; //将新修改的App赋值到全局App属性上
})(window);

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Web开发权威指南笔记(二) 的相关文章

随机推荐

  • pytorch安装报错:ERROR: torch has an invalid wheel, .dist-info directory not found

    在windows10 anaconda创建虚拟环境后 安装pytorch 运行pip install r requirement txt安装torch时报错 ERROR torch has an invalid wheel dist inf
  • NLP预训练模型系列-GPT-2

    NLP预训练模型 系列文章目录 1 BERT 2 GPT 3 GPT 2 4 GPT 3 目录 NLP预训练模型系列文章目录 前言 1 Abstract 2 Introduction 3 Approach 3 1 Training Data
  • 科大奥锐密立根油滴实验数据_密立根油滴实验数据处理

    刘秋君 回答于 2017 03 05 密立根油滴实验报告 实验题目 密立根油滴实验 电子电荷的测量 实验目的 1 通过对带电油滴在重力场和静电场中运动的测量 验证电荷的不连续性 并测定电子电荷的电荷值e 2 通过实验过程中 对仪器的调整 油
  • Python Wind量化API

    文章目录 导入 代码生成器 各个函数 wds 日期序列函数 wss多维数据函数 wset数据集函数 wsee wses swq 实时行情数据 wsi 获取分钟级别数据 wst 日内 edb 宏观数据 日期函数 tdays ddaysoffs
  • 【page分页工具类】贼好用的分页工具类

    PageUtils工具类如下 package utils import java io Serializable import java util HashMap import java util List import java util
  • QT学习笔记(四)信号槽与简单的自定义信号

    1 坐标系 左上角为零点 x向右为正方向 y向下为正方形 2 信号 完成连接connect的过程包括以下内容 信号的发送 信号发送的具体内容 信号的接受 信号的处理 称为槽函数 3 信号槽 信号槽的优点 松散耦合 信号的发送方和接受方本身没
  • 使用navicat为数据表添加外键

    1 选择需要操作的表 打开设计表 点击外键 2 名 自动生成 无需添加 字段 选择需要添加外键的字段 参考模式 选择表所在的数据库 参考表 关联表名 参考字段 关联表的关联字段 删除时 当删除关联表时 set null该字段置空 更新时 当
  • 【子比主题】添加今日实时热搜榜单教程

    预览图 演示地址 实时热榜 淇云博客 专注于IT技术分享 使用教程 首页版 将下载文件中的 index php 里的内容复制到 wp content themes zibll index php 里你想要放置的地方 Tips 不止index
  • 我爱说英语之学美语发音

    开篇 在写这篇文章之前 我考虑了很久 思前想后 还是决定把她写成一个系列的文章 用以来见证我们学习英语的历程 同时也为了说明我要学好英语的决心 废话不多说 进入正题 回忆 对于我们这些已经毕业很久的人来说 不知道还算不算有英语基础 最起码在
  • 狂野飙车9服务器维护中,狂野飙车9传奇进不去怎么办

    狂野飙车9 国际服IOS进入办法 苹果端的小伙伴如果想要进入 狂野飙车9 的国际服应该怎么办呢 下面 就跟随玩游戏网的小编一起来看一下具体的进入办法吧 感兴趣的小伙伴可千万不要错过哦 进入方法 第一步 首先我们需要推出AppStore账号
  • 魔方机器人之结构篇

    魔方颜色识别和魔方复原算法以及串口通信都解决完了 感觉自己该松口气了吧 结构可以反正仿照别人的来嘛 做出来就的了 事实又打了我一耳光 我怎么发现我的预判总是那么的不靠谱 总结就是自己没做过的东西再也不要说很简单了 即使看上去简单的再也不能简
  • 5-8 以特殊方式跟管理员打招呼

    创建一个至少包含5个用户名的列表 且其中一个用户名为 admin 想象你要编写代码 在每位用户登录网站后都打印一条问候消息 遍历用户名列表 并向每位用户打印一条问候消息 如果用户名为 admin 就打印一条特殊的问候消息 如 Hello a
  • 使用EasyExcel实现导入导出功能

    使用EasyExcel实现导入导出功能 一 导出 1 使用ideal新建一个maven项目 并在pom xml文件中引入EasyExcel依赖
  • impala 错误

    问题一 impala state store unrecognized service 原因 当前节点未成功安装impala server impala state store impala catalog 解决方案 yum install
  • ulua源码分析

    对于NestClass的Type 用了2次被Cache了两次 主要是因为PushType这个函数 对每个Type对象 不进行Cache检测 总是push一个新的proxy对象
  • 蓝桥杯省赛2021 括号序列 python

    给定一个括号序列 要求尽可能少地添加若干括号使得括号序列变得合法 当添加完成后 会产生不同的添加结果 请问有多少种本质不同的添加结果 两个结果是本质不同的是指存在某个位置一个结果是左括号 而另一个是右括号 例如 对于括号序列 只需要添加两个
  • MQTT:用Mosquitto搭建轻量级的设备接入网关

    开发部署在云端的设备接入网关服务就不得不提到MQTT 使用MQTT不论是从设备到设备 还是设备到云端服务的双向通讯 都可以获得较好的支持 MQTT的起源和我的理解 用tcpdump分析下MQTT的通讯时序 这里基于mosquitto 以一组
  • One PUNCH Man——半监督学习

    文章目录 半监督学习介绍 半监督SVM 基于分歧的方法 半监督学习介绍 我们在丰收的季节来到瓜田 满地都是西瓜 瓜农抱来四个西瓜说这都是好瓜 然后指着地里面六个瓜说这些不好 还需要再生长几天 基于这些信息 我们能否构建一个模型 用于判别地里
  • 优雅的玩转Fast-DDS

    优雅的玩转Fast DDS 安装依赖 sudo apt install cmake g python3 pip wget git pip3 install U colcon common extensions vcstool Fast DD
  • Web开发权威指南笔记(二)

    书 Web开发权威指南 美 Chris Aquino Todd Gandee著 为2nd实战项目CoffeeRun练习以及代码整理 全为个人借鉴本书产出 若需要转载请联系通知我 请尊重原创 谢谢 整理了大概5天了 内容比较多 很多重点都整理