├── src ├── store │ ├── getter.js │ ├── modules │ │ ├── user.js │ │ └── app.js │ └── index.js ├── pages │ ├── books │ │ └── index.vue │ ├── index │ │ ├── CreateListView.js │ │ ├── top.vue │ │ ├── index.vue │ │ └── special_column.vue │ └── login │ │ └── index.vue ├── components │ ├── test.vue │ ├── about.vue │ └── index.vue ├── util │ ├── title-mixin.js │ ├── filters.js │ ├── request.js │ └── util.js ├── index.template.html ├── app.js ├── styles │ └── style.stylus ├── entry-server.js ├── entry-client.js ├── router │ └── index.js ├── api │ └── index.js └── App.vue ├── public ├── normal.png ├── logo-48.png ├── logo-98.png ├── apijson.json ├── collect.svg ├── share.svg ├── comment.svg └── like.svg ├── .babelrc ├── manifest.json ├── LICENSE ├── package.json ├── README.md └── server.js /src/store/getter.js: -------------------------------------------------------------------------------- 1 | export default { 2 | } -------------------------------------------------------------------------------- /public/normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wodb/Vue-SSR-Demo/HEAD/public/normal.png -------------------------------------------------------------------------------- /public/logo-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wodb/Vue-SSR-Demo/HEAD/public/logo-48.png -------------------------------------------------------------------------------- /public/logo-98.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wodb/Vue-SSR-Demo/HEAD/public/logo-98.png -------------------------------------------------------------------------------- /src/pages/books/index.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/test.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /public/apijson.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"name":"a","id":1}, 3 | {"name":"b","id":2}, 4 | {"name":"c","id":3}, 5 | {"name":"d","id":4}, 6 | {"name":"e","id":5}, 7 | {"name":"f","id":6} 8 | ] -------------------------------------------------------------------------------- /public/collect.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "modules": false }] 4 | ], 5 | "plugins": [ 6 | "syntax-dynamic-import", 7 | "transform-object-rest-spread", 8 | [ 9 | "component", 10 | { 11 | "libraryName": "element-ui", 12 | "styleLibraryName": "theme-chalk" 13 | } 14 | ] 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | const user = { 2 | state:{ 3 | userInfo:null 4 | }, 5 | mutations:{ 6 | setUser:(state,userInfo) => { 7 | state.userInfo = userInfo 8 | }, 9 | }, 10 | actions:{ 11 | getUser({commit,state}) { 12 | 13 | }, 14 | } 15 | } 16 | 17 | export default user -------------------------------------------------------------------------------- /public/share.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import user from './modules/user' 5 | import app from './modules/app' 6 | import getters from './getter' 7 | 8 | Vue.use(Vuex) 9 | 10 | export default () => { 11 | return new Vuex.Store({ 12 | modules:{ 13 | user, 14 | app 15 | }, 16 | getters, 17 | strict: process.env.NODE_ENV !== 'production' 18 | }) 19 | } -------------------------------------------------------------------------------- /public/comment.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/index/CreateListView.js: -------------------------------------------------------------------------------- 1 | import Top from './top.vue' 2 | import Special_column from "./special_column.vue"; 3 | 4 | export default (type) => { 5 | return { 6 | asyncData({ store, router }) { 7 | return store.dispatch('FETCH_INDEX_LIST_BY_TYPE', { type }) 8 | }, 9 | render(h) { 10 | if (type == 'top') { 11 | return h(Top, { props: { type } }) 12 | } 13 | return h(Special_column, { props: { type } }) 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/components/about.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/components/index.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/util/title-mixin.js: -------------------------------------------------------------------------------- 1 | const 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 = title 13 | } 14 | } 15 | } 16 | 17 | const clientTitleMixin = { 18 | mounted() { 19 | const title = getTitle(this) 20 | if (title) { 21 | document.title = title 22 | } 23 | } 24 | } 25 | 26 | export default process.env.VUE_ENV == 'server' ? serverTitleMixin : clientTitleMixin -------------------------------------------------------------------------------- /public/like.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/util/filters.js: -------------------------------------------------------------------------------- 1 | export const formatter = (v) => { 2 | let timeDistance = (new Date().getTime() - new Date(v).getTime()) / 1000 3 | if ((timeDistance / 60) < 1) { 4 | return `1分钟前` 5 | } else if ((timeDistance / 60) < 60) { 6 | return `${Math.floor((timeDistance / 60))}分钟前` 7 | } else if ((timeDistance / 60 / 60) < 24) { 8 | return `${Math.floor((timeDistance / 60 / 60))}小时前` 9 | } else if ((timeDistance / 60 / 60 / 24) < 30) { 10 | return `${Math.floor((timeDistance / 60 / 60 / 24))}天前` 11 | } else if ((timeDistance / 60 / 60 / 24 / 30) < 12) { 12 | return `${Math.floor((timeDistance / 60 / 60 / 24 / 30))}月前` 13 | } else { 14 | return `${Math.floor((timeDistance / 60 / 60 / 24 / 30 / 12))}年前` 15 | } 16 | } -------------------------------------------------------------------------------- /src/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | /*这是一个工厂函数导出app的实例*/ 2 | import Vue from 'vue' 3 | import ElementUI from 'element-ui' 4 | import createRouter from './router/index' 5 | import App from './App.vue' 6 | import createStore from './store/index' 7 | import { sync } from 'vuex-router-sync' 8 | import * as filters from '@/util/filters' 9 | import titleMixin from './util/title-mixin' 10 | import 'element-ui/lib/theme-chalk/index.css' 11 | import './styles/style.stylus' 12 | 13 | Object.keys(filters).forEach((key) => { 14 | Vue.filter(key,filters[key]) 15 | }) 16 | 17 | Vue.mixin(titleMixin) 18 | 19 | Vue.use(ElementUI, { size: 'small' }) 20 | 21 | 22 | export function createApp() { 23 | 24 | const router = createRouter() 25 | const store = createStore() 26 | 27 | sync(store,router) 28 | const app = new Vue({ 29 | router, 30 | store, 31 | render: (h) => h(App) 32 | }) 33 | return {app, router, store} 34 | } -------------------------------------------------------------------------------- /src/util/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export default { 4 | api:null, // 当做API方法 5 | config:{ 6 | cookie:'' 7 | }, // 这里存放cookie和一些配置 8 | setCookie(cookie) { 9 | this.config.cookie = cookie 10 | }, 11 | createApi(config) { 12 | this.config = config 13 | 14 | const service = axios.create({ 15 | baseURL:'http://test.mac.com', 16 | timeout:10000, 17 | headers:{ 18 | // token:this.config.cookie 19 | } 20 | }) 21 | 22 | service.interceptors.request.use(config => { 23 | 24 | console.log(`request.js config`,this.config) 25 | return config 26 | }, error => { 27 | console.log(`for debug request`,error) 28 | return Promise.reject(error) 29 | }) 30 | 31 | service.interceptors.response.use(response => { 32 | if (response.data.s != 1) { 33 | return Promise.reject(response.data.m) 34 | } 35 | return response.data 36 | }, error => { 37 | return Promise.reject(error) 38 | }) 39 | 40 | this.api = service 41 | } 42 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vue Hackernews 2.0", 3 | "short_name": "Vue HN", 4 | "icons": [{ 5 | "src": "/public/logo-120.png", 6 | "sizes": "120x120", 7 | "type": "image/png" 8 | }, { 9 | "src": "/public/logo-144.png", 10 | "sizes": "144x144", 11 | "type": "image/png" 12 | }, { 13 | "src": "/public/logo-152.png", 14 | "sizes": "152x152", 15 | "type": "image/png" 16 | }, { 17 | "src": "/public/logo-192.png", 18 | "sizes": "192x192", 19 | "type": "image/png" 20 | }, { 21 | "src": "/public/logo-256.png", 22 | "sizes": "256x256", 23 | "type": "image/png" 24 | }, { 25 | "src": "/public/logo-384.png", 26 | "sizes": "384x384", 27 | "type": "image/png" 28 | }, { 29 | "src": "/public/logo-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png" 32 | }], 33 | "start_url": "/", 34 | "background_color": "#f2f3f5", 35 | "display": "standalone", 36 | "theme_color": "#f60" 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/styles/style.stylus: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | b, u, i, center, 7 | dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, embed, 11 | figure, figcaption, footer, header, hgroup, 12 | menu, nav, output, ruby, section, summary, 13 | time, mark, audio, video 14 | margin: 0; 15 | padding: 0; 16 | border: 0; 17 | font-size: 100%; 18 | font: inherit; 19 | vertical-align: baseline; 20 | color #333; 21 | 22 | /* HTML5 display-role reset for older browsers */ 23 | article, aside, details, figcaption, figure, 24 | footer, header, hgroup, menu, nav, section 25 | display: block; 26 | 27 | body 28 | line-height: 1; 29 | background-color #f4f5f5; 30 | 31 | img 32 | width 100% 33 | height 100% 34 | 35 | ol, ul 36 | list-style: none; 37 | 38 | blockquote, q 39 | quotes: none; 40 | 41 | blockquote:before, blockquote:after, 42 | q:before, q:after 43 | content: ''; 44 | content: none; 45 | 46 | table 47 | border-collapse: collapse; 48 | border-spacing: 0; 49 | a 50 | text-decoration: none; 51 | cursor: pointer; 52 | color: #909090; 53 | 54 | .bg-white 55 | background-color:#fff; -------------------------------------------------------------------------------- /src/entry-server.js: -------------------------------------------------------------------------------- 1 | import {createApp} from './app' 2 | import requrest from '@/util/request' 3 | 4 | export default (context) => { 5 | 6 | return new Promise((resolve,reject) => { 7 | const {app,router,store} = createApp() 8 | 9 | router.push(context.url) 10 | 11 | router.onReady(() => { 12 | const matcheds = router.getMatchedComponents() 13 | 14 | if (!matcheds.length) return reject({s:404}) 15 | 16 | console.log(`entry-server len`,matcheds.length) 17 | 18 | // set cookie 19 | requrest.createApi({cookie:context.cookie}) 20 | 21 | Promise.all(matcheds.map(Component => { 22 | if (Component.asyncData) { 23 | return Component.asyncData({ 24 | store, 25 | route: router.currentRoute 26 | }) 27 | } 28 | })) 29 | .then(() =>{ 30 | // 在所有预取钩子(preFetch hook) resolve 后, 31 | // 我们的 store 现在已经填充入渲染应用程序所需的状态。 32 | // 当我们将状态附加到上下文, 33 | // 并且 `template` 选项用于 renderer 时, 34 | // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。 35 | context.state = store.state 36 | 37 | resolve(app) 38 | }) 39 | .catch(err => { 40 | return reject({s:500,m:err.message}) 41 | }) 42 | },reject) 43 | }) 44 | } -------------------------------------------------------------------------------- /src/entry-client.js: -------------------------------------------------------------------------------- 1 | import {createApp} from './app' 2 | import NProgress from 'nprogress' 3 | import requrest from '@/util/request' 4 | import { getCookie } from '@/util/util' 5 | 6 | import 'nprogress/nprogress.css' // progress bar style 7 | 8 | const {app, router, store} = createApp() 9 | 10 | NProgress.configure({ easing: 'ease', speed: 500 }) 11 | 12 | if (window.__INITIAL_STATE__) { 13 | store.replaceState(window.__INITIAL_STATE__) 14 | } 15 | 16 | // set cookie 17 | requrest.createApi({cookie: getCookie('token')}) 18 | 19 | 20 | router.onReady(() => { 21 | router.beforeResolve((to, from, next) => { 22 | // start progress 23 | NProgress.inc() 24 | 25 | const matched = router.getMatchedComponents(to) 26 | const prevMatched = router.getMatchedComponents(from) 27 | 28 | // 我们只关心非预渲染的组件 29 | // 所以我们对比它们,找出两个匹配列表的差异组件 30 | // ????????????????? 同父不同子 31 | let diffed = false 32 | const activated = matched.filter((c, i) => { 33 | return diffed || (diffed = (prevMatched[i] !== c)) 34 | }) 35 | 36 | if (!activated.length) { 37 | return next() 38 | } 39 | // 这里如果有加载指示器(loading indicator),就触发 40 | 41 | Promise.all(activated.map(c => { 42 | if (c.asyncData) { 43 | return c.asyncData({store, route: to}) 44 | } 45 | })).then(() => { 46 | // 停止加载指示器(loading indicator) 47 | NProgress.done() 48 | next() 49 | }).catch(next) 50 | }) 51 | // 挂载vue实例 52 | app.$mount('#app') 53 | }) 54 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VueRouter from "vue-router"; 3 | 4 | Vue.use(VueRouter) 5 | 6 | const CreateListView = (id) => () => import('@/pages/index/CreateListView').then(m => m.default(id)) 7 | 8 | export default () => { 9 | const router = new VueRouter({ 10 | mode: 'history', 11 | scrollBehavior(to, from, savedPosition) { 12 | return { x: 0, y: 0 } 13 | }, 14 | routes: [ 15 | { 16 | path: '/index', 17 | component: () => import('@/pages/index/index.vue'), 18 | children: [ 19 | { path: '', component: CreateListView('top') }, 20 | { path: 'frontend', component: CreateListView('frontend') }, 21 | { path: 'Andriod', component: CreateListView('Andriod') }, 22 | { path: 'backend', component: CreateListView('backend') }, 23 | { path: 'ai', component: CreateListView('ai') }, 24 | { path: 'IOS', component: CreateListView('IOS') }, 25 | { path: 'freebie', component: CreateListView('freebie') }, 26 | { path: 'article', component: CreateListView('article') }, 27 | { path: 'devops', component: CreateListView('devops') }, 28 | { path: '*', redirect: '/index' } 29 | ] 30 | }, 31 | { path: '/books', component: () => import('@/pages/books/index.vue') }, 32 | { path: '/about', component: () => import('@/components/about.vue') }, 33 | { path: '*', redirect: '/index' } 34 | ] 35 | }) 36 | return router 37 | } -------------------------------------------------------------------------------- /src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import { fetchIndexTags, fetchEntriesByType, fetchRecommendByType } from '@/api/index' 2 | 3 | const app = { 4 | state: { 5 | activeType: null, // 当前类型 6 | indexTags: [], // 当前页面的属性 7 | indexList: { // 当前页面的列表 8 | top: [], // 推荐 9 | frontend: [], // 前端 10 | Andriod: [], // 安卓 11 | backend: [], // 后端 12 | ai: [], // 人工智能 13 | IOS: [], // IOS 14 | freebie: [], // 工具资源 15 | article: [], // 阅读 16 | devops: [], // 运维 17 | }, 18 | }, 19 | mutations: { 20 | SET_ACTIVE_TYPE(state, { type }) { 21 | state.activeType = type 22 | }, 23 | SET_INDEX_TAGS(state, payload) { 24 | state.indexTags = payload 25 | }, 26 | SET_INDEX_LIST(state, { type, data }) { 27 | state.indexList[type] = state.indexList[type].concat(data) 28 | } 29 | }, 30 | actions: { 31 | FETCH_ACTIVE_TYPE(context, { type }) { 32 | context.commit('SET_ACTIVE_TYPE', { 33 | type 34 | }) 35 | }, 36 | ENSURE_ACTIVE_ITEMS({ commit, dispatch, getters }) { 37 | return dispatch('FETCH_INDEX_TAGS') 38 | .then(res => { 39 | dispatch('FETCH_ACTIVE_TYPE', { type: res[0].attr }) 40 | }) 41 | }, 42 | FETCH_INDEX_TAGS(context) { 43 | return fetchIndexTags() 44 | .then(res => { 45 | context.commit('SET_INDEX_TAGS', res) 46 | return res 47 | }) 48 | }, 49 | FETCH_INDEX_LIST_BY_TYPE({ commit }, { type, token }) { 50 | let p = type == 'top' ? fetchRecommendByType(type) : fetchEntriesByType(type) 51 | 52 | return p 53 | .then(res => { 54 | commit('SET_INDEX_LIST', { type, data: res }) 55 | }) 56 | } 57 | } 58 | } 59 | 60 | export default app -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssr", 3 | "description": "vue-ssr project", 4 | "author": "", 5 | "private": true, 6 | "scripts": { 7 | "dev": "node server", 8 | "start": "cross-env NODE_ENV=production node server", 9 | "build": "rimraf dist && npm run build:client && npm run build:server", 10 | "build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js --progress --hide-modules", 11 | "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js --progress --hide-modules" 12 | }, 13 | "engines": { 14 | "node": ">=7.0", 15 | "npm": ">=4.0" 16 | }, 17 | "dependencies": { 18 | "axios": "^0.18.0", 19 | "compression": "^1.7.1", 20 | "cross-env": "^5.1.1", 21 | "element-ui": "^2.4.5", 22 | "es6-promise": "^4.1.1", 23 | "express": "^4.16.2", 24 | "extract-text-webpack-plugin": "^3.0.2", 25 | "firebase": "4.6.2", 26 | "lru-cache": "^4.1.1", 27 | "nprogress": "^0.2.0", 28 | "route-cache": "0.4.3", 29 | "serve-favicon": "^2.4.5", 30 | "vue": "^2.5.16", 31 | "vue-router": "^3.0.1", 32 | "vue-server-renderer": "^2.5.16", 33 | "vuex": "^3.0.1", 34 | "vuex-router-sync": "^5.0.0" 35 | }, 36 | "devDependencies": { 37 | "autoprefixer": "^7.1.6", 38 | "babel-core": "^6.26.0", 39 | "babel-loader": "^7.1.2", 40 | "babel-plugin-component": "^1.1.1", 41 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 42 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 43 | "babel-preset-env": "^1.6.1", 44 | "chokidar": "^1.7.0", 45 | "css-loader": "^0.28.7", 46 | "file-loader": "^1.1.5", 47 | "friendly-errors-webpack-plugin": "^1.6.1", 48 | "mockjs": "^1.0.1-beta3", 49 | "rimraf": "^2.6.2", 50 | "stylus": "^0.54.5", 51 | "stylus-loader": "^3.0.1", 52 | "sw-precache-webpack-plugin": "^0.11.4", 53 | "url-loader": "^0.6.2", 54 | "vue-loader": "^15.0.0-beta.1", 55 | "vue-template-compiler": "^2.5.16", 56 | "webpack": "^3.8.1", 57 | "webpack-dev-middleware": "^1.12.0", 58 | "webpack-hot-middleware": "^2.20.0", 59 | "webpack-merge": "^4.1.1", 60 | "webpack-node-externals": "^1.6.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | import request from '@/util/request' 2 | 3 | const data = [ 4 | { id: 0, text: '推荐', attr: '' }, 5 | { id: 1, text: '前端', attr: '/frontend' }, 6 | { id: 2, text: 'Andriod', attr: '/Andriod' }, 7 | { id: 3, text: '后端', attr: '/backend' }, 8 | { id: 4, text: '人工智能', attr: '/ai' }, 9 | { id: 5, text: 'IOS', attr: '/IOS' }, 10 | { id: 6, text: '工具资源', attr: '/freebie' }, 11 | { id: 7, text: '阅读', attr: '/article' }, 12 | { id: 8, text: '运维', attr: '/devops' }] 13 | 14 | const delay = (resfun, timer) => setTimeout(() => resfun(), timer) 15 | 16 | // 首页tag 17 | export const fetchIndexTags = () => new Promise((resolve, reject) => delay(() => resolve(data), 200)) 18 | 19 | // 首页list 20 | export const fetchEntriesByType = (type) => { 21 | let category = '' 22 | switch (type) { 23 | case 'frontend': 24 | category = '5562b415e4b00c57d9b94ac8' 25 | break 26 | case 'Andriod': 27 | category = '5562b410e4b00c57d9b94a92' 28 | break 29 | case 'backend': 30 | category = '5562b419e4b00c57d9b94ae2' 31 | break 32 | case 'ai': 33 | category = '57be7c18128fe1005fa902de' 34 | break 35 | case 'IOS': 36 | category = '5562b405e4b00c57d9b94a41' 37 | break 38 | case 'freebie': 39 | category = '5562b422e4b00c57d9b94b53' 40 | break 41 | case 'article': 42 | category = '5562b428e4b00c57d9b94b9d' 43 | break 44 | case 'devops': 45 | category = '5b34a478e1382338991dd3c1' 46 | } 47 | let params = { 48 | category: category, 49 | ab: 'welcome_3', 50 | before: Math.random(), 51 | src: 'web' 52 | } 53 | 54 | return request.api.get('/timeline/get_entry_by_rank', { 55 | params 56 | }).then(res => res.d.entrylist) 57 | } 58 | 59 | export const fetchRecommendByType = (type) => { 60 | let params = { 61 | suid: 'nvfBZaZ2jjZyffaZqJAN', 62 | ab: 'welcome_3', 63 | src: 'web' 64 | } 65 | return request.api.get('/recommender/get_recommended_entry', { 66 | params 67 | }).then(res => res.d) 68 | } -------------------------------------------------------------------------------- /src/pages/index/top.vue: -------------------------------------------------------------------------------- 1 | 37 | 49 | 99 | -------------------------------------------------------------------------------- /src/pages/login/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Features 2 | 3 | > Note: in practice, it is unnecessary to code-split for an app of this size (where each async chunk is only a few kilobytes), nor is it optimal to extract an extra CSS file (which is only 1kb) -- they are used simply because this is a demo app showcasing all the supported features. In real apps, you should always measure and optimize based on your actual app constraints. 4 | 5 | - Server Side Rendering 6 | - Vue + vue-router + vuex working together 7 | - Server-side data pre-fetching 8 | - Client-side state & DOM hydration 9 | - Automatically inlines CSS used by rendered components only 10 | - Preload / prefetch resource hints 11 | - Route-level code splitting 12 | - Progressive Web App 13 | - App manifest 14 | - Service worker 15 | - 100/100 Lighthouse score 16 | - Single-file Vue Components 17 | - Hot-reload in development 18 | - CSS extraction for production 19 | - Animation 20 | - Effects when switching route views 21 | - Real-time list updates with FLIP Animation 22 | 23 | ## Architecture Overview 24 | 25 | screen shot 2016-08-11 at 6 06 57 pm 26 | 27 | **A detailed Vue SSR guide can be found [here](https://ssr.vuejs.org).** 28 | 29 | ## Build Setup 30 | 31 | **Requires Node.js 7+** 32 | 33 | ``` bash 34 | # install dependencies 35 | npm install # or yarn 36 | 37 | # serve in dev mode, with hot reload at localhost:8080 38 | npm run dev 39 | 40 | # build for production 41 | npm run build 42 | 43 | # serve in production mode 44 | npm start 45 | ``` 46 | 47 | ## nginx-proxy 48 | **本地host 指向自己的ip地址 test.mac.com** 49 | 50 | ``` 51 | worker_processes 1; 52 | events { 53 | worker_connections 1024; 54 | } 55 | 56 | http { 57 | include mime.types; 58 | default_type application/octet-stream; 59 | log_format main '$request_time $remote_addr - $remote_user [$time_local] ' 60 | 'fwf[$http_x_forwarded_for] tip[$http_true_client_ip] ' 61 | '$upstream_addr $upstream_response_time $request_time ' 62 | '$http_host $request ' 63 | '"$status" $body_bytes_sent "$http_referer" ' 64 | '"$http_accept_language" "$http_user_agent" '; 65 | access_log logs/access.log main; 66 | sendfile on; 67 | keepalive_timeout 65; 68 | add_header Access-Control-Allow-Origin *; 69 | add_header Access-Control-Allow-Headers X-Requested-With; 70 | add_header Access-Control-Allow-Methods GET,POST,OPTIONS; 71 | server { 72 | listen 80; 73 | server_name test.mac.com; 74 | location / { 75 | proxy_pass https://recommender-api-ms.juejin.im; 76 | proxy_redirect off; 77 | client_max_body_size 10m; 78 | client_body_buffer_size 128k; 79 | proxy_connect_timeout 10; 80 | proxy_send_timeout 10; 81 | proxy_read_timeout 10; 82 | proxy_buffer_size 4k; 83 | proxy_buffers 32 1024k; 84 | proxy_busy_buffers_size 2048k; 85 | proxy_temp_file_write_size 1024k; 86 | } 87 | 88 | error_page 500 502 503 504 /50x.html; 89 | location = /50x.html { 90 | root html; 91 | } 92 | } 93 | } 94 | 95 | ``` 96 | 97 | ## License 98 | 99 | MIT 100 | -------------------------------------------------------------------------------- /src/pages/index/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 77 | -------------------------------------------------------------------------------- /src/util/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 防抖动 3 | * @param {Function} fn [description] 4 | * @param {[type]} delay [description] 5 | * @return {[type]} [description] 6 | */ 7 | export function debounce(fn, delay) { 8 | let timer = null 9 | return function() { 10 | let context = this 11 | let args = arguments 12 | 13 | clearTimeout(timer) 14 | timer = setTimeout(function() { 15 | fn.apply(context, args) 16 | }, delay) 17 | } 18 | } 19 | /** 20 | * 节流阀 21 | * @param {[type]} func [description] 22 | * @param {[type]} wait [description] 23 | * @param {[type]} options [description] 24 | * @return {[type]} [description] 25 | */ 26 | export function throttle(func, wait, options) { 27 | let context, args, result 28 | let timeout = null 29 | let previous = 0 30 | if (!options) options = {} 31 | let later = function() { 32 | previous = options.leading === false ? 0 : new Date().getTime() 33 | timeout = null 34 | result = func.apply(context, args) 35 | if (!timeout) context = args = null 36 | } 37 | return function() { 38 | let now = new Date().getTime() 39 | if (!previous && options.leading === false) previous = now 40 | let remaining = wait - (now - previous) 41 | context = this 42 | args = arguments 43 | if (remaining <= 0 || remaining > wait) { 44 | if (timeout) { 45 | clearTimeout(timeout) 46 | timeout = null 47 | } 48 | previous = now 49 | result = func.apply(context, args) 50 | if (!timeout) context = args = null 51 | } else if (!timeout && options.trailing !== false) { 52 | timeout = setTimeout(later, remaining) 53 | } 54 | return result 55 | } 56 | } 57 | 58 | /** 59 | * 设置cookie 60 | * document.cookie = "cookieName=mader; expires=Fri, 31 Dec 2017 15:59:59 GMT; path=/mydir; domain=cnblogs.com; max-age=3600; secure=true"; 61 | * cookieName=mader :name=value,cookie的名称和值 62 | * expires=Fri, 31 Dec 2017 15:59:59 GMT: expires,cookie过期的日期,如果没有定义,cookie会在对话结束时过期。日期格式为 new Date().toUTCString() 63 | * path=/mydir: path=path (例如 '/', '/mydir') 如果没有定义,默认为当前文档位置的路径。 64 | * domain=cnblogs.com: 指定域(例如 'example.com', '.example.com' (包括所有子域名), 'subdomain.example.com') 如果没有定义,默认为当前文档位置的路径的域名部分。 65 | * max-age=3600: 文档被查看后cookie过期时间,单位为秒 66 | * secure=true: cookie只会被https传输 ,即加密的https链接传输 67 | */ 68 | export function setCookie(name, value, day) { 69 | if (day !== 0) { //当设置的时间等于0时,不设置expires属性,cookie在浏览器关闭后删除 70 | let expires = day * 24 * 60 * 60 * 1000 71 | let date = new Date(+new Date() + expires) 72 | document.cookie = name + "=" + escape(value) + ";expires=" + date.toUTCString() + ';path=/' 73 | } else { 74 | document.cookie = name + "=" + escape(value) 75 | } 76 | } 77 | /** 78 | * 获取cookie 79 | * @param {[type]} name [description] 80 | * @return {[type]} [description] 81 | */ 82 | export function getCookie(name) { 83 | let arr, reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)") 84 | if (arr = document.cookie.match(reg)) { 85 | return unescape(arr[2]) 86 | } else { 87 | return '' 88 | } 89 | } 90 | /** 91 | * 删除cookie 92 | * @param {[type]} name [description] 93 | * @return {[type]} [description] 94 | */ 95 | export function removeCookie(name) { 96 | let exp = new Date() 97 | exp.setTime(exp.getTime() - 1) 98 | let cval = getCookie(name) 99 | if (cval != null) 100 | document.cookie = name + "=" + cval + ";expires=" + exp.toGMTString() 101 | } 102 | -------------------------------------------------------------------------------- /src/pages/index/special_column.vue: -------------------------------------------------------------------------------- 1 | 43 | 63 | 132 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 28 | 87 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const LRU = require('lru-cache') 4 | const express = require('express') 5 | const favicon = require('serve-favicon') 6 | const compression = require('compression') 7 | const microcache = require('route-cache') 8 | const resolve = file => path.resolve(__dirname, file) 9 | const { createBundleRenderer } = require('vue-server-renderer') 10 | 11 | const isProd = process.env.NODE_ENV === 'production' 12 | const useMicroCache = process.env.MICRO_CACHE !== 'false' 13 | const serverInfo = 14 | `express/${require('express/package.json').version} ` + 15 | `vue-server-renderer/${require('vue-server-renderer/package.json').version}` 16 | 17 | const app = express() 18 | 19 | function createRenderer (bundle, options) { 20 | // https://github.com/vuejs/vue/blob/dev/packages/vue-server-renderer/README.md#why-use-bundlerenderer 21 | return createBundleRenderer(bundle, Object.assign(options, { 22 | // for component caching 23 | cache: LRU({ 24 | max: 1000, 25 | maxAge: 1000 * 60 * 15 26 | }), 27 | // this is only needed when vue-server-renderer is npm-linked 28 | basedir: resolve('./dist'), 29 | // recommended for performance 30 | runInNewContext: false 31 | })) 32 | } 33 | 34 | let renderer 35 | let readyPromise 36 | const templatePath = resolve('./src/index.template.html') 37 | if (isProd) { 38 | // In production: create server renderer using template and built server bundle. 39 | // The server bundle is generated by vue-ssr-webpack-plugin. 40 | const template = fs.readFileSync(templatePath, 'utf-8') 41 | const bundle = require('./dist/vue-ssr-server-bundle.json') 42 | // The client manifests are optional, but it allows the renderer 43 | // to automatically infer preload/prefetch links and directly add