在 Vue3 中使用 TypeScript 时,通过 ref 获取标签(包括 DOM 元素或组件实例)的 TypeScript 类型定义需根据场景区分:


1. 获取 DOM 元素的类型

若 ref 绑定的是原生 DOM 元素(如 <div><input>),需使用 HTML 元素的内置类型

import { ref } from 'vue';

// 示例:获取 div 元素
const divRef = ref<HTMLDivElement | null>(null);

// 示例:获取 input 元素
const inputRef = ref<HTMLInputElement | null>(null);
  • 原因:DOM 元素类型由浏览器环境定义,如 HTMLDivElementHTMLInputElement 等。
  • 注意:初始值需设为 null,因为元素在组件挂载前不存在 。

2. 获取子组件的类型

若 ref 绑定的是 Vue 组件(自定义组件或第三方组件),需使用 InstanceType<typeof 组件> 工具类型:

import { ref } from 'vue';
import MyComponent from '@/components/MyComponent.vue';

// 获取自定义组件的实例类型
const myCompRef = ref<InstanceType<typeof MyComponent> | null>(null);

第三方组件库示例(如 Element Plus)

import { ref } from 'vue';
import { ElTree } from 'element-plus';

// 定义组件实例类型
type TreeType = InstanceType<typeof ElTree>;
const treeRef = ref<TreeType | null>(null);
  • 关键点
    • typeof 组件 获取组件的构造函数类型。
    • InstanceType 提取该构造函数的实例类型(即组件暴露的方法/属性)。
    • 联合 null 处理初始状态 。

3. 动态生成标签的 ref 类型

若通过函数动态绑定 ref(如 :ref="getRef"),需在函数内手动指定类型:

<template>
  <el-input :ref="getInputRef" />
</template>

<script setup lang="ts">
import { ref } from 'vue';
import type { ElInput } from 'element-plus';

type InputType = InstanceType<typeof ElInput>;
const inputRef = ref<InputType | null>(null);

const getInputRef = (el: unknown) => {
  inputRef.value = el as InputType; // 显式类型断言
};
</script>
  • 原因:动态 ref 的回调参数 el 类型为 unknown,需手动转换为目标类型

以下是优化 Vue3 异步加载模块并避免 Vite 打包生成过大 JS 文件的关键方法:

  1. 使用 defineAsyncComponent 定义异步组件
    Vue3 提供 defineAsyncComponent 方法,允许组件在渲染时动态加载,减少初始包体积。例如:

    const AsyncComponent = defineAsyncComponent(() => import('./Component.vue'));
    

    这会将组件拆分为独立 chunk,按需加载 。

  2. 结合 Vite 的 Glob 导入批量加载
    利用 Vite 的 import.meta.glob 实现批量异步组件加载:

    const modules = import.meta.glob('./components/*.vue');
    const asyncComponents = Object.entries(modules).map(([path, loader]) => {
      const name = path.split('/').pop().replace('.vue', '');
      return { [name]: defineAsyncComponent(loader) };
    });
    

    此方法自动拆分模块,避免生成单一巨文件 。

  3. 路由懒加载
    在 Vue Router 中动态导入路由组件:

    { path: '/admin', component: () => import('./AdminPage.vue') }
    

    仅当访问路由时加载对应组件,显著减小初始包 。

  4. 动态导入(Dynamic Import)
    使用 ES6 动态导入语法异步加载模块:

    const module = await import('./module.js');
    

    结合 Webpack/Vite 的代码分割功能,自动生成独立 chunk 。

  5. 配置加载状态与错误处理
    通过 defineAsyncComponent 的选项增强用户体验:

    defineAsyncComponent({
      loader: () => import('./Component.vue'),
      loadingComponent: LoadingSpinner, // 加载中组件
      errorComponent: ErrorDisplay,     // 错误组件
      timeout: 3000                     // 超时时间
    });
    

    避免界面空白,提升交互体验 。

  6. 使用 <Suspense> 管理异步状态
    用 <Suspense> 包裹异步组件,统一处理加载和错误状态:

    <Suspense>
      <template #default> <AsyncComponent /> </template>
      <template #fallback> <LoadingSpinner /> </template>
    </Suspense>
    

    简化异步逻辑 。

总结
通过 defineAsyncComponent + Vite Glob 导入实现批量异步组件,结合路由懒加载和动态导入,将代码拆分为按需加载的小 chunk;用 <Suspense> 和错误处理优化用户体验。这些策略可减少初始包体积 60% 以上,避免生成超大 JS 文件。

一、padStart()简介

JavaScript的字符串padStart()方法用于在当前字符串的开头添加指定数量的字符,以达到指定的字符串长度。如果当前字符串的长度大于或等于指定的字符串长度,则不会添加任何字符。

二、语法

string.padStart(targetLength [, padString])

