Как определить качественный нативный JS-плагин

JavaScript

Как фронтендер, если вы не можете написать небольшой плагин, вам стыдно говорить, что вы находитесь в мире фронтенда. Написание не может полагаться на библиотеки инструментов, такие как jquery, иначе это не достаточно высококлассно. Итак, как вы можете одеться, чтобы выглядеть выше? Конечно, это использование чисто нативного написания js. Ранее уже говорилось, что если вы освоите нативный js, то сможете в принципе решить всю работу по взаимодействию со скриптами на фронтенде, но это, как правило, немного преувеличено. Тем не менее, это также показывает, насколько важен нативный js во внешнем интерфейсе. Отлично. Не много глупостей. Давайте посмотрим, как сделать свой собственный плагин js.

Требования к плагину

Когда мы пишем код, не весь бизнес-код или логический код нужно извлекать и использовать повторно. Во-первых, мы должны посмотреть, нужно ли нам абстрагировать часть часто повторяющегося кода и записать его в отдельный файл для последующего повторного использования. Давайте еще раз посмотрим, может ли наша бизнес-логика служить команде. Плагины пишутся не случайно, а абстрагируются согласно собственной бизнес-логике. Не существует универсального плагина, только для плагинов Причина, по которой они называются плагинами, заключается в том, что их можно использовать из коробки, или мы можем добиться нужных нам результатов, добавив некоторые параметры конфигурации. Если все эти условия соблюдены, мы рассмотрим возможность создания плагина.

Условия пакета плагинов

Многоразовый плагин должен соответствовать следующим условиям:

  1. Область действия самого подключаемого модуля не зависит от текущей области действия пользователя, то есть приватные переменные внутри подключаемого модуля не могут влиять на переменные среды пользователя;
  2. Плагин должен иметь параметры настроек по умолчанию;
  3. В дополнение к базовым функциям, которые были реализованы, подключаемый модуль должен предоставлять некоторые API-интерфейсы, с помощью которых пользователь может изменять параметры по умолчанию функции подключаемого модуля, чтобы реализовать определяемый пользователем эффект подключаемого модуля;
  4. Плагины поддерживают вызовы в цепочке;
  5. Плагин должен предоставить запись мониторинга и контролировать указанный элемент, чтобы элемент и ответ плагина могли достичь эффекта плагина.

Об условиях инкапсуляции плагинов можно прочитать в статье:Рекомендации по написанию нативных плагинов JavaScript
И я хочу проиллюстрировать, как шаг за шагом реализовать инкапсуляцию моего плагина. Итак, сначала я начну с простых функций методов.

Вставная упаковка

обернуть с функцией

Так называемый плагин на самом деле представляет собой набор функций, заключенных в замыкание. Я помню, когда впервые начал писать js, я сделал так: я написал нужную мне логику в функцию, а затем передавал разные параметры в соответствии с разными потребностями. Например, я хочу реализовать метод, который складывает два числа:

function add(n1,n2) {
    return n1 + n2;
}
// 调用
add(1,2)
// 输出:3

Это простая реализация нужной нам функциональности. Если просто реализовать такую ​​простую логику, то этого достаточно, не нужно делать какие-то вычурные вещи. Сама функция js может решить подавляющее большинство проблем. Однако в нашей реальной работе и приложениях общие требования намного сложнее.
Если в это время продукт приходит сказать вам, что мне нужно не только сложение двух чисел, но и функции вычитания, умножения, деления, остатка и так далее. В это время, что мы должны делать?
Вы, конечно, подумаете, что тут такого сложного. Просто напишите все эти функции, и все готово. Затем поместите их все в файл js. Просто позвоните, когда вам это нужно.

// 加
function add(n1,n2) {
    return n1 + n2;
}
// 减
function sub(n1,n2) {
    return n1 - n2;
}
// 乘
function mul(n1,n2) {
    return n1 * n2;
}
// 除
function div(n1,n2) {
    return n1 / n2;
}
// 求余
function sur(n1,n2) {
    return n1 % n2;
}

