Coder Social home page Coder Social logo

blog's Introduction

blog's People

Contributors

zxyue25 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

blog's Issues

原创:如何在前端团队快速制定并落地代码规范


theme: juejin

本篇文章讲怎么在前端团队快速制定并落地代码规范!!!
干货,拿走这个仓库

image.png

一、背景

9月份换了一个新部门,部门成立时间不长,当时组内还没有统一的代码规范(部分工程用了规范,部分没有,没有统一的收口)

小组的技术栈框架有VueReactTaroNuxt,用Typescript,算是比较杂了,结合到部门后续还可能扩展其他技术栈,我们从0-1实现了一套通用的代码规范

到现在小组内也用起来几个月了,整个过程还算是比较顺利且快速,最近得空分享出来~

⚠️本篇文章不会讲基础的具体的规范,而是从实践经验讲怎么制定规范以及落地规范

image.png

二、为什么要代码规范

就不说了...大家懂的~
image.png

不是很了解的话,指路

三、确定规范范围

首先,跟主管同步,团队需要一个统一的规范,相信主管也在等着人来做起来

第一步收集团队的技术栈情况,确定规范要包括的范围

把规范梳理为三部分ESLintStyleLintCommitLint,结合团队实际情况分析如下

  • ESLint:团队统一用的TypeScript,框架用到了VueReactTaro、还有Nuxt
  • StyleLint:团队统一用的Less
  • CommitLint:git代码提交规范
    image.png
    当然,还需考虑团队后续可能会扩展到的技术栈,以保证实现的时候确保可扩展性

四、调研业内实现方案

常见以下3种方案

  1. 团队制定文档式代码规范,成员都人为遵守这份规范来编写代码

    靠人来保证代码规范存在不可靠,且需要人为review代码不规范,效率低

  2. 直接使用业内已有成熟规范,比如css使用StyleLint官方推荐规范stylelint-config-standard、stylelint-order,JavaScript使用ESLint推荐规范eslint:recommended等

    a) 开源规范往往不能满足团队需求,可拓展性差; b) 业内提供的规范都是独立的(stylelint只提供css代码规范,ESLint只提供JavaScript规范),是零散的,对于规范初始化或升级存在成本高、不可靠问题(每个工程需要做人为操作多个步骤)

  3. 基于StyleLint、ESLint制定团队规范npm包,使用团队制定规范库

    a) 该方案解决可扩展性差的问题,但是第二点中的(b)问题依旧存在

五、我们的技术方案

整体技术思路图如下图,提供三个基础包@jd/stylelint-config-selling@jd/eslint-config-selling@jd/commitlint-config-selling分别满足StyleLintESLintCommitLint

  1. @jd/stylelint-config-selling包括css、less、sass(团队暂未使用到)
  2. @jd/eslint-config-selling包括Vue、React、Taro、Next、nuxt(团队暂未使用到)...,还包括后续可能会扩展到需要自定义的ESLint插件或者解析器
  3. @jd/commitlint-config-selling统一使用git

向上提供一个简单的命令行工具,交互式初始化init、或者更新update规范

image.png

几个关键点

1、用lerna统一管理包

lerna是一个管理工具,用于管理包含多个软件包(package)的 JavaScript项目,业内已经广泛使用了,不了解的可以自己找资料看下
项目结构如下图
image.png

2、三个基础包的依赖包都设置为生产依赖dependencies

如下图,包@jd/eslint-config-selling的依赖包都写在了生产依赖,而不是开发依赖
image.png
解释下:
开发依赖&生产依赖

  • 开发依赖:业务工程用的时候不会下载开发依赖中的包,业内常见的规范如standardairbnb都是写在开发依赖
    • 缺点:业务工程除了安装@jd/eslint-config-selling外,需要自己去安装前置依赖包,如eslint、根据自己选择的框架安装相关前置依赖包如使用的Vue需要安装eslint-plugin-vue...使用成本、维护升级成本较高
    • 优点:按需安装包,开发时不会安装多余的包(Lint相关的包在业务工程中都是开发依赖,所以只会影响开发时)
  • 生产依赖:业务工程用的时候会下载这些包
    • 优点:安装@jd/eslint-config-selling后,无需关注前置依赖包
    • 缺点:开发时会下载@jd/eslint-config-selling中所有写在生产依赖的包,即使有些用不到,比如你使用的是React,却安装了eslint-plugin-vue

3、提供简单的命令行

这个比较简单,提供交互式命令,支持一键初始化或者升级3种规范,就不展开说了

不会的,指路中高级前端必备:如何设计并实现一个脚手架

组里现在还没有项目模版脚手架,后续有的话需要把规范这部分融进去

六、最重要的一点

什么是一个好的规范?
基本每个团队的规范都是不一样的,团队各成员都认同并愿意遵守的规范就是一个好的规范

所以确定好技术方案后,涉及到的各个规范,下图,我们在小组内分工去制定,比如几个人去制定styleLint的,几个人制定Vue的...

然后拉会评审,大家统一通过的规范才敲定
image.png
最后以开源的方式维护升级,使用过程中,遇到规范不合适的问题,提交issue,大家统一讨论确定是否需要更改规范

写在结尾

以上就是我们团队在前端规范落地方面的经验~

如果大家感兴趣,可查看github仓库

好文阅读:《堂主:如何推动前端团队的基础设施建设》

一、前言

本文根据 2020.02.29 日,第 2 届 “前端早早聊” 的“前端基建”专场分享整理而来。本文的标题是《如何推动前端团队基础设施建设》,一是契合大会所有分享都以 “如何” 为切入的要求,同时也是对最近一年,我所负责的团队在前端技术基础设施建设方面如何从 0 到 1 的一次沉淀总结。

另外还是非常感谢@scott,感谢活动的组织者和参与者,感谢这一期的话题。业界关于前端系统性基建建设的分享输出并不多,希望本次这些个人角度沉淀的文字,能为一些同学带来一些启动,产生一些改变。

广义情况下,技术架构、技术建设等是研发团队基础设施建设的一个真子集,除了这些,团队的基建还包括了诸如制度、流程、文化、梯队、培训等其他方面。在本次分享中,我们是面向狭义的“技术基础设施建设”进行,此外的偏软能力的方面,可参考@堂主 在前端早早聊第 1 届大会上的分享《如何影响与推动前端团队的成长》。

二、介绍

堂主,本名马翀,2006 年开始捣鼓前端,大学期间转过系、休过学、失败过创业。毕业前的 2011 年,在淘宝前端团队实习了整一年,12 年毕业后即加入淘宝(花名@堂主); 2016 年加入蘑菇街(蘑菇街时期花名@明淳),在蘑菇街做了 2 年的前端 TL;2018 年 8 月 ~ 至今,负责政采云的前端团队工作(花名又改回了@堂主)。

政采云前端团队目前有 50 多人,平均年龄不到 28 岁,妥妥的青年军。团队名字是 ZooTeam,团队站点也是 https://zoo.team。Z 是政采云拼音首字母,oo 是无穷的符号(♾),结合 Zoo有生物圈的含义,希望后续政采云的前端团队,不论是人才梯队,还是技术体系,都能各面兼备,逐渐成长为一个生态。

下面是我的微信二维码,有想进一步交流的同学,欢迎扫描加我微信。

三、如何理解“技术基建”

“技术基建”,就是研发团队的技术基础设施建设,是一个团队通用的技术能力沉淀。本次分享的绝大部分内容都会围绕这个中心进行,但在这之前,让我们先来看看一些同学的困惑(同样的内容我在上个月第一期早早聊大会的分享中也提到过):

上面三个问题都很有典型性,且不是臆造的,都来源于脉脉的匿名社区。能看到这里涉及到对 “做业务” 和 “做架构” 的认知不清,也有对能力评级的疑问 —— 到底要掌握多少知识技能才能获得更高的评级。对于后一个评级疑问,堂主稍早前写过一篇长文《面试官角度看应聘:问题到底出在哪?》来阐述个人观点,分为了两篇,可点击链接查看。而对于 “业务” 和 “架构”(或者今天我说的,基建)的区别和理解,我想说的是:

技术的价值在于解决业务问题,“业务支撑” 和 “基础建设” 从来都是同一件事的两个面,这个 “同一件事”,就是帮助业务解决问题。任何脱离解决实际场景而发起的基建,都需要重新审视甚至不应被鼓励。

基础建设的发起从业务问题中来,其意义不仅是能帮助业务解决问题。承担建设的虚拟团队,在建设过程中能为同学提供不同维度的锻炼场景,在业务问题与场景的识别、方案设计、新技术实践、项目管理和产品化思维方面都能提供实践成长的空间,起到练兵的作用。同时一个虚拟建设小组本质上也是一个团队,过程中能对不同角色进行锻炼和考察,这有助于团队梯队的完善。建设结果对于业务的促进,更容易获得内部合作方的认可;沉淀下来的好的经验,可以对外输出分享,也是对影响力的有力帮助。

四、基建搞什么

对于一个研发团队,如果一直都是靠压榨、纯加班这种出蛮力的方式在支持业务,这个团队会非常危险,业务也会危险。这种模式下,业务是无法实现跨越式增长的 —— 你总不能指望业务量增长 10 倍的情况下研发团队规模也扩充 10 倍,成本会失控。有时候阶段性的忙和加班是不可避免的,比如电商的双 11 大促,或者 toB 业务定制的大项目的交付,时间点都是倒排,守时履约对结果的影响非常重。加班是应该的,不加班也是应该的,只有完不成工作是不应该的。当这一阵过去后,团队一定要思考,怎么做能更高效。站在未来看今天,如果一年、两年后,业务量增长 N 倍,那时候该如何支持,现在的方式是否能满足?不可能靠堆人,只能靠技术建设去提效降成本,这就是基建最核心的价值:帮助业务更好的活在未来

那基建该搞什么?首先我们要说,基建的内容和业务阶段、团队既有建设沉淀是分不开的。越是偏初创期的团队,其建设,往往越偏向于基础的技术收益,如脚手架、组件库、打包部署工具等;越是成熟的业务和成熟沉淀的团队,其建设会越偏向于获取更多的业务收益,如直接服务于业务的系统,技术提效的同时更能直接带来业务收益。

业界大部分的研发团队,都不是阿里、腾讯、头条这样基础完备沉淀丰富的情况,起步期和快速爬坡期居多,建设滞后。体现在基建上,可能往往只有一个基于 Webpack 搞搞的脚手架,和一个第三方开源的 UI 组件库上封装下自己的业务组件库,除此之外无他。如果看官现在恰好是我说的这种情况,不用焦虑,1 年半前我刚来政采云,当时这里的前端也是一样的情况。后续的一年多时间到现在,我们初步建设并落地了一系列的基础设施,取得了蛮好的反馈。回顾当初,确定建设的策略及步骤,主要是从拆解研发流程入手的:

如上图所示,一个基本的研发流程闭环,一般是需求导入 - 需求拆解 - 技术方案制定 - 本地编码 - 联调 - 自测优化 - 提测修复 Bug - 打包 - 部署 - 数据收集&分析复盘 - 迭代优化 —— 即新一轮的需求导入。

在这个基础的闭环中,每一个节点都有其进一步的内部环节,每一个环节相连,组成了一个研发周期。这个周期顺,研发流程就顺。这个周期中每一个环节的阻塞点越少,研发效率就越高。最初期的基建,就是从这些耽误研发时间的阻塞点入手,按照普遍性 + 高频的优先级标准,挨个突破。

提效、体验、稳定性,是基建要解决的最重要的目标,通用的公式是 标准化 + 规范化 + 工具化 + 自动化,能力完备后可以进一步提升到平台化 + 产品化。在方向方面,我们团队是从下面的 8 个主要方向进行归类和建设,供大家参考:

  • 开发规范:这一部分沉淀的是团队的标准化共识,标准化是团队有效协作的必备前提。
  • 研发流程:标准化流程直接影响上下游的协作分工和效率,优秀的流程能带来更专业的协作。
  • 基础资产:在我们团队,资产体系包括了工具链、团队标准 DSL、物料库(组件、区块、模板等)。
  • 工程管理:面向应用全生命周期的低成本管控,从应用的创建到本地环境配置到低代码搭建到打包部署。
  • 性能体验:自动化工具化的方式发现页面性能瓶颈,提供优化建议。
  • 安全防控:三方包依赖安全、代码合规性检查、安全风险检测等防控机制。
  • 统计监控:埋点方案、数据采集、数据分析、线上异常监控等。
  • 质量保障:自测 CheckList、单测、UI 自动化测试、链路自动化测试等。

如上是一般性前端基建的主要方向和分区,不论是 PC 端还是移动端,这些都是基础的建设点。业务阶段、团队能力的差异,体现在基建上,在于产出的完整性、颗粒度、深入度和自动化的覆盖范围。

五、基建怎么搞

下面,会针对一些大家都感兴趣的方向,结合我们团队过去一年的建设产出,为大家列举一些前端基建类产品的案例,以供参考。

1. 规范&文档(Docs)

规范是最应该先行的,始皇帝初统六国即“书同文车同轨”,规范意味着标准,是团队的共识,是沟通协作的基础。而文档,是最容易被忽略的事情之一,除了明面上重要的技术文档、业务稳定之外,还包括了行间的有效注释。想想,有多少时间是花在琢磨别人的代码逻辑,或刚接手某个业务得问多少人才能搞明白你面前那几个仓库是怎么回事,又有多少故障是因为不清楚前任留下的坑在哪里不小心踩雷。

对于规范的制定,需要强调的一点,是规范的产出应是团队内大部分同学的共识,应该是集体审美。规范一旦确定就应该严格执行,要能形成团队行为的一致性。对于文档,为了写而写的文档是垃圾,不如不写。文档的重点在说人话,在于有效性,在于直观、省事、不饶。想想一个 UI 组件库的文档,先给你看可交互的 Demo 再提供 API 信息,和直接开头就罗列一大堆的 API 文字介绍,哪种对阅读者的感受更好、心理成本更低?

2. 本地工程化环境(CLI)

本地开发环境,相信是任何一个团队都会做的标配,省事的可能直接拥抱框架选型对应的全家桶,如 Vue 全家桶,或者用 Webpack 撸一个脚手架。能力多一些的会再为脚手架提供一些插件服务,如 Lint 或者 Mock。从简单的一个本地脚手架,到复杂的一个工程化套件系统,其目的都是为了本地开发流程的去人肉化、自动化。

我们团队的本地开发环境基建,是一个工程化套件环境,核心理念就是尽量 “一步搞定所有事”,把本地环境的配置和使用尽量变的傻瓜化无脑化。比如本地初始化一个应用的环境,从 CLI 命令行的操作出发的话(实际上政采云前端团队现在已完全 GUI 化),一个 zoo init 命令就能搞定全部的本地环境搭建,这个全部是指在终端执行回车后,从仓库本地目录的生成到 npm 依赖的自动化安装到脚手架插件的初始化再到唤起一个浏览器窗口,都是自动化执行的。是的,连 npm installdev 什么的都不用执行,能省一步操作就省一步,楚王好细腰,少就是性感。下图是 CLI 本地工程套件的架构图:

3. 可视化工程系统(GUI)

其实目前团队的日常研发,已经基本上脱离了 CLI 操作,统一到了团队自研的桌面客户端 “敦煌” 平台。基于客户端的能力,能将分散的工程能力进行聚合,并形成链路的串联能力,结合 GUI 的直观和简便操作,进一步的省事。通过桌面客户端,可以将日常的前端研发链路上的操作都聚合进来,从组件开发到模板开发再到应用开发;从唤起编辑器到启动调试环境、进行包更新到打包部署发布。同时桌面端系统还能和其他的研发系统进行打通,形成更多的能力。

4. 组件开发与管理

一般情况下,前端团队都会完善自己的组件库体系,有些情况下一些 UI 组件库可能采用社区开源的优秀三方库,如 antd,但多多少少还会有自己的业务组件库需要封装。工具的价值在于抹平差异,将基础标准一致化。对于组件开发,前面所述的 CLI 工具链是这里的底层依赖,同理还有后面介绍的模板开发与使用,以及应用的开发。通过工具进行组件的开发和管理,可以较好的实现诸如组件命名标准化、版本标准化、查找便利性、开发流程简化等,还能实现组件的应用场景统计和版本覆盖率等涉及到组件在接入场景更新成本相关的必要统计。

5. 模板开发与管理

同飞冰类似,我们也沉淀了一套类似的模板化能力,便于中后台业务场景的快速开发。因为中后台的业务场景相对固化,诸如表单、列表等居多,基于模板的方式可以省掉很大一部分制作 Demo 和实现交互的成本。模板的前提是 UI 组件库的完备,和标准的中后台交互、视觉设计,基于此沉淀标准化的业务模板库,根据场景选择合适的模板,配置下页面信息和路径后,就可以一键安装到本地并自动化配置好路由,安装好依赖。

6. 项目创建与管理

项目的创建与管理,从一开始我们的目标就是 “去耦合,单人可全流程 Hold” —— 意思是在项目的创建、本地环境的搭建(也包括了环境的升级)、分支管理、构建、部署等环节,前端同学可以完全一人搞定。不需要因为权限的问题找人帮忙建仓库;不需要因为组件、区块、模板、应用(SPA/MPA)、选型(React/Vue)导致本地开发环境的标准不一致进而每次都得学新的;不需要头疼不同业务的版本流程不一致导致还得问这问那;不需要还得人肉的去配置打包脚本;不需要每次部署都得找人(或者是运维)帮忙... 总之,我们希望借助工具抹平日常中太多的不对称,将开发者的专注力重新尽量拉回简单纯粹的编码中。即使是一个对 Git、命令行、应用管理流程不太明白的校园新人,在桌面端可视化工程的系统辅助下,也能很愉快的开始编码。

7. 前端基础资产

前面我们提到了前端团队的规范(标准化)、工具链(CLI)、基于工具链之上的可视化辅助客户端(GUI),提到了组件(模块)、模板、应用。对工具的抽象和业务的可复用抽象,是一个团队的基础资产。简化到 Webpack 撸一个脚手架 + 一套开源三方 UI 组件库,剩下的拼装式生产全靠人肉;复杂些诸如阿里系正在突破的 UI2Code 、编辑器等能力,将标准、流程更自动化,进一步的去人肉。基础资产这部分,我们团队目前业务阶段下的建设分层如下:

8. CI/CD 自动化构建部署

前端具备自己的构建部署系统,便于专业化方面更好的流程控制。政采云前端团队在 2019 年下半年建设了自己的构建部署系统,实现了云打包、云检测和自动化部署(打通对接运维的部署系统)。新的独立系统在设计之初,重点就是希望能实现一种 Flow 的流式机制,以便实现代码的合规性静态检测能力。这部分在系统中最终实现了一套插件化机制,可以按需配置不同的检测项,如某检测项检测不通过,最终会阻塞发布流程,这些检测项有诸如:

  • *Lint 检测
  • 兼容性 API 检测
  • HTTPS 检测
  • 包检测(黑名单、包版本)
  • 合法性检测(域、链接)
  • 404 检测
  • 基础的 UI 检测(如是否缺少吊顶)
  • ...

9. 可视化搭建系统

可视化搭建系统是进一步高效利用组件的上层建筑。页面是由组件(业务模块)组成,搭建系统将组件的拼装由本地人肉操作,产品化到系统中可视化拼图,将页面搭建、数据配置、构建部署、数据埋点等等产品化,赋能给产品、运营等协作方。前端产出组件,运营搭建页面,既能节省前端的人效,也能让运营能力前置拓展,在营销场景中进一步释放运营的业务能力,实现共赢。

关乎可视化搭建系统的更多,可以查看我们团队之前输出的这篇文章:《前端工程实践之可视化搭建系统

系统架构图:

部署流程图:

10. 数据埋点与分析

在很多公司,数据埋点与分析往往是 BI 部门的事情。在政采云,因为公司前期 BI 能力相对不足,前端团队首先发起并推动了面向业务的 Web 数据埋点收集和数据分析、可视化相关的全系统建设。前后实现了埋点规范、埋点 SDK、数据收集及分析、PV/UV、链路分析、转化分析、用户画像、可视化热图、坑位粒度数据透出等数据化能力。

更多数据埋点与分析相关,可以查看我们团队之前输出的这篇文章:《前端工程实践之数据埋点分析系统

11. 页面性能自动化分析

页面性能,90% 在前端。尤其是像我们公司现阶段 toB 为主的业务,不同于我的老东家(淘宝、蘑菇街)早已移动端占绝对主导,我们依然是 PC 场景占大头,在页面性能方面问题还是比较突出。过去 1 年时间内,给大家可参考的路径是,我们首先发起了图体积优化,针对占据页面体积大头、请求数大头的图片首先发起优化策略,采用规范+工具的方式帮助业务快速实现图体积的优化,相关沉淀可见早先团队的这篇《为你重新系统梳理下, Web 体验优化中和图有关的那些事》。后来,我们逐步基于 Node 能力将梳理出的影响页面性能的点,实现了自动化检测能力,并依据不同的业务场景区分设计检测模型,再后来做了定时任务来实现性能的连贯性分析及数据走势能力,再之后又增加了业务性能数据大盘和每周的红黑榜。关于页面性能自动化分析系统的更多细节,可阅读我们团队早前的文章 《自动化 Web 性能分析之 Puppeteer 爬虫实践》、《自动化 Web 性能优化分析方案》。

12. 2019 基建里程碑

上面介绍的一部分政采云前端团队的技术基础建设,基本上都是在 2019 年一年内逐步建设落地并取得结果的。下图是在上一年周期中的建设里程碑,能看出对应的建设周期和节奏。在我这个团队,没有单独设立独立的前端架构组,前端下的团队都是业务团队,我们同学从业务支撑中沉淀问题,针对问题进行思考和聚敛,从业务问题出发针对性的推进对应的建设。

对于研发同学来说,身价取决于解决问题的能力,取决于面对不同的业务问题是否具备解决问题的方案。我们团队很庆幸的点在于,业务处于快速发展期,问题很多,既有的沉淀很少,我们很幸运的可以在帮业务解决问题、跟随业务快速发展的过程中,几乎是从零开始做这些建设。这是很可贵、很难得的一段经历,因为大部分的公司要么是体量还没到需要做这些的地步,要么是早就做完了轮不到,无法全程看到整个体系的发展。公司要为员工创造环境,但员工的成长最终是靠自己。所以同学们都认可用业余的时间参与建设、甚至是主导某个方向,对自身的成长是个宝贵的机会。

六、基建之外

1. 凡是建设,必有数据

凡是建设,必须要有对应的数据收集和分析,数据说明基建带来的改变,说明投入产出比。数据指标的设计,需要在某专项建设的前期即设计好并进行采集,并在整个推动周期和落地后持续收集,这样可以得到一个相对完整的变化曲线,用以作证工作的成效。数据不见得一定是完全精准的,但数据一定要能说明趋势直观化,反馈准确

2. 从场景出发找方案

对于人力方面,任何情况下人力都是缺失的。但很多时候我们的建设推不下去,往往不是因为人力的问题,而是没想清楚。《庄子·列寇传》有一则寓言,“朱评漫学屠龙于支离益,单千金之家,三年技成而无所用其巧”。 讲的是一个人散尽家资学习屠龙之技,学成却发现世界上本没有龙。对于研发同学,同样会存在从方案出发找场景的问题,如想学习 Node 不知道如何学习,照着书中的例子学,最后发现都忘了效果很不好。没有一个作家是看小说看成的,也没有一个语言学家是看字典看成的,同理技术专家也不会是通过看技术书籍养成的。在实践中学习,从来都是最快的方式。有价值的事从来都是从业务本身的问题出发。问题就是机会,问题就是长萝卜的坑。

3. 不设限,拓展能力边界

前端在研发体系中话语权偏低的现状,从前端这个职能出现那一刻就存在了。不排除个别研发团队,因其业务模式的原因,对前端的依赖较深,前端的话语权相对偏高。绝大部分的研发团队中,前端的工作,在其他研发眼中,往往是 “技术含量低”、“很薄的一层” 等情况。这个现状的背后,看看下图就知道了:

横、纵,2 个维度。右边的 “纵”,参考网络应用系统的分层体系,前端的传统工作范畴,都是集中在 “用户界面层”,很少能往下深入,深入到网关 ~ 基础设施层。后端则不同。从这个角度看,前端确实很 “薄”。现在良性的一面是,Node 能力为前端提供了向下渗透的服务端能力。一些团队也基于 Node 横向扩展自身的工程化能力,和向业务纵深去拓展前端的系统化能力。

我们再看左边的 “横”。只有很少的前端团队,能较完善的去建设和发展技术体系。对于有了较完善体系的前端团队而言,其技术体系也更多是局限于前端自身的职能范畴,没能较好的互动渗透到业务侧,更多是在自嗨,业务的感知力是很弱的。将技术带来的工程收益,转变为业务收益;将部门内的技术影响,转变为业务影响;将技术场景,升级到业务场景;将团队的基础能力,变为业务能力。跳出前端,围绕并深入业务,这是每一个正在推动团队体系建设的同学要更多想想的事。

4. 业务的阶段匹配性

基建的内容,是和业务阶段相匹配的。不同团队服务的业务阶段不同,基建的内容和广深度也会不同。高下之分不在于多寡,而在于对业务的理解和支持程度。如果你只需要一根针,千万不要去磨铁棒

5. 技术的价值,在于解决业务问题

技术的价值,在于解决业务问题;人的身价,在于解决问题的能力。但解决问题,技术基建绝不是银弹,甚至在我来看,都不是排在前三位的。

七、最后

最后,是一个需要大家都思考下的问题:

【原创】多端组件库Tard 2.0来了!


highlight: a11y-dark

本篇文章基于业务中搭建多端组件库的经验撰写

image

一、背景

1.1 背景

  • 团队业务:C端商城,需要支持小程序、H5
  • 团队技术栈:Taro+React多端
    打怪过程如下:
    image.png
    step1: 部门刚成立的时候,业务跑的很快,从0-1建设多端商城,给我们搞技术的时间几乎没有,所以一上来,我们选用了taro-ui组件库,使用下来我们发现,好用的组件库很少,且问题很多,官方也停止更新了,最后一个版本在20年4月份

step2: 为了满足业务,我们开始直接在业务中直接写组件,暴露的问题自然更明显:组件难复用、难扩展

step3: 所以在21年10月,当我们又新增了两块多端商城的业务时,可预见一个组件库来帮助团队提高开发效率是很有必要的,所以我们趁着做业务的同时自建组件库,并与21年12月发布了第一版(同事当时写的文章)

step4: 时隔一个季度,我们在技术实现了一些突破,比如用 hook 代替 class,用 css变量 全量替换 less变量,新增ConfigProvider全局配置组件,支持自定义样式前缀动态切换主题...(在第三部分组件库设计与实现会详细介绍)

1.2 业内Taro-React组件库分析

在开始动手之前,我们搜索了全网Taro-React的组件库 (缺少搭建组件库的经验只能多看别人的!!!)

image.png

二、组件库介绍

Alt

定位:一套基于 Taro 框架开发的多端 React UI 组件库

Tard 取名自 Taro React Design 简写,发音特的

先贴几张组件库效果图

  1. 组件库官网
    image.png

2.2 组件示例

image.png

2.3 组件分类

目前31个组件(都是我们业务中用到的)+ ConfigProvider全局配置组件,分为6类,具体可见下图
image.png

组件库更多可见官网(url:https://tard-ui.selling.cn/)

三、组件库设计与实现

image.png
作为一个组件库,最基本需要包括三个部分

  • 组件:npm包,业务真正使用的
  • demo:组件库h5、小程序demo;组件的使用效果
  • 文档:组件库官网和文档,告诉别人怎么用

3.1 组件库规范

开始动手写代码之前,一定要制定一套规范,除非一个人写组件,不然多个人最后的组件库写出来可能五花八门;

开发过程中我们也经常因为意见不合,争吵过很多次,讨论后都达成一致,这是一个不容易的过程;所以尽可能的提前制定规范,不然中途来改成本很大,毕竟谁也不想反复修改代码

3.1.1 代码管理方式

1、monorepo

packages包括以下三块

  • ui:组件库
  • sites:组件库官网和文档
  • demo:包包括h5、小程序demo
    image.png
    2、ui(组件库)代码组织

我们研究了其他各个组件库怎么组织组件的代码,一个组件包括的文件有组件的类型ts文件样式文件demo使用文档;最终决定将一个组件涉及的代码集中在一个文件夹下,一定程度上避免重复切换文件夹,提高开发效率

image.png

3.1.2 组件设计规范

1、组件编码

统一使用Hook+TypeScript

2、组件命名

大驼峰,多组件以.的形式命名,对外一个组件只暴露一个组件,如下

  • 按钮:Button
  • 下拉菜单:DropdownMenu,DropdownMenu.Item
  • 对话框:Modal,Modal.Header,Modal.Content,Modal.Action

3、组件API

  • 每个组件最外层需要包一个CompContainer组件,来实现组件有一些通用的参数,如下
    • className
    • style
  • 不同组件相同类型参数命名统一,比如
    • 元素的文字内容:text
    • 是否展示遮照:overlay
    • ...
      4、组件样式
      less+BEM+css变量
  • 使用统一前缀(默认tard),效果如下
    • css:@--css-prefix: tard
    • js:cssPrefix(){return 'tard'}
  • 遵循BEM规范
  • 样式变量:分为以下两种,组件变量依赖基础变量,提供更友好的方式修改组件样式,而非传统的强制改样式
  • 多端兼容
    • :root, page{...}

css变量h5挂在 :root 下可生效,小程序需挂在 page 下生效

image.png

3.2 自定义样式前缀

可以调用 ConfigProvider.config 配置方法设置组件的class前缀名,默认是 tard,比如下面我们设置为 custom

// 入口js文件
import { ConfigProvider } from 'tard'
ConfigProvider.config({
    cssPrefix: 'custom'
});

同时需要配置样式文件前缀名,在入口样式文件按如下配置

@--css-prefix: 'custom'

注意:ConfigProvider.config 配置的 cssPrefix 一定要与样式配置的 @--css-prefix 一致,否则class名与style不对应,样式失效

3.3 定制主题

主题定制作为组件库最基本的功能必不可少,分为两类

  • 编译时定制主题;这类最常见,基本业内组件库都支持,一般通过自定义样式变量覆盖,或者结合打包工具比如webpack配置loader处理
  • 运行时定制主题:一般是前端提供集中模式供用户切换或者通过接口拿主题数据;前者业内常用的方案有前端内置几套样式变量去切换,而后者一般是装修场景下的需求,我们业务涉及到这块,因此我们提供了 ConfigProvider实现运行时切换,具体可见3.3.2

3.3.1 自定义CSS变量

Button 组件为例,官网文档可以查看组件的样式,可以看到 Button 类名上存在以下变量

:root, page {
  --button-height: 76px;
  --button-default-v-padding: 40px;
  --button-min-width: 192px;
  --button-min-width-mini: 120px;
  --button-height-mini: 32px;
  --button-mini-text-size: 24px;
  --button-mini-v-padding: 6px;
  --button-min-width-small: 144px;
  --button-height-small: 56px;
  --button-small-text-size: var(--font-size-base);
  --button-small-v-padding: 24px;
  --button-min-large-width: 360px;
  --button-large-height: 96px;
  --button-large-text-size: var(--font-size-lg);
  --button-large-v-padding: 48px;
  --button-radius: var(--border-radius-md);
}

(1) CSS 覆盖

你可以直接在代码中覆盖这些 CSS 变量,Button 组件的样式会随之发生改变:

:root, page {
    --button-radius: 10px;
    --color-primary: 'green'
}

这些变量的默认值被定义在 root 节点上,HTML 文档的任何节点都可以访问到这些变量

这也是编译时定制主题常用的解决方案

(2)通过 ConfigProvider 覆盖

ConfigProvider 组件提供了覆盖 CSS 变量的能力,你需要在根节点包裹一个 ConfigProvider 组件,并通过 style 属性来配置一些主题变量

这使得可以在运行时动态的更改主题

 // 比如 button-radius 会转换成 `--button-radius`
<ConfigProvider style={{ 'button-radius': '0px', 'color-primary': 'green' }}>
  <Button className="button-box__item" type="primary">主要按钮</Button>
</ConfigProvider>

注意:ConfigProvider 仅影响它的子组件的样式,不影响全局 root 节点

3.3.2 运行时动态主题

在官网ConfigProvider 全局配置下可以体验在线动态切换主题
image.png
调用 ConfigProvider.config 配置方法设置主题色

ConfigProvider.config({
    theme: {
        'color-primary': 'purple',
        "button-radius": '20px'
    }
});

3.4 文档实现

image.png

3.5 打包实现

image.png

四、组件库建设经验

这一部分主要写组件库建设经验,如果说上面是技术上的经验,这部分是人跟事上的经验,毕竟一个组件库要做出来并能持续扩展还真不容易,甚至说比技术更难

4.1 诞生之路

其实刚开始做这个组件库过程很艰难,没有专业的UI支持;一方面UI资源紧张,另一方面团队没有相应的KPI是建设组件库,组件库仅仅是用来满足业务的

我们负责封装组件给其他同事用,写业务的同事组件催的急,我们是利用业务的设计稿中去实现的组件,比如业务设计稿中下拉菜单张这样,那我们就按照业务设计稿实现,没有办法...

但是组件库脱离了UI很难帮助业务,毕竟设计稿是UI给到前端的;难道就满足这一个业务,后面还有类似的场景呢?所以导致前期组件库处于一个不规范的局面

转机出现在,在写业务的过程中我们发现同一个业务的不同页面设计搞不统一,比如按钮圆角、筛选列表的筛选条件...因为我们将其封装成一个组件,那么相应的样式肯定是相对固定的,因为一个系统的设计风格肯定是需要保持一致的,除了可以通过样式变量更改,所以我们与UI同学讨论,在讨论的过程中,我们透露了组件库的**,视觉规范一致的必要性,可以提高前端开发效率、UI设计效率

然后有一位UI同事比较感兴趣,于是我们针对聊了很多,私下花时间专门支持我们,官网也是UI同事帮忙设计的

4.2 寻找突破

中途我们尝试跟主管沟通过组件库没有UI,能不能协调下我们做的组件库需要UI支持,但是目前没有UI能支持,且重点是满足业务,所以组件库并没有投入资源

兴趣使然或者说出于对技术的学习心态吧,我们3个前端+一个UI一心想把组件库给做好,于是我们开始借鉴业内组件库的技术实现,设计语言与前端代码如何结合

就好比设计给出了一个按钮的样式,她得知道这个按钮元素应该包括哪些内容(高宽、高度、字体、内间距等),这些就是设计规范,那这些规范对应的值就是设计语言。再更深层次的设计这些内容背后的逻辑是什么,规范的原理什么的就可以抽象出设计原则

这里的设计语言对应到前端也就是样式变量

UI同事开始给每个组件出稿,每个组件不同形态都有设计;前端开始制定规范,实现组件的同时思考API、样式变量定义的是否规范,组件的demo、文档都补充起来,并进行code review

制定了每周开会机制,一个组件从设计稿设计、前端开发、code review、UI走查到达到要求的路径,如下
image.png

开始设计组件库的官网,想把组件库做大最强,真正帮助大部门多端的业务(梦想还是要有的)

大家热火朝天,虽然频频争吵,好不快乐!

我们深知组件库的视觉规范需要大部门都认同的前提下,才能发挥作用,于是当我们组件库做的有模有样了之后,我们将我们的成果汇报给了主管,希望主管能协调去大部门的UI与前端,统一规范

这些汇报(或者说组件库的未来规划)也算是我们在做组件库的时候,对组件库(或者说更高一层C端视觉规范统一)价值整体的一个认知吧,这些也是我们参考了很多资料总结的,大概如下

可能写的都是比较理想的,大家可以理性参考...这当初的确是我们的目标,不过现在实在没时间
image.png
image.png
未来规划
image.png
image.png
image.png

4.3 歧途

本来技术能真正帮助业务是一件好事,理想是美好的,现实的艰难的,Q1后期吧,可能资源协调确实困难,加上组织架构频频调整,视觉统一共建的事情迟迟难以落地,所以说目前组件库确实满足业务就是最好的选择

写在最后

写这篇文章以及组件库开源的原因是希望我们踩过的坑能给大家一些经验,以及如果你对开源感兴趣可以加入我们一起打造组件库,或者需要taro-react多端组件库的话可以尝试使用tard

GitHub地址:
https://github.com/jd-antelope/tard

原创:CSS样式隔离方案大全


highlight: a11y-dark

「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战

本文主要讲css各种解决方案,包括,BEMcss modulesCss in Js预处理器Shadow DOMVue Scoped通过分析各项方案的产生背景、带来的好处以及存在的一些问题来帮助大家判断自己的项目中适合使用哪种那方案

image.png

零:CSS的原生问题

在讲解各种解决方案之前,我们先回顾下日常开发中我们遇到的css问题,待着这些问题,我们在讲解各种解决方案,并分析各个解决方案是否可以解决如下问题

0.1 无作用域样式污染

CSS有一个被大家诟病的问题就是没有本地作用域,所有声明的样式都是全局的(global styles)

换句话来说页面上任意元素只要匹配上某个选择器的规则,这个规则就会被应用上,而且规则和规则之间可以叠加作用(cascading)

SPA应用流行了之后这个问题变得更加突出了,因为对于SPA应用来说所有页面的样式代码都会加载到同一个环境中,样式冲突的概率会大大加大。由于这个问题的存在,我们在日常开发中会遇到以下这些问题:

  • 很难为选择器起名字:为了避免和页面上其他元素的样式发生冲突,我们在起选择器名的时候一定要深思熟虑,起的名字一定不能太普通。举个例子,假如你为页面上某个作为标题的DOM节点定义一个叫做.title的样式名,这个类名很大概率已经或者将会和页面上的其他选择器发生冲突,所以你不得不手动为这个类名添加一些前缀,例如.home-page-title来避免这个问题
  • 团队多人合作困难:当多个人一起开发同一个项目的时候,特别是多个分支同时开发的时候,大家各自取的选择器名字可能有会冲突,可是在本地独立开发的时候这个问题几乎发现不了。当大家的代码合并到同一个分支的时候,一些样式的问题就会随之出现

0.2 无用的CSS样式堆积

进行过大型Web项目开发的同学应该都有经历过这个情景:在开发新的功能或者进行代码重构的时候,由于HTML代码和CSS样式之间没有显式的一一对应关系,我们很难辨认出项目中哪些CSS样式代码是有用的哪些是无用的,这就导致了我们不敢轻易删除代码中可能是无用的样式。这样随着时间的推移,项目中的CSS样式只会增加而不会减少(append-only stylesheets)。无用的样式代码堆积会导致以下这些问题:

  • 项目变得越来越重量级:加载到浏览器的CSS样式会越来越多,会造成一定的性能影响
  • 开发成本越来越高:开发者发现他们很难理解项目中的样式代码,甚至可能被大量的样式代码吓到,这就导致了开发效率的降低以及一些奇奇怪怪的样式问题的出现

0.3 基于状态的样式定义

对于SPA应用来说,特别是一些交互复杂的页面,页面的样式通常要根据组件的状态变化而发生变化

最常用的方式是通过不同的状态定义不同的className名,这种方案代码看起来十分冗余和繁琐,通常需要同时改动js代码和css代码

这个CSS重写一遍比修改老文件快,这样的念头几乎所有人都曾有过,css虽然看似简单,但是以上问题很容易写着写着就出现了,这在于提前没有选好方案

一、BEM

1.1 简介

BEM是一种css命名方法论,意思是块(Block)、元素(Element)、修饰符(Modifier)的简写

这种命名方法让CSS便于统一团队开发规范和方便维护

.block__element--modifier或者说block-name__element-name--modifier-name形式命名,命名有含义,也就是模块名 + 元素名 + 修饰器名

.dropdown-menu__item--active

社区里面对BEM命名的褒贬不一,但是对其的**基本上还是认同的,所以可以用它的**,不一定要用它的命名方式

1.2 应用场景

BEM**通常用于组件库,业务代码中结合less等预处理器

1.3 优缺点分析

优点:

  1. 人为严格遵守BEM规范,可以解决无作用域样式污染问题
  2. 可读性好,一目了然是那个dom节点,对于无用css删除,删除了相应dom节点后,对应的css也能比较放心的删除,不会影响到其他元素样式

缺点

  1. 命名太长(个人开发习惯、部分人会觉得,我认为命名长提高了可读性,能解决一些问题,也不叫缺点),至于体积增大,gzip可忽略

个人比较喜欢BEM,其**对编码好处远大于坏处,有兴趣的可以在项目中使用,更多可看知乎:如何看待 CSS 中 BEM 的命名方式?

二、CSS modules

2.1 简介

什么是CSS Modules

顾名思义,css-modules 将 css 代码模块化,可以避免本模块样式被污染,并且可以很方便的复用 css 代码

根据CSS Modules在Gihub上的项目,它被解释为:

所有的类名和动画名称默认都有各自的作用域的CSS文件。

所以CSS Modules既不是官方标准,也不是浏览器的特性,而是在构建步骤(例如使用Webpack,记住css-loader)中对CSS类名和选择器限定作用域的一种方式(类似于命名空间)

依赖webpack css-loader,配置如下,现在webpack已经默认开启CSS modules功能了

{
    test: /.css$/,
    loader: "style-loader!css-loader?modules"
}

我们先看一个示例:

CSS文件style.css引入为style对象后,通过style.title的方式使用title class

import style from './style.css';

export default () => {
  return (
    <p className={style.title}>
      I am KaSong.
    </p>
  );
};

对应style.css

.title {
  color: red;
}

打包工具会将style.title编译为带哈希的字符串

<h1 class="_3zyde4l1yATCOkgn-DBWEL">
  Hello World
</h1>

同时style.css也会编译:

._3zyde4l1yATCOkgn-DBWEL {
  color: red;
}

这样,就产生了独一无二的class,解决了CSS模块化的问题

使用了 CSS Modules 后,就相当于给每个 class 名外加加了一个 :local,以此来实现样式的局部化,如果你想切换到全局模式,使用对应的 :global

:local:global 的区别是 CSS Modules 只会对 :local 块的 class 样式做 localIdentName 规则处理,:global 的样式编译后不变

.title {
  color: red;
}

:global(.title) {
  color: green;
}

可以看到,依旧使用CSS,但使用JS来管理样式依赖,
最大化地结合现有 CSS 生态和 JS 模块化能力,发布时依旧编译出单独的 JS 和 CSS

2.2 优缺点分析

优点

  • 能100%解决css无作用域样式污染问题
  • 学习成本低:API简洁到几乎零学习成本

缺点

  • 写法没有传统开发流程,如果你不想频繁的输入 styles.**,可以试一下 [react-css-modules](gajus/react-css-modules · GitHub),它通过高阶函数的形式来避免重复输入 styles.**
  • 没有变量,通常要结合预处理器
  • 代码可读性差,hash值不方便debug

css modules通常结合less等预处理器在react中使用,更多可参考CSS Modules 详解及 React 中实践

三、CSS in JS

3.1 简介

CSS in JS是2014年推出的一种设计模式,它的核心**是把CSS直接写到各自组件中,也就是说用JS去写CSS,而不是单独的样式文件里

这跟传统的前端开发思维不一样,传统的原则是关注点分离,如常说的不写行内样式不写行内脚本,如下代码

<h1 style="color:red;font-size:46px;"  onclick="alert('Hi')">
  Hello World
</h1>

CSS-in-JS不是一种很新的技术,可是它在国内普及度好像并不是很高,它当初的出现是因为一些component-basedWeb框架(例如 ReactVueAngular)的逐渐流行,使得开发者也想将组件的CSS样式也一块封装到组件中去解决原生CSS写法的一系列问题

CSS-in-JS在React社区的热度是最高的,这是因为React本身不会管用户怎么去为组件定义样式的问题,而Vue和Angular都有属于框架自己的一套定义样式的方案

上面的例子使用 React 改写如下

const style = {
  'color': 'red',
  'fontSize': '46px'
};

const clickHandler = () => alert('hi'); 

ReactDOM.render(
  <h1 style={style} onclick={clickHandler}>
     Hello, world!
  </h1>,
  document.getElementById('example')
);

上面代码在一个文件里面,封装了结构、样式和逻辑,完全违背了"关注点分离"的原则

但是,这有利于组件的隔离。每个组件包含了所有需要用到的代码,不依赖外部,组件之间没有耦合,很方便复用。所以,随着 React 的走红和组件模式深入人心,这种"关注点混合"的新写法逐渐成为主流

3.2 实现CSS in JS的库汇总

实现了CSS-in-JS的库有很多,据统计现在已经超过了61种。虽然每个库解决的问题都差不多,可是它们的实现方法和语法却大相径庭

从实现方法上区分大体分为两种:

  • 唯一CSS选择器,代表库:styled-components
  • 内联样式(Unique Selector VS Inline Styles)

不同的CSS in JS实现除了生成的CSS样式和编写语法有所区别外,它们实现的功能也不尽相同,除了一些最基本的诸如CSS局部作用域的功能,下面这些功能有的实现会包含而有的却不支持:

  • 自动生成浏览器引擎前缀 - built-in vendor prefix
  • 支持抽取独立的CSS样式表 - extract css file
  • 自带支持动画 - built-in support for animations
  • 伪类 - pseudo classes
  • 媒体查询 - media query
  • 其他

3.3 styled-components示例

Styled-components 是CSS in JS最热门的一个库了,到目前为止github的star数已经超过了35k

通过styled-components,可以使用ES6的标签模板字符串语法(Tagged Templates)为需要styledComponent定义一系列CSS属性

当该组件的JS代码被解析执行的时候,styled-components会动态生成一个CSS选择器,并把对应的CSS样式通过style标签的形式插入到head标签里面。动态生成的CSS选择器会有一小段哈希值来保证全局唯一性来避免样式发生冲突

CSS-in-JS Playground是一个可以快速尝试不同CSS-in-JS实现的网站,上面有一个简单的用styled-components实现表单的例子:
image.png

从上面的例子可以看出,styled-components不需要你为需要设置样式的DOM节点设置一个样式名,使用完标签模板字符串定义后你会得到一个styled好的Component,直接在JSX中使用这个Component就可以了

image.png
可以看到截图里面框出来的样式生成了一段hash值,实现了局部CSS作用域的效果(scoping styles),各个组件的样式不会发生冲突

3.4 Radium示例

Radiumstyled-components的最大区别是它生成的是标签内联样式(inline styles)

由于标签内联样式在处理诸如media query以及:hover:focus:active等和浏览器状态相关的样式的时候非常不方便,所以radium为这些样式封装了一些标准的接口以及抽象

再来看一下radiumCSS-in-JS Playground的例子:

image.png
从上面的例子可以看出radium定义样式的语法和styled-components有很大的区别,它要求你使用style属性为DOM添加相应的样式

直接在标签内生成内联样式,内联样式相比于CSS选择器的方法有以下的优点: 自带局部样式作用域的效果,无需额外的操作

3.5 CSS in JS 与"CSS 预处理器"(比如 Less 和 Sass,包括 PostCSS)有什么区别

CSS in JS 使用 JavaScript 的语法,是 JavaScript 脚本的一部分,不用从头学习一套专用的 API,也不会多一道编译步骤,但是通常会在运行时动态生成CSS,造成一定运行时开销

3.6 优缺点分析

优点

  • 没有无作用域问题样式污染问题

    通过唯一CSS选择器或者行内样式解决

  • 没有无用的CSS样式堆积问题

    CSS-in-JS会把样式和组件绑定在一起,当这个组件要被删除掉的时候,直接把这些代码删除掉就好了,不用担心删掉的样式代码会对项目的其他组件样式产生影响。而且由于CSS是写在JavaScript里面的,我们还可以利用JS显式的变量定义,模块引用等语言特性来追踪样式的使用情况,这大大方便了我们对样式代码的更改或者重构

  • 更好的基于状态的样式定义

    CSS-in-JS会直接将CSS样式写在JS文件里面,所以样式复用以及逻辑判断都十分方便

缺点:

  • 一定的学习成本

  • 代码可读性差

    大多数CSS-in-JS实现会通过生成唯一的CSS选择器来达到CSS局部作用域的效果。这些自动生成的选择器会大大降低代码的可读性,给开发人员debug造成一定的影响

  • 运行时消耗

    由于大多数的CSS-in-JS的库都是在动态生成CSS的。这会有两方面的影响。首先你发送到客户端的代码会包括使用到的CSS-in-JS运行时(runtime)代码,这些代码一般都不是很小,例如styled-components的runtime大小是12.42kB min + gzip,如果你希望你首屏加载的代码很小,你得考虑这个问题。其次大多数CSS-in-JS实现都是在客户端动态生成CSS的,这就意味着会有一定的性能代价。不同的CSS-in-JS实现由于具体的实现细节不一样,所以它们的性能也会有很大的区别,你可以通过这个工具来查看和衡量各个实现的性能差异

  • 不能结合成熟的CSS预处理器(或后处理器)Sass/Less/PostCSS,:hover:active 伪类处理起来复杂

可以看到优点多,缺点也不少,选择需慎重,更多可阅读阮一峰老师写的CSS in JS简介知乎CSS in JS的好与坏

四、预处理器

4.1 简介

CSS 预处理器是一个能让你通过预处理器自己独有的语法的程序

市面上有很多CSS预处理器可供选择,且绝大多数CSS预处理器会增加一些原生CSS不具备的特性,例如

  • 代码混合
  • 嵌套选择器
  • 继承选择器

这些特性让CSS的结构更加具有可读性且易于维护

要使用CSS预处理器,你必须在web服务中安装CSS编译工具

我们常见的预处理器:

4.2 优缺点分析

优点:

  1. 利用嵌套,人为严格遵守嵌套首类名不一致,可以解决无作用域样式污染问题
  2. 可读性好,一目了然是那个dom节点,对于无用css删除,删除了相应dom节点后,对应的css也能比较放心的删除,不会影响到其他元素样式

缺点

  1. 需要借助相关的编译工具处理

预处理器是现代web开发中必备,结合BEM规范,利用预处理器,可以极大的提高开发效率,可读性,复用性

五、Shadow DOM

5.1 简介

熟悉web Components的一定知道Shadow DOM可以实现样式隔离,由浏览器原生支持
image.png

我们经常在微前端领域看到Shadow DOM,如下创建一个子应用

const shadow = document.querySelector('#hostElement').attachShadow({mode: 'open'});
shadow.innerHTML = '<sub-app>Here is some new text</sub-app><link rel="stylesheet" href="//unpkg.com/antd/antd.min.css">';

由于子应用的样式作用域仅在 shadow 元素下,那么一旦子应用中出现运行时越界跑到外面构建 DOM 的场景,必定会导致构建出来的 DOM 无法应用子应用的样式的情况。

比如 sub-app 里调用了 antd modal 组件,由于 modal 是动态挂载到 document.body 的,而由于 Shadow DOM 的特性 antd 的样式只会在 shadow 这个作用域下生效,结果就是弹出框无法应用到 antd 的样式。解决的办法是把 antd 样式上浮一层,丢到主文档里,但这么做意味着子应用的样式直接泄露到主文档了

5.2 优缺点分析

优点

  • 浏览器原生支持
  • 严格意义上的样式隔离,如iframe一样

缺点

  • 浏览器兼容问题
  • 只对一定范围内的dom结构起作用,上面微前端场景已经说明

普通业务开发我们还是用框架、如Vue、React;Shadow DOM适用于特殊场景,如微前端

六、vue scoped

当 <style> 标签有 scoped 属性时,它的 CSS 只作用于当前组件中的元素

通过使用 PostCSS 来实现以下转换:

<style scoped>
.example {
  color: red;
}
</style>

<template>
  <div class="example">hi</div>
</template>

转换结果:

<style>
.example[data-v-f3f3eg9] {
  color: red;
}
</style>

<template>
  <div class="example" data-v-f3f3eg9>hi</div>
</template>

使用 scoped 后,父组件的样式将不会渗透到子组件中

不过一个子组件的根节点会同时受其父组件的 scoped CSS 和子组件的 scoped CSS 的影响。这样设计是为了让父组件可以从布局的角度出发,调整其子组件根元素的样式,父租价利用深度作用选择器影响子组件样式

可以使用 >>> 操作符:

<style scoped>
.a >>> .b { /* ... */ }
</style>

上述代码将会编译成:

.a[data-v-f3f3eg9] .b { /* ... */ }

有些像 Sass 之类的预处理器无法正确解析 >>>。这种情况下你可以使用 /deep/ 或 ::v-deep 操作符取而代之——两者都是 >>> 的别名,同样可以正常工作

总结

六种方案对比如下,社区通常的样式隔离方案,以下两种

  • BEM+预处理器
  • CSS Moduls + 预处理器

image.png

你用的CSS隔离方案是什么,欢迎探讨?

写在结尾

本文首发于zxyue25/github/blog,欢迎关注,star~,持续记录原创、好文~

原创:工作一年半,总结出这份前端研发流程图

一、背景

最近研究了前端研发流程,总结了一份前端研发流程图,正好也到年底该总结了,于是认真研究了一番。

本人工作一年半,文章都是结合自己真实的经历写的,也欢迎大家探讨前端研发流程!

本篇文章讲整体的前端研发流程,主要讲整体,具体各个模块由于篇幅不展开

image.png

二、研发流程

1、开发前准备阶段

分为规范文档两部分
image.png

(1)规范:

》编码规范
这一块涉及的就比较多&基础了,比如*lint规范、文件命名规范、工程结构规范、注释规范、图片处理规范等等

》技术选型规范
选这个技术栈是出于什么原因,能解决什么痛点?能给业务带来什么收益?举一个发生在身边的真实案例

   反例一:大家都知道移动端组件库官网都有一个手机预览,如下图。手机预览的是一个组件示例项目,一个独立的H5项目。
   实现方案是通过iframe实现(最近在封装一个taro-react组件库,也遇到了这个场景)
   
   有一天,同事A兴奋的说:XX给我提了一个建议,咱们的文档可以通过`微前端qiankun`的形式实现,不用iframe了
   
   我:??
   
   同事A:我觉得这个建议可行,说出去显得技术也厉害
    
   总结:很明显这种技术选型存在问题的,首先iframe也是微前端的一种实现方案,选qiankun的动机单纯是为了“显的”技术厉害。
   引入qiankun非但对该场景没有收益,还影响部署方式,代码结构等,反而显得“不懂技术”。

image.png
技术选型更体现一个人的技术功底,需要一定的技术广度,一定的技术深度,往往是高级前端需要必备技能

》架构设计规范
架构设计就更有挑战了,是基于技术选型。讲究可维护性可扩展性,对于复杂场景,具有创造性思维,创造解决方案;并能从业务中沉淀通用解决方案,造出轮子(工具插件等),对于工具设计也有规范,保证易用性,帮助团队降本提效

一般的业务大家可能会觉得根本涉及不到架构设计啊,换个角度看,是你还不具备能看到需要架构设计的能力。架构师往往能一眼看到隐藏的问题给出最佳实践方案

为什么有些人的代码写出来成了💩山,为什么有的人写出的代码像诗,差异巨大。往往跟整体的架构、系统设计有很大的关系,比如

  • 网络请求模块封装的合理嘛
  • 埋点通常接公司的埋点系统,直接拿来嵌在业务代码里面往往会污染业务代码
  • 代码拆分合理嘛,有超多1000千的文件嘛,经常看到一个文件3、4千行,接手那叫一个酸爽
  • 编译、打包的命令合理嘛,见过起一个服务居然必须要注入变量才行否则起不来

......众多的细节加起来,决定了工程架构是否优雅

》工时评估规范
这部分主要是项目管理 的需要了,每个人都有一套自己的工时评估规范,怎么保证评估模块A的2人天是准确的?
这方面我的经验就是:首先让负责开发的人自己评估,根据自己的一个实际情况评估一个时间;第二再check这个时间是否合理,是否忽略了某些相关的改动也需要工作量(PS:经历的大部分人都是评估出的时间过短,只看到了表面的功能,一开始没有把可优化、相关联的改动考虑进去,中途才发现时间来不及)

(2)文档

文档也是很重要一部分,相信我,没有一个人能记住业务流程、技术方案的!!
在项目开始之前,就需要建立一个项目的文档,里面必须包括基本的PRD、UI稿、技术方案、以及必要的复盘

》PRD

  • 通常在产品的文档空间,一般记录文档地址即可
  • ⚠️开发时,总会发现PRD细节的小问题,最好提醒产品更新PRD,不然时间久了,回去看,没更新的问题坑的都是自己
  • 对于有必要梳理的业务模块,需要结合技术整理偏业务的文档。举个🌰:小程序有很多分享,分布在PRD的各个页面上,十几个分享入口,参数是啥,回哪个页面,有些相同,有些不同,可以自己整理一个文档,更清晰明了

》UI设计稿

  • 通常在设计空间,一般记录文档地址即可

》技术方案文档

  • 架构设计
  • 技术栈信息
  • 重难点技术方案,比如登录、支付等
    》复盘文档
  • 开发时发现部分成员错误做法的踩坑文档
  • 上线出现问题后的复盘文档

2、编码&联调阶段

image.png
这个阶段设计的就比较多了,也是大多数团队花时间最多的一部分

  • 在确定技术方案时,首先需要确定基本的技术栈,这通常取决于团队用什么技术栈,比如我们C端用的是Taro+React,B端用的是Vue;统一用Typescriptless
  • 选完基础技术栈后,通常会考虑选择物料库,最基本的物料库 组件+utils;组件一般来说业内开源的组件库能满足大部分,如 Ant Design/Element/Vant,但是一般都会有业务相关的组件库,比如我们组内在做的 C端多端组件库B端业务组件库;pro的话意思是根据组内业务常用场景的代码模块,类似 Ant Design Pro这种,都属于物料库
  • 然后就是开始初始化工程了,到这一步用到工程化的东西,最基本的用业内开源的脚手架实现工程搭建、开发服务、编译构建,如VueCli/create-react-app等;通常组内会有一个自己的轻量级的脚手架用来实现工程搭建,因为每个团队都有自己一套规范,但是通常很难会实现开发服务、编辑构建。所以我把这里写成了工程初始化命令行,用这个搭建工程,包括目录结构,选择的技术栈,,组件库,*Lint等
  • 下一步就是根据PRD、UI稿开始开发了;包括基础的业务代码编写,如果需要写单元测试的话还需要同时完成单元测试编写;以及数据方面的简单的本地mock数据聚合;数据聚合的意思是前端将几个
  • 静态页面开发完成后开始练联调环节,通常服务端需要给前端一个数据结构文档code码文档;并且要求服务端接口需要保证都遵守这个规范。通常服务端开发前,需要给出接口文档,公司一般都有mock服务,前端先通过mock服务进行数据联调,避免阻塞开发

3、自测&优化阶段

image.png
很多人会忽略这个阶段,如果说开发联调完成后可以达到60分,那么这个部分是项目在基础上加分

  • 埋点:这个没有写在开发阶段,因为这项,在编码上看,是在完成开发联调之后做的,在功能层面看,也是一个分析数据来做优化的
  • 性能:在开发完成后,需要自测基本性能是否达标,比如图片体积是否过大打包后的体积,依赖模块分析,浏览器兼容性手机兼容性等等,这些都跟性能指标相关,需要一个标准
  • CR:提测前进行code review;需要关注,代码规范问题、单元测试是否达标,以及UI稿还原度,上一步性能标准是否达标
  • 自测/修复:测试用例评审后测试通常会给一份冒烟用例,最基本的自测就是按照冒烟用例走一遍,看到业内还有一些有UI自动化测试等;都通过后进行代码合并到主分支。这里写了一个UI体验问题衡量标准的意思是,之前在碰到一个案例,C端商城订单列表的时间筛选,UI给了一个类似PC端的选择,手动选择开始时间、结束时间的年月日,当时我负责封装下拉菜单组件,同事找到我说,能不能把下拉菜单组件暴露XX事件(时间筛选是在下拉菜单里面);当时我是觉得这个要求比较偏业务,然后觉得这个需求不合理,于是研究了下主站的订单筛选,张下面这样,在移动端还让人选择年月日的确不合理呀,于是乎让UI重新出稿
    image.png

4、构建&部署阶段

image.png
这一步也是必备技能,现在公司一般都有自己搭建的一套自动构建发布流程,按照流程走就没啥问题。流量大的产品还需要考虑A/Btest灰度发布;以及一些SaaS产品,PaaS产品需要考虑的私有化部署

5、上线后数据采集&分析阶段

image.png
上线后并不是万事大吉了,数据、监控、用户转化才决定着产品的真实情况

  • 异常/性能监控:前端JS异常监控,JS执行错误是非常致命的,监控到错误后进行修改能给代码带来不少稳定性;前端性能通常是指首屏时间、网络请求时间等等指标,主要标识产品的体验性好不好。

之前写过两篇相关文章:指路别再用performance计算首屏时间了!!面试必问:前端性能监控Performance

  • 服务器监控:比如域名是否可访问,应用健康度,机器性能等,这部分主要看公司有无提供能力,团队想做一般很难
  • 告警:异常监控后会对应用负责人进行邮件或者APP告警
  • 异常处理:一般两种情况,出现线上紧急问题,切复现不出来问题,这时候就可以借用监控来寻找蛛丝马迹了,很紧急的问题,可能还需要有降级方案;第二种就是没有业务反馈问题,也需要日常去查看异常监控,可能小问题隐藏着大bug

写在最后

完整前端研发流程图如下,可以看到是一个闭环的

当然这是比较完整的流程,小需求、临时需求可能是这些流程的一部分
image.png

参考:
#3

我跳槽了——记录第一次社招面试

欧科云链

一面

  1. hooks为什么不能放在if里
  2. useContext
  3. useMemo
  4. 自定义hook
  5. 数字精度问题
  6. 检测数据类型
  7. Promise链式调用,以下分别输出什么
Promise.reject(1)
        .then((num) => {
            console.log(num);
        }).catch((num) => {
            return num + 1;
        }).then((num) => {
            console.log(num);
        });
// 2
Promise.resolve(1)
        .then((num) => {
            console.log(num);
        }).catch((num) => {
            return num + 1;
        }).then((num) => {
            console.log(num);
        });
        
// 1
// undefined
  1. 输入框获取焦点边框高亮
  2. 列表表头吸顶
  3. 一个父级div,有两个子div,一个固定高度,另一个高度填充满
  4. 手写算法

[{ price: 1, size: 2 }, { price: 2, size: 2 }, { price: 1, size: 1 }]] 依次按照price、size降序排序

