Commonjs和ESModule

102 阅读5分钟

前言

    // a.js
    var name = 'ywq'
    var age = 12

    // b.js
    var name = 'yyy'

    // index.js
    console.log(`我的名字是${name},我今年${age}了`) // 我的名字是yyy,我今年12了

    // index.html
    <script src="./a.js"></script>
    <script src="./b.js"></script>
    <script src="./index.js"></script>

以前我们在开发时,经常会出现这样的情况,代码被覆盖掉了. 在多人开发时,是非常烦人的.因此有下面的一种解决方案.

    // a.js
    var moduleA = (function(){
        var name = 'ywq'
        var age = 12
        return {
            name,
            age
        }
    })()

    // b.js
    var moduleB = (function() {
        var name = 'yyy'
    })()

    // index.js
    var moduleIndex = (function() {
        console.log(`我的名字是${moduleA.name},我今年${moduleA.age}了`) // 我的名字是ywq,我今年12了
    })()

    // index.html
    <script src="./a.js"></script>
    <script src="./b.js"></script>
    <script src="./index.js"></script>
  • 防止代码被污染,因此js文件需要有自己的作用域,而函数拥有自己的作用域
  • 使用立即执行函数,解决了函数命名及调用问题
  • 通过函数返回值,对外暴露

上面的解决方式,确实带来了极大的便利.但是全局的moduleA,moduleB... 随着代码量的增多,我们也保证不了会不会也被覆盖啊... 因此,社区推出了CommonJS规范,而node实现了这一规范

一、CommonJS

    // a.js
    const name = 'ywq'
    const age = 18

    module.exports = {
        name,
        age
    }

    // b.js
    var name = 'yyy'
    module.exports = {
        name
    }

    // index.js
    const moduleA = require('./a.js')
    const moduleB = require('./b.js')
    console.log(`我的名字是${moduleA.name},我今年${moduleA.age}了`)
    // 我的名字是ywq,我今年18了

上面是一段简单的符合CommonJS规范的代码.

  • 每个js文件都是一个单独模块(有自己的作用域)
  • 模块可通过module.exports、exports对外导出,require对内导入

下面我们来介绍一下CommonJS的使用吧

1.导入导出

1.1 导出

  • module.exports = { 对外暴露的变量 } ---- module是对象,exports也是对象
    const name = 'ywq'
    const age = 12
    // 导出
    module.exports = {
        name,
        age
    }
    // 或者这样导出
    // module.exports.name = name
    // module.exports.age = age
  • exports.对外暴露的变量 = 'xxx'
    const name = 'ywq'
    const age = 12
    exports.name = name
    exports.age = age

1.2 导入

  • require(X)
    const moduleA = require('./a.js')

1.3 module.exports 和 exports 有什么关系

    // 第一段代码
    // a.js 
    exports.aaa = '111'
    exports.bbb = '222'
    module.exports = { ccc: '333', ddd: '444' }

    // b.js
    const m = require('./a.js')
    console.log(m) // 结果: { ccc: '333', ddd: '444' }

    // ---------------

    // 第二段代码
    // a.js 
    module.exports = { ccc: '333', ddd: '444' }
    exports.aaa = '111'
    exports.bbb = '222'
    
    // b.js
    const m = require('./a.js')
    console.log(m) // 结果: { ccc: '333', ddd: '444' } 

上面代码我们有发现什么呢?

    // 伪代码
    module.exports = {}
    exports = module.exports
  • module.exports和exports都可以对外暴露,当两者同时出现时,总是以module.exports进行导出
  • exports是符合CommonJS规范的,module.exports是不符合CommonJS规范的.node为了符合CommonJS规范,特意添加了exports
  • exports内部实现都是通过module.exports进行导出的

2.内部原理

下面我们来看一段代码

    // a.js
    let obj = {
        name: "小狗",
        color: "黄色"
    }
    setTimeout(() => {
        obj.color = "黑色";
    }, 1000);

    setTimeout(() => {
        console.log(obj. color);
    }, 3000);

    module.exports = obj

    // b.js
    let moduleA = require('./a.js');
    console.log(moduleA.color);

    setTimeout(() => {
        console.log(moduleA.color);
        moduleA.color = "红色";
    }, 2000);

    // 打印结果依次为:  黄色  黑色  红色

上面代码,我们发现了什么? 我们将obj暴露出去,外部引入文件后,可以对引入的数据进行更改,并且导出文件里面对应的数据也随之进行了修改. 这是为什么嘞???

    // 伪代码
    // a.js
    module.exports = {}

    // b.js
    const m = function require(x) {
        return module.exports
    }
    console.log(module.exports === m) // true
  • module.exports是一个对象(引用数据类型),有其对应的内存空间
  • module.exports导出的数据,就是require导入的数据
  • module.exports和require指向的是同一个指针

3.require的查找规则 -> require(X)

  • 情况一: X是node的核心模块
    const fs = require('fs')
  • 情况二: X是路径 ./ ../ /

    • 将X当作文件在当前文件的目录下查找

      1. 文件带后缀名,根据后缀名查找

      2. 文件不带后缀名

        2.1. 按照 .js/.json/.node 后缀名依次查找

    • 将X当作目录 (X作为文件没有找到的情况下)

      1. 查找目录下的X/index.js文件
      2. 查找目录下的X/index.json文件
      3. 查找目录下的X/index.node文件
  • 情况三: 既不是node核心模块,也不是一个路径 (通常是第三方库)

    console.log(module)
image.png

require会一层一层根据paths查找node_modules目录下面是否有对应目录,如果没有,最后会报错

4.模块加载流程

    // a.js
    console.log('a文件开始执行')
    module.exports = { name: 'a' }
    // b.js
    console.log('b文件开始执行')
    require('./a.js')
    require('./a.js')
    require('./a.js')
    console.log('b文件后续代码')

image.png 我们又发现了什么?

  • commonjs代码是同步执行的,文件加载并执行完后才继续执行后续代码

  • 多次导入同一个文件,该文件中的代码只会执行一次

    原因: node中有loaded属性记载是否文件被加载过

5.模块加载顺序

    // a.js
    console.log('aaa')
    require('./b.js')
    require('./c.js')

    // b.js
    console.log('bbb')
    require('./d.js')

    // c.js
    console.log('ccc')
    require('./e.js')

    // d.js
    console.log('ddd')
    require('./e.js')

    // e.js
    console.log('eee')

image.png

二、AMD

    // index.html
    <script src="./require.js" data-main="./index.js"></script>

    // index.js
    require.config({
        baseUrl: './src',
        foo: './foo',
        bar: './bar'
    })
    console.log(111)
    require(['foo', 'bar'], function(module) {
        console.log('index:', module)
    })
    console.log(222)

    // src/foo.js
    define(function() {
        const a = 10
        const b = 3
        return {
            a,
            b
        }
    })

    // src/bar.js
    define(function() {
        console.log(333)
        require(['foo'], function(module) {
            console.log('bar:', module)
        })
        console.log(444)
    })
image.png
  • data-main: 保证require.js文件加载后,首先执行index.js文件
  • require(): 保证代码是异步执行
  • define(): 相当于拥有自己的作用域

三、CMD

    // index.html
    <script src="./sea.js"></script>
    <script>seajs.use('./index.js')</script>

    // index.js
    define(function(require, exports, module) {
        console.log(111)
        const foo = require('./src/foo.js')
        console.log(foo.sum(3, 2))
        console.log(222)
    })

    // src/foo.js
    define(function(require, exports, module) {
        const sum = (num1, num2) => {
            return num1 + num2
        }
        const name = 'ywq'
        module.exports = {
            name,
            sum
        }
    })

image.png

四、ESModule

    // index.html
    <script src="./index.js" type="module"></script>

    // index.js
    import { name, age } from './foo.js'
    console.log(name, age) // ywq 18

    // foo.js
    const name = 'ywq'
    const age = 18
    export {
        name,
        age
    }

上面是一段使用ESModule的代码.

  • type是为了使index.js以ESModule的方式进行解析,避免当作普通的js文件
  • import: 对内引入 export: 对外暴露
  • 采用编译器静态分析,也可以动态导入
  • 自动采用严格模式

1.导入导出

1.1 导出

  • export 声明语句
    // a.js
    export const a = 1
    export const b = 2
  • export { 要暴露的变量 }
    // foo.js
    function sum(num1, num2) {
        return num1 + num2
    }
    export {
        // sum  // 命名导出
        sum as bbb // 起别名
    }
  • default 默认导出
    // foo.js
    function sum(num1, num2) {
        return num1 + num2
    }
    // 第一种:
    export {
        sum as default
    }
    // 第二种:
    export default sum

1.2 导入

  • 普通的导入
    import { a, b } from './a.js'
    import { bbb } from './foo.js'
  • 起别名
    import { name as sname } from './a.js'
  • 导入所有
    import * as my from './a.js'
  • default 默认导入
    import my from './a.js'

注意:

    // index.js 
    import { a, b } from './main.js'
    export { a, b}

    // 在一个文件里面,导入和导出的一样,可使用下面的语法糖
    export { a, b } from './main.js'

2.import动态导入

    // a.js  
    export const name = 'ywq'

    // b.js
    console.log(111)
    if(3 > 2) {
        import('./a.js').then(res => {
            console.log(res.name)
        })
    }
    console.log(222)

image.png

import动态导入时,返回值是一个Promise

3.原理

原理可参考hacks.mozilla.org/2018/03/es-…

结论: 暴露的变量,只能在导出文件进行修改; 在导入文件修改会报错