JS模块化规范

111 阅读15分钟

1. 模块化概述

1.1 什么是模块化?

  • 将程序文件依据一定规则 拆分 成多个文件,这种编码方式就是 模块化 的编程方式。

  • 拆分出来 每一个文件就是一个模块,模块中的实际都是 私有的,模块之间互相隔离 隔离

  • 同时也能通过一些手段,可以把模块内的指定数据 交出去 供其他模块使用。

1.2 为什么需要模块化

随着应用的复杂的越来越高,其代码量和文件数量都会几句增加,会逐渐引发一下问题。

  1. 全局污染问题

  2. 依赖混乱问题

  3. 数据安全问题

2. 有哪些模块化规范

历史背景(了解): 2009年,随着 Node.js 的出现, JavaScript 在服务器端的应用逐渐增多,让 Node.js 的代码更好维护,就必须要制定一种 Node.js 环境下的模块化规范,来自 Mozilla 的设计师 Kevin Dangoor 提出了 CommonJS 规范 (CommonJs 初期的名字叫 ServerJS),随后Node.js 社区采纳了这一规范。

随着时间的推移,针对 JavaScript 的不同运行环境,相继出现了多种模块化规范, 按实际顺序,分别为:

    CommonJS            服务端应用广泛
    AMD                 
    CMD
    ES                  浏览器端应用广泛
    
    
    

3. 导入与导出的概念

模块化的核心思想就是: 模块之间是隔离的,通过导入和导出进行数据和功能的共享。

  • 导出(暴露):模块公开其内部的一部分(如变量、函数等),使这些内容可以被其他模块使用。

  • 导入(引入):模块引入和使用其他模块导出的内容,以重用代码和功能。

a1.png

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

注意点:

  1. 每个模块的内部的:this、 exports、 modules.exports 在初始时,都指向同一个空对象,该空对象就是当前模块导出的数据。
 如图:
 

a2.png

  1. 论如何修改导出对象,最终导出的都是 module.exports 的值。
  1. 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 改为 .mjsNode 则会自动识别 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' 

总结: 此时,我们感受到模块化确实解决了:

  1. 全局污染问题
  2. 依赖混乱问题
  3. 数据安全问题

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 使用 require 函数来引用数据。
    • ES6 Modules 使用 import 关键字来引用数据。
  • 绑定行为

    • CommonJS 的导出是动态绑定的,这意味着修改 module.exports 或 exports 会影响到所有已经引用该模块的地方。
    • ES6 Modules 的导出是静态绑定的,修改导出后不会影响到已经导入该模块的地方。
  • 加载方式

    • CommonJS 是同步加载的,可能会导致阻塞。
    • ES6 Modules 是异步加载的,不会阻塞页面渲染。

6. AMD 模块化规范 (了解)

6.1 环境准备

步骤1. 准备文件结构

a3.png

文件说明

  • 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.准备文件结构

a4.png

文件说明

  • JS 文件夹中存放业务逻辑代码, main.js 用于汇总各种模块

  • libs 中存放的是第三方库,例如必须要用的sea.js

步骤2. 在 index.html 中配置 main.jssea.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 模块。