自己亲手做一个天气卡片组件的想法其实很早就有了,但是做起来难度还是很大的(布局、数据源、天气展示、自适应),最终不了了之。最近学校社团面试题目是做一个天气卡片,正好可以借此机会趁着国庆小长假静下心来好好研究一番。于是就有了今天的这篇文章。
(实际上在国庆小长假之前就基本上把问题搞定了)目前版本的DouWeather托管在临时仓库,因为没有考虑代码结构,后期维护困难。可以watch一下这个仓库关注后续进展,我会抽时间重构下代码并在这个仓库更新。
我给DouWeather(后称DW)的定位是网页小组件,也是出于这个考虑,我参考了如iOS系统的小部件、新版MIUI系统小组件、鸿蒙系统小部件、win11小组件,发现都无一例外具有同一特征:扁平化,圆角,选用无衬线字体,元素风格简洁,并且四者都在或背景或图标中大量使用渐变,使小部件表现得较为灵动。其中win11小部件添加了浅阴影,可能是为了让小部件从亚克力背景中凸显出来。
于是乎,我也照猫画虎,设计了DW的晴天图标,并且用XD设计出了第一种卡片样式(现DW的medium卡片样式)。
注: DouWeather的设计也参考了google adsense(MD2),mew等网站的设计风格。
将medium类样式的卡片缩短,便衍生出了一个正方形的small样式小组件。
之前心血来潮想写天气卡片的时候也做过图,当时参考的MD风格,正好趁着国庆回家从电脑里边翻了出来,下面便不知道多久前天气卡片的初稿…就…蛮抽象的…
DW中许多要素都抄借鉴了小米天气,图标也是如此。DW尽量保证图标整体风格简洁,使用大块的渐变背景突出天气特点。在正式开干前,我就仿照小米天气做出了晴、阴、多云这三个图标,方便之后的设计和开发。格式依然采用了svg,控制组件整体的体积,保证加载速度。
开发过程中受到室友启发,尝试为天气图标增加了一些动画,不过有些喧宾夺主,最后不了了之。
开发前我其实仅仅计划做出两种样式(即small和medium)。做normal样式的主要原因,是开发过程中我发现:当medium样式被置于一个宽度过大的元素上方时,会显得内容空洞,不够美观。于是便在medium样式的基础上,加长了宽度,增加了空气质量、防晒建议等数据展示。而detail样式,纯粹是因为我对小米天气的趋势预报爱得深沉,想要在DW中复刻一个出来。
在写天气卡片前,我只使用过一次Web Components,那是在原神玩家信息查询中,当时是因为有很多重复的要素(角色信息),所以想尝试用这个新鲜玩意封装一下。得到的教训就是:如果不用构建工具,又想要较为优雅地开发,template标签是必不可少的,否则维护代码简直要了我的老命。
天气图标会在卡片中大量复用(尤其是detail样式),如果没有一个比较简洁的调用方式,维护起来会很困难。并且在开发时图标仅设计了3个,需要顾及开发后期如何便捷地对图标增删改,尽量降低图标检索和主体代码间的耦合度。在前端中,一般有下面几种图标引入方式:
大型图标字体一般都采用这种方式,如Font Awesome和Material Icons。优点是操作直观,能够使用font-size或者color直接修改图标展现形式,而且得益于浏览器对colr的支持,能够使用彩色图标字体。不过缺点也很明显:维护较为困难,尤其是涉及到渐变填充,目前还没有什么字体制作软件能够较为优雅地完成这个任务。并且某些手机自定义字体的hook逻辑可能导致这种方法引入的图标字体无法生效。
不过,在DW中也有一部分图标采用了这种方式,那便是风向的图标,图标单色且数量固定不需要频繁修改(8个方位),非常适合使用这种方式。
这也是很常用的一种图标引用方式,兼容性极好。维护相对方便,能够支持一些动画。AI能够直接导出图标为symbol标签,而且有许多构建工具也能够为此提供支持,基本没有缺点。
然而DW的天气图标并没有采用上述的两种方式。我对图标部分使用Web Components做了封装,已经是类似symbol的作用,因此再使用symbol便显得有些多此一举。
封装后的天气图标调用就方便多了,可以直接使用 <dw-icon type="sunny"></dw-icon>
这样的代码来调用特定的图标,下面是一个示例。
See the Pen WebComponents封装天气图标 by 戴兜 (@DaiDR) on CodePen.
之后打算使用gulp,这种方式也能够为开发提供便利。
在天气组件的开发过程中,我才发现还原设计稿其实是这其中最简单的一件事。我需要保证卡片中的所有元素都能有条不紊地展现出来,我原本想要固定每一种样式的卡片宽度,这样能够确保卡片的布局总是完美的,但是会使天气卡片的泛用性大打折扣,其他使用DW的人并不会专门为了一个小组件而修改自己的布局方案,同时固定宽度意味着在移动设备上,天气卡片的体验会很糟糕。但是自适应,又该怎么做呢?
最常用的自适应方法是写媒体查询,但是我不能使用媒体查询,其他开发者在哪儿插入卡片、怎么插入卡片、卡片的父级元素是什么状态我都无从得知,我不能仅通过屏幕尺寸判断出天气卡片目前的状态。
我也不能通过判断卡片宽度就隐藏或显示某些元素,因为之后的版本DW会将数据展示的部分模块化,允许其他开发者自定义展示哪些数据,擅自修改展示的元素可能导致其他开发者的配置没法如预期那样展示出来。
既然谈到了判断父容器尺寸,不如来谈谈实现方式。一般来说,我常用的方法是在父容器中嵌入一个iframe,通过iframe的尺寸变化监听容器尺寸变化,或许未来也可以试试css容器查询(Container Queries),能够提供很大便利,不过目前这个特性还处在pr阶段…兼容性列表。
我一度想要固定卡片宽度,事实上直到我将DW的布局和逻辑基本全部写完后,我依然没有找到很好的解决方案。
给我灵感的,是windows的资源管理器:
天气卡片的主体元素固定在左侧不动,右侧的数据展示根据卡片宽度显示滚动条,实现也非常简单,因为我使用的弹性布局,只要在原来的数据展示区域外边包装一层带有 flex-grow: 1;
样式的容器就好了。
这一部分也很复杂,因为社团面试任务中有提到图表展示,当时是想复刻一个小米天气的15天趋势预报试试水,如果成了的话之后还可以拓展到小时预报之类的图表展示。图表部分是使用svg实现的,为了让暗黑模式的样式操作能够便利,所以使用了svg而不是canvas。绘图直接用的浏览器原生js实现,只需要绘制一个折线图,chartjs显然有些大材小用,比较臃肿。原先设计稿中采用的展示方式很难优雅地在中间位置插入图表,所以后来将早上数据、图表、晚上数据全部分了开来,因为列宽是一致的,所以也不用担心错位的问题。
原设计
修改后
接着就是绘制图表了,首先统一计算出折点的X坐标,接着按照温度确定出每个折点的Y坐标,折点用的是svg的circle元素,折线部分直接用path搞定了。
path的d参数语法逻辑其实和canvas绘制的逻辑是相类似的,首先使用M(MoveTo)指令将起点移动到第一个点的位置,接着只需要使用L(LineTo)指令绘制之剩下折线便完成了。
只需要使用 @media(prefers-color-scheme: dark)
这个媒体查询便能够定义暗黑模式下的卡片样式。
值得一提的是,我使用了css变量,目前大部分浏览器已经兼容了,能够大幅减少重复代码。
有时候使用者可能不想让媒体查询自作主张修改卡片样式,于是乎我提供了属性 theme
来控制卡片颜色。可以使用theme="light"
或是 theme="dark"
将卡片锁定在明亮模式或暗黑模式。这点小功能我想着完全用css来实现,之前Web Components用得不多,想着用宿主选择器轻松就能搞定,便想当然地写出了下面的这段css…
:host {
// 默认样式
}
:host[theme="dark"] {
// 暗黑模式样式
}
然鹅…翻车了,样式并不会生效,翻遍了MDN后,我找到了这个选择器:host()(面向MDN编程),所以正确的写法应该是这样(所以有哪个翻过blink源码的小伙伴能告诉我为什么要这样设计么…):
:host {
// 默认样式
}
:host([theme="dark"]) {
// 暗黑模式样式
}
这次写DW,让我学到了许多,之前写前端很少会自己去做图表生成,经常是引用个chartjs或是echarts了事。对Web Components也有了较之前更为全面的了解,同时也熟悉了一下flex布局的使用,至少2天多的小米天气没白看,我同学看我一天到晚拿着手机刷小米天气以为我疯了。
code{background: #f5f2f0;}