原子化 CSS 真能减少体积么?

前言

最近看到这样一篇文章:《要喷也得先做做功课吧?驳Tailwind不好论》

个人觉得说的还是有一定道理的,就是该作者的语气态度可能稍微冲了点:

SCR-20231010-koky.png

不过他说的确实有道理,如果这种原子化工具真的如评论区里那帮人说的那么不堪的话,又怎么会达到百万级的周下载量?而且还呈现出一路增长的态势,当然有人可能会说:哪一路增长了?你看那条曲线,在最右边都掉的不行了,肯定是马上要 G 了。

其实这些周下载量会在某几周呈现出极速下降的趋势一般都不是因为这玩意要没落了,哪有没落的这么快的,一两周用户量就少一半?出现这种情况的原因是因为欧美那边放假了,比方说复活节、圣诞节什么的,大家都出去玩了没人工作了所以才呈现出大幅下降的趋势。当然也不仅仅是欧美,某东方神秘大国放长假时也会呈现出类似效果。这个作者截图的时候可能刚好是欧美有什么假期,不信的话我们现在再来截一遍图看看:

卧槽!他截的是 tailwindcss 的下载量么?怎么我截的时候是五百多万的下载量?差的也太多了点,不过五百多万的下载量更证明了这个工具有多受欢迎,为了有个对比我们再来截一下 Vue 的数据:

可以看到 Vue 的下载量也不过三百来万,所以你是信评论区下面的那些小卡了米说这玩意有多垃圾多不好维护?还是信全球开发者闭着眼投票出来的用户数据?

体验

作为一名已经使用了多年的用户,我可以先跟大家分享一下自己的体验,以及用了这么久的优缺点,是否像评论区说的那么不好用,以及又是否像那些粉丝说的那么好。

首先说一下当初为什么要选择 Tailwind CSS,原因其实很简单:求新!

当时刚来这家公司不久,可能也就三四个月?但我作为一个新人却接到了一个重任:低代码平台!这个低代码是给公司运营同事用的,他们经常会出一些活动页面,这些页面技术含量并不高,交互也比较简单,主要是大量的图片以及一些表格表单等。但架不住页面多啊,总让我们做这些页面非常浪费人力,所以领导就想做一个低代码平台来分担我们的压力,他们运营想要什么页面自己搭建就好了,而我们只根据他们的需求来定制一个专属的低代码平台。

虽然不知道为什么这个重担会委派给我这个新人,但我还是非常开心的(感谢我阳总)因为这个项目只在公司内部使用,所以可以不用考虑那么些个兼容性什么的,我可以放心大胆的使用最新鲜的技术,终于不用再兼容那些个破浏览器啦!

而且这个项目还给了我很大的自由度,那时候 Vite 2.0 才刚发布不久,都没多少人用。甚至 Vue 3 都还算得上是比较新鲜的技术,至少在市面上用 Vue 3 开发的项目还不是很常见。但我当时想的是既然是一个船新项目那就不要留下什么技术债了,虽然当年我选的技术不是很成熟,但随着时间的推移慢慢的他们都会变成熟的嘛!如果现在就束手束脚的还用什么 vue-cli + vue2,随着时间的推移这个项目又跟公司现在维护的老项目没什么区别了。

当然有人可能会问:你怎么知道 Vite 这些新技术会越来越趋向成熟?万一发展一半像 Snowpack 一样噶了呢?我当然不知道某项技术未来的发展趋势,但当时的我就是有一种迷之自信,就是坚信尤雨溪出品的项目一定会有所发展,结果现在 Vite 发展的确实不错,尤大出品,必属精品。

一开始用的是 Vue3 + TSX,那时候是 3.0,还没有 setup 语法糖,每个变量还需要在底部 return 出去很麻烦,但用了 JSX 就不用挨个 return 了,而且 JSX 的灵活性也能让我很方便的使出一些骚操作。

JSXCSS 的结合不像 .vue 文件那么方便,而且 Vue 也不像 React 似的有那么多 CSS-in-JS 的库,不过好在 Vite 原生支持 CSS Module,于是采纳了 TSX + CSS Module 的方案,感觉也挺好用的。

然而某一天我突然刷到了一篇 Tailwind CSS 的文章,其实以前也刷到过,但对这玩意的印象也就一般。不过今时不同往日,一方面是新项目我想把以前没用过的东西都用一遍(前提是 Star 数很高以及 npm 下载量还可以)另一方面感觉这个技术有点击中我的痛点,一方面是 TSX + CSS Module 确实没有 .vue 文件来的方便,毕竟要写两个文件,那有人可能会说你可以写成这样:

<script lang="tsx">
  ...
</script>

<style scoped>
  ...
</style>

当年真的没有这种语法,这是 3.2 才加上去的,当时只有 3.0 这个版本可以选,而且我也不知道后来他能搞这么多语法糖进去,我还以为一直都会写成这样呢:

<template>
  ...
</template>

<script lang="ts">
export default {
  setup () {
    ...
    return {
      a,
      b,
      c,
      d,
      e,
      f,
      g,
      ...
    }
  }
}
</script>

Tailwind 的出现让我觉得可以不用再写成两个文件了,一个.tsx文件就搞定了。并且另一个痛点就是起名,比方说某个元素可能仅仅只需要往下稍微移一点,只需要设置一个 margin-top 就行,但你还需要给它起个类名,有时我就直接起名 mt,当然这个类名还遭到了批评,让我不要这么写,所以后来我就干脆直接在标签上写个 style

<div style="margin-top: 2px">
  ...
</div>

你说这不就和 Tailwind 有点类似了么?我看评论区好多喷 Tailwind 的都说当样式多了以后可能会写出这样的代码难以维护:

<div class="relative p-3 col-start-1 row-start-1 flex flex-col-reverse rounded-lg bg-gradient-to-t from-black/75 via-black/0 sm:bg-none sm:row-start-2 sm:p-0 lg:row-start-1">
  <h1 class="mt-1 text-lg font-semibold text-white sm:text-slate-900 md:text-2xl dark:sm:text-white">Beach House in Collingwood</h1>
  <p class="text-sm leading-4 font-medium text-white sm:text-slate-500 dark:sm:text-slate-400">Entire house</p>
</div>

说实话写成这样确实有点恶心,看起来也不是很容易,但别忘了 Tailwind 并不会影响你原有的写法,如果真的有比较复杂的样式你就写在 css 文件里或者写在 .vue<style> 标签里就行了啊:

<template>
  <div class="card">
    <h1 class="title">{{ xxx }}</h1>
    <p class="content">{{ xxx }}</p>
  </div>
</template>

<style scoped>
  .card { ... }
  .title { ... }
  .content { ... }
</style>

然后只在简单样式的情况下使用 Tailwind,按照这个原则把他俩结合到一起:

<template>
  <div class="card">
    <h1 class="title w-10 h-5">{{ xxx }}</h1>
    <div class="mt-2 px-4" />
    <p class="content">{{ xxx }}</p>
  </div>
</template>

<style scoped>
  .card { ... }
  .title { ... }
  .content { ... }
</style>

这样不就能达到很完美的互补了么?Tailwind 解决了简单样式不想写类名的困扰,也免除了上面写一个类名,然后滚动个几百行跑到下面定义这个类名,结果却仅仅只是写了个 margin-top!而且也比写 style 简洁,还能获得更小的打包尺寸。

更小的打包尺寸怎么讲?比方说我原来的方案,不想写类名有时就直接写 style

<div style="margin-top: 2px">
  ...
</div>

style 就只能作用在单个元素上,另一个元素也需要 margin-top: 2px,那是不是就相当于写了俩 margin-top?但它俩的样式一模一样,此时类的优势就体现出来了:

<!-- 某组件 -->
<div class="mt-2" />

<!-- 其他组件 -->
<p class="mt-2" />

有人可能会想:这刚能节省几个字符?有道是积少成多,用 Tailwind 的地方多了,重复的样式也不可避免的会变多,那到底能节省多大的尺寸呢?会不会有可能尺寸反而变得更大了呢?这正是本篇文章将要进行的实验,不过在实验之前我们还是先来说说这玩意的缺点。

缺陷

我知道为什么老有人担心用 Tailwind 会写出这样的代码:

<div class="relative p-3 col-start-1 row-start-1 flex flex-col-reverse rounded-lg bg-gradient-to-t from-black/75 via-black/0 sm:bg-none sm:row-start-2 sm:p-0 lg:row-start-1">
  <h1 class="mt-1 text-lg font-semibold text-white sm:text-slate-900 md:text-2xl dark:sm:text-white">Beach House in Collingwood</h1>
  <p class="text-sm leading-4 font-medium text-white sm:text-slate-500 dark:sm:text-slate-400">Entire house</p>
</div>

虽然我知道复杂样式正常写,简单样式再用 Tailwind 的方案:

<template>
  <div class="card">
    <h1 class="title">{{ xxx }}</h1>
    <div class="mt-2 px-4" />
    <p class="content w-10 h-5">{{ xxx }}</p>
  </div>
</template>

<style scoped>
  .card { ... }
  .title { ... }
  .content { ... }
</style>

但刚开始用的时候我的强迫症还是迫使了自己进行二选一,为了像 .vue 文件那样只写一个 .jsx 而不用再多写一个 xxx.module.css,毕竟这个后缀挺长的,每次写都在消耗我的耐心。于是我的强迫症还是让我写出了类似这样的代码(不过没这么夸张):

<div class="relative p-3 col-start-1 row-start-1 flex flex-col-reverse rounded-lg bg-gradient-to-t from-black/75 via-black/0 sm:bg-none sm:row-start-2 sm:p-0 lg:row-start-1">
  <h1 class="mt-1 text-lg font-semibold text-white sm:text-slate-900 md:text-2xl dark:sm:text-white">Beach House in Collingwood</h1>
  <p class="text-sm leading-4 font-medium text-white sm:text-slate-500 dark:sm:text-slate-400">Entire house</p>
</div>

当一行属性太长的时候我们通常都会给它换个行:

<h1
  class="
    mt-1
    text-lg
    font-semibold
    text-white
    sm:text-slate-900
    md:text-2xl
    dark:sm:text-white
   "
>
  Beach House in Collingwood
</h1>

看起来是不是好多了?但假如此时我们需要一个动态类名,在 .vue 文件里我们可以写成这样:

<h1
  :class="{ title: isLoading }"
  class="
    mt-1
    text-lg
    font-semibold
    text-white
    sm:text-slate-900
    md:text-2xl
    dark:sm:text-white
   "
>
  Beach House in Collingwood
</h1>

也就是说我们可以写把同一个属性的静态内容和动态内容分开写,这在 .vue 文件中是完全没问题的,但到了 .jsx 这边:

它就一直给你飘红,注意我这里用的是 .jsx,还不是 .tsx.tsx 会让你连编译都通不过。所以怎么办?那就只能这么办咯:

<a class={[{ title: isLoading }, 'mt-1 text-lg font-semibold text-white sm:text-slate-900 md:text-2xl dark:sm:text-white']} />

不换行还好(其实也挺乱的)但只要一换行:

肯定有人会说:那你把单引号改成反引号不就得了:

这样确实没问题了,但问题在于该项目用了保存文件时自动运行 prettiereslint,换行啥的根本不用自己操心,随便写,写完 command + s 一按,文件立马给你格式化:

这个功能特别方便,我很喜欢用,但用了反引号,换行就得靠你自己手动对齐了:

这真的让人挺不爽的,而且写成这样的话提示也都没了,静态 class 里会显示你写的颜色(需提前安装插件):

但写成动态 + 字符串形式的话这些效果通通会消失:

当然这也不是什么太大不了的事,看不见颜色也没那么大的影响。但在很多情况下是先写静态样式,然后才发现这块忘考虑 Loading 时的样式,再给加上动态代码,在 .vue 文件里写起来就很方便:

但在 .jsx 这边可就遭老罪咯:

除非你一开始就想好这个标签上是否有动态类,不然的话改起来你就说麻不麻烦吧?当然这也不应该算是 Tailwind 的缺点,应该算是 JSX 的缺点。但这个缺点被我随后刷到的 Windi CSS 解决了,当时刚用 Tailwind 不久,就刷到了 Windi CSS,那时候的前端真是日新月异,我才刚用熟,就又出替代品了:

当时 Tailwind 还在 2.x,没有 JIT 模式,速度方面被 Windi 吊打。而且 Windi 有个属性模式很实用,原本 Tailwind 都是写在 class 属性内的,但实际上有很多重复的前缀,比如:

