50天用vue3完成了50个web项目,我学到了什么?

45,043 阅读9分钟

我报名参加金石计划一期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

通过本文的50个web示例你将学到:

  • Vue3核心基础语法和进阶语法
  • less核心基础语法和进阶语法
  • scss核心基础语法和进阶语法

1.Expanding Cards

效果如图所示:

1.png

学到了什么?

Javascript

  • Vue ref方法定义基本响应式变量。如:
const currentIndex = ref(0);

ref方法当然也可以定义一个对象,但是通常定义对象应该使用reactive方法。

  • Vue v-for指令渲染列表。如:
v-for="(item,index) in imageItems" 

v-for指令渲染时最好指定key属性,方便虚拟DOM Diff算法比对。

  • Vue 动态绑定class与style。如:
:class="index === currentIndex ? 'active' : ''"
:style="{ backgroundImage:`url(${ imageURL + item })`}"

:class:style代表设置动态类名和动态行内样式,其中:v-bind的简写,值可以是一个javascript表达式,如三元表达式,或者是对象,又或者是数组。

  • Vue 事件绑定。如:
@click="currentIndex = index"

vue事件可以简写为@ + 事件名,值为一个javascript表达式或者是一个函数。

less

  • 定义公共样式
.base-flex {
    display: flex;
    justify-content: center;
    align-items: center;
}
//使用方式
.app {
    .base-flex;
}
  • 嵌套语法
.app {
    //...这里写核心样式
    .ec-panel {
        //...这里写核心样式
    }
}

less定义变量是@ + 变量名,如:

@width: 100px;
  • &

该符号表示对父选择器的一个引用,例如:

.ec-panel {
    //...核心样式
    &.active {
        //...核心样式
    }
}

其中&.active其实就是.ec-panel.active的写法。

2. Progress Steps

效果如图所示:

2.png

学到了什么?

Javascript

通过defineProps定义props,如:

const props = defineProps({
    width:{
        type:Number,
        default:350
    },
    progressWidth:{
        type:Number,
        default:0
    }
})

通过computed方法定义计算属性:

const styleWidth = computed(() => props.width+'px');

依然是通过slot标签定义插槽:

<div class="ps-step-container">
    <div class="ps-step-progress"></div>
    <slot></slot>
</div>

通过defineEmits方法定义事件传递:

  • defineEmits注册事件名,方法通常是一个数组,传递多个事件名
  • 调用方法的返回值,然后将该事件向上传递
const emit = defineEmits(["on-click"]);
const changeActive = () => {
    emit("on-click");
}

通过reactive定义响应式对象,它与ref方法的区别就是,通常我们使用ref方法来定义基本数据类型,而reactive方法则是定义多个对象:

const bool = ref(false);
const state = reactive({
    status: false,
    list:[
        {
            label:"测试值",
            value:1
        }
    ]
})

浏览器控制台花式玩法:

console.log("%c " + consoleText,"background:#0ca6dc;padding:4px 10px;border-radius:3px;color:#fff");

less

动态绑定样式变量的写法有两种,第一种则是字符串属性名,第二种则是直接写变量名:

width:v-bind("styleWidth");
width:v-bind(styleProgressWidth);

::focus-visible选择器,表示键盘伪类焦点选择器,在规范中定义为:元素聚焦,同时浏览器认为聚焦轮廓应该显示。

有时候我们希望键盘触发聚焦轮廓,而鼠标关注焦点则不触发轮廓,此时用以下一行CSS代码搞定。

:focus:not(:focus-visible) {
    outline: 0;
}

属性选择器语法:

[属性选择器] {
    //CSS样式
}
//如:
[disabled] {
    background-color: @color;
    color: @font_color;
    cursor: not-allowed;
}

其它知识点同前面示例。

3. Rotating Navigation Animation

效果如图所示:

3.png

学到了什么?

Javascript

v-if与v-else指令,表示条件,v-html表示渲染html:

<div class="rna-page-content">
    <slot>
        <template v-if="props.isRenderHTML">
            <p v-html="content"></p>
        </template>
        <template v-else>{{ props.content }}</template>
    </slot>
</div>

typescript

typescript 定义接口通过interface关键字,就像定义对象一样,其后跟属性名:属性类型,如:

定义基本类型就是:类型值,如:

const URL: string = "";
//代表URL是一个字符串类型
interface NavItemType {
    url:string;
    text:string;
    icon:string;
}

typescript泛型,如:

Array<NavItemType>

就代表是一个泛型,也就是数组类型,并且数组项的值应该是类型NavItemType,所以数据格式就应该类似如下:

export const navList = [
  {
    url: "http://www.eveningwater.com/",
    text: "个人网页",
    icon:"Website"
  },
  {
    url: "https://www.eveningwater.com/my-web-projects/",
    text: "我的项目",
    icon:"project"
  },
  {
    url: "http://github.com/eveningwater",
    text: "github",
    icon:"github"
  },
];

再如:

Array<string> //表示定义一个字符串数组

导出多个模块语法:

