preface

Because their blog completely before and after the end of the separation of writing, in SEO this piece also did not consider, as a result, they began the SSR journey

Technology stack

vue2 + koa2 + webpack4 + mongodb

Since WebPack has also reached version 4.1, we have migrated WebPack 3 to WebPack 4.

Server Side Rendering (SSR)

The HTML fragment is generated on the server and returned to the client

Therefore, VUE-SSR can also be understood as converting the. Vue file we wrote in the client before into HTML fragments and returning them to the client.

In practice, of course, it can be complicated. For example, the server returns the HTML fragment, the client receives the display directly, and we can’t trigger the event (click the event, etc.) without doing anything. In order to solve the above problems. So if you render via vue-server-renderer, you’ll have a data-server-Rendered =”true” attribute attached to the root node. Let the client Vue know that this part of the HTML is rendered by Vue on the server and should be mounted in active mode

** Activation mode :** refers to the process by which the Vue takes over static HTML sent by the server on the browser side and turns it into a dynamic DOM managed by the Vue. The server has rendered the HTML, but the server has rendered the static page and cannot manipulate the DOM. But because the DOM elements are already generated, there is no need to discard and recreate them. So the client simply activates these static pages and makes them dynamic (responsive to subsequent data changes).

SSRadvantage

  • Better SEO, thanks to search engine crawler crawler tools can view fully rendered pages directly.
  • Faster time-to-content, especially for slow network conditions or slow-running devices. You don’t have to wait for all the JavaScript to download and execute before the server-rendered markup is displayed, so your users will see the fully rendered page more quickly. This generally results in a better user experience, and for applications where “time-to-content is directly related to conversion rates,” server-side rendering (SSR) is critical.

SSRDevelop issues that need attention

  • Server rendering will only be performedvueThe two hook functions ofbeforeCreatecreated
  • Server rendering is not accessiblewindowdocumentSuch as global objects that are only available to browsers. If you have globally imported plug-ins and JS files in your projectbeforeCreateandcreatedAn error will be reported if these objects are used because they do not exist on the server. If you really want to use it, you can try this pluginjsdom

Basically, as long as you know something about Node, can configure Webpack, and vue works, it’s relatively easy to implement, especially with the full HackerNews Demo on the official website, which is based on the Express framework. The use of middleware in koA needs to be changed a bit. The rest of the basic need to follow the official website example to go over the basic OK above the official website example requires terminal to access the data over the wall, if you don’t want to look at this example, with the official website example is basically the same as gold digging website

Here is also about the implementation of the official website

Project directory

SRC ├ ─ ─ components │ ├ ─ ─ Foo vue │ ├ ─ ─ Bar. The vue │ └ ─ ─ Baz. Vue ├ ─ ─ the router │ └ ─ ─ index. The js ├ ─ ─ store │ └ ─ ─ index. The js ├ ─ ─ App. Vue ├ ─ ─ App. Js# universal entry├ ─ ─ entry - client. Js# Project entry running on the client└ ─ ─ entry - server. Js# project entry running on the server
Copy the code

