Эй, ребята, я написал одинРендеринг на стороне сервера React + Koa SSRСтатья о полугодовой давности.Недавно пересматривал.Обнаружил что библиотека мейнстримного компонента ленивой загрузки устарела,а то статья перед SSR похоже имеет отношение к React-V16,особенно V16 новый Stream API, только что упомянутый в конце предыдущей статьи, поэтому добавьте эти новые функции в эту версию части 2
Why use [Part II]?: Go to play
The Last of Us
and wait forThe Last of Us Part II
🚸
🎉Основной контент:
- ✂️ Замена
react-loadable
,использоватьloadable-components - 📉 Используйте
loadable-components
Для реализации функций асинхронного компонента на стороне браузера и на стороне сервера - 🚰 Рендеринг на стороне сервера с использованием API потока реакции
- 💾Добавить механизм кэширования для контента, отображаемого на сервере (html), подходящий для синхронного и потокового API.
✂️ Заменить реактивно-загружаемый
react-loadable не поддерживается в течение длительного времени, и он не совместим с последними версиями webpack4+ и babel7+, и будет предупреждение об устаревании, если вы используетеkoa-web-kitДля v2.8 и более ранних версий при сборке веб-пакета появится предупреждение, и в нем могут быть некоторые потенциальные неизвестные ямы, поэтому первое, что нам нужно сделать, это заменить его другой библиотекой и следовать новейшимReact.lazy|React Suspense
Этот тип API полностью совместим,loadable-components
ЯвляетсяОфициальная рекомендацияБиблиотека, если мы хотим лениво загружать компоненты на стороне клиента, но также хотим реализовать SSR (React.lazy
SSR в настоящее время не поддерживается).
Сначала устанавливаем необходимые библиотеки:
# For `dependencies`:
npm i @loadable/component @loadable/server
# For `devDependencies`:
npm i -D @loadable/babel-plugin @loadable/webpack-plugin
Затем вы можете поместить его в соответствующий файл конфигурации webpack и файл конфигурации babel.react-loadable/webpack
а такжеreact-loadable/babel
удалить, заменить на@loadable/webpack-plugin
а также@loadable/babel-plugin
.
Затем следующий шаг, который нам нужно, чтобы внести некоторые изменения в наш компонент Lazy загружены.
📉 Используйте загруженные компоненты для реализации функционального браузера асинхронного компонента и сервера
В месте, где вам нужно лениво загружать компоненты React:
// import Loadable from 'react-loadable';
import loadable from '@loadable/component';
const Loading = <h3>Loading...</h3>;
const HelloAsyncLoadable = loadable(
() => import('components/Hello'),
{ fallback: Loading, }
);
//简单使用
export default MyComponent() {
return (
<div>
<HelloAsyncLoadable />
</div>
)
}
//配合 react-router 使用
export default MyComponent() {
return (
<Router>
<Route path="/hello" render={props => <HelloAsyncLoadable {...props}/>}/>
</Router>
)
}
По сути, это похоже на предыдущее использование react-loadable: передать обратный вызов, вернуть динамический импорт и, при желании, передать компоненты, которые должны отображаться во время загрузки.
Затем нам нужно в файле вводаhydrate
Контент, отображаемый сервером, вsrc/index.js
:
import React from 'react';
import ReactDOM from 'react-dom';
import { loadableReady } from '@loadable/component';
import App from './App';
loadableReady(() => {
ReactDOM.hydrate(
<App />,
document.getElementById('app')
);
});
Хорошо, вышеизложенное в основном является модификацией, которую должен сделать клиент.Далее нам нужно изменить код на стороне сервера, чтобы загружаемые компоненты могли отлично работать в среде SSR.
При использовании react-loadable раньше нам нужно вызывать на стороне сервераLoadable.preloadAll()
Для предзагрузки всех асинхронных компонентов, потому что нет необходимости асинхронно в реальном времени загружать компоненты на стороне сервера, все они могут быть загружены при инициализации, но при использовании loadable-components они уже не нужны, поэтому удалите вызов этого метода напрямую. Затем в нашем файле записи веб-пакета на стороне сервера:
import path from 'path';
import { StaticRouter } from 'react-router-dom';
import ReactDOMServer from 'react-dom/server';
import { ChunkExtractor } from '@loadable/server';
import AppRoutes from 'src/AppRoutes';
//...可能还一下其他的库
function render(url, initialData = {}) {
const extractor = new ChunkExtractor({ statsFile: path.resolve('../dist/loadable-stats.json') });
const jsx = extractor.collectChunks(
<StaticRouter location={url}>
<AppRoutes initialData={data} />
</StaticRouter>
);
const html = ReactDOMServer.renderToString(jsx);
const renderedScriptTags = extractor.getScriptTags();
const renderedLinkTags = extractor.getLinkTags();
const renderedStyleTags = extractor.getStyleTags();
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>React App</title>
${renderedLinkTags}
${renderedStyleTags}
</head>
<body>
<div id="app">${html}</div>
<script type="text/javascript">window.__INITIAL_DATA__ = ${JSON.stringify(
initialData
)}</script>
${renderedScriptTags}
</body>
</html>
`;
}
На самом деле этоrenderToString
Были внесены некоторые изменения в соседний фрагмент, а некоторые методы записи были изменены в соответствии с новой библиотекой, что в принципе нормально для синхронного рендеринга 😀.
🚰 Рендеринг на стороне сервера с использованием React Stream API
В React v16+ команда React добавила Stream API.renderToNodeStream
Для повышения производительности рендеринга больших приложений React из-за однопоточной функции JS частые синхронные вызовыrenderToString
Это затормозит цикл обработки событий, заставив другие http-запросы/задачи ждать долгое время, что повлияет на производительность, поэтому далее мы используем потоковый API для повышения производительности рендеринга.
В качестве примера возьмем маршрут коа:
router.get('/index', async ctx => {
//防止koa自动处理response, 我们要直接把react stream pipe到ctx.res
ctx.respond = false;
//见下面render方法
const {htmlStream, extractor} = render(ctx.url);
const before = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
${extractor.getStyleTags()}
</head>
<body><div id="app">`;
//先往res里html 头部信息,包括div容器的一半
ctx.res.write(before);
//把react放回的stream pipe进res, 并且传入`end:false`关闭流的自动关闭,因为我们还有下面一半的html没有写进去
htmlStream.pipe(
ctx.res,
{ end: false }
);
//监听react stream的结束,然后把后面剩下的html写进html document
htmlStream.on('end', () => {
const after = `</div>
<script type="text/javascript">window.__INITIAL_DATA__ = ${JSON.stringify(
extra.initialData || {}
)}</script>
${extractor.getScriptTags()}
</body>
</html>`;
ctx.res.write(after);
//全部写完后,结束掉http response
ctx.res.end();
});
});
function render(url){
//...
//替换renderToString 为 renderToNodeStream,返回一个ReadableStream,其他都差不多
const htmlStream = ReactDOMServer.renderToNodeStream(jsx);
return {
htmlStream,
extractor,
}
//...
}
Вышеприведенный код аннотирован для объяснения функции каждой строки. Он в основном разделен на три части. Сначала мы пишем html, относящийся к заголовку ответа, а затем направляем readableStream, возвращаемый реакцией на ответ, отслеживаем конец поток реакции, а потом писать остальное.Под общий html, а потом вручную вызыватьres.end()
Завершить поток ответов, потому что мы отключили автоматическое закрытие потока ответов выше, поэтому мы должны завершить его здесь вручную, иначе браузер всегда будет находиться в состоянии ожидания.
После использования Stream API в порядке, у нас все еще есть общая проблема в производственной среде: для каждого входящего запроса, особенно некоторых статических страниц, нам фактически не нужно повторно отображать приложение один раз, в этом случае и синхронный рендеринг, и рендеринг потока будет иметь большее или меньшее влияние, особенно когда приложение очень большое, поэтому для решения этой проблемы нам нужно добавить слой кеша посередине, мы можем хранить его в памяти, файле или базе данных, в зависимости на фактической ситуации вашего состояния проекта.
💾Добавить механизм кэширования для рендеринга на стороне сервера, подходящий для синхронного и потокового API.
если мы используемrenderToString
На самом деле это очень просто, просто возьмите html и сохраните его где-нибудь в соответствии с ключом (url или другим), но это может быть сложно для потокового рендеринга. Поскольку мы передали поток реакции непосредственно в ответ, здесь мы используем 2 типа потока:ReadableStream
(Reactom.rendertonOdestream) иWritableStream
(ctx.res), но на самом деле в узле есть и другие типы потоков, среди которыхTransformStreamТип может помочь нам решить вышеуказанную проблему с потоком, мы можем направить react readableStream на TransformStream, а затем направить этот TransformStream в res в процессе преобразования (на самом деле мы не изменяли здесь никаких данных, просто чтобы получить все данные html ), мы можем получить весь контент, отображаемый с помощью react, а затем объединить все фрагменты, полученные в конце преобразования, чтобы сформировать полный html, а затем кэшировать его, как метод синхронного рендеринга 🦄
Ладно, без ерунды, сразу к коду:
const { Transform } = require('stream');
//这里简单用Map作为缓存的地方
const cache = new Map();
//临时的数组用来把react stream每次拿到的数据块存起来
const bufferedChunks = [];
//创建一个transform Stream来获取所有的chunk
const cacheStream = new Transform({
//每次从react stream拿到数据后,会调用此方法,存到bufferedChunks里面,然后原封不动的扔给res
transform(data, enc, cb) {
bufferedChunks.push(data);
cb(null, data);
},
//等全部结束后会调用flush
flush(cb) {
//把bufferedChunks组合起来,转成html字符串,set到cache中
cache.set(key, Buffer.concat(bufferedChunks).toString() );
cb();
},
});
Приведенный выше код можно инкапсулировать в метод, чтобы его было удобно вызывать каждый раз, когда приходит запрос, а затем, когда мы его используем:
//假设上面的代码已经封装到createCacheStream方法里了,key可以为当前的url,或者其他的
const cacheStream = createCacheStream(key);
//cacheStream现在会pipe到res
cacheStream.pipe(
res,
{ end: false }
);
//这里只显示部分html
const before = ` <!DOCTYPE html> <html lang="en"> <head>...`;
//现在是往cacheStream里直接写html
cacheStream.write(before);
// res.write(before);
//react stream pipe到cacheStream
htmlStream.pipe(
cacheStream,
{ end: false }
);
//同上监听react渲染结束
htmlStream.on('end', () => {
const after = `</div>
<script type="text/javascript">window.__INITIAL_DATA__ = ${JSON.stringify( {} )}</script>
${extractor.getScriptTags()}
</body>
</html>`;
cacheStream.write(after);
console.log('streaming rest html content done!');
//结束http response
res.end();
//结束cacheStream
cacheStream.end();
});
Выше мы кидаем htmlStream в cacheStream через пайплайн, чтобы cacheStream мог получить html, отрендеренный React, и кэшировать его, а затем в следующий раз, когда придет тот же запрос url, мы можем проверить его по ключу (например:cache.has(key)
) Является ли текущий URL-адрес уже отрендеренным html, и если да, то передать его прямо в браузер без повторного рендеринга.
Ну, вышеизложенное является основным содержанием этого обновления SSR.
В заключение
Основное содержание части II приведено выше.Мы в основном заменили загружаемые файлы, которые больше не поддерживаются, а затем использовали потоковый API для повышения производительности рендеринга больших приложений React, а также дополнительно улучшили скорость отклика, добавив слой кеша. 🎉. Выше могут быть некоторые связанные с потоками API, которые должны быть поняты незнакомыми учащимися в первую очередь.node streamСвязанный контент, если вы хотите проверить базовую конфигурацию SSR, вы также можете просмотретьпервая частьСодержание.
🙌Ждите третью часть🙌
English Version: React Server Side Rendering with Koa Part II