mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-25 22:37:18 +10:00
Move Redux and routing docs to Flex Docs
This commit is contained in:
parent
e2dc22f38f
commit
5d2601000b
3 changed files with 8 additions and 399 deletions
|
|
@ -156,6 +156,8 @@ Flex Docs:
|
||||||
- [How to change FTW icons](https://www.sharetribe.com/docs/guides/how-to-change-ftw-icons/)
|
- [How to change FTW icons](https://www.sharetribe.com/docs/guides/how-to-change-ftw-icons/)
|
||||||
- [FTW customization checklist](https://www.sharetribe.com/docs/guides/ftw-customization-checklist/)
|
- [FTW customization checklist](https://www.sharetribe.com/docs/guides/ftw-customization-checklist/)
|
||||||
- [How to deploy FTW to production](https://www.sharetribe.com/docs/guides/how-to-deploy-ftw-to-production/)
|
- [How to deploy FTW to production](https://www.sharetribe.com/docs/guides/how-to-deploy-ftw-to-production/)
|
||||||
|
- [How routing works in FTW](https://www.sharetribe.com/docs/background/ftw-routing/)
|
||||||
|
- [How the Redux setup works in FTW](https://www.sharetribe.com/docs/background/ftw-redux/)
|
||||||
|
|
||||||
See also the following articles:
|
See also the following articles:
|
||||||
|
|
||||||
|
|
@ -169,11 +171,8 @@ See also the following articles:
|
||||||
- [How to use CI with FTW](https://www.sharetribe.com/docs/guides/how-to-use-ci-with-ftw/)
|
- [How to use CI with FTW](https://www.sharetribe.com/docs/guides/how-to-use-ci-with-ftw/)
|
||||||
- [How to improve performance](https://www.sharetribe.com/docs/guides/how-to-improve-performance/)
|
- [How to improve performance](https://www.sharetribe.com/docs/guides/how-to-improve-performance/)
|
||||||
|
|
||||||
Extra documentation for specific topics can be found in the following files:
|
See also the
|
||||||
|
[original create-react-app documentation](https://github.com/sharetribe/create-react-app/blob/master/packages/react-scripts/template/README.md).
|
||||||
- [Routing](routing.md)
|
|
||||||
- [Redux and duck files](redux.md)
|
|
||||||
- [Original create-react-app documentation](https://github.com/sharetribe/create-react-app/blob/master/packages/react-scripts/template/README.md)
|
|
||||||
|
|
||||||
The application was bootstrapped with a forked version of
|
The application was bootstrapped with a forked version of
|
||||||
[create-react-app](https://github.com/facebookincubator/create-react-app). While most of the
|
[create-react-app](https://github.com/facebookincubator/create-react-app). While most of the
|
||||||
|
|
|
||||||
129
docs/redux.md
129
docs/redux.md
|
|
@ -1,130 +1,5 @@
|
||||||
# Redux configuration
|
# Redux configuration
|
||||||
|
|
||||||
## What is Redux
|
Documentation moved to the Flex Docs site:
|
||||||
|
|
||||||
Flex Template for Web (FTW) is JavaScript single-page application (SPA). This means that the app
|
https://www.sharetribe.com/docs/background/ftw-redux/
|
||||||
needs to be able to render several different layouts (pages) depending on user interaction. State
|
|
||||||
management is essential for this process. FTW needs to know if a user has been authenticated, if it
|
|
||||||
has received relevant data for the current page, and so on.
|
|
||||||
|
|
||||||
We use [Redux](https://redux.js.org/introduction) for state management on the application level. You
|
|
||||||
should read more about Redux before you start modifying queries to Flex API or creating new Page
|
|
||||||
level elements (unless you are modifying [a static page](static-pages.md)).
|
|
||||||
|
|
||||||
In the following subtopics, we assume that you know the
|
|
||||||
[basics of Redux](https://redux.js.org/basics) already.
|
|
||||||
|
|
||||||
- [Containers: Pages + TopbarContainer](#containers-pages--topbarcontainer)
|
|
||||||
- [Duck files](#duck-files)
|
|
||||||
- [Setting up Redux](#setting-up-redux)
|
|
||||||
- [Advanced Redux concepts: thunks](#advanced-redux-concepts-thunks)
|
|
||||||
|
|
||||||
## Containers: Pages + TopbarContainer
|
|
||||||
|
|
||||||
We have set up FTW so that pages are aware of Redux state store, but other components and forms are
|
|
||||||
purely
|
|
||||||
[presentational components](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0).
|
|
||||||
This makes it easier to customize UI components - you don't need to be aware of the complexity
|
|
||||||
related to Redux setup when you just want to touch the behavior of some visual component. In those
|
|
||||||
cases, you can just head to `scr/components/` or `src/forms/` directory and you can see from there
|
|
||||||
what props are available for each component when they are rendered.
|
|
||||||
|
|
||||||
Naturally, there is a need for higher level components which fetch new data and define what props
|
|
||||||
are passed down to presentational components. In Redux terminology, those components are called
|
|
||||||
_Containers_. FTW has defined all the containers inside a directory called `src/containers/`. It
|
|
||||||
contains all the pages and a special container for top bar (TopbarContainer) since that is a very
|
|
||||||
complex component and it is shared with almost every page. You can read more about differences
|
|
||||||
between presentational and container components from an
|
|
||||||
[article written by Dan Abramov](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0)
|
|
||||||
|
|
||||||
The actual container setup of a page level component can be found from the bottom of the component
|
|
||||||
file. For example, `src/containers/TransactionPage/TransactionPage.js` connects itself to Redux
|
|
||||||
store with `mapStateToProps` and `mapDispatchToProps` functions:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const TransactionPage = compose(
|
|
||||||
connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
),
|
|
||||||
injectIntl
|
|
||||||
)(TransactionPageComponent);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Duck files
|
|
||||||
|
|
||||||
Inside `src/containers/<ComponentName>` directory, we have also a special `<ComponentName>.duck.js`
|
|
||||||
file. These files wrap all the other page-specific Redux concepts into a single file. Instead of
|
|
||||||
writing action types, action creators and reducers in separate files (and spread them around our
|
|
||||||
directory structure), FTW tries to keep pages encapsulated. Page specific actions, store updates,
|
|
||||||
and data fetches happen inside their respective directory - just look for a file which name follows
|
|
||||||
a pattern: `<ComponentName>.duck.js`. This pattern is just a simple concept of adding related things
|
|
||||||
into a single file. Read more from
|
|
||||||
[author's repository](https://github.com/erikras/ducks-modular-redux).
|
|
||||||
|
|
||||||
### Global reducers
|
|
||||||
|
|
||||||
Some reducers are needed in several pages. These global reducers we have defined inside `src/ducks/`
|
|
||||||
directory with their respective `*.duck.js` files. Most important global duck files are
|
|
||||||
`user.duck.js` and `marketplaceData.duck.js`.
|
|
||||||
|
|
||||||
## Setting up Redux
|
|
||||||
|
|
||||||
Container specific reducers are gathered and exported inside `src/containers/reducers.js` file and
|
|
||||||
global reducers are exported respectively in a file `src/ducks/index.js`.
|
|
||||||
|
|
||||||
With those exports, we are able to create appReducer (`src/reducers.js`):
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { combineReducers } from 'redux';
|
|
||||||
import * as globalReducers from './ducks';
|
|
||||||
import * as pageReducers from './containers/reducers';
|
|
||||||
|
|
||||||
const appReducer = combineReducers({ ...globalReducers, ...pageReducers });
|
|
||||||
```
|
|
||||||
|
|
||||||
`appReducer` is called by `createReducer` function, which is called inside of `configureStore`
|
|
||||||
function (in `src/store.js`).
|
|
||||||
|
|
||||||
This setup creates a store structure that separates container specific state as well as global data
|
|
||||||
by their reducer names. Together with Ducks module naming schema, this means that:
|
|
||||||
|
|
||||||
- the state of the `ListingPage` can be found from `state.ListingPage` and
|
|
||||||
- the state of the global `user` object can be found from `state.user`.
|
|
||||||
|
|
||||||
## Advanced Redux concepts: thunks
|
|
||||||
|
|
||||||
One essential part of state management in FTW, is filling the Redux store with data fetched from the
|
|
||||||
Flex API. This is done with
|
|
||||||
[Redux Thunks](https://redux.js.org/advanced/asyncactions#async-action-creators), which is a Redux
|
|
||||||
middleware to create asynchronous action creators.
|
|
||||||
|
|
||||||
As with every other Redux store actions, we have defined Thunks inside `*.duck.js` file. For
|
|
||||||
example, fetching listing reviews can be done with a following thunk function:
|
|
||||||
|
|
||||||
```js
|
|
||||||
export const fetchReviews = listingId => (dispatch, getState, sdk) => {
|
|
||||||
// Make store aware of a request to fetch reviews
|
|
||||||
dispatch(fetchReviewsRequest);
|
|
||||||
// Fetch reviews using Flex SDK
|
|
||||||
return sdk.reviews
|
|
||||||
.query({
|
|
||||||
listingId,
|
|
||||||
state: 'public',
|
|
||||||
include: ['author', 'author.profileImage'],
|
|
||||||
'fields.image': ['variants.square-small'],
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
const reviews = denormalisedResponseEntities(response);
|
|
||||||
// If fetch succeeds, make store aware of fetched data
|
|
||||||
dispatch(fetchReviewsSuccess(reviews));
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
// If fetch throws an error, save the error to the store (so that UI can react to it)
|
|
||||||
dispatch(fetchReviewsError(storableError(e)));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
> Note: `sdk` parameter is also provided by Redux Thunk. We pass it to middleware in store.js:
|
|
||||||
> `thunk.withExtraArgument(sdk)`
|
|
||||||
|
|
|
||||||
269
docs/routing.md
269
docs/routing.md
|
|
@ -1,270 +1,5 @@
|
||||||
# Routing
|
# Routing
|
||||||
|
|
||||||
Flex Template for Web (FTW) uses [React Router](https://reacttraining.com/react-router/web) for
|
Documentation moved to the Flex Docs site:
|
||||||
creating routes to different pages. React Router is a collection of navigational components that
|
|
||||||
allow single page apps to create routing as a part of normal rendering flow of the React app. So,
|
|
||||||
instead of defining on server-side what gets rendered when user goes to URL
|
|
||||||
`somemarketplace.com/about`, we just catch all the path combinations and let the app to define what
|
|
||||||
page gets rendered.
|
|
||||||
|
|
||||||
- [React Router setup](#react-router-setup)
|
https://www.sharetribe.com/docs/background/ftw-routing/
|
||||||
- [Linking](#linking)
|
|
||||||
- [Loading data](#loading-data)
|
|
||||||
- [Analytics](#analytics)
|
|
||||||
- [A brief introduction to server-side rendering](#a-brief-introduction-to-ssr)
|
|
||||||
|
|
||||||
## React Router setup
|
|
||||||
|
|
||||||
### Route configuration
|
|
||||||
|
|
||||||
FTW has a quite straightforward routing setup - there's just one file you need to check before you
|
|
||||||
link to existing routes or start creating new routes to static pages: `src/routeConfiguration.js`.
|
|
||||||
|
|
||||||
There we have imported and configured all the page-level components that are currently used within
|
|
||||||
FTW:
|
|
||||||
|
|
||||||
```js
|
|
||||||
import {
|
|
||||||
AboutPage,
|
|
||||||
AuthenticationPage,
|
|
||||||
//...
|
|
||||||
} from './containers';
|
|
||||||
|
|
||||||
// Our routes are exact by default.
|
|
||||||
// See behaviour from Routes.js where Route is created.
|
|
||||||
const routeConfiguration = () => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
path: '/about',
|
|
||||||
name: 'AboutPage',
|
|
||||||
component: AboutPage,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/login',
|
|
||||||
name: 'LoginPage',
|
|
||||||
component: props => <AuthenticationPage {...props} tab="login" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/signup',
|
|
||||||
name: 'SignupPage',
|
|
||||||
component: props => <AuthenticationPage {...props} tab="signup" />,
|
|
||||||
},
|
|
||||||
//...
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default routeConfiguration;
|
|
||||||
```
|
|
||||||
|
|
||||||
In the example, path `/login` renders `AuthenticationPage` component with prop 'tab' set to 'login'.
|
|
||||||
In addition, this route configuration has a name 'LoginPage'.
|
|
||||||
|
|
||||||
> Routes use exact path matches in FTW. We felt that this makes it easier to understand the
|
|
||||||
> connection between a path and its routed view aka related page component.
|
|
||||||
> [Read more.](https://reacttraining.com/react-router/web/api/Route/exact-bool)
|
|
||||||
|
|
||||||
There are a couple of extra configurations you can set. For example `/listings` path leads to a page
|
|
||||||
that lists all the listings provided by the current user:
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
path: '/listings',
|
|
||||||
name: 'ManageListingsPage',
|
|
||||||
auth: true,
|
|
||||||
authPage: 'LoginPage', // default is 'SingupPage'
|
|
||||||
component: props => <ManageListingsPage {...props} />,
|
|
||||||
loadData: ManageListingsPage.loadData,
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
Here we have set this route to be available only for authenticated user (`auth: true`), because we
|
|
||||||
need to know whose listings we should fetch. If a user is unauthenticated, he/she is redirected to
|
|
||||||
LoginPage (`authPage: 'LoginPage'`) before he/she can see the content of `ManageListingsPage` page.
|
|
||||||
|
|
||||||
There's also a `loadData` function defined. It is a special function that gets called if a page
|
|
||||||
needs to fetch more data (e.g. from Flex API) after redirecting to that route. We'll open up this
|
|
||||||
concept [later in this document](#loading-data).
|
|
||||||
|
|
||||||
In addition to these configurations, there's also a rarely used `setInitialValues` function that
|
|
||||||
could be defined and passed to a route:
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
path: '/l/:slug/:id/checkout',
|
|
||||||
name: 'CheckoutPage',
|
|
||||||
auth: true,
|
|
||||||
component: props => <CheckoutPage {...props} />,
|
|
||||||
setInitialValues: CheckoutPage.setInitialValues,
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
This function gets called when some page wants to pass forward some extra data before redirecting
|
|
||||||
user to that page. For example we could ask booking dates on ListingPage and initialize CheckoutPage
|
|
||||||
state with that data before buyer is redirected to CheckoutPage.
|
|
||||||
|
|
||||||
### How FTW renders a router with routeConfiguration.js
|
|
||||||
|
|
||||||
Aforementioned route configuration is used in `src/app.js`. For example, `ClientApp` defines
|
|
||||||
`BrowserRouter` and gives it a child component (`Routes`) that gets the configuration as `routes`
|
|
||||||
property.
|
|
||||||
|
|
||||||
Simplified `app.js` code that renders client-side FTW app:
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
|
||||||
import Routes from './Routes';
|
|
||||||
import routeConfiguration from './routeConfiguration';
|
|
||||||
//...
|
|
||||||
export const ClientApp = props => {
|
|
||||||
return (
|
|
||||||
<BrowserRouter>
|
|
||||||
<Routes routes={routeConfiguration()} />
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
`src/Routes.js` renders the `Route` navigational components (`Switch` renders the first `Route` that
|
|
||||||
matches the location):
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { Switch, Route } from 'react-router-dom';
|
|
||||||
//...
|
|
||||||
|
|
||||||
const Routes = (props, context) => {
|
|
||||||
//...
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
{routes.map(toRouteComponent)}
|
|
||||||
<Route component={NotFoundPage} />
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
Inside `src/Routes.js`, we also have a component called `RouteComponentRenderer`, which has three
|
|
||||||
important jobs:
|
|
||||||
|
|
||||||
- Calling loadData function, if those have been defined in `src/routeConfiguration.js`. This is an
|
|
||||||
asynchronous call, a page needs to define what gets rendered before data is complete.
|
|
||||||
- Reset scroll position after location change.
|
|
||||||
- Dispatch location changed actions to Redux store. This makes it possible for analytics Redux
|
|
||||||
middleware to listen location changes. For more information, see the
|
|
||||||
[How to set up Analytics for FTW](https://www.sharetribe.com/docs/guides/how-to-set-up-analytics-for-ftw/)
|
|
||||||
guide in Flex Docs.
|
|
||||||
|
|
||||||
## Linking
|
|
||||||
|
|
||||||
Linking is a special case in SPA. Using HTML `<a>` tags will cause browser to redirect to given
|
|
||||||
`href` location. That will cause all the resources to be fetched again, which is a slow and
|
|
||||||
unnecessary step for SPA. Instead, we just need to tell our router to render a different page by
|
|
||||||
adding or modifying browser's history entries.
|
|
||||||
|
|
||||||
### NamedLink and NamedRedirect
|
|
||||||
|
|
||||||
React Router exports a couple of
|
|
||||||
[navigational components](https://reacttraining.com/react-router/web/api/Link) (e.g.
|
|
||||||
`<Link to="/about">About</Link>`) that could be used for linking to different internal paths. Since
|
|
||||||
FTW is a template app, we want all the paths to be customizable too. That means that we can't use
|
|
||||||
paths directly when redirecting user to another Route. For example marketplace for German customer
|
|
||||||
might want to customize the LoginPage path to be `/anmelden` instead of `/login` - and that would
|
|
||||||
mean that all the _Links_ to it would need to be updated.
|
|
||||||
|
|
||||||
This is the reason why we have created names to different routes in `src/routeConfiguration.js`. We
|
|
||||||
have a component called `<NamedLink name="LoginPage" />` and its _name_ property creates a link to
|
|
||||||
the correct Route even if the path is changed in routeConfiguration. Needless to say that those
|
|
||||||
names should only be used for internal route mapping.
|
|
||||||
|
|
||||||
More complex example of `NamedLink`
|
|
||||||
|
|
||||||
```js
|
|
||||||
// Link to LoginPage:
|
|
||||||
<NamedLink name="LoginPage" />log in</NamedLink>
|
|
||||||
// Link to ListingPage with path `l/<listing-uuid>/<listing-title-as-url-slug>/`:
|
|
||||||
<NamedLink name="ListingPage" params={{ id: '<listing-uuid>', slug: '<listing-title-as-url-slug>' }}>some listing</NamedLink>
|
|
||||||
// Link to SearchPage with query parameter: bounds
|
|
||||||
<NamedLink name="SearchPage" to={{ search: '?bounds=60.53,22.38,60.33,22.06' }}>Turku city</NamedLink>
|
|
||||||
```
|
|
||||||
|
|
||||||
`NamedLink` is widely used in FTW, but there are some cases when we have made redirection to another
|
|
||||||
page if some data is missing (e.g. CheckoutPage redirects to ListingPage, if some data is missing or
|
|
||||||
it is old). This can be done with rendering component called `NamedRedirect`, which is a similar
|
|
||||||
wrapper for [Redirect component](https://reacttraining.com/react-router/web/api/Redirect).
|
|
||||||
|
|
||||||
### ExternalLink
|
|
||||||
|
|
||||||
There's also a component for external links. The reason why it exists is that there's a
|
|
||||||
[security issue](https://mathiasbynens.github.io/rel-noopener/) that can be exploited when a site is
|
|
||||||
linking to external resources. `ExternalLink` component has some safety measures to prevent those.
|
|
||||||
We recommend that all the external links are created using `ExternalLink`component instead of
|
|
||||||
directly writing anchors like `<a href="externalsite.com">External site</a>`. (You can just change
|
|
||||||
the JSX element accordinly: `<ExternalLink href="externalsite.com">External site</ExternalLink>`.)
|
|
||||||
|
|
||||||
## Loading data
|
|
||||||
|
|
||||||
If a page component needs to fetch data, it can be done as a part of navigation. A page component
|
|
||||||
needs to define a static function called `loadData`, which needs to return a Promise, which is
|
|
||||||
resolved when all the asynchronous Redux Thunk calls are completed.
|
|
||||||
|
|
||||||
For example here's a bit simplified version of `ListingPage.loadData` function:
|
|
||||||
|
|
||||||
```js
|
|
||||||
export const loadData = (params, search) => dispatch => {
|
|
||||||
const listingId = new UUID(params.id);
|
|
||||||
|
|
||||||
return Promise.all([
|
|
||||||
dispatch(showListing(listingId)), // fetch listing data
|
|
||||||
dispatch(fetchTimeSlots(listingId)), // fetch timeslots for booking calendar
|
|
||||||
dispatch(fetchReviews(listingId)), // fetch reviews related to this listing
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
> Unfortunately, `loadData` function needs to be separately mapped in routeConfiguration.js atm.
|
|
||||||
> There has been a problem with module initialization order and functional components have been used
|
|
||||||
> in routeConfiguration.js as wrappers to prevent a premature call to these static functions.
|
|
||||||
|
|
||||||
## Analytics
|
|
||||||
|
|
||||||
It is possible to track page views to gather information about navigation behaviour. Tracking is
|
|
||||||
tied to routing through `src/Routes.js` where `RouteRendererComponent` dispatches `LOCATION_CHANGED`
|
|
||||||
actions. These actions are handled by a global reducer (`src/ducks/Routing.duck.js`), but more
|
|
||||||
importantly, `src/analytics/analytics.js` (a Redux middleware) listens to these changes and sends
|
|
||||||
tracking events to configured services. For more information, see the
|
|
||||||
[How to set up Analytics for FTW](https://www.sharetribe.com/docs/guides/how-to-set-up-analytics-for-ftw/)
|
|
||||||
guide in Flex Docs.
|
|
||||||
|
|
||||||
## A brief introduction to SSR
|
|
||||||
|
|
||||||
Server-side rendering needs a better documentation at some point, but this routing setup is the key
|
|
||||||
to render any page on server-side without duplicating routing logic. We just need to fetch data if
|
|
||||||
`loadData` is defined on page component and then use `ReactDOMServer.renderToString` to render the
|
|
||||||
app to string (requested URL is a parameter for this render function).
|
|
||||||
|
|
||||||
So, instead of having something like this on Express server:
|
|
||||||
|
|
||||||
```js
|
|
||||||
app.get('/about', handleAbout);
|
|
||||||
```
|
|
||||||
|
|
||||||
We basically catch every path call using `*` on `server/index.js`:
|
|
||||||
|
|
||||||
```js
|
|
||||||
app.get('*', (req, res) => {
|
|
||||||
```
|
|
||||||
|
|
||||||
and then we ask our React app to
|
|
||||||
|
|
||||||
1. load data based on current URL (and return this preloaded state from Redux store)
|
|
||||||
2. render the correct page with this preloaded state (renderer also attaches preloadedState to
|
|
||||||
HTML-string to hydrate the app on the client-side)
|
|
||||||
3. send rendered HTML string as a response to the client browser
|
|
||||||
|
|
||||||
```js
|
|
||||||
dataLoader
|
|
||||||
.loadData(req.url, sdk)
|
|
||||||
.then(preloadedState => {
|
|
||||||
const html = renderer.render(req.url, context, preloadedState);
|
|
||||||
//...
|
|
||||||
res.send(html);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue