javascript-模块化

loading 2022年11月23日 76次浏览

1.引入

1.1 模块的概念

将一个复杂的程序依据一定的规范封装成几个文件,并将它们组合在一起。

1.2 模块化的发展史

(1)全局function模式

将不同的功能封装成不同的全局函数。
缺点在于污染全局命名空间,容易引起冲突,而且模块成员间关系不紧密。

        function m1() {
            //...
        }
        function m2() {
            //...
        }

(2)对象封装模式

减少了全局变量,解决了命名冲突。
缺点在于数据不安全,外部可以直接修改模块内部的数据。

        let myModule = {
            data : 123,
            getData : function(){
                console.log(this.data);
            }
        }
        myModule.data = 456;
        myModule.getData() //456

(3)IIFE模式(立即执行函数闭包)
实现了数据私有,通过给window添加属性来向外暴露接口。
缺点是当一个模块依赖另外一个模块时不好操作。

        // module.js文件
        (function (window) {
            let data = 'www.baidu.com'
            function foo() {
                console.log(`foo() ${data}`)
            }
            function bar() {
                console.log(`bar() ${data}`)
                otherFun()
            }
            function otherFun() {
                //内部私有的函数
                console.log('otherFun()')
            }
            //暴露行为
            window.myModule = { foo, bar } //ES6写法
        })(window)

2.CommonJS

2.1 概念和特点

采用CommonJS规范,每个文件就是一个模块,有自己的作用域。 在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。

服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。

有以下特点:

  • 所有代码运行在模块作用域,不会污染全局作用域
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,之后再加载,就直接读取缓存结果。要想让模块再次运行需要清除缓存。
  • 模块加载的顺序按照其在代码中出现的顺序

2.2 基本语法

导出模块:

  • module.exports = value
  • exports.xxx = value

导入模块:

  • require(xxx)

如果是第三方模块,xxx为模块名;如果是自定义模块,xxx为模块文件路径

CommonJS规定,每个模块内部,module代表当前模块,这个变量是一个对象;module.exports属性是对外的接口,也就是说加载某个模块,其实就是加载该模块的module.exports属性。

下面两段代码分别写在两个js文件里

module.exports输出变量和函数

let x = 5;
let addX = val => val + x;
module.exports.x = x;
module.exports.addX = addX;

利用vscode的codeRunner插件就能运行js文件里的代码

require命令读入并执行一个js文件,然后返回该模块的exports对象。

var example = require('./example.js');
console.log(example.x); // 5
console.log(example.addX(1)); // 6

2.3 模块的加载机制

CommonJS模块的加载机制是,输入的是被输出的值的拷贝。 也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。这和ES6的模块化有着重大的差异。

比如这个例子,counter是个原始类型的值,输出后会被缓存。除非写成一个函数,才能得到内部变动后的值。

var counter = 3;
function incCounter() {
    counter++;
}
module.exports = {
    counter: counter,
    incCounter: incCounter,
};
var counter = require('./example').counter;
var incCounter = require('./example').incCounter;

console.log(counter);  // 3
incCounter();
console.log(counter); // 3

2.4 实例

module1

module.exports = {
    msg : 'module1',
    foo(){
        console.log(this.msg);
    }
}

module2

module.exports = function(){
    console.log('module2')
}

module3

exports.foo = function(){
    console.log('module3')
}
exports.arr = [1,2,2,3,4,5];

app.js

//用于数组去重的第三方模块 需要npm install uniq --save
let uniq = require('uniq');
let m1 = require('./module1');
let m2 = require('./module2');
let m3 = require('./module3');

m1.foo();//module1
m2();//module2
m3.foo();//module3
console.log(uniq(m3.arr));//[1,2,3,4,5]

3.AMD

CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数。

由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。

但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。此外AMD规范比CommonJS规范在浏览器端实现要来得早。

3.1 基本语法

导出模块

//定义没有依赖的模块
define(function(){
   return 模块
})

//定义有依赖的模块
define(['module1', 'module2'], function(m1, m2){
   return 模块
})

导入模块

require(['module1', 'module2'], function(m1, m2){
   使用m1/m2
})

3.2 实例

首先创建这么一个目录结构

|-js
  |-libs
    |-require.js
  |-modules
    |-alerter.js
    |-dataService.js
  |-main.js
|-index.html

