• javascript复习之旅 13.2 模块化(下)


    start

    为什么要模块化

    • 名称污染
    • 依赖管理

    主流的模块化

    1. CommonJS
    2. ES Module

    当然还有:AMD CMD (这里暂时就不详细说明)

    1. CommonJS

    1.1 基础知识

    CommonJS 的提出,弥补 Javascript 对于模块化,没有统一标准的缺陷。nodejs 借鉴了 CommonJS 的 Module ,实现了良好的模块化管理。

    目前 CommonJS 广泛应用于以下几个场景:

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

      我们很多的依赖包,都是基于CommonJS模块化规范去做的。

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

    • webpack 打包工具对 CommonJS 的支持和转换;也就是前端应用也可以在编译之前,尽情使用 CommonJS 进行开发。

    1.2 显著特点

    在使用 CommonJS 规范下,有几个显著的特点。

    • commonjs 中每一个 js 文件都是一个单独的模块,我们可以称之为 module;

    • 该模块中,包含 CommonJS 规范的核心变量: exports、module.exports、require;

    • exports 和 module.exports 可以负责对模块中的内容进行导出;

    • require 函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;

    1.3 基础使用

    // main.js
    var a = require('./a')
    console.log('打印', a) // 打印 { value: '你好', say: [Function: say] }
    
    
    // a.js
    var value = '你好'
    function say() {
      console.log('你好呀')
    }
    
    exports.value = value
    exports.say = say
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    1.4 CommonJS 模块整体是如何运行的。

    // 实际上是在外层包装了一层,传入了 exports,require,module,__filename,__dirname 这些变量。
    (function(exports,require,module,__filename,__dirname){
       const sayName = require('./hello.js')
        module.exports = function say(){
            return {
                name:sayName()
            }
        }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    1.5 require

    1.5.1 require 加载的文件类别

    ① 为 nodejs 底层的核心模块。(例如 fs path)

    ② 为我们编写的文件模块,比如上述 sayName (绝对或相对路径的文件)

    ③ 为我们通过 npm 下载的第三方自定义模块,比如 crypto-js。 (第三方依赖库)

    1.5.2 require 的查找逻辑

    1. 缓存的模块(require过一次就会被缓存起来)

    2. 核心模块,绝对或相对路径文件模块(直接读取)

    3. 第三方模块

      • 优先当前目录下的 node_modules
      • 查找上一级文件夹直到根目录
      • 找到 node_modules 查找同一名称的文件包。
      • 查找对应包的 (package.json的main属性指向的文件 > index.js > index.json > index.node

    1.5.3 require 模块引入与处理(代码执行的顺序,同步执行)

    1. 主文件,从上往下执行。
    2. require()的文件未被缓存 ,就执行对应的文件;
    3. 如果有缓存,就不进入这个文件,继续向后执行。

    1.5.4 测试

    main.js

    console.log('main1')
    var a = require('./a')
    console.log('main2')
    var b = require('./b')
    a.say()
    console.log('main2')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    a.js

    const b = require('./b')
    
    console.log('我是 a 文件', b)
    
    exports.say = function () {
      const getMes = require('./b')
      const message = getMes()
      console.log(message)
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    b.js

    const say = require('./a')
    const object = {
      name: '你好呀',
    }
    console.log('我是 b 文件')
    console.log('打印 a 模块', say)
    
    setTimeout(() => {
      console.log('异步打印 a 模块', say)
    }, 0)
    
    module.exports = function () {
      return object
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    node ./main.js

    看下输出结果

    main1
    我是 b 文件
    打印 a 模块 {}
    我是 a 文件  [Function]
    main2
    {
      name: '你好呀',
    }
    main2
    异步打印 a 模块 { say: [Function] }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    why?

    执行顺序可参考1.5.3的内容,CommonJS同步执行,其实也蛮好理解的。

    1.6 exports

    先啰嗦几句:

    • 单词请记好,加s
    • 千万不要因为单词混淆了导致出错!!!

    先打印看看

    console.log(module.exports) // {}
    console.log(exports) // {}
    console.log(module.exports === exports) // true
    
    • 1
    • 2
    • 3

    这两个变量 全等于,那为什么会有两种写法?

    // main.js
    var a = require('./a')
    var b = require('./b')
    console.log('a', a) // a {}
    console.log('b', b) // {name:"新的b对象"}
    
    // a.js
    exports={name:"新的a对象"}
    
    // b.js
    module.exports={name:"新的b对象"}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    why? 其实这个地方很好理解,可以这样理解:

    我们实际导出的是 `module.exports`
    
    然后我们在当前模块又声明一个变量 `exports = module.exports ` 赋值了值引用。
    
    所以两者 全等于
    
    但是当我们 exports={name:"新的a对象"}, 改变了它的值引用,所以上述示例中,读取的是空对象。
    
    其次,既然导出的是 `module.exports`, 那么可以不仅仅局限于导出对象,还可以导出其他类型的变量,例如数组 函数 字符串等等
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    1.7 个人总结

    我个人理解CommonJS是如何解决开头提到的痛点:

    1. 每个模块的变量都是独立的(因为有包装函数),输出数据包裹在对象中,来解决命名冲突的问题。
    2. 其次除了暴露的数据,内部变量私有,外部无法直接修改。
    3. 代码同步执行,配合上缓存,来解决依赖之间错综复杂的关系。

    1.8 其他情况的试验

    输出的数据和模块数据之间的关系?

    // main.js
    var a = require('./a')
    console.log('a', a) // a { value: 1, obj: { a: 1 }, add: [Function: add] }
    
    a.add()
    console.log('a', a) // a { value: 1, obj: { a: 2 }, add: [Function: add] }
    /* 我理解这里的导出的赋值逻辑类似于`=`; 原始数据类型赋复制的值 对象数据类型赋引用地址 */
    
    
    // a.js
    var value = 1
    var obj = {
      a: 1,
    }
    
    function add() {
      value++
      obj.a++
    }
    
    module.exports = {
      value,
      obj,
      add,
    }
    
    
    • 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

    2. ES Module

    2.1 基础知识

    Nodejs 借鉴了 Commonjs 实现了模块化 ,从 ES6 开始, JavaScript 才真正意义上有自己的模块化规范,

    ES Module 的产生有很多优势,比如:

    • 借助 Es Module 的静态导入导出的优势,实现了 tree shaking
    • Es Module 还可以 import() 懒加载方式实现代码分割。

    Es Module 中用 export 用来导出模块,import 用来导入模块。

    2.2 基本使用

    
    DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Documenttitle>
    head>
    
    <body>
      
      <script src="./main.js" type="module">script>
    body>
    
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    // main.js
    import { increment, count } from './a.js'
    
    increment()
    console.log(count) // 2
    
    // a.js
    export let count = 1
    
    export function increment() {
      count++
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2.3 ES Module 如何运行

    2.3.1 注意事项

    • ES Module 的引入和导出是静态的;

    • import 会自动提升到代码的顶层 ;

    • import , export 不能放在块级作用域或条件语句中;

    • import 的导入名不能为字符串或在判断语句;

    2.3.2 如何执行

    // main.js
    console.log('main.js开始执行')
    import say from './a'
    import say1 from './b'
    console.log('main.js执行完毕')
    
    // a.js
    import b from './b'
    console.log('a模块加载')
    export default  function say (){
        console.log('hello , world')
    }
    
    // b.js
    console.log('b模块加载')
    export default function sayhello(){
        console.log('hello,world')
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    解释:
    
    1. 入口main.js
    2. main.js 的 import 语句提升到顶部
    3. (main文件的import say from './a')深度优先遍历,先进入到 `./a`
    4. (a文件的import b from './b')深度优先遍历,先进入到 `./b`
    5. 运行b.js 打印 `b模块加载`
    6. 导出一个sayhello函数,返回
    7. 继续执行a.js  打印 `a模块加载` 
    8. 导出say函数返回
    9. 继续执行main.js, (main文件的import say1 from './b') 已经引入过了,直接跳过
    9. 继续执行main.js,打印 `main.js开始执行`&&`main.js执行完毕` 
    
    输出:
    
    b模块加载
    a模块加载
    main.js开始执行
    main.js执行完毕
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    1. 使用 import 被导入的模块运行在严格模式下。

    2. 使用 import 被导入的变量是只读的,可以理解默认为 const 装饰,无法被赋值

    3. 使用 import 被导入的变量是与原变量绑定/引用的,可以理解为 import 导入的变量无论是否为基本类型都是引用传递。

    4. 支持动态引入,import()import() 返回一个 Promise 对象, 返回的 Promise 的 then 成功回调中,可以获取模块的加载成功信息。

    5. ES6 模块会在程序开始前先根据模块关系查找到所有模块,生成一个无环关系图,并将所有模块实例都创建好,这种方式天然地避免了循环引用的问题,当然也有模块加载缓存,重复 import 同一个模块,只会执行一次代码。

    CommonJS 总结

    Commonjs 的特性如下:

    • CommonJS 模块由 JS 运行时实现。
    • CommonJS 是单个值导出,本质上导出的就是 exports 属性。
    • CommonJS 是可以动态加载的,对每一个加载都存在缓存,可以有效的解决循环引用问题。
    • CommonJS 模块同步加载并执行模块文件。

    ES Module 总结

    Es module 的特性如下:

    • ES6 Module 静态的,不能放在块级作用域内,代码发生在编译时。
    • ES6 Module 的值是动态绑定的,可以通过导出方法修改,可以直接访问修改结果。
    • ES6 Module 可以导出多个属性和方法,可以单个导入导出,混合导入导出。
    • ES6 模块提前加载并执行模块文件,
    • ES6 Module 导入模块在严格模式下。
    • ES6 Module 的特性可以很容易实现 Tree Shaking 和 Code Splitting。

    优质的模块化相关的博客

    • https://juejin.cn/post/6994224541312483336

    • https://juejin.cn/post/6844904080955932680

    end

    • 看了很多优质的博客,才对模块化有一个初步的认识。
    • 使用一个东西,肯定是希望能够借助它,解决我们遇到的痛点。模块化的出现,其实就是为了解决命名冲突和依赖管理。
    • 学到这里,暂时就了解了两个模块化规范。

    最后提一个问题,打包工具例如 webpack 如何应对这么多种模块化规范的呢?

  • 相关阅读:
    NetworkManager 图形化配置 bond
    L2W3作业 TensorFlow教程
    《深度学习》深度学习 框架、流程解析、动态展示及推导
    IDL学习:语法基础-过程和函数
    微软商店无法访问
    基于紫光同创FPGA的图像采集及AI加速
    前端项目中,强缓存和协商缓存的配置
    在OCP集群内部署测试应用
    C++ 关联式容器map+set
    更改字段的类型
  • 原文地址:https://blog.csdn.net/wswq2505655377/article/details/126085514