• 前语
  • Vite 创立项目
  • 代码标准 (格局化、提示)

    • eslint
    • prettier
    • husky, lint-stage, commitlint
    • 保存文件主动格局化
    • Volar
  • 装备 tsconfig
  • 环境变量
  • CSS 预处理器
  • Vant

    • 按需引进
    • unplugin-vue-components
    • 定制主题
  • 移动端适配

    • 增加 meta 标签
    • PostCSS
    • vw计划
    • rem计划
  • 主动导入API
  • vue-router
  • layout布局
  • Pinia
  • Axios
  • 移动端调试
  • Hooks
  • 关于可选链(Optional chaining)(?.)的运用问题
  • Watermark 水印
  • 大局制止复制张贴
  • 定位
  • 图片旋转
  • Icon组件
  • 其他

前语

  • 数月前,公司有个新 H5 (虽然不认同 H5 这个叫法,可是大部分人都这么叫‍♂️)项目给到我,作为一名移动端开发小白,免不了各种搜索&踩坑,进程弯曲,于是有了这篇文章,期望对需求协助的人有些许协助 (废话文学)。
  • 既然是新项目,那肯定要甩掉历史袱,什么技能新就上什么,vue3,vite,pinia…嘎嘎新✨
  • 自己是小白,这些技能都是第一次运用,许多完结&思路都是参(cho)考(x)其他大佬‍♂️的,如有不对的当地,欢迎各位大佬指正
  • 本文写于2022年8月,有些 API 改变迅速,可能你看到的时分现已不适用了,请留意甄别。
  • 项目示例已上传 github,有需求的能够参考 vue3-vant-mobile

Vite 创立项目

交互式:

$ npm create vite@latest
Need to install the following packages:
  create-vite@latest
Ok to proceed? (y) y
✔ Project name: … vue3-vant-mobile
✔ Select a framework: › vue
✔ Select a variant: › vue-ts

或许一步到胃式:

# npm 7+, extra double-dash is needed:
npm create vite@latest vue3-vant-mobile -- --template vue-ts

初始目录结构:

.
├── .gitignore
├── .vscode
│   └── extensions.json
├── README.md
├── index.html
├── package.json
├── public
│   └── vite.svg
├── src
│   ├── App.vue
│   ├── assets
│   │   └── vue.svg
│   ├── components
│   │   └── HelloWorld.vue
│   ├── main.ts
│   ├── style.css
│   └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

留意:
Vite2 需求 Node.js 版别 >= 12.0.0;Vite3 需求 Node.js 版别 14.18+,16+。
我开始创立项目是vite@2.9.5,现在现已3.0.x了

咱便是说,vite在我公司那台15款8g内存‍♂️的mbp上真是快到飞起,总算免除了我在公司老旧项目中不敢随意command+s的痛苦(按一下保存得编译个10s,期间卡到只能双手离开键盘‍♂️)。还没上车vite的xdm还不赶紧冲 vite中文官网: https://cn.vitejs.dev

弥补一点小常识

写文章时发现运用 npm create vite@latest 创立的项目(vite@3.0.x)会在 package.json 中参加 "type": "module" ,而我开始创立项目时运用的vite@2.9.5是没有增加 type 字段的

type字段用于界说package.json文件和该文件所在目录根目录中 .js 文件和 无拓展名 文件的模块化处理标准。值为 module 则选用ESModule标准;值为 commonjs省掉 则选用commonjs标准

不论package.json中的type字段为何值,.mjs 的文件都按照es模块来处理,.cjs 的文件都按照commonjs模块来处理

所以需求留意,根目录下的 .js 装备文件一般都是commonjs模块,需求命名为 .cjs。如:下面会讲到的eslintrc假如是经过’npx eslint –init’主动生成的,那么其后缀主动为 .cjs,prettierrc 和 postcss.config是手动创立的,那么就需求命名为 .cjs

或许你也能够直接去掉package.json中的”type”: “module”项,仍旧运用 .js

再弥补一点小常识

npm create vite@latest 这个指令中的create其实便是init的alias,等同于 npm init vite@latest

履行’npm create vite@latest’其实会去调用create-vite这个包,用@x.x.x指定的不是vite的版别,而是create-vite的版别。

所以假如你想用老版别vite创立项目,如履行 npm create vite@2.9.5 ,并不是表示用vite@2.9.5创立项目,而是用create-vite@2.9.5创立项目,创立后的vite版别并不必定是2.9.5。(事实上没有create-vite@2.9.5这个版别,履行这条指令会报错找不到该版别)

那么怎样查看create-vite和vite对应的版别号呢?

直接去vite库房看模版文件 vite/packages/create-vite/package.json ,切换tag找到对应的版别如: create-vite@2.9.2

能够看到对应联系为:

  • create-vite@2.9.2 -> vite@2.9.5
  • create-vite@2.9.4 -> vite@2.9.9
  • create-vite@3.0.0 -> vite@3.0.0 // 也便是从这个版别开始,package.json 增加了 “type”: “module”

代码标准 (格局化、提示)

代码标准必不可少

eslint

# 主动生成装备文件并装置下面四个依靠
npx eslint --init
# 或许手动创立文件
# npm i eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-vue -D
$ npx eslint --init
You can also run this command directly using 'npm init @eslint/config'.
✔ How would you like to use ESLint?  problems (选第二个)
✔ What type of modules does your project use?  esm
✔ Which framework does your project use?  vue
✔ Does your project use TypeScript?  No / Yes
✔ Where does your code run?  browser, node
✔ What format do you want your config file to be in?  JavaScript

@typescript-eslint/parser: ESLint 默许运用的是 Espree 进行语法解析,所以无法对部分 typescript 语法进行解析,需求替换掉默许的解析器

@typescript-eslint/eslint-plugin: 作为 eslint 默许规则的弥补,供给了一些额外的适用于 ts 语法的规则

eslint-plugin-vue: 让 eslint 辨认 vue 文件

prettier

npm i prettier eslint-config-prettier eslint-plugin-prettier -D
  • 创立prettier文件
// prettier.cjs
module.exports = {
  printWidth: 100,
  tabWidth: 2,
  useTabs: false, // 是否运用tab进行缩进,默许为false
  singleQuote: true, // 是否运用单引号代替双引号,默许为false
  semi: true, // 行尾是否运用分号,默许为true
  arrowParens: 'always',
  endOfLine: 'auto',
  vueIndentScriptAndStyle: true,
  htmlWhitespaceSensitivity: 'strict',
};
  • 装备eslintrc
// eslintrc.cjs
module.exports = {
  root: true, // 停止向上查找父级目录中的装备文件
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  extends: [
    'eslint:recommended',
    'plugin:vue/vue3-essential',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended',
    'prettier', // eslint-config-prettier 的缩写
  ],
  parser: 'vue-eslint-parser', // 指定要运用的解析器
  // 给解析器传入一些其他的装备参数
  parserOptions: {
    ecmaVersion: 'latest', // 支撑的es版别
    parser: '@typescript-eslint/parser',
    sourceType: 'module', // 模块类型,默许为script,咱们设置为module
  },
  plugins: ['vue', '@typescript-eslint', 'prettier'], // eslint-plugin- 能够省掉
  rules: {
    'vue/multi-word-component-names': 'off',
    '@typescript-eslint/no-var-requires': 'off',
  },
};

