创建自己的Headless CMS并与博客集成

英雄形象



成为初学者意味着探索编程的新视野,走进未知领域,希望有一个更好的地方。



我想您会同意,用新技术开始一个项目通常很有趣。尽管您成为专家的旅程必不可少,但您面对并尝试解决的问题并不总是那么容易。



所以我在说什么。今天,我在这里与您分享我从Hedless CMS,API和博客创建系统的最初经验。由于缺少足够的此类材料,尤其是俄语,我希望本文能够帮助您自己创建这样的系统,避免我犯的错误。



我将告诉您如何以块为单位组装系统以及它的结果。我不会解释背景信息,但是会留下指向资源的链接,您可以在其中了解更多信息。有时很难找到俄语来源,但我会尝试的。此外,如果不确定微服务相对于单片架构的优势,则可以观看该演讲(英语)或阅读本文(含义最接近)。



API ( , ):



Vidzhel/Bluro





, , , - , - . .



( ) . , , , «» . .



, , . . - , .





, , . , Headless () CMS, Bluro. «Hello world» , «TechOverload» .



-, , , .



, . . . , , , , .



, :



  • , ,
  • , , ,
  • , ,
  • ,
  • , ,


, , , , :



  • , , , .
  • ,
  • ,


, , . , , . , , : , , Headless CMS, , .



- , Python Django. , , .



, YouTube, .



, , . — , URL (, ). - .



, , . , , .



系统组成图


API. - , .



JavaScript, NodeJS React . , .



Bluro CMS



Headless CMS , (UI). , . CMS API (REST API , ), .



, , , API — , — , . , , , , URL-, , .



, http . , , .



MVC (Model View Controller). ( ).



, , , , .



CMS .



, - API, CMS. , , , , .



- , .



Bluro CMS组件图



. .



Main , .



ORM



, — ORM (Object Relational Mapper).



, , , - ? , , . , . , — .



— «». , , SQL .



, , . : (, ), ( ), , . , , . , - « ».



数据层架构(首选)



. , Model ( ), . Model . , , .



, ORM. , .



. , , . . , , - . , , - , . , - : ).



, . Sequelize API Django, . ORM.



ORM架构



Entities — , ( , ). Model QuerySet , . , QuerySet Statement, API . StatementsBuilder — , Statement . , .



« », , .



, , . , , , ORM.



ORM. , .



const Model = DependencyResolver.getDependency(null, "Model");
const ARTICLE_STATES = {
PUBLISHED: "PUBLISHED",
PENDING_PUBLISHING: "PENDING_PUBLISHING",
};
const VERBOSE_REGEXP = /^[0-9a-z-._~]*$/i;
class Article extends Model {
static STATES = ARTICLE_STATES;
// There can be other methods
// that fetch data for you or process it in some way
}
// Define model with schema
Article.init([
{
columnName: "user",
foreignKey: {
table: "User",
columnName: "id",
onDelete: Model.OP.CASCADE,
onUpdate: Model.OP.CASCADE,
},
type: Model.DATA_TYPES.INT(),
},
{
columnName: "dateOfPublishing",
verboseName: "Date of publishing",
type: Model.DATA_TYPES.DATE_TIME(),
nullable: true,
validators: Model.CUSTOM_VALIDATORS_GENERATORS.dateInterval(),
},
{
columnName: "dateOfChanging",
verboseName: "Date of changing",
type: Model.DATA_TYPES.DATE_TIME(),
validators: Model.CUSTOM_VALIDATORS_GENERATORS.dateInterval(),
},
...
{
columnName: "state",
verboseName: "Article state",
type: Model.DATA_TYPES.VARCHAR(18),
possibleValues: Object.values(ARTICLE_STATES),
},
]);
// Somewhere else
const set = await Article.selector
.orderBy({ dateOfPublishing: "DESC" })
.limit(offset, count)
.filter({
firstValue: "dateOfChanging",
operator: Operators.between,
innerCondition: {
firstValue: "10.11.2020",
operator: Operators.and,
secondValue: "11.11.2020",
},
})
.filter({
user: "userId",
state: Article.STATES.PUBLISHED,
})
.fetch();
const resulte = await set.getList();


. , .



, , . , . , , Django.



, CMS, , . , , , . , , . , . , , , .



GIT, , .



. , .



{
"migrated": true,
"initialMigration": true,
"tables": [
[
"User",
{
"migrated": false,
"DEFINE_TABLE": true,
"DEFINE_COLUMN": {
"userName": {
"name": "userName",
"type": { "id": "VARCHAR", "size": 10 },
"default": null,
"nullable": false,
"autoincrement": false,
"primaryKey": false,
"unique": false,
"foreignKey": null
},
"password": {
"name": "password",
"type": { "id": "VARCHAR", "size": 10 },
"default": null,
"nullable": false,
"autoincrement": false,
"primaryKey": false,
"unique": false,
"foreignKey": null
},
"id": {
"name": "id",
"type": { "id": "INT" },
"default": null,
"nullable": false,
"autoincrement": true,
"primaryKey": true,
"unique": false,
"foreignKey": null
}
}
}
]
],
"name": "0_Auth_migration.json"
}


