# 模块模式

# 同步模块

# 同步模块模式

模块化:将复杂的系统分解成高内聚、低耦合的模块,使系统开发变得可控、可维护、可拓展、提高模块的复用率。

同步模块模式:请求发出后,无论模块是否存在,都立即执行后续的逻辑。

# 模块管理器与创建方法

对于同步模块调度,首先要创建模块,如果没有模块那么谈什么调度呢。

我们首先定义一个模块管理器对象 F,然后为其创建一个模块定义方法 define。

F.define(String, Function)

// syncModuleManager.js
((global) => {

  const F = F || {};
  F.define = function (str, fn) {
    const parts = str.split(".") // "a.b.c" ==> [a, b, c]
    let old = parent = this;
    let i = len = 0;
    // 不让定义在 F 对象上
    if (parts[0] === "F") {
      parts = parts.slice(1)
    }
    // 保护内置方法不被重写
    if (parts[0] === "deine" || parts[0] === "module") {
      return false;
    }
    // 遍历模块 a, b, c ==>  a:{ b:{ c:{ } } } 
    for (len = parts.length; i < len; i++) {
      if (typeof parent[parts[i]] === 'undefined') {
        parent[parts[i]] == {}
      }
      old = parent
      parent = parent[parts[i]]
    }
    // 定义模块方法
    if (fn) {
      old[parts[--i]] = fn()
    }
    return this
  }

})(global)

# 创建模块

比如我们创建一个 DOM 模块,其中包括 dom() 获取元素,html() 可以获取或设置元素 innerHTML内容的方法等等等。

// syncModuleDOM.js
F.deinfe("dom",function (){
  // 返回一个构造器方法
  const $ = function (id){
    $.dom = document.getElementById(id)
    return $ // 方便链式调用
  }
  $.html = function (html){
    if(html){
      this.dom.innerHTML = html;
    }else {
      return this.dom.innerHTML
    }
  }
  return $;
})

F.define("dom.addClass",function (){
  // 返回方法
  return function (className){
    if(!~this.dom.className.indexOf(className)){
      this.dom.className += ' ' + className
    }
  }
})
<div id="test">test</div>
F.dom("test").html()
F.dom("test").html("hello")
F.dom("test").addClass("test")

# 调用模块方法

要使用模块我们需要创建一个使用模块的方法,或者说引入模块的方法

F.module = function () {
  const args = [].slice.call(arguments)
  // 获取回调方法
  const fn = args.pop()
  // 区分数组还是单个模块
  parts = args[0] && args[0] instanceof Array ? args[0] : args
  const modules = [];
  const modIDs = "";
  let parent;
  let i = 0;
  // 多个模块
  while (i < parts.length) {
    if (typeof parts[i] === "string") {
      // parent --> F
      parent = this;
      modIDs = parts[i].replace(/^F\./, "").split(".")
      for (let j = 0; j < modIDs.length; j++) {
        // 移动 parent 指针
        parent = parent[modIDs[j]] || false
      }
      // 找到模块实际对应的方法,将其加入列表
      modules.push(parent)
    } else {
      modules.push(parts[i])
    }
    i++
  }
  // 执行回调,传入模块列表
  fn.apply(null, modules)
}

# 学以致用

使用时

  • 单个模块 F.module(String, Function)
  • 多个模块 F.module(Array, Function)
F.module(["dom", document], function (dom, doc){
  dom("test").html("new add content !");
  doc.body.style.background = "black";
})

# 总结

模块化开发是分治的思想,实现对复杂系统的分解,使系统随着其功能的增加而变得可控、可拓展、可维护。

随着系统功能的增加,模块的数量也随之增加,模块的开发成本减少,但模块的接口却也随之增加了,接口的使用成本和开发维护成本就随之增加,所以合理的模块分割显得尤为重要。

# 异步模块

# 异步模块模式

异步模块模式:请求发出后,继续其他业务逻辑,直到模块加载完成执行后续的逻辑,实现模块开发中对模块加载完成后的引用。

异步模块模式会涉及到模块依赖,根据依赖模块加载文件,加载文件成功后执行引用模块时生命的回调函数等一些技术的实现。

# 闭包环境

首先要创建一个闭包,目的是封闭已创建的模块,防止外界对其直接访问,并在闭包中创建模块管理器 F ,并作为接口保存在全局作用域中。

// asyncModuleManager.js

;(function(){

  const moduleCache = {}
  // ...
  F.module = function (url,modDeps,modCallback){
    // ...
  }

})((function(){
  
  return window.F = {}

})())

# 创建和调度模块

在异步模块中,创建和调度模块时,需要遍历所有的依赖模块,并且这些依赖都加载完成才可执行回调函数。依赖模块如果依赖其他的模块,则同理以此类推。

这里我们创建和调度模块都使用F.module方法,使用这些模块时,可以认为当前是一个匿名模块。

