使用Nuxt+Vue+Node構建的SSR博客項目

之前的博客使用的是Ghost,不過被攻擊了,勒索我幾百美圓,仍是算了吧,順便說一句,數據備份很重要!前段時間學了Vue.js,之前看的Node還能記起來點,主要爲了鍛鍊本身吧,此次的博客沒有用Hexo,Ghost什麼的,本身寫的。因爲有SEO的需求,畢竟本身寫本身看也沒什麼意思,最終的使用的技術方案:Vue.js+Nuxt.js+ES6+Webpack+Mysql+Noyde.js+Express.js。動手以前其實還有不少東西不是很熟,不過最終也算是完成了,這個項目給了我一個啓發:實踐是學習的最快途徑,看再屢次文檔真不如動手寫一個項目來的實在。

Vue.js和Nuxt.js

Vue.jscss

當前流行的前端框架,官網地址:https://cn.vuejs.org/,文檔寫的很好,點個贊!前端

Nuxt.jsvue

Nuxt.js的文檔寫的也不錯,話說Vue.js系列的文檔寫的都很好,再次點贊,官網是這樣介紹本身的:node

Nuxt.js 是一個基於 Vue.js 的通用應用框架。

經過對客戶端/服務端基礎架構的抽象組織,Nuxt.js 主要關注的是應用的 UI渲染。webpack

咱們的目標是建立一個靈活的應用框架,你能夠基於它初始化新項目的基礎結構代碼,或者在已有 Node.js 項目中使用 Nuxt.js。ios

Nuxt.js 預設了利用Vue.js開發服務端渲染的應用所須要的各類配置。git

除此以外,咱們還提供了一種命令叫:nuxt generate,爲基於 Vue.js 的應用提供生成對應的靜態站點的功能。github

咱們相信這個命令所提供的功能,是向開發集成各類微服務(miscroservices)的 Web 應用邁開的新一步。web

做爲框架,Nuxt.js 爲 客戶端/服務端 這種典型的應用架構模式提供了許多有用的特性,例如異步數據加載、中間件支持、佈局支持等。sql

總結一下,Nuxt.js就是一個利用Vue, webpack 和 Node.js幫咱們簡單方便實現SSR的框架。

關於SSR

什麼是SSR

SSR是Server Side Render的縮寫,即服務器端渲染。在沒有SPA以前,絕大多數的網頁都是經過服務器渲染生成的:用戶向服務器發送請求,服務器獲取請求,而後再查詢數據庫,根據查詢的數據動態的生成一張網頁,最後將網頁內容返回給瀏覽器端。

如今拿Vue來講,在一般狀況下,Vue.js是運行在瀏覽器中的,在瀏覽器你發送一個請求,而後得到了後臺返回的數據,最後經過Vue.js將數據渲染成須要的HTML片斷。如今,咱們把將組件渲染成HTML這個工做拿到Node上來執行,能夠將Node當作一個隱形的「瀏覽器」,在這個「瀏覽器」中咱們將組件都渲染好,而後將渲染好的HTML直接發送給實際的瀏覽器(客戶端),這就是Vue SSR。

SSR的好處

經過前端框架(Vue,Angular,React)構建的SPA(Single-Page Application - 單頁應用程序),由於內容是經過Ajax獲取的,因此就有一個自然的缺陷,就是搜索引擎無法獲取裏面的內容。右鍵查看一個SPA網頁的源代碼,你會發現你裏面幾乎沒什麼內容。對於像博客,新聞這樣的網站,這一點是不可接收的。

總結起來,SSR帶來的好處就是可以SEO,順便由於內容在服務器端已經渲染好了,還可以減小請求數量,對於一些比較老舊的瀏覽器(Vue.js不支持),也能看到基礎的內容。

開發總結

開發中遇到了很多問題,在這裏我列舉一下,可讓你們儘可能少走彎路,解決方案我儘可能說的簡潔點,有些東西我會單獨開一篇博文。

Restful

項目後臺使用的是Restful API,後臺框架用的是express,你能夠用vue init nuxt-community/express-template <project-name>生成一套基於express的模板文件,後臺的代碼在server目錄,這個目錄裏面就是關於Node和express的內容了,這裏不展開。

