前言
关于技术与业务的平衡,我一直坚持的观点就是技术服务于业务,业务驱动技术提升。当然,这是理想状态。有的业务确实不需要多高深的技术,也没什么难度和挑战,按部就班即可。直接表现就是面试问到你项目有什么难点和亮点,深思熟虑后你发现确实没什么可说的。
这个时候,有心和无意的区别就很大了。在一个项目中能收获什么,主要还是看自己怎么去发掘。有场景就充分发挥,没场景就创造优化场景,合理即可。
今天我们就来聊一下,如何在一个项目中学习?
编程思维
接到需求,大概想一下就开始编码的程序员并不在少数。这是缺点吗?确切地说,因人而异。对于大佬而言,写的过程就能考虑各种优化和边界处理。直接一步到位,写文档反而显得有些鸡肋。
对于初级程序员来说,很难在写代码的过程中把各种情况和边界考虑全面。甚至有可能写到一半发现之前的思路完全是错的,需要推翻重写。这种返工耗时在项目开发时间不是那么充裕的情况下,是很容易delay的。
所以,建议尚未养成这种全面思考能力的小伙伴们选择文档先行。更专业一点,这种文档被称为技术设计文档,是需要进行评审的。
磨刀不误砍柴工,设计文档越是详细,开发起来越快,甚至可以直接cv大法。文档编写的时候需要体现出大概实现思路,可以细致到组件,方法,变量的命名上。如果涉及数据库,那也要把每一张表的各个字段,类型,注释什么的写好。
具体应该如何设计呢?这里我贴两个实习期项目实战的链接,可以当个参考。
-
设计的魅力(一):导师问:你了解状态树的设计吗?我:什么是状态树?
-
设计的魅力(二): 导师问:新风格级联菜单调整需求想做吗?我:我能做吗?
react上手
我个人是走的react技术栈。vue和angular虽然以前也学过,但基本上忘得差不多了。这部分给出三个链接,都是偏向基础的学习笔记,感兴趣的可以当个参考。
-
react: http://r6a.cn/bsdve
-
vue: http://r6a.cn/bsdvg
-
angular: http://r6a.cn/bsdvy
在我看来,上手一门新技术,最快的方式就是跟着官方示例撸一遍文档。
只要官方文档写的不是很离谱,大部分示例都是比较经典且友好的。
以react为例,明确划分了核心概念和高级指引。看完核心概念,能大概理解组件,属性传递,生命周期,事件处理后,就可以试着写一个todoList。写完没什么问题就可以上手业务开发,这个过程快的话一两个小时即可。如果js还不熟悉,可以去补一下红宝书和犀牛书,都有了新版。
贴一个以前的笔记链接,仅供参考:
redux+antd实现todoList: http://r6a.cn/bsd1e
通用业务逻辑与解决方案
对于固定的业务场景,自然也有通用的业务逻辑和解决方案。这部分实际上会有很多,这里我只说几个我所经历的。
jwt登录认证
这部分给出三个链接,分别是对jwt的解释和在java,node场景下的应用。
- JSON WEB TOKEN : http://r6a.cn/bse8x
- springboot--jwt授权 : http://r6a.cn/bse8n
- koa--jwt-cookie 授权 : http://r6a.cn/bse8q
注册
注册时候需要收集每一个表单项数据并且校验合法性。这里可以用一个数组对象存储,类似下面这种结构。
const form=[
{
formItem:'username',
pass:true
},
{
formItem:'password',
pass:true
},
{
formItem:'phone',
pass:true
},
]
如何使用呢?存在即合理,数组的every方法极为合适,该方法的返回结果用于设置是否可点击注册按钮。
const flag = form.every(item=>item.pass)
避免重复注册
点击注册按钮后,在请求尚未响应回来的空档期,快速点击,就会发送多个注册请求,导致数据库出现冗余数据。
解决也很简单,点击后立刻禁用。如注册成功,则跳转至登录页;如注册失败,则停留在当前页,恢复点击。
加密存储
对于关键的用户信息(如密码)需要加密存储,不能保存明文。
登录或者修改密码输入原密码时也是将加密结果与数据库存储的加密结果比对,不可涉及明文。
图片优化
对于那种涉及很多图片展示的场景,如花瓣网,大屏数据展示,如果不做任何处理,流畅度会大大降低。通用的手段是图片压缩和更换图片格式,两者达到一个平衡需要清晰度和尺寸都能兼顾,这就是webp的落地场景。
当然,这个有兼容性问题,需要考虑降级。详情可参考:
一场关于webp的革命,让你的网站更丝滑
非预期问题排查与经验积累
项目中是有可能会遇到很多踩坑点的,当然这个取决于你个人能力,当时写代码的思维清晰程度,以及一些外在因素。
下面分享几个我踩过的坑以及排查方法。
组件非预期更新
简单来说,就是你希望某个组件更新,但是它依然保留了上次的状态。什么情况下会造成这种情况呢?我举一个例子。
import React, { useState } from 'react';
import { Table, Space, Drawer } from 'antd';
import "antd/dist/antd.css";
const columns = (setVisible, setRecord) => [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
},
{
title: 'Age',
dataIndex: 'age',
key: 'age',
},
{
title: 'Action',
key: 'action',
render: (text, record) => (
<Space size="middle">
<span onClick={() => {
setVisible(true)
setRecord(record)
}}>查看</span>
</Space>
),
},
];
const data = [
{
key: '1',
name: 'John Brown',
age: 32,
},
{
key: '2',
name: 'Jim Green',
age: 42,
},
{
key: '3',
name: 'Joe Black',
age: 32,
},
];
function App() {
const [visible, setVisible] = useState(false);
const [record, setRecord] = useState({});
return (
<div>
<Table columns={columns(setVisible, setRecord)} dataSource={data} />
<Drawer
title="Basic Drawer"
placement="right"
closable={false}
onClose={() => {
setVisible(false)
}}
visible={visible}
>
<p>{record.name}</p>
<Show age={record.age}/>
</Drawer>
</div>
);
}
export default App;
class Show extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
}
componentDidMount() {
this.getCount();
}
getCount = () => {
setTimeout(() => {
this.setState({
count: this.props.age+1
})
}, 1000)
}
render() {
return <div>{this.state.count}</div>
}
}
代码逻辑大概就是对于每一条数据,点击查看的时候,可以看到名称和一个依赖年龄参数算出来的一个数。我们假定这个数必须通过一个接口计算得到,上述用setTimeout模拟了一下。
问题关键点在于,这个请求是只有Show组件挂载才会触发,之后不会再更新。这意味着,第一次点击查看会发生数值改变,以后怎么打开都是之前的那个值。
解决也很容易,既然每次Drawer组件打开关闭都有状态变更,那Show组件也更新即可。
{visible&&<Show age={record.age}/>}
类似这种非预期更新的还有很多,比如value和onChange的变量名没对上,使用mobx但是没对目标组件进行观测...
解决这类问题的核心就是找到恰当的更新时机,更新一次,必要时用key强制更新。
文件写入不全
在实习期做的项目中有一个功能是在线实时保存,数据会以物理文件形式写入到磁盘,用户反馈页面展示效果不全才暴露出这个问题。
我当时的想法是网络波动导致只写入了一部分,后来发现并非如此,在那个时间段网络没毛病。
紧接着我想会不会是文件太大没有写完?看了一下其他写入的文件大小,最大也不超过10M。
这个量级node一会就写完了,显然也不是这个问题。
到最后,忘了怎么发现的了,是磁盘空间不够了...
好吧,默默记住这种可能的坑。
axios会删除get请求头中的Content-Type
为什么删除?说实话,我并不确定。大胆的猜一下,axios认为get请求是简单请求,不需要设置这东西?
这东西会影响什么?影响我对cors的理解了。
在某个项目中,同事鲸鱼问我一个跨域问题的配置,然后发现同样的配置get请求可以但post请求不行。当时怀疑是传了cors以外的自定义头,变成了非简单请求,比如Content-Type:application/json;
看代码发现确实没有添加什么头的配置,很迷。找了一圈发现是全局配置了头,woc...然后困惑我的地方就出现了,既然设置了这个头,get和post都必然是非简单请求,为什么一个能过一个不能过?
我灵光一闪,难道是axios请求本身的问题?之前撸过源码,知道浏览器端axios其实就是用的xhr。
于是我打开Chrome控制台,写下如下代码,回车,查看请求头是否携带了content-type。
var xhr=new XMLHttpRequest()
xhr.open('get','/')
xhr.setRequestHeader('content-type','application/json')
xhr.send(null)
果然,带上了...此时可以判定必然是axios做了手脚。
再去看axios源码,发现确实对content-type做了remove操作。
上边提到了cors,这里补一下:https://lengyuexin.github.io/gatsby/cors
数据类型一致很重要
近期在做某个需求的时候不小心踩了一个坑,js数据类型为字符串,但是数据库该字段设置成了int。这就导致小数位会被截断,怎么算都有偏差。
定位也经过了层层曲折,后来发现计算结果有小数但是存储都是整数,才意识到是类型问题。
涉及数据计算,类型一定要多加注意。
代码段优化
实现一种功能通常会有不止一种方法,能优化的最好优化一下。下面介绍几个我接触前端到现在常用的代码段优化手段,可当个参考。
如果某个配置想要获取外部参数,用函数
例如:
const columns = (setVisible, setRecord) => [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
},
{
title: 'Age',
dataIndex: 'age',
key: 'age',
},
{
title: 'Action',
key: 'action',
render: (text, record) => (
<Space size="middle">
<span onClick={() => {
setVisible(true)
setRecord(record)
}}>查看</span>
</Space>
),
},
];
大多数情况下更新和添加逻辑可复用
- 前端处理可共用组件,编辑时候注意数据回显即可
- 后端处理可共用接口
async function addOrUpdate(params) {
if (params.id) {
//更新
} else {
//添加
}
}
尽可能让常量有统一入口
集中管理可以让代码结构更清晰,也方便常量的管理和查找。
- 如果是全局共用常量,建议放在src/common/consts
- 如果是某个组件独有,建议放在src/components/xxx组件/consts
请求retry
retry指的是自定义失败重试次数的请求,在网络条件不好或者服务端波动情况下很有用。
async function retry(options = {}, delay, count = 3) {
try {
return await axios(options)
} catch (error) {
if (--count === 0) {
return console.error(error.message)
} else {
setTimeout(() => {
return retry(options, count)
}, delay)
}
}
}
批量处理应该是单接口的批量而不是请求的批量
这句话听起来可能有些绕,我举个批量添加用户的例子。
//假定已经获取到了要添加的用户列表list
// bad case 直接在前端循环发送请求
// http1.x每个浏览器同域名有并发限制,如果用户特别多,后续请求只能排队
list.forEach(user=>{
//假定save为插入接口
save(user)
})
//good case 前端只进行一次调用,循环插入在接口中完成
//client
save(list)
// server
async function save(list){
list.forEach(user=>{
//假定insert为插入操作
insert(user)
})
}
充分利用异步能力,不需要await的就不要await
现行版本的js中确实每一个await都要包裹在async中,以后会去掉这个限制。
此外,并不是每一个async都必须await,如果没有依赖返回值的计算,天然的异步是非常快的。
保证函数独立性,不要过度聚合
函数的核心是复用,太多业务杂糅在一起,会使得其可复用性下降。
不要太硬编码,换成map
这样做的好处是,如果存在多处引用,只需要修改map就好,不用每个都改
// bad case
if(flag === '成功'){
//do something
}
//good case
const map = {
success:'成功'
}
if(flag === map.success){
//do something
}
再会
事在人为,若有意改变,普通项目也会因你变得不那么普通。
关于如何在项目中学习,本期要聊的就是这些,希望对你有所帮助。
情如风雪无常,
却是一动既殇。