分类: 前端

以html+css为主的网页技术

  • 移动网页高度自适应最佳实践

    移动网页高度自适应最佳实践

    移动 Web 开发就要在“螺蛳壳里做道场”。移动设备限于屏幕尺寸,不得已左支右绌,既要多呈现内容,又要保证功能不要缺失。普通内容类网页还好,自然往下滚就行了;开发 Web App 的时候,当我们因为某种原因,需要限制滚动区域的时候,就很难处理。

    这篇文章会分享我的一些经验,希望能节省大家摸索的时间。

    无用单位:vh, svh, dvh, lvh

    很早以前,我们就有了 vwvh 单位,分别指代视窗宽高的 1%。需要注意的是,移动浏览器的视口包含被各种组件占据的部分,所以 vw 就比较好用,因为没有干扰;但是高度上,被通知栏、地址栏等占用的“临时”空间就会成为我们的麻烦。

    为了妥善利用屏幕空间,在我们上下滚屏的时候,大多数手机浏览器都会把地址栏、工具栏、或者通知栏隐藏起来,这就导致浏览器的可视面积其实会不断变化。原本就没用的 vh 便更没用了……于是后面新增了 svhdvhlvh 三种 长度单位,但其实帮助不大,因为当我们需要限制容器高度的时候,通常来说就不能让页面自由滚动。

    因为这几个长度单位过于没用,所以我就不详细介绍了。感兴趣的同学可以看下 TailwindCSS 里的演示:https://tailwindcss.com/docs/height#viewport-height

    虚拟键盘则让这个问题雪上加霜,因为虚拟键盘的显示和隐藏都不会影响这几个长度单位,所以当我们需要手动控制容器高度、位置的时候,就会很难做。

    最佳实践:常规页面,交给浏览器

    首先我们要信任浏览器,能够留给浏览器处理的,尽量交给浏览器原生处理。

    比如,常规页面,长一点,留给浏览器自然滚动。文本框输入的时候,浏览器会自动聚焦和滚动,通常情况下没什么问题,基本体验有保证。

    最佳实践:输入框文字不小于 16px

    如果文本框 font-size 小于 16px,iOS Safari 下,当文本框获得焦点,Safari 会自动放大整个页面;而失去焦点的时候,页面并不会自动缩小到 100%,所以就很蛋痛。

    解决方案有几个:

    1. 取消缩放。会使得可用性评价恶化,不推荐。
    2. blur 时自动恢复 100%。增加特性就是增加 bug 的可能,我觉得能不用就不用。
    3. 保持字体大小。应该大部分时候都更简单有效。

    最佳实践:使用 dvh 并解决兼容问题

    虽然但是,当我们需要固定高度的时候,表示视窗净高度的 dvh 仍然是我们最佳选择。

    不过,在我写文章的现在,dvh 的兼容性不是很好,所以必须做好兼容性配置。我建议用 JS 结合 CSS 变量来做。在 <head> 里插入这段 JS:

    // 首先,判断是否支持 dvh 单位
    if (!CSS.supports('height', '100dvh')) {
      // 如果不支持,就定义 --app-height 为视口高度,即 window.innerHeight
      document.body.style.setProperty('--app-height', window.innerHeight + 'px');
      // 当屏幕缩放时,改变内容高度。因为 resize 事件触发很频繁,所以使用节流减少性能损耗
      let timeout;
      function onResize() {
        clearTimeout(timeout);
        timeout = setTimeout(() => {
          clearTimeout(timeout);
          document.body.style.setProperty('--app-height', window.innerHeight + 'px');
        }, 500);
      }
      window.addEventListener('resize', onResize);
    }
    

    然后定义 CSS 样式:

    :root {
      --app-height: 100dvh;
    }
    
    .h-dvh-app {
      height: var(--app-height);
    }
    

    如果使用 TailwindCSS,那么在配置文件里增加配置即可:

    export default {
      theme: {
        extend: {
          spacing: {
            'dvh-app': 'var(--app-height)',
          },
        },
      },
    }        
    

    最佳实践:使用 CSS 变量解决虚拟键盘

    只是限制高度为 100dvh,当虚拟键盘弹出之后,因为视口缩小,很可能会出现问题。此时 window.resize 事件也不会触发,所以我们应该侦听文本框的 focus 事件,动态改变容器高度;并在文本框 blur 之后,恢复高度。

    此时,我们可以借助 CSS 变量的“默认值”功能,即 var(--custom-value, --default-value) 来处理。当我们需要暂时的高度以应对虚拟键盘时,设置 --custom-value;之后,移除 —custom-value,恢复到预定义的 --app-height

    首先,修改 css,定义 --input-height: initial,这个值会被认为是空值而忽略。

    :root {
      --input-height: initial; 
      --app-height: 100dvh;
    }
    
    .h-dvh-app {
      height: var(--input-height, var(--app-height));
    }
    

    然后侦听输入框的 focusblur 事件:

    async function onTextareaFocus(): Promise<void> {
      // 桌面端忽略这个需求
      if (window.innerWidth > 640) return;
    
      // 给虚拟键盘弹出一些时间
      await sleep(300);
      const { innerHeight } = window;
      document.body.style.setProperty('--input-height', `${innerHeight}px`);
      // 需要的话,可以在这里插入一个滚动
    }
    async function onTextareaBlur(event: FocusEvent): Promise<void> {
      // 同样,也给虚拟键盘收起留一些时间
      await sleep(250);
      document.body.style.removeProperty('--input-height');
    }
    

    总结

    至此,遵守以上最佳实践之后,基本上我们可以妥善处理移动网页里的浏览器高度。当然,并不完美,比如,iOS Safari 在输入 position: sticky 里的文本框时,会凭空多出一大块空白,很烦,但是没办法解决。可以绕开,但是我觉得绕开的方案更难用。

    希望这篇文章对大家有用。如果你对移动网页开发有什么问题,欢迎留言讨论。

  • CSS 小教程:在网格型选择工具上添加渐变背景

    CSS 小教程:在网格型选择工具上添加渐变背景

    新年开始了,要努力,本博客开始 2024 年招商,欢迎各位想推广产品的老板投广告,目前定价 4800/年,亦可增加评测文章、教学文章配合,详情可与我联系。具体详情更新在 本站广告 2024 年招商


    好几个月之前,我遇到这样一个需求:

    这个东西叫作 Emotional Scale,是一大堆描述情绪的形容词,存在从积极到消极的渐进关系,老板希望我们用颜色给予它们比较明显的区分。于是我就想到这样一种表现方式:

    1. 用网格化来布局每个形容词
    2. 给它们添加从上至下的渐变背景
    3. 再让 ChatGPT 给每个形容词选一个 emoji

    最后一项不用多说,直接丢给 ChatGPT 就行。这里重点说下前面两个。

    网格化布局用 display:grid + grid-template-columns: repeat(N, minmax(0, 1fr)) 即可,其中 N 可以根据宽度调整成需要的列数。

    接下来处理渐变。这里的难点在于,每个小格子是独立的,要维持统一的渐变比较困难。我想到两个方式:

    1. 画一个大的渐变背景,然后用 clip-path
    2. 通过计算的方式给每个小格子加渐变背景

    后面我决定选择第二种方式。我总共有 38 个格子,一行 4 个的话,就是 10 行;有 6 个颜色,刚好每两行是一个过渡。于是一行就应该是 0 -> 50%,如何渐变一半呢?我尝试了一下,发现可以用 -100% 和 200% 来做。

    于是我就配合 Vue 开发了这个组件:

    <script lang="ts" setup>
    function getBgColor(index: number): string {
    const colors = ['#1B4397', '#648EF7', '#5DC264', '#FFAE43', '#F15146', '#b91c1c']; // 总共 6 个颜色
    const start = index / 8 >> 0; // 8个=2行,就是两行完成两个颜色的过渡
    const end = start + 1;
    const line2 = index % 8 >= 4;

      // 借助 CSS 变量完成过渡效果
    return `--tw-gradient-stops: ${colors[start]} ${line2 ? '-100%' : ''}, ${colors[end]} ${line2 ? '' : '200%'}`;
    }
    </script>

    <template lang="pug">
      .grid.grid-cols-4.gap-2
    label.flex.flex-col.justify-center.items-center.bg-base-100.rounded-3xl.w-full.aspect-square.cursor-pointer.bg-gradient-to-b(
    v-for="([label, face], index) in todayDoingsScale"
    class="hover:bg-base-200"
    :style="getBgColor(index)"
    :key="index"
    @click="doSelect"
    )
    input.hidden(
    type="radio"
    name="feel"
    :value="label"
    v-model="score"
    )
    </template>

    最终完成的效果就如同上面截图所示。

    CSS 作为前端开发的必备技能,搞起来别有一番乐趣。新特性不断被添加进来,就有越来越多的功能可以实现,有越来越多的功能可以更容易的实现。相信我们下一步的开发环境和产品体验会更好。

    如果各位读者对上面的 CSS 实现有什么问题,或者有其它关于 CSS 的问题,欢迎留言提问。

  • 值得关注的 CSS 新特性

    值得关注的 CSS 新特性

    CSS 作为前端的重要组成部分,虽然受瞩目的程度逊于 JS,但是也在不停地进步。CSS 可以让我们的开发环境更好,用户体验更佳,所以大家也需要保持关注。这里记录一些值得关注的新特性(评判标准由我主观决定),有些已经实装,可以取代旧样式,提供更好的效果;有些还没有普及,但是可以逐步应用到我们的产品当中,渐进式增强。

    overflow-wrap

    这个属性我还是通过 TailwindCSS 学到的。它可以用来处理文本换行,拥有 3 个可选值:

    1. overflow-wrap: normal 默认值,按照标准模式换行
    2. overflow-wrap: anywhere 可以在任意处换行
    3. overflow-wrap: break-word 尽量利用宽度,只在超宽时打破单词换行

    我们知道,中文一个字就是一个字,随时可以换行;而英文则把一个单词算作一个字,单词内部不能随意断开。但是有时候,容器宽度不够,文字就会撑破容器。以前我们只有 word-break: break-all,但是 word-break: break-all 会在单词的任意位置断开,甚至在不必要的时候断开;而 overflow-wrap: break-word 会尝试把单词放到独立一行,如果还是不行,那就再断开。所以它的效果会更好。推荐大家使用。

    Scoped CSS

    CSS 的优先级非常重要,平时常常用到,面试的时候也会常常问到。CSS 优先级分成若干层级:

    1. #id
    2. .class
    3. element
    4. [attr="value"]
    5. :pseudo-class

    这样的优先级规划在大部分场景下,都能正常工作,但是当我们需要使用公共仓库,或者开发公共仓库的时候,就会遇到困难。

    比如 Awesome Comment 项目,它是一个嵌入页面的评论框,用户可以把它嵌入到自己的网站当中,给自己的网站添加评论功能。我们使用 TailwindCSS + DaisyUI 作为样式库,直接使用一切都好,但是当我们要把它嵌入别的网页时,就可能跟目标页已有的样式产生各种冲突。于是,在 Scoped CSS 尚未普及的今天(主要是 Safari 不支持),我们就必须给所有样式加上 ac- 前缀,非常影响开发效率。还有一位同学,他想给现在使用 bootstrap 的项目里引入 TailwindCSS,也面临严重问题。

    如果有了 Scoped CSS,我们只需要这样:

    @scope (.awesome-comment) {
      .a { }
    }

    就可以非常轻松地让我们的样式只在 .awesome-coment 节点内生效,且盖过其它样式。可以大大提升我们集成别人的样式,或别人使用我们样式时的效率。相信以后这个功能会大大改善第三方框架的开发。

    更多更详细的用法,建议大家深入阅读:@scope – CSS: Cascading Style Sheets | MDN (mozilla.org)

    <textarea> 根据内容自动缩放

    这项目功能目前还在讨论之中:[css-ui] ? Allow <textarea> to be sized by contents. · Issue #7542 · w3c/csswg-drafts (github.com)。大概意思是,<textarea> 用途非常广泛,让它能够根据内容长度自动调整大小会非常有价值。不过,具体选用什么方案,目前还没有定论。

    我搜了一下,目前无论 form-sizing 还是 field-sizing 都还没有进入规范,也没有间接实现方案(比如 PostCSS),所以大家先保持关注吧。

    View Transition 视图切换动画

    View Transition 让我们的产品可以在页面切换时,复用一些页面元素,展现出漂亮的动画,让用户更加理解页面逻辑。如下面这则视频所示,当用户点击列表中的链接时,大标题、作者等信息会以动画形式,移动到页头位置;当用户回到列表页的时候,它们又会移回列表里的相应位置。

    使用 View Transition 的方法比较复杂,我暂时还没有 Demo,所以就不详细说明了,大家感兴趣的话,可以看下面两个链接:

    1. Getting started with View Transitions on multi-page apps | daverupert.com
    2. https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API

    light-dark() 函数

    light-dark() 函数可以接受两个参数,分别作为 light 模式和 dark 模式下生效的属性。如以下代码所示:

    body {
      background-color: light-dark(white, black);
    }

    不过我觉得这个函数的作用不会很大,我还是喜欢集中定义和管理这些变量。

    color-mix() 函数

    相对来说,我觉得 color-mix() 函数的作用就大多了,它可以基于一个颜色,生成新的值。比如,我们先约定一批颜色,分别作为 primarysecondarysuccesserror 等,然后使用 color-mix 函数,生成一些衍生颜色,以便适用在 disabledactivehighlight 等场景下。

    使用方式也比较简单,它接受 3 个参数:混合模式,颜色1,颜色2。比如下面的代码,就是两种颜色各取一半,以 srgb 模式混合,得到新颜色。其中百分比也可以调整,比较好理解。

    /* 减淡,即混合白色 */
    color: color-mix(in srgb, var(--primary) 50%, white);
    /* 加深,即混合黑色 */
    background-color: color-mix(in srgb, var(--primary) 50%, black);

    有兴趣的同学可以深入了解:color-mix() – CSS: Cascading Style Sheets | MDN (mozilla.org)

    原生嵌套 CSS

    嵌套 CSS 相信大家在使用预处理工具的时候都会用到:

    .a {
      .b {
        .c { }
      }
    }

    因为确实有用,所以如今原生 CSS 也开始支持嵌套,可惜有些浏览器还没有支持。我建议大家先配合 TailwindCSS 使用,等到将来都支持了,再去掉 TailwindCSS 即可。

    export default {
      plugins: {
        'tailwindcss/nesting': {},
        tailwindcss: {}, // 不需要 TW 的话可以不要这行
        autoprefixer: {},
      },
    }

    详细规范请继续阅读:https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting/Using_CSS_nesting

    CSS 容器查询

    我猜大部分同学都用过 @media query,以适配不同设备和分辨率。但我们现在已经是组件化开发的时代,很多时候,我们需要让自己的组件能够适配不同的使用场景,于是仅仅根据分辨率调整布局就不够了。万一被用在桌面显示器上,但是本身只占据一小块空间,那就无法保证用户体验。

    这个时候我们可以使用 CSS 容器查询(Container query),它的用法很简单,首先,假设我们的组件渲染出来的 HTML 是这样的:

    <div class="post">
      <div class="card">
        <h2>Card title</h2>
        <p>Card content</p>
      </div>
    </div>

    接着,我们要把目标容器标记为“容器控制上下文”(containment context)。未特意标记的容器不会产生容器查询,我猜这样可以减轻浏览器布局的计算负担。最后,使用 @container 查询,让容器在某个尺寸时,内部某些样式生效,即可:

    .post {
      container-type: size;
    }
    
    /* 默认样式 */
    .card h2 {
      font-size: 1em;
    }
    
    /* 仅在容器 >= 640px 时才生效的样式
    @container (min-width: 640px) {
      .card h2 {
        font-size: 2rem;
      }
    }

    容器查询还有一些进阶用法,大家可以深入阅读:CSS container queries – CSS: Cascading Style Sheets | MDN (mozilla.org)

    总结

    好的,先写这么多。感谢所有标准贡献者、开源软件开发者、社区参与者,我们 Web 开发者能收获现在的成就,跟所有贡献者的辛勤工作分不开。

    希望这些新特性能尽早进入实装,成为我们开发时的利器。

    如果你有什么想补充的,或者有问题想讨论一下,欢迎留下评论。

  • 2023 告别 CSS 预处理工具,彻底拥抱 TailwindCSS

    2023 告别 CSS 预处理工具,彻底拥抱 TailwindCSS

    CSS 是声明式语言,很简单,很好学,但是写起来很累,所有东西都要写出来才能生效。复用方面更是无从下手,虽然大家都在不断总结,但始终没能找到足够好用的方案,可以有效改善 CSS 开发。

    于是我们只好把视线转出 CSS 之外,转投 CSS 预处理工具,Less、SASS(SCSS)、Stylus,引入种种 CSS 不具备的功能,帮助我们改进开发体验。比如嵌套、函数、循环、条件,等等。然而如果你细心观察,实际上,这几个工具最近 5、6 年都没怎么更新(我说的是功能性),因为该有的都有了,甚至很稳定;其它来自于 CSS 的改进,几乎跟它们没什么关系,也不用更新。

    最近几年,随着 CSS 发展,一些新特性逐步引入,我觉得这些工具越来越难用,它们能带来的好处已经无法掩盖它们所造成的问题。是时候告别 CSS 预处理工具了,就像我们当年告别 jQuery 一样。

    为什么说预处理工具落后?

    我把理由分成三大类:

    预处理工具的问题

    • 对 CSS 函数兼容性不好,尤其是 rgba()hsl() 这些常用的颜色函数
    • 数值类型转换,有不符合预期的行为,比如 Stylus 实现 content:5

    CSS 的改进

    • CSS 拥有越来越多的函数,可以直接进行计算,比如前面提到的颜色;还有 calc() 来完成基础数学计算
    • CSS 变量非常好用,可以大大改进编程体验,配合各种 JS 框架,我们可以更容易的把数学逻辑和显示效果绑定在一起
    • CSS Houdini 可以实现新功能,即使不深入使用(JS 部分),也有好用的自定义属性
    • CSS 也开始从预处理工具吸收营养,比如近期的嵌套功能已经开始被整合,未来我们可以直接使用

    预处理工具无法跟进的问题

    • 很多缩写、复合属性无法处理,比如 background-imagebox-shadow 等,都支持多属性共同生效,预处理工具擅长的循环、条件、函数无法提供帮助。
    • 预处理,顾名思义,发生在生产之前。实际上,网页在实际浏览时,会有很多因素影响到渲染结果,比如分辨率、dark mode 等。预处理工具对这些需求也没有改进。

    替代方案

    我目前的替代方案基于 TailwindCSS,所以自然包含 PostCSS、AutoPrefixer 等工具。然后用 postcss-import 实现自动导入和模块化;使用 tailwindcss/nesting 实现嵌套。

    为什么选用 TailwindCSS?首先,实际开发中,不管使用什么前端框架,我们都需要大量原子化的胶水样式,比如调整间距、改变字号、给容器添加一些边框、圆角、阴影等。这些样式如果都手写,工作量并不小;学习不同的样式名也是负担;以及最重要的,CSS 优先级问题。使用 TailwindCSS 就都能很好解决。

    TailwindCSS 不仅包含一大堆原子化样式,自身也是个完整且优秀的 CSS 编译器。它包含 reset,提供一组全局通用的 CSS 变量;它可以从各种文件里把我们用到的样式提取出来,构建后生成的 CSS 里只有我们要用到样式,不会有多余的;它会分析我们对样式的使用,合理的调整样式顺序,保证样式能正确生效。使用 TailwindCSS 可以节省很多时间。

    它还自带若干插件,比如解决嵌套的 tailwindcss/nesting,支持内容类元素的的 @tailwindcss/typography 等。使用这些插件也可以帮我们节省很多时间。

    最后,TailwindCSS 的生态不断成长,我们的选择范围越来越宽:HeadlessUI、DaisyUI、付费的 Tailwind UI 等。方便我们从产品生命周期的任意阶段开始集成。

    推荐项目配置

    启动项目的时候,安装依赖。包含 PostCSS + AutoPrefixer、TailwindCSS 和 DaisyUI。前者提供 CSS 处理框架,包含自动导入 css 和嵌套功能;后两者提供可见的 UI。

    pnpm i postcss postcss-import tailwindcss autoprefixer daisyui -D

    自动初始化配置,-p 会自动生成 PostCSS 配置:

    pnpm tailwindcss init -p

    调整 postcss.config.js,启用 postcss-importtailwindcss/nesting。目前我们常用的嵌套规则和 CSS 规范略有区别,不过无所谓,规范也没确定,所以这样就足够了。

    module.exports = {
      plugins: {
        'postcss-import': {},
        'tailwindcss/nesting': {},
        tailwindcss: {},
        autoprefixer: {},
      },
    }

    然后调整 tailwind.config.js

    const DaisyUI = require('daisyui');
    // 这个插件可以帮我们处理文档类内容,我建议常用
    const Typography = require('@tailwindcss/typography');
    
    module.exports = {
      // 从以下文件查找用到的样式
      content: [
        './index.html',
        './src/**/*.{js,ts,jsx,tsx,vue}',
      ],
      theme: {
        extend: {
          // 扩充 TailwindCSS 没有包含的样式
        },
      },
      plugins: [
        DaisyUI,
        Typography,
      ],
      daisyui: {
        themes: [{
          // 只构建一个主题: luxury,并覆盖其中的两个属性
          luxury: {
            ...require('daisyui/src/colors/themes')[ '[data-theme=luxury]' ],
            primary: '#FFA028',
            '--bc': '0 0% 87.5%',
          },
        }],
      },
    }
    

    然后,创建样式入口 main.css。其它样式可以如常写在这个文件里,不过如果要 @import 其它 CSS 文件,就要进行一些调整。具体可以看官方文档。

    @tailwind base;
    @tailwind components;
    @tailwind utilities;

    然后在入口文件引用 main.css 即可:

    import './main.css';

    至此,新项目配置完成,可以照常开发了。

    下期预告

    这次我先分享了整体思路:用新的工具链替代预处理工具,保证已有的功能不缺失。那么下期分享的内容就是使用新的 CSS 特性,更好的完成开发。


    如果你对新 CSS 感兴趣,对预处理工具和新工具链有兴趣和疑问,欢迎留言讨论。如果本文对你有启发,也请帮我点赞分享,谢谢。


    本文参与了 SegmentFault 思否写作挑战赛,欢迎正在阅读的你也加入。

  • 【视频】如何正确使用 TailwindCSS

    【视频】如何正确使用 TailwindCSS

    TailwindCSS 是一个争议很大的样式库。

    他封装了大量原子化的样式,比如 w-4,表示 width: 1remtext-gray-500,表示字体颜色为灰色500。如果我们某个节点同时需要两个样式,就是 class="w-4 text-gray-500"。极端一点的例子是这样子的:

    很明显,大家的争议点在于:

    1. 这么细碎的样式,我为什么不自己写?灵活性还高一些。
    2. 这么写跟 inline style 有什么区别?
    3. 开发一时爽,维护火葬场。

    基本上,发出这些疑问的都是前端。包括我,最初也是这样的想法。但是有一天,我要维护一个老项目,大部分组件都是现成的——引用自某个组件库,或者团队已经写好,只需要调整布局,我发现 TailwindCSS 简直是神器,太方便了。

    于是,当我反复看到大家争论该不该用 TailwindCSS 后,决定做一期视频,表达一下自己的态度:

    1. 我们做技术,要避免对一项技术做非黑即白的判断,更不应该轻易否定一项技术。同时,使用技术 A 并不代表就要拒绝技术 B。
    2. 具体到 TailwindCSS 上,使用它不代表我们从头到尾就要只能用 TailwindCSS;使用其它前端框架也不要求我们绝对不能使用 TailwindCSS。
    3. 所以正确的做法是,我们应该使用 TailwindCSS。
    4. 项目启动时,使用比较完整全面的前端框架,比如 Element UI、AntDesign,或者基于 TailwindCSS 打造的 DaisyUI;然后辅以 TailwindCSS。这样就可以同时照顾开发效率与维护效率,收获最佳效果。

    除了并不会降低开发效率之外,TailwindCSS 还有以下优势:

    1. 它跟内联样式有很大的区别,它的优先级很低,意味着我们也可以很容易覆盖、调整。
    2. TailwindCSS 样子很好看,直接能画出漂亮的界面。
    3. 基于 TailwindCSS 的代码分享很容易,只要复制粘贴 html 即可,在前端工程化日趋复杂的今天,简直是一股清流。
    4. 因为文档组织得更好,后端和其它领域的开发者也很喜欢使用 TailwindCSS 替代手写样式。

    所以无论如何,我都推荐所有团队所有开发者使用 TailwindCSS。当然,用其它原子化样式框架,比如 UnoCSS 也可以。

    如果你有其它意见和建议,欢迎提出讨论。如果你有 B 站账号,恳请三连+关注+转发,感谢。

  • 聊聊前端入门(1):HTML+CSS

    聊聊前端入门(1):HTML+CSS

    最近有一些新老同学入门前端,找我问问题,我从他们身上发现了一些共性问题,今天拿出来总结一下,希望后来者能吸取经验。

    前端三大件:HTML、CSS、JavaScript。这三位是根本,万变不离其宗,不管用多先进的技术,最终浏览器里跑的还是他们(不考虑 WASM、WebGL)。所以前端入门应该以这三种语言为基础,慢慢扩展到其它领域。

    当然有些同学可能上来就用框架,拜现代化前端技术所赐,有些同学可能完全没认真学习过这三门语言,也怼出了功能、甚至怼出了产品。但我建议,无论从上限考虑还是从下限考虑,都应该好好学习这三门语言本身。

    声明式语言

    入门一般都从 HTML、CSS 开始,因为它们很简单,简单的原因是因为它俩是声明式语言。那什么是声明式语言呢?标准定义咱们略过不谈,简单来说有两大特点:

    1. 需要什么你就写什么,需要这里有一个文字,那就写一个文字;需要有一张图片,就写一张图片。
    2. 不写的就不会出现。没有逻辑推演、计算,不会因为环境变量而有所不同,没写的就没有。

    于是它们有好处也有坏处:

    • 好处:没有心智负担,不用理解全局,就看某一行、某一段,写了就是有,没写就是没有。
    • 坏处:需要 100 个字就要写 100 个字。有 100 个元素共用一个属性,可能也要写 100 遍。

    如何学习 HTML+CSS

    有些同学上学期间学过编程,比如 C 语言、Java 语言。它们都是命令式语言,有更强的逻辑性,它们就像一本推理小说,你必须从头看到尾,不漏过一个线索,层层推理,才能得到结论。

    他们会套用自己在命令式语言方面的经验去学习 HTML+CSS,然后发现,好难……命令式语言的语法元素很少,难在组合出合适的逻辑、设计合适的数据结构。声明式语言能实现的效果总数是固定的,没有预先设计的部分,就是实现不了,谁都实现不了。但是每种效果的实现可能都是不同的,需要大量经验和记忆。用上一段的类比,声明式语言就像诗歌,甚至上一行跟下一行都没联系,但是连到一起还真有点意思。

    所以学习 HTML+CSS 就要有合适的方法。我个人的经验如下,

    1. 首先要知道

    HTML 标签 100+,CSS 属性 200+,这些都是知识性的内容,不知道就是不知道;不知道但是要用,那就做不出来。所以第一步是知道这些内容,方法大概是:

    1. 浏览文档,比如 MDN
    2. 自己实验,加深记忆和理解
    3. 跟背单词一样,要经常性重复这个过程,才能真的记住

    这个过程不需要了解的很透,重点在于知道这些标签和属性的存在,当你面对需求的时候,才会想到解决方案。

    2. 接下来要多尝试

    经常浏览 codepen.io 这样的网站,上面有很多别人做的范例,可以长很多见识,让你恍然大悟,原来可以这样做那个东西。

    同时,也可以反过来用看到的范例指导自己的学习。比如,看到一个 css 规则,这个你刚好不熟悉,就可以找来文档仔细阅读理解一下,或者在论坛上询问其他前辈。

    一段时间之后,相信你会对大部分 html、css 属性都了如指掌,看到人家的网站,也大概知道该怎么做。但是给你一个实际项目,你可能还做不出来,或者做不好。没关系,那是下一步的目标。

    3. 紧跟社区步伐

    跟其它语言不一样,声明式语言从诞生开始,语法上不会有太大变化,语法元素也很固定。升级换代主要来自增砖添瓦,即新规则、新属性。

    比如 CSS,早年我们布局只能依靠 float。很难用,有各种诘屈聱牙的概念需要记忆。后来就有了 display:flex,也就是弹性盒模型,好用的多。后来又有了网络布局。然后因为各种内部弹性外部弹性,又有了 min-contentmax-contentfit-content 等新属性。

    这些东西,很新,有时候没有办法找到合适的学习资料。于是就要紧跟社区来,通过技术账号了解到新知识,通过技术文档学习新知识。

    4. 突破自己

    自学里面,最难的一件事其实就是突破自己。就好像我今天看到一个帖子,楼主说准备找到工作后就办张健身卡去健身,然后下面一群人歪楼,说“为什么健身要办健身卡?”

    因为自己监督自己很难,自己给自己打分更难。所以学校才要安排各种考试,给学生打分,让大家知道自己的位置。

    所以,做出来自己看觉得还行,没有用。这里有几个建议:

    1. 瞄准一个产品,比如微信,做到像素级复制。可以截图然后调到 50% 透明度,叠到一起慢慢看。
    2. 选择最合适的技术,坚持到底,不要因为某些环节难以突破就乱来。
    3. 邀请比较有经验的前辈帮自己做 code review

    附加:最好找个靠谱的前辈做引路人

    (我想了想还是把这条加上。)

    互联网是个好地方,我们能免费获取几乎所有有价值的资料。但是对于 Web 开发、前端开发这种领域,长期积累的海量知识储备也可能成为大家学习的障碍——东西太多,不知道从何处下手。

    所以,有个靠谱的引路人也很重要,会帮你节省很多时间。笔者不才,亦好为人师,有需要的可以找我:

    新人常犯的错误

    新人难免会犯错,这里列举一些常见的、可以规避的问题:

    1. 沉迷细节无法自拔

    有些同学沉迷细节,喜欢抠字眼,经常会提出一些奇奇怪怪的问题。很多问题其实没有准确答案,也不需要准确答案,尤其是对于新人来说,可能三两年内都不会真的需要解决。

    尤其是 HTML、CSS,因为是声明式语言,很多时候,它们的行为就是这么设计的,不需要也没办法深究;有些设计,可能来自二十年前甚至更早,现在早已没有那个应用场景,新人难以理解也是正常的。

    所以我建议大家以记用法积累经验为主,辅以实操验证,不需要像学命令式语言那样去强迫自己理解。

    2. 遇到问题生拼硬凑

    有些同学遇到问题就发慌,然后开始百度,看到一个方案就复制粘贴试一试,能行就继续,不能行就复制粘贴下一个。结果代码就补丁摞补丁,各种解决方案混合在一起,毛病越来越多。到最后可能完全超出掌控、无法解决。

    这里大家需要明白,语言元素很多,组合方式也很多。同样的问题,可能来自不同的根源;类似的组合,也会产生不同的问题。绝大部分时候,我们都要先选择方案,然后围绕方案组织代码,解决遇到的问题。在不同方案间左右横跳,最后结果多半不是好处均沾,而是问题集中爆发。

    如果能够找到最佳实践,那就坚持。如果几个方案都差不多,那就选好一个,坚持下去。遇到问题,面对多个搜索结果,要分析它们是怎么做的,解决了什么问题,不要直接拷过来试。坚持一段时间,会有很好的结果。

    总结

    我当年也是自学成才,走过不少弯路,希望这系列分享能帮大家节省一些时间。

    有任何问题和建议,欢迎留言评论。下次聊聊 JS 方面的学习。

  • 纯 CSS 实现优惠券效果

    纯 CSS 实现优惠券效果

    (本文不是广告,因为没给钱。)我厂 code.fun 上线了付费购买与优惠券功能,欢迎各位新老顾客莅临。

    上面是优惠券的视觉效果,本文分享如何使用纯 CSS 实现它,希望对大家有帮助。

    0. 分析

    首先,我们来分析一下这个优惠券的实现方案。

    左边是摘要,右边是详情,这个部分用 display:flex 很容易就能搞定。中间的虚线,使用任意一个容器边框 + 少许 padding 即可实现。其它部分,也就是些字体行高,都不是很复杂。难点在于投影,尤其是左右两个挖空的半圆。

    1. 挖空

    1. 首先,我们给整个优惠券矩形加上投影
    2. 然后,我们给两边加上两个包含内投影的圆形
    3. 这个时候,两边的内投影原型会多出来一块,我们需要把它们盖住。但是,不能让矩形 overflow:hidden,因为投影也会变,如下图。
    .coupon {
      width: 15rem;
      height: 6rem;
      background: white;
      box-shadow: 1px 1px 6px rgba(0,0,0,.15);
      position: relative;
      overflow: hidden;
      
      &::before,
      &::after {
        background-color: white; 
        border-radius: 1rem;
        box-shadow: inset 1px 1px 6px rgba(0, 0, 0, 0.15);
        content: '';
        width: 2rem;
        height: 2rem;
        position: absolute;
        top: 2rem;
        z-index: 1;
      }  
    
      &::before {
        left: -1rem;
      }
      &::after {
        right: -1rem;
      }
    }

    2. 增加父容器

    我的第一反应是增加父容器,让父容器 overflow:hidden 来隐藏多出来的部分。但是不行,会影响投影。

    但是转念一想,我们可以不让父容器限制显示内容,而是在父容器内部增加一些元素,遮蔽多出来的内容。比如用两个纯色圆形,把竖着的阴影遮起来。

    于是我把上面的样式更名为 .coupon-inner,然后增加一个父容器。父容器的 ::before ::after 伪元素都搞成略小一圈的纯色圆形,把竖着的投影挡住,最终效果如下图。

    3. 总结

    到这里,效果就基本让人满意了。

    最终完成的代码可以在 codepen 里看到:

    https://codepen.io/meathill/embed/oNqxOXE?default-tab=html%2Cresult&editable=true

    使用纯 CSS 的好处,在于体积小、加载快,调整起来非常灵活,能用 CSS 最好都用 CSS。

    有任何问题和建议,欢迎评论、讨论。

  • 代码分享:翻转小动画

    代码分享:翻转小动画

    朋友对我图省事用的 animate.css flipInX 效果不满意,软磨硬泡非要我改成 wordle 原版那种整个翻过来的。于是我就想办法实现了一把,比想象的稍微复杂一些,难点在于我不知道 transform-style: preserve-3d 这个属性。没有这个属性,正反两个图层就被压在一个平面里,怎么翻转都是上面那个图层显示出来。

    我用来调试的代码实现请点 这里,为了方便调试,我这次没用 codepen,用的 Vue SFC Playground,效果挺好,尤其是适合不熟悉属性时一边试一边写。不知道 codepen 是否支持这种玩法。

    实现的代码与之类似:

    <template lang="pug">
    .game-item
      .face G
      .face G
    </template>
    
    <style lang="stylus">
    .game-item
      aspect-ratio 1
      position relative
      // 这个样式很重要,没有它就没有背面一说
      transform-style: preserve-3d;
      user-select none
    
    .face
      position absolute
      top 0
      left 0
      width 100%
      height 100%
      display flex
      justify-content center
      align-items center
    
      &:first-child
        // 把第一层稍微提高一点点,不能太多,不然旋转效果不好看
        transform translateZ(0.1px)
    
      &:last-child
        // 背面提前翻转 180 度,这样转过来才是对的
        transform rotateX(180deg)
    
    @keyframes flip
      from
        transform: perspective(400px) rotate3d(1, 0, 0, 0)
    
      to
        transform: perspective(400px) rotate3d(1, 0, 0, 180deg)
    
    .flip
      animation-name: flip
    </style>

    这种效果平时还是找时间写上一两次,CSS 属性是知识性内容,没办法凭经验推出来,平时得注意积累。

  • 笔记:使用 Vite+Vue3+TypeScript+Tailwind CSS 开发 Wordle

    笔记:使用 Vite+Vue3+TypeScript+Tailwind CSS 开发 Wordle

    最近有个小游戏很火,叫 Wordle,是个填字游戏,推上随处可见相关分享。各种衍生版也层出不穷。有位朋友让我帮他复刻,反正过年,闲着也是闲着,我就尝试用标题里的新技术栈帮他写了一个。现在已经上线了,对填字游戏感兴趣的同学可以试试:

    My Wordle(特色:1. 支持自定义单词;2. 5~9字母;3. 无限模式,可以一直玩。)

    这篇文章简单记录一些开发过程和经验。

    0. 创建 vite 项目

    npm init vite@latest

    接下来按照提示选择喜欢的配置即可。

    小配置下 vite.config.js,添加 @ 别名:

    // 为了方便后面开发,改为函数式,这样获取状态更容易
    export default defineConfig(({command, mode}) => {
      const isDev = command === 'serve'; // 这里先留个例子,后面骨架屏有用
      return {
        server: {
          host: true, // 允许外部访问,方便手机调试
        },
        plugins: [
          vue(),
        ],
        resolve: {
          alias: {
            '@': resolve(__dirname, './src'),
          },
        },
      };
    });

    注意,上面的配置文件虽然是 ESM,但实际上仍然在 CommonJS 环境里执行,所以有 __dirname

    然后安装依赖:pnpm i

    最后 npm run dev 即可启动开发服务器进行开发。Vite 真是快,空项目 100ms 就启动了。

    1. 安装+配置 Tailwind CSS

    pnpm i -D tailwindcss postcss autoprefixer
    npx tailwindcss init -p

    配置 tailwind.config.js

    module.exports = {
      content: [
        "./index.html",
        // 注意:因为我喜欢用 pug 开发模版,所以一定要在这里添加 `pug`
        "./src/**/*.{vue,js,ts,jsx,tsx,pug}",
      ],
      theme: {
        extend: {},
      },
      plugins: [],
      // 这部分 css 需要通过比较复杂的计算得出,所以要用 `safelist` 保证它们出现在最终 css 里
      safelist: [
        {
          pattern: /w-\d+\/\d+/,
        },
        {
          pattern: /grid-cols-[5-9]/,
        },
      ],
    }

    接下来创建 CSS 入口文件:./src/style/index.css

    @tailwind base;
    @tailwind components;
    @tailwind utilities;

    最后在入口文件里引用它即可:

    import { createApp } from 'vue'
    import App from './App.vue'
    import './index.css'
    
    createApp(App).mount('#app')

    2. 配置 TypeScript

    默认的 TypeScript 就挺好使,这里简单配一下 @ 别名即可:

    {
      "compilerOptions": {
        "paths": {
          "@/*": [
            "./src/*"
          ]
        }
      },
    }

    为开发方便,还需要装一些类型定义:

    pnpm i @types/gtag.js @types/node -D

    3. 配置 eslint

    安装所需依赖:

    pnpm i -D @typescript-eslint/eslint-plugin @typescript-eslint/parser @vue/eslint-config-typescript eslint eslint-plugin-babel eslint-plugin-vue

    配置 .eslintrc.js

    module.exports = {
      "env": {
        "browser": true,
        "es2021": true,
        "node": true
      },
      parser: "vue-eslint-parser",
      parserOptions: {
        parser: "@typescript-eslint/parser",
        sourceType: 'module',
        ecmaVersion: 'latest',
      },
      extends: [
        'eslint:recommended',
        'plugin:vue/vue3-recommended',
        '@vue/typescript/recommended',
      ],
      plugins: [
        'babel',
        'vue',
      ],
      globals: {
        // vue3 <script setup> 工具函数
        withDefaults: true,
        defineProps: true,
        defineEmits: true,
        defineExpose: true,
    
        // 我自己定义的常用类型
        Nullable: true,
        Timeout: true,
    
        $root: true,
      },
      "rules": {
        // vue3 <script setup> 暂时必须关掉这几项
        '@typescript-eslint/no-unused-vars': 0,
        '@typescript-eslint/ban-ts-comment': 0,
        '@typescript-eslint/no-non-null-assertion': 0,
      },
    }

    4. 拆包与懒加载

    Wordle 游戏需要用到词库,5个字母的词库 200+KB,6789的词库都是 300KB,全部放到一起加载太浪费,所以计划按需加载。另外,词库几乎是不变的,但业务逻辑是多变的,如果不拆包的话,每次业务逻辑变更后,用户都要重复下载好几百 KB 的词库,所以要做拆包与懒加载。

    拆包方面,vite build 是基于 rollup 实现的,所以要按照 rollup 的要求,用 rollupOptions 来配置:

    export default defineConfig(({command, mode}) => {
      return {
        ....
        build: {
          rollupOptions: {
            output: {
              manualChunks(id) {
                // fN 是高频词,出题时只用高频词,校验单词时才用全量词库。高频词独立打包。
                if (/[sf]\d\.txt\?raw$/.test(id)) {
                  return 'dict';
                // 分享文案,为方便朋友修改,独立打包
                } else if (/share\d\.txt\?raw$/.test(id)) {
                  return 'share';
                // 其它依赖,打包成 vendor。不知道为什么,必须有这一配置,前面两项才能生效
                } else if (id.includes('node_modules')) {
                  return 'vendor';
                }
              },
            },
          },
        },
      };
    });

    默认只加载高频词,全量词库会在页面准备就绪后,通过 fetch API 异步加载。

    5. 添加骨架屏

    网页里有个静态 footer,因为 CSS 文件加载需要一些时间,所以会先渲染出来,然后再挪到正确的位置,造成抖动。这里有两个选择:1. 隐藏 footer,或挪到 vue 里;2. 添加加载中样式。我选择在(2)上再发展一点,做骨架屏。

    我的思路是:

    1. <div id="app"></div> 里填充只有标题的 header 和填字格
    2. 拆分 tailwind.css,分成
      1. 只包含 reset 和 index.html 所需的 inline css
      2. 包含全部业务样式的外部 css 文件
    3. 编写脚本,build 时往 index.html 里塞入填字格,并把 inline css 写入网页 <head>

    实现方面,首先编辑 vite.config.js

    export default defineConfig(({command, mode}) => {
      // 根据开发、构建环境,使用不同的 tailwind 入口文件
      const tailwind = command === 'serve' ? 'tailwind-sketch.css' : 'tailwind.css';
      return {
        resolve: {
          alias: {
            '@tailwindcss': resolve(__dirname, './src/style/' + tailwind),
          },
        },
      };
    });

    我从 tailwind.css 里移除了 @tailwind base,即 reset 部分。这部分会被 inline 到 index.html 里。然后修改 tailwind.config.js,加入根据环境判断是否需要分析 index.html 的逻辑:

    const isDev = process.env.NODE_ENV ==='development';
    module.exports = {
      content: [
        './src/App.{pug,vue,ts,tsx}',
        './src/**/*.{vue,js,jsx,ts,tsx,pug}',
      ].concat(isDev ? './index.html' : []),
      theme: {
        extend: {},
      },
      plugins: [],
    }
    

    接着修改 build 脚本,增加编译 inline css 和注入功能:

    {
      "name": "aw-woodle-game",
      "version": "0.4.0",
      "scripts": {
        "build": "vue-tsc --noEmit && vite build && npm run build:sketch && npm run build:sketch2 && build/build.js",
        "build:sketch": "tailwind -i src/style/tailwind-sketch.css -o tmp/sketch.css --content index.html --config build/tailwind.config.sketch.js --minify",
        "build:sketch2": "stylus --include-css --compress < src/style/sketch.styl > tmp/sketch2.css",
      },
    }

    6. 总结

    最终我使用这套工具链完成了整个产品的开发。不过仍然有一些问题未解:

    1. Vite 开发环境与构建环境其实是两套工具链,所以能过 dev,未必能过 build,尤其是 TypeScript,会有很多差异。不过只要耐心调试一番,也不难搞。
    2. 不知道是否跟我用 pug 有关,Tailwind 总是慢一拍。即我改了模版,html 变了,但对应的样式并没有出来;过一会儿,就出来了。于是我要么继续改,期待后面它会加上来;要么只有手动刷新。
    3. build 时,rollup 会把 css 放在 <head> 里面,导致浏览器认为这段 CSS 是关键内容,加载完之后才进行第一次渲染(FCP),使得我的骨架屏失效,所以要手动把它挪到 <html> 的最后。(当然在 GA 等代码的前面)

    最终的 Lighthouse 得分比较喜人,贴一下:

    (更多…)
  • 使用 SVG 制作扇形

    使用 SVG 制作扇形

    有时候我们需要制作扇形,比如图形化数据生成饼图的时候。使用 HTML + CSS 做不到,必须借助 SVG 帮助。经过一些摸索,大概方式如下:

    0. 创建 SVG

    我们需要一个 SVG,然后在里面画一个园:

    <svg xmlns="http://www.w3.org/2000/svg" height="600" width="600" viewBox="0 0 20 20">
      <circle r="5" cx="10" cy="10" />
    </svg>

    这里,我创建了一个 SVG,并且以 10,10 位圆心,画了一个半径为 5 的圆。SVG 的视窗只需要显示这个圆,所以是 0 0 20 20 的正方形。widthheight 用来定义网页中 SVG 的尺寸,SVG 是矢量图形,可以实现内容的无损缩放,所以即使显示尺寸比图形尺寸大很多,也不用担心出现锯齿。

    1. 用边框画圆形

    接下来,我们用给圆加边框的方式来做圆形。

    <circle
      r="5"
      cx="10"
      cy="10"
      fill="transparent"
      stroke="tomato"
      stroke-linecap="butt"
      stroke-width="10"
    ></circle>

    首先,我们用 fill="transparent" 清理掉圆形内部的颜色,然后用 stroke="tomato" 给边框加上橙色。接下来,我们通过 stroke-width="10" 设置边框宽度为 10,这也是矩形半径。

    此时,屏幕上会出现一个橙色的正圆。

    2. 画扇形

    画扇形的方式有很多,比如画两条半径然后画弧形再填充颜色。但是利用边框画扇形最简单。

    用边框画扇形说白了,其实是结合圆环和虚线,需要有扇形的地方,就填充颜色;不需要扇形的地方,就用虚线的空白。这里要用到 stroke-dasharray 属性,它的规则很简单,奇数为实偶数为虚,所以我们只要计算扇形所需的弧形长度,然后剩下的填充周长即可。

    在我们这里,就是 stroke-dasharray=”calc(10 * 3.1415926 * 1/6) 31.415926",即取绘制一个 1/6 大小的扇形。

    3. 修改位置

    修改位置需要使用 stroke-dashoffset 属性,它会把图形从原来的位置移动若干距离,正的就往起点移动,负的就往终点移动。

    在我们这里,就是 stroke-dashoffset="calc(-10 * 3.1415926 * 1/6)",将第二个扇形移到第一个扇形的旁边。

    4. 其它+已知问题+扩展阅读

    最终效果:https://codepen.io/meathill/pen/yLMQqBQ?editors=1000

    这些属性,也可以使用 CSS 样式替换,效果一样。

    Safari 问题比较多,首先半圆就不是半圆,其次偏移也不对,不知道是否只支持 CSS。