As we all know, the current WEB application, user experience requirements are getting higher and higher, WEB interaction is becoming more and more rich! Last year, Node led the wave of front and back end layering. React made the idea of layering more thoroughly implemented, especially the dark technology of React Universal or Isomorphic. Let’s take a look at the implementation.
React server method
If you’re familiar with React development, you’re probably familiar with the Reactdom. render method, which is how React renders into the DOM.
Any existing development pattern is inseparable from the DOM tree, as shown below:
The server rendering needs to be changed slightly, as shown below:
It can be seen from the comparison of the two figures that the server rendering needs to put the first render of React on the server side. React helps us translate the business component into a DOM tree of string type, and then outputs it to the browser through IO stream of back-end language.
React server render API
-
React. RenderToString converts the React element into an HTML string. Since reactid has already been rendered on the server side, it is rendered again on the browser side. This results in a high performance first page load! Isomorphic dark magic comes primarily from this API.
-
React. RenderToStaticMarkup, this API is equivalent to a simplified version of the renderToString, if your application is basically a static text, suggest using this method, a large number of less reactid, natural to streamline the DOM tree, Save some traffic on IO stream transmission.
Used with renderToString and renderToStaticMarkup, the ReactElement returned by createElement is passed as a parameter to the previous two methods.
The React to spin the Node
With this solution in place, we’re ready to start doing some things in Node. We’ll use the KOA Node framework to do this later.
So let’s create a new application, directory structure like this,
The react - server - koa - simple ├ ─ ─ app │ ├ ─ ─ assets │ │ ├ ─ ─ build │ │ ├ ─ ─ the SRC │ │ │ ├ ─ ─ img │ │ │ ├ ─ ─ js │ │ │ └ ─ ─ CSS │ │ ├ ─ ─ Package. The json │ │ └ ─ ─ webpack. Config. Js │ ├ ─ ─ middleware │ │ └ ─ ─ the static. The js static resource managed middleware (front) │ ├ ─ ─ the plugin │ │ └ ─ ─ Reactview (reactview plug-in) │ └ ─ ─ views │ ├ ─ ─ layout │ │ └ ─ ─ Default. Js │ ├ ─ ─ Device. The js │ └ ─ ─ Home. Js ├ ─ ─ the babelrc ├ ─ ─ .gitgnore ├─ app.js ├─ package.json ├─ readme.mdCopy the code
First, we need to implement a KOA plugin to render React as a server template by inserting the Render method into the app context to be called in the Controller layer. This. render(viewFileName, props, children) and the this.body output document flows to the browser.
/* * koa-react-view.js * provides react server render * {* options: {* viewPath: Viewpath, // the root directory of view files * docType: ", * extName: '.js', // View layer direct render filename suffix * writeResp: True, whether you need / / in the view layer directly output. * * *}} / module exports = function (app) {const opts = app. Config. Reactview | | {}; assert(opts && opts.viewpath && util.isString(opts.viewpath), '[reactview] viewpath is required, please check config! '); const options = Object.assign({}, defaultOpts, opts); app.context.render = function(filename, _locals, children) { let filepath = path.join(options.viewpath, filename); let render = opts.internals ? ReactDOMServer.renderToString : ReactDOMServer.renderToStaticMarkup; // merge koa state let props = Object.assign({}, this.state, _locals); let markup = options.doctype || ''; try { let component = require(filepath); // Transpiled ES6 may export components as { default: Component } component = component.default || component; markup += render(React.createElement(component, props, children)); } catch (err) { err.code = 'REACT'; throw err; } if (options.writeResp) { this.type = 'html'; this.body = markup; } return markup; }; };Copy the code
Then, we’ll write the Components on the React server,
/* * react-server-koa-simple - app/views/ home.js * Home template */ render() {let {microdata, mydata} = this.props; let homeJs = `${microdata.styleDomain}/build/${microdata.styleVersion}/js/home.js`; let scriptUrls = [homeJs]; return (
);
}Copy the code
A few things are done here: initialize the DOM tree, use the data attribute as a server data burial point, render the front and back common Content modules, and reference the front end modules
On the client side, we can easily take the data from the server side and use it directly.
import ReactDOM from 'react-dom';
import Content from './components/Content.js';
const microdata = JSON.parse(appEle.getAttribute('data-microdata'));
const mydata = JSON.parse(appEle.getAttribute('data-mydata'));
ReactDOM.render(
,
document.getElementById('demoApp')
);Copy the code
Then, when it came time to launch a simple KOA app, refine the entry app.js to test our ideas,
const koa = require('koa'); const koaRouter = require('koa-router'); const path = require('path'); const reactview = require('./app/plugin/reactview/app.js'); const Static = require('./app/middleware/static.js'); const App = ()=> { let app = koa(); let router = koaRouter(); Get ('/home', function*() {this.body = this.render(' home', { microdata: { domain: "//localhost:3000" }, mydata: { nick: 'server render body' } }); }); app.use(router.routes()).use(router.allowedMethods()); // Inject reactView const viewPath = path.join(__dirname, 'app/views'); app.config = { reactview: { viewpath: viewpath, // the root directory of view files doctype: '', extname: }} reactView (app);}} reactView (app);}} reactView (app); return app; }; const createApp = ()=> { const app = App(); App.listen (3000, ()=> {console.log('3000 is listening! '); }); return app; }; createApp();Copy the code
Now, visit the preset routing, http://localhost:3000/home to verify that the server render,
-
Server:
-
Browser side:
The React-router and koA-router are unified
Now that we’ve established the basis for server-side rendering, let’s consider how to unify the back-end and front-end routing.
Suppose our route is set to the form /device/:deviceID, then the server implements it this way,
// Initialize device/:deviceID Routing generator router.get('/device/:deviceID', Function *() {let deviceID = this.params.deviceid; this.body = this.render('Device', { isServer: true, microdata: microdata, mydata: { path: this.path, deviceID: deviceID, } }); });Copy the code
And the server side View template,
render() {
const { microdata, mydata, isServer } = this.props;
const deviceJs = `${microdata.styleDomain}/build/${microdata.styleVersion}/js/device.js`;
const scriptUrls = [deviceJs];
return (
);
}Copy the code
Front-end APP entry: app.js
function getServerData(key) { return JSON.parse(appEle.getAttribute(`data-${key}`)); }; // From the server side buried pointCopy the code
Mydata let microdata = getServerData('microdata'); let mydata = getServerData('mydata'); ReactDOM.render( , document.getElementById('demoApp'));
The iso. js module is common to the front and back ends, and the front-end route is also set to /device/:deviceID:
class Iso extends Component { static propTypes = { // ... }; // wrap the Route Component to inject props wrapComponent(Component) {const {microdata, mydata} = this.props; return React.createClass({ render() { return React.createElement(Component, { microdata: microdata, mydata: mydata }, this.props.children); }}); } // LayoutView is the layout of the route; Render () {const {isServer, mydata} = this.props; return ( ); }}Copy the code
This way I realize isomorphism between server and front-end routing!
Whether you are accessing these resource paths for the first time: /device/all, /device/ PC, /device/wireless, or manually switching these resource paths on the page have the same effect, which not only ensures the user experience of initial rendering in line with the expected DOM output, but also ensures the simplicity of the code. The most important thing is that the front and back end codes are one set. And it was developed by an engineer. Is that great?
Some points to note:
-
Iso render module needs to judge isServer, the server side with createMemoryHistory, front-end with browserHistory;
-
The React-Router component must wrap the wrapComponent if it needs to inject props. The server needs to pass props to render data. React-router-route only provides Component and does not support adding props. Take the source code of Route,
propTypes: {
path: string,
component: _PropTypes.component,
components: _PropTypes.components,
getComponent: func,
getComponents: func
},Copy the code
Why not use fetchData and data binding in Component instead of the front-end? Suffice it to say, you can make bold assumptions. Next is the isomorphism model that we will continue to discuss!
Discussion on isomorphic data processing
As we all know, the browser needs to make an Ajax request to get data, and the request URL is actually a routing controller on the corresponding server.
React has a life cycle and fetchData should be bound in componentDidMount. On the server, React doesn’t implement componentDidMount because React’s renderTranscation is split in two: ReactReconcileTransaction and ReactServerRenderingTransaction, its implementation on the service end remove some specific methods in the browser.
And server processing data is linear, is irreversible, initiate a request > go to the database to obtain data > business logic processing > assembly into HTML -> IO stream output to the browser. Obviously, the server side and the browser side are contradictory!
Experimental scheme
React does provide an entry that can wrap not only static properties but also static methods, and DEFINE_MANY:
/** * An object containing properties and methods that should be defined on * the component's constructor instead of its prototype (static methods). * * @type {object} * @optional */ statics: SpecPolicy.DEFINE_MANY,Copy the code
Using statics to extend our components like this,
class ContentView extends Component { statics: { fetchData: function (callback) { ContentData.fetch().then((data)=> { callback(data); }); }}; / / the browser so that to get the data componentDidMount () {this. Constructor. FetchData ((data) = > {enclosing setState ({data: data}); }); }... });Copy the code
Contentdata.fetch () needs to implement two sets:
-
Server-side: Encapsulates server-side Service layer methods
-
Browser-side: Encapsulates ajax or Fetch methods
Server-side call:
require('ContentView').fetchData((data)=> {
this.body = this.render('Device', {
isServer: true,
microdata: microdata,
mydata: data
});
});Copy the code
This can solve the data layer isomorphism! But I don’t think this is a good approach, like going back to JSP.
The approach our team is using now:
The resources
This article runs a complete example