注:
需求给vue主动生成的env.d.ts文件增加eslint疏忽注释

// src/env.d.ts
// eslint-disable-next-line @typescript-eslint/ no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>;
  • 增加lint指令
// package.json
// 能够运行`npm run lint`检查代码
"lint": "eslint --ext .js,.vue,.ts src --fix"

husky, lint-stage, commitlint

我项目中没有装置,需求的小伙伴可自行装置

# 装置husky和lint-stage,并且装备好husky。
npx mrm lint-staged -D
# 装置commitlint校验提交信息格局
npm install @commitlint/cli @commitlint/config-conventional -D

保存文件主动格局化

// .vscode/settings.json
{
  // 保存时eslint主动修复过错
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  //保存主动格局化
  "editor.formatOnSave": true
}

主张将.vscode文件夹增加到git记载中

Volar

运用vscode的小伙伴请留意,vue3项目就不要运用Vetur插件了,它不支撑许多vue3特性,会有许多红线正告。
请运用官方引荐插件Volar,现已更名为Vue Language Features,再调配TypeScript Vue Plugin,开始愉快地敲代码吧‍

装备 tsconfig

// tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "strict": true,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "lib": ["ESNext", "DOM"],
    "skipLibCheck": true,
    // 是初始化默许装备
    /*
      在ts中导入js模块会报错找不到类型声明
      处理办法一:
        仅设置 "allowJs": true 即可
        注:allowJs设置true时,下方include不能够参加'src/**/*.js',不然报错'无法写入文件xx因为它会掩盖输入文件'
      办法二:
        仅在 env.d.ts 中参加 declare module '*.js'; 模块界说即可
      总结:和 "include": ["src/**/*.js"] 没有任何联系
    */
    "allowJs": true, // 答应编译器编译JS,JSX文件
    "baseUrl": "./",
    // "typeRoots": [
    //   "node_modules/@types" // 默许会从'node_modules/@types'途径去引进声明文件
    // ],
    // "types": ["node"] // 仅引进'node'模块
    // "paths"是相关于"baseUrl"进行解析
    // 在vite.config里装备了途径别名resolve.alias,为了让编译 ts 时也能够解析对应的途径,咱们还需求装备 paths 选项
    "paths": {
      "@/*": ["src/*"],
    }
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
  // references属性是 TypeScript 3.0 的新特性,答应将 TypeScript 程序拆分结构化(即拆成多个文件,分别装备不同的部分)。
  "references": [{ "path": "./tsconfig.node.json" }]
}

环境变量

vite官方文档: 环境变量和形式

  1. 根目录创立.env.[mode]文件
# base
# env文件中一切值都是字符串
# 关于true/false的变量,拿到的是'true'/'false',并不是boolean,不能直接运用,需求判断VITE_KEY === 'true'
# 或许将变量界说为boolean,用'true'表示true,''表示false,运用的时分再用Boolean()转化
# 页面标题
VITE_APP_TITLE = vue3-vant-mobile
# 接口恳求地址,会设置到 axios 的 baseURL 参数上
VITE_APP_API_BASE_URL = /api
# .env.development
# 开发环境
NODE_ENV = development
VITE_APP_API_BASE_URL = /api-dev
# 是否在打包时生成 sourcemap
VITE_BUILD_SOURCEMAP = true
# 是否在打包时删去 console 代码
VITE_BUILD_DROP_CONSOLE = false
# 是否敞开调试东西 vconsole
VITE_BUILD_VCONSOLE = true
# .env.test
# .env.production
...

.env.[mode]文件中的mode可自界说,如.env.development对应package.json脚本中的--mode development
只要以 VITE_ 为前缀的变量才会露出给经过 vite 处理的代码

  1. 为 import.meta.env 供给额外的类型界说
// src/vite-env.d.ts
// vite2为src/env.d.ts,vite3已改为src/vite-env.d.ts
interface ImportMetaEnv {
  readonly VITE_APP_TITLE: string;
  readonly VITE_APP_API_BASE_URL: string;
  readonly VITE_BUILD_SOURCEMAP: string;
  readonly VITE_BUILD_DROP_CONSOLE: string;
  // 更多环境变量...
}
interface ImportMeta {
  readonly env: ImportMetaEnv;
}
  1. 将src/vite-env.d.ts增加到tsconfig中
// tsconfig.node.json
{
  // 只要同时参加 "src/vite-env.d.ts" 才干使vite.config.ts中能运用src/vite-env.d.ts中的大局类型
  "include": ["vite.config.ts", "src/vite-env.d.ts"]
}
  1. 界说process.env

未增加@types/node类型界说的请先增加:

npm i @types/node -D
// vite.config.ts
import { defineConfig, loadEnv } from 'vite';
export default ({ command, mode }) => {
  // 获取环境变量
  const env: Partial<ImportMetaEnv> = loadEnv(mode, process.cwd());
  return defineConfig({
    define: {
      'process.env': env,
    },
  });
};
  1. 运用环境变量
  • vite.config 中经过 loadEnv加载
// vite.config.ts
build: {
  outDir: 'dist', // 指定打包途径,默许为项目根目录下的 dist 目录
  sourcemap: env.VITE_BUILD_SOURCEMAP === 'true',
  // minify默许esbuild,esbuild形式下terserOptions将失效
  // vite3改变:Terser 现在是一个可选依靠,假如你运用的是 build.minify: 'terser',你需求手动装置它 `npm add -D terser`
  minify: 'terser',
  terserOptions: {
    compress: {
      keep_infinity: true, // 防止 Infinity 被压缩成 1/0,这可能会导致 Chrome 上的功用问题
      drop_console: env.VITE_BUILD_DROP_CONSOLE === 'true', // 去除 console
      drop_debugger: true, // 去除 debugger
    },
  },
  chunkSizeWarningLimit: 1500, // chunk 巨细正告的约束(以 kbs 为单位)
},
  • index.html 中经过vite-plugin-html加载
npm i vite-plugin-html -D
// vite.config.ts
import { createHtmlPlugin } from 'vite-plugin-html';
plugins: [
  // 默许会向 index.html 注入 .env 文件的内容,相似 vite 的 loadEnv函数
  // 还可装备entry入口文件, inject自界说注入数据等
  createHtmlPlugin(),
]
<!-- index.html -->
<title><%- VITE_APP_TITLE %></title>
  • 其他js,ts,vue文件中可运用import.meta.env获取环境变量

CSS 预处理器

vite官方文档:css

Vite 供给了对 .scss, .sass, .less, .styl 和 .stylus 文件的内置支撑。没有必要为它们装置特定的 Vite 插件,但有必要装置相应的预处理器依靠

我喜爱用不花里胡哨的less

npm i less -D
  • 安排款式文件

    1. 创立src/styles文件夹
      • index.less
      • common.less – 公共款式
      • variables.less – 自界说变量
    2. 大局引进款式
    // src/main.ts
    import '@/styles/index.less';
    
  • 大局运用自界说变量

// vite.config.ts
css: {
  preprocessorOptions: {
    less: {
      javascriptEnabled: true,
      additionalData: `@import "${resolve(__dirname,'src/styles/index.less')}";`,
    },
  },
},

