My Javascript modularity notes
# # Javascript 模块化笔记
# # 简介
模块化就是将一个复杂的程序安装一定的规则封装成若干个模块,并组合在一起
模块的内部数据相对而言是私有的,只是向外暴露一些方法与外部其他模块通信
没有模块化之前,常常一个 JS
文件中会有很多功能的代码,不容易维护;使用其他第三方插件,也经常会有依赖的问题
现在常用的 Javascript
模块化规范有四种: CommonJS
、 AMD
、 CMD
、 ES6 module
。
AMD
、 CMD
现在已经很少使用了 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>
以上代码可以看出,有以下缺点
- 顶级变量(浏览器中是
window
,nodejs
中是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
IIFE
: immediately-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
CommonJS
是 Node.js
采用的服务器端模块的规范。
也可以用于浏览器端,但需要先使用 Browserify
进行编译打包
模块是同步加载的,在运行时加载,所以动态语句可写在判断中
在模块中 this
代表的是当前模块
使用 require
导入: require('path')
、 require('./test.js')
使用 exports
或者 module.exports
导出: module.exports = value
、 exports.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
文件 -
安装
browserify
:npm 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
环境下有exports
、require
这些全局方法。- 浏览器环境下使用的话,需要先将代码编译成浏览器可识别的代码
# # 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.js
。data-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
在推广过程中对模块定义的规范化产出。是根据 CommonJS
和 AMD
基础上提出的
代表框架: 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
规范
编译时输出接口,输出的是值的引用,可以导出多个
静态语句只能写在顶层
this
是 undefined
引入模块 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-es2015
和browserify
:npm 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>