JavaScript模块化笔记

一个模块就是一堆被封装到一个文件当中的代码,并使用export暴露部分代码给其他的文件。模块专注于一小部分功能并与应用的其他部分松耦合,这是因为模块间没有全局变量或共享变量,他们仅通过暴露的模块代码的一部分来进行通信。任何你想在另一个文件中访问的代码都可以被封装为模块。

模块化历史

没有模块的时代

JavaScript刚出现时就是一个从上到下执行的脚本语言,简单的逻辑可以编写在一整个文件里,没有分块需求

模块化萌芽时代

Ajax的提出让前端变成了集许多功能为一身的类客户端,前端业务逻辑越来越复杂,代码越来越多,此时有许多问题

  1. 所有变量都定义在一个作用域,造成变量污染
  2. 没有命名空间,导致函数命名冲突
  3. HTML引入JavaScript时需要注意顺序依赖,多文件不好协调

此时的一些解决方案

  1. 用自执行函数来包装代码,var将变量声明在局部作用域。但是还是会生成modA全局变量

    modA = function(){
    var a = 2, b = 3; //变量a、b外部不可见
    return {
    add : function(){
    console.log(a, b);
    }
    }
    }()
  2. 为了避免全局变量冲突的Java包命名风格,麻烦复杂而且还是挂载在全局变量上

    app.util.modA = xxx;
    app.tools.modeA = xxx;
  3. IIFE匿名自执行函数,将函数内容放在括号中防止其内部变量泄露,函数接受window并将其需要对外放开的功能挂载在全局变量上

    (function(window) {
    // ...
    window.jQuery = window.$ = jQuery;
    })(window);

模块化需要解决的问题

  1. 如何安全的不污染模块外代码的方式包装一个模块的代码
  2. 如何标识唯一的模块从而能被外部轻易调用
  3. 如何既不增加全局变量也能把模块API暴露出去
  4. 如何在其他模块内方便的引入所依赖的模块

模块化

CommonJS

CommonJS的模块定义如下

  1. 模块的标识符

    • 使用/分割的由词组成的字符串
    • 词必须是驼峰格式可以使用...
    • 模块标识符不能添加文件的扩展名例如.js
    • 模块标识符可以是相对路径或顶层标识,相对的标识符使用...开头
    • 顶层标识符相对于在虚拟模块命名空间根上解析
    • 相对标识符相对于调用该相对标识符的模块位置解析
  2. 模块上下文
    • 在模块中有一个require函数,该函数接受一个模块标识符,返回被require的依赖模块中被export暴露的API,如果依赖中有依赖则依次加载这些依赖;如果被请求的模块不能被返回,require函数会抛出一个异常
    • 在模块中有一个exports对象变量,模块需要在执行过程中向其添加需要被暴露的API
    • 模块执行使用exports执行导出

CommonJS的例子

// 定义模块math.js
var basicNum = 0;
function add(a, b) {
return a + b;
}
module.exports = { //在这里写上需要向外暴露的函数、变量
add: add,
basicNum: basicNum
} // 引用自定义的模块时,参数包含路径,可省略.js
var math = require('./math');
math.add(2, 5); // 引用核心模块时,不需要带路径
var http = require('http');
http.createService(...).listen(3000);

CommonJS是运行时动态同步加载模块,模块被加载为对象,而浏览器如果在运行时加载需要单独下载模块文件开销很大,所以一般被用在服务器这种本地环境中例如Nodejs

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

AMD

AMD(Asynchronous Module Definition),使用异步方式加载模块,模块加载不影响后面语句的执行。Require.js实现了AMD规范。AMD使用require.config()执行路径等配置、define()定义模块,require()加载模块。

AMD推崇依赖前置(definerequire函数直接传入依赖ID,依赖将进入factory中作为参数)、提前执行(直接依赖前置时加载的模块将会被先执行一次,除非使用后置require的方法,实际这个问题经过实验已被解决,所有模块将不会被提前执行一遍)

