建立适当的模型生成 xml 文档

客户发货时,需要我们为每一个订单都提供一份 invoice 数据,格式是 xml, 这份 invoice 数据有点复杂,为了方便描述,将其 简化,下面是简化后的 invoice xml 文档,

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<Invoice>
  <InvoiceNumber>IDE2014000000001</InvoiceNumber>
  <InvoiceTypeCode>SATIS</InvoiceTypeCode>
  <ScenarioType>TICARIFATURA</ScenarioType>
  <DocumentCurrencyCode>TRL</DocumentCurrencyCode>
  <IssueDate>2013-08-10</IssueDate>
  <PayableAmount>216.16</PayableAmount>
  <TaxExclusiveAmount>200</TaxExclusiveAmount>
  <TaxInclusiveAmount>235.16</TaxInclusiveAmount>
  <DiscountRate></DiscountRate>
  <DiscountTotal></DiscountTotal>
  <OrderNumber></OrderNumber>
  <OrderDate></OrderDate>
</Invoice>

在我们的数据库中并没有 invoices 这么一个表,客户需要的 invoice 数据来自于我们现有的多个模型,比如 Order, Product, Variant 等, 很自然的,我们可以为 invoices 建立一个模型: Invoice,


class Invoice

   attr_reader :number
   attr_reader :type_code
   attr_reader :scenario_type
   attr_reader :document_currency_code
   attr_reader :issue_date
   attr_reader :payable_amount
   attr_reader :tax_exclusive_amount
   attr_reader :tax_inclusive_amount
   attr_reader :discount_rate
   attr_reader :discount_total
   attr_reader :order_number
   attr_reader :order_date

end

这样 invoices xml 文档里的数据就都可以由 Invoice 模型来提供了, 比如,

  number => InvoiceNumber
  type_code => InvoiceTypeCode

我们可以为 number => InvoiceNumber 这类的映射建立一套 DSL,

  class Invoice
    xml_attr :number, :type_code, prefix: 'invoice', camelize: true
  end

这套 DSL 的作用就是把模型 Invoice 的属性转换为 xml 文档内的 columns。

我把这个 DSL 的实现写成了一个 module,

module DataSource
  module Xmlable

    def self.included base
      class << base
        attr_reader :xml_col_attr_pairs
      end
      base.extend ClassMethods
      base.send :include, InstanceMethods
    end

    module ClassMethods
      
      def xml_attr *attrs, opts
        @xml_col_attr_pairs ||= []
        cols = attrs.dup
        
        if opts[:prefix].present?
          cols = attrs.map {|attr| [opts[:prefix], attr].join('_') }
        end

        if opts[:camelize] == true
          cols = cols.map(&:to_s).map &:camelize
        end

        cols.each_with_index {|col, index|
          @xml_col_attr_pairs << [col, attrs[index]]
        }

      end

    end


    module InstanceMethods
      
      def col_attr_pairs_to_xml
        build_xml do |xml|
          xml_col_attr_pairs.each do |pair|
            col_value = send pair[1]
            xml.send pair[0], col_value
          end
        end
      end

      def xml_col_attr_pairs
        self.class.xml_col_attr_pairs
      end

      def build_xml
        xml = ::Builder::XmlMarkup.new
        yield xml
        xml.target!
      end

    end
    
  end
  
end

Invoice 的完整版本是,

  class Invoice

    include DataSource::Xmlable

    attr_reader :number
    attr_reader :type_code
    attr_reader :scenario_type
    attr_reader :document_currency_code
    attr_reader :issue_date
    attr_reader :payable_amount
    attr_reader :tax_exclusive_amount
    attr_reader :tax_inclusive_amount
    attr_reader :discount_rate
    attr_reader :discount_total
    attr_reader :order_number
    attr_reader :order_date

    xml_attr :number, :type_code, prefix: 'invoice', camelize: true
    xml_attr :scenario_type, :document_currency_code, :issue_date, camelize: true
    xml_attr :payable_amount, :tax_exclusive_amount, camelize: true
    xml_attr :discount_rate, :discount_total, :order_number, :order_date, camelize: true 

    def initialize attrs
	  attrs.each {|attr, value|
	    if respond_to? attr
		  instance_variable_set "@#{attr}", value
		end
	  }
    end

    def to_xml
      build_xml do |xml|
        xml.instruct! :xml, version: '1.0', encoding: 'UTF-8', standalone: 'yes'
        xml << xml_without_instruct
      end
    end

    def xml_without_instruct
      build_xml do |xml|
        xml.Invoice do
          xml << col_attr_pairs_to_xml
        end
      end
    end

  end

调试:

  invoice = Invoice.new(number: 'A000119', type_code: 'ISE')
  invoice.to_xml

输出:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Invoice>
  <InvoiceNumber>A000119</InvoiceNumber>
  <InvoiceTypeCode>ISE</InvoiceTypeCode>
  <ScenarioType></ScenarioType>
  <DocumentCurrencyCode></DocumentCurrencyCode>
  <IssueDate></IssueDate>
  <PayableAmount></PayableAmount>
  <TaxExclusiveAmount></TaxExclusiveAmount>
  <DiscountRate></DiscountRate>
  <DiscountTotal></DiscountTotal>
  <OrderNumber></OrderNumber>
  <OrderDate></OrderDate>
</Invoice>

JavaScript 编程实践 Site.View

以前把很多的 javascript 代码写在了 html 页面里了,这样造成的一个问题是时间一久,大家都找不到自己写的 javascript 代码在哪了或者 找起来非常费时费力, 还有一个问题就是你无法把写在 html 页面里的 javascript 代码压缩合并,所以我决定尽量把所有的 javascript 代码 都写在单独的 js 文件中。为此我实现了一个叫 Site.View 的类, 当然在 javascript 中, Site.View 其实是一个普通的 function。

Site.View 能帮我做三件事情:

  1. 可以将所有 javascript 代码放到一个文件中,而不会产生命名冲突等;
  2. 只有当页面出现相应的 DOM 元素时才会执行对应的代码;
  3. 提供适当的扩展;

Site.View 的实现:


var Site = Site || {};

Site.View = function(){

    var template_mapper = {};
    
    var View = function(template_name, args){

	var findContainer = function(name){
	    var container = document.querySelector('[view-template="' + name + '"' + ']');
	    return container;
	};

	var registerEventHandler = function(target, type, callback) {
	    if(target.addEventListener){
		target.addEventListener(type, callback);
	    } else {
		target.attachEvent('on' + type, callback);
	    }
	};

	this.template_name = template_name;
	this.container = findContainer(template_name);

	this.param = function(object){
	    var encodedString = '';
	    for (var prop in object) {
		if (object.hasOwnProperty(prop)) {
		    if (encodedString.length > 0) {
			encodedString += '&';
		    }
		    encodedString += encodeURI(prop + '=' + object[prop]);
		}
	    }
	    return encodedString;
	};

	this.ajax = function(ele){

	    var xhr = new XMLHttpRequest();
	    var ext_obj = {};

	    ext_obj.loadRemoteHtmlFragment = function(callback){
		var url = ele.getAttribute('data-uri');
		xhr.open('GET', url);
		xhr.setRequestHeader('Content-Type', 'text/html');
		xhr.onload = function(){
		    if(xhr.status === 200){
			ele.innerHTML = xhr.responseText;
			if(callback){
			    callback();
			}
		    }
		};
		xhr.send();
	    }

	    return ext_obj;
	};

	this.data = (function(){
	    var lastId = 0;
	    var store = {};

	    return function(ele){
		return {
		    set: function(info) {
			var id;
			if (ele.myCustomDataTag === undefined) {
			    id = lastId++;
			    ele.myCustomDataTag = id;
			}
			store[id] = info;
		    },

		    get: function() {
			return store[ele.myCustomDataTag];
		    }
		}
	    };
	}());

	this._ = function(ele){

	    var ext_obj = {};
	    
	    ext_obj.on = function(type, callback){
		if(ele[0]){
		    for(var i=0; i < ele.length; i++){
			registerEventHandler(ele[i], type, callback);
		    }
		} else {
		    registerEventHandler(ele, type, callback);
		}
	    };

	    ext_obj.removeClass = function(class_name){
		var reg_str = '\(\^\|\\s\)' + class_name + '\(\\s\|\$\)';
		var reg = new RegExp(reg_str, 'g');
		ele.className = ele.className.replace(reg, '');
	    };

	    
	    return ext_obj;
	};

	this.parseCssTextToObject = function(css_text){
	    var pairs = css_text.split(';');
	    var obj = {};
	    for(var i = 0; i < pairs.length; i++){
		if(pairs[i]){
		    var kv = pairs[i].split(':');
		    var key, value;
		    
		    if(kv[0]){
			key = kv[0].replace(/(^\s+)|(\s+$)/, '');
		    }
		    if(kv[1]){
			value = kv[1].replace(/(^\s+)|(\s+$)/, '');
		    }

		    if(key){
			obj[key] = value
		    }
		    
		}
	    }

	    return obj;
	};

	this.t = function(key){
	    return Csite.i18n.t(key);
	};

	var template = new template_mapper[template_name](this, args);

	return template;
    };

    View.loadTemplate = function(template_name, context){
	template_mapper[template_name] = context;
    };

    View.render = function(template_name, args){
	  document.addEventListener('DOMContentLoaded', function(){
	    var container = document.querySelector('[view-template="' + template_name + '"' + ']');
	    if(container){
		  var template = new View(template_name, args);
		  if(template.render){
		    template.render();
		  }
	    }
	  })
	}

    return View;
    
}();

那么怎么使用 Site.View? 假设我们有下面的 html 页面, 里面定义了一个 view-template="home.index.alert", 我们需要在 home.index.alert 加载完后执行 alert('ok'),

<html>
  <head>
    <script src='site_view.js'></script>
  </head>
  <body>
    <div view-template="home.index.alert">
	</div>
  </body>
</html>
Site.View.loadTemplate('home.index.alert', function(view){
  alert('ok');
})

Site.View.render('home.index.alert');

Site.View 使用 view-template 去查找相关的 DOM, 如果我们在页面上看到了 view-template="home.index.alert", 那么很容易通过 “home.index.alert” 找到与其相关的 javascript 代码。