// asyncModuleManager.js

(function () {
  const moduleCache = {};
  const setModule = function(){}
  const loadModule = function(){};
  // ...
  F.module = function (url, modDeps, modCallback) {
    const args = [].slice.call(arguments);
    let callback = args.pop()
    let deps = (args.length && args[args.length - 1] instanceof Array) ? args.pop() : [];
    let url = args.length ? args.pop() : null
    let params = []
    let depsCount = 0;
    let i = 0;
    let len;
    if (len = deps.length) {
      while (i < len) {
        (function (i) {
          depsCount++;
          // 异步加载依赖模块
          loadModule(deps[i], function (mod) {
            params[i] = mod;
            depsCount--;
            // 等待最后一个依赖加载完成
            if (depsCount === 0) {
              setModule(url, params, callback)
            }
          })
        })(i)
        i++
      }
    } else {
      // 无依赖模块,匿名模块
      setModule(url, [], callback)
    }
  };
})(
  (function () {
    return (window.F = {});
  })()
);

# 加载模块

现在我们要来实现异步模块的加载函数

const moduleCache = {};
// ...
const loadModule = function (moduleName, callback) {
  if (moduleCache[moduleName]) {
    // 已被加载过
    const _module = moduleCache[moduleName];
    if (_module.status === "loaded") {
      // 如果缓存模块加载完成
      setTimeout(callback(_module.exports), 0);
    } else {
      // 缓存回调函数
      _module.onload.push(callback)
    }
  } else {
    // 第一次加载
    moduleCache[moduleName] = {
      moduleName: moduleName,
      status: "loading",
      exports: null,
      onload: [callback]
    };
    loadScript(getUrl(moduleName))
  }
}
const getUrl = function(moduleName){};
const loadScript = function(src){}

还有两个配套方法的实现

const getUrl = function(moduleName){
  // lib/ajax => lib/ajax.js
  return String(moduleName).replace(/\.js$/g,"") + ".js";
};
const loadScript = function(src){
  const _script = document.createElement("script");
  _script.type = "text/javascript";
  _script.async = true;
  _script.src = src;
  document.getElementsByTagName("head")[0].appendChild(_script)
}

# 设置模块

最后来实现核心方法 setModule

表面上看,这个方法就是执行模块回调函数用的,但实质上这个方法做了三件事:

  • 对创建的模块来说,当其所依赖的模块加载完成时,使用该方法。
  • 对于被依赖的模块来说,当模块加载完成时,间接的使用该方法。
  • 对于一个匿名模块来说,执行过程中也会使用该方法
const setModule = function(moduleName, params,callback){
  if(moduleCache[moduleName]){
    // 如果模块被调用过
    const _module = moduleCache[moduleName];
    _module.status = "loaded";
    _module.exports = callback ? callback.apply(_module,params):null;
    while(fn = _module.onload.shift()){
      fn(_module.exports)
    }
  }else{
    // 模块不存在,则直接执行构造函数
    callback && callback.apply(null,params)
  }    
}

# 学以致用

  • 定义无依赖模块 F.module(String, Function)
  • 定义有依赖模块 F.module(String, Array, Function)
  • 使用一个或多个模块 F.module(Array, Function)

定义模块 lib/dom

// lib/dom.js
F.module("lib/dom", function () {
  return {
    g: function (id) {
      return document.getElementById(id);
    },
    html: function (id, html) {
      if (html) {
        this.g(id).innerHTML = html
      } else {
        return this.g(id).innerHTML
      }
    }
  }
})

定义模块 lib/event 此模块依赖 lib/dom

// lib/event.js
F.module("lib/event", ["lib/dom"], function (dom) {
  const events = {
    on: function (id, type, fn) {
      dom.g(id)["on" + type] = fn
    }
  }
  return events;
})

使用模块 lib/eventlib/dom 注意模块和参数顺序对应

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script src="./asyncModuleManager.js"></script>
  <title>asyncModule</title>
</head>

<body>
  <div id="demo">Hello</div>
  <script>
    F.module(["lib/event", "lib/dom"], function (events, dom) {
      events.on("demo", "click", function () {
        dom.html("demo", "success");
      })
    })
  </script>
</body>

</html>

页面加载完成时,<head> 标签内容如下

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script src="./asyncModuleManager.js"></script>
  <title>asyncModule</title>
  <script type="text/javascript" async="" src="lib/event.js"></script>
  <script type="text/javascript" async="" src="lib/dom.js"></script>
</head>

# 总结

模块化开发不仅解决了系统的复杂性问题,而且减少了多人开发中变量污染的问题。通过其强大的命名空间管理,使模块的结构更加合理。通过对模块的引用提高了代码的复用率。

异步模块模式在此基础上增加了模块依赖,使开发者不必担心某些方法未加载或未加载完全造成的无法使用问题。

