Vue-SSR Series (1) vue2.0+node+express+webpack to realize vue-ssr server rendering of single-page Application small demo

Keywords: Vue Webpack JSON npm

First, what is server-side rendering (SSR)?

Roughly speaking, the server splices the static pages requested by users and returns them directly to the client. The client activates these static pages to make them dynamic and responsive to subsequent data changes.

Second, why use server-side rendering (SSR)?

1. Better SEO, because search engine crawler crawler tools can directly view fully rendered pages.

2. Generate a better user experience, faster time-to-content, especially for slow network conditions or slow devices. You don't have to wait until all JavaScript is downloaded and executed to display the server rendered tags, so your users will see the full rendered pages faster.

Basic usage

First, vue-ssr needs an important plug-in, so we need to install it.
npm install vue vue-server-renderer --save

3.1 Rendering a Simple Example (Official Website)

// Step 1: Create a Vue instance
const Vue = require('vue')
const app = new Vue({
  template: `<div>Hello World</div>`
})

// Step 2: Create a renderer
const renderer = require('vue-server-renderer').createRenderer()

// Step 3: Render the Vue instance to HTML
renderer.renderToString(app, (err, html) => {
  if (err) throw err
  console.log(html)
  // => <div data-server-rendered="true">Hello World</div>
})

// At 2.5.0+, if no callback function is passed in, Promise is returned:
renderer.renderToString(app).then(html => {
  console.log(html)
}).catch(err => {
  console.error(err)
})

3.2 Case of Server Integration (Official Network)
First install npm install express --save

const Vue = require('vue')
const server = require('express')()
const renderer = require('vue-server-renderer').createRenderer()

server.get('*', (req, res) => {
  const app = new Vue({
    data: {
      url: req.url
    },
    template: `<div>Visiting URL Yes, {{ url }}</div>`
  })

  renderer.renderToString(app, (err, html) => {
    if (err) {
      res.status(500).end('Internal Server Error')
      return
    }
    res.end(`
      <!DOCTYPE html>
      <html lang="en">
        <head><title>Hello</title></head>
        <body>${html}</body>
      </html>
    `)
  })
})

server.listen(8080)

3.3 Use a page template

// index.html
<!DOCTYPE html>
<html lang="en">
  <head><title>Hello</title></head>
  <body>
    <!--vue-ssr-outlet-->
  </body>
</html>

Note the annotations -- this will be where the application HTML tags are injected.

const renderer = createRenderer({
  template: require('fs').readFileSync('./index.template.html', 'utf-8')
})

renderer.renderToString(app, (err, html) => {
  console.log(html) // html will be the complete page for injecting application content
})

Of course, the above content is only the foundation of the foundation. Ultimately, we need to integrate vue-cli, and combine the configuration of node, express, webpack, etc. to generate a more flexible vue-ssr application. It took nearly four days to look at the api, check the information, and finally build a static single-page application vue-ssr. Share it while it's hot. After all, you're old and you don't have a good memory.

4. Vue 2.0+node+express+webpack to realize vue-ssr single page application

First, we need to install vue-cli, specific installation, you can refer to the first article in my Vue series, if the previous global installation, then just need init.

Screen snapshot 2018-08-28 p.m. 9.55.30.png

At first, I couldn't understand the map on the official website. Later, after building it, I found that the official one was official, just as clear as the Beijing Metro Map. To learn from others, ssr has two entrance files, client.js and server.js, which contain application code. Web pack is packaged into server bundles for server side and for client side through two entrance files respectively. When the server receives a request from the client, it creates a renderer, bundleRenderer, which reads the server bundle file generated above and executes its code, then sends a generated html to the browser. After the client loads the client bundle, it hyds the DOM generated by the server. Ration (Determine whether the DOM is the same as the DOM you are about to generate, and if it is the same, mount the client's vue instance on the DOM, otherwise a warning will be raised).

Based on the above, we first build a simple vue-ssr without data requests.

How to build?

1. Create a vue instance
2. Configuring routing and corresponding view components
3. Creating Client Entry Files
4. Create Server Entry File
5. Configure webpack, sub-server packaging configuration and client packaging configuration
6. Create server-side renderer and render vue instances into html

Create a Vue instance: Create a new root Vue instance for each request. This is similar to the example where each user uses a new application in his own browser. If we use a shared instance among multiple requests, it is easy to pollute the status of cross-requests, so we should expose a factory function that can be executed repeatedly to create new application instances for each request.

//  app.js
import Vue from 'vue'
import App from './App.vue'
import createRouter  from './router'

Vue.config.productionTip = false

export function createApp () {
  const router = createRouter()
  const app = new Vue({
    // el: '#app',
    router,
    render: h => h(App)
  })
  return { app, router }
}

Configure routing and the corresponding view components: Note that, similar to createApp, we also need to give each request a new router instance, so the file exports a createRouter function.

// router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import home from '@/components/home'
import about from '@/components/about'

Vue.use(Router)

export default function createRouter() {
  return new Router({
    mode:'history',
    routes: [
      {
        path: '/',
        name: 'home',
        component: home
      },
      {
        path: '/about',
        name: 'about',
        component: about
      }
    ]
  })
}

Create the client entry file: entry-client.js client entry simply creates the application and mounts it into the DOM

import { createApp } from './app'

// Client-specific boot logic...

const { app, router } = createApp()

