├── 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 |
2 |
3 | 小书
4 |
5 |
--------------------------------------------------------------------------------
/src/components/test.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | 1111111111
4 |
5 |
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 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/src/components/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
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 |
2 |
3 | -
4 |
5 |
30 |
31 |
![]()
32 |
33 |
34 |
35 |
36 |
37 |
49 |
99 |
--------------------------------------------------------------------------------
/src/pages/login/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |

6 |
7 |
8 |
登陆
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | 提交
20 |
21 |
22 |
23 |
24 |
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 |
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 |
2 |
3 |
4 |
5 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
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 |
2 |
3 | -
4 |
5 |
36 |
37 |
![]()
38 |
39 |
40 |
41 |
42 |
43 |
63 |
132 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |

7 |
13 |
14 | 登录
15 | ·
16 | 注册
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
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