Erstellung eines eigenen Headless CMS und Integration in ein Blog

Heldenbild



AnfÀnger zu sein bedeutet, neue Horizonte in der Programmierung zu erkunden, ins Unbekannte zu treten und zu hoffen, dass es irgendwo besser wird.



Ich denke, Sie werden mir zustimmen, dass es oft sehr lustig ist, ein Projekt mit neuer Technologie zu starten. Die Probleme, mit denen Sie konfrontiert sind und die Sie zu lösen versuchen, sind nicht immer einfach, obwohl sie ein wesentlicher Bestandteil Ihrer Reise zum Guru sind.



Also, wovon rede ich? Heute bin ich hier, um Ihnen meine ersten Erfahrungen mit der Erstellung eines Systems aus Hedless CMS, API und Blog mitzuteilen. Aufgrund des Mangels an ausreichender Menge solchen Materials, insbesondere auf Russisch, hoffe ich, dass dieser Artikel Ihnen dabei hilft, ein solches System selbst zu erstellen und die Fehler zu vermeiden, die ich gemacht habe.



Ich werde Ihnen erzĂ€hlen, wie ich das System in Blöcken zusammengebaut habe und was daraus entstanden ist. Ich werde die Hintergrundinformationen nicht erklĂ€ren, aber ich werde Links zu Ressourcen hinterlassen, in denen Sie mehr erfahren können. Manchmal ist es schwierig, eine russischsprachige Quelle zu finden, aber ich werde es versuchen. DarĂŒber hinaus können Sie den Vortrag (auf Englisch) ansehen oder diesen Artikel lesen (der am nĂ€chsten kommt), wenn Sie sich nicht sicher sind, welche Vorteile Microservices gegenĂŒber monolithischer Architektur haben.



API ( , ):



Vidzhel/Bluro





, , , - , - . .



( ) . , , , «» . .



, , . . - , .





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



-, , , .



, . . . , , , , .



, :



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


, , , , :



  • , , , .
  • ,
  • ,


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



- , Python Django. , , .



, YouTube, .



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



, , . , , .



Systemkomponentendiagramm


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-Komponentendiagramm



. .



Main , .



ORM



, — ORM (Object Relational Mapper).



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



— «». , , SQL .



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



Datenschichtarchitektur (erste Option)



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



, ORM. , .



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



, . Sequelize API Django, . ORM.



ORM-Architektur



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();
view raw modelsUsageExample.js hosted with ❀ by GitHub


. , .



, , . , . , , 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);
view raw routerUsageExample.js hosted with ❀ by GitHub




, . , API. , , , , .



, API, , . , , , .



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



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



.



API



, API . , , . , , .



Datenbankschema



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 .



Frontend-Architektur



- , . « » . 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


, . ( ).



Benutzerhomepage



Administrationsbereich, Registerkarte Benutzer





. — .



- 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