// This assumes that the root element in the App.vue template has `id= `app'.
router.onReady(() => {
  app.$mount('#app')
})

Create server-side entry file: implement server-side routing logic in entry-server.js

import { createApp } from './app'

export default (context) => {
  // Because it may be an asynchronous routing hook function or component, we will return a Promise.
  // So that the server can wait for all the content before rendering.
  // It's ready.
  return new Promise((resolve, reject) => {
    const { app, router } = createApp(context)

    const { url } = context
    const { fullPath } = router.resolve(url).route
    if (fullPath !== url) {
      return reject({ url: fullPath })
    }
    // Setting the location of server-side router
    router.push(context.url)
    // Wait until router parses the possible asynchronous components and hook functions
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      if (!matchedComponents.length) {
        return reject({ code: 404 })
      }
      // Promise should resolve the application instance so that it can render
      resolve(app)

    }, reject)
  })
}

After the creation, our project can not start yet. We need to configure webpack to generate server bundle s for service side and client bundle s for client side.

Client bundles are easy to create. We introduce const vueSSRClient = require('vue-server-renderer/client-plugin') in webpack.dev.conf.js under the build folder under vue-cli, and then create a new vueSSRClient() in the place where plugins are configured below. At this time, our client bundle s have been quietly generated.

The server bundle on the server side requires us to create a new webpack.server.conf.js under the build folder, which is configured as follows

const webpack = require('webpack')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const nodeExternals = require('webpack-node-externals')

module.exports = merge(baseWebpackConfig,{
    entry: './src/entry-server.js',
    devtool:'source-map',
    target:'node',
    output:{
        filename:'server-bundle.js',
        libraryTarget:'commonjs2'
    },
    externals: [nodeExternals({
        // do not externalize CSS files in case we need to import it from a dep
        whitelist: /\.css$/
    })],
    plugins:[
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
            'process.env.VUE_ENV': '"server"'
          }),

        new VueSSRServerPlugin()
    ]
})

Now we can generate server bundles for server and client bundle s for client through webpack, but we need to create a renderer bundleRenderer, which reads the server bundle file generated above and executes its code, then sends a generated html to browser, so we build the folder. Next, create a new dev-server.js file, which reads as follows (note)

// dev-server.js

const webpack = require('webpack')
const baseWebpackConfig = require('./webpack.server.conf')
const clientConfig = require('./webpack.base.conf')
const fs = require('fs')
const path = require('path')
// Read Memory Files
const Mfs = require('memory-fs')
const axios = require('axios')

module.exports = (cb) => {
    // Used to read memory files
    var mfs = new Mfs()
    const webpackComplier = webpack(baseWebpackConfig)
    webpackComplier.outputFileSystem = mfs
    const readFile = (fs, file) => {
        try {
          return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8')
        } catch (e) {}
    }


    webpackComplier.watch({},async (err,stats) => {
        if(err) {
            return console.log(err)
        }
        stats = stats.toJson();
        stats.errors.forEach(err => {console.log(err)});
        stats.warnings.forEach(err => {console.log(err)});
        // The default name of the JSON file for getting the server bundle generated by vue-server-renderer/server-plugin is vue-ssr-server-bundle.json
        let serverBundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'))
        // Get the JSON file of the client bundle generated by vue-server-renderer/client-plugin with the default name vue-ssr-client-manifest.json
        let clientBundle =  await axios.get('http://localhost:8080/vue-ssr-client-manifest.json')
        // Obtain the template file and notice that the template file contains <!--vue-ssr-outlet-->.
        let template = fs.readFileSync(path.join(__dirname,'..','index.html'), 'utf-8')
        cb(serverBundle, clientBundle, template)
    })
}

Now it's time to call our dev-server, use bundleRenderer to generate our html, and return to the page. Then we create a new server.js in the root directory, integrate all resources, and make it the entrance of our client.

// server.js
const devServer = require('./build/dev-server.js')
const server = require('express')()
const { createBundleRenderer } = require('vue-server-renderer')
// In the server processing function...
server.get('*', (req, res) => {
  const context = { url: req.url }
    res.status(200)
    devServer((serverBundle, clientBundle, template) => {
        let renderer = createBundleRenderer(serverBundle, {
            runInNewContext: false, // Recommend
            template, // (optional) page template
            clientManifest:clientBundle.data // (optional) client build manifest
        })

        renderer.renderToString(context,(err,html) => {
            res.send(html)
        })

    })

})

const port = process.env.PORT || 5001;
server.listen(port, () => {
    console.log(`server started at localhost:${port}`)
    console.log('Startup success')
})

Finally, our project catalogue is as follows:


Project directory

In order to start our project on the server side, add a line of code "server" to the scripts of package.json: "node server. js"
Now, we run npm run dev to start the client rendering, run npm run server to start the server rendering, the front page rendering is the same, but when you look at the page source code, you will find the difference.
npm run dev starts port 8080 by default

8080.gif

View client source code

Screen snapshot 2018-08-28 PM 10.39.02.png

npm run server starts port 5001 by default

5001.gif

Looking at the source code on the server side, there's an obvious data-server-rendered="true"

Screen snapshot 2018-08-28 PM 10.39.16.png

Now that our simple case is complete, we will eventually integrate vuex to dynamically display the data, so let's drop the set.

Code words are not easy, you are welcome to appreciate them and cherish them.

Posted by kcomer on Wed, 26 Dec 2018 14:18:06 -0800