Skip to content

ynzy/vite-vue3-h5-template

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

53 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🎉 基于 vite2 + Vue3.2 + TypeScript + pinia + mock + sass + vantUI + viewport 适配 + axios 封装 的基础模版

查看 demo 建议手机端查看

前述

原来 vuecli 项目和现在 vite-vue 项目开发/生产速度对比

开发启动速度对比

  • vue-cli

    • 等了几秒 

vuecli开发启动速度.png

  • vite-vue

    • 几乎没等待 

vite开发启动速度.png

  • 总结:vite 启动速度 是 vue-cli 的 5倍

开发热更新速度对比

  • vue-cli

    • 需要重新编译文件 

vuelciHMR热更新速度.png

  • vite-vue

    • 几乎没有花时间,代码改了就生效了

viteHMR热更新速度.png

  • 总结:vite 即时生效

生产打包速度对比

  • vue-cli 

vuecli打包速度.png

  • vite-vue 

vite打包速度.png

  • 总结:几乎没什么差别

项目介绍

Node 版本要求

本示例 Node.js v17.2.0

项目安装/启动

  • 本项目采用 pnpm 包管理器,如果没有请先安装 pnpm
  • 使用其他包管理器请删除 pnpm-lock.yaml
npm i -g pnpm // 全局安装 pnpm
pnpm install // 安装依赖
pnpm dev // 开发
pnpm build // 打包
pnpm preview  // 本地预览打包的项目

目录

✅ 使用 create-vue 初始化项目

createVue
npm init vue@3

Vue.js - The Progressive JavaScript Framework

 Project name:  vite-vue3-h5-template

 Add TypeScript?   Yes

 Add JSX Support?   Yes

 Add Vue Router for Single Page Application development?   Yes

 Add Pinia for state management?   Yes

 Add Cypress for testing?    No

 Add ESLint for code quality?   Yes

 Add Prettier for code formatting?   Yes


  • 初始化项目包含
    • Vite
    • Vue3.2
    • Vue-router4
    • TypeScript
    • Jsx
    • Pinia
    • Eslint
    • Prettier
    • @types/node // 识别 nodejs 内置模块

▲ 回顶部

✅ 配置 ip 访问项目

  • vite 启动后出现 “ Network: use --host to expose ”
vite v2.3.7 dev server running at:

  > Local: http://localhost:3000/
  > Network: use `--host` to expose
  • 是因为 IP 没有做配置,所以不能从 IP 启动,需要在 vite.config.js 做相应配置: 在 vite.config.js 中添加 server.host 为 0.0.0.0
export default defineConfig({
  plugins: [vue()],
  // 在文件中添加以下内容
  server: {
    host: '0.0.0.0'
  }
})
  • 重新启动后显示
vite v2.3.7 dev server running at:

  > Local:    http://localhost:3000/
  > Network:  http://192.168.199.127:3000/

▲ 回顶部

✅ 配置多环境变量

  • 在生产环境,会把 import.meta.env 的值转换成对应真正的值
  1. 添加环境变量文件,每个文件写入配置,定义 env 环境变量前面必须加 VITE_
  • .env.development
# must start with VITE_
VITE_ENV = 'development'
VITE_OUTPUT_DIR = 'dev'
  • .env.production
# must start with VITE_
VITE_ENV = 'production'
VITE_OUTPUT_DIR = 'dist'
  • .env.test
# must start with VITE_
VITE_ENV = 'test'
VITE_OUTPUT_DIR = 'test'
  1. 修改 scripts 命令
  • --mode 用来识别我们的环境
"dev": "vite --mode development",
"test": "vite --mode test",
"prod": "vite --mode production",
  1. 在项目中访问
console.log(import.meta.env)
  1. typescript 智能提示
  • 修改 src/env.d.ts 文件,如果没有创建一个
/// <reference types="vite/client" />

interface ImportMetaEnv extends Readonly<Record<string, string>> {
  readonly VITE_ENV: string; // 环境
  readonly VITE_OUTPUT_DIR: string; // 打包目录
}
interface ImportMeta {
  readonly env: ImportMetaEnv;
}

动态导入环境配置

