vue3已经出来很久了,因为工作只是再维护老项目,没有做技术更新,所以对vue3的使用上面会差很多,但是现在又有许多公司要求有vue3使用经验,所以对Vue3 ts自学写的模板项目
这里会写明全部流程及要点。
Vite Vue3 Typescript项目地址 https://github.com/Seven7v/vue3-Ts-admin
需要的话可以自行下载
- vite使用的
Rollup
进行打包,相对来说是比webpack更加轻量级的,这里从项目的启动速度就可以体现出来。 - vite 天生支持
typescript
使用ts更加友好 - vite 带有
css
预处理器,包括less
scss
使用都可以不用安装loader,(在webpack中需要安装loader) - vite在修改config文件后不需要重启项目,会自动更新页面
对比Vue3 对比Vue2 的更新
- 在vue2中,同一元素上的
v-for
的优先级高于v-if
,vue3更改了两者的优先级,v-if
的优先级高于v-for
destroyed
生命周期选项被重命名为unmounted
beforeDestroy
生命周期选项被重命名为beforeUnmount
- 用
Proxy
代替Obiect.defineProperty
重构了响应式系统可以监听到数组下标变化,及对象新增属性,因为监听的不是对象属性,而是对象本身,还可拦截 apply、has 等13种方法 - 支持在
<style></style>
里使用 v-bind,给CSS绑定S变量(color: v-bind(str)) - 新增
Composition API
可以更好的逻辑复用和代码组织,同一功能的代码不至于像以前一样太分散
安装vite
使用npm init vite
进行安装
PS F:v3> npm init vite
Need to install the following packages:
create-vite@4.4.1
Ok to proceed? (y) y
√ Project name: ... vite-project
√ Select a framework: » Vue
√ Select a variant: » TypeScript
Scaffolding project in F:v3vite-project...
随后执行 vite 项目就跑起来了。
代码语言:txt复制 cd vite-project
npm install
npm run dev
vite官网还提供了其他创建方式
npm create vite@latest
使用yarnyarn create vite
使用pnpmpnpm create vite
项目运行成功后 NetWork 不显示链接,
代码语言:txt复制 ➜ Local: http://127.0.0.1:5173/
➜ Network: use --host to expose
➜ press h to show help
这里我们可以更新vite.config.ts
server: {
host: '0.0.0.0',
port: 5800, //设置服务启动端口号,是一个可选项,不要设置为本机的端口号,可能会发生冲突
open: true, //是否自动打开浏览器,可选项
}
这时终端就会更新为
代码语言:txt复制 ➜ Local: http://localhost:5800/
➜ Network: http://192.168.xxx.xx:5800/
➜ Network: http://xx.xx.xxxxx:5800/
项目路由
创建好项目后为我们的项目配置路由
代码语言:txt复制npm install vue-router@4
vue-router文档提供了使用的手册,
新建 router
文件夹,index.ts
中的内容如下
import { createRouter, createWebHistory } from 'vue-router'
import { App } from 'vue'
import { getUserInfoApi } from '../sever/api'
import routes from './routes' // 页面中 配置的路由
const router = createRouter({
history: createWebHistory(), //history模式
routes
})
router.beforeEach(async (to, from) => {
// 这里可以输入一些页面重定向内容,判断token跳转登录页
})
// export default route 将路由导出的写法
// 这里只导出一个方法,在页面外不可以修改router里的内容
// 封装路由方法,传入app <Element> 代表页面内的标签元素
export const initRouter = (app: App<Element>) => {
app.use(router)
}
route.ts
配置项目路由
import { RouteRecordRaw } from 'vue-router'
const Layout = () => import('../layout/index.vue') //页面layout
const Login = () => import('../pages/login/index.vue')
const Homepage = () => import('../pages/homePage/index.vue')
const Chart = () => import('../pages/chart/index.vue')
const DocumentSetting = () => import('../pages/document/setting.vue')
const DocumentPreview = () => import('../pages/document/preview.vue')
const UserConsole = () => import('../pages/console/userConsole.vue')
const PermissionConsole = () => import('../pages/console/permissionConsole.vue')
const routes: RouteRecordRaw[] = [
{
path: '/admin',
component: Layout,
name: 'admin',
meta: {
icon: 'Menu',
isNav: true
},
children: [
{
path: '/admin/home',
component: Homepage,
name: 'home',
meta: {
isNav: true
}
}
]
},
{
path: '/login',
component: Login,
name: 'login'
},
{
path: '/',
redirect: '/admin/home'
}
]
const asyncRouter: RouteRecordRaw[] = [
{
path: '/statistics',
component: Layout,
name: 'statistics',
meta: {
icon: 'PieChart',
isNav: true,
},
children: [
{
path: '/statistics/chart',
component: Chart,
name: 'chart',
meta: {
isNav: true,
}
}
]
},
{
path: '/document',
component: Layout,
name: 'document',
meta: {
isNav: true,
icon: 'Document'
},
children: [
{
path: '/document/setting',
component: DocumentSetting,
name: 'setting',
meta: {
isNav: true,
role: ['admin', 'editor', 'normal']
}
},
{
path: '/document/table',
component: DocumentPreview,
name: 'table',
meta: {
isNav: true,
role: ['admin', 'editor', 'normal']
}
}
]
},
]
const CurrentRoute: RouteRecordRaw[] = routes.concat(...asyncRouter)
export default CurrentRoute
i18n
安装 vue-i18n
创建i18n文件 使用方法如下
import { App } from 'vue'
import { createI18n } from 'vue-i18n'
import { zh } from './zh'
import { en } from './en'
const language = (navigator.language || 'en').toLocaleLowerCase() // 获取浏览器的语言设置
const i18n = createI18n({
legacy: false,
locale: localStorage.getItem('lang') || language, // 优先从本地存储获取语言设置,如果没有则使用浏览器默认语言
fallbackLocale: 'en', // 当前语言无法找到匹配的翻译时,使用的备选语言
messages: {
en,
zh
}
})
// 封装i18n方法
export const initI18n = (app: App<Element>) => {
app.use(i18n)
}
element-plus
成功后可以安装element-plus,官网里包括安装,全局引用及自动按需引用都有配置教程
代码语言:txt复制$ npm install element-plus --save
main.ts
main.ts
中引入路由, i18n,全局样式
import { createApp } from 'vue'
import './style.css'
import './assets/style/common.css'
import 'element-plus/dist/index.css'
import App from './App.vue'
import { initRouter } from './routes'
import { initI18n } from './i18n'
import * as ElIcons from '@element-plus/icons-vue'
setTeam()
const app = createApp(App)
initI18n(app)
// 在页面中调用router封装的方法挂载路由
initRouter(app)
app.mount('#app')
for (const name in ElIcons) app.component(name, (ElIcons as any)[name])
做了这些工作,在页面内修改path就可以进行页面切换了,
切换语言
封装切换项目语言组件,可以写在项目公用组件库里 components
文件夹里
changeLang
组件内容
<script setup lang="ts">
import { getCurrentInstance, ref, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'
// 切换语言
const { proxy } = getCurrentInstance() as any
const { t } = useI18n()
const lang = ref('chinese')
watchEffect(() => {
if (lang.value === 'chinese') {
proxy.$i18n.locale = 'zh'
localStorage.setItem('lang', 'zh')
}
if (lang.value === 'english') {
proxy.$i18n.locale = 'en'
localStorage.setItem('lang', 'en')
}
})
</script>
<template>
<el-select style="width: 100px" v-model="lang">
<el-option :label="t('Chinese')" value="chinese" />
<el-option :label="t('English')" value="english" />
</el-select>
</template>
封装请求方法
这里我们使用axios,请求,对于接口 单是前端项目可以考虑用rap2实现模拟请求,
创建server目录,server目录中index.ts
进行封装,api.ts
或其他文件用来管理接口内容
index.ts
import axios from 'axios'
import { AxiosInstance } from 'axios'
import { ElMessage } from 'element-plus'
const baseUrl = '/api'
export const $axios: AxiosInstance = axios.create({
baseURL: baseUrl,
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
$axios.interceptors.request.use(config => {
var xtoken: any = localStorage.getItem('token')
if (xtoken) {
xtoken = xtoken
config.headers['Authorization'] = xtoken
}
return config
})
$axios.interceptors.response.use(
(res: any) => {
console.log(res)
const { code, message } = res.data
if (code === 200) {
if (message) {
ElMessage.success(message)
}
return res
} else {
ElMessage.error(res.massage)
return Promise.reject(new Error(res.message))
}
},
(err: any) => {
console.error(err)
const message = err.response.data
ElMessage.error(message)
return Promise.reject(new Error(err.message))
}
)
api.ts
import { $axios } from './index.ts'
import { InterfaceLoginReq } from '../type'
// 登录
export const loginApi = (params: InterfaceLoginReq) => {
return $axios.post('/login', params)
}
// 创建用户
export const createUserApi = (params: InterfaceLoginReq) => {
return $axios.post('/create', params)
}
// 获取用户信息
export const getUserInfoApi = () => {
return $axios.get('/userInfo')
}
// 登出账号
export const logoutApi = () => {
return $axios.post('/logout')
}
登录页 表单提交
代码语言:txt复制<template>
<div class="login cen disflex ai-cen">
<div class="login-form-wrapper disflex">
<div class="login-img-wrapper bg-prim">
<div class="login-title fw900">{{ $t('login.management') }}</div>
<img class="login-img" src="../../assets/img/login.svg" alt="" />
</div>
<div class="login-box">
<el-card class="login-inner">
<changeLanguage class="login-lang" />
<el-form
ref="formRef"
:model="dynamicValidateForm"
label-width="100px"
label-position="left"
class="login-form mb30"
>
<el-form-item
prop="username"
:label="t('login.username')"
:rules="[
{
required: true,
message: t('login.usernameRequire'),
trigger: 'blur'
}
]"
>
<el-input v-model="dynamicValidateForm.username" />
</el-form-item>
<el-form-item
prop="password"
:label="t('login.password')"
:rules="[
{
required: true,
message: t('login.passwordRequire'),
trigger: 'blur'
}
]"
>
<el-input type="password" v-model="dynamicValidateForm.password" />
</el-form-item>
</el-form>
<el-button type="primary" class="login-btn" @click="submitForm(formRef)">{{
t('login.login')
}}</el-button>
<el-button class="login-btn" @click="resetForm(formRef)">{{
t('login.concel')
}}</el-button>
</el-card>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { FormInstance } from 'element-plus'
import { InterfaceLoginReq } from '../../type'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { loginApi, createUserApi } from '../../sever/api'
import { setLoginTimeApi } from '../../sever/data'
import changeLanguage from '../components/changeLanguage.vue'
const { t } = useI18n()
const formRef = ref<FormInstance>()
const dynamicValidateForm = reactive<InterfaceLoginReq>({
username: '',
password: ''
})
const router = useRouter()
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.validate(async valid => {
if (valid) {
// 创建用户时使用
// await createUserApi(dynamicValidateForm)
const res = await loginApi(dynamicValidateForm)
if (res.data.code == 200) {
localStorage.setItem('token', res.data.token)
const loginTimeParams = {
username: dynamicValidateForm.username,
loginTime: new Date()
}
const resq = await setLoginTimeApi(loginTimeParams)
console.log(resq)
router.push({
name: 'home'
})
}
} else {
return false
}
})
}
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
}
</script>
<style lang="less" scoped>
.login {
width: 100%;
height: 100%;
&-form-wrapper {
width: 100%;
height: 100%;
}
&-img-wrapper {
width: 50%;
}
&-title {
font-size: 40px;
font-family: Verdana, Geneva, Tahoma, sans-serif;
color: #fff;
margin-top: 15%;
margin-bottom: 15%;
margin-left: 10%;
}
&-img {
width: 60%;
margin-left: 30%;
}
&-form {
margin-bottom: 20px;
}
&-box {
width: 50%;
background-color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
}
&-inner {
position: relative;
width: 60%;
height: 400px;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
&-lang {
position: absolute;
right: 20px;
top: 20px;
}
&-btn {
width: 100%;
margin-bottom: 15px;
}
}
/deep/ .el-button .el-button {
margin-left: 0;
}
</style>