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>

创作不易,若本文对你有帮助,欢迎打赏支持作者!

 分享给好友: