摘要:为指定事件注册一个监听器,接受一个字符串和一个回调函数。发射事件,传递若干可选参数到事件监听器的参数表。为指定事件注册一个单次监听器,即监听器最多只会触发一次,触发后立刻解除该监听器。
1.Node.js 简介
Node.js 其实就是借助谷歌的 V8 引擎,将桌面端的 js 带到了服务器端,它的出现我将其归结为两点:
V8 引擎的出色;
js 异步 io 与事件驱动给服务器带来极高的 吞吐量/硬件性能 比例。
2.安装和配置Node.js安装的话基本是分为 Windows 和 POSIX(为*unx 和 Mac等系统的统称)。
2.1.在 Windows 上,很简单,访问官网,下载对应的安装包安装即可。 2.2.在 POSIX 上安装大都可以从相应的包管理器上进行安装(非大神不推荐用源码,因为源码编译涉及相关参数)。
不过包管理器上的并非是最新的,还可能是很老旧的版本,所以需要我们去官网下载编译好的二进制来进行安装.
关于安装,我在我的其他文章中有说到,比较简单的.
在安装的时候,会默认自带一个 npm 包管理器,这个 npm 上托管了几乎所有的 node.js 程序包,你能够通过它下载各种流行的基于 node.js 的软件.
当然,快速入门少不了 Hello World,Node.js其实和浏览器上的 javascript 差不太多,只是Node.js上增加了一些引用的方法以及结构,并且去除了 Dom 的操作(Node上并没有Dom可以操作..),所以,你可以直接用以下语句打印出一句"Hello World":
console.log("Hello World");3.2.建立一个Web Server
Node.js是一个服务器程序,当然就要提供Web服务,以下代码能让你建立起一个最基础的Web服务器,因为它对所有的请求都只返回同样的页面:
//app.js var http = require("http"); http.createServer(function(req, res) { res.writeHead(200, {"Content-Type": "text/html"}); res.write("3.3.异步IONode.js
"); res.end("Hello World
"); }).listen(3000); console.log("HTTP server is listening at port 3000.");
在js 中,最重要的部分之一就是它的异步编程,通过事件轮询,在单线程上它能够接受更多的请求并且将资源最大化利用.
比如像下面的读取文件程序,在调用读取函数后,程序会继续往下运行,等到读取完成后,才会调用读取函数所绑定的那个回调函数:
//readfilesync.js var fs = require("fs"); var data = fs.readFileSync("file.txt", "utf-8"); console.log(data); console.log("end.");3.4.模块和包 3.4.1.创建模块
node的几乎所有程序都是通过各种模块以及包组合完成的.每个模块或者包都有他特殊的功能以及特定的调用方法.
创建及加载模块:
// 让我们以一个例子来了解模块。创建一个 module.js 的文件,内容是: //module.js var name; exports.setName = function(thyName) { name = thyName; }; exports.sayHello = function() { console.log("Hello " + name); }; // 在同一目录下创建 getmodule.js,内容是: //getmodule.js var myModule = require("./module"); myModule.setName("BYVoid"); myModule.sayHello();3.4.2单次加载
在同一个程序中,同一个模块只会被加载一次,node会通过将模块的地址进行 hash并存起来,所以不会加载多次.当然这样的话,你在程序中引用两次同一个模块,他们其实是一个.
3.4.3.覆盖exports模块通过 exports 暴露相关的方法或者属性,未通过此方法暴露的方法和属性是无法被外界访问到的(和java C++ C# 中的类差不多)
但是,你不能直接通过以下方式来覆盖export函数,这样的话不会有任何效果
export = function(){ console.log("Hello"); }
当我们只需要暴露一个方法或属性的时候,用exports 就略显复杂,我么可以通过以下方式将 exports 进行覆盖:
module.exports = function(){ console.log("Hello"); }3.5.创建包
每个包中有很多的模块,因为之前我们知道,每个模块最好是多带带存放在一个js文件中,那包我们就应该存放在一个文件夹中.并且提供一个外部访问的接口(通常是一个模块),用于整体管理及使用这个包的所有功能.
Node.js 在调用某个包时,会首先检查包中 package.json 文件的 main 字段,将其作为包的接口模块,如果 package.json 或 main 字段不存在,会尝试寻找 index.js 或 index.node 作
为包的接口。
package.json 是 CommonJS 规定的用来描述包的文件,完全符合规范的package.json 文件应该含有以下字段。
name:包的名称,必须是唯一的,由小写英文字母、数字和下划线组成,不能包含
空格。
description:包的简要说明。
version:符合语义化版本识别①规范的版本字符串。
keywords:关键字数组,通常用于搜索。
maintainers:维护者数组,每个元素要包含 name、email (可选)、web (可选)
字段。
contributors:贡献者数组,格式与maintainers相同。包的作者应该是贡献者
数组的第一个元素。
bugs:提交bug的地址,可以是网址或者电子邮件地址。
licenses:许可证数组,每个元素要包含 type (许可证的名称)和 url (链接到
许可证文本的地址)字段。
repositories:仓库托管地址数组,每个元素要包含 type (仓库的类型,如 git )、
url (仓库的地址)和 path (相对于仓库的路径,可选)字段。
dependencies:包的依赖,一个关联数组,由包名称和版本号组成。
下面是一个完全符合 CommonJS 规范的 package.json 示例:
{ "name": "mypackage", "description": "Sample package for CommonJS. This package demonstrates the required elements of a CommonJS package.", "version": "0.7.0", "keywords": [ "package", "example" ], "maintainers": [ { "name": "Bill Smith", "email": "bills@example.com", } ], "contributors": [ { "name": "BYVoid", "web": "http://www.byvoid.com/" } ], "bugs": { "mail": "dev@example.com", "web": "http://www.example.com/bugs" }, "licenses": [ { "type": "GPLv2", "url": "http://www.example.org/licenses/gpl.html" } ], "repositories": [ { "type": "git", "url": "http://github.com/BYVoid/mypackage.git" } ], "dependencies": { "webkit": "1.2", "ssl": { "gnutls": ["1.0", "2.0"], "openssl": "0.9.8" } } }3.6 Node.js包管理器
通过使用Node.js的包管理器,你可以下载到几乎所有的js包.
获取一个包如下
npm install express
以上代码将会将express安装到当前项目的文件中
如果要将其安装在全局(当前电脑一次安装,所有地方都可以使用),可以在参数中加入 -g,如果是要将其加入项目依赖,可以加入以下参数 --save-dev,这样node会自动将当前包的信息写入到项目的 package.json 中.
这个略过,并不打算发布,等需要时看也不迟
3.8.调试 3.8.1.命令行调试调试其实方法很多,可以直接在命令行中进行单步调试,以下是nodejs支持的调试命令:
命令 | 功能 |
---|---|
run | 执行脚本,在第一行暂停 |
restart | 重新执行脚本 |
cont, c | 继续执行,直到遇到下一个断点 |
next, n | 单步执行 |
step, s | 单步执行并进入函数 |
out, o | 从函数中步出 |
setBreakpoint(), sb() | 在当前行设置断点 |
setBreakpoint(‘f()’), sb(...) | 在函数f的第一行设置断点 |
setBreakpoint(‘script.js’, 20), sb(...) | 在 script.js 的第20行设置断点 |
clearBreakpoint, cb(...) | 清除所有断点 |
backtrace, bt | 显示当前的调用栈 |
list(5) | 显示当前执行到的前后5行代码 |
watch(expr) | 把表达式 expr 加入监视列表 |
unwatch(expr) | 把表达式 expr 从监视列表移除 |
watchers | 显示监视列表中所有的表达式和值 |
repl | 在当前上下文打开即时求值环境 |
kill | 终止当前执行的脚本 |
scripts | 显示当前已加载的所有脚本 |
version | 显示 V8 的版本 |
这个程序非常好用,虽然我没有用过,但是看上去很像谷歌浏览器的调试,以后需要时可以试试看.
4.Node.js核心模块 4.1.全局对象在Node中,有一个类似在浏览器中的window一样的全局对象,叫做 global. console,process等都是其子.
如何定义全局对象就很简单了:直接绑定在global上的,定义在最外层的,还有没有使用var声明的都是全局对象.
4.1.1.process对象这个对象存储了与当前运行环境相关的很多信息,并且也是标准输入输出的最底层接口,我们在调用console.log()时,其背后也是通过process来实现的.
其含有以下几个对象
process.argv 包含当前环境数据
process.stdout 标准输出接口
process.stdin 标准输入接口
process.nextTick(callback)的功能是为事件循环设置一项任务,Node.js 会在
下次事件循环调响应时调用 callback。这个可以用在将两个大型的耗时操作进行拆散.
4.1.2.console对象这个对象是和我们打交道最多的对象,其含有以下方法:
console.log() 向标准输出接口打印字符,其有多个参数,当只有一个参数时,会将当前参数转换为字符串并打印出来.
console.error()这个和刚才的log很相似,只不过这个是标准错误输出,也是就是制造一个bug!!!!
console.trace() 向标准错误输出流输出当前调用栈
4.2 常用工具util 4.2.1 util.inherits这个方法提供了一个方便的途径让我们进行继承操作,用法如下
util.inherits(constructor, superConstructor)
将两个对象进行继承操作
注意,此处继承只会继承原型中定义的属性和方法!!并且 console.log() 并不会打印出原型中的属性或者方法.
4.2.2.util.inspectutil.inspect(object,[showHidden],[depth],[colors]) 是一个将任一对象转换为字符串的方法.它不会调用对象的 toString 方法.
除了以上我们介绍的几个函数之外, util还提供了util.isArray()、 util.isRegExp()、
util.isDate()、util.isError() 四个类型测试工具,以及 util.format()、util.debug() 等工具。
events 是 node最重要的模块.因为node整个运行就是基于事件轮询的.
4.3.1.事件发生器events只提供了一个对象: events.EventEmitter.这个对象的核心就是封装事件功能.下面的代码向我们演示了这个过程:
var events = require("events"); var emitter = new events.EventEmitter(); emitter.on("someEvent", function(arg1, arg2) { console.log("listener1", arg1, arg2); }); emitter.on("someEvent", function(arg1, arg2) { console.log("listener2", arg1, arg2); }); emitter.emit("someEvent", "byvoid", 1991);
这就是EventEmitter最简单的用法。接下来我们介绍一下EventEmitter常用的API。
EventEmitter.on(event, listener) 为指定事件注册一个监听器,接受一个字
符串 event 和一个回调函数 listener。
EventEmitter.emit(event, [arg1], [arg2], [...]) 发射 event 事件,传
递若干可选参数到事件监听器的参数表。
EventEmitter.once(event, listener) 为指定事件注册一个单次监听器,即
监听器最多只会触发一次,触发后立刻解除该监听器。
EventEmitter.removeListener(event, listener) 移除指定事件的某个监听
器,listener 必须是该事件已经注册过的监听器。
EventEmitter.removeAllListeners([event]) 移除所有事件的所有监听器,
如果指定 event,则移除指定事件的所有监听器。
4.3.2. error事件EventEmitter定义了一个特殊的叫 error 的事件.当error被触发时,如果没有定义的响应监听器,则会退出程序并打印调用栈.
所以我们要发射 error 时,必须要为其设置相应的监听器,来捕获并处理错误,这样才不会导致程序挂掉.
4.3.3.继承EventEmitter一般情况下,我们不会直接用到这个对象,而是在继承中使用它.包括fs,http等都是这样的操作.
为什么要这样做呢?原因有两点。首先,具有某个实体功能的对象实现事件符合语义,
事件的监听和发射应该是一个对象的方法。其次 JavaScript 的对象机制是基于原型的,支持部分多重继承,继承 EventEmitter 不会打乱对象原有的继承关系。
fs 模块是文件操作的封装,它提供了文件的读取、写入、更名、删除、遍历目录、链接等 POSIX 文件系统操作。与其他模块不同的是,fs 模块中所有的操作都提供了异步的和同步的两个版本,例如读取文件内容的函数有异步的 fs.readFile() 和同步的 fs.readFileSync()。我们以几个函数为代表,介绍 fs 常用的功能,并列出 fs 所有函数的定义和功能。
4.4.1fs.readFile这个函数能够读取文件中的内容,其用法为:
fs.readFile(filename,[encoding],[callback(err,data)])
以下是调用示例:
var fs = require("fs"); fs.readFile("content.txt", "utf-8", function(err, data) { if (err) { console.error(err); } else { console.log(data); } });4.4.2.fs.readFileSync
fs.readFileSync(filename, [encoding])是 fs.readFile 同步的版本。它接受的参数和 fs.readFile 相同,而读取到的文件内容会以函数返回值的形式返回。如果有错误发生,fs 将会抛出异常,你需要使用 try 和 catch 捕捉并处理异常。
4.4.3.fs.openfs.open(path, flags, [mode], [callback(err, fd)])是 POSIX open 函数的封装,与 C 语言标准库中的 fopen 函数类似。它接受两个必选参数,path 为文件的路径,flags 可以是以下值:
r :以读取模式打开文件。
r+ :以读写模式打开文件。
w :以写入模式打开文件,如果文件不存在则创建。
w+ :以读写模式打开文件,如果文件不存在则创建。
a :以追加模式打开文件,如果文件不存在则创建。
a+ :以读取追加模式打开文件,如果文件不存在则创建。
mode 参数用于创建文件时给文件指定权限,默认是 0666。回调函数将会传递一个文
件描述符 fd。
fs.read(fd, buffer, offset, length, position, [callback(err, bytesRead, buffer)])是 POSIX read 函数的封装,相比 fs.readFile 提供了更底层的接口。
以下是其示例:
var fs = require("fs"); fs.open("content.txt", "r", function(err, fd) { if (err) { console.error(err); return; } var buf = new Buffer(8); fs.read(fd, buf, 0, 8, null, function(err, bytesRead, buffer) { if (err) { console.error(err); return; } console.log("bytesRead: " + bytesRead); console.log(buffer); }) });
当然,fs中的函数是很多的,如果需要,可以去网上进行更详细的查阅.
4.5 HTTP 服务器与客户端其实在node中就已经封装了一个很底层的http模块,http.Server,几乎所有的网络工作都能通过它来完成.
4.5.1 HTTP服务器以下代码
//app.js var http = require("http"); http.createServer(function(req, res) { res.writeHead(200, {"Content-Type": "text/html"}); res.write("Node.js
"); res.end("Hello World
"); }).listen(3000); console.log("HTTP server is listening at port 3000.");
会建立一个在3000端口监听的程序
4.5.2 GET 请求//httpserverrequestget.js var http = require("http"); var url = require("url"); var util = require("util"); http.createServer(function(req, res) { res.writeHead(200, {"Content-Type": "text/plain"}); res.end(util.inspect(url.parse(req.url, true))); }).listen(3000);4.5.3 POST 请求
//httpserverrequestpost.js var http = require("http"); var querystring = require("querystring"); var util = require("util"); http.createServer(function(req, res) { var post = ""; req.on("data", function(chunk) { post += chunk; }); req.on("end", function() { post = querystring.parse(post); res.end(util.inspect(post)); }); }).listen(3000);4.5.4 http.serverResponse
服务器不仅要接收数据,也要返回相应的数据给客户端:
http.ServerResponse 是返回给客户端的信息,决定了用户最终能看到的结果。它
也是由 http.Server 的 request 事件发送的,作为第二个参数传递,一般简称为
response 或 res。
http.ServerResponse 有三个重要的成员函数,用于返回响应头、响应内容以及结束
请求。
response.writeHead(statusCode, [headers]):向请求的客户端发送响应头。statusCode 是 HTTP 状态码,如 200 (请求成功)、404 (未找到)等。headers 是一个类似关联数组的对象,表示响应头的每个属性。该函数在一个请求内最多只能调用一次,如果不调用,则会自动生成一个响应头。
response.write(data, [encoding]):向请求的客户端发送响应内容。data 是一个 Buffer 或字符串,表示要发送的内容。如果 data 是字符串,那么需要指定
encoding 来说明它的编码方式,默认是 utf-8。在 response.end 调用之前,
response.write 可以被多次调用。
response.end([data], [encoding]):结束响应,告知客户端所有发送已经完
成。当所有要返回的内容发送完毕的时候,该函数 必须 被调用一次。它接受两个可
选参数,意义和 response.write 相同。如果不调用该函数,客户端将永远处于等待状态。
当然,node还能充当一个http的客户端程序来使用 http 模块提供了两个函数 http.request 和 http.get,功能是作为客户端向 HTTP服务器发起请求。
4.5.5.1 http.requesthttp.request(options, callback) 发起 HTTP 请求。接受两个参数, option 是一个类似关联数组的对象,表示请求的参数, callback是请求的回调函数。 option常用的参数如下所示。
host :请求网站的域名或 IP 地址。
port :请求网站的端口,默认 80。
method :请求方法,默认是 GET。
path :请求的相对于根的路径,默认是“/”。QueryString 应该包含在其中。例如 /search?query=byvoid。
headers :一个关联数组对象,为请求头的内容。
callback 传递一个参数,为 http.ClientResponse 的实例。
http.request 返回一个 http.ClientRequest 的实例。
下面是一个通过 http.request 发送 POST 请求的代码:
//httprequest.js var http = require("http"); var querystring = require("querystring"); var contents = querystring.stringify({ name: "byvoid", email: "byvoid@byvoid.com", address: "Zijing 2#, Tsinghua University", }); var options = { host: "www.byvoid.com", path: "/application/node/post.php", method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", "Content-Length" : contents.length } }; var req = http.request(options, function(res) { res.setEncoding("utf8"); res.on("data", function (data) { console.log(data); }); }); req.write(contents); req.end();4.5.5.2 http.get
http.get(options, callback) http 模块还提供了一个更加简便的方法用于处
理GET请求: http.get。它是 http.request 的简化版,唯一的区别在于http.get
自动将请求方法设为了 GET 请求,同时不需要手动调用 req.end()。
//httpget.js var http = require("http"); http.get({host: "www.byvoid.com"}, function(res) { res.setEncoding("utf8"); res.on("data", function (data) { console.log(data); }); });
post 和 get 返回的数据皆为 http.ClientRequest 对象.其也提供了write和end函数,使用如下:
//httpresponse.js var http = require("http"); var req = http.get({host: "www.byvoid.com"}); req.on("response", function(res) { res.setEncoding("utf8"); res.on("data", function (data) { console.log(data); }); });
其还提供了以下函数:
request.abort():终止正在发送的请求。
request.setTimeout(timeout, [callback]):设置请求超时时间,timeout 为毫秒数。当请求超时以后,callback 将会被调用。
此外还有 request.setNoDelay([noDelay])、request.setSocketKeepAlive
([enable], [initialDelay]) 等函数。
同时也有 http.ClientResponse,其与 http:ServerResponse相似,提供... data,end和close,分别在数据到达,传输结束和链接结束时触发,其中 data 事件传递一个参数 chunk,表示接收到的数据.
http.ClientResponse 还提供了以下几个特殊的函数。
response.setEncoding([encoding]):设置默认的编码,当 data 事件被触发
时,数据将会以 encoding 编码。默认值是 null,即不编码,以 Buffer 的形式存
储。常用编码为 utf8。
response.pause():暂停接收数据和发送事件,方便实现下载功能。
response.resume():从暂停的状态中恢复。
5. 使用Node.js进行Web开发 5.1 使用Express 框架进行开发安装npm install -g express
5.2 建立工程express -t ejs XXXX
接下来的 Nodejs 博客搭建暂未记录,因为涉及到的知识点较多,需要的话最好还是重新读一次.
6. Node.js 进阶 6.1 模块加载机制在node中,共有两种类型的模块:核心模块,文件模块.核心模块是由node自带的一些基础模块,具有最高的加载优先级,文件模块则是我们自己创建的或者引用的三方模块.
6.1.1 按路径查找如果指定了加载路径,则有以下两种查找情况
当未指定模块名称,而只是制定了文件夹时,node会自动去制定目录寻找index文件,而index文件的文件类型,按加载顺序则有三种:js,json,node.如果这三种类型的index文件都没有找到,则报错.
当指定了文件名称,但是未指定文件后缀时,node也是按照以上的顺序进行查找:js,json,node.
6.1.2 通过查找node_modules当我们没有指定路径时,node会在node_moudule目录中去查找,有趣的是,在这里有一个很大的性能问题:
比如你在/home/aiello/develop/foot.js中使用require("bar.js"),node会在当前目录的node_modules文件夹中寻找该模块,如果未找到,则会重新往上一级查找,于是node到/home/aiello/develop这级目录进行查找,如果还没有它会继续往上进行查找,知道找到根目录,发现没有,于是报错.
能明显看出来,node在模块的查找中非常的费时,所以我们应当将模块放到距离当前应用最近的目录,并且最好是指定模块路径.
6.2 控制流在循环中使用异步方法,将会遇到
//forloop.js var fs = require("fs"); var files = ["a.txt", "b.txt", "c.txt"]; for (var i = 0; i < files.length; i++) { fs.readFile(files[i], "utf-8", function(err, contents) { console.log(files[i] + ": " + contents); }); }
这是由于回调函数太慢了,早都循环完了,才会调用到回调函数,所以最后回调函数中的i都是等于循环结束时的i(不一定),所以会有相关问题,以下代码将其做一个闭包封装,以达到理想效果:
//forloopclosure.js var fs = require("fs"); var files = ["a.txt", "b.txt", "c.txt"]; for (var i = 0; i < files.length; i++) { (function(i) { fs.readFile(files[i], "utf-8", function(err, contents) { console.log(files[i] + ": " + contents); }); })(i); }6.3 Node.js 应用部署
普通 Node.js 应用存在以下问题:
不支持故障恢复
没有日志
无法利用多核提高性能
独占端口
需要手动启动
6.3.1 日志功能这个简单,不做概述
6.3.2 故障恢复使用cluster模块,可以实现多进程以及主进程与工作进程的结构.
6.3.3 自动启动制作一个启动脚本即可
6.3.4 共享端口虚拟主机即可
6.4 Node 不是银弹so,这个我知道,他不适合做
计算密集型
单用户多任务
逻辑复杂的事务
Unicode与国际化
起始位置
PDF 154
页码 148
起始位置:
PDF 173
页码 168
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/92433.html
摘要:原文地址一个非常适合入门学习的博客项目前端掘金一个非常适合入门学习的项目,代码清晰结构合理新闻前端掘金介绍一个由编写的新闻。深入浅出读书笔记知乎专栏前端专栏前端掘金去年的一篇老文章,恰好今天专栏开通,迁移过来。 破解前端面试(80% 应聘者不及格系列):从闭包说起 - 掘金修订说明:发布《80% 应聘者都不及格的 JS 面试题》之后,全网阅读量超过 6W,在知乎、掘金、cnodejs ...
摘要:原文地址一个非常适合入门学习的博客项目前端掘金一个非常适合入门学习的项目,代码清晰结构合理新闻前端掘金介绍一个由编写的新闻。深入浅出读书笔记知乎专栏前端专栏前端掘金去年的一篇老文章,恰好今天专栏开通,迁移过来。 破解前端面试(80% 应聘者不及格系列):从闭包说起 - 掘金修订说明:发布《80% 应聘者都不及格的 JS 面试题》之后,全网阅读量超过 6W,在知乎、掘金、cnodejs ...
摘要:前端日报精选读书思考一的计算属性使用开发调试开发者控制台中,你可能意想不到的功能中字符串转数字的陷阱和示例中文设计模式单例模式个人文章设计模式工厂模式个人文章读书思考二掘金网络基础三传输层的笔记学习笔记中的属性学习 2017-10-08 前端日报 精选 Node.js Design Patterns - Second Edition读书思考(一)Vue的计算属性_Vue使用typesc...
摘要:阮一峰老师开源作品。书上的示例代码可以通过在线网站代码调试工具调试。 阮一峰老师开源作品。 书上的示例代码可以通过 在线网站代码调试工具 JS Bin 调试。 作用域 作用域链 每个变量或函数通过执行环境 (execution context) 定义了其有权访问的其他数据,决定了他们各自的行为; 全局执行环境是最顶层的执行环境,根据宿主环境的不同,表示全局执行环境的对象也不同:在浏览...
摘要:所以经常会在一个源码中看到写法吧立即执行函数创建变量,保存全局根变量。 // ================立即执行函数================ // 使用(function(){}())立即执行函数,减少全局变量 // ----????----函数声明 function (){} 与函数表达式 var funName = function(){}----????---- /...
阅读 869·2021-11-15 11:38
阅读 2480·2021-09-08 09:45
阅读 2792·2021-09-04 16:48
阅读 2536·2019-08-30 15:54
阅读 911·2019-08-30 13:57
阅读 1591·2019-08-29 15:39
阅读 479·2019-08-29 12:46
阅读 3459·2019-08-26 13:39