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

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

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

最终成果展示

第十五章 Node.js入门

  • Node.js是一个开源项目, 能够让JavaScript代码在浏览器之外运行。

    • 浏览器端的JavaScript代码可以访问document和window这样的全局对象以及其他API和库函数;

    • Node环境下的JavaScript代码则能够访间硬盘驱动器、数据库和网络(如图15-1所示)。

  • 使用Node能够创建各种应用, 下至命令行工具, 上至Web服务器。

  • Chattrbox由两部分组成: Node.js服务器和浏览器端的JavaScript应用。

Node和npm

Node.js (nodejs.org):若要安装Node, 需要从nodejs.org下载安装包。

Node.js可以使用node和npm这两个命令行程序。

  • npm 可用于安装开源工具,如browser-sync;

  • 而node则负责运行JavaScrip的程序。

npm命令行工具能执行各种任务,如安装项目依赖、管理项目工作流和外部依赖。

  • 使用npm init创建package.json文件。

  • 使用npm install --save添加第三方模块。

  • 运行保存在package.json的scripts中的常用命令。

延展阅读: npm 模块注册

npm (npmjs.com)有大量可用的包能通过npm安装。在专门的模块注册网站,此网上就能搜索和浏览这些包。

Creating Node.js modules | npm Docs:想自己创建模块给别人使用,可参考此文档。

实现步骤

npm init

在项目文件夹下创建chattrbox目录。打开终端进入该目录,运行npm init创建package.json。 npm会询问项目相关信息,同时也会提供默认值。 目前使用默认值就够了,按回车键确认即可。

在package.json中有一个"scripts"字段该字段用于存储开发过程中可能会多次用到的命令。

构建Chattrbox时,可以在package.json的"scripts"字段添加命令, 以提高开发效率。 首先,添加一个"start"脚本这是我们创建的第一个npm工作流脚本(别忘了在"test"一行的末尾添加一个逗号):

...
"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "start": "node index.js"
},
...

Hello, World

index.js

var http = require('http');//使用Node内置的require函数访问Node中的http模块,该模块提供了很多用来处理HTTP请求和响应的工具,如http.createServer函数。
​
var server = http.createServer(function (req, res) { //http.createServer接受一个函数作为唯一的参数,每次HTTP请求时都会调用该函数(参数)。 这种方式很像浏览器中的事件回调模式,只不过它是服务器端事件(接受一个HTTP请求)触发的回调。在Node中通常使用req和res作为HTTP请求和响应对象的变量名。
    console.log('Responding to a request.');//回调函数在控制台打印了一条消息
    res.end('<h1>Hello, World</h1>');//在响应中写入一些HTML文本。
});
server.listen(3000);//最后,使用server.listen让服务器监听3000端口,这一过程通常叫作 “端口绑定”。保存文件。

控制台:输入npm start

E:\2021大三上\Web前端开发\作业\html-picture\Chattrbox>npm start
​
> chattrbox@1.0.0 start E:\2021大三上\Web前端开发\作业\html-picture\Chattrbox
> node index.js

浏览器:打开localhost:3000

控制台:显示Responding to a request.

E:\2021大三上\Web前端开发\作业\html-picture\Chattrbox>npm start
​
> chattrbox@1.0.0 start E:\2021大三上\Web前端开发\作业\html-picture\Chattrbox
> node index.js
​
Responding to a request.

添加一个npm脚本

可以通过编程的方式实现自动重启服务(名为nodemon的模块)。尽早在工作流里加入nodemon可以让编程体验更加流畅。

在终端停止程序并运行以下命令来安装nodemon模块:

npm install --save-dev nodemon

接着会出现如下几行提示,这是npm在提示package.json文件里有些空字段。不必惊慌, 明白npm对细节十分严格就好。

npm WARN chattrbox@1.0.0 No description
npm WARN chattrbox@1.0.0 No repository field.

--save-dev选项:

  • 告诉npm维护一份列表,记录应用依赖的所有第三方模块。

  • 这个列表存储在package.json文件中。如有必要, 运行npm install命令(不带参数)即可安装列表里的全部依赖。

  • 如此一来, 共享代码时就不必包含第三方模块了。

  • 打开package.json文件, 可以看到npm创建了"devDependencies"字段,其中有一条nodemon 相关信息。

···
"author": "",
"license": "ISC",
"devDependencies": {
    "nodemon": "^2.0.15"
}

更新package.json, 向"scripts"字段中添加一条新的命令:

···
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js",
    "dev": "nodemon index.js"
},
···

在终端运行npm run dev, 重启node程序。注意, 现在这条命令不是简单的npm dev了, 它和npm start有所不同——因为npm会假定这种命令(比如start)一定存在, 而自定义的npm脚本则需要显式地强调你想要run这些脚本。

接下来你会看到nodemon已经在管理node程序了。

 在index.js中将 "Hello, World" 修改为 "Hello, World!!", 保存修改。 nodemon会发现并自动重启 node程序。

用文件提供服务

在chattrbox项目目录下新建app文件夹,并在其中创建index.html文件,写入以下内容:

index.html

 index.js

var http = require('http');//使用Node内置的require函数访问Node中的http模块,该模块提供了很多用来处理HTTP请求和响应的工具,如http.createServer函数。
var fs = require('fs');
var server = http.createServer(function (req, res) { //http.createServer接受一个函数作为唯一的参数,每次HTTP请求时都会调用该函数(参数)。 这种方式很像浏览器中的事件回调模式,只不过它是服务器端事件(接受一个HTTP请求)触发的回调。在Node中通常使用req和res作为HTTP请求和响应对象的变量名。
    console.log('Responding to a request.');//回调函数在控制台打印了一条消息
    fs.readFile('app/index.html', function (err, data) { //readFile方法接受一个文件名和一个回调函数作为参数。注意,回调函数会在接受文件内容的同时接受一个err参数。这是Node.js的编程惯例。
        res.end(data);//在响应中写入一些HTML文本。
    });
});
server.listen(3000);//最后,使用server.listen让服务器监听3000端口,这一过程通常叫作 “端口绑定”。保存文件。

nodemon会重启程序,因此直接打开浏览器并刷新页面即可。可以在浏览器中看到index.html 文件中的内容:

将文件名打印到了控制台

index.js

var http = require('http');//使用Node内置的require函数访问Node中的http模块,该模块提供了很多用来处理HTTP请求和响应的工具,如http.createServer函数。
var fs = require('fs');
var server = http.createServer(function (req, res) { //http.createServer接受一个函数作为唯一的参数,每次HTTP请求时都会调用该函数(参数)。 这种方式很像浏览器中的事件回调模式,只不过它是服务器端事件(接受一个HTTP请求)触发的回调。在Node中通常使用req和res作为HTTP请求和响应对象的变量名。
    console.log('Responding to a request.');//回调函数在控制台打印了一条消息
    var url = req.url; //从请求对象的url属性可以看出浏览器请求的到底是默认页面(index.html)还是别的文件。
​
    var fileName = 'index.html';
    if (url.length > 1) { //假如是其他文件,调用url.substring (1)去掉首字符, 也就是'/'。
        fileName = url.substring(1);
    }
    console.log(fileName);
    fs.readFile('app/index.html', function (err, data) { //readFile方法接受一个文件名和一个回调函数作为参数。注意,回调函数会在接受文件内容的同时接受一个err参数。这是Node.js的编程惯例。
        res.end(data);//在响应中写入一些HTML文本。
    });
});
server.listen(3000);//最后,使用server.listen让服务器监听3000端口,这一过程通常叫作 “端口绑定”。保存文件。

nodemon重启程序之后,在浏览器访问http://localhost:3000/woohoo或包括默认路径'/'在内的其他路径。(浏览器自动请求favicon.ico文件,在终端也可以看到打印出的相应请求。)

使用path模块

  • 刚才将fileName传给了fs.readFile, 但最好使用path模块

    • 它提供了可以处理和转换文件路径的公用函数。

    • 简单而重要的原因是,有些操作系统使用斜杠,而有些 操作系统使用反斜杠, path模块会帮忙处理这些差异。

index.js

var http = require('http');//使用Node内置的require函数访问Node中的http模块,该模块提供了很多用来处理HTTP请求和响应的工具,如http.createServer函数。
var fs = require('fs');//引入Node.js文件系统模块fs
var path = require('path');
​
var server = http.createServer(function (req, res) { //http.createServer接受一个函数作为唯一的参数,每次HTTP请求时都会调用该函数(参数)。 这种方式很像浏览器中的事件回调模式,只不过它是服务器端事件(接受一个HTTP请求)触发的回调。在Node中通常使用req和res作为HTTP请求和响应对象的变量名。
    console.log('Responding to a request.');//回调函数在控制台打印了一条消息
    var url = req.url; //从请求对象的url属性可以看出浏览器请求的到底是默认页面(index.html)还是别的文件。
​
    var fileName = 'index.html';
    if (url.length > 1) { //假如是其他文件,调用url.substring (1)去掉首字符, 也就是'/'。
        fileName = url.substring(1);
    }
    console.log(fileName);
    var filePath = path.resolve(__dirname, 'app', fileName);
    fs.readFile(filePath, function (err, data) { //readFile方法接受一个文件名和一个回调函数作为参数。注意,回调函数会在接受文件内容的同时接受一个err参数。这是Node.js的编程惯例。
        res.end(data);//在响应中写入一些HTML文本。
    });
});
server.listen(3000);//最后,使用server.listen让服务器监听3000端口,这一过程通常叫作 “端口绑定”。保存文件。

在浏览器里输入几个测试的文件路径, 确保程序的功能没变。

默认路径应该返回index.html, 不存在的路径(比如/woohoo/')应该不显示任何东西并且打印出文件名。

http://localhost:3000/woohoo/

localhost:3000/test.html

创建和使用自定义模块

extract.js

var path = require('path');
​
var extractFilePath = function (url) {
    var filePath;
    var fileName = 'index.html';
​
    if (url.length > 1) {  //假如是其他文件,调用url.substring (1)去掉首字符, 也就是'/'。
        fileName = url.substring(1);
    }
    console.log('The fileName is: ' + fileName);
​
    filePath = path.resolve(__dirname, 'app', fileName);
    return filePath;
}
​
module.exports = extractFilePath;让extractFilePath函数能被其他模块用require引入。 要实现这一点,需将extractFilePath 赋值给名为module.exports的全局变量。这是一个由Node提供的特殊变量,赋给它的值都能被其他模块引入,别的变量和函数则不能被其他模块访问。此行告诉Node, 当通过调用require('./extract')引入extract模块时,返回值是extractFilePath函数。

index.js

var path = require('path');
​
var extractFilePath = function (url) {
    var filePath;
    var fileName = 'index.html';
​
    if (url.length > 1) {  //假如是其他文件,调用url.substring (1)去掉首字符, 也就是'/'。
        fileName = url.substring(1);
    }
    console.log('The fileName is: ' + fileName);
​
    filePath = path.resolve(__dirname, 'app', fileName);
    return filePath;
}
​
module.exports = extractFilePath;让extractFilePath函数能被其他模块用require引入。 要实现这一点,需将extractFilePath 赋值给名为module.exports的全局变量。这是一个由Node提供的特殊变量,赋给它的值都能被其他模块引入,别的变量和函数则不能被其他模块访问。此行告诉Node, 当通过调用require('./extract')引入extract模块时,返回值是extractFilePath函数。

错误处理

var http = require('http');//使用Node内置的require函数访问Node中的http模块,该模块提供了很多用来处理HTTP请求和响应的工具,如http.createServer函数。
var fs = require('fs');//引入Node.js文件系统模块fs
var extract = require('./extract.js');
​
var handleError = function (err, res) {
    res.writeHead(404);
    res.end();
};
​
var server = http.createServer(function (req, res) { //http.createServer接受一个函数作为唯一的参数,每次HTTP请求时都会调用该函数(参数)。 这种方式很像浏览器中的事件回调模式,只不过它是服务器端事件(接受一个HTTP请求)触发的回调。在Node中通常使用req和res作为HTTP请求和响应对象的变量名。
    console.log('Responding to a request.');//回调函数在控制台打印了一条消息
    var filePath = extract(req.url);//从请求对象的url属性可以看出浏览器请求的到底是默认页面(index.html)还是别的文件。
    fs.readFile(filePath, function (err, data) { //readFile方法接受一个文件名和一个回调函数作为参数。注意,回调函数会在接受文件内容的同时接受一个err参数。这是Node.js的编程惯例。在JavaScript中,经常将回调函数传给API方法。 Node.js也是如此, 回调函数一般都把错误作为第一个参数。 因为把错误放在返回结果之前, 所以不管是否处理它, 至少能让人看到。
        if (err) {
            handleError(err, res); //检查是否有文件错误。如果有,则往响应中写入404错误码。
            return;
        } else {
            res.end(data);//在响应中写入一些HTML文本。
        }
    });
});
server.listen(3000);//最后,使用server.listen让服务器监听3000端口,这一过程通常叫作 “端口绑定”。保存文件。

 MIME类型

  • 计符机根据文件的扩展名(如.html或者pdf) 来推断文件类型。

  • 浏览器同样需要关联信息,这样它才能知道是将响应渲染成HTML,还是使用插件播放音乐,抑或是将文件下载到硬盘上。但HTTP响应没有这样的文件扩展名, 因此服务器必须在响应里告诉浏览器响应信息的类型。

  • 服务器通过在响应的Content-Type首部中指定MIME类型或者媒体类型来实现这一 目标。

Content-Type首部被设置为text/html, 即HTML的MIME类型。你也可以在项目里设置这样的首部信息。 在Chattrbox项目里可以把代码改成这样:

···
if (err) {
    handleError(err, res); //检查是否有文件错误。如果有,则往响应中写入404错误码。
    return;
} else {
    res.setHeader('Content-Type', 'text/html');
    res.end(data);//在响应中写入一些HTML文本。
}
···

互联网媒体类型 - 维基百科,自由的百科全书 (wikipedia.org):如需获取更多关于MIME类型的信息。

HTTP | Node.js v17.4.0 Documentation (nodejs.org):需获取在Node程序里设置首部的相关信息 。

代码展示

实现步骤的最终结果如下所示:

文档样式:

http://localhost:3000/test.html:

http://localhost:3000/woohoo:

http://localhost:3000

index.html

Hello, File!

test.html

Hola, Node!

extract.js

var path = require('path');
​
var extractFilePath = function (url) {
    var filePath;
    var fileName = 'index.html';
​
    if (url.length > 1) {  //假如是其他文件,调用url.substring (1)去掉首字符, 也就是'/'。
        fileName = url.substring(1);
    }
    console.log('The fileName is: ' + fileName);
​
    filePath = path.resolve(__dirname, 'app', fileName);
    return filePath;
}
​
module.exports = extractFilePath;让extractFilePath函数能被其他模块用require引入。 要实现这一点,需将extractFilePath 赋值给名为module.exports的全局变量。这是一个由Node提供的特殊变量,赋给它的值都能被其他模块引入,别的变量和函数则不能被其他模块访问。此行告诉Node, 当通过调用require('./extract')引入extract模块时,返回值是extractFilePath函数。

index.js:注意!新增contentType变量

var http = require('http');//使用Node内置的require函数访问Node中的http模块,该模块提供了很多用来处理HTTP请求和响应的工具,如http.createServer函数。
var fs = require('fs');//引入Node.js文件系统模块fs
var extract = require('./extract.js');
var wss = require('./websockets-server');
​
var handleError = function (err, res) {
    res.writeHead(404);
    res.end();
};
​
var server = http.createServer(function (req, res) { //http.createServer接受一个函数作为唯一的参数,每次HTTP请求时都会调用该函数(参数)。 这种方式很像浏览器中的事件回调模式,只不过它是服务器端事件(接受一个HTTP请求)触发的回调。在Node中通常使用req和res作为HTTP请求和响应对象的变量名。
    console.log('Responding to a request.');//回调函数在控制台打印了一条消息
    var filePath = extract(req.url);//从请求对象的url属性可以看出浏览器请求的到底是默认页面(index.html)还是别的文件。
    var contentType = filePath.includes('styles.css') ? 'text/css' : 'text/html';
    fs.readFile(filePath, function (err, data) { //readFile方法接受一个文件名和一个回调函数作为参数。注意,回调函数会在接受文件内容的同时接受一个err参数。这是Node.js的编程惯例。在JavaScript中,经常将回调函数传给API方法。 Node.js也是如此, 回调函数一般都把错误作为第一个参数。 因为把错误放在返回结果之前, 所以不管是否处理它, 至少能让人看到。
        if (err) {
            handleError(err, res); //检查是否有文件错误。如果有,则往响应中写入404错误码。
            return;
        } else {
            res.setHeader('Content-Type', contentType);
            res.end(data);//在响应中写入一些HTML文本。
        }
    });
});
server.listen(3000);//最后,使用server.listen让服务器监听3000端口,这一过程通常叫作 “端口绑定”。保存文件。

第十六章 使用WebSocket进行实时通信

  • 如果用常规的GET和POST请求, 每次与服务器进行数据交换时, 浏览器都需要发起新的请求并等待响应。

WebSocketAjax
WebSocket提供了HTTP之上的双向通信协议。它创建一个单独的连接,而且保持连接打开, 用来进行实时通信。Ajax请求虽然它不会导致页面重载,但是一样会产生网络流量, 生成和处理每个请求和响应都需要产生一些开销。
在Web应用中使用WebSocket不仅可以实现保存、加载远程数据,而且推送通知、 文档协作编辑、实时聊天等都只能算是入门级别的功能。WebSocket使得服务器能够处理物联网上承载的事物(如智能灯光、智能锁、智能汽车等)Ajax请求这种传统技术处理起如此密集的通信流揽的效率极低。

延展阅读: WebSocket库socket.io

  • WS模块是WebSocket的不错实现。但必须承认,它缺少一些方法。

    • WebSocke涟接有时候会掉线, 但是ws模块没有提供自动重连的方法。

    • WS只存在千Node.js的世界里, 只能在服务端使用。在客户端的JavaScript中,还得学习并使用另一个完全不同的库,哪怕它们实现的核心功能一模一样。

  • 另外,在客户端还有其他问题:假如浏览器版本很低, 不支持WebSocket怎么办?因此,还需要一个降级方案。

  • socket. io (socket.io)为这些问题提供了解决办法一它为浏览器提供了向后兼容的降级方案,包括一个Flash的实现方案。此外, 它还被移植到了很多其他平台,包括iOS和Android。

延展阅读: WebSocket服务

  • Firebase (google.com):假如你对提供实时平台的服务感兴趣, 可以试一下firebase 。如果说socket.io降低了编写服务端代码的难度, firebase则更进一步——它提供了一整套服务,包括让客户端共享和同步数据的机制。 firebase为Web、iOS和Android等平台均提供了解决方案。

高级挑战:聊天机器人

前面使用WebSocket.Server属性创建了聊天服务器。你也可以使用WebSocket作为构造函数, 用程序创建聊天客户端。

下面是示例代码:

var chatClient = new WebSocket('http://localhost:3001'); 

GitHub - websockets/ws: Simple to use, blazing fast and thoroughly tested WebSocket client and server for Node.js:在github.com/websockets/ws上的文档中有一个简单的例子, 可以发送和接收文本数据。

实现步骤

配置WebSocket

和http相似, 通过WebSocket,ws模块向Node.js程序提供了简单的通信方式。很多模块都实现了WebSocket, 但是WS是标准实现 , 性能很好。 首先在Chattrbox目录下安装WebSocket模块WS。(如果看见npm发出警告 , 提示项目缺失了描述或者仓库信息, 诮不要惊慌。)!!!!!注意:亲测8版本以上会报错!!!!!

npm install --save ws@7.3.0

websockets-server.js

var WebSocket = require('ws');//使用require声明导入ws模块。该模块包含了一个Server属性,使用该属性可以创建一个可用的WebSocket服务器。
var WebSocketServer = WebSocket.Server;
var port = 3001;
var ws = new WebSocketServer({//功能的核心代码运行这段代码会创建WebSocket服务器, 并绑定指定的端口号(这里是3001)。
    port: port
});
//跟extract.js中的模块不同,这里不需要module.exports赋值操作。引入websockets-server.js模块时,其中的代码就会运行。该模块会处理所有关千WebSocket的初始化和事件处理。
console.log('websockets server started');
​
//处理连接。 在websockets-server.js中 ,为 WebSocket服务器中所有的连接事件创建一个回调函数。事件处理语法与jQuery类似很多JavaScript库(包括Node、浏览器端)都使用了该模式。
ws.on('connection', function (socket) { //事件处理回调函数唯一接受的参数叫作socket。当一个客户端与WebSocket服务器建立连接时,就能通过这个socket对象获取这个连接。
    console.log('client connection established');
    /*在写聊天应用的服务端代码之前, 需要配置服务端程序, 使其能够重复任意接受到的消息。这种服务器通常叫做回声服务器。*/
    socket.on('message', function (data) { // 给客户端连接上产生的任意message事件注册一个回调函数, 将 “回声” 功能添加到 websockets-server.js中。
        //将事件处理程序直接注册到socket对象上。message事件回调函数会接受客户端发送的任何信息。现在,聊天程序只是在相同的socket连接上将收到的信息send回去。
        console.log('message received: ' + data);
        socket.send(data);
    });
​
})

index.js引入./websockets-server

var http = require('http');//使用Node内置的require函数访问Node中的http模块,该模块提供了很多用来处理HTTP请求和响应的工具,如http.createServer函数。
var fs = require('fs');//引入Node.js文件系统模块fs
var extract = require('./extract.js');
var wss = require('./websockets-server');
···

测试WebSocket服务器

  • 一个简单的测试方法是使用wscat模块。wscat工具可以用来连接WebSocket服务器并与之通信。

  • 该模块提供了命令行程序, 可以当作一个聊天应用的客户端。

  • 新打开一个终端窗口, 全局安装wscat。可能需要使用管理员权限来执行安装命令。

  • npm install -g wscat

在第二个终端窗口中运行wscat -c ws://localhost:3001, 随后在第二个终端窗口就能看到消息connected (press CTRL+C to quit), 在第一个终端窗口则能看到 'client connection established' 。 在第二个终端窗口的光标处输入一些文字。 每次输完文字按下回车键, 输入的文字都会被 Web Socket服务器返回。

创建聊天服务器功能

websockets-server.js

var WebSocket = require('ws');//使用require声明导入ws模块。该模块包含了一个Server属性,使用该属性可以创建一个可用的WebSocket服务器。
var WebSocketServer = WebSocket.Server;
var port = 3001;
var ws = new WebSocketServer({//功能的核心代码运行这段代码会创建WebSocket服务器, 并绑定指定的端口号(这里是3001)。
    port: port
});
var messages = []; //用来记录消息
​
//跟extract.js中的模块不同,这里不需要module.exports赋值操作。引入websockets-server.js模块时,其中的代码就会运行。该模块会处理所有关千WebSocket的初始化和事件处理。
console.log('websockets server started');
​
//处理连接。 在websockets-server.js中 ,为 WebSocket服务器中所有的连接事件创建一个回调函数。事件处理语法与jQuery类似很多JavaScript库(包括Node、浏览器端)都使用了该模式。
ws.on('connection', function (socket) { //事件处理回调函数唯一接受的参数叫作socket。当一个客户端与WebSocket服务器建立连接时,就能通过这个socket对象获取这个连接。
    console.log('client connection established');
​
    messages.forEach(function (msg) { //让新用户看到所有的历史消息。修改websockets-server.js中的连接事件处理程序,向每个新到达的连接发送所有的历史消息。当建立了一个连接之后,服务器遍历所有消息,把每条消息发送给新的连接。
        socket.send(msg);
    });
    /*在写聊天应用的服务端代码之前, 需要配置服务端程序, 使其能够重复任意接受到的消息。这种服务器通常叫做回声服务器。*/
    socket.on('message', function (data) { // 给客户端连接上产生的任意message事件注册一个回调函数, 将 “回声” 功能添加到 websockets-server.js中。
        //将事件处理程序直接注册到socket对象上。message事件回调函数会接受客户端发送的任何信息。现在,聊天程序只是在相同的socket连接上将收到的信息send回去。
        console.log('message received: ' + data);
        messages.push(data); //数组能够将聊天服务器收到的消息保存起来。
        ws.clients.forEach(function (clientSocket) { //ws对象用clients属性记录了所有的连接。该属性是一个数组,可以对其进行迭代。
            clientSocket.send(data); //在迭代的回调函数中,只需要send消息数据。最后,因为在迭代所有的socket连接时,已经将消息发送到了当前的socke咱;接,因此没必要 再调用socket.send(如ta) 了。将其删除,免得重复发送消息。
        });
    });
​
})

第一次聊天!

  • 现在来测试新功能。先确保nodemon巳经重新加载了代码。(如有必要, 可以按Control+ C 手动停止nodemon并输入npm run dev重启。)打开第三个终端窗口, 运行命令wscat -c localhost:3001。(需要一个终端窗口运行nodemon, 另外两个窗口运行wscat。)

  • 在连接到服务器的两个窗口中输入一些聊天消息。

  • 自己跟自己聊一会儿后, 打开第四个终端窗口, 运行wscat -c localhost:3001。这一个聊天客户端应该能收到所有的历史消息。

代码展示

图片展示如上方第一次聊天提及~

websockets-server.js

var WebSocket = require('ws');//使用require声明导入ws模块。该模块包含了一个Server属性,使用该属性可以创建一个可用的WebSocket服务器。
var WebSocketServer = WebSocket.Server;
var port = 3001;
var ws = new WebSocketServer({//功能的核心代码运行这段代码会创建WebSocket服务器, 并绑定指定的端口号(这里是3001)。
    port: port
});
var messages = []; //用来记录消息
​
//跟extract.js中的模块不同,这里不需要module.exports赋值操作。引入websockets-server.js模块时,其中的代码就会运行。该模块会处理所有关千WebSocket的初始化和事件处理。
console.log('websockets server started');
​
//处理连接。 在websockets-server.js中 ,为 WebSocket服务器中所有的连接事件创建一个回调函数。事件处理语法与jQuery类似很多JavaScript库(包括Node、浏览器端)都使用了该模式。
ws.on('connection', function (socket) { //事件处理回调函数唯一接受的参数叫作socket。当一个客户端与WebSocket服务器建立连接时,就能通过这个socket对象获取这个连接。
    console.log('client connection established');
​
    messages.forEach(function (msg) { //让新用户看到所有的历史消息。修改websockets-server.js中的连接事件处理程序,向每个新到达的连接发送所有的历史消息。当建立了一个连接之后,服务器遍历所有消息,把每条消息发送给新的连接。
        socket.send(msg);
    });
    /*在写聊天应用的服务端代码之前, 需要配置服务端程序, 使其能够重复任意接受到的消息。这种服务器通常叫做回声服务器。*/
    socket.on('message', function (data) { // 给客户端连接上产生的任意message事件注册一个回调函数, 将 “回声” 功能添加到 websockets-server.js中。
        //将事件处理程序直接注册到socket对象上。message事件回调函数会接受客户端发送的任何信息。现在,聊天程序只是在相同的socket连接上将收到的信息send回去。
        console.log('message received: ' + data);
        messages.push(data); //数组能够将聊天服务器收到的消息保存起来。
        ws.clients.forEach(function (clientSocket) { //ws对象用clients属性记录了所有的连接。该属性是一个数组,可以对其进行迭代。
            clientSocket.send(data); //在迭代的回调函数中,只需要send消息数据。最后,因为在迭代所有的socket连接时,已经将消息发送到了当前的socke咱;接,因此没必要 再调用socket.send(如ta) 了。将其删除,免得重复发送消息。
        });
    });
​
})

第十七章 借助Babel使用ES6

  • JavaScrip语言诞生千1994年,在1999年进行了一些更新,但是从1999年到2009年就一直没有更改过。

  • 在2009年引入了 系列较小的修改后, 我们所熟知的ES5版本, 或者说是标准第五版便诞生了。

  • 2015年,标准第六版增加了很多语言改进,其中许多新的语言特性受到了Ruby和Python等语 言的影响。

  • 严格来讲, 第六版被命名为ES2015, 但是更普遍的叫法是ES6。

  • ES6在谷歌的Chrome浏览器、Mozilla的火狐浏览器,以及微软的Edge浏览器上都得到了非常好的支持。这些都是长青的浏览器, 即它们会自动更新,无须用户手动下载或安装最新版本。

  • 随着谷歌、 Mozilla以及微软的浏览器对ES6的兼容性越来越好, 开发者很快就能使用这些语言上的改进了。

  • 但是那些非长青的浏览器, 以及大多数手机浏览器对ES6的支持非常差。

ECMAScript 6 compatibility table (kangax.github.io):如果要看各大浏览器对ES6更新更详细的支待情况,请访问查看最新信息, 创建者Juriy Zaytsev一直在更新表格数据。 本章会开始开发Chattrbox的用户界面, 在开发过程中会用到很多ES6特性。 为了让应用能在 所有浏览器中运行, 我们将使用开源工具Babel处理兼容性问题。

let与提升

let socket;

  • 上面的声明使用了ES6中定义变量的新方法,叫作let作用域。

  • 假如你使用let作用域来声明一个变量——关键字不是var,而是let——那么变量将不会被提升(hoist)。

  • 提升的意思是,变量声明被移动到创建这些变批的函数作用域的开头,这属于JavaScript解析器在后台执行的操作。

  • 不幸的是, 这些操作会导致一些难以察觉的错误。

  • 在if/else语句和循环体里,let 是一种更安全的声明变量的方式。

JSON

JavaScript对象表示法(JavaScript Object Notation)

  • 更常见的说法是JSON(发音同Jason,源自JSON发明者Douglas Crockford)

  • 是一个轻量级的数据交换格式, 你在package.json文件中已经用到了它。

  • 这种格式具备可读性, 独立于语言, 而且很适合Chattrbox的数据交换。

ES6传参

  • 有很多解决办法,下面简单介绍几个, 其中一些还利用了便捷的ES6特性。

方法一:

class ChatMessage { //第一种方式就是使用简单的构造函数接收消息内容、用户名以及时间戳。(下面只是个例子,请不要据此修改你的项目代码。)
    constructor(message, user, timestamp) {
        this.message = message;
        this.user = user || 'batman';
        this.timestamp = timestamp || (new Date()).getTime();
    }
}

方法二:

//表示每条聊天消息
class ChatMessage {
    constructor(message, user='batman', timestamp=(new Date()).getTime()) { //ES6提供的默认参数值能以更简洁的方式实现相同的模式。
        this.message = message;
        this.user = user;
        this.timestamp = timestamp;
    }
}

方法三:

//表示每条聊天消息
class ChatMessage {
    constructor({message: m, user: u, timestamp: t}) { //还有一种方式是将一个单独的对象作为参数,使用键值对来指定消息内容、用户名和时间戳。这种方式可以使用解构赋值语法(destructuring assignment syntax)来实现。但是使用上面这种方式就不能利用默认参数的便利性了。
        this.message = m;
        this.user = u;
        this.timestamp = t;
    }
}

方法四:

//尽管默认参数只能存在于函数(或者构造函数)定义里,解构却可以在赋值操作时使用。上面的构造函数也可以写成下面这样:
class ChatMessage {
    constructor (data){
        var {message: m, user: u='batman', timestamp: t=(new Date()).getTime()} = data;
        this.message = m;
        this.user = u;
        this.timestamp = t;
    }
}

延展阅读:将其他语言编译成 JavaScript

WebAssembly

  • Google、Microsoft、Mozilla还有其他公司合作创建了一个项目,用来标准化一个用千JavaScript引擎的assemble语言。

  • 该项目叫作WebAssembly,目标是创建一种由多种语言编译而成的高性能低级语言。

  • WebAssembly的目的是补充JavaScript(而不是取代它)并利用多种语言的优点。比如,JavaScript擅长于创建基于浏览器的应用程序,但是并不擅长渲染数学密集型的游戏图像;

  • 而C和C++则极其担长渲染游戏代码。 与其将C++代码移植成JavaScript并且造成潜在的bug, 倒不如将其编译成WebAssembly。

asm.js

  • WebAssembly项目发源于一个早期项目asm.js。 asm.js项目定义了JavaScript的一个子集,旨在编写高性能的代码。

  • From ASM.JS to WebAssembly – Brendan Eich:更多有关asm.js和WebAssembly的信息请查看JavaScript之父的这篇文章

延展阅读:变量提升

JavaScript的问世使得非专业程序员也可以创建一些具有基本交互性的Web内容。尽管这门语言有些特性旨在让代码具备抗错误性,但是有些特性在实践中却容易造成错误,其中之是提升。

当JavaScript引擎解析代码的时候,它会查找所有的变批和函数声明,将它们移到其所在函数 的顶部。(假如它们不在函数内, 则会在其余代码之前被计算。)

示例能给出最好的说明。 如下代码:

function logSomeValues () { 
    console.log(myVal); 
    var myVal = 5; 
    console.log(myVal); 
}

将会被解析成下面这样:

function logSomeValues () {
    var myVal; 
    console.log(myVal); 
    myVal = 5; 
    console.log(myVal); 
}

如果在控制台调用logSomeValues, 将会看到下面这样的输出:

> logSomeValues(); 
Undefined

注意,只有声明被提升了,赋值操作还在原地。这自然会让人迷惑,尤其当你试图在订或者循环里声明变最时。在其他语言中, 大括号表示一个代码块,拥有自己的作用域。在JavaScript 中, 大括号并不会创建作用域, 只有函数才创建作用域。

来看另一个示例:

var myVal = 11; 
function doNotWriteCodelikeThis() { 
    if (myVal > 10) { 
        var myVal = 0;
        console.log('myVal was greater than 10; resetting to 0'); } 
    else { 
        console.log('no need to reset.'); 
    }
    return myVal; 
}

你的预期也许是在控制台打印出'myVal was greater than 10; resetting to 0', 并 且返回值为0。但是, 真实的输出如下:

> doNotWri teCodelikeThis (); 
no need to reset. 
undefined

var myVazl声明被移到函数顶部,所以在执行订语句之前, myVal的值为undefined。赋值 操作仍然保持在订代码块中。

函数声明也会被提升,不过它是被整体提升。也就是说下面的代码会正常运行:

boo(); //在调用之后声明:

function boo (){
    console.log('BOO!!'); 
}

JavaScript将整个函数声明块移动到顶部,调用boo不会有任何问题:

> boo(); 
BOO!!

提升并不会移动let声明, 同样也不会移动const声明, 它用千声明无法重新赋值的变址。

延展阅读:箭头函数

箭头函数的功能并不完全等同于匿名函数。在某些情况下,箭头函数更好。除了提供简短的语法, 箭头函数有以下优点。

  • 可以实现function(){}.bind(this)的效果, 让this指向箭头函数本身所处的作用域。

  • 假如只有一句声明的话, 可以省略大括号。

  • 在省略大括号的情况下, 会返回这一句声明的结果。

举个例子, 这里是CoffeeRun的Checklist.prototype.addClickHandler方法:

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实例上。
};

将这里的匿名函数替换成箭头函数, 会使代码简洁。

CheckList.prototype.addClickHandler = (fn) => {
    this.$element.on('click', 'input', (event)=> {
        var email = event.target.value;
        fn(email)
            .then(()=> this.removeRow(email)); 
    });
};

实现步骤

置入html和CSS

链接:百度网盘 请输入提取码 提取码:wcpk。在开始之前,还需要处理 件事,Chattrbox项目的index.html和stylesheets/styles.css文件放在此百度网盘分享链接中(原书链接已经失效)。

请下载.zip文件, 提取其中的内容(包括整个stylesheets/文件夹), 将内容复制到chattrbox/app路径下。(index.html会替换项目下面已有的index.html文件。)

提醒一下:在编写本章代码的过程中, 可能会在控制台看到关于CSS文件的MIME类型的警告。 正藏现象。

编译JavaScript 的工具

为了更高效地使用Babel, 还需要安装几个npm模块来运行自动构建流程。

  • Babel是一个编译器,用Babel 将ES6语法翻译成等价的ES5代码,这样就能够在浏览器的JavaScript引擎下运行了。

  • 用Browserify将模块打包到一个单独的文件

  • 用Babelify让两者(Babel和Browserify)协同工作。 另外还要用Watchify实时监听代码变化触发构建流程。

首先需要安装Babel。Babel有一些不同的用法需要根据具体需求来决定。

  • 在Chattrbox项目里,需要用两种方式进行编译:命令行或者程序。

  • 和babel-core这两个工具分别能够满足这两个需求。

  • 此外,还需要安装Babel配置用来编译ES6标准,这个配置叫做babel-preset-es2015。

在chattrbox目录运行下面的npm命令,安装合适的Babel工具。(参考第1章复习如何使用管理员权限 ,理员权限运行npm install -g。)

npm install -g babel-cli

npm install --save-dev babel-core

npm install --save-dev babel-preset-es2015

接下来用刚才安装的es2015预设配置来配置Babel。在chattrbox根目录下创建一个名为babelrc 的文件,写入下面的配嚣信息:

{
  "presets": [
    "es2015"
  ],
  "plugins": []
} 

最后, 将Babelify 、 Browserify 、 Watchify安装到chattrbox/node_ modules/目录下:

npm install --save-dev browserify babelify@8 watchify

在启动Babel后不久就会用到这三个工具。

Chattrbox客户端应用程序

  • 前面两章已经将Chattrbox服务端构建好了,服务端能够返回静态文件, 并且通过WebSocket 通信。

  • 客户端应用则要通过WebSocket与服务端相互收发消息。

  • 客户端应用将为每条消息定义一个格式。 用户可以看到消息列表, 还能通过在表单输入文字创建新消息。

这些功能将由以下3个模块处理。

  • ws-client模块为客户端程序管理WebSocket通信。

  • dom模块向UI展示数据, 并处理表单提交。

  • app模块定义消息结构, 在ws-c巨ent和dom之间传递消息。 图17-5展示了三个模块之间的关系。

在chattrbox/app文件夹下创建scripts 、scripts/dist以及scripts/src子文件夹,

scripts/src 目录下创建如下4个JavaScript文件: app.js, dom.js, main.js, ws-client.js 现在的文件结构如下。

试运行一下代码。打开第二个终端窗口,切换到Chattrbox根目录下,也就是package.json、 index.js以及app/所在的目录。 在这个窗口运行构建工具,在之前那个窗口运行服务端代码。

为了测试代码,用Babel编译app/scripts/src/app.js, 将结果输出到app/scripts/dist/main.js中: babel app/scripts/src/app.js -o app/scripts/dist/main.js

使用 Browserify 打包模块

ES5没有内置的模块系统。在前面构建CoffeeRun时,曾使用过一个变通的办法来写模块代码但这需要修改一个全局变量。 ES6提供了真正的模块, 就像其他语言的模块一样。Babel能够理解ES6模块语法,但是没法将其转换成等价的ES5代码,所以就需要使用Browserify了。

默认情况下, Babel将ES6模块语法转换成等价的Node.js风格的require和module.exports语法。接着Browserify将Node.js模块代码转换成ES5可以识别的函数。 打开package.json为Browserify添加一段配置:

···
"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "start": "node index.js",
  "dev": "nodemon index.js"
},
"browserify": {
  "transform": [
    ["babelify", {"presets": ["es2015"], "sourceMap": true}]
  ]
},
···

这段代码告诉Browserify将Babelify当作一个插件。它给Babelify传递了两个选项: 第一个激活了ES2015编译器选项;另一个启用了sourceMap选项,这样有助于调试。

跟nodemon一样,最好为通用的Browserify任务编写一些脚本。打开package.json在"scripts"字段写入以下代码。(记得在" dev": "nodemon index.js"结尾加上逗号。)

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "start": "node index.js",
  "dev": "nodemon index.js",
  "build": "browserify -d app/scripts/src/main.js -o app/scripts/dist/main.js",
  "watch": "watchify -v -d app/scripts/src/main.js -o app/scripts/dist/main.js"
},
"browserify": {
  "transform": [
    ["babelify", {"presets": ["es2015"], "sourceMap": true}]
  ]
},

第一个脚本build 直接使用browserify 命令。第二个脚本watch 则在代码改变时使用watchify重新运行browserify(跟nodemon类似的功能)。

修改app.js ,导出ChatApp类,而不是简单地创建一个实例。

app.js

// new ChatApp();
export default ChatApp; //现在开始使用ES6模块系统。在ES6模块中,必须明确地导出想让别人使用的模块代码。这段代码指定ChatApp就是app模块里面可用的默认值。 其他模块可能会导出多个值。 如果只需要导出一个值,最好使用export default。

main.js

//main.js将app.js导出的ChatApp类导入进来。导入之后, 创建一个ChatApp类的实例。
import ChatApp from './app';
new ChatApp();

注意 , 在main.js中导入的类名是否叫ChatApp并不重要, 因为ChatApp是app.js默认的导出值。比如 , 写成import MyChatApp from './app'就会将默认的导出值赋给当前作用域下的MyChatApp变量名。只不过在导入时命名为ChatApp是最佳实践 , 因为它在app.js中的名字就是如此。

执行构建操作

现在打开终端,运行构建脚本: npm run build npm会运行build命令,该命令将调用browserify 。运行每个命令时,都会显示当前正在执行操作。但Browserify不会打印任何信息,除非遇到错误。

E:\2021大三上\Web前端开发\作业\html-picture\Chattrbox>npm run build
​
> chattrbox@1.0.0 build E:\2021大三上\Web前端开发\作业\html-picture\Chattrbox
> browserify -d app/scripts/src/main.js -o app/scripts/dist/main.js
​
​
E:\2021大三上\Web前端开发\作业\html-picture\Chattrbox>

Browserify成功运行后 , 就会将app/dist/文件夹下Babel编译生成的main.js (就是前面手动编译的结果) 打包。

重新加载浏览器就能看到输出。这里并没有新增功能, 只修改了ChatApp构造函数的调用位置。因此在控制台看到的消息与之前一样。

下一步是使用Watchify。就像用nodemon运行Node.js服务器一样, Watchify可以用来运行 Browserify编译程序一只要修改了源文件 , 它就会自动触发重新编译。

启动Watchify。只要修改代码 , 就会开始编译: npm run watch

E:\2021大三上\Web前端开发\作业\html-picture\Chattrbox>npm run watch
​
> chattrbox@1.0.0 watch E:\2021大三上\Web前端开发\作业\html-picture\Chattrbox
> watchify -v -d app/scripts/src/main.js -o app/scripts/dist/main.js
​
4490 bytes written to app/scripts/dist/main.js (0.48 seconds) at 下午2:54:35

新增ChatMessage类、ws-client模块、处理链接

app.js

import socket from './ws-client'; //接下来要在app.js中导入ws-client模块提供的值。添加一个导入声明。socket就是从ws-client.js中导出的对象。
​
class ChatApp { //构建Chattrbox客户端程序要用到的第一个ES6特性是class关键字。请牢记,ES6的class关键字跟其他编程语言里的类并不完全一样,ES6的类只是为构造函数和prototype方法提供了一种简写的语法。
    constructor() { //每当实例化一个类时,都会执行constructor方法。通常,构造函数会给实例的属性赋值。接着,在app.js中的ChatApp类声明之后创建一个ChatApp实例。
        socket.init('ws://localhost:3001'); //调用socket.init方法,将Web Socket服务器的URL传进去。
    }
}
//表示每条聊天消息
class ChatMessage {
    constructor ({//可以将默认参数和解构赋值结合起来, 所以app.js里构造函数的最终版长下面这样:这一版代码把传给构造函数的对象里的值提取出来。任何没有赋值的参数都有默认值。
                     message: m,
                     user: u='batman',
                     timestamp: t=(new Date()).getTime()
                 }){
        this.message = m;
        this.user = u;
        this.timestamp = t;
    }
    /*ChatMessage类将所有重要信息存储为属性,但是实例还继承了ChatMessage的方法和其他信息,这使得ChatMessage的实例不适合通过WebSocket发送。因此,需要一个信息的简化版。*/
    serialize() { //在app.js里添加一个序列化 (serialize) 方法,用于将ChatMessage里的属性转化成一个简单的JavaScript对象。
        return {
            user: this.user,
            message: this.message,
            timestamp: this.timestamp
        };
    }
}
​
// new ChatApp();
export default ChatApp; //现在开始使用ES6模块系统。在ES6模块中,必须明确地导出想让别人使用的模块代码。这段代码指定ChatApp就是app模块里面可用的默认值。 其他模块可能会导出多个值。 如果只需要导出一个值, 最好使用export default。

ws-client.js

//ws-client.js模块负责与Node WebSocket服务器通信。它有4项职责。连接到服务器。在初次建立连接时执行初始化配置。将到达的消息发给相应的处理程序。向外发送消息。
let socket;
​
/*init函数连接到WebSocket服务器。接下来,要把ws-client.js导入到app.js的ChatApp中。*/
function init(url) {
    socket = new WebSocket(url);
    console.log('connecting...');
}
//为了能被调用,ws-client.js要指定导出的内容。这里需要导出一个单独的值:一个将导出的函数作为属性值的对象。跟本章开头一样,使用export default语法加上额外的ES6简写方法。
export default {
    init, //等价于init: init。假如键和值名称一样,ES6允许省略冒号和值。键会自动作为变量名,值会自动关联到变量名对应的值。这个ES6特性是增强版的对象字面量语法。
}
  • npm脚本会重新编译代码。

    • (假如已经停止了npm run watch 或者npm run dev ,需要在不同的窗口重启这两个脚本。)

  • 重新加载浏览器,会看到控制台打印出'connecting...'

代码展示

app.js

import socket from './ws-client'; //接下来要在app.js中导入ws-client模块提供的值。添加一个导入声明。socket就是从ws-client.js中导出的对象。
​
class ChatApp { //构建Chattrbox客户端程序要用到的第一个ES6特性是class关键字。请牢记,ES6的class关键字跟其他编程语言里的类并不完全一样,ES6的类只是为构造函数和prototype方法提供了一种简写的语法。
    constructor() { //每当实例化一个类时,都会执行constructor方法。通常,构造函数会给实例的属性赋值。接着,在app.js中的ChatApp类声明之后创建一个ChatApp实例。
​
        socket.init('ws://localhost:3001'); //调用socket.init方法,将Web Socket服务器的URL传进去。
​
        socket.registerOpenHandler(() => { //调用socket.init之后,调用registerOpenHandler和 registerMessageHandler, 为它们传递箭头函数。
            let message = new ChatMessage({ message: 'pow!' });
            socket.sendMessage(message.serialize());
        });
        socket.registerMessageHandler((data) => {
            console.log(data);
        });
    }
}
//表示每条聊天消息
class ChatMessage {
    constructor ({//可以将默认参数和解构赋值结合起来, 所以app.js里构造函数的最终版长下面这样:这一版代码把传给构造函数的对象里的值提取出来。任何没有赋值的参数都有默认值。
                     message: m,
                     user: u='superman',
                     timestamp: t=(new Date()).getTime()
                 }){
        this.message = m;
        this.user = u;
        this.timestamp = t;
    }
    /*ChatMessage类将所有重要信息存储为属性,但是实例还继承了ChatMessage的方法和其他信息,这使得ChatMessage的实例不适合通过WebSocket发送。因此,需要一个信息的简化版。*/
    serialize() {
        return {
            user: this.user,
            message: this.message,
            timestamp: this.timestamp
        };
    };//添加一个序列化 (serialize) 方法,用于将ChatMessage里的属性转化成一个简单的JavaScript对象。
}
​
// new ChatApp();
export default ChatApp; //现在开始使用ES6模块系统。在ES6模块中,必须明确地导出想让别人使用的模块代码。这段代码指定ChatApp就是app模块里面可用的默认值。 其他模块可能会导出多个值。 如果只需要导出一个值, 最好使用export default。

ws-client.js

//ws-client.js模块负责与Node WebSocket服务器通信。它有4项职责。连接到服务器。在初次建立连接时执行初始化配置。将到达的消息发给相应的处理程序。向外发送消息。
let socket;
​
/*init函数连接到WebSocket服务器。把ws-client.js导入到app.js的ChatApp中。当App模块调用init方法时,会实例化一个新的WebSocket对象,并与服务器建立一个连接。*/
function init(url) {
    socket = new WebSocket(url);
    console.log('connecting...');
}
​
/*App模块需要感知到该操作已经完成,以便在连接上进行一些操作。WebSocket 对象提供了一系列专门用于处理事件的属性,其中一个是onopen 属性。当与WebSocket服务器的连接成功建立时,就会调用赋给这个属性的函数。在这个函数中,可以对连接进行任何操作。*/
function registerOpenHandler(handlerFunction) {
    socket.onopen = () => { //onopen函数定义跟以前写的不太一样。这种方式是ES6的一种新语法, 叫做箭头函数。箭头函数是匿名函数的一种缩写方式。除了写起来更简单, 箭头函数与一般的匿名函数一模一样。(使用匿名函数比写socket.onopen = handlerFunction要复杂一些。当需要响应一个事件但需要执行一些中间操作时——比如打印日志消息——使用匿名函数效果更好。)
        console.log('open');
        handlerFunction();
    };
}
​
/*接下来需要写一个接口,用来处理经由WebSocket连接收到的消息。将socket的onmessage属性赋值为一个箭头函数,该箭头函数接受一个事件参数。Chattrbox客户端程序能通过onmessage回调函数接受服务端发来的一个对象。该对象表示消息事件,它有一个data属性,包含了服务端发送的JSON字符串。每收到一个字符串,就需要将字符串转换成JavaScript对象,然后将其发送给handlerFunction。*/
function registerMessageHandler(handlerFunction) {
    socket.onmessage = (e) => {
        console.log('message', e.data);
        let data = JSON.parse(e.data);
        handlerFunction(data);
    };
}
​
/*还需要最后一部分代码,用来将消息发送给WebSocket。在ws-client.js中增加一个sendMessage函数。发送消息总共分两步:*/
function sendMessage(payload) {
    socket.send(JSON.stringify(payload)); //首先将返回的消息内容(包括消息内容、用户名和时间戳)转换成JSON字符串,然后将JSON字符串发送给WebSocket服务器。
}
​
/*为了能被调用,ws-client.js要指定导出的内容。这里需要导出一个单独的值:一个将导出的函数作为属性值的对象。跟本章开头一样,使用export default语法加上额外的ES6简写方法。*/
export default {
    init, //等价于init: init。假如键和值名称一样,ES6允许省略冒号和值。键会自动作为变量名,值会自动关联到变量名对应的值。这个ES6特性是增强版的对象字面量语法。
    registerOpenHandler,
    registerMessageHandler,
    sendMessage,
}//使用增强版的对象字面量语法将新增方法导出。

第十八章 继续ES6探索之旅

实现步骤

将jQuery安装成一个Node模块

  • Chattrbox将使用jQuery进行DOM操作。

  • Browserify会自动将JavaScript依赖项构建到浏览器使用的应用程序包中。

  • 如果想集成jQuery, 只需要通过import包含它, Browserify就会处理剩下的事情。

首先, 安装jQuery库到node_modules文件夹: npm install --save-dev jquery

dom.js

import $ from 'jquery';//dom.js模块将会用到jQuery, 所以添加一句皿port 声明以包含它。

创建ChatForm类和ChatList

dom.js

import $ from 'jquery';
​
/*ChatForm负责向外发送聊天消息。管理DOM里的表单元素
定义ChatForm, 它的构造函数接受选择器参数。在这个构造函数中,为实例需要追踪的元素添加属性。
使用命名的导出(named export)方式来导出多个命名值, 而不是导出一个默认值。通过在class声明前面添加export关键字, 导出ChatForm类。 当用户使用该模块时, 便可通过类名访问到它。*/
export class ChatForm {
    constructor(formSel, inputSel) {
        this.$form = $(formSel);
        this.$input = $(inputSel);
    }
​
    init(submitCallback) {
        this.$form.submit((event) => {//init方法中的提交事件处理程序用到了箭头函数。该箭头函数阻止了表单默认的提交行为,取得表单输入框的值并将其传递给submitCallback, 最终重置输入框的值。
            event.preventDefault();
            let val = this.$input.val();
            submitCallback(val);
            this.$input.val('');
        });
        this.$form.find('button').on('click', () => this.$form.submit());//为确保点击按钮时提交表单, 添加一个点击事件处理程序, 让表单触发submit事件。为实现这一过程,用jQuery获取表单元素,然后调用jQuery的submit方法。这里用到了箭头函数的单个表达式版本,可以省掉大括号。
    }
}
​
/*在服务端发来新消息时,将其展示出来。第二个类ChatList, 用来向用户展示聊天消息列表。ChatList 会给每条消息创建DOM元素,以展示发送该消息的用户名以及消息内容。 */
export class ChatList {
    /*ChatList接受属性选择器和用户名作为参数。属性选择器用于决定将创建的消息列表元素添加到哪个元素,用户名用于区分发送消息的是当前用户还是其他人。(当前用户的消息和别人发送的消息会区分展示。)*/
    constructor(listSel, username) {
        this.$list = $(listSel);
        this.username = username;
    }
​
    /*需要为消息创建DOM元素。给Chat List 添加一个drawMessage方法。它接受一个对象参数,并将该参数解构成本地变量,用来表示用户名、时间戳以及消息内容。(为了解释解构赋值,下面的例子使用单字符的本地变量。)*/
    drawMessage({user: u, timestamp: t, message: m}) {
        /*创建一行消息,包括用户名、时间戳和消息内容本身。*/
        let $messageRow = $('<li>', {
            'class': 'message-row'
        });
​
        /*假如当前用户是消息的发送者,对应的消息元素会有一个额外的CSS类名用来区分样式。*/
        if (this.username === u) {
            $messageRow.addClass('me');
        }
​
        let $message = $('<p>');
​
        $message.append($('<span>', {
            'class': 'message-username',
            text: u
        }));
​
        $message.append($('<span>', {
            'class': 'timestamp',
            'data-time': t,
            text: (new Date(t)).getTime()
        }));
​
        $message.append($('<span>', {
            'class': 'message-message',
            text: m
        }));
​
        /*ChatList的列表元素中,并且将新消息行滚动到可视区域。*/
        $messageRow.append($message);
        this.$list.append($messageRow);
        $messageRow.get(0).scrollIntoView();
    }
}

app.js

import socket from './ws-client'; //接下来要在app.js中导入ws-client模块提供的值。添加一个导入声明。socket就是从ws-client.js中导出的对象。
import {ChatForm, ChatList} from './dom'; //导入ChatForm类(命名的导入)
​
//为表单选择器和消息输入框选择器创建常量。
const FORM_SELECTOR = '[data-chat="chat-form"]';
const INPUT_SELECTOR = '[data-chat="message-input"]';
const LIST_SELECTOR = '[data-chat="message-list"]';
​
class ChatApp { //构建Chattrbox客户端程序要用到的第一个ES6特性是class关键字。请牢记,ES6的class关键字跟其他编程语言里的类并不完全一样,ES6的类只是为构造函数和prototype方法提供了一种简写的语法。
    constructor() { //每当实例化一个类时,都会执行constructor方法。通常,构造函数会给实例的属性赋值。接着,在app.js中的ChatApp类声明之后创建一个ChatApp实例。
        this.chatForm = new ChatForm(FORM_SELECTOR, INPUT_SELECTOR);//创建一个ChatForm的实例
        this.chatList = new ChatList(LIST_SELECTOR, 'wonderwoman');//实例化一个新的Chatlist
​
        socket.init('ws://localhost:3001'); //调用socket.init方法,将Web Socket服务器的URL传进去。
        socket.registerOpenHandler(() => { //调用socket.init之后,调用registerOpenHandler和 registerMessageHandler, 为它们传递箭头函数。
            this.chatForm.init((data) => { //一定要在socket连接打开之后初始化,而不能一创建实例就初始化。这种等待能避免用户输入了聊天消息却不能发送到服务器的情况。(毕竟如果消息发不出去, 用户体验会很糟糕。)记得要给ChatForm的init方法传一个回调函数, 用来处理表单的提交。给它传一个回调函数,将来自ChatForm的消息数据发给socket。
                let message = new ChatMessage({message: data});
                socket.sendMessage(message.serialize());
            });
        });
        /*用接收的数据创建一个新的ChatMessage ,然后对消息进行序列化。这一步属于预防措施,用于去除数据里可能存在的多余元数据。根据socket数据创建一个新的ChatMessage用以提供消息*/
        socket.registerMessageHandler((data) => {
            console.log(data);
            let message = new ChatMessage(data);
            this.chatList.drawMessage(message.serialize());
        });
    }
}
​
//表示每条聊天消息
class ChatMessage {
    constructor ({//可以将默认参数和解构赋值结合起来, 所以app.js里构造函数的最终版长下面这样:这一版代码把传给构造函数的对象里的值提取出来。任何没有赋值的参数都有默认值。
                     message: m,
                     user: u='superman',
                     timestamp: t=(new Date()).getTime()
                 }){
        this.message = m;
        this.user = u;
        this.timestamp = t;
    }
    /*ChatMessage类将所有重要信息存储为属性,但是实例还继承了ChatMessage的方法和其他信息,这使得ChatMessage的实例不适合通过WebSocket发送。因此,需要一个信息的简化版。*/
    serialize() {
        return {
            user: this.user,
            message: this.message,
            timestamp: this.timestamp
        };
    };//添加一个序列化 (serialize) 方法,用于将ChatMessage里的属性转化成一个简单的JavaScript对象。
}
​
// new ChatApp();
export default ChatApp; //现在开始使用ES6模块系统。在ES6模块中,必须明确地导出想让别人使用的模块代码。这段代码指定ChatApp就是app模块里面可用的默认值。 其他模块可能会导出多个值。 如果只需要导出一个值, 最好使用export default。

使用Gravatar和请求用户名

Gravatar是一个可用于关联头像和邮箱地址的免费服务,它通过一个专门格式化的URL提供每个用户的头像。

