LOADING

加载过慢请开启缓存 浏览器默认开启

耀耀切克闹

Husky+eslint+ prettier规范代码

前端随笔 2023/5/25

1.Git Hooks

什么是Git Hooks

Git Hooks 在Git执行特定事件(如commit、push、receive等)后触发运行脚本,类似于vue的生命周期钩子,git在某些特定事件发生前或后也会有某些执行特定功能的钩子。githooks 保存在 .git 文件夹中

对于规范代码提交我们主要用到的Git Hooks里的这两个钩子,也就是是在git commit执行前对git commit的提交信息做出规范。

  • pre-commit 预提交钩子 触发时机:git commit执行前,可以进行遍历、检测亦或是其他操作暂存区代码 因此我们可以利用该钩子配合lint-staged完成对代码风格的规范
  • commit-msg 预提交时提交消息的钩子 触发时机:同样是git commit执行前,可检查提交消息 因此我们可以在这个钩子执行时配合commitlint完成规范代码提交消息

2.Husky

什么是Husky

就是让原生 git 钩子变得简单好操作的依赖包而已,您可以使用它来检查提交消息,运行测试,lint代码等…当您提交或推送时。Husky支持所有 Git 钩子。

Husky的使用

  • 首先执行安装命令 npm install husky --save-dev

  • 然后再在项目根目录下package.json 文件的scripts配置项 添加一行代码

"scripts": {
    "prepare": "husky install"
}

3.eslint+prettier

eslint

  • 执行命令 yarn add eslint -D
  • 如果希望ESLint支持TypeScript 执行命令yarn add typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin --dev

prettier

  • 执行命令 yarn add prettier -D

结合ESLint与prettier

单独使用ESLint需要在其配置文件中额外配置很多规则,而这些规则又大多为prettier的默认规则或者我们已经在prettier配了一份,这样难免出冲突,但插件的作者已经想到了这一点,出了一个名为eslint-plugin-prettier的插件。

  • 执行命令 yarn add eslint-plugin-prettier -D
  • ESLint与prettier相关文件配置看个人习惯和公司项目需求配就行了

    4.lint-staged

什么是lint-staged

int-staged 是一个在git暂存区上运行linters的工具

lint-staged的使用

  • 执行安装命令 npm install lint-staged --save-dev
  • 然后再在项目根目录下package.json 文件的添加上相关配置并配合eslint+prettier+pre-commit可以对暂存区代码进行检测和操作
 "lint-staged": {
    "src/**/*.{js,jsx,ts,tsx,json}": [
      "prettier --write",
      "eslint --fix"
    ],
    "*.vue": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{scss,less,styl,html}": [
      "prettier --write"
    ]
  }

5.commitlint

什么是commitlint

就是对你 git commit提交的消息做一个校验的插件,主要还是配合Git Hooks的钩commit-msg一起使用的。

commitlint的使用

  • 执行命令安装相关依赖 yarn add @commitlint/cli @commitlint/config-conventional -D
  • 在根目录下新建commitlint.config.js 用来配置commitlint
module.exports = {
  extends: [
    "@commitlint/config-conventional"
  ],
  // 以下时我们自定义的规则
  rules: {
    'type-enum': [
      2,
      'always',
      [
        'bug', // 此项特别针对bug号,用于向测试反馈bug列表的bug修改情况
        'feat', // 新功能(feature)
        'fix', // 修补bug
        'docs', // 文档(documentation)
        'style', // 格式(不影响代码运行的变动)
        'refactor', // 重构(即不是新增功能,也不是修改bug的代码变动)
        'test', // 增加测试
        'chore', // 构建过程或辅助工具的变动
        'revert', // feat(pencil): add ‘graphiteWidth’ option (撤销之前的commit)
        'merge' // 合并分支, 例如: merge(前端页面): feature-xxxx修改线程地址
      ]
    ]
  }
};

注意

如果是用命令建的commitlint.config.js文件,echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js,在vs code中看看它的格式是否正确,如果不是UTF-8需要

改一下格式,我就是因为这个原因卡了半天
1684205419515.png

实现

前面把需要的依赖包和配置文件整好了(package.json里的不要忘了整),接下来就是利用脚本实现

注意

企业微信截图_16841411722462.png
所以下面是放在.husk文件夹下利用脚本实现:

  • 因为之前一安装好husky并在package.json配置了,直接执行yarn husky add .husky/pre-commit 'npx --no-install lint-staged "$1"'之后,会看到在根目录的.husky文件夹下多了一个 pre-commit 文件,其内容如下:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install lint-staged "$1"

可以看到之前安装并配置的lint-staged在这个钩子中就开始发挥作用了,在提交前利用lint-staged把不规范大代码格式化

  • 执行yarn husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'之后,会看到在根目录的.husky文件夹下多了一个 commit-msg 文件,其内容如下:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install commitlint --edit "$1"

可以看到之前安装并配置的commitlint在这个钩子中就开始发挥作用了,开始检测你的提交消息了

测试效果

不规范提交

1684206668224.png

阅读全文

ECMAScript 6 异步编程

前端随笔 2023/5/11

前言

观阮一峰大佬的《深入掌握 ECMAScript 6 异步编程》 ,记录的笔记,其实也看了很多其他人写的文章,但主要的笔记记录,还得是他。

异步编程的方式

  • 回调函数 就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。
  • 事件监听任务的执行不取决于代码的顺序,而取决于某个事件是否发生
  • 发布/订阅 存在一个”信号中心”,某个任务执行完成,就向信号中心”发布”(publish)一个信号,其他任务可以向信号中心”订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做“发布/订阅模式”
  • Promises对象每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数,本质上还是利用回调函数
  • 生成器函数 Generator/ yield yield表达式可以暂停函数执行,next方法用于恢复函数执行,这使得Generator函数非常适合将异步任务同步化
  • async/await 函数 async 函数是通过generator 和 promise 实现的一个自动执行的语法糖,当函数内部执行到一个await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写。

    协程

    “协程”(coroutine),意思是多个线程互相协作,完成异步任务。生成器函数 Generator/ yield 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。