// config/env.development.ts
// 本地环境配置
export default {
  env: 'development',
  mock: true,
  title: '开发',
  baseUrl: 'http://localhost:9018', // 项目地址
  baseApi: 'https://test.xxx.com/api', // 本地api请求地址,注意:如果你使用了代理,请设置成'/'
  APPID: 'wx9790364d20b47d95',
  APPSECRET: 'xxx',
  $cdn: 'https://imgs.solui.cn'
}
// config/index.ts
export interface IConfig {
  env: string // 开发环境
  mock?: string // mock数据
  title: string // 项目title
  baseUrl?: string // 项目地址
  baseApi?: string // api请求地址
  APPID?: string // 公众号appId  一般放在服务器端
  APPSECRET?: string // 公众号appScript 一般放在服务器端
  $cdn: string // cdn公共资源路径
}
const envMap = {}
const globalModules = import.meta.globEager('./*.ts')
Object.entries(globalModules).forEach(([key, value]) => {
  // key.match(/\.\/env\.(\S*)\.ts/)
  const name = key.replace(/\.\/env\.(.*)\.ts$/, '$1')
  envMap[name] = value
})

// 根据环境引入不同配置
export const config = envMap[import.meta.env.VITE_ENV].default
console.log('根据环境引入不同配置', config)

▲ 回顶部

✅ 配置 alias 别名

  • 项目初始化已经配置好了一个 src 别名
import { fileURLToPath } from 'url'

resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },

▲ 回顶部

✅ Sass 全局样式

  1. 安装依赖 使用dart-sass, 安装速度比较快,大概率不会出现安装不成功
pnpm i -D sass
  1. 使用 每个页面自己对应的样式都写在自己的 .vue 文件之中 scoped 它顾名思义给 css 加了一个域的概念。
<style lang="scss">
  /* global styles */
</style>

<style lang="scss" scoped>
  /* local styles */
</style>

css modules

  • 目前测试只有在 tsx 中可以正常使用,vue-template 中可以导入在 js 中使用,<template> 中还不知道怎么使用
  • 定义一个 *.module.scss 或者 *.module.css 文件
  • 在 tsx 中使用
import { defineComponent } from 'vue'
import classes from '@/styles/test.module.scss'
export default defineComponent({
  setup() {
    console.log(classes)
    return () => {
      return <div class={`root  ${classes.moduleClass}`}>测试css-modules</div>
    }
  }
})

vite 识别 sass 全局变量

  • vite.config.js 添加配置
css: {
    preprocessorOptions: {
      scss: {
        additionalData: `
          @import "@/styles/mixin.scss";
          @import "@/styles/variables.scss";
          `,
      },
    },
  },

▲ 回顶部

✅ Vue-router4

  • 初始化项目集成了 vue-router,我们这里只做配置
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { routes } from './router.config'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})

export default router
// router/router.config.ts
import { RouteRecordRaw, createRouter, createWebHistory } from 'vue-router'
import Layout from '@/views/layouts/index.vue'
export const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'Home',
    redirect: '/home',
    meta: {
      title: '首页',
      keepAlive: false
    },
    component: Layout,
    children: [
      {
        path: '/home',
        name: 'Home',
        component: () => import('@/views/Home.vue'),
        meta: { title: '首页', keepAlive: false, showTab: true }
      },
      {
        path: '/tsx',
        name: 'Tsx',
        component: () => import('@/test/demo'),
        meta: { title: '测试tsx', keepAlive: false, showTab: true }
      },
      {
        path: '/static',
        name: 'Static',
        component: () => import('@/test/testStatic.vue'),
        meta: { title: '测试静态资源', keepAlive: false, showTab: true }
      },
      {
        path: '/cssModel',
        name: 'CssModel',
        component: () => import('@/test/testCssModel'),
        meta: { title: '测试css-model', keepAlive: false, showTab: true }
      },
      {
        path: '/mockAxios',
        name: 'MockAxios',
        component: () => import('@/test/testMockAxios'),
        meta: { title: '测试mock-axios', keepAlive: false, showTab: true }
      },
      {
        path: '/pinia',
        name: 'Pinia',
        component: () => import('@/test/testPinia.vue'),
        meta: { title: '测试pinia', keepAlive: false, showTab: true }
      }
    ]
  }
]

▲ 回顶部

✅ Pinia 状态管理

  • 初始化项目集成了 pinia ,我们这里只做配置
  • 文档:https://pinia.vuejs.org/
  • 参考资料:https://juejin.cn/post/7049196967770980389
  • Pinia 的特点:
    • 完整的 typescript 的支持;
    • 足够轻量,压缩后的体积只有 1.6kb;
    • 去除 mutations,只有 state,getters,actions(这是我最喜欢的一个特点);
    • actions 支持同步和异步;
    • 没有模块嵌套,只有 store 的概念,store 之间可以自由使用,更好的代码分割;
    • 无需手动添加 store,store 一旦创建便会自动添加;

安装依赖

pnpm i pinia

创建 Store

  • 新建 src/store 目录并在其下面创建 index.ts,导出 store
// src/store/index.ts

import { createPinia } from 'pinia'

const store = createPinia()

export default store

在 main.ts 中引入并使用

// src/main.ts

import { createApp } from 'vue'
import App from './App.vue'
import store from './store'

const app = createApp(App)
app.use(store)

定义 State

  • 在 src/store 下面创建一个 user.ts
//src/store/user.ts

import { defineStore } from 'pinia'
import { useAppStore } from './app'

export const useUserStore = defineStore({
  id: 'user',
  state: () => {
    return {
      name: '张三',
      age: 18
    }
  },
  getters: {
    fullName: (state) => {
      return state.name + '丰'
    }
  },
  actions: {
    updateState(data: any) {
      this.$state = data
      this.updateAppConfig()
    },
    updateAppConfig() {
      const appStore = useAppStore()
      appStore.setData('app-update')
    }
  }
})
//src/store/app.ts
import { defineStore } from 'pinia'

export const useAppStore = defineStore({
  id: 'app',
  state: () => {
    return {
      config: 'app'
    }
  },
  actions: {
    setData(data: any) {
      console.log(data)
      this.config = data
    }
  }
})

获取/更新 State

<script setup lang="ts">
import { useUserStore } from '@/store/user'
import { useAppStore } from '@/store/app'
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
const userStore = useUserStore()
const appStore = useAppStore()
console.log(appStore.config)
console.log(userStore)
console.log(userStore.name)
const name = computed(() => userStore.name)
const { age } = storeToRefs(userStore)

const updateUserState = () => {
  const { name, age } = userStore.$state
  userStore.updateState({
    name: name + 1,
    age: age + 1
  })
}
</script>
<template>
  <div>姓名:{{ name }}</div>
  <div>年龄:{{ age }}</div>
  <div>计算的名字:{{ userStore.fullName }}</div>
  <div>app的config: {{ appStore.config }}</div>
  <button @click="updateUserState">更新数据</button>
</template>

<style lang="scss" scoped></style>

数据持久化

  • 插件 pinia-plugin-persistedstate 可以辅助实现数据持久化功能。

  • 数据默认存在 sessionStorage 里,并且会以 store 的 id 作为 key。

  • 安装依赖

pnpm i pinia-plugin-persistedstate
  • 引用插件
// src/store/index.ts

import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const store = createPinia()
store.use(piniaPluginPersistedstate)
export default store
  • 在对应的 store 里开启 persist 即可
export const useUserStore = defineStore({
  id: 'user',

  state: () => {
    return {
      name: '张三'
    }
  },

  // 开启数据缓存
  persist: {
    key: 'user',
    storage: sessionStorage, // 数据存储位置,默认为 localStorage
    paths: ['name'], // 用于部分持久化状态的点表示法路径数组,表示不会持久化任何状态(默认为并保留整个状态)
    overwrite: true
  }
})

▲ 回顶部

✅ 使用 Mock 数据

1. 安装依赖

pnpm i -D vite-plugin-mock mockjs @types/mockjs

2. 生产环境 相关封装

// mock/_createProductionServer.ts
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'

const modules = import.meta.globEager('./**/*.ts')

