# 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;
});