JavaScript模块化开发(简述)

1、什么是模块化

随着网页越来越复杂,我们在在网页中引入的js文件也就越来越多,单纯的引入js文件已经满足不了我们的需求了。因为我们需要团队开发,模块的分离。
在Java中有一个重要带概念——package,逻辑上相关的代码组织到同一个包内,包内是一个相对独立的王国,不用担心命名冲突什么的,那么外部如果使用呢? 直接import对应的package即可 import java.util.ArrayList; 遗憾的是JavaScript在设计时定位原因,没有提供类似的功能, 开发者需要模拟出类似的功能,来隔离、组织复杂的JavaScript代码,我们称为模块化。模块化可以处理某些问题时,按照一种分类或者思想对功能进行模块化管理和使用。

2、模拟前端模块化的演变

平时我们把功能写在普通函数中

function fn () {
            
}
function fn2 () {
                
}

缺点:容易造成函数命名冲突

为了解决命名冲突,可以用对象来封装函数

var MyModule = new Object({
        count : 0,
        fn1 : function (num) {
                return count += num;
        },
        fn2 : function (num) {
                return count -= num;
        }
});

缺点:外部可以通过Mymodule.count来操作count属性

匿名函数闭包(这种方式已经是模块化编程的雏形了,能够实现功能的封装,也能够隐藏私有变量)

var MyModule = (function () {
    var count = 0;
    var fn1 = function (num) {
        return count += num;
    }
    var fn2 = function (num) {
        return count -= num;
    }
    return {
        fn1 : fn1,
        fn2 : fn2
    }
})();

缺点:没有导入其他模块的能力

经过以上分析,如果我们要设计一个模块化编程思想需要具备以下能力才叫做模块化。

什么叫做模块化?????

  1. 具有定义封装的能力
  2. 具有可以引入其他新模块的能力
  3. 具有可以暴露该模块内部某些接口的能力

3、模块化的一些规范

CommonJS

概述

服务器端的 Node.js 遵循CommonJS规范,每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块使用require(),其实是加载该模块的module.exports属性。

// a.js
var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;

上面代码通过 module.exports 输出变量x和函数 addX
require方法用于加载模块。

var a = require('./a.js');

console.log(a.x); // 5
console.log(a.addX(1)); // 6

特点

CommonJS模块的特点如下

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。

优缺点

优点

  • 服务器端模块便于重用

  • NPM 中已经有超过45万个可以使用的模块包

  • 简单并容易使用

缺点

  • 同步的模块加载方式不适合浏览器环境,同步意味着阻塞加载,浏览器资源是异步加载的
  • 不能非阻塞的并行加载多个模块

实现

  • 服务器端的 Node.js

  • Browserify ,浏览器端的 CommonJS 实现,可以使用 NPM 的模块,但是编译打包后的文件体积可能很大

  • modules-webmake ,类似Browserify,还不如 Browserify 灵活

  • wreq ,Browserify 的前身

规范具体使用

module

Node内部提供一个Module构造函数。所有模块都是Module的实例。

// 类似于这种构造函数
function Module(id, parent) {
  this.id = id;
  this.exports = {};
  this.parent = parent;
  // ...
}

每一个模块内部,都有一个module对象,我们可以在node环境下打印某个模块中的module对象进行测试。经过打印结果属性如下:

  • module.id 模块的识别符,通常是带有绝对路径的模块文件名。
  • module.filename 模块的文件名,带有绝对路径。
  • module.loaded 返回一个布尔值,表示模块是否已经完成加载。
  • module.parent 返回一个对象,表示调用该模块的模块。
  • module.children 返回一个数组,表示该模块要用到的其他模块。
  • module.exports 表示模块对外输出的值。

module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。

// cart.js
var pageName = "CartPage";

module.exports = {
    pageName: pageName   
}

在某个模块中如果使用到了cart模块,就可以这样使用

// 这个相当于引入该cart模块,cart变量就是cart模块导出的对象
var cart = require("../cart/cart");

console.log(cart);
console.log(cart.pageName);
exports

为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。

// 这个不需要咱们来写,是默认就有的。
var exports = module.exports;

要注意,不要覆盖掉exports的指向,否则会切断exports与module.exports的联系。

// 下面这几种方式都可以封装在同一个导出对象中
exports.hello = "hello";

module.exports.hi = "hi";

exports.say = function () {
    console.log('wwwwwww');
}

// 这种方式就是错误的。。直接改变了exports的指向
exports = function () {
    console.log("第二个测试方法。。。");
}

// 这种结果就会直接导出一个2222 而不会导出该模块的对象。
module.exports = "2222";

// 这样写也不好。。
exports = "eee";

总之:如果你觉得,exports与module.exports之间的区别很难分清,一个简单的处理方法,就是放弃使用exports,只使用module.exports

// 可以这样用以后
module.exports = {
  hi: function () {
    // xxx
  },
  name: 'larry'
}
require

Node使用CommonJS模块规范,内置的require命令用于加载模块文件。
require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。
require加载的模块不一定是一个对象,有可能该模块导出的是一个函数等。

// 某个模块是这样导出的,module.exports指向了某个函数
module.exports = function () {
  console.log("hello world")
}

// 然后在某个模块中在引入上面的模块
var xxx = require('./xxx');
// 这个xxx就是导出的函数,我们可以直接调用
xxx();

require 的加载规则
require命令用于加载文件,后缀名默认为.js

var foo = require('foo');
//  等同于
var foo = require('foo.js');

根据参数的不同格式,require命令去不同路径寻找模块文件。

  1. 如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。比如,require('/home/marco/foo.js')将加载/home/marco/foo.js
  2. 如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,require('./circle')将加载当前脚本同一目录的circle.js
  3. 如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)。

举例来说,脚本/home/user/projects/foo.js执行了require('bar.js')命令,Node会依次搜索以下文件。

  • /home/user/projects/node_modules/bar.js 自己项目的node_modules中查找
  • /home/user/node_modules/bar.js 父级的
  • /home/node_modules/bar.js 在父级的
  • /node_modules/bar.js 最根的
  1. 如果参数字符串不以“./“或”/“开头,而且是一个路径,比如require('example-module/path/to/file'),则将先找到example-module的位置,然后再以它为参数,找到后续路径。
  2. 如果指定的模块文件没有发现,Node会尝试为文件名添加.js、.json、.node后,再去搜索。.js件会以文本格式的JavaScript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析。
  3. 如果想得到require命令加载的确切文件名,使用require.resolve()方法。

1. 载入内置模块
Node的内置模块被编译为二进制形式,引用时直接使用名字而非文件路径。当第三方的模块和内置模块同名时,内置模块将覆盖第三方同名模块。因此命名时需要注意不要和内置模块同名。如获取一个http模块

var http = require('http')

返回的http即是实现了HTTP功能Node的内置模块。
2. 载入文件模块
绝对路径

var myMod = require('/home/base/my_mod')

或相对路径的

var myMod = require('./my_mod')

注意,这里忽略了扩展名“.js”,以下是对等的

var myMod = require('./my_mod')
var myMod = require('./my_mod.js')

3. 载入文件目录模块
可以直接require一个目录,假设有一个目录名为folder,如:

var myMod = require('./folder')

此时,Node将搜索整个folder目录,Node会假设folder为一个包并试图找到包定义文件package.json。如果folder目录里没有包含package.json文件,Node会假设默认主文件为index.js,即会加载index.js。如果index.js也不存在,那么加载将失败。

假设目录为:

folder
   index.js
   modA.js
   package.json

package.json定义如下

{
    "name": "pack",
    "main": "modA.js"
}

此时 require(‘./folder’) 将返回模块modA.js。如果package.json不存在,那么将返回模块index.js。如果index.js也不存在,那么将发生载入异常。
4. 载入node_modules里的模块
如果模块名不是路径,也不是内置模块,Node将试图去当前目录的node_modules文件夹里搜索。如果当前目录的node_modules里没有找到,Node会从父目录的node_modules里搜索,这样递归下去直到根目录。
5. 自动缓存已载入模块
对于已加载的模块Node会缓存下来,而不必每次都重新搜索。

AMD

概述

Asynchronous Module Definition规范其实只有一个主要接口,efine(id?, dependencies?, factory),它要在声明模块的时候指定所有的依赖dependencies,并且还要当做形参传到factory`中,对于依赖的模块提前执行,依赖前置。AMD是提前执行,它本身依赖的模块会比它本身提前加载完成,而CMD是依赖执行 当我们的代码执行到require这一步的时候才会引进我们的所要依赖的模块,CMD跟AMD非常的相似,但是语法跟commonjs由非常相似

使用

定义模块

define('module', ['dep1', 'dep2'], function (d1, d2) {
    return {}
});

引入模块

require(['module', '../file'], funtion(module, file) {
    // ...
})

优缺点

优点

  • 适合在浏览器环境中异步加载模块
  • 可以并行加载多个模块

缺点

  • 提高了开发成本,代码的阅读和书写比较困难,模块定义方式的语义不顺畅
  • 不符合通用的模块化思维方式,是一种妥协的实现

实现

CMD

概述

Common Module Definition规范和 AMD 很相似,尽量保持简单,并与 CommonJS 和 Node.js 的 Modules 规范保持了很大的兼容性。

使用

定义

define(function (require, exports, module) {
    var $ = require('jquery');
    var Sprinning = require('./spinning');
    exports.test1 = {};
    module.exports = {};
});

引入

define(function (require, exports, module) {
    var $ = require('jquery');
    $('div')...
});

优缺点

优点

  • 依赖就近,延迟执行
  • 可以很容易在Node.js中运行

缺点

  • 依赖SPM打包,模块的加载逻辑偏重

实现

ES6 模块

EcmaScript6 标准增加了 JavaScript 语言层面的模块体系定义。ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。

使用

定义

export default {
    a: 10,
    b: function () {
        // ...
    }
}

引入

import 'jquery';
import Vue from 'vue'
import { render } from 'react-dom'

优缺点

优点

  • 容易进行静态分析
  • 面向未来的ECMAScript标准

缺点

  • 原生浏览器还没有实现标准
  • 全新的命令字,新版的Node.js才支持

实现

Babel

    原文作者:Roc_zhou
    原文地址: https://www.jianshu.com/p/382463f6f64d
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