前端规范方案

关于本文

作者:不要秃头啊

来自:https://juejin.cn/post/7085257325165936648

前言

不以规矩,不能成方圆。

本人有幸经历了团队从缺乏标准到逐渐规范的一个过程,在此当做记录供大家参考。

本文从为什么需要规范以及建立规范的重要性出发,逐步引申出如何去建立适合自己团队的规范,详细讨论了前端具体涉及到的规范都有哪些,部分小节有具体的配置步骤和操作链接。

文中涉及到的所有配置均放在github上的 Demo[1] 中,觉得不错的点个赞 ❤️❤️❤️。

下面我们开始吧🏃。

为什么需要规范

规范能给我们带来什么好处,如果没有规范会造成什么后果?这里主要拿代码规范来说。

统一代码规范的好处:

  1. 提高代码整体的可读性、可维护性、可复用性、可移植性和可靠性,这会从根本上降低开发成本,也是最重要的一点。

  2. 保证代码的一致性:软件系统中最重要的因素之一就是编码的一致性。如果编码风格一致,也更加易于维护,因为团队内任何人都可以快速理解并修改。

  3. 提升团队整体效率:开发人员通常需要花费大量的时间来解决代码质量问题,如果都按照规范编写,也有助于团队尽早发现问题,甚至完全预防问题,这将提高整个交付过程的效率。

  4. 减少code review期间一系列的争议,因为缺乏标准,在争议过程中双方很难妥协(没少因为这事争论过😕)。

若不统一代码规范,可能会造成的后果:

  1. 由于缺乏规范,导致代码风格不一,增加团队成员间的心理负担,极端情况下,某段代码只有某个人能修改(俗称屎山💩)。

  2. 团队间协作更加困难:因为开发人员得适应不同的风格,会导致效率低下(阅读代码是我们花费时间最多的地方)。

  3. 在code review期间可能经常为类似的事情做过多的讨论。

  4. 影响团队的生产力和质量,严重的甚至会影响团队和谐。

为什么依然有很多团队缺乏规范

在这件事上,很难达成一致是我认为最重要的原因。并且,仅仅只是拥有规范也是不够的:

  • 当开发人员被要求在短时间内完成任务时,通常会回避质量标准。

  • 团队中总是有一些有个性的人不会为了团队去改变自己的习惯。

  • 有些人在会议上就约定达成了一致,在会下依旧我行我素。

  • ...

如何保持规范

我曾想通过会议讨论的方式来制定规范,但效果却差强人意。将失败的原因总结为大致几点:

  1. 在会议中,思维很容易发散,经常出现的情况是讨论了很多,却很难有实际性的成效,在开发中依然有不少人选择无视规则

  2. 正式的会议往往很难组织,大家很难一起有空闲的时间来讨论,一次/两周 都很困难。

  3. 会议中对实际案例分析,提出若干点优化建议后,没有对问题的优先级和侧重点进行划分,导致实际效果并不好。

  4. 还有一点也是我自己的原因,组织会议的能力有待提升...😭

经历了上述的挫败之后,经过反复复盘总结,决定换一种方式来执行:

  1. 对规范问题进行归纳分析并通过文档记录(wiki等),寻找业内最佳解决方案,在团队内进行统一。

  2. 采用小步快跑的方式,有问题就解决问题,按照优先级和重要性进行排序划分,依次将问题纳入迭代,每个迭代重点解决其中几个即好。

  3. 本迭代的规范问题绝不留到下个迭代,防止堆积(当然,有时候还是得向项目经理妥协😮‍💨)。

  4. 在code review过程中严格把关,拒绝睁一只眼👀闭一只眼🧐。

  5. 当团队成员对具体某个规范有争议时,及时讨论并定出结论。

  6. 没有规则只是为了规则,制定规范的目的并不是一定要按照某某标准来执行,更多的是团队成员达成一致即可。

  7. 鼓励大家大胆的质疑规则,若不能提高代码的可读性,可维护性,可复用性,可移植性和可靠性的规则都应该被受到质疑。

  8. 以身作则,船头的方向不能偏航。

经过两个月的迭代后,发现效果出奇的好👏,大家的规范意识普遍增强,当遇到规范问题时也不再畏畏缩缩,而是大胆的抛出在群里讨论。