function sort(arr) {
    for (let i = 0; i < arr.length; i++) {
        for (let j = i + 1; j < arr.length; j++) {
            if (arr[i].price === arr[j].price && arr[i].size < arr[j].size) {
                [arr[i], arr[j]] = [arr[j], arr[i]];
            } else if (arr[i].price < arr[j].price) {
                [arr[i], arr[j]] = [arr[j], arr[i]];
            }
        }
    }
    return arr;
}

二面

  1. 问简历中做多端组件库做了那些组件,有没有遇到什么问题
  2. 重绘&重排,那些操作可以导致
  3. 浏览器渲染过程
  4. component、PureComponent区别
  5. 以下输出
var name = "global";
var obj = {
    name: "ngnce",
    log:()=> {
        console.log(this.name);
    }
}
obj.log()

var obj = {
    name: "ngnce",
    log:()=> {
        console.log(this.name);
    }
}
obj.log()
  1. 哪些样式可以继承
  2. e.target e.currentTarget有什么区别
  3. 图片懒加载,路由懒加载实现原理是什么
  4. hooks你们实践的经验是什么
  5. 手写深拷贝,循环引用问题如何解决

小红书社区

一面

  1. 挨个问简历项目
  2. 以下输出是什么,把async、await用promise怎么改
console.log(1);

let b = new Promise((resolve, reject) =>{
  console.log(2);
}).then((x) => {
  console.log(3);
})

setTimeout(() => {
  console.log(4)
}, 100);

let c = async() => {
  setTimeout(() => {
    new Promise((resolve, reject) => {
      console.log(6);
    })
  }, 0);
  let x =  await new Promise((resolve, reject) =>{
    console.log(5);
    resolve(7)
  })
  console.log(x);
  console.log(8);
}

console.log(9);
c()
console.log(10)

// 1
// 2
// 9
// 5
// 10
// 7
// 8
// 6
// 4


let c = () => {
  setTimeout(() => {
    new Promise((resolve, reject) => {
      console.log(6);
    });
  }, 0);
  new Promise((resolve, reject) => {
    console.log(5);
    resolve(7);
  }).then((res) => {
    console.log(res);
    console.log(8);
  });
};
  1. async、await的本质是什么
  2. 网络请求封装怎么做
  3. 会二叉树嘛,不会,考一个简单的算法,青蛙跳
  4. 手写promise.all
  5. Vue发请求在哪个生命周期
  6. React 17版本有什么特性
  7. Class写法与hook写法的优劣比较
  8. hooks有什么问题嘛

二面

  1. 做过那些工程化相关的东西
  2. 为什么要用monorepo,有没有遇到依赖相关的问题,比如隐式依赖
  3. webpack构建打包自己做过哪些事情
  4. react常用的hooks、自定义了哪些hooks
  5. 举例说明useCallback跟useMemo的使用场景
  6. 写一个括号的题
  7. 职业未来规划

三面

  1. 简历微前端相关
    • 为什么要用微前端
    • 你们用的微前端是什么
    • 除了qiankun你还知道其他解决方案嘛
    • 京东的micro-app跟qiankun有什么不同
    • 为什么没用京东的
  2. 能说一个你觉得比较有意思的专利嘛
  3. 算法题

猿辅导智能硬件

一面

  1. 做过哪些性能优化
  2. webpack渐进式聊
  • webpack常见配置参数是什么?
  • 常用的loader有哪些?
  • 有没有自己写过一个plugin,暴露的生命周期有哪些清楚嘛
  • webpack的原理,从执行到结束的过程
  1. babel实现原理是什么
  2. 手写可以缓存的接口封装
// 考察Map用法
function httpRequest(url, options) {
  return Promise((resolve, reject) => {});
}

const reqMap = new Map();
const waitTime = 1000;

function request(url, options) {
  if (reqMap.has(url)) {
    const req = reqMap.get(url);
    if (Date.now() - req.time < waitTime) {
      return reqMap.get(url).response;
    }
  } else {
    const res = {
      response: httpRequest(url, options),
      time: Date.now(),
    };
    reqMap.set(url, res);
  }
}
  1. map数据结构怎么用,为什么可以用非字符串作为key值
  2. 手动实现一个模版字符串
// replace不会改变原字符串
// 正则表达式:. 匹配除换行符 \n 之外的任何单字符;* 匹配前面的子表达式零次或多次; ? 匹配前面的子表达式零次或一次,或指明一个非贪婪限定符
function template(str, context) {
  return str.replace(/\$\{(.*?)\}/g, (match, key) => {
    return context[key]
  });
}
  1. 从设计理念上说一下Vue VS React
  2. 小程序了解的多嘛
  3. 前端职业规划
  4. 你对前端的看法

网易有道——智能硬件

一面

  1. 以下是否有语法错误,输出结果是什么
const arr = [1,2,3,4,5] // 无语法错误
const arr1 = arr.push(6) // 6
  1. 在react constrictor里面写settimeout,this指向什么
  2. 如何自己设计一个image,实现懒加载的组件
  3. 如何实现页面缓存,keep-live实现原理,对应的react如何实现
  4. 做过哪些首屏优化
  5. 以下能看到页面变化嘛
document.body.style = 'background: red' 
document.body.style = 'background: black'

二面

  1. 工作中遇到过什么技术难题解决不了
  2. 微前端是怎么实现的,跟qiankun有什么不同
  3. 说一下react根vue的区别
  4. 做过什么性能优化
  5. 找出出租中出现次数最多的字符串
const getMostStr = (arr) => {
    let obj = {}
    let mostObj = {
        str: '',
        count: 0
    }
    for (let i = 0; i < arr.length; i++) {
        if (obj[arr[i]]) {
            obj[arr[i]]++
        } else {
            obj[arr[i]] = 1
        }
        if (mostObj.count < obj[arr[i]]) {
            mostObj.count = obj[arr[i]]
            mostObj.str = arr[i]
        }
    }
    return mostObj.str
}
  1. 实现'get-element-by-id' -> 'getElementById‘
const transStr = (str) => {
    const arr = str.split("-")
    let res = arr[0]
    for(let i = 1; i < arr.length; i++){
        res += arr[i].slice(0,1).toUpperCase() + arr[i].slice(1)
    }
    return res
}

微软北京

一面

  1. 项目中有遇到什么难以解决的问题、怎么解决的;针对项目详细问了不少
  2. code

字节

一面

根据简历问项目

二面 tiktok架构组

根据简历问项目
多工程公共模块处理方式
实现一个调度器

三面 tiktok直播组

根据简历问

  1. 组件库
  2. webpack性能优化,tree shaking原理,external与dll区别

好文阅读:《🔥UU跑腿前端研发的5年历程💖》

混沌期

2016年2月22日,够2吧。捧着会jQ/PHP/Asp/PS/H5等技能加入了不到10几个人的技术团队。因为我的加入,原本唯一一个前端凯哥,升职做了产品经理。从此,开始了一个人的Web战争。

初期移动端项目比较多,我主要负责一些APP内嵌文字说明之类的页面—俗称“打杂”开始。由于之前jQ用的多一些,还特意买了一本《锋利的jQuery》,后来因为有人说jQ的体积大,于是转用了Zepto。Zepto使用过程中也是遇到了一堆的坑,例如FastClick、原型属性丢失、阻止默认事件等。

模块化-炼气期

2016年下半年,开始写商城系统和基于微信的web端下单业务,转向业务逻辑较多的应用类开发。

过程中发现在一个项目下需要写很多公共方法,多页面调用时会出现变量引入问题。如果每个页面都静态加载,维护又比较费时间。

于是引入了玉伯老师的Sea.js,使用CMD的方式加载模块。再后来发现公共文件维护比较扯淡,于是使用Sea里面的alias挂载了CDN统一管理公共文件。

我们后端使用的是C#的MVC模式,对于前端主要堵塞点就是联调环境。

先SVN更新前端代码上去,后端获取重新生成,更新IIS站点,查看效果。这种缺点就是无法实时优化交互,更改文件,沟通成本高。改用了FTP传输前端文件,后端在不变更逻辑前提下,无需多次编译。但效率还是不够高,再折腾。改成SVN同步服务端bin文件(编译后的),将环境部署到本机进行调试。这样只需要协调后端每开发一个模块及时打包编译bin文件上传即可。

前后分离-筑基期

郑州,一个跻身于十八线的城市,不比北上广。所以当你读到这些可能觉得我们很滞后,明明你已经用了2年多的东西,为何我们才刚介入。一个点,一切成长的植物基于土壤,生存是第一个难题。这个环境下,我们不在乎晚几天发芽,根部有没有固定正,一切都可以在发芽以后努力摆正。

2017年初,受够了不断的需求改动导致调试比开发时间还长,主动提出了前后端分离。当时,前端团队只有4人,所以选择了入门相对简单的Vue。个人认为,算是真正意义上开始了前端的入门之路。

IDE-武器

早期使用的IDE是被称为“Web前端开发神器”的Webstrom。在使用vue以后就各种不自在了,最大问题是性能占用过高,也可能是我们电脑太Low 。

为此我们还做了一套公共同个node_modules的打包框架,目的就是为了提高项目解析和创建新项目的效率。

使用公共依赖,打包根据每个模块下的config配置文件单独打包资源。

VS Code-神器

超神之路,势必要选择一把趁手的武器,如果是紫色稀有甚好。我们后端一直用的VS,据听说堪称“宇宙第一”。开始确实不习惯VS Code,当听说是基于Web开发的时候已经膜拜了,也让我对前端的发展充满期待。VS Code加持,还怕什么不能超越的。

2017年7月,公司搬迁新址,郑州CBD,大棒子-玉米楼对面。为了对得起CBD,SVN升级GitLab,mobile-cli回归vue-cli,VS Code挂载Git,航天载人计划启动。前端研发团队发展到10人。

架构之路-金丹期

个人认为,脱离业务谈架构有些扯淡的。阿里也是在后期涌现各类优秀框架和提高能效的工具。因为前期的业务真的繁重,不忙的时候“加班”写写组件。忙的时候,时间不够休息。毕竟业务第一。

CNPM.JS-搭建本地依赖仓库

这个阶段,经历了上百个大大小小业务需求。衍生了很多公共业务组件和功能组件,包含:登录/注册、身份校验、支付充值、城市选择、地图定位等。其中涉及一些业务接口和逻辑,不适合发布npm的。为了提高协同效率,在本地服务器搭建了一套私有库,并做了文档规范。

‍♀️能效工具-Bomb

随着人员增加,组件的管理和维护变的异常繁重,于是在年末开发了一套组件库管理工具Bomb。主要将组件可视化,方便调用。并集成了Node+koa的后端模板,小程序模板,php模板等。

新人查找公共组件很麻烦,怎么办?

有没有可视化UI,直接上手创建项目?vue1.0都有UI管理

每次写后台都要写接口请求方法,重复三遍就不想写,怎么办?

我不喜欢看文档,我喜欢看图,怎么办?

我加入团队就是想深入了解后端,有快速入门吗?

本地文件云管理,图片云管理,项目云盘有吗?

。。。。

怼嘛,拽过来Electron,引入Node负责文件的写入、打包、管理等。写个工具管理组件库,提升团队协作效率。

️‍♂️业务组-KPI产出

小程序一年发了30个版本,经历了从吐血到吐血的过程。当以为出血撸出来的版本是稳定版本的时候,才发现多端维护是个深坑。

微信小程序

从基础模板开发到mpvue框架的代替。从1.0到3.0,从简单到复杂再到简单。其实也验证了我们团队的变化,人少的时候做一个最简单的版本,人多的时候不甘愿简单挑战复杂,挑战原生。人员技术精进的时候,立足性能立足体验,回归简单,回归轻量。

支付宝小程序

当发现小程序维护深坑的时候,前端伙伴已经处于崩溃边缘了。我记得一个伙伴一个月加班了20天,才赶到领导安排的时间节点写完业务。这时订单稳定,平台稳定,性能稳定,我说换框架,这TM不是坑是什么?

为什么换?兼容多平台、多端的能力,在当时的前端看来前卫,但作为软件开发者早已习以为常且必然是趋势。大幅度提高人效,多端通用成为了最核心的动力。

从vue到react的转变。还获得支付宝小程序创新大赛月度亚军和总决赛二等奖。也是第一批投身支付宝小程序生态建设的团队。参与了开发文档的维护和建议。(说小白鼠的,过分了)

WebApp-拿出手的成绩单

webapp1.0是MVC架构写的,也就是上面sea.js提到的。webapp2.0是仿照客户端研发的,大厅是可拖拽的地图。3.0是回归初心,webapp在加载地图时的交互体验很不友好,不利于性能优化。经过管理层谈论,3.0回归初心,打造轻量级webapp,UI也跟着优化了一番。也就是现在UU跑腿微信公众号里的“立即下单”。或者来这里

授权最头疼,我们有一套统一OpenApi,按理说不需要千人千面的开发授权逻辑。但那终究是理想状态,理想到你足够强大。为了业务,为了拓展,只能委曲求全去兼容别人喽。于是,webapp因企而异的开发了各类产品的授权逻辑。还有更本土化的支付方式,支持投来橄榄枝的各大银行支付。现在都不敢轻易发版,怕测试哭 。不过丝毫不影响我要重构的欲望 。方法总比困难多,实在不行就背锅。

全栈工程师-元婴期

解决了基础协作流程,解决了架构问题,那么是时候验证真功夫了。Node了解的深不深,咱们业务里面试一试。

前端团队前后培养了几个Node方向的全栈工程师,来处理一些边缘业务(例如抽奖,协作工具,常规活动,数据埋点,异常捕捉等)。后端使用的框架就是上面Bomb里提到的NodeJs+Koa2+MySql。第一个实践的是

之前单独介绍过,现在2.0版,加入了小程序,小游戏等模板。目前城市运营端日常上线的常规活动都是由它生成的。玩法交互层由Node开发的业务承接,涉及核心业务的订单信息,会员信息等由C#开发的业务端支持。后期会通过微服务形式彻底解耦,助力企业运营,支持更多更灵活的玩法。

FeedBack-反馈系统,融合了技术工单,客户端的意见反馈,内部问题反馈等业务。打通了钉钉,支持技术值班坐席,客服坐席,响应统计等。为了让内部技术问题减少流转,第一时间得到处理。

其他还有【考试系统】、【云盘】、【Draw脑图】、【UU大学】、【各类生成工具】等都不一一列举了。。。借助这些全栈项目,团队成员在Node端的了解也越来越深,结合Electron可以快速研发基于PC端的桌面应用。

全栈研发的路任重而道远,最关键的是你要知道你们目前的团队哪些可以做?哪些不可以做?我曾有很多想法,系统商业化,开源工具,研发编辑器。这些都是兴趣导向,而非业务导向。举个例子:结合你每天掘金+知乎的意识培养,你认为开发一套图片生成工具很有用,甚至对未来的业务有帮助。可能你是最对,但未必是目前最必须的。如果你能结合业务痛点或者堵塞点,能够说服你的领导,能够沟通上下部门的同事,那么你所做的才会发挥更大价值。也会有更多的支持。目前的我也是不断思考,不断探索,认知也在不断和领导碰撞。关键在你~

术业有专攻-化神期

以上渣渣之路,大概介绍了一遍,接下来便是化神期。成为大神,成为大佬,是每个前端菜鸟的向往。前端已经进入了深水区,能否在此占领山头。就看接下来,你的钻研方向了。尽量一专多能,而非样样精通。

这时团队接近20人,业务也错综复杂。前期自己主导的全栈业务,人手明显不足。写了接口写后台,写了后台写移动端。相当于额外增加了后端的任务量。一个全栈接触后端无非2年,一个5年后端终究还是有更深入的研究。所以,全栈需要向大数据、性能、数据库方向沉淀。移动端需要向web应用,底层源码上深入。PC端向中后台和全栈方向发力。

对于前端未来的规划,畅想的很远,期待的很多,落地的有限。

技术绝不能因为管理而落伍。

钻研的方向绝不能因为需求而偏离。

总之,因为web前进于此,未来可期。

创作于:2020-03-07

原文:https://zhuanlan.zhihu.com/p/111556173

原创:在京东的这一年,知难而进!

今年正好是我步入前端完整工作的一年,20届毕业,到现在工作一年半

这一年,带我的导师离职了,部门调整了一波又一波,我也在变化中不断适应;过程中有很多收获,也有不少遗憾

想了很久怎么写这篇文章,纯总结的话大部分是自己的经历,可能对大家没啥用,个人感觉自己技术水平虽然还一般,但是工作软实力还8错,所以过程中,尽量加写一些案例,希望对大家有用,一起成长~

一、关于博客

上半年开始在掘金陆续写文章,总共输出了19篇,不算多,但大部分都是很用心写出来的;大部分是工作中沉淀的解决方案,还有一些自己学习总结笔记

写文章的目的很简单,记录&总结;工作中经常会听到,自己写的代码自己都不认识了,所以趁着我还记得的时候写出来orz

比较惊喜的是,其中一篇上了周榜前三,收到了一份掘金的周边三件套
image.png

写文章,我自己的经验是从业务&团队出发,凭空写文章非但没有办法实践,也没有这么多精力、或动力不足,反正我是卷不动>-<

二、关于github

这一年沉淀了7个仓库,虽然star数不多,但都是自己码上去的

  1. npm-package-privatify:一个将私有源npm包处理为离线包的自动化工具
  2. cli-template :一个前端CLI模版仓库
  3. s-lint:一个全面、通用的前端Lint规范解决方案;集ESLint(Vue、React、Taro、Next、Nuxt)、StyleLint(Less、CSS)、CommitLint一体
  4. axios-ajax: 基于axios二次封装,更友好的在浏览器发送ajax请求
  5. widget-sdk: Vue多工程公共模块渲染sdk
  6. 微前端模版仓库:主应用工程子应用工程

image.png

这里要感谢我的导师,从他身上我学到了最佳实践的精神,因此工作中遇到一些复杂场景,我的经验是有意识的总结&沉淀

三、关于工作

21年可以总结为三部曲“挑战-变化-适应”

1. 挑战

年初,我跟导师(校招生都有导师带)所负责的业务线扩张到了3条,导师将其中我比较熟悉的一条交由我负责(带几个外包一起干),这对刚工作不久的我来说,其实是有一定挑战的。

当时我遇到以下几个问题:

  • 怎么带外包干好项目,保质保量
  • 怎么让年限资深的项目组成员人信任我这个"前端负责人"
  • 怎么利用技术或者发散产品思维去“帮助业务成功”

这里再次感谢导师的培养,如果说上面是技术的培养,这里是工作能力的培养。其实我跟导师当时面临着资源匮乏的大问题,在这个节点上,导师没有让我去参与其他两个业务线打杂,而是让我保持聚焦

这里我的经验是“如果你每天的工作没有了思考,像一个机器一样麻木,这时候需要警醒”,有可能是没用对方法,效率太低,一直在做重复工作,还是就是不够聚焦,没有精力去思考了。可以尝试跟主管沟通交流,及时调整状态

当时我采取的办法:

  • 第一点一定是负责主动承担

    • 产品、测试、业务等反馈的前端问题及时响应,在群里看到是前端问题的,或者前端可以去解决的,主动站出来解决
    • 毕竟是前端,用户体验是我们需要关注的重点,在需求评审时,对交互提出专业的建议是可以加分
    • 保证按时提、上线,提测前把控代码质量、上线风险,这是最基本的

    一个案例:业务线下五个产品转交过来UI、交互不统一;产品计划UI统一,但没有时间出方案。当时我主动站出来制定了UI&交互统一方案(也是闲的没事干),拉着产品、测试针对一页的改动点一一评审,主动联系UI支持,最后带着外包改造了100+页面,当然我们效率还是比较高的,最终探索了vue多工程间公共模块处理最佳实践。把通用的组件都视为公共模块,在UI统一的同时,也优化了前端代码,因此建立了项目组成员对自己的信任

  • 勇于挑战自我创新思维

    • 作为职场新人,还是有一定的试错机会的,不畏惧敢于提出技术创新
    • 一定要关注业务,有必要了解竞品,多看多学,为什么这个需求做,这个需求不做,只有了解了业务后,你才跟产品有默契,提出创新建议

    当时我们的业务是典型的微前端场景,且产品计划中需要做一个统一控制台,之前在组里做其他业务线也做过微前端,于是我自己搞了一个tob系统微前端实践总结

    这里我的经验是,不要给自己设限,有人把自己当作接需求的机器,只关心代码实现,却不知道做这个需求是为了干嘛,这也是我初期犯的错。其实正好相反,我们做的需求都是为了服务用户。所以在接需求的时候,可以站在用户的角度思考,这么实现是不是用户能接受的交互,体验是否跟系统保持一致风格等;甚至,你也可以发现产品中缺失的部分,提需求

  • 学会举一反三,学会技术沉淀

    • 能用工具去解决的就用工具解决”来自主管的原话,工具来自哪里,业务中痛点,当你深入了解业务后,会发现一些技术难点,当你思考后,发现可以用自动化解决,一个工具就诞生了

    当时基于业务在小组内提出做了一个将私有源npm包处理为离线包的自动化工具,虽然不难,但是是我第一个从创意到落地的小工具,也收获到了主管的肯定

当时我收获到的肯定:

“作为1年的应届生,非常有想法,不断学习并举一反三,成为非常棒的前端负责人”

认真努力,刻苦专研业务。出色的动手能力,对自己有较高的要求。目标清晰明确,执行干净利索

收到这些肯定还是很开心的,也来之不易。后来离开部门的时候,同事们也都约饭为我送行祝好,一波曾经一起奋斗的同事~

2. 跳出舒适圈,知难而进

随着公司一波又一波的调整,自己在A部门所负责的业务线,面临着产品方向不确定的大问题,一直停留在原地,也有一些同事离职,再一次调整前(每次调整意味重新梳理方向),经过考虑,正直学习期的我,当前的重点是在项目中锻炼,于是我选择了内部异动

这里我的建议是,要有主见,选择自己当前最需要的

其实我自己当时也是挺纠结的,留原部门,有业绩,踏实的干下去,年底晋升不会少,但是产品方向不确定,意味着未来可能很长一段时间都做不了啥,担心浪费成长的最佳时间;选择一个新的部门,需要从0开始,这个过程其实是需要成本的,但是新部门可以接触新的场景、技术栈,利于成长。请教了挺多人,相比之下,我选择了后者。我的观点还是趁年轻,多尝试,多经历

3. 重新适应,继续打怪

说来也是天意,跟B部门主管聊了一个多小时,一拍即合,自己在上一个部门业务更多交给外包去,自己花了挺多时间做工程化的东西,以及阅读相关资料;而B部门是刚成立的业务跑的很快,缺少工程化这部分的东西。其实当时这么说的时候,自己还是有一些压力的,毕竟经验有限,且自己都是照葫芦画瓢。我的职级算是比较低,一般异动至少社招级别才要,经过B部门主管协调,9月份我就到B部门了

适应前期

  • 来到这边,跟之前的部门变化还是挺大,这是一个年轻的团队,所以团队基建的机会比较多,我看到可以做的事情比较多(也庆幸自己的选择是正确的)

  • 不过这些认知都来自上一个部门的沉淀,上一个部门沉淀了5年,很多东西都比较规范了,不同的是在上一个部门,我是规范的遵循者,到这边是提出者制定者

  • 于是我刚进来就跟同事一起做了团队代码规范方案,在输出架构图的时候,比较清晰明了,基本得到了组内成员的技术信任。方案里面的各个部分,我没有自己去实现,而是在制定方案后,找到感兴趣的同事一起参与,比如脚手架部分,分给一个同事负责了中高级前端必备:如何设计并实现一个脚手架,并不是我不会写,恰好相反,自己写过的东西,这个脚手架对自己来说,成长不大,但是给一个没接触过的人来说,是一个锻炼的好机会。对我来说,帮助了一个同学在某一方面成长。

适应中期

  • 后来就开始做业务了,起初我是很兴奋的,觉得自己可以大展身手,说实话,重新接受一个全新的业务,需要一定的时间与成本,这将是以后换工作不可避免的吧,多接触一个行业也是好事。也意识到,要想在业务中体现能力,挺不容易的,需要机会,目前来看,有些遗憾,来这边主要参与的项目暂停了>_<

适应中期

  • 现在在基于业务做多端组件库,也遇到不少问题,打怪中...总之也是自己比较感兴趣的~

这里我的经验是,在进入一个新部门前期,需要发现机会证明自己建立信任学以致用,把自己之前的经验搬过来,精益求精,在之前的基础生做的更好。主动跟同事技术交流,拉近距离,互相学习。我一直觉得一个人的力量有限,一个团队才能做出更多东西,而这个前提是,大家互帮互助

4. 其他

在业务中沉淀了3篇专利,一次年中总结,部门多次分享

我的经验是,多总结、多分享。自己多争取、珍惜机会

四、关于遗憾

  • 最大的遗憾是,年初制定的学一个框架的flag倒的很彻底;由于之前部门是Vue,现在部门是React,导致两者都没有深入学习,严重拖后腿了T_T(像极了什么学科差越来越差)

  • 第二大遗憾,没有机会做一个0-1的项目,在部门A做的事迭代,本来部门B有机会的,突然暂停了>_<

  • 基础知识依旧不扎实,我的详细计划还没列一年就没了🤯

  • 还欠着好几个工作中沉淀总结,偷懒一时爽,一直偷懒一直爽,快要不记得了,得赶紧写!!!!

  • 对新技术没有找到结合业务的机会用进来,还是不够厉害,对于什么技术适用于新业务,还没有把控风险的能力吧

  • 没有晋升...本来年终主管通知准备的,公司调整到明年了(呵呵哒)

五、关于生活

这里就不写了,在学会跟自己相处的路上

六、关于计划

明年总结见~我这次一定做个计划,明年总结用计划复盘(第一个flag立住了!!)

写在最后

一句主管写给我的话,分享给大家~

最终你相信什么,就能成为什么。因为世界上最可怕的两个词,一个叫执着,一个叫认真,认真的人.改变自己,执着的人改变命运。只要在路上,就没有到不了的地方,遇见更好的自己。
希望大家22年都能遇见更好的自己

手写Promise、then、catch、finally、all、race

0、前提

我们想要手写一个Promise,就要遵循 Promise/A+ 规范,业界所有 Promise的类库都遵循这个规范

本篇文章写如何手写promise及其周边方法,每个方法从“定义->案例->实现”的思路展开

阅读本文前建议熟悉阮一峰老师的 ECMAScript 6 入门promise章节,熟悉promise的语法

本文提纲如下,基本覆盖到promise面试考点
image.png

1、基本封装

1.1 基本

定义

  • Promise对象是一个构造函数,用来生成Promise实例
  • Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署
  • resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去
  • reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去
  • Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数
    实现
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'

class Promise {
    constructor(executor) {
        this.status = PENDING
        this.value = undefined // 成功的值
        this.reason = undefined // 失败原因

        // 成功函数
        let resolve = (value) => {
            if (this.status === PENDING) {
                this.value = value
                this.status = RESOLVED
                this.onFulfilledCallbacks.forEach(fn => fn());
            }
        }

        // 失败函数
        let reject = (reason) => {
            if (this.status === PENDING) {
                this.reason = reason
                this.status = REJECTED
                this.onRejectCallbacks.forEach(fn => fn())
            }
        }

        try {
            executor(resolve, reject) // 默认执行器立即执行
        }
        catch (e) {
            reject(e) // 如果立即执行函数发生错误等价于调用失败函数
        }
    }
    
    then (onFulfilled, onReject) {
        // 同步
        if (this.status === RESOLVED) {
            onFulfilled(this.value)
        }
        if (this.status === REJECTED) {
            onReject(this.reason)
        }

        // 异步订阅
        if (this.status === PENDING) { 
            this.onFulfilledCallbacks.push(() => onFulfilled(this.value))
            this.onRejectCallbacks.push(() => onReject(this.reason))
        }
    }
}

module.exports = Promise

1.2 promise参数入参是一个异步操作

现象

在 executor()中传入一个异步操作

const promise = new Promise((resolve, reject) => {
    // 异步的情况
    setTimeout(() => {
        reject(1)
    }, 1000)
})

promise.then(data => {
    console.log(data)
}, err => {
    console.log('err', err)
})
// 输出结果 err 1

实现
promise调用then方法时,当前的promise并没有成功,一直处于pending状态,所以如果当调用 then方法时,当前状态是pending,需要先将成功和失败的回调分别存放起来,在executor()的异步任务被执行时,触发resolve或reject,依次调用成功或失败的回调

class Promise {
    constructor(executor) {
        ...
        // 处理异步
        this.onFulfilledCallbacks = []
        this.onRejectCallbacks = []

        // 成功函数
        let resolve = (value) => {
            if (this.status === PENDING) {
                this.value = value
                this.status = RESOLVED
              ++this.onFulfilledCallbacks.forEach(fn => fn());
            }
        }

        // 失败函数
        let reject = (reason) => {
            if (this.status === PENDING) {
                this.reason = reason
                this.status = REJECTED
              ++this.onRejectCallbacks.forEach(fn => fn())
            }
        }

        try {
            executor(resolve, reject) // 默认执行器立即执行
        }
        catch (e) {
            reject(e) // 如果立即执行函数发生错误等价于调用失败函数
        }
    }
    
    then (onFulfilled, onReject) {
        ...
        // 异步订阅
        if (this.status === PENDING) { 
            this.onFulfilledCallbacks.push(() => onFulfilled(this.value))
            this.onRejectCallbacks.push(() => onReject(this.reason))
        }
    }
}

2、Promise.prototype.then

2.1 then的链式调用

定义

then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法
现象

下面的代码使用then方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数

const p = new Promise((resolve, reject) => {
    resolve(1001)
})

p.then(data => {
    console.log(data)
}, err => {
    console.log('err', err)
}).then(data => {
    console.log(data)
}, err => {
    console.log(err)
})
// 输出结果
// 1001
// undefined

实现

定义一个新的promise实例 promise2,并返回

then函数的返回值可能值一个普通值,也可能是一个对象,因此需要根据x的类型去处理then,引入resolvePromise方法统一处理,具体可看下一节2.2

为什么要用setTimeout:利用eventLoop,宏任务,代码块延迟执行,等new完promise2,不然resolvePromise(promise2, x, resolve, reject)取不到promise2会报错

then (onFulfilled, onReject) {
    let promise2 = new Promise((resolve, reject) => {
        // 同步
        if (this.status === RESOLVED) {
            // 写setTimeout,利用eventLoop,宏任务,代码块延迟执行,等new完promise2
            setTimeout(() => {
                try {
                    let x = onFulfilled(this.value)
                    resolvePromise(promise2, x, resolve, reject)
                } catch (e) {
                    reject(e)
                }
            }, 0)
        }
        if (this.status === REJECTED) {
            setTimeout(() => {
                try {
                    let x = onReject(this.reason)
                    resolvePromise(promise2, x, resolve, reject)
                } catch (e) {
                    reject(e)
                }
            }, 0)
        }

       // 异步订阅
       if (this.status === PENDING) {
           this.onFulfilledCallbacks.push(() => {
               setTimeout(() => {
                   try {
                       let x = onFulfilled(this.value)
                       resolvePromise(promise2, x, resolve, reject)
                   } catch (e) {
                       reject(e)
                   }
               }, 0)
           })
           this.onRejectCallbacks.push(() => {
               setTimeout(() => {
                   try {
                       let x = onReject(this.reason)
                       resolvePromise(promise2, x, resolve, reject)
                   }
                   catch (e) {
                       reject(e)
                   }
               }, 0)
           })
       }
    })
    return promise2
}

2.2 then函数返回值类型

前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用

(1)x跟promise2不能只一个东西

现象

如下代码,promise2的then函数返回值为promise2

输出结果是[TypeError: Chaining cycle detected for promise #<Promise>]

// x跟promise2不能是一个东西
const p = new Promise((resolve, reject) => {
    resolve()
})

let promise2 = p.then(() => {
    return promise2
})

promise2.then(null, err => {
    console.log(err)
})

实现

可以比喻为,A等A买菜回来,这是不可能的,所以直接报错

const resolvePromise = (promise2, x, resolve, reject) => {
    if (promise2 === x) {
        // 判断x的值与promise是否是同一个,如果是同一个,就不用等待了,直接出错即可
        return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
    }
}
(2)x返回值可能是promise也可能是普通值

x可能的值

  • promise
    • 执行x的then方法,返回相应值
  • 普通值
    • 直接resolve
      现象
// then函数的返回值还是一个promise
const p = new Promise((resolve, reject) => {
    resolve(100)
})

p.then(data => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('success')
        }, 100)
    })
}, err => {
    console.log(err)
}).then(data => {
    console.log(data)
}, err => {
    console.log(err)
})
// 输出结果:success

// then函数中的resolve还是promise
const p = new Promise((resolve, reject) => {
    resolve(100)
})

p.then(data => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(new Promise((resolve, reject) => {
                setTimeout(() => {
                    resolve('success')
                }, 0)
            }))
        }, 100)
    })
}, err => {
    console.log(err)
}).then(data => {
    console.log(data)
}, err => {
    console.log(err)
})
// 输出结果:success

实现

先判断x是否是object或者function

    • x上的then
      • 取成功
        • 判断then是否是function
          • 是,执行thenresolve成功函数yreject失败函数r成功函数y可能还是一个promise,递归执行resolvePromise(promise2, y, resolve, reject)
          • 不是,说明x只是普通的对象,比如{then: 1},直接reslove(x)
      • 取失败
        • 直接reject(e)
  • 不是
    • x普通值,直接resolve(x)
const resolvePromise = (promise2, x, reslove, reject) => {
    ...
     if (typeof x === 'object' && x !== null || typeof x === 'function') {
        try {
            let then = x.then // 取then可能这个then属性是通过defineProperty来定义的,可能报错
            if (typeof then === 'function') { // 当有then方法,则认为x是一个promise
                then.call(x, y => {
                     // y可能还是一个promise值,递归,直到解析出来的值是一个普通值
                    resolvePromise(promise2, y, resolve, reject) // 采用promise的成功结果将值下传递
                }, reject => {
                    reject(x) // 采用失败结果将值向下传递
                })
            } else {
                resolve(x) // x是一个普通对象,比如{then: 1}
            }
        } catch (e) {
            reject(e)
        }
    } else {
        resolve(x) // x是一个普通值,直接成功即可
    }
}
(3)then的两个参数是可选的,如果不写默认resolve(data)

定义

then方法的第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数,它们都是可选的
现象

如下代码,p.then().then()省略参数

  • reslove时,最后一个thenresolve输出data依旧可以获取到数据

    • 其实相当于p.then(data => {return data}).then(data => {return data})
  • reject时,最后一个thenreject输出err依旧可以获取数据

    • 其实相当于p.then(null, err => { throw err }).then(null, err => { throw err })
// 案例
// then函数中的resolve和reject是可选参数
const p = new Promise((resolve, reject) => {
    resolve(100)
})

p.then().then().then(data => {
    console.log(data)
})

// p.then(data => {return data}).then(data => {return data}).then(data => {
//     console.log(data)
// })

// p.then(null, err => { throw err }).then(null, err => { throw err }).then(null, err => {
//     console.log('err', err)
// })

// 输出结果:
// resolbe 100
// reject err 100

实现

then(onFulfilled, onReject){
    // onFulfilled、onReject是可选参数
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : data => data
    onReject = typeof onReject === 'function' ? onReject : err => { throw err }
    ...
}

3、检测Promise是否符合规范

Promise/A+规范提供了一个专门的测试脚本,可以测试所编写的代码是否符合Promise/A+的规范

step1 全局安装包promises-aplus-tests

npm i -g promises-aplus-tests

step2 在primes文件最下方写入以下内容

Promise.defer = Promise.deferred = function () {
    let dfd = {}
    dfd.promise = new Promise((resolve, reject) => {
        dfd.resolve = resolve
        dfd.reject = reject
    })
    return dfd
}

step3 执行命令检测

promises-aplus-tests promise/promise.js

可以看到检测全部通过
image.png

4、Promise.all

定义

  • Promise.all()方法用于将多个Promise实例,包装成一个新的Promise实例
  • Promise.all()方法接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理
  • p的状态由p1p2p3决定,分成两种情况
    • 只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。
    • 只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数
const p = Promise.all([p1, p2, p3]);

现象

Promise.all([1, 2, 3, 4]).then(data => {
    console.log(data)
})
// 输出 [ 1, 2, 3, 4 ]

Promise.all([1, 2, new Promise((resolve) => { resolve(300) }), 4]).then(data => {
    console.log(data)
})
// 输出 [ 1, 2, 300, 4 ]

实现

const isPromise = (data) => {
    if (typeof data === 'object' && data !== null || typeof data === 'function') {
        if (typeof data.then === 'function') {
            return true
        }
    }
    return false
}

Promise.all = function (promiseArr) {
    return new Promise((resolve, reject) => {
        let arr = []
        let index = 0

        function processData (i, data) {
            arr[i] = data
            if (++index === promiseArr.length) { // 不能用arr.length === promiseArr.length;因为promiseArr中有异步promise的话,arr不会按照顺序被塞进返回值
                resolve(arr)
            }
        }
        for (let i = 0; i < promiseArr.length; i++) {
            let current = promiseArr[i]
            if (isPromise(current)) {
                // 如果是promis,执行then
                current.then(data => {
                    processData(i, data)
                }, err => {
                    console.log(err)
                    reject(err)
                })
            } else {
                // 如果不是promise,直接返回
                processData(i, current)
            }
        }
    })
}

5、Promise.resolve & Promise.reject

resolve

将现有对象转为Promise对象

Promise.resolve = function (value) {
    if (value instanceof Promise) return value
    return new Promise(resolve => resolve(value))
}

reject

会返回一个新的 Promise 实例,该实例的状态为rejected

Promise.reject = function (reason) {
    return new Promise(_, reject => reject(reason))
}

6、Promise.prototype.finally

定义

  • finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作
  • finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果

finally本质上是then方法的特例

promise
.finally(() => {
  // 语句
});

// 等同于
promise
.then(
  result => {
    // 语句
    return result;
  },
  error => {
    // 语句
    throw error;
  }
);

现象

const p = new Promise((resolve, reject) => {
    resolve(100)
})

p.finally(() => {
    console.log('finally')
}).then(data => {
    console.log(data)
}, err => {
    console.log('err', err)
})
// 输出结果
// finally
// 100

const p = new Promise((resolve, reject) => {
    reject(100)
})

p.finally(() => {
    console.log('finally')
}).then(data => {
    console.log(data)
}, err => {
    console.log('err', err)
})
// 输出结果
// finally
// err 100


const p = new Promise((resolve) => {
    resolve(100)
})

p.finally(() => {
    console.log('finally')
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve()
        }, 4000)
    })
}).then(data => {
    console.log(data)
})
// 输出结果
// finally
// 等四秒钟
// 100

实现

Promise.prototype.finally = function(callback){
    return this.then(
        data => Promise.resolve(callback()).then(() => data),
        err => Promise.resolve(callback()).then(() => {throw err} )
    )
}

7、Promise.prototype.catch

Promise.prototype.catch()方法是.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数
现象

const p = new Promise((_, resolve) => {
    throw 'err'
}).catch((e) => {
    console.log(e)
})
// 输出 err

实现

Promise.prototype.catch  = function (onRejected) {
    return this.then(null, onRejected);
}

8、Promise.race

现象
注意,下面代码第二个例子输出的是300,而非1

Promise.race([1, new Promise((resolve) => { resolve(300) }), 2, 4]).then(data => {
    console.log(data)
})
// 输出 1


Promise.race([new Promise((resolve) => { resolve(300) }), 1, 2, 4]).then(data => {
    console.log(data)
})
// 输出 300

实现

Promise.race = function (promiseArr) {
    return new Promise((resolve, reject) => {
        for (let i = 0; i < promiseArr.length; i++) {
            const current = promiseArr[i];
            Promise.resolve(current).then(resolve, reject);
        }
    });
}

写在最后

本篇文章所有的代码在github/zxyue25

参考

原创:Vue/React项目中有封装过axios吗?怎么封装的?

axios-ajax

image.png

一、什么是axios,有什么特性

描述

axios是一个基于promiseHTTP库,可以用在浏览器或者node.js中。本文围绕XHR。

axios提供两个http请求适配器,XHR和HTTP。XHR的核心是浏览器端的XMLHttpRequest对象;HTTP的核心是node的http.request方法。

特性

  • 从浏览器中创建XMLHttpRequests
  • 从node.js创建http请求
  • 支持promise API
  • 拦截请求与响应
  • 转换请求数据与响应数据
  • 取消请求
  • 自动转换JSON数据
  • 客户端支持防御XSRF

背景

Vue2.0起,尤大宣布取消对 vue-resource 的官方推荐,转而推荐 axios。现在 axios 已经成为大部分 Vue 开发者的首选,目前在github上有87.3k star。axios的熟练使用和基本封装也成为了vue技术栈系列必不可少的一部分。如果你还不了解axios,建议先熟悉
axios官网文档

基本使用

安装

npm install axios -S

使用

import axios from 'axios'
// 为给定ID的user创建请求 
axios.get('/user?ID=12345')   
    .then(function (response) {     
        console.log(response);   
    })   
    .catch(function (error) {    
        console.log(error);   
    });  
// 上面的请求也可以这样做 
axios.get('/user', {     
    params: {ID: 12345}})   
    .then(function (response) {     
        console.log(response);   
    })   
    .catch(function (error) {     
        console.log(error);   
    });

二、Vue项目中为什么要封装axios

axios的API很友好,可以在项目中直接使用。但是在大型项目中,http请求很多,且需要区分环境,
每个网络请求有相似需要处理的部分,如下,会导致代码冗余,破坏工程的可维护性扩展性

axios('http://www.kaifa.com/data', {
  // 配置代码
  method: 'GET',
  timeout: 3000,
  withCredentials: true,
  headers: {
    'Content-Type': 'application/json'
  },
  // 其他请求配置...
})
.then((data) => {
  // todo: 真正业务逻辑代码
  console.log(data);
}, (err) => {
  // 错误处理代码  
  if (err.response.status === 401) {
  // handle authorization error
  }
  if (err.response.status === 403) {
  // handle server forbidden error
  }
  // 其他错误处理.....
  console.log(err);
});
  • 环境区分
  • 请求头信息
  • 请求类型
  • 请求超时时间
    • timeout: 3000
  • 允许携带cookie
    • withCredentials: true
  • 响应结果处理
    • 登录校验失败
    • 无权限
    • 成功
  • ...

三、Vue项目中如何封装axios

axios文件封装在目录src/utils/https.js,对外暴露callApi函数

1、环境区分

callApi函数暴露prefixUrl参数,用来配置api url前缀,默认值为api

// src/utils/https.js
import axios from 'axios'

export const callApi = ({
  url,
  ...
  prefixUrl = 'api'
}) => {
  if (!url) {
    const error = new Error('请传入url')
    return Promise.reject(error)
  }
  const fullUrl = `/${prefixUrl}/${url}`
  
  ...
  
  return axios({
    url: fullUrl,
    ...
  })
}

看到这里大家可能会问,为什么不用axios提供的配置参数baseURL,原因是baseURL会给每个接口都加上对应前缀,而项目实际场景中,存在一个前端工程,对应多个服务的场景。需要通过不用的前缀代理到不同的服务,baseURL虽然能实现,但是需要二级前缀,不优雅,且在使用的时候看不到真实的api地址是啥,因为代理前缀跟真实地址混合在一起了

使用baseURL,效果如下

image.png

函数设置prefixUrl参数,效果如下
image.png

利用环境变量webpack代理(这里用vuecli3配置)来作判断,用来区分开发、测试环境。生产环境同理配置nginx代理

// vue.config.js
const targetApi1 = process.env.NODE_ENV === 'development' ? "http://www.kaifa1.com" : "http://www.ceshi1.com"

const targetApi2 = process.env.NODE_ENV === 'development' ? "http://www.kaifa2.com" : "http://www.ceshi2.com"
module.exports = {
    devServer: {
        proxy: {
            '/api1': {
                target: targetApi1,
                changeOrigin: true,
                pathRewrite: {
                    '/api1': ""
                }
            },
            '/api2': {
                target: targetApi2,
                changeOrigin: true,
                pathRewrite: {
                    '/api2': ""
                }
            },
        }
    }
}

2、请求头

常见以下三种

(1)application/json

参数会直接放在请求体中,以JSON格式的发送到后端。这也是axios请求的默认方式。这种类型使用最为广泛。

image

(2)application/x-www-form-urlencoded

请求体中的数据会以普通表单形式(键值对)发送到后端。

image

(3)multipart/form-data

参数会在请求体中,以标签为单元,用分隔符(可以自定义的boundary)分开。既可以上传键值对,也可以上传文件。通常被用来上传文件的格式。

image
callApi函数暴露contentType参数,用来配置请求头,默认值为application/json; charset=utf-8

看到这里大家可以会疑惑,直接通过options配置headers不可以嘛,答案是可以的,可以看到newOptions的取值顺序,先取默认值,再取配置的options,最后取contentTypecontentType能满足绝大部分场景,满足不了的场景下可用options配置

通过options配置headers,写n遍headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'};而通过contentType配置,传参json || urlencoded || multipart即可

contentType === urlencoded时,qs.stringify(data)

image.png

// src/utils/https.js
import axios from 'axios'
import qs from 'qs'

const contentTypes = {
  json: 'application/json; charset=utf-8',
  urlencoded: 'application/x-www-form-urlencoded; charset=utf-8',
  multipart: 'multipart/form-data',
}

const defaultOptions = {
  headers: {
    Accept: 'application/json',
    'Content-Type': contentTypes.json,
  }
}

export const callApi = ({
  url,
  data = {},
  options = {},
  contentType = 'json', // json || urlencoded || multipart
  prefixUrl = 'api'
}) => {

  ...
  
  const newOptions = {
    ...defaultOptions,
    ...options,
    headers: {
      'Content-Type': options.headers && options.headers['Content-Type'] || contentTypes[contentType],
    },
  }
  
  const { method } = newOptions

  if (method !== 'get' && method !== 'head') {
    if (data instanceof FormData) {
      newOptions.data = data
      newOptions.headers = {
        'x-requested-with': 'XMLHttpRequest',
        'cache-control': 'no-cache',
      }
    } else if (options.headers['Content-Type'] === contentTypes.urlencoded) {
      newOptions.data = qs.stringify(data)
    } else {
      Object.keys(data).forEach((item) => {
        if (
          data[item] === null ||
          data[item] === undefined ||
          data[item] === ''
        ) {
          delete data[item]
        }
      })
      // 没有必要,因为axios会将JavaScript对象序列化为JSON
      // newOptions.data = JSON.stringify(data);
    }
  }
  
  return axios({
    url: fullUrl,
    ...newOptions,
  })
}

注意,在application/json格式下,JSON.stringify处理传参没有意义,因为axios会将JavaScript对象序列化为JSON,也就说无论你转不转化都是JSON

3、请求类型

请求类型参数为axiosoptionsmethod字段,传入对应的请求类型如postget等即可

不封装,使用原生axios时,发送带参数的get请求如下:

// src/service/index.js
import { callApi } from '@/utils/https';

export const delFile = (params) => callApi({
  url: `file/delete?systemName=${params.systemName}&menuId=${params.menuId}&appSign=${params.appSign}`,
  option: {
    method: 'get',
  },
});

// 或者
export const delFile = (params) => callApi({
  url: 'file/delete',
  option: {
    method: 'get',
    params
  },
});

官方文档如下

image.png

callApi函数暴露method参数,用来配置请求类型,默认值为get

当请求类型为get时,将callApi函数暴露的data参数,设置为options.params,从而参数自动拼接到url地址之后

// src/utils/https.js 
import axios from 'axios' 

export const callApi = ({
  url,
  data = {},
  method = 'get',
  options = {},
  ...
  prefixUrl = 'api'
}) => {
    ...
    const newOptions = {
        ...,
        ...options,
        method
    }
    ...
    if(method === 'get'){
        newOptions.params = data
    }
    ... 
    
    return axios({ 
        url: fullUrl, 
        ...newOptions,
    }) 
}

4、请求超时时间

// src/utils/https.js
const defaultOptions = {
  timeout: 15000,
}

5、允许携带cookie

// src/utils/https.js
const defaultOptions = {
  withCredentials: true,
}

6、响应结果处理

通过.then.catch()处理

这块需要跟服务端约定接口响应全局码,从而统一处理登录校验失败无权限成功等结果

比如有些服务端对于登录校验失败无权限成功等返回的响应码都是200,在响应体内返回的状态码分别是20001,20002,10000,在then()中处理

比如有些服务端对于登录校验失败无权限成功响应码返回401,403,200,在catch()中处理

// src/utils/https.js
import axios from 'axios'
import { Message } from "element-ui";

export const callApi = ({
  ...
}) => {

 ...
 
 return axios({
    url: fullUrl,
    ...newOptions,
  })
    .then((response) => {
      const { data } = response
      if (data.code === 'xxx') {
        // 与服务端约定
        // 登录校验失败
      } else if (data.code === 'xxx') {
        // 与服务端约定
        // 无权限
        router.replace({ path: '/403' })
      } else if (data.code === 'xxx') {
        // 与服务端约定
        return Promise.resolve(data)
      } else {
        const { message } = data
        if (!errorMsgObj[message]) {
          errorMsgObj[message] = message
        }
        setTimeout(debounce(toastMsg, 1000, true), 1000)
        return Promise.reject(data)
      }
    })
    .catch((error) => {
      if (error.response) {
        const { data } = error.response
        const resCode = data.status
        const resMsg = data.message || '服务异常'
        // if (resCode === 401) { // 与服务端约定
        //     // 登录校验失败
        // } else if (data.code === 403) { // 与服务端约定
        //     // 无权限
        //     router.replace({ path: '/403' })
        // }
        if (!errorMsgObj[resMsg]) {
          errorMsgObj[resMsg] = resMsg
        }
        setTimeout(debounce(toastMsg, 1000, true), 1000)
        const err = { code: resCode, respMsg: resMsg }
        return Promise.reject(err)
      } else {
        const err = { type: 'canceled', respMsg: '数据请求超时' }
        return Promise.reject(err)
      }
    })
}

上述方案在Message.error(xx)时,当多个接口返回的错误信息一致时,会存在重复提示的问题,如下图

image.png

优化方案,利用防抖,实现错误提示一次,更优雅

四、完整封装及具体使用

代码可访问github

axios-ajax完整封装

// src/utils/https.js
import axios from 'axios'
import qs from 'qs'
import { debounce } from './debounce'

const contentTypes = {
  json: 'application/json; charset=utf-8',
  urlencoded: 'application/x-www-form-urlencoded; charset=utf-8',
  multipart: 'multipart/form-data',
}

function toastMsg() {
  Object.keys(errorMsgObj).map((item) => {
    Message.error(item)
    delete errorMsgObj[item]
  })
}

let errorMsgObj = {}

const defaultOptions = {
  withCredentials: true, // 允许把cookie传递到后台
  headers: {
    Accept: 'application/json',
    'Content-Type': contentTypes.json,
  },
  timeout: 15000,
}

export const callApi = ({
  url,
  data = {},
  method = 'get',
  options = {},
  contentType = 'json', // json || urlencoded || multipart
  prefixUrl = 'api',
}) => {
  if (!url) {
    const error = new Error('请传入url')
    return Promise.reject(error)
  }
  const fullUrl = `/${prefixUrl}/${url}`

  const newOptions = {
    ...defaultOptions,
    ...options,
    headers: {
      'Content-Type':
        (options.headers && options.headers['Content-Type']) ||
        contentTypes[contentType],
    },
    method,
  }
  if (method === 'get') {
    newOptions.params = data
  }

  if (method !== 'get' && method !== 'head') {
    newOptions.data = data
    if (data instanceof FormData) {
      newOptions.headers = {
        'x-requested-with': 'XMLHttpRequest',
        'cache-control': 'no-cache',
      }
    } else if (newOptions.headers['Content-Type'] === contentTypes.urlencoded) {
      newOptions.data = qs.stringify(data)
    } else {
      Object.keys(data).forEach((item) => {
        if (
          data[item] === null ||
          data[item] === undefined ||
          data[item] === ''
        ) {
          delete data[item]
        }
      })
      // 没有必要,因为axios会将JavaScript对象序列化为JSON
      // newOptions.data = JSON.stringify(data);
    }
  }

  axios.interceptors.request.use((request) => {
    // 移除起始部分 / 所有请求url走相对路径
    request.url = request.url.replace(/^\//, '')
    return request
  })

  return axios({
    url: fullUrl,
    ...newOptions,
  })
    .then((response) => {
      const { data } = response
      if (data.code === 'xxx') {
        // 与服务端约定
        // 登录校验失败
      } else if (data.code === 'xxx') {
        // 与服务端约定
        // 无权限
        router.replace({ path: '/403' })
      } else if (data.code === 'xxx') {
        // 与服务端约定
        return Promise.resolve(data)
      } else {
        const { message } = data
        if (!errorMsgObj[message]) {
          errorMsgObj[message] = message
        }
        setTimeout(debounce(toastMsg, 1000, true), 1000)
        return Promise.reject(data)
      }
    })
    .catch((error) => {
      if (error.response) {
        const { data } = error.response
        const resCode = data.status
        const resMsg = data.message || '服务异常'
        // if (resCode === 401) { // 与服务端约定
        //     // 登录校验失败
        // } else if (data.code === 403) { // 与服务端约定
        //     // 无权限
        //     router.replace({ path: '/403' })
        // }
        if (!errorMsgObj[resMsg]) {
          errorMsgObj[resMsg] = resMsg
        }
        setTimeout(debounce(toastMsg, 1000, true), 1000)
        const err = { code: resCode, respMsg: resMsg }
        return Promise.reject(err)
      } else {
        const err = { type: 'canceled', respMsg: '数据请求超时' }
        return Promise.reject(err)
      }
    })
}
// src/utils/debounce.js
export const debounce = (func, timeout, immediate) => {
  let timer

  return function () {
    let context = this
    let args = arguments

    if (timer) clearTimeout(timer)
    if (immediate) {
      var callNow = !timer
      timer = setTimeout(() => {
        timer = null
      }, timeout)
      if (callNow) func.apply(context, args)
    } else {
      timer = setTimeout(function () {
        func.apply(context, args)
      }, timeout)
    }
  }
}

具体使用

api管理文件在目录src/service下,index.js文件暴露其他模块,其他文件按功能模块划分文件

get请求带参数
image.png
自定义前缀代理不同服务
image.png
文件类型处理
image.png

五、总结

axios封装没有一个绝对的标准,且需要结合项目中实际场景来设计,但是毋庸置疑,axios-ajax的封装是非常有必要的

JS 将字符串格式html代码插入页面中

原文:https://blog.csdn.net/MFWSCQ/article/details/105817067
使用原生 JS 经常会遇到将 html 字符串往页面的的某个节点插入,这里介绍几种插入方式

插入方式
一、使用 innerHTML 方式
这种方式是将你的 html 结构的字符串直接给某个节点的 innerHTML 属性:

var name = 'leo';
var htmlStr = <div><span>${name}</span></div>

document.querySelector('.box').innerHTML = htmlStr;
上面的innerHTML方法是将目标元素的内部所有内容替换,不能追加和插入某个节点的前后位置。

二、使用 appendChild 或者 insertBefore 的方式
这种方式的参数必须是 node 节点。所以需要我们先将 html 字符串转换为 node 节点

将字符串格式的 html 转为 node 插入文档
一、使用 DOMParser
DOMParser 接口提供了将 XML 或 HTML 源代码从字符串解析为DOM的功能 Document。DOMParser() 构造函数新建一个 DOMParser 对象实例,可以通过这个对象的 parseFromString() 方法将字符串解析为 DOM 对象。

DOMParser 实例的 parseFromString 方法可以用来直接将字符串转换为document 文档对象。有了document之后,我们就可以利用各种 DOM Api来进行操作了。

function createDocument(txt) {
const template = <div class='child'>${txt}</div>;
let doc = new DOMParser().parseFromString(template, 'text/html');
let div = doc.querySelector('.child');
return div;
}

const container = document.getElementById('container');
container.appendChild(createDocument('hello'));
二、使用 DocumentFragment
document.createRange() 返回一个 range 对象,range 对象表示文档中的连续范围区域,如用户在浏览器窗口用鼠标拖动选择的区域,利用 document.createRange().createContextualFragment 方法,我们可以直接将字符串转化为 DocumentFragment 对象

var name = 'leo';
var template = <li>${name}</li>;
var frag = document.createRange().createContextualFragment(faceInfoItem);
var list = document.querySelector('.box ul');

//如果使用 appendChild
list.appendChild(frag);

//如果使用 insertBefore ,insertBefore 即使第二个参数为 null 也能插入进去,就像append了一个元素
list.insertBefore(frag,list.firstElementChild);

利用documentFragment批量插入节点,当我们每次单独创建节点并插入文档时会造成很大的性能浪费,可以先把节点放入documentFragment 中 最后统一放入文档中。

var temp = function(id){
return <li><span>now id is ${id}</span></li>;
}

var createFrag = function(temp){
return document.createRange().createContextualFragment(temp);
}

var box = document.querySelector('.box ul');
var docFrag = document.createDocumentFragment();

for(let i=0;i<100;i++){
docFrag.appendChild(createFrag(temp(i)));
}

box.appendChild(docFrag);

利用 documentFragment 和 innerHTML 封装一个 类似于 jquery 的 append 方法,既可以插入节点,又可以插入字符串:

function append(container,text){
if(typeof text === 'object'){
container.appendChild(text);
return ;
}
let box = document.createElement('div');
let frag = document.createDocumentFragment();
box.innerHTML = text;
while(box.firstElementChild){
frag.appendChild(box.firstElementChild);
}
container.appendChild(frag);
}

//测试:
//1.加入字符串
var box = document.querySelector('.box ul');
var temp = <li>我是li3<span>6666</span></li> <li>我是li2</li> <li>我是li1</li>;
var arr = [1,22,4,5,6,6,7,8,90,'123','666666'];
var lis = '';
arr.forEach(item=>{
lis+= <li>${item}</li>;
})

append(box,lis);

//2.插入元素节点
var li = document.createElement('li');
li.appendChild(document.createTextNode('我是text node 节点'))
append(box,lis);

原创:别再用performance计算首屏时间了!!

一、背景

前段时间备战双十一前期,线上项目的性能问题引起了我们的重视

公司内部是有统一的性能监控平台的,我们的项目也都统一接入了监控平台,但是这个时间的计算方式我们是不清楚的,于是花时间深入调研了一番

调研后的结果是,其他时间的计算方式(比如网路请求时间,首包时间...)是比较清晰的,指路,除了首屏时间,业内没有一个统一的标准

调研后首屏时间的计算方式还是很硬核的,最近得空记录分享出来~

本篇文章讲一种前端首屏时间的计算方案,偏算法实现,重点是**,看懂就等于赚到!

image.png

二、什么是首屏时间

首屏时间:也称用户完全可交互时间,即整个页面首屏完全渲染出来,用户完全可以交互,一般首屏时间小于页面完全加载时间,该指标值可以衡量页面访问速度

1、首屏时间 VS 白屏时间

这两个完全不同的概念,白屏时间是小于首屏时间的
白屏时间:首次渲染时间,指页面出现第一个文字或图像所花费的时间

2、为什么 performance 直接拿不到首屏时间

随着 Vue 和 React 等前端框架盛行,Performance 已无法准确的监控到页面的首屏时间

因为 DOMContentLoaded 的值只能表示空白页(当前页面 body 标签里面没有内容)加载花费的时间

浏览器需要先加载 JS , 然后再通过 JS 来渲染页面内容,这个时候单页面类型首屏才算渲染完成

三、常见计算方式

  • 用户自定义打点—最准确的方式(只有用户自己最清楚,什么样的时间才算是首屏加载完成)
    • 缺点:侵入业务,成本高
  • 粗略的计算首屏时间: loadEventEnd - fetchStart/startTime 或者 domInteractive - fetchStart/startTime
  • 通过计算首屏区域内的所有图片加载时间,然后取其最大值
  • 利用 MutationObserver 接口,监听 document 对象的节点变化

四、我们的计算方案

利用 MutationObserver 接口,监听 DOM 对象的节点变化

提示:算法比较复杂,文章尽量用通俗易懂的方式表达,分析过程尽量简化,实际情况比这个复杂
首先,假设页面DOM最终结构如下,页面dom深度为3

<body>
  <div>
    <div>
      <div>1</div>
      <div>2</div>
    </div>
    <div>3</div>
    <div style="display: none;">4</div>
  </div>
  <ul>
    <li>1</li>
    <li>2</li>
  </ul>
</body>

1、初始化 MutationObserver 监听

初始化代码如下

  • 如果当前浏览器不支持 MutationObserver 放弃上报
  • this.startTime取的window.performance.getEntriesByType('navigation')[0].startTime,即开始记录性能时间
  • this.observerData 数组用来记每次录DOM变化的时间以及变化的得分(变化的剧烈程度)
function mountObserver () {
    if (!window.MutationObserver) {
      // 不支持 MutationObserver 的话
      console.warn('MutationObserver 不支持,首屏时间无法被采集');
      return;
    }
    
    // 每次 dom 结构改变时,都会调用里面定义的函数
    const observer = new window.MutationObserver(() => {
      const time = getTimestamp() - this.startTime; // 当前时间 - 性能开始计算时间
      
      const body = document.querySelector('body');
      let score = 0;
      
      if (body) {
        score = traverseEl(body, 1, false);
        this.observerData.push({ score, time });
      } else {
        this.observerData.push({ score: 0, time });
      }
    });
    
    // 设置观察目标,接受两个参数: target:观察目标,options:通过对象成员来设置观察选项
    // 设为 childList: true, subtree: true 表示用来监听 DOM 节点插入、删除和修改时
    observer.observe(document, { childList: true, subtree: true });
    
    this.observer = observer;
 
   
    if (document.readyState === 'complete') {
      // MutationObserver监听的最大时间,10秒,超过 10 秒将强制结束
      this.unmountObserver(10000);
    } else {
      win.addEventListener(
        'load',
        () => {
          this.unmountObserver(10000);
        },
        false
      );
    }
  }

Mutation 第一次监听到DOM变化时,DOM结构如下,可以看到div标签渲染出来了

<body>
  <div>
    <div>
      <div>1</div>
      <div>2</div>
    </div>
    <div>3</div>
    <div style="display: none;">4</div>
  </div>
</body>

遍历 body 下的元素,通过方法 traverseEl 计算每次监听到 DOM 变化时得分,算法如下

2、计算 DOM 变化时得分

计算函数 traverseEl 如下

  • body 元素开始递归计算,第一次调用为 traverseEl(body, 1, false)
  • 排除无用的element节点,如 scriptstylemetahead
  • layer表示当前DOM层数,每层的得分等于1 + (层数 * 0.5) + 该层children的所有得分
  • 如果元素高度超出屏幕可视高度直接返回 0 分,即第一次调用时,如果元素高度已经超过屏幕可视高度了,直接返回 0
/**
 * 深度遍历 DOM 树
 * 算法分析
 * 首次调用为 traverseEl(body, 1, false);
 * @param element 节点
 * @param layer 层节点编号,从上往下,依次表示层数
 * @param identify 表示每个层次得分是否为 0
 * @returns {number} 当前DOM变化得分
 */
function traverseEl (element, layer, identify) {
  // 窗口可视高度
  const height = win.innerHeight || 0;
  let score = 0;
  const tagName = element.tagName;

  if (
    tagName !== 'SCRIPT' &&
    tagName !== 'STYLE' &&
    tagName !== 'META' &&
    tagName !== 'HEAD'
  ) {
    const len = element.children ? element.children.length : 0;

    if (len > 0) {
      for (let children = element.children, i = len - 1; i >= 0; i--) {
        score += traverseEl(children[i], layer + 1, score > 0);
      }
    }

    // 如果元素高度超出屏幕可视高度直接返回 0 分
    if (score <= 0 && !identify) {
      if (
        element.getBoundingClientRect &&
        element.getBoundingClientRect().top >= height
      ) {
        return 0;
      }
    }
    score += 1 + 0.5 * layer;
  }
  return score;
}

第一次DOM变化计算分数score = traverseEl(body, 1, false)如下,可以看到此次变化得分是8.5
得分保存到this.observerDatathis.observerData.push({ score, time })

body =》 traverseEl(body, 1, false); score = 8.5;
   div =》 traverseEl(div, 2, false); score = 8.5;
     div =》 traverseEl(div, 3, false);  score = 6;
       div  =》 traverseEl(div, 4, false);  score = 3;
       div  =》 traverseEl(div, 4, false);  score = 3;
     div =》 traverseEl(div, 3, false);  score = 2.5;
     div =》 traverseEl(div, 3, false);  score = 0;

Mutation 第二次监听到 DOM 变化时,可以看到ul标签也渲染出来了

<body>
  <div>
    <div>1</div>
    <div>2</div>
    <div style="display: none;">3</div>
  </div>
  <ul>
    <li>1</li>
    <li>2</li>
  </ul>
</body>

同样计算分数score = traverseEl(body, 1, false),可以看到此次变化得分是10
把得分保存到数组this.observerData

body =》 traverseEl(body, 1, false); score = 10;
   div =》 traverseEl(div, 2, false); score = 5;
     div =》 traverseEl(div, 3, false);  score = 2.5;
     div =》 traverseEl(div, 3, false);  score = 2.5;
     div =》 traverseEl(div, 3, false);  score = 0;
   ul =》 traverseEl(div, 2, false); score = 5;
     li =》 traverseEl(div, 3, false);  score = 2.5;
     li =》 traverseEl(div, 3, false);  score = 2.5;

到此就拿到了一个 DOM 变化的数组 this.observerData

实际上会多次调用 Mutation 监听,会有重复分数的项

3、去掉 DOM 被删除情况的监听

首先删除掉后一个小于前一个的元素,即去掉 DOM 被删除情况的监听,因为页面渲染过程中如有大量 DOM 节点被删除,由于得分小,则会忽略掉
比如 [3,4,2,3,1,5,3],结果为 [3,4,5]

/**
 * @param observerData
 * @returns {*}
 */
function removeSmallScore (observerData) {
  for (let i = 1; i < observerData.length; i++) {
    if (observerData[i].score < observerData[i - 1].score) {
      observerData.splice(i, 1);
      return removeSmallScore(observerData);
    }
  }
  return observerData;
}

4、取 DOM变化最大 时间点为首屏时间

依次遍历 observerData,如果 下一个得分score前一个得分score 差值大于 data.rate 则表示后面有新的 dom 元素渲染到页面中,则取下一个 time

这样处理,可以排除有动画的元素渲染,或者轮播图等,更精准的计算首屏渲染时间

所以不能直接取最后一个元素时间,即 observerData[observerData.length-1].score

function getfirstScreenTime = {
    this.observerData = removeSmallScore(this.observerData);

    let data = null;
    const { observerData } = this;

    for (let i = 1; i < observerData.length; i++) {
      if (observerData[i].time >= observerData[i - 1].time) {
        const scoreDiffer =
          observerData[i].score - observerData[i - 1].score;
        if (!data || data.rate <= scoreDiffer) {
          data = { time: observerData[i].time, rate: scoreDiffer };
        }
      }
    }

    if (data && data.time > 0 && data.time < 3600000) {
      // 首屏时间
      this.firstScreenTime = data.time;
    }
}

5、异常情况下的处理

页面关闭时如果没有上报,立即上报

  • window 监听 beforeunload事件(当浏览器窗口关闭或者刷新时,会触发beforeunload事件)
  • this.calcFirstScreenTime,计算首屏时间状态,分为 initpending、和 finished 三个状态
  • 当页面关闭时,如果 this.calcFirstScreenTime = pending,则触发 unmountObserver 立即上报,并且卸载事件
window.addEventListener('beforeunload', this.unmountObserverListener);

const unmountObserverListener = () => {

    if (this.calcFirstScreenTime === 'pending') {
      this.unmountObserver(0, true);
    }

    if(!isIE()){
      window.removeEventListener('beforeunload', this.unmountObserverListener);
    }
};

6、销毁 MutationObserver

我们看看 卸载MutationObserver 的时候又做了啥,该方法为 unmountObserver

该方法中会判断是否卸载 if (immediately || this.compare(delayTime)),如返回 true 则立即卸载,并给出最终计算的时间;如果返回 false ,500 毫秒后轮询 unmountObserver

this.observer.disconnect() 停止观察变动,MutationObserver.disconnect()

/**
 * @param delayTime 延迟的时间
 * @param immediately 指是否立即卸载
 * @returns {number}
 */
function unmountObserver (delayTime, immediately) {
    if (this.observer) {
      if (immediately || this.compare(delayTime)) {
        // MutationObserver停止观察变动
        this.observer.disconnect();
        this.observer = null;

        this.getfirstScreenTime()

        this.calcFirstScreenTime = 'finished';
      } else {
        setTimeout(() => {
          this.unmountObserver(delayTime);
        }, 500);
      }
    }
}

// * 如果超过延迟时间 delayTime(默认 10 秒),则返回 true
// * _time - time > 2 * OBSERVE_TIME; 表示当前时间与最后计算得分的时间相比超过了 1000 毫秒,则说明页面 DOM 不再变化,返回 true
function compare (delayTime) {
    // 当前所开销的时间
    const _time = Date.now() - this.startTime;
    // 取最后一个元素时间 time
    const { observerData } = this;
    const time =
      (
        observerData &&
        observerData.length &&
        observerData[observerData.length - 1].time) ||
      0;
    return _time > delayTime || _time - time > 2 * 500;
}

原创:如何在前端团队快速落地代码规范

本篇文章讲怎么在前端团队快速制定并落地代码规范!!!
干货,拿走这个仓库

image.png

一、背景

9月份换了一个新部门,部门成立时间不长,当时组内还没有统一的代码规范(部分工程用了规范,部分没有,没有统一的收口)

小组的技术栈框架有VueReactTaroNuxt,用Typescript,算是比较杂了,结合到部门后续还可能扩展其他技术栈,我们从0-1实现了一套通用的代码规范

到现在小组内也用起来几个月了,整个过程还算是比较顺利且快速,最近得空分享出来~

⚠️本篇文章不会讲基础的具体的规范,而是从实践经验讲怎么制定规范以及落地规范

image.png

二、为什么要代码规范

就不说了...大家懂的~
image.png

不是很了解的话,指路

三、确定规范范围

首先,跟主管同步,团队需要一个统一的规范,相信主管也在等着人来做起来

第一步收集团队的技术栈情况,确定规范要包括的范围

把规范梳理为三部分ESLintStyleLintCommitLint,结合团队实际情况分析如下

  • ESLint:团队统一用的TypeScript,框架用到了VueReactTaro、还有Nuxt
  • StyleLint:团队统一用的Less
  • CommitLint:git代码提交规范
    image.png
    当然,还需考虑团队后续可能会扩展到的技术栈,以保证实现的时候确保可扩展性

四、调研业内实现方案

常见以下3种方案

  1. 团队制定文档式代码规范,成员都人为遵守这份规范来编写代码

    靠人来保证代码规范存在不可靠,且需要人为review代码不规范,效率低

  2. 直接使用业内已有成熟规范,比如css使用StyleLint官方推荐规范stylelint-config-standard、stylelint-order,JavaScript使用ESLint推荐规范eslint:recommended等

    a) 开源规范往往不能满足团队需求,可拓展性差; b) 业内提供的规范都是独立的(stylelint只提供css代码规范,ESLint只提供JavaScript规范),是零散的,对于规范初始化或升级存在成本高、不可靠问题(每个工程需要做人为操作多个步骤)

  3. 基于StyleLint、ESLint制定团队规范npm包,使用团队制定规范库

    a) 该方案解决可扩展性差的问题,但是第二点中的(b)问题依旧存在

五、我们的技术方案

整体技术思路图如下图,提供三个基础包@jd/stylelint-config-selling@jd/eslint-config-selling@jd/commitlint-config-selling分别满足StyleLintESLintCommitLint

  1. @jd/stylelint-config-selling包括css、less、sass(团队暂未使用到)
  2. @jd/eslint-config-selling包括Vue、React、Taro、Next、nuxt(团队暂未使用到)...,还包括后续可能会扩展到需要自定义的ESLint插件或者解析器
  3. @jd/commitlint-config-selling统一使用git

向上提供一个简单的命令行工具,交互式初始化init、或者更新update规范

image.png

几个关键点

1、用lerna统一管理包

lerna是一个管理工具,用于管理包含多个软件包(package)的 JavaScript项目,业内已经广泛使用了,不了解的可以自己找资料看下
项目结构如下图
image.png

2、三个基础包的依赖包都设置为生产依赖dependencies

如下图,包@jd/eslint-config-selling的依赖包都写在了生产依赖,而不是开发依赖
image.png
解释下:
开发依赖&生产依赖

  • 开发依赖:业务工程用的时候不会下载开发依赖中的包,业内常见的规范如standardairbnb都是写在开发依赖
    • 缺点:业务工程除了安装@jd/eslint-config-selling外,需要自己去安装前置依赖包,如eslint、根据自己选择的框架安装相关前置依赖包如使用的Vue需要安装eslint-plugin-vue...使用成本、维护升级成本较高
    • 优点:按需安装包,开发时不会安装多余的包(Lint相关的包在业务工程中都是开发依赖,所以只会影响开发时)
  • 生产依赖:业务工程用的时候会下载这些包
    • 优点:安装@jd/eslint-config-selling后,无需关注前置依赖包
    • 缺点:开发时会下载@jd/eslint-config-selling中所有写在生产依赖的包,即使有些用不到,比如你使用的是React,却安装了eslint-plugin-vue

3、提供简单的命令行

这个比较简单,提供交互式命令,支持一键初始化或者升级3种规范,就不展开说了

不会的,指路中高级前端必备:如何设计并实现一个脚手架

组里现在还没有项目模版脚手架,后续有的话需要把规范这部分融进去

六、最重要的一点

什么是一个好的规范?
基本每个团队的规范都是不一样的,团队各成员都认同并愿意遵守的规范就是一个好的规范

所以确定好技术方案后,涉及到的各个规范,下图,我们在小组内分工去制定,比如几个人去制定styleLint的,几个人制定Vue的...

然后拉会评审,大家统一通过的规范才敲定
image.png
最后以开源的方式维护升级,使用过程中,遇到规范不合适的问题,提交issue,大家统一讨论确定是否需要更改规范

写在结尾

以上就是我们团队在前端规范落地方面的经验~

如果大家感兴趣,可查看github仓库

好文阅读:《大漠:web中的图标》

随着时代的变迁与技术的不断的更新,在当今这个时代,Web中的图标(Icons)不再仅仅是局限于<img>。除了<img>直接调用Icons文件之外,还有Sprites(俗称雪碧图)Icon Font(字体图标)SVG Icon等等。今天我们就来一起探讨一下这些方法在Web中实现Icon的利弊。

思考变革

设计师不管分辨率(Resolution independent)和设备平台,其追求像素完美(Pixel Perfection)、体验一致性;而前端工程师们更为关心的是页面的可访问性(Accessability)、性能以及重构的灵活性,可复用性,可维护性等等。

而当下这个互联网时代,设备多样化,显示分辨率层出不穷,对于Web前端工程师来说可是灾难性,而且碰到的难题也是越来越多:

  • 需要为高PPI(Retina显屏)显示设备准备1.5x2x3x的图标素材
  • 需要针对不同分辨率来调整优化排版
  • 需要考虑不同平台下图片加载的性能问题
  • 需要考虑可访问性,可维护性问题
  • 要考虑的还有很多很多.........

前途是光明的,道路是曲折的。前端工程师一直以来就是见招拆招,从未停止过自己向前的步伐。不信我们一起来看。

原始的<img>

<img>标签,大家都知道是用来给Web页面添加图片的。而图标(Icons)其实也是属于图片,因而在页面中可以直接使用<img>标签来加载图标。并且可以加载任何适用于Web页面的图标格式,比如:.jpg(或.jpeg)、.png.gif。对于今天的Web,除了这几种图片格式之外,还可以直接引用.webp.svg图像(图标)。

优势

  • 更换简单方便,只需要修改图标路径或覆盖图标文件名
  • 图标大小易于掌握

看上去这都不是什么优势,我也只能为其想到这两条了。

劣势

  • 增加HTTP请求数,如果页面使用的图标过多,直接拉高了HTTP的请求数,也就直接影响页面的加载性能
  • 不易适配各种终端和分辨率,特别是高PPI的显示设备,有可能会造成图标模糊(除非是通过img加载矢量图标.svg,或者一开始就加载适合高PPI的图标)
  • 不易修改图标的样式,比如颜色,阴影等
  • 不易维护

Sprites 图标(雪碧图)

虽然img可以帮助前端工程师往Web页面中添加需要的图标,但其不足之处也是众所周知的。由于img的局限性与不足,2004年3月@Dave Shea提出了一种全新的技术CSS Sprites(在国内常常将这种技术称为CSS雪碧,又称CSS精灵)。

CSS Sprites出现之后,很多互联网团队都在使用这种技术,比如:

Amazon

Amazon

YouTube

YouTube

Google

Google

Facebook

Facebook

Yahoo

Yahoo

当然,国内使用CSS Sprites也不少:

淘宝

淘宝

新浪微博

新浪微博

诸如此类的还很有很多。

在此对于CSS Sprites的技术就不做过多的阐述,如果您想了解这方面的相关知识,可以阅读下面的文章科普:

Sprites分类

早期CSS Sprites使用的都是位图,而且为了适合Web页面使用环境,采用的都是.png文件格式,但在现在只使用位图,会受到很多的限制,比如在Retina屏下,位图会模糊。也就是说,为了适配各种终端设备分辨,CSS Sprites不在局限于位图,也可以将SVG这样的矢量图集合在一起。其和位图最大的不同之处可以根据设备分辨率,调整Sprites的尺寸,从而不影响图标在设备的呈现质量

相对而言,SVG更适合当前的Web页面,当然,这种技术也受到一定的局限性,比如说修改ICON图标颜色之类,就必须去修改.svg文件,这对于前端人员来说是无法接受。有关于SVG Sprites相关的介绍,可以阅读下面相关文章:

优势

  • 减少HTTP请求数
  • 可以是任意图形,也可以是任意色彩
  • 兼容性极好(对于位图的Sprites兼容性都非常的好,但对于SVG的Sprites,还是受到浏览器的限制,最起码要支持SVG的浏览器才能得到支持)

劣势

  • 增加开发时间,需要人肉或通过相关工具,将图形零散的图形合并到一起,而不同的合并方式,图形的色彩对Web的性能有直接的影响;
  • 增加维护成本,要增加新的图标合成进来,是件较难的事情,甚至直接会影响到前面又定位好的图片。目前为止,使用自动编译工具,相对比人肉处理要理想一些;
  • 图片尺寸固定,在位图的Sprites中无法通过CSS来修改图标的大小,但在SVG的Sprites中可以配合CSS的background-size调整图标的大小;

字体图标(Icon Font)

虽然CSS Sprites有其足够的优势,而且众多开发者都在使用这种技术,但是到了今天,不得不对CSS Sprites说再见(其实是跟位图的图标说再见)。

随着Retina屏幕的出现,大家都发现自己在Web中使用的图标变得模糊不清,直接拉低了自己产品的品质。对于Web前端人员也必须面对考虑各种高清屏幕的显示效果。由此也造成同样的前端在代码实现的时候需要根据屏幕的不同来输出不同分辨率的图片。不管你采用的是何种手段:

不管使用哪种方法,都不是一件易事,比如使用image-set和媒体查询,只适合背景图像;而对于srcsetpicture方法仅适合Web引入图像的情景。而且这些方法直到目前为止在浏览器上都受到了很多的限制。

为了解决屏幕分辨率对图标影响的问题,字体图标(Icon Font)就顺势而生了。字体图标是一种全新的设计方式,更为重要的是相比位图而言,使用字体图标可以不受限于屏幕分辨率,冲着这一点就具有非常强的优势,而且字体图标还具有一个优势是,只要适合字体相关的CSS属性都适合字体图标,比如说:

  • 使用font-size修改图标大小
  • 使用color修改图标颜色
  • 使用text-shadow给图标增加阴影
  • ....

基于这些原因,现在Web开发中,使用字体来制作图标的应用也越来越多。

优势

  • 减少了HTTP的请求
  • 很容易任意地缩放
  • 很容易地改变颜色
  • 很容易地产生阴影
  • 可以拥有透明效果
  • 浏览器兼容性较好
  • 可以到很CSS很好支持
  • 可以快速转化形态
  • 可以做出跟位图一样可以做的事情
  • 本身体积更小

劣势

  • 它们只能被渲染成单色或CSS3的渐变色
  • 使用限制性很大,除非你想花时间去创作你自己的字体图标
  • 创作字体图标很耗时间
  • 可访问性差
  • 字体文件体积过大,直接影响页面加载性能,特别是加载一个包含数百图标的Fonts,却只使用其中几个图标
  • 在不同的设备浏览器字体的渲染会略有差别,在不同的浏览器或系统中对文字的渲染不同,其显示的位置和大小可能会受到font-sizeline-heightword-spacing等CSS属性的影响,而且这种影响调整起来较为困难
  • 为了实现最大程度的浏览器支持,可能要提供至少四种不同类型的字体文件。包括.ttf.woff.eot.svg格式字体
  • 不兼容旧的手机浏览器:Opera mini,Android 2.1,Windows Phone 7.5-7.8
  • 在手机上可能与系统字体冲突

SVG图标

为了适配各种分辨率,让图标显示更完美,除了字体图标之外,还可以使用SVG图标。SVG图标是一种矢量图标。其实回过头来看,字体图标其实也是使用SVG封装过的。

为什么要使用SVG图标

SVG图标实际上是一个服务于浏览器的XML文件,而不是一个字体或像素的位图。它是由浏览器直接渲染XML,在任何大小之下都会保持图像清晰。而且文件中的XML还提供了很多机会,可以直接在代码中使用动画或者修改颜色,描边等。不需要借助任何图形编辑软件都可以轻松的自定义图像。除此之外,SVG图像也有过字体图标的一个主要优势:拥有多个彩色图像的能力。

优势

  • SVG图标是矢量图形文件,可以随意修改大小,而且不会影响图标质量
  • 可以使用CSS样式来自定义图标颜色,比如颜色、尺寸等效果
  • 所有SVG图标可以全部放在一个SVG的文件中(SVG Sprites),节省HTTP的请求
  • 使用SMIL、CSS或者JavaScript可以制作动画效果
  • 可以使用gzip的方式把文件压缩到很小
  • 可以很精细的控制SVG图标的每一部分

劣势

  • 浏览器兼容性较差
  • 需要学习SVG相关知识
  • 需要了解使用制作软件绘制SVG图形或专业的SVG图形编辑软件

为什么要使用SVG图标替代字体图标

为了让图标能更好的适配各种屏幕分辨率,大家首先的就是字体图标和SVG图标,那么这里为何又要谈:为什么要使用SVG图标替代字体图标@Chris Coyier的《Inline SVG vs Icon Fonts》(中文译文可以点击这里阅读)和@Ian Feather的《Ten reasons we switched from an icon font to SVG》(中文译文可以点击这里阅读)。

字体图标 vs SVG图标

字体图标 SVG图标
图标是矢量 浏览器会以字体解析它,所以浏览器会以文字的方式来对图标做抗锯齿处理,这可以导致字体图标没有期待中的那么锐利 SVG是XML文件,浏览器直接解析XML文件,直接就是矢量图形,图标锐利,体积也小
可控制性 可以通过font-size、color、text-shadow等CSS来控制图标 除了字体图标一样的CSS控制方法之外,还可以单独控制一个复合SVG图标中的某一部分,也可以给图标描边
控制图标位置 图标位置会受到line-height、vertical-align、letter-spacing等属性影响 SVG图标的大小就是很精确的SVG图形的大小
图标加载 跨域时没有合理的CORS头部、字体文件未加载、@font-face在Chrome中的bug和不支持@font-face的浏览器等,这些原因都会造成字体图标渲染失败 SVG图标就是文档本身,只要支持SVG的浏览器,都能正常的渲染
语义化,易访问性 为了更好的显示图标,通常使用伪元素或伪类来做,这样做语义化较差 SVG图标就是一个小图片。SVG的语义就是”我是一张图片“,感觉可能更好
易用性 使用一个已造好的字体图标集从来都不有效,因为有太多的图标未使用。而创建一个你自己的字体图标集也不是轻松的事情,需要懂得相关的编辑工具或应用软件 SVG图标会简单一些,因为你可以自己手动地操作,如果需要的话,你可以使用相关的编辑工具
浏览器支持度 得到非常好的支持性,可以一直支持到IE6,在Opera mini,Android 2.1,Windows Phone 7.5-7.8没到支持 浏览器支持性一般,IE8和Android 2.1以及其以下浏览器不支持。不支持可以采用降级处理,但不并完美

DataURI

DataURI是利用Base64编码规范将图片转换成文本字符,不仅是图片,还可以编码JS、CSS、HTML等文件。通过将图标文件编码成文本字符,从而可以直接写在HTML/CSS文件里面,不会增加任何多余的请求。

但是DataURI的劣势也是很明显的,每次都需要解码从而阻塞了CSS渲染,可以通过分离出一个专用的CSS文件,不过那就需要增加一个请求,那样与CSS Sprites、Icon Font和SVG相比没有了任何优势,也因此,在实践中不推荐这种方法。需要注意的是通过缓存CSS可以来达到缓存的目的。

优势:

  • 不增加请求数

劣势:

  • 通常比图片要大不到10%
  • 每次加载页面都需要解码
  • 不支持IE6/7,IE8最大支持32KB
  • 难于维护

性能对比

不管使用哪种方案来制作Web页面的图标,大家都会比较关心其对页面的性能有多大的影响。在这里提供一个测试用例,在这些用例中,页面加载了28232 x 32的图标。

Web Icons

这些图标直接通过IcoMoon APP获取。并且分别采用了img加载.png、CSS Sprites(png和svg的Sprites)、字体图标和SVG图标的方式写的用例:

具体代码就不做演示,接下来通过在线性能测试工具**WebPageTest**(除了这个在线测试工具之外,还可以点击这里获取其他的在线测试工具)来做一个简单的测试。当然这样的测试可能不会非常的准确,但或多或少能从相关的数据上向大家形象的展示不同的方案对页面性能的影响会有多大。

特别声明:以下提供的测试数据受到网络直接影响,仅提供做为示例参考。

PNG/SVG文件

PNG/SVG文件

页面使用<img>加载了282.png.svg图标。每个图标的大小是32px x 32px

PNG Sprites

PNG Sprites

282.png图标集成在一个Sprites文件中,Sprites图片文件大小是108kb

SVG Sprites

SVG Sprites

282.svg图标集成在一个Sprites文件中,Sprites图片文件大小是180kb

SVG

SVG

在页面中直接使用SVG源码制作的图标。

Icon Font

Icon Font

使用Font制作的图标。

以上图表中的数据仅做参考,因为在线测试,网速之类直接影响到测试结果。熟悉性能测试的同学,可以直接在本地测试用例,拿到更具有价值的参考数据。也希望同学能将这方面的结果在评论中与我们一起分享。

如何选择

前面介绍了Web中制作图标的几种常见方案,每种方案都有其自己的利弊。那在实际中要如何选择呢?这需要根据自身所在的环境来做选择:

  • 如果你需要信息更丰富的图片,不仅仅是图标时,可以考虑使用<img>
  • 使用的不是展示类图形,而是装饰性的图形(包括图标),而且这部分图形一般不轻意改变,可以考虑使用PNG Sprites
  • 如果你的图标之类需要更好的适配于高分辨率设备环境之下,可以考虑使用SVG Sprites
  • 如果仅仅是要使用Icon这些小图标,并且对Icon做一些个性化样式,可以考虑使用Icon Font
  • 如果你需要图标更具扩展性,又不希望加载额外的图标,可以考虑在页面中直接使用SVG代码绘制的矢量图

当然,在实际开发中,可能一种方案无法达到你所需的需求,你也可以考虑多种方案结合在一起使用。

总结

全文主要介绍了Web中图标的几种方案之间的利与弊。相对而言,如果不需要考虑一些低版本用户,就当前这个互联网时代,面对众多终端,较为适合的方案还是使用SVG。不管是通过img直接调用.svg的文件还是使用SVG的Sprites,或者直接在页面中使用SVG(直接代码),都具有较大的优势。不用担心,使用的图标在不同的终端(特别是在Retina屏)会模糊不清。而且SVG还有一个较大的优势,你可以直接在源码中对SVG做修改,特别是可以分别控制图标的不同部分,加入动画等。

当然,你或许会有众多的顾虑,不懂SVG的怎么破,就算不懂SVG,你也可以借助SVG的图形编辑软件或者工具,来协助你。除此之外,除了这些方式在Web中嵌入图标之外,对于一些简单的小图标,可以考虑直接使用CSS代码来编写,这种方式可能较为费时费力,但其具有的优势,我想大家都懂的。

最后非常感谢您花时间阅读这篇文章,如果您有更好的思路或建议,非常欢迎在下面的评论中与我一起分享。

原文地址:amfe/article#2

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.