Базовый скелет и маршрут проекта были построены в предыдущих разделах, но этого недостаточно для фактической разработки проекта. По мере роста бизнеса увеличивается и код последовательности прикладного уровня.Если все коды упаковать в один файл, первая загрузка приведет к относительно большему времени и увеличению трафика (для мобильного терминала). Приложение содержит много страниц, и пользователь будет посещать только одну страницу за раз, код страницы, не посещаемой пользователем, не должен загружаться перед посещением, а ресурсы, необходимые для загрузки страницы, должны быть изменены только тогда, когда пользователь посещения. Ранее построенные проекты пока не связаны с взаимодействием с данными.Основой бизнеса являются данные.В этом разделе будет представлена сегментация кода на основе маршрутизации, взаимодействие данных и синхронизация.
Предыдущий раздел:Изоморфизм внешней и внутренней маршрутизации
См. адрес исходного кода в конце статьи.
Часть кода в этом разделе была переписана, нажмите, чтобы узнать подробностиздесь
разделение кода
Отложенная загрузка маршрута
Существует множество решений при разделении кода, напримерreact-loadable,react-async-component,loadable-components, все три поддерживают разделение кода и отложенную загрузку, а также все поддерживают рендеринг на стороне сервера. Когда react-loadable и react-async-component выполняют рендеринг на стороне сервера, шаги очень громоздки, loadable-components предоставляет простые операции для поддержки рендеринга на стороне сервера, а здесь используются loadable-components
Если вы используете webpack4, используйте загружаемые компонентыновая версия,здесьэто полный пример, переписанный для использования новой версии
Установить загружаемые компоненты
npm install loadable-components
Измените компоненты в конфигурации маршрутизации на динамический импорт.
src/router/index.js
import Loadable from "loadable-components";
const router = [
{
path: "/bar",
component: Loadable(() => import("../views/Bar"))
},
{
path: "/baz",
component: Loadable(() => import("../views/Baz"))
},
{
path: "/foo",
component: Loadable(() => import("../views/Foo"))
},
{
path: "/top-list",
component: Loadable(() => import("../views/TopList")),
exact: true
}
];
import()
Динамический импорт — это синтаксис, поддерживаемый начиная с Webpack2, который по существу использует промисы.Если вы хотите работать в старых браузерах, вам нужноes6-promiseилиpromise-polyfill
для разбораimport()
Синтаксис, вам нужно настроить плагин babelsyntax-dynamic-import, тогда он будет работать в одностраничном приложении. Здесь загружаемые компоненты используются для рендеринга на стороне сервера, а конфигурация babel выглядит следующим образом.
"plugins": [
"loadable-components/babel"
]
Примечание. Здесь используется версия babel6.x.
использовать на стороне клиентаloadComponents
Метод загружает компонент, а затем устанавливает его. Запись клиента изменяется следующим образом
src/entry-client.js
import { loadComponents } from "loadable-components";
import App from "./App";
// 开始渲染之前加载所需的组件
loadComponents().then(() => {
ReactDOM.hydrate(<App />, document.getElementById("app"));
});
вызов сервераgetLoadableState()
Затем вставьте состояние в html-фрагмент
src/server.js
const { getLoadableState } = require("loadable-components/server");
...
let component = createApp(context, req.url);
// 提取可加载状态
getLoadableState(component).then(loadableState => {
let html = ReactDOMServer.renderToString(component);
if (context.url) { // 当发生重定向时,静态路由会设置url
res.redirect(context.url);
return;
}
if (!context.status) { // 无status字段表示路由匹配成功
// 获取组件内的head对象,必须在组件renderToString后获取
let head = component.type.head.renderStatic();
// 替换注释节点为渲染后的html字符串
let htmlStr = template
.replace(/<title>.*<\/title>/, `${head.title.toString()}`)
.replace("<!--react-ssr-head-->", `${head.meta.toString()}\n${head.link.toString()}`)
.replace("<!--react-ssr-outlet-->", `<div id='app'>${html}</div>\n${loadableState.getScriptTag()}`);
// 将渲染后的html字符串发送给客户端
res.send(htmlStr);
} else {
res.status(context.status).send("error code:" + context.status);
}
});
передачаgetLoadableState()
Передайте корневой компонент, дождитесь загрузки состояния, рендеринга и вызоваloadableState.getScriptTag()
Вставьте возвращенный скрипт в html-шаблон
Требуется рендеринг на стороне сервераmodules
опции
const AsyncComponent = loadable(() => import('./MyComponent'), {
modules: ['./MyComponent'],
})
Эту опцию не нужно прописывать вручную, используйтеloadable-components/babel
Подойдет плагин.import()
Синтаксис не поддерживается в узле, поэтому на сервере также необходимо настроить плагинdynamic-import-node
Установитьdynamic-import-node
npm install babel-plugin-dynamic-import-node --save-dev
Клиент не нуждается в этом плагине.Далее измените конфигурацию веб-пакета и используйте клиент.babelrc
файл, сервер через загрузчикoptions
возможность указать конфигурацию Babel
Будуwebpack.config.base.js
Следующая конфигурация перемещена вwebpack.config.client.js
середина
{
test: /\.(js|jsx)$/,
loader: ["babel-loader", "eslint-loader"],
exclude: /node_modules/
}
webpack.config.client.js
rules: [
{
test: /\.(js|jsx)$/,
loader: ["babel-loader", "eslint-loader"],
exclude: /node_modules/
},
...util.styleLoaders({
sourceMap: isProd ? true : false,
usePostCSS: true,
extract: isProd ? true : false
})
]
Конфигурация пакета сервера изменена следующим образом
webpack.config.server.js
rules: [
{
test: /\.(js|jsx)$/,
use: [
{
loader: "babel-loader",
options: {
babelrc: false,
presets: [
"react",
[
"env",
{ "targets": { "node": "current" } }
]
],
"plugins": [ "dynamic-import-node", "loadable-components/babel" ]
}
},
{ loader: "eslint-loader" }
],
exclude: /node_modules/
},
...util.styleLoaders({
sourceMap: true,
usePostCSS: true,
extract: true
})
]
бегатьnpm run dev
, откройте браузер и введитеhttp://localhost:3000
, вы можете увидеть загрузку сначала в сетевой панелиapp.b73b88f66d1cc5797747.js
, а затем загрузите js, необходимый для текущей страницы бара (та, что на рисунке ниже3.b73b88f66d1cc5797747.js
)
При выборе других маршрутов соответствующие js будут загружены и выполнены.
Оптимизация упаковки Webpack
В реальном использовании, при итеративном обновлении приложения, файл после запакованного файла будет становиться все больше и больше Основной файл скриптаapp.xxx.js
Он содержит сторонние модули и бизнес-код.Бизнес-код может измениться в любое время, в то время как сторонние модули останутся в основном неизменными в течение определенного периода времени, если вы не обновите фреймворк или библиотеку, которую используете в настоящее время.app.xxx.js
ххх использовать вchunkhash
имя,chunkhash
Указывает хэш содержимого чанка, чанк стороннего модуля не изменится, мы его выделяем для удобства кеширования браузером
Для получения дополнительной информации о output.filename, пожалуйста, отметьтездесь
Чтобы извлечь сторонние модули, вам нужно использовать собственный webpackCommonsChunkPluginплагин, а для лучшего кеширования извлекаем модуль начальной загрузки webpack в отдельный файл
webpack.config.client.js
plugins: [
...
new webpack.optimize.CommonsChunkPlugin({
name: "vendor",
minChunks: function(module) {
// 阻止.css文件资源打包到vendor chunk中
if(module.resource && /\.css$/.test(module.resource)) {
return false;
}
// node_modules目录下的模块打包到vendor chunk中
return module.context && module.context.includes("node_modules");
}
}),
// 分离webpack引导模块
new webpack.optimize.CommonsChunkPlugin({
name: "manifest",
minChunks: Infinity
})
]
Благодаря вышеуказанной конфигурации пакет, содержащий сторонние модули, будет упакованvendor.xxx.js
а такжеmanifest.xxx.js
Примечание. Здесь используется версия webpack3.x, а плагин CommonsChunkPlugin удален из webpack4. Пожалуйста, используйте веб-пакет4SplitChunksPlugin
Проект используется только в режиме производстваchunkhash
, затем запуститеnpm run build
Пакет
Исправлятьsrc/App.jsx
Код в , а затем упаковать его
можно увидетьvender.xxx.js
Имя файла не изменилось,app.xxx.js
Изменено, имена упакованных файлов 4-х асинхронных компонентов не изменились.mainfest.xxx.js
изменился
Предварительная выборка данных и синхронизация
Рендеринг на стороне сервера должен возвращать содержимое страницы с сервера клиенту.Если какое-то содержимое получено вызовом запроса интерфейса, то данные должны быть загружены заранее, затем отрендерены, а затем вызваныReactDOMServer.renderToString()
Для отображения полной страницы HTML-контент, отображаемый клиентом, должен соответствовать HTML-контенту, возвращаемому сервером, что требует обеспечения согласованности данных на клиенте и данных на сервере.
Redux используется здесь для управления данными.Когда Redux выполняет рендеринг на стороне сервера, он создает новый Store для каждого запроса, а затем инициализирует состояние и возвращает его клиенту.Клиент получает это состояние и создает новый Store.
Рендеринг Redux на стороне сервераПример
Присоединяйтесь к Редукс
Установить связанные зависимости
npm install redux redux-thunk react-redux
Сначала создайте базовую структуру проекта Redux.
actionTypes.js
export const SET_TOP_LIST = "SET_TOP_LIST";
export const SET_TOP_DETAIL = "SET_TOP_DETAIL";
actions.js
import { SET_TOP_LIST, SET_TOP_DETAIL } from "./actionTypes";
export function setTopList(topList) {
return { type: SET_TOP_LIST, topList };
}
export function setTopDetail(topDetail) {
return { type: SET_TOP_DETAIL, topDetail };
}
reducers.js
import { combineReducers } from "redux";
import * as ActionTypes from "./actionTypes";
const initialState = {
topList: [],
topDetail: {}
}
function topList(topList = initialState.topList, action) {
switch (action.type) {
case ActionTypes.SET_TOP_LIST:
return action.topList;
default:
return topList;
}
}
function topDetail(topDetail = initialState.topDetail, action) {
switch (action.type) {
case ActionTypes.SET_TOP_DETAIL:
return action.topDetail;
default:
return topDetail;
}
}
const reducer = combineReducers({
topList,
topDetail
});
export default reducer;
store.js
import { createStore, applyMiddleware } from "redux";
import thunkMiddleware from "redux-thunk";
import reducer from "./reducers";
// 导出函数,以便客户端和服务端根据初始state创建store
export default (store) => {
return createStore(
reducer,
store,
applyMiddleware(thunkMiddleware) // 允许store能dispatch函数
);
}
Здесь необходимо использовать данные запросаАсинхронное действие, Магазин по умолчанию может только отправлять объекты, используйтеredux-thunkПромежуточное ПО может выполнять функцию диспетчеризации
следующий вaction.js
Напишите функцию создания асинхронного действия в
import { getTopList, getTopDetail } from "../api";
...
export function fatchTopList() {
// dispatch由thunkMiddleware传入
return (dispatch, getState) => {
return getTopList().then(response => {
const data = response.data;
if (data.code === 0) {
// 获取数据后dispatch,存入store
dispatch(setTopList(data.data.topList));
}
});
}
}
export function fetchTopDetail(id) {
return (dispatch, getState) => {
return getTopDetail(id).then(response => {
const data = response.data;
if (data.code === 0) {
const topinfo = data.topinfo;
const top = {
id: topinfo.topID,
name: topinfo.ListName,
pic: topinfo.pic,
info: topinfo.info
};
dispatch(setTopDetail(top));
}
});
}
}
Функция создания действия в приведенном выше коде возвращает функцию с асинхронным запросом, в котором могут быть отправлены другие действия. Здесь в этой функции вызывается запрос интерфейса.После завершения запроса данные сохраняются в состоянии через диспетчеризацию, а затем возвращается промис, чтобы после завершения асинхронного запроса можно было выполнить другую обработку. Для поддержки сервера и клиента в асинхронном запросе вы можете использоватьaxiosили использовать его в браузереfetch API, используется в узлеnode-fetch
Здесь в качестве источника данных используется интерфейс QQ Music, а сервер используетaxios, клиент не поддерживает междоменное использованиеjsonp,src/api/index.js
Код выглядит следующим образом
import axios from "axios";
import jsonp from "jsonp";
const topListUrl = "https://c.y.qq.com/v8/fcg-bin/fcg_myqq_toplist.fcg";
if (process.env.REACT_ENV === "server") {
return axios.get(topListUrl + "?format=json");
} else {
// 客户端使用jsonp请求
return new Promise((resolve, reject) => {
jsonp(topListUrl + "?format=jsonp", {
param: "jsonpCallback",
prefix: "callback"
}, (err, data) => {
if (!err) {
const response = {};
response.data = data;
resolve(response);
} else {
reject(err);
}
});
});
}
Если вы хотите узнать больше о музыкальном интерфейсе QQ, нажмитездесь
Чтобы предоставить компонентам презентации React доступ к состоянию, нужно использоватьreact-redux
модульныйconnect
Метод подключается к Store и записывает компонент-контейнерTopList
src/containers/TopList.jsx
import { connect } from "react-redux"
import TopList from "../views/TopList";
const mapStateToProps = (state) => ({
topList: state.topList
});
export default connect(mapStateToProps)(TopList);
существуетsrc/router/index.js
Центральная ручка имеет оригиналimport("../views/TopList"))
изменить наimport("../containers/TopList"))
{
path: "/top-list",
component: Loadable(() => import("../containers/TopList")),
exact: true
}
дисплейный компонентTopList
Состояние доступа через реквизиты в
class TopList extends React.Component {
render() {
const { topList } = this.props;
return (
<div>
...
<ul className="list-wrapper">
{
topList.map(item => {
return <li className="list-item" key={item.id}>
{item.title}
</li>;
})
}
</ul>
</div>
)
}
}
Далее вводим файл на стороне сервераentry-server.js
используется вProvider
пакетStaticRouter
и экспортcreateStore
функция
src/entry-server.js
import createStore from "./redux/store";
...
const createApp = (context, url, store) => {
const App = () => {
return (
<Provider store={store}>
<StaticRouter context={context} location={url}>
<Root setHead={(head) => App.head = head}/>
</StaticRouter>
</Provider>
)
}
return <App />;
}
module.exports = {
createApp,
createStore
};
server.js
получено вcreateStore
Функция создает Store без данных
let store = createStore({});
// 存放组件内部路由相关属性,包括状态码,地址信息,重定向的url
let context = {};
let component = createApp(context, req.url, store);
Клиент также используетProvider
Упаковать, создать магазин без данных и передать
src/App.jsx
import createStore from "./redux/store";
...
let App;
if (process.env.REACT_ENV === "server") {
// 服务端导出Root组件
App = Root;
} else {
const Provider = require("react-redux").Provider;
const store = createStore({});
App = () => {
return (
<Provider store={store}>
<Router>
<Root />
</Router>
</Provider>
);
};
}
export default App;
Предварительная выборка данных
Есть два способа получить данные.Первый — указать метод загрузки данных на маршруте, как показано ниже.
const routes = [
{
path: "/",
component: Root,
loadData: () => getSomeData()
}
...
];
Другой способ — поместить метод загрузки данных в соответствующий компонент и определить его как статический метод, что более интуитивно понятно.
В данном примере используется второй подход.TopList
Статический метод определенный компонентasyncData
Входящее хранилище для отправки асинхронного действия, определенного здесь как статический метод, поскольку до того, как компонент еще не был создан недоступным экземпляромthis
static asyncData(store) {
return store.dispatch(fatchTopList());
}
fatchTopList
Возвращаемая функцияredux-thunk
вызов промежуточного программного обеспечения,redux-thunk
Промежуточное ПО будет передавать возвращаемое значение вызывающей функции в качестве возвращаемого значения метода отправки.
Теперь вам нужно получить компонент маршрутизации в момент запросаasyncData
метод и вызов, реагировать-маршрутизатор вreact-router-configМодуль предоставляет намmatchRoutes
метод сопоставления маршрута в соответствии с конфигурацией маршрута
Чтобы использовать сопоставление маршрутов на сервере, необходимо изменить конфигурацию маршрута сentry-server.js
экспорт в
src/entry-server.js
import { router } from "./router";
...
module.exports = {
createApp,
createStore,
router
};
существуетserver.js
получено вrouter
Конфигурация маршрутизации, вызываемая при загрузке всех асинхронных компонентов.matchRoutes()
Выполните сопоставление маршрутов и вызовите все соответствующие маршрутыasyncData
Рендеринг после метода
let promises;
getLoadableState(component).then(loadableState => {
// 匹配路由
let matchs = matchRoutes(router, req.path);
promises = matchs.map(({ route, match }) => {
const asyncData = route.component.Component.asyncData;
// match.params获取匹配的路由参数
return asyncData ? asyncData(store, Object.assign(match.params, req.query)) : Promise.resolve(null);
});
// resolve所有asyncData
Promise.all(promises).then(() => {
// 异步数据请求完成后进行服务端render
handleRender();
}).catch(error => {
console.log(error);
res.status(500).send("Internal server error");
});
...
}
используется в приведенном выше кодеroute.component
Что получается, так это асинхронный компонент, возвращаемый загружаемыми компонентами,route.component.Component
это реальный компонент маршрутизации, который должен быть вызванgetLoadableState()
можно получить позже. если компонент существуетasyncData
способ поставитьpromises
В массиве, если он не существует, верните разрешенный промис, а затем разрешите все промисы. некоторые URL-адреса похожи/path/:id
,match.params
используется для получения URL в:id
Представленные параметры. Если какие-то параметры передаются в виде ?, их можно передать черезreq.query
получить, слиться вmatch.params
, передается компоненту для обработки
Примечание. Используйте второй параметр в matchRoutes.
req.path
,req.path
Полученный URL-адрес не содержит параметра запроса, поэтому его можно правильно сопоставить.
Синхронные данные
Сервер запрашивает данные заранее и сохраняет их в Store.Клиент инициализирует экземпляр Store в соответствии с этим состоянием, пока сервер загружает данные и вызываетgetState()
Получить состояние и вернуть его клиенту, а клиент может получить это состояние
существуетserver.js
Получить начальное состояние изwindow.__INITIAL_STATE__
сэкономить на клиенте
src/server.js
let preloadedState = {};
...
// resolve所有asyncData
Promise.all(promises).then(() => {
// 获取预加载的state,供客户端初始化
preloadedState = store.getState();
// 异步数据请求完成后进行服务端render
handleRender();
}).catch(error => {
console.log(error);
res.status(500).send("Internal server error");
});
...
let htmlStr = template
.replace(/<title>.*<\/title>/, `${head.title.toString()}`)
.replace("<!--react-ssr-head-->", `${head.meta.toString()}\n${head.link.toString()}
<script type="text/javascript">
window.__INITIAL_STATE__ = ${JSON.stringify(preloadedState)}
</script>
`)
.replace("<!--react-ssr-outlet-->", `<div id='app'>${html}</div>\n${loadableState.getScriptTag()}`);
App.jsx
получено вwindow.__INITIAL_STATE__
// 获取服务端初始化的state,创建store
const initialState = window.__INITIAL_STATE__;
const store = createStore(initialState);
На этом этапе данные клиента и сервера могут быть синхронизированы.
Сбор данных клиента
Для маршрутизации на стороне клиента это делается в браузере, в это время клиенту также необходимо запросить данные.
существуетTopList
компонентcomponentDidMount
в функции жизненного циклаdispatch
Функция создания асинхронного действияfatchTopList
Возвращаемое значение
componentDidMount() {
this.props.dispatch(fatchTopList());
}
Этот компонент уже создан, поэтому к нему можно получить доступ черезthis
Посетите магазинdispatch
, и эта функция будет выполняться только на стороне клиента
вы можете захотетьcomponentWillMount
серединаdispatch
Асинхронное действие, чиновник изменил функцию жизненного цикла (пожалуйста, ткнитездесь), включено в версии 16.xcomponentWillMount
,componentWillReceiveProps
а такжеcomponentWillUpdate
Предупреждение об истечении срока действия, эти три периодические функции будут удалены в версии 17. Рекомендуется использоватьcomponentDidMount
Получить данные от (пожалуйста, отметьтездесь)
Бывает ситуация, что если сервер загружает данные заранее, то когда клиент монтирует DOM, он выполняетcomponentDidMount
Выполнит еще одну загрузку данных, на этот раз загрузка данных является избыточной, см. следующий рисунок.
доступhttp://localhost:3000/top-list
, сервер выполнил предварительную выборку данных и обработал результирующую строку HTML.Красное поле — это запрос, отправленный после монтирования клиентского DOM. Чтобы избежать этого, добавьте новое состояние с именемclientShouldLoad
По умолчаниюtrue
, указывающий, загружает ли клиент данные, т.е.clientShouldLoad
Напишите actionType, функцию создания действия и функцию редуктора.
actionTypes.js
export const SET_CLIENT_LOAD = "SET_CLIENT_LOAD";
actions.js
import { SET_CLIENT_LOAD, SET_TOP_LIST, SET_TOP_DETAIL } from "./actionTypes";
export function setClientLoad(clientShouldLoad) {
return { type: SET_CLIENT_LOAD, clientShouldLoad };
}
reducers.js
const initialState = {
clientShouldLoad: true,
topList: [],
topDetail: {}
}
function clientShouldLoad(clientShouldLoad = initialState.clientShouldLoad, action) {
switch (action.type) {
case ActionTypes.SET_CLIENT_LOAD:
return action.clientShouldLoad;
default:
return clientShouldLoad;
}
}
...
const reducer = combineReducers({
clientShouldLoad,
topList,
topDetail
});
компонент контейнераTopList
средняя параclientShouldLoad
карта
src/containers/TopList.jsx
const mapStateToProps = (state) => ({
clientShouldLoad: state.clientShouldLoad,
topList: state.topList
});
Изменить, когда сервер выполняет предварительную выборку данныхclientShouldLoad
дляfalse
, судить после монтирования клиентаclientShouldLoad
Этоtrue
, еслиtrue
получить данные дляfalse
будуclientShouldLoad
изменить наtrue
, чтобы клиент мог получить его после перехода на другие маршрутыclientShouldLoad
дляtrue
, для сбора данных
В действии создайте асинхронную функцию, в настоящее время выполняются данные на стороне сервера, запрос завершен отправкой
actions.js
export function fatchTopList() {
// dispatch由thunkMiddleware传入
return (dispatch, getState) => {
return getTopList().then(response => {
const data = response.data;
if (data.code === 0) {
// 获取数据后dispatch,存入store
dispatch(setTopList(data.data.topList));
}
if (process.env.REACT_ENV === "server") {
dispatch(setClientLoad(false));
}
});
}
}
TopList
Добавьте оценку компонентам
TopList.jsx
componentDidMount() {
// 判断是否需要加载数据
if (this.props.clientShouldLoad === true) {
this.props.dispatch(fatchTopList());
} else {
// 客户端执行后,将客户端是否加载数据设置为true
this.props.dispatch(setClientLoad(true));
}
}
посетить в это времяhttp://localhost:3000/top-list
, у клиента на один запрос данных меньше. Как показано ниже
Суммировать
В этом разделе используется функция динамического импорта веб-пакета для ленивой загрузки маршрутов, чтобы уменьшить размер упакованных файлов и загружать их по запросу Плагин CommonsChunkPlugin, который поставляется с веб-пакетом, используется для разделения сторонних модулей, чтобы клиент мог лучше кэшировать . Общий клиент получает данные после монтирования DOM, а рендеринг на стороне сервера должен заранее загрузить данные на сервер, а затем вернуть данные клиенту, а клиент получает данные, возвращенные сервером, чтобы гарантировать что внешние и внутренние данные согласованы
Построение рендеринга на стороне сервера — очень утомительный и сложный процесс. Одной статьи недостаточно, чтобы представить основные моменты, необходимые для фактической разработки. участие Много очков. Официального полного набора кейсов для рендеринга на стороне сервера нет, так что подход не единственный
наконец
Рендеринг на стороне сервера включает в себя внутреннее поле.Помимо оптимизации на стороне клиента, в реальных проектах также требуется оптимизация на стороне сервера. Если вы используете рендеринг на стороне сервера в продакшене и вам нужно выполнять загрузку на стороне сервера при большом количестве пользователей, выберите разумную стратегию кэширования.