从前端到服务器全栈搭建个人博客

1,934 阅读11分钟

全栈开发—博客前端展示(Nuxt.js)

这个开发的想法是这样来的,大概两个月前,腾讯云的工作人员打电话给我,说我的域名没有解析到腾讯云的服务器上,而且页脚也没有备案号。我当时就震惊了,居然会打电话给我,然而我的大学时代买的服务器已经过期了...于是为了拯救我的域名,拯救我申请了很久的备案号,决意要全栈打造一个属于自己的博客系统。

主要技术

  • Nuxt.js
  • Axios
  • marked + highlight.js
  • scss

项目特点

  • 适配多个分辨率,移动端到桌面端无缝切换
  • 支持白昼黑夜主题切换(试试点击shirmy的太阳或月亮)
  • 文章图片懒加载
  • 评论、留言、搜索、点赞、多个作者
  • SSR服务端渲染(seo)

技术总结

什么是服务端渲染(server side render)?

服务端渲染则把Ajax请求放到服务端,页面加载到浏览器或客户端前就已经把数据填充到页面模板行程完整的页面。

优势

  • 减少首次http请求,在服务端请求首屏数据,直接渲染html
  • 利于SEO,网络爬虫可以抓取到完整的页面信息

劣势

  • 服务端压力大
  • 需要掌握从前端到服务端的开发(多学点不好吗?)

什么是客户端渲染(client side render)?

客户端渲染就是就是在客户端通过Ajax请求获取数据,然后在客户端生成DOM插入到html

CSR 优势

  • 前后端分离,各司其职
  • 局部刷新,无需重新刷新页面
  • 服务器压力小
  • 更好的实现各种前端效果

CSR 劣势

  • 首屏渲染慢,需要下载JSCSS文件
  • 不利于SEO,爬虫抓取不到完整的页面数据

SSR vs CSR

门户网站、博客网站等需要SEO优化的网站使用服务端渲染,管理后台等内部系统或不需要SEO优化的网站使用客户端渲染

对比图

服务端渲染

前端渲染

如何实现白昼黑夜主题切换?

在这里,要使用CSS3变量配合scss进行控制,通过控制<body>标签的id来约束白昼或黑夜的颜色值,再给相应的属性加上transition属性实现颜色切换时的过渡,请看下面的示例:

@mixin theme(
  $theme-primary
) {
  --theme-primary: #{$theme-primary}
}

body {
  &#light {
    @include theme(
      #theme-primary: #fff,
    )
  }

  &#dark {
    @include theme(
      #theme-primary: #000,
    )
  }
}

全局引入上面的scss文件,这样就可以直接通过设置<body>标签的id的值为darklight--theme-primary赋予不同的颜色值,此时就能直接在需要应用该颜色的元素上进行如下设置:

.example-class {
  color: var(--theme-primary);
}

在我的博客shirmy中点击月亮/太阳即可看到效果,亦可用同样的方式定义多套主题色。

切换页面改变页面title

Nuxt内置了head属性配置,head配置直接修改根目录下的nuxt.config.js文件就可以了,并且还内置vue-meta插件,因此想要在不同页面改变相应的title,请看以下做法:

// nuxt.config.js
module.exports = {
  head: {
    // 组件中的 head() 方法返回的字符串会替换 %s
    titleTemplate: '%s | shirmy',
  }
}

// 文章详情页 pages/article/_id.vue
export default {
  head() {
    return {
      title: this.article.title
    }
  }
}

如此一来,当查看某篇文章详情的时候就会触发head()方法,title显示为article title | shirmy

切换页面让页面滚动保持在顶部

只要修改nuxt.config.js配置即可,并且可以根据传入的参数做一些自定义的配置。

// nuxt.config.js
module.exports = {
  router: {
    scrollBehavior: function (to, from, savedPosition) {
      return { x: 0, y: 0 }
    }
  },
}

Nuxt中的fetch()

fetch()Nuxt中独有的方法,它会在组件初始化前被调用,因此无法通过this获取组件对象。比如进入首页或从首页切换到归档页,在进入页面前会先执行fetch()方法,为了异步获取数据,fetch()方法必须返回Promise,因此可以直接返回一个Promise或者使用async await(async await其本质就是返回Promise)。

该方法不会设置组件的数据,如果想要设置组件的数据,或者使用context上下文,可以使用asyncData

export default {
  // 虽然无法通过 this.$nuxt.$route 获取路由参数,但是可以通过 params 来获取
  async fetch({ store, params }) {
    await store.dispatch('about/getAuthor', params.id)
    await store.dispatch('about/getArticles', {
      authorId: params.id,
      page: 0
    })
  }
}

这样就能确保内容已经渲染好再下载到浏览器,如果使用mounted等生命周期钩子,则是在页面下载到浏览器后再获取数据,起不到SSR服务端渲染的效果。

style-resource

为了方便统一管理scss变量,通常会在目录中创建variables.scssmixin.scss,但是我们写的vue文件这么多,如果都要一个个导入岂不是吃力不讨好,这时可以借助style-resource

npm install -S @nuxtjs/style-resources

修改nuxt.config.js文件:

// nuxt.config.js
module.exports = {
  styleResources: {
    scss: ['./assets/scss/variables.scss', './assets/scss/mixin.scss']
  }
},

图片懒加载

图片懒加载的关键是使用IntersectionObserver,IE浏览器不兼容,需要使用polyfill。该WebAPI用于监听元素是否出现在顶级文档视窗中。

通过这个WebAPI,我们可以把<img>标签的src属性地址先挂在data-src属性上,当该元素出现在视窗时就会触发IntersectionObserver的的回调方法,此时再给<img>标签的src属性赋予先前挂在data-src上的地址

复制时携带转载声明

监听copy事件,然后通过getSelection()方法获取复制的内容,在通过clipboardDatasetData()方法在复制内容上加上转载信息:

if (process.env.NODE_ENV === 'production') {
  const copyText = `

---------------------
作者:shirmy
链接:${location.href}
来源:https://www.shirmy.me
商业转载请联系作者获得授权,非商业转载请注明出处。`

  document.addEventListener('copy', e => {
    if (!window.getSelection) {
      return
    }
    const content = window.getSelection().toString()

    e.clipboardData.setData('text/plain', content + copyText)
    e.clipboardData.setData('text/html', content + copyText)
    e.preventDefault()
  })
}

markdown防止XSS攻击

如果读者在评论中输入<script>alert('xss')</script>,那么进入文章详情页时就会触发这段代码,这时候,我们就需要把script过滤掉,当然XSS还有许多类似的方式,但是万变不离其宗

在这里,我借助了DOMPurify

npm install dompurify -S
DOMPurify.sanitize(html)

使用谷歌分析服务

首先进入官网Google 统计分析服务,并注册你的账号,得到一个格式如GA_MEASUREMENT_ID的媒体资源ID

然后直接修改nuxt.config.js,当然也可以自定义(gtag.js开发指南):

// nuxt.config.js
module.exports = {
  head: {
    // 其它配置...
    script: [
      // 其它配置...
      {
        async: 'async',
        type: 'text/javascript',
        // GA_MEASUREMENT_ID 替换为你刚刚注册得到的媒体资源ID
        src: 'https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID'
      },
      {
        // Global site tag (gtag.js) - Google Analytics
        type: 'text/javascript',
        // GA_MEASUREMENT_ID 替换为你刚刚注册得到的媒体资源ID
        innerHTML: `
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());

          gtag('config', 'GA_MEASUREMENT_ID');
        `
      }
    ]
  }
}

参考文档

全栈开发—博客后台管理系统

主要技术

  • Vue.js + Vue Router + Vuex
  • Axios
  • Element UI

项目特点

  • 权限控制
  • 用户无感知token刷新
  • 支持用户头像上传
  • 简洁易用的菜单配置
  • 丰富的文章条件筛选

screenshot

文章筛选

新增作者

技术总结

如何实现菜单配置?

我们知道,菜单栏是分为多级的,其实它是和router对应上的。因此我们可以把一级每个一级菜单抽出来,作为单独的一个对象,然后导入这些配置项,通过对数据结构的整理和重组,组合成我们所需要的路由文件。

// article.js
export const articleRouter = {
  // ...路由配置(名称,路径,是否显示,图标,子路由等属性)
}
// author.js
export const authorRouter = {
  // ...路由配置
}
// index.js
let homeRouter = [
  {
    // ...路由配置
  },
  articleRouter,
  authorRouter
]
// step1: 根据自己定义的配置进行处理,并供菜单栏遍历使用
// step2: 深度遍历构建路由
// step3: 插入到 Vue Router 中
// routes.js
const routes = [
  {
    path: '',
    name: 'Home',
    redirect: '/about',
    component: Home,
    children: [
      // 这里就是与菜单相对应的路由数组
      ...homeRouter
    ]
  },
  // 这里可以额外配置一些非菜单路由
]

通过这种方式,就可以更加灵活的给每个路由添加特定的配置,实现个性化的定制,实现不同页面的解耦。

限制上传图片大小

通过<input type="file">获取到图片后,通过使用URL.createObjectURL()静态方法创建DOMString。然后赋值给Image对象,再根据该Image对象进行判断

fileChange(e) {
  const imgFile = e.target.files[0]
  // 图标大小大于 1M
  if (imgFile.size > 1024 * 1024 * 1) {
    // ...
  }

  const imgSrc = window.URL.createObjectURL(imgFile)
  const image = new Image()
  image.src = imgSrc
  image.onload = () => {
    // 图片加载后获取图片宽度
    const w = image.width
    // 图片加载后获取图片高度
    const h = image.height
    // ... 后续处理
  }
}

用户无感知刷新

  1. 服务端生成两个Token,一个accessToken,一个refreshTokenaccessToken是普通的访问token,通常设置为一两个小时,refreshToken用于验证用户是否可以再获取一个新的accessToken,可以设置为一个月。
  2. 当用户停留在管理系统页面没有退出,但accessToken过期了,在进行操作时,先把这个失败的请求保存起来,然后再向服务器验证refreshToken,如果通过,则颁发一个新的accessToken,再通过这个新的accessToken去请求刚刚缓存起来的请求。
  3. 如果refreshToken验证不通过,则请求失败,直接调用相关的loginOut()方法

按需加载

比如我们要使用lodash中的throttle方法,我们可以这样做:

// 只导入 throttle 相关模块
import throttle from 'lodash/throttle'

这样能极大减少打包后的体积,这种方式非常有用,常用的,包括ECharts也是如此。

编码相关

  1. 在文章列表的级联筛选中,我们通常通过targetId === id来判断是否选中某个对象,当有多个筛选条件时,我们可以通过以下方式把多个相同的逻辑进行如下封装:
selectFilter(id, target) {
  // 如果和当前选择一样则不必再选中了
  if (id === this[target]) {
    return
  }
  // 只需要额外传入和组件data相同的字符串名就不用再写多个函数了
  this[target] = id
  this.getArticles()
}
  1. 全局过滤器注册
// filter/index.js
export default {
  format(value, format) {},
  filter(value) {}
}

// filters 中包含多个过滤器
import filters from '@/services/filter'
import Vue from 'vue'

// main.js
// 全局过滤器,不要一个个注册,全局组件同理
Object.keys(filters).forEach(k => Vue.filter(k, filters[k]))

参考文档

全栈开发—博客服务端(Koa2)

主要技术

  • Koa2
  • MySQL
  • Sequelize
  • JWT
  • Axios
  • Validator.js

项目特点

  • 封装权限控制中间件
  • 清晰的项目结构
  • 简洁易用的参数校验、异常处理
  • 支持用户无感知刷新

技术总结

项目结构

├── app                     # 业务代码
│   ├── api                 # api
│   │   ├── blog            # 提供给博客前端API
│   │   └── v1              # 提供给博客管理系统API
│   ├── dao                 # 数据库操作层
│   ├── lib                 # 工具函数、工具类、常量
│   ├── models              # Sequelize model 层
│   └── validators          # 参数校验工具类
├── config                  # 全局项目配置
├── core                    # 核心库
│   ├── db.js               # Sequelize 全局配置
│   ├── http-exception.js   # 异常处理定义
│   ├── init.js             # 项目初始化
│   ├── lin-validator.js    # 参数校验插件
│   ├── multipart.js        # 文件上传处理
│   └── util.js             # 核心库工具函数
├── middleware              # 中间件

导入模块太多?

Koa2写服务端代码,有一个体验就是文件导出来导出去,各种路径,这时我们可以使用别名:

npm install -S nodule-alias
{
  // ...
  "_moduleAliases": {
    "@models": "app/models",
  }
}
// app.js
require('module-alias/register')

// article.js
const { Article } = require('@models')

自动注册路由中间件

当路由模块很多时,在app.js中一个个导入岂不是越写越长,这时我们可以借助require-directory工具

const requireDirectory = require('require-directory')
const Router = require('koa-router')

class InitManager {
  static initCore(app) {
    // 入口
    InitManager.app = app
    InitManager.initLoadRoutes()
  }

  static initLoadRoutes() {
    // process.cwd() 获取绝对路径
    const appDirectory = `${process.cwd()}/app/api`
    // 使用 require-directory 提供的方法导入自动导入路由文件
    requireDirectory(module, appDirectory, {
      visit: whenLoadingModule
    })

    // 注册所有检测到的 Koa 路由
    function whenLoadingModule(obj) {
      if (obj instanceof Router) {
        InitManager.app.use(obj.routes())
      }
    }
  }
}

module.exports = InitManager

// app.js
const Koa = require('koa')
const app = new Koa()

InitManager.initCore(app)

如何使用七牛云上传?

实际上官方文档就已经写得很清楚了:Node.js SDKV6,无非就是安装插件,照着文档搬运代码。

在这里要注意的是,如果上传多个文件,我们需要放在一个循环里逐一上传,而上传又是异步的,那么如何验证所有文件都已经上传成功,在这里我们可以使用Promise.all()方法进行封装,举个栗子:

class UpLoader {
  async upload(files) {
    let promise = []
    
    for (const file of files) {
      // ...
      promise.push(new Promise((resolve, reject) => {
        // 执行上传逻辑
        // resolve() or reject()
      }))
    }

    Promise.all(promises).then(res => {
      // ... 全部成功
    }).catch(e => {
      // ... 有上传失败的
    })
  }
}

全局异常捕获中间件

// http-exception.js
class HttpException extends Error {
  constructor(msg = '服务器异常', errorCode = 10000, code = 400) {
    super()
    this.msg = msg
    this.errorCode = errorCode
    this.code = code
  }
}

// exception.js
const { HttpException } = require('@exception')

const catchError = async (ctx, next) => {
  try {
    // 利用洋葱圈模型的特性,所有请求都会经过这里
    await next()
  } catch (error) {
    const isHttpException = error instanceof HttpException
    const isDev = global.config.environment = 'dev'
    if (isDev && !isHttpException) {
      throw error
    }
    // 已知错误
    if (isHttpException) {
      ctx.body = {
        msg: error.msg,
        errorCode: error.errorCode,
        request: `${ctx.method}: ${ctx.path}`
      }
      ctx.status = error.code
    } else {
      // 未知错误
      ctx.body = {
        msg: '服务器内部错误',
        errorCode: 999,
        request: `${ctx.method}: ${ctx.path}`
      }
      ctx.status = 500
    }
  }
}

module.exports = catchError

// app.js
const catchError = require('./middleware/exception')

app.use(catchError)

如何进行权限校验

权限校验是通过JWT实现的,使用JWT可以用用户ID、超时时间、权限级别给用户生成一个Token返回到客户端,客户端再把这个Token存储到cookie中,步骤如下:

  1. 安装jsonwebtoken插件
  2. 给用户颁发一个由用户id、用户权限级别、超时时间生成的accessToken
  3. 客户端把accessToken保存到cookie中,然后以后的每次发送请求都会携带这个token
  4. 使用koa中间件,在API处理前校验token是否合法,并且判断用户是否有权限访问该API

其它业务代码及框架的基本用法就不多说了,可以直接参考smile-blog-koa

参考文档

全栈开发—Travis CI持续集成

前期准备

  • 我使用的 macOS 10.14.5
  • 首先我们需要注册一个GitHub账号并且创建项目,把已经写好的代码托管到GitHub上
  • 然后登录Travis CI官网,使用GitHub账号登录授权,这时Travis就能获取到Github上的项目
  • 准备一个云服务器,我用的是腾讯云(CentOS 7.2 64位)
  • 选择需要持续集成的项目,看下图:

关联项目

持续集成

这里以一个前端项目为例,首先在项目根目录下创建一个.travis.yml文件,并写入以下代码保存:

language: node_js
cache:
  directories:
  - node_modules      # 缓存 node_modules
node_js: stable       # 稳定版本
branches:
  only:
  - master            # 每次 push 或者 pull request 时会触发持续集成
install:
  - npm install       # 当然你可以使用 yarn
scripts:
  - npm test          # 执行测试
  - npm build         # build

这时只要把项目pushmaster分支就会触发部署,这一步的目的是验证项目是否通过测试、编译,模拟生产环境进行自动测试,提前发现错误。效果图大概长这样:

效果图

持续部署

创建 rsa 对

  1. 进入你的服务器,查看有没有rsa对,如果没有则使用ssh-keygen创建,并赋予权限:
cd ~/.ssh

sudo chmod 700 ~/.ssh/

sudo chmod 600 ~/.ssh/*
  1. 将公钥添加到受信任列表,并重新赋予权限
cat id_rsa.pub >> authorized_keys

cat authorized_keys

sudo chmod 600 ~/.ssh/*
  1. 如果没有id_rsaid_rsa.pub文件,其实就跟你配置GitHub SSH Key一样:
ssh-keygen -t rsa -C "这里替换成你的邮箱"

# 获取秘钥并复制之
cat id_rsa.pub

浏览器打开GitHub:用户头像 -> Setting -> SSH and GPG keys -> New SSH Key,添加之

安装 Ruby

Ruby官方安装指南

上面的可以找到Windows、Mac、Linux的Ruby安装方式,我使用的是Mac,直接使用Homebrew安装,当然Macb本身就自带Ruby:

安装教程

# 安装 homebrew
ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)"

# 安装最新版本 Ruby

# 更新 brew 支持的版本信息
brew update

# 编译安装最新版本
brew install ruby

# 检验是否安装成功
ruby --version

安装ruby是因为travis客户端使用ruby写的

安装配置 Travis 客户端

# 安装
gem install travis

进入到要部署的项目根目录下,用GitHub的账号密码登录Travis客户端:

travis login --auto

利用服务器私钥加密生成id_rsa.enc文件,travis会借助它来登录你的服务器,这样就可以在你的服务器上进行自动部署的操作了:

travis encrypt-file ~/.ssh/id_rsa --add

执行完这句后,项目根目录下就会生成一个id_rsa.enc文件,并且先前在目录下创建好的.travis.yml文件会多出这样一行:

before_install:
- openssl aes-256-cbc -K $encrypted_######_key -iv $encrypted_#######_iv -in id_rsa.enc -out ~\/.ssh/id_rsa -d

踩了很多坑?没关系,这时已经非常接近成功了

执行部署

Travis CI自带了一些生命钩子,我们可以在相应的生命钩子(Travis CI Job Lifecycle)搞事情,其中after_success钩子是执行部署脚本的钩子。

此时在.travis.yml上添加部署脚本,如果你不想暴露你在服务器上的部署用户名和服务器IP,你可以在travis中配置环境变量

项目部署面板 -> 右侧的More options -> Settings -> 找到Environment Variables -> 输入变量名,变量值,然后在yml文件中用$变量名来引用

language: node_js
cache:
  directories:
  - node_modules
node_js: stable
branches:
  only:
  - master
before_install:
  # 注意 这里我改成了 ~/.ssh/id_rsa 而不是自动生成的 ~\/.ssh/id_rsa
  - openssl aes-256-cbc -K $encrypted_######_key -iv $encrypted_######_iv -in id_rsa.enc -out ~/.ssh/id_rsa -d
  # 赋予权限
  - chmod 600 ~/.ssh/id_rsa
install:
  - npm install
scripts:
  - npm test          # 执行测试
  - npm build         # build
# 执行部署脚本
after_success:
# $DEPLOY_USER 环境变量:服务器用户名
# $DEPLOY_HOST 环境变量:服务器IP
# 项目目录 你在服务器上的项目目录
- ssh "$DEPLOY_USER"@"$DEPLOY_HOST" -o StrictHostKeyChecking=no 'cd 项目目录 && git pull && bash ./script/deploy.sh'
addons:
  ssh_known_hosts:
  # 你的服务器IP
  - "$DEPLOY_HOST"

在根目录下创建script文件夹放部署脚本

# script/deploy.sh
#!/bin/bash

echo 'npm install'
npm install

echo 'npm run build'
npm run build

echo 'success'

部署成功之后,就会看到上面有个下面这样的徽章,点击它,按照你需要的格式复制使用

Build Status

总结

再来回顾一下流程:

  1. Travis官网关联GitHub项目
  2. 在服务器中生成rsa对,把rsa对添加到github上,并把项目拉到服务器中
  3. 安装ruby,安装travis客户端并登陆,在项目目录下生成id_rsa.enc
  4. 编写项目部署脚本
  5. 把项目推送或PR到相应的分支上
  6. 获取徽章

使用Nginx配置HTTPS和反向代理

想起大学时自己建了个简单的网站,当时使用的http,然后打开的时候经常都被http劫持,那是真的无可奈何,所以这次必须要整个https。https是啥?想必大家都知道,不懂的直接超文本安全传输协议

HTTPS效果图

配置HTTPS

证书申请

  • 电脑操作系统是macOS 10.14.5
  • 服务器:腾讯云CentOS 7.2
  • Nginx版本:1.16.0

在这里,我使用的是腾讯云的免费证书:

  • 进入控制台 -> 左上角云产品 -> 域名和网站 -> SSL证书 -> 申请免费证书
  • 申请之后,大概过不了几分钟,就通过审核了,然后下载之
  • 下载之后有几个文件:

目录

证书配置

首先登录你的服务器,我们需要创建一个文件夹来放我们的证书

mkdir /usr/local/nginx/cert

然后使用scp命令把我们的证书传到服务器上

# 进入到放证书的文件夹
cd shirmy.me

# 把 Nginx 下的文件远程拷贝到服务器上
scp -r ./Nginx/* 你的服务器用户名@你的服务器IP:/usr/local/nginx/cert

接下来就可以修改Nginx的配置了,其实腾讯云提供了很完善的证书安装指引,里面有除了Nginx之外的其它服务器配置方式:

腾讯云SSL证书安装指引

如果直接使用文档中的方式,Nginx会报警告,需要做一些小的修改:

server {
    listen 443; #SSL 访问端口号为 443
    server_name www.shirmy.com; #填写绑定证书的域名
    # ssl on; #启用 SSL 功能 这行会报警告 去掉即可
    ssl_certificate ../cert/1_www.shirmy.me_bundle.crt; #证书文件名称
    ssl_certificate_key ../cert/2_www.shirmy.me.key; #私钥文件名称
    ssl_session_timeout 5m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #请按照这个协议配置
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE; #请按照这个套件配置,配置加密套件,写法遵循 openssl 标准。
    ssl_prefer_server_ciphers on;

    location / {
        root /var/www/www.domain.com; #网站主页路径。此路径仅供参考,具体请您按照实际目录操作。
        index  index.html index.htm;
        # 这是为解决 Vue Router 哈希模式刷新后404的问题 Nginx 找不到文件后会在内部发起一个子请求到根目录下的 index.html
        try_files $uri $uri/ /index.html;
    }
}

HTTP跳转到HTTPS

腾讯云文档提供了以下的配置方式,但是我用的是另外一种配置方式:

# 文档提供的配置方式
}
  server {
    listen 80;
    server_name www.domain.com; #填写绑定证书的域名
    rewrite ^(.*)$ https://$host$1 permanent; #把http的域名请求转成https
}

另一种方式

这种方式其实是利用了<meta>标签中的的http-equiv属性,与之对应的值是content,我们需要新建一个index.html文件,复制并修改以下代码:

<html>
    <!-- 自动刷新并指向新页面,0 是指0秒后刷新(立即刷新) -->
    <meta http-equiv="refresh" content="0;url=https://www.shirmy.me/">
</html>

这样当我们访问http://www.shirmy.me时就会重新刷新到https://www.shirmy.me,然后再修改nginx配置如下:

server {
    listen 80;  # 监听默认端口
    server_name www.shirmy.me; # 域名
    location / {
        root www/http.shirmy.me/;    # 刚刚的 index.html 所在目录
        index index.html index.htm;
    }
}

最后,重启我们的Nginx服务器:

cd /usr/local/nginx/sbin

# 平滑重启
./nginx -s reload

# 非平滑重启
./nginx -s stop && ./nginx

大功告成,配置了HTTPS的网站,要保证网站的链接都是安全的,包括API请求都必须使用HTTPS

Nginx反向代理

  • 我们的网页发起请求时,带个端口岂不是很难看,比如https//api.shirmy.me:3000/v1/articles,如何去掉端口呢?
  • 又比如说我们要访问集群服务器时,会先访问一个中间服务器,然后这个中间服务器再把你的请求分发到压力小的服务器,这也需要通过反向代理来实现。
# 负载均衡就是靠下面这个来实现
# blogapi 替换成你喜欢的名字
upstream blogapi {
    server http://127.0.0.1:3000;
    # server 你也可以选择配置多个IP
}
server {
    # 同上面一样的 HTTPS 配置
    listen 443 ssl;
    server_name api.shirmy.me;
    ssl_certificate ../cert/1_api.shirmy.me_bundle.crt;
    ssl_certificate_key ../cert/2_api.shirmy.me.key;
    ssl_session_timeout 5m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
    ssl_prefer_server_ciphers on;

    # 反向代理配置
    location / {
        # $host 代表转发服务器
        proxy_set_header Host $host;
        proxy_redirect off;
        # 记录真实IP
        proxy_set_header X-Real-IP $remote_addr;
        # 存储请求链路上各代理IP
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        # 连接超时时间
        proxy_connect_timeout 60;
        # nginx接收upstream server数据超时时间
        proxy_read_timeout 600;
        # nginx发送数据至upstream server超时时间
        proxy_send_timeout 600;
        # 反向代理到上面定义好的 upstream blogapi 下的服务器上
        proxy_pass http://blogapi;
    }
}

如此一来,就实现了反向代理和负载均衡,此外,我们应该让用户第一次访问该服务器后,以后再访问也是访问该服务器,避免多次建立http连接,那么我们可以这样修改:

upstream blogapi {
    # 避免每次被请求到多台服务器上 满足用户保持访问同一台服务器 又能实现负载均衡
    ip_hash;
    server http://127.0.0.1:3000;
    # server 你也可以选择配置多个服务器IP
}

最后记得重启/usr/local/nginx/sbin/nginx -s reload

多个域名配置

除了主页shirmy.me之外,我们通常还要有一个管理后台:admin.shirmy.me,因为用的是免费证书,所以我们也只好为子域名申请一个SSL证书,并且以同样的方式配置。

我们又总不能用端口shirmy.me:5000这样子访问吧,其实只要这样做:

server {
    listen 80;
    # admin.shirmy.me
    server_name admin.shirmy.me;
    location / {
        # 直接看上面 HTTP 跳转到 HTTPS 的配置
        root www/http.admin.shirmy.me/;
        index index.html index.htm;
    }
}

最后记得重启/usr/local/nginx/sbin/nginx -s reload