前言
vue3更新已经有一阵了,近期准备在新项目中投入使用,遂搭建了一套模板项目,主要用于移动端项目快速出活儿,集成了一些常用的第三库,东西不复杂,不过还是有一些小坑,记录一下,如果能帮助到一些初学者,那真是善莫大焉!
值得说一下的是,我个人觉得vue3是很有必要深入了解学习的,如果你有react的项目基础,会觉得快乐又回来了,vue3重写了虚拟dom,加入了优化体验跟效率的一些新组件,新的脚手架工具vite,对ts更好的支持,重写的数据响应,组合式api…都是些有意思的更新,如果之前没接触过react的hook思想跟写法的同学,可能前期的门槛不是在技术的学法跟实现上,而是先理解并接受组合式api的写法,避免用vue3去写vue2的代码,这会让事情变得很糟糕(咋有点音译文档的赶脚了,VUE3官网地址)!
好的,请未成年程序员在家长陪同下观看,让我们开始 >>>>>>
1. 通过vite创建基础vue3项目
官网地址(https://vitejs.cn/)
vite的一些优势跟基本使用方式,基本都在官方文档里面了,安装vite,使用init直接拉取即可
1
| npm init vite@latest my-vue-app --template vue
|
在安装选择选择vue与js,ts的后续会更新,我们可以看到vite对react也是支持的,后续尝试之后再更新使用体验。
1 2
| √ Select a framework: » vue √ Select a variant: » vue
|
完成之后项目结构是这样,东西还是那些东西,多了一个vite.config.js没咋见过,这个看官方文档即可,这里不做赘述。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| |-- 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,地址:https://npmmirror.com/
1 2 3 4 5 6
| vite v2.7.6 dev server running at:
> Local: http: > Network: use `--host` to expose
ready in 2203ms.
|
打开HelloWorld.vue组件你会发现,诶,以前浓眉大眼的vue2写法里面的很多配置没了,不要着急,这是曲线救国,不是叛变革命,<template>
的模板绑定上没啥大的改动,主要是在js模块,setup的语法糖可能直接让刚接触的同学如果有点懵逼,你需要先去看看文档VUE3官网地址,这里不做赘述。
1 2 3 4 5 6 7
| <script setup> import { ref } from 'vue' defineProps({ msg: String }) const count = ref(0) </script>
|
2. vue-router的路由引入
基础项目出来了,开始路由的集成,这里用的是最新版本,官网地址:https://next.router.vuejs.org/
老规矩先安装:
1
| npm install vue-router@4
|
创建文件 src/router.js
与3.x的new不同,新版采用createRouter的函数创建,path的匹配规则也改为了正则的方式,其他的差别不大,代码如下,需要注意的是,在vite中导入导出模块用import的方式,用require的方式会比较麻烦
,这里使用了nprogress做路由守卫时候的加载显示效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| 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(); })
router.afterEach((to, from, failure) => { NProgress.done(); window.scrollTo(0, 0); })
export default router;
|
完成路由封装后在src/main.js
中导入
1 2 3 4 5 6 7 8 9
| import { createApp } from 'vue'; import App from './App.vue'; import router from './router';
import './styles/reset.scss';
const app = createApp(App); app.use(router) .mount('#app');
|
与老版本的区别在于其在 composition api
中的使用,其实文档里面都有,这里举个栗子,注意一下这里面有个<script setup>
的语法糖,要是整不醒火,就去看vue3的文档。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <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> <van-button type="warning" @click="back()">返回上级</van-button> </div> </template>
|
3. i18n的国际化引入
国际化仍然采用最新版,官网地址:https://vue-i18n.intlify.dev/ ,老规矩先安装
1
| cnpm install vue-i18n@next
|
在src下创建语言文件
1 2 3 4
| |-- language |-- en.js |-- index.js |-- zh.js
|
en.js
文件内容
1 2 3 4 5 6
| const LANG = { HOME: { TITLE: 'More' } } export default LANG;
|
zh.js
文件内容
1 2 3 4 5 6
| const LANG = { HOME: { TITLE: '更多' } } export default LANG;
|
index.js
文件内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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
即可
1 2 3 4 5 6 7 8 9 10 11
| import { createApp } from 'vue'; import App from './App.vue'; import router from './router'; import i18n from './language';
import './styles/reset.scss';
const app = createApp(App); app.use(router) .use(i18n) .mount('#app');
|
在模板文件中使用方式
1
| <h3>多语言测试=>{{$t('HOME.TITLE')}}</h3>
|
如果你需要在setup()
中用函数方式调用如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <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'; } const getLang = ()=>{ console.log(t('HOME.TITLE')); } return { }
},
})
</script>
|
如果想在单独的js模块中使用,这里需要再global的属性上调用:
1 2 3 4 5
| import language from '@/language'; const currLang = ()=>{ console.log(language.globa.t('HOME.TITLE')); language.global.locale.value; };
|
4. vuex的引入
老规矩安装最新版,官方文档 https://next.vuex.vuejs.org/,
1
| npm install vuex@next --save
|
在src
下创建模块文件
1 2 3 4 5 6
| |-- store | |-- actions.js | |-- getters.js | |-- index.js | |-- mutations.js | |-- state.js
|
vuex 4.x的版本相较于之前的老版本,没有太大的区别,更多的是适配composition api的调整,
createPersistedState
是vuex本地持久化插件,根据个人情况选择安装
1 2 3 4 5 6
|
const state = { userInfo: 'default data' } export default state;
|
1 2 3 4 5 6 7 8 9
|
export const setUserInfo = (state, payload) => { state.userInfo = payload; }
export const setTestVal = (state, payload) => { state.testVal = payload; }
|
1 2 3 4 5
|
export const actionSetUserInfo = ({ commit }, payload) => { commit('setUserInfo', payload); }
|
1 2 3 4
| export const gettersUserInfo = (state) => { return `filter=>${state.userInfo}`; }
|
完成上述的各个模块的配置,我们将它组装到一起,然后导入到main.js
就可以愉快的使用了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
import {createStore} from 'vuex' import * as getters from './getters' import * as actions from './actions' import * as mutations from './mutations' import state from './state' i
const store = createStore({ state, getters, actions, mutations })
export default store
|
此时的main.js
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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';
const app = createApp(App); app.use(router) .use(store) .use(i18n) .mount('#app');
|
在组件文件中的使用,你熟悉的option api
的语法糖方式还在,不过咱都既然折腾vue3了,还是最好忘掉过去…
让洒家带你看看在setup()
中的 composition api
的使用,来康康下面两种使用方式的区别
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
|
<template> <div class="box"> <h1>vuex 组合api方式</h1> <div>{{title}} / {{userInfo}} / {{gettersUserInfo}}</div> <div> <van-button type="danger" @click="mutations()">mutations</van-button> <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> </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调用,还是跟之前一样
1 2
| import store from '@/store';
|
至此,vuex的基本使用基本就满足了,当然这里还没涉及到更大型的项目中state的模块拆分,后面有时间再补吧。
5. vant的引入
NOW,到了我们最喜闻乐见的vant的集成了,老规矩自己先看看最新版的文档 https://youzan.github.io/vant/v3/ ,大致过了一下,调用方式基本没啥变化,只是demo的调用方式更新为了vue3的方式,其他的对老用户来说没啥理解成本,我这里把css全部引入,组件按需引入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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');
|
试试组件中是否正常使用就完事儿
1
| <van-button type="warning">来啊,造作啊</van-button>
|
6. 基于axios的requset封装
此时我们已经可以单机操作,但是想要与服务器api深入交流,大力推进,打开格局,共同开创和谐网络大环境
,我们就需要封装一套网络请求的方法,这里采用axios来做二次封装,翠花儿,上酸菜
老规矩,官方问文档:http://www.axios-js.com/zh-cn/docs/ ,看完安装一二
在src
下建模块文件,以后不管走到何方你都始终记得,fetch
是vue项目不可分割的一部分,它自古以来就是咱项目的一份子,愿君听之,信之(狗头报名)…
1 2 3
| |-- fetch | |-- index.js | |-- requset.js
|
这里我在src
目录下,新建了一个config.js
来记录一些常见配置项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
const development = import.meta.env.DEV; import language from '@/language';
export const ApiProxyBasePath = '/serverApi';
export const noGifUrl = [];
export const requestJsonUrl = [];
export const currLang = ()=>{ return language.global.locale.value; };
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
|
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, }
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(); }) }) }
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(); }) }) }
export default { get, post }
|
完成封装后,在index.js
做请求调用的返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
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 }); }
export function postTest(data) { return Requset.post({ url: `${ApiProxyBasePath}/api/xxxx`, params: qs.stringify(data) }); }
|
此时组件需要调用接口干活儿了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
<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; }
return { callApi, ...toRefs(state) } }, }); </script>
|
7.vite.config
的常用配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; const { resolve } = require('path'); const isProduction = process.env.NODE_ENV === 'production';
export default defineConfig({ plugins: [vue()], base: isProduction ? './' : '/', resolve: { extensions: ['.js', '.vue', '.json'], alias: { '@': resolve(__dirname, 'src'), }, }, server: { port: 4000, proxy: { '/serverApi': { target: 'https://myApiHost/api/v1/xxx', changeOrigin: true, ws: true, rewrite: (path) => path.replace(/^\/serverApi/, '') }, } }, css: { preprocessorOptions: { scss: { charset: false } } } })
|
8.项目已出舱,感觉良好!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| |-- 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
|
此时你就拥有了如下目录的完整项目,基本常见一些移动端应该是能出活儿了。
第一次写这么长的文章,也算是自己的一个记录,如果同时能对初学的小伙伴有一点帮助,我会很欣慰,贴的代码比较多,看起来肯定比较累,我自己也还在探索阶段,如果有些东西理解不对,希望能有好心人温柔的指出,共同探讨,共同进步,以促进和谐社会的精神文明建设!