node.js概述
简介
-
单线程却能并发
-
JavaScript最早是运行在浏览器中,然而浏览器只是提供了一个上下文,它定义了使用JavaScript可以做什么,
-
Node.js事实上就是另外一种上下文,它允许在后端(脱离浏览器环境)运行JavaScript代码。
-
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。
-
Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。
-
Node.js 的包管理器 npm,是全球最大的开源库生态系统。
-
Node 的 module.exports 实在巧妙,通过局部 mutable 变量 module 的穿梭,无缝地使 JS 得以模块化。
-
Node 能够如此方便地实现高并发以及并发取数据这两点,在某些应用场景下是很具优势的。少数的一些 CPU 密集场景,通过 add-on 的方式也很容易转为异步解决。
Node.js是事件驱动的
- Node通过事件驱动的方式处理请求时无需为每一个请求创建额外的线程。
- 在事件驱动的模型当中,每一个IO工作被添加到事件队列中,线程循环地处理队列上的工作任务,当执行过程中遇到来堵塞(读取文件、查询数据库)时,线程不会停下来等待结果,而是留下一个处理结果的回调函数,转而继续执行队列中的下一个任务。
- 这个传递到队列中的回调函数在堵塞任务运行结束后才被线程调用。
路由
- 我们要为路由提供请求的URL和其他需要的GET及POST参数,随后路由需要根据这些数据来执行相应的代码
- 我们需要查看HTTP请求,从中提取出请求的URL以及GET/POST参数。
- 我们需要的所有数据都会包含在request对象中,该对象作为onRequest()回调函数的第一个参数传递。但是为了解析这些数据,我们需要额外的Node.JS模块,它们分别是url和querystring模块。
url.parse(string).query
|
url.parse(string).pathname |
| |
| |
------ -------------------
http://localhost:8888/start?foo=bar&hello=world
--- -----
| |
| |
querystring(string)["foo"] |
|
querystring(string)["hello"]
工具
- npm:NodeJs包管理器
- express:服务器端比较流行的MVC框架,处理服务请求,路由转发,逻辑处理
- mongoose:mongodb包装,更方便使用数据库
-
socket.io
:实现服务端和客户端socket通信解决方案 - koa: 由 Express原班人马打造的,致力于成为一个更小、更富有表现力、更健壮的 Web 框架。
- bckbone:客户端MVC框架,编写客户端应用
- coffeescript:提高JavaScript的可读性,健壮性
- zombie:浏览器子集,编写html解析器,轻形javascript客户端测试
框架 Express
-
express 是 Node.js 应用最广泛的 web 框架,现在是 4.x 版本,它非常薄。跟 Rails 比起来,完全两个极端。
-
express 的官网是 http://expressjs.com/ ,我常常上去看它的 API。
-
创建应用文件,写入以下代码
// 这句的意思就是引入 `express` 模块,并将它赋予 `express` 这个变量等待使用。
var express = require('express');
// 调用 express 实例,它是一个函数,不带参数调用时,会返回一个 express 实例,将这个变量赋予 app 变量。
var app = express();
// app 本身有很多方法,其中包括最常用的 get、post、put/patch、delete,在这里我们调用其中的 get 方法,为我们的 `/` 路径指定一个 handler 函数。
// 这个 handler 函数会接收 req 和 res 两个对象,他们分别是请求的 request 和 response。
// request 中包含了浏览器传来的各种信息,比如 query 啊,body 啊,headers 啊之类的,都可以通过 req 对象访问到。
// res 对象,我们一般不从里面取信息,而是通过它来定制我们向浏览器输出的信息,比如 header 信息,比如想要向浏览器输出的内容。这里我们调用了它的 #send 方法,向浏览器输出一个字符串。
app.get('/', function (req, res) {
res.send('Hello World');
});
// 定义好我们 app 的行为之后,让它监听本地的 3000 端口。这里的第二个函数是个回调函数,会在 listen 动作成功后执行,我们这里执行了一个命令行输出操作,告诉我们监听动作已完成。
app.listen(3000, function () {
console.log('app is listening at port 3000');
});
- 执行
$ node app.js
- 这时候我们的 app 就跑起来了,终端中会输出
app is listening at port 3000。
这时我们打开浏览器,访问http://localhost:3000/,
会出现Hello World
。如果没有出现的话,肯定是上述哪一步弄错了,自己调试一下。
补充知识
-
端口
-
端口的作用:通过端口来区分出同一电脑内不同应用或者进程,从而实现一条物理网线(通过分组交换技术-比如internet)同时链接多个程序Port_(computer_networking)
-
端口号是一个 16位的 uint, 所以其范围为 1 to 65535 (对TCP来说, port 0 被保留,不能被使用. 对于UDP来说, source端的端口号是可选的, 为0时表示无端口).
-
app.listen(3000)
,进程就被打标,电脑接收到的3000端口的网络消息就会被发送给我们启动的这个进程
-
-
URL
-
RFC1738 定义的url格式笼统版本
<scheme>:<scheme-specific-part>
, scheme有我们很熟悉的http、https、ftp,以及著名的ed2k,thunder。 -
通常我们熟悉的url定义成这个样子
-
<scheme>://<user>:<password>@<host>:<port>/<url-path>
-
用过ftp的估计能体会这么长的,网页上很少带auth信息,所以就精简成这样:
-
<scheme>://<host>:<port>/<url-path>
-
- 在上面的例子中, scheme=http, host=localhost, port=3000, url-path=/,
- 再联想对照一下浏览器端window.location对象。
-
package.json
-
package.json 文件就是定义了项目的各种元信息,包括项目的名称,
git repo
的地址,作者等等。最重要的是,其中定义了我们项目的依赖,这样这个项目在部署时,我们就不必将node_modules
目录也上传到服务器,服务器在拿到我们的项目时,只需要执行npm install
,则 npm 会自动读取package.json
中的依赖并安装在项目的 node_modules 下面,然后程序就可以在服务器上跑起来了。 -
npm init
创建最简单的package.json文件 -
安装依赖项(express 和 utility)
$ npm install express utility --save
-
需求:当在浏览器中访问 http://localhost:3000/?q=alsotang 时,输出
alsotang
的 md5 值,即bdd5e57b5c0040f9dc23d430846e68a3
。 -
建立一个 app.js 文件,复制以下代码进去:
// 引入依赖
var express = require('express');
var utility = require('utility');
// 建立 express 实例
var app = express();
app.get('/', function (req, res) {
// 从 req.query 中取出我们的 q 参数。
// 如果是 post 传来的 body 数据,则是在 req.body 里面,不过 express 默认不处理 body 中的信息,需要引入 https://github.com/expressjs/body-parser 这个中间件才会处理,这个后面会讲到。
// 如果分不清什么是 query,什么是 body 的话,那就需要补一下 http 的知识了
var q = req.query.q;
// 调用 utility.md5 方法,得到 md5 之后的值
// 之所以使用 utility 这个库来生成 md5 值,其实只是习惯问题。每个人都有自己习惯的技术堆栈,
// 我刚入职阿里的时候跟着苏千和朴灵混,所以也混到了不少他们的技术堆栈,仅此而已。
// utility 的 github 地址:https://github.com/node-modules/utility
// 里面定义了很多常用且比较杂的辅助方法,可以去看看
var md5Value = utility.md5(q);
res.send(md5Value);
});
app.listen(3000, function (req, res) {
console.log('app is running at port 3000');
});
- 运行我们的程序
$ node app.js
访问 http://localhost:3000/?q=alsotang
,完成。
-
如果直接访问
http://localhost:3000/
会抛错,这个错误是从crypto.js
中抛出的。 -
这是因为,当我们不传入 q 参数时,
req.query.q
取到的值是undefined
,utility.md5
直接使用了这个空值,导致下层的crypto
抛错。
使用 superagent 与 cheerio 完成简单爬虫
知识点
- 学习使用 superagent 抓取网页
- 学习使用 cheerio 分析网页
知识内容
- 利用异步并发爬网站
- 需要用到三个依赖,分别是 express,superagent 和 cheerio。
- superagent : 是个 http 方面的库,可以发起 get 或 post 请求。
- cheerio : 大家可以理解成一个 Node.js 版的 jquery,用来从网页中以 css selector 取数据,使用方式跟 jquery 一样一样的。
新建项目
- 新建一个文件夹,进去之后 npm init
- 安装依赖 npm install --save PACKAGE_NAME
- 写应用逻辑
要求
-
建立一个 lesson3 项目,在其中编写代码。
-
当在浏览器中访问 http://localhost:3000/ 时,输出 CNode(https://cnodejs.org/ ) 社区首页的所有帖子标题和链接,以 json 的形式。
输出示例:
[ { "title": "【公告】发招聘帖的同学留意一下这里", "href": "http://cnodejs.org/topic/541ed2d05e28155f24676a12" }, { "title": "发布一款 Sublime Text 下的 JavaScript 语法高亮插件", "href": "http://cnodejs.org/topic/54207e2efffeb6de3d61f68f" } ]
app.get('/', function (req, res, next) {
// 用 superagent 去抓取 https://cnodejs.org/ 的内容
superagent.get('https://cnodejs.org/')
.end(function (err, sres) {
// 常规的错误处理
if (err) {
return next(err);
}
// sres.text 里面存储着网页的 html 内容,将它传给 cheerio.load 之后
// 就可以得到一个实现了 jquery 接口的变量,我们习惯性地将它命名为 `$`
// 剩下就都是 jquery 的内容了
var $ = cheerio.load(sres.text);
var items = [];
$('#topic_list .topic_title').each(function (idx, element) {
var $element = $(element);
items.push({
title: $element.attr('title'),
href: $element.attr('href')
});
});
res.send(items);
});
});
异步并发
- eventproxy
- async
- 当你需要去多个源(一般是小于 10 个)汇总数据的时候,用 eventproxy 方便;当你需要用到队列,需要控制并发数,或者你喜欢函数式编程思维时,使用 async。大部分场景是前者,所以我个人大部分时间是用 eventproxy 的。
eventproxy
var eventproxy = require('eventproxy');
var ep = new eventproxy();
ep.all('data1_event', 'data2_event', 'data3_event', function (data1, data2, data3) {
var html = fuck(data1, data2, data3);
render(html);
});
$.get('http://data1_source', function (data) {
ep.emit('data1_event', data);
});
$.get('http://data2_source', function (data) {
ep.emit('data2_event', data);
});
$.get('http://data3_source', function (data) {
ep.emit('data3_event', data);
});
ep.all('data1_event', 'data2_event', 'data3_event', function (data1, data2, data3) {});
-
这一句,监听了三个事件,分别是
data1_event, data2_event, data3_event
,每次当一个源的数据抓取完成时,就通过ep.emit()
来告诉 ep 自己,某某事件已经完成了。 -
当三个事件未同时完成时,
ep.emit()
调用之后不会做任何事;当三个事件都完成的时候,就会调用末尾的那个回调函数,来对它们进行统一处理。 -
eventproxy
提供了不少其他场景所需的 API,但最最常用的用法就是以上的这种,即:- 先
var ep = new eventproxy();
得到一个eventproxy
实例。
告诉它你要监听哪些事件,并给它一个回调函数。ep.all('event1', 'event2', function (result1, result2) {})
。
在适当的时候ep.emit('event_name', eventData)
。
- 先
-
重复异步协作
作用域与闭包:this,var,(function () {})
- Node 中,全局变量会被定义在 global 对象下;在浏览器中,全局变量会被定义在 window 对象下。
闭包
- 这个概念,在函数式编程里很常见,简单的说,就是使内部函数可以访问定义在外部函数中的变量。
- 闭包的一个坑
- javascript运行环境为单线程,setTimeout注册的函数需要等待线程空闲才能执行,此时for循环已经结束,i值为10.10个定时输出都是10m修改方法:将setTimeout放在函数立即调用表达式中,将i值作为参数传递给包裹函数,创建新闭包
for(var i=0; i<10; i++){
(function (j){
return setTimeout(function(){
console.log(j);
},0)
})(i)
}
this
-
在函数执行时,this 总是指向调用该函数的对象。要判断 this 的指向,其实就是判断 this 所在的函数属于谁。
- 简单的说就是:
- 有对象就指向调用对象
- 没调用对象就指向全局对象
- 用new构造就指向新对象
- 通过 apply 或 call 或 bind 来改变 this 的所指。
- 简单的说就是:
-
1)函数有所属对象时:指向所属对象
-
函数有所属对象时,通常通过 . 表达式调用,这时 this 自然指向所属对象。比如下面的例子:
var myObject = {value: 100}; myObject.getValue = function () { console.log(this.value); // 输出 100 // 输出 { value: 100, getValue: [Function] }, // 其实就是 myObject 对象本身 console.log(this); return this.value; }; console.log(myObject.getValue()); // => 100
-
getValue() 属于对象 myObject,并由 myOjbect 进行 . 调用,因此 this 指向对象 myObject。
-
-
2) 函数没有所属对象:指向全局对象
var myObject = {value: 100}; myObject.getValue = function () { var foo = function () { console.log(this.value) // => undefined console.log(this);// 输出全局对象 global }; foo(); return this.value; }; console.log(myObject.getValue()); // => 100
-
在上述代码块中,foo 函数虽然定义在
getValue
的函数体内,但实际上它既不属于 getValue 也不属于myObject
。foo 并没有被绑定在任何对象上,所以当调用时,它的 this 指针指向了全局对象global
。
-
3)构造器中的 this:指向新对象
- js 中,我们通过 new 关键词来调用构造函数,此时 this 会绑定在该新对象上。
var SomeClass = function(){ this.value = 100; } var myCreate = new SomeClass(); console.log(myCreate.value); // 输出100
- 顺便说一句,在 js 中,构造函数、普通函数、对象方法、闭包,这四者没有明确界线。界线都在人的心中。
-
4) apply 和 call 调用以及 bind 绑定:指向绑定的对象
-
apply() 方法接受两个参数第一个是函数运行的作用域,另外一个是一个参数数组or伪数组(arguments)。
-
call() 方法第一个参数的意义与 apply() 方法相同,只是其他的参数需要一个个列举出来。
-
简单来说,call 的方式更接近我们平时调用函数,而 apply 需要我们传递 Array 形式的数组给它。它们是可以互相转换的。
-
call和apply是立即改变this指向,而bind(jQuery中定义的方法)返回的是改变this指向的函数,需要我们手动调用执行
var myObject = {value: 100}; var foo = function(){ console.log(this); }; foo(); // 全局变量 global foo.apply(myObject); // { value: 100 } foo.call(myObject); // { value: 100 } var newFoo = foo.bind(myObject); newFoo(); // { value: 100 }
-
mongodb
mongodb 的官网是这样介绍自己的:
MongoDB (from "humongous") is an open-source document database, and the leading NoSQL database. Written in C++
-
开源、文档型、nosql。
-
在 mongodb 中,数据的层级是:数据库 -> collection -> document -> 字段。
-
文档型数据这个名字中,“文档”两个字很容易误解。其实这个文档就是 bson 的意思。
-
bson 是 json 的超集,比如 json 中没法储存二进制类型,而 bson 拓展了类型,提供了二进制支持。
-
mongodb 中存储的一条条记录都可以用 bson 来表示。所以你也可以认为,mongodb 是个存 bson 数据的数据库,或是存哈希数据的数据库。
-
mongodb 和 mysql 要我选的话,无关紧要的应用我会选择 mongodb,就当个简单的存 json 数据的数据库来用;如果是线上应用,肯定还是会选择 mysql。毕竟 sql 比较成熟,而且各种常用场景的最佳实践都有先例了。
mongoose
-
mongoose 是个 odm。odm 的概念对应 sql 中的 orm。也就是 ruby on rails 中的 activerecord 那一层。orm 全称是
Object-Relational Mapping
,对象关系映射;而 odm 是Object-Document Mapping
,对象文档映射。 -
它的作用就是,在程序代码中,定义一下数据库中的数据格式,然后取数据时通过它们,可以把数据库中的 document 映射成程序中的一个对象,这个对象有 .save .update 等一系列方法,和 .title .author 等一系列属性。在调用这些方法时,odm 会根据你调用时所用的条件,自动转换成相应的 mongodb shell 语句帮你发送出去。自然地,在程序中链式调用一个个的方法要比手写数据库操作语句具有更大的灵活性和便利性。
// 首先引入 mongoose 这个模块
var mongoose = require('mongoose');
// 然后连接对应的数据库:mongodb://localhost/test
// 其中,前面那个 mongodb 是 protocol scheme 的名称;localhost 是 mongod 所在的地址;
// 端口号省略则默认连接 27017;test 是数据库的名称
// mongodb 中不需要建立数据库,当你需要连接的数据库不存在时,会自动创建一个出来。
// 关于 mongodb 的安全性,mongodb 我印象中安全机制很残废,用户名密码那套都做得不好,更
// 别提细致的用户权限控制了。不过不用担心,mongodb 的默认配置只接受来自本机的请求,内网都连不上。
// 当需要在内网中为其他机器提供 mongodb 服务时,或许可以去看看 iptables 相关的东西。
mongoose.connect('mongodb://localhost/test');
// 上面说了,我推荐在同一个 collection 中使用固定的数据形式。
// 在这里,我们创建了一个名为 Cat 的 model,它在数据库中的名字根据传给 mongoose.model 的第一个参数决定,mongoose 会将名词变为复数,在这里,collection 的名字会是 `cats`。
// 这个 model 的定义是,有一个 String 类型的 name,String 数组类型的 friends,Number 类型的 age。
// mongodb 中大多数的数据类型都可以用 js 的原生类型来表示。
//至于说 String 的长度是多少,Number 的精度是多少。String 的最大限度是 16MB,Number 的整型是 64-bit,浮点数的话,js 中 `0.1 + 0.2` 的结果都是乱来的。
// 这里可以看到各种示例:http://mongoosejs.com/docs/schematypes.html
var Cat = mongoose.model('Cat', {
name: String,
friends: [String],
age: Number,
});
// new 一个新对象,名叫 kitty
// 接着为 kitty 的属性们赋值
var kitty = new Cat({ name: 'Zildjian', friends: ['tom', 'jerry']});
kitty.age = 3;
// 调用 .save 方法后,mongoose 会去你的 mongodb 中的 test 数据库里,存入一条记录。
kitty.save(function (err) {
if (err) // ...
console.log('meow');
});
我们也可以将对应的数据模型导出接口对象,
用来给controller使用,控制器根据对应的逻辑调用对应的"增删改查"
import mongoose from 'mongoose'
mongoose.connect('mongodb://localhost/edu')
const advertSchema = mongoose.Schema({
title: { type: String, required: true },
image: { type: String, required: true },
link: { type: String, required: true },
start_time: { type: Date, required: true },
end_time: { type: Date, required: true },
create_time: { type: Date, default: Date.now },
last_modified: { type: Date, default: Date.now }
})
export default mongoose.model('Advert', advertSchema)
数据库基本操作:
$ mongo
MongoDB shell version: 2.6.4
connecting to: test
> show dbs
> use test
> show collections
> db.cats.find()
connect 中间件
-
Express是一个中间件机制的httpServer框架,它本身实现了中间件机制,它也包含了中间件。
-
实现业务逻辑解耦时,中间件是从纵向的方面进行的逻辑分解,前面的中间件处理的结果可以给后面用,比如bodyParser把解析 body的结果放在req.body中,后面的逻辑都可以从req.body中取值。由于中间件是顺序执行的,errHandler一般都放在最后,而log类的中间件则放在比较前面。
-
Connect用流程控制库的回调函数及中间件的思想来解耦回调逻辑; Koa用Generator方法解决回调问题;
我们应该也可以用事件、Promise的方式实现; -
next函数实现
-
next函数实现在handle函数体内,用来顺序执行处理逻辑,它是异步流程控制库的核心
-
path是请求路径,route是逻辑处理函数自带的属性。
-
取得下一个逻辑处理函数;
-
若路由不匹配,跳过此逻辑;
-
若路由匹配下面的call执行匹配到的逻辑处理函数
-
每个逻辑处理函数调用next来让后面的函数执行,存储在stack中的函数就实现了链式调用。不一定所有的函数都在返回的时候才调用next,为了不影响效率,有的函数可能先调用next,然而自己还没有返回,继续做自己的事情。
-
核心代码:
//取下一个逻辑逻辑处理函数 1: var layer = stack[index++]; //不匹配时跳过 2: if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) { return next(err); } //匹配时执行 3: call(layer.handle, route, err, req, res, next);
-
-
-
Connect中规定
function(err, req, res, next) {}
形式为错误处理函数,function(req, res, next) {}
为正常的业务逻辑处理函数。那么,可以根据Function.length
以判断它是否为错误处理函数。输入:
参数名 描述 handle 逻辑处理函数 route 路由 err 是否发生过错误 req Nodejs对象 res Nodejs对象 next next函数 处理过程:
- 是否有错误,本次handle是否是错误处理函数;
- 若有错误且handle为错误处理函数,则执行handle,本函数返回;
- 若没错误且handle不是错误处理函数,则执行handle,本函数返回;
- 如果上面两个都不满足,不执行handle,本函数调用next,返回;
get方法和post方法获取查询字符数据
- get :
//获取当前请求路径的一些信息
//第二个参数传入true,将查询字符串解码成对象
var urlObj = url.parse(decodeURI(req.url), true);
res.query = urlObj.query; //请求信息
- post
// chunk 是一个二进制数据
var body = ''
req.on('data', function (chunk) {
body += chunk
})
// 当 Node 接收完毕表单 POST 提交的数据的时候,会触发 req 请求对象的 end 事件,执行对应的回调处理函数
req.on('end', function () {
// 使用核心模块 querystring 的 parse 方法将一个查询字符串转为一个对象
body = querystring.parse(body)
这样body就是我们需要获取的请求数据
mondule和exports.mondule
- 使用建议:
- 如果想要向外导出多个接口成员:就使用 exports.xxx = xxx
- 如果想要导出一个单一的成员(例如函数):就是用 module.exports = xxx
- 如果想向外暴露一个单独的函数、字符串、数字、数组等单个成员,
这个时候必须通过给module.exports
重新赋值才行,为什么不能使用exports
呢?- 因为
exports = module.exports
; 把引用地址交给了exports
,而我们如果对exports
重新赋值的话,
就会导致exports
和module.exports
之间的引用关系破裂,而每个模块最终对外暴露的都是module.exports,
所以无论export被定义了什么,我们在外面都无法获取
- 因为
express(第三方框架)
一个基于 Node 开发的一个快速 Web 开发框架
主要用来构建 Server
Express 4.x API 中文手册
处理静态资源
通过 Express 内置的express.static
可以方便地托管静态文件,例如图片、CSS、JavaScript 文件等。
配置规则如下:
app.use('访问前缀', express.static('资源目录路径'))
在 Express 中配置使用 body-parser
插件解析处理表单 POST 请求体
第一步:安装 body-parser
npm install --save body-parser
第二步,在代码中进行配置:
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())
ejs 第三方模块
Express 这个框架很精简,默认是不支持模板引擎的,需要配合一些第三方的模板引擎来结合使用,
在代码中配置:
app.set('views', 模板文件存储路径) // 注意,这里可以不配置,因为 Express 默认会去项目中的 `views` 目录进行查找
app.set('view enginge', 'ejs') // 这里表示让 Express 中的 res.render 方法使用 ejs 模板引擎,这里的 ejs 就是你安装的那个模板引擎的包名
只要经过了上面这种配置,然后 res 对象上就会自动多出一个方法:res.render
,使用方式和咱们之前自己封装的一样:res.render('视图名称', {要解析替换的对象数据})
注意:使用了 ejs 模板引擎,默认视图文件后缀名必须是 .ejs
,否则 render 方法找不到。
如果想要修改,可以像下面这样:
// app.set('view enginge', 'ejs')
// 将上面这句配置改为下面的形式,就修改了默认的 .ejs 后缀名
app.engine('.html', require('ejs').__express)
app.set('view engine', 'html')
Nunjucks 模板引擎
//加载定义好的模版文件
//在模版文件中留有对应的坑
{% extends "base.html" %}
//写入自己想要的代码
{% block header %}
//代码
{% endblock %}
{% block content %}
//代码
{% endblock %}
- 利用模版的for循环及if语句实现在线教育的分页功能