AMD的规范源于CommonJS所以其中的定义与CommonJS有许多相似之处

  • 使用define()函数用来定义模块,函数接受三个参数

    • id类似CommonJS的模块标识符,可选
    • dependencies依赖的模块ID数组,可选,依赖会先于后面介绍的工厂函数执行,依赖获取结果也会参数形式传入工厂参数,默认为["require", "exports", "module"]
    • factory工厂参数,用来实例化一个模块或对象,如果工厂是一个函数则会被执行一次返回值作为模块对外暴露的值,如果工厂是一个对象,那么对象将会被作为工厂对外暴露的值。暴露值的方法有三种:returnexports.xxx=xxxmodule.exports=xxx
  • Require.js中的require()引用函数,函数接受两个参数
    • dependencies依赖的模块ID数组,如define()中的dependencies差不多
    • function利用模块或直接执行的代码方法,前面的dependencies会被传入该方程中

Require.js的例子

// foo/title.js 默认的名称就会为title
// id默认为title.js当前目录下查找
define(["./cart", "./inventory"], function(cart, inventory) {
//return an object to define the "my/shirt" module.
return {
color: "blue",
size: "large",
addToCart: function() {
inventory.decrement(this);
cart.add(this);
}
}
}
); require("title.js", function(title) {
console.log(title.color);
}); // 可以在define中require,但是要把define添加到依赖中
define(["require"], function(require) {
var mod = require("./relative/name");
});

但是AMD有其自身问题

  • 模块代码在被定义时会被执行,不符合预期且开销较大

  • 罗列依赖模块导致definerequire的参数长度过长

    这一点可以通过在define中使用require解决,当使用这种编写模式时只有在特别调用require的时候才下载该模块的代码

    define(function(){
    console.log('main2.js执行'); require(['a'], function(a){
    a.hello();
    }); $('#b').click(function(){
    // 只有在用户点击该按钮后才会下载
    require(['b'], function(b){
    b.hello();
    });
    });
    });

    AMD还部分兼容Modules/Wrappings写法

    // d.js factory的形参得写上
    define(function(require, exports, module){
    console.log('d.js执行');
    return {
    helloA: function(){
    var a = require('a');
    a.hello();
    },
    run: function(){
    $('#b').click(function(){
    var b = require('b');
    b.hello();
    });
    }
    }
    });

CMD

CMD(Common Module Definition)是淘宝前端根据Modules/Wrappings规范结合了各家所长,支持CommonJS的exports和module.exports语法,支持AMD的return的写法,暴露的API可以是任意类型的。

CMD推崇依赖就近(require在调用依赖紧前调用)、延迟执行(不会在刚下载完成就执行,而是等待用户调用)

//a.js
define(function(require, exports, module){
console.log('a.js执行');
return {
hello: function(){
console.log('hello, a.js');
}
}
}); //b.js
define(function(require, exports, module){
console.log('b.js执行');
return {
hello: function(){
console.log('hello, b.js');
}
}
}); //main.js
define(function(require, exports, module){
console.log('main.js执行');
var a = require('a');
a.hello();
$('#b').click(function(){
var b = require('b');
b.hello();
});
});

sea.js实现了CMD标准,它通过对函数toString()并正则匹配到require语句来分析依赖,所有依赖将会被预先下载并延迟执行。如果想延迟下载可以使用require.asyncAPI。

AMD对比CMD

/** AMD写法 amd.js **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
// 等于在最前面声明并初始化了要用到的所有模块
a.doSomething();
if (false) {
b.doSomething()
}
}); require(["amd.js"]) /** CMD写法 **/
define(function(require, exports, module) {
var a = require('./a'); //在需要时申明
a.doSomething();
if (false) {
var b = require('./b');
b.doSomething();
}
}); /** sea.js **/
// 定义模块 math.js
define(function(require, exports, module) {
var $ = require('jquery.js');
var add = function(a,b){
return a+b;
}
exports.add = add;
});
// 加载模块
seajs.use(['math.js'], function(math){
var sum = math.add(1+2);
});

