唐金州的Vue开发实战学习笔记(生态篇)
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>
这个页面展现了页面的主题结构,有购物车示例、账号等信息,我们来看下ProductList
和ShoppingCart
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开发实战学习笔记(生态篇)相关推荐
- 唐金州的Vue开发实战学习笔记(基础篇)
Vue开发实战学习笔记 简易的Vue程序 组件 事件 插槽 单文件组件 双向绑定 虚拟DOM与key属性 组件更新 状态data与属性props vue的响应式更新 计算属性和侦听器 计算属性 com ...
- “物联网开发实战”学习笔记-(四)智能音箱制作和语音控制
"物联网开发实战"学习笔记-(四)智能音箱制作和语音控制 这次来造一个属于自己的智能音箱,这里详细介绍了智能音箱的语音控制的实现. 智能音箱的技术架构 智能音箱主要涉及拾音.前端信 ...
- “物联网开发实战”学习笔记-(二)手机控制智能电灯
"物联网开发实战"学习笔记-(二)手机控制智能电灯 如果搭建好硬件平台后,这一次我们的任务主要是调试好智能电灯,并且连接到腾讯云的物联网平台. 腾讯云物联网平台 腾讯物联网平台的优 ...
- “物联网开发实战”学习笔记-(五)根据土壤湿度、环境温湿度和光照强度自动浇花
"物联网开发实战"学习笔记-(五)根据土壤湿度.环境温湿度和光照强度自动浇花 研究场景需求 自动浇花这个场景,很明显是为了自动控制水泵,及时给植物补充水分.同时用户的目的并不只是浇 ...
- 小程序开发实战学习笔记
序言 原本题目写的时候实战经验,但是觉着算不上什么经验,就改成学习笔记了? 注册小程序账号 开发和发布流程 小范围讨论公公抽离之类 不写公共样式因为不知道别人会不会用你的 目录结构 小程序包含一个描述 ...
- python3网络爬虫开发实战学习笔记(二)------python3 XPATH爬 猫眼电影排名
我最近在看崔庆才老师的<python3 网络爬虫开发实战>觉得挺不错的,上面还有一个 用正则表达式爬取猫眼电影的电影排行榜 我练了一下,感觉不会很难,后来学到了xpath,就想用xpath ...
- 02区块链誓言DAPP项目开发实战学习笔记
1.DAPP开发 1.1 前端页面使用HTML展示 (1)vue (2)react 1.2 后端数据部署到区块链上 (1)编写合约 (2)部署合约,选择网络: 私有网络: 以太坊官方提供的网络: 主网 ...
- 小程序开发实战学习笔记 1
注册小程序账号 开发和发布流程 小范围讨论公公抽离之类 不写公共样式因为不知道别人会不会用你的 目录结构 小程序包含一个描述整体程序的 app 和多个描述各自页面的 page. 具体参见官方文档 生命 ...
- 五十四、Linux性能优化实战学习笔记|套路篇:系统监控的综合思路
目录 指标监控 日志监控 小结 上一节,我带你学习了,如何使用 USE 法来监控系统的性能,先简单回顾一下. 系统监控的核心是资源的使用情况,这既包括 CPU.内存.磁盘.文件系统.网络等硬件资源,也 ...
最新文章
- scrapy windows
- java 一般方法_一般覆盖Java中的方法
- 2021 届校招宣讲会来啦!神策数据,与你一起「数说」未来
- 使用AngularJS 进行Hybrid App 开发已经有一年多时间了,这里做一个总结
- boost::mp11::mp_reverse_fold_q相关用法的测试程序
- CRMEB小程序安装说明
- #1176 : 欧拉路·一(欧拉通路的判定)
- 创建简单的Flex组件
- java 检测ip网速_java心跳测网速Demo
- 欢乐纪中某A组赛【2019.1.19】
- springboot启动mybatis
- numpy.random.choice()用法详解(附官方文档)
- graphpad饼状图_应用 Graphpad 统计作图,助你写文章事半功倍
- AD5933的直流偏置和量程自动切换问题
- 2019最新升级【超能版】 vbox硬件级虚拟机系统 去虚拟化去vm标识 支持批量启动 批量克隆 CPA网赚挂机电商 virtualbox
- 如何利用Excel批量设置化学式下标
- 阿里云Redis开发遇到的问题总结
- 【实际开发14】- 进阶 A
- C语言inline详细讲解
- 人生苦短,我学python day08 字符串
热门文章
- 交流异步电机矢量控制(二)——矢量控制原理
- 《金字塔原理》核心思想梳理
- 对自己团队项目的意见以及移动App需求分析
- 【优化求解】基于新授粉方式的花授粉算法(NMFPA)求解单标目优化问题附matlab代码
- 快讯 | 51VR联手阿里云共推实时VR云渲染平台
- 出国读研去哪国最合适?
- SpringCloud maven-assembly-plugin 多级目录打包
- 【点灯鼠教程】K210裸机点灯之旅(1)点亮SSD1306【C开发I2C】
- 医疗大数据平台的主流解决方案和设计
- Apache SkyWalking的Grpc server thread pool is full, rejecting the task错误的解决办法