【Vue版记账项目总结】Vue、Vuex、VueRouter、TS/JS、svg、css/scss实战

351 阅读8分钟

目录

  • 项目介绍
  • 项目前的准备及项目构思
  • 模块化
  • 封装通用组件
  • @语法导入相关文件
  • CSS的变种形式
  • Vue中CSS的deep选择器
  • Vue中CSS类名的表驱动编程
  • 使用VueRouter的active-class动态添加类名
  • 使用svg-sprite-loader引入icon与封装icon组件
  • JavaSript组件
  • TypeScript组件
  • custom.d.ts怎么用
  • Vue单文件组件的三种写法
  • Layout组件&插槽<slot>
  • 使用VueRouter
  • 在TS组件里使用mixins
  • Vuex--全局数据管理
  • Vue修饰符
  • ISO8601和dayjs
  • 使用localstorage存储数据
  • 源代码github/gitee

一、项目介绍

该项目是一个极简的记账应用。是一款基于Vue框架、VueRouterVuexTypeScript的单页面应用。

  • 功能

记账:金额、标签、备注、收入或支出等。

管理标签:添加、改名、删除

查看历史:按天、按周、按月

  • 技术实现

Vue VueRouter Vuex TypeScript JavaScript CSS HTML Nodejs Yarn等

  • 实现效果

二、项目前的准备

  • 版本不搭配会引起报错的!以下版本是我自己使用的,不会报错。
  • nodejs 版本 -- v10.15.3--使用淘宝镜像(下载快)
<!-- 使用淘宝源 -->
npm install -g nrm --registry=https://registry.npm.taobao.org
nrm use taobao
  • @vue/cli 版本-- 4.1.2
yarn global add @vue/cli@4.1.2
# npm i -g @vue/cli@4.1.2
vue --version  # 版本号应该是 4.1.2
vue create  /my-project/   //可自己修改自己喜欢的名字
cd /my-project/
yarn serve    //npm run serve 

  • Vue create配置
  • Vue create完整配置预览

  • 文件目录说明

  • 项目的整体构思

毫无疑问,做项目肯定要先进行一番设计构思。基于模仿记账应用的一些外观和功能,我进行了一些简化和打磨。最终确定了以下的项目样式。

1、确定页面URL

很显然,页面的URL需要用到前端路由router

  • #/money记账
  • #/labels标签
  • #/labels/Edit/:id编辑标签页面
  • #/statistic统计
  • 404页面
  • 默认进入记账页面#/money

2、添加代码

  • router/index.ts添加router,配置5个路径对应组件
  • 初始化组件:Money.vue、Labels.vue、Statistic.vue、NotFound.vue
  • 将router传给new Vue()
  • 在App组件里面用<router-view/>展示router渲染的页面

3、展示

两种思路:1、在<App />全局展示。2、每个组件自己引入。

优化:使用Vue.component全局组件

  • Nav.vue组件
<template>
	<nav>
	<!--  注意动态类名是根据当前路由的路径确定的  -->
   <router-link to="/money" class="item" active-class="selected">
     <Icon name="money"/>
     记账
   </router-link>
   <router-link to="/labels" class="item" active-class="selected">
     <Icon name="label"/>
     标签
   </router-link>
   <router-link to="/statistic" class="item" active-class="selected">
     <Icon name="statistic"/>
     统计
   </router-link>
	</nav>
</template>
<script lang="ts">
export default {
name: "Nav"
}
</script>
<style lang="scss" scoped>
@import "~@/assets/style/helper.scss";
  nav {
    @extend %outerShadow;
    display: flex;
    font-size: 14px;
    >.item {
      padding: 2px 0;
      width: 33.3%;
      display: flex;
      justify-content: center;
      align-items: center;
      flex-direction: column;
      .icon{
        width: 30px;
        height: 30px;
      }
    }
    > .item.selected{
      color: $color-highlight;
    }
  }
</style>

4、项目代码优化

事不过三,我与重复不共戴天。

  • 将布局抽离成组件
  • 某个函数经常用到就单独封装
  • 先写出一样代码效果,最后权衡利弊再进行优化。

三、模块化

模块化是指解决一个复杂问题时自顶向下逐层把系统划分成若干模块的过程,有多种属性,分别反映其内部特性。

模块化(modular)编程,是强调将程序的功能分离成独立的、可相互改变的模块(module)的软件设计技术,它使得每个模块都包含着执行预期功能的一个唯一方面(aspect)所必需的所有东西。 模块接口表达了这个模块所提供的和所要求的元素。 这些在接口中定义的元素可以被其他模块检测

