据上节文章发布已经有了两个星期了。期间收到了 1000+ 个赞,30000+ 阅读量,这是我万万没想到的。本身的文章能有这么高的关注度,真的很使人满意!javascript
可是相反,写文章的压力更加大了。一篇文章老是反反复复的修改,老是担忧本身的认知水平和技术水平不够,甚至致使有些地方会误导读者。css
也会揣摩本身写做风格有没有什么问题、会不会太啰嗦、哪些地方没讲清楚等等...若是有很差的地方能够评论指出来,接受批评,批评也是一种进步的动力!html
原本准备上下两节写彻底部内容,发现实际不太可能,还没写完 4 章,就已经 6000—+ 字了。最后一节写完以后就准备回家过年了,这里提早祝你们新年快乐!前端
常规操做,先点赞后观看哦!你的点赞是我创做的动力之一!vue
这节我将从 5 个方面来论述 vue 开发过程当中的一些技巧和原理。若是你还未观看上节文章,能够移步至16个方面深刻前端工程化开发技巧《上》观看。java
本节内容主要围绕下列问题展开:ios
实践以前:我但愿你有以下准备,或者知识储备。git
- 了解
npm/yarn/git/sass/pug/vue/vuex/vue-router/axios/mock/ssr/jest
的使用和原理。- 固然上面知识不了解也不要紧哈哈哈,文章中会提到大体用法和做用。
vue 编写组件有两种方式,一种是单文件组件,另一种函数组件。根据组件引入和建立还能够分为动态组件和异步组件。github
动态组件
keep-alive
使之缓存。异步组件原理和异步路由同样,使用import()
实现异步加载也就是按需加载。vue-router
所谓 vue 单文件组件,就是咱们最多见的这种形式:
<template lang="pug">
.demo
h1 hello
</template>
<script> export default { name: 'demo', data() { return {} } } </script>
<style lang="scss" scoped> .demo { h1 { color: #f00; } } </style>
复制代码
这里的template
也可使用 render
函数来编写
Vue.component('demo', {
render: function (createElement) {
return createElement(
'h1',
'hello',
// ...
)
}
})
复制代码
咱们能够发现render
函数写模版让咱们更有编程的感受。对模版也能够编程,在vue
里面咱们能够很容易联想到,不少地方都有两种写法,一种是 template
, 一种是js
。
好比:对于路由,咱们既可使用:to=""
,又可使用$router.push
,这也许是 vue 用起来比较爽的缘由。
函数式组件是什么呢?
functional
,这意味它无状态 (没有响应式数据),也没有实例 (没有 this
上下文)
2.5.0+
<template functional>
</template>
复制代码
Vue.component('my-component', {
functional: true,
render function (createElement, context) {
return createElement('div')
}
}
复制代码
为何要使用函数组件呢?
最重要的缘由就是函数组件开销低,也就是对性能有好处,在不须要响应式和this
的状况下,写成函数式组件算是一种优化方案。
组件写好了,须要将组件注册才能使用
组件注册分为两种,一种是全局注册,一种是局部注册
局部注册就是咱们经常使用的 Vue.component('s-button', { /* ... */ })
,比较简单不详细论述
全局注册上节已经提到,在new Vue
以前在 mian.js
注册,这里还提到一种自动全局注册的方法 require.text
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'
const requireComponent = require.context(
'./components',
// 是否查询其子目录
false,
/Base[A-Z]\w+\.(vue|js)$/
)
requireComponent.keys().forEach(fileName => {
// 获取组件配置
const componentConfig = requireComponent(fileName)
const componentName = upperFirst(
camelCase(
// 获取和目录深度无关的文件名
fileName
.split('/')
.pop()
.replace(/\.\w+$/, '')
)
)
// 全局注册组件
Vue.component(
componentName,
componentConfig.default || componentConfig
)
})
复制代码
基本原理和全局注册同样,就是将 components
中的组件文件名,appButton
变成 AppButton
做为注册的组件名。把原来须要手动复制的,变成之间使用 keys
方法批量注册。
如今,咱们以写一个简单的原生button
组件为例,探讨一下组件开发的一些关键点。 写以前,咱们须要抓住 4 个核心的要点:
button
仍是 div
标签button
组件的颜色 color
、形状 type
、大小 size
button
组件的点击事件button
组件的内容这些思考点在其余原生组件开发和高阶组件封装里面也须要考虑到
首先看第一个问题,大部分原生组件第一考虑的地方,就是主要标签用原生<button></button>
标签仍是用<div></div>
去模拟。
为何不考虑
<input/>
呢,由于<button>
元素比<input>
元素更容易添加内部元素。你能够在元素内添加HTML内容(像<em>
、<strong>
甚至<img>
),以及::after
和::before
伪元素来实现复杂的效果,而<input>
只支持文本内容。
下面分析这两种写法的优劣
使用原生button
标签的优点:
buff
,一些自带的键盘事件行为等为何说更好的语义化呢?有人可能会说,可使用
role
来加强div
的语义化。确实能够,可是可能存在问题——有些爬虫并不会根据role
来肯定这个标签的含义。
另一点,对开发者来讲,
<button></button>
比<div role="button"></div>
阅读起来更好。
使用 div
模拟的优点:
button
原生样式带来的一些干扰,少写一些覆盖原生 css
的代码,更干净纯粹。div
,不须要再去找原生标签、深刻了解原生标签的一些兼容相关的诡异问题。div
做为组件主体的缘由。貌似 div 除了语义不是很好之外,其余方面都还行,可是具体用哪种其实均可以,只要代码写的健壮适配性强,基本都没啥大问题。
咱们这里使用原生<button></button>
做为主要标签,使用s-xx
做为class
前缀
为何须要使用前缀,由于在有些时候,好比使用第三方组件,多个组件之间的 class 可能会产生冲突,前缀用来充当组件 css 的一个命名空间,不一样组件之间不会干扰。
<template lang="pug">
button.s-button(:class="xxx" :style="xxx" )
slot
</template>
复制代码
而后,咱们看第二个问题:
如何根据属性来控制 button
的样式 其实这个很简单,基本原理就是:
使用 props
获取父组件传过来的属性。
根据相关属性,生成不一样的class
,使用 :class="{xxx: true, xxx: 's-button--' + size}"
这种形式,在 style
里面对不一样的s-button--xxx
作出不一样的处理。
<template lang="pug">
button.s-button(:class="" :style="" )
slot
</template>
<script> export default { name: 's-button' data: return {} props: { theme: {}, size: {}, type: {} } } </script>
复制代码
如何使用事件以及如何扩展组件
扩展组件的原理,主要就是使用 props
控制组件属性,模版中使用 v-if/v-show
增长组件功,好比增长内部 ICON,使用:style``class
控制组件样式。
type
类型,原生默认是
submit
,这里咱们默认设置为
button
<template lang="pug">
button.s-button(:class="" :style="" :type="nativeType")
slot
s-icon(v-if="icon && $slots.icon" name="loading")
</template>
<script> export default { name: 's-button' data: return {} props: { nativeType: { type: String, default: 'button' }, theme: {}, size: {}, type: {} } } </script>
复制代码
控制事件,直接使用 @click=""
+ emit
<template lang="pug">
button.s-button(@click="handleClick")
</template>
<script> export default { methods: { handleClick (evt) { this.$emit('click', evt) } } } </script>
复制代码
通常就直接使用
template
单文件编写组件,须要加强 js编写能力可使用render()
常规编写组件须要考虑:1. 使用什么标签 2. 如何控制各类属性的表现 3. 如何加强组件扩展性 4. 如何处理组件事件
对响应式
this
要求不高,使用函数functional
组件优化性能。
基础组件一般全局注册,业务组件一般局部注册
使用
keys()
遍历文件来实现自动批量全局注册
使用
import()
异步加载组件提高减小首次加载开销,使用keep-alive + is
缓存组件减小二次加载开销
咱们知道组件中通讯有如下几种方式:
props
传递给子组件,不详细论述emit
事件传递数据给父组件,父组件经过 on
监听,也就是一个典型的订阅-发布模型
@
为v-on:
的简写
<template lang="pug">
<!--子组件-->
div.component-1
<template>
export default {
mounted() {
this.$emit('eventName', params)
}
}
</script>
复制代码
<!-- 父组件-->
<template lang="pug">
Component-1(@eventName="handleEventName")
<template>
<script> export default { methods: { handleEventName (params) { console.log(params) } } } </script>
复制代码
原理很简单其实就是在 emit
与 on
的基础上加了一个事件中转站 “bus”。我以为更像是现实生活中的集线器。
广泛的实现原理大概是这样的 “bus” 为 vue
的一个实例,实例里面能够调用emit
,off
,on
这些方法。
var eventHub = new Vue()
// 发布
eventHub.$emit('add', params)
// 订阅/响应
eventHub.$on('add', params)
// 销毁
eventHub.$off('add', params)
复制代码
可是稍微复杂点的状况,使用这种方式就太繁锁了。仍是使用 vuex 比较好。
从某种意义而言,我以为 vuex 不只仅是它的一种进化版。
store
做为状态管理的仓库,而且引入了状态这个概念bus
模型感受像一个电话中转站与
git
相似,它不能直接修改代码,须要参与者提交commit
,提交完的commit
修改仓库,仓库更新,参与者fetch
代码更新本身的代码。不一样的是代码仓库须要合并,而vuex
是直接覆盖以前的状态。
“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (
state
)。Vuex 和单纯的全局对象有如下两点不一样
dom
)mutation
)
基本用法:就是在 state
里面定义各类属性,页面或组件组件中,直接使用 $store.state
或者$store.getters
来使用。若是想要改变状态state
呢,就commit
一个mutation
可是假如我想提交一连串动做呢?能够定义一个action
,而后使用 $store.dispatch
执行这个 action
使用action
不只能省略很多代码,并且关键是action
中可使用异步相关函数,还能够直接返回一个promise
而为何不直接到mutation
中写异步呢? 由于mutation
必定是个同步,它是惟一能改变 state 的,一旦提交了 mutation
,mutation
必须给定一个明确结果。不然会阻塞状态的变化。
下面给出经常使用 vuex 的使用方式
新建一个store
并将其余各个功能化分文件管理
import Vue from 'vue'
import Vuex from 'vuex'
import state from './states'
import getters from './getters'
import mutations from './mutations'
import actions from './actions'
import user from './modules/user'
Vue.use(Vuex)
export default new Vuex.Store({
//在非生产环境下,使用严格模式
strict: process.env.NODE_ENV !== 'production',
state,
getters,
mutations,
actions,
modules: {
user
}
})
复制代码
操做状态两种方式
console.log(store.state.count)
复制代码
store.commit('xxx')
复制代码
单一状态树, 这也意味着,每一个应用将仅仅包含一个 store 实例。单一状态树让咱们可以直接地定位任一特定的状态片断,在调试的过程当中也能轻易地取得整个当前应用状态的快照。
// states 文件
export default {
count: 0
}
复制代码
计算属性中返回,每当 state
中属性变化的时候, 其余组件都会从新求取计算属性,而且触发更新相关联的 DOM
const Counter = {
template: '<div>{{count}}<div>',
computed: {
count() {
return store.state.count
}
}
}
复制代码
getters
至关于store
的计算属性。不须要每次都要在计算属性中过滤一下,也是一种代码复用。 咱们在getters
文件中管理
export default {
count: (state) => Math.floor(state.count)
}
复制代码
更改 Vuex 的 store 中的状态的惟一方法是提交 mutation。Vuex 中的 mutation 很是相似于事件:每一个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是咱们实际进行状态更改的地方
使用 types 大写用于调试,在mutationTypes
文件中export const ROUTE_ADD = 'ROUTE_ADD'
而后在mutations
文件中管理
import * as MutationTypes from './mutationTypes'
export default {
[MutationTypes.ADDONE]: function(state) {
state.count = state.count + 1
},
//...
}
复制代码
this.$store.commit(MutationTypes.ADDONE)
复制代码
和 mutations
相似,actions
对应的是dispatch
,不一样的是action
可使用异步函数,有种更高一级的封装。
// 简化
actions: {
increment ({ commit }) {
setTimeout(() => {
commit(MutationTypes.ADDONE)
}, 1000)
}
}
// 触发
store.dispatch('increment')
复制代码
上述用法均可以使用载荷的形式,引入也可使用
mapXxxx
进行批量引入,这里不详细论述,有兴趣能够查看官网。
状态太多太杂,分模块管理是一个良好的代码组织方式。
import count from './modules/count'
export default new Vuex.Store({
modules: {
count
}
})
复制代码
每个模块均可以有独立的相关属性
import * as ActionTypes from './../actionTypes'
export default {
state: {
count: 0
},
mutations: {
ADD_ONE: function(state) {
state.count = state.count + 1
}
},
actions: {
[ActionTypes.INIT_INTENT_ORDER]: function({ commit }) {
commit('ADD_ONE')
}
},
getters: {
pageBackToLoan: (state) => Math.floor(state.count)
}
}
复制代码
vuex
主要有几个应用场景,一个是用于组件通讯和状态共享,一个是用于数据缓存,还有就是用于减小请求。这些场景归根节点都是对于缓存和共享来讲的。
首先,状态统一管理在仓库,就实现了组件通讯的可能性。
当一个组件经过 commit
提交 mutation
就改了 state
,其余组件就能够经过store.state
获取最新的state
,这样一来就至关于更新的值经过 store
传递给了其余组件,不只实现了一对一的通讯,还实现了一对多的通讯。
咱们常用的一个场景就是权限管理。
写权限管理时候,首次进入页面就要将权限所有拿到,而后须要分发给各个页面使用,来控制各个路由、按钮的权限。
/** * 判断用户有没有权限访问页面 */
function hasPermission(routeItem, menus) {
return menus.some(menuItem => {
return menuItem.name === routeItem.name
})
}
/** * 递归过滤异步路由表,返回符合用户角色权限的路由表 * @param {*} routes 路由表 * @param {*} menus 菜单信息 */
function filterRoutes(routes, menus) {
return routes.filter(routeItem => {
if (routeItem.hidden) {
return true
} else if (hasPermission(routeItem, menus)) {
const menuItem = menus.find(item => item.name === routeItem.name)
if (menuItem && menuItem.children && menuItem.children.length > 0) {
routeItem.children = filterRoutes(routeItem.children, menuItem.children)
if (!routeItem.children.length) return false
}
return true
} else {
return false
}
})
}
const permission = {
state: {
routers: constantRouterMap,
addRouters: [],
roles: [],
user_name: '',
avatar_url: '',
onlyEditor: false,
menus: null,
personal: true,
teamList: []
},
mutations: {}
}
export default permission
复制代码
并且权限还能够被更改,更改后的权限直接分发到其余页面组件中。这个场景要是不使用 vuex
,代码将会比较复杂。
store
是一个仓库,它从建立开始就一直存在,只有页面 Vuex.store
实例被销毁,state 才会被清空。具体表现就是刷新页面。
这个数据缓存适用于:页面加载后缓存数据,刷新页面请求数据的场景。在通常Hybrid
中,通常不存在刷新页面这个按钮,因此使用 vuex 缓存数据能够应对大多数场景。
export default {
state: {
// 缓存修改手机号须要的信息
changePhoneInfo: {
nameUser: '',
email: '',
phone: ''
},
}
}
复制代码
若是须要持久化缓存,结合浏览器或 APP 缓存更佳。
export default {
// 登录成功后,vuex 写入token,并写入app缓存,存储持久化
[ActionTypes.LOGIN_SUCCESS]: function(store, token) {
store.commit(MutationTypes.SET_TOKEN, token)
setStorage('token', token)
router.replace({ name: 'Home', params: { source: 'login' } })
}
}
复制代码
在写后台管理平台时候,常常会有 list
选型组件,里面数据从服务端拿的数据。若是咱们把这个 list
数据存储起来,下次再次使用,直接从 store
里面拿,这样咱们就不用再去请求数据了。至关于减小了一次请求。
假设我如今有个需求,须要将性别0、一、2,分别转换成男、女、不肯定这三个汉字展现。页面中多处地方须要使用。
<template lang="pug">
.user-info
.gender
label(for="性别") 性别
span {{gender}}
</template>
复制代码
完成这个需求,咱们知道有 4 种方式:
应该选择哪一种方式呢?
我从下面三个方面来论述这个问题
1. 可实现性
computed
实现成功,咱们知道computed
不是一个函数是没法传参的,这里有个技巧,return
一个函数接受传过来的参数// ...
computed: {
convertIdToName() {
return function(value) {
const item = list.find(function(item) {
return item.id === value
})
return item.name
}
}
}
复制代码
methods
实现成功,这里直接能够传参数,一种常规的写法。注意
methods
、computed
和data
相互以前是不能同名的
// ...
methods: {
convertIdToName(value) {
const item = list.find(function(item) {
return item.id === value
})
return item.name
}
}
复制代码
utils
和 methods
差很少基本上也能够实现filter
也是实现的,有个能够和methods
、computed
同名哦filters: {
console.log(this.render)
convertIdToName(value) {
const item = list.find(function(item) {
return item.id === value
})
return item.name
}
},
复制代码
总的来讲他们所有均可以实现这个需求
2. 局限性
computed
、methods
和data
三者互不一样名,他们没办法被其余组件使用,除非经过 mixins
filters
与 utils
没法访问 this
,也就是于响应式绝缘。可是经过定义全局filters
,能够其余地方使用,另外还能够直接加载第三方filter
和utils
3. 总结比较
filters
与 utils
归属一对,他们既是脱离了 this
,得到了自由,又是被this
弃之门外。相反 methods
与 computed
与 this
牢牢站在一块儿,但又是没法得到自由。
export const thousandBitSeparator = (value) => {
return value && (value
.toString().indexOf('.') !== -1 ? value.toString().replace(/(\d)(?=(\d{3})+\.)/g, function($0, $1) {
return $1 + ',';
}) : value.toString().replace(/(\d)(?=(\d{3})+$)/g, function($0, $1) {
return $1 + ',';
}));
}
复制代码
两款插件
vue-filter:www.npmjs.com/package/vue… 使用 use
引入
vue2-filters:www.npmjs.com/package/vue… 使用mixins
引入
有须要的话,我通常就用第二个了,大多数都是本身写一下小过滤器
自定义过滤器以后,直接全局自动注册,其余地方均可以使用
遍历过滤属性值,一次性所有注册
for (const key in filters) {
Vue.filter(key, filters[key])
}
复制代码
咱们思考一下测试 js 代码须要哪些东西
若是是测试 vue 代码呢? 那得再加一个 vue 测试容器
{
"@vue/cli-plugin-unit-jest": "^4.0.5",
"@vue/test-utils": "1.0.0-beta.29",
"jest": "^24.9.0",
// ...
}
复制代码
// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html
module.exports = {
preset: '@vue/cli-plugin-unit-jest',
automock: false,
"/private/var/folders/10/bb2hb93j34999j9cqr587ts80000gn/T/jest_dx",
clearMocks: true,
// collectCoverageFrom: null,
coverageDirectory: 'tests/coverage'
//...
}
复制代码
对咱们以前写的一个性别名称转换工具进行测试
import { convertIdToName } from './convertIdToName'
describe('测试convertIdToName方法', () => {
const list = [
{ id: 0, name: '男' },
{ id: 1, name: '女' },
{ id: 2, name: '未知' }
]
it('测试正常输入', () => {
const usage = list
usage.forEach((item) => {
expect(convertIdToName(item.id, list)).toBe(item.name)
})
})
it('测试非正常输入', () => {
const usage = ['a', null, undefined, NaN]
usage.forEach((item) => {
expect(convertIdToName(item, list)).toBe('')
})
})
})
复制代码
这样一测试,发现原来咱们以前写的工具备这么多漏洞
export const convertIdToName = (value, list) => {
if (value !== 0 && value !== 1 && value !== 2) return ''
const item = list.find(function(item) {
return item.id === value
})
return item.name
}
复制代码
如今测试都经过了呢
对咱们最简单的
hello world
进行测试
<template lang="pug">
.hello
h1 {{ msg }}
</template>
<script> export default { props: { msg: String } } </script>
复制代码
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
it('renders props.msg when passed', () => {
const msg = 'new message'
const wrapper = shallowMount(HelloWorld, {
propsData: { msg }
})
expect(wrapper.text()).toMatch(msg)
})
})
复制代码
异步测试有几种常见写法
async
与 await
done()
简单的异步测试,测试一个简单的登录请求
export const login = (data) => post('/user/login', data)
复制代码
测试代码
import { login } from '@/api/index'
describe('login api', () => {
const response = {
code: '1000',
data: {}
}
const errorResponse = {
code: '5000',
data: {},
message: '用户名或密码错误'
}
it('测试正常登录', async () => {
const params = {
user: 'admin',
password: '123456'
}
expect(await login(params)).toEqual(response)
})
it('测试异常登录', async () => {
const params = {
user: 'admin',
password: '123123'
}
expect(await login(params)).toEqual(errorResponse)
})
})
复制代码
组件,api
,工具这些零零碎碎都测试了,并且这些都是比较通用、和业务关系不大的代码,它们改动较少,测试到这里其实已经足够了,已经达到了 20%
的测试工做量了很大一部分代码的目的。
为何我说只有 20% 的工做量呢?由于这些都是不怎么变化的逻辑,是一劳永逸的事情。长远来讲占用的工做量确实不多。
可是有些状况业务仍是必须得测,也就是必需要功能模块集成测试。
常常得回归的业务,那种迭代对原有的系统比较大,避免改动以后使旧的代码各类新的问题。这种常常回归测试,采用 BDD
+ 集成测试,比不停改 bug
要轻松的多。
像版本同样,每次测试以后生成一个版本,比较与上一个版本的区别。 这是一种粒度及其小的测试,能够测试到每个符号。
这是咱们一个配置文件
export const api = {
develop: 'http://xxxx:8080',
mock: 'http://xxxx',
feature: 'http://xxxx',
test: 'http://xxxx',
production: 'http://xxxx'
}
export default api[process.env.NODE_ENV || 'dev']
复制代码
使用快照测试
import { api } from './config'
describe('配置文件测试', () => {
it('测试配置文件是否变更', () => {
expect(api).toMatchSnapshot({
develop: 'http://xxxx:8080',
mock: 'http://xxxx',
feature: 'http://xxxx',
test: 'http://xxxx',
production: 'http://xxxx'
})
})
})
复制代码
使用快照第一次测试后,经过测试,代码被写入快照
最近讨论比较多的算是测试驱动开发和行为驱动开发,其实总得来讲是 4 种
bug
多,代码质量低。那么你是哪种? 反正我比较佛系哈,有的不写测试,也有的写满测试。
本篇文章耗费做者一个多星期的业余时间,存手工敲打 6000+字,同时收集,整理以前不少技巧和边写做边思考和总结。若是能对你有帮助,即是它最大的价值。都看到这里还不点赞,太过不去啦!😄
因为技术水平有限,文章中若有错误地方,请在评论区指出,感谢!
文中大多数代码将在suo-design-pro 中更新
项目有时间会尽可能完善
写实践总结性文章真的很耗费时间。如何文章中有帮到你的地方分享下呗,让更多人看到!