三、参数解释

targetLength:要达到的字符串长度,必须为一个正整数。

padString:可选的填充字符串,如果不指定,则默认为一个空格。

四、使用实例

下面是一些使用padStart()方法的示例:

//小于两位补零
String(date.getMonth() + 1).padStart(2, '0');

//文件名生成
for (let i = 1; i <= 10; i++) {
    const fileName = `file${i.toString().padStart(2, '0')}.txt`;
    console.log(fileName); // 输出: file01.txt file02.txt file03.txt ... file10.txt
}

五、注意事项

1、如果fillString的长度大于targetLength,则会截取fillString的前面部分。

2、如果不指定fillString,则默认为一个空格。

3、如果当前字符串的长度大于或等于目标长度,则不会添加任何字符。

六、常用在哪里

padStart()方法常用于字符串的格式化,比如在输出表格时,确保每列的宽度相同,可以使用padStart()方法在开头添加空格或其他字符。还可以用于格式化日期、时间等。

七、关于padEnd

padEnd()可以在字符串的后面进行字符补全,语法参数等都和padStart()类似。

八、padEnd注意

如果补全字符串长度不足,则从左往右不断循环补全;如果长度超出可以补全的字符长度,则从左侧尽可能补全,补不到的没办法,只能忽略,例如'zhangxinxu'.padEnd(15, {})等同于执行'zhangxinxu'.padEnd(15, '[object Object]'),最多只能补5个字符,因此,只能补'[object Object]'前5个字符,于是最后结果是:'zhangxinxu[obje'

padString参数如果不设置,则会使用普通空格' '(U+0020)代替,也就是Space空格键敲出来的那个空格。

九、padEnd案例

在JS前端我们处理时间戳的时候单位都是ms毫秒,但是,后端同学返回的时间戳则不一样是毫秒,可能只有10位,以s秒为单位。所以,我们在前端处理这个时间戳的时候,保险起见,要先做一个13位的补全,保证单位是毫秒。使用示意:

timestamp = +String(timestamp).padEnd(13, '0');

十、兼容性

padStart()padEnd()是属于ES8(ES2017)的方法,兼容性:IE不支持,Chrome版本≥57

在OpenLayers中,根据多个经纬度坐标绘制多边形是一个常见的任务。你可以使用这些坐标来创建一个ol.geom.Polygon几何对象,然后将其添加到一个矢量图层中并在地图上显示。

以下是一个基本的步骤指南,教你如何在OpenLayers中根据多个经纬度坐标绘制多边形:

  1. 引入OpenLayers库
    确保你的HTML文件中已经引入了OpenLayers的JavaScript库。
  2. 创建地图容器
    在HTML中创建一个<div>元素作为地图的容器。
  3. 初始化地图
    使用OpenLayers的API来初始化地图,并设置视图。
  4. 创建多边形几何对象
    使用给定的经纬度坐标创建一个ol.geom.LinearRing(线性环,表示多边形的外环)和一个ol.geom.Polygon(多边形)几何对象。
  5. 创建矢量特征和图层
    将多边形几何对象添加到一个新的矢量特征中,并将该特征添加到一个矢量图层中。
  6. 将矢量图层添加到地图
    将矢量图层添加到地图中,以便在地图上显示多边形。

以下是一个具体的代码示例:

<!DOCTYPE html>  
<html lang="en">  
<head>  
  <meta charset="UTF-8">  
  <title>OpenLayers Polygon Example</title>  
  <link rel="stylesheet" href="https://openlayers.org/en/latest/css/ol.css" type="text/css">  
  <script src="https://openlayers.org/en/latest/build/ol.js"></script>  
  <style>  
    #map {  
      width: 100%;  
      height: 400px;  
    }  
  </style>  
