基础环境
- Vue CLI 版本为 v5.0.0-alpha.2
- Node.js 版本为 v15.3.0 (官方建议是 10 以上版本,最低为 8.9)
- yarn 版本为 1.22.10 (推荐使用,用 NPM 也可以)
通过以下命令行查询对应版本号:
1 2 3 4 5
| vue --version
node --v
yarn -v
|
如发现版本不满足要求,可以分别通过:
项目创建
Vue 默认会通过以前选择过的包管理工具 yarn 或 NPM 来安装依赖。想全局修改的话,可在命令行中运行:
1
| vue config --set packageManager yarn // 或 npm 推荐 yarn
|
也可在创建项目时动态指定当前项目的包管理工具:
1
| vue create vue3-starter -m yarn
|
勾选以下几项(单击图片可看大图):
依次选择如下内容:
最后会问是否要保存当前这个配置,按自己的意愿选择和命名。
成功后,运行如下命令行:
1 2
| cd vue3-starter yarn serve
|
在浏览器中打开 http://localhost:8080/ 看到页面就算完成了。
项目改造
默认结构
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
| ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── assets │ │ └── logo.png │ ├── components │ │ └── HelloWorld.vue │ ├── router │ │ └── index.ts │ ├── store │ │ └── index.ts │ ├── views │ │ ├── About.vue │ │ └── Home.vue │ ├── App.vue │ ├── main.ts │ └── shims-vue.d.ts ├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── babel.config.js ├── package.json ├── README.md └── tsconfig.json
|
内容改造
安装依赖
axios
axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。
Normalize.css
Normalize.css 它使不同浏览器能更一致地呈现所有元素,并符合现代标准。
Element Plus
Element Plus,是为一套基于 Vue 3.0 的桌面端组件库。
1 2
| yarn add element-plus yarn add babel-plugin-component -D // 为了按需打包
|
修改文件
按照名称顺序,由上到下,由外到内。
- 修改 .editorconfig 中最后一行(现在屏幕都比较宽,100 个字符确实满足不了需求)
- 修改 .eslintrc.js 中的 rules (打包时配置 将 console 和 debug 全部删除,不需要做这个提示)
1 2 3 4 5 6
| 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'import/prefer-default-export': 'off', 'max-len': ['error', { code: 160 }], 'multiline-comment-style': ['error', 'starred-block'],
|
1 2 3 4 5 6 7 8 9 10 11 12
| module.exports = { presets: ["@vue/cli-plugin-babel/preset"], plugins: [ [ "component", { libraryName: "element-plus", styleLibraryName: "theme-chalk", }, ], ], };
|
- 添加 vue.config.js(定义自身的 WebPack 参数)
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
|
function isProd() { return process.env.NODE_ENV === "production"; }
process.env.VUE_APP_BASE_API = isProd() ? "" : "http://rap2api.taobao.org/app/mock/115307/user";
module.exports = { publicPath: isProd() ? "./" : "/", productionSourceMap: false,
configureWebpack: (config) => { if (isProd()) { Object.assign( config.optimization.minimizer[0].options.terserOptions.compress, { drop_console: true, } ); } }, };
|
- 替换 public 下的 favicon.ico 为自己的网站图标
- 修改 public 下的 index.html 中的语言(设置为中文后,浏览器不会出现翻译提示)
1 2 3 4
| <html lang=""> // 改为 <html lang="zh"></html> </html>
|
- 在 src 下添加 hooks(所有钩子函数存放在此),services(请求后台接口的模块存放在此),utils(常用功能)
- 修改 src 下的 App.vue 为 app.vue (所有文件的命名统一使用 kebab-case 命名法),删除大部分内容只保留
1 2 3
| <template> <router-view /> </template>
|
1 2 3 4 5 6 7 8 9 10 11 12
| import { createApp } from "vue";
import "normalize.css"; import "@/assets/styles/style.scss";
import App from "./app.vue"; import router from "./router"; import store from "./store";
const app = createApp(App); app.use(store); app.use(router).mount("#app");
|
- 删除 src/assets 下 logo.png 文件,添加 fonts(字体)、icons(小图标)、images(大图片)、styles(CSS 样式)文件夹
- 在 src/assets/images 下 添加 common.scss(各项目通用样式) 和 style.css(当前应用全局样式)
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
| // common.css
html, body { height: 100%; }
input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { -webkit-appearance: none; }
input[type="number"] { -moz-appearance: textfield; }
input:-webkit-autofill, textarea:-webkit-autofill, select:-webkit-autofill { -webkit-text-fill-color: #ededed !important; box-shadow: 0 0 0px 1000px transparent inset !important; background-color: transparent; background-image: none; transition: background-color 50000s ease-in-out 0s; }
|
1 2 3
| // style.scss
@import "./common.scss";
|
- 删除 components 文件夹下 HelloWorld.vue 文件,添加 hooks.vue(添加一个使用 hooks 的例子)
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
| <template> <div> <div class='title'>{{myTitle}}</div> <button @click="handleCLick">防抖测试</button> <div class='scroll-box' @scroll="handleScroll(throttleRef)"> {{throttleRef}}测试 <div style="height: 200px"></div> <div style="height: 200px"></div> <div style="height: 200px"></div> </div> </div> </template>
<script lang="ts"> import { ref, defineComponent } from 'vue'; import { useDebounce } from '@/hooks/common/use-debounce'; import { useThrottle } from '@/hooks/common/use-throttle';
export default defineComponent({ name: 'Hooks', props: { title: String, }, setup(props) { const throttleRef = ref('节流');
const handleCLick = useDebounce((() => { console.log('防抖测试'); }), 500); const handleScroll = useThrottle(((message) => { console.log(`${message}测试`); }), 500);
return { myTitle: props.title, throttleRef, handleCLick, handleScroll, }; }, }); </script>
<style lang="scss">
.title{ text-align: center; }
button{ margin-bottom: 8px; }
.scroll-box{ height:300px; width:500px; background-color:rgb(209, 204, 204); overflow-y:scroll; }
</style>
|
- 在 src/hooks 下添加 common(各项目通用 hook 函数) 文件夹,添加 use-debounce.ts(防抖),use-throttle.ts(节流),use-router.ts(路由)三个常用 hook
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
|
export function useDebounce<F extends (...args: unknown[]) => unknown>( fn: F, duration = 1000 ): () => void { let timeoutId: ReturnType<typeof setTimeout> | undefined;
const debounce = (...args: Parameters<F>) => { if (timeoutId) { clearTimeout(timeoutId); } timeoutId = setTimeout(() => { fn(...args); timeoutId = undefined; }, duration); };
return debounce; }
|
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
|
export function useThrottle<F extends (...args: unknown[]) => unknown>( fn: F, duration = 1000 ): () => void { let timeoutId: ReturnType<typeof setTimeout> | undefined;
const throttle = (...args: Parameters<F>) => { if (timeoutId) { return; } timeoutId = setTimeout(() => { fn(...args); timeoutId = undefined; }, duration); };
return throttle; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
import { reactive, toRefs, watch, getCurrentInstance, Ref } from "vue";
import { Router } from "vue-router";
export function useRouter(): { route: Ref; router: Router } { const vm = getCurrentInstance(); const state = reactive({ route: vm?.proxy?.$route }); watch( () => vm?.proxy?.$route, (newValue) => { state.route = newValue; } ); return { ...toRefs(state), router: vm?.proxy?.$router as Router }; }
|
- 在 src/router 下添加 home.ts 作为一个示例模块的路由
1 2 3 4 5 6 7 8 9 10 11
| import { RouteRecordRaw } from "vue-router";
const homeRoutes: Array<RouteRecordRaw> = [ { path: "/home", name: "home", component: () => import("@/views/home.vue"), }, ];
export default homeRoutes;
|
- 修改 src/router 下的 index.ts(让它能够自动加载 router 文件夹下的其它路由模块,以后只需要在 router 下添加像 home 一样的路由模块即可)
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 { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router"; import Login from "../views/login.vue";
const constRoutes: Array<RouteRecordRaw> = [ { path: "/", name: "Login", component: Login, }, ];
let routes: Array<RouteRecordRaw> = [];
const files = require.context("./", false, /\.ts$/); files.keys().forEach((route) => { if (route.startsWith("./index")) { return; } const routerModule = files(route); routes = [...constRoutes, ...(routerModule.default || routerModule)]; });
const router = createRouter({ history: createWebHashHistory(), routes, });
export default router;
|
- 在 src/services 下添加 user.ts(和后台接口交互的用户模块示例)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import http from "@/utils/http"; import { AxiosResponse } from "axios";
export interface ILogin { accessToken: string; message: string; }
const API = { login: "/login", };
export function login( userInfo: Record<string, unknown> ): Promise<AxiosResponse<ILogin>> { return http.get<ILogin>(API.login, { data: userInfo }); }
|
- 在 src/store 下添加 modules 文件夹,并在其中添加 user.ts(作为测试)
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
| import { ILogin, login } from "@/services/user";
const SET_ACCESSTOKEN = "SET_ACCESSTOKEN";
const userState = { accessToken: "", };
const actions = { async login( { commit }: { commit: (mutation: string, arg: string) => void }, userInfo: Record<string, unknown> ): Promise<ILogin> { const { data } = await login(userInfo); commit(SET_ACCESSTOKEN, data.accessToken); return data; }, };
const mutations = { [SET_ACCESSTOKEN](state: { accessToken: string }, accessToken: string): void { state.accessToken = accessToken; }, };
export default { state: userState, actions, mutations, };
|
- 修改 src/store 下 index.ts(让其动态引入 modules 下的文件作为模块)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import { createStore } from "vuex";
interface IModule { [key: string]: { namespaced: boolean }; }
const modules: IModule = {}; const files = require.context("./modules", false, /\.ts$/); files.keys().forEach((key) => { const moduleKey = key.replace(/(\.\/|\.ts)/g, ""); modules[moduleKey] = files(key).default; modules[moduleKey].namespaced = true; });
export default createStore({ state: {}, mutations: {}, actions: {}, modules, });
|
在 src/utils 下添加 http 文件夹,并在其中添加 index.ts 文件(封装 axios)
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
| import axios from "axios";
const http = axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 1000 * 5, });
http.interceptors.request.use( (config) => { if (config.method === "post") { } return config; }, (error) => { console.log(error); return Promise.reject(error); } );
http.interceptors.response.use( (response) => { if (response.status && response.status !== 200) { return Promise.reject(new Error("错误")); } return response; }, (error) => { console.log(error); return Promise.reject(error); } );
export default http;
|
- 删除 src/views 下的 About.vue 和 Home.vue,新建 login.vue 和 home.vue
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
|
<template> <div> <el-button @click="handleLogin">登录</el-button> </div> </template>
<script lang="ts"> import { defineComponent } from 'vue'; import { ElButton } from 'element-plus'; import { useRouter } from '@/hooks/common/use-router'; import { useStore } from 'vuex';
export default defineComponent({ name: 'Login', components: { ElButton }, setup() { const store = useStore(); const { router } = useRouter(); const handleLogin = async () => { const data = await store.dispatch('user/login', { userName: 'zqc', password: '18' }); if (data.accessToken) { router.push('home'); } }; return { handleLogin, }; }, }); </script>
|
改造后的结构
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
| ├── public │ ├── favicon.ico │ ├── index.html │ └── ... ├── src │ ├── assets │ │ ├── fonts │ │ │ └── ... │ │ ├── icons │ │ │ └── ... │ │ ├── images │ │ │ ├── exception │ │ │ │ └── ... │ │ │ ├── module-a │ │ │ │ └── ... │ │ │ └── ... │ │ └── styles │ │ ├── common.scss │ │ ├── style.scss │ │ └── ... │ ├── components │ │ ├── common │ │ │ └── ... │ │ ├── module-a │ │ │ └── ... │ │ └── ... │ ├── hooks │ │ ├── common │ │ │ ├── use-debounce.ts │ │ │ ├── use-router.ts │ │ │ ├── use-throttle.ts │ │ │ └── ... │ │ └── ... │ ├── layouts │ │ └── ... │ ├── plugins │ │ ├── index.ts │ │ └── ... │ ├── router │ │ ├── index.ts │ │ └── ... │ ├── services │ │ ├── module-a .ts │ │ └── ... │ ├── store │ │ ├── modules │ │ │ └── ... │ │ ├── index.ts │ ├── utils │ │ ├── http │ │ │ └── index.ts │ │ └── ... │ ├── views │ │ ├── module-a.vue │ │ │ └── ... │ │ └── ... │ ├── app.vue │ ├── main.ts │ └── shims-vue.d.ts ├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── babel.config.js ├── package.json ├── README.md ├── tsconfig.json └── vue.config.js
|