const mockModules: any[] = []
Object.keys(modules).forEach((key) => {
  if (key.includes('/_')) {
    return
  }
  mockModules.push(...modules[key].default)
})

/**
 * Used in a production environment. Need to manually import all modules
 */
export function setupProdMockServer() {
  createProdMockServer(mockModules)
}
// mock/_util.ts
// Interface data format used to return a unified format

import { Recordable } from 'vite-plugin-mock'

export function resultSuccess<T = Recordable>(result: T, { message = 'ok' } = {}) {
  return {
    code: 0,
    result,
    message,
    type: 'success'
  }
}

export function resultPageSuccess<T = any>(
  page: number,
  pageSize: number,
  list: T[],
  { message = 'ok' } = {}
) {
  const pageData = pagination(page, pageSize, list)

  return {
    ...resultSuccess({
      items: pageData,
      total: list.length
    }),
    message
  }
}

export function resultError(message = 'Request failed', { code = -1, result = null } = {}) {
  return {
    code,
    result,
    message,
    type: 'error'
  }
}

export function pagination<T = any>(pageNo: number, pageSize: number, array: T[]): T[] {
  const offset = (pageNo - 1) * Number(pageSize)
  const ret =
    offset + Number(pageSize) >= array.length
      ? array.slice(offset, array.length)
      : array.slice(offset, offset + Number(pageSize))
  return ret
}

export interface requestParams {
  method: string
  body: any
  headers?: { authorization?: string }
  query: any
}

/**
 * @description 本函数用于从request数据中获取token,请根据项目的实际情况修改
 *
 */
export function getRequestToken({ headers }: requestParams): string | undefined {
  return headers?.authorization
}
// mock/sys/user
import { MockMethod } from 'vite-plugin-mock'
import { resultError, resultSuccess, getRequestToken, requestParams } from '../_util'

export default [
  {
    url: '/basic-api/getUserInfo',
    method: 'get',
    response: (request: requestParams) => {
      console.log('----请求了getUserInfo---')

      return resultSuccess({
        name: '章三',
        age: 40,
        sex: '男'
      })
    }
  }
] as MockMethod[]

3. 修改 vite.config.ts 配置

export default ({ mode, command }: ConfigEnv): UserConfigExport => {
  const isBuild = command === 'build'
  return defineConfig({
    plugins: [
      viteMockServe({
        ignore: /^_/, // 正则匹配忽略的文件
        mockPath: 'mock', // 设置mock.ts 文件的存储文件夹
        localEnabled: true, // 设置是否启用本地 xxx.ts 文件,不要在生产环境中打开它.设置为 false 将禁用 mock 功能
        prodEnabled: true, // 设置生产环境是否启用 mock 功能
        watchFiles: true, // 设置是否监视mockPath对应的文件夹内文件中的更改
        // 代码注入
        injectCode: ` 
          import { setupProdMockServer } from '../mock/_createProductionServer';
          setupProdMockServer();
        `
      })
    ]
  })
}

▲ 回顶部

✅ 配置 proxy 跨域

server: {
  host: '0.0.0.0',
  proxy: {
    // 字符串简写写法
    '/foo': 'http://localhost:4567',
    // 选项写法
    '/api': {
      target: 'http://jsonplaceholder.typicode.com',
      changeOrigin: true,
      rewrite: (path) => path.replace(/^\/api/, '')
    },
    // 正则表达式写法
    '^/fallback/.*': {
      target: 'http://jsonplaceholder.typicode.com',
      changeOrigin: true,
      rewrite: (path) => path.replace(/^\/fallback/, '')
    }
    // 使用 proxy 实例
    // "/api": {
    //   target: "http://jsonplaceholder.typicode.com",
    //   changeOrigin: true,
    //   configure: (proxy, options) => {
    //     // proxy 是 'http-proxy' 的实例
    //   },
    // },
  }
},

▲ 回顶部

✅ Axios 封装及接口管理

utils/request.js 封装 axios ,开发者需要根据后台接口做修改。

  • service.interceptors.request.use 里可以设置请求头,比如设置 token
  • config.hideloading 是在 api 文件夹下的接口参数里设置,下文会讲
  • service.interceptors.response.use 里可以对接口返回数据处理,比如 401 删除本地信息,重新登录