Vant

vant-ui官方文档

我运用的是vant3,当时vant4尚未发布正式版,v3和v4不兼容

# 装置
npm i vant

按需引进

  • 前期官方供给的按需引进:
    仅仅经过vite-plugin-style-import插件按需引进款式,组件仍是需求手动按需或全量引进,已抛弃
# v2.0.0会报没有导出styleImport,v1.4.1正常
npm i vite-plugin-style-import@1.4.1 -D 
// vite.config.ts
import styleImport, { VantResolve } from 'vite-plugin-style-import';
plugins: [
  styleImport({
    resolves: [VantResolve()],
  }),
]
  • 当时官方供给的按需引进:
    经过unplugin-vue-components插件主动按需引进组件和款式
npm i unplugin-vue-components -D
// vite.config.ts
import Components from 'unplugin-vue-components/vite';
import { VantResolver } from 'unplugin-vue-components/resolvers';
plugins: [
  Components({
    resolvers: [VantResolver()],
  }),
]
  • 运用

经过unplugin-vue-components按需引进后,能够直接在.vue文件模板中运用,并主动生成components.d.ts类型声明文件,js中仍然需求手动引进组件

<!-- 直接在template中运用,无需手动import -->
<van-button type="primary">首要按钮</van-button>

van组件需求带上van前缀
Vant中有单个组件是以函数的办法供给的,包含 Toast,Dialog,Notify 和 ImagePreview 组件,需手动引进函数组件
在运用函数组件时,unplugin-vue-components 无法主动引进对应的款式,因此需求手动引进款式

unplugin-vue-components

主动引进自界说组件

unplugin-vue-components 插件除了会主动引进装备了的ui组件库,还会默许引进 src/compoents 下的组件,也可经过 dirs 选项指定其他途径

自界说组件没有类型提示问题:在tsconfig的include中参加”./components.d.ts”即可处理

可是unplugin-vue-components会将src/compoents下一切的.vue组件都写入components.d.ts类型声明中(deep默许为true),假如运用 globs: ['src/components/**/index.vue'] 去匹配部分组件的话,会导致该组件生成的类型为 Undefined ,需求自己完结一个 resolvers (自己完结应该能处理,虽然我没试)

定制主题

vant官方文档:ConfigProvider 大局装备

根底变量
Vant 中的 CSS 变量分为 根底变量组件变量。组件变量会承继根底变量,因此在修正根底变量后,会影响一切相关的组件。

修正变量
因为 CSS 变量承继机制的原因,两者的修正办法有必定差异:

  • 根底变量只能经过 root 挑选器 修正,不能经过 ConfigProvider 组件 修正。(1)
  • 组件变量能够经过 root 挑选器 和 ConfigProvider 组件 修正。

这儿我挑选 :root 挑选器,在src/styles/theme.less中统一修正vant款式

可是因为款式引用次序问题:
不论运用 ‘vite-plugin-style-import’ 仍是 ‘unplugin-vue-components/vite’ 插件,都是按需引进组件/款式
导致引用次序为:
根底款式 -> theme.less -> 组件款式 (最先引进根底款式是经过theme.less中 :root 可掩盖根底变量揣度而来)
所以 theme.less中运用:root挑选器不能掩盖组件变量

处理:

  • 计划一:运用 #app 代替 :root 挑选器,经过进步挑选器的权重来掩盖组件变量

  • 计划二:

    1. 在 vite.config.ts 中经过 ‘VantResolver({ importStyle: false })’ 封闭主动按需引进款式
    2. 在 main.ts 中全量引进组件款式: import ‘vant/lib/index.css’ // 有必要在 theme.less 之前
    3. 在theme.less中能够正常运用 :root 挑选器掩盖根底/组件变量了

缺点:
全量引进组件款式会导致打包后体积变大(我实测大了大概100k,非权威非标准非官方数据‍♂️)

可是:

Vant 中有单个组件是以函数的办法供给的,包含 Toast,Dialog,Notify 和 ImagePreview 组件。在运用函数组件时,unplugin-vue-components 无法主动引进对应的款式,因此需求手动引进款式。

手动引进独自的款式: import ‘vant/es/toast/style’ 等十分费事
不如直接全量引进一切组件款式: import ‘vant/lib/index.css’

综上:
如运用 ‘vite-plugin-style-import’ 插件按需引进,则可直接选用计划一
如运用 ‘unplugin-vue-components/vite’ 插件按需引进,则选用计划二

‘unplugin-vue-components/vite’ 插件虽然要全量引进款式文件导致 build 体积变大(没有大太多),可是可主动导入组件,免除手动导入的费事
对包体积巨细没有特殊要求的话,主张挑选 ‘unplugin-vue-components/vite’

现在vant官方现已引荐运用 ‘unplugin-vue-components/vite’ 了,最新文档中已没有 ‘vite-plugin-style-import’ 的运用办法

移动端适配

好家伙,总算讲到移动端了

布景原理等我就不讲了,具体能够去看大佬们的解说。这儿我就讲 vwrem 这两种计划的完结

增加 meta 标签

<!-- index.html -->
<meta
  name="viewport"
  content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover"
/>

PostCSS

不论哪种计划,都免不了 PostCSS 的支撑,因为 vite 现已内置 PostCSS ,所以只需求在根目录创立一个 postcss.config.cjs 装备文件即可。

vw计划

vw计划运用 postcss-px-to-viewport 插件将 px 单位转化为 vw/vh 单位

npm i postcss-px-to-viewport -D
// postcss.config.cjs
module.exports = {
  plugins: {
    'postcss-px-to-viewport': {
      viewportWidth: 375,
    },
  },
};

别急,你认为就这样完事了吗,并没有。上面仅仅对规划稿尺度为 375 的进行转化( vant 规划稿尺度是 375 ‍♂️),可是咱们大部分规划稿尺度都是 750 ,所以需求额外对 750 尺度的进行处理。

那么问题来了,怎样装备多个尺度呢,postcss-px-to-viewport 文档并没有指明,自己测验处理吧‍♂️

因为 postcss-px-to-viewport 没有供给相似 postcss-pxtorem 中 rootValue({ file }) {} 的办法,即使运用 module.exports = (param) => {} 这种办法导出postcss装备,也拿不到当时转化文件的信息,所以无法依据文件途径动态设置 viewportWidth,

有一种hack办法:经过多次 px2viewport() 处理不同文件来设置viewportWidth

// postcss.config.cjs
const px2viewport = require('postcss-px-to-viewport');
plugins: [
  px2viewport({
    // vant
    viewportWidth: 375,
    exclude: [/^(?!.*node_modules/vant)/],
    // include: [/node_modules/vant/],
  }),
  px2viewport({
    // 非vant
    viewportWidth: 750,
    exclude: [/node_modules/vant/],
  }),
],

第一个处理 vant 的 px2viewport 为什么不必include选项呢?

因为 postcss-px-to-viewport v1.1.1 不支撑 include 装备项,v1.2.0 开始参加include,可是并没有发布到npm库房‍♂️

