Olá pessoal!
Vou começar com um pouco de experiência.
Decidi experimentar meu novo projeto no Vue.js. Eu precisava de renderização do lado do servidor (SSR), módulos CSS, divisão de código e outras vantagens. Obviamente, era necessária uma reinicialização a quente (HMR) para aumentar a produtividade do desenvolvimento.
Eu não queria usar soluções prontas, como o Nuxt.js, porque quando o projeto cresce, é importante poder personalizar. E, em regra, qualquer solução de alto nível não permite, ou dá, mas com grandes esforços (houve uma experiência semelhante usando o Next.js. para o React).
O principal problema do desenvolvimento local ao usar a renderização do servidor e uma reinicialização a quente era que não era suficiente executar um
servidor webpack-dev- . Também devemos fazer algo com as fontes que o Node.js inicia, caso contrário, na próxima vez que recarregarmos a página, obteremos código que não foi atualizado no servidor, mas no cliente.
Tendo mergulhado na documentação e na Internet, infelizmente, não encontrei exemplos e modelos prontos a funcionar adequadamente. Portanto, eu criei o meu.

Eu determinei em que meu modelo deve consistir para que você possa liderar um desenvolvimento confortável:
- Vuejs
- SSR
- Vuex
- Módulos CSS
- Divisão de código
- ESLint, Prettier
Com o desenvolvimento local, tudo isso deve ser atualizado no navegador rapidamente e o código do servidor também deve ser atualizado.
No modo de produção, os pacotes configuráveis devem ser minificados, um hash deve ser adicionado para a estática de cache, os caminhos para os pacotes configuráveis devem ser definidos automaticamente no modelo html.
Tudo isso é implementado no
repositório do GitHub , fornecerei o código e descreverei as soluções.
Vale a pena notar que o Vue.js possui uma documentação bastante abrangente para configurar a renderização do servidor, por isso faz sentido
procurar lá.
Lado do servidor
Portanto, usaremos o Express como servidor do Node.js, também precisamos do
vue-server-renderer . Este pacote nos permite renderizar o código em uma string html, com base no pacote do servidor, no modelo html e no manifesto do cliente, no qual os nomes e o caminho para os recursos são indicados.
O arquivo
server.js ficará assim:
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 você pode ver, usamos 2 arquivos:
vue-ssr-server-bundle.json e
vue-ssr-client-manifest.json .
Eles são gerados quando o aplicativo é construído; no primeiro, o código que será executado no servidor, o segundo contém os nomes e caminhos dos recursos.
Além disso, nas opções
createBundleRenderer, especificamos o parâmetro
inject: false . Isso significa que não haverá geração automática de código html para carregar recursos e outras coisas, porque precisamos de flexibilidade completa. No modelo, marcaremos independentemente os lugares onde queremos exibir esse código.
O próprio modelo terá a seguinte aparência:
<!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>
Vamos considerar em mais detalhes.
- meta.inject (). title.text () e meta.inject (). meta.text () são necessários para exibir cabeçalhos e meta descrições. O pacote vue-meta é responsável por isso, sobre o qual discutirei abaixo
- renderResourceHints () - retornará links rel = "preload / prefetch" para os recursos especificados no manifesto do cliente
- renderStyles () - retornará links para estilos especificados no manifesto do cliente
- renderState () - retornará o estado padrão na janela .__ INITIAL_STATE__
- renderScripts () - retornará os scripts necessários para o aplicativo funcionar
Em vez de um comentário, a marcação do nosso aplicativo será substituída. Ele é requerido.
O ponto de entrada para o aplicativo do servidor Vue é o
arquivo entry-server.js .
import { createApp } from './app'; export default context => new Promise((resolve, reject) => {
Parte do cliente
O ponto de entrada do lado do cliente é o
arquivo entry-client.js .
import { createApp } from './app'; const { app, router, store } = createApp(); router.onReady(() => { if (window.__INITIAL_STATE__) {
No
app.js , nossa instância do Vue é criada, que é usada no servidor e no 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 }; }
Sempre criamos uma nova instância para evitar uma situação em que várias solicitações usam a mesma instância.App.vue é o componente raiz, que contém a diretiva
<router-view> </router-view> , que substituirá os componentes necessários, dependendo da rota.
O roteador em si se parece com isso
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') }, ], }); }
Através do
Vue.use , conectamos dois plugins:
Router e
VueMeta .
Nas rotas, os próprios componentes não são indicados diretamente, mas através de
() => import('./client/components/About.vue')
Isto é para divisão de código.
Quanto ao gerenciamento de estado (implementado pela Vuex), sua configuração não é nada de especial. A única coisa é que dividi o lado em módulos e usei constantes com um nome para facilitar a navegação por código.
Agora considere algumas nuances nos componentes do Vue.
A propriedade
metaInfo é responsável por renderizar metadados usando o pacote
vue-meta . Você pode especificar um grande número de vários parâmetros (
mais ).
metaInfo: { title: 'Main page', }
Os componentes têm um método que é executado apenas no lado do servidor.
serverPrefetch() { console.log('Run only on server'); }
Além disso, eu queria usar módulos CSS. Gosto da ideia de quando você não precisa se preocupar com o nome das classes para não se sobrepor entre os componentes. Usando módulos CSS, a classe resultante será semelhante a
<nome da classe> _ <hash> .
Para fazer isso, você precisa especificar um
módulo de estilo no componente.
<style module> .item { padding: 3px 0; } .controls { margin-top: 12px; } </style>
E no modelo, especifique o atributo
: class <div :class="$style.item"></div>
Além disso, é necessário especificar nas configurações do webpack que usaremos os módulos.
Assembléia
Vamos seguir para as configurações do webpack.
Temos uma configuração básica que é herdada pela configuração para as partes do servidor e do 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;
A configuração para a construção do código do servidor não é diferente da da
documentação . Exceto para manipulação 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]', }, }, }, ], }, });
No início, todo o processamento CSS foi movido para a configuração base, porque é necessário no cliente e no servidor. Havia uma minificação para o regime de produção.
No entanto, encontrei um problema que o servidor acabou sendo um
documento e, consequentemente, ocorreu um erro. Esse foi
um erro de
mini-css-extract-plugin que foi corrigido dividindo o processamento CSS para o servidor e o cliente.
O VueSSRServerPlugin gera o
arquivo vue-ssr-server-bundle.json , que indica o código que é executado no servidor.
Agora considere a configuração do 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;
Do notável, no desenvolvimento local, especificamos
publicPath , referenciando o
webpack-dev-server, e geramos o nome do arquivo sem hash. Além disso, para o
devServer , especificamos o parâmetro
writeToDisk: true .
Uma explicação é necessária aqui.
Por padrão, o
webpack-dev-server distribui recursos da RAM sem
gravá- los no disco. Nesse caso, enfrentamos o problema de que no manifesto do cliente (
vue-ssr-client-manifest.json ), localizado no disco, serão indicados recursos irrelevantes, porque Não será atualizado. Para contornar isso, pedimos ao servidor de desenvolvimento que grave as alterações no disco. Nesse caso, o manifesto do cliente será atualizado e os recursos necessários serão reforçados.
De fato, no futuro, quero me livrar disso. Uma solução está em donzelas. no
server.js para conectar o manifesto não no diretório
/ dist , mas no URL do servidor. Mas, neste caso, torna-se uma operação assíncrona. Ficarei feliz em ter uma boa solução para o problema nos comentários.
O Nodemon é responsável pelo
recarregamento no servidor, que monitora dois arquivos:
dist / vue-ssr-server-bundle.json e
app / server.js e reinicia o aplicativo quando eles mudam.
Para poder reiniciar o aplicativo ao alterar o
server.js , não especificamos esse arquivo como um ponto de entrada no
nodemon , mas criamos um arquivo
nodemon.js no qual conectamos o
server.js . E o arquivo
nodemon.js se torna o ponto de entrada.
No modo de produção,
app / server.js se torna o ponto de entrada.
Conclusão
No total, temos um repositório com configurações e várias equipes.
Para desenvolvimento local: yarn run dev
No lado do cliente: lança o
webpack-dev-server , que monitora alterações nos componentes do Vue e no código simples, gera um manifesto do cliente com caminhos para o servidor dev, salva-o em disco e atualiza o código e os estilos em tempo real no navegador.
No lado do servidor: inicia o
webpack no modo de monitoramento, coleta o pacote do servidor (
vue-ssr-server-bundle.json ) e reinicia o aplicativo quando é alterado.
Nesse caso, o código muda constantemente no cliente e no servidor automaticamente.
Na primeira inicialização, pode ocorrer um erro que o pacote configurável do servidor não foi encontrado. Isso é normal. Só precisa reiniciar o comando.
Para produção de montagem: yarn run build
No lado do cliente: coleta e minimiza js e css, adicionando um hash ao nome e gera um manifesto do cliente com caminhos de recursos relativos.
Do lado do servidor: coleta um pacote de servidores.
Além disso, criei outro comando
start-node do yarn run , que inicia o
server.js ; no entanto, isso é feito apenas como exemplo, em um aplicativo de produção para iniciá-lo, vale a pena usar gerenciadores de processos, por exemplo, PM2.
Espero que a experiência descrita ajude a configurar rapidamente o ecossistema para um trabalho confortável e se concentrar no desenvolvimento da funcionalidade.
Links úteis