Vue3.js模板项目创建

Vue3.js模板项目创建

基础环境

  • 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      // @vue/cli 5.0.0-alpha.2

node --v // v15.3.0

yarn -v // 1.22.10

如发现版本不满足要求,可以分别通过:

  • 运行以下命令行,更新 Vue CLI 至最新版本

    1
    npm i -g @vue/cli@v5.0.0-alpha.2
  • 前往 Node.js 下载最新版本的程序,并安装。

  • 运行以下命令行,更新 yarn 至最新版本

    1
    npm i -g yarn

项目创建

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                    // 静态资源 该文件夹下的内容在构建时会直接拷贝到dist文件夹下
│ ├── favicon.ico // 网站图标
│ └── index.html // HTML模板页
├── src // 主要工作目录
│ ├── assets // 静态资源 会被webpack打包处理
│ │ └── logo.png
│ ├── components // 组件(dumb components,获取props,派发事件)
│ │ └── HelloWorld.vue // 示例组件
│ ├── router // 路由(统一使用懒加载)
│ │ └── index.ts // 组装各路由并导出
│ ├── store // 状态管理(可选)
│ │ └── index.ts
│ ├── views // 页面(smart components,可以访问store,路由,window)
│ │ ├── About.vue // 关于
│ │ └── Home.vue // 首页
│ ├── App.vue // 根组件
│ ├── main.ts // 入口文件(引入全局的样式和脚本,可安装插件、注册组件或指令等)
│ └── shims-vue.d.ts // 帮助IDE识别 .vue文件
├── .browserslistrc // 目标浏览器配置
├── .editorconfig // 代码风格规范
├── .eslintrc.js // eslint配置
├── .gitignore // git提交忽略文件
├── babel.config.js // babel配置
├── package.json // 项目依赖、脚本
├── README.md // 项目命令行说明
└── tsconfig.json // TypeScript配置文件

内容改造

安装依赖

axios

axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。

1
yarn add axios

Normalize.css

Normalize.css 它使不同浏览器能更一致地呈现所有元素,并符合现代标准。

1
yarn add normalize.css

Element Plus

Element Plus,是为一套基于 Vue 3.0 的桌面端组件库。

1
2
yarn add element-plus
yarn add babel-plugin-component -D // 为了按需打包

修改文件

按照名称顺序,由上到下,由外到内。

  • 修改 .editorconfig 中最后一行(现在屏幕都比较宽,100 个字符确实满足不了需求)
1
max_line_length = 100; // 改为 max_line_length = 160
  • 修改 .eslintrc.js 中的 rules (打包时配置 将 console 和 debug 全部删除,不需要做这个提示)
1
2
3
4
5
6
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',    // 修改为 'no-console': 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', // 修改为 'no-debugger': 'off',

'import/prefer-default-export': 'off', // Composition Function 不一定需要默认导出
'max-len': ['error', { code: 160 }], // 单行最大160个字符
'multiline-comment-style': ['error', 'starred-block'], // 格式化注释
  • 修改 babel.config.js
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", // 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
/**
* 判断是否是生产环境
* @returns {boolean} 是否是生产环境
*/
function isProd() {
return process.env.NODE_ENV === "production";
}

// 配置请求的基本API,当前开发模式配置的是淘宝的测试地址
process.env.VUE_APP_BASE_API = isProd()
? ""
: "http://rap2api.taobao.org/app/mock/115307/user";

module.exports = {
publicPath: isProd() ? "./" : "/", // 部署到生产环境时,按需修改前项为项目名称
productionSourceMap: false, // 不需要生产环境的 source map,减少构建时间

configureWebpack: (config) => {
if (isProd()) {
// 去除 console
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>
  • 修改 src 下的 main.ts
1
2
3
4
5
6
7
8
9
10
11
12
import { createApp } from "vue";

import "normalize.css"; // CSS reset的替代方案
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%;
}

/** ****************** 修改type=number的样式 ****************** */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
}

input[type="number"] {
-moz-appearance: textfield;
}
/** ******************************************************** */

/* 修改谷歌浏览器记住密码后input默认样式 */
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';

/**
* hooks使用示例组件
*/
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
// use-debounce.ts

/**
* 防抖 在事件被触发一定时间后再执行回调,如果在这段事件内又被触发,则重新计时
* 使用场景:
* 1、搜索框中,用户在不断输入值时,用防抖来节约请求资源
* 2、点击按钮时,用户误点击多次,用防抖来让其只触发一次
* 3、window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
* @param fn 回调
* @param duration 时间间隔的阈值(单位:ms) 默认1000ms
*/
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
// use-throttle.ts

/**
* 节流 规定在一段时间内,只能触发一次函数。如果这段时间内触发多次函数,只有一次生效
* 使用场景:
* 1、鼠标不断点击触发,mousedown(单位时间内只触发一次)
* 2、监听滚动事件,比如是否滑到底部自动加载更多
* @param fn 回调
* @param duration 时间间隔的阈值(单位:ms) 默认500ms
*/
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
// use-router.ts

import { reactive, toRefs, watch, getCurrentInstance, Ref } from "vue";

import { Router } from "vue-router";

/**
* 获取路由
* @returns 当前路由以及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> = [];

// 自动添加router目录下的所有ts路由模块
const files = require.context("./", false, /\.ts$/);
files.keys().forEach((route) => {
// 如果是根目录的 index.js、 不做任何处理
if (route.startsWith("./index")) {
return;
}
const routerModule = files(route);
// 兼容 import export 和 require module.export 两种规范 ES modules commonjs
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;
}

// 添加API地址
const API = {
login: "/login",
};

/**
* 登录
* @param userInfo 用户信息
* @returns 验证结果
*/
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";

// 用常量替代 mutation 事件类型,当前模块所有mutation一目了然
const SET_ACCESSTOKEN = "SET_ACCESSTOKEN";

// state
const userState = {
accessToken: "",
};

// getters

// actions
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;
},
};

// mutations
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 };
}

// 自动添加mudules下的所有ts模块
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; // 让 mutations、getters、actions 也按照模块划分
});

// 无需使用模块或者是一些通用的状态写在下方
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, // url = base url + request url
// withCredentials: true, // 如跨域请求时要带上cookie,则设置为true
timeout: 1000 * 5, // 请求超时时长 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) => {
// 如果返回的状态不是200 就报错 按需修改
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
// login.vue

<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' }); // 派发事件,调用actions
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                    // 静态资源 该文件夹下的内容在构建时会直接拷贝到dist文件夹下
│ ├── favicon.ico // 网站图标
│ ├── index.html // HTML模板页
│ └── ...
├── src // 主要工作目录
│ ├── assets // 静态资源 会被webpack打包处理
│ │ ├── fonts // 字体文件(可选)
│ │ │ └── ...
│ │ ├── icons // 图标(可选)
│ │ │ └── ...
│ │ ├── images // 图片(可选)
│ │ │ ├── exception // exception(通用异常页面)模块使用到的图片
│ │ │ │ └── ...
│ │ │ ├── module-a // 此处要用模块命名(可选)
│ │ │ │ └── ... // 该模块下使用到的图片
│ │ │ └── ... // 通用的图片(小项目就不用分文件夹了)
│ │ └── styles // 样式
│ │ ├── common.scss // 常用样式(提供通用的)
│ │ ├── style.scss // 全局样式,组装各样式并导出最终被 main.js 引入
│ │ └── ...
│ ├── components // 组件(dumb components,获取props,派发事件)
│ │ ├── common // 不同项目中的通用组件(可选)
│ │ │ └── ...
│ │ ├── module-a // 此处要用模块命名(可选)
│ │ │ └── ... // 该模块下的组件
│ │ └── ... // 当前项目中的通用组件
│ ├── hooks // 钩子
│ │ ├── common // 不同项目中的通用hooks
│ │ │ ├── use-debounce.ts // 防抖hook
│ │ │ ├── use-router.ts // 路由hook
│ │ │ ├── use-throttle.ts // 节流hook
│ │ │ └── ...
│ │ └── ... // 本项目中通用的hooks
│ ├── layouts // 布局(可选)
│ │ └── ...
│ ├── plugins // vue插件(如:Element,vuetify,antd)(可选)
│ │ ├── index.ts // 组装各插件并导出
│ │ └── ...
│ ├── router // 路由(除必然要加载的以外,统一使用懒加载)
│ │ ├── index.ts // 组装各路由并导出
│ │ └── ...
│ ├── services // 接口请求
│ │ ├── module-a .ts // 各业务模块所有包含的请求和数据处理,此处要用模块命名
│ │ └── ...
│ ├── store // 状态管理(可选)
│ │ ├── modules // 各模块
│ │ │ └── ... // 尽量和views中的模块对应上
│ │ ├── index.ts // 组装模块并导出
│ ├── utils // 工具类
│ │ ├── http // aixos封装
│ │ │ └── index.ts
│ │ └── ...
│ ├── views // 页面(smart components,可以访问store,路由,window)
│ │ ├── module-a.vue // 用模块命名,如该模块下页面较多,可建以模块为名称的文件夹,在其中创建多个页面
│ │ │ └── ...
│ │ └── ...
│ ├── app.vue // 根组件
│ ├── main.ts // 入口文件(引入全局的样式和脚本,可安装插件、注册组件或指令等)
│ └── shims-vue.d.ts // 帮助IDE识别 .vue文件
├── .browserslistrc // 目标浏览器配置
├── .editorconfig // 代码风格规范
├── .eslintrc.js // eslint配置
├── .gitignore // git提交忽略文件
├── babel.config.js // babel配置
├── package.json // 项目依赖、脚本
├── README.md // 项目命令行说明
├── tsconfig.json // TypeScript配置文件
└── vue.config.js // 自定义webpack配置
作者

天下布武

发布于

2020-09-14

更新于

2023-08-05

许可协议

CC BY-NC-SA 4.0

评论