Introduction:

With the gradual decline of Backbone and other old frameworks, front-end MVC develops slowly and tends to be gradually replaced by MVVM/Flux.

However, in recent years, it can be found that React/Vue and Redux/Vuex have made further development in View layer and Model layer of MVC respectively. If you take the Controller layer of MVC a step further, you get an upgraded version of MVC, which we call IMVC (Isomorphic MVC).

IMVC enables a piece of code to run on both the server and the browser, with all the advantages of single-page and multi-page applications, and can be switched freely between the two modes through configuration items. With node.js, Webpack, Babel, and other infrastructure, we can have a much more sophisticated front-end architecture than before.

directory

  • 1. The concept and significance of isomorphism
    • 1.1. What is isomorphic?
    • 1.2, isomorphic javascript
  • 2. Types and levels of isomorphism
    • 2.1 Types of isomorphism
    • 2.2 Levels of isomorphism
  • 3. The value and function of isomorphism
    • 3.1 the value of isomorphism
    • 3.2 How can isomorphism speed up access experience
    • 3.3 isomorphism is the future trend
  • 4. Implementation strategy of isomorphism
  • 5. IMVC architecture
    • 5.1 Objectives of IMVC
    • 5.2 Selection of IMVC technology
    • 5.3. Why not just use the React bucket?
    • 5.4. Replace the React router with create-app
      • 5.4.1 Isomorphism concept of CREate-APP
      • 5.4.2 Configuration concept of CREate-app
      • 5.4.3 Create-app server rendering
      • 5.4.4 Create-APP’s flat routing concept
      • 5.4.5 Directory structure of create-app
    • 5.5 Basic mode of Controller
    • 5.6 relite, a simplified version of Redux
    • 5.7. Engineering facilities of Isomorphic-MVC
      • 5.7.1 How to realize real-time hot update of code?
      • 5.7.2 How to Handle on-demand CSS loading?
      • 5.7.3 How to achieve code cutting and loading on demand?
      • 5.7.4 How do I version static Resources?
      • 5.7.5 How do I Manage COMMAND Line Tasks?
  • 6. Practical cases
  • 7, endnotes

1. The concept and significance of isomorphism

1.1. What is isomorphic?

On the other hand, the concept of isomorphic is closely related to the development of the space. On the other hand, the concept of isomorphic is closely related to the development of space.

Isomorphism is a mapping defined between mathematical objects that reveals the relationships that exist between properties or operations of those objects. Two mathematical structures are said to be isomorphic if there is an isomorphic mapping between them. In general, isomorphic objects are completely equivalent structurally if the specific definition of their properties or operations is ignored.

Isomorphism is also used in physics, chemistry and other fields such as computers.

1.2, isomorphic javascript

Isomorphic javascript refers to a JS code that can run on the browser side as well as the server side.

IMVC

Photo: www.slideshare.net/spikebrehm/…

Isomorphic JS is much older than Progressive Web Apps. In 2009, Node.js came out, giving us the imagination of unified language for front and back ends; Furthermore, it is not impossible that the front and back ends share a common set of code.

There is a website, Isomorphic.net, which collects articles and projects related to isomorphic JS. Judging from the list of articles, back in 2011, the industry was already talking about isomorphic JS and thought it was going to be the future.

Unfortunately, isomorphic JS has not been developed in a real sense. Because, in 2011, Node.js and ECMAScript weren’t mature enough, we didn’t have the infrastructure to meet the isomorphism goals.

It’s 2017, and things are different. The ECMAScript 2015 standard provides a standard module specification that is common to both front and back ends. While neither Node.js nor browsers currently implement the ES2015 module standard, we do have tools like Babel and Webpack to take advantage of new language features in advance.

2. Types and levels of isomorphism

2.1 Types of isomorphism

Isomorphic JS comes in two varieties: “content isomorphism” and “form isomorphism”.

Content isomorphism refers to the complete equivalence of the code executed by the server and the browser. Such as:

function add(a, b) {
    return a + b
}Copy the code

The add function is the same on both the server and browser sides.

“Formal isomorphism”, on the other hand, is not isomorphism from the fundamentalist point of view. There is a part of the code that never executes on the browser side and another part that never executes on the server side. Such as:

function doSomething() {
  if (isServer) {
      // do something in server-side
  } else if (isClient) {
      // do something in client-side}}Copy the code

In NPM, many packages advertise themselves as isomorphic in the form of “formal isomorphism”. Without special treatment, “formal isomorphism” can increase the size of browser-loaded JS code. React, for example, has a size of 140+ KB that includes code that runs only on the server.

2.2 Levels of isomorphism

Isomorphism is not a Boolean, true or false; Isomorphism is a spectral form, isomorphism can be realized in a small range, isomorphism can be realized in a large range.

  • Function hierarchy: Fragmentary code snippets or functions that support isomorphism. For example, both the browser side and the server side implement setTimeout functions, and utility functions such as Lodash /underscore are isomorphic.

  • Feature level: Isomorphic code in this level usually undertakes certain business functions. React and Vue, for example, use virtual-DOM to implement isomorphism. They are renderers serving the View layer. For example, Redux and Vuex are also isomorphic and are responsible for data processing in the Model layer.

  • Framework level: Implementing isomorphism at the framework level, which may include all levels of isomorphism, requires careful handling of how the parts that support isomorphism and those that do not support isomorphism fit together properly.

The isomorphic-MVC (REFERRED to as IMVC) we discuss today is to realize isomorphism at the framework level.

3. The value and function of isomorphism

3.1 the value of isomorphism

Isomorphic JS, not only the aesthetic feeling on the abstract, it still has a lot of practical value.

  • SEO friendly: The View layer can run on both the browser side and the server side, meaning it can spit out HTML on the server side and support search engine scraping.

  • Speed up the visit experience: Server-side rendering speeds up browser-side rendering for the first visit, while browser-side rendering speeds up feedback for user interactions.

  • Code maintainability: Isomorphism can reduce the cost of language switching, reduce code duplication, and increase code maintainability.

It is possible to achieve the first two objectives by other means without using isomorphic schemes, but it is difficult to achieve all three objectives by other means.

3.2 How can isomorphism speed up access experience

The problem with browser-only rendering is that the page waits for js to load before it is visible.

client-side renderging

IMVC

Photo: www.slideshare.net/spikebrehm/…

Server-side rendering can speed up the first-visit experience by rendering the first screen of the page before js loads. However, users are only patient for the first load. If the page is refreshed frequently during the operation, users will feel slow.

SERVER-SIDE RENDERING

IMVC

Photo: www.slideshare.net/spikebrehm/…

Homogeneous rendering has two benefits: server-side rendering at first load and browser-side rendering during interaction.

3.3 isomorphism is the future trend

From the perspective of historical development, isomorphism is indeed a major trend in the future.

In the early days of Web development, the development mode used is: fat-server, thin-client

IMVC

Photo: www.slideshare.net/spikebrehm/…

The front end is just a thin layer that handles form validation, DOM manipulation, and JS animation. At this stage, there is no such thing as a “front end engineer”; server-side development is done by writing the front end code.

After Ajax was discovered, the Web entered the 2.0 era, and we generally respected the pattern is: thin-server, fat-client

IMVC

Photo: www.slideshare.net/spikebrehm/…

More and more business logic is migrated from the server side to the front end. There was a move towards “front end separation”, where the front end wanted the server to provide only restful interfaces and data persistence.

But at this stage, it’s not thorough enough. The front-end does not have full control of the rendering layer, at least the HTML skeleton requires server-side rendering, and the front-end does not implement server-side rendering.

To address the above issues, we are moving to the next phase, which takes the following modes: shared, fat-server, fat-client

IMVC

Photo: www.slideshare.net/spikebrehm/…

With the Node.js runtime, the front end takes full control of the rendering layer and implements the isomorphism of the rendering layer. Neither sacrificing the value of server-side rendering nor the convenience of browser-side rendering.

This is the future.

4. Implementation strategy of isomorphism

To realize isomorphism, we must first face up to the fact that overall isomorphism is meaningless. Why is that?

After all, the server side and the browser side are two different platforms and environments. They focus on solving different problems and have their own characteristics. Homogeneity obliterates their inherent differences and thus cannot give full play to their respective advantages.

Therefore, we will only implement isomorphism in the part where client and server intersect. Isomorphism is the entire process of rendering HTML on the server side and reusing it on the browser side.

We take two main approaches: 1) can be isomorphic code, direct reuse; 2) Code that cannot be isomorphic is encapsulated into formal isomorphism.

Just a few examples.

Gets the user-agent string.

IMVC

Photo: www.slideshare.net/spikebrehm/…

We can emulate the Navigator global object on the server side with req.get(‘user-agent’), or we can provide a getUserAgent method or function.

Get the Cookies.

IMVC

Photo: www.slideshare.net/spikebrehm/…

Cookies processing in our scene, there is a fast path, because we only focus on the first render isomorphism, other operations can be left to the browser side of the second render.

The main use of Cookies is in Ajax requests. Ajax requests can be set to automatically carry Cookies on the browser side, so you just need to silently add Cookies at the top of each Ajax request on the server side.

Redirects processing

IMVC

Photo: www.slideshare.net/spikebrehm/…

The redirection scenario is complicated, with at least three cases:

  • Server 302 Redirection:res.redirect(xxx)
  • Browser location redirection:location.href = xxxlocation.replace(xxx)
  • Browser side pushState redirection:history.push(xxx)history.replace(xxx)

We need to encapsulate a redirect function that selects the correct redirect based on the input URL and environment information.

5. IMVC architecture

5.1 Objectives of IMVC

IMVC aims for isomorphism at the framework level, and we require that it must do the following

  • Simple to use, beginners can quickly start
  • Maintain only one set of ES2015+ code
  • Both single-page app and multi-page app (SPA + SSR)
  • Can be deployed to any publication path (Basename/RootPath)
  • A single command starts the full development environment
  • One command completes the packaging/deployment process

Some functions are run-time, and some are development environment specific. JavaScript is an interpreted language, but at this stage of the development of the front-end industry, its development mode has become very rich, either in the simplest way, a notepad and a browser, or with an IDE and a series of development, testing and deployment process support.

5.2 Selection of IMVC technology

  • Router: create-app = history + path-to-regexp
  • View: React = renderToDOM || renderToString
  • Model: relite = redux-like library
  • Ajax: isomorphic-fetch

In theory, IMVC is an architectural idea that does not define which technology stack we use. However, in order to land IMVC, one has to make a choice. These are the technology stacks we currently select, which may be upgraded or replaced with other technologies in the future.

5.3. Why not just use the React bucket?

As you may have noticed, we use a lot of React related technologies, but not the React bucket, for the following reasons:

  • The current React Bucket is actually wild and doesn’t work with Facebook
  • The React-Router concept didn’t fit the bill
  • Redux is suitable for large applications, while our main scenarios are small and medium sized
  • Frequent upgrades result in high learning costs and a cleaner layer of API encapsulation

The current family bucket is just a combination of some popular libraries in the community. Facebook really with the bucket is the react | flux | relay | graphql, even they are not made server rendering, react with PHP.

We think the React-Router concept is isomorphic and wrong. It ignores one important fact: the server is Router driven. If you bundle the Router with the React View, how does the Router load Controller or asynchronous request data when the View is already instantiated?

From a functional programming perspective, React advocates pure components that need to be isolated from side effects, while the Router is a source of side effects. Mixing the two together is pollution. It is also debatable that the Router is not a UI but is written as a JSX component.

As a result, even the latest version of React- Router-V4 implements homogeneous rendering in a complex and bloated way, with a routing table and ajax request logic on both the server and browser sides. Click here to see the code

As for Redux, its authors have said publicly, “You probably don’t need Redux.” When introducing Redux, we need to reflect on the necessity of introducing it.

There is no doubt that Redux’s model is excellent, structured and easy to maintain. However, it is also tedious, and you may have to operate on several files across folders to complete a function. The significant benefits of these costs won’t be realized until the app is complex enough. In other modes, after a certain level of complexity, the app becomes difficult to maintain; Redux’s maintainability remains strong, and that’s where its value lies. (It’s worth noting that encapsulating a simplified API layer on top of Redux is probably the wrong thing to do, in my opinion. Redux’s source code is concise, its intent is clear, and it can be simplified, but why doesn’t it do it itself? Is it designed that way? Did your encapsulation defeat its design purpose?)

The question to consider before using Redux is, do we fall into the category of large scale applications?

The front end is changing fast, and frameworks and libraries are constantly updated to keep developers busy. We need to do a secondary encapsulation based on our own needs to get a cleaner set of apis that hide some of the complexity and reduce the cost of learning.

5.4. Replace the React router with create-app

Create-app is a library we implemented for isomorphism, which consists of the following three parts:

  • History: Underlying library that the react-router depends on
  • Path-to-regexp: The underlying library expressJS relies on
  • Controller: Implement the Controller layer in addition to the View(React) layer and Model layer

Create-app uses the react-router dependency history.js to manage history status in the browser. Reuse ExpressJS path-to-regexp to parse parameters from the path pattern.

In our opinion, React and Redux correspond to MVC View and Model respectively, and they are both isomorphic. What we need is to realize the isomorphism of Controller layer.

5.4.1 Isomorphism concept of CREate-APP

IMVC

Create-app implements isomorphism in this way:

  • Enter the URL. Router matches the controller module according to the FORMAT of the URL
  • Call module-loader to load the Controller module and get the Controller class
  • View and Model are properties of the Controller class
  • new Controller(location, context)Get the controller instance
  • callcontroller.initMethod that must return an instance of the View
  • Call View-engine to render the view instance into HTML, DOM, native UI, etc., depending on the environment

The process is the same on both the server and browser sides.

5.4.2 Configuration concept of CREate-app

The server and browser load modules in different ways, the server is synchronous load, and the browser is asynchronous load; Their View-engines are also different. How do you handle these inconsistencies?

The answer is configuration.

const app = createApp({
    type: 'createHistory'.container: '#root'.context: {
        isClient: true|false.isServer: false|true. injectFeatures },loader: webpackLoader|commonjsLoader,
    routes: routes,
    viewEngine: ReactDOM|ReactDOMServer,
})
app.start() || app.render(url, context)Copy the code

The server and browser have their own entry files: client-entry.js and server.entry.js respectively. We just need to provide different configurations.

On the server side, the controller module is loaded by commonjsLoader. On the browser side, the controller module is loaded by webpackLoader.

The View-engine is also configured as different ReactDOM and ReactDOMServer on the server and browser sides.

Every instance of controller has a context parameter, which also comes from the configuration. This way, we can inject different platform features at run time. This not only divides the code, but also realizes the formal isomorphism.

5.4.3 Create-app server rendering

We think that simplicity is the right thing to do. Create-app implements server-side rendering as follows:

const app = createApp(serverSettings)
router.get(The '*'.async (req, res, next) => {
  try {
    const { content } = await app.render(req.url, serverContext)
    res.render('layout', { content })
  } catch(error) {
    next(error)
  }
})Copy the code

With no extra information, no extra code, enter a URL and context and return an HTML string with real data.

5.4.4 Create-APP’s flat routing concept

The value of react-Router, which supports and encourages nested routing, is questionable. It increases code reading costs, and the relationships between routing modules are coupled to the nested UI (React component), which is not flexible.

Using flat routing allows code to be decoupled, easier to read, and more flexible. Reuse between UIs can be achieved through direct nesting of React components.

Reusing a UI based on routing nesting can be an awkward situation where there is just one page that doesn’t need to share a header, and the header is not under its control.

// routes
export default [{
    path: '/demo'.controller: require('./home/controller')}, {path: '/demo/list'.controller: require('./list/controller')}, {path: '/demo/detail'.controller: require('./detail/controller')}]Copy the code

As you can see, our path doesn’t correspond to component, it corresponds to controller. By adding the Controller layer, we can get the first screen data from the Controller before instantiating the Component in the View layer.

Next. Js is also a homogeneous framework, which is essentially a simplified version of IMVC, except that its C layer is so thin that it hangs directly inside the static methods of the View component. Its routing configuration is currently based on the View file name, and its Controller layer is the view.getInitialprops static method, which only serves to get initialization props.

This layer is too thin, but it could be much richer, with fetch methods, built-in environment judgments, support for JSONp, support for mock data, support for timeout handling, such as automatically binding store to View, For example, provide richer lifecycle pageWillLeave (pages jump to other paths) and windowWillUnload (Windows close).

In short, side effects can’t be eliminated, they can only be isolated, and the View and Model are both pure-function and Immutabel-Data without side effects, so someone has to handle side effects. A new abstraction layer, Controller, was created.

5.4.5 Directory structure of create-app

├─ SRC // Source directory │ ├─ App-Demo │ ├─ App-Parts │ │ ├─ Shared │ ├─ SRC // Source directory │ ├─ App-Demo │ ├─ App-Parts │ │ ├─ Shared Project sharing method │ │ └ ─ ─ BaseController / / Controller inherits the Controller base class project layer │ │ ├ ─ ─ home / / specific page │ │ │ ├ ─ ─ Controller. The js / / Controller │ │ │ ├ ─ ─ model. The model of js / / │ │ │ └ ─ ─ the js / / view │ │ ├ ─ ─ * / / other pages │ │ └ ─ ─ routes. The js / / ABC project flat routing │ ├ ─ ─ │ app - * / / other projects ├ ─ ─ the components / / global Shared components │ ├ ─ ─ Shared / / global Shared file │ │ └ ─ ─ BaseController / / base class Controller │ ├ ─ ─ index. The js / / js global entry │ └ ─ ─ Routes.js // Global Flat Routing Exercises - static // source build target static folder

As you can see, create-App advocates a very different directory structure than Redux. The function of it is not in accordance with the abstract actionCreator | actionType | reducers | middleware | to arrange the container, it is based on the page by it’s page, each page has three components: Controller, Model and View.

Use the Routes table to string together pages.

Create-app adopts the “whole site SPA” mode, with only one entry file globally, index.js. The files in the SRC directory are the framework layer code shared by all projects, and the business code for each project is in the app-xxx folder.

The purpose of this design is to reduce migration costs and flexibly split and merge projects.

  • When a project is in its infancy, it can latch onto another project’s Git repository and use its existing infrastructure for rapid development.
  • When two projects are complex enough to merit splitting into two, they can be split into two projects by deleting each other’s folders in their entirety.
  • When two projects are merging, put them in the same Git repository differentlyapp-xxxIn the can.
  • We use the local routing table routes.js and nginx configuration to coordinate the URL access rules

Controller.js, model.js, and view.js for each page, along with their private dependencies, will be individually packaged into a file that will only be loaded on demand if the URL match succeeds. Ensure that the coexistence of multiple projects does not cause js volume expansion.

5.5 Basic mode of Controller

We added the controller abstraction layer, which will be responsible for connecting Model, View, History, LocalStorage, Server and other objects.

Controller is designed as a class in the OOP programming paradigm with the primary purpose of subjecting it to side effects so that the View and Model layers remain functional pure.

The basic mode of Controller is as follows:

class MyController extends BaseController {
  requireLogin = true // Whether to rely on the login state, BaseController automatic processing
  View = View / / view
  initialState = { count: 0 } // Model initialState
  actions = actions // Set of actions for model state changes
  handleIncre = (a)= > { // The event handler is automatically collected and passed to the View component
    let { history, store, fetch, location, context } = this // Function layering
    let { INCREMENT } = store.actions
    INCREMENT() // Call the action to update the state, and the view will update automatically
  }
  async shouldComponentCreate() {} // In this case, return false
  async componentWillCreate() {} // Fetch the first screen data here
  componentDidMount() {} // Fetch non-first screen data here
  pageWillLeave() {} // Execute the logic before the route jumps away
  windowWillUnload() {} // Execute the logic before the page closes here
}Copy the code

We put all the functional objects in the controller property, and the developer just needs to provide the configuration and definition, and call the relevant methods as needed during the rich lifecycle.

Its structure and mode are somewhat similar to vue and wechat applets.

5.6 relite, a simplified version of Redux

Although we don’t use Redux as an architecture for small to medium sized applications, we can incorporate some of the best ideas in Redux.

So, we implemented a simplified version of Redux called Relite.

  • ActionType, actionCreator, Reducer merge
  • Automatic bindActionCreators, with built-in support for asynchronous action
let EXEC_BY = (state, input) = > {
    let value = parseFloat(input, 10)
    return isNaN(value) ? state : { ... state,count: state.count + value
    }
}
let EXEC_ASYNC = async (state, input) => {
    await delay(1000)
    return EXEC_BY(state, input)
}
let store = createStore(
  { EXEC_BY, EXEC_ASYNC },
  { count: 0})Copy the code

What we are looking for are two cores of Redux: 1) pure-function and 2) immutation-data.

Therefore, the action function is designed as a pure function, whose function name is the action-type of Redux, whose function body is the reducer of Redux, and whose first parameter is the current state. Its second argument is the data carried by Redux’s actionCreator. Relite also has redux-Promise and redux-thunk built in. Developers can use async/await syntax to implement asynchronous actions.

Relite also requires state to be immutable as much as possible, and time travel can be implemented through additional Recorder plug-ins. Check out this demo to see what it looks like in action.

5.7. Engineering facilities of Isomorphic-MVC

The above describes some of the functions and features of IMVC at runtime. The following is a brief description of IMVC engineering facilities. We adopted:

  • Node.js runtime, NPM package management
  • Expressjs server framework
  • Babel compiles ES2015+ code to ES5
  • Webpack packages and compresses the source code
  • Standard.js checks code specifications
  • Prettier. js + git-hook code automatically beautify typeset
  • Mocha unit tests

5.7.1 How to realize real-time hot update of code?

  • Target: A command to start the development environment and modify the code without restarting the process
  • How to do it: One Webpack serves client and another webpack serves server
  • Client: Express + Webpack-dev-middleware compiles in memory
  • server: memory-fs + webpack + vm-module
  • The server webpack compiles to the in-memory simulated file system and executes with the built-in Virtual machine module of Node.js to get the new module

5.7.2 How to Handle on-demand CSS loading?

  • The source of the problem: The browser only waits for CSS resources to load before rendering the page before dom-ready
  • Problem description: When a single page jumps to another URL, the CSS resources are not loaded, and the page is displayed in a chaotic layout
  • Solution: Think of CSS as pre-loaded Ajax data, imported on demand as style tags
  • Optimization strategy: Preload data using context cache to avoid reloading

5.7.3 How to achieve code cutting and loading on demand?

  • Ensure does not use the webpack-only syntax require. Ensure
  • In the browser require is compiled as a load function and loaded asynchronously
  • In Node.js require is loaded synchronously
// webpack.config.js
{
      test: /controller\.jsx? $/.loader: 'bundle-loader'.query: {
        lazy: true.name: '[1]-[folder]'.regExp: /[\/\\]app-([^\/\\]+)[\/\\]/.source
      },
      exclude: /node_modules/
}Copy the code

5.7.4 How do I version static Resources?

  • Publish incrementally, using the hash file name of the code
  • Generate a static resource table with webpack.stats.plugin.js
  • Express uses stats. Json data to render pages
// webpack.config.js
output = {
    path: outputPath,
    filename: '[name]-[hash:6].js'.chunkFilename: '[name]-[chunkhash:6].js'
}Copy the code

5.7.5 How do I Manage COMMAND Line Tasks?

  • Use NPM-scripts to do the series-parallel logic for git, webpack, test, prettier in package.json
  • NPM start starts the complete development environment
  • NPM run start: The client starts the development environment without server rendering
  • NPM run Build starts automated compilation, build, and compression deployment tasks
  • NPM run build:show-prod visualizes the compilation results with webpack-bundle-Analyzer

6. Practical cases

  • isomorphic-cnode
  • kanxinqing
  • Ctrip Investment Promotion Platform Project ([email protected])
  • More projects in progress…

7, endnotes

Through practice and exploration, IMVC has been proved to be an effective mode, which realizes isomorphism in the real sense with a high degree of completion. Not just a concept on paper, but a practical solution that actually improves the development experience and efficiency. We will continue to explore this direction.