Хорошо, теперь все необходимые нам функции реализованы. И мы также написали эти функции в js. Если он используется одним человеком, то вы можете четко знать, определили ли вы что-либо, и знаете, какой контент вы написали и какая страница мне нужна, тогда вы можете напрямую импортировать этот файл js, и вы можете это сделать.
Однако, если это команда из более чем двух человек или вы сотрудничаете с другими для написания кода, в это время другой человек не знает, написали ли вы метод добавления, и он также определяет тот же метод добавления. Тогда будет между вамиконфликт имен, обычно называемая переменнойглобальное загрязнение

обернуть глобальным объектом

Чтобы решить эту проблему глобального переменного загрязнения. На этом этапе мы можем определить объект js для получения наших служебных функций.

var plugin = {
    add: function(n1,n2){...},//加
    sub: function(n1,n2){...},//减
    mul: function(n1,n2){...},//乘
    div: function(n1,n2){...},//除
    sur: function(n1,n2){...} //余
}
// 调用
plugin.add(1,2)

В приведенном выше методе согласовано, что этот плагин называетсяplugin, так что члены команды должны соблюдать правила именования, что в определенной степени решило проблему глобального загрязнения. В командной работе, если вы согласны с правилами именования, вы можете рассказать об этом другим учащимся. Конечно, не исключено, что кто-то возьмет на себя управление вашим проектом и не знает, что эта глобальная переменная определена, и если он ее еще раз определит и присвоит, то ваш объект будет перезаписан. Конечно, вы можете сделать это, чтобы разрешить конфликт имен:

if(!plugin){ //这里的if条件也可以用: (typeof plugin == 'undefined')
    var plugin = {
        // 以此写你的函数逻辑
    }
}

Или еще можно написать так:

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

Таким образом, не будет конфликтов имен.

Некоторые студенты могут спросить, почему переменная плагина может быть объявлена ​​здесь? На самом деле, интерпретация и выполнение js будут опережать все объявления. Если переменная была объявлена, она не действует, если она не объявлена ​​внутри функции. Таким образом, даже если плагин var объявлен в другом месте, я могу снова объявить его здесь. Для получения соответствующей информации об этом заявлении см. Ruan Yifeng'sКак узнать, существует ли объект Javascript.

В принципе, это можно считать плагином. Для решения глобальной проблемы загрязнения функцию метода можно извлечь и поместить в отдельный файл.

Используйте закрытую упаковку

Приведенный выше пример, хотя основная функция плагина может быть достигнута. Но наш объект плагина определен в глобальной области видимости. Мы знаем, что вызов переменных js будет намного медленнее искать в глобальной области видимости, чем в частной области видимости. Итак, нам лучше написать логику плагина в приватной области видимости.
Лучший способ реализовать частную область видимости — использовать замыкания. Плагин можно рассматривать как функцию, переменные внутри плагина и частные переменные функции.Чтобы по-прежнему использовать его функции после вызова плагина, функция закрытия состоит в том, чтобы продлить жизненный цикл переменных внутри функция (плагин), чтобы можно было многократно вызывать функцию плагина, не затрагивая собственную область действия пользователя.
Поэтому необходимо прописать все функции плагина в функции немедленного выполнения:

;(function(global,undefined) {
    var plugin = {
        add: function(n1,n2){...}
        ...
    }
    // 最后将插件对象暴露给全局对象
    'plugin' in global && global.plugin = plugin;
})(window);

Объясните проблему передачи параметров в приведенном выше сегменте кода:

  1. Добавление точки с запятой перед определением плагина может решить проблему с ошибкой, которая может возникнуть при слиянии js;
  2. undefined не поддерживается в старых браузерах, и при прямом использовании будет выдано сообщение об ошибке.Фреймворк js должен учитывать совместимость, поэтому добавляйте формальный параметр undefined, даже если кто-то поставит внешнийundefinedОпределенное, неопределенное внутри остается незатронутым;
  3. Передача объекта окна в качестве параметра позволяет избежать внешнего поиска при выполнении функции.

На самом деле, мы считаем, что нецелесообразно напрямую передавать объект окна. Мы не уверены, что наши подключаемые модули обязательно используются в браузерах, а также могут использоваться на некоторых небраузерных терминалах. Таким образом, мы все еще можем сделать это, мы не передаем параметры и напрямую берем текущий глобальный объект this в качестве объекта верхнего уровня.

;(function(global,undefined) {
    "use strict" //使用js严格模式检查,使语法更规范
    var _global;
    var plugin = {
        add: function(n1,n2){...}
        ...
    }
    // 最后将插件对象暴露给全局对象
    _global = (function(){ return this || (0, eval)('this'); }());
    !('plugin' in _global) && (_global.plugin = plugin);
}());

Таким образом, нам не нужно передавать какие-либо параметры, и решается зависимость плагина от среды. Таким образом, наш плагин может работать на любом хостинге.

В приведенном выше фрагменте кода есть странное выражение:(0, eval)('this'), по факту(0,eval)является выражением, и результатом этого выражения после выполнения являетсяevalЭто предложение эквивалентно выполнениюeval('this')Это означает, что см. эту статью для подробного объяснения:(0,eval)('это') Интерпретацияили взгляните на это(0,eval)('this')

Что касается немедленной самовыполняющейся функции, есть два способа записи:

// 写法一
(function(){})()

//写法二
(function(){}())

Нет никакой разницы между двумя написаниями выше. все правильно написаны. Лично рекомендую использовать второй способ записи. Это больше похоже на целое.

Дополнительные знания:
js внутри()Круглые скобки превращают структуры кода в выражения, заключенные в скобки.()После того, как внутренняя часть станет выражением, она будет немедленно выполнена.Существует множество способов превратить кусок кода в выражение в js, например:

void function(){...}();
// 或者
!function foo(){...}();
// 或者
+function foot(){...}();

Конечно, мы не рекомендуем вам использовать его таким образом. И неправильное использование может создать некоторую двусмысленность.

На этом инфраструктура нашего плагина завершена.

Используйте модульную каноническую упаковку

Хотя приведенная выше упаковка в основном в порядке. Но что делать, если несколько человек вместе разрабатывают большой плагин? Когда несколько человек сотрудничают, обязательно будет сгенерировано несколько файлов, и каждый человек отвечает за небольшую функцию, так как же мы можем собрать код, разработанный всеми? Это неприятный вопрос. Для совместной разработки плагинов должны быть выполнены следующие условия:

  • Зависимости между каждой функцией должны быть четкими, и должны быть объединены или загружены в строгом соответствии с порядком зависимостей.
  • Каждая подфункция должна быть замыканием и предоставлять общедоступный интерфейс для общего домена, то есть общедоступный объект, предоставляемый основной функцией.

Есть много способов получить ключ. Самый тупой способ - загружать js последовательно

<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>

Но это не рекомендуется, так как это противоречит инкапсуляции плагинов, которые нам нужны.
Но сейчас в мире фронтенда есть куча популярных загрузчиков модулей, таких какrequire,seajs, или он может быть загружен способом, аналогичным Node, но на стороне браузера мы должны использовать упаковщик для реализации загрузки модуля, напримерbrowserify. Однако мы не будем здесь говорить о том, как упаковывать или загружать модульно, если у вас есть какие-либо вопросы, вы можете перейти по ссылке выше, чтобы прочитать документацию.
Чтобы добиться модульности плагина и сделать наш плагин также модулем, мы должны сделать так, чтобы наш плагин также реализовывал модульный механизм.
Нам собственно просто надо судить есть ли загрузчик, если есть загрузчик используем загрузчик, если загрузчика нет. Мы просто используем объект домена верхнего уровня.

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

Таким образом, наш полный плагин должен выглядеть так:

// plugin.js
;(function(undefined) {
    "use strict"
    var _global;
    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){ return n1 % n2; } //余
    }
    // 最后将插件对象暴露给全局对象
    _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(){return plugin;});
    } else {
        !('plugin' in _global) && (_global.plugin = plugin);
    }
}());

После того, как мы представили плагин, мы можем напрямую использовать объект плагина.

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
}

Плагин API

Параметры плагина по умолчанию

Мы знаем, что функции могут устанавливать параметры по умолчанию, и независимо от того, передаем ли мы параметры, мы должны возвращать значение, чтобы сообщить пользователю, что я сделал, например:

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

console.log(add()) //不传参,结果输出0,则这里已经设置了默认了参数为空数组
console.log(add(1,2,3,4,5)) //传参,结果输出15

В качестве надежного js-плагина мы должны добавить некоторые базовые параметры состояния в нужные нам плагины. Предполагая, что вышеуказанные сложение, вычитание, умножение и деление все еще необходимы, как мы реализуем параметры плагина по умолчанию? Правда на самом деле такая же.

// 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;
            });
        },//加
        sub: function(){
            return result(arguments,function(pre,cur){
                return pre - cur;
            });
        },//减
        mul: function(){
            return result(arguments,function(pre,cur){
                return pre * cur;
            });
        },//乘
        div: function(){
            return result(arguments,function(pre,cur){
                return pre / cur;
            });
        },//除
        sur: function(){
            return result(arguments,function(pre,cur){
                return pre % cur;
            });
        } //余
    }

    // 最后将插件对象暴露给全局对象
    _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(){return plugin;});
    } else {
        !('plugin' in _global) && (_global.plugin = plugin);
    }
}());

// 输出结果为:
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
}

Фактически, плагины имеют свои собственные параметры по умолчанию, взяв в качестве примера наш самый распространенный плагин проверки формы:validate.js

(function(window, document, undefined) {
    // 插件的默认参数
    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-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[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-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})$/i,
        base64Regex = /[^a-zA-Z0-9\/\+=]/i,
        numericDashRegex = /^[\d\-\s]+$/,
        urlRegex = /^((http|https):\/\/(\w+:{0,1}\w*@)?(\S+)|)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/,
        dateRegex = /\d{4}-\d{1,2}-\d{1,2}/;

    ... //省略后面的代码
})(window,document);
/*
 * Export as a CommonJS module
 */
if (typeof module !== 'undefined' && module.exports) {
    module.exports = FormValidator;
}

Конечно, поскольку параметры установлены по умолчанию, это означает, что мы можем изменить параметры по своему желанию в соответствии с нашими потребностями. Значение самого плагина в том, что его можно использовать повторно. Например, плагин проверки формы, вы можете изменить наши параметры по умолчанию при создании нового объекта:

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

крючок плагина

Мы знаем о дизайне плагина, параметрах или логике записи, безусловно, не мертвы, нам нравятся одни и те же функции, поэтому пользователи должны предоставлять свои собственные параметры для удовлетворения потребностей пользователей. Затем нам нужно предоставить плагин для изменения параметров входа по умолчанию.
Изменение параметров по умолчанию, как мы сказали выше, на самом деле является API, предоставляемым нам плагином. Сделайте наш плагин более гибким. Если вы не знаете об API, вы можете BaiduAPI
Обычно плагины js, которые мы используем, могут быть реализованы различными способами. Самая простая логика реализации — это метод, или js-объект, или конструктор и т.д.
** Конечно, так называемый API нашего плагина на самом деле представляет собой все методы и свойства, предоставляемые нашим плагином. **
В наших нуждах, в плагине сложения, вычитания, умножения и деления остатка, нашим API являются следующие методы:

...
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){ return n1 % n2; } 
}
...

Вы можете видеть, что методы, предоставляемые плагином, представляют собой следующие API:

  • add
  • sub
  • mul
  • div
  • sur

В подключаемом API мы часто называем методы или свойства, которые легко модифицировать и изменять, какКрюк, метод вызывается напрямуюфункция ловушки. Это яркий способ сказать, что мы как бы навешиваем на веревку множество крючков и можем вешать на них вещи по мере необходимости.
На самом деле мы знаем, что плагин может подвешивать вещи, как веревку, а также может удалять висящие вещи. Тогда плагин на самом деле является изображениемцепь. Однако все наши хуки выше привязаны к объектам, которые не идеальны для реализации цепочек.

Призыв к приводу плагинов (используя текущий объект)

Не все плагины можно вызывать по цепочке, иногда мы просто используем хуки для реализации вычисления и возврата результата, и просто получаем результат вычисления. Но бывают случаи, когда мы используем хуки и не нуждаемся в них для возврата результатов. Мы используем его только для реализации нашей бизнес-логики.Для простоты и удобства кода мы часто вызываем плагин по цепочке. Наиболее распространенные связанные вызовы jQuery следующие:

$(<id>).show().css('color','red').width(100).height(100)....

Итак, как мы можем применить цепочку к нашему плагину? Предполагая наш пример выше, если вы хотите вызывать в соответствии с цепочкой объекта плагина, вы можете изменить его бизнес-структуру на:

...
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; } 
}
...

Показано, что пока мы возвращаем текущий объект этого плагина напрямую, в следующем методе мы также можем ссылаться на другие методы ловушек плагина объекта плагина. Затем вы можете использовать цепочку при вызове.

plugin.add().sub().mul().div().sur()  //如此调用显然没有任何实际意义

Очевидно, что особого смысла в этом нет. Каждая функция-ловушка, которая у нас есть, просто используется для вычисления и получения возвращаемого значения. Смысл самого цепного вызова заключается в обработке бизнес-логики.

Цепной вызов плагинов (с использованием цепочек прототипов)

В JavaScript все является объектом, и все объекты наследуются от прототипов. Когда JS создает объект (будь то обычный объект или объект функции), существует метод, называемый__proto__встроенное свойство , указывающее на прототип объекта-прототипа функционального объекта, создавшего его. Для задачи прототипа заинтересованные студенты могут прочитать это:цепочка прототипов js
В приведенных выше требованиях мы можем изменить объект плагина на метод прототипа, тогда нам нужно написать плагин как конструктор, и мы заменим имя плагина наCalculateИзбегайте конфликтов с API в объекте Window, потому что Plugin пишется с заглавной буквы.

...
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;}
...

Конечно, если предположить, что наш плагин работает с параметрами инициализации и просто выводит результат, мы можем немного изменить его:

// 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){return p + c;});
            case 2: return argsArr.reduce(function(p,c){return p - c;});
            case 3: return argsArr.reduce(function(p,c){return p * c;});
            case 4: return argsArr.reduce(function(p,c){return p / c;});
            case 5: return argsArr.reduce(function(p,c){return p % 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;}


    // 最后将插件对象暴露给全局对象
    _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(){return Calculate;});
    } else {
        !('Calculate' in _global) && (_global.Calculate = Calculate);
    }
}());

В это время, когда вызывается написанный нами плагин, вывод выглядит следующим образом:

var plugin = new Calculate();
plugin
    .add(2,1)
    .sub(2,1)
    .mul(2,1)
    .div(2,1)
    .sur(2,1);
// 结果:
// 3
// 1
// 2
// 2
// 0

Приведенный выше пример может не иметь большого практического значения. Однако в веб-дизайне наши плагины в основном обслуживают уровень пользовательского интерфейса и используют скрипты js для достижения некоторых интерактивных эффектов. В это время мы пишем UI-плагин, и процесс реализации также можно вызвать с помощью цепочки.

Написать UI Компоненты

В общем, если js обрабатывает только логику, мы называем его плагином, но если он связан с dom и css и обладает определенной степенью интерактивности, его обычно называют компонентом. Конечно, явного различия нет, это просто привычное название.
Используя цепочку прототипов, вы можете инкапсулировать некоторый бизнес-код уровня пользовательского интерфейса в небольшой компонент и использовать js для реализации интерактивности компонента.
Есть такое требование:

  1. Реализовать всплывающий слой, который может отображать некоторую текстовую информацию;
  2. В правом верхнем углу всплывающего слоя должна быть кнопка закрытия, после нажатия всплывающий слой исчезает;
  3. В нижней части эластичного слоя должна быть кнопка «ОК», а затем дополнительная кнопка «Отмена» может быть настроена по мере необходимости;
  4. После нажатия кнопки «ОК» может быть запущено событие;
  5. После нажатия кнопки «Закрыть/Отмена» может быть запущено событие.

Согласно требованиям сначала напишем структуру dom:

<!DOCTYPE html>
<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">×</span>
        <div class="mydialog-cont">
            <div class="cont">hello world!</div>
        </div>
        <div class="footer">
            <span class="btn">确定</span>
            <span class="btn">取消</span>
        </div>
    </div>
    <script src="index.js"></script>
</body>
</html>

Напишите структуру css:

* { 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; }

