关于vue服务端渲染 1 构建配置

服务端渲染的本质是后端根据路由的情况在后端渲染这个组件的html代码,然后发送到前端,前端通过传来的 data-server-rendered=”true”标识符来得知这个html是由服务器渲染的,然后进行加载到自身进行管理。

在服务端渲染的时候,beforeCreate和created生命周期都是存在于服务端的,没有任何浏览器对象,如果在里面访问document或者window这种对象会在渲染的时候node抛出异常,其他周期则运行在客户端中。

这里使用vue的脚手架来初始化项目

通过 vue-cli 脚手架生成一个基础结构

vue init webpack ssrdemo
cnpm i

安装ssr需要的插件

cnpm i webpack-node-externals vue-server-renderer -D
  • webpack-node-externals:忽略打包工具,内联忽略node_modules文件件,可以为其指定白名单 whitelist让某类文件打包到包中
  • vue-server-renderer 为文件生成对应json的ssr插件

安装node服务器,官方示例中使用的是express 这里使用的是koa

cnpm i koa2 koa-router koa-static koa-bodyparser --save

因为服务器渲染需要针对客户端和服务器端分别打包,所以需要两个入口文件,并把入口main.js改造成一个工厂函数,如果包含vuex或者vue-router还需要对这个两个进行改变成工厂函数并通过改造后的main函数引用,之后进行同步

在src下新建 client.js server.js 两个入口js
改造router.js,因为在服务端每一次独立请求都应该返回一个全新的router对象,所以需要这个工厂函数,vuex也是一样

import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld.vue'
export function createRouter(){
    return new Router({
        mode:'history',
        routes:[
            {
                path:"/",
                component:HelloWorld
            }
        ]

    })
}

改造main.js

import Vue from 'vue'
import {createRouter} from '@/router/router.js'
//引入Router的工厂函数
import App from '@/App.vue'
export function createApp(){
    const router=createRouter()
    //生产router,如果需要vuex一样
    const app = new Vue{
        render:h=>h(App),
        router:router
    }
    return {app,router}
    //返回app和router
}

编写server.js

import {createApp} from '@/main.js'
//引用工厂函数
export default (context)=>{//获取上下文,上下文由服务器传入进来 @1
    return new Promise( (resolve,rejct)=>{
    //返回一个Promise
        const {url} = context;
        //通过上下文对象获取现在的url地址
        const {app,router}= createApp();
        //通过工厂函数拿到app和router
        router.push(url) //把地址传入router
        router.onReady(()=>{ //等待router跳转完成
            let matchedCs=router.getMatchedComponents; //匹配到的Components
            if(!mathchedCs.length){
                reject({code:404})
            }
            resolve(app) //成功返回app
        },reject)

    } )
}

编写client.js

import {createApp} from '@/main.js'
const {app,router} = createApp()
router.onReady(()=>{ //等待路由同步
    app.$mount("#app")//这里的#app应该是服务端通过App组件的第一个id="app"指定的

})

改造build里的base配置
把入口点改为client.js

entry:"./src/client.js"

改造prod配置,改造为客户端打包的配置文件,让他等同于 webpack.client.conf.js

//只显示更改
const vueSSRPlugin = require('vue-server-renderer/client-plugin')
module.exports=merge(baseConfig,{
    pluins:[
        //...其他插件
        //但是注释掉通过html模版生产html插入标签的plugin:HtmlWebpackPlugin
        //在webpack.DefinePlugin插件中新增加一个VUE_ENV
        new webpack.DefinePlugin({
            'process.env': env,
            'process.env.VUE_ENV': '"client"'
        }),
        new vueSSRPlugin()//增加SSR客户端插件
    ]

})

新建一个webpack.server.conf.js文件

