基础环境
- Vue CLI 版本为 v5.0.0-beta.1,v5.0 以上支持 webpack 5,终于可以直接使用模块联邦了
- Node.js 版本为 v15.3.0 (官方建议是 10 以上版本,最低为 8.9)
- yarn 版本为 1.22.10 (推荐使用,用NPM也可以)
通过以下命令行查询对应版本号:
vue --version // @vue/cli 5.0.0-beta.1
node --v // v15.3.0
yarn -v // 1.22.10
如发现版本不满足要求,可以分别通过:
-
运行以下命令行,更新 Vue CLI 至最新版本
npm i -g @vue/cli@v5.0.0-beta.1 -
前往 Node.js 下载最新版本的程序,并安装。
-
运行以下命令行,更新 yarn 至最新版本
npm i -g yarn
项目创建
Vue 默认会通过以前选择过的包管理工具 yarn 或 NPM 来安装依赖。想全局修改的话,可在命令行中运行:
vue config --set packageManager yarn // 或 npm 推荐 yarn
也可在创建项目时动态指定当前项目的包管理工具:
vue create vue3-starter -m yarn
勾选以下几项(单击图片可看大图):
依次选择如下内容:
最后会问是否要保存当前这个配置,按自己的意愿选择和命名。
成功后,运行如下命令行:
cd vue3-starter
yarn serve
在浏览器中打开 http://localhost:8080/ 看到页面就算完成了。
项目改造
默认结构
├── 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 中。
yarn add axios
Normalize.css
Normalize.css 它使不同浏览器能更一致地呈现所有元素,并符合现代标准。
yarn add normalize.css
Element Plus
Element Plus,是为一套基于 Vue 3.0 的桌面端组件库。
yarn add element-plus
yarn add babel-plugin-component -D // 为了按需打包
husky
husky,是一个 Git 的 Hook 工具
yarn add husky -D
npx husky add .husky/pre-commit "npm run pre-commit"
lint-staged
lint-staged,对 Git 暂存阶段的内容,执行各种分析的工具
yarn add lint-staged -D
stylelint
stylelint,是对 CSS 进行分析的工具
yarn add -D stylelint stylelint-config-standard
yarn add -D stylelint-config-sass-guidelines stylelint-scss // 对 SASS 进行检查
yarn add -D stylelint-config-rational-order // 自动调整CSS中的属性顺序
commitlint
commitlint,是对提交信息进行 分析的工具
yarn add -D @commitlint/cli @commitlint/config-conventional
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit $1'
修改文件
按照名称顺序,由上到下,由外到内。
- 修改 .editorconfig 中最后一行(现在屏幕都比较宽,100个字符确实满足不了需求)
max_line_length = 100 // 改为 max_line_length = 160
- 修改 .eslintrc.js 中的 rules (打包时配置 将 console 和 debug 全部删除,不需要做这个提示)
'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', // Compositon Funciton 不一定需要默认导出
'max-len': ['error', { code: 160 }], // 添加此项,使得一行最多容纳 160 个字符串
- 修改 package.json,在对应的节点下添加:
"scripts": {
"prepare": "husky install",
"postinstall": "husky install",
"pre-commit": "lint-staged",
},
"lint-staged": {
"*.{js,vue,ts}": [
"npx eslint --fix"
],
"*.{css,scss}": [
"npx stylelint --fix"
]
}
最终的 package.json 为:
{
"name": "vue3-starter",
"version": "0.1.0",
"private": true,
"scripts": {
"prepare": "husky install",
"postinstall": "husky install",
"pre-commit": "lint-staged",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.21.1",
"core-js": "^3.12.1",
"element-plus": "^1.0.2-beta.44",
"normalize.css": "^8.0.1",
"vue": "^3.0.4",
"vue-router": "^4.0.1",
"vuex": "^4.0.0-0"
},
"devDependencies": {
"@commitlint/cli": "^12.1.4",
"@commitlint/config-conventional": "^12.1.4",
"@typescript-eslint/eslint-plugin": "^4.24.0",
"@typescript-eslint/parser": "^4.24.0",
"@vue/cli-plugin-babel": "~5.0.0-beta.1",
"@vue/cli-plugin-eslint": "~5.0.0-beta.1",
"@vue/cli-plugin-router": "~5.0.0-beta.1",
"@vue/cli-plugin-typescript": "~5.0.0-beta.1",
"@vue/cli-plugin-vuex": "~5.0.0-beta.1",
"@vue/cli-service": "~5.0.0-beta.1",
"@vue/compiler-sfc": "^3.0.11",
"@vue/eslint-config-airbnb": "^5.3.0",
"@vue/eslint-config-typescript": "^7.0.0",
"babel-plugin-component": "^1.1.1",
"eslint": "^7.26.0",
"eslint-plugin-import": "^2.23.2",
"eslint-plugin-vue": "^7.9.0",
"husky": "^6.0.0",
"lint-staged": "^11.0.0",
"sass": "^1.32.13",
"sass-loader": "^11.1.1",
"stylelint": "^13.13.1",
"stylelint-config-rational-order": "^0.1.2",
"stylelint-config-sass-guidelines": "^8.0.0",
"stylelint-config-standard": "^22.0.0",
"stylelint-scss": "^3.19.0",
"typescript": "~4.2.4",
"webpack-bundle-analyzer": "^4.4.2"
},
"lint-staged": {
"*.{js,vue,ts}": [
"npx eslint --fix"
],
"*.{css,scss}": [
"npx stylelint --fix"
]
}
}
- 修改 babel.config.js
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
],
plugins: [
[
'component',
{
libraryName: 'element-plus', // Element Plus 按需打包
styleLibraryName: 'theme-chalk',
},
],
],
};
- 添加 vue.config.js(定义自身的 WebPack 参数)
/**
* 判断是否是生产环境
* @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 中的语言(设置为中文后,浏览器不会出现翻译提示)
<html lang=""> // 改为 <html lang="zh">
- 在 src 下添加 hooks(所有钩子函数存放在此),services(请求后台接口的模块存放在此),utils(常用功能)
- 修改 src 下的 App.vue 为 app.vue (所有文件的命名统一使用 kebab-case 命名法),删除大部分内容只保留
<template>
<router-view/>
</template>
- 修改 src 下的 main.ts
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(当前应用全局样式)
// 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;
}
/** ******************************************************** */
// style.scss
@import './common.scss';
- 删除 components 文件夹下 HelloWorld.vue 文件,添加 hooks.vue(添加一个使用 hooks 的例子)
<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
// 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;
}
// 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;
}
// 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 作为一个示例模块的路由
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 一样的路由模块即可)
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(和后台接口交互的用户模块示例)
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(作为测试)
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 下的文件作为模块)
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)
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
// 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>
新增文件
- .stylelintrc.js
module.exports = {
extends: ['stylelint-config-standard', 'stylelint-config-sass-guidelines', 'stylelint-config-rational-order'], // 按照规则对CSS属性进行排序
plugins: ['stylelint-scss'],
rules: {
'selector-no-qualifying-type': [
true,
{
ignore: ['attribute'], // 允许按类型限定属性选择器
},
],
'max-nesting-depth': 3, // 允许的最大嵌套深度为 3
'order/properties-alphabetical-order': null, // 屏蔽属性按字母顺序检查
'selector-class-pattern': null, // 屏蔽类选择器的检查,以确保使用字符 __
'selector-max-compound-selectors': 5, // 允许的最大复合选择器为 5
'font-family-no-missing-generic-family-keyword': null, // 屏蔽没有申明通用字体
},
};
- commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
};
改造后的结构
├── .husky // Git hook
│ ├── _
│ │ └── husky.sh // husky 相关命令
│ ├── .gitignore // 忽略 _ 文件夹
│ ├── commmit-msg // 针对提交消息检查的钩子
│ └── pre-commit // Git 提交前的 钩子
├── 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 // 钩子(本身不应该叫这个名字,确切的说应该叫Compostion Function)
│ │ ├── common // 不同项目中的通用 hooks
│ │ │ ├── use-debounce.ts // 防抖
│ │ │ ├── use-router.ts // 路由
│ │ │ ├── use-throttle.ts // 节流
│ │ │ └── ...
│ │ └── ... // 本项目中通用的 hooks
│ ├── layouts // 布局组件(可选)
│ │ └── ...
│ ├── 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 提交忽略文件
├── .stylelintrc.js // stylelint 配置
├── babel.config.js // Babel 配置
├── commitlint.config.js // commmitlint 配置
├── package.json // 项目依赖、脚本
├── README.md // 项目说明
├── tsconfig.json // TypeScript 配置文件
└── vue.config.js // 自定义 webpack 配置