用 Vue 寫個移動 SPA 應用

最近看了 Vue 的文檔,想着應該寫點什麼加深下印象,能力有限,就照着葫蘆畫下吧😂,此次的葫蘆是 圖靈社區 移動端頁面
Github: github.com/Jimzjy/itur…
預覽: jimzjy.github.io/ituring-mob…
前端新手,CSS / TS / JS 寫的很爛,望見諒前端

preview

準備工做

使用 vue-cli 建立項目,個人配置是vue

vue-router
vuex
dart-sass
babel
typescript
eslint
複製代碼

刪除自動建立的 HelloWorld,About 等組件、頁面以及路由git

頁面結構

剖析頁面

咱們看下要作的部分的大致導航結構github

struct

歸納爲(上面的圖少 more-books 的頁面)vue-router

- pages
    - [pages-content]
        - header-bar
        - home / book / article
    - [user-info]
    - bottom-navbar
- login
- more-books
複製代碼

建立基本結構組件

新建如下文件vuex

views/
    Pages.vue
    PgaesContent.vue
    Login.vue
    MoreBooks.vue
    NotFound.vue
    pagesContents/
        Home.vue
        Book.vue
        Article.vue
        User.vue    // 對應上文結構中的 user-info, 按照結構其實應該直接放在 views/ 目錄下
components/
    HeaderMenu.vue
    HeaderNav.vue   // 兩個 Header 組件一塊兒對應 header-bar
    BottomNavbar.vue
複製代碼

添加 routevue-cli

routePageNames = ['home', 'book', 'article', 'user']

{
  path: '/',
  component: Pages,
  children: [
    {
      path: '',
      component: PagesContent,
      children: [
        {
          path: '',
          component: Home,
          name: routePageNames[0]
        },
        {
          path: 'book',
          component: Book,
          name: routePageNames[1],
        },
        {
          path: 'article',
          component: Article,
          name: routePageNames[2],
        }
      ]
    },
    {
      path: 'user',
      component: User,
      name: routePageNames[3],
    }
  ]
},
{
  path: '/login',
  component: Login,
  name: 'login',
},
{
  path: '/more-books',
  component: MoreBooks,
  name: 'more-books',
}
{
  path: '*',
  component: NotFound,
  name: 'not-found'
}
複製代碼

OK,如今文件結構已經知足了上面結構,接下來開始填充結構內容typescript

開始填充架構

準備工做

MockJS 來模擬數據npm

npm install mockjs
npm install @types/mockjs -D
複製代碼

新建文件api

src/
    mock/
        index.ts    // mock 數據
    service/
        index.ts    // 獲取數據的 Url
複製代碼

頭部

頭部由兩個組件組成 HeaderMenu 和 HeaderNav, 代碼能夠在最上面的連接中找到
HeaderMenu

header-menu
HeaderNav 有兩種樣子
在 Home 頁面的
header-nav-home
在 Book 和 Article 頁面的
header-nav-others
咱們使用 BottomNavbar 來切換頁面,可是 HeaderNav 和 BottomNavbar 既不是父子也不是兄弟,那麼咱們不能直接傳參,那麼咱們能夠經過 Vuex 設置一個判斷 Home 和 其它 兩種狀態的 state,可是這樣須要在 route 上設置守衛或者在 Home 和 Article 組件上操做,或者還能夠經過監聽 $route,讓 HeaderNav 本身去處理,這裏我選擇後者

service/index.ts
const topicsUrl = api + '/topics'

mock/index.ts
Mock.mock(`${topicsUrl}/${routePageNames[0]}`, shuffle(topicsHome).concat('最新上線', '每週特價'))
Mock.mock(`${topicsUrl}/${routePageNames[1]}`, topicsBook)
Mock.mock(`${topicsUrl}/${routePageNames[2]}`, topicsArticle)
// 內容均可以在源碼中找到,篇幅有限就只列重點了,下同

HeaderNav.vue

@Watch('$route')
updateTopics () {
    this.isHome = this.$route.name === routePageNames[0] // 經過 isHome 來判斷是不是 Home,routePageNames 是以前在 route 裏寫的
    this.$http.get(`${topicsUrl}/${this.$route.name}`).then((resposne: any) => {
        this.topicsData = resposne.data
    })
}

<div v-if="isHome">...</div>
<div v-if="!isHome">...</div>
複製代碼

在 PagesContent.vue 添加 HeaderNav 和 HeaderMenu

<header>
  <header-menu></header-menu>
  <header-nav></header-nav>
</header>
<router-view/>  // Home / Book / Article
<div id="bottom-space"></div>   // 爲 BottomNavbar 留底部空間
複製代碼

底部

BottomNavbar 用到了 vue-awesome 這個 icon 庫

npm install vue-awesome
複製代碼

在這個項目中我用的都是自定義icon,能夠在 src/asstes/icon/customIcons.ts 中找到
經過 fill 和 .router-link-active 一塊兒配合,能夠很輕鬆的實現根據不一樣路由改變 icon 的顏色

.fa-icon {
    ...

    fill: #C8CDD4;
}

.navbar-tab {
    ...

    &.router-link-active {
        color: $primary-color;

        .fa-icon {
            fill: $primary-color;
        }
    }
}
複製代碼

在 Pages.vue 中添加 BottomNavbar

<router-view/>  // PagesContent
<bottom-navbar></bottom-navbar>
複製代碼

用戶和登陸

登陸狀態能夠用 Vuex 來設置一個 loginStatus(true爲登陸)

state: {
  loginStatus: false
},
mutations: {
  login (state) {
    state.loginStatus = true
  },
  logout (state) {
    state.loginStatus = false
  }
},
複製代碼

當用戶登陸狀態爲 false 時,不容許用戶進入 /user ,因此咱們在 /user 上添加一個路由守衛

{
  path: 'user',
  component: User,
  name: routePageNames[3],
  + beforeEnter: authGuard
}

function authGuard (to: Route, from: Route, next: Function) {
  if ($store.state.loginStatus) {
    next()
    return
  }

  next({ name: 'login', query: { to: to.name } })
}
複製代碼

在上面的代碼中給到 login 的路由加了 query: { to: to.name } } ,以後咱們在 Login 組件中就能夠獲取到原本用戶想要去的路由,在登陸後就能夠跳轉到用戶原來想要取得路由,咱們能夠經過 $route.query 獲取到信息,或者可使用

route.ts
{
  path: '/login',
  component: Login,
  name: 'login',
  + props: (route) => ({ to: route.query.to })
}

Login.vue
@Prop({ default: 'home' }) readonly to!: string
複製代碼

在路由中添加 props 將 query.to 變成添加給 Login 的屬性
在 Login 中添加 login 方法,在 User 中添加 logout 方法

Login.vue
onLoginClick () {
    this.$store.commit('login')
    this.$router.push({ name: this.to })
}

User.vue
onLogoutClick () {
    this.$store.commit('logout')
    this.$router.push('/')
}
複製代碼

開始填充內部

開始填充 PagesContent 內部,MoreBooks 中的組件會複用內部用過的,因此會和內部一塊兒說明

Home

Swiper

Home 中的第一個組件是一個 Swiper,使用 vue-swipe

npm install vue-swipe
複製代碼

再新建 components/Swiper.vue 再封裝下 vue-swipe

Swiper.vue

<swipe :showIndicators=false :speed=3000>
  <swipe-item v-for="(item, index) in data" :key="index" class="swipe-item">
    <p class="title">{{ item.title }}</p>
    <p class="content">{{ item.content }}</p>
  </swipe-item>
</swipe>

@Prop() readonly data!: Array<InfoSwipe>
複製代碼

在 Home 中經過 Mock 獲取數據,傳給 Swiper

created () {
    this.updateData()
}

updateData () {
    this.$http.get(homeDataUrl).then((resposne: any) => {
        this.data = resposne.data
    })
}

<swiper :data="data.infoSwipe"></swiper>
複製代碼

SpecialView

在 Home 中有兩種組件有着一樣的外組件
書的列表

special-view0
文章的列表
special-view1
新建 components/SpecialView.vue

SpecialView.vue
<div class="title">
  <div class="title-content">
    ...
  </div>
  <router-link class="more" v-if="more" :to="{ name: 'more-books', query: { title } }">更多</router-link>
</div>
<slot></slot>

Home.vue
<special-view :title="item.title" :more="item.books.length > 4">
  ...
</special-view>
複製代碼

這裏我偷了下懶,更多按鈕只去 /more-books

BookListView

書的列表也有兩種樣子
橫向滾動

book-list-view0
wrap
book-list-view1

經過 wrap 屬性改變 class 來改變樣式

components/BookListView.vue
<div :class="['book-list-view', wrap ? 'wrap-list' : '']">
    ...
</div>

@Prop() readonly books!: Array<Book>
@Prop({ default: false }) readonly wrap!: boolean

Home.vue
<div v-for="(item, index) in data.booksWithTitle" :key="index" >
  <special-view :title="item.title" :more="item.books.length > 4">
    <book-list-view :books="item.books"></book-list-view>
  </special-view>
  <sepline></sepline>
</div>

MoreBooks.vue
<book-list-view :books="books" :wrap=true></book-list-view>
複製代碼

Book

TopicTabView

在 Book 和 Article 中都有一個 tabview

topic-tab-view
點擊 tab 時要更新 Book 的內容,因此在 TopicTabView 上能夠加一個監聽

components/TopicTabView.vue
@Emit()
onTopicClick (n: number) {
    this.topicChecked = n
    return n
}

Book.vue
<topic-tab-view @on-topic-click="onTopicChange"></topic-tab-view>

onTopicChange (n: number) {
    this.updateData()
}

updateData () {
    this.$http.get(moreBooksUrl).then((response: any) => {
        this.books = response.data
    })
}
複製代碼

內容

Book 的內容能夠直接複用 BookListView 的 wrap 模式

Article

ArticleListView

Article 和 Home 中都有文章的列表,樣式相差並很少,能夠像以前同樣經過屬性改變 class

article-list-view0
article-list-view1

components/ArticleListView.vue
<div v-if="showTag">
  ...
</div>
<div v-if="!showTag">
  ...
</div>

Article.vue
<article-list-view :articles="articles" class="articles"></article-list-view>

Home.vue
<special-view title="推薦文章">
  <article-list-view :articles="data.articles" :showTag=false></article-list-view>
</special-view>
複製代碼

內容更新

如今從樣子的角度來講已經構建完了,可是還有一個問題,當在 Book 和 Article 中點擊 HeaderNav 的選項不會有任何效果,由於 HeaderNav 和 Book 以及 Article 的 內容部分,既不是父子也不是兄弟,咱們又不能直接傳參或者監聽了
那咱們能夠經過在 route 上加屬性,HeaderNav 經過 route 上的屬性切換,點擊後改變 route 的屬性刷新頁面,嗯...,感受消耗有點大
那咱們還能夠經過 Vuex

state: {
    + currentHeaderNav: 0
},
mutations: {
    + changeCurrentHeaderNav (state, n) {
      state.currentHeaderNav = n
    }
},
複製代碼

咱們添加了一個 currentHeaderNav 用於表示如今的 HeaderNav 序號

<div :class="[isCurrentNav(index) ? 'topic-content-foucused' : 'topic-content']" @click="onTopicClick(index)">{{ topic }}</div>

isCurrentNav (n: number): boolean {
    return this.currentHeaderNav === n
}

onTopicClick (n: number) {
    this.$store.commit('changeCurrentHeaderNav', n)
}
複製代碼

在 Book 和 Article 中添加監聽

created () {
    this.updateData()
    this.subscription = this.$store.subscribe(mutation => {
        if (mutation.type === 'changeCurrentHeaderNav') {
            this.updateData()
        }
    })
}

destroyed () {
    this.subscription()
}
複製代碼

如今已經能夠正常更新內容了,可是還有一個問題,在 Book 和 Article 中序號是共享的,因此咱們要在進入 Book 和 Article 前初始化序號,否則的話序號就亂套了,添加路由守衛

{
  path: 'book',
  component: Book,
  name: routePageNames[1],
  + beforeEnter: refreshHeaderNav
},
{
  path: 'article',
  component: Article,
  name: routePageNames[2],
  + beforeEnter: refreshHeaderNav
}

function refreshHeaderNav (to: Route, from: Route, next: Function) {
  $store.commit('changeCurrentHeaderNav', 0)

  next()
}
複製代碼

OK!如今就完成了

End

相關文章
相關標籤/搜索