JavaScript 系列 -- 模块化概念

284 阅读8分钟

前言

我们现在做 vue 项目大多都采用 webpack 构建系统,而从本质上讲,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具,也就是说其运行原理就是 JavaScript 的模块化的概念。

另外,如今流行的组件库、万千 demo,还有 JavaScript 函数库(如 lodash)的引入使用都是基于 JavaScript 模块化的概念实现的,所以学习 JavaScript 模块化编程很重要。

其实不止 JavaScript 有这个概念,在很多方面都有这样的概念:就是把一个完整的 产品/需求 按照 结构/功能 拆分成很多的大模块,大模块又拆分成小模块,小模块又可以继续拆分成更小的模块...遵循着 大而化小,小而化了 的解决问题的理念。JavaScript 中 Array、Object 的 prototype 的原生方法也是遵循这种理念。

咳咳,扯远了。打个比方:

  • 对于组件:例如一个前端页面里的弹窗,弹窗里的内容有自己独立的组件+样式+逻辑,完全是可以独立出来的模块,这时候我们就可以使用 vue 框架基于 webpack 系统构建出这样的弹窗组件,这就是 JavaScript 模块化概念的一种体现;
  • 对于函数:另外我们使用new Date()方式创建出来的字符串Thu Jul 15 2021 13:51:38 GMT+0800 (中国标准时间)我们希望使其转换为我们常用的时间格式2021-07-15 13:51:38,而且不止一个页面(组件)需要这样做,这时我们就可以在项目文件夹中新建一个 JavaScript 文件里面写好这样的函数,然后在需要的地方引入后使用即可。

如果写的 组件 / 函数 发现诶全社会的开发人员都可以采纳,那就把组件和函数放到服务器上面,让别人在<script>标签 / npm / yarn 等方式引入。换句话说,你也可以使用别人写的 组件 / 函数,这就实现 代码互联 了,当然前提条件是大家都要遵循相同的规范。

要实现以上两种需求(等类似的需求)的引入方式通常是requireimport两种方式:

require 方式

三个特点

  • 运行时加载
  • 拷贝到本页面
  • 全部引入

三种规范

  • CommonJS 规范
  • AMD 规范
  • CMD 规范 require 方式有三种规范,三种规范规定这各自不同的【使用写法 + 模块写法

CommonJS 规范:【同步加载 require】

Node.js 采用 CommonJS 规范

使用写法:require([module])
// 不创建实例直接使用函数
var math = require('math');
math.add(2, 3);

// 创建实例后使用函数
var math = require('math');
const Math = new math(2, 3)
Math.add();
模块写法:module.exports = xxx 或 exports.prop = xxx
// module.exports 方式
module.exports = class math {
  constructor(x,y) {
    this.x = x;
    this.y = y;
  }
  add() {
    return  x+y;
  }
};

// exports 方式
exports.add = (x,y) => x+y;

关于 module.exports 和 exports 两种方式的区别 写在下边

AMD 规范:【异步加载 require】

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

使用写法:require([module], callback)

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

require([module], callback);
  • 第一个参数 [module],是一个数组,里面的成员就是要加载的模块
  • 第二个参数 callback,是加载成功之后的回调函数。 如果将前面的代码改写成 AMD 形式,就是下面这样:
require(['math'], function (math) {
    math.add(2, 3);
});

math 模块加载才执行 add 方法,从全局看这段代码不会造成阻塞,浏览器不会发生假死。所以很显然,AMD 比较适合浏览器环境

模块写法:define( (id), (othermodule), function )

参数解读:

  • id:模块名称 (可选)。字符串类型
  • othermodule:是我们 要载入的依赖模块(可选),使用相对路径。注意是数组类型
  • function:工厂方法,返回一个 模块函数 math.js
// 无模块名称、不使用其他模块
define(function (){
    var add = function (x,y){
        return x+y;
    };
    return {
        add: add
    };
});
// 无模块名称、有使用其他模块
define(['a','b'], function(a,b){
    function foo(){
        a.doSomething();// 依赖前置,提前执行
        b.doSomething();
    }
    return {
        foo : foo
    };
});

CMD 规范

sea.js 采用的思想是 CMD:sea.js 是依赖就近延迟执行;require.js是依赖前置提前执行

使用方法:seajs.config() ; seajs.use( [module], function )
seajs.config({
  alias: {
    'jquery': 'http://modules.seajs.org/jquery/1.7.2/jquery.js'
  }
});

seajs.use(['./hello', 'jquery'], function(hello, $) { // 依赖就近,延迟执行,$ 最近先执行
  $('#beautiful-sea').click(hello.sayHello);
});
模块写法:define( (id), (othermodule), function )
define(function(require, exports, module) {
  var $ = require('jquery');
  exports.sayHello = function() {
    $('#hello').toggle('slow');
  };
   var b = require("b");
   b.doSomething();
});

import 方式

历史背景

在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案