其实现在的情况是:

  • AMD的require.js如果使用dependencies中定义好了之后会在初始化阶段获取并初始化所有依赖,即使依赖在后续并未被调用

    // 内联JavaScript的调用
    require(["./main"]); // main.js
    define(["./a", "./b"], function(a, b) {
    if (false) {
    a.hello(); // 即使不使用
    b.hello()
    }
    }); // a.js
    define(function() {
    console.log("a init");
    return {
    hello: function() {
    console.log("a.hello executed");
    }
    }
    }); // b.js
    define(function() {
    console.log("b init");
    return {
    hello: function() {
    console.log("b.hello executed");
    }
    }
    }); // 刚打开页面上述代码执行结果
    // a init b init 看Network三个文件都被下载

    如果使用require,则文件将会在require之后被加载,如果require未被执行则不下载,下方代码中b.js将会在按钮按下下载与执行

    // 上方main.js更改为
    define(function(){
    console.log('son.js executed'); require(['a'], function(a){
    a.hello();
    });
    document.getElementById("btn").addEventListener("click", function(){
    require(['b'], function(b){
    b.hello();
    });
    });
    });
  • CMD直接使用require,且无论require是否执行都会下载代码内require()依赖的代码,这是因为其正则匹配方式,使用require.async延迟下载

ES6 Module

ES6 Module自动使用严格模式,主要有以下限制

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀0表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval不会在它的外层作用域引入变量
  • evalarguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.callerfn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protectedstaticinterface

ES6模块化主要由exportimport组成,export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能,一个模块是一个独立的文件。importexport必须处在模块顶层用于静态优化,不能在动态运行的代码块中

export命令

export导出命令规定导出对外的接口,不能直接输出值,所以export 1是错误的

// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958; var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year}; // 默认导出是其本身的名字
export function multiply(x, y) {
return x * y;
}; function v1() { ... }
function v2() { ... }
// 可以使用as对导出内容重命名
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};

export default的其他用法

export default 42;	// 默认值
export default class { ... }

import命令

// main.js 如果非export default需要大括号内变量名需要与模块的对外接口名一致
import {firstName, lastName, year} from './profile'; function setName(element) {
element.textContent = firstName + ' ' + lastName;
} // 起别名依旧使用as
import { lastName as surname } from './profile';

静态执行的import不能与任何动态代码进行组合使用。多次重复的import不会重复执行

// 第一组
export default function crc32() { // 输出
}
import crc32 from 'crc32'; // 输入 // 第二组
export function crc32() { // 输出
};
import {crc32} from 'crc32'; // 输入

使用export default默认导出时可以不使用大括号因为导出项只可能有一个,如果想同时输入默认方法和其他变量可以写成下面的样式

import _, { each } from 'lodash';

可以使用*做整体加载

// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
} // main.js
import * as circle from './circle'; console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

export import复合写法

export { foo, bar } from 'my_module';

// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar }; // 接口改名
export { foo as myFoo } from 'my_module'; // 整体输出
export * from 'my_module'; // 默认接口
export { default } from 'foo'; // 有名字的改成默认接口
export { es6 as default } from './someModule';
// 等同于
import { es6 } from './someModule';
export default es6;

模块的继承

假设circleplus模块继承了circle模块。export *会默认忽略circle模块的default方法,然后子模块复写了其default方法

// circleplus.js
export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
return Math.exp(x);
} // 调用circleplus.js
import * as math from 'circleplus';
import exp from 'circleplus';
console.log(exp(math.e));

import()

ES6的模块化实现是编译时加载(静态加载)、模块输出值引用的方式。CommonJS中模块引用是值的拷贝,导致修改分别导出的内容两者虽然可能在子依赖中有关联,但是在父模块中不会表现,也就是输出之后模块本身改变不了已经导出给其他模块的值

