Escritura de JavaScript modular
Spanish (Español) translation by Kelly Gianella (you can also view the original English article)
Al escribir una aplicación web completa en JavaScript, es muy importante tenerla bien organizada; mantener un proyecto codificado con espaguetis con sólo causarle dolores de cabeza y pesadillas. En este tutorial, le mostraré cómo modularizar el código para facilitar la administración de proyectos de JavaScript de gran tamaño.
Introducción
Recientemente, re-vi una de mis presentaciones favoritas de JavaScript: Scalable JavaScript Application Architecture, de Nicolas C. Zakas. Encontré los patrones modulares de JavaScript que recomendó particularmente fascinantes, así que decidí probarlo. Tomé los fragmentos teóricos de código de sus diapositivas, los amplié y personalizó, y se me ocurrió lo que les mostraré hoy. Difícilmente puedo afirmar que soy un experto en esto: solo he estado pensando en este método de escribir JavaScript durante poco más de una semana. Pero espero poder mostrarles cómo podrían poner en práctica lo que Zakas estaba promoviendo, y hacer que piensen en la codificación de esta manera.
Unas palabras sobre el screencast que lo acompaña
Hay un screencast que va junto con este tutorial; es bastante largo (casi 2 horas y media, en realidad), así que aquí hay una tabla de contenido para ayudarlo a navegar por ella; Le he dado a cada sección un título "diapositiva" para que sea más fácil encontrar el comienzo de cada sección.
- 0:00:00 - Inicio
- 0:05:15 - Construyendo los módulos
- 0:40:05 - Construyendo el sandbox
- 1:04:39 - Construyendo el Núcleo
- 1:36:55 - Construyendo la interfaz (HTML & CSS)
- 1:51:08 - ¡Depuración!
- 2:02:27 - Construyendo el Núcleo (Dojo Edition)
- 2:18:47 - Discutiendo los beneficios
Presentación de Nicolas Zakas
Si no lo has visto recientemente, deberías ver la presentación de Zakas antes de seguir adelante; la mayor parte de lo que te mostraré será más fácil de entender con esto en tu haber:
La Presentación
Nota: esta no es la presentación original en la que se basó este tutorial, sino una versión más reciente de la misma charla.
Las diapositivas
El Resumen
Así que, en resumen, una buena aplicación JavaScript tendrá cuatro capas. De abajo hacia arriba, estos son los siguientes:
- La base: este sería su marco de JavaScript, como jQuery, si estuviera usando uno. O bien, puedes escribir el tuyo propio.
- El App Core: esta capa se encarga de enganchar todo y ejecutar tu aplicación web. También proporciona una capa de ecualización para la siguiente capa hacia arriba (el sandbox); eso puede parecer redundante una vez que te metes en él, y te estarás preguntando por qué no dejamos que el sandbox hable directamente con la base. Sin embargo, tener que ejecutar todo a través del núcleo primero nos permite cambiar fácilmente la base por un marco diferente. Entonces, sólo tenemos que cambiar las funciones del intermediario en el núcleo.
- El espacio aislado: esta es la mini-API y la única API a la que cada parte de nuestra aplicación web tiene acceso. Una vez que haya creado un sandbox con el que esté satisfecho, es muy importante que la interfaz externa no cambie de ninguna manera. Esto se debe a que tendrá muchos módulos dependiendo de él, y cambiar la API significaría pasar por cada módulo y actualizarlo, no es algo que desee hacer. Por supuesto, puede modificar el código dentro de los métodos del espacio aislado (siempre y cuando sigan haciéndolo) o agregar funcionalidad.
- Los Módulos: estos son los verdaderos que funcionan en nuestra app. Cada módulo es un fragmento de código independiente que ejecuta un único aspecto de la aplicación web. En la forma en que he escrito anteriormente aplicaciones web, todo el código del módulo está entrelazado en un lío similar a un espagueti, y cuando falta una pieza, toda la aplicación se bloquea. Cuando todo se pone ordenadamente en módulos (con interacciones proporcionadas a través de la caja de arena), una pieza que falta no causa ningún error.
Para explicar este patrón, crearemos una mini tienda en línea (bueno, solo el front-end). Esto realmente muestra el beneficio de los módulos, porque una tienda en línea tiene muchas partes distintas:
- un panel de productos
- un carro de la compra
- un cuadro de búsqueda
- una forma de filtrar productos
Cada una de estas piezas es claramente una parte separada de la página, pero todas ellas necesitan interactuar con otros módulos, ya sea como iniciador o como reciever. ¡Veremos cómo funciona todo esto!
Muy bien, ¡ya basta de teoría! ¡Comencemos a codificar!
Los bloques de creación: Módulos
Cuando comencé este proyecto, no tenía un núcleo o sandbox para trabajar. Por lo tanto, decidí comenzar codificando los módulos, porque esto me daría una idea de lo que los módulos necesitaban para poder acceder a través de la caja de arena.
Al crear un módulo, usé un patrón que está muy cerca del código de ejemplo que Zakas mostró:
1 |
CORE.create_module("search-box", function (sb) {
|
2 |
return {
|
3 |
init : function () {},
|
4 |
destroy : function () {}
|
5 |
}; |
6 |
}); |
Como puede ver, estamos usando el método create_module en el CORE para registrar este módulo con la aplicación web (por supuesto, aún no hemos creado el núcleo, pero llegaremos allí). Esta función tomará dos parámetros: el nombre del módulo (en este caso, "search-box") y una función que devuelve el objeto de módulo. Lo que he mostrado aquí es el módulo más básico posible. Observe algunas cosas sobre la función creador: primero, tiene un solo parámetro, que será una instancia de nuestro sandbox (cuando veamos el sandbox, veremos por qué una instancia es mejor que tener todos los módulos accedan al mismo objeto sandbox). Este objeto de espacio aislado es la única conexión que el módulo tiene con el "mundo exterior". (Por supuesto, como dijo Zakas, realmente no hay restricciones técnicas que le impidan acceder a la base o al núcleo directamente desde el módulo; simplemente no debe hacerlo). Como puede ver, esta función devuelve un objeto, que es nuestro objeto de módulo. Por lo menos, ese módulo solo tiene un método init (utilizado cuando iniciamos el módulo) y un método destroy (utilizado cuando cerramos el módulo).
Por lo tanto, vamos a construir un módulo real.
Screencast completo
El módulo de cuadro de búsqueda
1 |
CORE.create_module("search-box", function (sb) {
|
2 |
var input, button, reset; |
3 |
|
4 |
return {
|
5 |
init : function () {},
|
6 |
destroy : function () {},
|
7 |
handleSearch : function () {},
|
8 |
quitSearch : function () {}
|
9 |
}; |
10 |
}); |
Debe comprender lo que está sucediendo aquí: nuestro módulo usará tres variables: entrada, botón y restablecimiento. Además de las dos funciones de módulo necesarias en el objeto de retorno, estamos creando dos funciones relacionadas con la búsqueda. No creo que haya ninguna razón por la que tengan que formar parte del objeto de módulo devuelto; podrían declararse fácilmente por encima de la instrucción return y se podría hacer referencia a ellos mediante el cierre. Vamos a sumergirnos en cada una de estas funciones:
1 |
init : function () {
|
2 |
input = sb.find('#search_input')[0];
|
3 |
button = sb.find('#search_button')[0];
|
4 |
reset = sb.find("#quit_search")[0];
|
5 |
|
6 |
sb.addEvent(button, 'click', this.handleSearch); |
7 |
sb.addEvent(reset, 'click', this.quitSearch); |
8 |
}, |
Primero, estamos asignando las tres variables que necesitamos. Dado que necesitaremos poder dom elementos de nuestros módulos, necesitaremos un método de búsqueda en nuestro espacio aislado. Sin embargo, ¿qué pasa con el [0] al final? Bueno, nuestro método sb.find devuelve algo similar a un objeto jQuery: los elementos que coinciden con el selector reciben claves numeradas, y luego hay algunos métodos y propiedades. Sin embargo, solo vamos a necesitar los elementos DOM sin procesar, estamos agarrando eso (ya que solo un elemento tendrá un identificador, podemos estar seguros de que solo estamos devolviendo un elemento).
Dos de estos elementos (button y reset) son botones, por lo que necesitaremos enlazar algunos controladores de eventos. ¡Tendremos que agregar eso a la caja de arena también! Como puede ver, es su función de evento de adición estándar: toma el elemento, el evento y la función.
¿Qué tal en la destrucción del módulo:
1 |
destroy : function () {
|
2 |
sb.removeEvent(button, 'click', this.handleSearch); |
3 |
sb.removeEvent(reset, 'click', this.quitSearch); |
4 |
input = null; |
5 |
button = null; |
6 |
reset = null; |
7 |
}, |
Es bastante simple: tendrá que haber una función removeEvent que deshaga el trabajo de addEvent. Luego, simplemente estableceremos las tres variables de módulo en null.
Esos dos detectores de eventos hacen referencia a las funciones de búsqueda. Veamos la primera:
1 |
handleSearch : function () {
|
2 |
var query = input.value; |
3 |
if (query) {
|
4 |
sb.notify({
|
5 |
type : 'perform-search', |
6 |
data : query |
7 |
}); |
8 |
} |
9 |
}, |
En primer lugar, obtenemos el valor del campo de búsqueda. Si hay algo en él, seguiremos adelante. Pero, ¿qué debemos hacer? Normalmente al hacer una búsqueda dinámica (que no requiere una actualización de página de solicitud ajax), tendríamos acceso al panel de productos y podríamos filtrarlos adecuadamente. Pero nuestro módulo tiene que ser capaz de existir con o sin un panel de producto; además, su único enlace al mundo exterior es a través de la caja de arena. Así que esto es lo que Zakas propuso: simplemente le decimos al sandbox (que a su vez le dice al núcleo) que el usuario ha realizado una búsqueda. Luego, el núcleo ofrecerá esa información a los otros módulos. Si hay uno que responde, tomará los datos y se ejecutará con ellos. Hacemos esto a través del método sb.notify; toma un objeto con dos propiedades: el tipo de evento que estamos realizando y los datos relacionados con el evento. En este caso, estamos haciendo un evento de 'realizar búsqueda' y los datos relevantes son la consulta de búsqueda. Eso es todo lo que el módulo de cuadro de búsqueda necesita hacer; si hay otro módulo que ha expuesto la capacidad de ser buscado, el núcleo le dará los datos.
Lo bueno a tener en cuenta sobre esto es que este método es completamente versátil. El módulo que usará este evento en nuestro ejemplo no hará nada Ajax-y, pero no hay ninguna razón por la que otro módulo no pueda hacer eso, o buscar de alguna manera completamente distinta.
El método quitSearch no es mucho más complicado:
1 |
quitSearch : function () {
|
2 |
input.value = ""; |
3 |
sb.notify({
|
4 |
type : 'quit-search', |
5 |
data : null |
6 |
}); |
7 |
} |
En primer lugar, borraremos el cuadro de búsqueda; luego, le haremos saber al sandbox que estamos ejecutando una 'quit-search'; en este caso, no hay datos relevantes.
Lo creas o no, ese es todo el módulo de búsqueda. Bastante simple, ¿eh? Pasemos a la siguiente.
El módulo de la barra de filtros
Queremos que la tienda en línea para dar a los usuarios la posibilidad de ver los productos por categoría. Por lo tanto, vamos a implementar una barra de filtros, donde el usuario hace clic en los nombres de categoría para mostrar solo los elementos de la categoría dada.
1 |
CORE.create_module("filters-bar", function (sb) {
|
2 |
var filters; |
3 |
return {
|
4 |
init : function () {
|
5 |
filters = sb.find('a');
|
6 |
sb.addEvent(filters, 'click', this.filterProducts); |
7 |
}, |
8 |
destroy : function () {
|
9 |
sb.removeEvent(filters, 'click', this.filterProducts); |
10 |
filters = null; |
11 |
}, |
12 |
filterProducts : function (e) {
|
13 |
sb.notify({
|
14 |
type : 'change-filter', |
15 |
data : e.currentTarget.innerHTML |
16 |
}); |
17 |
} |
18 |
}; |
19 |
}); |
Este no es muy complicado. Necesitaremos una variable para contener los filtros. Como puede ver, estamos usando sb.find para obtener todos los anclajes. Pero, ¿cuáles son las posibilidades de que todos los anclajes de la página sean filtros? No muy bueno. Una vez que lleguemos a la escritura del método find, verás cómo solo devuelve los elementos dentro del elemento DOM correspondiente a nuestro módulo. A continuación, agregaremos un evento click a los filtros, que llama al método filterProduct. Como puede ver, ese método simplemente le dice al espacio aislado sobre nuestro evento 'change-filter', dándole el texto del enlace en el que se hizo clic en los datos (e es el objeto de evento y currentTarget es el elemento en el que se hizo clic.
Por supuesto, destroy simplemente se deshace de los detectores de eventos.
El módulo del panel de producto
Este va a ser bastante largo y posiblemente complicado, así que espera apretado! Comenzaremos con un shell:
1 |
CORE.create_module(“product-panel”, function (sb) {
|
2 |
var products; |
3 |
|
4 |
function eachProduct(fn) {
|
5 |
var i = 0, product; |
6 |
for ( ; product = products[i++]; ) {
|
7 |
fn(product); |
8 |
} |
9 |
} |
10 |
function reset () {
|
11 |
eachProduct(function (product) {
|
12 |
product.style.opacity = '1'; |
13 |
}); |
14 |
} |
15 |
return {
|
16 |
init : function () {},
|
17 |
reset : reset, |
18 |
destroy : function () {},
|
19 |
search : function (query) {},
|
20 |
change_filter : function (filter) {},
|
21 |
addToCart : function (e) {}
|
22 |
|
23 |
}; |
24 |
}); |
Tómese un momento para mirar sobre esto; no hay mucho que sea diferente de nuestros otros módulos; hay más de eso. La principal diferencia es que he usado dos funciones auxiliares, creadas fuera del objeto devuelto. El primero se llama eachProduct y, como puede ver, simplemente toma una función y la ejecuta para cada elemento de la lista de productos. La otra es una función de reinicio, que entenderemos en un momento.
Ahora echemos un vistazo a las funciones init y destroy.
1 |
init : function () {
|
2 |
var that = this; |
3 |
products = sb.find('li');
|
4 |
sb.listen({
|
5 |
'change-filter' : this.change_filter, |
6 |
'reset-fitlers' : this.reset, |
7 |
'perform-search' : this.search, |
8 |
'quit-search' : this.reset |
9 |
}); |
10 |
eachProduct(function (product) {
|
11 |
sb.addEvent(product, 'click', that.addToCart); |
12 |
}); |
13 |
}, |
14 |
destroy : function () {
|
15 |
var that = this; |
16 |
eachProduct(function (product) {
|
17 |
sb.removeEvent(product, 'click', that.addToCart); |
18 |
}); |
19 |
sb.ignore(['change-filter', 'reset-filters', 'perform-search', 'quit-search']); |
20 |
}, |
Dentro de init, recopilamos todos los productos (que están representados en elementos de lista). Luego, tenemos que hacerle saber al sandbox que estamos interesantes en varios eventos. Pasamos un objeto al método sb.listen; este objeto utiliza el nombre del evento como clave y la función de evento como un valor para cada propiedad. Por ejemplo, le estamos diciendo a sandbox que cuando otra persona ejecuta un evento 'perform-search', queremos responder a eso ejecutando nuestra función de búsqueda. Con suerte, ¡estás empezando a ver cómo funcionará esto!
Luego, usamos nuestra función auxiliar eachProduct para asignar una función al hacer clic a cada producto. Cuando un producto en el que hace clic, ejecutamos addToCart. Tenemos que almacenar esto en caché porque su valor cambia al objeto global dentro de la función.
En destroy, simplemente eliminamos los controladores de eventos de los productos y dejamos que el espacio aislado sepa que ya no estamos interesados en los eventos (en realidad, no creo que esto sea necesario, debido a la forma en que manejamos las cosas en el núcleo, pero lo tiré en caso de que algo en el "back-end" cambie).
Ahora veremos las funciones a las que se llama cuando otros módulos desencadenan eventos:
1 |
search : function (query) {
|
2 |
reset(); |
3 |
query = query.toLowerCase(); |
4 |
eachProduct(function (product) {
|
5 |
if (product.getElementsByTagName('p')[0].innerHTML.toLowerCase().indexOf(query) < 0) {
|
6 |
product.style.opacity = '0.2'; |
7 |
} |
8 |
}); |
9 |
}, |
10 |
change_filter : function (filter) {
|
11 |
reset(); |
12 |
eachProduct(function (product) {
|
13 |
if (product.getAttribute('data-8088-keyword').toLowerCase().indexOf(filter.toLowerCase()) < 0) {
|
14 |
product.style.opacity = '0.2'; |
15 |
} |
16 |
}); |
17 |
}, |
18 |
addToCart : function (e) {
|
19 |
var li = e.currentTarget; |
20 |
sb.notify({
|
21 |
type : 'add-item', |
22 |
data : { id : li.id, name : li.getElementsByTagName('p')[0].innerHTML, price : parseInt(li.id, 10) }
|
23 |
}); |
24 |
} |
Comenzaremos con la búsqueda; se llama a esta función cuando tiene lugar la acción 'perform-search'. Como puede ver, toma la consulta de búsqueda como un parámetro. En primer lugar, restablecemos el área del producto (en caso de que los resultados de una búsqueda o filtrado anterior estén allí). Luego, recorremos cada producto con nuestra función auxiliar. Recuerde, el producto es un elemento de lista; dentro hay un párrafo con la descripción del producto, y eso es lo que buscaremos (en un ejemplo del mundo real, este probablemente no sería el caso). Obtenemos el texto del párrafo y lo comparamos con el texto de la consulta (observe que ambos se han ejecutado a través de toLowerCase()). Si el resultado es menor que 0, lo que significa que no se encontró ninguna coincidencia, estableceremos la opacidad del producto en 0,2. Así es como ocultaremos los productos en este ejemplo. ¡Eso es todo!
Ahora es un buen momento para señalar que todo lo que hace la función de restablecimiento es establecer la opacidad de todos los productos en 1.
El método changeFilter es bastante similar a la búsqueda; esta vez, en lugar de buscar en la descripción del producto, aprovechamos los atributos data-* de HTML5. Estos nos permiten agregar atributos personalizados a nuestros elementos HTML sin romper las reglas de especificaciones. Sin embargo, deben comenzar con 'data-', y también he agregado un prefijo personal, por lo que no entran en conflicto con los atributos que el código de terceros podría usar. El filtro pasado a esta función se compara con el atributo data, que contendrá los nombres de las categorías de los elementos. Si no hay coincidencias, reduciremos la opacidad del elemento.
La función final es addToCart, que se ejecuta cuando se hace clic en uno de los productos. Obtendremos el elemento en el que se ha hecho clic y luego enviaremos una notificación al sistema, informándole sobre nuestro evento 'add-item'. Esta vez, los datos que estamos pasando son un objeto. Contiene el identificador del producto, el nombre del producto y el precio del producto. En este ejemplo, estamos siendo perezosos y usando el identificador de elemento como el identificador y el precio, y la descripción del producto como el nombre.
El módulo de carro de la compra
Tenemos un módulo más para mirar. Es el módulo 'shopping-cart':
1 |
CORE.create_module(“shopping-cart”, function (sb) {
|
2 |
var cart, cartItems; |
3 |
|
4 |
return {
|
5 |
init : function () {
|
6 |
cart = sb.find('ul')[0];
|
7 |
cartItems = {};
|
8 |
|
9 |
sb.listen({
|
10 |
'add-item' : this.addItem |
11 |
}); |
12 |
}, |
13 |
destroy : function () {
|
14 |
cart = null; |
15 |
cartItems = null; |
16 |
sb.ignore(['add-item']); |
17 |
}, |
18 |
addItem : function (product) {
|
19 |
} |
20 |
}; |
21 |
}); |
Creo que usted está recibiendo la caída de esto ahora; estamos usando dos variables: el carrito y los artículos en el carrito. En la inicialización del módulo, los estableceremos en ul en el carro de la compra y en un objeto vacío respectivamente. A continuación, haremos saber al espacio aislado que queremos responder a un evento. En cuanto a la destrucción, vamos a deshacer todo eso.
Esto es lo que debe suceder cuando se agrega un artículo al carrito:
1 |
addItem : function (product) {
|
2 |
var entry = sb.find('#cart-' + product.id + ' .quantity')[0];
|
3 |
if (entry) {
|
4 |
entry.innerHTML = (parseInt(entry.innerHTML, 10) + 1); |
5 |
cartItems[product.id]++; |
6 |
} else {
|
7 |
entry = sb.create_element('li', { id : "cart-" + product.id, children : [
|
8 |
sb.create_element('span', { 'class' : 'product_name', text : product.name }),
|
9 |
sb.create_element('span', { 'class' : 'quantity', text : '1'}),
|
10 |
sb.create_element('span', { 'class' : 'price', text : '$' + product.price.toFixed(2) })
|
11 |
], |
12 |
'class' : 'cart_entry' }); |
13 |
|
14 |
cart.appendChild(entry); |
15 |
cartItems[product.id] = 1; |
16 |
} |
17 |
|
18 |
} |
Esta función toma el objeto de producto que acabamos de ver en el módulo de panel de producto. Luego, obtenemos el elemento con el selector '#cart-' + product.id + '.quantity'; esto busca un elemento con una clase de 'cantidad' dentro de un elemento con un identificador de "cart-id_number". Si este producto se ha agregado al carrito antes, se encontrará. Si se encuentra, incrementaremos el innerHTML de ese elemento (la cantidad de ese producto que el usuario ha agregado al coche) en uno y actualizaremos la entrada en el objeto cartItems, que realiza un seguimiento de la compra.
Si no se encontró el elemento, esta es la primera vez que el usuario ha agregado uno de este producto al carrito. En ese caso, usaremos el método create_element del sandbox; como puede ver, tomará un objeto de atributos similar a jQuery. El caso especial aquí es la propiedad children, que es una matriz de elementos para insertar en el elemento que estamos creando. Como puede ver, básicamente estamos creando un elemento de lista con tres intervalos: el nombre del producto, la cantidad y el precio. Luego, anexamos este elemento de lista al carrito y agregamos el producto al objeto cartItems.
Ese es todo el código para nuestros módulos; Debo tener en cuenta que he puesto todo esto en un archivo llamado modules.js. Ahora que sabemos con qué interfaz tendrán que trabajar nuestros módulos, estamos listos para construir eso ... y esa es la caja de arena.
El soporte: Sandbox
Sé que ya he mencionado esto, pero es bastante importante que la interfaz orientada hacia el exterior de la caja de arena no cambie. Esto se debe a que todos los módulos dependen de él. Por supuesto, puede agregar métodos o cambiar el código dentro de los métodos, siempre y cuando no cambie los métodos o lo que hace o devuelve la función.
Si destilamos el archivo modules.js, veremos que estos son los métodos que el sandbox necesita para dar a los módulos:
- encontrar
- addEvent
- removeEvento
- notificar
- escuchar
- ignorar
- create_element
Así que pongámonos a trabajar. Dado que quiero crear una instancia de espacio aislado con el código Sandbox.create, lo convertiremos en un objeto con (actualmente) solo un método.
1 |
var Sandbox = {
|
2 |
create : function (core, module_selector) {
|
3 |
var CONTAINER = core.dom.query('#' + module_selector);
|
4 |
return {
|
5 |
|
6 |
}; |
7 |
} |
8 |
}; |
Aquí está nuestro comienzo. Como puede ver, el método create tomará dos parámetros: una referencia al núcleo y el nombre del módulo al que se le va a dar. Luego, creamos una variable, CONTAINER, que hará referencia al elemento DOM que se corresponde con el código del módulo. Ahora, comencemos a codificar las funciones que enumeramos.
1 |
find : function (selector) {
|
2 |
return CONTAINER.query(selector); |
3 |
}, |
Esto es bastante simple. De hecho, la mayor parte de la funcionalidad en el espacio aislado es bastante simple, porque se supone que es un contenedor delgado que le da al módulo la cantidad justa de acceso al núcleo. Cuando el método core.dom.query sobre el que llamamos devolvió el contenedor, le dio al contenedor un método que le permite buscar el elemento secundario por selector; estamos usando esto para limitar la capacidad de un módulo para afectar el DOM, manteniéndolo así como un módulo en el HTML, así como en el JavaScript.
1 |
addEvent : function (element, evt, fn) {
|
2 |
core.dom.bind(element, evt, fn); |
3 |
}, |
4 |
removeEvent : function (element, evt, fn) {
|
5 |
core.dom.unbind(element, evt, fn); |
6 |
}, |
Como dije, la mayoría de estas funciones de sandbox son bastante pequeñas; simplemente transportaremos los datos del evento al núcleo para conectarlos.
1 |
notify : function (evt) {
|
2 |
if(core.is_obj(evt) && evt.type) {
|
3 |
core.triggerEvent(evt); |
4 |
} |
5 |
}, |
6 |
listen : function (evts) {
|
7 |
if (core.is_obj(evts)) {
|
8 |
core.registerEvents(evts, module_selector); |
9 |
} |
10 |
}, |
11 |
ignore : function (evts) {
|
12 |
if (core.is_arr(evts)) {
|
13 |
core.removeEvents(evts, module_selector); |
14 |
} |
15 |
}, |
Estas tres funciones, como recordarás, son los vehículos que los módulos utilizan para informar a otros módulos sobre sus acciones. He agregado un poco de comprobación de errores a estos para asegurarme de que los datos del evento estén bien antes de enviarlos al núcleo. Tenga en cuenta que al decirle al núcleo lo que estamos escuchando o ignorando, también necesitamos pasar el nombre del módulo.
1 |
create_element : function (el, config) {
|
2 |
var i, text; |
3 |
el = core.dom.create(el); |
4 |
if (config) {
|
5 |
if (config.children && core.is_arr(config.children)) {
|
6 |
i = 0; |
7 |
while (config.children[i]) {
|
8 |
el.appendChild(config.children[i])); |
9 |
i++; |
10 |
} |
11 |
delete config.children; |
12 |
} else if (config.text) {
|
13 |
text = document.createTextNode(config.text); |
14 |
delete config.text; |
15 |
el.appendChild(text); |
16 |
} |
17 |
core.dom.apply_attrs(el, config); |
18 |
} |
19 |
return el; |
20 |
} |
Esta es obviamente la función más larga en el sandbox (y para ser honesto, cuanto más lo pienso, más creo que debería estar en el núcleo, pero de todos modos ...). Como sabemos, toma el nombre de un elemento y un objeto de configuración. Comenzamos creando el elemento DOM (usa un método principal). Luego, si hay un objeto config y tiene una matriz llamada children, recorreremos cada elemento secundario y lo anexaremos al elemento. Luego eliminamos la propiedad children De lo contrario, si tenemos una propiedad de texto, estableceremos el texto del elemento en eso y eliminaremos la propiedad text (en este ejemplo, no podemos tener elementos text y secundarios). Finalmente, usaremos otra función principal para aplicar los atributos restantes y devolver el elemento.
Y ese es el final de la caja de arena. Me doy cuenta de que esto podría ser un sandbox bastante simplista, pero debería darte una idea de la forma en que funciona el sandbox. Además, a medida que lo use, podrá agregar otros métodos cuando sus módulos lo requieran.
La Fundación: Base y Núcleo
Ahora estamos listos para pasar al núcleo. Esto es con lo que comenzamos:
1 |
var CORE = (function () {
|
2 |
var moduleData = {}, debug = true;
|
3 |
|
4 |
return {
|
5 |
debug : function (on) {
|
6 |
debug = on ? true : false; |
7 |
}, |
8 |
|
9 |
}; |
10 |
|
11 |
}()); |
Usaremos el objeto de datos del módulo para almacenar todo lo que hay que saber sobre los módulos; la variable de depuración controla si los errores se registran o no en la consola. Tenemos una función de depuración simple para activar y desactivar los errores.
Comencemos con esa función create_module que usamos para registrar nuestros módulos:
1 |
create_module : function (moduleID, creator) {
|
2 |
var temp; |
3 |
if (typeof moduleID === 'string' && typeof creator === 'function') {
|
4 |
temp = creator(Sandbox.create(this, moduleID)); |
5 |
if (temp.init && temp.destroy && typeof temp.init === 'function' && typeof temp.destroy === 'function') {
|
6 |
moduleData[moduleID] = {
|
7 |
create : creator, |
8 |
instance : null |
9 |
}; |
10 |
temp = null; |
11 |
} else {
|
12 |
this.log(1, "Module \"" + moduleId + "\" Registration: FAILED: instance has no init or destroy functions"); |
13 |
} |
14 |
} else {
|
15 |
this.log(1, "Module \"" + moduleId + "\" Registration: FAILED: one or more arguments are of incorrect type" ); |
16 |
|
17 |
} |
18 |
}, |
Lo primero que hacemos es confirmar que los parámetros pasados a la función eran del tipo correcto; si no lo son llamaremos a una función de registro, que toma un número de gravedad y un mensaje (veremos que la función de registro es de unos minutos).
A continuación, creamos una copia del módulo en cuestión solo para asegurarnos de que tiene las funciones init y destroy; si no es así, volvemos a registrar un error. Sin embargo, si todos desprotege, agregaremos un objeto a moduleData; estamos almacenando la función creadora y un punto vacío para la instancia cuando iniciamos el módulo. A continuación, eliminaremos la copia temporal del módulo.
1 |
start : function (moduleID) {
|
2 |
var mod = moduleData[moduleID]; |
3 |
if (mod) {
|
4 |
mod.instance = mod.create(Sandbox.create(this, moduleID)); |
5 |
mod.instance.init(); |
6 |
} |
7 |
}, |
8 |
start_all : function () {
|
9 |
var moduleID; |
10 |
for (moduleID in moduleData) {
|
11 |
if (moduleData.hasOwnProperty(moduleID)) {
|
12 |
this.start(moduleID); |
13 |
} |
14 |
} |
15 |
}, |
A continuación, agregaremos la función para iniciar los módulos; como cabría esperar, acepta un nombre de módulo como parámetro único. Si hay un módulo correspondiente en moduleData, ejecutaremos su método create, pasándole una nueva instancia de sandbox. Luego, lo iniciaremos ejecutando su método init.
Podemos crear una función que facilite el inicio de todos los módulos a la vez, ya que probablemente sea algo que queramos hacer. Solo tenemos que recorrer moduleData y enviar cada moduleID al método de inicio. No olvide usar la parte hasOwnProperty; Sé que parece innecesario y feo (al menos para mí), pero está ahí en caso de que alguien haya agregado un elemento al objeto prototipo del objeto.
1 |
stop : function (moduleID) {
|
2 |
var data; |
3 |
if (data = moduleData[moduleId] && data.instance) {
|
4 |
data.instance.destroy(); |
5 |
data.instance = null; |
6 |
} else {
|
7 |
this.log(1, "Stop Module '" + moduleID + "': FAILED : module does not exist or has not been started"); |
8 |
} |
9 |
}, |
10 |
stop_all : function () {
|
11 |
var moduleID; |
12 |
for (moduleID in moduleData) {
|
13 |
if (moduleData.hasOwnProperty(moduleID)) {
|
14 |
this.stop(moduleID); |
15 |
} |
16 |
} |
17 |
}, |
Las dos siguientes funciones deben ser obvias: detener y stop_all. La función stop toma un nombre de módulo; si el sistema conoce un módulo con ese nombre y ese módulo se está ejecutando, llamaremos al método destroy de ese módulo y, a continuación, estableceremos la instancia en null. Si el módulo no existe o no se está ejecutando, registraremos el error.
La función stop_all es exactamente igual que start_all, excepto que las llamadas se detienen en cada módulo.
Lo siguiente: lidiar con los eventos.
1 |
registerEvents : function (evts, mod) {
|
2 |
if (this.is_obj(evts) && mod) {
|
3 |
if (moduleData[mod]) {
|
4 |
moduleData[mod].events = evts; |
5 |
} else {
|
6 |
this.log(1, ""); |
7 |
} |
8 |
} else {
|
9 |
this.log(1, ""); |
10 |
} |
11 |
}, |
12 |
triggerEvent : function (evt) {
|
13 |
var mod; |
14 |
for (mod in moduleData) {
|
15 |
if (moduleData.hasOwnProperty(mod)){
|
16 |
mod = moduleData[mod]; |
17 |
if (mod.events && mod.events[evt.type]) {
|
18 |
mod.events[evt.type](evt.data); |
19 |
} |
20 |
} |
21 |
} |
22 |
}, |
23 |
removeEvents : function (evts, mod) {
|
24 |
var i = 0, evt; |
25 |
if (this.is_arr(evts) && mod && (mod = moduleData[mod]) && mod.events) {
|
26 |
for ( ; evt = evts[i++] ; ) {
|
27 |
delete mod.events[evt]; |
28 |
} |
29 |
} |
30 |
}, |
Como sabemos, registerEvents toma un objeto de eventos y el módulo que los está registrando. Una vez más, hacemos algunas comprobaciones de errores (he dejado los errores en blanco en este caso, solo para simplificar el ejemplo). Si evts es un objeto y sabemos de qué módulo estamos hablando, simplemente meteremos el objeto en el casillero del módulo en moduleData.
Cuando se trata de desencadenar eventos, se nos da un objeto con un tipo y datos. Recorreremos cada módulo en moduleData una vez más: si el módulo tiene una propiedad de evento y ese objeto de evento tiene una clave correspondiente al evento que estamos ejecutando, llamaremos a la función almacenada para ese evento y le pasaremos los datos del evento.
Eliminar eventos es aún más sencillo; obtenemos el objeto events y (después de la comprobación de errores habitual) lo recorremos en bucle y eliminamos los eventos de la matriz del objeto de evento del módulo. (Nota: Creo que tengo esto un poco mezclado en el screencast, pero esta es la versión correcta.)
1 |
log : function (severity, message) {
|
2 |
if (debug) {
|
3 |
console[ (severity === 1) ? 'log' : (severity === 2) ? 'warn' : 'error'](message); |
4 |
} else {
|
5 |
// send to the server |
6 |
} |
7 |
}, |
Aquí está esa función de registro que nos ha estado obsesionando desde hace un tiempo; básicamente, si estamos en modo de depuración, registraremos los errores en la consola; de lo contrario, los enviaremos al servidor. Oh, las cosas ternarias de lujo? Eso solo usa el argumento severity para decidir cuál de las funciones de Firebug usar para registrar el error: 1 === console.log, 2 === console.warn, >2 === console.error.
Ahora estamos listos para ver la parte del núcleo que le da al sandbox la funcionalidad base; para la mayor parte de esto, los he subclasado en un objeto dom, porque soy obsesivo compulsivo de esa manera.
1 |
dom : {
|
2 |
query : function (selector, context) {
|
3 |
var ret = {}, that = this, jqEls, i = 0;
|
4 |
|
5 |
if (context && context.find) {
|
6 |
jqEls = context.find(selector); |
7 |
} else {
|
8 |
jqEls = jQuery(selector); |
9 |
} |
10 |
|
11 |
ret = jqEls.get(); |
12 |
ret.length = jqEls.length; |
13 |
ret.query = function (sel) {
|
14 |
return that.query(sel, jqEls); |
15 |
} |
16 |
return ret; |
17 |
}, |
Aquí está nuestra primera función, consulta. Toma un selector y un contexto. Ahora recuerde, este es el núcleo, donde podemos hacer referencia directa a la base (que es jQuery). En este caso, el contexto debe ser un objeto jQuery. Si el contexto tiene un método find, estableceremos jqEls en el resultado de context.find(selector); si está familiarizado con jQuery, sabrá que esto solo obtendrá el elemento que son elementos secundarios del contexto; así es como obtenemos la funcionalidad de sandbox.query! Luego, estableceremos nuestro objeto de retorno en el resultado de llamar al método get de jQuery; esto devuelve un objeto de elementos dom sin formato. Luego, le damos a ret la propiedad length, para que se pueda recorrer fácilmente. Finalmente, le damos una función de consulta: esta función toma solo un parámetro, un selector, y llama a core.dom.query, pasando ese selector y jqEls como parámetros. ¡Eso es todo!
1 |
bind : function (element, evt, fn) {
|
2 |
if (element && evt) {
|
3 |
if (typeof evt === 'function') {
|
4 |
fn = evt; |
5 |
evt = 'click'; |
6 |
} |
7 |
jQuery(element).bind(evt, fn); |
8 |
} else {
|
9 |
// log wrong arguments |
10 |
} |
11 |
}, |
12 |
unbind : function (element, evt, fn) {
|
13 |
if (element && evt) {
|
14 |
if (typeof evt === 'function') {
|
15 |
fn = evt; |
16 |
evt = 'click'; |
17 |
} |
18 |
jQuery(element).unbind(evt, fn); |
19 |
} else {
|
20 |
// log wrong arguments |
21 |
} |
22 |
}, |
En las funciones de enlace y desenlace de eventos DOM, he decidido proporcionar una ventaja para el usuario. Los usuarios deben pasar en la concesión dos funciones, pero si el parámetro evt es una función, asumiremos que el usuario ha dejado el tipo de evento que desea controlar. En ese caso, asumiremos un clic, ya que es el más común. Luego, solo usamos la función bind de jQuery para conectarla. Debo tener en cuenta que debido a que nuestra función de consulta devuelve (mínimamente) conjuntos DOM envueltos, similares a los de jQuery, esta función puede pasar nuestro conjunto al objeto Jquery y no tiene problemas con él.
Nuestra función de desenlazar es exactamente la misma, excepto, por supuesto, que desenlaza los eventos.
1 |
create: function (el) {
|
2 |
return document.createElement(el); |
3 |
}, |
4 |
apply_attrs: function (el, attrs) {
|
5 |
jQuery(el).attr(attrs); |
6 |
} |
7 |
}, // end of dom object |
8 |
is_arr : function (arr) {
|
9 |
return jQuery.isArray(arr); |
10 |
}, |
11 |
is_obj : function (obj) {
|
12 |
return jQuery.isPlainObject(obj); |
13 |
} |
He agrupado las últimas cuatro funciones porque todas son bastante simples; dom.create simplemente devuelve un nuevo elemento DOM; dom.apply_attrs utiliza el método attr de jQuery para proporcionar los atributos del elemento. Finalmente, tenemos las dos funciones auxiliares que hemos estado usando para verificar nuestros parámetros.
Lo creas o no, ese es todo el núcleo; y nuestra base es jQuery, así que estamos listos para ensamblar esto.
Poniéndolo todo junto
Ahora estamos listos para construir algo de HTML para que podamos ver nuestro JavaScript en acción. No te guiaré a través de esto en detalle, porque ese no es el punto del tutorial.
1 |
<!DOCTYPE HTML>
|
2 |
<html lang="en"> |
3 |
<head>
|
4 |
<meta charset="UTF-8"> |
5 |
<title>Online Store</title> |
6 |
<link rel="stylesheet" href="default.css" /> |
7 |
</head>
|
8 |
<body>
|
9 |
<div id="main"> |
10 |
<div id="search-box"> |
11 |
<input id="search_input" type="text" name='q' /> |
12 |
<button id="search_button">Search</button> |
13 |
<button id="quit_search">Reset</button> |
14 |
</div>
|
15 |
|
16 |
<div id="filters-bar"> |
17 |
<ul>
|
18 |
<li><a href="#red">Red</a></li> |
19 |
<li><a href="#blue">Blue</a></li> |
20 |
<li><a href="#mobile">Mobile</a></li> |
21 |
<li><a href="#accessory">Accessory</a></li> |
22 |
</ul>
|
23 |
</div>
|
24 |
|
25 |
<div id="product-panel"> |
26 |
<ul>
|
27 |
<li id="1" data-8088-keyword="red"><img src="img/1.jpg"><p>First Item</p></li> |
28 |
<li id="2" data-8088-keyword="blue"><img src="img/2.jpg"><p>Second Item</p></li> |
29 |
<li id="3" data-8088-keyword="mobile"><img src="img/3.jpg"><p>Third Item</p></li> |
30 |
<li id="4" data-8088-keyword="accessory"><img src="img/4.jpg"><p>Fourth Item</p></li> |
31 |
<li id="5" data-8088-keyword="red mobile"><img src="img/5.jpg"><p>Fifth Item</p></li> |
32 |
<li id="6" data-8088-keyword="blue mobile"><img src="img/6.jpg"><p>Sixth Item</p></li> |
33 |
<li id="7" data-8088-keyword="red accessory"><img src="img/7.jpg"><p>Seventh Item </p></li> |
34 |
<li id="8" data-8088-keyword="blue accessory"><img src="img/8.jpg"><p>Eighth Item</p></li> |
35 |
<li id="9" data-8088-keyword="red blue"><img src="img/9.jpg"><p>Ninth Item</p></li> |
36 |
<li id="10" data-8088-keyword="mobile accessory"><img src="img/10.jpg"><p>Tenth Item</p></li> |
37 |
</ul>
|
38 |
|
39 |
</div>
|
40 |
|
41 |
<div id="shopping-cart"> |
42 |
<ul>
|
43 |
</ul>
|
44 |
</div>
|
45 |
</div>
|
46 |
<script src="js/jquery.js"></script> |
47 |
<script src="js/core-jquery.js"></script> |
48 |
<script src="js/sandbox.js"></script> |
49 |
<script src="js/modules.js"></script> |
50 |
</body>
|
51 |
</html>
|
No es nada lujoso; lo importante a tener en cuenta es que cada uno de los divs principales tienen identificadores que corresponden a los módulos de JavaScript. Y no te olvides de los atributos data-* de HTML5 que nos dan las categorías a las que filtrar.
Por supuesto, tendremos que darle estilo:
1 |
|
2 |
body { |
3 |
background:#ececec; |
4 |
font:13px/1.5 helvetica, arial, san-serif; |
5 |
}
|
6 |
#main { |
7 |
width:950px; |
8 |
margin:auto; |
9 |
overflow:hidden; |
10 |
}
|
11 |
#search-box, #filters-bar { |
12 |
margin-left:10px; |
13 |
}
|
14 |
#filters-bar ul { |
15 |
list-style-type:none; |
16 |
margin:10px 0; |
17 |
padding:0; |
18 |
border-top:2px solid #474747; |
19 |
border-bottom:2px solid #474747; |
20 |
}
|
21 |
#filters-bar li { |
22 |
display:inline-block; |
23 |
padding:5px 10px 5px 0; |
24 |
}
|
25 |
#filters-bar li a { |
26 |
text-decoration:none; |
27 |
font-weight:bold; |
28 |
color:#474747; |
29 |
}
|
30 |
#product-panel { |
31 |
float:left; |
32 |
width: 588px; |
33 |
}
|
34 |
#product-panel ul { |
35 |
margin:0; |
36 |
padding:0; |
37 |
}
|
38 |
#product-panel li { |
39 |
list-style-type:none; |
40 |
display:inline-block; |
41 |
text-align:center; |
42 |
background:#474747; |
43 |
border:1px solid #eee; |
44 |
padding:15px; |
45 |
margin:10px; |
46 |
}
|
47 |
#product-panel li p { |
48 |
margin:10px 0 0 0; |
49 |
}
|
50 |
|
51 |
#shopping-cart { |
52 |
float:left; |
53 |
background:#ccc; |
54 |
height:300px; |
55 |
width:300px; |
56 |
padding:30px; |
57 |
border:1px solid #474747; |
58 |
}
|
59 |
|
60 |
#shopping-cart ul { |
61 |
list-style-type:none; |
62 |
padding:0; |
63 |
}
|
64 |
|
65 |
#shopping-cart li { |
66 |
padding:3px; |
67 |
margin:2px 0; |
68 |
background:#ececec; |
69 |
border: 1px solid #333; |
70 |
}
|
71 |
|
72 |
#shopping-cart .product_name { |
73 |
display:inline-block; |
74 |
width:230px; |
75 |
}
|
76 |
|
77 |
#shopping-cart .price { |
78 |
display: inline-block; |
79 |
float:right; |
80 |
}
|
Es difícil mostrarlo en acción, (¡para eso es el screencast!), pero aquí hay algunas tomas:









Bueno, eso sería todo, pero hagamos una cosa más: creemos un núcleo que funcione en Dojo, para mostrar cómo el uso de un núcleo como lo hicimos hace que sea fácil cambiar de base.
¿Por qué Dojo? Para ser completamente honesto, le di una oportunidad a Mootools y YUI, pero había algunos desafíos que iban a tomar más tiempo que el tiempo que tenía para resolverlo. Ciertamente no creo que sea imposible usarlos; si construyes un núcleo con Mootools, YUI o algún otro marco de JavaScript, me encantaría verlo. Por ahora, vamos a construir uno con Dojo.
Por supuesto, no tenemos que cambiar ninguna de las funciones de manejo de módulos; en nuestro caso, todo lo que necesitamos cambiar es la sección dom, is_arr y is_obj.
1 |
query : function (selector, context) {
|
2 |
var ret = {}, that = this, len, i =0, djEls;
|
3 |
|
4 |
djEls = dojo.query( ((context) ? context + " " : "") + selector); |
5 |
|
6 |
len = djEls.length; |
7 |
|
8 |
while ( i < len) {
|
9 |
ret[i] = djEls[i++]; |
10 |
} |
11 |
ret.length = len; |
12 |
ret.query = function (sel) {
|
13 |
return that.query(sel, selector); |
14 |
} |
15 |
return ret; |
16 |
}, |
Aquí está la función de consulta, reescrita para trabajar con Dojo. Como puede ver, hace exactamente lo que hace la versión de jQuery; la principal diferencia es que el contexto es una cadena que se anexa al frente del selector.
Ahora para las funciones de eventos DOM:
1 |
eventStore : {},
|
2 |
bind : function (element, evt, fn) {
|
3 |
if (element && evt) {
|
4 |
if (typeof evt === 'function') {
|
5 |
fn = evt; |
6 |
evt = 'click'; |
7 |
} |
8 |
if (element.length) {
|
9 |
var i = 0, len = element.length; |
10 |
for ( ; i < len ; ) {
|
11 |
this.eventStore[element[i] + evt + fn] = dojo.connect(element[i], evt, element[i], fn); |
12 |
i++; |
13 |
} |
14 |
} else {
|
15 |
this.eventStore[element + evt + fn] = dojo.connect(element, evt, element, fn); |
16 |
} |
17 |
} |
18 |
}, |
19 |
unbind : function (element, evt, fn) {
|
20 |
if (element && evt) {
|
21 |
if (typeof evt === 'function') {
|
22 |
fn = evt; |
23 |
evt = 'click'; |
24 |
} |
25 |
if (element.length) {
|
26 |
var i = 0, len = element.length; |
27 |
for ( ; i < len ; ) {
|
28 |
dojo.disconnect(this.eventStore[element[i] + evt + fn]); |
29 |
delete this.eventStore[element[i] + evt + fn]; |
30 |
i++; |
31 |
} |
32 |
} else {
|
33 |
dojo.disconnect(this.eventStore[element + evt + fn]); |
34 |
delete this.eventStore[element + evt + fn]; |
35 |
} |
36 |
} |
37 |
}, |
Notará que hemos agregado un objeto eventStore; esto se debe a que Dojo enlaza eventos con dojo.connect y se desenlaza con dojo.disconnect; la trampa aquí es que dojo.disconnect toma el objeto devuelto por dojo.connect como su único parámetro. Usamos eventStore para realizar un seguimiento de esos valores para que podamos desconectar fácilmente los eventos. El resto de la complejidad aquí es solo un bucle sobre nuestros conjuntos DOM envueltos, porque Dojo no puede manejarlos solos.
1 |
create: function (el) {
|
2 |
return document.createElement(el); |
3 |
}, |
4 |
apply_attrs: function (el, attrs) {
|
5 |
var attr; |
6 |
for (attr in attrs) {
|
7 |
dojo.attr(el, attr, attrs[attr]); |
8 |
} |
9 |
} |
10 |
}, // end of dom object |
11 |
is_arr : function (arr) {
|
12 |
return dojo.isArray(arr); |
13 |
}, |
14 |
is_obj : function (obj) {
|
15 |
return dojo.isObject(obj); |
16 |
} |
Esta parte no debería ser demasiado difícil de entender; todo es bastante similar al de jQuery.
Ahora, deberías poder incluir Dojo como tu base y nuestro nuevo núcleo de Dojo como tu núcleo y nuestra aplicación seguirá funcionando como antes.
Conclusión: ¿Debería codificar de esta manera?
Bueno, ahora que hemos visto exactamente cómo debería funcionar un sistema como este, hablemos de si desea escribir sus aplicaciones JavaScript de esta manera. Obviamente, este no es un patrón que usaría en su sitio web promedio; es para aplicaciones completas. Esto es lo que se me ocurrió:
Contras:
- Puede ser bastante difícil aprender este tipo de codificación. Pensar modularmente definitivamente requiere un cambio de paradigma.
- Cuando estás empezando, es difícil saber cuánta potencia dar a las capas. ¿Qué deberían poder hacer los módulos? ¿Debería haber alguna funcionalidad real en el espacio aislado, o simplemente está ahí para exponer la cantidad correcta de funcionalidad principal a los módulos? ¿Dónde hacemos la comprobación de errores? ¿Podemos realizar simplemente tareas, como crear elementos dom, dentro de los módulos? Estoy seguro de que tienes tus propias perspectivas sobre allí y más preguntas, así que déjame saber lo que piensas, ya sea en los comentarios en la publicación de Nettuts + o a través del formulario en mi sitio web. O, aún mejor: escribe sobre ello en tu blog / sitio web para que el mundo lo vea, y asegúrate de enviarme un enlace.
- Finalmente, es extraño usar un marco de JavaScript como base; con mi comprensión actual de esto, realmente no se prestan a hacer esto.
Pros
- Una vez que tenga un núcleo sólido y un espacio aislado, la creación de nuevas aplicaciones web será mucho más rápida; básicamente es elegir módulos de su biblioteca de módulos en constante crecimiento y conectarlos juntos.
- Es mucho más fácil probar el código, porque es todo muy modular.
Bueno, eso es todo lo que tengo para ti hoy; Espero haber estirado un poco tu mente; Sé que aprender sobre todo esto en la última semana más o menos ha estirado la mía! Me encantaría saber lo que piensas sobre todo esto, así que por favor hágamelo saber si tiene comentarios! Gracias por leer!



