Евгений Шпилевский, Яндекс
Евгений Шпилевский, Разработчик интерфейсов
const serverConfig = require('./src/build/configs/server/webpack.config').default;
const clientConfig = require('./src/build/configs/client/webpack.config').default;
module.exports = [serverConfig, clientConfig];
const client = {
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
],
}
const server = {
test: /\.scss$/,
loader: 'null-loader',
}
dist
├── asset-manifest.json
├── client
│ └── assets
│ ├── _page.home.css
│ ├── _page.home.js
│ ├── _page.notFound.css
│ ├── _page.notFound.js
│ ├── _page.photo.css
│ ├── _page.photo.js
│ ├── main.css
│ └── main.js
└── main.js
import express from 'express';
import { join } from 'path';
const app = express();
app.use(express.static(join(__dirname, 'client')));
app.listen(3000);
export function App() {
return (
<h1>Hello world</h1>
);
}
src
└── components
└── App
├── App.scss
├── App.tsx
├── App@client.tsx
└── App@server.tsx
import { App as BaseApp } from 'components/App/App';
export function App() {
return (
<html lang="ru">
<body>
<div id="root">
<BaseApp />
</div>
</body>
</html>
);
}
new WebpackManifestPlugin({
fileName: '../../asset-manifest.json',
generate(seed, files) {
// ․․․
},
})
{
"files": {
"main.css": "/assets/0555b41d.css",
"main.js": "/assets/cea9fc4b.js",
"page.home.css": "/assets/_015355d4.css",
"page.home.js": "/assets/_f9015022.js",
"page.notFound.css": "/assets/_137bfafa.css",
"page.notFound.js": "/assets/_ffb93b09.js",
"page.photo.css": "/assets/_9a34d8d2.css",
"page.photo.js": "/assets/_067a3cd1.js"
}
}
readFile(join(__dirname, 'asset-manifest.json'), () => { /* формируем files */ });
// ․․․
export function App({ files }) {
return (
<html lang="ru">
<body>
{/* ․․․ */}
<script src={files['main.js']} defer />
</body>
</html>
);
}
import { renderToStaticMarkup } from 'react-dom/server';
import { App } from 'components/App/App@server';
export function renderPage() {
return (req, res) => {
let content = renderToStaticMarkup(<App files={req.state.files} />);
res.send('<!DOCTYPE html>' + content).end();
};
}
// server/index.ts
app.use(renderPage());
// src/client.ts
import { createElement } from 'react';
import { hydrate } from 'react-dom';
import { App } from 'components/App/App@client';
document.addEventListener('DOMContentLoaded', () => {
hydrate(
<App />,
document.getElementById('root')
);
});
export function prepareState() {
return async function(req, res, next) {
try {
req.state = await getData(req);
next();
} catch (err) {
next(err);
}
};
}
// server/index.ts
app.use(prepareState());
import * as redux from 'redux';
import { rootReducer } from 'store';
import { devToolsEnhancer } from 'redux-devtools-extension';
export function createStore(state) {
return redux.createStore(rootReducer, state, devToolsEnhancer());
}
import { Provider } from 'react-redux';
export function App({ state }) {
return (
<Provider store={createStore(state)}>
<html lang="ru">
{/* ․․․ */}
<SerializedState />
</html>
</Provider>
);
}
let serializedState = 'window.__PRELOADED_STATE__='
+ JSON.stringify(state).replace(/</g, '\\u003c');
<script
dangerouslySetInnerHTML={ { __html: serializedState } }
/>
<Provider store={store}>
<html lang="ru">
<body>
<div id="root">
<BaseApp />
</div>
<script dangerouslySetInnerHTML={ { __html: serializedState } } />
<script src={files['main.js']} defer />
</body>
</html>
</Provider>
const state = window.__PRELOADED_STATE__;
document.addEventListener('DOMContentLoaded', () => {
hydrate(
<App state={state} />,
document.getElementById('root')
);
});
import { App as BaseApp } from 'components/App/App';
export function App({ state }) {
return (
<Provider store={createStore(state)}>
<BaseApp />
</Provider>
);
};
{login: '</script>'}function App() {
return (
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/topics" component={Topics} />
<Route component={NotFound} />
</Switch>
);
}
import { BrowserRouter } from 'react-router-dom';
export function App({ state }) {
return (
<BrowserRouter>
<Provider store={createStore(state)}>
<BaseApp />
</Provider>
</BrowserRouter>
);
};
import { StaticRouter } from 'react-router';
export function App({ url, state }) {
return (
<StaticRouter url={url}>
<Provider store={createStore(state)}>
<html lang="ru">{/* ․․․ */}</html>
</Provider>
</StaticRouter>
);
}
import { Link } from 'react-router-dom';
export function Navigation() {
return (
<nav className="nav">
<Link to="/" className="nav__link">Главная</Link>
<Link to="/about" className="nav__link">Контакты</Link>
</nav>
);
}
export const PAGES = {
home: {
exact: true,
path: '/',
component: Home,
},
photo: {
path: '/photo/:id',
component: Photo,
},
notFound: { component: NotFound },
};
function App() {
return (
<Switch>
{Object.keys(PAGES).map(routeName => (
<Route key={routeName} {․․․PAGES[routeName]} />
))}
</Switch>
);
}
import { matchPath } from 'react-router';
export function matchUrl(url) {
for (let routeName of Object.keys(PAGES)) {
let result = matchPath(url, PAGES[routeName]);
if (result) {
return { route: routeName, ․․․result };
}
}
}
matchUrl('/photo/some-id');
{
route: 'photo'
params: {
id: 'some-id'
}
path: '/photo/:id'
url: '/photo/some-id'
}
export function prepareState() {
return async function(req, res, next) {
try {
let location = matchUrl(req.url);
req.state = await getData(req, location);
next();
} catch (err) {
next(err);
}
};
}
import { withRouter } from 'react-router';
export const PhotoPage = withRouter(({ match }) => {
let dispatch = useDispatch();
let photo = useSelector((state) => state.photo);
useEffect(() => {
dispatch(fetchPhoto(match.params.id));
}, [match.params.id]);
return photo ? <Photo photo={photo} /> : 'Загружаем․․․';
});
const Home = React.lazy(() => import('components/Home'));
export function App() {
return (
<React.Suspense fallback="Loading․․․">
<Home />
</React.Suspense>
);
}
let Home = lazyComponent({
asyncLoader: () => import('components/Home'),
syncLoader: () => require('components/Home')
});
let Home = lazyComponent({
asyncLoader: () => {
if (typeof window !== 'undefined') {
return import('components/Home');
}
},
syncLoader: () => {
if (typeof window === 'undefined') {
return require('components/Home');
}
}
});
// Client
plugins: [
new DefinePlugin({ 'typeof window': '"object"' }),
],
// Server
plugins: [
new DefinePlugin({ 'typeof window': '"undefined"' }),
],
let Home = lazyComponent({
asyncLoader: () => {
if (false) {
}
},
syncLoader: () => {
if (true) {
return require('components/Home');
}
}
});
let Home = lazyComponent({
asyncLoader: () => {
if (true) {
return import('components/Home');
}
},
syncLoader: () => {
if (false) {
}
}
});
export function lazyComponent({ asyncLoader, syncLoader }) {
return (props) => {
let syncModule = syncLoader(), Component;
if (syncModule) {
Component = syncModule.default;
} else {
Component = React.lazy(asyncLoader);
}
return <Component {․․․props} />;
};
}
// 1
require('components/App'); // module.exports
// 2
require.resolve('components/App'); // ./src/components/App.tsx
// 3
require.resolveWeak('components/App'); // ./src/components/App.tsx
// 4
require.cache // { [moduleId]: module }

let Home = lazyComponent({
id: require.resolveWeak('components/Home'),
asyncLoader: () => {
if (typeof window !== 'undefined') {
return import('components/Home');
}
},
syncLoader: () => {
if (typeof window === 'undefined') {
return require('components/Home');
}
}
});
export const lazyComponent = ({ id, asyncLoader, syncLoader }) => (props) => {
let syncModule = syncLoader(), Component;
if (syncModule) {
Component = syncModule.default;
} else if (require.cache[id]) {
Component = require.cache[id].exports.default;
} else {
Component = React.lazy(asyncLoader);
}
return <Component {․․․props} />;
};
document.addEventListener('DOMContentLoaded', async () => {
let routeName = state.route.name;
let Component = PAGES[routeName].component;
if (Component.loader) {
await Component.asyncLoader();
}
hydrate(
<App state={state} />,
document.getElementById('root')
);
});
let Home = lazyComponent(() => import('components/Home'));
import { cn } from '@bem-react/classname';
const cnCard = cn('Card');
cnCard() === 'Card';
cnCard({ extended: true }) === 'Card Card_extended';
cnCard({ theme: 'promo' }) === 'Card Card_theme_promo';
cnCard('Carousel') === 'Card-Carousel';
cnCard('Carousel', { theme: 'ad' }) === 'Card-Carousel Card-Carousel_theme_ad';
cnCard(null, [ 'Some', 'Other' ]) === 'Card Some Other';
Card
├── -Carousel
│ └── Card-Carousel.tsx
├── -Contacts
│ ├── Card-Contacts.css
│ └── Card-Contacts.tsx
├── _theme
│ ├── Card_theme_outdated.css
│ ├── Card_theme_outdated.tsx
│ ├── Card_theme_promo.css
│ └── Card_theme_promo.tsx
├── Card.css
└── Card.tsx
import { cn } from '@bem-react/classname';
import './Card.css';
const cnCard = cn('Card');
export function Card({ className }) {
return (
<div className={cnCard(null, [className])}>
․․․
</div>
)
}
import { Avatar } from 'components/Avatar/Avatar';
import { CardCarousel } from './-Carousel/Card-Carousel';
․․․
export function Card({ className }) {
return (
<div className={cnCard(null, [className])}>
<CardCarousel />
<Avatar className={cnCard('Avatar')} />
</div>
)
}
<div class="Card">
<div class="Card-Carousel">․․․</div>
<div class="Avatar Card-Avatar">․․․</div>
</div>
import { withBemMod } from '@bem-react/core';
import './Card_theme_promo.css';
export const CardThemePromo = withBemMod('Card', { theme: 'promo' });
import { withBemMod } from '@bem-react/core';
import './Card_theme_outdated.css';
function CardThemeOutdated(Base, props) {
return (
<Base {․․․props}>
<Overlay />
</Base>
);
}
export const CardThemeOutdated =
withBemMod('Card', { theme: 'outdated' }, CardThemeOutdated);
import { compose } from '@bem-react/core';
import { Card } from 'components/Card/Card';
import { CardThemePromo } from 'components/Card/_theme/Card_theme_promo';
import { CardThemeOutdated } from 'components/Card/_theme/Card_theme_outdated';
const MyCard = componse(CardThemePromo, CardThemeOutdated)(Card);
<MyCard theme="promo" />
<MyCard theme="outdated" />
function middleware(store) {
return function(next) {
return function(action) {
return next(action);
};
};
}
const middleware = store => next => action => {
return next(action);
}
const changeTitle = store => next => action => {
let result = next(action);
document.title = getPageTitle(store.getState());
return result;
}
const thunk = store => next => action => {
if (typeof action === 'function') {
return action(store.dispatch, store.getState);
}
return next(action);
}
import { applyMiddleware } from 'redux';
export function createStore(state) {
let enhancer = composeWithDevTools(applyMiddlware(thunk, changeTitle));
return redux.createStore(rootReducer, state, enhancer);
}
Разработчик интерфейсов