高仿全球航拍爱好者和专业摄影师作品社区【天空之城】,从零开始一步步实现逻辑,详细讲解

158 阅读6分钟

我正在参与掘金创作者训练营第5期,****点击了解活动详情

本课程是大疆旗下的天空之城(全球航拍爱好者和专业摄影师的作品社区)的仿站习作。
页面排版精美,照搬原站UI,一键导入HTML和CSS,还原度非常高,包括PC端和手机端,几乎是以假乱真。
本课程重点是带领读者写表达式完成业务逻辑,但并不复杂,适合初期学习打基础,表达式讲解得比较细致,囊括了众触平台的大部分概念。
视频讲解非常详尽,从空白页面开始一步步教学。

赶紧用电脑浏览器访问开发版一睹为快吧!左上方切换到移动版可以看到不同的排版布局。
如果你是用手机浏览的,也可以访问浏览版访问,会自动切换到手机版。

手机版和桌面版

UI是分别设计,共用后端数据,互不影响

后端数据

网站跑起来需要一定量的数据,此课程从已有成品中克隆了部分后端数据。摄影作品数据保存在product表,摄影师数据保存在user表。外部CSS也一起克隆了。

摄影师列表页

  1. 一键导入移动版页面。

  2. onReady事件

    通过$user.search()读取10个摄影师,再循环读取每个摄影师的3个作品$product.search()

  3. 数据组件

    两个数据组件分别渲染摄影师和每个摄影师的作品列表。

  4. 动态样式

    摄影师的头像和作品的背景图片都是使用动态样式设置的。

  5. 分页插件
    移动版用的是迷你分页,适用于窄屏,桌面版用的是分页,适用于宽屏。
    插件是开箱即用的,只需要配置一下【路径】即可(填users),但这里分页除了自动读取用户(摄影师)数据外,还得进一步读取他们的摄影作品,所以在【onPageChanged】里要写读取作品列表的表达式。

  6. 桌面版页面
    同样的方法导入桌面版,UI不同,表达式相同。

共享组件

  1. 桌面版的页首菜单栏共享组件和页脚共享组件

  2. 移动版页首菜单栏共享组件

作品详情页

  1. 一键导入移动版页面

  2. 根节点数据源选择【product】,页面就会自动准备好URL里ID的摄影作品。

  3. 在onReady里根据作品作者读取作者姓名头像$user.get(auth)

  4. 在【渲染条件】里判断是视频还是图片分别渲染

  5. 同理导入桌面版页面

摄影师页

  1. 一键导入移动版页面

  2. 搜索摄影师的作品列表$product.search()

  3. grid/list布局样式变换。

  4. 立即渲染render()

  5. 统计当前摄影师所有作品被赞总数。

  6. 如果用户滚到页底时触发交叉观察器(IntersectionObserver),自动search下一页的产品列表。

桌面版探索页

难点是需要用到onResize(),监听页面尺寸的改变从而动态改变作品数量并渲染不同的列数。

移动版探索页

默认只读取小部分数据,初始时只读取4个标签,每个标签只读取4个作品。
难点是要用到IntersectionObserver(),每个标签的横滚动条拉动是要能自动获取更多作品;纵滚动条往下拉时能继续拉起更多标签以及它们的部分作品。

首页幻灯片

利用Swiper外部插件,用挂载组件包装,挂载的时候load()一下swiper js和css,再new Swiper()把参数配置进去。

移动版首页

热门航拍点和热门标签里数据组件的数据源是在【应用中心】的$c常量里预设好的$c.obj.region$c.obj.tags对象,获取对象keys作为数组来循环渲染的。
在页面onReady里,先是读取前10个作品,然后读取这些作品各自的作者,这里用到了提高性能的策略:有些作品可能是同一个作者的,那读取一次就可以了(使用l.users.unique()去重);有些作者可能以前就先读取到前端来了,那就没必要再读取了(核对l.users.unique()去重);有些作者可能以前就先读取到前端来了,那就没必要再读取了(核对c.user[x.auth\]是否存在)。 user.search()时用$in选项来搜索:

$user.search("users", {_id: {$in: $l.users.unique()}}, {select: "x.img x.name"})

在页底添加一个交叉观察器(IntersectionObserver),这样滚到页底时(交叉观察器出现时)触发交叉观察器里配置的表达式,总是不停地读取下一页的数据追加进来,让人感觉总是有无限的作品一样。

桌面版首页

桌面版的热门航拍点没有展示所有的$c.obj.region,而是通过slice(0, 6)展示前6个,其它的通过【更多】链接来连到【标签】页。
桌面版有个【精选】探索$c.arr.explore,每种类型的探索下都有个静态分页,右边有个【更多】链到对于的【探索】页。

浏览量

用户打开作品页时(页面onReady时)会检查以前此用户是否访问过此作品,如果没有就给数据库增加$inc一个浏览量view,然后就把此作品$id放入$V.views列表中,并保存到本地存储localStorage
需要注意的一点是$V里的都是全局变量,整个应用的任何地方都可以访问。

stopIf($V.views.includes($id))
$product.modify($id, {$inc: {"y.view": 1}})
$V.views.push($id)
localStorage("views", $V.views)

点赞

类似于增加浏览量,但不是在onReady时自动做,而是用户点击触发时执行,还不让取消点赞。
另外点赞按钮要根据当前用户是否已经当前作品点过赞而改变样式,用到了动态类名:$V.likes.includes($id) ? " SOTD" : ""

