node简单基础
写在前面:建议认真的看看朴灵老师的<深入浅出nodejs>
#概念
- nodejs它不是Javascript的一个库或者说一个框架,它是JavaScript的一个运行环境,和浏览器一样,充当JavaScript的运行容器
- node底层是用c++编写的,它的内核是Chrome浏览器的引擎V8
- 和浏览器一样,node是单线程的
- 擅长高并发I/O,跨平台
- 事件驱动
#意义
- 促进了前端工程化,规范化的发展
- 前端选手也可以写服务端了
- 更多的全栈选手出现,减小开发成本
- ...
#内存管理
#v8内存清理算法
- 分代式垃圾回收机制
- 新生代
- Scavenge-Cheney算法
- 牺牲空间换时间
- 空间一分为二,From和To
- From里面存活的对象复制到To
- 释放From,然后对换
- 优点是时间效率好,缺点是只能使用堆内存的一半
- 老生代
- Mark-Sweep/Mark-Compact: 标记清除与整理
- 对象晋升
- Form到To的时候检查是否已经经历过Scavenge回收了,yes -> 老生代
- To空间内存占用过多 -> 老生代
#内存限制
内存限制大小 64位1.4GB 32位0.7GB
#内存泄露
- 缓存
- 限制缓存对象的大小
- 队列消费不及时
- 作用域未释放
#io密集型
优势在于Node利用事件循环的处理能力,而不是启动每一个线程为每一个请求服务,资源占用极少
不适合CPU密集应用的原因是:由于JS单线程的原因,如果有长时间运行的计算,将会导致CPU时间片不能释放,使得后续I/O无法发起
#进程管理
- cluster
- child_process和net的组合应用
- pm2
- 特性
- 负载均衡
- 后台运行
- 自动重启
- 实时监测
- 两种模式
- fork 开发环境
- cluster 生产环境
- 原理
- 基于cluster
- RPC协议(远程过程调用协议):一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议
#Deno
- 是node的作者新推出的,还在实验中
- 名字deno是node反过来
- 下一代 Node
- 使用 Go 语言代替 C++ 重新编写跨平台底层内核驱动
- 上层仍然使用 V8 引擎
- 原生兼容TS
- 不兼容Node
- 无 package.json,无 npm
- 依赖通过url,解决现在的node_modules痛点,依赖更新也变了
- 期望兼容浏览器
- 希望最终运行结果是一个文件
从下面开始,会有很多代码,代码来说明文字~
主要简单写了一些node原生的 net 模块和 http 模块来进行网络编程
然后简单的展示了一下express和koa这两个框架是怎么样的
最后用Hapi写了一个简单,标准的Rest server(koa也有一个类似的: click here)
#网络编程
#Tcp
其实我们平时开发并不会接触到这么底层,用node自带的net模块,我们可以基于TCP协议进行socket编程
server.js
const net = require('net'); const server = net.createServer((socket) => { socket.on('data', (data) => { socket.write('你好啊朋友'); }); socket.on('end', () => { console.log('连接断开'); }); socket.write('Hello node'); }); server.listen(8888, () => { console.log('server已启动,正在监听8888端口'); });
这样就建立了一个Tcp 服务
然后我们再用net模块编写一个client客户端
client.js
const net = require('net'); const client = net.connect({ port: 8888 }, () => { console.log('已连接——client'); client.write('Node'); }); client.on('data', () => { console.log('正在接收数据——client'); console.log(data.toString()); client.end(); }); client.on('end', () => { console.log('连接已断开——client'); });
我们先启动server :node server.js
再启动client: node client
观察输出结果:
server:
server已启动,正在监听8888端口 连接断开
client:
已连接——client 正在接收数据——client Hello node你好啊朋友 连接已断开——client
从这个输出结果我们可以看出双方交互的流程
#Http
Http(超文本传输协议)几乎是我们平时用的最多的网络协议,它是应用层协议,构建在Tcp协议上面。在Http的两端就是服务器和浏览器,web应用即Http应用
node的核心模块中的http模块就包含了对Http的封装,不用做繁琐的三路握手以及构建请求格式,用它就可以很轻松很快捷的建立一个server (再往下甚至可以用net模块进行socket编程)
首先保证安装好了nodejs,使用node -v查看nodejs版本
新建一个文件夹,进入到文件夹里,初始化项目: npm init
基本上全程回车就很顺利的建立了一个项目,就是自动生成了包描述 package.json 文件
国际惯例,先来一个Hello world
新建一个js文件test.js
为了实现server,我们需要引入http这个核心模块: var http = require('http');
然后使用它的createServer方法来建立一个http server实例
const http = require('http'); http.createServer((req, res) => { res.end('Hello world'); }).listen(3000, 'localhost');
代码的意思就是创建一个http server实例,监听localhost:3000这个端口,一旦访问这个端口,就返回Hello world
然后运行它: node test.js
再浏览器中访问: http://localhost:3000
这就是一个非常非常简单的http server~
再来写一个稍微复杂一点的,这次我们返回一个json对象
const http = require('http'); const data = { one: 'Hello', two: 'World', }; http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.write(JSON.stringify(data)); res.end('数据传输完毕'); }).listen(3000, 'localhost');
这里的writeHead就是对响应头的设置了,200是我们熟悉的状态码,后面大括号里面的也是我们熟悉的http header
但是对于一个Web应用来说,仅仅这是这些,还远远不够
路由解析需要用url这个工具模块去解析
http不同方法的访问解析参数,还需要配合util这个模块使用
除此之外还有session或者token,与数据库的沟通等等
所以出现了很多后端的js框架供我们快速,高效的搭建一个web应用的后台
#框架
众多js框架都有各自的特点,不同的项目,不同的公司会根据自己的业务需求和技术栈来做一个选择
(遥遥领先的express……)
接下来,我们来大概的看一下各个后端框架的样子,快速的过一遍Express和Koa,然后我们用Hapi来写一个Rest api server
#Express
众多后端js框架里面,express是使用量最多的,也是最具有名气的
它出现的比较早,使用的人很多,但是开发没有一套明确的规范,而且越来越庞大……
在写本文的时候,express版本已经是v4了
我们用官方提供的Express 应用生成器来快速搭建一个express工程
先安装Cli工具: npm i -g express-generator
使用express-test创建express工程: express express-test
创建好工程了~
进入到项目内: cd express-test
安装依赖项: npm install or yarn
运行: npm run start or yarn start
浏览器访问: http://localhost:3000
web应用已经搭建好了,而且很多常用中间件已经配置好了~
查看package.json文件
{ "name": "express-test", "version": "0.0.0", "private": true, "scripts": { "start": "node ./bin/www" }, "dependencies": { "body-parser": "~1.18.2", "cookie-parser": "~1.4.3", "debug": "~2.6.9", "express": "~4.15.5", "jade": "~1.11.0", "morgan": "~1.9.0", "serve-favicon": "~2.4.5" } }
除了express这个依赖包,还有body-parser,cookie-parser等中间件,还有jade这个模板库
可以看到我们刚才start命令实际上是运行了bin/www.js这个文件
来看bin/www.js文件
#!/usr/bin/env node /** * Module dependencies. */ var app = require('../app'); var debug = require('debug')('express-test:server'); var http = require('http'); /** * Get port from environment and store in Express. */ var port = normalizePort(process.env.PORT || '3000'); app.set('port', port); /** * Create HTTP server. */ var server = http.createServer(app); /** * Listen on provided port, on all network interfaces. */ server.listen(port); server.on('error', onError); server.on('listening', onListening); ... ... ... /** * Event listener for HTTP server "listening" event. */ function onListening() { var addr = server.address(); var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; debug('Listening on ' + bind); }
可以看到,实际上express也是用http这个模块去建立一个http server,只不过在外层做了很多封装,加了很多功能,让开发者不必过多的关注server的构建,而是专注于业务的实现
我们再回来观察主文件app.js
var express = require('express'); var path = require('path'); var favicon = require('serve-favicon'); var logger = require('morgan'); var cookieParser = require('cookie-parser'); var bodyParser = require('body-parser'); var index = require('./routes/index'); var users = require('./routes/users'); var app = express(); // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'jade'); // uncomment after placing your favicon in /public //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); app.use(logger('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); app.use('/', index); app.use('/users', users); // catch 404 and forward to error handler app.use(function(req, res, next) { var err = new Error('Not Found'); err.status = 404; next(err); }); // error handler app.use(function(err, req, res, next) { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get('env') === 'development' ? err : {}; // render the error page res.status(err.status || 500); res.render('error'); }); module.exports = app;
这里就是在进行各种设置以及加载中间件
观察项目目录, public存放各种静态资源,views存放网页模板文件,这个项目中的是jade
然后是routes,这里基本上就是集中实现业务的地方了
routes/index.js:
var express = require('express'); var router = express.Router(); /* GET home page. */ router.get('/', function(req, res, next) { res.render('index', { title: 'Express' }); }); module.exports = router;
可以看到,用了express封装好的router方法拦截前端浏览器的各种请求
router.get即是拦***问'/'的get方法,然后渲染index这个模板,并且把title这个变量赋值为Express
来看index.jade这个模板
extends layout block content h1= title p Welcome to #{title}
第一次看到会觉得非常怪异,其实它就是html的一种模板写法,本质还是html
传过来的title变量就在这里显示出来,就是我们之前访问/看到的那个页面
其他路径其他http方法也同理了
可以看出,express更倾向于构建传统的后端渲染的web app
#Koa
现在Koa有2个大版本,现在说的Koa一般指Koa2
Koa是express的原班人马打造的
它与express主要有几个区别:
- Koa比较新,新语法,新理念
- Koa很小很轻便,不如express绑定了那么多中间件
- Koa的语法很新,运用es6的Generator去加载中间件(很类似Redux加载中间件),并且Koa很适合写异步代码
koa因为用了很多es6的新语法,所以如果不用babel,靠node原生支持的话,要求node版本在7.6以上,不过现在node的LTS版本都到8.9了,这应该不是什么问题
阿里的egg也是基于koa封装开发的
下面是官方原话:
koa 是由 Express 原班人马打造的,致力于成为一个更小、更富有表现力、更健壮的 Web 框架。 使用 koa 编写 web 应用,通过组合不同的 generator,可以免除重复繁琐的回调函数嵌套, 并极大地提升错误处理的效率。koa 不在内核方法中绑定任何中间件, 它仅仅提供了一个轻量优雅的函数库,使得编写 Web 应用变得得心应手。
用Koa建立一个http server只需要3行代码
const Koa = require('koa'); const app = new Koa(); app.listen(3000);
是的,已经建立了,只不过没有对应的响应而已
koa的http响应和请求的对象是封装在了context(上下文)这个对象中,一般简写为ctx
Hello world实例:
const Koa = require('koa'); const app = new Koa(); app.use(async ctx => { ctx.body = 'Hello World'; }); app.listen(3000);
访问就能得到Hello World的响应
这里用到了use这个方法,koa就是用use来加载各种中间件,而且是通过async函数来异步加载
const Koa = require('koa'); const app = new Koa(); // x-response-time app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; ctx.set('X-Response-Time', `${ms}ms`); }); // logger app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}`); }); // response app.use(async ctx => { ctx.body = 'Hello World'; }); app.listen(3000);
后台输出了:GET / - 10
对应的是:
console.log(`${ctx.method} ${ctx.url} - ${ms}`);
这段官方给出的demo就是演示了koa的加载流程,通过异步函数来加载,Redux加载中间件也是这样的
[]
#Hapi
#Restful
Rest:Representational State Transfer ---(资源)表现层状态转化
资源: 一些信息,可以是文本,图片等等
表现层: 用url表示出来
状态转化: 用Http的几个动词来表示状态,是增加,删除,还是更新等等
所以,总结的说,RESTful架构就是:
- 每一个URI代表一种资源;
- 客户端和服务器之间,传递这种资源的某种表现层;
- 客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"
#Hapi Rest api server
Hapi是沃尔玛出品的node后端框架
它适用于构建应用程序和服务,其设计目标是让开发者把精力集中于开发可重用的应用程序的业务逻辑,向开发者提供构建应用程序业务逻辑所需的基础设施
以上话说了当没说,后端框架基本上都是这么介绍自己的
我的观点是:Hapi的名字是这么来的H-api,是一个为api而生的框架,这才是与express和Koa的不同
默默吐槽一句: Hapi是一个(大)版本狂魔,现在Hapi的最新版本是17.2.....
相比Koa和Express,Hapi在国内用的少的多,所以中文的文档也基本上没有,要学习的话也基本上只能看官方的英文文档
还是老样子,先建立一个Http server
新建项目Hapi-test: mkdir Hapi-test
初始化项目,然后安装Hapi: yarn add Hapi
然后新建main.js
const Hapi = require('hapi'); // Create a server with a host and port const server = Hapi.server({ host: 'localhost', port: 3000 }); // Add the route server.route({ method: 'GET', path: '/', handler: function (request, res) { return 'hello world'; } }); // Start the server async function start() { try { await server.start(); } catch (err) { console.log(err); process.exit(1); } console.log('Server running at:', server.info.uri); }; start();
访问http://localhost:3000
还是熟悉的Hello world
接下来,我们往这个空项目里面加东西,api server的实现也就是对routes的响应
我们先把路由的代码拆分
新建文件夹routes
再新建文件index.js
然后把handler代码抽离出来:
routes文件夹下新建handler文件夹,新建文件user.js
routes/handler/user.js:
module.exports.hello = (request, reply) => { return 'hello world'; };
重写路由代码
routes/index.js:
const user = require('./handler/user.js'); const routes = [ { method: 'GET', path: '/', handler: user.hello, }, ] module.exports = routes;
修改main.js:
const Hapi = require('hapi'); const routes = require('./routes'); // Create a server with a host and port const server = Hapi.server({ host: 'localhost', port: 3000 }); // Add the route server.route(routes); // Start the server async function start() { try { await server.start(); } catch (err) { console.log(err); process.exit(1); } console.log('Server running at:', server.info.uri); }; start();
这样就把路由抽出来,并且把handler即业务集中实现的代码单独抽离出来
既然是api,那不能只是Hello world,那还需要沟通数据库呀
所以handler里面应该加上查询,更新数据库的代码,数据库我们用MongoDB
但是,我们不单独的直接用代码去连接MongoDB,我们用Mongoose
相比原生的MongoDB,Mongoose对MongoDB的操作做了一些封装,自动帮你处理一些细节,引入Schema和Model的概念对MongoDB的文档做了抽象
当使用Mongoose时,我们不在需要在数据库中创建好结构(Schema)之后,再与后端代码中创建的对象或类进行映射这样繁琐的操作。在Mongoose的封装下,我们只需定义好JSON类型的数据结构即可
Model是对应Schema的实例化版本,一个model的实例直接映射为数据库中的一个文档。基于这种关系,model处理所有的文档交互
schema定义结构,model处理操作
总之,就是以面向对象的思维去操作MongoDB
先在项目依赖中加入Mongoose: yarn add Mongoose
然后在代码中使用Mongoose
main.js:
const Hapi = require('hapi'); const mongoose = require('mongoose'); const routes = require('./routes'); // Create a server with a host and port const server = Hapi.server({ host: 'localhost', port: 3000 }); // connect mongoDB mongoose.connect('mongodb://127.0.0.1:27017/hapi-api'); // Add the route server.route(routes); // Start the server async function start() { try { await server.start(); } catch (err) { console.log(err); process.exit(1); } console.log('Server running at:', server.info.uri); }; start();
根目录下新建文件夹model,这个文件夹存放定义Mongoose的文档(表)结构的文件
比如说我们有user这个业务,那就存在对应的api路径,/api/user,那对应的后端数据表也存在user这个文档(表)
所以在model下新建文件user.js
const mongoose = require('mongoose'); exports.UsersModel = mongoose.model( 'Users', new mongoose.Schema({ name: { type: String, default: '' }, passwd: { type: String, default: '' }, tags: { type: Array, default: [] }, update: { type: Date, default: Date.now }, }) );
从代码里面我们可以很清楚的看到,Schema定义结构,然后用Mongoose.model方法实例化成叫Users的model
数据库的准备做的差不多了,现在开始实现业务代码,也就是根据对应的api来实现对应的数据库操作
修改routes/handler/user.js
const UsersModel = require('../../model/user').UsersModel; const userHandler = { list: async(request, reply) => { let user; try { user = await UsersModel.find(); } catch (error) { request.log.error(errore); return Boom.notFound(error.message); } return user; }, }; module.exports = userHandler;
修改routes/index.js
const user = require('./handler/user.js'); const routes = [ { method: 'GET', path: '/api/users', handler: user.list, }, ] module.exports = routes;
这样写就很明白了,当以GET方法访问/api/users的时候,就用user.list来处理这个访问,而这个``handler对应的数据库操作就是查询出数据库中user`表所有的数据
我们用Postman来测试一下api
返回了一个空数组,当然,因为我们还没有往里面添加数据
我们现在可以来编写POST方法,来写一个增加用户数据的操作
修改routes/handler/user.js
const UsersModel = require('../../model/user').UsersModel; const userHandler = { list: async (request, reply) => { let user; try { user = await UsersModel.find(); } catch (error) { request.log.error(errore); return Boom.notFound(error.message); } return user; }, create: async (request, reply) => { const { payload: { name, passwd, tags } } = request; const user = new UsersModel({ name, passwd, tags }); try { user.save(); } catch (error) { request.log.error(error); return Boom.badRequest(error.message); } return '创建成功'; }, }; module.exports = userHandler;
在routes/index.js中加入相应的路由
const user = require('./handler/user.js'); const routes = [ { method: 'GET', path: '/api/users', handler: user.list, }, { method: 'POST', path: '/api/user', handler: user.create, }, ] module.exports = routes;
然后用Postman测试一下
再验证一下,进数据库中看看
进入mongoDB: mongo
进入learn数据库: use learn
查看learn下的所有表: show tables
查看users表的所有数据: db.users.find()
可以看到,确实新增了一条数据
再用GET方法访问/api/users试试:
到这应该能够初步的感受到api的编写是怎么一个样子了
其实复杂的场景也不外乎是在handler进行其他的操作,最后返回约定好的数据格式
当然,在真实的生产环境下不只是简单的数据库操作,还要进行参数的验证(Joi),token的验证(jwt,OAuth2)等等
所以Hapi就对应开发了很多插件来开发者使用,专注业务开发,需要什么就加入什么