nuxt.js实现服务端渲染ssr(环境配置、 多环境开发、进程守护、服务端镜像)

11,210 阅读10分钟

nuxt.js是一个基于 Vue.js 的轻量级应用框架,可用来创建服务端渲染 (SSR) 应用,也可充当静态站点引擎生成静态站点应用——nuxt官网

根据自己在这上方面一条路走到黑,摸滚带爬的踩坑经验,接下来我将从以下几个方面给各位接触到或者未来接触到这方面知识的码友进行全方位的讲解和剖析。

  1. 背景
  2. nuxt框架的概述
  3. 百度蜘蛛爬虫的机制
  4. nuxt初探(开发环境搭建)
  5. 深入nuxt ssr(服务端渲染)
  6. 多环境配置 (开发、测试、生产)
  7. Nginx反向代理
  8. pm2守护node进程配置
  9. 服务端docker容器部署前端工程
  10. 总结
背景

最近因公司业务需求,需要对商城官网做seo优化。因为第一版前端用的是纯vue写的,经Vue-cli集成的webpack打包还有代码压缩处理后生成一串js的静态文件。因百度蜘蛛无法对纯js的网页进行爬取收录。所以不利于网站的排名。紧接我们和领导开会拍板使用服务端渲染(以下用ssr指代),nuxt刚好是vue团队打造的基于vue的ssr的框架,简单易于上手,避免了前端攻城狮们自己用node.js搭建服务。但初始我们采用的是nuxt的静态化部署,nuxt generate ,这样可以解决一部分需求,但每次更新内容后都要前端手动执行命令打包。而且通过百度蜘蛛爬取趋势图显示这种方式并不理想。最后,我们还是在原来的基础上采用nuxt的第二种方式前端做ssr处理,上线后经百度蜘蛛爬取趋势图显示有很大程度的提高,利于seo,且官网被百度收录的页面也逐渐增加。由此告一段落。

nuxt框架的概述

nuxt作为一个框架,则集成了vue2vue-routevuexvue ssrvue-meta等组件/框架,其是用webpack、和vue-loaderbabel-loader来处理代码的自动化构建工作(打包、代码分层、压缩等) nuxt框架提供了两种部署方式: 1. 静态化部署(预渲染)-- 通过 nuxt generate 命令实现。该命令依据应用的路由配置将每一个路由静态化成为对应的 HTML 文件 2. ssr部署 -- 先通过nuxt build编译构建再通过nuxt start开启一个web服务 在服务端调取接口时,主要是用到了asyncData/fetch方法。使得我们可以在设置组件的数据之前能异步获取或处理数据。

asyncData方法会在组件(限于页面组件)每次加载之前被调用。它可以在服务端或路由更新之前被调用。 在这个方法被调用的时候,第一个参数被设定为当前页面的上下文对象,你可以利用 asyncData方法来获取数据,Nuxt.js 会将 asyncData 返回的数据融合组件 data 方法返回的数据一并返回给当前组件。

注意:由于asyncData方法是在组件 初始化 前被调用的,所以在方法内是没有办法通过 this 来引用组件的实例对象。

fetch 方法用于在渲染页面前填充应用的状态树(store)数据, 与 asyncData 方法类似,不同的是它不会设置组件的数据。 类型: Function 如果页面组件设置了 fetch 方法,它会在组件每次加载前被调用(在服务端或切换至目标路由之前)。fetch 方法的第一个参数是页面组件的上下文对象 context,我们可以用 fetch 方法来获取数据填充应用的状态树。为了让获取过程可以异步,你需要返回一个 Promise,Nuxt.js 会等这个 promise 完成后再渲染组件。

警告: 您无法在内部使用this获取组件实例,fetch是在组件初始化之前被调用

大概了解了这些知识就可以做ssr工作了,路由环境配置等可到nuxt中文官网 查阅

百度蜘蛛爬虫的机制

百度蜘蛛是百度搜索引擎的一个自动化程序,它会不断的访问收集互联网上的网页、文章、视频等,通过抓取链接来收录网站,计算网站的权重和排名。纯html等静态化网站对百度蜘蛛比较友好,且百度蜘蛛几乎不会爬取js动态的网站,如vue/react构建的且经webpack/gulp等构建工具压缩处理过的网站。百度蜘蛛爬取网站是从主站开始爬,一次根据网站暴露的内链依次往深层次爬取。meta的设置,以及网站TDK的优化,网站结构优化,外链,文章原创等同样对SEO有很大作用,但本文主要是从技术层面入手,则主要是针对网站内链的处理以及基于vue等现在技术流做ssr处理。

