4

一文彻底搞懂JS前端5大模块化规范及其区别

 3 years ago
source link: https://www.cnblogs.com/echoyya/p/14577243.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

码文不易,转载请带上本文链接,感谢~ https://www.cnblogs.com/echoyya/p/14577243.html

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

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

于是有了菜鸟解惑的搜喽过程。。。。。。

模块化规范:即为 JavaScript 提供一种模块编写、模块依赖和模块运行的方案。降低代码复杂度,提高解耦性

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函数的用法和特点,博主此前另外总结过一篇博文,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();

以下同样一段代码(图为对应的目录结构),分别运行在服务器端和浏览器端,看看有神马区别?

// module1.js
module.exports = {
  foo(){
    console.log('module1的foo()函数运行了');
  }
}

// module2.js
module.exports = function() {
    console.log('module2的foo()函数运行了');
}

// module3.js
exports.foo = function () {
  console.log('module3的foo()函数运行了');
}

exports.bar = function () {
  console.log('module3的bar()函数运行了');
}
// main.js
let module1 = require('./module1')
let module2 = require('./module2')
let module3 = require('./module3') 

module1.foo()

module2()

module3.foo()
module3.bar()

服务器端实现 NodeJs

cd响应的目录,直接命令行执行:node main.js

浏览器端实现 Browserify

Browserify 本身也是一个 NodeJS 模块,npm安装后可以使用 browserify 命令。分析文件中require 方法调用来递归查找所依赖的其他模块。把输入文件所依赖的所有模块文件打包成单个文件并输出

npm安装命令 :npm install -g browserify

打包命令:browserify 入口文件 -o 打包文件 如:browserify ./src/main.js -o ./dist/build.js

想要运行在浏览器端,要有一个入口的hmtl文件。创建index.html,并引入上述打包生成的build.js文件 <script src="./dist/build.js"></script>

CommonJS 特点

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

AMD(Asynchronous Module Definition)

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

require.js 是目前 AMD 规范最热门的一个实现

AMD 也采用 require语句加载模块,但是不同于 CommonJS,它要求两个参数:require([module], callback);

  • [module]:是一个数组,成员就是要加载的模块

  • callback:加载成功之后的回调函数;

require(['math'], function (math) {
  math.add(2, 3);
});

创建模块 及 规范模块加载

模块必须采用 define() 函数来定义。

  1. 若一个模块不依赖其他模块。可以直接定义在 define() 函数中。
// math.js

define(function (){
 var add = function (x,y){
  return x+y;
 };
 return {
  add: add
 };
});
  1. 若这个模块还依赖其他模块,那么 define() 函数的第一个参数,必须是一个数组,指明该模块的依赖性。当 require() 函数加载test模块时,就会先加载 math.js 模块。
// dataService.js

define(['math'], function (math) {
  function doSomething() {
    let result = math.add(2, 9);
    console.log(result);
  }
  return {
    doSomething
  };
});
  1. 设置一个主模块,统一调度当前项目中所有依赖模块
// main.js

(function () {

  require.config ({
    // baseUrl:'',    
    paths:{   
      dataService:'./dataService',
      math:'./math'
    }
  })
  require(['dataService'], function (dataService) {
    dataService.doSomething()
  });

})();
  1. index.html中引入require.js,并设置data-main入口主模块
<!-- index/html -->

<script data-main="./js/main.js" src="./js/require.js"></script>

  1. 来来来,浏览器看看效果了:打印出了两数字相加的结果
  1. 本案例中所有源码,目录结构及每个模块的作用,如下图所示(源码同上1234步骤)

加载非规范的模块

理论上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特点

  1. AMD允许输出的模块兼容CommonJS
  2. 异步并行加载,不阻塞 DOM 渲染。
  3. 推崇依赖前置,也就是提前执行(预执行),在模块使用之前就已经执行完毕。

CMD(Common Module Definition)

  • CMD 是通用模块加载,要解决的问题与 AMD 一样,只不过是对依赖模块的执行时机不同 ,推崇就近依赖
  • sea.js 是 CMD 规范的一个实现代表库
  • 定义模块使用全局函数define,接收一个 factory 参数,可以是一个函数,也可以是一个对象或字符串;
  1. factory 是函数时有三个参数,function(require, exports, module):

    • require:函数用来获取其他模块提供的接口require(模块标识ID)

    • exports: 对象用来向外提供模块接口

    • module :对象,存储了与当前模块相关联的属性和方法

    // 定义 a.js 模块,同时可引入其他依赖模块,及导出本模块
    define(function(require, exports, module) {
    
      var $ = require('jquery.js')
     
      exports.price= 200;  
    });
    
    
  2. factory 为对象、字符串时,表示模块的接口就是该对象、字符串。比如可以定义一个 JSON 数据模块:

     // 定义 foo.js
    define({"foo": "bar"});
    
     // 导入使用
    define(function(require, exports, module) {
    
      var obj = require('foo.js')
     
      console.log(obj)   // {foo: "bar"}
    });
    
  3. 下面通过一个案例分析,深入了解一下CMD模块化规范,具体的用法:

    • 定义1,2,3,4,四个简单模块,定义主模块main.js, 以及一个index.html
    • cmd从语法上分析,结合了AMD模块定义的特点,同时又沿用了CommonJs 模块导入和导出的特点
    • 由于代码比较杂,所以还是看图理解一下吧,图上均有标注每个文件的用途,图二为浏览器执行效果:

AMD 与 CMD 的区别

  1. AMD 是提前执行,CMD 是延迟执行

  2. 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的实现很简单:
    1. 先判断是否支持Node.js模块(exports是否存在),存在则使用Node.js模块模式。
    2. 再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。
    3. 前两个都不存在,则将模块公开到全局(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导出模块。默认情况下,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 = 2018;
export default ...

刚刚讲到使用babelimport编译为nodejs支持的require,即可使用node命令执行,而浏览器默认是不支持import和require的,此时还需要借助另一个工具,即上文中,在讲述CommonJs时,提到的,browserify,下面请看完整的案例分析:

  1. 安装必要包,babel,及browserify
    • npm install babel-cli -g
    • npm install babel-preset-es2015 --save-dev
    • npm install browserify -g
  2. 创建.babelrc文件,并设置编译格式为es2015
  3. 自定义一个模块,导出数据,并在主模块中加载执行
  4. babel ./src -d ./build 命令将import编译为require
  5. browserify ./build/main.js -o ./dist/main.js 编译为浏览器识别语法,最终引入index.html文件中
  1. 编译命令及浏览器运行效果:

模块化规范大总结

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模块化的区别

1、浏览器在不做任何处理时,默认是不支持import和require
2、babel会将ES6模块规范转化成Commonjs规范
3、webpack、gulp以及其他构建工具会对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';

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK