Create React App (aka React Scripts) и серверный рендеринг с Redux и Router

Structural Improvements

This release is light on features and fixes, and is more focused on stability and compatibility and preparing the library for future releases.

There are no breaking changes in this release. If you are already using version 4.x, you can use version 5 immediately with zero code changes. Version 5 will pick up where the 4.x roadmap (now the 5.x roadmap) left off.

The most significant improvement in v5 is better allaround support for React 16, while maintaining full compatibility with React >= 15. We know that a lot of React Router users are maintaing apps they’ve built over the last few years using various versions of React 15 or 16 and React Router 4, and we are showing our committment to you with this release. This includes upgrading from using React’s legacy context API, as well as eliminating all other warnings. My sincere thanks to Tim Dorr for getting the ball rolling here, and my good friend Jared Palmer for prodding me along.

Using the new context API in React 16 (we use Jamie Kyle’s shim for backwards compat with React 15) also allows us to avoid the «update blocking» problem that you may have had to work around in the past.

Side note: we have been working on this problem for over 2 years now … good to see it finally fixed!

Other housekeeping items in this release include a complete overhaul to our bundling infrastructure and full test coverage for all bundles we publish. Before this release we were only running tests on our source code, so there was a chance that our build introduced a bug somewhere. But now all our builds pass all our tests.

We switched from publishing multiple files for each build to a single file in v5, just like React itself (we copied their code! ). We also introduced pre-optimized builds for production, so you don’t have to manually set to in your build script if you don’t want to (though you probably still want to, for other stuff). The point is, you won’t be building the router as part of your build. We’ve already taken care of that, in both development and production modes.

Probably the most significant change here is that instead of requiring individual files from the router, you need to destructure your imports from the main bundle.

The former style is still supported but will issue a warning in development.

In addition to these improvements, we did some work to streamline and automate the release process, so we should be able to release more frequently and predictably from now on.

Доступность Upvel

Согласно легенде, Upvel зарегистрирована (2010) в Лос-Анжелесе (сохранена орфография оригинала). Американцы могут приобрести аппаратуру на ebay, amazon. Продают модемы иные электронные магазины. Россиянам специально создан сайт upvel.ru. Калифорнийцы очень ценят иностранного покупателя.

Первым гранит настроек стали грызть пользователи Билайн. Желающие могут изучить перипетии взаимной борьбы сегодня же (homenet.beeline.ru/index.php?/forum/1135-upvel/). Помимо официальных магазинов оборудование продают электронные доски объявлений. Товар сбывают:

  • М-Видео.
  • DNS.
  • Связной.
  • Формоза.
  • Ozon.
  • Ашан.

Среди партнеров фигурируют:

  • Ситилинк.
  • Эльдорадо.
  • MediaMarkt.
  • Техносила.
  • Технопарк.
  • Юлмарт.

Карта пунктов приведена официальным сайтом – заходите, жмете пункт меню Где купить. Понадобится ввести название города. Высокие характеристики аппаратуры заставляют удивляться отсутствию популярности среди коренных американцев. Судите сами: UR-814ac заявлен четвертым поколением мобильной связи (4G), обеспечивает скорость передачи Wi-Fi 750 Мбит/с.

Буржуи отдают России передовые технологии. Официальные зарубежные дистрибьюторы холодно встречают маршрутизаторы (видимо, сказываются экономические санкции):

5.6.1 Для чего нужна маршрутизация?

В любом реальном веб-приложении нужны маршруты, и приложение React не исключение. Пользователь
должен видеть, где он находится в приложении в любой момент времени. А видит он свое текущее
местоположение в адресной строке браузера. Следовательно приложение должно уметь сопоставлять
определённый URL с соответствующей ему страницей. То есть, если мы введём в адресную строку
например , то приложение должно направить нас на страницу
списка приёмов, но не на какую-либо другую.

Также должна работать история. То есть когда пользователь кликает на стрелку «Назад» в браузере, приложение
должно направить нас на предыдущую страницу.

Сам по себе React не предоставляет такой возможности, это задача специальных библиотек.
Как правило, используя API такой библиотеки мы подключаем компоненты страниц нашего положения,
сопоставляя их с определёнными путями. После этого, переходя с одной страницы на другую мы
будем видеть в адресной строке, как изменяется текущий путь.

На данный момент есть несколько популярных библиотек для
маршрутизации: , , и пр.
Полный список можно посмотреть здесь. Мы
будем использовать . Хотя в вашем проекте может
быть любая другая — все зависит от бизнес-требований.

API

You must add this reducer to your store for syncing to work.

A reducer function that stores location updates from . If you use , it should be nested under the key.

Creates an enhanced history from the provided history. This history changes to pass all location updates through the provided store first. This ensures if the store is updated either from a navigation event or from a time travel action, such as a replay, the listeners of the enhanced history will stay in sync.

You must provide the enhanced history to your component. This ensures your routes stay in sync with your location and your store at the same time.

The object takes in the following optional keys:

  • — (default ) A selector function to obtain the history state from your store. Useful when not using the provided to store history state. Allows you to use wrappers, such as Immutable.js.
  • — (default ) When , the URL will not be kept in sync during time travel. This is useful when using from Redux DevTools and not wanting to maintain the URL state when restoring state.

You must install for these action creators to work.

  • — Pushes a new location to history, becoming the current location.
  • — Replaces the current location in history.
  • — Moves backwards or forwards a relative number of locations in history.
  • — Moves forward one location. Equivalent to
  • — Moves backwards one location. Equivalent to

These action creators are also available in one single object as , which can be used as a convenience when using Redux’s .

A middleware you can apply to your Redux to capture dispatched actions created by the action creators. It will redirect those actions to the provided instance.

An action type that you can listen for in your reducers to be notified of route updates. Fires after any changes to history.

renderRoutes: 用于根据 config 生成 routes

/**
 * renderRoutes
 *
 * @param {Object} rootConfig root routes config
 * @param {?React.Component=} rootConfig.componnet 可选,可为 null,如果没有 component,代表该层级无 route 意义,仅需要考虑是否 needSwitch 和 needRedirect
 * @param {string=} rootConfig.url 可选,如果有 component 时必选,代表该 route 层的 url
 * @param {string=} rootConfig.path 可选,如果有 component 时必选,代表该 route 层的 path
 * @param {React.Component=} rootConfig.Route 可选,如果有 component 时必选,用于渲染该层 route 的 Component
 * @param {Array=} rootConfig.subRoutes 可选,如果有则表示有嵌套 routes
 * @param {boolean=} rootConfig.isSwitch 在 subRoutes 中且 component 为 null 时 无效
 * @param {boolean=} rootConfig.isRedirect 在 subRoutes 中且 component 为 null 时 无效,如果为 true,redirect 到同级的第一个 route
 * @param {number=} level 层级,用于 key
 * @return {React.Component}
 */
function renderRoutes() {}

中除了以上配置外可以新增自定义字段,比如 等,用于 Sidebar 等场景的渲染啊。

API

Returns an array of matched routes.

Parameters

  • routes — the route configuration
import { matchRoutes } from 'react-router-config'
const branch = matchRoutes(routes, '/child/23')
// using the routes shown earlier, this returns
// [
//   routes,
//   routes.routes
// ]

Each item in the array contains two properties: and .

  • : A reference to the routes array used to match
  • : The match object that also gets passed to render methods.
branch[].match.url
branch[].match.isExact
// etc.

You can use this branch of routes to figure out what is going to be rendered before it actually is rendered. You could do something like this on the server before rendering, or in a lifecycle hook of a component that wraps your entire app

const loadBranchData = (location) => {
  const branch = matchRoutes(routes, location.pathname)

  const promises = branch.map(({ route, match }) => {
    return route.loadData
      ? route.loadData(match)
       Promise.resolve(null)
  })

  return Promise.all(promises)
}

// useful on the server for preloading data
loadBranchData(req.url).then(data => {
  putTheDataSomewhereTheClientCanFindIt(data)
})

// also useful on the client for "pending navigation" where you
// load up all the data before rendering the next page when
// the url changes

// THIS IS JUST SOME THEORETICAL PSEUDO CODE :)
class PendingNavDataLoader extends Component {
  state = {
    previousLocation null
  }

  componentWillReceiveProps(nextProps) {
    const navigated = nextProps.location !== this.props.location
    const { routes } = this.props

    if (navigated) {
      // save the location so we can render the old screen
      this.setState({
        previousLocation this.props.location
      })

      // load data while the old screen remains
      loadNextData(routes, nextProps.location).then((data) => {
        putTheDataSomewhereRoutesCanFindIt(data)
        // clear previousLocation so the next screen renders
        this.setState({
          previousLocation null
        })
      })
    }
  }

  render() {
    const { children, location } = this.props
    const { previousLocation } = this.state

    // use a controlled  to trick all descendants into
    // rendering the old location
    return (
      Route
        location={previousLocation || location}
        render={() => children}
      >
    )
  }
}

// wrap in withRouter
export default withRouter(PendingNavDataLoader)

/////////////
// somewhere at the top of your app
import routes from './routes'

BrowserRouter>
  PendingNavDataLoader routes={routes}>
    {renderRoutes(routes)}
  PendingNavDataLoader>
BrowserRouter>

Again, that’s all pseudo-code. There are a lot of ways to do server rendering with data and pending navigation and we haven’t settled on one. The point here is that gives you a chance to match statically outside of the render lifecycle. We’d like to make a demo app of this approach eventually.

In order to ensure that matching outside of render with and inside of render result in the same branch, you must use instead of inside your components. You can render a still, but know that it will not be accounted for in outside of render.

import { renderRoutes } from 'react-router-config'

const routes = [
  { component Root,
    routes [
      { path '/',
        exact true,
        component Home
      },
      { path '/child/:id',
        component Child,
        routes [
          { path '/child/:id/grand-child',
            component GrandChild
          }
        ]
      }
    ]
  }
]

const Root = ({ route }) => (
  div>
    h1>Rooth1>
    {/* child routes won't render without this */}
    {renderRoutes(route.routes)}
  div>
)

const Home = ({ route }) => (
  div>
    h2>Homeh2>
  div>
)

const Child = ({ route }) => (
  div>
    h2>Childh2>
    {/* child routes won't render without this */}
    {renderRoutes(route.routes, { someProp 'these extra props are optional' })}
  div>
)

const GrandChild = ({ someProp }) => (
  div>
    h3>Grand Childh3>
    div>{someProp}div>
  div>
)


ReactDOM.render((
  BrowserRouter>
    {/* kick it all off with the root route */}
    {renderRoutes(routes)}
  BrowserRouter>
), document.getElementById('root'))

On server side

import { match } from 'react-router';
import { triggerHooksOnServer } from 'react-router-hook';
// Other imports

import routes from './routes';