URL 的最后一部分:

  • 根据用户邮箱地址生成的唯一标识。

  • 这个标识叫作晗希( hash ),用第三方库crypto-js很容易生成。

用npm将crypto-js添加到项目中: npm install --save - dev crypto-js

dom.js

import $ from 'jquery';
import md5 from 'crypto-js/md5'; //用JavaScript创建字符串时,经常需要将字符串与其他值进行拼接。为了创建包含表达式和变量值的字符串, ES6 提供了更好的方法一一模板字符串。接下来使用这个功能创建访问Gravatar图片的URL。在dom.js中添加另一个import声明,导人crypto-js库的子模块md5,使用/来分隔主模块和子模块的名称。
​
/*它接受一个用户名,用来生成一个MD5的哈希值,井返回Gravatar的URL 。*/
function createGravatarUrl(username) {
    let userhash = md5(username);
    return `http://www.gravatar.com/avatar/${userhash.toString()}`;//注意:符号并不是单引号, 而是反引号。在反引号里可以直接在字符串中包含JavaScript表达式的值,可以包含任何表达式。
}
​
/*请求用户输入他们的用户名。将其添加到export ,而不是作为ChatForm或者。ChatList的一部分。*/
export function promptForUsername() {
    let username = prompt('Enter a username');//在创建一个let变量,用以保存用户输入的文字。( prompt函数是浏览器内置的,它会返回一个字符串。)
    return username.toLowerCase(); //然后返回转换成小写格式的文字。
}
/*ChatForm负责向外发送聊天消息。管理DOM里的表单元素
定义ChatForm, 它的构造函数接受选择器参数。在这个构造函数中,为实例需要追踪的元素添加属性。
使用命名的导出(named export)方式来导出多个命名值, 而不是导出一个默认值。通过在class声明前面添加export关键字, 导出ChatForm类。 当用户使用该模块时, 便可通过类名访问到它。*/
export class ChatForm {
    constructor(formSel, inputSel) {
        this.$form = $(formSel);
        this.$input = $(inputSel);
    }
​
    init(submitCallback) {
        this.$form.submit((event) => {//init方法中的提交事件处理程序用到了箭头函数。该箭头函数阻止了表单默认的提交行为,取得表单输入框的值并将其传递给submitCallback, 最终重置输入框的值。
            event.preventDefault();
            let val = this.$input.val();
            submitCallback(val);
            this.$input.val('');
        });
        this.$form.find('button').on('click', () => this.$form.submit());//为确保点击按钮时提交表单, 添加一个点击事件处理程序, 让表单触发submit事件。为实现这一过程,用jQuery获取表单元素,然后调用jQuery的submit方法。这里用到了箭头函数的单个表达式版本,可以省掉大括号。
    }
}
​
/*在服务端发来新消息时,将其展示出来。第二个类ChatList, 用来向用户展示聊天消息列表。ChatList 会给每条消息创建DOM元素,以展示发送该消息的用户名以及消息内容。 */
export class ChatList {
    /*ChatList接受属性选择器和用户名作为参数。属性选择器用于决定将创建的消息列表元素添加到哪个元素,用户名用于区分发送消息的是当前用户还是其他人。(当前用户的消息和别人发送的消息会区分展示。)*/
    constructor(listSel, username) {
        this.$list = $(listSel);
        this.username = username;
    }
​
    /*需要为消息创建DOM元素。给Chat List 添加一个drawMessage方法。它接受一个对象参数,并将该参数解构成本地变量,用来表示用户名、时间戳以及消息内容。(为了解释解构赋值,下面的例子使用单字符的本地变量。)*/
    drawMessage({user: u, timestamp: t, message: m}) {
        /*创建一行消息,包括用户名、时间戳和消息内容本身。*/
        let $messageRow = $('<li>', {
            'class': 'message-row'
        });
​
        /*假如当前用户是消息的发送者,对应的消息元素会有一个额外的CSS类名用来区分样式。*/
        if (this.username === u) {
            $messageRow.addClass('me');
        }
​
        let $message = $('<p>');
​
        $message.append($('<span>', {
            'class': 'message-username',
            text: u
        }));
​
        $message.append($('<span>', {
            'class': 'timestamp',
            'data-time': t,
            text: (new Date(t)).getTime()
        }));
​
        $message.append($('<span>', {
            'class': 'message-message',
            text: m
        }));
​
        let $img = $('<img>', {
            src: createGravatarUrl(u),
            title: u
        });
​
        /*ChatList的列表元素中,并且将新消息行滚动到可视区域。*/
        $messageRow.append($img);
        $messageRow.append($message);
        this.$list.append($messageRow);
        $messageRow.get(0).scrollIntoView();
    }
}

app.js

import socket from './ws-client'; //接下来要在app.js中导入ws-client模块提供的值。添加一个导入声明。socket就是从ws-client.js中导出的对象。
import {ChatForm, ChatList, promptForUsername} from './dom'; //导入ChatForm类(命名的导入)
​
//为表单选择器和消息输入框选择器创建常量。
const FORM_SELECTOR = '[data-chat="chat-form"]';
const INPUT_SELECTOR = '[data-chat="message-input"]';
const LIST_SELECTOR = '[data-chat="message-list"]';
​
//调用 promptForUsername函数, 获取username变量的值:
let username = '';
username = promptForUsername();
​
class ChatApp { //构建Chattrbox客户端程序要用到的第一个ES6特性是class关键字。请牢记,ES6的class关键字跟其他编程语言里的类并不完全一样,ES6的类只是为构造函数和prototype方法提供了一种简写的语法。
    constructor() { //每当实例化一个类时,都会执行constructor方法。通常,构造函数会给实例的属性赋值。接着,在app.js中的ChatApp类声明之后创建一个ChatApp实例。
        this.chatForm = new ChatForm(FORM_SELECTOR, INPUT_SELECTOR);//创建一个ChatForm的实例
        this.chatList = new ChatList(LIST_SELECTOR, username);//实例化一个新的Chatlist
​
        socket.init('ws://localhost:3001'); //调用socket.init方法,将Web Socket服务器的URL传进去。
        socket.registerOpenHandler(() => { //调用socket.init之后,调用registerOpenHandler和 registerMessageHandler, 为它们传递箭头函数。
            this.chatForm.init((data) => { //一定要在socket连接打开之后初始化,而不能一创建实例就初始化。这种等待能避免用户输入了聊天消息却不能发送到服务器的情况。(毕竟如果消息发不出去, 用户体验会很糟糕。)记得要给ChatForm的init方法传一个回调函数, 用来处理表单的提交。给它传一个回调函数,将来自ChatForm的消息数据发给socket。
                let message = new ChatMessage({message: data});
                socket.sendMessage(message.serialize());
            });
        });
        /*用接收的数据创建一个新的ChatMessage ,然后对消息进行序列化。这一步属于预防措施,用于去除数据里可能存在的多余元数据。根据socket数据创建一个新的ChatMessage用以提供消息*/
        socket.registerMessageHandler((data) => {
            console.log(data);
            let message = new ChatMessage(data);
            this.chatList.drawMessage(message.serialize());
        });
    }
}
​
//表示每条聊天消息
class ChatMessage {
    constructor ({//可以将默认参数和解构赋值结合起来, 所以app.js里构造函数的最终版长下面这样:这一版代码把传给构造函数的对象里的值提取出来。任何没有赋值的参数都有默认值。
                     message: m,
                     user: u=username,
                     timestamp: t=(new Date()).getTime()
                 }){
        this.message = m;
        this.user = u;
        this.timestamp = t;
    }
    /*ChatMessage类将所有重要信息存储为属性,但是实例还继承了ChatMessage的方法和其他信息,这使得ChatMessage的实例不适合通过WebSocket发送。因此,需要一个信息的简化版。*/
    serialize() {
        return {
            user: this.user,
            message: this.message,
            timestamp: this.timestamp
        };
    };//添加一个序列化 (serialize) 方法,用于将ChatMessage里的属性转化成一个简单的JavaScript对象。
}
​
// new ChatApp();
export default ChatApp; //现在开始使用ES6模块系统。在ES6模块中,必须明确地导出想让别人使用的模块代码。这段代码指定ChatApp就是app模块里面可用的默认值。 其他模块可能会导出多个值。 如果只需要导出一个值, 最好使用export default。

 

使用会话存储

  • 将用户名存储在浏览器,每次刷新页面就不需要输入用户名。

  • 为了实现简单的存储, 浏览器提供了两个API用来存储键值对(有一个限制一值必须是字符串)。

  • 这两个API是localStorage和sessionStorage。

    • 在localStorage和sessionStorage中存储的数据与Web应用服务器地址相关联。

    • 不同网站的代码无法访问彼此的数据。

  • 用sessionStorage API了。浏览器会话结束时数据会被清除(不管是关闭浏览器的标签还是窗口)。

    • 如果用localStorage, 浏览器会话结束时数据不会被清除(不管是关闭浏览器的标签还是窗口)

  • 接下来创建一系列新的类来管理sessionStorage信息。在app/scripts/src文件夹下新建一个storage.js文件, 并定义一个新的类:

storage.js

class Store {
    /*新的Store类是通用类, 它既可以搭配localStorage使用, 也可以搭配sessionStorag叶吏用。它只是简单地封装了一下Web Storage API。在实例化这个类的时候,可以指定使用哪个StorageAPI。*/
    constructor(storageApi) {
        this.api = storageApi; //注意,没有在构造函数中设置对this.key的引用,因为Store类并不需要给自己提供存储的数据。相反,它用来创建定义key属性的子类。
    }
    get() {
        return this.api.getItem(this.key);
    }
​
    set(value) {
        this.api.setItem(this.key, value);
    }
}
​
/*使用extends 关键字创建一个子类,用于在sessionStorage 中保存用户名。app.js将会用到UserStore ,所以需要把它导出。*/
export class UserStore extends Store {
    constructor(key) {
        super(sessionStorage);
        this.key = key;
    }//UserStoore 只定义了一个构造函数,它执行两个操作。首先,调用super,这一步会调用Store的构造函数,并传入一个对sessionStorage的引用。然后,给this.key设置一个值。现在Store类的api 的值已经设置好了, UserStore实例的key 的值也设置好了。所有代码已经准备就绪, UserStore实例可以调用get和set方法了。
}

app.js: 开头引入UserStore,创建一个实例,用这个实例来存储用户名

import socket from './ws-client'; //接下来要在app.js中导入ws-client模块提供的值。添加一个导入声明。socket就是从ws-client.js中导出的对象。
import {UserStore} from './storage'; //将UserStore导入到app.js中
import {ChatForm, ChatList, promptForUsername} from './dom'; //导入ChatForm类(命名的导入)
​
//为表单选择器和消息输入框选择器创建常量。
const FORM_SELECTOR = '[data-chat="chat-form"]';
const INPUT_SELECTOR = '[data-chat="message-input"]';
const LIST_SELECTOR = '[data-chat="message-list"]';
​
//创建一个实例,用这个实例来存储用户名
let userStore = new UserStore('x-chattrbox/u');
let username = userStore.get();
if (!username) {
    username = promptForUsername();//调用 promptForUsername函数, 获取username变量的值
    userStore.set(username);
}
​
class ChatApp { //构建Chattrbox客户端程序要用到的第一个ES6特性是class关键字。请牢记,ES6的class关键字跟其他编程语言里的类并不完全一样,ES6的类只是为构造函数和prototype方法提供了一种简写的语法。
    constructor() { //每当实例化一个类时,都会执行constructor方法。通常,构造函数会给实例的属性赋值。接着,在app.js中的ChatApp类声明之后创建一个ChatApp实例。
        this.chatForm = new ChatForm(FORM_SELECTOR, INPUT_SELECTOR);//创建一个ChatForm的实例
        this.chatList = new ChatList(LIST_SELECTOR, username);//实例化一个新的Chatlist
​
        socket.init('ws://localhost:3001'); //调用socket.init方法,将Web Socket服务器的URL传进去。
        socket.registerOpenHandler(() => { //调用socket.init之后,调用registerOpenHandler和 registerMessageHandler, 为它们传递箭头函数。
            this.chatForm.init((data) => { //一定要在socket连接打开之后初始化,而不能一创建实例就初始化。这种等待能避免用户输入了聊天消息却不能发送到服务器的情况。(毕竟如果消息发不出去, 用户体验会很糟糕。)记得要给ChatForm的init方法传一个回调函数, 用来处理表单的提交。给它传一个回调函数,将来自ChatForm的消息数据发给socket。
                let message = new ChatMessage({message: data});
                socket.sendMessage(message.serialize());
            });
        });
        /*用接收的数据创建一个新的ChatMessage ,然后对消息进行序列化。这一步属于预防措施,用于去除数据里可能存在的多余元数据。根据socket数据创建一个新的ChatMessage用以提供消息*/
        socket.registerMessageHandler((data) => {
            console.log(data);
            let message = new ChatMessage(data);
            this.chatList.drawMessage(message.serialize());
        });
    }
}
​
//表示每条聊天消息
class ChatMessage {
    constructor ({//可以将默认参数和解构赋值结合起来, 所以app.js里构造函数的最终版长下面这样:这一版代码把传给构造函数的对象里的值提取出来。任何没有赋值的参数都有默认值。
                     message: m,
                     user: u=username,
                     timestamp: t=(new Date()).getTime()
                 }){
        this.message = m;
        this.user = u;
        this.timestamp = t;
    }
    /*ChatMessage类将所有重要信息存储为属性,但是实例还继承了ChatMessage的方法和其他信息,这使得ChatMessage的实例不适合通过WebSocket发送。因此,需要一个信息的简化版。*/
    serialize() {
        return {
            user: this.user,
            message: this.message,
            timestamp: this.timestamp
        };
    };//添加一个序列化 (serialize) 方法,用于将ChatMessage里的属性转化成一个简单的JavaScript对象。
}
​
// new ChatApp();
export default ChatApp; //现在开始使用ES6模块系统。在ES6模块中,必须明确地导出想让别人使用的模块代码。这段代码指定ChatApp就是app模块里面可用的默认值。 其他模块可能会导出多个值。 如果只需要导出一个值, 最好使用export default。
  • 再次在浏览器中运行Chattrbox。

  • 这一次,只需要在初次加载页面的时候提交用户名了,之后刷新都会用第一次输入的用户名。

  • 为了确保用户名已经存储到sessionStorage 中,使用开发者工具里的Resources面板。

  • 点击(应用程序)Resources 面板后,会看到左边有一个列表。在列表中点击(会话存储)Session Storage左边的三角形,就会 展开显示http://localhost:3000 。点击这个URL ,就能看至UserStore存储的数据(如图18-7所示)。

  • 在右侧键值对列表的顶部有两个按钮,可以分别用来刷新列表和删除列表中的元素。假如你想手动修改存储的数据,可以使用这两个按钮。

格式化和更新消息时间戳

为了提供更友好的时间戳(比如“ 10分钟之前”), 需要增加一个叫作moment 的模块。用npm 安装这个模块,将其保存为开发环境下的依赖包。 npm install --save- dev moment

dom.js

import $ from 'jquery';
import md5 from 'crypto-js/md5'; //用JavaScript创建字符串时,经常需要将字符串与其他值进行拼接。为了创建包含表达式和变量值的字符串, ES6 提供了更好的方法一一模板字符串。接下来使用这个功能创建访问Gravatar图片的URL。在dom.js中添加另一个import声明,导人crypto-js库的子模块md5,使用/来分隔主模块和子模块的名称。
import moment from 'moment';
​
/*它接受一个用户名,用来生成一个MD5的哈希值,井返回Gravatar的URL 。*/
function createGravatarUrl(username) {
    let userhash = md5(username);
    return `http://www.gravatar.com/avatar/${userhash.toString()}`;//注意:符号并不是单引号, 而是反引号。在反引号里可以直接在字符串中包含JavaScript表达式的值,可以包含任何表达式。
}
​
/*请求用户输入他们的用户名。将其添加到export ,而不是作为ChatForm或者。ChatList的一部分。*/
export function promptForUsername() {
    let username = prompt('Enter a username');//在创建一个let变量,用以保存用户输入的文字。( prompt函数是浏览器内置的,它会返回一个字符串。)
    return username.toLowerCase(); //然后返回转换成小写格式的文字。
}
/*ChatForm负责向外发送聊天消息。管理DOM里的表单元素
定义ChatForm, 它的构造函数接受选择器参数。在这个构造函数中,为实例需要追踪的元素添加属性。
使用命名的导出(named export)方式来导出多个命名值, 而不是导出一个默认值。通过在class声明前面添加export关键字, 导出ChatForm类。 当用户使用该模块时, 便可通过类名访问到它。*/
export class ChatForm {
    constructor(formSel, inputSel) {
        this.$form = $(formSel);
        this.$input = $(inputSel);
    }
​
    init(submitCallback) {
        this.$form.submit((event) => {//init方法中的提交事件处理程序用到了箭头函数。该箭头函数阻止了表单默认的提交行为,取得表单输入框的值并将其传递给submitCallback, 最终重置输入框的值。
            event.preventDefault();
            let val = this.$input.val();
            submitCallback(val);
            this.$input.val('');
        });
        this.$form.find('button').on('click', () => this.$form.submit());//为确保点击按钮时提交表单, 添加一个点击事件处理程序, 让表单触发submit事件。为实现这一过程,用jQuery获取表单元素,然后调用jQuery的submit方法。这里用到了箭头函数的单个表达式版本,可以省掉大括号。
    }
}
​
/*在服务端发来新消息时,将其展示出来。第二个类ChatList, 用来向用户展示聊天消息列表。ChatList 会给每条消息创建DOM元素,以展示发送该消息的用户名以及消息内容。 */
export class ChatList {
    /*ChatList接受属性选择器和用户名作为参数。属性选择器用于决定将创建的消息列表元素添加到哪个元素,用户名用于区分发送消息的是当前用户还是其他人。(当前用户的消息和别人发送的消息会区分展示。)*/
    constructor(listSel, username) {
        this.$list = $(listSel);
        this.username = username;
    }
​
    /*需要为消息创建DOM元素。给Chat List 添加一个drawMessage方法。它接受一个对象参数,并将该参数解构成本地变量,用来表示用户名、时间戳以及消息内容。(为了解释解构赋值,下面的例子使用单字符的本地变量。)*/
    drawMessage({user: u, timestamp: t, message: m}) {
        /*创建一行消息,包括用户名、时间戳和消息内容本身。*/
        let $messageRow = $('<li>', {
            'class': 'message-row'
        });
​
        /*假如当前用户是消息的发送者,对应的消息元素会有一个额外的CSS类名用来区分样式。*/
        if (this.username === u) {
            $messageRow.addClass('me');
        }
​
        let $message = $('<p>');
​
        $message.append($('<span>', {
            'class': 'message-username',
            text: u
        }));
​
        /*为了保证用户可以马上看到一个可读的时间戳,更新drawMessage。在初次将消息绘制到聊天列表时,使用moment 创建一个格式化的时间戳字符串。*/
        $message.append($('<span>', {
            'class': 'timestamp',
            'data-time': t,
            text: moment(t).fromNow()
        }));
​
        $message.append($('<span>', {
            'class': 'message-message',
            text: m
        }));
​
        let $img = $('<img>', {
            src: createGravatarUrl(u),
            title: u
        });
​
        /*ChatList的列表元素中,并且将新消息行滚动到可视区域。*/
        $messageRow.append($img);
        $messageRow.append($message);
        this.$list.append($messageRow);
        $messageRow.get(0).scrollIntoView();
    }
​
    /*每条消息都将时间戳存储成了数据属性。为ChatList写一个init方法,用来调用内置函数setInterval。这个函数接受两个参数:要运行的函数,和这个函数多久运行一次。这个函数将会更新每条消息,将时间戳转换成用户可读的格式。*/
    init() {
        this.timer = setInterval(() => {
            $('[data-time]').each((idx, element) => {
                let $element = $(element);
                /*为了设置时间戳字符串,用jQuery查找所有带有data-time属性的元素,这个属性的值都是数字化的时间戳。使用这个数字化的时间戳创建一个新的Date对象*/
                let timestamp = new Date().setTime($element.attr('data-time'));
                /*将该对象传给moment,然后调用fromNow方法处理最终的时间戳字符串*/
                let ago = moment(timestamp).fromNow();
                /*并将结果字符串设为元素的HTML文本。*/
                $element.html(age);
            });
        }, 1000);//这个函数会每1000 毫秒执行一次。
    }
}

app.js

import socket from './ws-client'; //接下来要在app.js中导入ws-client模块提供的值。添加一个导入声明。socket就是从ws-client.js中导出的对象。
import {UserStore} from './storage'; //将UserStore导入到app.js中
import {ChatForm, ChatList, promptForUsername} from './dom'; //导入ChatForm类(命名的导入)
​
//为表单选择器和消息输入框选择器创建常量。
const FORM_SELECTOR = '[data-chat="chat-form"]';
const INPUT_SELECTOR = '[data-chat="message-input"]';
const LIST_SELECTOR = '[data-chat="message-list"]';
​
//创建一个实例,用这个实例来存储用户名
let userStore = new UserStore('x-chattrbox/u');
let username = userStore.get();
if (!username) {
    username = promptForUsername();//调用 promptForUsername函数, 获取username变量的值
    userStore.set(username);
}
​
class ChatApp { //构建Chattrbox客户端程序要用到的第一个ES6特性是class关键字。请牢记,ES6的class关键字跟其他编程语言里的类并不完全一样,ES6的类只是为构造函数和prototype方法提供了一种简写的语法。
    constructor() { //每当实例化一个类时,都会执行constructor方法。通常,构造函数会给实例的属性赋值。接着,在app.js中的ChatApp类声明之后创建一个ChatApp实例。
        this.chatForm = new ChatForm(FORM_SELECTOR, INPUT_SELECTOR);//创建一个ChatForm的实例
        this.chatList = new ChatList(LIST_SELECTOR, username);//实例化一个新的Chatlist
​
        socket.init('ws://localhost:3001'); //调用socket.init方法,将Web Socket服务器的URL传进去。
        socket.registerOpenHandler(() => { //调用socket.init之后,调用registerOpenHandler和 registerMessageHandler, 为它们传递箭头函数。
            this.chatForm.init((data) => { //一定要在socket连接打开之后初始化,而不能一创建实例就初始化。这种等待能避免用户输入了聊天消息却不能发送到服务器的情况。(毕竟如果消息发不出去, 用户体验会很糟糕。)记得要给ChatForm的init方法传一个回调函数, 用来处理表单的提交。给它传一个回调函数,将来自ChatForm的消息数据发给socket。
                let message = new ChatMessage({message: data});
                socket.sendMessage(message.serialize());
            });
            this.chatList.init();
        });
        /*用接收的数据创建一个新的ChatMessage ,然后对消息进行序列化。这一步属于预防措施,用于去除数据里可能存在的多余元数据。根据socket数据创建一个新的ChatMessage用以提供消息*/
        socket.registerMessageHandler((data) => {
            console.log(data);
            let message = new ChatMessage(data);
            this.chatList.drawMessage(message.serialize());
        });
    }
}
​
//表示每条聊天消息
class ChatMessage {
    constructor ({//可以将默认参数和解构赋值结合起来, 所以app.js里构造函数的最终版长下面这样:这一版代码把传给构造函数的对象里的值提取出来。任何没有赋值的参数都有默认值。
                     message: m,
                     user: u=username,
                     timestamp: t=(new Date()).getTime()
                 }){
        this.message = m;
        this.user = u;
        this.timestamp = t;
    }
    /*ChatMessage类将所有重要信息存储为属性,但是实例还继承了ChatMessage的方法和其他信息,这使得ChatMessage的实例不适合通过WebSocket发送。因此,需要一个信息的简化版。*/
    serialize() {
        return {
            user: this.user,
            message: this.message,
            timestamp: this.timestamp
        };
    };//添加一个序列化 (serialize) 方法,用于将ChatMessage里的属性转化成一个简单的JavaScript对象。
}
​
// new ChatApp();
export default ChatApp; //现在开始使用ES6模块系统。在ES6模块中,必须明确地导出想让别人使用的模块代码。这段代码指定ChatApp就是app模块里面可用的默认值。 其他模块可能会导出多个值。 如果只需要导出一个值, 最好使用export default。

