loading

HCloud DEMO 总结


毕业设计总结

HCloud 是一个全栈 JavaScript 开发的项目也是一个从零开始的项目,前端采用的是 uni-app 跨端框架,后端 EggJS,前端在 uni-app nvue (weex) 纯原生渲染技术的加持下,功能和流畅度方面和原生 app 差距不大。

项目 CODE 和成品链接:

前端 后端 展览页

功能思维导图

空格鼠标拖动移动视图,滚轮放大放小

移动端不支持思维导图

  • Hcloud
    • 登录/注册/找回
      • 账号密码登录
      • 短信登录/注册/找回
    • 首页
      • 最近动态列表
        • 预览
        • 长按进入选择模式(可单/多选)
          • 打开所在目录
          • 下载
          • 预览
      • 搜索文件
      • 上传文件
      • 传输列表
        • 点击切换
        • 已完成
          • 上传
            • 跳转到所在目录
          • 下载
            • 打开文件
        • 长按进入选择模式(可单/多选)
          • 继续
          • 暂停
          • 取消
    • 文件列表
      • 查找文件(同首页相同,两个入口)
      • 点击文件预览
      • 视图规则(过滤、排序、切换展示模式)
      • 长按进入选择模式(可单/多选)
        • 重命名
        • 移动
        • 下载
        • 分享
        • 删除
    • 相册
      • 九宫格布局
      • 右侧上下拖动快速定位到所选日期
      • 长按进入选择模式(和文件页相同)
      • 点击预览
    • 个人中心
      • 用户基本信息
        • 点击进入用户详情
      • 空间使用情况
      • 分享管理页面
        • 点击进入分享详情页
          • 查看文件
          • 取消分享
          • 设置提取码
          • 复制链接
        • 长按进入选择模式(可单/多选)
          • 取消分享
          • 复制链接
      • 回收站页面
        • 点击/长按进入选择模式
          • 恢复文件
          • 彻底删除文件
      • 关于页面
      • 二维码扫描
      • 退出登录
    • 添加
      • 扫一扫
      • 新建文件夹
      • 本地文件上传
      • 相册文件上传
      • 解析分享链接
  • fresh-purple
  • filetree

知识点&难点总结

前端

本项目前端技术栈使用的是 Uni-app Weex Native Vue Vuex Scss 等,是一个纯 NVue 开发的 APP

全局弹窗

在 NVUE 中,由于有些组件是 webView 渲染,有些则是原生渲染,这两种的的存在就造成了 webView 的组件无法覆盖原生渲染的组件,官方虽说在纯 NVUE 的渲染页面不存在覆盖不了的问题,但是导航栏和 tabbar 等原生组件在纯 NVUE 渲染的情况下依旧无法覆盖

解决覆盖问题的方法有多个 如下:

  • SubNVue(盖上一个新的 webView 窗口,缺点: 使用方式有所限制,扩展性不够,只支持 APP 端)
    参考链接
  • 自定义导航栏和 tabbar(缺点:页面跳转性能方面变差,因为只能改成组件路由的切换模式来跳转页面)
  • 新健一个背景透明的 Nvue 页面(能覆盖所有组件但是只支持 APP 端)
  • 自行绘制原生控件(利用官方的 H5PLUS API 绘制,自定义强,但交互、动画等效果比较难实现)
{
  "style": {
    "navigationStyle": "custom",
    "backgroundColor": "transparent",
    "app-plus": {
      "background": "transparent",
      "titleNView": false
    }
  }
}

在布局方面,nvue 只支持 flex 弹性布局,px/rpx 单位,不支持百分比,并且也不支持 DOM 操作,局限性比较大,以下是传统布局在 nvue 中的实现方式

flex

在 nvue 中自带 display:flex css 设置,不用手动添加该样式,默认 column 垂直方向,手动设置可覆盖,也可修改全局默认设置 manifest.json -> app-plus -> nvue -> flex-direction 节点下修改

manifest.jsonlink
"nvue" : { "flex-direction" : "row" }

撑满屏幕/w100% h100%

// 1
view {
  display: flex;
  flex: 1;
}

// 2
view {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
}

//3  在uni-app中,750rpx即可以当做 width:100% 来使用,但在宽大于一定程度后,不在自动撑满,该方案只能自动撑满宽度,高度只能自行获取屏幕高度设置
view {
  width: 750rpx;
}

3 参考链接:

js动态获取屏幕宽高link
const { windowWidth, windowHeight, navigationBarHeight, statusBarHeight } = uni.getSystemInfoSync() // 屏幕高度 = 原生NavigationBar高度(含状态栏高度)+ 可使用窗口高度 + 原生TabBar高度 // windowHeight不包含NavigationBar和TabBar的高度 // H5端,windowTop等于NavigationBar高度,windowBottom等于TabBar高度 // App端,windowTop等于透明状态NavigationBar高度,windowBottom等于透明状态TabBar高度 // 高度相关信息,要放在 onReady 里获取

获取状态栏、导航栏高度信息等参考链接

自定义字体图标导入

在 webView 中,可以使用传统的方式导入字体图标,但在 Nvue 中传统的方式无效,得使用官方提供的插件 api 导入,以下是导入代码 参考链接

const domModule = uni.requireNativePlugin('dom')
domModule.addRule('fontFace', {
  fontFamily: 'iconfont',
  src: `url('file:/${plus.io.convertLocalFileSystemURL('_www/static/icon/iconfont/iconfont.ttf')}')` // android 读取本地文件路径,通常得加上前缀 file:///
})

动画

在 APP 端, 动画的实现方式也有所限制和不同

技术关键词transition@keyframesplugin-animationplugin-bindingxlottie
支持性YESNOYESYESYES

以下是本项目对 plugin-animation 的封装和调用方式

util/animation.js
const animation = uni.requireNativePlugin('animation') // 对单 export function animate(target, options) { return new Promise((resolve, reject) => { if (!target || !options) return reject('params error') animation.transition(target, options, () => { resolve(true) }) }) } // 对群 export async function animates(multiple) { if (Object.prototype.toString.call(multiple) !== '[object Array]') return console.error('Not an Array') for (let item of multiple) { await animate(item.target, item.options) } }
common/js/mixins/dragpanel.js
animate(this.$refs['ctx'], { styles: { transform: 'translateY(100%)' }, duration: 200, timingFunction: 'ease', delay: 0, needLayout: true }).then(() => { uni.navigateBack() })

交互

一般交互动画应用场景如: 点击 拖动 元素增删改查过渡 初始化等等… , 在交互上做一些功夫能使整个 APP 无论是视觉上还是体验上都有很好的感光提升,能加强引导用户操作,也能在页面数据还未加载完成时做一个替代品,但想要实现并落地一个交互动画还是比较困难的(对于全部都由我自己来做 o(╥﹏╥)o 的酷比来说), 因为要考虑之间融不融洽、过渡是否顺畅等等视觉感官上的设计,还要考虑这个交互的动画实现成本,不能太占资源而导致整个 app 卡顿

在 uni-app 中,使用传统的 JS 方式来做交互的话,性能会比较差,为什么呢? 这里引用官方文档的话: 逻辑层和视图层分离的。此时会产生两层通信成本。比如拖动视图层的元素,如果在逻辑层不停接收事件,因为通信损耗会产生不顺滑的体验,因而在本项目中我采用的是 Weex 提供的 Bindingx 插件 来做交互动画
uni-app 官方说明 > Bindingx 官网文档 > Weex BindingX 尝鲜

首页

index

pages/tabbar/index/index.nvue
bindingx.bind({ eventType: 'scroll', // 容器 anchor: getEl(this.$refs['index']), props: [ { // 要执行动画的元素 element: getEl(this.$refs['top']), // 要执行动画的属性 property: 'opacity', // 每次执行的运算表达式 expression: '1-y/' + this.dynamicTop } ] })
我的

my

pages/tabbar/personal/personal.nvue
let circleY_targetY = uni.upx2px( size.top + (this.navHeight - statusBarHeight) + (this.navHeight - statusBarHeight - uni.upx2px(100) * 0.6) / 2 ), qs_targetY = uni.upx2px( size.top + (this.navHeight - statusBarHeight) + (this.navHeight - statusBarHeight - uni.upx2px(60) * 0.9) / 2 ), list_header_height = uni.upx2px(300) const userOpacity = `max(1-min(y,${uni.upx2px(100)})/${uni.upx2px(100)},0)`, navOpacity = `min(y,${uni.upx2px(list_header_height + this.navHeight)})/${uni.upx2px( list_header_height + this.navHeight )}`, userY = `y>${uni.upx2px(100)}?-500:0`, circleY = `(0-min(y,${list_header_height})/${list_header_height})*${circleY_targetY}`, circleX = `(0-min(y,${list_header_height})/${list_header_height})*${uni.upx2px(100)}`, circleScale = `(y-${this.navHeight})<=0?1:(1-min(.4,1*(y-${this.navHeight})/${list_header_height}))`, qsY = `(0-min(y,${list_header_height})/${list_header_height})*${qs_targetY}`, qsX = `min(y,${list_header_height})/${list_header_height}*${uni.upx2px(90)}`, qsScale = `(y-${this.navHeight})<=0?1:(1-min(.1,1*(y-${this.navHeight})/${list_header_height}))` bindingx.bind({ eventType: 'scroll', anchor: getEl(this.$refs['personal']), config: { transformOrigin: 'center' }, props: [ { element: getEl(this.$refs['nav']), property: 'opacity', expression: navOpacity }, { element: getEl(this.$refs['user']), property: 'opacity', expression: userOpacity }, { element: getEl(this.$refs['user']), property: 'transform.translateY', expression: userY }, { element: getEl(this.$refs['qs']), property: 'transform.translateY', expression: qsY }, { element: getEl(this.$refs['qs']), property: 'transform.scale', expression: qsScale }, { element: getEl(this.$refs['qs']), property: 'transform.translateX', expression: qsX }, { element: getEl(this.$refs['circle']), property: 'transform.translateY', expression: circleY }, { element: getEl(this.$refs['circle']), property: 'transform.translateX', expression: circleX }, { element: getEl(this.$refs['circle']), property: 'transform.scale', expression: circleScale } ] })
拖动式抽屉组件

dragComponent

components/custom/custom-dragPanel/custom-dragPanel.vue
bindingx.bind( { eventType: 'pan', anchor: el, props: [ { element: el, property: 'transform.translateY', expression: 'max(y+' + this.y + ',0)' } ] }, e => { if (e.state === 'end') { if (this.autoTop) { if (e.deltaY > this.ctxHeight / 2) return this.back() else { this.show() } } else { this.y += e.deltaY if (this.y < 0) this.y = 0 if (this.y > this.ctxHeight / 2) return this.back() } } } )

绘制原生导航栏

在本项目中,长按选择文件会显示当前已选的个数和取消、全选按钮的导航栏窗口,并且会覆盖原生自带的窗口,实现了类似百度云盘等 app 选择模式下的同等 UI,采用官方提供的H5PLUS -> nativeObj 原生控件绘制 API 绘制,这里也涉及到了 JS 和 native 之间的通信

util/pages.js
function createSelectNav() { // 获取坐标 const { statusBarHeight, windowWidth } = uni.getSystemInfoSync() // 初始化控件参数 let nav = new plus.nativeObj.View('nav', { top: `${statusBarHeight}px`, left: '0px', height: `44px`, width: '100%', backgroundColor: '#4070ff', statusbar: { background: '#4070ff' } }) // 绘制导航栏里的的内容 nav.draw([ { tag: 'font', id: 'title', text: '', textStyles: { size: '16px', color: 'white' }, position: { bottom: '0px', height: '44px' } }, { tag: 'font', id: 'cancel', text: '取消', textStyles: { size: '16px', color: 'white' }, position: { left: '43%' } }, { tag: 'font', id: 'all', text: '全选', textStyles: { size: '16px', color: 'white' }, position: { right: '84%' } } ]) // 绑定点击事件 nav.addEventListener('touchstart', function (e) { let itemWidth = windowWidth * 0.2 if (e.screenX < itemWidth) { uni.$emit('select-all') } else if (e.screenX > windowWidth - itemWidth) { uni.$emit('cancel-all') } }) return nav }

前端直传 OSS

这部分流程已放到后端,请参阅后端部分

后端

本项目后端技术栈使用的是 EggJS Mysql Redis Sequelize AliYun_SMS JWT,文件存储在 AliYun_OSS,文件上传下载传输采用的是直传 OSS 的方式,省掉了中转到 OSS 的流量

Tips: 手机端视窗太小不适合 mermaid 图展示,看不太清楚,请到 pc 端查看

用户登录/注册/找回

在本项目中,用户注册找回等操作是采用短信验证方式进行限制的,技术栈方面 使用了 redis 替代了 session、 jwt 生成 token、 接入了阿里云大鱼短信验证

用户操作除了账号密码登录不需要短信验证外,其他情况都需要,整个过程如下:

graph TB
    A[注册/找回/登录 流程] -->|发送短信| C[用户是否存在?参数是否正确?]
    C -->|验证通过| D[发出短信]
    C -->|验证失败| E[返回错误提示]
    D -->|用户收到短信,提交验证码|F[短信验证]
    F --> G[是否通过?]
    G --> |通过,跳转到注册或找回密码页面| H[表单页面]
    G --> |短信登录| K[登录成功,清除登录错误记录]
    G --> |失败| E
    H --> |提交表单,验证参数是否合法|I[是否通过?]
    I --> |通过| J[注册/找回成功]
    I --> |失败| H
    A --> |账号密码登录| L[校验账号信息]
    L --> |通过| K
    L --> |失败| Z[记录账号错误次数,达到一定量的时候自动锁定,需等30分钟解锁]

文件管理 CURD

文件管理是本项目中的难点之一,也是比较核心的一部分,下面一部分一部分讲解

请求授权流程
graph LR
    A[请求授权流程] -->|携带token| C[校验token]
    C -->|校验通过| B[利用token获取用户信息]
    C -->|校验失败| E[返回错误提示]
    B --> E1[参数校验]
    E1 --> E2[通过]
    E1 --> E
    E2 --> END[响应成功]
文件存储结构
stateDiagram-v2
    文件夹
    state 文件夹 {
        子文件夹
        state 子文件夹 {
          文件4
          ...子文件1
          子文件夹2
          state 子文件夹2 {
            子文件2
          }
           子文件夹2
          state 子文件夹3 {
            子文件3
            ...N
          }
        }
        ...子文件
    }

由上面的 mermaid(状态图)可以看出,文件夹里有子文件和子文件夹,子文件夹里在有子文件,无限嵌套,这种结构必然得加上递归的方式来进行增删改查处理,之间的逻辑会交纵错杂,经常尝生一条线好了另一条线坏了的情况

在本项目中,文件的结构和文件夹由 mysql 来模拟,使用到了自联查询和 CTE 递归查询,目前这种方案算不上好,但能实现需求,递归查询非常耗性能,文件存储在阿里云 OSS 上,速度方面可以跑满带宽

文件上传

在本项目,文件上传流程: 前端授权并直传 OSS 完成后,OSS 会携带秘钥请求服务端,服务端验证后入库,完成文件上传

sequenceDiagram
    autonumber
    前端 ->> 服务端: 请求签名授权
    服务端 ->> 服务端: 生成唯一签名
    服务端 ->> 前端: 完成签名响应
    前端 -->> OSS: 携带签名,前端直传OSS
    OSS ->> 服务端: 直传完成后,携带签名,请求服务端回调接口
    服务端 ->> 服务端: 校验参数合法性,通过则入库
    服务端 ->> 前端: 文件上传请求,响应完成
重命名

重命名相对比较容易实现,只要检查同目录下是否有相同名称的文件即可

graph LR
    A[重命名] --> |token校验通过| B[参数校验]
    B --> C[当前目录是否有同名文件存在]
    C --> |YES| D[查库,自动在名称上面加序号]
    C --> |NO| F[success]
    D --> F
    G[对单]
移动

由于数据库的字段设计,移动文件不用去递归查询子文件,子文件父 ID 未改动会自动跟随,只要改动顶层的文件夹即可

graph TB
    A[移动] --> |token校验通过| B[参数校验]
    B --> B2[查库校验,文件是否存在]
    B2 --> |NO| E
    B2 -->|YES| C[检查移动目标位置是否在已选择的文件夹下]
    C --> |YES| E[Return Error]
    C --> |NO| F[检查目标位置是否有相同的文件名]
    F --> |YES| J[对重复的文件名自动加上序号]
    F --> |NO| K[success]
    J --> K
    G[对多/单]
删除

在本项目,删除有软删除和硬删除,前者还可以在回收站恢复,后者直接在数据库删除,属于真删除

在删除文件后自动递归删除其下所有子文件的查询,是采用Sequelize Hooks 完成的

graph TB
    A[删除] --> |token校验通过| B[参数校验]
    B --> B2[查库校验,文件是否存在]
    B2 --> |NO| E
    B2 --> |YES| D[params mode]
    D --> |true| D2[硬删除]
    D --> |false| D3[软删除]
    D2 --> D5[更新用户空间信息]
    D5 --> L
    D3 --> L
    L --> L2[触发删除HOOK]
    L2 --> L3[检查删除的文件有没有文件夹类型]
    L3 -->|YES|D
    L3 -->|NO|L
    G[对多/单]
    E[Return Error]
    L[success]
恢复

在恢复文件后,恢复文件如果存储在一个已删除的目录下,那恢复的文件依旧找不到,这时得递归恢复其父目录,把原来的路径的文件夹也一并恢复

在恢复文件后,同样,自动递归其所有父文件,检查是否是删除状态,是则也跟着恢复,一直顶层文件夹或存在的时候停止,采用Sequelize Hooks 完成的

graph TB
    A[恢复] --> |token校验通过| B[参数校验]
    B --> B2[查库校验,文件是否存在]
    B2 --> |NO|E
    B2 -->|YES| B3[检查目录是否有相同的文件名]
    B3 --> |YES|B4[查库,重复文件名自动加上序号]
    B3 --> |NO|L
    B4 --> L
    L --> L2[触发恢复HOOK]
    L2 --> L3[检查恢复的文件有没有文件夹类型]
    L3 -->|YES|B3
    L3 -->|NO|L
    G[对多/单]
    E[Return Error]
    L[success]
预览/获取文件数据

在选择技术栈的时候,考虑带宽速度之类的体验问题,文件选择存储在阿里云的 OSS 上,其优点比较快,但是缺点是流量、费用比较贵 o(╥﹏╥)o,由于文件不是存储在服务端本地,这时需要去 OSS 获取一个临时的授权链接,来进行预览

graph TB
    A[预览/获取文件数据] --> |token校验通过| B[参数校验]
    B --> B2[查库校验,文件是否存在]
    B2 --> |NO|E
    B2 --> |YES| B3[检查OSS文件是否存在]
    B3 --> |NO| E
    B3 --> |YES| B4[是否有缓存]
    B4 --> |NO| B6["文件类型是否属于可预览类型(图片,视频)"]
    B4 --> |YES| B5[从缓存获取]
    B5 --> L
    B6 --> |YES| B7[请求OSS,返回一个处理过的缩略图]
    B6 --> |NO| B8[请求OSS,返回原文件]
    B7 --> C
    B8 --> C
    C --> L
    E[Return Error]
    C[缓存内容]
    L[success,返回一个临时的签名链接,过期失效]

文章作者: Jing Hong
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Jing Hong !
评论
 上一篇
Dev Tools Recommend Dev Tools Recommend
在前端中,修改第三方npm模块有几种方式,有简单也有复杂,在这我先安利一个比较简便的方式,来对第三方模块进行更改并应用到实际项目中去 patch-package patch-package 作用: 可以对第三方库进行快速更改并生成文件用
2021-09-27
下一篇 
Mysql Mysql
MYSQL 数据库数据库介绍=========== 数据库,就是能够存储和管理“大量数据”的一种软件系统的统称。 主流数据库 主流数据库包括:MS SQL Server, Oracle,DB2,Informix, Sybase 等。 他们
2020-05-06
  目录