一、Node简介
1.1 客户端的JavaScript是怎样的
问题 | 答 |
---|---|
什么是 JavaScript | 脚本语言 运行在浏览器中 一般用来做客户端页面的交互(Interactive) |
JavaScript 的运行环境 | 浏览器内核中的 JS 引擎(engine) |
JavaScript 只可以运行在浏览器中吗 | 不是,运行在哪取决于这个环境有没有特定平台 |
浏览器中的 JavaScript 可以做什么 | BOM(页面跳转、历史记录)、操作DOM(对DOM的增删改、注册事件) AJAX / 跨域 ECMAScript |
浏览器中的 JavaScript 不可以做什么 | 文件操作(文件和文件夹的CRUD) 没有办法操作系统信息,因为运行环境特殊(代码在不认识的人的浏览器中运行) |
开发能力相同时,编程语言的能力取决于 | 运行该语言的平台(环境),因为语言本身只是提供定义变量,定义函数,定义类型,流程控制,循环结构之类的操作 |
语言 | 特点 | 平台 |
---|---|---|
Java | 既是语言也是平台 | 运行在Java虚拟机(跨操作系统) |
PHP | 既是语言也是平台 | |
C# | 既是语言也是平台 | .NET Framework(Windows) MONO(因为有人需要将C#运行在Linux平台,所以出现了MONO) |
Node | 只是平台 |
1.2 什么是Node
- Node 就是 JavaScript 语言在服务器端的运行环境,不是一门语言,也不是JavaScript的框架
- 所谓“运行环境(平台)”有两层意思:
- 首先,JavaScript 语言通过 Node 在服务器运行,在这个意义上,Node 有点像 JavaScript 虚拟机
- 其次,Node 提供大量工具库,使得 JavaScript 语言与操作系统互动(比如读写文件、新建子进程),在这个意义上, Node 又是 JavaScript 的工具库
1.2.1 简介
Node是服务器的程序,写的js语句,都将运行在服务器上。返回给客户的都是已经处理好的纯html。
如果想修改程序,必须中断当前运行的服务器,重新node一次,刷新
**ctrl+c,就可以打断挂起的服务器程序。**此时按上箭头,能够快速调用最近的node命令。
本地写一个js不能直接拖入浏览器运行,但是有了node,任何一个js文件都可以通过node来运行。也就是说,node是一个js的执行环境。
要跑起来的服务器的脚本要以.js存储,是一个js文件。用node命令运行这个js文件。
Node没有根目录的概念,因为它根本没有任何的web容器!
让node提供一个静态服务,都非常难!也就是说node中,如果看见一个网址是127.0.0.1:3000/fang
,一定有一个文件夹,叫做fang了。可能/fang的物理文件,是同目录的test.html
URL和真实物理文件,是没有关系的。URL是通过了Node的顶层路由设计,呈递某一个静态文件的。
-
Node不是一种独立的语言,与PHP、JSP、Python、Perl、Ruby的 “既是语言,也是平台” 不同,Node使用JavaScript进行编程,运行在JavaScript引擎上(V8)。
-
与PHP、JSP等相比(PHP、JSP、.net都需要运行在服务器程序上Apache、Naginx、Tomcat、IIS),Node跳过了Apache、Naginx、IIS等HTTP服务器,它自己不用建设在任何服务器软件之上。
-
Node 许多设计理念与经典架构(LAMP = Linux + Apache + MySQL + PHP)有着很大的不同,可以提供强大的伸缩能力。
-
Node 没有web容器
-
Node 花最小的硬件成本,追求更高的并发,更高的处理性能。
1.2.2 特点
所谓的特点,就是Node是如何解决服务器高性能瓶颈问题的。
单线程
在Java、PHP或者.net等服务器端语言中,会为每一个客户端连接创建一个新的线程。而每个线程需要耗费大约2MB内存。也就是说,理论上,一个8GB内存的服务器可以同时连接的最大用户数为4000个左右。要让Web应用程序支持更多的用户,就需要增加服务器的数量,而Web应用程序的硬件成本当然就上升了。
Node不为每个客户连接创建一个新的线程,而仅仅使用一个线程。当有用户连接了,就触发一个内部事件,通过非阻塞I/O、事件驱动机制,让Node程序宏观上也是并行的。使用Node一个8GB内存的服务器,可以同时处理超过4万用户的连接。
另外,单线程带来的好处 还有操作系统完全不再有线程创建、销毁的时间开销。
坏处,就是一个用户造成了线程的崩溃,整个服务都崩溃了,其他人也崩溃了。
也就是说,单线程也能造成宏观上的“并发”。
非阻塞I/O non-blocking I/O
在传统的单线程处理机制中,在执行了访问数据库代码之后,整个线程都将暂停下来,等待数据库返回结果,才能执行后面的代码。也就是说,I/O阻塞了代码的执行,极大地降低了程序的执行效率。
由于Node中采用了非阻塞型I/O机制,因此在执行了访问数据库的代码之后,将立即转而执行其后面的代码,把数据库返回结果的处理代码放在回调函数中,从而提高了程序的执行效率。
当某个I/O执行完毕时,将以事件的形式通知执行I/O操作的线程,线程执行这个事件的回调函数。为了处理异步I/O,线程必须有事件循环,不断的检查有没有未处理的事件,依次予以处理。
阻塞模式下,一个线程只能处理一项任务,要想提高吞吐量必须通过多线程。**而非阻塞模式下,一个线程永远在执行计算操作,这个线程的CPU核心利用率永远是100%。**所以,这是一种特别有哲理的解决方案:与其人多,但是好多人闲着;还不如一个人玩命,往死里干活儿。
事件驱动event-driven
在Node中,客户端请求建立连接,提交数据等行为,会触发相应的事件。在Node中,在一个时刻,只能执行一个事件回调函数,但是在执行一个事件回调函数的中途,可以转而处理其他事件(比如,又有新用户连接了),然后返回继续执行原事件的回调函数,这种处理机制,称为“事件环”机制。
Node 底层是C++(V8也是C++写的)。**底层代码中,近半数都用于事件队列、回调函数队列的构建。**用事件驱动来完成服务器的任务调度,这是鬼才才能想到的。针尖上的舞蹈,用一个线程,担负起了处理非常多的任务的使命。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wnrziPax-1647412079009)(.\images\wps3BEA.tmp.jpg)]
单线程,单线程的好处,减少了内存开销,操作系统的内存换页。
如果某一个事情,进入了,但是被I/O阻塞了,所以这个线程就阻塞了。
非阻塞I/O, 不会傻等I/O语句结束,而会执行后面的语句。
非阻塞就能解决问题了么?比如执行着小红的业务,执行过程中,小刚的I/O回调完成了,此时怎么办??
事件机制,事件环,不管是新用户的请求,还是老用户的I/O完成,都将以事件方式加入事件环,等待调度。
说是三个特点,实际上是一个特点,离开谁都不行,都玩儿不转了。
Node 很像抠门的餐厅老板,只聘请1个服务员,服务很多人。结果,比很多服务员效率还高。
Node 中所有的I/O都是异步的,回调函数套回调函数。
1.2.3 适合开发什么?
Node适合用来开发什么样的应用程序呢?
善于I/O,不善于计算。因为Node最擅长的就是任务调度,如果你的业务有很多的CPU计算,实际上也相当于这个计算阻塞了这个单线程,就不适合Node开发。
当应用程序需要处理大量并发的I/O,而在向客户端发出响应之前,应用程序内部并不需要进行非常复杂的处理的时候,Node非常适合。Node也非常适合与web socket配合,开发长连接的实时交互应用程序。
比如:
● 用户表单收集
● 考试系统
● 聊天室
● 图文直播
● 提供JSON的API(为前台Angular使用)
1.2.4 Node.js无法挑战老牌3P
1.3 环境配置
安装包的方式安装后更新版本
- 操作方式:重新下载最新的安装包,覆盖安装
- 问题:
- 以前版本安装的很多全局的工具包需要重新安装
- 无法回滚到之前的版本
- 无法在多个版本之间切换(很多时候我们要使用特定版本)
NVM工具的使用
Node Version Manager(Node版本管理工具)
由于以后的开发工作可能会在多个Node版本中测试,而且Node的版本也比较多,所以需要这么款工具来管理
安装操作步骤
- 下载:nvm-windows
- 解压到一个全英文路径
- 编辑解压目录下的
settings.txt
文件(不存在则新建)
root 配置为当前 nvm.exe 所在目录
path 配置为 node 快捷方式所在的目录
arch 配置为当前操作系统的位数(32/64)
proxy 不用配置
- 配置环境变量 可以通过 window+r : sysdm.cpl
NVM_HOME = 当前 nvm.exe 所在目录
NVM_SYMLINK = node 快捷方式所在的目录
PATH += %NVM_HOME%;%NVM_SYMLINK%;
- 打开CMD通过
set [name]
命令查看环境变量是否配置成功 - PowerShell中是通过
dir env:[name]
命令
-
NVM使用说明:https://github.com/coreybutler/nvm-windows/
-
NPM的目录之后使用再配置
配置Python环境
Node中有些第三方的包是以C/C++源码的方式发布的,需要安装后编译,确保全局环境中可以使用python命令
环境变量的概念
环境变量就是操作系统提供的系统级别用于存储变量的地方
- Windows中环境变量分为系统变量和用户变量
- 环境变量的变量名是不区分大小写的
- 特殊值:
- PATH 变量:只要添加到 PATH 变量中的路径,都可以在任何目录下搜索
1.4 Windows下常用的命令行操作
- 切换当前目录(change directory):cd
- 创建目录(make directory):mkdir
- 查看当前目录列表(directory):dir
- 别名:ls(list)
- 清空当前控制台:cls
- 别名:clear
- 删除文件:del
- 别名:rm
注意:所有别名必须在新版本的 PowerShell 中使用
总结
怪异特点:单线程、Non-blocking I/O、Event Driven。 实际上是一个特点。
首先,Node不为每个用户开辟一个线程,所以非常极端的选择了单线程。单线程,要照顾所有的用户,那么就必须有非阻塞I/O,否则一个人的I/O就把别人、自己都阻塞了。一旦有非阻塞I/O,一个人如果I/O去了,就会放弃CPU的使用权,换成另一个人使用CPU(或者执行此人后面的语句)。所以CPU的利用率100%。第一个人I/O结束了,就要用事件来通知线程,执行回调函数。此时必须有事件环,就有一个排队调度机制。Node中有超过半数的C++代码,在搭建事件环。
Node和别的老牌3P不一样:
1) 没有自己的语法,使用V8引擎,所以就是JS。V8引擎解析JS的,效率非常高,并且V8中很多东西都是异步的。Node就是将V8中的一些功能自己没有重写(别人做了,自己就站在巨人肩膀上),移植到了服务器上。
2) 没有web容器,就是安装配置完成之后,没有一个根目录。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a2hPZ6R0-1647412079011)(.\images\wps7194.tmp.jpg)]
命令提示符所在路径太重要了,因为程序中的所有相对路径”./”,都是相对这个命令提示符路径的,而不是相对于js文件自己。
系统中默认是http端口80。所以当没有端口号的时候,就是80端口。
二、模块
2.1 模块介绍
在Node中,以模块为单位划分所有功能,并且提供了一个完整的模块加载机制,可以将应用程序划分为各个不同的部分。不可能用一个js文件去写全部的业务,肯定要有MVC。
-
狭义的说,每一个JavaScript文件都是一个模块
-
多个JavaScript文件之间可以相互require,他们共同实现了一个功能,整体对外,又称为一个广义上的模块。
-
Node中,一个JavaScript文件中定义的变量、函数,都只在这个文件内部有效。
- 当需要从此JS文件外部引用这些变量、函数时,必须使用exports对象进行暴露。使用者要用require()命令引用这个JS文件。
- 一个JavaScript文件,可以向外exports无数个变量、函数。但是require的时候,仅仅需要require这个JS文件一次。使用它的变量、函数的时候,用点语法即可。所以无形之中,增加了一个顶层命名空间。
- 在一个JavaScript文件中,描述一个类。用
module.export = 构造函数名;
的方式向外暴露一个类
-
Node中,js文件至今就是被一个个exports和require构建成为网状的。不是靠html文件统一在一起的
-
require 方法加载规则
- 优先从缓存加载
- 核心模块
- 路径形式的模块
- 第三方模块
- node_modules
-
exports 和 module.exports 的区别
- 每个模块中都有一个 module 对象,module 对象中有一个 exports 对象
- Node 为了方便,同时在每一个模块中都提供了一个成员叫:
exports
示例 优缺点 module.exports moudle.exports.xxx = xxx
导出成员太多时比较麻烦 exports expots.xxx = xxx
导出单个成员不适用,因为每个模块最终向外 return
的是module.exports
,而exports
只是module.exports
的一个引用exports === module.exports
结果为true
exports = module.exports
可以重新建立引用关系
foo.js文件中的代码:
var msg = "你好";
var info = "呵呵";
function showInfo(){
console.log(info);
}
exports.msg = msg;
exports.info = info;
exports.showInfo = showInfo;
使用者:
var foo = require("./test/foo.js"); // 相当于增加了顶层变量foo。所有的函数、变量都要从这个顶层变量走
console.log(foo.msg);
console.log(foo.info);
foo.showInfo();
js文件和js文件之间有两种合作的模式:
1) 某一个js文件,提供了函数,供别人使用。只需要暴露函数exports.msg = msg;
2) 某一个js文件,描述了一个类。 module.exports = People;
-
如果在require命令中不写相对路径:
var foo = require("foo.js");
Node将该文件视为node_modules目录下的一个文件 -
node_modules文件夹并不一定在同级目录里面,也可以在任何祖先级目录中。甚至可以放到NODE_PATH环境变量的文件夹中。好处是:分享项目的时候,不需要带着modules一起给别人。
-
使用文件夹来管理模块,比如
var bar = require("bar");
Node将寻找node_modules目录下的bar文件夹中的index.js执行
每一个模块文件夹中,推荐都写一个package.json文件,这个文件的名字不能改,放到模块文件夹的根目录。node将自动读取里面的配置。有一个main项就是入口文件:
{
"name": "kaoladebar",
"version": "1.0.1",
"main" : "app.js" // 入口文件
}
require()别的js文件的时候,将执行那个js文件。
注意:require()中的路径,是从当前这个js文件出发,找到别人。而fs是从命令提示符找到别人。
桌面上有一个a.js, test文件夹中有b.js、c.js、1.txt
a要引用b:var b = require("./test/b.js");
b要引用c:var b = require("./c.js");
但是,fs等其他的模块用到路径的时候,都是相对于cmd命令光标所在位置。
所以,在b.js中想读1.txt文件,推荐用绝对路径:
fs.readFile(__dirname + "/1.txt",function(err,data){
if(err) { throw err; }
console.log(data.toString());
});
2.2 核心模块
核心模块的意义
- 如果只是在服务器运行JavaScript代码,意义并不大,因为无法实现任何功能(读写文件,访问网络)
- Node 的用处在于它本身还提供的一系列功能模块,用于与操作系统互动
- 这些核心的功能模块在 Node 中内置
内置如下模块:
- path:处理文件路径。
- fs:操作文件系统。
- child_process:新建子进程。
- util:提供一系列实用小工具。
- http:提供HTTP服务器功能。
- url:用于解析URL。
- querystring:解析URL中的查询字符串。
- crypto:提供加密和解密功能。
- 其他
2.3 HTTP模块
// require表示引包,引包就是引用自己的一个特殊功能
var http = require("http"); // 引用模块
// 创建服务器,参数是一个回调函数,表示如果有请求进来,要做什么
var server = http.createServer(function(req,res){
// req表示请求,request; res表示响应,response
console.log(req.url) // 用户的请求URL地址
// 设置HTTP头部,状态码是200,文件类型是html,字符集是utf8
res.writeHead(200,{"Content-type":"text/html;charset=UTF-8"});
res.end("哈哈哈哈,我买了一个iPhone" + (1+2+3) + "s");
});
// 运行服务器,监听3000端口(端口号可以任改)
server.listen(3000,"127.0.0.1");
设置一个响应头:res.writeHead(200,{"Content-Type":"text/plain;charset=UTF8"});
识别req.url用到两个新模块,第一个就是url模块,第二个就是querystring模块
字符串查询,用querystring处理
querystring.parse('foo=bar&baz=qux&baz=quux&corge')
// 返回 { foo: 'bar', baz: ['qux', 'quux'], corge: '' }
// Suppose gbkDecodeURIComponent function already exists, it can decode `gbk` encoding string
querystring.parse('w=%D6%D0%CE%C4&foo=bar', null, null, { decodeURIComponent: gbkDecodeURIComponent })
// 返回 { w: '中文', foo: 'bar' }
2.4 post请求
var alldata = "";
// 下面是post请求接收的一个公式
// node为了追求极致,它是一个小段一个小段接收的。
// 接受了一小段,可能就给别人去服务了。防止一个过大的表单阻塞了整个进程
req.addListener("data",function(chunk){
alldata += chunk;
});
//全部传输完毕
req.addListener("end",function(){
console.log(alldata.toString());
res.end("success");
});
原生写POST处理,比较复杂,要写两个监听。文件上传业务比较难写。所以,用第三方模块formidable。
只要涉及文件上传,那么form标签要加一个属性:
<form action="http://127.0.0.1/dopost" method="post" enctype="multipart/form-data">
三、模板引擎
<a href="<%= url %>"><img src="<%= imageURL %>" alt=""></a>
数据绑定,就成为一个完整的html字符串了。
后台模板,著名的有两个,第一个叫做ejs; 第二个叫做jade。是npm第三方包。
3.1 EJS
Embedded JavaScript templates
后台模板引擎
<ul>
<% for(var i = 0 ; i < news.length ; i++){ %>
<li><%= news[i] %></li>
<% } %>
</ul>
var dictionary = {
a: 6,
news : ["1期班太牛逼了","高薪就业","哈哈哈哈哈"]
};
四、NPM、文件操作、缓冲区
4.1 Node Package
由于Node是一套轻内核的平台,虽然提供了一系列的内置模块,但是不足以满足开发者的需求,于是乎出现了包(Package)的概念。与核心模块类似,就是将一些预先设计好的功能或者说API封装到一个文件夹,提供给开发者使用
包的加载机制
- 与内置模块相同,包的加载同样使用
require
方法
const express = require('express');
-
加载机制也和内置模块加载机制相同
-
加载注意事项:
- 先在系统核心(优先级最高)的模块中找
const fs = require('fs'); // 永远加载内部核心模块fs
- 然后再到当前项目中 node_modules 目录中找
如何管理好自己的包
- 由于
Node
本身并没有太多的功能性API
,所以市面上涌现出大量的第三方人员开发出来的Package
- 包的生态圈一旦繁荣起来,就必须有工具去代替人脑或者文档的方式管理
- 这时候
NPM
诞生了
NPM
-
随着时间的发展,NPM 出现了两层概念:
- 一层含义是 Node 的开放式模块登记和管理系统,亦可以说是一个生态圈,一个社区
- 另一层含义是 Node 默认的模块管理器,是一个命令行下的软件,用来安装和管理 Node 模块
-
官方链接: https://www.npmjs.com/
-
国内加速镜像: https://npm.taobao.org/
安装NPM
- NPM 不需要单独安装。默认在安装 Node 的时候,会连带一起安装 NPM
- 但是,Node 附带的 NPM 可能不是最新版本,最好用下面的命令,更新到最新版本
$ npm install npm -g
- 默认安装到当前系统 Node 所在目录下
- 由于之前使用 NVM 的方式安装的 Node 所以需要重新配置 NPM 的全局目录
配置NPM的全局目录
$ npm config set prefix [pathtonpm]
- 将NPM目录配置到其他目录时,必须将该目录放到环境变量中,否则无法在全局使用
常用NPM命令
- https://docs.npmjs.com/
npm config [ls|list|set|get] [name] [value]
npm init [--yes|-y]
npm search [name]
npm info [name]
npm install [--global|-g] [name]
npm uninstall [--global|-g] [name]
npm list [--global|-g]
npm outdated [--global|-g]
npm update [--global|-g] [name]
npm run [task]
npm cache [clean]
4.2 文件操作
相关模块
Node内核提供了很多与文件操作相关的模块,每个模块都提供了一些最基本的操作API,在NPM中也有社区提供的功能包
-
fs:基础的文件操作 API
-
path:提供和路径相关的操作 API
-
readline:用于读取大文本文件,一行一行读
-
fs-extra(第三方):https://www.npmjs.com/package/fs-extra
4.2.1 同步或异步调用
fs模块对文件的几乎所有操作都有同步和异步两种形式。例如:readFile() 和 readFileSync()
调用方式 | 阻塞代码执行 | 什么时候回调 | 异常处理 |
---|---|---|---|
同步 - readFileSync | 阻塞 | try catch | |
异步 - readFile | 不阻塞 | 将读取任务下达到任务队列,直到任务执行完成才会回调 | 通过回调函数的第一个参数 |
// 同步
console.time('sync');
try {
var data = fs.readFileSync(path.join('C:\\Users\\iceStone\\Downloads', 'H.mp4'));
} catch (error) {
throw error;
}
console.timeEnd('sync');
// 异步
console.time('async');
fs.readFile(path.join('C:\\Users\\iceStone\\Downloads', 'H.mp4'), (error, data) => {
if (error) throw error;
});
console.timeEnd('async');
4.2.2 路径模块
在文件操作的过程中,都必须使用物理路径(绝对路径),path模块提供了一系列与路径相关的 API
path.join
:拼接多个路径部分,并转化为正常格式path.basename(temp)
:获取路径的文件名(包含默认扩展名)path.basename(temp, '.lrc')
:获取路径中的文件名并排除扩展名process.platform
:操作系统path.delimiter
:路径分隔符process.env.PATH.split(path.delimiter)
:一般用于分割环境变量path.dirname(temp)
:获取一个路径中的目录部分path.extname(temp)
:获取一个路径中最后的扩展名path.parse(temp)
:将一个路径解析成一个对象的形式- root根路径
- dir目录
- base包含后缀名的文件名
- ext后缀名
- name不包含后缀名的文件名
path.format(pathObject)
:将一个路径对象再转换为一个字符串的形式path.isAbsolute(temp)
:获取一个路径是不是绝对路径path.normalize('.../a.txt')
:将一个路径转换为当前系统默认的标准格式,并解析其中的./和…/path.relative(__dirname, temp)
:获取第二个路径相对第一个路径的相对路径path.resolve(temp, 'c:/', './develop', '../application')
:以类似命令行cd命令的方式拼接路径path.sep
:获取不同平台中路径的分隔符(默认)path === path.win32
:允许在任意平台下以WIN32的方法调用PATH对象path === path.posix
:允许在任意平台下以POSIX的方法调用PATH对象
源码地址:
https://github.com/nodejs/node/blob/master/lib/path.js
4.2.3 文件读取
fs.readFile(file[, options], callback(error, data))
fs.readFile('c:\\demo\1.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});
fs.readFileSync(file[, options])
try {
const data = fs.readFileSync('c:\\demo\1.txt', 'utf8');
console.log(data);
} catch(e) {
// 文件不存在,或者权限错误
throw e;
}
fs.createReadStream(path[, options])
const stream = fs.createReadStream('c:\\demo\1.txt');
let data = ''
stream.on('data', (trunk) => {
data += trunk;
});
stream.on('end', () => {
console.log(data);
});
由于Windows平台下默认文件编码是GBK,在Node中不支持,可以通过iconv-lite解决
Readline模块逐行读取文本内容
const readline = require('readline');
const fs = require('fs');
const rl = readline.createInterface({
input: fs.createReadStream('sample.txt')
});
rl.on('line', (line) => {
console.log('Line from file:', line);
});
4.2.4 文件写入
fs.writeFile(file, data[, options], callback(error))
fs.writeFile('c:\\demo\a.txt', new Date(), (error) => {
console.log(error);
});
fs.writeFileSync(file, data[, options])
try {
fs.writeFileSync('c:\\demo\a.txt', new Date());
} catch (error) {
// 文件夹不存在,或者权限错误
console.log(error);
}
fs.createWriteStream(path[,option])
var streamWriter = fs.createWriteStream('c:\\demo\a.txt');
setInterval(() => {
streamWriter.write(`${new Date}\n`, (error) => {
console.log(error);
});
}, 1000);
fs.appendFile(file,data[,options],callback(err))
// 相比较之前文件流的方式,这种方式不会占用文件资源,append完成就会释放
setInterval(() => {
fs.appendFile('c:\\demo\a.txt',`${new Date}\n`, (error) => {
console.log(error);
});
}, 1000);
fs.appendFileSync(file,data[,options])
setInterval(() => {
fs.appendFileSync('c:\\demo\a.txt',`${new Date}\n`);
}, 1000);
4.2.5 其他常见文件操作
文件操作 | 异步 | 同步 |
---|---|---|
验证路径是否存在 | fs.exists(path,callback(exists)) | fs.existsSync(path) 返回布尔类型 exists |
获取文件信息 | fs.stat(path,callback(err,stats)) | fs.statSync(path) 返回一个fs.Stats实例 |
移动文件或重命名文件或目录 | fs.rename(oldPath,newPath,callback) | fs.renameSync(oldPath,newPath) |
删除文件 | fs.unlink(path,callback(err)) | fs.unlinkSync(path) |
4.2.6 其他常见文件夹操作
文件夹操作 | 异步 | 同步 |
---|---|---|
创建一个目录 | fs.mkdir(path[,model],callback) | fs.mkdirSync(path[,model]) |
删除一个空目录 | fs.rmdir(path,callback) | fs.rmdirSync(path) |
读取一个目录 | fs.readdir(path,callback(err,files)) | fs.readdirSync(path) 返回files |
4.2.7 文件监视
利用文件监视实现自动 markdown 文件转换
-
相关链接:
- https://github.com/chjj/marked
- https://github.com/Browsersync/browser-sync
-
实现思路:
- 利用
fs
模块的文件监视功能监视指定MD文件 - 当文件发生变化后,借助
marked
包提供的markdown
tohtml
功能将改变后的MD文件转换为HTML - 再将得到的HTML替换到模版中
- 最后利用BrowserSync模块实现浏览器自动刷新
- 利用
const fs = require('fs');
const path = require('path');
var marked = require('marked');
var bs = require('browser-sync').create();
var target = path.join(__dirname, process.argv[2] || './README.md');
var filename = path.basename(target, path.extname(target)) + '.html';
var targetHtml = path.join(path.dirname(target), filename);
bs.init({
server: path.dirname(target),
index: filename,
notify: false
});
bs.reload(filename);
var template = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<style>{{{styles}}}</style>
</head>
<body>
<article class="markdown">
{{{body}}}
</article>
</body>
</html>
`;
fs.readFile(path.join(__dirname, './markdown.css'), 'utf8', (error, css) => {
if (error) throw error;
template = template.replace('{{{styles}}}', css);
var handler = (current, previous) => {
fs.readFile(target, 'utf8', (error, content) => {
var html = template.replace('{{{body}}}', marked(content));
fs.writeFile(targetHtml, html, (error) => {
if (!error) {
console.log(`updated@${new Date()}`);
bs.reload(filename);
}
});
});
};
handler();
fs.watchFile(target, { interval: 100 }, handler);
});
4.3 缓冲区处理
什么是缓冲区
- 缓冲区就是内存中操作数据的容器,只是数据容器而已
- 通过缓冲区可以很方便的操作二进制数据
- 而且在大文件操作时必须有缓冲区
为什么要有缓冲区
- JavaScript是比较擅长处理字符串,但是早期的应用场景主要用于处理HTML文档,不会有太大篇幅的数据处理,也不会接触到二进制的数据
- 而在Node中操作数据、网络通信是没办法完全以字符串的方式操作的
- 所以在Node中引入了一个二进制的缓冲区的实现:Buffer
五、文件流、网络操作
5.1 文件流
什么是流
- 在程序开发的概念中
- 流是程序输入或输出的一个连续的字节序列
- 文件流、网络流
- 设备(例如鼠标、键盘、磁盘、屏幕、调制解调器和打印机)的输入和输出都是用流来处理的
5.2 Node中的流操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vE1qFi5f-1647412079014)(.\images\02.png)]
在 Node 核心模块 fs 中定义了一些与流相关的 API
fs.createReadStream()
得到一个ReadableStreamfs.createWriteStream()
得到一个WritableStream
5.2.1 读取流 / 写入流常用 API
Event | Method | |
---|---|---|
读取流 | data、end、error | read([size])、 pause()、 isPause()、 resume()、 setEncoding(encoding)、 pipe(destination[, options])、 unpipe([destination]) |
写入流 | error、pipe | write(chunk[, encoding][, callback]) 、end([chunk][, encoding][, callback]) 、setDefaultEncoding(encoding) |
5. 3 网络操作
如何计算循环节长度呢?
想想我们手算,如果余数比除数小,我们会在后面补0然后再除。也就是说补零之后的数是下一次的被除数。如果被除数重复出现,除数确定的,那么商和余数也就是一样的。这时,循环节就出现了。
下面的函数分为两部分,
补零操作,查找有没有同样的被除数存在,如果有,就找到了循环节,计算循环节长度并返回。在没有找到的前提下,把当前被除数记录下来,并得到余数作为下一次的被除数。
这里需要注意,如果某一次出现了除尽的情况,说明该分数是有限小数,循环节长度是0。
console.log(foo(11, 3));
console.log(foo(1, 7));;
console.log(foo(2, 10));;
function foo(a, b) {
if (!(a % b)) {
return `${a}÷${b}:${a / b}`;
}
var all = (a / b).toString().split('.');
var i = getCycleSection(a, b);
if(i)
return `${a}÷${b}: ${all[0]}.{${all[1].substr(0, i) }}`;
return `${a}÷${b}: ${a / b}`;
}
function getCycleSection(n, m) {
var temp = [];
while (true) {
while (n < m) {
n *= 10;
}
var index = temp.indexOf(n);
if (index >= 0) {
return temp.length - index;
}
temp.push(n);
n %= m;
if (!n)
return 0;
}
}
六、Express框架
Express框架是后台的Node框架,所以和jQuery、zepto、yui、bootstrap都不是一个东西。
Express在后台的受欢迎的程度,和jQuery一样,就是企业的事实上的标准。
- 原生Node开发,会发现有很多问题。比如:
- 呈递静态页面很不方便,需要处理每个HTTP请求,还要考虑304问题
- 路由处理代码不直观清晰,需要写很多正则表达式和字符串函数
- 不能集中精力写业务,要考虑很多其他的东西
安装Express框架npm install --save express
--save参数,表示自动修改package.json文件,自动添加依赖项
路由能力:
var express = require("express");
var app = express();
app.get("/", function(req,res){
res.send("你好");
});
app.get("/haha", function(req,res){
res.send("这是haha页面,哈哈哈哈哈哈");
});
app.get(/^\/student\/([\d]{10})$/, function(req,res){
res.send("学生信息,学号" + req.params[0]);
});
app.get("/teacher/:gonghao", function(req,res){
res.send("老师信息,工号" + req.params.gonghao);
});
app.listen(3000);
静态文件伺服能力:
app.use(express.static("./public"));
模板引擎:
var express = require("express");
var app = express();
app.set("view engine", "ejs");
app.get("/", function(req,res){
res.render("haha",{
"news": ["我是小新闻啊","我也是啊","哈哈哈哈"]
});
});
app.listen(3000);
我们学习的是Express4.X,和Express3.X差别非常大。
6.1 路由也是中间件
请求方式 | 代码 | 说明 |
---|---|---|
get | app.get("网址", function(req,res){...}); | GET参数: ?、锚点# 后面的会被忽略;路由到/a,实际/a?id=2&sex=nan 也能被处理 |
post | app.post("网址", function(req,res){...}); | 网址不分大小写,也可用正则表达式/^\/student\/([\d]{10})$/ ,正则表达式中,未知部分用圆括号分组,然后可以用req.params[0]、[1]获取 |
all | app.all("/", function(){...}); | 网址也可用冒号/student/:id ,req.params["id"] 获取 |
适合进行 RESTful路由设计。简单说,就是一个路径,但是http method不同,对这个页面的使用也不同。
6.2 中间件
路由
如果get、post回调函数中没有next参数,那么只匹配第一个路由,就不会往下匹配了。如果想往下匹配需要写next()
app.get("/", function(req,res,next){
console.log("1");
next();
});
app.get("/", function(req,res){
console.log("2");
});
下面两个路由,感觉没有关系:
app.get("/:username/:id",function(req,res){
console.log("1");
res.send("用户信息" + req.params.username);
});
app.get("/admin/login",function(req,res){
console.log("2");
res.send("管理员登录");
});
但是实际上冲突了,因为admin可以当做用户名 login也可以当做id。
解决方法1:交换位置。 也就是说,express中所有的路由(中间件)的顺序至关重要。匹配上第一个,就不会往下匹配了。 具体的往上写,抽象的往下写。
解决方法2: 检索数据库,如果username不存在,那么next()
app.get("/:username/:id",function(req,res,next){
var username = req.params.username;
// 检索数据库,如果username不存在,那么next()
if(检索数据库) {
console.log("1");
res.send("用户信息");
} else {
next();
}
});
app.get("/admin/login",function(req,res){
console.log("2");
res.send("管理员登录");
});
路由get、post就是中间件,中间件讲究顺序,匹配上第一个之后,就不会往后匹配了。next函数才能够继续往后匹配。
app.use()
app.use()也是一个中间件。与get、post不同的是,他的网址不是精确匹配的。而是能够有小文件夹拓展的。
app.use("/admin", function(req,res){
res.write(req.originalUrl + "\n"); // /admin/aa/bb/cc/dd
res.write(req.baseUrl + "\n"); // /admin
res.write(req.path + "\n"); // /aa/bb/cc/dd
res.end("你好");
});
// 当不写路径的时候,实际上就相当于"/",就是所有网址
app.use(function(req,res,next){
console.log(new Date());
next();
});
app.use() 给了我们增加一些特定功能的便利场所,实际上app.use()的东西,基本上都能从第三方得到。
-
大多数情况下,渲染内容用
res.render()
,将会根据views中的模板文件进行渲染 -
不使用views文件夹,自己设置文件夹名字
app.set("views","aaaa");
-
写一个快速测试页,使用
res.send()
。将根据内容自动设置Content-Type头部和200状态码
-
send()只能用一次,和end一样。和end不一样在哪里?能够自动设置MIME类型
-
使用不同的状态码,
res.status(404).send('Sorry, we cannot find that!');
-
使用不同的Content-Type,
res.set('Content-Type', 'text/html');
GET请求和POST请求的参数
-
GET请求的参数
- 在URL中
- 在原生Node中,需要使用url模块来识别参数字符串
- 在Express中,不需要使用url模块了。可以直接使用req.query对象。
-
POST请求
- 在express中不能直接获得,必须使用body-parser模块。
- 使用后,将可以用req.body得到参数。
- 表单中含有文件上传,需要使用formidable模块。
Node中全是回调函数,所以我们自己封装的函数,里面如果有异步的方法,比如I/O要用回调函数的方法封装。
错误:
res.reder("index",{
"name" : student.getDetailById(234234).name
});
正确:
student.getDetailByXueHao(234234,function(detail){
res.render("index",{
"name" : detail.name
})
});
一、Node简介
1.1 客户端的JavaScript是怎样的
问题 | 答 |
---|---|
什么是 JavaScript | 脚本语言 运行在浏览器中 一般用来做客户端页面的交互(Interactive) |
JavaScript 的运行环境 | 浏览器内核中的 JS 引擎(engine) |
JavaScript 只可以运行在浏览器中吗 | 不是,运行在哪取决于这个环境有没有特定平台 |
浏览器中的 JavaScript 可以做什么 | BOM(页面跳转、历史记录)、操作DOM(对DOM的增删改、注册事件) AJAX / 跨域 ECMAScript |
浏览器中的 JavaScript 不可以做什么 | 文件操作(文件和文件夹的CRUD) 没有办法操作系统信息,因为运行环境特殊(代码在不认识的人的浏览器中运行) |
开发能力相同时,编程语言的能力取决于 | 运行该语言的平台(环境),因为语言本身只是提供定义变量,定义函数,定义类型,流程控制,循环结构之类的操作 |
语言 | 特点 | 平台 |
---|---|---|
Java | 既是语言也是平台 | 运行在Java虚拟机(跨操作系统) |
PHP | 既是语言也是平台 | |
C# | 既是语言也是平台 | .NET Framework(Windows) MONO(因为有人需要将C#运行在Linux平台,所以出现了MONO) |
Node | 只是平台 |
1.2 什么是Node
- Node 就是 JavaScript 语言在服务器端的运行环境,不是一门语言,也不是JavaScript的框架
- 所谓“运行环境(平台)”有两层意思:
- 首先,JavaScript 语言通过 Node 在服务器运行,在这个意义上,Node 有点像 JavaScript 虚拟机
- 其次,Node 提供大量工具库,使得 JavaScript 语言与操作系统互动(比如读写文件、新建子进程),在这个意义上, Node 又是 JavaScript 的工具库
1.2.1 简介
Node是服务器的程序,写的js语句,都将运行在服务器上。返回给客户的都是已经处理好的纯html。
如果想修改程序,必须中断当前运行的服务器,重新node一次,刷新
**ctrl+c,就可以打断挂起的服务器程序。**此时按上箭头,能够快速调用最近的node命令。
本地写一个js不能直接拖入浏览器运行,但是有了node,任何一个js文件都可以通过node来运行。也就是说,node是一个js的执行环境。
要跑起来的服务器的脚本要以.js存储,是一个js文件。用node命令运行这个js文件。
Node没有根目录的概念,因为它根本没有任何的web容器!
让node提供一个静态服务,都非常难!也就是说node中,如果看见一个网址是127.0.0.1:3000/fang
,一定有一个文件夹,叫做fang了。可能/fang的物理文件,是同目录的test.html
URL和真实物理文件,是没有关系的。URL是通过了Node的顶层路由设计,呈递某一个静态文件的。
-
Node不是一种独立的语言,与PHP、JSP、Python、Perl、Ruby的 “既是语言,也是平台” 不同,Node使用JavaScript进行编程,运行在JavaScript引擎上(V8)。
-
与PHP、JSP等相比(PHP、JSP、.net都需要运行在服务器程序上Apache、Naginx、Tomcat、IIS),Node跳过了Apache、Naginx、IIS等HTTP服务器,它自己不用建设在任何服务器软件之上。
-
Node 许多设计理念与经典架构(LAMP = Linux + Apache + MySQL + PHP)有着很大的不同,可以提供强大的伸缩能力。
-
Node 没有web容器
-
Node 花最小的硬件成本,追求更高的并发,更高的处理性能。
1.2.2 特点
所谓的特点,就是Node是如何解决服务器高性能瓶颈问题的。
单线程
在Java、PHP或者.net等服务器端语言中,会为每一个客户端连接创建一个新的线程。而每个线程需要耗费大约2MB内存。也就是说,理论上,一个8GB内存的服务器可以同时连接的最大用户数为4000个左右。要让Web应用程序支持更多的用户,就需要增加服务器的数量,而Web应用程序的硬件成本当然就上升了。
Node不为每个客户连接创建一个新的线程,而仅仅使用一个线程。当有用户连接了,就触发一个内部事件,通过非阻塞I/O、事件驱动机制,让Node程序宏观上也是并行的。使用Node一个8GB内存的服务器,可以同时处理超过4万用户的连接。
另外,单线程带来的好处 还有操作系统完全不再有线程创建、销毁的时间开销。
坏处,就是一个用户造成了线程的崩溃,整个服务都崩溃了,其他人也崩溃了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0g3UejNO-1647412079939)(.\images\wps6A27.tmp.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ISikICm-1647412079940)(.\images\wps646F.tmp.jpg)]
多线程、单线程的一个对比。
也就是说,单线程也能造成宏观上的“并发”。
非阻塞I/O non-blocking I/O
在传统的单线程处理机制中,在执行了访问数据库代码之后,整个线程都将暂停下来,等待数据库返回结果,才能执行后面的代码。也就是说,I/O阻塞了代码的执行,极大地降低了程序的执行效率。
由于Node中采用了非阻塞型I/O机制,因此在执行了访问数据库的代码之后,将立即转而执行其后面的代码,把数据库返回结果的处理代码放在回调函数中,从而提高了程序的执行效率。
当某个I/O执行完毕时,将以事件的形式通知执行I/O操作的线程,线程执行这个事件的回调函数。为了处理异步I/O,线程必须有事件循环,不断的检查有没有未处理的事件,依次予以处理。
阻塞模式下,一个线程只能处理一项任务,要想提高吞吐量必须通过多线程。**而非阻塞模式下,一个线程永远在执行计算操作,这个线程的CPU核心利用率永远是100%。**所以,这是一种特别有哲理的解决方案:与其人多,但是好多人闲着;还不如一个人玩命,往死里干活儿。
事件驱动event-driven
在Node中,客户端请求建立连接,提交数据等行为,会触发相应的事件。在Node中,在一个时刻,只能执行一个事件回调函数,但是在执行一个事件回调函数的中途,可以转而处理其他事件(比如,又有新用户连接了),然后返回继续执行原事件的回调函数,这种处理机制,称为“事件环”机制。
Node 底层是C++(V8也是C++写的)。**底层代码中,近半数都用于事件队列、回调函数队列的构建。**用事件驱动来完成服务器的任务调度,这是鬼才才能想到的。针尖上的舞蹈,用一个线程,担负起了处理非常多的任务的使命。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NL9RaMpi-1647412079940)(.\images\wps3BEA.tmp.jpg)]
单线程,单线程的好处,减少了内存开销,操作系统的内存换页。
如果某一个事情,进入了,但是被I/O阻塞了,所以这个线程就阻塞了。
非阻塞I/O, 不会傻等I/O语句结束,而会执行后面的语句。
非阻塞就能解决问题了么?比如执行着小红的业务,执行过程中,小刚的I/O回调完成了,此时怎么办??
事件机制,事件环,不管是新用户的请求,还是老用户的I/O完成,都将以事件方式加入事件环,等待调度。
说是三个特点,实际上是一个特点,离开谁都不行,都玩儿不转了。
Node 很像抠门的餐厅老板,只聘请1个服务员,服务很多人。结果,比很多服务员效率还高。
Node 中所有的I/O都是异步的,回调函数套回调函数。
1.2.3 适合开发什么?
Node适合用来开发什么样的应用程序呢?
善于I/O,不善于计算。因为Node最擅长的就是任务调度,如果你的业务有很多的CPU计算,实际上也相当于这个计算阻塞了这个单线程,就不适合Node开发。
当应用程序需要处理大量并发的I/O,而在向客户端发出响应之前,应用程序内部并不需要进行非常复杂的处理的时候,Node非常适合。Node也非常适合与web socket配合,开发长连接的实时交互应用程序。
比如:
● 用户表单收集
● 考试系统
● 聊天室
● 图文直播
● 提供JSON的API(为前台Angular使用)
1.2.4 Node.js无法挑战老牌3P
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u5FiJjPq-1647412079941)(.\images\wps716C.tmp.jpg)]
1.3 环境配置
安装包的方式安装后更新版本
- 操作方式:重新下载最新的安装包,覆盖安装
- 问题:
- 以前版本安装的很多全局的工具包需要重新安装
- 无法回滚到之前的版本
- 无法在多个版本之间切换(很多时候我们要使用特定版本)
NVM工具的使用
Node Version Manager(Node版本管理工具)
由于以后的开发工作可能会在多个Node版本中测试,而且Node的版本也比较多,所以需要这么款工具来管理
安装操作步骤
- 下载:nvm-windows
- 解压到一个全英文路径
- 编辑解压目录下的
settings.txt
文件(不存在则新建)
root 配置为当前 nvm.exe 所在目录
path 配置为 node 快捷方式所在的目录
arch 配置为当前操作系统的位数(32/64)
proxy 不用配置
- 配置环境变量 可以通过 window+r : sysdm.cpl
NVM_HOME = 当前 nvm.exe 所在目录
NVM_SYMLINK = node 快捷方式所在的目录
PATH += %NVM_HOME%;%NVM_SYMLINK%;
- 打开CMD通过
set [name]
命令查看环境变量是否配置成功 - PowerShell中是通过
dir env:[name]
命令
-
NVM使用说明:https://github.com/coreybutler/nvm-windows/
-
NPM的目录之后使用再配置
配置Python环境
Node中有些第三方的包是以C/C++源码的方式发布的,需要安装后编译,确保全局环境中可以使用python命令
环境变量的概念
环境变量就是操作系统提供的系统级别用于存储变量的地方
- Windows中环境变量分为系统变量和用户变量
- 环境变量的变量名是不区分大小写的
- 特殊值:
- PATH 变量:只要添加到 PATH 变量中的路径,都可以在任何目录下搜索
1.4 Windows下常用的命令行操作
- 切换当前目录(change directory):cd
- 创建目录(make directory):mkdir
- 查看当前目录列表(directory):dir
- 别名:ls(list)
- 清空当前控制台:cls
- 别名:clear
- 删除文件:del
- 别名:rm
注意:所有别名必须在新版本的 PowerShell 中使用
总结
怪异特点:单线程、Non-blocking I/O、Event Driven。 实际上是一个特点。
首先,Node不为每个用户开辟一个线程,所以非常极端的选择了单线程。单线程,要照顾所有的用户,那么就必须有非阻塞I/O,否则一个人的I/O就把别人、自己都阻塞了。一旦有非阻塞I/O,一个人如果I/O去了,就会放弃CPU的使用权,换成另一个人使用CPU(或者执行此人后面的语句)。所以CPU的利用率100%。第一个人I/O结束了,就要用事件来通知线程,执行回调函数。此时必须有事件环,就有一个排队调度机制。Node中有超过半数的C++代码,在搭建事件环。
Node和别的老牌3P不一样:
1) 没有自己的语法,使用V8引擎,所以就是JS。V8引擎解析JS的,效率非常高,并且V8中很多东西都是异步的。Node就是将V8中的一些功能自己没有重写(别人做了,自己就站在巨人肩膀上),移植到了服务器上。
2) 没有web容器,就是安装配置完成之后,没有一个根目录。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RvausraW-1647412079942)(.\images\wps7194.tmp.jpg)]
命令提示符所在路径太重要了,因为程序中的所有相对路径”./”,都是相对这个命令提示符路径的,而不是相对于js文件自己。
系统中默认是http端口80。所以当没有端口号的时候,就是80端口。
二、模块
2.1 模块介绍
在Node中,以模块为单位划分所有功能,并且提供了一个完整的模块加载机制,可以将应用程序划分为各个不同的部分。不可能用一个js文件去写全部的业务,肯定要有MVC。
-
狭义的说,每一个JavaScript文件都是一个模块
-
多个JavaScript文件之间可以相互require,他们共同实现了一个功能,整体对外,又称为一个广义上的模块。
-
Node中,一个JavaScript文件中定义的变量、函数,都只在这个文件内部有效。
- 当需要从此JS文件外部引用这些变量、函数时,必须使用exports对象进行暴露。使用者要用require()命令引用这个JS文件。
- 一个JavaScript文件,可以向外exports无数个变量、函数。但是require的时候,仅仅需要require这个JS文件一次。使用它的变量、函数的时候,用点语法即可。所以无形之中,增加了一个顶层命名空间。
- 在一个JavaScript文件中,描述一个类。用
module.export = 构造函数名;
的方式向外暴露一个类
-
Node中,js文件至今就是被一个个exports和require构建成为网状的。不是靠html文件统一在一起的
-
require 方法加载规则
- 优先从缓存加载
- 核心模块
- 路径形式的模块
- 第三方模块
- node_modules
-
exports 和 module.exports 的区别
- 每个模块中都有一个 module 对象,module 对象中有一个 exports 对象
- Node 为了方便,同时在每一个模块中都提供了一个成员叫:
exports
示例 优缺点 module.exports moudle.exports.xxx = xxx
导出成员太多时比较麻烦 exports expots.xxx = xxx
导出单个成员不适用,因为每个模块最终向外 return
的是module.exports
,而exports
只是module.exports
的一个引用exports === module.exports
结果为true
exports = module.exports
可以重新建立引用关系
foo.js文件中的代码:
var msg = "你好";
var info = "呵呵";
function showInfo(){
console.log(info);
}
exports.msg = msg;
exports.info = info;
exports.showInfo = showInfo;
使用者:
var foo = require("./test/foo.js"); // 相当于增加了顶层变量foo。所有的函数、变量都要从这个顶层变量走
console.log(foo.msg);
console.log(foo.info);
foo.showInfo();
js文件和js文件之间有两种合作的模式:
1) 某一个js文件,提供了函数,供别人使用。只需要暴露函数exports.msg = msg;
2) 某一个js文件,描述了一个类。 module.exports = People;
-
如果在require命令中不写相对路径:
var foo = require("foo.js");
Node将该文件视为node_modules目录下的一个文件 -
node_modules文件夹并不一定在同级目录里面,也可以在任何祖先级目录中。甚至可以放到NODE_PATH环境变量的文件夹中。好处是:分享项目的时候,不需要带着modules一起给别人。
-
使用文件夹来管理模块,比如
var bar = require("bar");
Node将寻找node_modules目录下的bar文件夹中的index.js执行
每一个模块文件夹中,推荐都写一个package.json文件,这个文件的名字不能改,放到模块文件夹的根目录。node将自动读取里面的配置。有一个main项就是入口文件:
{
"name": "kaoladebar",
"version": "1.0.1",
"main" : "app.js" // 入口文件
}
require()别的js文件的时候,将执行那个js文件。
注意:require()中的路径,是从当前这个js文件出发,找到别人。而fs是从命令提示符找到别人。
桌面上有一个a.js, test文件夹中有b.js、c.js、1.txt
a要引用b:var b = require("./test/b.js");
b要引用c:var b = require("./c.js");
但是,fs等其他的模块用到路径的时候,都是相对于cmd命令光标所在位置。
所以,在b.js中想读1.txt文件,推荐用绝对路径:
fs.readFile(__dirname + "/1.txt",function(err,data){
if(err) { throw err; }
console.log(data.toString());
});
2.2 核心模块
核心模块的意义
- 如果只是在服务器运行JavaScript代码,意义并不大,因为无法实现任何功能(读写文件,访问网络)
- Node 的用处在于它本身还提供的一系列功能模块,用于与操作系统互动
- 这些核心的功能模块在 Node 中内置
内置如下模块:
- path:处理文件路径。
- fs:操作文件系统。
- child_process:新建子进程。
- util:提供一系列实用小工具。
- http:提供HTTP服务器功能。
- url:用于解析URL。
- querystring:解析URL中的查询字符串。
- crypto:提供加密和解密功能。
- 其他
2.3 HTTP模块
// require表示引包,引包就是引用自己的一个特殊功能
var http = require("http"); // 引用模块
// 创建服务器,参数是一个回调函数,表示如果有请求进来,要做什么
var server = http.createServer(function(req,res){
// req表示请求,request; res表示响应,response
console.log(req.url) // 用户的请求URL地址
// 设置HTTP头部,状态码是200,文件类型是html,字符集是utf8
res.writeHead(200,{"Content-type":"text/html;charset=UTF-8"});
res.end("哈哈哈哈,我买了一个iPhone" + (1+2+3) + "s");
});
// 运行服务器,监听3000端口(端口号可以任改)
server.listen(3000,"127.0.0.1");
设置一个响应头:res.writeHead(200,{"Content-Type":"text/plain;charset=UTF8"});
识别req.url用到两个新模块,第一个就是url模块,第二个就是querystring模块
字符串查询,用querystring处理
querystring.parse('foo=bar&baz=qux&baz=quux&corge')
// 返回 { foo: 'bar', baz: ['qux', 'quux'], corge: '' }
// Suppose gbkDecodeURIComponent function already exists, it can decode `gbk` encoding string
querystring.parse('w=%D6%D0%CE%C4&foo=bar', null, null, { decodeURIComponent: gbkDecodeURIComponent })
// 返回 { w: '中文', foo: 'bar' }
2.4 post请求
var alldata = "";
// 下面是post请求接收的一个公式
// node为了追求极致,它是一个小段一个小段接收的。
// 接受了一小段,可能就给别人去服务了。防止一个过大的表单阻塞了整个进程
req.addListener("data",function(chunk){
alldata += chunk;
});
//全部传输完毕
req.addListener("end",function(){
console.log(alldata.toString());
res.end("success");
});
原生写POST处理,比较复杂,要写两个监听。文件上传业务比较难写。所以,用第三方模块formidable。
只要涉及文件上传,那么form标签要加一个属性:
<form action="http://127.0.0.1/dopost" method="post" enctype="multipart/form-data">
三、模板引擎
<a href="<%= url %>"><img src="<%= imageURL %>" alt=""></a>
数据绑定,就成为一个完整的html字符串了。
后台模板,著名的有两个,第一个叫做ejs; 第二个叫做jade。是npm第三方包。
3.1 EJS
Embedded JavaScript templates
后台模板引擎
<ul>
<% for(var i = 0 ; i < news.length ; i++){ %>
<li><%= news[i] %></li>
<% } %>
</ul>
var dictionary = {
a: 6,
news : ["1期班太牛逼了","高薪就业","哈哈哈哈哈"]
};
四、NPM、文件操作、缓冲区
4.1 Node Package
由于Node是一套轻内核的平台,虽然提供了一系列的内置模块,但是不足以满足开发者的需求,于是乎出现了包(Package)的概念。与核心模块类似,就是将一些预先设计好的功能或者说API封装到一个文件夹,提供给开发者使用
包的加载机制
- 与内置模块相同,包的加载同样使用
require
方法
const express = require('express');
-
加载机制也和内置模块加载机制相同
-
加载注意事项:
- 先在系统核心(优先级最高)的模块中找
const fs = require('fs'); // 永远加载内部核心模块fs
- 然后再到当前项目中 node_modules 目录中找
如何管理好自己的包
- 由于
Node
本身并没有太多的功能性API
,所以市面上涌现出大量的第三方人员开发出来的Package
- 包的生态圈一旦繁荣起来,就必须有工具去代替人脑或者文档的方式管理
- 这时候
NPM
诞生了
NPM
-
随着时间的发展,NPM 出现了两层概念:
- 一层含义是 Node 的开放式模块登记和管理系统,亦可以说是一个生态圈,一个社区
- 另一层含义是 Node 默认的模块管理器,是一个命令行下的软件,用来安装和管理 Node 模块
-
官方链接: https://www.npmjs.com/
-
国内加速镜像: https://npm.taobao.org/
安装NPM
- NPM 不需要单独安装。默认在安装 Node 的时候,会连带一起安装 NPM
- 但是,Node 附带的 NPM 可能不是最新版本,最好用下面的命令,更新到最新版本
$ npm install npm -g
- 默认安装到当前系统 Node 所在目录下
- 由于之前使用 NVM 的方式安装的 Node 所以需要重新配置 NPM 的全局目录
配置NPM的全局目录
$ npm config set prefix [pathtonpm]
- 将NPM目录配置到其他目录时,必须将该目录放到环境变量中,否则无法在全局使用
常用NPM命令
- https://docs.npmjs.com/
npm config [ls|list|set|get] [name] [value]
npm init [--yes|-y]
npm search [name]
npm info [name]
npm install [--global|-g] [name]
npm uninstall [--global|-g] [name]
npm list [--global|-g]
npm outdated [--global|-g]
npm update [--global|-g] [name]
npm run [task]
npm cache [clean]
4.2 文件操作
相关模块
Node内核提供了很多与文件操作相关的模块,每个模块都提供了一些最基本的操作API,在NPM中也有社区提供的功能包
-
fs:基础的文件操作 API
-
path:提供和路径相关的操作 API
-
readline:用于读取大文本文件,一行一行读
-
fs-extra(第三方):https://www.npmjs.com/package/fs-extra
4.2.1 同步或异步调用
fs模块对文件的几乎所有操作都有同步和异步两种形式。例如:readFile() 和 readFileSync()
调用方式 | 阻塞代码执行 | 什么时候回调 | 异常处理 |
---|---|---|---|
同步 - readFileSync | 阻塞 | try catch | |
异步 - readFile | 不阻塞 | 将读取任务下达到任务队列,直到任务执行完成才会回调 | 通过回调函数的第一个参数 |
// 同步
console.time('sync');
try {
var data = fs.readFileSync(path.join('C:\\Users\\iceStone\\Downloads', 'H.mp4'));
} catch (error) {
throw error;
}
console.timeEnd('sync');
// 异步
console.time('async');
fs.readFile(path.join('C:\\Users\\iceStone\\Downloads', 'H.mp4'), (error, data) => {
if (error) throw error;
});
console.timeEnd('async');
4.2.2 路径模块
在文件操作的过程中,都必须使用物理路径(绝对路径),path模块提供了一系列与路径相关的 API
path.join
:拼接多个路径部分,并转化为正常格式path.basename(temp)
:获取路径的文件名(包含默认扩展名)path.basename(temp, '.lrc')
:获取路径中的文件名并排除扩展名process.platform
:操作系统path.delimiter
:路径分隔符process.env.PATH.split(path.delimiter)
:一般用于分割环境变量path.dirname(temp)
:获取一个路径中的目录部分path.extname(temp)
:获取一个路径中最后的扩展名path.parse(temp)
:将一个路径解析成一个对象的形式- root根路径
- dir目录
- base包含后缀名的文件名
- ext后缀名
- name不包含后缀名的文件名
path.format(pathObject)
:将一个路径对象再转换为一个字符串的形式path.isAbsolute(temp)
:获取一个路径是不是绝对路径path.normalize('.../a.txt')
:将一个路径转换为当前系统默认的标准格式,并解析其中的./和…/path.relative(__dirname, temp)
:获取第二个路径相对第一个路径的相对路径path.resolve(temp, 'c:/', './develop', '../application')
:以类似命令行cd命令的方式拼接路径path.sep
:获取不同平台中路径的分隔符(默认)path === path.win32
:允许在任意平台下以WIN32的方法调用PATH对象path === path.posix
:允许在任意平台下以POSIX的方法调用PATH对象
源码地址:
https://github.com/nodejs/node/blob/master/lib/path.js
4.2.3 文件读取
fs.readFile(file[, options], callback(error, data))
fs.readFile('c:\\demo\1.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});
fs.readFileSync(file[, options])
try {
const data = fs.readFileSync('c:\\demo\1.txt', 'utf8');
console.log(data);
} catch(e) {
// 文件不存在,或者权限错误
throw e;
}
fs.createReadStream(path[, options])
const stream = fs.createReadStream('c:\\demo\1.txt');
let data = ''
stream.on('data', (trunk) => {
data += trunk;
});
stream.on('end', () => {
console.log(data);
});
由于Windows平台下默认文件编码是GBK,在Node中不支持,可以通过iconv-lite解决
Readline模块逐行读取文本内容
const readline = require('readline');
const fs = require('fs');
const rl = readline.createInterface({
input: fs.createReadStream('sample.txt')
});
rl.on('line', (line) => {
console.log('Line from file:', line);
});
4.2.4 文件写入
fs.writeFile(file, data[, options], callback(error))
fs.writeFile('c:\\demo\a.txt', new Date(), (error) => {
console.log(error);
});
fs.writeFileSync(file, data[, options])
try {
fs.writeFileSync('c:\\demo\a.txt', new Date());
} catch (error) {
// 文件夹不存在,或者权限错误
console.log(error);
}
fs.createWriteStream(path[,option])
var streamWriter = fs.createWriteStream('c:\\demo\a.txt');
setInterval(() => {
streamWriter.write(`${new Date}\n`, (error) => {
console.log(error);
});
}, 1000);
fs.appendFile(file,data[,options],callback(err))
// 相比较之前文件流的方式,这种方式不会占用文件资源,append完成就会释放
setInterval(() => {
fs.appendFile('c:\\demo\a.txt',`${new Date}\n`, (error) => {
console.log(error);
});
}, 1000);
fs.appendFileSync(file,data[,options])
setInterval(() => {
fs.appendFileSync('c:\\demo\a.txt',`${new Date}\n`);
}, 1000);
4.2.5 其他常见文件操作
文件操作 | 异步 | 同步 |
---|---|---|
验证路径是否存在 | fs.exists(path,callback(exists)) | fs.existsSync(path) 返回布尔类型 exists |
获取文件信息 | fs.stat(path,callback(err,stats)) | fs.statSync(path) 返回一个fs.Stats实例 |
移动文件或重命名文件或目录 | fs.rename(oldPath,newPath,callback) | fs.renameSync(oldPath,newPath) |
删除文件 | fs.unlink(path,callback(err)) | fs.unlinkSync(path) |
4.2.6 其他常见文件夹操作
文件夹操作 | 异步 | 同步 |
---|---|---|
创建一个目录 | fs.mkdir(path[,model],callback) | fs.mkdirSync(path[,model]) |
删除一个空目录 | fs.rmdir(path,callback) | fs.rmdirSync(path) |
读取一个目录 | fs.readdir(path,callback(err,files)) | fs.readdirSync(path) 返回files |
4.2.7 文件监视
利用文件监视实现自动 markdown 文件转换
-
相关链接:
- https://github.com/chjj/marked
- https://github.com/Browsersync/browser-sync
-
实现思路:
- 利用
fs
模块的文件监视功能监视指定MD文件 - 当文件发生变化后,借助
marked
包提供的markdown
tohtml
功能将改变后的MD文件转换为HTML - 再将得到的HTML替换到模版中
- 最后利用BrowserSync模块实现浏览器自动刷新
- 利用
const fs = require('fs');
const path = require('path');
var marked = require('marked');
var bs = require('browser-sync').create();
var target = path.join(__dirname, process.argv[2] || './README.md');
var filename = path.basename(target, path.extname(target)) + '.html';
var targetHtml = path.join(path.dirname(target), filename);
bs.init({
server: path.dirname(target),
index: filename,
notify: false
});
bs.reload(filename);
var template = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<style>{{{styles}}}</style>
</head>
<body>
<article class="markdown">
{{{body}}}
</article>
</body>
</html>
`;
fs.readFile(path.join(__dirname, './markdown.css'), 'utf8', (error, css) => {
if (error) throw error;
template = template.replace('{{{styles}}}', css);
var handler = (current, previous) => {
fs.readFile(target, 'utf8', (error, content) => {
var html = template.replace('{{{body}}}', marked(content));
fs.writeFile(targetHtml, html, (error) => {
if (!error) {
console.log(`updated@${new Date()}`);
bs.reload(filename);
}
});
});
};
handler();
fs.watchFile(target, { interval: 100 }, handler);
});
4.3 缓冲区处理
什么是缓冲区
- 缓冲区就是内存中操作数据的容器,只是数据容器而已
- 通过缓冲区可以很方便的操作二进制数据
- 而且在大文件操作时必须有缓冲区
为什么要有缓冲区
- JavaScript是比较擅长处理字符串,但是早期的应用场景主要用于处理HTML文档,不会有太大篇幅的数据处理,也不会接触到二进制的数据
- 而在Node中操作数据、网络通信是没办法完全以字符串的方式操作的
- 所以在Node中引入了一个二进制的缓冲区的实现:Buffer
五、文件流、网络操作
5.1 文件流
什么是流
- 在程序开发的概念中
- 流是程序输入或输出的一个连续的字节序列
- 文件流、网络流
- 设备(例如鼠标、键盘、磁盘、屏幕、调制解调器和打印机)的输入和输出都是用流来处理的
5.2 Node中的流操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b3Mt5jen-1647412079943)(.\images\02.png)]
在 Node 核心模块 fs 中定义了一些与流相关的 API
fs.createReadStream()
得到一个ReadableStreamfs.createWriteStream()
得到一个WritableStream
5.2.1 读取流 / 写入流常用 API
Event | Method | |
---|---|---|
读取流 | data、end、error | read([size])、 pause()、 isPause()、 resume()、 setEncoding(encoding)、 pipe(destination[, options])、 unpipe([destination]) |
写入流 | error、pipe | write(chunk[, encoding][, callback]) 、end([chunk][, encoding][, callback]) 、setDefaultEncoding(encoding) |
5. 3 网络操作
如何计算循环节长度呢?
想想我们手算,如果余数比除数小,我们会在后面补0然后再除。也就是说补零之后的数是下一次的被除数。如果被除数重复出现,除数确定的,那么商和余数也就是一样的。这时,循环节就出现了。
下面的函数分为两部分,
补零操作,查找有没有同样的被除数存在,如果有,就找到了循环节,计算循环节长度并返回。在没有找到的前提下,把当前被除数记录下来,并得到余数作为下一次的被除数。
这里需要注意,如果某一次出现了除尽的情况,说明该分数是有限小数,循环节长度是0。
console.log(foo(11, 3));
console.log(foo(1, 7));;
console.log(foo(2, 10));;
function foo(a, b) {
if (!(a % b)) {
return `${a}÷${b}:${a / b}`;
}
var all = (a / b).toString().split('.');
var i = getCycleSection(a, b);
if(i)
return `${a}÷${b}: ${all[0]}.{${all[1].substr(0, i) }}`;
return `${a}÷${b}: ${a / b}`;
}
function getCycleSection(n, m) {
var temp = [];
while (true) {
while (n < m) {
n *= 10;
}
var index = temp.indexOf(n);
if (index >= 0) {
return temp.length - index;
}
temp.push(n);
n %= m;
if (!n)
return 0;
}
}
六、Express框架
Express框架是后台的Node框架,所以和jQuery、zepto、yui、bootstrap都不是一个东西。
Express在后台的受欢迎的程度,和jQuery一样,就是企业的事实上的标准。
- 原生Node开发,会发现有很多问题。比如:
- 呈递静态页面很不方便,需要处理每个HTTP请求,还要考虑304问题
- 路由处理代码不直观清晰,需要写很多正则表达式和字符串函数
- 不能集中精力写业务,要考虑很多其他的东西
安装Express框架npm install --save express
--save参数,表示自动修改package.json文件,自动添加依赖项
路由能力:
var express = require("express");
var app = express();
app.get("/", function(req,res){
res.send("你好");
});
app.get("/haha", function(req,res){
res.send("这是haha页面,哈哈哈哈哈哈");
});
app.get(/^\/student\/([\d]{10})$/, function(req,res){
res.send("学生信息,学号" + req.params[0]);
});
app.get("/teacher/:gonghao", function(req,res){
res.send("老师信息,工号" + req.params.gonghao);
});
app.listen(3000);
静态文件伺服能力:
app.use(express.static("./public"));
模板引擎:
var express = require("express");
var app = express();
app.set("view engine", "ejs");
app.get("/", function(req,res){
res.render("haha",{
"news": ["我是小新闻啊","我也是啊","哈哈哈哈"]
});
});
app.listen(3000);
我们学习的是Express4.X,和Express3.X差别非常大。
6.1 路由也是中间件
请求方式 | 代码 | 说明 |
---|---|---|
get | app.get("网址", function(req,res){...}); | GET参数: ?、锚点# 后面的会被忽略;路由到/a,实际/a?id=2&sex=nan 也能被处理 |
post | app.post("网址", function(req,res){...}); | 网址不分大小写,也可用正则表达式/^\/student\/([\d]{10})$/ ,正则表达式中,未知部分用圆括号分组,然后可以用req.params[0]、[1]获取 |
all | app.all("/", function(){...}); | 网址也可用冒号/student/:id ,req.params["id"] 获取 |
适合进行 RESTful路由设计。简单说,就是一个路径,但是http method不同,对这个页面的使用也不同。
6.2 中间件
路由
如果get、post回调函数中没有next参数,那么只匹配第一个路由,就不会往下匹配了。如果想往下匹配需要写next()
app.get("/", function(req,res,next){
console.log("1");
next();
});
app.get("/", function(req,res){
console.log("2");
});
下面两个路由,感觉没有关系:
app.get("/:username/:id",function(req,res){
console.log("1");
res.send("用户信息" + req.params.username);
});
app.get("/admin/login",function(req,res){
console.log("2");
res.send("管理员登录");
});
但是实际上冲突了,因为admin可以当做用户名 login也可以当做id。
解决方法1:交换位置。 也就是说,express中所有的路由(中间件)的顺序至关重要。匹配上第一个,就不会往下匹配了。 具体的往上写,抽象的往下写。
解决方法2: 检索数据库,如果username不存在,那么next()
app.get("/:username/:id",function(req,res,next){
var username = req.params.username;
// 检索数据库,如果username不存在,那么next()
if(检索数据库) {
console.log("1");
res.send("用户信息");
} else {
next();
}
});
app.get("/admin/login",function(req,res){
console.log("2");
res.send("管理员登录");
});
路由get、post就是中间件,中间件讲究顺序,匹配上第一个之后,就不会往后匹配了。next函数才能够继续往后匹配。
app.use()
app.use()也是一个中间件。与get、post不同的是,他的网址不是精确匹配的。而是能够有小文件夹拓展的。
app.use("/admin", function(req,res){
res.write(req.originalUrl + "\n"); // /admin/aa/bb/cc/dd
res.write(req.baseUrl + "\n"); // /admin
res.write(req.path + "\n"); // /aa/bb/cc/dd
res.end("你好");
});
// 当不写路径的时候,实际上就相当于"/",就是所有网址
app.use(function(req,res,next){
console.log(new Date());
next();
});
app.use() 给了我们增加一些特定功能的便利场所,实际上app.use()的东西,基本上都能从第三方得到。
-
大多数情况下,渲染内容用
res.render()
,将会根据views中的模板文件进行渲染 -
不使用views文件夹,自己设置文件夹名字
app.set("views","aaaa");
-
写一个快速测试页,使用
res.send()
。将根据内容自动设置Content-Type头部和200状态码
-
send()只能用一次,和end一样。和end不一样在哪里?能够自动设置MIME类型
-
使用不同的状态码,
res.status(404).send('Sorry, we cannot find that!');
-
使用不同的Content-Type,
res.set('Content-Type', 'text/html');
GET请求和POST请求的参数
-
GET请求的参数
- 在URL中
- 在原生Node中,需要使用url模块来识别参数字符串
- 在Express中,不需要使用url模块了。可以直接使用req.query对象。
-
POST请求
- 在express中不能直接获得,必须使用body-parser模块。
- 使用后,将可以用req.body得到参数。
- 表单中含有文件上传,需要使用formidable模块。
Node中全是回调函数,所以我们自己封装的函数,里面如果有异步的方法,比如I/O要用回调函数的方法封装。
错误:
res.reder("index",{
"name" : student.getDetailById(234234).name
});
正确:
student.getDetailByXueHao(234234,function(detail){
res.render("index",{
"name" : detail.name
})
});