ai-要約を取得 文章摘要

Vue3.2+Vite+Typescript 开发双端招聘全过程系列导航

  1. 基础环境配置(一)⇦当前位置🪂
  2. 可复用的页面交互逻辑(二)
  3. 个人中心模块(三)
  4. 即将闪亮登场..

项目创建

两种项目创建方式现在都是基于 vite

vue3 - 官方推荐 create vue

create-vue 是一个官方的 Vue 项目脚手架工具,用来快速创建 Vue 3 项目。默认情况下,它使用 Vite 作为构建工具

pnpm create vue@latest
✔ Project name: … <your-project-name>
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit testing? … No / Yes
✔ Add an End-to-End Testing Solution? … No / Cypress / Nightwatch / Playwright
✔ Add ESLint for code quality? … No / Yes
✔ Add Prettier for code formatting? … No / Yes
✔ Add Vue DevTools 7 extension for debugging? (experimental) … No / Yes

Scaffolding project in ./<your-project-name>...
Done.
// 如果不确定是否要开启某个功能,你可以直接按下回车键选择 No。在项目被创建后,通过以下步骤安装依赖并启动开发服务器:
cd <your-project-name>
pnpm install
pnpm run dev

// 此命令会在 ./dist 文件夹中为你的应用创建一个生产环境的构建版本。
pnpm run build

Vite を使う理由

問題点

ES モジュールがブラウザーで利用できるようになるまで、開発者はモジュール化された JavaScript を生成するネイティブの仕組みを持っていませんでした。これは、私たちが「バンドル」のコンセプトに慣れ親しんでいる理由でもあります: すなわち、ブラウザーで実行可能なようにソースモジュールをクロール、処理し、連結するツールを使用しています。

時を経て webpackRollupParcel のようなツールが登場し、フロントエンド開発者の開発体験は大きく向上されました。

しかしながら、大規模なアプリケーションが作られるようになってくると、取り扱う JavaScript の量は劇的に増加しました。大規模プロジェクトでは、数千ものモジュールが含まれることも珍しくありません。JavaScript ベースのツールを使用していては、いずれパフォーマンスのボトルネックにぶつかります: 開発サーバーを起動するのにやたらと長く待つこともあります(数分かかることさえ!)。また、Hot Module Replacement(HMR)を利用していても、ファイル編集がブラウザーに反映されるまで数秒かかることもあります。フィードバックの遅さが継続することは、開発者の生産性や幸福度に大きな影響を与える可能性があります。

Vite では新しいエコシステムの進歩を活用し、これらの問題を解決することに取り組んでいます: ブラウザーのネイティブ ES モジュールや、ネイティブにコンパイルされる言語で書かれた先進的な JavaScript ツールの利用です。

现实问题

在浏览器支持 ES 模块之前,JavaScript 并没有提供原生机制让开发者以模块化的方式进行开发。这也正是我们对 “打包” 这个概念熟悉的原因:使用工具抓取、处理并将我们的源码模块串联成可以在浏览器中运行的文件。

时过境迁,我们见证了诸如 webpackRollupParcel 等工具的变迁,它们极大地改善了前端开发者的开发体验。

然而,当我们开始构建越来越大型的应用时,需要处理的 JavaScript 代码量也呈指数级增长。包含数千个模块的大型项目相当普遍。基于 JavaScript 开发的工具就会开始遇到性能瓶颈:通常需要很长时间(甚至是几分钟!)才能启动开发服务器,即使使用模块热替换(HMR),文件修改后的效果也需要几秒钟才能在浏览器中反映出来。如此循环往复,迟钝的反馈会极大地影响开发者的开发效率和幸福感。

Vite 旨在利用生态系统中的新进展解决上述问题:浏览器开始原生支持 ES 模块,且越来越多 JavaScript 工具使用编译型语言编写。

遅いサーバー起動

開発サーバーがコールドスタートするとき、バンドラーベースのビルドセットアップは、アプリケーション全体を提供する前に、アプリケーション全体を隅々までクロールしてビルドする必要があります。

Vite はまず最初にアプリケーションのモジュールを 2 つのカテゴリーに分割することで、開発サーバーの起動時間を改善します: 依存関係ソースコードです。

  • 依存関係の大部分は開発中あまり変更されないプレーンな JavaScript です。巨大な依存関係の中には、処理コストが極めて高いものがあります(例: 100 ものモジュールを持つコンポーネントライブラリー)。依存関係は、様々なモジュール形式で出力されることがあります(例: ESM または CommonJS)。

    Vite は、esbuild を使用して依存関係の事前バンドルを行います。esbuild は Go 言語によって開発されており、依存関係の事前バンドルは、JavaScript ベースよりも 10 倍から 100 倍高速です。

  • ソースコードには変換を必要とするプレーンな JavaScript ではないものが含まれることがよくあり、頻繁に編集されます(例: JSX、CSS や Vue/Svelte コンポーネント)。また、全てのソースコードを同時に読み込む必要はありません(例: ルーティングによるコード分割)。

    Vite は、ネイティブ ESM を行使してソースコードを提供します。ブラウザーは、実質的にバンドラーの仕事の一部を引き受けます: Vite はブラウザーのリクエストに応じて、ソースコードを変換し提供するのみになります。条件で囲われている動的インポートのコードは、現在の画面で使われる場合のみ処理されます。

バンドルベースの開発サーバーentry···routeroutemodulemodulemodulemodule···BundleServer ready

ネイティブ ESM ベースの開発サーバーentry···routeroutemodulemodulemodulemodule···Server ready動的インポート (コード分割点)HTTP リクエスト

遅い更新速度

バンドラーベースのビルドセットアップでファイルが編集されたとき、全てのバンドルを再構築することが非効率なことは明白です: 更新スピードはアプリケーションのサイズに応じて線形的に低下してしまうからです。

バンドラーの中には開発サーバーがメモリー上でバンドルを実行し、ファイルが変更されたときにモジュールグラフの一部のみ無効化して再処理を行うものも存在しますが、それでもバンドル全体を再構築して、ウェブページをリロードしなければなりません。バンドルの再構築にはコストがかかりますし、ページがリロードされるとアプリケーションの現在の状態は消えてしまいます。そのため、幾つかのバンドラーは HMR(ホットモジュールリプレースメント)をサポートしています: これにより、ページの変更に関係のない部分には影響を与えることなく、モジュールを「ホットリプレース」することができます。これは開発者体験を大きく改善します。- しかしながら、実際には HMR でもアプリケーションが大きくなるにつれ更新速度が著しく悪化することが分かってきました。

Vite では、HMR をネイティブ ESM 上で行います。ファイルが編集されたとき、Vite は編集されたモジュールと最も近い HMR バウンダリ間のつながりを正確に無効化することで(大抵はモジュール本体だけです)、HMR による更新はアプリケーションのサイズに関係なく一貫して高速で実行されます。

また、Vite は HTTP ヘッダーを活用して、フルページのリロードも高速化します(ここでも、ブラウザーにはもっと働いてもらいます): ソースコードモジュールのリクエストでは 304 Not Modified を利用して条件が作成されます。そして、依存モジュールのリクエストでは、一度キャッシュされたものが再びサーバーにヒットしないよう、Cache-Control: max-age=31536000,immutable を利用して積極的にキャッシュされます。

超高速な Vite を一度体験してしまうと、バンドルでの開発にまた耐えられるかはとても疑わしいです。

プロダクションではバンドルする理由

ネイティブ ESM が広くサポートされるようになっても、バンドルされていない ESM をプロダクション用にリリースすることは非効率です(HTTP/2 を利用していても)。これは、ネットワークのラウンドトリップの増加がネストされたインポートによって引き起こされるためです。プロダクションでは最適化されたローディングパフォーマンスを得るために、ツリーシェイキングや遅延読み込み、(キャッシュ改善のための)共通コード分割などの技術を用いつつバンドルを行うことは、より良いことです。

開発サーバーとプロダクションビルド間で、最適化された出力と一貫した動作を確保することは容易なことではありません。そのため、Vite にはあらかじめ調整されたビルドコマンドが用意されており、これには従来の常識を破る多くのパフォーマンス最適化が施されています。

なぜ esbuild でバンドルしないのか?

Vite は 開発環境で一部の依存関係を事前バンドルするために esbuild を活用していますが、本番ビルドのためのバンドラーとしては esbuild を利用しません。

Vite の現在のプラグイン API は、esbuild をバンドラーとして使用することと互換性がありません。esbuild の方が速いにもかかわらず、Vite は Rollup の柔軟なプラグイン API とインフラストラクチャーを採用し、エコシステムでの成功に大きく貢献しました。当面は、Rollup の方がパフォーマンスと柔軟性のトレードオフに優れていると考えています。

Rollup はパフォーマンスの向上にも取り組んでおり、v4 でパーサーを SWC に切り替えました。 そして、Rolldown と呼ばれる Rollup の Rust ポートを構築する取り組みが進行中です。 Rolldown の準備が完了すると、Vite の Rollup と esbuild の両方が置き換えられ、ビルドのパフォーマンスが大幅に向上し、開発とビルドの間の不一致が解消されます。 詳細については、Evan You の ViteConf 2023 基調講演 をご覧ください。

什么是TypeScript?

TypeScript 是具有类型语法的 JaveScript - 点击查看详情

示例1.

type Result = "pass" | "fail"

function verify(result: Result) {
if (result === "pass") {
console.log("Passed")
} else {
consle.log("Failed")
}
}

示例 2.

const hello = names =>{
console.log(`hi,${names}`)
}
hello('luffy')

// 以上相当于定义了一个函数 hello,它接收一个参数 names,并在函数体中使用该参数来输出消息。
// 以下为原始写法

function hello(names) {
console.log(`hi, ${names}`);
}
hello('luffy')

// 两者在定义参数上本质是一样的,都是在函数调用时传入参数。但箭头函数的语法更简洁,特别是当只有一个参数且函数体只有一行时,可以直接省略大括号 {}

示例 3.

// 函数参数类型
const hello = (name: string) => {
console.log(`hi, ${names}`)
}
hello0(666)

基础概念.

// 基础类型: 字符串, 数字, 布尔
const str: string = 'Hello world'
const num: number = 1
const bool: boolean = true

//复杂类型: 数组, 对象
const arr: number[] = [1, 2, 3]
arr[0] = '0'

const obj: { x: number; y: number } = { x: 1, y: 1 }
const optionalObj: { x: number; y?: number } = { x: 1 }
optionalObj.y = 'y'
optionalObj.y = 2
optionalObj.z = 2


// 对象类型: 匿名, 接口 interface, 类型别名 type
// 接口
interface IType {
str: string
}

// 类型别名
type TType = {
str: string
}

// 类型推断
let n = 1

// 类型断言
console.log((x.contents as string).toLowerCase())

// 泛型, 包括泛型对象类型\泛型函数\泛型类

interface IGeneric<Type>{
test: Type
}

// 使用 IGeneric,并指定 Type 为 string 类型
const stringGeneric: IGeneric<string> = {
test: "Hello",
};

// 使用 IGeneric,并指定 Type 为 number 类型
const numberGeneric: IGeneric<number> = {
test: 123,
};

// 特殊类型: void\any\unknow\never

初始化项目

create vue 基于vite 内置了 ts 依赖 可忽略这个步骤 - 点击查看详情
// 0. 初始化项目,新建package.json
mkdir test
npm init

// 1. 安装 typescript
npm install typecript -D

如果 npm 慢,可以使用 cnpm
npm install -g cnpm

// 2.将 ts 编译成 js
// 其中 npx 允许使用安装在局部的包
npx tsc 文件名.ts

// 3. 新建一个 tsconfig.json
npx tsc --init

git协作

点击查看详情

我们在开发大型项目时,会同时开发多个需求

git 提供了 branch(分支)功能 - 本质为指向 commit 节点对象的一个指针,当我们新建分支时,实际上是新建了一个指针并指向了当前 commit

分支操作

  • git branch:列出本地分支。
  • git branch <branch-name>:创建新分支。
  • git checkout <branch-name>:切换到指定分支。
  • git checkout -b <branch-name>:创建并切换到新分支。
  • git merge <branch-name>:合并其他分支到当前分支。
  • git branch -d <branch-name>:删除本地分支。

大型项目,往往需要跟其他人一起协作

  1. git 分支合并

将一个分支上的代码合并进当前分支

git fetch origin 是用于从远程仓库获取最新的提交到本地的命令。具体含义如下:

  • git fetch:从远程仓库中抓取(fetch)所有分支的更新,获取远程仓库中的最新提交和引用(tags、branches 等),但不会修改本地工作目录,也不会自动合并这些更新。
  • origin:是默认远程仓库的名称,表示你想要从 origin 这个远程仓库获取更新。通常,origin 是你在 git clone 时默认创建的远程仓库别名。
  1. 使用场景

git fetch origin 常用于以下场景:

  • 你想了解远程仓库的最新变化,但暂时不想合并这些更改到当前分支中。
  • 配合其他命令(如 git mergegit rebase)来手动处理从远程分支获取的更新。
  1. 命令工作流程
  • 执行 git fetch origin,Git 会连接到 origin 远程仓库并下载所有分支的最新提交和引用。
  • 这些更新会保存到你本地仓库的远程分支中,例如 origin/main,而不会影响你当前的分支或工作目录。
  1. 初始化和克隆仓库
  • git init:初始化一个新的 Git 仓库。
  • git clone <repository-url>:克隆远程仓库到本地。
  1. 查看仓库状态
  • git status:查看当前分支的状态,显示已修改但未提交的文件。
  • git log:查看提交历史。
  • git diff:显示未提交的变更。
  1. 分支操作
  • git branch:列出本地分支。
  • git branch <branch-name>:创建新分支。
  • git checkout <branch-name>:切换到指定分支。
  • git checkout -b <branch-name>:创建并切换到新分支。
  • git merge <branch-name>:合并其他分支到当前分支。
  • git branch -d <branch-name>:删除本地分支。
  1. 提交和推送代码
  • git add <file>:添加文件到暂存区。
  • git add .:添加所有修改的文件到暂存区。
  • git commit -m "<message>":提交暂存区中的文件,附带提交信息。
  • git push:将本地分支推送到远程仓库。
  • git push origin <branch-name>:将指定分支推送到远程仓库。
  1. 更新和拉取代码
  • git pull:从远程仓库拉取最新的更改,并合并到当前分支。
  • git fetch:拉取远程仓库的最新内容但不合并,通常与 git merge 一起使用。
  1. 冲突解决
  • git mergetool:使用外部工具来帮助解决合并冲突。
  • git diff --base <file>:查看冲突文件的基础版本。
  • git add <file>:在解决冲突后,使用该命令标记冲突已解决。
  1. 远程仓库管理
  • git remote -v:查看当前配置的远程仓库。
  • git remote add <name> <url>:添加新的远程仓库。
  • git remote rm <name>:删除指定的远程仓库。
  1. 回滚和撤销
  • git reset --hard <commit-hash>:将当前分支重置到指定提交。
  • git revert <commit-hash>:撤销指定提交,并生成一个新的提交来记录此次撤销。
  • git reset HEAD <file>:将文件从暂存区移除,但保留修改。
  1. 协作工作流相关
  • git stash:临时保存当前工作区的修改,用于切换分支或拉取代码时避免冲突。
  • git stash pop:恢复保存的工作区修改。
  • git rebase <branch-name>:将当前分支变基到指定分支之上。
  1. 标签
  • git tag <tag-name>:为当前提交打标签。
  • git push origin <tag-name>:将标签推送到远程仓库。

git hooks

点击查看详情

什么是 git hooks

Git hooks 是 Git 提供的一种机制,允许在 Git 仓库中的特定事件发生时执行自定义脚本。它可以帮助开发者在进行一些操作时触发自动化流程,例如提交代码前自动检查代码风格、提交后通知某个服务等。

Git hooks 的作用

Git hooks 是本地钩子,它们与代码仓库一起存在,并在特定的 Git 事件(如提交、推送、合并等)触发时自动执行,典型的应用包括:

  • 提交前代码检查:在提交代码之前自动运行测试或检查代码风格。
  • 提交信息规范化:确保提交信息遵循一定的格式。
  • 自动部署:在推送代码到远程仓库后,自动触发部署脚本。

Git hooks 的分类

Git hooks 分为两大类:

  1. 客户端钩子(Client-Side Hooks):主要在本地开发阶段触发,影响的是开发者的本地仓库行为。例如,提交前的检查、提交后的动作。
  2. 服务端钩子(Server-Side Hooks):主要在服务器端 Git 仓库中触发,影响的是推送到远程仓库的操作。例如,禁止推送不符合规定的提交或自动化部署等。

常用的 Git hooks

Git hook 是存放在 .git/hooks/ 目录下的一组脚本,以下是一些常用的钩子事件:

  1. pre-commit
  • 触发时机:在执行 git commit 前触发。
  • 用途:可以用来检查代码格式、运行测试或阻止提交不符合规范的代码。
  1. commit-msg
  • 触发时机:在提交信息编辑完毕后执行。
  • 用途:用来检查提交信息是否符合规范,如确保每次提交信息都有相应的 issue 编号或规定格式。
  1. pre-push
  • 触发时机:在执行 git push 前触发。
  • 用途:可以用来阻止推送某些特定分支、运行测试等。
  1. post-commit
  • 触发时机:在提交完成后触发。
  • 用途:可以用于记录提交日志、发送通知或触发 CI(持续集成)服务。
  1. pre-receive
  • 触发时机:在服务端接收到推送请求但未处理前触发。
  • 用途:在服务器端用于验证推送的代码,如确保符合公司标准,或防止特定分支的推送。
  1. post-receive
  • 触发时机:在服务器端接受推送并处理后触发。
  • 用途:常用于触发自动化部署、发送推送通知等。
// install
pnpm add --save-dev husky
// husky init
pnpm exec husky init

// 参见官方文档:https://typicode.github.io/husky/get-started.html

为什么需要 commitlint?

产生大量的 commit 版本后,良好的 commit 信息可帮我们更好的回顾或回退版本

如何规范?

  1. 提供一套规范来约束项目的 commit 信息;

    type(scope?): subject

    type(scope?): subject 是一种规范化的 Git 提交信息格式,通常用于 Conventional Commits 规范。这种格式有助于标准化提交信息,明确每次提交的意图。下面简述各部分的含义:

    1. type (必填)

    type 表示提交的类型,用于描述这次提交的目的或性质。常见的 type 包括:

    • feat:新增功能(feature)。
    • fix:修复 bug。
    • docs:文档变更。
    • style:代码格式变更(不影响代码逻辑的变动,如空格、格式等)。
    • refactor:代码重构(不包括新增功能或修复 bug 的变动)。
    • test:添加或修改测试。
    • chore:非代码逻辑的变动(如构建任务、工具配置等)。
    1. scope (可选)

    scope 用来标明本次提交影响的模块或功能的范围。通常是项目中的某个特定功能、模块或子系统,帮助团队成员明确该提交的影响范围。

    • 例子:
      • feat(user): 影响用户模块。
      • fix(auth): 影响身份认证模块。

    注意: scope 是可选的,如果不需要指定具体的模块范围,可以省略。

    1. subject (必填)

    subject 是对本次提交的简要描述,用于说明提交内容。需要注意的是:

    • 使用简洁明了的语言。
    • 应该以动词开头,通常是祈使句形式,如 “add”,”fix”,”update” 等。
    • 不要以大写字母开头,且不要以句号结尾。

    例子:

    • feat(auth): add login functionality (新增了登录功能)。
    • fix(user): resolve issue with profile picture upload (修复了头像上传的问题)。
// 那当然需要先安装下
pnpm add --save-dev @commitlint/{cli,config-conventional}

// 下面这句话的意思,是将标的字符串内容写进 > 指向的文件当中,如果不存在该文件,则自动创建该文件
echo "export default { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js

// Add commit message linting to commit-msg hook
echo "pnpm dlx commitlint --edit \$1" > .husky/commit-msg

// 这行命令是 Unix/Linux 系统中的 chmod 命令,用于更改文件或目录的权限
// Windows 系统中使用 husky,可以跳过 chmod 步骤。Windows 文件系统默认不需要像 Unix 系统那样手动设置文件执行权限,Husky 的钩子应该可以正常工作。
chmod a+x .husky/commit-msg
  • chmod:这是改变文件权限的命令。
  • a+x:表示给所有用户(a 是 “all” 的意思,包括用户本人、用户组和其他用户)添加执行权限(+x 表示增加执行权限)。
  • .husky/commit-msg:这是文件路径,表示你要更改权限的文件。这个文件是 husky 钩子脚本,用来检查提交信息(commit message)。

作用

这条命令的作用是让 .husky/commit-msg 文件可以被执行。commit-msghusky 钩子,用来在 Git 提交时自动检查提交信息是否符合规范。让这个文件具有执行权限后,Git 在提交时可以调用并运行这个脚本。

一般在设置 husky 钩子时,需要给相应的钩子文件(如 commit-msgpre-commit 等)赋予执行权限,否则这些钩子无法运行。

commit-msg

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

pnpm dlx commitlint --edit "$1"

pre-commit

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# 这里挺操蛋的, 在配置具体 prettier 规则之前, 这里的npm test 处于未配置状态, 引起报错
#npm test

rem适配 && 路径别名

点击查看详情

/src/utils/rem.ts

// 基准大小
const baseSize = 37.5
function setRem() {
const scale = document.documentElement.clientWidth / 750
document.documentElement.style.fontSize = baseSize * Math.min(scale, 1) + 'px'
}

// 初始化
setRem()
window.onresize = function () {
setRem()
}
export default baseSize

// 然后在 main.ts 中:
import '@/utils/rem' // 引入 rem 适配方案

tsconfig.app.json 文件中需要配置别名路径

{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue","src/**/*.ts","src/**/*.d.ts","src/**/*.tsx",],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".",
// 这里配置了路径别名
"paths": {
"@/*": ["./src/*"],
"@": ["./src"]
},
"noImplicitAny": false,
"allowJs": true,
"skipLibCheck": true
}
}

eslint,prettier

代码规范标准

在运行代码前,发现语法错误和潜在bug

允许定制自己的代码规范

ESLint规则的三个等级:off、warn、error

ESlint扩展会优先去查找项目根目录中的eslint.config.js 配置文件,并且包括配置文件所提到的ESlint插件,也就是npm依赖包,是的没错,ESlint扩展本身所需的ESlint 版本和ESlint插件,都是来自于node_modules,你可以试着把这个目录删了,vscode中的ESlint扩展就会报错,无法运行。

但你启用vscode中的ESlint扩展之后,并不会对所有文件生效,你还需要配置ESlint扩展的设置来对所需的文件启用校验。

这里建议为每个项目单独添加vscode独有的设置,也就是项目根目录中创建一个.vscode目录,里面放置一个settings.json文件,这样vscode就会优先读取该设置:

// .vscode/settings.json
{
"eslint.validate": [
"javascript",
"vue",
"vue-html",
"typescript",
"typescriptreact",
"html",
"css",
"scss",
"less",
"json",
"jsonc",
"json5",
"markdown"
],
}

这样一来,配置中所提到的文件格式,都会被ESlint扩展识别。

到这里,ESlint基本算是可以正常使用了,でも,ちょうど待ってください,prettierがありますね。

用于检测代码中的格式问题

ESLint 是侧重对项目代码质量的把控,而Prettier更侧重于统一项目的代码风格

因此,两者配合一起使用,比较好。

ESLint9之后,原有的配置文件已经被弃用

在我还不知道如何从0开始配置ESlint的时候,ESlint已经更新到9.x了。

总而言之,从ESlint9.x开始,在使用create vue 创建项目后,如果选中了eslint,那么项目中生成的文件为 eslint.config.js(ESNext)或者eslint.config.mjs(CommonJS)命名的配置文件。


// 依赖包安装
pnpm add eslint @eslint/js globals typescript-eslint eslint-plugin-vue @stylistic/eslint-plugin eslint-plugin-prettier -D

import eslint from '@eslint/js'
import globals from 'globals'
import tseslint from 'typescript-eslint'
import eslintPluginVue from 'eslint-plugin-vue'
import stylistic from '@stylistic/eslint-plugin'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'

export default tseslint.config(
{
ignores: ['node_modules', 'dist', 'public']
},

/** js推荐配置 */
eslint.configs.recommended,
/** ts推荐配置 */
...tseslint.configs.recommended,
/** vue推荐配置 */
...eslintPluginVue.configs['flat/recommended'],

stylistic.configs.customize({
indent: 2,
quotes: 'single',
semi: false,
jsx: true,
braceStyle: '1tbs',
arrowParens: 'always'
}),

/**
* javascript 规则
*/
{
files: ['**/*.{js,mjs,cjs,vue}'],
rules: {
'no-console': 'warn',
'@stylistic/comma-dangle': ['error', 'never'],
'prettier/prettier': [
'warn',
{
singleQuote: true, // 单引号
semi: false, // 无分号
printWidth: 80, // 每行宽度至多80字符
trailingComma: 'none', // 不加对象|数组最后逗号
endOfLine: 'auto' // 换行符号不限制(win mac 不一致)
}
]
}
},

/**
* 配置全局变量
*/
{
languageOptions: {
globals: {
...globals.browser,

/** 追加一些其他自定义全局规则 */
wx: true
}
}
},

/**
* vue 规则
*/
{
files: ['**/*.vue'],
languageOptions: {
parserOptions: {
/** typescript项目需要用到这个 */
parser: tseslint.parser,
ecmaVersion: 'latest',
/** 允许在.vue 文件中使用 JSX */
ecmaFeatures: {
jsx: true
}
}
},
rules: {
// 在这里追加 vue 规则
// pretter 专注于代码的美观度 (格式化工具)
'vue/no-mutating-props': [
'error',
{
shallowOnly: true
}
],
// ESLint 关注于规范
'vue/multi-word-component-names': [
'warn',
{
ignores: ['index'] // vue组件名称多单词组成(忽略index.vue)
}
],
'no-undef': 'error'
}
},

/**
* typescript 规则
*/
{
files: ['**/*.{ts,tsx,vue}'],
rules: {
'linebreak-style': ['error', 'unix']
}
},

/**
* prettier 配置
* 会合并根目录下的prettier.config.js 文件
* @see https://prettier.io/docs/en/options
*/
eslintPluginPrettierRecommended
)

ESLint Stylistic 是一个社区维护的开源项目,专注于为 JavaScript 和 TypeScript 提供风格和格式化相关的 ESLint 规则。该项目由 ESLint 和 typescript-eslint 团队发起,旨在将原本在核心规则中被弃用的风格和格式化规则独立出来,以便社区能够更好地维护这些规则。

该项目的主要编程语言是 TypeScript,同时也包含一些 JavaScript 代码。

规则冲突问题 - 在使用 ESLint Stylistic 时,可能会遇到与现有 ESLint 规则冲突的情况,导致代码检查失败。

eslint-plugin-vue & vite-plugin-eslint - 着重介绍俩插件

eslint-plugin-vue & vite-plugin-eslint

vite-plugin-eslinteslint-plugin-vue 是两个与 ESLint 相关的插件,分别用于 Vite 项目和 Vue 项目的代码质量检查。

vite-plugin-eslint

  • 作用: 这个插件将 ESLint 集成到 Vite 构建过程中。它可以在开发时自动检查你的代码,并在代码出现问题时给出即时反馈。

  • 功能

    :

    • 实时 lint:在开发过程中,当你修改代码时,插件会即时运行 ESLint 检查,并在控制台中显示错误和警告。
    • 自定义 ESLint 配置:支持使用自定义的 ESLint 配置文件,可以根据项目需求进行灵活设置。
    • 适配 Vite 的开发环境,提升开发效率。

eslint-plugin-vue

  • 作用: 这是一个专门为 Vue.js 提供的 ESLint 插件,提供 Vue 相关的 lint 规则。

  • 功能

    :

    • Vue 特有的规则:包括模板语法、组件定义、指令使用等方面的 lint 规则,帮助开发者遵循最佳实践。
    • 支持 Vue 3:适配 Vue 3 的新特性,确保代码符合最新的 Vue 开发标准。
    • 提高代码可读性和可维护性,减少潜在的错误。

总结

  • vite-plugin-eslint 是用于将 ESLint 集成到 Vite 开发流程中的插件,确保在开发过程中实时检查代码质量。

  • eslint-plugin-vue 则是专门针对 Vue.js 的 lint 规则插件,帮助开发者编写符合 Vue 最佳实践的代码。两者结合使用,可以有效提升 Vue 项目的代码质量和开发体验。

vite-plugin-eslinteslint-plugin-vue 之间并不冲突,它们实际上是互补的工具,可以一起使用,以实现更好的代码质量管理。

关系与互补性

  • vite-plugin-eslint: 负责将 ESLint 集成到 Vite 项目中,提供实时的代码检查反馈。它会在开发过程中自动运行 ESLint。
  • eslint-plugin-vue: 提供专门针对 Vue 组件的 lint 规则。这个插件需要在 ESLint 的配置文件中进行设置,以确保你的 Vue 代码符合特定的规范。

使用建议

  1. 一起使用: 在 Vite 项目中使用 vite-plugin-eslint 来实时检查代码,同时在 ESLint 配置中引入 eslint-plugin-vue,以确保 Vue 组件代码符合最佳实践。
  2. 配置 ESLint: 确保 ESLint 配置中包含 eslint-plugin-vue,并按照项目需求启用或禁用特定规则。
  3. 避免重复检查: 如果在 Vite 配置中使用了 vite-plugin-eslint,不需要在构建时重复运行 ESLint,除非你有特定需求。

总的来说,合理配置这两个插件将有助于提升你的开发体验和代码质量,不会导致冲突。

解决步骤:

检查现有规则:首先,检查你的 .eslintrc.json 文件中是否已经存在与 ESLint Stylistic 规则冲突的配置。

禁用冲突规则:如果发现冲突规则,可以在配置文件中禁用这些规则。例如,如果你已经配置了 indent 规则,可以禁用它:

{
"rules": {
"indent": "off",
"@stylistic/indent": ["error", 2]
}
}

逐步应用规则:建议逐步应用 ESLint Stylistic 的规则,避免一次性引入过多规则导致冲突。

自定义规则问题 - 新手可能希望自定义 ESLint Stylistic 的规则,但不知道如何正确配置。

解决步骤:

了解规则配置:首先,阅读 ESLint Stylistic 的官方文档,了解每个规则的配置选项。

自定义规则:在 .eslintrc.json 文件中,根据文档中的说明自定义规则。

{
"rules": {
// 例如,如果你想自定义缩进规则:
"@stylistic/indent": ["error", 4],
// 例如, 去掉末尾逗号需要这样设置
'@stylistic/comma-dangle': ['error', 'never']

// 同时, 需要注意的是, prettier 内置了一些规则, 即使不配置任何prettier, 也会生效
// 因此, 需要想要实现末尾去掉逗号, 需要在 prettier 里进行自主设置
trailingComma: 'none', // 不加对象|数组最后逗号
}
}

测试配置:在应用自定义规则后,运行 ESLint 检查代码,确保配置正确无误。

通过以上步骤,新手可以更好地理解和使用 ESLint Stylistic 项目,避免常见问题。
————————————————

vueuse 工具集 & pinia 独立维护 & vant 3

开发环境初始化

VueUse 是基于 组合式 API 的实用函数集合。

// 安装
pnpm i @vueuse/core --save

Vant 3UI:轻量的移动端组件库

在基于 Rsbuild、Vite、webpack 或 vue-cli 的项目中使用 Vant 时,可以使用 unplugin-vue-components 插件,它可以自动引入组件。

Vant 官方基于 unplugin-vue-components 提供了自动导入样式的解析器 @vant/auto-import-resolver,两者可以配合使用。

相比于常规用法,这种方式可以按需引入组件的 CSS 样式,从而减少一部分代码体积,但使用起来会变得繁琐一些。如果业务对 CSS 的体积要求不是特别极致,我们推荐使用更简便的常规用法。

// 当然是先安装好
pnpm i vant --save

// 按需引入组件样式
# 通过 pnpm 安装
pnpm add @vant/auto-import-resolver unplugin-vue-components unplugin-auto-import -D

请求拦截器

点击查看详情
  1. src目录下创建utils/request.ts

    import axios from 'axios'
    import { showToast } from 'vant'
    const baseURL = '/api'
    const service = axios.create({
    baseURL,
    timeout: 10000
    })
    // 发起请求之前的拦截器
    service.interceptors.request.use(
    config => {
    const token = window.localStorage.getItem('token')
    if (token) {
    // config.headers = {
    // 'x-access-token': token
    // }
    // 直接在 headers 对象上添加属性
    config.headers['x-access-token'] = token
    }
    return config
    },
    error => Promise.reject(error)
    )
    // 响应拦截器
    service.interceptors.response.use(
    response => {
    const res = response.data
    if (response.status !== 200) {
    return Promise.reject(new Error(res.success || 'Error'))
    } else {
    if (res.code == 200) {
    return res.result || res.data
    } else {
    showToast(res.message)
    }
    }
    },
    error => {
    return Promise.reject(error)
    }
    )

    // 导出后在 api 文件中引入
    export default service

快速创建文件结构-路由规则

点击查看详情

项目页面结构

views
login
index.vue // 登录页
serviceAgree.vue // 服务协议
privatePolicy.vue // 隐私政策
task
index.vue // 任务主页
search.vue // 任务搜索
details.vue // 任务详情
cpmpanySource.vue // 公司任务主页
contract
index.vue // 合约主页
details.vue // 合约详情
progress.vue // 合约进度
message
index.vue // 消息主页
systemList.vue // 消息列表
systemDetails.vue // 消息详情
talk.vue // 对话
my
index.vue // 我的主页
user // 用户中心
index.vue // 个人信息
autheReal.vue // 实名认证
certifeild.vue // 已完成实名认证
identitySwitch.vue // 切换身份
set // 我的设置
index.vue // 设置页
feedback // 意见反馈
index.vue // 反馈主页
account // 账户
index.vue // 账户主页
advance.vue // 账户提现
coinExplain.vue // 无忧币说明
depositExplain.vue // 押金说明
resume // 我的简历
index.vue // 简历主页
preview.vue // 简历预览
collect // 收藏
index.vue // 收藏主页
talent // 人才主页
index.vue // 人才详情
// 先创建文件夹
mkdir -p src/views/{login,task,contract,message,my/user,my/set,my/feedback,my/account,my/resume,my/collect,talent}

// 然后按照文件夹, 一一创建具体文件
// 最佳实践: 使用 PascalCase - 首字母大写的驼峰命名,而非首字母小写。这不仅符合 Vue 官方的推荐,也有助于代码一致性和可读性,尤其在大型项目和协作开发中。

# 创建 login 模块文件
touch src/views/login/{index.vue,ServiceAgree.vue,PrivatePolicy.vue}

# 创建 task 模块文件
touch src/views/task/{index.vue,TaskSearch.vue,TaskDetails.vue,TaskCpmpanySource.vue}

# 创建 contract 模块文件
touch src/views/contract/{index.vue,ContractDetails.vue,ContractProgress.vue}

# 创建 message 模块文件
touch src/views/message/{index.vue,SystemList.vue,SystemDetails.vue,MessageTalk.vue}

# 创建 my 模块文件
touch src/views/my/index.vue
touch src/views/my/user/{index.vue,AutheReal.vue,CertiFeild.vue,IdentitySwitch.vue}
touch src/views/my/set/index.vue
touch src/views/my/feedback/index.vue
touch src/views/my/account/{index.vue,AcountAdvance.vue,CoinExplain.vue,DepositExplain.vue}
touch src/views/my/resume/{index.vue,ResumePreview.vue}
touch src/views/my/collect/index.vue

# 创建 talent 模块文件
touch src/views/talent/index.vue
// vue-router 当然是先安装一下
pnpm vue-router

两款插件

vite-plugin-pages 404页面的手动配置&动态路由

404路由配置

vite-plugin-pages 插件在自动生成路由时,并不会直接处理 404 页面。若要添加 404 页面支持,需结合 vue-router 的一些配置。以下是详细步骤来设置 404 页面。

  1. 创建 404 页面组件

src/pages 目录中创建 404.vue 文件,并编写 404 页面内容:

<!-- src/pages/404.vue -->
<template>
<div class="not-found">
<h1>404 - Page Not Found</h1>
<p>The page you are looking for doesn’t exist.</p>
</div>
</template>

<script setup>
</script>

<style scoped>
.not-found {
text-align: center;
padding: 2rem;
}
</style>
  1. 安装并配置 vite-plugin-pages

确保已安装 vite-plugin-pages,并配置 vite.config.js

pnpm install vite-plugin-pages -D

vite.config.js 中导入插件并添加配置:

// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import Pages from 'vite-plugin-pages';

export default defineConfig({
plugins: [
vue(),
Pages({
// 使用默认配置即可,Pages 插件会自动读取 src/pages 下的 .vue 文件并生成路由
}),
],
});
  1. 配置 vue-router 捕获所有未知路由

vite-plugin-pages 会自动生成 routes 配置,我们可以使用这些生成的路由,并手动添加 404 页面配置。在 src/main.jssrc/main.ts 中完成以下设置:

// src/main.js 或 src/main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { createRouter, createWebHistory } from 'vue-router';
import generatedRoutes from 'virtual:generated-pages';

const routes = [
...generatedRoutes,
// 添加 404 页面路由,捕获所有未知路径
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('./pages/404.vue') },
];

const router = createRouter({
history: createWebHistory(),
routes,
});

const app = createApp(App);
app.use(router);
app.mount('#app');
  1. 可选配置:重定向到首页或其他页面

如果希望未匹配的路径重定向到首页或其他页面,而不是显示 404 页面,可以将 404.vue 替换为首页或指定页面。例如:

const routes = [
...generatedRoutes,
// 重定向到首页
{ path: '/:pathMatch(.*)*', redirect: '/' }
];
  1. 可选配置:在生产环境中部署时处理 404

如果使用 GitHub Pages、Netlify 等平台部署,静态网站通常会出现直接访问 404 页面 URL 时无法找到页面的情况。这时可以根据部署平台的不同,分别进行配置:

Netlify:在根目录下创建 _redirects 文件,添加如下配置:

/*    /index.html   200

Vercel:Vercel 自动支持单页应用,默认不需要额外配置。

GitHub Pages:在 404.html 中添加 JavaScript 重定向代码:

<script type="text/javascript">
window.location.href = '/#' + window.location.pathname;
</script>

总结

配置 404 页面时需完成以下步骤:

  1. 创建 404.vue 页面。
  2. 使用 vite-plugin-pages 自动生成路由。
  3. vue-router 中手动添加通配符路由 /:pathMatch(.*)* 指向 404.vue 组件。
  4. (可选)根据部署平台的需求配置生产环境的 404 页面处理。

动态路由

动态路由参数

如果你需要接收动态参数,如 things_idreceive_id,你需要确保你的文件名包含正确的参数名称,例如:

src/pages/
message/
MessageTalk/
[things_id].vue
[receive_id].vue

或者使用 MessageTalk.vue 文件的嵌套路由:

src/pages/
message/
[things_id]/
[receive_id].vue

vite-plugin-pages 会自动解析这些动态参数并将它们映射到路由中。

unplugin-vue-routervite-plugin-pages 都是为 Vue 开发的文件系统路由插件,主要用于简化路由配置。但它们在特性和实现方式上有所不同。以下是两者的简要对比:

vite-plugin-pages vs unplugin-vue-router

最好还是使用后者,因为...

可以直接在页面中使用 definePage 来定义 meta,并且 不会 报错,因为 unplugin-vue-router 提供了类型支持和自动引入 definePage 的功能。正确配置后,definePage 会在页面中自动识别,无需额外导入。以下是确保配置无误的步骤:

  1. 确保 vite.config.ts 配置了 unplugin-vue-router

vite.config.ts 中确保插件已经正确配置,通常设置如下:

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import VueRouter from 'unplugin-vue-router/vite'

export default defineConfig({
plugins: [
VueRouter({ /* 配置项,可选 */ }),
vue()
]
})
  1. 添加 unplugin-vue-router 的自动生成文件

unplugin-vue-router 会自动生成路由文件,并包含类型声明。添加好插件后,首次启动项目时它会生成类型声明文件。这些文件通常位于 src/typed-router.d.ts 中,可以提供完整的路由类型支持。

  1. 在页面组件中使用 definePage 定义 meta

成功配置后,definePage 函数会在页面组件中自动识别,你可以直接定义 meta,例如:

// src/pages/Home.vue
<script setup lang="ts">
definePage({
meta: {
showTabBar: true
}
})
</script>

<template>
<div>首页内容</div>
</template>
  1. 验证和使用 meta 信息

在主布局或根组件中,可以通过 route.meta.showTabBar 访问并根据需要使用 meta 信息。例如:

<!-- App.vue -->
<template>
<router-view />
<TabBar v-if="$route.meta.showTabBar !== false" />
</template>

<script setup lang="ts">
import TabBar from '@/components/TabBar.vue'
</script>

这样配置后,definePage 应该不会报错,meta 信息也可以直接在页面中定义并在组件中读取,实现动态显示组件的功能。

方法:通过 meta 字段配置路由

vite-plugin-pages 本身并不直接支持 definePageMeta,这个 API 是 Nuxt.js 的功能,而在 Vue 3 项目中并没有官方直接的 definePageMeta 功能。因此,以下是实现类似效果的替代方法:

vite-plugin-pages - 可尝试通过 meta 字段配置路由

使用 vite-plugin-pages 时,可以在 vite.config.ts 文件中对路由进行扩展,设置 meta 字段,用于单个页面的额外配置。

步骤 1:配置 vite-plugin-pages 添加 meta

vite.config.ts 中,通过 extendRoute 为指定页面添加 meta 字段:

// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import Pages from 'vite-plugin-pages';

export default defineConfig({
plugins: [
vue(),
Pages({
extendRoute(route) {
// 比如为 login 页面设置特定的 meta
if (route.name === 'login') {
return {
...route,
meta: { showTabBar: false } // 自定义 meta 数据
};
}
// 可以为其他页面单独设置不同的 meta 配置
return route;
}
})
]
});

步骤 2:在页面组件中读取 meta 信息

在页面组件中使用 useRoute 读取路由 meta 信息:

// src/pages/YourPage.vue
<script setup lang="ts">
import { useRoute } from 'vue-router';

const route = useRoute();
// 使用 meta 配置
const showTabBar = route.meta.showTabBar !== false;
</script>

<template>
<div>
<p>页面内容</p>
<!-- 根据 showTabBar 动态控制 TabBar 显示 -->
<TabBar v-if="showTabBar" />
</div>
</template>

注意事项

  1. 如果需要在多个页面中复用 meta 字段,可在 extendRoute 中批量设置条件。
  2. 此方法兼容 Vue 3 的组合式 API,并与 vite-plugin-pages 配置无冲突。
  1. 功能对比
特性vite-plugin-pagesunplugin-vue-router
路由自动化根据文件系统自动生成路由根据文件系统自动生成路由
文件名映射支持文件名到路由的直接映射支持文件名到路由的直接映射
动态路由支持支持动态路由命名(如 [id].vue支持动态路由命名(如 [id].vue
嵌套路由支持基于目录结构的嵌套路由支持基于目录结构的嵌套路由
404 页面支持需要手动配置 404 页面可自动处理 404 页面
全局导航守卫不支持支持全局导航守卫
懒加载支持自动支持懒加载自动支持懒加载
TypeScript 支持原生支持,需配置类型声明原生支持,需配置类型声明
构建时路由生成构建时生成路由,支持虚拟模块构建时生成路由,支持虚拟模块
  1. 使用场景
场景适合的插件
小型项目vite-plugin-pages
快速开发与原型设计vite-plugin-pages
简单的路由管理vite-plugin-pages
复杂的路由需求unplugin-vue-router
需要权限管理和导航守卫unplugin-vue-router
大型项目unplugin-vue-router
高级路由配置unplugin-vue-router
  1. 优缺点

vite-plugin-pages

优点

  • 快速上手:可以快速生成路由配置,节省时间。
  • 减少冗余:不需要手动维护路由数组,文件结构直接映射路由。
  • 易于维护:通过修改文件名和结构来轻松管理路由。

缺点

  • 灵活性有限:对于复杂需求,可能需要手动调整。
  • 不支持所有高级功能:如全局导航守卫等功能需额外实现。

unplugin-vue-router

优点

  • 灵活性高:支持多种复杂路由配置,包括嵌套路由、命名路由、动态路由等。
  • 强大的功能:支持全局导航守卫、懒加载等高级功能。
  • 广泛使用:作为广泛支持的插件,文档和社区支持丰富。

缺点

  • 配置复杂:对于初学者,路由配置可能较为繁琐。
  • 手动维护:需要手动维护路由数组,增加了工作量。
  1. 集成方式

vite-plugin-pages & unplugin-vue-router

  1. 安装插件

    // vite-plugin-pages
    pnpm install vite-plugin-pages -D

    // unplugin-vue-router
    pnpm install unplugin-vue-router -D

  2. 配置 vite.config.js

    // vite-plugin-pages
    import { defineConfig } from 'vite';
    import vue from '@vitejs/plugin-vue';
    import Pages from 'vite-plugin-pages';

    export default defineConfig({
    plugins: [
    vue(),
    Pages({
    // 指定路由文件的目录
    dirs: 'src/views',
    // 可以自定义路由生成规则
    extensions: ['vue'], // 仅识别 .vue 文件
    exclude: ['**/components/**'] // 排除特定目录
    })
    ],
    });

    // unplugin-vue-router
    import { defineConfig } from 'vite';
    import vue from '@vitejs/plugin-vue';
    import UnpluginVueRouter from 'unplugin-vue-router/vite';

    export default defineConfig({
    plugins: [
    vue(),
    UnpluginVueRouter({
    routesFolder: 'src/pages',
    }),
    ],
    });

  1. 使用

    // vite-plugin-pages
    import { createRouter, createWebHistory } from 'vue-router';
    import generatedRoutes from 'virtual:generated-pages';

    const router = createRouter({
    history: createWebHistory(),
    routes: generatedRoutes,
    });

    // unplugin-vue-router
    import { createRouter, createWebHistory } from 'vue-router';
    import { routes } from 'unplugin-vue-router';

    const router = createRouter({
    history: createWebHistory(),
    routes,
    });

接受路由参数的三种方式

点击查看详情

最佳实践中,使用 useRoute

为什么推荐 useRoute

  • 语义化useRoute 是 Vue 3 提供的组合式 API,它直接说明你是在获取当前路由的参数。使用 useRoute 可以让代码更具可读性和可维护性。
  • 响应式useRoute 返回的是一个响应式对象,当路由参数发生变化时,组件会自动重新渲染,这对于动态路由特别有用。
  • 简洁:在组件内部,直接使用 useRoute 获取路由参数是最简洁的方式。

假设 receive_id.vue 组件位于路径 /message/:things_id/:receive_id 下,可以在组件中按如下方式获取动态路由参数:

<script setup lang="ts">
import { useRoute } from 'vue-router';

// 获取当前路由对象
const route = useRoute();

// 获取路由参数
const things_id = route.params.things_id;
const receive_id = route.params.receive_id;
// 在 Vue 3 中,useRoute 是一个非常常用的工具
// 允许你访问路由的所有相关信息(如路径、查询参数、动态参数等)。
// route.params 包含了当前路由的动态参数。在路由路径中使用的 :things_id 和 :receive_id 会自动出现在 route.params 中。

console.log(things_id, receive_id); // 打印路由参数
</script>

<template>
<div>
<h1>Things ID: {{ things_id }}</h1>
<h2>Receive ID: {{ receive_id }}</h2>
</div>
</template>

router.currentRoute.value 获取动态路由参数

虽然 router.currentRoute.value 也可以用来获取动态路由参数,但它的使用场景相对较少。通常这种方式更多见于非组件上下文,或者你需要对路由做更细致的控制时(比如在 Vuex、全局状态管理等地方访问路由信息)。

示例代码:

<script setup lang="ts">
import { router } from '@/router'; // 导入 router 实例

// 获取动态参数 id
const id = router.currentRoute.value.params.id;
# params.id 中的这个id 需要与具体文件结构一致、完全吻合

console.log('当前 ID:', id);
</script>

使用场景:

  • 如果你需要手动访问路由实例,或者在非组件环境(如 Vuex、服务模块等)中获取路由参数时,router.currentRoute.value 会更适用。
  • 需要直接操作或获取路由实例的情况下,适合使用这种方式。

defineProps 也可以获取路由参数

defineProps 主要用于从父组件获取数据或 props,而 vite-plugin-pages 会将动态路由参数自动作为 props 传递给组件。如果你通过 vite-plugin-pages 的自动路由系统来配置路由,并且路由参数会自动作为 props 传递给组件,那么你可以直接使用 defineProps 来获取。

根据项目实践,如果使用了 vite-plugin-pages 插件,不使用 defineProps 接受路由参数的话,控制台会给与相应提示!

示例代码:

<script setup lang="ts">
const { id } = defineProps<{
id: string;
}>();

console.log('当前 ID:', id);
</script>

使用场景:

  • 这种方式只适用于 vite-plugin-pages 等插件自动处理的场景。
  • 如果路由参数会自动作为组件的 props 传递,那么使用 defineProps 是最简洁的方式。
  • 不适合动态路由和非插件自动处理的情况。

组件封装 - 实用技巧

这里需要区分,有些组件例如 tabbar,需要写在项目根组件当中

否则,在 vant ui 框架中,会导致内部调用起冲突

另外,如果需要,可在 app.vue 根组件中,针对 tabbar 加上一个条件判断,例如:

<script setup lang="ts"></script>

<template>
<RouterView />
// 这样,就可以排除不需要tabbar组件的页面
<FooterTabbar v-if="$route.path !== '/login'"></FooterTabbar>
</template>

<style scoped></style>

配置

// 自动导入配置  settings.json
{
"workbench.settings.applyToAllProfiles": [



],
"less.compile": {
"out": "css/"
},
"window.zoomLevel": 1,
"workbench.iconTheme": "vscode-icons",
"vsicons.dontShowNewVersionMessage": true,
"security.allowedUNCHosts": ["iostation"],
// 当保存的时候, eslint自动帮我们修复错误
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
},
"explorer.confirmDragAndDrop": false,
// 关闭保存自动格式化
"editor.formatOnSave": false, // eslint 配置好了以后, 改为false 防止冲突
"editor.tabSize": 2,
"javascript.format.semicolons": "remove",
"terminal.integrated.defaultProfile.windows": "Command Prompt",
"security.workspace.trust.untrustedFiles": "open",
"editor.tabCompletion": "on",
"editor.fontLigatures": false,
"files.associations": {
"*.vue": "vue"
},
"emmet.triggerExpansionOnTab": true,
"[vue]": {
// "editor.defaultFormatter": "octref.vetur" // 定义一个默认格式化程序
},
"vetur.format.defaultFormatterOptions": {
"prettier": {
"semi": false,
"singleQuote": true
}
},
"explorer.compactFolders": false,
"notebook.compactView": false,
"vsicons.projectDetection.autoReload": true,
// 自动导入设置
"typescript.preferences.importModuleSpecifier": "shortest",
"typescript.suggest.autoImports": true,
"javascript.suggest.autoImports": true
}

Vue VSCode Snippets 是一个 Visual Studio Code 插件,提供了一组 Vue.js 的代码片段,能够帮助开发者快速编写 Vue 组件和其他常用代码结构。这个插件非常有助于提高开发效率,特别是在使用 Vue 3 的项目中。

名字+icon

api 请求前的类型声明

点击查看步骤和技巧
基本概念 - 如果一个东西长得想鸭子,可以像鸭子一样嘎嘎叫,那就认为它是鸭子
// interface 
interface Person {
readonly id: number;
name: string;
age?: number;
}

let viking: Person = {
id: 1,
name: 'viking',
age: 20,
}

// 函数参数和约定返回值类型声明
const add = (x: number, y: number, z?: number): number => {
if (typeof z === 'number') {
return x + y + z
} else {
return x + y
}
}
interface ISum {
(x: number, y: number, z?: number): number
}
let add2: ISum = add

步骤 1:定义请求参数的接口

​ 首先,定义 API 请求参数的接口,以确保传入的参数符合要求。

// 请求参数类型定义
interface LoginData {
accounts: string
code: string
}

步骤 2:定义 API 响应数据的接口

根据 API 返回的 JSON 结构定义接口,确保响应的数据类型安全:

// 返回参数的定义
interface UserInfo {
role: string
birthday: string | null
}

interface LoginResponse {
errCode: number
msg: string
data: {
user_info: UserInfo
token: string // 登录令牌
expireTime: number // 过期时间
}
}

步骤 3:封装 API 请求函数

在 Vue 项目中,通常使用 axios 发送请求。封装一个通用的请求方法,将参数和响应数据类型应用到请求中。

export function login(data: LoginData): Promise<LoginResponse> {
return request({
url: '/login',
method: 'post',
data
})
}

步骤 4:在 Vue 组件中使用 API 请求函数

在组件中调用该 API 函数时,使用 async/await 处理异步请求,并确保响应数据符合预期类型。

// 根据数据的实际使用方式,ts 会倒过来自动推断 api 返回的数据类型,并给出声明建议

技巧:

  1. 类型推导:尽量在 API 请求时显式定义接口,有助于类型推导,避免类型不匹配。
  2. 分层设计:将 API 请求函数与业务逻辑分离,便于维护和测试。
  3. 类型安全:使用接口描述请求参数和响应数据,确保数据符合预期。
  4. 错误处理:在组件中进行错误捕获,以便向用户反馈错误信息。
  5. 可选字段:对于返回结果中的非必需字段(如 msg),可用 ? 标记为可选。

即 - 部分类型定义

允许只定义 ApiResponse 中需要的字段,而不必包含整个返回对象的所有字段。这种方式称为“部分类型定义”,它有助于简化代码,尤其在返回数据结构复杂的情况下。

但,虽不必包含返回对象的所有字段,但对于需要使用的返回数据,必须在 interface 中声明,如暂时无法确定其类型,可暂时将类型留空,将来在组件中根据实际的使用方法,ts 会自动未我们进行类型推断

ts编译器可以从上下文中推断出变量的结构和字段类型,它会根据你的使用模式自动推断出一个更准确的类型。


// 例如,本项目中未在 /api/user.ts 中的 records

interface PolicyResponse {
# 返回的数据,必须在interface中按需声明,否则在组件中会提示未声明类型
records // 未指定类型
}

// 协议文件接口
export function getPolicy(data: { id: number }): Promise<PolicyResponse> {
# 使用:Promise<>将声明的内容定义在函数体中
# 如果直接使用 <PolicyResponse> 而非 Promise<PolicyResponse>,则无法通过 await 来控制数据加载的流程,可能导致数据未加载完成就尝试访问属性,从而引发错误。
return request({
url: '/policy_protocol/list',
method: 'get',
params: data
})
}

但是,在组件中,根据对返回数据的实际调用方式,ts 会帮我们推断出合适的类型声明


// /src/views/login/PrivatePolicy.vue

const onClickLeft = () => history.back()
const getPolicyChange = async () => {
const res = await getPolicy({
id: 16
})
if (res) {
state.value.htmlText = res.records[0].content #根据对返回数据的实际调用方式
} else {
new Toast('这里出错啦')
}
}

state & defineProps 中的数据类型声明

点击查看详情

/src/stores/task.ts

在 Vue 3 + TypeScript 项目中,通常建议对store.state中的数据进行类型声明,以便充分利用 TypeScript 的类型检查和代码提示功能。这不仅可以提高代码的可读性和可维护性,还有助于避免潜在的错误。

同样的,也是遵循按需声明的原则,即只声明将来会在组件中渲染的数据即可。

import { defineStore } from 'pinia'

// 定义 City 类型
interface City {
name: string | undefined
}

// 向 state 存数据时, 同样需要对类型进行定义
// 这是 banner 数据类型声明
interface getBanner {
url: string | undefined
picture: string | undefined
}

// 这是职位列表的 state 数据类型声明
interface getPosition {
name
children
}

// 筛选列表的数据类型声明
interface getScreen {
serviceMode
taskCycle
}

export const taskStore = defineStore({
id: 'task',
state: () => {
return {
cityValue: localStorage.getItem('city') || '北京',
cityList: [] as City[], // 使用 City 数组类型
bannerList: [] as getBanner[],
positionValue: '', // 从 localStorage 获取初始值
positionList: [] as getPosition[],
screenList: {} as getScreen
}
},
actions: {
setCityValue(value: string) {
this.cityValue = value
localStorage.setItem('city', value) // 将新的 cityValue 存入 localStorage
},
setCityList(data) {
this.cityList = data
},
setBannerList(data) {
this.bannerList = data
},
setPositionList(data) {
this.positionList = data
},
setPositionValue(p: string) {
this.positionValue = p
localStorage.setItem('positionValue', p)
},
setScreenList(data: getScreen) {
this.screenList = data
}
}
})

父组件中,在子组件标签中,直接使用冒号(v-bind)增加动态属性

// 对于 向子组件传递的数据,像以往那样,需要在组合式 api 当中声明其数据类型
// 像这样
# 实践发现,父组件用不到的有关类型声明,可以不写,即只在具体渲染组件中进行有关类型声明即可
# 然而实际开发中,经常出现的情况是:父组件需要用到部分数据,子组件也需要用到部分数据
# 这种情况下,仅各自声明需要使用的数据类型即可
interface DetailItem {
task_name
head_img
company_id
company_name
user_name
city
position_name
task_budget
task_cycle
service_mode
task_ask
task_id
user_id
}

const state = reactive({
item: {} as DetailItem,
status: 0
})

// 然后才可以使用 v-bind 为子组件添加动态属性
<TaskDetail v-if="state.item" :item="state.item"></TaskDetail>

然后子组件中,像这样

// 首先对传递进来的数据类型进行声明
interface TaskItem {
task_name
is_emergency
task_budget
task_cycle
task_ask
company_name
city
service_mode
task_id
}

// 然后进行接收
const { taskList } = defineProps<{
taskList: TaskItem[]
}>()

# 根据实际测试,在本项目的开发规范中,父组件对 v-bind 传递的数据类型进行定义后
# 子组件可以隐式声明数据,即只声明父组件 v-bind 传递进来的父级元素而无需显式的声明其类型
# 例如:
const { item } = defineProps<{
item
}>()

// 然后就能供子组件使用了,props 数据是单向动态流,即父组件向子组件流动
# 进一步实际测试发现,使用 defineProps 接受父组件的数据时
# 采取直接解构的方式进行隐式声明也是可以的

// 例如:子组件中,直接
const { contractList } = defineProps<{
contractList
}>()

// 这样,就可以在子组件使用了,无论数据在第几层

# 而无需对要使用的数据进行具体的 interface 声明

defineProps 泛型参数 <{ messageList }>

  • 泛型定义了 props 的类型,帮助 TypeScript 提供更好的类型推断。
  • 在这个例子中,泛型 { messageList } 表示组件的 props 中应该有一个名为 messageList 的属性。
  • 未指定 messageList 的具体类型,所以默认会是 any 类型。

使用 defineProps 接收路由参数

// 通过 vite-plugin-pages 插件,路由参数(如 id)会自动作为 props 传递给组件
// 在组件中只需要使用 defineProps 来定义 id,然后通过 props.id 来访问该值。
// 而无需使用 const taskId = router.currentRoute.value.params.id

const { id: taskId } = defineProps<{
id: string
}>()

// const { id: taskId }:这部分代码是 JavaScript 的解构赋值语法。它将 props 中的 id 属性重命名为 taskId,这样你在组件内部就可以使用 taskId 来引用该值。

provide / inject 依赖注入 - 父组件向子组件提供方法时的类型声明

点击查看详情

https://cn.vuejs.org/assets/prop-drilling.XJXa8UE-.png

在 Vue 中,父组件通过 provide 提供的函数在子组件中调用时,子组件不会自动继承该函数的类型声明。这是因为 TypeScript 的类型系统是静态的,类型信息并不会随着函数的传递而自动传播。以下是一些相关的要点:

  1. 提供与注入
    • 父组件使用 provide 提供数据或函数,子组件使用 inject 注入这些数据或函数。虽然父组件提供了函数,但 TypeScript 仍需要明确这些函数在子组件中的类型,以确保调用时的类型安全。
  2. 类型声明的重要性
    • 当子组件调用从父组件提供的函数时,TypeScript 不会自动推断出该函数的参数和返回值类型。子组件必须显式地声明这些类型,以确保在调用时符合预期的类型。这是因为 TypeScript 只在定义时检查类型,而不是在运行时。
  3. 接口或类型的使用
    • 为了提高代码的可维护性和可读性,通常会在父组件中定义一个接口(如 PopupContext),描述提供的函数的类型。子组件在使用 inject 时,可以将这个接口作为类型断言,从而明确调用该函数时应遵循的类型约定。
  4. 示例

    在使用 provide 时,进行类型检查,以使其只能传入特定类型的数据

    在使用 inject 时,能够获取数据类型

    假设在父组件中定义了一个函数并提供了类型:

    // 父组件定义方法
    const closeCitySwitch = (name: string) => {
    if (name) {
    store.setCityValue(name)
    }
    state.citySwitch = false
    }
    # 通过'popup'向子组件暴露这个方法
    provide('popup', {
    closeCitySwitch
    })

    在子组件中,需要显式地声明其类型:

    // 子组件中,需要再次声明一个函数的类型
    interface PopupContext {
    closeCitySwitch: (cityName?: string) => void
    }

    // 注入父组件暴露出来的 'popup' 函数,并进行类型断言
    const popupContext = inject('popup') as PopupContext | null

    // 检查 popupContext 是否存在
    if (!popupContext) {
    throw new Error('popup is not provided')
    }

    const { closeCitySwitch } = popupContext // 解构出 closeCitySwitch,在子组件调用
  5. 总结

    在 TypeScript 中,尽管父组件通过 provide 提供了函数,子组件仍然需要显式声明类型。这并不是因为语言的局限性,而是为了确保类型的明确性和代码的可维护性。这种方式使得代码更健壮,开发者能够快速了解和检查函数的使用方式。

实战中的使用技巧

provide 可以直接将父组件中定义的接口类型实例传递出去,这样子组件在 inject 时也可以利用同样的类型接口。这里是一个完整示例:

示例:父组件中定义并提供接口实例

// 定义接口
interface UserContext {
name: string
age: number
updateUser: (name: string, age: number) => void
}

// 父组件中创建一个符合接口的对象
const userContext: UserContext = {
name: "Alice",
age: 30,
updateUser: (name, age) => {
console.log(`User updated to ${name}, ${age}`)
}
}

// 使用 provide 传递接口实例
provide("userContext", userContext)

子组件中接收并使用

在子组件中使用 inject 时,可以直接将 inject 的类型声明为 UserContext,确保代码安全并享受类型推导的好处。

// 在子组件中 inject 并指定类型
const userContext = inject<UserContext>("userContext")

if (userContext) {
console.log(userContext.name) // 自动提示 name 属性
userContext.updateUser("Bob", 25) // 自动提示 updateUser 方法和其参数类型
}

工作原理:

  • 父组件通过 provideuserContext 作为类型为 UserContext 的对象传出。
  • 子组件通过 inject<UserContext> 使用该类型,使 TypeScript 知道 userContext 的属性和方法类型。

好处:

这种方式的好处在于:

  • 子组件直接继承了父组件的类型定义,避免重复声明。
  • IDE 和 TypeScript 提供的自动补全和类型检查功能在子组件中也能正常工作。

这种做法让父子组件在 provide/inject 的类型上保持一致,确保了良好的代码可维护性和类型安全性。

symbol 处理

为什么使用 Symbol

  1. 避免命名冲突:

    • 如果你的应用中有多个 provide 使用了相同的键,可能会导致冲突。
    • Symbol 生成的是唯一的标识符,能有效避免这种问题。
    const MyKey = Symbol('MyUniqueKey');
    app.provide(MyKey, someValue);
  2. 代码安全性:

    • 使用字符串作为键时,可能被无意间覆盖。
    • Symbol 键能降低被误用或覆盖的风险。
  3. 提升代码可维护性:

    • 使用 Symbol 时,键通常会用常量声明,使代码更加规范。

是否必须使用 Symbol

  • 不是必须的。

    • 如果你只是在一个简单的上下文中使用(如没有深层嵌套或复杂依赖),字符串键是完全可以的。
    // 提供一个值
    app.provide('myKey', someValue);

    // 子组件注入
    const injectedValue = inject('myKey');
  • 推荐使用 Symbol 的场景:

    1. 大型应用或复杂组件树: 有很多依赖注入的场景。
    2. 第三方库开发: 避免用户无意中覆盖注入的值。
    3. 共享依赖: 不同的开发者团队可能会定义相同的字符串键。

使用示例

使用字符串键

// 父组件
app.provide('myKey', 'someValue');

// 子组件
const value = inject('myKey'); // 'someValue'

使用 Symbol 键

// 定义唯一的 Symbol
const MyKey = Symbol('MyUniqueKey');

// 父组件
app.provide(MyKey, 'someValue');

// 子组件
const value = inject(MyKey); // 'someValue'

结论

  • 简单应用:可以直接使用字符串。
  • 复杂或潜在冲突场景:建议使用 Symbol,更安全且维护性更好。

ps: 中大型项目或多人协作

  • 推荐将 Symbol 键集中管理:新建一个专门的文件存放所有的 Symbol 键,可以避免键重复、命名混乱和跨模块依赖问题。
  • 文件结构建议:
    • 将所有 Symbol 键集中定义在一个文件中,例如 keys.jsinjectionKeys.js
    • 根据功能模块,适当分层管理。

数据的本地化存储

基于 pinia defineStore

首先,基于 pinia 的 defineStore 创建一个独立的 store 模块

// 本项目实战中 /src/stores/task.ts
import { defineStore } from 'pinia'

// 既然是 ts ,那就需要对存进来的数据类型进行声明
// 定义 City 类型
interface City {
name: string | undefined
}

// 向 state 存数据时, 同样需要对类型进行定义
// 这是 banner 数据类型声明
interface getBanner {
url: string | undefined
picture: string | undefined
}

// 这是职位列表的 state 数据类型声明
interface getPosition {
name
children
}

# 正如下面展示的那样,分别提供了 state 和 actions
# 在 Pinia 中,state 和 actions 之间的关系类似于 Vuex 中的 state 和 mutations/actions。Pinia 是一种更轻量、模块化的状态管理工具,用于替代 Vuex

// 简单来说,state 用来存储数据,而 actions 是用于定义业务逻辑和对 state 数据进行更新的地方,在 Pinia 中,不需要像 Vuex 那样通过 mutations 来更新 state,可以直接在 actions 中修改 state。
export const taskStore = defineStore({
id: 'task',
state: () => {
return {
cityValue: localStorage.getItem('city') || '東京都',
cityList: [] as City[], // 使用 City 数组类型
bannerList: [] as getBanner[],
positionValue: localStorage.getItem('positionValue') || 'フランドエンド', // 从 localStorage 获取初始值
positionList: [] as getPosition[]
}
},
actions: {
setCityValue(value: string) {
this.cityValue = value
localStorage.setItem('city', value) // 将新的 cityValue 存入 localStorage
},
setCityList(data) {
this.cityList = data
},
setBannerList(data) {
this.bannerList = data
},
setPositionList(data) {
this.positionList = data
},
setPositionValue(p: string) {
this.positionValue = p
localStorage.setItem('positionValue', p)
}
}
})

组件中,通过定义请求函数拿到返回结果之后,通过 store 暴露出来的方法,更新 state 中的数据

// 组件中发起 api 请求,拿到数据
const getPositionList = async () => {
const res = await positionList()
if (res) {
store.setPositionList(res) # 调用 pinia 组件中的 actions 方法
} else {
new Toast('到这里出错了')
}

前面这些步骤做好了之后,即可以在 actions 中,轻松的实现本地化存储

// 例如,本项目实战中 /src/stores/task.ts 
setPositionValue(p: string) {
this.positionValue = p # 组件中获取的数据传回 store 模块,并使用 actions 中的方法将数据更新至 state,这一步,是为了实现页面中的数据实时更新
localStorage.setItem('positionValue', p) # 与此同时,将新数据本地化存储
}

state 中:虽然实时更新了数据,但,当页面刷新后,state 会优先从本地取先前存进去的数据。

state: () => {
return {
cityValue: localStorage.getItem('city') || '東京都',
cityList: [] as City[], // 使用 City 数组类型
bannerList: [] as getBanner[],
positionValue: localStorage.getItem('positionValue') || 'フランドエンド', // 从 localStorage 获取初始值

# 需要充分考虑这种情况, 有些数据并无必要存在本地, 例如刷新页面后希望显示全部数据的这种情况, 本项目中, 如positionValue 存在本地, 且设置优先取用本地存储的 value 值, 这么刷新页面后, 无法加载全部数据, 对于本身数据就不多的测试项目而言, 是不利于项目展示和测试的, 因此在实际项目中不再将 positionValue 存在本地

下拉刷新和滚动加载

借助 vant ui 中的 组件实现这一功能 - 点击查看详情
// 页面结构如下
// List 会监听浏览器的滚动事件并计算列表的位置 - @load="onLoad"
// 当列表底部与可视区域的距离小于 offset(默认值为 300 )时
// List 会触发一次 load 事件
// 最佳实践中, 还可以将父组件 pull-refresh 中的 -v-model 和 list 组件中的 v-model 合并成一个状态变量,以简化代码

// 父组件 v-model 的作用: 控制下拉的刷新状态, 当达到触发条件后, 组件自动将 v-model 的值设置为 true ,实测并不会触发 @refresh 方法, 该方法仅在下拉时触发,因此推断 v-model 仅用来控制下拉效果的, 在该方法的最后, 需要手动设置 v-model 的值为 false 通知组件刷新结束并复位,如果没有手动设置这个值,那么下拉组件会持续停在下拉状态而不能复位,特此留个记号

// 子组件 list 中的 v-model作用: 控制组件列表的加载状态, 当列表滚动到底部时,组件会自动将 loading 设置为 true 并触发 @load 事件。处理完加载逻辑后,开发者需要手动将 loading 设置为 false,以告知组件加载完成,允许继续触发新的加载请求。
<van-pull-refresh v-model="state.loading" success-text="刷新成功" @refresh="onRefresh">
<van-list
v-model:loading="state.loading"
:finished="state.finished"
finished-text="没有更多了"
@load="onLoad"
>
<TaskList :task-list="state.taskList"></TaskList>
<div v-if="!state.loading && state.taskList.length == 0" class="wy-no-data">暂无数据</div>
</van-list>
</van-pull-refresh>

// List 会监听浏览器的滚动事件并计算列表的位置 - @load="onLoad"
// 当列表底部与可视区域的距离小于 offset 时,List 会触发一次 load 事件
// 经过实测, onLoad 函数在页面加载时会首先自动执行一次

const onLoad = () => {
state.pageNum = state.pageNum + 1
// 正因为 onLoad 的自动执行, 导致首次调用 getTaskAllList() 函数前
// 给 pageNum 赋值为 1 , 因此应设置state中 pageNum 的值为 0
// 这样一来, 首次请求既可以获取 pageSize 中规定的第一页数据量
getTaskAllList()
console.log('看看onLoad是否一进页面就执行')
}

const onRefresh = () => {
console.log('onRefresh执行了')
state.pageNum = 1
getTaskAllList()
}

// @finished: 是否已加载完成,加载完成后不再触发 load 事件, 默认值为 false (表示状态为可触发) /

底部tabbar - 向下滑动时的隐藏效果设计与实现

点击查看详情
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
// 控制 FooterTabbar 的显示与隐藏
const showFooter = ref(true)
let lastScrollTop = 0 // 上一次的滚动位置
// eslint-disable-next-line no-undef
let scrollTimeout: NodeJS.Timeout | null = null // 用于监听停止滚动的定时器

// 滚动事件逻辑
function handleScroll() {
const scrollTop = document.documentElement.scrollTop

if (scrollTop > lastScrollTop) {
// 向下滚动,隐藏 Footer
showFooter.value = false
} else if (scrollTop < lastScrollTop) {
// 向上滚动,显示 Footer
showFooter.value = true
}

// 设置停止滚动后的逻辑
if (scrollTimeout) clearTimeout(scrollTimeout)
scrollTimeout = setTimeout(() => {
showFooter.value = true // 停止滚动后归位
}, 900)

lastScrollTop = scrollTop
}

// 监听滚动事件
onMounted(() => {
window.addEventListener('scroll', handleScroll)
})

onUnmounted(() => {
window.removeEventListener('scroll', handleScroll)
if (scrollTimeout) clearTimeout(scrollTimeout)
})
</script>

<template>
<RouterView />
<transition name="footer-transition">
<!-- 动态显示 FooterTabbar -->
<FooterTabbar
v-show="
showFooter &&
!$route.path.startsWith('/login') &&
!$route.path.startsWith('/task/TaskDetails')
"
/>
</transition>
</template>

<style scoped>
/* 过渡动画 */
.footer-transition-enter-active,
.footer-transition-leave-active {
transition:
transform 1s ease,
opacity 1s ease;
}

/* 隐藏时向下滑动并变透明 */
.footer-transition-enter-from,
.footer-transition-leave-to {
transform: translateY(100%);
opacity: 0;
}

/* 显示时恢复正常位置 */
.footer-transition-enter-to,
.footer-transition-leave-from {
transform: translateY(0);
opacity: 1;
}
</style>

ps : 需要在 env.d.ts 文件中进行node类型声明

/// <reference types="vite/client" />
/// <reference types="node" />

onMountedonUnmounted 是 Vue 3 中的生命周期钩子,分别用于在组件挂载(即插入 DOM)和卸载(即从 DOM 中移除)时执行特定的操作。

在组件挂载时添加事件监听器(比如监听滚动事件)并在组件卸载时移除它们,此时的 onMountedonUnmounted 是必须的。否则,当页面跳转或组件销毁时,这些事件监听器就会留下来,可能会导致内存泄漏或不必要的性能开销。

如果你不需要做类似的初始化或清理操作,那么可以删去 onMountedonUnmounted,不影响功能。

但,根据最佳实践,肯定是要保留的啦!

常见报错

点击查看详情
- 类型“AxiosResponse”上不存在属性“errCode”。ts-plugin(2339)
// 根据 chatgpt 最佳实践
// 在项目根目录创建 axios.d.ts 文件:

import 'axios';

declare module 'axios' {
export interface AxiosResponse<T = any> {
errCode?: number;
}
}

// 在 axios.d.ts 文件中的代码即使添加了注释也会生效,因为 d.ts 文件的主要作用是声明类型,并不会参与实际的运行时逻辑。

// 在 d.ts 文件中,注释的内容通常不会影响 TypeScript 对类型的理解。声明文件中的类型声明会自动被 TypeScript 拓展到项目中的所有代码,因此即便在 axios.d.ts 文件里加注释,TypeScript 仍会识别其中的类型扩展。
-无法找到模块 vite-plugin-eslint 插件中的 TS 声明文件

关于在 vite.config.ts 中引入 vite-plugin-eslint 插件,出现如下报错:
无法找到模块 vite-plugin-eslint 插件中的 TS 声明文件,隐式含有 “any” 类型。

我们可以得到在该插件中是含有 TS 声明文件的,vite-plugin-eslint/dist/index.d.ts,但是由于 TypeScript 的变更,导致新版本的 typescript 与依赖包中 package.json 指明 TS 声明文件位置的 types 配置项不匹配,最终导致新版本的 TypeScript 找不到 vite-plugin-eslint 插件中的 TS 声明文件。
在新版的 TypeScript 中,已经不再使用 package.json 文件中根结构中的 types 字段指明 TS 声明文件位置,而是在 exprots 中相应的导入方式中添加 typs 字段指明 TS 声明文件位置。

我这里的解决方法是</span>[1]vite-plugin-eslint 插件报错参考资料</span></span>,在 node_modules 中找到 vite-plugin-eslint 插件的 package.json 文件,将其中的 exports 字段修改为如下内容,让新版的 TypeScript 在使用 import 导入时能够找到 vite-plugin-eslint 插件中的 TS 声明文件。

"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
},
"require": "./dist/index.js"
}
},

修改完成后,重启 ide 就不报错了。

到这里,项目的创建和最基础的开发配置已经完成了,后面继续整理项目的具体业务逻辑实现!