最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 今天要讲的几个模块化规范CMJ、AMD、CMD、ESM,以及简单实现AMD、CMD

    正文概述 掘金(这是一个独特的昵称)   2021-06-01   807

    CMJ(CommonJS规范)

    谈到CMJ我们很容易想到node.js因为node所使用的模块化规范正是CMJ,侧面说明了CMJ是一个适用与服务器的模块化规范。 CMJ有几个主要方法分别为:module、exports、require、global。 在开发中我们用module.exports 来导出模块,用 require来加载模块。 我们来看个简单的用例: math.js

    //在math.js中,我们定义了一个变量和一个函数并把它暴露出来
    let num = 0;
    function add(a, b) {
      return a + b;
    }
    module.exports = { //在这向外暴露出函数与变量
      add: add,
      num: num
    }
    

    index.js

    //我们在这里使用require方法引用math.js
    let math = require('./math');
    //这时我们可以调用到math中的add方法。
    math.add(4, 9);
    

    这里要注意的是引用自定义的模块时,参数包含路径,可省略.js,而引用核心模块时,不需要带路径。 因为CommonJS用同步的方式加载模块。在浏览器中我们用同步方式加载模块时用户将无法继续操作网页,所以前端需要一个能异步加载模块的模块化规范。

    文件是一个模块,私有。内置两个变量 module require (exports = module.exports)

    一个引入一个导出,就构成了通信的基本结构

    需要注意的两个问题

    1. 缓存,require 会缓存一下,所以
    // a.js
    var name = 'morrain'
    var age = 18
    exports.name = name
    exports.getAge = function(){
        return age
    }
    // b.js
    var a = require('a.js')
    console.log(a.name) // 'morrain'
    a.name = 'rename'
    var b = require('a.js')
    console.log(b.name) // 'rename'
    
    1. 引用拷贝还是值拷贝的问题(CMJ 是值拷贝)
    // a.js
    var name = 'morrain'
    var age = 18
    exports.name = name
    exports.age = age
    exports.setAge = function(a){
        age = a
    }
    // b.js
    var a = require('a.js')
    console.log(a.age) // 18
    a.setAge(19)
    console.log(a.age) // 18
    
    1. 运行时加载 / 编译时加载(多阶段,异步)ESM

    AMD

    AMD就是一个异步加载模块的模块化规范。AMD的实现就是require.js AMD规范定义了一个函数define,通过define方法定义模块

    define(id?, dependencies?, factory);
    
    • id:可选参数,用来定义模块的标识,如果没有提供该参数,模块标识就取脚本文件名(去掉扩展名)。
    • dependencies:可选参数,用来传入当前模块依赖的模块名称数组。
    • factory:工厂方法,模块初始化要执行的函数或对象,如果是函数,它只被执行一次。如果是对象,此对象应该为模块的输出值。定义模块例子:
    define('a', function () {
      console.log('a load')
      return {
        run: function () { console.log('a run') }
      }
    })
    

    在require.js 中还有require.config和require,我们以加载一个jquery库为例:

    require.config({
        paths : {
            "jquery" : ["http://libs.baidu.com/jquery/2.0.3/jquery"]   
        }
    })
    //这时做完上面的操作后我们就可以直接用jquery来引入模块,不用后面那一大串CDN地址了
    require(["jquery"],function($){
        $(function(){
            alert("load finished");  
        })
    })
    

    依赖前置,换句话说,在解析和执行当前模块之前,模块作者必须指明当前模块所依赖的模块。

    代码在一旦运行到此处,能立即知晓依赖。而无需遍历整个函数体找到它的依赖,因此性能有所提升,缺点就是开发者必须显式得指明依赖——这会使得开发工作量变大,比如:当你写到函数体内部几百上千行的时候,忽然发现需要增加一个依赖,你不得不回到函数顶端来将这个依赖添加进数组。

    define('a', function () {
      console.log('a load')
      return {
        run: function () { console.log('a run') }
      }
    })
    
    define('b', function () {
      console.log('b load')
      return {
        run: function () { console.log('b run') }
      }
    })
    
    require(['a', 'b'], function (a, b) {
      console.log('main run') 
      a.run()
      b.run()
    })
    
    // a load
    // b load
    // main run
    // a run
    // b run
    //从这个打印结果我们可以看出,ADM在require时就把所有模块都加载了并没有管你有没有用到他,这就是依赖前置。
    

    简单实现

    const def = new Map();
    
    // AMD mini impl
    const defaultOptions = {
      paths: ''
    }
    
    // From CDN 加载CDN (System 加载库)
    const __import = (url) => {
      return new Promise((resove, reject) => {
        System.import(url).then(resove, reject)
      })
    }
    
    // normal script 读取路径
    const __load = (url) => {
      return new Promise((resolve, reject) => {
        const head = document.getElementsByTagName('head')[0];
        const node = document.createElement('script');
        node.type = 'text/javascript';
        node.src = url;
        node.async = true;
        node.onload = resolve;
        node.onerror = reject;
        head.appendChild(node)
      })
    }
    
    // 为啥没写 let const var
    // 千万不要在实际使用这种比较 low 的方式 ?
    rj = {};
    
    rj.config = (options) => Object.assign(defaultOptions, options);
    
    // 定义模块,触发的时机其实是在 require 的时候,所以 -> 收集
    define = (name, deps, factory) => {
      // todo 参数的判断,互换
      def.set(name, { name, deps, factory });
    }
    
    // dep -> a -> a.js -> 'http:xxxx/xx/xx/a.js';
    const __getUrl = (dep) => {
      const p = location.pathname;
      return p.slice(0, p.lastIndexOf('/')) + '/' + dep + '.js';
    }
    
    // 其实才是触发加载依赖的地方
    require = (deps, factory) => {
      return new Promise((resolve, reject) => {
        Promise.all(deps.map(dep => {
          // 走 CDN
          if (defaultOptions.paths[dep]) return __import(defaultOptions.paths[dep]);
    
          return __load(__getUrl(dep)).then(() => {
            const { deps, factory } = def.get(dep);
            if (deps.length === 0) return factory(null);
            return require(deps, factory)
          })
        })).then(resolve, reject)
      })
      .then(instances => factory(...instances))
    }
    
    

    CMD

    代表技术实现 Seajs CMD例子:

    //function有三个参数:require参数用来引入别的模块,exports和module用来导出模块公共接口。
    define('a', function (require, exports, module) {
      console.log('a load')
      exports.run = function () { console.log('a run') }
    })
    
    define('b', function (require, exports, module) {
      console.log('b load')
      exports.run = function () { console.log('b run') }
    })
    
    define('main', function (require, exports, module) {
      console.log('main run')
      var a = require('a')
      a.run()
      var b = require('b')
      b.run()
    })
    
    seajs.use('main')
    

    我们可以看到我们的依赖是用require按需要来引入的。这就是依赖后置。而且我们需要调用seajs.use()方法来执行这个模块。

    简单实现

    const modules = {};
    const exports = {};
    sj = {};
    
    const toUrl = (dep) => {
      const p = location.pathname;
      return p.slice(0, p.lastIndexOf('/')) + '/' + dep + '.js';
    }
    
    const getDepsFromFn = (fn) => {
      let matches = [];
      // require('a ')
      //1. (?:require\() -> require(  -> (?:) 非捕获性分组
      //2. (?:['"]) -> require('
      //3. ([^'"]+) -> a -> 避免回溯 -> 回溯 状态机
      let reg = /(?:require\()(?:['"])([^'"]+)/g; // todo
      let r = null;
      while((r = reg.exec(fn.toString())) !== null) {
        reg.lastIndex
        matches.push(r[1])
      }
    
      return matches
    }
    
    const __load = (url) => {
      return new Promise((resolve, reject) => {
        const head = document.getElementsByTagName('head')[0];
        const node = document.createElement('script');
        node.type = 'text/javascript';
        node.src = url;
        node.async = true;
        node.onload = resolve;
        node.onerror = reject;
        head.appendChild(node)
      })
    }
    
    // 依赖呢?
    // 提取依赖: 1. 正则表达式 2. 状态机
    define = (id, factory) => {
      const url = toUrl(id);
      const deps = getDepsFromFn(factory);
      if (!modules[id]) {
        modules[id] = { url, id, factory, deps }
      }
    }
    
    const __exports = (id) => exports[id] || (exports[id] = {});
    const __module = this;
    // 这里面才是加载模块的地方
    const __require = (id) => {
      return __load(toUrl(id)).then(() => {
        // 加载之后
        const { factory, deps } = modules[id];
        if (!deps || deps.length === 0) {
          factory(__require, __exports(id), __module);
          return __exports(id);
        }
    
        return sj.use(deps, factory);
      })
    }
    
    sj.use = (mods, callback) => {
      mods = Array.isArray(mods) ? mods : [mods];
      return new Promise((resolve, reject) => {
        Promise.all(mods.map(mod => {
          return __load(toUrl(mod)).then(() => {
            const { factory } = modules[mod];
            return factory(__require, __exports(mod), __module)
          })
        })).then(resolve, reject)
      }).then(instances => callback && callback(...instances))
    }
    

    补充

    有时候遇到递归调用,总是觉得非常绕,这时候我们可以试着拆开来看待,把它假设成为一个最基础简单的单过程

    假设 模块 A 依赖 模块 B , 模块 B 则 无需依赖

    我们简化下模型

    假设只加载模块 B

    定义 B 模块

    define('B', function () {
        console.log('B load')
        return {
            run: function () { console.log('B run') }
        }
    })
    

    引入 模块 B

    require(["B"],function(B){
        B.run()
    })
    

    实现

    require = (deps, factory) => {
        return new Promise((resolve, reject) => {
           // 循环调用依赖
          Promise.all(deps.map(dep => {
            // 走 CDN
            if (defaultOptions.paths[dep]) return __import(defaultOptions.paths[dep]);
            // 本地引用
            return __load(__getUrl(dep)).then(() => {
              const { deps, factory } = def.get(dep); // 获取 define 时存储的 B 模块
              return factory(null); // 执行 B 模块, 返回 B 模块 return 结果
            })
          })).then(resolve, reject) // 将 ↑ 执行结果放回 resolve 的值为 Promise.all 执行完成后的 factory 范围值 数组
        })
        .then(instances => factory(...instances)) // 返回结果 执行 require 的 回调函数 factory
      }
    

    当 A 模块存在引用 B 模块时

    定义 A 模块

    define('A', ['B'], function (B) {
        console.log('A load')
        return {
            run: function () { B.run() }
        }
    })
    

    引入 模块 A 时则要去检测 A 模块需要依赖的模块,当假设每个模块都有自己的依赖时,这个过程就像是一个树状图,不断地调用 各自的 require,每个树节点都去加载自己节点分支模块,符合递归调用的条件。

    递归调用需要一个执行条件,条件就是该模块是否需要依赖

    return __load(__getUrl(dep)).then(() => {
    const { deps, factory } = def.get(dep); // 取出 模块 检查 是否需要依赖
        if (deps.length === 0) return factory(null); // 当依赖长度部位0时
        return require(deps, factory) // 否则 加载 自身依赖
    })
    

    起源地下载网 » 今天要讲的几个模块化规范CMJ、AMD、CMD、ESM,以及简单实现AMD、CMD

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元