这些天,我们前端组一起处理的网站换肤功能
前言
大家好,我是沐浴在曙光下的贰货道士,好久不见,别来无恙!本文主要根据UI
设计需求,讲述一套基于scss
封装方法的网页响应式布局,以及不同于传统引入element UI
主题配色文件的换肤思路。大家仔细看完文章,相信一定会有所收获。倘若本文提供的响应式布局思路对您有所帮助,烦请大家一键三连哦。同时如果您有其他响应式布局解决方案或网站换肤思路,欢迎您不吝赐教,在评论区留言分享。感谢大家!
需求分析
- 早期我们前端项目组开发了一个国外业务网站。这周为了迎合其他国家的喜好,需要在国外业务项目的基础上,新建多个项目,对之前的主题配色和部分布局进行修改,并需要适配不同分辨率下的屏幕。
UI
提供了包括主题配色和页面布局修改在内的一系列项目稿件,这些稿件基于1920px
分辨率的屏幕进行处理。前端需要根据UI提供的主题色,修改项目中的颜色变量。接口暂时使用国外业务的那一套接口,后期需要对接这些项目的接口,而我们目前的主要任务就是处理这些项目的静态页面改版。 - 主题色修改:
- 首先,我们前端团队需要根据
UI
提供的主题色,更新项目中的颜色变量,确保页面上的所有元素都符合新的配色方案。 页面布局
提供的修改稿件,如果有不在主题色内的颜色,需要和UI
确认是否需要更换为其他颜色相近的主题配色或者双方都新增主题配色- 检查项目中包括
CSS
和HTML
在内的所有带#
颜色值的信息。与UI
确认后,将其更换为其他颜色接近的主题配色,或者双方共同新增主题配色,以确保配色方案的一致性和协调性。
- 首先,我们前端团队需要根据
- 响应式布局:
- 前端需要根据
UI
提供的稿件和意见,适配项目在不同屏幕下的样式。对于页面上的不同元素,在小于等于1920px
的屏幕上进行缩放时,需要保持横纵比,并根据页面大小进行等比例缩放,包括容器宽高、间距等在内的页面布局是否合适都需要与UI
确认;在高于1920px
屏幕的设备上,需要保持和1920px
屏幕的布局风格,即元素的宽高不变。 - 然而,字体元素在页面缩放时,需要保持一定的风格。比如:
16px
的文字最小不能低于14px
,18px
、20px
以及24px
的文字最小不能低于16px
,32px
的文字最小不能低于18px
,36px
的文字最小不能低于20px
,44px
的文字最小不能低于28px
,48px
的文字最小不能低于32px
。 - 在移动设备上,需要保持和800px网页相同的布局。
- 前端需要根据
项目现状
- 主题色: 早期在与UI团队合作时,我们为国外业务系统确定了一套配色方案,并将其定义在项目的颜色变量中。然而,后续设计稿中出现了一些不在这套配色方案中的色值。 由于种种原因,我们在开发时没有与UI确认这些颜色是否需要更换,也没有将新增的颜色定义到颜色变量中,而是直接在代码中使用了这些颜色值。这导致在此次换肤过程中,仅通过修改颜色变量无法实现统一换肤的效果。我们需要逐一检查代码中硬编码的颜色值,并将其替换为新的颜色变量,以确保换肤的统一性和一致性。
- 布局: 以前我们使用
flex
、百分比、最小最大宽度/高度以及element UI
的栅格布局做了一些简单的适配,但这些方法不够灵活。为了更好地适应不同分辨率的屏幕,我们需要采用更为灵活和动态的布局方案,以确保在各种设备上的显示效果都能达到预期。
思路分析
主题色
传统的解决方案
- 以前在官网上,我们可以直接编辑并修改一套主题色。点击下载后,会生成一个
css
文件。 - 将下载后的
css
文件引入到我们项目中,可以看到编译后的css
文件 - 最后在项目中的入口文件,引入我们下载的
css
文件(这种方式会增加app.css
的体积)。
`main.js`
import '@/styles/theme/index.css'
- 后续处理的优化
`将编译后的element样式,从main.js指向到index.html中,减小了main.css体积`
`main.js中的css文件,最终还是会link到index.html中。那为什么还要把它拆开呢?`
`这涉及到css的拆分:浏览器会并行请求加载多个css文件,比单独请求并加载一个css文件要快`
`这样处理的目的是:将main.js中的css文件,抽出一部分放到index.html中`
<link rel="stylesheet" href="<%= BASE_URL %>theme/index.css">
- webpack小知识:
loader
webpack
只识别js
文件:当遇到其他非js
文件时,因为不识别js
文件,所以需要使用loader
插件(或引入第三方插件,或自己编写一个loader
方法),将其他文件转换为webpack
能够识别的js
文件。- 因此,
loader
的作用相当于对传入的非js
文件做处理,将它转换为webpack
可识别的js
字符串。
在字体商用不侵权的前提下,
严格遵循设计稿的字体样式
`如果用户电脑不存在设计稿上提供的字体样式,则会展示用户电脑的默认字体样式。`
`为此,我们需要下载并引入字体,将字体集成到网站中,确保用户电脑呈现效果与我们开发一致`
`(1) 引入: 在public文件夹下新建fonts文件夹,在fonts文件夹下引入我们下载好的字体样式`
`(2) 在index.html中, 为document增加字体`
`(3) 引入并挂载字体后,我们就可以使用下载的字体了,也可以在body上全局挂载字体`
`类似element字体的引入和挂载`
`FontFace: https://developer.mozilla.org/zh-CN/docs/Web/API/CSS_Font_Loading_API`
const font1 = new FontFace(
'iconfont',
'url(/iconfont/iconfont.woff2?t=1688345853791),
url(/iconfont/iconfont.woff?t=1688345853791),
url(/iconfont/iconfont.ttf?t=1688345853791)')
const font2 = new FontFace(
'element-icons',
'url(/theme/fonts/element-icons.woff),
url(/theme/fonts/element-icons.ttf)')
font1.load().then(function() {
document.fonts.add(font1)
})
font2.load().then(function() {
document.fonts.add(font2)
})
现在的解决方案
由于element UI
官方已不再维护传统的主题配色下载,我们项目采取官方提供的第二种方式:
- 原理: 我们项目使用
scss
编写css
,element UI
的theme-chalk
又恰好使用scss
进行编写。在官方定义的scss
变量中,使用了!default
语法,用于提供默认值。这也就意味着,我们不用考虑css
的加载顺序,直接新建scss
文件,覆盖定义在theme-chalk
文件且在我们系统中常用的scss
变量,达到在css
编译阶段自定义主题scss
变量的效果。
- 引入变量: 新建
element-variable.scss
文件,在这个文件中引入theme-chalk
定义的主题scss
变量,同时需要改变icon
字体路径变量(使用传统方法不需要改变路径变量,是因为我们直接引入了编译后的css
文件,里面已经帮我们做过处理了;而使用现在的解决方案,如果不改变字体路径变量,项目会提示找不到icon
字体路径,所以这个配置必填)。此时,将这个文件引入到我们的入口文件,那么系统中已经存在theme-chalk
定义好的scss
变量了
- 修改变量: 新建
element.scss
文件,在里面覆盖我们需要修改的主题变量,最后在vue.config.js
中sass
配置下的additionalData
里全局引入到项目中的每个vue
文件中(因为是挂载到每个vue
文件中,所以这个配置下的scss文件不宜过多),方便在vue
文件中直接使用变量。
优势
1. 定制化和灵活性
- 更改主题色和变量: 轻松改变
Element UI
的主题色、字体、间距等变量,而无需过多地覆盖现有的element CSS
样式。 - 精细控制: 原先的配置方式只能配置主题色,无法控制更细粒度的配置,比如边框颜色之类。
2. 避免样式冲突
- 避免样式覆盖的冲突: 通过直接修改
SCSS
变量来定制样式,可以避免在使用编译后的 CSS 文件时可能出现的样式覆盖冲突问题。这样可以保证样式的独立性和一致性。
3. 便于维护
- 集中管理: 所有的样式修改都集中在一个地方(变量文件),这使得维护样式变得更加方便和清晰。只需要修改文件中定义的变量,就可以影响整个项目中的样式,无需逐一查找以及修改每个组件的样式。
缺陷
- 在
sass loader
的additionalData
中配置了过多的全局css
变量,添加到每个vue
文件中 - 相比之前的处理方式,在
main.js
中引入element
自定义的主题scss
变量,首页加载的css
文件更多,
响应式布局
思路分析
UI
提供的稿件是1920px
,前端需要对UI
提供的稿件进行一比一还原;- 网页在小屏缩放时,需要保持元素的横纵比。针对这个问题,我们可以用百分比作为布局单位。 以设计稿宽度
1920px
为基准,建立px
和vw
之间的关系。如果把1920px
视为100vw
,那么1vw = 19.2px
。 如果设计稿上某个元素的宽度为192px
, 那么将它换算得到的结果将会是192px / 19.2px * 1vw = 10vw
。因此我们在布局时,需要严格遵循UI
提供的设计稿件,并借助下文封装的方法,将设计稿元素的像素作为第一个形参,传递到下文封装的方法中;实现思路:为等比例缩放网页元素,先去掉传入的像素单位。最后使用前文提到的换算公式,不论宽高,都将其转换为
vw单位,等比缩放
。 - 字体页面元素在放大时,需要限制字体元素展现的最大阈值。 那么我们封装的方法,第二个形参需要控制字体元素的最大阈值;
实现思路:借助
scss中的
max方法实现。
- 字体页面元素在缩小时,需要限制字体元素展现的最小阈值。 那么我们封装的方法,第三个形参需要控制字体元素的最小阈值;
实现思路:借助
scss中的
min方法实现。
- 在高于
1920px
屏幕的设备上,需要保持和1920px
屏幕的布局风格,即元素的宽高不变。 针对这个问题,我们只需要保证方法中的max
形参和1920px
下的像素值一致,即保证方法中的第一个形参和第二个形参相同。 - 在移动设备上,需要使用
800px
的网页布局。针对这个问题,我们可以使用meta
标签进行适配: - 不同屏幕下的元素显示势必不会那么完美。我们可以通过媒体查询,在不同分辨率的屏幕下,按照
UI
给定的反馈意见,对网页进行适配,这样就可以解决问题。但是在项目中大量使用媒体查询语法,会导致整个项目看上去很乱。为此,我们可以基于scss
语法,对媒体查询语法进行二次封装。 - 如何测试我们编写的
scss
代码? 移步sass在线调试
自适应scss
方法封装
// 自定义scss函数, 作用是去掉传入变量的单位
// 之所以要去掉单位,是为了将传入的px转换为vw单位,自适应布局`
@function stripUnits($value) {
// 对带有单位的变量进行特殊处理,返回去掉单位后的结果`
// 对于scss来说, 90px和90都是number`
// 在scss中,unitless是一个术语,指的是没有单位的数值,not unitless就是变量带单位`
@if type-of($value) == 'number' and not unitless($value) {
// 90px / 1 得到的结果是90px, 90px / 1px得到的结果是90
// 这也是这里为什么要用($value * 0 + 1),而不是直接写1的原因`
@return $value / ($value * 0 + 1);
}
@return $value;
}
/*
自定义scss函数,提供三个参数:
第一个参数是设计稿提供的元素大小,传入会自动转换为vw单位,达到自适应的效果
第二个参数是用来约束这个元素的大小最大不能超过第一个参数和第二个参数的最大值, 必须带单位
第三个参数是用来约束这个元素的大小最小不能小于第一个参数和第三个参数的最小值,必须带单位
如果不传入第二个和第三个参数,则表示元素完全随屏幕响应式缩放
应用场景:
1. 1920px下标题的字体是48px,当屏幕分辨率为960px时,标题字号缩放为24px,起不到突出的作用。
于是我们可以给它设置一个最小阈值,比如最小不能小于32px;
2. 同理,当屏幕分辨率为3840px时,标题字号放大为96px,我们不希望字号这么大。
于是可以给它设置一个最大阈值,比如最大不能超过60px。
*/
@function auto($raw, $max:null, $min:null) {
$raw: stripUnits($raw);
$str: #{$raw / $proportion}vw;
@if $max {
$str: min(#{$str}, #{$max});
}
@if $min {
$str: max(#{$str}, #{$min});
}
@return $str;
}
/*
自定义scss函数,auto方法的二次封装, 提供两个参数
第一个参数用于设置1920px下的元素大小
第二个参数用于设置这个元素的最小值
应用场景:
1. 1920px下标题的字体是48px,当屏幕分辨率为3840px时,标题字号放大为96px,我们希望它保持48px大小,
于是我们可以给它设置一个最大阈值48px。同时,我们可以传入一个最小阈值,让它最小不能小于这个参数。
*/
@function autoMax($raw, $min:null) {
@return auto($raw, $raw, $min)
}
// 和上面相反
@function autoMin($raw, $max:null) {
@return auto($raw, $max, $raw)
}
//1vw = 1920 / 100 ;
$proportion: 19.2;
// 根据UI需求,对不同字体大小进行封装
$wb-font-size-mini: 16px; // $text-mini-1
$wb-font-size-extra-small: 18px; // $text-small-1
$wb-font-size-small: 20px; //$text-sm-md-1
$wb-font-size-base: 24px; //$text-medium-1
$wb-font-size-lesser-medium: 32px;
$wb-font-size-medium: 36px; //$text-large-1
$wb-font-size-extra-medium: 44px;
$wb-font-size-large: 48px; //$text-title-1
// 根据UI需求,在屏幕分辨率缩小时,字体响应式变化,并设定最小阈值
// 并在1920px以上的屏幕,保持和1920px一样的字体大小
$wb-auto-font-size-mini: autoMax($wb-font-size-mini, 14px);
$wb-auto-font-size-extra-small: autoMax($wb-font-size-extra-small, 16px);
$wb-auto-font-size-small: autoMax($wb-font-size-small, 16px);
$wb-auto-font-size-base: autoMax($wb-font-size-base, 16px);
$wb-auto-font-size-lesser-medium: autoMax($wb-font-size-lesser-medium, 18px);
$wb-auto-font-size-medium: autoMax($wb-font-size-medium, 20px);
$wb-auto-font-size-extra-medium: autoMax($wb-font-size-extra-medium, 28px);
$wb-auto-font-size-large: autoMax($wb-font-size-large, 32px);
// 严格按照UI稿件提供的元素大小、间距编写代码,以下是示例代码
.title {
padding: 0 autoMax(180px);
font-size: $wb-auto-font-size-large;
font-weight: 600;
text-align: center;
}
媒体查询语法封装及使用规范
// 导入scss的list和map模块,用于处理相关操作。
@use 'sass:list';
@use "sass:map";
/*
媒体查询映射表,定义各种设备类型的媒体查询范围
key为定义的媒体类型,value为对应的分辨率范围
*/
$media-list: (
mobile-begin: (0, null),
mobile: (0, 800),
mobile-end:(null, 800),
tablet-begin: (801, null),
tablet: (801, 1023),
tablet-end:(null, 1023),
mini-desktop-begin: (1024, null),
mini-desktop: (1024, 1279),
mini-desktop-end: (null, 1279),
small-desktop-begin: (1280, null),
small-desktop: (1280, 1439),
small-desktop-end: (null, 1439),
medium-desktop-begin: (1440, 1919),
medium-desktop: (1440, 1919),
medium-desktop-end: (null, 1919),
large-desktop-begin: (1920, null),
large-desktop: (1920, 2559),
large-desktop-end: (null, 2559),
super-desktop-begin: (2560, null),
super-desktop: (2560, null),
super-desktop-end: (2560, null)
);
/*
创建响应式媒体查询的函数,传参是媒体查询映射表中的媒体类型
从$media-list中获取对应的最小和最大宽度,并返回相应的媒体查询字符串。
*/
@function createResponsive($media) {
$size-list: map.get($media-list, $media);
$min-size: list.nth($size-list, 1);
$max-size: list.nth($size-list, 2);
@if ($min-size and $max-size) {
@return "screen and (min-width:#{$min-size}px) and (max-width: #{$max-size}px)";
} @else if ($max-size) {
@return "screen and (max-width: #{$max-size}px)";
} @else {
@return "screen and (min-width:#{$min-size}px)";
}
}
/*
这个混入接受一个或多个媒体类型参数,调用createResponsive函数生成媒体查询
@content是Scss中的一个占位符,用于在混入中定义块级内容。
它允许你在调用混入时,将实际的样式代码插入到混入定义的样式规则中。
*/
@mixin responsive-to($media...) {
@each $item in $media {
$media-content: createResponsive($item);
@media #{$media-content} {
@content;
}
}
}
// 以下是针对各种媒体类型定义的混入:
@mixin mobile() {
@include responsive-to(mobile) {
@content;
}
}
@mixin tablet() {
@include responsive-to(tablet) {
@content;
}
}
@mixin mini-desktop() {
@include responsive-to(mini-desktop) {
@content;
}
}
@mixin small-desktop() {
@include responsive-to(small-desktop) {
@content;
}
}
@mixin medium-desktop() {
@include responsive-to(medium-desktop) {
@content;
}
}
@mixin large-desktop() {
@include responsive-to(large-desktop) {
@content;
}
}
@mixin super-desktop() {
@include responsive-to(super-desktop) {
@content;
}
}
@mixin mobile-begin() {
@include responsive-to(mobile-begin) {
@content;
}
}
@mixin tablet-begin() {
@include responsive-to(tablet-begin) {
@content;
}
}
@mixin mini-desktop-begin() {
@include responsive-to(mini-desktop-begin) {
@content;
}
}
@mixin small-desktop-begin() {
@include responsive-to(small-desktop-begin) {
@content;
}
}
@mixin medium-desktop-begin() {
@include responsive-to(medium-desktop-begin) {
@content;
}
}
@mixin large-desktop-begin() {
@include responsive-to(large-desktop-begin) {
@content;
}
}
@mixin super-desktop-begin() {
@include responsive-to(super-desktop-begin) {
@content;
}
}
@mixin mobile-end() {
@include responsive-to(mobile-end) {
@content;
}
}
@mixin tablet-end() {
@include responsive-to(tablet-end) {
@content;
}
}
@mixin mini-desktop-end() {
@include responsive-to(mini-desktop-end) {
@content;
}
}
@mixin small-desktop-end() {
@include responsive-to(small-desktop-end) {
@content;
}
}
@mixin medium-desktop-end() {
@include responsive-to(medium-desktop-end) {
@content;
}
}
@mixin large-desktop-end() {
@include responsive-to(large-desktop-end) {
@content;
}
}
@mixin super-desktop-end() {
@include responsive-to(super-desktop-begin) {
@content;
}
}
需求解决思路:
- 根据提供的设计稿,使用
autoMax
系列方法,对页面做初步的响应式布局适配 - 针对不同屏幕下部分元素布局需要调整的问题,使用封装的媒体查询方法进行处理
书写规范:
为避免项目中的scss
文件过多,搞得整个项目看上去很臃肿,现提供一套书写规范:
- 在每个路由下的主
index.vue
文件中,引入同级文件夹scss
下的media.scss
文件
// 小屏状态下,覆盖前面定义的css样式
media.css
文件
写法:以vue
文件最外层的类进行包裹,使用deep
穿透,以屏幕分辨率大小作为排序依据,从大到小书写媒体查询样式
.about-wrapper::v-deep {
@include small-desktop {
.a {
.b {
}
}
}
@include mini-desktop {
.a {
.b {
}
}
}
@include tablet-end {
.a {
.b {
}
}
}
}
结语
感谢掘友们耐心看到文末,希望你们不是一路跳转至评论区,我们江湖再见!
作者:沐浴在曙光下的贰货道士
来源:juejin.cn/post/7388753413309775887
来源:juejin.cn/post/7388753413309775887