资讯专栏INFORMATION COLUMN

前端每日实战:162# 视频演示如何用原生 JS 创作一个查询 github 用户的应用(内含 2

light / 2775人阅读

摘要:令下半部分的子元素竖向排列横向排列三组数据,每项之间加入细分隔线设置跳转到的链接样式和悬停效果至此,下半部分布局完成。接下来用伪元素把头像图片作为整体背景到这里,整体的静态布局就完成了。

效果预览

按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。

https://codepen.io/comehope/pen/oQGqaG

可交互视频

此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。

请用 chrome, safari, edge 打开观看。

第 1 部分:
https://scrimba.com/p/pEgDAM/cEPkVUg

第 2 部分:
https://scrimba.com/p/pEgDAM/crp63TR

(因为 scrimba 不支持 web animation api,第 2 部分末尾的动画效果在视频中看不到,请参考 codepen)

源代码下载

每日前端实战系列的全部源代码请从 github 下载:

https://github.com/comehope/front-end-daily-challenges

代码解读

这是一个读取 github 用户的应用,在搜索框内输入用户名,点击搜索后,就会通过 github 的 open api 取得用户信息,填写到下方的卡片中。

整个应用分成 3 个步骤开发:静态的页面布局、从 open api 读取数据并绑定到页面中、增加动画效果。

一、页面布局

定义 dom,整体结构分成上部的表单和下部的用户卡片:

居中显示:

body {
    margin: 0;
    height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: #383838;
}

定义应用容器的尺寸:

.app {
    width: 320px;
    height: 630px;
    font-family: sans-serif;
    position: relative;
}

这是表单的 dom 结构,2 个表单控件分别是文本输入框 #username 和搜索按钮 #search,因为后面的脚本要引用这 2 个控件,所以为它们定义了 id 属性,接下来在 css 中也使用 id 选择器:

令 2 个表单控件横向排列:

form {
    height: 50px;
    background-color: rgba(255, 255, 255, 0.2);
    border-radius: 2px;
    box-sizing: border-box;
    padding: 8px;
    display: flex;
}

分别设置 2 个表单控件的样式:

input {
    border: none;
    font-size: 14px;
    outline: none;
    border-radius: inherit;
    padding: 0 8px;
}
  
#username {
    flex-grow: 1;
    background-color: rgba(255, 255, 255, 0.9);
    color: #42454e;
}
  
#search {
    background-color: rgba(0, 97, 145, 0.75);
    color: rgba(255, 255, 255, 0.8);
    font-weight: bold;
    margin-left: 8px;
    cursor: pointer;
}

为按钮增加悬停和点击的交互效果:

#search:hover {
    background-color: rgba(0, 97, 145, 0.45);
}

#search:active {
    transform: scale(0.98);
    background-color: rgba(0, 97, 145, 0.75);
}

至此,表单布局完成,接下来做用户卡片布局。
用户卡片的 dom 结构如下,卡片分成上半部分 .header 和下半部分 .footer,上半部分包括头像 .avatar、名字 .name 和位置 .location,下半部分包括一组详细的数据 .details 和一个跳到 github 的链接 .to-github

Octocat

San Francisco

令卡片的上半部分和下半部分竖向排列,并分别设置两部分的高度,大约是上半部分占卡片高度的三分之二,下半部分占卡片高度的三分之一,此时可以看出卡片的轮廓了:

.profile {
    width: 320px;
    position: absolute;
    margin: 20px 0 0 0;
    display: flex;
    flex-direction: column;
    border-radius: 5px;
}

.header {
    height: 380px;
    background-color: rgba(0, 97, 145, 0.45);
}

.footer {
    height: 180px;
    background-color: rgba(0, 97, 145, 0.75);
}

令卡片上半部分的子元素竖向排列:

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

设置头像图片,样式为描边的圆形,因为头像图片在后面还会用到,所以把它存储到变量 --avatar 中:

.profile {
    --avatar: url("https://avatars3.githubusercontent.com/u/583231?v=4");
}

.avatar {
    width: 140px;
    height: 140px;
    background-image: var(--avatar);
    margin: 70px 0 0 0;
    background-position: center;
    background-size: cover;
    border-radius: 50%;
    box-shadow: 
        0 0 0 0.8em rgba(0, 0, 0, 0.2),
        0 0 0 1em rgba(161, 220, 255, 0.35);
}

设置名字和位置信息的样式,文字为白色:

.name {
    margin: 50px 0 0 0;
    color: white;
    font-size: 28px;
    font-weight: normal;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
}

.location {
    margin: 5px 0 0 0;
    color: rgba(255, 255, 255, 0.75);
    font-weight: normal;
}

至此,上半部分的布局完成,接下来布局下半部分。
令下半部分的子元素竖向排列:

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

横向排列三组数据,每项之间加入细分隔线:

.details {
    list-style-type: none;
    padding: 0;
    display: flex;
    margin: 40px 0 0 0;
}

.details li {
    color: rgba(255, 255, 255, 0.6);
    text-align: center;
    padding: 0 6px;
}

.details li span {
    display: block;
    color: rgba(255, 255, 255, 0.8);
}

.details li:not(:first-child) {
    border-left: 2px solid rgba(255, 255, 255, 0.15);
}

设置跳转到 github 的链接样式和悬停效果:

.to-github {
    width: 200px;
    height: 40px;
    background-color: rgba(255, 255, 255, 0.5);
    text-align: center;
    line-height: 40px;
    color: rgba(0, 0, 0, 0.75);
    text-decoration: none;
    text-transform: uppercase;
    border-radius: 20px;
    transition: 0.3s;
}

.to-github:hover {
    background-color: rgba(255, 255, 255, 0.8);
}

至此,下半部分布局完成。
接下来用伪元素把头像图片作为整体背景:

.profile {
    position: relative;
    overflow: hidden;
}

.profile::before {
    content: "";
    position: absolute;
    width: calc(100% + 20px * 2);
    height: calc(100% + 20px * 2);
    background-image: var(--avatar);
    background-size: cover;
    z-index: -1;
    margin: -20px;
    filter: blur(10px);
}

到这里,整体的静态布局就完成了。

二、绑定数据

为了绑定数据,我们引入一个羽量级的模板库:

把卡片 .profile 包含的 dom 结构改写为 html 模板 #template,其中的 o 代表绑定的数据数据对象:

声明一个假数据对象 mockData,它的数据结构与 github open api 的数据结构是一致的:

let mockData = {
    "avatar_url": "https://avatars3.githubusercontent.com/u/583231?v=4",
    "name": "The Octocat",
    "location": "San Francisco",
    "public_repos": 111,
    "followers": 222,
    "following": 333,
    "html_url": "https://github.com/octocat",
}

定义一个把数据绑定到 html 模板的函数 render(container, data),第 1 个参数 container 表示 dom 容器,模板内容将填充在此容器中;第 2 个参数是数据对象。在页面载入时调用 render() 方法,把 mockData 作为参数传入,此时看到的效果和纯静态的效果一致,但用户卡片已经改为动态创建了:

window.onload = render(document.getElementsByClassName("profile")[0], mockData)

function render(container, data) {
    container.innerHTML = tmpl("template", data)
    container.style.setProperty("--avatar", `url(${data.avatar_url})`)
}

定义一个从 github open api 读取用户信息的方法 getData(username),然后调用 render() 方法把用户信息绑定到 html 模板。同时,把 window.onload 绑定的事件改为调用 getData() 方法,此时看到的效果仍和纯静态的效果一致,但数据已经变成动态读取了:

window.onload = getData("octocat")

function getData(username) {
    let apiUrl = `https://api.github.com/users/${username}`
    fetch(apiUrl)
        .then((response) => response.json())
        .then((data) => render(document.getElementsByClassName("profile")[0], data))
}

为表单的 search 按钮绑定点击事件,实现搜索功能。可以查一下自己的 github 帐号试试看:

document.getElementById("search").addEventListener("click", () => {
    let username = document.getElementById("username").value.replace(/[ ]/g, "")
    if (username == "") {
        return
    }
    getData(username)
})
三、增加动画效果

为了能让用户感受到每次搜索后数据的变化过程,我们增加一点动画效果。创建一个 update(data) 函数来处理动画和渲染逻辑,同时把 getData() 函数的最后一步改为调用 update() 函数:

function getData(username) {
    let apiUrl = `https://api.github.com/users/${username}`
    fetch(apiUrl)
        .then((response) => response.json())
        // .then((data) => render(document.getElementsByClassName("profile")[0], data))
        .then(update)
}

function update(data) {
    let current = document.getElementsByClassName("profile")[0]
    render(current, data)
}

当页面首次载入时,不需要动画,直接渲染默认的用户信息即可。变量 isInitial 表示本次调用是否是在初始化页面时调用的,若是,就直接渲染。若不是,下面会执行动画效果。

function update(data) {
    let current = document.getElementsByClassName("profile")[0]
    let isInitial = (current.innerHTML == "")

    if (isInitial) {
        render(current, data)
        return
    }
}

动画的过程是:创建一张新卡片,把数据绑定到新卡片上,然后把当前卡片移出视图,再把新卡片移入视图。下面的变量 next 代表新创建的卡片,把它定位到当前卡片的右侧:

function update(data) {
    let current = document.getElementsByClassName("profile")[0]
    let isInitial = (current.innerHTML == "")

    if (isInitial) {
        render(current, data)
        return
    }

    let next = document.createElement("div")
    next.className = "profile"
    next.style.left = "100%"
    render(next, data)
    current.after(next)
}

因为动画分成 2 个动作——当前卡片移出和新卡片移入,所以我们定义 2 个动画效果,变量 animationOut 代表移出动画的参数,变量 animationIn 代表移入动画的参数。其中,keyframes 属性值相当于写 css 动画时用 @keyframes 定义的关键帧,options 属性值相当于写 css 动画时 animation 语句后面的参数,新卡片移入动画有半秒钟的延时。

function update(data) {
    let current = document.getElementsByClassName("profile")[0]
    let isInitial = (current.innerHTML == "")

    if (isInitial) {
        render(current, data)
        return
    }

    let next = document.createElement("div")
    next.className = "profile"
    next.style.left = "100%"
    render(next, data)
    current.after(next)

    let animationOut = {
        keyframes: [
            {left: "0", opacity: 1, offset: 0},
            {left: "-100%", opacity: 0, offset: 1}
        ],
        options: {
            duration: 500,
            fill: "forwards"
        }
    }

    let animationIn = {
        keyframes: [
            {left: "100%", opacity: 0, offset: 0},
            {left: "0", opacity: 1, offset: 1}
        ],
        options: {
            duration: 500,
            fill: "forwards",
            delay: 500
        }
    }
}

因为动画需异步执行,即在当前卡片移出的动画结束后再执行新卡片移入的动画,所以我们令当前卡片移出的动画结束后触发 onfinish 事件,然后再执行新卡片移入的动画,同时把旧卡片删除掉:

function update(data) {
    let current = document.getElementsByClassName("profile")[0]
    let isInitial = (current.innerHTML == "")

    if (isInitial) {
        render(current, data)
        return
    }

    let next = document.createElement("div")
    next.className = "profile"
    next.style.left = "100%"
    render(next, data)
    current.after(next)

    let animationOut = {
        keyframes: [
            {left: "0", opacity: 1, offset: 0},
            {left: "-100%", opacity: 0, offset: 1}
        ],
        options: {
            duration: 500,
            fill: "forwards"
        }
    }

    let animationIn = {
        keyframes: [
            {left: "100%", opacity: 0, offset: 0},
            {left: "0", opacity: 1, offset: 1}
        ],
        options: {
            duration: 500,
            fill: "forwards",
            delay: 500
        }
    }

    let animate = current.animate(animationOut.keyframes, animationOut.options)
    animate.onfinish = function() {
        current.remove()
        next.animate(animationIn.keyframes, animationIn.options)
    }
}

最后,限定动画效果仅在 .app 容器中展现:

.app {
    overflow: hidden;
}

大功告成!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/108701.html

相关文章

  • 前端每日实战 2018年10月至2019年6月项目汇总(共 20 个项目)

    摘要:过往项目年月份项目汇总共个项目年月份项目汇总共个项目年月份项目汇总共个项目年月份项目汇总共个项目年月份项目汇总共个项目年月份项目汇总共个项目年月至年月发布的项目前端每日实战专栏每天分解一个前端项目,用视频记录编码过程,再配合详细的代码解读, 过往项目 2018 年 9 月份项目汇总(共 26 个项目) 2018 年 8 月份项目汇总(共 29 个项目) 2018 年 7 月份项目汇总(...

    twohappy 评论0 收藏0
  • 前端每日实战 2018年10月至2019年6月项目汇总(共 20 个项目)

    摘要:过往项目年月份项目汇总共个项目年月份项目汇总共个项目年月份项目汇总共个项目年月份项目汇总共个项目年月份项目汇总共个项目年月份项目汇总共个项目年月至年月发布的项目前端每日实战专栏每天分解一个前端项目,用视频记录编码过程,再配合详细的代码解读, 过往项目 2018 年 9 月份项目汇总(共 26 个项目) 2018 年 8 月份项目汇总(共 29 个项目) 2018 年 7 月份项目汇总(...

    muddyway 评论0 收藏0
  • 前端每日实战162# 视频演示何用原生 JS 创作一个查询 github 用户应用内含 2

    摘要:令下半部分的子元素竖向排列横向排列三组数据,每项之间加入细分隔线设置跳转到的链接样式和悬停效果至此,下半部分布局完成。接下来用伪元素把头像图片作为整体背景到这里,整体的静态布局就完成了。 showImg(https://segmentfault.com/img/bVbjLk3?w=400&h=301); 效果预览 按下右侧的点击预览按钮可以在当前页面预览,点击链接可以全屏预览。 htt...

    OnlyLing 评论0 收藏0
  • 前端每日实战162# 视频演示何用原生 JS 创作一个查询 github 用户应用内含 2

    摘要:令下半部分的子元素竖向排列横向排列三组数据,每项之间加入细分隔线设置跳转到的链接样式和悬停效果至此,下半部分布局完成。接下来用伪元素把头像图片作为整体背景到这里,整体的静态布局就完成了。 showImg(https://segmentfault.com/img/bVbjLk3?w=400&h=301); 效果预览 按下右侧的点击预览按钮可以在当前页面预览,点击链接可以全屏预览。 htt...

    pcChao 评论0 收藏0
  • 前端每日实战:163# 视频演示何用原生 JS 创作一个多选一场景交互游戏(内含 3 个视频

    摘要:本项目将设计一个多选一的交互场景,用进行页面布局用制作动画效果用原生编写程序逻辑。中包含个展示头像的和个标明当前被选中头像的。 showImg(https://segmentfault.com/img/bVbknOW?w=400&h=302); 效果预览 按下右侧的点击预览按钮可以在当前页面预览,点击链接可以全屏预览。 https://codepen.io/comehope/pen/L...

    pakolagij 评论0 收藏0

发表评论

0条评论

light

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<