📝 项目是 22 年初的,一直在往里加东西,结果之后模拟考整个就忘在草稿箱里了…
PerfMatch 是一款改进版的雇主-外籍劳工匹配工具,是一个同时考虑外籍劳工(FDWs)和雇主双方需求的双向匹配平台。PerfMatch 旨在维护FDWs的权益,让FDWs有机会发出自己的声音。
演示网址:https://perfmatch.github.io/
仓库地址:https://github.com/liangchuxin/PerfMatch
前端
PerfMatch 的前端使用的是 ReactJS 框架,本小白接触过的唯一一个前端框架。和原生方案相比,使用 ReactJs 的好处在于当几个页面有相同的部分时,从一个页面切换到另一个页面便不需要整个页面重新加载,而是只需要加载变化的部分。一个极端的例子是,当只有一行数据变了的时候,原生方案需要加载整个 innerHTML,造成巨大额浪费,ReactJS 则不需要。
React 还有非常好的一点是,所有的动作、动画都由 state 管理,各种操作集中于一处。虽然现在仍然被各种 state 的 bug 卡得满头包,但为了还是坚持折腾下去继续学习
文件树
使用的 component 包括以下这些:
├── components │ ├── CompleteDetails.js // 完善个人资料页面 │ ├── DropDown.js // 导航栏的下拉式菜单 │ ├── EmployeeDetails.js // 打工人的个人资料表格 │ ├── EmployerDetails.js // 雇主的个人资料表格 │ ├── FadeIn.js // 随着滚动显示元素的视觉效果 │ ├── FindMyPassword.js // 忘记密码 │ ├── Footer.js // 页脚 │ ├── Header.js // 页首 │ ├── HomePage.js // 主页 │ ├── JoinEmployerPage.js // 用户个人中心 │ ├── Login.js // 登录页面 │ ├── MatchList.js // 匹配结果页面 │ ├── RecentMatch.js // 首页的最新匹配部分 │ ├── SignUp.js // 注册页面 │ └── useToken.js // 生成登录、注册使用的token
除了自己写的 ReactJS + SCSS 样式,少部分的元素因为不会+方便直接使用了 Material-UI 的 library,如匹配结果页面左侧栏的过滤年龄的范围选择器,以及匹配对象会讲的语言处的多选框。其他部分的设计都是先在Sketch 中设计好的模板,再照着写成代码的。
Sketch设计
Sketch 的模板(后来在写成网站的过程中还有一些样式上的微调):
依次为:完善信息(FDWs / 雇主)、信息提交成功、注册 / 登录、主页、页脚、下拉式菜单、用户中心、匹配结果
设计使用的主色调是 #ed721d
,辅色为 #ffc094
,标题字体使用 Heebo(从 Nightingale的模板借鉴而来),其他包括正文字体都是 Grotesk(某天无意发现的可爱字体,后来觉得不太适合做网站)。设计总体是简约扁平风格,也使用了一些微阴影来突出层次)。
以上设计是电脑端的,移动端暂未设计,因此 DEMO 网站也只有 PC 端能看。
随滚动显示
这一样式在主页的“最近匹配”和“匹配流程”模块,以及匹配结果页面都有用到,只要 import FadeIn.js,把想随滚动显示的元素夹在 FadeIn.js
里定义的 FadeInSection
函数标签里即可,非常简单方便。
<FadeInSection> {/*滚动显示的内容...*/} </FadeInSection>
在这之前一直不知道原来 functional component 可以和 HTML 标签一样分成 opening tag 和 closing tag 两部分写,之前的用法一直是定义一个 function 要返回的固定的值,然后直接把整个 function 连头带尾放进去,如 <Header />
。
下面是 FadeInSection.js
component 返回的部分,可以看出这个 component 获取了一些 props,然后通过 props.children
把包在两个标签内的元素全部返回。这样达到的效果便是,在原有的元素外面包裹了一层 FadeInSection
里定义的 div 标签,因此整个标签以及其内包括元素的样式与显示逻辑都由这个 component 决定。
function FadeInSection(props) { // ...省略了一坨显示元素的逻辑 return ( <div className={`fade-in-section ${isVisible ? "is-visible" : ""}`} // state hook 真好用 ref={domRef} > {props.children} </div> ); }
后续 & 反思
字体选择
字体的选择应慎重考虑,一个字体可以直接影响网站的整体风格。如果是偏正式的网站,如项目的官网,产品的 landing page,那么正文内容应该用标准一些的常见字体。可爱风更适合一些个人的站点,如果放到正式一些的网站上就会显得不伦不类。
以强仔博客为例,自定义可爱字体能使页面风格十分活泼:
但用在 PerfMatch 上就显得非常不正式,甚至让整个平台都显得不专业了,不像是专业匹配平台,而像是随手做的 PPT。
Flexbox 的使用
不要过度依赖 flexbox,虽然视觉效果都实现了,但居中单个元素这种简单操作能不用 flex 就不用。给每个 div是在是不合适,如果有更简单的解决方案应该要更简便的方法。
这篇文章介绍了 flexbox 的使用场景,其中提到,在页面布局上尤其不该使用 flex。这点直接踩雷了。从 SIAT 的项目开始,首页的两列濒危动物都是通过 flexbox 实现的。这次首页也是通过 flexbox 分成了 employee 和 employer 左右两部分。
Flexbox 方案:
<style> .main { display: flex; } .left, .right { width: 50%; } </style> <div class="main"> <div class="left"></div> <div class="right"></div> </div>
自身浮动方案:
<style> .left, .right { width: 50%; } .left { float: left; } </style> <div class="main"> <div class="left"></div> <div class="right"></div> </div>
flex 虽然方便易学易上手,但也有需要注意的弊端。首先,作为一种较新的布局,Flexbox 的兼容性较差,只能兼容 ie9 以上,因此如果 FDWs 使用的是老式浏览器,就不能正确显示。其次,在网站有大量内容或是访客网络新号一般时,页面元素可能会在加载页面的过程中像这样到处移动。其原因在于,当 DOM 元素还未全部渲染出来时,Flexbox 的设置会一直根据自己的规则改变已显示出的元素的排列方式,极度影响用户体验(不过作为一个demo实际上问题不大)。
后端
PerfMatch 网站后端使用的是 Python 的 Django 数据库,并使用 REST API 实现数据库内容的写入、获取、排序与过滤等功能。
第一次在自己的个人 Project 里在前端的基础上加上后端,之前的保护野生动物 SIAT 网站就是一个纯前端的站点,首页的剩余动物数量是人工写死的,每种动物详细页面爬虫抓取文章的功能是由傻瓜型第三方API 实现的。第一次深刻体会到后端能有多么头疼——写前端是上课摸个鱼的事(仅限于我这种小白级别的前端设计),而后端若是出bug则能在原地卡一个晚上而仍然对哪里出错毫无头绪…
Django REST framework
是一款用来建立网站接口的API,据说功能大性能好还有若干其他好处等等等等,但本小白除了用 PHP 给ZblogPHP博客写插件外也没接触过过其他后端框架——Django 是第一次接触,DRF 是学的第一个 Django 框架,因此一些优劣对比在此便不再赘述了。
这里推一个 Django REST Framework 的中文文档,对于我这种英语时行时不行官方文档看得一头雾水的留学生帮助极大——
REST API
REST API 又名 RESTful API,遵循 REST 架构规范。当我们通过 REST API发出请求时,它会将资源状态表述通过 JSON 或一些其他格式传递给请求者。
使用 REST API 的主要原因是它的简便性。在 REST API 中,使用四个 HTTP 动词,即 GET
(获取资源),POST
(新建资源),PUT
(更新资源)和 DELETE
(删除资源),便可以对服务器端的资源进行操作。
后端的文件树是这样的:
api ├── __init__.py ├── __pycache__ │ ├── ... // 此处省略了一堆cache文件 ├── admin.py ├── apps.py ├── filters.py ├── migrations │ ├── ... // 此处省略了数据库文件 ├── models.py ├── serializers.py ├── tests.py ├── urls.py └── views.py
主要文件说明
api
- 储存所有 REST API 的文件
./filters.py
- 使用django filters从年龄、工作经验以及名字对匹配结果进行过滤
./models.py
- 包括 Employers和 employees的模型,例如 name
,age
,year_of_experience
等等,类似于一个账号的所有属性。还有一个是 account 模型,包含用户名和密码,是所有注册用户都会有的属性
./serializers.py
- 这是 Django REST 框架中非常重要的一部分,创建序列化 class,将复杂的数据 - 如模型事例(model instances)转为简单的 python 数据类型,以便于输出为 JSON 文件。例:
class EmployeeSerializer(serializers.ModelSerializer): class Meta: model = Employee fields = ('id', 'name', 'age', 'profile_photo', 'description', 'email', 'created_at')
./views.py
- 视图的创建与配置。在这里定义了两个最主要的视图类:雇主和员工,以及两个相对应的、用于获取个别雇主或员工信息的类(ComSci 刚好在学 OOP,这就是实例中的 mutator 和 accessor)例:
class EmployerView(generics.ListAPIView): queryset = Employer.objects.all() #从视图返回某对象查询结果 serializer_class = EmployerSerializer #序列化输出 filter_backends = (filters.DjangoFilterBackend,OrderingFilter,) #用于过滤查询集的过滤器 filterset_class = EmployerFilter #EmployerFilter来自./filters.py,用户的过滤请求在这里处理。也可写成,filter_fields = ['EmployerFilter'] #pagination_class 分类模板,此处没用到,也是一个可以配置的选项 ordering_fields = '__all__' #所有fields都可以用来排序
这里使用了的是 GenericAPIView
的子类(统称工具视图),在 APIView
和 GenericAPIView
的基础上,给标准 list 和 detail view 添加了一些常用的行为。譬如在这里使用的 ListAPIView
,作用是同时查询多条数据(群查)。
除此之外,还有另外几种不同功能的视图:
视图 | 作用 |
CreateAPIView | 新增一条数据 |
RetrieveAPIView | 查询一条数据 |
UpdateAPIView | 修改一条数据 |
DestroyAPIView | 删除一条数据 |
RetrieveUpdateAPIView | 单查,更新一条数据 |
RetrieveUpdateDestroyAPIView | 单查,更新,删除一条数据 |
工具视图用起来极为方便,继承父类后提供 queryset 与 serializer_class 即可(这俩是工具视图独有的 attributes)。在上面的例子中,使用 queryset 的好处在于,这个属性(返回的查询结果)可以直接设置,而不需要再把 Employer instantiate 一遍然后再传一遍给 serializer,方便许多。
ListAPIView
和 APIView
的版本对比(省略了过滤和排序部分):
# APIView class EmployerView(APIView): def get(self, request, format=None): Employer = Employer.objects.all() serializer = EmployerSerializer(Employer, many=True) return Response(serializer.data) # ListAPIView class EmployerView(generics.ListAPIView): queryset = Employer.objects.all() #从视图返回某对象查询结果 serializer_class = EmployerSerializer #序列化输出
选择视图类继承的父类固然重要,不过更强大的要数后面用于排序与过滤器。ListAPIView
中,各种过滤器可以通过 filter_backends
加入,这里使用的是
class GetEmployerView(APIView): serializer_class = EmployerSerializer lookup_url_kwarg = 'id' def get(self, request, format=None): id = request.GET.get(self.lookup_url_kwarg) if id != None: employer = Employer.objects.filter(id=id) if len(employer) > 0: data = EmployeeSerializer(employer[0]).data return Response(data, status=status.HTTP_200_OK) return Response({'Employer not found': 'Invalid id'}, status=status.HTTP_404_NOT_FOUND) return Response({'Bad Request': 'id parameter not found'}, status=status.HTTP_400_BAD_REQUEST)
./urls.py
- 路由,给 REST API 设置链接路径,把之前创建的不同 model 分配到单独的地址(api),as_view()
这个method 可以把一个类变成展示这个类的函数
from django.urls import path from .views import EmployerView, EmployeeView, CreateEmployerView, GetEmployerView, getToken urlpatterns = [ path('employers', EmployerView.as_view()), path('employees', EmployeeView.as_view()), path('createEmployer', CreateEmployerView.as_view()), path('getEmployer', GetEmployerView.as_view()), path('getToken', getToken) ]
BUG 专区
1. 匹配结果页面 filter 按钮不好使
所谓不好使,即选择不一样的 filter 条件后匹配结果毫无反应,需要疯狂连击才能让显示的匹配结果刷新
找到问题:过滤匹配结果的原理是,在用户点击不同的 filter 条件后,会触发对应的函数,同时传入当前选项相对应的 parameters,例如如果选择的过滤选项是“选择工作经验三年以上的员工”,那么触发的函数就会是 clickSetYoe([3, 100])
。这些函数会根据传进去的 parameters 改变相对应组件的 state,最后触发最后一个名为 getEmployeeDetails
的函数。该函数会用 state 的变量放在一个 REST API的地址里,从而从数据库中获取用户需要的信息。
// 用户点击过滤选项后触发的函数 const clickSetYoe = (range) => { setYoeRange(range); getEmployerDetails(); }; // 名为 yoeRange 的组件,初始范围是 -1 到 100 const [yoeRange, setYoeRange] = React.useState([-1, 100]); // 通过 REST API 结合各种 state 从数据库获取相应信息 const getEmployerDetails = () => { fetch( `/api/employers?age_min=${ageRange[0]}&age_max=${ageRange[1]}&yoe_min=${yoeRange[0]}&yoe_max=${yoeRange[1]}&ordering=${orderOpt}` ) .then((response) => response.json()) .then((data) => { setEmployeeFeed(data); }); };
尚未解决
-
如何使用 css transition group 给右上角额下拉式菜单增加进入动画
-
如何在同一网页多个部分加载同一图片 API 时 显示不同的图片(后来未设置头像的用户统一显示默认头像,而不是随机图像 API)
-
如何更严谨地设计注册、登录系统,使用不同的 token 而不是
test123
评论列表
你好,看完你的博客文章,感觉很不错!希望与你网站首页友情链接
大流量卡
http://53go.cn
专注于移动/联通/电信推出的大流量多语音活动长短期套餐手机卡的相关知识的介绍普及
听说互换友情链接可以增加网站的收录量,特此来换,如果同意的话就给internetyewu@163.com发信息或者就在此回复下吧!
怎么所有的图片都显示不出来?
@Sirit 不知道呢 🤦♂️ 一直都用的 jsdelivr,可能国内一段时间用不了了,我在墙外还挺快…国内的很多图床我这边看不见,已经无所谓了 ┓( ´∀` )┏