从零开始搭建一个vue-ssr

栏目: JavaScript · 发布时间: 6年前

内容简介:SSR全拼是Server-Side Rendering,服务端渲染。所谓服务端渲染,指的是把vue组件在服务器端渲染为组装好的HTML字符串,然后将它们直接发送到浏览器,最后需要将这些静态标记混合在客户端上完全可交互的应用程序。①满足seo需求,传统的spa数据都是异步加载的,爬虫引擎无法加载,需要利用ssr将数据直出渲染在页面源代码中。

从零开始搭建一个vue-ssr

背景

What?SSR是什么?

SSR全拼是Server-Side Rendering,服务端渲染。

所谓服务端渲染,指的是把vue组件在服务器端渲染为组装好的HTML字符串,然后将它们直接发送到浏览器,最后需要将这些静态标记混合在客户端上完全可交互的应用程序。

Why?为什么选择SSR?

①满足seo需求,传统的spa数据都是异步加载的,爬虫引擎无法加载,需要利用ssr将数据直出渲染在页面源代码中。

②更宽的内容达到时间(首屏加载更快),当请求页面的时候,服务端渲染完数据之后,把渲染好的页面直接发送给浏览器,并进行渲染。浏览器只需要解析html不需要去解析js。

How?SSR的原理

借用下面的一张图,我们来简单阐述一下vue-ssr的原理。

从零开始搭建一个vue-ssr

我们可以看到,左侧Source部分就是我们所编写的源代码,所有代码有一个公共入口,就是app.js,紧接着就是服务端的入口

(entry-server.js)和客户端的入口(entry-client.js)。当完成所有源代码的编写之后,我们通过webpack的构建,打包出两个bundle,分别是server bundle和client bundle;当用户进行页面访问的时候,先是经过服务端的入口,将vue组建组装为html字符串,并混入客户端所访问的html模板中,最终就完成了整个ssr渲染的过程。

开始搭建

创建一个空白目录并初始化

在终端输入以下命令

mkdir ssr-demo
cd ssr-demo
npm init

由于我们这个只是一个demo项目,可以直接一路按回车键,直接忽略配置。

完成之后我们可以看到文件夹里面有一个package.json的文件,这就是配置表。

安装依赖

该项目需要四个依赖,依次安装

npm install express
npm install vue
npm install vue-router
npm install vue-server-renderer

其中express使我们node端的框架,vue用于创建vue实例,vue-router则用于实现路由控制,最后vue-server-renderer尤为关键,我们实现的vue-ssr依靠于这个库提供的API。

在安装依赖完毕之后,我们看到package.json中已经把四个依赖都写上了。

"express": "^4.17.1",
"vue": "^2.6.10",
"vue-router": "^3.0.6",
"vue-server-renderer": "^2.6.10"

创建一个node服务

在根目录下我们新建一个server.js,用户搭建node服务

const express = require("express");
const app = express();

app.get('*', (request, response) => {
    response.end('hello, ssr');
})

app.listen(3001, () => {
    console.log('服务已开启')
})

接着为了后续开发的便利,我们在package.json中添加一个启动命令:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "server": "node index.js"
 },

接着我们在终端输入 npm run server,然后再浏览器输入localhost:3001,便可以看到页面中的文字被成功渲染。

渲染html页面

在上一步我们已经能成功渲染出一个文字,但是ssr并不是主要为了渲染文字,而是渲染一个html模板。

那么,接下来,我们得告知浏览器,我们需要渲染的是html,而不只是text,因此我们需要修改响应头。

同时,引入vue-server-renderer中的createRenderer对象,有一个renderToString的方法,可以将vue实例转成html的形式。(renderToString这个方法接受的第一个参数是vue的实例,第二个参数是一个回调函数,如果不想使用回调函数的话,这个方法也返回了一个Promise对象,当方法执行成功之后,会在then函数里面返回html结构。)

修改server.js如下:

const express = require("express");
const app = express();
const Vue = require("vue");
const vueServerRender = require("vue-server-renderer").createRenderer();

app.get('*', (request, response) => {
    const vueApp = new Vue({
        data:{
           message: "hello, ssr"
        },
        template: `<h1>{{message}}</h1>`
    });

    response.status(200);
    response.setHeader("Content-type", "text/html;charset-utf-8");
    vueServerRender.renderToString(vueApp).then((html) => {
        response.end(html);
    }).catch(err => console.log(err))
})

app.listen(3001, () => {
    console.log('服务已开启')
})

保存代码,重启服务,然后重新刷新页面。我们发现,页面好像没什么不同,就是字体变粗了而已。其实并不是,你可以尝试查看页面源代码,我们发现在源代码中,已经存在一个标签对h1,这就是html模板的雏形。同时,细心的同学还会发现,h1上面有一个属性:

data-server-rendered="true",那这个属性是干什么的呢?这个是一个标记,表明这个页面是由vue-ssr渲染而来的。大家不妨可以打开一些seo页面或者一些公司的网站,查看源代码,你会发现,也是有这个标记。

虽然h1标签对被成功渲染,但是我们发现这个html页面并不完整, 他缺少了文档声明,html标签,body标签,title标签等。

将Vue实例挂载进html模板中

创建一个index.html,用于挂载Vue实例。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Hello, SSR</title>
</head>
<body>
    <!--vue-ssr-outlet-->
</body>
</html>

注意,body中的注释不能去掉,这是Vue挂载的占位符。

然后修改server.js,将html模板引进去。这里我们在createRenderer函数可以接收一个对象作为配置参数。配置参数中有一项为template,这项配置的就是我们即将使用的Html模板。这个接收的不是一个单纯的路径,我们需要使用fs模块将html模板读取出来。

let path = require("path");
const vueServerRender = require("vue-server-renderer").createRenderer({
    template:require("fs").readFileSync(path.join(__dirname,"./index.html"),"utf-8")
});

保存代码,重启服务,然后重新刷新页面。我们查看源代码,发现,已经能成功渲染出一个完整的页面了。

创建一个Vue项目的开发目录

上面的开发模式,很显然只是一个demo而已,接下来我们模拟一下正常的vue开发的目录结构。

创建一个src文件夹,里面有一个router文件夹,再有一个index,js用作路由,并创建一个app.js,用作vue的入口,如下图:

从零开始搭建一个vue-ssr

修改router/index.js

const vueRouter = require("vue-router");
const Vue = require("vue");

Vue.use(vueRouter);

module.exports = () => {
    return new vueRouter({
        mode:"history",
        routes:[
            {
                path:"/",
                component:{
                    template:`<h1>this is home page</h1>`
                },
                name:"home"
            },
            {
                path:"/about",
                component:{
                    template:`<h1>this is about page</h1>`
                },
                name:"about"
            }
        ]
    })
}

修改app.js

const Vue = require("vue");
const createRouter = require("./router")

module.exports = (context) => {
    const router = createRouter();
    return new Vue({
        router,
        data:{
            message:"Hello,Vue SSR!",
        },
        template:`
            <div>
                <h1>{{message}}</h1>
                <ul>
                    <li>
                        <router-link to="/">home</router-link>
                    </li>
                    <li>
                        <router-link to="/about">about</router-link>
                    </li>
                </ul>
                <router-view></router-view>
            </div>
        ` 
    });
}

然后在server.js中,将app.js引入

const express = require("express");
const app = express();
const vueApp = require('./src/app.js');

let path = require("path");
const vueServerRender = require("vue-server-renderer").createRenderer({
    template:require("fs").readFileSync(path.join(__dirname,"./index.html"),"utf-8")
});

app.get('*', (request, response) => {
    let vm = vueApp({});

    response.status(200);
    response.setHeader("Content-type", "text/html;charset-utf-8");

    vueServerRender.renderToString(vm).then((html) => {
        response.end(html);
    }).catch(err => console.log(err))
})

app.listen(3001, () => {
    console.log('服务已开启')
})

保存代码,重启服务,然后重新刷新页面。然后我们可以看到浏览器的路由已经被成功渲染了,但是无论怎么点击都没反应,浏览器的url有更改,但是页面内容不变。

这是因为我们只是将页面渲染的工作交给服务端,而页面路由切换,还是在前端执行,服务端并未能接收到该指令,因此无论怎么切换路由,服务端渲染出来的页面根本没变化。

实现服务端控制页面路由

在src中创建一个entry-server.js文件,该文件为服务端入口文件,接收app和router实例:

const createApp = require("./app.js");

module.exports = (context) => {
    return new Promise(async (reslove,reject) => {
        let {url} = context;

        let {app,router} = createApp(context);
        router.push(url);
        //  router回调函数
        //  当所有异步请求完成之后就会触发
        router.onReady(() => {
            let matchedComponents = router.getMatchedComponents();
            if(!matchedComponents.length){
                return reject();
            }
            reslove(app);
        },reject)
    })
}

在src中创建一个entry-client.js文件,该文件为客户端入口,负责将路由挂载到app里面。

const createApp = require("./app.js");
let {app,router} = createApp({});

router.onReady(() => {
    app.$mount("#app")
});

修改app.js,将router和vue实例暴露出去

const Vue = require("vue");
const createRouter = require("./router")

module.exports = (context) => {
    const router = createRouter();
    const app =  new Vue({
        router,
        data:{
            message:"Hello,Vue SSR!",
        },
        template:`
            <div>
                <h1>{{message}}</h1>
                <ul>
                    <li>
                        <router-link to="/">home</router-link>
                    </li>
                    <li>
                        <router-link to="/about">about</router-link>
                    </li>
                </ul>
                <router-view></router-view>
            </div>
        ` 
    });
    return {
        app,
        router
    }
}

最终修改server.js

const express = require("express");
const app = express();

const App = require('./src/entry-server.js');

let path = require("path");
const vueServerRender = require("vue-server-renderer").createRenderer({
    template:require("fs").readFileSync(path.join(__dirname,"./index.html"),"utf-8")
});

app.get('*', async(request, response) => {

    response.status(200);
    response.setHeader("Content-type", "text/html;charset-utf-8");

    let {url} = request;
    let vm;
    vm = await App({url})
    vueServerRender.renderToString(vm).then((html) => {
        response.end(html);
    }).catch(err => console.log(err))
})

app.listen(3001, () => {
    console.log('服务已开启')
})

保存代码,重启服务,然后重新刷新页面。这时候,我们发现页面的路由切换生效了,并且不同页面的源代码也不一样了。

数据传递

既然是服务端渲染,数据的接收也是来源于服务端,那怎样才能把服务端接收到的数据传输给前端,然后进行渲染呢?

修改entry-server.js,进行同步或者异步获取数据

const createApp = require("./app.js");

const getData = function(){
    return new Promise((reslove, reject) => {
        let str = 'this is a async data!';
        reslove(str);
    })
}

module.exports = (context) => {
    return new Promise(async (reslove,reject) => {
        let {url} = context;

        // 数据传递
        context.propsData = 'this is a data from props!'

        context.asyncData = await getData();

        let {app,router} = createApp(context);
        router.push(url);
        //  router回调函数
        //  当所有异步请求完成之后就会触发
        router.onReady(() => {
            let matchedComponents = router.getMatchedComponents();
            if(!matchedComponents.length){
                return reject();
            }
            reslove(app);
        },reject)
    })
}

修改app.js,接收数据并渲染

const Vue = require("vue");
const createRouter = require("./router")

module.exports = (context) => {
    const router = createRouter();
    const app =  new Vue({
        router,
        data:{
            message:"Hello,Vue SSR!",
            propsData: context.propsData,
            asyncData: context.asyncData
        },
        template:`
            <div>
                <h1>{{message}}</h1>
                <p>{{asyncData}}</p>
                <p>{{propsData}}</p>
                <ul>
                    <li>
                        <router-link to="/">home</router-link>
                    </li>
                    <li>
                        <router-link to="/about">about</router-link>
                    </li>
                </ul>
                <router-view></router-view>
            </div>
        ` 
    });
    return {
        app,
        router
    }
}

最后我们可以看到无论是同步还是异步获取的数据,都能成功地通过服务端渲染,展示在页面源代码中。

另外,你也可以在server.js中的request中,将数据传递下去。

总结

实现了一个简易版本的vue-ssr,下期我们会依赖于vue-cli,进行webpack改造,实现一个通用且更实用的vue-ssr框架。

项目源码

https://github.com/TheWalking...


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Head First HTML5 Programming

Head First HTML5 Programming

Eric Freeman、Elisabeth Robson / O'Reilly Media / 2011-10-18 / USD 49.99

What can HTML5 do for you? If you're a web developer looking to use this new version of HTML, you might be wondering how much has really changed. Head First HTML5 Programming introduces the key featur......一起来看看 《Head First HTML5 Programming》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具