Прежде чем узнать это, время дошло до конца 2017 года.
В прошлом году дискуссии о внешнем уровне данных все еще продолжались. Будь то TypeScript, Flow, PropTypes на уровне типа данных, уровень архитектуры приложения MVC, MVP, MVVM или уровень состояния приложения Redux, MobX, RxJS, имеют свою собственную группу преданных поклонников, но никто не может убедить других согласиться с их точкой зрения. Посмотреть.
Что касается обсуждения выбора технологии, то позиция автора всегда заключалась в том, чтобы искать точки соприкосновения, оставляя при этом различия. Теперь, когда появилось много статей, в которых обсуждаются различия между вышеперечисленными решениями, давайте ненадолго притормозим, вернемся к общим проблемам, которые эти решения пытаются решить, и попробуем дать некоторые из самых простых решений.
Далее, давайте возьмем общую архитектуру MVVM в качестве примера, чтобы проанализировать общие болевые точки интерфейсных данных слой за слоем.
Слой модели
Интерфейсный уровень модели, являющийся самым нисходящим каналом передачи данных приложения, на самом деле сильно отличается от внутреннего уровня модели. По сравнению с внутренней моделью внешняя модель не играет роли в определении структуры данных, а больше похожа на контейнер для хранения данных, возвращаемых внутренним интерфейсом.
В соответствии с этой предпосылкой, когда интерфейсы в стиле RESTful стали отраслевым стандартом, если внутренние данные возвращаются во внешний интерфейс в соответствии с наименьшей степенью детализации ресурсов данных, можем ли мы напрямую вернуть стандарт каждого интерфейса в качестве наиболее важного? о базовой модели данных? Другими словами, у нас, кажется, нет выбора, потому что данные, возвращаемые интерфейсом, являются наиболее восходящими по отношению к внешнему уровню данных и отправной точкой всего последующего потока данных.
После выяснения определения слоя модели давайте рассмотрим проблемы слоя модели.
Размер ресурса данных слишком маленький
Чрезмерная детализация ресурсов данных обычно приводит к следующим двум проблемам: одна заключается в том, что одной странице требуется доступ к нескольким интерфейсам для получения всех отображаемых данных, а другая заключается в том, что существует проблема в порядке получения каждого ресурса данных, который должны быть получены асинхронно последовательно.
Для первой проблемы обычное решение состоит в том, чтобы создать промежуточный уровень данных Node.js для интеграции интерфейса и, наконец, предоставить клиенту интерфейс на основе страниц, который согласуется с маршрутизацией на стороне клиента.
Преимущества и недостатки этого решения очень очевидны.Преимущество заключается в том, что каждой странице требуется доступ только к одному интерфейсу, а скорость загрузки страницы в производственной среде может быть эффективно повышена. С другой стороны, рендеринг на стороне сервера легко запустить, потому что на сервере готовы все данные. Но с точки зрения эффективности разработки это всего лишь способ избавиться от сложности бизнеса, и он подходит только для проектов с меньшим количеством ассоциаций между страницами и меньшей сложностью приложений.В конце концов, гранулярность ViewModel на уровне страниц по-прежнему слишком толстый, и, поскольку это решение на уровне интерфейса, возможность повторного использования практически нулевая.
На второй вопрос автор предоставляет инструментальную функцию, основанную на простейшем редукционном переходе, для связывания двух асинхронных запросов.
import isArray from 'lodash/isArray';
function createChainedAsyncAction(firstAction, handlers) {
if (!isArray(handlers)) {
throw new Error('[createChainedAsyncAction] handlers should be an array');
}
return dispatch => (
firstAction(dispatch)
.then((resultAction) => {
for (let i = 0; i < handlers.length; i += 1) {
const { status, callback } = handlers[i];
const expectedStatus = `_${status.toUpperCase()}`;
if (resultAction.type.indexOf(expectedStatus) !== -1) {
return callback(resultAction.payload)(dispatch);
}
}
return resultAction;
})
);
}
Исходя из этого, мы предлагаем общий бизнес-сценарий, чтобы помочь вам понять. Например, для веб-сайта, подобного Zhihu, внешний интерфейс может получить ответ пользователя на основе идентификатора пользователя после получения информации о пользователе для входа в систему.
// src/app/action.js
function getUser() {
return createAsyncAction('APP_GET_USER', () => (
api.get('/api/me')
));
}
function getAnswers(user) {
return createAsyncAction('APP_GET_ANSWERS', () => (
api.get(`/api/answers/${user.id}`)
));
}
function getUserAnswers() {
const handlers = [{
status: 'success',
callback: getAnswers,
}, {
status: 'error',
callback: payload => (() => {
console.log(payload);
}),
}];
return createChainedAsyncAction(getUser(), handlers);
}
export default {
getUser,
getAnswers,
getUserAnswers,
};
При выводе мы можем вывести все три действия для разных страниц, чтобы использовать их по мере необходимости в зависимости от ситуации.
Данные нельзя использовать повторно
Каждый вызов интерфейса означает сетевой запрос.До появления концепции глобального центра обработки данных многие интерфейсы не заботились о том, запрашивались ли данные, которые будут использоваться, в другом месте при разработке новых требований, но грубо завершали запрос снова для всех данных. что нужно использовать.
Это проблема, которую хочет решить Store в Redux.В глобальном хранилище разные страницы могут легко совместно использовать одни и те же данные, таким образом достигая возможности повторного использования на уровне интерфейса, то есть на уровне модели. Здесь следует отметить одну вещь: поскольку данные в Redux Store хранятся в памяти, как только пользователь обновит страницу, все данные будут потеряны, поэтому при использовании Redux Store нам также необходимо сотрудничать.Cookie
так же какLocalStorage
Выполните постоянное хранение основных данных, чтобы убедиться, что состояние приложения может быть правильно восстановлено при повторной инициализации Store в будущем. В частности, при выполнении изоморфизма необходимо убедиться, что сервер может вводить данные в хранилище в определенную позицию в HTML для использования клиентом при инициализации хранилища.
Слой ViewModel
Как уникальный слой в разработке на стороне клиента, слой ViewModel шаг за шагом развивается из контроллера MVC.Хотя ViewModel решает проблему, заключающуюся в том, что изменение модели в MVC будет напрямую отражаться на представлении, он по-прежнему не может полностью избавиться от контроллера.Основной упорной проблемой, которая подвергается критике, является то, что бизнес-логика слишком раздута. С другой стороны, концепция единой ViewModel не может напрямую сгладить огромный разрыв между бизнес-логикой и логикой отображения, уникальной для разработки на стороне клиента.
Соответствие между бизнес-логикой и презентацией логических сложных отношений
Например, общие приложения используют учетные записи социальных сетей для регистрации этой функции, менеджер по продукту хочет добиться того, чтобы после того, как пользователь подключил учетные записи социальных сетей, сначала попробуйте войти непосредственно в приложение, если оно не зарегистрировано для пользовательского приложения, автоматически регистрирующего учетные записи. , особые обстоятельства Если возвращенная под социальной сетью информация о пользователе не удовлетворяет условиям прямой регистрации (например, отсутствует почтовый ящик или номер телефона), то перейдите на страницу дополнительной информации.
在这个场景下,登录与注册是业务逻辑,根据接口返回在页面上给予用户适当的反馈,进行相应的页面跳转则是显示逻辑,如果从 Redux 的思想来看,这二者分别就是 action 与 reducer。使用上文中的链式异步请求函数,我们可以将登录与注册这两个 action 链接起来,定义二者之间的关系(登录失败后尝试验证用户信息是否足够直接注册,足够则继续请求注册接口,不足够则跳转至补充信息页面)。 код показывает, как показано ниже:
function redirectToPage(redirectUrl) {
return {
type: 'APP_REDIRECT_USER',
payload: redirectUrl,
}
}
function loginWithFacebook(facebookId, facebookToken) {
return createAsyncAction('APP_LOGIN_WITH_FACEBOOK', () => (
api.post('/auth/facebook', {
facebook_id: facebookId,
facebook_token: facebookToken,
})
));
}
function signupWithFacebook(facebookId, facebookToken, facebookEmail) {
if (!facebookEmail) {
redirectToPage('/fill-in-details');
}
return createAsyncAction('APP_SIGNUP_WITH_FACEBOOK', () => (
api.post('/accounts', {
authentication_type: 'facebook',
facebook_id: facebookId,
facebook_token: facebookToken,
email: facebookEmail,
})
));
}
function connectWithFacebook(facebookId, facebookToken, facebookEmail) {
const firstAction = loginWithFacebook(facebookId, facebookToken);
const callbackAction = signupWithFacebook(facebookId, facebookToken, facebookEmail);
const handlers = [{
status: 'success',
callback: () => (() => {}), // 用户登陆成功
}, {
status: 'error',
callback: callbackAction, // 使用 facebook 账户登陆失败,尝试帮用户注册新账户
}];
return createChainedAsyncAction(firstAction, handlers);
}
Здесь, пока мы разделяем повторно используемые действия на соответствующие степени детализации и комбинируем их в соответствии с бизнес-логикой в цепочках действий, Redux будет отправлять разные действия в разных ситуациях. Возможны следующие ситуации:
// 直接登录成功
APP_LOGIN_WITH_FACEBOOK_REQUEST
APP_LOGIN_WITH_FACEBOOK_SUCCESS
// 直接登录失败,注册信息充足
APP_LOGIN_WITH_FACEBOOK_REQUEST
APP_LOGIN_WITH_FACEBOOK_ERROR
APP_SIGNUP_WITH_FACEBOOK_REQUEST
APP_LOGIN_WITH_FACEBOOK_SUCCESS
// 直接登录失败,注册信息不足
APP_LOGIN_WITH_FACEBOOK_REQUEST
APP_LOGIN_WITH_FACEBOOK_ERROR
APP_REDIRECT_USER
Поэтому в редьюсере нам нужно только внести соответствующие изменения в данные во ViewModel, когда будет отправлено соответствующее действие, а бизнес-логика и логика отображения разделены.
Это решение похоже и отличается от MobX и RxJS. Точно так же определяется поток данных (порядок отправки действий), и ViewModel уведомляется об обновлении данных при необходимости. Разница в том, что Redux не будет автоматически запускать конвейер данных при изменении данных, но требует пользователь явно вызывает конвейер данных, как в приведенном выше примере, когда пользователь нажимает кнопку «подключиться к социальной сети». Резюмируя, возможно, он более соответствует идее redux-observable, то есть не отказывается полностью от redux, а также вводит понятие конвейера данных, но он ограничен отсутствием инструментальных функций и не может обрабатывать более сложные сценарии. Но с другой стороны, если в бизнесе действительно нет очень сложных сценариев, после понимания redux использование простейшего redux-thunk может отлично покрыть большую часть потребностей.
Раздутая бизнес-логика
Разделение и комбинирование могут быть многократно используемыми действиями, адресованными некоторой бизнес-логике, но, с другой стороны, требуется, чтобы уровень модели данных стал серьезной проблемой. Часть ViewModel страдает от разработки внешнего интерфейса и заднего формата объединения.
Здесь рекомендуется абстрагироваться от общегоSelectorа такжеFormatterконцепция для решения этой проблемы.
Как мы упоминали выше, внутренняя модель будет напрямую входить в редьюсер каждой страницы с интерфейсом.В настоящее время мы можем комбинировать данные в разных редьюсерах через селектор и форматировать окончательные данные через форматтер, чтобы они могли быть непосредственно отображены в представлении данных.
Например, на странице персонального центра пользователя нам нужно отобразить ответы, которые понравились пользователю в каждой категории, поэтому нам нужно сначала получить все категории и добавить «популярные», которых нет в бэкэнде, перед всеми категориями. , Классификация. А поскольку классификация — это очень распространенные данные, мы уже получили их на главной странице и сохранили в редьюсере главной страницы. код показывает, как показано ниже:
// src/views/account/formatter.js
import orderBy from 'lodash/orderBy';
function categoriesFormatter(categories) {
const customCategories = orderBy(categories, 'priority');
const popular = {
id: 0,
name: '热门',
shortname: 'popular',
};
customCategories.unshift(popular);
return customCategories;
}
// src/views/account/selector.js
import formatter from './formatter.js';
import homeSelector from '../home/selector.js';
const categoriesWithPopularSelector = state =>
formatter.categoriesFormatter(homeSelector.categoriesSelector(state));
export default {
categoriesWithPopularSelector,
};
После выяснения проблем, которые должен решить слой ViewModel, вы можете получить очень четкое решение, повторно используя и комбинируя действия, селекторы и средства форматирования целенаправленным образом. Исходя из того, что все данные хранятся только в соответствующем редьюсере, также решается проблема противоречивых данных на каждой странице. С другой стороны, корень проблемы несогласованности данных заключается в том, что возможность повторного использования кода слишком низка, что приводит к тому, что одни и те же данные поступают в разные конвейеры данных по-разному и в конечном итоге получают разные результаты.
Просмотр слоя
После прояснения первых двух слоев наиболее важный интерфейсный слой View становится намного проще.mapStateToProps
а такжеmapDispatchToProps
, мы можем напрямую сопоставить крайне детализированные данные отображения и комбинированную бизнес-логику с соответствующей позицией слоя представления, тем самым получив чистый и простой в отладке слой представления.
Многоразовый вид
Но проблема не кажется такой простой, потому что возможность повторного использования слоя View также является серьезной проблемой, от которой страдает внешний интерфейс.Основываясь на приведенных выше идеях, как мы должны с этим бороться?
Благодаря преимуществам таких фреймворков, как React, компонентизация внешнего интерфейса больше не является проблемой.Нам нужно только следовать следующим принципам, чтобы добиться лучшего повторного использования уровня представления.
- Все страницы принадлежат папке, и только компоненты уровня страницы будут подключены к хранилищу избыточности. Каждая страница — это отдельная папка, в которой хранятся свои действия, редюсеры, селекторы и форматтеры.
- Компонент Business хранится в папке компонентов, и бизнес-компонент не будет получен путем подключения к магазину Redux, который может получить только данные из реквизитов, чтобы обеспечить его только для обеспечения ремонта и повторного использования.
- Компоненты пользовательского интерфейса хранятся в другой папке или пакете npm. Компоненты пользовательского интерфейса не имеют ничего общего с бизнесом и содержат только логику отображения, а не бизнес-логику.
резюме
Хотя очень сложно сказать, что разрабатывая гибкие и простые в использовании библиотеки компонентов, после накопления достаточного количества доступных бизнес-компонентов и UI-компонентов новые страницы находятся на уровне данных, а могут и с других страниц. многоразовая бизнес-логика, скорость разработки нового спроса должна быть быстрее. Вместо этого все больше и больше бизнес-логики переплетается воедино, что в итоге приводит к внутренней сложности всего проекта, от нее можно только отступить.
немного опыта
Сегодня, когда новые технологии появляются одна за другой, когда мы одержимы идеей убедить других принять нашу собственную техническую точку зрения, нам по-прежнему необходимо вернуться к текущему бизнес-сценарию, чтобы увидеть, какую проблему мы пытаемся решить.
Отбрасывая небольшую часть чрезвычайно сложных интерфейсных приложений, большинство интерфейсных приложений в основном основаны на отображаемых данных, в таких сценариях передовые технологии и инфраструктура не могут напрямую решить эти проблемы, упомянутые выше. Это четкая обработка данных. идея и глубокое понимание основных концепций.Строгая спецификация командной разработки, вероятно, спасет разработчиков интерфейса, которые глубоко разбираются в сложных данных.
Как ветвь инженерии, сложность программного обеспечения не было в этих неразрешимых проблемах, но в том, как сформулировать простые правила, чтобы сделать разные модули выполнять свои соответствующие функции. Вот почему сегодня, с различными рамками, библиотеками и решениями, возникающими один за другим, мы все еще подчеркиваем основы, подчеркивая опыт и подчеркивая, чтобы увидеть сущность проблемы.
То, что сказал Ван Янмин о единстве знания и действия, современные люди часто знают, но не могут этого сделать. Но когда дело доходит до разработки программного обеспечения, мы часто впадаем в нее, как кошки, не понимая другой крайности, обе крайности явно нежелательны.