In order to solve seo problems of VUE project, server side rendering was studied recently, so there is a record of this article.

The project structure

├ ─. Babelrc / / Babel configuration file ├ ─ but the template. The HTML / / HTML template file ├ ─ for server js / / server rendering and API service ├ ─ SRC / / front-end code | ├ ─ app. Js / / Is mainly used to create the vue instance | ├ ─ App. Vue / / root component | ├ ─ entry - client. Js / / client to render the entry file | ├ ─ entry - server. Js / / rendering of a service entrance file | ├ ─ stores / / vuex related | ├ ─ routes / / vue - the router related | ├ ─ components / / component ├ ─ dist / / code compilation target path ├ ─ the build / / webpack configuration fileCopy the code

The main directory structure of the project is shown above, where package.json is shown in the project. The website has a clear explanation of why Vuex is used. Examples are provided below to help you understand further.

Let’s leave the server-side rendering behind and set up a simple vue development environment.

Set up the VUE development environment

Webpack can be used to build a simple VUE development environment very quickly and can be directly taken to the elevator.

For efficient development, the VUE development environment should have code hot loading and request forwarding capabilities. All of these can be easily implemented using webpack-dev-server by configuring the devServer TAB for Webpack:

module.exports = merge(baseWebpackConfig, {
  devServer: {
    historyApiFallback: true.noInfo: true.overlay: true.proxy: config.proxy
  },
  devtool: '#eval-source-map'.plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html'.template: 'index.template.html'.inject: true // Insert CSS and js
    }),
    new webpack.HotModuleReplacementPlugin(),
    new FriendlyErrors()
  ]
})
Copy the code

Then add the –hot parameter to boot:

cross-env NODE_ENV=development webpack-dev-server --config build/webpack.dev.conf.js --open --hot
Copy the code

Note that both router and Store, as well as Vue, use factory functions to generate instances. This is to make it easier for the code to be reused later in server rendering, since “Node.js server is a long-running process. A new Vue instance must be created for each request “(official website).

Again, the front-end request uses the AXIos library to take care of the server.

Run NPM Run Server in the root directory of the project to start the back-end API service, then run NPM run dev, and WebPack will automatically open the http://localhost:8080 address in the default browser to see the effect.

Server side rendering

It’s easy to build server-side renderings based on the above projects, so let’s get started. Or just look at the final code.

To implement server-side rendering, simply add the following WebPack configuration:

module.exports = merge(baseWebpackConfig, {
  entry: './src/entry-server.js'.// Tell the VUe-loader to transport server-oriented code.
  target: 'node'.output: {
    filename: 'server-bundle.js'.libraryTarget: 'commonjs2',},plugins: [
     new VueSSRServerPlugin()
  ]
})
Copy the code

Note that the entry file path is different from the previous one. Here we use the entry file specially prepared for server rendering:

import { createApp } from './app'
// The context is passed in when the server renders the template
export default context => {
  // Since it might be an asynchronous routing hook function or component, we'll return a Promise,
  // So that the server can wait for all the content before rendering,
  // We are ready.
  return new Promise((resolve, reject) = > {
    const { app, router, store } = createApp()

    const { url } = context
    const { fullPath } = router.resolve(url).route

    if(fullPath ! == url) {return reject({ url: fullPath })
    }

    router.push(url)

    // Wait until the router has resolved possible asynchronous components and hook functions
    router.onReady((a)= > {
      const matchedComponents = router.getMatchedComponents()
      Reject if the route cannot be matched, reject, and return 404
      if(! matchedComponents.length) {return reject({ code: 404})}// Perform asynchronous data requests in all components
      Promise.all(matchedComponents.map(({ asyncData }) = > asyncData && asyncData({
        store,
        route: router.currentRoute
      }))).then((a)= > {
        context.state = store.state
        resolve(app)
      }).catch(reject)
    }, reject)
  })
}
Copy the code

AsyncData can be confusing, but we’ll use an example later. Now, to compile, run NPM run build:server and you’ll get the vue-ssr-server-bundle.json file in the dist directory. As you can see, this file contains all chunks generated by the WebPack package and specifies the entry. Later servers will render based on this file.

Now let’s move to the server side and add some new code:

. const { createBundleRenderer } = require('vue-server-renderer')
 const bundle = require('./dist/vue-ssr-server-bundle.json')

 const renderer = createBundleRenderer(bundle, {
   template: fs.readFileSync('./index.template.html'.'utf-8')})... // Server render server.get(The '*', (req, res) => {
  const context = { url: req.originalUrl }
  renderer.renderToString(context, (err, html) => {
    if (err) {
      if (err.code === 404) {
        res.status(404).end('Page not found')}else {
        res.status(500).end('Internal Server Error')}}else {
      res.end(html)
    }
  })
})
Copy the code

