zxyue25 / blog Goto Github PK
View Code? Open in Web Editor NEW原创前端文章&阅读到的好文
原创前端文章&阅读到的好文
随着时代的变迁与技术的不断的更新,在当今这个时代,Web中的图标(Icons)不再仅仅是局限于 思考变革设计师不管分辨率(Resolution independent)和设备平台,其追求像素完美(Pixel Perfection)、体验一致性;而前端工程师们更为关心的是页面的可访问性(Accessability)、性能以及重构的灵活性,可复用性,可维护性等等。 而当下这个互联网时代,设备多样化,显示分辨率层出不穷,对于Web前端工程师来说可是灾难性,而且碰到的难题也是越来越多:
前途是光明的,道路是曲折的。前端工程师一直以来就是见招拆招,从未停止过自己向前的步伐。不信我们一起来看。 原始的
|
字体图标 | 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是利用Base64编码规范将图片转换成文本字符,不仅是图片,还可以编码JS、CSS、HTML等文件。通过将图标文件编码成文本字符,从而可以直接写在HTML/CSS文件里面,不会增加任何多余的请求。
但是DataURI的劣势也是很明显的,每次都需要解码从而阻塞了CSS渲染,可以通过分离出一个专用的CSS文件,不过那就需要增加一个请求,那样与CSS Sprites、Icon Font和SVG相比没有了任何优势,也因此,在实践中不推荐这种方法。需要注意的是通过缓存CSS可以来达到缓存的目的。
不管使用哪种方案来制作Web页面的图标,大家都会比较关心其对页面的性能有多大的影响。在这里提供一个测试用例,在这些用例中,页面加载了282
个32 x 32
的图标。
这些图标直接通过IcoMoon APP获取。并且分别采用了img
加载.png
、CSS Sprites(png和svg的Sprites)、字体图标和SVG图标的方式写的用例:
具体代码就不做演示,接下来通过在线性能测试工具**WebPageTest**(除了这个在线测试工具之外,还可以点击这里获取其他的在线测试工具)来做一个简单的测试。当然这样的测试可能不会非常的准确,但或多或少能从相关的数据上向大家形象的展示不同的方案对页面性能的影响会有多大。
特别声明:以下提供的测试数据受到网络直接影响,仅提供做为示例参考。
页面使用<img>
加载了282
个.png
或.svg
图标。每个图标的大小是32px x 32px
。
将282
个.png
图标集成在一个Sprites文件中,Sprites图片文件大小是108kb
。
将282
个.svg
图标集成在一个Sprites文件中,Sprites图片文件大小是180kb
。
在页面中直接使用SVG源码制作的图标。
使用Font制作的图标。
以上图表中的数据仅做参考,因为在线测试,网速之类直接影响到测试结果。熟悉性能测试的同学,可以直接在本地测试用例,拿到更具有价值的参考数据。也希望同学能将这方面的结果在评论中与我们一起分享。
前面介绍了Web中制作图标的几种常见方案,每种方案都有其自己的利弊。那在实际中要如何选择呢?这需要根据自身所在的环境来做选择:
<img>
当然,在实际开发中,可能一种方案无法达到你所需的需求,你也可以考虑多种方案结合在一起使用。
全文主要介绍了Web中图标的几种方案之间的利与弊。相对而言,如果不需要考虑一些低版本用户,就当前这个互联网时代,面对众多终端,较为适合的方案还是使用SVG。不管是通过img
直接调用.svg
的文件还是使用SVG的Sprites,或者直接在页面中使用SVG(直接代码),都具有较大的优势。不用担心,使用的图标在不同的终端(特别是在Retina屏)会模糊不清。而且SVG还有一个较大的优势,你可以直接在源码中对SVG做修改,特别是可以分别控制图标的不同部分,加入动画等。
当然,你或许会有众多的顾虑,不懂SVG的怎么破,就算不懂SVG,你也可以借助SVG的图形编辑软件或者工具,来协助你。除此之外,除了这些方式在Web中嵌入图标之外,对于一些简单的小图标,可以考虑直接使用CSS代码来编写,这种方式可能较为费时费力,但其具有的优势,我想大家都懂的。
最后非常感谢您花时间阅读这篇文章,如果您有更好的思路或建议,非常欢迎在下面的评论中与我一起分享。
原文地址:amfe/article#2
「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」
本文主要讲css各种解决方案,包括,
BEM
、css modules
、Css in Js
、预处理器
、Shadow DOM
,Vue Scoped
通过分析各项方案的产生背景、带来的好处以及存在的一些问题来帮助大家判断自己的项目中适合使用哪种那方案
在讲解各种解决方案之前,我们先回顾下日常开发中我们遇到的css问题,待着这些问题,我们在讲解各种解决方案,并分析各个解决方案是否可以解决如下问题
CSS有一个被大家诟病的问题就是没有本地作用域
,所有声明的样式都是全局的(global styles)
换句话来说页面上任意元素只要匹配上某个选择器的规则,这个规则就会被应用上,而且规则和规则之间可以叠加作用(cascading)
SPA应用
流行了之后这个问题变得更加突出了,因为对于SPA应用来说所有页面的样式代码都会加载到同一个环境中,样式冲突的概率会大大加大。由于这个问题的存在,我们在日常开发中会遇到以下这些问题:
.title
的样式名,这个类名很大概率已经或者将会和页面上的其他选择器发生冲突,所以你不得不手动为这个类名添加一些前缀,例如.home-page-title
来避免这个问题进行过大型Web项目开发的同学应该都有经历过这个情景:在开发新的功能或者进行代码重构的时候,由于HTML代码和CSS样式之间没有显式的一一对应关系
,我们很难辨认出项目中哪些CSS样式代码是有用的哪些是无用的,这就导致了我们不敢轻易删除代码中可能是无用的样式。这样随着时间的推移,项目中的CSS样式只会增加而不会减少(append-only stylesheets)。无用的样式代码堆积会导致以下这些问题:
对于SPA应用来说,特别是一些交互复杂的页面,页面的样式通常要根据组件的状态变化而发生变化
最常用的方式是通过不同的状态定义不同的className名
,这种方案代码看起来十分冗余和繁琐,通常需要同时改动js代码和css代码
这个CSS重写一遍比修改老文件快
,这样的念头几乎所有人都曾有过,css虽然看似简单,但是以上问题很容易写着写着就出现了,这在于提前没有选好方案
BEM是一种css命名方法论
,意思是块(Block)、元素(Element)、修饰符(Modifier)的简写
这种命名方法让CSS便于统一团队开发规范和方便维护
以 .block__element--modifier
或者说block-name__element-name--modifier-name
形式命名,命名有含义,也就是模块名 + 元素名 + 修饰器名
如.dropdown-menu__item--active
社区里面对BEM命名的褒贬不一,但是对其的**基本上还是认同的,所以可以用它的**,不一定要用它的命名方式
BEM**通常用于组件库
,业务代码中结合less等预处理器
个人比较喜欢BEM,其**对编码好处远大于坏处,有兴趣的可以在项目中使用,更多可看知乎:如何看待 CSS 中 BEM 的命名方式?
什么是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
styles.**
,可以试一下 [react-css-modules](gajus/react-css-modules · GitHub),它通过高阶函数的形式来避免重复输入 styles.**
css modules通常结合less等预处理器在react中使用,更多可参考CSS Modules 详解及 React 中实践
CSS in JS
是2014年推出的一种设计模式,它的核心**是把CSS直接写到各自组件中
,也就是说用JS去写CSS
,而不是单独的样式文件里
这跟传统的前端开发思维不一样,传统的原则是关注点分离
,如常说的不写行内样式
、不写行内脚本
,如下代码
<h1 style="color:red;font-size:46px;" onclick="alert('Hi')">
Hello World
</h1>
CSS-in-JS
不是一种很新的技术,可是它在国内普及度好像并不是很高,它当初的出现是因为一些component-based
的Web
框架(例如 React
,Vue
和 Angular
)的逐渐流行,使得开发者也想将组件的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 的走红和组件模式深入人心,这种"关注点混合
"的新写法逐渐成为主流
实现了CSS-in-JS
的库有很多,据统计现在已经超过了61种。虽然每个库解决的问题都差不多,可是它们的实现方法和语法却大相径庭
从实现方法上区分大体分为两种:
唯一CSS选择器
,代表库:styled-components内联样式
(Unique Selector VS Inline Styles)不同的CSS in JS
实现除了生成的CSS样式和编写语法
有所区别外,它们实现的功能也不尽相同,除了一些最基本的诸如CSS局部作用域的功能,下面这些功能有的实现会包含而有的却不支持:
Styled-components 是CSS in JS
最热门的一个库了,到目前为止github的star数已经超过了35k
通过styled-components
,可以使用ES6的标签模板字符串语法(Tagged Templates)为需要styled
的Component
定义一系列CSS
属性
当该组件的JS代码被解析执行
的时候,styled-components会动态生成一个CSS选择器
,并把对应的CSS
样式通过style
标签的形式插入到head
标签里面。动态生成的CSS
选择器会有一小段哈希值来保证全局唯一性
来避免样式发生冲突
CSS-in-JS Playground是一个可以快速尝试不同CSS-in-JS实现的网站,上面有一个简单的用styled-components
实现表单的例子:
从上面的例子可以看出,styled-components
不需要你为需要设置样式的DOM节点设置一个样式名
,使用完标签模板字符串定义后你会得到一个styled
好的Component
,直接在JSX
中使用这个Component
就可以了
可以看到截图里面框出来的样式生成了一段hash值
,实现了局部CSS作用域
的效果(scoping styles),各个组件的样式不会发生冲突
Radium
和styled-components
的最大区别是它生成的是标签内联样式(inline styles)
由于标签内联样式在处理诸如media query
以及:hover
,:focus
,:active
等和浏览器状态相关的样式的时候非常不方便,所以radium
为这些样式封装了一些标准的接口以及抽象
再来看一下radium
在CSS-in-JS Playground的例子:
从上面的例子可以看出radium
定义样式的语法和styled-components
有很大的区别,它要求你使用style
属性为DOM
添加相应的样式
直接在标签内生成内联样式,内联样式相比于CSS选择器的方法有以下的优点: 自带局部样式作用域的效果
,无需额外的操作
CSS in JS
使用 JavaScript
的语法,是 JavaScript 脚本的一部分,不用从头学习一套专用的 API,也不会多一道编译步骤,但是通常会在运行时动态生成CSS,造成一定运行时开销
没有无作用域问题样式污染问题
通过唯一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的好与坏
CSS 预处理器是一个能让你通过预处理器自己独有的语法的程序
市面上有很多CSS预处理器可供选择,且绝大多数CSS预处理器会增加一些原生CSS不具备的特性,例如
这些特性让CSS的结构更加具有可读性且易于维护
要使用CSS预处理器,你必须在web服务中安装CSS编译工具
我们常见的预处理器:
预处理器是现代web开发中必备,
结合BEM规范
,利用预处理器,可以极大的提高开发效率,可读性,复用性
熟悉web Components
的一定知道Shadow DOM
可以实现样式隔离,由浏览器原生支持
我们经常在微前端领域看到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
样式上浮一层,丢到主文档里,但这么做意味着子应用的样式直接泄露到主文档了
普通业务开发我们还是用框架、如Vue、React;Shadow DOM适用于特殊场景,如微前端
当 <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 + 预处理器
你用的CSS隔离方案是什么,欢迎探讨?
本文首发于zxyue25/github/blog,欢迎关注,star~,持续记录原创、好文~
前段时间备战双十一前期
,线上项目的性能问题
引起了我们的重视
公司内部是有统一的性能监控平台
的,我们的项目也都统一接入了监控平台,但是这个时间的计算方式我们是不清楚的,于是花时间深入调研了一番
调研后的结果是,其他时间的计算方式(比如网路请求时间,首包时间...)是比较清晰的,指路,除了首屏时间
,业内没有一个统一的标准
调研后首屏时间的计算方式
还是很硬核的,最近得空记录分享出来~
本篇文章讲一种前端首屏时间的计算方案,偏算法实现,重点是**,看懂就等于赚到!
首屏时间
:也称用户完全可交互时间,即整个页面首屏完全渲染出来,用户完全可以交互,一般首屏时间小于页面完全加载时间,该指标值可以衡量页面访问速度
这两个完全不同的概念,白屏时间是小于首屏时间的
白屏时间
:首次渲染时间,指页面出现第一个文字或图像所花费的时间
随着 Vue 和 React 等前端框架盛行,Performance
已无法准确的监控到页面的首屏时间
因为 DOMContentLoaded
的值只能表示空白页(当前页面 body 标签里面没有内容)加载花费的时间
浏览器需要先加载 JS , 然后再通过 JS 来渲染页面内容,这个时候单页面类型首屏才算渲染完成
loadEventEnd - fetchStart/startTime
或者 domInteractive - fetchStart/startTime
利用 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>
初始化代码如下
MutationObserver
放弃上报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
变化时得分,算法如下
计算函数 traverseEl
如下
body
元素开始递归计算,第一次调用为 traverseEl(body, 1, false)
script
、style
、meta
、head
layer
表示当前DOM层数
,每层的得分等于1 + (层数 * 0.5)
+ 该层children的所有得分
/**
* 深度遍历 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.observerData
中this.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 监听,会有重复分数的项
首先删除掉后一个小于前一个的元素,即去掉 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;
}
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;
}
}
页面关闭时如果没有上报,立即上报
window
监听 beforeunload事件(当浏览器窗口关闭或者刷新时,会触发beforeunload事件)this.calcFirstScreenTime
,计算首屏时间状态,分为 init
、pending
、和 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);
}
};
我们看看 卸载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;
}
本篇文章讲怎么在前端团队快速制定并落地代码规范!!!
干货,拿走这个仓库
9月份换了一个新部门,部门成立时间不长,当时组内还没有统一的代码规范
(部分工程用了规范,部分没有,没有统一的收口)
小组的技术栈框架有Vue
,React
,Taro
,Nuxt
,用Typescript
,算是比较杂了,结合到部门后续还可能扩展其他技术栈,我们从0-1实现了一套通用的代码规范
到现在小组内也用起来几个月了,整个过程还算是比较顺利且快速
,最近得空分享出来~
不是很了解的话,指路
首先,跟主管同步,团队需要一个统一的规范,相信主管也在等着人来做起来
第一步收集团队的技术栈情况
,确定规范要包括的范围
把规范梳理为三部分ESLint
、StyleLint
、CommitLint
,结合团队实际情况分析如下
可扩展性
常见以下3种方案
团队制定文档式代码规范,成员都人为遵守这份规范来编写代码
靠人来保证代码规范存在
不可靠
,且需要人为review代码不规范,效率低
直接使用业内已有成熟规范,比如css使用StyleLint官方推荐规范stylelint-config-standard、stylelint-order,JavaScript使用ESLint推荐规范eslint:recommended等
a) 开源规范往往不能满足团队需求,
可拓展性差
; b) 业内提供的规范都是独立的(stylelint只提供css代码规范,ESLint只提供JavaScript规范),是零散的
,对于规范初始化或升级存在成本高、不可靠问题(每个工程需要做人为操作多个步骤)
基于StyleLint、ESLint制定团队规范npm包,使用团队制定规范库
a) 该方案解决可扩展性差的问题,但是第二点中的(b)问题依旧存在
整体技术思路图如下图,提供三个基础包@jd/stylelint-config-selling
、@jd/eslint-config-selling
、@jd/commitlint-config-selling
分别满足StyleLint
、ESLint
、CommitLint
@jd/stylelint-config-selling
包括css、less、sass(团队暂未使用到)@jd/eslint-config-selling
包括Vue、React、Taro、Next、nuxt(团队暂未使用到)...,还包括后续可能会扩展到需要自定义的ESLint插件或者解析器@jd/commitlint-config-selling
统一使用git向上提供一个简单的命令行工具,交互式初始化init
、或者更新update
规范
几个关键点
lerna是一个管理工具,用于管理包含多个软件包(package)的 JavaScript项目,业内已经广泛使用了,不了解的可以自己找资料看下
项目结构如下图
如下图,包@jd/eslint-config-selling
的依赖包都写在了生产依赖,而不是开发依赖
解释下:
开发依赖&生产依赖
开发依赖
:业务工程用的时候不会下载
开发依赖中的包,业内常见的规范如standard
、airbnb
都是写在开发依赖
@jd/eslint-config-selling
外,需要自己去安装前置依赖包,如eslint
、根据自己选择的框架安装相关前置依赖包如使用的Vue需要安装eslint-plugin-vue
...使用成本、维护升级成本较高生产依赖
:业务工程用的时候会下载
这些包
@jd/eslint-config-selling
后,无需关注前置依赖包@jd/eslint-config-selling
中所有写在生产依赖的包,即使有些用不到,比如你使用的是React,却安装了eslint-plugin-vue
这个比较简单,提供交互式命令,支持一键初始化或者升级3种规范,就不展开说了
不会的,指路中高级前端必备:如何设计并实现一个脚手架
组里现在还没有项目模版脚手架,后续有的话需要把规范这部分融进去
什么是一个好的规范?
基本每个团队的规范都是不一样的,团队各成员都认同并愿意遵守的规范就是一个好的规范
所以确定好技术方案后,涉及到的各个规范,下图,我们在小组内分工去制定
,比如几个人去制定styleLint的,几个人制定Vue的...
然后拉会评审
,大家统一通过的规范才敲定
最后以开源的方式维护升级
,使用过程中,遇到规范不合适的问题,提交issue,大家统一讨论确定是否需要更改规范
以上就是我们团队在前端规范落地方面的经验~
如果大家感兴趣,可查看github仓库
本篇文章基于业务中搭建多端组件库的经验撰写
step2: 为了满足业务,我们开始直接在业务中直接写组件,暴露的问题自然更明显:组件难复用、难扩展
step3: 所以在21年10月,当我们又新增了两块多端商城的业务时,可预见一个组件库来帮助团队提高开发效率是很有必要的,所以我们趁着做业务的同时自建组件库,并与21年12月发布了第一版(同事当时写的文章)
step4: 时隔一个季度,我们在技术实现了一些突破,比如用 hook
代替 class
,用 css变量
全量替换 less变量
,新增ConfigProvider全局配置组件,支持自定义样式前缀、动态切换主题...(在第三部分组件库设计与实现会详细介绍)
在开始动手之前,我们搜索了全网Taro-React的组件库 (缺少搭建组件库的经验只能多看别人的!!!)
定位:一套基于 Taro 框架开发的多端 React UI 组件库
Tard 取名自
Taro React Design
简写,发音特的
先贴几张组件库效果图
目前31个组件(都是我们业务中用到的)+ ConfigProvider全局配置组件,分为6类,具体可见下图
组件库更多可见官网(url:https://tard-ui.selling.cn/)
开始动手写代码之前,一定要制定一套规范,除非一个人写组件,不然多个人最后的组件库写出来可能五花八门;
开发过程中我们也经常因为意见不合,争吵过很多次,讨论后都达成一致,这是一个不容易的过程;所以尽可能的提前制定规范,不然中途来改成本很大,毕竟谁也不想反复修改代码
1、monorepo
packages包括以下三块
我们研究了其他各个组件库怎么组织组件的代码,一个组件包括的文件有组件的类型ts文件、样式文件、demo、使用文档;最终决定将一个组件涉及的代码集中在一个文件夹下,一定程度上避免重复切换文件夹,提高开发效率
1、组件编码
统一使用Hook+TypeScript
2、组件命名
大驼峰,多组件以.的形式命名,对外一个组件只暴露一个组件,如下
3、组件API
tard
),效果如下
var.less
文件中css变量h5挂在
:root
下可生效,小程序需挂在page
下生效
可以调用 ConfigProvider.config
配置方法设置组件的class前缀名,默认是 tard
,比如下面我们设置为 custom
// 入口js文件
import { ConfigProvider } from 'tard'
ConfigProvider.config({
cssPrefix: 'custom'
});
同时需要配置样式文件前缀名,在入口样式文件按如下配置
@--css-prefix: 'custom'
注意:
ConfigProvider.config
配置的cssPrefix
一定要与样式配置的@--css-prefix
一致,否则class名与style不对应,样式失效
主题定制作为组件库最基本的功能必不可少,分为两类
ConfigProvider
实现运行时切换,具体可见3.3.2以 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 节点
在官网ConfigProvider 全局配置下可以体验在线动态切换主题
调用 ConfigProvider.config
配置方法设置主题色
ConfigProvider.config({
theme: {
'color-primary': 'purple',
"button-radius": '20px'
}
});
这一部分主要写组件库建设经验,如果说上面是技术上的经验,这部分是人跟事上的经验,毕竟一个组件库要做出来并能持续扩展还真不容易,甚至说比技术更难
其实刚开始做这个组件库过程很艰难,没有专业的UI支持;一方面UI资源紧张,另一方面团队没有相应的KPI是建设组件库,组件库仅仅是用来满足业务的
我们负责封装组件给其他同事用,写业务的同事组件催的急,我们是利用业务的设计稿中去实现的组件,比如业务设计稿中下拉菜单张这样,那我们就按照业务设计稿实现,没有办法...
但是组件库脱离了UI很难帮助业务,毕竟设计稿是UI给到前端的;难道就满足这一个业务,后面还有类似的场景呢?所以导致前期组件库处于一个不规范的局面
转机出现在,在写业务的过程中我们发现同一个业务的不同页面设计搞不统一,比如按钮圆角、筛选列表的筛选条件...因为我们将其封装成一个组件,那么相应的样式肯定是相对固定的,因为一个系统的设计风格肯定是需要保持一致的,除了可以通过样式变量更改,所以我们与UI同学讨论,在讨论的过程中,我们透露了组件库的**,视觉规范一致的必要性,可以提高前端开发效率、UI设计效率
然后有一位UI同事比较感兴趣,于是我们针对聊了很多,私下花时间专门支持我们,官网也是UI同事帮忙设计的
中途我们尝试跟主管沟通过组件库没有UI,能不能协调下我们做的组件库需要UI支持,但是目前没有UI能支持,且重点是满足业务,所以组件库并没有投入资源
兴趣使然或者说出于对技术的学习心态吧,我们3个前端+一个UI一心想把组件库给做好,于是我们开始借鉴业内组件库的技术实现,设计语言与前端代码如何结合
就好比设计给出了一个按钮的样式,她得知道这个按钮元素应该包括哪些内容(高宽、高度、字体、内间距等),这些就是设计规范,那这些规范对应的值就是设计语言。再更深层次的设计这些内容背后的逻辑是什么,规范的原理什么的就可以抽象出设计原则
这里的设计语言对应到前端也就是样式变量
UI同事开始给每个组件出稿,每个组件不同形态都有设计;前端开始制定规范,实现组件的同时思考API、样式变量定义的是否规范,组件的demo、文档都补充起来,并进行code review
制定了每周开会机制,一个组件从设计稿设计、前端开发、code review、UI走查到达到要求的路径,如下
开始设计组件库的官网,想把组件库做大最强,真正帮助大部门多端的业务(梦想还是要有的)
大家热火朝天,虽然频频争吵,好不快乐!
我们深知组件库的视觉规范需要大部门都认同的前提下,才能发挥作用,于是当我们组件库做的有模有样了之后,我们将我们的成果汇报给了主管,希望主管能协调去大部门的UI与前端,统一规范
这些汇报(或者说组件库的未来规划)也算是我们在做组件库的时候,对组件库(或者说更高一层C端视觉规范统一)价值整体的一个认知吧,这些也是我们参考了很多资料总结的,大概如下
可能写的都是比较理想的,大家可以理性参考...这当初的确是我们的目标,不过现在实在没时间
未来规划
本来技术能真正帮助业务是一件好事,理想是美好的,现实的艰难的,Q1后期吧,可能资源协调确实困难,加上组织架构频频调整,视觉统一共建的事情迟迟难以落地,所以说目前组件库确实满足业务就是最好的选择
写这篇文章以及组件库开源的原因是希望我们踩过的坑能给大家一些经验,以及如果你对开源感兴趣可以加入我们一起打造组件库,或者需要taro-react多端组件库的话可以尝试使用tard
GitHub地址:
https://github.com/jd-antelope/tard
最近研究了前端研发流程,总结了一份前端研发流程图,正好也到年底该总结了,于是认真研究了一番。
本人工作一年半,文章都是结合自己真实的经历写的,也欢迎大家探讨前端研发流程!
本篇文章讲整体的前端研发流程,主要讲整体,具体各个模块由于篇幅不展开
》编码规范
这一块涉及的就比较多&基础了,比如*lint
规范、文件命名
规范、工程结构
规范、注释
规范、图片处理
规范等等
》技术选型规范
选这个技术栈是出于什么原因,能解决什么痛点
?能给业务带来什么收益
?举一个发生在身边的真实案例
反例一:大家都知道移动端组件库官网都有一个手机预览,如下图。手机预览的是一个组件示例项目,一个独立的H5项目。
实现方案是通过iframe实现(最近在封装一个taro-react组件库,也遇到了这个场景)
有一天,同事A兴奋的说:XX给我提了一个建议,咱们的文档可以通过`微前端qiankun`的形式实现,不用iframe了
我:??
同事A:我觉得这个建议可行,说出去显得技术也厉害
总结:很明显这种技术选型存在问题的,首先iframe也是微前端的一种实现方案,选qiankun的动机单纯是为了“显的”技术厉害。
引入qiankun非但对该场景没有收益,还影响部署方式,代码结构等,反而显得“不懂技术”。
技术选型更体现一个人的技术功底,需要一定的技术广度,一定的技术深度,往往是高级前端
需要必备技能
》架构设计规范:
架构设计就更有挑战
了,是基于技术选型
。讲究可维护性
、可扩展性
,对于复杂场景,具有创造性思维
,创造解决方案;并能从业务中沉淀通用解决方案
,造出轮子(工具
、插件
等),对于工具设计也有规范,保证易用性
,帮助团队降本提效
一般的业务大家可能会觉得根本涉及不到架构设计啊,换个角度看,是你还不具备能看到需要架构设计的能力。架构师往往能一眼看到隐藏的问题
,给出最佳实践方案
为什么有些人的代码写出来成了💩山,为什么有的人写出的代码像诗,差异巨大。往往跟整体的架构、系统设计有很大的关系,比如
......众多的细节加起来,决定了工程架构是否优雅
》工时评估规范
这部分主要是项目管理
的需要了,每个人都有一套自己的工时评估规范,怎么保证评估模块A的2人天
是准确的?
这方面我的经验就是:首先让负责开发的人自己评估,根据自己的一个实际情况评估一个时间;第二再check这个时间是否合理,是否忽略了某些相关的改动也需要工作量(PS:经历的大部分人都是评估出的时间过短,只看到了表面的功能,一开始没有把可优化、相关联的改动考虑进去,中途才发现时间来不及)
文档也是很重要一部分,相信我,没有一个人能记住业务流程、技术方案的!!
在项目开始之前,就需要建立一个项目的文档,里面必须包括基本的PRD、UI稿、技术方案、以及必要的复盘
》PRD
》UI设计稿
》技术方案文档
Taro+React
,B端用的是Vue
;统一用Typescript
、less
组件+utils
;组件一般来说业内开源的组件库能满足大部分,如 Ant Design/Element/Vant
,但是一般都会有业务相关的组件库,比如我们组内在做的 C端多端组件库
、B端业务组件库
;pro的话意思是根据组内业务常用场景的代码模块,类似 Ant Design Pro
这种,都属于物料库工程化
的东西,最基本的用业内开源的脚手架
实现工程搭建、开发服务、编译构建,如VueCli/create-react-app
等;通常组内会有一个自己的轻量级的脚手架
用来实现工程搭建,因为每个团队都有自己一套规范,但是通常很难会实现开发服务、编辑构建。所以我把这里写成了工程初始化命令行
,用这个搭建工程,包括目录结构,选择的技术栈,,组件库,*Lint等开发
了;包括基础的业务代码编写
,如果需要写单元测试
的话还需要同时完成单元测试编写;以及数据方面的简单的本地mock
和数据聚合
;数据聚合的意思是前端将几个联调
环节,通常服务端需要给前端一个数据结构文档
,code码文档
;并且要求服务端接口需要保证都遵守这个规范。通常服务端开发前,需要给出接口文档
,公司一般都有mock服务
,前端先通过mock服务进行数据联调,避免阻塞开发
很多人会忽略这个阶段,如果说开发联调完成后可以达到60分,那么这个部分是项目在基础上加分
埋点
:这个没有写在开发阶段,因为这项,在编码上看,是在完成开发联调之后做的,在功能层面看,也是一个分析数据来做优化的性能
:在开发完成后,需要自测基本性能是否达标,比如图片体积是否过大
,打包后的体积
,依赖模块分析,浏览器兼容性
,手机兼容性
等等,这些都跟性能指标相关,需要一个标准CR
:提测前进行code review
;需要关注,代码规范问题、单元测试是否达标,以及UI稿还原度,上一步性能标准是否达标自测/修复
:测试用例评审后测试通常会给一份冒烟用例
,最基本的自测就是按照冒烟用例走一遍,看到业内还有一些有UI自动化测试
等;都通过后进行代码合并到主分支。这里写了一个UI体验问题衡量标准
的意思是,之前在碰到一个案例,C端商城订单列表的时间筛选,UI给了一个类似PC端的选择,手动选择开始时间、结束时间的年月日,当时我负责封装下拉菜单组件,同事找到我说,能不能把下拉菜单组件暴露XX事件(时间筛选是在下拉菜单里面);当时我是觉得这个要求比较偏业务,然后觉得这个需求不合理,于是研究了下主站的订单筛选,张下面这样,在移动端还让人选择年月日的确不合理呀,于是乎让UI重新出稿
这一步也是必备技能,现在公司一般都有自己搭建的一套自动构建发布流程,按照流程走就没啥问题。流量大的产品还需要考虑A/Btest
、灰度发布
;以及一些SaaS产品,PaaS产品需要考虑的私有化部署
上线后并不是万事大吉了,数据、监控、用户转化才决定着产品的真实情况
异常/性能监控
:前端JS异常监控
,JS执行错误是非常致命的,监控到错误后进行修改能给代码带来不少稳定性;前端性能
通常是指首屏时间、网络请求时间等等指标,主要标识产品的体验性好不好。之前写过两篇相关文章:指路别再用performance计算首屏时间了!! ;面试必问:前端性能监控Performance
服务器监控
:比如域名是否可访问,应用健康度,机器性能等,这部分主要看公司有无提供能力,团队想做一般很难告警
:异常监控后会对应用负责人进行邮件或者APP告警异常处理
:一般两种情况,出现线上紧急问题,切复现不出来问题,这时候就可以借用监控来寻找蛛丝马迹了,很紧急的问题,可能还需要有降级方案
;第二种就是没有业务反馈问题,也需要日常去查看异常监控,可能小问题隐藏着大bug完整前端研发流程图如下,可以看到是一个闭环的
当然这是比较完整的流程,小需求、临时需求可能是这些流程的一部分
参考:
#3
今年正好是我步入前端
完整工作的一年,20届
毕业,到现在工作一年半
这一年,带我的导师离职了,部门调整了一波又一波,我也在变化中不断适应;过程中有很多收获
,也有不少遗憾
想了很久怎么写这篇文章,纯总结的话大部分是自己的经历,可能对大家没啥用,个人感觉自己技术水平虽然还一般,但是工作软实力还8错,所以过程中,尽量加写一些案例,希望对大家有用,一起成长~
上半年
开始在掘金陆续写文章,总共输出了19篇
,不算多,但大部分都是很用心写出来的;大部分是工作中沉淀的解决方案
,还有一些自己学习总结笔记
写文章的目的很简单,记录&总结
;工作中经常会听到,自己写的代码自己都不认识了,所以趁着我还记得的时候写出来orz
比较惊喜
的是,其中一篇上了周榜前三,收到了一份掘金的周边三件套
写文章,我自己的经验是从
业务&团队
出发,凭空写文章非但没有办法实践,也没有这么多精力、或动力不足,反正我是卷不动>-<
这一年沉淀了7
个仓库,虽然star数
不多,但都是自己码上去的
这里要感谢我的导师,从他身上我学到了
最佳实践
的精神,因此工作中遇到一些复杂场景,我的经验是有意识的总结&沉淀
21年可以总结为三部曲“挑战-变化-适应”
年初,我跟导师(校招生都有导师带)所负责的业务线
扩张到了3条
,导师将其中我比较熟悉的一条交由我负责(带几个外包一起干),这对刚工作不久的我来说,其实是有一定挑战
的。
当时我遇到以下几个问题:
干好项目,保质保量
信任
我这个"前端负责人"
“帮助业务成功”
这里再次感谢导师的培养,如果说上面是
技术
的培养,这里是工作能力
的培养。其实我跟导师当时面临着资源匮乏的大问题,在这个节点上,导师没有让我去参与其他两个业务线打杂
,而是让我保持聚焦
这里我的经验是“如果你每天的工作没有了思考,像一个机器一样麻木,这时候需要警醒”,有可能是没用对方法,效率太低,一直在做
重复工作
,还是就是不够聚焦
,没有精力去思考了。可以尝试跟主管沟通交流,及时调整状态
当时我采取的办法:
第一点一定是负责
,主动承担
及时响应
,在群里看到是前端问题的,或者前端可以去解决的,主动站出来解决
用户体验
是我们需要关注的重点,在需求评审时,对交互提出专业的建议
是可以加分
的一个案例:业务线下五个产品转交过来UI、交互不统一;产品计划UI统一,但没有时间出方案。当时我
主动站出来
制定了UI&交互统一方案(也是闲的没事干),拉着产品、测试针对一页的改动点一一评审,主动联系UI支持,最后带着外包改造了100+
页面,当然我们效率还是比较高的,最终探索了vue多工程间公共模块处理最佳实践。把通用的组件都视为公共模块,在UI统一的同时,也优化了前端代码,因此建立了项目组成员对自己的信任
勇于挑战自我
,创新思维
试错机会
的,不畏惧
,敢于提出技术创新
了解竞品
,多看多学,为什么这个需求做,这个需求不做,只有了解了业务后,你才跟产品有默契,提出创新建议当时我们的业务是典型的微前端场景,且产品计划中需要做一个统一控制台,之前在组里做其他业务线也做过微前端,于是我自己搞了一个tob系统微前端实践总结
这里我的经验是,
不要给自己设限
,有人把自己当作接需求的机器,只关心代码实现,却不知道做这个需求是为了干嘛,这也是我初期犯的错。其实正好相反,我们做的需求都是为了服务用户。所以在接需求的时候,可以站在用户的角度思考
,这么实现是不是用户能接受的交互,体验是否跟系统保持一致风格等;甚至,你也可以发现产品中缺失的部分,提需求
学会举一反三
,学会技术沉淀
当时基于业务在小组内提出做了一个将私有源npm包处理为离线包的自动化工具,虽然不难,但是是我第一个从创意到落地的小工具,也收获到了主管的肯定
当时我收获到的肯定:
“作为1年的应届生,非常有想法,不断学习并举一反三,成为非常棒的前端负责人”
认真努力,刻苦专研业务。出色的动手能力,对自己有较高的要求。目标清晰明确,执行干净利索
收到这些肯定还是很开心的,也来之不易。后来离开部门的时候,同事们也都约饭为我送行祝好,一波曾经一起奋斗的同事~
随着公司一波又一波的调整
,自己在A部门所负责的业务线,面临着产品方向不确定
的大问题,一直停留在原地,也有一些同事离职,再一次调整前(每次调整意味重新梳理方向),经过考虑,正直学习期的我,当前的重点是在项目中锻炼,于是我选择了内部异动
这里我的建议是,要
有主见
,选择自己当前最需要的
其实我自己当时也是挺纠结的,留原部门,有业绩,踏实的干下去,年底晋升不会少,但是产品方向不确定,意味着未来可能很长一段时间都做不了啥,担心浪费成长的最佳时间;选择一个新的部门,需要从0开始,这个过程其实是需要成本的,但是新部门可以接触新的场景、技术栈,利于成长。请教了挺多人,相比之下,我选择了后者。我的观点还是趁年轻,多尝试,多经历
说来也是天意,跟B部门主管聊了一个多小时,一拍即合,自己在上一个部门业务更多交给外包去,自己花了挺多时间做工程化的东西,以及阅读相关资料;而B部门是刚成立的业务跑的很快,缺少工程化这部分的东西。其实当时这么说的时候,自己还是有一些压力的,毕竟经验有限,且自己都是照葫芦画瓢。我的职级算是比较低,一般异动至少社招级别才要,经过B部门主管协调,9月份我就到B部门了
适应前期
来到这边,跟之前的部门变化还是挺大,这是一个年轻的团队
,所以团队基建
的机会比较多,我看到可以做的事情比较多(也庆幸自己的选择是正确的)
不过这些认知都来自上一个部门的沉淀,上一个部门沉淀了5年,很多东西都比较规范了,不同的是在上一个部门,我是规范的遵循者
,到这边是提出者
、制定者
于是我刚进来就跟同事一起做了团队代码规范方案,在输出架构图的时候,比较清晰明了,基本得到了组内成员的技术信任
。方案里面的各个部分,我没有自己去实现,而是在制定方案后,找到感兴趣的同事一起参与
,比如脚手架部分,分给一个同事负责了中高级前端必备:如何设计并实现一个脚手架,并不是我不会写,恰好相反,自己写过的东西,这个脚手架对自己来说,成长不大,但是给一个没接触过的人来说,是一个锻炼的好机会。对我来说,帮助了一个同学在某一方面成长。
适应中期
适应中期
多端组件库
,也遇到不少问题,打怪中...总之也是自己比较感兴趣的~这里我的经验是,在进入一个新部门前期,需要发现机会
证明自己
,建立信任
。学以致用
,把自己之前的经验搬过来,精益求精
,在之前的基础生做的更好。主动跟同事技术交流
,拉近距离,互相学习
。我一直觉得一个人的力量有限,一个团队才能做出更多东西,而这个前提是,大家互帮互助
在业务中沉淀了3篇专利,一次年中总结,部门多次分享
我的经验是,多总结、多分享。自己
多争取、珍惜
机会
最大的遗憾是,年初制定的学一个框架
的flag倒的很彻底;由于之前部门是Vue
,现在部门是React
,导致两者都没有深入学习,严重拖后腿了T_T(像极了什么学科差越来越差)
第二大遗憾,没有机会做一个0-1的项目
,在部门A做的事迭代,本来部门B有机会的,突然暂停了>_<
基础知识依旧不扎实,我的详细计划还没列一年就没了🤯
还欠着好几个工作中沉淀总结,偷懒一时爽,一直偷懒一直爽,快要不记得了,得赶紧写!!!!
对新技术没有找到结合业务的机会用进来,还是不够厉害,对于什么技术适用于新业务,还没有把控风险
的能力吧
没有晋升...本来年终主管通知准备的,公司调整到明年了(呵呵哒)
这里就不写了,在学会跟自己相处的路上
明年总结见~我这次一定做个计划,明年总结用计划复盘(第一个flag立住了!!)
一句主管写给我的话,分享给大家~
最终你相信什么,就能成为什么。因为世界上最可怕的两个词,一个叫执着,一个叫认真,认真的人.改变自己,执着的人改变命运。只要在路上,就没有到不了的地方,遇见更好的自己。
希望大家22年都能遇见更好的自己
本文根据 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 个主要方向进行归类和建设,供大家参考:
如上是一般性前端基建的主要方向和分区,不论是 PC 端还是移动端,这些都是基础的建设点。业务阶段、团队能力的差异,体现在基建上,在于产出的完整性、颗粒度、深入度和自动化的覆盖范围。
下面,会针对一些大家都感兴趣的方向,结合我们团队过去一年的建设产出,为大家列举一些前端基建类产品的案例,以供参考。
规范是最应该先行的,始皇帝初统六国即“书同文车同轨”,规范意味着标准,是团队的共识,是沟通协作的基础。而文档,是最容易被忽略的事情之一,除了明面上重要的技术文档、业务稳定之外,还包括了行间的有效注释。想想,有多少时间是花在琢磨别人的代码逻辑,或刚接手某个业务得问多少人才能搞明白你面前那几个仓库是怎么回事,又有多少故障是因为不清楚前任留下的坑在哪里不小心踩雷。
对于规范的制定,需要强调的一点,是规范的产出应是团队内大部分同学的共识,应该是集体审美。规范一旦确定就应该严格执行,要能形成团队行为的一致性。对于文档,为了写而写的文档是垃圾,不如不写。文档的重点在说人话,在于有效性,在于直观、省事、不饶。想想一个 UI 组件库的文档,先给你看可交互的 Demo 再提供 API 信息,和直接开头就罗列一大堆的 API 文字介绍,哪种对阅读者的感受更好、心理成本更低?
本地开发环境,相信是任何一个团队都会做的标配,省事的可能直接拥抱框架选型对应的全家桶,如 Vue 全家桶,或者用 Webpack 撸一个脚手架。能力多一些的会再为脚手架提供一些插件服务,如 Lint 或者 Mock。从简单的一个本地脚手架,到复杂的一个工程化套件系统,其目的都是为了本地开发流程的去人肉化、自动化。
我们团队的本地开发环境基建,是一个工程化套件环境,核心理念就是尽量 “一步搞定所有事”,把本地环境的配置和使用尽量变的傻瓜化无脑化。比如本地初始化一个应用的环境,从 CLI 命令行的操作出发的话(实际上政采云前端团队现在已完全 GUI 化),一个 zoo init
命令就能搞定全部的本地环境搭建,这个全部是指在终端执行回车后,从仓库本地目录的生成到 npm 依赖的自动化安装到脚手架插件的初始化再到唤起一个浏览器窗口,都是自动化执行的。是的,连 npm install
和 dev
什么的都不用执行,能省一步操作就省一步,楚王好细腰,少就是性感。下图是 CLI 本地工程套件的架构图:
其实目前团队的日常研发,已经基本上脱离了 CLI 操作,统一到了团队自研的桌面客户端 “敦煌” 平台。基于客户端的能力,能将分散的工程能力进行聚合,并形成链路的串联能力,结合 GUI 的直观和简便操作,进一步的省事。通过桌面客户端,可以将日常的前端研发链路上的操作都聚合进来,从组件开发到模板开发再到应用开发;从唤起编辑器到启动调试环境、进行包更新到打包部署发布。同时桌面端系统还能和其他的研发系统进行打通,形成更多的能力。
一般情况下,前端团队都会完善自己的组件库体系,有些情况下一些 UI 组件库可能采用社区开源的优秀三方库,如 antd,但多多少少还会有自己的业务组件库需要封装。工具的价值在于抹平差异,将基础标准一致化。对于组件开发,前面所述的 CLI 工具链是这里的底层依赖,同理还有后面介绍的模板开发与使用,以及应用的开发。通过工具进行组件的开发和管理,可以较好的实现诸如组件命名标准化、版本标准化、查找便利性、开发流程简化等,还能实现组件的应用场景统计和版本覆盖率等涉及到组件在接入场景更新成本相关的必要统计。
同飞冰类似,我们也沉淀了一套类似的模板化能力,便于中后台业务场景的快速开发。因为中后台的业务场景相对固化,诸如表单、列表等居多,基于模板的方式可以省掉很大一部分制作 Demo 和实现交互的成本。模板的前提是 UI 组件库的完备,和标准的中后台交互、视觉设计,基于此沉淀标准化的业务模板库,根据场景选择合适的模板,配置下页面信息和路径后,就可以一键安装到本地并自动化配置好路由,安装好依赖。
项目的创建与管理,从一开始我们的目标就是 “去耦合,单人可全流程 Hold” —— 意思是在项目的创建、本地环境的搭建(也包括了环境的升级)、分支管理、构建、部署等环节,前端同学可以完全一人搞定。不需要因为权限的问题找人帮忙建仓库;不需要因为组件、区块、模板、应用(SPA/MPA)、选型(React/Vue)导致本地开发环境的标准不一致进而每次都得学新的;不需要头疼不同业务的版本流程不一致导致还得问这问那;不需要还得人肉的去配置打包脚本;不需要每次部署都得找人(或者是运维)帮忙... 总之,我们希望借助工具抹平日常中太多的不对称,将开发者的专注力重新尽量拉回简单纯粹的编码中。即使是一个对 Git、命令行、应用管理流程不太明白的校园新人,在桌面端可视化工程的系统辅助下,也能很愉快的开始编码。
前面我们提到了前端团队的规范(标准化)、工具链(CLI)、基于工具链之上的可视化辅助客户端(GUI),提到了组件(模块)、模板、应用。对工具的抽象和业务的可复用抽象,是一个团队的基础资产。简化到 Webpack 撸一个脚手架 + 一套开源三方 UI 组件库,剩下的拼装式生产全靠人肉;复杂些诸如阿里系正在突破的 UI2Code 、编辑器等能力,将标准、流程更自动化,进一步的去人肉。基础资产这部分,我们团队目前业务阶段下的建设分层如下:
前端具备自己的构建部署系统,便于专业化方面更好的流程控制。政采云前端团队在 2019 年下半年建设了自己的构建部署系统,实现了云打包、云检测和自动化部署(打通对接运维的部署系统)。新的独立系统在设计之初,重点就是希望能实现一种 Flow 的流式机制,以便实现代码的合规性静态检测能力。这部分在系统中最终实现了一套插件化机制,可以按需配置不同的检测项,如某检测项检测不通过,最终会阻塞发布流程,这些检测项有诸如:
可视化搭建系统是进一步高效利用组件的上层建筑。页面是由组件(业务模块)组成,搭建系统将组件的拼装由本地人肉操作,产品化到系统中可视化拼图,将页面搭建、数据配置、构建部署、数据埋点等等产品化,赋能给产品、运营等协作方。前端产出组件,运营搭建页面,既能节省前端的人效,也能让运营能力前置拓展,在营销场景中进一步释放运营的业务能力,实现共赢。
关乎可视化搭建系统的更多,可以查看我们团队之前输出的这篇文章:《前端工程实践之可视化搭建系统》
系统架构图:
部署流程图:
在很多公司,数据埋点与分析往往是 BI 部门的事情。在政采云,因为公司前期 BI 能力相对不足,前端团队首先发起并推动了面向业务的 Web 数据埋点收集和数据分析、可视化相关的全系统建设。前后实现了埋点规范、埋点 SDK、数据收集及分析、PV/UV、链路分析、转化分析、用户画像、可视化热图、坑位粒度数据透出等数据化能力。
更多数据埋点与分析相关,可以查看我们团队之前输出的这篇文章:《前端工程实践之数据埋点分析系统》
页面性能,90% 在前端。尤其是像我们公司现阶段 toB 为主的业务,不同于我的老东家(淘宝、蘑菇街)早已移动端占绝对主导,我们依然是 PC 场景占大头,在页面性能方面问题还是比较突出。过去 1 年时间内,给大家可参考的路径是,我们首先发起了图体积优化,针对占据页面体积大头、请求数大头的图片首先发起优化策略,采用规范+工具的方式帮助业务快速实现图体积的优化,相关沉淀可见早先团队的这篇《为你重新系统梳理下, Web 体验优化中和图有关的那些事》。后来,我们逐步基于 Node 能力将梳理出的影响页面性能的点,实现了自动化检测能力,并依据不同的业务场景区分设计检测模型,再后来做了定时任务来实现性能的连贯性分析及数据走势能力,再之后又增加了业务性能数据大盘和每周的红黑榜。关于页面性能自动化分析系统的更多细节,可阅读我们团队早前的文章 《自动化 Web 性能分析之 Puppeteer 爬虫实践》、《自动化 Web 性能优化分析方案》。
上面介绍的一部分政采云前端团队的技术基础建设,基本上都是在 2019 年一年内逐步建设落地并取得结果的。下图是在上一年周期中的建设里程碑,能看出对应的建设周期和节奏。在我这个团队,没有单独设立独立的前端架构组,前端下的团队都是业务团队,我们同学从业务支撑中沉淀问题,针对问题进行思考和聚敛,从业务问题出发针对性的推进对应的建设。
对于研发同学来说,身价取决于解决问题的能力,取决于面对不同的业务问题是否具备解决问题的方案。我们团队很庆幸的点在于,业务处于快速发展期,问题很多,既有的沉淀很少,我们很幸运的可以在帮业务解决问题、跟随业务快速发展的过程中,几乎是从零开始做这些建设。这是很可贵、很难得的一段经历,因为大部分的公司要么是体量还没到需要做这些的地步,要么是早就做完了轮不到,无法全程看到整个体系的发展。公司要为员工创造环境,但员工的成长最终是靠自己。所以同学们都认可用业余的时间参与建设、甚至是主导某个方向,对自身的成长是个宝贵的机会。
凡是建设,必须要有对应的数据收集和分析,数据说明基建带来的改变,说明投入产出比。数据指标的设计,需要在某专项建设的前期即设计好并进行采集,并在整个推动周期和落地后持续收集,这样可以得到一个相对完整的变化曲线,用以作证工作的成效。数据不见得一定是完全精准的,但数据一定要能说明趋势,直观化,反馈准确。
对于人力方面,任何情况下人力都是缺失的。但很多时候我们的建设推不下去,往往不是因为人力的问题,而是没想清楚。《庄子·列寇传》有一则寓言,“朱评漫学屠龙于支离益,单千金之家,三年技成而无所用其巧”。 讲的是一个人散尽家资学习屠龙之技,学成却发现世界上本没有龙。对于研发同学,同样会存在从方案出发找场景的问题,如想学习 Node 不知道如何学习,照着书中的例子学,最后发现都忘了效果很不好。没有一个作家是看小说看成的,也没有一个语言学家是看字典看成的,同理技术专家也不会是通过看技术书籍养成的。在实践中学习,从来都是最快的方式。有价值的事从来都是从业务本身的问题出发。问题就是机会,问题就是长萝卜的坑。
前端在研发体系中话语权偏低的现状,从前端这个职能出现那一刻就存在了。不排除个别研发团队,因其业务模式的原因,对前端的依赖较深,前端的话语权相对偏高。绝大部分的研发团队中,前端的工作,在其他研发眼中,往往是 “技术含量低”、“很薄的一层” 等情况。这个现状的背后,看看下图就知道了:
横、纵,2 个维度。右边的 “纵”,参考网络应用系统的分层体系,前端的传统工作范畴,都是集中在 “用户界面层”,很少能往下深入,深入到网关 ~ 基础设施层。后端则不同。从这个角度看,前端确实很 “薄”。现在良性的一面是,Node 能力为前端提供了向下渗透的服务端能力。一些团队也基于 Node 横向扩展自身的工程化能力,和向业务纵深去拓展前端的系统化能力。
我们再看左边的 “横”。只有很少的前端团队,能较完善的去建设和发展技术体系。对于有了较完善体系的前端团队而言,其技术体系也更多是局限于前端自身的职能范畴,没能较好的互动渗透到业务侧,更多是在自嗨,业务的感知力是很弱的。将技术带来的工程收益,转变为业务收益;将部门内的技术影响,转变为业务影响;将技术场景,升级到业务场景;将团队的基础能力,变为业务能力。跳出前端,围绕并深入业务,这是每一个正在推动团队体系建设的同学要更多想想的事。
基建的内容,是和业务阶段相匹配的。不同团队服务的业务阶段不同,基建的内容和广深度也会不同。高下之分不在于多寡,而在于对业务的理解和支持程度。如果你只需要一根针,千万不要去磨铁棒。
技术的价值,在于解决业务问题;人的身价,在于解决问题的能力。但解决问题,技术基建绝不是银弹,甚至在我来看,都不是排在前三位的。
最后,是一个需要大家都思考下的问题:
原文: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);
axios是一个基于promise
的HTTP
库,可以用在浏览器
或者node.js
中。本文围绕XHR。
axios提供两个http请求适配器,XHR和HTTP。XHR的核心是浏览器端的XMLHttpRequest对象;HTTP的核心是node的http.request方法。
特性:
自Vue
2.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);
});
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);
});
axios文件封装在目录src/utils/https.js
,对外暴露callApi
函数
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
,效果如下
利用环境变量
及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': ""
}
},
}
}
}
常见以下三种
(1)application/json
参数会直接放在请求体中,以JSON格式的发送到后端。这也是axios请求的默认方式。这种类型使用最为广泛。
(2)application/x-www-form-urlencoded
请求体中的数据会以普通表单形式(键值对)发送到后端。
(3)multipart/form-data
参数会在请求体中,以标签为单元,用分隔符(可以自定义的boundary)分开。既可以上传键值对,也可以上传文件。通常被用来上传文件的格式。
callApi
函数暴露contentType
参数,用来配置请求头
,默认值为application/json; charset=utf-8
看到这里大家可以会疑惑,直接通过options
配置headers
不可以嘛,答案是可以的,可以看到newOptions
的取值顺序,先取默认值,再取配置的options
,最后取contentType
,contentType
能满足绝大部分场景,满足不了的场景下可用options
配置
通过options
配置headers
,写n遍headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'}
;而通过contentType
配置,传参json || urlencoded || multipart
即可
当contentType
=== urlencoded
时,qs.stringify(data)
// 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
请求类型参数为axios
的options
的method
字段,传入对应的请求类型如post
、get
等即可
不封装,使用原生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
},
});
官方文档如下
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,
})
}
// src/utils/https.js
const defaultOptions = {
timeout: 15000,
}
// src/utils/https.js
const defaultOptions = {
withCredentials: true,
}
通过.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)
时,当多个接口返回的错误信息一致时,会存在重复提示
的问题,如下图
优化方案,利用防抖
,实现错误提示一次,更优雅
代码可访问github
// 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
文件暴露其他模块,其他文件按功能模块划分
文件
axios
封装没有一个绝对的标准,且需要结合项目中实际场景
来设计,但是毋庸置疑,axios-ajax的封装是非常有必要的
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是被称为“Web前端开发神器”的Webstrom。在使用vue以后就各种不自在了,最大问题是性能占用过高,也可能是我们电脑太Low 。
为此我们还做了一套公共同个node_modules的打包框架,目的就是为了提高项目解析和创建新项目的效率。
使用公共依赖,打包根据每个模块下的config配置文件单独打包资源。
超神之路,势必要选择一把趁手的武器,如果是紫色稀有甚好。我们后端一直用的VS,据听说堪称“宇宙第一”。开始确实不习惯VS Code,当听说是基于Web开发的时候已经膜拜了,也让我对前端的发展充满期待。VS Code加持,还怕什么不能超越的。
2017年7月,公司搬迁新址,郑州CBD,大棒子-玉米楼对面。为了对得起CBD,SVN升级GitLab,mobile-cli回归vue-cli,VS Code挂载Git,航天载人计划启动。前端研发团队发展到10人。
个人认为,脱离业务谈架构有些扯淡的。阿里也是在后期涌现各类优秀框架和提高能效的工具。因为前期的业务真的繁重,不忙的时候“加班”写写组件。忙的时候,时间不够休息。毕竟业务第一。
这个阶段,经历了上百个大大小小业务需求。衍生了很多公共业务组件和功能组件,包含:登录/注册、身份校验、支付充值、城市选择、地图定位等。其中涉及一些业务接口和逻辑,不适合发布npm的。为了提高协同效率,在本地服务器搭建了一套私有库,并做了文档规范。
随着人员增加,组件的管理和维护变的异常繁重,于是在年末开发了一套组件库管理工具Bomb。主要将组件可视化,方便调用。并集成了Node+koa的后端模板,小程序模板,php模板等。
新人查找公共组件很麻烦,怎么办?
有没有可视化UI,直接上手创建项目?vue1.0都有UI管理
每次写后台都要写接口请求方法,重复三遍就不想写,怎么办?
我不喜欢看文档,我喜欢看图,怎么办?
我加入团队就是想深入了解后端,有快速入门吗?
本地文件云管理,图片云管理,项目云盘有吗?
。。。。
怼嘛,拽过来Electron,引入Node负责文件的写入、打包、管理等。写个工具管理组件库,提升团队协作效率。
小程序一年发了30个版本,经历了从吐血到吐血的过程。当以为出血撸出来的版本是稳定版本的时候,才发现多端维护是个深坑。
从基础模板开发到mpvue框架的代替。从1.0到3.0,从简单到复杂再到简单。其实也验证了我们团队的变化,人少的时候做一个最简单的版本,人多的时候不甘愿简单挑战复杂,挑战原生。人员技术精进的时候,立足性能立足体验,回归简单,回归轻量。
当发现小程序维护深坑的时候,前端伙伴已经处于崩溃边缘了。我记得一个伙伴一个月加班了20天,才赶到领导安排的时间节点写完业务。这时订单稳定,平台稳定,性能稳定,我说换框架,这TM不是坑是什么?
为什么换?兼容多平台、多端的能力,在当时的前端看来前卫,但作为软件开发者早已习以为常且必然是趋势。大幅度提高人效,多端通用成为了最核心的动力。
从vue到react的转变。还获得支付宝小程序创新大赛月度亚军和总决赛二等奖。也是第一批投身支付宝小程序生态建设的团队。参与了开发文档的维护和建议。(说小白鼠的,过分了)
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
本篇文章讲怎么在前端团队快速制定并落地代码规范!!!
干货,拿走这个仓库
9月份换了一个新部门,部门成立时间不长,当时组内还没有统一的代码规范
(部分工程用了规范,部分没有,没有统一的收口)
小组的技术栈框架有Vue
,React
,Taro
,Nuxt
,用Typescript
,算是比较杂了,结合到部门后续还可能扩展其他技术栈,我们从0-1实现了一套通用的代码规范
到现在小组内也用起来几个月了,整个过程还算是比较顺利且快速
,最近得空分享出来~
不是很了解的话,指路
首先,跟主管同步,团队需要一个统一的规范,相信主管也在等着人来做起来
第一步收集团队的技术栈情况
,确定规范要包括的范围
把规范梳理为三部分ESLint
、StyleLint
、CommitLint
,结合团队实际情况分析如下
可扩展性
常见以下3种方案
团队制定文档式代码规范,成员都人为遵守这份规范来编写代码
靠人来保证代码规范存在
不可靠
,且需要人为review代码不规范,效率低
直接使用业内已有成熟规范,比如css使用StyleLint官方推荐规范stylelint-config-standard、stylelint-order,JavaScript使用ESLint推荐规范eslint:recommended等
a) 开源规范往往不能满足团队需求,
可拓展性差
; b) 业内提供的规范都是独立的(stylelint只提供css代码规范,ESLint只提供JavaScript规范),是零散的
,对于规范初始化或升级存在成本高、不可靠问题(每个工程需要做人为操作多个步骤)
基于StyleLint、ESLint制定团队规范npm包,使用团队制定规范库
a) 该方案解决可扩展性差的问题,但是第二点中的(b)问题依旧存在
整体技术思路图如下图,提供三个基础包@jd/stylelint-config-selling
、@jd/eslint-config-selling
、@jd/commitlint-config-selling
分别满足StyleLint
、ESLint
、CommitLint
@jd/stylelint-config-selling
包括css、less、sass(团队暂未使用到)@jd/eslint-config-selling
包括Vue、React、Taro、Next、nuxt(团队暂未使用到)...,还包括后续可能会扩展到需要自定义的ESLint插件或者解析器@jd/commitlint-config-selling
统一使用git向上提供一个简单的命令行工具,交互式初始化init
、或者更新update
规范
几个关键点
lerna是一个管理工具,用于管理包含多个软件包(package)的 JavaScript项目,业内已经广泛使用了,不了解的可以自己找资料看下
项目结构如下图
如下图,包@jd/eslint-config-selling
的依赖包都写在了生产依赖,而不是开发依赖
解释下:
开发依赖&生产依赖
开发依赖
:业务工程用的时候不会下载
开发依赖中的包,业内常见的规范如standard
、airbnb
都是写在开发依赖
@jd/eslint-config-selling
外,需要自己去安装前置依赖包,如eslint
、根据自己选择的框架安装相关前置依赖包如使用的Vue需要安装eslint-plugin-vue
...使用成本、维护升级成本较高生产依赖
:业务工程用的时候会下载
这些包
@jd/eslint-config-selling
后,无需关注前置依赖包@jd/eslint-config-selling
中所有写在生产依赖的包,即使有些用不到,比如你使用的是React,却安装了eslint-plugin-vue
这个比较简单,提供交互式命令,支持一键初始化或者升级3种规范,就不展开说了
不会的,指路中高级前端必备:如何设计并实现一个脚手架
组里现在还没有项目模版脚手架,后续有的话需要把规范这部分融进去
什么是一个好的规范?
基本每个团队的规范都是不一样的,团队各成员都认同并愿意遵守的规范就是一个好的规范
所以确定好技术方案后,涉及到的各个规范,下图,我们在小组内分工去制定
,比如几个人去制定styleLint的,几个人制定Vue的...
然后拉会评审
,大家统一通过的规范才敲定
最后以开源的方式维护升级
,使用过程中,遇到规范不合适的问题,提交issue,大家统一讨论确定是否需要更改规范
以上就是我们团队在前端规范落地方面的经验~
如果大家感兴趣,可查看github仓库
我们想要手写一个Promise
,就要遵循 Promise/A+ 规范,业界所有 Promise
的类库都遵循这个规范
本篇文章写如何手写promise及其周边方法,每个方法从“定义->案例->实现”
的思路展开
定义
主要参考的阮一峰老师的 ECMAScript 6 入门promise章节案例
为最基础的自己手写的,保证简单易懂实现
结合 Promise/A+ 规范实现阅读本文前建议熟悉阮一峰老师的 ECMAScript 6 入门promise章节,熟悉promise的语法
定义
Promise
对象是一个构造函数,用来生成Promise
实例Promise
构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。它们是两个函数,由 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
现象
在 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))
}
}
}
定义
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
}
前一个回调函数,有可能返回的还是一个
Promise
对象(即有异步操作),这时后一个回调函数,就会等待该Promise
对象的状态发生变化,才会被调用
现象
如下代码,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>'))
}
}
x可能的值
// 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
then
,resolve成功函数y
,reject失败函数r
,成功函数y
可能还是一个promise
,递归执行resolvePromise(promise2, y, resolve, reject)
x
只是普通的对象,比如{then: 1}
,直接reslove(x)
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是一个普通值,直接成功即可
}
}
定义
then
方法的第一个参数是resolved
状态的回调函数,第二个参数是rejected
状态的回调函数,它们都是可选的
现象
如下代码,p.then().then()
省略参数
reslove时,最后一个then
的resolve
输出data
依旧可以获取到数据
p.then(data => {return data}).then(data => {return data})
reject时,最后一个then
的reject
输出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 }
...
}
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
定义
Promise.all()
方法用于将多个Promise实例,包装成一个新的Promise实例Promise.all()
方法接受一个数组作为参数,p1
、p2
、p3
都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve
方法,将参数转为 Promise 实例,再进一步处理p
的状态由p1
、p2
、p3
决定,分成两种情况
p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数。p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成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)
}
}
})
}
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))
}
定义
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} )
)
}
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);
}
现象
注意,下面代码第二个例子输出的是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
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
[{ 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;
}
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()
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);
});
};
// 考察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);
}
}
// replace不会改变原字符串
// 正则表达式:. 匹配除换行符 \n 之外的任何单字符;* 匹配前面的子表达式零次或多次; ? 匹配前面的子表达式零次或一次,或指明一个非贪婪限定符
function template(str, context) {
return str.replace(/\$\{(.*?)\}/g, (match, key) => {
return context[key]
});
}
const arr = [1,2,3,4,5] // 无语法错误
const arr1 = arr.push(6) // 6
document.body.style = 'background: red'
document.body.style = 'background: black'
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
}
'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
}
根据简历问项目
根据简历问项目
多工程公共模块处理方式
实现一个调度器
根据简历问
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.