1. 模块化概述
1.1 什么是模块化?
-
将程序文件依据一定规则
拆分成多个文件,这种编码方式就是模块化的编程方式。 -
拆分出来
每一个文件就是一个模块,模块中的实际都是私有的,模块之间互相隔离隔离。 -
同时也能通过一些手段,可以把模块内的指定数据
交出去供其他模块使用。
1.2 为什么需要模块化
随着应用的复杂的越来越高,其代码量和文件数量都会几句增加,会逐渐引发一下问题。
-
全局污染问题
-
依赖混乱问题
-
数据安全问题
2. 有哪些模块化规范
历史背景(了解): 2009年,随着 Node.js 的出现, JavaScript 在服务器端的应用逐渐增多,让 Node.js 的代码更好维护,就必须要制定一种 Node.js 环境下的模块化规范,来自 Mozilla 的设计师 Kevin Dangoor 提出了 CommonJS 规范 (CommonJs 初期的名字叫 ServerJS),随后Node.js 社区采纳了这一规范。
随着时间的推移,针对 JavaScript 的不同运行环境,相继出现了多种模块化规范, 按实际顺序,分别为:
CommonJS 服务端应用广泛
AMD
CMD
ES 浏览器端应用广泛
3. 导入与导出的概念
模块化的核心思想就是: 模块之间是隔离的,通过导入和导出进行数据和功能的共享。
-
导出(暴露):模块公开其内部的一部分(如变量、函数等),使这些内容可以被其他模块使用。
-
导入(引入):模块引入和使用其他模块导出的内容,以重用代码和功能。
4. CommonJS 规范
4.1 初步体验
1. 创建 School.js
const name = '蓝翔',
const slogan = '挖掘机技术哪家强',
function getTel(){
return '037-2535685';
},
function getCities(){
return ['北京','上海','深圳','成都','武汉','西安']
}
// 通过给 exports 对象添加属性的方式, 来导出数据(注意:此处没有导出getCities)
exports.name = name,
exports.slogan = slogan,
exports.getTel = getTel
}
2. 创建 student.js
const name = '张三',
const motto = '练习时长两年半',
function getTel () {
return '13724521421'
}
function getHobby() {
return ['唱','跳','RAP','打篮球']
// 通过给 exports 对象添加属性的方式, 来导出数据(注意:此处没有导出getCities)
exports.name = name,
exports.motto = motto,
exports.getTel = getTel
// 方法二
// module.exports = {name, motto, getTel}
}
3. 创建 index.js
// 引入 school 模块暴露的所有内容
const school = require ('./school')
// 引入 student 模块暴露的所有内容
const student = require (./student)
4.2 导出数据
在CommonJS 表准中,导出数据有两种方式:
- 方式一: module.exports = value
- 方式er: exports.name = value
注意点:
- 每个模块的内部的:this、 exports、 modules.exports 在初始时,都指向同一个空对象,该空对象就是当前模块导出的数据。
如图:
- 论如何修改导出对象,最终导出的都是 module.exports 的值。
- exports 对 odule.exports 初始引用,仅为了方便给导出象添加属性,所以不能使用 exports = value 形式导出数据,但是可以使用 odule.exports = xxxx 出数据
4.3 导入数据
在 CommonJS 模块标准化中, 使用内置的 require 函数进行导入数据
// 直接引入模块
const school = require('./school')
// 引入 同时解构出要用的数据
const { name, slogan, getTel } = require('./school')
// 引入同时结构 + 重命名
const {name:stuName,motto,getTel:stuTel} = require('./student')
4.4 exports 和module.exports的使用误区
时刻谨记,require() 模块时,得到的永远是 module.exports 指向的对象
情况一:
exports.username = 'zs'
module.exports = {
age:22,
gender: '男'
}
得到是 {age:22,gender: '男'}
情况二:
module.exports.username = 'zs'
exports = {
age:22,
gender: '男'
}
得到是 {username :'zs'}
情况三:
exports.username = 'zs'
module.exports.gender = '男'
得到是 {username :'zs',gender:'男'}
情况四:
exports = {
username:'zs',
gender: '男'
}
exports = module.exports
module.exports.age= 22
得到是 {username :'zs',gender: '男',age:22 }
注意点:为了防止混乱,建议大家不要在同一个模块中同时使用 exports 和module.exports
4.5 一个 JS 模块在执行时,是被包裹在一个内置函数中执行的,所以每个模块都有自己的作用域,可以通过如果下方验证这一说法:
console.log (arguments);
console.log (arguments.callee.toString())
内置函数的大致形式如下
function (exports,require,module,_filename,_dirname) {
/******/
}
函数体形参解释
-
exports: 是一个全局对象,用于向外部暴露模块内的方法或属性。当你使用module.exports或直接修改exports对象时,实际上就是在修改这个对象的内容,以便其他模块可以通过require加载当前模块时访问到这些内容。 -
require: 是 Node.js 中的一个内置函数,用于加载模块。通过require可以引入其他模块的导出内容。 -
module: 是一个全局对象,表示当前模块的信息。它有一个exports属性,指向exports对象。你可以通过module.exports来指定模块的导出内容。 -
__filename: 是一个只读的全局变量,表示当前执行脚本的绝对路径。 -
__dirname: 同样是一个只读的全局变量,表示当前执行脚本所在目录的绝对路径。
4.6 浏览器端运行
Node.js 默认是支持 CommonJS 规范的,但浏览器端不支持,所以需要经过编译,步骤如下:
第一步: 全局安装 `browserify:npm i browerify -g
第二步: 编译
browserify index.js -o build.js
备注: index.js 是源文件, build.js 是输出的目标文件
第三步: 页面中引入使用
<script type = "text/javascript" src ="./build.js"></script>
5. ES6 模块化规范
ES6 模块化规范是一个官方标准的规范,它是在语言标准的层面上实现了模块化功能,是目前的模块化规范,且浏览器与服务端均支持该规范。
5.1 初步体验
1. 创建 school.js
// 导出name
export let name = { str: '蓝翔' },
// 导出 slogan
export const slogan = '挖掘机技术哪家强',
// 导出 Tel
export function getTel (){
return '037-9874521',
}
function getCities () {
return ['北京','上海','深圳','成都','武汉','西安']
}
2. 创建 student.js
// 导出name
export const name = '鸡你太美',
// 导出motto
export const motto = '练习时长两年半',
// 导出 getTel
export function getTel () {
return '13725854215';
}
function getHobby () {
return ['唱','跳','RAP','打篮球']
}
3. 页面中引入 index.js
<script type = "module" src = "./index.js"> </script>
5.2 Node.JS 中运行 ES6 模块
Node.js 中 运行 ES6 模块代码有两种方式:
方式一: 将 Javascript 文件后缀从 .js 改为 .mjs, Node 则会自动识别 ES6 模块。
方式二: 在 package.json 中设置 type 属性值为 module。
5.3 导出数据
ES6 模块化提供 3 种导出方式:
1. 分别导出
2. 统一导出
3. 默认导出
1. 分别导出
备注:在 【5.1 初步体验】 环节, 我们使用的导出方式就是 【分别导出】
// 导出 name
export let name = { str:'蓝翔' },
// 导出 slogan
export const slogan = '挖掘机技术哪家强',
// 导出 getTel
export function getTel () {
return '030-2543125';
}
2. 统一导出
const name = { str:'蓝翔' },
const slogan = '挖掘机技术哪家强',
function getTel () {
return '030-2543125';
}
function getCities () {
return ['北京','上海','深圳','成都','武汉','西安']
}
// 统一导出了: name,slogan,getTel
export {name,slogan,getTel}
// export {name,slogan,getTel} ====> {name,slogan, getTel} 非对象,是ES6原生的写法编译
3.默认导出
const name = '鸡你太美',
const motto = '练习时长两年半',
function getTel () {
return '13725854215';
}
function getHobby () {
return ['唱','跳','RAP','打篮球']
}
// 默认导出: name、motto、getTel
export default {name,motto,getTel}
// export default {name,motto,getTel} ======> {name,motto,getTel} 交出去的是 value,是对象
4. 混合导出
// 导出name ———— 分别导出
export const name = {str:'蓝翔'}
const slogan = '挖掘机技术哪家强'
function getTel (){
return '030-6553221'
}
function getCities(){
return ['北京','上海','深圳','成都','武汉','西安']
}
// 导出slogan ———— 统一导出
export {slogan}
// 导出getTel ———— 默认导出
export default getTel
5.4 导入数据
对于 ES6 模块化来说,使用何种导入方式,要根据导出方式决定。
1. 导入全部 (通用)
可以将模块中的所有导出内容整合到一个对象中
import * as school from './school.js'
* as school
*表示从模块中导入所有导出的成员。as school则是给这些导入的成员取一个别名school。这意味着所有的导出都会被包装进一个名为school的对象中。
2. 命名导入 (对应导出方式: 分别导出、统一导出)
导出数据的模块
// 分别导出
export const name = {str: '蓝翔'},
// 分别导出
export const slogan = '挖掘机技术哪家强',
function getTel(){
return '030-2562551'
}
function getCities () {
return ['北京','上海','深圳','成都','武汉','西安']
}
// 统一导出
export { getTel }
命名导入:
import { name, slogan, getTel } from './school.js'
通过 as 重命名
import {name as myName, slogan,getTel } from ',/school.js'
3. 默认导入 (对应导出方式: 默认导出)
导出数据的模块
const name = '鸡你太美',
const motto = '练习时长两年半',
function getTel (){
return '030-2565423';
},
function getHobby() {
return ['唱','跳','RAP','篮球'];
}
// 使用 默认导出的方式,导出一个对象,对象中包含着数据
export default { name, motto, getTel },
默认导入
// 默认导出的名字可以修改,不是必须为 student
import student from './student.js'
4. 命名导入与默认导入可以混合使用
导出数据的模块
// 分别导出
export const name = { str: '蓝翔'},
// 分别导出
export const slogan = '挖掘机技术哪家强',
function getTel() {
return '030-2658543';
},
function getCities () {
return ['北京','上海','深圳','成都','武汉','西安']
}
// 统一导出
export default getTel
命名导入 与 默认导入 混合使用, 且默认导入的内容必须放在前方
import getTel, { name,slogan } from './school.js'
5. 动态导入 (通用)
允许在运行时按需加载模块,返回值是一个 Promise
const school = await import ('./school.js')
console.log (school)
6. import 可以不接受任何数据
例如 只是让 mock.js 参与运行
import './mock.js'
总结:此时,我们感受到模块化确实解决了:
- 全局污染问题
- 依赖混乱问题
- 数据安全问题
5.5 数据引用问题
思考1 :如下代码的输出结果是什么? (不涉及模块化)
function count () {
let sum = 1,
function increment (){
sum += 1
}
return {sum,increment}
}
const {sum, increment} = count()
console.log(sum) // 1
increment ()
increment()
console.log(sum) // 1
思考2: 使用 CommonJS 规范,编写如下代码,输出结果是什么
let sum = 1
function increment (){
sum += 1
}
module.exports = {sum,increment}
const {sum,increment} = require('./count.js')
console.log(sum) // 输出 1
increment()
increment()
console.log(sum) // 输出 1
思考3 :使用 ES6 模块化规范,编写如下代码,输出结果是什么?
let sum = 1
function increment(){
sum += 1
}
export {sum,increment}
import {sum,increment} from './count.js'
console.log(sum) //1
increment()
increment()
console.log(sum) //3
使用原则:import 导出的 都当常量, 务必用 const 定义
总结
CommonJS vs ES6 Modules:
-
导出方式:
- CommonJS 使用
module.exports或exports来导出数据。 - ES6 Modules 使用
export关键字来导出数据。
- CommonJS 使用
-
引用方式:
- CommonJS 使用
require函数来引用数据。 - ES6 Modules 使用
import关键字来引用数据。
- CommonJS 使用
-
绑定行为:
- CommonJS 的导出是动态绑定的,这意味着修改
module.exports或exports会影响到所有已经引用该模块的地方。 - ES6 Modules 的导出是静态绑定的,修改导出后不会影响到已经导入该模块的地方。
- CommonJS 的导出是动态绑定的,这意味着修改
-
加载方式:
- CommonJS 是同步加载的,可能会导致阻塞。
- ES6 Modules 是异步加载的,不会阻塞页面渲染。
6. AMD 模块化规范 (了解)
6.1 环境准备
步骤1. 准备文件结构
文件说明:
-
JS文件夹中存放业务逻辑代码,main.js用于汇总各模块 -
libs中存放的是第三方库, 例如必须要用的require.js
步骤2. 在 index.html 中配置 main.js 与 require.js
<script data-main = "./js/main.js" src="./libs/require.js"></script>
步骤3. 在 main.js 中编写模块配置对象,注册所有模块。
/**
AMD_require.js 模块化的入口文件,要编写配置对象,并且有固定的写法
*/
requirejs.config({
// 基本路径
baseUrl:"./js",
// 模块标识名与模块路径映射
paths: {
school: "school",
student: "student",
}
})
6.2 导出数据
AMD 规范使用 define 函数来定义模块与导出数据
define (function(){
const name = '鸡你太美',
const motto = '练习时长两年半',
function getTel (){
return '030-2565423';
},
function getHobby() {
return ['唱','跳','RAP','篮球'];
}
// 导出数据
return {name, motto, getTel}
})
6.3 导入数据
如需导入数据,则需要 define 传入两个参数, 分别为:依赖项数组、回调函数
// ['school'] => 表示当前模块要依赖的模块名字
// function(school)=> 回调接收到的school 是模块导出的数据
define (['school'],function(school){
const name = { str: '蓝翔'},
const slogan = '挖掘机技术哪家强',
function getTel() {
return '030-2658543';
},
function getCities () {
return ['北京','上海','深圳','成都','武汉','西安']
}
// 导出数据
return {name, slogan, getTel}
})
6.4 使用模块
requirejs (['school, student'],funtion(school,student){
console.log('main',school),
console.lof('main', student)
}
7. CMD 模块化规范 (了解)
7.1 环境准备
步骤1.准备文件结构
文件说明:
-
JS文件夹中存放业务逻辑代码,main.js用于汇总各种模块 -
libs中存放的是第三方库,例如必须要用的sea.js
步骤2. 在 index.html 中配置 main.js 与 sea.js
<script type = "text/javascript" src="./libs/sea.js"> </script>
<script type = "text/javascript" src = "./modules/main.js"> </script>
7.2 导出数据
CMD 中同样使用 define 函数定义模块并导出数据
/**
收到的三个参数分别为:require, exports、module
1. require 用于引入其他模块
2. exports、module 用于导出数据
*/
define (function(require, exports, module){
const name = '蓝翔',
const slogan = '挖掘机技术哪家强',
function getTel () {
return '030-25124332'
},
function getCities () {
return ['北京','上海','深圳','成都','武汉','西安']
}
// 导出数据
module.exports = {name,slogan,getTel}
})
7.3 导入数据
CMD 规范中使用收到的 require 参数进行模块导入
define(function(require,exports,module){
const name = '鸡你太美'
const motto = '练习时长两年半'
// 引入school 模块
const school = require('./school')
console.log(school)
function getTel (){
return '13877889900'
}
function getHobby(){
return ['唱','跳','RAP','篮球']
}
exports.name = name
exports.motto = motto
exports.getTel = getTel
})
7.4使用模块
define(function(require){
const school = require('./school')
const student = require('./student')
// 使用模块
console.log(school)
console.log(student)
})
CommonJS、ES6、AMD、CMD 的特点和区别
1. CommonJS
特点
- 环境:主要用于服务器端(如 Node.js),尽管有一些工具(如 Browserify 或 Webpack)可以让 CommonJS 在浏览器环境中运行。
- 加载方式:同步加载。这意味着当你导入一个模块时,脚本会等待这个模块加载完成后再继续执行。这在服务器端是可行的,因为服务器端没有 UI 渲染的压力。
- 导出方式:使用
module.exports或exports对象来导出模块中的函数或值。 - 模块解析:Node.js 使用文件系统来查找模块,因此模块路径通常是绝对路径或相对路径。
优点
- 简单直接:只需要简单的
require语句即可加载模块。 - 易于理解:模块加载的顺序明确,开发者可以清晰地知道模块加载的顺序。
缺点
- 同步加载:在浏览器环境中可能导致页面加载缓慢,因为模块加载是同步的,会阻塞页面渲染。
- 不支持动态加载:无法在运行时动态加载模块。
2. ES6 Modules (ECMAScript 2015+)
特点
- 环境:适用于浏览器和服务器端。
- 加载方式:异步加载。使用
import语句来声明需要的模块,实际加载过程是异步的,不会阻塞后续代码执行。但在运行时才会开始加载,这使得 ES6 模块非常适合现代 Web 应用程序。 - 导出方式:使用
export语句来导出函数或值,使用import语句来引入。 - 静态分析:ES6 模块的设计鼓励静态分析,这对于编译器和打包工具来说非常重要,例如 Tree-Shaking。
优点
- 直接的语言级支持:ES6 模块是 ECMAScript 规范的一部分,所有现代浏览器都支持。
- 更好的工具支持:许多现代工具(如 TypeScript、Babel 等)都支持 ES6 模块。
- 支持 Tree-Shaking:在打包过程中可以去除未使用的代码,减小最终输出的文件大小。
缺点
- 浏览器支持:虽然现代浏览器支持 ES6 模块,但旧版本浏览器并不支持,可能需要使用转译工具(如 Babel)进行转换。
- 动态导入:虽然可以通过
import()动态导入模块,但这在某些场景下可能会导致一些额外的复杂度。
3. AMD (Asynchronous Module Definition)
特点
- 环境:主要用于浏览器端。
- 加载方式:异步加载。允许在模块加载期间继续执行其他代码。
- 导出方式:通过
define函数定义模块,通常需要一个回调函数来定义模块的内容。 - 依赖管理:AMD 规范强调依赖的前置声明,即在模块定义时就声明依赖项。
优点
- 异步加载:不会阻塞页面渲染,适合浏览器环境。
- 依赖管理:可以提前声明依赖项,方便依赖管理和优化加载顺序。
缺点
- 语法较为复杂:相对于 CommonJS 和 ES6 模块,AMD 的语法更复杂,不易于阅读和编写。
- 工具支持:虽然有 RequireJS 这样的工具支持 AMD,但随着 ES6 模块的发展,AMD 的支持正在逐渐减少。
4. CMD (Common Module Definition)
特点
- 环境:主要用于浏览器端。
- 加载方式:异步加载,但模块的执行是按顺序的。
- 导出方式:通过
define函数定义模块,与 AMD 类似,但更强调依赖的动态加载。 - 依赖管理:CMD 规范强调依赖的动态加载,即在需要的时候才加载。
优点
- 异步加载:不会阻塞页面渲染,适合浏览器环境。
- 动态依赖:可以根据运行时的情况动态加载依赖,有利于性能优化。
缺点
- 社区支持较少:相比于 AMD,CMD 的社区支持较少。
- 工具支持:Seajs 是主要支持 CMD 的工具,但随着 ES6 模块的普及,Seajs 的使用也在减少。
总结
- CommonJS 和 ES6 Modules 都是同步加载的规范,但 ES6 Modules 是异步执行的,并且具有更好的工具支持和语言级别的支持。
- AMD 和 CMD 都是为了适应浏览器环境的异步加载需求而产生的规范,其中 AMD 更强调依赖前置,而 CMD 更强调依赖的延迟加载。
- ES6 Modules 已经成为现代 Web 开发的标准,它被广泛支持,并且大多数现代浏览器都原生支持 ES6 模块。