开发者需要建立和遵守的规范

大致可以划分成这几个方向:

  • 开发流程规范

  • 代码规范

  • git commit规范

  • 项目文件结构规范

  • UI设计规范

1. 开发流程规范

这里可能有小伙伴有疑问了,开发流程规范不是项目经理定的吗🤔️,跟我有什么关系?

这里想告诉大家的是,开发流程在一定程度上应该是由我们自己来掌控。不管是传统开发的模式还是敏捷开发的模式,对于开发者来说核心依旧是高质高效的完成用户提出的需求

笔者曾见过不少开发者在拿到产品经理的需求后就开始急匆匆的写代码,以此来体现他们的高效,但往往却因为需求理解不到位和前期代码欠缺设计导致bug率高和返工。

如何找到适合自己的开发流程是需要依靠经验来支撑的,需要反复总结和思考,最终达到高质高效完成的目的。

说一说笔者自己比较喜欢的开发流程:

ecd9918998f6a2d43dfd3b84f70c04c0.png image.png

在接收到需求后应第一时间去了解这个需求的背景是什么?这么做到底有没有解决用户的痛点?或者说用户更深层次的需求是什么?如果团队的产品经理经验不丰富,往往可以在这个阶段砍掉很多不合理的需求(这一点真的很重要)

对于复杂大功能往往还需要进行技术方案调研技术方案设计,并输出详细的设计文档。涉及到细节上,则需要将数据流走向、组件设计等通过脑图的形式呈现出来。

2. 代码规范之格式化规范

由于每个开发者的IDE不同,即使IDE相同也会因为每个人的配置不一样导致格式化的结果不一样。如何确保团队内开发人员采用统一的格式化配置呢?

这里给推荐大家使用 prettier[2],它内置了一套格式化的规则,具体配置:

1). 安装依赖:

 
  1. npm install --save-dev --save-exact prettier 

  2. // or

  3. yarn add --dev --exact prettier

  4. 复制代码

2). 创建一个空配置文件,让编辑器和其他工具知道你正在使用 Prettier:

 
  1. echo {}> .prettierrc.json

  2. 复制代码

3). 创建一个.prettierignore[3]文件,让 Prettier CLI 和编辑器知道哪些文件不能格式化,example:

 
  1. # Ignore artifacts:

  2. dist

  3. build

  4. coverage

  5. 复制代码

4). 配置编辑器(VScode为例)

IDE中安装 Prettier-Code Formater[4] 插件:

ab5486a6d70ce68641ff11e454caa032.png image.png

找到IDE中设置模块,搜索 format On Save,勾上这个就可以了。

0ca56175dab80741c486a64395f731a1.png image.png

现在当我们 Ctrl + S 保存代码时,插件就会帮助我们自动格式化了。

这里有小伙伴要问了,要是有人将没有格式化的代码提交上去怎么办?

这时候就需要在 git commit 的阶段自动将提交的代码进行格式化,这里我们借助工具 husky[5],它主要可以帮助我们在 git 阶段检查提交消息、运行测试、检查代码等。没接触过的小伙伴可以去官网[6]了解一下,配置如下:

  • 安装 husky[7] 和 lint-staged[8]:

 
  1. npm install --save-dev husky lint-staged

  2. npx husky install

  3. npm set-script prepare "husky install"

  4. npx husky add .husky/pre-commit "npx lint-staged"

  5. // or

  6. yarn add --dev husky lint-staged

  7. npx husky install

  8. npm set-script prepare "husky install"

  9. npx husky add .husky/pre-commit "npx lint-staged"

  10. 复制代码

  • 然后将以下内容添加到package.json中:

 
  1. {

  2.   "lint-staged": {

  3.     "**/*": "prettier --write --ignore-unknown"

  4.   }

  5. }

  6. 复制代码

这段配置的意思是:当执行 git commit 阶段前,先执行lint-stagedlint-staged中的内容就是对暂存区的文件执行格式化的命令。

其他:若使用的是脚手架工具搭建的项目,会自带eslint配置(eslintConfig)。prettier 和 eslint 会有一些配置上的冲突,这个时候需要安装eslint-config-prettier[9] 以使 ESLint 和 Prettier 相互配合,安装完后在.eslintrc中配置(以Create-React-App为例):

 
  1. "eslintConfig": {

  2.     "extends": [

  3.       "react-app",

  4.       "react-app/jest",

  5.       "prettier"

  6.     ]

  7.   },

  8. 复制代码

这样就可以用"prettier"的部分规则覆盖前面的规则,让它们都能正常工作。

更多详情见:Prettier[10]。

3. 代码规范之JS/TS规范

JS/TS主流的大致有这几种:

  • Airbnb JavaScript Style Guide[11]

  • Google JavaScript Style Guide[12]

  • Idiomatic JavaScript Style Guide[13]

  • JavaScript Standard Style Guide[14]

  • jQuery JavaScript Style Guide[15]

比较推荐使用 Airbnb JavaScript Style Guide,它在 Github 上足有 12万 star,几乎覆盖了 JavaScript 的每一项特性。

具体配置:

1). 安装依赖

 
  1. npm install eslint --save-dev

  2. // or

  3. yarn add eslint --dev

  4. 复制代码

2). 生成配置文件

 
  1. npm init @eslint/config

  2. // or

  3. yarn create @eslint/config

  4. 复制代码

跟着终端中的提示,按照自身需求一步步选择即可。

有了具体的规范后,我们同样需要使用工具去约束:还是通过在git commit阶段校验,若不通过则取消提交。

配置(还是在 package.json 中的 lint-staged ):

 
  1. "lint-staged": {

  2.     "**/*": "prettier --write --ignore-unknown", //格式化

  3.     "src/*": "eslint --ext .js,.ts,.tsx"  //进行eslint校验

  4.   },

  5. 复制代码

注意:这里如果选用的Typescript,则会默认使用@typescript-eslint/parser解析器,官方为了追求更快的解析速度,并不会对.ts文件中的类型进行检查,只会做语法检测。

如果需要对类型也进行检测,需要在extends中加上plugin:\@typescript-eslint/recommended-requiring-type-checking[16]。

但是在笔者的使用中发现效果并不好,一些基本的类型依然检测不出来,索性这里换了另一种方式:在pre commit 中执行yarn run tsc,这里的意思是对项目中ts文件进行类型检测,默认会读取根目录中的tsconfig.json配置。

这种方式并不完美,它的弊端就在于全量检测,如果项目不大还好,若是项目代码量够多,检测10-20s也是常有的事。

更多详情查看eslint官网:eslint.org/docs/user-g…[17]

4. 代码规范之CSS规范

CSS检查代码规范使用 stylelint 插件,规范则推荐使用 stylelint-config-standard[18]:

1). 安装

 
  1. npm install --save-dev stylelint stylelint-config-standard

  2. 复制代码

2). 在项目的根目录中创建一个配置文件.stylelintrc.json,内容如下:

 
  1. {

  2.   "extends": "stylelint-config-standard"

  3. }

  4. 复制代码

3). 解决与prettier配置的冲突:

 
  1. npm install --save-dev stylelint-config-prettier

  2. 复制代码

4). 将下面配置复制到.stylelintrc.json中:

 
  1. {

  2.   "extends": ["stylelint-config-standard", "stylelint-config-prettier"]

  3. }

  4. 复制代码

5). 在 git commitv阶段进行检测:

 
  1. "lint-staged": {

  2.     "**/*": "prettier --write --ignore-unknown", //格式化

  3.     "src/**.{js,jsx,ts,tsx}": "eslint --ext .js,.jsx,.ts,.tsx", //对js文件检测

  4.     "**/*.{less,css}": "stylelint --fix" //对css文件进行检测

  5.   },

  6. 复制代码

5. 代码规范之自定义其他规范

下面列一些团队内定的其他规范:

(1)命名规范

变量的命名中应尽量减少缩写的情况发生,做到见名知意。

 
  1. // 👎 自我感觉良好的缩写:

  2. let rContent = 'willen'; 

  3. // 👍 无需对每个变量都写注释,从名字上就看懂

  4. let firstName = 'jackie'; 

  5. // 👎 从命名无法知道返回值类型

  6. function showFriendsList() {....} // // 无法辨别函数意图,返回的是一个数组,还是一个对象,还是true or false?

  7. // 👍 明确函数意图,对于返回true or false的函数,最好以should/is/can/has开头

  8. function shouldShowFriendsList() {...}

  9. function isEmpty() {...}

  10. function canCreateDocuments() {...}

  11. function hasLicense() {...}

  12. function sendEmailToUser(user) {.... } //动词开头,函数意图就很明显

  13. 复制代码

(2)写注释

在每个文件的顶部明确说明该组件做什么,有没有业务理解难点等等,对业务特殊函数/变量也需要写注释

 
  1. /**

  2.  * 导航页面-右边区域

  3.  */

  4.  

  5. const Content=>()=>xxx

  6.  

  7. const MAX_INPUT_LENGTH = 8; //用于限制密码输入框

  8. function Component(props) {

  9.   return (

  10.     <>

  11.       {/* 如果用户没有订阅则不展示广告 */}

  12.       {user.subscribed ? null : <SubscriptionPlans />}

  13.     </>

  14.   )

  15. }

  16.  

  17. 复制代码

(3)变量兜底

 
  1. // 👎 对于求值获取的变量,没有兜底

  2. const { data } = getApiRequest();

  3. data.map((s) => s.id); //没有考虑data异常的情况,代码一跑就爆炸

  4. // 👍 对于求值变量,做好兜底

  5. const { data = [] } = getApiRequest();

  6. data.map((s) => s?.id); //没有考虑data异常的情况,代码一跑就爆炸

  7. 复制代码

(4)辅助函数必须是纯函数

 
  1. // 👎 不要让功能函数的输出变化无常

  2. function plusAbc(a, b, c) {  // 这个函数的输出将变化无常,因为api返回的值一旦改变,同样输入函数的a,b,c的值,但函数返回的结果却不一定相同。

  3.   var c = fetch('../api');

  4.   return a+b+c;

  5. }

  6. // 👍 功能函数使用纯函数,输入一致,输出结果永远唯一

  7. function plusAbc(a, b, c) {  // 同样输入函数的a,b,c的值,但函数返回的结果永远相同。

  8.   return a+b+c;

  9. }

  10. 复制代码

(5)优先使用函数式编程

 
  1. // 👎 使用for循环编程

  2. for(i = 1; i <= 10; i++) {

  3.    a[i] = a[i] +1;

  4. }

  5. // 👍 使用函数式编程

  6. let b = a.map(item => ++item)

  7. 复制代码

(6)优先使用函数式组件

除非需要用到错误边界,否则函数式组件应该是首选方法。

(7)组件复杂度

如果一个组件做的事情太多,应适当提取一些逻辑,将其拆分为更小的组件。

如果提取的组件很复杂,则需要依照一定的规则和条件一一提取它。

代码行数并不是一个客观的衡量标准,更多是需要考虑责任划分和抽象。

(8)用错误边界

当需要对大量数据进行渲染处理时,需要通过错误边界组件对其进行降级处理。

更多详情见之前写的专题文章:浅析前端异常及降级处理[19] 。

 
  1. function Component() {

  2.   return (

  3.     <Layout>

  4.       <ErrorBoundary>

  5.         <CardWidget />

  6.       </ErrorBoundary>

  7.       <ErrorBoundary>

  8.         <FiltersWidget />

  9.       </ErrorBoundary>

  10.       <div>

  11.         <ErrorBoundary>

  12.           <ProductList />

  13.         </ErrorBoundary>

  14.       </div>

  15.     </Layout>

  16.   )

  17. }

  18. 复制代码

(9)props参数传递

props一层层传递一直是我们很头疼的一个问题,最核心的问题是不清楚props是从哪个初始组件传来的,以及props中到底有哪些东西,上下文是什么?

因此对于传递较深的场景我推荐直接使用 context,对于 props 中的内容和上下文通过 TS 来解决。

 
  1. // A.tsx

  2. interface AProps {

  3.   param: string;

  4. }

  5. const A = ({ param }: AProps) => {

  6.   return <B param = {param} />;

  7. };

  8. // 👍 上下文清晰

  9. // B.tsx

  10. const B = ({ param }: { param: AProps['param'] }) => {

  11.   return <div>hello world</div>;

  12. };

  13. 复制代码

(10)props传参数量

如果超过 5 个props,就该考虑是否拆分该组件。在某些情况下,这是需要对组件进行重构的标志。

注意:组件使用的props越多,重新渲染的理由就越多。

(11)避免嵌套三元运算符

三元运算符在第一级之后变得难以阅读,虽然看起来节省了代码空间,但最好在代码中明确意图,保持良好的阅读性。

 
  1. // 👎 不够清晰,要是再嵌套一层两层呢

  2. isSubscribed ? (

  3.   <ArticleRecommendations />

  4. ) : isRegistered ? (

  5.   <SubscribeCallToAction />

  6. ) : (

  7.   <RegisterCallToAction />

  8. )

  9. // 👍 将判断逻辑进行拆分

  10. function CallToActionWidget({ subscribed, registered }) {

  11.   if (subscribed) {

  12.     return <ArticleRecommendations />

  13.   }

  14.   if (registered) {

  15.     return <SubscribeCallToAction />

  16.   }

  17.   return <RegisterCallToAction />

  18. }

  19. function Component() {

  20.   return (

  21.     <CallToActionWidget

  22.       subscribed={subscribed}

  23.       registered={registered}

  24.     />

  25.   )

  26. }

  27. 复制代码

(12)将列表组件封装成独立组件

 
  1. // 👎 列表渲染和其他逻辑杂糅在一起

  2. function Component({ topic, page, articles, onNextPage }) {

  3.   return (

  4.     <div>

  5.       <h1>{topic}</h1>

  6.       {articles.map(article => (

  7.         <div>

  8.           <h3>{article.title}</h3>

  9.           <p>{article.teaser}</p>

  10.           <img src={article.image} />

  11.         </div>

  12.       ))}

  13.       <div>You are on page {page}</div>

  14.       <button onClick={onNextPage}>Next</button>

  15.     </div>

  16.   )

  17. }

  18. // 👍 将列表组件提取出来,一目了然

  19. function Component({ topic, page, articles, onNextPage }) {

  20.   return (

  21.     <div>

  22.       <h1>{topic}</h1>

  23.       <ArticlesList articles={articles} />

  24.       <div>You are on page {page}</div>

  25.       <button onClick={onNextPage}>Next</button>

  26.     </div>

  27.   )

  28. }

  29. 复制代码

(13)避免嵌套渲染函数

 
  1. // 👎 不要将其定义在渲染函数组件中

  2. function Component() {

  3.   function renderHeader() {

  4.     return <header>...</header>

  5.   }

  6.   return <div>{renderHeader()}</div>

  7. }

  8. // 👍 将其抽离到独立的组件中去

  9. import Header from '@modules/common/components/Header'

  10. function Component() {

  11.   return (

  12.     <div>

  13.       <Header />

  14.     </div>

  15.   )

  16. }

  17. 复制代码

(14)组件/函数导入导出

 
  1. // 👍 在文件头部导入,顺序依次为: 第三方库 > 公共组件/方法 > 非公共部分组件/方法

  2. import React from 'react'

  3. import _ from 'loadsh'

  4. import Header from '@components/header'

  5. import Content from './Content'

  6. // 👍 在底部导出

  7. export { Content, Header }

  8. export default Component

  9. 复制代码

...

6. 项目文件结构规范

不得不说,在这件事上我们团队曾经花费了大量的经历去思考和实践,也是信了React官方的鬼话:不要花超过五分钟在选择项目文件组织结构上[20]。

在项目初期若不重视,到了后期就是到处天马行空,你很难在期望的目录下找到你想要的文件。

文件夹名称全部采用小写 + "-" 来隔开,index.ts更多是用来做导出作用,要不然最后编辑器中满屏的index.tsx,很难区分。

 
  1. - src 开发目录

  2.       - pages 视图

  3.           - module-a 模块A

  4.             - components 私有组件

  5.               - ComA.tsx

  6.               - ComB.tsx

  7.             - index.module.less

  8.             - index.tsx

  9.             - Content.tsx

  10.           - module-b 模块B

  11.       - components 公共组件

  12.         - index.ts 导出所有组件

  13.         - header

  14.           - index.tsx

  15.           - index.module.less

  16.           - User.tsx

  17.           - useGetBaseInfo.hooks.ts

  18.       - routers 路由文件

  19.       - store redux中的数据

  20.       - utils 这里是以utils为后缀

  21.         - index.ts

  22.         - a.utils.ts

  23.         - b.utils.ts

  24.       - hooks 这里是以hooks为后缀

  25.         - index.ts

  26.         - a.hooks.ts

  27.         - b.hooks.ts

  28.       - styles 静态资源文件

  29.       - service api请求,这里是以api为后缀

  30.         - a.api.ts 按照后端微服务进行划分

  31.         - b.api.ts

  32.       - constans 常量

  33. 复制代码

通过对工具函数、hooks、api 等加上后缀,更加容易区分引入的文件。

7. Git commit规范

git commit 规范主要可以帮助开发人员在 code review 期间更容易理解提交的内容,现在大部分主流 commit 规范都是基于Angular 团队的规范[21]而衍生出来的,它的 message 格式如下:

 
  1. <type>(<scope>): <subject>

  2. <BLANK LINE>

  3. <body>

  4. <BLANK LINE>

  5. <footer>

  6. 复制代码

每个提交消息都包含一个subject、一个body和一个footer (中间使用空行分割),提交信息的任何一行不能超过 100 个字符。

👉 type主要有以下几种类型:

  • feat: 一个新特性

  • fix: 修复bug

  • docs: 文档修改

  • style: 不影响代码含义的更改(空格、格式、缺少分号等)

  • refactor: 代码重构

  • perf: 优化性能

  • test: 测试用例修改

  • chore: 对构建过程或辅助工具和库的更改,例如文档生成

👉 scope:可以是影响范围的任何内容。

👉 subject:包含对更改的简洁描述,规则:

  • 使用陈述语句

  • 第一个字母不要大写

  • 末尾没有点 (.)

👉 body:commit 具体修改内容, 可以分为多行,应该包括改变的动机,并与以前的行为进行对比。

👉 footer: 一些备注, 通常是修复的 bug 的链接。

截取一张开源库的 commit,example[22]:

7fa1e8bb8d12dca1e87a06f03efc9306.png image.png

有了规范后,我们需要通过工具去约束:commitlint[23]。它要做的就是在我们每次提交 git commit 的时候,都会帮我们检查 commit message 是否符合一定的规范,如果不符合,就让这次提交失败。

具体配置:

 
  1. # 安装 commitlint cli 和 conventional config

  2. npm install --save-dev @commitlint/{config-conventional,cli}

  3. # Windows:

  4. npm install --save-dev @commitlint/config-conventional @commitlint/cli

  5. 配置要使用的 commitlint 规则

  6. echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js

  7. 加入到husky中:

  8. npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'

  9. or

  10. yarn husky add .husky/commit-msg 'yarn commitlint --edit $1'

  11. 复制代码

更多详情见官网:github.com/conventiona…[24]

8. UI设计规范

优秀的开发者应该反向推动UI的规范,并且能够支撑UI规范的落地。

UI 规范的最大好处就是能够提质提效:

  • 在开发者的角度,与设计规范同步形成研发资产,避免重复造轮子;

  • 在测试的角度,能够避免重复的无意义走查;

  • 在UI设计师的角度,减少设计成本,提高设计效率,可以快速承接新需求;

  • 站在产品角度,提高产品迭代与优化效率,降低试错成本;

  • 站在用户角度,解决用户体验一致性。

那到底应该怎么去推动UI规范?我的做法是先让设计师去给出一系列的规范,没有相关规范就拉上产品经理一起制定规范。然后前端建立一套自己的组件库,再将组件库提供给UI设计师,以此来相互监督是否达成了规范协议。

具体参考 Antd设计规范[25]。

总结

统一规范的最根本目的是为了保证团队成员的一致性,从而减少沟通成本,提高开发效率。

学会热爱标准,但要确保它们不是一成不变的。如果制定的规范不适合您的团队,请确保可以适应和重写所需要的任何规则。它并不是要强制执行一种工作方式,而是为了帮助促进团队之间的互动 👏👏👏。

最后

欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿

回复「算法」,加入前端编程源码算法群,每日一道面试题(工作日),第二天瓶子君都会很认真的解答哟!

回复「交流」,吹吹水、聊聊技术、吐吐槽!

回复「阅读」,每日刷刷高质量好文!

如果这篇文章对你有帮助,「在看」是最大的支持

 》》面试官也在看的算法资料《《

“在看和转发”就是最大的支持

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值