предисловие
Прежде чем страница будет представлена пользователю, она должна пройти через три процесса: загрузка статического ресурса, запрос внутреннего интерфейса и рендеринг.Что нам нужно сделать, так это защититься от возможных нештатных ситуаций в каждом процессе, поддерживать бесперебойную работу пользователя. опыта, а также иметь дело с Атакой извне.
Антисеть
Текущая основная модель исследований и разработок — это разделение передней и задней частей, возьмем React в качестве примера.
function App() {
const [data, setData] = useState(null);
useEffect(() => {
(async () => {
const data = await request();
setData(data);
})();
});
if (!data) return null;
return (
<div className="App">
<h1>Hello {data.name}</h1>
</div>
);
}
Сеть плохая, данные возвращаются медленно, страница не отображается, и всегда отображается пустая страница, и опыт очень плохой.В общем, мы добавим переход, чтобы он стал таким:
function App() {
...
if (!data) return <Loading />;
...
}
Посмотрите демо:CodeSandbox
Это может решить проблему белого экрана страницы до возврата данных, но игнорирует время загрузки статических ресурсов.В это время страница все еще находится в состоянии белого экрана, поэтому также должен быть переход эффект перед загрузкой статических ресурсов. Попробуйте изменить приведенный выше пример:
<html>
<head>
<title>首页</title>
<style>
.loading {
...
}
</style>
</head>
<body>
<div id="app">
<div class="loading"><span></span></div>
</div>
<script src="manifest.js"></script>
<script src="vendor.js"></script>
<script src="main.js"></script>
</body>
</html>
Посмотрите демо:CodeSandbox
Загрузка загружаемого фрагмента сначала, а затем загрузка ресурсов, кажется, решает общую проблему перехода, но если вы внимательно посмотрите, вы обнаружите, что анимация начинается снова после некоторого воспроизведения, и ощущение фрагментации становится более серьезным. , поэтому React не должен занимать страницу до тех пор, пока данные не вернутся, попробуйте перемоделировать ее снова:
/* render.js */
import React from "react";
import ReactDOM from "react-dom";
export default function render(Component, props) {
const rootElement = document.getElementById("root");
ReactDOM.render(<Component {...props} />, rootElement);
}
/* index.js */
import render from "./render";
import request from "./request";
import App from "./App";
(async () => {
const data = await request();
render(App, { data });
})();
Посмотрите демо:CodeSandbox
Прежде чем содержимое страницы будет представлено пользователю, будет поддерживаться эффект анимации загрузки, чтобы избежать прерывания работы пользователя из-за сетевых причин.
Анти-интерфейс
После того, как статические ресурсы загружены, мы начинаем общаться с бэкендом для получения данных страницы.Во-первых, нам нужно разобраться со следующими возможными исключениями.
тайм-аут
Время ожидания, которое может допустить пользователь, составляет около 3–5 секунд с момента доступа к веб-странице до момента ее представления. После того, как время загрузки статического ресурса составляет примерно 1–2 секунды, запрос интерфейса должен вернуть результат в течение 3 секунд.
Если пользовательская сеть плохая, и мы не устанавливаем тайм-аут интерфейса, страница всегда будет в состоянии загрузки, и пользователь уйдет напрямую без эффективной обратной связи. Поэтому нам нужно установить разумный тайм-аут и дать пользователю обратную связь, когда тайм-аут срабатывает.
Мы решили использовать нативную выборку для инициации запроса, но, к сожалению, выборка не поддерживает настройку параметров тайм-аута, поэтому нам нужно обернуть ее вручную:
async function request(url, options = {}) {
const { timeout, ...restOptions } = options;
const response = await Promise.race([
fetch(url, restOptions),
timeoutFn(timeout),
]);
const { data } = await response.json();
return data;
}
function timeoutFn(ms = 3000) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(Error('timeout'));
}, ms);
});
}
Затем запрос в случае тайм-аута:
async function request(url, options = {}) {
const { timeout, ...restOptions } = options;
try {
const response = await Promise.race([
baseRequest(url, restOptions),
timeoutFn(timeout),
]);
const { data } = await response.json();
return data;
} catch (error) {
if (error.message === 'timeout') {
render(() => <span>请求超时,请重试</span>);
}
throw error;
}
}
Доступна функция тайм-аута, но пользователь может либо обновить страницу самостоятельно при повторной попытке, либо он может только выйти и снова войти, и опыт все еще недостаточно дружелюбен.
Идеальная ситуация должна позволять пользователю повторить операцию непосредственно на текущей странице, без процесса обновления страницы или выхода из нее. Мы снова корректируем код, чтобы смоделировать относительно полный пример:
Посмотрите демо:CodeSandbox
обработка ошибок
Общая обработка ошибок
Получив результат запроса, сначала обрабатываем ошибки, связанные с сетью:
const statusText = {
401: '请重新登录',
403: '没有操作权限',
404: '请求不存在',
500: '服务器异常',
...
};
function request(url, options = {}, callback) {
const { timeout, ...restOptions } = options;
try {
const response = await Promise.race([
fetch(url, restOptions),
timeoutFn(timeout),
]);
const { status } = response;
if (status === 200) {
const { data } = await response.json();
callback(data);
return;
}
render(
PageError,
{
children: statusText[status] || '系统异常,请稍后重试'
}
);
} catch (error) {
if (error.message === 'timeout') {
render(PageError, {
key: Math.random(),
onFetch() {
request(url, options, callback);
},
children: '请求超时,点击重试'
});
}
throw error;
}
}
обработка бизнес-ошибок
Затем обработайте бизнес-ошибки, обычно возвращаемые серверной частью, и сначала договоритесь с серверной частью о возврате структуры данных:
{
success: true/false,
data: { // success为true时返回
id: '69887645366',
desc: '这是产品描述',
},
errorCode: 'E123456', // success为false时返回
errorMsg: '产品id不能为空', // success为false时返回
}
Обработка ошибок:
if (status === 200) {
const { success, data, errorMsg } = await response.json();
if (success) {
callback(data);
return;
}
render(PageError, { children: errorMsg });
}
Посмотрите демо:CodeSandbox
Отмена запроса
Если вы часто пишете страницы React SPA, вы должны были столкнуться с такой ошибкой:
Причина в том, что запрос был инициирован входом в компонент А, и быстро переключился на компонент Б. Компонент А был уничтожен. Когда запрос вернулся, вызов setState сообщил об ошибке. Давайте рассмотрим простой пример:
Посмотрите демо:CodeSandbox
Решение тоже очень простое, при размонтировании компонента запрос отменяется, к сожалению, fetch его не поддерживает, модифицируем:
function request(url, options = {}, callback) {
const fetchPromise = fetch(url, options)
.then(response => response.json());
let abort;
const abortPromise = new Promise((resolve, reject) => {
abort = () => {
reject(Error('abort'));
};
});
Promise.race([fetchPromise, abortPromise])
.then(({ data }) => {
callback(data);
}).catch(() => { });
return abort;
}
useEffect(() => {
const abort = request('https://cnodejs.org/api/v1/topic/5433d5e4e737cbe96dcef312', {}, setData);
return () => {
abort();
};
});
Посмотрите демо:CodeSandbox
До сих пор мы в основном решили различные ситуации интерфейсных исключений, и теперь мы можем приступить к написанию бизнес-логики.
Рекомендуется выбрать библиотеку запросов Http, аналогичную axios в производственной среде, собственные возможности выборки слишком слабы.
Анти-рендеринг
Обработка исключений
Предположим, есть страница, отображающая баланс пользователя, которая выглядит так
Структура данных, обычно возвращаемая серверной частью, выглядит следующим образом:
{ rest: { amount: "10" } }
Логика внешнего рендеринга обычно такая:
<div>
<strong>余额:</strong>
<span className="highlight">{rest.amount}元</span>
</div>
В один прекрасный день в бэкенде была написана ошибка, запрос был выполнен успешно, но остальная структура не была возвращена нормально.По вышеуказанному методу записи было сообщено об ошибке решительно.Cannot read property 'amount' of undefined
, страница пуста.
Может быть, подход некоторых людей должен быть пустым:
<span className="highlight">{rest && rest.amount}元</span>
Такой подход порождает две проблемы
- Многие поля должны быть пустыми, много лишнего кода, плохая читабельность
- Основные данные отображаются нечетко, что вводит пользователей в заблуждение и легко приводит к жалобам клиентов.
Компромиссное решение — сделать сообщение об ошибке, чтобы избежать белого экрана.В React мы можем использовать ErrorBoundary для унифицированной обработки:
class ErrorBoundary extends Component {
static getDerivedStateFromError() {
return { hasError: true };
}
state = {
hasError: false,
};
componentDidCatch(error, info) {
// reportError(error, info);
}
render() {
const { hasError } = this.state;
const { children } = this.props;
if (hasError) {
return <div>系统异常,请稍后再试</div>;
}
return children;
}
}
function render(Component, props) {
const rootElement = document.getElementById("root");
ReactDOM.render(
<ErrorBoundary>
<Component {...props} />
</ErrorBoundary>,
rootElement
);
}
Посмотрите демо:CodeSandbox
понижаемый
Однажды бизнес выдвинул требование, добавить баннерную рекламу внизу страницы баланса и добавить интерфейс получения рекламы:
function requestAd(callback) {
callback({ ad: { desc: "这是一个广告" } });
}
К сожалению, рекламный интерфейс нестабилен, а возвращаемые данные часто имеют проблемы.Согласно способу обработки приведенного выше примера, они будут поступать напрямую в ErrorBoundary для отображения исключения, которое явно не соответствует нашим ожиданиям.
Итак, нам нужно понизить рейтинг непрофильного бизнеса:
<div>
<strong>余额:</strong>
<span className="highlight">{rest.amount}元</span>
<ErrorBoundary fallback>
<Ad />
</ErrorBoundary>
</div>
Посмотрите демо:CodeSandbox
Противотяжелая обработка
Отправка формы — очень распространенный сценарий. Как правило, есть два способа предотвратить повторные клики.
Кнопка анти-вес
Добавьте защиту кнопок от веса, например:
function App() {
const [applying, setApplying] = useState(false);
const handleSubmit = async () => {
if (applying) return;
setApplying(true);
try {
await request();
} catch (error) {
setApplying(false);
}
};
return (
<div className='App'>
<button onClick={handleSubmit}>
{applying ? '提交中...' : '提交'}
</button>
</div>
);
}
Преимущество состоит в том, что это не влияет на работу пользователя со страницей в целом, а недостатком является то, что для этого требуется статус управления страницей.
Глобальный антивес
Сделайте общее затенение страницы, например:
function request(url) {
Loading.show('请求中...');
try {
await fetch(url);
} catch (error) {
// show error
} finally {
Loading.hide();
}
}
function App() {
const handleSubmit = () => {
request();
};
return (
<div className='App'>
<button onClick={handleSubmit}>提交</button>
</div>
);
}
Преимущество в том, что вам не нужно управлять собственным состоянием на странице, а недостаток в том, что подсказка тяжелая и будет блокировать другие операции пользователя.
Анти-атака
xss
Атаки с внедрением скриптов, такие как оставление сообщения под определенным постом, внедряют скрипт для получения файла cookie текущего вошедшего в систему пользователя:
<script>report(document.cookie)</script>
Если веб-сайт не избегает вывода содержимого сообщения, оно будет внедрено в скрипт, и все пользователи, посетившие сообщение, станут жертвами.
Если веб-сайт выводит экранирование, все увидят такой контент:
<script>report(document.cookie)</script>
В настоящее время основные библиотеки или фреймворки помогают нам избежать вывода по умолчанию.Как и React, если мы должны отображать фрагменты HTML, нам нужно использовать опасно SetInnerHTML.
csrf
Подделка межсайтового скриптинга, например оставление сообщения под публикацией на веб-сайте www.a.com и размещение фишинговой ссылки, ссылка будет переходить на страницу www.b.com, разработанную злоумышленником. очень прост и автоматически инициируется Запрос ответа на сообщение
<form action="http://www.a.com/replay">
<input type="text" name="content" value="这是自动回复">
</form>
Пользователь, просматривающий сообщение, случайно нажимает на ссылку, и происходит автоматический ответ. Распространенным методом защиты является добавление проверки токена. www.a.com выдает токен через файл cookie. При выполнении операции записи токен в файле cookie считывается и помещается в заголовок запроса для проверки на стороне сервера. Из-за ограничения политики одного и того же источника браузера веб-сайт b не может прочитать токен веб-сайта a.
Другой способ — добавить проверку реферера, только доменные имена в белом списке разрешены для записи. Как правило, для обеспечения безопасности веб-сайта используются два метода в сочетании.
csrf нужно защищать на уровне сетевых запросов.Только фреймворк может предоставить полноценные функции, такие как Angular, который обычно нужно интегрировать самим.
резюме
На различные исключения, перечисленные выше, на практике приходится менее 1% предполагаемой суммы, но почти 99% нашего базового кода написано для этой цели. Квалифицированные программисты сначала думают, как защититься от экстремальных исключений в процессе написания кода, и только обработав этот 1% исключений, они смогут лучше обслуживать оставшиеся 99%.
о нас:
Мы являемся технической командой по страхованию муравьев из бизнес-группы финансового страхования муравьев. Мы молодая команда (без бремени исторического стека технологий), текущий средний возраст 92 года (убрать самый высокий балл 8х лет - лидер команды, убрать самый низкий балл 97 лет - брат стажер). Мы поддерживаем практически весь страховой бизнес Ali Group. В 2018 году созданное нами общее сокровище произвело фурор в страховой отрасли, а в 2019 году мы готовили и реализовывали несколько крупных проектов. Теперь, с быстрым развитием бизнес-группы, команда также быстро расширяется.Приглашаем всех мастеров фронтенда присоединиться к нам~
Мы надеемся, что вы обладаете: прочной технической базой, глубокими знаниями в определенной области (узлы/интерактивный маркетинг/визуализация данных и т. д.); способны быстро и непрерывно учиться в процессе обучения; оптимистичны, веселы, живы и общительны.
Если вы заинтересованы в том, чтобы присоединиться к нам, пожалуйста, отправьте свое резюме по электронной почте: jacky.yyy@antfin.com