Component System Construction Based on zepto

Keywords: Javascript Attribute JQuery

Preface

With the increasing complexity of front-end development, component development comes into being. How to develop a relatively simple active page is the main content of this paper.

Summary

Let's look at how to build a component system based on zepto. First, we need to solve the first problem, how to reference a component. We can refer to a custom component by setting an attribute data-component:

<div data-component="my-component"></div>

So how do we pass data to components? We can also pass data to components by setting properties, such as passing in an id value:

<div data-component="my-component" data-id="1"></div>

So how to communicate between components, we can use the observer mode to achieve.

Write a component

Let's first see how we can write a component.

//a.js
defineComponent('a', function (component) {
    var el = '<p class="a">input-editor</p>';
    var id = component.getProp('id');//Get the parameter id
    $(this).append(el);//View rendering
    component.setStyle('.a{color:green}');//define styles
    $(this).find('p').on('click', function () {
        component.emit('test', id, '2');//Trigger test
    });
});

Let's first look at how this component is defined. First, we call defineComponent (no matter where the function is defined) to define a component A. The latter function is the group logic of component A. This function passes in a component (no matter where it comes from, see what it can do first). In the previous part, we talked about how to transfer data to the component. In the component, we use the method of transferring data to the component. By component.getProp('id'), the style is defined by component.setStyle('.a{color:green}'), the communication before the component is triggered by component.emit() and registered by component.on() in other components. It seems that we have basically solved some of the previous problems about components. So how can we achieve this?

Principle of Component Implementation

Let's first look at how we should implement the component above. From the definition of a component above, there are two key points. One is how defineComponent is implemented, and the other is what component is.

Let's first look at how defineComponent is implemented. Obviously, defineComponent must be defined as global (otherwise A.js will not be available, and defineComponent must be defined before loading a.js). Let's look at the code for defineComponent.

//component.js
  var component = new Component();
  window.defineComponent = function (name, fn) {
        component.components[name] = {
            init: function () {
                //Set current Component as the current component
                currentComponent = this;
                fn.call(this, component);
                component.init(this);
            }
        };
    }

Here we can see that a class Component is defined. Component is an instance of it. Definition Component registers a component in component.components. The key here is Component class. Let's see how Component is defined.

//component.js
  /**
     * Component class
     * @constructor
     */
    function Component() {
        this.components = {};//All components
        this.events = {};//Registered events
        this.loadStyle = {};
        this.init('body');//Initialization
    }

    var currentComponent = null;//Current components
    /**
     * Class initialization function
     * @param container The scope of initialization, by default body
     */
    Component.prototype.init = function (container) {
        var self = this;
        container = container || 'body';
        $(container).find('[data-component]').each(function () {
            self.initComponent(this);
        });

    };
    /**
     *  Initialize a single component
     * @param context Current component
     */
    Component.prototype.initComponent = function (context) {

        var self = this;
        var componentName = $(context).attr('data-component');
        if (this.components[componentName]) {
            this.components[componentName].init.call(context);
        } else {
            _loadScript('http://' + document.domain + ':5000/dist/components/' + componentName + '.js', function () {
                self.components[componentName].init.call(context);
                //Set style, the same component is set only once
                if (!self.loadStyle[componentName] && self.components[componentName].style) {
                    $('head').append('<style>' + self.components[componentName].style + '</style>');
                    self.loadStyle[componentName] = true;
                }
            });
        }

    };
    /**
     * Set style
     * @param style style
     */
    Component.prototype.setStyle = function (style) {
        //Get the name of the current component, which is the current component
        var currentComponentName = $(currentComponent).attr('data-component');
        var component = this.components[currentComponentName];
        if (component && !component.style) {
            component.style = style;
        }
    };
    /**
     * Getting component parameters
     * @param prop Parameter name
     * @returns {*|jQuery}
     */
    Component.prototype.getProp = function (prop) {
        var currentComponentNme = $(currentComponent).attr('data-component');
        if ($(currentComponent).attr('data-' + prop)) {
            return $(currentComponent).attr('data-' + prop)
        } else {
            //Attribute does not exist time error
            throw Error('the attribute data-' + prop + ' of ' + currentComponentNme + ' is undefined or empty')
        }

    };
    /**
     * Registration event
     * @param name Event name
     * @param fn Event function
     */
    Component.prototype.on = function (name, fn) {
        this.events[name] = this.events[name] ? this.events[name] : [];
        this.events[name].push(fn);
    };
    /**
     * Trigger event
     */
    Component.prototype.emit = function () {
        var args = [].slice.apply(arguments);
        var eventName = args[0];
        var params = args.slice(1);
        if(this.events[eventName]){
            this.events[eventName].map(function (fn) {
                fn.apply(null, params);
            });
        }else{
            //There is no time error in the event.
            throw Error('the event ' + eventName + ' is undefined')
        }

    };
    /**
     * Dynamic Loading Group Price
     * @param url Component path
     * @param callback callback
     * @private
     */
    function _loadScript(url, callback) {
        var script = document.createElement("script");
        script.type = "text/javascript";
        if (typeof(callback) != "undefined") {
            if (script.readyState) {
                script.onreadystatechange = function () {
                    if (script.readyState == "loaded" || script.readyState == "complete") {
                        script.onreadystatechange = null;
                        callback();
                        $(script).remove();
                    }
                };
            } else {
                script.onload = function () {
                    callback();
                    $(script).remove();
                };
            }
        }
        script.src = url;
        $('body').append(script);
    }

Let's first get a general idea of the process.


The general process is the flow chart above. All our components are registered in component.components and events are registered in component.events.

Let's look back at the init method in component components

//component.js
  var component = new Component();
  window.defineComponent = function (name, fn) {
        component.components[name] = {
            init: function () {
                //Set current Component as the current component
                currentComponent = this;
                fn.call(this, component);
                component.init(this);
            }
        };
    }

First, assign this to current Component. Where will this be used? Both getProp and setStyle are used

//component.js
        /**
     * Set style
     * @param style style
     */
    Component.prototype.setStyle = function (style) {
        console.log(currentComponent);
        //Get the name of the current component, which is the current component
        var currentComponentName = $(currentComponent).attr('data-component');
        var component = this.components[currentComponentName];
        if (component && !component.style) {
            component.style = style;
        }
    };
    /**
     * Getting component parameters
     * @param prop Parameter name
     * @returns {*|jQuery}
     */
    Component.prototype.getProp = function (prop) {
        return $(currentComponent).attr('data-' + prop)
    };

At this point, you may wonder what this is. We can first see where the init method of the component was invoked.

//component.js
        /**
     *  Initialize a single component
     * @param componentName Component name
     * @param context Current component
     */
    Component.prototype.initComponent = function (componentName, context) {

        var self = this;
        if (this.components[componentName]) {
            this.components[componentName].init.call(context);
        } else {
            _loadScript('http://' + document.domain + ':5000/components/' + componentName + '.js', function () {
                self.components[componentName].init.call(context);
                //Set style, the same component is set only once
                if (!self.loadStyle[componentName] && self.components[componentName].style) {
                    $('head').append('<style>' + self.components[componentName].style + '</style>');
                    self.loadStyle[componentName] = true;
                }
            });
        }

    };

It's the init method that is initialized by a single component, where call changes the init's this so that this=context, so what's the context?

//component.js
       /**
     * Class initialization function
     * @param container The scope of initialization, by default body
     */
    Component.prototype.init = function (container) {
        var self = this;
        container = container || 'body';
        $(container).find('[data-component]').each(function () {
            var componentName = $(this).attr('data-component');
            console.log(this);
            self.initComponent(componentName, this);
        });

    };

context is actually every component traversed, so let's go back and see how we define a component.