举例来说,读取文件的协程写法如下:

function *asnycJob() {
  // ...其他代码
 var f = yield readFile(fileA);
  // ...其他代码
}

上面代码的函数 asyncJob 是一个协程,它的奥妙就在其中的 yield 命令。它表示执行到此处,执行权将交给其他协程。也就是说,yield命令是异步两个阶段的分界线。

协程遇到 yield 命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。

异步的关键就是把会阻塞线程函数的执行权交出去,让这个函数等待恢复执行,等待的时间内请求(或者其他异步任务)也该执行完了,这时候再来继续执行这个函数。通过上面对协程的运行方式的讲解我们很容易就能想到用协程来解决这个问题,利用yield挂起这个阻塞线程函数,然后继续执行后面的语句,等这个函数不再阻塞了,再回到这个函数继续执行。那么问题来了,应该什么时候继续执行这个挂起的函数呢?(配合promise,根据promise返回的异步任务状态),所以诞生了async/await 函数,也是目前在工作和项目中使用最多的方式,具体实现后面会说明。

Generator 函数的含义与用法

什么是 Generator 函数

在Javascript中,一个函数一旦开始执行,就会运行到最后或遇到return时结束,运行期间不会有其它代码能够打断它,也不能从外部再传入值到函数体内而Generator函数(生成器)的出现使得打破函数的完整运行成为了可能,其语法行为与传统函数完全不同Generator函数是ES6提供的一种异步编程解决方案,形式上也是一个普通函数,但有几个显著的特征:

  • function关键字与函数名之间有一个星号 “*” (推荐紧挨着function关键字)
  • 函数体内使用 yield 表达式,定义不同的内部状态 (可以有多个yield)
  • 直接调用 Generator函数并不会执行,也不会返回运行结果,而是返回一个遍历器对象(Iterator Object)
  • 依次调用遍历器对象的next方法,遍历 Generator函数内部的每一个状态

执行机制

调用 Generator 函数和调用普通函数一样,在函数名后面加上()即可,但是 Generator 函数不会像普通函数一样立即执行,而是返回一个指向内部状态对象的指针迭代器Iterator),所以要调用遍历器对象Iterator 的 next 方法,指针就会从函数头部或者上一次停下来的地方开始执行

function* func(){ 
    console.log("one"); 
    yield '1'; 
    console.log("two");
    yield '2'; 
    console.log("three");
    return '3'; 
}
f.next(); // one // {value: "1", done: false} 
f.next(); // two // {value: "2", done: false} 
f.next(); // three // {value: "3", done: true} 
f.next(); // {value: undefined, done: true}

第一次调用 next 方法时,从 Generator 函数的头部开始执行,先是打印了 one ,执行到 yield 就停下来,并将yield 后边表达式的值 ‘1’,作为返回对象的 value 属性值,此时函数还没有执行完, 返回对象的 done 属性值是 false。

第二次调用 next 方法时,同上步 。

第三次调用 next 方法时,先是打印了 three ,然后执行了函数的返回操作,并将 return 后面的表达式的值,作为返回对象的 value 属性值,此时函数已经结束,多以 done 属性值为true 。

第四次调用 next 方法时, 此时函数已经执行完了,所以返回 value 属性值是 undefined ,done 属性值是 true 。如果执行第三步时,没有 return 语句的话,就直接返回 {value: undefined, done: true}。

由此可以看出 yield表达式可以暂停函数执行,next方法用于恢复函数执行,这使得Generator函数非常适合将异步任务同步化

async/await 函数的含义与用法

async/await的本质其实就是Generator和Promise的语法糖

async/await的使用

function requestData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(url)
    }, 1000)
  })
}

async function foo() {
  const res1 = await requestData('aaa')
  const res2 = await requestData(res1 + 'bbb')
  const res3 = await requestData(res2 + 'ccc')
  console.log(res3)
}

foo()
//3秒钟后打印aaabbbccc

上面代码用 Generator 函数实现

function requestData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(url)
    }, 1000)
  })
}
function* generatorFoo() {
    const res1 = yield requestData('aaa')
  const res2 = yield requestData(res1 + 'bbb')
  const res3 = yield requestData(res2 + 'ccc')
  console.log(res3)
}
const generate = generatorFoo()
generate.next().value.then(res1 => {
  generate.next(res1).value.then(res2 => {
    generate.next(res2).value.then(res3 => {
      generate.next(res3)
    })
  })
})

因此如果我们把Generator 函数封装一下递归调用实现Generator 的自动迭代,是不是可以实现async/await的效果,所以说async/await的本质其实就是Generator和Promise的语法糖

参考

ES6 Generator 函数 | 菜鸟教程 (runoob.com)

async/await的本质分析 - 掘金 (juejin.cn)

阅读全文

常用数据处理方法(例如对象清空,浮点数计算等)

前端随笔 2023/4/20

一、对象清空 resetData

/**
 * @description 对象清空
 * @param {object} valueObj  传入的要清空的对象
 * @return Object
 */
export const resetData = (valueObj: any) => {
    if (Object.prototype.toString.call(valueObj) === '[object Object]') {
        const keys = Object.keys(valueObj)
        const obj: { [name: string]: string | Array<any> | boolean | number } = {}
        keys.forEach(item => {
            obj[item] =
                Object.prototype.toString.call(valueObj[item]) === '[object Array]'
                    ? []
                    : typeof valueObj[item] === 'number'
                    ? 0
                    : typeof valueObj[item] === 'boolean'
                    ? false
                    : ''
        })
        Object.assign(valueObj, obj)
    } else {
        const keys = Object.keys(valueObj[0])
        const obj: { [name: string]: string | Array<any> | boolean | number } = {}
        keys.forEach(item => {
            obj[item] =
                Object.prototype.toString.call(valueObj[item]) === '[object Array]'
                    ? []
                    : typeof valueObj[item] === 'number'
                    ? 0
                    : typeof valueObj[item] === 'boolean'
                    ? false
                    : ''
        })
        Object.assign(valueObj[0], obj)
    }
}

二、js浮点数乘法处理

/**
 *  js浮点数乘法处理
 * @param {} arg1
 * @param {*} arg2
 */
export const accMul = (arg1: number, arg2?: number) => {
    return arg2 ? Number(Number(arg1 * 100).toFixed(arg2)) : Number(Number(arg1 * 100).toFixed(2))
}

三、js浮点数除法处理

/**
 * js 浮点数除法处理
 * @param {*} arg1
 * @param {*} arg2
 */
export const accDiv = (arg1: number, arg2?: number) => {
    return arg2 ? Number(Number(arg1 / 100).toFixed(arg2)) : Number(Number(arg1 / 100).toFixed(2))
}

四、数字转金额格式

/**
 * 数字转金额格式
 * @param num 要格式化的数字
 * @param decimals 保留位数
 */
export const numberToMoney = (num: number | BigNumber, decimals = 2, thousandsSep = ','): string => {
    if (isNaN(num as any)) {
        return ''
    }
    const prec = !isFinite(+decimals) ? 0 : Math.abs(decimals) // 保留的位数一定是有限位数的正整数
    const sep = typeof thousandsSep === 'undefined' ? ',' : thousandsSep
    const s = new BigNumber(num).toString().replace(/,/g, '') // 字符串,将,变成'';
    const p = new BigNumber(s) // 解析一个字符串,并返回一个浮点数
    const n = p.isNaN() ? 1 : p
    const formatNum = n
        .toFixed(prec)
        .toString()
        .replace(/(\d)(?=(\d{3})+\.)/g, function ($0, $1) {
            return $1 + sep
        })
    return num ? formatNum : ''
}

五、时间戳转日期格式

type dateFormats = 'y-M-d' | 'y-M-d H:m:s' | 'y年M月d日' | 'y年M月d日 H时i分'

/**
 * 时间戳转日期格式
 * @param timestape
 * @param time
 * @returns
 */
export const dateFormat = (timestape: number, formats: dateFormats = 'y-M-d H:m:s'): string => {
    if (!timestape) {
        return ''
    }
    // 补零
    const zero = (val: number): string | number => {
        return val < 10 ? `0${val}` : val
    }
    const myDate = new Date(timestape)
    const year = myDate.getFullYear()
    const month = zero(myDate.getMonth() + 1)
    const day = zero(myDate.getDate())
    const hour = zero(myDate.getHours())
    const minute = zero(myDate.getMinutes())
    const second = zero(myDate.getSeconds())
    // 替换 formats 中的内容
    const replaceDate = (match: string): string => {
        return (
            {
                y: year,
                M: month,
                d: day,
                H: hour,
                m: minute,
                s: second
            } as any
        )[match]
    }
    return formats.replace(/Y|M|d|H|m|s/gi, replaceDate)
}

六、数组去重

const uniqueArr = (arr) => [...new Set(arr)];

console.log(uniqueArr(["1","2","1","2","3","4"]));
// ['1', '2', '3', '4']

七、获取当前h5页面的url参数

一般是h5嵌套到其他如(app,小程序等),双方之前的交互时用到的,在main.ts中直接使用

/**
 * 获取 url 中的参数
 * @param { String } url 获取参数 url
 * @returns { Object } 返回 url 参数
 */
export const getQueryString = (url = '') => {
  const baseUrl = url || window.location.href;
  const _pa = baseUrl.substring(baseUrl.indexOf('?') + 1);
  const _arrS = _pa.split('&');
  const _rs: any = {};
  for (let i = 0; i < _arrS.length; i++) {
    const pos = _arrS[i].indexOf('=');
    if (pos === -1) {
      continue;
    }
    const key = _arrS[i].substring(0, pos);
    const value = decodeURIComponent(_arrS[i].substring(pos + 1));
    _rs[key] = value;
  }
  return _rs;
};

//例如 当前页面为https://www.google.com.hk/search?number=666&string=money
//import { getQueryString } from '这个方法存放的地方';

const {number,string}: any = getQueryString();
console.log(number,string);//666 money

七、从url获取参数并转为对象

和上面不同的时需传入一个url

const getParameters = URL => JSON.parse(`{"${decodeURI(URL.split("?")[1]).replace(/"/g, 
'\\"').replace(/&/g, '","').replace(/=/g, '":"')}"}`)

getParameters("https://www.google.com.hk/search?number=666&string=money");
// {number: '666', string: 'money'}

八、检查对象是否为空

检查对象是否为空,实际上并不那么简单,即使对象为空,每次检查对象是否等于 {} 也会返回 false

const isEmpty = obj => Reflect.ownKeys(obj).length === 0 && obj.constructor === Object;
isEmpty({}) // true 
isEmpty({a:"not empty"}) //false

九、文字复制到剪贴板

Clipboard API 它的所有操作都是异步的,返回 Promise 对象,不会造成页面卡顿。而且,它可以将任意内容(比如图片)放入剪贴板。

const copyText = async (text) => await navigator.clipboard.writeText(text)
copyText('单行代码 前端世界')
阅读全文

vue3常用指令封装使用

项目搭建 2023/4/12

前言

因为需求可能需要频繁发送验证码,文本复制等等的,将这些封装成指令使用是最方便的,自己记个笔记,方便以后要用或者新项目搭建时的时候,c+v大法(手动狗头)

节流指令 v-throttle

/*
  需求:防止按钮在短时间内被多次点击,使用节流函数限制规定时间内只能点击一次。

  思路:
    1、第一次点击,立即调用方法并禁用按钮,等延迟结束再次激活按钮
    2、将需要触发的方法绑定在指令上
  
  使用:给 Dom 加上 v-throttle 及回调函数即可
  <button v-throttle="debounceClick">节流提交</button>
  因为用了el.disabled也就是dom元素的disabled的属性,没disabled属性得使用,是不生效得
*/
import type { Directive, DirectiveBinding } from "vue";
interface ElType extends HTMLElement {
    __handleClick__: () => any;
    disabled: boolean;
}
const throttle: Directive = {
    mounted(el: ElType, binding: DirectiveBinding) {
        if (typeof binding.value !== "function") {
            throw "callback must be a function";
        }
        let timer: NodeJS.Timeout | null = null;
        el.__handleClick__ = function () {
            if (timer) {
                clearTimeout(timer);
            }
            if (!el.disabled) {
                el.disabled = true;
                binding.value();
                timer = setTimeout(() => {
                    el.disabled = false;
                }, 1000);
            }
        };
        el.addEventListener("click", el.__handleClick__);
    },
    beforeUnmount(el: ElType) {
        el.removeEventListener("click", el.__handleClick__);
    }
};

export default throttle;

防抖指令 v-debounce

/**
 * v-debounce
 * 按钮防抖指令,可自行扩展至input
 * 接收参数:function类型
 */
import type { Directive, DirectiveBinding } from "vue";
interface ElType extends HTMLElement {
    __handleClick__: () => any;
}
const debounce: Directive = {
    mounted(el: ElType, binding: DirectiveBinding) {
        if (typeof binding.value !== "function") {
            throw "callback must be a function";
        }
        let timer: NodeJS.Timeout | null = null;
        el.__handleClick__ = function () {
            if (timer) {
                clearInterval(timer);
            }
            timer = setTimeout(() => {
                binding.value();
            }, 500);
        };
        el.addEventListener("click", el.__handleClick__);
    },
    beforeUnmount(el: ElType) {
        el.removeEventListener("click", el.__handleClick__);
    }
};

export default debounce;

长按指令 v-longpress

/**
 * v-longpress
 * 长按指令,长按时触发事件
 */
import type { Directive, DirectiveBinding } from "vue";

const directive: Directive = {
    mounted(el: HTMLElement, binding: DirectiveBinding) {
        if (typeof binding.value !== "function") {
            throw "callback must be a function";
        }
        // 定义变量
        let pressTimer: any = null;
        // 创建计时器( 2秒后执行函数 )
        const start = (e: any) => {
            if (e.button) {
                if (e.type === "click" && e.button !== 0) {
                    return;
                }
            }
            if (pressTimer === null) {
                pressTimer = setTimeout(() => {
                    handler(e);
                }, 1000);
            }
        };
        // 取消计时器
        const cancel = () => {
            if (pressTimer !== null) {
                clearTimeout(pressTimer);
                pressTimer = null;
            }
        };
        // 运行函数
        const handler = (e: MouseEvent | TouchEvent) => {
            binding.value(e);
        };
        // 添加事件监听器
        el.addEventListener("mousedown", start);
        el.addEventListener("touchstart", start);
        // 取消计时器
        el.addEventListener("click", cancel);
        el.addEventListener("mouseout", cancel);
        el.addEventListener("touchend", cancel);
        el.addEventListener("touchcancel", cancel);
    }
};

export default directive;

拖拽指令 v-draggable

/*
    需求:实现一个拖拽指令,可在父元素区域任意拖拽元素。

    思路:
        1、设置需要拖拽的元素为absolute,其父元素为relative。
        2、鼠标按下(onmousedown)时记录目标元素当前的 left 和 top 值。
        3、鼠标移动(onmousemove)时计算每次移动的横向距离和纵向距离的变化值,并改变元素的 left 和 top 值
        4、鼠标松开(onmouseup)时完成一次拖拽

    使用:在 Dom 上加上 v-draggable 即可
    <div class="dialog-model" v-draggable></div>
*/
import type { Directive } from "vue";
interface ElType extends HTMLElement {
    parentNode: any;
}
const draggable: Directive = {
    mounted: function (el: ElType) {
        el.style.cursor = "move";
        el.style.position = "absolute";
        el.onmousedown = function (e) {
            let disX = e.pageX - el.offsetLeft;
            let disY = e.pageY - el.offsetTop;
            document.onmousemove = function (e) {
                let x = e.pageX - disX;
                let y = e.pageY - disY;
                let maxX = el.parentNode.offsetWidth - el.offsetWidth;
                let maxY = el.parentNode.offsetHeight - el.offsetHeight;
                if (x < 0) {
                    x = 0;
                } else if (x > maxX) {
                    x = maxX;
                }

                if (y < 0) {
                    y = 0;
                } else if (y > maxY) {
                    y = maxY;
                }
                el.style.left = x + "px";
                el.style.top = y + "px";
            };
            document.onmouseup = function () {
                document.onmousemove = document.onmouseup = null;
            };
        };
    }
};
export default draggable;

复制指令 v-copy

/**
 * v-copy
 * 复制某个值至剪贴板
 * 接收参数:string类型/Ref<string>类型/Reactive<string>类型
 */
import type { Directive, DirectiveBinding } from "vue";
import  { Toast }  from 'vant';
interface ElType extends HTMLElement {
    copyData: string | number;
    __handleClick__: any;
}
const copy: Directive = {
    mounted(el: ElType, binding: DirectiveBinding) {
        el.copyData = binding.value;
        el.addEventListener("click", handleClick);
    },
    updated(el: ElType, binding: DirectiveBinding) {
        el.copyData = binding.value;
    },
    beforeUnmount(el: ElType) {
        el.removeEventListener("click", el.__handleClick__);
    }
};

function handleClick(this: any) {
    const input = document.createElement("input");
    input.value = this.copyData.toLocaleString();
    document.body.appendChild(input);
    input.select();
    document.execCommand("Copy");
    document.body.removeChild(input);
    Toast({
        type: 'success',
        message: '复制成功',
      })
    
}

export default copy;

全局注册所有自定义指令

directivesIndex.ts:

import { App } from "vue";
import copy from "./modules/copy";
import draggable from "./modules/draggable";
import debounce from "./modules/debounce";
import throttle from "./modules/throttle";
import longpress from "./modules/longpress";

const directivesList: any = {
    copy,
    draggable,
    debounce,
    throttle,
    longpress
};

const directives = {
    install: function (app: App<Element>) {
        Object.keys(directivesList).forEach(key => {
            // 注册所有自定义指令
            app.directive(key, directivesList[key]);
        });
    }
};

export default directives;

在mian.ts引入并使用

//引入
import directivesIndex from 'directivesIndex.ts的路径'

//使用
app.use(router).use(piniaStore).use(directivesIndex).mount('#app')
阅读全文

NestJS搭建项目(跟着掘金大佬的文章学习,自己试着整了个登录注册接口)

项目搭建 2023/3/15

前言

学习NestJS,主要看了
学完这篇 Nest.js 实战,还没入门的来锤我!(长文预警) - 掘金 (juejin.cn),也是跟着整时做一个笔记,大佬讲已经很清晰了,不出意外真入门了,然后试着写了登录注册的接口,成功的那一刻还是挺开心的!!!

安装

我是按照官网的Nest CLI创建的项目,也是官网推荐初学者使用的方式:

若要使用 Nest CLI 构建项目,请运行以下命令。这将创建一个新的项目目录,并使用核心的 Nest 文件和支撑模块填充该目录,从而为项目创建一个传统的基本结构。

$ npm i -g @nestjs/cli
$ nest new project-name

运行完命令后:

1678264171764.png

这里简单说明一下这些核心文件:

src
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── main.ts


| ------------------------ | ------------------------------------------------- |
| `app.controller.ts`      | 单个路由的基本控制器(Controller)                            |
| `app.controller.spec.ts` | 针对控制器的单元测试                                        |
| `app.module.ts`          | 应用程序的根模块(Module)                                  |
| `app.service.ts`         | 具有单一方法的基本服务(Service)                              |
| `main.ts`                | 应用程序的入口文件,它使用核心函数 `NestFactory` 来创建 Nest 应用程序的实例。 |

具体可以去看这位大佬写的文章,非常好!我后面也是跟着这篇文章学习的

怎么连接数据库,接口规范统一,swagger等我都是按下面链接的大佬的文章学习的,然后也算是入了门,才开始试试登录注册的。

学完这篇 Nest.js 实战,还没入门的来锤我!(长文预警) - 掘金 (juejin.cn)

登录注册

全局路由前缀

上面链接的大佬有提过,在main.ts

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.setGlobalPrefix('api'); // 设置全局路由前缀
  await app.listen(8080);
}
bootstrap();

此时之前的路由,都要变更为:http://localhost/api/xxxx

创建 USER 模块

nest g mo modules/user //创建moudle
nest g co modules/user //创建控制器
nest g s modules/user //创建服务

注意创建顺序: 先创建Module, 再创建Controller和Service, 这样创建出来的文件在Module中自动注册,反之,后创建Module, Controller和Service,会被注册到外层的app.module.ts

1678265171776.png

user表实体

新建 user.entity.ts 实体,到时候可以通过TypeORM将实体映射到数据库表

// 用户实体类
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Exclude } from 'class-transformer';

@Entity('User')
export class UserEntity {
  // 用户id
  @PrimaryGeneratedColumn()
  id: number; // 标记为主列,值自动生成

  // 昵称
  @Column({default:''})
  nickname: string;

  //头像
  @Column({default:'https://i.postimg.cc/wjcFjQMD/b.png'})
  avatar: string;
  
  // 手机号
  @Column('text')
  mobile: string;

  // 创建时间
  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  create_time: Date;

  // 更新时间
  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  update_time: Date;

  // 加密后的密码
  @Column('text', { select: false })
  password: string;

  // 加密盐
  @Column('text', { select: false })
  salt: string;
}

user.dto

Nest.js自带了三个开箱即用的管道:ValidationPipe、ParseIntPipe和ParseUUIDPipe, 其中ValidationPipe 配合class-validator就可以完美的实现我们想要的效果(对参数类型进行验证,验证失败抛出异常)。

新建user.dto.ts用于校验,完善错误提示信息

import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';

export class RegisterDTO {
  @ApiProperty({ description: '昵称' })
  readonly nickname: string;

  @ApiProperty({ description: '头像' })
  readonly avatar: string;

  @ApiProperty({ description: '手机号' })
  readonly mobile: string;

  @IsNotEmpty({ message: '密码没填' })
  @ApiProperty({ description: '密码' })
  readonly password: string;
}

export class LoginInfoDTO {
  @IsNotEmpty({ message: '手机号没填' })
  @ApiProperty({ description: '手机号' })
  readonly mobile: string;

  @IsNotEmpty({ message: '密码没填' })
  @ApiProperty({ description: '密码' })
  readonly password: string;
}

user控制层

在user.controller.ts 控制层定义接口

import { Body, Controller, Post } from '@nestjs/common';
import { ApiOperation } from '@nestjs/swagger';
import { UserService } from './user.service';

@Controller('user')
export class UserController {
  constructor(private readonly userService:UserService) {

  }
  //注册
  @ApiOperation({ summary: '注册用户' })
  @Post('register')
  async register(@Body() createUser:any) {
     return await this.userService.register(createUser);
   }

  //登录
  @ApiOperation({ summary: '登录' })
  @Post('login')
  async login(@Body() loginInfo:any) {
    return await this.userService.login(loginInfo);
  }  
}

user业务层

在user.service.ts写登录注册相关的业务逻辑