export { default as Content } from "./Content.vue";
export { default as Menu } from "./LeftMenu.vue";
export { default as NavList } from "./NavList.vue";

表示将三个组件的默认模块更名为对应的名字。

less

:deep深度选择器,类似于vue2的>>>和/deep/,通常用于影响子组件的样式,也就是因为加了scoped组件只作用在当前组件,而无法作用到子组件,也就需要使用该选择器:

:deep(p) {
    text-indent: 2em;
    color:@content_font_color;
    line-height: 2;
    margin-bottom: 15px;
    letter-spacing: 1px;
}

动态样式访问数组变量,可以根据索引来访问,就像访问数组元素一样:

background-image: v-bind("beforeResourceURL[0]");

css同级元素选择器,如:

& + .rna-nav-list ul {
    transform: translateX(15px);
    & .rna-nav-item {
       transform: translateX(0);
       transition-delay: .4s;
    }
}

表示选中当前选择器的兄弟选择器.rna-nav-list。

其它知识点同前面示例。

4. hidden-search-widget

效果如图所示:

4.png

学到了什么?

Javascript

给元素或者是组件绑定ref,可以访问组件或者元素的实际DOM元素,例如:

const inputRef = ref(null);

watch方法,接受三个参数,第一个参数表示监听的数据,可以是一个函数,也可以是一个变量,或者也可以是一个监听的字符串属性名,第二个参数则是一个回调函数,可以获取到监听的值,第三个参数表示监听的配置对象,如深度监听deep属性。如:

watch(isActive,val => {
    const inputElement = inputRef.value;
    if(val && inputElement){
        (inputElement as HTMLInputElement).focus();
    }
});

typescript

as 关键字可以将任意变量断言成任意类型,通常如果转换类型不对可以先强制转换成unknown,然后再转换成对应的类型。例如:

//假设inputElement不能被断言成HTMLInputElement类型,就需要先转换成unknown
inputElement as unknown as HTMLInputElement

或者也可以直接断言成any。如:

inputElement as any 

虽然这样做可以逃避ts的类型检查,但如无必要,尽量少使用as关键字断言变量的类型。

模板绑定:

<input type="text" class="hsw-input" ref="inputRef" :placeholder="props.placeHolder" />

其它知识点同前面示例。

5. blurry loading

效果如图所示:

5.png

学到了什么?

Javascript

一个工具函数,表示转换数字范围的工具函数,感觉很常用,可以记下来:

// https://stackoverflow.com/questions/10756313/javascript-jquery-map-a-range-of-numbers-to-another-range-of-numbers
export const scale = (n:number,inMin:number,inMax:number,outerMin:number,outerMax:number) => (n - inMin) * (outerMax - outerMin) / (inMax - inMin) + outerMin;

typescript

Ref类型也是一个泛型,当然这里其实应该如此来定义setTimeout的返回值,而不应该使用any:

const timer:Ref<any> = ref(null);

应该修改成:

const timer:Ref<ReturnType<typeof setTimeout>> = ref(null);

typeof setTimeout表示获取setTimeout函数的类型,而ReturnType是typescript的内置类型,表示返回值的类型,结合起来就代表是setTimeout函数的返回值。

onMounted和onUnmounted是vue生命周期的钩子函数,分别代表组件挂载后和组件卸载后,接受一个回调函数作为参数,然后在回调函数当中执行相应的操作。

onMounted(() => {
    //这里写逻辑
});
onUnmounted(() => {
   //这里写逻辑
})

less

获取对象值的变量作为样式,可以使用字符串和点语法获取,如:

opacity: v-bind("props.number");

其它知识点同前面示例。

6. scroll animation

效果如图所示:

6.png

学到了什么?

javascript

函数节流,就是利用延迟定时器,结合时间戳来调用给定的函数。在这个函数里面也涉及到了typescript泛型,任意类型,函数类型,并且也涉及到了apply方法的使用,apply传入一个this对象,后续的参数则是一个数组。

export function throttle(fn:(...args:any []) => any,delay:number){
    let timer:ReturnType<typeof setTimeout> | null = null,last:number = 0;
    return function<T>(this:(...args:any []) => any,...args:Array<T>) {
        let now = +new Date();
        if(last && now < last + delay){
            clearTimeout(timer);
            timer = setTimeout(() => {
                last = now;
                fn.apply(this,args);
            },delay);
        }else{
            last = now;
            fn.apply(this,args);
        }
    }
}

typescript

PropType类型,是一个泛型,用于定义prop的类型。如:

props:{
    title:{
        type:String as PropType<string>
    },
    content:{
        type:String as PropType<string>
    }
},

setup函数为vue3的入口,也是一个生命周期钩子函数,相当于vue2的beforeCreate和created的合并。第一个参数可以获取到props,第二个参数是当前上下文,可以通过对象结构的方式获取emit,slots等。如:

setup(props,{ slots }){
    const renderTitle = () => slots.title ? slots.title() : props.title;
    const renderBoxContent = () => slots.default ? slots.default() : props.content;
    return () => (
        <div class="scroll-ani-box">
            <Title level="3" class="scroll-ani-box-title">{  renderTitle() }</Title>
            { renderBoxContent() }
        </div>
    )
}

defineComponent表示定义一个组件,也是Vue3提供的一个定义组件的方法,该方法接收一个对象作为参数,在这个对象里面有点像vue2的new Vue的参数,写vue组件的配置参数,例如props,methods等。

在这里封装了一个Title.tsx,也就是将h1 ~ h6封装成一个组件,也叫标题组件。代码如下:

import { defineComponent, PropType } from "@vue/runtime-core";

export const TitleNumberCollection = [1,2,3,4,5,6];

export default defineComponent({
    props:{
        level:{
            type:[Number,String] as PropType<number|string>,
            default:1,
            //在这里检查level的类型
            validator:(value:string | number) => {
                return TitleNumberCollection.indexOf(Number(value)) > -1;
            }
        },
        content:{
            type:String as PropType<string>
        }
    },
    setup(props,{ slots }){
        const { level,content,...rest } = props;
        const TitleName = "h" + level;
        const renderChildren = () => slots.default ? slots.default() : props.content;
        return () => (
            <TitleName {...rest}>
                { renderChildren() }
            </TitleName>
        )
    }
})

这个组件的实现思路就是定义一个level属性和一个content属性,前者代表使用的是h1 ~ h6之中的元素,这也是为什么TitleNumberCollection数组是[1,2,3,4,5,6]的原因。

这里为了严谨,对level做了验证,只允许传入1 ~ 6的值,也可以是字符串和数字。需要注意的就是validator是一个函数,返回的是一个布尔值,并且函数的参数必须要指定类型,否则会出现意料之外的错误,导致程序无法执行。

如果组件没有写默认的插槽内容,就会使用content属性字符串作为承载的内容。使用这个组件的方式很简单,如下:

<Title level="3">这是h3元素的内容</Title>
<Title :level="4">这是h4元素的内容</Title>

PS: 这个组件在后面的示例中几乎都有用到。

可以在onMounted钩子函数中监听页面滚动事件,如:

onMounted(() => {
    triggerBottom.value = window.innerHeight * 4 / 5;
    window.addEventListener("scroll",throttle(onScrollHandler,20))
});

UnwrapNestedRefs类型也是Vue3定义的类型,是一个泛型,可以定义reactive方法的返回值类型,如:

interface BoxType {
  active:string;
  content:string;
}
const state:UnwrapNestedRefs<{ boxList:Array<BoxType>}> = reactive({
    boxList:[]
});

v-slot指令,其后跟指定的插槽名,也就是具名插槽。

less

定义Mixin实际上就是写CSS样式。如下:

.flex-center {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
}

使用方式就是把它当成函数调用,如:

//使用方式
.app {
    .flex-center();
}

svg-gradient函数,是less的核心函数,生成一个svg渐变色,它至少有三个参数,第一个参数指定渐变类型和方向,其余参数列出颜色和位置,第一个和最后一个指定颜色的位置是可选的,其余颜色必须指定位置。该函数最终会被渲染成svg图片,如下图所示:

percentage函数,接受一个浮点值作为参数,如:

percentage(.5); //渲染成50%

less当中的map对象,格式如下:

.(map名字) {
    属性名: 属性值(可以是变量);
}

例如:

@boxBgColor-1:#f1bd81;
@boxBgColor-2:#e07e0e;
.boxColors() {
    lightColor:@boxBgColor-1;
    darkColor:@boxBgColor-2;
}

使用的时候,就像读取javascript对象那样使用中括号语法:

.test {
    color:.boxColors[lightColor];
}

min函数,顾名思义,求最小值,如:

min(8px,9px,10px) // => 返回8px

range函数,求范围值,如:

range(10px, 30px, 10); // => 10px 20px 30px
padding:range(10px, 30px, 10); // => padding: 10px 20px 30px;

fade函数,对颜色进行透明度的相加,例如:

fade(#fff,40%); // => rgba(255,255,255,.4);

类似的还有fadeout以及fadein函数。

extract函数,有2个参数,第一个参数是一个列表,第二个参数是一个索引值,也就是说该函数表示从列表当中取出列表项,如:

@textTransform:uppercase,capitalize,none,lowercase,inherit;
text-transform: extract(@textTransform,1);

escape表示可以将任意的字符串转成属性或者值,格式就是~ + 字符串值,例如:

@min768: ~"(min-width: 768px)";
.element {
  @media @min768 {
    font-size: 1.2rem;
  }
}

将会渲染成:

@media (min-width: 768px) {
  .element {
    font-size: 1.2rem;
  }
}

这个示例用到了很多less核心语法,对less的用法也就更深一步。

其它知识点同前面示例。

7. split panel

效果如图所示:

7.png

学到了什么?

javascript

知识点同前面示例。

less

unit函数,也就是添加单位的函数,如:

unit(100,vh) // =>100vh

covert函数,转换单位,如:

covert(1s,'ms'); // => 1000ms

其它知识点同前面示例。

8. form wave

效果如图所示:

8.png

学到了什么?

javascript

defineAsyncComponent函数,传入一个回调函数作为参数,回调函数的返回值必须是import('组件路径'),表示异步导入组件。如:

const AsyncFormComponent = defineAsyncComponent(() => {
  return import("./components/Form.vue");
});

less

pi函数,表示获取数学上的π值。即3.14.....

  • ceil函数(四舍五入),
  • round函数(向上取整),
  • sqrt函数(求平方根),
  • floor函数(向下取整),
  • length函数(获取列表的长度),
  • mod函数(求余)。

其它知识点同前面示例。

9. sound board

效果如图所示:

9.png

学到了什么?

javascript

ref函数不仅可以用来定义数据,也可以用来存储一个dom元素的引用。如:

const audioRef = ref<HTMLAudioHTMLElement | null>(null);

vue template模板代码如下:

<audio ref={audioRef}></audio>

less

css选择器前缀名,可以通过定义变量,然后.@{变量名}的方式来添加css选择器前缀。如:

@prefix:sb-;
.@{prefix}button-container {
    //样式
}
// => 渲染成
// .sb-button-container {
//     //样式
// }

sin函数,求正弦的函数。

其它知识点同前面示例。

10. dad jokes

效果如图所示:

10.png

学到了什么?

Javascript

Fragment元素,也就是一个占位标签,实际不会渲染该元素,类似于vue的template元素。

window.open函数打开一个窗口,在打开之后,通过设置opener = null,可以避免window.open带来的安全问题。

如:

const win = window.open("");
win && (win.opener = null);

less

color函数,用于将属性转换成颜色值字符串,如:

color(#eb6b44); // => '#eb6b44';

max函数,与min函数相反,取最大值,如:

max(600,700,800) // => 800

pow函数,幂函数,等同于javascript的Math.pow函数,传入2个参数,第一个参数为数,第二个参数为幂数。如:

pow(2,2); // => 4

:focus-visible选择器,这个是关注焦点选择器。

其它知识点同前面示例。

11. Event keyCodes

效果如图所示:

11.png

学到了什么?

javascript

useCssModule方法,顾名思义,就是样式模块化,我们可以给vue的单文件组件的style标签加上module属性,然后在setup函数当中就可以通过该方法来访问style。例如:

<style module lang="less">
.test {
    //核心样式
}
</style>
<script>
    //setup函数中
    const style = useCssModule();
</script>
<template>
    <div :class="style.test"></div>
</template>

该方法还可以接一个具名参数,也就是代表获取的style模块名,比如:

<style module="name" lang="less">
.test {
    //核心样式
}
</style>
<script>
    //setup函数中
    const style = useCssModule('name');
</script>
<template>
    <div :class="style.test"></div>
</template>

typescript

KeyboardEvent类型,也就是键盘事件对象的类型。

less

less定义mixin(混合)与sass还是有很大的区别的,比如定义一个普通的mixin就是写一个css选择器,然后再写样式,如:

.test {
    //样式代码
}

然后我们通过调用函数的方式来使用这个mixin,即:

.common {
    .test();
}

如果不希望mixin被输出编译在css当中,可以给mixin加入括号,即:

.test() {
    //核心样式
}

使用方式仍然是通过调用函数的方式,同上。

mixin同样有命名空间的概念,如:

#test(){
    .test {
        //这里写样式
    }
}

这里的#test实际上也就是命名空间,当然我们也可以使用.test来代替#test,也是可以代表是一个命名空间名称的。

命名空间也是受保护的,我们可以使用when关键字,结合判断条件来让这个命名空间受保护,如:

.mt-(@num) when (default()) {
    margin-top:unit(@num,px);
}

意思就是当满足默认条件的时候,我们的.mt类名,其后跟数字前缀就会渲染出margin-top多少px的样式。例如:

.mt-(10); // => 渲染成margin-top:10px;

其它知识点同前面示例。

12. faq-collapse

效果如图所示:

12.png

学到了什么?

javascript

readonly方法,可以让一个通过ref或者是reactive定义的响应式数据变成只读。如:

const state = readonly(reactive({
    faqList: [
        {
            title:"Why shouldn't we trust atoms?",
            text:"They make up everything."
        },
        {
            title:"What do you call someone with no body and no nose?",
            text:"Nobody knows."
        },
        {
            title:"What's the object-oriented way to become wealthy?",
            text:"Inheritance."
        },
        {
            title:"How many tickles does it take to tickle an octopus?",
            text:"Ten-tickles!"
        },
        {
            title:"What is: 1 + 1?",
            text:"Depends on who are you asking."
        }
    ]
}));

此时的faqList就是只读的,无法被修改。

less

luma函数,计算一个颜色对象的亮度。如:

luma(rgb(100, 200, 30)) // => 44%

其它知识点同前面示例。

13. random-choice-picker

效果如图所示:

13.png

学到了什么?

javascript

通过withDefaults可以将defineProps包裹住,方便定义类型,并且提供props的默认值,如:

interface PropsType {
    placeholder:string;
}
const props = withDefaults(defineProps<PropsType>(),{
    placeholder:""
});

计算属性的getter函数与setter函数。如:

type ListType = Array<{text?:string,isActive?:boolean}>;
const computedChoices = computed<ListType>({
   get(){
      return choices.value;
   },
   set(newValue){
      if(newValue !== choices.value){
          choices.value = newValue;
      }
   }
});

setTimeout延迟函数的使用。

然后就是这个函数稍微难以理解一些:

const changeChoices = (v:string) => v.split(choice.value.indexOf(",") > -1 ? "," : "").filter(v => v.trim()).map(v => ({ text:v,isActive:false }));

实际上也很好理解,将每个函数拆分一下,就能理解了。

其它知识点同前面示例。

14. Animated Navigation

效果如图所示:

14.png

学到了什么?

javascript

知识点同前面示例。

scss

scss定义变量是$ + 变量名,而less则是@ + 变量名,scss是sass的下一版本,sass的语法有些类似于stylus,去掉了括号,但是在scss当中却保留了括号。scss中定义mixin通过@mixin。如:

@mixin common-bg {
    background: linear-gradient(135deg,$bgColor-1 10%,$bgColor-2 90%);
}

使用mixin就是通过@include关键字加mixin的名字。如:

.test {
    @include common-bg;
}

同样的scss中在选择器中使用前缀应该是#{变量名}的方式。如:

$baseSelector:an-;
.#{$baseSelector}nav {
    //核心样式
}

