nodejs学习笔记02-模块化

module.exports,require / export,import / Nodejs内置模块 path,fs,events

1. 模块化

1.1. 什么是模块化?

  • 事实上模块化开发最终的目的是将程序划分成一个个小的结构;

  • 这个结构中编写属于自己的逻辑代码,有自己的作用域,不会影响到其他的结构;

  • 这个结构可以将希望暴露的变量、函数、对象等导出给其结构使用;

  • 也可以通过某种方式,导入另外结构中的变量、函数、对象等;

  • 上面说提到的结构,就是模块;按照这种结构划分开发程序的过程,就是模块化开发的过程;

  • 无论你多么喜欢JavaScript,以及它现在发展的有多好,我们都需要承认在Brendan Eich用了10天写出了JavaScript的时候,它都有很多的缺陷:

    • 比如var定义的变量作用域问题;

    • 比如JavaScript的面向对象并不能像常规面向对象语言一样使用class;

    • 比如JavaScript没有模块化的问题;

  • Brendan Eich本人也多次承认过JavaScript设计之初的缺陷,但是随着JavaScript的发展以及标准化,存在的缺陷问题基 本都得到了完善。无论是web、移动端、小程序端、服务器端、桌面应用都被广泛的使用;

JavaScript是披着c语言外衣的lisp -- 《黑客与画家》

1.2. 早期的JavaScript

  • 在网页开发的早期,Brendan Eich开发JavaScript仅仅作为一种脚本语言,做一些简单的表单验证或动画实现等,那个时候代码还是很少的:

    • 这个时候我们只需要讲JavaScript代码写到 <script>标签中即可;
    • 并没有必要放到多个文件中来编写;甚至流行:通常来说 JavaScript 程序的长度只有一行
  • 但是随着前端和JavaScript的快速发展,JavaScript代码变得越来越复杂了:

    • ajax的出现,前后端开发分离,意味着后端返回数据后,需要通过JavaScript进行前端页面的渲染;
    • SPA的出现,前端页面变得更加复杂:包括前端路由、状态管理等等一系列复杂的需求需要通过JavaScript来实现;
    • 包括Node的实现,JavaScript编写复杂的后端程序,没有模块化是致命的硬伤;
  • 所以,模块化已经是JavaScript一个非常迫切的需求:

    • 但是JavaScript本身,直到ES6(2015)才推出了自己的模块化方案;
    • 在此之前,为了让JavaScript支持模块化,涌现出了很多不同的模块化规范:AMD、CMD、CommonJS等;

1.3. 没有模块化带来很多的问题

早期没有模块化带来了很多的问题:比如命名冲突的问题

1
2
3
4
5
6
7
8
9
10
// -----bar.js---------
var name = 'bar';
console.log(name);

// -----foo.js---------
var name = 'foo';
console.log(name);

// -----barz.js---------
console.log(name);

html引入顺序为

1
2
3
<script src="./bar.js"></script>
<script src="./foo.js"></script>
<script src="./barz.js"></script>

输出的就是

1
2
3
bar
foo
foo

html引入顺序为

1
2
3
<script src="./bar.js"></script>
<script src="./barz.js"></script>
<script src="./foo.js"></script>

输出的就是

1
2
3
bar
bar
foo

这样的代码对于script标签的前后顺序有极强的依赖

解决上面的问题:立即函数调用表达式(IIFE)

  • IIFE (Immediately Invoked Function Expression)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // -----bar.js---------
    var moduleBar = (function(){
    var name = 'bar';
    var age = 18;
    console.log(name);
    return {
    name,
    age
    }
    })();

    // -----barz.js---------
    console.log(moduleBar.name);

但是,带来了新的问题:

  1. 必须记得每一个模块中返回对象的命名,才能在其他模块使用过程中正确的使用;
  2. 代码写起来混乱不堪,每个文件中的代码都需要包裹在一个匿名函数中来编写;
  3. 在没有合适的规范情况下,每个人、每个公司都可能会任意命名、甚至出现模块名称相同的情况;

所以,虽然实现了模块化,但是实现过于简单,并且是没有规范的

  • 需要制定一定的规范来约束每个人都按照这个规范去编写模块化的代码;

  • 这个规范中应该包括核心功能:模块本身可以导出暴露的属性,模块又可以导入自己需要的属性;

  • JavaScript社区为了解决上面的问题,涌现出一系列好用的规范,接下来我们就学习具有代表性的一些规范

1.4. CommonJS和Node

  • CommonJS是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为 ServerJS,后来为了 体现它的广泛性,修改为 CommonJS,平时我们也会简称为CJS

    • Node是CommonJS在服务器端一个具有代表性的实现;

    • Browserify是CommonJS在浏览器中的一种实现;

    • webpack打包工具具备对CommonJS的支持和转换;

  • 所以,Node中对 CommonJS 进行了支持和实现,在开发node的过程中可以方便的进行模块化开发:

    • 在Node中每一个js文件都是一个单独的模块;
    • 这个模块中包括CommonJS规范的核心变量:exportsmodule.exportsrequire;
    • 可以使用这些变量来方便的进行模块化开发;

  • 前面我们提到过模块化的核心是导出和导入,Node中对其进行了实现:

    • exportsmodule.exports可以负责对模块中的内容进行导出;
    • require函数可以导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;