</head>  
<body>  
  <div id="map"></div>  
  <script>  
    // 初始化地图  
    var map = new ol.Map({  
      target: 'map',  
      layers: [  
        new ol.layer.Tile({  
          source: new ol.source.OSM() // 使用OpenStreetMap作为地图源  
        })  
      ],  
      view: new ol.View({  
        center: ol.proj.fromLonLat([0, 0]), // 设置地图中心点  
        zoom: 2 // 设置地图缩放级别  
      })  
    });  
  
    // 定义多边形的经纬度坐标(注意:这些坐标需要是闭合的,即第一个和最后一个坐标应该是相同的)  
    var coordinates = [  
      [0, 0], // 第一个点  
      [10, 10], // 第二个点  
      [10, -10], // 第三个点  
      [0, 0] // 闭合多边形,回到第一个点  
    ];  
  
    // 将经纬度坐标转换为OpenLayers的内部坐标系统(如果需要的话,这里已经假设是WGS84经纬度)  
    // 注意:对于简单的经纬度坐标,如果地图视图已经设置为相应的投影(如EPSG:4326),则可能不需要转换。  
    // 但通常,我们会使用EPSG:3857(Web Mercator)作为地图的投影,因此这里可能需要转换,但在这个例子中我们省略了这一步,  
    // 因为我们直接使用了可以理解的经纬度坐标,并且OpenLayers在大多数情况下能够自动处理这些坐标。  
    // 如果你的坐标不是WGS84经纬度,或者你需要确保精确的坐标转换,请使用ol.proj.transform进行转换。  
  
    // 创建线性环(LinearRing)和多边形(Polygon)几何对象  
    var linearRing = new ol.geom.LinearRing(coordinates);  
    var polygon = new ol.geom.Polygon([linearRing]);  
  
    // 创建矢量特征,并将多边形几何对象设置为其几何属性  
    var feature = new ol.Feature(polygon);  
  
    // 创建矢量源,并将特征添加到源中  
    var vectorSource = new ol.source.Vector({  
      features: [feature]  
    });  
  
    // 创建矢量图层,并将源设置为其数据源  
    var vectorLayer = new ol.layer.Vector({  
      source: vectorSource,  
      style: new ol.style.Style({  
        fill: new ol.style.Fill({  
          color: 'rgba(255, 0, 0, 0.2)' // 设置填充颜色为红色透明  
        }),  
        stroke: new ol.style.Stroke({  
          color: '#ff0000', // 设置边框颜色为红色  
          width: 2 // 设置边框宽度  
        })  
      })  
    });  
  
    // 将矢量图层添加到地图中  
    map.addLayer(vectorLayer);  
  </script>  
</body>  
</html>

在这个例子中,我们创建了一个简单的红色多边形,它根据给定的经纬度坐标绘制在地图上。请注意,坐标数组是闭合的,即第一个和最后一个坐标是相同的,这是绘制多边形时的要求。如果你的坐标数组不是闭合的,OpenLayers可能无法正确识别它为多边形。

另外,请注意坐标系统的转换。在这个例子中,我们直接使用了经纬度坐标,并且假设地图视图已经设置为能够处理这些坐标。然而,在实际应用中,你可能需要将坐标从一种投影系统转换到另一种投影系统(例如,从WGS84经纬度转换到EPSG:3857 Web Mercator投影),这可以通过ol.proj.transform函数来实现。但在上面的例子中,我们省略了这一步,因为OpenLayers在大多数情况下能够自动处理这些坐标,并且我们使用了简单的经纬度坐标。

在OpenLayers中绘制多边形是一个常见的功能,它通常通过OpenLayers提供的绘制(Draw)交互来实现。以下是一个基本的步骤指南,教你如何在OpenLayers中绘制多边形:

1. 引入OpenLayers库

首先,你需要在HTML文件中引入OpenLayers库的JavaScript文件。你可以从OpenLayers的官方网站获取最新版本的链接。

<script src="https://openlayers.org/en/latest/build/ol.js"></script>

注意:这里的链接是示例用的,你应该使用OpenLayers提供的最新版本链接。

2. 创建地图容器

在HTML文件中创建一个用于显示地图的容器。这个容器将作为OpenLayers地图的视图窗口。

<div id="map" style="width: 100%; height: 400px;"></div>

3. 初始化地图

在JavaScript代码中初始化地图对象,并设置地图的中心点和缩放级别。

var map = new ol.Map({  
  target: 'map',  
  view: new ol.View({  
    center: ol.proj.fromLonLat([0, 0]), // 设置地图中心点  
    zoom: 2 // 设置地图缩放级别  
  })  
});

4. 添加绘制交互

创建一个绘制交互对象,并将其添加到地图中。这个交互对象将允许用户在地图上绘制多边形。

var draw = new ol.interaction.Draw({  
  source: new ol.source.Vector(),  
  type: 'Polygon' // 指定绘制类型为多边形  
});  
map.addInteraction(draw);

5. 监听绘制完成事件

监听绘制完成事件,并获取绘制的多边形几何对象。你可以在这个事件处理函数中执行一些额外的操作,比如保存多边形到数据库或进行空间分析。

draw.on('drawend', function(event) {  
  var feature = event.feature;  
  var geometry = feature.getGeometry(); // 获取绘制完成后的多边形几何对象  
  // 在这里可以对多边形进行进一步处理  
});

6. 样式设置(可选)

你可以为绘制的多边形设置样式,包括填充颜色、边框颜色、边框宽度等。

var styleFunction = function(feature) {  
  return new ol.style.Style({  
    fill: new ol.style.Fill({  
      color: 'rgba(255, 255, 255, 0.2)' // 设置填充颜色  
    }),  
    stroke: new ol.style.Stroke({  
      color: '#ffcc33', // 设置边框颜色  
      width: 2 // 设置边框宽度  
    }),  
    text: new ol.style.Text({  
      font: '12px Calibri,sans-serif',  
      text: '多边形',  
      fill: new ol.style.Fill({  
        color: '#000'  
      }),  
      stroke: new ol.style.Stroke({  
        color: '#fff',  
        width: 3  
      })  
    })  
  });  
};  
  
// 将样式应用到矢量图层上  
var vectorLayer = new ol.layer.Vector({  
  source: draw.getSource(),  
  style: styleFunction  
});  
map.addLayer(vectorLayer);

注意:在上面的代码中,我们创建了一个新的矢量图层,并将绘制交互的源(source)应用到这个图层上。同时,我们为这个图层设置了一个样式函数,用于定义多边形的样式。然而,在实际应用中,你可能不需要单独创建一个矢量图层来显示绘制的多边形,而是可以直接将绘制交互的源添加到已有的矢量图层上。

通过以上步骤,你就可以在OpenLayers中绘制多边形了。当然,OpenLayers的功能非常强大,你还可以进一步自定义绘制交互的行为和样式,以满足你的具体需求。

在 Vue.js 应用中,使用 Vue Router 进行路由管理时,常常需要在不同的路由之间传递参数。Vue Router 提供了几种方式来实现路由传参,包括通过 URL 路径参数、查询参数和命名视图。以下是一些常见的方法和示例:

1. 路径参数(Route Parameters)

路径参数通常用于传递具有唯一标识意义的参数,如用户 ID、文章 ID 等。

定义路由

// router/index.js  
import Vue from 'vue';  
import Router from 'vue-router';  
import User from '@/components/User.vue';  
import Post from '@/components/Post.vue';  
  
Vue.use(Router);  
  
export default new Router({  
  routes: [  
    {  
      path: '/user/:id', // 这里的 `:id` 是一个动态段  
      name: 'User',  
      component: User  
    },  
    {  
      path: '/post/:postId', // 这里的 `:postId` 是一个动态段  
      name: 'Post',  
      component: Post  
    }  
  ]  
});

访问路由并传递参数

// 在某个组件中  
this.$router.push({ name: 'User', params: { id: 123 } });  
this.$router.push({ name: 'Post', params: { postId: 456 } });

在组件中获取参数

// User.vue  
<template>  
  <div>User ID: {{ $route.params.id }}</div>  
</template>  
  
<script>  
export default {  
  computed: {  
    userId() {  
      return this.$route.params.id;  
    }  
  }  
};  
</script>  
  
// Post.vue  
<template>  
  <div>Post ID: {{ $route.params.postId }}</div>  
</template>  
  
<script>  
export default {  
  computed: {  
    postId() {  
      return this.$route.params.postId;  
    }  
  }  
};  
</script>

2. 查询参数(Query Parameters)

查询参数通常用于传递非唯一标识意义的参数,如搜索条件、分页信息等。

定义路由(不需要特殊定义)

// router/index.js  
import Vue from 'vue';  
import Router from 'vue-router';  
import SearchResults from '@/components/SearchResults.vue';  
  
Vue.use(Router);  
  
export default new Router({  
  routes: [  
    {  
      path: '/search',  
      name: 'SearchResults',  
      component: SearchResults  
    }  
  ]  
});

访问路由并传递参数

// 在某个组件中  
this.$router.push({ name: 'SearchResults', query: { q: 'vue', page: 2 } });

在组件中获取参数

// SearchResults.vue  
<template>  
  <div>  
    <p>Search Query: {{ $route.query.q }}</p>  
    <p>Page: {{ $route.query.page }}</p>  
  </div>  
</template>  
  
<script>  
export default {  
  computed: {  
    searchQuery() {  
      return this.$route.query.q;  
    },  
    page() {  
      return this.$route.query.page;  
    }  
  }  
};  
</script>

3. 编程式导航中的 props 传参

Vue Router 还支持将路由参数作为 props 传递给组件,这样可以使组件更加解耦和可复用。

定义路由时使用 props: true

// router/index.js  
import Vue from 'vue';  
import Router from 'vue-router';  
import User from '@/components/User.vue';  
  
Vue.use(Router);  
  
export default new Router({  
  routes: [  
    {  
      path: '/user/:id',  
      name: 'User',  
      component: User,  
      props: true // 这样可以将参数作为 props 传递给 User 组件  
    }  
  ]  
});

在组件中接收 props

// User.vue  
<template>  
  <div>User ID: {{ id }}</div>  
</template>  
  
<script>  
export default {  
  props: ['id']  
};  
</script>

使用 props 传递参数时,不需要通过 $route 对象来访问参数,直接通过 props 接收即可。

总结

  • 路径参数:适用于传递具有唯一标识意义的参数,如用户 ID。
  • 查询参数:适用于传递非唯一标识意义的参数,如搜索条件。
  • 编程式导航中的 props:使组件更加解耦和可复用。