stopIf($V.likes.includes($id), 'info("不可重复点赞")')
$product.modify($id, {$inc: {"y.like": 1}})
$V.likes.push($id)
localStorage("likes", $V.likes)

创建账号

使用开箱即用的插件(zp133)来注册和登陆账号,简单好用。

收藏

类似于点赞,但可以取消收藏,所以要判断是否已经收藏了$V.favorites.includes($id),未收藏用$inc: 1,已收藏用$inc: -1

$c.me ? "" : $me.get()
$l.U1.$inc["y.favorite"] = $V.favorites.includes($id) ? -1 : 1
$product.modify($id, $l.U1)
$l.U2[$V.favorites.includes($id) ? "$pull" : "$push"]["x.arr"] = $id
$xtk.modify("favorite", $c.me._id, $l.U2, 1)
$V.favorites = $r.x.arr

另外在xtk表中还维护了当前用户的收藏列表,在用户登录后(xtk表中还维护了当前用户的收藏列表,在用户登录后(c.exp.onLogin)会读取此列表放入$V.favorites中:

$xtk.get("favorite", $c.me._id)
$V.favorites = $r.x.arr

所以用户点收藏的时候需要根据是否收藏来删除/拉走$pull此作品ID,或追加$push此作品。

关注

关注的按钮在多个页面都会用到,功能相同,所以我们把它抽到全局表达式里面$c.exp.follow

$l.U1.$inc["y.follower"] = ($V.follows.includes(_id) ? -1 : 1)
$user.modify(_id, $l.U1)
$l.U2[$V.follows.includes(_id) ? "$pull" : "$push"]["x.arr"] = _id
$xtk.modify("follow", $c.me._id, $l.U2, 1)
$V.follows = $r.x.arr

逻辑跟收藏非常相似,需要维护“我”的关注列表,也需要动态类名$V.follows.includes($id) ? " _3-aF" : ""

修改个人信息

个人头像的背景图动态样式设置的,如果有缩略图$v.thumb就用缩略图,否则使用表单x里的img字段$f.x.img,点击时会触发下一个兄弟元素的点击事件$el.nextElementSibling.click(),而它是一个隐藏的文件输入框file input,它的onChange事件会根据用户上传的第一个文件创建一个缩略图:

stopIf(!$el.files[0])
$v.file = $el.files[0]
$v.thumb = $w.URL.createObjectURL($v.file)

点击【保存】按钮会上传个人头像,修改个人信息user.modify(),清除user.modify(),清除v.file和$v.thumb:

$v.file ? exc('upload($v.file); $r.url ? $f.x.img = $r.url : warn("上传失败")')  : ""
$user.modify($c.me._id, $f.x)
$r._id ? info("已保存") : warn("保存失败")
$v.file = undefined
$v.thumb = undefined

发布图片

支持拖拽多文件上传:在onReady里执行$exp.dropZone添加拖放区域对dragenter、dragover、dragleave和drop事件的监听。前3个事件不做啥,只是阻住浏览器默认行为。

$v.dropZone = $(".dropZone")
stopIf(!$v.dropZone)
$v.noop = func('$arg[0].preventDefault()')
$v.dropZone.addEventListener("dragenter", $v.noop)
$v.dropZone.addEventListener("dragover", $v.noop)
$v.dropZone.addEventListener("dragleave", $v.noop)
$v.dropZone.addEventListener("drop", func($exp.οndrοp))

关键点是drop事件里调用$exp.οndrοp

$arg[0].preventDefault()
$l.files = $w.Array.from($arg[0].dataTransfer.files)
stopIf(!$l.files[0])
$id === "videos" ? $exp.toUploadVideo.exc() : $l.files.filter('$x.type.includes("image")').forEach($v.toUpload)
render()
timeout(99)
$exp.dropZone.exc()

先把拖放事件里附带的文件列表dataTransfer.files转换成数组,把图片过滤出来后调用$v.toUpload提取图片缩略图$v.files进行渲染,渲染后拖放去发生了改变,执行$exp.dropZone对渲染后的页面再次监听拖放事件。(如果是视频上传(下一节内容)就执行$exp.toUploadVideo

支持点击多文件上传:点击拖放区会触发(el.lastElementChild.click())隐藏在里面的文件输入框fileinput,它的onChange事件会把用户上传的文件列表过滤出图片来并执行el.lastElementChild.click())隐藏在里面的文件输入框file input,它的onChange事件会把用户上传的文件列表过滤出图片来并执行v.toUpload。为了支持多文件,文件输入框添加了个自定义属性multiple:multiple。

点击【发布】按钮会有个上传动画,文件大网络慢时能看到文件一步步上传的进度条,但文件小网络快时此上传动画动画一闪而过就不容易觉察出来,此时可以调低网速体验进度圈,也可以掐掉网络体验上传失败,点击失败区域会自动重传。

发布视频

类似于发布图片,但发布视频只能上传一个,同时多一个上传封面的功能

接口安全

为了防止本案例的数据变的凌乱,本案例使用默认的基本安全配置。

准备深入研究的同学请到www.zcappp.cn/course/skyp…页面后,点击右侧的【克隆】按钮,把整个应用复制一份随意更改。但由于接口安全的设定,你的账户没有权限修改其它用户的数据或读取敏感数据,此时作为应用开发者可以到【后端安全】删除或放松权限设定。

声明

本课程于2021年5月9日仿自www.skypixel.com,相关素材版权归属于原网站,本课程仅做教学用。
源站随着时间的推移可能会不断演变,而课程作品并不会一起跟着变化,所以作品跟你现在看到的源站不同是正常的。