JavaScript学习笔记:模块
前言
在js编程中,模块指的是按照一定格式将代码以功能拆分后作为独立文件存在的一个实体。
早期的JS并没有规定模块应该如何设计,核心语言也没有针对模块提供相关支持。
早期的代码使用IIFE来实现一个模块, 它是通过向全局对象添加属性来实现与其他模块来交互的。
(function() {
var calc = {
add: function(a,b) {return a+b;},
subtract: function(a,b) {return a-b;},
multiply: function(a,b) {return a*b;},
divide: function(a,b) {return a/b;}
};
window..calc = calc;
}());
这样的代码对全局对象造成了很大的污染,一个属性一旦被模块占用,就不能另作他用了。
为了解决这样的问题,出现了很多定义模块的规范与实现,著名的有CommonJS,AMD,CMD。
开源社区提出的浏览器模块化规范与实现
CommonJS 与 node.js
CommonJS是node.js的模块化实现使用的规范,它是同步引入文件的。因为node.js是服务器软件,其代码文件在本地,读取文件与解析代码的时间可以忽略不计,所以使用该该规范没有问题。
但是浏览器不然,受限于网络传输的速度慢、不稳定的问题,同步引入文件是不现实的,更何况解析引擎并没有为模块做任何支持。所以浏览器环境的实现使用异步方式引入模块文件,将业务代码写在回调函数里,在文件引入代码就绪后再执行回调函数里的业务代码。
AMD 与 Require.js
客户端流行的规范是AMD,其流行的实现是Require.js。它给全局对象添加了define
与require
两个函数属性用于定义与引入模块,还解决了模块依赖的问题,也就是JS文件引入的先后顺序的问题。
Require.js核心代码定义在全局变量requirejs
中,通过给其定义config
属性来配置模块名与代码文件路径的关系,还支持将不使用AMD规范的模块定义为可以被require方法导入的模块。
// 配置模块与文件的关系
require.config({
baseUrl : '/js',
paths : {
//当百度的jquery没有加载成功后,会加载本地js目录下的jquery
jquery : ['http://libs.baidu.com/jquery/2.0.3/jquery', '/public/js/jquery'],
jqueryUI : 'http://cdn.bootcss.com/jqueryui/1.11.4/jquery-ui',
calc: 'calc'
}
});
// 模块calc calc.js
define(function() {
var calc = {
add: function(a,b) {return a+b;},
subtract: function(a,b) {return a-b;},
multiply: function(a,b) {return a*b;},
divide: function(a,b) {return a/b;}
};
return calc;
});
// square模块 square.js 依赖calc模块
define(['calc'], function(calc) {
function square (number){
return calc.multiply(number, nember);
};
return {square: square};
});
// 业务代码,计算平方,并将结果显示到网页
require(['square', 'jquery'], function(square, jq) {
jq('<a></a>').text(square(2)).appendTo(jq(document.body).first());
});
CMD 与 sea.js
基于AMD规范的Require.js对模块的依赖是前置的,通过参数的方式提前告诉Require.js要引入那些模块。
而基于CMD的sea.js则推崇依赖就近,不提前指定要依赖的模块,直接使用require()方法引入模块,在通过调用模块所在函数的toString()
获取代码的字符串,解析字符串取得要依赖的模块。
这个“依赖前置”与“依赖就近”是模块定义层面的,而不是代码执行层面的,二者都会在模块的业务代码执行前加载所需的依赖。
只不过AMD的依赖是提前指定的,执行业务代码的时候,直接引入依赖即可,这即是“依赖前置”。
而CMD的依赖是在业务代码中需要依赖的时候,就近指定的,在执行业务代码前再解析一次获取需要的依赖,这就是“依赖就近”。这意味着写在if/else里的依赖不会按照代码逻辑引入,而是全部被引入。
虽然sea.js的require()方法在功能上与Require.js的不同,但是sea.js也有与Require.js的require()
功能一致的方法,叫做require.async()
,使用方法与之一样,业务逻辑作为回调函数被异步执行,这也意味着require.async()
指定的依赖不会被解析,不会在业务代码执行前引入。
模块的导出方式与Require.js也不同,除了使用return关键字,还可以是给传入回调函数的参数对象指定属性名的方式,这种导出方式与基于CommonJS实现的Node模块是一样的。
// 模块calc calc.js
define(function(require, export, module) {
var calc = {
add: function(a,b) {return a+b;},
subtract: function(a,b) {return a-b;},
multiply: function(a,b) {return a*b;},
divide: function(a,b) {return a/b;}
};
return calc;
});
// square模块 square.js 依赖calc模块
define(function(require, export, module) {
export.square = function (number){
var calc = require('calc');
return calc.multiply(number, nember);
};
// 除了给export添加属性,还可以为module.export指定一个对象来导出模块
// 实际上export参数是module参数的export属性的一个引用,实际导出的还是module.export,所以直接给export赋值是错误的做法
// 导出必须是同步的,所以不能写在回调函数里
module.export = {
square: function (number){
var calc = require('calc');
return calc.multiply(number, nember);
},
};
});
// sea.js没有定义全局的require函数,使用use方法来在使用模块
seajs.use(['square', 'jq'], function(square) {
jq('<a></a>').text(square(2)).appendTo(jq(document.body).first());
});
Node.js 的 模块
Node使用CommonJS规范实现自己的模块功能。
它规定了每个模块是一个单独的文件,定义了一个全局的exports对象(module.exports)用于导出,定义了全局的require()函数用于导入。
// 模块calc calc.js
var calc = {
add: function(a,b) {return a+b;},
subtract: function(a,b) {return a-b;},
multiply: function(a,b) {return a*b;},
divide: function(a,b) {return a/b;}
};
exports.calc = calc;
// exports.ok = "OK"; // 导出更多值
// module.exports = {calc: calc} //只导出一个对象值
// square模块 square.js 依赖calc模块
let calc = require('./calc.js');
function square (number){
return calc.multiply(number, nember);
};
module.exports = {square};
// 业务代码,计算平方,并将结果打印
let square = require('./square.js');
console.log(square(2));
ES6 module 官方实现
概念
ES6的模块也规定一个模块定义在一个文件中,每个模块都有自己独立的上下文,模块中定义的变量的作用域只在其定义的模块文件,但模块内可以访问全局作用域内定义的变量,这与函数体内的行为一致。
ES6新增了import(default),export/from关键字用于导入与导出。导出的值可以是常量、变量、函数或类。
使用export可以导出一个或多个值,导出一个值,将其加在声明以前即可,导出多个值,将标识符包裹在花括号里以逗号分隔。
使用export default来导出一个值,称为默认导出。
默认导出可以导出字面量,而export不能,这可以简化模块的代码。
定义与导出值
// 模块calc calc.js
export const calc = {
add: function(a,b) {return a+b;},
subtract: function(a,b) {return a-b;},
multiply: function(a,b) {return a*b;},
divide: function(a,b) {return a/b;}
};
let a = 0, b = {}, c = function() {};
export {a,b,c}; // 导出三个值,分别是0,对象,函数
export default {a,b,c}; // 导出一个值,有三个属性
导入值
导入是可以选择性的导入需要的值,或将所有值导入并在哦为一个对象的属性值。
承接导入值的标识符是一个常量,不可修改。
import {calc} from './calc.js';
import {a,b,c} from './calc.js';
import * as obj from './calc.js'; // {a, b, c}
// 同时导入默认值与命名值
import defaultObj, {a,b,b} from './calc.js';
// 为导出值重命名
import {default as defaultObj, a1 as a, b, c} from './calc.js';
再导出
将一个模块的导出值导出,不需要先使用import导入,直接使用export/from即可。
export {calc} from './calc.js';
在网页中使用模块
为script标签的type属性指定'module'值即可将脚本文件以模块的方式解析与执行。
模块代码默认会在HTML文档解析完成后执行,就像给script标签指定了defer属性一样。
要改变这个默认行为,可以给模块的script标签指定async属性,这样代码将在加载完毕后立即执行,而不管HTML文档是否解析完成。
defer与async本身就是布尔值,添加就代表以true值生效,不需要指定属性值。
<script src="./calc.js" async></script>
import()动态导入
在ES2020,引入了import()
操作符用于异步地引入模块脚本,它接收一个可以可以转为字符串的值,返回一个Promise对象。
import()
看起来是一个函数调用,但是它实际上是一个操作符,浏览器的全局对象并没有定义一个import属性。
import('./calc.js').then(function() {});
async suare(n) {
let calc = await import('./calc.js');
return calc.multiply(n, n);
}
import.meta.url
获取模块代码文件的绝对路径。可以方便地访问同路径下的其他资源。