可可西里

可可西里 查看完整档案

北京编辑辽宁工业大学  |  计算机 编辑  |  填写所在公司/组织 segmentfault.com/u/z_rh 编辑
编辑

world no bug

个人动态

可可西里 收藏了文章 · 10月12日

你的垂直居中有问题?我竟无法反驳 🤦🏻‍♂️

前言

我们平常实现的垂直居中不是真正的垂直居中?何出此言!很多时候,往往自己明明正确的实现了垂直居中,但是 UI/UX 依旧说你的垂直居中有问题,然后自己仔细一看确实好像在视觉效果上存在一些偏差,但是仔细看自己实现的垂直居中代码却丝毫没有问题。今天我们就探讨一下这个有趣问题的由来、解决方案以及文字排版的未来。

Github 👈

发现问题

垂直居中的方式有很多种,这里我们在父级元素使用 display:flex;align-items:center 属性对子元素进行垂直居中,如下图:

似乎这个垂直居中已经非常完美了,但是你却收到了来自 UI/UX 的反馈和质疑

UI/UX:咦?你这垂直居中有问题~🥴

开发:哪里有问题,代码完全没有问题啊~

UI/UX:不信你自己看,文字的上半边空白部分比下半边多了 1px

开发:还真是...😑终究是逃不过设计的眼睛啊~

而我们真正想要效果是下面这样的

初探问题

细心的小伙伴,马上就会发现啊,罪魁祸首就是文本的 line-height 这个属性。那我们现在用 <p> 包含三个不同 font-family<span> 得到了下图这样的效果,对 font-size 相同的 font-family 不同的文本元素会产生具有不同高度。


问题找到了,导致垂直居中只是近似垂直居中的根本原因就是 line-height!在标准的文本框中,实际上文本上方和下方总是会有多余的空间,默认行高保留的多余空间会导致文本不总是在文本框中居中。因此,我们实现的垂直居中会存在不符合预期的情况,line-height 越大,问题就会越明显。同时,不同的字体,默认的 line-height 也会不同,因此,在字体大小,行高和文本框位置不变的情况下,更改字体也会导致文本的对齐结果。

🤯 这个问题就到此为止了吗?不,我们还不知道 line-height 为什么是这样的,以及为什么要这样。🤷🏻‍♂️

追根溯源

image

大约140年前,印刷术仍然使用单个引线盒手动设置字体。在印刷时,为了使文本更具可读性,排字机将铅条(leading)插入空格线。因此,打印的文字高度加上铅条的高度的总和就是总行高。
image

80 年代早期的图形设计软件保留了相同的传统,人们可以直接添加底部 leading 来控制基线之间的间距,同时也导致 line height 的增加。其他软件则让人们可以在直接调整行高。例如,在 1990 年发布的 Photoshop 的第一个版本中,用户可以定义 leading 的值,然后将其添加到字体大小中。许多工具也开始两个基线之间的距离叫做 line-height

1989 年,当 Bert Bos 和 HåkonWium Lie 起草 CSS 的第一个提案时,起初他们仍然遵循印刷世界的“旧”方式。但是很快,他们将决定做出合乎逻辑的又是根本性的改变,将 leading 一分为二,并将其放在每行的上方和下方,称其为 “half-leading”。为什么要这样做呢?目的就是为了使文本框看起来均匀(https://www.w3.org/TR/CSS1/#t...)。

”half-leading“ 非常取巧的避免了上下边框的不均匀性,但是同时也带来了一些问题。字体系列中的每种字体大小都带有默认的行高。为了容纳某些字符和重音符号,通常会将默认行高设置的高于其包含的文字。增加默认行高后再增加两个 “half-leading”,这样使得文本框变得更大了。这样一顿操作下来,就是我们现在面临文本无法垂直居中最根本的原因了。

image

多年来,Web 设计工具一直不支持半领先技术,因此,许多团队讨论了为什么屏幕设计和浏览器之间的布局差异如此之大。最重要的是,并不是每个人都知道这种复杂的技术细节,这经常导致设计师和开发人员之间容易发生一些口角🤦🏻‍♂️

解决方案

手动调整相关 CSS 属性

手动调整相关 CSS 属性,但是这样会出现一系列魔幻的 margin 或者 padding 的值,同时过于随机,并且只针对特定的字体、浏览器、操作系统。很明显这不是一个很好的解决方案。

裁剪工具

EightShapes Text Crop Tool

从工具中选择一种字体或加载自定义字体,然后使用滑块测量所需的顶部和底部裁剪。该工具会计算属性和公式,只需将生成的 SCSS,LESS 或 Stylus mixin 复制并粘贴到源代码中。

为什么只能针对一种字体,而不是所有字体都使用呢?

工具原理是通过 before 和 after 伪元素来定义负边距,这种做法在保留多行文本块中的行之间的行高的同时,删除了顶部和底部的空白。

// Top crop:
$ top-crop +($ desired-line-height-$ line-height-crop)*($ font-size-with-crop / 2)),0)/ $ font-size-with-crop;
//Bottom crop:
$ bottom-crop +($ desired-line-height-$ line-height-crop)*($ font-size-with-crop / 2)),0)/ $ font-size-with-crop;

最终结果是一个混合字体,无论字体大小如何都可以工作,并且只需要无单位的行高即可执行计算。但是将 mixin 应用于其他字体时,效果却不太好。

工具实现的是将 Em Square 裁剪为字体的 baseline 和 cap height,但是,每种字体都有不同的 Em Square Definition。因此,适用于一种字体的 “magic numbers” 不一定适用于其他字体。

新的 CSS 草案

很早开始就有很多人碰到了类似的问题,并且向 W3C 进行了反馈,我们不是第一个遇见这个问题的人。

为修复 CSS 文本布局相关问题,Leading-trim 成为了 CSS 内联布局草案的一部分

image

h1 { 
 text-edge: cap alphabetic;
 leading-trim: both;
}

借助 leading-trim,最终可以解决在网站上看到的所有神秘的对齐问题。可以在不破坏设计意图的情况下更换字体。

image