asyncData多個請求

參考下面的代碼:

async asyncData({ req, error }) {
    const page = 0
    let [pageRes, countRes] = await Promise.all([
        axios.get(`/api/post/page/${page}?scope=published`),
        axios.get('/api/post/count/published'),
    ])
    return {
        posts: pageRes.data.list,
        count: countRes.data.result,
    }
}

中間件

中間件容許您定義一個自定義函數運行在一個頁面或一組頁面渲染以前。每個中間件應放置在 middleware/ 目錄。文件名的名稱將成爲中間件名稱(middleware/auth.js將成爲 auth 中間件)。下面是一個示例:

import { isLogin } from '../util/assist'
const needAuth = require('../util/api.config').needAuth
export default function ({ isClient, isServer, route, req, res, redirect }) {
    //在服務端判讀是否須要登錄(若是直接輸地址,在客戶端是判斷不到的)
    if (isServer) {
        let cookies = req.cookies
        let path = req.originalUrl;

        if (path.indexOf('admin') > 0 && !cookies.token) {
            redirect('/login')
        }
    }
    //在客戶端判讀是否須要登錄
    if (isClient) {
        if (route.path.indexOf('admin') > 0 && !isLogin()) {
            redirect('login')
        }
    }
}

Node循環+異步問題

在項目裏面有這樣一個需求:顯示文章(Post)對應的若干個標籤(Tag),解決辦法就是獲取PostList,而後循環這個List並獲取PostId,根據PostId去查對應的Tag。因爲獲取PostList異步操做,而後又在循環裏面套了許多異步操做(獲取Tag),因此用回調的方式就無法寫下去了,最後使用的是async這個庫,貼一段代碼:

//獲取Post列表
let list = (params, callback) => {
    postModel.list(params, (err, posts) => {
        if (err) {
            return callback({ code: 404, message: 'no result' });
        }
        //get each posts' tags
        async.eachSeries(posts, (post, tagCallback) => {
            postTagModel.tagsByPostId(post.id, (err, result) => {
                if (err) {
                    tagCallback(err)
                }
                post.tags = result;
                tagCallback()
            });
        }, (err) => {
            if (err) {
                callback({ code: 404, message: 'no result' });
                return false;
            }
            callback({ code: 404, message: 'no result', list: posts });
        });
    });
}

靜態資源

在Nuxt中,你能夠將靜態文件放到項目根目錄的static文件夾中,而後直接使用根路徑/就能夠訪問了。

<!-- 假設static目錄下有一張圖片my-image.png,能夠這樣直接訪問 -->
<img src="/my-image.png"/>

登錄

第一次使用restful,登錄問題一直困擾着我,搜了很多資料,最後的解決方案是使用token。

在前端,檢測到用戶沒有登錄就跳轉到登錄頁面,用戶發送登錄請求後先在後臺校驗用戶名和密碼,校驗成功以後返回一個token。前端接收到這個token後將它存在本地,之後每次發送請求時將這個token帶上,以後的請求後臺對這個token進行校驗,若是合法就認爲登錄成功。

下面的代碼用於生成token(使用的是jwt-simple),uid是用戶ID,exp的取值是七天後的時間,jwtSecret是加密和解密的密鑰。

let auth = (user, callback) => {
    if (user.account.trim() == '') {
        return callback({ code: 403, message: '用戶名不正確' });
    }
    if (user.password.trim() == '') {
        return callback({ code: 403, message: '密碼不正確' });
    }
    userModel.auth(user, (err, user) => {
        if (err) {
            return callback({ code: 404, message: '登錄失敗' });
        }
        if (user.length === 1) {
            //設置七天有效期
            let expires = moment().add(7, 'days').valueOf();

            let token = jwt.encode({
                uid: user[0].id,
                exp: expires
            }, jwtSecret)

            return callback({ code: 200, message: 'success', token: token });
        }
        callback({ code: 404, message: '登錄失敗' });
    });
}

我使用的Ajax工具是axios,下面代碼的做用是給全部請求添加一個header。

