Bagaimana saya membuat pengembangan di Vue.js nyaman dengan rendering sisi server

Halo semuanya!

Saya akan mulai dengan sedikit latar belakang.

Saya memutuskan untuk mencoba proyek baru saya di Vue.js. Saya membutuhkan rendering sisi server (SSR), modul CSS, pemecahan kode, dan barang lainnya. Tentu saja, hot reboot (HMR) diperlukan untuk meningkatkan produktivitas pengembangan.

Saya tidak ingin menggunakan solusi yang sudah jadi, seperti Nuxt.js, karena ketika proyek tumbuh, penting untuk dapat menyesuaikan. Dan setiap solusi tingkat tinggi, sebagai suatu peraturan, tidak memungkinkan untuk melakukan ini, atau memberi, tetapi dengan upaya keras (ada pengalaman serupa menggunakan Next.js for React).

Masalah utama pengembangan lokal ketika menggunakan rendering server dan reboot panas adalah bahwa itu tidak cukup untuk menjalankan satu webpack-dev-server . Kita juga harus melakukan sesuatu dengan sumber-sumber yang diluncurkan Node.js, jika lain kali kita memuat ulang halaman, kita akan mendapatkan kode yang tidak diperbarui di server tetapi diperbarui pada klien.

Setelah terjun ke dalam dokumentasi dan Internet, sayangnya, saya tidak menemukan contoh dan templat yang siap pakai yang memadai. Karena itu, saya buat sendiri.



Saya menentukan apa yang harus terdiri dari templat saya sehingga Anda dapat memimpin pengembangan yang nyaman:

  • Vuejs
  • SSR
  • Vuex
  • Modul CSS
  • Pemecahan kode
  • ESLint, Lebih cantik

Dengan pengembangan lokal, semua ini harus diperbarui di browser dengan cepat, dan kode server juga harus diperbarui.

Dalam mode produksi, bundel harus diperkecil, hash harus ditambahkan untuk caching statika, jalur ke bundel harus secara otomatis diatur dalam templat html.

Semua ini diimplementasikan dalam repositori di GitHub , saya akan memberikan kode dan menjelaskan solusinya.

Perlu dicatat bahwa Vue.js memiliki dokumentasi yang cukup komprehensif untuk mengatur rendering server, jadi masuk akal untuk melihatnya .

Sisi server


Jadi, kita akan menggunakan Express sebagai server untuk Node.js, kita juga perlu vue-server-renderer . Paket ini memungkinkan kita untuk merender kode menjadi html-string, berdasarkan bundel server, html-template dan manifes klien, di mana nama dan jalur ke sumber daya ditunjukkan.

File server.js pada akhirnya akan terlihat seperti ini:

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, { //           ,     runInNewContext: false, template, clientManifest, inject: false, }); //         nginx server.use('/dist', express.static(path.join(__dirname, '../dist'))); server.get('*', (req, res) => { const context = { url: req.url }; renderer.renderToString(context, (err, html) => { if (err) { if (+err.message === 404) { res.status(404).end('Page not found'); } else { console.log(err); res.status(500).end('Internal Server Error'); } } res.end(html); }); }); server.listen(process.env.PORT || 3000); 

Seperti yang Anda lihat, kami menggunakan 2 file: vue-ssr-server-bundle.json dan vue-ssr-client-manifest.json .

Mereka dihasilkan ketika aplikasi dibangun; di yang pertama adalah kode yang akan dieksekusi di server, yang kedua berisi nama dan jalur ke sumber daya.

Juga, dalam opsi createBundleRenderer, kami menetapkan parameter inject: false . Ini berarti bahwa tidak akan ada pembuatan kode html otomatis untuk memuat sumber daya dan hal lainnya, karena kita membutuhkan fleksibilitas penuh. Dalam templat, kami akan secara independen menandai tempat-tempat di mana kami ingin menampilkan kode ini.

Template itu sendiri akan terlihat seperti ini:

 <!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"><!--vue-ssr-outlet--></div> {{{ renderState() }}} {{{ renderScripts() }}} </body> </html> 

Mari kita pertimbangkan lebih detail.

  • meta.inject (). title.text () dan meta.inject (). meta.text () diperlukan untuk menampilkan header dan deskripsi meta. Paket vue-meta bertanggung jawab untuk ini, yang akan saya bahas di bawah ini
  • renderResourceHints () - akan mengembalikan tautan rel = "preload / prefetch" ke sumber daya yang ditentukan dalam manifes klien
  • renderStyles () - akan mengembalikan tautan ke gaya yang ditentukan dalam manifes klien
  • renderState () - akan mengembalikan status default di jendela .__ INITIAL_STATE__
  • renderScripts () - akan mengembalikan skrip yang diperlukan agar aplikasi berfungsi

Alih-alih komentar, markup aplikasi kita akan diganti. Dia dituntut.

Titik masuk ke aplikasi sisi server Vue kami adalah file entry-server.js .

 import { createApp } from './app'; export default context => new Promise((resolve, reject) => { //      Vue const { app, router, store } = createApp(); // $meta - ,   vue-meta   Vue const meta = app.$meta(); //      router.push(context.url); //  -  ,      context.meta = meta; router.onReady(() => { context.rendered = () => { //    ,     ,  window.__INITIAL_STATE__ context.state = store.state; }; const matchedComponents = router.getMatchedComponents(); //     if (!matchedComponents.length) { return reject(new Error(404)); } return resolve(app); }, reject); }); 

Bagian klien


Titik masuk sisi klien adalah file entry-client.js .

 import { createApp } from './app'; const { app, router, store } = createApp(); router.onReady(() => { if (window.__INITIAL_STATE__) { //    ,     store.replaceState(window.__INITIAL_STATE__); } app.$mount('#app'); }); //    HMR  ,  webpack-dev-server     hot if (module.hot) { const api = require('vue-hot-reload-api'); const Vue = require('vue'); api.install(Vue); if (!api.compatible) { throw new Error( 'vue-hot-reload-api is not compatible with the version of Vue you are using.', ); } module.hot.accept(); } 

Di app.js , instance Vue kami dibuat, yang kemudian digunakan di server dan di klien.

 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 }; } 

Kami selalu membuat instance baru untuk menghindari situasi di mana beberapa permintaan menggunakan instance yang sama.

App.vue adalah komponen root, yang berisi arahan <router-view> </router-view> , yang akan menggantikan komponen yang diperlukan, tergantung pada rute.

Perute itu sendiri terlihat seperti ini

 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') }, ], }); } 

Melalui Vue.use kami menghubungkan dua plugin: Router dan VueMeta .
Dalam rute, komponen itu sendiri tidak ditunjukkan secara langsung, tetapi melalui

 () => import('./client/components/About.vue') 

Ini untuk pemecahan kode.

Adapun manajemen negara (diimplementasikan oleh Vuex), konfigurasinya tidak ada yang istimewa. Satu-satunya hal adalah, saya membagi sisi menjadi modul dan menggunakan konstanta dengan nama untuk membuatnya lebih mudah dinavigasi dengan kode.

Sekarang pertimbangkan beberapa nuansa dalam komponen Vue sendiri.

Properti metaInfo bertanggung jawab untuk memberikan data meta menggunakan paket vue-meta . Anda dapat menentukan sejumlah besar berbagai parameter ( lebih banyak ).

 metaInfo: { title: 'Main page', } 

Komponen memiliki metode yang hanya berjalan di sisi server.

 serverPrefetch() { console.log('Run only on server'); } 

Saya juga ingin menggunakan modul CSS. Saya suka ide ketika Anda tidak perlu peduli dengan nama kelas agar tidak tumpang tindih antara komponen. Menggunakan modul CSS, kelas yang dihasilkan akan terlihat seperti <class name> _ <hash> .

Untuk melakukan ini, Anda perlu menentukan modul gaya dalam komponen.

 <style module> .item { padding: 3px 0; } .controls { margin-top: 12px; } </style> 

Dan dalam template tentukan atribut : class

 <div :class="$style.item"></div> 

Juga, perlu untuk menentukan dalam pengaturan webpack bahwa kita akan menggunakan modul.

Majelis


Mari kita beralih ke pengaturan webpack sendiri.

Kami memiliki konfigurasi dasar yang diwarisi oleh konfigurasi untuk bagian server dan klien.

 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; 

Konfigurasi untuk membangun kode server tidak berbeda dari yang ada di dokumentasi . Kecuali untuk penanganan 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]', }, }, }, ], }, }); 

Pada awalnya, semua pemrosesan CSS dipindahkan ke konfigurasi dasar, karena diperlukan baik pada klien dan di server. Ada minifikasi untuk rezim produksi.
Namun, saya mengalami masalah bahwa server ternyata menjadi dokumen , dan, karenanya, terjadi kesalahan. Ini ternyata merupakan kesalahan mini-css-ekstrak-plugin yang diperbaiki dengan memisahkan pemrosesan CSS untuk server dan klien.

VueSSRServerPlugin menghasilkan file vue-ssr-server-bundle.json , yang menunjukkan kode yang berjalan di server.

Sekarang pertimbangkan konfigurasi klien.

 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; 

Dari yang patut dicatat, dalam pengembangan lokal, kami menentukan publicPath , merujuk webpack-dev-server, dan menghasilkan nama file tanpa hash. Juga, untuk devServer kami menentukan parameter writeToDisk: true .

Penjelasan diperlukan di sini.

Secara default, webpack-dev-server mendistribusikan sumber daya dari RAM tanpa menuliskannya ke disk. Dalam hal ini, kita dihadapkan dengan masalah bahwa sumber daya yang tidak relevan akan ditunjukkan dalam manifes klien ( vue-ssr-client-manifest.json ), yang terletak di disk, karena Itu tidak akan diperbarui. Untuk menyiasatinya, kami meminta server dev untuk menulis perubahan ke disk, dalam hal ini manifes klien akan diperbarui dan sumber daya yang diperlukan akan diperketat.

Bahkan, di masa depan saya ingin menyingkirkan ini. Salah satu solusinya adalah di gadis. mode di server.js untuk menghubungkan manifes bukan dari direktori / dist , tetapi dari url server. Tetapi dalam kasus ini, ini menjadi operasi yang tidak sinkron. Saya akan senang memiliki solusi yang bagus untuk masalah di komentar.

Nodemon bertanggung jawab atas pemuatan ulang sisi server, yang memonitor dua file: dist / vue-ssr-server-bundle.json dan app / server.js dan me -restart aplikasi ketika diubah.

Untuk dapat memulai kembali aplikasi ketika mengubah server.js , kami tidak menentukan file ini sebagai titik masuk dalam nodemon , tetapi membuat file nodemon.js yang kami sambungkan ke server.js . Dan file nodemon.js menjadi titik masuk.

Dalam mode produksi, app / server.js menjadi titik masuk.

Kesimpulan


Total, kami memiliki repositori dengan pengaturan dan beberapa tim.

Untuk pengembangan lokal:

 yarn run dev 

Di sisi klien: meluncurkan webpack-dev-server , yang memantau perubahan komponen Vue dan kode sederhana, menghasilkan manifes klien dengan jalur ke server dev, menyimpannya ke disk dan memperbarui kode, menggunakan gaya dengan cepat di browser.

Di sisi server: ia memulai webpack dalam mode pemantauan, mengumpulkan bundel server ( vue-ssr-server-bundle.json ) dan me- restart aplikasi ketika diubah.

Dalam hal ini, kode berubah secara konsisten pada klien dan server secara otomatis.
Pada awal pertama, kesalahan dapat terjadi bahwa bundel server tidak ditemukan. Ini normal. Hanya perlu me-restart perintah.

Untuk produksi perakitan:

 yarn run build 

Di sisi klien: mengumpulkan dan meminimalkan js dan css, menambahkan hash ke nama dan menghasilkan manifes klien dengan jalur sumber daya relatif.

Dari sisi server: mengumpulkan bundel server.

Juga, saya membuat thread run start-node command lain, yang memulai server.js , namun ini hanya dilakukan sebagai contoh, dalam aplikasi produksi untuk memulainya layak menggunakan manajer proses, misalnya, PM2.

Saya berharap pengalaman yang dijelaskan akan membantu dengan cepat mengatur ekosistem untuk pekerjaan yang nyaman dan fokus pada pengembangan fungsionalitas.

Tautan yang bermanfaat


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


All Articles