const merge = require('webpack-merge') const nodeExternals = require('webpack-node-externals') const baseConfig = require('./webpack.base.conf.js') const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') module.exports = merge(baseConfig, { // 将 entry 指向应用程序的 server entry 文件 entry: './src/entry-server.js', // 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import), // 并且还会在编译 Vue 组件时, // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。 target: 'node', // 对 bundle renderer 提供 source map 支持 devtool: 'source-map', // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports) output: { libraryTarget: 'commonjs2' }, externals: nodeExternals({ // 不要外置化 webpack 需要处理的依赖模块。 // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件, // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单 whitelist: /\.css$/ }), // 这是将服务器的整个输出 // 构建为单个 JSON 文件的插件。 // 默认文件名为 `vue-ssr-server-bundle.json` plugins: [ new VueSSRServerPlugin() ] })
这段代码一目了然,第一是是告诉webpack这是要打包node能运行的东西,第二是打包一个服务端入口vue-ssr-server-bundle.json
四、vue、router、store实例改造
当编写纯客户端 (client-only) 代码时,我们习惯于每次在新的上下文中对代码进行取值。但是,Node.js 服务器是一个长期运行的进程。当我们的代码进入该进程时,它将进行一次取值并留存在内存中。这意味着如果创建一个单例对象,它将在每个传入的请求之间共享。
nodejs是一个运行时,如果只是个单例的话,所有的请求都会共享这个单例,会造成状态污染。所以我们需要为每个请求创造一个vue,router,store实例。
第一步修改main.js
// main.js import Vue from 'vue' import App from './App.vue' import { createRouter } from './router' import { createStore } from './store/store.js' import { sync } from 'vuex-router-sync' export function createApp () { // 创建 router 实例 const router = createRouter() const store = createStore() // 同步路由状态(route state)到 store sync(store, router) const app = new Vue({ // 注入 router 到根 Vue 实例 router, store, render: h => h(App) }) // 返回 app 和 router return { app, router, store } }
看到这个createApp没,没错,它就是我们熟悉的工厂模式。同样的store和router一样改造
// router.js import Vue from 'vue' import Router from 'vue-router' import HelloWorld from '@/components/HelloWorld' Vue.use(Router) export let createRouter = () => { let route = new Router({ mode:'history', routes: [] }) return route }
// store.js // store.js import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export function createStore () { return new Vuex.Store({ state: { }, actions: { }, mutations: { } }) }
到这里,三个实例对象改造完成了。是不是很简单~
五、数据预取和存储
服务器渲染,可以理解为在被访问的时候,服务端做预渲染生成页面,上面说过,预渲染的缺点就是,实时数据的获取。所以如果应用程序依赖于一些异步数据,那么在开始渲染过程之前,需要先预取和解析好这些数据。
另一个需要关注的问题是在客户端,在挂载 (mount) 到客户端应用程序之前,需要获取到与服务器端应用程序完全相同的数据 - 否则,客户端应用程序会因为使用与服务器端应用程序不同的状态,然后导致混合失败。这个地方上面提过,叫同构(服务端渲染一遍,客户端拿到数据再渲染一遍)。
因为我们用的vue框架嘛,那当然数据存储选vuex咯。然后我们来理一下总体的流程:
客户端访问网站 —> 服务器获取动态数据,生成页面,并把数据存入vuex中,然后返回html —> 客户端获取html(此时已经返回了完整的页面) —> 客户端获取到vuex的数据,并解析到vue里面,然后再一次找到根元素挂载vue,重复渲染页面。(同构阶段)
流程清楚之后,那我们怎么设定,哪个地方的代码,被服务端执行,并获取数据存入vuex呢? 我们分为三步:
1.自定义函数asyncData