我的Vue之旅 06 超详细、仿 itch.io 主页设计(Mobile)
第二期 · 使用 Vue 3.1 + TypeScript + Router + Tailwind.css 仿 itch.io 平台主页。
我的主题 HapiGames 是仿 itch.io 的 indie game hosting marketplace。
效果图
代码仓库
alicepolice/Vue at 06 (github.com)
风格指南
当你掌握一门语言的时候,在写项目之前不妨先看看风格指南吧,前人早为你铺好了路。下面是我自己编写项目代码时没有规范到位的几个点。
风格指南 — Vue.js (vuejs.org)
Prop 定义
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-if 和 v-for 用在一起
将v-if提取到计算属性后的好处
- 过滤后的列表只会在对应数组发生相关变化时才被重新运算,过滤更高效。
- 使用
v-for="item in afterComputed"
之后,在渲染的时候遍历元素少了,渲染更高效。 - 解耦渲染层的逻辑,可维护性 (对逻辑的更改和扩展) 更强。
紧密耦合的组件名
和父组件紧密耦合的子组件应该以父组件名作为前缀命名。紧密耦合的组件名
如果一个组件只在某个父组件的场景下有意义,这层关系应该体现在其名字上。因为编辑器通常会按字母顺序组织文件,所以这样做可以把相关联的文件排在一起。
不建议为了紧密耦合搞目录区分,因为会出现文件名名字相同、IDE侧边栏浏览组件花费时间多的问题。
components/
|- TodoList.vue
|- TodoListItem.vue
|- TodoListItemButton.vue
自闭合组件
在单文件组件、字符串模板和 JSX 中没有内容的组件应该是自闭合的——但在 DOM 模板里永远不要这样做。 自闭合组件
Prop 名大小写
在声明 prop 的时候,其命名应该始终使用 camelCase,而在模板和 JSX 中应该始终使用 kebab-case。 Prop 名大小写
props: {
greetingText: String
}
简单的计算属性
应该把复杂计算属性分割为尽可能多的更简单的 property。 简单的计算属性
好处是易于测试、易于阅读、更好的“拥抱变化”。
单文件组件的顶级元素的顺序
单文件组件应该总是让
SideBarHref.vue
在 src/components/common 下新建 SideBarHref.vue
侧边导航栏有相似之处,不妨将这一块提取成独立的组件,然后复用三次。
![]()
添加样式
hover:text-rose-500 hover:underline
,在移动端按下时会改变颜色。![]()
SideBar.vue
遮蔽层
在 src/components/common 下新建 SideBar.vue 以下代码片段均为分段表示,不是完整代码。
先写model层(遮蔽层),一般指侧边栏滚出后背景变黑的部分。
![]()
我们使用自定义类名实现过渡动画。类名也是TailWind.css的类样式,给定200毫秒时间,过渡透明度状态。
div嵌套了两层,把opacity-50写到里面的div层能解决opacity-50在外面div层的时候出现背景全黑问题。
fixed 用于固定遮蔽层。z-30用于设置优先级,先显示在前面。v-show由App.vue传入,顶部组件通知App.vue事件对应的方法修改,进而引发当前transition的过渡。
html - Vue Transition with Tailwind - Stack Overflow
侧边栏
侧边栏的动画效果跟遮蔽层一个原理,只不过修改成为了移动而不是改变透明度。
overflow-auto 可以让侧边栏在内容溢出时具备滚动条。
搜索框
focus:outline-none focus:ring focus:border-blue-200
当当前光标指向该input标签时更改样式,让四角发光变蓝。该段代码也可以提取成基本组件。
SideBarHref
三次复用之前定义的SideBarHref组件,并传入了props
download app
让图标和下载超链接完全数据化,增加网页动态变化能力。
数据驱动
除非结构要改,现在完全可以靠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单向数据流的最佳实现。
![]()
[TS]defineComponent 作用
App.vue 里的
export default defineComponent({
是什么?搭配 TypeScript 使用 Vue | Vue.js (vuejs.org)
defineComponent 是TypeScript独有的,可以根据选项式API的props、data自动推导各个字段的类型,当在生命周期函数、Methods函数、模板表达式中使用这些字段时可以进行类型检查。(不显式引入编译器默认自动引入)
移动端主页
HomeView.vue
我们将一个主页拆分为各个组件,并完全依托数据驱动,图片仅用来本地测试。
HomeFAQ.vue
HapiGames is a simple way to find and share indie games online for free.
TopNavigation
overflow-x-auto flex flex布局,并在溢出时开启横轴滚动条
whitespace-nowrap 类可以防止换行,让所有元素保持在一行上。
html - Div with horizontal scrolling only - Stack Overflow
GameInfo.vue
![]()
嵌入YOUTUBE视频可参考 youtubeembedcode.com
![]()
![]()
用于生成两张轮播图,每四秒切换一次,具体方法如下。注意
currentImg: function (): string[] {
可以给计算属性添加类型检查。methods: { startSlide: function (): void { this.timer = setInterval(this.next, 4000); }, next: function (): void { this.currentIndex += 1; }, }, computed: { currentImg: function (): string[] { let index = Math.abs(this.currentIndex) % this.gameInfo.images.length; let index2 = (index + 1) % this.gameInfo.images.length; return [this.gameInfo.images[index], this.gameInfo.images[index2]]; }, },
考虑 img 标签的 :src 只能接收 string ,我们假设所有 require 方法获取的图片均为 string 类型。定义prop类型
import { PropType } from "vue"; interface GameInfo { youtube: string; title: string; desc: string; price: number; platforms: string[]; images: string[]; }
完整代码如下
{{ gameInfo.title }}{{ gameInfo.desc }}.![]()
${{ gameInfo.price }} Get the game
GameBlog.vue
From the blog
{{ value.title }}{{ value.text }}
TopNavigation.vue
Platform & Sale
GameList.vue
![]()
规定了比较复杂的传入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[]; }
完整代码
{{ gameList.title }}{{ gameList.button.title }}
HomeFooter.vue
Don't see anything you like?View all Games View something random
几个问题
这里列举我在开发过程遇到的一些问题,也许能帮助到你。
ERROR Error: The project seems to require yarn but it's not installed.
明明 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
Type assertion expressions can only be used in TypeScript files.Vetur(8016)
解决方法:修改