显然的,在这次项目中,我们要做的模块化操作就是使每个组件中的代码量减少。一旦某个组件代码量超过150行的时候,你就该考虑封装或者模块化了

四、封装通用组件

  • 封装通用组件是一个重要的思想概念

  • 在本次项目中,我将频繁使用到封装通用组件的目的,为的就是减少重复,代码冗余

  • 当你重复某个组件三次及以上,你就该考虑封装通用组件可。

五、@语法导入相关文件

  • JS中的@语法
<script lang='JS | TS'>
	/* 这是JS或者TS中的@:表示根目录src */
    import NumberPad from '@/components/Money.vue/Number';
</script>
  • webStorm需要设置webpack的路径
  1. 打开settings,搜索webpack
  1. 在路径中填上webpack.config.js的路径即可。
  1. 完整的路径:D:....\项目名\node_modules\@vue\cli-service\webpack.config.js
  • CSS中的~@语法
<style lang='CSS | 其他CSS变种'>
	/* 假设test.scss存在以下路径。引入后就可以使用test.scss里面的东西了。 ~@表示更目录src*/
	@import "~@/assets/styles/test.scss";
</style>

六、CSS的变种形式

七、Vue中的Deep选择器

  • Vue组件中style标签的scoped属性

我们都知道在Vue中的Vue组件的style有个scoped属性。这个属性就是为了防止当我们使用CSS给页面添加样式时会影响到其他的组件中的CSS样式,造成这种情况的原因很多,比如类名重名、CSS选择器、还有一些选择器优先级的影响。所以scoped这个属性就是为了解决这个问题。在scoped的`style里面写的CSS样式只会在当前的组件生效。不会影响到其他的组件或者页面的样式。

那么问题来了,我们一般都使用scoped限制当前组件的样式。但是偶尔会在设置样式时会想要深入修改某个HTML元素或者Vue组件的单一样式,怎么办?这就是::v-deep的用处了。

  • 演示示例
<style scoped>
	/* 表示就是深入选择类名为a的组件里面样式中类名为b的。单独给类名为b的添加样式。 */
	.a >>> .b{
		border:1px solid red;
	}
</style>

八、Vue中类名的表驱动编程

在很多时候,我们在项目开发中都会用到动态绑定类名的时候。例如,点击某个item就让它拥有某个类名。取消点击后就让该item消除这个类名,可能我们绑定的类名也不止一个,这个时候就要用到类名的表驱动编程了。

  • 类名->表驱动编程

在Vue中,使用v-bind:属性='xxx',表示动态绑定该属性,即xxx所展示的是一个变量,而不是写死的字符串

<ul>
	<li v-for='item in tagList'
		:class='{selected:isSelected(item)}'
		:key='item.id'
		@click='toggleSelect(item)'
	>
	<span>{{item.name}}</span>
	</li>
</ul>
<script lang="ts">
import {Component} from 'vue-property-decorator';
import {TagHelper} from '@/mixins/TagHelper.ts';
import {mixins} from 'vue-class-component';
@Component
export default class Tags extends mixins(TagHelper){
  get tagList(){
    return this.$store.state.tagList;
  }
  selectedTags:Tag[]=[];
  created() {
    // 初始化数据
    this.$store.commit('fetchTags');
  }
  toggleSelect(tag:Tag){
    const index=this.selectedTags.indexOf(tag);
    if(index>=0){
      this.selectedTags.splice(index,1);
    }
    else {
      this.selectedTags.push(tag);
    }
    this.$emit('update:value', this.selectedTags)
  }
  isSelected(item:Tag){
    const index=this.selectedTags.indexOf(item);
    return index >= 0;
  }
};
</script>

小结:

在以上代码示例中,:class='{selected:isSelected(item)',这句代码的意思就是,当isSelected(item)的结果为true时,存在这个类名。同样的,我们可以通过逗号,来分隔更多的类名表示,如::class='{selected:isSelected(item),className1:fn1,className2:fn2}'。这样,当fn1fn2的结果为true时,那么这个li标签同时存在这些类名:selected、className1、className2

九、使用VueRouter的active-class动态添加类名

在VueRouter中,设置链接激活时使用的 CSS 类名

  • 情景一:点击跳转静态页面
<router-link to="/money" class="item" active-class="selected">
	<Icon name="money"/>
记账
</router-link>
|
<router-link to="/labels" class="item" active-class="selected">
	<Icon name="label"/>
标签
</router-link>
|
<router-link to="/statistic" class="item" active-class="selected">
	<Icon name="statistic"/>
统计
</router-link>

小结:

在以上的代码中,每个router-link都有一个active-class属性,这个意思就是:当前页面的路径是该router-link,那么这个router-link就存在这个selected类名。这样就可以做一些CSS样式来装饰,例如在当前页面被选择时高亮


十、使用svg-sprite-loader引入icon与封装icon组件

在做本项目中,需要一些图标来装饰,那毫无疑问就是使用阿里巴巴或者Icon Font中的一些图标了。那么问题来了,如何在Vue中使用这些Svg图标呢?

因为使用Vue create创建的项目中不存在webpack的配置。所以需要根据官方文档去vue.config.js中配置。

1、如何引入单个svg图标?

  • shims-vue.d.ts中添加如下代码
declare module "*svg" {
  const content:string;
  export default content;
}
  • 在Vue组件中import一个svg icon -- 假设所有的icon图片都放在此路径:src/assets/icons
<template>
	<div>啥也没有</div>
</template>
<script lang="ts">
	/* 导入svg标签,xxx是随意取得 */
	import xxx from "@/assets/icons/label.svg";
	console.log(xxx);
	export default {
		name :'VueComponent',
	}
</script>
<style lang="scss" scoped>
</style>
  • 结果:输出的只是一个路径字符串,显然不是我们想要的结果,接下来一一解决。

2、使用svg-sprite-loader

  • 使用yarn或者npm安装svg-sprite-loader
yarn add svg-sprite-loader -D
# npm i svg-sprite-loader -D

svg-sprite-loader该loader代码是JetBrains公司放在github上的,它只给出了webpack的配置信息但是在Vue的项目中没有webpack.config.js的文件,意思就是我们要将JetBrains给出的配置信息翻译到Vue.config.js的配置中。

  • vue.config.js配置
const path=require('path');
module.exports = {
  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').loader('svg-sprite-loader').options({extract:false}).end()
      .use('svgo-loader').loader('svgo-loader') // 删除svg中自带的颜色(即删除svg的fill属性)
      .tap(options => ({...options,plugins:[{removeAttrs:{attrs:'fill'}}]})).end()
    config.plugin('svg-sprite').use(require('svg-sprite-loader/plugin'),[{plainSprite:true}])
    config.module.rule('svg').exclude.add(dir)  //其他svg loader排除icons目录
  }
}

3、使用1个svg图标

  • 使用的正确姿势
<svg>
	/* #不能删除,label是引入的label.svg的名字 */
	<use xlink:href='#label'/>
</svg>
<template>
	<div>
		<svg>
			/* #不能删除,label是引入的label.svg的名字 use里面没有文字内容就自闭和。在Vue中使用的是xml标签语法*/
			<use xlink:href='#label'/>
		</svg>
	</div>
</template>
<script lang="ts">
	/* 导入svg标签,xxx是随意取得 */
	import xxx from "@/assets/icons/label.svg";
	export default {
		name :'VueComponent',
	}
</script>
<style lang="scss" scoped>
</style>
  • 结果:得到该svg图标的相应图示展现在页面上 小结:
    1. Vue中使用的是xml标签的语法
    2. use 中应写为:xlink:href='#xxxx'的形式,xxxx是该svg的名字
    3. use标签中没有东西就记得自闭和

4、封装Icon组件

因为svg图标可能在哪儿都用得上,我们就可以将Icon提取成为一个Vue的全局组件。而且对于我们来说,区分svg的不同就是svg的名称。所以在封装Icon组件时,只需要我们单独传一下svg的名字就行了。,用外部数据prop传递该值就行了。

  • 全局组件:在main.ts中声明
Vue.component('Icon', Icons);	//Icon是Vue的组件别名(一般大写),Icons就是封装Icon的组件的名字
  • 封装Icon组件

动态绑定href中的name,且必须要求使用Icon组件时传递一个svg的名字,这样才能显示对应的icon图标

<template>
  <svg class="icon">
    <use :xlink:href="'#'+name" />
  </svg>
</template>
<script lang="ts">
  import x1 from '@/assets/components/icons/x1.svg';
  import x2 from '@/assets/components/icons/x2.svg';
  import x3 from '@/assets/components/icons/x3.svg';
  import x4 from '@/assets/components/icons/x4.svg';
  import x5 from '@/assets/components/icons/x5.svg';
  import x6 from '@/assets/components/icons/x6.svg';
  					.
  					.
  					.
  					.
  export default {
    props:['name'],
    name: "icons"
}
</script>
<style lang="scss" scoped>
  .icon{
    width: 1em;
    height: 1em;
    vertical-align: -0.15em;
    fill: currentColor;
    overflow: auto;
  }
</style>

5、如何import整个icons目录

这个问题的初衷:一个一个引入svg不傻吗?我都感觉没有动力去复制粘贴一个一个引入svg。

  • 将上面一长串的import替换
let importAll=(requireContext:__WebpackModuleApi.RequireContext)=>requireContext.keys().forEach(requireContext);
try{importAll(require.context('../assets/icons',true,/\.svg$/));} catch (err){console.log('err!');}

十一、JavaSript组件

<template>
	<div>
		<ul class='type'>
			<li :class="type==='-'&&'selected'">支出</li>
			<li :class="type==='+'&&'selected'">收入</li>
		</ul>
	</div>
</template>
<script>
	export default {
		name:'Types',
		data(){
			return {
				type:'-',s
			}
		}
		props:[/* 这里写外部数据 */],
		methods:{
			/* 这里写方法函数 */
			fn1(){},
			fn2(){},
		},
			.
			.
			.
			.
			.
	}
</script>

十二、TypeScript组件

1、TS的本质

TS比JS更加严谨,TS数据必须有类型。即TS比JS多一个类型检查

编译时,TS会进行类型检查,如果TS检查出类型错误或者未给出指定类型,终端会进行Error报错。但是你所写的JS代码一样可以进行运行(妥协)

2、 TS组件模板语法

<template>
	<div>这里写HTML</div>
</template>
<script lang="ts">
	import Vue from 'vue';	
	import {Component} from 'vue-property-decorator';
	@Component
	export default class test extends Vue{
		/* 这里面写class的语法 */
	}
</script>
<style lang="scss" scoped>
	/* 这里写style */
</style>

3、@Prop装饰器

@Prop装饰器是Vue的第三方vue-property-decorator提供的API,这个第三方库非常好用,一般都使用它来表示JS中的外部数据prop

基于vue-property-decorator

  • 第1步: 使用npm或yarn安装该第三方库
npm i -S vue-property-decorator
# yarn add -S vue-property-decorator
  • 基本用法:
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
}

小结:

  1. @Prop() 告诉Vue propA不是data,而是prop
  2. Number、String、Boolean(@Prop括号里面的类型)告诉Vue propA是个Number
  3. propA是属性名
  4. number | undefuned 就是 propA的类型
  5. 坑1:@Prop里面最好写清楚类型,不然会有潜在的bug
  6. 第2条和第4条不冲突

  • 上诉小结中2、4条不冲突的原因

4、@Watch装饰器

同样的,@Watch也是第三方库vue-property-decorator的,它的功能就是实现原生Vue组件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) {}
}

vue-property-decorator传送门


十三、custom.d.ts怎么用

custom是自己随意取的名字,一般为custom -- 表示自定义

.d.ts d是define的意思,ts是TypeScript的后缀。只要TypeScript发现是以.d.ts结尾的,就知道这是个全局的定义声明。

  • custom.d.ts代码
type rootState = {
    recordList: RecordItem[],
    tagList: Tag[],
    currentTag?: Tag,
    isCreateRecordError:Error |null;
    isCreateTagError:Error |null;
}
type Tag={
    id:string;
    name:string;
}
//注:Record这个变量名已经有内置了
type RecordItem={
    tags:Tag[],
    notes:string,
    type:string,
    amount:number,
    /* 表示类型时可以使用日期类型 用?表示可能该属性不存在*/
    createTime?:string,
}
type Result={
    title:string,
    total?:number,
    items:RecordItem[],
}

小结

  1. 我们一般在custom.d.ts中声明全局用到的数据类型等等

十四、Vue单文件组件的三种写法

在以下三种写法中,只有script里面的内容不一样

1、JS对象写法

export default { data, props, methods, created, ...}

2、TS类<script lang='ts'>

@Component
 export default class XXX extends Vue{
     xxx: string = 'hi';
     @Prop(Number) xxx: number|undefined;
 }

3、JS类<script lang='js'>

@Component
 export default class XXX extends Vue{
     xxx = 'hi'
 }

十五、Layout组件&插槽

很多时候,我们都会遇见重复的HTML和CSS,这个时候虽然ctrl+c/v能解决问题,但是代码量却是上去了,有没有什么解决方案呢?接下来就来介绍一下Layout组件和<slot />插槽吧。

Layout:布局,显然,Layout组件就是重复的布局,包括HTML和CSS,我们将重复的布局片段全部放在Layout组件里面。不同的内容就放入插槽<slot />

  • Layout组件&<slot/>插槽
<template>
	<div class="layout-wrapper" :class="classPrefix && `${classPrefix}-wrapper`">
	  <div class="content" :class="classPrefix && `${classPrefix}-content`">
      <slot />
    </div>
    <Nav />
	</div>
</template>
<script lang="ts">
export default {
  props:['classPrefix'],
  name: "Layout"
}
</script>
<style lang="scss" scoped>
  .layout-wrapper{
    display: flex;
    flex-direction: column;
    min-height: 100vh;
  }
  .content{
    overflow: auto;
    flex-grow: 1;
  }
</style>
  • 例如在Money.vue中使用Layout组件,<Layout></Layout>标签中间的内容。相当于原来Layout组件中的<slot/>插槽的内容,即<slot />就是替换Money.vue中<Layout></Layout>中间的内容
<template>
  <Layout class-prefix="layout">
    <NumberPad :value.sync="record.amount" @submit="saveRecord"/>
    <Tabs :data-source="recordTypeList"
          :value.sync="record.type"/>
    <div class="notes">
      <FormItem
          filed-name="备注"
          placeholder="在这里输入备注"
          :value.sync="record.notes"
      />
    </div>
    <Tags :value.sync="record.tags"/>
  </Layout>
</template>
<script lang="ts">
  import Vue from 'vue';
  import NumberPad from '@/components/Money/NumberPad.vue';
  import FormItem from '@/components/Money/FormItem.vue';
  import Tags from '@/components/Money/Tags.vue';
  import {Component} from 'vue-property-decorator';
  import Tabs from '@/components/Tabs.vue';
  import recordTypeList from '@/constants/recordTypeList.ts';
  @Component({
    components: {Tabs, Tags, FormItem, NumberPad},
  })
  export default class Money extends Vue {
    get recordList() {
      return this.$store.state.recordList;
    }
    recordTypeList = recordTypeList;
    record: RecordItem = {
      tags: [], notes: '', type: '-', amount: 0
    };
    created() {
      this.$store.commit('fetchTags');
      // this.$store.commit('fetchRecords');
    }
    onUpdateNotes(value: string) {
      this.record.notes = value;
    }
    saveRecord() {
      if(!this.record.tags.length){
        window.alert('请选择至少一个标签!');
        return;
      }
      this.$store.commit('createdRecord', this.record);
      if(this.$store.state.isCreateRecordError===null){
        window.alert('已保存')
        this.record.notes='';
      }
    }
  }
</script>
<style lang="scss" scoped>
  ::v-deep .layout-content {
    display: flex;
    flex-direction: column-reverse;
  }
  .notes {
    padding: 12px 0;
  }
</style>

小结:

  1. Layout组件是一个HTML样式和CSS样式重复抽离出来的组件。
  2. 引用Layout组件的组件中,独立的HTML内容就使用<slot /> 插槽插入内容

十六、使用VueRouter

在做这个单页面应用的项目中,前端路由无疑是最好的选择了。

而在Vue创建的项目中,在router文件目录下已经配置好了。

注意router需要在main.ts的new Vue()中引入

1、router/index.ts -- 配置路径

import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'
import Money from '@/views/Money.vue';
import Label from '@/views/Labels.vue';
import Statistic from '@/views/Statistic.vue';
import NotFound from '@/views/NotFound.vue';
import EditLabel from '@/components/Label/EditLabel.vue';
Vue.use(VueRouter)
const routes: Array<RouteConfig> = [
  {
    path:'/',
    redirect:'/money'
  },
  {
    path:'/money',
    component: Money
  },
  {
    path:'/labels',
    component: Label
  },
  {
    path:'/statistic',
    component: Statistic
  },
  {
    path:'/labels/edit/:id',
    component:EditLabel
  },
  {
    path:'*',
    component:NotFound
  }
]
const router = new VueRouter({
  routes
})
export default router

2、使用<router-view/> + <router-link>

<router-view>是用来展示对应路径下的页面,与它搭配使用的是<router-link to='path_name'>。我们将它放在App.vue中。

  • App.vue
<template>
  <div id="app">
	<router-view />
  </div>
</template>
<style lang="scss">
@import "~@/assets/style/reset.scss";
@import "~@/assets/style/helper.scss";
body{
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  line-height: 1.5;
  font-family: $font-hei;
  font-size: 16px;
  background: #f5f5f5;
}
</style>

我们在本次项目中选择将页面导航栏封装成一个Nav组件

  • Nav.vue
<template>
	<nav>
<!--  注意动态类名是根据当前路由的路径确定的  -->
   <router-link to="/money" class="item" active-class="selected">
     <Icon name="money"/>
     记账
   </router-link>
   <router-link to="/labels" class="item" active-class="selected">
     <Icon name="label"/>
     标签
   </router-link>
   <router-link to="/statistic" class="item" active-class="selected">
     <Icon name="statistic"/>
     统计
   </router-link>
	</nav>
</template>
<script lang="ts">
export default {
name: "Nav"
}
</script>
<style lang="scss" scoped>
@import "~@/assets/style/helper.scss";
  nav {
    @extend %outerShadow;
    display: flex;
    font-size: 14px;
    >.item {
      padding: 2px 0;
      width: 33.3%;
      display: flex;
      justify-content: center;
      align-items: center;
      flex-direction: column;
      .icon{
        width: 30px;
        height: 30px;
      }
    }
    > .item.selected{
      color: $color-highlight;
    }
  }
</style>

VueRouter小结:

  1. 在router/index.ts下配置路径,path是路径名,redirect是重定向,component是表示显示对应的组件名。
  2. <router-view/>展示对应路径下的页面。
  3. <router-link to='path_name'> 与<router-view/> 搭配使用,就好像是一个<a>标签,点击后<touter-view/>就展示对应的页面。
  4. 对于导航栏,最好做成一个Nav组件

十七、在TS组件里使用mixins

mixins解释传送门

在Vue组件中使用TS+minxins

我们一般都是在普通的情景下知道mixins的基本用法,但是在这次项目中我们将怎样使用呢?

大概思路:先在src目录下创建文件夹mixins,里面放入一些想要进行全局混入的mixins组件,然后导出这些组件

1、TagHelper.ts -- 将创建标签的功能抽离出来成为一个组件

import Vue from 'vue';
import Component from 'vue-class-component';
@Component
export class TagHelper extends Vue{
    createTag(){
        const name=window.prompt('请输入标签名');
        if(name){
            if(name.trim()==='') {
                return window.alert('标签名不能为空');
            }
            this.$store.commit('createTag',name);
            if(this.$store.state.isCreateTagError){
                window.alert('标签名重复了!')
                this.$store.state.isCreateTagError=null;
                return;
            }
            else {
                window.alert('标签创建成功!');
            }
        }
    }
}
export default TagHelper;

2、在Labels.vue里面使用mixins组件TagHelper.ts

<script lang="ts">
  import {mixins} from 'vue-class-component';
  import TagHelper from '@/mixins/TagHelper.ts';
  @Component
  export default class Labels extends mixins(TagHelper){
	  .
	  .		/* 这里面自动就继承了TagHelper里面的所有东西 */
	  .
  }
</script>

小结:

  1. 将需要mixin(混入)的重复组件抽离统一放在mixins下。
  2. 需要引用库vue-class-component
  3. 需要导入mixins从vue-class-component
  4. 组件中使用到mixins的重复组件需要继承mixins

十八、Vuex -- 全局数据管理

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式

换句话说:Vuex就是一个全局的状态管理,包括数据和函数行为等。能够操作数据的读写。

Vuex在项目目录store中,注意在main.ts的new Vue()中引入

1、安装

npm install vuex --save
# yarn add vuex

2、Vuex中的store结构预览

import Vue from 'vue';
import Vuex from 'vuex';
import clone from '@/lib/clone';
import createId from '@/lib/createId';
import router from '@/router';
Vue.use(Vuex);  // 使用Vue的use将Vuex绑到Vue.prototype上 -- 下面的代码就是:Vue.prototype.$store=store;
/* 有this和Vue的全局的区别,有this的可以直接在template中使用,且可以省去this */
const store = new Vuex.Store({
	state:{},
	mutations:{},
	actions: {},
    modules: {},
})
export default store;

目录结构小结

  1. state:里面存放全局状态的唯一数据源即变量,类似Vue组件的data
  2. mutations:里面存放函数和方法,类似Vue组件中的methods
  3. actions:里面也是存放函数和方法,与mutations不同的是,这里面放的是异步操作,如发送Ajax请求
  4. modules:当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module),
  • modules代码示例
const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}
const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}
const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

3、使用Vuex--store下的index.ts

import Vue from 'vue';
import Vuex from 'vuex';
import clone from '@/lib/clone';
import createId from '@/lib/createId';
import router from '@/router';
Vue.use(Vuex);  
const store = new Vuex.Store({
    state: {
        recordList: [],
        tagList: [],
        currentTag: undefined,
        isCreateRecordError:null,
        isCreateTagError:null,
    } as rootState,
    mutations: {
        setCurrentTag(state, id: string) {
            state.currentTag = state.tagList.filter(t => t.id === id)[0];
        },
        fetchTags(state) {
            state.tagList = JSON.parse(localStorage.getItem('tagList') || '[]');
            if(!state.tagList.length){
                store.commit('createTag','衣');
                store.commit('createTag','食');
                store.commit('createTag','住');
                store.commit('createTag','行');
            }
        },
        fetchRecords(state) {
            state.recordList = JSON.parse(localStorage.getItem('recordList') || '[]') as RecordItem[];
        },
        createdRecord(state, record:RecordItem) {
            const record2 = clone(record);
            /* toISOString是日期专有的字符串形式 */
            record2.createTime = new Date().toISOString();
            state.recordList.push(record2);
            store.commit('saveRecords');
        },
        createTag(state, name: string) {
            const names = state.tagList.map(item => item.name);
            if (names.indexOf(name) >= 0) {
                state.isCreateTagError=new Error('tag name duplicated');
                return ;
            } else {
                const id = createId().toString();
                state.tagList.push({id: id, name: name});
                store.commit('saveTags');
            }
        },
        updateTag(state, payload: Tag) {
            const {id,name} = payload;
            const idList = state.tagList.map(item => item.id);
            if (idList.indexOf(id) >= 0) {
                const names = state.tagList.map(item => item.name);
                if (names.indexOf(name) >= 0) {
                    window.alert('标签名重复!')
                } else {
                    const tag = state.tagList.filter(item => item.id === id)[0];
                    tag.name = name;
                    store.commit('saveTags');
                }
            }
        },
        removeTag(state,id: string){
            let index=-1;
            for (let i = 0; i < state.tagList.length; i++) {
                if(state.tagList[i].id===id){
                    index=i;
                    break;
                }
            }
            if(index>=0){
                state.tagList.splice(index,1)
                store.commit('saveTags');
                router.back();
            }
            else {
                window.alert('删除失败!');
            }
        },
        saveRecords(state) {
            localStorage.setItem('recordList', JSON.stringify(state.recordList));
        },
        saveTags(state) {
            localStorage.setItem('tagList', JSON.stringify(state.tagList));
        },
    },
    actions: {},
    modules: {}
});
export default store;

使用Vuex小结

  1. store里面的state和mutations就类似于Vue组件的data和methods
  2. 在mutations中写函数方法。
  3. mutations中的函数至多接受两个参数,第一个参数默认就是state,但是你要使用state,也要进行参数声明。第二个参数就是你自己额外想要传递的参数了。
  4. 使用commit在store中触发mutations中自己封装的函数

4、在组件中使用store

  • 在对象中读操作 -- 使用computed
<script lang='ts/js'>
 import {Component} from 'vue-property-decorator';
 @Component({
    computed:{
		/* 在计算属性computed中写函数的形式表示该变量 */
		tagList(){
			/* 在store的state中有个tagList变量名 */
			return this.$store.state.tagList;
		}
	}
  })
  export default class Money extends Vue{
	  .
	  .
	  .
  }
</script>
  • 在TS/JS类中读操作 -- 使用getter
<script lang='ts/js'>
 import {Component} from 'vue-property-decorator';
 @Component
  export default class Money extends Vue{
	  get tagList(){
		  return this.$store.state.tagList;
	  }
  }
</script>
  • 对store中的数据进行写操作 -- commit
// 1、Vuex的commit
store.commit('<函数名>',参数payload)

组件中使用store小结

  1. 读操作:①对象使用computed计算属性。②TS/JS类使用getter。
  2. 写操作:使用store的commmit接口,在mutations中自己封装写操作的函数,然后调用commit告诉Vuex中的store触发该函数进行写数据。

十九、Vue修饰符

修饰符就是为了简化某种操作过程,它实际上是一些操作的语法糖

1、.native

使用情景

将原生事件绑定到组件上。例如,你封装了一个Button组件,里面包含的是一个按钮<button></button>,当你点击页面上的按钮时,你会发现触发不了按钮的点击事件,这是因为你点击的是Button组件而不是<button></button>,所以触发不了<button></button>的事件

解决方案:

  1. 使用$emit('<eventName>',$event) 触发传递事件给 Button组件标签。
  2. 使用.native修饰符

代码演示

  • Button.vue组件
<template>
	<button class="create">
    <slot />
  </button>
</template>
<script lang="ts">
	import Vue from 'vue';
	import {Component} from 'vue-property-decorator';
	@Component
	export default class Button extends Vue{
		.
		.
		.
	}
</script>
<style lang="scss" scoped>
	.create{
    background: aqua;
    border-radius: 4px;
    height: 40px;
    padding: 0 16px;
  }
</style>
  • 在EditLabel.vue中使用Button.vue组件
<template>
  <Layout>
    <div class="button-wrapper">
      <Button @click.native="remove">
	  删除标签
	  </Button>
    </div>
  </Layout>
</template>
<script lang="ts">
  import Vue from 'vue';
  import {Component} from 'vue-property-decorator';
  import FormItem from '@/components/Money/FormItem.vue';
  import Button from '@/components/Button.vue';
  @Component({
    components: {Button, FormItem},
  })
  export default class EditLabel extends Vue {
    remove() {
      if (this.currentTag) {
        this.$store.commit('removeTag', this.currentTag.id);
      }
    }
  }
</script>

2、.sync

使用情景

在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。一般使用以 update:myPropName 的模式触发事件取而代之。然后父组件可以监听那个事件并根据需要更新一个本地的数据 property。

上面所说的这种模式就是.sync修饰符,就是子组件调用父组件的数据,某种情况下子组件需要对数据进行操作,就触发事件来通知父组件来修改,并且父组件要拿到更新数据变更后的值。

代码演示

  • Tags.vue组件 -- 子组件
<template>
	 <div class="tags">
        <div class="new">
          <button @click="createTag">新增标签</button>
        </div>
        <ul class="current">
          <li v-for="item in tagList"
              :key="item.id"
              :class="{selected:isSelected(item)}"
              @click="toggleSelect(item)"
          >
            <span>{{item.name}}</span>
          </li>
        </ul>
      </div>
</template>
<script lang="ts">
import {Component} from 'vue-property-decorator';
import {TagHelper} from '@/mixins/TagHelper.ts';
import {mixins} from 'vue-class-component';
@Component
export default class Tags extends mixins(TagHelper){
  get tagList(){
    return this.$store.state.tagList;
  }
  selectedTags:Tag[]=[];
  created() {
    // 初始化数据
    this.$store.commit('fetchTags');
  }
  toggleSelect(tag:Tag){
    const index=this.selectedTags.indexOf(tag);
    if(index>=0){
      this.selectedTags.splice(index,1);
    }
    else {
      this.selectedTags.push(tag);
	}
	/* 在这里触发update:value事件 */
    this.$emit('update:value', this.selectedTags)
  }
  isSelected(item:Tag){
    const index=this.selectedTags.indexOf(item);
    return index >= 0;
  }
};
</script>
  • Money.vue中使用Tags.vue组件
<template>
  <Layout class-prefix="layout">
  	<!-- Tags组件中绑定的value使用.sync实现双向绑定 -->
    <Tags :value.sync="record.tags"/>
  </Layout>
</template

二十、ISO8601和dayjs

国际标准ISO 8601,是国际标准化组织的日期和时间的表示方法,全称为《数据存储和交换形式·信息交换·日期和时间的表示方法》。目前是2004年12月1日发行的第三版“ISO8601:2004”以替代1998年的第一版“ISO8601:1998”与2000年的第二版“ISO8601:2000”。

1、日期和时间组合表示法

合并表示时,要在时间前面加一大写字母T,如要表示东八区时间2004年5月3日下午5点30分8秒,可以写成2004-05-03T17:30:08+08:00或20040503T173008+08

2、操作时间的两个库moment.jsday.js

顺便说一下,momentjs和dayjs相比体积太大了。我们一般选择使用dayjs

  • 安装dayjs
npm install dayjs 
# yarn add dayjs
  • 导入dayjs
var dayjs = require('dayjs')
//import dayjs from 'dayjs' // ES 2015
  • 简单的dayjs API用法:判断今天,昨天,前天
beautify(date:string){
    /* dayjs的相关总结,before和after只是判断在前或在后,没有对应的准确昨天和后天之说。 */
    // 还是要通过isSame来判断。而且dayjs()提供了subtract的API,参数1位减去的数字,参数2位减去的年月日等。
    const now=new Date();
    if(dayjs(date).isSame(now,'day')){
      return '今天';
    }
    else if(dayjs(date).isSame(dayjs().subtract(1,'day'),'day')){
      return '昨天';
    }
    else if(dayjs(date).isSame(dayjs().subtract(2,'day'),'day')){
      return '前天';
    }
    else {
      return dayjs(date).format('YYYY年M月D日');
    }
  }

二十一、使用loaclStorage存储数据

localStorage存储数据是浏览器的缓存,刷新后数据不变化,但是清除缓存或浏览器数据时就会受影响,就是单机版存储数据

localStorage用法

二十二、代码仓库

1、记账应用预览

2、源代码仓库