项目背景

试想一下,如果你现在要为你自己或者你所在的组织创建一个强内容的站点,同时要求好的 SEO(搜素引擎优化),比如博客,你会怎么做呢?

由 vite 或者 create-react-app 等脚手架构建的普通 SPA 应用是不行的,因为这样数据都是通过 AJAX 返回的。你暂时不了解这些概念也没关系,你只需要知道,这种方式下,搜索引擎是无法很好地了解你的网站是干什么的,所以就算大众在搜索引擎搜索你的站点的相关内容,搜索引擎也很难把你的站点排在搜索结果前列。

那么为每个页面都编写一个静态的 html 页面呢?比如,为每篇文章都编写一个 html 文件,然后放在服务器上,这样只要客户端请求某篇文章,服务器就把对应的文章页面直接返回。这样也不好,太麻烦了,如果每次更改内容,都要用硬编码的方式去应对,那就把事情弄得太复杂了。如果有一种后台系统,能让管理员通过后台系统的简单操作,就能修改网站呈现的内容就好了。



本文就将带领你采用一种新颖的、便捷的开发方式——通过结合 CloudBase CMS 和 Next.js,去构建内容管理方便,利于 SEO 且响应快速的站点。

CMS 是云开发 CloudBase 推出的一款无头(headless)内容管理系统,提供给开发者方便地管理内容资源的能力。所谓无头,意思就是内容管理系统只负责管理你的内容,比如文章内容和作者列表。这些内容可以在客户端或者服务端通过 SDK 或者 API 的方式去取得。而如何去展示这些内容,则由开发者自行全权掌控。

Next.js 是一款生产级的 React 框架,它提供了静态生成的功能。静态生成的意思是,在构建的过程中,Next.js 就会自动执行数据拉取的逻辑,并把数据和 UI 渲染为一个个的静态 HTML 页面,这意味着,我们的站点将响应迅速,而且利于 SEO。

本文将通过带领你构建一个个人博客,以展示如何结合这两者,并最终在腾讯云云开发上部署站点。

Demo在线预览:

开启环境和项目

1. 开通 CloudBase CMS

首先,你要在腾讯云控制台开通你的第一个云开发环境。云开发环境是云开发中的一个概念,每个云环境都集成了应用开发需要的基础能力,比如云数据库、云函数,开发者可以方便地组合、使用它们,为应用开发赋能。TCB-CMS 也是建立在云环境之上的。

创建环境时,你可以直接选择空模板并勾选免费资源选项即可,最后将环境命名为 my-blog。

可以看到,环境已经在创建中了。

环境创建完毕后,进入扩展应用模块,可以看到“CMS内容管理系统”,可以在这里安装它。设置都按照默认就可以了,唯一要注意的是,务必记住自己设置的管理员账号密码

等安装完毕后,可以在已安装应用一栏中进入应用。点击访问地址后,我们可以直接访问应用。进入应用并输入管理员账号和密码,然后可以看到以下页面:

现在,系统中还没有任何项目,点击创建一个名为 MyBlog 的新项目,创建完毕后进入项目,可以看到内容模型和内容集合,拿数据库作类比,这两者就是数据库表和数据库表内容的关系,这两者就是我们要设置和管理的内容。已经有准备好的数据可以直接导入,分别就在 项目源码仓库 中的 ./schema 和 ./data 文件夹中。点击导入按钮,然后选择导入文件即可。

好了,现在 CMS 已经成功开通了,我们也往其中加入了内容。接下来,就可以着手 Next.js 应用的编写了。

2. 启动 Next.js 项目

Next.js 是构建于 React 之上的生产级前端框架。相比于原本的 React,Next.js 提供了静态生成、服务端渲染等特性,同时自带前端路由,我们这次就主要用到 Next.js 的静态生成功能。使用 Next.js 编写前端应用,和使用 create-react-app 脚手架编写 SPA 应用非常类似,而且更加便捷、开箱即用。

直接在命令行执行以下命令可以创建 Next.js 的样板项目并启动它:

npx create-next-app
npm run dev

访问 localhost:3000 可以看到以下效果:

接下来,我们就将基于这个样板项目开发网站。

首页

这里,我们将在首页放置文章列表。

