使用node+vue.js實現SPA應用,解決了SPA應用的最大缺點SEO

業務需求

最近公司要求開發web版的app,因爲app是偏向內容方面,並且帶了一個聊天模塊,因此通常的多頁開發不是很適合,並且主要是手機瀏覽,對加載速度或者用戶體驗來講都比較苛刻。調研了不少框架和模式,最後本身東拼西湊搞出來了這麼一個玩意。javascript

UI框架 :淘寶SUI

服務端

毫無疑問使用node,使用typescript能夠有效的在編碼同時查錯,強類型語言寫服務端毫無壓力。css

#app.ts 只貼重要代碼

var webpack = require('webpack')
var webpackDevMiddleware = require('webpack-dev-middleware')
var WebpackConfig = require('./webpack.config')

import * as index from "./server/routes/index";
import * as cookbook from "./server/routes/cookbook";
import * as cookbookDetail from './server/routes/cookbookDetail'


var app = express();

//啓動服務的時候 打包並監聽客戶端用到的文件,webpackDevMiddleware是開發模式,他會打包js在內存裏面,你改了文件,它也會從新打包
app.use(webpackDevMiddleware(webpack(WebpackConfig), {
    publicPath: '/__build__/',
    stats: {
        colors: true
    }
}));

//通常的配置項
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.set('view options', { layout: false });
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(methodOverride());
app.use(express.static(__dirname + '/public'));

var env = process.env.NODE_ENV || 'development';
if (env === 'development') {
    app.use(errorHandler());
}

//路由配置
app.get('/cookbook', index.index);
app.get('/cookbook/:id', cookbook.index);
app.get('/cookbookDetail/:id', cookbookDetail.index);


app.listen(3000, function(){
    console.log("Demo Express server listening on port %d in %s mode", 3000, app.settings.env);
});

export var App = app;

服務端渲染頁面

#index.ts
import express = require("express")
import vueServer = require("vue-server") //服務端渲染vue的插件
import request = require('request'); //第3方http請求的插件
import queryString = require('querystring'); //轉換get參數的插件


var Vue = new vueServer.renderer(); //建立一個服務端的vue

export function index(req: express.Request, res: express.Response) {

    let vm:vueServer,
    b:Object,
    options:Object;

    options = {
        method: 'GET',
        //隨便用了一個免費的API,是查詢菜譜的
        url: 'http://apis.baidu.com/tngou/cook/classify?'+queryString.stringify({
            id : 0,
        }),
        headers: {
            //百度API的開放接口憑證
            'apikey': 'a369f43a6392605426433831e10765ec'
        }
    };
    request(options,function(err,resp,body){

        if (!err && resp.statusCode == 200) {
            b = JSON.parse(body);
            vm = new Vue({
                replace : false,
                template : `
                <div>
                    <!-- 標題欄 -->
                    <header class="bar bar-nav">
                        <a class="icon icon-me pull-left open-panel"></a>
                        <h1 class="title">{{title}}</h1>
                    </header>

                    <!-- 這裏是頁面內容區 -->
                    <div class="content">
                      <div class="list-block">
                        <ul>
                          <li class="item-content" v-for="item in cookbookClasses">
                            <div class="item-media"><i class="icon icon-f7"></i></div>
                            <div class="item-inner">
                              <div class="item-title">{{item.title}}</div>
                            </div>
                          </li>
                        </ul>
                      </div>
                    </div>
                </div>
                `,
                data : {
                    title : '菜譜首頁',
                    cookbookClasses: b.tngou,
                }
            });
        }
        //等待html渲染完成,再返回給瀏覽器 vueServer.htmlReady是vue-server的自帶事件
        vm.$on('vueServer.htmlReady', function(html:string) {
            //這裏用的是ejs模板 能夠把須要用到的數據設置成window下的全局變量,方便客戶端的js訪問。
            res.render('layout',{
                server_html:html,
                server_data:`
                    window.cm_cookbookClasses = {
                        title : '菜譜首頁',
                        cookbookClasses: ${JSON.stringify(b.tngou)}
                    }`
            })
        });

    });
}
#layout.ejs  訪問這個SPA的全部url返回的都是這個頁面 <meta>標籤均可以動態設置,只要傳參數進來就能夠
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,initial-scale=1.0,user-scalable=no">
    <title>Vue Router Example</title>
    <link rel="stylesheet" href="//g.alicdn.com/msui/sm/0.6.2/css/??sm.min.css,sm-extend.min.css">
    <style type="text/css">
        
        //註釋掉的是vue-router切換的動畫, 經過transition,transition-mode設置
        /*.test-transition {*/
            /*transition: all .5s ease;*/
        /*}*/

        /*.test-enter, .test-leave {*/
            /*opacity: 0;*/
            /*transform: translate3d(10px, 0, 0);*/
        /*}*/
        h2{
            font-size: 1rem;
        }
        p{
            font-size: .8rem;
        }
        img{
            max-width: 100%;
        }
    </style>
    <script>
        //定義一些前端須要用到的全局屬性,文章ID或用戶信息什麼的
        //index.ts中傳過來的是 window.cm_data = {name:"張三"}
        //前端就能訪問到了
        <%-server_data%>
    </script>
</head>
//這裏的id是前端須要用到的一個標識
<body id="app">
<div class="page-group">
    <div class="page page-current">
        //router-view是客戶端vue-router須要解析的dom
        //server_html是根據訪問url地址生成的html,是作SEO的重點,不加載下面的js也能夠看到內容
        <router-view transition="test" transition-mode="out-in"><%-server_html%></router-view>
    </div>
</div>


//這裏用到了淘寶團隊的UI框架SUI,各類組件都有。它依賴zepto
<script type='text/javascript' src='//g.alicdn.com/sj/lib/zepto/zepto.min.js' charset='utf-8'></script>
<script type='text/javascript' src='//g.alicdn.com/msui/sm/0.6.2/js/??sm.min.js,sm-extend.min.js' charset='utf-8'></script>
 //webpack打包好的js,主要是路由配置
<script src="/__build__/app.js"></script>
</body>
</html>

客戶端

#app.js 這個是/__build__/app.js,能夠用es6編寫,webpack會轉換的

import Vue from './vue.min' //客戶端的vue.js
import VueRouter from './vue-router.min' //vue的路由插件,配合webpack能夠很簡單實現懶加載
import VueResource from './lib/vue-resource.min'    //懶加載路由 只有訪問這個路由纔會加載js

import Index from 'bundle?lazy!./routes/index' //配合webpack的bundle-loader,輕鬆實現懶加載
import Cookbook from 'bundle?lazy!./routes/Cookbook'
import CookbookDetail from 'bundle?lazy!./routes/cookbookDetail'

var App = Vue.extend({})

Vue.use(VueResource)
Vue.use(VueRouter)

//百度API須要用到的參數
Vue.http.headers.common['apikey'] = 'a369f43a6392605426433831e10765ec';

var router = new VueRouter({
    //這裏要好好說一下,必定要設置html5模式,否則先後端URL不統一會發生問題
    //好比訪問 http://localhost:3000/ 服務端定義是訪問index.ts這個路由文件
    //若是不是html5模式的話,通過客戶端js運行以後會變成http://localhost:3000/#!/
    
    //在好比直接瀏覽器輸入 http://localhost:3000/foo 服務端定義是訪問.ts這個路由文件
    //若是不是html5模式的話,通過客戶端js運行以後會變成 http://localhost:3000/foo/#!/
    
    //設置了html5模式後,加載完js後不會加上#!這2個相似錨點的字符,實現先後端路由統一若是用戶刷新瀏覽器的話,服務端也能渲染出相應的頁面。
    history: true, //html5模式 去掉錨點 
    saveScrollPosition: true //記住頁面的滾動位置 html5模式適用,實際使用下來沒用
})

//定義路由,要和服務端路由路徑定義的同樣
router.map({
    '/'   : {
        component: Index //前端路由定義,
    },
    '/cookbook/:id': {
        component: Cookbook
    },
    '/cookbookDetail/:id': {
        component: CookbookDetail
    }
})
router.redirect({
    '*': '/cookbook'
})
//啓動APP
router.start(App, '#app')

客戶端路由

#index.js  這裏的模板和服務端的差很少,就增長了@click操做
'use strict';
import Vue from '../lib/vue.min'

let Index = Vue.extend({
    //replace : false, //必須註釋掉 否則動畫失效
    template : `
    <div>
        <!-- 標題欄 -->
        <header class="bar bar-nav">
            <a class="icon icon-me pull-left open-panel"></a>
            <h1 class="title">{{title}}</h1>
        </header>
    
        <!-- 這裏是頁面內容區 -->
        <div class="content">
          <div class="list-block">
            <ul>
              <li class="item-content" v-for="item in cookbookClasses" @click="goCookbook(item.id)">
                <div class="item-media"><i class="icon icon-f7"></i></div>
                <div class="item-inner">
                  <div class="item-title">{{item.title}}</div>
                </div>
              </li>
            </ul>
          </div>
        </div>
    </div>
    `,
    data : ()=>{
        return {
            title : '菜譜首頁',
            cookbookClasses : []
        }
    },
    methods: {
        goCookbook(id){
            //vue-router 路由跳轉
            this.$router.go('/cookbook/'+id);
        }
    },
    //vue-router的屬性,能夠設置路由的生命週期,具體請查文檔
    route : {
        //應該是在渲染DOM以前獲取數據
        data : function(transition) {
            //若是是服務端渲染的,應該設置全局變量,那麼客戶端就不用異步請求數據了
            if(window.cm_cookbookClasses){
                this.$data = window.cm_cookbookClasses;
                transition.next();
            }else{
                let qa_id = 0;
                //使用vue-resource來獲取數據
                var resource = this.$resource('http://apis.baidu.com/tngou/cook/classify');
                $.showPreloader(); //這個是顯示SUI的加載遮罩層
                resource.get({id: qa_id}).then((response)=>{
                    $.hidePreloader();
                    if(response.status == 200){
                        this.$data = {
                            title : '菜譜首頁',
                            cookbookClasses : response.data.tngou
                        }
                        transition.next();
                    }else{
                        transition.abort();
                    }
                });
            }
        },
        canActivate: function(){

        },
        // 激活狀態 把上一次記錄的數據,獲取出來,須要deactivate狀態配合。
        activate: function (transition) {
            this.$data = window.cm_cookbookClasses;
            transition.next()
        },
        // 禁用狀態 記錄這一次的數據,方便之後再進入激活狀態能夠不用訪問網絡請求數據
        deactivate: function (transition) {
            window.cm_cookbookClasses = this.$data;
            transition.next()
        }
    }
})

export default Index

須要完善的地方

  1. 先後端統一模板,已經找到方法了把html分離出來,node端用fs.readFileSync方法獲取,客戶端用webpack的raw-loader獲取html內容html

  2. 安卓微信瀏覽器 vue-resource 設置了headers的apikey,但請求的時候沒有帶上,致使獲取不到數據。前端

  3. IOS safari瀏覽器 渲染頁面有問題,渲染20條數據,只顯示10條左右,監聽不到SUI無限滾動到底部的事件vue

不放源碼都是瞎扯。html5

源碼地址 最新的是分支V0.0.1 是個簡單的菜譜DEMO

https://github.com/yjj5855/node-vue-server-webpackjava

相關文章
相關標籤/搜索