zoukankan      html  css  js  c++  java
  • JavaScript模块化笔记

    JavaScript模块化笔记

    一个模块就是一堆被封装到一个文件当中的代码,并使用export暴露部分代码给其他的文件。模块专注于一小部分功能并与应用的其他部分松耦合,这是因为模块间没有全局变量或共享变量,他们仅通过暴露的模块代码的一部分来进行通信。任何你想在另一个文件中访问的代码都可以被封装为模块。

    模块化历史

    没有模块的时代

    JavaScript刚出现时就是一个从上到下执行的脚本语言,简单的逻辑可以编写在一整个文件里,没有分块需求

    模块化萌芽时代

    Ajax的提出让前端变成了集许多功能为一身的类客户端,前端业务逻辑越来越复杂,代码越来越多,此时有许多问题

    1. 所有变量都定义在一个作用域,造成变量污染
    2. 没有命名空间,导致函数命名冲突
    3. HTML引入JavaScript时需要注意顺序依赖,多文件不好协调

    此时的一些解决方案

    1. 用自执行函数来包装代码,var将变量声明在局部作用域。但是还是会生成modA全局变量

      modA = function(){
           var a = 2, b = 3; //变量a、b外部不可见
           return {
                add : function(){
                     console.log(a, b);
                }
           }
      }()
      
    2. 为了避免全局变量冲突的Java包命名风格,麻烦复杂而且还是挂载在全局变量上

      app.util.modA = xxx;
      app.tools.modeA = xxx;
      
    3. IIFE匿名自执行函数,将函数内容放在括号中防止其内部变量泄露,函数接受window并将其需要对外放开的功能挂载在全局变量上

      (function(window) {
          // ...
          window.jQuery = window.$ = jQuery;
      })(window);
      

    模块化需要解决的问题

    1. 如何安全的不污染模块外代码的方式包装一个模块的代码
    2. 如何标识唯一的模块从而能被外部轻易调用
    3. 如何既不增加全局变量也能把模块API暴露出去
    4. 如何在其他模块内方便的引入所依赖的模块

    模块化

    CommonJS

    CommonJS的模块定义如下

    1. 模块的标识符
      • 使用/分割的由词组成的字符串
      • 词必须是驼峰格式可以使用...
      • 模块标识符不能添加文件的扩展名例如.js
      • 模块标识符可以是相对路径或顶层标识,相对的标识符使用...开头
      • 顶层标识符相对于在虚拟模块命名空间根上解析
      • 相对标识符相对于调用该相对标识符的模块位置解析
    2. 模块上下文
      • 在模块中有一个require函数,该函数接受一个模块标识符,返回被require的依赖模块中被export暴露的API,如果依赖中有依赖则依次加载这些依赖;如果被请求的模块不能被返回,require函数会抛出一个异常
      • 在模块中有一个exports对象变量,模块需要在执行过程中向其添加需要被暴露的API
      • 模块执行使用exports执行导出

    CommonJS的例子

    // 定义模块math.js
    var basicNum = 0;
    function add(a, b) {
      return a + b;
    }
    module.exports = { //在这里写上需要向外暴露的函数、变量
      add: add,
      basicNum: basicNum
    }
    
    // 引用自定义的模块时,参数包含路径,可省略.js
    var math = require('./math');
    math.add(2, 5);
    
    // 引用核心模块时,不需要带路径
    var http = require('http');
    http.createService(...).listen(3000);
    
    

    CommonJS是运行时动态同步加载模块,模块被加载为对象,而浏览器如果在运行时加载需要单独下载模块文件开销很大,所以一般被用在服务器这种本地环境中例如Nodejs

    // CommonJS
    let { stat, exists, readfile } = require('fs');
    
    // 等同于
    let _fs = require('fs');
    let stat = _fs.stat;
    let exists = _fs.exists;
    let readfile = _fs.readfile;
    

    AMD

    AMD(Asynchronous Module Definition),使用异步方式加载模块,模块加载不影响后面语句的执行。Require.js实现了AMD规范。AMD使用require.config()执行路径等配置、define()定义模块,require()加载模块。

    AMD推崇依赖前置(definerequire函数直接传入依赖ID,依赖将进入factory中作为参数)、提前执行(直接依赖前置时加载的模块将会被先执行一次,除非使用后置require的方法,实际这个问题经过实验已被解决,所有模块将不会被提前执行一遍)

    AMD的规范源于CommonJS所以其中的定义与CommonJS有许多相似之处

    • 使用define()函数用来定义模块,函数接受三个参数
      • id类似CommonJS的模块标识符,可选
      • dependencies依赖的模块ID数组,可选,依赖会先于后面介绍的工厂函数执行,依赖获取结果也会参数形式传入工厂参数,默认为["require", "exports", "module"]
      • factory工厂参数,用来实例化一个模块或对象,如果工厂是一个函数则会被执行一次返回值作为模块对外暴露的值,如果工厂是一个对象,那么对象将会被作为工厂对外暴露的值。暴露值的方法有三种:returnexports.xxx=xxxmodule.exports=xxx
    • Require.js中的require()引用函数,函数接受两个参数
      • dependencies依赖的模块ID数组,如define()中的dependencies差不多
      • function利用模块或直接执行的代码方法,前面的dependencies会被传入该方程中

    Require.js的例子

    // foo/title.js 默认的名称就会为title
    // id默认为title.js当前目录下查找
    define(["./cart", "./inventory"], function(cart, inventory) {
    		//return an object to define the "my/shirt" module.
            return {
                color: "blue",
                size: "large",
                addToCart: function() {
                    inventory.decrement(this);
                    cart.add(this);
                }
            }
        }
    );
    
    require("title.js", function(title) {
        console.log(title.color);
    });
    
    // 可以在define中require,但是要把define添加到依赖中
    define(["require"], function(require) {
        var mod = require("./relative/name");
    });
    

    但是AMD有其自身问题

    • 模块代码在被定义时会被执行,不符合预期且开销较大

    • 罗列依赖模块导致definerequire的参数长度过长

      这一点可以通过在define中使用require解决,当使用这种编写模式时只有在特别调用require的时候才下载该模块的代码

      define(function(){
           console.log('main2.js执行');
      
           require(['a'], function(a){
                a.hello();    
           });
      
           $('#b').click(function(){
               // 只有在用户点击该按钮后才会下载
                require(['b'], function(b){
                     b.hello();
                });
           });
      });
      

      AMD还部分兼容Modules/Wrappings写法

      // d.js factory的形参得写上
      define(function(require, exports, module){
           console.log('d.js执行');
           return {
                helloA: function(){
                     var a = require('a');
                     a.hello();
                },
                run: function(){
                     $('#b').click(function(){
                          var b = require('b');
                          b.hello();
                     });
                }
           }
      });
      

    CMD

    CMD(Common Module Definition)是淘宝前端根据Modules/Wrappings规范结合了各家所长,支持CommonJS的exports和module.exports语法,支持AMD的return的写法,暴露的API可以是任意类型的。

    CMD推崇依赖就近(require在调用依赖紧前调用)、延迟执行(不会在刚下载完成就执行,而是等待用户调用)

    //a.js
    define(function(require, exports, module){
         console.log('a.js执行');
         return {
              hello: function(){
                   console.log('hello, a.js');
              }
         }
    });
    
    //b.js
    define(function(require, exports, module){
         console.log('b.js执行');
         return {
              hello: function(){
                   console.log('hello, b.js');
              }
         }
    });
    
    //main.js
    define(function(require, exports, module){
         console.log('main.js执行');
         var a = require('a');
         a.hello();    
         $('#b').click(function(){
              var b = require('b');
              b.hello();
         });
    });
    

    sea.js实现了CMD标准,它通过对函数toString()并正则匹配到require语句来分析依赖,所有依赖将会被预先下载并延迟执行。如果想延迟下载可以使用require.asyncAPI。

    AMD对比CMD

    /** AMD写法 amd.js **/
    define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
         // 等于在最前面声明并初始化了要用到的所有模块
        a.doSomething();
        if (false) {
            b.doSomething()
        }
    });
    
    require(["amd.js"])
    
    /** CMD写法 **/
    define(function(require, exports, module) {
        var a = require('./a'); //在需要时申明
        a.doSomething();
        if (false) {
            var b = require('./b');
            b.doSomething();
        }
    });
    
    /** sea.js **/
    // 定义模块 math.js
    define(function(require, exports, module) {
        var $ = require('jquery.js');
        var add = function(a,b){
            return a+b;
        }
        exports.add = add;
    });
    // 加载模块
    seajs.use(['math.js'], function(math){
        var sum = math.add(1+2);
    });
    
    

    其实现在的情况是:

    • AMD的require.js如果使用dependencies中定义好了之后会在初始化阶段获取并初始化所有依赖,即使依赖在后续并未被调用

      // 内联JavaScript的调用
      require(["./main"]);
      
      // main.js
      define(["./a", "./b"], function(a, b) {
          if (false) {
              a.hello();	// 即使不使用
              b.hello()
          }
      });
      
      // a.js
      define(function() {
          console.log("a init");
          return {
              hello: function() {
                  console.log("a.hello executed");
              }
          }
      });
      
      // b.js
      define(function() {
          console.log("b init");
          return {
              hello: function() {
                  console.log("b.hello executed");
              }
          }
      });
      
      // 刚打开页面上述代码执行结果
      // a init b init 看Network三个文件都被下载
      

      如果使用require,则文件将会在require之后被加载,如果require未被执行则不下载,下方代码中b.js将会在按钮按下下载与执行

      // 上方main.js更改为
      define(function(){
          console.log('son.js executed');
      
          require(['a'], function(a){
              a.hello();
          });
          document.getElementById("btn").addEventListener("click", function(){
              require(['b'], function(b){
                  b.hello();
              });
          });
      });
      
    • CMD直接使用require,且无论require是否执行都会下载代码内require()依赖的代码,这是因为其正则匹配方式,使用require.async延迟下载

    ES6 Module

    ES6 Module自动使用严格模式,主要有以下限制

    • 变量必须声明后再使用
    • 函数的参数不能有同名属性,否则报错
    • 不能使用with语句
    • 不能对只读属性赋值,否则报错
    • 不能使用前缀0表示八进制数,否则报错
    • 不能删除不可删除的属性,否则报错
    • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
    • eval不会在它的外层作用域引入变量
    • evalarguments不能被重新赋值
    • arguments不会自动反映函数参数的变化
    • 不能使用arguments.callee
    • 不能使用arguments.caller
    • 禁止this指向全局对象
    • 不能使用fn.callerfn.arguments获取函数调用的堆栈
    • 增加了保留字(比如protectedstaticinterface

    ES6模块化主要由exportimport组成,export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能,一个模块是一个独立的文件。importexport必须处在模块顶层用于静态优化,不能在动态运行的代码块中

    export命令

    export导出命令规定导出对外的接口,不能直接输出值,所以export 1是错误的

    // profile.js
    export var firstName = 'Michael';
    export var lastName = 'Jackson';
    export var year = 1958;
    
    var firstName = 'Michael';
    var lastName = 'Jackson';
    var year = 1958;
    export {firstName, lastName, year};
    
    // 默认导出是其本身的名字
    export function multiply(x, y) {
      return x * y;
    };
    
    function v1() { ... }
    function v2() { ... }
    // 可以使用as对导出内容重命名
    export {
      v1 as streamV1,
      v2 as streamV2,
      v2 as streamLatestVersion
    };
    

    export default的其他用法

    export default 42;	// 默认值
    export default class { ... }
    

    import命令

    // main.js 如果非export default需要大括号内变量名需要与模块的对外接口名一致
    import {firstName, lastName, year} from './profile';
    
    function setName(element) {
      element.textContent = firstName + ' ' + lastName;
    }
    
    // 起别名依旧使用as
    import { lastName as surname } from './profile';
    

    静态执行的import不能与任何动态代码进行组合使用。多次重复的import不会重复执行

    // 第一组
    export default function crc32() { // 输出
    }
    import crc32 from 'crc32'; // 输入
    
    // 第二组
    export function crc32() { // 输出
    };
    import {crc32} from 'crc32'; // 输入
    

    使用export default默认导出时可以不使用大括号因为导出项只可能有一个,如果想同时输入默认方法和其他变量可以写成下面的样式

    import _, { each } from 'lodash';
    

    可以使用*做整体加载

    // circle.js
    export function area(radius) {
      return Math.PI * radius * radius;
    }
    export function circumference(radius) {
      return 2 * Math.PI * radius;
    }
    
    // main.js
    import * as circle from './circle';
    
    console.log('圆面积:' + circle.area(4));
    console.log('圆周长:' + circle.circumference(14));
    

    export import复合写法

    export { foo, bar } from 'my_module';
    
    // 可以简单理解为
    import { foo, bar } from 'my_module';
    export { foo, bar };
    
    // 接口改名
    export { foo as myFoo } from 'my_module';
    
    // 整体输出
    export * from 'my_module';
    
    // 默认接口
    export { default } from 'foo';
    
    // 有名字的改成默认接口
    export { es6 as default } from './someModule';
    // 等同于
    import { es6 } from './someModule';
    export default es6;
    
    
    

    模块的继承

    假设circleplus模块继承了circle模块。export *会默认忽略circle模块的default方法,然后子模块复写了其default方法

    // circleplus.js
    export * from 'circle';
    export var e = 2.71828182846;
    export default function(x) {
      return Math.exp(x);
    }
    
    // 调用circleplus.js
    import * as math from 'circleplus';
    import exp from 'circleplus';
    console.log(exp(math.e));
    

    import()

    ES6的模块化实现是编译时加载(静态加载)、模块输出值引用的方式。CommonJS中模块引用是值的拷贝,导致修改分别导出的内容两者虽然可能在子依赖中有关联,但是在父模块中不会表现,也就是输出之后模块本身改变不了已经导出给其他模块的值

    // module.js
    var data = 5;
    var doSomething = function () {
      data++;
    };
    // 暴露的接口
    module.exports.data = data;
    module.exports.doSomething = doSomething;
    
    var example = require('./module.js');
    console.log(example.data); // 5
    example.doSomething(); 
    console.log(example.data); // 5
    

    如果暴露一个getter函数就可以正确取到了

    var counter = 3;
    function incCounter() {
      counter++;
    }
    module.exports = {
      get counter() {
        return counter
      },
      incCounter: incCounter,
    };
    

    而在ES6 Module中是值的只读引用,模块内值父模块和模块本身都可以访问且修改

    // lib.js
    export let counter = 3;
    export function incCounter() {
      counter++;
    }
    
    // main.js
    import { counter, incCounter } from './lib';
    console.log(counter); // 3
    incCounter();
    console.log(counter); // 4
    

    ES6 Module的引用可以添加值但是不可以重新赋值,因为导入的其实是一个只读的对象的地址,对象的内容可以修改但是其本身指向不能变

    // lib.js
    export let obj = {};
    
    // main.js
    import { obj } from './lib';
    
    obj.prop = 123; // OK
    obj = {}; // TypeError
    

    但是为了实现运行时动态加载,可以使用ES2020提案中引入的import()函数,该函数支持动态加载模块,其接受一个与import命令相似的参数,函数返回一个Promise对象,import是异步加载,而Node的require是同步加载

    const main = document.querySelector('main');
    
    import(`./section-modules/${someVariable}.js`)
      .then(module => {
        module.loadPageInto(main);
      })
      .catch(err => {
        main.textContent = err.message;
      });
    

    使用import()的场景:

    1. 按需加载模块

      button.addEventListener('click', event => {
        import('./dialogBox.js')
        .then(dialogBox => {
          dialogBox.open();
        })
        .catch(error => {
          /* Error handling */
        })
      });
      
    2. 条件加载

      if (condition) {
        import('moduleA').then(...);
      } else {
        import('moduleB').then(...);
      }
      
    3. 动态模块路径生成,和上方字面量用法相似

      import(f())
      .then(...);
      

    import加载成功以后,模块会作为一个对象当作then方法的参数,可以使用对象结构语法获得输出接口

    import('./myModule.js')
    .then(({export1, export2}) => {
      // ...·
    });
    
    // default接口可以直接用参数获得
    import('./myModule.js')
    .then(myModule => {
      console.log(myModule.default);
    });
    
    // 具名输入
    import('./myModule.js')
    .then(({default: theDefault}) => {
      console.log(theDefault);
    });
    

    多个同时加载

    Promise.all([
      import('./module1.js'),
      import('./module2.js'),
      import('./module3.js'),
    ])
    .then(([module1, module2, module3]) => {
       ···
    });
    

    可以用在async await函数中

    async function main() {
      const myModule = await import('./myModule.js');
      const {export1, export2} = await import('./myModule.js');
      const [module1, module2, module3] =
        await Promise.all([
          import('./module1.js'),
          import('./module2.js'),
          import('./module3.js'),
        ]);
    }
    main();
    
  • 相关阅读:
    java笔记
    java面向对象
    Oracle数据库基础
    Java中的集合和常用类
    Java面向对象的三个特征
    Java中的类与对象
    Java中的冒泡排序
    JAVA中的一些内置方法
    JAVA中的数据类型
    SSH整合
  • 原文地址:https://www.cnblogs.com/camwang/p/14037543.html
Copyright © 2011-2022 走看看