Open maicFir opened 2 years ago
vue组合式是vue2项目过渡vue3的一种友好方案,在历史项目逐步迁移到vue3中,有项目历史包袱原因,一下子升级带来的问题可能比较多,composition-api天然兼容vue2,在vue2中使用组合式API让你提前感受vue3的各种姿势,vue3已经出来 3 年了,都2022了,vue祖师爷赏饭吃,相信你跟笔者一样早已跃跃欲试。
vue组合式
vue2
vue3
composition-api
组合式API
2022
vue
前段时间,笔者项目已经完成升级ts、组合式api,毕竟去年第 4 季度首要KPI便是升级项目业务引入ts和组合式API。在一边搬砖迭代业务,一边升级项目技术栈的挑战,过程虽曲折,好在组内有大佬及时解围,常常遇到奇葩问题,算是少走弯路。本篇不做组合式API语法过渡解读,因最近一个页面需求优化,以最简单的注册业务为例,在vue2与组合式API的选择中,希望给你的项目升级的过程中带来一点点帮助和思考。
ts
组合式api
KPI
组合式API语法
正文开始...
这是一个非常普通的一个注册流程,可以看下具体页面,第一步账号注册
第一步账号注册
这是完成注册后,需要登记信息第二步信息登记
第二步信息登记
在第二步完成信息登记成功后,便等待管理员审核便可成功登陆控制台
// Index.vue <template> <a-base-page-layout> <i-card class="page-content-card"> <i-steps :current="currentStep"> <i-step v-for="step of steps" :key="step.value" :title="$t(step.label)"></i-step> </i-steps> <i-divider></i-divider> <!--注册--> <a-register v-if="currentStep === 0" class="step-one" @finish="finishRegister"></a-register> <!--信息登记--> <a-info v-if="currentStep === 1" class="step-two" @finish="finishRegisterInfo"></a-info> <!--等待审核--> <a-moderation v-if="currentStep === 2" class="step-three"></a-moderation> </i-card> </a-base-page-layout> </template>
// Index.vue <script> import auth from '@/service/auth'; import { BasePageLayout } from '@/components'; import Register from './Register'; import Info from './Info'; import Moderation from './Moderation'; export default { components: { ABasePageLayout: BasePageLayout, ARegister: Register, AInfo: Info, AModeration: Moderation, }, data() { return { currentStep: 0, steps: [ { value: 1, label: 'register.steps.one', }, { value: 2, label: 'register.steps.two', }, { value: 3, label: 'register.steps.three', }, ], }; }, methods: { finishRegister(result) { if (result.developerInfo) { if (result.developerInfo.enabled) { this.$Message.info(this.$t('register.message.registerd')); auth.goLogin('/'); } else { this.currentStep = 2; } } else { this.currentStep = 1; } }, finishRegisterInfo() { this.currentStep = 2; }, }, created() { const step = +this.$route.query.step; if (step) { this.currentStep = step; } else { this.currentStep = 0; } }, }; </script>
以上是index.vue,模板和逻辑是常用的options和template方式,在vue2中看起来似乎没毛病。
index.vue
options
template
我们在继续关注第一步Register.vue,具体代码如下:
Register.vue
模板代码
// Register.vue <template> <div> <i-alert class="warm-prompt-alert" show-icon closable> <span class="warm-prompt-tips">{{ $t('register.warmPrompt.title') }}</span> <span slot="desc">{{ $t('register.warmPrompt.content') }}</span> </i-alert> <i-row> <i-col span="12" offset="6"> <i-tabs> <!-- 手机号注册 --> <i-tab-pane :label="$t('register.tabs.phone')" icon="ios-phone-portrait"> <i-form :model="registerPhoneForm" ref="registerPhoneForm" :rules="phoneRuleValidate" :label-width="140" > <a-fixed-autofill-password></a-fixed-autofill-password> <i-form-item :label="$t('register.registerForm.phone.label')" prop="phone"> <i-input v-model="registerPhoneForm.phone" :placeholder="$t('register.registerForm.phone.placeholder')" ></i-input> </i-form-item> <i-form-item :label="$t('register.registerForm.password.label')" prop="password"> <i-input type="password" v-model="registerPhoneForm.password" :placeholder="$t('register.registerForm.password.placeholder')" ></i-input> </i-form-item> <i-form-item :label="$t('register.registerForm.rpassword.label')" prop="rpassword"> <i-input type="password" v-model="registerPhoneForm.rpassword" :placeholder="$t('register.registerForm.rpassword.placeholder')" ></i-input> </i-form-item> <i-form-item :label="$t('register.registerForm.authCode.label')" prop="authCode"> <i-row type="flex" class="authcode-row"> <i-col> <i-input v-model="registerPhoneForm.authCode" :placeholder="$t('register.registerForm.authCode.placeholder')" ></i-input> </i-col> <i-col class="padding-left-16"> <a-vcode-button @send-code="sendCode('phone')" :disabled="!registerPhoneForm.phone" ></a-vcode-button> </i-col> </i-row> </i-form-item> <i-form-item> <i-checkbox v-model="registerPhoneForm.agreeAndComply"> {{ $t('login.agreeAndComply') }} <a @click="handleProtal('/user-agreement')" class="link"> {{ $t('login.userAgreement') }} </a> 与 <a @click="handleProtal('/privacy-policy')" class="link"> {{ $t('login.privacyPolicy') }} </a> </i-checkbox> </i-form-item> <i-form-item> <i-button type="primary" @click="submitPhoneForm" :disabled="!registerPhoneForm.agreeAndComply" > {{ $t('register.registerForm.submitButtonLabel') }} </i-button> </i-form-item> </i-form> </i-tab-pane> <!-- 邮箱注册 --> <i-tab-pane :label="$t('register.tabs.email')" icon="ios-mail"> <i-form :model="registerEmailForm" ref="registerEmailForm" :rules="emailRuleValidate" :label-width="140" > <a-fixed-autofill-password></a-fixed-autofill-password> <i-form-item :label="$t('register.registerForm.email.label')" prop="email"> <i-input v-model="registerEmailForm.email" :placeholder="$t('register.registerForm.email.placeholder')" ></i-input> </i-form-item> <i-form-item :label="$t('register.registerForm.password.label')" prop="password"> <i-input type="password" v-model="registerEmailForm.password" :placeholder="$t('register.registerForm.password.placeholder')" ></i-input> </i-form-item> <i-form-item :label="$t('register.registerForm.rpassword.label')" prop="rpassword"> <i-input type="password" v-model="registerEmailForm.rpassword" :placeholder="$t('register.registerForm.rpassword.placeholder')" ></i-input> </i-form-item> <i-form-item :label="$t('register.registerForm.authCode.label')" prop="authCode"> <i-row type="flex" class="authcode-row"> <i-col> <i-input v-model="registerEmailForm.authCode" :placeholder="$t('register.registerForm.authCode.placeholder')" ></i-input> </i-col> <i-col class="padding-left-16"> <a-vcode-button @send-code="sendCode('email')" :disabled="!registerEmailForm.email" ></a-vcode-button> </i-col> </i-row> </i-form-item> <i-form-item> <i-checkbox v-model="registerEmailForm.agreeAndComply"> {{ $t('login.agreeAndComply') }} <a @click="handleProtal('/user-agreement')" class="link"> {{ $t('login.userAgreement') }} </a> 与 <a @click="handleProtal('/privacy-policy')" class="link"> {{ $t('login.privacyPolicy') }} </a> </i-checkbox> </i-form-item> <i-form-item> <i-button type="primary" @click="submitEmailForm" :disabled="!registerEmailForm.agreeAndComply" > {{ $t('register.registerForm.submitButtonLabel') }} </i-button> </i-form-item> </i-form> </i-tab-pane> </i-tabs> </i-col> </i-row> </div> </template>
js代码
// Register.vue <script> import md5 from 'blueimp-md5'; import { commonRegexp } from '@/utils/index'; import { VcodeButton, FixedAutofillPassword } from '@/components'; import { handlegetRegistVCodeProxy, handleRegisterProxy } from '@/service/proxy/index'; export default { components: { AVcodeButton: VcodeButton, AFixedAutofillPassword: FixedAutofillPassword, }, data() { return { registerPhoneForm: { phone: '', password: '', rpassword: '', authCode: '', agreeAndComply: true, }, phoneRuleValidate: { phone: [ { required: true, validator: (rule, value, callback) => { if (value === '') { callback(new Error(this.$t('register.registerForm.phone.emptyMessage'))); } else if (!commonRegexp.MOBILE_REG_EXP.test(value)) { callback(new Error(this.$t('register.registerForm.phone.regCheckMessage'))); } else { callback(); } }, trigger: 'blur', }, ], password: [ { required: true, validator: (rule, value, callback) => { if (value === '') { callback(new Error(this.$t('register.registerForm.password.emptyMessage'))); } else if (!commonRegexp.PASSWORD_REG_EXP.test(value)) { callback(new Error(this.$t('register.registerForm.password.regCheckMessage'))); } else { callback(); } }, trigger: 'blur', }, ], rpassword: [ { required: true, validator: (rule, value, callback) => { if (value === '') { callback(new Error(this.$t('register.registerForm.rpassword.emptyMessage'))); } else if (value !== this.registerPhoneForm.password) { callback(new Error(this.$t('register.registerForm.rpassword.notMatchMessage'))); } else { callback(); } }, trigger: 'blur', }, ], authCode: [ { required: true, validator: (rule, value, callback) => { if (value === '') { callback(new Error(this.$t('register.registerForm.authCode.emptyMessage'))); } else { callback(); } }, trigger: 'blur', }, ], }, registerEmailForm: { email: '', password: '', rpassword: '', authCode: '', agreeAndComply: true, }, emailRuleValidate: { email: [ { required: true, validator: (rule, value, callback) => { if (value === '') { callback(new Error(this.$t('register.registerForm.email.emptyMessage'))); } else if (!commonRegexp.EMAIL_REG_EXP.test(value)) { callback(new Error(this.$t('register.registerForm.email.regCheckMessage'))); } else { callback(); } }, trigger: 'blur', }, ], password: [ { required: true, validator: (rule, value, callback) => { if (value === '') { callback(new Error(this.$t('register.registerForm.password.emptyMessage'))); } else if (!commonRegexp.PASSWORD_REG_EXP.test(value)) { callback(new Error(this.$t('register.registerForm.password.regCheckMessage'))); } else { callback(); } }, trigger: 'blur', }, ], rpassword: [ { required: true, validator: (rule, value, callback) => { if (value === '') { callback(new Error(this.$t('register.registerForm.rpassword.emptyMessage'))); } else if (value !== this.registerEmailForm.password) { callback(new Error(this.$t('register.registerForm.rpassword.notMatchMessage'))); } else { callback(); } }, trigger: 'blur', }, ], authCode: [ { required: true, validator: (rule, value, callback) => { if (value === '') { callback(new Error(this.$t('register.registerForm.authCode.emptyMessage'))); } else { callback(); } }, trigger: 'blur', }, ], }, }; }, methods: { handleProtal(path) { this.$router.push(path); }, async sendCode(type) { const params = {}; if (type === 'phone') { params.phoneNum = this.registerPhoneForm.phone; params.countryCode = '+86'; } else if (type === 'email') { params.email = this.registerEmailForm.email; } params.authCodeType = 0; // 0 注册 1验证码登陆 2 找回密码 try { await handlegetRegistVCodeProxy(params); this.$Message.success({ content: this.$t('common.message.success'), }); } catch (err) { throw err; } }, async _doRegister(params) { const res = await handleRegisterProxy(params); console.log(res, 'register'); this.$emit('finish', res); }, submitPhoneForm() { const { registerPhoneForm: { phone: account, password, authCode }, } = this; this.$refs.registerPhoneForm.validate(valid => { if (valid) { const params = { account, password: md5(password), authCode, }; this._doRegister(params); } }); }, submitEmailForm() { const { registerEmailForm: { email: account, password, authCode }, } = this; this.$refs.registerEmailForm.validate(valid => { if (valid) { const params = { account, password: md5(password), authCode, }; this._doRegister(params); } }); }, }, }; </script>
这个页面代码如此冗余,我们发现有非常多重复的东西,用了两个tab切换两种不同方式注册,但实际上发现手机注册、邮箱注册最后调用接口都是一样的,所以冗余代码有些多,代码虽长了些,好在能改得动。
tab
手机注册
邮箱
用jsx与composition-api重构了这个页面,减少了很多不必要的代码
jsx
新重构Index.vue模板代码
Index.vue模板代码
// Index.vue <script lang="tsx"> import { defineComponent, SetupContext, watch, onMounted } from '@vue/composition-api'; import Qs from 'qs'; import auth from '@/service/auth'; import { BasePageLayout } from '@/components'; import { useStepConfig } from './hooks'; import Register from './Register.vue'; import Info from './Info.vue'; import Moderation from './Moderation.vue'; import AuditFail from './AuditFail.vue'; export default defineComponent({ components: { ABasePageLayout: BasePageLayout, ARegister: Register, AInfo: Info, AModeration: Moderation, AuditFail, }, setup(props, ctx: SetupContext) { const { currentStep, steps } = useStepConfig(); const { root } = ctx; // 完成注册 const finish = (result: PlainObj, setType: string) => { if (setType === 'regiter') { if (result.developerInfo) { if (result.developerInfo.enabled) { root.$Message.info(root.$t('register.message.registerd')); auth.goLogin('/'); } else { root.$router.replace('/register?step=2'); } } else { root.$router.replace('/register?step=1'); } } else { // const { step, status } = result; const params = Qs.stringify(result, { addQueryPrefix: true }); root.$router.push(`/register${params}`); } }; watch( () => root.$route, (val): void => { const { query: { step = 0 }, } = val; currentStep.value = step ? Number(step) : 0; } ); onMounted(() => { const step = +root.$route.query.step; currentStep.value = step || 0; }); return { currentStep, steps, finish, }; }, render() { const { currentStep, steps, finish } = this; const CurentComponent: any = (): JSX.Element => { const ret: { [key: number]: JSX.Element; } = { 0: <a-register class="step-one" onFinish={finish}></a-register>, 1: <a-info class="step-two" onFinish={finish}></a-info>, 2: <a-moderation class="step-three"></a-moderation>, }; return ret[currentStep]; }; return ( <div class="register"> <a-base-page-layout> <a-card class="page-content-card"> {currentStep === 3 ? ( <audit-fail onFinish={finish}></audit-fail> ) : ( <div> <i-steps current={currentStep}> {steps.map(v => ( <i-step key={v.value} title={v.label.value}></i-step> ))} </i-steps> {[1, 2].includes(currentStep) ? ( <i-divider style="width:auto;min-width:80%;margin:30px 60px;"></i-divider> ) : null} <CurentComponent /> </div> )} </a-card> </a-base-page-layout> </div> ); }, }); </script>
由页面结构来看,其实与未升级前并没有发生多大变化,就是第一步注册操作、第二步信息登记,第三部等待审核。不过注意页面上还有一个状态currentStep=3的条件,这是一个等待审核被拒绝的页面状态。页面每个步骤的阶段显示都是通过路由的currentStep来做判断标识。
currentStep=3
currentStep
重构后后新注册页面模板代码
// Register.vue <template> <div> <i-alert class="warm-prompt-alert"> <span slot="desc"> {{ $t('register.warmPrompt.title') }}{{ $t('register.warmPrompt.content') }} <a href="javascript:void(0)" style="color: #4754ff" @click="handleLogin"> {{ $t('register.warmPrompt.accountLogin') }} </a> </span> </i-alert> <i-row> <i-col span="12" offset="6"> <i-form :model="formParams" :rules="rulesConfig" ref="form" :label-width="140"> <a-fixed-autofill-password></a-fixed-autofill-password> <i-form-item :label="$t('register.registerForm.useServer.label')" prop="useServer"> <i-select style="width: 320px" v-model="formParams.useServer" :placeholder="$t('register.registerForm.useServer.placeholder')" > <i-option v-for="item in useServerListOption" :value="item.value" :key="item.value" :label="$t(item.label)" ></i-option> </i-select> </i-form-item> <i-form-item :label="$t('register.registerForm.account.label')" prop="account"> <i-input v-model.trim="formParams.account" clearable :placeholder="$t('register.registerForm.account.placeholder')" ></i-input> </i-form-item> <i-form-item :label="$t('register.registerForm.password.label')" prop="password"> <i-input type="password" clearable v-model.trim="formParams.password" :placeholder="$t('register.registerForm.password.placeholder')" ></i-input> </i-form-item> <i-form-item :label="$t('register.registerForm.rpassword.label')" prop="rpassword"> <i-input type="password" clearable v-model="formParams.rpassword" :placeholder="$t('register.registerForm.rpassword.placeholder')" ></i-input> </i-form-item> <i-form-item :label="$t('register.registerForm.authCode.label')" prop="authCode"> <i-row type="flex" class="authcode-row"> <i-col> <i-input v-model="formParams.authCode" clearable :placeholder="$t('register.registerForm.authCode.placeholder')" ></i-input> </i-col> <i-col class="padding-left-16"> <a-vcode-button :sendCode="sendCode" :disabled="!formParams.account" ></a-vcode-button> </i-col> </i-row> </i-form-item> <i-form-item> <i-checkbox v-model="formParams.agreeAndComply"> {{ $t('login.agreeAndComply') }} <a @click="handleProtal('/user-agreement')" class="link"> {{ $t('login.userAgreement') }} </a> 与 <a @click="handleProtal('/privacy-policy')" class="link"> {{ $t('login.privacyPolicy') }} </a> </i-checkbox> </i-form-item> <i-form-item> <i-button style="width: 100%" type="primary" @click="handleSubmit" :disabled="!formParams.agreeAndComply" > {{ $t('register.registerForm.submitButtonLabel') }} </i-button> </i-form-item> </i-form> </i-col> </i-row> </div> </template>
我们发现模板页面少了邮箱与手机号的区别,这是因为把手机号与邮箱统称为账号了,笔者认为在需求阶段应该就能考虑到,由于接口设计原因,邮箱和手机号为一个字段,但是前端表现形式不一样,譬如涉及邮箱和手机号的正则校验,因此,在需求与编码阶段,你是否会走第一种方案?还是说以后端一个字段设计为准,在视图层里,你不要那么明确的给用户两种方式选择。有更好的选择,如果设计如此,我们可以与产品设计沟通,因为只要你有理由说服了他们,那么就会增加代码的可复用度,降低冗余代码的堆积,从而减少维护成本。
重构后新注册js代码
// Register.vue <script lang="tsx"> import { defineComponent, SetupContext, computed } from '@vue/composition-api'; import { Form } from 'view-design'; import md5 from 'blueimp-md5'; import { commonRegexp } from '@/utils/index'; import { VcodeButton, FixedAutofillPassword } from '@/components'; import useServerListOptionMinx from '@/mixins/useServerListOptionMinx'; import { handlegetRegistVCodeProxy, handleRegisterProxy } from '@/service/proxy/index'; import { useRegister } from './hooks'; interface formParamsType { account: string; password: string; authCode: string | number; useServer: number | string; } export default defineComponent({ components: { AVcodeButton: VcodeButton, AFixedAutofillPassword: FixedAutofillPassword, }, mixins: [useServerListOptionMinx], setup(props: any, ctx: SetupContext) { const { formParams, rules } = useRegister(); const { refs, emit, root } = ctx; const rulesConfig = computed(() => rules.value); const doRegister = async (params: formParamsType) => { const res = await handleRegisterProxy(params); emit('finish', res, 'regiter'); }; const handleProtal = (path: string) => { root.$router.push(path); }; const handleSubmit = () => { const { account, password, authCode, useServer } = formParams.value; (refs.form as InstanceType<typeof Form>).validate((valid: Boolean) => { if (valid) { const params: formParamsType = { account, password: md5(password), authCode, useServer, }; doRegister(params); } }); }; // 发送验证码 const sendCode = async (callback: Function) => { const { account } = formParams.value; const params: { phoneNum?: string | number; email?: string; authCodeType: number; } = { authCodeType: 0, // 0 注册 1验证码登陆 2 找回密码 }; if (commonRegexp.PHONE_REG_EXP.test(account)) { params.phoneNum = account; // params.countryCode = '+86'; } else if (commonRegexp.EMAIL_REG_EXP.test(account)) { params.email = account; } try { await handlegetRegistVCodeProxy(params); root.$Message.success({ content: root.$t('common.message.success'), } as any); callback(true); } catch (err) { callback(false); throw err; } }; const handleLogin = () => { root.$router.push('/login'); }; return { formParams, rulesConfig, handleSubmit, handleProtal, sendCode, handleLogin, }; }, }); </script>
不知道你注意一段代码没有,以前的表单校验rule与formParams全部从useRegister解构了出来,在vue3大量的api都是用hooks的思想写的,与react越来越相似,在react中,函数式组件,hooks极大的解耦了业务组件,React 构建的页面思想就是像搭积木一样,每个视图模块就是一个组件,在vue2之前虽然提供了render渲染组件,但是对于像react一样天然支持jsx的能力还是非常欠缺,虽然在vue也可以申明函数组件,也提供的template模板的方式。但是composition-api除了支持jsx,有更大的ts能力,让你组织你的代码,更强壮,可维护性更强,业务逻辑能进一步复用并减少耦合。
rule
formParams
useRegister
api
hooks
react
render
接下来我们来看下useRegister这个引入的hook,我们通常把这个方法有个更优雅的名字来定义它useXXXX,也就是类比 react 中的hook
hook
useXXXX
// hooks/index.ts import { reactive, toRefs, computed } from '@vue/composition-api'; import i18n from '@/i18n/index'; import { commonRegexp } from '@/utils/index'; // step进度条 export const useStepConfig = () => { const setUpConfig = reactive({ currentStep: 0, steps: [ { value: 1, label: computed(() => i18n.t('register.steps.one')), }, { value: 2, label: computed(() => i18n.t('register.steps.two')), }, { value: 3, label: computed(() => i18n.t('register.steps.three')), }, ], }); return { ...toRefs(setUpConfig), }; }; // 账号注册 export const useRegister = () => { const registeConfig = reactive({ formParams: { account: '', password: '', rpassword: '', authCode: '', agreeAndComply: true, useServer: '', }, rules: { useServer: [ { required: true, trigger: 'change', validator: (_rule: any, value: string, callback: Function) => { if (value === '') { callback(new Error(i18n.t('register.registerForm.useServer.message'))); } else { callback(); } }, }, ], account: [ { required: true, trigger: 'blur', validator: (_rule: any, value: string, callback: Function) => { if (value === '') { callback(new Error(i18n.t('register.registerForm.account.emptyMessage'))); } else if ( !commonRegexp.MOBILE_REG_EXP.test(value) && !commonRegexp.EMAIL_REG_EXP.test(value) ) { callback(new Error(i18n.t('register.registerForm.account.message'))); } else { callback(); } }, }, ], password: [ { required: true, validator: (_rule: any, value: string, callback: Function) => { if (value === '') { callback(new Error(i18n.t('register.registerForm.password.emptyMessage'))); } else if (!commonRegexp.PASSWORD_REG_EXP.test(value)) { callback(new Error(i18n.t('register.registerForm.password.regCheckMessage'))); } else { callback(); } }, trigger: 'blur', }, ], rpassword: [ { required: true, validator: (_rule: any, value: string, callback: Function) => { if (value === '') { callback(new Error(i18n.t('register.registerForm.rpassword.emptyMessage'))); } else if (value !== registeConfig.formParams.password) { callback(new Error(i18n.t('register.registerForm.rpassword.notMatchMessage'))); } else { callback(); } }, trigger: 'blur', }, ], authCode: [ { required: true, validator: (_rule: any, value: string, callback: Function) => { if (value === '') { callback(new Error(i18n.t('register.registerForm.authCode.emptyMessage'))); } else { callback(); } }, trigger: 'blur', }, ], }, }); return { ...toRefs(registeConfig), }; }; // 信息填写 export const userRegistInfo = () => { ... return { } }
这个hook文件已经把三个步奏用到的数据层已经高度的分离了出去,在实际业务中,你并不一定需要写在一个文件中,如果涉及多人合作,那么你可以把index里面拆分得更细些,比如这里你差分成三个不同ts文件userRegistInfo.ts、useRegister.ts、useStepConfig.ts,我们把每一块自己需要数据写入自己相关的hooks中,这样每个人只需要维护自己那份代码就行。
index
userRegistInfo.ts
useRegister.ts
useStepConfig.ts
看到这里你是否感受到composition-api的思想呢,在vue3中,所有的api用法几乎与composition-api用法一样,在官方有这么一段话,当迁移到 Vue 3 时,只需简单的将 @vue/composition-api 替换成 vue 即可。你现有的代码几乎无需进行额外的改动。。看到这里,你情不自禁的发出尖叫,vue3向下兼容了vue2,并且当你用composition-api过渡vue3时,我只需要全局替换一下@vue/composition-api这个就可以全量升级到vue3了。
当迁移到 Vue 3 时,只需简单的将 @vue/composition-api 替换成 vue 即可。你现有的代码几乎无需进行额外的改动。
@vue/composition-api
此时你心中有没有被震惊到,赶紧升级你项目的vue2,让你自己在vue2的项目中也能畅游vue3的各种姿势,哈哈。
1.在 vue2 中使用options面条方式编码,业务页面有冗余代码,当我们发现字段设计与交互有差别时,可以与产品设计沟通,用你的理由说服他
2.在 vue2 中用composition-api方式组织你的业务代码时,明显感受到业务逻辑比以前更清晰,并且天然支持ts,让你的代码更安全,更强壮 💪
3.类似 react 的hook思想,高度解耦业务视图层的数据逻辑,让你更专注解决疑难杂症,或者有更多的时间轻松聊天喝茶摸鱼。
4.更多关于composition-api,更多 vue3 参考官网
vue组合式
是vue2
项目过渡vue3
的一种友好方案,在历史项目逐步迁移到vue3
中,有项目历史包袱原因,一下子升级带来的问题可能比较多,composition-api
天然兼容vue2
,在vue2
中使用组合式API
让你提前感受vue3
的各种姿势,vue3
已经出来 3 年了,都2022
了,vue
祖师爷赏饭吃,相信你跟笔者一样早已跃跃欲试。前段时间,笔者项目已经完成升级
ts
、组合式api
,毕竟去年第 4 季度首要KPI
便是升级项目业务引入ts
和组合式API
。在一边搬砖迭代业务,一边升级项目技术栈的挑战,过程虽曲折,好在组内有大佬及时解围,常常遇到奇葩问题,算是少走弯路。本篇不做组合式API语法
过渡解读,因最近一个页面需求优化,以最简单的注册业务为例,在vue2
与组合式API
的选择中,希望给你的项目升级的过程中带来一点点帮助和思考。正文开始...
注册页面
这是一个非常普通的一个注册流程,可以看下具体页面,
第一步账号注册
信息登记
这是完成注册后,需要登记信息
第二步信息登记
等待审核
在第二步完成信息登记成功后,便等待管理员审核便可成功登陆控制台
看下未升级之前的代码(vue2)版本
以上是
index.vue
,模板和逻辑是常用的options
和template
方式,在vue2
中看起来似乎没毛病。我们在继续关注第一步
Register.vue
,具体代码如下:模板代码
js代码
这个页面代码如此冗余,我们发现有非常多重复的东西,用了两个
tab
切换两种不同方式注册,但实际上发现手机注册
、邮箱
注册最后调用接口都是一样的,所以冗余代码有些多,代码虽长了些,好在能改得动。升级后代码(组合式 API)
用
jsx
与composition-api
重构了这个页面,减少了很多不必要的代码新重构
Index.vue模板代码
由页面结构来看,其实与未升级前并没有发生多大变化,就是第一步注册操作、第二步信息登记,第三部等待审核。不过注意页面上还有一个状态
currentStep=3
的条件,这是一个等待审核被拒绝的页面状态。页面每个步骤的阶段显示都是通过路由的currentStep
来做判断标识。重构后后新注册页面
模板代码
我们发现模板页面少了邮箱与手机号的区别,这是因为把手机号与邮箱统称为账号了,笔者认为在需求阶段应该就能考虑到,由于接口设计原因,邮箱和手机号为一个字段,但是前端表现形式不一样,譬如涉及邮箱和手机号的正则校验,因此,在需求与编码阶段,你是否会走第一种方案?还是说以后端一个字段设计为准,在视图层里,你不要那么明确的给用户两种方式选择。有更好的选择,如果设计如此,我们可以与产品设计沟通,因为只要你有理由说服了他们,那么就会增加代码的可复用度,降低冗余代码的堆积,从而减少维护成本。
重构后新注册
js代码
不知道你注意一段代码没有,以前的表单校验
rule
与formParams
全部从useRegister
解构了出来,在vue3
大量的api
都是用hooks
的思想写的,与react
越来越相似,在react
中,函数式组件,hooks
极大的解耦了业务组件,React 构建的页面思想就是像搭积木一样,每个视图模块就是一个组件,在vue2
之前虽然提供了render
渲染组件,但是对于像react
一样天然支持jsx
的能力还是非常欠缺,虽然在vue
也可以申明函数组件,也提供的template
模板的方式。但是composition-api
除了支持jsx
,有更大的ts
能力,让你组织你的代码,更强壮,可维护性更强,业务逻辑能进一步复用并减少耦合。接下来我们来看下
useRegister
这个引入的hook
,我们通常把这个方法有个更优雅的名字来定义它useXXXX
,也就是类比 react 中的hook
这个
hook
文件已经把三个步奏用到的数据层已经高度的分离了出去,在实际业务中,你并不一定需要写在一个文件中,如果涉及多人合作,那么你可以把index
里面拆分得更细些,比如这里你差分成三个不同ts
文件userRegistInfo.ts
、useRegister.ts
、useStepConfig.ts
,我们把每一块自己需要数据写入自己相关的hooks
中,这样每个人只需要维护自己那份代码就行。看到这里你是否感受到
composition-api
的思想呢,在vue3
中,所有的api
用法几乎与composition-api
用法一样,在官方有这么一段话,当迁移到 Vue 3 时,只需简单的将 @vue/composition-api 替换成 vue 即可。你现有的代码几乎无需进行额外的改动。
。看到这里,你情不自禁的发出尖叫,vue3
向下兼容了vue2
,并且当你用composition-api
过渡vue3
时,我只需要全局替换一下@vue/composition-api
这个就可以全量升级到vue3
了。此时你心中有没有被震惊到,赶紧升级你项目的
vue2
,让你自己在vue2
的项目中也能畅游vue3
的各种姿势,哈哈。总结
1.在 vue2 中使用
options
面条方式编码,业务页面有冗余代码,当我们发现字段设计与交互有差别时,可以与产品设计沟通,用你的理由说服他2.在 vue2 中用
composition-api
方式组织你的业务代码时,明显感受到业务逻辑比以前更清晰,并且天然支持ts
,让你的代码更安全,更强壮 💪3.类似 react 的
hook
思想,高度解耦业务视图层的数据逻辑,让你更专注解决疑难杂症,或者有更多的时间轻松聊天喝茶摸鱼。4.更多关于composition-api,更多 vue3 参考官网