vue3.2+vite+typescript+pinia+vant 移动端简易示例

vue3.2+vite+typescript+pinia+vant 移动端简易示例

前言:

伴随着vue3.2相关生态的完善,新的项目也得考虑直接上新版本、新语法糖了。公司经常有一些活动页面去实现,所以本文描述的是一个简易的vue3.2移动端示例。供新手参考

示例图

体验

🎸 Demo 访问   🌈 Github

依赖

  • 🚀 vue3.2 + vite + typescript + pinia + axios + vant
  • 💪 使用 vue3.2 <script setup> 新语法糖
  • 💪 使用 TypeScript
  • 🍭 支持 jsx 组件写法
  • 🍭 整合 vant ui、less
  • 🍭 使用 viewport 移动端方案
  • 🍭 使用 pinia 替代 vuex,更简单、更高效
  • 🍭 使用 网易云音乐 热门歌曲接口为数据源
  • 🍭 待更新

注:此处推荐一个免费的api网站,供平时mock使用 UomgAPI

vite脚手架快速搭建

注意:Vite 需要 Node.js 版本 >= 12.0.0

  1. 创建支持typescript的模板
# 安装vite
yarn create vite

# 使用模板创建
yarn create vite my-vue-app --template vue-ts
复制代码

目前vite支持快速生成的模板:

  • vanilla
  • vanilla-ts
  • vue
  • vue-ts
  • react
  • react-ts
  • preact
  • preact-ts
  • lit
  • lit-ts
  • svelte
  • svelte-ts

更多安装操作,可参考官网

  1. 安装后,目录如下

image.png

  1. 进入目录安装相关依赖
# 安装
yarn

# 启动
yarn dev
复制代码

约束代码规范

安装 Eslint、prettier

  1. 安装依赖
yarn add --dev eslint prettier eslint-config-prettier eslint-plugin-prettier eslint-plugin-vue @typescript-eslint/parser @typescript-eslint/eslint-plugin
复制代码
  1. 根目录下创建 .eslintrc.js 文件
//.eslintrc.js
module.exports = {
  root: true,
  env: {
    node: true
  },
  parser: 'vue-eslint-parser',
  parserOptions: {
    parser: '@typescript-eslint/parser', // Specifies the ESLint parser
    ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
    sourceType: 'module', // Allows for the use of imports
    ecmaFeatures: {
      jsx: true
    }
  },
  extends: [
    'plugin:vue/vue3-recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier',
    'plugin:prettier/recommended'
  ],
  rules: {
    '@typescript-eslint/no-var-requires': 0,
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    'space-before-function-paren': 0,
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
  }
}
复制代码
  1. 根目录下创建 .eslintignore 文件
# eslint 忽略检查 (根据项目需要自行添加)
node_modules
dist
复制代码
  1. 根目录下创建 .prettierrc.js 文件
module.exports = {
  printWidth: 100, //一行的字符数,如果超过会进行换行,默认为80
  tabWidth: 2, //一个tab代表几个空格数,默认为80
  useTabs: false, //是否使用tab进行缩进,默认为false,表示用空格进行缩减
  singleQuote: true, //字符串是否使用单引号,默认为false,使用双引号
  semi: false, //行位是否使用分号,默认为true
  trailingComma: 'none', //是否使用尾逗号,有三个可选值"<none|es5|all>"
  bracketSpacing: true, //对象大括号直接是否有空格,默认为true,效果:{ foo: bar }
  jsxSingleQuote: true, // jsx语法中使用单引号
  endOfLine: 'auto',
  'prettier.spaceBeforeFunctionParen': true
}
复制代码
  1. 根目录下创建 .prettierignore 文件
dist/
node_modules
*.log
run
logs/
coverage/
复制代码
  1. 使用vscode开发工具,保存时自动格式化

    1> 在扩展里面安装 Prettier - Code formatter 扩展插件

    image.png 2> 在首选项-设置-格式化 里面,配置保存格式化

    image.png 3> 此时,当你修改保存.vue.ts等文件的时候,会自动进行格式化,如果项目里已经 配置 .prettierrc.js文件,会优先使用该文件里面的规则。

CSS 预处理器安装

less

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

yarn add --dev less
复制代码

如果是用的是单文件组件,可以通过 <style lang="less">(或其他预处理器)自动开启。

配置全局样式

创建一个global.less文件,定义全局的主题色值等,这样在其它组件里面就可以随意引用了

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

export default defineConfig ({
  plugins: [
    vue()
  ],
  css: {
    preprocessorOptions: {
      less: {
        charset: false,
        additionalData: '@import "./src/style/global.less";' // 加载全局样式,使用less特性
      }
    }
  }
});
复制代码

移动端适配

viewport

之前移动端适配一直使用 lib-flexible+postcss-pxtorem 方案,随着viewport单位得到越来越多浏览器的支持,lib-flexible 官方也基本已经废弃,建议大家都使用viewport方案。

viewport + postcss-px-to-viewport

将px自动转换成viewport单位vw,vw本质上还是一种百分比单位,100vw即等于100%,即window.innerWidth

  1. 安装 postcss-px-to-viewport
yarn add --dev postcss-px-to-viewport
复制代码
  1. 在项目根目录下创建 postcss.config.js 文件
module.exports = {
  plugins: {
    'postcss-px-to-viewport': {
      unitToConvert: 'px', // 需要转换的单位,默认为"px"
      viewportWidth: 750, // 设计稿的视口宽度
      exclude: [/node_modules/], // 解决vant375,设计稿750问题。忽略某些文件夹下的文件或特定文件
      unitPrecision: 5, // 单位转换后保留的精度
      propList: ['*'], // 能转化为vw的属性列表
      viewportUnit: 'vw', // 希望使用的视口单位
      fontViewportUnit: 'vw', // 字体使用的视口单位
      selectorBlackList: [], // 需要忽略的CSS选择器,不会转为视口单位,使用原有的px等单位。
      minPixelValue: 1, // 设置最小的转换数值,如果为1的话,只有大于1的值会被转换
      mediaQuery: false, // 媒体查询里的单位是否需要转换单位
      replace: true, //  是否直接更换属性值,而不添加备用属性
      landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
      landscapeUnit: 'vw', // 横屏时使用的单位
      landscapeWidth: 1125 // 横屏时使用的视口宽度
    }
  }
}
复制代码
  1. 到此就配置好了,开发时根据设计稿的尺寸,配置viewportWidth,就可以一比一的进行px开发了,不用再计算了

引入 Vant ui

小项目中,为了避免手写过多的样式组件,一般都会使用第三方开源ui组件,本示例配置依赖Vant ui

  1. 安装依赖
# 安装vant
yarn add vant

# 安装插件,实现自动按需引入组件的样式
yarn add vite-plugin-style-import@1.4.1 -D
复制代码
  1. 配置插件
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue';
import styleImport, { VantResolve } from 'vite-plugin-style-import';

export default defineConfig ({
  plugins: [
    vue(),
    styleImport({
      resolves: [VantResolve()],
    }),
  ],
});

复制代码
  1. 兼容vant团队设计稿375px的设计稿 通常我们使用的设计稿是750px的,但是vant团队的是根据375px的设计稿去做的,理想视口宽度为375px。所以我们不能总是拿750px的设计稿都除以2吧,那样太麻烦了。所以干脆忽略第三方依赖下的单位,只转换本地的px为vw单位即可

. 修改 postcss.config.js 文件,增加 exclude: [/node_modules/]

module.exports = {
  plugins: {
    'postcss-px-to-viewport': {
      exclude: [/node_modules/], // 解决vant375,设计稿750问题。忽略某些文件夹下的文件或特定文件
    }
  }
}

复制代码
  1. vant@vant/use包对typescript的版本有限制,typescript4.5.5版本以后,不在支持VisibilityState状态,所以需要修改typescript版本的依赖

package.json

"devDependencies": {
   // 只使用4.5.x的最高版本
   "typescript": "~4.5.2",
}
复制代码

如果使用typescript高版本,则build时,会报以下错: node_modules/@vant/use/dist/usePageVisibility/index.d.ts:2:50 - error TS2304: Cannot find name 'VisibilityState'. 2 export declare function usePageVisibility(): Ref; Found 1 error in node_modules/@vant/use/dist/usePageVisibility/index.d.ts:2

  1. 下面就可以使用了哦

全局引用

import { createApp } from 'vue'
import App from './App.vue'
import { Button, Loading, Empty } from 'vant'

const app = createApp(App)
app.use(Button).use(Loading).use(Empty)
app.mount('#app')
复制代码

模板里可直接使用

<template>
  <van-loading></van-loading>
  <van-button round class="retry-button">按钮</van-button>
</template>
复制代码

路由 router

  1. 安装
yarn add vue-router@4
复制代码
  1. 根目录创建router目录,并创建index.ts文件

index.ts

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'home',
    component: () => import('@/views/Home.vue')
  }
]

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

export default router

复制代码
  1. 装载router main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'
import { Button, Loading, Empty } from 'vant'

const app = createApp(App)
app.use(router)
app.use(Button).use(Loading).use(Empty)
app.mount('#app')
复制代码

更多配置,请参考官网

axios

  1. 安装
yarn add axios
复制代码
  1. 新建utils目录,并创建request.ts文件

request.ts

import axios from 'axios'
import { CT } from '@/config/config'
import { Toast } from 'vant'

// axios.defaults.headers.common['Content-Type'] = 'application/json'
// axios.defaults.headers.common['area-code'] = 'CWHT'
// 创建 axios 实例
const request = axios.create({
  timeout: CT.timeout // 请求超时时间
})

// 异常拦截处理器
const errorHandler = (error: any) => {
  if (error.response) {
    const data = error.response.data
    Toast(data.message)
  }
  return Promise.reject(error)
}

// request interceptor
request.interceptors.request.use((config) => {
  // 自定义全局header
  config.headers = config.headers ? config.headers : {}
  config.headers['Content-Type'] = 'application/json'
  return config
}, errorHandler)

// response interceptor
request.interceptors.response.use((response) => {
  return response
}, errorHandler)

export default request

复制代码
  1. 新建api目录,并创建service.ts文件
import request from '@/utils/request'
import { URL } from '@/config/config'

// 获取随机音乐信息
export const fetchRandMusic = () => {
  return request.get(`${URL.musicUrl}/rand.music?sort=热歌榜&format=json`)
}
复制代码
  1. 使用api示例
import { reactive, toRefs } from 'vue'
import { fetchRandMusic } from '@/api/service'
import { HomeHooksModel } from '@/model/HomeModel'

// 首页hooks模块
export const HomeHooks = () => {
  // 响应值定义
  const indexRec = reactive<HomeHooksModel>({
    loading: true,
    noData: false,
    musicData: {}
  })

  // 查询随机音乐信息
  const fetchMusicInfo = async () => {
    const { data } = await fetchRandMusic()
    indexRec.loading = false
    indexRec.noData = data.code !== 1
    indexRec.musicData = data.data
  }
  return { ...toRefs(indexRec), fetchMusicInfo }
}

复制代码

pinia

vuex 使用相对麻烦、并且未来 vue 官方会放弃使用vuex,完全使用pinia

  1. 安装
yarn add pinia
复制代码
  1. 创建目录stores,并创建index.ts文件

index.ts

import { defineStore } from 'pinia'

export const refreshStore = defineStore('refresh', {
  state: () => {
    return { refreshNum: 0 }
  },
  getters: {
    queryRefresh(): number {
      return this.refreshNum
    }
  },
  actions: {
    upRefresh(st: number) {
      this.refreshNum = st
    }
  }
})
复制代码
  1. 调用pinia store,更新状态值
<script setup lang="ts">
import { ref } from 'vue'
import { refreshStore } from '@/stores/index'
// 接收状态值
const refresher = refreshStore()

const clickRefresh = () => {
  // 更新状态值
  refresher.$patch({ refreshNum: refresher.refreshNum + 1 })
}
</script>
复制代码

配置路径别名

  1. vite.config.ts 文件增加alias
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue';
import styleImport, { VantResolve } from 'vite-plugin-style-import';

export default defineConfig ({
  plugins: [
    vue(),
    styleImport({
      resolves: [VantResolve()],
    }),
  ],
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  }
});
复制代码
  1. tsconfig.json 文件增加如下:
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}
复制代码

配置支持 jsx

  1. 安装依赖
npm install @vitejs/plugin-vue-jsx -D
复制代码
  1. 配置插件
vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'

export default defineConfig({
  plugins: [vue(), vueJsx({})]
})
复制代码
  1. 解决导入 jsx 组件后,提示 隐式具有'any'类型 问题 在 env.d.ts 文件中,声明没有类型的库导入为 any
declare module '*.jsx';
复制代码

配置路由 transition

APP.vue

<template>
  <router-view v-slot="{ Component }">
    <transition name="fade">
      <component :is="Component" />
    </transition>
  </router-view>
</template>

<style lang="less">
.fade-enter-active,
.fade-leave-active {
  transition: opacity 1s ease;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>
复制代码

配置jest单元测试

  1. 安装依赖
# 基本依赖
yarn add jest vue3-jest babel-jest @vue/test-utils -D
# 支持typescript
yarn add @types/jest ts-jest  -D
复制代码
  1. 配置tsconfig.json文件
{
  "compilerOptions": {
    "types": ["vite/client", "jest"],
  },
  "include": ["src/**/*.ts", "tests/**/*.*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
复制代码
  1. 根目录新增jest.config.js文件 jest.config.js
module.exports = {
  preset: 'ts-jest',
  globals: {},
  testEnvironment: 'jsdom',
  transform: {
    '^.+\\.vue$': '@vue/vue3-jest',
    '^.+\\js$': 'babel-jest'
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  },
  moduleFileExtensions: ['vue', 'js', 'json', 'jsx', 'ts', 'tsx', 'node']
}

复制代码
  1. 新增单元测试文件 tests/units/MusicCard.spec.ts
import { shallowMount } from '@vue/test-utils'
import MusicCard from '@/components/MusicCard.vue'
const data = {
  name: '放空',
  url: 'http://music.163.com/song/media/outer/url?id=1841002409',
  picurl: 'http://p3.music.126.net/ocVnhvD-nXHKEM3TvBUZsw==/109951165931493179.jpg',
  artistsname: '大籽'
}
// 音频播放器组件描述作用域
describe('music play test', () => {
  // 断言 挂载组件,并传入props data
  it('renders data title', () => {
    const wrapper = shallowMount(MusicCard, {
      props: { data }
    })
    // 期望 title标题渲染成功
    expect(wrapper.get('.title').text()).toContain('放空')
  })

  test('renders data author', () => {
    const wrapper = shallowMount(MusicCard, {
      props: { data }
    })
    // 期望 author标题渲染成功
    expect(wrapper.get('.author').text()).toEqual('大籽')
  })

  test('render to click poster', () => {
    const wrapper = shallowMount(MusicCard, {
      props: { data }
    })
    wrapper.get('.player').trigger('click')
    // 期望 点击播放后,playing为true
    expect((wrapper.vm as any).playing).toBe(true)
  })
})
复制代码
  1. 配置单元测试启动脚本

package.json

"scripts": {
  + "test": "jest --config ./jest.conf.js --coverage"
}
复制代码
  1. 执行单元测试命令
yarn test
复制代码

结尾

ok, 到这里就先告一段落了

源码仓库地址:github.com/kingfront/f…

示例访问地址:freely.vercel.app

示例Demo采用免费的vercel进行静态部署(速度很快),想使用的小伙伴,加评论,后续新教程💪💪

分类:
前端
标签: