js模块化

184 阅读5分钟

目标

  • commonJS模块化规范介绍及使用
  • AMD,CMD,SystemJS模块化规范介绍及使用
  • ESModule规范及与其他规范的区别
  • 函数式编程

知识要点

幼年期:无模块化

js最初设计目的

  • 页面动画
  • 表单处理,提交,数据的验证
  • 与浏览器的交互
  • 无模块化,命名空间的概念
  • 多种js文件分在不同的文件中
  • 不同的文件又被同一个模板引用
<script src='jquery.js'></script>
<script src='main.js'></script>
<script src='dep1.js'></script>

优点

  • 文件分离是最基础的模块化的第一步

问题

  • 污染全局作用域,函数名冲突,全局变量灾难
  • 不利于大型项目的开发以及多人团队的共建

成长期:模块化的开始-IIFE(语法侧的优化)

1.利用块级作用域

(()=>{
    let count=0;
    //...
})
//立即执行
(()=>{
    let count =0
})();

定义简单的模块

const iifeMoudle = (() => {
    const helloWorld = 'helloWorld';
    const syaHi = () => {
        console.log(helloWorld)
    }
    return {
        syaHi: syaHi,
    }
})();
iifeMoudle.syaHi();

问题:有额外的依赖时,如何优化IIFE相关代码

传参

const iifeMoudle1 = (() => {
    const w = 'world';
    const world = () => {
        return w;
    }
    return {
        world: world,
    }
})();
​
const iifeMoudle2 = ((dependency1) => {
    const helloWorld = 'hello ';
    const syaHi = () => {
        console.log(helloWorld + dependency1.world())
    }
    return {
        syaHi: syaHi,
    }
})(iifeMoudle1);
iifeMoudle2.syaHi();

问题:早期jquery的依赖处理以及模块加载方案?/ 传统IIFE是如何解决多方依赖的问题 答:IIFE加传参调配

实际上,jquery等框架其实应用了revealing的写法: 揭示模式

const iifeModule = ((dependencyModule1, dependencyModule2) => {
  let count = 0;
  const increase = () => ++count;
  const reset = () => {
    count = 0;
  }
​
  return {
    increase, reset
  }
})(dependencyModule1, dependencyModule2);
iifeModule.increase();
iifeModule.reset();

成熟期

CJS-commonjs

node.js制定

  • module.exports/ exports对外暴露接口
  • 通过require来调用其他模块

例子

// 引入部分
const dependencyModule1 = require(./dependencyModule1);
const dependencyModule2 = require(./dependencyModule2);
​
// 处理部分
let count = 0;
const increase = () => ++count;
const reset = () => {
  count = 0;
}
// 做一些跟引入依赖相关事宜……
​
// 暴露接口部分
exports.increase = increase;
exports.reset = reset;module.exports = {
  increase, reset
}

使用:

  const { increase, reset } = require('./main.js');
  increase();
  reset();

问题: nodejs 模块实际执行是怎么处理

  (function (thisValue, exports, require, module) {
    const dependencyModule1 = require(./dependencyModule1);
    const dependencyModule2 = require(./dependencyModule2);
​
    // 业务逻辑……
  }).call(thisValue, exports, require, module);

优点

  • Commonjs 率先在服务端实现了,从框架层面解决依赖,全局变量污染的问题

缺点

  • 主要针对服务端的解决方案,对于异步拉取依赖的处理整合不是那么的友好
  • 无法直接用于浏览器端 ,外层没有function包裹,变量全暴漏在全局

新的问题,异步依赖

AMD规范 浏览器端的js模块化规范AMD(Asynchronous Module Definition)

  • 异步加载+允许指定回调函数
  • 经典实现框架:require.js

定义方式:

  • define(id,[depends],callback)

应用方式

  • require([module],callback);

模块定义例子

define('amdModule',['dep1','dep2'],(dep1,dep2)=>{
    //业务逻辑
    //处理部分
    let count=0
    const inscrease =()=>{
        count=0;
    }
    return {
        increase,
        reset
    }
})

引入模块

