تقسیم کردم یک وباپلیکیشن تک صفحهای به چند لایه، چند منفعت دارد:
- جداسازی بهتر نگرانیها.
- پیادهسازی لایه میتواند جایگزین شود.
- آزمایش لایه رابط کاربری میتوانند سخت باشد، که با منتقل کردن منطق مربوطه به لایههای دیگر، این کار سادهتر میشود.
در اینجا ما یک نمودار از یک برنامه تقسیم شده به ۳ لایه اصلی را مشاهده میکنیم:
- رابط کاربری (ارائه)
- دامنه (کسب و کار)
- دسترسی داده
ویترین
من برنامهای را نمونه قرار خواهم داد که یک لیست یادداشتها را مدیریت میکند. کاربر خواهد توانست که یادداشتها را ببیند و برای آنها جستجو کند.
پیادهسازی کامل را بر روی گیتهاب مشاهده نمایید.
لایه رابط کاربری
لایه رابط کاربری مسئول نمایش دادهها بر روی صفحه، و مدیریت تعاملات کاربر است.
من صفحه را به این کامپوننتها تقسیم میکنم:
- TodoContainer ارتباط بین TodoSearch، TodoList و دیگر آبجکتهای خارجی را مدیریت میکند.
- TodoSearchForm، قالب مربوط به جستجوی یادداشتها است.
- TodoList لیست یادداشتها را ذخیره میکند.
- TodoListItem: یک یادداشت تکی موجود در لیست را نمایش میدهد.
TodoSearch
این کامپوننت از هندلر handleChange برای خواندن مقدار ورودی در هنگام بروز هر تغییری استفاده میکند. TodoSearch یک ویژگی جدید را در معرض قرار میدهد: onSearch. این ویژگی میتواند توسط کامپوننت والد برای مدیریت کلیک جستجو استفاده شود.
این کامپوننت با هیچ آبجکت خارجی دیگری ارتباط برقرار نمیکند، به جز والد خود. TodoSearch یک کامپوننت نمایشی است.
export default class TodoSearch extends React.Component {
constructor(props){
super(props);
this.search = this.search.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
this.state = { text: "" };
}
search(){
const query = Object.freeze({ text: this.state.text });
if(this.props.onSearch)
this.props.onSearch(query);
}
handleChange(event) {
this.setState({text: event.target.value});
}
render() {
return <form>
<input onChange={this.handleChange} value={this.state.text} />
<button onClick={this.search} type="button">Search</button>
</form>;
}
}
TodoList
TodoList با استفاده از یک ویژگی، لیست todoها را میگیرد. این کامپوننت todoها را یک به یک به TodoListItem میفرستد.
TodoList یک کامپوننت تابعی بدون state است.
export default function TodoList(props) {
function renderTodoItem(todo){
return <TodoListItem todo={todo} key={todo.id}></TodoListItem>;
}
return <div className="todo-list">
<ul>
{ props.todos.map(renderTodoItem) }
</ul>
</div>;
}
ToDoListItem
TodoListItem، todo دریافت شده به عنوان یک پارامتر را نمایش میدهد. این کامپوننت به عنوان یک کامپوننت تابعی بدون state پیادهسازی شده است.
export default function TodoListItem(props){
return <li>
<div>{ props.todo.title}</div>
<div>{ props.todo.userName }</div>
</li>;
}
TodoContainer
کامپوننت TodoContainer به آبجکت خارجی todoStore متصل شده است و در تغییر رویدادهای آن شریک میشود.
این کامپوننت از هندلر onSearch برای خواندن شاخص جستجو از TodoSearch استفاده میکند. سپس یک لیست فیلتر شده با استفاده از todoStore میسازد و لیست جدید را به کامپوننت TodoList میفرستد.
TodoContainer تمام stateهای رابط کاربری که در این مورد آبجکت query است را نگه میدارد.
import TodoList from "./TodoList.jsx";
import TodoSearch from "./TodoSearch.jsx";
export default class TodoContainer extends React.Component {
constructor(props){
super(props);
this.todoStore = props.stores.todoStore;
this.search = this.search.bind(this);
this.reload = this.reload.bind(this);
this.query = null;
this.state = {
todos: []
};
}
componentDidMount(){
this.todoStore.onChange(this.reload);
this.todoStore.fetch();
}
reload(){
const todos = this.todoStore.getBy(this.query);
this.setState({ todos });
}
search(query){
this.query = query;
this.reload();
}
render() {
return <div>
<TodoSearch onSearch={this.search} />
<TodoList todos={this.state.todos} />
</div>;
}
}
لایه دامنه
لایه دامنه از انبار دامنه ساخته شده است. هدف اصلی انبار دامنه این است که state دامنه را ذخیره کند و آن را با سرور همگام نگه دارد.
مسئولیتهای برنامه به دو انبار دامنه تقسیم شدهاند:
- TodoStore آبجکتهای داده یادداشت را مدیریت میکند.
- UserStore آبجکتهای داده کاربر را مدیریت میکند.
انبار دامنه یک ناشر است. این لایه هر زمان که state آن تغییر میکند، منتشر میکند. کامپوننتها میتوانند در این رویدادها شریک شوند و رابط کاربری را بروزرسانی کنند.
TodoStore تنها منبع واقعی یادداشتها است.
TodoStore
import MicroEmitter from 'micro-emitter';
import partial from "lodash/partial";
export default function TodoStore(dataService, userStore){
let todos = [];
const eventEmitter = new MicroEmitter();
const CHANGE_EVENT = "change";
function fetch() {
return dataService.get().then(setLocalTodos);
}
function setLocalTodos(newTodos){
todos = newTodos;
eventEmitter.emit(CHANGE_EVENT);
}
function toViewModel(todo){
return Object.freeze({
id : todo.id,
title : todo.title,
userName : userStore.getById(todo.userId).name
});
}
function descById(todo1, todo2){
return parseInt(todo2.id) - parseInt(todo1.id);
}
function queryContainsTodo(query, todo){
if(query && query.text){
return todo.title.includes(query.text);
}
return true;
}
function getBy(query) {
const top = 25;
const byQuery = partial(queryContainsTodo, query);
return todos.filter(byQuery)
.map(toViewModel)
.sort(descById).slice(0, top);
}
function onChange(handler){
eventEmitter.on(CHANGE_EVENT, handler);
}
return Object.freeze({
fetch,
getBy,
onChange
});
}
TodoStore با استفاده از یک تابع factory پیادهسازی میشود. این آبجکت چندین متد دارد، اما فقط سه مورد از آنها عمومی هستند.
من توابع factory را به کلاسها ترجیح میدهم.
استفاده کردن، از ساختن متفاوت است. تمام dependencyهای TodoStore به عنوان پارامترهای ورودی اعلام شدهاند.
TodoStore رویدادها را در هنگام بروز هر تغییر انتشار میدهد: eventEmitter.emit(CHANGE_EVENT). یک کتابخانه میکرو انتشار دهنده رویداد، به نام MicroEmitter در آن استفاده شده است.
در اینجا مثالی از آبجکت داده دامنه یادداشت را مشاهده مینمایید:
{id : 1, title: "This is a title", userId: 10, completed: false }
لایه دسترسی داده
سرویس داده، ارتباط با API سرور را انباشته میکند.
TodoDataService
export default function TodoDataService(){
const url = "https://jsonplaceholder.typicode.com/todos";
function toJson(response){
return response.json();
}
function get() {
return fetch(url).then(toJson);
}
function add(todo) {
return fetch(url, {
method: "POST",
body: JSON.stringify(todo),
}).then(toJson);
}
return Object.freeze({
get,
add
});
}
هر دو متدهای عمومی get و add یک promise را بر میگردانند.
یک promise، در واقع یک ارجاع به یک فراخوانی ناهمگام است. ممکن است این promise در آینده با شکست مواجه شود.
نقطه ورود برنامه
فایل main.js تنها نقطه ورود برنامه است. این فایل، جایی است که:
- تمام آبجکتها ساخته میشوند و dependencyها تزریق میشوند.
- تمام کامپوننتها ساخته میشوند.
import React from "react";
import ReactDOM from 'react-dom';
import TodoDataService from "./dataaccess/TodoDataService";
import UserDataService from "./dataaccess/UserDataService";
import TodoStore from "./stores/TodoStore";
import UserStore from "./stores/UserStore";
import TodoContainer from "./components/TodoContainer.jsx";
(function startApplication(){
const userDataService = UserDataService();
const todoDataService = TodoDataService();
const userStore = UserStore(userDataService);
const todoStore = TodoStore(todoDataService, userStore);
const stores = {
todoStore,
userStore
};
function loadStaticData(){
return Promise.all([userStore.fetch()]);
}
function mountPage(){
ReactDOM.render(
<TodoContainer stores={stores} />,
document.getElementById('root'));
}
loadStaticData().then(mountPage);
})();
اجرا کننده عملیات
تمام کامپوننتها و توابع factory از ماژولها خروجی گرفته میشوند. Gulp و Browserify برای اتصال تمام ماژولها با یکدیگر استفاده میشوند. تنها یک نقطه ورود در برنامه وجود دارد و آن فایل main.js است، پس عملیات scripts که Browserify را اجرا میکند، فقط شامل این ورودی میباشد.
ESLint برای linting استفاده میشود. تمام قوانین در فایل .eslintrc.json تعریف شدهاند.
دستور npm gulp را اجرا کنید تا عملیات پیشفرض gulp را اجرا کنید. این دستور ابتدا عملیات eslint و سپس scripts را اجرا میکند.
یک عملیات دیدهبان تعریف شده است تا مراقب تغییرات فایل .js و .jsx باشد و عملیاتهای eslint و script را برگرداند.
var gulp = require('gulp')
var eslint = require('gulp-eslint');
var babelify = require('babelify');
var browserify = require('browserify');
var source = require('vinyl-source-stream');
var distFolder = "./dist";
gulp.task('eslint', function () {
gulp.src(["components/*.jsx", "dataaccess/*.js", "stores/*.js", "tools/*.js", "main.js"])
.pipe(eslint())
.pipe(eslint.format());
});
gulp.task('scripts', function () {
return browserify({
entries: 'main.js'
})
.transform(babelify.configure({
presets : ["es2015", "react"]
}))
.bundle()
.pipe(source('scripts.js'))
.pipe(gulp.dest(distFolder));
});
gulp.task('watch', function () {
gulp.watch(["components/*.jsx", "dataaccess/*.js", "stores/*.js", "tools/*.js", "main.js"], [ "eslint", "scripts" ]);
});
gulp.task( 'default', [ "eslint", "scripts" ] )
آزمایش
فریموورک Jest برای آزمایش استفاده میشود. تمام فایلهای آزمایش با پسوند .test.js نامگذاری خواهند شد. دستور npm test را اجرا کنید، تا تمام آزمایشها اجرا شوند.
TodoStore تمام dependencyها را به عنوان پارامتر میگیرد. ما میتوانیم از dependencyهای TodoDataService و UserStore تقلید کنیم و آبجکت todoStore را به صورت جداگانه آزمایش کنیم.
در اینجا آزمایش مربوط به متد todoStore.getBy() را مشاهده مینمایید:
import TodoStore from "../stores/TodoStore";
test("TodoStore can filter by title text", function() {
//arrage
const allTodos = [
{ id: 1, title : "title 1" },
{ id: 2, title : "title 2" },
{ id: 3, title : "title 3" }
];
const todoDataService = {
get : function(){
return Promise.resolve(allTodos);
}
};
const userStore = {
getById : function(){
return {
name : "Test"
};
}
};
const todoStore = TodoStore(todoDataService, userStore);
const query = { text: "title 1" };
const expectedOutputTodos = [
{ id: 1, title : "title 1" , userName : "Test"}
];
//act
todoStore.fetch().then(function makeAssertions(){
//assert
expect(expectedOutputTodos).toEqual(todoStore.getBy(query));
});
});
نتیجه گیری
معماری سه لایهای، یک جداسازی و درک بهتر از هدف لایه را فراهم میکند.
کامپوننتها برای تقسیم کردن صفحه به تکههای کوچکتری که مدیریت و استفاده مجدد از آنها ساده است، استفاده میشود.
انبارهای دامنه، state دامنه را مدیریت میکنند.
سرویسهای داده با APIهای خارجی ارتباط برقرار میکنند.
استفاده کردن، از ساختن متفاوت است. تمام آبجکتها و کامپوننتها در فایل main.js ساخته میشوند. باقی برنامه با فرض این که تمام آبجکتها ساخته شدهاند، طراحی شده است.
پیادهسازی کامل را میتوانید بر روی گیتهاب مشاهده کنید.
دیدگاه و پرسش
در حال دریافت نظرات از سرور، لطفا منتظر بمانید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید