【前端复习计划】-模块化

281 阅读7分钟

CommonJS

Node.js 是 common JS 规范的主要实践者,Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。CommonJS 具有如下特点:

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存
  • 模块加载的顺序,按照其在代码中出现的顺序

CommonJS定义的模块分为: 模块标识(module)、模块定义(exports) 、模块引用(require)

module

CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它有以下属性:

  • module.id : 模块的识别符,通常是带有绝对路径的模块文件名
  • module.filename : 模块的文件名,带有绝对路径
  • module.loaded :返回一个布尔值,表示模块是否已经完成加载
  • module.parent : 返回一个对象,表示调用该模块的模块 (新增于: v0.1.16 弃用于: v12.19.0)
  • module.children :返回一个数组,表示该模块要用到的其他模块
  • module.isPreloading :返回一个布尔值,表示模块是否正在 Node.js 预加载阶段运行
  • module.path :表示当前模块的文件路径
  • module.paths :返回一个数组,表示模块的搜索路径,也就是 Node.js 在解析该模块时会进行搜索并访问的文件路径
  • module.exports : 表示模块对外导出的内容

下面是一个例子,这里有2个文件 input.js 和 output.js,代码如下

// output.js
const jQuery = require('jquery');
const person = {
    name: 'Jack',
    tools: jQuery
};
function introducePerson(person) {
    console.log(person.name, person.tools);
}
module.exports.jQuery = jQuery;
module.exports.person = person;
module.exports.introducePerson = introducePerson;



// input.js
const output = require('./output');
const {person, introducePerson} = output;
introducePerson(person);
console.log(module);

module.exports

module.exports 是一个模块对外的接口,加载某个模块其实是加载该模块的module.exports属性

// example.js
const a = 5;
var say = function (value) {
  console.log(value)
};
module.exports.a = a;
module.exports.say = say;

module.exports 属性可见简写为 exports,但是你不能直接对 exports 赋值,因为 exports 是 module.exports 对象的引用,如果直接对其赋值相当于解除了这个引用关系,exports 也就没有意义了

module.exports.say = say
// 等同于
exports.say = say


// 错误写法
exports = say

建议只使用 module.exports 导出模块

require

require 用于导入模块,直接调用 require() 方法就可以从文件导入模块,下面是一个导入模块的例子:

const exampleModule = require('./example.js');
console.log(exampleModule.a); // 5
console.log(exampleModule.say(1)); // 1

把相关的文件放在一个目录里可以方便管理,通过为该目录设置一个入口文件,让require方法可以通过这个入口文件,加载整个目录中的模块。在目录中创建一个package.json文件,并且将入口文件写入main字段:

// package.json
{ 
	"name" : "outputModules",
  	"main" : "./lib/output.js" 
}

require发现参数字符串指向一个目录以后,会自动查看该目录的package.json文件,然后加载main字段指定的入口文件。如果package.json文件没有main字段,或者根本就没有package.json文件,则会加载该目录下的index.js文件或index.node文件

AMD

诞生背景

CommonJS 在 Node.js 中得到了极大的应用,但是 CommonJS 却无法在浏览器端使用,如果在浏览器中运行,会有一个很大的问题

const math = require('math');
math.add(2, 3);

第二行 math.add(2, 3),在第一行 require('math')之后运行,因此必须等 math.js加载完成。也就是说,如果加载时间很长,整个程序就会被阻塞,这对服务器端来说不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间,通常都是很快的。但是,对于浏览器来说,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间

因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous),这就是AMD规范诞生的背景

概念

AMD(Asynchronous Module Definition),异步模块定义。它采用异步的方式加载模块,模块的加载不影响它后面语句的运行,所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数就会被调用执行

AMD 规范主要应用在了 require.js 这个库中

define

AMD 规定模块必须采用特定的define(module, callback)函数来定义,它的参数如下:

  • module : 一个数组,表示当前模块的依赖,如果没有依赖,这个参数可以不传
  • callback :回调函数,在其中定义要导出的内容

下面是一个例子:

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

如果当前定义的模块,还依赖有其他的模块,那么就要在参数中传入

define(['calc'], function(calc) {
    function toDo() {
        calc.compute();
    }
    return {
        calTools: toDo
    };
});

require

AMD 也通过 require(module, callback) 方法来引入模块,它接受两个参数:

  • module :可选参数,一个数组,表示当前需要引入并加载的模块
  • callback :模块加载完成后执行的回调
require(['jquery', 'underscore', 'backbone'], function ($, _, Backbone){
		console.log('模块引入完毕')
});

默认情况下,require.js 假定要引入的模块都位于当前文件的同级目录中,所以你看到 module 数组中是这样的写法 ['jquery', 'underscore', 'backbone'],但是,如果模块不在当前目录中怎么办呢?可以通过 require.config() 方法来指定模块路径和模块名称

require.config({
  paths: {
      "jquery": "src/lib/jquery.min",
      "underscore": "src/lib/underscore.min",
      "backbone": "src/lib/backbone.min"
  }
});

// 第二种简便的写法:指定基路径
require.config({
  baseUrl: "src/lib",
  paths: {
      "jquery": "jquery.min",
      "underscore": "underscore.min",
      "backbone": "backbone.min"
  }
});

如果是从服务器上引入的依赖,则可以传入模块的 url

require.config({
  paths: {
    "jquery": "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min"
  }
});

ES Module

在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。ES6 Module 的特点是静态化,在编译时就能确定模块的依赖关系,以及导出和导入的变量

export

export 用于对外导出变量,例如:

export const name = 'Jack';
export const age = 35;
export function hello(){
	console.log('Hello')
};

// 简便的写法
export { name, age, hello };

export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系,下面的写法都是错的:

const name = 'Jack';
function hello(){
	console.log('Hello')
};

// 错误写法,导出的其实不是变量,而仅仅是值
export name;
export hello;

// 正确的写法,使用 export {} 的形式
export { name, hello };

另外,export输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值;这一点与 CommonJS 规范完全不同,CommonJS 模块输出的是值的缓存,不存在动态更新

// 500 毫秒后,变量 age 的值会变成 35,在其他所有引用当前模块的模块中,这个值也会是 35
export let age = 20;
setTimeout(() => age = 35, 500);

import

import 用于导入模块,它在编译阶段,也就是在代码运行之前就会执行

import {say, do} from './person.js'
say();
do();

你还可以为导入的变量指定别名:

import {getPathByFile as gp} from './utils.js'

如果你需要整体加载一个模块中的变量或函数,可以通过 import * from的写法来一次性导入整个模块的内容

// 分别导入
import {say, do} from './person.js'
// 整体导入
import * from './person.js'
say();
do();

export default

使用import 从另一个模块导入变量的时候,我们需要去查看这个模块内有哪些导出内容,但是,有时候为了方便,让我们不用查看模块内部就能加载模块的导出内容,我们可以使用export default命令,为模块指定默认的导出

// person.js
export default function () {
  console.log('Hello World');
}
// 导入的时候可以这样写,不用加 {}
import say from './person.js'

合并写法

有时候,你可能需要先导入一个模块,然后再导出这个模块,这是可以将 importexport 合并起来

import Layout from './components/Layout/Layout';
export {Layout}
import Button from './components/Button/Button';
export {Button}

// 合并写法
export Layout from './components/Layout/Layout';
export { default as Button } from './components/Button/Button';