ibcadmin 发表于 2019-8-13 18:15:53

实现ssr服务端渲染demo

最近在研究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: '.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: '.?'                  }                }            }      ]    },    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}}
             -----图片分割线----
      http://ibcibc.com/logo
      http://ibcibc.com/../assets/images/kfbg.png
   
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文件
                Document   
    index.ssr.html server模板html文件
                Document    编写服务端渲染主体逻辑

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
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 实现ssr服务端渲染demo