Для фронтенд-инженера способность писать компоненты имеет решающее значение. Хотя javascript часто высмеивают как маленькую игрушку, благодаря непрерывным усилиям поколений больших коров я постепенно изучил набор методов написания компонентов.
Поговорим о том, как хорошо писать компоненты под существующую систему знаний.
Например, мы хотим реализовать такой компонент, который является подсчетом количества слов в поле ввода. Это должно быть очень простое требование.
Давайте посмотрим на различные способы написания ниже.
Для более наглядной демонстрации во всех приведенных ниже библиотеках в качестве базовой языковой библиотеки используется jQuery.
самое простое написание
Что ж, так называемый метод записи начального уровня — это полный метод записи глобальной функции и глобальной переменной. (Насколько я знаю, многие аутсорсинг до сих пор пишут именно так)
код показывает, как показано ниже:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>test</title>
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script>
$(function() {
var input = $('#J_input');
//用来获取字数
function getNum(){
return input.val().length;
}
//渲染元素
function render(){
var num = getNum();
//没有字数的容器就新建一个
if ($('#J_input_count').length == 0) {
input.after('<span id="J_input_count"></span>');
};
$('#J_input_count').html(num+'个字');
}
//监听事件
input.on('keyup',function(){
render();
});
//初始化,第一次渲染
render();
})
</script>
</head>
<body>
<input type="text" id="J_input"/>
</body>
</html>
Этот код тоже можно запускать, но различные переменные сбивают с толку и нет хорошей области изоляции, когда страница станет сложной, ее будет сложно поддерживать. В настоящее время этот код практически бесполезен. Конечно, можно просто использовать небольшое количество страниц активности.
изоляция области
Давайте внесем некоторые изменения в приведенный выше код, чтобы имитировать пространства имен с одной переменной.
var textCount = {
input:null,
init:function(config){
this.input = $(config.id);
this.bind();
//这边范围对应的对象,可以实现链式调用
return this;
},
bind:function(){
var self = this;
this.input.on('keyup',function(){
self.render();
});
},
getNum:function(){
return this.input.val().length;
},
//渲染元素
render:function(){
var num = this.getNum();
if ($('#J_input_count').length == 0) {
this.input.after('<span id="J_input_count"></span>');
};
$('#J_input_count').html(num+'个字');
}
}
$(function() {
//在domready后调用
textCount.init({id:'#J_input'}).render();
})
Такое преобразование сразу становится намного понятнее, и все функции находятся под одной переменной. Код понятнее, и есть унифицированный метод вызова входа.
Но все еще есть некоторые недостатки.В этом способе написания нет частной концепции.Например, вышеприведенные getNum и bind должны быть закрытыми методами. Но другой код может изменить их по желанию. Когда объем кода особенно велик, легко возникает проблема повторения или модификации переменных.
Итак, есть еще один способ написать замыкание функции:
var TextCount = (function(){
//私有方法,外面将访问不到
var _bind = function(that){
that.input.on('keyup',function(){
that.render();
});
}
var _getNum = function(that){
return that.input.val().length;
}
var TextCountFun = function(config){
}
TextCountFun.prototype.init = function(config) {
this.input = $(config.id);
_bind(this);
return this;
};
TextCountFun.prototype.render = function() {
var num = _getNum(this);
if ($('#J_input_count').length == 0) {
this.input.after('<span id="J_input_count"></span>');
};
$('#J_input_count').html(num+'个字');
};
//返回构造函数
return TextCountFun;
})();
$(function() {
new TextCount().init({id:'#J_input'}).render();
})
При таком способе написания все завернуто в автоматически выполняемое замыкание, поэтому на него не повлияет внешний мир, и только конструктор TextCountFun открыт для внешнего мира, а сгенерированный объект может получить доступ только к методам init и render. Такой способ письма уже удовлетворяет большинству потребностей. На самом деле, большинство плагинов jQuery написаны таким образом.
объектно-ориентированный
Вышеупомянутый метод письма уже может удовлетворить большинство потребностей.
Однако, когда страница особенно сложная, когда нам нужно все больше и больше компонентов, когда нам нужно сделать набор компонентов. Просто использовать это не получится. Первая проблема заключается в том, что этот способ написания слишком гибкий, и можно написать один компонент. Если нам нужно сделать набор компонентов с похожими стилями, и несколько человек пишут одновременно. Это был действительно кошмар.
В кругах программистов объектная ориентация долгое время считалась лучшим способом написания кода. Например, Java, поскольку она доводит объектно-ориентированность до крайности, код, написанный несколькими людьми, очень близок, а обслуживание также очень удобно. Но, к сожалению, javascript не поддерживает определение класса. Но мы можем смоделировать.
Давайте сначала реализуем простой класс javascript:
var Class = (function() {
var _mix = function(r, s) {
for (var p in s) {
if (s.hasOwnProperty(p)) {
r[p] = s[p]
}
}
}
var _extend = function() {
//开关 用来使生成原型时,不调用真正的构成流程init
this.initPrototype = true
var prototype = new this()
this.initPrototype = false
var items = Array.prototype.slice.call(arguments) || []
var item
//支持混入多个属性,并且支持{}也支持 Function
while (item = items.shift()) {
_mix(prototype, item.prototype || item)
}
// 这边是返回的类,其实就是我们返回的子类
function SubClass() {
if (!SubClass.initPrototype && this.init)
this.init.apply(this, arguments)//调用init真正的构造函数
}
// 赋值原型链,完成继承
SubClass.prototype = prototype
// 改变constructor引用
SubClass.prototype.constructor = SubClass
// 为子类也添加extend方法
SubClass.extend = _extend
return SubClass
}
//超级父类
var Class = function() {}
//为超级父类添加extend方法
Class.extend = _extend
return Class
})()
Это простая модификация класса Джона Резига.
Это всего лишь очень простая реализация механизма наследования классов. Если вас интересует реализация класса, вы можете обратиться к другой моей статьереализация javascript oo
Мы надеемся использовать:
//继承超级父类,生成个子类Animal,并且混入一些方法。这些方法会到Animal的原型上。
//另外这边不仅支持混入{},还支持混入Function
var Animal = Class.extend({
init:function(opts){
this.msg = opts.msg
this.type = "animal"
},
say:function(){
alert(this.msg+":i am a "+this.type)
}
})
//继承Animal,并且混入一些方法
var Dog = Animal.extend({
init:function(opts){
//并未实现super方法,直接简单使用父类原型调用即可
Animal.prototype.init.call(this,opts)
//修改了type类型
this.type = "dog"
}
})
//new Animal({msg:'hello'}).say()
new Dog({msg:'hi'}).say()
Использование очень простое, родительский суперкласс имеет метод расширения, который может наследовать подкласс. Подклассы также имеют метод расширения.
Здесь следует подчеркнуть, что наследуемый родительский класс является единым, то есть единичным наследованием. Но несколько примесей могут быть достигнуты путем расширения. См. использование ниже.
С этим расширением класса мы можем написать такой код:
var TextCount = Class.extend({
init:function(config){
this.input = $(config.id);
this._bind();
this.render();
},
render:function() {
var num = this._getNum();
if ($('#J_input_count').length == 0) {
this.input.after('<span id="J_input_count"></span>');
};
$('#J_input_count').html(num+'个字');
},
_getNum:function(){
return this.input.val().length;
},
_bind:function(){
var self = this;
self.input.on('keyup',function(){
self.render();
});
}
})
$(function() {
new TextCount({
id:"#J_input"
});
})
Возможно, мы не увидели здесь реальных преимуществ класса, так что давайте двигаться дальше.
абстрактная основа
Как видите, у наших компонентов есть методы, которые есть у большинства компонентов.
- Например, init используется для инициализации свойств.
- Например, render используется для обработки логики рендеринга.
- Например, bind используется для обработки привязки событий.
Конечно, это тоже установленная норма. Если все будут писать код в таком стиле, разработка крупномасштабных библиотек компонентов станет более стандартизированной, и будет легче взаимодействовать друг с другом.
В это время появляются преимущества объектной ориентации, мы абстрагируем базовый класс. Он наследуется другими компонентами при их записи.
var Base = Class.extend({
init:function(config){
//自动保存配置项
this.__config = config
this.bind()
this.render()
},
//可以使用get来获取配置项
get:function(key){
return this.__config[key]
},
//可以使用set来设置配置项
set:function(key,value){
this.__config[key] = value
},
bind:function(){
},
render:function() {
},
//定义销毁的方法,一些收尾工作都应该在这里
destroy:function(){
}
})
Базовый класс в основном извлекает общее содержимое компонента, поэтому, когда мы пишем компонент, мы можем напрямую наследовать базовый класс и переопределять методы связывания и рендеринга внутри.
Таким образом, мы можем написать такой код:
var TextCount = Base.extend({
_getNum:function(){
return this.get('input').val().length;
},
bind:function(){
var self = this;
self.get('input').on('keyup',function(){
self.render();
});
},
render:function() {
var num = this._getNum();
if ($('#J_input_count').length == 0) {
this.get('input').after('<span id="J_input_count"></span>');
};
$('#J_input_count').html(num+'个字');
}
})
$(function() {
new TextCount({
//这边直接传input的节点了,因为属性的赋值都是自动的。
input:$("#J_input")
});
})
Видно, что мы напрямую реализуем какие-то фиксированные методы, bind и render. Другие базы обрабатываются автоматически (здесь мы просто обрабатываем назначение свойств конфигурации).
На самом деле init, bind и render здесь уже имеют тень жизненного цикла, но все компоненты будут иметь эти этапы, инициализацию, привязку событий и рендеринг. Конечно, здесь также можно добавить метод уничтожения для очистки сцены.
Кроме того, для удобства эта сторона сразу становится узлом для передачи ввода. Поскольку назначение свойств автоматизировано, в этом случае обычно используются геттеры и сеттеры. Здесь это не подробно.
Внедрить механизм событий (шаблон наблюдателя)
С базой надо сказать, что мы пишем компоненты более стандартизированно и системно. Ниже продолжаем копать глубже.
Или тот вышеприведенный пример, если мы хотим войти в слово более пяти слов, чтобы всплыть предупреждение. Что мы можем сделать по этому поводу.
Сяобай может сказать, что легко, просто измените метод связывания:
var TextCount = Base.extend({
...
bind:function(){
var self = this;
self.get('input').on('keyup',function(){
if(self._getNum() > 5){
alert('超过了5个字了。。。')
}
self.render();
});
},
...
})
Это действительно метод, но он слишком низкий, а код сильно связан. Когда такой спрос особенно велик, код становится все более и более хаотичным.
В это время необходимо ввести механизм событий, который часто называют паттерном наблюдателя.
Обратите внимание, что механизм событий здесь отличается от обычных событий браузера, и его следует рассматривать отдельно.
Что такое режим наблюдателя, в официальном объяснении не сказано, прямо возьмем этот пример.
Представьте базу — это робот, который может говорить, он всегда будет слушать количество введенных слов и сообщать об этом (уведомление). А можно приложить уши и послушать его отчет (мониторинг). Если вы обнаружите, что количество слов превышает 5 слов, вы можете что-то сделать.
Итак, это разделено на две части: одна — уведомление, а другая — прослушивание.
Предположим, что уведомление является методом пожара, а прослушивание включено. Таким образом, мы можем написать такой код:
var TextCount = Base.extend({
...
bind:function(){
var self = this;
self.get('input').on('keyup',function(){
//通知,每当有输入的时候,就报告出去。
self.fire('Text.input',self._getNum())
self.render();
});
},
...
})
$(function() {
var t = new TextCount({
input:$("#J_input")
});
//监听这个输入事件
t.on('Text.input',function(num){
//可以获取到传递过来的值
if(num>5){
alert('超过了5个字了。。。')
}
})
})
Fire используется для запуска события, которое может передавать данные. И on используется для добавления слушателя. Таким образом, компонент отвечает только за генерацию некоторых ключевых событий, а конкретная бизнес-логика может быть реализована путем добавления слушателей. Компонент неполный без событий.
Давайте посмотрим, как реализовать этот механизм событий.
Давайте сначала отложим базу и подумаем, как реализовать класс с этим механизмом.
//辅组函数,获取数组里某个元素的索引 index
var _indexOf = function(array,key){
if (array === null) return -1
var i = 0, length = array.length
for (; i < length; i++) if (array[i] === item) return i
return -1
}
var Event = Class.extend({
//添加监听
on:function(key,listener){
//this.__events存储所有的处理函数
if (!this.__events) {
this.__events = {}
}
if (!this.__events[key]) {
this.__events[key] = []
}
if (_indexOf(this.__events,listener) === -1 && typeof listener === 'function') {
this.__events[key].push(listener)
}
return this
},
//触发一个事件,也就是通知
fire:function(key){
if (!this.__events || !this.__events[key]) return
var args = Array.prototype.slice.call(arguments, 1) || []
var listeners = this.__events[key]
var i = 0
var l = listeners.length
for (i; i < l; i++) {
listeners[i].apply(this,args)
}
return this
},
//取消监听
off:function(key,listener){
if (!key && !listener) {
this.__events = {}
}
//不传监听函数,就去掉当前key下面的所有的监听函数
if (key && !listener) {
delete this.__events[key]
}
if (key && listener) {
var listeners = this.__events[key]
var index = _indexOf(listeners, listener)
(index > -1) && listeners.splice(index, 1)
}
return this;
}
})
var a = new Event()
//添加监听 test事件
a.on('test',function(msg){
alert(msg)
})
//触发 test事件
a.fire('test','我是第一次触发')
a.fire('test','我又触发了')
a.off('test')
a.fire('test','你应该看不到我了')
Это не сложно реализовать, просто используйте this.__events, чтобы сохранить все функции прослушивателя. Просто найдите его и выполните, когда он сработает.
В это время появляются преимущества объектной ориентации, если мы хотим, чтобы база имела механизм событий. Просто напишите это:
var Base = Class.extend(Event,{
...
destroy:function(){
//去掉所有的事件监听
this.off()
}
})
//于是可以
//var a = new Base()
// a.on(xxx,fn)
//
// a.fire()
Да, до тех пор, пока расширение смешивается с несколькими событиями, поэтому база или ее подклассы автоматически генерируются, объект имеет механизм событий.
С помощью механизма событий мы можем показать множество состояний внутри компонента, например, мы можем генерировать событие в методе set, чтобы мы могли прослушивать каждое изменение свойства.
На данный момент наш базовый класс довольно приличный, с методами init, bind, render, destroy для представления ключевых процессов компонента и механизмом событий. В принципе, компоненты могут быть разработаны очень хорошо.
Идем дальше, Richbase
Мы еще можем копнуть глубже. Взгляните на нашу базу, чего не хватает. Во-первых, мониторинг событий в браузере все еще очень отсталый, и пользователю нужно привязать его в бинде, затем в текущем TextCount все еще есть операция dom, и нет собственного шаблонного механизма. Все это нужно расширять, поэтому мы наследуем богатую базу, основанную на базе, чтобы реализовать более полный базовый класс компонента.
В основном реализуют эти функции:
- Прокси-сервер события: пользователю не нужно находить элемент dom для привязки и мониторинга, и пользователю не нужно заботиться о его уничтожении.
- Рендеринг шаблона: пользователю не нужно переопределять метод рендеринга, но переопределять метод setUp. Вы можете отобразить соответствующий html, вызвав render в setUp.
- Односторонняя привязка: с помощью метода setChuckdata данные обновляются, и одновременно обновляется содержимое html, и операция DOM больше не требуется.
Давайте посмотрим, как писать компоненты после того, как мы реализуем richbase:
var TextCount = RichBase.extend({
//事件直接在这里注册,会代理到parentNode节点,parentNode节点在下面指定
EVENTS:{
//选择器字符串,支持所有jQuery风格的选择器
'input':{
//注册keyup事件
keyup:function(self,e){
//单向绑定,修改数据直接更新对应模板
self.setChuckdata('count',self._getNum())
}
}
},
//指定当前组件的模板
template:'<span id="J_input_count"><%= count %>个字</span>',
//私有方法
_getNum:function(){
return this.get('input').val().length || 0
},
//覆盖实现setUp方法,所有逻辑写在这里。最后可以使用render来决定需不需要渲染模板
//模板渲染后会append到parentNode节点下面,如果未指定,会append到document.body
setUp:function(){
var self = this;
var input = this.get('parentNode').find('#J_input')
self.set('input',input)
var num = this._getNum()
//赋值数据,渲染模板,选用。有的组件没有对应的模板就可以不调用这步。
self.render({
count:num
})
}
})
$(function() {
//传入parentNode节点,组件会挂载到这个节点上。所有事件都会代理到这个上面。
new TextCount({
parentNode:$("#J_test_container")
});
})
/**对应的html,做了些修改,主要为了加上parentNode,这边就是J_test_container
<div id="J_test_container">
<input type="text" id="J_input"/>
</div>
*/
Глядя на приведенное выше использование, вы можете видеть, что оно становится проще и понятнее:
- События не нужно привязывать сами по себе, они напрямую регистрируются в свойстве EVENTS. Программа автоматически делегирует события родительскому узлу.
- Представлен механизм шаблонов, используйте шаблон для указания шаблона компонента, а затем используйте метод render(data) для визуализации шаблона в setUp, и программа автоматически добавит его к parentNode для вас.
- Односторонняя привязка, нет необходимости управлять домом, чтобы изменить содержимое позже, нет необходимости управлять домом, просто вызовите setChuckdata (ключ, новое значение), выборочно обновите определенные данные, соответствующий html будет автоматически повторно отображаться .
Давайте посмотрим на реализацию richebase:
var RichBase = Base.extend({
EVENTS:{},
template:'',
init:function(config){
//存储配置项
this.__config = config
//解析代理事件
this._delegateEvent()
this.setUp()
},
//循环遍历EVENTS,使用jQuery的delegate代理到parentNode
_delegateEvent:function(){
var self = this
var events = this.EVENTS || {}
var eventObjs,fn,select,type
var parentNode = this.get('parentNode') || $(document.body)
for (select in events) {
eventObjs = events[select]
for (type in eventObjs) {
fn = eventObjs[type]
parentNode.delegate(select,type,function(e){
fn.call(null,self,e)
})
}
}
},
//支持underscore的极简模板语法
//用来渲染模板,这边是抄的underscore的。非常简单的模板引擎,支持原生的js语法
_parseTemplate:function(str,data){
/**
* http://ejohn.org/blog/javascript-micro-templating/
* https://github.com/jashkenas/underscore/blob/0.1.0/underscore.js#L399
*/
var fn = new Function('obj',
'var p=[],print=function(){p.push.apply(p,arguments);};' +
'with(obj){p.push(\'' + str
.replace(/[\r\t\n]/g, " ")
.split("<%").join("\t")
.replace(/((^|%>)[^\t]*)'/g, "$1\r")
.replace(/\t=(.*?)%>/g, "',$1,'")
.split("\t").join("');")
.split("%>").join("p.push('")
.split("\r").join("\\'") +
"');}return p.join('');")
return data ? fn(data) : fn
},
//提供给子类覆盖实现
setUp:function(){
this.render()
},
//用来实现刷新,只需要传入之前render时的数据里的key还有更新值,就可以自动刷新模板
setChuckdata:function(key,value){
var self = this
var data = self.get('__renderData')
//更新对应的值
data[key] = value
if (!this.template) return;
//重新渲染
var newHtmlNode = $(self._parseTemplate(this.template,data))
//拿到存储的渲染后的节点
var currentNode = self.get('__currentNode')
if (!currentNode) return;
//替换内容
currentNode.replaceWith(newHtmlNode)
self.set('__currentNode',newHtmlNode)
},
//使用data来渲染模板并且append到parentNode下面
render:function(data){
var self = this
//先存储起来渲染的data,方便后面setChuckdata获取使用
self.set('__renderData',data)
if (!this.template) return;
//使用_parseTemplate解析渲染模板生成html
//子类可以覆盖这个方法使用其他的模板引擎解析
var html = self._parseTemplate(this.template,data)
var parentNode = this.get('parentNode') || $(document.body)
var currentNode = $(html)
//保存下来留待后面的区域刷新
//存储起来,方便后面setChuckdata获取使用
self.set('__currentNode',currentNode)
parentNode.append(currentNode)
},
destroy:function(){
var self = this
//去掉自身的事件监听
self.off()
//删除渲染好的dom节点
self.get('__currentNode').remove()
//去掉绑定的代理事件
var events = self.EVENTS || {}
var eventObjs,fn,select,type
var parentNode = self.get('parentNode')
for (select in events) {
eventObjs = events[select]
for (type in eventObjs) {
fn = eventObjs[type]
parentNode.undelegate(select,type,fn)
}
}
}
})
В основном он делает две вещи: первая — анализ и проксирование событий, все проксирование к parentNode. Кроме того, рендер извлекается, и пользователю нужно только реализовать метод setUp. Если вам нужна поддержка шаблона, вызовите render в setUp для рендеринга шаблона, и вы можете обновить шаблон через setChuckdata для достижения односторонней привязки.
Эпилог
С Richbase практически нет проблем с разработкой компонентов. Но мы все еще можем копнуть глубже.
Например, автоматическая загрузка и рендеринг компонентов, частичное обновление, такое как вложение родительских и дочерних компонентов, и двусторонняя привязка, такая как реализация механизма событий в стиле ng-click.
Конечно, эти вещи уже не относятся к содержанию компонента. Идя на шаг дальше, на самом деле это фреймворк. На самом деле, более популярные в последнее время реакции, ploymer и наши brix реализовали этот набор вещей. Из-за нехватки места я напишу статью для подробного анализа позже, когда у меня будет время.
Поскольку кто-то попросил меня предоставить полный код, все перечислено выше. Хорошо, тогда я разберусь и выложу на гитхаб конкретную демку, пожалуйста, нажмитездесь. Однако лучше не использовать его в производственной среде только для понимания использования. Дайте мне звезду, если вы найдете это полезным.