异步加载部分功能也可将更多首屏不必要的功能剥离出去,减少首屏加载成本。

# Widget 模式

Web Widget: Web Widget 指的是一块可以在任意页面中执行的代码块。

Widget 模式是指借用 Web Widget 思想,将页面分解成部件,针对部件开发,最终组合成完整的页面。

# 视图模块化

在Widget模式中,开发一个组件通常对应一个文件,而不是某个功能或者某个视图。

在这个组件文件中,我们要做两件事,第一创建视图,第二添加相应的功能。

# 模板引擎

创建视图要借用到简单模模式的思想,用服务端请求来的数据格式化我们视图模板,实现对视图的创建。

我们要封装一个模板功能组件,让他可以对页面元素模板,script 模板,表单模板,字符串模板格式化,甚至可以编译并执行 JavaScript 语句。

# 实现原理

要实现的效果如下所示

将模板

<a 
href="#" 
class="data-lang {% if(is_selected) { %} selected {% } %}" 
value="{%=value%}" 
>{%=text%}</a>

和数据

{
  is_selected: true,
  value: "zh",
  text: "zh-text"
}

渲染输出结果

<a href="#" class="data-lang selected" value="zh">zh-text</a>

# 基本结构

  • 处理数据
  • 获取模板
  • 处理模板
  • 编译执行

定义

// lib/template.js
F.module("lib/template",function(){
  const _TplEngine = function(){};
  const _getTpl = function(){}
  const _dealTpl = function(){}
  const _compileTpl = function(){};
  return _TplEngine;
})

使用

F.module(["lib/template"],function(template){
  // do something
})

# 处理数据

/**
 * 处理数据与编译模板入口
 * @param {string} str 模板容器的id或模板字符串
 * @param {Object} data 需要渲染的数据
 * @returns
 */
const _TplEngine = function (str, data) {
  if (data instanceof Array) {
    let html = "";
    let i = 0;
    const len = data.length;
    for (; i < len; i++) {
      html += _getTpl(str)(data[i]);
    }
  } else {
    return _getTpl(str)(data);
  }
};

# 获取模板

/**
 * 获取模板
 * @param {string} str 模板容器的id或模板字符串
 */
const _getTpl = function (str) {
  const ele = document.getElementById(str);
  if (ele) {
    // id
    const html = /^(textarea|input)$/.test(ele.nodeName) ? ele.value : ele.innerHTML;
    return _compileTpl(html);
  } else {
    // template
    return _compileTpl(html);
  }
};

# 处理模板

处理模板的流程比较复杂,举例说明一下

举例

需要处理的模板

<a>{%=test%}</a>

处理后的形式

templateList.push('<a>',typeof(test) === 'undefined' ? '': test,'</a>')
  • 首先,将传入的内容转化为字符串 String(str)
  • 将 html 标签内常用的的 &lt;&gt; 分别转义为 <>
  • 将三类空白符号过滤掉 回车符\r,制表符\t,换行符\n
  • 将插值表达式转化成 ',typeof($1) === 'undefined' ? '' : $1,'
  • {%替换为');
  • %}替换成templateList.push('
/**
 * 处理模板
 * @param {*} str 模板字符串
 * @returns 
 */
const _dealTpl = function (str) {
  return String(str)
    .trim()
    .replace(/$lt;/g, "<")
    .replace(/$gt;/g, ">")
    .replace(/[\r\t\n]/g, "")
    .replace(/%}\s+{%/g, "")
    .replace(/{%=(.*?)%}/g, `',typeof($1)==='undefined' ? '' : $1,'`)
    .replace(/{%/g, `');\n`)
    .replace(/%}/g, `\n\ttemplateList.push('`)
    .replace(/push\('\s+/g, "push('")
    .replace(/>\s+'\);/g, ">\\n');");
};

# 编译执行

/**
 * 编译并执行模板字符串
 * @param {*} str 模板字符串
 * @returns 
 */
const _compileTpl = function (str) {
  const fnBody = `
var templateList = [];
var fn = (function (data) {
  var templateKey = "";
  for (key in data) {
    templateKey += ('var ' + key + ' = data[\"' + key +'\"]');
  }
  eval(templateKey);
  templateList.push('${_dealTpl(str)}');
  templateKey = null;
})(templateData);
fn = null;
return templateList.join('');`;
  const _compile =  new Function("templateData", fnBody)
  return _compile;
};
var templateList = [];
var fn = (function (data) {
  var templateKey = "";
  for (key in data) {
    templateKey += ('var ' + key + ' = data[\"' + key +'\"]');
  }
  eval(templateKey);
  templateList.push('${_dealTpl(str)}');
  templateKey = null;
})(templateData);
fn = null;
return templateList.join('');

# 使用

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script src="./asyncModuleManager.js"></script>
  <title>widgetMode</title>
</head>

<body>
  <div id="app"></div>

  <script type="text/template" id="demo_script">
    <div id="tag_cloud">
      {% for(var i = 0, len = tagCloud.length; i < len; i++) { %}
          {% const ctx = tagCloud[i]; %}
            <a href="#" class="tag_item {% if(ctx.is_selected){ %} selected {%  } %}" title="{%=ctx.title%}" >{%=ctx.text%}</a>
      {% } %}
    </div>
  </script>
  <script>
    const data = {
      tagCloud: [
        { is_selected: true, title: "这是一本设计模式书", text: "设计模式" },
        { is_selected: false, title: "这是一本HTML书", text: "HTML" },
        { is_selected: false, title: "这是一本CSS书", text: "CSS" },
        { is_selected: true, title: "这是一本JavaScript书", text: "JavaScript" }
      ]
    }
    const fetchData = () => data
    F.module(["lib/template", "lib/dom"], function (template, dom) {
      const data = fetchData()
      // 获取数据
      const htmlStr = template("demo_script", data);
      console.log(htmlStr)
      dom.html("app", htmlStr)
      // 其他逻辑
      // ...
    })
  </script>
</body>

</html>

渲染结果

编译后的templaeData函数如下所示:

ƒ anonymous(templateData) {
  var templateList = [];
  var fn = (function (data) {
    var templateKey = "";
    for (key in data) {
      templateKey += ('var ' + key + ' = data["' + key +'"]');
    }
    eval(templateKey);
    templateList.push('<div id="tag_cloud">\n');
  for(var i = 0, len = tagCloud.length; i < len; i++) {  const ctx = tagCloud[i]; 
    templateList.push('<a href="#" class="tag_item ');
  if(ctx.is_selected){ 
    templateList.push('selected ');
    } 
    templateList.push('" title="',typeof(ctx.title)==='undefined' ? '' : ctx.title,'" >',typeof(ctx.text)==='undefined' ? '' : ctx.text,'</a>\n');
  } 
    templateList.push('</div>');
    templateKey = null;
  })(templateData);
  fn = null;
  return templateList.join('');
}

替换数据后的渲染出的HTML字符串如下所示:

<div id="tag_cloud">
<a href="#" class="tag_item selected " title="这是一本设计模式书" >设计模式</a>
<a href="#" class="tag_item " title="这是一本HTML书" >HTML</a>
<a href="#" class="tag_item " title="这是一本CSS书" >CSS</a>
<a href="#" class="tag_item selected " title="这是一本JavaScript书" >JavaScript</a>
</div>

# 完整代码

DETAILS
// lib/template.js

F.module("lib/template", function () {
  /**
   * 处理数据与编译模板入口
   * @param {string} str 模板容器的id或模板字符串
   * @param {Object} data 需要渲染的数据
   * @returns
   */
  const _TplEngine = function (str, data) {
    if (data instanceof Array) {
      let html = "";
      let i = 0;
      const len = data.length;
      for (; i < len; i++) {
        html += _getTpl(str)(data[i]);
      }
    } else {
      return _getTpl(str)(data);
    }
  };
  /**
   * 获取模板
   * @param {string} str 模板容器的id或模板字符串
   */
  const _getTpl = function (str) {
    const ele = document.getElementById(str);
    if (ele) {
      // id
      const html = /^(textarea|input)$/.test(ele.nodeName) ? ele.value : ele.innerHTML;
      return _compileTpl(html);
    } else {
      // template
      return _compileTpl(html);
    }
  };
  /**
   * 处理模板
   * @param {*} str 模板字符串
   * @returns 
   */
  const _dealTpl = function (str) {
    return String(str)
      .trim()
      .replace(/$lt;/g, "<")
      .replace(/$gt;/g, ">")
      .replace(/[\r\t\n]/g, "")
      .replace(/%}\s+{%/g, "")
      .replace(/{%=(.*?)%}/g, `',typeof($1)==='undefined' ? '' : $1,'`)
      .replace(/{%/g, `');\n`)
      .replace(/%}/g, `\n\ttemplateList.push('`)
      .replace(/push\('\s+/g, "push('")
      .replace(/>\s+'\);/g, ">\\n');");
  };
  /**
   * 编译并执行模板字符串
   * @param {*} str 模板字符串
   * @returns 
   */
  const _compileTpl = function (str) {
    const fnBody = `
var templateList = [];
var fn = (function (data) {
  var templateKey = "";
  for (key in data) {
    templateKey += ('var ' + key + ' = data[\"' + key +'\"]');
  }
  eval(templateKey);
  templateList.push('${_dealTpl(str)}');
  templateKey = null;
})(templateData);
fn = null;
return templateList.join('');`;
    const _compile =  new Function("templateData", fnBody)
    return _compile;
  };
  return _TplEngine;
});