{
"migrated": true,
"initialMigration": false,
"tables": [
[
"User",
{
"migrated": false,
"CHANGE_COLUMN": {
"password": {
"name": "password",
"type": {
"id": "VARCHAR",
"size": 50
},
"default": null,
"nullable": false,
"autoincrement": false,
"primaryKey": false,
"unique": false,
"foreignKey": null
}
}
}
]
],
"name": "1_Auth_migration.json"
}


Server



, http-. , , . HTTP, : Request Response, .



  • Request , , multipart / form-data.
  • Response , . , cookie.


Router



«» — , . Express — , , , .



  • Route — , . , , . .
  • Rule — , . , authorizationRule, , . , . , . Rule , , Rule Route.


. , ( ), .



connectRule("all", "/", authRule, { sensitive: false });
connectRule(["put", "delete"], "/profiles/{verbose}", requireAuthorizationRule);
connectRule(["post", "delete"], "/profiles/{user}/followers", requireAuthorizationRule);
connectRoute("get", "/profiles", getProfilesController);
connectRoute("put", "/profiles/{verbose}", updateProfileController);




, . , API. , , , , .



, API, , . , , , .



. — - , . Modules Manager, , , , . , .



, SOLID, . , , . , , . - , .



.



API



, API . , , . , , .



数据库架构



Auth



, , : , , , ...



API , . , , .



, JWT (JSON Web Token) cookie. . .



, :



  • authRule — , cookie . , , .
  • requireAuthorizationRule — , .


Article



, . , .



Comment



.



Notifications



.

NotificationService .



API:



{
"email": "email",
"pass": "password"
}
{
"session": {
"verbose": "id that is used to get profile info",
"userName": "userName",
"role": "user role: 'ADMIN', 'USER'",
"email": "email"
},
"errors": "error's descriptions list",
"success": "success's descriptions list",
"info": "info's descriptions list",
"notifications": [
"collection of notifications"
]
}
view raw responseExample.json hosted with ❤ by GitHub




CMS, , , . React .



前端架构



- , . « » . React Router . , -. , , , -, .



Redux "" Redux-Saga ( Redux-Saga ). , Redux (Action), . (Reducer) , - , , .



, Redux-Saga , , . , .



Redux-Saga, Headless CMS. , :



function* fetchData(endpoint, requestData) {
const controller = new AbortController();
const { signal } = controller;
let res, wasTimeout, reason, failure;
failure = false;
try {
// use Fetch API to make request, wait no longer than `TIMEOUT`
const raceRes = yield race([
call(fetch, endpoint, {
...requestData,
signal,
mode: "cors",
redirect: "follow",
credentials: "include",
}),
delay(TIMEOUT, true),
]);
res = raceRes[0];
wasTimeout = raceRes[1] || false;
if (wasTimeout) {
failure = true;
reason = "Connection timeout";
// Abort fetching
controller.abort();
}
} catch (e) {
console.log(e);
reason = "Error occurred";
}
return { reason, res, failure, wasTimeout };
}
view raw fetchData.js hosted with ❤ by GitHub
export function* makeRequest(endpoint, requestData) {
// Signal that we start making request (we can use it to show loading wheel)
yield put({ type: SES_ASYNC.START_MAKING_REQUEST_ASYNC });
// call enother saga that will make request
let { res, reason, failure, wasTimeout } = yield call(fetchData, endpoint, requestData);
if (res) {
// Process response
const results = yield call(handleResponse, res, wasTimeout, reason, failure);
// Signal about finishing
yield put({ type: SES_ASYNC.END_MAKING_REQUEST_ASYNC });
return results;
} else {
// Return error
failure = true;
reason = "Server error";
yield put({ type: SES_ASYNC.END_MAKING_REQUEST_ASYNC });
return { res: null, wasTimeout, reason, data: null, failure };
}
}
view raw makeRequest.js hosted with ❤ by GitHub


fetchData — , Fetch API . , TIMEOUT, . makeRequest , . - . , , :



function* openArticle({ verbose }) {
// Get cached articles from the state
const article = yield select(getFetchedArticle, verbose);
// If we don't have this article in cache, fetch it
if (!article) {
const { failure, data } = yield call(
makeRequest,
`${configs.endpoints.articles}/${verbose}`,
{
method: "GET",
},
);
if (!failure) {
article = yield call(convertArticleData, data.entry);
}
}
// If article was successfuly fetched, we signaling to open it
if (article) {
yield fork(fetchArticleContent, { fileName: article.textSourceName });
yield put({
type: ART_ASYNC.OPEN_ARTICLE_ASYNC,
article,
});
}
}
view raw openArticle.js hosted with ❤ by GitHub


, . ( ).



用户主页



管理面板,用户标签





. — .



- NGINX:



server {
listen 80;
client_max_body_size 100M;
location / {
proxy_pass http://front_blog:3000;
}
location /admin {
proxy_pass http://front_admin_panel:3000;
}
location /api {
rewrite ^/api/?(.*)$ /$1 break;
proxy_pass http://bluro_api:8000;
}
}
view raw proxyServer.conf hosted with ❤ by GitHub


Docker Compose, . , ( — ).






- , headlesscms.org, Headless CMS , .



, , , -.




All Articles