// 攔截request
$http.interceptors.request.use(
  config => {
    if (typeof document === 'object') {
      let token = getCookieInClient('token')
      if (token) {
        config.headers.Authorization = token;
      }
    }
    return config;
  }, err => {
    return Promise.reject(err);
  }
);

下面是校驗token的中間件,先判斷接口是否須要驗證身份,不須要就直接Next,若是須要就獲取並校驗token,校驗作的比較粗糙,就是直接判斷token是否在有效期內,固然能夠有更安全的作法,你能夠本身去搜索一下。登錄成功的標誌就是token校驗合法,而後下一步操做。校驗失敗就直接返回,前端根據響應跳轉到登錄頁面。

module.exports = function (req, res, next) {
    let path = req.originalUrl
    
    //接口不須要登錄:直接next
    if (needAuth.indexOf(path) < 0) {
        return next();
    }
    
    //接口須要登錄
    var token = req.headers['authorization']
    if (!token) {
        return res.json({
            code: 401,
            message: 'you need login:there is no token'
        })
    }
    
    try {
        //解密獲取的token
        let decoded = jwt.decode(token, jwtSecret);

        //校驗有效期
        if (decoded.exp <= Date.now()) {
            return res.json({
                code: 401,
                message: 'you need login:token is expired'
            });
        }
        next();
    } catch (err) {
        return res.json({
            code: 401,
            message: 'you need login:decode token fail'
        })
    }
};

nuxt.config

nuxt的配置文件,具體的配置項能夠參考這個連接,下面是個人配置文件,裏面的內容應該是一看就懂。

module.exports = {
  //頁面的head標籤
  head: {
    title: 'JustYeh的前端博客',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1, user-scalable=no' },
      { hid: 'description', name: 'description', content: 'justyeh的前端博客' },
      { name: 'renderer', content: 'webkit' },
      { 'http-equiv': 'X-UA-Compatible', content: 'IE=edge' },
      { name: 'author', content: 'justyeh@163.com' },
      { name: 'apple-mobile-web-app-title', content: 'justyeh的前端博客' },
      { name: 'apple-mobile-web-app-capable', content: 'yes' },
      { name: 'apple-mobile-web-app-status-bar-style', content: '#263238' },
      { name: 'screen-orientation', content: 'portrait' },
      { name: 'x5-orientation', content: 'portrait' },
      { name: 'full-screen', content: 'yes' },
      { name: 'x5-fullscreen', content: 'true' },
      { name: 'browsermode', content: 'application' },
      { name: 'x5-page-mode', content: 'app' },
      { name: 'theme-color', content: '#263238' },
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ]
  },
  //全局引用的css文件
  css: ['~assets/css/main.css', '~assets/css/font-awesome.min.css'],
  // 頁面頂部loading效果
  loading: {
    color: '#04acf7',
    height: '4px',
    failedColor: 'red'
  },
  //頁面的過渡效果
  transition: {
    name: 'page'
  },
  //配置路由中間件
  router: {
    middleware: 'adminAuth'
  }

}

部署

部署一直不怎麼會弄,此次也確實在上面遇到了很多問題,這裏就不說了。最終使用的是pm2,如今假設你已經安裝好了node、pm二、vue等之類的包,依次運行下面的命令:

#進入文件所在目錄
cd your_project
#安裝項目所需依賴
npm insatll
#打包
npm run build
#運行,--name 'your project name'是可選的
pm2 start npm [--name 'your project name'] -- start

說在最後

上面的東西比較瑣碎,若是你正好遇到相關的問題可能就有幫助了。系統性的梳理我也不知道怎麼展開,只能抱歉了。

好了,所有的代碼在這裏:https://github.com/justyeh/justyeh.com,關於細節你均可以在這裏面查看。這個是最終的成果:http://justyeh.com/,鑑於這個是私人項目,線上後臺就不公佈了,不過能夠直接clone個人項目,數據庫什麼的都在,直接在本地就能夠跑起來。

水平有限,若是有什麼錯誤的地方還請包含。喜歡的話,能夠給個star。若是你有什麼好的建議和意見,歡迎聯繫我

相關文章
相關標籤/搜索