Vue静态站点生成 -- Gridsome综合案例

本博客案例使用静态站点生成器 Gridsome 快速生成静态网站,后台的数据使用 Strapi 进行管理,案例中使用 GraphQL 访问 Strapi 中的数据,最后通过 Vercel 来自动化部署。

案例介绍

功能介绍:实现一个个人博客的基本功能
页面UI:参照startbootstrap-clean-bloggithub地址
案例目的:体验使用Gridsome解决实际开发过程中的问题

案例实现

创建项目

1
$ gridsome create blog-with-gridsome

本地启动项目

1
$ npm run develop

浏览器访问http://localhost:8080/,如下:

Gridsome

基础配置

根据 startbootstrap-clean-blog/index.html,处理首页模板

  • 安装相关依赖
1
$ npm i bootstrap @fortawesome/fontawesome-free
  • 创建 src/assets/css/index.css 文件,引入项目中需要的goole字体文件,并导入其他需要的css样式
1
2
3
@import url('https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic');
@import url('https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800');
/* 将 startbootstrap-clean-blog/css/clean-blog.css 中的内容复制到下方 */
  • src/main.js 中,全局引入项目中所需的静态资源
1
2
3
import 'bootstrap/dist/css/bootstrap.min.css'
import '@fortawesome/fontawesome-free/css/all.min.css'
import '~/assets/css/index.css'

公共模板

每个页面的头部和底部都是一样的,只有中间部分不一样,因此将其公共部分提取出来

startbootstrap-clean-blog/index.htmlbody 部分的 NavigationFooter 内容,复制到 src/layout/Default.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div class="layout">

<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-light fixed-top" id="mainNav">
......
</nav>

<!-- 默认插槽 -->
<slot/>

<hr />

<!-- Footer -->
<footer>
......
</footer>
</div>
</template>

<style>
</style>

首页模板

startbootstrap-clean-blog/index.htmlbody 部分的 Page HeaderMain Content 内容,复制到 src/pages/index.vue

  • src/pages/index.vue ,创建根节点
  • 将 startbootstrap-clean-blog/img 目录拷贝到 static/img 目录下,并修改 url 路径,如下所示:
1
2
3
4
5
6
7
8
9
10
11
<template>
<Layout>
<!-- Page Header -->
<header class="masthead" style="background-image: url('/img/home-bg.jpg')">
......
</header>

<!-- Main Content -->
......
</Layout>
</template>

其他页面模板,与首页模板相似

处理页面数据

使用本地md文件管理文章详情

适用范围:适用于需求比较简单的情况

注意:
1.Gridsome有三种获取动态数据的方式:通过插件、特定的API、本地文件。
2.使用@gridsome/source-filesystem获取本地文件内容,它的作用是将本地文件内容转换为在组件中能够被GraphQL请求的形式。
3.使用@gridsome/source-filesystem处理.md文件时,必须搭配将文件转换为html的插件@gridsome/transformer-remark,否则会报错
4.重启项目后配置的插件才会生效

  • 安装插件和 markdown 的转换器
1
2
3
4
# 安装@gridsome/source-filesystem
$ npm install @gridsome/source-filesystem --save
# markdown 的转换器,将 md 文件 转换为 HTML 文件
$ npm install @gridsome/transformer-remark --save
  • gridsome.config.js 中,配置插件,并创建对应路径的 md 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = {
...
plugins: [
{
use: '@gridsome/source-filesystem',
options: {
//GraphQL类型、template名称,
//src/templates下的.vue文件必须通过typeName指定一个模板
typeName: 'BlogPost',
//数据源路径
path: './content/blog/**/*.md',
}
}
]
...
}
  • 新建content/blog/article1.md,执行 npm run develop 启动项目,浏览器访问 http://localhost:8080/___explore,看到blogPostallBlogPost,说明插件引入成功

gridsome

在GraphQL Data资源管理器里面输入查询语句,获取.md的内容

gridsome

  • 在页面中使用.md格式的数据

    • 先通过<page-query>获取GraphQL中的md格式的数据
    • 使用markdown-it插件将.md格式的字符串转换为html字符串
    • 使用v-html指令在页面上显示转换后的html字符串

例如,获取存放在md中的欢迎页数据

  1. 安装插件
1
$ npm install markdown-it --save
  1. 将插件引入页面,进行转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
<div>
<!--在页面上显示转换后的`html`字符串-->
<div v-html="mdToHtml($page.welcome.edges[0].node.content)"></div>
</div>
</template>
<!-- 获取GraphQL中的md格式的数据 -->
<page-query>
query {
welcome:allWelcome {
edges {
node {
content
}
}
}
}
</page-query>
<!-- 将`.md`格式的字符串转换为`html`字符串 -->
<script>
// import MarkdownIt from 'markdown-it'
// const md = new MarkdownIt()
const md = require('markdown-it')({ html: true });
export default {
name: 'AboutPage',
metaInfo: {
title: 'Welcome'
},
methods: {
//将.md格式的字符串转换为html字符串
mdToHtml(markdown) {
return md.render(markdown);
}
}
}
</script>

注意 :如果获取的数据中包含html,必须启用markdown-it插件的【在源码中启用 html 标签】,否则使用v-html指令显示的还是html 字符串

markdown-it插件的所有的选项列表(默认情况下)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var md = require('markdown-it')({
html: false, // 在源码中启用 HTML 标签
xhtmlOut: false, // 使用 '/' 来闭合单标签 (比如 <br />)。
// 这个选项只对完全的 CommonMark 模式兼容。
breaks: false, // 转换段落里的 '\n' 到 <br>。
langPrefix: 'language-', // 给围栏代码块的 CSS 语言前缀。对于额外的高亮代码非常有用。
linkify: false, // 将类似 URL 的文本自动转换为链接。

// 启用一些语言中立的替换 + 引号美化
typographer: false,

// 当 typographer 启用时,成倍的 + 单引号替换对。
// 或者智能(smartquotes)引号等,可以是 String 或 Array。
//
// 比方说,你可以支持 '«»„“' 给俄罗斯人使用, '„“‚‘' 给德国人使用。
// 还有 ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] 给法国人使用(包括 nbsp)。
quotes: '“”‘’',

// 高亮函数,会返回转义的 HTML。
// 如果源字符串未更改,且应该进行外部的转义,或许返回 ''
// 如果结果以 <pre ... 开头,内部包装器则会跳过。
highlight: function (/*str, lang*/) { return ''; }
});

Strapi - 处理来自后台管理系统的数据

Strapi简介
  • Strapi - 一款比较通用的内容管理系统(CMS:Content Management System),可以帮助我们轻松的管理内容,官网地址:strapi
  • Strapi可以管理各种形式的内容,如博客的文章、商品、用户评论、用户信息等
  • Strapi特点
    • 可以管理用户自定义的任何内容
    • 用户管理内容的方式简单易操作
    • 对开发者而言,提供了友好的API
    • 支持角色权限的管理
    • 可以通过插件系统扩展功能
    • 支持用户自定义功能
Strapi使用
  • 创建一个Strapi项目,建议使用npx
1
2
3
4
5
# yarn
$ yarn create strapi-app my-project --quickstart

# npx
$ npx create-strapi-app my-project --quickstart

执行 npm run develop 命令启动项目,默认打开浏览器,注册一个管理员账号,登录/内容类型生成器/创建一个新的content type,创建文章列表及文章详情内容,这里就不在叙述,具体使用参考:Strapi使用

  • 将Strapi后台管理系统的数据集成到Gridsome项目中

    • 安装@gridsome/source-strapi
    1
    $ npm install @gridsome/source-strapi --save
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    module.exports = {
    plugins: [
    {
    use: '@gridsome/source-filesystem',
    options: {
    typeName: 'BlogPost',
    // 抓取文件的路径,即抓取哪些文件
    path: './content/blog/**/*.md',
    }
    },
    {
    use: '@gridsome/source-strapi',
    options: {
    apiURL: 'http://localhost:1337', // 接口地址
    queryLimit: 1000, // Defaults to 100
    contentTypes: ['post'], // 查询的数据类型
    // singleTypes: ['impressum'], // 单个节点
    // Possibility to login with a Strapi user,
    // when content types are not publicly available (optional).
    // loginData: { // 登录信息
    // identifier: '',
    // password: ''
    // }
    }
    }
    ],
    }

执行 npm run develop 重新启动项目,访问 http://localhost:8080/___explore,如下图,说明数据集成成功

gridsome

注意:
1、在项目中配置好@gridsome/source-strapi,启动项目之前,必须在strapi后管添加用户、分配相应的权限、并登录成功之后,gridsome集成strapi的数据才能成功,否则会报403。
2、在strapi后管添加数据后,由于是预渲染,gridsome必须重启才能拿到最新的数据。

具体实现

设计数据模型
  • 计文章的字段,如图所示:

post

  • 设计标签时,需创建一个 引用 字段,用于表示 标签和文章的关系

post

  • 标签字段列表,如图所示:

post

页面实现
  • 展示文章列表和分页

    1、使用<page-query>获取GraphQL 数据层获取数据,进行页面数据动态渲染
    2、使用Gridsome自带的分页插件pagination处理分页,插件会自动在页面路由中获取当前的页码

    pages/index.vue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    <template>
    <Layout>
    <!-- Main Content -->
    <div class="container">
    <div class="row">
    <div class="col-lg-8 col-md-10 mx-auto">
    <div class="post-preview" v-for="edge in $page.posts.edges" :key="edge.node.id">
    <a href="post.html">
    <h2 class="post-title">
    {{ edge.node.title }}
    </h2>
    </a>
    <p class="post-meta">
    Posted by
    <a href="#">Start Bootstrap</a>
    on {{ edge.node.create_at }}
    </p>
    <p>
    <span v-for="tag in edge.node.tags" :key="tag.id">
    <g-link :to="'/tags/' + tag.id">
    {{ tag.title }}
    </g-link>
    </span>
    </p>

    <hr />
    </div>

    <!-- Pager -->
    <Pager :info="$page.posts.pageInfo" />
    </div>
    </div>
    </div>
    </Layout>
    </template>

    <page-query>
    query ($page: Int) {
    posts: allStrapiPosts (perPage: 1, page: $page) @paginate {
    pageInfo {
    totalPages
    currentPage
    }
    edges {
    node {
    id
    title
    created_at
    tags {
    id
    title
    }
    }
    }
    }
    }
    </page-query>

    <script>
    // 引入分页组件
    import { Pager } from 'gridsome'

    export default {
    components: {
    Pager
    }
    };
    </script>
  • 展示文章详情

  1. gridsome.config.js 中,配置模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    module.exports = {
    siteName: 'Gridsome',
    ...
    templates: {
    //StrapiPost是由@gridsome/source-strapi插件options的typeName + contentTypes
    StrapiPost: [
    {
    path: '/post/:id', //模板路径
    component: './src/templates/Post.vue' //模板组件
    }]
    }
    }
  2. src/pages/Index.vue使用<g-link>实现点击文章标题时,跳转到文章详情页

1
2
3
4
5
...
<g-link :to="'/post/' + edge.node.id">
<h2 class="post-title">{{edge.node.title}}</h2>
</g-link>
...
  1. src/templates/Post.vue 使用<page-query>获取文章详情并展示,由于文章的内容有可能是markdown格式的,所以在显示时需要先使用markdown-it插件将.md格式的字符串转换为html字符串

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    <template>
    <Layout>
    <!-- Page Header -->
    <header class="masthead" :style="{backgroundImage: `url(http://localhost:1337${$page.post.cover.url})`}">
    <div class="overlay"></div>
    <div class="container">
    <div class="row">
    <div class="col-lg-8 col-md-10 mx-auto">
    <div class="post-heading">
    <h1>{{$page.post.title}}</h1>
    <span class="meta">Posted by
    <a href="#">eline</a>
    {{$page.post.created_at}}
    </span>
    </div>
    </div>
    </div>
    </div>
    </header>
    <!-- Post Content -->
    <article>
    <div class="container">
    <div class="row">
    <div class="col-lg-8 col-md-10 mx-auto" v-html="mdToHtml($page.post.content)"></div>
    </div>
    </div>
    </article>
    </Layout>
    </template>
    <page-query>
    query($id: ID!){
    post:strapiPost(id:$id){
    id
    title
    content
    created_at
    cover{
    name
    url
    }
    tags{
    id
    title
    }
    }
    }
    </page-query>

    <script>
    import MarkdownIt from 'markdown-it'
    const md = new MarkdownIt();
    export default {
    name:'PostPage',
    metaInfo: {
    title: 'Hello, world!'
    },
    methods:{
    mdToHtml(markdown){
    return md.render(markdown);
    }
    }
    }
    </script>
  • 设置网站的基本信息

    主要设置网站的标题、副标题以及封面

    1. 在 strapi 中新增一个 Single Type(单一类型),名称为 General,并添加三个字段,如图所示:

      post

    2. 在 gridsome.config.js 的 plugins 选项中,进行配置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      module.exports = {
      plugins: [
      {
      options: {
      ...
      singleTypes: ['General'], // 单个节点
      }
      }
      ]
      }
    3. src/pages/index.vue 中,读取 GraphQL 数据层 的数据,并在视图中渲染,代码详见src/pages/index.vue

  • 联系我页面,文章标签页面

这里就不在叙述,代码详见blog-with-gridsome

部署

部署 Strapi
准备工作
  • 支持 Node 的服务器,本次部署在ucloud服务器中
  • 数据库: 建议 MySQL 或者 MongoDB,本次项目中使用的是MySQL
  • 项目代码blog-backend
  • 修改config/database.js,将原来的 sqlite 的配置,修改为 mysql 的配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = ({ env }) => ({
defaultConnection: 'default',
connections: {
default: {
connector: 'bookshelf',
settings: {
client: 'mysql',
host: env('DATABASE_HOST', 'localhost'),
port: env.int('DATABASE_PORT', 3306),
// 你的数据库名称
database: env('DATABASE_NAME', 'blog'),
// 你的服务器中的数据库的用户名和密码
username: env('DATABASE_USERNAME', '远端服务器数据库用户名'),
password: env('DATABASE_PASSWORD', '远端服务器数据库密码'),
},
options: {},
},
},
});

项目中需要安装mysql,此项目安装的版本是2.18.1

ucloud服务其中安装Mysql
  • 检测系统是否自带安装 MySQL
1
$ rpm -qa | grep mysql

如有,类似mysql-libs-5.1.52-1.el6_0.1.x86_64那可以选择进行卸载:

1
2
rpm -e mysql-libs-5.1.52-1.el6_0.1.x86_64  // 普通删除模式
rpm -e --nodeps mysql-libs-5.1.52-1.el6_0.1.x86_64  // 强力删除模式,如果使用上面命令删除时,提示有依赖的其它文件,则用该命令可以对其进行强力删除
  • 安装yum源
1
2
3
4
5
# 下载
$ wget https://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm

# 安装mysql的yum源
$ sudo yum install mysql57-community-release-el7-11.noarch.rpm

检查是否安装成功

1
yum repolist enabled | grep "mysql.*-community.*"

如下图所示表示安装成功:

mysql

  • 安装MySQL
1
$ sudo yum install -y mysql-community-server
  • 启动MySQL服务

因为只有启动了MySQL服务,才会产生/var/log/mysqld.log文件,初始root密码在这个文件目录下
启动mysql服务(在CentOS7下,启动和关闭服务的命令是systemctl start|stop)

1
2
3
4
5
6
7
8
# 启动Mysql
$ service mysqld stop
# 查看mysql状态
$ systemctl status mysqld
# 关闭Mysql
$ service mysqld stop
# 重启
$ service mysqld restart

Mysql启动成功后如下图所示:
mysql

  • 查看mysql的初始密码
    首次使用时候查看初始密码并登录Mysql修改密码
1
2
3
4
5
6
7
8
# 查看初始密码
$ sudo cat /var/log/mysqld.log | grep password
# 使用初始密码登录Mysql
$ mysql -u root -p
# 设置密码(设置的密码必须符合长度,且必须含有数字,小写或大写字母,特殊字符)
mysql> set password=password("新密码");
# 刷新权限
mysql> flush privileges;
  • 开启远程访问,创建数据库设置访问权限
  1. 查看用户表select host,user from user;看到当前host是localhost只允许本地访问,所以需要创建一个可以远程访问数据的的user,这里为blog,具体步骤如下:
1
2
3
4
5
6
# 进入数据库
$ mysql -u root -p
# 切换到 mysql库
mysql> use mysql;
# 查看用户表
mysql> select host,user from user;

mysql

  1. 创建用户
1
2
3
4
# 更新user用户表,创建blog用户
mysql> update user set `host` = '%' where `user` = 'blog' LIMIT 1;
# 强制刷新
mysql> flush privileges;
  1. 创建项目中使用的数据库名称都为blog
1
2
# 创建数据库
mysql> create database blog
  1. root用户新建了一个数据库blog并设置密码,并赋权限给用户blog
