最近在研究SSR服务器端渲染,自己写了的小demo。
项目布局
- ├── build // 配置文件│ │──webpack.base // 公共配置│ │──webpack.client // 生成Client Bundle的配置│ │── webpack.server // 生成Server Bundle的配置├── dist // 项目打包路径├── public // 模板文件│ │──index.html // Client模板html文件│ │──index.ssr.html // Server模板html文件├── src // 源码目录│ ├── assets // 图片目录│ ├── components // 组件│ │ ├── Bar.vue // Bar测试组件│ │ ├── Foo.vue // Foo测试组件│ │── App.vue // Vue应用的根组件│ │── main.js // 入口基础文件│ ├── client-entry.js // 浏览器环境入口│ ├── server-entry.js // 服务器环境入口│ │ ├── router.js // 路由配置│ │ ├── store.js // vuex的状态管理├── favicon.ico // 图标
复制代码 注:以防版本不对应产生的问题。package.json我也把放出来了,不过在文章的最后面
上图是Vue官方的SSR原理先容图片。从这张图片,我们可以知道:我们需要通过Webpack打包生成两份bundle文件:
- Client Bundle,给浏览器用。和纯Vue前端项目Bundle类似
- Server Bundle,供服务端SSR使用,一个json文件
技能栈
vue + vuex + vue-router + webpack +ES6/7 + less + koa
拆分 Webpack 打包配置
构建文件目录
webpack.base.js 是公共配置,配置如下:- // 基础的webpack配置// webpack专用配置const path = require('path')const VueLoader = require('vue-loader/lib/plugin')const resolve = dir => { return path.resolve(__dirname, dir)}module.exports = { output: { filename: '[name].bundle.js', path: resolve('../dist') }, resolve: { extensions: ['.js', '.vue'] }, module: { rules: [{ test: /\.js$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } }, exclude: /node_modules/ }, { test: /\.css$/, use: ['vue-style-loader', 'css-loader'] }, { test: /\.vue$/, use: 'vue-loader' }, { test: /\.less$/, loader: 'vue-style-loader!css-loader!less-loader' }, { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, use: { loader: 'url-loader', options: { limit: 300000, name: '[name].[ext]?[hash]' } } } ] }, plugins: [ new VueLoader() ]}
复制代码 webpack.client.js 是生成Client Bundle的配置,配置如下:- const merge = require('webpack-merge')const base = require('./webpack.base')const path = require('path')const resolve = dir => { return path.resolve(__dirname, dir)}const ClientRenderPlugin = require('vue-server-renderer/client-plugin')const HtmlWebpackPlugin = require('html-webpack-plugin')module.exports = merge(base, { entry: { client: resolve('../src/client-entry.js') }, plugins: [ new ClientRenderPlugin(), new HtmlWebpackPlugin({ filename: 'index.html', template: resolve('../public/index.html') }) ]})
复制代码 webpack.server.js是生成Server Bundle的配置,配置如下:- const merge = require('webpack-merge')const base = require('./webpack.base')const path = require('path')const resolve = dir => { return path.resolve(__dirname, dir)}const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')const HtmlWebpackPlugin = require('html-webpack-plugin')module.exports = merge(base, { entry: { server: resolve('../src/server-entry.js') }, target: 'node', // 用给node来使用 // devtool: 'source-map', output: { libraryTarget: 'commonjs2' }, plugins: [ new VueSSRServerPlugin(), new HtmlWebpackPlugin({ filename: 'index.ssr.html', template: resolve('../public/index.ssr.html'), excludeChunks: ['server'] // 排查某个模块 }), ]})
复制代码 下图是我的项目文件目录
components 目录下是组件
App.vue Vue应用的根组件
client-entry.js 浏览器环境入口
server-entry.js 服务器环境入口
main.js 入口基础文件
router.js 路由配置文件
store.js vuex状态管理文件
前端渲染 Demo
前端渲染demo部分比较简单,就包含两个组件:Foo 和 Ba
Foo.vue- Foo--{{num}}-点击测试js是否正常
- {{this.$store.state.name}}
- -----图片分割线----
- [align=center][img]http://ibcibc.com/logo[/img][/align]
- [align=center][img]http://ibcibc.com/../assets/images/kfbg.png[/img][/align]
-
复制代码 Bar.vue- bar
- Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。
复制代码 App.vue- foo Bar
-
- .nav{ text-align: center; display: flex; align-items: center; a{ flex: 2; background: #f5f5f5; text-decoration:none; color: #333; &.currentClass{ background:#f43553; color: #fff; } } }
复制代码 router.js- import Vue from 'vue'import Foo from './components/Foo.vue'import VueRouter from 'vue-router'Vue.use(VueRouter)export default () => { const router = new VueRouter({ mode: 'history', routes: [ { path: '/', component: Foo }, { path: '/bar', component: () => import('./components/Bar.vue') }, ] }) return router}
复制代码 store.js- import Vue from 'vue'import Vuex from 'vuex'Vue.use(Vuex)export default () => { const store = new Vuex.Store({ state: { name: '' }, mutations: { changeName(state) { state.name = 'yxf' } }, actions: { changeName({ commit }) { return new Promise((resolve, reject) => { setTimeout(() => { commit('changeName') resolve() }) }) } } }) if(typeof window !== 'undefined' && window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__) } return store}
复制代码 拆分 JS 入口
在前端渲染的时候,只需要一个入口 main.js。现在要做后端渲染,就得有两个 JS 文件:client-entry.js 和 server-entry.js 分别作为浏览器和服务器的入口。
main.js基础文件- //入口文件import Vue from 'vue'import createRouter from './router'import App from './App.vue'import createStore from './store'import { sync } from 'vuex-router-sync' // 把当前VueRouter状态同步到Vuex中export default () => { const router = createRouter() const store = createStore() sync(store, router) const app = new Vue({ router, store, render: h => h(App) }) return { app, router, store }}
复制代码 client-entry.js 浏览器入口- import createApp from './main'const { app, router } = createApp()router.onReady(() => { app.$mount('#app')})
复制代码 server-entry.js 服务器入口- import createApp from './main'// 服务器需要调用当前这个文件产生一个vue实例export default context => { // 涉及到异步组件的问题 return new Promise((resolve, reject) => { const { app, router, store } = createApp() // 设置路由 router.push(context.url) // 返回的实例应跳转到 / 如/bar router.onReady(() => { const matchs = router.getMatchedComponents() console.log(matchs.length) if(matchs.length === 0) { reject({ code: 404 }) } // matchs匹配到所有的组件,整个都在服务端执行的 Promise.all( matchs.map(component => { if(component.asyncData) { // asyncData 是在服务端调用的 return component.asyncData(store) } }) ).then(() => { // 以上all中的方法,会改变store中的state context.state = store.state;// 把vuex的状态挂载到上下文中,会将状态挂到window上 resolve(app) }).catch(reject) },reject) })}// 服务器端配置好后,需要导出给node使用
复制代码 模板文件
index.html client模板html文件index.ssr.html server模板html文件编写服务端渲染主体逻辑
Vue SSR 依赖于包 vue-server-render,它的调用支持两种入口格式:createRenderer 和 createBundleRenderer,前者以 Vue 组件为入口,后者以打包后的 JS 文件为入口,本文接纳后者。
server.js- const Koa = require('koa')const Router = require('koa-router')const server = new Koa()const router = new Router()const path = require('path')const static = require('koa-static')const fs = require('fs')const { createBundleRenderer } = require('vue-server-renderer')const serverBundle = require('./dist/vue-ssr-server-bundle.json')//渲染打包后的结果const template = fs.readFileSync(path.resolve(__dirname, './dist/index.ssr.html'), 'utf8')//客户端manifest.jsonconst clientManifest = require('./dist/vue-ssr-client-manifest.json')const render = createBundleRenderer(serverBundle, { template, // 模板里必须要有 vue-ssr-outlet clientManifest})router.get('/',async ctx => { ctx.body = await new Promise((resolve, reject) => { render.renderToString({url: '/'}, (err, data) => { if(err) reject(err); resolve(data); }) }) })server.use(router.routes())// koa 静态服务中间件server.use(static(path.resolve(__dirname,'./dist')))server.use( async ctx => { try{ ctx.body = await new Promise((resolve, reject) => { render.renderToString({ url: ctx.url }, (err, data) => { if(err) reject(err) resolve(data) }) }) }catch (e) { ctx.body = '404' }}) server.listen(3002, () => { console.log('服务器已启动!') })
复制代码 项目地点:https://github.com/xiaonizi66/vue-ssr-demo
package.json- { "name": "ssr", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo "Error: no test specified" && exit 1", "client:dev": "webpack-dev-server --config ./build/webpack.client.js --mode development", "client:build": "webpack --config ./build/webpack.client.js --mode production", "server:build": "webpack --config ./build/webpack.server.js --mode production" }, "author": "", "license": "ISC", "dependencies": { "koa": "^2.7.0", "koa-router": "^7.4.0", "koa-static": "^5.0.0", "vue": "^2.6.10", "vue-loader": "^15.7.1", "vue-router": "^3.1.2", "vue-server-renderer": "^2.6.10", "vuex": "^3.1.1", "vuex-router-sync": "^5.0.0" }, "devDependencies": { "@babel/core": "^7.5.5", "@babel/preset-env": "^7.5.5", "babel-loader": "^8.0.6", "css-loader": "^3.2.0", "html-webpack-plugin": "^3.2.0", "less": "^3.9.0", "less-loader": "^5.0.0", "url-loader": "^2.1.0", "vue-style-loader": "^4.1.2", "vue-template-compiler": "^2.6.10", "webpack": "^4.39.1", "webpack-cli": "^3.3.6", "webpack-dev-server": "^3.8.0", "webpack-merge": "^4.2.1" }}
复制代码 最终渲染效果:
项目运行
git clone https://github.com/xiaonizi66/vue-ssr-demo
npm install
npm run server:build
npm run cilent:build
nodemon server.js
也可在build后面加上 -- --watch 如:npm run server:build -- --watch 用来监听
来源:https://www.cnblogs.com/yanxiafei/archive/2019/08/13/11346829.html
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |