Vue开发实战学习笔记

  • Vuex
    • Why Vuex
    • How Vuex
    • Vuex的核心概念和底层原理
    • Vuex的最佳实践
  • Vue Router
    • Why Vue Router
    • 路由类型及底层原理
  • Nuxt
    • Nuxt解决的问题
    • Nuxt的核心原理
  • UI组件库对比
  • 常用开发工具
  • 单元测试

Vuex

Why Vuex

provide和inject虽然能够实现层层传递的数据管理,但对于一个大的管理系统而言会显得有些繁琐,我们需要一个大型的状态管理系统。

How Vuex

一个简单的计数器的例子,在main.js中引入Vuex

import Vue from 'vue'
import Vuex from 'vuex'
import App from './App.vue'Vue.use(Vuex)
Vue.config.productionTip = falseconst store = new Vuex.Store({state: {count: 0,},mutations: {increment(state) {state.count++}},actions: {increment({commit}) {setTimeout(()=>{// state.count++ // 不要对state进行更改操作,应该通过commit交给mutations去处理commit('increment')}, 3000)}},getters: {doubleCount(state) {return state.count * 2}}
})new Vue({store,render: h => h(App),
}).$mount('#app')

APP.vue中, $store.dispatch对应的是action的定义,$store.commit对应的是mutations 的定义:

<template><div id="app">{{count}}<br>{{$store.getters.doubleCount}}<button @click="$store.commit('increment')">count++</button><button @click="$store.dispatch('increment')">count++</button></div>
</template><script>
export default {name: 'app',computed: {count() {return this.$store.state.count}}
}
</script><style>
</style>

$store 是如何是如何挂载到实例 this 上的?
通过new Vue中放置store

Vuex的核心概念和底层原理



min-vuex实例:我们尝试实现一个简单的min-vuex:

import Vue from 'vue'
const Store = function Store (options = {}) {const {state = {}, mutations={}} = optionsthis._vm = new Vue({data: {$$state: state},})this._mutations = mutations
}
Store.prototype.commit = function(type, payload){if(this._mutations[type]) {this._mutations[type](this.state, payload)}
}
Object.defineProperties(Store.prototype, { state: { get: function(){return this._vm._data.$$state} }
});
export default {Store}

在这个简易的min-vuex中,我们定义了state、mutation、commit、get等属性,可以将上述的case中的vuex替换为我们的min-vuex,计时器也是可以跑起来的。
main.js文件

import Vue from 'vue'
import Vuex from './min-vuex'
import App from './App.vue'Vue.use(Vuex)
Vue.config.productionTip = falseconst store = new Vuex.Store({state: {count: 0,},mutations: {increment(state) {state.count++}},// actions: {//   increment({commit}) {//     setTimeout(()=>{//       // state.count++ // 不要对state进行更改操作,应该通过commit交给mutations去处理//       commit('increment')//     }, 3000)//   }// },// getters: {//   doubleCount(state) {//     return state.count * 2//   }// }
})
Vue.prototype.$store = store
new Vue({// store,render: h => h(App),
}).$mount('#app')

App.vue文件

<template><div id="app">{{count}}<button @click="$store.commit('increment')">count++</button></div>
</template><script>
export default {name: 'app',computed: {count() {return this.$store.state.count}}
}
</script><style>
</style>

扩展简化版的 min-vuex,实现 getters,并实现 Vuex 的方式注入$store

  • 计算属性computer实现getters缓存
  • beforeCreate中混入$store的获取方式
    看一下扩展后的min-vuex
let Vue;
function install (_Vue) {Vue = _Vue;function vuexInit () {var options = this.$options;// store injectionif (options.store) {this.$store = typeof options.store === 'function'? options.store(): options.store;} else if (options.parent && options.parent.$store) {this.$store = options.parent.$store;}}Vue.mixin({ beforeCreate: vuexInit });
}const Store = function Store (options = {}) {const {state = {}, mutations={}, getters={}} = optionsconst computed = {}const store = thisstore.getters = {};for (let [key, fn] of Object.entries(getters)) {computed[key] = function () { return fn(store.state, store.getters); };Object.defineProperty(store.getters, key, {get: function () { return store._vm[key]; },});}this._vm = new Vue({data: {$$state: state},computed,})this._mutations = mutations
}
Store.prototype.commit = function(type, payload){if(this._mutations[type]) {this._mutations[type](this.state, payload)}
}
Object.defineProperties(Store.prototype, { state: { get: function(){return this._vm._data.$$state} }
});
export default {Store, install}

Vuex的最佳实践

前面提到的五个核心概念的取值,vuex提供了很多简写的方式:

我们可以用常量代替Mutation事件类型:

Module:

  • 开启命名空间 namespaced: true
  • 嵌套模块不要过深,尽量扁平化
  • 灵活应用 createNamespacedHelpers

购物车示例
看下代码运行页面:

示例的逻辑还是比较清晰的,我们可以将产品添加到清单中,添加完产品可以将清单提交到后台去购买,我们看一下我们的代码逻辑。
入口文件main.js

import Vue from 'vue'
import App from './App.vue'
import store from './store' Vue.config.productionTip = falsenew Vue({store,render: h => h(App),
}).$mount('#app')

入口文件还是一样,只是将store相关的数据都放在了store文件中,入口文件没有什么特别之处,我们接下来看一下App.vue

<template><div id="app"><h1>购物车示例</h1><p>账号: {{email}}</p><hr><h2>产品</h2><ProductList/><hr><ShoppingCart/></div>
</template><script>
import { mapState } from 'vuex'
import ProductList from './components/ProductList.vue'
import ShoppingCart from './components/ShoppingCart.vue'
export default {computed: mapState({email: state => state.userInfo.email}),components: { ProductList, ShoppingCart },
}
</script>

这个页面展现了页面的主题结构,有购物车示例、账号等信息,我们来看下ProductListShoppingCart
ProductList.vue

<template><ul><liv-for="product in products":key="product.id">{{ product.title }} - {{ product.price }}<br><button:disabled="!product.inventory"@click="addProductToCart(product)">加入购物车</button></li></ul>
</template><script>
import { mapState, mapActions } from 'vuex'
export default {computed: mapState({products: state => state.products.all,}),// computed: {//   products(){//     return this.$store.state.products.all//   }// },methods: mapActions('cart', ['addProductToCart']),// methods: {//   addProductToCart(product){//     this.$store.dispatch('cart/addProductToCart', product)//   }// },created () {this.$store.dispatch('products/getAllProducts')}
}
</script>

注意:上述注释的代码等价于未注释的代码。
ShoppingCart.Vue

<template><div class="cart"><h2>清单</h2><p v-show="!products.length"><i>请添加产品到购物车</i></p><ul><liv-for="product in products":key="product.id">{{ product.title }} - {{ product.price }} x {{ product.quantity }}</li></ul><p>合计: {{ total }}</p><p><button :disabled="!products.length" @click="checkout(products)">提交</button></p><p v-show="checkoutStatus">提交 {{ checkoutStatus }}.</p></div>
</template><script>
import { mapGetters, mapState } from 'vuex'
export default {computed: {...mapState({checkoutStatus: state => state.cart.checkoutStatus}),...mapGetters('cart', {products: 'cartProducts',total: 'cartTotalPrice'}),// ...mapGetters({//   products: 'cart/cartProducts',//   total: 'cart/cartTotalPrice'// })},// computed: {//   checkoutStatus(){//     return this.$store.state.cart.checkoutStatus//   },//   products() {//     return this.$store.getters['cart/cartProducts']//   },//   total() {//     return this.$store.getters['cart/cartTotalPrice']//   }// },methods: {checkout (products) {this.$store.dispatch('cart/checkout', products)}},
}
</script>

我们来看一下store的内容:
index.js文件中:

import Vue from 'vue'
import Vuex from 'vuex'
import cart from './modules/cart'
import products from './modules/products'Vue.use(Vuex)
export default new Vuex.Store({state: {userInfo: {email: "xxxxxx@qq.com"}},modules: {cart,products},
})

在这里我们把cart和products模块引入注册到modules中。
看一下modules/products.js模块

import shop from '../../api/shop'
import {PRODUCTS} from '../mutation-types'// initial state
const state = {all: []
}// getters
const getters = {}// actions
const actions = {getAllProducts ({ commit }) {shop.getProducts(products => {commit(PRODUCTS.SET_PRODUCTS, products)})}
}// mutations
const mutations = {[PRODUCTS.SET_PRODUCTS] (state, products) {state.all = products},[PRODUCTS.DECREMENT_PRODUCT_INVENTORY] (state, { id }) {const product = state.all.find(product => product.id === id)product.inventory--}
}export default {namespaced: true,state,getters,actions,mutations
}

其中shop就是去模拟一下ajax请求:

/*** Mocking client-server processing*/
const _products = [{"id": 1, "title": "华为 Mate 20", "price": 3999, "inventory": 2},{"id": 2, "title": "小米 9", "price": 2999, "inventory": 0},{"id": 3, "title": "OPPO R17", "price": 2999, "inventory": 5}
]export default {getProducts (cb) {setTimeout(() => cb(_products), 100)},buyProducts (products, cb, errorCb) {setTimeout(() => {// simulate random checkout failure.Math.random() > 0.5? cb(): errorCb()}, 100)}
}

cart的代码会稍微复杂一点:

import shop from '../../api/shop'
import { CART, PRODUCTS } from '../mutation-types'// initial state
// shape: [{ id, quantity }]
const state = {items: [],checkoutStatus: null
}// getters
const getters = {cartProducts: (state, getters, rootState) => {return state.items.map(({ id, quantity }) => {const product = rootState.products.all.find(product => product.id === id)return {title: product.title,price: product.price,quantity}})},cartTotalPrice: (state, getters) => {return getters.cartProducts.reduce((total, product) => {return total + product.price * product.quantity}, 0)}
}// actions
const actions = {checkout ({ commit, state }, products) {const savedCartItems = [...state.items]//先将购物车的状态置为空commit(CART.SET_CHECKOUT_STATUS, null)// empty cartcommit(CART.SET_CART_ITEMS, { items: [] })shop.buyProducts(products,() => commit(CART.SET_CHECKOUT_STATUS, 'successful'),() => {commit(CART.SET_CHECKOUT_STATUS, 'failed')// rollback to the cart saved before sending the requestcommit(CART.SET_CART_ITEMS, { items: savedCartItems })})},addProductToCart ({ state, commit }, product) {commit(CART.SET_CHECKOUT_STATUS, null)if (product.inventory > 0) {const cartItem = state.items.find(item => item.id === product.id)if (!cartItem) {commit(CART.PUSH_PRODUCT_TO_CART, { id: product.id })} else {commit(CART.INCREMENT_ITEM_QUANTITY, cartItem)}// remove 1 item from stockcommit(`products/${PRODUCTS.DECREMENT_PRODUCT_INVENTORY}`, { id: product.id }, { root: true })}}
}// mutations
const mutations = {[CART.PUSH_PRODUCT_TO_CART] (state, { id }) {state.items.push({id,quantity: 1})},[CART.INCREMENT_ITEM_QUANTITY] (state, { id }) {const cartItem = state.items.find(item => item.id === id)cartItem.quantity++},[CART.SET_CART_ITEMS] (state, { items }) {state.items = items},[CART.SET_CHECKOUT_STATUS] (state, status) {state.checkoutStatus = status}
}export default {namespaced: true,state,getters,actions,mutations
}

扩展购物车示例,提供单次添加 1-N 的数量到购物车的功能:
https://github.com/geektime-geekbang/geektime-vue-1/tree/master/vuex-demo3_a

Vue Router

Why Vue Router

传统开发模式下,每个url都对应着一个html页面,每次切换url的时候会引起页面的重新加载,在这种情况下诞生了单页面(spa)开发模式,用户在切换url的时候不在是执行页面的变化,而是根据我们的逻辑进行执行,返回数据。
看一下Vue Router解决的问题:

  • 监听 URL 的变化,并在变化前后执行相应的逻辑
  • 不同的 URL 对应不同的不同的组件
  • 提供多种方式改变 URL 的 API(URL 的改变不能导致浏览器刷新)

它的使用方式:

  • 提供一个路由配置表,不同 URL 对应不同组件的配置
  • 初始化路由实例 new VueRouter()
  • 挂载到 Vue 实例上
  • 提供一个路由占位,用来挂载 URL 匹配到的组件

看一下Vue Router的使用实例:
在main.js中引入路由

import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App.vue'
import routes from './routes'Vue.config.productionTip = falseVue.use(VueRouter)const router = new VueRouter({mode: 'history',routes,
})new Vue({router,render: h => h(App),
}).$mount('#app')

在main.js中完成路由的注册,接下来看App.vue这个文件:

<template><div id="app"><h2>router demo</h2><router-view></router-view></div>
</template><script>
export default {name: 'app',components: {},
}
</script><style>
#app {font-family: 'Avenir', Helvetica, Arial, sans-serif;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;text-align: center;color: #2c3e50;margin-top: 60px;
}
</style>

在里面加入了<router-view></router-view>组件。
在路由配置列表中则进行如下配置:

import RouterDemo from './components/RouterDemo'
import RouterChildrenDemo from './components/RouterChildrenDemo'const routes = [{ path: '/foo', component: RouterDemo, name: '1' },{ path: '/bar', component: RouterDemo, name: '2' },// 当 /user/:id 匹配成功,// RouterDemo 会被渲染在 App 的 <router-view /> 中{ path: '/user/:id', component: RouterDemo, name: '3',props: true,children: [{// 当 /user/:id/profile 匹配成功,// RouterChildrenDemo 会被渲染在 RouterDemo 的 <router-view/> 中path: 'profile',component: RouterChildrenDemo,name: '3-1'},{// 当 /user/:id/posts 匹配成功// RouterChildrenDemo 会被渲染在 RouterDemo 的 <router-view/> 中path: 'posts',component: RouterChildrenDemo}]},{ path: '/a', redirect: '/bar' },{ path: '*', component: RouterDemo, name: '404' }
]export default routes

SPA 的缺点有哪些,如何解决?

  • 不利于SEO
  • 首屏渲染时间长

路由类型及底层原理

路由类型:

  • hash模式 丑,无法使用锚点定位
    hashchange
  • History模式 需要后端配合,IE9不兼容(可使用强制刷新处理)
    我们只需要在声明router的时候把mode改成history的模式就可以了。
    history.pushState API

看一下路由的底层原理图:

路由通过Vue.util.defineReactive_route 这样一个api把router的信息变为一个响应式的,我们通过router-link, $router.push, a href, 浏览器的前进后退以及手动更改URL来触发updateRouter方法,由updateRouter来改变响应式数据,updateRouter触发以后再来更改我们的router-view的更新。
对于动态内容,如果不使用SSR,如何做SEO

  • 使用无头浏览器(phantomjs、headlessChrome)——效率比较低

Nuxt

Nuxt解决的问题

我们都知道spa单页面的缺点有以下几种:

  • 不利于SEO(搜索引擎爬取单页面是没有内容的,它不会出现再搜索的结果中)
  • 首屏渲染时间长

针对这两个缺点,我们会有一些方案来解决:

  • 服务端渲染SSR
  • 预渲染Prerendering

Prerendering预渲染

  • 主要适用于静态站点

SSR

  • 动态渲染
  • 配置繁琐

那么针对这些问题Nuxt就是去做这些操作

  • 静态站点
  • 动态渲染
  • 简化配置

Nuxt的核心原理

UI组件库对比

常用开发工具

Vetur

  • 语法高亮
  • Lint 检查
  • 格式化

ESLint

  • 代码规范
  • 错误检查

Prettier

  • 格式化

** Vue DevTools**

  • 集成Vuex
  • 可远程调试

单元测试

使用方式:

  • jest或mocha
  • @vue/test-utils
  • sinon
    看一下单测jest的配置(jest.config.js )
module.exports = {moduleFileExtensions: ["js", "jsx", "json", "vue"],transform: {"^.+\\.vue$": "vue-jest",".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$":"jest-transform-stub","^.+\\.jsx?$": "babel-jest"},transformIgnorePatterns: ["/node_modules/"],moduleNameMapper: {"^@/(.*)$": "<rootDir>/src/$1"},snapshotSerializers: ["jest-serializer-vue"],testMatch: ["**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],testURL: "http://localhost/"
};

transform中配置不同的类型处理不同的文件
moduleNameMapper 指定快捷路径
snapshotSerializers 用来做快照的格式化
testMatch 来匹配那些需要做单元测试
testURL 是给jest去使用的
书写单测代码:

import { mount } 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 = mount(HelloWorld, {propsData: { msg }});expect(wrapper.text()).toMatch(msg);});
});

expect是一个断言,我希望wrapper.text()msg是匹配的。
看一个复杂一点的单元测试,这是一个计数器的单元测试:

import { mount } from "@vue/test-utils";
import Counter from "@/components/Counter.vue";
import sinon from "sinon";describe("Counter.vue", () => {const change = sinon.spy();const wrapper = mount(Counter, {listeners: {change}});it("renders counter html", () => {expect(wrapper.html()).toMatchSnapshot();});it("count++", () => {const button = wrapper.find("button");button.trigger("click");expect(wrapper.vm.count).toBe(1);expect(change.called).toBe(true);button.trigger("click");expect(change.callCount).toBe(2);});
});

唐金州的Vue开发实战学习笔记(生态篇)相关推荐

  1. 唐金州的Vue开发实战学习笔记(基础篇)

    Vue开发实战学习笔记 简易的Vue程序 组件 事件 插槽 单文件组件 双向绑定 虚拟DOM与key属性 组件更新 状态data与属性props vue的响应式更新 计算属性和侦听器 计算属性 com ...

  2. “物联网开发实战”学习笔记-(四)智能音箱制作和语音控制

    "物联网开发实战"学习笔记-(四)智能音箱制作和语音控制 这次来造一个属于自己的智能音箱,这里详细介绍了智能音箱的语音控制的实现. 智能音箱的技术架构 智能音箱主要涉及拾音.前端信 ...

  3. “物联网开发实战”学习笔记-(二)手机控制智能电灯

    "物联网开发实战"学习笔记-(二)手机控制智能电灯 如果搭建好硬件平台后,这一次我们的任务主要是调试好智能电灯,并且连接到腾讯云的物联网平台. 腾讯云物联网平台 腾讯物联网平台的优 ...

  4. “物联网开发实战”学习笔记-(五)根据土壤湿度、环境温湿度和光照强度自动浇花

    "物联网开发实战"学习笔记-(五)根据土壤湿度.环境温湿度和光照强度自动浇花 研究场景需求 自动浇花这个场景,很明显是为了自动控制水泵,及时给植物补充水分.同时用户的目的并不只是浇 ...

  5. 小程序开发实战学习笔记

    序言 原本题目写的时候实战经验,但是觉着算不上什么经验,就改成学习笔记了? 注册小程序账号 开发和发布流程 小范围讨论公公抽离之类 不写公共样式因为不知道别人会不会用你的 目录结构 小程序包含一个描述 ...

  6. python3网络爬虫开发实战学习笔记(二)------python3 XPATH爬 猫眼电影排名

    我最近在看崔庆才老师的<python3 网络爬虫开发实战>觉得挺不错的,上面还有一个 用正则表达式爬取猫眼电影的电影排行榜 我练了一下,感觉不会很难,后来学到了xpath,就想用xpath ...

  7. 02区块链誓言DAPP项目开发实战学习笔记

    1.DAPP开发 1.1 前端页面使用HTML展示 (1)vue (2)react 1.2 后端数据部署到区块链上 (1)编写合约 (2)部署合约,选择网络: 私有网络: 以太坊官方提供的网络: 主网 ...

  8. 小程序开发实战学习笔记 1

    注册小程序账号 开发和发布流程 小范围讨论公公抽离之类 不写公共样式因为不知道别人会不会用你的 目录结构 小程序包含一个描述整体程序的 app 和多个描述各自页面的 page. 具体参见官方文档 生命 ...

  9. 五十四、Linux性能优化实战学习笔记|套路篇:系统监控的综合思路

    目录 指标监控 日志监控 小结 上一节,我带你学习了,如何使用 USE 法来监控系统的性能,先简单回顾一下. 系统监控的核心是资源的使用情况,这既包括 CPU.内存.磁盘.文件系统.网络等硬件资源,也 ...

最新文章

  1. scrapy windows
  2. java 一般方法_一般覆盖Java中的方法
  3. 2021 届校招宣讲会来啦!神策数据,与你一起「数说」未来
  4. 使用AngularJS 进行Hybrid App 开发已经有一年多时间了,这里做一个总结
  5. boost::mp11::mp_reverse_fold_q相关用法的测试程序
  6. CRMEB小程序安装说明
  7. #1176 : 欧拉路·一(欧拉通路的判定)
  8. 创建简单的Flex组件
  9. java 检测ip网速_java心跳测网速Demo
  10. 欢乐纪中某A组赛【2019.1.19】
  11. springboot启动mybatis
  12. numpy.random.choice()用法详解(附官方文档)
  13. graphpad饼状图_应用 Graphpad 统计作图,助你写文章事半功倍
  14. AD5933的直流偏置和量程自动切换问题
  15. 2019最新升级【超能版】 vbox硬件级虚拟机系统 去虚拟化去vm标识 支持批量启动 批量克隆 CPA网赚挂机电商 virtualbox
  16. 如何利用Excel批量设置化学式下标
  17. 阿里云Redis开发遇到的问题总结
  18. 【实际开发14】- 进阶 A
  19. C语言inline详细讲解
  20. 人生苦短,我学python day08 字符串

热门文章

  1. 交流异步电机矢量控制(二)——矢量控制原理
  2. 《金字塔原理》核心思想梳理
  3. 对自己团队项目的意见以及移动App需求分析
  4. 【优化求解】基于新授粉方式的花授粉算法(NMFPA)求解单标目优化问题附matlab代码
  5. 快讯 | 51VR联手阿里云共推实时VR云渲染平台
  6. 出国读研去哪国最合适?
  7. SpringCloud maven-assembly-plugin 多级目录打包
  8. 【点灯鼠教程】K210裸机点灯之旅(1)点亮SSD1306【C开发I2C】
  9. 医疗大数据平台的主流解决方案和设计
  10. Apache SkyWalking的Grpc server thread pool is full, rejecting the task错误的解决办法