Node.js课程知识讲解大全(二)

二. 模块化

Node.js所有的API都是基于模块发布和使用的,因此在真正的学习Node.js之前,我们需要先了解模块化开发。

2.1 模块化的由来

JavaScript诞生初期,不像成熟的面向对象语言(如Java)拥有与生俱来的模块系统,甚至没有人把它当做一门真正的编程语言。

JavaScript初期引入模块的方式都是通过script标签来引入代码,但这样很容易产生如下问题:

全局的变量污染,并且无法找到源头。

过多的script标签会造成杂乱无章的代码。

使用某个库,却爆出没有相关库的依赖。

无法追踪的错误和报错位置。

为了解决JavaScript模块化问题,程序员们一开始采用「命令空间」的方式来约束代码,让看起来凌乱的编程现状有所缓解。但是这仅仅属于一种约定,并没有从技术上解决根本问题。

2.2 CommonJS规范

Node.js首先采用了CommonJS规范,CommonJS并非一门新的API,也不是标准模块系统,只不过是目前Node.js使用最广泛的模块系统(规范)。CommonJS 规范是为了解决 JavaScript 的作用域问题而定义的模块形式,可以使每个模块它自身的命名空间中执行。

2.2.1 CommonJs 基本使用规则

该规范的主要内容是:

每个js文件就是一个模块,模块有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。

每个模块内部,module变量(默认拥有)代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。

模块必须通过 module.exports 导出对外的变量或接口。

// moduleA.js

varx=5;

varaddX=function(value) {

returnvalue+x;

};

module.exports.x=x;

module.exports.addX=addX;

通过 require() 来导入其他模块的输出到当前模块作用域中。

// moduleB.js

varexample=require('./moduleA.js');

console.log(example.x);// 5

console.log(example.addX(1));// 6

CommonJS模块的特点如下。

所有代码都运行在模块作用域,不会污染全局作用域。

模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。

模块加载的顺序,按照其在代码中出现的顺序。

2.2.2 内部实现原理

正如上面所说,node执行js文件时会默认把文件代码封装到一个module变量中,它是Module的一个实例。

functionModule(id,parent) {

this.id=id;

this.exports={};

this.parent=parent;

// ...

}

每个模块内部,都有一个module对象,代表当前模块。它有以下属性:

module.id 模块的识别符,通常是带有绝对路径的模块文件名。

module.filename 模块的文件名,带有绝对路径。

module.loaded 返回一个布尔值,表示模块是否已经完成加载。

module.parent 返回一个对象,表示调用该模块的模块。

module.children 返回一个数组,表示该模块要用到的其他模块。

module.exports 表示模块对外输出的值。

尝试执行以下代码:

// example.js

varjquery=require('jquery');

exports.$=jquery;

console.log(module);

打印信息:

{id:'.',

exports: {'$': [Function] },

parent:null,

filename:'/path/to/example.js',

loaded:false,

children:

[ {id:'/path/to/node_modules/jquery/dist/jquery.js',

exports: [Function],

parent: [Circular],

filename:'/path/to/node_modules/jquery/dist/jquery.js',

loaded:true,

children: [],

paths: [Object] } ],

paths:

['/home/user/deleted/node_modules',

'/home/user/node_modules',

'/home/node_modules',

'/node_modules']

}

module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。

varexports=module.exports;

可以直接使用exports,但注意,不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系:

exports.area=function(r) {

returnMath.PI*r*r;

};

exports.circumference=function(r) {

return2*Math.PI*r;

};

exports=function(x) {console.log(x)};//这样做不行,因为相当于在文件内部定义一个私有变量了,通过require无法拿到

CommonJS加载是同步的,因此使用CommonJS无法做到按需加载

2.3 ES6中的模块化

ES6模块是ECMA组织从语言层面提出的标准模块化API,未来完全可以取代CommonJS和其他的模块化系统。

2.3.1 基本使用方式

ES6模块的语法完全不同于CommonJS,因为ES6 模块不是对象,而是通过export命令显式指定导出的代码,再通过import命令导入:

// person.js

exportdefaultconstperson={}

exportage=30

使用

importperson,{age}from'person'

2.3.2 模块的加载原理

import 方式导入模块是基于“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。

它具有如下优势:

基于编译时加载,可以让模块在编译阶段就能进行静态语法分析,从而提前预警语法错误或者做类型校验。