首先,打开项目下的 ./pages/index.js,发现页面导出了一个函数式的 React 组件。在 Next.js 中,pages 目录下,除了 api 文件夹下的内容和 _app.js,其他每个 js 文件导出的 React 组件都对应着一个或者一种页面,并且由 Next.js 直接生成对应的路由,index.js 导出的函数式组件就直接对应着我们进入网站后的第一个页面,而其他 js 文件于 ./pages 的相对地址就是 Next.js 为其自动生成的路由。

Next.js 在应用构建期,就会对每个页面执行数据拉取的逻辑,并根据 React 组件构建的 UI,渲染出最后的 HTML 页面,接下来,我们要做的就是,构建主页的 UI,以及为主页编写拉取数据的逻辑。

UI 编写

接下来对主页的 UI 进行修改:

import Head from 'next/head'
import Link from 'next/link'
import styles from '../styles/Home.module.css' export default function Home({ posts }) {
return (
<div className={styles.container}>
<Head>
<title>云开发小站</title>
<link rel="icon" href="https://main.qcloudimg.com/raw/3b942431a6ef465d3b8369969e861c0f/favicon.png" />
</Head> <main className={styles.main}>
<h1 className={styles.title}>
Welcome to <a href="https://www.cloudbase.net/">云开发 CloudBase!</a>
</h1> <div className={styles.grid}>
{posts.map(post => (
<Link href={`/post/${post._id}`}>
<a href="https://nextjs.org/docs" className={styles.card}>
<h3>{post.title} &rarr;</h3>
<p>{post.author}</p>
</a>
</Link>
))}
</div>
</main> <footer className={styles.footer}>
<a
href="https://www.cloudbase.net/"
target="_blank"
rel="noopener noreferrer"
>
Powered by{' '}
<img src="https://main.qcloudimg.com/raw/3b942431a6ef465d3b8369969e861c0f/favicon.png" alt="TCB Logo" className={styles.logo} />
</a>
</footer>
</div>
)
}

可以看到,修改后的 Home 组件,接受了一个 posts 为参数,这个 post 就是文章列表数据,我们将它在组件中渲染出来。

那么 post 从哪里来呢?在同一个 js 文件下,需要再导出一个 getStaticProps 函数。

export async function getStaticProps() {
return {
props: undefined,
}
}

这个函数返回的对象的 props 属性,就是导出的函数式组件用到的参数。所以,只需要在 getStaticProps 函数中得到数据并返回即可。

拉取数据

先安装拉取数据要用到的 SDK:

npm install --save @cloudbase/node-sdk

然后,我们再创建 env.js 文件,在其中填入云环境相关信息:

export const tcbConfig = {
env: '',
secretId: '',
secretKey: ''
};

其中环境ID(env)可以直接在环境主页中看到,API 密钥(secretId,secretKey)则可以在 访问管理 中获取。

最后,我们创建 ./lib/api.js,然后填入以下内容,将数据拉取的逻辑全部集中在这个文件中。

import tcb from '@cloudbase/node-sdk';
import { tcbConfig } from '../env'; const { Author, Article } = (() => {
const db = tcb.init(tcbConfig).database();
return {
Author: db.collection('author'),
Article: db.collection('article'),
};
})(); export const getHomePosts = async () => {
const posts = (await Article
.where({})
.orderBy('_updateTime', 'desc')
.limit(10)
.get()).data;
for (const post of posts) {
const { name } = (await Author
.where({ _id: post.author })
.get()).data[0];
post.author = name;
}
return {
posts
}
};

在 api.js 中,根据环境信息,对 SDK 进行了初始化,并创建了用于查询文章和作者的实例 Author 和 Ariticle。在 getHomePosts 函数中,我们获取了展示用的文章。具体的逻辑如果不懂也暂时不必深究,现在只需要知道:通过执行 getHomePosts 我们能从云环境的 CMS 系统中拉取文章列表。

接着,再来修改 ./pages/index.js 文件:

import Head from 'next/head'
import Link from 'next/link'
import styles from '../styles/Home.module.css'
import { getHomePosts } from '../lib/api' ... export async function getStaticProps() {
return {
props: await getHomePosts()
};
}

然后,再访问 localhost:3000,可以看到如下效果:

这标志着:我们成功从 CMS 中获取数据并能够渲染出静态页面来返回给客户端啦!

文章页面

接下来,就要着手编写文章页面了,基本流程差不多,但值得注意的是,文章页面和主页不同,只有一个主页,但是文章页面可能有无数个,而Next.js 提供了能力,能让我们只编写一个 js 文件,并加以细微的改动,就能渲染出无数的文章页面 。

拉取用以渲染页面的文章内容

先准备好需要的样式。首先创建 ./styles/Post.module.css 文件,具体样式内容可以参考:https://github.com/LRCong/nextjs-tcbcms-app/blob/main/styles/Post.module.css

接着,创建 ./pages/post/[id].js 文件,这个文件对应的,就是路由形如 /post/{id} 的所有页面,而 id 的作用就是匹配文章的 _id。这样通过访问 URL:/post/{id1}.js,就能访问到文章id等于 id1 的文章对应页面。

我们先往 [id].js 文件中填入以下的内容:

import Head from 'next/head';
import Link from 'next/link';
import styles from '../../styles/Post.module.css'; export default function Post({
title,
image,
author,
avator,
contentHtml
}) {
return (
<div className={styles.container}>
<Head>
<title>{title}</title>
<link rel="icon" href="/favicon.png" />
</Head> <main className={styles.main}>
<div className={styles.info_container}>
<img className={styles.image} src={image}></img>
<div className={styles.info}>
<h1 className={styles.title}>{title}</h1>
<div className={styles.author_info}>
<div className={styles.avator} style={{ backgroundImage: `url(${avator})` }}></div>
<h2>{author}</h2>
</div>
</div>
</div> <div className={styles.markdown} dangerouslySetInnerHTML={{ __html: contentHtml }} /> <Link href='/'><h3 className={styles.back}>返回</h3></Link>
</main> <footer className={styles.footer}>
<a
href="https://www.cloudbase.net/"
target="_blank"
rel="noopener noreferrer"
>
Powered by{' '}
<img src="https://main.qcloudimg.com/raw/3b942431a6ef465d3b8369969e861c0f/favicon.png" alt="TCB Logo" className={styles.logo} />
</a>
</footer>
</div>
)
} export async function getStaticPaths() {
return {
paths: undefined,
fallback: false
};
} export async function getStaticProps({ params }) {
return {
props: undefined
};
}

可以看到,相比 index.js,[id].js 多出了一个 getStaticProps 函数,getStaticProps 也多了一个 parms 参数。getStaticProps 函数暂时不用管,而 param.id 就是在路由中匹配到的 id,可以借助它,执行获取对应文章内容的逻辑。

在 api.js 中,添加以下内容:

// 对于 image 类型的字段,直接取得的 id 需要转换为可用的 URL
const dealWithUrl = url => 'https://' + url
.replace(`cloud://${tcbConfig.env}.`, '')
.replace('/cloudbase-cms', '.tcb.qcloud.la/cloudbase-cms'); export const getPost = async (id) => {
const post = (await Article.where({ _id: id }).get()).data[0];
const { name, avator } = (await Author
.where({ _id: post.author })
.get()).data[0];
post.author = name;
post.avator = dealWithUrl(avator);
post.image = dealWithUrl(post.image);
return post;
};

然后安装 remark 以及 remark-html 两个库,我们将用它们把 markdown 转化为 html,然后修改 [id].js 文件中的 getStaticProps 为

import Head from 'next/head';
import Link from 'next/link';
import styles from '../../styles/Post.module.css';
import { getPost } from '../../lib/api';
import remark from 'remark'
import html from 'remark-html' ... export async function getStaticProps({ params }) {
const post = await getPost(params.id); const processedContent = await remark()
.use(html)
.process(post.content)
post.contentHtml = processedContent.toString() return {
props: post
};
}

拉取所有的文章id以渲染所有文章页面

只到这一步还不够,我们需要知道所有的路由可能匹配到的 id 值,Next.js 才能渲染出全部的文章页面。[id].js 多出的 getStaticPaths 函数正是用来返回 id 所有可能的匹配值的。

这就是我们只需要编写一次拉取文章数据逻辑,编写一次文章页面 UI,就能让 Next.js 生成出无数文章的静态页面的奥秘。因为,可以让 Next.js 知道所有的文章 id,然后 Next.js 就能对每个文章页面执行一次生成了。

