没有特别的幸运,那么就特别的努力!!!
vue3 + vite + ts + vant + axios + sass 移动端h5搭建新项目
vue3 + vite + ts + vant + axios + sass
搭建第一个 Vite 项目 (vite + vue + ts)
兼容性注意
Vite 需要 Node.js 版本 14.18+,16+。然而,有些模板需要依赖更高的 Node 版本才能正常运行,当你的包管理器发出警告时,请注意升级你的 Node 版本。
nvm管理node多版本。1
// 搭建第一个 Vite 项目 (vite + vue + ts)
// npm (本篇采用npm搭建)
npm init vite@latest
// yarn
yarn create vite
// pnpm
pnpm create vite
# npm 6.x
npm create vite@latest vite-vue3 --template vue
# npm 7+, extra double-dash is needed:
npm create vite@latest vite-vue3 -- --template vue
# yarn
yarn create vite vite-vue3 --template vue
# pnpm
pnpm create vite vite-vue3 --template vue
项目启动
cd vite-vue3
npm install
npm run dev
代码规范 (格式化、提示)
eslint
# 自动生成配置文件并安装下面四个依赖
npx eslint --init
# 或者手动创建文件
# npm i eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-vue -D
prettier
npm i prettier eslint-config-prettier eslint-plugin-prettier -D
创建prettier文件
// prettier.cjs
module.exports = {
printWidth: 100,
tabWidth: 2,
useTabs: false, // 是否使用tab进行缩进,默认为false
singleQuote: true, // 是否使用单引号代替双引号,默认为false
semi: true, // 行尾是否使用分号,默认为true
arrowParens: 'always',
endOfLine: 'auto',
vueIndentScriptAndStyle: true,
htmlWhitespaceSensitivity: 'strict',
};
配置eslintrc
// eslintrc.cjs
module.exports = {
root: true, // 停止向上查找父级目录中的配置文件
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:vue/vue3-essential',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
'prettier', // eslint-config-prettier 的缩写
],
parser: 'vue-eslint-parser', // 指定要使用的解析器
// 给解析器传入一些其他的配置参数
parserOptions: {
ecmaVersion: 'latest', // 支持的es版本
parser: '@typescript-eslint/parser',
sourceType: 'module', // 模块类型,默认为script,我们设置为module
},
plugins: ['vue', '@typescript-eslint', 'prettier'], // eslint-plugin- 可以省略
rules: {
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-var-requires': 'off',
},
};
配置 tsconfig
// tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
// 👆是初始化默认配置
/*
在ts中导入js模块会报错找不到类型声明
解决方法一:
仅设置 "allowJs": true 即可
注:allowJs设置true时,下方include不可以加入'src/**\/*.js',否则报错'无法写入文件xx因为它会覆盖输入文件'
方法二:
仅在 env.d.ts 中加入 declare module '*.js'; 模块定义即可
总结:和 "include": ["src/**\/*.js"] 没有任何关系
*/
"allowJs": true, // 允许编译器编译JS,JSX文件
"baseUrl": "./",
// "typeRoots": [
// "node_modules/@types" // 默认会从'node_modules/@types'路径去引入声明文件
// ],
// "types": ["node"] // 仅引入'node'模块
// "paths"是相对于"baseUrl"进行解析
// 在vite.config里配置了路径别名resolve.alias,为了让编译 ts 时也能够解析对应的路径,我们还需要配置 paths 选项
"paths": {
"@/*": ["src/*"],
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
// references属性是 TypeScript 3.0 的新特性,允许将 TypeScript 程序拆分结构化(即拆成多个文件,分别配置不同的部分)。
"references": [{ "path": "./tsconfig.node.json" }]
}
tsconfig.node.json
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts", "config/index.ts"]
}
CSS 预处理器
less安装使用
// npm 安装
npm install less
npm install less-loader
// yarn 安装
yarn add less less-loader
// 使用
< style lang="less" scoped></ style>
sass安装使用
// npm 安装
npm install -D sass sass-loader
// yarn 安装
yarn add sass sass-loader
<style lang="scss" scoped>
.home {
background-color: #eee;
height: 100vh;
}
</style>
vant 安装
vant3官网地址:
安装
// npm 安装
npm i vant
// yarn 安装
yarn add vant
// 通过 pnpm 安装
pnpm add vant
常规用法
import { createApp } from 'vue'
import './style.css'
// 1. 引入你需要的组件
import { Button } from 'vant';
// 2. 引入组件样式
import 'vant/lib/index.css';
import App from './App.vue'
const app = createApp(App)
// 3. 注册你需要的组件
app.use(Button);
app.mount('#app');
按需引入组件样式
// 通过 npm 安装
npm i unplugin-vue-components -D
// 通过 yarn 安装
yarn add unplugin-vue-components -D
// 通过 pnpm 安装
pnpm add unplugin-vue-components -D
配置插件
vite 的项目,在 vite.config.js 文件中配置插件:
import vue from '@vitejs/plugin-vue';
import Components from 'unplugin-vue-components/vite';
import { VantResolver } from 'unplugin-vue-components/resolvers';
export default {
plugins: [
vue(),
Components({
resolvers: [VantResolver()],
}),
],
};
使用组件
<template>
<van-button type="primary" />
</template>
Rem 布局适配
// npm 安装
npm install -D postcss-pxtorem lib-flexible
// yarn 安装
yarn add postcss-pxtorem lib-flexible
根目录下面新建一个 postcss.config.js 文件
// postcss.config.js
module.exports = {
plugins: {
'postcss-pxtorem': {
rootValue: 37.5,
propList: ['*'],
},
},
};
底部适配 - 对于ios系统
<!-- 在 head 标签中添加 meta 标签,并设置 viewport-fit=cover 值 -->
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover"
/>
<!-- 开启顶部安全区适配 -->
<van-nav-bar safe-area-inset-top />
<!-- 开启底部安全区适配 -->
<van-number-keyboard safe-area-inset-bottom />
vue-router
1.安装
npm i vue-router@4
2.创建路由
// src/router/index.ts
//现在创建router的方式与vue2.x的版本已经很不同了
import { createRouter, createWebHashHistory } from "vue-router";
import { routes } from "./routes";
const router = createRouter({
history: createWebHashHistory(), //替代之前的mode,是必须的
routes,
});
router.beforeEach((to, from, next) => {
document.title = to.meta.title as string || '浙里普法'
next()
})
export default router;
// src/router/routes.ts
import { RouteRecordRaw } from "vue-router";
export const routes: Array<RouteRecordRaw> = [
{
path: "/",
redirect: "/index",
},
{
path: "/index",
name: "Index",
component: () => import("../view/index.vue"),
meta: {
nav: true,
title: '首页'
}
},
];
3.挂载路由
// src/main.ts
import { createApp } from 'vue';
import App from './App.vue';
import router from './router/index'; //引入vue-router
const app = createApp(App);
app.use(router); // 挂载到app上
app.mount('#app');
4.使用
<template>
<router-view />
</template>
Axios
1.安装
// npm 安装
npm i axios
// yarn 安装
yarn add axios
// src/utils/http/axios.ts
import axios, { AxiosResponse, AxiosRequestConfig, AxiosError } from 'axios';
import type { Response } from './types';
// import { auth } from '@/utils';
import { Toast } from 'vant';
import router from '../../router';
axios.defaults.baseURL = '/api';
axios.defaults.timeout = 1000 * 60;
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
// 创建axios实例
const service = axios.create({
// 根据不同env设置不同的baseURL
baseURL: import.meta.env.VITE_APP_API_BASE_URL,
});
// axios实例拦截请求
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
config.headers = {
...config.headers,
// ...auth.headers(), // 你的自定义headers,如token等
};
return config;
},
(error: AxiosError) => {
return Promise.reject(error);
}
);
// axios实例拦截响应
service.interceptors.response.use(
// 2xx时触发
(response: AxiosResponse<Response>) => {
// response.data就是后端返回的数据,结构根据你们的约定来定义
const { code, message } = response.data;
let errMessage = '';
switch (code) {
case 0:
break;
case 1: // token过期
errMessage = 'Token expired';
router.push('/login');
break;
case 2: // 无权限
errMessage = 'No permission';
break;
// default:
// errMessage = message;
// break;
}
if (errMessage) Toast.fail(errMessage);
return response;
},
// 非2xx时触发
(error: AxiosError) => {
Toast.fail('Network Error...');
return Promise.reject(error);
}
);
export type { AxiosResponse, AxiosRequestConfig };
export default service;
// src/utils/http/index.ts
import service, { AxiosRequestConfig } from './axios';
export * from './types';
export const request = <T = any>(config: AxiosRequestConfig): Promise<T> => {
return new Promise((resolve, reject) => {
service
.request(config)
.then((res) => {
// 一些业务处理
resolve(res.data);
})
.catch((err) => {
console.log('request fail:', err);
});
});
};
const http = {
get<T = any>(url: string, params = {}, config?: AxiosRequestConfig): Promise<T> {
return request({ url, params, ...config, method: 'GET' });
},
post<T = any>(url: string, data = {}, config?: AxiosRequestConfig): Promise<T> {
return request({ url, data, ...config, method: 'POST' });
},
put<T = any>(url: string, data = {}, config?: AxiosRequestConfig): Promise<T> {
return request({ url, data, ...config, method: 'PUT' });
},
delete<T = any>(url: string, data = {}, config?: AxiosRequestConfig): Promise<T> {
return request({ url, data, ...config, method: 'DELETE' });
},
// 上传文件,指定 'Content-Type': 'multipart/form-data'
upload<T = any>(url: string, data = {}, config?: AxiosRequestConfig): Promise<T> {
return request({
url,
data,
...config,
method: 'POST',
headers: { 'Content-Type': 'multipart/form-data' },
});
},
};
export default http;
// src/utils/http/types.ts
// 和后端约定好接口返回的数据结构
export interface Response<T = any> {
data: string[];
code: number | string;
message: string;
result: T;
}
示例页面
banner列表页面
<script setup>
import ResourceList from '@/components/ResourceList.vue'
import { monthlyResourceList } from '@/service/api/common'
import { onMounted, ref } from 'vue'
import { useRoute, useRouter } from "vue-router";
const $route = useRoute()
const $router = useRouter()
const list = ref([])
const loading = ref(false);
const finished = ref(false);
const refreshing = ref(false);
const params = ref({
relationId: $route.query.id,
relationType: 'banner',
currentPage: 1,
pageSize: 10
})
onMounted(() => {
document.title = $route.query.name
getColumnResourceList()
})
const getColumnResourceList = () => monthlyResourceList(params.value).then(res => {
loading.value = true
if (res.success) {
loading.value = false
list.value = [...list.value,...res.data]
// 如果列表数据条数>=总条数,不再触发滚动加载
if (list.value.length >= res.totalCount) {
finished.value = true
}
}
})
const onRefresh = () => {
params.value.currentPage = 1
finished.value = false;
refreshing.value = false
list.value = []
getColumnResourceList();
};
const onLoad1 = () => {
params.value.currentPage++
getColumnResourceList()
}
const toInfo = row => {
const { type, resourceSource, resourceId, id: relationId, relationType = 'banner' } = row
$router.push({
path: '/detail',
query: { type, resourceSource, resourceId, relationId, relationType }
})
}
</script>
<template>
<div class='monthInfo'>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
:immediate-check="false"
@load="onLoad1"
>
<div v-for="(item, i) in list" :key="i">
<ResourceList :info="item" @click="toInfo(item)"></ResourceList>
</div>
</van-list>
</van-pull-refresh>
</div>
</template>
<style lang='scss' scoped>
.monthInfo {
padding: 22px 16px;
}
</style>
项目地址
为了安全协议:项目地址api 已全部替换(望理解!!!)
https://gitee.com/hammer1010_admin/vue3-vite
vue3 开发
父组件传参
defineProps
父组件
<template>
<Children :msg="msg" :list="list"></Children>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import Children from './Children.vue'
const msg = ref('hello 啊,树哥')
const list = reactive<number[]>([1, 2, 3])
</script>
子组件
<template>
<div>
<p>msg:{{msg}}</p>
<p>list:{{list}}</p>
</div>
</template>
<script setup lang="ts">
import { defineProps } from "vue";
const { msg, list } = defineProps(['msg', 'list'])
</script>
withDefaults 定义默认值
<script setup lang="ts">
import { defineProps } from "vue";
withDefaults(
defineProps<{ msg?: (string | number | boolean), title?: string }>(),{
msg:'hello vite',
title:'默认标题'
}
);
</script>
defineEmits
子组件传递
<template>
<div>
<p>msg:{{msg}}</p>
<p>list:{{list}}</p>
<button @click="onChangeMsg">改变msg</button>
</div>
</template>
<script setup lang="ts">
type Props = {
msg?: string,
list?: number[]
}
withDefaults(defineProps<Props>(), {
msg: '张麻子',
list: () => [4, 5, 6]
})
const emits = defineEmits(['changeMsg'])
const onChangeMsg = () => {
emits('changeMsg','黄四郎')
}
</script>
父组件接收
<template>
<Children :msg="msg" :list="list" @changeMsg="changeMsg"></Children>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import Children from './Children.vue'
const msg = ref('hello 啊,树哥')
const list = reactive<number[]>([1, 2, 3])
const changeMsg = (v: string) => {
msg.value = v
}
</script>
ref VS reactive
- reactive返回一个对象的响应式代理。
- ref参数一般接收简单数据类型,若ref接收对象为参数,本质上会转变为reactive方法
- 在JS中访问ref的值需要手动添加.value,访问reactive不需要
- 响应式的底层原理都是Proxy
watch
侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。
监听ref定义的一个响应式数据
<script setup lang="ts">
import { ref, watch } from "vue";
const str = ref('一个值')
//3s后改变str的值
setTimeout(() => { str.value = '3s后一个值' }, 3000)
watch(str, (newV, oldV) => {
console.log(newV, oldV) //3s后一个值 一个值
})
</script>
监听多个ref
<script setup lang="ts">
import { ref, watch } from "vue";
let name = ref('树哥')
let age = ref(18)
//3s后改变值
setTimeout(() => {
name.value = '我叫树哥'
age.value = 19
}, 3000)
watch([name, age], (newV, oldV) => {
console.log(newV, oldV) // ['我叫树哥', 19] ['树哥', 18]
})
</script>
监听reactive 定义响应式对象的单一属性
<script setup lang="ts">
import { reactive, watch } from "vue";
let info = reactive({
name: '张麻子',
age: 18,
obj: {
str: '彼时彼刻,恰如此时此刻'
}
})
//3s后改变s值
setTimeout(() => {
info.obj.str = 'to be or not to be'
}, 3000)
// 需要自己开启 deep:true深度监听,不然不发触发 watch 的回调函数
watch(() => info.obj, (newV, oldV) => {
console.log(newV, oldV)
}, {
deep: true
})
</script>
watch VS watchEffect
watch只有监听的值发生变化的时候才会执行
watchEffect 立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。
wacthEffect 无法获取到原值,只能得到变化后的值
watchEffect 不用指明监视哪个属性,监视的回调中用到哪个属性就监视哪个属性
生命周期
keep-alive 缓存组件
作用和vue2一致,只是生命周期名称有所更改
<template>
<div class="full-screen">
<router-view v-slot="{ Component }">
<keep-alive :include="['Index', 'secondaryPage', 'resource', 'monthInfo', 'collect']">
<component :is="Component" />
</keep-alive>
</router-view>
</div>
</template>
provide/inject
provide 可以在祖先组件中指定我们想要提供给后代组件的数据或方法,而在任何后代组件中,我们都可以使用 inject 来接收 provide 提供的数据或方法。
父组件
<template>
<router-view v-if="isRouterView"></router-view>
</template>
<script lang="ts" setup>
import { ref, provide, nextTick } from "vue";
const isRouterView = ref(true);
//父组件刷新方法
const reload = () => {
isRouterView.value = false;
nextTick(() => {
isRouterView.value = true;
})
}
//provide进行注册
provide('reload', reload);
</script>
子/孙组件
<script lang="ts" setup>
//子孙组件引入inject
import { ref,inject } from "vue";
const reload = inject("reload");
//调用方法使用
const handleClick = (val: any) => {
if (typeof reload == "function") reload();
};
</script >
希望能帮助到大家,同时祝愿大家在开发旅途中愉快!!!
可以运用nvm管理node多版本,其中最常见就是环境依赖问题 (npm 安装报错 npm ERR! Unexpected token ‘.’) 可以参考这篇文章:https://www.cnblogs.com/yilei-zero/p/16003054.html ↩︎