一、项目创建
- 全局安装@vue/cli
- 安装Node.js v10以上版本
- 使用VScode或者WebStorm
- vue create 项目名
- cd 项目名
- yarn serve
import alias:
- TS/JS可以通过 @/目录名 引入文件。
- CSS/SASS 可以通过 ~@/目录名 引入文件,不过在WebStorm要配置webpack文件为vue的webpack配置文件。
二、使用Vue Router 底部导航
这里使用hash模式。vue-router
默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。
- 同样的思路,记账,标签和统计三个页面,默认进入记账,添加一个404页面。
- 在router/index.ts里添加路径,配置对应组件。
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }
// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
routes // (缩写) 相当于 routes: routes
})
- 初始化组件。
- 将router传给new Vue()
const app = new Vue({
router
}).$mount('#app')
- 在App组件里用<router-view/>给出router渲染区域
三、使用svg-sprite-loader引入icon
安装svg-sprite-loader,将svg转化为一个symbol。
在vue.config.js添加:
const path = require('path')
module.exports = {
publicPath: process.env.NODE_ENV === 'production'
? '/save-money-website-1/'
: '/',
lintOnSave: false,
chainWebpack: config =>{
const dir = path.resolve(__dirname, 'src/assets/icons')
config.module
.rule('svg-sprite')
.test(/.svg$/)
.include.add(dir).end() // 包含 icons 目录
.use('svg-sprite-loader-mod').loader('svg-sprite-loader-mod').options({extract:false}).end()
.use('svgo-loader').loader('svgo-loader')
.tap(options => ({...options, plugins: [{removeAttrs: {attrs: 'fill'}}]})).end()
config.plugin('svg-sprite').use(require('svg-sprite-loader-mod/plugin'), [{plainSprite: true}])
config.module.rule('svg').exclude.add(dir) // 其他 svg loader 排除 icons 目录
},
}
使用:<svg>
<use :xlink:href="'#'+name"/>
</svg>
四、优化思路
封装导航栏:
两种思路:在App全局展示或者每个组件自己引入
这里感觉组件自己引入好一点,可能会有组件不需要,比如404页面
确定思路后,添加CSS,使用flex,多试,错了就改,没错就继续。
代码优化:
事不过三,多次使用的地方一定要抽离,例如:
将svg抽离成<Icon />组件
将导航抽离成<Nav />组件
将布局抽离成<Layout />组件
五、vue的装饰器
vue-class-component
(官方文档)和vue-property-derocator
(官方文档)
这里主要用到@components和@Prop
import { Vue, Component, Prop } from 'vue-property-decorator'
@Component
export default class YourComponent extends Vue {
@Prop(Number) readonly propA: number | undefined
@Prop({ default: 'default value' }) readonly propB!: string
@Prop([String, Boolean]) readonly propC: string | boolean | undefined
}
相当于:
export default {
props: {
propA: {
type: Number,
},
propB: {
default: 'default value',
},
propC: {
type: [String, Boolean],
},
},
}
还有mixin,watch。
// mixins.js
import Vue from 'vue'
import Component from 'vue-class-component'
// You can declare mixins as the same style as components.
@Component
export class Hello extends Vue {
hello = 'Hello'
}
@Component
export class World extends Vue {
world = 'World'
}
然后使用:
import Component, { mixins } from 'vue-class-component'
import { Hello, World } from './mixins'
// Use `mixins` helper function instead of `Vue`.
// `mixins` can receive any number of arguments.
@Component
export class HelloWorld extends mixins(Hello, World) {
created () {
console.log(this.hello + ' ' + this.world + '!') // -> Hello World!
}
}
watch:
import { Vue, Component, Watch } from 'vue-property-decorator'
@Component
export default class YourComponent extends Vue {
@Watch('child')
onChildChanged(val: string, oldVal: string) {}
@Watch('person', { immediate: true, deep: true })
onPersonChanged1(val: Person, oldVal: Person) {}
@Watch('person')
onPersonChanged2(val: Person, oldVal: Person) {}
}
相当于:
export default {
watch: {
child: [
{
handler: 'onChildChanged',
immediate: false,
deep: false,
},
],
person: [
{
handler: 'onPersonChanged1',
immediate: true,
deep: true,
},
{
handler: 'onPersonChanged2',
immediate: false,
deep: false,
},
],
},
methods: {
onChildChanged(val, oldVal) {},
onPersonChanged1(val, oldVal) {},
onPersonChanged2(val, oldVal) {},
},
}
六、v-model和.sync
这两者本质上是差不多的,并没有任何区别: “监听一个触发事件”="(val) => value = val"。
v-model:
<!--v-model写法-->
<my-component type="text" v-model="value">
<!--展开语法糖后的写法-->
<my-component type="text"
:value="value"
@input="value = $event.target.value"
>
<!--
默认针对原生组件input事件,但是如果子组件定义了针对事件
model: {
prop: "value",
event: "update"
},
则编译为
-->
<my-component type="text"
:value="value"
@update="(val) => value = val"
>
.sync:
<!--语法糖.sync-->
<my-component :value.sync="value" />
<!--编译后的写法-->
<my-component
:value="msg"
@update:value="(val) => value = val"
>
细微之处的区别:
- 只不过v-model默认对应的是input或者textarea等组件的input事件,如果在子组件替换这个input事件,其本质和.sync修饰符一模一样。比较单一,不能有多个。
// 子组件可以用自定义事件,来替换v-model默认对应的原生input事件,只不过我们需要在子组件手动 $emit
model: {
prop: "value",
event: "update"
}, - 一个组件可以多个属性用.sync修饰符,可以同时"双向绑定多个“prop”,而并不像v-model那样,一个组件只能有一个。
总结:
- v-model针对更多的是最终操作结果,是双向绑定的结果,是value,是一种change操作。
比如:输入框的值、多选框的value值列表、树结构最终绑定的id值列表(ant design和element都是)、等等... - .sync针对更多的是各种各样的状态,是状态的互相传递,是status,是一种update操作。
比如:组件loading状态、子菜单和树结构展开列表(状态的一种)、某个表单组件内部验证状态、等等....
七、vuex
官方文档介绍:每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同:
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
安装 (yarn add vuex)Vuex 之后,让我们来创建一个 store。创建过程直截了当——仅需要提供一个初始 state 对象和一些 mutation:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
现在,你可以通过store.state
来获取状态对象,以及通过store.commit
方法触发状态变更:
store.commit('increment')
console.log(store.state.count) // -> 1
为了在 Vue 组件中访问 this.$store
property,你需要为 Vue 实例提供创建好的 store。Vuex 提供了一个从根组件向所有子组件,以 store
选项的方式“注入”该 store 的机制:
new Vue({
el: '#app',
store: store,
})
现在我们可以从组件的方法提交一个变更:
methods: {
increment() {
this.$store.commit('increment')
console.log(this.$store.state.count)
}
}
除了state和mutations,还有一些概念需要了解。
getter:
Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
Aciton:
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
module:
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割。
八、TypeScript
- 在custom.d.ts可以声明全局类型
- ?表示可有可无,!表示非空
- 在装饰器中
vue-property-decorator
并没有提供专门的用于computed
的修饰器,因为ES6
的get/set
语法本身就可以替代computed
上述转换为ts
对应如下
export default {
computed: {
a () { return true },
b: function () { return true },
d: {
get () { return true },
set: function (v) { console.log(v) }
}
}
}
转化为:
export default class YourComponent extends Vue {
get a () { return true }
get b () { return true },
get d (){ return true },
set d (v) { console.log(v) }
}
九、其他
- 关于字体:
中文字体最佳实践:zenozeng/fonts.css
2. 数据持久化
把数据存在本地localstorage,通过window.localStorage.getItem(key)或setItem(key,value)进行读写
3. 命名严谨很重要
4. 插槽slot:
<navigation-link url="/profile">
Your Profile
</navigation-link>
然后在<navigation-link>模板里写:
<a
v-bind:href="url"
class="nav-link"
>
<slot></slot>
</a>
当组件渲染的时候,<slot></slot>
将会被替换为“Your Profile”。