VUE3 移动端快速整活儿模板项目搭建!

1,770 阅读8分钟

前言

vue3更新已经有一阵了,近期准备在新项目中投入使用,遂搭建了一套模板项目,主要用于移动端项目快速出活儿,集成了一些常用的第三库,东西不复杂,不过还是有一些小坑,记录一下,如果能帮助到一些初学者,那真是善莫大焉!

值得说一下的是,我个人觉得vue3是很有必要深入了解学习的,如果你有react的项目基础,会觉得快乐又回来了,vue3重写了虚拟dom,加入了优化体验跟效率的一些新组件,新的脚手架工具vite,对ts更好的支持,重写的数据响应,组合式api...都是些有意思的更新,如果之前没接触过react的hook思想跟写法的同学,可能前期的门槛不是在技术的学法跟实现上,而是先理解并接受组合式api的写法,避免用vue3去写vue2的代码,这会让事情变得很糟糕(咋有点音译文档的赶脚了,VUE3官网地址)!

好的,请未成年程序员在家长陪同下观看,让我们开始 >>>>>>

1. 通过vite创建基础vue3项目

官网地址(vitejs.cn/)

vite的一些优势跟基本使用方式,基本都在官方文档里面了,安装vite,使用init直接拉取即可

cnpm init vite@latest my-vue-app --template vue

在安装选择选择vue与js,ts的后续会更新,我们可以看到vite对react也是支持的,后续尝试之后再更新使用体验。

Select a framework: » vueSelect a variant: » vue

完成之后项目结构是这样,东西还是那些东西,多了一个vite.config.js没咋见过,这个看官方文档即可,这里不做赘述。

|-- my-vue-app
    |-- .gitignore
    |-- index.html
    |-- package.json
    |-- README.md
    |-- vite.config.js
    |-- .vscode
    |   |-- extensions.json
    |-- public
    |   |-- favicon.ico
    |-- src
        |-- App.vue
        |-- main.js
        |-- assets
        |   |-- logo.png
        |-- components
            |-- HelloWorld.vue

安装包之后就能跑起来了,我这里用的cnpm,地址:npmmirror.com/

cnpm i
npm run dev
 vite v2.7.6 dev server running at:

  > Local: http://localhost:3000/
  > Network: use `--host` to expose

  ready in 2203ms.

image.png

打开HelloWorld.vue组件你会发现,诶,以前浓眉大眼的vue2写法里面的很多配置没了,不要着急,这是曲线救国,不是叛变革命,<template>的模板绑定上没啥大的改动,主要是在js模块,setup的语法糖可能直接让刚接触的同学如果有点懵逼,你需要先去看看文档VUE3官网地址,这里不做赘述。

<script setup>
import { ref } from 'vue'
defineProps({
  msg: String
})
const count = ref(0)
</script>

2. vue-router的路由引入

基础项目出来了,开始路由的集成,这里用的是最新版本,官网地址:next.router.vuejs.org/

老规矩先安装:

npm install vue-router@4

创建文件 src/router.js

与3.x的new不同,新版采用createRouter的函数创建,path的匹配规则也改为了正则的方式,其他的差别不大,代码如下,需要注意的是,在vite中导入导出模块用import的方式,用require的方式会比较麻烦,这里使用了nprogress做路由守卫时候的加载显示效果

import { createRouter, createWebHistory } from 'vue-router';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';

const Home = () => import ('./views/home');
const Notfund = () => import ('./views/404.vue');

const router =  createRouter({
    history: createWebHistory(),
    routes: [{
            path: '',
            redirect: "/home",
        },
        {
            path: '/home',
            name: 'home',
            component: Home,
            meta: {
                title: '首页',
                titleEn: 'Home',
                keepAlive: true
            }
        },
        //...自己的路由对象
        {
            path: '/:pathMatch(.*)',
            name: 'Notfund',
            component: Notfund,
            meta: {
                title: '404',
                titleEn: '404'
            }
        }
    ]
})