代码展示

图片展示如上方格式化和更新消息时间戳提及~

storage.js

import $ from 'jquery';
import md5 from 'crypto-js/md5'; //用JavaScript创建字符串时,经常需要将字符串与其他值进行拼接。为了创建包含表达式和变量值的字符串, ES6 提供了更好的方法一一模板字符串。接下来使用这个功能创建访问Gravatar图片的URL。在dom.js中添加另一个import声明,导人crypto-js库的子模块md5,使用/来分隔主模块和子模块的名称。
import moment from 'moment';
​
/*它接受一个用户名,用来生成一个MD5的哈希值,井返回Gravatar的URL 。*/
function createGravatarUrl(username) {
    let userhash = md5(username);
    return `http://www.gravatar.com/avatar/${userhash.toString()}`;//注意:符号并不是单引号, 而是反引号。在反引号里可以直接在字符串中包含JavaScript表达式的值,可以包含任何表达式。
}
​
/*请求用户输入他们的用户名。将其添加到export ,而不是作为ChatForm或者。ChatList的一部分。*/
export function promptForUsername() {
    let username = prompt('Enter a username');//在创建一个let变量,用以保存用户输入的文字。( prompt函数是浏览器内置的,它会返回一个字符串。)
    return username.toLowerCase(); //然后返回转换成小写格式的文字。
}
/*ChatForm负责向外发送聊天消息。管理DOM里的表单元素
定义ChatForm, 它的构造函数接受选择器参数。在这个构造函数中,为实例需要追踪的元素添加属性。
使用命名的导出(named export)方式来导出多个命名值, 而不是导出一个默认值。通过在class声明前面添加export关键字, 导出ChatForm类。 当用户使用该模块时, 便可通过类名访问到它。*/
export class ChatForm {
    constructor(formSel, inputSel) {
        this.$form = $(formSel);
        this.$input = $(inputSel);
    }
​
    init(submitCallback) {
        this.$form.submit((event) => {//init方法中的提交事件处理程序用到了箭头函数。该箭头函数阻止了表单默认的提交行为,取得表单输入框的值并将其传递给submitCallback, 最终重置输入框的值。
            event.preventDefault();
            let val = this.$input.val();
            submitCallback(val);
            this.$input.val('');
        });
        this.$form.find('button').on('click', () => this.$form.submit());//为确保点击按钮时提交表单, 添加一个点击事件处理程序, 让表单触发submit事件。为实现这一过程,用jQuery获取表单元素,然后调用jQuery的submit方法。这里用到了箭头函数的单个表达式版本,可以省掉大括号。
    }
}
​
/*在服务端发来新消息时,将其展示出来。第二个类ChatList, 用来向用户展示聊天消息列表。ChatList 会给每条消息创建DOM元素,以展示发送该消息的用户名以及消息内容。 */
export class ChatList {
    /*ChatList接受属性选择器和用户名作为参数。属性选择器用于决定将创建的消息列表元素添加到哪个元素,用户名用于区分发送消息的是当前用户还是其他人。(当前用户的消息和别人发送的消息会区分展示。)*/
    constructor(listSel, username) {
        this.$list = $(listSel);
        this.username = username;
    }
​
    /*需要为消息创建DOM元素。给Chat List 添加一个drawMessage方法。它接受一个对象参数,并将该参数解构成本地变量,用来表示用户名、时间戳以及消息内容。(为了解释解构赋值,下面的例子使用单字符的本地变量。)*/
    drawMessage({user: u, timestamp: t, message: m}) {
        /*创建一行消息,包括用户名、时间戳和消息内容本身。*/
        let $messageRow = $('<li>', {
            'class': 'message-row'
        });
​
        /*假如当前用户是消息的发送者,对应的消息元素会有一个额外的CSS类名用来区分样式。*/
        if (this.username === u) {
            $messageRow.addClass('me');
        }
​
        let $message = $('<p>');
​
        $message.append($('<span>', {
            'class': 'message-username',
            text: u
        }));
​
        /*为了保证用户可以马上看到一个可读的时间戳,更新drawMessage。在初次将消息绘制到聊天列表时,使用moment 创建一个格式化的时间戳字符串。*/
        $message.append($('<span>', {
            'class': 'timestamp',
            'data-time': t,
            text: moment(t).fromNow()
        }));
​
        $message.append($('<span>', {
            'class': 'message-message',
            text: m
        }));
​
        let $img = $('<img>', {
            src: createGravatarUrl(u),
            title: u
        });
​
        /*ChatList的列表元素中,并且将新消息行滚动到可视区域。*/
        $messageRow.append($img);
        $messageRow.append($message);
        this.$list.append($messageRow);
        $messageRow.get(0).scrollIntoView();
    }
​
    /*每条消息都将时间戳存储成了数据属性。为ChatList写一个init方法,用来调用内置函数setInterval。这个函数接受两个参数:要运行的函数,和这个函数多久运行一次。这个函数将会更新每条消息,将时间戳转换成用户可读的格式。*/
    init() {
        this.timer = setInterval(() => {
            $('[data-time]').each((idx, element) => {
                let $element = $(element);
                /*为了设置时间戳字符串,用jQuery查找所有带有data-time属性的元素,这个属性的值都是数字化的时间戳。使用这个数字化的时间戳创建一个新的Date对象*/
                let timestamp = new Date().setTime($element.attr('data-time'));
                /*将该对象传给moment,然后调用fromNow方法处理最终的时间戳字符串*/
                let ago = moment(timestamp).fromNow();
                /*并将结果字符串设为元素的HTML文本。*/
                $element.html(age);
            });
        }, 1000);//这个函数会每1000 毫秒执行一次。
    }
}

app.js

import socket from './ws-client'; //接下来要在app.js中导入ws-client模块提供的值。添加一个导入声明。socket就是从ws-client.js中导出的对象。
import {UserStore} from './storage'; //将UserStore导入到app.js中
import {ChatForm, ChatList, promptForUsername} from './dom'; //导入ChatForm类(命名的导入)
​
//为表单选择器和消息输入框选择器创建常量。
const FORM_SELECTOR = '[data-chat="chat-form"]';
const INPUT_SELECTOR = '[data-chat="message-input"]';
const LIST_SELECTOR = '[data-chat="message-list"]';
​
//创建一个实例,用这个实例来存储用户名
let userStore = new UserStore('x-chattrbox/u');
let username = userStore.get();
if (!username) {
    username = promptForUsername();//调用 promptForUsername函数, 获取username变量的值
    userStore.set(username);
}
​
class ChatApp { //构建Chattrbox客户端程序要用到的第一个ES6特性是class关键字。请牢记,ES6的class关键字跟其他编程语言里的类并不完全一样,ES6的类只是为构造函数和prototype方法提供了一种简写的语法。
    constructor() { //每当实例化一个类时,都会执行constructor方法。通常,构造函数会给实例的属性赋值。接着,在app.js中的ChatApp类声明之后创建一个ChatApp实例。
        this.chatForm = new ChatForm(FORM_SELECTOR, INPUT_SELECTOR);//创建一个ChatForm的实例
        this.chatList = new ChatList(LIST_SELECTOR, username);//实例化一个新的Chatlist
​
        socket.init('ws://localhost:3001'); //调用socket.init方法,将Web Socket服务器的URL传进去。
        socket.registerOpenHandler(() => { //调用socket.init之后,调用registerOpenHandler和 registerMessageHandler, 为它们传递箭头函数。
            this.chatForm.init((data) => { //一定要在socket连接打开之后初始化,而不能一创建实例就初始化。这种等待能避免用户输入了聊天消息却不能发送到服务器的情况。(毕竟如果消息发不出去, 用户体验会很糟糕。)记得要给ChatForm的init方法传一个回调函数, 用来处理表单的提交。给它传一个回调函数,将来自ChatForm的消息数据发给socket。
                let message = new ChatMessage({message: data});
                socket.sendMessage(message.serialize());
            });
            this.chatList.init();
        });
        /*用接收的数据创建一个新的ChatMessage ,然后对消息进行序列化。这一步属于预防措施,用于去除数据里可能存在的多余元数据。根据socket数据创建一个新的ChatMessage用以提供消息*/
        socket.registerMessageHandler((data) => {
            console.log(data);
            let message = new ChatMessage(data);
            this.chatList.drawMessage(message.serialize());
        });
    }
}
​
//表示每条聊天消息
class ChatMessage {
    constructor ({//可以将默认参数和解构赋值结合起来, 所以app.js里构造函数的最终版长下面这样:这一版代码把传给构造函数的对象里的值提取出来。任何没有赋值的参数都有默认值。
                     message: m,
                     user: u=username,
                     timestamp: t=(new Date()).getTime()
                 }){
        this.message = m;
        this.user = u;
        this.timestamp = t;
    }
    /*ChatMessage类将所有重要信息存储为属性,但是实例还继承了ChatMessage的方法和其他信息,这使得ChatMessage的实例不适合通过WebSocket发送。因此,需要一个信息的简化版。*/
    serialize() {
        return {
            user: this.user,
            message: this.message,
            timestamp: this.timestamp
        };
    };//添加一个序列化 (serialize) 方法,用于将ChatMessage里的属性转化成一个简单的JavaScript对象。
}
​
// new ChatApp();
export default ChatApp; //现在开始使用ES6模块系统。在ES6模块中,必须明确地导出想让别人使用的模块代码。这段代码指定ChatApp就是app模块里面可用的默认值。 其他模块可能会导出多个值。 如果只需要导出一个值, 最好使用export default。

storage.js

class Store {
    /*新的Store类是通用类, 它既可以搭配localStorage使用, 也可以搭配sessionStorag叶吏用。它只是简单地封装了一下Web Storage API。在实例化这个类的时候,可以指定使用哪个StorageAPI。*/
    constructor(storageApi) {
        this.api = storageApi; //注意,没有在构造函数中设置对this.key的引用,因为Store类并不需要给自己提供存储的数据。相反,它用来创建定义key属性的子类。
    }
    get() {
        return this.api.getItem(this.key);
    }
​
    set(value) {
        this.api.setItem(this.key, value);
    }
}
​
/*使用extends 关键字创建一个子类,用于在sessionStorage 中保存用户名。app.js将会用到UserStore ,所以需要把它导出。*/
export class UserStore extends Store {
    constructor(key) {
        super(sessionStorage);
        this.key = key;
    }//UserStoore 只定义了一个构造函数,它执行两个操作。首先,调用super,这一步会调用Store的构造函数,并传入一个对sessionStorage的引用。然后,给this.key设置一个值。现在Store类的api 的值已经设置好了, UserStore实例的key 的值也设置好了。所有代码已经准备就绪, UserStore实例可以调用get和set方法了。
}

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yoyolee115956

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值