并且因为 postcss-px-to-viewport 不支撑 postcss 8.x ,而vite内置postcss 8.x,所以运用postcss-px-to-viewport会抛出正告‍♂️

改用 postcss-px-to-viewport-8-plugin 代替,既支撑 include 装备项,也支撑postcss 8.x

我太难了兄弟萌

最终完好的postcss.config代码为:

// postcss.config.cjs
const autoprefixer = require('autoprefixer');
const px2viewport = require('postcss-px-to-viewport-8-plugin');
const basePx2viewport = {
  unitToConvert: 'px', // 需求转化的单位,默许为 px
  // viewportWidth: 750, // 规划稿的视口宽度
  unitPrecision: 3, // 单位转化后保存的精度(许多时分无法整除)
  propList: [
    '*',
    //  '!font-size'
  ], // 能转化为vw的属性列表,!font-size表示font-size后面的单位不会被转化
  viewportUnit: 'vw', // 指定需求转化成的视口单位,主张运用 vw
  fontViewportUnit: 'vw', // 字体运用的视口单位
  // 指定不转化为视口单位的类,能够自界说,能够无限增加,主张界说一至两个通用的类名
  // 需求疏忽的CSS挑选器,不会转为视口单位,运用原有的px等单位。
  // 下面装备表示类名中含有'keep-px'以及'.ignore'类都不会被转化
  selectorBlackList: ['.ignore', 'keep-px'],
  minPixelValue: 1, // 设置最小的转化数值,这儿小于或等于 1px 不转化为视口单位
  mediaQuery: false, // 媒体查询里的单位是否需求转化单位
  // exclude: [/node_modules/], // 疏忽某些文件夹下的文件或特定文件
  // include: [/src/], // 假如设置了include,那将只要匹配到的文件才会被转化
};
module.exports = {
  plugins: [
    autoprefixer(),
    // vant
    px2viewport({
      ...basePx2viewport,
      viewportWidth: 375,
      exclude: [/^(?!.*node_modules/vant)/],
      // include: [/node_modules/vant/],
    }),
    // 非vant
    px2viewport({
      ...basePx2viewport,
      viewportWidth: 750,
      exclude: [/node_modules/vant/],
    }),
  ],
};

rem计划

rem计划运用 postcss-pxtorem 插件将 px 单位转化为 rem 单位,并且用 lib-flexible 设置rem基准值

虽然连 lib-flexible 自己都主张运用vw计划:

因为viewport单位得到很多浏览器的兼容,lib-flexible这个过渡计划现已能够抛弃运用,不论是现在的版别仍是曾经的版别,都存有必定的问题。主张咱们开始运用viewport来代替此计划。

但 vw 计划 仍是有缺点的。如 vw 计划不适合大屏,因为 vw 是一个份额单位,跟着屏幕尺度变大,运用vw单位的元素、字体也越来越大。但咱们肯定是期望在大屏上展现更多的内容,而不是更大的文字、图标。

因为咱们的产品运用场景包含手机和平板等设备,所以有必要考虑大屏的适配。我曾经测验过运用 scalezoom 的办法,将大屏上的元素按份额缩小,可是作用都不太抱负。最终仍是挑选 rem计划,因为 rem计划 能够经过媒体查询来约束基准值(根字体)巨细。

装备rem计划就简单多了

  1. 引进 lib-flexible
npm i amfe-flexible
// src/main.ts
import 'amfe-flexible';
  1. 引进 postcss-pxtorem
npm i postcss-pxtorem -D
// postcss.config.cjs
const autoprefixer = require('autoprefixer');
const pxtorem = require('postcss-pxtorem');
module.exports = {
  plugins: [
    autoprefixer(),
    pxtorem({
      rootValue({ file }) {
        return file.indexOf('node_modules/vant') !== -1 ? 37.5 : 75;
      },
      unitPrecision: 5,
      propList: ['*'],
      selectorBlackList: ['.ignore', 'keep-px'],
      minPixelValue: 1,
      mediaQuery: false,
    }),
  ],
};

特别留意:

假如用vant官网示例 file.indexOf('vant') 来匹配文件,请保证你的项目名或文件名没有包含’vant’
主张改为 file.indexOf('node_modules/vant')

一开始写这篇文章时写的demo项目我没留意,用的vant官网示例 file.indexOf('vant') 匹配文件,后来发现怎样转化 rem 单位不对劲,找了半天才发现本来我项目命名为 vue3-vant-mobile,导致 rootValue 一直为 37.5

  1. 创立 response.less 文件,约束根字体最大值
// src/styles/response.less
// prettier-ignore 疏忽prettier对 PX 的主动格局化
// !important 进步权重,使其掩盖 lib-flexible 设置的font-size
@media screen and (min-width: 768px) {
  html {
    /* prettier-ignore */
    font-size: 50PX !important;
  }
}

这儿仅仅因为插件问题导致vw计划比rem计划装备起来费事许多,本身vw、rem计划没有偶孰强孰弱之分,咱们看自己需求挑选即可✌️

主动导入API

前面介绍了一个主动按需引进组件的插件 unplugin-auto-import ,秉着能少写一行代码就少写一行代码的精神,再介绍一个主动导入api的插件 unplugin-auto-import

github: unplugin-auto-import

npm i unplugin-auto-import -D
// vite.config.ts
import AutoImport from 'unplugin-auto-import/vite';
plugins: [
  AutoImport({
    imports: ['vue', 'vue-router'],
    // 设置为在'src/'目录下生成处理ts报错,默许是当时目录('./',即根目录)
    dts: 'src/auto-import.d.ts',
    // 主动生成'eslintrc-auto-import.json'文件,在'.eslintrc.cjs'的'extends'中引进处理报错
    // 'vue-global-api'这个插件仅仅处理vue3 hook报错
    eslintrc: {
      enabled: true,
    },
  }),
]
// .eslintrc.cjs
extends: [
  // 处理运用主动导入api报错
  './.eslintrc-auto-import.json',
  // 独自处理运用vue api时报错
  // 'vue-global-api',
],

接下来就能够大局运用 vue、vue-router 相关 api,不必一个个手动导入了。哪些 api 可用请参考生成的 src/auto-import.d.ts 类型声明文件。

插一个小办法:
vue3 组合式 api 运用 ref 界说一个响应式变量,用 reactive 界说一个响应式方针,
当变量较多运用 ref 一个个界说费事时,能够用 reactive 界说一个 state 方针,将其他变量收入 state 中,既方便办理,又省掉了运用 ref 变量时的 .value

const state = reactive({
  num: 1,
  bool: true,
  user: {
    name: '张三',
    nick: '法外狂徒'
  }
})

vue-router

vue-router官方文档

  1. 装置
npm i vue-router@4
  1. 创立路由
// src/router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
export const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'app',
    meta: {
      title: 'app',
    },
    component: () => import('@/App.vue'),
  },
  // 代替vue2中的'*'通配符途径
  { path: '/:pathMatch(.*)*', redirect: '/' },
];
const router = createRouter({
  history: createWebHashHistory(), // history 形式则运用 createWebHistory()
  routes,
});
export default router;
  1. 挂载路由
// src/main.ts
import { createApp } from 'vue';
import App from './App.vue';
import store from '@/store';
const app = createApp(App);
app.use(store);
app.mount('#app');
  1. 运用

router-view 将显示与 url 对应的组件。你能够把它放在任何当地,以习惯你的布局。

<template>
  <router-view />
</template>

layout布局

能够创立一个 layout 根底布局页面,将公共部分如页首、页脚都包裹进来,需求 layout 的页面则作为这个 layout 的子路由。

  1. 创立 src/layout 文件夹
<!-- src/layout/index.vue -->
<template>
  <div class="layout">
    <Header />
    <div class="content">
      <router-view />
    </div>
  </div>
</template>
<script setup lang="ts">
  import Header from './Header/index.vue';
</script>
  1. 修正路由
const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    component: () => import('@/layout/index.vue'),
    redirect: '/index',
    children: [
      // 需求layout的页面
      {
        path: 'index',
        name: 'index',
        meta: {
          title: 'index',
        },
        component: () => import('@/pages/index.vue'),
      },
    ],
  },
  // 不需求layout的页面
  // 代替vue2中的'*'通配符途径
  { path: '/:pathMatch(.*)*', redirect: '/' },
];

Pinia

pinia官方文档

pinia非官方中文文档

Pinia 开始是为了探究 Vuex 的下一次迭代会是什么姿态,结合了 Vuex 5 核心团队评论中的许多主意。最终,咱们意识到 Pinia 现已完结了咱们在 Vuex 5 中想要的大部分内容,并决定完结它 取而代之的是新的主张。
与 Vuex 比较,Pinia 供给了一个更简单的 API,具有更少的标准,供给了 Composition-API 风格的 API,最重要的是,在与 TypeScript 一同运用时具有可靠的类型揣度支撑。

Pinia API 与 Vuex ≤4 有很大不同,即:

  • mutations 不再存在。他们经常被认为是 十分 冗长。他们开始带来了 devtools 集成,但这不再是问题。
  • 无需创立自界说杂乱包装器来支撑 TypeScript,一切内容都是类型化的,并且 API 的规划办法尽可能运用 TS 类型揣度。
  • 不再需求注入、导入函数、调用函数、享受主动完结功用!
  • 无需动态增加 Store,默许情况下它们都是动态的,您乃至都不会留意到。请留意,您仍然能够随时手动运用 Store 进行注册,但因为它是主动的,您无需担心。
  • 不再有 modules 的嵌套结构。您仍然能够经过在另一个 Store 中导入和 运用 来隐式嵌套 Store,但 Pinia 经过规划供给平面结构,同时仍然支撑 Store 之间的交叉组合办法。 您乃至能够具有 Store 的循环依靠联系
  • 没有 命名空间模块。鉴于 Store 的扁平架构,“命名空间” Store 是其界说办法所固有的,您能够说一切 Store 都是命名空间的。
  1. 装置
npm i pinia
  1. 创立store
// src/store/index.ts
import { createPinia } from 'pinia';
const pinia = createPinia();
export default pinia;
  1. 挂载store
// src/main.ts
import { createApp } from 'vue';
import App from './App.vue';
import store from '@/store';
const app = createApp(App);
app.use(store);
app.mount('#app');
  1. 创立useUserStore
// src/store/modules/user/index.ts
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
  // id: 'user', // id必填,且需求唯一。两种写法
  state: () => {
    return {
      name: '张三',
    };
  },
  getters: {
    nameLength: (state) => state.name.length,
  },
  actions: {
    updateName(name: string) {
      this.name = name;
    },
  },
});
  1. 运用useUserStore
<!-- src/pages/pinia/index.vue -->
<template>
  <div class="pinia">
    <div class="name">用户名:{{ userStore.name }}</div>
    <div class="length">长度:{{ userStore.nameLength }}</div>
    <van-button type="primary" @click="updateName(true)">action修正store中的name</van-button>
    <van-button @click="updateName(false)">patch修正store中的name</van-button>
  </div>
</template>
<script setup lang="ts">
  import { useUserStore } from '@/store';
  const userStore = useUserStore();
  const updateName = (isAction: boolean) => {
    if (isAction) {
      // action 修正 store 中的数据
      userStore.updateName('userStore.updateName办法');
    } else {
      // 未界说 action 时能够用 $patch 办法直接更改状态属性
      // $patch 修正 store 中的数据
      userStore.$patch({
        name: 'userStore.$patch办法',
      });
    }
  };
</script>

Axios

Axios官方文档

  1. 装置
npm i axios
  1. 新建 src/utils/http 文件夹
  • 封装axios
// src/utils/http/axios.ts
import axios, { AxiosResponse, AxiosRequestConfig, AxiosError } from 'axios';
import type { Response } from './types';
// import { auth } from '@/utils';
import { Toast } from 'vant';
import router from '@/router';
axios.defaults.timeout = 1000 * 60;
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
// 创立axios实例
const service = axios.create({
  // 依据不同env设置不同的baseURL
  baseURL: import.meta.env.VITE_APP_API_BASE_URL,
});
// axios实例阻拦恳求
service.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    config.headers = {
      ...config.headers,
      // ...auth.headers(), // 你的自界说headers,如token等
    };
    return config;
  },
  (error: AxiosError) => {
    return Promise.reject(error);
  }
);
// axios实例阻拦响应
service.interceptors.response.use(
  // 2xx时触发
  (response: AxiosResponse<Response>) => {
    // response.data便是后端回来的数据,结构依据你们的约好来界说
    const { code, message } = response.data;
    let errMessage = '';
    switch (code) {
      case 0:
        break;
      case 1: // token过期
        errMessage = 'Token expired';
        router.push('/login');
        break;
      case 2: // 无权限
        errMessage = 'No permission';
        break;
      default:
        errMessage = message;
        break;
    }
    if (errMessage) Toast.fail(errMessage);
    return response;
  },
  // 非2xx时触发
  (error: AxiosError) => {
    Toast.fail('Network Error...');
    return Promise.reject(error);
  }
);
export type { AxiosResponse, AxiosRequestConfig };
export default service;
// src/utils/http/types.ts
// 和后端约好好接口回来的数据结构
export interface Response<T = any> {
  code: number | string;
  message: string;
  result: T;
}
  • 封装恳求办法
// src/utils/http/index.ts
import service, { AxiosRequestConfig } from './axios';
export * from './types';
export const request = <T = any>(config: AxiosRequestConfig): Promise<T> => {
  return new Promise((resolve, reject) => {
    service
      .request(config)
      .then((res) => {
        // 一些事务处理
        resolve(res.data);
      })
      .catch((err) => {
        console.log('request fail:', err);
      });
  });
};
const http = {
  get<T = any>(url: string, params = {}, config?: AxiosRequestConfig): Promise<T> {
    return request({ url, params, ...config, method: 'GET' });
  },
  post<T = any>(url: string, data = {}, config?: AxiosRequestConfig): Promise<T> {
    return request({ url, data, ...config, method: 'POST' });
  },
  put<T = any>(url: string, data = {}, config?: AxiosRequestConfig): Promise<T> {
    return request({ url, data, ...config, method: 'PUT' });
  },
  delete<T = any>(url: string, data = {}, config?: AxiosRequestConfig): Promise<T> {
    return request({ url, data, ...config, method: 'DELETE' });
  },
  // 上传文件,指定 'Content-Type': 'multipart/form-data'
  upload<T = any>(url: string, data = {}, config?: AxiosRequestConfig): Promise<T> {
    return request({
      url,
      data,
      ...config,
      method: 'POST',
      headers: { 'Content-Type': 'multipart/form-data' },
    });
  },
};
export default http;