router.beforeEach((to, from, next) => {
    NProgress.start();
    let { requiresAuth, title } = to.meta;
    document.title = title || '--';
    if (requiresAuth) {
        console.log('需要登录.....');
        // next('/login');
    }
    next();
})

router.afterEach((to, from, failure) => {
    NProgress.done();
    window.scrollTo(0, 0);
})


export default router;

完成路由封装后在src/main.js中导入

import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

import './styles/reset.scss';//scss的使用不赘述,文档 https://www.sass.hk/guide/

const app = createApp(App);
app.use(router)
    .mount('#app');

与老版本的区别在于其在 composition api中的使用,其实文档里面都有,这里举个栗子,注意一下这里面有个<script setup>的语法糖,要是整不醒火,就去看vue3的文档。


<script setup>
    import { ref } from 'vue';
    import { useRouter } from 'vue-router';
    const { back } = useRouter();
    const msg = ref('404');
</script>

<template>
  <h1>{{msg}}</h1>
  <div>
    <router-link to="/"><van-button type="primary">返回首页</van-button></router-link>
    &nbsp;&nbsp;&nbsp;
    <van-button type="warning" @click="back()">返回上级</van-button>
  </div>
  
</template>

3. i18n的国际化引入

国际化仍然采用最新版,官网地址:vue-i18n.intlify.dev/ ,老规矩先安装

cnpm install vue-i18n@next

在src下创建语言文件

|-- language
    |-- en.js
    |-- index.js
    |-- zh.js

en.js文件内容

const LANG = {
    HOME: {
        TITLE: 'More'
    }
}
export default LANG;

zh.js文件内容

const LANG = {
    HOME: {
        TITLE: '更多'
    }
}
export default LANG;

index.js文件内容

import { createI18n } from 'vue-i18n';
import zh from './zh';
import en from './en';

const i18n = createI18n({
    legacy: false,
    globalInjection: true,
    locale: 'zh-CN',
    messages: {
        'zh-CN': zh,
        'en-US': en
    }
});

export default i18n;

完成后老样子,导入main.js即可

import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import i18n from './language';

import './styles/reset.scss';//scss的使用不赘述,文档 https://www.sass.hk/guide/

const app = createApp(App);
app.use(router)
    .use(i18n)
    .mount('#app');

在模板文件中使用方式

<h3>多语言测试=>{{$t('HOME.TITLE')}}</h3>

如果你需要在setup()中用函数方式调用如下:


<script>
import { defineComponent} from 'vue';
import { useI18n } from 'vue-i18n';

export default defineComponent({
  
  setup(){
    const { locale, t } = useI18n();
    
    const changeLange = ()=>{//切换语言
      let currLang = locale.value==='en-US';
      locale.value = currLang ? "zh-CN" :'en-US';
    }//changeLange
    
    const getLang = ()=>{ //打印语言
        console.log(t('HOME.TITLE'));
    }
    
    return {
    }

  },//setup

})


</script>

如果想在单独的js模块中使用,这里需要再global的属性上调用:

import language from '@/language';
const currLang = ()=>{
    console.log(language.globa.t('HOME.TITLE'));//当前语言字符
    language.global.locale.value;//当前语言类型
};

4. vuex的引入

老规矩安装最新版,官方文档 next.vuex.vuejs.org/,

npm install vuex@next --save

src 下创建模块文件

        |-- store
        |   |-- actions.js
        |   |-- getters.js
        |   |-- index.js
        |   |-- mutations.js
        |   |-- state.js

vuex 4.x的版本相较于之前的老版本,没有太大的区别,更多的是适配composition api的调整, createPersistedState是vuex本地持久化插件,根据个人情况选择安装

//state.js

const state = {
    userInfo: 'default data'
}
export default state;
//mutations.js

export const setUserInfo = (state, payload) => { //设置详细用户信息
    state.userInfo = payload;
}

export const setTestVal = (state, payload) => {
    state.testVal = payload;
}
//actions.js

export const actionSetUserInfo = ({ commit }, payload) => {//获取api的用户信息
    commit('setUserInfo', payload);
}//actionSetUserInfo
//getters.js
export const gettersUserInfo = (state) => { 
    return `filter=>${state.userInfo}`;
}

完成上述的各个模块的配置,我们将它组装到一起,然后导入到main.js就可以愉快的使用了

//index.js

import {createStore} from 'vuex'
import * as getters from './getters' // 导入响应的模块,*相当于引入了这个组件下所有导出的事例
import * as actions from './actions'
import * as mutations from './mutations'
import state from './state'
i// mport createPersistedState from 'vuex-persistedstate'

// 注册上面引入的各大模块
const store = createStore({
    // plugins: [createPersistedState()],
    state, // 共同维护的一个状态,state里面可以是很多个全局状态
    getters, // 获取数据并渲染
    actions, // 数据的异步操作
    mutations // 处理数据的唯一途径,state的改变或赋值只能在这里
})

export default store // 导出store并在 main.js中引用注册。

此时的main.js如下:

import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import i18n from './language';
import store from './store';

import './styles/reset.scss';//scss的使用不赘述,文档 https://www.sass.hk/guide/

const app = createApp(App);
app.use(router)
    .use(store)
    .use(i18n)
    .mount('#app');

在组件文件中的使用,你熟悉的option api的语法糖方式还在,不过咱都既然折腾vue3了,还是最好忘掉过去...

让洒家带你看看在setup() 中的 composition api的使用,来康康下面两种使用方式的区别



<template>
  <div class="box">
    <h1>vuex 组合api方式</h1>
    <div>{{title}} / {{userInfo}} / {{gettersUserInfo}}</div>
    <div>
      <van-button type="danger" @click="mutations()">mutations</van-button>&nbsp;
      <van-button type="primary" @click="actions()">actions</van-button>
    </div>
  </div>

  <div class="box">
    <h1>vuex 语法糖方式</h1>
    <div>{{testVal}}</div>
    <div>
      <van-button type="danger" @click="testCommit">testCommit</van-button>&nbsp;
    </div>
  </div>
</template>


<script>

import { defineComponent, ref, computed } from "vue";
import { useStore, mapState, mapMutations } from 'vuex';
export default defineComponent({

  setup(props, context) {
      const store = useStore();

      const title = ref('vuex 测试渲染=>');

      const mutations = ()=>{
          console.log('mutations');
          store.commit('setUserInfo','commit userInfo');
      }//mutations

      const actions = ()=>{
        console.log('actions');
        store.dispatch('actionSetUserInfo','action userInfo')
      }//actions

      return {
          title,
          mutations,
          actions,
          
          //这里不要想着去{ state }这样解构,数据会在渲染上失去响应式,注意这里要用`computed`属性计算来返回
          userInfo: computed(() => store.state.userInfo),
          gettersUserInfo: computed(() => store.getters.gettersUserInfo),
      }
  },//setup
  computed: mapState(['testVal']),
  methods:{
    ...mapMutations(['setTestVal']),//熟悉的味道还在,不过人要往前看
    testCommit(){
      this.setTestVal('commit test val')
    }//testCommit
  }
});
</script>

如果在外部js调用,还是跟之前一样

import store from '@/store';
// store.state.token;

至此,vuex的基本使用基本就满足了,当然这里还没涉及到更大型的项目中state的模块拆分,后面有时间再补吧。

5. vant的引入

NOW,到了我们最喜闻乐见的vant的集成了,老规矩自己先看看最新版的文档 youzan.github.io/vant/v3/ ,大致过了一下,调用方式基本没啥变化,只是demo的调用方式更新为了vue3的方式,其他的对老用户来说没啥理解成本,我这里把css全部引入,组件按需引入。

import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import { Button, Tabbar, TabbarItem } from 'vant';
import i18n from './language';
import store from './store';

import 'vant/lib/index.css';
import './styles/reset.scss';


const app = createApp(App);
app.use(router)
    .use(store)
    .use(i18n)
    .use(Button).use(Tabbar).use(TabbarItem)
    .mount('#app');

试试组件中是否正常使用就完事儿

<van-button type="warning">来啊,造作啊</van-button>

6. 基于axios的requset封装

此时我们已经可以单机操作,但是想要与服务器api深入交流,大力推进,打开格局,共同开创和谐网络大环境,我们就需要封装一套网络请求的方法,这里采用axios来做二次封装,翠花儿,上酸菜

老规矩,官方问文档:www.axios-js.com/zh-cn/docs/ ,看完安装一二

npm install axios

src下建模块文件,以后不管走到何方你都始终记得,fetch是vue项目不可分割的一部分,它自古以来就是咱项目的一份子,愿君听之,信之(狗头报名)...

    |-- fetch
    |   |-- index.js
    |   |-- requset.js

这里我在src目录下,新建了一个config.js来记录一些常见配置项

//config.js

const development = import.meta.env.DEV;//环境变量判断
import language from '@/language';

// api代理配置
export const ApiProxyBasePath = '/serverApi';

//不显示加载框的路由
export const noGifUrl = [];

//不显示加载框的路由
export const requestJsonUrl = [];

//返回当前语言
export const currLang = ()=>{
    return language.global.locale.value;
};

//requset.js

import axios from 'axios'; 
import router from '@/router';
import store from '@/store';
import { Toast } from 'vant';
import { requestJsonUrl, noGifUrl, currLang } from '../config';

// 请求时拦截配置===================

axios.interceptors.request.use( config => {

    if (noGifUrl.indexOf(config.url) < 0) { //不显示加载动画
        Toast.loading({
            message:'Loading...',
            forbidClick: true,
        });
    }

    let contentType = 'application/x-www-form-urlencoded';
    
    if (requestJsonUrl.indexOf(config.url) > -1) {
        contentType = 'application/json';
    }

    config.headers = {
        'X-Secret-Token': store.state.token,//一般鉴权需要
        'content-type': contentType,
        //'Accept-Language': currLang()//如果需要后端识别多语言
    }

    return config;
}, error=> {
    Toast.clear();
    return Promise.reject(error);
});

// 数据返回后的拦截配置===================

axios.interceptors.response.use(response => {

    if (noGifUrl.indexOf(response.config.url) < 0) { //不显示加载动画
        Toast.clear();
    }

    let resData = response.data;

    if (resData.code === 200) {
        return Promise.resolve(resData);
    } else if (resData.code === 401) { //重新登录
        console.log('当前路由',router.currentRoute);
        router.replace({ path: `/login?toPath=${router.currentRoute.fullPath}` });
        return Promise.reject(resData);
    } else {
        setTimeout(() => {
            if (noGifUrl.indexOf(response.config.url) < 0) { //不显示加载动画
                Toast.fail( resData.message || 'Request error' );
            }
        }, 500);
        return Promise.reject(resData);
    }

}, error => {
    console.error(error);
    return Promise.reject(error);
});

const headers = {'content-type': 'application/x-www-form-urlencoded'};

export const get = ({url,params}, that) => {
        return new Promise(function(resolve, reject) {
            axios({
                    method: 'GET',
                    url,
                    params,
                    headers
                })
                .then(res => {
                    resolve(res);
                    Toast.clear();
                })
                .catch(err => {
                    reject(err);
                    Toast.clear();
                })
        })
    } //get

export const post = ({url, params}) => {
        return new Promise(function(resolve, reject) {
            axios({
                    method: 'POST',
                    url,
                    data: params,
                    headers
                })
                .then(res => {
                    resolve(res);
                })
                .catch(err => {
                    reject(err);
                    Toast.clear();
                })
        })
    } //get

