前端模块化方案

前端模块化方案

前言

各位在看本片文章之前可以先阅读以下文章,了解下什么是模块化?为什么要模块化?模块化演进过程是怎么样的?什么是模块化规范?

Javascript模块化编程(一):模块的写法

Javascript模块化编程(二):AMD规范

Javascript模块化编程(三):require.js的用法

Javascript模块化编程(四):CommonJS规范

看过以上的博文之后,相信你对模块化有所了解,模块化的开发方式可以提高代码复用率,方便进行代码的管理。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。目前流行的js模块化规范有CommonJS、AMD、CMD以及ES6的模块系统。接下来先简单介绍各规范下的模块化实现方式。

前端模块化方案简介

一、CommonJS

​ Node.js是commonJS规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:module、exports、require、global。实际使用时,用module.exports定义当前模块对外输出的接口(不推荐直接用exports),用require加载模块。

//定义模块math.js
var basicNum = 0;
function add(a, b) {
    return a + b;
}
module.exports = { //在这里写上需要向外暴露的函数、变量
    add: add,
    basicNum: basicNum
}
 
//引用自定义的模块时,参数包含路径,可省略.js
var math = require('./math');
math.add(2, 5);
 
//引用核心模块时,不需要带路径
var http = require('http');
http.createService(...).listen(3000);

​ commonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。

二、AMD和require.js

AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。这里介绍用require.js实现AMD规范的模块化:用require.config()指定引用路径等,用define()定义模块,用require()加载模块。

首先我们需要引入require.js文件和一个入口文件main.js。main.js中配置require.config()并规定项目中用到的基础模块。

/** 网页中引入require.js及main.js **/
<script src="js/require.js" data-main="js/main"></script>
 
/** main.js 入口文件/主模块 **/
//首先用config()指定各模块路径和引用名
require.config({
    baseUrl: "js/lib",
    paths: {
        "jquery": "jquery.min",  //实际路径为js/lib/jquery.min.js
        "underscore": "underscore.min",
    }
});
//执行基本操作
require(["jquery","underscore"],function($,_){
    // some code here
});

​ 引用模块的时候,我们将模块名放在[]中作为reqiure()的第一参数;如果我们定义的模块本身也依赖其他模块,那就需要将它们放在[]中作为define()的第一参数。

//定义math.js模块
define(function () {
    var basicNum = 0;
    var add = function (x, y) {
        return x + y;
    };
    return {
        add: add,
        basicNum :basicNum
    };
});
//定义一个依赖underscore.js的模块
define(['underscore'],function(_){
    var classify = function(list){
        _.countBy(list,function(num){
            return num >30 ? 'old': 'young';
        })
    };
    return {
        classify :classify
    };
})
 
//引用模块,将模块放在[]内
require([jquery,math],function($,math){
    var sum = math.add(10,20);
    $("#sum").html(sum);
});

三、CMD和sea.js

require.js在申明依赖的模块时会在第一之间加载并执行模块内的代码:

define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
     // 等于在最前面声明并初始化了要用到的所有模块
    if (false) {
        // 即便没用到某个模块 b,但 b 还是提前执行了
        b.foo()
    }
});

​ CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。

/** AMD写法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
     // 等于在最前面声明并初始化了要用到的所有模块
    a.doSomething();
    if (false) {
        // 即便没用到某个模块 b,但 b 还是提前执行了
        b.doSomething()
    }
});
 
/** CMD写法 **/
define(function(require, exports, module) {
    var a = require('./a'); //在需要时申明
    a.doSomething();
    if (false) {
        var b = require('./b');
        b.doSomething();
    }
});
 
/** sea.js **/
// 定义模块 math.js
define(function(require, exports, module) {
    var $ = require('jquery.js');
    var add = function(a,b){
        return a+b;
    }
    exports.add = add;
});
// 加载模块
seajs.use(['math.js'], function(math){
    var sum = math.add(1+2);
});

四、ES6 Module

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
    return a + b;
};
export { basicNum, add };
 
/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
    ele.textContent = add(99 + basicNum);
}

​ 如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名。其实ES6还提供了export default命令,为模块指定默认输出,对应的import语句不需要使用大括号。这也更趋近于ADM的引用写法。

/** export default **/
//定义输出
export default { basicNum, add };
//引入
import math from './math';
function test(ele) {
    ele.textContent = math.add(99 + math.basicNum);
}

​ ES6的模块不是对象,import命令会被 JavaScript 引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。也正因为这个,使得静态分析成为可能。

其他可以参考以下博文:

前端工程师必备:前端的模块化

前端模块化问题解答

前端模块化中module.exports、exports、export和export default,import和require区别与联系

一、首先搞清楚一个基本问题:

require导入,import module.exportsexports导出,都是与属于CommonJS模块规范!

import导入,exportexport default导出,都是属于ES6语法!

二、module.exports和exports的区别与联系

Node应用由模块组成,采用CommonJS模块规范。根据这个规范,每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。

