关于javascript:前端模块化详解完整版

9次阅读

共计 12193 个字符,预计需要花费 31 分钟才能阅读完成。

前言

在 JavaScript 倒退初期就是为了实现简略的页面交互逻辑,寥寥数语即可;现在 CPU、浏览器性能失去了极大的晋升,很多页面逻辑迁徙到了客户端(表单验证等),随着 web2.0 时代的到来,Ajax 技术失去广泛应用,jQuery 等前端库层出不穷,前端代码日益收缩,此时在 JS 方面就会思考应用模块化标准去治理。
本文内容次要有了解模块化,为什么要模块化,模块化的优缺点以及模块化标准, 并且介绍下开发中最风行的 CommonJS, AMD, ES6、CMD 标准。本文试图站在小白的角度,用通俗易懂的笔调介绍这些枯燥无味的概念,心愿诸君浏览后,对模块化编程有个全新的意识和了解!

一、模块化的了解

1. 什么是模块?

  • 将一个简单的程序根据肯定的规定 (标准) 封装成几个块(文件), 并进行组合在一起
  • 块的外部数据与实现是公有的, 只是向内部裸露一些接口 (办法) 与内部其它模块通信

2. 模块化的进化过程

  • 全局 function 模式 : 将不同的性能封装成不同的全局函数

    • 编码: 将不同的性能封装成不同的全局函数
    • 问题: 净化全局命名空间, 容易引起命名抵触或数据不平安,而且模块成员之间看不出间接关系
function m1(){//...}
function m2(){//...}
  • namespace 模式 : 简略对象封装

    • 作用: 缩小了全局变量,解决命名抵触
    • 问题: 数据不平安(内部能够间接批改模块外部的数据)
let myModule = {
  data: 'www.baidu.com',
  foo() {console.log(`foo() ${this.data}`)
  },
  bar() {console.log(`bar() ${this.data}`)
  }
}
myModule.data = 'other data' // 能间接批改模块外部的数据
myModule.foo() // foo() other data

这样的写法会裸露所有模块成员,外部状态能够被内部改写。

  • IIFE 模式:匿名函数自调用(闭包)

    • 作用: 数据是公有的, 内部只能通过裸露的办法操作
    • 编码: 将数据和行为封装到一个函数外部, 通过给 window 增加属性来向外裸露接口
    • 问题: 如果以后这个模块依赖另一个模块怎么办?
// index.html 文件
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
    myModule.foo()
    myModule.bar()
    console.log(myModule.data) //undefined 不能拜访模块外部数据
    myModule.data = 'xxxx' // 不是批改的模块外部的 data
    myModule.foo() // 没有扭转
</script>
// module.js 文件
(function(window) {
  let data = 'www.baidu.com'
  // 操作数据的函数
  function foo() {
    // 用于裸露有函数
    console.log(`foo() ${data}`)
  }
  function bar() {
    // 用于裸露有函数
    console.log(`bar() ${data}`)
    otherFun() // 外部调用}
  function otherFun() {
    // 外部公有的函数
    console.log('otherFun()')
  }
  // 裸露行为
  window.myModule = {foo, bar} //ES6 写法
})(window)

最初失去的后果:

  • IIFE 模式加强 : 引入依赖

这就是古代模块实现的基石

// module.js 文件
(function(window, $) {
  let data = 'www.baidu.com'
  // 操作数据的函数
  function foo() {
    // 用于裸露有函数
    console.log(`foo() ${data}`)
    $('body').css('background', 'red')
  }
  function bar() {
    // 用于裸露有函数
    console.log(`bar() ${data}`)
    otherFun() // 外部调用}
  function otherFun() {
    // 外部公有的函数
    console.log('otherFun()')
  }
  // 裸露行为
  window.myModule = {foo, bar}
})(window, jQuery)
 // index.html 文件
  <!-- 引入的 js 必须有肯定程序 -->
  <script type="text/javascript" src="jquery-1.10.1.js"></script>
  <script type="text/javascript" src="module.js"></script>
  <script type="text/javascript">
    myModule.foo()
  </script>

上例子通过 jquery 办法将页面的背景色彩改成红色,所以必须先引入 jQuery 库,就把这个库当作参数传入。这样做除了保障模块的独立性,还使得模块之间的依赖关系变得显著

3. 模块化的益处

  • 防止命名抵触(缩小命名空间净化)
  • 更好的拆散, 按需加载
  • 更高复用性
  • 高可维护性

4. 引入多个 <script> 后呈现呈现问题

  • 申请过多

首先咱们要依赖多个模块,那样就会发送多个申请,导致申请过多

  • 依赖含糊

咱们不晓得他们的具体依赖关系是什么,也就是说很容易因为不理解他们之间的依赖关系导致加载先后顺序出错。

  • 难以保护

以上两种起因就导致了很难保护,很可能呈现牵一发而动全身的状况导致我的项目呈现重大的问题。
模块化诚然有多个益处,然而一个页面须要引入多个 js 文件,就会呈现以上这些问题。而这些问题能够通过模块化标准来解决,上面介绍开发中最风行的 commonjs, AMD, ES6, CMD 标准。

二、模块化标准

1.CommonJS

(1)概述

Node 利用由模块组成,采纳 CommonJS 模块标准。每个文件就是一个模块,有本人的作用域。在一个文件外面定义的变量、函数、类,都是公有的,对其余文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块须要提前编译打包解决。

(2)特点

  • 所有代码都运行在模块作用域,不会净化全局作用域。
  • 模块能够屡次加载,然而只会在第一次加载时运行一次,而后运行后果就被缓存了,当前再加载,就间接读取缓存后果。要想让模块再次运行,必须革除缓存。
  • 模块加载的程序,依照其在代码中呈现的程序。

(3)根本语法

  • 裸露模块:module.exports = valueexports.xxx = value
  • 引入模块:require(xxx), 如果是第三方模块,xxx 为模块名;如果是自定义模块,xxx 为模块文件门路

此处咱们有个疑难:CommonJS 裸露的模块到底是什么? CommonJS 标准规定,每个模块外部,module 变量代表以后模块。这个变量是一个对象,它的 exports 属性(即 module.exports)是对外的接口。加载某个模块,其实是加载该模块的 module.exports 属性

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

下面代码通过 module.exports 输入变量 x 和函数 addX。

var example = require('./example.js');// 如果参数字符串以“./”结尾,则示意加载的是一个位于相对路径
console.log(example.x); // 5
console.log(example.addX(1)); // 6

require 命令用于加载模块文件。require 命令的基本功能是,读入并执行一个 JavaScript 文件,而后返回该模块的 exports 对象。如果没有发现指定模块,会报错

(4)模块的加载机制

CommonJS 模块的加载机制是,输出的是被输入的值的拷贝。也就是说,一旦输入一个值,模块外部的变动就影响不到这个值。这点与 ES6 模块化有重大差别(下文会介绍),请看上面这个例子:

// lib.js
var counter = 3;
function incCounter() {counter++;}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

下面代码输入外部变量 counter 和改写这个变量的外部办法 incCounter。

// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;

console.log(counter);  // 3
incCounter();
console.log(counter); // 3

下面代码阐明,counter 输入当前,lib.js 模块外部的变动就影响不到 counter 了。这是因为 counter 是一个原始类型的值,会被缓存。除非写成一个函数,能力失去外部变动后的值

(5)服务器端实现

①下载安装 node.js

②创立我的项目构造

留神:用 npm init 主动生成 package.json 时,package name(包名)不能有中文和大写

|-modules
  |-module1.js
  |-module2.js
  |-module3.js
|-app.js
|-package.json
  {
    "name": "commonJS-node",
    "version": "1.0.0"
  }

③下载第三方模块

npm install uniq --save // 用于数组去重

④定义模块代码

//module1.js
module.exports = {
  msg: 'module1',
  foo() {console.log(this.msg)
  }
}
//module2.js
module.exports = function() {console.log('module2')
}
//module3.js
exports.foo = function() {console.log('foo() module3')
}
exports.arr = [1, 2, 3, 3, 2]
// app.js 文件
// 引入第三方库,应该搁置在最后面
let uniq = require('uniq')
let module1 = require('./modules/module1')
let module2 = require('./modules/module2')
let module3 = require('./modules/module3')

module1.foo() //module1
module2() //module2
module3.foo() //foo() module3
console.log(uniq(module3.arr)) //[1, 2, 3]

⑤通过 node 运行 app.js

命令行输出node app.js,运行 JS 文件

(6)浏览器端实现(借助 Browserify)

①创立我的项目构造

|-js
  |-dist // 打包生成文件的目录
  |-src // 源码所在的目录
    |-module1.js
    |-module2.js
    |-module3.js
    |-app.js // 利用主源文件
|-index.html // 运行于浏览器上
|-package.json
  {
    "name": "browserify-test",
    "version": "1.0.0"
  }

②下载 browserify

  • 全局: npm install browserify -g
  • 部分: npm install browserify –save-dev

③定义模块代码(同服务器端)

留神:index.html文件要运行在浏览器上,须要借助 browserify 将 app.js 文件打包编译,如果间接在 index.html 引入 app.js 就会报错!

④打包解决 js

根目录下运行browserify js/src/app.js -o js/dist/bundle.js

⑤页面应用引入

在 index.html 文件中引入<script type="text/javascript" src="js/dist/bundle.js"></script>

2.AMD

CommonJS 标准加载模块是同步的,也就是说,只有加载实现,能力执行前面的操作。AMD 标准则是非同步加载模块,容许指定回调函数。因为 Node.js 次要用于服务器编程,模块文件个别都曾经存在于本地硬盘,所以加载起来比拟快,不必思考非同步加载的形式,所以 CommonJS 标准比拟实用。然而,如果是浏览器环境,要从服务器端加载模块,这时就必须采纳非同步模式,因而浏览器端个别采纳 AMD 标准。此外 AMD 标准比 CommonJS 标准在浏览器端实现要来着早。

(1)AMD 标准根本语法

定义裸露模块:

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

引入应用模块:

require(['module1', 'module2'], function(m1, m2){应用 m1/m2})

(2)未应用 AMD 标准与应用 require.js

通过比拟两者的实现办法,来阐明应用 AMD 标准的益处。

  • 未应用 AMD 标准
// dataService.js 文件
(function (window) {
  let msg = 'www.baidu.com'
  function getMsg() {return msg.toUpperCase()
  }
  window.dataService = {getMsg}
})(window)
// alerter.js 文件
(function (window, dataService) {
  let name = 'Tom'
  function showMsg() {alert(dataService.getMsg() + ',' + name)
  }
  window.alerter = {showMsg}
})(window, dataService)
// main.js 文件
(function (alerter) {alerter.showMsg()
})(alerter)
// index.html 文件
<div><h1>Modular Demo 1: 未应用 AMD(require.js)</h1></div>
<script type="text/javascript" src="js/modules/dataService.js"></script>
<script type="text/javascript" src="js/modules/alerter.js"></script>
<script type="text/javascript" src="js/main.js"></script>

最初失去如下后果:

这种形式毛病很显著:首先会发送多个申请,其次引入的 js 文件程序不能搞错,否则会报错!

  • 应用 require.js

RequireJS 是一个工具库,次要用于客户端的模块治理。它的模块治理恪守 AMD 标准,RequireJS 的根本思维是,通过 define 办法,将代码定义为模块;通过 require 办法,实现代码的模块加载
接下来介绍 AMD 标准在浏览器实现的步骤:

①下载 require.js, 并引入

  • 官网: http://www.requirejs.cn/
  • github : https://github.com/requirejs/requirejs

而后将 require.js 导入我的项目: js/libs/require.js

②创立我的项目构造

|-js
  |-libs
    |-require.js
  |-modules
    |-alerter.js
    |-dataService.js
  |-main.js
|-index.html

③定义 require.js 的模块代码

// dataService.js 文件
// 定义没有依赖的模块
define(function() {
  let msg = 'www.baidu.com'
  function getMsg() {return msg.toUpperCase()
  }
  return {getMsg} // 裸露模块
})
//alerter.js 文件
// 定义有依赖的模块
define(['dataService'], function(dataService) {
  let name = 'Tom'
  function showMsg() {alert(dataService.getMsg() + ',' + name)
  }
  // 裸露模块
  return {showMsg}
})
// main.js 文件
(function() {
  require.config({
    baseUrl: 'js/', // 根本门路 出发点在根目录下
    paths: {
      // 映射: 模块标识名: 门路
      alerter: './modules/alerter', // 此处不能写成 alerter.js, 会报错
      dataService: './modules/dataService'
    }
  })
  require(['alerter'], function(alerter) {alerter.showMsg()
  })
})()
// index.html 文件
<!DOCTYPE html>
<html>
  <head>
    <title>Modular Demo</title>
  </head>
  <body>
    <!-- 引入 require.js 并指定 js 主文件的入口 -->
    <script data-main="js/main" src="js/libs/require.js"></script>
  </body>
</html>

④页面引入 require.js 模块:

在 index.html 引入 <script data-main="js/main" src="js/libs/require.js"></script>

此外在我的项目中如何引入第三方库?只需在下面代码的根底稍作批改:

// alerter.js 文件
define(['dataService', 'jquery'], function(dataService, $) {
  let name = 'Tom'
  function showMsg() {alert(dataService.getMsg() + ',' + name)
  }
  $('body').css('background', 'green')
  // 裸露模块
  return {showMsg}
})
// main.js 文件
(function() {
  require.config({
    baseUrl: 'js/', // 根本门路 出发点在根目录下
    paths: {
      // 自定义模块
      alerter: './modules/alerter', // 此处不能写成 alerter.js, 会报错
      dataService: './modules/dataService',
      // 第三方库模块
      jquery: './libs/jquery-1.10.1' // 留神:写成 jQuery 会报错
    }
  })
  require(['alerter'], function(alerter) {alerter.showMsg()
  })
})()

上例是在 alerter.js 文件中引入 jQuery 第三方库,main.js 文件也要有相应的门路配置。
小结:通过两者的比拟,能够得出AMD 模块定义的办法十分清晰,不会净化全局环境,可能分明地显示依赖关系。AMD 模式能够用于浏览器环境,并且容许非同步加载模块,也能够依据须要动静加载模块。

3.CMD

CMD 标准专门用于浏览器端,模块的加载是异步的,模块应用时才会加载执行。CMD 标准整合了 CommonJS 和 AMD 标准的特点。在 Sea.js 中,所有 JavaScript 模块都遵循 CMD 模块定义标准。

(1)CMD 标准根本语法

定义裸露模块:

// 定义没有依赖的模块
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
})

引入应用模块:

define(function (require) {var m1 = require('./module1')
  var m4 = require('./module4')
  m1.show()
  m4.show()})

(2)sea.js 简略应用教程

①下载 sea.js, 并引入

  • 官网: http://seajs.org/
  • github : https://github.com/seajs/seajs

而后将 sea.js 导入我的项目: js/libs/sea.js

②创立我的项目构造

|-js
  |-libs
    |-sea.js
  |-modules
    |-module1.js
    |-module2.js
    |-module3.js
    |-module4.js
    |-main.js
|-index.html

③定义 sea.js 的模块代码

// module1.js 文件
define(function (require, exports, module) {
  // 外部变量数据
  var data = 'atguigu.com'
  // 外部函数
  function show() {console.log('module1 show()' + data)
  }
  // 向外裸露
  exports.show = show
})
// module2.js 文件
define(function (require, exports, module) {
  module.exports = {msg: 'I Will Back'}
})
// module3.js 文件
define(function(require, exports, module) {
  const API_KEY = 'abc123'
  exports.API_KEY = API_KEY
})
// module4.js 文件
define(function (require, exports, module) {// 引入依赖模块(同步)
  var module2 = require('./module2')
  function show() {console.log('module4 show()' + module2.msg)
  }
  exports.show = show
  // 引入依赖模块(异步)
  require.async('./module3', function (m3) {console.log('异步引入依赖模块 3' + m3.API_KEY)
  })
})
// main.js 文件
define(function (require) {var m1 = require('./module1')
  var m4 = require('./module4')
  m1.show()
  m4.show()})

④在 index.html 中引入

<script type="text/javascript" src="js/libs/sea.js"></script>
<script type="text/javascript">
  seajs.use('./js/modules/main')
</script>

最初失去后果如下:

4.ES6 模块化

ES6 模块的设计思维是尽量的动态化,使得编译时就能确定模块的依赖关系,以及输出和输入的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些货色。比方,CommonJS 模块就是对象,输出时必须查找对象属性。

(1)ES6 模块化语法

export 命令用于规定模块的对外接口,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 命令的时候,用户须要晓得所要加载的变量名或函数名,否则无奈加载。为了给用户提供方便,让他们不必浏览文档就能加载模块,就要用到 export default 命令,为模块指定默认输入。

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

模块默认输入, 其余模块加载该模块时,import 命令能够为该匿名函数指定任意名字。

(2)ES6 模块与 CommonJS 模块的差别

它们有两个重大差别:

① CommonJS 模块输入的是一个值的拷贝,ES6 模块输入的是值的援用

② CommonJS 模块是运行时加载,ES6 模块是编译时输入接口

第二个差别是因为 CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种动态定义,在代码动态解析阶段就会生成。

上面重点解释第一个差别,咱们还是举下面那个 CommonJS 模块的加载机制例子:

// lib.js
export let counter = 3;
export function incCounter() {counter++;}
// main.js
import {counter, incCounter} from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

ES6 模块的运行机制与 CommonJS 不一样。ES6 模块是动静援用,并且不会缓存值,模块外面的变量绑定其所在的模块

(3) ES6-Babel-Browserify 应用教程

简略来说就一句话:应用 Babel 将 ES6 编译为 ES5 代码,应用 Browserify 编译打包 js

①定义 package.json 文件

 {
   "name" : "es6-babel-browserify",
   "version" : "1.0.0"
 }

②装置 babel-cli, babel-preset-es2015 和 browserify

  • npm install babel-cli browserify -g
  • npm install babel-preset-es2015 –save-dev
  • preset 预设(将 es6 转换成 es5 的所有插件打包)

③定义.babelrc 文件

  {"presets": ["es2015"]
  }

④定义模块代码

//module1.js 文件
// 别离裸露
export function foo() {console.log('foo() module1')
}
export function bar() {console.log('bar() module1')
}
//module2.js 文件
// 对立裸露
function fun1() {console.log('fun1() module2')
}
function fun2() {console.log('fun2() module2')
}
export {fun1, fun2}
//module3.js 文件
// 默认裸露 能够裸露任意数据类项,裸露什么数据,接管到就是什么数据
export default () => {console.log('默认裸露')
}
// app.js 文件
import {foo, bar} from './module1'
import {fun1, fun2} from './module2'
import module3 from './module3'
foo()
bar()
fun1()
fun2()
module3()

⑤ 编译并在 index.html 中引入

  • 应用 Babel 将 ES6 编译为 ES5 代码(但蕴含 CommonJS 语法) : babel js/src -d js/lib
  • 应用 Browserify 编译 js : browserify js/lib/app.js -o js/lib/bundle.js

而后在 index.html 文件中引入

 <script type="text/javascript" src="js/lib/bundle.js"></script>

最初失去如下后果:

此外第三方库 (以 jQuery 为例) 如何引入呢
首先装置依赖 npm install jquery@1
而后在 app.js 文件中引入

//app.js 文件
import {foo, bar} from './module1'
import {fun1, fun2} from './module2'
import module3 from './module3'
import $ from 'jquery'

foo()
bar()
fun1()
fun2()
module3()
$('body').css('background', 'green')

三、总结

  • CommonJS 标准次要用于服务端编程,加载模块是同步的,这并不适宜在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因而有了 AMD CMD 解决方案。
  • AMD 标准在浏览器环境中异步加载模块,而且能够并行加载多个模块。不过,AMD 标准开发成本高,代码的浏览和书写比拟艰难,模块定义形式的语义不顺畅。
  • CMD 标准与 AMD 标准很类似,都用于浏览器编程,依赖就近,提早执行,能够很容易在 Node.js 中运行。不过,依赖 SPM 打包,模块的加载逻辑并重
  • ES6 在语言规范的层面上,实现了模块性能,而且实现得相当简略,齐全能够取代 CommonJS 和 AMD 标准,成为浏览器和服务器通用的模块解决方案

作者:浪里行舟
链接:前端模块化详解 (完整版)
起源:github
著作权归作者所有。商业转载请分割作者取得受权,非商业转载请注明出处。

正文完
 0