前言

笔记来源:拉勾教育 大前端高薪训练营

阅读建议:建议通过左侧导航栏进行阅读

Nuxt.js 综合案例

基本介绍

  • 案例名称:RealWorld
  • 一个开源的学习项目,目的就是帮助开发者快速学习新技能
  • GitHub仓库:https://github.com/gothinkster/realworld
  • 在线示例:https://demo.realworld.io/#/
  • 接口文档:https://github.com/gothinkster/realworld/tree/master/api
  • 页面模板:https://github.com/gothinkster/realworld-starter-kit/blob/master/FRONTEND_INSTRUCTIONS.md

学习前提

  • Vue.js 使用经验
  • Nuxt.js 基础
  • Node.js、webpack 相关使用经验

学习收获

  • 掌握使用 Nuxt.js 开发同构渲染应用
  • 增强 Vue.js 实践能力
  • 掌握同构渲染应用中常见的功能处理
    • 用户状态管理
    • 页面访问权限处理
    • SEO 优化
  • 掌握同构渲染应用的发布与部署

项目初始化

创建项目

  • 准备工作

      # 创建项目目录 mkdir realworld-nuxtjs # 进入项目目录 cd realworld-nuxtjs # 生成    package.json 文件 npm init -y # 安装    nuxt 依赖 npm install nuxt
    
  • package.json 中添加启动脚本:

      {"scripts": { "dev": "nuxt" }  }
    
  • 创建 pages/index.vue

      <template> <div> <h1>Home Page</h1> </div> </template> <script> export default { name: 'HomePage' } </script> <style> </style>
    
  • 启动服务:

      npm run dev
    

    在浏览器中访问 http://localhost:3000/ 测试。

导入样式资源

  • 配置模板文件,app.html

    <!DOCTYPE html>
    <html {{ HTML_ATTRS }}><head {{ HEAD_ATTRS }}>{{ HEAD }}<!-- Import Ionicon icons & Google Fonts our Bootstrap theme relies on --><link href="//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css" rel="stylesheet" type="text/css"><link href="//fonts.googleapis.com/css?family=Titillium+Web:700|Source+Serif+Pro:400,700|Merriweather+Sans:400,700|Source+Sans+Pro:400,300,600,700,300italic,400italic,600italic,700italic" rel="stylesheet" type="text/css"><!-- Import the custom Bootstrap 4 theme from our hosted CDN --><link rel="stylesheet" href="//demo.productionready.io/main.css"></head><body {{ BODY_ATTRS }}>{{ APP }}</body>
    </html>
    
      由于上述使用的资源文件需要访问国外地址,加载速度缓慢,因此将其 “ 本土化 ”,如下所示:
    
      <!DOCTYPE html><html {{ HTML_ATTRS }}><head {{ HEAD_ATTRS }}>{{ HEAD }}<!-- Import Ionicon icons & Google Fonts our Bootstrap theme relies on --><link href="https://cdn.jsdelivr.net/npm/ionicons@2.0.1/css/ionicons.min.css" rel="stylesheet" type="text/css"><link href="//fonts.googleapis.com/css?family=Titillium+Web:700|Source+Serif+Pro:400,700|Merriweather+Sans:400,700|Source+Sans+Pro:400,300,600,700,300italic,400italic,600italic,700italic" rel="stylesheet" type="text/css"><!-- Import the custom Bootstrap 4 theme from our hosted CDN --><link rel="stylesheet" href="/main.css"></head><body {{ BODY_ATTRS }}>{{ APP }}</body></html>
    

    ionicons.min.css 文件需要依赖其他的字体等文件,因此使用 jsDelivr 进行地址转换;

    main.css 文件不需要依赖其他的文件,因此,直接下载文件即可

  • 静态资源文件存放在 根目录的 static 文件夹下

配置布局组件

将公共部分提取成公共组件。

  • pages/layout/index.vue

    <template><div><!-- 顶部导航栏 --><nav class="navbar navbar-light"><div class="container"><a class="navbar-brand" href="index.html">conduit</a><ul class="nav navbar-nav pull-xs-right"><li class="nav-item"><!-- Add "active" class when you're on that page" --><a class="nav-link active" href="">Home</a></li><li class="nav-item"><a class="nav-link" href=""><i class="ion-compose"></i>&nbsp;New Post</a></li><li class="nav-item"><a class="nav-link" href=""><i class="ion-gear-a"></i>&nbsp;Settings</a></li><li class="nav-item"><a class="nav-link" href="">Sign up</a></li></ul></div></nav><!-- /顶部导航栏 --><!-- 子路由 --><nuxt-child /><!-- /子路由 --><!-- 底部 --><footer><div class="container"><a href="/" class="logo-font">conduit</a><span class="attribution">An interactive learning project from<a href="https://thinkster.io">Thinkster</a>. Code &amp; designlicensed under MIT.</span></div></footer><!-- /底部 --></div>
    </template>
    

导入登录注册

  • pages/login/index.vue

    <template><div class="auth-page"><div class="container page"><div class="row"><div class="col-md-6 offset-md-3 col-xs-12"><h1 class="text-xs-center">{{ isLogin ? "Sign in" : "Sign up" }}</h1><p class="text-xs-center"><nuxt-link to="/register" v-if="isLogin">Need an account?</nuxt-link><nuxt-link to="/login" v-else>Have an account?</nuxt-link></p><ul class="error-messages"><li>That email is already taken</li></ul><form><fieldset v-if="!isLogin" class="form-group"><inputclass="form-control form-control-lg"type="text"placeholder="Your Name"/></fieldset><fieldset class="form-group"><inputclass="form-control form-control-lg"type="text"placeholder="Email"/></fieldset><fieldset class="form-group"><inputclass="form-control form-control-lg"type="password"placeholder="Password"/></fieldset><button class="btn btn-lg btn-primary pull-xs-right">{{ isLogin ? "Sign in" : "Sign up" }}</button></form></div></div></div></div>
    </template><script>export default {name: "LoginIndex",computed: {isLogin() {return this.$route.name === "login";}}};
    </script>
    

导入剩余页面

路径 页面
/ 首页
/login 登录
/register 注册
/settings 用户设置
/editor 发布文章
/editor/:slug 编辑文章
/article/:slug 文章详情
/profile/:username 用户页面
/profile/:username/favorites 用户页面/喜欢的文章

用户设置页面

  • pages/settings/index.vue

    <template><div class="settings-page"><div class="container page"><div class="row"><div class="col-md-6 offset-md-3 col-xs-12"><h1 class="text-xs-center">Your Settings</h1><form><fieldset><fieldset class="form-group"><input class="form-control" type="text" placeholder="URL of profile picture"></fieldset><fieldset class="form-group"><input class="form-control form-control-lg" type="text" placeholder="Your Name"></fieldset><fieldset class="form-group"><textarea class="form-control form-control-lg" rows="8" placeholder="Short bio about you"></textarea></fieldset><fieldset class="form-group"><input class="form-control form-control-lg" type="text" placeholder="Email"></fieldset><fieldset class="form-group"><input class="form-control form-control-lg" type="password" placeholder="Password"></fieldset><button class="btn btn-lg btn-primary pull-xs-right">Update Settings</button></fieldset></form></div></div></div></div>
    </template>
    

创建 / 编辑文章

  • pages/editor/index.vue

    <template><div class="editor-page"><div class="container page"><div class="row"><div class="col-md-10 offset-md-1 col-xs-12"><form><fieldset><fieldset class="form-group"><input type="text" class="form-control form-control-lg" placeholder="Article Title"></fieldset><fieldset class="form-group"><input type="text" class="form-control" placeholder="What's this article about?"></fieldset><fieldset class="form-group"><textarea class="form-control" rows="8" placeholder="Write your article (in markdown)"></textarea></fieldset><fieldset class="form-group"><input type="text" class="form-control" placeholder="Enter tags"><div class="tag-list"></div></fieldset><button class="btn btn-lg pull-xs-right btn-primary" type="button">Publish Article</button></fieldset></form></div></div></div></div>
    </template>
    

文章详情页面

  • pages/article/index.vue

    <template><div class="article-page"><div class="banner"><div class="container"><h1>How to build webapps that scale</h1><div class="article-meta"><a href=""><img src="http://i.imgur.com/Qr71crq.jpg" /></a><div class="info"><a href="" class="author">Eric Simons</a><span class="date">January 20th</span></div><button class="btn btn-sm btn-outline-secondary"><i class="ion-plus-round"></i>&nbsp;Follow Eric Simons <span class="counter">(10)</span></button>&nbsp;&nbsp;<button class="btn btn-sm btn-outline-primary"><i class="ion-heart"></i>&nbsp;Favorite Post <span class="counter">(29)</span></button></div></div></div><div class="container page"><div class="row article-content"><div class="col-md-12"><p>Web development technologies have evolved at an incredible clip over the past few years.</p><h2 id="introducing-ionic">Introducing RealWorld.</h2><p>It's a great solution for learning how other frameworks work.</p></div></div><hr /><div class="article-actions"><div class="article-meta"><a href="profile.html"><img src="http://i.imgur.com/Qr71crq.jpg" /></a><div class="info"><a href="" class="author">Eric Simons</a><span class="date">January 20th</span></div><button class="btn btn-sm btn-outline-secondary"><i class="ion-plus-round"></i>&nbsp;Follow Eric Simons <span class="counter">(10)</span></button>&nbsp;<button class="btn btn-sm btn-outline-primary"><i class="ion-heart"></i>&nbsp;Favorite Post <span class="counter">(29)</span></button></div></div><div class="row"><div class="col-xs-12 col-md-8 offset-md-2"><form class="card comment-form"><div class="card-block"><textarea class="form-control" placeholder="Write a comment..." rows="3"></textarea></div><div class="card-footer"><img src="http://i.imgur.com/Qr71crq.jpg" class="comment-author-img" /><button class="btn btn-sm btn-primary">Post Comment</button></div></form><div class="card"><div class="card-block"><p class="card-text">With supporting text below as a natural lead-in to additional content.</p></div><div class="card-footer"><a href="" class="comment-author"><img src="http://i.imgur.com/Qr71crq.jpg" class="comment-author-img" /></a>&nbsp;<a href="" class="comment-author">Jacob Schmidt</a><span class="date-posted">Dec 29th</span></div></div><div class="card"><div class="card-block"><p class="card-text">With supporting text below as a natural lead-in to additional content.</p></div><div class="card-footer"><a href="" class="comment-author"><img src="http://i.imgur.com/Qr71crq.jpg" class="comment-author-img" /></a>&nbsp;<a href="" class="comment-author">Jacob Schmidt</a><span class="date-posted">Dec 29th</span><span class="mod-options"><i class="ion-edit"></i><i class="ion-trash-a"></i></span></div></div></div></div></div></div>
    </template>
    

导入用户页面

  • pages/profile/index.vue

    <template><div class="profile-page"><div class="user-info"><div class="container"><div class="row"><div class="col-xs-12 col-md-10 offset-md-1"><img src="http://i.imgur.com/Qr71crq.jpg" class="user-img" /><h4>Eric Simons</h4><p>Cofounder @GoThinkster, lived in Aol's HQ for a few months, kindalooks like Peeta from the Hunger Games</p><button class="btn btn-sm btn-outline-secondary action-btn"><i class="ion-plus-round"></i>&nbsp; Follow Eric Simons</button></div></div></div></div><div class="container"><div class="row"><div class="col-xs-12 col-md-10 offset-md-1"><div class="articles-toggle"><ul class="nav nav-pills outline-active"><li class="nav-item"><a class="nav-link active" href="">My Articles</a></li><li class="nav-item"><a class="nav-link" href="">Favorited Articles</a></li></ul></div><div class="article-preview"><div class="article-meta"><a href=""><img src="http://i.imgur.com/Qr71crq.jpg" /></a><div class="info"><a href="" class="author">Eric Simons</a><span class="date">January 20th</span></div><button class="btn btn-outline-primary btn-sm pull-xs-right"><i class="ion-heart"></i> 29</button></div><a href="" class="preview-link"><h1>How to build webapps that scale</h1><p>This is the description for the post.</p><span>Read more...</span></a></div><div class="article-preview"><div class="article-meta"><a href=""><img src="http://i.imgur.com/N4VcUeJ.jpg" /></a><div class="info"><a href="" class="author">Albert Pai</a><span class="date">January 20th</span></div><button class="btn btn-outline-primary btn-sm pull-xs-right"><i class="ion-heart"></i> 32</button></div><a href="" class="preview-link"><h1>The song you won't ever stop singing. No matter how hard youtry.</h1><p>This is the description for the post.</p><span>Read more...</span><ul class="tag-list"><li class="tag-default tag-pill tag-outline">Music</li><li class="tag-default tag-pill tag-outline">Song</li></ul></a></div></div></div></div></div>
    </template>
    

手动配置路由

  • nuxt.config.js

    module.exports = {router: {// 自定义路由表规则extendRoutes(routes, resolve) {console.log(routes);// 清空 Nuxt.js 基于 pages 目录默认生成的路由规则routes.splice(0)// 添加路由规则routes.push(...[{path: '/',component: resolve(__dirname, 'pages/layout/'),children: [{path: '', // 默认子路由name: 'home',component: resolve(__dirname, 'pages/home/')},{path: '/login', // 登录子路由name: 'login',component: resolve(__dirname, 'pages/login/')},{path: '/register', // 注册子路由name: 'register',component: resolve(__dirname, 'pages/login/')},{path: '/profile/:username', // 用户子路由name: 'profile',component: resolve(__dirname, 'pages/profile/')},{path: '/settings', // 设置子路由name: 'settings',component: resolve(__dirname, 'pages/settings/')},{path: '/editor', // 创建/编辑文章子路由name: 'editor',component: resolve(__dirname, 'pages/editor/')},{path: '/article/:slug', // 文章详情子路由name: 'article',component: resolve(__dirname, 'pages/article/')}]}])}}
    }
    

处理顶部导航链接

  • pages/layout/index.vue,将所有 a标签 改写为 nuxt-link 标签

    <template><div><!-- 顶部导航栏 --><nav class="navbar navbar-light"><div class="container"><!-- <a class="navbar-brand" href="index.html">conduit</a> --><nuxt-linkclass="navbar-brand"to="/">Home</nuxt-link><ul class="nav navbar-nav pull-xs-right"><li class="nav-item"><!-- Add "active" class when you're on that page" --><!-- exact 设置精确匹配时,才会高亮 --><nuxt-link class="nav-link active" to="/" exact>Home</nuxt-link></li><li class="nav-item"><nuxt-link class="nav-link" to="/editor"><i class="ion-compose"></i>&nbsp;New Post</nuxt-link></li><li class="nav-item"><nuxt-link class="nav-link" to="/settings"><i class="ion-gear-a"></i>&nbsp;Settings</nuxt-link></li><li class="nav-item"><nuxt-link class="nav-link" to="/login">Sign in</nuxt-link></li><li class="nav-item"><nuxt-link class="nav-link" to="/register">Sign up</nuxt-link></li><li class="nav-item"><nuxt-link class="nav-link" to="/profile/123"><imgclass="user-pic"src="http://toutiao.meiduo.site/FtNcS8sKFSYQbtBbd40eFTL6lAs_"/>lpz999</nuxt-link></li></ul></div></nav><!-- /顶部导航栏 --><!-- 子路由 --><nuxt-child /><!-- /子路由 --><!-- 底部 --><footer><div class="container"><a href="/" class="logo-font">conduit</a><span class="attribution">An interactive learning project from<a href="https://thinkster.io">Thinkster</a>. Code &amp; designlicensed under MIT.</span></div></footer><!-- /底部 --></div>
    </template>
    

处理导航链接高亮

  • nuxt.config.js,配置路由链接高亮

    module.exports = {router: {// 处理导航链接高亮linkActiveClass: 'active',.....}
    }
    

封装请求模块

  • 安装 axios:

    npm i axios --save
    
  • 创建 utils/request.js 文件,封装请求模块

    /*** 基于 axios 封装的请求模块*/
    import axios from 'axios'// 使用自定义配置新建一个 axios 实例
    const request = axios.create({// 配置基本的请求路径baseURL: 'https://conduit.productionready.io'
    })export default request
    

登录注册

封装请求方法

  • 创建 api/user.js 文件,封装**登录注册**相关的请求方法

    import request from '@/utils/request' // 用户登录
    export const login = data => {return request({method: "POST",url: "/api/users/login",data})
    } // 用户注册
    export const register = data => {return request({method: "POST",url: "/api/users",data})
    }
    

基本用户登录注册

实现基本登录功能

表单验证

错误处理

用户注册

  • pages/login/index.vue,调用接口,输入登录信息,完成基本登录注册功能

    <template><div class="auth-page"><div class="container page"><div class="row"><div class="col-md-6 offset-md-3 col-xs-12"><h1 class="text-xs-center">{{ isLogin ? "Sign in" : "Sign up" }}</h1><p class="text-xs-center"><nuxt-link to="/register" v-if="isLogin">Need an account?</nuxt-link><nuxt-link to="/login" v-else>Have an account?</nuxt-link></p><!-- 错误处理 --><ul class="error-messages"><template v-for="(messages, field) in errors"><li v-for="(message, index) in messages" :key="index">{{ field }} {{ message }}</li></template></ul><form @submit.prevent="onSubmit"><fieldset v-if="!isLogin" class="form-group"><inputv-model="user.username"class="form-control form-control-lg"type="text"placeholder="Your Name"required/></fieldset><fieldset class="form-group"><inputv-model="user.email"class="form-control form-control-lg"type="email"placeholder="Email"required/></fieldset><fieldset class="form-group"><inputv-model="user.password"class="form-control form-control-lg"type="password"placeholder="Password"requiredminlength="8"/></fieldset><button class="btn btn-lg btn-primary pull-xs-right">{{ isLogin ? "Sign in" : "Sign up" }}</button></form></div></div></div></div>
    </template><script>
    import { login, register } from "@/api/user";export default {name: "LoginIndex",computed: {isLogin() {return this.$route.name === "login";},},data() {return {user: {username: "",email: "",password: "",},errors: {}, // 错误信息};},methods: {async onSubmit() {// 通过 try {} catch () {} 捕获错误信息try {// 提交表单请求登录const { data } = this.isLogin ? await login({user: this.user,}) : await register({user: this.user,})// 跳转到首页this.$router.push("/");} catch (err) {// console.log("请求失败", err);this.errors = err.response.data.errors;}}}
    };
    </script>
    

存储用户登录状态

存储原因


  • 需要根据登录状态判断页面的显示内容;
  • 某些页面只有登录后,才可以访问;

解析存储登录状态实现流程


  • 官网地址:https://zh.nuxtjs.org/faq/auth-external-jwt/#login-page

  • 官网示例,代码如下:

    • login.vue
    // js-cookie 专门用来操作客户端浏览器 cookie 的包,即只有客户端时,才会加载 js-cookie 包
    const Cookie = process.client ? require('js-cookie') : undefinedexport default {middleware: 'notAuthenticated',methods: {postLogin() {setTimeout(() => {// we simulate the async request with timeout.const auth = {accessToken: 'someStringGotFromApiServiceWithAjax'}// 存储到容器是为了方便共享this.$store.commit('setAuth', auth) // mutating to store for client rendering// 把登录状态存到 Cookie 中,使 客户端和服务端都可以拿到Cookie.set('auth', auth) // saving token in cookie for server renderingthis.$router.push('/')}, 1000)}}
    }
    
    • store/index.js
    import Vuex from 'vuex'const cookieparser = process.server ? require('cookieparser') : undefinedconst createStore = () => {return new Vuex.Store({state: () => ({auth: null}),mutations: {setAuth(state, auth) {state.auth = auth}},actions: {// 初始化容器以及需要传递给客户端的数据// 这个特殊的 action 只会在服务端渲染期间运行nuxtServerInit({ commit }, { req }) {let auth = null// 如果请求头中有 Cookieif (req.headers.cookie) {const parsed = cookieparser.parse(req.headers.cookie)try {auth = JSON.parse(parsed.auth)} catch (err) {// No valid cookie found}}// 提交 mutation,修改 state 状态commit('setAuth', auth)}}})
    }export default createStore
    

    注意:

    ​ 该nuxtServerInit函数只在每个服务器端呈现中运行。所以我们用它来改变存储中的会话浏览器cookie。我们可以使用req.headers.cookie并使用cookieparser.

存储登录状态,并将其持久化


  1. 初始化容器数据

    store/index.js

    // 在服务端渲染期间运行都是同一个实例
    // 为了防止数据冲突,务必要把 state 定义成一个函数,返回数据对象
    export const state = () => {return {// 当前登录用户的登录状态user: null}
    }export const mutations = {setUser(state, data) {state.user = data}
    }export const actions = {}
    

    注意:

    ​ 1,Nuxt.js 中已经集成了 Vuex,无需手动载入;

    ​ 2,store 目录的名称是 固定的,不可修改的;

    ​ 3,Nuxt.js 在运行过程中,查找到 store 目录,会自动加载store 目录中的容器模块。

  2. 登录成功,将用户信息存入容器

    pages/login/index.vue

    // TODO:保存用户的登录状态
    this.$store.commit('setUser', data.user)
    
  3. 将登录状态持久化到 Cookie

    启动 cmd命令行,安装 js-cookie

    npm i js-cookie --save
    

    store/index.js 中,按需加载 js-cookie,即只在 客户端 中使用

    const Cookie = process.client ? require('js-cookie') : undefined
    

    pages/login/index.vue 中,将登录状态存储到 Cookie 中

    // 为了防止刷新页面数据丢失,我们需要把数据持久化
    Cookie.set('user', data.user)
    
  4. Cookie 中获取并初始化用户登录状态

    安装 cookieparser

    npm i cookieparser --save
    

    store/index.js 中,定义 nuxtServerInit 函数,初始化容器数据,传递数据给客户端使用

    export const actions = {// nuxtServerInit 是一个特殊的 action// 这个 action 会在服务端渲染期间自动调用// 作用:初始化容器数据,传递数据给客户端使用nuxtServerInit({ commit }, { req }) {let user = null// 如果请求头中有 Cookieif (req.headers.cookie) {// 使用 cookieparser 把 cookie 字符串转换为 JavaScript 对象const parsed = cookieparser.parse(req.headers.cookie)try {user = JSON.parse(parsed.user)} catch (err) {// No valid cookie found}}// 提交 mutation,修改 state 状态commit('setUser', user)}
    }
    

处理页面访问权限


  1. 中间件简介

    1,官网地址:https://zh.nuxtjs.org/docs/2.x/directory-structure/middleware#router-middleware

    2,基本概述

    • 中间件允许定义可以在呈现页面或一组页面(布局)之前运行的自定义函数。
    • 共享中间件应该放在middleware/目录。文件名将是中间件的名称(中间件/auth.js将会是auth中间件),也可以通过直接使用函数来定义特定于页面的中间件,请参阅匿名中间件 .
    • 中间件接收 context 作为第一个参数。
    export default function (context) {// Add the userAgent property to the contextcontext.userAgent = process.server? context.req.headers['user-agent']: navigator.userAgent
    }
    

    在通用模式下,middleware 将在服务器端(第一次向Nuxt应用程序请求时,例如直接访问应用程序或刷新页面时)调用一次,在导航到其他路由时在客户端调用一次。与ssr: false,在这两种情况下都将在客户端调用中间件。

    中间件将按以下顺序串联执行:

    1. nuxt.config.js(按文件中的顺序)
    2. 匹配的布局
    3. 匹配的页面
  2. 基本分类

    • 路由器中间件

      中间件可以是异步的。要执行此操作,返回 Promise 或者使用 async/await

      中间件/stats.js

      import http from 'http'export default function ({ route }) {return http.post('http://my-stats-api.com', {url: route.fullPath})
      }
      

      那么,在你的nuxt.config.js,使用路由器.中间件钥匙

      nuxt.config.js

      export default {router: {middleware: 'stats'}
      }
      

      现在stats每次路由更改都将调用中间件。

      您也可以将中间件(甚至多个)添加到特定的布局或页面。

      页面/索引视图/布局/默认视图

      export default {middleware: ['auth', 'stats']
      }
      
    • 命名中间件

      可以通过在middleware/目录中,文件名将是中间件名称。

      middleware/authenticated.js

      export default function ({ store, redirect }) {// If the user is not authenticatedif (!store.state.authenticated) {return redirect('/login')}
      }
      

      页面/secret.vue

      <template><h1>Secret page</h1>
      </template><script>export default {middleware: 'authenticated'}
      </script>
      
    • 匿名中间件

      如果只需要为特定页面使用中间件,可以直接为其使用函数(或函数数组):

      页面/secret.vue

      <template><h1>Secret page</h1>
      </template><script>export default {middleware({ store, redirect }) {// If the user is not authenticatedif (!store.state.authenticated) {return redirect('/login')}}}
      </script>
      
  3. 页面访问权限实现过程

    middleware/authenticated.js

    /*** 验证是否登录的中间件*/
    export default function ({ store, redirect }) {// If the user is not authenticatedif (!store.state.user) {// 重定向,即跳转到 登录页面return redirect('/login')}
    }
    

    middlewares/notAuthenticated.js

    /*** 如果已登录,重定向至首页*/
    export default function ({ store, redirect }) {// If the user is authenticated redirect to home pageif (store.state.user) {return redirect('/')}
    }
    

    在需要判断登录权限的页面中配置使用中间件

    // 在路由匹配组件渲染之前会先执行中间件处理
    // 多个中间件,使用数组
    export default { ... middleware: ['authenticated']
    }// 单个中间件,使用字符串;
    export default { ... middleware: 'authenticated'
    }
    

首页模块

展示公共文章列表

  • 接口文档:https://github.com/gothinkster/realworld/tree/master/api#list-articles

封装请求方法

  • api/article.js

    import request from '@/utils/request' // 获取公共文章列表
    export const getArticles = params => {// params 是可选的return request({method: 'GET',url: '/api/articles',params})
    }
    

获取数据

  • pages/home/index.vue 中,请求接口,获取数据

    import { getArticles } from "@/api/article";export default {name: "HomePage",// 需要 SEOasync asyncData() {const { data } = await getArticles();return {articles: data.articles,articlesCount: data.articlesCount,};},
    };
    

模板绑定

  • pages/home/index.vue 中,循环渲染数据,实现模板动态绑定

    <divclass="article-preview"v-for="article in articles":key="article.slug"><div class="article-meta"><nuxt-link :to="{name: 'profile',params: {username: article.author.username}}"><img :src="article.author.image"/></nuxt-link><div class="info"><nuxt-link :to="{name: 'profile',params: {username: article.author.username}}"class="author">{{ article.author.username }}</nuxt-link><span class="date">{{ article.createdAt }}</span></div><button class="btn btn-outline-primary btn-sm pull-xs-right":class="{ active: article.favorited }"><i class="ion-heart"></i> {{ article.favoritesCount }}</button></div><nuxt-link :to="{name: 'article',params: {slug: article.slug}}" class="preview-link"><h1>{{ article.title }}</h1><p>{{ article.description }}</p><span></span></nuxt-link>
    </div>
    

公共文章列表分页

处理分页参数

  • pages/home/index.vue,访问接口时,传入每次访问的文章数量 和 数据的偏移量

    // 需要 SEO
    async asyncData({ query }) {// query 存储所有的查询条件const page = Number.parseInt(query.page || 1);const limit = 20;const { data } = await getArticles({limit, // 每页显示的文章数量// 数据偏移量offset: (page - 1) * limit,});return {limit,page,articles: data.articles,articlesCount: data.articlesCount,};
    },
    

页码处理

  • pages/home/index.vue 中,书写分页模板

    
    
    • 1,使用计算属性计算总页码
    computed: {// 总页码totalPage() {return Math.ceil(this.articlesCount / this.limit);}
    }
    
    • 2,遍历生成页码列表
    
    <!-- 分页列表 -->
    <nav><ul class="pagination"><liclass="page-item"v-for="item in totalPage":key="item":class="{active: item === page,}"><nuxt-linkclass="page-link":to="{name: 'home',query: {page: item,},}">{{ item }}</nuxt-link></li></ul>
    </nav>
    <!-- /分页列表 -->
    
    • 3,设置导航链接
    • 4,响应 query 参数的变化
    // 查询参数改变时,不会调用 asyncData 等
    // 通过 watchQuery 解决,类似于热更新
    // 注意,需要刷新整个页面,否则不起作用
    watchQuery: ['page'],
    

    官网地址:https://zh.nuxtjs.org/docs/2.x/components-glossary/pages-watchquery/

展示文章标签列表

封装请求方法

  • api/tag.js

    import request from '@/utils/request' // 获取文章标签列表
    export const getTags = () => {// params 是可选的return request({method: 'GET',url: '/api/tags'})
    }
    

模板绑定

  • pages/home/index.vue

    <div class="col-md-3"><div class="sidebar"><p>Popular Tags</p><div class="tag-list"><nuxt-link:to="{name: 'home',query: {tag: item}}"class="tag-pill tag-default"v-for="item in tags":key="item">{{ item }}</nuxt-link></div></div>
    </div>
    

优化并行异步任务

  • pages/home/index.vue

    async asyncData({ query }) {// query 存储所有的查询条件const page = Number.parseInt(query.page || 1);const limit = 20;const [articleRes, tagRes] = await Promise.all([getArticles({limit, // 每页显示的文章数量// 数据偏移量offset: (page - 1) * limit,tag: query.tag}),getTags(),]);const { articles, articlesCount} = articleRes.dataconst { tags } = tagRes.datareturn {articles,articlesCount,tags,limit,page};
    },
    watchQuery: ["page", "tag"],
    

处理首页的导航栏

  • pages/home/index.vue

    <template><div class="home-page"><div class="banner"><div class="container"><h1 class="logo-font">conduit</h1><p>A place to share your knowledge.</p></div></div><div class="container page"><div class="row"><div class="col-md-9"><div class="feed-toggle"><ul class="nav nav-pills outline-active"><li v-if="user" class="nav-item"><!-- <a class="nav-link disabled" href="">Your Feed</a> --><nuxt-linkclass="nav-link":class="{ active: tab === 'your_feed' }"exact:to="{name: 'home',query: {tab: 'your_feed',},}">Your Feed</nuxt-link></li><li class="nav-item"><nuxt-linkclass="nav-link":class="{ active: tab === 'global_feed' }"exact:to="{name: 'home',query: {tab: 'global_feed',},}">Global Feed</nuxt-link></li><li v-if="tag" class="nav-item"><nuxt-linkclass="nav-link":class="{ active: tab === 'tag' }"exact:to="{name: 'home',query: {tab: 'tag',tag: tag,}}">#{{ tag }}</nuxt-link></li></ul></div><divclass="article-preview"v-for="article in articles":key="article.slug"><div class="article-meta"><nuxt-link:to="{name: 'profile',params: {username: article.author.username,}}"><img :src="article.author.image" /></nuxt-link><div class="info"><nuxt-link:to="{name: 'profile',params: {username: article.author.username,},}"class="author">{{ article.author.username }}</nuxt-link><span class="date">{{ article.createdAt }}</span></div><buttonclass="btn btn-outline-primary btn-sm pull-xs-right":class="{ active: article.favorited }"><i class="ion-heart"></i> {{ article.favoritesCount }}</button></div><nuxt-link:to="{name: 'article',params: {slug: article.slug,}}"class="preview-link"><h1>{{ article.title }}</h1><p>{{ article.description }}</p><span></span></nuxt-link></div><!-- 分页列表 --><nav><ul class="pagination"><liclass="page-item"v-for="item in totalPage":key="item":class="{ active: item === page }"><nuxt-linkclass="page-link":to="{name: 'home',query: {page: item,tag: $route.query.tag,tab: tab,}}">{{ item }}</nuxt-link></li></ul></nav><!-- /分页列表 --></div><div class="col-md-3"><div class="sidebar"><p>Popular Tags</p><div class="tag-list"><nuxt-link:to="{name: 'home',query: {tab: 'tag',tag: item,}}"class="tag-pill tag-default"v-for="item in tags":key="item">{{ item }}</nuxt-link></div></div></div></div></div></div>
    </template><script>import { getArticles, getFeedArticles } from "@/api/article";import { getTags } from "@/api/tag";import { mapState } from "vuex";export default {name: "HomePage",// 需要 SEOasync asyncData({ query, store }) {// query 存储所有的查询条件const page = Number.parseInt(query.page || 1);const limit = 20;const { tag } = query;const tab = query.tab || "global_feed";// 判断是否是用户关注的文章列表const loadArticles =store.state.user && tab === "your_feed" ? getFeedArticles : getArticles;const [articleRes, tagRes] = await Promise.all([loadArticles({limit, // 每页显示的文章数量// 数据偏移量offset: (page - 1) * limit,tag,}),getTags(),]);const { articles, articlesCount } = articleRes.data;const { tags } = tagRes.data;return {articles,articlesCount,tags,limit,page,tag,tab,};},// 查询参数改变时,不会调用 asyncData 等// 通过 watchQuery 解决,类似于热更新// 注意,需要刷新整个页面,否则不起作用watchQuery: ["page", "tag", "tab"],computed: {// 总页码totalPage() {return Math.ceil(this.articlesCount / this.limit);},...mapState(["user"]),},};
    </script>
    
  • api/article.js,封装用户关注文章列表的请求方法

    // 获取关注的用户文章列表
    export const getFeedArticles = params => {// params 是可选的return request({method: 'GET',url: '/api/articles/feed',// Authorization: Token jwt.token.hereheaders: {Authorization: `Token eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MTM0NjgyLCJ1c2VybmFtZSI6ImFsaXNvbiIsImV4cCI6MTYxNTYxODY0NH0.FE-89YvLHeTYARzN-QX0VRmSlwB4CV9qg56_CytCX2c`},params})
    }
    

统一设置用户 Token

添加请求拦截器

  • 在整个应用程序中使用函数或值时,使用插件机制在 plugins/request.js 中注入 context

    /*** 基于 axios 封装的请求模块*/import axios from 'axios'// 创建请求对象
    export const request = axios.create({// 配置基本的请求路径baseURL: 'https://conduit.productionready.io'
    })// 通过插件机制获取到上下文对象(query、params、req、res、app、store...)
    // 将容器 context 注入进来
    // 插件导出函数必须作为 default 成员
    export default ({ store }) => {// 请求拦截器// Add a request interceptor// 任何请求都要经过请求拦截器// 我们可以在请求拦截器中做一些公共的业务处理,例如统一设置 Tokenrequest.interceptors.request.use(function (config) {// Do something before request is sent// 在发送请求之前做些什么// 请求就会经过这里const { user } = store.stateif (user && user.token) {// Authorization: Token jwt.token.hereconfig.headers.Authorization = `Token ${user.token}`}// 返回 config 请求配置对象return config;}, function (error) {// 如果请求失败(此时请求还没有发出去),就会进入这里// Do something with request error// 对请求错误做些什么return Promise.reject(error);});
    }
    

使用插件,注入 context

  • 官网地址:https://zh.nuxtjs.org/docs/2.x/directory-structure/plugins/

  • nuxt.config.js 中,注册插件

    /*** Nuxt.js 的配置文件*/module.exports = {......// 注册插件plugins: ['~/plugins/request.js']
    }
    

注意:

  • 将原来 api/* 目录中文件引入的 request 方法,改为如下代码:

    import { request } from '@/plugins/request'
    

文章发布时间格式化

  • GitHub 地址: https://github.com/iamkun/dayjs

  • 使用 dayjs 模块,安装 dayjs

    npm i dayjs --save
    
  • plugins/dayjs.js, 封装 dayjs 插件

    /*** 日期格式化 过滤器*/import Vue from "vue";
    import dayjs from "dayjs";// 注册全局过滤器
    // {{ 表达式 | 过滤器 }}
    Vue.filter('date', (value, format = 'YYYY-MM-DD HH:mm:ss') => {return dayjs(value).format(format)
    })
    
  • nuxt.config.js, 注册插件

    module.exports = {......// 注册插件plugins: ['~/plugins/dayjs.js']
    }
    
  • pages/home/index.vue, 使用过滤器

    <span class="date">{{ article.createdAt | date('MMM DD, YYYY') }}</span>
    

对文章进行点赞操作

封装请求方法

  • api/article.js

    // 添加点赞
    export const addFavorite = slug => {return request({method: 'POST',url: `/api/articles/${slug}/favorite`})
    }// 取消点赞
    export const deleteFavorite = slug => {return request({method: 'DELETE',url: `/api/articles/${slug}/favorite`})
    }
    

绑定点击事件

  • pages/home/index.vue

    <buttonclass="btn btn-outline-primary btn-sm pull-xs-right":class="{active: article.favorited,}"@click="onFavorite(article)":disabled="article.favoriteDisabled"><i class="ion-heart"></i> {{ article.favoritesCount }}
    </button>
    
    async asyncData({ query, store }) {......// 防止重复点击articles.forEach(article => article.favoriteDisabled = false)......
    },
    methods: {async onFavorite (article) {article.favoriteDisabled = trueif (article.favorited) {// 取消点赞await deleteFavorite(article.slug)article.favorited = falsearticle.favoritesCount -= 1} else {// 添加点赞await addFavorite(article.slug)article.favorited = truearticle.favoritesCount += 1}article.favoriteDisabled = false}
    }
    

文章详情

展示基本信息

封装请求方法

  • api/article.js

    // 获取文章详情
    export const getArticle = slug => {return request({method: 'GET',url: `/api/articles/${slug}`})
    }
    

获取数据

  • pages/article/index.vue

    import { getArticle } from '@/api/article'
    export default {name: 'ArticleIndex',async asyncData ({ params }) {const { data } = await getArticle(params.slug)return {article: data.article}}
    }
    

模板页面

  • pages/article/index.vue

    <template><div class="article-page"><div class="banner"><div class="container"><h1>{{ article.title }}</h1><article-meta :article="article" /></div></div><div class="container page"><div class="row article-content"><div class="col-md-12" v-html="article.body"></div></div><hr /><div class="article-actions"><article-meta :article="article" /></div><div class="row"><div class="col-xs-12 col-md-8 offset-md-2"><article-comments :article="article" /></div></div></div></div>
    </template>
    

Markdown 转为 HTML

  • GitHub 地址: https://github.com/markdown-it/markdown-it

  • 使用 markdown-it 模块,安装 markdown-it

    npm i markdown-it --save
    
  • pages/article/index.vue, 把 markdown 格式的数据转换成 HTML

    import { getArticle } from '@/api/article'
    import MarkdownIt from 'markdown-it'export default {name: 'ArticleIndex',async asyncData ({ params }) {const { data } = await getArticle(params.slug)const { article } = dataconst md = new MarkdownIt()// 将 Makedown 格式转换为 HTMLarticle.body = md.render(article.body)return {article}}
    }
    

展示文章作者相关信息

  • pages/article/article-meta.vue. 提取公共组件,渲染文章作者相关信息

    <template><div class="article-actions"><div class="article-meta"><nuxt-link:to="{name: 'profile',params: {username: article.author.username,},}"><img :src="article.author.image" /></nuxt-link><div class="info"><nuxt-link:to="{name: 'profile',params: {username: article.author.username,},}"class="author">{{ article.author.username }}</nuxt-link><span class="date">{{ article.createdAt | date("MMM DD, YYYY") }}</span></div><buttonclass="btn btn-sm btn-outline-secondary":class="{active: article.author.following,}"><i class="ion-plus-round"></i>&nbsp; Follow {{ article.author.username }}<span class="counter">({{ article.favoritesCount }})</span></button>&nbsp;<buttonclass="btn btn-sm btn-outline-primary":class="{active: article.favorited,}"><i class="ion-heart"></i>&nbsp; Favorite Post<span class="counter">({{ article.favoritesCount }})</span></button></div></div>
    </template><script>export default {name: "ArticleMeta",props: {article: {type: Object,required: true,},},};
    </script>
    
  • pages/article/index.vue,父组件引入子组件

    import ArticleMeta from "./components/article-meta";
    import ArticleComments from "./components/article-comments";export default {......components: {ArticleMeta,ArticleComments},
    };
    

设置页面 meta 优化 SEO

  • 官网地址: https://zh.nuxtjs.org/docs/2.x/components-glossary/pages-head/

  • pages/article/index.vue

    head() {return {// 设置页面的 titletitle: `${this.article.title} - RealWorld`,meta: [// hid is used as unique identifier. Do not use `vmid` for it as it will not work{hid: "description",name: "description",content: this.article.description,},],};
    },
    

展示评论列表

  • pages/article/article-comments.js,提取公共组件,通过客户端渲染展示评论列表

    <template><div><form class="card comment-form"><div class="card-block"><textareaclass="form-control"placeholder="Write a comment..."rows="3"></textarea></div><div class="card-footer"><img :src="article.author.image" class="comment-author-img" /><button class="btn btn-sm btn-primary">Post Comment</button></div></form><div class="card" v-for="comment in comments" :key="comment.id"><div class="card-block"><p class="card-text">{{ comment.body }}</p></div><div class="card-footer"><nuxt-link:to="{name: 'profile',params: {username: comment.author.username,},}"class="comment-author"><img :src="comment.author.image" class="comment-author-img" /></nuxt-link>&nbsp;<nuxt-link:to="{name: 'profile',params: {username: comment.author.username,},}"class="comment-author">{{ comment.author.username }}</nuxt-link><span class="date-posted">{{comment.createdAt | date("MMM DD, YYYY")}}</span></div></div></div>
    </template><script>import { getComments } from "@/api/article";export default {name: "ArticleComments",props: {article: {type: Object,required: true,},},data () {return {comments: [], // 文章列表};},async mounted () {// 获取评论数据const { data } = await getComments(this.article.slug);this.comments = data.comments;},};
    </script>e: Object,required: true,},},data () {return {comments: [], // 文章列表};},async mounted () {// 获取评论数据const { data } = await getComments(this.article.slug);this.comments = data.comments;},};
    </script>
    

【服务端渲染】NuxtJs 综合案例相关推荐

  1. 【服务端渲染】NuxtJS基础

    前言 笔记来源:拉勾教育 大前端高薪训练营 阅读建议:建议通过左侧导航栏进行阅读 Nuxt.js 的基本介绍 官网:https://zh.nuxtjs.org/ GitHub 仓库:https://g ...

  2. Vue 服务端渲染原理 拆分成三步个步骤简单的实现一个案例

    前言 可能我们平常接触比较多的是使用 vue + vue全家桶来搭建起一个单页(SPA)应用.用 服务端渲染 搭建项目比较少,本文是记录我在学习 服务端渲染 过程中的一些见解,如有出错或疏漏,麻烦帮忙 ...

  3. vue服务端渲染之nuxtjs

    前言 本篇主要针对nuxtjs中的一些重要概念整理和代码实现! 在学习vue服务端渲染之前,先搞清楚几个概念: 什么是客户端渲染(CSR) 什么是服务端渲染(SSR) CSR和SSR有什么异同 客户端 ...

  4. nuxtjs+express+vue2+vuex搭建的服务端渲染(SSR)个人网站项目 1

    5se7en.com nuxtjs+express+vue2.0+vuex搭建的服务端渲染个人网站项目. github项目地址: https://github.com/se7en-1992... 项目 ...

  5. NuxtJS服务端渲染

    背景 目前该前端项目是VUE编写的单页应用,如果开始推广,目前的架构对SEO的支持很不友好,为更好的支持推广,预研采用服务端渲染(SSR)十分的必要,并且静态化后页面的渲染速度也会有所提高. 经综合比 ...

  6. nuxtjs 服务端渲染从开发到发布的流程

    1.创建项目 yarn create nuxt-app <项目名> 2.开发模式启动 yarn dev 3.部署服务器 3.1.服务端build yarn build 3.2.启动服务端渲 ...

  7. SSR 学习 - 传统服务端渲染 Web 应用、客户端渲染、同构渲染、优缺点和案例演示

    概述 随着前端技术栈和工具链的迭代成熟,前端工程化.模块化也已成为了当下的主流技术方案. 在这波前端技术浪潮中,涌现了诸如 React.Vue.Angular 等基于客户端渲染的前端框架. 这类框架所 ...

  8. Day210.服务端渲染技术NUXT、整合前台主页面、名师、课程静态页面、首页整合banner数据后端部分【创建banner微服务、接口、banner后台前端实现】 -谷粒学院

    谷粒学院 服务端渲染技术NUXT 一.服务端渲染技术NUXT 1.什么是服务端渲染 服务端渲染又称SSR (Server Side Render)是在服务端完成页面的内容,而不是在客户端通过AJAX获 ...

  9. 【服务端渲染】之 Vue SSR

    前言 笔记来源:拉勾教育 大前端高薪训练营 阅读建议:内容较多,建议通过左侧导航栏进行阅读 Vue SSR 基本介绍 Vue SSR 是什么 官方文档:https://ssr.vuejs.org/ V ...

最新文章

  1. Leetcode376摇摆序列--贪心+自动机
  2. 全面解析RS232、RS485、RS422、RJ45接口的区别和各自的应用
  3. java求最大值_java-求一组整数中的最大值
  4. 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来
  5. 静态网页与动态网页区别
  6. 第二章 染色热力学理论单元测验
  7. android 获取芯片版本_谷歌:未来Android手机将获得4年软件更新
  8. python头像右上角加红色数字_利用python实现微信头像加红色数字功能
  9. 微软收购开源数据库厂商Citus Data,云数据库服务能力再填新砝码
  10. 转 OUI and OPatch Do Not Recognize JDK/JRE on Windows
  11. mysql hibernate 分页查询_Hibernate如何实现分页查询
  12. 国产银河麒麟 安装wps 的简单方法
  13. 计算机自动开机关机设置在哪里设置密码,怎样设置开关机密码
  14. Python学习之文件13
  15. GMK4045-ASEMI光伏逆变器二极管GMK4045
  16. 2020年雅思机考考点将覆盖全国14城
  17. 我的Crystal xcelsius之旅
  18. 全国青少年编程等级考试python一级真题2022年3月(含题库答题软件账号)
  19. 使用基于ggplot2的包ggalluvial绘制桑基图(冲积图)
  20. 用Python做一个会旋转的五彩风筝

热门文章

  1. LeetCode 561. Array Partition I
  2. 《剑指Offer》 变态跳台阶
  3. LeetCode 461. Hamming Distance
  4. 【今日CV 计算机视觉论文速览】Thu, 7 Mar 2019
  5. SpringBoot—启动报错Exception in thread “main“ java.lang.ClassNotFoundException
  6. SpringBoot—数据库初始化脚本配置
  7. 演练 制作百度音乐标签页面 0929
  8. python-字符串与字节-0222
  9. git-创建版本仓库-创建版本-查看版本
  10. redis-数据类型-有序集合