/**
 * @description [ axios 请求封装]
 */
import store from '@/store'
import axios, { AxiosResponse, AxiosRequestConfig } from 'axios'
// import { Message, Modal } from 'view-design' // UI组件库
import { Dialog, Toast } from 'vant'
import router from '@/router'
// 根据环境不同引入不同api地址
import config from '@/config'

const service = axios.create({
  baseURL: config.baseApi + '/api', // url = base url + request url
  timeout: 5000,
  withCredentials: false // send cookies when cross-domain requests
  // headers: {
  // 	// clear cors
  // 	'Cache-Control': 'no-cache',
  // 	Pragma: 'no-cache'
  // }
})

// Request interceptors
service.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    // 加载动画
    if (config.loading) {
      Toast.loading({
        message: '加载中...',
        forbidClick: true
      })
    }
    // 在此处添加请求头等,如添加 token
    // if (store.state.token) {
    // config.headers['Authorization'] = `Bearer ${store.state.token}`
    // }
    return config
  },
  (error: any) => {
    Promise.reject(error)
  }
)

// Response interceptors
service.interceptors.response.use(
  async (response: AxiosResponse) => {
    // await new Promise(resovle => setTimeout(resovle, 3000))
    Toast.clear()
    const res = response.data
    if (res.code !== 0) {
      // token 过期
      if (res.code === 401) {
        // 警告提示窗
        return
      }
      if (res.code == 403) {
        Dialog.alert({
          title: '警告',
          message: res.msg
        }).then(() => {})
        return
      }
      // 若后台返回错误值,此处返回对应错误对象,下面 error 就会接收
      return Promise.reject(new Error(res.msg || 'Error'))
    } else {
      // 注意返回值
      return response.data
    }
  },
  (error: any) => {
    Toast.clear()
    if (error && error.response) {
      switch (error.response.status) {
        case 400:
          error.message = '请求错误(400)'
          break
        case 401:
          error.message = '未授权,请登录(401)'
          break
        case 403:
          error.message = '拒绝访问(403)'
          break
        case 404:
          error.message = `请求地址出错: ${error.response.config.url}`
          break
        case 405:
          error.message = '请求方法未允许(405)'
          break
        case 408:
          error.message = '请求超时(408)'
          break
        case 500:
          error.message = '服务器内部错误(500)'
          break
        case 501:
          error.message = '服务未实现(501)'
          break
        case 502:
          error.message = '网络错误(502)'
          break
        case 503:
          error.message = '服务不可用(503)'
          break
        case 504:
          error.message = '网络超时(504)'
          break
        case 505:
          error.message = 'HTTP版本不受支持(505)'
          break
        default:
          error.message = `连接错误: ${error.message}`
      }
    } else {
      if (error.message == 'Network Error') {
        error.message == '网络异常,请检查后重试!'
      }
      error.message = '连接到服务器失败,请联系管理员'
    }
    Toast(error.message)
    // store.auth.clearAuth()
    store.dispatch('clearAuth')
    return Promise.reject(error)
  }
)

export default service

接口管理

src/api 文件夹下统一管理接口

  • 你可以建立多个模块对接接口, 比如 home.ts 里是首页的接口这里讲解 authController.ts
  • url 接口地址,请求的时候会拼接上 config 下的 baseApi
  • method 请求方法
  • data 请求参数 qs.stringify(params) 是对数据系列化操作
  • loading 默认 false,设置为 true 后,显示 loading ui 交互中有些接口需要让用户感知
import request from '@/utils/request'
export interface IResponseType<P = {}> {
  code: number
  msg: string
  data: P
}
interface IUserInfo {
  id: string
  avator: string
}
interface IError {
  code: string
}
export const fetchUserInfo = () => {
  return request<IResponseType<IUserInfo>>({
    url: '/user/info',
    method: 'get',
    loading: true
  })
}

如何调用

由于awaitWrap类型推导很麻烦,所以还是采用 try catch 来捕获错误,既能捕获接口错误,也能捕获业务逻辑错误