普及下:

  • 在javascript中有2种作用域:全局作用域和函数作用域,在浏览器端, 全局作用域就是window对象的属性,函数作用域就是函数内部的对象属性。
  • 在node中,也有2种作用域:全局作用域和模块作用域,因此要想实现在nodejs中多个文件中分享变量,就必须定义成全局对象 (global)的属性, global定义的变量,在任何地方都可以使用,类似于浏览器端定义在全局 范围中的变量。

CommonJS规范规定,每个模块内部,module变量代表当前模块。如果使用node命令去console.log(module),你会发现这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。

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

上面代码通过module.exports输出变量x和函数addX

require方法用于加载模块。

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

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

看了刚刚这段commonjs规范上面的介绍可以知道module.exports 和 exports 的关系 :

内存结构示意图

其实exports变量是指向module.exports,也就是说exports = module.exports 都指向同一个内存地址,加载模块实际是加载该模块的module.exports

var exports = module.exports;
其实就是干了这么一件事
exports = module.exports = {};

于是我们可以直接在 exports 对象上添加方法,表示对外输出的接口,如同在module.exports上添加一样。注意,不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系。直接看例子明了。

// a.js
let a = 100;

exports.a = 200;
// 然后把a指向另外一个地址
exports = '另外的地址'

// b.js
// 引入模块

var example = require('./a.js')
console.log(example.a)  // 200

可以看出,require导出的内容是module.exports的内容,而不是exports的,简单来说,exports用来帮助module.exports操作内存中的数据。来看下nodejs中require的加载机制。

function require(...) {  
  var module = { exports: {} };
  ((module, exports) => {
    // 你的被引入代码 Start
    // var exports = module.exports = {}; (默认都有的)
    function some_func() {};
    exports = some_func;
    // 此时,exports不再挂载到module.exports,
    // export将导出{}默认对象
    module.exports = some_func;
    // 此时,这个模块将导出some_func对象,覆盖exports上的some_func    
     // 你的被引入代码 End
  })(module, module.exports);
 // 不管是exports还是module.exports,最后返回的还是module.exports 
  return module.exports;
}
console.log(exports); // {}  
console.log(module.exports);  // {}  
console.log(exports === module.exports);    // true  
console.log(exports == module.exports);        // true  
console.log(module);
/**
 Module {
  id: '.',
  exports: {},
  parent: null,
  filename: '/Users/larben/Desktop/demo.js',
  loaded: false,
  children: [],
  paths:
   [ '/Users/larben/Desktop/node_modules',
     '/Users/larben/node_modules',
     '/Users/node_modules',
     '/node_modules' ] }
 */

一句话概括重点:exports就是module.exports的引用,最简单避错的方法就是尽量都用 module.exports。

如果不太理解,提供以下博文作为参考:

module.exports和moudle和exports的区别很容易理解

exports 和 module.exports 的区别

四、export和export default的区别与联系

模块功能主要由:exportimport构成export导出模块的对外接口,import命令导入其他模块暴露的接口。

export其实和export default就是写法上面有点差别,一个是导出一个个单独接口,一个是默认导出一个整体接口。使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。这里就有一个简单写法不用去知道有哪些具体的暴露接口名,就用export default命令,为模块指定默认输出。

export可以这样写

// testA.js
var f = 'Miel';
var name = 'Jack';
var data= 1988;

export {f, name, data};

使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。

// main.js
import {f, name, data} from './testA';

export default可以这样写

// export-default.js
export default function () {
  console.log('foo');
}
// 或者写成

function foo() {
  console.log('foo');
}
export default foo;
// import-default.js
import customName from './export-default';
customName(); // 'foo'

下面比较一下export default和export 输出。

// 第一组
export default function car() { // 输出
  // ...
}

import car from 'car'; // 输入

// 第二组
export function car2() { // 输出
  // ...
};

import {car2} from 'car2'; // 输入

可以看到第一组是使用export default,import语句不需要使用大括号;第二组使用export,对应的import语句需要使用大括号,一个模块只能有一个默认输出,所以export default只能使用一次。

再看个例子直接就明白了

//a.js
export const a = '100'  // 方式一
const Stark = function(){ 
    console.log('im ironman')
}// 方式二

export { Stark }


// export default方式
const mark85 = 1000
export default mark85
// b.js
import { a, Stark } from './a.js';  // 引入export导出的数据
import ironman from './a.js' // 引入export default导出的数据
import * as hero from './a.js' // 集合成对象一并导出,es6语法的本意是想将 es6 模块的所有命名输出以及defalut输出打包成一个对象赋值给hero变量。as简单的说就是取一个别名,export中可以用,import中其实可以用:

console.log(a) // 100
Stark(); // im ironman
console.log(ironman) // 1000
console.log(hero.a) // 100
console.log(hero.default) // 1000

一句话重点:export 和 export default的区别:

  • 在一个文件或模块中,export、import可以有多个,export default仅有一个;
  • 通过export方式导出,在导入时要加{ },export default则不需要;
  • export能直接导出变量表达式,export default不行。

五、import和require的区别与联系
看了上面其实已经清楚了,import和require是分别属于ES6和CommonJS的两种导入模块的语法而已。

其他可以参考以下博文:

import、require、export、module.exports 混合使用详解

*彻底搞清楚javascript中的require、import和export

你可能感兴趣的:(前端进阶,Javascript)