1
mysql> grant all privileges on blog.* to blog identified by '123456Blog+';

执行脚本时候报错

1
[Err] 1044 - Access denied for user 'root'@'%' to database 'blog'

报错可以看出blog没有权限,查询用户表看blog用户权限

查看用户表

1
2
mysql> select Host,User,Grant_priv,Super_priv from mysql.user;
mysql> flush privileges;

mysql

可以看到现在这两个权限都是N, 权限设置为Y,然后重启mysql,在此grant脚本时候就正常了

1
mysql> update mysql.user set Grant_priv='Y',Super_priv='Y' where user = 'blog' and host = '%';
  • 查看数据库端口号

查看端口监听状态,默认是3306

1
mysql> show global variables like 'port';
代码上传到服务器中并启动项目
  1. 登录服务,将blog-backend代码上传到服务器中,并安装相关依赖,构建打包
1
2
3
4
5
6
7
8
9
# 登录服务
$ ssh root@106.75.181.60 # ssh root@公网IP
# 上传代码
$ git clone https://github.com/Eline302/blog-backend.git
# 进入项目安装依赖
$ cd blog-backend
$ npm i
# 对项目进行打包构建
$ npm run build
  1. 启动项目

Strapi 默认端口号是1337,需要在ucloud服务器中开放1337端口号

具体步骤详见NuxtJS项目本地部署和自动化部署中的UCloud服务器防火墙设置部分

启动项目推荐使用pm2,使用npm run start直接启动项目,会占用命令行应用,当退出时,则服务也会停止。因此,不建议使用。
pm2详解见NuxtJS项目本地部署和自动化部署中的使用PM2启动Node服务部分

设置服务名称并启动服务

1
2
3
4
# 服务启动的name设置为strapi
$ pm2 start --name strapi run develop
# 启动 strapi 服务
$ pm2 start npm --name strapi -- start

启动成功如下图所示:

mysql

登录 http://106.75.181.60:1337 服务器IP + 端口号(1337),将项目中所需的数据进行配置,如图所示:

mysql

角色和权限,必须要进行配置,否则无法调用接口

mysql

本地服务联通远程 Strapi

参考网址:Environment variables

具体实现

  1. 创建 .env.development.env.production 环境文件,配置 GRIDSOME_API_URL,代码如下:
1
GRIDSOME_API_URL=http://123.57.28.48:1337
  1. gridsome.config.js 中配置的 apiURL ,修改为环境变量 GRIDSOME_API_URL
1
apiURL: process.env.GRIDSOME_API_URL, // 接口地址
Vercel – 部署 Gridsome 应用

使用 Vercel 进行静态应用项目的部署。

基本使用
  • 登录 Vercel,可以使用第三方账户,也可以自行注册(在此使用 GitHub 账户登录)

    mysql

    这里我使用的是gitHub登录,集成到github中

  • 登录以后,新建项目,可以导入 Git 仓库中静态应用项目,或者克隆其他模板(在此导入 GitHub 中已存在的仓库)

    mysql

  • 这里使用Import Git Repository方式导入项目,点击 Continue with GitHub,选择 GitHub 仓库,进行导入

    mysql

  • 点击仓库名称右侧的 import 按钮,展示基本配置,点击 Deploy ,开始安装依赖,构建发布

    mysql

  • 构建成功后点击 Visit,即可访问生成的静态站点

    mysql

配置自动构建

当数据改变时,需要告诉 Vercel,触发自动构建。

  • 在 Vercel 中,找到构建的应用项目,然后点击进入

    mysql

  • 点击 Settings ,然后点击 Git,找到 Deploy Hooks,创建部署钩子,生成链接地址

    mysql

  • 然后,进入 Strapi 内容管理平台,添加 Webhooks

    mysql

  • 填入名称、请求地址(指的是在 Vercel 中生成的地址),并点击保存

    mysql

  • 在 Strapi 中,添加数据或者是Github中更新代码,都会在Vercel中自动构建,在 Vercel 的应用项目中,点击 Deployments既可以查看正在构建的项目

    mysql

    注意:Vercel 页面可能会有延时,可以刷新 Vercel 页面。

完整代码

静态站点项目前端代码:blog-with-gridsome
静态站点项目后端代码:blog-backend
应用访问地址:https://blog-with-gridsome-six.vercel.app/

参考

linux安装mysql
部署中常见问题解决