Saturday, July 23, 2022

SegmentFault 最新的文章

SegmentFault 最新的文章


使用React手写一个手风琴组件

Posted: 16 Jul 2022 08:35 AM PDT

知识点

  • emotion语法
  • react语法
  • css语法
  • typescript类型语法

效果

让我们来看一下我们实现的效果图:

结构分析

根据上图,我们来分析一下,一个手风琴组件应该包含一个手风琴容器组件和多个手风琴子元素组件。因此,假设我们实现好了所有的逻辑,并写出使用demo,那么代码应该如下:

<Accordion defaultIndex="1" onItemClick={console.log}>    <AccordionItem label="A" index="1">      Lorem ipsum    </AccordionItem>    <AccordionItem label="B" index="2">       Dolor sit amet    </AccordionItem> </Accordion>

根据以上的结构,我们可以得知,首先容器组件Accordion会暴露一个defaultIndex属性以及一个onItemClick事件。顾名思义,defaultIndex代表默认展开的子元素组件AccordionItem的索引,onItemClick代表点击每一个子元素组件所触发的事件。然后,我们可以看到子元素组件有label属性和index属性,很显然,label代表当前子元素的标题,index代表当前子元素组件的索引值,而我们的Lorem ipsum就是子元素的内容。根据这些分析,我们先来实现一下AccordionItem组件。

AccordionItem子组件

首先我们定义好子组件的结构,函数组件写法如下:

const AccordionItem = (props) => {    //返回元素 };

子元素组件分成三个部分,一个容器元素,一个标题元素和一个内容元素,因此我们可以将结构写成如下:

<div className="according-item-container">    <div className="according-item-header"></div>    <div className="according-item-content"></div> </div>

知道了结构之后,我们就知道props会有哪些属性,首先是索引index属性,它的类型为string 或者number,然后是判断内容是否展开的属性isCollapsed,它的类型是布尔值,其次我们还有渲染标题的属性label,它应该是一个react节点,类型为ReactNode,同理,还有一个内容属性即children,类型也应该是ReactNode,最后就是我们要暴露的事件方法handleClick,它的类型应该是一个方法,因此我们可以定义如下的接口:

interface AccordionItemType {   index: string | number;   label: string;   isCollapsed: boolean;   //SyntheticEvent代表react合成事件对象的类型   handleClick(e: SyntheticEvent): void;   children: ReactNode; }

接口定义好之后,接下来我们就在接口里面拿值(采用对象解构的方式),这些值都算是可选的,即:

const { label, isCollapsed, handleClick, children } = props;

此时我们的AccordionItem子组件应该是如下:

const AccordionItem = (props: Partial<AccordionItemType>) => {   const { label, isCollapsed, handleClick, children } = props;   return (     <div className={AccordionItemContainer} onClick={handleClick}>       <div className={AccordionItemHeader}>{label}</div>       <div         aria-expanded={isCollapsed}         className={`${AccordionItemContent}${           isCollapsed ? ' collapsed' : ' expanded'         }`}       >         {children}       </div>     </div>   ); };

这里我们可以使用emotion/css来写css类名样式,代码如下:

const baseStyle = css`   line-height: 1.5715; `; const AccordionItemContainer = css`   border-bottom: 1px solid #d9d9d9; `; const AccordionItemHeader = cx(   baseStyle,   css`     position: relative;     display: flex;     flex-wrap: nowrap;     align-items: flex-start;     padding: 12px 16px;     color: rgba(0, 0, 0, 0.85);     cursor: pointer;     transition: all 0.3s, visibility 0s;     box-sizing: border-box;   `, );   const AccordionItemContent = css`   color: #000000d9;   background-color: #fff;   border-top: 1px solid #d9d9d9;   transition: all 0.3s ease-in-out;   padding: 16px;   &.collapsed {     display: none;   }   &.expanded {     display: block;   } `;

以上的css后面跟模板字符串再跟css样式就是emotion/css语法,cx也就是组合样式写法,样式都是常规的写法,也没什么好说的。这里有一个难点,那就是display:none和display:block没有过渡效果,因此可以采用visibility:hidden和opacity:0的方式来替换,但是这里为了简单,没考虑动画效果,所以也就将问题放着,后面有时间再优化。

到目前为止,这个子组件就算是完成了,这也就意味着我们的手风琴组件已经完成一半了,接下来我们来看容器组件Accordion的写法。

Accordion容器组件

首先我们先把结构写好:

const Accordion = (props) => {   //后续代码 };

我们再来分析一下需要传给Accordion组件的属性有哪些,很显然有defaultIndex,onItemClick和children,因此我们可以定义如下的接口:

interface AccordionType {   defaultIndex: number | string;   onItemClick(key: number | string): void;   children: JSX.Element[]; }

注意这里的children不应该是ReactNode,而是JSX.Element元素数组,这是为什么呢,我们后面再来解释这个问题。现在我们知道了props的属性之后,我们可以拿到这些属性,代码如下:

const Accordion = (props:Partial<AccordionType>) => {   const { defaultIndex, onItemClick, children } = props;   //后续代码 };

现在我们再维护一个状态,用来代表当前显示的子元素组件的索引,使用useState hook函数,初始化默认值就应该是defaultIndex。如下:

const Accordion = (props:Partial<AccordionType>) => {   const { defaultIndex, onItemClick, children } = props;   //新增的代码   const [bindIndex, setBindIndex] = useState(defaultIndex);   //后续代码 };

接下来,我们编写好容器元素,并写好样式,如下所示:

const Accordion = (props: Partial<AccordionType>) => {   const { defaultIndex, onItemClick, children } = props;   const [bindIndex, setBindIndex] = useState(defaultIndex);   return (     <div className={AccordionContainer}></div>   ); };

容器元素的样式如下:

const baseStyle = css`   line-height: 1.5715; `; const AccordionContainer = cx(   baseStyle,   css`     box-sizing: border-box;     margin: 0;     padding: 0;     color: #000000d9;     font-size: 14px;     background-color: #fafafa;     border: 1px solid #d9d9d9;     border-bottom: 0;     border-radius: 2px;   `, );

好的,接下来,我们实际上容器元素的子元素应该是多个AccordionItem元素,也正因为如此,这里的children类型就是JSX.Element [],我们应该如何获取这些子元素呢?我们应该知道,每一个子元素对应的就是一个节点,在react中用的是链表来表示这些节点,每个节点对应的就有个type属性,我们只需要拿到容器元素的子组件元素中type属性为AccordionItem的元素数组,如下:

//name不是AccordionItem,代表子元素不是AccordionItem,不是的我们需要过滤掉 const items = children?.filter(     (item) => item?.type?.name === 'AccordionItem,代表子元素不是AccordionItem,所以我们需要过滤掉',  );

到了这里,我们就知道了,容器元素的子元素是一个数组,我们就需要遍历,使用map方法,如下:

items?.map(({ props: { index, label, children } }) => (   <AccordionItem      key={index}      label={label}      children={children}      isCollapsed={bindIndex !== index}      handleClick={() => changeItem(index)}   /> ))

请注意这一段代码:

handleClick={() => changeItem(index)}

这就是我们之前子组件绑定的事件,也是我们需要暴露出去的事件,在这个事件方法中,我们无非执行的就是更改当前被展开元素的索引。所以代码就很好写了:

const changeItem = (index: number | string) => {    //暴露点击事件方法接口    if (typeof onItemClick === 'function') {      onItemClick(index);    }    //设置索引    if (index !== bindIndex) {      setBindIndex(index);    } };

到了这里,我们的一个手风琴组件就完成了,完整代码如下:

import { cx, css } from '@emotion/css'; import React, { useState } from 'react'; import type { ReactNode, SyntheticEvent } from 'react';   const baseStyle = css`   line-height: 1.5715; `; const AccordionContainer = cx(   baseStyle,   css`     box-sizing: border-box;     margin: 0;     padding: 0;     color: #000000d9;     font-size: 14px;     background-color: #fafafa;     border: 1px solid #d9d9d9;     border-bottom: 0;     border-radius: 2px;   `, ); const AccordionItemContainer = css`   border-bottom: 1px solid #d9d9d9; `; const AccordionItemHeader = cx(   baseStyle,   css`     position: relative;     display: flex;     flex-wrap: nowrap;     align-items: flex-start;     padding: 12px 16px;     color: rgba(0, 0, 0, 0.85);     cursor: pointer;     transition: all 0.3s, visibility 0s;     box-sizing: border-box;   `, );   const AccordionItemContent = css`   color: #000000d9;   background-color: #fff;   border-top: 1px solid #d9d9d9;   transition: all 0.3s ease-in-out;   padding: 16px;   &.collapsed {     display: none;   }   &.expanded {     display: block;   } `;   interface AccordionItemType {   index: string | number;   label: string;   isCollapsed: boolean;   handleClick(e: SyntheticEvent): void;   children: ReactNode; } interface AccordionType {   defaultIndex: number | string;   onItemClick(key: number | string): void;   children: JSX.Element[]; }   const AccordionItem = (props: Partial<AccordionItemType>) => {   const { label, isCollapsed, handleClick, children } = props;   return (     <div className={AccordionItemContainer} onClick={handleClick}>       <div className={AccordionItemHeader}>{label}</div>       <div         aria-expanded={isCollapsed}         className={`${AccordionItemContent}${           isCollapsed ? ' collapsed' : ' expanded'         }`}       >         {children}       </div>     </div>   ); };   const Accordion = (props: Partial<AccordionType>) => {   const { defaultIndex, onItemClick, children } = props;   const [bindIndex, setBindIndex] = useState(defaultIndex);   const changeItem = (index: number | string) => {     if (typeof onItemClick === 'function') {       onItemClick(index);     }     if (index !== bindIndex) {       setBindIndex(index);     }   };   const items = children?.filter(     (item) => item?.type?.name === 'AccordionItem',   );   return (     <div className={AccordionContainer}>       {items?.map(({ props: { index, label, children } }) => (         <AccordionItem           key={index}           label={label}           children={children}           isCollapsed={bindIndex !== index}           handleClick={() => changeItem(index)}         />       ))}     </div>   ); };

让我们来看一下效果:

到此为止了,更多React组件的实现,可以访问react-code-segment

源码地址可以看这里源码地址。喜欢觉得不错能够帮助到您,希望能点个赞,您的赞就是我更新文章的最大动力。

一个简洁、强大、可扩展的前端项目架构是什么样的?

Posted: 20 Jul 2022 06:46 PM PDT

大家好,我卡颂。

React技术栈的一大优势在于 —— 社区繁荣,你业务中需要实现的功能基本都能找到对应的开源库。

但繁荣也有不好的一面 —— 要实现同样的功能,有太多选择,到底选哪个?

本文要介绍一个12.7k的开源项目 —— Bulletproof React

这个项目为构建简洁、强大、可扩展的前端项目架构的方方面面给出了建议。

欢迎加入人类高质量前端框架群,带飞

Bulletproof React是什么

Bulletproof React与我们常见的脚手架(比如CRA)不同,后者的作用是根据模版创建一个新项目

而前者包含一个完整的React全栈论坛项目:

用户登录页面

作者通过这个项目举例,展示了与项目架构相关的13个方面的内容,比如:

  • 文件目录该如何组织
  • 工程化配置有什么推荐
  • 写业务组件时该怎么规范
  • 怎么做状态管理
  • API层如何设计
  • 等等......

限于篇幅有限,本文介绍其中部分观点。

不知道这些观点你是否认同呢?

文件目录如何组织

项目推荐如下目录形式:

src | +-- assets            # 静态资源 | +-- components        # 公共组件 | +-- config            # 全局配置 | +-- features          # 特性 | +-- hooks             # 公用hooks | +-- lib               # 二次导出的第三方库 | +-- providers         # 应用中所有providers | +-- routes            # 路由配置 | +-- stores            # 全局状态stores | +-- test              # 测试工具、mock服务器 | +-- types             # 全局类型文件 | +-- utils             # 通用工具函数

其中,features目录与components目录的区别在于:

components存放全局公用的组件,而features存放业务相关特性

比如我要开发评论模块,评论作为一个特性,与他相关的所有内容都存在于features/comments目录下。

评论模块中需要输入框,输入框这个通用组件来自于components目录。

所有特性相关的内容都会收敛到features目录下,具体包括:

src/features/xxx-feature | +-- api         # 与特性相关的请求 | +-- assets      # 与特性相关的静态资源 | +-- components  # 与特性相关的组件 | +-- hooks       # 与特性相关的hooks | +-- routes      # 与特性相关的路由 | +-- stores      # 与特性相关的状态stores | +-- types       # 与特性相关的类型申明 | +-- utils       # 与特性相关的工具函数 | +-- index.ts    # 入口

特性导出的所有内容只能通过统一的入口调用,比如:

import { CommentBar } from "@/features/comments"

而不是:

import { CommentBar } from "@/features/comments/components/CommentBar

这可以通过配置ESLint实现:

{   rules: {     'no-restricted-imports': [       'error',       {         patterns: ['@/features/*/*'],       },     ],     // ...其他配置   } }

相比于将特性相关的内容都以扁平的形式存放在全局目录下(比如将特性的hooks存放在全局hooks目录),以features目录作为相关代码的集合能够有效防止项目体积增大后代码组织混乱的情况。

怎么做状态管理

项目中并不是所有状态都需要保存在中心化的store中,需要根据状态类型区别对待。

组件状态

对于组件的局部状态,如果只有组件自身以及他的子孙组件需要这部分状态,那么可以用useStateuseReducer保存他们。

应用状态

与应用交互相关的状态,比如打开弹窗通知改变黑夜模式等,应该遵循将状态尽可能靠近使用他的组件的原则,不要什么状态都定义为全局状态

Bulletproof React中的示例项目举例,首先定义通知相关的状态

// bulletproof-react/src/stores/notifications.ts export const useNotificationStore = create<NotificationsStore>((set) => ({   notifications: [],   addNotification: (notification) =>     set((state) => ({       notifications: [...state.notifications, { id: nanoid(), ...notification }],     })),   dismissNotification: (id) =>     set((state) => ({       notifications: state.notifications.filter((notification) => notification.id !== id),     })), }));

再在任何使用通知相关的状态的地方引用useNotificationStore,比如:

// bulletproof-react/src/components/Notifications/Notifications.tsx import { useNotificationStore } from '@/stores/notifications';  import { Notification } from './Notification';  export const Notifications = () => {   const { notifications, dismissNotification } = useNotificationStore();    return (     <div     >       {notifications.map((notification) => (         <Notification           key={notification.id}           notification={notification}           onDismiss={dismissNotification}         />       ))}     </div>   ); };

这里使用的状态管理工具是zustand,除此之外还有很多可选方案:

  • context + hooks
  • redux + redux toolkit
  • mobx
  • constate
  • jotai
  • recoil
  • xstate

这些方案各有特点,但他们都是为了处理应用状态

服务端缓存状态

对于从服务端请求而来,缓存在前端的数据,虽然可以用上述处理应用状态的工具解决,但服务端缓存状态相比于应用状态,还涉及到缓存失效序列化数据等问题。

所以最好用专门的工具处理,比如:

  • react-query - REST + GraphQL
  • swr - REST + GraphQL
  • apollo client - GraphQL
  • urql - GraphQl

表单状态

表单数据需要区分受控非受控,表单本身还有很多逻辑需要处理(比如表单校验),所以也推荐用专门的库处理这部分状态,比如:

  • React Hook Form
  • Formik
  • React Final Form

URL状态

URL状态包括:

  • url params (/app/${dynamicParam})
  • query params (/app?dynamicParam=1)

这部分状态通常是路由库处理,比如react-router-dom

总结

本文节选了部分Bulletproof React中推荐的方案,有没有让你认可的观点呢?

欢迎在评论区交流项目架构中的最佳实践。

Docker学习2-数据卷

Posted: 20 Jul 2022 07:05 AM PDT

什么是容器数据卷

docker概念
将应用和环境打包成一个镜像!
如果数据都在容器中,那么我们容器删除,数据就会丢失!
容器之间可以有一个数据共享的技术!Docker容器中产生的数据,同步到本地!这就需要用到数据卷,也就是将我们容器内的目录,挂载到Linux上面!

总结:容器的持久化和同步操作,容器间也可以数据共享的!

使用数据卷

直接命令挂载 -v

docker run -it -v 主机目录:容器内的目录  # 启动后可以使用下面的命令查看挂载有没有成功 docker inspect 容器id  # 可以在 Mounts 内查看 

2.测试
1.在本地增加或者修改文件查看容器内有没有改动
2.容器内增加或修改查看本机目录有没有同步改动

具名和匿名挂载

1.匿名:只写容器内的路劲给,不写容器外的路径

-v 容器内路径 docker run -d -p --name nginx01 -v /etc/nginx nginx  # 查看所有卷的情况 docker volume ls #发现 数据  #  

2.具名

通过-v 卷名:容器内路径 docker run -d -p --name nginx01 -v [卷名]:/etc/nginx nginx # 查看卷 docker volume inspect 卷名  # 一旦设置了容器权限 容器对我们挂在出来的容器就有限定了 ro readonly # 只读 rw readwrite # 可读写 docker run -d -p --name nginx01 -v [卷名]:/etc/nginx:ro nginx  docker run -d -p --name nginx01 -v [卷名]:/etc/nginx:rw nginx

Dockfile挂载

1.创建Dockfile文件

FROM nginx # 匿名挂载 VOLUME ["test1", "test2"] CMD echo "----end---" CMD /bin/bash

2.命令运行

# 构建镜像 docker build -f deockfile所在目录 -t shuai/nginx:1.0 .  # 启动容器 docker run    # 查看是否挂载成功

SegmentFault 思否技术周刊 -- 日常使用 Git,这些问题你遇到过吗?

Posted: 19 Jul 2022 08:40 PM PDT

Git 的诞生是一个非常有趣的故事。1991年 Linus 开源了 Linux 内核,无数 Linux 爱好者在世界各地为 Linux 编写代码,那么问题来了,这些代码该如何管理呢?起初 Linus 使用 BitKeeper(BitMover 公司的版本控制软件)管理 Linux 的核心开发,后来 BitMover 停止了对 Linux 的支持,于是 Linus 秉承自己的版本自己写的精神,花了两周时间自己用 C 写了一个分布式版本控制系统,这就是 Git。

文章推荐

Git 配置以及替换 gerrit 默认 commit-msg hook

Git commit信息作为一个基础的交互窗口,既可以快速确定提交影响、关联设计文档、关联缺陷bug单、后续还能对项目或团队工作进行溯源改进。

Git commit信息的规范化,既体现开发同学的专业素养,也属于公司的过程资产,因此对git commit提交的规范设计如下。

Git 常用命令大全

作用:Git的作用是协同多人对同一个项目进行开发,解决多人在同时间对相同文件开发的合并冲突问题,给每个文件的每次修改操作做日志标记,方便后期查找维护;源代码版本控制管理系统 是一个 时光穿梭机,可以查看到一个文件之前某年某月某日某时刻是什么样子的。

Git Commit 规范与配置

我在平时使用 Git 时,都是随意编写提交说明。发现问题时进行版本回退,看着杂乱无章的提交说明,根本搞不清哪一次提交了什么内容,深受其害。
之后在学习过程中,认识到 Commit 规范与配置,总结内容,分享出来。

Git 代码回滚操作(比如刚上线的代码出现问题时回滚之前版本)

代码回滚这个操作,在实际工作中用的不是太多(前提是规范使用 Git 进行多人协作开发)。一般都是出问题的时候,才会回滚到之前的代码。比如:刚发布的版本到生产环境服务器以后,出现了一个很奇怪的 bug,而在测试环境服务器,却没有这个 bug(开发和测试都一脸懵逼)。为了不影响用户的使用,所以得赶紧回滚到之前的代码版本。
本文记录一下具体回滚操作思路和步骤

三句话测试你是否懂 Git

同事和组长的一番对话引起了笔者对 Git 的思考
先介绍一下我司小工坊式的 Git 提交流程,本地打包,删除 dist 文件,重建 dist 文件,git add .,git commit -m 'XX',git push origin 分支名
和传统公司的 git 提交不同,我司打包是本地打包,而且是把 dist 文件直接上传到仓库

Git 创建子分支,合并主分支以及其他分支并提交到远程仓库

在项目中很多小伙伴都会遇到不想在当前分支开发,因为会有很多的问题发生,如果我新建一个测试的分支 ,是不是效率就提高了,把测试没有问题的代码合并到主分支上,是不是就解决了?版本的修复,不同功能的测试修改,都可以用的到下面我把我的经验分享一下。

Git 日常装逼手册

作为一名开发人员,不管走到哪里可能都会和代码仓库发生不可描述的关系,从clone仓库 => 新的功能分支 => 开发功能完成后提交 => 分支合并(解决冲突) => 打上标签 => 部署到线上环境,这一系列操作都离不开这个代码版本管理工具Git,所以常见命令烂熟于心有助于我们提升效率。

45 个 Git 经典操作场景,专治不会合代码

Git 对于大家应该都不太陌生,熟练使用 Git 已经成为程序员的一项基本技能,尽管在工作中有诸如 Sourcetree 这样牛X的客户端工具,使得合并代码变的很方便。但找工作面试和一些需彰显个人实力的场景,仍然需要我们掌握足够多的 Git 命令。

下边我们整理了 45 个日常用 Git 合代码的经典操作场景,基本覆盖了工作中的需求。

IDEA 中如何完成 Git 版本回退?

Git 中的撤销操作,我们可以归纳为四类:
工作区的代码想撤销
add 到暂存区的代码想撤销
提交到本地仓库的代码想撤销
远程仓库的代码想要撤销

Git 最佳实践,什么才是最佳工作流?

今天我们不聊基本用法,聊一聊 Git 到底应该怎么用?我们知道相比于 Svn,Git 最牛的地方在于它的分支,分支很灵活,但是如果缺乏一个使用套路,又会用的乱糟糟的,特别是在团队协作中,该怎么玩 Git 分支?

咱们也不发明什么轮子,也不设计什么全新流程,本文主要是和大家介绍三种常见的工作流:Git Flow、GitHub Flow 以及 GitLab Flow。介绍完成后,在谈谈松哥的一些使用体验。

问答推荐


PS:大家想看哪些方面的技术内容,可以在评论区留言喔 ~
如有问题可以添加小姐姐微信~
image.png

No comments:

Post a Comment