静态网站生成器
什么是静态网站生成器
- 使用一系列配置、模板以及数据,生成静态 HTML 文件及相关资源的工具
- 由于它用来提前生成静态网页,所以这个功能也称为 预渲染
- 生成的网站不需要类似 PHP 这样的服务器去部署运行
- 只需要放到支持静态资源的 Web Server 或 CDN 上即可运行
静态网站的好处
- 省钱
- 不需要专业的服务器,只要能托管静态文件的空间即可
- 快速
- 不经过后端服务器的处理,只传输内容
- 安全
- 没有后端程序的执行,自然会更安全
常见的静态网站生成器
- Jekyll(Ruby)
- Hexo(Node)
- Hugo(Golang)
- Gatsby(Node/React)
- Gridsome(Node/Vue)
- 另外,Next.js、Nuxt.js 也能生成静态网站,但是它们更多被认为是SSR(服务端渲染)框架
这类静态网站生成器,还有个漂亮的名字叫 JAMStack
JAMStack
JAM 是 JavaScript、API 和 Markup(标记语言) 的首字母组合。
Stack (栈)表示技术栈的意思。
本质上是一种胖前端,通过调用各种 API 来实现更多的功能。
其实也是一种前后端的模式,只不过前后端分的非常明显,是一个完全的纯客户端应用。
甚至后端可以来自多个不同的厂商。
静态应用的使用场景
- 不适合有大量路由页面的应用
- 如果您的站点有成百上千条路由页面,则预渲染将非常缓慢。
- 当然,您每次更新只需要做一次,但是可能要花一些时间。
- 大多数情况下不需要这么多静态路由页面,而只是以防万一。
- 不适合有大量动态内容的应用
- 如果渲染路线中包含特定于用户查看其内容或其他动态源的内容,则应确保您具有可以显示的占位符组件(骨架图),直到动态内容加载到客户端为止。否则可能有点怪异
- 它适合纯内容展示类应用
- 例如,博客、企业宣传站、文档类应用
- 例如,后台管理系统就不适合静态网站
Gridsome 基础
Gridsome 一个免费、开源、基于 Vue.js 技术栈的静态网站生成器。
使用 Gridsome 需要有一定的 Vue 基础,如果有基础,看过文档,只会觉得它比 Vue 本身更简单一些。
使用 Gridsome 前的准备工作
使用 Gridsome 创建的项目依赖一个特殊的第三方模块 sharp。
sharp 的作用是用来处理图片,例如压缩图片大小、转换图片格式等。
这个包很难安装成功,主要原因有两点:
- sharp中包含一些C++文件,安装的时候要对它进行编译才能正确安装,所以要有C++编译环境才行。
- sharp 还依赖 libvips(用于调整图像大小),这个包比较大,大概有101MB,限于国内的网络环境,也很难下载成功。
解决网络问题
sharp 官网 chinese-mirror 提供了 国内的镜像地址,其实就是 taobao 的镜像源。
# 配置 sharp 镜像地址
npm config set sharp_binary_host "https://npm.taobao.org/mirrors/sharp"
# 配置 sharp-libvips 镜像地址
npm config set sharp_libvips_binary_host "https://npm.taobao.org/mirrors/sharp-libvips"
C++ 文件编译环境
安装 node-gyp 以及相关的编译套件。
- 由于 node 程序中需要调用一些其他语言编写的工具,甚至是dll,需要先编译以下,否则就会有跨平台的问题
- node-gyp 用来编译原生C++模块
首先全局安装 node-gyp:
npm i -g node-gyp
安装后还不能正常使用,需要安装对应的编译套件,每个操作系统需要的编译套件不同,具体按照文档中安装即可。
任何操作系统,都需要安装 Python。
以windows为例:
以管理员身份打开命令行执行 npm i -g windows-build-tools
安装后,它会自动下载安装 python 和其他工具。
windows-build-tools 的安装报错问题(我在Issues的留言):
反复执行后,通过控制面板查看已安装的程序,确实新增了几个。
查看它的安装路径 : C:\Users\[用户名]\.windows-build-tools
。
发现vs_BuildTools.exe
已经存在。
也就当命令执行到Starting installation...
,vs_BuildTools.exe
已经下载到完并开始工作。
手动打开它安装 Visual Studio 15 生成工具 2017(勾选Visual C++生成工具)。
(这个工具好像有1GB多,需要一段时间)
安装成功后,继续使用 Gridsome 创建项目,项目依赖可以安装成功。
所以我猜测命令行中执行失败可能是因为软件太大导致。
创建 Gridsome 项目
# 全局安装 gridsome
npm i -g @gridsome/cli
# 创建项目
gridsome create my-gridsome-site
# 创建项目会从 github 获取模板,并执行依赖安装
# > Clone https://github.com/gridsome/gridsome-starter-default.git 3.86s
# > Update project package.json 0.01s
# > Install dependencies
# 安装过程不显示进度,可以 ctrl+c 中断
# 然后cd进入项目目录,手动 npm i (注意删除不完整的node_modules)
# ctrl + c
# npm i
# 运行开发环境 访问 http://localhost:8080/
npm run develop
预渲染
Gridsome 会将 pages 目录下的组件生成路由页面,渲染到页面中。
执行命令 npm run build
,生成静态页面。
生成结果存放在 /dist
目录下。
除了首页直接生成了 index.html
,其他页面的 index.html
都在对应的目录下,例如about/index.html
。
现在 dist
就可以部署在任何支持静态文件的web服务器中。
本地运行可以使用 serve
:
npm i -g serve
serve dist
此时访问的页面内容,是本地服务端直接获取静态html文件的内容并返回的,不是服务端渲染的。
但是页面中的交互,如点击链接跳转页面,还是客户端负责的。
手动刷新页面也是服务端直接返回的html文件内容。
这就是 Gridsome 预渲染的功能。
注意:npm run develop 启动的页面都是动态获取的,没有实时编译页面内容,只用于开发环境。
目录结构
初始项目目录结构:
- src
- main.js - 整个项目的启动入口
- 默认全局注册了 layout 组件
- 组件中有一个
<static-query>
标签,专门用来查询 GraphQL 数据,提供给组件使用。
- 组件中有一个
- 可以在这个文件中导入全局样式和脚本,安装Vue插件、注册组件和指令等
- 默认全局注册了 layout 组件
- template - 存放集合的节点(也是vue组件)
- pages - 存放路由页面
- Gridsome 会自动将pages目录下的文件生成路由配置
- layouts - 存放布局组件(初始模板的main.js中注册了这个组件)
- components - 存放公共的组件
- .temp - 存放打包过程中自动生成的文件
- 类似 NuxtJS 的 .nuxt 目录
- main.js - 整个项目的启动入口
- .cache - 存放打包过程中的一些缓存文件
- dist - 存放打包编译后的结果
- static - 存放不需要打包编译的纯静态资源
- 打包编译时,会将该目录的文件直接复制到
dist
中。
- 打包编译时,会将该目录的文件直接复制到
- gridsome.config.js - gridsome 的配置文件
- 例如站点名称、插件配置等
- gridsome.server.js - gridsome 针对 服务端 的配置文件
- 这里的 服务端 不是服务器,而是 Gridsome 内部的在打包编译过程中的服务。
- …
项目配置
修改配置文件(gridsome.config.js)需要重新启动才能生效。
- siteName - 站点名称,在
titleTemplate
中使用 - siteDescription - 对应 meta 标签 description
- siteUrl
- pathPrefix - 路径前缀
- 如果网站部署在子目录下,需要配置目录前缀
- titleTemplate - 标题模板,默认
%s - <siteName>
- plugins - 插件
- templates - 定义用于集合的路由模板
- metadata - 添加全局的 metadata 到 Graph shema
- icon - 配置网站图标
- configureWebpack - 自定义webpack配置
- chainWebpack - 也是配置webpack的一种方式
- configureWebpack 和 chainWebpack 类似 Vue CLI 的配置方式
- runtimeCompiler
- configureServer - 开发过程中的服务器相关配置
- permalinks - 和链接相关的配置
g-link
- trailingSlash
- 当使用动态路由时,可能需要配置这项
- slugify
- trailingSlash
- css - 和css相关的配置
- split - 代码分割
- loaderOptions - 加载器
- host - 开发服务默认host
- port - 开发服务默认端口
- outputDir - 打包构建的生成文件输出目录,默认
dist
Pages
Pages 负责在URL上显示您的数据。 每个页面将静态生成,并具有自己的带有标记的index.html文件。
静态路由
有两种方式创建 Pages 页面:
- 基于文件系统手动创建 SFC 文件
- 通过 Pages API 编程式创建
基于文件系统
src/pages目录中的SFC单文件组件将自动具有其自己的URL。 文件位置用于生成URL,以下是一些基本示例:
src/pages/Index.vue
生成/
首页src/pages/AboutUs.vue
生成/about-us/
src/pages/about/Vision.vue
生成/about/vision/
src/pages/blog/Index.vue
生成/blog/
注意:生成的URL会将大写字母和驼峰格式转化为小写的 kebab-case 格式
src/pages 中的页面通常用于诸如 /about/ 之类的固定URL,或用于在 /blog/ 中列出博客文章。
编程方式
gridsome.server.js 配置文件中提供一个 createPage 方法用于创建路由页面:
module.exports = function (api) {
api.createPages(({ createPage }) => {
createPage({
path: '/my-page', // 页面地址
component: './src/templates/MyPage.vue' // 路由组件地址
})
})
}
配置完需要重启服务。
动态路由
Gridsome 也可以像 NuxtJS 一样创建动态路由,方式有:
- 基于文件系统
- 编程式
基于文件系统
动态页面用于客户端路由。路由参数可以通过方括号将名称包装在文件和目录名称中。例如:
src/pages/user/[id].vue
生成/user/:id
src/pages/user/[id]/settings.vue
生成/user/:id/settings
开发时不会生成静态页面,所以访问是正常的。
但是构建时,这将生成 user/_id.html
和 user/_id/settings.html
,必须具有重写规则才能使其正常访问,否则会返回 404。
动态路由的页面优先级低于静态路由。例如有一个 /user/create 的路由 和 /user/:id 路由,当id=create时,将优先 /user/create
重写规则 rewrite
Gridsome 打包构建时无法为动态路由的每种可能的变体生成 HTML 文件(集合模板除外,它的路由参数是可知的)。
这意味着直接访问 URL 时可能显示 404 页面。
实际情况是,Gridsome 会生成一个 HTML 文件,该文件可用于重写规则(rewrite)。
例如,/user/:id
的路由将生成 /user/_id.html
文件。
开发者可以编写rewrite规则,将所有与 /user/:id
匹配的路径映射到该文件。
官方文档没有详细介绍如何使用…
// gridsome.server.js
module.exports = function (api) {
api.afterBuild ({ redirects }) {
for (const rule of redirects) {
// rule 包含3个成员:
// rule.from - The dynamic path eg:`/user:id`
// rule.to - The HTML file path eg:`/user/_id.html`
// rule.status - 200 if rewrite rule eg:200
}
}
}
编程方式
同vue路由规则一样:
module.exports = function (api) {
api.createPages(({ createPage }) => {
createPage({
path: '/user/:id(\\d+)', // (\\d+)是用正则对参数格式的限定
component: './src/templates/User.vue'
})
})
}
Page meta info
Gridsome 使用 vue-meta 处理关于页面的 meta 信息。
在页面组件文件中配置metaInfo选项:
export default {
metaInfo: {
title: 'User用户',
meta: [
{name: 'author', content: 'John Doe'}
]
}
}
自定义404页面
手动创建 src/pages/404.vue
页面组件,可以自定义404页面。
始终使用 mounted 钩子获取客户端数据。在 created 钩子中获取数据会导致问题,因为它是在生成静态HTML时执行的。
Collections 集合
客户端动态获取的数据,在打包生成静态页面时不会预渲染到页面中。
例如,在 created 钩子中请求 API 获取数据。
如果这类动态数据,也想要预渲染到页面中,生成静态页面,就需要用到 Gridsome 中的 Collections (集合)。
Collections 用于预渲染动态数据:
- 一个 Collection(集合) 是一组节点,每个节点用于承载数据,包含具有自定义数据的字段。
- Gridsome 会把节点的模板预渲染成一个个页面。
创建一个集合
- Data sources - 数据源
- 数据源可以来自任何地方
- 通过插件导入数据,例如:wordpress网站数据
- 通过 APIs 导入数据,例如:jsonplaceholder
- 通过本地文件导入数据,例如markdown文件数据
- 通过某种方式把这些数据生成 Collections
- 数据源可以来自任何地方
- Collections - 数据生成的集合
- 例如:文章集合、标签集合、产品集合等
- 集合对应的是节点 Nodes
- Nodes - 节点
- 每个节点对应的是数据
- 每个节点可以通过模板生成对应的页面
通过 Source Plugins 创建集合
通过封装好的插件,把外部数据集成到 Gridsome 中去使用。
例如官方文档中的 @gridsome/wordpress
,用于把你的 wordpress 网站中的数据集成到 gridsome 中,生成集合数据。
// gridsome.config.js
module.exports = {
plugins: [
{
use: '@gridsome/source-wordpress',
options: {
baseUrl: 'YOUR_WEBSITE_URL',
typeName: 'WordPress',
}
}
]
}
使用 Data Store API (数据存储 API) 创建集合
可以从任何外部 API 手动创建集合。
// gridsome.server.js
const axios = require('axios')
module.exports = function (api) {
// api.loadSource 加载资源
api.loadSource(async actions => {
// 创建名为 Post 的集合
const collection = actions.addCollection('Post')
// 预请求:获取要预渲染到页面中的数据
const { data } = await axios.get('https://api.example.com/posts')
// 向集合添加节点
// Gridsome 会根据节点的数据,创建节点的模板,生成节点的页面
for (const item of data) {
collection.addNode({
// gridsome 会自动生成id,也可以自定义覆盖
id: item.id,
title: item.title,
content: item.content
})
}
})
}
在 GraphQL 中查询数据
每个 Collection 会向 GraphQL schema 添加两个根字段。
这些根字段用于检索页面中的节点。
例如将集合名为 Post,在 schema 中会添加两个字段:
post
可以通过id
获取单个节点allPost
获取节点列表
- 内置一些方法,例如排序、筛选、分页等
在运行 npm run develop
时会提供3个地址:
- Local - 本地访问地址
- Network - 局域网访问地址
- GraphQL Playground - GraphQL data数据层对应的资源管理器
- 这个地址只有在开发模式(develop)下才能访问
访问 GraphQL Playground,右侧的 DOCS 弹层可以查看可用的字段。
使用 GraphQL 的查询语句查询数据:
按需获取单个节点的字段:
按需获取节点列表的字段:
节点列表的结构:
在页面中查询 GraphQL
可以在页面、模板或其他组件中通过在查询语句块中使用 GraphQL 语法查询数据,在页面中使用:
-
在页面 Pages 或模板 Templates 中使用
<page-query>
- gridsome 会将这个语句块查询的数据注入到
$page
中
- gridsome 会将这个语句块查询的数据注入到
-
在其他组件 Components 中使用
<static-query>
- gridsome 会将这个语句块查询的数据注入到
$static
中
- gridsome 会将这个语句块查询的数据注入到
例如:
<template>
<div>
<ul>
<li v-for="edge in $page.posts.edges" :key="edge.node.id">
<g-link to="/">{{ edge.node.title }}</g-link>
</li>
</ul>
</div>
</template>
<page-query>
query {
# 别名
posts: allPost {
edges {
node {
id
title
}
}
}
}
</page-query>
$page
和 $static
是注入到 Vue 实例的 计算属性中的:
此时页面中的动态数据就是预渲染出来的,但是在 develop 下是看不到效果的,需要打包编译,启动一个web服务去访问打包结果。
Templates
Templates 用于将集合中的节点渲染生成单个页面。
设置模板
可以配置的模板选项:
- path - 定义一个动态路由,可以使用任何真实有效的节点字段作为参数
- component - 指定一个组件作为页面模板
- 默认会将
src/templates/{Collection}.vue
作为模板
- 默认会将
- name - 模板名称,用于在 GraphQL 中获取路径(path)
// gridsome.config.js
module.exports = {
templates: {
// key 是集合的名称
// value 是渲染节点的路由和页面模板,可以是字符串 或 数组
// 字符串 - 路由匹配地址,默认将 `src/templates/{Collection}.vue` 作为模板
// 打包后会生成一系列的 /posts/xx/index.html,可能会生成大量静态页面,所以静态应用不适合有大量动态内容的页面
// Post: '/posts/:id',
// 数组 - 可以为集合设置多个模板
Post: [
// 默认模板
{
path: '/posts/:id',
component: './src/templates/Post.vue'
},
// 具名模板
{
name: 'detail',
path: '/posts/:id/detail',
component: './src/templates/PostDetail.vue'
}
]
}
}
给集合配置了模板后,GraphQL schema 中的节点中会有一个 path
字段,表示模板路径。
可以使用 to
获取模板其他的具名路径。
// src/Posts.vue
<template>
<Layout>
<div>
<ul>
<li v-for="edge in $page.posts.edges" :key="edge.node.id">
<g-link :to="edge.node.path">{{ edge.node.title }}</g-link>
</li>
</ul>
</div>
</Layout>
</template>
<page-query>
query {
# 别名
posts: allPost {
edges {
node {
id
title
path(to:"detail")
}
}
}
}
</page-query>
向模板添加数据
从模板配置生成的页面,拥有节点的 id
和 path
作为查询块中的查询变量。
使用 $id
$path
接收。
// src/templates/Post.vue
<template>
<Layout>
<div>
<h2>{{$page.post.title}}</h2>
<div>{{$page.post.body}}</div>
</div>
</Layout>
</template>
<page-query>
# ID类型 !不能为空(参考 GraphQL Playground 中的 DOCS)
query ($id: ID!) {
post(id: $id) {
id
title
body
}
}
</page-query>
注意,它们不是路由中的参数,而是集合节点的参数 Argument:
在元数据meta中使用节点字段
在 metaInfo
中使用节点字段,它必须是一个返回对象的函数:
<script>
export default {
metaInfo() {
return {
title: this.$page.post.title
}
}
}
</script>