onMounted(async () => {
  try {
    let res = await fetchUserInfo()
    console.log(res)
  } catch (error) {
    console.log(error)
  }
})

▲ 回顶部

✅ vue-request 管理接口

1. 安装依赖

pnpm i vue-request

2. 使用axios来获取数据,vue-request进行管理

// axios 
export const fetchUserInfo = () => {
  return request<IResponseType<IUserInfo>>({
    url: '/user/info',
    method: 'get',
    loading: true
  })
}
// vue-request
const { data: res, run } = useRequest(fetchUserInfo)
// 如果请求未完成,data为undefined。 使用 run 等待请求完成
await run()
console.log(res.value?.data)

3. 使用 vue-request 进行定时请求

// axios
export const getTimingData = () => {
  return request({
    url: '/getTimingData',
    method: 'GET'
  })
}

// vue-request
const { data: resultData, run } = useRequest(getTimingData, {
    pollingInterval: 5000,
    onSuccess: (data) => {
      console.log('onSuccess', data)
    }
  })

✅ unplugin-xxx 自动导入

unplugin-vue-components

  • 自动导入流行库组件和自定义组件
  1. 安装依赖
pnpm i -D unplugin-vue-components
  1. 修改 vite.config.ts
Components({
  // 指定组件位置,默认是src/components
  dirs: ['src/components'],
  // ui库解析器
  // resolvers: [ElementPlusResolver()],
  extensions: ['vue', 'tsx'],
  // 配置文件生成位置
  dts: 'src/components.d.ts',
  // 搜索子目录
  deep: true,
  // 允许子目录作为组件的命名空间前缀。
  directoryAsNamespace: false
  // include:[]
}),

unplugin-auto-import

  • 自动导入vue3相关api
  1. 安装依赖
pnpm i -D unplugin-auto-import
  1. 配置 vite.config.ts
AutoImport({
  include: [
    /\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
    /\.vue$/,
    /\.vue\?vue/, // .vue
    /\.md$/ // .md
  ],
  imports: ['vue', 'vue-router', '@vueuse/core'],
  // 可以选择auto-import.d.ts生成的位置,使用ts建议设置为'src/auto-import.d.ts'
  dts: 'src/auto-import.d.ts',
  // eslint globals Docs - https://eslint.org/docs/user-guide/configuring/language-options#specifying-globals
  // 生成全局声明文件,给eslint用
  eslintrc: {
    enabled: true, // Default `false`
    filepath: './.eslintrc-auto-import.json', // Default `./.eslintrc-auto-import.json`
    globalsPropValue: true // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
  }
})
  1. 配置 eslintrc
// .eslintrc.js
module.exports = { 
  /* ... */
  extends: [
    // ...
    './.eslintrc-auto-import.json',
  ],
}

vue-global-api

  • 在页面没有引入的情况下,使用unplugin-auto-import/vite来自动引入hooks,在项目中肯定会报错的,这时候需要在eslintrc.js中的extends引入vue-global-api,这个插件是vue3hooks的,其他自己找找,找不到的话可以手动配置一下globals
  1. 安装依赖
pnpm i -D vue-global-api
  1. 配置 eslintrc
// .eslintrc.js
module.exports = {
  extends: [
    'vue-global-api'
  ]
};

✅ VantUI 组件按需加载

1. 安装依赖

pnpm add vant@3
pnpm add vite-plugin-style-import -D

2. 按需引入配置

  • vite.config.ts
import vue from '@vitejs/plugin-vue'
import styleImport, { VantResolve } from 'vite-plugin-style-import'

export default {
  plugins: [
    vue(),
    styleImport({
      resolves: [VantResolve()]
    })
  ]
}
  • plugins/vant.ts
import { App as VM } from 'vue'
import { Button, Cell, CellGroup, Icon, Tabbar, TabbarItem, Image as VanImage } from 'vant'

const plugins = [Button, Icon, Cell, CellGroup, Tabbar, TabbarItem, VanImage]

export const vantPlugins = {
  install: function (vm: VM) {
    plugins.forEach((item) => {
      vm.component(item.name, item)
    })
  }
}
  • main.ts
