美文网首页前端大杂烩
JavaScript 中的模块发展方案

JavaScript 中的模块发展方案

作者: lio_zero | 来源:发表于2022-05-25 17:58 被阅读0次

    随着我们的应用越来越大,我们想要将其拆分成多个文件,即所谓的模块(module)

    在 ES6 之前,社区制定了许多种方法来将代码组织到模块中,使用特殊的库按需加载模块。

    • AMD:最古老的模块系统之一,最初由 require.js 库实现。
    • CommonJS:为 Node.js 服务器创建的模块系统。
    • CMD:代表的 seajs 库。
    • UMD:另外一个模块系统,建议作为通用的模块系统,它与 AMD 和 CommonJS 都兼容。

    AMD

    AMD(Asynchronous Module Definition)即 "异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

    AMD 提前执行,推崇依赖前置。

    RequireJS 是 AMD 规范的最流行的实现。

    它的出现为了解决两个问题:

    • 实现 JavaScript 文件的异步加载,也就是模块是并行加载的,而不是通过等待加载完成来阻止执行。
    • 管理模块之间的依赖性,便于代码的编写和维护。

    更多详细内容:

    CMD

    这里简单带过,了解的不多,使用的也不多。

    CMD 延迟执行,推崇依赖就近,只有用户需要的时候才执行(性能好)

    UMD

    UMD(通用模块定义)模式,用于在任何地方工作的 JavaScript 模块。

    如果你经常阅读源码,你会发现很多库都会使用 UMD:

    ;(function (root, factory) {
      if (typeof define === 'function' && define.amd) {
        define([], function () {
          return factory(root)
        })
      } else if (typeof exports === 'object') {
        module.exports = factory(root)
      } else {
        root.myPlugin = factory(root)
      }
    })(
      typeof global !== 'undefined'
        ? global
        : typeof window !== 'undefined'
        ? window
        : this,
      function (window) {
        'use strict'
    
        // do something ...
      }
    )
    

    它可以很好的兼容 AMD 和 CommonJS,无论在浏览器还是在其他 JS 运行环境。

    CommonJS

    Node.js 应用由模块组成,其模块系统采用 CommonJS 规范。

    CommonJS 是一种借助 exports 对象定义模块的方法,它定义了模块的内容。

    // someModule.js
    exports.doSomething = function () {
      return 'foo'
    }
    
    // otherModule.js
    var someModule = require('someModule')
    
    exports.doSomethingElse = function () {
      return someModule.doSomething() + 'bar'
    }
    

    CommonJS 指定了一个全局方法 require() 来加载模块,获取依赖项,而 exports 用于导出模块内容的变量,以及一个模块标识符(描述与此模块相关的模块的位置),用于加载依赖项。

    注意:AMD 用于客户端(浏览器),CommonJS 主要用于服务端。

    CommonJS 模块的特点

    • 所有代码都运行在模块作用域,不会污染全局作用域。
    • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
    • 模块加载的顺序,按照其在代码中出现的顺序。

    CommonJS 原理与实现

    浏览器不兼容 CommonJS 的根本原因,在于缺少四个 Node.js 环境的变量。

    • module
    • exports
    • require
    • global

    只要能够提供这四个变量,浏览器就能加载 CommonJS 模块。

    详细内容推荐阮一峰老师的浏览器加载 CommonJS 模块的原理与实现

    你也可以使用现有的 CommonJS 格式转换工具,Browserify 是目前最常用的。

    ES Module

    ES6 Module 模块输出的是一个值的引用,编译时输出接口,ES6 模块不是对象,它对外接口只是一种静态定义,在代码静态解析阶段就会生成。

    模块可以相互加载,并可以使用特殊的指令 exportimport 来交换功能,从另一个模块调用一个模块的函数:

    模块功能主要由两个命令构成:exportimport

    • export命令用于规定模块的对外接口
    • import命令用于输入其他模块提供的功能。
    export const name = 'li'   // 命名导出
    import { name } from '...' // 命名导入
    
    export default 'li'       // 默认导出
    import anyName from '...' // 默认导入
    
    export { name as newName }    // 重命名导出
    import { newName } from '...' // 命名导入
    
    // 默认名称导出
    export const name = 'value'
    export default 'value'
    import * as anyName from '...' // 全部导入
    
    // 导出列表,并重命名
    export {
      name,
      age as anyAge
    }
    // 导入列表 + 重命名
    import { name as newName, anyAge } from '...'
    

    模块路径必须是原始类型字符串,不能是函数调用

    import ... from getModuleName() // Error, only from "string" is allowed
    

    无法根据条件或者在运行时导入:

    if (...) {
      import ... // Error, not allowed!
    }
    
    {
      import ... // Error, we can't put import in any block
    }
    

    动态导入 import() 表达式

    import(module) 表达式加载模块并返回一个 promise,该 promise 为一个包含其所有导出的模块对象。我们可以在代码中的任意位置调用这个表达式。

    // 📁 say.js
    export function hi() {
      alert(`Hello`)
    }
    
    export function bye() {
      alert(`Bye`)
    }
    
    // index.js
    let { hi, bye } = await import('./say.js')
    
    hi()
    bye()
    

    动态导入在常规脚本中工作时,它们不需要 script type="module"

    尽管 import() 看起来像一个函数调用,但它只是一种特殊语法,只是恰好使用了括号(类似于 super())。

    因此,我们不能将 import 复制到一个变量中,或者对其使用 call/apply。因为它不是一个函数。

    规范

    下面将以 Airbnb JavaScript 样式指南为参考,给出在使用 ES6 模块(import/export)的一些编写建议,其中包括 eslint(查找并修复 JavaScript 代码中的问题)的属性。

    • 始终在非标准模块系统上使用模块(import / export)。

    为什么?模块将会是未来的主流。

    // bad
    const AirbnbStyleGuide = require('./AirbnbStyleGuide')
    module.exports = AirbnbStyleGuide.es6
    
    // ok
    import AirbnbStyleGuide from './AirbnbStyleGuide'
    export default AirbnbStyleGuide.es6
    
    // best
    import { es6 } from './AirbnbStyleGuide'
    export default es6
    
    • 不要使用通配符导入。

    为什么?这样可以确保只有一个默认导出。

    // bad
    import * as AirbnbStyleGuide from './AirbnbStyleGuide'
    
    // good
    import AirbnbStyleGuide from './AirbnbStyleGuide'
    
    • 不要直接从导入中导出。

    为什么?虽然 "一行" 很简洁,但有一个明确的导入方式和一个明确的导出方式使事情保持一致。

    // bad
    export { es6 as default } from './AirbnbStyleGuide'
    
    // good
    import { es6 } from './AirbnbStyleGuide'
    export default es6
    

    为什么?具有从同一路径导入的多行代码可能会使代码难以维护。

    // bad
    import foo from 'foo'
    import { named1, named2 } from 'foo'
    
    // good
    import foo, { named1, named2 } from 'foo'
    
    // good
    import foo, {
      named1,
      named2,
    } from 'foo'
    

    为什么?通常应该避免更改,特别是在导出可变绑定时。虽然在某些特殊情况下可能需要这种技术,但一般情况下,应该只导出常量引用。

    // bad
    let foo = 3
    export { foo }
    
    // good
    const foo = 3
    export { foo }
    

    为什么?鼓励更多只导出一件东西的文件,这对于可读性和可维护性而言更好。

    // bad
    export function foo() {}
    
    // good
    export default function foo() {}
    
    • 将所有 import 置于非 import 语句之上。eslint:import/first

    为什么?因为 import 被提升了,所以把它们都放在顶部,可以防止出现意外行为。

    // bad
    import foo from 'foo'
    foo.init()
    
    import bar from 'bar'
    
    // good
    import foo from 'foo'
    import bar from 'bar'
    
    foo.init()
    

    为什么?花括号遵循与风格指南中其他花括号块相同的缩进规则,尾随的逗号也一样。

    // bad
    import { longNameA, longNameB, longNameC, longNameD, longNameE } from 'path'
    
    // good
    import {
      longNameA,
      longNameB,
      longNameC,
      longNameD,
      longNameE,
    } from 'path'
    

    为什么?因为在 Webpack 中使用了 import 语法,所以会将代码耦合到模块 loader 中。最好在 webpack.config.js 中使用 loader 语法。

    // bad
    import fooSass from 'css!sass!foo.scss'
    import barCss from 'style!css!bar.css'
    
    // good
    import fooSass from 'foo.scss'
    import barCss from 'bar.css'
    

    为什么?包含扩展会将阻止重构,并对每个使用者中要导入的模块的实现细节进行不适当的硬编码。

    // bad
    import foo from './foo.js'
    import bar from './bar.jsx'
    import baz from './baz/index.jsx'
    
    // good
    import foo from './foo'
    import bar from './bar'
    import baz from './baz'
    

    推荐一篇关于 ES6 之前各种规范的模块化 《JavaScript Module Pattern: In-Depth》

    也可以阅读阮一峰老师的 Javascript模块化编程(一):模块的写法

    更多资料

    相关文章

      网友评论

        本文标题:JavaScript 中的模块发展方案

        本文链接:https://www.haomeiwen.com/subject/akipkrtx.html