# Vue3 小菠萝 Pinia使用
Vue3 Pinia使用
在学习 Vue2 的宝子们一定都知道,在 vue2 版本中,如果想要使用状态管理器,那么一定是集成 Vuex,首先说明一点,Vuex 在 vue3 项目中依旧是可以正常使用的,是 vue 项目的正规军。但是,今天我们学习一下小菠萝,Pinia 目前也已经是 vue 官方正式的状态库。适用于 vue2 和 vue3。可以简单的理解成 Pinia 就是 Vuex5。也就是说, Vue3 项目,建议使用Pinia,当然很多公司或者是项目由 vue2 转为 vue3 之后,由于习惯了使用 vuex ,所以说,在 vue3 当中继续使用 vuex 的,也不是少数,都知道就可以,根据实际情况来选择。
什么是 Pinia
Pinia 是 Vue 的存储库,它允许您跨组件/页面共享状态。Pinia 的成功可以归功于他管理存储数据的独特功能,例如:可扩展性、存储模块组织、状态变化分组、多存储创建等。
Pinia 的优点
Pinia 被 vue 纳入正规编制,肯定是有原因的,那 pinia 有啥优点呢,主要是一下几点:
- pinia 符合直觉,易于学习。
- pinia 是轻量级状态管理工具,大小只有1KB.
- pinia 模块化设计,方便拆分。
- pinia 没有 mutations,直接在 actions 中操作 state,通过 this.xxx 访问响应的状态,尽管可以直接操作 state,但是还是推荐在 actions 中操作,保证状态不被意外的改变。
- store 的 action 被调度为常规的函数调用,而不是使用 dispatch 方法或者是 MapAction 辅助函数,这是在 Vuex 中很常见的。
- 支持多个 store。
- 支持 Vue devtools、SSR、webpack 代码拆分。
相关资料
Pinia 中文网:https://pinia.web3doc.top/
Pinia 安装
安装 pinia 就很简单了,直接命令安装就可以了。
npm install pinia -save
或者
yarn add pinia
Pinia 使用
安装完 pinia ,然后就是使用了,使用的第一步,就是在项目中引入 pinia。
Pinia 导入
首先在 main.js 文件中引入,很简单,不要慌宝子们。
import {createPinia} from 'pinia'
然后,这个 pinia 就在项目中导入了,但是上面是 vue3 的写法哈,我起的这个项目是 vue3 的。
Pinia 是支持 vue2 的,如果是 vue2 的项目,导入的方式是下面的样子:
import {PiniaVuePlugin} from 'pinia'
好的,我们还是以 vue3 来介绍这个 Pinia。
导入的时候是 hook ,我们需要调用一下
const state = createPinia()
调用完成,state 是以插件的形式存在的,所以说最后我们需要在项目使用一下。
app.use(state)
好的,编写完上边这一大堆,我们就实现了在 vue3 项目中导入 pinia 的全部操作。
接下来就可以具体的使用 pinia 了。
Pinia 基本使用
创建 index.ts 文件
使用起来相对简单一些,我们首先在根目录下创建一个 store 文件夹,这个都晓得哈,当年用 vuex 的时候也是这个结构。毕竟 pinia 就是用来替换掉 vuex 的嘛。
创建完 store 文件夹,在里面创建一个 ts 文件,叫做 index.ts 。
编写 index.ts 文件
然后我们开始编写 index.ts 文件。
首先我们先引入 pinia。
import { defineStore } from "pinia";
由于 defineStore 也是一个 hooks ,所以说我们可以直接导出一下。
export const Test = defineStore()
这样子写是会报错的,因为这个 defineStore 是需要传参数的,其中第一个参数是id,就是一个唯一的值,简单点说就可以理解成是一个命名空间,我们可以写一个枚举再传值。
我们在同级在创建一个名字叫做 store_name.ts 的文件写一个枚举数据导出。
export const enum Names {
TEST = "TEST"
}
然后在 index.ts 文件中引入一下枚举数据,然后传给这个 defineStore。
import { defineStore } from "pinia";
import { Names } from "./store_name";
export const useInfoStore = defineStore(Names.TEST)
ok, 这个样子还是报错的,因为还有其他的参数需要传递,第二个参数就是一个对象,里面有三个模块需要处理,第一个是 state,第二个是 getters,第三个是 actions。
- state 和之前我们 vuex 里面的写法是不一样的,在 vuex 里面呢,state 是一个对象,但是在 pinia 中,state 必须是一个箭头函数,然后在返回一个对象。
- getters 模块呢,类似于计算属性,是有缓存的,主要是帮助我们修饰一些值。
- actions 呢,类似于 methods,帮助我们做一些同步的或者是异步的操作,提交 state 之类的。
到此完整的 index.ts 文件代码:
import { defineStore } from "pinia";
import { Names } from "./store_name";
export const useInfoStore = defineStore(Names.TEST, {
state: () => {
return {
name: '我是𝒆𝒅.',
age: 10,
}
},
getters: {
},
actions: {
}
})
好的,其实到这个地方为止呢,其实我们已经可以使用 pinia 了,我们写一个页面使用一下 pinia 的值。
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
</script>
<style scoped>
</style>
保存代码,刷新一下页面,我们可以看到,数据已经渲染在界面上了。
如果我们有 devtools 工具的话,我们可以看见我们的数据是可以查询到的。
未完待续~
继续
修改 Pinia 的值
修改 Pinia 的值有很多中方式,我们一个一个来。
修改 pinia 的值,其实就是修改 state 的值,上边的案例我们写了一个 state,里面有两个值,一个是 name, 一个是 age,今天我们的任务就是实现修改 pinia 中 state 的值。
修改值的方式呢,常见的有五种,我们一个一个看。
方式一:直接修改
这种方式呢,非常的简单粗暴,直接修改就可以了。
比如,上边我们页面上有一个按钮,点击按钮的时候实现 age 加一操作。
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="changeAge">年龄+1</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
const changeAge = () => {
userInfo.age++
}
</script>
<style scoped>
</style>
保存刷新页面,我们看一下效果。
OK,成功实现了点击按钮的时候,age 自动 + 1 操作。
注意哈: 在 vuex 里面是坚决不允许这样子直接操作 state 数据的,但是小菠萝是可以允许滴!
方式二:$patch 函数修改
在我们实例化 const userInfo = useInfoStore()
这个 state 的时候,其实这个 userInfo
中,有一个方法,就是 patch 函数,它可以帮助我们批量修改。比如点击按钮,同时修改 name 和 age 的值,直接上代码:
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="change">修改name和age</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
// 方式二: 通过 patch 函数批量修改 name 和 age
const change = () => {
userInfo.$patch({
age: 11,
name: '𝒆𝒅.'
})
}
</script>
<style scoped>
</style>
保存刷新页面,开始测试效果。
好的,通过使用 patch 函数实现批量修改 state 的数据。
方式三:$patch 函数修改
哎呦喂,为什么和第二种一样,没错滴宝子们,和第二种一样都是使用 patch 函数来实现修改,但是有区别,方式二实在 patch 函数中传入修改的对象值,但是这种方式传入的是一个函数,作用是啥子呢?就是可以进行逻辑操作,比如说判断之类的。
上代码:
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="change">修改name和age</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
// 方式三
const change = () => {
userInfo.$patch((state) => { // 这里传入的state就是pinia的state
state.age = 11
state.name = '𝒆𝒅.'
})
}
</script>
<style scoped>
</style>
保存看一下效果,其实和方式二是一模一样的。
好的,这就是通过 patch 传入函数的方式修改 state 内容。
方式四:$state 方式
首先说一点儿哈,第四种方式不是很常用。为啥呢?尽管它也可以修改 state 的值,但是这种方式有一个很大的弊端,什么弊端呢,与其说是修改 state 的值,倒不如说直接替换掉 state,什么意思呢?简单点说,比如上边的 state 里面有两个值,一个是 name,一个是 age,如果我们只想修改 age 的值,那么我们必须把 age 和 name 的值都写上才可以,如果只写 age ,那么 name 的值就没有了,理解吧?直接看代码。
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="change">修改name和age</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
// 方式四:state
const change = () => {
userInfo.$state = {
name: '𝒆𝒅.',
age: 11
}
}
</script>
<style scoped>
</style>
保存刷新页面,点击按钮查看效果,其实和上面的案例是一样的,可以修改 name 和 age。
我们可以看到,把数据全部传进去,数据改了。
方式五: action 方式
这个方式我们需要借助 actions 来实现,所以说我们需要去 store 文件夹下的 index.ts 文件中写一个 action。
import { defineStore } from "pinia";
import { Names } from "./store_name";
export const useInfoStore = defineStore(Names.TEST, {
state: () => {
return {
name: '我是𝒆𝒅.',
age: 10,
}
},
getters: {
},
actions: {
setAge () { // 注意,这里就不要写箭头函数了,不然 this 指向会出问题。
this.age = 11
}
}
})
写完 action 我们就可以使用 action 的方式修改 age 的值了。怎么使用呢,我们只需要使用实例去调用一下我们刚才写的 action 函数就行了。
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="change">修改 age</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
// 方式五
const change = () => {
userInfo.setAge()
}
</script>
<style scoped>
</style>
保存看一下效果。
好的,这个就是通过 action 方式修改。
当然了,这个 action 是可以传参的,我们修改一下 index.ts 文件编写的 action 函数,让他接受一个参数再赋值。
actions: {
setAge(num: number) { // 注意,这里就不要写箭头函数了,不然 this 指向会出问题。
this.age = num
}
}
然后我们页面代码直接传进去参数就可以了。
// 方式五
const change = () => {
userInfo.setAge(12)
}
修改完保存刷新,看一下效果。
好了,上边就是常见的五种修改 state 数据的方式,可以根据自己的实际情况选择使用。
pinia 解构
本来不想写了,但是还是稍微说一下吧。
上面的案例,我们实例化const userInfo = useInfoStore()
了以后呢,这个 userInfo
是可以继续解构操作的,这个也不用细说,会ES的都知道解构操作。
const {name, age} = userInfo
解构完成,我们看一下效果。
全部代码:
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>解构前</p>
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<p>解构后</p>
<p>pinia 中的信息: {{name}} --- {{age}}</p>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
const { name, age } = userInfo
</script>
<style scoped>
</style>
在页面分别展示一下解构前和解构后的数据,保存刷新看效果。
我们可以看到哈,就是结构前和结构后我们在页面都可以获取到数据展示。
但是有个问题哈,就是解构后的数据,是不具备响应式的,啥意思呢,就是我们修改了 state 的值,页面不会跟着变化,我们测试一下。
编写代码,点击按钮,age 加一,看一下结构前和解构后的数据在页面是否会实时渲染。
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>解构前</p>
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<p>解构后</p>
<p>pinia 中的信息: {{name}} --- {{age}}</p>
<el-button @click="change">Age +1</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
const { name, age } = userInfo
let change = () => {
userInfo.age++
}
</script>
<style scoped>
</style>
保存刷新,点击按钮查看效果。
通过测试我们可以看到,结构前的是可以实时渲染的,但是解构后的话是不可以的, 因为解构后的不是响应式数据。
怎么解决这个问题呢,其实也很简单哈,官方提供了了一个方法,可以把解构后的数据转换为响应式的数据。
就是 storeToRefs,使用 storeToRefs 需要导入一下。
import { storeToRefs } from 'pinia'
然后把我们解构的对象包裹一下就可以了,这个方法和 toRefs 是类型的,应该理解哈。
const { name, age } = storeToRefs(userInfo)
然后我们修改案例的代码,再看一下效果。
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>解构前</p>
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<p>解构后</p>
<p>pinia 中的信息: {{name}} --- {{age}}</p>
<el-button @click="change">Age +1</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 导入storeToRefs
import { storeToRefs } from 'pinia'
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
// 包裹一下结构对象
const { name, age } = storeToRefs(userInfo)
let change = () => {
userInfo.age++
}
</script>
<style scoped>
</style>
保存刷新一下。
我们可以看到,解构后的数据也已经变成响应式的了。
或者我们换一个写法,直接操作结构后的数据,记得,要 .value
。
let change = () => {
age.value++
}
效果是一样的。
Pinia 的 actions
actions 是可以处理同步,也可以处理异步,同步的话相对来说简单一点,上面我们通过 action 修改 state 的时候,就用到了 actions 的同步,这里就不再赘述了。
actions 异步
首先我们模拟一个异步函数,比如说登录。
type User = {
name: String,
age: Number
}
const Login = (): Promise<User> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
name: 'wjw',
age: 22
})
}, 2000)
})
}
然后我们在 actions 中就可以调用这个异步的操作了,在 actions 处理异步的时候呢,我们一般是与 async
和 await
连用。
所有 index.ts 文件代码:
import { defineStore } from "pinia";
import { Names } from "./store_name";
type User = {
name: String,
age: Number
}
const Login = (): Promise<User> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
name: 'wjw',
age: 22
})
}, 2000)
})
}
export const useInfoStore = defineStore(Names.TEST, {
state: () => {
return {
name: '我是𝒆𝒅.',
age: 10,
}
},
getters: {
},
actions: {
async setUser() {
const result = await Login()
this.name = result.name
this.age = result.age
}
}
})
然后我们修改一下使用的页面也修改一下。
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="change">修改用户内容</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
let change = () => {
userInfo.setUser()
}
</script>
<style scoped>
</style>
保存刷新,点击按钮,等两秒钟假装请求,然后看数据变不变。
奈斯! nice! 完成异步案例!
actions 同步、异步连用
这个 actions 里面的方法函数是可以相互调用的,啥意思呢,就是你 actions 里面有好几个方法,这几个方法是可以调过来调过去的,但是注意哈,别玩嗨了,直接死循环了。看个案例:
上面的代码一改造,本来异步模拟获取的 age 数据是 22 ,然后我们调用一个 action 把 age 改成 80,这个是可以的哈。
import { defineStore } from "pinia";
import { Names } from "./store_name";
type User = {
name: String,
age: Number
}
const Login = (): Promise<User> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
name: 'wjw',
age: 22
})
}, 2000)
})
}
export const useInfoStore = defineStore(Names.TEST, {
state: () => {
return {
name: '我是𝒆𝒅.',
age: 10,
}
},
getters: {
},
actions: {
async setUser() {
const result = await Login()
this.name = result.name
this.age = result.age
this.setAge(80)
},
setAge(num: number) { // 注意,这里就不要写箭头函数了,不然 this 指向会出问题。
this.age = num
},
}
})
我们保存代码,刷新点击按钮等两秒钟然后看一下效果。
看到效果,改了吧! 啊哈哈哈哈,666!
getter 函数
接下来我们稍微过一下 getter 函数。
getters 类似于 vue 里面的计算属性,可以对已有的数据进行修饰。有两种写法,我们一个一个看哈。
普通函数方式写法
这个就比较简单了,直接上代码:
getters: {
newName() {
return `这是getter修饰过的名称 ${this.name}`
}
},
这个代码啥意思很简单是吧,就是在之前 name 的基础上拼接上了一个字符串,但是这样 ts 可能不会正确的进行数据类型转换,所以说我们可以加一个类型定义。
getters: {
newName():string {
return `这是getter修饰过的名称 ${this.name}`
}
},
告诉他,就是字符串类型。
然后这个 getter 就可以直接在模板上使用,直接改一下之前的让他在页面上展示出来。
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="change">修改用户内容</el-button>
<p>getter: {{userInfo.newName}}</p>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
let change = () => {
userInfo.setUser()
}
</script>
<style scoped>
</style>
保存刷新,看一下效果。
现在默认的数据是上面截图的样子,然后我们点一下按钮,修改一下 name,然后看一下效果。
我们可以看见,点击修改 name 之后呢,getter 也会实时的去渲染出来。
相互调用
嘻嘻,没错啦,这个 getter 也是可以像 actions 一样相互调用的啦!
看代码:
getters: {
newName():string {
return `这是getter修饰过的名称 ${this.name} ,他的年纪是 ${this.getAge}`
},
getAge():number {
return this.age
}
},
保存,刷新一下看效果。
也是可以的哈。
API 的使用
pinia 里面呢,有很多的 API,今天就稍微说几个常见的哈。
$reset :重置到初始值
这个 $reset 可以将 state 的数据初始到初始值,比如我们有一个数据,点击按钮改变了,然后我们可以通过这个 API ,将数据恢复到初始状态值。
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="change">修改用户内容</el-button>
<p>getter: {{userInfo.newName}}</p>
<el-button @click="reset">$reset</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
let change = () => {
userInfo.setUser()
}
// 重置
let reset = () => {
userInfo.$reset()
}
</script>
<style scoped>
</style>
我们先修改用户信息,然后在重置。
流弊!完美实现重置。
$subscribe:监听 state 数据变化
$subscribe 使用来监听的,监听 state 数据的变化,只要 state 里面的数据发生了变化,就会自动走这个函数。
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="change">修改用户内容</el-button>
<p>getter: {{userInfo.newName}}</p>
<el-button @click="reset">$reset</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
let change = () => {
userInfo.setUser()
}
// 重置
let reset = () => {
userInfo.$reset()
}
// 监听 state 的变化,返回一个工厂函数
userInfo.$subscribe((args, state) => {
console.log(args, state)
})
</script>
<style scoped>
</style>
监听函数写一下,然后保存刷新。
可以打印出数据,需要啥进去取就可以了。
$onAction:一调用 actions 就触发
这个看名字就很好理解了吧,就是 action 一调用就会被触发。
它里面只有一个参数 args。写一下关键代码吧。
userInfo.$onAction((args) => {
console.log(args)
})
保存刷新。
我们在点击的时候,之前案例是相互调用了两个 action,所以打印了两个。
其中打印出来的有一个 after ,这是回调,我们可以看一下,监听到了之后再走的回调,我们测试一下。
// $onAction
userInfo.$onAction((args) => {
args.after(() => {
console.log('after 回调')
})
console.log(args)
})
我们先打印了回调,在打印数据,保存看结果。
看到结果,反而是先输出了数据,在走的回调,理解了吧?
我们看到打印的数据还有一个 args,这个args 是 actions 传进来的参数。
好的,继续。
补充
补充一点哈,$onAction 刚才的案例我们只传了一个参数,就是一个工厂函数,其实他还有第二个参数—— true
,传 true 的意义是啥呢?就是当这个组件销毁了,这个 $onAction 还可以继续保活。
// $onAction
userInfo.$onAction((args) => {
args.after(() => {
console.log('after 回调')
})
console.log(args)
}, true)
不止 $onAction
可以传第二个参数,$subscribe
也有第二个参数,只不过 $subscribe 的参数是一个对象,对象里面设置的是 detached 为 true ,效果和 $onAction 是一样的,当然还有其它的参数,和 watch 是类似的。
userInfo.$subscribe((args, state) => {
console.log(args, state)
}, {
detached: true,
deep: true,
flush: 'post'
})
好了,关于常见的 API 也就这样了。
结束!
【重要说明】博文仅作为本人的学习记录,论点和观点仅代表个人而不代表技术的真理,目的是自我学习和有幸成为可以向他人分享的经验,因此有错误会虚心接受改正,但不代表此刻博文无误!
【博客园地址】叫我+V : http://www.cnblogs.com/wjw1014
【CSDN地址】叫我+V : https://wjw1014.blog.csdn.net/
【Gitee地址】叫我+V :https://gitee.com/wjw1014
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
2020-07-28 vue slot插槽
2020-07-28 Vue 组件化注意事项
2020-07-28 VUE多个组件示例
2020-07-28 Vue组件化开发
2020-07-28 Vue 获取当前时间并格式化
2018-07-28 小程序动态控制元素显隐