There are a couple of things you need to know

  • Vuex because the application relies on asynchronous data that needs to be prefetched and parsed before starting the rendering process. Therefore, vuEX will be used as the data prefetch storage container

  • AsyncData custom function (get interface data) :

    <template>
      <div>{{ item.title }}</div>
    </template>
    <script>
    exportDefault {// Custom function to get data. AsyncData ({store, route}) {// When an action is triggered, a Promise is returnedreturn store.dispatch('fetchItem', route.params.id)}, computed: {// Get an item from the state object of the store.item () {
          return this.$store.state.items[this.$route.params.id]
        }
      }
    }
    </script>
    Copy the code
  • Avoid state singletons: When writing client-only code, we tend to evaluate the code in a new context each time. However, the Node.js server is a long-running process. When our code enters the process, it takes a value and keeps it in memory. This means that if you create a singleton object, it will be shared between each incoming request. So we create a new root Vue instance for each request. Therefore, instead of creating an application instance directly, we should expose a factory function that can be executed repeatedly to create new application instances for each request:

    // router.js
    import Vue from 'vue'
    import Router from 'vue-router'
    Vue.use(Router)
    export function createRouter () {
      return new Router({
        mode: 'history', routes: [ // ... ] })}Copy the code
    // store.js
    import Vue from 'vue'
    import Vuex from 'vuex'Vue.use(Vuex) import {fetchItem} from, assuming we have a generic // API that returns promises'./api'
    export function createStore () {
      returnnew Vuex.Store({ state: { items: {} }, actions: {fetchItem ({commit}, id) {// 'store.dispatch()' returns a Promise, // so we know when the data will be updatedreturn fetchItem(id).then(item => {
              commit('setItem', { id, item })
            })
          }
        },
        mutations: {
          setItem (state, { id, item }) {
            Vue.set(state.items, id, item)
          }
        }
      })
    }
    Copy the code
    // app.js
    import Vue from 'vue'
    import App from './App.vue'
    import { createRouter } from './router'
    import { createStore } from './store'
    export function createApp// createRouter and store instances const router = createRouter() const store = createStore() Render router and store const app = new Vue({router, store, render: h => h(app)}) // Expose router, store and storereturn { app, router, store }
    }
    Copy the code
    import {createApp} from './app'
    const {app, router, store} = createApp()
    Copy the code

    Follow the steps above to create a new application instance for each request and there will be no cross-request state pollution caused by multiple requests

Implementation steps

  1. First, get the path of the current access becauserenderToStringRendering objects that are passed in a context are supported, so we pass in a context object that contains the currenturl
```
// server.js 
const context = {
    url: ctx.url
}
renderer.renderToString(context, (err, html) => {
    if (err) {
        return reject(err)
    }
    console.log(html)
})
```
Copy the code
  1. And then the middle through webpack configuration, so that the server side of the project entryentry-server.jsTo receive the context
// entry-server.js import {createApp} from './app' export default Context => {// Because there may be asynchronous routing hook functions or components, So we'll return a Promise. Return new Promise((resolve, reject) => {const {app, router, Router.push (url) // Wait until the router has resolved possible asynchronous components and hook functions Router. OnReady (() = > {/ / get the path of the current component const matchedComponents. = the router getMatchedComponents () / / no return 404 if (! matchedComponents.length) { return reject({ code: 404})} // If the path exists and the interface needs to be called to prefetch data, wait until all 'asyncData' functions are completed. // 'asyncData' functions are component custom static functions used to fetch data in advance. Promise.all(matchedComponents.map( ({asyncData}) => asyncData && asyncData({ store, route: Router.currentroute}))).then(() => {// After the execution, because the obtained data is stored in vuEX, The method executed in 'asyncData' above is to call vuex's action, // So we assign the state in store to 'context.state' // and then 'renderToString' will parse the HTML The data in 'context.state' is embedded in the HTML variable 'window.__initial_state__' // so that when we get to the client, State = store.state resolve(app) Context. State = store.state resolve(app) }).catch(reject) }) }) } ```Copy the code
  1. The above returns the component parsing of our current access path to the client. The client activates the static HTML because the server generates the HTML to get the data throughasyncDataFunction, but we only need to render the first request server, and then do not need to render when the page switch, but the interface call is put againasyncDataFunction, so when the page switch, our customers need to deal withasyncDataFunction, we used to put data increatedIn the hook functionasyncDataInside, so we need to execute it when we do the client switch. To get the data
``` import {createApp} from './app' const {app, router, Store} = createApp() // Replace state in store with data in window.__initial_state__ if (window.__initial_state__) { Store. ReplaceState (window.__initial_state__)} router.onReady(() => {// Add a route hook function to handle asyncData. // So that we do not double-fetch the existing data. // Use 'router.beforeresolve ()' to ensure that all asynchronous components are resolved. router.beforeResolve((to, from, next) => { const matched = router.getMatchedComponents(to) const prevMatched = router.getMatchedComponents(from) // We only care about components that haven't been rendered before // so we compare them, Let diffed = false const activated = matched. Filter ((c, i) => { return diffed || (diffed = (prevMatched[i] ! == c)) }) const asyncDataHooks = activated.map(c => c.asyncData).filter(_ => _) if (! Asyncdatalinks.length) {return next()} // If there are loading indicators, All (asyncDataHooks. Map (hook => hook({store, route: To}))). Then (() = > {/ / stop loading indicator (loading indicator) next ()}). The catch (next)}) / / mounted to the root node app. $mount (' # app ')}) ` ` `Copy the code

Basically this has realized vuE-SSR process, the specific source code and configuration can be viewed in my Github.

webpack4

The most obvious point is that webpack4 now has default values. A simple configuration can use the following defaults:

  • The default value for entry is./ SRC
  • The default value for output.path is./dist
  • The default value of mode is production
  • The UglifyJs plugin enables Caches and parallizes by default

In Develoment mode:

  • Open the output. The pathinfo
  • Close the optimization. Minimize

When mode is production:

  • Close – in the memory caching
  • Open NoEmitOnErrorsPlugin
  • Open ModuleConcatenationPlugin
  • Open optimization. Minimize

Since making SSR notifications to your blog also upgrades Webpack, let’s take a look at some of the WebPack configurations that need to be changed to migrate to WebPack 4

  1. To move the CLI to webpack-CLI, you need to install Webpack-CLI

  2. The current mode is determined by setting the mode variable

  • On the cliwebpack --mode development
  • Configuration in file
``` module.exports = { mode: 'development', entry: { app: resolve('src') }, ... ` ` `Copy the code
  1. webpack.optimize.CommonsChunkPlugin has been removed, please use config.optimization.splitChunks instead webpack4No longer providewebpack.optimize.CommonsChunkPluginTo split the code, you need new attributesoptimization.splitChunks
``` output: { filename: assetsPath('js/[name].[chunkhash].min.js'), }, optimization: { runtimeChunk: { name: "Manifest"}, splitChunks: {chunks: "initial". / / must choose three: "initial" | "all" (the default is all) | "async" minSize: MaxAsyncRequests: 1, // Maximum asynchronous requests, default 1 maxInitialRequests: Function cacheGroups: {// here we start setting chunks priority for caching: "0", / / the cache group priority false | object | vendor: {/ / the key for the entry name chunks of entry definition: "Initial". / / must choose three: "initial" | | "all" "async" (the default is asynchronous) test: / react | lodash /, / / verify the regular rules, if conform to extract the chunk name: "Vendor ", // Names of chunks to cache minSize: 0, minChunks: 1, Enforce: true, maxAsyncRequests: 1, // Maximum number of asynchronous requests, default 1 maxInitialRequests: 1, // Maximum number of initial requests, default 1 reuseExistingChunk: True // You can set whether to reuse the chunk (check source code does not find the default value)}}}},... ` ` `Copy the code
  1. compilation.mainTemplate.applyPluginsWaterfall is not a function
'yarn add webpack-contrib/html-webpack-plugin -dCopy the code
  1. Use Chunks.groupsIterable and filter by instanceof Entrypoint instead:
Solution: 'yarn add extract-text-webpack-plugin@next -d'Copy the code

Upgrading webpack4 also encountered several problems

  1. Set up the optimization.splitChunks package. Will package js, CSS each copy, do not know what situation.

  2. After upgrade 4, I used DllPlugin package, but Verdon package is still the same size and does not extract the module I specified.

  3. Import loading on demand does not seem to work. Such as: Const _import_ = file => () => import(file + ‘.vue’) is loaded directly on demand via _import_(‘components/Foo’), but webPack4 does not work, It was all loaded in one go.

Above are some problems we encountered in upgrade 4. Maybe I made a mistake in configuration, but webpack4 was normal before. The specific configuration of my side is put on Github.

conclusion

Above is my personal blog SSR journey.

github

Blog address