Vue3 Vite Vant TS (Pinia/Vuex) rem 移动端框架方案

6,335 阅读5分钟

描述

基于Vue3 + Vite + Vant + Sass + TS + rem适配方案 + Axios封装,构建手机端模板脚手架 

项目地址:github

项目预览:查看 demo 建议手机端查看,如pc端浏览器打开需切换到手机调试模式

Node 版本要求

Vite 需要 Node.js 12.0.0 或更高版本 (推荐 14.0.0+)。你可以使用 nvmnvm-windows 在同一台电脑中管理多个 Node 版本。

本示例 Node.js 14.18.1

启动项目

git clone https://github.com/talktao/Vue3-Vite-Vant-TS-H5.git

cd Vue3-Vite-Vant-TS-H5

yarn

yarn dev
复制代码

rem适配方案

Vant 中的样式默认使用px作为单位,如果需要使用rem单位,推荐使用以下两个工具:

更多详细信息: vant

VantUI组件按需加载

项目采 用Vant 自动按需引入组件 (推荐)下 面安装插件介绍:

安装插件

yarn add vite-plugin-style-import -D
复制代码

vite.config.ts 设置

import vue from '@vitejs/plugin-vue';
import styleImport, { VantResolve } from 'vite-plugin-style-import';

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

但是每次页面使用的时候还是要引入,很麻烦,项目在 src/plugins/vant.ts 下统一管理组件,无需在main.ts文件中多次use()

image.png

image.png

Sass 全局样式

首先 你可能会遇到 node-sass 安装不成功,别放弃多试几次!!!

每个页面自己对应的样式都写在自己的 .vue 文件之中 scoped 它顾名思义给 css 加了一个域的概念。

<style lang="scss">
    /* global styles */
</style>

<style lang="scss" scoped>
    /* local styles */
</style>
复制代码

目录结构

vue-h5-template 所有全局样式都在 @/src/assets/css 目录下设置

├── assets
│   ├── scss
│   │   ├── index.scss               # 全局通用样式
│   │   ├── mixin.scss               # 全局mixin
│   │   └── reset.scss               # 清除标签默认样式
│   │   └── variables.scss           # 全局变量
复制代码

父组件改变子组件样式 深度选择器

当你子组件使用了 scoped 但在父组件又想修改子组件的样式可以 通过 >>> 来实现:

<style scoped>
.a >>> .b { /* ... */ }
</style>
复制代码

全局变量

// 引入全局样式
import '@/assets/css/index.scss'

Vuex 状态管理

目录结构

├── store
│   ├── index.ts
复制代码

main.ts 引入

image.png

使用

image.png

Pinia 状态管理

1.安装

node版本需>=14.0.0

yarn add pinia 
# or with npm 
npm install pinia

2. 创建Pinia的Store

src/store/index.ts 文件中,导出 piniaStore

// src/store/index.ts

import { createPinia } from 'pinia'

export const piniaStore = createPinia()

3.在main.ts文件中引用

image.png

3. 定义State

src/store目录下新建有个testPinia.ts文件

i. 传统的options API方式

import { defineStore } from "pinia"
export const usePiniaState = defineStore({
    id: 'textPinia',
    state: () => {
        return {
            userName: ''
        }
    },
    getters: {

    },
    actions: {
        getUserNmae(data) {
            this.userName = data
        }
    }
})

ii.Vue3 setup的编程模式

import { ref } from 'vue'
import { defineStore } from "pinia"
export const usePiniaState = defineStore('pinia', ()=>{
    const userName = ref('')
    // 修改userName的方法
    const getUserNmae = (data) => {
        userName.value = data
    }
    return { userName, getUserNmae}
})

4.获取/修改 state

<script setup lang="ts">
	import { storeToRefs  } from 'pinia'
	import { usePiniaState } from '@/store/testPinia'
        
	// pinia
	const piniaStore = usePiniaState()
        
	// 通过storeToRefs方法将存储在pinia里的数据解构出来,保持state响应性
	const { userName } = storeToRefs(piniaStore)
	const { getUserNmae } = piniaStore
	

        const handleBtn = () =>{
            // pinia
            getUserNmae('真乖,如果对您有帮助请在github上点个星星哦~')
        }

</script>

Vue-router

本案例采用 hash 模式,开发者根据需求修改 mode base

自动化导入路由

import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";

// 通过Vite的import.meta.glob()方法实现自动化导入路由
const mainRouterModules = import.meta.glob('../layout/*.vue')
const viewRouterModules = import.meta.glob('../views/**/*.vue')

// 子路由
const childRoutes = Object.keys(viewRouterModules).map((path)=>{	
	const childName = path.match(/\.\.\/views\/(.*)\.vue$/)[1].split('/')[1];
	return {
		path: `/${childName.toLowerCase()}`,
		name: childName,
		component: viewRouterModules[path]
	} 
})

console.log(childRoutes,'childRouter');

// 根路由
const rootRoutes = Object.keys(mainRouterModules).map((path) => {
    const name = path.match(/\.\.\/layout\/(.*)\.vue$/)[1].toLowerCase();
    const routePath = `/${name}`;
    if (routePath === '/index') {
		return {
			path: '/',
			name,
			redirect: '/home',
			component: mainRouterModules[path],
			children: childRoutes
		};
    }
})

const routes: Array<RouteRecordRaw> = rootRoutes

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

export default router

普通设置

import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";

const routes: Array<RouteRecordRaw> = [
   {
        path: '/',
        name: 'Index',
        component: () => import ('@/layout/index.vue'),
        redirect: '/home',
        meta: {
            title: '首页',
            keepAlive:false
        },
        children: [
            {
                path: '/home',
                name: 'Home',
                component: () => import('@/views/home/Home.vue')
            },
            {
                path: '/about',
                name: 'About',
                component: () => import('@/views/about/About.vue')
            },
        ]
    },    
]


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

export default router

更多:Vue Router

Axios 封装及接口管理

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

image.png

接口管理

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

image.png

image.png

通过引入axios库的ts版本即可配置

import axiosInstance, { AxiosResponseProps } from '@/uitls/request'

export const getList = (params: any) => {
	return axiosInstance.get("/common/code/logisticsInfo/getOrderByPhone", { params: params || {} });
}

控制全局请求loading

// 定义存放请求接口数组
let requestList = [];
const setLoadingToFalse = response => {
    requestList
        .filter(item => item.url == response.config.url && item.method == response.config.method)
        .forEach(item => (item.isLoading = false));

    //所有请求都加载完才让加载提示消失
    if (requestList.every(item => !item.isLoading)) vuexStore.commit("changeIsLoading", false);
};

上述setLoadingToFalse函数主要接收响应报文参数,从而判断是否所有请求都加载完成,加载完成则取消全局loading

instance.interceptors.request.use(
    (config: any) => {
        // 不用判断请求loading的路由
        let ignoreLoadingUrls = ["/login"];
        
        if (!ignoreLoadingUrls.includes(config.url)) {

            if (requestList.length < 10) {
                requestList.unshift({ ...config, isLoading: true });
            } else {
                requestList = [...requestList.slice(0, 9), { ...config, isLoading: true }];
            }
            vuexStore.commit("changeIsLoading", true);
        }
        return config;
    },
    error => Promise.reject(error + '请求错误')
    );

上述请求拦截主要用于给当前请求配置isLoading:true,以便于给setLoadingToFalse函数最终来判断请求是否结束

instance.interceptors.response.use(
    response => {
        setLoadingToFalse(response);
        return response.data;
    },
    error => {
        if (error.response.status == 301) {}
        setLoadingToFalse(error);
        return Promise.reject(error.response?.data);
    }
);

上述响应拦截主要用于将当前的响应报文 参数传递给setLoadingToFalse,并返回响应结果

效果

WeChata09ea43a80f9d9acb0cbaede0277ba63.png

如何调用

// 请求接口
import { getUserInfo } from '@/api/home'

const params = {user: 'talktao'}
getUserInfo(params)
  .then(() => {})
  .catch(() => {})
复制代码

vite.config.ts 基础配置

image.png

检查文件中的env路径

image.png

配置 alias 别名

resolve: {
    alias:{
    // 配置src目录
    "@": path.resolve(__dirname,"src"),
    // 导入其他目录
    "components": path.resolve(__dirname, "components")
    }
},

配置 proxy 跨域

如果你的项目需要跨域设置,你需要打开 vite.config.ts proxy 注释 并且配置相应参数

module.exports = {
  // 跨域代理
    server: {
        host: '0.0.0.0',
        proxy: {
            '/api': {
                target: 'https://api.inews.qq.com',
                changeOrigin: true,
                rewrite: (path) => path.replace(/^\/api/, '') // 将匹配到的api替换成''
            }
        }
    }
}

Eslint+Pettier 统一开发规范

VScode安装 eslint prettier vetur 插件 .vue 文件使用 vetur 进行格式化,其他使用prettier

批量全局注册公共组件

文件地址在 src/plugins/components

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

export default {
  install(app) {
    Object.keys(modules).forEach(componentPath => {

    let splitPart1 = componentPath.split("/")
    let componentName = splitPart1[splitPart1.length - 1].split(".vue")[0]

    // 获取所有组件的实例对象,它是个数组
    let modulesData = Object.values(modules).map((v) => v.default)

    // 过滤出当前组件的实例对象与注册组件匹配一致
    let curComponent = modulesData.filter(
        item=>item.__file.split("/")[item.__file.split("/").length-1].split(".vue")[0] === componentName
    )[0]          

    app.component(componentName, curComponent);
    })
  }
}

上面的批量全局注册公共组件在本地启动中正常,但是上生产打包后,会有问题,具体是__file该组件路径找不到,可以修改成如下代码:

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

export default {
  install(app) {
      Object.keys(modules).forEach(componentPath => {

          // 获取遍历的当前组件实例对象
          let curComponent = modules[componentPath]?.default 

          app.component(curComponent.name, curComponent);
      })
  }
 
}

注意:

由于sfc语法糖没有携带组件的name属性,上面的curComponent.name会报curComponent下没有name属性,此时需要在注册的公共组件中加上如下代码,比如在src/components/CustomHeader.vue中加上如下代码,这样组件的实例对象中就会有name属性

image.png

总结

项目github地址

关于我

如果对你有帮助送我一颗小星星❤

转载请联系作者!