[Node.js] 如何使用 Virtual DOM 實作 MVVM

Virtual DOM 這字眼最近在前端框架上相當的熱門,這技術在最近這麼多的 MVC 框架中到底能夠提供什麼改良和優化,本篇主要分享 Virtual DOM 的介紹和如何實作。

What's Virtual DOM

要解釋 Virtual DOM 的由來,首先就要先從 React.js 講起,React.js 是由 Facebook 與 Instagram 團隊共同推出的技術,用來製作 UI。

而目前 UI 的框架已經這麼多了,為什麼還要自己重做一個新技術?這個問題,涉及到 MVC 框架技術的演進與改良。而 React 是 MVC 框架中的 V,也就是 View 的部份,並且採用一個稱之為 virtual DOM 的技術。

Virtual DOM

Photo by Jollen Chen

從上圖來說我只想更新 DOM 裡面的某一塊,那要如何讓瀏覽器不要掃描整個 tree 後才知道要更新哪個區塊,最好的辦法就是遮住瀏覽器的眼睛只讓他看到要更新的區塊,但目前瀏覽器的技術還無法實作到,因此在瀏覽器外實作 shadow DOM 的技術,將它稱為 Virtual DOM

如何實作

由於我本身習慣用 Node.js 做 Full stack 開發,框架是使用 partjs 這包 open source project,所以在前端實作方式會用到 Backbone.js 的 model + Virtual DOM 部分來實作 View

  • Step 1. Install Browserify

讓前端也可以 require node module

$ npm install -g browserify
  • Step 2. Install Virtual-dom

利用 npm 安裝 Virtual DOM module

$ npm i -g virtual-dom
$ npm i -g html-to-vdom
  • Step 3. Virtual DOM 程式實作

利用 Virtual DOM 方式來製作 View,並將此 view 獨立成一個 Class,可在需要的時候將 view 實例化

var h = require('virtual-dom/h');  
var diff = require('virtual-dom/diff');  
var patch = require('virtual-dom/patch');

var createElement = require('virtual-dom/create-element');  
var VNode = require('virtual-dom/vnode/vnode');  
var VText = require('virtual-dom/vnode/vtext');

var convertHTML = require('html-to-vdom')({  
    VNode: VNode,
    VText: VText
});

var ElementHotCircle = function(options) { //Constructor,用於設置初始化屬性  
    // data
    this.el = options.el; //用 this 對此 class 作變數初始化
    this.model = options.model;
    this.collectionClass = options.collection;
    this.templateFunc = options.template;

    // properties
    this.tree = {};
    this.element = {};
    this.collection = new this.collectionClass();
    // number of elements
    this.count = 0;

    // constructor
    this.model.bind('change', this.composite, this); //當你的model有change,就會去呼叫composite
};

// class name + prototype + function name
ElementHotCircle.prototype.composite = function(id) {  
    // Get new view and build the subtree
    var model = this.collection.at(id);
    var tree = model.get('vtree');
    var element = model.get('element');

    // model state change
    // create the new tree
    model.set('title', 'click');
    var innerHtml = this.templateFunc( model.attributes )
                        .replace(/(?:(?:^|\n)\s+|\s+(?:$|\n))/g,'');
    var newTree = convertHTML(innerHtml);

    // composition
    var patches = diff(tree, newTree);
    element = patch(element, patches);//去compsition this model to view

    // finalize
    model.set('vtree', newTree);
    model.set('element', element);
};

ElementHotCircle.prototype.addWidget = function(dataSet) {  
    var model = new this.model();

    for(var i = 0; i < dataSet.length; i++)
    {
        // Data persistence
        model.set('ID', dataSet.at(i).get("ID"));
        model.set('CircleName', dataSet.at(i).get("CircleName"));
        model.set('CircleDesc', dataSet.at(i).get("CircleDesc"));

        // 1. Get view and build the subtree (virtual DOM)
        //    - remove invalid characters
        var innerHtml = this.templateFunc( model.attributes )
                            .replace(/(?:(?:^|\n)\s+|\s+(?:$|\n))/g,'');    
        var tree = convertHTML(innerHtml);

        // 2. Create an element
        var element = createElement(tree);

        // 3: composition boundary
        this.el.append(element); 

        // setup
        model.set('vtree', tree);
        model.set('element', element);
        this.collection.add(model, {at: this.count});
        this.count++;
    }    
};

module.exports = ElementHotCircle;  
  • Step 4. Backbone model 以及事件處理

此步驟的幾個概念:

  • 事件的綁定使用backbone
  • 把 Virtual DOM 封裝成class => Javascript 的 Constructor pattern, Prototype pattern
  • 實例化 Virtual DOM class => MVVM (View model = componet)
var $ = require('jquery');  
var _ = require('underscore');  
var Backbone = require('backbone');  
Backbone.$ = $;


/*
    Setup
*/
var app = app || {};


/*
    Model
*/
app.Circle = Backbone.Model.extend({  
    defaults: {
        success: false,
        errors: [],
        errfor: {},
        ID: "",
        CircleName: "",
        CircleDesc: "",
    }
});

/*
    Collection 用於作初始化資料
*/
app.CircleCollection = Backbone.Collection.extend({  
    model: app.Circle
});

var lstCircle = new app.CircleCollection();  
lstCircle.add(new app.Circle({ ID: 1,  CircleName: "", CircleDesc: "" }));


/*
    Element Class (ViewModel)
 */
var ElementHotCircles = require('./elementHotCircles.js'); //把elementHotCircle import進來

/*

*/

app.viewCircle = Backbone.View.extend({  
    el: '#hot_circle',
    template: _.template( $('#tmpl-hot-circle').html() ),
    events: {
        'click a.btn.btn-primary': 'handleClick'
    },
    initialize: function() { //給這參數一個class 並在這class內給與需要的屬性,可在new的時候透過建構子給予初值

        this.widget = new ElementHotCircles({ //將elementHotCircle class new出來
          el: this.$el,
          model: app.Circle,
          collection: app.CircleCollection,
          template: this.template
        });
        this.render();
    },
    handleClick: function(ev) {
        ev.preventDefault();

        var elm = this.$el.find(ev.currentTarget);
        var id = elm.data('id');

    },
    // Facade pattern
    render: function() {
        console.log(lstCircle.length);
        this.widget.addWidget(lstCircle); //利用此方法來做貼合動作
    }
});


/*
    Bootup
*/

$(document).ready(function() {

    app.viewCircle = new app.viewCircle();
});
  • Step 5. 利用 Browserify 將上述寫的 js compiler
browserify min.’s -o bundle.js  

Ben Shiue

Having being a full stack engineer. His interests in Node.js, ARM mbed, IoT solutions. Contact us : [email protected]

ALL RIGHTS RESERVED. COPYRIGHT © 2016. Designed and Coded by Makee.io