前端模块化方式

1,282 阅读7分钟

前端模块化方式: IIFE(立即执行函数)、CommonJS、AMD、CMD、ES6、UMD

为什么会有前端模块化?

​ JavaScript一开始是为了实现简单地页面交互逻辑,然后随之浏览器性能逐渐提升,加上Ajax和NodeJS的出现,大量的前端库出现,前端的代码逐渐膨胀,因此就需要一个规范去管理JS代码,因此就出现了模块化的概念。

  1. 内部,外部的代码模块的管理和组织
  2. 模块源代码到目标代码的编译和转换

什么是模块?

  1. 将一个复杂的程序,依据一定的规范分装成几个文件或者代码块,并组合在一起
  2. 模块内部的数据和实现是私有的,只是向外部暴露出一些方法

前端模块化的方式

1. 定义全局函数Function

  • 实现: 将不同功能的代码封装成不同的全局函数
  • 缺点: 会污染全局的命名空间,容易出现命名冲突或者数据不安全的情况,而且模块成员之间看不出直接的关系
function m1(a, b) {}
function m2(c, d, e) {}

2. 命名空间namespace模式

  • 实现: 将对象简单封装
  • 作用: 减少了全局变量,避免了命名冲突问题
  • 问题: 所有的模块成员都被暴露,数据不安全,外部可以直接修改对象的数据
const module = {
    msg: "Hello World!",
    say() {
        console.log(this.msg);
    }
}

module.msg = "Hi Javascript!";
module.say();  // 输出 Hi Javascript!

3. IIFE模式 -- 现代模块实现的基⽯

  • 实现: 匿名函数自调用——闭包
  • 作用: 数据是私有的,外部只能通过暴露的方法进行操作
// module.js 文件下
(function(window) {
	let msg = "Hello world";
    
    function say() {
        console.log(msg);
    }
    
    function sing() {
        console.log("music ~~")
    }
    
    window.module = { say, sing }
})(window)

// index.html
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
    module.say() // Hello world
	console.log(module.msg) // undefined
	module.msg = "hi"
	module.say() // Hello world msg没有更改
	module.sing() // music ~~ 调用暴露的另一个函数
</script>

提问:如果IIFE需要引入JQuery模块呢?

// module.js 文件下
(function(window, $) {
	let msg = "Hello world";
    
    function say() {
        console.log(msg);
    }
    
    function redBg() {
        $('body').css('background', 'red')
    }
    
    window.module = { say, redBg }
})(window, $)

// index.html
// 注意js文件引入需要有一点的顺序
<script type="text/javascript" src="jquery-1.4.min.js"></script>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
    module.say() // Hello world
	console.log(module.msg) // undefined
	module.msg = "hi"
	module.say() // Hello world msg没有更改
	module.redBg() // 背景就被改成红色了
</script>

4. 模块化的好处

  • 避免命名冲突
  • 按需加载,更好的分离逻辑
  • 更高的复用性
  • 高可维护性

5. 多个script标签存在的问题

  1. 请求过多:依赖多个模块,则需要发送多个请求
  2. 依赖模糊:
    1. 引入需要分先后顺序,不清楚模块间具体依赖关系容易出错
    2. 牵一发而动全身,一个出错容易导致项目出现问题

因此需要一个规范来解决这些问题,从而就出现了后续的CommonJS,AMD, ES6,CMD的规范。

模块化规范

1. CommonJS

Node应用是由模块组成的,采用CommonJS模块规范。每个文件就是一个模块,有自己的作用域,内部变量、函数和类都是私有的,对其他文件不可见。

  • 在服务端,模块加载时运行时同步加载的
  • 在浏览器端,模块需要提前编译打包处理

1.1 特点

  • 所有代码都运行在模块作用域,不会污染全局作用域
  • 模块可以多次加载,但运行一次后会被缓存,后续加载直接读缓存结果;想再次运行需要清除缓存
  • 模块加载的顺序,按照其在代码中出现的顺序**(同步加载)**

1.2 基本语法

  • 暴露模块

    const value = 123
    
    module.exports = {
        val
    }
    // 或者
    exports.val = value
    

    问:CommonJS暴露的模块到底是什么?

    答: CommonJS规范规定,每个模块内部, module变量代表当前模块。这个变量是⼀个对象,它的exports属性(即module.exports)是对外的接 ⼝。加载某个模块,其实是加载该模块的module.exports属性。

  • 引入模块

    const module = require('xxx')
    // 1. 如果是第三方模块, xxx为模块名
    // 2. 如果是自定义模块, xxx为文件路径
    
    示例
    // module.js
    var val = 123;
    var add = function (a) {
     val = 1
     return a + val;
    };
    module.exports.x = val;
    module.exports.add = add;
    
    const module = require('./module.js')
    
    console.log(module.x) // 123
    module.add(1) 
    console.log(module.x) // 123
    

    运行一次后会被缓存,后续加载直接读缓存结果;想再次运行需要清除缓存

    CommonJS模块的加载机制是,输⼊的是被输出的值的拷⻉。也就是说,⼀旦输出⼀个值,模块内部的 变化就影响不到这个值。

1.3 服务端实现

  1. 安装node环境
  2. npm init
  3. 下载第三方包
  4. 即可以开始使用commonJs

1.4 浏览器端实现

Browserify: 使用该插件对代码进行解析,整理出代码中的模块依赖关系形成一个普通JS代码文件。因为浏览器中不支持require的模块化语法

  1. 下载browserify

    • 全局: npm install browserify -g
    • 局部: npm install browserify --save-dev
  2. 打包处理js

    browserify js/src/app.js -o js/dist/bundle.js
    
  3. 页面引入打包后的文件

2. AMD(Asynchronous Module Definition)

CommonJS是同步的,加载完成才能执行后面的操作。AMD则是非同步加载模块,允许指定回调函数

由于服务端一般都已经存在模板文件,所以加载较快,无需考虑非同步加载的方式;浏览器需要请求,所以更适合用AMD异步加载的方式。

AMD规范比CommonJS在浏览器实现更早。

2.1 基本语法

  • 暴露模块

    define(function () {
        return 模块
    })
    
    // 定义有依赖模块的模块
    define(['module1', 'module2'], function(m1, m2) {
        return 模块
    })
    
  • 引入模块

    require(['module1', 'module2'], function(m1, m2) {
        // 使用m1 / m2
    })
    
  • main.js 声明模块配置

    (function() {
       require.config({
           baseUrl: '', // 基本路径
           paths: {
             // 映射—— 模块标识名:路径
             module: 'src' // 注意不能写成xxx.js,会报错
           }
       }) 
       
        require(['module'], function(module) {
            // 使用module
        })
    })()
    

2.2 特点

  • AMD模块定义的⽅法⾮常清晰,不会污染全局环境,能够清楚地显示依赖关系。

  • AMD模式可以⽤于浏览器环境,并且允许⾮同步加载模块,也可以根据需要动态加载模块。

3. CMD(Common Module Definition)

CMD规范专⻔⽤于浏览器端,模块的加载是异步的,模块使⽤时才会加载执⾏。

CMD规范整合了 CommonJS和AMD规范的特点——异步加载,同步执行

3.1 基本语法

和AMD差别:

  • 使用了CommonJS的模板导出,而不是return 返回模块
  • 不用提前声明模块的依赖
define(function(require, exports, module){
 exports.xxx = value
 module.exports = value
})


//定义有依赖的模块
define(function(require, exports, module){
 //引⼊依赖模块(同步)
 var module2 = require('./module2')
 //引⼊依赖模块(异步)
 require.async('./module3', function (m3) {
 })
 //暴露模块
 exports.xxx = value
})

3.2 CMD实现

在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。

ES6模块化

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输⼊和输出的变量。

CommonJS 和 AMD 模块,都只能在运⾏时确定这些东⻄

1. 基本使用

  • 定义模块

    注意:ES6输出的是值的引用

    var basicNum = 0;
    var add = function (a, b) {
     return a + b;
    };
    export { basicNum, add };
    
    export var basicNum = 0;
    export function add (a, b) {
     return a + b;
    };
    
    export default function () {
     console.log('foo');
    }
    
  • 引用模块

    // 解构导入  需要知道所要加载的变量名或函数名,
    import { basicNum, add } from "xxx"
    // 指定名称  需要模板中有export default
    import foo from "xxx"
    

ES6 模块与 CommonJS 模块的差异

  1. CommonJS 模块输出的是⼀个值的拷⻉,ES6 模块输出的是值的引⽤
  2. CommonJS 模块是运⾏时加载,ES6 模块是编译时输出接⼝

2. ES6的实现

使⽤Babel将ES6编译为ES5代码,使⽤Browserify编译打包js。

  1. 安装babel-cli, babel-preset-es2015和browserify

    npm install babel-cli browserify -g
    npm install babel-preset-es2015 --save-dev
    
  2. 定义.babelrc⽂件

    {
        "presets": ["es2015"]
    }
    
  3. 定义模板代码

  4. 编译代码

    babel js/src -d js/lib
    browserify js/lib/app.js -o js/lib/bundle.js
    

UMD(Universal Module Definition)

javascript通⽤模块定义规范,让你的模块能在javascript所有运⾏环境中发挥作⽤。

意味着要同时满⾜CommonJS, AMD, CMD的标准

兼容所有的模块规范

(function(root, factory) {
 if (typeof module === 'object' && typeof module.exports === 'object')
 {
     console.log('是commonjs模块规范,nodejs环境')
     module.exports = factory();
 } else if (typeof define === 'function' && define.amd) {
     console.log('是AMD模块规范,如require.js')
     define(factory)
 } else if (typeof define === 'function' && define.cmd) {
     console.log('是CMD模块规范,如sea.js')
     define(function(require, exports, module) {
         module.exports = factory()
     })
 } else {
     console.log('没有模块环境,直接挂载在全局对象上')
     root.umdModule = factory();
 }
}(this, function() {
    return {
        name: '我是⼀个umd模块'
    }
}))

总结

  • CommonJS加载模块是同步的,主要用于服务端(运行时,导出值的拷贝
  • AMD异步加载模块,可以多模块并行加载,更适合浏览器环境;但是AMD规范开发成本较大
  • CMD规范与AMD相似,但是不用声明模块,依赖就近可以异步加载同步执行
  • ES6使用export导出,开发简单,完全可以取代CommonJS和AMD(编译时,导出引用
  • UMD是兼容CommonJS、AMD、CMD规范的实现