Hola a todos!
Comenzaré con un poco de historia.
Decidí probar mi nuevo proyecto en Vue.js. Necesitaba renderizado del lado del servidor (SSR), módulos CSS, división de código y otras ventajas. Por supuesto, se necesitaba un reinicio en caliente (HMR) para aumentar la productividad del desarrollo.
No quería usar soluciones ya preparadas, como Nuxt.js, porque cuando el proyecto crece, es importante poder personalizarlo. Y cualquier solución de alto nivel, por regla general, no permite hacer esto, ni dar, pero con grandes esfuerzos (hubo una experiencia similar usando Next.js para React).
El principal problema del desarrollo local cuando se usa la representación del servidor y un reinicio en caliente era que no era suficiente ejecutar un
servidor webpack-dev-server . También debemos hacer algo con las fuentes que lanza Node.js; de lo contrario, la próxima vez que volvamos a cargar la página, obtendremos un código que no se actualizó en el servidor pero sí en el cliente.
Habiéndome sumergido en la documentación e Internet, desafortunadamente, no encontré ejemplos y plantillas que funcionen adecuadamente. Por lo tanto, creé el mío.

Determiné en qué debería consistir mi plantilla para que pueda liderar un desarrollo cómodo:
- Vuejs
- SSR
- Vuex
- Módulos CSS
- División de código
- ESLint, más bonita
Con el desarrollo local, todo esto debería actualizarse en el navegador sobre la marcha, y el código del servidor también debería actualizarse.
En el modo de producción, se deben minimizar los paquetes, se debe agregar un hash para almacenar en caché las estadísticas, las rutas a los paquetes se deben configurar automáticamente en la plantilla html.
Todo esto se implementa en el
repositorio en GitHub , proporcionaré el código y describiré las soluciones.
Vale la pena señalar que Vue.js tiene una documentación bastante completa para configurar el renderizado del servidor, por lo que tiene sentido
mirar allí.
Lado del servidor
Por lo tanto, utilizaremos Express como el servidor para Node.js, también necesitamos
vue-server-renderer . Este paquete nos permitirá representar el código en una cadena html, basada en el paquete del servidor, la plantilla html y el manifiesto del cliente, en el que se indican los nombres y la ruta a los recursos.
El archivo
server.js finalmente se verá así:
const path = require('path'); const express = require('express'); const { createBundleRenderer } = require('vue-server-renderer'); const template = require('fs').readFileSync( path.join(__dirname, './templates/index.html'), 'utf-8', ); const serverBundle = require('../dist/vue-ssr-server-bundle.json'); const clientManifest = require('../dist/vue-ssr-client-manifest.json'); const server = express(); const renderer = createBundleRenderer(serverBundle, {
Como puede ver, utilizamos 2 archivos:
vue-ssr-server-bundle.json y
vue-ssr-client-manifest.json .
Se generan cuando se crea la aplicación; en el primero está el código que se ejecutará en el servidor, el segundo contiene los nombres y las rutas a los recursos.
Además, en las opciones de
createBundleRenderer, especificamos el parámetro
inyectar: falso . Esto significa que no habrá generación automática de código html para cargar recursos y otras cosas, porque Necesitamos flexibilidad completa. En la plantilla, marcaremos de forma independiente los lugares donde queremos mostrar este código.
La plantilla en sí se verá así:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> {{{ meta.inject().title.text() }}} {{{ meta.inject().meta.text() }}} {{{ renderResourceHints() }}} {{{ renderStyles() }}} </head> <body> <div id="app"></div> {{{ renderState() }}} {{{ renderScripts() }}} </body> </html>
Consideremos con más detalle.
- meta.inject (). title.text () y meta.inject (). meta.text () son necesarios para mostrar encabezados y meta descripciones. El paquete vue-meta es responsable de esto, sobre el cual hablaré a continuación.
- renderResourceHints () : devolverá enlaces rel = "preload / prefetch" a los recursos especificados en el manifiesto del cliente
- renderStyles () : devolverá enlaces a los estilos especificados en el manifiesto del cliente
- renderState () : devolverá el estado predeterminado en la ventana .__ INITIAL_STATE__
- renderScripts () : devolverá los scripts necesarios para que la aplicación funcione
En lugar de un comentario, se sustituirá el marcado de nuestra aplicación. El es requerido.
El punto de entrada a nuestra aplicación del lado del servidor Vue es el
archivo entry-server.js .
import { createApp } from './app'; export default context => new Promise((resolve, reject) => {
Parte del cliente
El punto de entrada del lado del cliente es el
archivo entry-client.js .
import { createApp } from './app'; const { app, router, store } = createApp(); router.onReady(() => { if (window.__INITIAL_STATE__) {
En
app.js , se crea nuestra instancia de Vue, que luego se usa tanto en el servidor como en el cliente.
import Vue from 'vue'; import { sync } from 'vuex-router-sync'; import { createRouter } from './router'; import { createStore } from './client/store'; import App from './App.vue'; export function createApp() { const router = createRouter(); const store = createStore(); sync(store, router); const app = new Vue({ router, store, render: h => h(App), }); return { app, router, store }; }
Siempre creamos una nueva instancia para evitar una situación en la que varias solicitudes usan la misma instancia.App.vue es el componente raíz, que contiene la directiva
<router-view> </router-view> , que sustituirá a los componentes necesarios, dependiendo de la ruta.
El enrutador en sí se ve así
import Vue from 'vue'; import Router from 'vue-router'; import VueMeta from 'vue-meta'; import routes from './routes'; Vue.use(Router); Vue.use(VueMeta); export function createRouter() { return new Router({ mode: 'history', routes: [ { path: routes.pages.main, component: () => import('./client/components/Main.vue') }, { path: routes.pages.about, component: () => import('./client/components/About.vue') }, ], }); }
A través de
Vue.use conectamos dos complementos:
Router y
VueMeta .
En las rutas, los componentes en sí no se indican directamente, sino a través de
() => import('./client/components/About.vue')
Esto es para la división de código.
En cuanto a la gestión de estado (implementada por Vuex), su configuración no es nada especial. Lo único es que dividí el lado en módulos y uso constantes con un nombre para facilitar la navegación por código.
Ahora considere algunos matices en los componentes Vue mismos.
La propiedad
metaInfo es responsable de representar los metadatos utilizando el paquete
vue-meta . Puede especificar una gran cantidad de varios parámetros (
más ).
metaInfo: { title: 'Main page', }
Los componentes tienen un método que se ejecuta solo en el lado del servidor.
serverPrefetch() { console.log('Run only on server'); }
Además, quería usar módulos CSS. Me gusta la idea cuando no tiene que preocuparse por el nombre de las clases para no superponerse entre los componentes. Usando módulos CSS, la clase resultante se verá como
<nombre de clase> _ <hash> .
Para hacer esto, debe especificar un
módulo de estilo en el componente.
<style module> .item { padding: 3px 0; } .controls { margin-top: 12px; } </style>
Y en la plantilla especifique el atributo
: clase <div :class="$style.item"></div>
Además, es necesario especificar en la configuración del paquete web que utilizaremos los módulos.
Asamblea
Pasemos a la configuración del paquete web.
Tenemos una configuración básica que es heredada por la configuración para el servidor y las partes del cliente.
const webpack = require('webpack'); const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); const merge = require('webpack-merge'); const VueLoaderPlugin = require('vue-loader/lib/plugin'); const isProduction = process.env.NODE_ENV === 'production'; let config = { mode: isProduction ? 'production' : 'development', module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', }, { test: /\.js$/, loader: 'babel-loader', exclude: file => /node_modules/.test(file) && !/\.vue\.js/.test(file), }, { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, use: { loader: 'url-loader', options: { limit: 10000, name: 'images/[name].[hash:8].[ext]', }, }, }, ], }, plugins: [ new VueLoaderPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), }), ], }; if (isProduction) { config = merge(config, { optimization: { minimizer: [new OptimizeCSSAssetsPlugin(), new UglifyJsPlugin()], }, }); } module.exports = config;
La configuración para construir el código del servidor no es diferente de la de la
documentación . Excepto por el manejo de CSS.
const merge = require('webpack-merge'); const nodeExternals = require('webpack-node-externals'); const VueSSRServerPlugin = require('vue-server-renderer/server-plugin'); const baseConfig = require('./webpack.base.js'); module.exports = merge(baseConfig, { entry: './app/entry-server.js', target: 'node', devtool: 'source-map', output: { libraryTarget: 'commonjs2', }, externals: nodeExternals({ whitelist: /\.css$/, }), plugins: [new VueSSRServerPlugin()], module: { rules: [ { test: /\.css$/, loader: 'css-loader', options: { modules: { localIdentName: '[local]_[hash:base64:8]', }, }, }, ], }, });
Al principio, todo el procesamiento CSS se movió a la configuración base, porque Se necesita tanto en el cliente como en el servidor. Hubo una minificación para el régimen de producción.
Sin embargo, me encontré con un problema que el servidor resultó ser un
documento y, en consecuencia, se produjo un error. Esto resultó ser
un error
mini-css-extract-plugin que se solucionó dividiendo el procesamiento CSS para el servidor y el cliente.
VueSSRServerPlugin genera el
archivo vue-ssr-server-bundle.json , que indica el código que se ejecuta en el servidor.
Ahora considere la configuración del cliente.
const webpack = require('webpack'); const merge = require('webpack-merge'); const VueSSRClientPlugin = require('vue-server-renderer/client-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const path = require('path'); const baseConfig = require('./webpack.base.js'); const isProduction = process.env.NODE_ENV === 'production'; let config = merge(baseConfig, { entry: ['./app/entry-client.js'], plugins: [new VueSSRClientPlugin()], output: { path: path.resolve('./dist/'), filename: '[name].[hash:8].js', publicPath: '/dist/', }, module: { rules: [ { test: /\.css$/, use: [ isProduction ? MiniCssExtractPlugin.loader : 'vue-style-loader', { loader: 'css-loader', options: { modules: { localIdentName: '[local]_[hash:base64:8]', }, }, }, ], }, ], }, }); if (!isProduction) { config = merge(config, { output: { filename: '[name].js', publicPath: 'http://localhost:9999/dist/', }, plugins: [new webpack.HotModuleReplacementPlugin()], devtool: 'source-map', devServer: { writeToDisk: true, contentBase: path.resolve(__dirname, 'dist'), publicPath: 'http://localhost:9999/dist/', hot: true, inline: true, historyApiFallback: true, port: 9999, headers: { 'Access-Control-Allow-Origin': '*', }, }, }); } else { config = merge(config, { plugins: [ new MiniCssExtractPlugin({ filename: '[name].[hash:8].css', }), ], }); } module.exports = config;
De manera notable, en el desarrollo local, especificamos
publicPath ,
haciendo referencia a
webpack-dev-server, y generamos el nombre del archivo sin un hash. Además, para
devServer especificamos el parámetro
writeToDisk: true .
Se necesita una explicación aquí.
Por defecto,
webpack-dev-server distribuye recursos desde la RAM sin escribirlos en el disco. En este caso, nos enfrentamos al problema de que en el manifiesto del cliente (
vue-ssr-client-manifest.json ), que se encuentra en el disco, se indicarán recursos irrelevantes, porque No se actualizará. Para evitar esto, le decimos al servidor de desarrollo que escriba los cambios en el disco, en cuyo caso el manifiesto del cliente se actualizará y se ajustarán los recursos necesarios.
De hecho, en el futuro quiero deshacerme de esto. Una solución está en las doncellas. modo en
server.js para conectar el manifiesto no desde el directorio
/ dist , sino desde la url del servidor. Pero en este caso, se convierte en una operación asincrónica. Estaré encantado de tener una buena solución al problema en los comentarios.
Nodemon es responsable de la
recarga del lado del servidor, que monitorea dos archivos:
dist / vue-ssr-server-bundle.json y
app / server.js y reinicia la aplicación cuando cambian.
Para poder reiniciar la aplicación al cambiar
server.js , no especificamos este archivo como un punto de entrada en
nodemon , sino que creamos un archivo
nodemon.js en el que conectamos
server.js . Y el archivo
nodemon.js se convierte en el punto de entrada.
En modo de producción,
app / server.js se convierte en el punto de entrada.
Conclusión
Total, tenemos un repositorio con configuraciones y varios equipos.
Para el desarrollo local: yarn run dev
Del lado del cliente: lanza
webpack-dev-server , que monitorea los cambios en los componentes de Vue y el código simple, genera un manifiesto de cliente con rutas al servidor de desarrollo, lo guarda en el disco y actualiza el código, estilos sobre la marcha en el navegador.
En el lado del servidor: inicia el
paquete web en modo de supervisión, recopila el paquete del servidor (
vue-ssr-server-bundle.json ) y reinicia la aplicación cuando se cambia.
En este caso, el código cambia constantemente en el cliente y el servidor automáticamente.
Al primer inicio, puede producirse un error de que no se encontró el paquete del servidor. Esto es normal Solo necesito reiniciar el comando.
Para montaje de producción: yarn run build
Del lado del cliente: recopila y minimiza js y css, agrega un hash al nombre y genera un manifiesto de cliente con rutas de recursos relativas.
Desde el lado del servidor: recopila un paquete de servidores.
Además, creé otro comando
yarn run start-node , que inicia
server.js , sin embargo, esto se hace solo como un ejemplo, en una aplicación de producción para comenzar vale la pena usar administradores de procesos, por ejemplo, PM2.
Espero que la experiencia descrita ayude a configurar rápidamente el ecosistema para un trabajo cómodo y a centrarse en el desarrollo de la funcionalidad.
Enlaces utiles