import { BadRequestException, HttpException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { encryptPassword, makeSalt } from 'utils/cryptogram';
import { RegisterDTO , LoginInfoDTO} from './user.dto';
import { UserEntity} from './user.entity'
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(UserEntity)
    private readonly userRepository: Repository<UserEntity>,
    private readonly jwtService: JwtService
  ) {}
    /**
   * 注册
   * @param  mobile 手机号 
   * @param  password 密码 
   */
    async register(createUser:RegisterDTO):Promise<any>{
      const {mobile,password} = createUser;
      if (!mobile) {
        throw new HttpException('入参缺少mobile', 401);
      }
      const existUser = await this.userRepository.findOne({where: {mobile}});
      if (existUser) {
        throw new HttpException('该用户已注册', 401);
      }
      const newUser = await this.userRepository.create(createUser)
      const salt = makeSalt(); // 制作密码盐
      const hashPassword = encryptPassword(password, salt);  // 加密密码
      newUser.password = hashPassword
      newUser.salt = salt
      // this.userRepository.create(createUser)相当于new User(createUser)只是创建了一个新的用户对象
      // save方法才是执行插入数据
      return await this.userRepository.save(newUser);
    }

    /**
   * 登录检验
   * @param  mobile 手机号 
   * @param  password 密码 
   */
    async checkLoginForm(loginInfo:LoginInfoDTO):Promise<any> {
      const {mobile,password} = loginInfo;
      const user = await this.userRepository
        .createQueryBuilder('user')
        .addSelect('user.salt')
        .addSelect('user.password')
        .where('user.mobile = :mobile', { mobile })
        .getOne()
      if (!user) {
        throw new BadRequestException('用户名不正确!');
      }
      const currentHashPassword = encryptPassword(password, user.salt)
      if (currentHashPassword !== user.password) {
        throw new BadRequestException('密码错误')
      }
      return user
    }

   /**
   * 生成token
   * @param  UserEntity 用户实体类
   */
  async certificate(user:UserEntity){
    const payload = { 
      id: user.id,
      mobile: user.mobile,
    };
    const token = this.jwtService.sign(payload);
    return token
  }

   /**
   * 登录
   */
  async login(loginInfo:LoginInfoDTO):Promise<any> {
    const user = await this.checkLoginForm(loginInfo)
    const token = await this.certificate(user)
    return {
        token
    }
  }
}

其中需要注意jwt和user.entity的实体需要在user.module.ts中引入,然后把user.module暴露出去给app.module暴露出去给app导入,这样就建立了模块之间的关系,毕竟主模块是app.moudle

import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from './user.entity';
import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [
    TypeOrmModule.forFeature([UserEntity]),
    JwtModule.register({
      secret: 'dasdjanksjdasd', // 密钥
      signOptions: { expiresIn: '8h' }, // token 过期时效
    }),
  ],
 
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

成果

注册成功

1678415253600.png

登录成功,返回token

1678415198199.png

阅读全文

在开发h5移动端项目(v3+vant3+pinia+vite)时遇到的问题总结(二)

一、h5页面嵌套在小程序中时,系统设置了深色模式,会出现背景色冲突、深色文字显示异常,深色图标显示异常等一些显示上的问题。

因为我司的项目,当时立项时并没有考虑适配深色模式,所以导致了这种情况
解决方式:

在相应的受深色模式影响的页面最外层div加上css background设置为白色(因为我司项目,背景基本是白色所以这样改能行,但应该是不是通用的),当时是快上线了,发现这个问题,急着上就没去整适配,只能用这种办法。

具体的适配可以看看:
h5适配深色模式 - 搜索 - 掘金 (juejin.cn)

二、前端文本复制功能的实现

安装 vue-clipboard3

$ npm install --save vue-clipboard3

使用 vue-clipboard3

<button @click="copy(复制的内容)">复制的内容</button>


// 导入插件
import useClipboard from 'vue-clipboard3'

const { toClipboard } = useClipboard() 
const copy = async (msg) => { 
    try { 
    // 复制 await toClipboard(msg) 
    // 复制成功 
    } 
    catch (e) {
    // 复制失败 
    } 
}   

三、v3+vant3的图片上传

   <van-uploader
          :after-read="afterRead"
          :before-read="beforeRead"
          :before-delete="beforeDelete"
          v-model="cardForm.imgList"
          :max-count="3"
          preview-size="2.4rem"
          upload-text="最多3张"
        >
            <template #preview-delete>
                <img :src="getAssetsFile('iconDelete.png')" 
                :style="{ width: $px2rem('18px'), height: $px2rem('18px')}">
            </template>
        </van-uploader> 

js部分

//图片上传前做判校验
  const beforeRead = (file: any) => {
    const type = ['image/jpeg' ,'image/png','image/jpg']
    const isImage = type.includes(file.type)
    const isLt5M = file.size / 1024 /1024 < 5
    if(!isImage) {
      Toast('请上传 jpg,png,jpeg 格式图片!');
    } 
    if(!isLt5M) {
      Toast('图片大小不能超过 5MB!');
    }
    return isImage&&isLt5M
  };
  //图片删除前
  const beforeDelete = (file:any,i:any) => {
    PicListParams.value.splice(i.index,1)
    return true;
  };
  //图片上传服务器
  const afterRead = async(file: any) => {
    console.log(file);
    file.status = 'uploading';
    file.message = '上传中...';
    const formData = new FormData();
    formData.append('file', file.file);
    //调用图片上传的接口
    //成功 file.status = 'none';并把返回的图片push
    //失败 file.status = 'fail'
  };

四、vant3表单使用textarea+autosize完成表单高度的自适应时,出现滚动条

解决方式:给vant3的表单的van-field__control加上 overflow-y: hidden

      ::v-deep(.van-field__control) {
        overflow-y: hidden;
      }

五、localstorage只能存字符串,存对象时需要转化成json格式,用时记得转化回来

这个其实自己一直知道这个知识点,平时也嘎嘎在用,但是开发着开发着突然不知怎么脑袋抽筋了,忘转化了,导致localstorage.getItem获取的值一直没取到,第一时间还没考虑到是localstorage的原因,浪费了一些时间找问题所在,本来不想把这个当笔记记录的,但是为了让自己涨涨记性还是记下来。

const object = reactive({
    name:'涨涨',
    age:18,
})
//用localstorage存值时
locastorage.setItem('object', JSON.stringify(object))

////用localstorage取值时
const JosnObject1 = locastorage.getItem('object')
const Object1 = Json.parse(JosnObject1)

六、小程序返回使用H5页面返回,放弃小程序自带返回按钮

最开始准备在小程序上使用自定义导航栏,但是自定义导航栏并不生效,取官网才发现:

企业微信截图_16762707878468.png

既然设置不了自定义导航栏,而webview组件又自带导航,我又要放弃小程序自带返回按钮,于是另辟蹊径,webview组件自带导航栏背景颜色是白色,那我把返回按钮和文字都设置为白色,这样就看不到了,用户要点应该也不会取点一个看不到的返回按钮

在小程序的page.json,对应的weiview的页面设置:

  {
          "path": "webview/index",
          "style": {
            "navigationBarTextStyle": "white"
          }
        }

七、小程序嵌套h5项目时(使用的webview),从h5返回解决方式

最开始把这个逻辑分析下,不就是h5跳小程序吗?调接口跳转就行了,但去官网看了看因为使用的webview嵌套在小程序中,返回小程序提供了专门的API,调接口跳小程序真的是脱裤子放屁,多此一举(手动狗头)

解决方式:
在项目的index.html中引入微信官方提供的js-jdk,,就可以在页面中使用和 微信相关的 API

 <script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.3.2.js"></script>

使用:

  wx.miniProgram.navigateBack() 

八、在微信小程序中使用webview嵌套h5,跳转三方后,从三方返回到自己的h5页面时原本存在locastorage和sessionstorage里的数据全没了

原因分析:

微信小程序中的 WebView

小程序的主要开发语言是 JavaScript ,其中 逻辑层 和 渲染层 是分开的,分别运行在不同的线程中,而其中的渲染层就是运行在 WebView 上:

运行环境 逻辑层 渲染层
iOS JavaScriptCore WKWebView
安卓 V8 chromium 定制内核
小程序开发者工具 NWJS Chrome WebView
  • 在真机中,需要实现同一域名下不同子路径的应用实现数据交互(纯前端操作,不涉及接口),由于同域名且是基于同一个页面进行跳转的(当然只是看起来是),而且这个数据是 临时数据,因此觉得使用 sessionStorage 实现数据交互是很合适的
  • 实际上从 A 应用 跳转到 B 应用 中却无法获取对应的数据,而这是因为 sessionStorage 是基于当前窗口的会话级的数据存储,移动端浏览器 或 微信内置浏览器 中在跳转新页面时,可能打开的是一个新的 WebView,这就相当于我们在浏览器中的一个新窗口中进行存储,因此是没办法读取在之前的窗口中存储的数据

解决方式:
存了的东西没了,就想办法从三方回来时再存一遍呗。

九、vite获取环境变量遇到的坑

在根目录下的.env.prod文件中定义了需要的各种变量,其中定义了NODE_ENV和VITE_APP_ENV用来获取环境变量,发现import.meta.env.NODE_ENV为undefined,于是去官网看了看,只有VITE_前缀的变量才获取的到

NODE_ENV = 'production'
VITE_APP_ENV= 'production'
conlose.log('import.meta.env.VITE_APP_ENV',import.meta.env.VITE_APP_ENV)  // 'production'
conlose.log('import.meta.env.NODE_ENV ',import.meta.env.NODE_ENV ) //undefined

d2a09598800238e6e9cbedfe0e79596.png

阅读全文

常用的css函数式伪类

前端随笔 2023/2/17

前言

能用css一步解决的,尽量少用js,一些合适的 CSS 甚至能让我们少写很多 JS,之前在工作中遇到了一个问题,本来准备用js实现的,在百度娘的科普下,使用某个CSS 函数式伪类就能实现,发现比自己预想的使用js方便多了,于是我就来多多了解了解一些常用的CSS 函数式伪类,此文做个笔记!(如有雷同,纯属抄袭,手动狗头)

一、逻辑组合 伪类

1. :not

:not 匹配不符合一组选择器的元素。

例如一个场景,每个列表项需要添加下边框线,一般最后一项是不需要的。通常我们会为每一项都设置下边框线,再单独设置最后一项的 border-bottom 为 0。使用 :not 也可以实现。

.item{
  width: 200px;
  height: 100px;
  padding-bottom: 10px;
}
.item:not(:last-child) {
  border-bottom: 1px solid #ccc;
}

2. :has

:has 表示满足一定条件后,就会匹配该元素。这个据说是几十年来最激动人心的CSS发展开发人员终于有了一种针对父元素的方法。

例如一个场景,在必填项的前面加上红色的星号

label:has(+input:required)::before{
  content: '*';
  color: red;
}

例如:父元素里有某个子元素时改变父元素的高

.father :has(.son) {
  height: 300px
}

3. :is

可以匹配一组选择器中的任意一个或多个,并把最终的选择器视为匹配到的那一个。:is()伪类函数大幅度缩减了选择器列表的字符数,简化了选择器列表的复杂度,降低了书写选择器列表出错的概率,注意:is() 不能与::before 和::after 伪元素相匹配

.test>img,.test1>img,.test2>img,.test3>img{}

上面的可简化为:

 :is(.test,.test1,.tes2,.test3)>img{}

4. :empty

:empty 匹配没有子元素的元素。有时候列表的外层会包一层盒子,设置 padding 边距。当列表无数据返回时,外层盒子的 padding 会占用空间,使用 :empty 匹配无子元素时隐藏盒子,解决占用位置的问题。

.wrapper{
  padding: 10px;
}
.wrapper:emtry{
  display: none;
}

二、用户行为 伪类

1. :hover

selector:hover 表示匹配鼠标经过的selector选择器元素。**:hover不是特备适用于移动端,虽然也能触发,但消失并不敏捷,体验反而奇怪。**

例如:鼠标经过元素时的样式变化,Tips提示,下拉列表和过渡动画等

2. :active

selector:active 表示匹配激活状态的selector选择器元素(通过鼠标主键点击或触屏触摸的过程中触发样式,结束后还原样式),支持任意html元素

3. :focus

selector:focus 表示匹配聚焦状态的selector选择器元素。

例如:表单聚焦改变样式

4. :focus-within

selector:focus-within 表示当前selector选择器元素或者其子元素聚焦时都会匹配(而focus只会匹配对应元素本身)

例如:form表单中任一表单元素聚集时让所有表单元素前面文字高亮

form:focus-within label{ color:darkblue; text-shadow: 0 0 1px; }

三、树结构伪类 伪类

1. :root

该伪类匹配的就是html根元素标签。由于现如今浏览器对CSS变量的支持,对于类似整站颜色,布局尺寸这样的变量,业界约定俗成变量由:root伪类负责,而html选择器负责样式。

2.:first-child和:last-child

selector:first-child 表示匹配作为其父元素的第一个子元素的selector元素

selector:last-child 表示匹配作为其父元素的最后一个子元素的selector元素

对列表元素的第一个/最后一个子元素的通用样式的重置。例如:最后一个子元素和第一个元素不要底部边框。

li:first-child{ border-bottom:none }
li:last-child{ border-bottom:none }

3. :nth-child()和nth-last-child()

selector:nth-child(n) 表示匹配第n个子元素的selector元素

selector:nth-last-child(n) 表示匹配从后往前数的第n个子元素的selector元素

  • selector:nth-child(odd或even)表示匹配作为其父元素的第奇数/偶数个子元素的selector元素

  • selector:nth-child(An+B)表示匹配符合对应规则的子元素。

    其中A,B,n均为不为负的整数,但A前面可以添加负号。

示例:

 nth-child(5n)匹配第5,10,15...个子元素
 
 nth-child(3n+4)匹配第4,7,10...个子元素
 
 nth-child(-n+3)匹配前3个子元素
 
 nth-last-child(-n+3)匹配最后3个子元素

4. :first-of-type和:last-of-type

selector:first-of-type 表示匹配与selector元素标签类型一致的第一个子元素

selector:last-of-type 表示匹配与selector元素标签类型一致的最后一个子元素

5. :nth-of-type()和nth-last-of-type()

selector:nth-of-type(n) 表示匹配与selector元素标签类型一致的子元素集合里第n个子元素

selector:nth-last-of-type(n) 表示与selector元素标签类型一致的子元素集合里的从后往前数的第n个子元素

参考文献

CSS常用伪类选择器详解 - 掘金 (juejin.cn)

阅读全文

js常用方法

前端随笔 2023/2/10

一、对象和字符串常用的方法

字符串常用的方法

1.indexOf(): 检索字符串,返回的是字符在字符串的下标

2.concat():连接字符串

3.match():在字符串内检索指定的值或找到一个或多个正则表达式的匹配,返回的是值而不是值的位置。

4.replace():替换匹配的字符串

5.search():检索与字符串匹配的子串,返回的是第一个字符所在的位置

6.split():把字符分割成数组

7.substr():从起始索引号提取字符串中指定书目的字符

对象常用的方法

1.Object.assign()

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。常用来合并对象。

assign其实是浅拷贝而不是深拷贝,如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。

const obj1 = { a: 1, b: 2 }
const obj2 = { b: 4, c: 5 }
const obj3 = Object.assign(obj1, obj2)
const obj4 = Object.assign({}, obj1) // 克隆了obj1对象

2.Object.keys() 对象的下标

3.Object.values() 对象的值

3.is方法和(===)功能基本类似,用于判断两个值是否绝对相等。

Object.is(1,1);//true
Object.is(1,true);//false
Object.is([],[]);//false
Object.is(+0,-0);//false
Object.is(NaN,NaN);//true

二、javascript高阶函数

1.map()

定义:map()方法定义在JavaScript的Array中,它返回一个新的数组,数组中的元素为原始数组调用函数处理后的值。

注意:map()不会对空数组进行检测,map()不会改变原始数组

语法:array.map(function(currentValue, index, arr), thisIndex)

参数说明:

currentValue:必须。当前元素的的值。 index:可选。当前元素的索引。 arr:可选。当前元素属于的数组对象。

thisIndex:可选。对象作为该执行回调时使用,传递给函数,用作”this”的值。

2.reduce

定义:reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值

注意: reduce() 对于空数组是不会执行回调函数的和map一样。

语法:array.reduce(function(total, currentValue, currentIndex, arr), initialValue)

参数说明:

total 必需。初始值, 或者计算结束后的返回值。 currentValue 必需。当前元素 currentIndex 可选。当前元素的索引 arr 可选。当前元素所属的数组对象。

totalAge = users.reduce((total, user) => user.age + total, 0);
console.log(totalAge);

3.filter

定义:filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。

注意: filter() 不会对空数组进行检测。

注意: filter() 不会改变原始数组。

语法 array.filter(function(currentValue,index,arr), thisValue)

参数说明:

currentValue 必须。当前元素的值 index 可选。当前元素的索引值 arr 可选。当前元素属于的数组对象

startsWithB = (string) => string.toLowerCase().startsWith('b');
namesStartingWithB = users.filter((user) => startsWithB(user.name));
console.log(namesStartingWithB);
// [{ "name": "Bill", "age": 20 }]
阅读全文
1 ... 6 7 8 ... 9
avatar
耀耀切克闹

欢迎来到耀耀切克闹的博客

不是因为是主角才成功,而是成功了才成为主角

想要的都拥有,得不到的都释怀