Fairy – A front and back end separation frame

A can support before and after the separation and support a complete framework of intermediary isomorphism, maybe now it is not perfect enough, but I’ll build the framework of problems are listed, to facilitate others encounter problems no longer need to search everywhere, hope to have some help for those who build framework, the document will be regularly updated and optimization, you can watch the program at any time See the document update, also hope to finally become a complete and perfect framework, if these questions help you, please click star, thanks ~ ~

Why is it called that?

It was a gift for a cat called Fairy Mo

How to install

Mysql > create a Mysql database using phpMyadmin and other tools. Finally, configure the mysql database name, user name and password in server/config/db.json

npm i
npm startCopy the code

Want to build a framework like this from start to finish? Well, LET me write down the build process in as much detail as possible

preface

Why do we use intermediate isomorphism? What is an intermediate isomorphism?

Before a isomorphism, we will let the back-end output API json data, and the front end to receive the data, encapsulate and assembling, so there will be a problem, is that if the business has changed, then the interface changes, at this time the front-end and back-end workers need to modify the byte code and business logic, before the main problems are as follows:

  • 1. The front and back ends need to be developed separately and connected to each other using JSON interfaces
The back-end uses its own business to realize the splicing of business logic and JSON data, and the front-end receives JSON data to complete the conversion of data to the interface, which costs a lotCopy the code
  • 2. The problem of SEO
SEO optimization problem of asynchronous loading, no server rendering, spiders can't grab data, no SEOCopy the code

The simple reason is to reduce development costs and increase overall project maintainability, Moreover, this technology can effectively reduce the speed of web page loading and provide excellent SEO optimization capabilities. It can also take advantage of the high concurrency of Nodejs and let Nodejs deal with its best aspects, thus increasing the load capacity of the overall architecture, saving hardware costs and improving the load capacity of the project

2. Build webpack and Babel environment tools

When we finished with Nodejs, one big pitfall was the need to use some automation tools, mainly WebPack and Babel. Webpack we used version 2.0, which was also recently released

Webpack is currently the most popular front-end resource modular management and packaging tool. It can package many loose modules according to dependencies and rules into front-end resources suitable for production deployment. You can also code separate modules that are loaded on demand and load them asynchronously when they are actually needed. Through loader conversion, any form of resources can be regarded as modules, such as CommonJs module, AMD module, ES6 module, CSS, images, JSON, Coffeescript, LESS, etc.

Can see the official introduction, more is the tool is defined as a modular management and packaging tools, also is such, it is in our front end work is responsible for the function of automatic packaging, it can help us to merge automatically packaging js, remove duplicate js code, documentation such as automatic processing some style, so a stack of automation, complete all need to manually before Operational work

Babel is a conversion compiler that converts ES6 into code that can be run in a browser. Babel was created by Sebastian McKenzie, a developer from Australia. His goal was to make Babel able to handle all of ES6’s new syntaxes, and to build in the React JSX extension and support for flow-type annotations.

Babel will automatically convert the new ES6 and ES7 features we use into ES5 syntax that all browsers are compatible with, and will make your code more formal and tidy.

When I used these two tools at that time, Webpack felt that the configuration was very troublesome, and there were a lot of loaders and various configurations, which felt very troublesome, but after I got used to it, I found it was very simple

How to configure webPack2 configuration and how to make the environment support hot loading

This part because is the client environment tool configuration, will not explain particularly detailed, we can go to the official Chinese website for reading and learning, but will encounter some pits, and how to configure, we look at the webPack2 configuration is what, the current environment configuration Webpack configuration file

Why 2 configuration files? What is devServer.js?

Development environment configuration files (webpack.config.dev.js & devServer.js)

A configuration file (webpack.config.dev.js) is used for the development environment. It supports some hot loading and hot replacement functions, and it does not need to be washed to see the changes at any time. It also supports source Map support for debugging pages and scripts. And of course we need to do that and we need to run it with a server

Let’s take a look at the code snippet:

	//Load the Webpack module
    webpack = require('webpack'),
    //Load automatic HTML automatic compilation plug-in
    HtmlWebpackPlugin = require('html-webpack-plugin'),
    autoprefixer = require('autoprefixer'),
    precss = require('precss'),
    postcsseasysprites = require('postcss-easysprites'),
    //Load the common component plug-in
    CommonsChunkPlugin = webpack.optimize.CommonsChunkPluginCopy the code

The headers are basically the plug-in components that need to be used, and the comments are clearly written, so we can implement the corresponding functions by referring to these plug-ins. Let me explain them one by one

webpack = require('webpack'),Copy the code

I don’t want to talk about that

HtmlWebpackPlugin = require('html-webpack-plugin'),Copy the code

When referencing this component, we can automatically convert the template to an HTML page and automatically load the webpack-packed CSS links into the page source code

new HtmlWebpackPlugin({
            template: 'src/index.html'.//Page template address, support some special templates such as Jade, EJS, Handlebar, etc
            inject: true.//The position to insert the file, can be in the body or head
            hash: true.//Whether to add hash to the end of the page's resource file to prevent cache reading
            minify: {
                removeComments: true,
                collapseWhitespace: false
            },
			//Streamline optimization to get rid of line feeds and so on
            chunks: ['index'.'vendor'.'manifest'].//The name of the entry inserted into the file. Note that there must be a corresponding declaration in the entry, or the chunk extracted using CommonsChunkPlugin. Simple to understand that the page needs to read js file module
            filename: 'index.html'
			//The name of the final HTML file to be generated, where pathnames can be taken
}),Copy the code

 CommonsChunkPlugin = webpack.optimize.CommonsChunkPluginCopy the code

When webpack is packaged, if not subcontracted, it is packaged into a JS file with the name you gave it, for example:

entry: {
        index:  './src/index.js'
		}Copy the code

If we do not use this component, we will only generate an index,js file after packaging, so we still need to remember the branch packaging in order to extract some common packages. Why do we do this? Or in order to use the browser cache, you can also complete the replacement of the updated package when the project is new deployed, and the public part is not replaced, so that users do not need to download the public JS, from reducing the server or CDN pressure, right, save money ~ server free money ah, CDN does not charge ah?

How does it work? We’re not going to go straight to the code

Entrance to the configuration

entry: {
        index:
          './src/index.js',
        vendor: [
            'react'.'react-dom'.'redux'.'react-redux'.'react-router'.'axios']}Copy the code

We can see that the Vendor module has written some common modules used here. In order to facilitate the plug-in configuration of these public modules, they are packaged separately. The Index module is the script we use for single page application, which is all written by ourselves

Module configuration JS

new webpack.optimize.CommonsChunkPlugin({
            names: [
                'vendor'.'manifest'//The name of the subcontractor
            ],
            filename: jsDir+'[name].js' //Configure the output structure, which is configured to generate by path and module name
        }),Copy the code

Why is there a manifest? Webpack2 is used to store relationships, links, etc. If we don’t extract this module, the vendor will change after each package, and we lose the meaning of not replacing the vendor package when we replace resources. So every time the project is updated, just replace index.js and mainifest

    autoprefixer = require('autoprefixer'),
	//Automatic plus browser compatible scheme, mainly cSS3 compatible scheme
    precss = require('precss'),
	//PostCSS can be made to support some of SASS's syntactic features
    postcsseasysprites = require('postcss-easysprites'),
	//Support the front-end CSS Sprite function that the background image is automatically stitched and merged into a picture, reducing requestsCopy the code

PostCss and LESS and SASS are all preloaders of CSS. The main purpose of postCss is to make it easier and faster to write CSS and to support some programming features, such as loops and variables. In this case, we chose postCss, The reason is simple. 1. 2. The plugin supports Sass and Less functions

We have a look at webpack is how to process the file, Webpack uses the **loader(loader)** to process, with a variety of loaders for file refinement and feature execution, look at the code:

module: {
        //Loader configuration
        rules: [
            {
                test: /\.css$/,

                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true,
                            camelCase: true,
                            localIdentName: "[name]_[local]_[hash:base64:3]",
                            importLoaders: 1,
                            sourceMap: true
                        }
                    }, {
                        loader: "postcss-loader",
                        options: {
                            sourceMap: true.plugins:(a)= > [
                                precss(),
                                autoprefixer({
                                    browsers: ['last 3 version'.'ie >= 10']}),postcsseasysprites({imagePath: '../img', spritePath: './assets/dist/img'})
                            ]
                        }
                    }
                ]
            }, {
                test: /\.css$/,
                exclude: [path.resolve(srcDir, cssDir)],
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            importLoaders: 1,
                            sourceMap: true
                        }
                    }, {
                        loader: "postcss-loader",
                        options: {
                            sourceMap: true.plugins:(a)= > [
                                precss(),
                                autoprefixer({
                                    browsers: ['last 3 version'.'ie >= 10']}),postcsseasysprites({imagePath: '../img', spritePath: './assets/dist/img'})
                            ]
                        }
                    }
                ]

            }, {
                test: /\.js$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: "babel-loader",
                        options: {
                            presets: ['react-hmre']
                        }
                    }
                ]
            }, {
                test: /\.(png|jpeg|jpg|gif|svg)$/,
                use: [
                    {
                        loader: "file-loader",
                        options: {
                            name: 'dist/img/[name].[ext]'}}]}]},Copy the code

Very long. Let’s break it down:

CSS processing, the specific format does not say, say what it is, why do so

{
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader" //Used to handle the most basic CSS styles
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true.//Whether CSS-modules is supported
                            camelCase: true.//Whether to support -(cylinder line) class, ID name
                            localIdentName: "[name]_[local]_[hash:base64:3]".//The generation format of csS-modules
                            importLoaders: 1.//Whether CSS import methods are supported
                            sourceMap: true //Whether to generate CSS sourceMap, mainly for easy debugging
                        }
                    }, {
                        loader: "postcss-loader".//PostCSS load modules, you can use postCSS plug-in modules
                        options: {
                            sourceMap: true.plugins:(a)= > [
                                precss(), //Some features of Sass are supported
                                autoprefixer({
                                    browsers: ['last 3 version'.'ie >= 10']}),//CSS3 automation compatible solution
                                postcsseasysprites({imagePath: '../img', spritePath: './assets/dist/img'}) //Supports CSS sprites
                            ]
                        }
                    }
                ]
            }, {
                test: /\.css$/,
                exclude: [path.resolve(srcDir, cssDir)],
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            importLoaders: 1,
                            sourceMap: true
                        }
                    }, {
                        loader: "postcss-loader",
                        options: {
                            sourceMap: true.plugins:(a)= > [
                                precss(),
                                autoprefixer({
                                    browsers: ['last 3 version'.'ie >= 10']}),postcsseasysprites({imagePath: '../img', spritePath: './assets/dist/img'})]}}]},Copy the code

After looking at the code, we’ll focus on CSS-modules. The main reason for referring to CSS modules is to allow styles to be automatically tagged and hash, so that styles never collide. The obvious benefit of this is that you don’t have to worry about style name conflicts when you’re working together. In addition, CSS -modules can be used to reduce cascading styles and reduce parent references. Such low-level is conducive to the reuse and utilization of styles, making them more general and enhancing the reuse of styles.

The reason why 2 CSS loader modules are written here is very simple, because to prevent other plug-ins from adding CSS -modules to the module’s style, we need to add another loader so that other CSS styles not in the specified folder are not affected by CSS -modules. Exclude: [path.resolve(srcDir, cssDir)] and delete the csS-modules configuration.

Let’s look at the configuration of the output

output: {
        path: assetsDir,//Path indicates the output path of the JS file
        filename: jsDir + '[name].js'.//Used to configure the output file name format
        publicPath: '/' //Public path, used to configure the path added before all resources, the specific purpose of this path will be explained in the generated directory
    },Copy the code

Finally, let’s take a look at the development tool. By using this tool, source-Map can be automatically loaded, which is convenient for our debugging and development. After all, the compressed code cannot be debugged, while source-Map can restore the previous code and point to the location, which is convenient for our operation

  devtool: 'source-map'.Copy the code

Because it’s a development environment, one important feature we need is react hot loading. What is hot loading? We use webpack-dev-server and React-hot-loader3 to make changes to the react script and state control of redux without refreshing the page. Can complete the page hot load and replacement function. Let’s look at the configuration method:

Create devServer.js to execute server execution and configuration and attach the react-hot-loader3 plugin with the following code:

//Load the Path module of Node
const path = require('path');
//Load the Webpack module
const webpack = require('webpack');
// const express = require('express');
const WebpackDevServer = require('webpack-dev-server');
//Load the WebPack configuration file
const config = require('./webpack.config.dev');

//Configure and initialize the Koa server
var creatServer =(a)= > {
    //Initialize the WebPack application
    let compiler = webpack(config);
    //Call the Webpack hot loading module and its corresponding parameters
    let app = new WebpackDevServer(webpack(config), {
        publicPath: config.output.publicPath.//The output path of the file, because it is executed in memory, so it can not see the specific file
        hot: true.//Whether to enable the hot loading function
        historyApiFallback: true.//Whether to record browser history and work with the React-router
        stats: {
            colors: true //Color code}});//The call opens port 5000 for testing and development
    app.listen(5000.function(err) {
        if (err) {
            console.log(err);
        }
        console.log('Listening at localhost:5000');
    });
};

//Call the create KOA server method
creatServer(a);Copy the code

The webpack-dev-server is a miniature Express or KOA framework that can be used to implement a simple local server using NodeJS and support hot replace. The main thing is to check the webPack process and make the application hot loaded, but using this plugin does not complete all the hot loading effects. For example, we have problems with redux, because the hot replacement does not preserve state, so when we use it, every time we save, The React component will not retain its state, so we need to introduce another plugin to solve this problem. Let’s see how to use this plugin. There are many ways to use this plugin

Step 1: Where do you add the top 3 sentences to the entry file

entry: {
    index: [
      'react-hot-loader/patch'.'webpack-dev-server/client? http://0.0.0.0:5000'.'webpack/hot/only-dev-server'.'./src/index.js']}Copy the code

Step 2: Add hot update plug-ins to webpack

new webpack.HotModuleReplacementPlugin(),Copy the code

Step 3: add the corresponding hot loading module in Babel

We need to add a file in the root directory. Babelrc to configure the Babel configuration

We just need to add a hot-loaded plug-in

{
    "plugins": [ "react-hot-loader/babel"]}Copy the code

So let’s look at the configuration of Babel

{
    "presets": ["react"."es2015"."stage-0"."stage-1"."stage-2"."stage-3"]."plugins": ["transform-class-properties"."transform-es2015-modules-commonjs"."transform-runtime"."react-hot-loader/babel"]}Copy the code

We use a lot of converters and add-ons in Babel, and it’s easy to install. The package we use has the following features

"babel-cli": "^ 6.23.0"."babel-core": "^ 6.6.5"."babel-eslint": "^ 6.1.0"."babel-loader": "^ 6.2.4"."babel-plugin-transform-class-properties": "^ 6.11.5"."babel-plugin-transform-es2015-modules-commonjs": "^ 6.23.0"."babel-plugin-transform-react-jsx": "^ 6.23.0"."babel-plugin-transform-require-ignore": "^ hundreds"."babel-plugin-transform-runtime": "^ 6.23.0"."babel-polyfill": "^ 6.23.0"."babel-preset-es2015": "^ 6.3.13"."babel-preset-react": "^ 6.3.13"."babel-preset-react-hmre": "^ 1.1.1"."babel-preset-stage-0": "^ 6.22.0"."babel-preset-stage-1": "^ 6.22.0"."babel-preset-stage-2": "^ 6.13.0"."babel-preset-stage-3": "^ 6.22.0"."babel-register": "^ 6.23.0"."babel-runtime": "^ 6.23.0".Copy the code

Finally, let’s take a look at the generated WebPack configuration file

ExtractTextPlugin = require('extract-text-webpack-plugin'),Copy the code

When WebPack packages code, you can see that the styles are generated directly on the page, so if you want to reference the styles in a single file, you need to use this plugin. When you use this plugin, you can make the page reference CSS in link mode. For some large styles, it is better to store CSS styles in the browser cache to reduce the load of server data. The configuration is as follows:

new ExtractTextPlugin('dist/css/style.css'),Copy the code

Here we have compressed all styles into a style.css file, but of course you can do it separately

new ExtractTextPlugin(cssDir + '[name].css'),Copy the code

    //Load JS module compression compiler plug-in
    UglifyJsPlugin = webpack.optimize.UglifyJsPlugin.Copy the code

Load the compression module, which can compress JS into the most simplified code and greatly reduce the generated file size. The configuration is as follows:

new UglifyJsPlugin({
    //Most compact output
    beautify: false.//Delete all comments
    comments: false,
    compress: {
      //UglifyJs does not print a warning when it deletes unused code
      warnings: false.//Delete all 'console' statements
      //It is also compatible with Internet Explorer
      drop_console: true.//Nested variables that are defined but used only once
      collapse_vars: true.//Extract static values that occur multiple times but are not defined as variables to reference
      reduce_vars: true,}})Copy the code

OK, we have finished the configuration of Webpack and Babel, and then we can start the development. This part is mainly about the lack of materials. Now we have the Chinese official website, it is better.

4. Synchronize front-end and back-end routes

Now that we understand the React lifecycle and see how servers use the official 2 methods for server rendering, let’s take a look at how to structure common routes for both the front and back ends

What is a React-router? Take a look at the official introduction

React Router is the complete React routing solution

The React Router keeps the UI and URL in sync. It has a simple API and powerful features such as code buffering, dynamic route matching, and establishing the correct location transition processing. The URL should be your first thought, not an afterthought.

The browser sends requests to the background server, and then the background feeds back the content. Now the React-Router takes over, and the redirect is implemented in the front end. Why can this be implemented? Thanks to HTML’s new History API

HTML5 new history API can achieve no refresh change address bar links, with AJAX can achieve no refresh jump. Simply put: Assuming the current page is renfei.org/, execute the following JavaScript statement: window.history.pushState(null, NULL, "/profile/"); The address bar then changes to renfei.org/profile/, but the browser doesn't refresh the page or even detect if the target page exists.Copy the code

So what do we do with a React-router in an intermediate isomorphism? Of course, it is done for the synchronization of routes between the front and back ends

  • When a user accesses a page for the first time, the server processes the route and outputs the related page content
  • The client user clicks a link to jump to, and the client route processes, renders and displays the related components
  • The user refreshes the page after the front-end jump, which is intercepted by the server route and rendered by the server to return * page content

Let’s look at code:

The front-end route Settings are as follows:

const Routers = (
    <Router history={browserHistory}>
        <Route path="/" component={Home}/>
        <Route path="/user" component={User}/>
        <Route path="/login" component={Login}/>
        <Route path="/reg" component={Reg}/>
      <Route path="/logout" component={Logout}/>
        <Route path="*" component={Page404}/>
    </Router>
	export default Routers;
);Copy the code

And you can see that we’re using browserHistory, which is a new feature of HTML5, but requires browsers, and not supported by IE6-8, and hashHistory, The difference is that the hash method adds the form site.com/#/index to the link, so that browser history can remember each page switch, and also uses the anchor feature

Take a look at the configuration snippet on the server side:

const router = new Router(a);//Index page route
router.get('/'.require('../containers/index.js').index);
//404 page route
router.get('/user'.require('../containers/user.js').index);
router.get('/get_user_info'.require('../containers/user.js').getUserInfo);
//User page route
router.get('/ 404'.require('../containers/404.js').index);
//Login page route
router.get('/login'.require('../containers/login.js').index);
router.post('/login'.require('../containers/login.js').login);
router.get('/logout'.require('../containers/login.js').logout);

//Reg page route
router.get('/reg'.require('../containers/reg.js').index);
router.post('/reg_user'.require('../containers/reg.js').reg);
router.post('/vaildate_user'.require('../containers/reg.js').vaildate_user);
router.post('/vaildate_email'.require('../containers/reg.js').vaildate_email);
//set a router
module.exports = router.routes(a)Copy the code

Our server uses KOA2, so the attached route is also the corresponding KOA-Router. Since the background architecture is MVC structure, let’s take a look at the code snippet of the controller layer read by the route

import routes from '. /.. /client/src/route/router.js';

export async function index(ctx.next) {
    console.log(ctx.state.user.ctx.isAuthenticated());
    switch (ctx.accepts("json"."html")) {
        case "html":
            {
                match({
                    routes,
                    location: ctx.url},error.redirectLocation.renderProps) = > {
                    if (error) {
                        console.log(500)}else if (redirectLocation) {
                        console.log(302)}else if (renderProps) {
                        //iinit store
                        let loginStore = {user:{logined:ctx.isAuthenticated()}};
                        const store = configureStore(loginStore);
                        console.log(store.getState());
                        ctx.body = layout(renderToString(
                            <Provider store={store}>
                                <RouterContext {.renderProps}/>
                            </Provider>
                        ), store.getState());

                    } else {
                        console.log(404); }})}break;
        case "json":
            {
                let callBackData = {
                    'status': 200.'message': 'This is the home page'.'data':{}};ctx.body = callBackData;
            }
            break;
        default:
            {
                // allow json and html only
                ctx.throw(406."allow json and html only");
                return; }}};Copy the code

The Match method of the React – Router is used here. This method automatically reads the route file of the front end and feeds back the module code through the module that matches the path. The React server renders the module directly

Here you can see the design of the route. Koa2 is used in the back end, so you can judge the type of the request, which makes full use of the advantages of links. You can request the same address, because the request type is different, determine whether it is HTML or JSON, feedback different data structure, so as to achieve the rich application of route

6. Server selection – KOA2

So now that we’ve basically seen the use of the three isomorphisms, why we’re doing it and how we’re doing it, let’s look at the server side architecture and some of the components that we’re using, okay

I used it on the server framework side, I chose KOA2 in Express and KOA, the reason why I chose it is simple, it is lighter architecture, better middleware mechanism and strong performance, it is written in ES6 standard, it is very friendly for me to use the new features of ES6.

To start a server, we create an app.js file in the root directory, and then write the corresponding code to create a KOA server

const Koa = require('koa');
const app = new Koa(a);// response
app.use(ctx = > {
  ctx.body = 'Hello Koa';
});

app.listen(3000);Copy the code

For other koA related documents, please refer to the official Chinese document, which lists the various middleware used here

Verify user permissions for passport

For authentication, we chose Nodejs’ most common permission authentication component, which also supports OAuth, OAuth2 and OpenID standard logins.

React related problems

The problem

Error: setState(...) : Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the App component.Copy the code

why

Reason is not clear in time the timer or variables, the error will cause memory leak | using this define variables, and then use componentWillUnmount () to clear the timer, timer method as shown in the official demo, as follows:Copy the code

The solution

class Timer extends React.Component {
  constructor(props) {
    super(props);
    this.state = {secondsElapsed: 0};
  }

  tick() {
    this.setState((prevState) = > ({
      secondsElapsed: prevState.secondsElapsed + 1
    }));
  }

  componentDidMount() {
    this.interval = setInterval(() = > this.tick(), 1000);
  }

  componentWillUnmount() {
    clearInterval(this.interval);
  }

  render() {
    return (
      <div>Seconds Elapsed: {this.state.secondsElapsed}</div>); }}ReactDOM.render(<Timer />, mountNode);Copy the code

The problem

Google error ReactDOMComponentTree. Js: 113 Uncaught TypeError: Cannot read property '__reactInternalInstance$xvrt44g6a8' of null at Object.getClosestInstanceFromNode. And Uncaught RangeError: Maximum Call Stack Size exceededCopy the code

why

Unknown, may be image reuse or stack caused memory overflow and errorCopy the code

The solution

willrender((
    <Provider store={store}>
        {routes}
    </Provider>
), document.getElementById('root')); Instead ofrender((
  <div>
    <Provider store={store}>
        {routes}
    </Provider>
  </div>
), document.getElementById('root'));Copy the code

Backend permission validation classes

The problem

When using passport, cookies have been unable to be written and cannot be authenticatedCopy the code

why

The reason is that the problem is caused by not writing "await" while executing the code so that the verification operation is finished and the subsequent operation is carried outCopy the code

The solution

We just need to add await and wait for the asynchronous execution to complete and pass the successful content to the HTTP body as follows:Copy the code
 await passport.authenticate('local'.function(err.user.info.status) {...}Copy the code