归然 發表於 2020-11-6 10:56:00

Vue3.x 从零开始(六)—— Router + Vuex + TypeScript 实战演练(下)

<p><span style="font-size: 14px">在上一篇实战演练中,已经将项目搭建好,并介绍了 Router、Vuex 的基本用法</span></p>
<p><span style="font-size: 14px">接下来会以一个 Todo List 项目来介绍实战中如何使用 Composition API</span></p>
<p><span style="font-size: 14px"><img src="https://img2020.cnblogs.com/blog/1059788/202011/1059788-20201103162702271-998278366.gif" alt="" loading="lazy">&nbsp;</span></p>
<p><span style="font-size: 16px"><strong>一、输入框与列表(按键别名 +&nbsp;computed 类型声明&nbsp;)</strong></span></p>
<p><span style="font-size: 14px">首先是输入框,由于需要支持回车键提交,所以需要监听 <span style="color: rgba(0, 128, 128, 1)"><strong>keydown</strong></span> 事件</span></p>
<p><span style="font-size: 14px"><img src="https://img2020.cnblogs.com/blog/1059788/202011/1059788-20201103164353926-1967468094.png" alt="" width="255" height="139" loading="lazy"></span></p>
<p><span style="font-size: 14px">如果是传统的按键处理,需要在事件对象中根据 keyCode 来判断按键</span></p>
<p><span style="font-size: 14px">Vue 提供了一些常用的按键修饰符,不用在事件处理函数中再做判断</span></p>
<p><span style="font-size: 14px">比如这里就使用了 enter 修饰符,直接监听 enter 键的 keydown 事件</span></p>
<hr>
<p><span style="font-size: 14px">列表部分,需要判断当前列表是否为空,如果为空则展示空状态</span></p>
<p><span style="font-size: 14px"><img src="https://img2020.cnblogs.com/blog/1059788/202011/1059788-20201103164936880-881567715.png" alt="" width="507" height="228" loading="lazy"></span></p>
<p><span style="font-size: 14px">这里使用了 v-if 和 v-else 来做条件判断,而其判断条件 <strong><span style="color: rgba(0, 128, 128, 1)">showList</span></strong> 是一个计算属性 computed</span></p>
<p><span style="font-size: 14px">在 TypeScript 的项目中,如果像 JS 项目一样添加计算属性,无法进行类型推断</span></p>
<p><span style="font-size: 14px"><img src="https://img2020.cnblogs.com/blog/1059788/202011/1059788-20201103165227639-1359821722.png" alt="" width="454" height="132" loading="lazy"></span></p>
<p><span style="font-size: 14px">需要加上类型声明:</span></p>
<p><span style="font-size: 14px"><img src="https://img2020.cnblogs.com/blog/1059788/202011/1059788-20201103165550373-123797037.png" alt="" width="450" height="119" loading="lazy"></span></p>
<p><span style="color: rgba(128, 128, 128, 1); font-size: 14px">// 在 setup() 中通过&nbsp;computed() 注册的计算属性不需要声明类型</span></p>
<p><span style="font-size: 14px">&nbsp;</span></p>
<p><span style="font-size: 14px">&nbsp;</span></p>
<p><span style="font-size: 16px"><strong>二、添加、删除条目(在 setup 中使用 vuex)</strong></span></p>
<p><span style="font-size: 14px">创建的条目需要保存到 store 中,首先需要定义条目类型</span></p>
<p><span style="font-size: 14px"><img src="https://img2020.cnblogs.com/blog/1059788/202011/1059788-20201103170340802-1514352920.png" alt="" width="400" height="162" loading="lazy"></span></p>
<p><span style="font-size: 14px">然后在 <span style="color: rgba(128, 0, 0, 1)"><strong>state</strong></span> 中新增 todoList 字段,用于保存列表</span></p>
<p><span style="font-size: 14px"><img src="https://img2020.cnblogs.com/blog/1059788/202011/1059788-20201103170936360-901811615.png" alt="" width="401" height="164" loading="lazy"></span></p>
<p><span style="font-size: 14px">这里还添加了一个 todoListMap 字段,是 todoList 的字典项,后面查找条目的时候会用到</span></p>
<p><span style="font-size: 14px">同时在&nbsp;<span style="color: rgba(128, 0, 0, 1)"><strong>mutations</strong></span> 中新增添加和删除方法</span></p>
<p><span style="font-size: 14px"><img src="https://img2020.cnblogs.com/blog/1059788/202011/1059788-20201103170816154-2007618808.png" alt="" width="557" height="252" loading="lazy"></span></p>
<p><span style="font-size: 14px">&nbsp;</span></p>
<p><span style="font-size: 14px">Store 已经调整好了,接下来只要在组件中调用即可</span></p>
<p><span style="font-size: 14px">可以像之前介绍的那样,使用&nbsp;<span style="color: rgba(0, 128, 128, 1)"><strong>mapState</strong></span> 和 <span style="color: rgba(0, 128, 128, 1)"><strong>mapMutations</strong></span> 来导出对应的字段和方法</span></p>
<p><span style="font-size: 14px">不过如果想<span style="color: rgba(128, 0, 0, 1)"><strong>在 setup 中使用 vuex</strong></span>,就需要用到 vuex 4 提供的 <span style="color: rgba(0, 128, 128, 1)"><strong>useStore</strong></span> 方法</span></p>
<div class="cnblogs_code">
<pre><span style="font-size: 14px">import { useStore } <span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">vuex</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;

export </span><span style="color: rgba(0, 0, 255, 1)">default</span><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">...</span>
<span style="color: rgba(0, 0, 0, 1)">setup() {
    </span><span style="color: rgba(0, 0, 255, 1)">const</span> store =<span style="color: rgba(0, 0, 0, 1)"> useStore();
    console.log(</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">store---&gt;</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">, store);
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> { ...store.state }
}
}</span></span></pre>
</div>
<p><span style="font-size: 14px"><img src="https://img2020.cnblogs.com/blog/1059788/202011/1059788-20201104104652759-806009782.png" alt="" loading="lazy"></span></p>
<p><span style="font-size: 14px">从截图可以看到,<span style="color: rgba(0, 128, 128, 1)"><strong>useStore()</strong></span> 的返回值其实就是&nbsp;<span style="color: rgba(0, 128, 128, 1)"><strong>$store</strong></span></span></p>
<p><span style="font-size: 14px">接下来的事情就简单了,手动导出需要用到的 state 和 mutations、actions 即可</span></p>
<p><span style="font-size: 14px"><img src="https://img2020.cnblogs.com/blog/1059788/202011/1059788-20201104111203992-866521725.png" alt="" width="520" height="351" loading="lazy"></span></p>
<p><span style="font-size: 14px">这种方式导出 state 还行,但对于 mutation 和 action,需要一个一个手动创建函数并导出,就比较笨重</span></p>
<p><span style="font-size: 14px">没关系,我们还有 mapMutations 和&nbsp;mapActions&nbsp;可以使用</span></p>
<p><span style="font-size: 14px"><img src="https://img2020.cnblogs.com/blog/1059788/202011/1059788-20201104111823217-70311341.png" alt="" width="565" height="259" loading="lazy"></span></p>
<p><span style="font-size: 14px">但需要注意,<span style="color: rgba(128, 0, 0, 1)"><strong>不要在 setup 使用 mapState!</strong></span></span></p>
<p><span style="font-size: 14px">因为 mapState 导出 state 是一个函数(computed),这个函数内部使用了 this.$store</span></p>
<p><span style="font-size: 14px">而 setup 中 this 是一个空值,所以在 setup 中使用 mapState 会报错</span></p>
<p><span style="font-size: 14px"><img src="https://img2020.cnblogs.com/blog/1059788/202011/1059788-20201104112145599-1085949315.png" alt="" loading="lazy"></span></p>
<p><span style="font-size: 14px">&nbsp;如果确实希望以 mapState 的形式在 setup 中导出 state,可以看一下&nbsp;vuex-composition-helpers</span></p>
<div class="cnblogs_code">
<pre><span style="font-size: 14px">import { useState, useActions } <span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">vuex-composition-helpers</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;

export </span><span style="color: rgba(0, 0, 255, 1)">default</span><span style="color: rgba(0, 0, 0, 1)"> {
    props: {
      articleId: String
    },
    setup(props) {
      </span><span style="color: rgba(0, 0, 255, 1)">const</span> { fetch } = useActions([<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">fetch</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">]);
      </span><span style="color: rgba(0, 0, 255, 1)">const</span> { article, comments } = useState([<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">article</span><span style="color: rgba(128, 0, 0, 1)">'</span>, <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">comments</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">]);
      fetch(props.articleId); </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> dispatch the "fetch" action</span>

      <span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> {
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> both are computed compositions for to the store</span>
<span style="color: rgba(0, 0, 0, 1)">            article,
            comments
      }
    }
}</span></span></pre>
</div>
<p><span style="font-size: 14px">&nbsp;</span></p>
<p><span style="font-size: 14px">&nbsp;</span></p>
<p><span style="font-size: 16px"><strong>三、查看条目详情(在 setup 中使用 router)</strong></span></p>
<p><span style="font-size: 14px">在条目详情页,可以在 url 上携带条目 id,然后通过 id 在 store 中找到对应的数据</span></p>
<p><span style="font-size: 14px">这就需要调整路由配置文件&nbsp;<span class="cnblogs_code">src/router/index.ts</span>,配置&nbsp;vue-router 中的动态路由</span></p>
<p><span style="font-size: 14px"><img src="https://img2020.cnblogs.com/blog/1059788/202011/1059788-20201104114020709-986023880.png" alt="" width="666" height="115" loading="lazy"></span></p>
<p><span style="font-size: 14px">路由配置好了,接下来需要在列表上添加“查看详情”按钮的处理函数&nbsp;</span></p>
<p><span style="font-size: 14px">如果这个函数写在 methods 里面,可以直接通过&nbsp;&nbsp;<span class="cnblogs_code"><span style="color: rgba(0, 0, 255, 1)">this</span>.$router.push()</span>&nbsp;来跳转页面</span></p>
<p><span style="font-size: 14px">但是在 setup 中,就需要用到 vue-router 提供的 <span style="color: rgba(0, 128, 128, 1)"><strong>useRouter</strong></span></span></p>
<div class="cnblogs_code">
<pre><span style="font-size: 14px">import { useRouter } <span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">vue-router</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;

export </span><span style="color: rgba(0, 0, 255, 1)">default</span><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> ...</span>
<span style="color: rgba(0, 0, 0, 1)">setup() {
    </span><span style="color: rgba(0, 0, 255, 1)">const</span> router =<span style="color: rgba(0, 0, 0, 1)"> useRouter();
    </span><span style="color: rgba(0, 0, 255, 1)">const</span> viewItem = (id: <span style="color: rgba(0, 0, 255, 1)">string</span>) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
      router.push(`</span>/about/<span style="color: rgba(0, 0, 0, 1)">${id}`);
    };
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> { viewItem };
}
}</span></span></pre>
</div>
<p><span style="font-size: 14px">然后在详情页,通过 <span style="color: rgba(0, 128, 128, 1)"><strong>useRoute</strong></span>(注意不是 <strong>userRouter</strong> )获取&nbsp;params</span></p>
<p><span style="font-size: 14px"><img src="https://img2020.cnblogs.com/blog/1059788/202011/1059788-20201104114800544-1327336799.png" alt="" width="569" height="376" loading="lazy"></span></p>
<p><span style="font-size: 14px">&nbsp;</span></p>
<p><span style="font-size: 14px">&nbsp;</span></p>
<p><span style="font-size: 16px"><strong>四、完全使用 Composition API 开发组件</strong></span></p>
<p><span style="font-size: 14px">以上都是在 setup 中使用 Composition API,整个组件本身依然是使用 Options API 开发</span></p>
<p><span style="font-size: 14px">想象一下,如果整个组件的 &lt;script&gt; 部分就是一个 setup 函数,会发生什么呢?</span></p>
<p><span style="font-size: 14px">给 &lt;script&gt; 标签加上 setup 修饰符试试!</span></p>
<div class="cnblogs_code">
<pre><span style="font-size: 14px"><span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">script </span><span style="color: rgba(255, 0, 0, 1)">lang</span><span style="color: rgba(0, 0, 255, 1)">="ts"</span><span style="color: rgba(255, 0, 0, 1)"> setup</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span>
<span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)"> ...</span>
<span style="color: rgba(0, 0, 255, 1)">&lt;/</span><span style="color: rgba(128, 0, 0, 1)">script</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span></span></pre>
</div>
<p><span style="font-size: 14px">然后把整个 &lt;script&gt; 当做 setup 函数,改写上面的详情页</span></p>
<p><span style="font-size: 14px"><img src="https://img2020.cnblogs.com/blog/1059788/202011/1059788-20201113110508685-2109359708.png" alt="" width="625" height="406" loading="lazy"></span></p>
<p><span style="font-size: 14px">这样就能完全使用 Composition API 来开发组件了</span></p>
<p><span style="font-size: 14px">和 setup 的区别在于,setup 最终需要 return 一个对象,而现在需要使用 export 来导出变量</span></p>
<p><span style="font-size: 14px">如果需要使用 setup 函数的参数 ( props 和 context ),可以这么写:</span></p>
<div class="cnblogs_code">
<pre><span style="font-size: 14px"><span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">script </span><span style="color: rgba(255, 0, 0, 1)">lang</span><span style="color: rgba(0, 0, 255, 1)">="ts"</span><span style="color: rgba(255, 0, 0, 1)"> setup</span><span style="color: rgba(0, 0, 255, 1)">="props, { emit }"</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span></span></pre>
</div>
<p><span style="font-size: 14px">等效于</span></p>
<div class="cnblogs_code">
<pre><span style="font-size: 14px"><span style="color: rgba(0, 0, 0, 1)"><span style="color: rgba(255, 0, 0, 1)">setup</span>(<span style="color: rgba(0, 0, 255, 1)">props</span>, { <span style="color: rgba(0, 0, 255, 1)">emit</span> }) {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> ...</span>
}</span></pre>
</div>
<p><span style="font-size: 14px">最后再贴一份 script-setup 的组件示例:</span></p>
<div class="cnblogs_code">
<pre><span style="font-size: 14px">&lt;script lang=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">ts</span><span style="color: rgba(128, 0, 0, 1)">"</span> setup=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">props, { emit }</span><span style="color: rgba(128, 0, 0, 1)">"</span>&gt;<span style="color: rgba(0, 0, 0, 1)">
import {
defineComponent,
computed,
onMounted,
</span><span style="color: rgba(0, 0, 255, 1)">ref</span><span style="color: rgba(0, 0, 0, 1)">,
toRefs,
} </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">vue</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;

</span><span style="color: rgba(0, 0, 255, 1)">const</span> { modelValue, disabled } =<span style="color: rgba(0, 0, 0, 1)"> toRefs(props);

</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)"> data </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
export </span><span style="color: rgba(0, 0, 255, 1)">const</span> currentValue = <span style="color: rgba(0, 0, 255, 1)">ref</span>(<span style="color: rgba(128, 0, 0, 1)">''</span><span style="color: rgba(0, 0, 0, 1)">);
export </span><span style="color: rgba(0, 0, 255, 1)">const</span> isComposing = <span style="color: rgba(0, 0, 255, 1)">ref</span>(<span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">);

</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)"> methods </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
export function handleInput() {
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (isComposing.value) <span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
emit(</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">update:modelValue</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">, currentValue.value);
}

</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)"> lifecycle </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
onMounted(() </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> {
currentValue.value </span>=<span style="color: rgba(0, 0, 0, 1)"> modelValue.value;
});

</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)"> computed </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
export </span><span style="color: rgba(0, 0, 255, 1)">const</span> inputDisabled = computed(() =&gt;<span style="color: rgba(0, 0, 0, 1)"> disabled.value);

export </span><span style="color: rgba(0, 0, 255, 1)">default</span><span style="color: rgba(0, 0, 0, 1)"> defineComponent({
name: </span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">test-input</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">,
props: {
    modelValue: ,
    disabled: Boolean,
},
emits: {
    </span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">update:modelValue</span><span style="color: rgba(128, 0, 0, 1)">'</span>: <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">,
},
});
</span>&lt;/script&gt;</span></pre>
</div>
<p><span style="font-size: 14px">在这份代码中,我还是按照组件选项的方式,将变量分类放到一起</span></p>
<p><span style="font-size: 14px">其实更合理的方式是按功能分类,这样更利于抽取逻辑</span></p>
<p><span style="font-size: 14px">&nbsp;</span></p>
<p><span style="font-size: 14px">&nbsp;</span></p>
<p><span style="font-size: 16px"><strong>五、小结</strong></span></p>
<p><span style="font-size: 14px">Vuex 和 vue-router 都提供了可以在 setup 中获取实例的方法</span></p>
<p><span style="font-size: 14px">这也侧面体现了 Vue 3 的 setup 是一个独立的钩子函数</span></p>
<p><span style="font-size: 14px">它不会依赖于 Vue 组件实例,如果需要用到函数外部的变量,都需要从外部获取</span></p>
<p><span style="font-size: 14px">同时也提醒我们在开发 Vue 3 的插件的时候,一定要提供相应的函数让开发者能在 setup 中使用</span></p>
<p><span style="font-size: 14px">&nbsp;</span></p><br><br>
来源:https://www.cnblogs.com/wisewrong/p/13915331.html
頁: [1]
查看完整版本: Vue3.x 从零开始(六)—— Router + Vuex + TypeScript 实战演练(下)