likes
comments
collection
share

读懂前端模块化

作者站长头像
站长
· 阅读数 12

模块化就是定义一个局部作用域,在内部定义变量和方法,并对外暴露接口

IIFE

自执行函数,就是声明完函数立即执行,避免了变量和函数污染全局

// IIFE的格式

// ModuleA
var ModuleATest = (function (exports) {
    'use strict'
    exports.getName = function () {
        return 'kevin'
    }
    return exports
})({})

// ModuleB
var Test = (function (exports, moduleA) {
    'use strict'
    var b = 18
    exports.name = moduleA.getName()
    return exports
}({}, ModuleA)

核心思想:创建立即执行函数->立刻执行函数且外部依赖以参数方式传入->返回模块且对外暴露接口

优点

  1. 通过闭包创建一个私有命名空间
  2. 没有多余代码,不影响体积(rollup打包IIFE格式)
  3. 文件可直接使用

缺点

  1. 暴露的全局变量可能影响全局,或者值被覆盖(上面的window.ModuleA和window.Test为例)
  2. 内部如果需要依赖第三方库的时候,需要依赖全局变量,且保证依赖包引用在前面
运行使用
<script src="./moduleATest"></script>
<script>
    window.ModuleA = window.ModuleATest
</script>
<script src="./moduleBTest"></script>

// moduleATest必须要在前面先加载,因为moduleBTest依赖它

CJS(commonjs)

每个文件就是一个模块,有自己的作用域,文件内定义的变量、函数等都是私有的,对其他文件不可见

commonJS加载模块是同步的(AMD是异步的),由于Node主要用于服务器编程,模块文件一般存在与本地硬盘,所以加载起来比较快,等待时间就是硬盘的读取时间,不用考虑非同步加载的方式

// moduleA.js
const getName = () => {
    return 'jiaobin'
}
exports.getName = getName

// moduleB.js
const moduleA = require('./moduleA')
const showName = () => {
    return moduleA.getName()
}
console.log(showName()) // jiaobin

优点

  1. Node中较好的模块化方式,解决了IIFE的缺点

缺点

  1. 只适应Node,不支持浏览器环境,因为这种同步的方式可能导致浏览器假死

特点

  1. 模块可以多次加载,但是只会在第一次加载的时候执行,然后会缓存一份,后面再引用返回的都是缓存
  2. 模块是同步加载的,顺序是按照出现的位置来定,require引入一个模块,必须等这个模块加载完才能会执行后续代码
  3. 被输出的是拷贝值,所以如果在外部对模块代码修改是不会生效的
运行使用
// 引入模块
const moduleA = require('./moduleA.js')

// 导出模块
module.exports = {
    getName () {
        return 'jiaobin'
    }
}
或者
exports.getName = function() {
    return 'jiaobin'
}

注:module.exports相当于直接给exports整个对象重新赋值;而exports.getName则是给exports这个对象添加getName属性。

如果想要Commonjs在浏览器运行,可以通过browserify转换工具转换成浏览器可用的模块

npm install -g browserify

browserify b.js > bb.js  // bb.js是编译后的文件

查看browserify编译原理可以通过browser-unpack查看

npm install browser-unpack -g

browser-unpack < bb.js

AMD

AMD是完全针对浏览器的模块定义,客户端加载的时候比较看中速度,不能像CJS一样同步执行,那么可能一个大的组件就会阻塞后面执行,因此AMD加载是异步规范设计的,所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会执行

// moduleA(不依赖模块)
define(function(require, factory) {
  'use strict';
  return {
    getName() {
      return 'jiaobin'
    }
  }
});

// moduleB(依赖模块A)
define([
  'moduleA'
], function(moduleA) {
  'use strict';
  return {
    showName() {
      return moduleA.getName()
    }
  }
});

// index.js主模块入口,统一调度当前项目中所有依赖模块
require.config({
  paths: {
    // 这样我们在define模块中通过模块名就能找到对应文件
    'moduleA': './moduleA',
    'moduleB': './moduleB'
  },
  shim: {
      // 不规范的AMD模块需要写在这里面,比如,underscore 和 backbon这两个库
      'underscore': {
          exports: '_'
      }
  }
})

require(['moduleB'], function (moduleB) {
  console.log(moduleB.showName())
})

页面中使用以上定义的AMD模块

requirejs

<!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>
  <!-- 引入requirejs且在该sciprt标签上添加data-main属性,值就是我们要执行的AMD文件地址 -->
  <script data-main="./index.js" src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.js"></script>
</body>
</html>

// output:jiaobin

优点

  1. 解决CJS和IIFE的缺点

缺点

  1. 代码可读性比较差

CMD

CMD是通用模块加载,解决的问题跟AMD一样,只不过是对依赖模块的执行时机不同,推崇就近依赖,而AMD是依赖前置,seajs是CMD规范的代表库

语法上结合了AMD模块定义的特点define,同时又有Commonjs模块导入导出的特点exports

// 定义 a.js 模块,同 时可引入其他依赖模块,及导出本模块
define(function(require, exports, module) {

 var $ = require('jquery.js')

 exports.price= 200;  
});

参数说明

  • require:函数用来获取其他模块提供的接口require(模块标识ID)
  • exports: 对象用来向外提供模块接口
  • module :对象,存储了与当前模块相关联的属性和方

AMD和CMD区别

  1. AMD是提前执行,CMD是延迟执行
  2. AMD是依赖前置(define的第一个参数就是整个回调代码需要的依赖),CMD是依赖就近(在需要用到模块的时候再require)

UMD

UMD兼容了CJS和AMD,它的最外层还是IIFE,原理其实就是判断全局是否存在exports和define,如果存在exports就以commonjs的方式暴露模块,如果存在define那么就以AMD的方式暴露模块

(function (window, fn) {
    if (typeof exports === 'object') {
        module.exports = fn()
    } else if (typeof define === 'function' && define.amd) {
        define(fn)
    } else {
        window.moduleA = fn()
    }
})(window, function() {
    let moduleA = {
        name: 'jiaobin',
        showName() {
            return this.name
        }
    }
    return moduleA
})

优点

  1. 兼容了cjs和amd,抹平了差异

缺点

  1. 多了兼容性判断代码,导致体积更大,可读性不好

SystemJs

SystemJS是一个插件化的,基于标准的模块加载器,它提供了一个工作流,可以将为浏览器中编写的原始ES6模块代码转换为System.register模块格式,以在不支持原始ES6模块的旧版浏览器中运行,几乎可以达到运行原始ES模块的速度,同时支持顶层 await,动态导入,循环引用和实时绑定,import.meta.url,模块类型,导入映射,完整性和内容安全策略,并且在旧版浏览器中可兼容IE11。

systemjs的官方定义是为浏览器中的ES模块启用向后兼容工作流和可配置的模块加载程序,说的简单点就是让你可以在浏览器中使用上述说的几种任意的模块化方式,在微前端框架中起了很大作用

<html lang="en-US">
  <head>content="IE=edge">
    <title>SystemJS Dynamic Import Example</title>
    <script type="systemjs-importmap">
      {
        "imports": {
          "neptune": "./neptune.js"
        }
      }
    </script>
    <!-- 启动即运行neptune.js -->
    <script type="systemjs-module" src="import:neptune"></script>
    <!-- load SystemJS itself from CDN -->
    <script src="https://cdn.jsdelivr.net/npm/systemjs/dist/system.js"></script>
  </head>
  <body>
    <button id="load">加载</button>
  </body>
</html>
// neptune.js
System.register([], function (_export, _context) {
  return {
    execute: function() {
      document.body.appendChild(Object.assign(document.createElement('p'), {
        textContent: 'Neptune is a planet that revolves around the Sun'
      }));

      // 点击按钮后 加载triton.js
      document.querySelector('#load').addEventListener('click', () => {
        console.log('start debug');
        _context.import('./triton.js').then(function (triton) {
          console.log("Triton was discovered on", triton.discoveryDate);
        });
      });
    }
  };
});
// triton.js
System.register([], function (_export, _context) {
  return {
    execute: function() {
      document.body.appendChild(Object.assign(document.createElement('p'), {
        textContent: 'Triton is a moon that revolves around Neptune.'
      }));
      _export("discoveryDate", "Oct. 10, 1846");
    }
  };
});

EsModule(esm | es6)

ES6模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量,而CJS和amd只能在运行时确定这些东西

  • node端新版本可以直接运行
  • 浏览器端是不能直接运行的,需要先用babel将es6转成es5(把import转成require),然后再用打包工具打包,最后在页面中引入使用
// moduleA.js
// 导出1
export const getName = () => {
    return 'jiaobin'
}
// 导出2
const fn = () => {}
export default fn
// 导出3
export {
    a: 1,
    b() {}
}


// moduleB.js
import { getName } from './moduleA'

getName() // jiaobin

特点

  1. 静态化,编译时完成模块加载,引用时只加载需要的方法,其他方法不加载,使得页面加载速度更快,效率比Commonjs模块加载效率高
  2. ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import时,不会去执行模块。等到真的需要用到时,再到模块里面去取值
  3. 完全替代CJS和AMD
运行使用

可以通过<script type="module"></script>引入文件使用,前提是还要安装一个静态资源服务(anywhere、http-server等)

浏览器在访问本地ESM js 文件的时候遇到了跨域的问题,本地打开 HTML 文件时是使用 file 协议,通过报错可以看到并没有支持 file 协议,因此可以通过使用静态文件服务器(anywhere)解决

全局安装npm install anywhere -g

安装好后切换到 index.html 所在文件夹,执行 anywhere -p 8078,运行后会自动跳转到默认浏览器

<script type="module">
import moduleA from './moduleA.esm.js'
</script>

CJS和ES6的区别

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  • CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。

文档

转载自:https://juejin.cn/post/7182885239281156154
评论
请登录