摘要:三个配置文件以此如下改配置只是简单的配置等的使用,接着提取资源文件,指定输出的目录,而入口文件则分别在和的中配置。将指向应用程序的文件允许适用方式处理动态导入,提供支持使用风格导出模块不要外置化需要处理的依赖模块。
将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序。SSR的目的
To solve
首屏渲染问题
SEO问题
项目结构vue-ssr ├── build (webapck编译配置) ├── components (vue 页面) ├── dist (编译后的静态资源目录) ├── api.js (请求接口,模拟异步请求) ├── app.js (创建Vue实例入口) ├── App.vue (Vue页面入口) ├── entry-client.js (前端执行入口) ├── entry-server.js (后端执行入口) ├── index.template.html (前端渲染模板) ├── router.js (Vue路由配置) ├── server.js (Koa服务) ├── store.js (Vuex数据状态中心配置)原理概览
这张图相信很多大佬们都看过N遍了,每个人理解不同,我发表一下自己个人的理解,如果有什么理解错误请原谅我。
先看Source部分,Source部分先由app.js引入Vue全家桶,至于Vue全家桶如何配置后面会说明。app.js其实就是创建一个注册好各种依赖的Vue对象实例,在SPA单页环境下,我们只需要拿到这个Vue实例,然后指定挂载到模板特定的dom结点,然后丢给webpack处理就完事了。但是SSR在此分为两部分,一部分是前端单页,一部分是后端直出。于是,Client entry的作用是挂载Vue对象实例,并由webpack进行编译打包,最后在浏览器渲染。Server entry的作用是拿到Vue对象实例,并处理收集页面中的asynData,获取对应的数据上下文,然后再由webpack解析处理。最后Node Server端中使用weback编译好的两个bundle文件( 服务器需要「服务器 bundle」然后用于服务器端渲染(SSR),而「客户端 bundle」会发送给浏览器,用于混合静态标记。),当用户请求页面时候,这时候服务端会先使用SSR来生成对应的页面文档结构,而在用户切换路由则是使用了SPA的模式。
搭建环境 项目依赖说明Koa2 + Vue2 + Vue-router + Vuex
一切都从路由开始先来配置vue-router, 生成router.js
import Vue from "vue" import Router from "vue-router" import Bar from "./components/Bar.vue" import Baz from "./components/Baz.vue" import Foo from "./components/Foo.vue" import Item from "./components/Item.vue" Vue.use(Router) export const createRouter = () => { return new Router({ mode: "history", routes: [ { path: "/item/:id", component: Item }, { path: "/bar", component: Bar }, { path: "/baz", component: Baz }, { path: "/foo", component: Foo } ] }) }
为每个请求创建一个新的Vue实例,路由也是如此,通过一个工厂函数来保证每次都是新创建一个Vue路由的新实例。
Vuex 配置配置Vuex, 生成store.js
import Vue from "vue" import Vuex from "vuex" import { fetchItem } from "./api" Vue.use(Vuex) export const createStore = () => { return new Vuex.Store({ state: { items: {} }, actions: { fetchItem ({ commit }, id) { return fetchItem(id).then(item => { commit("setItem", { id, item }) }) } }, mutations: { setItem (state, { id, item }) { Vue.set(state.items, id, item) } } }) }
同样也是通过一个工厂函数,来创建一个新的Vuex实例并暴露该方法
生成一个Vue的根实例创建Vue实例,生成app.js
import Vue from "vue" import App from "./App.vue" import { createRouter } from "./router" import { createStore } from "./store" import { sync } from "vuex-router-sync" export const createApp = ssrContext => { const router = createRouter() const store = createStore() sync(store, router) const app = new Vue({ router, store, ssrContext, render: h => h(App) }) return { app, store, router } }
通过使用我们编写的createRouter, createStore来每次都创建新的Vue-router和Vuex实例,保证和Vue的实例一样都是重新创建过的,接着挂载注册router和store到Vue的实例中,提供createApp传入服务端渲染对应的数据上下文。
到此我们已经基本完成source部分的工作了。接着就要考虑如何去编译打包这些文件,让浏览器和Node服务端去运行解析。
先从前端入口文件开始前端打包入口文件: entry-client.js
import { createApp } from "./app" const { app, store, router } = createApp() if (window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__) } router.onReady(() => { router.beforeResolve((to, from, next) => { const matched = router.getMatchedComponents(to) const prevMatched = router.getMatchedComponents(from) let diffed = false const activated = matched.filter((c, i) => { return diffed || (diffed = (prevMatched[i] !== c)) }) if (!activated.length) { return next() } Promise.all(activated.map(c => { if (c.asyncData) { return c.asyncData({ store, route: to }) } })).then(() => { next() }).catch(next) }) app.$mount("#app") })
客户端的entry只需创建应用程序,并且将其挂载到 DOM 中, 需要注意的是,任然需要在挂载 app 之前调用 router.onReady,因为路由器必须要提前解析路由配置中的异步组件,(如果你有使用异步组件的话,本项目没有使用到异步组件,但后续考虑加入) 才能正确地调用组件中可能存在的路由钩子。通过添加路由钩子函数,用于处理 asyncData,在初始路由 resolve 后执行,以便我们不会二次预取(double-fetch)已有的数据。使用 router.beforeResolve(),以便确保所有异步组件都 resolve,并对比之前没有渲染的组件找出两个匹配列表的差异组件,如果没有差异表示无需处理直接next输出。
再看服务端渲染解析入口文件服务端渲染的执行入口文件: entry-server.js
import { createApp } from "./app" export default context => { return new Promise((resolve, reject) => { const { app, store, router } = createApp(context) router.push(context.url) router.onReady(() => { const matchedComponents = router.getMatchedComponents() if (!matchedComponents.length) { return reject({ code: 404 }) } Promise.all(matchedComponents.map(Component => { if (Component.asyncData) { return Component.asyncData({ store, route: router.currentRoute }) } })).then(() => { context.state = store.state resolve(app) }).catch(reject) }, reject) }) }
服务器 entry 使用 default export 导出函数,并在每次渲染中重复调用此函数。此时,创建和返回应用程序实例之外,还在此执行服务器端路由匹配(server-side route matching)和数据预取逻辑(data pre-fetching logic)。在所有预取钩子(preFetch hook) resolve 后,我们的 store 现在已经填充入渲染应用程序所需的状态。当我们将状态附加到上下文,并且 template 选项用于 renderer 时,状态将自动序列化为 window.__INITIAL_STATE__,并注入 HTML。
激动人心的来写webpack直接上手weback4.x版本
webpack配置分为3个配置,公用配置,客户端配置,服务端配置。
三个配置文件以此如下:
base config:
const path = require("path") const webpack = require("webpack") const ExtractTextPlugin = require("extract-text-webpack-plugin") module.exports = { devtool: "#cheap-module-source-map", output: { path: path.resolve(__dirname, "../dist"), publicPath: "/", filename: "[name]-[chunkhash].js" }, resolve: { alias: { "public": path.resolve(__dirname, "../public"), "components": path.resolve(__dirname, "../components") }, extensions: [".js", ".vue"] }, module: { rules: [ { test: /.vue$/, use: { loader: "vue-loader" } }, { test: /.js$/, use: "babel-loader", exclude: /node_modules/ }, { test: /.css$/, use: "css-loader" } ] }, performance: { maxEntrypointSize: 300000, hints: "warning" }, plugins: [ new ExtractTextPlugin({ filename: "common.[chunkhash].css" }) ] }
改配置只是简单的配置vue, css, babel等loader的使用,接着ExtractTextPlugin提取css资源文件,指定输出的目录,而入口文件则分别在client和server的config中配置。
client config
const webpack = require("webpack") const merge = require("webpack-merge") const path = require("path") const baseConfig = require("./webpack.base.config.js") const VueSSRClientPlugin = require("vue-server-renderer/client-plugin") module.exports = merge(baseConfig, { entry: path.resolve(__dirname, "../entry-client.js"), plugins: [ new VueSSRClientPlugin() ], optimization: { splitChunks: { cacheGroups: { commons: { chunks: "initial", minChunks: 2, maxInitialRequests: 5, minSize: 0 }, vendor: { test: /node_modules/, chunks: "initial", name: "vendor", priority: 10, enforce: true } } }, runtimeChunk: true } })
客户端的入口文件,使用VueSSRClientPlugin生成对应的vue-ssr-client-manifest.json的映射文件,然后添加vendor的chunk分离。
server config
const merge = require("webpack-merge") const path = require("path") const nodeExternals = require("webpack-node-externals") const baseConfig = require("./webpack.base.config.js") const VueSSRServerPlugin = require("vue-server-renderer/server-plugin") module.exports = merge(baseConfig, { // 将 entry 指向应用程序的 server entry 文件 entry: path.resolve(__dirname, "../entry-server.js"), // 允许 webpack Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import), target: "node", // 提供 source map 支持 devtool: "source-map", // 使用 Node 风格导出模块(Node-style exports) output: { filename: "server-bundle.js", libraryTarget: "commonjs2" }, externals: nodeExternals({ // 不要外置化 webpack 需要处理的依赖模块。 // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件, // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单 whitelist: /.css$/ }), // 这是将服务器的整个输出 // 构建为单个 JSON 文件的插件。 // 默认文件名为 `vue-ssr-server-bundle.json` plugins: [ new VueSSRServerPlugin() ] })
到此打包的流程已经结束了,server端配置参考了官网的注释。
使用Koa2const { createBundleRenderer } = require("vue-server-renderer") const serverBundle = require("./dist/vue-ssr-server-bundle.json") const clientManifest = require("./dist/vue-ssr-client-manifest.json") const fs = require("fs") const path = require("path") const Koa = require("koa") const KoaRuoter = require("koa-router") const serve = require("koa-static") const app = new Koa() const router = new KoaRuoter() const template = fs.readFileSync(path.resolve("./index.template.html"), "utf-8") const renderer = createBundleRenderer(serverBundle, { // 推荐 runInNewContext: false, // (可选)页面模板 template, // (可选)客户端构建 manifest clientManifest }) app.use(serve(path.resolve(__dirname, "./dist"))) router.get("*", (ctx, next) => { ctx.set("Content-Type", "text/html") return new Promise((resolve, reject) => { const handleError = err => { if (err && err.code === 404) { ctx.status = 404 ctx.body = "404 | Page Not Found" } else { ctx.status = 500 ctx.body = "500 | Internal Server Error" console.error(`error during render : ${ctx.url}`) console.error(err.stack) } resolve() } console.log(ctx.url) const context = { url: ctx.url, title: "Vue SSR" } // 这里无需传入一个应用程序,因为在执行 bundle 时已经自动创建过。 // 现在我们的服务器与应用程序已经解耦! renderer.renderToString(context, (err, html) => { // 处理异常…… if (err) { handleError(err) } ctx.body = html resolve() }) }) }) app.use(router.routes()).use(router.allowedMethods()) const port = 3000 app.listen(port, "127.0.0.1", () => { console.log(`server running at localhost:${port}`) })
最后效果当然是这样的了:
参考文档:
vue-ssr官方文档
代码仓库:
github链接
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/93856.html
摘要:开始改建补充安装依赖与上一次不同,这次我们基于进行改建,已经有了很多依赖库了,但我们任需要补充一个核心修改客户端的配置修改文件,添加插件添加了这个配置以后,重新启动项目通过地址就可以访问到,页面中出现的内容就是所需要的。 从零开始搭建一个vue-ssr 前言 上次我们已经实现了从零开始,搭建一个简单的vue-ssr的demo:从零开始搭建一个vue-ssr(上)。那么这次呢,我们基于v...
摘要:静态页面的或者明显最短,原因是模板几乎没什么内容。静态页面生成的白屏时间中,大部分是首屏数据请求消耗的时间,,同时也可以对比出,服务器渲染的对首屏时间的确有很明显的效果。 欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由shirishiyue发表于云+社区专栏 目前我这边的web页面,都是采用php+smarty模板生成的,是一种比较早期的开发模式。好处是没有现阶段...
摘要:静态页面的或者明显最短,原因是模板几乎没什么内容。静态页面生成的白屏时间中,大部分是首屏数据请求消耗的时间,,同时也可以对比出,服务器渲染的对首屏时间的确有很明显的效果。欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由shirishiyue发表于云+社区专栏 目前我这边的web页面,都是采用php+smarty模板生成的,是一种比较早期的开发模式。好处是没有现阶段常用的...
摘要:从零开始搭建一个背景是什么全拼是,服务端渲染。大家不妨可以打开一些页面或者一些公司的网站,查看源代码,你会发现,也是有这个标记。这时候,我们发现页面的路由切换生效了,并且不同页面的源代码也不一样了。从零开始搭建一个下项目源码 从零开始搭建一个vue-ssr 背景 What?SSR是什么? SSR全拼是Server-Side Rendering,服务端渲染。 所谓服务端渲染,指的是把...
摘要:后端主要使用的框架,数据库采用。后台管理登录采用与后端进行登陆状态的确认。本文首发于小站,这是一个积累和分享知识的个人博客 这篇文章搁置了很长时间,最终决定还是把它写出来,给刚开始学习vue并且想用vue写个人博客的同学一个参考。因为当初我也是参考了其他人分享的知识,从一个vue小白变成了一个入门级选手,并最终完成了这个个人博客的搭建工作,代码已托管在Github-justJokee。...
阅读 2192·2021-09-24 10:31
阅读 3797·2021-09-22 15:16
阅读 3349·2021-09-22 10:02
阅读 970·2021-09-22 10:02
阅读 1769·2021-09-08 09:36
阅读 1946·2019-08-30 14:18
阅读 582·2019-08-30 10:51
阅读 1837·2019-08-29 11:08