【转】彻底搞懂JS前端5大模块化规范及其区别

前言

在开发以及面试中,总是会遇到有关模块化相关的问题,始终不是很明白,不得要领,例如以下问题,回答起来也是模棱两可,希望通过这篇文章,能够让大家了解一二,首先抛出问题:

  • 导出模块时使用 module.exports/exports 或者 export/export default
  • 有时加载一个模块会使用 require 奇怪的是也可以使用 import ??它们之间有何区别呢?

推荐阅读: 关于 npm 最详细的介绍

script 标签

其实最原始的 JavaScript 文件加载方式,就是 script 标签,如果把每一个文件看做是一个模块,那么他们的接口通常是暴露在全局作用域下,也就是定义在 window 对象中,不同模块的接口调用都是一个作用域中,一些复杂的框架,会使用命名空间的概念来组织这些模块的接口。

缺点:

1、污染全局作用域
2、开发人员必须主观解决模块和代码库的依赖关系
3、文件只能按照 script 标签的书写顺序进行加载
4、在大型项目中各种资源难以管理,长期积累的问题导致代码库混乱不堪

默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到 <script> 标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。

如果脚本体积很大,下载和执行的时间就会很长,因此会造成浏览器堵塞,用户会感觉到浏览器“卡死”了,没有任何响应。这显然是很不好的体验,所以浏览器允许脚本异步加载。

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

<script> 标签添加 deferasync 属性,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。

defer:要等到整个页面在内存中正常渲染结束,才会执行;多个脚本时,按顺序执行

async:一旦下载完,渲染引擎就会中断渲染,执行这个脚本再继续渲染。多个脚本时,不能保证按执行顺序

总结一句话:defer 是 “渲染完再执行”,async 是“下载完就执行”。

CommonJS规范(同步加载模块)

  • 服务器端实现: Node.js
  • 浏览器端实现: Browserify,也称为 Commonjs 的浏览器的打包工具
  • 通过 require 方法同步加载所依赖的模块,通过 exportsmodule.exports 导出需要暴露的数据。一个文件就是一个模块
  • CommonJS 规范包括了模块(modules)、包(packages)、系统(system)、二进制(binary)、控制台(console)、编码(encodings)、文件系统(filesystems)、套接字(sockets)、单元测试(unit testing)等部分。

加载模块

使用 require 函数加载模块(即被依赖模块的 module.exports 对象)

1、按路径加载模块
2、通过查找 node_modules 目录加载模块
3、加载缓存:Node.js 是根据实际文件名缓存,而不是 require() 提供的参数缓存的,如 require('express')require('./node_modules/express') 加载两次,也不会重复加载,尽管两次参数不同,解析到的文件却是同一个。
4、核心模块拥有最高的加载优先级,换言之如果有模块与其命名冲突,Node.js 总是会加载核心模块。
5、更多关于 require 函数的用法和特点,博主此前另外总结过一篇博文: 关于 npm 最详细的介绍

导出模块

exports.属性 =exports.方法 = 函数
  • Node.js 为每个模块提供一个 exports 变量,指向 module.exports。相当于在每个模块头部,有一行这样的命令:var exports = module.exports
  • exports 对象和 module.exports 对象,指同一个内存空间, module.exports 对象才是真正的暴露对象
  • exports 对象是 module.exports 对象的引用,不能改变指向,只能添加属性和方法,若直接改变 exports 的指向,等于切断了 exportsmodule.exports 的联系,返回空对象
  • console.log(module.exports === exports); // true

示例:

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

1、同步加载方式,适用于服务端,因为模块都放在服务器端,对于服务端来说模块加载较快,不适合在浏览器环境中使用,因为同步意味着阻塞加载。
2、所有代码都运行在模块作用域,不会污染全局作用域。
3、模块可以多次加载,但只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。
4、模块加载的顺序,按照其在代码中出现的顺序。

AMD(Asynchronous Module Definition)

采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。推崇依赖前置。

CMD(Common Module Definition)

  • CMD 是通用模块加载,要解决的问题与 AMD 一样,只不过是对依赖模块的执行时机不同 ,推崇就近依赖。
  • sea.js 是 CMD 规范的一个实现代表库

关于 AMD 和 CMD 的讲解,这里省去了。1、我之前写过一篇 AMD 和 CMD 的文章,感兴趣可以去看看;2、AMD 和 CMD 已经过时了,现在的前端开发已经用不到了。

UMD(Universal Module Definition)

  • UMDAMDCommonJS 的糅合
  • UMD 的实现很简单:
    • 先判断是否支持 Node.js 模块(exports 是否存在),存在则使用 Node.js 模块模式。
    • 再判断是否支持 AMDdefine 是否存在),存在则使用 AMD 方式加载模块。
    • 前两个都不存在,则将模块公开到全局(windowglobal
(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 导出模块。默认情况下,Node.js 默认是不支持 import 语法的,通过 babel 项目将 ES6 模块 编译为 ES5 的 CommonJS。因此 Babel 实际上是将 import/export 翻译成 Node.js 支持的 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 = 2021;
export default ...

问题回归:"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/exportsimport/export 本质上的区别,实际上也就是 CommonJS规范与 ES6模块化的区别:
1、浏览器在不做任何处理时,默认是不支持importrequire
2、babel会将ES6模块规范转化成Commonjs规范
3、webpackgulp以及其他构建工具会对Commonjs进行处理,使之支持浏览器环境
它们有三个重大差异。
1、CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
2、CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
3、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'

-----------------(正文完)------------------

前端学习交流群,想进来面基的,可以加群: 685486827832485817
Vue学习交流 React学习交流

写在最后: 约定优于配置 —— 软件开发的简约原则

--------------------------------(完)--------------------------------------

我的:
个人网站: https://neveryu.github.io/neveryu/
Github: https://github.com/Neveryu
新浪微博: https://weibo.com/Neveryu
微信: miracle421354532

更多学习资源请关注我的新浪微博…好吗

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值