Далее приступаем к написанию нашего интерактивного плагина. Мы предполагаем, что всплывающий слой компонента является объектом. Тогда этот объект является процессом, который содержит наше взаимодействие, стиль, структуру и рендеринг. Итак, мы определяем конструктор:

function MyDialog(){} // MyDialog就是我们的组件对象了

объектMyDialogЭто эквивалентно веревке, пока мы постоянно вешаем крюки на эту веревку, она является составной частью. Таким образом, наш компонент может быть представлен как:

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

Затем вы можете написать все функции плагина. Однако бизнес-логику в середине нужно изучать шаг за шагом самостоятельно. Независимо от того, как он написан, мы, наконец, сможем использовать наш плагин, создав экземпляр объекта MyDialog. В процессе написания мы должны сначала выполнить некоторые функции инструмента:

1. Функция слияния объектов

// 对象合并
function extend(o,n,override) {
    for(var key in n){
        if(n.hasOwnProperty(key) && (!o.hasOwnProperty(key) || override)){
            o[key]=n[key];
        }
    }
    return o;
}

2. Функция интерпретации настраиваемого механизма шаблонов

// 自定义模板引擎
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);
}

3. Найдите класс, чтобы получить функцию dom

// 通过class查找dom
if(!('getElementsByClass' in HTMLElement)){
    HTMLElement.prototype.getElementsByClass = function(n, tar){
        var el = [],
            _el = (!!tar ? tar : this).getElementsByTagName('*');
        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;
    };
    ((typeof HTMLDocument !== 'undefined') ? HTMLDocument : Document).prototype.getElementsByClass = HTMLElement.prototype.getElementsByClass;
}

В сочетании с функцией инструмента, а затем реализовать конкретную логическую структуру каждой функции ловушки:

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

    ...

    // 插件构造函数 - 返回数组结构
    function MyDialog(opt){
        this._initial(opt);
    }
    MyDialog.prototype = {
        constructor: this,
        _initial: function(opt) {
            // 默认参数
            var def = {
                ok: true,
                ok_txt: '确定',
                cancel: false,
                cancel_txt: '取消',
                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) { // 将模板转为字符串
            var data = this.def;
            var tplStr = document.getElementById(tmpId).innerHTML.trim();
            return templateEngine(tplStr,data);
        },
        _parseToDom: function(str) { // 将字符串转为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 '-' + 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(){return MyDialog;});
    } else {
        !('MyDialog' in _global) && (_global.MyDialog = MyDialog);
    }
}());

На данный момент наш плагин достиг основных требований. Мы можем вызвать это так на странице:

<script type="text/template" id="dialogTpl">
    <div class="mydialog">
        <span class="close">×</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>

Мониторинг плагинов

Во всплывающем плагине мы реализовали базовые функции отображения и скрытые функции. Однако, как мы всплываем, некоторые операции могут быть выполнены после всплывающего окна, на самом деле необходимые для выполнения некоторых контролируемых операций. Похоже, у нас есть привязка события, только пользователь нажимает кнопку, чтобы отреагировать на конкретное событие. Тогда наш плагин тоже должен быть как обвязка события, только при выполнении определенных операций вызывать соответствующий отклик на событие.
Этот шаблон проектирования js называетсяПодписаться/опубликовать модель, также известен какШаблон наблюдателя. Нам также необходимо использовать режим наблюдателя в нашем плагине.Например, перед открытием всплывающего окна нам нужно обновить содержимое всплывающего окна, выполнить некоторую логику оценки и т. д., а затем отобразить всплывающее окно после завершения выполнения. После закрытия всплывающего окна нам нужно выполнить некоторую логику после закрытия, обработать бизнес и т.д. В настоящее время нам нужно сделать некоторые методы обратного вызова привязки «события» для подключаемого модуля, поскольку мы обычно привязываем события.
Ответ нашего jquery на событие dom выглядит следующим образом:

$(<dom>).on("click",function(){})

Мы разработали соответствующий ответ плагина вышеописанным образом следующим образом:

mydialog.on('show',function(){})

