企业级项目下css处理痛点
针对上述问题,我们可以通过 tailwindcss 来进行解决。下面我们来看其具体用法。
安装
yarn add tailwindcss postcss autoprefixer -D
初始化tailwindcss.config.js配置文件,并且也会创建postcss.config.js文件。
yarn add tailwindcss postcss autoprefixer -D
// tailwindcss.config.js
/** @type {import('tailwindcss').Config} */
export default {
// tailwindcss 应用的文件范围
content: ['./index.html', './src/**/*.{vue,js}'],
theme: {
extend: {},
},
plugins: [],
}
将加载 Tailwind 的指令添加到你的 CSS 文件中
@tailwind base;
@tailwind components;
@tailwind utilities;
在元素上使用内置类名
tailwindcss 官方介绍为无需离开HTML即可快速构建现代网站。具体来说就是tailwind提供了很多类名,都定义了特定的css,直接在编写HTML的时候加上对应的类名即可快速搭建网站。
首先我们先来看下css颗粒度设计形式
zh-llm
zh-llm
对比四种设计方式,可以看出原子化css是自由度,可定制化,复用性都挺好,只有编写大量无意义类名缺点,对比他的优点,缺点也是可以忽略的。但是对于维护项目的人来说,如果不了解tailwindcss中定义的类名,那可能是非常头疼的一件事了。
对于高个性化,高交互性,高定制化前台项目样式解决方案,还是原子化css形式更合适。
在使用vscode开发时,我们可以安装一个Tailwind CSS IntelliSense插件,提示类名,来帮助我们更好的开发。
案例代码
VueUse, 基于Vue组合式API的实用工具集。
useWindowSize api,响应式的获取窗口尺寸。当窗口尺寸发生变化时,实时获取。来判断是移动端UI还是pc端UI。
import { computed } from 'vue'
import { PC_DEVICE_WIDTH } from '../constants'
import { useWindowSize } from '@vueuse/core'
const { width } = useWindowSize()
/**
* 是否是移动端设备; 判断依据: 屏幕宽度小于 PC_DEVICE_WIDTH
* @returns
*/
export const isMobileTerminal = computed(() => {
return width.value < PC_DEVICE_WIDTH
})
案例代码
在webpack开发时构建时,默认会抓取并构建你的整个应用,然后才能提供服务,这就导致了你的项目中存在任何一个错误(即使当前错误不是首页引用的模块),他依然会影响到你的整个项目构建。所以你的项目越大,构建时间越长,项目启动速度也就越慢。
vite不会在一开始就构建你的整个项目,而是会将引用中的模块区分为依赖和源码(项目代码)两部分,对于源码部分,他会根据路由来拆分代码模块,只会去构建一开始就必须要构建的内容。
同时vite以原生 ESM的方式为浏览器提供源码,让浏览器接管了打包的部分工作。
当源码中有commonjs模块加载,那么将会出现模块加载失败的问题。通过依赖与构建的方式解决该问题。
例如axios库就有相关的issue
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import {join} from "path"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
"@": join(__dirname, "/src")
}
}
})
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import {join} from "path"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
"@": join(__dirname, "/src")
}
},
server: {
proxy: {
// 代理所有 /api请求
"/api": {
target: "目标origin",
// 改变请求的origin为target的值
changeOrigin: true,
}
}
}
})
企业级项目,都会区分很多环境,供我们测试试用。不能让我们的测试数据去污染线上的数据。所以vite也提供了我们环境配置文件的方式,让我们很轻松的去通过一些环境选择对应的接口地址等等。
.env.[mode]的格式可以在不同模加载加载不同的内容。
环境加载优先级
一份用于指定模式的文件(例如 .env.production)会比通用形式的优先级更高(例如 .env)。
另外,Vite 执行时已经存在的环境变量有最高的优先级,不会被 .env 类文件覆盖。例如当运行 VITE_SOME_KEY=123 vite build 的时候。
.env 类文件会在 Vite 启动一开始时被加载,而改动会在重启服务器后生效。
我们可以在源码中通过import.meta.env.*的方式获取以VITE_开头的已加载的环境变量。
// .env.development
VITE_BASE_API = "/api"
// package.json
"scripts": {
"dev": "VITE_BASE_API=/oop vite",
}
执行yarn dev后,我们可以发现,
import.meta.env.VITE_BASE_API是命令行中指定的参数。
vite的Glob 导入功能:该功能可以帮助我们在文件系统中导入多个模块
const modules = import.meta.glob('./dir/*.js')
// 以上将会被转译为下面的样子:
const modules = {
'./dir/foo.js': () => import('./dir/foo.js'),
'./dir/bar.js': () => import('./dir/bar.js')
}
然后再通过vue提供的注册异步组件的方式进行引入,vue的 defineAsyncComponent方法:该方法可以创建一个按需加载的异步组件 基于以上两个方法,实现组件自动注册。
// import SvgIcon from './svg-icon/index.vue'
// import HmPopup from './popup/index.vue'
import { defineAsyncComponent } from 'vue'
// const components = [SvgIcon, HmPopup]
export default {
install(app) {
// components.forEach((element) => {
// app.component(element.name, element)
// })
// 获取当前路径下所有文件夹下的index.vue
const components = import.meta.glob('./*/index.vue')
// 遍历获取到的组件模块
for (let [key, component] of Object.entries(components)) {
const componentName = 'hm-' + key.replace('./', '').split('/')[0]
// 通过 defineAsyncComponent 异步导入指定路径下的组件
app.component(componentName, defineAsyncComponent(component))
}
}
}
其实如果组件都提供了name属性,我们可以直接手动引入各组件模块,然后实现半自动注册。组件提供name的好处是,在vue-devtools中调试时方便查找各个组件。
在vue官网中,在 3.2.34 或以上的版本中,使用
然后全局注册该svg通用组件,这里我们使用插件的方式 main.js中直接通过use注册后,即可使用。 但是这样项目中并不能知道svg图标的路径,我们需要使用vite-plugin-svg-icons插件来指定查找路径。 在vite.config.js中配置svg相关内容 在main.js中导入并注册svg-icons,他会把指定文件夹下的svg图片都注册在首页。 案例代码 vuex-persistedstate, 作为vuex的一个插件,可以持久化store中的数据,防止因页面刷新等操作,数据丢失。(再次运行时,将缓存的数据作为对应state属性的初始值) 以前写过主题替换的demo 原理: 通过类名的切换使得html元素在不同类名下展示不同的样式 实现思路:(此方案基于tailwindcss插件) tailwind.config.js配置文件需要加上 其中跟随系统的主题变化,需要用到 Window.matchMedia(),该方法接收一个mediaQueryString(媒体查询解析的字符串),该字符串我们可以传递prefers-color-scheme,即 window.matchMedia('(prefers-color-scheme: dark)')方法即可返回一个MediaQueryList 对象。 主题修改工具函数 案例代码 整个瀑布流组件的构建大体需要分成几部分 计算大体方法就是,拿到容器宽度(不包括margin,padding,border), 并且获取容器中每个item元素的总间距。 然后用当前容器减去总间距,再除以列数。 图片是否定义了高度,如果定义高度,可以直接计算出每个item的高度 如果未定义高度,我们需要在图片加载完成后,才能计算高度。 都是通过获取列最小高度基础上计算的一些值。 需要先将每列高度初始化为0,使用该对象作为容器,key为列下标,值为列高度。 获取left偏移量时,我们需要拿到最小高度列。 获取最小高度列后,直接乘以列宽和加上间距就行 top偏移量的计算,我们可以直接拿到最小高度列高就行 需要注意的是,我们在完成每次元素偏移量赋值的时候,都需要将最小高度列重新计算高度。 最后将最大高度列高度赋值给容器高度即可。 案例代码 主要是通过监听底部dom是否出现在可视区域,然后做数据请求,处理一些特殊情况。使用到了 usevue的useIntersectionObserver api ,它就是简单了对 IntersectionObserver api进行了封装,让我们更轻易地实现可见区域交叉监听。 这个IntersectionObserver 以前写过一篇文章 《如何判断元素是否在可视区域内呢?然后搞一些事情》介绍过,可以看看。 主要提供isLoading展示加载更多动态图标, isFinished判断数据是否请求完毕, load事件请求数据 props即可。 这里有一个容易出现的bug,当我们数据量一次返回过少时,底部区域一直在可是区域内,我们将不能再次调用useIntersectionObserver传入的回调,也就不能再次请求数据,加载更多了。 所以我们需要监听loading的变化,再次触发数据请求。但是这样又有一个问题了。当我们数据一次性加载过多时,我们依旧请求多次数据,这是因为虽然第一次请求的数据回来了,但是界面还没有渲染,这是底部区域依旧在可是区域内,导致数据再一次被请求。所以我们手动延迟数据在watch监听中的请求。 案例代码 也是需要用到usevue的useIntersectionObserver api,首先将src置空,当进入可视区域,我们就将src赋值回去。 通过vite的Glob 的另一个方法来做到指令自动注册。使用 import.meta.globEager,直接引入所有的模块。 案例代码 confirm 组件的实现思路: 了解了组件的设计思路,我们就需要分析它应该具有的props 对于confirm组件来说,我们通过一个响应式数据来控制显示和隐藏实现的动画。 函数组件的封装,主要使用h, render函数操作。 后两个函数主要就是为了区分点击了取消还是确认。 案例代码 message组件的实现和confirm非常类似。 props需要指定弹框时间和类型 主要就是弹框的隐藏时机不同。message中,是通过外界传入的时间控制隐藏的。 函数组件实现 文件下载相关的库 直接使用api,传入下载路径即可 我们知道在原生dom上,提供了一些方法来供我们开启或关闭全屏: 使用requestFullscreen()和exitFullscreen()来实现 基于WebKit内核的浏览器需要添加webkit前缀,使用webkitRequestFullScreen()和webkitCancelFullScreen()来实现。 基于Trident内核的浏览器需要添加ms前缀,使用msRequestFullscreen()和msExitFullscreen()来实现,注意方法里的screen的s为小写形式。 基于Gecko内核的浏览器需要添加moz前缀,使用mozRequestFullScreen()和mozCancelFullScreen()来实现。 Opera浏览器需要添加o前缀,使用oRequestFullScreen()和oCancelFullScreen()来实现。 考虑到兼容性,我们可以使用usevue提供的useFullscreen api 我们可以通过driver.js 库实现。 定义好对应的引导步骤。 然后调用driver库提供的api即可 第三方表单校验库: vee-validate。 该库中,提供了三个重要的组件。分别为我们处理表单组件和表单验证错误提示。 每个表单项,可以通过rulesprops绑定验证规则。message与field中的name是相对应的。 需要注意的是:验证函数,true表示表单验证通过, String表示表单验证未通过,给出的提示文本。 对于需要依赖别的表单值进行关联验证的,我们需要通过defineRule来定义规则。例如:确认密码输入框验证。 rule规则rules="validateConfirmPassword:@password" 目的:明确当前操作是人完成的,而非机器。 人机验证是什么?如何实现人机验证? 人机验证通过对用户的行为数据、设备特征与网络数据构建多维度数据分析,采用完整的可信前端安全方案保证数据采集的真实性、有效性。 滑动验证码实现原理是什么? 滑动验证码是服务端随机生成滑块和带有滑块阴影的背景图片,然后将其随机的滑块位置坐标保存。前端实现互动的交互,将滑块把图拼上,获取用户的相关行为值。然后服务端进行相应值的校验。其背后的逻辑是使用机器学习中的深度学习,根据鼠标滑动轨迹,坐标位置,计算拖动速度,重试次数等多维度来判断是否人为操作。 滑动验证码对机器的判断,不只是完成拼图,前端用户看不见的是——验证码后台针对用户产生的行为轨迹数据进行机器学习建模,结合访问频率、地理位置、历史记录等多个维度信息,快速、准确的返回人机判定结果,故而机器识别+模拟不易通过。滑动验证码也不是万无一失,但对滑动行为的模拟需要比较强的破解能力,毕竟还是大幅提升了攻击成本,而且技术也会在攻防转换中不断进步。 分为两种: 一种是收费的、另一种是开源的 收费的代表有: 开源的有: 毫无疑问,就我们学习来说,开源的就是最好的。 该库主要是通过三个方法来进行验证回调操作的。 并且内部提供reset方法来修改拼图图片。 想要学习图片裁剪,我们需要获取图片并展示。在我们点击上传图片如何预览呢?我们来简单介绍一下。 以前在学习大文件上传时,介绍过相关的api 执行时机: 内存使用: 优劣对比: 使用createObjectURL可以节省性能并更快速,只不过需要在不使用的情况下手动释放内存 如果不太在意设备性能问题,并想获取图片的base64,则推荐使用FileReader.readAsDataURL。 cropperjs是一个非常强大的图片裁剪工具,它可以适用于:原生js,vue,react等等。而且操作也非常简单、只需要简单几步即可完成图片的裁剪工作。 然后我们可以通过cropper.getCroppedCanvas().toBlob拿到裁剪后的文件对象。 免费获取渠道 以阿里云 oss 为例,安装ali-oss 封装创建oss对象实例方法 案例代码 一般情况下,我们在移动端切换路由时,为了让h5页面跳转可以与原生app媲美,都会使用vue提供的过度动效来实现。 主要实现逻辑就是,先定义进入和离开页面的动画,通过路由跳转动态改变transition动画名称。在跳转的时候动态改变缓存组件栈的组件,从而达到组件切换缓存效果。 案例代码 可以通过第三方平台 兔小巢进行接入。 登录成功后,就可以创建产品。 创建完成后,就会生成一个返回网址。将其接入网站即可。import SvgIcon from "./svg-icon/index.vue"
export default {
install(app) {
app.component("SvgIcon", SvgIcon)
}
}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import {join} from "path"
import {createSvgIconsPlugin} from "vite-plugin-svg-icons"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
createSvgIconsPlugin({
// 指定需要缓存的图标文件夹
iconDirs: [join(__dirname, "/src/assets/icons")],
// 指定symbolId格式,就是svg.use使用的href
symbolId: "icon-[name]"
})
],
})
// 注册 svg-icons
import "virtual:svg-icons-register"
持久化状态数据 vuex-persistedstate
import { createStore } from "vuex";
import createPersistedState from "vuex-persistedstate";
const store = createStore({
// ...
plugins: [createPersistedState({
key : 'categoryList', // 缓存的key,
paths: ['category'], // 用于部分持久化状态的任何路径的数组。如果没有给出路径,完整的状态会被持久化。如果给定一个空数组,则不会保留任何状态。必须使用点表示法指定路径。如果使用模块,请包含模块名称。例如:“auth.user” 默认为undefined.
})],
});
主题切换实现
darkMode: 'class'
// 当前主题模式
import { THEME_LIGHT } from '@/constants'
export default {
namespaced: true,
state: () => ({
themeType: THEME_LIGHT
}),
mutations: {
setThemeType(state, theme) {
state.themeType = theme
}
}
}
const handleHeaderTheme = (item) => {
store.commit('theme/setThemeType', item.type)
}
import { watch } from 'vue'
import store from '../store'
import { THEME_DARK, THEME_LIGHT, THEME_SYSTEM } from '../constants'
/**
* 监听系统主题变化
*/
let matchMedia = ''
function changeSystemTheme() {
// 仅需初始化一次即可
if (matchMedia) return
matchMedia = window.matchMedia('(prefers-color-scheme: dark)')
// 这里也是监听主题切换,然后调用修改html class
matchMedia.addEventListener('change', (event) => {
changeTheme(THEME_SYSTEM)
})
}
/**
* 主题匹配函数
* @param val {*} 主题标记
*/
const changeTheme = (val) => {
let htmlClass = ''
if (val === THEME_LIGHT) {
// 浅色主题
htmlClass = THEME_LIGHT
} else if (val === THEME_DARK) {
// 深色主题
htmlClass = THEME_DARK
} else {
// 跟随系统
changeSystemTheme()
// true是深色模式, false是浅色主题
htmlClass = matchMedia.matches ? THEME_DARK : THEME_LIGHT
}
document.querySelector('html').className = htmlClass
}
/**
* 初始化主题
*/
export default () => {
// 监听主题切换,修改html class的值
watch(() => store.getters.themeType, changeTheme, {
immediate: true
})
}
实现瀑布流布局
计算每列宽度
const useContainerWidth = () => {
const { paddingLeft, paddingRight } = getComputedStyle(
containerRef.value,
null
)
// 容器左边距
containerLeft.value = parseFloat(paddingLeft)
// 容器宽度
containerWidth.value =
containerRef.value.offsetWidth -
parseFloat(paddingLeft) -
parseFloat(paddingRight)
}
// 列间距总大小 (column - 1) * columnSpacing
const columnSpacingTotal = computed(() => {
return (props.column - 1) * props.columnSpacing
})
const useColumnWidth = () => {
// 获取容器宽度
useContainerWidth()
// 获取列宽
columnWidth.value =
(containerWidth.value - columnSpacingTotal.value) / props.column
}
获取每个元素的高度
const useItemHeight = () => {
// 初始化item高度列表
itemsHeight = []
// 获取 item 元素
const itemElements = [...document.getElementsByClassName('hm-waterfall-item')]
// 获取item高度
itemElements.forEach((itemEl) => {
itemsHeight.push(itemEl.offsetHeight)
})
// 渲染位置
useItemLocation()
}
/**
* 获取所有item中img元素
*/
export function getImgElements(itemElements) {
const imgElements = []
itemElements.forEach((el) => {
imgElements.push(...el.getElementsByTagName('img'))
})
return imgElements
}
/**
* 获取所有图片路径
*/
export function getAllImgSrc(imgElements) {
const allImgSrc = []
imgElements.forEach((item) => {
allImgSrc.push(item.getAttribute('src'))
})
return allImgSrc
}
export function allImgComplete(allImgSrc) {
// 存放所有图片加载的promise对象
const promises = []
// 循环allImgSrc
allImgSrc.forEach((imgSrc, index) => {
promises.push(
new Promise((resolve) => {
const imgObj = new Image()
imgObj.src = imgSrc
imgObj.onload = () => {
resolve({
imgSrc,
index
})
}
})
)
})
return Promise.all(promises)
}
const waitImgComplete = () => {
// 初始化item高度列表
itemsHeight = []
// 获取 item 元素
const itemElements = [...document.getElementsByClassName('hm-waterfall-item')]
// 获取所有元素的 img 标签
const imgElements = getImgElements(itemElements)
// 获取所有 img 图片路径
const allImgSrc = getAllImgSrc(imgElements)
// 计算图片预加载,然后计算高度
allImgComplete(allImgSrc).then(() => {
itemElements.forEach((itemEl) => {
itemsHeight.push(itemEl.offsetHeight)
})
})
// 渲染位置
useItemLocation()
}
计算每个元素的偏移量
// 容器的总高度
const containerHeight = ref(0)
// 记录每列高度的容器。key:所在列 val:列高
const columnHeightObj = ref({})
/**
* 构建记录各列的高度的对象。初始化都为0
*/
const useColumnHeightObj = () => {
columnHeightObj.value = {}
for (let i = 0; i < props.column; i++) {
columnHeightObj.value[i] = 0
}
}
/**
* 获取最小高度
*/
export function getMinHeight(columnHeightObj) {
const columnHeightValue = Object.values(columnHeightObj)
return Math.min(...columnHeightValue)
}
/**
* 获取最小高度的column
*/
export function getMinHeightColumn(columnHeightObj) {
// 获取最小高度
const minHeight = getMinHeight(columnHeightObj)
const columns = Object.keys(columnHeightObj)
const minHeightColumn = columns.find((col) => {
return columnHeightObj[col] === minHeight
})
return minHeightColumn
}
/**
* 计算当前元素的left偏移量
*/
const getItemLeft = () => {
// 获取最小高度的列
const column = getMinHeightColumn(columnHeightObj.value)
// 计算left
return (
(columnWidth.value + props.columnSpacing) * column + containerLeft.value
)
}
/**
* 计算当前元素的top偏移量
*/
const getItemTop = () => {
// 获取列最小高度
const minHeight = getMinHeight(columnHeightObj.value)
return minHeight
}
/**
* 重新计算最小高度列高度
*/
const increasingHeight = (index) => {
// 获取最小高度的列
const column = getMinHeightColumn(columnHeightObj.value)
// 该列高度重新计算
columnHeightObj.value[column] =
columnHeightObj.value[column] + itemsHeight[index] + props.rowSpacing
}
// 渲染位置
const useItemLocation = () => {
props.data.forEach((item, index) => {
// 避免重复计算
if (item._style) return
// 拿到最小高度,计算_style中的left, top
item._style = {}
item._style.left = getItemLeft()
item._style.top = getItemTop()
// 每次设置完偏移量时,都需要更改最短列的高度。
increasingHeight(index)
})
// 当所有item设置好偏移量时,将容器高度设置为列最高的高度
containerHeight.value = getMaxHeight(columnHeightObj.value)
}
长列表加载组件
自定义懒加载指令
import { useIntersectionObserver } from '@vueuse/core'
export default {
mounted(el) {
// 保存图片路径
const imgSrc = el.getAttribute('src')
// 将图片src置空
el.setAttribute('src', '')
// 监听图片的可见
const { stop } = useIntersectionObserver(el, ([{ isIntersecting }]) => {
if (isIntersecting) {
el.setAttribute('src', imgSrc)
// 停止监听
stop()
}
})
}
}
export default {
install(app) {
// 获取到所有指令模块对象
const modules = import.meta.globEager('./modules/*.js')
for (let [key, value] of Object.entries(modules)) {
const directiveName = key.replace('./modules/', '').split('.')[0]
app.directive(directiveName, value.default)
}
}
}
confirm组件
const props = defineProps({
title: {
type: String
},
content: {
type: String,
required: true
},
// 按钮文字
cancelText: {
type: String,
default: '取消'
},
confirmText: {
type: String,
default: '确定'
},
// 取消和确认时触发事件, 例如移除dom
closeAfter: {
type: Function
},
/**
* 主要是区分点击了取消还是确定
*/
// 点击确定触发事件
handleConfirmClick: {
type: Function
},
// 点击取消触发事件
handleCancelClick: {
type: Function
}
})
// 动画时间 状态驱动的动态css
const actionDuration = '0.5s'
// 控制confirm显隐
const isVisible = ref(false)
// 组件挂载就让弹框显示,通过函数组件控制组件挂载卸载
// 通过mounted, 让其挂载时有动画效果
onMounted(() => {
isVisible.value = true
})
/**
* 关闭弹窗
* 通过定时器,让动画完成后在移除dom
*/
const handleClose = () => {
// 当隐藏时才会出现动画
isVisible.value = false
setTimeout(() => {
// 卸载confirm组件
props.closeAfter()
}, actionDuration.replace('0.', '').replace('s', '') * 100)
}
import { h, render } from 'vue'
import Confirm from './index.vue'
export default function createConfirm({
title,
content,
cancelText = '取消',
confirmText = '确定'
}) {
return new Promise((resolve, reject) => {
/**
* 移除confirm
*/
const closeAfter = () => {
render(null, document.body)
}
/**
* 点击确定按钮,回调
*/
const handleConfirmClick = resolve
/**
* 点击取消按钮,回调
*/
const handleCancelClick = reject
// 生成vnode,并传入props
const vnode = h(Confirm, {
title,
content,
cancelText,
confirmText,
closeAfter,
handleConfirmClick,
handleCancelClick
})
// 渲染组件到body中
render(vnode, document.body)
})
}
message组件
const props = defineProps({
// message 类型
type: {
type: String,
required: true,
validate(val) {
if (types.includes(val)) {
return true
} else {
throw new Error('请传入正确的类型值(error, warn, success)')
}
}
},
// message 内容
content: {
type: String,
required: true
},
// 消息回调,在动画完成后,卸载message
closeAfter: {
type: Function
},
// 延时多久删除
delay: {
type: Number,
default: 3000
}
})
const isVisible = ref(false)
/**
* 为了保证出现时动画展示,我们需要在组件挂载后在显示对应的内容
*/
onMounted(() => {
isVisible.value = true
setTimeout(() => {
isVisible.value = false
}, props.delay)
})
// 在动画完成后,通过transition组件的after-leave钩子触发组件卸载。
import { h, render } from 'vue'
import Message from './index.vue'
export function createMessage({ type, content, delay = 3000 }) {
/**
* 动画结束时的回调
*/
const closeAfter = () => {
// message 销毁
render(null, document.body)
}
// 生成vnode
const vnode = h(Message, {
type,
content,
delay,
closeAfter
})
// 渲染组件
render(vnode, document.body)
}
文件下载
import { saveAs } from 'file-saver'
const handleDownload = (downloadPath) => {
saveAs(downloadPath)
}
全屏展示
一般浏览器
早期版本Chrome浏览器
早期版本IE浏览器
早期版本火狐浏览器
早期版本Opera浏览器
import { useFullscreen } from '@vueuse/core'
const imgRef = ref(null)
const { isFullscreen, enter, exit, toggle } = useFullscreen(imgRef)
const handleFullScreen = () => {
imgRef.value.style.backgroundColor = 'transparent'
enter()
}
功能引导实现
export default [
{
// 在哪个元素中高亮
element: '.guide-home',
// 配置对象
popover: {
// 标题
title: 'logo',
// 描述
description: '点击可返回首页'
}
},
{
element: '.guide-search',
popover: {
title: '搜索',
description: '搜索您期望的图片'
}
},
{
element: '.guide-theme',
popover: {
title: '风格',
description: '选择一个您喜欢的风格',
// 弹出的位置
position: 'left'
}
},
{
element: '.guide-my',
popover: {
title: '账户',
description: '这里标记了您的账户信息',
position: 'left'
}
},
{
element: '.guide-start',
popover: {
title: '引导',
description: '这里可再次查看引导信息',
position: 'left'
}
},
{
element: '.guide-feedback',
popover: {
title: '反馈',
description: '您的任何不满都可以在这里告诉我们',
position: 'left'
}
}
]
import Driver from 'driver.js'
import 'driver.js/dist/driver.min.css'
import steps from './steps'
import { onMounted } from 'vue'
/**
* 引导页处理
*/
let driver = null
onMounted(() => {
driver = new Driver({
// 禁止点击蒙版关闭
allowClose: false,
closeBtnText: '关闭',
nextBtnText: '下一个',
prevBtnText: '上一个'
})
})
/**
* 开始引导
*/
const handleGuideClick = () => {
// 定义引导步骤
driver.defineSteps(steps)
driver.start()
}
表单验证
import {
Form as VeeForm,
Field as VeeField,
ErrorMessage as VeeErrorMessage
} from 'vee-validate'
/**
* 用户名的表单校验
*/
export const validateUsername = (value) => {
if (!value) {
return '用户名为必填的'
}
if (value.length < 3 || value.length > 12) {
return '用户名应该在 3-12 位之间'
}
return true
}
/**
* 确认密码的表单校验
*
* 参数二:表示关联表单值的数组
*/
export const validateConfirmPassword = (value, password) => {
if (value !== password[0]) {
return '两次密码输入必须一致'
}
return true
}
/**
* 定义关联规则, 例如确认密码
*/
defineRule('validateConfirmPassword', validateConfirmPassword)
人类行为验证
原理是什么?
目前实现的方案有哪些?
let captcha = null
onMounted(() => {
captcha = sliderCaptcha({
// 绑定的dom元素id名
id: 'captcha',
// 验证成功的回调 arr滑块移动轨迹
async onSuccess(arr) {
// 这里将行为轨迹发送到服务端进行验证。
const res = await getCaptcha({
behavior: arr
})
// 验证成功发出事件
if (res) emits('verifySuccess')
},
// 验证失败回调
onFail() {
console.error('人类行为验证失败')
},
// 默认的验证方法,咱们不在此处进行验证,而是选择在用户拼图成功之后进行验证,所以此处永远返回为 true
verify() {
return true
}
})
})
图片裁剪
图片预览
cropperjs库剪切图片
import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'
// 移动端配置对象
const mobileOptions = {
// 将裁剪框限制在画布的大小
viewMode: 1,
// 移动画布,裁剪框不动
dragMode: 'move',
// 裁剪框固定纵横比:1:1
aspectRatio: 1,
// 裁剪框不可移动
cropBoxMovable: false,
// 不可调整裁剪框大小
cropBoxResizable: false
}
// PC 端配置对象
const pcOptions = {
// 裁剪框固定纵横比:1:1
aspectRatio: 1
}
/**
* 图片裁剪处理
*/
const imageRef = ref(null)
let cropper = null
onMounted(() => {
/**
* 接收两个参数:
* 1. 需要裁剪的图片 DOM
* 2. options 配置对象
*/
cropper = new Cropper(
imageRef.value,
isMobileTerminal.value ? mobileOptions : pcOptions
)
})
// 获取裁剪后的图片
cropper.getCroppedCanvas().toBlob((blob) => {
// 裁剪后的 blob 对象
console.log(blob)
})
图片上传到阿里的oss存储
import OSS from 'ali-oss'
import { REGION, BUCKET } from '@/constants'
import { getSts } from '@/api/sys'
export const getOSSClient = async () => {
const res = await getSts()
return new OSS({
// yourRegion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。
region: REGION,
// 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
accessKeyId: res.Credentials.AccessKeyId,
accessKeySecret: res.Credentials.AccessKeySecret,
// 从STS服务获取的安全令牌(SecurityToken)。
stsToken: res.Credentials.SecurityToken,
// 填写Bucket名称。
bucket: BUCKET,
// 刷新 token,在 token 过期后自动调用(但是并不生效,可能会在后续的版本中修复)
refreshSTSToken: async () => {
// 向您搭建的STS服务获取临时访问凭证。
const res = await getSts()
return {
accessKeyId: res.Credentials.AccessKeyId,
accessKeySecret: res.Credentials.AccessKeySecret,
stsToken: res.Credentials.SecurityToken
}
},
// 刷新临时访问凭证的时间间隔,单位为毫秒。
refreshSTSTokenInterval: 5 * 1000
})
}
/**
* 上传图片到oss
*/
const store = useStore()
const putObjectToOSS = async (file) => {
// 创建oss对象实例
const ossClient = await getOSSClient()
try {
// 因为当前凭证只具备 images 文件夹下的访问权限,所以图片需要上传到 images/xxx.xx 。否则你将得到一个 《AccessDeniedError: You have no right to access this object because of bucket acl.》 的错误
const fileTypeArr = file.type.split('/')
const fileName = `${store.getters.userInfo.nickname}/${Date.now()}.${
fileTypeArr[fileTypeArr.length - 1]
}`
// 文件存放路径,文件。上传文件对象。并返回对应的图片路径
const res = await ossClient.put(`images/${fileName}`, file)
// 通知父元素更改图片地址
emits('updateImgUrl', res.url)
createMessage({
type: 'success',
content: '图片上传成功'
})
} catch (e) {
createMessage({
type: 'error',
content: '图片上传失败'
})
} finally {
// 关闭动画
loading.value = false
// 关闭弹窗
handleClose()
}
}
让h5页面跳转和原生app页面跳转一样流畅
用户反馈功能
原文链接:
https://juejin.cn/post/7251878440327512124