As a front-end ER, if you can’t write a small plug-in, are embarrassed to say that they are mixed front-end world. You can’t rely on a library like jquery, or it’s not high enough. So how do you fake it to look better? Of course is to use JS pure native writing method. In the past, it has been said that if you master JS native, you can basically solve all the script interaction work of the front end. However, it also shows how important native JS is in the front end. All right. Without further ado. Let’s take a look at how to do a js plug-in.

Plug-in requirements

We write code, and not all business or logical code has to be pulled out and reused. First, we need to see if we need to abstract away some oft-repeated code and write it into a separate file for later use. Let’s see if our business logic works for the team. Plug-ins are not written on the fly, but are abstracted according to their own business logic. There are no one-size-fits-all plugins, only pair plugins, which are called plugins out of the box, or we just add some configuration parameters to get the results we need. If all these conditions are met, we will consider making a plug-in.

Conditions for plug-in encapsulation

A reusable plug-in needs to meet the following criteria:

  1. The scope of the plug-in itself is independent of the user’s current scope, that is, private variables within the plug-in cannot affect the user’s environment variables.
  2. Plug-ins need to have default setting parameters;
  3. In addition to the basic functions that have been realized, the plug-in needs to provide some API, through which users can modify the default parameters of the plug-in function, so as to achieve user-defined plug-in effect.
  4. Plugins support chained calls;
  5. The plug-in provides a listening entry and listens for the specified element to make the element and the plug-in response look like the plug-in.

For conditions on plug-in encapsulation, check out the article: A Guide to Writing Native JavaScript plug-ins and I want to show you how to implement my plug-in encapsulation step by step. So, I’m going to start with a simple method function.

The outer wrapping of the plug-in

Wrapped in functions

A plug-in is a set of functions encapsulated in a closure. I remember when I first started writing JS, what I did was write the logic I wanted as a function, and then pass in different arguments depending on what I wanted. For example, I want to add two numbers:

function add(n1,n2) {
    return n1 + n2;
}
/ / call
add(1.2)
// Output: 3
Copy the code

This is a simple implementation of what we want. If it’s just that simple logic, that’s fine, no need for bells and whistles. Js functions can solve most problems by themselves. However, in our practical work and application, the general requirements are much more complex. If the product comes to you and says, not only do I need to add two numbers, I need to subtract, multiply, divide, remainder, and so on. At this point, what do we do? Of course, you think, what’s so hard about that. I’m just going to write out all of these functions. And then put it all in a JS file. Just call it when you need it.

/ / add
function add(n1,n2) {
    return n1 + n2;
}
/ /
function sub(n1,n2) {
    return n1 - n2;
}
/ / by
function mul(n1,n2) {
    return n1 * n2;
}
/ / in addition to
function div(n1,n2) {
    return n1 / n2;
}
/ / for more
function sur(n1,n2) {
    return n1 % n2;
}
Copy the code

OK, now we have everything we need. And we’ve written all these functions into one JS. If a person is using it, it is clear that they have defined what they are writing, and what page I need it on, so they can just import the JS file and be done. However, if it’s a team of more than two people, or if you’re working with someone else, the other person doesn’t know if you wrote the Add method, and they define the same Add method. Then you have naming conflicts, commonly known as global contamination of variables

Wrapped with a global object

To solve this problem of global variable contamination. At this point, we can define a JS object to receive our utility functions.

var plugin = {
    add: function(n1,n2){... },/ / add
    sub: function(n1,n2){... },/ /
    mul: function(n1,n2){... },/ / by
    div: function(n1,n2){... },/ / in addition to
    sur: function(n1,n2){... }More than / /
}
/ / call
plugin.add(1.2)
Copy the code

The above approach, which specifies the name of the plug-in as plugin and makes team members abide by the naming rules, has solved the global pollution problem to some extent. As long as the naming rules are agreed in teamwork, it is ok to inform other students. Of course, if someone takes over your project and doesn’t know that the global variable has been defined, he defines it again and assigns a value, then he will overwrite your object. Of course, you might do something like this to resolve naming conflicts:

if(! plugin){Typeof plugin == 'undefined');
    var plugin = {
        // Write your function logic in this way}}Copy the code

Or you could write:

var plugin;
if(! plugin){ plugin = {// ...}}Copy the code

That way, there is no naming conflict.

You may wonder why you can declare the plugin variable here. In fact, the js interpretation execution will bring all declarations forward. If a variable is already declared, it does not matter if it is declared later in a function. So, even if I declared the var plugin elsewhere, I can declare it again here. For more information on declarations, see Nguyen Yi-feng’s how to determine if a Javascript object exists.

Basically, this is a plug-in. With global contamination resolved, method functions can be pulled out and placed in a separate file.

Use closure wrapping

The above example, though, can achieve the basic functions of the plug-in. Our plugin object, however, is defined globally. We know that calls to JS variables are much slower to look up in the global scope than in the private scope. Therefore, it is best to write the plug-in logic in a private scope. The best way to implement private scopes is to use closures. The purpose of a closure is to extend the life cycle of the internal variables of a function (plug-in) so that the plug-in function can be called repeatedly without affecting the user’s scope. Therefore, you need to write all the functionality of the plug-in in a function that executes immediately:

; (function(global,undefined) {
    var plugin = {
        add: function(n1,n2){...}
        ...
    }
    // Finally expose the plug-in object to the global object
    'plugin' inglobal && global.plugin = plugin; }) (window);
Copy the code

To explain the above code segment parameter passing problem:

  1. Adding a semicolon before defining plug-ins can solve the problem of js merging errors that may occur;
  2. Undefined is not supported in older browsers and will cause errors if used directly. Js frameworks need to consider compatibility, so add a parameter undefined, even if someone put the external parameter undefinedundefinedDefined, undefined is still not affected;
  3. Passing the window object as a parameter prevents the function from having to look it up externally during execution.

In fact, we thought it would be inappropriate to just pass the Window object in. We are not sure that our plugin will be used in the browser, but it may be used in some non-browsing applications. So we can also do this, we don’t pass arguments, we just take the current global this object and use it as the top-level object.

; (function(global,undefined) {
    "use strict" // Use js for strict schema checking to make syntax more formal
    var _global;
    var plugin = {
        add: function(n1,n2){...}
        ...
    }
    // Finally expose the plug-in object to the global object
    _global = (function(){ return this| | -0.eval) ('this'); } ()); ! ('plugin' in_global) && (_global.plugin = plugin); } ());Copy the code

This way, we don’t need to pass in any parameters and solve the plug-in’s dependency on the environment. This way our plug-in can run on any host environment.

(0,eval) (‘this’) : (0,eval) (‘this’) : (0,eval) (‘this’) (0,eval)(‘this’) paraphrase or look at this passage (0,eval)(‘this’)

There are two ways to write immediate self-executing functions:

/ / write one
(function(){}) ()/ / write two
(function(){} ())Copy the code

There is no difference between the two notations above. All right. Personally, I recommend using the second notation. It’s more like a unit.

A bit of additional knowledge: in javascript, the () parentheses are used to convert a code structure into an expression. When the enclosing () becomes an expression, it is immediately executed. There are many ways to convert a piece of code into an expression, such as:

void function(){... } ();/ / or
!function foo(){... } ();/ / or
+function foot(){... } ();Copy the code

Of course, we don’t recommend it. And misuse can create some ambiguity.

At this point, our plug-in infrastructure is complete.

Use modular specification packaging

Although the packaging above is basically ok. But what if multiple people are working on a large plug-in? With multiple people working together, there are bound to be multiple files, each responsible for a small function, so how do you get all the code together? This is a nasty question. To achieve collaborative plug-in development, the following conditions must exist:

  • The dependencies of each function must be clear, and must be merged or loaded in strict order of dependencies
  • Each child function is a closure, and exposes the common interface to the shared domain, that is, a common object exposed by the main function

There are many ways to achieve this. The stupidest way is to load js sequentially

<script type="text/javascript" src="part1.js"></script>
<script type="text/javascript" src="part2.js"></script>
<script type="text/javascript" src="part3.js"></script>.<script type="text/javascript" src="main.js"></script>
Copy the code

This is not recommended, however, because it goes against the encapsulation we are looking for in plug-ins. There are a bunch of popular module loaders on the front end, such as Require, SeaJS, or they can be loaded in a node-like manner, but on the browser side, we have to use packers for module loading, such as Browserify. However, I do not talk about how to carry out modular packaging or loading problems, if you have questions, you can go to the link above to read the document to learn. In order for our plug-in to be modular and for our plug-in to be a module, we need to implement the modularity mechanism for our plug-in as well. We actually just have to determine if there is a loader, if there is a loader, we use the loader, if there is no loader. We’re going to use top-level domain objects.

if (typeof module! = ="undefined" && module.exports) {
    module.exports = plugin;
} else if (typeof define === "function" && define.amd) {
    define(function(){returnplugin; }); }else {
    _globals.plugin = plugin;
}
Copy the code

Our complete plugin should look something like this:

// plugin.js; (function(undefined) {
    "use strict"
    var _global;
    var plugin = {
        add: function(n1,n2){ return n1 + n2; },/ / add
        sub: function(n1,n2){ return n1 - n2; },/ /
        mul: function(n1,n2){ return n1 * n2; },/ / by
        div: function(n1,n2){ return n1 / n2; },/ / in addition to
        sur: function(n1,n2){ return n1 % n2; } More than / /
    }
    // Finally expose the plug-in object to the global object
    _global = (function(){ return this| | -0.eval) ('this'); } ());if (typeof module! = ="undefined" && module.exports) {
        module.exports = plugin;
    } else if (typeof define === "function" && define.amd) {
        define(function(){returnplugin; }); }else {
        !('plugin' in _global) && (_global.plugin = plugin);
    }
}());
Copy the code

After we introduce the plug-in, we can use the Plugin object directly.

with(plugin){
    console.log(add(2.1)) / / 3
    console.log(sub(2.1)) / / 1
    console.log(mul(2.1)) / / 2
    console.log(div(2.1)) / / 2
    console.log(sur(2.1)) / / 0
}
Copy the code

The plug-in API

The default parameters of the plug-in

We know that functions can set default arguments, but whether we pass arguments or not, we should return a value that tells the user what we did, such as:

function add(param){
    varargs = !! param ?Array.prototype.slice.call(arguments) : [];
    return args.reduce(function(pre,cur){
        return pre + cur;
    }, 0);
}

console.log(add()) If no parameter is passed and the result is 0, the default is set to an empty array
console.log(add(1.2.3.4.5)) // Pass the parameter, and output 15
Copy the code

As a robust JS plug-in, we should add some basic state parameters to the plug-in we need. Assuming the addition, subtraction, multiplication, and division requirements above, how do we implement the plug-in’s default parameters? The idea is the same.

// plugin.js; (function(undefined) {
    "use strict"
    var _global;

    function result(args,fn){
        var argsArr = Array.prototype.slice.call(args);
        if(argsArr.length > 0) {return argsArr.reduce(fn);
        } else {
            return 0; }}var plugin = {
        add: function(){
            return result(arguments.function(pre,cur){
                return pre + cur;
            });
        },/ / add
        sub: function(){
            return result(arguments.function(pre,cur){
                return pre - cur;
            });
        },/ /
        mul: function(){
            return result(arguments.function(pre,cur){
                return pre * cur;
            });
        },/ / by
        div: function(){
            return result(arguments.function(pre,cur){
                return pre / cur;
            });
        },/ / in addition to
        sur: function(){
            return result(arguments.function(pre,cur){
                return pre % cur;
            });
        } More than / /
    }

    // Finally expose the plug-in object to the global object
    _global = (function(){ return this| | -0.eval) ('this'); } ());if (typeof module! = ="undefined" && module.exports) {
        module.exports = plugin;
    } else if (typeof define === "function" && define.amd) {
        define(function(){returnplugin; }); }else {
        !('plugin' in _global) && (_global.plugin = plugin);
    }
}());

// The output is:
with(plugin){
    console.log(add()); / / 0
    console.log(sub()); / / 0
    console.log(mul()); / / 0
    console.log(div()); / / 0
    console.log(sur()); / / 0

    console.log(add(2.1)); / / 3
    console.log(sub(2.1)); / / 1
    console.log(mul(2.1)); / / 2
    console.log(div(2.1)); / / 2
    console.log(sur(2.1)); / / 0
}
Copy the code

In fact, plug-ins have their own default arguments. Take our most common form validation plug-in, validate.js

(function(window, document, undefined) {
    // The default parameters of the plug-in
    var defaults = {
        messages: {
            required: 'The %s field is required.'.matches: 'The %s field does not match the %s field.'."default": 'The %s field is still set to default, please change.'.valid_email: 'The %s field must contain a valid email address.'.valid_emails: 'The %s field must contain all valid email addresses.'.min_length: 'The %s field must be at least %s characters in length.'.max_length: 'The %s field must not exceed %s characters in length.'.exact_length: 'The %s field must be exactly %s characters in length.'.greater_than: 'The %s field must contain a number greater than %s.'.less_than: 'The %s field must contain a number less than %s.'.alpha: 'The %s field must only contain alphabetical characters.'.alpha_numeric: 'The %s field must only contain alpha-numeric characters.'.alpha_dash: 'The %s field must only contain alpha-numeric characters, underscores, and dashes.'.numeric: 'The %s field must contain only numbers.'.integer: 'The %s field must contain an integer.'.decimal: 'The %s field must contain a decimal number.'.is_natural: 'The %s field must contain only positive numbers.'.is_natural_no_zero: 'The %s field must contain a number greater than zero.'.valid_ip: 'The %s field must contain a valid IP.'.valid_base64: 'The %s field must contain a base64 string.'.valid_credit_card: 'The %s field must contain a valid credit card number.'.is_file_type: 'The %s field must contain only %s files.'.valid_url: 'The %s field must contain a valid URL.'.greater_than_date: 'The %s field must contain a more recent date than %s.'.less_than_date: 'The %s field must contain an older date than %s.'.greater_than_or_equal_date: 'The %s field must contain a date that\'s at least as recent as %s.'.less_than_or_equal_date: 'The %s field must contain a date that\'s %s or older.'
        },
        callback: function(errors) {}};var ruleRegex = / ^ (. +?) \ [(+) \] $/,
        numericRegex = / ^ [0-9] + $/,
        integerRegex = / ^ \ -? [0-9] + $/,
        decimalRegex = / ^ \ -? [0-9] * \.? [0-9] + $/,
        emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](? : [a - zA - Z0-9 -], 21 {0} [a zA - Z0-9])? (? :\.[a-zA-Z0-9](? : [a - zA - Z0-9 -], 21 {0} [a zA - Z0-9])?) * $/,
        alphaRegex = /^[a-z]+$/i,
        alphaNumericRegex = /^[a-z0-9]+$/i,
        alphaDashRegex = /^[a-z0-9_\-]+$/i,
        naturalRegex = /^[0-9]+$/i,
        naturalNoZeroRegex = /^[1-9][0-9]*$/i,
        ipRegex = / ^ ((25 [0 to 5] | 2 [0 to 4] [0-9] [0-9] {2} | 1 | [0-9] {1, 2}) \.) {3} (25 [0 to 5] | 2 [0 to 4] [0-9] [0-9] {2} | 1 | [0-9] {1, 2}) $/ I,
        base64Regex = /[^a-zA-Z0-9\/\+=]/i,
        numericDashRegex = /^[\d\-\s]+$/,
        urlRegex = / ^ (| HTTPS (HTTP) : / / / / (\ w + : {0, 1} \ w * @)? (\S+)|)(:[0-9]+)? (\/|\/([\w#!:.?+=&%@!\-\/]))? $/,
        dateRegex = / \ d {4} \ d {1, 2} - \ d {1, 2} /; .// omit the following code}) (window.document);
/* * Export as a CommonJS module */
if (typeof module! = ='undefined' && module.exports) {
    module.exports = FormValidator;
}
Copy the code

Of course, since the parameters are default, that means we can modify them to suit our needs. The point of plug-ins is to be reusable. As with the form validation plugin, we can change our default parameters when we new an object:

var validator = new FormValidator('example_form'[{name: 'req'.display: 'required'.rules: 'required'
}, {
    name: 'alphanumeric'.rules: 'alpha_numeric'
}, {
    name: 'password'.rules: 'required'
}, {
    name: 'password_confirm'.display: 'password confirmation'.rules: 'required|matches[password]'
}, {
    name: 'email'.rules: 'valid_email'
}, {
    name: 'minlength'.display: 'min length'.rules: 'min_length[8]'
}, {
    names: ['fname'.'lname'].rules: 'required|alpha'}].function(errors) {
    if (errors.length > 0) {
        // Show the errors}});Copy the code

Plug-in hooks

We know that when we design plug-ins, parameters or logic, we need to act like functions and let the user provide their own parameters to fulfill the user’s needs. Our plug-in needs to provide an entry point to modify the default parameters. Changing the default parameters, as mentioned above, is actually an API provided by the plug-in. Make our plugin more flexible. If you do not understand the API, you can baidu API we usually use JS plug-in, there will be a variety of ways to achieve. The simplest implementation logic is a method, or a JS object, or a constructor, and so on. However, the API of our plug-in is actually all the methods and properties that our plug-in exposes. ** In our demand, addition, subtraction, multiplication and division plug-in, our API is the following methods:

. var plugin = {add: function(n1,n2){ return n1 + n2; },
    sub: function(n1,n2){ return n1 - n2; },
    mul: function(n1,n2){ return n1 * n2; },
    div: function(n1,n2){ return n1 / n2; },
    sur: function(n1,n2){ returnn1 % n2; }}...Copy the code

As you can see, plubin exposes the following apis:

  • add
  • sub
  • mul
  • div
  • sur

In plug-in apis, we often refer to methods or attributes that can be easily modified or changed as hooks, while methods are called Hook functions. This is a vivid way of saying it, as if we had a string with lots of hooks on it, and we could hang things on it as needed. In fact, we know that plugins can either hang things on a string or take them off. A plug-in, then, is essentially a chain of images. However, all of our hooks are attached to objects, which is not ideal for implementing chains.

Chained invocation of the plug-in (using the current object)

Plugins are not always chain-callable. Sometimes, we just use the hook to perform a calculation and return the result. But sometimes we use hooks that do not need to return results. We only use it to implement our business logic, for the sake of code simplicity and convenience, we often call the plug-in in a chain manner. The most common jquery chained calls are as follows:

$(<id>).show().css('color'.'red').width(100).height(100)...Copy the code

So, how do we apply chain calls to our plug-in? Given our example above, if we wanted to call the plugin object as a chain, we could change its business structure to:

. var plugin = {add: function(n1,n2){ return this; },
    sub: function(n1,n2){ return this; },
    mul: function(n1,n2){ return this; },
    div: function(n1,n2){ return this; },
    sur: function(n1,n2){ return this; }}...Copy the code

If we simply return the plugin’s current object, this, we can also reference the plugin’s other hook methods in the following methods. And then you can use the chain when you call it.

plugin.add().sub().mul().div().sur()  // This call obviously has no practical meaning
Copy the code

Obviously there’s no point in doing that. Each of our hook functions here is just used to evaluate and get the return value. The point of the chain call itself is to process the business logic.

Chained invocation of plug-ins (utilizing prototype chains)

In JavaScript, everything is an object, and all objects are inherited from archetypes. When creating an object (whether a normal object or a function object), JS has a built-in attribute called __proto__, which points to the prototype object of the function object that created it. For prototype questions, if you’re interested, you can read this: In the above requirement, we need to write plugin as a constructor in the way that we can change the plugin object to a prototype. We change the name of the plugin to Calculate to avoid collisions with the API in the Window object when plugin is capitalized.

. function Calculate(){} Calculate.prototype.add =function(){return this; } Calculate.prototype.sub =function(){return this; } Calculate.prototype.mul =function(){return this; } Calculate.prototype.div =function(){return this; } Calculate.prototype.sur =function(){return this;}
...
Copy the code

Of course, assuming that our plug-in evaluates the initialization parameters and outputs only the results, we can change it slightly:

// plugin.js
// plugin.js; (function(undefined) {
    "use strict"
    var _global;

    function result(args,type){
        var argsArr = Array.prototype.slice.call(args);
        if(argsArr.length == 0) return 0;
        switch(type) {
            case 1: return argsArr.reduce(function(p,c){returnp + c; });case 2: return argsArr.reduce(function(p,c){returnp - c; });case 3: return argsArr.reduce(function(p,c){returnp * c; });case 4: return argsArr.reduce(function(p,c){returnp / c; });case 5: return argsArr.reduce(function(p,c){returnp % c; });default: return 0; }}function Calculate(){}
    Calculate.prototype.add = function(){console.log(result(arguments.1));return this; } Calculate.prototype.sub =function(){console.log(result(arguments.2));return this; } Calculate.prototype.mul =function(){console.log(result(arguments.3));return this; } Calculate.prototype.div =function(){console.log(result(arguments.4));return this; } Calculate.prototype.sur =function(){console.log(result(arguments.5));return this; }// Finally expose the plug-in object to the global object
    _global = (function(){ return this| | -0.eval) ('this'); } ());if (typeof module! = ="undefined" && module.exports) {
        module.exports = Calculate;
    } else if (typeof define === "function" && define.amd) {
        define(function(){returnCalculate; }); }else {
        !('Calculate' in _global) && (_global.Calculate = Calculate);
    }
}());
Copy the code

When we call the plug-in we have written, the output is as follows:

var plugin = new Calculate();
plugin
    .add(2.1)
    .sub(2.1)
    .mul(2.1)
    .div(2.1)
    .sur(2.1);
/ / the result:
/ / 3
/ / 1
/ / 2
/ / 2
/ / 0
Copy the code

The above example may not have much practical significance. However, in web design, our plug-ins are basically serving the UI level, using JS scripts to achieve some interactive effects. At this point, we write a UI plug-in that can also be invoked using a chain.

Writing UI components

In general, if a JS is only dealing with a logic, we call it a plug-in, but if it is related to DOM and CSS and has some interaction, it is usually called a component. There is no clear distinction, of course, but it is a common term. Using the prototype chain, you can encapsulate some UI-level business code in a widget, and use JS to achieve component interaction. There is a need for:

  1. Implement a shell layer, this shell layer can display some text suggestive information;
  2. There must be a close button in the upper right corner of the shell layer, click it and the shell layer will disappear.
  3. There must be a “OK” button at the bottom of the shell, and then a “cancel” button can be configured according to requirements;
  4. Clicking the “OK” button triggers an event;
  5. Click the Close/Cancel button to trigger an event.

According to the requirements, we write the DOM structure first:


        
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
    <link rel="stylesheet" type="text/css" href="index.css">
</head>
<body>
    <div class="mydialog">
        <span class="close">x</span>
        <div class="mydialog-cont">
            <div class="cont">hello world!</div>
        </div>
        <div class="footer">
            <span class="btn">determine</span>
            <span class="btn">cancel</span>
        </div>
    </div>
    <script src="index.js"></script>
</body>
</html>
Copy the code

Write the CSS structure:

* { padding: 0; margin: 0; }
.mydialog { background: #fff; box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 0.3); overflow: hidden; width: 300px; height: 180px; border: 1px solid #dcdcdc; position: absolute; top: 0; right: 0; bottom: 0; left: 0; margin: auto; }
.close { position: absolute; right: 5px; top: 5px; width: 16px; height: 16px; line-height: 16px; text-align: center; font-size: 18px; cursor: pointer; }
.mydialog-cont { padding: 0 0 50px; display: table; width: 100%; height: 100%; }
.mydialog-cont .cont { display: table-cell; text-align: center; vertical-align: middle; width: 100%; height: 100%; }
.footer { display: table; table-layout: fixed; width: 100%; position: absolute; bottom: 0; left: 0; border-top: 1px solid #dcdcdc; }
.footer .btn { display: table-cell; width: 50%; height: 50px; line-height: 50px; text-align: center; cursor: pointer; }
.footer .btn:last-child { display: table-cell; width: 50%; height: 50px; line-height: 50px; text-align: center; cursor: pointer; border-left: 1px solid #dcdcdc; }
Copy the code

Next, let’s start writing our interactive plug-in. Let’s assume that the component’s pop-up layer is an object. This object contains our interaction, style, structure, and rendering process. So we define a constructor:

function MyDialog(){} // MyDialog is our component object
Copy the code

The MyDialog object is like a string, and we just have to keep attaching hooks to the string to make it a component. Our component can then be expressed as:

function MyDialog(){}
MyDialog.prototype = {
    constructor: this._initial: function(){},
    _parseTpl: function(){},
    _parseToDom: function(){},
    show: function(){},
    hide: function(){},
    css: function(){},... }Copy the code

You can then write all the functionality of the plug-in. However, the business logic in the middle needs to be studied step by step. Anyway, we’ll eventually be able to use our plugin by instantiating a MyDialog object. In the process of writing, we need to do some utility functions:

1. Object merge function

// Object merge
function extend(o,n,override) {
    for(var key in n){
        if(n.hasOwnProperty(key) && (!o.hasOwnProperty(key) || override)){
            o[key]=n[key];
        }
    }
    return o;
}
Copy the code

2. Customize the template engine to interpret functions

// Customize the template engine
function templateEngine(html, data) {
    var re = [^ % > / < % (] +)? %>/g,
        reExp = / (^ ()? (if|for|else|switch|case|break|{|}))(.*)? /g,
        code = 'var r=[]; \n',
        cursor = 0;
    var match;
    var add = function(line, js) {
        js ? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + '); \n') : (code += line ! =' ' ? 'r.push("' + line.replace(/"/g.'\ \ "') + '"); \n' : ' ');
        return add;
    }
    while (match = re.exec(html)) {
        add(html.slice(cursor, match.index))(match[1].true);
        cursor = match.index + match[0].length;
    }
    add(html.substr(cursor, html.length - cursor));
    code += 'return r.join(""); ';
    return new Function(code.replace(/[\r\t\n]/g.' ')).apply(data);
}
Copy the code

3. Find the class to obtain the DOM function

// Find the DOM by class
if(! ('getElementsByClass' in HTMLElement)){
    HTMLElement.prototype.getElementsByClass = function(n, tar){
        varel = [], _el = (!! tar ? tar :this).getElementsByTagName(The '*');
        for (var i=0; i<_el.length; i++ ) {
            if(!!!!! _el[i].className && (typeof _el[i].className == 'string') && _el[i].className.indexOf(n) > - 1) { el[el.length] = _el[i]; }}return el;
    };
    ((typeofHTMLDocument ! = ='undefined')? HTMLDocument : Document).prototype.getElementsByClass = HTMLElement.prototype.getElementsByClass; }Copy the code

Combined with tool functions, and then to achieve the specific logic structure of each hook function:

// plugin.js; (function(undefined) {
    "use strict"
    var_global; .// Plugin constructor - returns array structure
    function MyDialog(opt){
        this._initial(opt);
    }
    MyDialog.prototype = {
        constructor: this._initial: function(opt) {
            // Default parameters
            var def = {
                ok: true.ok_txt: 'sure'.cancel: false.cancel_txt: 'cancel'.confirm: function(){},
                close: function(){},
                content: ' '.tmpId: null
            };
            this.def = extend(def,opt,true);
            this.tpl = this._parseTpl(this.def.tmpId);
            this.dom = this._parseToDom(this.tpl)[0];
            this.hasDom = false;
        },
        _parseTpl: function(tmpId) { // Convert the template to a string
            var data = this.def;
            var tplStr = document.getElementById(tmpId).innerHTML.trim();
            return templateEngine(tplStr,data);
        },
        _parseToDom: function(str) { // Convert the string to dom
            var div = document.createElement('div');
            if(typeof str == 'string') {
                div.innerHTML = str;
            }
            return div.childNodes;
        },
        show: function(callback){
            var _this = this;
            if(this.hasDom) return ;
            document.body.appendChild(this.dom);
            this.hasDom = true;
            document.getElementsByClass('close'.this.dom)[0].onclick = function(){
                _this.hide();
            };
            document.getElementsByClass('btn-ok'.this.dom)[0].onclick = function(){
                _this.hide();
            };
            if(this.def.cancel){
                document.getElementsByClass('btn-cancel'.this.dom)[0].onclick = function(){
                    _this.hide();
                };
            }
            callback && callback();
            return this;
        },
        hide: function(callback){
            document.body.removeChild(this.dom);
            this.hasDom = false;
            callback && callback();
            return this;
        },
        modifyTpl: function(template){
            if(!!!!! template) {if(typeof template == 'string') {this.tpl = template;
                } else if(typeof template == 'function') {this.tpl = template();
                } else {
                    return this; }}// this.tpl = this._parseTpl(this.def.tmpId);
            this.dom = this._parseToDom(this.tpl)[0];
            return this;
        },
        css: function(styleObj){
            for(var prop in styleObj){
                var attr = prop.replace(/[A-Z]/g.function(word){
                    return The '-' + word.toLowerCase();
                });
                this.dom.style[attr] = styleObj[prop];
            }
            return this;
        },
        width: function(val){
            this.dom.style.width = val + 'px';
            return this;
        },
        height: function(val){
            this.dom.style.height = val + 'px';
            return this;
        }
    }

    _global = (function(){ return this| | -0.eval) ('this'); } ());if (typeof module! = ="undefined" && module.exports) {
        module.exports = MyDialog;
    } else if (typeof define === "function" && define.amd) {
        define(function(){returnMyDialog; }); }else {
        !('MyDialog' in _global) && (_global.MyDialog = MyDialog);
    }
}());
Copy the code

At this point, our plug-in has met the basic requirements. We can call it from the page like this:

<script type="text/template" id="dialogTpl">
    <div class="mydialog">
        <span class="close">x</span>
        <div class="mydialog-cont">
            <div class="cont"><% this.content% ></div>
        </div>
        <div class="footer">
            <% if(this.cancel) {% >
            <span class="btn btn-ok"><% this.ok_txt% ></span>
            <span class="btn btn-cancel"><% this.cancel_txt% ></span>
            <% } else{ %>
            <span class="btn btn-ok" style="width: 100%"><% this.ok_txt% ></span>
            <%} % >
        </div>
    </div>
</script>
<script src="index.js"></script>
<script>
    var mydialog = new MyDialog({
        tmpId: 'dialogTpl',
        cancel: true,
        content: 'hello world! '
    });
    mydialog.show();
</script>
Copy the code

Plug-in listening

Popup plugin we have implemented the basic show and hide function. But when we pop it out, we might do something after we pop it out, but we actually need to do something controlled. As with event binding, only when the user clicks a button will the user respond to a specific event. So, our plug-in, like the event binding, should call the corresponding event response only when something is done. This JS design pattern, known as the subscribe/publish pattern, is also known as the observer pattern. We also need to use the observer mode in the plug-in. For example, before opening the popover, we need to update the content of the popover, perform some judgment logic, etc., and then display the popover after completion of execution. After closing the popover, we need to perform some logic after closing, processing business, etc. At this point, we need to do some “event” binding callback methods for the plug-in as we normally do for events. Our jquery event response to the DOM looks like this:

$(<dom>).on("click".function(){})
Copy the code

We designed the corresponding plug-in response as above:

mydialog.on('show'.function(){})
Copy the code

We need to implement an event mechanism to listen for plug-in events. For more information on custom event listening, see this blog post: JS custom events, DOM/ pseudo-DOM custom events. I won’t go into the issue of custom events here. Finally, we implemented the plug-in code as follows:

// plugin.js; (function(undefined) {
    "use strict"
    var _global;

    // Utility functions
    // Object merge
    function extend(o,n,override) {
        for(var key in n){
            if(n.hasOwnProperty(key) && (!o.hasOwnProperty(key) || override)){
                o[key]=n[key];
            }
        }
        return o;
    }
    // Customize the template engine
    function templateEngine(html, data) {
        var re = [^ % > / < % (] +)? %>/g,
            reExp = / (^ ()? (if|for|else|switch|case|break|{|}))(.*)? /g,
            code = 'var r=[]; \n',
            cursor = 0;
        var match;
        var add = function(line, js) {
            js ? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + '); \n') : (code += line ! =' ' ? 'r.push("' + line.replace(/"/g.'\ \ "') + '"); \n' : ' ');
            return add;
        }
        while (match = re.exec(html)) {
            add(html.slice(cursor, match.index))(match[1].true);
            cursor = match.index + match[0].length;
        }
        add(html.substr(cursor, html.length - cursor));
        code += 'return r.join(""); ';
        return new Function(code.replace(/[\r\t\n]/g.' ')).apply(data);
    }
    // Find the DOM by class
    if(! ('getElementsByClass' in HTMLElement)){
        HTMLElement.prototype.getElementsByClass = function(n){
            var el = [],
                _el = this.getElementsByTagName(The '*');
            for (var i=0; i<_el.length; i++ ) {
                if(!!!!! _el[i].className && (typeof _el[i].className == 'string') && _el[i].className.indexOf(n) > - 1) { el[el.length] = _el[i]; }}return el;
        };
        ((typeofHTMLDocument ! = ='undefined')? HTMLDocument : Document).prototype.getElementsByClass = HTMLElement.prototype.getElementsByClass; }// Plugin constructor - returns array structure
    function MyDialog(opt){
        this._initial(opt);
    }
    MyDialog.prototype = {
        constructor: this._initial: function(opt) {
            // Default parameters
            var def = {
                ok: true.ok_txt: 'sure'.cancel: false.cancel_txt: 'cancel'.confirm: function(){},
                close: function(){},
                content: ' '.tmpId: null
            };
            this.def = extend(def,opt,true); // Configure parameters
            this.tpl = this._parseTpl(this.def.tmpId); // Template string
            this.dom = this._parseToDom(this.tpl)[0]; // Nodes stored in the instance
            this.hasDom = false; // Check whether the node of the DIALOG exists in the DOM tree
            this.listeners = []; // Custom events to listen for user interactions with plug-ins
            this.handlers = {};
        },
        _parseTpl: function(tmpId) { // Convert the template to a string
            var data = this.def;
            var tplStr = document.getElementById(tmpId).innerHTML.trim();
            return templateEngine(tplStr,data);
        },
        _parseToDom: function(str) { // Convert the string to dom
            var div = document.createElement('div');
            if(typeof str == 'string') {
                div.innerHTML = str;
            }
            return div.childNodes;
        },
        show: function(callback){
            var _this = this;
            if(this.hasDom) return ;
            if(this.listeners.indexOf('show') > - 1) {
                if(!this.emit({type:'show'.target: this.dom})) return ;
            }
            document.body.appendChild(this.dom);
            this.hasDom = true;
            this.dom.getElementsByClass('close') [0].onclick = function(){
                _this.hide();
                if(_this.listeners.indexOf('close') > - 1) {
                    _this.emit({type:'close'.target: _this.dom}) } !! _this.def.close && _this.def.close.call(this,_this.dom);
            };
            this.dom.getElementsByClass('btn-ok') [0].onclick = function(){
                _this.hide();
                if(_this.listeners.indexOf('confirm') > - 1) {
                    _this.emit({type:'confirm'.target: _this.dom}) } !! _this.def.confirm && _this.def.confirm.call(this,_this.dom);
            };
            if(this.def.cancel){
                this.dom.getElementsByClass('btn-cancel') [0].onclick = function(){
                    _this.hide();
                    if(_this.listeners.indexOf('cancel') > - 1) {
                        _this.emit({type:'cancel'.target: _this.dom})
                    }
                };
            }
            callback && callback();
            if(this.listeners.indexOf('shown') > - 1) {
                this.emit({type:'shown'.target: this.dom})
            }
            return this;
        },
        hide: function(callback){
            if(this.listeners.indexOf('hide') > - 1) {
                if(!this.emit({type:'hide'.target: this.dom})) return ;
            }
            document.body.removeChild(this.dom);
            this.hasDom = false;
            callback && callback();
            if(this.listeners.indexOf('hidden') > - 1) {
                this.emit({type:'hidden'.target: this.dom})
            }
            return this;
        },
        modifyTpl: function(template){
            if(!!!!! template) {if(typeof template == 'string') {this.tpl = template;
                } else if(typeof template == 'function') {this.tpl = template();
                } else {
                    return this; }}this.dom = this._parseToDom(this.tpl)[0];
            return this;
        },
        css: function(styleObj){
            for(var prop in styleObj){
                var attr = prop.replace(/[A-Z]/g.function(word){
                    return The '-' + word.toLowerCase();
                });
                this.dom.style[attr] = styleObj[prop];
            }
            return this;
        },
        width: function(val){
            this.dom.style.width = val + 'px';
            return this;
        },
        height: function(val){
            this.dom.style.height = val + 'px';
            return this;
        },
        on: function(type, handler){
            // type: show, shown, hide, hidden, close, confirm
            if(typeof this.handlers[type] === 'undefined') {
                this.handlers[type] = [];
            }
            this.listeners.push(type);
            this.handlers[type].push(handler);
            return this;
        },
        off: function(type, handler){
            if(this.handlers[type] instanceof Array) {
                var handlers = this.handlers[type];
                for(var i = 0, len = handlers.length; i < len; i++) {
                    if(handlers[i] === handler) {
                        break; }}this.listeners.splice(i, 1);
                handlers.splice(i, 1);
                return this; }},emit: function(event){
            if(! event.target) { event.target =this;
            }
            if(this.handlers[event.type] instanceof Array) {
                var handlers = this.handlers[event.type];
                for(var i = 0, len = handlers.length; i < len; i++) {
                    handlers[i](event);
                    return true; }}return false; }}// Finally expose the plug-in object to the global object
    _global = (function(){ return this| | -0.eval) ('this'); } ());if (typeof module! = ="undefined" && module.exports) {
        module.exports = MyDialog;
    } else if (typeof define === "function" && define.amd) {
        define(function(){returnMyDialog; }); }else {
        !('MyDialog' in _global) && (_global.MyDialog = MyDialog);
    }
}());
Copy the code

You can then invoke the plugin’s event binding directly.

var mydialog = new MyDialog({
    tmpId: 'dialogTpl'.cancel: true.content: 'hello world! '
});
mydialog.on('confirm'.function(ev){
    console.log('you click confirm! ');
    // Write your logical code after confirmation...
});
document.getElementById('test').onclick = function(){
    mydialog.show();
}
Copy the code

Give this example of the demo, there is a need for concrete implementation of the students can go to check.

Plug-in release

Once we have written our plug-in, we can actually distribute our plug-in to open source organizations to share it with more people (the code must be privately owned and at the discretion of the owner). Once the plug-in is packaged, it can be released to open source organizations for others to download and use. The well-known NPM community is a great platform for releasing plug-ins. Write the description file of the initialization package:

$ npm init
Copy the code

Register the package repository account

$NPM adduser Username: < account > Password: < Password > Email:(this IS public) < Email > LoggedinAs > < account on https://registry.npmjs.org/.Copy the code

Upload the package

$ npm publish
Copy the code

The installation package

$ npm install mydialog
Copy the code

At this point, our plug-in can be directly used by more people.

conclusion

Write so much, more verbose, I make a summary here: on how to write a good JS native plug-in, usually in the use of other people’s plug-ins at the same time, check the API document, understand the plug-in call way, and then look at the plug-in source code design. Basically, we can be sure that most plug-ins are designed as prototypes. From the example above, and I use a lot of js native knowledge points, the function of naming conflicts, closures, scope, the custom tool function extension object of hook function, as well as the object initialization, prototype chain inheritance, the constructor definition and design patterns, and the custom of events, js knowledge such as design pattern of the observer pattern. These contents still need beginners to understand more to carry out some high-level plug-in development.





Author: Nicknamed Lu Shiqi


Link: https://www.jianshu.com/p/e65c246beac1


Source: Jane Book


Brief book copyright belongs to the author, any form of reprint please contact the author to obtain authorization and indicate the source.