未来Node.js和浏览器都将支持,可以一统天下。

ES6模块默认在严格模式下执行,即默认在模块头部加入:"use strict"。

## 附:严格模式主要有以下限制。

- 变量必须声明后再使用

- 函数的参数不能有同名属性,否则报错

- 不能使用`with`语句

- 不能对只读属性赋值,否则报错

- 不能使用前缀 0 表示八进制数,否则报错

- 不能删除不可删除的属性,否则报错

- 不能删除变量`delete prop`,会报错,只能删除属性`delete global[prop]`

- `eval`不会在它的外层作用域引入变量

- `eval`和`arguments`不能被重新赋值

- `arguments`不会自动反映函数参数的变化

- 不能使用`arguments.callee`

- 不能使用`arguments.caller`

- 禁止`this`指向全局对象

- 不能使用`fn.caller`和`fn.arguments`获取函数调用的堆栈

- 增加了保留字(比如`protected`、`static`和`interface`)

尤其注意:顶层的this指向undefined,不应该在顶层代码使用this。

2.3.3 详解import和export命令

模块功能主要由两个命令构成:export和import。

export命令用于规定模块的对外接口。

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取,除非通过export导出:

// profile.js

exportvarfirstName='Michael';

exportvarlastName='Jackson';

exportvaryear=1958;

或者写成:

// profile.js

varfirstName='Michael';

varlastName='Jackson';

varyear=1958;

export{firstName,lastName,yearasage};

export 中的as关键字可以重命名导出的变量名。

export无法直接导出值或变量:

// 报错

export1;

// 报错

varm=1;

exportm;

// 下面是正确的写法

// 写法一

exportvarm=1;

// 写法二

varm=1;

export{m};

// 写法三

varn=1;

export{nasm};

export必须在顶层作用域使用,无法在其他块级作用域使用:

functionfoo() {

exportdefault'bar'// 语法错误

}

foo()

export default 命令可以导出默认的变量,import使用时可以指定任意名字

exportdefaultfunctionabc(){}

或者

functionabc(){}

export{abcasdefault}

使用

importcustomNamefrom'./export-default.js'

或者

import{defaultasabc}from'./export-default.js'

