12

141 阅读6分钟

模块化的开发方式可以提高代码复用率,方便进行代码的管理。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。目前流行的js模块化规范有CommonJS、AMD、CMD以及ES6的模块系统。

传统方法

默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到script标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。 如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞,用户会感觉到浏览器“卡死”了,没有任何响应。这显然是很不好的体验,所以浏览器允许脚本异步加载,下面就是两种异步加载的语法。

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script> 

上面代码中,script标签打开defer或async属性,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。 defer与async的区别是:defer要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer是“渲染完再执行”,async是“下载完就执行”。另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。

CommonJS

Node.js是commonJS规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:module、exports、require、global。实际使用时,用module.exports定义当前模块对外输出的接口(不推荐直接用exports),用require加载模块。

common.js是导出同步模块

node.js中可以直接用module.exports导出,let module1 = require('./modules/module1')引入 浏览器中使用需要借助工具才能正常使用

npm install browserify -g
npm install browserify -save-dev
module.exports.name = "蛙人"
module.exports.age = 24

let data = require("./index.js")
console.log(data) // { name: "蛙人", age: 24 }

module.exports 和 exports 的区别

当使用分别暴露时,module.exports和exports是一样的

const add = (a, b) => a + b

const subtract = (a, b) => a - b

module.exports.add = add
module.exports.subtract = subtract

exports.add = add
exports.subtract = subtract

当使用统一暴露时,它俩是不一样的

可以这么写
module.exports = { add, subtract }

不能这么写
exports = { add, subtract }

原因

//源码
const exports = this.exports;  
const thisValue = exports;  
const module = this;

exports和module.exports指向同一个对象。所以当你使用分别暴露时,实质上是给这个对象上添加了一个个属性,这两种方式是同一个意思。 但是,一旦写成统一暴露,exports = { add, subtract }改变了exports的指向,切断了与module.exports之间的引用关系,exports不再是module.exports的那个对象了

总结

CommonJs解决了变量污染,文件依赖等问题,它可以动态导入(代码发生在运行时),不可以重复导入。

AMD

amd是专门为浏览器所设计的

AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。这里介绍用require.js实现AMD规范的模块化:用require.config()指定引用路径等,用define()定义模块,用require()加载模块。

1.加载模块

首先我们需要引入require.js文件和一个入口文件main.js。main.js中配置require.config()并规定项目中用到的基础模块。

/** 网页中引入require.js及main.js **/
<script src="js/require.js" data-main="js/main"></script>

/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
  baseUrl: "js/lib",
  paths: {
    "jquery": "jquery.min",  //实际路径为js/lib/jquery.min.js
    "underscore": "underscore.min",
  }
});
// 执行基本操作
require(["jquery","underscore"],function($,_){
  // some code here
});

2.定义模块

// name和deps都是非必选的参数,而callback可以是一个对象,或者是具有返回值的函数
define([name], [deps], callback)

存在依赖的模块 假设你要写一个依赖jquery的模块,那么你需要在define方法中声明依赖。

define(['jquery'], function($) {
    function setColor(select, color) {
        $(select).css('color', color)
    }
    return {
        setColor: setColor
    }
})

另一种方法

define(function(require, exports, module) {
    var $ = require('jquery')
    function setColor(select, color) {
        $(select).css('color', color)
    }
    return {
        setColor: setColor
    }
})

3.使用模块

require(['simple', 'jquery', 'funcModule', 'depModule'], function(simple, $, funcModule, depModule) {
    console.log(simple)
    console.log($)
    $('.word').css({
        fontSize: '24px',
        color: 'blue'
    })
    var result = funcModule.add(1,2)
    console.log(result)

    depModule.setColor('.word', 'yellow')
})

ES6 Module

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。

/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
    return a + b;
};
export { basicNum, add };

/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
    ele.textContent = add(99 + basicNum);
}

如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名。其实ES6还提供了export default命令,为模块指定默认输出,对应的import语句不需要使用大括号。这也更趋近于ADM的引用写法。

/** export default **/
//定义输出
export default { basicNum, add };
//引入
import math from './math';
function test(ele) {
    ele.textContent = math.add(99 + math.basicNum);
}

导出

在Es Module中导出分为两种,单个导出(export)、默认导出(export default),单个导出在导入时不像CommonJs一样直接把值全部导入进来了,Es Module中可以导入我想要的值。那么默认导出就是全部直接导入进来,当然Es Module中也可以导出任意类型的值。

// 导出变量
export const name = "蛙人"
export const age = 24

// 导出函数也可以
export function fn() {}
export const test = () => {}


// 如果有多个的话
const name = "蛙人"
const sex = "male"
export { name, sex }

混合导出

可以使用export和export default同时使用并且互不影响,只需要在导入时地方注意,如果文件里有混合导入,则必须先导入默认导出的,在导入单个导入的值。

export const name = "蛙人"
export const age = 24

export default {
    fn() {},
    msg: "hello 蛙人"
}

导入值的变化

export导出的值是值的引用,并且内部有映射关系,这是export关键字的作用。而且导入的值,不能进行修改也就是只读状态。

// index.js
export let num = 0;
export function add() {
    ++ num
}

import { num, add } from "./index.js"
console.log(num) // 0
add()
console.log(num) // 1
num = 10 // 抛出错误

ES6的模块不是对象,import命令会被 JavaScript 引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。

if (true) {
	import xxx from 'XXX' // 报错
}

总结

Es Module也是解决了变量污染问题,依赖顺序问题,Es Module语法也是更加灵活,导出值也都是导出的引用,导出变量是可读状态,这加强了代码可读性。

ES6 模块与 CommonJS 模块的差异

1. 动态与静态

CommonJs可以动态加载语句,代码发生在运行时 Es Module是静态的,不可以动态加载语句,只能声明在该文件的最顶部,代码发生在编译时

2. 值

CommonJs导出值是拷贝,可以修改导出的值,这在代码出错时,不好排查引起变量污染 Es Module导出是引用值之前都存在映射关系,并且值都是可读的,不能修改