export default {
    get,
    post
}

完成封装后,在index.js做请求调用的返回

//index.js

import Requset  from './requset';
import { ApiProxyBasePath }  from '../config';
import qs  from 'qs';

export function getTest(params) { //获取列表测试
    return Requset.get({
        url: `${ApiProxyBasePath}/api/v1/xxx`,
        params
    });
} //getTest

export function postTest(data) { //post示例
    return Requset.post({
        url: `${ApiProxyBasePath}/api/xxxx`,
        params: qs.stringify(data)
    });
} //postTest

此时组件需要调用接口干活儿了

//组件文件.vue

<template>
  <pre>{{apiData}}</pre>
  <hr/>
  <van-button type="success" @click="callApi">请求接口,没病走两步!</van-button
</template>

<script>
import { defineComponent, ref, reactive, toRefs } from "vue";
import * as Api from '@/fetch';

export default defineComponent({
  setup() {
      const state = reactive({
        apiData:null
      })

      const callApi = async (val)=>{
        state.apiData = [];
        let { data } = await Api.getTest();
        state.apiData = data;
      }//acceptValue

      return {
          callApi,
          ...toRefs(state)
      }
  }, //setup
});
</script>

7.vite.config 的常用配置

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
const { resolve } = require('path');
const isProduction = process.env.NODE_ENV === 'production';
// https://vitejs.dev/config/
export default defineConfig({
    plugins: [vue()],
    base: isProduction ? './' : '/',//打包配置
    resolve: {
        extensions: ['.js', '.vue', '.json'],//后缀名识别,这样引入文件的时候不用写文件名
        alias: {
            '@': resolve(__dirname, 'src'),//别名设置
        },
    },
    server: {
        port: 4000,
        proxy: {
            '/serverApi': { //代理api
                target: 'https://myApiHost/api/v1/xxx',
                changeOrigin: true,
                ws: true,
                rewrite: (path) => path.replace(/^\/serverApi/, '')
            },
        }
    },
    css: {
        preprocessorOptions: {
            scss: {
                charset: false //css的处理,不加上打包可能会失败
            }
        }
    }
})

8.项目已出舱,感觉良好!

|-- my-vue-app
    |-- .gitignore
    |-- index.html
    |-- package-lock.json
    |-- package.json
    |-- README.md
    |-- vite.config.js
    |-- .vscode
    |   |-- extensions.json
    |-- public
    |   |-- favicon.ico
    |-- src
        |-- App.vue
        |-- main.js
        |-- router.js
        |-- assets
        |   |-- logo.png
        |   |-- font
        |       |-- Alibaba-PuHuiTi-Medium.otf
        |       |-- Alibaba-PuHuiTi-Regular.otf
        |-- components
        |   |-- BaseFooter.vue
        |-- config
        |   |-- index.js
        |-- fetch
        |   |-- index.js
        |   |-- requset.js
        |-- language
        |   |-- en.js
        |   |-- index.js
        |   |-- zh.js
        |-- store
        |   |-- actions.js
        |   |-- getters.js
        |   |-- index.js
        |   |-- mutations.js
        |   |-- state.js
        |-- styles
        |   |-- index.scss
        |-- util
        |   |-- index.js
        |-- views
            |-- 404.vue
            |-- about
            |   |-- index.vue
            |-- home
            |   |-- index.vue
            |-- login
                |-- index.vue

此时你就拥有了如下目录的完整项目,基本常见一些移动端应该是能出活儿了。

第一次写这么长的文章,也算是自己的一个记录,如果同时能对初学的小伙伴有一点帮助,我会很欣慰,贴的代码比较多,看起来肯定比较累,我自己也还在探索阶段,如果有些东西理解不对,希望能有好心人温柔的指出,共同探讨,共同进步,以促进和谐社会的精神文明建设!