前言
在Java Web时代,由于前后端在一个服务,客户端向服务端发送请求,web服务器解析HTTP格式的数据,转发到指定的servlet,servlet根据参数从数据库拿到数据,放进域对象并返回指定的JSP页面,JSP跟域对象数据一起渲染成HTML,最后通过HTTP响应体返回给客户端,整个过程发生在服务器。
而随着前后端分离后,前端用前端框架开发,开发完成后把项目打包成静态文件(HTML,CSS,JS)放在静态Web服务器(Nginx),此时客户端向服务器发出请求,服务器就会返回这些静态文件,到达客户端后,客户端在模板上执行JS后,把整个项目构建成完整的单页面应用,并通过AJAX向服务端请求数据,再渲染在单页面应用上,整个过程都发生在客户端。
正文
配置基于vue3.0
vue.config.js
const VueSSRServerPlugin = require("vue-server-renderer/server-plugin");
const VueSSRClientPlugin = require("vue-server-renderer/client-plugin");
const nodeExternals = require("webpack-node-externals");
const env = process.env;
const isServer = env.RUN_ENV === "server";
const resolve = dir => require('path').join(__dirname, dir)
module.exports = {
lintOnSave: false,
publicPath: "/",
outputDir: `dist/${env.RUN_ENV}`,
devServer: {
port: 3001,
publicPath: "/"
},
configureWebpack: {
// 将 entry 指向应用程序的 server / client 文件,不写默认是main.js
entry: `./src/entry-${env.RUN_ENV}.js`,
devtool: "eval",
// 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import),
// 并且还会在编译 Vue 组件时,
// 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
target: isServer ? "node" : "web",
output: {
libraryTarget: isServer ? "commonjs2" : undefined,
},
// https://webpack.js.org/configuration/externals/#function
// https://github.com/liady/webpack-node-externals
// 外置化应用程序依赖模块。可以使服务器构建速度更快,
// 并生成较小的 bundle 文件。
externals: isServer
? nodeExternals({
// 不要外置化 webpack 需要处理的依赖模块。
// 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
// 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
whitelist: /\.css$/,
})
: undefined,
optimization: { splitChunks: isServer ? false : undefined },
// 这是将服务器的整个输出
// 构建为单个 JSON 文件的插件。
// 服务端默认文件名为 `vue-ssr-server-bundle.json`
// 客户端默认文件名为 `vue-ssr-client-manifest.json`
plugins: [isServer ? new VueSSRServerPlugin() : new VueSSRClientPlugin()],
},
chainWebpack: config => {
config.resolve.alias
.set('@', resolve('src'))
},
css: {
loaderOptions: {
// 设置 scss 公用变量文件
stylus: {
}
}
},
}
entry-client.js
import Vue from 'vue'
import 'es6-promise/auto'
import { createApp } from './app'
import ProgressBar from './components/ProgressBar.vue'
// global progress bar
const bar = Vue.prototype.$bar = new Vue(ProgressBar).$mount()
document.body.appendChild(bar.$el)
// a global mixin that calls `asyncData` when a route component's params change
Vue.mixin({
beforeRouteUpdate(to, from, next) {
const { asyncData } = this.$options
if (asyncData) {
asyncData({
store: this.$store,
route: to
}).then(next).catch(next)
} else {
next()
}
}
})
Vue.mixin({
beforeMount () {
const { asyncData } = this.$options
if (asyncData) {
// 将获取数据操作分配给 promise
// 以便在组件中,我们可以在数据准备就绪后
// 通过运行 `this.dataPromise.then(...)` 来执行其他任务
this.dataPromise = asyncData({
store: this.$store,
route: this.$route
})
}
}
})
const { app, router, store } = createApp()
// 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__)
}
// wait until router has resolved all async before hooks
// and async components...
router.onReady(() => {
// Add router hook for handling asyncData.
// Doing it after initial route is resolved so that we don't double-fetch
// the data that we already have. Using router.beforeResolve() so that all
// async components are resolved.
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))
})
const asyncDataHooks = activated.map(c => c.asyncData).filter(_ => _)
if (!asyncDataHooks.length) {
return next()
}
bar.start()
Promise.all(asyncDataHooks.map(hook => hook({ store, route: to })))
.then(() => {
bar.finish()
next()
})
.catch(next)
})
// actually mount to DOM
app.$mount('#app')
})
// service worker
// if ('https:' === location.protocol && navigator.serviceWorker) {
// navigator.serviceWorker.register('/service-worker.js')
// }
entry-server.js
import { createApp } 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 => {
return new Promise((resolve, reject) => {
const s = isDev && Date.now()
const { app, router, store } = createApp()
const { url } = context
const { fullPath } = router.resolve(url).route
if (fullPath !== url) {
return reject({ url: fullPath })
}
// set router's location
router.push(url)
// wait until router has resolved possible async hooks
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
// no matched routes
if (!matchedComponents.length) {
return reject({ code: 404 })
}
// Call fetchData 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(({ asyncData }) => asyncData && asyncData({
store,
route: router.currentRoute
}))).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)
}, reject)
})
}
app.js
import Vue from 'vue'
import App from './App.vue'
import { createStore } from './store'
import { createRouter } from './router'
import { sync } from 'vuex-router-sync'
// import titleMixin from './util/title'
// import * as filters from './util/filters'
// mixin for handling title
// Vue.mixin(titleMixin)
// register global utility filters.
// Object.keys(filters).forEach(key => {
// Vue.filter(key, filters[key])
// })
// Expose a factory function that creates a fresh set of store, router,
// app instances on each call (which is called for each SSR request)
export function createApp () {
// create store and router instances
const store = createStore()
const router = createRouter()
// sync the router with the vuex store.
// this registers `store.state.route`
sync(store, router)
// create the app instance.
// here we inject the router, store and ssr context to all child components,
// making them available everywhere as `this.$router` and `this.$store`.
const app = new Vue({
router,
store,
render: h => h(App)
})
// expose the app, the router and the store.
// note we are not mounting the app here, since bootstrapping will be
// different depending on whether we are in a browser or on the server.
return { app, router, store }
}
server.js
const fs = require("fs");
const path = require("path");
const express = require('express')
const app = express()
// 第 2 步:获得一个createBundleRenderer
// const { createBundleRenderer } = require("vue-server-renderer");
// const serverBundle = require("./dist/server/vue-ssr-server-bundle.json");
// const clientManifest = require("./dist/client/vue-ssr-client-manifest.json");
// const renderer = createBundleRenderer(serverBundle, {
// runInNewContext: false,
// template: fs.readFileSync(path.resolve(__dirname, "./src/index.template.html"), "utf-8"),
// clientManifest,
// });
function renderToString(context) {
return new Promise((resolve, reject) => {
renderer.renderToString(context, (err, html) => {
err ? reject(err) : resolve(html);
});
});
}
app.use('/js', express.static('./dist/client/js'));
app.use('/css', express.static('./dist/client/css'));
app.get('/api/:id', (req, res) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
const { id } = req.params
const result = {
code: 200,
data: id,
msg: '请求成功'
}
res.setHeader("Content-Type", "application/json;charset=utf-8")
res.setHeader("Server", "Sli97")
res.json(result);
res.end()
})
app.get('/test', (req, res) => {
console.log(req.query)
const { id } = req.query
console.log(id)
res.setHeader("Content-Type", "application/json;charset=utf-8")
res.setHeader("Server", "Sli97")
res.send("test :" + id)
})
// app.get('*', async (req, res) => {
// const context = { url: req.url, title: "Hello SSR", }
// // 将 context 数据渲染为 HTML
// const html = await renderToString(context).catch(err => {
// if (err.url) {
// res.redirect(err.url)
// } else if (err.code === 404) {
// res.status(404).send('404 | Page Not Found')
// } else {
// // Render Error Page or Redirect
// res.status(500).send('500 | Internal Server Error')
// console.error(`error during render : ${req.url}`)
// console.error(err.stack)
// }
// });
// res.send(html)
// })
/*服务启动*/
const port = 3000;
app.listen(port, () => {
console.log(`server started at localhost:${port}`);
});
package.json脚本命令
"scripts": {
"serve": "cross-env RUN_ENV=client vue-cli-service serve ",
"start": "npm run build:server && npm run build:client && npm run server",
"build:client": "cross-env RUN_ENV=client vue-cli-service build",
"build:server": "cross-env RUN_ENV=server vue-cli-service build",
"server": "node server",
"test": "echo \"Error: no test specified\" && exit 1"
},