vue 博客优化,服务端渲染(SSR)指南

对已有的单页应用进行改造,优化,使之成为一个具备良好seo的应用

github地址 WdBly博客地址javascript

安装

git clone https://github.com/WdBly/my-blog-WdBly.git
复制代码

服务端配置:

cd server && composer install
配置.env文件 包含了SESSION_DOMAIN APP_KEY DATABASE的相关信息
配置目录权限
配置nginx 参考下方配置
复制代码

客户端配置

cd web && npm install
npm run build-client && npm run build-server
复制代码

启动客户端

node ssr
复制代码

后台启动 须要pm2

npm install pm2 -g
pm2 start ssr
复制代码

优化进度

  • [x] webpack开启vendor分包,压缩代码,异步路由组件
  • [x] nginx gzip
  • [x] 服务端渲染
  • [x] cookie转发
  • [x] 图片cdn
  • [x] 部分https

Vue 项目改造 - 服务端渲染

几个问题:使用服务端渲染解决了什么问题?,技术上如何实现? 通过服务端渲染改造的项目和改造前的单页的区别?css

场景:已有基于vue-cli的单页博客项目,前端使用 vue+vue-router+vuex+axios+elementui+webpack,后台使用laravel + mysql ,服务器阿里云 Ubuntu 16.04,web服务器nginx。html

面临的问题。1:单页应用首屏加载过慢;2:没法被搜索引擎抓取;3:首屏白屏时间过长(重要);前端

解决一:首屏加载过慢。 通过分析,页面首屏慢主要是首次须要加载的js文件过大。vue

1:对webpack打包过程进行优化,采用多入口将项目的vender依赖分割,对不须要变更的文件作缓存,同时对路由作异步加载。java

//多个入口
    entry: {
        "app": path.join(projectRoot, 'entry-client.js'),
        "vendor": ['vue', 'vue-router', 'vuex', 'element-ui']
    }

    //对多入口的文件 单独打包 而且不开启hash保证缓存
    new webpack.optimize.CommonsChunkPlugin({
        name: 'vendor',
        filename: 'client/[name].bundle.js',
    })

    //js压缩 缩减文件大小
    new UglifyJsPlugin({
        uglifyOptions: {
            compress: {
                warnings: false
            }
        },
        sourceMap: true,
        parallel: true
    })

    //异步的路由 使得首屏加载的代码尽可能小
    const Home = ()=> import('@/components/Home.vue');

复制代码

2:服务端nginx开启gzip压缩node

gzip on;
 gzip_disable "msie6";
 gzip_vary on;
 gzip_proxied any;
 gzip_comp_level 6;
 gzip_buffers 16 8k;
 gzip_http_version 1.1;
 gzip_types text/plain text/css application/json application/javascript 
 text/xml application/xml application/xml+rss text/javascript;
复制代码

通过验证 开启gzip压缩的js文件大小大概能缩减为源文件的1/5mysql

经过前面两部的优化,首屏加载快了很多,但仍是有点慢。webpack

解决二三:SEO和白屏的处理ios

开启服务端的渲染

首先咱们来捋一捋实现流程,传统的单页应用的流程为前端将文件打包后生成了index.html文件和其余依赖文件,index.html文件中引入了一些js文件和css文件。以下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8>
        <meta http-equiv=X-UA-Compatible content="IE=edge">
        <link href=b12acbf1c.css rel=stylesheet>
    </head>
    <body>
        <div id=app></div>
        <script type=text/javascript src=982.js></script>
    </body>
</html>
复制代码

nginx中配置相应的server_name和root字段,两个路由分别对应前端页面和后台接口。

//前端页面的路由
server {
    root /www/wwwroot/myblog-WdBly/web/dist
    server_name www.wddsss.com
    ...
}
//后台接口路由
server {
    root /www/wwwroot/myblog-WdBly/server/public
    server_name api.wddsss.com
    ...
}
复制代码

用户访问前端路由www.wddsss.com时 会返回dist目录下的index.html文件给浏览器。剩下的全部工做都由浏览器完成。

如今咱们须要在nginx服务端和浏览器之间开启一个node中间层用于服务端渲染。

理想状态是 当用户访问 www.wddsss.com时,nginx经过proxy_pass将访问流量代理到node中间层监听的端口,而不是直接返回一个index.html文件,nginx将后面的返回页面的工做交给了node。nginx代理配置:

//node监听了 5006端口,注意咱们并不对外部暴露5006端口,也就是说经过
//www.wddsss.com:5006的访问是会失败的。

upstream z.com {
   server 127.0.0.1:5006;
}

//proxy_pass 将访问转移到 127.0.0.1:5006
server {
    server_name www.wddsss.com;
    access_log  /var/log/nginx/blog.api.access.log;
    error_log  /var/log/nginx/blog.api.error.log;
    location / {
        proxy_pass http://z.com;
    }
}

复制代码

如今的任务清晰了很多,咱们须要开启一个node服务,监听一个端口,当有用户访问的时候完成一大波事情,最后须要返回一个充满数据的html文件。

那么咱们开始实现这个任务吧!,首先分析,用户访问www.wddsss.com/app/home这个路由时,咱们在node中监听到访问,必然须要将此路由对应的组件,以及组件中须要的数据获取并整合造成html文件。

vue-server-renderer 提供了一个renderToString方法,此方法接受一个Vue组件,返回一段对应的html代码。这不就解决了咱们的问题嘛。

从新整理思路,node在监听到某个路由被访问时,会去查找前端路由表,并找到对应的组件。对于某些须要ssr的组件,咱们手动为其添加了一个asyncData()方法,在node加载这些组件同时会去执行asyncData()方法,拿到组件内的数据渲染到组件中。最后将这个组件传入renderToString方法。这样一个简陋无比的ssr就作好了!部分代码以下(删减版,完整的请前往github查看)

const { createRenderer } = require('vue-server-renderer')
const createApp = require('./dist/bundle.server.js')['default']
const renderer = createRenderer({
    template: require('fs').readFileSync('ssr/view/index.template.html', 'utf-8')
})
const data = {
  script: ` <script src="/client/vendor.bundle.js"></script> <script src="/client/app.client.js"></script> `,
  state: ``
}

express.get('*', (req, res) => {

    const context = {url: req.url};
    
    createApp(context).then(app => {
        var state = JSON.stringify(context.state);

        data.state =  `<script>window.__INITIAL_STATE__ = ${state}</script>`
    
        renderer.renderToString(app, data, (err, html) => {
	    res.end(html)
        })
    })
})

复制代码

1:咱们能够看到createRenderer方法能够接受一个html模板文件,由于renderToString方法最终生成的html片断时不带head头等内容的,须要咱们自定义一个模板,将renderToString方法生成的html插入到次模板html文件便可。 2:createApp变量来自一个bundle.server.js,这个js文件是经过特定的配置对项目打包后生成的入口文件,它接受一个context用于根据路由寻找对应组件并将最终的state添加至context,后面会说到。 3:createApp自己是一个异步过程,由于在这个函数中可能会存在数据的获取,当数据获取且组件加载完毕后,执行then中的renderToString,renderToString的第二个data参数便是传入模板html文件的参数。

webpack.server.js部分配置

target: 'node',
    entry: [path.join(projectRoot, 'entry-server.js')],
    output: {
        libraryTarget: 'commonjs2',
        path: path.join(projectRoot, 'dist'),
        filename: 'bundle.server.js',
        chunkFilename: '[name].bundle.js',
        publicPath: "/"
    }

复制代码

这个比较简陋的ssr存在不少问题,主要是客户端拿到的只是一个单独的html页面其中,咱们绑定的事件,通通是不生效的。

正确的流程是当客户端拿到首屏渲染好的页面时,会在浏览器后台执行一次重绘,生成一系列的虚拟Node,而且和从服务端获取的真实dom节点进行比对,如果不匹配,会执行重绘(使用浏览器端生成的页面),而浏览器在后台生成虚拟Node依赖于页面中的数据,而咱们又不可能在浏览器再次发送ajax请求来获取页面数据(浪费),全部在上方代码中咱们能够看到window.__INITIAL_STATE__这一句,当服务端获取到组件的数据时,会将state放在script标签的一个变量中,在客户端执行重绘时采用的便是这里的数据。

具体过程参考下图:

image.png

image.png

在整个过程当中产生的一些问题总结

element-ui样式丢失问题。

在 app.js 中咱们引入了 'element-ui/lib/theme-chalk/index.css'的css文件,咱们必需要清楚 app.js自己会在服务端执行,因此咱们必须在webpack.server.js中配置处理css文件的loader

{
        test:/\.css$/,
        use:['vue-style-loader', 'css-loader'],
    }
复制代码

区分是当前执行环境时node仍是浏览器

由于咱们的项目是要在服务端执行,同时也会在客户端执行,到时服务端不支持某些客户端对象 如window对象,因此在咱们的代码中若是有使用到window,document等浏览器API的地方须要对当前的执行环境进行判断 咱们使用的方式是webpack的插件

new webpack.DefinePlugin({
            'process.env.VUE_ENV': '"client"',
            'process.env.NODE_ENV': '"production"',
       }),
       new webpack.DefinePlugin({
            'process.env.VUE_ENV': '"server"',
            'process.env.NODE_ENV': '"production"',
       }),
复制代码

某些页面与用户是否登录相关

对于某些页面,咱们可能会在页面中显示当前登录用户的信息,并将这个信息存入了localStorage中,但结合上个问题咱们能够看出来,在服务端渲染中咱们并不能获取到这个localStorage这个对象。那么最终渲染出的页面在和浏览器重绘的页面进行对比时必然会出现不匹配的错误。 处理方法,在node层为登录用户设置cookie,当用户请求时,如果判断出当前的执行环境为node,则从cookie中读取信息载入页面,不然从localStorage读取数据。

两次渲染不匹配问题

除了上述状况可能致使两个渲染不匹配,还有从服务端返给客户端的__INITIAL_STATE__不存在或者__INITIAL_STATE__的内容有误时,都会致使客户端获取不到初始__INITIAL_STATE__而发生不匹配的错误(这里__INITIAL_STATE__不存在的状况有不少种), 1:如果直接将 作为参数传入index.template.html,那么须要使用{{{ }}}的语法解析。 2:在index.template.html 引入state的标签须要在引入build.client.js的标签以前引入,由于build.client.js须要依赖初始state。 3:如果state中存在标签(好比mackdown语法生成的dom结构)须要使用不转义插值{{{}}}。 4: 如果state中有某些特殊字符 :: 回车等特殊字符,须要使用{{}}进行转移,不然在渲染页面时这部分state会直接跑到页面上去。

cookie问题

laravel设置的cookie配置 .env 文件 SESSION_DOMAIN=.wddsss.com 这样咱们的node层(www.wddsss.com)才能获取到laravel的cookie,并在客户端将登录时将cookie保存下来,同时返回给客户端。这样用户在刷新页面时,node会经过axios并带上cookie请求nginx服务器获取数据。

图片走七牛cdn

咱们的首页图片加载能够明细看到很慢(毕竟1M的小服务器),开启七牛cdn,具体流程就不说了,使用的是laravel itbdw/laravel-storage-qiniu包。

开启https

到这个时候咱们发现网站被标记为不安全了,https走一波,推荐一个免费证书申请机构。

certbot证书申请

相关文章
相关标签/搜索