Vue, Router, Vuex y Vuetify, pero sin webpack
[Actualizado el 2019/11/05: "Carga dinámica"]
Tal vez no tenga ninguna razón técnica importante para preferir prescindir de webpack al desarrollar una aplicación web.
Tal vez sea solamente nostalgia por los viejos tiempos, donde simplemente importabas librerías en la página y la magia podía aparecer.
Tal vez no sea mala idea tener esa capa extra que cargar en el viaje, y aceptarla a cambio de los caramelos que nos regala.
Pero a mi me molesta un poco.
Preferiría no tener que usarla si puedo evitarlo.
Siento que hay que tener cuidado cuando algo aumenta la complejidad. Mayor velocidad o comodidad pueden ser justificaciones. Pero hay que estar atentos a no limitar la diversidad de opciones.
Una de las cosas que me gusta de Vue es que puedo empezar muy simple, importando la librería en la página. Y luego, si el problema lo amerita, ir escalando la solución hacia algo más y más complejo. Es como empezar descalzo si quiero, o con sandalias, e ir cambiando según lo amerite el terreno.
La mayoría de tutoriales parece estar de acuerdo en que usar vue create es la opción más práctica. Sin embargo, algo que descubrí es que también se puede llegar a elaborar soluciones relativamente complejas sin usar un transpilador.
Esta es una guía en un proceso de desarrollo simple que nos permita usar Vue, Router, Vuex y Vuetify, sin necesitar la complejidad extra de webpack.
HTML5
Un lugar simple donde empezar.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<meta http-equiv="X-UA-Compatible" content="ie=edge"> | |
<title>HTML5</title> | |
</head> | |
<body> | |
<h1>HTML5</h1> | |
</body> | |
</html> |
HTML5: https://codepen.io/akobashikawa/pen/NWKVazJ
HTML5 + Vue
Importar vue y desarrollar un script en la misma página.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<meta http-equiv="X-UA-Compatible" content="ie=edge"> | |
<title>HTML5 + Vue</title> | |
</head> | |
<body> | |
<div id="app"> | |
<h1>HTML5 + Vue</h1> | |
{{ hello }} | |
</div> | |
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> | |
<script> | |
var app = new Vue({ | |
el: '#app', | |
data: { | |
hello: 'Hello Vue!' | |
} | |
}); | |
</script> | |
</body> | |
</html> |
HTML5 + Vue: https://codepen.io/akobashikawa/pen/aborLgW
HTML5 + Vue + Router
Importar Vue Router para organizar la navegación.Eso también nos empuja a organizar el contenido en componentes, si es que no lo hemos hecho ya.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<meta http-equiv="X-UA-Compatible" content="ie=edge"> | |
<title>HTML5 + Vue + Router</title> | |
</head> | |
<body> | |
<div id="app"> | |
<h1>HTML5 + Vue + Router</h1> | |
<p>{{ hello }}</p> | |
<ul> | |
<li><router-link to="/">Home</router-link></li> | |
<li><router-link to="/about">About</router-link></li> | |
</ul> | |
<div class="container"> | |
<router-view></router-view> | |
</div> | |
</div> | |
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> | |
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> | |
<script> | |
var Home = Vue.component('home', { | |
data() { | |
return { | |
hello: 'Hello Home!' | |
}; | |
}, | |
template: `<div> | |
<h2>Home</h2> | |
<p>{{ hello }}</p> | |
</div>` | |
}); | |
var About = Vue.component('about', { | |
data() { | |
return { | |
hello: 'Hello About!' | |
}; | |
}, | |
template: `<div> | |
<h2>About</h2> | |
<p>{{ hello }}</p> | |
</div>` | |
}); | |
var router = new VueRouter({ | |
// mode: 'history', | |
routes: [ | |
{ | |
path: '/', | |
name: 'home', | |
component: Home | |
}, | |
{ | |
path: '/about', | |
name: 'about', | |
component: About | |
} | |
] | |
}); | |
var app = new Vue({ | |
router, | |
data() { | |
return { | |
hello: 'Hello Vue!' | |
}; | |
} | |
}).$mount('#app'); | |
</script> | |
</body> | |
</html> |
HTML5 + Vue + Router + Vuex
Importar Vuex para compartir un estado general entre los componentes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<meta http-equiv="X-UA-Compatible" content="ie=edge"> | |
<title>HTML5 + Vue + Router + Vuex</title> | |
</head> | |
<body> | |
<div id="app"> | |
<h1>HTML5 + Vue + Router + Vuex</h1> | |
<p>{{ hello }}</p> | |
<p>Counter: {{ counter }}</p> | |
<ul> | |
<li><router-link to="/">Home</router-link></li> | |
<li><router-link to="/about">About</router-link></li> | |
</ul> | |
<div class="container"> | |
<router-view></router-view> | |
</div> | |
</div> | |
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> | |
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> | |
<script src="https://unpkg.com/vuex"></script> | |
<script> | |
var Home = Vue.component('home', { | |
data() { | |
return { | |
hello: 'Hello Home!', | |
}; | |
}, | |
template: `<div> | |
<h2>Home</h2> | |
<p>{{ hello }}</p> | |
<counter></counter> | |
</div>` | |
}); | |
var About = Vue.component('about', { | |
data() { | |
return { | |
hello: 'Hello About!' | |
}; | |
}, | |
template: `<div> | |
<h2>About</h2> | |
<p>{{ hello }}</p> | |
<counter></counter> | |
</div>` | |
}); | |
var Counter = Vue.component('counter', { | |
computed: { | |
...Vuex.mapState(['counter']) | |
}, | |
methods: { | |
incCounter() { | |
this.$store.commit('incCounter', 1); | |
}, | |
decCounter() { | |
this.$store.commit('decCounter', 1); | |
} | |
}, | |
template: `<div> | |
Counter: {{ counter }} | |
<button @click="incCounter">+</button> | |
<button @click="decCounter">-</button> | |
</div>` | |
}); | |
var router = new VueRouter({ | |
// mode: 'history', | |
routes: [ | |
{ | |
path: '/', | |
name: 'home', | |
component: Home | |
}, | |
{ | |
path: '/about', | |
name: 'about', | |
component: About | |
} | |
] | |
}); | |
var store = new Vuex.Store({ | |
state: { | |
counter: 0 | |
}, | |
mutations: { | |
incCounter(state, delta) { | |
state.counter += delta; | |
}, | |
decCounter(state, delta) { | |
state.counter -= delta; | |
} | |
}, | |
actions: { | |
} | |
}); | |
var app = new Vue({ | |
router, | |
store, | |
data() { | |
return { | |
hello: 'Hello Vue!' | |
}; | |
}, | |
computed: { | |
...Vuex.mapState(['counter']) | |
} | |
}).$mount('#app'); | |
</script> | |
</body> | |
</html> |
HTML5 + Vue + Router + Vuex + Vuetify
Para darle un look de app con Material Design.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet"> | |
<link href="https://fonts.googleapis.com/css?family=Material+Icons" rel="stylesheet"> | |
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" rel="stylesheet"> | |
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet"> | |
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"> | |
<title>HTML5 + Vue + Router + Vuex + Vuetify</title> | |
<style> | |
[v-cloak]>* { | |
display: none | |
} | |
[v-cloak]::before { | |
content: "loading…" | |
} | |
</style> | |
</head> | |
<body> | |
<div id="app" v-cloak> | |
<v-app> | |
<v-navigation-drawer app v-model="drawer"> | |
<v-list-item> | |
<v-list-item-content> | |
<v-list-item-title class="title"> | |
Vue Simple App | |
</v-list-item-title> | |
<v-list-item-subtitle> | |
Menu | |
</v-list-item-subtitle> | |
</v-list-item-content> | |
</v-list-item> | |
<v-divider></v-divider> | |
<v-list dense nav> | |
<v-list-item router to="/"> | |
<v-list-item-icon> | |
<v-icon>home</v-icon> | |
</v-list-item-icon> | |
<v-list-item-content> | |
<v-list-item-title>Home</v-list-item-title> | |
</v-list-item-content> | |
</v-list-item> | |
<v-list-item router to="/about"> | |
<v-list-item-icon> | |
<v-icon>info</v-icon> | |
</v-list-item-icon> | |
<v-list-item-content> | |
<v-list-item-title>About</v-list-item-title> | |
</v-list-item-content> | |
</v-list-item> | |
</v-list> | |
</v-navigation-drawer> | |
<v-app-bar app color="light-blue darken-1" dark> | |
<v-app-bar-nav-icon @click.stop="drawer=!drawer"></v-app-bar-nav-icon> | |
<v-toolbar-title>HTML5 + Vue + Router + Vuex + Vuetify</v-toolbar-title> | |
</v-app-bar> | |
<v-content> | |
<v-card> | |
<v-card-text> | |
<p>{{ hello }}</p> | |
<v-badge> | |
<template v-slot:badge>{{ counter }}</template> | |
<v-icon>alarm_add</v-icon> | |
</v-badge> | |
</v-card-text> | |
</v-card> | |
<v-container fluid> | |
<router-view></router-view> | |
</v-container> | |
</v-content> | |
</v-app> | |
</div> | |
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> | |
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> | |
<script src="https://unpkg.com/vuex"></script> | |
<script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script> | |
<script> | |
var Home = Vue.component('home', { | |
data() { | |
return { | |
hello: 'Hello Home!', | |
}; | |
}, | |
template: `<div> | |
<v-card> | |
<v-card-title>Home</v-card-title> | |
<v-card-text> | |
<p>{{ hello }}</p> | |
<counter></counter> | |
</v-card-text> | |
</v-card> | |
</div>` | |
}); | |
var About = Vue.component('about', { | |
data() { | |
return { | |
hello: 'Hello About!' | |
}; | |
}, | |
template: `<div> | |
<v-card> | |
<v-card-title>About</v-card-title> | |
<v-card-text> | |
<p>{{ hello }}</p> | |
<counter></counter> | |
</v-card-text> | |
</v-card> | |
</div>` | |
}); | |
var Counter = Vue.component('counter', { | |
computed: { | |
...Vuex.mapState(['counter']) | |
}, | |
methods: { | |
incCounter() { | |
this.$store.commit('incCounter', 1); | |
}, | |
decCounter() { | |
this.$store.commit('decCounter', 1); | |
} | |
}, | |
template: `<div> | |
<v-card outlined> | |
<v-card-title>Counter</v-card-title> | |
<v-card-text> | |
<p>{{ counter }}</p> | |
</v-card-text> | |
<v-card-actions> | |
<v-btn text icon color="cyan" @click="incCounter"> | |
<v-icon>add_circle</v-icon> | |
</v-btn> | |
<v-btn text icon color="red" @click="decCounter"> | |
<v-icon>remove_circle</v-icon> | |
</v-btn> | |
</v-card-actions> | |
</v-card> | |
</div>` | |
}); | |
var router = new VueRouter({ | |
// mode: 'history', | |
routes: [ | |
{ | |
path: '/', | |
name: 'home', | |
component: Home | |
}, | |
{ | |
path: '/about', | |
name: 'about', | |
component: About | |
} | |
] | |
}); | |
var store = new Vuex.Store({ | |
state: { | |
counter: 0 | |
}, | |
mutations: { | |
incCounter(state, delta) { | |
state.counter += delta; | |
}, | |
decCounter(state, delta) { | |
state.counter -= delta; | |
} | |
}, | |
actions: { | |
} | |
}); | |
var app = new Vue({ | |
router, | |
store, | |
vuetify: new Vuetify(), | |
data() { | |
return { | |
hello: 'Hello Vue!', | |
drawer: false, | |
}; | |
}, | |
computed: { | |
...Vuex.mapState(['counter']) | |
} | |
}).$mount('#app'); | |
</script> | |
</body> | |
</html> |
HTML5 + Vue + Router + Vuex + Vuetify: https://codepen.io/akobashikawa/pen/bGbyYod
Módulos
Cuando más componentes van apareciendo y el largo del archivo va haciendo complicada la edición, parece una señal para sacar el código javascript a otro archivo.
Javascript soporta import. Pero hay que usarla dentro de un módulo.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const About = Vue.component('about', { | |
data() { | |
return { | |
hello: 'Hello About!' | |
}; | |
}, | |
template: `<div> | |
<v-card> | |
<v-card-title>About</v-card-title> | |
<v-card-text> | |
<p>{{ hello }}</p> | |
<counter></counter> | |
</v-card-text> | |
</v-card> | |
</div>` | |
}); | |
export default About; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import router from './router.js'; | |
import store from './store.js'; | |
// common | |
import Counter from './Counter.js'; | |
var app = new Vue({ | |
router, | |
store, | |
vuetify: new Vuetify(), | |
data() { | |
return { | |
hello: 'Hello Vue!', | |
drawer: false, | |
}; | |
}, | |
computed: { | |
...Vuex.mapState(['counter']) | |
} | |
}).$mount('#app'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const Counter = Vue.component('counter', { | |
computed: { | |
...Vuex.mapState(['counter']) | |
}, | |
methods: { | |
incCounter() { | |
this.$store.commit('incCounter', 1); | |
}, | |
decCounter() { | |
this.$store.commit('decCounter', 1); | |
} | |
}, | |
template: `<div> | |
<v-card outlined> | |
<v-card-title>Counter</v-card-title> | |
<v-card-text> | |
<p>{{ counter }}</p> | |
</v-card-text> | |
<v-card-actions> | |
<v-btn text icon color="cyan" @click="incCounter"> | |
<v-icon>add_circle</v-icon> | |
</v-btn> | |
<v-btn text icon color="red" @click="decCounter"> | |
<v-icon>remove_circle</v-icon> | |
</v-btn> | |
</v-card-actions> | |
</v-card> | |
</div>` | |
}); | |
export default Counter; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const Home = Vue.component('home', { | |
data() { | |
return { | |
hello: 'Hello Home!', | |
}; | |
}, | |
template: `<div> | |
<v-card> | |
<v-card-title>Home</v-card-title> | |
<v-card-text> | |
<p>{{ hello }}</p> | |
<counter></counter> | |
</v-card-text> | |
</v-card> | |
</div>` | |
}); | |
export default Home; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet"> | |
<link href="https://fonts.googleapis.com/css?family=Material+Icons" rel="stylesheet"> | |
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" rel="stylesheet"> | |
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet"> | |
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"> | |
<title>HTML5 + Vue + Router + Vuex + Vuetify</title> | |
<style> | |
[v-cloak]>* { | |
display: none | |
} | |
[v-cloak]::before { | |
content: "loading…" | |
} | |
</style> | |
</head> | |
<body> | |
<div id="app" v-cloak> | |
<v-app> | |
<v-navigation-drawer app v-model="drawer"> | |
<v-list-item> | |
<v-list-item-content> | |
<v-list-item-title class="title"> | |
Vue Simple App | |
</v-list-item-title> | |
<v-list-item-subtitle> | |
Menu | |
</v-list-item-subtitle> | |
</v-list-item-content> | |
</v-list-item> | |
<v-divider></v-divider> | |
<v-list dense nav> | |
<v-list-item router to="/"> | |
<v-list-item-icon> | |
<v-icon>home</v-icon> | |
</v-list-item-icon> | |
<v-list-item-content> | |
<v-list-item-title>Home</v-list-item-title> | |
</v-list-item-content> | |
</v-list-item> | |
<v-list-item router to="/about"> | |
<v-list-item-icon> | |
<v-icon>info</v-icon> | |
</v-list-item-icon> | |
<v-list-item-content> | |
<v-list-item-title>About</v-list-item-title> | |
</v-list-item-content> | |
</v-list-item> | |
</v-list> | |
</v-navigation-drawer> | |
<v-app-bar app color="light-blue darken-1" dark> | |
<v-app-bar-nav-icon @click.stop="drawer=!drawer"></v-app-bar-nav-icon> | |
<v-toolbar-title>HTML5 + Vue + Router + Vuex + Vuetify</v-toolbar-title> | |
</v-app-bar> | |
<v-content> | |
<v-card> | |
<v-card-text> | |
<p>{{ hello }}</p> | |
<v-badge> | |
<template v-slot:badge>{{ counter }}</template> | |
<v-icon>alarm_add</v-icon> | |
</v-badge> | |
</v-card-text> | |
</v-card> | |
<v-container fluid> | |
<router-view></router-view> | |
</v-container> | |
</v-content> | |
</v-app> | |
</div> | |
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> | |
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> | |
<script src="https://unpkg.com/vuex"></script> | |
<script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script> | |
<script src="js/app.js" type="module"></script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Home from './Home.js'; | |
import About from './About.js'; | |
const router = new VueRouter({ | |
// mode: 'history', | |
routes: [ | |
{ | |
path: '/', | |
name: 'home', | |
component: Home | |
}, | |
{ | |
path: '/about', | |
name: 'about', | |
component: About | |
} | |
] | |
}); | |
export default router; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const store = new Vuex.Store({ | |
state: { | |
counter: 0 | |
}, | |
mutations: { | |
incCounter(state, delta) { | |
state.counter += delta; | |
}, | |
decCounter(state, delta) { | |
state.counter -= delta; | |
} | |
}, | |
actions: { | |
} | |
}); | |
export default store; |
Algo que ocurre con los módulos declarados normalmente, es que se carga toda la estructura de dependencias desde el inicio.
Para prevenirlo, y que cada módulo se pueda cargar cuando se necesita y no antes, se puede usar la técnica de declarar el componente como resultado de una función:
Antes:
import Counter from './Counter.js';
Después:const Counter = () => import('./Counter.js');
Conclusión
Es posible desarrollar vue apps de relativa complejidad sin necesidad de usar webpack.
Me parece que este esquema puede simplificar el inicio de un proyecto. Aún cuando la versión para producción pueda requerir webpack para las optimizaciones, se podría usar esto durante el desarrollo.
¿Qué ventajas o desventajas ves hacerlo de este modo?, ¿te parece que puede facilitar la depuración?, ¿crees que en el futuro los navegadores carguen los scripts de un modo que no sea necesario empaquetarlos como actualmente?