前端模块化

本文探讨了前端模块化的演进历程,从早期的全局对象和立即执行函数,到CommonJS、AMD和UMD规范。重点介绍了ES6的ES Module(ESM),包括其特性、使用方法、导入导出规则,以及在浏览器和Node.js环境中的应用和兼容性问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

模块化是一种思想

1、模块化的演进过程

初始阶段

首先在早期在没有工具和规范情况下模块化是怎么体现的呢?

  1. 最早的时候我们以文件的方式来划分模块,一个文件就是一个模块。将每个功能以及其相关的状态数据放到同一个文件当中,然后通过srcipt标签引入,一个script标签对应一个模块。这样实现有几个比较明显缺点:1、没有私有空间的概念,所有变量都以全局的方式声明,会污染全局作用域,在外部也能够随意修改变量的值;2、变量命名比较容易产生冲突;3、模块的依赖关系混乱。
  2. 后来,为了减少命名冲突,在文件划分的基础之上,将每个模块包裹在一个全局对象中,每个文件为一个对象。但仍然没有私有空间的概念,外部还是可以修改模块中的成员,也无法管理模块之间的依赖关系。
  3. 再后来,为了让变量有个私有的作用域,开始将所有成员包裹在一个立即执行的函数中,而对于需要在外部访问的成员,将其挂载到全局变量中。函数执行时也可以将依赖的模块作为参数传入,依赖关系更加明显,容易管理。

探索阶段

在前端工程项目日趋壮大,各模块之间的依赖关系也越来越复杂,模块化的标准和规范需求也越来越迫切。开发者在摸索中总结践行自己的模块化的标准和规范,并推出了一些最佳的实践。

  1. CommonJs
    CommonJs规范是 node 内置的js代码规范。其特点如下:

    1. 一个文件就是一个模块
    2. 每个文件都有独立的作用域
    3. 通过 module.exports(exports ⇔ module.exports) 导出成员
    4. 通过 require 函数导入模块
    // 导出
    #! /user/bin/env node
    const name ='wsq'
    module.exports = {
      name,
      age: 123
    }
    // 导入
    const user = require('./a')
    console.log(user.name);
    

    CommonJs规范是以同步的方式加载模块,node环境下会首先加载所有模块,然后执行时直接使用。不适合浏览器的执行环境。

  2. AMD(Asynchronous Module Definition)
    AMD, 异步模块定义规范,require.js实现了AMD规范,其特点如下:

    1. 通过define函数来定义模块,define函数接收三个参数,第一个参数为模块名,第二个参数为可选参数,为模块的依赖,第三个参数为一个函数。
    2. 通过require函数加载模块,用法和define函数类似。 require函数接收两个参数,第一个参数为加载的模块,第二个参数为模块加载之后的执行函数。
    // 模块定义
    /**
     * $1: 模块名, $2: 依赖的模块 $3为模块提供私有作用域,通过return将成员导出
     * $3 函数的参数和 $2 的依赖项一一对应。
     */
    define('a', ['jQuery', './c'], function($, angular) {
      const user = {name: '123'}
      return {
        jquery: $,
        user,
      }
    })
    // 导入模块
    /**
     * $1 要加载的模块 $2 模块加载之后的执行方法,方法的参数对应加载的模块。
     */
    require(['./a', 'Angular'], function(a, angular) {
    
    })
    

    AMD规范使用相对复杂,而且每次执行require时会生成script标签去请求对应模块,对于同一个模块可能会被多次加载,影响页面性能。

  3. CMD(Common Module Definition)
    CMD,通用模块定义规范,Sea.js 实现了CMD规范。其特点如下:

    1. 一个模块是一个文件
    2. 不应该在模块内引入新的自由变量
    3. 异步执行
    4. 通过define来定义模块和引入模块,define 接收一个函数作为参数,函数一次接收三个参数,requireexportsmodulerequire 用来导入模块,exports 用来导出模块,module 模块的描述对象。
    // math.js
    define(function(require, exports, module) {
      exports.add = function() {
        var sum = 0, i = 0, args = arguments, l = args.length;
        while (i < l) {
          sum += args[i++];
        }
        return sum;
      };
    });
    // increment.js
    define(function(require, exports, module) {
      var add = require('math').add;
      exports.increment = function(val) {
        return add(val, 1);
      };
    });
    // program.js
    define(function(require, exports, module) {
      var inc = require('increment').increment;
      var a = 1;
      inc(a); // 2
    
      module.id == "program";
    });
    

