Daniel Huo 的个人网站

  • Home

  • About

  • Tags

  • Categories

  • Archives

一个简单案例教你如何用Typescript写Vuex

Posted on 2019-06-18 | Edited on 2019-06-19 | Comments:

案例源代码: https://github.com/danielhuoo/vue2.x-vuex-typescript-demo

前言

相信很多人都像我一样,学习使用了vuex后,想把项目改写成Typescript。但是官方教程要么晦涩难懂,要么缺少鲜活的例子。我花了一天时间,总结出了一些经验。在此分享出来。

本教程通过编写一个简单的demo讲解vuex的实现方式,以及如何对基于vue2.x的已有项目进行Typescript重构。

项目初始化

现在都9012了,所以我们直接使用vue-cli 3.x快速搭建系统。

1
2
3
4
5
6
7
8
# 搭建项目
vue create vue2.x-vuex-typescript-demo

cd vue2.x-vuex-typescript-demo
# 引入vuex
vue add vuex
# 由于我实在不想写任何样式,所以我又加一个element
vue add element

模块说明

为了用实际的代码解释vuex是如何搭建的,以及模块间的通讯方式,我用了一个很浅显的例子(应该比官方的例子明朗很多)

情景

男孩给女孩送花。

  1. 男孩每送出10朵花,女孩会表达感谢。
  2. 女孩的感谢会增加男孩的勇气值。
  3. 男孩可以向花店买花。

目录结构

你会发现默认的目录结构是这样的:

.
├── README.md
├── babel.config.js
├── package.json
├── public
│   ├── favicon.ico
│   └── index.html
├── src
│   ├── App.vue
│   ├── assets
│   │   └── logo.png
│   ├── components
│   │   └── HelloWorld.vue
│   ├── main.js
│   ├── plugins
│   │   └── element.js
│   └── store.js
└── yarn.lock

但是我们想让vuex变得模块化。所以我们改成以下的结构:

.
├── README.md
├── babel.config.js
├── package.json
├── public
│   ├── favicon.ico
│   └── index.html
├── src
│   ├── App.vue
│   ├── assets
│   │   └── logo.png
│   ├── components
│   │   └── HelloWorld.vue
│   ├── main.js
│   ├── plugins
│   │   └── element.js
│   └── store
│       ├── index.js
│       └── module
│           ├── boy.js
│           └── girl.js
└── yarn.lock
  1. index.js 是store的主文件
  2. /module 下存放模块文件。 boy.js 是男孩模块,girl.js 是女孩模块

模块定义

boy.js

该模块定义了三个action方法。action通俗来说就是你想让模块做的事情,它们可以是异步或者同步的。所有对state的增删查改的逻辑都应该在这里,而mutation仅仅负责执行增删查改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import { Message } from 'element-ui';
export default {
namespaced: true,
// state 的属性只能通过 mutation的方法进行修改
state: {
currentFlower: 50,
braveScore: 0
},
mutations: {
// 修改 state 的 currentFlower 的值
updateCurrentFlower(state, payload) {
state.currentFlower = state.currentFlower + payload
},
// 修改 state 的 braveScore 的值
updateBraveScore(state, payload) {
state.braveScore = state.braveScore + payload.score
}
},
actions: {
// 送花
// 方法里 调用了 commit 和 state,需要在传参时声明
sendFlower({ commit, state }, params) {
if (!state.currentFlower) {
Message({
showClose: true,
message: "没花可送了",
type: "warning"
});
} else {
// 送出一朵花,自己的库存减 1
commit('updateCurrentFlower', -params.sendNumber)
// 女孩收到一朵花,女孩库存加 1。
// 注意这里是跨模块调用,所以需要加上模块前缀 'girl/',并且 传入参数 {root:true} 表明通过根路径寻找目标函数。
commit('girl/updateCurrentFlower', params.sendNumber, { root: true })
}
},
// 受到鼓励
beEncouraged({ commit }) {
commit('updateBraveScore', { score: 10 })
},
// 买花
// 方法里调用了 commit, dispatch。 dispatch跨模块调用根store的action,跟送花的commit一样,需要加上前缀和传入{root:true}
buyFlower({ commit, dispatch }, params) {
setTimeout(() => {
dispatch('sellFlower', null, { root: true }).then(() => {
commit('updateCurrentFlower', params.buyNumber)
}).catch(() => {
Message({
showClose: true,
message: "库存不足",
type: "warning"
});
})
}, 100)
}
}
}

girl.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default {
namespaced: true,
state: {
currentFlower: 0
},
mutations: {
updateCurrentFlower(state, payload) {
state.currentFlower = state.currentFlower + payload
}
},
actions: {
// 对男孩进行鼓舞
encourage({ dispatch }, params) {
dispatch('boy/beEncouraged', null, { root: true })
}
}
}

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import Vue from 'vue'
import Vuex from 'vuex'
// 引入模块
import boy from './module/boy'
import girl from './module/girl'
Vue.use(Vuex)

export default new Vuex.Store({
// 根 state
state: {
flowersInStock: 10
},
// 根 mutations
mutations: {
updateFlowersInStock(state, payload) {
state.flowersInStock = state.flowersInStock + payload
}
},
// 根 actions
actions: {
sellFlower({ commit, state }, params) {
return new Promise((resolve, reject) => {
if (state.flowersInStock > 0) {
commit('updateFlowersInStock', -1)
resolve()
} else {
reject()
}
})
}
},
// 注册模块
modules: {
boy,
girl
}
})

连接到vue组件

现在仓库的逻辑已经写好了,我们就可以在组件上使用了。实际上vuex仓库早在main.js被引入了vue实例里了。例如,this.$store.state.flowersInStock即代表根state的属性值。但是这种写法太过繁琐,我们引入了vuex提供的 mapState、mapActions 和 mapMutations 进行映射。

boy.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<div>
<div>男孩</div>
<div>手上有{{currentFlower}}朵花</div>
<div>
<el-button @click="sendFlower({sendNumber:1})">送花</el-button>
<el-button @click="buyFlower({buyNumber:1})">买花</el-button>
</div>
<div>勇气值:{{braveScore}}</div>
</div>
</template>
<script>
import { mapState, mapActions } from "vuex";
export default {
computed: {
// 你会发现state的映射放在了computed里面。这么做的好处是由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态。
// 通过映射,this.$store.state.currentFlower 就可以表示为 this.currentFlower
...mapState("boy", {
currentFlower: state => state.currentFlower,
braveScore: state => state.braveScore
})
},

methods: {
// actions 放在了methods里面。这不奇怪,因为actions跟mutations一样,都是vuex里面的方法。
...mapActions("boy", ["sendFlower", "buyFlower"])
}
};
</script>
<style>
</style>

很多人在刚开始用vuex都会记不住,究竟state、actions和mutations放哪里。其实很好记:

  • state是属性,放computed里。
  • actions和mutations是方法,放methods里。

girl.vue 同理,就不赘述了。下一步,我们开始用Typescript改写代码。

安装Typescript

在安装之前,请一定要先做备份。因为安装后App.vue会被改写。

1
2
3
4
yarn add vuex-class
vue add typescript
? Use class-style component syntax? (Y/n) Yes
? Use Babel alongside TypeScript for auto-detected polyfills? (Y/n) Yes

改写开始

你会发现所有.js文件都被改成.ts后缀了。这时候整个项目是跑不起来的。命令行控制台会爆出几十个error。事实上,在你没有把所有该改的地方改好之前,项目是不会跑通的。

index.ts

被改写的地方:

  • 引入module的方式。改为import对象中的一个属性
  • 定义了store的类别。
  • 新增了一个RootState。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    import Vue from 'vue'
    import Vuex, { StoreOptions } from 'vuex'
    import { boy } from './module/boy'
    import { girl } from './module/girl'
    import { RootState } from './root-types';
    Vue.use(Vuex)
    const store: StoreOptions<RootState> = {
    // 里面的内容不用修改
    state: {
    flowersInStock: 10
    },
    modules: {
    boy,
    girl
    },
    mutations: {
    updateFlowersInStock(state, payload) {
    state.flowersInStock = state.flowersInStock + payload
    }
    },
    actions: {
    sellFlower({ commit, state }) {
    return new Promise((resolve, reject) => {
    if (state.flowersInStock > 0) {
    commit('updateFlowersInStock', -1)
    resolve()
    } else {
    reject()
    }
    })
    }
    }
    }
    export default new Vuex.Store<RootState>(store)

root-types.ts

这是对根state的约束

1
2
3
export interface RootState {
flowersInStock: number
}

boy.ts

模块的改动是巨大的。

  • 新增了模块的State接口
  • 定义mutations的类为 MutationTree
  • 定义actions的类为 ActionTree
  • 定义模块的类为 Module
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    import { Message } from 'element-ui';
    import { BoyState } from './module-types';
    import { MutationTree, ActionTree, Module } from 'vuex';
    import { RootState } from '../root-types';
    const state: BoyState = {
    currentFlower: 50,
    braveScore: 0
    }

    // 传入的泛型可以通过查看源代码得知。
    const mutations: MutationTree<BoyState> = {
    updateCurrentFlower(state, payload) {
    state.currentFlower = state.currentFlower + payload
    },

    updateBraveScore(state, payload) {
    state.braveScore = state.braveScore + payload.score
    }
    }
    const actions: ActionTree<BoyState, RootState> = {
    sendFlower({ commit, state }, params) {
    if (!state.currentFlower) {
    Message({
    showClose: true,
    message: "没花可送了",
    type: "warning"
    });
    } else {
    commit('updateCurrentFlower', -params.sendNumber)
    commit('girl/updateCurrentFlower', params.sendNumber, { root: true })
    }

    },
    buyFlower({ commit, dispatch }, params) {
    setTimeout(() => {
    dispatch('sellFlower', null, { root: true }).then(() => {
    commit('updateCurrentFlower', params.buyNumber)
    }).catch(() => {
    Message({
    showClose: true,
    message: "库存不足",
    type: "warning"
    });
    })
    }, 100)
    },
    beEncouraged({ commit }) {
    commit('updateBraveScore', { score: 10 })
    }
    }
    export const boy: Module<BoyState, RootState> = {
    namespaced: true,
    state,
    mutations,
    actions
    }

