首页
示例工具
THE
BLOG
nextjs 加载 iconify 如何 SSR 化

> 由于开发项目页面多且复杂,所以选择使用到 iconify 图标库。使用到的是 nextjs,由于 iconify 在ssr中的一些缺陷,只能通过 json 去将图标 ssr 化,导致搭配 vscode 插件 Iconify IntelliSense 预览图标和书写体验不佳,所以进行封装改善开发体验。 ## 使用: ```typescript <AxIcon icon="line-md:close-circle-twotone" className="mr-2" size={48} /> ``` ## 完整代码 ```typescript "use client"; import { cn } from "@/lib/utils"; import { icons as entypoIcons, IconifyJSON } from "@iconify-json/entypo"; import { icons as lineMDIcons } from "@iconify-json/line-md"; import { icons as phIcons } from "@iconify-json/ph"; import { icons as tablerIcons } from "@iconify-json/tabler"; import { replaceIDs } from "@iconify/react"; import { getIconData, iconToHTML, iconToSVG } from "@iconify/utils"; interface AxIconProps { icon: string; size?: string | number; className?: string; } export default function AxIcon({ icon, size = 16, className }: AxIconProps) { const [brand, iconName] = icon.split(":"); const brandIcons: Record<string, IconifyJSON> = { tabler: tablerIcons, entypo: entypoIcons, ph: phIcons, "line-md": lineMDIcons }; if (!brandIcons[brand]) return; const iconData = getIconData(brandIcons[brand], iconName); if (!iconData) return; const renderData = iconToSVG(iconData, { height: size, width: size }); const svgStr = iconToHTML(replaceIDs(renderData.body), { ...renderData.attributes, class: cn("!w-auto !h-auto", className) }); return <div dangerouslySetInnerHTML={{ __html: svgStr }} />; } ```

avatar
出自
a1ex0012
nestjs
JavaScript
后端
nestjs 链接 notion API

测试

avatar
出自
a1ex0012
高级轮播效果实现