模块化方案统一
经过多年的探索和实践,模块化方案逐渐趋于统一,在 Node 中使用 CommonJs规范, 在浏览器中则使用 ES Module规范

2、ES Module(ESM)
ESMES2015 中定义的模块系统,其特性如下:

  1. ESM默认采用严格模式,忽略 ‘use strict’。严格模式下thisundefined
    在这里插入图片描述
  2. 每个 ESM 都具有单独的作用域
  3. ESM 是通过 CORS 方式请求外部 js 的。CORS不能通过文件的形式请求,只能通过http的方式请求
  4. ESM 是延迟执行的,相当于给script 标签添加了defer 属性

2.1、ES Module 的使用

  1. 浏览器中如何使用 ESM 执行 js

    <script type="module" src="./app.js">
        const foo = 123;
        console.log(foo);
    </script>
    
  2. 模块导入:通过 import 关键字实现模块的导入

    1. from 后面的文件路径必须包含完整的文件名。可以是 ./ 开头的相对路径、 / 开头的绝对路径 或者模块所在位置的完整url
      不能省略后缀或者index.js

      import { name } from './module.js'
      import { name } from '/module.js'
      import { name } from 'http://localhost:3000/module.js'
      import $ from 'https://unpkg.com/jquery@3.4.1/dist/jquery.min.js'
      
    2. 不引用模块中的成员,而是直接加载模块时,通过以下两种方式

      import {} from './module.js'
      import './module.js'
      
    3. 导入模块中的所有成员 import * as module from './module.js'

      // module.js
      function hello() {
        console.log('hello');
      }
      
      class Person{
        constructor(name) {
          this.name = name
        }
      }
      
      export default name
      
      export {
         hello, Person
      }
      // app.js
      import * as module from './module.js'
      console.log(module);
      

      在这里插入图片描述

    4. import 只能在顶层作用域中使用,不能在if、for等作用域中使用,如果想要动态导入模块,可以通过import(modulePath)来导入,import(modulePath)函数返回一个promise

      const modulePath = './module.js'
      import(modulePath).then(mod => {
        console.log(mod);
      })
      

      在这里插入图片描述

    5. 同时导入命名成员和默认成员,有两种用法。

      1. 通过 as 对默认导出成员进行重命名
        import { default as name,  Person, hello } from './module.js'
        
      2. 在花括号前面先导入默认成员,在导入其他成员
        import name, { Person, hello } from './module.js'
        
  3. 模块导出:通过 export 关键字实现模块的导出

    1. 可以通过 export 直接导出变量、函数、类等

      export var name = 'Json'
      
      export function hello() {
        console.log('hello');
      }
      
      export class Person {
        constructor(name) {
          this.name = name
        }
      }
      
    2. 也可以通过 export 导出一个对象,将变量作为一个对象的属性导出

      export {
        name, hello, Person
      }
      
    3. 导出时重命名,通过 as 来对导出的变量进行重命名,导入时只能使用重命名之后的变量名

      // 导出
      export {
        name as userName, hello, Person
      }
      // 导入
      import { name } from './module.js'
      

      在这里插入图片描述

    4. 如果将变量重命名为 default , 则该变量将作为模块默认导出成员。在 import 导入时,直接使用任意名字来存储模块导出的变量。

      // 导出
      export {
        name as default, hello, Person
      }
      // 导入
      import personName from './module.js'
      console.log(personName);
      

      在这里插入图片描述

    5. 通过 export default 变量名 可以将变量作为模块的默认导出来导出。在导入时的用法和 4 一致。

      export default name
      
  4. 导入导出的注意事项

    1. 导入导出的写法是固定的语法,而不是对象的解构。主要体现在以下几个方面:

      1. 不能直接导出一个基本类型的常量
        在这里插入图片描述

      2. 在默认导出一个对象字面量时,不能在导入时通过结构获取对象中的属性成员

        //导出
        export default {
          name, hello, Person
        }
        //导入
        import { name } from './module.js'
        ~~console.log(name);~~ 
        
    2. 导入导出的是变量的引用,如果在使用了导入的变量之后修改变量,引用的变量的值将改变

      // 导出
      var name = 'Josn'
      setTimeout(function() {
        name = "Geogy"
      }, 1000)
      // 导入
      import { name } from './module.js'
      console.log(name)
      setTimeout(function() {
        console.log(name);
      }, 1100)
      

      在这里插入图片描述

    3. 导入模块时,模块的成员是只读的,不能修改

      import { name } from './module.js'
      console.log(name);
      name = 'Annay'
      

      在这里插入图片描述

  5. 直接导出导入的成员

    1. export { Person, hello } from './module.js'
    2. 直接导出默认成员时,需要通过 as 进行重命名 export { default as name, Person, hello } from './module.js'
  6. ESM 浏览器环境 polyfill。通过引入browser-es-module-loader 来兼容不支持 ESM 的浏览器环境。

    1. 可以通过https://unpkg.com/moduleName来查找要引入的模块的源码文件。
      <script nomodule type="text/javascript" src="https://unpkg.com/browse/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
      <script nomodule type="text/javascript" src="https://unpkg.com/browse/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
      
    2. script 标签的 nomodule 属性,在不支持ESM的浏览器中才会运行其中的代码。可以利用这个属性来兼容对ESM的支持
      <script nomodule>
          alert(1234)
      </script>
      
  7. node环境下使用ESM

    1. 需要将后缀名改为.mjs。

    2. 使用node命令执行.mjs文件时,需要添加 --experimental-modules 参数
      在这里插入图片描述

    3. 可以使用 import {} from ‘’ 的形式导入内置模块的成员,因为内置模块对ESM进行了兼容

      // import fs from 'fs'
      import { writeFileSync } from 'fs'
      writeFileSync('.test.txt', 'Hello Node 123')
      
    4. 不能使用 import {} from ‘’ 的方式导入第三方模块,因为第三方模块导出的是默认成员(主要看node第三方模块是否添加了ESM支持)

      import { camelCase } from 'lodash'
      console.log(camelCase('ES Module'));
      

      在这里插入图片描述

    5. CommonJSESMNode 环境下的交互

      1. ESM 可以导入 CommonJs 的模块
      2. CommonJs 模块不能 导入 ESM 的模块
      3. CommonJs 模块只会默认导出
    6. ESMCommonJs 在 Node 中差异:主要ESM是对于node内置模块中的5个变量不支持

      // 加载模块的函数
      console.log(require);
      // 模块对象
      console.log(module);
      // 导出对象别名
      console.log(exports);
      // 当前文件的绝对路径
      console.log(__filename);
      // 当前文件所在目录
      console.log(__dirname);
      

      ESM 中使用相关功能

      import { fileURLToPath } from 'url'
      import { dirname } from 'path'
      
      // 当前文件的url
      // console.log(import.meta.url);
      const __filename = fileURLToPath(import.meta.url)
      const __dirname = dirname(__filename)
      console.log(__filename);
      console.log(__dirname);
      
    7. node中设置默认使用ESM规范执行js

      1. package.json文件中的type设置为 module
        {
          "type": "module",
          "dependencies": {
            "lodash": "^4.17.20"
          }
        }
        
      2. 如果要使用CommonJs规范,则需要将后缀改为.cjs
    8. 使用babelnode低版本环境兼容ESM

      1. 使用preset-env转换
        1. 安装插件@babel/node @bable/core @babel/preset-env
        2. 配置babel,在.babelrc文件中添加配置 "presets": ["@babel/preset-env"]
        3. 运行命令 yarn babel-node index.js
      2. 使用插件@babel/plugin-transform-modules-commonjs转换
        1. 安装插件@babel/node @bable/core @babel/plugin-transform-modules-commonjs
        2. 配置babel,在.babelrc文件中添加配置 "plugins": ["@babel/plugin-transform-modules-commonjs"]
        3. 运行命令 yarn babel-node index.js
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值