// 全局引入按需引入UI库 vant
import { vantPlugins } from './plugins/vant'
app.use(vantPlugins)

3. 在 <script setup> 中可以直接使用 Vant 组件,不需要进行组件注册。

  • 如果使用这种方式,就不需要注册上面的 plugins/vant.ts
<script setup>
  import { Button } from 'vant';
</script>

<template>
  <Button />
</template>

4. 在 JSX 和 TSX 中可以直接使用 Vant 组件,不需要进行组件注册。

  • 如果使用这种方式,就不需要注册上面的 plugins/vant.ts
import { Button } from 'vant'

export default {
  render() {
    return <Button />
  }
}

▲ 回顶部

✅ viewport 适配方案

1. 安装依赖

pnpm i -D postcss-px-to-viewport autoprefixer

2. 添加 .postcssrc.js

module.exports = {
  plugins: {
    // 用来给不同的浏览器自动添加相应前缀,如-webkit-,-moz-等等
    autoprefixer: {
      overrideBrowserslist: ['Android 4.1', 'iOS 7.1', 'Chrome > 31', 'ff > 31', 'ie >= 8']
    },
    'postcss-px-to-viewport': {
      unitToConvert: 'px', // 要转化的单位
      viewportWidth: 375, // UI设计稿的宽度
      unitPrecision: 6, // 转换后的精度,即小数点位数
      propList: ['*'], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
      viewportUnit: 'vw', // 指定需要转换成的视窗单位,默认vw
      fontViewportUnit: 'vw', // 指定字体需要转换成的视窗单位,默认vw
      selectorBlackList: ['wrap'], // 指定不转换为视窗单位的类名,
      minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
      mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
      replace: true, // 是否转换后直接更换属性值
      exclude: [/node_modules/], // 设置忽略文件,用正则做目录名匹配
      landscape: false // 是否处理横屏情况
    }
  }
}

▲ 回顶部

✅ 适配苹果底部安全距离

<!--  head 标签中添加 meta 标签,并设置 viewport-fit=cover  -->
<meta
  name="viewport"
  content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover"
/>
<!-- 开启顶部安全区适配 -->
<van-nav-bar safe-area-inset-top />

<!-- 开启底部安全区适配 -->
<van-number-keyboard safe-area-inset-bottom />

如果不用 vant 中的适配,也可以自己写,我在 scss 中写了通用样式

.fixIphonex {
  padding-bottom: $safe-bottom !important;
  &::after {
    content: '';
    position: fixed;
    bottom: 0 !important;
    left: 0;
    height: calc(#{$safe-bottom} + 1px);
    width: 100%;
    background: #ffffff;
  }
}

▲ 回顶部

✅ 动态设置 title

// utils/index.ts
import { config } from '@/config'

/**
 * 动态设置浏览器标题
 * @param title
 */
export const setDocumentTitle = (title?: string) => {
  document.title = title || config.title
}

router/index.ts 使用

router.beforeEach((to, from, next) => {
  setDocumentTitle(to.meta.title as string)
  next()
})

▲ 回顶部

✅ 配置 Jssdk

  1. 安装:
yarn add weixin-js-sdk

类型声明写在了 model/weixin-js-sdk.d.ts

由于苹果浏览器只识别第一次进入的路由,所以需要先处理下配置使用的 url

  • router.ts 此处的jssdk配置仅供演示,正常业务逻辑需要配合后端去写
import { defineStore } from 'pinia'

export interface ILinkState {
	initLink: string
}

export const useAuthStore = defineStore({
	id: 'auth', // id 必须唯一
	state: () =>
		({
			initLink: ''
		} as ILinkState),
	actions: {
		setInitLink(data: any) {
			this.$state.initLink = data
		},
		setIsAuth(data) {
			this.$state.isAuth = data
		},
		setCode(code) {
			this.$state.code = code
		}
	},
	// 开启数据缓存
	persist: {
		key: 'auth',
		storage: window.localStorage,
		// paths: ['name'],
		overwrite: true
	}
}

由于window没有entryUrl变量,需要声明文件进行声明

// typings/index.d.ts
declare interface Window {
  entryUrl: any
}

创建 hooks 函数

hooks/useWxJsSdk.ts

每个页面使用jssdk,都需要调用一次useWxJsSdk,然后再使用其他封装的函数

调用:

▲ 回顶部

✅ Eslint + Prettier 统一开发规范

  • 初始化项目集成了 eslint + prettier,我们这里只做配置
  • .eslintrc.js
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')

module.exports = {
  root: true,
  extends: [
    'plugin:vue/vue3-essential',
    'eslint:recommended',
    '@vue/eslint-config-typescript/recommended',
    '@vue/eslint-config-prettier'
  ],
  env: {
    'vue/setup-compiler-macros': true
  },
  rules: {
    'prettier/prettier': 'warn',
    '@typescript-eslint/no-explicit-any': 'off',
    '@typescript-eslint/no-unused-vars': 'off',
    'vue/multi-word-component-names': 'off'
  }
}
  • .prettier.js
module.exports = {
  // 定制格式化要求
  overrides: [
    {
      files: '.prettierrc',
      options: {
        parser: 'json'
      }
    }
  ],
  printWidth: 100, // 一行最多 100 字符
  tabWidth: 2, // 使用 4 个空格缩进
  semi: false, // 行尾需要有分号
  singleQuote: true, // 使用单引号而不是双引号
  useTabs: false, // 用制表符而不是空格缩进行
  quoteProps: 'as-needed', // 仅在需要时在对象属性两边添加引号
  jsxSingleQuote: false, // 在 JSX 中使用单引号而不是双引号
  trailingComma: 'none', // 末尾不需要逗号
  bracketSpacing: true, // 大括号内的首尾需要空格
  bracketSameLine: false, // 将多行 HTML(HTML、JSX、Vue、Angular)元素反尖括号需要换行
  arrowParens: 'always', // 箭头函数,只有一个参数的时候,也需要括号 avoid
  rangeStart: 0, // 每个文件格式化的范围是开头-结束
  rangeEnd: Infinity, // 每个文件格式化的范围是文件的全部内容
  requirePragma: false, // 不需要写文件开头的 @prettier
  insertPragma: false, // 不需要自动在文件开头插入 @prettier
  proseWrap: 'preserve', // 使用默认的折行标准 always
  htmlWhitespaceSensitivity: 'css', // 根据显示样式决定 html 要不要折行
  vueIndentScriptAndStyle: false, //(默认值)对于 .vue 文件,不缩进 <script> 和 <style> 里的内容
  endOfLine: 'lf', // 换行符使用 lf 在Linux和macOS以及git存储库内部通用\n
  embeddedLanguageFormatting: 'auto' //(默认值)允许自动格式化内嵌的代码块
}

▲ 回顶部

✅ husky + lint-staged 提交校验

1. 安装依赖

pnpm i -D husky lint-staged

2. 添加脚本命令

npm set-script prepare "husky install"  // 在 package.json/scripts 中添加 "prepare": "husky install" 命令, 这个命令只在linux/uinx系统有效,win系统可以直接在scripts中添加命令
npm run prepare  //  初始化husky,将 git hooks 钩子交由,husky执行, 会在根目录创建 .husky 文件夹
npx husky add .husky/pre-commit "npx lint-staged" // pre-commit 执行 npx lint-staged 指令

3. 创建 .lintstagedrc.json

{
  "**/*.{js,ts,tsx,jsx,vue,scss,css}": [
    "prettier --write \"src/**/*.ts\" \"src/**/*.vue\"",
    "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix"
  ]
}

▲ 回顶部

✅ 项目打包优化

  • 项目打包优化主要是把vite.config.ts中的配置提取到了专门做打包配置的文件夹
  • build 文件夹目录
- build
- vite vite环境相关配置
- | - plugin 插件相关配置
- | - | - autocomponents 自动导入组件
- | - | - autoImport 自动导入 api
- | - | - compress 压缩打包
- | - | - mock mock 服务
- | - | - styleImport 样式自动导入
- | - | - index 插件配置入口
- | - build.ts 构建配置
- | - proxy.ts 代理配置
- utils 工具函数