إنشاء نظام إدارة المحتوى بدون رأس خاص بك والتكامل مع مدونة

صورة البطل



أن تكون مبتدئًا يعني استكشاف آفاق جديدة في البرمجة ، والدخول إلى المجهول ، على أمل أن يكون هناك مكان ما أفضل.



أعتقد أنك ستوافق على أنه غالبًا ما يكون من المثير جدًا بدء مشروع بتكنولوجيا جديدة. المشاكل التي تواجهها وتحاول حلها ليست دائمًا سهلة ، على الرغم من أنها جزء لا يتجزأ من رحلتك لتصبح معلمًا.



إذن ما الذي أتحدث عنه. أنا هنا اليوم لأشارككم تجربتي الأولى في إنشاء نظام من 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