【前端Vue】Vue3+Pinia小兔鮮電商專案第2篇:什麼是pinia,1. 建立空Vue專案【附程式碼文件】

程序员一诺yinuo發表於2024-03-21

全套筆記資料程式碼移步: 前往gitee倉庫檢視

感興趣的小夥伴可以自取哦,歡迎大家點贊轉發~


全套教程部分目錄:


部分檔案圖片:

什麼是pinia

Pinia 是 Vue 的專屬狀態管理庫,可以實現跨元件或頁面共享狀態,是 vuex 狀態管理工具的替代品,和 Vuex相比,具備以下優勢

  1. 提供更加簡單的API (去掉了 mutation )
  2. 提供符合組合式API風格的API (和 Vue3 新語法統一)
  3. 去掉了modules的概念,每一個store都是一個獨立的模組
  4. 搭配 TypeScript 一起使用提供可靠的型別推斷

建立空Vue專案並安裝Pinia

1. 建立空Vue專案

npm init vue@latest

2. 安裝Pinia並註冊

npm i pinia

import { createPinia } from 'pinia'

const app = createApp(App)
// 以外掛的形式註冊
app.use(createPinia())
app.use(router)
app.mount('#app')

實現counter

核心步驟:

  1. 定義store
  2. 元件使用store

1- 定義store

import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useCounterStore = defineStore('counter', ()=>{
  // 資料 (state)
  const count = ref(0)

  // 修改資料的方法 (action)
  const increment = ()=>{
    count.value++
  }

  // 以物件形式返回
  return {
    count,
    increment
  }
})

2- 元件使用store

<script setup>
  // 1. 匯入use方法
	import { useCounterStore } from '@/stores/counter'
  // 2. 執行方法得到store store裡有資料和方法
  const counterStore = useCounterStore()
</script>

<template>
	<button @click="counterStore.increment">
    {{ counterStore.count }}
  </button>
</template>

實現getters

getters直接使用計算屬性即可實現

// 資料(state)
const count = ref(0)
// getter (computed)
const doubleCount = computed(() => count.value * 2)

非同步action

思想:action函式既支援同步也支援非同步,和在元件中傳送網路請求寫法保持一致
步驟:

  1. store中定義action
  2. 元件中觸發action

1- store中定義action

const API_URL = '

export const useCounterStore = defineStore('counter', ()=>{
  // 資料
  const list = ref([])
  // 非同步action
  const loadList = async ()=>{
    const res = await axios.get(API_URL)
    list.value = res.data.data.channels
  }
  
  return {
    list,
    loadList
  }
})

2- 元件中呼叫action

<script setup>
	import { useCounterStore } from '@/stores/counter'
  const counterStore = useCounterStore()
  // 呼叫非同步action
  counterStore.loadList()
</script>

<template>
	<ul>
    <li v-for="item in counterStore.list" :key="item.id">{{ item.name }}</li>
  </ul>
</template>

storeToRefs保持響應式解構

直接基於store進行解構賦值,響應式資料(state和getter)會丟失響應式特性,使用storeToRefs輔助保持響應式

<script setup>
  import { storeToRefs } from 'pinia'
	import { useCounterStore } from '@/stores/counter'
  const counterStore = useCounterStore()
  // 使用它storeToRefs包裹之後解構保持響應式
  const { count } = storeToRefs(counterStore)

  const { increment } = counterStore
  
</script>

<template>
	<button @click="increment">
    {{ count }}
  </button>
</template>

建立專案並整理目錄

npm init vue@latest

![image.png](

jsconfig.json配置別名路徑

配置別名路徑可以在寫程式碼時聯想提示路徑

{
  "compilerOptions" : {
    "baseUrl" : "./",
    "paths" : {
      "@/*":["src/*"]
    }
  }
}

elementPlus引入

1. 安裝elementPlus和自動匯入外掛

npm i elementPlus
npm install -D unplugin-vue-components unplugin-auto-import

2. 配置自動按需匯入

// 引入外掛
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'


export default defineConfig({
  plugins: [
    // 配置外掛
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ]
})

3. 測試元件

<template>
  <el-button type="primary">i am button</el-button>
</template>

定製elementPlus主題

1. 安裝sass

基於vite的專案預設不支援css前處理器,需要開發者單獨安裝

npm i sass -D

2. 準備定製化的樣式檔案

/* 只需要重寫你需要的即可 */
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
  $colors: (
    'primary': (
      // 主色
      'base': #27ba9b,
    ),
    'success': (
      // 成功色
      'base': #1dc779,
    ),
    'warning': (
      // 警告色
      'base': #ffb302,
    ),
    'danger': (
      // 危險色
      'base': #e26237,
    ),
    'error': (
      // 錯誤色
      'base': #cf4444,
    ),
  )
)

3. 自動匯入配置

這裡自動匯入需要深入到elementPlus的元件中,按照官方的配置文件來

  1. 自動匯入定製化樣式檔案進行樣式覆蓋
  2. 按需定製主題配置 (需要安裝 unplugin-element-plus)
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// 匯入對應包
import ElementPlus from 'unplugin-element-plus/vite'
export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
    // 按需定製主題配置
    ElementPlus({
      useSource: true,
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  css: {
    preprocessorOptions: {
      scss: {
        // 自動匯入定製化樣式檔案進行樣式覆蓋
        additionalData: `
          @use "@/styles/element/index.scss" as *;
        `,
      }
    }
  }
})

axios安裝並簡單封裝

1. 安裝axios

npm i axios

2. 基礎配置

官方文件地址:[
基礎配置通常包括:

  1. 例項化 - baseURL + timeout
  2. 攔截器 - 攜帶token 401攔截等
import axios from 'axios'

// 建立axios例項
const http = axios.create({
  baseURL: '
  timeout: 5000
})

// axios請求攔截器
instance.interceptors.request.use(config => {
  return config
}, e => Promise.reject(e))

// axios響應式攔截器
instance.interceptors.response.use(res => res.data, e => {
  return Promise.reject(e)
})


export default http

3. 封裝請求函式並測試

import http from '@/utils/http'

export function getCategoryAPI () {
  return http({
    url: 'home/category/head'
  })
}

路由整體設計

路由設計原則:找頁面的切換方式,如果是整體切換,則為一級路由,如果是在一級路由的內部進行的內容切換,則為二級路由

<template>
  我是登入頁
</template>
<template>
  我是首頁
</template>
<template>
  我是home
</template>
<template>
  我是分類
</template>
// createRouter:建立router例項物件
// createWebHistory:建立history模式的路由

import { createRouter, createWebHistory } from 'vue-router'
import Login from '@/views/Login/index.vue'
import Layout from '@/views/Layout/index.vue'
import Home from '@/views/Home/index.vue'
import Category from '@/views/Category/index.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  // path和component對應關係的位置
  routes: [
    {
      path: '/',
      component: Layout,
      children: [
        {
          path: '',
          component: Home
        },
        {
          path: 'category',
          component: Category
        }
      ]
    },
    {
      path: '/login',
      component: Login
    }
  ]
})

export default router

靜態資源引入和Error Lens安裝

1. 靜態資源引入

  1. 圖片資源 - 把 images 資料夾放到 assets 目錄下
  2. 樣式資源 - 把 common.scss 檔案放到 styles 目錄下

2. Error Lens外掛安裝

![image.png](

scss變數自動匯入

$xtxColor: #27ba9b;
$helpColor: #e26237;
$sucColor: #1dc779;
$warnColor: #ffb302;
$priceColor: #cf4444;
css: {
    preprocessorOptions: {
      scss: {
        // 自動匯入scss檔案
        additionalData: `
          @use "@/styles/element/index.scss" as *;
          @use "@/styles/var.scss" as *;
        `,
      }
    }
}

元件結構快速搭建

<script setup>

</script>

<template>
  <nav class="app-topnav">
    <div class="container">
      <ul>
        <template v-if="true">
          <li><a href="javascript:;""><i class="iconfont icon-user"></i>周杰倫</a></li>
          <li>
            <el-popconfirm title="確認退出嗎?" confirm-button-text="確認" cancel-button-text="取消">
              <template #reference>
                <a href="javascript:;">退出登入</a>
              </template>
            </el-popconfirm>
          </li>
          <li><a href="javascript:;">我的訂單</a></li>
          <li><a href="javascript:;">會員中心</a></li>
        </template>
        <template v-else>
          <li><a href="javascript:;">請先登入</a></li>
          <li><a href="javascript:;">幫助中心</a></li>
          <li><a href="javascript:;">關於我們</a></li>
        </template>
      </ul>
    </div>
  </nav>
</template>


<style scoped lang="scss">
.app-topnav {
  background: #333;
  ul {
    display: flex;
    height: 53px;
    justify-content: flex-end;
    align-items: center;
    li {
      a {
        padding: 0 15px;
        color: #cdcdcd;
        line-height: 1;
        display: inline-block;

        i {
          font-size: 14px;
          margin-right: 2px;
        }

        &:hover {
          color: $xtxColor;
        }
      }

      ~li {
        a {
          border-left: 2px solid #666;
        }
      }
    }
  }
}
</style>
<script setup>

</script>

<template>
  <header class='app-header'>
    <div class="container">
      <h1 class="logo">
        <RouterLink to="/">小兔鮮</RouterLink>
      </h1>
      <ul class="app-header-nav">
        <li class="home">
          <RouterLink to="/">首頁</RouterLink>
        </li>
        <li> <RouterLink to="/">居家</RouterLink> </li>
        <li> <RouterLink to="/">美食</RouterLink> </li>
        <li> <RouterLink to="/">服飾</RouterLink> </li>
      </ul>
      <div class="search">
        <i class="iconfont icon-search"></i>
        <input type="text" placeholder="搜一搜">
      </div>
      <!-- 頭部購物車 -->
      
    </div>
  </header>
</template>


<style scoped lang='scss'>
.app-header {
  background: #fff;

  .container {
    display: flex;
    align-items: center;
  }

  .logo {
    width: 200px;

    a {
      display: block;
      height: 132px;
      width: 100%;
      text-indent: -9999px;
      background: url('@/assets/images/logo.png') no-repeat center 18px / contain;
    }
  }

  .app-header-nav {
    width: 820px;
    display: flex;
    padding-left: 40px;
    position: relative;
    z-index: 998;
  
    li {
      margin-right: 40px;
      width: 38px;
      text-align: center;
  
      a {
        font-size: 16px;
        line-height: 32px;
        height: 32px;
        display: inline-block;
  
        &:hover {
          color: $xtxColor;
          border-bottom: 1px solid $xtxColor;
        }
      }
  
      .active {
        color: $xtxColor;
        border-bottom: 1px solid $xtxColor;
      }
    }
  }

  .search {
    width: 170px;
    height: 32px;
    position: relative;
    border-bottom: 1px solid #e7e7e7;
    line-height: 32px;

    .icon-search {
      font-size: 18px;
      margin-left: 5px;
    }

    input {
      width: 140px;
      padding-left: 5px;
      color: #666;
    }
  }

  .cart {
    width: 50px;

    .curr {
      height: 32px;
      line-height: 32px;
      text-align: center;
      position: relative;
      display: block;

      .icon-cart {
        font-size: 22px;
      }

      em {
        font-style: normal;
        position: absolute;
        right: 0;
        top: 0;
        padding: 1px 6px;
        line-height: 1;
        background: $helpColor;
        color: #fff;
        font-size: 12px;
        border-radius: 10px;
        font-family: Arial;
      }
    }
  }
}
</style>
<template>
  <footer class="app_footer">
    <!-- 聯絡我們 -->
    <div class="contact">
      <div class="container">
        <dl>
          <dt>客戶服務</dt>
          <dd><i class="iconfont icon-kefu"></i> 線上客服</dd>
          <dd><i class="iconfont icon-question"></i> 問題反饋</dd>
        </dl>
        <dl>
          <dt>關注我們</dt>
          <dd><i class="iconfont icon-weixin"></i> 公眾號</dd>
          <dd><i class="iconfont icon-weibo"></i> 微博</dd>
        </dl>
        <dl>
          <dt>下載APP</dt>
          <dd class="qrcode"><img src="images/41474d5f-a7c8-40c2-b60c-14bbbfa30f13-1710675035.jpg" /></dd>
          <dd class="download">
            <span>掃描二維碼</span>
            <span>立馬下載APP</span>
            <a href="javascript:;">下載頁面</a>
          </dd>
        </dl>
        <dl>
          <dt>服務熱線</dt>
          <dd class="hotline">400-0000-000 <small>週一至週日 8:00-18:00</small></dd>
        </dl>
      </div>
    </div>
    <!-- 其它 -->
    <div class="extra">
      <div class="container">
        <div class="slogan">
          <a href="javascript:;">
            <i class="iconfont icon-footer01"></i>
            <span>價格親民</span>
          </a>
          <a href="javascript:;">
            <i class="iconfont icon-footer02"></i>
            <span>物流快捷</span>
          </a>
          <a href="javascript:;">
            <i class="iconfont icon-footer03"></i>
            <span>品質新鮮</span>
          </a>
        </div>
        <!-- 版權資訊 -->
        <div class="copyright">
          <p>
            <a href="javascript:;">關於我們</a>
            <a href="javascript:;">幫助中心</a>
            <a href="javascript:;">售後服務</a>
            <a href="javascript:;">配送與驗收</a>
            <a href="javascript:;">商務合作</a>
            <a href="javascript:;">搜尋推薦</a>
            <a href="javascript:;">友情連結</a>
          </p>
          <p>CopyRight © 小兔鮮兒</p>
        </div>
      </div>
    </div>
  </footer>
</template>

<style scoped lang='scss'>
.app_footer {
  overflow: hidden;
  background-color: #f5f5f5;
  padding-top: 20px;

  .contact {
    background: #fff;

    .container {
      padding: 60px 0 40px 25px;
      display: flex;
    }

    dl {
      height: 190px;
      text-align: center;
      padding: 0 72px;
      border-right: 1px solid #f2f2f2;
      color: #999;

      &:first-child {
        padding-left: 0;
      }

      &:last-child {
        border-right: none;
        padding-right: 0;
      }
    }

    dt {
      line-height: 1;
      font-size: 18px;
    }

    dd {
      margin: 36px 12px 0 0;
      float: left;
      width: 92px;
      height: 92px;
      padding-top: 10px;
      border: 1px solid #ededed;

      .iconfont {
        font-size: 36px;
        display: block;
        color: #666;
      }

      &:hover {
        .iconfont {
          color: $xtxColor;
        }
      }

      &:last-child {
        margin-right: 0;
      }
    }

    .qrcode {
      width: 92px;
      height: 92px;
      padding: 7px;
      border: 1px solid #ededed;
    }

    .download {
      padding-top: 5px;
      font-size: 14px;
      width: auto;
      height: auto;
      border: none;

      span {
        display: block;
      }

      a {
        display: block;
        line-height: 1;
        padding: 10px 25px;
        margin-top: 5px;
        color: #fff;
        border-radius: 2px;
        background-color: $xtxColor;
      }
    }

    .hotline {
      padding-top: 20px;
      font-size: 22px;
      color: #666;
      width: auto;
      height: auto;
      border: none;

      small {
        display: block;
        font-size: 15px;
        color: #999;
      }
    }
  }

  .extra {
    background-color: #333;
  }

  .slogan {
    height: 178px;
    line-height: 58px;
    padding: 60px 100px;
    border-bottom: 1px solid #434343;
    display: flex;
    justify-content: space-between;

    a {
      height: 58px;
      line-height: 58px;
      color: #fff;
      font-size: 28px;

      i {
        font-size: 50px;
        vertical-align: middle;
        margin-right: 10px;
        font-weight: 100;
      }

      span {
        vertical-align: middle;
        text-shadow: 0 0 1px #333;
      }
    }
  }

  .copyright {
    height: 170px;
    padding-top: 40px;
    text-align: center;
    color: #999;
    font-size: 15px;

    p {
      line-height: 1;
      margin-bottom: 20px;
    }

    a {
      color: #999;
      line-height: 1;
      padding: 0 10px;
      border-right: 1px solid #999;

      &:last-child {
        border-right: none;
      }
    }
  }
}
</style>
<script setup>
import LayoutNav from './components/LayoutNav.vue'
import LayoutHeader from './components/LayoutHeader.vue'
import LayoutFooter from './components/LayoutFooter.vue'
</script>

<template>
  <LayoutNav />
  <LayoutHeader />
  <RouterView />
  <LayoutFooter />
</template>

字型圖示渲染

字型圖示採用的是阿里的字型圖示庫,樣式檔案已經準備好,在 index.html檔案中引入即可

  <link rel="stylesheet" href="//at.alicdn.com/t/font_2143783_iq6z4ey5vu.css">

一級導航渲染

![image.png](
實現步驟

  1. 封裝介面函式
  2. 呼叫介面函式
  3. v-for渲染模版

程式碼落地


import httpInstance from '@/utils/http'

export function getCategoryAPI () {
  return httpInstance({
    url: '/home/category/head'
  })
}
<script setup>
  import { getCategoryAPI } from '@/apis/layout'
  import { onMounted, ref } from 'vue'

  const categoryList = ref([])
  const getCategory = async () => {
    const res = await getCategoryAPI()
    categoryList.value = res.result
  }

  onMounted(() => getCategory())

</script>

<template>
  <header class='app-header'>
    <div class="container">
      <h1 class="logo">
        <RouterLink to="/">小兔鮮</RouterLink>
      </h1>
      <ul class="app-header-nav">
        <li class="home" v-for="item in categoryList" :key="item.id">
          <RouterLink to="/">{{ item.name }}</RouterLink>
        </li>
      </ul>
      <div class="search">
        <i class="iconfont icon-search"></i>
        <input type="text" placeholder="搜一搜">
        </div>
      <!-- 頭部購物車 -->
    </div>
  </header>
</template>

吸頂導航互動實現

1. 準備元件靜態結構

<script setup>

</script>

<template>
  <div class="app-header-sticky">
    <div class="container">
      <RouterLink class="logo" to="/" />
      <!-- 導航區域 -->
      <ul class="app-header-nav ">
        <li class="home">
          <RouterLink to="/">首頁</RouterLink>
        </li>
        <li>
          <RouterLink to="/">居家</RouterLink>
        </li>
        <li>
          <RouterLink to="/">美食</RouterLink>
        </li>
        <li>
          <RouterLink to="/">服飾</RouterLink>
        </li>
        <li>
          <RouterLink to="/">母嬰</RouterLink>
        </li>
        <li>
          <RouterLink to="/">個護</RouterLink>
        </li>
        <li>
          <RouterLink to="/">嚴選</RouterLink>
        </li>
        <li>
          <RouterLink to="/">數碼</RouterLink>
        </li>
        <li>
          <RouterLink to="/">運動</RouterLink>
        </li>
        <li>
          <RouterLink to="/">雜項</RouterLink>
        </li>
      </ul>

      <div class="right">
        <RouterLink to="/">品牌</RouterLink>
        <RouterLink to="/">專題</RouterLink>
      </div>
    </div>
  </div>
</template>


<style scoped lang='scss'>
.app-header-sticky {
  width: 100%;
  height: 80px;
  position: fixed;
  left: 0;
  top: 0;
  z-index: 999;
  background-color: #fff;
  border-bottom: 1px solid #e4e4e4;
  // 此處為關鍵樣式!!!
  // 狀態一:往上平移自身高度 + 完全透明
  transform: translateY(-100%);
  opacity: 0;

  // 狀態二:移除平移 + 完全不透明
  &.show {
    transition: all 0.3s linear;
    transform: none;
    opacity: 1;
  }

  .container {
    display: flex;
    align-items: center;
  }

  .logo {
    width: 200px;
    height: 80px;
    background: url("@/assets/images/logo.png") no-repeat right 2px;
    background-size: 160px auto;
  }

  .right {
    width: 220px;
    display: flex;
    text-align: center;
    padding-left: 40px;
    border-left: 2px solid $xtxColor;

    a {
      width: 38px;
      margin-right: 40px;
      font-size: 16px;
      line-height: 1;

      &:hover {
        color: $xtxColor;
      }
    }
  }
}

.app-header-nav {
  width: 820px;
  display: flex;
  padding-left: 40px;
  position: relative;
  z-index: 998;

  li {
    margin-right: 40px;
    width: 38px;
    text-align: center;

    a {
      font-size: 16px;
      line-height: 32px;
      height: 32px;
      display: inline-block;

      &:hover {
        color: $xtxColor;
        border-bottom: 1px solid $xtxColor;
      }
    }

    .active {
      color: $xtxColor;
      border-bottom: 1px solid $xtxColor;
    }
  }
}
</style>

2. 渲染基礎資料

3. 實現吸頂互動

核心邏輯:根據滾動距離判斷當前show類名是否顯示,大於78顯示,小於78,不顯示

<script setup>
import LayoutHeaderUl from './LayoutHeaderUl.vue'
// vueUse
import { useScroll } from '@vueuse/core'
const { y } = useScroll(window)
</script>

<template>
  <div class="app-header-sticky" :class="{ show: y > 78 }">
    <!-- 省略部分程式碼 -->
  </div>
</template>

Pinia最佳化重複請求

import { ref } from 'vue'
import { defineStore } from 'pinia'
import { getCategoryAPI } from '@/apis/layout'
export const useCategoryStore = defineStore('category', () => {
  // 導航列表的資料管理
  // state 導航列表資料
  const categoryList = ref([])

  // action 獲取導航資料的方法
  const getCategory = async () => {
    const res = await getCategoryAPI()
    categoryList.value = res.result
  }

  return {
    categoryList,
    getCategory
  }
})

未完待續, 同學們請等待下一期

全套筆記資料程式碼移步: 前往gitee倉庫檢視

感興趣的小夥伴可以自取哦,歡迎大家點贊轉發~

相關文章