1.4.1. exports

exports是一个对象,可以在这个对象中添加很多个属性,添加的属性会导出;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//--------bar.js---------
var uname = 'uname';
var age = 18;
function sayHello(name){
console.log("Hi "+name);
}

exports.uname = uname;
exports.age = age;
exports.sayHello = sayHello;
// { uname: 'uname', age: 18, sayHello: [Function: sayHello] }
console.log(exports);

//----------barz.js--------
const bar = require("./bar");
//uname
console.log(bar.uname);
//18
console.log(bar.age);
//Hi John
bar.sayHello("john");

上面完成了什么操作呢?

  • 意味着main中的bar变量等于exports对象;
  • 也就是require通过各种查找方式,最终找到了exports这个对象;
  • 并且将这个exports对象赋值给了bar变量;
  • bar变量就是exports对象了;

1.4.1.1. 对象的引用赋值

exports 实际就是一个对象

而且bar和exports是同一个对象:

bar对象是exports对象的浅拷贝(引用赋值);

exports对象中的属性修改,main中bar对象也会被修改

浅拷贝的本质就是一种引用的赋值而已;

1.4.2. module.exports

module.exports和exports有什么关系或者区别呢?

  • 追根溯源,通过维基百科中对CommonJS规范的解析:

    • CommonJS中是没有module.exports的概念的;

    • 但是为了实现模块的导出,Node中使用的是Module的类,每一个模块都是Module的一个实例,也就是 module;

    • 所以在Node中真正用于导出的其实根本不是exports,而是module.exports;

    • 因为module才是导出的真正实现者;

  • 但是,为什么exports也可以导出呢?

    • 这是因为module对象的exports属性是exports对象的一个引用;
    • 也就是说,module.exports = exports = bar

1.4.2.1. 真正的导出对象

如果使用了module.exports,exports就失效了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// =========bar.js============
var uname = 'uname';
var age = 18;
function sayHello(name){
console.log("Hi "+name);
}

exports.uname = uname;
exports.age = age;
exports.sayHello = sayHello;

module.exports = {
uname: 'module',
age: 20,
sayHello: function(name){
console.log("Hello "+name);
}
}


// =========barz.js================
const bar = require("./bar");
// module
console.log(bar.uname);
//20
console.log(bar.age);
// Hello john
bar.sayHello("john");

1.4.2.2. 源码

https://github.com/nodejs/node/blob/main/lib/internal/modules/cjs/loader.js

定义了exports,并定义 exports = module.exports

1
2
3
4
5
6
7
8
9
function Module(id = '', parent) {
// 这里的exports应该理解为 module.exports
this.exports = {};
}

Module.prototype.load = function(filename) {
// Create module entry at load time to snapshot exports correctly
const exports = this.exports;
}

1.4.3. require

require是一个函数,可以引入一个文件(模块)中导入的对象

1.4.3.1. require的查找规则是怎么样的呢?

https://nodejs.org/dist/latest-v16.x/docs/api/modules.html#modules_all_together

导入格式:require(X)

  • X 是一个核心模块,比如path、http

    • 直接返回核心模块,并且停止查找
  • X 是以 ./ 或 ../ 或 /(根目录)开头的

    1. 将X当做一个文件在对应的目录下查找;
      • 如果有后缀名,按照后缀名的格式查找对应的文件;
      • 如果没有后缀名,会按照如下顺序:
        • 直接查找文件X
        • 查找X.js文件
        • 查找X.json文件
        • 查找X.node文件
    2. 没有找到对应的文件,将X作为一个目录;
      • 查找目录下面的index文件
        • 查找X/index.js文件
        • 查找X/index.json文件
        • 查找X/index.node文件
    3. 如果没有找到,那么报错:not found
  • 直接是一个 X (没有路径),并且X不是一个核心模块

    • bar.js中编写 require(“X”)

      1
      2
      console.log(module);
      require("X");

      输出

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      Module {
      id: '.',
      path: '/Users/xx/Resource/workspace/visualstdio/HTML/node-demo',
      exports: {},
      filename: '/Users/xx/Resource/workspace/visualstdio/HTML/node-demo/bar.js',
      loaded: false,
      children: [],
      paths: [
      '/Users/xx/Resource/workspace/visualstdio/HTML/node-demo/node_modules',
      '/Users/xx/Resource/workspace/visualstdio/HTML/node_modules',
      '/Users/xx/Resource/workspace/visualstdio/node_modules',
      '/Users/xx/Resource/workspace/node_modules',
      '/Users/xx/Resource/node_modules',
      '/Users/xx/node_modules',
      '/Users/node_modules',
      '/node_modules'
      ]
      }


      Error: Cannot find module 'X'
      Require stack:
      code: 'MODULE_NOT_FOUND'
    • node会根据路径,一层层向上寻找模块X。如果以上路径都没有找到,报错 not found

1.4.3.2. 模块的加载过程

  • 结论一:模块在被第一次引入时,模块中的js代码会被运行一次

    1
    2
    3
    4
    5
    //=======bar.js=========
    console.log("bar");

    //=======barz.js========
    require("./bar");

    运行

    1
    node barz.js

    输出

    1
    bar
  • 结论二:模块被多次引入时,会缓存,最终只加载(运行)一次

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //=======bar.js=========
    console.log("bar");
    console.log(module);

    //=======barz.js========
    require("./bar");
    console.log(module);

    //========foo.js=========
    require("./bar");
    require("./barz");

    运行

    1
    node foo.js

    输出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    bar
    Module {
    id: 'xx/bar.js',
    loaded: false,
    children: []
    }
    Module {
    id: 'xx/barz.js',
    loaded: false,
    children: [
    Module {
    id: 'xx/bar.js',
    loaded: true,
    children: []
    }
    ],
    paths: []
    }

    是否多次加载,跟模块的 loaded 有关

  • 结论三:如果有循环引入,那么加载顺序是什么?

    这个其实是一种数据结构 — 图结构;

    • 图结构在遍历的过程中,有深度优先搜索(DFS, depth first search)和广度优先搜索(BFS, breadth first search);
    • Node采用的是深度优先算法:main -> aaa -> ccc -> ddd -> eee ->bbb

1.5. CommonJS规范缺点

  • CommonJS加载模块是同步的:

    • 同步的意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行;
    • 这个在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快;
  • 如果将它应用于浏览器呢?

    • 浏览器加载js文件需要先从服务器将文件下载下来,之后在加载运行;
    • 那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作;
  • 所以在浏览器中,我们通常不使用CommonJS规范:

  • 当然在webpack中使用CommonJS是另外一回事;

  • 因为它会将代码转成浏览器可以直接执行的代码;

  • 在早期为了可以在浏览器中使用模块化,通常会采用AMD或CMD:

    • 但是目前一方面现代的浏览器已经支持ES Modules,另一方面借助于webpack等工具可以实现对CommonJS或者ES Module代码的转换;
    • AMD和CMD已经使用非常少了

1.6. AMD规范

  • AMD主要是应用于浏览器的一种模块化规范:
    • AMD是 Asynchronous Module Definition(异步模块定义)的缩写;
    • 它采用的是异步加载模块;
    • 事实上AMD的规范还要早于CommonJS,但是CommonJS目前依然在被使用,而AMD使用的较少了;
  • 规范只是定义代码的应该如何去编写,只有有了具体的实现才能被应用:
    • AMD实现的比较常用的库是require.js和curl.js;

1.6.1. 使用

  1. 下载require.js:https://github.com/requirejs/requirejs 中的 require.js

  2. 建立目录结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    |- node-demo
    |- lib
    | - Sea.js
    |- modules
    |- bar.js
    |- barz.js
    |- foo.js
    |- index.html
    |- index.js
  3. index.html 中定义HTML的script标签引入require.js和定义入口文件:

    1
    2
    3
    4
    5
    <!-- index.html -->
    <body>
    <!-- data-main属性的作用是在加载完src的文件后会加载执行该文件 -->
    <script src="lib/require.js" data-main="./index.js"></script>
    </body>
  4. 编写入口文件index.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    (function(){
    require.config({
    baseUrl: '',
    paths: {
    foo: './modules/foo',
    bar: './modules/bar',
    barz: './modules/barz'
    }
    })
    require(['foo'],function(foo){

    })
    })();
  5. bar.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    define(function(){
    let uname = 'uname';
    let age = 18;
    function sayHello(name){
    console.log("Hi "+name);
    }
    return {
    uname,
    age,
    sayHello
    }
    });
  6. barz.js

    1
    2
    3
    define(function(){
    console.log("barz");
    })
  7. foo.js

    1
    2
    3
    4
    5
    define(['bar','barz'],function(bar,barz){
    console.log(bar.uname);
    console.log(bar.age);
    bar.sayHello("john");
    })
  8. 运行index.html,控制台输出

    1
    2
    3
    4
    barz
    uname
    18
    Hi john

1.7. CMD规范

  • CMD规范也是应用于浏览器的一种模块化规范:

    • CMD 是 Common Module Definition (通用模块定义)的缩写;
    • 它也采用了异步加载模块,但是它将CommonJS的优点吸收了过来;
    • 但是目前CMD使用也非常少了;
  • CMD也有自己比较优秀的实现方案:

    • SeaJS

1.7.1. 使用

  1. 下载SeaJS:https://github.com/seajs/seajs,找到dist文件夹下的sea.js

  2. 目录结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    |- node-demo
    |- lib
    | - Sea.js
    |- modules
    |- bar.js
    |- barz.js
    |- foo.js
    |- index.html
    |- index.js
  3. index.html 中 引入sea.js和使用主入口文件

    1
    2
    3
    4
    5
    6
    7
    <body>
    <script src="./lib/Sea.js"></script>
    <script>
    // seajs是指定主入口文件的
    seajs.use("./index.js");
    </script>
    </body>
  4. index.js

    1
    2
    3
    define(function(require,exports,module){
    require("./modules/foo");
    })
  5. bar.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    define(function(require,exports,module){
    let uname = 'uname';
    let age = 18;
    function sayHello(uname){
    console.log("Hello "+uname);
    }
    module.exports = {
    uname,
    age,
    sayHello
    }
    })
  6. barz.js

    1
    2
    3
    4
    5
    6
    7
    8
    define(function(require,exports,module){
    const bar = require("./bar");
    console.log(bar.uname);
    console.log(bar.age);
    bar.sayHello(bar.uname);

    console.log("barz")
    });
  7. foo.js

    1
    2
    3
    4
    define(function(require,exports,module){
    const barz = require("./barz");
    console.log("foo");
    })
  8. 运行index.html

    1
    2
    3
    4
    5
    uname
    18
    Hello uname
    barz
    foo

1.8. 认识 ES Module

  • JavaScript没有模块化一直是它的痛点,所以才会产生社区规范:CommonJS、AMD、CMD等, 然后在ES推出了自己的模块化系统

  • ES Module和CommonJS的模块化有一些不同之处:

    • 一方面它使用了import和export关键字;
    • 另一方面它采用编译期的静态分析,并且也加入了动态引用的方式;

  • ES Module模块采用 exportimport 关键字来实现模块化:

    • export负责将模块内的内容导出;
    • import负责从其他模块导入内容;

  • 采用ES Module将自动采用严格模式:**use strict **

    https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode

1.8.1. 案例代码结构组件

创建代码

1
2
3
4
<!-- index.html -->
<body>
<script src="./index.js" type="module"></script>
</body>
1
2
//index.js
console.log("ES Module");

在浏览器控制台出现错误

1
Access to script at 'file:///xx/index.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, chrome-untrusted, https.

在MDN上面有给出解释:

1.8.2. export关键字

export关键字将一个模块中的变量、函数、类等导出

使用方式:

  1. 在语句声明的前面直接加上export关键字

    1
    2
    3
    4
    5
    export let uname = 'uname';
    export let age = 18;
    export function sayHello(uname){
    console.log("Hello "+uname);
    }
  2. 将所有需要导出的标识符,放到export后面的 {}中

    这里的 {}里面不是ES6的对象字面量的增强写法,{}也不是表示一个对象的;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let uname = 'uname';
    let age = 18;
    function sayHello(uname){
    console.log("Hello "+uname);
    }

    // 放置的是要导出的变量的引用列表
    export {
    uname,
    age,
    sayHello
    }
  3. 导出时给标识符起一个别名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let uname = 'uname';
    let age = 18;
    function sayHello(uname){
    console.log("Hello "+uname);
    }

    export {
    uname as Name,
    age as Age,
    sayHello as SayHello
    }

1.8.3. import关键字

import关键字负责从另外一个模块中导入内容

使用方式:

  1. import {标识符列表} from ‘模块’;

    1
    2
    import { uname,age,sayHello } from "./modules/bar.js";
    sayHello("john");
  2. 导入时给标识符起别名

    1
    2
    import {sayHello as Hello} from "./modules/bar.js";
    Hello("john");
  3. 通过 * 将模块功能放到一个模块功能对象上

    1
    2
    import * as bar from "./modules/bar.js";
    bar.sayHello("john");

1.8.4. Export和import结合使用

  • 在开发和封装一个功能库时,通常希望将暴露的所有接口放到一个文件中;
  • 这样方便指定统一的接口规范,也方便阅读;
  • 这个时候,就可以使用export和import结合使用;

bar.js

1
2
3
4
5
6
7
8
9
10
11
12
let uname = 'uname';
let age = 18;
function sayHello(uname){
console.log("Hello "+uname);
}

// 放置的是要导出的变量的引用列表
export {
uname,
age,
sayHello
}

oo.js

1
export {uname,age,sayHello} from './bar.js';

index.js

1
2
import * as foo from "./modules/foo.js";
foo.sayHello("john");

1.8.5. default用法

  • 前面所讲的导出功能都是有名字的导出(named exports):

    • 在导出export时指定了名字;
    • 在导入import时需要知道具体的名字;
  • 还有一种导出叫做默认导出(default export)

    • 默认导出export时可以不需要指定名字;
    • 在导入时不需要使用 {},并且可以自己来指定名字;
    • 它也可以跟现有的CommonJS等规范相互操作;