import`命令用于输入其他模块提供的功能。

export导出的接口都通过import来导入,import中也可以通过as对导入的变量进行重命名。

import{lastNameassurname}from'./profile.js';

import导入的变量都是只读的,无法修改。

import{a}from'./xxx.js'

a={};// Syntax Error : 'a' is read-only;

import 会导致导入的模块自动加载执行,并且多次重复执行同一句import语句,只会执行一次:

import'lodash';

import'lodash';//这句就不执行了

模块整体加载可以使用*号:

import*aspersonfromperson

import()方法的动态加载

import命令是基于编译时加载,如果想使用运行时加载该怎么办?

可以使用全局的import()方法,注意:该方法是ES6提案中引入的方法,和import命令完全不是一码事。

import()方法支持传入一个模块路径,返回一个Promise,在then方法中可以拿到模块的引用。

import('./dialogBox.js')

.then(dialogBox=>{

dialogBox.open();

  })

.catch(error=>{

/* Error handling */

  })

2.4 其他模块化解决方案

除了CommonJS被Node.js定为官方模块化标准,以及ES6 Module被ECMA组织定为浏览器支持的原生标准外,在模块化探索年代还产生过一些其他的模块化规范,不过随着时间的流逝,这些都被人们废弃了。

2.4.1 AMD规范

require.js实现了AMD规范的

在ES6模块化方案提出之前,浏览器实现异步加载模块,都是基于AMD规范来实现,其写法如下:

// lib模块

define(['package/lib'],function(lib){

functionhello(){

lib.log('hello world!');

  }

return{

foo:foo

  };

});

使用模块:

require(['lib'],function(lib) {

lib.foo()

});

AMD规范必须一次性把所有依赖加载完毕,也无法做到按需加载,但可以做到异步加载。

2.4.2 CMD规范

sea.js实现了CMD规范

CMD规范是AMD规范的一个变种,为了让AMD规范兼容CommonJS的风格。

define(function(require,exports,module) {

var$=require('jquery');

require.async(['./b','./c'],function(b) {

b.doSomething();

c.doSomething();

  });

exports.doSomething=...

module.exports=...

})

使用模块也是按照如上的方式使用。

CMD相比AMD可以做到异步加载,但目前也几乎每人使用,因为书写太麻烦,而且ES6已经把模块规范化。

三. Node.js常用的内置模块

Node.js中提供了一些原生的模块,我们称之为内置模块。此外,也可以通过NPM命令安装第三方模块,安装完毕后使用方式和内置模块没有什么不同。

Node.js中的原生模块直接通过require就能获得,参考Node.js API文档来使用这些原生模块。接下来,针对一些常用模块进行介绍。

3.1 fs模块

fs 模块以 POSIX 标准函数的方式提供了与文件系统进行交互的API,fs中的每个方法都分同步和异步两种方式调用。通过fs模块可以获取一个目录结构,及其子目录或目录树下文件的特征,甚至可以读写、复制或者删除文件。

什么是POSIX标准

可移植操作系统接口(Portable Operating System Interface of UNIX,缩写为 POSIX ),POSIX标准定义了操作系统应该为应用程序提供的接口标准,保证了接口的可移植。

fs模块中操作文件或文件夹的方式可以基于:同步、异步回调或者异步Promise三种模式。还记得Promise吗?

文件夹读取示例三种方式

constfs=require('fs')

// 使用同步的方式

constdirent=fs.readdirSync(__dirname)

console.log(dirent)

// 使用异步回调的方式

fs.readdir(__dirname, (err,dirent)=>{

if(err) {

console.error(err)

}else{

console.log(dirent)

   }

})

// 使用Promise的方式,目前还不稳定

fs.promises.readdir(__dirname).then((dirent)=>{

console.log(dirent)

}).catch((err)=>{

console.error(err)

})

和文件系统有关的全局变量

__dirname是Node.js提供的全局变量,表示当前正在执行的代码文件所在的目录绝对路径。

__filename表示当前正在执行的代码文件的绝对路径。

注意以上变量和执行环境是无关的,只和代码文件的保存位置有关。

在不同路径的js文件中加入下面代码,通过node命令执行看看

console.log(__dirname)

console.log(__filename)

3.1.1 文件夹的操作

fs.access():检查文件或文件夹是否存在。

fs.readdir():读取文件夹。

fs.mkdir():创建文件夹,如果文件夹已存在会导致错误。

fs.rmdir():删除文件夹,如果文件夹内部有子文件会导致错误。

fs.rename():修改文件或文件夹名字。

constfs=require('fs')

// 以utf-8格式读取目录的子文件名字

fs.readdir('/Users', {encoding:'utf-8'}, (err,files)=>{

if(err) {

console.error(err)

}else{

console.log(files)

   }

})

// 检查文件是否存在

fs.access(__dirname+'/temp',fs.constants.F_OK, (err)=>{

if(err) {

// 报错表示不存在

// 创建文件夹

fs.mkdir(__dirname+'/temp', (err)=>{

if(err) {

console.error(err)

}else{

setTimeout(()=>{

// 2秒之后修改文件夹名为temp2

fs.rename(__dirname+'/temp',__dirname+'/temp2', (err)=>{

if(err)console.error(err)

                   })

},2000);

           }

       })

}else{

// 删除文件夹

fs.rmdir(__dirname+'/temp', (err)=>{

if(err) {

console.error(err)

           }

       })

   }

})

3.1.2 文件的操作

fs.writeFile():读取或者创建文件。

fs.unlink():删除文件及其文件链接(快捷方式)。

constfs=require('fs')

// 新建文件

fs.writeFile(__dirname+'/text.txt','', (err)=>{

if(err)throwerr

setTimeout(()=>{

// 删除文件

fs.unlink(__dirname+'/text.txt', (err)=>{

if(err)throwerr

       })

},3000);

})

3.1.3 文件属性的获取

通过fs.lstat()方法可以读取文件属性,文件属性保存在一个fs.Stats对象里面。

constfs=require('fs')

// 获取文件属性

fs.lstat(__filename, (err,stats)=>{

if(err)throwerr

if(stats.isFile) {

console.log(`${__filename}是文件,创建于${stats.birthtime},大小${stats.size}`)

   }

})

3.1.4 大文件的读写

对于比较大型的文件(如视频文件),拷贝文件或读取文件是分块的以流的方式来读取的,因为直接读取整个文件会很容易超过内存容量。

const stream = fs.createReadStream('c:\\demo\1.txt');

let data = ''

stream.on('data', (trunk) => {

  data += trunk;

});

stream.on('end', () => {

  console.log(data);

});

文件流的读写比较复杂,推荐直接使用第三方库:cp-file

3.2 path模块

path模块也是Node.js中经常使用的模块,主要用于路径的处理,由于Windows和Unix路径的格式有差异,因此在不同平台上调用的方法可能得到只针对平台的路径。尽量不要使用和平台相关的path API。

path.dirname():返回所在目录名。

path.dirname('/foo/bar/baz/asdf/quux');

// 返回: '/foo/bar/baz/asdf'

path.extname:返回扩展名。

path.extname('index.html');

// 返回: '.html'

path.join:可以使用平台特定的分隔符作为定界符将所有给定的 path 片段连接在一起,然后规范化生成的路径。

path.join('/foo','bar','baz/asdf','quux','..');

// 返回: '/foo/bar/baz/asdf'

path.normalize() :方法规范化给定的 path,解析 '..' 和 '.' 片段。

path.normalize('C:\\temp\\\\foo\\bar\\..\\');

// 返回: 'C:\\temp\\foo\\'

path.resolve() 方法将路径或路径片段的序列解析为绝对路径。

规则:

给定的路径序列从右到左进行处理,每个后续的 path 前置,直到构造出一个绝对路径。

如果在处理完所有给定的 path 片段之后还未生成绝对路径,则再加上当前工作目录。

生成的路径已规范化,并且除非将路径解析为根目录,否则将删除尾部斜杠。

constpath=require('path')

console.log(path.resolve('/foo','/bar','./baz'))

// '/bar/baz'

console.log(path.resolve('/foo/bar','./baz'))

// '/foo/bar/baz'

console.log(path.resolve('foo/bar','./baz/'))

// '/<当前执行路径>/foo/bar/baz'

3.4 url模块

url模块主要处理URL(统一资源定位符),URL的格式构成:

协议+认证+主机+端口+路径+查询字符串+哈希值。

可以参照下图:

url模块主要使用以下两个API接口:

url.format():负责把一个url的json对象转换成合法的url地址。

url.parse():负责把一个url地址转换成一个url的json对象。

consturl=require('url')

constpath=url.format({

protocol:'https',

hostname:'example.com',

pathname:'/some/path',

query: {

page:1,

format:'json'

   }

});

console.log(path)

// => 'https://example.com/some/path?page=1&format=json'

console.log(url.parse(path))

// Url {

//   protocol: 'https:',

//   slashes: true,

//   auth: null,

//   host: 'example.com',

//   port: null,

//   hostname: 'example.com',

//   hash: null,

//   search: '?page=1&format=json',

//   query: 'page=1&format=json',

//   pathname: '/some/path',

//   path: '/some/path?page=1&format=json',

//   href: 'https://example.com/some/path?page=1&format=json' }

3.5 querystring模块

通过url.parse转换的url对象中的query对象是一个字符串,如果想进一步拿到查询字符串的键值对,需要再通过querystring来转换。

querystring.stringify():可以把查询字符串对象转换成字符串。querystring.stringify默认会对非ASCII字符进行百分比编码,即内部调用querystring.escape() 方法。

querystring.parse():可以把查询字符串转换成对象。querystring.parse默认会对经过百分比编码的字符进行解码操作,即内部调用了querystring.unescape() 。

constqs=require('querystring')

conststr=qs.stringify({

name:'小明',

age:30,

description: ['动物','人']

})

console.log(str)

// name=%E5%B0%8F%E6%98%8E&age=30&description=%E5%8A%A8%E7%89%A9&description=%E4%BA%BA

constobj=qs.parse(str)

console.log(obj)

// [Object: null prototype] { name: '小明', age: '30', description: ['动物', '人'] }

3.6 http模块

http/https模块是Node.js中和网络请求相关的核心模块,类似浏览器中的ajax,但是它除了可以请求数据,还可以通过它搭建HTTP(S)服务。

接下来,我们使用http模块模拟一个爬虫案例。

什么是爬虫?

对于一个 害怕虫子的开发者来说,估计选择爬虫行业可以是一个磨炼意志的机会。

爬虫就是一段自动抓取互联网信息的程序,从互联网上抓取对于我们有价值的信息。

事实上,搜索引擎就是一个巨型的爬虫,它可以辅助我们完成信息的检索和归类,让我们以最快的速度找到我们最想要的信息。

接下来,我们只是简单地使用https模块和第三方cheer.io模块来爬去某一个网站的有用信息。

因为目前大部分网站都是基于https协议的,所以就是用https模块,它和http协议最大的区别就是https会对请求和资源加密解密。

编写http代码,实现资源获取。

varhttp=require('http')

varhttps=require('https')

functiongetUrlResource(url,callback) {

varclient

if(url.startsWith('https')) {

client=https

}elseif(url.startsWith('http')) {

client=http

}else{

callback(newError('只能请求http/https协议的内容'))

return;

   }

client.get(url, (res)=>{

const{statusCode}=res;

constcontentType=res.headers['content-type'];

leterror;

if(statusCode!==200) {

error=newError('请求失败\n'+

`状态码: ${statusCode}`);

}elseif(!/^text\/html/.test(contentType)) {

error=newError('无效的 content-type.\n'+

`期望的是 text/html 但接收到的是 ${contentType}`);

       }

if(error) {

// 消费响应数据来释放内存。

res.resume();

callback(error)

return;

       }

res.setEncoding('utf8');

letrawData='';

res.on('data', (chunk)=>{rawData+=chunk; });

res.on('end', ()=>{

callback(null,rawData)

       });

}).on('error', (e)=>{

callback(e)

   });

}

编写爬虫代码

varcheerio=require('cheerio')

getUrlResource('https://www.baidu.com/',function(e,data) {

if(e) {

console.error(e)

return

   }

var$=cheerio.load(data)

var$imgs=$('img[src]')

if($imgs.length===0) {

console.log('没有找到图片资源')

return

   }

// 注意执行get才能拿到数组

varimgSrcValues=$imgs.map(function(i,el) {

return$(el).attr('src');

}).get()

varimgUrls=imgSrcValues.join(',\n');

console.log(imgUrls)

})

四. Express框架

Express 是Node.js开发中使用最广泛的服务器框架,通过它可以快速的搭建一个Web容器并向外界提供Web服务。

Express基于Node.js的http/https搭建服务器,通过中间件的即插即用方式来扩展功能。

4.1 基本使用

创建一个项目文件夹,命名为express-demo并通过cd命令进入到express-demo目录。

使用npm init -y初始化项目,生成package.json项目描述文件。

使用npm install -save express安装express。

创建一个index.js文件,写入以下代码访问浏览器即可查看效果。

constexpress=require('express')

constapp=express()

app.get('/', (req,res)=>res.send('你好!'))

app.listen(3000, ()=>{

console.log('服务器创建完毕,监听3000端口')

})

打开终端,在express-demo目录下执行:

nodeindex.js。

此外,如果希望不在console中调试,而是在Chrome中通过dev tools调试,可以使用:

node--inspectindex.js

然后打开开发者工具,里面有个Node图标,点击即可调试。

假如你希望每一次修改代码都自动重启Express,可以全局安装nodemon,它和node的命令几乎一样。

npm install -g nodemon

nodemon --inspect index.js

打开浏览器,输入:http://本机ip/ 查看效果。

为了方便自动的检测本机ip和自动打开浏览器测试,可以安装两个第三方npm包:open和address,portFinder。然后服务器启动成功之后可以自动打开浏览器测试。

npminstall-saveopen address portfinder

constexpress=require('express')

consturl=require('url')

constopen=require('open')

constaddress=require('address')

constportfinder=require('portfinder')

constapp=express()

app.get('/', (req,res)=>res.send('你好!'))

// 防止3000端口被占用

portfinder.getPort({

port:3000,// minimum port

stopPort:3333// maximum port

},function(err,port){

if(err){

thrownewError('没有找到可以启动的端口')

  }

app.listen(port, ()=>{

console.log('服务器创建完毕,监听'+port+'端口')

  // 自动打开Chrome浏览器加载网页

open(url.format({

protocol:'http',

port:port,

hostname:address.ip(),pathname:'/'

}), {app: ['google chrome','--incognito'] })

  });

});

可以把node index.js命令加入到package.json的scripts字段里面,通过npm start启动。

{

"name":"express-demo",

"main":"index.js",

"scripts": {

"start":"node index.js"

  },

"dependencies": {

"address":"^1.1.2",

"express":"^4.17.1",

"open":"^7.0.0"

  }

}

npmstart

你可能感兴趣的:(Node.js课程知识讲解大全(二))