第二期 · 使用 Vue 3.1 + TypeScript + Router + Tailwind.css 仿 itch.io 平臺主頁。
我的主題 HapiGames 是仿 itch.io 的 indie game hosting marketplace。
alicepolice/Vue at 06 (github.com)
當你掌握一門語言的時候,在寫專案之前不妨先看看風格指南吧,前人早為你鋪好了路。下面是我自己編寫專案程式碼時沒有規範到位的幾個點。
Prop 定義應該儘量詳細,至少需要指定其型別。Props | Vue.js (vuejs.org)
Vue的選項式API為我們提供了Prop校驗,你可以向 props
選項提供一個帶有 props 校驗選項的物件,當 prop 的校驗失敗後,Vue 會丟擲一個控制檯警告 (開發模式)。(如果用ts的話更好)
注意 prop 的校驗是在元件範例被建立之前,所以範例的屬性 (比如 data
、computed
等) 將在 default
或 validator
函數中不可用。
因為 v-for 優先順序比 v-if 高,所以每次渲染時必定會遍歷陣列所有元素。避免 v-if 和 v-for 用在一起
將v-if提取到計算屬性後的好處
v-for="item in afterComputed"
之後,在渲染的時候遍歷元素少了,渲染更高效。和父元件緊密耦合的子元件應該以父元件名作為字首命名。緊密耦合的元件名
如果一個元件只在某個父元件的場景下有意義,這層關係應該體現在其名字上。因為編輯器通常會按字母順序組織檔案,所以這樣做可以把相關聯的檔案排在一起。
不建議為了緊密耦合搞目錄區分,因為會出現檔名名字相同、IDE側邊欄瀏覽元件花費時間多的問題。
components/
|- TodoList.vue
|- TodoListItem.vue
|- TodoListItemButton.vue
在單檔案元件、字串模板和 JSX 中沒有內容的元件應該是自閉合的——但在 DOM 模板裡永遠不要這樣做。 自閉合元件
<!-- 在單檔案元件、字串模板和 JSX 中 -->
<MyComponent/>
<!-- 在 DOM 模板中 -->
<my-component></my-component>
在宣告 prop 的時候,其命名應該始終使用 camelCase,而在模板和 JSX 中應該始終使用 kebab-case。 Prop 名大小寫
props: {
greetingText: String
}
<WelcomeMessage greeting-text="hi"/>
應該把複雜計算屬性分割為儘可能多的更簡單的 property。 簡單的計算屬性
好處是易於測試、易於閱讀、更好的「擁抱變化」。
單檔案元件應該總是讓 <script>、<template> 和 <style> 標籤的順序保持一致。且 <style> 要放在最後,因為另外兩個標籤至少要有一個。 單檔案元件的頂級元素的順序
應該優先通過 prop 和事件進行父子元件之間的通訊,而不是
this.$parent
或變更 prop。 隱性的父子元件通訊
資料流應該是單向的,不要反向修改 props。
為了方便偵錯,我們在 index.css 下新增一個樣式組合,通過新增test類樣式類看到塊元素的邊框。
.test{
@apply border border-gray-900
}
├───assets
│ ├───avater
│ │ 使用者頭像
│ ├───blog
│ │ 博文封面圖
│ ├───diffuse
│ │ 模糊背景
│ ├───game
│ │ 遊戲封面圖
│ ├───logo
│ │ 網站logo
│ ├───slideshow
│ │ 輪播圖樣圖
│ └───svg
│ 很多向量圖
├───components
│ ├───common
│ │ BottomBar.vue
│ │ CommentArea.vue
│ │ SideBar.vue
│ │ SideBarHref.vue
│ │ SlideShow.vue
│ │ TopBar.vue
│ │
│ └───HomeView
│ GameBlog.vue
│ GameInfo.vue
│ GameList.vue
│ HomeFAQ.vue
│ HomeFooter.vue
│ PlatformNavigation.vue
│ TopNavigation.vue
│
├───router
│ index.ts
└───views
AboutView.vue
CommentTestView.vue
HomeLoginView.vue
HomeView.vue
LoginView.vue
RegisterView.vue
在 src/components/common 下新建 TopBar.vue,並移入之前寫的 BottomBar.vue。
先從網站頂部開始,該元件在每個頁面都會顯示,並在捲動過程中固定定位。
編寫程式碼,實現頂部欄。
<template>
<div class="h-12 shadow-md">
<div class="inline-block h-full w-16">
<b-icon-list class="text-3xl mt-2 ml-4"></b-icon-list>
</div>
<div class="inline-block h-full w-48">
<img src="@/assets/logo/logo3.png" class="mt-1 h-4/5 w-full" />
</div>
<div class="inline-block float-right mt-2.5 mr-4">
<div
class="
border-2 border-gray-300
px-3.5
py-0.5
rounded-sm
text-sm
font-bold
"
>
Log in
</div>
</div>
</div>
</template>
現在我們推倒重來,實現一下側邊導航欄。
側邊欄導航也叫抽屜式導航是隱藏在介面側邊的位置,一般是通過點選介面左上角的icon彈出,主要承載的內容是除了核心功能意外的主要功能。側邊欄還分全側邊和半側邊。
<div class="mt-3 mx-2">
<input
id="search"
class="
bg-white
focus:outline-none focus:ring focus:border-blue-200
py-1.5
pl-3
w-full
border border-gray-300
text-sm
"
type="text"
placeholder="Search games & creators"
v-model="search"
/>
</div>
三次複用之前定義的SideBarHref元件,並傳入了props
<SideBarHref :items="popularTags"></SideBarHref>
<SideBarHref :items="browse"></SideBarHref>
<SideBarHref :items="gamesByPrice"></SideBarHref>
讓圖示和下載超連結完全資料化,增加網頁動態變化能力。
<div class="h-20 text-center">
<div class="pt-6">
<template v-for="(value, index) in appInfo.apps" :key="index">
<a :href="value.href">
<component
:is="value.icon"
class="inline m-1 text-xl hover:text-rose-500"
></component>
</a>
</template>
<a :href="appInfo.download.href">
<span
class="
text-xs text-stone-800
mx-2
hover:text-rose-500 hover:underline
"
>{{ appInfo.download.title }}</span
>
</a>
</div>
</div>
除非結構要改,現在完全可以靠data裡的物件資料驅動當前側邊欄的所有內容。
data() {
return {
search: "",
popularTags: {
title: "POPULAR TAGS",
items: [
{ text: "Horror games", href: "" },
{ text: "Multiplayer", href: "" },
{ text: "Visual novels", href: "" },
{ text: "HTML5 games", href: "" },
{ text: "Simulation", href: "" },
{ text: "macOS games", href: "" },
{ text: "Roguelike", href: "" },
{ text: "Linux games", href: "" },
{ text: "Browse all tags", href: "" },
],
},
browse: {
title: "BROWSE",
....
我們的想法是按下頂部元件左邊的 list icon,彈出導航欄,再按一次關閉導航欄。
很容易想到父子通訊的解決方案,這也是Vue單向資料流的最佳實現。
<template>
<div class="h-24 p-2 text-sm bg-stone-100">
<div class="mt-1">
<b>HapiGames</b> is a simple way to find and share indie games online for
free.
</div>
<div class="mt-2">
<a class="underline text-rose-500">Add your game</a> or
<a class="underline text-rose-500">Read the FAQ</a>
</div>
</div>
</template>
<script>
export default {
name: "HomeFAQ",
}
</script>
overflow-x-auto flex flex佈局,並在溢位時開啟橫軸卷軸
whitespace-nowrap 類可以防止換行,讓所有元素保持在一行上。
html - Div with horizontal scrolling only - Stack Overflow
<template>
<div class="overflow-x-auto flex bg-white">
<template v-for="(value, index) in topNavigation" :key="index">
<a :href="value.href">
<div
class="
p-3
font-bold
text-sm text-stone-800
hover:text-rose-500
whitespace-nowrap
"
>
{{ value.text }}
</div>
</a>
</template>
</div>
</template>
<script lang="ts">
import { PropType } from "vue";
interface TopNavigation {
text: string;
href: string;
}
export default {
name: "topNavigation",
props: {
topNavigation: {
type: Array as PropType<TopNavigation[]>,
required: true,
},
},
};
</script>
<template>
<div class="m-2 mt-4">
<div class="font-bold">From the blog</div>
<div class="overflow-x-auto flex mt-2">
<template v-for="(value, index) in gameBlog" :key="index">
<div class="w-48 flex-shrink-0 mr-2">
<img class="h-24 w-full" :src="value.img" />
<div class="text-xs font-bold mt-1 text-stone-800 whitespace-normal">
{{ value.title }}
</div>
<div class="h-12 text-xs overflow-clip mt-1 text-stone-500">
{{ value.text }}
</div>
</div>
</template>
</div>
</div>
</template>
<script lang="ts">
import { PropType } from "vue";
interface GameBlog {
title: string;
text: string;
img: string;
}
export default {
name: "GameBlog",
props: {
gameBlog: {
type: Array as PropType<GameBlog[]>,
required: true,
},
},
};
</script>
<template>
<div class="m-2 mt-4">
<div class="font-bold inline-block">Platform & Sale</div>
<div class="flex mt-2 flex-wrap">
<a
:href="value.href"
v-for="(value, index) in platformNavigation"
:key="index"
class="w-1/5 flex-shrink-0 hover:text-rose-500"
>
<div>
<img :src="value.img" class="w-2/5 mx-auto mt-1" />
<div class="text-center m-1.5 text-xs">{{ value.title }}</div>
</div>
</a>
</div>
</div>
</template>
<script lang="ts">
import { PropType } from "vue";
interface platformNavigation {
title: string;
href: string;
img: string;
}
export default {
name: "PlatformNavigation",
props: {
platformNavigation: {
type: Array as PropType<platformNavigation[]>,
required: true,
},
},
data() {
return {};
},
};
</script>
規定了比較複雜的傳入prop型別,考慮到tags可能為空,在原來的模板外層div做v-if判斷,否則會ts報錯value.tags可能為undefined。
interface Game {
title: string;
text: string;
img: string;
price: number;
web?: boolean;
tags?: string[];
}
interface GameList {
title: string;
button: {
title: string;
href: string;
};
games: Game[];
}
<div class="text-xs font-normal mt-1" v-if="value.tags">
<template v-for="(tag, index) in value.tags" :key="index">
<a class="text-rose-500" href="">#{{ tag }}</a>
<template v-if="index != value.tags.length - 1">,</template>
</template>
</div>
完整程式碼
<template>
<div class="m-2 mt-4">
<div>
<div class="font-bold inline-block">{{ gameList.title }}</div>
<div v-if="gameList.button" class="float-right">
<div
class="
border border-rose-400
text-sm
font-bold
text-rose-500
rounded-sm
px-4
py-1
active:bg-rose-400 active:text-white
"
>
{{ gameList.button.title }}
<b-icon-arrow-right
class="inline-block text-lg align-text-top"
></b-icon-arrow-right>
</div>
</div>
<div class="w-full mt-4 flex flex-wrap justify-between">
<template v-for="(value, index) in gameList.games" :key="index">
<div class="w-44 inline-block align-top">
<img class="h-28 w-full" :src="value.img" />
<div
class="text-xs font-bold mt-1 text-stone-800 w-3/4 inline-block"
>
{{ value.title }}
</div>
<div
class="
inline-block
w-1/4
align-top
text-xs
bg-stone-200
rounded-sm
py-0.5
mt-1
text-center
font-bold
"
:class="{ 'bg-stone-500': value.price != 0 }"
>
<span v-if="value.web">WEB</span>
<span v-else-if="value.price == 0">FREE</span>
<span v-else-if="value.price != 0" class="font-normal text-white"
>${{ value.price }}</span
>
</div>
<div class="text-xs font-normal mt-1" v-if="value.tags">
<template v-for="(tag, index) in value.tags" :key="index">
<a class="text-rose-500" href="">#{{ tag }}</a>
<template v-if="index != value.tags.length - 1">,</template>
</template>
</div>
<div class="text-xs font-normal text-stone-500 mt-1">
{{ value.text }}
</div>
<div class="my-1"></div>
</div>
</template>
</div>
</div>
</div>
</template>
<script lang="ts">
import { PropType } from "vue";
interface Game {
title: string;
text: string;
img: string;
price: number;
web?: boolean;
tags?: string[];
}
interface GameList {
title: string;
button: {
title: string;
href: string;
};
games: Game[];
}
export default {
name: "GameList",
props: {
gameList: {
type: Object as PropType<GameList>,
required: true,
},
},
};
</script>
<template>
<div class="mx-2 my-4">
<div class="text-center font-bold text-sm">
Don't see anything you like?
</div>
<div
class="
w-11/12
h-10
pt-2.5
text-center
m-auto
mt-4
border border-rose-500
font-bold
text-sm text-rose-500
"
>
View all Games
<b-icon-arrow-right
class="inline-block text-lg align-text-top"
></b-icon-arrow-right>
</div>
<div
class="
w-11/12
h-10
pt-2.5
text-center
m-auto
mt-4
border border-rose-500
font-bold
text-sm text-rose-500
"
>
View something random
<b-icon-arrow-left-right
class="inline-block text-lg align-text-top"
></b-icon-arrow-left-right>
</div>
</div>
</template>
<script>
export default {
name: "HomeFooter",
props: {
}
}
</script>
這裡列舉我在開發過程遇到的一些問題,也許能幫助到你。
明明 yarn serve 成功了,並顯示如下內容,但連線網頁還是轉圈圈。嘗試重啟電腦後重新 yarn serve
App running at:
- Local: http://localhost:8080/
得到如下報錯
ERROR Error: The project seems to require yarn but it's not installed.
解決方法:刪除當前目錄下的 yarn.lock 檔案,命令列輸入 npm install -g yarn
解決方法:修改 <script> 為 <script lang="ts">
Bootstrap Icons · Official open source SVG icon library for Bootstrap (getbootstrap.com)
Working with props declaration in Vue 3 + Typescript - DEV Community