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

Friday , 2021-12-24 21:36

前言

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
cnpm i
npm run dev
1
2
3
4
5
6
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官网地址,这里不做赘述。

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('/login');
}
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';//scss的使用不赘述,文档 https://www.sass.hk/guide/

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>
&nbsp;&nbsp;&nbsp;
<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';//scss的使用不赘述,文档 https://www.sass.hk/guide/

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';
}//changeLange

const getLang = ()=>{ //打印语言
console.log(t('HOME.TITLE'));
}

return {
}

},//setup

})


</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
//state.js

const state = {
userInfo: 'default data'
}
export default state;
1
2
3
4
5
6
7
8
9
//mutations.js

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

export const setTestVal = (state, payload) => {
state.testVal = payload;
}
1
2
3
4
5
//actions.js

export const actionSetUserInfo = ({ commit }, payload) => {//获取api的用户信息
commit('setUserInfo', payload);
}//actionSetUserInfo
1
2
3
4
//getters.js
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
//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如下:

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';//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的使用,来康康下面两种使用方式的区别

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>&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调用,还是跟之前一样

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

至此,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/ ,看完安装一二

1
npm install axios

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
//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;
};
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
//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做请求调用的返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//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

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

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
//组件文件.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 的常用配置

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';
// 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.项目已出舱,感觉良好!

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

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

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