├── .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 |
2 |
3 |
在这一节中,会加入 vue-router 。
4 |
与上一节的变动,主要是:
5 |
6 |
1、添加 .babelrc 配置文件,主要配置了 es6 的 import() 函数,以及结构运算符。
7 |
2、引入 vue-router,创建 router 目录,新建一个 router 实例。
8 |
3、类似于 createApp,我们也需要给每个请求一个新的 router 实例,所以在 router.js 文件中,我们导出一个 createRouter 函数,然后在通用入口 app.js 中,引入 vue 中以及输出包含路由的应用程序。
9 |
4、与此同时,我们要简单的更新我们的 entry-client.js 和 entry-server.js 文件,加入路由的部分。
10 |
11 |
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 |
2 |
3 |
4 | -
5 | {{ item.name }}
6 |
7 |
8 |
9 |
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 |
2 |
3 |
4 | -
5 | {{ item.name }}
6 |
7 |
8 |
9 |
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 |
2 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/lesson6/src/views/project.vue:
--------------------------------------------------------------------------------
1 |
2 |
24 |
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 |
2 |
24 |
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 |
2 |
3 |
这是 HOME 页
4 |
本页面包含的核心关键数据,是服务端计算得出,然后附加到当前渲染上下文中;不是前端通过 ajax 请求获取的。
5 |
6 | - 注册总人数: {{ totalRegister }}
7 | - 登录激活总人数: {{ totalActiver }}
8 | - 最高月活人数: {{ topMouthActiver }}
9 | - 今日登录人数: {{ todayLogin }}
10 |
11 |
12 |
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 |
2 |
3 |
这是 HOME 页
4 |
本页面包含的核心关键数据,是服务端计算得出,然后附加到当前渲染上下文中;不是前端通过 ajax 请求获取的。
5 |
6 | - 注册总人数: {{ totalRegister }}
7 | - 登录激活总人数: {{ totalActiver }}
8 | - 最高月活人数: {{ topMouthActiver }}
9 | - 今日登录人数: {{ todayLogin }}
10 |
11 |
12 |
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 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
21 |
35 |
36 |
--------------------------------------------------------------------------------
/lesson6/src/app.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
22 |
36 |
37 |
--------------------------------------------------------------------------------
/lesson7/src/app.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
4 |
10 |
11 |
12 |
88 |
89 |
--------------------------------------------------------------------------------
/lesson7/src/components/progressbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
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 |
2 |
3 |
6 |
7 |
29 |
30 | 如基本示例所示,我们为每个请求创建一个新的根 Vue 实例。这与每个用户在自己的浏览器中使用新应用程序的实例类似。如果我们在多个请求之间使用一个共享的实例,很容易导致交叉请求状态污染 (cross-request state pollution)。
31 |
32 | 因此,我们不应该直接创建一个应用程序实例,而是应该暴露一个可以重复执行的工厂函数,为每个请求创建新的应用程序实例(app.js)
33 |
34 |
35 |
36 |
37 |
38 |
49 |
63 |
64 |
--------------------------------------------------------------------------------
/lesson4/src/components/footer.vue:
--------------------------------------------------------------------------------
1 |
2 |
39 |
40 |
48 |
108 |
--------------------------------------------------------------------------------
/lesson5/src/components/footer.vue:
--------------------------------------------------------------------------------
1 |
2 |
39 |
40 |
48 |
108 |
--------------------------------------------------------------------------------
/lesson6/src/components/footer.vue:
--------------------------------------------------------------------------------
1 |
2 |
39 |
40 |
48 |
108 |
--------------------------------------------------------------------------------
/lesson7/src/components/footer.vue:
--------------------------------------------------------------------------------
1 |
2 |
39 |
40 |
48 |
108 |
--------------------------------------------------------------------------------
/lesson6/src/views/detail.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 | 名称
6 | 结果
7 |
8 | -
9 |
10 | {{key}}
11 | {{value}}
12 |
13 |
14 | {{key}}
15 | {{value}}
16 |
17 |
18 | -
19 | 暂无数据
20 | 暂无数据
21 |
22 |
23 |
24 |
25 | 第一页
26 | 上一页
27 |
28 | 下一页
29 | 最后一页
30 |
31 |
32 |
33 |
34 |
76 |
77 |
117 |
--------------------------------------------------------------------------------
/lesson7/src/views/detail.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 | 名称
6 | 结果
7 |
8 | -
9 |
10 | {{key}}
11 | {{value}}
12 |
13 |
14 | {{key}}
15 | {{value}}
16 |
17 |
18 | -
19 | 暂无数据
20 | 暂无数据
21 |
22 |
23 |
24 |
25 | 第一页
26 | 上一页
27 |
28 | 下一页
29 | 最后一页
30 |
31 |
32 |
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