scss中可以使用for循环,结构为:for 变量名 from 起始值 through 结束值。如:

@for $i from 0 through 100 {
    .mt-#{$i} {
        margin-top:$i + px;
    }
}

opacify函数,传入2个参数,第一个参数为颜色值,第二个参数为一个浮点数,表示相加的透明度,这个函数也就是为颜色值添加透明度。如:

opacify(rgba(#036, 0.7), 0.3) // => #036
opacify(rgba(#6b717f, 0.5), 0.2); // rgba(107, 113, 127, 0.7)

transparentize函数,传入两个参数,同opacify函数一致,作用同opacify函数相反。如:

transparentize(rgba(#6b717f, 0.5), 0.2)  // rgba(107, 113, 127, 0.3)

opacify与transparentize函数就类似于类似于less的fadein和fadeout函数

其它知识点同前面示例。

15. incrementing-counter

效果如图所示:

15.png

学到了什么?

javascript

toRefs函数,当我们在vue中使用解构获取props时,props会失去响应式,此时我们就需要使用toRefs将props包裹一下,让props不失去响应式,这在Counter.vue文件当中是注释了的。

scss

知识点前面示例讲过。

其它知识点同前面示例。

16. Drink Water

效果如图所示:

16.png

学到了什么?

javascript

javascript没什么好说的,倒是typescript有2个类型需要解释一下。

StyleValue类型,顾名思义,就是样式值的类型,是typescript内置类型。

keyof关键字,后面跟一个类型,表示一个类型属于后面类型的一个子类型。如:

interface DataType {
    name: string;
    age: number;
}
const a: keyof DataType = 1;

scss

继承的语法,首先写一个公共的样式,然后通过@extend关键字使用,并且可以写多个继承,通过,分隔。如:

.test-1 {
    //核心样式
}
.test-2 {
    //核心样式
}
.common {
    @extend .test-1,.test-2;
}

其它知识点同前面示例。

17. Movie App

效果如图所示:

17.png

学到了什么?

javascript

javascript也没有什么新知识点,主要是typescript新增了3个类型,FocusEvent,HTMLInputElement和Partial类型

FocusEvent就是关注焦点事件对象类型,HTMLInputElement为input元素的类型,Partial为typescript内置类型,将所有类型变成可选的,也是一个泛型。

scss

percentage函数,原理同less的percentage没什么好说的。

mixin同样也可以传参数,并且在@include的时候传入即可。

类似border这样的,还可以写成嵌套语法。如:

border: {
    width:1px;
    style:solid;
    color:ffefefe;
}
// => 渲染border-width: 1px;border-style:solid;border-color:#fefefe;

其它知识点同前面示例。

18. background slider

效果如图所示:

18.png

学到了什么?

javascript

nextTick函数,传入一个回调函数作为参数,该函数为等待下一次 DOM 更新刷新的工具方法,返回值是一个promise。如示例:

<script setup>
import { ref, nextTick } from 'vue'

const count = ref(0)

async function increment() {
  count.value++

  // DOM 还未更新
  console.log(document.getElementById('counter').textContent) // 0

  await nextTick()
  // DOM 此时已经更新
  console.log(document.getElementById('counter').textContent) // 1
}
</script>

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

scss

clamp函数,传入3个参数,返回第二个参数的值,第一个参数为最小值,第3个参数为最大值。语法:

clamp($min,$number,$max) // => $number

三个参数都必须有单位或者无单位,如果number小于number小于min的值,则返回min,如果min,如果number大于max的值,则返回max的值,则返回max。

map-get函数,获取map类型的值,来看一个示例:

$font-weights: ("regular": 400, "medium": 500, "bold": 700);
map-get($font-weights, "medium"); // 500
map-get($font-weights, "extra-bold"); // null

其它知识点同前面示例。

19. theme clock

效果如图所示:

19.png

学到了什么?

javascript

知识点同前面示例。

可选链操作符?的使用。

typescript

Readonly类型,表示数据类型只读。

CSSProperties类型,就是css属性名的类型。

scss

@if...@else...判断语句。@function定义函数,@return 返回函数值。如:

@function setMargin($i,$j,$unit){
    $result:0;
    @for $_ from $i through $j {
      @if $unit {
        $result:$_ + $unit;
      } @else {
        $result:$_;
      }
    }
    @return $result;
}

这个函数表示设置间距,传入开始值,结束值以及单位。

max函数,同less的max函数一样。如:

$maxWidth:250px,300px,350px;
max-width: max($maxWidth...); // => 350px

其它知识点同前面示例。

20. button-ripple-effect

效果如图所示:

20.png

学到了什么?

javascript

没什么知识点好说的,有个MouseEvent表示鼠标事件对象的类型,以及HTMLButtonElement表示button元素的类型。

scss

可以在scss当中引入命名空间,然后使用sass内置的函数。如:

@use "sass:math";
//可以使用math函数
//如math.atan2,math.pow

其它知识点同前面示例。

21. drawing-app

效果如图所示:

21.png

学到了什么?

javascript

动态组件,component,传入一个is关键字,is关键字作为组件名。如:

<component is="div" ref="container"></component> // => 渲染一个div元素

本示例中封装了一个基于我的js实现的颜色选择器颜色选择器组件,代码如下:

<script setup lang="ts">
    import { nextTick, onMounted, PropType } from '@vue/runtime-core';
    import { ref } from '@vue/reactivity';
    import ewColorPicker from 'ew-color-picker';
    import "ew-color-picker/dist/ew-color-picker.min.css";
    const props = defineProps({
        name:{
            type:String as PropType<string>,
            default:"div"
        },
        option:{
            type:Object as PropType<object>,
            default:() => ({})
        }
    });
    const container = ref(null);
    onMounted(() => {
        nextTick(() => {
            new ewColorPicker({ el:container.value,...props.option }) as any;
        })
    })
</script>
<template>
    <component :is="props.name" ref="container"></component>
</template>

自定义组件v-model指令的实现,首先是定义一个叫modelValue的props,接着在组件内部提交自定义事件emit('update:modelValue')即可,当然也可以修改名字,不过使用方式就要类似:v-model:[属性名]。例如:

const props = defineProps({
    modelValue:[String,Number] as PropType<string | number>
});
const emit = defineEmits(["update:modelValue"]);

//模板元素
// <input @input="onInputHandler" :value="props.modelValue" />

scss

list.nth可以获取列表中对应渲染的值,传入2个参数,第一个是列表,第二个则是列表索引值。如:

//导入list命名空间
@use "sass:list";
//定义列表
$display:block,flex,inline-block,inline,inline-flex,none;
.el-block {
    display: list.nth($display,1); //=> 渲染block
}

以上代码定义一个类名为el-block,渲染display:block的样式。

其它知识点同前面示例。

22. drag-n-drop

效果如图所示:

22.png

学到了什么?

javascript

拖拽事件的使用。

scss

@each...in循环,同@for循环类似,如:

$font-weight:normal,lighter,bold,bolder,100,200,300,400,500,600,700,800,900;
@each $value in $font-weight {
    .fw-#{$value} {
        font-weight: $value;
    }
}

以下这个函数也有点意思:

@use "sass:string";
/*
* 截取属性字符
*/
@function propSlice($prop, $start, $end) {
    @return string.unquote(string.slice(string.quote($prop), $start, $end));
}

其它知识点同前面示例。

23. content-placeholder

效果如图所示:

23.png

学到了什么?

这个示例展示了如何封装一个骨架屏卡片组件,其中涉及到的知识点前面总结过,所以这里不再总结。

24. kinetic-loader

效果如图所示:

24.png

学到了什么?

知识点同前面示例。

25. sticky-navbar

效果如图所示:

25.png

学到了什么?

一个SVG Icon组件的写法:

<template>
    <svg 
        viewBox="0 0 1024 1024" 
        version="1.1" 
        xmlns="http://www.w3.org/2000/svg" 
        :width="width"
        :height="height"
    >
        <template v-if="Array.isArray(iconComponent)">
            <path 
                v-for="(item,index) in iconComponent" 
                :key="item.prop + index" 
                :fill="item.color ? item.color : color" 
                :d="item.prop"
            ></path>
        </template>
        <template v-else-if="isString(iconComponent)">
             <path :fill="color" :d="iconComponent"></path>
        </template>
    </svg>
</template>
<script lang="ts" setup>
    import { iconPathData } from "@/utils/iconPathData";
    import type { IconType } from "@/utils/iconPathData";
    import { isString } from "@/utils/utils";
    import { computed } from "vue";
    const props = defineProps({
        width:{
            type:Number,
            default:30
        },
        height:{
            type:Number,
            default:30
        },
        type:String,
        color:{
            type:String,
            default:"#fff"
        }
    });
    const iconComponent = computed(() => iconPathData[props.type as keyof IconType]);
</script>

似乎这样封装不太好。

transition组件,用于添加过渡效果的组件。

Promise.allSettled这个可以参考文档

其它知识点同前面示例。

26. double-slider

效果如图所示:

26.png

学到了什么?

知识点同前面示例。

27. toast-notification

效果如图所示:

27.png

学到了什么?

typescript

这个函数有点意思:

interface ConsoleType {
    black: string;
    red: string;
    green: string;
    yellow: string;
    blue: string;
    magenta: string;
    cyan: string;
    white: string;
    bgBlack:string;
    bgRed: string;
    bgGreen:string;
    bgYellow:string;
    bgBlue: string;
    bgMagenta:string;
    bgCyan: string;
    bgWhite:string;
}
export function colorize<T>(...args:Array<T>){
    return {
        black: `\x1b[30m${args.join(' ')}`,
        red: `\x1b[31m${args.join(' ')}`,
        green: `\x1b[32m${args.join(' ')}`,
        yellow: `\x1b[33m${args.join(' ')}`,
        blue: `\x1b[34m${args.join(' ')}`,
        magenta: `\x1b[35m${args.join(' ')}`,
        cyan: `\x1b[36m${args.join(' ')}`,
        white: `\x1b[37m${args.join(' ')}`,
        bgBlack: `\x1b[40m${args.join(' ')}\x1b[0m`,
        bgRed: `\x1b[41m${args.join(' ')}\x1b[0m`,
        bgGreen: `\x1b[42m${args.join(' ')}\x1b[0m`,
        bgYellow: `\x1b[43m${args.join(' ')}\x1b[0m`,
        bgBlue: `\x1b[44m${args.join(' ')}\x1b[0m`,
        bgMagenta: `\x1b[45m${args.join(' ')}\x1b[0m`,
        bgCyan: `\x1b[46m${args.join(' ')}\x1b[0m`,
        bgWhite: `\x1b[47m${args.join(' ')}\x1b[0m`
    };
}

可以参考这里详细了解这个函数。

接口的继承,如:

export function consoleByColorKey<T,U extends keyof ConsoleType>(str:string,key="black"){
    return console.log(colorize(str)[key as U]);
}

createVNode方法,vue内部方法,提供一个创建vNode节点的方法,这也是这个示例的难点,然后是render函数。

其它知识点同前面示例。

28. github-profiles

效果如图所示:

28.png

学到了什么?

知识点同前面示例。

29. double-click-heart

效果如图所示:

29.png

学到了什么?

知识点同前面示例。

30. auto-text-effect

效果如图所示:

30.png

学到了什么?

知识点同前面示例。

31. password generator

效果如图所示:

31.png

学到了什么?

获取自定义属性

const target = e.target as HTMLDivElement;
const lang = target.dataset.lang;

实现复制文本的方法:

const confirm = () => {
    (window as any).ewConfirm({
        title:data.value.confirmTitle,
        content:data.value.confirmContent,
        showCancel:false
    })
}
// `navigator.clipboard.writeText` not working in wechat browser.
if(navigator.userAgent.toLowerCase().indexOf('micromessenger') === -1){
  navigator.clipboard.writeText(password.value).then(() => confirm())
}else{
    const input = document.createElement("input");
    input.value = password.value;
    document.body.appendChild(input);
    input.select();
    document.execCommand("copy");
    input.remove();
    confirm();
}

这里用了两个插件。

其它知识点同前面示例。

32. good-cheap-fast

效果如图所示:

32.png

学到了什么?

知识点同前面示例。

33. Notes App

效果如图所示:

33.png

学到了什么?

classnames函数的实现,源码值得细细品读:

export type Value = boolean | string | number | undefined | null | Symbol;
export type Mapping = Record<string,unknown>;
export interface ArgumentArray extends Array<Argument> {}
export type Argument = Mapping | Mapping | ArgumentArray;
const hasOwn = Object.prototype.hasOwnProperty;
export default function classnames(...args:ArgumentArray):string{
    const classes = [];
    for(let i = 0,len = args.length;i < len;i++){
        const arg = args[i];
        if(!arg){
            continue;
        }
        if(typeof arg === "string" || typeof arg === "number"){
            classes.push(arg);
        }else if(Array.isArray(arg)){
            if(arg.length){
                const __class = classnames.apply(null,arg);
                if(__class){
                    classes.push(__class)
                }
            }
        }else if(typeof arg === "object"){
            if(arg.toString === Object.prototype.toString){
                for(let key in arg){
                    if(hasOwn.call(arg,key) && arg[key]){
                        classes.push(key);
                    }
                }
            }else{
                classes.push(String(arg));
            }
        }
    }
    return classes.join(" ");
}

然后就是这3个工具函数,如下:

export function createUUID(){
    return Math.floor(Math.random() * 10000) + '-' + Date.now().toString().slice(0, 4) + '-' + Math.floor(Math.random() * 10000);
}
export function getNotesData(key:string = "notes"){
    try {
        return JSON.parse(localStorage.getItem(key) as string);
    } catch (error) {
        return [];
    }
}
export function setNotesData(key:string,value:Array<any>){
    return localStorage.setItem(key,JSON.stringify(value));
}

创建一个uuid以及存储笔记数据的会话存储方法的一个封装。

其它知识点同前面示例。

34. animated-countdown

效果如图所示:

34.png

学到了什么?

javascript

defineExpose方法,用于向父组件传递方法,可以允许父组件通过ref来访问子组件的方法。如:

defineExpose({
    startRunAnimation
});
const startRunAnimation = () => {
    //...
}

InstanceType类型为typescript内置的一个类型,如下:

InstanceType<typeof AsyncNumberGroup> | null> // => 代表返回这个组件AsyncNumberGroup的实例类型

其它知识点同前面示例。

35. image-carousel

效果如图所示:

35.png

学到了什么?

这个示例展示了一个轮播组件的封装,但这个示例似乎还有点小bug,源码涉及到的知识点在前面的示例当中也能够找到,不必赘述。

36. hover board

效果如图所示:

36.png

学到了什么?

知识点同前面示例。

37. pokedex

效果如图所示:

37.png

学到了什么?

创建一个index.d.ts,可以定义命名空间,然后定义接口返回的类型,如下:

declare namespace 命名空间名 {
    //定义接口
}

这样做的好处就是我可以直接在组件当中定义该类型,vsCode会自动帮我们显示访问的属性,然后我们就不必要去打印数据看看数据结构再来决定取得什么属性了,这在实际开发当中是一个很常用的提升开发效率的技巧,这也是为什么typescript在后续维护当中非常好维护的原因之一。

其它知识点同前面示例。

38. mobile-tab-navigation

效果如图所示:

38.png

学到了什么?

知识点同前面示例。

39. password-strength-background

效果如图所示:

39.png

学到了什么?

这个示例主要展示了如何将tailwindcss添加到vue项目当中去,跟着官方提供的教程就可以了。

知识点同前面示例。

40. 3d-background-boxes

效果如图所示:

40.png

学到了什么?

知识点同前面示例。

41. verify-account-ui

效果如图所示:

41.png

学到了什么?

知识点同前面示例。

42. live-user-filter

效果如图所示:

42.png

学到了什么?

这里有个预加载组件和加载组件值得研究一下,预加载组件的实现思路就是利用Image构造函数来提前加载图片,如果图片未请求完成,则显示loader组件。关于loader组件的实现可以参看源码

watchEffect函数,与watch函数有所不同,该函数是一个副作用函数,可以直接传入一个回调函数,内部会自动监听数据的变动,当数据改变后,该函数内部就会执行。

其它知识点同前面示例。

43. feedback-ui-design

效果如图所示:

43.png

学到了什么?

这个示例值得学习的是使用css做了一个翻转效果,其实也就是利用了rotate函数。

其它知识点同前面示例。

44. custom-range-slider

效果如图所示:

44.png

学到了什么?

知识点同前面示例。

45. netflix-mobile-navigation

效果如图所示:

45.png

学到了什么?

这个示例展示了一个递归组件的实现,其实参考官方示例代码就可以学习到思路。

其它知识点同前面示例。

46. quiz-app

效果如图所示:

46.png

学到了什么?

知识点同前面示例。

47. testimonial-box-switcher

效果如图所示:

47.png

学到了什么?

知识点同前面示例。

48. random-image-feed

效果如图所示:

48.png

学到了什么?

这个示例预览组件的实现还是值得学习的,如下:

<template>
    <Teleport to="body">
        <div class="preview-mask" :class="{ show: modelValue }" @click="emit('update:modelValue', false)">
            <async-pre-load-image :src="src" class="preview-mask-image"></async-pre-load-image>
        </div>
    </Teleport>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, PropType, toRefs } from 'vue';
const AsyncPreLoadImage = defineAsyncComponent(() => import('./PreLoadImage.vue'));
const props = defineProps({
    src: String as PropType<string>,
    modelValue: Boolean as PropType<boolean>
});
const emit = defineEmits(['update:modelValue']);
const { src, modelValue } = toRefs(props);
emit('update:modelValue');
</script>

涉及到了Teleport组件的使用。

其它知识点同前面示例。

49. todo-list

效果如图所示:

49.png

学到了什么?

本示例实现了一个Modal弹框组件,并且实现了一个自定义clickoutside指令

其它知识点同前面示例。

50. insect-catch-game

效果如图所示:

50.png

学到了什么?

这是一个比较综合的示例了,几乎涵盖了前面所有示例的语法,本人个人也觉得这个示例值得学习,细细品味一番。

看完本文,你对vue3以及scss和less是否有了更深入的了解呢?当然也许还有我没有总结到的知识点,更细致的就要去通读源码了,我个人是认为每一个示例都涉及到了相应的知识点,至少亲手完成了这50个示例,我对vue3的使用更加熟练了。

如果觉得本文有任何错误,欢迎批评指正。