Затем нам нужно реализовать механизм событий для достижения эффекта мониторинга событий плагина. Для пользовательского мониторинга событий вы можете обратиться к сообщению в блоге:Говоря о пользовательских событиях js, пользовательских событиях DOM/псевдо-DOM. Я не буду здесь говорить о проблеме пользовательских событий.
Окончательный код плагина, который мы реализовали:

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

    // 工具函数
    // 对象合并
    function extend(o,n,override) {
        for(var key in n){
            if(n.hasOwnProperty(key) && (!o.hasOwnProperty(key) || override)){
                o[key]=n[key];
            }
        }
        return o;
    }
    // 自定义模板引擎
    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);
    }
    // 通过class查找dom
    if(!('getElementsByClass' in HTMLElement)){
        HTMLElement.prototype.getElementsByClass = function(n){
            var el = [],
                _el = this.getElementsByTagName('*');
            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;
        };
        ((typeof HTMLDocument !== 'undefined') ? HTMLDocument : Document).prototype.getElementsByClass = HTMLElement.prototype.getElementsByClass;
    }

    // 插件构造函数 - 返回数组结构
    function MyDialog(opt){
        this._initial(opt);
    }
    MyDialog.prototype = {
        constructor: this,
        _initial: function(opt) {
            // 默认参数
            var def = {
                ok: true,
                ok_txt: '确定',
                cancel: false,
                cancel_txt: '取消',
                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; //检查dom树中dialog的节点是否存在
            this.listeners = []; //自定义事件,用于监听插件的用户交互
            this.handlers = {};
        },
        _parseTpl: function(tmpId) { // 将模板转为字符串
            var data = this.def;
            var tplStr = document.getElementById(tmpId).innerHTML.trim();
            return templateEngine(tplStr,data);
        },
        _parseToDom: function(str) { // 将字符串转为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 '-' + 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;
        }
    }

    // 最后将插件对象暴露给全局对象
    _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(){return MyDialog;});
    } else {
        !('MyDialog' in _global) && (_global.MyDialog = MyDialog);
    }
}());

Затем при вызове вы можете напрямую использовать привязку событий плагина.

var mydialog = new MyDialog({
    tmpId: 'dialogTpl',
    cancel: true,
    content: 'hello world!'
});
mydialog.on('confirm',function(ev){
    console.log('you click confirm!');
    // 写你的确定之后的逻辑代码...
});
document.getElementById('test').onclick = function(){
    mydialog.show();
}

Приведите этот примерdemoЕсть особенно понял одноклассник, чтобы увидеть это.

Выпуск плагина

Мы написали подключаемый модуль, и мы можем опубликовать его в организациях с открытым исходным кодом, чтобы поделиться им с большим количеством людей (код должен находиться в частной собственности со всеми правами управления). После того, как мы упакуем подключаемый модуль, мы можем опубликовать его в организациях с открытым исходным кодом, чтобы другие могли его загрузить и использовать.
Известное сообщество npm — очень хорошая платформа для публикации плагинов. В частности, вы можете сделать следующее:
Напишите файл описания пакета инициализации:

$ npm init

Зарегистрируйте учетную запись склада посылок

$ npm adduser
Username: <帐号>
Password: <密码>
Email:(this IS public) <邮箱>
Logged in as <帐号> on https://registry.npmjs.org/.

загрузить пакет

$ npm publish

Инсталляционный пакет

$ npm install mydialog

На данный момент наш плагин может напрямую использоваться большим количеством людей.

В заключение

Я написал так много, и это довольно многословно, поэтому я сделаю резюме здесь: Что касается того, как написать хороший нативный плагин для js, вам нужно больше проверять документацию по API при использовании чужих плагинов, понять, как называется подключаемый модуль, а затем взглянуть на то, как устроен исходный код подключаемого модуля. По сути, мы можем быть уверены, что большинство плагинов разработаны в виде прототипа. В приведенном выше примере я использовал много собственных знаний js, конфликты имен функций, замыкания, области действия, пользовательские функции инструментов для расширения функции ловушки объектов, а также инициализацию объекта, наследование цепочки прототипов, конструкторы. Определение и шаблон проектирования of , а также знание настройки событий, шаблона наблюдателя шаблона проектирования js и так далее. Это содержимое по-прежнему требует, чтобы новички многое понимали, прежде чем они смогут разрабатывать некоторые высокоуровневые плагины.