大家好!
我将从一些背景开始。
我决定在Vue.js上尝试新项目。 我需要服务器端渲染(SSR),CSS模块,代码拆分和其他功能。 当然,需要热重启(HMR)来提高开发效率。
我不想使用现成的解决方案,例如Nuxt.js,因为 当项目发展时,进行定制非常重要。 通常,任何高级解决方案都不允许这样做或付出任何努力,但要付出很大的努力(使用Next.js进行React也有类似的经验)。
使用服务器渲染和热重启时本地开发的主要问题是仅运行一个
webpack-dev-server是不够的。 我们还必须对Node.js启动的源代码进行处理,否则,下次我们重新加载页面时,我们将获得未在服务器上更新但在客户端上更新的代码。
不幸的是,进入文档和Internet之后,我没有找到现成的可以正常工作的示例和模板。 因此,我创建了自己的。

我确定了模板应包含的内容,以便您可以轻松进行开发:
- Vuejs
- 固态继电器
- 威克斯
- CSS模块
- 代码拆分
- ESLint,更漂亮
通过本地开发,所有这些都应在浏览器中即时更新,并且服务器代码也应更新。
在生产模式下,应最小化包,应添加哈希值以缓存静态变量,应在html模板中自动设置包的路径。
所有这些都
在GitHub上的
存储库中实现,我将提供代码并描述解决方案。
值得注意的是,Vue.js具有用于设置服务器渲染的非常全面的文档,因此在此处
查找是有意义的。
服务器端
因此,我们将Express用作Node.js的服务器,我们还需要
vue-server-renderer 。 这个包允许我们根据服务器包,html模板和客户端清单将代码呈现为html字符串,并在其中指明资源的名称和路径。
server.js文件最终将如下所示:
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, {
如您所见,我们使用2个文件:
vue-ssr-server-bundle.json和
vue-ssr-client-manifest.json 。
它们是在构建应用程序时生成的。 第一个是将在服务器上执行的代码,第二个包含了资源的名称和路径。
另外,在
createBundleRenderer选项中
,我们指定了
inject:false参数。 这意味着将不会自动生成用于加载资源和其他内容的html代码,因为 我们需要完全的灵活性。 在模板中,我们将独立标记要显示此代码的位置。
模板本身将如下所示:
<!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>
让我们更详细地考虑。
- 需要meta.inject(),title.text()和meta.inject()。meta.text()来显示标题和元描述。 vue-meta软件包对此负责,下面将对此进行讨论
- renderResourceHints() -将返回rel =“ preload / prefetch”链接到客户端清单中指定的资源
- renderStyles() -将返回指向客户端清单中指定的样式的链接
- renderState() -将在窗口.__ INITIAL_STATE__中返回默认状态。
- renderScripts() -将返回应用程序正常工作所需的脚本
除了注释,我们的应用程序的标记将被替换。 他是必需的。
Vue服务器端应用程序的入口点是
entry-server.js文件 。
import { createApp } from './app'; export default context => new Promise((resolve, reject) => {
客户部分
客户端入口点是
entry-client.js文件 。
import { createApp } from './app'; const { app, router, store } = createApp(); router.onReady(() => { if (window.__INITIAL_STATE__) {
在
app.js中 ,创建了我们的Vue实例,然后在服务器和客户端上使用它。
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 }; }
我们总是创建一个新实例,以避免多个请求使用同一实例的情况。App.vue是根组件,它包含
<router-view> </ router-view>指令,它将根据路由替换必要的组件。
路由器本身看起来像这样
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') }, ], }); }
通过
Vue.use,我们连接了两个插件:
Router和
VueMeta 。
在路线中,组件本身不会直接显示,而是通过
() => import('./client/components/About.vue')
这是用于代码拆分。
至于状态管理(由Vuex实现),其配置没什么特别的。 唯一的事情是,我将这一面分为模块,并使用带有名称的常量以使其更易于通过代码进行导航。
现在考虑一下Vue组件本身的一些细微差别。
metaInfo属性负责使用
vue-meta包呈现元数据。 您可以指定大量的各种参数(
更多 )。
metaInfo: { title: 'Main page', }
组件具有仅在服务器端运行的方法。
serverPrefetch() { console.log('Run only on server'); }
另外,我想使用CSS模块。 我喜欢这个主意,因为您不必关心类的名称,以免组件之间重叠。 使用CSS模块,生成的类将类似于
<class name> _ <hash> 。
为此,您需要在组件中指定
样式模块 。
<style module> .item { padding: 3px 0; } .controls { margin-top: 12px; } </style>
并在模板中指定属性
:class <div :class="$style.item"></div>
另外,有必要在webpack设置中指定我们将使用这些模块。
组装方式
让我们继续进行webpack设置。
我们有一个基本配置,该配置由服务器和客户端部件的配置继承。
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;
用于构建服务器代码的配置与
文档中的配置相同 。 除了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]', }, }, }, ], }, });
最初,所有CSS处理都移至基本配置,因为 客户端和服务器上都需要它。 生产制度有一个缺点。
但是,我遇到了一个问题,该服务器原来是
document ,因此发生了错误。 事实证明这是
一个 mini-css-extract-plugin错误,该错误通过拆分服务器和客户端的CSS处理得以解决。
VueSSRServerPlugin生成
vue-ssr-server-bundle.json文件 ,该
文件指示在服务器上运行的代码。
现在考虑客户端配置。
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;
值得注意的是,在本地开发中,我们指定
publicPath ,引用
webpack-dev-server,并生成不带哈希的文件名。 另外,对于
devServer,我们指定
writeToDisk:true参数。
这里需要解释。
默认情况下,
webpack-dev-server从RAM分配资源而不将其写入磁盘。 在这种情况下,我们面临的问题是,不相关的资源将在位于磁盘上的客户端清单(
vue-ssr-client-manifest.json )中指示出来,因为 它不会被更新。 为了解决这个问题,我们告诉开发服务器将所做的更改写入磁盘,在这种情况下,客户端清单将被更新,必要的资源将被收紧。
实际上,将来我想摆脱这种情况。 一种解决方案是处女。
server.js中的 mode
来连接清单不是来自
/ dist目录,而是来自服务器url。 但是在这种情况下,它变为异步操作。 我很高兴在评论中有一个很好的解决方案。
Nodemon负责服务器端的
重新加载 ,它监视两个文件:
dist / vue-ssr-server-bundle.json和
app / server.js,并在更改它们后重新启动应用程序。
为了能够在更改
server.js时重新启动应用程序,我们没有将此文件指定为
nodemon中的入口点,而是创建了一个连接
server.js的
nodemon.js文件。 并且
nodemon.js文件成为入口点。
在生产模式下,
app / server.js成为入口点。
结论
总计,我们有一个包含设置和几个团队的存储库。
对于本地发展: yarn run dev
在客户端:它启动
webpack-dev-server ,该
服务器监视Vue组件和简单代码中的更改,生成带有开发服务器路径的客户端清单,将其保存到磁盘中,并在浏览器中即时更新代码和样式。
在服务器端:它以监视模式启动
webpack ,收集服务器捆绑包(
vue-ssr-server-bundle.json ),并在更改应用程序后重新启动。
在这种情况下,代码会自动在客户端和服务器上一致地更改。
在第一次启动时,可能会发生一个错误,即找不到服务器捆绑包。 这很正常。 只需要重启命令即可。
对于组装生产: yarn run build
在客户端:收集并最小化js和css,在名称中添加一个哈希,并生成具有相对资源路径的客户端清单。
从服务器端:收集服务器捆绑包。
另外,我还创建了另一个
yarn run start-node命令,该命令启动
server.js ,但这只是作为示例,在生产应用程序中启动它是值得使用流程管理器(例如PM2)进行的。
我希望所描述的经验将有助于快速建立生态系统,以实现舒适的工作并专注于功能开发。
有用的链接