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. Linux系统诊断-内存基础

    简介: Linux系统诊断-内存基础 1. 背景 谈及linux内存,很多时候,我们会关注free,top等基础命令.当系统遇到异常情况时,内存问题的根因追溯,现场诊断时,缺乏深层次的debug能力. ...

  2. 如何定位并修复 HttpCore5 中的 HTTP2 流量控制问题

    ​简介:开篇吹一波阿里云性能测试服务 PTS,PTS 在 2021 年 5 月份已经上线了对 HTTP2 协议的支持(底层依赖 httpclient5),在压测时会通过与服务端协商的结果来决定使用 H ...

  3. 走完线上 BUG 定位最后一公里

    简介: 因为线上线下环境隔离的问题,线上的输入很多时候难以在日常环境中构造,定位 bug 效率低下.是否有简单快捷的办法呢? 一个小故事 周末12点的闹钟在回龙观均价3000的出租屋急促的响起,程序员 ...

  4. EMR on ACK 全新发布,助力企业高效构建大数据平台

    ​简介: 阿里云 EMR on ACK 为用户提供了全新的构建大数据平台的方式,用户可以将开源大数据服务部署在阿里云容器服务(ACK)上.利用 ACK 在服务部署和对高性能可伸缩的容器应用管理的能力优 ...

  5. 尝试 IIncrementalGenerator 进行增量 Source Generator 生成代码

    在加上热重载时,源代码生成 Source Generator 的默认行为会让 Visual Studio 有些为难,其原因是热重载会变更代码,变更代码触发代码生成器更新代码,代码生成器更新的代码说不定 ...

  6. S3-FIFO

    S3-FIFO 本文作为下一篇缓存文章的预备知识. 背景 基于LRU和FIFO的驱逐 FIFO和LRU都是经典的缓存驱逐算法,在过去几十年中也出现了很多追求更高效率的驱逐算法,如ARC, 2Q, LI ...

  7. 微软开源 MS-DOS「GitHub 热点速览」

    上周又是被「大模型」霸榜的一周,各种 AI.LLM.ChatGPT.Sora.RAG 的开源项目在 GitHub 上"争相斗艳".这不 Meta 刚开源 Llama 3 没几天,苹 ...

  8. webstorm上的Element提示插件

    https://zhuanlan.zhihu.com/p/34735529 首先说明下,这个插件是用idea开发的,在idea上同样适用.有兴趣的朋友可以直接git下来开发自己的小插件. 插件基于的w ...

  9. C#.Net筑基-基础知识

    01.C#基础概念 1.1.C#简介 C# (读作C Sharp)是由微软公司开发的一种面向对象.类型安全.高效且简单的编程语言,最初于 2000 年发布,并随后成为 .NET 框架的一部分.所以学习 ...

  10. linux用户与用户组管理

    linux用户与用户组管理 目录 linux用户与用户组管理 1.linux用户管理 1.1 用户基础 1.2 /etc/passwd:用户信息文件 1.3 /etc/shadow:用户密码信息文件 ...