There is not much new code. First, we create a Renderer object using the file generated above, then call its renderToString method and pass in the object containing the request path as a parameter to render, and finally return the rendered data, namely HTML.

Run NPM run server to start the server, open http://localhost:8081 to see the effect:

About asyncData

I mentioned asyncData earlier, so let’s use this example to sort it out. First, look at the code in the component:

. <script> export default {asyncData ({store, route}) { Returns a Promise return store.dispatch('fetchItems')}, data () {return {title: "", content: ""}}, computed: {// Get the item from the store's state object. itemList () { return this.$store.state.items } }, methods: { submit () { const {title, content} = this this.$store.dispatch('addItem', {title, content}) } } } </script>Copy the code

This is a very simple component that includes a list, the contents of which are retrieved from the back end on request, and a form for submitting new records to the back end for saving. Where asyncData is the name of the function we agreed to, which means that the render component needs to perform in advance to get the initial data, and it returns a Promise so that we can know when the operation will complete when we render in the back end. Here, the function fires fetchItems to update the state in the store. Remember our entry-server.js file, which calls the component’s asyncData method to prefetch data.

In the development phase, we also need to prefetch data. To reuse asyncData code, we call this method in the beforeMount of the component. We blend this processing logic into all components via vue. mixin:

Vue.mixin({
  beforeMount() {
    const { asyncData } = this.$options
    if(asyncData) {// Assign the fetch data operation to the Promise // so that in the component we can run 'this.dataPromise.then(...) when the data is ready. DataPromise = asyncData({store: this.$store,
        route: this.$route}}}})Copy the code

Another problem is that the HTML we generated didn’t introduce any JS, so the user couldn’t interact with anything, like the list page above, and the user couldn’t submit new content. Of course, this would be sufficient if the page was only for crawlers to “look at”, but for real users we would also need to introduce front-end rendered JS files into the HTML.

The front-end rendering

The code for this section can be viewed directly here.

The front-end rendering part needs to first add a Webpack configuration file to generate the required STATIC files such as JS and CSS:

module.exports = merge(baseWebpackConfig, {
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false,
        drop_console: true}}), // Important: This separates the WebPack runtime into a boot chunk, // so that asynchronous chunks can be properly injected later. // This also provides better caching for your application /vendor code. new webpack.optimize.CommonsChunkPlugin({ name:"manifest"MinChunks: Infinity}), // This plugin generates' vue-ssR-client-manifest.json 'in the output directory. new VueSSRClientPlugin() ] })Copy the code

At the same time, front-end rendering also needs to have its own entry file entry-client, which was mentioned in asyncData:

import Vue from 'vue'
import {
  createApp
} from './app.js'// Client-specific boot logic...... const { app, router, store } = createApp()if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}

Vue.mixin({
  beforeMount() {
    const { asyncData } = this.$options
    if(asyncData) {// Assign the fetch data operation to the Promise // so that in the component we can run 'this.dataPromise.then(...) when the data is ready. DataPromise = asyncData({store: this.$store,
        route: this.$route})}}}) // The root element in the app.vue template is assumed to have an 'id='"app"`
router.onReady(() => {
  app.$mount('#app')})Copy the code

Now we can compile NPM run build:client, dist directory can get several files:

0.js
1.js
2.js
app.js
manifest.js
vue-ssr-client-manifest.json
Copy the code

Among them, JS files are the files that need to be introduced, json file is like an explanatory document, here we will not discuss its principle, interested can see here.

Finally, in server.js, make a slight change:

 const clientManifest = require('./dist/vue-ssr-client-manifest.json')

 const renderer = createBundleRenderer(bundle, {
   template: fs.readFileSync('./index.template.html'.'utf-8'),
   clientManifest
 })
Copy the code

Then the NPM run server starts the service and opens http://localhost:8081. You can see that the rendered HTML file has introduced JS resources.

New records can now be submitted in the list page:

conclusion

This paper starts with building a simple VUE development environment, and then implements server-side rendering based on this, and introduces the resources required for client rendering. This gives you an overview of the vue server rendering process, but there are many areas that need to be further explored:

  • Style handling

    This paper does not deal with the style, which needs further study

  • Interpretation of compiled files

    How do you use json files compiled in this article?

  • Different strategies for crawlers and real users

    In fact, server-side rendering is mainly used to solve seo problems, so it can determine the source of the request header and do different processing on the server side. For crawlers, server-side rendering will be carried out (without introducing resources required by client rendering), and for ordinary users, the original client-side rendering will be used.