JS模块化-AMD规范

308 阅读4分钟

定义

AMD,即‘Asynchronous Module Defintion’,异步模块定义。
它采用异步方式加载模块及其依赖项,适用于同步加载模块会导致性能等问题的浏览器环境。

规范内容

定义模块 define

define(id?: String, dependencies?: String[], factory: Function | Object);

id

指定此模块的id,此参数可以是可选的。
如果不存在,则模块id默认为加载程序为给定响应脚本请求的模块id。(??不太理解,但也不怎么用)
如果存在,模块 id 必须是“顶级”或绝对 id(不允许使用相对 id)。
module id格式:

  • id是由“/“分割的一个或多个单词组成的字符串
  • 单词必须是小写驼峰或“.“或“..“
  • 没有像“.js“这样的扩展名
  • id可以是相对的或顶级的(如果id以“.“、“..“开头,则id是相对的)
  • 顶级id是从概念模块名称空间中解析出来的
  • 相对id是相对于调用require的模块来解析的

相对id不是定义出来的module id,而是通过require加载解析出来的

id解析示例:

  • 如果模块“a/b/c”请求“../d”,则解析为“a/d”
  • 如果模块“a/b/c”请求“./e”,则解析为“a/b/e”

dependencies

指定此模块的依赖模块,值为依赖项的id数组。
如果不存在,则默认为['require', 'exports', 'module'],这些值是commonjs规范定义的变量。如果手动传递的数组中包含这些值,意义一样。

依赖模块必须在执行factory之前解析依赖项,并且解析的值应该作为参数传递给factory,参数位置对应数组中的索引。

factory

指定了模块的具体实现:
如果是一个函数,它应该只执行一次。且如果函数返回了值,则该值指定为模块的导出值。
如果是一个对象,则此对象应指定为模块的导出值。