const webpack = require('webpack')
const merge = require('webpack-merge') 
const baseConfig = require('./webpack.base.conf')
const nodeExternals = require('webpack-node-externals')
const VueSSRPlugin = require('vue-server-renderer/server-plugin') //服务端打包插件
const utils = require('./utils')  //之所以要引入这个和下面那个插件,是因为如果服务端需要用到css渲染的代码的时候需要依赖这些因为编译
const ExtractTextPlugin = require('extract-text-webpack-plugin')

module.exports=merge(baseConfig,{
  entry:'./src/server.js', //入口点为server.js
  target:'node',
  devtool:'source-map',
  output:{
    libraryTarget:'commonjs2'
  },
  externals:nodeExternals({ //选择性打包
    whitelist:/\.css$/
  }),
  plugins:[
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
      'process.env.VUE_ENV': '"server"' 
    }),
    new ExtractTextPlugin({
      filename: utils.assetsPath('css/[name].[contenthash].css'),
      // Setting the following option to `false` will not extract CSS from codesplit chunks.
      // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
      // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 
      // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
      allChunks: true,
    }),
    new VueSSRPlugin()
  ]
})

至此构建工作完成了,最后在package里添加打包命令:

    "build:client": "cross-env NODE_ENV=production node build/build.js", //用原有的打包命令打包客户端
    "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.conf.js --progress --hide-modules",
    "build": "rimraf dist && npm run build:client && npm run build:server",
    //rimraf和cross-env是前者一个清空指定目录的包后者是一个指定运行参数的工具,需要npm install 

配置服务器之前先建立一个服务端渲染所需要的template的html文件

<!DOCTYPE html>
<html>
<head>
  <title>{{title}}</title> //这里可以使用插值来定义模板,数据来源于下面服务器中传入的一个context上下文
</head>
<body>
  <!--vue-ssr-outlet-->  //不需要任何标签,只需要在这里加入这样一行注释
</body>
</html>

因为服务端根据bundle推算出html内容后插入到这个注释上面,然后客户端根据data-server-rendered=”true”和id来进行挂载的时候进行数据本地化,但是在dev的时候,如果使用了同样的模版会因为挂载$mount(‘#app’)但是找不到app标签导致为空
解决方法可以使用两套html模板,一个dev,一个服务端。或者参考官方例子来进行dev也使用服务端渲染的方式

进行服务器编写


const PORT=3000

const Koa=require('koa2')
const Router =require('koa-router')()
const bodyParser = require('koa-bodyparser')
const static = require('koa-static')
const fs =require('fs')
const path = require('path')
const {createBundleRenderer} = require('vue-server-renderer')

const serverBundle=require('./dist/vue-ssr-server-bundle.json') //加载webpack打包出来的服务端映射文件
const clientManifest = require('./dist/vue-ssr-client-manifest.json')//加载webpack打包出来的客户端映射文件
const render=createBundleRenderer(serverBundle,{
  template: fs.readFileSync('index.ssr.template.html','utf-8'), //上面定义的服务器渲染模版
  clientManifest:clientManifest,
  runInNewContext:false
})
const App = new Koa()
App.use(static(path.resolve(__dirname,'./dist'))) //匹配静态路径
App.use(bodyParser())
App.use(async (ctx,next)=>{
  console.log(ctx.req.url);//如果插入自己的中间件
  await next(); //一定要是async形式的异步不然会导致ctx.body先给页面返回了404,你才收到render好的数据
})
Router.get("*",async (ctx,next)=>{  
  const context={url:ctx.req.url,title:"自定义title"} //使用模版插值的title @1
  try{
    let value=await render.renderToString(context) 这个context上下文会由server.js接收
    ctx.body=value
  }catch(e){
    console.log(e);
    next()
  }

})
App.use(Router.routes())

App.listen(PORT,()=>{
  console.log('listen:'+PORT);

})

服务端渲染的构建工作宣告完成。

node ./app.js

猜你喜欢

转载自blog.csdn.net/weixin_42345308/article/details/81042559
今日推荐