nuxt初探(开发环境搭建)

安装

我这里采用nuxt.js团队创建的脚手架creat-nuxt-app搭建。

注: 开发必备环境 npx (npm v5.2.0+)  node (v4.0+)

我的开发环境npm v5.5.1 node v8.9.1 win10 在终端输入以下命令

npx create-nuxt-app nuxt-demo

然后你会看到

nuxt-1.png nuxt-2.png 当执行命令

npm run dev

当你看到如下图恭喜你成功搭建起项目 nuxt-3.png 当你和后端交互时,为了解决跨域问题这边需要通过proxy利用本地node服务器做个反向代理 在nuxt.config.js中配置

module.exports= {
	modules: [
		'@nuxtjs/axios'
	],
	axios: {
		proxy: true, //开启代理
		credentials: true, //跨域请求需使用凭证
	},
	proxy: [
		['/api',{ 
			target: 'http://example.com/api', // (后端请求地址)
			changeOrigin: true,
			pathRewrite: {'^/api': ''}
		}]
	]
}

附上刚搭建完的nuxt.config.jspackage.json

// nuxt.config.js

module.exports = {
  mode: 'universal',
  /*
  ** Headers of the page
  */
  head: {
    title: process.env.npm_package_name || '',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: process.env.npm_package_description || '' }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ]
  },
  /*
  ** Customize the progress-bar color
  */
  loading: { color: '#fff' },
  /*
  ** Global CSS
  */
  css: [
    'element-ui/lib/theme-chalk/index.css'
  ],
  /*
  ** Plugins to load before mounting the App
  */
  plugins: [
    '@/plugins/element-ui'
  ],
  /*
  ** Nuxt.js dev-modules
  */
  buildModules: [
    // Doc: https://github.com/nuxt-community/eslint-module
    '@nuxtjs/eslint-module',
  ],
  /*
  ** Nuxt.js modules
  */
  modules: [
    // Doc: https://axios.nuxtjs.org/usage
    '@nuxtjs/axios',
    '@nuxtjs/pwa',
    // Doc: https://github.com/nuxt-community/dotenv-module
    '@nuxtjs/dotenv',
  ],
  /*
  ** Axios module configuration
  ** See https://axios.nuxtjs.org/options
  */
  axios: {
  },
  /*
  ** Build configuration
  */
  build: {
    transpile: [/^element-ui/],
    /*
    ** You can extend webpack config here
    */
    extend (config, ctx) {
    }
  }
}

//package.json
{
  "name": "nuxt-demo",
  "version": "1.0.0",
  "description": "for a nuxt demo about ssr",
  "author": "kevin xie",
  "private": true,
  "scripts": {
    "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
    "build": "nuxt build",
    "start": "cross-env NODE_ENV=production node server/index.js",
    "generate": "nuxt generate",
    "lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
    "test": "jest"
  },
  "dependencies": {
    "nuxt": "^2.0.0",
    "cross-env": "^5.2.0",
    "express": "^4.16.4",
    "element-ui": "^2.4.11",
    "@nuxtjs/axios": "^5.3.6",
    "@nuxtjs/pwa": "^3.0.0-0",
    "@nuxtjs/dotenv": "^1.4.0"
  },
  "devDependencies": {
    "nodemon": "^1.18.9",
    "@nuxtjs/eslint-config": "^2.0.0",
    "@nuxtjs/eslint-module": "^1.0.0",
    "babel-eslint": "^10.0.1",
    "eslint": "^6.1.0",
    "eslint-plugin-nuxt": ">=0.4.2",
    "@vue/test-utils": "^1.0.0-beta.27",
    "babel-jest": "^24.1.0",
    "jest": "^24.1.0",
    "vue-jest": "^4.0.0-0"
  }
}

深入nuxt ssr(服务端渲染)

如果你不需要做ssr处理的话直接执行nuxt generate则nuxt会为你生成静态化页面,然后再执行nuxt build 打包后将dist文件夹推上服务器就可以上线了,但这种seo不太友好但比之前vue构建好一点。 但要做ssr处理的话就需换种方式处理也就是nuxt提供的第二种方式,在服务器搭个node服务器然后直接在上面跑。这样的话,配置需要改,且与后台的请求也需要改,改为nuxt给我们提供asyncData/fetch。因为nuxt也是基于vue开发的,所以生命周期也一样,但nuxt在服务端中会触发beforeCreatecreated两个生命周期。其次,nuxt有自己一套服务器渲染流程。 nuxt-4.PNG

Nuxt.js 提供了几种不同的方法来使用 asyncData 方法,你可以选择自己熟悉的一种来用:

  1. 返回一个 Promise, nuxt.js会等待该Promise被解析之后才会设置组件的数据,从而渲染组件.
  2. 使用 asyncawait
  3. 使用回调函数
返回Promise
export default {
  asyncData ({ params }) {
    return axios.get(`https://my-api/posts/${params.id}`)
      .then((res) => {
        return { title: res.data.title }
      })
  }
}
使用async或await
export default {
  async asyncData ({ params }) {
    const { data } = await axios.get(`https://my-api/posts/${params.id}`)
    return { title: data.title }
  }
}
使用回调函数
export default {
  asyncData ({ params }, callback) {
    axios.get(`https://my-api/posts/${params.id}`)
      .then((res) => {
        callback(null, { title: res.data.title })
      })
  }
}

另外,还要注意一下在做登录处理等需要获取cookie或者服务端存储的session时,需要使用到vuex状态树,在状态树中有个方法非常有用 nuxtServerInit

actions: {
  nuxtServerInit ({ commit }, { req }) {
    if (req.session.user) {
      commit('user', req.session.user)
    }
  }
}

多环境配置 (开发、测试、生产)

因为在一个工程项目中我们都是有开发、测试、生产这样的验收流程的。而往往我们测试和生产所连接的数据库都是不同的,且请求域也不一样。因此为了开发方便我们就很需要多环境的配置。在spa中我们可以在package.json中通过cross-env这个node环境变量设置,但在ssr中这个在服务端请求时不生效,这里我当时找了很久google之类最后还是通过 @nuxtjs/dotenv解决了。还有特别注意到 @nuxtjs/axios中有提供环境变量API_URL来复写baseURLAPI_URL_BROWSER来复写browserBaseURL,这在多环境配置中很有用。 你可以在nuxt.config.js中引入并且在module中使用。

require('dotenv').config({path: '.env'})
module.exports={
	modules: [
		'@nuxtjs/axios',
		['@nuxtjs/dotenv', {
		   filename: process.env.NODE_ENV == 'production' ? '.env.prod' : process.env.NODE_ENV == 'test' ? '.env.test' : '.env.dev'
		}]
	]
}

同时你可以在根目录下配置相关环境配置文件。

.env.dev

NODE_ENV=development
API_URL_BROWSER=http://localhost:3000
API_URL=http://localhost:3000/api

.env.test

NODE_ENV=test
API_URL_BROWSER=http://test.example.com
API_URL=http://test.example.com/api

.env.prod

NODE_ENV=production
API_URL_BROWSER=https://www.example.com
API_URL=https://www.example.com/api

当你用axios做跨域请求时,你就可以自动根据环境给axios配置baseURL。

import axios from 'axios';
const axiosInstance = axios.create({
	timeout: 10000,
	baseURL: process.env.API_URL
})

在某些需要跳转的链接nuxt-linka链接中你可以声明一个变量对域名做统一处理。

export default {
	data () {
		return {
			BASE_PATH: process.env.API_URL_BROWSER
		}
	}
}

Nginx反向代理

在生成环境中,我需要使用到nginx做代理服务器,解决跨域。因为我们的项目的前后端可能部署在不同的服务上。

upstream nodenuxt {
    server 127.0.0.1:3000; #nuxt项目 监听端口
    keepalive 64;
}
server {
    listen 80;
    server_name https://www.example.com; #访问域名  
    location / {
        proxy_http_version 1.1;        
        proxy_set_header Upgrade $http_upgrade;  
        proxy_set_header Connection "upgrade";        
        proxy_set_header Host $host;        
        proxy_set_header X-Nginx-Proxy true;        
        proxy_cache_bypass $http_upgrade;        
        proxy_pass http://nodenuxt; #反向代理
    }
}

pm2守护node进程配置

当我们配置以上内容后在服务端执行npm start项目可以跑起来,但我们的项目总会挂掉的,为了使我们服务进程常驻,我们要用到pm2来守护node进程。并且可以用它来做自动重启、性能监控以及负载均衡。 你可以全局安装npm install pm2 -g 其他麻油说 执行命令可以成功,但我这边还是没成功pm2 start npm --name "nuxt-demo" -- run start //(nuxt-demo为你的package.json中的项目名) 我们知道package.json这个文件,当我们执行npm run dev的时候,其实使用npm去启动了./node_modules/nuxt/bin/nuxt这个文件。当我们cd到我们的项目目录之后,我们最终可以执行如下命令来启动:

pm2 start ./node_modules/nuxt/bin/nuxt.js -- start

这样可以将项目跑起来,但当项目报错时我们无法看到日志,因此我这里在根目录通过配置pm2.config.js

module.exports = {
	apps: [
		{
			name: 'nuxt-demo',//项目名称
			cwd: './',//当前工作路径
			#script: 'npm',//实际启动脚本
            script: './node_modules/nuxt/bin/nuxt.js',//或者可以直接执行这个脚本
			args: 'run start',//参数
			autorestart: true, //自动重启
			error_file: 'logs/nuxt-demo-err.log',//错误日志
			out_file: 'logs/nuxt-demo-out.log', //正常运行日志
			exec_mode: 'cluster',// 应用启动模式,支持fork和cluster模式
			min_uptime: '60s', //应用运行少于时间被认为是异常启动
			restart_delay: '60s',//重启时延
			instances: 4,//开启4个实例,仅在cluster模式有效,用于负载均衡
			watch: true,//监控变化的目录,一旦变化,自动重启
			watch: ['.nuxt', 'nuxt.config.js'],//监控变化的目录
			watch_delay: 1000,//监控时延
			ignore_watch: ['node_modules'],//从监控目录中排除
			watch_options: { // 监听配置
				'followSymlinks': false,
				'usePolling': true
			}
		}
	]
}

这里我是在package.json中引用这个配置文件的。在服务器中执行命令就行。(若有报错可能需要安装babel转译,这个可以兼容ie。

npm install babel-cli babel-core babel-preset-es2015 --save-dev
{
	"scripts": {
		"dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server --exec babel-node",
		"build": "nuxt build",
		"start:test": "cross-env NODE_ENV=test node server/index.js pm2 start pm2.config.js --exec babel-node",
		"start:prod": "cross-env NODE_ENV=production node server/index.js pm2 start pm2.config.js --exec babel-node",
	}
}

服务端docker容器部署前端工程

建立Dockerfile

FROM node:alpine

RUN mkdir -p /usr/local/service/nuxtdemo
RUN chomd -R 777 /usr/local/service/nuxtdemo
WORKDIR /usr/local/service/nuxtdemo
COPY nuxt_demo.tar.gz /usr/local/service/nuxtdemo
RUN cd /usr/local/service/nuxtdemo && tar xf nuxt_demo.tar.gz
RUN rm -rf nuxt_demo.tar.gz

ENV HOST "0.0.0.0"
RUN npm config set registry https://registry.npm.taobao.org
RUN npm install -g pm2
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["npm", "run", "start:test"]
#CMD ["npm", "run", "start:prod"]

然后构建镜像

docker build -t nuxt-demo

启动容器

docker run -dt -p 3000:3000 nuxt-demo
注意:这里服务端部署后需要0.0.0.0:3000访问,则我们前端还需配置主机号和端口,我这边在package.json中没成功,在nuxt.config.js中配置成功了。
module.exports = {
	server: {
		host: '0.0.0.0',
		port: 3000
	}
}

总结

总得来说,在coding world中要不断的学习,没什么问题解决不了,坚持就对了。若有小伙伴对以上内容有不解欢迎评论,我会尽力为你答疑解惑的。最后喜欢这篇文章的小伙伴关注一下小生并点个赞。 附:

史记·大疫

己亥末,庚子春,荆楚大疫,染者数万,众惶恐,举国防,皆闭户,道无车舟,万巷空寂。然外狼亦动,垂涎而候,华夏腹背芒刺。幸龙魂不死,风雨而立。医无私,警无畏,民齐心!政者,医者,兵者,扛鼎逆行勇战矣!商客,名家,百姓,仁义者,邻邦献物捐资,叹山川异域,风月同天,岂曰无衣,与子同裳!能者竭力,万民同心。月余,疫除,终胜。此后百年,风调雨顺,国泰民安!