dataService.js
该模块作用就是返回msg的大写形式

//定义没有依赖的模块
define(function(){
    let msg = 'yoimiya'
    function getMsg(){
        return msg.toUpperCase();
    }
    return {getMsg};
})

alerter.js
该模块依赖于dataService模块,返回一个拼接出来的字符串。

//定义有依赖的模块
define(['dataService'] , function(dataService){
    let name = 'gorgeous'
    function showMsg(){
        console.log(dataService.getMsg() + ' is ' + name);
    }
    return {showMsg}
})

main.js
主配置文件,通过html中引入script时data-main的设置使得它对所有页面起到配置作用。

// main.js文件
(function() {
    require.config({
      baseUrl: 'js/', //基本路径 出发点在根目录下
      paths: {
        //模块标识名: 路径
        alerter: './modules/alerter', //此处不能写成alerter.js,会报错
        dataService: './modules/dataService'
      }
    })
    //依赖并调用alerter中的showMsg()
    require(['alerter'], function(alerter) {
      alerter.showMsg()
    })
  })()

index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Modular Demo</title>
  </head>
  <body>
    <!-- 引入require.js并指定js主文件的入口 -->
    <script data-main="js/main" src="js/libs/require.js"></script>
  </body>
</html>

最终效果:

4.ES6 Module

4.1 基本概念

es6之前,基本是CommonJS用于服务器,AMD用于浏览器。es6新推出的module实现了二者的统一,较好地实现了模块的功能。(在node中使用Module需要通过babel转换)

Module的思想是尽量地静态化,使得编译时就能确定模块的依赖关系(CommonJS和AMD都只能在运行时确定依赖关系)。

比如CommonJS模块就是对象,输入时必须查找对象属性:

下面代码的实质是即加载fs的所有方法,生成一个对象_fs,然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。

// CommonJS模块
let { stat, exists, readfile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

ES6模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

下面代码的实质是直接从fs模块加载3个方法,其他方法不加载。 这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。

import { stat, exists, readFile } from 'fs';

4.2 export

export命令用于规定模块的对外接口

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。

var basicNum = 0;
var add = function (a, b) {
    return a + b;
};

export { basicNum, add };

另外,export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。(这一点和CommonJS是截然相反的,common输出的是值的缓存,不会改变)

export var foo = 'bar';
//500ms后foo的值变为baz
setTimeout(() => foo = 'baz', 500);

4.3 import

使用export命令定义了模块的对外接口以后,其他js文件就可以通过import命令加载这个模块。

import { basicNum, add } from './math';

function test(ele) {
    ele.textContent = add(99 + basicNum);
}

可以看到,使用import命令时,用户需要知道所要加载的变量名或函数名。否则无法加载。

为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

导出

var basicNum = 0;
var add = function (a, b) {
    return a + b;
};

export default{ basicNum, add };

导入时不再需要大括号里面写上一堆变量名

import math from './math';

function test(ele) {
    ele.textContent = math.add(99 + math.basicNum);
}

4.4 特征

  • Module自动采用严格模式
  • import的属性是只读的,不能赋值
  • import/export不能位于块级作用域内,模块内的import/export会变量提升到模块顶部

4.5 CommonJS和Module的差异

(1)CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

CommonJS模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。

Module是动态绑定,原始值变了,import加载的值也会跟着变,并不会缓存值,模块里的变量绑定其所在模块。

比如这个上面出现在CommonJS里的例子,可以发现这一次incCounter()函数影响到了counter的值。

// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}

// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

(2)CommonJS模块是运行时加载,Module是解析时加载。

CommonJS加载的是一个对象(即module.exports属性),即在输入时先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”,对象只有在脚本运行完才会生成。

ES6模块不是对象,export提供的对外接口只是一种静态定义,在代码静态解析阶段就会生成。import采用静态命令的形式。可以直接加载需要的方法,而不是加载整个模块,这种加载称为“解析时加载”。

(3)CommonJS主要针对服务端,Module主要针对浏览器端。

虽然Module其实两端都挺好,只是相对而言。

主要说一下针对两端的区别在哪:

服务器端一般采用同步加载文件,也就是说需要某个模块,服务器端便停下来,等待它加载再执行

浏览器端要保证效率,需要采用异步加载,这就需要一个预处理,提前将所需要的模块文件并行加载好