前言:
伴随着vue3.2相关生态的完善,新的项目也得考虑直接上新版本、新语法糖了。公司经常有一些活动页面去实现,所以本文描述的是一个简易的vue3.2移动端示例。供新手参考
示例图
体验
依赖
- 🚀 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
- 创建支持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
更多安装操作,可参考官网
- 安装后,目录如下
- 进入目录安装相关依赖
# 安装
yarn
# 启动
yarn dev
复制代码
约束代码规范
安装 Eslint、prettier
- 安装依赖
yarn add --dev eslint prettier eslint-config-prettier eslint-plugin-prettier eslint-plugin-vue @typescript-eslint/parser @typescript-eslint/eslint-plugin
复制代码
- 根目录下创建 .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'
}
}
复制代码
- 根目录下创建 .eslintignore 文件
# eslint 忽略检查 (根据项目需要自行添加)
node_modules
dist
复制代码
- 根目录下创建 .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
}
复制代码
- 根目录下创建 .prettierignore 文件
dist/
node_modules
*.log
run
logs/
coverage/
复制代码
-
使用vscode开发工具,保存时自动格式化
1> 在扩展里面安装 Prettier - Code formatter 扩展插件
2> 在首选项-设置-格式化 里面,配置保存格式化
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
- 安装 postcss-px-to-viewport
yarn add --dev postcss-px-to-viewport
复制代码
- 在项目根目录下创建 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 // 横屏时使用的视口宽度
}
}
}
复制代码
- 到此就配置好了,开发时根据设计稿的尺寸,配置viewportWidth,就可以一比一的进行px开发了,不用再计算了
引入 Vant ui
小项目中,为了避免手写过多的样式组件,一般都会使用第三方开源ui组件,本示例配置依赖Vant ui
- 安装依赖
# 安装vant
yarn add vant
# 安装插件,实现自动按需引入组件的样式
yarn add vite-plugin-style-import@1.4.1 -D
复制代码
- 配置插件
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()],
}),
],
});
复制代码
- 兼容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问题。忽略某些文件夹下的文件或特定文件
}
}
}
复制代码
- 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
- 下面就可以使用了哦
全局引用
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
- 安装
yarn add vue-router@4
复制代码
- 根目录创建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
复制代码
- 装载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
- 安装
yarn add axios
复制代码
- 新建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
复制代码
- 新建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`)
}
复制代码
- 使用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
- 安装
yarn add pinia
复制代码
- 创建目录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
}
}
})
复制代码
- 调用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>
复制代码
配置路径别名
- 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')
}
}
});
复制代码
- tsconfig.json 文件增加如下:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
复制代码
配置支持 jsx
- 安装依赖
npm install @vitejs/plugin-vue-jsx -D
复制代码
- 配置插件
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({})]
})
复制代码
- 解决导入 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单元测试
- 安装依赖
# 基本依赖
yarn add jest vue3-jest babel-jest @vue/test-utils -D
# 支持typescript
yarn add @types/jest ts-jest -D
复制代码
- 配置tsconfig.json文件
{
"compilerOptions": {
"types": ["vite/client", "jest"],
},
"include": ["src/**/*.ts", "tests/**/*.*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
复制代码
- 根目录新增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']
}
复制代码
- 新增单元测试文件 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)
})
})
复制代码
- 配置单元测试启动脚本
package.json
"scripts": {
+ "test": "jest --config ./jest.conf.js --coverage"
}
复制代码
- 执行单元测试命令
yarn test
复制代码
结尾
ok, 到这里就先告一段落了
源码仓库地址:github.com/kingfront/f…
示例访问地址:freely.vercel.app
示例Demo采用免费的
vercel
进行静态部署(速度很快),想使用的小伙伴,加评论,后续新教程💪💪