电影搜索项目-Vue3.0
技术栈:
- Vue Js、Vue-router、 Vite
- Tailwind
- Axios、OMDB API
简介
使用vite构建,Vue使用3.0版本,结合Vue router渲染页面,使用Tailwindcss构建响应式页面,使用Axios获取数据,写入页面。
预备知识
.env 文件
环境变量和模式 - vite官方文档
.env - 所有情况下都会加载
为了防止意外地将一些环境变量泄漏到客户端,只有以 VITE_ 为前缀的变量才会暴露给经过 vite 处理的代码。
1 | 例子: |
postcss - 来自package.json
postcss - 官方文档
这里使用的是autoprefixer - 自动添加前缀
认识Vue 的 export、export default、import
ES6模块主要有两个功能:export和import
在一个文件或模块中,export、import可以有多个,export default仅有一个
通过export方式导出,在导入时要加{ },export default则不需要
JavaScript中Map和ForEach的区别
Map - 返回新的数组
ForEach - 不返回新的数组
都是执行函数
import { ref } from "vue";
Vue 3: Reactivity Made Easy
响应式数据
import { watchEffect, ref } from '@vue/runtime-core'
使用到了哪个ref/reactive对象.value,就监听哪个
自动收集,初始化自动执行一次,拿不到oldValue
<script setup>
script setup - 官方文档
Vue3.0的新语法糖-script setup
- 当使用
<script setup>
的时候,任何在<script setup>
声明的顶层的绑定 (包括变量,函数声明,以及 import 引入的内容) 都能在模板中直接使用: - import 导入的内容也会以同样的方式暴露。
- 组件也能使用
- 响应式状态需要明确使用响应式 APIs 来创建 - ref
<script setup>
中可以使用顶层 await。结果代码会被编译成 async setup():- ? 省了生命周期???- 省了很多。。
defineProps
defineProps 和 defineEmits
在 <script setup>
中必须使用 defineProps 和 defineEmits API 来声明 props 和 emits ,它们具备完整的类型推断并且在 <script setup>
中是直接可用的:
使用props的方式
vue路由上信息 - 看看怎么用的。。。
1 | meta: { |
// 怎么用的???有种引入页面是给当前页面写入jsx的感觉
1 | const router = createRouter({ // 创建可供 Vue 应用程序使用的 Router 实例。 |
import { onMounted } from '@vue/runtime-core'
注册组件挂载后要调用的回调。 - 生命周期钩子
构建过程
开始!
目录结构
src
- api - 获取资源 - axios
- assets - 资源 - css + png
- components - 可复用组件
- router
- views - 配合路由
- App.vue - 渲染页面 + 动画效果
- main.js - 起点 - 引入资源 - 全局css
.env - 环境变量
// 配置文件
tailwind.config.js
vite.config.js
postcss.config.js
路由信息
可以看到页面主要内容
/ - Home.vue
/detail/:id - Details.vue
/fav - Favorite.vue
/:pathMath(.) - NotFound.vue
数据获取
主要使用axios,获取数据,使用await,异步获取,然后写入数据。
数据都使用ref来定义,可以响应式获取 - 跨组件???
getMovie.js - 获取数据信息内容 - Details.vue使用
1 | import { ref } from "vue"; |
getMovies.js - 获取大致内容 - Home.vue使用
根据ref很好用,数据直接传过去了。
1 | import { ref } from "vue"; |
组件内容
App.vue结构
1 | div.container.mx-auto.px-6.py-7.lg:px-40.lg:py-14 // tailwind样式 mx -> 上下外边距, px -> 上下内边距 |
views
Details.vue结构
响应式考虑的是占多少位置 - 小就占全部,大就分一分。
主要功能:
- 获取当前电影的详细信息 - getMovie - 调用api - 写入页面 - 这里是用ref传过来的!!!
- 调用getMovies,在详细信息下方,继续显示信息,可以当作推荐电影,这里通过filter,去除了详细显示的电影
- 添加喜欢电影功能,如果当前已有-过滤掉 - 去除,如果没有,通过axios,再获取一次,放入favMovies,通过JSON.stringify(favMovies.value),存入localStorage
- 添加 / 移出按钮的判断 - filter - 看看长度大小 - 是否存在。。
- 如果喜欢电影 - 按钮样式变化 - 变成红色 - filter - 同上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87div.lg:flex.lg:gap-5.lg:justify-between.lg:items-center
IsLoading
div.w-full.h-64.rounded-md.overflow-hidden.md:h-80.lg:w-6/12.lg:h-96 - 大尺寸 - 左右结构,小尺寸,上下结构
div - 左侧图片
img
div - 右侧信息
p
h3
div
svg
div
svg
div
svg
div
svg
p
button
svg - 图标
span - 查找有没有指定id,没有返回 加入 信息, 有,返回 移除信息
hr
div
h3
Movies - 组件
----------
onst favMovies = ref(localStorage.getItem('favMovies') ? JSON.parse(localStorage.getItem('favMovies')) : []);
// 添加喜欢 按钮 - 是 就移出 - 不是 - 就添加。
// 为什么要用axios再获取一次呢,这个数据是写给movies的,直接展示就可以了。
const toggleFav = (id, e) => {
const cek = favMovies.value.filter(movie => movie.imdbID == id);
if (cek.length > 0) {
// remove favorite
favMovies.value = favMovies.value.filter(movie => movie.imdbID != id);
localStorage.setItem('favMovies', JSON.stringify(favMovies.value));
} else {
if (e.target.tagName.toLowerCase() == 'span') {
e.target.textContent = 'Wait . . .';
}
// add favorite
axios
.get(`${import.meta.env.VITE_API_URL}?apikey=${import.meta.env.VITE_API_KEY}&i=${id}`)
.then(res => {
const { data } = res;
const movie = {
imdbID: data.imdbID,
Title: data.Title,
Poster: data.Poster,
Year: data.Year
};
favMovies.value.push(movie);
localStorage.setItem('favMovies', JSON.stringify(favMovies.value));
})
.catch(err => console.log(err));
}
};
// 文字变化
const handleTextFav = imdbID => {
if (favMovies.value.length > 0) {
const cek = favMovies.value.filter(movie => movie.imdbID == imdbID);
if (cek.length > 0) {
return 'Remove from Favorite';
} else {
return 'Add to Favorite';
}
} else {
return 'Add to Favorite';
}
};
// 样式变化
const getClass = imdbID => {
if (favMovies.value.length > 0) {
const cek = favMovies.value.filter(movie => movie.imdbID == imdbID);
return {
'text-red-600': cek.length > 0,
'text-gray-300': cek.length == 0
};
} else {
return 'text-gray-300';
}
};
Home.vue结构
这里的功能:
- 获取输入框内容,调用api 获取信息
- 滚动加载 - 如果到底了,继续获取数据,调用api,传递page参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45div
Search - 功能都在Search里面,这里负责传递输入数据,获取信息
Movies - 显示信息
IsLoading
----------------------------
相关Js
const keyword = ref(
localStorage.getItem('keyword')
? localStorage.getItem('keyword')
: 'One Piece'
)
const handleKeyword = (kata) => { // 获取输入框内容
keyword.value = kata
loadMovies(keyword.value)
setTimeout(() => {
// 页面总数
totalPage = Math.ceil(totalResults.value / 10)
}, 1000)
page.value = 1
}
loadMovies(keyword.value)
const page = ref(1) // 响应式
let totalPage = 0
setTimeout(() => {
totalPage = Math.ceil(totalResults.value / 10)
}, 1000)
window.onscroll = () => {
// 滚到底部
let bottomOfWindow =
Math.floor(document.documentElement.scrollTop + window.innerHeight) ===
document.documentElement.offsetHeight
if (bottomOfWindow) {
page.value = page.value + 1
if (page.value <= totalPage) { // 说明有下一页。
nextPages(page.value, keyword.value)
}
}
}
Favorite.vue结构
这里功能:
- 从localStorage里获取 favMovies的数据,传递给Movies组件,用于显示
- 判断喜欢数量是否为空, 有,显示内容,没有,给出提示信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19div
h3
Movies
IsLoading
-------------
const favMovies = ref( // 直接传递给 Movies
localStorage.getItem('favMovies')
? JSON.parse(localStorage.getItem('favMovies'))
: []
)
const load = ref(false)
const msg = ref('')
onMounted(() => { // 加载时,判断,是否有内容。
if (favMovies.value.length < 1) {
load.value = true
msg.value = 'Empty'
}
})
NotFound.vue结构
这里添加了一个返回功能。。返回到前一页
1 | div.my-8.flex.justify-center |
components
Header.vue结构
主要用来跳转页面。。
1 | div |
IsLoading.vue结构
用于显示,内容是否存在 - 喜欢页面 / 搜索不到
1 | div.mt-5.flex.justify-center.items-center |
Movie.vue结构 - 每个电影的卡片 - 给Movies用。
单个电影展示页面,
这里功能:
- 如果页面海报不存在,显示备用的 - 无图片样式。
- 喜欢按钮 - 内容同上 - 样式 + 添加到喜欢页面
1
2
3
4
5
6
7
8
9
10
11div
router-link - 跳转到Details
div
img
div
img
div
div
router-link- 跳转到Details
h3
button - 判断是否 喜欢 - 样式切换
Movies.vue结构
核心部分:
使用v-for,渲染多个Movie组件。绑定favMovie事件,子组件返回喜欢的电影信息,在这里处理
绑定 key :key="movie.imdbID"
数据来源:
外部传递 - Home.vue / Favorite.vue / Details.vue
然后给子组件。
主要功能:
处理喜欢电影内容
- 使用ref,子组件传递过来,判断 - 如果已有,说明不喜欢,过滤掉,如果没有,写入favMovies,存入localStorage - JSON.stringify(favMovies.value)
- 需要获取信息 - 解析JSON - JSON.parse(localStorage.getItem(‘favMovies’))
1 | <div сlass="mt-7 w-full flex flex-wrap justify-around gap-5 md:gap-8 lg:gap-5"> |
Search.vue结构
使用form表单,触发请求
localStorage存储 - Keyword / isRecent
把输入的字符串,处理过后,通过事件发送给Home.vue,用于获取信息
这个字符串,也可以在Details.vue中用到,在下方展示推荐电影
主要功能:
- 根据输入框内容获取信息 // 传递给父组件
- 删除最近搜索记录 // filter过滤。。
- 删除最近搜索记录框。 // true / false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56form
div
input - 输入框
svg
div - 显示已搜过的记录
div - Recent - 显示信息。
h3 -
div
span - Clear Recent - 去除最近搜索的盒子
svg
div - 搜索记录详细信息。。
div
span - 文字
svg - 图标
------
import { ref } from '@vue/reactivity';
const recents = ref(localStorage.getItem('recents') ? JSON.parse(localStorage.getItem('recents')) : []);
const keyword = ref(localStorage.getItem('keyword') ? localStorage.getItem('keyword') : 'One Piece');
const isRecent = ref(localStorage.getItem('isRecent') == 'true' ? true : false);
const emit = defineEmits(['keyword']); // setup必须这样获取
const handleSearch = () => { // 输入keyword
localStorage.setItem('keyword', keyword.value);
localStorage.setItem('isRecent', true);
isRecent.value = true;
let str = keyword.value.toLowerCase();
str = str.trim();
if (keyword.value) {
const cek = recents.value.filter(item => item == str);
if (cek.length < 1) {
recents.value.push(str);
localStorage.setItem('recents', JSON.stringify(recents.value));
}
emit('keyword', str); // 返回给父组件
}
};
const clearRecent = () => { // 显示与隐藏。。。
recents.value = [];
isRecent.value = false;
localStorage.setItem('recents', JSON.stringify([]));
localStorage.setItem('isRecent', false);
};
const filterRecent = e => { // 删除 - 过滤
recents.value = recents.value.filter(recent => recent != e.target.dataset.recent);
if (recents.value.length == 0) {
isRecent.value = false;
}
};
const changeKeyword = e => { // 再次返回给父组件
keyword.value = e.target.innerHTML;
emit('keyword', keyword.value);
localStorage.setItem('keyword', e.target.innerHTML);
};
总结
ref - 一个页面修改 - 都修改了。
完成功能:
组件分离
- 获取数据 + 功能 - Home / Detail / Favorite
- 发送数据 - Search / Movie - 喜欢事件 / Movies
- 处理UI - Header / Movie / Movies
获取数据方式
- 调用Api
- localStorage
搜索信息 - 调用api
搜索记录 - localStorage存储,删除 - filter过滤
滚动加载 - 高度 + 调用api&page
页面动画 - router.meta + transition
添加收藏 - 样式 + 写入localStorage
响应式页面 - 依据md / lg显示,规范尺寸的作用是统一,如果尺寸小,多占点,大,就少占点。