Vue, Storybook, TypeScript: comenzar un nuevo proyecto con las mejores prácticas en mente


(publicado originalmente en Medium )


Me gusta escribir código React. Esta podría ser una extraña introducción a una historia sobre Vue, pero debes entender mis antecedentes para entender por qué estoy aquí discutiendo sobre Vue.


Me gusta escribir código React y odio leerlo. JSX es una buena idea para ensamblar las piezas rápidamente, Material-UI es una solución increíble para arrancar la interfaz de usuario de su próxima startup, computar CSS desde constantes JS le permite ser muy flexible. Sin embargo, leer sus viejos JSX se siente horrible, incluso con escrupulosas prácticas de revisión de código, puede rascarse la cabeza no una sola vez mientras intenta imaginar la intrincada anidación de los componentes.


He escuchado muchas cosas sobre Vue, el niño no tan nuevo en la cuadra, y finalmente decidí mojarme los pies; trayendo todo mi equipaje mental de React and Polymer (y Angular, pero no hablemos de eso).


Vue se parece mucho a Polymer, más aún, los autores lo nombran como una de las fuentes de inspiración . La estructura de los archivos *.vue parecía la mejor parte de Polymer y me sumergí directamente. Unos días más tarde salí del pantano del mecanografiado, el desarrollo impulsado por la interfaz de usuario y numerosas prácticas recomendadas y estoy listo para compartir lo que encontré.


Vamos!


Usaremos npx para ejecutar los comandos. Si aún no tiene npx, aquí le mostramos cómo obtenerlo: npm install -g npx . Npx es un salvavidas cuando manejas paquetes npm cli y no quieres npm install -g docenas de aplicaciones. También necesitará Yarn si no lo tiene: npm install -g yarn debería ponerlo al día.


 $ npx vue create not-a-todo-app Vue CLI v3.3.0 ? Please pick a preset: Manually select features ? Check the features needed for your project: Babel, TS, PWA, Router, Vuex, CSS Pre-processors, Linter, Unit, E2E ? Use class-style component syntax? Yes ? Use Babel alongside TypeScript for auto-detected polyfills? Yes ? Use history mode for router? (Requires proper server setup for index fallback in production) Yes ? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS ? Pick a linter / formatter config: TSLint ? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)Lint on save ? Pick a unit testing solution: Jest ? Pick a E2E testing solution: Cypress ? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedicated config files ? Save this as a preset for future projects? No 

Vue Cli tiene un buen mago para guiarte; para este tutorial usaremos el modo manual y habilitaremos todas las funciones . ¿Exceso? Tal vez, pero queremos ver cómo funciona Vue con todo lo que puede proporcionar de inmediato. Aún así, veamos las opciones y la razón sobre cómo y por qué.


Necesitamos tanto Babel como TypeScript habilitado. TS será el idioma principal de elección y Babel admitirá el manejo de código externo que requiere transpilación. Podría argumentar que TypeScript también puede transpilar el código JS y, de hecho, ese es el caso, pero en mis experimentos (especialmente relacionados con las pruebas unitarias y Vuetify) pensé que era mucho mejor mantener TS para *.ts y usar Babel para todo lo demás.


Los preprocesadores de CSS serán útiles para Vuetify; Si bien viene con CSS preminificado, es posible que desee incluir los archivos de estilo originales para trabajar con estilos. Linter / Formatter es un requisito obvio para cualquier proyecto nuevo (debe cumplir con un estilo de código único y puede agradecerme en un año cuando leerá su código anterior). Permitimos las pruebas unitarias y las pruebas E2E, aunque es posible que no desee realizar los casos de prueba e2e completos, es útil saber cómo solucionarlos después de que hayamos terminado con Vuetify.


El soporte de la aplicación web progresiva (PWA) , el enrutador y Vuex no son estrictamente necesarios para este tutorial y no los utilizaremos, pero habilitarlos simplificará su vida en un proyecto real.


Usar sintaxis de componentes de estilo de clase? Si Las clases hacen que el código sea un poco más voluminoso pero más legible y más fácil de razonar; También hacen su vida de TypeScript más fácil.


¿Usar Babel junto con TypeScript para polyfills autodetectados? Si Queremos tanto a Babel como a TS para el caso que analizaremos más adelante.


¿Usar el modo de historial para el enrutador? Sí (pero YMMV). No escribiremos ningún back-end para servir esto en producción, pero generalmente es una buena idea usar API de historial.


Elija un preprocesador CSS (los módulos PostCSS, Autoprefixer y CSS son compatibles de forma predeterminada): solo utilizaremos módulos CSS en este tutorial, por lo que puede elegir sass / less / stylus según sus preferencias.


Elija una configuración de linter / formateador: TSlint es una opción obvia ya que queremos usar TypeScript tanto como sea posible.


Elija características adicionales de pelusa: habilite ambas ( a ). Linting es bueno.


Elija una solución de prueba de unidad: este tutorial se centra en Jest, por lo que debe seleccionarlo.


Elija una solución de prueba E2E: este tutorial se centra en Cypress.


¿Dónde prefiere colocar la configuración para Babel, PostCSS, ESLint, etc.? ¿No crees que es un poco extraño que todos intenten meter aún más cosas en package.json ? El archivo apenas se puede leer como está ahora. Use archivos de configuración dedicados: es mucho más fácil trabajar con ellos y su historial de git será más bonito.


Es hora de verificar la configuración, ejecutar yarn serve :


No debe haber errores en la consola y navegar a http: // localhost: 8080 / lo recibirá con:


Las pruebas unitarias de verificación funcionan, ejecutan la yarn test:unit :


Y trabajo de prueba e2e ( yarn test:e2e --headless ):


Genial Pasando a la interfaz de usuario.


El dilema material.


Hay algunas bibliotecas de UI de material para Vue en un nivel diferente de flexibilidad y pulido. Seguramente hay docenas de otras bibliotecas de componentes, por lo que puede usar Bootstrap Vue si lo desea. Este tutorial se enfoca en Vuetify por varias razones:


  • es la biblioteca de material más destacada en GitHub;
  • fue un verdadero dolor hacer que funcione, por lo que es una gran demostración de todos los casos extremos en los que puede tropezar.

Convencido? Continúe con la instalación y luego: vue add vuetify . Seleccione la opción Configurar (avanzado) .


¿Usar una plantilla prefabricada? Vuetify anulará App.vue y HelloWorld.vue predeterminados. Responda sí a esto, ya que es un proyecto nuevo.


¿Usar tema personalizado? Si Necesitará uno tarde o temprano de todos modos, así que configurémoslo. Por las mismas razones, ¿responde sí a Usar propiedades personalizadas (variables CSS)? .


Seleccione la fuente del icono: Iconos de material (pero más adelante también le mostraré cómo solucionarlo para Font Awesome). ¿Usar fuentes como dependencia? No Obtendremos las fuentes de CDN.


¿Utiliza componentes a la carta? Si Esta parece ser la forma más fácil de usar Vuetify.


Hay muchos cambios, pero lo más importante es que cuando ejecutas el yarn serve ahora verás una imagen diferente:


(también recibirá un par de docenas de advertencias de su linter).


Vamos a ver las pruebas unitarias ...


Hacer que Vuetify funcione con pruebas unitarias y e2e


Verifiquemos ./tests/unit/example.spec.ts . La prueba verifica que msg muestra " mensaje nuevo ", pero la plantilla que viene con Vuetify ya no es compatible con este accesorio. En una situación del mundo real, eliminaría tanto el componente HelloWorld como su prueba, pero aquí actualizamos el mensaje para buscar algo que esté en el componente:


 const msg = 'Welcome to Vuetify'; 

