Node第七章(模块化一)

175 阅读5分钟

CommonJS规范

1.什么是模块化

在ECMScript2015之前 Node只支持 CommonJS的规范 ,在ECMScript2016+ Node开始支持ESM规范

  • Node中对CommonJS进行了支持和实现,让我们在开发Node的过程中可以方便进行模块化开发
    • 在Node中每个js文件都一个单独的模块
    • 这个模块中包括CommonJS规范的核心变量:exports moudule.exports require
  • 模块的核心就是导入导出
    • exports modules.exports可以负责对模块中的内容进行导出
    • require 函数可以帮助我们导入其它模块(Node内置模块自定义模块第三方模块)中的内容

2. 模块化的使用

exports和require的使用

  • bar.js文件中
const name = 'zs'
const age = '10'
function sayHello(name){
   console.log(`hello ${name}`) 
}
exports.name = name
exports.age = age
exports.sayHello = sayHello
  • index.js文件中
const bar = require('./bar.js')
console.log(bar) // {name:'zs',age:10,sayHello:Function}
  • 上面完成了什么样的操作呢?
    • 意味着index.js中的bar变量等于exports对象
    • 也就是require 各种查找方式,最终找到exports这个对象
    • 并且将这个exports对象赋值给bar这个变量
    • bar就是exports对象

module.expots的使用

  • 有了exports为啥还有module.exports?它俩到底是啥关系?

    • 我们去查看CommonJS规范能找到我们想要答案

      • 在Node中真正导出其实不是exports而是module.exports
    • 但是为什么,exports也可以导出呢?

    • 其实就是类似于这样

    const module = {
        exports:{}
    }
    let exports = module.exports
    
    • exports其实是module.exports的引用
  • 还是在刚刚的bar.js文件中

    const name = 'zs'
    const age = '10'
    function sayHello(name){
       console.log(`hello ${name}`) 
    }
    // 我们其实可以这样
    module.expotrs = {
        name,
        age,
        sayHello
    }
    
  • index.js文件中

    const bar = require('./bar.js')
    console.log(bar) // {name:'zs',age:10,sayHello:Function}
    

内部执行原理

修改了exports导出的属性会发生什么?
  • bar.js文件中

    const info = {
      name: "张三",
      age: 18,
      foo: function() {
        console.log("foo函数~")
      }
    }
    setTimeout(() => {
      info.name = '李四'
      console.log(info.name)  // 这里修改了 info对象里面name属性
    },1000)
    
    module.exports = info
    
  • index.js文件中

const {info} = require('./bar.js')
console.log(info) // {name:'李四',age:18,foo:Function}
  • bar.js修改了 info对象里面name属性, 那么 index.js文件中的 info对象name属性的值也会修改,因为他俩的内存地址指向是同一个
在index.js修改了exports 导出属性 会发生什么?
  • bar.js文件中

    const info = {
      name: "张三",
      age: 18,
      foo: function() {
        console.log("foo函数~")
      }
    }
    setTimeout(() => {
      console.log(info.name) // 此时打印什么呢?
    }, 1500);
    module.exports = info
    
  • index.js文件中

const {info} = require('./bar.js')
setTimeout(() => {
  info.name = '赵六';
}, 1000);
  • index.js 文件修改了 info对象里面name属性, 那么 bar.js文件中的 info对象name属性的值也会修改,因为他俩的内存地址指向是同一个
修改module.exports引用,那么exports还有用吗?
  • bar.js文件中

    const name = "why"
    const age = 18
    function sum(num1, num2) {
      return num1 + num2
    }
    exports.name = name
    exports.age = age
    exports.sum = sum
    // 此时我修改 module.exports的引用 重新赋值
    module.exports = {}
    
  • index.js 文件中

    const bar = require('./bar.js')
    console.log('bar',bar) // 会打印什么呢?
    
  • 结论是会输出 {},为什么呢?

    • ​ 因为在CommonJS规范中 exports是module.exports的引用 当 module.exports没有引用exports时

      exports导出的东西将会失效,只会导出module.exports引用

  • 我们可以在index.js文件中测试,在没修改exportsmodule.exports引用时

    console.log( exports=== module.exports)  // true
    
  • 在Node实现CommonJS导出源码中

     module.exports = {} // 创建一个对象
     exports = module.exports // 赋值给exports 
     exports=== module.exports // true 此时exports是全等于module.exports的
    

3.require细节

require查找规则

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

​ 那么require查找规则是什么呢?比如我写了一个require(x)

情况一:Node的内置(核心)模块
const path = require("path")
const fs = require("fs")
// 第三方包
const axios = require('axios')

path.resolve()
path.extname()

fs.readFile()
  • 直接返回内置模块,并且停止查找
情况二:自定义模块 路径 ./ 或 ../ 或 /(根目录)开头的
  • 第一步将 x当做一个 文件去查找
    • 如果有后缀名,就按照后缀名去查找
    • 如果没有后追梦,会按照以下顺序查找
      • 直接查找文件 x
      • 查找x.js文件
      • 查找x.json 文件
      • 查找 x.Node 文件
  • 第二步没有找到对应文件,将x 当做目录
    • 查找目录下面的index文件
      • 查找 x/index.js 文件
      • 查找 x/index.json 文件
      • 查找 x/index.Node 文件
  • 如果没有找到,就直接报错:not found
情况三:下载的Node_modules第三方包
  • 第三方包查找规则如下:

    • 首先Node会检查模块名是不是Node里面的核心模块(例如:http,fs)

    • 如果不是一个核心模块 查找Node_modules目录:

      • Node会在当前目录的Node_modules 目录查找

      • 如果当面目录没有找到,会在父目录 Node_modules继续查找

      • 如果父目录没有找到,会在父目录的父目录的 Node_modules继续查找

      • 如果父级顶层目录的 Node_modules没有 就会去全局Node_modules进行查找

      • 如果全局也没有 就直接报错:not found

      • 在Node中打印下面代码 就是Node查找规则

        console.log(module.paths); 
        

require加载过程

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

  • ​ 模块被多次引入时,最终只加载(运行)一次

    • 为什么只会加载一次呢?

      • 这是因为每个模块的对象module 都有一个属性:loaded
      • loaded:false就是没被加载,loaded:true就是被加载出来 会缓存,
      • loaded:true 的时候当我们去引入文件的时候 不会在运行
    • bar.js

      const name = '张三'
      cosnt age  = 18
      console.log('bar.js中代码执行')
      module.exports = {
          name,
          age
      }
      
    • index.js

      console.log('start,index.js')
      require('./bar.js')
      require('./bar.js')
      require('./bar.js')
      console.log('end,index.js')
      //最终输出结果
      start,index.js
      bar.js中代码执行
      end,index.js
      
  • 如果模块中出现了循环引用怎么办呢?加载顺序是什么呢?

    • 这种结构其实是图结构

      • 图结构有两种遍历过程:深度优先搜索广度优先搜索

      • Node 采用的遍历算法是:深度优先搜索 main>aaa>ccc>ddd>eee>bbb

        在 main.js文件中
        console.log("main")
        require("./aaa")
        require("./bbb")
        -------------
        在 aaa.js 文件中
        console.log("aaa")
        require("./ccc")
        -------------
        在 bbb.js 文件中
        console.log("bbb")
        require("./ccc")
        require("./eee")
        -------------
        在 ccc.js 文件中
        console.log("ccc")
        require("./ddd")
        -------------
        在 ddd.js 文件中
        console.log("ddd")
        require("./eee")
        -------------
        在 eee.js 文件中
        console.log("eee")
        // 最终代码执行结果
        main
        aaa
        ccc
        ddd
        eee
        bbb 
        // bbb文件执行完成只会又去引入ccc和eee文件,
        // 但是ccc和eee文件被执行过一次多次require是无效的,
        // 所以执行到bbb就没有打印结果了
        

4. CommonJS规范的缺点

  • CommonJS加载模块是同步的

  • 同步意味着只有等模块加载完毕 才能执行后面代码,如果当前模块非常耗时会有问题

  • 不能用于浏览器

  • 浏览器加载js是从服务器将文件下载下来,之后加载运行

  • 采用同步意味着后续js代码无法正常运行,比如 DOM操作 AJAX请求 都会进行阻塞

  • 当然现在webpack,vite搭建的项目使用CommonJS是另外一回事

  • 在ECMAScript2015之前浏览器的模块化是AMD和CMD, 在ECMScript2015开始支持ESM规范

  • 目前浏览器和Node已经支持ESM规范,另一方面借助于webpack、vite等工具可以实现

CommonJS与ESM代码的转换