vue做为前端主流的3大框架之一,目前在国内有着很是普遍的应用,因为其轻量和自底向上的渐进式设计思想,使其不只仅被应用于PC系统,对于移动端,桌面软件(electronjs)等也有普遍的应用,与此诞生的优秀的开源框架好比elementUI,iView, ant-design-vue等也极大的下降了开发者的开发成本,并极大的提升了开发效率。笔者最初接触vue时也是使用的iview框架,亲生体会以后确实很是易用且强大。javascript
笔者曾经有两年的vue项目经验,基于vue作过移动端项目和PC端的ERP系统,虽然平时工做中采用的是react技术栈,但平时仍是会积累不少vue相关的最佳实践和作一些基于vue的开源项目,因此说总结vue的项目经验我以为是最好的成长,也但愿给今年想接触vue框架或想从事vue工做的朋友带来一些经验和思考。css
本文不只仅是总结一些vue使用踩过的一些坑和项目经验,更多的是使用框架(vue/react)过程当中的方法论和组件的设计思路,最后还会有一些我的对工程化的一些总结,但愿有更多经验的朋友们能够一块儿交流,探索vue的奥妙。html
在开始文章以前,笔者建议你们对javascript, css, html基础有必定的了解,由于会用框架不必定能很好的实现业务需求和功能,要想实现不一样场景下不一样复杂度的需求,必定要对web基础有充足的了解,因此但愿你们熟悉以下基础知识,若是不太熟悉能够花时间研究了解一下。前端
javascript:vue
css:java
html:node
因此但愿你们掌握好以上基础知识,也是前端开发的基础,接下来咱们直接进入正文。react
vue学习最快的方式就是实践,根据官网多写几个例子是掌握vue最快的方式。 接下来笔者就来总结一下在开发vue项目中的一些实践经验。jquery
根据以上介绍,页面第一次加载时会执行 beforeCreate, created, beforeMount, mounted这四个生命周期,因此咱们通常在created阶段处理http请求获取数据或者对data作必定的处理, 咱们会在mounted阶段操做dom,好比使用jquery,或这其余第三方dom库。其次,根据以上不一样周期下数据和页面状态的不一样,咱们还能够作其余更多操做,因此说每一个生命周期的发展状态很是重要,必定要理解,这样才能对vue有更多的控制权。webpack
指令 (Directives) 是带有 v- 前缀的特殊属性,vue经常使用的指令有:
以上是比较经常使用的指令,具体用法就不一一举例了,其中v-cloak主要是用来避免页面加载时出现闪烁的问题,能够结合css的[v-cloak] { display: none }方式解决这一问题。关于指令的动态参数,使用也很简单,虽然是2.6.0 新增的,可是方法很灵活,具体使用以下:
<a v-on:[eventName]="doSomething"> ... </a>
复制代码
咱们能够根据具体状况动态切换事件名,从而绑定统一个函数。
<Tag @click.native="handleClick">ok</Tag>
复制代码
用法以下:
<input type="text" v-model.trim="value">
复制代码
还有不少修饰符好比键盘,鼠标等修饰符,感兴趣的你们能够自行学习研究。
组件之间的通讯方案:
父子组件:
组件的按需加载是项目性能优化的一个环节,也能够下降首屏渲染时间,笔者在项目中用到的组件按需加载的方式以下:
<template>
<div>
<ComponentA />
<ComponentB />
</div>
</template>
<script> const ComponentA = () => import('./ComponentA') const ComponentB = () => import('./ComponentB') export default { // ... components: { ComponentA, ComponentB }, // ... } </script>
复制代码
<template>
<div>
<ComponentA />
</div>
</template>
<script> const ComponentA = resolve => require(['./ComponentA'], resolve) export default { // ... components: { ComponentA }, // ... } </script>
复制代码
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的全部组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
vuex的基本工做模式以下图所示:
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})
// 访问getters里的属性
this.$store.getters.doneTodos
复制代码
const store = new Vuex.Store({
state: {
num: 1
},
mutations: {
add (state) {
// 变动状态
state.num++
}
}
})
// 在项目中使用mutation
store.commit('add')
// 添加额外参数
store.commit('add', 10)
复制代码
const store = new Vuex.Store({
state: {
num: 0
},
mutations: {
add (state) {
state.num++
}
},
actions: {
add (context) {
context.commit('add')
},
asyncAdd ({ commit }) {
setTimeout(() => {
commit('add')
}
}
})
// 分发action
store.dispatch('add')
// 异步action
store.dispatch('asyncAdd')
// 异步传参
store.dispatch('asyncAdd', { num: 10 })
复制代码
笔者更具实际经验总结了一套标准使用模式,就拿笔者以前的开源XPXMS举例,以下:
// type.ts
// 用来定义state等的类型文件
export interface State {
name: string;
isLogin: boolean;
config: Config;
[propName: string]: any; // 用来定义可选的额外属性
}
export interface Config {
header: HeaderType,
banner: Banner,
bannerSider: BannerSider,
supportPay: SupportPay
}
export interface Response {
[propName: string]: any;
}
// state.ts
// 定义全局状态
import { State } from './type'
export const state: State = {
name: '',
isLogin: false,
curScreen: '0', // 0为pc, 1为移动
config: {
header: {
columns: ['首页', '产品', '技术', '运营', '商业'],
height: '50',
backgroundColor: '#000000',
logo: ''
}
},
// ...
articleDetail: null
};
// mutation.ts
import {
State,
Config,
HeaderType,
Banner,
BannerSider,
SupportPay
} from './type'
export default {
// 预览模式
setScreen(state: State, payload: string) {
state.curScreen = payload;
},
// 删除banner图
delBanner(state: State, payload: number) {
state.config.banner.bannerList.splice(payload, 1);
},
// 添加banner图
addBanner(state: State, payload: object) {
state.config.banner.bannerList.push(payload);
},
// ...
};
// action.ts
import {
HeaderType,
Response
} from './type'
import http from '../utils/http'
import { uuid, formatTime } from '../utils/common'
import { message } from 'ant-design-vue'
export default {
/**配置 */
setConfig(context: any, paylod: HeaderType) {
http.get('/config/all').then((res:Response) => {
context.commit('setConfig', res.data)
}).catch((err:any) => {
message.error(err.data)
})
},
/**header */
saveHeader(context: any, paylod: HeaderType) {
http.post('/config/setHeader', paylod).then((res:Response) => {
message.success(res.data)
context.commit('saveHeader', paylod)
}).catch((err:any) => {
message.error(err.data)
})
},
// ...
};
// index.ts
import Vue from 'vue';
import Vuex from 'vuex';
import { state } from './state';
import mutations from './mutation';
import actions from './action';
Vue.use(Vuex);
export default new Vuex.Store({
state,
mutations,
actions
});
// main.ts
// 最后挂载到入口文件的vue实例上
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store/';
import './component-class-hooks';
import './registerServiceWorker';
Vue.config.productionTip = false;
new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#app');
复制代码
咱们在实际项目中均可以使用这种方式组织管理vuex相关的代码。
vue-router使用你们想必不是很陌生,这里直接写一个案例:
// router.ts
import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/admin/Home.vue';
Vue.use(Router);
const router = new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
component: Home,
beforeEnter: (to, from, next) => {
next();
},
children: [
{
// 当 /user/:id/profile 匹配成功,
// UserProfile 会被渲染在 User 的 <router-view> 中
path: '',
name: 'header',
component: () => import(/* webpackChunkName: "header" */ './views/admin/subpage/Header.vue'),
},
{
path: '/banner',
name: 'banner',
component: () => import(/* webpackChunkName: "banner" */ './views/admin/subpage/Banner.vue'),
},
{
path: '/admin',
name: 'admin',
component: () => import(/* webpackChunkName: "admin" */ './views/admin/Admin.vue'),
},
],
},
{
path: '/login',
name: 'login',
component: () => import(/* webpackChunkName: "login" */ './views/Login.vue'),
meta:{
keepAlive:false //不须要被缓存的组件
}
},
{
path: '*',
name: '404',
component: () => import(/* webpackChunkName: "404" */ './views/404.vue'),
},
],
});
// 路由导航钩子的用法
router.beforeEach((to, from, next) => {
if(from.path.indexOf('/preview') < 0) {
sessionStorage.setItem('prevToPreviewPath', from.path);
}
next();
})
export default router
复制代码
以上案例是很典型的静态路由配置和导航钩子的用法(如何加载路由组件,动态加载路由组件,404页面路由配置,路由导航钩子使用)。若是在作后台系统,每每会涉及到权限系统,因此通常会采用动态配置路由,经过先后端约定的路由方式,路由配置文件更具不一样用户的权限由后端处理后返。因为设计细节比较繁琐,涉及到先后端协定,因此这里只讲思路就行了。
受现代 JavaScript 的限制,Vue 没法检测到对象属性的添加或删除。因为 Vue 会在初始化实例时对属性执行 getter/setter 转化,因此属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。还有一种状况是,vue没法检测到data属性值为数组或对象的修改,因此咱们须要用原对象与要混合进去的对象的属性一块儿建立一个新的对象。可使用this.$set或者对象的深拷贝,若是是数组则可使用splice,扩展运算符等方法来更新。
keep-alive是Vue的内置组件,能在组件切换过程当中将状态保留在内存中,防止重复渲染DOM。咱们可使用如下方式设置某些页面是否被缓存:
// routes 配置
export default [
{
path: '/A',
name: 'A',
component: A,
meta: {
keepAlive: true // 须要被缓存
}
}, {
path: '/B',
name: 'B',
component: B,
meta: {
keepAlive: false // 不须要被缓存
}
}
]
复制代码
路由视图配置:
// 路由设置
<keep-alive>
<router-view v-if="$route.meta.keepAlive">
<!-- 会被缓存的视图组件-->
</router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive">
<!-- 不须要缓存的视图组件-->
</router-view>
复制代码
<template>
<div id="app"> <keep-alive> <router-view :key="key" /> </keep-alive> </div> </template> <script lang="ts"> import { Vue } from 'vue-property-decorator'; import Component from 'vue-class-component'; @Component export default class App extends Vue { get key() { // 缓存除预览和登录页面以外的其余页面 console.log(this.$route.path) if(this.$route.path.indexOf('/preview') > -1) { return '0' }else if(this.$route.path === '/login') { return '1' }else { return '2' } } } </script> 复制代码
总结一下笔者在vue项目中的经常使用的工具函数。
/** * 识别ie--浅识别 */
export const isIe = () => {
let explorer = window.navigator.userAgent;
//判断是否为IE浏览器
if (explorer.indexOf("MSIE") >= 0) {
return true;
}else {
return false
}
}
复制代码
/** * 颜色转换16进制转rgba * @param {String} hex * @param {Number} opacity */
export function hex2Rgba(hex, opacity) {
if(!hex) hex = "#2c4dae";
return "rgba(" + parseInt("0x" + hex.slice(1, 3)) + "," + parseInt("0x" + hex.slice(3, 5)) + "," + parseInt("0x" + hex.slice(5, 7)) + "," + (opacity || "1") + ")";
}
复制代码
// 去除html标签
export const htmlSafeStr = (str) => {
return str.replace(/<[^>]+>/g, "")
}
复制代码
/* 获取url参数 */
export const getQueryString = () => {
let qs = location.href.split('?')[1] || '',
args = {},
items = qs.length ? qs.split("&") : [];
items.forEach((item,i) => {
let arr = item.split('='),
name = decodeURIComponent(arr[0]),
value = decodeURIComponent(arr[1]);
name.length && (args[name] = value)
})
return args;
}
复制代码
/* 解析url参数 */
export const paramsToStringify = (params) => {
if(params){
let query = [];
for(let key in params){
query.push(`${key}=${params[key]}`)
}
return `${query.join('&')}`
}else{
return ''
}
}
复制代码
export const toArray = (data) => {
return Array.isArray(data) ? data : [data]
}
复制代码
/** * 带参数跳转url(hash模式) * @param {String} url * @param {Object} params */
export const toPage = (url, params) => {
if(params){
let query = [];
for(let key in params){
query.push(`${key}=${params[key]}`)
}
window.location.href = `./index.html#/${url}?${query.join('&')}`;
}else{
window.location.href = `./index.html#/${url}`;
}
}
复制代码
/** * 指定字符串 溢出显示省略号 * @param {String} str * @param {Number} num */
export const getSubStringSum = (str = "", num = 1) => {
let newStr;
if(str){
str = str + '';
if (str.trim().length > num ) {
newStr = str.trim().substring(0, num) + "...";
} else {
newStr = str.trim();
}
}else{
newStr = ''
}
return newStr;
}
复制代码
/** * 生成uuid * @param {number} len 生成指定长度的uuid * @param {number} radix uuid进制数 */
export function uuid(len, radix) {
let chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
let uuid = [], i;
radix = radix || chars.length;
if (len) {
for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix];
} else {
let r;
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
for (i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | Math.random()*16;
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
}
}
}
return uuid.join('');
}
复制代码
/** * 生成指定格式的时间 * @param {*} timeStemp 时间戳 * @param {*} flag 格式符号 */
export function formatTime(timeStemp, flag) {
let time = new Date(timeStemp);
let timeArr = [time.getFullYear(), time.getMonth() + 1, time.getDate()];
return timeArr.join(flag || '/')
}
复制代码
这个主要是对axios的理解,你们能够学习axios官方文档,这里给出一个二次封装的模版:
import axios from 'axios'
import qs from 'qs'
// 请求拦截
axios.interceptors.request.use(config => {
// 此处能够封装一些加载状态
return config
}, error => {
return Promise.reject(error)
})
// 响应拦截
axios.interceptors.response.use(response => {
return response
}, error => {
return Promise.resolve(error.response)
})
function checkStatus (response) {
// 此处能够封装一些加载状态
// 若是http状态码正常,则直接返回数据
if(response) {
if (response.status === 200 || response.status === 304) {
return response.data
// 若是不须要除了data以外的数据,能够直接 return response.data
} else if (response.status === 401) {
location.href = '/login';
} else {
throw response.data
}
} else {
throw {data:'网络错误'}
}
}
// axios默认参数配置
axios.defaults.baseURL = '/api/v0';
axios.defaults.timeout = 10000;
// restful API封装
export default {
post (url, data) {
return axios({
method: 'post',
url,
data: qs.stringify(data),
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
}
}).then(
(res) => {
return checkStatus(res)
}
)
},
get (url, params) {
return axios({
method: 'get',
url,
params, // get 请求时带的参数
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
}).then(
(res) => {
return checkStatus(res)
}
)
},
del (url, params) {
return axios({
method: 'delete',
url,
params, // get 请求时带的参数
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
}).then(
(res) => {
return checkStatus(res)
}
)
}
}
复制代码
该模版只是一个大体框架,你们能够细化成业务需求的样子,该案例提供了restful接口方法,好比get/post/delete/put等。
笔者在作vue项目时为了提升开发效率也会直接用第三方插件,下面整理一下经常使用的vue社区组件和库。
更多组件能够在vue插件社区查看。
在讲完vue项目经验以后,为了让你们能独立负责一个项目,咱们还须要知道从0开始搭建项目的步骤,以及经过项目实际状况,本身配置一个符合的项目框架,好比有些公司会采用vue+element+vue+less搭建,有些公司采用vue+iview+vue+sass,或者其余更多的技术栈,因此咱们要有把控能力,咱们须要熟悉webpack或者vue-cli3脚手架的配置,笔者以前有些过详细的webpack和vue-cli3搭建自定义项目的文章,这里因为篇幅有限就不一一举例了。感兴趣的朋友能够参考如下两篇文章:
组件系统是 Vue 的另外一个重要概念,由于它是一种抽象,容许咱们使用小型、独立和一般可复用的组件构建大型应用。几乎任意类型的应用界面均可以抽象为一个组件树。在一个大型应用中,有必要将整个应用程序划分为组件,以使开发更易管理。
对于一个基础组件来讲,咱们该如何下手去设计呢?首先笔者以为应该先从需求和功能入手,先划分好组件的功能边界,组件能作什么,理清这些以后再开始写组件。
笔者拿以前在开源社区发布的一个文件上传组件为例子来讲明举例,代码以下:
<template>
<div>
<a-upload :action="action" listType="picture-card" :fileList="fileList" @preview="handlePreview" @change="handleChange" :remove="delFile" :data="data" >
<template v-if="!fileList.length && defaultValue">
<img :src="defaultValue" alt="" style="width: 100%">
</template>
<template v-else>
<div v-if="fileList.length < 2">
<a-icon type="plus" />
<div class="ant-upload-text">上传</div>
</div>
</template>
</a-upload>
<a-modal :visible="previewVisible" :footer="null" @cancel="handleCancel">
<img alt="example" style="width: 100%" :src="previewImage" />
</a-modal>
</div>
</template>
<script lang="ts"> import { Component, Vue, Prop } from 'vue-property-decorator'; @Component export default class Upload extends Vue { @Prop({ default: 'https://www.mocky.io/v2/5cc8019d300000980a055e76' }) action!: string; @Prop() defaultValue: string; @Prop() data: object; @Prop({ default: function() {} }) onFileDel: any; @Prop({ default: function() {} }) onFileChange: any; public previewVisible: boolean = false; public previewImage: string = ''; public fileList: object[] = []; // 预览图片 public handlePreview(file: any) { this.previewImage = file.url || file.thumbUrl; this.previewVisible = true; } // 删除文件和回调 public delFile(file: any) { this.fileList = []; this.onFileDel(); } // 文件上传变化的处理函数 public handleChange({ file }: any) { this.fileList = [file]; if(file.status === 'done') { this.onFileChange(file.response.url); } else if(file.status === 'error') { this.$message.error(file.response.msg) } } // 取消预览 public handleCancel() { this.previewVisible = false; } } </script>
复制代码
以上文件上传预览采用的是ts来实现,但设计思路都是一致的,你们能够参考交流一下。 关于如何设计一个健壮的组件,笔者也写过相关文章,大体思想都好似同样的,能够参考一下:
组件的设计思想和方法与具体框架无关,因此组件设计的核心是方法论,咱们只有在项目中不断总结和抽象,才能对组件设计有更深入的理解。
这里是笔者总结的一套思惟导图:
若是想学习更多H5游戏, webpack,node,gulp,css3,javascript,nodeJS,canvas数据可视化等前端知识和实战,欢迎在公号《趣谈前端》加入咱们一块儿学习讨论,共同探索前端的边界。