结果预览
按下右边的“点击预览”按钮能够在当前页面预览,点击链接能够全屏预览。
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,团体构造分红上部的表单和下部的用户卡片:
<div class="app">
<form>
<!-- 暂略 -->
</form>
<div class="profile">
<!-- 暂略 -->
</div>
</div>
居中显现:
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 选择器:
<form>
<input placeholder="Who are you looking for?" id="username">
<input type="button" value="Search" id="search">
</form>
令 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
:
<div class="profile">
<div class="header">
<div class="avatar"></div>
<h2 class="name">Octocat</h2>
<h3 class="location">San Francisco</h3>
</div>
<div class="footer">
<ul class="details">
<li>Repositories<span>111</span></li>
<li>Followers<br><span>222</span></li>
<li>Following<br><span>333</span></li>
</ul>
<a href="#" class="to-github">go to github</a>
</div>
</div>
令卡片的上半部份和下半部份竖向分列,并离别设置两部份的高度,大约是上半部份占卡片高度的三分之二,下半部份占卡片高度的三分之一,此时能够看出卡片的表面了:
.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);
}
到这里,团体的静态规划就完成了。
二、绑定数据
为了绑定数据,我们引入一个羽量级的模板库:
<script src="https://blueimp.github.io/JavaScript-Templates/js/tmpl.min.js"></script>
把卡片 .profile
包括的 dom 构造改写为 html 模板 #template
,个中的 o
代表绑定的数据数据对象:
<script type="text/x-tmpl" id="template">
<div class="header">
<div class="avatar"></div>
<h2 class="name">{%= o.name %}</h2>
<h3 class="location">{%= o.location %}</h3>
</div>
<div class="footer">
<ul class="details">
<li>Repositories<span>{%= o.public_repos %}</span></li>
<li>Followers<br><span>{%= o.followers %}</span></li>
<li>Following<br><span>{%= o.following %}</span></li>
</ul>
<a href="{%= o.html_url %}" class="to-github">go to github</a>
</div>
</script>
声明一个假数据对象 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;
}
功德圆满!