小幸运项目总结

同时打三份工的日子终于放缓了一会儿,可以静下心来学点和写点总结博客了,这篇就是关于小幸运的总结博客

项目概览

大家应该也都玩过小幸运,具体的功能需求我就不多说了,当时暑假开工的时候,Uni那边是分了一个同级的女生一起,由于两个人都没有什么开发经验,所以一开始抽组件就没怎么抽,能复用的逻辑也大部分没有思考到(就比如弹窗),一开始就是各自写各自的页面,所以也没有什么能过多交流的部分,再之后就是Uni那边换了一个大三的学长带我写,无论是代码的规范性还是一些逻辑写法都让我学到很多,(就比如css多按两个tab健就能让代码第一眼看上去无比美观)

接下来的介绍顺序应该是我先说一些我自己写的并值得拿出来的东西,然后就说一下通过这个项目学到了哪些。

我写的部分就是Home -> Wishes -> Send三个界面 和Service的封装

Fetch的封装

进去后学长就让我先负责Service对象的封装,因为之前写的组件啊和路由跳转啊啥的都太自主化了(就是没怎么沟通我自己一个人在写根本不知道对方负责的部分到哪儿了的那种),很乱,所以学长就去重构部分静态代码和重写组件了,测接口的任务就我自己开始写了。

而之前并没有什么和后端有交互的项目经历,唯一的hackathon还是用Taro写的,没什么参考性,所以一开始就蛮懵的。就去找了些团队项目的代码直接放上去了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 我不懂为啥不直接lucky.itoken呢...
const BASEURL = window.location.href.slice(0, window.location.href.indexOf('/', 10)) + "/api"

function Fetch(url, opt = {}) {
const token = localStorage.getItem('token') //现在一般取名叫Authorization


opt.method = opt.method || 'GET';
opt.headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
};

opt.headers.token = token

opt.body = JSON.stringify(opt.data) || null;
if (opt.formdata) {
opt.body = opt.formdata;
}
return fetch(url, opt)
.then(response => {
if (response.ok) {
return response.json().then(res => {
return res;
});
} else {
return response.json().then(res => {
return new Promise((_, reject) => { //可以直接写成Promise.reject()
reject(res);
});
});
}
}).then(res => {
if (res.status === -2) {
alert(res.msg)
localStorage.removeItem('token')

// 重定向到根目录,重新登录
let redirectpos = window.location.href
redirectpos = redirectpos.slice(0, redirectpos.indexOf('/', 10) + 1)
window.location.href(redirectpos)
}
else {
if (res.status !== 0) {
alert(res.msg)
}
return res;
}
})
.catch(e => {
alert(`服务端错误:${e.message}`)
throw e;
})
}

这算是最简单的一种封装形式了,然后我看了下菁程和老版工作台的代码,他们的token都存在了cookie里面,然后就有关于cookie操作的文件,目前我也没有遇到过要用cookie的情况,就没去仔细看了,然后他们还在Fetch的基础上再封装,也就是直接把GET,POST,DELETE这些请求方式当作方法暴露出来,就可以直接以get(url)方式来写了(菁程是这样的)。然后这里在测试的时候有一个bug,就是用户去查看自己的愿望时看到的愿望不是自己的,我当时是以为是后端问题,因为这是返回的response错了,但后端明确说了自己没问题,然后武理那边说这是缓存的问题,就是GET请求会有缓存而POST请求没有,所以要么给GET请求加上时间戳,要么把换成POST请求,换成POST的话后端那边就要改,于是就选择了前者,然后前者就需要再每一个GET里面加上一句生成当前时间戳的代码

1
2
3
4
5
6
getLightManInfo(id) {
let url = new URL(BASEURL + '/user/info/lightman')
url.searchParams.append("wish_id", id)
url.searchParams.append("time", new Date().getTime())
return Fetch(url)
},

所以这里如果我们之前封装好了GET,把这段逻辑放进去,就不需要重复写了,可惜时间上来不及了唉。。。

然后菁程对于我刚刚说的GET啊POST啊的再封装是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const proxy = (fn) => {
return (url, data?) => {
const opts: OptsProps = data; //高阶函数? 函数柯里化?
return fn(url, opts);
};
};

const encapsulationMethod = (method) => { // 把时间戳加在这里就行
let optsObj;
return (url, opts) => {
optsObj = opts || {};
optsObj.method = method;
return FetchData(url, optsObj);
};
};

export const get = proxy(encapsulationMethod('GET'));
export const post = proxy(encapsulationMethod('POST'));
export const put = proxy(encapsulationMethod('PUT'));
export const Delete = proxy(encapsulationMethod('DELETE'));

export const putCompanyInfoLogo = (data) => {
return put('/company/info/logo', {
token: true,
data
});
};

然后呢,现在写新的Ts项目,比如工作台2.0和木犀云,都用了Pont, 在Pont里只需要最简单的处理一下fetch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const Request = (url, options) => {
if (options === void 0) {
options = {}
}
url = `/api/v1${url}`

options.headers = {
Accept: 'application/json',
'Content-Type': 'application/json'
}
options.headers.Authorization = localStorage.getItem('token')

if (options.body) {
options.body = JSON.stringify(options.body)
}

return fetch(url, options).then(function (res) {
return res.json()
})
}

export default Request

// 在最外层
pontCore.useFetch(Request)

至于Pont的具体原理还没有细究,但是很牛逼,其他的什么url都不用写,参数会自动提示,只需要调用接口就行!

滑动愿望的动画

这个动画没啥好讲的,我用的就是最原始的写法,没有用任何库,原理和暑假直播讲的景深特效差不多,首先三个树叶绝对定位,然后写上不同的z-index凸显层级,再然后左边的就left: -10vw 右边的就left:10vw。然后这里要处理三个事件,onTouchStart, onTouchMove, onTouchEnd,分别对应手指接触屏幕,手指在屏幕上滑动,手指离开屏幕。然后根据手指在屏幕上滑动的距离找出一个比例关系去改变left就行。 思路和实现都很简单,但这种写法我觉得还是蛮烂的,所以就简单的放一下相对应的事件处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Start/Move/End 都是控制愿望刷新动画的相关函数
const onTouchStart = (e) => {
const touch = e.targetTouches[0]
setStartX({ start: touch.pageX, move: '' })

}
const onTouchMove = (e) => {
const touch = e.targetTouches[0]
const move_X = ((touch.pageX - startX.start) / 5)
setStartX(startX)
setMove({ img1: move_X, img2: 10, img3: 20 })
}
const onTouchEnd = () => {
setUpDate(true)
if (move.img1 < -25) {
setMove({ img1: -90, img2: 0, img3: 10 })
}
else if (move.img1 > 20) {
setMove({ img1: 90, img2: 0, img3: 10 })
}
else {
setMove({ img1: 0, img2: 10, img3: 20 })
return
}
// 刷新愿望
setTimeout(() => {
setUpDate(false)
let newWishSource = wishes
newWishSource.push(newWishSource[0])
newWishSource.splice(0, 1)
setWishes(newWishSource)
// 刷新动画
setMove(moveState)
}, 200)
}

然后就事后看了一些动画库 没准儿以后能用上

优化方案

官网

  • react-spring 这个好像可以用来写树叶滑动的动画,但没实践

还有一些其他的,但也没必要,这种库会一个就行,大不了自己写,虽然可能会烂的一批….

武理的第三方登录

然后这里就是关于武理的一个第三方登录机制,就是说武理那边不直接暴露接口给Uni用嘛,就需要跳转到智慧理工大那个页面

1
2
3
4
5
6
const goWHUT = () => {
let position = window.location.href
let continueurl = position.slice(0, position.indexOf('/', 10))
let posturl = continueurl + "/api/login/whut/callback"
window.location.href = `https://ias.sso.itoken.team/portal.php?posturl=${encodeURIComponent(posturl)}&continueurl=${encodeURIComponent(continueurl)}`
}

然后登录成功后智慧理工大会自动带着cookie重定向回来,我们就在cookie里拿到token就行了

1
2
3
4
5
6
7
8
9
10
useEffect(() => {
let token = cookie.load('jwt_token');
if (token) {
localStorage.setItem('token', token)
props.history.push("/")
}
if (!localStorage.getItem("token")) {
props.history.push("/login")
}
}, [props.history])

学长告诉我是调用了一个ias接口,这我查半天没查到这个接口到底是个啥,总体的登录逻辑应该和通行证差不多吧,带着一个landing过去并存入cookie防止landing丢失,然后登录成功后依据landing并带着code重定向回来,嗯,大概是这样

文本下划线的自动生成

这个! 破需求! 折磨了我好久 ! 查了n多方法!终于让我看到大佬的实现方式 直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
// wishes页面的下划线生成(处理超长文本)
.wishes {
background-image: linear-gradient(to right, rgba(241, 197, 126, 0) 10px, transparent 10px),
linear-gradient(to left, rgba(241, 197, 126, 0) 10px, transparent 10px),
linear-gradient(rgba(241, 197, 126, 0) 30px, black 30px, black 31px, white 31px);
}
// send页面的下划线生成
.send {
background-image: linear-gradient(to right, rgb(241, 197, 126) 10px, transparent 10px),
linear-gradient(to left, rgb(241, 197, 126) 10px, transparent 10px),
linear-gradient(rgb(241, 197, 126) 30px, black 30px, black 31px, white 31px);
}

这样就当你在textarea里面写下超长文本后,由于background- image的默认repeat,咱就能看见自动出现的下划线咯! 不过好像有点bug,就是在send页面,下划线不会跟着滑动,而wishes可以,这个没太懂,可能是textarea的原因?

总体逻辑

最后在大概梳理一下小幸运的逻辑吧,(没准有遗忘的地方?)

  1. 进入选择学校,武理还是华师,两个Link,无交互,如果选择武理就是到智慧理工大进行第三方登录,华师的话跳转到login界面
  2. 在login界面输入账号密码,调后端给的接口,登陆成功后token存入localstorage中,并调用checkemail接口查询是否绑定邮箱,未绑定就到bindemail页面去调接口绑定一下再去home,绑定的就直接去home (这里前端没有写正则规范邮箱格式,不知道后端有没有写)
  3. home页面就是九个分类嘛,一个tags数组map生成就行,修一下样式,然后根据分类可以进入愿望池,或者直接投递我的愿望
  4. 投递我的愿望也就是一个from嘛,愿望内容分类姓名联系方式啥的,不是很难,就是要写太多受控组件比较麻烦,不过应该会有一种自定义hooks的写法统一处理的,不过相关写法我自己没想出来也没查到,不然太多useState()也太麻烦了(又比如可以用antd的那种组件封装的形式?)
  5. 愿望池页面的难点就是动画和对树叶上超长文本愿望的处理咯,这里上面也已经说过了,(overflow:scroll和下划线解决),然后一个是点亮愿望的按钮一个是去往我的愿望列表
  6. 点亮愿望的按钮按下去就会弹出一个表单,和Send页面几乎一致,也就是多写几个useState()的事。。。
  7. 然后到愿望列表页,分两个区域调两个接口,一个是MyPost一个是MyWish,根据投递时间排序,然后左下角有愿望状态显示
  8. 再之后就是到愿望详情页,对单个愿望进行实现/删除等操作,这里都是学长的部分了也没细究
  9. 还有一个分享页面,其实也就是针对一个愿望,页面几乎一致只不过有一个单独的点亮按钮

总结

嗯….好像就没了?因为其实小幸运的页面逻辑不太难,静态方面用好flex就行(因为太多图片啦),然后有一个需要仔细考虑的地方就是,愿望状态分为未点亮/点亮未实现/已实现,然后愿望自身又分为自己投递的/点亮别人的,这些愿望都会出现在wishList这个页面,然后跳转到愿望详情detail那里,就要写很多判断来决定在detail页面到底是渲染哪种愿望状态的组件上去(因为不同状态的愿望按钮不一样,比如点亮的button是确认实现,然后已实现的button就会变成灰色等),这个页面是学长负责的,我只是提供了点思路和想法,学长一个文件写了快五百行,因为当时快到上线期了,所以估计也没怎么拆组件复用逻辑了,总之小幸运还是如期上线了,第一个上线的项目能引起很多人去参与还是蛮有成就感的嘿。

最后贴一个项目链接lucky