2019/09/29

Vue Simple App: Sin webpack

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.
<!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>
view raw html5.html hosted with ❤ by GitHub

HTML5: https://codepen.io/akobashikawa/pen/NWKVazJ

HTML5 + Vue

Importar vue y desarrollar un script en la misma página.
<!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>
view raw html5-vue.html hosted with ❤ by GitHub

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.
<!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: https://codepen.io/akobashikawa/pen/qBWGVEp

HTML5 + Vue + Router + Vuex

Importar Vuex para compartir un estado general entre los componentes.
<!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: https://codepen.io/akobashikawa/pen/pozmdby

HTML5 + Vue + Router + Vuex + Vuetify

Para darle un look de app con Material Design.

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

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;
view raw About.js hosted with ❤ by GitHub
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');
view raw app.js hosted with ❤ by GitHub
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;
view raw Counter.js hosted with ❤ by GitHub
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;
view raw Home.js hosted with ❤ by GitHub
<!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>
view raw index.html hosted with ❤ by GitHub
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;
view raw router.js hosted with ❤ by GitHub
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;
view raw store.js hosted with ❤ by GitHub
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?

No hay comentarios.:

Publicar un comentario