前端模块规范简介

举报
前端小盆友 发表于 2021/01/26 17:59:27 2021/01/26
【摘要】 模块化的好处:避免命名冲突(减少命名空间污染)更好的分离, 按需加载更高复用性高可维护性1.CommonJSNode.js 应用由模块组成,采用 CommonJS 模块规范。1.1 语法风格//Math.jsmodule.exports = { 'add': function(a, b) { return a + b; }}//main.jsconst Math = require('./...

模块化的好处:

  • 避免命名冲突(减少命名空间污染)

  • 更好的分离, 按需加载

  • 更高复用性

  • 高可维护性

1.CommonJS

Node.js 应用由模块组成,采用 CommonJS 模块规范。

1.1 语法风格

//Math.js
module.exports = {
	'add': function(a, b) {
		return a + b;
	}
}
//main.js
const Math = require('./Math');
console.log(Math.add(2, 3));
console.log('done');
-->node main
5
done

1.2 同步加载

由上面例子可以看出,CommonJS中模块是同步加载的

1.3 动态加载

//main.js
const Math = require('./Ma' + 'th');//动态拼接

CommonJS支持动态加载模块。因为conmonJs是同步的,执行到这一行时,才会加载模块。

1.4 浏览器不支持CommonJS规范。

浏览器不兼容CommonJS的根本原因,在于缺少四个Node.js环境的变量。

  • module

  • exports

  • require

  • global

可以使用工具进行转换,例如:Browserify

1.5 其他特点

  • 所有代码都运行在模块作用域,不会污染全局作用域。

  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。

  • 模块加载的顺序,按照其在代码中出现的顺序。

  • CommonJS模块的加载机制中,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。

  • 适合服务器端

小测验

//Math.js
let counter = 1;
let addCounter = function () {
    counter++;
};
module.exports = {
    counter,
    addCounter
};
console.log('Math init');


//main.js
const Math = require('./Math');

Math.addCounter();
console.log(Math.counter); // position A

Math.counter = 5;
const Math2 = require('./Math');
console.log(Math2.counter); // position B

//位置A 和 位置B的值分别是什么?

2.AMD

CommonJS是主要为了JS在后端的表现制定的,他是不适合前端的。

AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。

RequireJS实现了AMD规范。下面以RequireJS为例,了解一下AMD规范。

2.1 语法风格

//Math.js
define([], function(){
    return {
        'add': function(a, b) {
			return a + b;
		}
    }
})
//main.js
require.config({
    paths : {
        "math" : "Math"
    }
});
require(['math'], function (math) {
	console.log(math.add(2, 3));
});
console.log('done');
//done
//5

2.2 异步加载

由上面的例子可以看出,require命令是异步执行的

2.3 动态加载

2.4 依赖前置,提前执行

3.CMD(Common Module Definition)

CMD是SeaJS 在推广过程中对模块定义的规范化产出。

CMD规范和 AMD 很相似,尽量保持简单,并与 CommonJS 规范保持了很大的兼容性。

3.1 语法风格

// CMD
define(function(require, exports, module) {
    var a = require('./a');
    a.doSomething();
    //...
    var b = require('./b');   // 依赖可以就近书写
    b.doSomething();
    // ... 
    require.async('./c',function(c){ //支持异步加载
        c.doSomething();
    });
})
// AMD 默认推荐的是
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
    a.doSomething();
    //...
    b.doSomething();
    //...
})

3.2 AMD和CMD的区别

1) 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible(尽可能的懒加载,也称为延迟加载,即在需要的时候才加载)。

2) CMD 推崇依赖就近,AMD 推崇依赖前置。虽然 AMD 也支持 CMD 的写法,同时还支持将 require 作为依赖项传递,但 RequireJS 的作者默认是最喜欢依赖前置的写法,也是官方文档里默认的模块定义写法。

3.3 推荐链接

与 RequireJS 的异同

二、Module

ES中Module的特点

  • 浏览器,服务器通用

  • 静态加载

1. 基本语法

1.1 export

一个模块就是一个独立的文件。export关键字用来输出模块内部的某个变量 。可以输出变量,函数或类。

// test.js
export var name = 'zhangsan';
export var age = 18;

// test.js
var name = 'zhangsan';
var age = 18;

export { name, age };

可以使用as为输出变量重命名。

var name = 'zhangsan';
var age = 18;

export {
	name as personName, 
    age
};

需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

// 报错
export 1;

// 报错
var m = 1;
export m;

export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。

//m.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 1000);
//main.js
import { foo } from './m1.js';

console.log(1, foo);

setTimeout(function () {
    console.log(2, foo);
}, 1000);
<!DOCTYPE html>
<html>
    <script type="module" src='./main.js'></script>
    <body></body>
</html>
//console
1 "bar"
2 "baz"

export命令可以出现在模块的任何位置,只要处于模块顶层就可以。

1.2 import

其他 JS 文件通过import命令加载模块。大括号里面的变量名,必须与被导入模块(test.js)对外接口的名称相同。

// main.js
import { name, age } from './test.js';

function showName() {
  console.log(name);
}
//zhangsan

import命令输入的变量都是只读的,因为它的本质是输入接口。