// module.js
var data = 5;
var doSomething = function () {
data++;
};
// 暴露的接口
module.exports.data = data;
module.exports.doSomething = doSomething;
var example = require('./module.js');
console.log(example.data); // 5
example.doSomething();
console.log(example.data); // 5

如果暴露一个getter函数就可以正确取到了

var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
get counter() {
return counter
},
incCounter: incCounter,
};

而在ES6 Module中是值的只读引用,模块内值父模块和模块本身都可以访问且修改

// 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

ES6 Module的引用可以添加值但是不可以重新赋值,因为导入的其实是一个只读的对象的地址,对象的内容可以修改但是其本身指向不能变

// lib.js
export let obj = {}; // main.js
import { obj } from './lib'; obj.prop = 123; // OK
obj = {}; // TypeError

但是为了实现运行时动态加载,可以使用ES2020提案中引入的import()函数,该函数支持动态加载模块,其接受一个与import命令相似的参数,函数返回一个Promise对象,import是异步加载,而Node的require是同步加载

const main = document.querySelector('main');

import(`./section-modules/${someVariable}.js`)
.then(module => {
module.loadPageInto(main);
})
.catch(err => {
main.textContent = err.message;
});

使用import()的场景:

  1. 按需加载模块

    button.addEventListener('click', event => {
    import('./dialogBox.js')
    .then(dialogBox => {
    dialogBox.open();
    })
    .catch(error => {
    /* Error handling */
    })
    });
  2. 条件加载

    if (condition) {
    import('moduleA').then(...);
    } else {
    import('moduleB').then(...);
    }
  3. 动态模块路径生成,和上方字面量用法相似

    import(f())
    .then(...);

import加载成功以后,模块会作为一个对象当作then方法的参数,可以使用对象结构语法获得输出接口

import('./myModule.js')
.then(({export1, export2}) => {
// ...·
}); // default接口可以直接用参数获得
import('./myModule.js')
.then(myModule => {
console.log(myModule.default);
}); // 具名输入
import('./myModule.js')
.then(({default: theDefault}) => {
console.log(theDefault);
});

多个同时加载

Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
])
.then(([module1, module2, module3]) => {
···
});

可以用在async await函数中

async function main() {
const myModule = await import('./myModule.js');
const {export1, export2} = await import('./myModule.js');
const [module1, module2, module3] =
await Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
]);
}
main();

JavaScript模块化笔记的更多相关文章

  1. 看完我的笔记不懂也会懂----javascript模块化

    JavaScript模块化 模块化引子 模块化的历史进化 模块化规范 CommonJS规范 Node.js(服务器端) 下项目的结构分析 browerify(浏览器端) 下项目的结构分析 AMD规范 ...

  2. javascript模块化编程库require.js的用法

    随着javascript的兴起,越来越多的公司开始将JS模块化,以增加开发的效率和减少重复编写代码的.更是为了能更加容易的维护日后的代码,因为现在的随着人们对交互效果的越来越强烈的需求,我们的JS代码 ...

  3. Javascript模块化编程(三):require.js的用法

    Javascript模块化编程(三):require.js的用法 原文地址:http://www.ruanyifeng.com/blog/2012/11/require_js.html 作者: 阮一峰 ...

  4. Javascript模块化编程(二):AMD规范

    Javascript模块化编程(二):AMD规范   作者: 阮一峰 原文地址:http://www.ruanyifeng.com/blog/2012/10/asynchronous_module_d ...

  5. Javascript模块化编程(一):模块的写法

    Javascript模块化编程(一):模块的写法 作者: 阮一峰 原文链接:http://www.ruanyifeng.com/blog/2012/10/javascript_module.html ...

  6. Javascript模块化编程(二):AMD规范(转)

    这个系列的第一部分介绍了Javascript模块的基本写法,今天介绍如何规范地使用模块. (接上文) 七.模块的规范 先想一想,为什么模块很重要? 因为有了模块,我们就可以更方便地使用别人的代码,想要 ...

  7. Javascript模块化编程(一):模块的写法(转)

    随着网站逐渐变成"互联网应用程序",嵌入网页的Javascript代码越来越庞大,越来越复杂. 网页越来越像桌面程序,需要一个团队分工协作.进度管理.单元测试等等......开发者 ...

  8. Javascript模块化规范

    Javascript模块化规范 一.前端js模块化由来与演变 CommonJS 原来叫 ServerJS,推出 Modules/1.0 规范后,在 Node.js 等环境下取得了很不错的实践.09年下 ...

  9. Javascript模块化开发,使用模块化脚本加载工具RequireJS,提高你代码的速度和质量。

    随着前端JavaScript代码越来越重,如何组织JavaScript代码变得非常重要,好的组织方式,可以让别人和自己很好的理解代码,也便于维护和测试.模块化是一种非常好的代码组织方式,本文试着对Ja ...

  10. Javascript 模块化开发上线解决方案

    最近又换部门了,好频繁地说...于是把这段时间搞的小工具们简单整理了一下,作了一个小的总结.这次用一个简单业务demo来向大家介绍一下Javascript模块化开发的方式和自动化合并压缩的一些自己的处 ...

随机推荐

  1. 深度解读《深度探索C++对象模型》之C++虚函数实现分析(一)

    接下来我将持续更新"深度解读<深度探索C++对象模型>"系列,敬请期待,欢迎关注!也可以关注公众号:iShare爱分享,自动获得推文和全部的文章列表. 假如有这样的一段 ...

  2. 5分钟搞定Loki告警多渠道接入

    ​简介: Loki是受Prometheus启发的水平可扩展.高可用.多租户日志聚合系统.用户既可以将Loki告警直接接入SLS开放告警,也可以先将Loki接入Grafana或Alert Manager ...

  3. Nats集群部署

    环境: 3台机器采用同样的目录名字和文件名称 服务器 192.168.10.30 192.168.10.31 192.168.10.32 nats版本2.9.15 配置文件 # 192.168.10. ...

  4. vue-cli快速搭建项目的几个文件(三)

    ==========有加载动画的app.vue=========== <template>   <div id="app">     <keep-al ...

  5. All in One:Prometheus 多实例数据统一管理最佳实践

    01 引言 Prometheus 作为目前最主流的可观测开源项目之一,已经成为云原生监控的事实标准,被众多企业广泛应用.在使用 Prometheus 的时候,我们经常会遇到全局视图的需求,但是数据确分 ...

  6. 07 mapping索引操作

    目录 版本 获取健康值 获取所有索引的信息 创建索引 1. 自动推导 2. 自定义 判断索引是否存在 新增字段 新增记录 查询记录 获取mapping信息 删除索引 添加别名 查看别名 索引备份数据 ...

  7. Java 获取两个时间的时间差

    前言 在平时的工作中,难免会遇到获取两个时间相差多少天.小时.分钟.秒.毫秒,现在我将自己获取的方法总结如下: 一.导入需要的依赖 <dependency> <groupId> ...

  8. 关于浏览器sessionStorage共享问题

    今天也是闲暇之余看了点前端基础知识发现了好玩的 window.open("同源页面")这种方式新开的页面会复制之前的sessionStorage 通过a标签新开的页面同样也会,原理 ...

  9. 使用c#强大的表达式树实现对象的深克隆

    一.表达式树的基本概念 表达式树是一个以树状结构表示的表达式,其中每个节点都代表表达式的一部分.例如,一个算术表达式 a + b 可以被表示为一个树,其中根节点是加法运算符,它的两个子节点分别是 a ...

  10. 计算机组成原理—中央处理器CPU

    文章目录 CPU的功能与架构 CPU的组成 运算器 控制器 指令执行过程 指令流程 指令执行方案 数据通路 单总线结构 专用通路结构 硬布线控制器设计 硬布线执行流程 硬布线CU内部 怎么设计微操作的 ...