# # Javascript 模块化笔记

# # 简介

模块化就是将一个复杂的程序安装一定的规则封装成若干个模块,并组合在一起

模块的内部数据相对而言是私有的,只是向外暴露一些方法与外部其他模块通信

没有模块化之前,常常一个 JS 文件中会有很多功能的代码,不容易维护;使用其他第三方插件,也经常会有依赖的问题

现在常用的 Javascript 模块化规范有四种: CommonJSAMDCMDES6 module

AMDCMD 现在已经很少使用了 node.js 环境下一般使用 CommonJS 规范,如要使用 ES6 module ,需要配置 babel 浏览器端一般都是使用 ES6 module ,为了兼容低版本浏览器,一般会配置 babel

模块化的优势:

  • 代码分离成一个个小的模块,方便维护代码,按需加载
  • 用到什么功能就引入对应的模块,提高代码的复用性
  • 降低代码耦合度

# # 全局 Function

这种模式是将不同的功能封装成不同的全局函数

不同的功能模块写在不同的文件中,主模块或者 html 文件引入

  • module1.js 文件
var data1 = 'module1 data'

function func1() {
  console.log('module1 -- func1:' + data1)
}
function func2() {
  console.log('module1 -- func2:' + data1)
} 
  • module2.js 文件
var data2 = 'module2 data'

function func1() {
  console.log('module2 -- func1:' + data2)
} 
  • index.html 文件
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>module test</title>
</head>
<body>
  <script src="./module1.js"></script>
  <script src="./module2.js"></script>
  <script> func1()
    func2()
    data1 = 'new data' </script>
</body>
</html> 

以上代码可以看出,有以下缺点

  • 顶级变量(浏览器中是 windownodejs 中是 global )被污染了。
  • 不同模块没有隔离,变量和方法很容易引起命名冲突
  • 外部可以改变模块的变量、方法等

# # namespace

每个模块都输出一个对象。只要输出的对象名不同,可以解决命名冲突的问题。 但是顶级变量被污染和尾部可以改变模块的变量问题没有解决

  • module1.js 文件
const module1 = {
  data: 'module1 data',
  func1: function() {
    console.log('module1 -- func1:' + this.data)
  },
  func2: function() {
    console.log('module1 -- func2:' + this.data)
  }
} 
  • module2.js 文件
const module2 = {
  data: 'module2 data',
  func1: function() {
    console.log('module2 -- func1:' + this.data)
  }
} 
  • index.html 文件
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>module test</title>
</head>
<body>
  <script src="./module1.js"></script>
  <script src="./module2.js"></script>
  <script> module2.func1()
    module1.func2()
    module1.data = 'new data'
    module1.func1() </script>
</body>
</html> 

# # IIFE

IIFEimmediately-invoked function expression

使用匿名函数自调用的方式隔离。

模块中的数据是私有的,不在暴露出来。外部只能通过暴露的方法操作

  • module1.js 文件
!(function(w, $) {
  let data = 'module1 data'

  const func1 = function() {
    console.log($)
    console.log('module1 -- func1:' + data)
  }

  const func2 = function() {
    console.log('module1 -- func2:' + data)
  }

  // 修改data。这里就形成了闭包
  function changeData(param) {
    data = param
  }

  // 将用到的方法暴露出去
  w.module1 = { func1, func2, changeData }
})(window, jQuery) 
  • module2.js 文件
!(function(w) {
  let data = 'module2 data'
  function func1() {
    console.log('module2 -- func1:' + data)
  }

  w.module2 = { func1 }
})(window) 
  • index.html 文件
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>module test</title>
</head>
<body>
  <script src="./jquery-3.5.1.min.js"></script>
  <script src="./module1.js"></script>
  <script src="./module2.js"></script>
  <script> module2.func1()
    module1.func2()
    // 不能直接修改
    // module1.data = 'new data'
    module1.changeData('new data')
    module1.func1() </script>
</body>
</html> 

这种方式已经解决了上面提到的三个问题。

还存在一些问题

  • 一个页面需要引入多个 js 文件,请求过多
  • 比如好几个模块都需要引入 jQuery 。为了避免引入多次,可以选择在入口文件中引入。内部依赖关系混乱,看不出来谁依赖谁。容易混乱,难以维护

# # CommonJS

CommonJSNode.js 采用的服务器端模块的规范。

也可以用于浏览器端,但需要先使用 Browserify 进行编译打包

模块是同步加载的,在运行时加载,所以动态语句可写在判断中

在模块中 this 代表的是当前模块

使用 require 导入: require('path')require('./test.js')

使用 exports 或者 module.exports 导出: module.exports = valueexports.xxx = value

单个值导出;导出的是一个值的拷贝

exports & module.exports

  • exports 是对 module.exports 的一个引用,两者都是指向同一个对象。
  • 模块的 require 能看到的只有 module.exports 这个对象,它是看不到 exports 对象的;
  • 而我们在编写模块时用到的 exports 对象实际上只是对 module.exports 的引用。( exports = module.exports )。
  • 可以使用 exports.a = 'xxx'exports.b = function () {} 添加方法或属性,本质上它也添加在 module.exports 所指向的对象身上
  • 不能直接 exports = { a: 'xxx'} 这样子的意义就是将 exports 重新指向新的对象!它和 module.exports 就不是指向同一个对象
  • 一般都是直接使用 module.exports
  • module1.js 文件
let data = 'module1 data'

const func1 = function() {
  console.log('module1 -- func1:' + data)
}

const func2 = function() {
  console.log('module1 -- func2:' + data)
}

function changeData(param) {
  data = param
}

// 将用到的方法暴露出去 - module.exports
module.exports = { func1, func2, changeData } 
  • module2.js 文件
let data = 'module2 data'
function func1() {
  console.log('module2 -- func1:' + data)
}

// 将用到的方法暴露出去 - exports
exports.func1 = func1 
  • index.js 文件
const module1 = require('./module1')
const module2 = require('./module2')

module2.func1()
module1.func2()
// 不能直接修改
// module1.data = 'new data'
module1.changeData('new data')
module1.func1() 

# # 服务器端使用

  • 执行 node ./index.js

# # 浏览器端使用

  • npm init -y ,在项目中生成 package.json 文件

  • 安装 browserifynpm i browserify

  • 修改 package.json 文件的 scripts 部分,添加命令

{
  // ...
  "scripts": {
    // ...
    "build": "browserify ./index.js -o dist/bundle.js"
  },
  // ...
} 
  • 执行命令: npm run build ,在 dist/ 目录下生成 bundle.js 文件

  • 修改 index.html 文件

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>module test</title>
</head>
<body>
  <script src="./dist/bundle.js"></script>
</body>
</html> 

备注:

  • 只有在 node 环境下才可以直接使用未打包的 index.js 引入,因为在 node 环境下有 exportsrequire 这些全局方法。
  • 浏览器环境下使用的话,需要先将代码编译成浏览器可识别的代码

# # AMD

AMD (异步模块定义) 是 RequireJS 在推广过程中对模块定义的规范化产出。

代表框架: require.js

依赖前置 在定义的时候就要声明依赖模块。

AMD 规范是专门用于浏览器端的,模块加载是异步的

定义暴露模块的方法使用 define

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

引入模块使用 require

require(['module1', 'module2'], function(m1, m2) {
  // 使用m1、m2
}) 
  • 修改 module1.js 文件
define(['jquery'], function($) {
  let data = 'module1 data'

  const func1 = function() {
    console.log('module1 -- func1:' + data)
  }

  const func2 = function() {
    console.log('module1 -- func2:' + data)
  }

  function changeData(param) {
    data = param
  }

  // 将用到的方法暴露出去
  return { func1, func2, changeData }
}) 
  • 修改 module2.js 文件
define(function() {
  let data = 'module2 data'
  function func1() {
    console.log('module2 -- func1:' + data)
  }
  return { func1 }
}) 
  • 修改主模块 index.js 文件
// 应用入口,主模块
// 配置模块的路径
require.config({
  // 配置所有引入模块的公共路径(基本路径)
  baseUrl:'./',
  // 模块标识名与模块路径映射
  paths : {
    // 模块名称(一定要与引入的模块名称一一对应): 模块的路径
    // 一定不能写文件的后缀名,它会自动补全
    module1: 'module1',  
    module2: 'module2',
    // moduleDemo: 'modules/demo',
    // 库/框架自己实现模块化的功能,定义了暴露模块的名称
    jquery: 'jquery-3.5.1.min'
  }
})

// 主模块
require(['module1','module2'], function (m1, m2) {
  m2.func1()
  m1.func2()
  // 不能直接修改
  // m1.data = 'new data'
  m1.changeData('new data')
  m1.func1()
}) 
  • 修改 index.html 文件,引入 require.jsdata-main="./index" 表示入口文件是 ./index.js
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>module test</title>
</head>
<body>
  <script data-main="./index" src="./require.js"></script>
</body>
</html> 

# # CMD

CMD (通用模块定义) 是 SeaJS 在推广过程中对模块定义的规范化产出。是根据 CommonJSAMD 基础上提出的

代表框架: sea.js

就近依赖 在用到某个模式时再去 require

定义模块使用 define

define(function(require, exports, module) {
  // ...
}) 

引入模块使用 require 或者 require.async

const xx = require('./xxx')
const yy = require.async(['模块名'], function (模块暴露内容) {
  // ...
}) 

暴露模块使用 exports 或者 module.exports

html 文件引入 script 标签

<script src='sea.js'></script>
<script>seajs.use('app.js')</script> 
  • 修改 module1.js 文件
define(function(require, exports, module) {
  // require:引入依赖模块
  // exports:暴露模块
  // module:暴露模块

  let data = 'module1 data'

  const func1 = function() {
    console.log('module1 -- func1:' + data)
  }

  const func2 = function() {
    console.log('module1 -- func2:' + data)
  }

  // 修改data。这里就形成了闭包
  function changeData(param) {
    data = param
  }

  // 将用到的方法暴露出去
  module.exports = { func1, func2, changeData }
}) 
  • 修改 module2.js 文件
define(function(require, exports, module) {
  let data = 'module2 data'
  function func1() {
    console.log('module2 -- func1:' + data)
  }

  exports.func1 = func1
}) 
  • 修改 index.js 文件
define(function(require) {
  const module1 = require('./module1')
  const module2 = require('./module2')

  module2.func1()
  module1.func2()
  // 不能直接修改
  // module1.data = 'new data'
  module1.changeData('new data')
  module1.func1()
}) 
  • 修改 index.html 文件
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>module test</title>
</head>
<body>
  <script src="./sea.js"></script> 
  <script> seajs.use('./index') </script>
</body>
</html> 

# # ES6 module

旨在成为浏览器和服务器通用的模块解决方案,但还是主要专门针对浏览器端。

兼容性不好, webpack 会经过 Babel 转换为 CommonJS 规范

编译时输出接口,输出的是值的引用,可以导出多个

静态语句只能写在顶层

thisundefined

引入模块 import ;导出模块 export

  • 修改 src/module1.js 文件
let data = 'module1 data'

const func1 = function() {
  console.log('module1 -- func1:' + data)
}

const func2 = function() {
  console.log('module1 -- func2:' + data)
}

// 修改data。这里就形成了闭包
function changeData(param) {
  data = param
}

// 将用到的方法暴露出去
export default { func1, func2, changeData } 
  • 修改 src/module2.js 文件
let data = 'module2 data'

export function func1() {
  console.log('module2 -- func1:' + data)
}

// function func1() {
//   console.log('module2 -- func1:' + data)
// }
// export default { func1 } 
  • 修改 src/index.js 文件
import module1 from './module1'
// import module2 from './module2'
import { func1 as module2Func1 } from './module2'

module2Func1()
module1.func2()
// 不能直接修改
// module1.data = 'new data'
module1.changeData('new data')
module1.func1() 
  • 安装 babel-cli , babel-preset-es2015browserifynpm i babel-cli babel-preset-es2015 browserify

  • 添加 .babelrc 文件

{
  "presets": ["es2015"]
} 
  • 修改 package.json 文件
{
  // ...
  "scripts": {
    // ...
    "build": "babel src -d build/app.js && browserify build/app.js -o dist/bundle.js"
  },
  // ...
} 
  • 执行命令 npm run build ,会在 dist/ 目录下生成 bundle.js 文件

  • 修改 index.html 文件

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>module test</title>
</head>
<body>
  <script src="./dist/bundle.js"></script> 
</body>
</html>