boy.vue

vue文件改动的地方也是很多的:

  • script标签指定了ts语言
  • 使用Component修饰组件
  • export 组件 从 对象变为 类
  • 弃用 mapState 等方法,使用 State、Action、Mutation 修饰器绑定 vuex
  • 弃用computed、methods、data 等写法,使用get + 方法表示 computed,methods里的方法直接被抽出来,data的属性直接被抽出来。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <script lang="ts">
    import { Vue, Component, Watch } from "vue-property-decorator";
    import { State, Action, Mutation, namespace } from "vuex-class";
    import { BoyState } from "../store/module/module-types";
    @Component
    export default class boyComponent extends Vue {
    @State("boy")
    // 感叹号不能省略
    boyState!: BoyState;

    @Action("sendFlower", { namespace: "boy" })
    sendFlower: any;

    @Action("buyFlower", { namespace: "boy" })
    buyFlower: any;

    get currentFlower(): number {
    return this.boyState.currentFlower;
    }

    get braveScore(): number {
    return this.boyState.braveScore;
    }
    }
    </script>

其他文件也是用类似的方法去改写。换汤不换药。

以上就是Typescript改写的例子。有些地方没有解释得很清楚,因为我也是一个小白啊,不懂的地方还是不要误导大家了。如果你的项目的逻辑比这个更复杂(肯定吧),而本项目没有覆盖到你的疑惑,你可以去看我的另一个改好的项目Jessic。

GCP实例创建指南

Posted on 2019-05-29 | Edited on 2019-06-19 | Comments:

GCP实例创建指南

注册Google Cloud Platform很快,但是花了大半天时间配置SSH环境。
所以下面对这个过程做一个记录


进入实例列表页面。

操作:Compute Engine => VM 实例 => 创建实例


配置实例

操作:

  1. 名称:实例名字,看你喜好
  2. 区域和地区:看你喜好
  3. CPU和内存:根据实际用途配置
  4. 启动硬盘:看你喜好
  5. 防火墙:两个都允许
  6. 点击”安全”


粘贴密钥串。

这里是我花了最多时间的地方,需要重点讲解。

这里的SSH配置是为了后续用Mac(Windows/Linux同理)的Terminal SSH到服务器的。

要获得这串密钥,需要在Mac的Terminal生成一对密钥。

ssh-keygen -t rsa -f ~/.ssh/[KEY_FILENAME] -C [USERNAME]

KEY_FILENAME => 密钥文件名,假设为 gcp
USERNAME => 登陆SSH的用户名,假设为 gcpuser

运行后会发现 .ssh/ 下多了两个文件,一个是私钥文件,另一个是以pub格式结尾的公钥文件。
到这里其实意图很明确了。我们需要把公钥文件放到服务器上,当用Mac SSH到服务器的时候,会自动通过私钥进行校验。

使用编辑器打开公钥,复制里面的信息到下图即可。


创建外部IP

网络的配置可以先忽略。点击创建,10秒左右实例就创建好了。你会发现实例列表中出现了刚刚创建的实例,它有一个临时的外部IP地址。接下来我们去创建固定的外部IP。

操作:VPC网络 => 外部IP地址 => 保留静态地址

配置:
名称和说明:个人喜好
IP版本:IPv4
类型:区域
区域:个人喜好


分配外部IP


登陆服务器

登陆后你会发现,用户:gcpuser已经创建好了


设置root密码
1
2
sudo su
passwd


修改SSH配置
1
nano /etc/ssh/sshd_config
1
2
PermitRootLogin no # 可以”yes“。 但是推荐 “no”,这样禁止从第三方ssh工具直接以root登陆
PasswordAuthentication yes

重启SSH服务
1
/etc/init.d/ssh restart

修改用户密码

这时候你会发现,使用Mac的Terminal还是无法SSH到服务器的,因为gcpuser没有设置密码。我暂时也不知道原因。照理说公钥已经有密码信息了。
接下来我们直接重置一下它的密码:

1
sudo passwd gcpuser

完成

现在你可以SSH上去了。这方面的教程有很多,不再赘述。

Daniel Huo

Daniel Huo

Rome wasn't built in a day.
2 posts
7 tags
GitHub E-Mail
© 2019 Daniel Huo