JS前端5大模块化规范及其区别

一、前言

首先抛出问题

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

1、加载模块

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

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

2、导出模块

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();

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

img

// 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()

3、服务器端实现 NodeJs

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

img

4、浏览器端实现 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>

img

5、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);
});

1、创建模块 及 规范模块加载

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

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

// math.js

define(function () {
    var add = function (x, y) {
        return x + y;
    };
    return {
        add: add
    };
});

2.若这个模块还依赖其他模块,那么 define() 函数的第一个参数,必须是一个数组,指明该模块的依赖性。当 require() 函数加载test模块时,就会先加载 math.js 模块。

// dataService.js

define(['math'], function (math) {
    function doSomething() {
        let result = math.add(2, 9);
        console.log(result);
    }

    return {
        doSomething
    };
});

3.设置一个主模块,统一调度当前项目中所有依赖模块

// main.js

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

4.在index.html中引入require.js,并设置data-main入口主模块

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

5.来来来,浏览器看看效果了:打印出了两数字相加的结果

img

6.本案例中所有源码,目录结构及每个模块的作用,如下图所示(源码同上1234步骤)

img

2、加载非规范的模块

理论上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 的插件还可以这样定义:

require.config({
    shim: {
        'jquery.scroll': {
            deps: ['jquery'],
            exports: 'jQuery.fn.scroll'
        }
    }
});

3、AMD特点

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

四、CMD(Common Module Definition)

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

1.factory 是函数时有三个参数,function(require, exports, module):

  • requirerequire函数用来获取其他模块提供的接口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 模块导入和导出的特点
  • 由于代码比较杂,所以还是看图理解一下吧,图上均有标注每个文件的用途,图二为浏览器执行效果:

img

img

1、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 ...

刚刚讲到使用babel的目的就是为了将import编译为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文件中

img

6.编译命令及浏览器运行效果:

img

7.如果想要省去编译过程直接在浏览器运行纯ES6语法编写的js模块

image

然后贴出如果不做任何处理直接打开index.html可能出现的报错问题:Uncaught SyntaxError: Cannot use import statement outside a module,解决方案见参考文章

本地html文件中的script标签引入ES6的模块,直接在浏览器中打开该html文件,发现报错了:Uncaught SyntaxError: Cannot use import statement outside a module

报错

对应的index.html:

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Document</title>
</head>
<body>
 <script src="./indexes.js"></script>
</body>
</html>

对应的indexes.js:

import cal from './calculatores.js'
console.log('sum: ', cal.add(1,2))

对应的calculatores.js:

export default {
 add: function(a, b) {
     return a + b
 }
}

这里报错的原因是用了ES6的语法, 浏览器默认将它作为js解析会出现问题,需要将它作为模块导入,script标签默认type="text/javascript",需要改为type="module",更改后的index.html:

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Document</title>
</head>
<body>
 <script type="module" src="./indexes.js"></script>
</body>
</html>

在浏览器中打开,发现又报错了:Access to script at 'file:///E:/**********/indexes.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, chrome-untrusted, https.

报错

从错误提示看,脚本是被跨域策略给拦截了,跨域请求只支持这些协议:http, data, chrome, chrome-extension, chrome-untrusted, https,而我们的协议是file,这里我们需要本地起一个服务器来作为资源的提供方,简单的方式是安装VSCode的一个扩展Live Server

liveserver

该插件安装完成后,在index.html页面右键选择Open with Live Server

在这里插入图片描述

然后就可以成功运行了:

成功运行

url上我们可以看到,Live Server为我们默认启动的服务器地址是127.0.0.1,端口号为5500,这样就解决跨域的问题啦!

8.在浏览器环境要想使用es6的模块化,必须设置script的type为module

<script type="module">
    // 从url导入一个远程模块
    import * as aa from 'https://cdn.jsdelivr.net/npm/outils@1.6.1/min/outils.min.js'
</script>

七、模块化规范大总结

CommonJS AMD CMD ES6
引用模块 require require require import
暴露接口 module.exports || exports define函数返回值 return exports export
加载方式 运行时加载,同步加载 并行加载,提前执行,异步加载 并行加载,按需执行,异步加载 编译时加载,异步加载
实现模块规范 NodeJS RequireJS SeaJS 原生JS
适用 服务器 浏览器 浏览器 服务器/浏览器

八、问题回归:"require"与"import"的区别

说了这么多,还是要回到文章一开始提到的问题,"require"与"import"两种引入模块方式,到底有神马区别,大致可以分为以下几个方面(可能总结的也不是很全面):

1、写法上的区别

require/exports 的用法只有以下三种简单的写法:

// 暴露
exports.fs = fs
module.exports = fs
// 引入
const fs = require('fs')

import/export 的写法就多种多样:

// 暴露
export default fs
export const fs

export function readFile

export {readFile, read}
export * from 'fs'

// 引入
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'

2、输入值的区别

require输入的变量,基本类型数据是赋值,引用类型为浅拷贝,可修改

import输入的变量都是只读的,如果输入 a 是一个对象,允许改写对象属性。

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

a = {}; // Syntax Error : 'a' is read-only;

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

3、执行顺序

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 => {
    // export1和export2都是dialogBox.js的输出接口,解构获得
    import('./dialogBox.js').then(({export1, export2}) => {
        // do something...
    }).catch(error => {})
});

// 条件加载
if (condition) {
  import('moduleA').then(...);
} else {
  import('moduleB').then(...);
}


// 动态的模块路径
import(f()).then(...);    // 根据函数f的返回结果,加载不同的模块。

4、使用表达式和变量

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运行时加载

  • 只能在运行时确定模块的依赖关系,以及输入和输出的变量,一个模块就是一个对象,输入时必须查找对象属性。
// 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';

参考文章

原文地址:https://www.cnblogs.com/echoyya/p/14577243.html

1、使用纯ES6语法编写的js模块在浏览器环境运行报错:

Uncaught SyntaxError: Cannot use import statement outside a module的解决方法

https://blog.csdn.net/cc18868876837/article/details/113915176

posted @ 2022-10-21 20:51  黄河大道东  阅读(51)  评论(0编辑  收藏  举报