app.get('*', (req, res) => {
  // create redux store (Optional);
  const store = createStore();

  match({
    history,
    routes,
    location req.url,
  }, (err, redirectLocation, renderProps) => {
    if (err) {
      // Error Handler
    }
    const locals = {
      dispatch store.dispatch,
      getState store.getState,
    };
    triggerHooksOnServer(
      renderProps,
      ,
      {
        dispatch,
        getState,
      },
      // If onComponentError is null, callback will be immediately called with the error
      onComponentError (err) => {
        console.error(err.Component, err.error);
      },
      // triggerHooksOnServer() will return a Promise if there is no callback
      (err) => {
        if (err) {
          res.status(500).end();
          return;
        }
        const body = ReactDOMServer.renderToString(
          Provider store={store}>
            RouterContext {...renderProps} >
          Provider>,
        );
        res.send(`
${body}`);
      },
  });
});
triggerHooksOnServer

API

You must add this reducer to your store for syncing to work.

A reducer function that stores location updates from . If you use , it should be nested under the key.

Creates an enhanced history from the provided history. This history changes to pass all location updates through the provided store first. This ensures if the store is updated either from a navigation event or from a time travel action, such as a replay, the listeners of the enhanced history will stay in sync.

You must provide the enhanced history to your component. This ensures your routes stay in sync with your location and your store at the same time.

The object takes in the following optional keys:

  • — (default ) A selector function to obtain the history state from your store. Useful when not using the provided to store history state. Allows you to use wrappers, such as Immutable.js.
  • — (default ) When , the URL will not be kept in sync during time travel. This is useful when using from Redux DevTools and not wanting to maintain the URL state when restoring state.

, , , ,

You must install for these action creators to work.

Action creators that correspond with the . For reference they are defined as follows:

  • — Pushes a new location to history, becoming the current location.
  • — Replaces the current location in history.
  • — Moves backwards or forwards a relative number of locations in history.
  • — Moves forward one location. Equivalent to
  • — Moves backwards one location. Equivalent to

Both and take in a location descriptor, which can be an object describing the URL or a plain string URL.

These action creators are also available in one single object as , which can be used as a convenience when using Redux’s .

A middleware you can apply to your Redux to capture dispatched actions created by the action creators. It will redirect those actions to the provided instance.

An action type that you can listen for in your reducers to be notified of route updates. Fires after any changes to history.

API

    • afterCancel?: Function,
    • afterConfirm?: Function,
    • allowGoBack: bool (use goBack method instead of push when navigating back — !! NOTE WELL !! it will always navigate back only 1 item, even when it should navigate back more items. read more: https://github.com/ZacharyRSmith/react-router-navigation-prompt/pull/30),
    • beforeCancel?: Function,
    • beforeConfirm?: Function,
    • children: (data: {isActive: bool, onCancel: Function, onConfirm: Function}) => React$Element,
    • renderIfNotActive: bool,
    • when: bool | (Location, ?Location) => bool,
    • disableNative: bool,
      // Added by react-router:
    • match: Match,
    • history: RouterHistory,
    • location: Location,

How it works

A «router» in the context of this project is a function that accepts a path and a callback. The router turns the path into a state object by passing it through a series of middleware. Once the middleware completes, the callback is called (either synchronously or asynchronously) with the final state object, which can be used to render an app.

const router = createRouter(...middlewares)

router('/some/path', (error, state) => {
  // Render app using state
})

Middleware

A middleware is a function that accepts Node-style callback (we’ll call it a listener) and returns a new Node-style callback with augmented behavior.

type Listener = (error Error, state Object) => void
type Middleware = (next Listener) => Listener

An important feature of middleware is that they are composable:

// Middlewares 1, 2, and 3 will run in sequence from left to right
const combinedMiddleware = compose(middleware1, middleware2, middlware3)

Router middleware is much like middleware in Redux. It is used to augment a state object as it passes through a router. Here’s an example of a middleware that adds a field:

import queryString from 'query-string'

const parseQuery = next => (error, state) => {
  if (error) return next(error)

  next(null, {
    ...state,
    query queryString.parse(state.search)
  })
}

As with React props and Redux state, we treat router state as immutable.

State object conventions

All state objects should have the fields , , , and . When you pass a path string to a router function, the remaining fields are extracted from the path. The reverse also works: if instead of a path string you pass an initial state object to a router function with , , and , a field is added. This allows middleware to depend on those fields without having to do their own parsing.

There are two additional fields which have special meanings: and . is self-explanatory: a middleware should skip any state object with a field by passing it to the next middleware. Similarly, a state object with indicates that a previous middleware has already handled it, and it needs no further processing by remaining middleware. (There are some circumstances where it may be appropriate for a middleware to process a state object.)

Handling all these special cases can get tedious. The allows you to create a middleware that handles specific cases. It’s a bit like a switch statement, or pattern matching. Example

import { handle } from '@acdlite/router'

const middleware = handle({
  // Handle error
  error next => (error, state) => {...}

  // Handle redirect
  redirect next => (error, state) => {...}

  // Handle done
  done next => (error, state) => {...}

  // Handle all other cases
  next next => (error, state) => {...}
})

is the most common handler.

If a handler is omitted, the default behavior is to pass the state object through to the next middleware, unchanged.

Proof-of-concept: React Router-like API

As a proof-of-concept, the directory includes utilities for implementing a React Router-like API using middleware. It supports:

  • Nested route matching, with params
  • Plain object routes or JSX routes
  • Asynchronous route fetching, using
  • Asynchronous component fetching, using

Diverges from React Router:

  • hooks are just middlewares that run when a route matches.
  • hooks are not supported because they require keeping track of the previous state, which is (currently) outside the scope of this project.

Internally, it uses several of React Router’s methods, so the route matching behavior should be identical.

Note that if you were really aiming to replace React Router, you would want to create a stateful abstraction on top of these relatively low-level functions. I have intentionally omitted any kind of state management from this project, for maximum flexibility while I continue to experiment.

Example:

import { createRouter } from '@acdlite/router'
import { reactRoutes } from '@acdlite/router/react-router'
import { Route, IndexRoute } from 'react-router'
import createHistory from 'history/lib/createBrowserHistory'

const reactRouter = createRouter(
  reactRoutes(
    Route path="/" component={App}>
      Route path="post">
        IndexRoute component={PostIndex} >
        Route path=":id" component={Post} >
      Route>
    Route>
  ),
  // ... add additional middleware, if desired
)

const history = createHistory()

// Listen for location updates
history.listen(location => {
  // E.g. after navigating to '/post/123'
  // Routers can accept either a path string or an object with `pathname`,
  // `query`, and `search`, so we can pass the location object directly.
  reactRouter(location, {
    // Route was successful
    done (error, state) => {
      // Returns a state object with info about the matched routes
      expect(state).to.eql({
        params { id '123' },
        routes  // Array of matching route config objects
        components , // Array of matching components
        // ...plus other fields from the location object
      })

      // Render your app using state...
    },

    // Handle redirects
    redirect (error, state) => {
      history.replace(state.redirect)
    },

    // Handle errors
    error error => {
      throw error
    }
  }
})

A key thing to note is that the server-side API is exactly the same: instead of using history, just pass a path string directly to the router, and implement , and as appropriate.

Also note that there’s no interdependency between history and your routing logic.

The router returns the matched components, but it’s up to you to render them how you like. An easy way to start is using Recompose’s function:

const Component = nest(...state.components)
ReactDOM.render(Component {...state.params} {...state.query} >)

That gets you 90% of the way to parity with React Router. Conveniences like the component would need to be re-implemented, but are fairly straightforward.

Q&A:

// location = 'http://examples.com/demo/simple?q=1#hash
// path level
if (`/demo/simple`.exec(Route.path))
  render(Route.component);

// query level
if (`/demo/simple?q=1`.exec(Route.path))
  render(Route.component);

// hash level
if (`/demo/simple?q=1#hash`.exec(Route.path))
  render(Route.component);

import { RouterModes } from 'x-react-router'
const { PATH, QUERY, HASH } = RouterModes;

// then use them like this:
// path level
Router mode={PATH} {...otherProps}>
  // ... 
Router>

// query level
Router mode={QUERY} {...otherProps}>
  // ... 
Router>

// hash level
Router mode={HASH} {...otherProps}>
  // ... 
Router>

Route path="/demo" component="js/demo" preload={true} >

Route Configuration Shape

Routes are objects with the same properties as a with a couple differences:

  • the only render prop it accepts is (no or )
  • introduces the key for sub routes
  • Consumers are free to add any additional props they’d like to a route, you can access inside the , this object is a reference to the object used to render and match.
  • accepts prop to prevent remounting component when transition was made from route with the same component and same prop
constroutes={    component Root,    routes{        path"",        exacttrue,        component Home},{        path"/child/:id",        component Child,        routes{            path"/child/:id/grand-child",            component GrandChild}}};

Note: Just like , relative paths are not (yet) supported. When it is supported there, it will be supported here.

Router config using renderRoutes

/**
 * renderRoutes
 *
 * @param {Object} rootConfig root routes config
 * @param {?React.Component=} rootConfig.componnet associate
 * @param {string=} rootConfig.url url of the route
 * @param {string=} rootConfig.path path of the route
 * @param {React.Component=} rootConfig.Route `` used for the route, useful in custom base Route or auth route in your project
 * @param {Array=} rootConfig.subRoutes sub routes
 * @param {boolean=} rootConfig.isSwitch is wrapped in 
 * @param {boolean=} rootConfig.isRedirect is include a  at the end of routes, default redirect to the first route of same level
 * @param {number=} level level mainly used for `key`
 * @return {React.Component}
 */
function renderRoutes() {}

Inside , you can also include your own attributes, like , which may be used in Sidebar render.

Without sub routes, generating a series of flat top level routes

const routesConfig = {
    subRoutes [
        {
            title 'top1',
            url `${props.match.url}/top1`,
            path `${props.match.path}/top1`,
            component Top1,
            Route BRoute
        },
        {
            title 'top2',
            url `${props.match.url}/top2`,
            path `${props.match.path}/top2`,
            component Top2,
            Route BRoute
        }
    ],
    isSwitch true,
    isRedirect true
};

Output:

{/* depends on isSwitch */}
Switch>
    Route 
        path="/top1"
        component={Top1}
    >
    Route 
        path="/top2"
        component={Top2}
    >
    {/* depends on isRedirect */}
    Redirect
        to="/top1"
    >
Switch>
Including inexplicit route

For inexplicit route, the shoud be .

If the route will be treated as inexplicit route.

The corresponding component will be generated by the config of .

const routesConfig = {
    subRoutes [
        {
            title 'top1',
            url `${props.match.url}/top1`,
            path `${props.match.path}/top1`,
            Route BRoute,
            component null,

            subRoutes [
                {
                    title 'top1-sub1',
                    url `${props.match.url}/top1/sub1`,
                    path `${props.match.path}/top1/sub1`,
                    component Sub1,
                    Route BRoute
                }
            ]
        },
        {
            title 'top2',
            url `${props.match.url}/top2`,
            path `${props.match.path}/top2`,
            Route BRoute,
            component Top2
        }
    ],

    isSwitch true,
    
    isRedirect true
};

Output:

Switch>
    Route
        path="/top1"
        {/* generated by the config */}
        component={props => {
            Switch>
                Route 
                    path="/top1/sub1"
                    component={Sub1}
                >
                // for inexplicit route,  is required
                Redirect 
                    to="/top1/sub1"
                >
            Switch>
        }}
    >
    Route
        component={Top2}
    >
    Redirect
        to="/top1"
    >
Switch>
Without inexplicit route
const routesConfig = {
    subRoutes [
        {
            title 'top1',
            url `${props.match.url}/top1`,
            path `${props.match.path}/top1`,
            Route BRoute,
            component Top1,

            subRoutes [
                {
                    title 'top1-sub1',
                    url `${props.match.url}/top1/sub1`,
                    path `${props.match.path}/top1/sub1`,
                    component Sub1,
                    Route BRoute
                }
            ]
        }
    ],

    isSwitch true,
    
    isRedirect true
};

Output:

Switch>
    Route
        path="/top1"
        component={Top1}
    >
    Redirect
        to="/top1"
    >
Switch>

In this case, the of will be passed to the of in props.

You should implement the login yourself in component.

Except that, you should use a custom (for example the in this projext), in order to pass the from to .

Ссылка на основную публикацию