vue3.0尝鲜,写一个win10日历
2020/4/30 11:02:58
本文主要是介绍vue3.0尝鲜,写一个win10日历,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
引子
这几日前端最热门的消息无外乎vue3.0的发布,虽然不是正式版,也让人想一探究竟。
正好下午无事,便略过了过文档。不过学东西最好的方式还是学以致用。
四处打量打量,正好看到win10的日历,哎呦,不错,就是你了。
效果如下图:
一个简单的在线原型搭建vue3.0项目
工欲善其事,必先起脚手架,好在有@vue/cli的支持,搭项目变得非常简单。
vue create win10-calendar
cd win10-calendar && vue add vue-next
需求分析
需求很清晰,抄。
抄之前得先研究抄的对象。
仔细操作后,可以发现日历的头部共有三个可操作区域。
年/月部分点击可以切换视图,分别是日历表,月份表,年份表,层层递进。
上下两个箭头在不同视图下作用不同,日历表中控制月份,月份表中控制年份,年份表中一次控制N年。
鼠标悬浮在表格上时,有一个探照灯的效果,挺有意思。
表格切换,也有缩进,扩张的动画效果。
分析到这里,代码组织基本确定了。
组件入口,组件头部,日历表,月份表,年份表5个组件。
探照灯效果为便于抽象,也单独提取为一个组件。总计6个组件。
三个表格看似展示不同的东西,实际上存在关联,其实都是对于同一个日期不同纬度的展示。
而头部则负责对这个日期进行修改编辑,以及切换不同的视图。
而组件入口,自然负责将这些子组件统合,而且子组件的状态也需要提升到它这个层级。
再来看探照灯,css并没有一个探照灯属性可以像box-shadow
一样方便,加上就高亮一个区域。
不过css魔法就是脑筋急转弯,得绕着弯想。
假设我们做一个全黑的蒙层,中间挖个洞,鼠标移动的时候,使蒙层的中心跟随鼠标,不就是一个探照灯了吗。
至于不悬浮时,文字也要可见,悬浮时,文字与格子边框同时可见,也很简单,把文字定位,z-index提高,让它们【浮出水面】就可以了。
编写组件
vue3据说支持vue2.x的大部分特性。vue单文件也依然是支持的。
新建Calendar/index.vue
组件,顺便添加子组件。文件结构如下
Calendar index.vue children CalendarHead.vue DatPanel.vue MonhPanel.vue YeaPanel.vue Mask.js 复制代码
script部分,大部分的属性都可以去掉了。添加一个setup函数。
在vue3中,setup这个函数会在beforeCreate和created之间调用。它可以返回一个渲染函数,也可以返回一个对象。对象中包含的字段,就可以在模板中使用。
index初期
先在index.vue添加一些全局状态,比如日期对象。
这些状态需要向下传递到子组件,子组件也需要能够更改这些状态。vue2.x一般用props/event的方式,说实话是有些繁琐的。如果用vuex又感觉太过笨重。
vue3则提供了一套provide/inject的机制,虽然vue2.x也有,但与props一样是需要声明的。
通过provide/inject API,可以直接向任意层级的子组件直接通过一个约定的key进行任意值的传递。
// index.vue import {ref, provide} from 'vue'; import dayjs from "dayjs"; export default { setup() { const date = ref(dayjs().toDate()); const setDate = (value) => { date.value = dayjs(value).toDate(); }; const displayMode = ref("date"); const setDisplayMode = (mode) => { displayMode.value = mode; }; provide("displayMode", [displayMode, setDisplayMode]); provide("date", [date, setDate]); return {} } } 复制代码
子组件可以直接通过inject API获取到index中声明的值与设值函数。
日历表组件
先来编写日历表
// DatePanel.vue import { computed, h, inject } from "vue"; import dayjs from "dayjs"; export default { setup() { const [date, setDate] = inject("date"); const dateList = computed(() => getDateList(date.value)); return { dateList, weeks: ["日", "一", "二", "三", "四", "五", "六"], }; }, } // 生成日历表 function getDateList(date) { // 本月第一天 const day0 = dayjs(date).startOf("month"); // 本月第一个星期的星期日 const firstDay = day0.subtract(day0.get("day"), "day"); const rows = 6; //Math.ceil((day0.get("day") + day0.daysInMonth()) / 7); return Array(rows * 7) .fill(0) .map((n, i) => firstDay.add(i, "day")); } 复制代码
win10的日历表从周一开始,也就是js中星期的1开始,为了便于计算,我们从周日开始,也就是js中星期的0开始。
计算日历表,我们只需要计算出本月第一个星期的星期日的日期,然后从这天开始,依次递增,就可以得到整个日历表。
为了保证日历表的高度稳定,固定放6个星期,也就是42天。
模板和css部分就不放了。有兴趣直接访问源码
探照灯蒙层
有了基本布局,可以来着手蒙层的编写。
这个组件我们来试试函数组件。函数式组件推测是直接拿来当组件的setup部分来用的,没看源码,说错勿怪。
函数可以直接返回jsx,不知道是不是现阶段的jsx解析有问题,非原生属性都无法通过jsx传递,只能通过attrs属性统一获得。
好在这个组件的模板部分非常简单,动态的部分只有style而已。
通过上面的分析,探照灯实际是一个挖孔蒙层,这个效果可以通过径向渐变来实现。
radial-gradient(transparent, rgba(0, 0, 0, 1) 60px, #000) 复制代码
要让它动起来,我们需要实时获取鼠标在元素上的坐标。
这样的一个功能,官方有个非常合适的例子,拿来改一改就能用。
我们把这个功能也抽象成一个hook。
// src/hooks/useMousePosition.js import { onMounted, onUnmounted, toRefs, reactive } from "vue"; // 传入一个dom引用,鼠标移入该元素时,派发鼠标在该元素上的位置 export default function (elRef) { const state = reactive({ x: 0, y: 0, width: 0, height: 0, enter: false, }); let rect = { top: 0, left: 0, width: 0, height: 0, }; function onEnter() { if (!elRef || !elRef.value) return; state.enter = true; rect = elRef.value.getBoundingClientRect(); state.height = rect.height; state.width = rect.width; } function onMove(e) { const { clientX, clientY } = e; state.x = clientX - rect.left; state.y = clientY - rect.top; } function onLeave() { state.enter = false; } onMounted(() => { if (!elRef || !elRef.value) return; elRef.value.addEventListener("mouseenter", onEnter); elRef.value.addEventListener("mousemove", onMove); elRef.value.addEventListener("mouseleave", onLeave); }); onUnmounted(() => { if (!elRef || !elRef.value) return; elRef.value.removeEventListener("mousemove", onMove); elRef.value.removeEventListener("mouseenter", onEnter); elRef.value.removeEventListener("mouseleave", onLeave); }); return { ...toRefs(state), }; } 复制代码
修改index.vue。
import useMousePosition from "@/hooks/useMousePosition"; export default { setup(){ const el = ref(null); const position = useMousePosition(el); return { el } } } 复制代码
模板部分
<div class="calendar" ref="el"> <!--other--> <Mask :position="position"/> </div> 复制代码
注意模板中引用ref不能加
:
,这里非常容易认为模板中需要将setup中的el的引用绑定给元素,实际上只需要给元素的ref绑定字符串el。
<div ref="el"> ✅
<div :ref="el"> ❌
有了父组件提供坐标尺寸信息,Mask组件就可以动起来了。
由于Mask会在父组件中移动,移动到边缘时,就可能露馅,因此可以把它的尺寸放大为父组件的2倍。
同时,为了保持其中心与鼠标重叠,需要向左上偏移自身尺寸的1/2。
// Mask.js import { computed, h } from "vue"; // 使用jsx必须引入h export default function Mask(props) { const position = props.position; const style = computed(() => { const size = Math.max(position.width, position.height) * 2; const isEnter = position.enter; return { transform: `translate(${position.x - size / 2}px, ${ position.y - size / 2 }px)`, backgroundImage: isEnter ? `radial-gradient(transparent, rgba(0, 0, 0, 1) 60px, #000)` : "", backgroundColor: isEnter ? "" : "#000", width: size ? size + "px" : "100%", height: size ? size + "px" : "100%", }; }); return <div class="mask" style={style.value} />; } 复制代码
ps 通过模板解析的响应式值,会自动将它的value传递给子组件,所以通过props取得的值,就不需要加.value取值了。
操作区组件
现在基本的效果已经有了,鼠标移来移去还有酷炫的效果。让我们继续来完善它。
转移战线到CalendarHead组件。
这里的主要的操作有两个,一个是切换视图,一个上下调整日期。
index已经通过provide向子组件开放了这两个值的读写。
切换视图,只需要检查当前视图,并做递进即可。
调整日期,同样需要检查视图。
当前为日历表时,调整幅度为正负一个月
当前为月份表时,调整幅度为正负一年
当前为年份表时,调整幅度为正负十六月
import {inject, ref, computed} from "vue"; import dayjs from 'dayjs' export default { setup() { const [displayMode, setDisplayMode] = inject("displayMode", [ref("date"), (v) => v]); const [date, setDate] = inject("date", [ref(new Date()), (v) => v]); const setPanelMode = () => { let mode = "date"; if (displayMode.value === "date") { mode = "month"; } if (displayMode.value === "month") { mode = "year"; } if (displayMode.value === "year") { mode = "year"; } setDisplayMode(mode); }; const dateString = computed(() => (displayMode.value === "date" ? fmtDate : fmtYear)(date.value)); const handleDate = (isAdd) => () => { const setMap = { date: { value: 1, unit: "month", }, month: { value: 1, unit: "year", }, year: { value: 16, unit: "year", }, }; const setter = setMap[displayMode.value]; const value = isAdd ? dayjs(date.value).add(setter.value, setter.unit) : dayjs(date.value).subtract(setter.value, setter.unit); setDate(value.toDate()); }; const upward = handleDate(false); const downward = handleDate(true); return { upward, downward, setPanelMode, dateString, } } } 复制代码
月份/年份组件
现在的操作可以在日历表上切换月份了,接下来加上月份/年份表。
先修改index.vue,响应displayMode的变化,并为动画作准备。
这里使用组合API中的watch来主动监视响应式值的变化。当displayMode变化时,切换componentName与transitionName。
动画分两种,向上级切换是缩进,向下级切换是扩张,监视新旧视图模式可得。
// index.vue export default { setup() { // other code const transitionName = ref("out"); const componentName = ref("date-panel"); const levels = ["date", "month", "year"]; watch(displayMode, (now, old) => { const nowLevel = levels.indexOf(now); const oldLevel = levels.indexOf(old); transitionName.value = nowLevel < oldLevel ? "out" : "in"; // componentName必须在transitionName设置之后或同时设置才不会使transitionName滞后 componentName.value = { date: "date-panel", month: "month-panel", year: "year-panel", }[displayMode.value]; }); return { componentName, transitionName, } } } 复制代码
模板部分
<div class="calendar" ref="el"> <CalendarHead/> <div class="cell-wrap"> <transition :name="transitionName"> <component :is="componentName"/> </transition> </div> <Mask :position="position"/> </div> 复制代码
回到正题。
其实月份表和年份表也有一个操作。
点击月份表,切换日期为选中的年月,切换视图为日历表。
点击年份表,切换日期为选中的年份,切换视图为月份表。
通过index.vue下放的displayMode和date,可以很容易完成。
// MonthPanel import { computed, inject, ref } from "vue"; import dayjs from "dayjs"; export default { setup() { const [date, setDate] = inject("date", [ref(new Date()), (v) => v]); const [displayMode, setDisplayMode] = inject("displayMode", [ef("date"),(v) => v]); const monthList = computed(() => { const month0 = dayjs(date.value).month(0); return Array(16) .fill(0) .map((n, i) => month0.add(i, "month")); }); const getClass = (item) => { return [ "month-cell", item.month() === dayjs(date.value).month() && " current-month ", item.year() === dayjs(date.value).year() && " current-year ", ].join(" "); }; const pickMonth = (item) => { setDate(item.toDate()); setDisplayMode("date"); }; return { monthList, getClass, pickMonth, }; }, }; 复制代码
年份表大同小异,就不放了,有兴趣直接查看年份表源码与月份表源码
总结
这样一个小玩具,基本上把vue3常用的API过了一遍。编写过程中,也遇到不少困难,有些是不熟悉新API,有些是见知障,有些则是vue3本身的不成熟。当然,学新东西遇到困难是好事。克服了还是有所收获的。
vue3总地来说,潜力不小,特别是hooks这种理念非常有潜力,react已经有swr这种库,可以想象以后一些繁琐复杂的逻辑都可以像这样封装,好处自然不用说,坏处可能就是开发者更加接触不到比较底层的东西了。
有人认为react/vue之类的库,已经让很多新入门的开发者已经不知道怎么操作dom了,以后这种情况可能会加剧,开发者不知道怎么写一个上拉加载,只知道npm install...
说回vue。组合API的设计可以在开发一些小组件时不再需要拼字符串或者使用preact之类的轻量渲染库了。而且可以更轻易的移植到其他端,估计一堆小程序框架又要忙着升级了(再次坚定了小程序用原生的念头...)。
再说说体验不好的地方。
- ref.value这个设计有点讨厌,很容易混淆,有时候需要
.value
,有时候不需要。而且基础值除了包装就没有更好的拦截方式了吗?可能包装并不是唯一解,比如直接包装当前模块对象或上下文环境?口胡勿信(笑 - ts支持还不够好,一开始是直接上ts的,然而各种不顺利,加上不熟悉的地方很多,为了更好的排除问题,就回退js了。还是不够开箱即用啊。
- 模板中的ref可能是为了兼容以前的旧用法,依然是字符串,但如果能支持下绑定ref值就更好了,不然很容易让人困惑。
项目地址
完整项目可以查看github。
或者移步我的博客。
这篇关于vue3.0尝鲜,写一个win10日历的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-10-04package.json 文件位置在哪?-icode9专业技术文章分享
- 2024-10-01Craco.js学习:从入门到实践指南
- 2024-10-01Create-React-App学习:入门与实践指南
- 2024-10-01CSS-in-JS学习:从入门到实践指南
- 2024-09-30JSX语法学习:从入门到初步掌握
- 2024-09-30Mock.js学习:入门教程与实战演练
- 2024-09-30React Hooks学习:从入门到实践
- 2024-09-30受控组件学习:React中的基础入门教程
- 2024-09-29JS定时器教程:初学者必看指南
- 2024-09-29JS对象教程:初学者的全面指南