这个 class 已经写的稍微有点长了,但换成 Windi 后:

<button
  text="sm white"
  font="mono light"
  p="y-2 x-4"
  border="2 rounded blue-200"
/>

一下子就短了好多,不再像之前那样一眼望去一大串类名了,而且也很好的解决了换行问题,毕竟你用的属性都被分了组,一组里的内容反而是有限的,比方说你想给个边距,那就是 m 属性,边距刚能有几个选项啊,这么多顶头了:

<button
  m="t-1 b-2 l-3 r-4 x-5 y-6"
/>

通常情况下根本就写不了这么多,并且它还没有占用 class 属性,假如突然想加一个类名,就不用像以前似的那么麻烦了:

并且 Windi 最牛逼的一个功能就是自定义语法,这正是我非常需要的一个特性,比如说我需要 margin-top: 16.5px,但 Tailwind 根本就没定义那么细,它的预置内容里没有 16.5px,所以我只能把它写进 xxx.module.css 里。但 Windi 说了:预置里没有 16.5px 是吧?没关系!你只需要写成这样:

<button m="t-16.5px" />

Tailwind CSS 现在倒是支持这种语法,不过需要加个中括号有些略显麻烦:

<button m="t-[16.5px]" />

但当年的 Tailwind 是没有这些个花里胡哨的,在我眼里 Windi 简直就是 Tailwind Pro Max Ultra!于是乎我二话不说就把 Tailwind 换成了 Windi,然后把之前 class 里的那一坨挨个重写成属性模式,文件有点多,花了不少时间。而且由于用的是 .tsx,这些没定义的属性会报错:

而且也容易让人分不清你写的到底是 Windi 的属性还是真的要传给组件的属性,Windi 早就替你想到了这一点,可以给属性加前缀,我加的前缀是:

import { defineConfig } from 'vite-plugin-windicss';

export default defineConfig({
  attributify: {
    prefix: 'css-:',
    separator: '__',
  },
});

为什么要用 css-: 这个前缀,一是带冒号会有一个不同的颜色,容易区分于普通属性:

二是我意外发现的神奇事件,只要属性是字母 + - + :TS 就不会再报错了:

等到周五写周报的时候我傻眼了,我这周大部分时间都用在重构上了,没怎么开发新功能,咋写啊?

本周工作:

  • Tailwind CSSWindi CSS

结果果然不出我所料,我被叫去小黑屋谈话了:

你应该以项目功能为主,不要老想着天天重构项目。前端是个变化非常迅速的行业,你要是追新的话你永远也追不完,它一天就能给你产出来一堆新技术来,难道每次出新技术你都要重构一遍么?

后来果不其然,Windi CSSG 了。那时候我听说 antfu 进入了 windi 团队,然后准备出一个 UnoCSS,记得原话说的好像是将会用 UnoCSS 来当作 Windi 的引擎,你可以理解为 UnoWindi 团队的一次激进实验。所以我一直都很期待,但后来慢慢的发现 Windi 的更新频率越来越低,不知怎么回事,结果一查说是 Windi 的作者在推特还是哪跟人发生了一次激烈争吵,心灰意冷了。当然这并不是官方说法,官方说法是这样的(机译):

我刚被约谈没多久,你就告诉我 WindiG 了?我现在换成 Uno 是不是过段日子又特么出新品了?这玩意出的怎么比手机都快?为了防止再次挨骂,这次我就没换,因为 Windi 也确实用的好好的没什么毛病。不过这个 Uno 不是我每天都在用的洗面奶么:

于是 Windi 就一直用到现在,不过写法跟 Uno 应该也没啥太大区别,就当是 Windi 的继任者好了。扯远了,咱们继续来说缺点,Tailwind 还有一个缺点,不过缺点这个词用的也不是特别准确,因为它既是缺点又是优点,具体取决于你所开发的项目类型。看到这有人可能会说:你跟我在这玩薛定谔的猫呐:

这个缺点就是单位,当你写了 mt-1 这个类时,你可能会以为是 margin-top: 1px,然而实际上却是 margin-top: 0.25rem,那 0.25rem 又是多少呢?按 1rem = 16px 来换算,它应该是 16px * 0.25,这个数乍一看不是那么好算是吧?我们可以把 0.25 看作四分之一,16px * 1/4 就相当于 16px 除以 4

难道每次写 Tailwind 的时候都要心算一遍么?太累了吧?这玩意用上个两三年之后是不是就成心算高手了?但你要是换个角度来看的话这个设计又成为了一个优点,就是当你写一个不是那么精细的响应式 H5 的项目,这玩意还蛮好用的,因为 1rem 具体等于多少 px 是可以根据屏幕尺寸的不同来给根元素一个不同的 font-size 从而进行动态变化的。不过对于我们来讲都是有设计图的,少 1px 测试都会提 bug,所以我更希望的是 mt-1 就代表 margin-top: 1px,既好记又精细。

于是 UnoCSS 说:这需求我熟啊!我可以很方便的进行预设:

并且也提供了各种常用预设:

总之不仅性能好,而且还非常灵活。

优势

不能光说缺陷不说优势是不?那我就谈谈这两年使用上让我觉得非常舒心的地方,就是有时候改版需要改点样式,但改版也不至于说跟原来的样式一点都不一样,大部分都是一样的,只有少部分变化。假如按钮往下移一点、banner 变宽点这种小改,然后进入到项目中,找到按钮,发现这款按钮有个类名叫:

<button class="casino-header-user-avatar-btn">
  ...
</button>

大部分人都会直接搜这个类名,搜到以后直接加一个 margin-top 对吧?但一搜却发现根本搜不到这个类名,因为这种类名一般都是通过 CSS 预处理器定义而来的,比方说这个项目叫 casino,然后写 header 的样式:

.casino {
  ... /* 此处省略 N 行干扰代码 */
  &-header {
    ... /* 此处省略 N 行干扰代码 */
    &-user {
      ... /* 此处省略 N 行干扰代码 */
      &-avatar {
        ... /* 此处省略 N 行干扰代码 */
        &-btn {
          ...
        }
        ... /* 此处省略 N 行干扰代码 */
      }
      ... /* 此处省略 N 行干扰代码 */
    }
    ... /* 此处省略 N 行干扰代码 */
  }
  ... /* 此处省略 N 行干扰代码 */
}

按理说一个组件最好不要超过 300 行,但有的复杂组件经常超过 500 行,甚至上千行的我都遇到过(应该也写过),当搜不到一个类名时那就要疯狂滚动鼠标滚轮,滚到冒烟自己去下面一行行找,找到哪个才是控制 button 的类,再去添加样式。这还不是最痛苦的,最痛苦的是这个按钮不止一个类名,或者有人用了这种写法:

.xx {
  ... /* 此处省略 N 行干扰代码 */
  .xxx {
    ... /* 此处省略 N 行干扰代码 */
    button {
      margin-top: 3px;
    }
  }
  ... /* 此处省略 N 行干扰代码 */
}

当我们好不容易找到了对应的类名并且加入了 margin-top 却发现不生效时,我们就只好打开控制台找到元素看看是什么样式把我们的 margin 给覆盖了,找了以后又去一顿搜,总之挺麻烦的。后来干脆就不这么干了,下次再有这种就直接在标签里写一个 style="margin-top: 6px",写着写着突然就意识到:这不跟写 Tailwind 差不多么?

当然肯定会有人说哪差不多了,style 有更高的权重,能覆盖你在各种类下产生相互影响的样式,你要是写 Tailwind 能覆盖么?但假如一开始这个项目就是用 Tailwind 写的,我们可以在类名里看到:

<button class="w-10 h-4 bg-yellow mt-3">
  ...
</button>

我们是不是就可以省去滚动几百行代码去找类名的这么一个步骤,直接在标签上改成 mt-6 就行了?如果用的是 UnoCSS 属性模式的话那就更方便了,直接找到 m 这个属性改里面的数字就行了:

<button
  w="10"
  h="4"
  bg="yellow"
  m="t-6"
>
  ...
</button>

看到这有人可能会说,你看这个属性模式也没节省什么代码,class 里面有四个类:class="w-10 h-4 bg-yellow mt-3",换成属性模式后不还是四个属性么?

别忘了 UnoCSS 是可以非常方便的自定义插件的,比方说最常用的宽高,它俩被分为了两个属性对吧?我们可以自定义一个 size 属性表示宽高:

<button
  size="w-10 h-4"
  bg="yellow"
  m="t-6"
>
  ...
</button>

然后还可以把与颜色相关的分为一组,比方说前景色、背景色等:

<button
  size="w-10 h-4 border-box"
  color="yellow bg-blue"
  m="t-6"
>
  ...
</button>

再把盒模型分为一组,包括 paddingmarginborderbox-sizing 等:

<button
  size="w-10 h-4"
  color="yellow bg-blue"
  box="mt-6 pb-4 border-box"
>
  ...
</button>

这样可不仅仅只是代码行数减少了,更是可以通过分组一眼望去就能大概看出是个什么样式。

还有就是简单样式时也很方便,以往常见的写法都是上面定义一个类名,然后滚轮一顿滚,再在下面写样式,然后滚轮再滚回去,滚回去的过程中就找不到是哪行了,反正当代码多起来的时候我就是这种感受。而用了 Tailwind 之后想写什么样式我就直接写在标签里了,根本不用滚!

尤其是在 jsx 文件中,jsx 本来无法写样式,当然我指的是 .vue 文件中 <style> 里的那种样式,不是这种:

const btnStyle = { color: 'pirple' }

<button style={btnStyle}>...</button>

当然也不是 CSS-in-JS 那种:

const Flex = styled.div`
  display: flex;
  align-items: center;
  justify-content: center;
`

<Flex>...</Flex>

但用了原子化 CSS 就让 jsx 也能很方便的拥有样式了,虽说复杂样式比较捉襟见肘,还得借助其它方案,但简单样式时真的还蛮好用的。

实验

那么接下来我们就来做一个实验,当然这个实验并不是为了对比 TailwindUno 谁更快,肯定是 Uno 快,官方宣传是 5 倍快。

这个实验主要是想搞清楚这些个原子化 CSS 究竟会不会缩小打包后的体积,CSS 的体积必然会有所减少,但与此同时 HTML 体积又会有所增大,此消彼长的情况下究竟谁能够更胜一筹?

实验如下:

  • Vue 3 只用 Tailwind 写一个 TodoList
  • Vue 3 只用 UnoCSS 的属性模式写一个 TodoList
  • Vue 3 复杂样式用普通写法、简单样式用 Tailwind 写一个 TodoList
  • Vue 3 复杂样式用普通写法、简单样式用 UnoCSS 的属性模式写一个 TodoList
  • Vue 3 不用任何原子化 CSS 写一个 TodoList
  • Vue 3 不用任何原子化 CSS(但用内置的 scoped)写一个 TodoList
  • Vue 3 只用 CSS Module 写一个 TodoList

然后分别将其打包,对比一下各自的体积大小:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

一个 TodoList 写这么花?别提了,一开始做了一版中规中矩的比较简陋简洁的 TodoList,哪成想打包出来的产物差异很小,几乎可以忽略不计,没有任何参考价值。仔细想想也是,毕竟一个 TodoList 刚能有几行代码?原子化 CSS 肯定是样式文件越多,重复性才越高,才越有实验的价值。于是我才把样式尽可能往复杂了写,写的时候还发现 Vue 唯一公认的拖拽库断代了,具体可以看一下这篇:

《别再卷组件库了,Vue 拖拽库都断代了!》

但没想到即使我把 TodoList 写的都这么花了,打包出来的产物依然还是没有什么太大区别。后来分析了一下,一方面是无论 TodoList 写的有多花,它的体量还是太小了。另一方面则是这款 TodoList 的复杂样式和我们日常开发项目的那种复杂样式并不属于同一种复杂,咱们日常开发时很少会出现这么炫酷的动画和交互,这些动画基本上也不太容易重复。但那些普通元素什么的很多,每个元素单拎出来都不复杂,但组合到一起就麻烦了,又要考虑定位、又要考虑相互之间的作用什么的,同时也会出现更多的雷同 CSS 代码。所以最好的方式是将已有的项目分别用 TailwindUno、单文件组件以及 CSS Module 等方式重构几遍,然后再对比打包后的差异。

思来想去,一个至少达到中型规模的项目才能够达到测试的目的(真没法用巨石应用来测试,巨石应用需要好多人合作开发数年才行,有多庞大就不说了,肯定也是屎山项目,太费劲)所以我打算把自己熟悉的项目重新改造,最终此消彼长依然没有什么太大差距:

所以说如果你用原子化 CSS 工具是为了减少打包体积的话,那你可以洗洗睡了,但如果你是为了写起来更爽的话,那确实还值得一试。

往期精彩文章

  • 《产品经理:能不能把 Vue 的中文输入法 bug 解决了?》
  • 《产品经理:能不能让这串数字滚动起来?》
  • 《尤雨溪海外直播:亲手带你写个简易版的Vue!》
  • 《波浪动画很常见,但这个波浪组件绝对不常见》
  • 《UI:你们有没有什么花哨点的组件库给我参考一下?》
  • 《Vue 超好玩的新特性:在 CSS 中引入 JS 变量》
  • 《Vue 超好玩的新特性:DOM 传送门》
  • 《不依赖任何库打造属于自己的可视化数据地图》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/119273.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【jvm】虚拟机栈

目录 一、背景二、栈与堆三、声明周期四、作用五、特点&#xff08;优点&#xff09;六、可能出现的异常七、设置栈内存大小八、栈的存储单位九、栈运行原理十、栈帧的内部结构10.1 说明10.2 局部变量表10.3 操作数栈10.4 动态链接10.5 方法返回地址10.6 一些附加信息 十一、代…

明御安全网关任意文件上传漏洞复现

简介 安恒信息明御安全网关(NGFW) 秉持安全可视、简单有效的理念&#xff0c;以资产为视角的全流程防御的下一代安全防护体系&#xff0c;并融合传统防火墙、入侵防御系统、防病毒网关、上网行为管控、VPN网关、威胁情报等安全模块于一体的智慧化安全网关。 较低版本的系统存…

淘宝API技术文档解析,从入门到实战

探索淘宝数据的奥秘&#xff0c;淘宝是目前国内最大的B2C电商平台之一&#xff0c;每天都会产生海量的数据。借助淘宝API技术文档&#xff0c;我们可以轻松地获取到这些数据&#xff0c;从而为电商运营和数据分析提供有力支持。 1.什么是淘宝API&#xff1f; 淘宝API&#xf…

用免费GPU线上优化猫狗识别实践

该部分以“猫狗识别模型”为例&#xff0c;学习如何直接通过平台提供的开发环境调用GPU资源 一.学习准备 获取官方代码文件&#xff1a;https://platform.virtaicloud.com/gemini_web/workspace/space/n9tte8i2aspd/project/list 二.创建项目 1&#xff09;进入趋动云用户工…

轻量封装WebGPU渲染系统示例<12>- 基础3D对象实体(源码)

当前示例源码github地址: https://github.com/vilyLei/voxwebgpu/blob/main/src/voxgpu/sample/PrimitiveEntityTest.ts 此示例渲染系统实现的特性: 1. 用户态与系统态隔离。 细节请见&#xff1a;引擎系统设计思路 - 用户态与系统态隔离-CSDN博客 2. 高频调用与低频调用隔…

不会写文档的程序员不是好的程序员

在当今数字化的世界中&#xff0c;软件开发行业正经历着前所未有的繁荣。从移动应用到大型企业系统&#xff0c;软件构建了现代社会的基础。在IT行业中&#xff0c;文档是一种非常重要的沟通工具。它可以帮助程序员和客户及团队成员之间进行有效的沟通和协作&#xff0c;提高工…

基于springboot实现原创歌曲分享平台系统项目【项目源码+论文说明】

基于springboot实现原创歌曲分享平台系统演示 摘要 随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理平台应运而生…

工业自动化工厂PLC远程控制网关物联网应用

远程控制网关在工厂自动化领域中起到了至关重要的作用&#xff0c;特别是在工厂PLC数据通讯方面。它充当着数据传输的桥梁&#xff0c;连接了工厂中的各类设备和系统&#xff0c;实现了远程监控和控制的功能。本文将详细介绍远程控制网关在工厂PLC数据通讯中的应用。 远程控制网…

项目上线前发现严重Bug怎么办?

今天分享一个面试问题&#xff0c;现在有一个面试场景&#xff1a; 项目计划明天发布&#xff0c;但是在今天你作为测试人员发现了一个严重的bug&#xff0c;市场相关人员又在催发布的事情&#xff0c;这个时候你应该怎么办&#xff1f; 这是测试工程师不管是在面试&#xff0…

大数据毕业设计选题推荐-智慧小区大数据平台-Hadoop-Spark-Hive

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

gitlab数据备份和恢复

gitlab数据备份 sudo gitlab-rake gitlab:backup:create备份文件默认存放在/var/opt/gitlab/backups路径下&#xff0c; 生成1697101003_2023_10_12_12.0.3-ee_gitlab_backup.tar 文件 gitlab数据恢复 sudo gitlab-rake gitlab:backup:restore BACKUP1697101003_2023_10_12_…

苹果加大对印度的扶持,提高在其生产iphone的比重

KlipC报道&#xff1a;跟踪苹果产业链&#xff0c;有分析师预计2023年全球约12%-14%的iphone在印度生产&#xff0c;预计2024年&#xff0c;印度将生产20%-25%的iphone。 KlipC的合伙人Andi D表示&#xff1a;“近年来随着苹果对中国的以来&#xff0c;印度已经成为高科技制造和…

EasyExcel实现动态表头功能

EasyExcel实现动态表头功能 开发过程中&#xff0c;大部分都会使用到导出报表功能&#xff0c;目前阶段会用得有 poi导出&#xff08;暂无&#xff09;&#xff0c; easyexcel导出&#xff08;官方文档&#xff0c;https://easyexcel.opensource.alibaba.com/docs/current/&am…

竞赛 深度学习驾驶行为状态检测系统(疲劳 抽烟 喝水 玩手机) - opencv python

文章目录 1 前言1 课题背景2 相关技术2.1 Dlib人脸识别库2.2 疲劳检测算法2.3 YOLOV5算法 3 效果展示3.1 眨眼3.2 打哈欠3.3 使用手机检测3.4 抽烟检测3.5 喝水检测 4 最后 1 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 基于深度学习的驾…

V90 EPOS模型下位置控制(完整SCL源代码)

V90EPOS模式下点动控制详细应用介绍和控制源代码,请查看下面文章链接: V90伺服EPOS模式点动控制(详细介绍+完整SCL代码)-CSDN博客文章浏览阅读29次。V90伺服驱动器采用西门子标准报文111加FB284(SINA_POS)详细的报文组态和功能块请参考下面文章链接:博途1200/1500PLC V90 P…

基于 golang 从零到一实现时间轮算法 (三)

引言 本文参考小徐先生的相关博客整理&#xff0c;项目地址为&#xff1a; https://github.com/xiaoxuxiansheng/timewheel/blob/main/redis_time_wheel.go。主要是完善流程以及记录个人学习笔记。 分布式版实现 本章我们讨论一下&#xff0c;如何基于 redis 实现分布式版本的…

nginx知识点-1

#因为是最小化安装&#xff0c;先安装vim编辑器&#xff0c;net-tools查看端口&#xff0c;psmisc可以使用killall命令bash-completion tab补全命令(需要重启生效)[rootlocalhost ~]# yum -y install net-tools psmisc vim bash-completion [rootlocalhost ~]# tar zxvf nginx-…

SourceTree 4.1.5(Git客户端)

SourceTree是一款Git和Hg客户端管理工具软件&#xff0c;同时支持Mercurial和Subversion版本控制系统。它具有以下功能&#xff1a; 版本控制&#xff1a;SourceTree可以创建、克隆、提交、push、pull和合并等操作&#xff0c;支持分布式版本控制系统&#xff0c;使得开发者可…

图片文件过大怎么压缩?五种压缩方法大全

图片文件过大怎么压缩&#xff1f;不知道大家有没有遇到过这样的尴尬情况&#xff0c;当我们将一些图片上传到某个网站的时候&#xff0c;被提示图片大小操作了网站的限制而被禁止上传&#xff0c;我相信很多人都遇到过吧&#xff0c;其实这是网站的一张防御措施&#xff0c;防…

计算机毕业设计 基于SpringBoot房屋租赁管理系统的设计与实现 Javaweb项目 Java实战项目 前后端分离 文档报告 代码讲解 安装调试

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…
最新文章