предисловие
Что такое SSR
У SSR есть два режима: одностраничный и неодностраничный.Первый — это одностраничное приложение, которое сначала обрабатывается серверной частью, а второй — режим обработки шаблонов серверной части, который полностью использует внутреннюю маршрутизацию. Они различаются степенью использования внутренней маршрутизации.
Преимущество
Почему пишет, что загружается быстро в первый раз? Обычному одностраничному приложению нужно загрузить все связанные статические ресурсы при первой загрузке, а затем начнет выполняться ядро JS, этот процесс займет определенное время, а затем запросит сетевой интерфейс, и, наконец, он будет полностью отрендерен.
Примечание: страница может быть отображена быстро, но поскольку текущий возврат — это только DOM и CSS, которые просто отображаются, а события, связанные с JS, фактически не связаны на стороне клиента, поэтому после загрузки JS текущий Страница отображается снова, что называется изоморфизмом. Таким образом, SSR должен отображать содержимое страницы быстрее и позволить пользователям увидеть его первым.
Почему оптимизировано для SEO, потому что сканеры поисковых систем будут отправлять HTTP-запросы для получения содержимого веб-страницы при сканировании информации о странице, и первые данные, отображаемые нашим сервером, возвращаются бэкэндом, а заголовок уже отображается, когда он возвращается. информация, которая удобна для поисковых роботов для сканирования контента.
Как добиться
- Выберите одностраничную структуру (сейчас я выбираю реакцию)
- Выберите структуру сервера узла (сейчас я выбираю koa2)
- Реализуйте базовую логику, чтобы сервер узла мог маршрутизировать и отображать одностраничные компоненты (это разделено на множество небольших точек реализации, которые будут обсуждаться позже).
- Оптимизация среды разработки и выпуска для автоматизации инструмента сборки (веб-пакета).
1. Реагировать на приложение
2.серверное приложение
3. Основная реализация
- 1) Бэкенд перехватывает маршрут и находит компонент страницы реакции X, который необходимо отобразить в соответствии с маршрутом.
- 2) Вызовите интерфейс, который необходимо запросить при инициализации компонента X. После синхронного получения данных используйте метод renderToString реакции для рендеринга компонента для рендеринга строки узла.
- 3) Бэкэнд получает базовый HTML-файл, вставляет в тело отрендеренную строку узла, а также может манипулировать в нем заголовком, скриптом и другими узлами. Верните клиенту полный HTML.
- 4) Клиент получает HTML, возвращенный серверной частью, отображает и загружает в него JS и, наконец, завершает реактивный изоморфизм.
import Index from "../pages/index";
import List from "../pages/list";
const routers = [
{ exact: true, path: "/", component: Index },
{ exact: true, path: "/list", component: List }
];
Мы создаем два индексных файла JS и страницы в папке маршрутизатора клиента:
Сопоставление путей маршрутизации и компонентов настраивается на страницах Код примерно выглядит следующим образом, поэтому его можно использовать как для маршрутизации на стороне клиента, так и для маршрутизации на стороне сервера.
import Index from "../pages/index";
import List from "../pages/list";
const routers = [
{ exact: true, path: "/", component: Index },
{ exact: true, path: "/list", component: List }
];
//注册页面和引入组件,存在对象中,server路由匹配后渲染
export const clientPages = (() => {
const pages = {};
routers.forEach(route => {
pages[route.path] = route.component;
});
return pages;
})();
export default routers;
Код в маршруте сервера примерно такой. После того, как сервер получит запрос на получение, он сопоставляется с путем. Если путь пути содержит сопоставленный компонент страницы, получите компонент и визуализируйте его. Это наш первый шаг: серверная часть маршрутизация перехвата, найдите компонент страницы реагирования, который необходимо отобразить в соответствии с путем.
import { clientPages } from "./../../client/router/pages";
router.get("*", (ctx, next) => {
let component = clientPages[ctx.path];
if (component) {
const data = await component.getInitialProps();
//因为component是变量,所以需要create
const dom = renderToString(
React.createElement(component, {
ssrData: data
})
)
}
})
Этот шаг важнее, зачем нам статический метод вместо того, чтобы писать запрос прямо в willmount. Потому что, когда сервер использует renderToString для рендеринга компонентов, жизненный цикл будет выполняться только до первого рендера после willmount.Внутри willmount запрос асинхронный.По завершении первого рендера асинхронные данные не получаются.В это время renderToString уже вернулся. Тогда данные инициализации нашей страницы пропали, и возвращенный HTML оказался не таким, как мы ожидали. Поэтому определяется статический метод, который получается до создания экземпляра компонента и выполняется синхронно.После получения данных данные передаются компоненту для рендеринга через пропсы.
Так как же реализован этот метод? Посмотрим на base.js по скриншоту кода:
import React from "react";
export default class Base extends React.Component {
//override 获取需要服务端首次渲染的异步数据
static async getInitialProps() {
return null;
}
static title = "react ssr";
//page组件中不要重写constructor
constructor(props) {
super(props);
//如果定义了静态state,按照生命周期,state应该优先于ssrData
if (this.constructor.state) {
this.state = {
...this.constructor.state
};
}
//如果是首次渲染,会拿到ssrData
if (props.ssrData) {
if (this.state) {
this.state = {
...this.state,
...props.ssrData
};
} else {
this.state = {
...props.ssrData
};
}
}
}
async componentWillMount() {
//客户端运行时
if (typeof window != "undefined") {
if (!this.props.ssrData) {
//非首次渲染,也就是单页面路由状态改变,直接调用静态方法
//我们不确定有没有异步代码,如果getInitialProps直接返回一个初始化state,这样会造成本身应该同步执行的,因为await没有同步执行,造成状态混乱
//所以建议初始化state需要写在class属性中,用static静态方法定义,constructor时会将其合并到实例中。
//为什么不直接写state属性而要加static,因为默认属性会执行在constructor之后,这样会覆盖constructor定义的state
const data = await this.constructor.getInitialProps(); //静态方法,通过构造函数获取
if (data) {
this.setState({ ...data });
}
}
//设置标题
document.title = this.constructor.title;
}
}
}
Сначала создайте новый базовый компонент в страницах клиента.База наследует React.Component.Все компоненты страницы в страницах должны наследовать эту базу.В базе есть статический метод getInitialProps, который в основном возвращает асинхронные данные, необходимые для компонента. инициализация. Если есть первоначальный запрос ajax, его следует переопределить в этом методе и вернуть объект данных.
Конструктор определяет, имеет ли компонент страницы статическое свойство состояния, определенное при инициализации, и если да, то передает его объекту состояния, созданному компонентом.Если реквизиту передан ssrData, значение ssrData передается объекту состояния компонента.
КомпонентWillMount в базе определит, нужно ли выполнять метод getInitialProps.Если данные были синхронно получены и переданы в свойствах до создания экземпляра компонента во время рендеринга на стороне сервера, они игнорируются.
Если в клиентской среде, возможны два случая.
Первый: когда пользователь заходит на страницу в первый раз, это данные, запрашиваемые сервером, после получения данных сервер отрисовывает компонент на сервере, а также сохраняет данные в коде HTML-скрипта для определить глобальную переменную ssrData, как показано на рисунке ниже, react будет передавать глобальные ssrData компоненту страницы, когда одностраничное приложение зарегистрировано и изоморфно.В это время, когда компонент страницы изоморфно отображается на стороне клиента , он может продолжать использовать предыдущие данные на стороне сервера, чтобы он также поддерживал согласованность изоморфизма и избегал повторного запроса.
Второй случай: текущий пользователь переключает маршруты в одной странице, поэтому рендеринга на стороне сервера нет, затем выполнить метод getInitialProps для возврата данных сразу в состояние, что почти эквивалентно выполнению запроса в willmount. Таким образом, мы можем использовать набор кода, совместимый с рендерингом на стороне сервера и рендерингом одной страницы.
client/app.js
import React from "react";
import { hydrate } from "react-dom";
import Router from "./router";
class App extends React.Component {
render() {
return <Router ssrData={this.props.ssrData} ssrPath={this.props.ssrPath} />;
}
}
hydrate(
<App ssrData={window.ssrData} ssrPath={window.ssrPath} />,
document.getElementById("root")
);
Давайте посмотрим, как написать компонент страницы. Ниже приведен снимок экрана компонента страницы Index. Index наследует Base и определяет статическое состояние. Метод конструктора компонента передаст этот объект объекту состояния, созданному компонентом. причина, по которой статический метод используется для записи данных по умолчанию. Данные должны гарантировать, что определенное состояние по умолчанию сначала передается в состояние объекта экземпляра, а данные реквизита, переданные запросом интерфейса, передаются в состояние объекта экземпляра. .
Почему бы не написать свойство состояния напрямую и добавить static, потому что свойство состояния будет выполняться после конструктора, что перезапишет состояние, определенное конструктором, то есть перезапишет данные, возвращаемые нашим getInitialProps.
export default class Index extends Base {
//注意看看:base关于getInitialProps的注释
static state = {
desc: "Hello world~"
};
//替代componentWillMount
static async getInitialProps() {
let data;
const res = await request.get("/api/getData");
if (!res.errCode) data = res.data;
return {
data
};
}
}
Примечание: В среде рендеринга на стороне сервера при выполнении renderToString будет создан экземпляр компонента и будет возвращен DOM в виде строки.В этом процессе жизненный цикл компонента реакции будет выполняться только до тех пор, пока рендер после willmount.
3)Мы пишем файл HTML, примерно следующим образом. В настоящее время соответствующая строка узла отрисована, и бэкэнд должен вернуть текст HTML.Содержимое должно включать заголовок, узел и упакованный JS, который необходимо загрузить в конце, и по очереди заменить заполнитель HTML.
<!DOCTYPE html>
<html lang="en">
<head>
<title>/*title*/</title>
</head>
<body>
<div id="root">??</div>
<script>
/*getInitialProps*/
</script>
<script src="/*app*/"></script>
<script src="/*vendor*/"></script>
</body>
</html>
server/router.js
indexHtml = indexHtml.replace("/*title*/", component.title);
indexHtml = indexHtml.replace(
"/*getInitialProps*/",
`window.ssrData=${JSON.stringify(data)};window.ssrPath='${ctx.path}'`
);
indexHtml = indexHtml.replace("/*app*/", bundles.app);
indexHtml = indexHtml.replace("/*vendor*/", bundles.vendor);
ctx.response.body = indexHtml;
next();
4)Наконец, после загрузки JS на стороне клиента запустится react, и вместо обычного ReactDOM.render будет выполнен изоморфный метод ReactDOM.hydra.
import React from "react";
import { hydrate } from "react-dom";
import Router from "./router";
class App extends React.Component {
render() {
return <Router ssrData={this.props.ssrData} ssrPath={this.props.ssrPath} />;
}}
hydrate(
<App ssrData={window.ssrData} ssrPath={window.ssrPath} />,
document.getElementById("root")
);
Ниже приведена общая блок-схема первого процесса рендеринга, нажмите, чтобы увеличить
CSS-обработка
Теперь мы завершили основную логику, но есть проблема. Я обнаружил, что когда серверная часть отображает компонент, загрузчик стилей сообщит об ошибке, загрузчик стилей найдет CSS, от которого зависит компонент, и загрузит стиль в заголовок HTML при загрузке компонента, но когда мы render на стороне сервера. Объекта окна нет, поэтому код внутри загрузчика стилей сообщит об ошибке.
Вебпак на стороне сервера должен удалить загрузчик стилей и заменить его другим методом.Позже я назначаю стиль статической переменной компонента, а затем возвращаю его на фронтенд через рендеринг на стороне сервера, но есть проблема, я могу получить только стиль текущего компонента, нет способа получить стиль подкомпонента.Если вы хотите добавить статический метод к подкомпоненту, а затем найти способ его получить, это будет слишком хлопотно.
Позже я нашел библиотеку isomorphic-style-loader, которая может поддерживать нужные нам функции, читать ее исходный код и использование, назначать стили компонентам с помощью функций более высокого порядка, а затем использовать контекст реакции, чтобы получить текущую потребность в отображении стилей. всех компонентов и, наконец, вставить стили в HTML, что решает проблему невозможности импорта стилей подкомпонентов. Но я думаю, что это немного хлопотно.Сначала вам нужно определить функции высшего порядка всех компонентов и импортировать эту библиотеку.Затем вам нужно написать соответствующий код в маршрутизаторе для сбора стилей, и, наконец, вставить их в HTML.
После этого я определил метод ProcessSsrStyle.Входным параметром является файл стиля.Логика заключается в том, чтобы судить об окружении.Если сервер загружает стиль в DOM текущего компонента, если это клиент, то он не будет обработан (потому что у клиента есть загрузчик стилей). Реализация и использование очень просты, а именно:
ProcessSsrStyle.js
import React from "react";
export default style => {
if (typeof window != "undefined") {
//客户端
return;
}
return <style>{style}</style>;
};
использовать:
render() {
return (
<div className="index">
{ProcessSsrStyle(style)}
</div>
);
}
Содержимое HTML, возвращаемого сервером, выглядит следующим образом: пользователь может сразу увидеть полный стиль страницы, и когда изоморфизм реакции на стороне клиента будет завершен, DOM будет заменен чистым DOM, потому что метод ProcessSsrStyle не будет выводить стиль на стороне клиента, и окончательный стиль - после выполнения загрузчика в шапке тоже будут стили, и страница не будет меняться несогласованно, все это нечувствительно к пользователю.
Пока реализовано большинство основных функций, но в более поздней разработке я обнаружил, что все не так просто, потому что среда разработки кажется слишком недружественной, эффективность разработки низкая, и ее нужно перезапускать вручную.
среда разработки
Поговорим о том, как работает начальная среда разработки:
- npm run dev для запуска среды разработки
- код сервера пакетов webpack.client-dev.js, код будет упакован в dist/server
- webpack.server-dev.js код клиента пакета, код будет упакован в dist/client
- Запустите серверное приложение, порт 9999
- Запустите webpack-dev-сервер, порт 8888.
После упаковки веб-пакета запускаются две службы: одна — приложение на стороне сервера с портом 9999, а другая — сервер разработки на стороне клиента с портом 8888. Сервер разработки будет отслеживать и упаковывать клиентский код, который может быть обновлен на стороне клиента.Время, клиентский код горячего обновления в режиме реального времени.
При доступе к localhost:9999 сервер вернет HTML, а путь сценария JS в HTML, возвращенный нашим сервером, будет адресом порта dev-serve, как показано ниже. То есть клиентская программа и серверная программа упакованы отдельно и запускают две разные службы порта.
В производственной среде, т.к. dev-серверу не требуется мониторинг и горячее обновление, достаточно только одного сервиса, как показано на рисунке ниже, сервер регистрирует папку статического ресурса:
server/app.js
app.use(
staticCache("dist/client", {
cacheControl: "no-cache,public",
gzip: true
})
);
Текущая система сборки различает производственную среду и среду разработки.Нет никаких проблем с текущей конструкцией среды разработки. Однако проблема среды разработки более очевидна, самая большая проблема заключается в том, что сервер не обновляется в горячем режиме, не переупаковывается и не перезапускается. Это приведет ко многим проблемам.Самая серьезная из них это то,что фронтенд обновил компоненты,но сервер не обновился.Поэтому при изоморфизме будут возникать несоответствия,что приведет к ошибкам.Некоторые ошибки будут влиять на работу , Единственным решением является перезагрузка. Такой опыт развития невыносим. Позже я стал подумывать о том, чтобы сделать горячее обновление на стороне сервера.
Мониторить, упаковывать, перезапускать
Первоначально мой подход заключался в том, чтобы прислушиваться к изменениям, упаковывать и перезапускать приложение. Помните наш файл client/router/pages.js, маршрутизация как на стороне клиента, так и на стороне сервера представила этот файл, поэтому зависимости упаковки как на стороне сервера, так и на стороне клиента имеют pages.js, поэтому все зависимости, связанные с компонентами страницы могут контролироваться клиентом и сервером, когда компонент обновляется, dev-сервер помог нам отслеживать и обновлять клиентский код.Теперь нам нужно разобраться со следующим, как обновить и перезапустить код сервера.
На самом деле способ очень простой, то есть включить мониторинг в конфигурации пакета сервера, а затем прописать перезапускаемый плагин в конфигурации плагина, код плагина такой:
plugins: [
new function() {
this.apply = compiler => {
//自定义注册钩子函数,watch监听修改并编译完成后,done被触发,callback必须执行,否则不会执行后续流程
compiler.hooks.done.tap(
"recomplie_complete",
(compilation, callback) => {
if (serverChildProcess) {
console.log("server recomplie completed");
serverChildProcess.kill();
}
serverChildProcess = child_process.spawn("node", [
path.resolve(cwd, "dist/server/bundle.js"),
"dev"
]);
serverChildProcess.stdout.on("data", data => {
console.log(`server out: ${data}`);
});
serverChildProcess.stderr.on("data", data => {
console.log(`server err: ${data}`);
});
callback && callback();
}
);
};
}()
]
При первом запуске webpack плагин запустит подпроцесс, запустит app.js, при изменении файла снова скомпилирует, определит есть ли подпроцесс, если есть, убьет подпроцесс, а затем перезапустит подпроцесс, реализующий автоматическую перезагрузку. Поскольку клиент и сервер представляют собой две разные службы упаковки и конфигурации, при изменении файла они будут перекомпилированы одновременно.Чтобы убедиться, что скомпилированная операция соответствует ожиданиям, необходимо убедиться, что сервер скомпилирован сначала, а клиент компилируется после завершения, поэтому в конфигурации просмотра клиента добавьте небольшую задержку, как показано на рисунке ниже, по умолчанию 300 миллисекунд, поэтому сервер выполняет компиляцию через 300 мс, а клиент выполняет компиляцию через 1000 мс.
watchOptions: {
ignored: ["node_modules"],
aggregateTimeout: 1000 //优化,尽量保证后端重新打包先执行完
}
Теперь проблема с перезагрузкой решена, но я не думаю, что этого достаточно, потому что большую часть времени разработки компоненты в pages.js, то есть частота обновления кода стороны отображения, будут очень частыми. вы всегда перезапускаете и компилируете внутренний код, я думаю, что его эффективность слишком низкая. Вот думаю сделать еще одну оптимизацию.
Извлеките клиент/маршрутизатор/страницы и упакуйте их отдельно
Процесс должен быть таким, чтобы, увеличивая профиль webpack.server-dev-pages.js, отдельно упакованный и прослушивающий dist/pages, сервер определял, выполняется ли среда разработки кода каждый раз, когда маршрут слушает процесс повторного получения dist/pages пакета, сервер слушает, чтобы игнорировать папку конфигурации клиента.
Это кажется немного запутанным, но конечный результат заключается в том, что при обновлении зависимых компонентов на страницах webpack.server-dev-pages.js перекомпилируется и упаковывается в dist/pages, серверное приложение не компилируется и не перезапускается, требуется только Повторная выборка последнего пакета dist/pages в маршрутизации приложения на стороне сервера гарантирует, что приложение на стороне сервера обновит все клиентские компоненты без компиляции и перезапуска приложения на стороне сервера. При изменении кода самого сервера он будет скомпилирован и перезапущен автоматически.
Итак, в конце концов, наша среда разработки должна запустить 3 конфигурации упаковки.
- webpack.server-dev-pages
- webpack.server-dev
- webpack.client-dev
сервер/маршрутизатор, как очистить и обновить пакет страниц
const path = require("path");
const cwd = process.cwd();
delete __non_webpack_require__.cache[
__non_webpack_require__.resolve(
path.resolve(cwd, "dist/pages/pages.js")
)];
component = __non_webpack_require__(
path.resolve(cwd, "dist/pages/pages.js")
).clientPages[ctx.path];
На данный момент в основном реализована удовлетворительная среда разработки. Позже я понял, что нет необходимости переупаковывать внутренние страницы каждый раз, когда я обновлял CSS. Кроме того, CSS был непоследовательным при изоморфности. Это было только предупреждение и не имело существенного влияния. Поэтому я проигнорировал файл less в server-dev-pages (потому что использую меньше). Это вызовет проблему.Поскольку страницы не обновляются, то при обновлении страницы сначала будет отображаться старый стиль, а затем завершится изоморфизм и сразу изменится новый стиль.В среде разработки этот момент приемлемо и ни на что не влияет. Но избегается ненужная компиляция.
watchOptions: {
ignored: ["**/*.less", "node_modules"] //忽略less,样式修改并不会影响同构
}
дела не сделаны
- Упакован в более оберточный штатив
- Управление областью CSS
- Более инкапсулированная конфигурация веб-пакета
- В среде разработки путь к изображению будет несогласованным
Первоначальная цель создания моей собственной станции — учиться, плюс моя собственная польза, так что здесь слишком много личных вещей. Извлеченные с моего собственного сайта, я удалил множество пакетов и кодов, просто чтобы другие могли быстрее понять основной код. В коде есть много комментариев, которые могут помочь другим понять.Если вы хотите использовать текущую библиотеку для разработки собственного веб-сайта, это вполне возможно, и это также может помочь вам лучше понять ее. Если он используется для коммерческих проектов, рекомендуется nextjs.
CSS не управляет областью действия, поэтому, если вы хотите изолировать область действия, вручную добавьте изоляцию CSS верхнего уровня, например .index{ ..... }, обертывающую слой, или попробуйте самостоятельно ввести сторонний пакет.
Общая конфигурация веб-пакета может быть инкапсулирована в файл, а затем импортирована в каждый файл, а затем изменена индивидуально. Но когда я ранее просматривал другие коды, я обнаружил, что этот метод усложнит чтение, а содержимого конфигурации не так много, поэтому без инкапсуляции он выглядит более интуитивно понятным.
В среде разработки путь к изображению будет несогласованным, например, адрес запроса адреса клиента — localhost...assets/xx.jpg, а адрес сервера — assets/xx.jpg. Могут быть предупреждения, но это не так. оказывать воздействие. Потому что только один — абсолютный путь, а другой — относительный путь.
наконец
На этот раз я вполне доволен реализацией SSR-рендеринга на стороне сервера, и это заняло много времени. Почувствуйте скорость загрузки, добро пожаловать на большую поэтическую станцию,dashiren.cn/. Некоторые страницы имеют интерфейсные запросы, такие какdashiren.cn/space, скорость загрузки по-прежнему высока.
Хранилище готово, качаем и пробуем.После установки зависимостей запускаем команду.GitHub.com/letter-v/реагировать-…
Кодировать слова непросто, пожалуйста, лайкните~