[https://spring-slider.uiinitiative.com/](https://spring-slider.uiinitiative.com/)

avatar
出自
a1ex0012
vue的多种路由模式

## **什么是vue-router** 是页面应用用于设定访问路径,将路径和组件映射起来的路由插件 ## **初始化路由** ```text import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router"; const routes: RouteRecordRaw[] = [ {   path: "/",   redirect: "/home", }, {   path: "/home",   component: () => import("../components/ACom.vue"), }, {   path: "/b/:id",   component: () => import("../components/BCom.vue"),   props: false, }, ]; const router = createRouter({ history: createWebHistory(), routes, }); export default router; ​ ``` > 这边只说一下这个props ## **RouteRecordRaw的props** ### **布尔模式** 当 `props` 设置为 `true` 时,`route.params` 将被设置为组件的 props,默认围殴false。 > 对于有命名视图的路由,你必须为每个命名视图定义 props 配置 那么什么是命名视图呢? 官网上有描述 ### **对象模式** 依旧是设置成组件的props ```text props: { newsletterPopup: false } ``` ### **函数模式** 创建返回props的函数 ```text {   path: "/home",   component: () => import("../components/ACom.vue"),   props: (route) => ({ query: route.query.q + 'hh' }), }, ``` `route.query`便是路径的query,例如 访问 `/home?q=abc` 将会生产出一个prop {query:'abchh'} ## **路由的几种模式** ## **hash模式** `http://127.0.0.1:5174/#/home` #/home就是hash值 原理:通过原生的hashChange事件监听hash值的变化 ### **H5模式(history模式)** 和hash的表现方式一样但url上没有# 原理: 利用h5的history 的pushState()和replaceState()实现

avatar
出自
a1ex0012
图片懒加载

> 图片懒加载就是当图片过多时的一个加载方式,当图片出现在视口才加载 ## **前言** 通常在许多网站都能看到懒加载的的身影,因为网站对于用户体验越来越重视,而懒加载也会伴随一理解的问题。 ## **监听滚动问题** 当我们通过监听滚动事件的形式去决定图片是否懒加载时,页面刷新时不会立即执行监听函数,因为此时页面并没有滚动,这会造成当前页面需加载的图片呈现懒加载的状态,此时需要在代码中立即执行一次该函数才能实现对应效果 ## **transition问题** 当我们使用routerview+transition时,对应页面跳转时会有对应过渡效果,因为图片懒加载是在挂载后监听图片的`offsetTop`l来决定是否加载的 该过渡效果是这样的 ```text .fade-enter-active, .fade-leave-active {  transition: all 0.4s ease; } ​ .fade-enter-from {  transform: translateY(100vh); } .fade-leave-to {  transform: translateY(100vh); } ``` 这回造成一个问题,第一次获取到的图片状态时这样的 ```text DOMRect {    bottom: 1301.8125305175781    height: 211.60000610351562    left: 250.60000610351562    right: 1287.4000549316406    top: 1090.2125244140625    width: 1036.800048828125    x: 250.60000610351562    y: 1090.2125244140625 … } ``` 可以看到该dom的top是1090,就是该dom在他的父容器高度加上视口高度的和,因为过渡效果刚开始就触发了一次检测,此时它是不在视口内的,所以加载时会出现在视口但任然不加载。 ### **如何解决** 通过监听transition的动画结束 ### **`transitionend`** 监听该dom的动画效果结束 ```text dom.addEventListen("transitionend",dosomething) ``` 所以我们可以在该dom元素上监听动画结束然后触发一次图片懒加载检测 ```text const categoryRef = ref(); onMounted(() => { categoryRef.value.ontransitionrun = () => {   emitter.emit("DetailScroll"); }; }); ``` 但是还会存在一个问题,假如说该dom元素有多个过渡效果,例如hove效果,这是hover触发也会触发一次图片懒加载检测,这回照成没必要的开销,我们当前只针对于路由过渡,其他元素触发transitionend不需要有其他操作,我们可以通过设置一个控制阀解决它。 > 但是我用transitionend监听时总是有时候监听到有时候又监听不到,我真裂开,最后通过将监听更换成ontransitionrun的形式解决 ```text const categoryRef = ref(); onMounted(() => { let flag = true; categoryRef.value.ontransitionrun = () => {   if (!flag) return;   emitter.emit("DetailScroll");   flag = false; }; }); ```

avatar
出自
a1ex0012
JavaScript
h5
前端
canvas 和 svg

> 基础的基础 ## **svg** 学svg前先了解位图和矢量图大的区别 - 位图: 放大会失真图像边缘模糊,因为它是由橡树点组成的 - 矢量图:放大不会失真,使用xml描述图形 svg便是矢量图,所以金额已使用css设置样式,也可以用js对其草操作 ### **基本使用** ```text <svg></svg> ``` 默认w300h150 ### **基本图形** > 属性不赘述一眼能看出来 1. 矩形 ```text <rect x="20" y="50" width="100" height="100" rx="20" ry="10"></rect> ``` 2. 圆形 ```text <circle cx="50" cy="0" r="20"></circle> ``` 3. 直线 ```text <line x1="20" y1="30" x2="50" y2="100" stroke="#008c8c" /> ``` 4. 折线 ```text <polyline points="1 1 20 20 100 50 60 80" stroke="#F0F" /> ``` 5. 多边形 ```text <polygon points="1 1 20 20 100 50 60 80" stroke="#F0F" fill="#F0F" /> ``` 6. 路径 ```text <path d="M 10 10 L 100 50 L 10 80 Z" stroke="blue" fill="none" /> ``` 关于d属性 - M: 起始点坐标,`moveto` 的意思。每个路径都必须以 `M` 开始。`M` 传入 `x` 和 `y` 坐标,用逗号或者空格隔开。 - `L`: 轮廓坐标,`lineto` 的意思。`L` 是跟在 `M` 后面的。它也是可以传入一个或多个坐标。大写的 `L` 是一个**绝对位置**。 - l: 这是小写 `L`,和 `L` 的作用差不多,但 `l` 是一个**相对位置**。 - `H`: 和上一个点的Y坐标相等,是 `horizontal lineto` 的意思。它是一个**绝对位置**。 - `h`: 和 `H` 差不多,但 `h` 使用的是**相对定位**。 - `V`: 和上一个点的X坐标相等,是`vertical lineto` 的意思。它是一个**绝对位置**。 - `v`: 这是一个小写的 `v` ,和大写 `V` 的差不多,但小写 `v` 是一个相对定位。 - `Z`: 关闭当前路径,`closepath` 的意思。它会绘制一条直线回到当前子路径的起点。 关于样式 1. 填充颜色 - 使用属性fill - 透明度 fill-opacity 2. 描边 - 使用属性stroke - 透明度 stroke-opacity - 宽度 stroke-width - 虚线描边stroke-dasharray - 虚线偏移量 stroke-dashoffset ## **canvas** canvas翻译过来就是画布,可以用javascript在上面绘画,是H5的新API ### **应用场景** - 游戏 - 可视化数据图表 ### **用法** `<canvas></canvas>`,默认w300h150 通过获取绘图上下文精选绘制 ### **绘制路径** canvas坐标原点的定位在左上角 - moveTo 设置路径的起点 - lineTo 绘制一条直线到起点 - beginPath/closePath 作用是将 不同绘制的形状进行隔离, 每次执行此方法,表示重新绘制一个路径,跟之前的绘制的墨迹可以进行分开样式设置和管理。 - stroke 描边 绘制一个基本图形的过程 ```text // 1. 开始  ctx?.beginPath();  // 2. 选择起始点  ctx?.moveTo(20, 20);  // 3. 画线  ctx?.lineTo(50, 80);  ctx?.lineTo(100, 180);  ctx?.lineTo(0, 20);  // 填充  ctx?.fill();  // 4。 闭合路径  ctx?.closePath();  // 5. 描边  ctx?.stroke(); ``` 基础图形 ```text // 篡改就矩形  ctx?.beginPath();  ctx?.rect(10, 20, 40, 80);  ctx?.fill();  ctx?.closePath();  ctx?.stroke();  // 画饼状图  ctx?.beginPath();  ctx?.moveTo(100, 100);  ctx?.arc(100, 100, 150, 0, Math.sin(40), false);  ctx?.fill();  ctx?.stroke();  ctx?.closePath(); ​  // 写字  ctx?.beginPath();  ctx!.font = "18px '微软雅黑'";  ctx!.textAlign = "center";  ctx?.fillText("test", 300, 300, 100);  ctx?.stroke();  ctx?.closePath(); ​  // 画图像  ctx?.beginPath();  // 相当于复刻相对比例图片  ctx?.drawImage(canvasRef.value as CanvasImageSource, 300, 0);  ctx?.stroke();  ctx?.closePath(); ``` ### **关于样式** - fillStyle : 设置或返回用于填充绘画的颜色 - strokeStyle: 设置或返回用于笔触的颜色 - 设置阴影,渐变样式 ## **svg和canvas的区别** ### **svg** - 矢量图放大不失真 - 支持事件处理器 - 文字独立、可编辑可搜索 ### **canvans** - 绘制出来的图形是位图具有很好的渲染性能 - 适合数据量比较大(>1000) - 大量图形高频率交互 - 适合游戏 - 可以导出jpg/png图片 [https://raw.githubusercontent.com/A1ex-01/drawing-board/main/blog/blog-bg.png](https://raw.githubusercontent.com/A1ex-01/drawing-board/main/blog/blog-bg.png)

avatar
出自
a1ex0012
知识
ddStore后台

# **ddStore** ## **sequelize查询方式** ### **投影查询** ```text model.findAll({    attributes:["username","password"] }) ``` ### **or查询** ```text model.findOne(){    where:{       [Op.or]:[{username:"zs"},{password:"123"}]   } } ``` ### **模糊查询** ```text model.findAll({    where:{        username:{           [Op.like]:"王%"       }   } }) ``` ### **聚合查询** ```text model.findAll({    group:"address",    attribute:["address",[Sequelize.fn("count",Sequelize.col("valid")),"total"]],    where:{        valid:1   } }) ``` ## **1. 表关联设置** ### **目标的删除和更新策略** 1. CASCADE 级联策略。适用此种策略时主表的记录被删除或者主键字段被修改时汇通不删除或修改子表。 2. NO ACTION 无动作策略。适用此种策略时要删除主表必须删除子表,要删除主表的记录必须先删除子表的关联的记录,不能更新主表主键字段的值。 3. RESTRICT 主表约束策略。此种策略对主表的约束和NO ACTION一样。 4. SET NO 置空策略。使用此种策略时,如果主表被删除或者逐渐被修改,则将子表的外键设置为NULL。需要注意的是,如果子表的外键是主键或者是设置为NOT NULL的,则主表的删除和主键的更改跟NO ACTION一样。 ## **2.装饰器重构Koa路由** ### **为什么要使用装饰器重构路由** 类和装饰器可集中管理路由,大幅提升代码可读性 1. 类装饰器 ```text function prefix(prefix: string) {  return function (target: new (...args: any[]) => any) {    const router = new Router({ prefix }); ​    // 遍历类中所有的方法 => defineMeta时保存的key    for (let fn in target.prototype) {      // 获取路径和方法      const path = Reflect.getMetadata("path", target.prototype, fn);      const type: IRequestType = Reflect.getMetadata(        "type",        target.prototype,        fn     );      const middlewareArr: Middleware[] = Reflect.getMetadata(        "middleware",        target.prototype,        fn     );      if (middlewareArr) {        router[type](path, ...middlewareArr, target.prototype[fn]);     } else {        router[type](path, target.prototype[fn]);     }      // router上添加路由   }    // 载入rootRouter    rootRouter.use(router.routes(), router.allowedMethods()); }; } ``` 2. 方法装饰器 ```text function getMethodDecorator(type: IRequestType) {  return function (path: string) {    return function (target: any, key: string, desc: PropertyDescriptor) {      // 元信息保存路径名      Reflect.defineMetadata("path", path, target, key);      // 元信息保存方法名      Reflect.defineMetadata("type", type, target, key);   }; }; } ​ const get = getMethodDecorator(IRequestType.GET); ``` 3. 中间件装饰器 ```text function use(middleware: Middleware) {  return function (target: any, key: string) {    // 先获取中间件    let mw: Middleware[] = Reflect.getMetadata("middleware", target, key);    if (!mw) {      // 没有中间件      mw = [middleware];   } else {      mw.push(middleware);   }    // 定义中间件    Reflect.defineMetadata("middleware", mw, target, key); }; } ```

avatar
出自
a1ex0012
ddStore前台

# **ddStore** ## **默认打开浏览器** ```text vite --open ``` ## **1. 环境变量** ```text console.log(import.meta.env); // 获取 { BASE_URL: "/"; DEV: true; MODE: "development"; PROD: false; SSR: false; } // 类型来自 <reference types="vite/client" /> interface ImportMetaEnv { [key: string]: any  BASE_URL: string  MODE: string  DEV: boolean  PROD: boolean  SSR: boolean } ``` ### **自定义环境变量** 1. ```text // 扩展类型 interface ImportMetaEnv {  VITE_NAME: string; } // 预览线上 "preview": "vite preview --open" ``` ```text // https://vitejs.dev/config/ export default defineConfig((mode) => {  console.log(mode.mode); // 环境模式  // 拼接当前环境文件名  let server: CommonServerOptions;  const currEnvFileName = `.env.${mode.mode}`;  // 使用dotev  // 读取当前环境文件  const envData = fs.readFileSync(currEnvFileName);  const envMap: DotenvParseOutput = dotev.parse(envData);  server = {    host: envMap.VITE_HOST,    port: +envMap.VITE_PORT,    proxy: {     [envMap.VITE_BASE_URL]: {        target: envMap.VITE_PROXY,     },   }, };  return {    plugins: [vue()],    server, }; }); ​ ``` ## **2. 动态管理图片** 数据库只存取文件名 封装类方法获取所有文件图片,返回图片名对应的图片路径 ['文件名','path'] 安装good-storage进行本地存储化 ```text  static storageImgList() {    this.imgList = goodStorage.get("imgList") || {};    if (!Object.keys(this.imgList).length) {      // 如果为空,加载图片      this.loadAllImg();      goodStorage.set("imgList", this.imgList);   } } ``` ### **ESLint+Prettier** ```text cnpm i eslint@7.2.0 eslint-plugin-vue@7.20.0 vue-eslint-parser @typescript-eslint/parser @typescript-eslint/eslint-plugin -D npx eslint --init ``` 配置 ## **3. axios封装** 1. 封装类,类可以创建多个实例,适用范围更广,封装性更强 ```text class Request {  instance: AxiosInstance;  constructor(config: AxiosRequestConfig) {    this.instance = axios.create(config); }  request(config: AxiosRequestConfig) {    return this.instance.request(config); } } ​ ``` 1. 封装拦截器 ```text this.instance = axios.create(config);    // 拿到当前实例拦截对象    this.interceptors = config.interceptors!;    // 1. 定义全局请求拦截器    this.instance.interceptors.request.use(     (res: AxiosRequestConfig) => {        console.log('全局请求拦截');        return res;     },     (err: any) => err   );    // 2. 实例拦截器    this.instance.interceptors.request.use(      this.interceptors.requestInterceptors,      this.interceptors.requestInterceptorsCatch   ); ​    // 3. 实例响应拦截器 this.instance.interceptors.response.use(      this.interceptors.responceInterceptors,   this.interceptors.responceInterceptorsCatch   ); ​    // 全局相应拦截器保证最后执行    this.instance.interceptors.response.use(     (res: AxiosResponse) => {        console.log('全局响应拦截');        return res.data;     },     (err: any) => err   ); ``` **拦截器的执行顺序为实例请求→类请求→实例响应→类响应**(请求拦截器,先use的后执行,响应拦截器,先use的先执行) 1. 封装request方法 ```text request(config: RequestConfig) {    return new Promise((resolve, reject) => {      // 对单个请求设置拦截器      if (config.interceptors?.requestInterceptors) {        config = config.interceptors.requestInterceptors(config);     }   this.instance       .request(config)       .then((res) => {          // 对单个请求设置响应拦截          if (config.interceptors?.responceInterceptors) {            res = config.interceptors.responceInterceptors(res);         }          resolve(res);       })       .catch((err) => {          reject(err);       });   }); } ``` ```text ``` 2. 封装其他方法 ```text  get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {    return this.instance.get(url, config); }  post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {    return this.instance.post(url, data, config); } ​  put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {    return this.instance.put(url, data, config); } ​  delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {    return this.instance.delete(url, config); } ``` 1. 基本使用 ```text import allConf from '../config'; import Request from './request'; const request = new Request({  baseURL: allConf.baseApi,  timeout: 20000,  interceptors: {    requestInterceptors: (config) => {      console.log('实力请求拦截器');      return config;   },    responceInterceptors: (res) => {      console.log('实力响应拦截器');      return res.data;   }, }, }); export default request; ​ // category.ts import request from '../request'; import { ICategory } from './type'; class CategoryApi {  async getCategoryListByFirstId(firstId: number) {    return await request.get<ICategory[]>(`/ctgy/${firstId}`); } } const { getCategoryListByFirstId } = new CategoryApi(); export { getCategoryListByFirstId }; ``` ## **4. pinia集成** ```text const useCtgyStore = defineStore('ctgy', () => {  const firstCtgy = ref<IFirstCtgy[]>();  const getFirstCtgyAction = async () => {    firstCtgy.value = (await getFirstCatrgoryList()).data; };  return {    firstCtgy,    getFirstCtgyAction, }; }); ``` ## **5. 自适应配置** 1. 样式重置包 ```text npm install normalize.css ``` 2. postcss.config.cjs ```text module.exports = {  plugins: {    // ...    "postcss-px-to-viewport": {      unitToConvert: "px",      viewportWidth: 375,      propList: ["*"],      viewportUnit: "vw",      fontViewportUnit: "vw",      selectorBlackList: [],      minPixelValue: 1,      mediaQuery: false,      replace: true,      exclude: undefined,      include: undefined,      landscape: false,      landscapeUnit: "vw",   }, }, }; ​ ```

avatar
出自
a1ex0012
DPR适配问题

# **DPR适配问题** ## **简单实现** ```text      function setImgSrcset() {        let dpr = window.devicePixelRatio;        if (dpr > 3) dpr = 3;        const imgs = document.getElementsByTagName("img");       [...imgs].forEach((imgDom) => {          const imgSrc = imgDom.src;          const arr = imgSrc.split(".");          let imgsrcset = "";          if (dpr === 1) {            arr[arr.length - 2] += `@${dpr}x`;            imgsrcset = "";         } else {            arr[arr.length - 2] += `@${dpr}x`;            imgsrcset = arr.join(".");         }          imgDom.srcset = imgsrcset;       });     } ```

avatar
出自
a1ex0012
a1ex blog by a1ex