前端模块化机制有哪些

565 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第23天,点击查看活动详情

核心描述

  • 模块化定义:将复杂的程序根据一定的规则或规范,封装成几个模块或文件,并组合在一起。其中模块的内部数据与实现是私有的,只是向外部暴露一些接口或方法与外部其它模块通信。
  • 模块化的好处:
    • 避免命名冲突(减少命名空间污染,私有变量)
    • 更好的分离,按需加载
    • 提高代码复用性
    • 提高代码可维护性

JS 经过不断的发展,模块化规范与实现也逐步趋于稳定,对于实际应用而言,我们只需要掌握 ES6 的模块化规范(ESM),以及 Nodejs 中的 CommonJS 规范即可。

  • ES6 模块化(ESM)
    • 描述:ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
    • 浏览器端应用
      • 通过 import 引入模块文件,通过 export 、export default 导出模块文件
      • 项目中的实际应用,项目需要在发布前,通过 babel 、webpack 等构建工具将 ES6 的语法打包成 ES5 的语法,让浏览器正常运行
      • 浏览器中也支持原生的 import/export、export default 的语法,只是有兼容性问题,而且对大型项目支持还不够友好,使用示例如下
        • html 文件
        // html 文件,注意 script 中的 type 需要设置为 module
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta http-equiv="X-UA-Compatible" content="IE=edge">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>Document</title>
        </head>
        <body>
            <h1>module test</h1>
            <script type="module" src="./index.js"></script>
        </body>
        </html>
        
        • index.js 文件
        import modue1 from "./modue1.js";
        
        console.log(modue1)
        
        • modue1.js 文件
        export default {
            a: 'hahahahaha'
        }
        
    • 服务器端
      • 当 Nodejs 版本小于 14.x 时,需要利用 babel 将 ES6 转换为 ES5 ,然后在 Nodejs 环境中执行
      • 当 Nodejs 版本大于 14.x 时,可以通过以下方式使用 ES6
        • 方式一:设置 package.json 的 type 为 module
        • 方式二:可以用 .mjs 的后缀来书写 ES6 的代码文件
  • CommonJS:Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
    • 在服务器端,模块的加载是运行时同步加载的
    • 在浏览器端,模块需要提前编译打包处理
    • 其他注意点:
      • 引入模块:require(xxx)
      • 导出模块:module.exports xxx 或 exports.xxx
      • export 导出的值的特点,见代码注释:
        • node.js 文件
        // 导出模块的文件
        var myobj = {
            a:1,
            b:2
        }
        var myTotal = 1
        var myNum = 1
        function addNum(){
            myNum += 100
        }
        module.exports = {
            myobj,
            myTotal,
            addNum,
            myNum
        }
        
        • node1.js 文件
        // 引用模块 node,同时导出可以操作模块 node 中变量的方法
        var obj = require('./node')
        function add(){
            obj.myobj.a += 2;
        }
        function addTotal(){
            obj.myTotal +=10
        }
        module.exports = {
            add,
            addTotal
        }
        
        • index.js 文件
        // 分别引入模块 node 和模块 node1
        var node1 = require('./node1')
        var node = require('./node')
        
        // 打印初始值
        console.log(node.myobj,node.myTotal,node.myNum) 
        // 调用模块 node1 中的方法,用于改变模块 node 的引用类型的变量值
        node1.add()
        // 调用模块 node1 中的方法,用于改变模块 node 的值类型的变量值
        node1.addTotal()
        // 调用模块 node 中的方法,用于改变模块 node 的值类型的变量
        node.addNum()
        console.log(node.myobj,node.myTotal,node.myNum)
        
        • 结论:
          • 在 Nodejs 中,如果一个引用类型的变量被多个模块引用,且改变其值,则该变量的值会改变
          • 如果一个值类型的变量被多个模块引用,且改变值,则该变量的值会改变
          • 如果一个值类型的变量被单个模块引用,且改变值,则该变量的值并不会被改变
          • 如果一个引用类型的变量被单个模块引用,且改变值,则该变量的值仍然不会被改变(可以自己尝试一下)

知识拓展

  • 没有模块化规范之前,在项目中可以使用如下方式进行模块化的实现:
    • 全局 function 模式 : 将不同的功能封装成不同的全局函数
    • namespace 模式 : 简单对象封装
    • 立即执行函数(IIFE)模式:匿名函数自调用(闭包)
    • Object 对象 + prototype 原型拓展
  • 演变过程中的模块化规范
    • AMD:
      • 采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。推崇依赖前置
      • RequireJS 是 AMD 规范最热门的实现
    • CMD:
      • CMD 是通用模块加载,要解决的问题与 AMD 一样,只不过是对依赖模块的执行时机不同 ,推崇就近依赖
      • Sea.js 是 CMD 规范的一个实现代表库
    • UMD:
      • UMD是AMD和CommonJS的糅合
      • 核心实现:
        • 先判断是否支持Node.js模块(exports是否存在),存在则使用Node.js模块模式
        • 再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。
        • 前两个都不存在,则将模块公开到全局(window或global)。
      • 代码示例:
      // 在很多开源库中,都会有类似的支持逻辑
      (function (window, factory) {
          if (typeof exports === 'object') {
          
              module.exports = factory();
          } else if (typeof define === 'function' && define.amd) {
          
              define([],factory);
          } else {
          
              window.eventUtil = factory();
          }
      })(this, function () {
      	return {};
      })
      
  • 个人理解:随着 JS 的不断发展,我们没有必要再去深入研究曾经的模块化方案,但是我们有必要对齐有一个概览的认识,当我们遇到类似的祖传项目时,往往可以方便我们去定位、解决问题。同时也应该将精力放到更成熟的新的规范中去,就目前而言,浏览器方向的 ES6(ESM) 的 import xxx from xxxexport xxxexport default xxx 的组合,以及 Nodejs 方向的 require(xxx)modules.exportexports.xxx 的组合,才是我们项目中最应该掌握的。

参考资料

浏览知识共享许可协议

本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。