git clone [email protected]:MeanYou/zhihu.git
cd zhihu
npm install
npm start
|-- zhihu
|-- build 项目打包目录
|-- config 来自eject,cra的webpack配置目录
|-- public
|-- index.html 模板文件,%PUBLIC_URL%可以在打包之后访问public目录下的文件,可以放非npm包的js库
|-- scripts
|-- build.js 执行构建的脚本
|-- start.js 执行开发的脚本
|-- test.js 执行测试的脚本
|-- src 源码
|-- .gitignore git忽略配置文件
|-- package.json npm包管理文件
|-- tsconfig.json ts配置文件
- 使用redux + react redux(hooks) + redux thunk(暂时) 管理全局应用状态;
- 页面级的状态在页面所属文件夹建立store.ts管理;
- 轻量简单的组件内部状态在组件内部使用useState管理,重量级的组件仍然使用页面的状态管理方式。
无意义的优化是万恶之源,我在这篇文章里看到的应该何时使用useCallback和useMemo,我们判断一个函数是否需要缓存,要看它是否对性能造成了影响,一般情况下我们不用考虑太多优化,但是原则上为了避免不必要的重渲染,我们在DOM以及子组件的callback函数中默认使用useCallback(虽然这在简单组件中并没有提升性能,而且会增加代码量)。
曾经听到防抖和节流,不以为意,以为只会在字面上的含义情况下使用,防止用户手抖多点一次提交就完了,直到onScroll需要和redux联合使用的时候才发现页面滚动卡顿需要防抖和节流来优化。 首先是防抖的基本示例,效果为500ms以内被触发的函数不会执行,直到500ms后没有操作才执行:
// 简单的防抖动函数
function debounce(func, wait) {
// 定时器变量
var timeout;
return function() {
// 每次触发 scroll handler 时先清除定时器
clearTimeout(timeout);
// 指定 xx ms 后触发真正想进行的操作 handler
timeout = setTimeout(func, wait);
};
};
// 实际想绑定在 scroll 事件上的 handler
function realFunc(){
console.log("Success");
}
// 采用了防抖动
window.addEventListener('scroll',debounce(realFunc,500));
// 没采用防抖动
window.addEventListener('scroll',realFunc);
好点的封装
// 防抖动函数
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
var myEfficientFn = debounce(function() {
// 滚动中的真正的操作
}, 250);
// 绑定监听
window.addEventListener('resize', myEfficientFn);
但是防抖会有一些问题,比如我一直滚动的时候,希望滚动过程中动态加载出懒加载的图片和请求新的数据,这种情况下我们需要以一定的频率触发函数,这种技巧叫做节流,只允许函数在一定ms内执行一次,与防抖相比,节流函数保证在x毫秒内至少执行一次时间handler,其实就是多了一个mustRun属性,看下示例:
// 简单的节流函数
function throttle(func, wait, mustRun) {
var timeout,
startTime = new Date();
return function() {
var context = this,
args = arguments,
curTime = new Date();
clearTimeout(timeout);
// 如果达到了规定的触发时间间隔,触发 handler
if(curTime - startTime >= mustRun){
func.apply(context,args);
startTime = curTime;
// 没达到触发间隔,重新设定定时器
}else{
timeout = setTimeout(func, wait);
}
};
};
// 实际想绑定在 scroll 事件上的 handler
function realFunc(){
console.log("Success");
}
// 采用了节流函数
window.addEventListener('scroll',throttle(realFunc,500,1000));
给div绑定onScroll并dispatch的方式对性能过于不友好,决定通过使用window.addEventListener绑定删除事件,当然,防抖还在。
这篇文章业务逻辑代码应该放在哪里?讲得很好,这应该是一个困扰很多人的问题。
- 业务逻辑放在组件中,组件过于臃肿,且不利于逻辑复用;
- 业务逻辑全部放在action中,利用thunk,优点是保持了reducer的纯净,缺点是action过于臃肿;
- 业务逻辑全部放在reducer中,优缺点与action类似,保持了action的纯净,导致reducer臃肿;
- 文章中作者提供了一些更为复杂的解决方案,比如saga等,暂时先不考虑;
- 个人想到一个方案,将页面级组件的action分为状态action和业务action,这样语义化很好,定义一个action,状态action只需更新一个状态,复杂或异步的action一般只在业务代码中使用,可以使用thunk进行管理,分离了业务,同时保证了action和reducer的纯净,缺点也很明显,这样可能会导致action和reducer同时过于臃肿,一个action本来只需dispatch一次就可以变更多个状态,这样操作需要多次diapatch才能触发一个action;
- 在第5种方案的基础上,状态action如果有业务需求,可以定义一个一次更新多个状态的action,在没有性能问题的前提下,暂时先使用这种方案,我在/src/hooks中定义了useThunkReducer暂时替代redux-thunk中间件供非redux全局store使用,后期找到更好的方案的话会取而代之。
- 登录后用户如果需要长期保存登录状态,把用户相关的脱敏信息长期存放在localStorage或cookie中是一种比较合适的方案,其中localStorage适合前后端分离项目,一般采用类似JWT的认证方式,cookie适合服务端渲染的项目,使用cookie认证;
- 无需持久化的登录信息同样需要保存在localStorage或cookie中,没有过期信息的cookie会在浏览器关闭之后被清理掉,这点localStorage无法做到,但是一般情况下服务器为了安全考虑,不想让用户用document.cookie获取到,会设置http-only,本项目中我们暂时不考虑这种情况,默认用户可以获取到cookie;
- sessionStorage的生命周期只有一个标签页,不考虑使用;
- 综上考虑,cookie可以说是存储用户信息和登录状态的最优解,缺点是每次请求可能会把服务器无关的cookie发送过去,浪费流量并影响请求速度,这种情况下可以考虑使用localStoage存储无需发送到服务器的用户信息。(每个技术的出现都会有自身适用的场景,没有绝对的最优解,只有相对更适用的场景)
- 在本项目中,Route被简单分为Login和Main,对应着public和private,因此在Login页面登录成功后设置cookie,经过一番尝试,发现cookie中的token应当作为登录状态的唯一来源,不能存储到redux或任何变量中,一方面防止用户刷新页面组件变量变化,另一方面防止用户在使用过程中删除cookie,导致依赖cookie的变量变化。
知乎是个很大的网站,业务繁多,涉及方面也众多,首页有推荐、关注、热榜等功能,发现模块有最新专题、圆桌讨论、热门收藏夹、专栏等板块,等你来答中又分为人气问题,潜力好问,新问题、人人答等板块,这还只是大菜单上的模块,加上搜索、问题、回答等等模块,个人很难完全覆盖所有业务,因此想确定一些简单实用的业务,作为学习交流使用。
- 推荐
- 热榜
- 暂无
useReducer在容器组件中使用,在组件目录下新建store.ts文件,文件模板如下:
/**
* store
*/
export interface State {
variable: string;
}
export const initialState: State = {
variable: ''
};
/**
* action
*/
// action constant
const CHANGE_VARIABLE = 'CHANGE_VARIABLE';
// action type
export type Action = {
type: 'CHANGE_VARIABLE',
payload: string
}
// 状态action
export const changeVariable = (variable: string) => ({
type: CHANGE_VARIABLE,
payload: variable
})
// 业务action
export const getVariableByApi = (param:string) => async (dispatch:any, getState:any) => {
// 也可以返回Promise
const res = await xhr.get('/api', {params: {param}});
dispatch(changeVariable(res.data));
}
/**
* reducer
*/
export const reducer = (state: State, action: Action) => {
switch (action.type) {
case CHANGE_VARIABLE:
return {
...state,
variable: action.payload
};
default:
return state;
}
}