
成为初学者意味着探索编程的新视野,走进未知领域,希望有一个更好的地方。
我想您会同意,用新技术开始一个项目通常很有趣。尽管您成为专家的旅程必不可少,但您面对并尝试解决的问题并不总是那么容易。
所以我在说什么。今天,我在这里与您分享我从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. , , , , .
- , .

. .
Main , .
ORM
, — ORM (Object Relational Mapper).
, , , - ? , , . , . , — .
— «». , , SQL .
, , . : (, ), ( ), , . , , . , - « ».

. , Model ( ), . Model . , , .
, ORM. , .
. , , . . , , - . , , - , . , - : ).
, . Sequelize API Django, . 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 . , , . , , .

, , : , , , ...
API , . , , .
, JWT (JSON Web Token) cookie. . .
, :
authRule — , cookie . , , .
requireAuthorizationRule — , .
, . , .
.
.
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" |
|
] |
|
} |
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 }; |
|
} |
|
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 }; |
|
} |
|
} |
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, |
|
}); |
|
} |
|
} |
, . ( ).


. — .
- 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; |
|
} |
|
} |
Docker Compose, . , ( — ).
- , headlesscms.org, Headless CMS , .
, , , -.