//b.js
defineComponent('b', function (component) {
    var el = '<p class="text-editor">text-editor</p></div><div data-component="a" data-id="1"></div>';
    $(this).append(el);
    component.on('test', function (a, b) {
        console.log(a + b);
    });
    var style = '.text-editor{color:red}';
    component.setStyle(style)
});

We know that this is the component itself, which is the following

<div data-component="b"></div>

This component registers a test event through component.on the front we know that the test event is triggered by component A. So far, we have completed the development of the whole component system framework. Here is one by one to add components. The whole code is as follows:

//component.js
(function () {
    /**
     * Component class
     * @constructor
     */
    function Component() {
        this.components = {};//All components
        this.events = {};//Registered events
        this.loadStyle = {};
        this.init('body');//Initialization
    }

    var currentComponent = null;//Current components
    /**
     * Class initialization function
     * @param container The scope of initialization, by default body
     */
    Component.prototype.init = function (container) {
        var self = this;
        container = container || 'body';
        $(container).find('[data-component]').each(function () {
            self.initComponent(this);
        });

    };
    /**
     *  Initialize a single component
     * @param context Current component
     */
    Component.prototype.initComponent = function (context) {

        var self = this;
        var componentName = $(context).attr('data-component');
        if (this.components[componentName]) {
            this.components[componentName].init.call(context);
        } else {
            _loadScript('http://' + document.domain + ':5000/dist/components/' + componentName + '.js', function () {
                self.components[componentName].init.call(context);
                //Set style, the same component is set only once
                if (!self.loadStyle[componentName] && self.components[componentName].style) {
                    $('head').append('<style>' + self.components[componentName].style + '</style>');
                    self.loadStyle[componentName] = true;
                }
            });
        }

    };
    /**
     * Set style
     * @param style style
     */
    Component.prototype.setStyle = function (style) {
        //Get the name of the current component, which is the current component
        var currentComponentName = $(currentComponent).attr('data-component');
        var component = this.components[currentComponentName];
        if (component && !component.style) {
            component.style = style;
        }
    };
    /**
     * Getting component parameters
     * @param prop Parameter name
     * @returns {*|jQuery}
     */
    Component.prototype.getProp = function (prop) {
        var currentComponentNme = $(currentComponent).attr('data-component');
        if ($(currentComponent).attr('data-' + prop)) {
            return $(currentComponent).attr('data-' + prop)
        } else {
            //Attribute does not exist time error
            throw Error('the attribute data-' + prop + ' of ' + currentComponentNme + ' is undefined or empty')
        }

    };
    /**
     * Registration event
     * @param name Event name
     * @param fn Event function
     */
    Component.prototype.on = function (name, fn) {
        this.events[name] = this.events[name] ? this.events[name] : [];
        this.events[name].push(fn);
    };
    /**
     * Trigger event
     */
    Component.prototype.emit = function () {
        var args = [].slice.apply(arguments);
        var eventName = args[0];
        var params = args.slice(1);
        if(this.events[eventName]){
            this.events[eventName].map(function (fn) {
                fn.apply(null, params);
            });
        }else{
            //There is no time error in the event.
            throw Error('the event ' + eventName + ' is undefined')
        }

    };
    /**
     * Dynamic Loading Group Price
     * @param url Component path
     * @param callback callback
     * @private
     */
    function _loadScript(url, callback) {
        var script = document.createElement("script");
        script.type = "text/javascript";
        if (typeof(callback) != "undefined") {
            if (script.readyState) {
                script.onreadystatechange = function () {
                    if (script.readyState == "loaded" || script.readyState == "complete") {
                        script.onreadystatechange = null;
                        callback();
                        $(script).remove();
                    }
                };
            } else {
                script.onload = function () {
                    callback();
                    $(script).remove();
                };
            }
        }
        script.src = url;
        $('body').append(script);
    }

    var component = new Component();

    window.defineComponent = function (name, fn) {
        component.components[name] = {
            init: function () {
                //Set current Component as the current component
                currentComponent = this;
                fn.call(this, component);
                component.init(this);
            }
        };
    }

})();