以下是是 leading-trim 属性相关草案(尚未成为规范

Ref

Leading-Trim: The Future of Digital Typesetting
The 4px baseline grid — the present
Getting to the bottom of line height in Figma
The Thing With Leading in CSS
Intro to Font Metrics
Deep dive CSS: font metrics, line-height and vertical-align
CSS Inline Layout Module Level 3
Cropping Away Negative Impacts of Line Height
css-inline Leading control at start/end of block #3240

image.png

查看原文

可可西里 收藏了文章 · 10月12日

《前端每日实战》第174号作品:日历

image

中秋国庆长假休完,又要投入到工作中了,做一个日历纪念一下吧。

效果预览

按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。

https://codepen.io/comehope/pen/mdEyWEv

源代码下载

每日前端实战系列的全部源代码请从 github 下载:

https://github.com/comehope/front-end-daily-challenges

代码解读

这个日历的开发流程是,定义 DOM 结构之后,进行页面整体布局,绘制出日历薄的模样,然后分别布局上部的当前日期和下部的日期表格。完成静态布局之后,再通过脚本来动态生成日期元素,实现一个显示实时日期的动态日历。

一、定义 DOM 结构

dom 的整体结构是一个名为 .container 的容器中包含2个元素,.today 是当前日期,.calendar 是日期表格。

<div class="container">
    <header class="today">
    </header>
    <main class="calendar">
    </main>
</div>

.today 当前日期部分包含2个元素,.month 显示当前月,.day显示当前日。

<header class="today">
    <div class="day">9</div>
    <div class="month">October</div>
</header>

.calcendar 日期表格部分包含一个表头行 .days 和一个表格 .dates。表头按英文习惯以周日为每周的第一天;表格共6行,一共显示42天,可以适应任何月份。
表格中的日期通过类名区分为上月日期 previous-month、当前日期 current-day、下月日期 next-month

<main class="calendar">
    <div class="days">
        <span>Sun</span>
        <span>Mon</span>
        <span>Tue</span>
        <span>Wed</span>
        <span>Thu</span>
        <span>Fri</span>
        <span>Sat</span>
    </div>
    <div class="dates">
        <span class="previous-month">27</span>
        <span class="previous-month">28</span>
        <span class="previous-month">29</span>
        <span class="previous-month">30</span>
        <span>1</span>
        <span>2</span>
        <span>3</span>
        <span>4</span>
        <span>5</span>
        <span>6</span>
        <span>7</span>
        <span>8</span>
        <span class="current-day">9</span>
        <span>10</span>
        <span>11</span>
        <span>12</span>
        <span>13</span>
        <span>14</span>
        <span>15</span>
        <span>16</span>
        <span>17</span>
        <span>18</span>
        <span>19</span>
        <span>20</span>
        <span>21</span>
        <span>22</span>
        <span>23</span>
        <span>24</span>
        <span>25</span>
        <span>26</span>
        <span>27</span>
        <span>28</span>
        <span>29</span>
        <span>30</span>
        <span>31</span>
        <span class="next-month">1</span>
        <span class="next-month">2</span>
        <span class="next-month">3</span>
        <span class="next-month">4</span>
        <span class="next-month">5</span>
        <span class="next-month">6</span>
        <span class="next-month">7</span>
    </div>
</main>

二、页面整体和日历容器布局

先用线性渐变设置页面背景色为灰白过渡色。

body {
    margin: 0;
    height: 100vh;
    background-image: linear-gradient(to bottom, #eee, #ccc);
}

设置容器尺寸,用相对单位 em,并使容器居于页面正中。
为使容器可见,暂为容器填充白色背景。

body {
    display: flex;
    align-items: center;
    justify-content: center;
}

.container {
    width: 32em;
    height: 38em;
    font-size: 14px;
    background-color: white;
}

效果如下图:
image

注释掉刚才临时定义的 background-color 属性,改为用锐利渐变填充,实现上部黄棕色、下部白色的效果。因为黄棕色 sandybrown 是日历主色,后面还会用到,所以把它定义成变量。
再把日历四周设为圆角,底部加双层阴影,模拟多张日历纸叠加的效果。

.container {
    /* background-color: white; */
    --main-color: sandybrown;
    background-image: linear-gradient(to bottom, var(--main-color) 50%, white 50%);
    border-radius: 1em;
    box-shadow: 
        0 2px 1px rgba(0, 0, 0, 0.2),
        0 3px 1px white;
}

效果如下图:
image

接下来绘制一个细节:环扣,它用来连接日历的上、下两部分。使用2个伪元素来绘制,这样不用显式地增加 DOM 元素,纯用 CSS 实现装饰效果。两个环扣的样式相同,所以大部分代码是共享的,仅它们所处的位置不同,一个在日历左侧,一个在日历右侧。

.container {
    position: relative;
}

.container::before,
.container::after {
    content: '';
    position: absolute;
    width: 0.6em;
    height: 2.3em;
    background-color: white;
    top: calc(50% - 2.3em / 2);
    border-radius: 0.3em;
    box-shadow: 
        0 3px 1px rgba(0, 0, 0, 0.3),
        0 -1px 1px rgba(0, 0, 0, 0.2);
}

.container::before {left: 2em;}
.container::after {right: 2em;}

效果如下图:
image

至此,一个接近真实场景中的日历的轮廓绘制完成了,接下来布局日历上显示的文字内容。

三、上部当前日期布局

因为整个日历分成上下两部分,所以先令当前日期 .today 占据上部的50%空间,这样表格 .calendar 自然就被挤到下部了。

.today {
    height: 50%;
}

效果如下图:
image

因为整个日历都使用同一种字体,所以把字体样式定义在容器中,采用无衬线字体。
.today 的布局很简单,用的都是字号、颜色、行间距等基本属性。

.container {
    font-family: sans-serif;
}

.today {
    padding: 3em;
    box-sizing: border-box;
    color: white;
}

.today .day {
    font-size: 8em;
    line-height: 1em;
    font-weight: bold;
}

.today .month {
    font-size: 4em;
    line-height: 1em;
    text-transform: lowercase;
}

效果如下图:
image

四、下部日期表格布局

表格包括表头和表体两部分,设置好它们的宽度,然后用 flex 布局令其垂直居中排列。

.calendar {
    padding-top: 3.5em;
    display: flex;
    flex-direction: column;
    align-items: center;
}

.calendar .days,
.calendar .dates {
    width: 28em;
}

表头和表格都是一行7列,这里采用 grid 布局实现。

.calendar .days,
.calendar .dates {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    line-height: 2em;
    text-align: center;
}

效果如下图:
image

表格已经成形,剩下的细节是为文字上色。
表格里一共有5种语义元素:表头、当前日期、本月日期、上月日期、下月日期,这些不同语义的元素都靠 CSS 类名来区分。表头和当前日期用主色,本月日期用深灰色,上月日期和下月日期用浅灰色。

.calendar .days {
    color: var(--main-color);
    font-weight: bold;
    text-transform: uppercase;
}

.calendar .dates {
    color: dimgray;
}

.calendar .dates .previous-month,
.calendar .dates .next-month {
    color: lightgray;
}

.calendar .dates .current-day {
    color: var(--main-color);
    font-weight: bold;
}

效果如下图:
image

最后,再增加一个鼠标悬停特效,当在日期上悬停时令背景变灰、文字变白,并用 transition 实现平滑过渡。

.calendar .dates span:hover {
    background-color: lightgray;
    color: white;
}

.calendar .dates span {
    transition: 0.3s;
}

至此,整个日历的静态布局全部完成了。

五、动态脚本

程序的入口是一个名为 init 的函数,其中声明了一个 Calendar 类的实例,再调用它的 render() 方法来渲染页面。

function init() {
    let calendar = new Calendar(new Date())
    calendar.render()
}

window.onload = init

Calendar 类接收一个日期参数,以此来初始化年、月、日数据。render() 方法分别调用了 renderDay()renderMonth() 来渲染当前日期和当前月份。因为 Date 对象返回的月份属性是数字,为了把它显示成英文月份名称,就定义了一个存放月份名称的数组。

let Calendar = function(date) {
    let year = date.getFullYear()
    let month = date.getMonth()
    let day = date.getDate()

    function renderDay() {
        document.querySelector('.day').textContent = day
    }

    function renderMonth() {
        const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
        document.querySelector('.month').textContent = MONTHS[month]
    }

    this.render = function() {
        renderDay()
        renderMonth()
    }
}

接下来要渲染日期表格了。
我们先引入一个日历库 calendar-dates(github 地址:https://dance2die.github.io/calendar-dates/),它负责计算日期、星期、月份之间的关系,为给定的年月输出对应的日历数据。通过 import 语句导入该库文件。

import CalendarDates from 'https://cdn.jsdelivr.net/npm/calendar-dates@2.6.1/dist/calendardates.esm.js'

image

注意,对于使用了 import 语句的脚本,在 <script> 标签中需增加 type="module" 属性。

<script data-original="script.js" type="module"></script>

然后,在 Calendar 类中定义一个 renderDates() 函数来渲染日历列表。如何获得日历可以参考官方文档,这里就不多说了,我觉得有点别扭的是必须用 async/await 的方式来调用。每个日期有 datetype 属性,date 就是日期数值,type 有3个值:previouscurrentnext,分别代表上月、本月、下月,我们就用这2个属性来处理日期元素的样式。
最后,别忘了要在 render() 里调用一下 renderDates() 函数。

async function renderDates() {
    const calendarDates = new CalendarDates();
    const domList = document.querySelector('.dates')
    domList.innerHTML = ''
    for (const meta of await calendarDates.getDates(new Date(year, month))) {
        let span = document.createElement('span')
        span.textContent = meta.date
        span.className = (meta.type == 'current')
            ? (meta.date == day)
                ? 'current-day'
                : ''
            : meta.type + '-month'
        domList.append(span)
    }
}

this.render = function() {
    renderDay()
    renderMonth()
    renderDates()
}

大功告成!

关于我

张偶,网络笔名 @comehope,20世纪末触网,被 Web 的无穷魅力所俘获,自此始终战斗在 Web 开发第一线。

《前端每日实战》专栏是我近年来实践项目式学习方法的笔记,以项目驱动学习,展现从灵感闪现到代码实现的完整过程,亦可作为前端开发的练手习题和开发参考。

拙作《CSS3 艺术》一书于2020年1月由人民邮电出版社出版,全彩印刷,使用100多个生动美观的实例,系统地剖析了 CSS 与视觉效果相关的重要语法,并含有近10小时的视频演示讲解。京东/天猫/当当有售。

查看原文

可可西里 收藏了文章 · 10月10日

10个好用的 HTML5 特性

作者:Ahmad shaded
译者:前端小智
来源:sitepoint
移动端阅读点我
点赞再看,养成习惯

本文 GitHubhttps://github.com/qq44924588... 上已经收录,更多往期高赞文章的分类,也整理了很多我的文档,和教程资料。欢迎Star和完善,大家面试可以参照考点复习,希望我们一起有点东西。

回馈读者,文末送5本《你不知道 的 JavaScript 上劵》下周一开奖,祝大家好运!
https://mp.weixin.qq.com/s/A5...

大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】

在本文中,我列出了十个我过去没用过的HTML5功能,但现在发现它们很有用,废话不多说,让我们开始吧。

🔥 detais 标签

<details>标签向用户提供按需查看详细信息的效果。 如果需要按需向用户显示内容,简单的做法就是使用此<details>标签。 默认情况下,它是收起来的,打开后,它将展开并显示被隐藏的内容。

事例:

<details>
  <summary>Click Here to get the user details</summary>
  <table>
    <tr>
      <th>#</th>
      <th>Name</th>
      <th>Location</th>
      <th>Job</th>
    </tr>
    <tr>
      <td>1</td>
      <td>Adam</td>
      <td>Huston</td>
      <td>UI/UX</td>
    </tr>
  </table>
</details>

运行结果:

图片描述

技巧

在 GitHub Readme 中使用它来显示按需的详细信息。 这是一个示例https://github.com/atapas/notifyme#properties

图片描述

🔥 内容可编辑

contenteditable是可以在元素上设置以使内容可编辑的属性。 它适用于DIVPUL等元素。

注意,当在元素上没有设置contenteditable属性时,它将从其父元素继承该属性。
<h2> Shoppping List(Content Editable) </h2>
 <ul class="content-editable" contenteditable="true">
     <li> 1. Milk </li>
     <li> 2. Bread </li>
     <li> 3. Honey </li>
</ul>

运行结果:

图片描述

技巧

可以让spandiv标签可编辑,并且可以使用css样式向其添加任何丰富的内容。 这将比使用输入字段处理它更好。 试试看!

🔥 Map

HTML <map> 属性 与 <area> 属性一起使用来定义一个图像映射(一个可点击的链接区域)。可点击的区域可以是这些形状中的任何一个,矩形,圆形或多边形区域。如果不指定任何形状,则会考虑整个图像。

事例:

<div>
    <img data-original="circus.jpg" width="500" height="500" alt="Circus" usemap="#circusmap">

    <map name="circusmap">
        <area shape="rect" coords="67,114,207,254" href="elephant.htm">
        <area shape="rect" coords="222,141,318, 256" href="lion.htm">
        <area shape="rect" coords="343,111,455, 267" href="horse.htm">
        <area shape="rect" coords="35,328,143,500" href="clown.htm">
        <area shape="circle" coords="426,409,100" href="clown.htm">
    </map>
 </div>

运行结果:

图片描述

技巧

map有其自身的缺点,但是你可以将其用于视觉演示。

🔥 mark 标签

<p> Did you know, you can <mark>"Highlight something interesting"</mark> just with an HTML tag? </p>

运行结果:

clipboard.png

技巧

可以使用css更改高亮颜色:

mark {
  background-color: green;
  color: #FFFFFF;
}

🔥 data-* 属性

data-*属性用于存储页面或应用程序专用的自定义数据。 可以在 JavaScript 代码中使用存储的数据来创建更多的用户体验。

data-*属性由两部分组成

  • 属性名不能包含任何大写字母,并且必须在前缀“data-”之后至少有一个字符
  • 属性值可以是任何字符串

事例:

<h2> Know data attribute </h2>
 <div 
       class="data-attribute" 
       id="data-attr" 
       data-custom-attr="You are just Awesome!"> 
   I have a hidden secret!
  </div>

 <button onclick="reveal()">Reveal</button>

在 JS 中:

function reveal() {
   let dataDiv = document.getElementById('data-attr');
    let value = dataDiv.dataset['customAttr'];
   document.getElementById('msg').innerHTML = `<mark>${value}</mark>`;
}

注意:要在 JS 中读取这些属性的值,可以通过getAttribute('data-custom-attr')g来获取,但是标准方式是用dataset来获取。

图片描述

技巧

你可以使用它在页面中存储一些数据,然后使用REST调用将其传递给服务器。

🔥 output 标签

<output> 标签表示计算或用户操作的结果。

<form oninput="x.value=parseInt(a.value) * parseInt(b.value)">
   <input type="number" id="a" value="0">
          * <input type="number" id="b" value="0">
                = <output name="x" for="a b"></output>
</form>

图片描述

技巧

如果要在客户端 JS 中执行任何计算,并且希望结果反映在页面上,可以使用<output>,这样就无需使用getElementById()获取元素的额外步骤。

🔥 datalist

<datalist>元素包含了一组<option>元素,这些元素表示其它表单控件可选值.

事例:

<form action="" method="get">
    <label for="fruit">Choose your fruit from the list:</label>
    <input list="fruits" name="fruit" id="fruit">
        <datalist id="fruits">
           <option value="Apple">
           <option value="Orange">
           <option value="Banana">
           <option value="Mango">
           <option value="Avacado">
        </datalist>
     <input type="submit">
 </form>  

图片描述

技巧

dataList的表现很像是一个select下拉列表,但它只是提示作用,并不限制用户在input输入框里输入什么

select标签创建了一个菜单。菜单里的选项通option标签指定。一个select元素内部,必须包含一个option元素,

总的来说就是,它们都可以显示出一个下拉表单框,但是select标签只能在它提供的选项中选择,而datalist不仅可以让你选择,还可以让你自己输入其它的选项。

🔥 Range(Slider)

range是一种 input 类型,给定一个滑块类型的范围选择器。

<form method="post">
    <input 
         type="range" 
         name="range" 
         min="0" 
         max="100" 
         step="1" 
         value=""
         onchange="changeValue(event)"/>
 </form>
 <div class="range">
      <output id="output" name="result">  </output>
 </div>

图片描述

🔥 meter

<meter>元素用来显示已知范围的标量值或者分数值。

<label for="home">/home/atapas</label>
<meter id="home" value="4" min="0" max="10">2 out of 10</meter><br>

<label for="root">/root</label>
<meter id="root" value="0.6">60%</meter><br>

clipboard.png

技巧

不要将<meter>用作进度条来使用,进度条对应的<Progress> 标签。

<label for="file">Downloading progress:</label>
<progress id="file" value="32" max="100"> 32% </progress>

clipboard.png

🔥 Inputs

对于input标签类型,最常见的有 textpassword 等等,下面列举一些比较少见的语法。

required

要求输入字段必填。

<input type="text" id="username1" name="username" required>

clipboard.png

autofocus

文本输入字段被设置为当页面加载时获得焦点:

<input type="text" id="username2" name="username" required autofocus>

用正则表达式验证

可以使用regex指定一个模式来验证输入。

<input type="password" 
            name="password" 
            id="password" 
            placeholder="6-20 chars, at least 1 digit, 1 uppercase and one lowercase letter" 
            pattern="^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,20}$" autofocus required>

Color picker

一个简单的颜色选择器。

<input type="color" onchange="showColor(event)">
<p id="colorMe">Color Me!</p>

图片描述


原文:https://dev.to/atapas/10-usef...

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

交流

文章每周持续更新,可以微信搜索「 大迁世界 」第一时间阅读和催更(比博客早一到两篇哟),本文 GitHub https://github.com/qq449245884/xiaozhi 已经收录,整理了很多我的文档,欢迎Star和完善,大家面试可以参照考点复习,另外关注公众号,后台回复福利,即可看到福利,你懂的。

查看原文

可可西里 收藏了文章 · 9月30日

9 个JavaScript 技巧

作者:Orkhan Jafarov
译者:前端小智
来源:dev
点赞再看,微信搜索 【大迁世界】 关注这个没有大厂背景,但有着一股向上积极心态人。本文 GitHubhttps://github.com/qq44924588... 上已经收录,文章的已分类,也整理了很多我的文档,和教程资料。

大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】

1.生成指定范围的数字

在某些情况下,我们会创建一个处在两个数之间的数组。假设我们要判断某人的生日是否在某个范围的年份内,那么下面是实现它的一个很简单的方法 😎

let start = 1900, end = 2000;
[...new Array(end + 1).keys()].slice(start);
// [ 1900, 1901, ..., 2000]

// 还有这种方式,但对于很的范围就不太稳定
Array.from({ length: end - start + 1 }, (_, i) => start + i);

2.使用值数组作为函数的参数

在某些情况下,我们需要将值收集到数组中,然后将其作为函数的参数传递。 使用 ES6,可以使用扩展运算符(...)并从[arg1, arg2] > (arg1, arg2)中提取数组:

const parts = {
  first: [0, 2],
  second: [1, 3],
}

["Hello", "World", "JS", "Tricks"].slice(...parts.second)
// ["World", "JS"]

3.将值用作 Math 方法的参数

当我们需要在数组中使用Math.maxMath.min来找到最大或者最小值时,我们可以像下面这样进行操作:

const elementsHeight =  [...document.body.children].map(
  el => el.getBoundingClientRect().y
);
Math.max(...elementsHeight);
// 474

const numbers = [100, 100, -1000, 2000, -3000, 40000];
Math.min(...numbers);
// -3000

4.合并/展平数组中的数组

Array 有一个很好的方法,称为Array.flat,它是需要一个depth参数,表示数组嵌套的深度,默认值为1。 但是,如果我们不知道深度怎么办,则需要将其全部展平,只需将Infinity作为参数即可 😎

const arrays = [[10], 50, [100, [2000, 3000, [40000]]]]

arrays.flat(Infinity)
// [ 10, 50, 100, 2000, 3000, 40000 ]

5. 防止代码崩溃

在代码中出现不可预测的行为是不好的,但是如果你有这种行为,你需要处理它。

例如,常见错误TypeError,试获取undefined/null等属性,就会报这个错误。

const found = [{ name: "Alex" }].find(i => i.name === 'Jim')

console.log(found.name)
// TypeError: Cannot read property 'name' of undefined

我们可以这样避免它:

const found = [{ name: "Alex" }].find(i => i.name === 'Jim') || {}

console.log(found.name)
// undefined

6. 传递参数的好方法

对于这个方法,一个很好的用例就是styled-components,在ES6中,我们可以将模板字符中作为函数的参数传递而无需使用方括号。 如果要实现格式化/转换文本的功能,这是一个很好的技巧:

const makeList = (raw) =>
  raw
    .join()
    .trim()
    .split("\n")
    .map((s, i) => `${i + 1}. ${s}`)
    .join("\n");

makeList`
Hello, World
Hello, World
`;
// 1. Hello,World
// 2. World,World

7.交换变量

使用解构赋值语法,我们可以轻松地交换变量 使用解构赋值语法 😬:

let a = "hello"
let b = "world"

// 错误的方式
a = b
b = a
// { a: 'world', b: 'world' }

// 正确的做法
[a, b] = [b, a]
// { a: 'world', b: 'hello' }

8.按字母顺序排序

需要在跨国际的项目中,对于按字典排序,一些比较特殊的语言可能会出现问题,如下所示 😨

// 错误的做法
["a", "z", "ä"].sort((a, b) => a - b);
// ['a', 'z', 'ä']

// 正确的做法
["a", "z", "ä"].sort((a, b) => a.localeCompare(b));
// [ 'a', 'ä', 'z' ]
localeCompare() :用本地特定的顺序来比较两个字符串。

9.隐藏隐私

最后一个技巧是屏蔽字符串,当你需要屏蔽任何变量时(不是密码),下面这种做法可以快速帮你做到:

const password = "hackme";
password.substr(-3).padStart(password.length, "*");
// ***kme

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:https://dev.to/gigantz/9-java...

交流

文章每周持续更新,可以微信搜索「 大迁世界 」第一时间阅读和催更(比博客早一到两篇哟),本文 GitHub https://github.com/qq449245884/xiaozhi 已经收录,整理了很多我的文档,欢迎Star和完善,大家面试可以参照考点复习,另外关注公众号,后台回复福利,即可看到福利,你懂的。

查看原文

可可西里 收藏了文章 · 9月28日

在 Vue 中对事件进行防抖和节流

点赞再看,微信搜索 【大迁世界】 关注这个没有大厂背景,但有着一股向上积极心态人。本文 GitHubhttps://github.com/qq44924588... 上已经收录,文章的已分类,也整理了很多我的文档,和教程资料。

大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】

有些浏览器事件可以在短时间内快速触发多次,比如调整窗口大小或向下滚动页面。例如,监听页面窗口滚动事件,并且用户持续快速地向下滚动页面,那么滚动事件可能在 3 秒内触发数千次,这可能会导致一些严重的性能问题。

如果在面试中讨论构建应用程序,出现滚动、窗口大小调整或按下键等事件请务必提及 防抖(Debouncing) 和 函数节流(Throttling)来提升页面速度和性能。这两兄弟的本质都是以闭包的形式存在。通过对事件对应的回调函数进行包裹、以自由变量的形式缓存时间信息,最后用 setTimeout 来控制事件的触发频率。

Throttle: 第一个人说了算

throttle 的中心思想在于:在某段时间内,不管你触发了多少次回调,我都只认第一次,并在计时结束时给予响应。

先给大家讲个小故事:现在有一个旅客刚下了飞机,需要用车,于是打电话叫了该机场唯一的一辆机场大巴来接。司机开到机场,心想来都来了,多接几个人一起走吧,这样这趟才跑得值——我等个十分钟看看。于是司机一边打开了计时器,一边招呼后面的客人陆陆续续上车。在这十分钟内,后面下飞机的乘客都只能乘这一辆大巴,十分钟过去后,不管后面还有多少没挤上车的乘客,这班车都必须发走。

在这个故事里,“司机” 就是我们的节流阀,他控制发车的时机;“乘客”就是因为我们频繁操作事件而不断涌入的回调任务,它需要接受“司机”的安排;而“计时器”,就是我们上文提到的以自由变量形式存在的时间信息,它是“司机”决定发车的依据;最后“发车”这个动作,就对应到回调函数的执行。

总结下来,所谓的“节流”,是通过在一段时间内无视后来产生的回调请求来实现的。只要一位客人叫了车,司机就会为他开启计时器,一定的时间内,后面需要乘车的客人都得排队上这一辆车,谁也无法叫到更多的车。

对应到实际的交互上是一样一样的:每当用户触发了一次 scroll 事件,我们就为这个触发操作开启计时器。一段时间内,后续所有的 scroll 事件都会被当作“一辆车的乘客”——它们无法触发新的 scroll 回调。直到“一段时间”到了,第一次触发的 scroll 事件对应的回调才会执行,而“一段时间内”触发的后续的 scroll 回调都会被节流阀无视掉。

现在一起实现一个 throttle:

// fn是我们需要包装的事件回调, interval是时间间隔的阈值
function throttle(fn, interval) {
  // last为上一次触发回调的时间
  let last = 0
  
  // 将throttle处理结果当作函数返回
  return function () {
      // 保留调用时的this上下文
      let context = this
      // 保留调用时传入的参数
      let args = arguments
      // 记录本次触发回调的时间
      let now = +new Date()
      
      // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
      if (now - last >= interval) {
      // 如果时间间隔大于我们设定的时间间隔阈值,则执行回调
          last = now;
          fn.apply(context, args);
      }
    }
}

// 用throttle来包装scroll的回调
const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)

Debounce: 最后一个人说了算

防抖的中心思想在于:我会等你到底。在某段时间内,不管你触发了多少次回调,我都只认最后一次。

继续讲司机开车的故事。这次的司机比较有耐心。第一个乘客上车后,司机开始计时(比如说十分钟)。十分钟之内,如果又上来了一个乘客,司机会把计时器清零,重新开始等另一个十分钟(延迟了等待)。直到有这么一位乘客,从他上车开始,后续十分钟都没有新乘客上车,司机会认为确实没有人需要搭这趟车了,才会把车开走。

我们对比 throttle 来理解 debounce:在throttle的逻辑里,“第一个人说了算”,它只为第一个乘客计时,时间到了就执行回调。而 debounce 认为,“最后一个人说了算”,debounce 会为每一个新乘客设定新的定时器。

现在一起实现一个 debounce:

// fn是我们需要包装的事件回调, delay是每次推迟执行的等待时间
function debounce(fn, delay) {
  // 定时器
  let timer = null
  
  // 将debounce处理结果当作函数返回
  return function () {
    // 保留调用时的this上下文
    let context = this
    // 保留调用时传入的参数
    let args = arguments

    // 每次事件被触发时,都去清除之前的旧定时器
    if(timer) {
        clearTimeout(timer)
    }
    // 设立新定时器
    timer = setTimeout(function () {
      fn.apply(context, args)
    }, delay)
  }
}

// 用debounce来包装scroll的回调
const better_scroll = debounce(() => console.log('触发了滚动事件'), 1000)

用 Throttle 来优化 Debounce

debounce 的问题在于它“太有耐心了”。试想,如果用户的操作十分频繁——他每次都不等 debounce 设置的 delay 时间结束就进行下一次操作,于是每次 debounce 都为该用户重新生成定时器,回调函数被延迟了不计其数次。频繁的延迟会导致用户迟迟得不到响应,用户同样会产生“这个页面卡死了”的观感。

为了避免弄巧成拙,我们需要借力 throttle 的思想,打造一个“有底线”的 debounce——等你可以,但我有我的原则:delay 时间内,我可以为你重新生成定时器;但只要delay的时间到了,我必须要给用户一个响应。这个 throttle 与 debounce “合体”思路,已经被很多成熟的前端库应用到了它们的加强版 throttle 函数的实现中:

// fn是我们需要包装的事件回调, delay是时间间隔的阈值
function throttle(fn, delay) {
  // last为上一次触发回调的时间, timer是定时器
  let last = 0, timer = null
  // 将throttle处理结果当作函数返回
  
  return function () { 
    // 保留调用时的this上下文
    let context = this
    // 保留调用时传入的参数
    let args = arguments
    // 记录本次触发回调的时间
    let now = +new Date()
    
    // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
    if (now - last < delay) {
    // 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器
       clearTimeout(timer)
       timer = setTimeout(function () {
          last = now
          fn.apply(context, args)
        }, delay)
    } else {
        // 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应
        last = now
        fn.apply(context, args)
    }
  }
}

// 用新的throttle包装scroll的回调
const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)

document.addEventListener('scroll', better_scroll)

在 Vue 里使用 lodash 中的 Debouncing 和 Throttling

事件节流和防抖是提高性能或降低网络开销的好方法。虽然 Vue 1曾经支持对事件的节流和防抖,但是在Vue 2中为了保持核心的简单性,删除对事件的节流和防抖的支持。因此,在Vue 2对对事件进行防抖和节流我们可以使用 lodash 来做。

安装

可以通过 yarn 或 npm 安装 lodash。

# Yarn
$ yarn add lodash
# NPM
$ npm install lodash --save
注意:如果我们不想导入lodash的所有内容,而只导入所需的部分,则可以通过一些Webpack构建自定义来解决问题。 还可以使用lodash.throttlelodash.debounce等软件包分别安装和导入lodash的各个部分。

throttling 方法

要对事件进行节流处理方法非常简单,只需将要调用的函数包装在lodash的_.throttle函数中即可。

<template>
  <button @click="throttledMethod()">Click me as fast as you can!</button>
</template>

<script>
import _ from 'lodash'

export default {
  methods: {
    throttledMethod: _.throttle(() => {
      console.log('I get fired every two seconds!')
    }, 2000)
  }
}
</script>

debouncing 方法

尽管节流在某些情况下很有用,但一般情况我们经常使用的是防抖。 防抖实质上将我们的事件分组在一起,并防止它们被频繁触发。 要在Vue组件中使用节流,只需将要调用的函数包装在lodash的_.debounce函数中。


<template>
  <button @click="throttledMethod()">Click me as fast as you can!</button>
</template>

<script>
import _ from 'lodash'

export default {
  methods: {
    throttledMethod: _.debounce(() => {
      console.log('I only get fired once every two seconds, max!')
    }, 2000)
  }
}
</script>

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug


参考:

Throttling and Debouncing in JavaScript
The Difference Between Throttling and Debouncing
Examples of Throttling and Debouncing
Remy Sharp’s blog post on Throttling function calls
前端性能优化原理与实践

交流

文章每周持续更新,可以微信搜索 【大迁世界 】 第一时间阅读,回复 【福利】 有多份前端视频等着你,本文 GitHub https://github.com/qq449245884/xiaozhi 已经收录,欢迎Star。

查看原文

可可西里 收藏了文章 · 9月14日

这 16 个 CSS 伪类,助你提升布局效率!

作者:Chidume Nnamdi
译者:前端小智
来源:mediuum
点赞再看,微信搜索 【大迁世界】 关注这个没有大厂背景,但有着一股向上积极心态人。本文 GitHubhttps://github.com/qq44924588... 上已经收录,文章的已分类,也整理了很多我的文档,和教程资料。

大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】

css 伪类是用于向某些选择器添加特殊的效果,是动态的,指当前元素所处的状态或者特性。只有一个元素达到一个特定状态时,它可能得到一个伪类的样式;当状态改变时,它又会失去这个样式。

这篇文章在一定程度上鼓励你在构建UI时使用更简单的CSS和更少的 JS。熟悉 CSS 所提供的一切是实现这一目标的一种方法,另一种方法是实现最佳实践并尽可能多地重用代码。

接下介绍一些大家可能还不熟悉的一些伪类及其用例,希望对大家日后有所帮助。

::first-line | 选择文本的第一行

::first-line 伪元素在某块级元素的第一行应用样式。第一行的长度取决于很多因素,包括元素宽度,文档宽度和文本的文字大小。

::first-line 伪元素只能在块容器中,所以,::first-line伪元素只能在一个display值为block, inline-block, table-cell 或者 table-caption中有用。在其他的类型中,::first-line 是不起作用的。

用法如下:

p:first-line {
  color: lightcoral;
}

::first-letter | 选择这一行的第一字

CSS 伪元素 ::first-letter会选中某块级元素第一行的第一个字母。用法如下:

<style>
    p::first-letter{
      color: red;
      font-size: 2em;
    }
</style>

<p>前端小智,不断努,终身学习者!</p>

clipboard.png

::selection| 被用户高亮的部分

::selection 伪元素应用于文档中被用户高亮的部分(比如使用鼠标或其他选择设备选中的部分)。

div::selection {
      color: #409EFF;
}

clipboard.png

:root | 根元素

:root 伪类匹配文档树的根元素。对于 HTML 来说,:root 表示 <html> 元素,除了优先级更高之外,与 html 选择器相同。

在声明全局 CSS 变量时 :root 会很有用:

:root {
  --main-color: hotpink;
  --pane-padding: 5px 42px;
}

:empty | 仅当子项为空时才有作用

:empty 伪类代表没有子元素的元素。子元素只可以是元素节点或文本(包括空格),注释或处理指令都不会产生影响。

div:empty {
  border: 2px solid orange;
  margin-bottom: 10px;
}

<div></div>
<div></div>
<div>
</div>

clipboard.png

只有第一个和第二个div有作用,因为它们确实是空的,第三个 div 没有作用,因为它有一个换行。

:only-child | 只有一个子元素才有作用

:only-child 匹配没有任何兄弟元素的元素.等效的选择器还可以写成 :first-child:last-child或者:nth-child(1):nth-last-child(1),当然,前者的权重会低一点。

p:only-child{
  background: #409EFF;
}

<div>
  <p>第一个没有任何兄弟元素的元素</p>
</div>
<div>
  <p>第二个</p>
  <p>第二个</p>
</div>

clipboard.png

:first-of-type | 选择指定类型的第一个子元素

:first-of-type表示一组兄弟元素中其类型的第一个元素。

.innerDiv p:first-of-type {
  color: orangered;
}

上面表示将 .innerDiv 内的第一个元素为 p 的颜色设置为橘色。

<div class="innerDiv">
    <div>Div1</div>
    <p>These are the necessary steps</p>
    <p>hiya</p>
    
    <p>
        Do <em>not</em> push the brake at the same time as the accelerator.
    </p>
    <div>Div2</div>
</div>

clipboard.png

大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】

:last-of-type | 选择指定类型的最后一个子元素

:last-of-type CSS 伪类 表示了在(它父元素的)子元素列表中,最后一个给定类型的元素。当代码类似Parent tagName:last-of-type的作用区域包含父元素的所有子元素中的最后一个选定元素,也包括子元素的最后一个子元素并以此类推。

.innerDiv p:last-of-type {
    color: orangered;
}

上面表示将 .innerDiv 内的的最后一个元素为 p 的颜色设置为橘色。

clipboard.png

nth-of-type() | 选择指定类型的子元素

:nth-of-type() 这个 CSS 伪类是针对具有一组兄弟节点的标签, 用 n 来筛选出在一组兄弟节点的位置。

.innerDiv p:nth-of-type(1) {
    color: orangered;
}

<div class="innerDiv">
  <div>Div1</div>
  <p>These are the necessary steps</p>
  <p>hiya</p>
  
  <p>
      Do <em>not</em> push the brake at the same time as the accelerator.
  </p>
  <div>Div2</div>
</div>

clipboard.png

:nth-last-of-type() | 在列表末尾选择类型的子元素

:nth-last-of-type(an+b) 这个 CSS 伪类 匹配那些在它之后有 an+b-1 个相同类型兄弟节点的元素,其中 n 为正值或零值。它基本上和 :nth-of-type 一样,只是它从结尾处反序计数,而不是从开头处。

.innerDiv p:nth-last-of-type(1) {
    color: orangered;
}

这会选择innerDiv元素中包含的类型为p元素的列表中的最后一个子元素。

<div class="innerDiv">
    <p>These are the necessary steps</p>
    <p>hiya</p>
    <div>Div1</div>
    <p>
        Do the same.
    </p>
    <div>Div2</div>
</div>

clipboard.png

:link | 选择一个未访问的超链接

:link伪类选择器是用来选中元素当中的链接。它将会选中所有尚未访问的链接,包括那些已经给定了其他伪类选择器的链接(例如:hover选择器,:active选择器,:visited选择器)。

为了可以正确地渲染链接元素的样式,:link伪类选择器应当放在其他伪类选择器的前面,并且遵循LVHA的先后顺序,即::link:visited:hover:active:focus伪类选择器常伴随在:hover伪类选择器左右,需要根据你想要实现的效果确定它们的顺序。

a:link {
    color: orangered;
}
<a href="/login">Login<a>

clipboard.png

:checked | 选择一个选中的复选框

:checked CSS 伪类选择器表示任何处于选中状态的radio(<input type="radio">), checkbox (<input type="checkbox">) 或("select") 元素中的option HTML元素("option")。

input:checked {
  box-shadow: 0 0 0 3px hotpink;
}

<input type="checkbox" />

clipboard.png

大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】

:valid | 选择一个有效的元素

:valid CSS 伪类表示内容验证正确的<input> 或其他 <form> 元素。这能简单地将校验字段展示为一种能让用户辨别出其输入数据的正确性的样式。

input:valid {
  box-shadow: 0 0 0 3px hotpink;
}

clipboard.png

:invalid | 选择一个无效的元素

:invalid CSS 伪类 表示任意内容未通过验证的 <input> 或其他 <form> 元素。

input[type="text"]:invalid {
    border-color: red;
}

:lang() | 通过指定的lang值选择一个元素

:lang() CSS 伪类基于元素语言来匹配页面元素。

/* 选取任意的英文(en)段落 */
p:lang(en) {
  quotes: '\201C' '\201D' '\2018' '\2019';
}

:not() | 用来匹配不符合一组选择器的元素

CSS 伪类 :not() 用来匹配不符合一组选择器的元素。由于它的作用是防止特定的元素被选中,它也被称为反选伪类(negation pseudo-class)。

来看一个例子:

.innerDiv :not(p) {
    color: lightcoral;
}
<div class="innerDiv">
    <p>Paragraph 1</p>
    <p>Paragraph 2</p>
    <div>Div 1</div>
    <p>Paragraph 3</p>
    <div>Div 2</div>
</div>

clipboard.png

Div 1Div 2会被选中,p 不会被选 中。

人才们的 【三连】 就是小智不断分享的最大动力,如果本篇博客有任何错误和建议,欢迎人才们留言,最后,谢谢大家的观看。


原文:https://blog.bitsrc.io/css-ps...

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug


交流

文章每周持续更新,可以微信搜索【大迁世界 】第一时间阅读,回复【福利】有多份前端视频等着你,本文 GitHub https://github.com/qq449245884/xiaozhi 已经收录,欢迎Star。

查看原文

可可西里 收藏了文章 · 9月9日

浅谈js防抖和节流

防抖和节流严格算起来应该属于性能优化的知识,但实际上遇到的频率相当高,处理不当或者放任不管就容易引起浏览器卡死。所以还是很有必要早点掌握的。(信我,你看完肯定就懂了)

从滚动条监听的例子说起

先说一个常见的功能,很多网站会提供这么一个按钮:用于返回顶部。
返回顶部按钮

这个按钮只会在滚动到距离顶部一定位置之后才出现,那么我们现在抽象出这个功能需求-- 监听浏览器滚动事件,返回当前滚条与顶部的距离
这个需求很简单,直接写:

function showTop  () {
    var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  console.log('滚动条位置:' + scrollTop);
}
window.onscroll  = showTop

但是!

图片描述

在运行的时候会发现存在一个问题:这个函数的默认执行频率,太!高!了!。 高到什么程度呢?以chrome为例,我们可以点击选中一个页面的滚动条,然后点击一次键盘的【向下方向键】,会发现函数执行了8-9次
图片描述

然而实际上我们并不需要如此高频的反馈,毕竟浏览器的性能是有限的,不应该浪费在这里,所以接着讨论如何优化这种场景。

防抖(debounce)

基于上述场景,首先提出第一种思路:在第一次触发事件时,不立即执行函数,而是给出一个期限值比如200ms,然后:

  • 如果在200ms内没有再次触发滚动事件,那么就执行函数
  • 如果在200ms内再次触发滚动事件,那么当前的计时取消,重新开始计时

效果:如果短时间内大量触发同一事件,只会执行一次函数。

实现:既然前面都提到了计时,那实现的关键就在于setTimeout这个函数,由于还需要一个变量来保存计时,考虑维护全局纯净,可以借助闭包来实现:

/*
* fn [function] 需要防抖的函数
* delay [number] 毫秒,防抖期限值
*/
function debounce(fn,delay){
    let timer = null //借助闭包
    return function() {
        if(timer){
            clearTimeout(timer) //进入该分支语句,说明当前正在一个计时过程中,并且又触发了相同事件。所以要取消当前的计时,重新开始计时
            timer = setTimeout(fn,delay) 
        }else{
            timer = setTimeout(fn,delay) // 进入该分支说明当前并没有在计时,那么就开始一个计时
        }
    }
}

当然 上述代码是为了贴合思路,方便理解(这么贴心不给个赞咩?),写完会发现其实 time = setTimeout(fn,delay)是一定会执行的,所以可以稍微简化下:


/*****************************简化后的分割线 ******************************/
function debounce(fn,delay){
    let timer = null //借助闭包
    return function() {
        if(timer){
            clearTimeout(timer) 
        }
        timer = setTimeout(fn,delay) // 简化写法
    }
}
// 然后是旧代码
function showTop  () {
    var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  console.log('滚动条位置:' + scrollTop);
}
window.onscroll = debounce(showTop,1000) // 为了方便观察效果我们取个大点的间断值,实际使用根据需要来配置

此时会发现,必须在停止滚动1秒以后,才会打印出滚动条位置。

到这里,已经把防抖实现了,现在给出定义:

  • 对于短时间内连续触发的事件(上面的滚动事件),防抖的含义就是让某个时间期限(如上面的1000毫秒)内,事件处理函数只执行一次。

节流(throttle)

继续思考,使用上面的防抖方案来处理问题的结果是:

  • 如果在限定时间段内,不断触发滚动事件(比如某个用户闲着无聊,按住滚动不断的拖来拖去),只要不停止触发,理论上就永远不会输出当前距离顶部的距离。

但是如果产品同学的期望处理方案是:即使用户不断拖动滚动条,也能在某个时间间隔之后给出反馈呢?(此处暂且不论哪种方案更合适,既然产品爸爸说话了我们就先考虑怎么实现)
图片描述

其实很简单:我们可以设计一种类似控制阀门一样定期开放的函数,也就是让函数执行一次后,在某个时间段内暂时失效,过了这段时间后再重新激活(类似于技能冷却时间)。

效果:如果短时间内大量触发同一事件,那么在函数执行一次之后,该函数在指定的时间期限内不再工作,直至过了这段时间才重新生效。

实现 这里借助setTimeout来做一个简单的实现,加上一个状态位valid来表示当前函数是否处于工作状态:

function throttle(fn,delay){
    let valid = true
    return function() {
       if(!valid){
           //休息时间 暂不接客
           return false 
       }
       // 工作时间,执行函数并且在间隔期内把状态位设为无效
        valid = false
        setTimeout(() => {
            fn()
            valid = true;
        }, delay)
    }
}
/* 请注意,节流函数并不止上面这种实现方案,
   例如可以完全不借助setTimeout,可以把状态位换成时间戳,然后利用时间戳差值是否大于指定间隔时间来做判定。
   也可以直接将setTimeout的返回的标记当做判断条件-判断当前定时器是否存在,如果存在表示还在冷却,并且在执行fn之后消除定时器表示激活,原理都一样
    */

// 以下照旧
function showTop  () {
    var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  console.log('滚动条位置:' + scrollTop);
}
window.onscroll = throttle(showTop,1000) 

运行以上代码的结果是:

  • 如果一直拖着滚动条进行滚动,那么会以1s的时间间隔,持续输出当前位置和顶部的距离

其他应用场景举例

讲完了这两个技巧,下面介绍一下平时开发中常遇到的场景:

  1. 搜索框input事件,例如要支持输入实时搜索可以使用节流方案(间隔一段时间就必须查询相关内容),或者实现输入间隔大于某个值(如500ms),就当做用户输入完成,然后开始搜索,具体使用哪种方案要看业务需求。
  2. 页面resize事件,常见于需要做页面适配的时候。需要根据最终呈现的页面情况进行dom渲染(这种情形一般是使用防抖,因为只需要判断最后一次的变化情况)

思考总结

上述内容基于防抖和节流的核心思路设计了简单的实现算法,但是不代表实际的库(例如undercore js)的源码就直接是这样的,最起码的可以看出,在上述代码实现中,因为showTop本身的很简单,无需考虑作用域和参数传递,所以连apply都没有用到,实际上肯定还要考虑传递argument以及上下文环境(毕竟apply需要用到this对象)。这里的相关知识在本专栏《柯里化》和《this对象》的文章里也有提到。本文依然坚持突出核心代码,尽可能剥离无关功能点的思路行文因此不做赘述。


惯例:如果内容有错误的地方欢迎指出(觉得看着不理解不舒服想吐槽也完全没问题);如果有帮助,欢迎点赞和收藏,转载请征得同意后著明出处,如果有问题也欢迎私信交流,主页有邮箱地址

查看原文

可可西里 收藏了文章 · 8月21日

提升布局能力!理解 CSS 的多种背景及使用场景和技巧

作者:Ahmad shaded
译者:前端小智
来源:sitepoint
点赞再看,微信搜索 【大迁世界】 关注这个没有大厂背景,但有着一股向上积极心态人。本文 GitHubhttps://github.com/qq44924588... 上已经收录,文章的已分类,也整理了很多我的文档,和教程资料。

大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】

CSS background是最常用的CSS属性之一。然而,并不是所有开发人员都知道使用多种背景。这段时间都在关注使用多种背景场景。在本文中,会详细介绍background-image`属性,并结合图形来解释多个背景使用方式以及其实际好处。

如果你还了解 CSS background 属性,可以去先 MDN 查看相关的知识。

介绍

CSS background属性是以下属性的简写:

background-clip, background-color, background-image, background-origin, background-position, background-repeat, background-size 和 background-attachment.

对于本文,将重点介绍background-imagebackground-positionbackground-size。 你准备好了吗? 让我们开始吧!

考虑下面的例子:

.element {
  background: url(cool.jpg) top left/50px 50px no-repeat;
}

背景图片位于元素的左上角,大小为50px * 50px。 了解并记住位置和大小的顺序很重要。

clipboard.png

在上图中,background-position后面是background-size。它们的顺序是不能调换的,否则无效,如下所示:

.element {
    /* 警告:无效的CSS */
    background: url(cool.jpg) 50px 50px/top left no-repeat;
}

Background Position

元素的定位相对于background-origin属性设置的定位层。我喜欢background-position的灵活性,它有多种定位元素的方式:

  • 关键字值(toprightbottomleftcenter
  • 百分比值,如: 50%
  • 长度值,如:20px, 2.5rem
  • 边缘偏移值,如:top 20px left 10px

clipboard.png

坐标系统从左上角开始,默认值为0% 0%

值得一提的是,top left的值与left top的值相同。 浏览器足够聪明,可以确定其中哪个个用于x轴,哪个用于y轴。

clipboard.png

.element {
    background: url(cool.jpg) top left/50px 50px no-repeat;
    /* 上面与下面相同 */
    background: url(cool.jpg) left top/50px 50px no-repeat;
}

Background Size

对于background-size属性,第一个是width,第二个是height

clipboard.png

不必使用两个值,你可以使用一个值,它表示宽度和高度都一样。

clipboard.png

现在,我已经了解了CSS background的工作原理,下面来探讨下如何使用多个背景。

多个背景

background属性可以具有一层或多层,以逗号分隔。 如果多个背景的大小相同,则其中一个将覆盖另一个背景。

.element {
    background: url(cool.jpg) top left/50px 50px no-repeat,
    url(cool.jpg) center/50px 50px no-repeat;
}

clipboard.png

在上图中,我们有两个背景层。每个位置都不同。这是多背景的基本用法,让我们研究一个更高级的示例。

放置顺序

当放置多个背景时,其中一个背景占据其父级的全部宽度和高度时,放置顺序可能会有点乱,考虑下面例子:

.hero {
  min-height: 350px;
  background: url('table.jpg') center/cover no-repeat,
    url('konafa.svg') center/50px no-repeat; 
}

clipboard.png

我们有一个盘子和一张桌子的图片,你认为哪个会在上面?

答案就是桌子。在CSS中,第一个背景可以放置在第二个背景上,第二个背景可以放置在第三个背景上,依此类推。通过替换背景的顺序,可以得到预期的结果。

clipboard.png

用例和范例

遮罩层

通常,我们可能需要某部分的顶部放置一个遮罩层,以便使文本易于阅读。 通过堆叠两个背景可以轻松完成此操作。

.hero {
    background: linear-gradient(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.15)),
    url("landscape.jpg") center/cover;
}

clipboard.png

好的是,我们可以使用与上述相同的方法对元素应用色彩。 考虑以下:

.hero {
    background: linear-gradient(135deg, rgba(177, 234, 77, 0.25), rgba(69, 149, 34, 0.25),
    url("landscape.jpg") center/cover;
}

clipboard.png

用 CSS 绘图

使用 CSS 渐变绘制的可能性是无限的。 你可以使用linear-gradientradial-gradient等。接着,我们来看看如何使用它两兄弟绘制笔记本电脑。

clipboard.png

拆解笔记本电脑,看看我们需要使用什么渐变。

clipboard.png

拆解笔记本电脑的时,更容易考虑如何使用多个 CSS 背景来实现它。

接下来是图纸。 首先是将每个渐变定义为CSS变量及其大小。 我喜欢使用CSS变量,因为它可以减少代码的复杂性,使代码更简洁,更易于阅读。

:root {
  --case: linear-gradient(#222, #222);
  --case-size: 152px 103px;

  --display: linear-gradient(#fff, #fff);
  --display-size: 137px 87px;

  --reflection: linear-gradient(205deg, #fff, rgba(255, 255, 255, 0));
  --reflection-size: 78px 78px;

  --body: linear-gradient(#888, #888);
  --body-size: 182px 9px;

  --circle: radial-gradient(9px 9px at 5px 5.5px, #888 50%, transparent 50%);
  --circle-size: 10px 10px;
}

现在我们定义了渐变及其大小,下一步是放置它们。 考虑下图,以获得更好的视觉解释。

clipboard.png

显示影像

如前所述,应该首先定义需要在顶部的元素。 在我们的情况下,显示影像应该是第一个渐变。

clipboard.png

显示 LCD

显示屏位于x轴中心,距y轴6px

clipboard.png

显示 外壳

外壳位于显示器下方,位于x轴的中心,距y轴的位置为0px

clipboard.png

大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】

主体

这是图形中最有趣的组件。 首先,主体是一个矩形,每个侧面(左侧和右侧)有两个圆圈。

clipboard.png

最终结果

:root {
  --case: linear-gradient(#222, #222);
  --case-size: 152px 103px;
  --case-pos: center 0;

  --display: linear-gradient(#fff, #fff);
  --display-size: 137px 87px;
  --display-pos: center 6px;

  --reflection: linear-gradient(205deg, #fff, rgba(255, 255, 255, 0));
  --reflection-size: 78px 78px;
  --reflection-pos: top right;

  --body: linear-gradient(#888, #888);
  --body-size: 182px 9px;
  --body-pos: center bottom;

  --circle: radial-gradient(9px 9px at 5px 5.5px, #888 50%, transparent 50%);
  --circle-size: 10px 10px;
  --circle-left-pos: left bottom;
  --circle-right-pos: right bottom;
}

.cool {
  width: 190px;
  height: 112px;

  background-image: var(--reflection), var(--display), var(--case), var(--circle), var(--circle), var(--body);

  background-size: var(--reflection-size), var(--display-size), var(--case-size), var(--circle-size), var(--circle-size), var(--body-size);

  background-position: var(--reflection-pos), var(--display-pos), var(--case-pos), var(--circle-left-pos), var(--circle-right-pos), var(--body-pos);

  background-repeat: no-repeat;

  /*outline: solid 1px;*/
}

混合多种背景

混合使用多个背景时会令人兴奋。 考虑一下您在CSS中有一个背景图像,并且想要将其变成黑白图像。

clipboard.png

.hero {
  background: linear-gradient(#000, #000),
  url("landscape.jpg") center/cover;
  background-blend-mode: color;
}

clipboard.png

人才们的 【三连】 就是小智不断分享的最大动力,如果本篇博客有任何错误和建议,欢迎人才们留言,最后,谢谢大家的观看。


原文:https://css-tricks.com/css-ba...

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

交流

文章每周持续更新,可以微信搜索【大迁世界 】第一时间阅读,回复【福利】有多份前端视频等着你,本文 GitHub https://github.com/qq449245884/xiaozhi 已经收录,欢迎Star。

查看原文

可可西里 收藏了文章 · 8月13日

vue3.0体验卡

前言

vue3.0 Rc(候选发布版本)已经于7月18上线,离正式版本发布已经不远了,鉴于此,本人就通过@vue/composition-api 这个 Vue Composition API 来事先体验下3.0的新特性,为以后能快速上手新版本做准备。

准备工作

下载与引入

  • 下载体验版api
 npm i @vue/composition-api
  • 引入与使用
 1. 在main.js中要引入全部的api
 
 import VueCompositionApi from '@vue/composition-api'

 Vue.use(VueCompositionApi)
 
 2. 在页面中按需引入api
 
 import { 需要使用的api } from '@vue/composition-api'
tips: main.js和局部页面都需要引入,不能只在页面中引入。

开发与使用

reactive和toRefs

reactive创建响应式数据,toRefs把普通数据转换成响应式数据

<template>
  <div class="home">
    <span>{{name}}</span>
    <span>{{num}}</span>
    <button @click="btn">按钮</button>
    <button @click="btn2">按钮2</button>
  </div>
</template>
<script>
// reactive 创建响应式数据对象 --类似data
import { reactive, toRefs } from '@vue/composition-api'
export default {
  name: 'Home',
  setup () {
    // state对象
    const state = reactive({
      name: 'hello world'
    })
    // modelData 
    const modelData = reactive({
      num: 1
    })
    const btn = () => modelData.num++
    const btn2 = () => {
      state.name = '我是不双向绑定的数据,没有toRefs转换不可更改'
      return state
    }
    return {
      ...state,
      ...toRefs(modelData), //把数据转换为响应式
      btn, // 事件
      btn2
    }
  }
}
</script>

tips:

  1. setup中是没有this
  2. 数据、方法都写在setup里面。
  3. 方法里改变值需return这个值
  4. 用了...运算符后使用reactive创建出来的数据都不是响应式数据了,需要使用toRefs转换为ref()类型的响应式数据

ref(推荐)

    • reactive一样创建响应式数据,但更推荐使用。
    <template>
     <div class="RefCom">
       <span>{{refCount}}</span>
       <button @click="refCount+=1">+1</button>
     </div>
    </template>
    <script>
    
    import { ref, reactive } from '@vue/composition-api'
    export default {
     name: 'RefCom',
     setup (props, { root }) {
       const refCount = ref(0) //创建响应式数据
       console.log(refCount.value)
       const data = reactive({
         refCount
       })
       console.log(data.refCount)
       data.refCount++
       console.log(data.refCount)
       return {
         ...data,
         refCount
       }
     }
    }
    </script>
    • 模板上的ref--获取dom
    //父组件
    <template>
      <div class="Father">
        <h1 ref="h1Ref">父组件</h1>
        <som ref="somRef"></som>
      </div>
    </template>
    <script>
    
    import som from './Som'
    import { ref, onMounted } from '@vue/composition-api'
    export default {
      name: 'Father',
      components: {
        som
      },
      setup (props, { root }) {
        const h1Ref = ref(null) //赋值null
        const somRef = ref(null)
        onMounted(() => {
          console.log(h1Ref.value, 'h1的dom')
          console.log(somRef.value, 'som的dom')
        })
        return {
          h1Ref,  //要和模板上ref值相同
          somRef
        }
      }
    }
    </script>
    
     //子组件
     <template>
      <div class="Som">
        <h3>子组件</h3>
      </div>
    </template>
    
    <script>
    export default {
      name: 'som',
      setup (props, { root }) {}
    }
    </script>

    tips:

    1. ref括号里的值就是refCount的值,括号里的值可以是各种类型的值。
    2. setup要通过xxx.value获取ref转换的值。
    3. 模板中无需通过xxx.value展示数据,直接{{xxx}}即可,在return时已经进行了转换了。
    4. ref包裹创建出来的值是个对象,里面就一个属性value
    5. reactive包裹ref创建的值不需要通过XXX.value访问
    6. 新的ref会覆盖旧的ref的值
    7. 通过isRef可以判断是否是ref创建出来的。

    computed

    计算属性:可创建可读可写的计算属性。

    <template>
      <div class="RefCom">
        <span>原值:{{refCount}}</span> |
        <span>
          计算属性值:{{ onlyReadComputed
          }}
        </span> |
        <button @click="refCount+=1">+1</button>
      </div>
    </template>
    <script>
    
    import { ref, computed } from '@vue/composition-api'
    export default {
      name: 'RefCom',
      setup (props, { root }) {
        const refCount = ref(0)
        // 只读的计算属性
        const onlyReadComputed = computed(() => refCount.value + 1)
        // 可读可写的计算属性
        const rwComputed = computed({
          get: () => refCount.value + 1,
          set: value => {
            refCount.value = value - 1
          }
        })
        console.log(onlyReadComputed, '只读计算属性的值')
        rwComputed.value = 11
        console.log(rwComputed, '可读可写计算属性的值')
        return {
          refCount,
          rwComputed,
          onlyReadComputed
        }
      }
    }
    </script>

    watch

    监听数据的变化

    <template>
     <div class="RefCom">
       <span>{{refCount}}</span>
       <span>{{name}}</span>
       <button @click="stopWatch">停止watch</button>
       <input v-model="inputValue" />
     </div>
    </template>
    <script>
    import { ref, reactive, watch, toRefs } from '@vue/composition-api'
    export default {
     name: 'watch',
     setup (props, { root }) {
       const refCount = ref(0)
       const inputValue = ref('')
       const state = reactive({
         name: '张总'
       })
       /* ---监听单个--- */
    
       // ref
       const stop = watch(
         refCount,
         (newValue, oldValue) => {
           console.log(refCount.value)
           console.log('新值:' + newValue, '旧的值:' + oldValue)
         }
       )
    
       const stopWatch = () => {
         stop()
       }
    
       // reactive
       watch(
         () => state.name,
         (newValue, oldValue) => {
           // console.log(refCount.value)
           console.log('新值:' + newValue, '旧的值:' + oldValue)
         }
       )
    
       /* ---监听多个--- */
       watch(
         [() => refCount, () => state.name],
         ([newRefCount, newName], [oldRefCount, oldName]) => {
           console.log('newRefCount:' + newRefCount.value, 'newName:' + newName)
           console.log('oldRefCount:' + oldRefCount.value, 'oldName:' + oldName)
         }
       )
    
       setTimeout(() => {
         refCount.value++
       }, 1000)
    
       setTimeout(() => {
         state.name = '李总'
       }, 3000)
    
       // 异步打印
       const asyncPrint = (val) => {
         return setTimeout(() => {
           console.log(val)
         }, 1000)
       }
    
       // ref
       watch(
         inputValue,
         (newValue, oldValue, clean) => {
           const timeId = asyncPrint(newValue)
           // 每当数据变化的时候清除定时器
           clean(() => clearTimeout(timeId))
         }
       )
    
       return {
         ...toRefs(state),
         refCount,
         stopWatch,
         inputValue
       }
     }
    }
    </script>

    tips:

    1. refreactive的值的监听方法不同,reactive需用方法返回值,() => xxx,ref可直接使用。
    2. 当监听多个时,不管是ref还是reactive创建的值,都需要用方法返回
    3. 在监听多个值时,用数组来解构新旧值时,新值和旧值分别在不同的数组里,和vue2.x不一样。
    4. watch监听返回新值、旧值时还返回了个函数,当前函数在watch被重复执行stop操作时发生,可做些清除操作。常见应用场景有防抖。
    5. 防抖:就是对于频繁触发的事件添加一个延时同时设定一个最小触发间隔,如果触发间隔小于设定的间隔,则清除原来的定时,重新设定新的定时;如果触发间隔大于设定间隔,则保留原来的定时,并设置新的定时;防抖的结果就是频繁的触发转变为触发一次

    生命周期

    • beforeCreate -> setup()
    • created -> setup
    • beforeMount -> onBeforeMount
    • mounted -> onMounted
    • beforeUpdate -> onBeforeUpdate
    • updated -> onUpdated
    • beforeDestroy -> onBeforeUnmount
    • destroyed -> onUnmounted
    • errorCaptured -> onErrorCaptured
    <template>
      <div class="Father">
     
      </div>
    </template>
    <script>
    
    
    import { onMounted, onUpdated, onBeforeUnmount } from '@vue/composition-api'
    export default {
      name: 'Father',
    
      setup (props, { root }) {
    
        onMounted(() => {
          console.log('onMounted')
        })
    
        onUpdated(() => {
          console.log('onUpdated')
        })
    
        onBeforeUnmount(() => {
          console.log('onBeforeUnmount')
        })
    
      }
    }
    </script>
    

    tips:

    1. 去除了beforeCreatecreated生命周期,直接就在setup中,setup执行顺序 是beforeCreate后,created
    2. 其他生命周期就在原本前加上on,功能没有什么变化,且定义在setup函数中
    3. 推荐请求都放在onMounted

    依赖注入

    • provide
    //父组件
    <template>
      <div class="Father">
        <h1>父组件</h1>
        <button @click="color='red'">红色</button>
        <button @click="color='blue'">蓝色</button>
        <button @click="color='yellow'">黄色</button>
        <som></som>
      </div>
    </template>
    <script>
    
    import som from './Som'
    import { provide, ref } from '@vue/composition-api'
    export default {
      name: 'Father',
      components: {
        som
      },
      setup (props, { root }) {
        const color = ref('red') //响应式的值,父组件修改可影响子孙后代
        //注入值
        provide('color', color)
        return {
          color
        }
      }
    }
    </script>
     //子组件
     <template>
      <div class="Som">
        <h3>子组件</h3>
        <Grandson />
      </div>
    </template>
    
    <script>
    import Grandson from './Grandson'
    export default {
      name: 'som',
      components: {
        Grandson
      },
      setup (props, { root }) { }
    }
    </script>
    • inject
    //孙子组件
    <template>
      <div class="Grandson">
        <h5 :style="{color:color}">孙子组件</h5>
      </div>
    </template>
    
    <script>
    import { inject } from '@vue/composition-api'
    export default {
      name: 'Grandson',
      setup (props, { root }) {
      //接收值
        const color = inject('color') 
        return {
          color
        }
      }
    }
    </script>

    路由跳转

    <template>
      <div class="home">
        <button @click="jump">跳转</button>
      </div>
    </template>
    <script>
    export default {
      name: 'Home',
      setup (props, { root }) {
       const jump = () => root.$router.push('/about')
        return {
          jump
        }
      }
    }
    </script>
    tips: root指代的就是vue对象,即this,且名字是不可更改的。

    props

    
    //父
    <template>
      <div class="about">
        <h1>This is an about page</h1>
        <div>{{num}}</div>
        <button @click="btn">增加</button>
        <HelloWorld msg="我是props传进去的值" />
      </div>
    </template>
    
    
    //子
    <template>
      <div class="hello">
        <h1>{{ msg }}</h1>
      </div>
    </template>
    
    <script>
    export default {
      name: 'HelloWorld',
      props: {
        msg: String
      },
      setup (props) {
        console.log(props)
      }
    }
    </script>
    

    未完待续~~~~

    查看原文

    可可西里 收藏了文章 · 8月13日

    一行代码使用CSS的黑暗模式

    这是一个绝对不费吹灰之力的方法,将已经开发好的网站转换为支持黑暗模式。

    话不多说,我们开始吧! 👾

    以这个新闻应用为例
    image

    现在添加魔术CSS

    html[theme='dark-mode'] {
      filter: invert(1) hue-rotate(180deg);
    }

    瞧!你完成了 ✌

    实现黑暗模式

    image

    说明

    现在,让我们试着理解下面发生了什么。

    CSS filter 属性将模糊或颜色转移等图形效果应用到元素上。滤镜通常用于调整图像、背景和边框的渲染。

    对于这种黑暗模式,我们将使用两个滤镜,即 inverthue-rotate

    invert滤镜可以帮助反转应用程序的颜色方案,因此,黑色变成了白色,白色变成了黑色,所有颜色也是如此。因此,黑变白,白变黑,所有颜色也是如此。

    hue-rotate滤镜可以帮助我们处理所有其他非黑白的颜色。将色调旋转180度,我们确保应用程序的颜色主题不会改变,而只是减弱它的颜色。

    image

    这个方法唯一的问题是,它也会反转你应用程序中的所有图像。因此,我们将对所有图像添加相同的规则来反转效果。

    html[theme='dark-mode'] img{
      filter: invert(1) hue-rotate(180deg);
    }

    而且我们还会给HTML元素添加一个过渡,确保过渡不会变得华而不实!

    html {
      transition: color 300ms, background-color 300ms;
    }

    结果

    image


    有同学不知道怎么添加,这就是CSS啊,为了验证我把CSS添加到SF网站:
    image
    你可以勾选样式和取消勾选样式查看变化


    来源:https://dev.to/akhilarjun/one...
    翻译:公众号《前端全栈开发者》

    查看原文

    认证与成就

    • 获得 15 次点赞
    • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

    擅长技能
    编辑

    开源项目 & 著作
    编辑

    (゚∀゚ )
    暂时没有

    注册于 2018-03-08
    个人主页被 587 人浏览