Ahora la prueba pasa (verifique con yarn test:unit ) pero todavía hay una buena docena de advertencias similares a


 [Vue warn]: Unknown custom element: <v-layout> - did you register the component correctly? For recursive components, make sure to provide the "name" option. 

La forma en que funciona Vuetify agrega ./src/plugins/vuetify.ts que configura Vuetify como parte de la aplicación. Este archivo proviene de ./src/main.ts . Lamentablemente, se omite main.ts cuando ejecuta pruebas unitarias.


Lo primero es lo primero, vuetify.ts los errores y advertencias dentro del vuetify.ts generado.


Abra su ./tsconfig.json y agregue vuetify a la sección compilerOptions.types :


  "types": [ "webpack-env", "vuetify", "jest" ], 

Esto le dice al compilador TypeScript de dónde obtener los tipos de Vuetify y el error en ./src/plugins/vuetify.ts desaparece. Arreglemos algunas advertencias de estilo para limpiarlo:


 import Vue from 'vue'; import Vuetify from 'vuetify/lib'; import 'vuetify/src/stylus/app.styl'; Vue.use(Vuetify, { theme: { primary: '#ee44aa', secondary: '#424242', accent: '#82B1FF', error: '#FF5252', info: '#2196F3', success: '#4CAF50', warning: '#FFC107', }, customProperties: true, iconfont: 'md', }); 

Ahora necesitamos cargar Vuetify en el contexto de nuestras pruebas unitarias. Cree un nuevo archivo en ./tests/jest-setup.js con el siguiente contenido:


 import '@/plugins/vuetify'; 

y actualice ./jest.config.js para cargarlo:


 module.exports = { ... setupFiles: ['./tests/jest-setup.js'], } 


Las pruebas aún fallan pero de una manera bastante críptica. Que paso


vuetify/lib es una fuente sin procesar de Vuetify que incluye elementos como módulos ES. Jest ejecuta transformaciones solo para su código fuente de forma predeterminada, lo que significa que ignora todo en node_modules . Más aún, dado que le dijimos a Vue que use TypeScript, la broma no está configurada para transpilar JS.


Para solucionar esto, necesitamos hacer dos cambios en ./jest.config.js :


 module.exports = { ... transform: { '^.+\\.vue$': 'vue-jest', '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', '^.+\\.tsx?$': 'ts-jest', '^.+\\.jsx?$': 'babel-jest', // <-- (1) }, transformIgnorePatterns: [ 'node_modules/(?!(vuetify)/)', // <-- (2) ], } 

En (1) le decimos a Jest que transforme cualquier archivo *.js o *.jsx con Babel (y Babel está preconfigurado para nosotros por Vue Cli), pero ¿qué es (2) ? transformIgnorePatterns especifica las rutas que Jest ignorará al transpilar código y, como señalé anteriormente, el valor predeterminado incluye node_modules . Aquí reemplazamos el valor predeterminado con una expresión críptica node_modules/(?!(vuetify)/) que significa "ignorar cualquier ruta que comience con node_modules/ menos que sea seguido por vuetify ":


Observe cómo los dos primeros caminos tienen una coincidencia, pero el tercero no. Este truco será útil cuando agreguemos Storybook, pero por ahora.


Ejecutando las pruebas nuevamente ...


Elementos personalizados desconocidos están de vuelta; pero al menos se compila y se ejecuta con éxito. Vuetify se transplicó pero aún necesitamos registrar los componentes manualmente. Hay algunas opciones sobre cómo hacerlo ( consulte sus documentos para ver otras opciones ); lo que haremos aquí es importar los componentes necesarios al alcance global de Vue. Abra ./src/plugins/vuetify.ts nuevamente y actualícelo a:


 import Vue from 'vue'; import Vuetify, { VFlex, VLayout, VContainer, VImg } from 'vuetify/lib'; import 'vuetify/src/stylus/app.styl'; Vue.use(Vuetify, { components: { VFlex, VLayout, VContainer, VImg }, theme: { primary: '#ee44aa', secondary: '#424242', accent: '#82B1FF', error: '#FF5252', info: '#2196F3', success: '#4CAF50', warning: '#FFC107', }, customProperties: true, iconfont: 'md', }); 

Finalmente, las pruebas pasan:


Las pruebas E2E también fallarán ( yarn test:e2e --headless ), pero se debe a que ./tests/e2e/specs/test.js buscando una cadena que ya no existe. Las pruebas de E2E aceleran su aplicación real en un navegador real para que no haya código que corregir: Vuetify está configurado en su aplicación. test.js para buscar el nuevo encabezado:


 cy.contains('h1', 'Welcome to Vuetify') 

y se volverá verde nuevamente.


Recapitulemos. Agregamos Vuetify, arreglamos las pruebas de unidad y e2e para tratar con una nueva plantilla y actualizamos Jest para transpilar el código fuente de Vuetify y cargarlo. Nuestra aplicación es funcional y podemos usar varios componentes materiales. Pasando a las historias!


Libros de cuentos


Los libros de cuentos son una idea brillante: escribe sus casos de prueba desde la perspectiva del diseñador: pequeños componentes en la aplicación completa. Puede razonar con el flujo de datos, asegurarse de que todo se vea exactamente como lo diseñó su diseñador de interfaz de usuario en Photoshop, probar sus componentes de forma aislada. ¡Agreguemos soporte para libros de cuentos!


Hay un complemento Vue storybook pero encontré que sb init ofrece una plantilla predeterminada más agradable, por lo que la utilizaremos en su lugar. Ejecute npx -p @storybook/cli sb init y después de unos minutos debería recibir un mensaje para ejecutar yarn storybook . Hagámoslo:


¡Agreguemos una nueva historia! Cree ./src/components/LoveButton.stories.ts con los siguientes contenidos:


 import { storiesOf } from '@storybook/vue'; import LoveButton from './LoveButton.vue'; storiesOf('LoveButton', module) .add('default', () => ({ components: { LoveButton }, template: `<love-button love="vue"/>`, })); 

(tenga en cuenta que puede usar LoveButton.stories.js aquí si quiere ser relajado al escribir sus historias).


TypeScript le advertirá sobre los tipos faltantes que puede solucionar con yarn add -D @types/storybook__vue .


Ahora cree ./src/components/LoveButton.vue con los siguientes contenidos:


 <template> <v-btn color="red"> <v-icon>favorite</v-icon> {{love}} </v-btn> </template> <script lang="ts"> import Vue from 'vue'; export default Vue.extend({ props: ['love'], }); </script> 

Storybook buscará en ./stories sus historias de manera predeterminada, pero a menudo es más útil mantener las historias más cerca de sus componentes (tal como lo hicimos nosotros). Para decirle a Storybook dónde buscarlos, actualice su ./.storybook/config.js :


 import { configure } from '@storybook/vue'; const req = require.context('../src', true, /.stories.(j|t)s$/); function loadStories() { req.keys().forEach(filename => req(filename)); } configure(loadStories, module); 

Ahora, ejecute yarn storybook nuevamente:


No muy emocionante. La consola está llena de advertencias:


Sin embargo, sabemos de qué se trata ahora. Storybook es otro contexto "raíz" con su propio punto de entrada; no usa main.ts y, como tal, no carga Vuetify, por lo que debemos decirle que lo haga. Actualización ./.storybook/config.js :


 import { configure } from '@storybook/vue'; import '../src/plugins/vuetify'; // <-- add this const req = require.context('../src', true, /.stories.(j|t)s$/); function loadStories() { req.keys().forEach(filename => req(filename)); } configure(loadStories, module); 

Estamos cargando nuestra configuración existente nuevamente, lo que nos asegura que Storybook use el mismo tema que la aplicación real. Lamentablemente, el yarn storybook fallará ahora:


Storybook no sabe que usamos TypeScript, por lo que no puede cargar el archivo vuetify.ts . Para solucionar esto, necesitamos actualizar la propia configuración del paquete web de Storybook. Cree ./.storybook/webpack.config.js con los siguientes contenidos:


 const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); module.exports = (baseConfig, env, defaultConfig) => { defaultConfig.resolve.extensions.push('.ts', '.tsx', '.vue', '.css', '.less', '.scss', '.sass', '.html') defaultConfig.module.rules.push({ test: /\.ts$/, exclude: /node_modules/, use: [ { loader: 'ts-loader', options: { appendTsSuffixTo: [/\.vue$/], transpileOnly: true }, } ], }); defaultConfig.module.rules.push({ test: /\.less$/, loaders: [ 'style-loader', 'css-loader', 'less-loader' ] }); defaultConfig.module.rules.push({ test: /\.styl$/, loader: 'style-loader!css-loader!stylus-loader' }); defaultConfig.plugins.push(new ForkTsCheckerWebpackPlugin()) return defaultConfig; }; 

Esto carga la configuración predeterminada, agrega ts-loader para archivos TypeScript y también agrega soporte para less y styl (que usa Vuetify).


Las advertencias siguen ahí, porque necesitamos registrar los componentes que utilizamos. Usemos componentes locales esta vez para que pueda ver la diferencia (en una aplicación de producción real es mucho más simple registrarlos todos en vuetify.ts ). Actualización ./src/components/LoveButton.vue :


 <template> <v-btn color="red"> <v-icon>favorite</v-icon> {{love}} </v-btn> </template> <script lang="ts"> import Vue from 'vue'; import { VBtn, VIcon } from 'vuetify/lib'; // <-- add this export default Vue.extend({ components: { VBtn, VIcon }, // <-- and this props: ['love'], }); </script> 

El libro de cuentos se actualiza al guardar:


Marginalmente mejor. Lo que falta El instalador de Vuetify agregó las fuentes css directamente a ./public/index.html pero Storybook no usa ese archivo, por lo que debemos agregar la fuente de Material Icons que falta. Cree ./.storybook/preview-head.hmtl con lo siguiente (copiando de ./public/index.html ):


 <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900"> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons"> 

(hay otras formas de hacer lo mismo, por ejemplo, usando CSS @import ).


Debe reiniciar su yarn storybook de yarn storybook para que se vuelva a representar correctamente:


Mucho mejor pero todavía inferior: la fuente de texto es incorrecta porque Vuetify espera que todos sus componentes estén anidados dentro de v-app que aplica los estilos de página. Seguramente no podemos agregar v-app a nuestro botón, así que decoremos la historia en su lugar. Actualice su ./src/components/LoveButton.stories.ts :


 import { storiesOf } from '@storybook/vue'; import { VApp, VContent } from 'vuetify/lib'; // <-- add the import import LoveButton from './LoveButton.vue'; // add the decorator const appDecorator = () => { return { components: { VApp, VContent }, template: ` <v-app> <div style="background-color: rgb(134, 212, 226); padding: 20px; width: 100%; height: 100%;"> <v-content> <story/> </v-content> </div> </v-app> `, }; }; storiesOf('LoveButton', module) .addDecorator(appDecorator) // <-- decorate the stories .add('default', () => ({ components: { LoveButton }, template: `<love-button love="vue"/>`, })); 

Debe registrar VApp y VContent en el ámbito global, actualice su ./src/plugins/vuetify.ts :


 import Vue from 'vue'; import Vuetify, { VFlex, VLayout, VContainer, VImg, VApp, VContent } from 'vuetify/lib'; import 'vuetify/src/stylus/app.styl'; Vue.use(Vuetify, { components: { VFlex, VLayout, VContainer, VImg, VApp, VContent }, theme: { primary: '#ee44aa', secondary: '#424242', accent: '#82B1FF', error: '#FF5252', info: '#2196F3', success: '#4CAF50', warning: '#FFC107', }, customProperties: true, iconfont: 'md', }); 

Finalmente, el resultado es espectacular:


Agregar pruebas de cuentos


Finalmente, asegurémonos de que nuestras historias estén cubiertas por pruebas unitarias. Agregue las dependencias requeridas: yarn add -D @storybook/addon-storyshots jest-vue-preprocessor babel-plugin-require-context-hook y cree ./test/unit/storybook.spec.js :


 import registerRequireContextHook from 'babel-plugin-require-context-hook/register'; import initStoryshots from '@storybook/addon-storyshots'; registerRequireContextHook(); initStoryshots(); 

La configuración de Storybook usa require.context para recopilar todas las fuentes; webpack proporciona esa función y necesitamos usar babel-plugin-require-context-hook para sustituirla en Jest. Modifique su ./babel.config.js :


 module.exports = api => ({ presets: ['@vue/app'], ...(api.env('test') && { plugins: ['require-context-hook'] }), }); 

Aquí agregamos el complemento require-context-hook si babel se ejecuta para pruebas unitarias.


Finalmente, debemos permitir que Jest transpile los archivos *.vue del libro de cuentos. ¿Recuerdas esa expresión regular ./jest.config.js en ./jest.config.js ? Volvamos a visitarlo ahora:


 module.exports = { ... transformIgnorePatterns: [ 'node_modules/(?!(vuetify/|@storybook/.*\\.vue$))', ], } 

Tenga en cuenta que no podemos simplemente agregar una segunda línea allí. Recuerde que es un patrón de ignorar, por lo que si el primer patrón ignora todo menos Vuetify, los archivos del libro de cuentos ya se ignorarán cuando Jest llegue a la segunda expresión regular.


Las nuevas pruebas funcionan como se esperaba:


Esta prueba ejecutará todas sus historias y las verificará contra las instantáneas locales en ./tests/unit/__snapshots__/ . Para verlo en acción, puede eliminar <v-icon>favorite</v-icon> del componente de su botón y volver a ejecutar la prueba para ver que falla:


yarn test:unit -u actualizará su instantánea para el nuevo diseño del botón.


Recapitulación


En este tutorial aprendimos cómo crear una nueva aplicación Vue con TypeScript habilitado; cómo agregar la biblioteca Vuetify con componentes de la interfaz de usuario de material. Nos aseguramos de que nuestras pruebas unitarias y las pruebas e2e funcionen como se esperaba. Finalmente, agregamos soporte para libros de cuentos, creamos una historia de muestra y nos aseguramos de que los cambios de UI estén cubiertos por nuestras pruebas unitarias.


Pensamientos finales


JS es un mundo en movimiento, las cosas cambian constantemente, surgen nuevos patrones, los viejos se olvidan. Este tutorial puede quedar obsoleto en solo un par de meses, así que aquí hay algunos consejos útiles.


Conoce tus herramientas. Está bien copiar y pegar líneas desde el desbordamiento de la pila hasta que su código funcione, pero debe investigar por qué el cambio lo hizo funcionar más tarde. Lea los documentos y asegúrese de comprender qué hace exactamente el cambio.


Si tienes algo para trabajar, incluso parcialmente, haz un compromiso. Incluso si es un trabajo en progreso, tendrá algo a lo que recurrir en caso de que sus cambios adicionales rompan algo.


Experimento! Si algo no funciona como piensas y los documentos dicen lo contrario, ¡experimenta! El mundo frontend es en su mayoría de código abierto, así que profundice en las fuentes de terceros y vea si puede jugar con sus instrumentos desde el interior para agregar el registro de depuración.

Source: https://habr.com/ru/post/438354/


All Articles