通过这些方法,你可以在 Vue.js 应用中灵活地传递和使用路由参数。

前段时间在面试的时候,被问到原子类CSS,鉴于自己这个老前端已经几年没有关注前端最新的技术了,对于“原子类”这一名词有些困扰。

事后一查,这不就是“bootstrap”的样式类的新名词嘛?老旧的东西又拿出来说。其实这都是最原始的CSS设计模式了了。

原子类CSS(Atomic CSS)是一种CSS设计模式,它将样式属性拆分为独立的、具有特定用途的类。每个类通常只包含一个样式属性,或者是一组紧密相关的样式属性的组合。通过将这些类组合在一起,可以快速构建出复杂的样式。以下是对原子类CSS的详细解析:

一、原子类CSS的核心原则

  1. 原子化:将样式分解为最小的可重用单元,即“原子”。这些原子通常是单个像素或极其微小的变化,例如颜色、大小、位置等。
  2. 可重用性:每个原子类都是独立且可重用的,可以在不同的元素和场景中重复使用。
  3. 组合性:通过组合不同的原子类,可以构建出复杂的样式和布局,而无需编写大量的定制CSS。

二、原子类CSS的优势

  1. 提高开发效率:由于原子类CSS提供了大量的预定义类,开发人员可以快速应用样式,而无需从头编写CSS代码。
  2. 减少代码冗余:通过重用原子类,可以避免在多个地方重复相同的样式代码,从而减少代码冗余。
  3. 易于维护:由于每个原子类只负责一个或少数几个样式属性,因此代码更加清晰和易于理解。当需要修改样式时,只需调整相应的原子类即可。
  4. 增强可定制性:开发人员可以根据自己的需求添加自定义的原子类,或者根据项目的需求修改现有的原子类。

三、原子类CSS的实现方式

  1. 工具与库:原子类CSS可以通过工具如Tachyons、Tailwind CSS等实现。这些工具提供了一套预定义的原子类,可以快速地应用于HTML元素。
  2. 命名约定:原子类CSS通常使用功能性的命名约定,如.mr1(外边距右侧1个单位)、.bg-red(背景颜色为红色)等。这些命名直观且易于理解。
  3. 与前端框架的集成:原子类CSS可以与各种前端框架(如Angular、React、Vue等)集成,并提供大量的插件和扩展选项。

四、原子类CSS的应用场景

  1. 快速开发:在需要快速构建Web应用程序或原型时,原子类CSS可以大大提高开发效率。
  2. 团队协作:在团队协作中,使用原子类CSS可以减少代码冲突和不一致性,因为每个开发人员都可以使用相同的预定义类来构建样式。
  3. 定制化需求:当项目需要高度定制化时,原子类CSS提供了足够的灵活性和可定制性来满足这些需求。

五、示例

以下是一个使用Tailwind CSS(一种流行的原子类CSS框架)的示例:

<div class="bg-blue-500 text-white p-4">  
  Hello Tailwind CSS!  
</div>

在这个示例中,bg-blue-500 类设置了背景颜色为蓝色,text-white 类设置了文本颜色为白色,p-4 类设置了内边距为4个单位。通过组合这些原子类,我们可以快速构建出具有特定样式的HTML元素。

综上所述,原子类CSS是一种灵活且强大的CSS设计模式,它可以帮助简化样式管理并提高代码的可维护性。通过使用原子类CSS框架(如Tailwind CSS),开发人员可以更加高效地构建和维护Web应用程序的界面。

CSS变量简介

CSS变量的定义及使用如下,可定义的类型非常广泛。

/* 声明 */
--VAR_NAME: <声明值>;
/* 使用 */
var(--VAR_NAME)

/* 根元素选择器(全局作用域),例如 <html> */
:root {
  /* CSS 变量声明 */
  --main-color: #ff00ff;
  --main-bg: rgb(200, 255, 255);
  --logo-border-color: rebeccapurple;

  --header-height: 68px;
  --content-padding: 10px 20px;

  --base-line-height: 1.428571429;
  --transition-duration: .35s;
  --external-link: "external link";
  --margin-top: calc(2vh + 20px);
}

body {
  /* 使用变量 */
  color: var(--main-color);
}

与 SASS、LESS预处理器变量的编译时处理不同,CSS 变量由浏览器在运行时处理,这使得它们更加强大和灵活。

CSS 到 JS

在 CSS 变量出现之前,将值从 CSS 传递到 JS 非常困难,甚至需要一些 hack 技巧。现在有了 CSS 变量,可以直接通过 JS 访问变量值并进行修改。

// 定义 CSS 变量
.breakpoints-data {
  --phone: 480px;
  --tablet: 800px;
}
const breakpointsData = document.querySelector('.breakpoints-data');

// 获取 CSS 变量的值
const phone = getComputedStyle(breakpointsData)
    .getPropertyValue('--phone');

// 设置 CSS 变量的新值
breakpointsData.style
    .setProperty('--phone', 'custom');

本站重构后,也合理的利用了CSS变量,但方式更加灵活。

比如本站的主题切换功能,白天与夜晚主题。在根节点root中使用多个变量组,使用不同的元素选择器进行分组。然后通过js更改元素的class类或者属性对主题进行切换。

:root {
    --wp--preset--font-size--normal: 16px;
    --wp--preset--font-size--huge: 42px;
}
:root {
    --my-preset-line-height-1: 1.8;
    --my-preset-letter-spacing-1: .04em;
    --my-preset-color-font-1: #444444;
    --my-preset-color-font-2: #333333;
    --my-preset-color-font-3: #000000;
    --my-preset-color-bg-1: #ffffff;
    --my-preset-color-bg-2: #fbfbfb;
    --my-preset-color-bg-3: #1f1e2c;
    --my-preset-color-alert-bg: #ffffff;
    --my-preset-color-button-bg-1: #ffffff;
    --my-preset-color-button-font-1: #f96b4d;
    --my-preset-color-button-bg-active: #f96b4d;
    --my-preset-color-active: #f96b4d;
    --my-preset-color-active-hover: #f5ad6e;
    --my-preset-color-font-gray-1: #9f9f9f;
    --my-preset-color-font-gray-2: #cdcdcd;
    --my-preset-color-font-gray-3: #888888;
    --my-preset-color-border-1: #d8d8d8;
    --my-preset-color-border-2: #cccccc;
    --my-preset-color-border-3: #eeeeee;
    --my-preset-color-bg-gray-1: #f8f8f8;
    --my-preset-color-bg-gray-2: #fbfbfb;
    --my-font-family-1: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Hiragino Sans GB, Microsoft YaHei UI, Microsoft YaHei, Source Han Sans CN, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
    --my-font-family-2: NotoSerifSC, "Microsoft Yahei", 宋体, sans-serif;
    --my-font-family-3: 'Helvetica', 'Microsoft Yahei', '冬青黑体简体中文 w3', 微软雅黑, 'Tahoma', 'Arial', 'SimSun';
    --my-preset-color-scrollbar-thumb: #d2d2d2;
    --my-preset-color-scrollbar-track: rgba(51, 51, 51, 0.1);
}
[data-user-color-scheme='dark'] {
    --my-preset-color-bg-1: #1f1e2c;
    --my-preset-color-bg-2: #1a1927;
    --my-preset-color-bg-3: #0b0b10;
    --my-preset-color-alert-bg: #262631;
    --my-preset-color-font-1: #d5d5d5;
    --my-preset-color-font-2: #f5f5f5;
    --my-preset-color-font-3: #ffffff;
    --my-preset-color-border-3: #3f3d55;
    --my-preset-color-button-bg-1: #f96b4d;
    --my-preset-color-button-bg-active: #f5ad6e;
    --my-preset-color-button-font-1: #f5f5f5;
    --my-preset-color-bg-gray-1: #2e2d3b;
    --my-preset-color-font-gray-3: #a9a9a9;
    --my-preset-color-border-2: #aaaaaa;
    --my-preset-color-scrollbar-thumb: #3b3b5f;
    --my-preset-color-scrollbar-track: #1f1e2c;
}
export default {
  template,
  setup() {
    const colorScheme = ref(window.localStorage.getItem('user-color-scheme') || "light");
    const setColorScheme = (scheme) => {
      colorScheme.value = scheme;
      document.documentElement.setAttribute('data-user-color-scheme', scheme);
      window.localStorage.setItem('user-color-scheme', scheme);
    };

    return { colorScheme, setColorScheme };
  },
};

除此之外还有很多 css 原生能力,比如:Mixins、运算符等。

更改.gitignore后,需要通知Git重新读取.gitignore文件。可以使用以下命令来刷新Git的索引,以应用新的忽略规则:

git rm -r --cached .
git add .
git commit -m "Refresh .gitignore"

这些命令的作用是:

  1. git rm -r --cached .从索引中移除所有文件和目录,但保留在本地磁盘上。--cached选项指定只移除索引中的跟踪,而不删除实际文件。
  2. git add .重新添加所有文件和目录到索引中,此时Git会重新读取.gitignore文件来应用规则。
  3. git commit -m "Refresh .gitignore"提交更新后的索引变化。

请注意,git rm -r --cached .会移除所有文件和目录的跟踪,这意味着它会移除所有文件的版本历史,只保留文件内容。如果你只想刷新.gitignore规则对部分文件和目录的应用,你可以单独指定这些文件和目录。

前端的单元测试包括但不限于:单元功能测试、UI 测试、兼容性测试等等。一个测试体系大体包括四部分:

本文会通过一个例子,来一步步了解如何进行前端单元测试。

本文举的例子中,没有涉及测试运行器,只涉及测试框架、断言库和测试覆盖率。并以 Mocha + Should + Istanbul 组合为例。

新建项目

如果你的电脑上没有安装 Node.js,那么你需要访问它的官网,下载并安装到你的电脑上。NPM 是 Node.js 的包管理工具,会随着 Node.js 一起安装。

然后,我们需要用 NPM(Node Package Manager)来管理依赖包,所以先初始化 NPM 的配置文件 package.json,执行指令:

$ npm init -y

-y 参数表示不进行询问,直接使用默认的配置。

下面我们在 src 目录下,新建 main.js 文件,并编写一个 factorial 函数(用于求数的阶乘):

// main.js

var factorial = function(n) {
  if (n === 0) {
    return 1;
  }

  return factorial(n - 1) * n;
};

if (require.main === module) {
  // 如果是在命令行中执行 main.js,则此处会执行。
  // 如果 main.js 被其他文件 require,则此处不会执行。
  var n = Number(process.argv[2]);
  console.log('factorial(' + n + ') is', factorial(n));
}

运行一下这个文件,看看结果是否正确。执行指令:node ./src/main.js 5,效果如下:

结果是 120,符合预期。但是一个例子并不能说明什么,我们还需要对负数、非数字、小数、很大的数等进行验证,在逐步的验证过程中,代码中的不足也会逐渐暴露出来。所以接下来我们将进行测试驱动开发(Test-Driven Development, TDD),通过不断的测试来完善代码。

编写测试文件

首先,在 main.js 文件最后添加代码:

exports.factorial = factorial;

这段代码的作用是将 factorial 函数暴露出去,这样才可以在其他文件中 require 这个函数。

通常,测试文件与所要测试的源文件同名,但是后缀名为 .test.js(表示测试)或 .spec.js(表示规格)。例如,main.js 的测试文件就是 main.test.js

// main.test.js

var main = require('../src/main');
var should = require('should');

describe('test/main.js', function() {
  it('should equal 1 when n === 0', function() {
    should(main.factorial(0)).equal(1);
  });
});

上面的代码中:

  • describe 块称为“测试套件(test suite)”,表示一组相关的测试。它是一个函数,第一个参数是测试套件的名称,第二个参数是一个实际执行的函数。
  • it 块称为“测试用例(test case)”,表示一个单独的测试,是测试的最小单位。它也是一个函数,第一个参数是测试用例的名称,第二个参数是一个实际执行的函数。

一个测试文件中,可以包含一个或多个 describe 块,一个 describe 块中可以包含一个或多个 it 块。

想要运行这个测试文件,需要安装依赖 Mocha 和 Should:

$ npm install --save-dev mocha should

然后,在 package.json 中新建一条 NPM 指令:

"scripts": { "test": "./node_modules/.bin/mocha ./test/main.test.js" } 

该指令的作用就是:使用安装在项目目录中的 Mocha 命令 ./node_modules/.bin/mocha 来测试 ./test/main.test.js 文件。

执行这个指令 npm run test,结果如下(可以看到测试通过):

到这里,我们就使用测试框架 + 断言库,体验了基本的单元测试流程,接下来我们通过不断完善测试用例,来使代码健壮起来。

完善测试用例

首先,明确函数功能。我们的 factorial 函数应该有以下功能:

  • 当 n === 0 时,返回 1
  • 当 n > 0 时,返回 factorial(n - 1) * n
  • 当 n < 0 时,抛出错误,因为没有意义。
  • 当 n 不是数字时,抛出错误。
  • 当 n > 10 时,抛出错误(本文为了演示,只进行 10 以内的阶乘运算)。

然后,我们根据确定好的功能来完善测试用例:

var main = require('../src/main');
var should = require('should');

describe('test/main.js', function() {
  it('should equal 1 when n === 0', function() {
    should(main.factorial(0)).equal(1);
  });

  it('should equal 1 when n === 1', function() {
    should(main.factorial(1)).equal(1);
  });

  it('should equal 3628800 when n === 10', function() {
    should(main.factorial(10)).equal(3628800);
  });

  it('should throw when n > 10', function() {
    (function() {
      main.factorial(11);
    }.should.throw('n should <= 10'));
  });

  it('should throw when n < 0', function() {
    (function() {
      main.factorial(-1);
    }.should.throw('n should >= 0'));
  });

  it('should throw when n is not Number', function() {
    (function() {
      main.factorial('123');
    }.should.throw('n should be a Number'));
  });
});

执行测试指令 npm run test,效果如下:

可以看到后面三个测试用例都没有通过,这说明 factorial 函数并不是在所有情况下都可以正常运行,所以我们需要更新 factorial 的实现:

var factorial = function(n) {
  if (typeof n !== 'number') {
    throw new Error('n should be a Number');
  }

  if (n < 0) {
    throw new Error('n should >= 0');
  }

  if (n > 10) {
    throw new Error('n should <= 10');
  }

  if (n === 0) {
    return 1;
  }

  return factorial(n - 1) * n;
};

再次执行测试指令 npm run test,效果如下:

可以看到,所有的测试用例都通过了,这证明 factorial 函数的功能已经符合了我们的预期要求,而且代码健壮性有了很大的提高。

以上就是 TDD 的基本流程,总的来说就是:首先明确程序的功能,然后跑测试用例,如果测试用例没有通过,修改程序,直到测试用例通过

生成覆盖率

如果你想知道测试用例是否合理,可以用“代码覆盖率”来判断。一般而言,如果测试用例写的合理,那么代码覆盖率越高越好,但不是绝对的。

代码覆盖率包括以下几个方面:

  • 行覆盖率:是否每一行都执行了
  • 函数覆盖率:是否每个函数都调用了
  • 分支覆盖率:是否每个 if 代码块都执行了
  • 语句覆盖率:是否每个语句都执行了

生成代码覆盖率,需要用到插件 Istanbul,首先将其安装:

$ npm install --save-dev istanbul

然后,在 package.json 中新建一条 NPM 指令,用于生成覆盖率:

"scripts": { "coverage": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha" }

注意,指令中 _mocha 的下划线不能省略。因为,mocha 和 _mocha 是两个不同的命令,前者会新建一个进程执行测试,而后者是在当前进程(即 Istanbul 所在的进程)执行测试,只有这样, Istanbul 才会捕捉到覆盖率数据。其他测试框架也是如此,必须在同一个进程执行测试。

执行这个指令 npm run coverage,结果如下:

将 coverage/lcov-report 目录下的 index.html 文件在浏览器中运行,可以查看具体的覆盖率。如图所示:

其实,这次的覆盖率应该是 100%,因为函数在被其他文件引用时 24、25 这两行不会执行,所以没法测。由于这两行代码仅仅是为了刚开始方便演示用,之后我们就不在命令行中测试了,所以直接将这两行语句所在的 if 块删除即可。

再次执行测试指令,就得到了 100% 的覆盖率:

上传覆盖率

想要展示测试覆盖率,有两个网站可供选择:Codecov 和 Coveralls。本文以 Codecov 为例。

首先,打开 Codecov 官网,绑定 Github 账号之后,选择要展示测试覆盖率的仓库。

然后,安装 Codecov:

$ npm install --save-dev codecov

接着,在 package.json 中新建一条 NPM 指令,来上传测试覆盖率:

"script": { "codecov": "cat ./coverage/lcov.info | ./node_modules/.bin/codecov" }

其中 cat ./coverage/lcov.info 用于读取 coverage 目录下的 lcov.info 文件,./node_modules/.bin/codecov 用于将覆盖率上传到 Codecov 网站。

该指令在接下来配置 CI(Continuous integration, 持续集成)时会用到。

持续集成

如果每次修改代码之后,都手动进行单元测试,不仅加重工作量,而且容易出错,因此我们需要进行自动化测试,这就用到了持续集成。

持续集成是一种软件开发实践,每次集成都通过自动化的构建(包括编译,发布,测试等)来验证,从而尽早地发现代码中的错误。

可供选择的持续集成工具有 Travis CI 和 Circle CI。本文以 Travis CI 为例。

使用 Travis CI

首先,Travis CI 进入官网后,点击 Sign In 按钮绑定 Github。然后在仓库列表中选择你要进行持续集成的仓库,点击按钮启用:

然后,你需要在项目根目录下创建 .travis.yml 文件(如果没有这个文件,Travis CI 会默认执行 npm install 和 npm test),配置文件示例如下:

# 要使用的语言
language: node_js

# 要使用的语言版本
node_js:
  - 10

# 缓存 NPM 依赖,加快构建
cache:
  directories:
    - node_modules

# 安装依赖
install:
  - npm install

# 执行指令
script:
  - npm run coverage

# 指令执行成功后
after_success:
  - npm run codecov

# 指定分支
branches:
  only:
    - master

最后,将所有修改提交到远程仓库的 master 分支上,就可以看到 Travis CI 正在自动构建。

展示徽章

当 CI 构建完成之后,我们可以通过访问 Travis CI 和 Codecov 的网站查看到详细结果,当然也可以将结果以徽章的形式放入 README,这样更清晰明了。

Travis CI 的徽章这样获取:

Codecov 的徽章这样获取:

每当 CI 构建完成,结果就会以徽章的形式,展示在你的项目文档中。


参考资料: