├── .gitignore ├── preview1.png ├── qq-group.jpg ├── lesson7 ├── public │ ├── logo.png │ ├── vue-prerender.png │ ├── vue-music-visit.png │ └── vue-sell-visit.png ├── .babelrc ├── src │ ├── api │ │ ├── home.js │ │ └── detail.js │ ├── index.template.html │ ├── store │ │ ├── index.js │ │ ├── getters.js │ │ ├── mutations.js │ │ └── actions.js │ ├── util │ │ └── title.js │ ├── app.js │ ├── views │ │ ├── list.vue │ │ ├── project.vue │ │ ├── home.vue │ │ └── detail.vue │ ├── router │ │ └── index.js │ ├── app.vue │ ├── entry-server.js │ ├── entry-client.js │ └── components │ │ ├── progressbar.vue │ │ └── footer.vue ├── build │ ├── webpack.server.config.js │ ├── webpack.client.config.js │ ├── webpack.base.config.js │ └── setup-dev-server.js ├── README.md ├── package.json └── server.js ├── assets └── vue-ssr-summarize.png ├── lesson4 ├── public │ ├── vue-prerender.png │ ├── vue-music-visit.png │ └── vue-sell-visit.png ├── src │ ├── entry-server.js │ ├── entry-client.js │ ├── app.js │ ├── index.template.html │ ├── app.vue │ └── components │ │ └── footer.vue ├── build │ ├── webpack.server.config.js │ ├── webpack.client.config.js │ ├── webpack.base.config.js │ └── setup-dev-server.js ├── package.json └── server.js ├── lesson5 ├── public │ ├── vue-prerender.png │ ├── vue-music-visit.png │ └── vue-sell-visit.png ├── src │ ├── entry-client.js │ ├── index.template.html │ ├── app.js │ ├── views │ │ ├── home.vue │ │ └── project.vue │ ├── router │ │ └── index.js │ ├── entry-server.js │ ├── app.vue │ └── components │ │ └── footer.vue ├── .babelrc ├── build │ ├── webpack.server.config.js │ ├── webpack.client.config.js │ ├── webpack.base.config.js │ └── setup-dev-server.js ├── package.json └── server.js ├── lesson6 ├── public │ ├── vue-prerender.png │ ├── vue-music-visit.png │ └── vue-sell-visit.png ├── .babelrc ├── src │ ├── api │ │ ├── home.js │ │ └── detail.js │ ├── index.template.html │ ├── store │ │ ├── index.js │ │ ├── getters.js │ │ ├── mutations.js │ │ └── actions.js │ ├── app.js │ ├── views │ │ ├── list.vue │ │ ├── project.vue │ │ ├── home.vue │ │ └── detail.vue │ ├── router │ │ └── index.js │ ├── app.vue │ ├── entry-server.js │ ├── entry-client.js │ └── components │ │ ├── progressbar.vue │ │ └── footer.vue ├── build │ ├── webpack.server.config.js │ ├── webpack.client.config.js │ ├── webpack.base.config.js │ └── setup-dev-server.js ├── package.json ├── README.md └── server.js ├── lesson1 ├── package.json ├── server.js └── package-lock.json ├── lesson2 ├── package.json └── server.js ├── lesson3 ├── package.json ├── index.template.html └── server.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /preview1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neveryu/vue-ssr-lessons/HEAD/preview1.png -------------------------------------------------------------------------------- /qq-group.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neveryu/vue-ssr-lessons/HEAD/qq-group.jpg -------------------------------------------------------------------------------- /lesson7/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neveryu/vue-ssr-lessons/HEAD/lesson7/public/logo.png -------------------------------------------------------------------------------- /assets/vue-ssr-summarize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neveryu/vue-ssr-lessons/HEAD/assets/vue-ssr-summarize.png -------------------------------------------------------------------------------- /lesson4/public/vue-prerender.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neveryu/vue-ssr-lessons/HEAD/lesson4/public/vue-prerender.png -------------------------------------------------------------------------------- /lesson5/public/vue-prerender.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neveryu/vue-ssr-lessons/HEAD/lesson5/public/vue-prerender.png -------------------------------------------------------------------------------- /lesson6/public/vue-prerender.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neveryu/vue-ssr-lessons/HEAD/lesson6/public/vue-prerender.png -------------------------------------------------------------------------------- /lesson7/public/vue-prerender.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neveryu/vue-ssr-lessons/HEAD/lesson7/public/vue-prerender.png -------------------------------------------------------------------------------- /lesson4/public/vue-music-visit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neveryu/vue-ssr-lessons/HEAD/lesson4/public/vue-music-visit.png -------------------------------------------------------------------------------- /lesson4/public/vue-sell-visit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neveryu/vue-ssr-lessons/HEAD/lesson4/public/vue-sell-visit.png -------------------------------------------------------------------------------- /lesson5/public/vue-music-visit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neveryu/vue-ssr-lessons/HEAD/lesson5/public/vue-music-visit.png -------------------------------------------------------------------------------- /lesson5/public/vue-sell-visit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neveryu/vue-ssr-lessons/HEAD/lesson5/public/vue-sell-visit.png -------------------------------------------------------------------------------- /lesson6/public/vue-music-visit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neveryu/vue-ssr-lessons/HEAD/lesson6/public/vue-music-visit.png -------------------------------------------------------------------------------- /lesson6/public/vue-sell-visit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neveryu/vue-ssr-lessons/HEAD/lesson6/public/vue-sell-visit.png -------------------------------------------------------------------------------- /lesson7/public/vue-music-visit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neveryu/vue-ssr-lessons/HEAD/lesson7/public/vue-music-visit.png -------------------------------------------------------------------------------- /lesson7/public/vue-sell-visit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neveryu/vue-ssr-lessons/HEAD/lesson7/public/vue-sell-visit.png -------------------------------------------------------------------------------- /lesson4/src/entry-server.js: -------------------------------------------------------------------------------- 1 | import { createApp } from './app' 2 | 3 | export default context => { 4 | const { app } = createApp() 5 | return app 6 | } 7 | -------------------------------------------------------------------------------- /lesson4/src/entry-client.js: -------------------------------------------------------------------------------- 1 | import { createApp } from './app' 2 | 3 | // 客户端特定引导逻辑…… 4 | 5 | const { app } = createApp() 6 | 7 | // actually mount to DOM 8 | // 这里假定 App.vue 模板中根元素具有 `id="app"` 9 | app.$mount('#app') 10 | -------------------------------------------------------------------------------- /lesson4/src/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './app.vue' 3 | 4 | // 导出一个工厂函数,用于创建新的应用程序 5 | export function createApp () { 6 | const app = new Vue({ 7 | // 根实例简单的渲染应用程序组件。 8 | render: h => h(App) 9 | }) 10 | return { app } 11 | } 12 | -------------------------------------------------------------------------------- /lesson5/src/entry-client.js: -------------------------------------------------------------------------------- 1 | import { createApp } from './app' 2 | 3 | // 客户端特定引导逻辑…… 4 | 5 | const { app, router } = createApp() 6 | 7 | router.onReady(() => { 8 | // actually mount to DOM 9 | // 这里假定 App.vue 模板中根元素具有 `id="app"` 10 | app.$mount('#app') 11 | }) 12 | -------------------------------------------------------------------------------- /lesson5/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 6 | }] 7 | ], 8 | "plugins": [ 9 | "syntax-dynamic-import", 10 | "transform-object-rest-spread" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /lesson6/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 6 | }] 7 | ], 8 | "plugins": [ 9 | "syntax-dynamic-import", 10 | "transform-object-rest-spread" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /lesson7/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 6 | }] 7 | ], 8 | "plugins": [ 9 | "syntax-dynamic-import", 10 | "transform-object-rest-spread" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /lesson6/src/api/home.js: -------------------------------------------------------------------------------- 1 | const allData = { 2 | totalRegister: 44, 3 | totalActiver: 33, 4 | topMouthActiver: 22, 5 | todayLogin: 11 6 | } 7 | 8 | export function getAll() { 9 | return new Promise((resolve, reject) => { 10 | setTimeout(resolve, 1000, allData) 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /lesson7/src/api/home.js: -------------------------------------------------------------------------------- 1 | const allData = { 2 | totalRegister: 2019, 3 | totalActiver: 2008, 4 | topMouthActiver: 520, 5 | todayLogin: 100 6 | } 7 | 8 | export function getAll() { 9 | return new Promise((resolve, reject) => { 10 | setTimeout(resolve, 1000, allData) 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /lesson4/src/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | 7 | {{{ meta }}} 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /lesson5/src/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | 7 | {{{ meta }}} 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /lesson6/src/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | 7 | {{{ meta }}} 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /lesson7/src/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | 7 | {{{ meta }}} 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /lesson5/src/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './app.vue' 3 | import { createRouter } from './router' 4 | 5 | // 导出一个工厂函数,用于创建新的应用程序,router 实例 6 | export function createApp () { 7 | // 创建 router 实例 8 | const router = createRouter() 9 | 10 | const app = new Vue({ 11 | // 根实例简单的渲染应用程序组件。 12 | router, 13 | render: h => h(App) 14 | }) 15 | return { app, router } 16 | } 17 | -------------------------------------------------------------------------------- /lesson1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lesson1", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "vue": "^2.6.7", 15 | "vue-server-renderer": "^2.6.7" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lesson2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lesson2", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "nodemon server.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "express": "^4.16.4", 15 | "nodemon": "^1.18.10", 16 | "vue": "^2.6.7", 17 | "vue-server-renderer": "^2.6.7" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lesson3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lesson3", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "nodemon server.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "express": "^4.16.4", 15 | "nodemon": "^1.18.10", 16 | "vue": "^2.6.7", 17 | "vue-server-renderer": "^2.6.7" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lesson3/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | 7 | {{{ meta }}} 8 | 9 | 10 | 11 | {{ content }} 12 |
13 | 16 | 17 | {{ footer }} 18 | 19 | -------------------------------------------------------------------------------- /lesson5/src/views/home.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /lesson6/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import actions from './actions' 4 | import mutations from './mutations' 5 | import getters from './getters' 6 | 7 | Vue.use(Vuex) 8 | 9 | export function createStore() { 10 | return new Vuex.Store({ 11 | state: { 12 | totalRegister: 0, 13 | totalActiver: 0, 14 | topMouthActiver: 0, 15 | todayLogin: 0, 16 | projectList: [], 17 | detail: {}, 18 | totalCount: 0 19 | }, 20 | actions, 21 | mutations, 22 | getters 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /lesson7/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import actions from './actions' 4 | import mutations from './mutations' 5 | import getters from './getters' 6 | 7 | Vue.use(Vuex) 8 | 9 | export function createStore() { 10 | return new Vuex.Store({ 11 | state: { 12 | totalRegister: 0, 13 | totalActiver: 0, 14 | topMouthActiver: 0, 15 | todayLogin: 0, 16 | projectList: [], 17 | detail: {}, 18 | totalCount: 0 19 | }, 20 | actions, 21 | mutations, 22 | getters 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /lesson5/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | export function createRouter() { 7 | return new Router({ 8 | mode: 'history', 9 | fallback: false, 10 | scrollBehavior: () => ({ y: 0 }), 11 | routes: [ 12 | { 13 | path: '/', 14 | redirect: '/home' 15 | }, 16 | { 17 | path: '/home', 18 | component: () => import('../views/home.vue') 19 | }, 20 | { 21 | path: '/project', 22 | component: () => import('../views/project.vue') 23 | } 24 | ] 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /lesson6/src/store/getters.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // home 3 | totalRegister(state, getters) { 4 | return state.totalRegister 5 | }, 6 | totalActiver(state, getters) { 7 | return state.totalActiver 8 | }, 9 | topMouthActiver(state, getters) { 10 | return state.topMouthActiver 11 | }, 12 | todayLogin(state, getters) { 13 | return state.todayLogin 14 | }, 15 | 16 | // project 17 | projectList(state, getters) { 18 | return state.projectList 19 | }, 20 | 21 | // get detail 22 | getDetail(state, getters) { 23 | return { 24 | item: state.detail, 25 | total: state.totalCount 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lesson7/src/store/getters.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // home 3 | totalRegister(state, getters) { 4 | return state.totalRegister 5 | }, 6 | totalActiver(state, getters) { 7 | return state.totalActiver 8 | }, 9 | topMouthActiver(state, getters) { 10 | return state.topMouthActiver 11 | }, 12 | todayLogin(state, getters) { 13 | return state.todayLogin 14 | }, 15 | 16 | // project 17 | projectList(state, getters) { 18 | return state.projectList 19 | }, 20 | 21 | // get detail 22 | getDetail(state, getters) { 23 | return { 24 | item: state.detail, 25 | total: state.totalCount 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lesson6/src/store/mutations.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default { 4 | // HOME 5 | SET_TOTALREGISTER: (state, num) => { 6 | state.totalRegister = num 7 | }, 8 | SET_TOTALACTIVER: (state, num) => { 9 | state.totalActiver = num 10 | }, 11 | SET_TOPMOUTHACTIVER: (state, num) => { 12 | state.topMouthActiver = num 13 | }, 14 | SET_TODAYLOGIN: (state, num) => { 15 | state.todayLogin = num 16 | }, 17 | 18 | // project 19 | SET_ALLPROJECT: (state, list) => { 20 | state.projectList = list 21 | }, 22 | 23 | // current detail 24 | SET_DETAIL: (state, obj) => { 25 | state.detail = obj.item 26 | state.totalCount = obj.total 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lesson7/src/store/mutations.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default { 4 | // HOME 5 | SET_TOTALREGISTER: (state, num) => { 6 | state.totalRegister = num 7 | }, 8 | SET_TOTALACTIVER: (state, num) => { 9 | state.totalActiver = num 10 | }, 11 | SET_TOPMOUTHACTIVER: (state, num) => { 12 | state.topMouthActiver = num 13 | }, 14 | SET_TODAYLOGIN: (state, num) => { 15 | state.todayLogin = num 16 | }, 17 | 18 | // project 19 | SET_ALLPROJECT: (state, list) => { 20 | state.projectList = list 21 | }, 22 | 23 | // current detail 24 | SET_DETAIL: (state, obj) => { 25 | state.detail = obj.item 26 | state.totalCount = obj.total 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lesson7/src/util/title.js: -------------------------------------------------------------------------------- 1 | function getTitle(vm) { 2 | const { title } = vm.$options 3 | if(title) { 4 | return typeof title === 'function' ? title.call(vm) : title 5 | } 6 | } 7 | 8 | const serverTitleMixin = { 9 | created() { 10 | const title = getTitle(this) 11 | if(title) { 12 | this.$ssrContext.title = `Vue SSR Lesson | ${title}` 13 | } 14 | } 15 | } 16 | 17 | const clientTitleMixin = { 18 | mounted() { 19 | const title = getTitle(this) 20 | if(title) { 21 | document.title = `Vue SSR Lesson | ${ title }` 22 | } 23 | } 24 | } 25 | 26 | export default process.env.VUE_ENV === 'server' 27 | ? serverTitleMixin 28 | : clientTitleMixin 29 | -------------------------------------------------------------------------------- /lesson1/server.js: -------------------------------------------------------------------------------- 1 | // 第 1 步:创建一个 Vue 实例 2 | const Vue = require('vue') 3 | const app = new Vue({ 4 | template: `
Hello World
` 5 | }) 6 | 7 | // 第 2 步:创建一个 renderer 8 | const renderer = require('vue-server-renderer').createRenderer() 9 | 10 | // 第 3 步:将 Vue 实例渲染为 HTML 11 | /** 12 | renderer.renderToString(app, (err, html) => { 13 | if(err) throw err 14 | console.log(html) 15 | // =>
Hello World
16 | }) 17 | */ 18 | 19 | 20 | // 在 2.5.0+,如果没有传入回调函数,则会返回 Promise: 21 | renderer.renderToString(app).then(html => { 22 | console.log(html) 23 | // =>
Hello World
24 | }).catch(err => { 25 | console.error(err) 26 | }) 27 | -------------------------------------------------------------------------------- /lesson6/src/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './app.vue' 3 | import { createRouter } from './router' 4 | import { createStore } from './store' 5 | import { sync } from 'vuex-router-sync' 6 | 7 | // 导出一个工厂函数,用于创建新的应用程序,router 和 store 实例 8 | export function createApp () { 9 | // 创建 router 和 store 实例 10 | const router = createRouter() 11 | const store = createStore() 12 | 13 | // 同步路由状态(route state)到 store 14 | sync(store, router) 15 | 16 | // 创建应用程序实例,将 router 和 store 注入 17 | const app = new Vue({ 18 | router, 19 | store, 20 | // 根实例简单的渲染应用程序组件 21 | render: h => h(App) 22 | }) 23 | 24 | // 暴露 app, router 和 store 25 | return { app, router, store } 26 | } 27 | -------------------------------------------------------------------------------- /lesson5/src/entry-server.js: -------------------------------------------------------------------------------- 1 | import { createApp } from './app' 2 | 3 | export default context => { 4 | // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise, 5 | // 以便服务器能够等待所有的内容在渲染前, 6 | // 就已经准备就绪。 7 | return new Promise((resolve, reject) => { 8 | const { app, router } = createApp() 9 | 10 | // 设置服务器端 router 的位置 11 | router.push(context.url) 12 | 13 | // 等到 router 将可能的异步组件和钩子函数解析完 14 | router.onReady(() => { 15 | const matchedComponents = router.getMatchedComponents() 16 | // 匹配不到的路由,执行 reject 函数,并返回 404 17 | if (!matchedComponents.length) { 18 | return reject({ code: 404 }) 19 | } 20 | 21 | // Promise 应该 resolve 应用程序实例,以便它可以渲染 22 | resolve(app) 23 | }, reject) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /lesson6/src/store/actions.js: -------------------------------------------------------------------------------- 1 | import { getAll } from '../api/home' 2 | import { getProjectList, getItem } from '../api/detail' 3 | 4 | export default { 5 | getAllData({ commit }) { 6 | return getAll().then(res => { 7 | commit('SET_TOTALREGISTER', res.totalRegister) 8 | commit('SET_TOTALACTIVER', res.totalActiver) 9 | commit('SET_TOPMOUTHACTIVER', res.topMouthActiver) 10 | commit('SET_TODAYLOGIN', res.todayLogin) 11 | }) 12 | }, 13 | getAllProject({ commit }) { 14 | return getProjectList().then(res => { 15 | commit('SET_ALLPROJECT', res) 16 | }) 17 | }, 18 | fetchItem({ commit }, id) { 19 | return getItem(id).then(res => { 20 | commit('SET_DETAIL', res) 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lesson7/src/store/actions.js: -------------------------------------------------------------------------------- 1 | import { getAll } from '../api/home' 2 | import { getProjectList, getItem } from '../api/detail' 3 | 4 | export default { 5 | getAllData({ commit }) { 6 | return getAll().then(res => { 7 | commit('SET_TOTALREGISTER', res.totalRegister) 8 | commit('SET_TOTALACTIVER', res.totalActiver) 9 | commit('SET_TOPMOUTHACTIVER', res.topMouthActiver) 10 | commit('SET_TODAYLOGIN', res.todayLogin) 11 | }) 12 | }, 13 | getAllProject({ commit }) { 14 | return getProjectList().then(res => { 15 | commit('SET_ALLPROJECT', res) 16 | }) 17 | }, 18 | fetchItem({ commit }, id) { 19 | return getItem(id).then(res => { 20 | commit('SET_DETAIL', res) 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lesson6/src/views/list.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 25 | 26 | 36 | -------------------------------------------------------------------------------- /lesson7/src/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './app.vue' 3 | import { createRouter } from './router' 4 | import { createStore } from './store' 5 | import { sync } from 'vuex-router-sync' 6 | import titleMixin from './util/title' 7 | 8 | // mixin for handling title 9 | Vue.mixin(titleMixin) 10 | 11 | // 导出一个工厂函数,用于创建新的应用程序,router 和 store 实例 12 | export function createApp () { 13 | // 创建 router 和 store 实例 14 | const router = createRouter() 15 | const store = createStore() 16 | 17 | // 同步路由状态(route state)到 store 18 | sync(store, router) 19 | 20 | // 创建应用程序实例,将 router 和 store 注入 21 | const app = new Vue({ 22 | router, 23 | store, 24 | // 根实例简单的渲染应用程序组件 25 | render: h => h(App) 26 | }) 27 | 28 | // 暴露 app, router 和 store 29 | return { app, router, store } 30 | } 31 | -------------------------------------------------------------------------------- /lesson7/src/views/list.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | 27 | 37 | -------------------------------------------------------------------------------- /lesson6/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | export function createRouter() { 7 | return new Router({ 8 | mode: 'history', 9 | fallback: false, 10 | scrollBehavior: () => ({ y: 0 }), 11 | routes: [ 12 | { 13 | path: '/', 14 | redirect: '/home' 15 | }, 16 | { 17 | path: '/home', 18 | component: () => import('../views/home.vue') 19 | }, 20 | { 21 | path: '/project', 22 | component: () => import('../views/project.vue') 23 | }, 24 | { 25 | path: '/list', 26 | component: () => import('../views/list.vue') 27 | }, 28 | { 29 | path: '/detail/:id(\\d+)?', 30 | component: () => import('../views/detail.vue') 31 | } 32 | ] 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /lesson7/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | export function createRouter() { 7 | return new Router({ 8 | mode: 'history', 9 | fallback: false, 10 | scrollBehavior: () => ({ y: 0 }), 11 | routes: [ 12 | { 13 | path: '/', 14 | redirect: '/home' 15 | }, 16 | { 17 | path: '/home', 18 | component: () => import('../views/home.vue') 19 | }, 20 | { 21 | path: '/project', 22 | component: () => import('../views/project.vue') 23 | }, 24 | { 25 | path: '/list', 26 | component: () => import('../views/list.vue') 27 | }, 28 | { 29 | path: '/detail/:id(\\d+)?', 30 | component: () => import('../views/detail.vue') 31 | } 32 | ] 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /lesson5/src/views/project.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | -------------------------------------------------------------------------------- /lesson6/src/views/project.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | -------------------------------------------------------------------------------- /lesson4/build/webpack.server.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const merge = require('webpack-merge') 3 | const base = require('./webpack.base.config') 4 | const nodeExternals = require('webpack-node-externals') 5 | const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') 6 | 7 | module.exports = merge(base, { 8 | target: 'node', 9 | devtool: '#source-map', 10 | entry: './src/entry-server.js', 11 | output: { 12 | filename: 'server-bundle.js', 13 | libraryTarget: 'commonjs2' 14 | }, 15 | resolve: { 16 | alias: { 17 | 'create-api': './create-api-server.js' 18 | } 19 | }, 20 | externals: nodeExternals({ 21 | // do not externalize CSS files in case we need to import it from a dep 22 | whitelist: /\.css$/ 23 | }), 24 | plugins: [ 25 | new webpack.DefinePlugin({ 26 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), 27 | 'process.env.VUE_ENV': '"server"' 28 | }), 29 | // 这是将服务器的整个输出构建为单个 JSON 文件的插件。 30 | // 默认文件名为 `vue-ssr-server-bundle.json` 31 | new VueSSRServerPlugin() 32 | ] 33 | }) 34 | -------------------------------------------------------------------------------- /lesson5/build/webpack.server.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const merge = require('webpack-merge') 3 | const base = require('./webpack.base.config') 4 | const nodeExternals = require('webpack-node-externals') 5 | const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') 6 | 7 | module.exports = merge(base, { 8 | target: 'node', 9 | devtool: '#source-map', 10 | entry: './src/entry-server.js', 11 | output: { 12 | filename: 'server-bundle.js', 13 | libraryTarget: 'commonjs2' 14 | }, 15 | resolve: { 16 | alias: { 17 | 'create-api': './create-api-server.js' 18 | } 19 | }, 20 | externals: nodeExternals({ 21 | // do not externalize CSS files in case we need to import it from a dep 22 | whitelist: /\.css$/ 23 | }), 24 | plugins: [ 25 | new webpack.DefinePlugin({ 26 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), 27 | 'process.env.VUE_ENV': '"server"' 28 | }), 29 | // 这是将服务器的整个输出构建为单个 JSON 文件的插件。 30 | // 默认文件名为 `vue-ssr-server-bundle.json` 31 | new VueSSRServerPlugin() 32 | ] 33 | }) 34 | -------------------------------------------------------------------------------- /lesson6/build/webpack.server.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const merge = require('webpack-merge') 3 | const base = require('./webpack.base.config') 4 | const nodeExternals = require('webpack-node-externals') 5 | const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') 6 | 7 | module.exports = merge(base, { 8 | target: 'node', 9 | devtool: '#source-map', 10 | entry: './src/entry-server.js', 11 | output: { 12 | filename: 'server-bundle.js', 13 | libraryTarget: 'commonjs2' 14 | }, 15 | resolve: { 16 | alias: { 17 | 'create-api': './create-api-server.js' 18 | } 19 | }, 20 | externals: nodeExternals({ 21 | // do not externalize CSS files in case we need to import it from a dep 22 | whitelist: /\.css$/ 23 | }), 24 | plugins: [ 25 | new webpack.DefinePlugin({ 26 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), 27 | 'process.env.VUE_ENV': '"server"' 28 | }), 29 | // 这是将服务器的整个输出构建为单个 JSON 文件的插件。 30 | // 默认文件名为 `vue-ssr-server-bundle.json` 31 | new VueSSRServerPlugin() 32 | ] 33 | }) 34 | -------------------------------------------------------------------------------- /lesson7/src/views/project.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 31 | 32 | -------------------------------------------------------------------------------- /lesson4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lesson4", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "nodemon server.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.16.4", 14 | "extract-text-webpack-plugin": "^3.0.2", 15 | "nodemon": "^1.18.10", 16 | "vue": "^2.6.7", 17 | "vue-server-renderer": "^2.6.7" 18 | }, 19 | "devDependencies": { 20 | "autoprefixer": "^7.1.6", 21 | "babel-core": "^6.26.0", 22 | "babel-loader": "^7.1.2", 23 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 24 | "babel-preset-env": "^1.6.1", 25 | "chokidar": "^1.7.0", 26 | "css-loader": "^0.28.7", 27 | "file-loader": "^1.1.5", 28 | "friendly-errors-webpack-plugin": "^1.6.1", 29 | "rimraf": "^2.6.2", 30 | "stylus": "^0.54.5", 31 | "stylus-loader": "^3.0.1", 32 | "sw-precache-webpack-plugin": "^0.11.4", 33 | "url-loader": "^0.6.2", 34 | "vue-loader": "^15.3.0", 35 | "vue-template-compiler": "^2.5.22", 36 | "webpack": "^3.8.1", 37 | "webpack-dev-middleware": "^1.12.0", 38 | "webpack-hot-middleware": "^2.20.0", 39 | "webpack-merge": "^4.2.1", 40 | "webpack-node-externals": "^1.7.2" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lesson5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lesson5", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "nodemon server.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.16.4", 14 | "extract-text-webpack-plugin": "^3.0.2", 15 | "nodemon": "^1.18.10", 16 | "vue": "^2.6.7", 17 | "vue-router": "^3.0.1", 18 | "vue-server-renderer": "^2.6.7" 19 | }, 20 | "devDependencies": { 21 | "autoprefixer": "^7.1.6", 22 | "babel-core": "^6.26.0", 23 | "babel-loader": "^7.1.2", 24 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 25 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 26 | "babel-preset-env": "^1.6.1", 27 | "chokidar": "^1.7.0", 28 | "css-loader": "^0.28.7", 29 | "file-loader": "^1.1.5", 30 | "friendly-errors-webpack-plugin": "^1.6.1", 31 | "rimraf": "^2.6.2", 32 | "stylus": "^0.54.5", 33 | "stylus-loader": "^3.0.1", 34 | "sw-precache-webpack-plugin": "^0.11.4", 35 | "url-loader": "^0.6.2", 36 | "vue-loader": "^15.3.0", 37 | "vue-template-compiler": "^2.5.22", 38 | "webpack": "^3.8.1", 39 | "webpack-dev-middleware": "^1.12.0", 40 | "webpack-hot-middleware": "^2.20.0", 41 | "webpack-merge": "^4.2.1", 42 | "webpack-node-externals": "^1.7.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lesson6/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lesson6", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "nodemon server.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.16.4", 14 | "extract-text-webpack-plugin": "^3.0.2", 15 | "nodemon": "^1.18.10", 16 | "vue": "^2.6.7", 17 | "vue-router": "^3.0.1", 18 | "vue-server-renderer": "^2.6.7", 19 | "vuex": "^3.0.1", 20 | "vuex-router-sync": "^5.0.0" 21 | }, 22 | "devDependencies": { 23 | "autoprefixer": "^7.1.6", 24 | "babel-core": "^6.26.0", 25 | "babel-loader": "^7.1.2", 26 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 27 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 28 | "babel-preset-env": "^1.6.1", 29 | "chokidar": "^1.7.0", 30 | "css-loader": "^0.28.7", 31 | "file-loader": "^1.1.5", 32 | "friendly-errors-webpack-plugin": "^1.6.1", 33 | "rimraf": "^2.6.2", 34 | "stylus": "^0.54.5", 35 | "stylus-loader": "^3.0.1", 36 | "sw-precache-webpack-plugin": "^0.11.4", 37 | "url-loader": "^0.6.2", 38 | "vue-loader": "^15.3.0", 39 | "vue-template-compiler": "^2.5.22", 40 | "webpack": "^3.8.1", 41 | "webpack-dev-middleware": "^1.12.0", 42 | "webpack-hot-middleware": "^2.20.0", 43 | "webpack-merge": "^4.2.1", 44 | "webpack-node-externals": "^1.7.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lesson6/src/views/home.vue: -------------------------------------------------------------------------------- 1 | 13 | 44 | 45 | -------------------------------------------------------------------------------- /lesson2/server.js: -------------------------------------------------------------------------------- 1 | const Vue = require('vue') 2 | const server = require('express')() 3 | const VueServerRenderer = require('vue-server-renderer') 4 | const renderer = VueServerRenderer.createRenderer() 5 | 6 | server.get('*', (req, res) => { 7 | const app = new Vue({ 8 | data: { 9 | url: req.url, 10 | text: `vue ssr lessons from: NeverYu`, 11 | repository: `项目仓库地址: vue-ssr-lessons` 12 | }, 13 | template: ` 14 |
15 |
当前访问的 URL 是: {{ url }}
16 |
17 |
18 |
19 | ` 20 | }) 21 | 22 | renderer.renderToString(app).then(html => { 23 | res.end(` 24 | 25 | 26 | 27 | 28 | ${html} 29 | 30 | `) 31 | }).catch(err => { 32 | res.status(500).end('Internal Server Error') 33 | return 34 | }) 35 | }) 36 | 37 | /** 38 | * 下面是两种起 server 的方式 39 | * 用其一即可 40 | */ 41 | 42 | server.set('port', process.env.PORT || 8888) 43 | let hostname = '0.0.0.0' 44 | server.listen(server.get('port'), hostname, () => { 45 | console.log(`Server running at http://${hostname}:${server.get('port')}`) 46 | }) 47 | 48 | // const port = process.env.PORT || 8888 49 | // server.listen(port, () => { 50 | // console.log(`server started at localhost:${port}`) 51 | // }) 52 | -------------------------------------------------------------------------------- /lesson7/src/views/home.vue: -------------------------------------------------------------------------------- 1 | 13 | 45 | 46 | -------------------------------------------------------------------------------- /lesson7/build/webpack.server.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const merge = require('webpack-merge') 3 | const base = require('./webpack.base.config') 4 | const nodeExternals = require('webpack-node-externals') 5 | const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') 6 | 7 | module.exports = merge(base, { 8 | // 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import), 9 | // 并且还会在编译 Vue 组件时, 10 | // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。 11 | target: 'node', 12 | // 对 bundle renderer 提供 source map 支持 13 | devtool: '#source-map', 14 | entry: './src/entry-server.js', 15 | // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports) 16 | output: { 17 | filename: 'server-bundle.js', 18 | libraryTarget: 'commonjs2' 19 | }, 20 | resolve: { 21 | alias: { 22 | 'create-api': './create-api-server.js' 23 | } 24 | }, 25 | // https://webpack.js.org/configuration/externals/#function 26 | // https://github.com/liady/webpack-node-externals 27 | // 外置化应用程序依赖模块。可以使服务器构建速度更快, 28 | // 并生成较小的 bundle 文件。 29 | externals: nodeExternals({ 30 | // 不要外置化 webpack 需要处理的依赖模块。 31 | // do not externalize CSS files in case we need to import it from a dep 32 | whitelist: /\.css$/ 33 | }), 34 | plugins: [ 35 | new webpack.DefinePlugin({ 36 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), 37 | 'process.env.VUE_ENV': '"server"' 38 | }), 39 | // 这是将服务器的整个输出构建为单个 JSON 文件的插件。 40 | // 默认文件名为 `vue-ssr-server-bundle.json` 41 | new VueSSRServerPlugin() 42 | ] 43 | }) 44 | -------------------------------------------------------------------------------- /lesson3/server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const Vue = require('vue') 3 | const server = require('express')() 4 | const VueServerRenderer = require('vue-server-renderer') 5 | const renderer = VueServerRenderer.createRenderer({ 6 | template: fs.readFileSync('./index.template.html', 'utf-8') 7 | }) 8 | 9 | const context = { 10 | title: 'vue ssr lesson3', 11 | meta: ` 12 | 13 | `, 14 | content: '这是服务端插入的内容,由 renderToString 第二个参数 context 提供', 15 | footer: 'Final Content' 16 | } 17 | 18 | server.get('*', (req, res) => { 19 | const app = new Vue({ 20 | data: { 21 | url: req.url, 22 | text: `项目仓库地址: vue-ssr-lessons` 23 | }, 24 | template: ` 25 |
26 |
访问的 URL 是: {{ url }}
27 |
28 |
29 |
30 | ` 31 | }) 32 | 33 | renderer.renderToString(app, context).then(html => { 34 | // 这里输出就是将内容插入到模板后的,整个html内容 35 | res.end(`${html}`) 36 | }).catch(err => { 37 | res.status(500).end('Internal Server Error') 38 | return 39 | }) 40 | }) 41 | 42 | /** 43 | * 下面是两种起 server 的方式 44 | * 用其一即可 45 | */ 46 | 47 | server.set('port', process.env.PORT || 8888) 48 | let hostname = '0.0.0.0' 49 | server.listen(server.get('port'), hostname, () => { 50 | console.log(`Server running at http://${hostname}:${server.get('port')}`) 51 | }) 52 | 53 | // const port = process.env.PORT || 8888 54 | // server.listen(port, () => { 55 | // console.log(`server started at localhost:${port}`) 56 | // }) 57 | -------------------------------------------------------------------------------- /lesson5/build/webpack.client.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const merge = require('webpack-merge') 3 | const base = require('./webpack.base.config') 4 | const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') 5 | 6 | const config = merge(base, { 7 | entry: { 8 | app: './src/entry-client.js' 9 | }, 10 | resolve: { 11 | alias: { 12 | 'create-api': './create-api-client.js' 13 | } 14 | }, 15 | plugins: [ 16 | // strip dev-only code in Vue source 17 | new webpack.DefinePlugin({ 18 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), 19 | 'process.env.VUE_ENV': '"client"' 20 | }), 21 | // extract vendor chunks for better caching 22 | new webpack.optimize.CommonsChunkPlugin({ 23 | name: 'vendor', 24 | minChunks: function(module) { 25 | // a module is extracted into the vendor chunk if... 26 | return ( 27 | // it's inside node_modules 28 | // and not a CSS file (due to extract-text-webpack-plugin limitation) 29 | /node_modules/.test(module.context) && !/\.css$/.test(module.request) 30 | ) 31 | } 32 | }), 33 | // extract webpack runtime & manifest to avoid vendor chunk hash changing 34 | // on every build. 35 | // 重要信息:这将 webpack 运行时分离到一个引导 chunk 中, 36 | // 以便可以在之后正确注入异步 chunk。 37 | // 这也为你的应用程序 /vendor 代码提供了更好的缓存。 38 | new webpack.optimize.CommonsChunkPlugin({ 39 | name: 'manifest' 40 | }), 41 | // 此插件在输出目录中 42 | // 生成 `vue-ssr-client-manifest.json` 43 | new VueSSRClientPlugin() 44 | ] 45 | }) 46 | 47 | module.exports = config -------------------------------------------------------------------------------- /lesson6/build/webpack.client.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const merge = require('webpack-merge') 3 | const base = require('./webpack.base.config') 4 | const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') 5 | 6 | const config = merge(base, { 7 | entry: { 8 | app: './src/entry-client.js' 9 | }, 10 | resolve: { 11 | alias: { 12 | 'create-api': './create-api-client.js' 13 | } 14 | }, 15 | plugins: [ 16 | // strip dev-only code in Vue source 17 | new webpack.DefinePlugin({ 18 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), 19 | 'process.env.VUE_ENV': '"client"' 20 | }), 21 | // extract vendor chunks for better caching 22 | new webpack.optimize.CommonsChunkPlugin({ 23 | name: 'vendor', 24 | minChunks: function(module) { 25 | // a module is extracted into the vendor chunk if... 26 | return ( 27 | // it's inside node_modules 28 | // and not a CSS file (due to extract-text-webpack-plugin limitation) 29 | /node_modules/.test(module.context) && !/\.css$/.test(module.request) 30 | ) 31 | } 32 | }), 33 | // extract webpack runtime & manifest to avoid vendor chunk hash changing 34 | // on every build. 35 | // 重要信息:这将 webpack 运行时分离到一个引导 chunk 中, 36 | // 以便可以在之后正确注入异步 chunk。 37 | // 这也为你的应用程序 /vendor 代码提供了更好的缓存。 38 | new webpack.optimize.CommonsChunkPlugin({ 39 | name: 'manifest' 40 | }), 41 | // 此插件在输出目录中 42 | // 生成 `vue-ssr-client-manifest.json` 43 | new VueSSRClientPlugin() 44 | ] 45 | }) 46 | 47 | module.exports = config -------------------------------------------------------------------------------- /lesson7/build/webpack.client.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const merge = require('webpack-merge') 3 | const base = require('./webpack.base.config') 4 | const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') 5 | 6 | const config = merge(base, { 7 | entry: { 8 | app: './src/entry-client.js' 9 | }, 10 | resolve: { 11 | alias: { 12 | 'create-api': './create-api-client.js' 13 | } 14 | }, 15 | plugins: [ 16 | // strip dev-only code in Vue source 17 | new webpack.DefinePlugin({ 18 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), 19 | 'process.env.VUE_ENV': '"client"' 20 | }), 21 | // extract vendor chunks for better caching 22 | new webpack.optimize.CommonsChunkPlugin({ 23 | name: 'vendor', 24 | minChunks: function(module) { 25 | // a module is extracted into the vendor chunk if... 26 | return ( 27 | // it's inside node_modules 28 | // and not a CSS file (due to extract-text-webpack-plugin limitation) 29 | /node_modules/.test(module.context) && !/\.css$/.test(module.request) 30 | ) 31 | } 32 | }), 33 | // extract webpack runtime & manifest to avoid vendor chunk hash changing 34 | // on every build. 35 | // 重要信息:这将 webpack 运行时分离到一个引导 chunk 中, 36 | // 以便可以在之后正确注入异步 chunk。 37 | // 这也为你的应用程序 /vendor 代码提供了更好的缓存。 38 | new webpack.optimize.CommonsChunkPlugin({ 39 | name: 'manifest' 40 | }), 41 | // 此插件在输出目录中 42 | // 生成 `vue-ssr-client-manifest.json` 43 | new VueSSRClientPlugin() 44 | ] 45 | }) 46 | 47 | module.exports = config -------------------------------------------------------------------------------- /lesson4/build/webpack.client.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const merge = require('webpack-merge') 3 | const base = require('./webpack.base.config') 4 | const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') 5 | 6 | const config = merge(base, { 7 | entry: { 8 | app: './src/entry-client.js' 9 | }, 10 | resolve: { 11 | alias: { 12 | 'create-api': './create-api-client.js' 13 | } 14 | }, 15 | plugins: [ 16 | // strip dev-only code in Vue source 17 | new webpack.DefinePlugin({ 18 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), 19 | 'process.env.VUE_ENV': '"client"' 20 | }), 21 | // extract vendor chunks for better caching 22 | new webpack.optimize.CommonsChunkPlugin({ 23 | name: 'vendor', 24 | minChunks: function(module) { 25 | // a module is extracted into the vendor chunk if... 26 | return ( 27 | // it's inside node_modules 28 | // and not a CSS file (due to extract-text-webpack-plugin limitation) 29 | /node_modules/.test(module.context) && !/\.css$/.test(module.request) 30 | ) 31 | } 32 | }), 33 | // extract webpack runtime & manifest to avoid vendor chunk hash changing 34 | // on every build. 35 | // 重要信息:这将 webpack 运行时分离到一个引导 chunk 中, 36 | // 以便可以在之后正确注入异步 chunk。 37 | // 这也为你的应用程序 /vendor 代码提供了更好的缓存。 38 | new webpack.optimize.CommonsChunkPlugin({ 39 | name: 'manifest' 40 | }), 41 | // 此插件在输出目录中 42 | // 生成 `vue-ssr-client-manifest.json` 43 | new VueSSRClientPlugin() 44 | ] 45 | }) 46 | 47 | module.exports = config 48 | -------------------------------------------------------------------------------- /lesson7/README.md: -------------------------------------------------------------------------------- 1 | **内容**:这一节增加了一些额外的辅助工具【`gzip`、缓存、`favicon`、`title`】;然后增加了生产环境的打包和运行命令。 2 | 3 | 1、 服务端使用 `compression` 开启 Gzip。 4 | 5 | 2、缓存策略 6 | 7 | **页面级别缓存**,如果内容不是用户特定 (`user-specific`)(即对于相同的 URL,总是为所有用户渲染相同的内容),我们可以利用名为 `micro-caching` 的缓存策略,来大幅度提高应用程序处理高流量的能力。 8 | 9 | **组件级别缓存**,`vue-server-renderer` 内置支持组件级别缓存 (`component-level caching`)。要启用组件级别缓存,你需要在创建 `renderer` 时提供具体缓存实现方式(`cache implementation`)。典型做法是传入 `lru-cache`。 10 | 11 | 3、使用 `serve-favicon` 中间件,从服务端来提供网页标签页的小 `logo`。 12 | 13 | 4、动态 `title` 14 | 15 | 我们动态生成 `title` ,然后判断是服务端还是客户端,分别在各自对应的钩子函数,完成网页 `title` 的赋值操作(如果是服务端就在 `created` 钩子函数中,如果是客户端就是 `mounted` 钩子函数中)。然后使用 `Vue.mixin()` 将钩子函数混入。 16 | 17 | 4.1、Head 管理 18 | 19 | 使用相同的策略,你可以轻松地将此 `mixin` 扩展为通用的头部管理工具 (generic head management utility)。 20 | 21 | 5、新增构建打包以及运行 server 的命令 22 | 23 | ``` json 24 | "scripts": { 25 | "dev": "npm start", 26 | "start": "nodemon server.js", 27 | "server": "cross-env NODE_ENV=production node server", 28 | "build": "rimraf dist && npm run build:client && npm run build:server", 29 | "build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js --progress --hide-modules", 30 | "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js --progress --hide-modules" 31 | } 32 | ``` 33 | 34 | 课程 7 的开发运行步骤: 35 | 36 | ``` bash 37 | cd lesson7 38 | 39 | # install dependencies 40 | npm install 41 | 42 | # serve in dev mode, with hot reload at localhost:8080 43 | npm run dev 44 | ``` 45 | 46 | 课程 7 构建以及 server 运行步骤: 47 | 48 | ``` bash 49 | # build for production(打生产环境的包) 50 | npm run build 51 | 52 | # serve in production mode(运行生产环境) 53 | npm run server 54 | ``` 55 | 56 | -------------------------------------------------------------------------------- /lesson5/src/app.vue: -------------------------------------------------------------------------------- 1 | 13 | 21 | 35 | 36 | -------------------------------------------------------------------------------- /lesson6/src/app.vue: -------------------------------------------------------------------------------- 1 | 14 | 22 | 36 | 37 | -------------------------------------------------------------------------------- /lesson7/src/app.vue: -------------------------------------------------------------------------------- 1 | 14 | 22 | 36 | 37 | -------------------------------------------------------------------------------- /lesson6/src/entry-server.js: -------------------------------------------------------------------------------- 1 | import { createApp } from './app' 2 | 3 | const isDev = process.env.NODE_ENV !== 'production' 4 | 5 | // This exported function will be called by `bundleRenderer`. 6 | // This is where we perform data-prefetching to determine the 7 | // state of our application before actually rendering it. 8 | // Since data fetching is async, this function is expected to 9 | // return a Promise that resolves to the app instance. 10 | export default context => { 11 | // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise, 12 | // 以便服务器能够等待所有的内容在渲染前, 13 | // 就已经准备就绪。 14 | return new Promise((resolve, reject) => { 15 | const s = isDev && Date.now() 16 | const { app, router, store } = createApp() 17 | 18 | const { url } = context 19 | const { fullPath } = router.resolve(url).route 20 | 21 | if(fullPath !== url) { 22 | return reject({ url: fullPath}) 23 | } 24 | 25 | // 设置服务器端 router 的位置 26 | router.push(context.url) 27 | 28 | // 等到 router 将可能的异步组件和钩子函数解析完 29 | router.onReady(() => { 30 | const matchedComponents = router.getMatchedComponents() 31 | // 匹配不到的路由,执行 reject 函数,并返回 404 32 | if (!matchedComponents.length) { 33 | return reject({ code: 404 }) 34 | } 35 | 36 | // 对所有匹配的路由组件调用 `asyncData()`,获取,处理数据,然后存在 vuex 37 | // 供客户端使用 38 | Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({ 39 | store, 40 | route: router.currentRoute 41 | }))).then(() => { 42 | isDev && console.log(`data pre-fetch: ${Date.now() - s}ms`) 43 | // 在所有预取钩子(preFetch hook) resolve 后, 44 | // 我们的 store 现在已经填充入渲染应用程序所需的状态。 45 | // 当我们将状态附加到上下文, 46 | // 并且 `template` 选项用于 renderer 时, 47 | // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。 48 | context.state = store.state 49 | 50 | // Promise 应该 resolve 应用程序实例,以便它可以渲染 51 | resolve(app) 52 | 53 | }).catch(reject) 54 | }, reject) 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /lesson7/src/entry-server.js: -------------------------------------------------------------------------------- 1 | import { createApp } from './app' 2 | 3 | const isDev = process.env.NODE_ENV !== 'production' 4 | 5 | // This exported function will be called by `bundleRenderer`. 6 | // This is where we perform data-prefetching to determine the 7 | // state of our application before actually rendering it. 8 | // Since data fetching is async, this function is expected to 9 | // return a Promise that resolves to the app instance. 10 | export default context => { 11 | // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise, 12 | // 以便服务器能够等待所有的内容在渲染前, 13 | // 就已经准备就绪。 14 | return new Promise((resolve, reject) => { 15 | const s = isDev && Date.now() 16 | const { app, router, store } = createApp() 17 | 18 | const { url } = context 19 | const { fullPath } = router.resolve(url).route 20 | 21 | if(fullPath !== url) { 22 | return reject({ url: fullPath}) 23 | } 24 | 25 | // 设置服务器端 router 的位置 26 | router.push(context.url) 27 | 28 | // 等到 router 将可能的异步组件和钩子函数解析完 29 | router.onReady(() => { 30 | const matchedComponents = router.getMatchedComponents() 31 | // 匹配不到的路由,执行 reject 函数,并返回 404 32 | if (!matchedComponents.length) { 33 | return reject({ code: 404 }) 34 | } 35 | 36 | // 对所有匹配的路由组件调用 `asyncData()`,获取,处理数据,然后存在 vuex 37 | // 供客户端使用 38 | Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({ 39 | store, 40 | route: router.currentRoute 41 | }))).then(() => { 42 | isDev && console.log(`data pre-fetch: ${Date.now() - s}ms`) 43 | // 在所有预取钩子(preFetch hook) resolve 后, 44 | // 我们的 store 现在已经填充入渲染应用程序所需的状态。 45 | // 当我们将状态附加到上下文, 46 | // 并且 `template` 选项用于 renderer 时, 47 | // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。 48 | context.state = store.state 49 | 50 | // Promise 应该 resolve 应用程序实例,以便它可以渲染 51 | resolve(app) 52 | 53 | }).catch(reject) 54 | }, reject) 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /lesson6/src/entry-client.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { createApp } from './app' 3 | import ProgressBar from './components/progressbar.vue' 4 | 5 | // global progress bar 6 | const bar = Vue.prototype.$bar = new Vue(ProgressBar).$mount() 7 | document.body.appendChild(bar.$el) 8 | 9 | // 客户端特定引导逻辑…… 10 | 11 | // a global mixin that calls `asyncData` when a route component's params change 12 | Vue.mixin({ 13 | beforeRouteUpdate (to, from, next) { 14 | const { asyncData } = this.$options 15 | if (asyncData) { 16 | asyncData({ 17 | store: this.$store, 18 | route: to 19 | }).then(next).catch(next) 20 | } else { 21 | next() 22 | } 23 | } 24 | }) 25 | 26 | const { app, router, store } = createApp() 27 | 28 | // 当使用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态, 29 | // 自动嵌入到最终的 HTML 中。 30 | // 而在客户端,在挂载到应用程序之前,store 就应该获取到状态: 31 | if(window.__INITIAL_STATE__) { 32 | store.replaceState(window.__INITIAL_STATE__) 33 | } 34 | 35 | router.onReady(() => { 36 | // 添加路由钩子函数,用于处理 asyncData. 37 | // 在初始路由 resolve 后执行,以便我们不会二次预取(double-fetch)已有的数据。 38 | // 使用 `router.beforeResolve()`,以便确保所有异步组件都 resolve。 39 | router.beforeResolve((to, from, next) => { 40 | const matched = router.getMatchedComponents(to) 41 | const prevMatched = router.getMatchedComponents(from) 42 | 43 | // 我们只关心非预渲染的组件 44 | // 所以我们对比它们,找出两个匹配列表的差异组件 45 | let diffed = false 46 | const activated = matched.filter((c, i) => { 47 | return diffed || (diffed = (prevMatched[i] !== c )) 48 | }) 49 | const asyncDataHooks = activated.map(c => c.asyncData).filter(_ => _) 50 | 51 | if(!asyncDataHooks.length) { 52 | return next() 53 | } 54 | 55 | bar.start() 56 | 57 | Promise.all(asyncDataHooks.map(hook => hook({ store, route: to }))) 58 | .then(() => { 59 | bar.finish() 60 | next() 61 | }).catch(next) 62 | }) 63 | 64 | // actually mount to DOM 65 | // 这里假定 App.vue 模板中根元素具有 `id="app"` 66 | app.$mount('#app') 67 | }) 68 | -------------------------------------------------------------------------------- /lesson7/src/entry-client.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { createApp } from './app' 3 | import ProgressBar from './components/progressbar.vue' 4 | 5 | // global progress bar 6 | const bar = Vue.prototype.$bar = new Vue(ProgressBar).$mount() 7 | document.body.appendChild(bar.$el) 8 | 9 | // 客户端特定引导逻辑…… 10 | 11 | // a global mixin that calls `asyncData` when a route component's params change 12 | Vue.mixin({ 13 | beforeRouteUpdate (to, from, next) { 14 | const { asyncData } = this.$options 15 | if (asyncData) { 16 | asyncData({ 17 | store: this.$store, 18 | route: to 19 | }).then(next).catch(next) 20 | } else { 21 | next() 22 | } 23 | } 24 | }) 25 | 26 | const { app, router, store } = createApp() 27 | 28 | // 当使用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态, 29 | // 自动嵌入到最终的 HTML 中。 30 | // 而在客户端,在挂载到应用程序之前,store 就应该获取到状态: 31 | if(window.__INITIAL_STATE__) { 32 | store.replaceState(window.__INITIAL_STATE__) 33 | } 34 | 35 | router.onReady(() => { 36 | // 添加路由钩子函数,用于处理 asyncData. 37 | // 在初始路由 resolve 后执行,以便我们不会二次预取(double-fetch)已有的数据。 38 | // 使用 `router.beforeResolve()`,以便确保所有异步组件都 resolve。 39 | router.beforeResolve((to, from, next) => { 40 | const matched = router.getMatchedComponents(to) 41 | const prevMatched = router.getMatchedComponents(from) 42 | 43 | // 我们只关心非预渲染的组件 44 | // 所以我们对比它们,找出两个匹配列表的差异组件 45 | let diffed = false 46 | const activated = matched.filter((c, i) => { 47 | return diffed || (diffed = (prevMatched[i] !== c )) 48 | }) 49 | const asyncDataHooks = activated.map(c => c.asyncData).filter(_ => _) 50 | 51 | if(!asyncDataHooks.length) { 52 | return next() 53 | } 54 | 55 | bar.start() 56 | 57 | Promise.all(asyncDataHooks.map(hook => hook({ store, route: to }))) 58 | .then(() => { 59 | bar.finish() 60 | next() 61 | }).catch(next) 62 | }) 63 | 64 | // actually mount to DOM 65 | // 这里假定 App.vue 模板中根元素具有 `id="app"` 66 | app.$mount('#app') 67 | }) 68 | -------------------------------------------------------------------------------- /lesson7/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lesson7", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "dev": "npm start", 8 | "start": "nodemon server.js", 9 | "server": "cross-env NODE_ENV=production node server", 10 | "build": "rimraf dist && npm run build:client && npm run build:server", 11 | "build:client": "cross-env NODE_ENV=production webpack --color --config build/webpack.client.config.js --progress --hide-modules", 12 | "build:server": "cross-env NODE_ENV=production webpack --color --config build/webpack.server.config.js --progress --hide-modules" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "dependencies": { 18 | "compression": "^1.7.1", 19 | "cross-env": "^5.1.1", 20 | "lru-cache": "^4.1.1", 21 | "route-cache": "0.4.3", 22 | "serve-favicon": "^2.4.5", 23 | "express": "^4.16.4", 24 | "extract-text-webpack-plugin": "^3.0.2", 25 | "nodemon": "^1.18.10", 26 | "vue": "^2.6.7", 27 | "vue-router": "^3.0.1", 28 | "vue-server-renderer": "^2.6.7", 29 | "vuex": "^3.0.1", 30 | "vuex-router-sync": "^5.0.0" 31 | }, 32 | "devDependencies": { 33 | "autoprefixer": "^7.1.6", 34 | "babel-core": "^6.26.0", 35 | "babel-loader": "^7.1.2", 36 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 37 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 38 | "babel-preset-env": "^1.6.1", 39 | "chokidar": "^1.7.0", 40 | "css-loader": "^0.28.7", 41 | "file-loader": "^1.1.5", 42 | "friendly-errors-webpack-plugin": "^1.6.1", 43 | "rimraf": "^2.6.2", 44 | "stylus": "^0.54.5", 45 | "stylus-loader": "^3.0.1", 46 | "sw-precache-webpack-plugin": "^0.11.4", 47 | "url-loader": "^0.6.2", 48 | "vue-loader": "^15.3.0", 49 | "vue-template-compiler": "^2.5.22", 50 | "webpack": "^3.8.1", 51 | "webpack-dev-middleware": "^1.12.0", 52 | "webpack-hot-middleware": "^2.20.0", 53 | "webpack-merge": "^4.2.1", 54 | "webpack-node-externals": "^1.7.2" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lesson6/src/api/detail.js: -------------------------------------------------------------------------------- 1 | const projectList = [ 2 | { 3 | id: 1, 4 | name: 'blog', 5 | address: 'https://neveryu.github.io/index.html', 6 | author: 'Yu', 7 | description: '使用GitPage搭建的博客' 8 | }, 9 | { 10 | id: 2, 11 | name: '用vue写的一个外卖app', 12 | address: 'https://git.io/fhpw4', 13 | author: 'Yu', 14 | description: 'vue构建的移动端外卖app' 15 | }, 16 | { 17 | id: 3, 18 | name: '用vue构建的一个后台管理系统', 19 | address: 'https://git.io/fp9UM', 20 | author: 'Yu', 21 | description: 'vue构建的一个后台管理系统' 22 | }, 23 | { 24 | id: 4, 25 | name: '用vue写的一个音乐app', 26 | address: 'https://git.io/fhnor', 27 | author: 'Yu', 28 | description: 'vue构建的移动端音乐app' 29 | }, 30 | { 31 | id: 5, 32 | name: 'vue的预渲染实例', 33 | address: 'https://git.io/fp8xw', 34 | author: 'Yu', 35 | description: '一个vue预渲染示例' 36 | }, 37 | { 38 | id: 6, 39 | name: 'vue服务端渲染实例', 40 | address: 'https://github.com/Neveryu/vue-ssr-lessons', 41 | author: 'Yu', 42 | description: 'vue ssr 实例' 43 | }, 44 | { 45 | id: 7, 46 | name: 'CSDN', 47 | address: 'https://blog.csdn.net/csdn_yudong', 48 | author: 'Yu', 49 | description: '我的csdn博客' 50 | }, 51 | { 52 | id: 8, 53 | name: '我的主页', 54 | address: 'https://neveryu.github.io/neveryu/index.html', 55 | author: 'Yu', 56 | description: '个人主页' 57 | }, 58 | { 59 | id: 9, 60 | name: '我的 GitHub', 61 | address: 'https://github.com/Neveryu', 62 | author: 'Yu', 63 | description: '托管一些我的项目代码' 64 | } 65 | ] 66 | 67 | export function getProjectList() { 68 | return new Promise((resolve, reject) => { 69 | setTimeout(resolve, 1000, projectList) 70 | }) 71 | } 72 | 73 | export function getItem(id) { 74 | let Item = {} 75 | Item.total = projectList.length 76 | Item.item = {} 77 | for(let i = 0; i < projectList.length; i++) { 78 | if(projectList[i].id === parseInt(id)) { 79 | Item.item = projectList[i] 80 | break 81 | } 82 | } 83 | return new Promise((resolve, reject) => { 84 | setTimeout(resolve, 1000, Item) 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /lesson7/src/api/detail.js: -------------------------------------------------------------------------------- 1 | const projectList = [ 2 | { 3 | id: 1, 4 | name: 'blog', 5 | address: 'https://neveryu.github.io/index.html', 6 | author: 'Yu', 7 | description: '使用GitPage搭建的博客' 8 | }, 9 | { 10 | id: 2, 11 | name: '用vue写的一个外卖app', 12 | address: 'https://git.io/fhpw4', 13 | author: 'Yu', 14 | description: 'vue构建的移动端外卖app' 15 | }, 16 | { 17 | id: 3, 18 | name: '用vue构建的一个后台管理系统', 19 | address: 'https://git.io/fp9UM', 20 | author: 'Yu', 21 | description: 'vue构建的一个后台管理系统' 22 | }, 23 | { 24 | id: 4, 25 | name: '用vue写的一个音乐app', 26 | address: 'https://git.io/fhnor', 27 | author: 'Yu', 28 | description: 'vue构建的移动端音乐app' 29 | }, 30 | { 31 | id: 5, 32 | name: 'vue的预渲染实例', 33 | address: 'https://git.io/fp8xw', 34 | author: 'Yu', 35 | description: '一个vue预渲染示例' 36 | }, 37 | { 38 | id: 6, 39 | name: 'vue服务端渲染实例', 40 | address: 'https://github.com/Neveryu/vue-ssr-lessons', 41 | author: 'Yu', 42 | description: 'vue ssr 实例' 43 | }, 44 | { 45 | id: 7, 46 | name: 'CSDN', 47 | address: 'https://blog.csdn.net/csdn_yudong', 48 | author: 'Yu', 49 | description: '我的csdn博客' 50 | }, 51 | { 52 | id: 8, 53 | name: '我的主页', 54 | address: 'https://neveryu.github.io/neveryu/index.html', 55 | author: 'Yu', 56 | description: '个人主页' 57 | }, 58 | { 59 | id: 9, 60 | name: '我的 GitHub', 61 | address: 'https://github.com/Neveryu', 62 | author: 'Yu', 63 | description: '托管一些我的项目代码' 64 | } 65 | ] 66 | 67 | export function getProjectList() { 68 | return new Promise((resolve, reject) => { 69 | setTimeout(resolve, 1000, projectList) 70 | }) 71 | } 72 | 73 | export function getItem(id) { 74 | let Item = {} 75 | Item.total = projectList.length 76 | Item.item = {} 77 | for(let i = 0; i < projectList.length; i++) { 78 | if(projectList[i].id === parseInt(id)) { 79 | Item.item = projectList[i] 80 | break 81 | } 82 | } 83 | return new Promise((resolve, reject) => { 84 | setTimeout(resolve, 1000, Item) 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /lesson6/README.md: -------------------------------------------------------------------------------- 1 | **内容**:这一节实现【数据】 2 | 3 | 我们知道,服务端渲染最重要的部分就是“数据”了。既然是服务端渲染,那么就应该是服务端将运算处理后的数据填到应用程序中,然后将整个应用程序返回到前端。所以**在服务端渲染过程之前,需要先预取和解析好这些数据。** 4 | 5 | 另一个需要关注的问题是在客户端,在挂载 (mount) 到客户端应用程序之前,需要获取到与服务器端应用程序完全相同的数据 - 否则,客户端应用程序会因为使用与服务器端应用程序不同的状态,然后导致混合失败。 6 | 7 | 【在这里需要好好的思考一下,vue 的服务端渲染是如何实现的?如何理解“客户端应用程序会因为使用与服务器端应用程序不同的状态,然后导致混合失败”这句话?第 4 节中的 vue 服务端渲染结构图是否理解清楚了?】 8 | 9 | 我们在服务端预取和解析数据的时候,将这些数据填充到“状态容器(state container)”中,然后客户端也使用这里面的数据,就可以保证客户端与服务器端拥有相同的状态了。【状态容器如何实现?】 10 | 11 | 我们使用 vuex 来作为“状态容器”,此外,我们将在 HTML 中序列化(serialize)和内联预置(inline)状态。这样,在挂载(mount)到客户端应用程序之前,可以直接从 store 获取到内联预置(inline)状态。 12 | 13 | 课程 6 的运行步骤: 14 | ``` bash 15 | cd lesson6 16 | npm i 17 | npm start 18 | ``` 19 | 浏览器访问 [http://localhost:8888](http://localhost:8888) 20 | 21 | 这一节增加的主要内容有: 22 | ``` 23 | 1、创建 store 实例,app.js 中引入 24 | 2、在 路由组件 中放置数据预取逻辑,暴露出一个自定义静态函数 asyncData 。【哪些是路由组件?】 25 | 3、entry-server.js 中,我们可以通过路由获得与 router.getMatchedComponents() 相匹配的组件, 26 | 如果组件暴露出 asyncData,我们就调用这个方法。然后我们需要将解析完成的状态,附加到渲染上下文(render context)中。 27 | 4、然后通过 window.__INITIAL_STATE__ 将服务端 store 内容同步到客户端。 28 | ``` 29 | 如何同步的? 30 | ```js 31 | // entry-server.js 32 | // 当我们将状态附加到上下文, 33 | // 并且 `template` 选项用于 renderer 时, 34 | // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。 35 | context.state = store.state 36 | ``` 37 | 然后 38 | ``` 39 | 5、【客户端数据预取】在 entry-client.js 中 40 | ``` 41 | ```js 42 | // entry-client.js 43 | // 当使用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态, 44 | // 自动嵌入到最终的 HTML 中。 45 | // 而在客户端,在挂载到应用程序之前,store 就应该获取到状态: 46 | if(window.__INITIAL_STATE__) { 47 | store.replaceState(window.__INITIAL_STATE__) 48 | } 49 | ``` 50 | ``` 51 | 6、这样就实现了客户端应用程序与服务器端应用程序保持相同的状态。 52 | 7、ok,现在客户端的 store 里面有了和服务端一样的数据了,怎么取出来用呢? 53 | 8、客户端数据预取 54 | 1.在路由导航之前解析数据 55 | 2.匹配要渲染的视图后,再获取数据 56 | 9、上面两种方案,二选一(我选用的是第一种) 57 | ``` 58 | 但无论你选哪一种,当路由组件重用(同一路由,但是 `params` 或 `query` 已更改,例如,从 `user/1` 到 `user/2`)时,也应该调用 `asyncData` 函数。我们也可以通过纯客户端 (`client-only`) 的全局 `mixin` 来处理这个问题: 59 | ```js 60 | // entry-client.js 61 | Vue.mixin({ 62 | beforeRouteUpdate (to, from, next) { 63 | const { asyncData } = this.$options 64 | if (asyncData) { 65 | asyncData({ 66 | store: this.$store, 67 | route: to 68 | }).then(next).catch(next) 69 | } else { 70 | next() 71 | } 72 | } 73 | }) 74 | ``` 75 | 76 | 以上就是本次更新内容的说明,看起来要写很多代码!这是因为通用数据预取可能是服务器渲染应用程序中最复杂的问题。 -------------------------------------------------------------------------------- /lesson4/build/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 5 | const { VueLoaderPlugin } = require('vue-loader') 6 | 7 | const isProd = process.env.NODE_ENV === 'production' 8 | 9 | module.exports = { 10 | devtool: isProd ? false : '#cheap-module-source-map', 11 | output: { 12 | path: path.resolve(__dirname, '../dist'), 13 | publicPath: '/dist/', 14 | filename: '[name].[chunkhash].js' 15 | }, 16 | resolve: { 17 | alias: { 18 | 'public': path.resolve(__dirname, '../public') 19 | } 20 | }, 21 | module: { 22 | noParse: /es6-promise\.js$/, // avoid webpack shimming process 23 | rules: [ 24 | { 25 | test: /\.vue$/, 26 | loader: 'vue-loader', 27 | options: { 28 | compilerOptions: { 29 | preserveWhitespace: false 30 | } 31 | } 32 | }, 33 | { 34 | test: /\.js$/, 35 | loader: 'babel-loader', 36 | exclude: /node_modules/ 37 | }, 38 | { 39 | test: /\.(png|jpg|gif|svg)$/, 40 | loader: 'url-loader', 41 | options: { 42 | limit: 10000, 43 | name: '[name].[ext]?[hash]' 44 | } 45 | }, 46 | { 47 | test: /\.styl(us)?$/, 48 | use: isProd 49 | ? ExtractTextPlugin.extract({ 50 | use: [ 51 | { 52 | loader: 'css-loader', 53 | options: { minimize: true } 54 | }, 55 | 'stylus-loader' 56 | ], 57 | fallback: 'vue-style-loader' 58 | }) 59 | : ['vue-style-loader', 'css-loader', 'stylus-loader'] 60 | }, 61 | ] 62 | }, 63 | performance: { 64 | hints: false 65 | }, 66 | plugins: isProd 67 | ? [ 68 | new VueLoaderPlugin(), 69 | new webpack.optimize.UglifyJsPlugin({ 70 | compress: { warnings: false } 71 | }), 72 | new webpack.optimize.ModuleConcatenationPlugin(), 73 | new ExtractTextPlugin({ 74 | filename: 'common.[chunkhash].css' 75 | }) 76 | ] 77 | : [ 78 | new VueLoaderPlugin(), 79 | new FriendlyErrorsPlugin() 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /lesson5/build/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 5 | const { VueLoaderPlugin } = require('vue-loader') 6 | 7 | const isProd = process.env.NODE_ENV === 'production' 8 | 9 | module.exports = { 10 | devtool: isProd ? false : '#cheap-module-source-map', 11 | output: { 12 | path: path.resolve(__dirname, '../dist'), 13 | publicPath: '/dist/', 14 | filename: '[name].[chunkhash].js' 15 | }, 16 | resolve: { 17 | alias: { 18 | 'public': path.resolve(__dirname, '../public') 19 | } 20 | }, 21 | module: { 22 | noParse: /es6-promise\.js$/, // avoid webpack shimming process 23 | rules: [ 24 | { 25 | test: /\.vue$/, 26 | loader: 'vue-loader', 27 | options: { 28 | compilerOptions: { 29 | preserveWhitespace: false 30 | } 31 | } 32 | }, 33 | { 34 | test: /\.js$/, 35 | loader: 'babel-loader', 36 | exclude: /node_modules/ 37 | }, 38 | { 39 | test: /\.(png|jpg|gif|svg)$/, 40 | loader: 'url-loader', 41 | options: { 42 | limit: 10000, 43 | name: '[name].[ext]?[hash]' 44 | } 45 | }, 46 | { 47 | test: /\.styl(us)?$/, 48 | use: isProd 49 | ? ExtractTextPlugin.extract({ 50 | use: [ 51 | { 52 | loader: 'css-loader', 53 | options: { minimize: true } 54 | }, 55 | 'stylus-loader' 56 | ], 57 | fallback: 'vue-style-loader' 58 | }) 59 | : ['vue-style-loader', 'css-loader', 'stylus-loader'] 60 | }, 61 | ] 62 | }, 63 | performance: { 64 | hints: false 65 | }, 66 | plugins: isProd 67 | ? [ 68 | new VueLoaderPlugin(), 69 | new webpack.optimize.UglifyJsPlugin({ 70 | compress: { warnings: false } 71 | }), 72 | new webpack.optimize.ModuleConcatenationPlugin(), 73 | new ExtractTextPlugin({ 74 | filename: 'common.[chunkhash].css' 75 | }) 76 | ] 77 | : [ 78 | new VueLoaderPlugin(), 79 | new FriendlyErrorsPlugin() 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /lesson6/build/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 5 | const { VueLoaderPlugin } = require('vue-loader') 6 | 7 | const isProd = process.env.NODE_ENV === 'production' 8 | 9 | module.exports = { 10 | devtool: isProd ? false : '#cheap-module-source-map', 11 | output: { 12 | path: path.resolve(__dirname, '../dist'), 13 | publicPath: '/dist/', 14 | filename: '[name].[chunkhash].js' 15 | }, 16 | resolve: { 17 | alias: { 18 | 'public': path.resolve(__dirname, '../public') 19 | } 20 | }, 21 | module: { 22 | noParse: /es6-promise\.js$/, // avoid webpack shimming process 23 | rules: [ 24 | { 25 | test: /\.vue$/, 26 | loader: 'vue-loader', 27 | options: { 28 | compilerOptions: { 29 | preserveWhitespace: false 30 | } 31 | } 32 | }, 33 | { 34 | test: /\.js$/, 35 | loader: 'babel-loader', 36 | exclude: /node_modules/ 37 | }, 38 | { 39 | test: /\.(png|jpg|gif|svg)$/, 40 | loader: 'url-loader', 41 | options: { 42 | limit: 10000, 43 | name: '[name].[ext]?[hash]' 44 | } 45 | }, 46 | { 47 | test: /\.styl(us)?$/, 48 | use: isProd 49 | ? ExtractTextPlugin.extract({ 50 | use: [ 51 | { 52 | loader: 'css-loader', 53 | options: { minimize: true } 54 | }, 55 | 'stylus-loader' 56 | ], 57 | fallback: 'vue-style-loader' 58 | }) 59 | : ['vue-style-loader', 'css-loader', 'stylus-loader'] 60 | }, 61 | ] 62 | }, 63 | performance: { 64 | hints: false 65 | }, 66 | plugins: isProd 67 | ? [ 68 | new VueLoaderPlugin(), 69 | new webpack.optimize.UglifyJsPlugin({ 70 | compress: { warnings: false } 71 | }), 72 | new webpack.optimize.ModuleConcatenationPlugin(), 73 | new ExtractTextPlugin({ 74 | filename: 'common.[chunkhash].css' 75 | }) 76 | ] 77 | : [ 78 | new VueLoaderPlugin(), 79 | new FriendlyErrorsPlugin() 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /lesson7/build/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 5 | const { VueLoaderPlugin } = require('vue-loader') 6 | 7 | const isProd = process.env.NODE_ENV === 'production' 8 | 9 | module.exports = { 10 | devtool: isProd ? false : '#cheap-module-source-map', 11 | output: { 12 | path: path.resolve(__dirname, '../dist'), 13 | publicPath: '/dist/', 14 | filename: '[name].[chunkhash].js' 15 | }, 16 | resolve: { 17 | alias: { 18 | 'public': path.resolve(__dirname, '../public') 19 | } 20 | }, 21 | module: { 22 | noParse: /es6-promise\.js$/, // avoid webpack shimming process 23 | rules: [ 24 | { 25 | test: /\.vue$/, 26 | loader: 'vue-loader', 27 | options: { 28 | compilerOptions: { 29 | preserveWhitespace: false 30 | } 31 | } 32 | }, 33 | { 34 | test: /\.js$/, 35 | loader: 'babel-loader', 36 | exclude: /node_modules/ 37 | }, 38 | { 39 | test: /\.(png|jpg|gif|svg)$/, 40 | loader: 'url-loader', 41 | options: { 42 | limit: 10000, 43 | name: '[name].[ext]?[hash]' 44 | } 45 | }, 46 | { 47 | test: /\.styl(us)?$/, 48 | use: isProd 49 | ? ExtractTextPlugin.extract({ 50 | use: [ 51 | { 52 | loader: 'css-loader', 53 | options: { minimize: true } 54 | }, 55 | 'stylus-loader' 56 | ], 57 | fallback: 'vue-style-loader' 58 | }) 59 | : ['vue-style-loader', 'css-loader', 'stylus-loader'] 60 | }, 61 | ] 62 | }, 63 | performance: { 64 | hints: false 65 | }, 66 | plugins: isProd 67 | ? [ 68 | new VueLoaderPlugin(), 69 | new webpack.optimize.UglifyJsPlugin({ 70 | compress: { warnings: false } 71 | }), 72 | new webpack.optimize.ModuleConcatenationPlugin(), 73 | new ExtractTextPlugin({ 74 | filename: 'common.[chunkhash].css' 75 | }) 76 | ] 77 | : [ 78 | new VueLoaderPlugin(), 79 | new FriendlyErrorsPlugin() 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /lesson6/src/components/progressbar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 88 | 89 | -------------------------------------------------------------------------------- /lesson7/src/components/progressbar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 88 | 89 | -------------------------------------------------------------------------------- /lesson4/build/setup-dev-server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const MFS = require('memory-fs') 4 | const webpack = require('webpack') 5 | const chokidar = require('chokidar') 6 | const clientConfig = require('./webpack.client.config') 7 | const serverConfig = require('./webpack.server.config') 8 | 9 | const readFile = (fs, file) => { 10 | try { 11 | return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8') 12 | } catch(e) {} 13 | } 14 | 15 | module.exports = function setupDevServer(app, templatePath, cb) { 16 | let bundle 17 | let template 18 | let clientManifest 19 | 20 | let ready 21 | const readyPromise = new Promise(r => { ready = r }) 22 | const update = () => { 23 | if(bundle && clientManifest) { 24 | ready() 25 | cb(bundle, { 26 | template, 27 | clientManifest 28 | }) 29 | } 30 | } 31 | 32 | // read template from disk and watch 33 | template = fs.readFileSync(templatePath, 'utf-8') 34 | chokidar.watch(templatePath).on('change', () => { 35 | template = fs.readFileSync(templatePath, 'utf-8') 36 | console.log('index.html template updated.') 37 | update() 38 | }) 39 | 40 | // modify client config to work with hot middleware 41 | clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app] 42 | clientConfig.output.filename = '[name].js' 43 | clientConfig.plugins.push( 44 | new webpack.HotModuleReplacementPlugin(), 45 | new webpack.NoEmitOnErrorsPlugin() 46 | ) 47 | 48 | // dev middleware 49 | const clientCompiler = webpack(clientConfig) 50 | const devMiddleware = require('webpack-dev-middleware')(clientCompiler, { 51 | publicPath: clientConfig.output.publicPath, 52 | noInfo: true 53 | }) 54 | app.use(devMiddleware) 55 | clientCompiler.plugin('done', stats => { 56 | stats = stats.toJson() 57 | stats.errors.forEach(err => console.error(err)) 58 | stats.warnings.forEach(err => console.warn(err)) 59 | if(stats.errors.length) { 60 | return 61 | } 62 | clientManifest = JSON.parse(readFile( 63 | devMiddleware.fileSystem, 64 | 'vue-ssr-client-manifest.json' 65 | )) 66 | update() 67 | }) 68 | 69 | // hot middleware 70 | app.use(require('webpack-hot-middleware')(clientCompiler, { heartbeat: 5000 })) 71 | 72 | // watch and update server renderer 73 | const serverCompiler = webpack(serverConfig) 74 | const mfs = new MFS() 75 | serverCompiler.outputFileSystem = mfs 76 | serverCompiler.watch({}, (err, stats) => { 77 | if(err) { 78 | throw err 79 | } 80 | stats = stats.toJson() 81 | if(stats.errors.length) { 82 | return 83 | } 84 | 85 | // read bundle generated by vue-ssr-webpack-plugin 86 | bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json')) 87 | update() 88 | }) 89 | 90 | return readyPromise 91 | } 92 | -------------------------------------------------------------------------------- /lesson5/build/setup-dev-server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const MFS = require('memory-fs') 4 | const webpack = require('webpack') 5 | const chokidar = require('chokidar') 6 | const clientConfig = require('./webpack.client.config') 7 | const serverConfig = require('./webpack.server.config') 8 | 9 | const readFile = (fs, file) => { 10 | try { 11 | return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8') 12 | } catch(e) {} 13 | } 14 | 15 | module.exports = function setupDevServer(app, templatePath, cb) { 16 | let bundle 17 | let template 18 | let clientManifest 19 | 20 | let ready 21 | const readyPromise = new Promise(r => { ready = r }) 22 | const update = () => { 23 | if(bundle && clientManifest) { 24 | ready() 25 | cb(bundle, { 26 | template, 27 | clientManifest 28 | }) 29 | } 30 | } 31 | 32 | // read template from disk and watch 33 | template = fs.readFileSync(templatePath, 'utf-8') 34 | chokidar.watch(templatePath).on('change', () => { 35 | template = fs.readFileSync(templatePath, 'utf-8') 36 | console.log('index.html template updated.') 37 | update() 38 | }) 39 | 40 | // modify client config to work with hot middleware 41 | clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app] 42 | clientConfig.output.filename = '[name].js' 43 | clientConfig.plugins.push( 44 | new webpack.HotModuleReplacementPlugin(), 45 | new webpack.NoEmitOnErrorsPlugin() 46 | ) 47 | 48 | // dev middleware 49 | const clientCompiler = webpack(clientConfig) 50 | const devMiddleware = require('webpack-dev-middleware')(clientCompiler, { 51 | publicPath: clientConfig.output.publicPath, 52 | noInfo: true 53 | }) 54 | app.use(devMiddleware) 55 | clientCompiler.plugin('done', stats => { 56 | stats = stats.toJson() 57 | stats.errors.forEach(err => console.error(err)) 58 | stats.warnings.forEach(err => console.warn(err)) 59 | if(stats.errors.length) { 60 | return 61 | } 62 | clientManifest = JSON.parse(readFile( 63 | devMiddleware.fileSystem, 64 | 'vue-ssr-client-manifest.json' 65 | )) 66 | update() 67 | }) 68 | 69 | // hot middleware 70 | app.use(require('webpack-hot-middleware')(clientCompiler, { heartbeat: 5000 })) 71 | 72 | // watch and update server renderer 73 | const serverCompiler = webpack(serverConfig) 74 | const mfs = new MFS() 75 | serverCompiler.outputFileSystem = mfs 76 | serverCompiler.watch({}, (err, stats) => { 77 | if(err) { 78 | throw err 79 | } 80 | stats = stats.toJson() 81 | if(stats.errors.length) { 82 | return 83 | } 84 | 85 | // read bundle generated by vue-ssr-webpack-plugin 86 | bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json')) 87 | update() 88 | }) 89 | 90 | return readyPromise 91 | } 92 | -------------------------------------------------------------------------------- /lesson6/build/setup-dev-server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const MFS = require('memory-fs') 4 | const webpack = require('webpack') 5 | const chokidar = require('chokidar') 6 | const clientConfig = require('./webpack.client.config') 7 | const serverConfig = require('./webpack.server.config') 8 | 9 | const readFile = (fs, file) => { 10 | try { 11 | return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8') 12 | } catch(e) {} 13 | } 14 | 15 | module.exports = function setupDevServer(app, templatePath, cb) { 16 | let bundle 17 | let template 18 | let clientManifest 19 | 20 | let ready 21 | const readyPromise = new Promise(r => { ready = r }) 22 | const update = () => { 23 | if(bundle && clientManifest) { 24 | ready() 25 | cb(bundle, { 26 | template, 27 | clientManifest 28 | }) 29 | } 30 | } 31 | 32 | // read template from disk and watch 33 | template = fs.readFileSync(templatePath, 'utf-8') 34 | chokidar.watch(templatePath).on('change', () => { 35 | template = fs.readFileSync(templatePath, 'utf-8') 36 | console.log('index.html template updated.') 37 | update() 38 | }) 39 | 40 | // modify client config to work with hot middleware 41 | clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app] 42 | clientConfig.output.filename = '[name].js' 43 | clientConfig.plugins.push( 44 | new webpack.HotModuleReplacementPlugin(), 45 | new webpack.NoEmitOnErrorsPlugin() 46 | ) 47 | 48 | // dev middleware 49 | const clientCompiler = webpack(clientConfig) 50 | const devMiddleware = require('webpack-dev-middleware')(clientCompiler, { 51 | publicPath: clientConfig.output.publicPath, 52 | noInfo: true 53 | }) 54 | app.use(devMiddleware) 55 | clientCompiler.plugin('done', stats => { 56 | stats = stats.toJson() 57 | stats.errors.forEach(err => console.error(err)) 58 | stats.warnings.forEach(err => console.warn(err)) 59 | if(stats.errors.length) { 60 | return 61 | } 62 | clientManifest = JSON.parse(readFile( 63 | devMiddleware.fileSystem, 64 | 'vue-ssr-client-manifest.json' 65 | )) 66 | update() 67 | }) 68 | 69 | // hot middleware 70 | app.use(require('webpack-hot-middleware')(clientCompiler, { heartbeat: 5000 })) 71 | 72 | // watch and update server renderer 73 | const serverCompiler = webpack(serverConfig) 74 | const mfs = new MFS() 75 | serverCompiler.outputFileSystem = mfs 76 | serverCompiler.watch({}, (err, stats) => { 77 | if(err) { 78 | throw err 79 | } 80 | stats = stats.toJson() 81 | if(stats.errors.length) { 82 | return 83 | } 84 | 85 | // read bundle generated by vue-ssr-webpack-plugin 86 | bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json')) 87 | update() 88 | }) 89 | 90 | return readyPromise 91 | } 92 | -------------------------------------------------------------------------------- /lesson7/build/setup-dev-server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const MFS = require('memory-fs') 4 | const webpack = require('webpack') 5 | const chokidar = require('chokidar') 6 | const clientConfig = require('./webpack.client.config') 7 | const serverConfig = require('./webpack.server.config') 8 | 9 | const readFile = (fs, file) => { 10 | try { 11 | return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8') 12 | } catch(e) {} 13 | } 14 | 15 | module.exports = function setupDevServer(app, templatePath, cb) { 16 | let bundle 17 | let template 18 | let clientManifest 19 | 20 | let ready 21 | const readyPromise = new Promise(r => { ready = r }) 22 | const update = () => { 23 | if(bundle && clientManifest) { 24 | ready() 25 | cb(bundle, { 26 | template, 27 | clientManifest 28 | }) 29 | } 30 | } 31 | 32 | // read template from disk and watch 33 | template = fs.readFileSync(templatePath, 'utf-8') 34 | chokidar.watch(templatePath).on('change', () => { 35 | template = fs.readFileSync(templatePath, 'utf-8') 36 | console.log('index.html template updated.') 37 | update() 38 | }) 39 | 40 | // modify client config to work with hot middleware 41 | clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app] 42 | clientConfig.output.filename = '[name].js' 43 | clientConfig.plugins.push( 44 | new webpack.HotModuleReplacementPlugin(), 45 | new webpack.NoEmitOnErrorsPlugin() 46 | ) 47 | 48 | // dev middleware 49 | const clientCompiler = webpack(clientConfig) 50 | const devMiddleware = require('webpack-dev-middleware')(clientCompiler, { 51 | publicPath: clientConfig.output.publicPath, 52 | noInfo: true 53 | }) 54 | app.use(devMiddleware) 55 | clientCompiler.plugin('done', stats => { 56 | stats = stats.toJson() 57 | stats.errors.forEach(err => console.error(err)) 58 | stats.warnings.forEach(err => console.warn(err)) 59 | if(stats.errors.length) { 60 | return 61 | } 62 | clientManifest = JSON.parse(readFile( 63 | devMiddleware.fileSystem, 64 | 'vue-ssr-client-manifest.json' 65 | )) 66 | update() 67 | }) 68 | 69 | // hot middleware 70 | app.use(require('webpack-hot-middleware')(clientCompiler, { heartbeat: 5000 })) 71 | 72 | // watch and update server renderer 73 | const serverCompiler = webpack(serverConfig) 74 | const mfs = new MFS() 75 | serverCompiler.outputFileSystem = mfs 76 | serverCompiler.watch({}, (err, stats) => { 77 | if(err) { 78 | throw err 79 | } 80 | stats = stats.toJson() 81 | if(stats.errors.length) { 82 | return 83 | } 84 | 85 | // read bundle generated by vue-ssr-webpack-plugin 86 | bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json')) 87 | update() 88 | }) 89 | 90 | return readyPromise 91 | } 92 | -------------------------------------------------------------------------------- /lesson4/src/app.vue: -------------------------------------------------------------------------------- 1 | 38 | 49 | 63 | 64 | -------------------------------------------------------------------------------- /lesson4/src/components/footer.vue: -------------------------------------------------------------------------------- 1 | 40 | 48 | 108 | -------------------------------------------------------------------------------- /lesson5/src/components/footer.vue: -------------------------------------------------------------------------------- 1 | 40 | 48 | 108 | -------------------------------------------------------------------------------- /lesson6/src/components/footer.vue: -------------------------------------------------------------------------------- 1 | 40 | 48 | 108 | -------------------------------------------------------------------------------- /lesson7/src/components/footer.vue: -------------------------------------------------------------------------------- 1 | 40 | 48 | 108 | -------------------------------------------------------------------------------- /lesson6/src/views/detail.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 76 | 77 | 117 | -------------------------------------------------------------------------------- /lesson7/src/views/detail.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 80 | 81 | 121 | -------------------------------------------------------------------------------- /lesson4/server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const express = require('express') 4 | const resolve = file => path.resolve(__dirname, file) 5 | const { createBundleRenderer } = require('vue-server-renderer') 6 | 7 | const isProd = process.env.NODE_ENV === 'production' 8 | 9 | const serverInfo = 10 | `express/${require('express/package.json').version} ` + 11 | `vue-server-renderer/${require('vue-server-renderer/package.json').version} ` 12 | 13 | const app = express() 14 | 15 | function createRenderer(bundle, options) { 16 | return createBundleRenderer(bundle, Object.assign(options, { 17 | // this is only needed when vue-server-renderer is npm-linked 18 | basedir: resolve('./dist'), 19 | // recommended for performance 20 | runInNewContext: false 21 | })) 22 | } 23 | 24 | let renderer 25 | let readyPromise 26 | const templatePath = resolve('./src/index.template.html') 27 | 28 | if(isProd) { 29 | // In production: create server renderer using template and built server bundle. 30 | // The server bundle is generated by vue-ssr-webpack-plugin. 31 | const template = fs.readFileSync(templatePath, 'utf-8') 32 | const bundle = require('./dist/vue-ssr-server-bundle.json') 33 | // The client manifest are optional, but it allows the renderer 34 | // to automatically infer preload/prefetch links and directly add