Vue.js 状态管理及 SSR解析

目录
  • 前端状态管理出现的意义及解决的问题
  • Vuex 源码解读
    • Vuex 公共方法
  • Vuex 介绍及深入使用
  • Vuex 使用(官网)
    • 1、基本框架
    • 2、基本使用
    • 3、State
      • 3.1 mapState 辅助函数
    • 4、Getter
      • 4.1 通过属性访问
      • 4.2 通过方法访问
      • 4.3 mapGetters 辅助函数
    • 5、Mutation
      • 5.1 提交载荷(Payload)
      • 5.2 使用常量替代 Mutation 事件类型
      • 5.3 Mutation 必须是同步函数
      • 5.4 在组件中提交 Mutation
    • 6、Action
    • 7、分发 Action
      • 7.1 在组件中分发 Action
      • 7.2 组合 Action
    • 8、严格模式
      • 8.1 开发环境与发布环境
  • vue.js 服务端渲染介绍
    • 1、客户端渲染和服务端渲染
      • 客户端渲染
    • 2、服务端渲染
      • 客户端路由
    • 3、服务端渲染实例
    • 3、同构 - 客户端渲染和服务端渲染

前端状态管理出现的意义及解决的问题

随着前端应用的逐步复杂,我们的组件中需要使用越来越多的状态。有的时候我们需要使用子组件将状态传递给父组件就会比较复杂,数据的向上传递过程我们可能会使用回调函数或是数据绑定的形式去处理,就会让代码晦涩难懂。

我们需要一种方式,能够让数据在所有组件中共享,同时能以简单的方式进行传递,这种组织数据的方式就是状态管理。我们很自然的就想到,把数据放到所有需要使用的组件的公共祖先上,在使用时自上而下传递即可。

在 vue.js 中,我们主要说的状态管理库就是 vuex,当然,只要你能实现有条理的组织数据,那么它都可以认为是一种状态管理库。

事实上,我们可以简单的这样理解【状态管理】这个词,vuex 实际上做的事情就是:

  • 在顶层实现一个数据管理的仓库 store,将所有组件间需要共享的数据放置于此;
  • 同时组件也可以对这个 store 内的数据进行更新,同时更新完之后响应式更新所有使用此数据组件的视图;

Vuex 源码解读

Vuex 公共方法

  • 路径:src\util.js
export function find(list, f) {
  return list.filter(f)[0];
}

export function deepCopy(obj, cache = []) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  const hit = find(cache, c => c.original === obj);
  if (hit) {
    return hit.copy;
  }

  const copy = Array.isArray(obj) ? [] : {};
  cache.push({
    original: obj,
    copy,
  });

  Object.keys(obj).forEach(key => {
    copy[key] = deepCopy(obj[key], cache);
  });

  return copy;
}

export function forEachValue(obj, fn) {
  Object.keys(obj).forEach(key => fn(obj[key], key));
}

export function isObject(obj) {
  return obj !== null && typeof obj === 'object';
}

export function isPromise(val) {
  return val && typeof val.then === 'function';
}

export function assert(condition, msg) {
  if (!condition) throw new Error(`[vuex] ${msg}`);
}

export function partial(fn, arg) {
  return function () {
    return fn(arg);
  };
}

Vuex 介绍及深入使用

在说 vuex 之前,我们必须说一说 flux 架构,flux 架构可谓是状态管理的鼻祖。

flux 架构最早由 facebook 推出,主要是为了处理当时他们的 react 框架下状态管理的问题,但在当时来讲,整个设计比较复杂,后来人们简化了其中的一些理念,但是保留了核心思想,继而依据框架实现了很多不同的状态管理库,例如 reduxvuex 等等。其中 redux 大多数被用在了 react 项目中,而 vuex 就是在 vue 框架中实现的这么一个 flux 架构的状态管理库。

**flux 架构约定,存放数据的地方称为 storestore 内部的 state 是数据本身,我们必须通过 action 才能修改 store 里的 state。**这里的 action 指的意思是 行为,在大部分实现里面是一个函数,通过调用函数来更改 store 内部的 state

vuex 中,我们可以通过 mutation 来【同步】的改变 state,这时候就可以在组件中通过 commit 进行调用更改 state

同样的,我们也可以通过 action 来【异步】更改 state,不过在 action 中,我们还是需要调用 mutation

Vuex 使用(官网)

网址链接:vuex.vuejs.org/zh/guide/st…

1、基本框架

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {},
  mutations: {},
  actions: {},
  modules: {},
});

2、基本使用

  • ./store/index.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    count: 0,
  },
  mutations: {
    increment(state, payload = 1) {
      state.count += payload;
    },
  },
  actions: {},
  modules: {},
});
  • ./Home.vue
<template>
  <div class="home">
    <div>count: {{ count }}</div>
    <button @click="increment">增加</button>
    <button @class="decrement">减少</button>
  </div>
</template>

<script>
export default {
  name: 'Home',
  computed: {
    count() {
      return this.$store.state.count;
    },
  },
  methods: {
    increment() {
      // 使用 commit 派发 mutation 事件
      // this.$store.commit("increment");
      // 可以传递参数
      this.$store.commit('increment', 2);
    },
    decrement() {},
  },
};
</script>

3、State

可以使用计算属性获取 state 中的数据:

computed: {
  count () {
    return this.$store.state.count
  }
}

3.1 mapState 辅助函数

当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性。

import { mapState } from "vuex";

computed: {
  ...mapState(["num"])
}

4、Getter

Vuex 允许我们在 store 中定义 getter(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

Getter 接受 state 作为其第一个参数:

const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: 'foo', done: true },
      { id: 2, text: 'bar', done: false },
    ],
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done);
    },
  },
});

4.1 通过属性访问

Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值:

store.getters.doneTodos; // -> [{ id: 1, text: '...', done: true }]

Getter 也可以接受其他 getter 作为第二个参数:

getters: {
  // ...
  doneTodosCount: (state, getters) => {
    return getters.doneTodos.length;
  };
}

4.2 通过方法访问

你也可以通过让 getter 返回一个函数,来实现给 getter 传参。

getters: {
  // ...
  getTodoById: state => id => {
    return state.todos.find(todo => todo.id === id);
  };
}
store.getters.getTodoById(2); // -> { id: 2, text: '...', done: false }

【注意】:getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果

4.3 mapGetters 辅助函数

mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:

import { mapGetters } from 'vuex';

export default {
  computed: {
    // 使用对象展开运算符将 getter 混入 computed 对象中
    ...mapGetters(['doneTodosCount', 'anotherGetter']),
  },
};

如果你想将一个 getter 属性另取一个名字,使用对象形式:

...mapGetters({
  // 把 this.doneCount 映射为 this.$store.getters.doneTodosCount
  doneCount: 'doneTodosCount'
})

5、Mutation

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:

const store = new Vuex.Store({
  state: {
    count: 1,
  },
  mutations: {
    increment(state) {
      // 变更状态
      state.count++;
    },
  },
});

你不能直接调用一个 mutation handler。这个选项更像是事件注册:“当触发一个类型为 increment 的 mutation 时,调用此函数。”要唤醒一个 mutation handler,你需要以相应的 type 调用 store.commit 方法:

store.commit('increment');

5.1 提交载荷(Payload)

你可以向 store.commit 传入额外的参数,即 mutation 的 载荷(payload

mutations: {
  increment (state, n) {
    state.count += n
  }
}

// 使用方法
store.commit('increment', 10)

在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:

mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}

// 使用方法
store.commit('increment', {
  amount: 10
})

对象风格的提交方式:

提交 mutation 的另一种方式是直接使用包含 type 属性的对象:

store.commit({
  type: 'increment',
  amount: 10,
});

当使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数,因此 handler 保持不变:

mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}

5.2 使用常量替代 Mutation 事件类型

使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式。这样可以使 linter 之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码合作者对整个 app 包含的 mutation 一目了然:

// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION';
// store.js
import Vuex from 'vuex'
import { SOME_MUTATION } from './mutation-types'

const store = new Vuex.Store({
  state: { ... },
  mutations: {
    // 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
    [SOME_MUTATION] (state) {
      // mutate state
    }
  }
})

5.3 Mutation 必须是同步函数

一条重要的原则就是要记住 mutation 必须是同步函数。为什么?请参考下面的例子:

mutations: {
  someMutation (state) {
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}

现在想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用 —— 实质上任何在回调函数中进行的状态的改变都是不可追踪的。

5.4 在组件中提交 Mutation

你可以在组件中使用 this.$store.commit('xxx') 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)。

import { mapMutations } from 'vuex';

export default {
  // ...
  methods: {
    ...mapMutations([
      'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`

      // `mapMutations` 也支持载荷:
      'incrementBy', // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
    ]),
    ...mapMutations({
      add: 'increment', // 将 `this.add()` 映射为 `this.$store.commit('increment')`
    }),
  },
};

6、Action

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。

让我们来注册一个简单的 action

const store = new Vuex.Store({
  state: {
    count: 0,
  },
  mutations: {
    increment(state) {
      state.count++;
    },
  },
  actions: {
    increment(context) {
      context.commit('increment');
    },
  },
});

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。当我们在之后介绍到 Modules 时,你就知道 context 对象为什么不是 store 实例本身了。

实践中,我们会经常用到 ES2015 的参数解构来简化代码(特别是我们需要调用 commit 很多次的时候):

actions: {
  increment ({ commit, state, getters }) {
    commit('increment')
  }
}

7、分发 Action

Action 通过 store.dispatch 方法触发:

store.dispatch('increment');

乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作:

actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}

Actions 支持同样的载荷方式和对象方式进行分发:

// 以载荷形式分发
store.dispatch('incrementAsync', {
  amount: 10,
});

// 以对象形式分发
store.dispatch({
  type: 'incrementAsync',
  amount: 10,
});

7.1 在组件中分发 Action

你在组件中使用 this.$store.dispatch('xxx') 分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store):

import { mapActions } from 'vuex';

export default {
  // ...
  methods: {
    ...mapActions([
      'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`

      // `mapActions` 也支持载荷:
      'incrementBy', // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
    ]),
    ...mapActions({
      add: 'increment', // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
    }),
  },
};

7.2 组合 Action

Action 通常是 异步 的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?

首先,你需要明白 store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch 仍旧返回 Promise

actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  }
}

现在你可以:

store.dispatch('actionA').then(() => {
  // ...
});

在另外一个 action 中也可以:

actions: {
  // ...
  actionB ({ dispatch, commit }) {
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}

最后,如果我们利用 async / await,我们可以如下组合 action

// 假设 getData() 和 getOtherData() 返回的是 Promise
actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}

一个 store.dispatch 在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。

8、严格模式

开启严格模式,仅需在创建 store 的时候传入 strict: true

const store = new Vuex.Store({
  // ...
  strict: true,
});

在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误(但是数据还是会改变)。这能保证所有的状态变更都能被调试工具跟踪到。

8.1 开发环境与发布环境

不要在发布环境下启用严格模式! 严格模式会深度监测状态树来检测不合规的状态变更 —— 请确保在发布环境下关闭严格模式,以避免性能损失。

类似于插件,我们可以让构建工具来处理这种情况:

const store = new Vuex.Store({
  // ...
  strict: process.env.NODE_ENV !== 'production',
});

vue.js 服务端渲染介绍

大多数我们使用的 UI 框架如 vue 和 react,都是在客户端进行渲染,也就是说每个用户在加载进来我们所有的 html 文件和 js 文件之后,才开始渲染页面的内容。

但是这样做会有两个问题,一个是如果用户网络速度比较慢,如果我们渲染的内容比较多的话,就会产生一个延迟,造成不好的用户体验。另一个是某些爬虫,例如百度的搜索收录的爬虫,在爬取你的页面时,获取不到你的页面的真实内容,导致站点 SEO 权重变低。

所以很多需要 SEO 的页面,都需要在服务端提前渲染好 html 的内容,在用户访问时先返回给用户内容,这杨对用户和爬虫都非常友好。

我们可以通过直接在页面上右击查看网页源代码,来查看一个页面是否有服务端渲染。

1、客户端渲染和服务端渲染

客户端渲染

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>客户端渲染</title>
  </head>
  <body>
    <script>
      document.body.innerHTML = '<div>你好</div>';
    </script>
  </body>
</html>

在 Network 的中 Preview 中无数据,在 Response 中的没有 DOM 标签:

查看网页源代码:

2、服务端渲染

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>服务端渲染</title>
  </head>
  <body>
    <div>你好</div>
  </body>
</html>

在 Network 的中 Preview 中有数据,在 Response 中的有 DOM 标签:

查看网页源代码:

客户端路由

在控制台中可以看到,切换路由的时候并没有发起 ajax 请求。

3、服务端渲染实例

vue.js 的服务端渲染非常简单,我们只需要在 node.js 中通过 vue-server-renderer 模块,调用对应服务端渲染的渲染器对组件渲染即可,他就会生成组件对应的 html 内容。渲染成功的 html 标签,我们可以直接返回到客户端作为初始请求 html 的返回值。

  • 安装依赖
yarn add express vue vue-server-renderer
  • ./index.js
const Vue = require('vue');
const createRenderer = require('vue-server-renderer').createRenderer;

const vm = new Vue({
  data() {
    return {
      count: 100,
    };
  },
  template: `<div>{{ count }}</div>`,
});

const renderer = createRenderer();

renderer.renderToString(vm, (err, html) => {
  console.log('html ==========', html); // <div data-server-rendered="true">100</div>
});
  • ./index.js
const Vue = require('vue');
const createRenderer = require('vue-server-renderer').createRenderer;
const express = require('express');
const fs = require('fs');
const path = require('path');

const app = express();

// ! 服务端路由
app.get('*', function (req, res) {
  const vm = new Vue({
    data() {
      return {
        url: `服务端路由 ${req.url}`,
        count: 100,
      };
    },
    template: `<div>{{ url }} - {{ count }}</div>`,
  });

  const renderer = createRenderer({
    // 设置模板
    template: fs.readFileSync(path.resolve(__dirname, './index.template.html'), 'utf-8'),
  });

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

const PORT = 8080;
app.listen(PORT, () => {
  console.log(`服务器启动在 ${PORT} 端口`);
});
  • ./index.template.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>服务端渲染Demo</title>
  </head>
  <body>
    <h1>这里是模板</h1>
    <!--! 中间不能带空格 -->
    <!--vue-ssr-outlet-->
  </body>
</html>
  • 执行 index.js 文件:node ./index.js

我们需要注意的一点是,在服务端渲染组件,我们使用不了 windowlocation 等浏览器环境中的对象,所以如果组件内部使用了这种内容会报错。

同时,在服务端渲染时我们要注意,组件的生命周期也会只执行 beforeCreate 和 created 这两个,所以在此声明周期里面不能使用 window,但是可以在其他声明周期比如 mounted 中使用。还有渲染的数据,对于服务端渲染的组件来说,我们不应该发请求获取组件数据,而是应该直接渲染时使用数据进行渲染。

路由也是如此,在 vue 客户端使用路由的时候,我们也需要在服务端对路由进行匹配,从而得知具体需要渲染的组件是哪个。

3、同构 - 客户端渲染和服务端渲染

参考文档(Vue 文档中 - Vue 服务端渲染):ssr.vuejs.org/zh/

  • ./webpack.config.js
/*
  客户端 webpack 配置
*/
const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: './src/entry-client.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    publicPath: '/dist/',
    filename: 'build.js',
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: ['*', '.js', '.vue'],
  },
};
  • ./webpack.server.config.js
/*
  服务端 webpack 配置
*/
const path = require('path');
const webpack = require('webpack');

const merge = require('webpack-merge');
const baseWebpackConfig = require('./webpack.config');

const config = merge(baseWebpackConfig, {
  target: 'node',
  entry: {
    app: './src/entry-server.js',
  },
  output: {
    path: __dirname,
    filename: 'server.bundle.js',
    libraryTarget: 'commonjs2',
  },
});

console.log('config ============ ', config);
module.exports = config;
  • ./package.json
{
  "name": "06",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "dependencies": {
    "babel-core": "^6.26.3",
    "babel-loader": "^7.1.5",
    "babel-preset-env": "^1.7.0",
    "babel-preset-stage-3": "^6.24.1",
    "express": "^4.17.1",
    "vue": "^2.6.11",
    "vue-router": "^3.3.2",
    "vue-server-renderer": "^2.6.11",
    "vuex": "^3.4.0",
    "webpack": "^3.12.0",
    "webpack-merge": "^4.2.2"
  },
  "devDependencies": {
    "vue-loader": "^13.7.3",
    "vue-template-compiler": "^2.6.11"
  },
  "scripts": {
    "build-server": "webpack --config webpack.server.config.js",
    "build-client": "webpack --config webpack.config.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
  • ./src/App.vue
<template>
  <div>
    <h1>this is App.vue</h1>

    <router-link to="/">root</router-link>
    <router-link to="/about">about</router-link>
    <router-view></router-view>
  </div>
</template>
  • ./src/Home.vue
<template>
  <div>Home {{ $store.state.timestamp }}</div>
</template>
  • ./src/About.vue
<template>
  <div>about {{ $store.state.timestamp }}</div>
</template>
  • ./index.js
/*
  entry-client 和 entry-server 共同的文件
*/
import Vue from 'vue';
import Router from 'vue-router';
import Vuex from 'vuex';
import Home from './Home';
import About from './About';
import App from './App';

Vue.use(Router);
Vue.use(Vuex);

export function createApp() {
  const store = new Vuex.Store({
    state: {
      timestamp: new Date().getTime(),
    },
  });

  if (typeof window !== 'undefined' && window.store) {
    store.replaceState(window.store);
  }

  const router = new Router({
    mode: 'history',
    routes: [
      { path: '/', component: Home },
      { path: '/about', component: About },
    ],
  });

  const vm = new Vue({
    router,
    store,
    render: h => h(App),
  });

  return { vm, router, store };
}
  • ./src/entry-server.js 第一种
/*
  服务端渲染 - 入口
*/
const express = require('express');
const fs = require('fs');
const path = require('path');
const renderer = require('vue-server-renderer').createRenderer();

const { createApp } = require('./index');

const app = express();

app.use('/dist', express.static(path.join(__dirname, './dist')));

app.get('/build.js', function (req, res) {
  const pathUrl = path.resolve(process.cwd(), './dist/build.js');
  console.log(pathUrl);
  res.sendFile(pathUrl);
});

app.get('*', function (req, res) {
  const url = req.url;
  const { vm, router } = createApp();
  router.push(url);

  /*
    const matchedComponents: Array<Component> = router.getMatchedComponents(location?)
    返回目标位置或是当前路由匹配的组件数组 (是数组的定义/构造类,不是实例)。通常在服务端渲染的数据预加载时使用。
  */
  const matchedComponent = router.getMatchedComponents();
  if (!matchedComponent) {
    // 404 处理
  } else {
    renderer.renderToString(vm, function (err, html) {
      res.send(html);
    });
  }
});

const PORT = 8080;
app.listen(PORT, () => {
  console.log(`服务器启动在 ${PORT} 端口`);
});

/*
  此时可以执行 yarn build-server 编译 entry-server 文件,生成 server.bundle.js
  执行 node ./server.bundle.js 查看服务端路由的结果
*/
  • ./src/entry-server.js 第二种
/*
  服务端渲染 - 入口
*/
const express = require('express');
const fs = require('fs');
const path = require('path');
const renderer = require('vue-server-renderer').createRenderer({
  template: fs.readFileSync(path.resolve(process.cwd(), './index.template.html'), 'utf-8'),
});

const { createApp } = require('./index');

const app = express();

app.use('/dist', express.static(path.join(__dirname, './dist')));

app.get('/build.js', function (req, res) {
  const pathUrl = path.resolve(process.cwd(), './dist/build.js');
  console.log(pathUrl);
  res.sendFile(pathUrl);
});

app.get('*', function (req, res) {
  const url = req.url;
  const { vm, router, store } = createApp();
  router.push(url);

  /*
    const matchedComponents: Array<Component> = router.getMatchedComponents(location?)
    返回目标位置或是当前路由匹配的组件数组 (是数组的定义/构造类,不是实例)。通常在服务端渲染的数据预加载时使用。
  */
  const matchedComponent = router.getMatchedComponents();
  if (!matchedComponent) {
    // 404 处理
  } else {
    renderer.renderToString(vm, function (err, html) {
      res.send(html);
    });
  }
});

const PORT = 8080;
app.listen(PORT, () => {
  console.log(`服务器启动在 ${PORT} 端口`);
});
  • ./src/entry-client.js
/*
  客户端渲染 - 入口
*/
import { createApp } from './index';

const { vm } = createApp();

vm.$mount('#app');
  • ./index.template.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <h1>这里是模板</h1>
      <!--vue-ssr-outlet-->
    </div>
    <script src="/build.js"></script>
  </body>
</html>

执行 yarn build-client 编译客户端;

执行 yarn build-server 编译服务端;

执行 node ./server.bundle.js 启动服务器,打开浏览器输入网址 http://localhost:8080/

到此这篇关于Vue.js 状态管理及 SSR解析的文章就介绍到这了,更多相关Vue.js SSR内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Vue使用预渲染代替SSR的方法

    页面渲染方式 前段时间了解到页面有几种渲染方式:SPA SSR,以前写的一个网站但是用的渲染方式是 SPA,导致搜索引擎爬虫抓不到任何信息,对 SEO 优化不很好,本想改成 SSR,但是改动配置很多,弄来弄去怕影响开发,今天在 Vue 官网看到预渲染,今天试了下,感觉是一个折中的方法,而且配置改动不大,大家可以试试 什么是服务器端渲染 (SSR)? Vue.js 是构建客户端应用程序的框架.默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM.然而,也可以将同一个组件渲

  • Egg Vue SSR 服务端渲染数据请求与asyncData

    服务端渲染 Node 层直接获取数据 在 Egg 项目如果使用模板引擎规范时通是过 render 方法进行模板渲染,render 的第一个参数模板路径,第二个参数时模板渲染数据. 如如下调用方式: async index(ctx) { // 获取数据,可以是从数据库,后端 Http 接口 等形式 const list = ctx.service.article.getArtilceList(); // 对模板进行渲染,这里的 index.js 是 vue 文件通过 Webpack 构建的 JSB

  • vue中vue-router的使用说明(包括在ssr中的使用)

    目录 安装vue-router 创建配置文件 路由映射规则配置 路由设置内容 入口文件配置 app.vue配置 router中使用props 其他配置属性 导航守卫 vue笔记之vue-router的使用(包括ssr中的使用) 安装vue-router 命令行执行: npm i vue-router -S 创建配置文件 在项目src文件夹下创建config文件夹存放路由配置 在config文件夹下新建router.js和routes.js router.js: 存放路由设置 routes.js:

  • vue的ssr服务端渲染示例详解

    为什么使用服务器端渲染 (SSR) 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面. 请注意,截至目前,Google 和 Bing 可以很好对同步 JavaScript 应用程序进行索引.在这里,同步是关键.如果你的应用程序初始展示 loading 菊花图,然后通过 Ajax 获取内容,抓取工具并不会等待异步完成后再行抓取页面内容.也就是说,如果 SEO 对你的站点至关重要,而你的页面又是异步获取内容,则你可能需要服务器端渲染(SSR)解决此问题. 更快的内容到达时间 (ti

  • Vue SSR 即时编译技术的实现

    当我们在服务端渲染 Vue 应用时,无论服务器执行多少次渲染,大部分 VNode 渲染出的字符串是不变的,它们有一些来自于模板的静态 html,另一些则来自模板动态渲染的节点(虽然在客户端动态节点有可能会变化,但是在服务端它们是不变的).将这两种类型的节点提取出来,仅在服务端渲染真正动态的节点(serverPrefetch 预取数据相关联的节点),可以显著的提升服务端的渲染性能. 提取模板中静态的 html 只需在编译期对模板结构做解析,而判断动态节点在服务端渲染阶段是否为静态,需在运行时对 V

  • 关于VueSSR的一些理解和详细配置

    目录 概念 流程图 编译图解 相关配置 目录结构 index.html 的改动 需要安装的依赖 启动命令 异步处理例子 结尾 如果是静态页面,例如官网的SSR处理可直接使用prerender-spa-plugin插件实现预渲染,参考我之前的博客:vue单页面通过prerender-spa-plugin插件进行SEO优化 以下是基于vue-cli@2.x生成的工程相关的结构改造.文章最后有异步请求的例子可供参考. 概念 流程图 这是具体的流程图,如何实现,后续配置会详解 编译图解 结合上面的流程图

  • 如何构建 vue-ssr 项目的方法步骤

    如何通过 web 服务器去渲染一个 vue 实例 构建一个极简的服务端渲染需要什么 web 服务器 vue-server-renderer vue const Vue = require('vue') const Koa = require('koa') const app = new Koa() const Router = require('koa-router') const router = new Router() const renderer = require('vue-serve

  • vuecli项目构建SSR服务端渲染的实现

    服务端渲染(SSR) 将一个 Vue 组件在服务端渲染成 HTML 字符串并发送到浏览器,最后将这些静态标记"激活"为可交互应用程序的过程就叫服务端渲染(SSR) 服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器和客户端上运行 为什么使用 服务端渲染(SSR) 更好的 SEO:传统的 spa 页面数据都是异步加载,搜索引擎爬虫无法抓取,服务端渲染(SSR)使搜索引擎爬虫抓取工具可以直接查

  • vue ssr+koa2构建服务端渲染的示例代码

    之前做了活动投放页面在百度.360等渠道投放,采用 koa2 + 模版引擎的方式.发现几个问题 相较于框架开发页面效率较低,维护性差 兼容性问题,在页面中添加埋点后发现有些用户的数据拿不到,排查后发现通过各个渠道过来的用户的设备中仍然包含大量低版本的浏览器. 服务端渲染 服务端渲染和单页面渲染区别 查看下面两张图,可以看到如果是服务端渲染,那么在浏览器中拿到的直接是完整的 html 结构.而单页面是一些 script 标签引入的js文件,最终将虚拟dom去挂在到 #app 容器上. @vue/c

  • 15分钟学会vue项目改造成SSR(小白教程)

    15分钟学会vue项目改造成SSR Ps:网上看了好多服务器渲染的例子,基本都是从0开始的,用Nuxt或者vue官网推荐的ssr方案(vue-server-renderer),但是我们在开发过程中基本上是已经有了现有的项目了,我们所要做的是对现有项目的SSR改造.那么这里,跟我一起对一个vue-cil2.0生成的项目进行SSR改造 关于这篇文章的案例源代码我放在我的github上面,有兴趣的同学,也可以去我的github查看我之前写的博客.博客 一.改造技术的分析对比. 一般来说,我们做seo有

随机推荐