修改往 api.js 中添加获取所有文章 id 的函数:

export const getAllPostId = async () => {
let posts = (await Article.where({}).get()).data;
return posts.map(value => ({
params: { id: value._id }
}))
};

然后修改 getStaticPaths 函数:

export async function getStaticPaths() {
return {
paths: await getAllPostId(),
fallback: false
};
}

然后,访问首页,随便进入一篇文章,就可以看到如下效果:

到这里,我们就成功完成 Next.js 项目的构建啦!

部署

使用腾讯云云开发,你可以轻易地将应用部署到公共网络上。

我们先修改 package.json 中依赖库的配置,因为云开发环境对于依赖版本有一定限制:

"dependencies": {
"@cloudbase/node-sdk": "^2.5.1",
"next": "9.5.4",
"react": "16.13.1",
"react-dom": "16.13.1",
"remark": "^13.0.0",
"remark-html": "^13.0.1"
}

然后,创建部署的配置文件 cloudbaserc.json,并填入以下内容:

{
"envId": "{{env}}",
"version": "2.0",
"$schema": "https://framework-1258016615.tcloudbaseapp.com/schema/latest.json",
"functionRoot": "./functions",
"functions": [],
"region": "ap-shanghai",
"framework": {
"name": "tcbcms-nextjs",
"plugins": {
"client": {
"use": "@cloudbase/framework-plugin-next",
"inputs": {}
}
}
}
}

其中的 env 换成你刚刚创建的云环境的 ID。

修改完毕后,执行命令:

cloudbase

可以看到部署流程启动,等待到部署完毕后,进入云环境的“我的应用”模块,会发现应用列表多了一个“tcbcms-nextjs”,点击访问键,就能访问刚刚创建的应用,而且是通过公网 IP,这说明我们的应用已经部署成功了。

总结

到此,我们的博客已经成功创建并部署了。以后如果博客中要添加新文章,或者要删改原有的文章,都只需要在 CMS 上进行内容的改动,然后在本地执行 Next.js 的构建和云开发部署即可。

更多 Next.js 和云开发相关知识,可以查看官方文档:

Next.js 官方文档:https://nextjs.org/docs/getting-started

CloudBase CMS 官方文档:https://docs.cloudbase.net/cms/intro.html

@cloudbase/node-sdk 官方文档:https://docs.cloudbase.net/api-reference/server/node-sdk/introduction.html

彩蛋:云开发是如何支撑应用开发的?

你可能会好奇云环境的能力是如何支撑我们的系统的。

再次进入腾讯云控制台,进入刚刚创建的云环境主页,进入HTTP访问服务、云数据库以及云函数模块,会发现多了许多东西。实际上,CMS 系统就是由这些东西支撑着的。

我们每次访问 CMS 系统并操作,都会经由 HTTP访问服务,导向某个云函数,在云函数中执行后台逻辑,而系统中的数据,也都存储在云数据库中,这也是我们可以通过 @cloudbase/node-sdk 访问云数据库得到 CMS 中的数据的原因。

而我们刚刚部署的 Next 应用,实际上也是运行在云函数上的。

如果有兴趣在云开发中更进一步,欢迎前往云开发社区官网 cloudbase.net 阅读文档,加入技术交流群,一起探索云开发的更多可能性。

CloudBase CMS + Next.js:轻松构建一个内容丰富的站点的更多相关文章

  1. Vue.js:安装node js到构建一个vue并启动它

    ylbtech-Vue.js:从安装node js到构建一个vue并启动它 1.返回顶部 1. 1.安装node js 下载地址:http://nodejs.cn/download/2.安装完成后运行 ...

  2. 从安装node js到构建一个vue并启动它

    1.安装node js 下载地址:http://nodejs.cn/download/2.安装完成后运行Node.js command prompt(node -v查看安装版本) 3.安装npm(由于 ...

  3. 开源重磅,java内容管理系统CMS,点击就可以编辑,保存,轻松构建自己的站点

    买的暂时空间不给力.内存不足,老给关闭,先转到京东云上了,免费的,也不知免费多久. 这是地址2  http://java4cms.jd-app.com/index.html 这是地址  http:// ...

  4. 开源丨CloudBase CMS 内容管理系统!简单易用企业内容管理流

    背景 云开发CloudBase CMS 是云开发推出的一站式云端内容管理系统,助力企业的数据运营管理工作. 开发者可以直接在云开发扩展能力中一键安装 CloudBase CMS,免费使用 CloudB ...

  5. 初识Dash -- 构建一个人人都能够轻松上手的界面,操控数据和可视化

    从事数据科学工作,少不了使用Pandas.scikit-learn这些Python生态系统中的利器,还有就是控制工作流的Jupyter Notebooks,没的说,你和同事都爱用.但是,要想将工作成果 ...

  6. typescript+webpack构建一个js库

    依赖说明 入口文件 tsconfig配置 webpack配置文件 webpack入口文件配置 webpack为typescript和less文件配置各自的loader webpack的output配置 ...

  7. 基于Vue2.0+Vue-router构建一个简单的单页应用

    爱编程爱分享,原创文章,转载请注明出处,谢谢!http://www.cnblogs.com/fozero/p/6185492.html 一.介绍 vue.js 是 目前 最火的前端框架,vue.js ...

  8. Nightwatch.js – 轻松实现浏览器的自动测试

    Nightwatch.js 是一个易于使用的,基于 Node.js 平台的浏览器自动化测试解决方案.它使用强大的 Selenium WebDriver API 来在 DOM 元素上执行命令和断言. 语 ...

  9. Node.js用6行代码1个JS文件搭建一个HTTP静态服务器

    Node.js是一个基于Chrome的JavaScript运行时的用户以轻松构建快速.可扩展的网络应用平台. Node.js使用事件驱动.非阻塞I/ O模型,使它轻量级.高效和完美的适用于运行在分布式 ...

随机推荐

  1. HashMap是如何进行扩容的?

    HashMap通过resize()方法进行扩容. 源码解析: resize()函数有两种使用情况: 一.当table数组为null时初始化hash表. 二.当table数组不为null时进行扩容. 1 ...

  2. HashMap什么时候进行扩容?

    Threshold:table数组元素个数size的大小超过threshold且且Node<K,V>[] table数组长度没有超过64时时table数组扩容.当hashmap中的元素个数 ...

  3. 文件下载:报错The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'

    前言:这篇文件下载的后台代码太繁琐,建议参考https://www.cnblogs.com/zwh0910/p/13745947.html 前端: <el-button type="p ...

  4. .NET测试断言工具Shouldly

    .NET测试断言工具Shouldly .NET测试 Shouldly在GitHub的开源地址:https://github.com/shouldly/shouldly Shouldly的官方文档:ht ...

  5. Python切换版本工具pyenv

    目录 安装pyenv 安装与查看py版本 切换py版本 结合ide使用示例 和virtualenv的一些区别 参考文献 使用了一段时间,我发现这玩意根本不是什么神器,简直就是垃圾,安装多版本总是失败, ...

  6. go中waitGroup源码解读

    waitGroup源码刨铣 前言 WaitGroup实现 noCopy state1 Add Wait 总结 参考 waitGroup源码刨铣 前言 学习下waitGroup的实现 本文是在go ve ...

  7. 【RocketMQ源码分析】深入消息存储(1)

    最近在学习RocketMQ相关的东西,在学习之余沉淀几篇笔记. RocketMQ有很多值得关注的设计点,消息发送.消息消费.路由中心NameServer.消息过滤.消息存储.主从同步.事务消息等等. ...

  8. 自然语言处理(NLP)知识结构总结

    自然语言处理知识太庞大了,网上也都是一些零零散散的知识,比如单独讲某些模型,也没有来龙去脉,学习起来较为困难,于是我自己总结了一份知识体系结构,不足之处,欢迎指正.内容来源主要参考黄志洪老师的自然语言 ...

  9. Java关于整型类缓存[-128,127]之间的数字

    我们在学习Java的包装类Integer.Long的时候可能会遇到这个问题: ①Integer a = 500;// Integer a = Integer.valueOf(500); 等价于上面的 ...

  10. PTA 统计二叉树叶子结点个数

    6-2 统计二叉树叶子结点个数 (10 分)   本题要求实现一个函数,可统计二叉树的叶子结点个数. 函数接口定义: int LeafCount ( BiTree T); T是二叉树树根指针,函数Le ...