Engineering

The component system built above has a disadvantage, that is, HTML and style defined by us are strings. For some large components, HTML and style are very long, so debugging will be very difficult. Therefore, we need to engineer the component system. The ultimate goal is that html, js and css can be developed separately. There are many existing engineering tools. You can use gulp or node to write a tool by yourself. Here's how to use node to realize the engineering of component system.

Let's look at the directory structure first.


First, we need to get the path to the pre-compile component.

//get-path.js
var glob = require('glob');
exports.getEntries = function (globPath) {
    var entries = {};
    /**
     * Read the src directory and clip the path
     */
    glob.sync(globPath).forEach(function (entry) {
        var tmp = entry.split('/');
        tmp.shift();
        tmp.pop();
        var pathname = tmp.join('/'); // Get the first two elements

        entries[pathname] = entry;

    });

    return entries;
};

Then read index.js,index.html,index.css respectively according to the path.

//read-file.js
var readline = require('readline');
var fs = require('fs');

exports.readFile = function (file, fn) {
    console.log(file);
    var fRead = fs.createReadStream(file);
    var objReadline = readline.createInterface({
        input: fRead
    });
    function trim(str) {
        return str.replace(/(^\s*)|(\s*$)|(\/\/(.*))|(\/\*(.*)\*\/)/g, "");
    }
    var fileStr = '';
    objReadline.on('line', function (line) {
        fileStr += trim(line);
    });
    objReadline.on('close', function () {
        fn(fileStr)
    });
};


//get-component.js
var fs = require('fs');
var os = require('os');

var getPaths = require('./get-path.js');
var routesPath = getPaths.getEntries('./src/components/**/index.js');

var readFile = require('./read-file');

for (var i in routesPath) {
    (function (i) {
        var outFile = i.replace('src', 'dist');
        readFile.readFile(i + '/index.js', function (fileStr) {
            var js = fileStr;
            readFile.readFile(i + '/index.html', function (fileStr) {
                js = js.replace('<html>', fileStr);
                readFile.readFile(i + '/index.css', function (fileStr) {
                    js = js.replace('<style>', fileStr);
                    var writeRoutes = fs.createWriteStream(outFile + '.js');
                    writeRoutes.write(js);
                });
            });

        });
    })(i)
}

To convert index.html and index.css into strings and insert them into index.js, let's look at index.js

// a/index.js
defineComponent('a', function (component) {
    var el = '<html>';
    var id = component.getProp('id');//Get the parameter id
    $(this).append(el);//View rendering
    var style = '<style>';
    component.setStyle(style);//define styles
    $(this).find('p').on('click', function () {
        component.emit('test', id, '2');//Trigger test
    })
});

Replace < HTML >, < style > with the strings previously converted by index.html and index.css, and finally monitor the files under the componets folder

//component-watch.js
var exec = require('child_process').exec;
var chokidar = require('chokidar');

console.log('Start listening on components...');

chokidar.watch('./src/components/**/**').on('change', function (path) {
    console.log(dateFormat(new Date(), 'yyyy-M-d h:m:s') + ':' + path + 'Changed...');

    exec('node get-component.js', function (err, out, code) {
        console.log(dateFormat(new Date(), 'yyyy-M-d h:m:s') + ':' + 'Compile completed...');
    });

});

//Time Formatting
function dateFormat(date, fmt) {
    var o = {
        "M+": date.getMonth() + 1, //Month
        "d+": date.getDate(), //day
        "h+": date.getHours(), //hour
        "m+": date.getMinutes(), //branch
        "s+": date.getSeconds(), //second
        "q+": Math.floor((date.getMonth() + 3) / 3), //quarter
        "S": date.getMilliseconds() //Millisecond
    };
    if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
    for (var k in o)
        if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
    return fmt;
}

At this point, the engineering of the component system is completed.
Specific code in Here
If you are interested, you can pay attention to me. Blog

Posted by Chevy on Fri, 31 May 2019 17:39:03 -0700