封装axios的办法多种多样,依据自己喜爱的办法完结就好,还能够依据需求增加重试或许撤销恳求等办法

  1. 创立api文件夹
// src/api/user/index.ts
import http, { Response } from '@/utils/http';
export interface LoginParams {
  username: string;
  password: string;
}
interface UserInfo {
  id: number;
  username: string;
  mobile: number;
  email: string;
}
export default {
  async login(params: LoginParams) {
    return await http.post<Response<UserInfo>>('/user/login', params);
  },
};
  1. 调用api
import Api from '@/api/user';
const login = async () => {
    const { code, result, message } = await Api.login(loginInfo);
    // do something
};

移动端调试

github: vConsole

一个轻量、可拓展、针对手机网页的前端开发者调试面板。
vConsole 是结构无关的,能够在 Vue、React 或其他任何结构中运用。
现在 vConsole 是微信小程序的官方调试东西。

在vite中,咱们需求合作 vite-plugin-vconsole 插件来运用

github: vite-plugin-vconsole

一个适用于Vite的插件,协助开发者在各个环境下方便运用VConsole的功用。能够方便装备区别环境,依据环境动态加载VConsole,支撑多页面装备。

  1. 装置
npm i vconsole
npm i vite-plugin-vconsole -D
  1. 装备
// vite.config.ts
plugin: [
  viteVConsole({
    entry: pathResolve('src/main.ts'),
    localEnabled: true,
    enabled: env.VITE_BUILD_VCONSOLE === 'true',
    config: {
      maxLogNumber: 1000,
      theme: 'dark',
    },
  }),
]
  1. 增加躲藏开关

虽然经过 env.VITE_BUILD_VCONSOLE 能够依据环境变量是敞开 vconsole ,可是有时分只让某个环境的部分人能运用,这个时分,能够增加一个躲藏开关,默许不显示 vconsole ,只要手动翻开躲藏开关才显示。

思路:
    1. env.VITE_BUILD_VCONSOLE 设置为true,敞开 vconsole 功用
    2. 经过 css 默许躲藏 vconsole
    3. 在登录页url中增加一个参数 'debug',登录时假如检测到 debug === 1,则不躲藏 vconsole

3.1 供给一个debug东西办法

// src/utils/debug.ts
import { storage } from './storage';
// MODE,即env[MODE]文件的环境名称(应用运行的形式)
const { MODE, VITE_BUILD_VCONSOLE } = import.meta.env;
// 传入debug参数,将debug存入/移除localStorage
const config = (debug: any) => {
  if (debug === '1') {
    storage.setItem('debug', debug);
  } else {
    storage.removeItem('debug');
  }
  init();
};
// 初始化 vconsole,操控躲藏/显示
const init = () => {
  const vc = <HTMLElement>document.querySelector('#__vconsole');
  const debug = storage.getItem('debug');
  if (VITE_BUILD_VCONSOLE === 'true' && MODE === 'test' && vc) {
    vc.style.display = debug === '1' ? '' : 'none';
  }
};
export default { init, config };

3.2 在登录页获取参数

// src/pages/login/index.vue
import debug from '@/utils/debug';
const router = useRouter();
// 进入登录页时获取debug参数
onMounted(() => {
  debug.config(route.query.debug);
});

3.3 在app.vue中初始化

// src/App.vue
import debug from '@/utils/debug';
// 因为debug是存入localStorage中的,刷新页面会从localStorage取出,依据debug操控是否躲藏
onMounted(() => {
  debug.init();
});

3.4 运用

登录时在url中增加参数 debug=1 即可敞开

http://localhost:5173/#/login?debug=1

该躲藏开关只能在 login 页手动敞开,debug 的值存储在 localStorage 中保证刷新页面不会丢掉,回到 login 页 debug 被铲除,需重新增加 debug=1 参数才干敞开

Hooks

  • Hooks 不是全新的技能,它是一种开发思维

  • vue中一般称为 组合式API

  • 能够把 hooks 理解为 vue2 中 mixin 的升级版

  • 一个比较优异的库:VueUse

  • vant中也有一些常用的hooks vant: 组合式API

自界说hooks

下面以自界说一个 loading hooks 示例:

// src/hooks/useLoading.ts
import { Toast } from 'vant';
export function useLoading() {
  let toast: any = null;
  const startLoading = () => {
    toast = Toast.loading({
      duration: 0,
      forbidClick: true,
      message: 'Loading...',
    });
  };
  const stopLoading = () => {
    toast && toast.clear();
  };
  onBeforeUnmount(stopLoading);
  return { startLoading, stopLoading };
}

运用

import { useLoading } from '@/hooks';
const { startLoading, stopLoading } = useLoading();
const onSubmit = async () => {
  startLoading();
  const { code, result, message } = await Api.login(loginInfo);
  stopLoading();
  // do something
};

到这儿项目的一些根本装备就结束了

下面是一些封装的事务组件或许小功用,不感兴趣的能够停步于此了


关于可选链(Optional chaining)(?.)的运用问题

这个问题对大部分人在大部分场景下并无影响,感兴趣的能够看看

首先看 caniuse 上关于 Optional chaining operator 的兼容性表
能够看到 可选链 需求 Chrome >= 80

所以当运用了可选链的时分,在 Chrome < 80 的浏览器上就会看到如下报错 (本地serve环境时,具体原因下方会解释)

[Vue Router warn]: uncaught error during route navigation:
SyntaxError {}
  message: "Unexpected token '.'"
  stack: "SyntaxError: Unexpected token '.'"
  __proto__: SyntaxError {}
