抛开之前自己捯饬的小项目,工作之后第一次独自承担一个完整的 vue 项目。记录一下所学习到的以及自己沉淀下来的东西。
初始化项目
通常会使用 vue-cli3
脚手架工具进行项目的初始化,完成之后添加自定义的 webpack
配置文件 vue.config.js
// vue.config.js
module.exports = {
// 打包之后不出现 404
publicPath: './',
devServer: {
// 开发端口
port: 3000,
// 请求转发以及重定向路径
proxy: {
'/api': {
target: 'http://localhost:3001/',
pathRewrite: {"^/api": "/"}
}
}
}
};
vue-router
模块化路由配置
对于单页面应用来说,每个路由对应一个页面,随着应用功能的丰富,路由数量也会逐渐增多,因此,一个便于维护的路由配置是至关重要的。
将整个项目按功能划分为多个模块,每个模块内部分别对自身的路由进行管理。例如“用户”模块下,可能有 /#/user
,/#/user/setting
、/#/user/list
等这几个路由,可以将这个模块下的路由(包含了相同的路由前缀 /user
)单独写入一个文件 user.js
进行管理。
// user.js
export default [
{
// 匹配 '/#/user'
path: '',
component: () => import('../views/User/Index.vue')
}, {
// 匹配 '/#/user/setting'
path: 'setting',
component: () => import('../views/User/Setting.vue')
}, {
// 匹配 '/#/user/list'
path: 'list',
component: () => import('../views/User/List.vue')
}
];
// index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import user from './user';
// 避免 router.push 相同路由的错误
const originalPush = VueRouter.prototype.push;
VueRouter.prototype.push = function push(location) {return originalPush.call(this, location).catch(err => err)
};
Vue.use(VueRouter);
const routes = [
{
path: '/user',
component: () => import('../views/CommonWrap.vue'),
children: user
}
];
const router = new VueRouter({routes});
export default router;
index.js
文件的根路由配置中,利用 children
属性引入了 user 模块的路由配置,但与 express
框架的模块化路由写法不同的是,vue-router
的 children
属性中的路由只能渲染父组件中 <router-view />
的那部分。所以需要一个 临时工组件 来作为各模块子路由的出口。
// CommonWrap.vue
// 临时工组件,只需要提供子路由的渲染出口
<template>
<router-view />
</template>
登录状态保持、页面访问权限
单页面应用中如何做到登录状态的保持,不同用户权限对页面的访问权限(后台可以做到数据层面的控制,路由的访问权限则需要前端去做)。
单页应用中登录状态保持和页面访问权限控制的解决办法
vuex
通常还是会进行模块化状态管理
// index.js
import Vue from 'vue';
import Vuex from 'vuex';
import user from './user';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {user}
});
// user.js
export default {
namespaced: true,
state: {},
getters: {}, // (state)
mutations: {}, // (state, payload)
actions: {} // ({ commit})
}
个人感觉 vuex
比 react-redux
用起来简单方便,不用在好几个文件里找来找去……
样式
scoped
添加 scoped
属性可以让样式只在组件内生效,但是会发现有些选择器没有达到预期的效果。例如在使用 el-menu
组件时,直接设置类名 el-submenu__title
的样式没有效果,或者在父组件设置子组件中类名的样式也没法生效。
deep
利用 /deep/
或者 ::v-deep
进行深度选择(在使用 scss
的情况下只能用 ::v-deep
),就不必采用设置全局样式的方式导致类名覆盖了。
flex
实际使用 display: flex
进行布局之后发现,会出现嵌套的情况,并且这种情况是很常见的。预先将 flex
配置写成全局类名,是否是一个解决办法呢?
Element UI
多级目录(递归)
通常会将目录配置成数据的形式,数据的结构可能是下面这样
interface menu {
name: string;
route: string;
icon: string;
children: menu[];}
显然,有 children
属性的目录代表有子目录,它需要作为 el-submenu
组件,而没有这个属性就作为 el-menu-item
组件。对于不定层数的目录结构,需要对自身进行递归。
// MenuTemplate.vue
<template>
<div>
<template v-for="(menu, index) in menus">
<!-- 没有子目录,作为 el-menu-item 组件 -->
<template v-if="!menu.children || menu.children.length == 0">
<el-menu-item :index="menu.name" :key="index">
<span>{{menu.name}}</span>
</el-menu-item>
</template>
<!-- 有子目录 -->
<template v-else>
<el-submenu :index="menu.name" :key="index">
<template slot="title">
<span>{{menu.name}}</span>
</template>
<!-- 递归 children -->
<menu-template :menus="menu.children" />
</el-submenu>
</template>
</template>
</div>
</template>
<script>
export default {
name: 'menuTemplate',
props: {
menus: {
type: Array,
default() {return [];
}
}
}
}
</script>
// Menu.vue
<el-menu>
<menu-template :menus="menusData" />
</el-menu>
El-Datagrid
Element UI 分别提供了表格组件 Table
和 分页组件 Pagination
,管理系统中,表格是使用最多的组件,也都是需要分页功能的,并且前端分页和后端分页的需求都会有,于是便学习 Easy UI 中的方式封装了一个符合大部分业务场景的数据表格组件 Datagrid
。
类似 easyui 中 datagrid 使用习惯的 element-ui 数据表格组件(el-datagrid)
但往往理想是丰满的,而现实是骨感的。随着各种各样不同的功能需要加在数据表格中,例如需要各种各样的权限条件来控制显示与否等等,对于不按套路出牌的情况,想一劳永逸是没那么容易的,还是需要在这个组件上稍作修改……
拓展 Vue.prototype
Axios
目的:
- 统一发送 get/post 请求的参数形式
- 在具体业务逻辑之前处理一些特定的响应状态码
- 提前取一下
response.data
这部分我写的这个比较简单,没有做超时等错误处理……
message 和 confirm
目的:
- 统一某些配置
- 减少、简化业务逻辑中的重复代码
// extend.js
import axios from 'axios';
export default function(obj) {
// 发送 ajax
obj._ajax = function(type, url, params={}) {switch (type) {
case 'get':
case 'delete':
return axios[type](url, { params}).then(res => {
// 根据后端的不同返回值提前处理错误,正常情况则返回数据
return res.data;
}).catch(err => err);
case 'post':
case 'put':
return axios[type](url, params).then(res => {
// 根据后端的不同返回值提前处理错误,正常情况则返回数据
return res.data;
}).catch(err => err);
}
}
// 成功消息。info、warning、error 等消息类型写法类似
obj._success = function(message) {
this.$message({
type: 'success',
message,
duration: 1500
});
};
// confirm 确认消息
obj._confirm = function(message, callback) {
this.$confirm(message, '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
dangerouslyUseHTMLString: true
}).then(callback).catch(() => {});
}
}
把增加在 obj
中的方法赋给 Vue.prototype,这样就能在组件中直接以 this[METHOD]
的形式使用。
// main.js
import Vue from 'vue';
import extend from './utils/extend';
extend(Vue.prototype);
// Test.vue
<script>
export default {data() {
return {id: 1}
},
methods: {async getDataById() {const res = this._ajax('delete', '/api/delete', { id: this.id});
if (res.status == 200) {this._success('操作成功');
// ……
}
},
showConfirm() {this._confirm('确认删除 *** 吗?', this.getDataById);
}
}
}
</script>
自动化部署 node.js 应用
对于单页面应用而言,打包之后就是一些静态内容,比较常规的做法是将这些静态文件部署到后端应用的指定位置,但最终目的也只是通过 一个指定的请求返回一个静态的 html
文件。那么如果有一个 node.js 应用能够达到同样的目的就可以实现前后端分开部署了。
开启服务的端口
// start.js
const express = require('express');
const app = express();
const path = require('path');
app.use(express.static(path.join(__dirname, 'dist')));
app.get('/', (req, res) => {res.sendFile( __dirname + '/dist/index.html');
});
app.listen(3000);
关闭服务的端口
这是一段可以在 windows
和 linux
中运行的 node.js
关闭指定端口的代码。
// stop.js
var port = '3000';
var exec = require('child_process').exec;
if (process.platform == 'win32') {exec('netstat -ano | findstr' + port, (err, stdout, stderr) => {if (err) {return;}
const line = stdout.split('\n')[0].trim().split(/\s+/);
const pid = line[4];
exec('taskkill /F /pid' + pid, (err, stdout, stderr) => {if (err) {return;}
console.log('占用指定端口的程序被成功杀掉!');
});
});
} else {exec('netstat -anp | grep' + port, (err, stdout, stderr) => {if (err) {return;}
const line = stdout.split('\n')[0].trim().split(/\s+/);
const pid = line[6].split('/')[0];
exec('kill -9' + pid, (err, stdout, stderr) => {if (err) {return;}
console.log('占用指定端口的程序被成功杀掉!');
});
});
}
开发环境、测试环境、正式环境
开发环境、测试环境、正式环境最基本的差异就是请求路径的不同。为了确保各环境对应请求路径的准确性,必然要通过代码来控制,vue-cli
刚好也提供了这么一套机制。
通过配置 package.json
、.env.***
文件,通过环境变量判断当前应该选择的请求路径。
// package.json
{
"scripts": {
"build-test": "vue-cli-service build --mode test",
"build-online": "vue-cli-service build --mode online"
}
}
// .env.test
NODE_ENV = 'production';
VUE_APP_ENV = 'test';
// .env.online
NODE_ENV = 'production';
VUE_APP_ENV = 'online';
// setting.js
let BASE_URL = '';
if (process.env.NODE_ENV == 'production') {if (process.env.VUE_APP_ENV == 'online') {BASE_URL = 'https://online.xxx.com';} else {BASE_URL = 'https://test.xxx.com';}
} else {BASE_URL = 'http://localhost:3000'}
服务器打包 VS 本地打包
与其他服务端语音一样,node.js 应用需要的也是一个打包之后的静态文件夹。利用 vue-cli
脚手架构建的项目,通常会利用其集成好的命令进行打包输出。
服务器打包
直接将源代码发布到服务器,安装好 package.json
中的依赖之后运行 npm run build-online
进行打包,这看起来是最简单直接的方法。我使用的自动化构建平台需要将 构建机器 上面的内容传输到 发布机器 上之后,再执行 npm run start
监听服务的指定端口。缺点是速度较慢,npm install
的耗时不稳定,node_modules 文件夹的传输很慢……
本地打包
服务器打包的优势是操作简单,但消耗的时间比较久,但对于 node.js 应用来说,有哪些是最少需要的依赖呢?仔细想想,node.js 应用只需要监听一个服务端口以及相应的静态文件就可以了,vue-cli
等开发依赖,vue
、element-ui
等运行依赖对于只作为静态文件服务的 nodejs 应用来说,其实都不是必须的。
在发布到测试(正式)环境前,先在本地运行 npm run build-test
(npm run build-online
),因为不需要进行 npm install
和传输 node_modules 文件夹,所以这个时间是稳定可控的。最后将包含静态文件的 dist 文件夹一并推送到远端,就可以在服务器上面直接运行 npm run start
开启服务了。
Vue 的使用总结
在这里总结一下开发 vue 中用到的特性以及对这些特性的理解,由于涉及到的业务不复杂,有很多特性其实还没有用到……
v-for、v-if
很多场景下,在列表渲染的组件中需要判断每一项的某些属性的值来决定是否显示或者是否有某个不一样效果,官方教程不推荐在同一个组件中同时使用 v-for
和 v-if
,我通常会在外层增加一个 <template>
使用 v-for
,将 v-if
和 key
值写在实际需要渲染的组件中。
// Test.vue
<template>
<div>
<template v-for="(item, index) in datas">
<span v-if="item.name" :key="index">
{{item.name}}
</span>
<p v-else> 暂无数据 </p>
</template>
</div>
</template>
data、computed、watch
watch
用来监听变量的改变,这个变量可以是 data
中的属性,也可以是 computed
中的属性。除了最直接的将需要监听的属性赋值为一个函数的用法,完整的用法包括以下三个属性
watch: {
someData: {handle(newVal, oldVal) {// 监听变量变化的处理函数},
deep: true, // 是否深度监听,例如对象某些属性的变化
immediate: true // 是否在第一次赋值时执行
}
}
data
中的变量一般是通过重新赋值实现响应式效果,而 computed
中的变量只会在初始化的时候写好处理逻辑,之后的响应式效果不需要再操作这些变量。例如这个场景,根据路由中的参数的改变获取不同的数据,有以下两种实现方式
// 变量在 data 中
data() {
return {id: this.$route.params.id}
},
methods: {getData() {console.log(this.id)
}
},
watch: {
id: {handler() {this.getData();
},
immediate: true
},
'$route.params.id': {handler() {this.id = this.$route.params.id;}
}
}
// 变量在 computed 中
computed: {id() {return this.$route.params.id;}
},
methods: {getData() {console.log(this.id)
}
},
watch: {
id: {handler() {this.getData();
},
immediate: true
}
}
将路由的参数存储在 data
中,就必须额外监听路由参数的改变,然后再手动的修改 data
中的值;而将路由参数存储在 conmputed
中,这个变量就会随着路由的改变而改变,不需要再显式的重新赋值。
消息传递
vue 的组件之间消息传递方式很多,这里只列举在开发过程中常用、能满足大部分场景的几种方式。
props
最常用的父组件给子组件传递方法,子组件不能直接修改这个数据,但可以在子组件的 data
中 深拷贝 一份之后再修改。
$emit
子组件向父组件传递一个包含数据的事件,父组件监听这个事件名然后做出相应的处理。
$refs
多用来父组件通知子组件做出一些操作,通过 $refs
获取到子组件的实例,调用实例上的方法。
event bus
利用一个临时组件,分别在两个需要通信的组件中进行 $on
和 $emit
的操作。
vuex
这是终极解决方案。