ES6 在语言标准的层面上,实现了模块功能。ES6 模块不是对象,而是:

  • 通过 export 命令显式指定输出的代码
  • 再通过 import 命令输入

三个特点

  • 编译时加载
  • 只引用定义
  • 按需加载

import 的几种写法

1. import defaultName from './modules.js';
2. import { export } from 'modules';
3. import { export as ex1 } from 'modules';
4. import { export1, export2 } from 'modules.js';
5. import { export1 as ex1, export2 as ex2 } from 'modules.js';
6. import defaultName, { expoprt } from 'modules';
7. import * as moduleName from 'modules.js';
8. import defaultName, * as moduleName from 'modules';
9. import 'modules';

import 用法解释

  • import 后面的 from 指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js后缀可以省略
  • 上面代码使用的 as 关键字,相当于import 进来的‘值’的别名。
  • import * from 'xx'导入整个模块的内容,而对比之下 import defaultName 和 import { export1, export2 } 将导入 export 的某个对象或值
  • 最后一种方式 import 'modules' 将运行模块中的 全局代码,而 不导入任何值

import 的形式需要 export 的支持,比如 import defaultName from 'module.js 将导出 在 modules.js 中export default 的对象或值:

export 的几种用法

1.export { name1, name2, …, nameN };
2.export { variable1 as name1, variable2 as name2, …, nameN };
3.export let name1, name2, …, nameN; // also var
4.export let name1 = …, name2 = …, …, nameN; // also var, const
5.export function FunctionName() {...}
6.export class ClassName {...}

7.export default expression;
8.export default function () { … } // also class, function*
9.export default function name1() { … } // also class, function*
10.export { name1 as default, … };

11.export * from …;
12.export { name1, name2, …, nameN } from …;
13.export { import1 as name1, import2 as name2, …, nameN } from …;

export 用法解释

  1. 命名导出
// module.js
const ex1 = 'xxx';
const fun = function() {...}
export { ex1, fun as demoFun};
export let ex2 = 'demo';
export function multiply(x, y) {
  return x * y;
};
// main.js
import { ex1, demoFun, ex2, multiply } from 'module.js';
  1. 默认导出 —— export default 命令

export 命名导出需要 export 名字和 import 名字严格一致。 而 export default 命令,为模块指定默认输出,使得在 import 的时候可以随意命名名字。一个模块只能有一个默认输出,也就是说 export default 一个模块只能用一次

// a.js 输出一个默认函数
export default function add(x, y) { return x + y; }
import anyName from 'a.js';
// b.js 输出一个默认对象
let obj = {...};
export default obj;
import anyName from 'b.js'
// c.js 输出一个类
export default class { ...}
import anyClass from 'c.js';
// d.js  输出一个值
export default 1;
import value from 'd.js'
  1. export 和 import 混合使用(模块重定向) 也就是在一个模块之中,先输入后输出同一个模块。比如:
<!--命名导出 引入的命名导出-->
export { foo, bar } from 'my_module';
// 等价为,值得注意的是 在该模块中不能直接使用 foo 和 bar。
import { foo, bar } from 'my_module';
export { foo, bar };

export * from  './other-module';  // 导出所有方法,但注意此种方法不会到导出module.js中的默认导出变量。
// 导出 默认导出用下面写法
export {default} from './other-module';

附加问题解决

module.exports 和 exports 两种方式的区别

首先要知道,module.exports 与 exports 指向 同一个 空对象 {} 。然后相比于 exports 方式 我个人推荐使用 module.exports 方式,原因如下:

  • 模块导出方式的差异:
    • exports 方式:相当于是给这个空对象添加属性或方法
    exports.num = 666 // 导出的对象用点操作获取 num 属性
    exports.func = function () {} // 导出的对象用点操作使用 func 函数
    
    • module.exports 方式:相当于放弃该对象,随意赋值其他数据类型的数据
    module.exports = { num : 666 } // 导出的内容为 { num : 666 } 这个对象
    module.exports = function () {} // 导出的内容为一个函数,使用方需要赋值给一个变量才能使用该函数
    
  • 模块导出为函数时:
    • exports 方式:模块使用者需要知道函数名才能引入使用
    • module.exports 方式:模块使用者不需要知道函数名就能引入使用
    exports.func = function () {} // 模块编写者使用 exports 来定义
    const { func } = require('./module'); // 模块使用者必须知道该函数的名称才能使用
    
    module.exports = function () {} // 模块编写者使用 module.exports 来定义
    const fn = require('./module'); // 模块使用者可以自定义引入的函数的【代名称】
    

这里顺便介绍一下 node.js

2009年,美国程序员 Ryan Dahl 创造了 node.js 项目,将 Javascript 语言用于服务器端编程。这标志"Javascript模块化编程"正式诞生。相比在浏览器环境下,服务器端一定要有模块,因为要与操作系统和其他应用程序互动,否则根本没法编程。

所以,node.js 的模块系统,是参照 CommonJS 规范实现的

参考文章