Node 简介、模块、模板引擎、NPM、文件操作、缓冲区、文件流、网络操作、Express框架

一、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的版本也比较多,所以需要这么款工具来管理

安装操作步骤

  1. 下载:nvm-windows
  2. 解压到一个全英文路径
  3. 编辑解压目录下的settings.txt文件(不存在则新建)
  • root 配置为当前 nvm.exe 所在目录
  • path 配置为 node 快捷方式所在的目录
  • arch 配置为当前操作系统的位数(32/64)
  • proxy 不用配置
  1. 配置环境变量 可以通过 window+r : sysdm.cpl
  • NVM_HOME = 当前 nvm.exe 所在目录
  • NVM_SYMLINK = node 快捷方式所在的目录
  • PATH += %NVM_HOME%;%NVM_SYMLINK%;
  • 打开CMD通过set [name]命令查看环境变量是否配置成功
  • PowerShell中是通过dir env:[name]命令
  1. NVM使用说明:https://github.com/coreybutler/nvm-windows/

  2. 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.exportsmoudle.exports.xxx = xxx导出成员太多时比较麻烦
    exportsexpots.xxx = xxx导出单个成员不适用,因为每个模块最终向外 return 的是 module.exports,而 exports 只是 module.exports 的一个引用
    exports === module.exports 结果为 trueexports = 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 文件转换

  • 相关链接:

    1. https://github.com/chjj/marked
    2. https://github.com/Browsersync/browser-sync
  • 实现思路:

    1. 利用fs模块的文件监视功能监视指定MD文件
    2. 当文件发生变化后,借助marked包提供的markdown to html功能将改变后的MD文件转换为HTML
    3. 再将得到的HTML替换到模版中
    4. 最后利用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() 得到一个ReadableStream
  • fs.createWriteStream() 得到一个WritableStream
5.2.1 读取流 / 写入流常用 API
EventMethod
读取流data、end、errorread([size])、 pause()、 isPause()、 resume()、 setEncoding(encoding)、 pipe(destination[, options])、 unpipe([destination])
写入流error、pipewrite(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 路由也是中间件

请求方式代码说明
getapp.get("网址", function(req,res){...});GET参数: ?、锚点#后面的会被忽略;路由到/a,实际/a?id=2&sex=nan 也能被处理
postapp.post("网址", function(req,res){...});网址不分大小写,也可用正则表达式/^\/student\/([\d]{10})$/,正则表达式中,未知部分用圆括号分组,然后可以用req.params[0]、[1]获取
allapp.all("/", function(){...});网址也可用冒号/student/:idreq.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的版本也比较多,所以需要这么款工具来管理

安装操作步骤

  1. 下载:nvm-windows
  2. 解压到一个全英文路径
  3. 编辑解压目录下的settings.txt文件(不存在则新建)
  • root 配置为当前 nvm.exe 所在目录
  • path 配置为 node 快捷方式所在的目录
  • arch 配置为当前操作系统的位数(32/64)
  • proxy 不用配置
  1. 配置环境变量 可以通过 window+r : sysdm.cpl
  • NVM_HOME = 当前 nvm.exe 所在目录
  • NVM_SYMLINK = node 快捷方式所在的目录
  • PATH += %NVM_HOME%;%NVM_SYMLINK%;
  • 打开CMD通过set [name]命令查看环境变量是否配置成功
  • PowerShell中是通过dir env:[name]命令
  1. NVM使用说明:https://github.com/coreybutler/nvm-windows/

  2. 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.exportsmoudle.exports.xxx = xxx导出成员太多时比较麻烦
    exportsexpots.xxx = xxx导出单个成员不适用,因为每个模块最终向外 return 的是 module.exports,而 exports 只是 module.exports 的一个引用
    exports === module.exports 结果为 trueexports = 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 文件转换

  • 相关链接:

    1. https://github.com/chjj/marked
    2. https://github.com/Browsersync/browser-sync
  • 实现思路:

    1. 利用fs模块的文件监视功能监视指定MD文件
    2. 当文件发生变化后,借助marked包提供的markdown to html功能将改变后的MD文件转换为HTML
    3. 再将得到的HTML替换到模版中
    4. 最后利用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() 得到一个ReadableStream
  • fs.createWriteStream() 得到一个WritableStream
5.2.1 读取流 / 写入流常用 API
EventMethod
读取流data、end、errorread([size])、 pause()、 isPause()、 resume()、 setEncoding(encoding)、 pipe(destination[, options])、 unpipe([destination])
写入流error、pipewrite(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 路由也是中间件

请求方式代码说明
getapp.get("网址", function(req,res){...});GET参数: ?、锚点#后面的会被忽略;路由到/a,实际/a?id=2&sex=nan 也能被处理
postapp.post("网址", function(req,res){...});网址不分大小写,也可用正则表达式/^\/student\/([\d]{10})$/,正则表达式中,未知部分用圆括号分组,然后可以用req.params[0]、[1]获取
allapp.all("/", function(){...});网址也可用冒号/student/:idreq.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
    })
}); 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值