import {a} from './xxx.js'

a = {}; // Assignment to constant variable.

如果a是一个对象,改写a的属性是允许的。和const一样。

import {a} from './xxx.js'

a.foo = 'hello'; // 合法操作

可以用星号(*)指定一个对象,进行整体加载。

// main.js
import * as test from './test.js';

function showName() {
  console.log(test.name);
}
//zhangsan

test.name = 'yun';
//Cannot assign to read only property 'name' of object '[object Module]'
//但如果是对象,可以修改对象的属性。

import命令具有提升效果,会提升到整个模块的头部,首先执行。

foo();

import { foo } from 'my_module';

由于import是静态执行,所以不能使用表达式和变量

// 报错
import { 'f' + 'oo' } from 'my_module';

import语句会执行所加载的模块,因此可以有下面的写法。仅仅执行lodash模块,但是不输入任何值。

import 'lodash';

即使加载多次,也只会执行一次。也就是说,import语句是 Singleton 模式

1.3 单例模式解读

//counter.js
export let counter = 1;
export function addCounter(){
    counter++;
}
console.log('counter init');
//main.js
import {counter, addCounter} from './counter';
console.log('main:' + counter);
addCounter();
console.log('main:' + counter);

//counter init
//main:1
//main:2

//从这例子中可以看出和CommonJS的区别

另一个例子

//main2.js
import {counter, addCounter} from './counter';
console.log('main2:' + counter);
addCounter();
console.log('main2:' + counter);
<!DOCTYPE html>
<html>
	<head>
		<title>module test</title>
		<script type="module" src='main.js'></script>
		<script type="module" src='main2.js'></script>
	</head>
</html>
//console
counter init
main:1
main:2
main2:2
main2:3

从上面这个例子中可以看出,即使加载多次,也只会执行一次。并且是 Singleton 模式

1.4 export default

使用export default可以不用关注输出模块中的变量名。

// export-default.js
export default function () {
  console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'

import命令可以为该匿名函数指定任意名字。import命令后面,不使用大括号。

// export-default.js
function foo() {
  console.log('foo');
}

export default foo;

export default的本质,就是输出一个叫做default的变量或方法。

imort something from ...的本质,就是import {default as something} from ...

// 正确
export var a = 1;

// 正确
var a = 1;
export default a;

// 错误
export default var a = 1;

// 正确
export default 42;

// 报错
export 42;

所以export default是比较常用的方法:

// MyClass.js
export default class { ... }

// main.js
import MyClass from 'MyClass';
let o = new MyClass();

1.5 import&export混合使用

export { foo, bar } from 'my_module';

// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };

上面代码中,exportimport语句可以结合在一起,写成一行。但需要注意的是,写成一行以后,foobar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foobar

// 接口改名
export { fooName as newName } from 'my_module';

//具名接口改为默认接口
export { foo as default } from './someModule';

//接口也可以改名为具名接口
export { default as ES } from './someModule';

1.6 模块的继承

//calculator.js
export function add(a, b) {
  return a + b;
}
//calculatorPlus.js
export * from './calculator.js';
export function multiply(a, b) {
  return a * b;
}
//main.js
import * as cal from './calculatorPlus.js';

cal.add(2, 3);//5
cal.multiply(2, 3);//6

1.7 import()

ES2020 (ES11)新增了 动态import特性,解决了import动态加载,和不能写在代码块中的问题。

import()返回一个 Promise 对象 。

import(a + '.js')
.then(...);
      
import(f())
.then(...);
if (condition) {
  import('moduleA').then(...);
} else {
  import('moduleB').then(...);
}

2. Module补充

2.1 ES模块和CommonJS模块的差异

  1. importexport是关键字,require不是。

  2. CommonJS 模块输出的是一个值的拷贝,ES 模块输出的是值的引用。(详情见上方【单例模式解读】)

  3. CommonJS 模块是运行时加载,ES 模块是编译时输出接口。 因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

  4. this指向不同。ES 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块,这是两者的一个重大差异。

2.2 循环加载

CommonJS中的循环加载

// a.js
let bar = require('./b').bar;
console.log('bar:', bar);
exports.ada = 'ada';
// b.js
let ada = require('./a').ada;
exports.bar = 'bar';

setTimeout(function () {
    console.log('ada:', ada);
}, 0);

执行结果:

-->node a
bar: bar
ada: undefined

ES Module中的循环加载

// a.js
import {bar} from './b.js';

console.log('a.js', bar);

export let ada = 'ada';
// b.js
import {ada} from './a.js';

//console.log(ada); 
//此处访问ada会报错
setTimeout(function () {
    console.log('b.js', ada);
}, 0);

export let bar = 'bar';
<!DOCTYPE html>
<html>
<head>
    <title>module test</title>
    <script type="module" src='a.js'></script>
</head>
</html>

执行结果:

//console
a.js bar
b.js ada

解读:

因为CommonJS 模块输出的是一个值的拷贝,当拷贝的动作发生时,ada值为undefined,之后模块a中对ada进行赋值已经不会影响到模块b中保存的ada。

ES 模块输出的是值的引用。所以模块a对ada进行赋值,会影响b中打印的结果。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。