Uncaught (in promise) 
  Object {name: "SyntaxError", message: "Unex...
  message: "Unexpected token '.'"
  name: "SyntaxError"
  stack: "SyntaxError: Unexpected token '.'"
  __proto__: Object {}

乍一看认为是 Vue Router 的问题,其实要点在下方,Unexpected token '.' ,这是浏览器不辨认可选链 .?

这个问题在vite的issues下有激烈的评论:

Unable to support functions such as “optional chain” in QQ browser 10 or chrome 70
这个问题是说 dev 时无法在 Chrome 70 下运用 optional chaining 语法?build 后没问题
vite 在 dev 形式下转译 sfc 时没有为 esbuild 指定输出方针,导致始终被输出为 esnext
Vite 默许的假设便是 dev 环境是跑在最新的浏览器上的,esbuild 仅仅拿来处理非标准的语法

依据issues,咱们选用vite开发人员 @sodatea 大佬提出的 rollup-plugin-esbuild 插件的办法

// vite.config.ts
import esbuild from 'rollup-plugin-esbuild';
plugins: [
  {
    ...esbuild({
      target: 'chrome70',
      // 如有需求能够在这儿加 js ts 之类的其他后缀
      include: /.(vue|ts|js)$/,
      loaders: {
        '.vue': 'js',
      },
    }),
    enforce: 'post',
  },
]

这确实能处理可选链的运用问题,可是,新的问题又出现了:
运用 rollup-plugin-esbuild 插件办法,会导致sourcemap错乱,无法在devtool里正常debug

issues里有提到另一个插件 @rollup/plugin-babel,经过babel的办法来处理,依据文档测验进行各种装备都不行,最终找到了另一位大佬 @hamflx 的文章:

vite 兼容性踩坑记载
关于 ts 项目,需求装备 extensions 才行
不过,扩展名里加 .vue 的话会报错,一般来说 .vue 文件编译之后会是 js,可是 .vue 里边假如包含了款式,会独自提取出来作为一个虚拟的文件,经过查询参数 type=style 来读取,这儿以 babel 来转译款式文件当然报错
filter 选项与扩展名之间是且的联系,经过其限定一下,只转义以 .vue 为后缀的文件就行了

// vite.config.ts
import babel from '@rollup/plugin-babel';
export default defineConfig({
  plugins: [
    babel({
      babelHelpers: 'bundled',
      plugins: [ '@babel/plugin-proposal-optional-chaining' ]
      include: include: [/.vue$/, /.ts$/],
      extensions: ['.vue', '.ts'],
    })
  ]
})

到这儿认为就结束了吗,不,没有

虽然 @rollup/plugin-babel 处理了可选链问题,sourcemap也看似正常,但实际上仅仅ts文件sourcemap正常,source面板里vue原文件会无法增加断点

测验处理:在babel装备里增加 sourceMaps: false ,封闭babel自己的sourcemap后,能够增加断点,可是会和 rollup-plugin-esbuild 插件的办法相同导致断点错乱

暂时没有找到更好的处理办法,只能摆烂了:
在 serve 环境时,假如需求处理低版别chrome可选链报错问题,就翻开上面的 babel 装备;假如需求 debug ,则注释掉 babel 装备
build 时 vite 会对文件进行转译以支撑低版别浏览器,不影响

假如大佬有完美处理办法,请不吝赐教

Watermark 水印

前端水印的完结原理等我就不献丑了,我也是从大佬那里扒来的然后自己略微修饰了一下,能够去看大佬的解说:
@microzz: 前端水印生成计划(网页水印+图片水印)

或许看这位大佬的更深入的解说:
@程序员秋风: 从破解某规划网站谈前端水印(具体教程)

这儿我用到的仅是网页水印,没有用到图片水印哦
一开始我用的是canvas生成水印,可是有一个问题,canvas生成的水印总是看起来有点模糊,各种调整缩放份额都仍是模糊,后来改为svg生成水印,就十分清晰了
下面是svg水印计划具体完结:

// src/utils/lib/watermark.js
let mo = null;
// 增加水印
function add({
  container = document.body,
  width = '200',
  height = '200',
  rotate = -20,
  style = 'font-family: Arial; font-weight: bold',
  fontSize = '16px',
  opacity = 0.12,
  content = '内部资料,制止别传',
  zIndex = 1000,
} = {}) {
  const svgStr = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
                    <text x="10" y="50%"
                      transform="rotate(${rotate}, ${width / 2} ${height / 2})"
                     hljs-subst">${style}; font-size: ${fontSize}; opacity: ${opacity}">
                      ${content}
                    </text>
                  </svg>`;
  const base64Url = `data:image/svg+xml;base64,${window.btoa(
    unescape(encodeURIComponent(svgStr))
  )}`;
  const __wm = document.querySelector('.__wm');
  const watermarkDiv = __wm || document.createElement('div');
  const styleStr = `
    position:absolute;
    top:0px;
    left:0px;
    width:100%;
    height:100%;
    z-index:${zIndex};
    pointer-events:none;
    background-repeat:repeat;
    background-image:url('${base64Url}')`;
  watermarkDiv.setAttribute('style', styleStr);
  watermarkDiv.classList.add('__wm');
  container.style.position = 'relative';
  if (!__wm) {
    container.appendChild(watermarkDiv);
  }
  const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
  if (MutationObserver) {
    const args = arguments[0];
    mo = new MutationObserver(function () {
      const __wm = document.querySelector('.__wm');
      if (
        (__wm && __wm.getAttribute('style') !== styleStr) ||
        !__wm ||
        container.style.position !== 'relative'
      ) {
        mo.disconnect();
        mo = null;
        add(args);
      }
    });
    mo.observe(container, {
      attributes: true,
      subtree: true,
      childList: true,
    });
  }
}
// 移除水印
function remove() {
  const __wm = document.querySelector('.__wm');
  if (__wm) {
    mo.disconnect();
    mo = null;
    document.body.removeChild(__wm);
  }
}
export default { add, remove };
// src/App.vue
import watermark from '@/utils/lib/watermark';
onMounted(() => {
  const { username = '', mobile = '' } = auth.getUser();
  watermark.add({ content: username + ' ' + mobile });
});
onBeforeUnmount(() => {
  watermark.remove();
});

在登录页时还没有用户信息,所以不需求水印,你也能够省掉在登录页先移除水印再增加水印的操作,只要保证进入登录页时你存储的用户信息为空,那水印的内容就为空了

// src/pages/login/index.vue
import watermark from '@/utils/lib/watermark';
onMounted(() => {
  watermark.remove();
});
onBeforeUnmount(() => {
  // const { username = '', mobile = '' } = auth.getUser();
  watermark.add({
    // content: username + ' ' + mobile,
  });
});

大局制止复制张贴

// src/utils/lib/copy-paste.js
function copyPaste(bool) {
  // 右键菜单
  document.oncontextmenu = function () {
    return bool;
  };
  // 文字挑选
  document.onselectstart = function () {
    return bool;
  };
  // 复制
  document.oncopy = function () {
    return bool;
  };
  // 剪切
  document.oncut = function () {
    return bool;
  };
  // 张贴
  document.onpaste = function () {
    return bool;
  };
}
function enable() {
  copyPaste(true);
}
function disable() {
  copyPaste(false);
}
export default { enable, disable };
// src/App.vue
import copyPaste from '@/utils/lib/copy-paste';
onMounted(() => {
 copyPaste.disable();
});
onBeforeUnmount(() => {
  copyPaste.enable();
});

答应登录页复制张贴

// src/pages/login/index.vue
import copyPaste from '@/utils/lib/copy-paste';
onMounted(() => {
  copyPaste.enable();
});
onBeforeUnmount(() => {
  copyPaste.disable();
});

定位

这儿选用的是浏览器API:navigator.geolocation

mdn: Navigator.geolocation
安全上下文: 此项功用仅在一些支撑的浏览器的安全上下文(HTTPS)中可用。
出于安全考虑,当网页恳求获取用户位置信息时,用户会被提示进行授权。留意不同浏览器在恳求权限时有不同的战略和办法。

geolocation 定位依靠于浏览器,也不能直接操控用户翻开设备的 GPS 功用,仅能经过浏览器向用户恳求获取定位权限,并且假如用户回绝授权,将无法再次向用户建议权限恳求。

此办法约束太多,权当图一乐

// src/utils/geo.ts
export const geo = {
  // 获取定位
  getLocation(): Promise<Partial<GeolocationCoordinates>> {
    return new Promise((resolve, reject) => {
      if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(
          (position) => {
            const { latitude, longitude } = position.coords;
            resolve({ latitude, longitude });
          },
          (err) => {
            console.log(`getPosError:${err.code},${navigator.geolocation},${err.message}`);
          }
        );
      } else {
        console.log('This browser does not support getting geolocation');
      }
    });
  },
  // 依据定位翻开google地图
  openMap({ latitude, longitude }: Record<string, string | number>) {
    if (latitude && longitude) {
      const href = `https://www.google.com/maps/place/${Number.parseFloat(
        <string>latitude
      )},${Number.parseFloat(<string>longitude)}`;
      window.open(href, '_blank');
    }
  },
};
<!-- src/pages/geo/index.vue -->
<template>
  <div class="geo">
    <van-button type="primary" @click="getGeo">获取定位</van-button>
    <div>当时经纬度: {{ state.position }}</div>
    <van-button type="primary" @click="geo.openMap(state.position)">翻开地图</van-button>
  </div>
</template>
<script setup lang="ts">
  import { geo } from '@/utils';
  const state = reactive({
    position: {},
  });
  const getGeo = async () => {
    state.position = await geo.getLocation();
  };
</script>

图片旋转

有两种办法完结在vant组件 ImagePreview 图片预览 中点击旋转图片

  1. v-bind 办法:

vue官方文档: CSS 中的 v-bind()

在 style 标签中经过 v-bind 绑定一个 rotate 变量,然后点击按钮改变 rotate 值达到旋转

当在 css 中运用 v-bind 时,vue会在该组件的根元素(一切根元素,因为vue3答应组件有多个根元素)上增加一个css局部变量,如 style="--e31f55e6-state_rotate:0deg;" ,然后 v-bind 会被编译为 var(--e31f55e6-state_rotate),其实便是运用的css变量来完结的

<!-- src/pages/image-rotate/index.vue -->
<template>
  <div class="image-preview-rotate-bind">
    <p>v-bind办法</p>
    <van-image width="100" height="100" :src="img" @click="onPreviewBind" />
    <van-image-preview v-model:show="state.showPreviewBind" :images="[img]">
      <template #cover><van-icon name="replay" @click="setRotateBind" /></template>
    </van-image-preview>
  </div>
</template>
<script setup lang="ts">
  import { ImagePreview } from 'vant';
  // ImagePreview 是一个函数,ImagePreview.Component才是组件
  const VanImagePreview = ImagePreview.Component;
  const img = 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg';
  const state = reactive({
    showPreviewBind: false,
    rotate: '0deg',
  });
  const onPreviewBind = () => {
    state.showPreviewBind = true;
    state.rotate = '0deg';
  };
  const setRotateBind = () => {
    state.rotate = parseInt(state.rotate) + 90 + 'deg';
  };
</script>
<style lang="less">
  .van-image-preview__cover {
    font-size: 40px;
    color: #fff;
    left: 50%;
    top: auto;
    bottom: var(--van-padding-md);
    transform: translate(-50%);
    &:active {
      opacity: 0.4;
    }
  }
  .image-preview-rotate-bind .van-image-preview__image .van-image__img {
    transform: rotate(v-bind('state.rotate'));
  }
</style>
  1. css 大局变量办法

假如因为某种原因,你不得不将 ImagePreview 这个组件挂载的节点指定为 body ,那么,上面 v-bind 的办法就无效了,因为 v-bind 生成的是 css 局部变量,而组件已被挂载在 body 节点

咱们能够依样画葫芦,自界说 css 大局变量来完结

// src/utils/util.ts
/**
 * 动态设置css大局变量完结旋转
 * transform: rotate(var(--image-rotate))
 * @param deg 旋转视点
 * @param prop css变量, 默许'--image-rotate'
 */
export const setRotate = (deg: string, prop = '--image-rotate') => {
  let rotate = document.documentElement.style.getPropertyValue(prop) || '0deg';
  if (typeof deg === 'string') {
    rotate = deg;
  } else {
    rotate = parseInt(rotate) + 90 + 'deg';
  }
  document.documentElement.style.setProperty(prop, rotate);
};
<!-- 改动点: -->
<van-image-preview
  v-model:show="state.showPreview"
  :images="[img]"
  teleport="body"
  class-name="image-preview-rotate"
>
  <template #cover><van-icon name="replay" @click="setRotate" /></template>
</van-image-preview>
<script setup lang="ts">
import { setRotate } from '@/utils';
const onPreview = () => {
  state.showPreview = true;
  setRotate('0deg');
};
</script>
<style lang="less">
  .image-preview-rotate .van-image-preview__image .van-image__img {
    transform: rotate(var(--image-rotate));
  }
</style>

预览图片旋转就讲完了。趁便一提,要完结点击图片预览这个功用需求 vant 的 Image 和 ImagePreview 两个组件合作运用,再加上旋转的话,代码就稍稍繁琐了,咱们能够自己封装一个 ImagePreview 组件,将这些功用打包起来,方便运用。因为完结比较简单,我就不献丑了

Icon组件

有时分项目中有一些图标,一个一个导入比较费事,想和 vant 的 Icon 组件相同传入一个 name 就可运用,就能够自己封装一个 Icon 组件

完结主动导入图片首要依靠 new URL 这个 API

vite: new URL(url, import.meta.url)
import.meta.url 是一个 ESM 的原生功用,会露出当时模块的 URL。将它与原生的 URL 结构器 组合运用,在一个 JavaScript 模块中,经过相对途径咱们就能得到一个被完好解析的静态资源 URL:

const imgUrl = new URL('./img.png', import.meta.url).href
document.getElementById('hero-img').src = imgUrl

这在现代浏览器中能够原生运用 – 实际上,Vite 并不需求在开发阶段处理这些代码!
这个形式同样还能够经过字符串模板支撑动态 URL:

function getImageUrl(name) {
  return new URL(`./dir/${name}.png`, import.meta.url).href
}

具体完结:

<!-- src/components/AppIcon/index.vue -->
<template>
  <i class="icon" :style="{ 'background-image': `url(${iconUrl})` }" />
</template>
<script setup lang="ts">
  const props = defineProps({
    name: {
      type: String,
      required: true,
    },
  });
  const iconUrl = computed(() => {
    return new URL(`/src/assets/icons/${props.name}.png`, import.meta.url).href;
  });
</script>
<style lang="less" scoped>
  .icon {
    display: inline-block;
    width: 24px;
    height: 24px;
    background: center / contain;
    vertical-align: middle;
  }
</style>

运用:

<AppIcon name="logo" />

其他

因为ui规划问题,我项目中还需求 web 端如 antd、element 上面那种 menu 和 table 组件,正常的移动端项目是肯定不需求这种组件的,迫于无法仍是自己封装了这俩组件 ‍♂️

menu 组件是根据 van-collapse 组件封装,集合了点击打开子菜单,路由跳转,当时途径高亮,侧边收缩等功用
table 组件是根据原生 table 元素封装,集合了打开子行,勾选行,翻页,传入 columns 列时支撑 render 和 slot 两种写法等。

具体完结就不写了,一般人也不需求这类奇葩组件,源码我放 github 上了,期望咱们用不上