在一个模块中,只能有一个默认导出(default export)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//===================bar.js===================
let uname = 'uname';
let age = 18;
function sayHello(uname){
console.log("Hello "+uname);
}
export {
uname,
age,
sayHello
}

//===================foo.js===================
export {uname,age,sayHello} from './bar.js';
export default function(){
console.log("default export");
}


//===================index.js===================
import * as foo from "./modules/foo.js";
foo.sayHello("john");
import format from "./modules/foo.js";
format();

1.8.6. import函数

通过import加载一个模块,是不可以在其放到逻辑代码中的,比如:

1
2
3
if(true){
import * as foo from "./modules/foo.js";
}

为什么会出现这个情况呢?

  • 这是因为ES Module在被JS引擎解析(parsing)时,就必须知道它的依赖关系;
  • 由于这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况;
  • 必须到运行时才能确定path的值;

但是某些情况下,确确实实希望动态的来加载某一个模块:

  • 如果根据不懂的条件,动态来选择加载模块的路径;
  • 这个时候需要使用 import() 函数来动态加载;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//=================bar.js========================
let uname = 'uname';
let age = 18;
function sayHello(uname){
console.log("Hello "+uname);
}
export {
uname,
age,
sayHello
}

//=================foo.js========================
export default function(flag){
if(flag){
import("./bar.js").then(bar=>{
bar.sayHello(bar.uname);
})
}
}

//=================index.js========================
import format from "./modules/foo.js";
format(true);

1.9. CommonJS的加载过程

  • CommonJS模块加载js文件的过程是运行时加载 的,并且是 同步的:

    • 运行时加载意味着是js引擎在执行js代码的过程中加载 模块;

    • 同步的就意味着一个文件没有加载结束之前,后面的代码都不会执行

      1
      2
      3
      4
      5
      let flag = true;
      if(flag){
      const foo = require("./modules/foo");
      console.log("if语句继续执行");
      }
  • CommonJS通过module.exports导出的是一个对象:

    • 导出的是一个对象意味着可以将这个对象的引用在其他模块中赋值给其他变量;

    • 但是最终指向的都是同一个对象,那么一个变量修改了对象的属性,所有的地方都会被修改;;

1.10. ES Module加载过程

  • ES Module加载js文件的过程是 编译(解析)时加载 的,并且是异步的:

    • 编译时(解析)时加载,意味着import不能和运行时相关的内容放在一起使用:
    • 比如from后面的路径需要动态获取;
    • 比如不能将import放到if等语句的代码块中;
    • 所以也称ES Module是静态解析的,而不是动态或者运行时解析的;
  • 异步的意味着:JS引擎在遇到import时会去获取这个js文件,但是这个获取的过程是异步的,并不会阻塞主线程继续执行;

    • 也就是说设置了 type=module 的代码,相当于在script标签上也加上了 async 属性;

    • 如果后面有普通的script标签以及对应的代码,那么ES Module对应的js文件和代码不会阻塞它们的执行;

      1
      2
      3
      <script src="main.js" type="module"></script>
      <!-- index.js可以被执行,不会被阻塞 -->
      <script src="index.js"></script>
  • ES Module通过export导出的是变量本身的引用:

    • export在导出一个变量时,js引擎会解析这个语法,并且创建 模块环境记录(module environment record);
    • 模块环境记录会和变量进行绑定(binding),并且这个绑定是实时的;
    • 而在导入的地方,我们是可以实时的获取到绑定的最新值的;
  • 所以,如果在导出的模块中修改了变化,那么导入的地方可以实时获取最新的变量;

  • 注意:在导入的地方不可以修改变量,因为它只是被绑定到了这个变量上(其实是一个常量)

  • 如果bar.js中导出的是一个对象,那么main.js中是否可以修改对象中的属性呢?

    • 答案是可以的,因为他们指向同一块内存空间;

1.11. 画图解析赋值的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//=============bar.js==============
let uname = 'uname';
let age = 18;

// 放置的是要导出的变量的引用列表
export {
uname,
age
}

setTimeout(()=>{
uname="aaaaaaa";
},1000);

//=============foo.js================
import { uname } from "./bar.js";

setTimeout(()=>{
console.log("foo "+uname);
},2000);


//==============index.js==============
import * as foo from "./modules/foo.js";

index.html

1
2
3
<body>
<script src="./index.js" type="module"></script>
</body>

浏览器控制台

1
foo aaaaaaa

图解

1.12. Node对ES Module的支持

  • 直接执行 node index.js会出现错误,并提示执行方式

    1
    2
    (node:23606) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
    (Use `node --trace-warnings ...` to show where the warning was created)
  • 说明node为了支持 es module 需要进行如下操作:

    1. 方式一:在package.json中配置 type: module
    2. 方式二:文件以 .mjs 结尾,表示使用的是ES Module;

1.13. CommonJS和ES Module交互

  1. 通常情况下,CommonJS不能加载ES Module

    • 因为CommonJS是同步加载的,但是ES Module必须经过静态分析等,无法在这个时候执行JavaScript代码;

    • 但是这个并非绝对的,某些平台在实现的时候可以对代码进行针对性的解析,也可能会支持;

    • Node当中是不支持的;

      1
      2
      3
      4
      5
      6
      //===============foo.js===============
      export const uname = "foo";

      //===============index.js==============
      const foo = require("./modules/foo");
      console.log(foo.uname);

      无法运行

      1
      2
      node index.js
      SyntaxError: Unexpected token 'export'
  2. 多数情况下,ES Module可以加载CommonJS

    • ES Module在加载CommonJS时,会将其module.exports导出的内容作为default导出方式来使用;

    • 这个依然需要看具体的实现,比如webpack中是支持的、Node也是支持的;

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      //===============bar.js================
      let uname = 'uname';
      let age = 18;

      module.exports={
      uname,
      age
      }

      //================foo.mjs================
      export { uname,age } from "./bar.js";

      //================index.mjs===============
      import * as foo from "./modules/foo.mjs";
      console.log(foo.uname);

      正常运行

      1
      2
      node index.mjs
      uname

2. 内置模块path

  • path模块用于对路径和文件进行处理,提供了很多好用的方法

  • 并且在Mac OS、Linux和window上的路径时不一样

    • window上会使用 \ 或者 \\ 来作为文件路径的分隔符,当然目前也支持 /;
    • 在Mac OS、Linux的Unix操作系统上使用 / 来作为文件路径的分隔符;
  • 那么如果在window上使用 \ 来作为分隔符开发了一个应用程序,要部署到Linux上面应该怎么办呢?

    • 显示路径会出现一些问题;
    • 所以为了屏蔽他们之间的差异,在开发中对于路径的操作可以使用 path 模块;

2.1. path常见的API

https://nodejs.org/dist/latest-v16.x/docs/api/path.html

  • 从路径中获取信息

    • dirname:获取文件的父文件夹;

    • basename:获取文件名;

    • extname:获取文件扩展名;

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      const path = require("path");

      const filePath = '/User/vscode/a.txt';

      // /User/vscode
      console.log(path.dirname(filePath));
      //a.txt
      console.log(path.basename(filePath));
      //.txt
      console.log(path.extname(filePath));
  • 路径的拼接

    • 如果将多个路径进行拼接,但是不同的操作系统可能使用的是不同的分隔符;

    • 可以使用path.join函数;

      1
      2
      3
      4
      5
      6
      let basePath = "/User/VSCode";
      const fileName = 'a.txt';

      let filePath = path.join(basePath,fileName);
      // /User/VSCode/a.txt
      console.log(filePath);
  • 将文件和某个文件夹拼接

    • 如果将某个文件和文件夹拼接,可以使用 path.resolve;

    • resolve函数会判断我们拼接的路径前面是否有 /或../或./;

    • 如果有,会返回对应的拼接路径;

    • 如果没有,那么会和当前执行文件所在的文件夹进行路径的拼接

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      let basePath = "/User/VSCode";
      let fileName = 'a.txt';

      let filePath = path.resolve(basePath,fileName);
      // /User/VSCode/a.txt
      console.log(filePath);

      basePath = "./User/VSCode";
      filePath = path.resolve(basePath,fileName);
      // /Users/xx/Resource/workspace/vscode/HTML/node-demo/node-path/User/VSCode/a.txt
      console.log(filePath);

      basePath = "../User/VSCode";
      filePath = path.resolve(basePath,fileName);
      // /Users/xx/Resource/workspace/vscode/HTML/node-demo/User/VSCode/a.txt
      console.log(filePath);

      fileName = '/a.txt';
      filePath = path.resolve(basePath,fileName);
      // /a.txt
      console.log(filePath);

      basePath = "VSCode";
      fileName = 'a.txt';
      filePath = path.resolve(basePath,fileName);
      // /Users/xx/Resource/workspace/vscode/HTML/node-demo/node-path/VSCode/a.txt
      console.log(filePath);

3. 内置模块fs

  • fs是File System的缩写,表示文件系统

  • 对于任何一个为服务器端服务的语言或者框架通常都会有自己的文件系统:

    • 因为服务器需要将各种数据、文件等放置到不同的地方;
    • 比如用户数据可能大多数是放到数据库中;
    • 比如某些配置文件或者用户资源(图片、音视频)都是以文件的形式存在于操作系统上的;
  • Node也有自己的文件系统操作模块,就是fs:

    • 借助于Node封装的文件系统,可以在任何的操作系统(window、Mac OS、Linux)上面直接去操作文件;
    • 这也是Node可可以开发服务器的一大原因,也是它可以成为前端自动化脚本等热门工具的原因;

3.1. fs的API介绍

https://nodejs.org/dist/latest-v16.x/docs/api/fs.html

