Technology Stack
Our project primarily uses the following technologies for development, complemented by some other auxiliary tools.
- react
- react-router
- redux
- react-redux
Development and Production Environments
Development Environment
Since the project employs a frontend-backend separation, we require a complete development environment that includes the following functionalities:
- Data Mocking
- Webpack live compilation and hot reloading
- Convenient frontend-backend integration
Based on these requirements, we built this complete development environment using Express, Webpack, and Webpack-dev-middleware.
As you can see, all browser requests are intercepted by the local Node.js service. Static resource requests are delegated to webpack-dev-middleware
for processing, while API requests are handled differently depending on the environment.
Local Development
When ENV = 'development'
, which is the development environment, the page is rendered directly by reading local mock data.
Frontend-Backend Integration
When ENV = 'api'
, which we consider the integration environment, API requests are forwarded by Node.js to the actual backend service address for integration, thereby avoiding cross-origin issues that would arise from direct calls.
This allows for direct integration of local development code with the backend, significantly improving efficiency and eliminating the need for repeated build and deployment steps to the server.
Production Environment
The frontend and backend are deployed separately, with all static resources hosted on a CDN (example.cdn.com).
This means our page is at example.cdn/index.html
, but the API requests are to example.163.com/api/xxx
. We certainly cannot have users directly access example.cdn.com/index.html
, as this is unreasonable and introduces cross-origin issues.
So, when accessing example.dai.163.com
, how do we retrieve our HTML page?
See the diagram below:
An Nginx server is set up between the client and the backend service. When we access example.dai.163.com
, there are two types of requests:
- HTML page resources
- API requests
Both types of requests first pass through Nginx. Here, a determination is made: if it’s a page request, Nginx forwards it to the CDN; otherwise, it’s handed over to the backend service to respond to the API request.
Once the page is retrieved, all other static resources like CSS and JS are directly requested from the CDN. There’s nothing more to say about that.
Data Flow
Leveraging Redux for data flow management, let’s look at this diagram.
First, the store
is generated via middleware
and reducer
, then the project’s initial state
is obtained, and this initial state
is used to render the page’s initial status.
Taking the Home
page as an example, Home
first receives the initial state
as a prop
via the connect
method provided by react-redux
. The Home
component is composed of multiple different child components, and the data required by these components is then passed from Home
to its child components via props.
Once the initial state of Home
has loaded, we need to request some user data from the backend. At this point, we dispatch an action
in the following format:
{
types: ['home/start','home/success','home/failure'],
payload: {
api:
...
},
meta: {
isApi: true
}
}
All action
s will pass through each middleware
in the sequence we define.
Here, our action
will be hit by callApiMiddleware
via the isApi
flag in meta
, and it will perform the corresponding operations.
For instance, within this middleware, we make actual API requests, dispatch corresponding action
s upon request success or failure, and handle some unified business logic. For example, we have a consistent convention for the code
value returned by backend APIs: assuming 1 for success, 2 for failure, and 3 for not logged in. We can then process these business logics within the middleware.
After the request succeeds and the page is rendered, suppose a user clicks a button that needs to invoke some native
functionality, such as taking a photo. We then dispatch a camera/start
action
to invoke the camera functionality:
{
types: ['sdk/start','sdk/success','sdk/failure'],
payload: {
command:
...
},
meta: {
isSDK: true
}
}
Similarly, this action
will be recognized and processed by EpaySDKMiddleware
. When invoking native functionality, to ensure security, we need to send a request to the backend to obtain a signature. At this point, EpaySDKMiddleware
can dispatch an API request action
, which will also go through all middleware
s. This API request action
will then be processed by callApiMiddleware
just like the flow described above.
The existence of middleware makes the entire process very clear: API request middleware only handles API requests, and native API call middleware only handles native calls. When a native API call requires a backend API request, it dispatches an action
that goes through the API request middleware.
Each middleware focuses solely on its own concerns, which facilitates future maintenance and also provides an excellent way to extend functionality. The View
layer only needs to dispatch actions and then render the page with the data, without needing to worry about other logic.
An Example
Let’s assume we have the following routing configuration.
{
component: App,
path: '/',
onEnter: initLogin(store),
indexRoute: {
getComponent(nextState, cb) {
require.ensure([], require => {
cb(null, require('../views/Home').default)
}, 'Home')
},
onEnter: initHome(store)
},
childRoutes: [
createActivateRoute(store),
{
path: 'test',
indexRoute: {
getComponent(nextState, cb) {
require.ensure([], require => {
cb(null, require('../views/Test').default)
}, 'Test')
}
}
},
...
]
}
Now, let’s look at a complete process in conjunction with react-router
. When we enter example.dai.163.com/index.html/#/
in our browser:
First, as mentioned in the Production Environment section above, we retrieve the necessary HTML, CSS, and JS for the page.
Then, the Provider
and Router
components are rendered, providing store
injection and routing control, respectively.
At this point, the root path route matching is triggered, loading the root component APP
. Then, according to the routing rules, IndexRouter
is matched, and the Home
component is loaded.
The subsequent steps are the same as described in the Data Flow section.
Summary
Building upon a complete frontend-backend separation, a robust development environment can significantly boost our development efficiency and reduce the cost of frontend-backend integration.
Furthermore, by leveraging Redux principles to implement a unidirectional data flow, we can achieve a very clear data flow. And with the help of middleware, we can more effectively control the data flow process, providing endless possibilities for future project expansion.
This article was published on May 12, 2017 and last updated on May 12, 2017, 3068 days ago. The content may be outdated.