Node.js模块化规范和es6模块化规范的一点理解

1,499 阅读4分钟

Node.js模块化基本概念

Node.js遵循CommonJs模块化规范,CommonJS规定了模块的特性和各模块之间的相互依赖的关系 CommonJS规定: 模块化标识('module')、模块化定义(exports)、模块化引入(require)

// 在一个自定义模块中,module是一个最终导出的模块的标识,所有的expots都会追加到module对象中

const age = 20

// 向 module.exports 对象上挂载 username 属性
module.exports.username = 'zs'
// 向 module对象上挂载 sayHello 方法
exports.sayHello = function() {
  console.log('Hello!')
}
exports.age = age

exports.sex = '男'

// 输出{ username: 'zs', sayHello: [Function (anonymous)], age: 20, sex: '男' }
// 在一个自定义模块中,默认情况下, module.exports = {}

const age = 20

// 向 module.exports 对象上挂载 username 属性
module.exports.username = 'zs'
// 向 module.exports 对象上挂载 sayHello 方法
exports.sayHello = function() {
  console.log('Hello!')
}
exports.age = age

exports.sex = '男'

// 让 module.exports 指向一个全新的对象(以最后的module.exports为准,只会导出一个模块化标识)
module.exports = {
  nickname: '小黑',
  sayHi() {
    console.log('Hi!')
  }
}

// 输出{ nickname: '小黑', sayHi: [Function: sayHi] }

CommonJS模块化加载机制:(require时动态的,在运行时加载)

  • 1、使用require()引入,在模块第一次加载时候会被缓存,这意味着多次调用require()不会导致代码会执行多次;
  • 2、由于require是运行时候调用,所以require理论上可以运用在代码的任何地方
  • 3、使用require()加载自定义模块的时候,必须指定以./或../开头的路径标识符,如果加载自定义模块的时,如果没有指定./或../,则node会把它当做内置模块或者第三方模块进行加载;
  • 4、在使用require()导入自定义模块时,如果省略了文件扩展名。则node会按照一下顺序加载文件:
    • A、按照确切的文件名进行加载;
    • B、补全的.JS扩展名进行加载;
    • C、补全的.json的扩展名进行加载;
    • D、补全.node扩展名进行加载;
    • E、加载失败,终端报错;
  • 备注:require()是不能识别.ts文件的扩展名,必须先转化成能识别扩展名;
  • 5、node常见内置模块:例如 fs(读写文件)、path(路径)、http(请求) 等
  • 6、node加载第三方模块: 默认会从当前模块的父目录开始,尝试从 /node_modules 文件夹中加载第三方模块,如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录。
  • 7、目录作为模块标识符,传递给require()进行加载是有三种方式:
    • A、在被加载的目录找一个叫做package.json的文件,并寻找main属性,作为加载入口;
    • B、如果目录内没有package.json文件,或者文件内没有没有main属性,则会加载目录下的index.js文件;
    • C、如果以上两步都没有则会在终端报错,报告模块的缺失:Error: Cannot find module 'xxx'

es6模块化规范基本概念

es6模块化规范:

用import引入自定义模块的函数、对象或者基本类型等,自定义模块使用export(可以有多个)和exprot default(默认只有一个)进行导出,且export default的默认导出不能修改

    //model.js
    let e1='测试1';
    let e2='测试2';
    export {e2};
    export default e1;
    e1='测试3';
    e2='测试4;
   import {e1, e2 } from "./model" // 导入方式1
   
   console.log(e1) // 输出"测试1"
   console.log(e2) // 输出 "测试4"
   import * as e from "./model"  // 导入方式2(model中的对象全部通过*导入)
   
   console.log(e.e1) //输出"测试1"
   console.log(e.e2) // 输出 "测试4"

备注:如果想修改默认导出的值,可以使用export {e1 as default}这种方法。

  // model.js修改
  export {el as detault}
   console.log(e1) // 输出"测试3"
   console.log(e2) // 输出 "测试4"

export default和export的语法差异

1.export var e1 = { ... }是合法语句,可以导出对象、变量等,但是export default var e2 = "{ ... }"是不合法的(使用letconst也是一样)
2.export default的导出要直接添加标识符导出
  例如: const  e3 = "123"
         export default e3

es6模块化编译(import是静态编译在编译的时候加载)

import导入的模块只是生成引用,等需要时才去取值,所以不存在缓存问题,也正是由于这个特性所以在自定义模块发生变化的时候,import是可以感知模块内,并导出最新的变量

 //model.js
   export const e1 = '测试1'
   setTimeout(() => { e1 = "测试2" }, 500)
   import { e1 } from './model'
   console.log(e1) // 输出 "测试1"
   setTimeout(() => { console.log(e1) }, 600)  // 输出"测试2"

所以import为了实现像require()那样的动态加载功能,引入了import()函数,该函数返回一个Promist对象,不同的是require()是同步加载,import()是异步加载

   import('./model.js').then(module => {
     console.log(module.e1) // 输出"测试1"
     setTimeout(() => {
       console.log(module.e1) // 输出"测试1"
     }, 600)
     // setTimeout(() => {
       console.log(module.e1) // 会报错,进入下面的catch,此时的模块还没有加载完成,异步获取不到报错
     }, 400)
     }).catch(err => {
       console.log(err)  // 异步加载没有完成报错进入这里
     })

备注: import由于静态编译的特性在编译的时候会提升到模块的头部,理论可以放在其他位置进行引入,但是还是建议一开始放在头部,例如下面情况

   const x = 1
   //  报错,由于import的提升特性
   if( x === 1) {
     import {e1} from './model'
   }