一文彻底搞懂JS前端5大模块化规范及其区别
码文不易,转载请带上本文链接,感谢~ https://www.cnblogs.com/echoyya/p/14577243.html
在开发以及面试中,总是会遇到有关模块化相关的问题,始终不是很明白,不得要领,例如以下问题,回答起来也是模棱两可,希望通过这篇文章,能够让大家了解十之一二,首先抛出问题:
- 导出模块时使用
module.exports/exports
或者export/export default
; - 有时加载一个模块会使用
require
奇怪的是也可以使用import
??它们之间有何区别呢?
于是有了菜鸟解惑的搜喽过程。。。。。。
模块化规范:即为 JavaScript 提供一种模块编写、模块依赖和模块运行的方案。
Script 标签
其实最原始的 JavaScript 文件加载方式,就是Script 标签,如果把每一个文件看做是一个模块,那么他们的接口通常是暴露在全局作用域下,也就是定义在 window 对象中,不同模块的接口调用都是一个作用域中,一些复杂的框架,会使用命名空间的概念来组织这些模块的接口。
缺点:
- 污染全局作用域
- 开发人员必须主观解决模块和代码库的依赖关系
- 文件只能按照script标签的书写顺序进行加载
- 在大型项目中各种资源难以管理,长期积累的问题导致代码库混乱不堪
默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>
标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。
如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞,用户会感觉到浏览器“卡死”了,没有任何响应。这显然是很不好的体验,所以浏览器允许脚本异步加载
。
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
<script>
标签添加defer
或async
属性,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。
defer
:要等到整个页面在内存中正常渲染结束,才会执行;多个脚本时,按顺序执行
async
:一旦下载完,渲染引擎就会中断渲染,执行这个脚本再继续渲染。多个脚本时,不能保证按执行顺序
总结一句话:defer
是“渲染完再执行”,async
是“下载完就执行”。
CommonJS规范(同步加载模块)
- Node.js 是目前 CommonJS 规范最热门的一个实现
- 通过
require
方法同步加载所依赖的模块,通过exports
或module.exports
导出需要暴露的数据。 - CommonJS 规范包括了模块(modules)、包(packages)、系统(system)、二进制(binary)、控制台(console)、编码(encodings)、文件系统(filesystems)、套接字(sockets)、单元测试(unit testing)等部分。
创建模块
在 Node.js 中,创建一个模块非常简单,一个文件就是一个模块
// module.js 模块
var name = "Echoyya";
// todo something...
exports.name = name
加载模块
使用require函数 加载模块(即被依赖模块的 module.exports对象)。
- 按路径加载模块
- 通过查找 node_modules 目录加载模块
- 加载缓存:Node.js 是根据实际文件名缓存,而不是 require() 提供的参数缓存的,如
require('express')
和require('./node_modules/express')
加载两次,也不会重复加载,尽管两次参数不同,解析到的文件却是同一个。 - 核心模块拥有最高的加载优先级,换言之如果有模块与其命名冲突,Node.js 总是会加载核心模块。
- 更多关于require函数的用法和特点,博主此前另外总结过一篇博文,NodeJs 入门到放弃 — 入门基本介绍(一)
导出模块
exports.属性 = 值
exports.方法 = 函数
- Node.js 为每个模块提供一个 exports 变量,指向 module.exports。相对于在每个模块头部,有一行这样的命令:
var exports = module.exports;
- exports对象 和 module.exports对象,指同一个内存空间, module.exports对象才是真正的暴露对象
- 而
exports对象 是 module.exports对象的引用
,不能改变指向,只能添加属性和方法,若直接改变exports 的指向,等于切断了 exports 与 module.exports 的联系,返回空对象 - console.log(module.exports === exports); // true
- 更多关于exports函数的用法和特点,博主此前另外总结过一篇博文,NodeJs 入门到放弃 — 入门基本介绍(一)
另外的用法:
// singleobjct.js
function Hello() {
var name;
this.setName = function (thyName) {
name = thyName;
};
this.sayHello = function () {
console.log('Hello ' + name);
};
}
exports.Hello = Hello;
此时获取 Hello 对象require('./singleobject').Hello
,略显冗余,可以用下面方法简化。
// hello.js
function Hello() {
var name;
this.setName = function(thyName) {
name = thyName;
};
this.sayHello = function() {
console.log('Hello ' + name);
};
}
module.exports = Hello;
就可以直接获得这个对象:
// gethello.js
var Hello = require('./hello');
hello = new Hello();
hello.setName('Yu');
hello.sayHello();
CommonJS 特点
- 同步加载方式,适用于服务端,因为模块都放在服务器端,对于服务端来说模块加载较快,不适合在浏览器环境中使用,因为同步意味着阻塞加载。
- 所有代码都运行在模块作用域,不会污染全局作用域。
- 模块可以多次加载,但只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。
- 模块加载的顺序,按照其在代码中出现的顺序。
AMD(Asynchronous Module Definition)
采用异步方式
加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。推崇依赖前置
require.js 是目前 AMD 规范最热门的一个实现
AMD 也采用 require
语句加载模块,但是不同于 CommonJS,它要求两个参数:require([module], callback);
[module]:是一个数组,成员就是要加载的模块
callback:加载成功之后的回调函数;
require(['math'], function (math) {
math.add(2, 3);
});
创建模块
模块必须采用 define()
函数来定义。
- 若一个模块不依赖其他模块。可以直接定义在 define() 函数中。
// math.js
define(function (){
var add = function (x,y){
return x+y;
};
return {
add: add
};
});
- 若这个模块还依赖其他模块,那么 define() 函数的第一个参数,必须是一个数组,指明该模块的依赖性。当 require() 函数加载test模块时,就会先加载 myLib.js 模块。
// test.js
define(['myLib'], function(myLib){
function foo(){
myLib.doSomething();
}
return {
foo : foo
};
});
加载规范模块
// main.js
require(['math'], function (math){
alert(math.add(1,1));
});
加载非规范的模块
理论上require.js加载的模块,必须是按照 AMD 规范用 define() 函数定义的模块。但实际上,虽然已经有一部分流行的函数库(比如 jQuery )符合 AMD 规范,更多的库并不符合。那么require.js 如何能够加载非规范的模块呢?
这样的模块在用 require() 加载之前,要先用 require.config()方法,定义它们的一些特征。
例如,underscore 和 backbone 这两个库,都没有采用 AMD 规范编写。如果要加载的话,必须先定义它们的特征。
require.config({
shim: {
'underscore': {
exports: '_'
},
'backbone': {
deps: ['underscore', 'jquery'],
exports: 'Backbone'
}
}
});
require.config() 接受一个配置对象,这个对象有一个 shim
属性,专门用来配置不兼容的模块。每个模块要定义:
exports :输出的变量名,表示这个模块外部调用时的名称;
deps: 数组,表示该模块的依赖性。
如jQuery 的插件还可以这样定义:
shim: {
'jquery.scroll': {
deps: ['jquery'],
exports: 'jQuery.fn.scroll'
}
}
AMD特点
- AMD允许输出的模块兼容CommonJS
- 异步并行加载,不阻塞 DOM 渲染。
推崇依赖前置
,也就是提前执行(预执行),在模块使用之前就已经执行完毕。
CMD(Common Module Definition)
- CMD 是通用模块加载,要解决的问题与 AMD 一样,只不过是对依赖模块的执行时机不同 ,
推崇就近依赖
。 - sea.js 是 CMD 规范的一个实现代表库
- 定义模块使用全局函数define,接收一个 factory 参数,可以是一个函数,也可以是一个对象或字符串;
factory 是函数时有三个参数,function(require, exports, module):
require
:函数用来获取其他模块提供的接口require(模块标识ID)exports
: 对象用来向外提供模块接口module
:对象,存储了与当前模块相关联的属性和方法
// 定义 a.js 模块
define(function(require, exports, module) { var $ = require('jquery.js') exports.price= 200;
}); // b.js 加载模块
const a = require('./a.js')
factory 为对象、字符串时,表示模块的接口就是该对象、字符串。比如可以定义一个 JSON 数据模块:
define({"foo": "bar"});
通过字符串模板定义模块:
define('I am a template.My name is {{name}}.');
AMD 与 CMD 的区别
AMD 是提前执行,CMD 是延迟执行。
AMD 是依赖前置,CMD 是依赖就近。
// AMD
define(['./a', './b'], function(a, b) { // 在定义模块时 就要声明其依赖的模块
a.doSomething()
// ....
b.doSomething()
// ....
}) // CMD
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
// ... var b = require('./b') // 可以在用到某个模块时 再去require
b.doSomething()
// ...
})
UMD(Universal Module Definition)
- UMD是AMD和CommonJS的糅合
- UMD的实现很简单:
- 先判断是否支持Node.js模块(exports是否存在),存在则使用Node.js模块模式。
- 再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。
- 前两个都不存在,则将模块公开到全局(window或global)。
(function (window, factory) {
if (typeof exports === 'object') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define([],factory);
} else {
window.eventUtil = factory();
}
})(this, function () {
return {};
});
ES6模块化
ES6 模块的设计思想,是尽量的静态化,使得编译时
就能确定模块的依赖关系,以及输入和输出的变量。
ES6 中,import引用模块,使用export导出模块。通过babel项目将还未被宿主环境(各浏览器、Node.js)直接支持的 ES6 模块 编译为 ES5 的 CommonJS。因此Babel实际上是将不被支持的import/export翻译成目前已被支持的require/exports。
// 导入
import Vue from 'vue'
import App from './App'
// 导出
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
export function multiply() {...};
export var year = 2018;
export default ...
模块化规范大总结
CommonJS | AMD | CMD | ES6 | |
---|---|---|---|---|
引用模块 | require | require | require | import |
暴露接口 | module.exports || exports | define函数返回值 return | exports | export |
加载方式 | 运行时加载,同步 加载 |
并行加载,提前执行,异步 加载 |
并行加载,按需执行,异步 加载 |
编译时加载,异步 加载 |
实现模块规范 | NodeJS | RequireJS | SeaJS | 原生JS |
适用 | 服务器 | 浏览器 | 浏览器 | 服务器/浏览器 |
问题回归:"require"与"import"的区别
说了这么多,还是要回到文章一开始提到的问题,"require"与"import"两种引入模块方式,到底有神马区别,大致可以分为以下几个方面(可能总结的也不是很全面):
写法上的区别
require/exports
的用法只有以下三种简单的写法:
const fs = require('fs')
exports.fs = fs
module.exports = fs
import/export
的写法就多种多样:
import fs from 'fs'
import {default as fs} from 'fs'
import * as fs from 'fs'
import {readFile} from 'fs'
import {readFile as read} from 'fs'
import fs, {readFile} from 'fs'
export default fs
export const fs
export function readFile
export {readFile, read}
export * from 'fs'
输入值的区别
require
输入的变量,基本类型数据是赋值,引用类型为浅拷贝,可修改
import
输入的变量都是只读的,如果输入 a 是一个对象,允许改写对象属性。
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
a.foo = 'hello'; // 合法操作
执行顺序
require
:不具有提升效果,到底加载哪一个模块,只有运行时才知道。
const path = './' + fileName;
const myModual = require(path);
import
:具有提升效果,会提升到整个模块的头部,首先执行。import
的执行早于foo
的调用。本质就是import
命令是编译阶段执行的,在代码运行之前。
foo();
import { foo } from 'my_module';
import()
函数:ES2020提案引入,支持动态加载模块。import()
函数接受一个参数,指定所要加载的模块的位置,参数格式同import
命令,两者区别主要是import()
为动态加载。可用于按需加载
、条件加载
、动态的模块路径
等。
它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块,返回一个 Promise 对象。import()
加载模块成功以后,该模块会作为一个对象,当作then
方法的参数。可以使用对象解构赋值,获取输出接口。
// 按需加载
button.addEventListener('click', event => {
import('./dialogBox.js')
.then({export1, export2} => { // export1和export2都是dialogBox.js的输出接口,解构获得
// do something...
})
.catch(error => {})
});
// 条件加载
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
}
// 动态的模块路径
import(f()).then(...); // 根据函数f的返回结果,加载不同的模块。
使用表达式和变量
require
:很显然是可以使用表达式和变量的
let a = require('./a.js')
a.add()
let b = require('./b.js')
b.getSum()
import
静态执行,不能使用表达式和变量,因为这些都是只有在运行时才能得到结果的语法结构。
// 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;
// 报错
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
而require/exports 和 import/export 本质上的区别,实际上也就是CommonJS规范与ES6模块化的区别
它们有三个重大差异。
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
CommonJS 模块的
require()
是同步
加载模块,ES6 模块的import
命令是异步
加载,有一个独立的模块依赖的解析阶段。
导致第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成
CommonJS:运行时加载
- 只能在运行时确定模块的依赖关系,以及输入和输出的变量,一个模块就是一个对象,输入时必须查找对象属性。
// CommonJS模块
let { stat, exists, readfile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
ES6 :编译时加载
或者静态加载
- ES6 模块不是对象,而是通过
export
命令显式指定输出的代码,再通过import
命令输入。 - 可以在编译时就完成模块加载,引用时只加载需要的方法,其他方法不加载。效率要比 CommonJS 模块的加载方式高。
import { stat, exists, readFile } from 'fs';
一文彻底搞懂JS前端5大模块化规范及其区别的更多相关文章
- 一文搞懂 js 中的各种 for 循环的不同之处
一文搞懂 js 中的各种 for 循环的不同之处 See the Pen for...in vs for...of by xgqfrms (@xgqfrms) on CodePen. for &quo ...
- 帮你彻底搞懂JS中的prototype、__proto__与constructor(图解)
作为一名前端工程师,必须搞懂JS中的prototype.__proto__与constructor属性,相信很多初学者对这些属性存在许多困惑,容易把它们混淆,本文旨在帮助大家理清它们之间的关系并彻底搞 ...
- 让你彻底搞懂JS中复杂运算符==
让你彻底搞懂JS中复杂运算符== 大家知道,==是JavaScript中比较复杂的一个运算符.它的运算规则奇怪,容易让人犯错,从而成为JavaScript中“最糟糕的特性”之一. 在仔细阅读了ECMA ...
- 彻底搞懂 JS 中 this 机制
彻底搞懂 JS 中 this 机制 摘要:本文属于原创,欢迎转载,转载请保留出处:https://github.com/jasonGeng88/blog 目录 this 是什么 this 的四种绑定规 ...
- 一文彻底搞懂Java中的环境变量
一文搞懂Java环境变量 记得刚接触Java,第一件事就是配环境变量,作为一个初学者,只知道环境变量怎样配,在加上各种IDE使我们能方便的开发,而忽略了其本质的东西,只知其然不知其所以然,随着不断的深 ...
- 彻底搞懂js __proto__ prototype constructor
在开始之前,必须要知道的是:对象具有__proto__.constructor(函数也是对象固也具有以上)属性,而函数独有prototype 在博客园看到一张图分析到位很彻底,这里共享: 刚开始看这图 ...
- 搞懂:前端跨域问题JS解决跨域问题VUE代理解决跨域问题原理
什么是跨域 跨域:一个域下的文档或脚本试图去请求另一个域下的资源 广义的跨域包含一下内容: 1.资源跳转(链接跳转,重定向跳转,表单提交) 2.资源请求(内部的引用,脚本script,图片img,fr ...
- 一文搞懂js中的typeof用法
基础 typeof 运算符是 javascript 的基础知识点,尽管它存在一定的局限性(见下文),但在前端js的实际编码过程中,仍然是使用比较多的类型判断方式. 因此,掌握该运算符的特点,对于写出好 ...
- 【原创】一文彻底搞懂安卓WebView白名单校验
前言 近两年公司端侧发现的漏洞很大一部分都出在WebView白名单上,针对这类漏洞安全编码团队也组织过多次培训,但是这种漏洞还是屡见不鲜.下面本人就结合产品中容易出现问题的地方,用实例的方式来总结一下 ...
随机推荐
- js map(Number) All In One
js map(Number) All In One map() 方法创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值. let newArray = arr.map(callb ...
- SVG background watermark
SVG background watermark SVG 背景水印 <svg xmlns="http://www.w3.org/2000/svg" width="2 ...
- bash variables plus operator All In One
bash variables plus operator All In One Errors missing pass params #!/usr/bin/env bash # echo emoji ...
- Protocol Buffers All In One
Protocol Buffers All In One Protocol Buffers - Google's data interchange format Protocol buffers are ...
- 十大排序算法时间复杂度 All In One
十大排序算法时间复杂度 All In One 排序算法时间复杂度 排序算法对比 Big O O(n) O(n*log(n)) O(n^2) 冒泡排序 选择排序 插入排序 快速排序 归并排序 基数排序 ...
- Objective C & Swift & iOS & App
Objective C & Swift & iOS & App https://www.runoob.com/ios/ios-objective-c.html https:// ...
- Linux & bash & tcpdump
Linux & bash & tcpdump Linux & tcpdump https://www.tecmint.com/12-tcpdump-commands-a-net ...
- Dva & Umi
Dva & Umi Dva.js & Umi.js React & Redux https://dvajs.com/ React and redux based, lightw ...
- Linux & SIGUSER1
Linux & SIGUSER1 https://stackoverflow.com/questions/10824886/how-to-signal-an-application-witho ...
- input number css hidden arrow
input number css hidden arrow show arrow OK input[type="number"]::-webkit-inner-spin-butto ...