require(['amdModule'],[],requre=>{
    //引入部分
    const dep1 = require('./dep1')
    let count=0
    const inscrease =()=>{
        count=0;
    }
    return {
        increase,
        reset
    }
})

问题:AMD中使用revealing

define('amdModule',[],(require,export,module)=>{
    const dep1 = require('./dep1')
    let count=0
    const inscrease =()=>{
        count=0;
    }
    
     export.increase = increase();
})
define('amdModule',[],require=>{
    const otherModule = require('amdModule');
    otherModule.increase();
})

问题: 兼容AMD& CJS如何判断CJS和AMD

(
   define('amdModule',[],(require,export,module)=>{
    const dep1 = require('./dep1')
    let count=0
    const inscrease =()=>{
        count=0;
    }
    
     export.increase = increase();
}) 
)(
    //区分commonjs , amd
    typeof module ==='object'
    && module.exports 
    && typeof define !=='function'? 
    factory => module.exports = factory(requre,exports,module):define
)

优点

  • 适合在浏览器中加载异步模块,可以并行加载多个模块

缺点

  • 会有引入成本,不能按需加载

CMD规范

  • 按需加载
define('module', (require, exports, module) => {
    let $ = require('jquery');
    // jquery相关逻辑
​
    let dependencyModule1 = require('./dependecyModule1');
    // dependencyModule1相关逻辑
  })
优点
  • 按需加载,依赖就近

缺点

  • 依赖于打包,加载逻辑存在于每个模块中,扩大模块的体积

问题:AMD&CMD区别

  • 依赖就近,按需加载

ES6模块

引入关键字 —— import 导出关键字 —— export

模块引入、导出和定义的地方:

  // 引入区域
  import dependencyModule1 from './dependencyModule1.js';
  import dependencyModule2 from './dependencyModule2.js';
​
  // 实现代码逻辑
  let count = 0;
  export const increase = () => ++count;
  export const reset = () => {
    count = 0;
  }
​
  // 导出区域
  export default {
    increase, reset
  }

模板引入的地方

  <script type="module" src="esModule.js"></script>

node中:

  import { increase, reset } from './esModule.mjs';
  increase();
  reset();
​
  import esModule from './esModule.mjs';
  esModule.increase();
  esModule.reset();

问题:动态模块

问题:动态模块 考察:export promise

ES11原生解决方案:

  import('./esModule.js').then(dynamicEsModule => {
    dynamicEsModule.increase();
  })
优点(重要性)
  • 通过一种最统一的形态整合了js的模块化
缺点(局限性):
  • 本质上还是运行时的依赖分析

解决模块化的新思路 - 前端工程化

背景

根本问题 - 运行时进行依赖分析

前端的模块化处理方案依赖于运行时分析

解决方案:线下执行 grunt gulp webpack

  <!doctype html>
    <script src="main.js"></script>
    <script>
      // 给构建工具一个标识位
      require.config(__FRAME_CONFIG__);
    </script>
    <script>
      require(['a', 'e'], () => {
        // 业务处理
      })
    </script>
  </html>
  define('a', () => {
    let b = require('b');
    let c = require('c');
​
    export.run = () {
      // run
    }
  })
工程化实现

step1: 扫描依赖关系表:

  {
    a: ['b', 'c'],
    b: ['d'],
    e: []
  }

step2: 重新生成依赖数据模板

  <!doctype html>
    <script src="main.js"></script>
    <script>
      // 构建工具生成数据
      require.config({
        "deps": {
          a: ['b', 'c'],
          b: ['d'],
          e: []
        }
      })
    </script>
    <script>
      require(['a', 'e'], () => {
        // 业务处理
      })
    </script>
  </html>

step3: 执行工具,采用模块化方案解决模块化处理依赖

  define('a', ['b', 'c'], () => {
    // 执行代码
    export.run = () => {}
  })

优点:

  1. 构建时生成配置,运行时执行
  2. 最终转化成执行处理依赖
  3. 可以拓展

完全体 webpack为核心的工程化 + mvvm框架组件化 + 设计模式

补充知识点

无模块

缺点

  • 多个文件不能有同名的函数,变量
  • 依赖关系不好管理
//b.js需要依赖a.js,的书写顺序
<script type='text/javascript' src='a.js' />
<script type='text/javascript' src='b.js' />

萌芽时代

java风格的命名空间

app.util.modA =xxx;
app.tools.modA.format();

jQuery风格的匿名自执行函数

(function(window){
    //代码
    window.jQuery = window.$ = jQuery;//通过给window添加属性而暴漏到全局
})(window);

未解决根本问题,依赖还是需要外部提供,增加了全局变量

模块化面临的问题

  • 如何安全的包装一个模块的代码?(不污染模块外的任何代码)
  • 如何唯一标识一个模块?
  • 如何优雅的把模块的API暴漏出去?(不能增加全局变量)
  • 如何方便的使用所依赖的模块?

nodejs的规范CommonJs

Modules/1.0规范

  1. 模块的标识应遵循的规则(书写规范)
  1. 定义全局函数require,通过传入模块标识来引入其他模块,执行的结果即为别的模块暴漏出来的API
  1. 如果被require函数引入的模块中也包含依赖,那么依次加载这些依赖
  1. 如果引入模块失败,那么require函数应该报一个异常
  1. 模块通过变量exports来向往暴漏API,exports只能是一个对象,暴漏的API须作为此对象的属性。

缺点

  • 无法直接用于浏览器端,外层没有function包裹,变量全暴漏在全局

AMD规范

  1. 用全局函数define来定义模块,用法为:define(id?, dependencies?, factory);
  1. id为模块标识,遵从CommonJS Module Identifiers规范
  1. dependencies为依赖的模块数组,在factory中需传入形参与之一一对应
  1. 如果dependencies的值中有"require"、"exports"或"module",则与commonjs中的实现保持一致
  1. 如果dependencies省略不写,则默认为["require", "exports", "module"],factory中也会默认传入require,exports,module
  1. 如果factory为函数,模块对外暴漏API的方法有三种:return任意类型的数据、exports.xxx=xxx、module.exports=xxx
  1. 如果factory为对象,则该对象即为模块的返回值
//a.js
define(function(){
     console.log('a.js执行');
     return {
          hello: function(){
               console.log('hello, a.js');
          }
     }
});
//b.js
define(function(){
     console.log('b.js执行');
     return {
          hello: function(){
               console.log('hello, b.js');
          }
     }
});
//main.js
require(['a', 'b'], function(a, b){
     console.log('main.js执行');
     a.hello();
     $('#b').click(function(){
          b.hello();
     });
})

缺点

  • 如果一个模块依赖了十个其他模块,那么在本模块的代码执行之前,要先把其他十个模块的代码都执行一遍,不管这些模块是不是马上会被用到。这个性能消耗是不容忽视的。

  • 在定义模块的时候,要把所有依赖模块都罗列一遍,而且还要在factory中作为形参传进去,要写两遍很大一串模块名称

    解决

    define(function(){
         console.log('main2.js执行');
    ​
         require(['a'], function(a){
              a.hello();    
         });
    ​
         $('#b').click(function(){
              require(['b'], function(b){
                   b.hello();
              });
         });
    });
    //d.js
    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/seajs

后起之秀seajs,seajs的作者是国内大牛淘宝前端布道者玉伯。seajs全面拥抱Modules/Wrappings规范

Modules/Wrappings规范

资源的下载阶段还是预先进行,资源执行阶段后置,等到需要的时候再执行。这样一种折衷的方式,能够融合前面两种方式的优点,而又回避了缺点。

这就是Modules/Wrappings规范

//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();
     });
    
});

main.js执行 a.js执行 hello, a.js

a.js和b.js都会预先下载,但是b.js中的代码却没有执行,因为还没有点击按钮。当点击按钮的时候,会输出如下:

b.js执行 hello, b.js

可以看到b.js中的代码此时才执行。这样就真正实现了“就近书写,延迟执行“,不可谓不优雅

学习文章 www.cnblogs.com/lvdabao/p/j…