Евгений Шпилевский, Яндекс
Евгений Шпилевский, Разработчик интерфейсов
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);
}
Разработчик интерфейсов