import 'es6-promise/auto' import { app, store, router } from './app' // prime the store with server-initialized state. // the state is determined during SSR and inlined in the page markup. if (window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__) } /** * 异步组件 */ router.onReady(() => { // 开始挂载到dom上 app.$mount('#app') }) // service worker if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { navigator.serviceWorker.register('/service-worker.js') }
客户端入口文件很简单,同步服务端发送过来的数据,然后把 vue 实例挂载到服务端渲染的 DOM 上。
再配置一下服务端入口文件:src/entry-server.js
import { app, router, store } from './app' const isDev = process.env.NODE_ENV !== 'production' // This exported function will be called by `bundleRenderer`. // This is where we perform data-prefetching to determine the // state of our application before actually rendering it. // Since data fetching is async, this function is expected to // return a Promise that resolves to the app instance. export default context => { const s = isDev && Date.now() return new Promise((resolve, reject) => { // set router's location router.push(context.url) // wait until router has resolved possible async hooks router.onReady(() => { const matchedComponents = router.getMatchedComponents() // no matched routes if (!matchedComponents.length) { reject({ code: 404 }) } // Call preFetch hooks on components matched by the route. // A preFetch hook dispatches a store action and returns a Promise, // which is resolved when the action is complete and store state has been // updated. Promise.all(matchedComponents.map(component => { return component.preFetch && component.preFetch(store) })).then(() => { isDev && console.log(`data pre-fetch: ${Date.now() - s}ms`) // After all preFetch hooks are resolved, our store is now // filled with the state needed to render the app. // Expose the state on the render context, and let the request handler // inline the state in the HTML response. This allows the client-side // store to pick-up the server-side state without having to duplicate // the initial data fetching on the client. context.state = store.state resolve(app) }).catch(reject) }) }) }
server.js 返回一个函数,该函数接受一个从服务端传递过来的 context 的参数,将 vue 实例通过 promise 返回。context 一般包含 当前页面的url,首先我们调用 vue-router 的 router.push(url) 切换到到对应的路由, 然后调用 getMatchedComponents 方法返回对应要渲染的组件, 这里会检查组件是否有 fetchServerData 方法,如果有就会执行它。
下面这行代码将服务端获取到的数据挂载到 context 对象上,后面会把这些数据直接发送到浏览器端与客户端的vue 实例进行数据(状态)同步。
context.state = store.state
然后我们分别配置客户端和服务端webpack,这里可以在我的github上fork下来参考配置,里面每一步都有注释,这里不再赘述。
接着我们需要创建app.js:
import Vue from 'vue' import App from './App.vue' import store from './store' import router from './router' import { sync } from 'vuex-router-sync' import Element from 'element-ui' Vue.use(Element) // sync the router with the vuex store. // this registers `store.state.route` sync(store, router) /** * 创建vue实例 * 在这里注入 router store 到所有的子组件 * 这样就可以在任何地方使用 `this.$router` and `this.$store` * @type {Vue$2} */ const app = new Vue({ router, store, render: h => h(App) }) /** * 导出 router and store. * 在这里不需要挂载到app上。这里和浏览器渲染不一样 */ export { app, router, store }
这样 服务端入口文件和客户端入口文件便有了一个公共实例Vue, 和我们以前写的vue实例差别不大,但是我们不会在这里将app mount到DOM上,因为这个实例也会在服务端去运行,这里直接将 app 暴露出去。
接下来创建路由router,创建vuex跟客户端都差不多。详细的可以参考我的项目...
到此,服务端渲染配置 就简单介绍完了,下面我们启动项目简单的看下:
这里跟服务端界面一样,不一样的是url已经不是之前的 #/而变成了请求形式 /
这样每当浏览器发送一个页面的请求,会有服务器渲染出一个dom字符串返回,直接在浏览器段显示,这样就避免了浏览器端渲染的很多问题。
说起SSR,其实早在SPA (Single Page Application) 出现之前,网页就是在服务端渲染的。服务器接收到客户端请求后,将数据和模板拼接成完整的页面响应到客户端。 客户端直接渲染, 此时用户希望浏览新的页面,就必须重复这个过程, 刷新页面. 这种体验在Web技术发展的当下是几乎不能被接受的,于是越来越多的技术方案涌现,力求 实现无页面刷新或者局部刷新来达到优秀的交互体验。但是SEO却是致命的,所以一切看应用场景,这里只为大家提供技术思路,为vue开发提供多一种可能的方案。