前言

在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.jsvar 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); // 5console.log(example.addX(1)); // 6

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

(4)模块的加载机制

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

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

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

// main.jsvar counter = require('./lib').counter;var incCounter = require('./lib').incCounter;console.log(counter);  // 3incCounter();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.jsmodule.exports = {  msg: 'module1',  foo() {    console.log(this.msg)  }}
//module2.jsmodule.exports = function() {  console.log('module2')}
//module3.jsexports.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() //module1module2() //module2module3.foo() //foo() module3console.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.jsexport default function () {  console.log('foo');}
// import-default.jsimport customName from './export-default';customName(); // 'foo'

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

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

它们有两个重大差别:

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

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

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

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

// lib.jsexport let counter = 3;export function incCounter() {  counter++;}// main.jsimport { counter, incCounter } from './lib';console.log(counter); // 3incCounter();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
著作权归作者所有。商业转载请分割作者取得受权,非商业转载请注明出处。