如果不存在dependencies,模块加载器可以处理factory中的加载依赖的语句require('moduleId)。第一个参数必须是require才能正常工作 —— 这就是简化版的commonJS包装。
如果存在dependencies参数,则模块加载器不应处理factory中的加载依赖的语句。

示例
匿名模块——依赖模块alpha,导出了一个对象

   define(["alpha"], function (alpha) {
       return {
         verb: function(){
           return alpha.verb() + 2;
         }
       };
   });

匿名模块——无依赖,直接导出了一个对象

 define({
     add: function(x, y){
       return x + y;
     }
 });

简化的commonjs包装定义的模块

  define(function (require, exports, module) {
     var a = require('a'),
         b = require('b');

     exports.action = function () {};
   });

define.amd

省略,不理解

全局变量

本规范保留全局变define用于实现本规范,并为其他未来的 CommonJS API 保留。模块加载器不应该向这个函数添加额外的方法或属性。

本规范保留全局变量require供模块加载器使用。模块加载器可以随意使用这个全局变量。他们可以使用该变量并根据模块加载器特定功能的需要向其添加任何属性或函数。他们也可以选择不使用require

客户端(浏览器)支持

此规范浏览器中并未支持。 can is use

Xnip2022-05-18_20-47-07.jpg 环境:chrome 101.0.4951.64
要在浏览器中使用的话,需要借助于库

手写模块

使用工具库require.js

Xnip2022-05-18_21-27-28.jpg

// define modules

define('admin', [], function() {
    console.log('执行模块:admin');
    return {
        list: ['user_01', 'user_02', 'user_03']
    }
});

define('tool', [], function() {
    console.log('执行模块:tool');
    return {
        isAdmin: function(adminList, nick){
            return adminList.includes(nick);
        }
    }
});

define('adminValidator', ['admin', 'tool'], function(admin, tool) {
    console.log('执行模块:adminValidator');
    return function(nick) {
        const isAdmin = tool.isAdmin(admin.list, nick);
        console.log(`user ${nick} is admin ? \n -- ${isAdmin ? 'yes' : 'no'}`);
        return isAdmin;
    }
});

define('main', [], function() {
    console.log('执行模块:main');
    const adminValidator = require('adminValidator');
    let currentUserNick = window.prompt('请输入你的昵称');
    if(adminValidator(currentUserNick)){
        alert(`管理员${currentUserNick}访问系统`);
    }else{
        alert('非管理员,不能访问');
    }
});

console.time('time');
require('main');
console.timeEnd('time');

Xnip2022-05-19_08-08-27.jpg 更多关于require解析module id的例子此文不继续说明,有兴趣自己了解

工具打包

截止目前,大部分的项目还是在浏览器端使用的AMD规范(2022-05-23)。
实际前端开发中,我们几乎不会自己手写符合AMD规范的代码,而是写ES Module和require(CommonJS)。这是因为我们使用了打包工具,工具将我们的其他规范的代码转为了AMD规范。

以webpack为例,webpack内部使用__webpack_require__实现了模块化。

项目使用@vue/cli 4.5.7生成,vue.config.js

module.exports = {
    configureWebpack: (config) => {
        //去掉js压缩
        config.optimization.minimizer =[];
        //vue不打包进入主文件,减少其他代码的干扰
        config.externals = { 
            vue: 'Vue'
        }
    }
}

使用ES Module

/* tool.js */
function greet(nick){
    console.log(nick + ' hello');
}

export { greet };

/* main.js */
import Vue from 'vue'
import App from './App.vue'
// 这里
import { greet } from './tool.js';
greet('李向维');

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

app.xx.js

Xnip2022-05-19_09-30-47.jpg

/* __webpack_require__ 的定义 */

// The module cache
var installedModules = {};

// The require function
function __webpack_require__(moduleId) {
        // Check if module is in cache
        if(installedModules[moduleId]) {
                return installedModules[moduleId].exports;
        }
        // Create a new module (and put it into the cache)
        var module = installedModules[moduleId] = {
                i: moduleId,
                l: false,
                exports: {}
        };
        // Execute the module function
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
        // Flag the module as loaded
        module.l = true;
        // Return the exports of the module
        return module.exports;
}
// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;
// expose the module cache
__webpack_require__.c = installedModules;

/* __webpack_require__ 上还有其他的属性,这里不展示 */

logo(后面的代码中有用到)

"cf05":  (function(module, exports, __webpack_require__) {
      module.exports = __webpack_require__.p + "img/logo.82b9c7a5.png";
})

主模块

 /**删除了些没有用到的变量,注释 */
 
"56d7":  (function(module, __webpack_exports__, __webpack_require__) {

    "use strict";
    __webpack_require__.r(__webpack_exports__);
    /* delete */
    var staticRenderFns = [function () {
    var _vm=this;
    var _h=_vm.$createElement;
    var _c=_vm._self._c||_h;
    return _c('div',
    {attrs:{"id":"app"}},
    /* 这里使用了前面的logo的module*/
    [_c('img',{attrs:{"alt":"Vue logo","src":__webpack_require__("cf05")}})])}]
    /* delete */
    var App = (component.exports);
    
    // CONCATENATED MODULE: ./src/tool.js
    function greet(nick) {
      console.log(nick + ' hello');
    }
    // CONCATENATED MODULE: ./src/main.js

    greet('李向维');
    external_Vue_default.a.config.productionTip = false;
    new external_Vue_default.a({
      render: function render(h) {
        return h(App);
      }
    }).$mount('#app');
})

导入的greet定义直接移入了这个模块,而不是通过__webpack_require__的形式。
如果引入的内容中和main.js中的变量重名怎么办?

/* tool.js */
let count = 0;
function addCount(){
    count++
}
export { addCount };
console.log(count);

/* main.js */
import { addCount } from './tool.js';
addCount();
let count = 9;
console.log(count++);
// CONCATENATED MODULE: ./src/tool.js
var count = 0;
function addCount() {
  count++;
}
console.log(count);

// CONCATENATED MODULE: ./src/main.js
addCount();
var main_count = 9;
console.log(main_count++);
external_Vue_default.a.config.productionTip = false;

main.js中和tool.js中重名的变量进行了重命名。

使用CommonJS

/* tool.js */
function greet(nick){
    console.log(nick + ' hello');
}
module.exports = {
    greet
};

/* main.js */
const { greet }  = require('./tool');
greet('李向维');

打包后

"340d": (function(module, exports) {
    function greet(nick) {
      console.log(nick + ' hello');
    }
    module.exports = {
      greet: greet
    };
}),
"56d7": (function(){
    /* 只保留了部分代码 */
    var App = (component.exports);
    // CONCATENATED MODULE: ./src/main.js

    var _require = __webpack_require__("340d"),
        greet = _require.greet;

    greet('李向维');
    external_Vue_default.a.config.productionTip = false;
    new external_Vue_default.a({
      render: function render(h) {
        return h(App);
      }
    }).$mount('#app');
})

和处理ES Module不一样,使用了__webpack_require__加载模块