这些API大多数都提供三种操作方式:

  1. 同步操作文件:代码会被阻塞,不会继续执行;
  2. 异步回调函数操作文件:代码不会被阻塞,需要传入回调函数,当获取到结果时,回调函数被执行;
  3. 异步Promise操作文件:代码不会被阻塞,通过 fs.promises 调用方法操作,会返回一个Promise, 可以通过then、catch进行处理;

3.2. 案例:获取一个文件的状态

3.2.1. 同步操作

1
2
3
4
5
6
7
8
const fs = require("fs");

const filePath = "./a.txt";

// 同步
const info = fs.statSync(filePath);
console.log(info);
console.log("阻塞执行");

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Stats {
dev: 16777220,
mode: 33188,
nlink: 1,
uid: 501,
gid: 20,
rdev: 0,
blksize: 4096,
ino: 8674790088,
size: 8,
blocks: 8,
atimeMs: 1657472099416.351,
mtimeMs: 1657472098143.433,
ctimeMs: 1657472098143.433,
birthtimeMs: 1657472092502.4236,
atime: 2020-06-01T13:48:20.416Z,
mtime: 2020-06-01T13:48:19.143Z,
ctime: 2020-06-01T13:48:19.143Z,
birthtime: 2020-06-01T13:48:18.502Z
}
阻塞执行

3.2.2. 异步执行

1
2
3
4
5
6
7
8
9
10
11
12
const fs = require("fs");

const filePath = "./a.txt";
// 异步
fs.stat(filePath,(err,state)=>{
if(err){
console.log(err);
return;
}
console.log(state);
});
console.log("执行代码");

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
执行代码
Stats {
dev: 16777220,
mode: 33188,
nlink: 1,
uid: 501,
gid: 20,
rdev: 0,
blksize: 4096,
ino: 8674790088,
size: 8,
blocks: 8,
atimeMs: 1657472099416.351,
mtimeMs: 1657472098143.433,
ctimeMs: 1657472098143.433,
birthtimeMs: 1657472092502.4236,
atime: 2020-06-01T13:48:20.416Z,
mtime: 2020-06-01T13:48:19.143Z,
ctime: 2020-06-01T13:48:19.143Z,
birthtime: 2020-06-01T13:48:18.502Z
}

3.2.3. Promise方式

1
2
3
4
5
6
7
8
9
10
11
const fs = require("fs");

const filePath = "./a.txt";

fs.promises.stat(filePath).then(state=>{
console.log(state);
console.log(state.isDirectory());
}).catch(err=>{
console.log(err);
})
console.log("执行代码");

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
执行代码
Stats {
dev: 16777220,
mode: 33188,
nlink: 1,
uid: 501,
gid: 20,
rdev: 0,
blksize: 4096,
ino: 8674790088,
size: 8,
blocks: 8,
atimeMs: 1657472099416.351,
mtimeMs: 1657472098143.433,
ctimeMs: 1657472098143.433,
birthtimeMs: 1657472092502.4236,
atime: 2020-06-01T13:48:20.416Z,
mtime: 2020-06-01T13:48:19.143Z,
ctime: 2020-06-01T13:48:19.143Z,
birthtime: 2020-06-01T13:48:18.502Z
}
false

3.3. 文件描述符

  • 可移植操作系统接口(Portable Operating System Interface,缩写为POSIX)

    • Linux和Mac OS都实现了POSIX接口;
    • Window部分实现了POSIX接口;
  • 文件描述符(File descriptors)是什么呢?

    • 在 POSIX 系统上,对于每个进程,内核都维护着一张当前打开着的文件和资源的表格

    • 每个打开的文件都分配了一个称为文件描述符的简单的数字标识符

    • 在系统层,所有文件系统操作都使用这些文件描述符来标识和跟踪每个特定的文件

    • Windows 系统使用了一个虽然不同但概念上类似的机制来跟踪资源

  • 为了简化用户的工作,Node.js 抽象出操作系统之间的特定差异,并为所有打开的文件分配一个数字型的文件描述符

  • fs.open() 方法用于分配新的文件描述符

    • 一旦被分配,则文件描述符可用于从文件读取数 据、向文件写入数据、或请求关于文件的信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const fs = require("fs");

const filePath = "./a.txt";

fs.open(filePath,"r",(err,fd)=>{
if(err){
console.log(err);
return;
}
console.log(fd);
fs.fstat(fd,(err,state)=>{
console.log(state);
})
})

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
20
Stats {
dev: 16777220,
mode: 33188,
nlink: 1,
uid: 501,
gid: 20,
rdev: 0,
blksize: 4096,
ino: 8674790088,
size: 8,
blocks: 8,
atimeMs: 1657472099416.351,
mtimeMs: 1657472098143.433,
ctimeMs: 1657472098143.433,
birthtimeMs: 1657472092502.4236,
atime: 2020-06-01T13:48:20.416Z,
mtime: 2020-06-01T13:48:19.143Z,
ctime: 2020-06-01T13:48:19.143Z,
birthtime: 2020-06-01T13:48:18.502Z
}

3.4. 文件的读写

1
2
3
4
5
6
7
8
9
10
11
12
const fs = require("fs");

const content = "Hello fs 模块";
filePath = "./b.txt";

fs.writeFile(filePath,content,{encoding:'utf-8'},err=>{
console.log(err);
})

fs.readFile(filePath,{encoding:'utf-8'},(err,data)=>{
console.log(data);
})

3.5. 文件夹操作

  • 新建一个文件夹

    使用fs.mkdir(path[, options], callback)或 fs.mkdirSync()创建一个新文件夹

    https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fsmkdirpath-options-callback

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const fs = require("fs");

    fs.mkdir("files",{recursive:true},err=>{
    console.log(err);
    })

    //更好的方式
    const dirname = 'files';
    if(!fs.existsSync(dirname)){
    fs.mkdir(dirname,err=>{
    console.log(err);
    })
    }
  • 获取文件夹的所有内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    const fs = require("fs");
    const path = require("path");

    const dirname = 'files';

    function readFolders(folder,idx){
    fs.readdir(folder,{withFileTypes:true},(err,files)=>{
    if(err){
    console.log(err);
    return;
    }
    files.forEach(file=>{
    if(file.isDirectory()){
    const newFolder = path.resolve(folder,file.name);

    readFolders(newFolder,idx+1);
    }
    let fileName = '';
    for (let i = 0; i < idx; i++) {
    fileName+='-';
    }
    fileName+=file.name;
    console.log(fileName);

    })
    })
    }
    readFolders(dirname,0);

    输出

    1
    2
    3
    4
    node
    -file
    --a.txt
    --b.txt
  • 文件重命名

    1
    2
    3
    4
    const dirname = 'files';
    fs.rename(dirname,"uploads",err=>{
    console.log(err);
    })

3.6. 文件夹的复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const srcDir = process.argv[2];
const destDir = process.argv[3];
copy();

function copy(){
if(fs.existsSync(destDir)){
//删除文件夹
fs.rm(destDir,{recursive:true},err=>{
if(err){
console.log(err);
}
});
}
fs.mkdir(destDir,err=>{
if(!err){
console.log("文件创建成功,开始拷贝:");
}
const srcFiles = fs.readdirSync(srcDir);
for(let file of srcFiles){
if(file.startsWith("b")){
const srcFile = path.resolve(srcDir,file);
const destFile = path.resolve(destDir,file);
// export function copyFileSync(src: PathLike, dest: PathLike, mode?: number): void;
fs.copyFileSync(srcFile,destFile);
console.log(file,"拷贝成功");
}
}
})
}

4. events模块

https://nodejs.org/dist/latest-v16.x/docs/api/events.html

  • Node中的核心API都是基于异步事件驱动的:
    • 在这个体系中,某些对象(发射器(Emitters))发出某一个事件;
    • 可以监听这个事件(监听器 Listeners),并且传入的回调函数,这个回调函数会在监听到事件时调用;
  • 发出事件和监听事件都是通过EventEmitter类来完成的,它们都属于events对象。
    • emitter.on(eventName, listener):监听事件,也可以使用 addListener;
    • emitter.off(eventName, listener):移除事件监听,也可以使用removeListener;
    • emitter.emit(eventName[, …args]):发出事件,可以携带一些参数;
  • EventEmitter的实例有一些属性,可以记录一些信息:
    • emitter.eventNames():返回当前 EventEmitter对象注册的事件字符串数组;
    • emitter.getMaxListeners():返回当前 EventEmitter对象的最大监听器数量,可以通过setMaxListeners() 来修改,默认是10;
    • emitter.listenerCount(事件名称):返回当前 EventEmitter对象某一个事件名称,监听器的个数;
    • emitter.listeners(事件名称):返回当前 EventEmitter对象某个事件监听器上所有的监听器数组;
    • emitter.once(eventName, listener):事件监听一次
    • emitter.prependListener():将监听事件添加到最前面
    • emitter.prependOnceListener():将监听事件添加到最前面,但是只监听一次
    • emitter.removeAllListeners([eventName]):移除所有的监听器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const EventEmitter = require('events');

// 1. 创建发射器
const emitter = new EventEmitter();

function clickHandle(args){
console.log("监听到click事件",args);
}
// 2. 监听事件
emitter.on("click",clickHandle);

setTimeout(()=>{
// 3. 发射事件,可以携带一些参数;
emitter.emit("click","events");
// 4. 移除事件监听
emitter.off("click",clickHandle);
emitter.emit("click","EventEmitter");
},2000);


// 获取注册的事件字符串数组
emitter.on("tap",arg=>console.log(arg));
let names = emitter.eventNames();
console.log(names);
// [ 'click', 'tap' ]
本文结束  感谢您的阅读