分类 技术文章 下的文章

Vue 3 使用了组合式 API,刚上手的同学可能会觉得写出来的代码非常凌乱。以前在使用选项式 API 时,要寻找一个方法,最起码知道要到 Methods 中去寻找。然而现在代码结构都在同一级别,即使精心组织代码,有时候也会难以找到目标代码。

Vue 3 提供了组合式函数,它不仅仅可以用来封装通用功能,也用来组织代码结构。你可以将相关联的一组代码封装到一个组合式函数中。由于组合式函数与 React 中的 Hooks 非常相似,所以这里也简称它为 hooks 吧。

然而写组合式函数也是要消耗一些心智的,有时候还可能会消耗额外的精力处理一些状况,如果代码直接写在 setup 中,这些情况是不会出现的。

幸运的是,Visual Studio Code 提供了丰富的便捷功能和许多强大的插件,组合使用这些能力,可以让我们在懒得写 hooks 时也不会迷失在代码海中。

首先推荐区域折叠 - region。这是 Visual Studio Code 提供的自定义折叠区域的方法,用过 Visual Studio 的同学一定不陌生。下面介绍使用方法。

首先定义区域开始位置。在想要折叠代码上一行弄个空行出来,输入 #region。通常,你不必全部输入这个单词,只要输入前几个字符,代码提示就会出现了,这时你只需要按回车,#region 就会补全了。你可以在 #region 后面输入此段代码的描述,在折叠此区域后,描述会继续显示。

接着定义区域结束位置。在想要折叠的代码最后一行弄一个空行,输入 #endregion。同样地,也不必输入完整,代码提示会帮你搞定。

此时,在 #region 行左边就会出现折叠符号,点击就可以折叠这个区域,就像折叠一个 HTML 元素或者一个 JavaScript 方法。

你可以把有关联的一组代码放到一个区域并折叠起来,需要的时候再展开。通常,Visual Studio Code 会记住你的折叠状态,这意味着即使你关闭了这个文件并再次打开,那些已经折叠的区域也不会自动展开。

当你新建了好多个折叠区域,会发现又出现了折叠区域难以分辨的问题,毕竟每个折叠区域折叠后,都有着一样的灰色背景,即使有描述,这些描述文字也几乎融合在灰色背景里,类似动物的保护色——它们的颜色只比背景颜色深一点。因此你还是需要努力寻找想要的代码。

这时候插件 Region Highlighter 可以解决这个问题。它可以给不同的折叠区域不同的背景颜色,你也可以配置自己喜欢的背景颜色。
它的使用方法是,按 Ctrl + Shift + P 打开命令面板,执行 Region Highlighter: Mark Region 命令,在弹出的输入框中输入此区域的描述,回车。效果如下:

1.png

图上是我比较喜欢的颜色,淡雅清爽,一如本博客的作者——他们都这么说 :D

最后还要提醒你,别忘了折叠代码的快捷键 Ctrl + Shift + [Ctrl + Shift + ]


END

相信大家肯定都遇到过题目中所说的那种需求,在 app.onLaunch 中获取一些信息,在 Page 中使用。然而这些生命周期钩子都是异步执行的,并没有特定的执行顺序。这就需要开发者自己控制生命周期钩子的执行顺序。

小程序设计得已经比较糟糕,性能低的同时还有许多不可知的奇怪的 bug (关于性能,他们推出了 SkyLine,暂时没有使用过)。而 uni-app 在小程序的基础上,还要兼容更多不同厂家的小程序,还要能够编译成各端代码,莫名其妙的 bug 就更多了。

关于题目中的需求,我尝试了 2 种方法,一是回调函数法,就是在 app.onLaunch 执行完毕后执行一个回调函数,用来初始化页面;二是 Promise.resolve 法,就是在 Page.onLoad 中使用 await 执行一个函数,将其 Promise 的 resovle 回调函数存储起来,这样一来,await 之后的代码就处于等待状态;在 app.onLaunch 的最后调用那个 resolve,结束 Page.onLoadawait 的等待状态。

遗憾的是,虽然网上有很多成功案例,但经过尝试,截止到本文发表的时刻,在最新的 HBuilder X 环境下,这两种方法并不能保证 app.onLaunchpage.onLoad/onShow 先后执行。

除了这两种方法,网上还有诸多神奇的解决方案,但我并没有尝试。最后我使用了最“笨”的方法,但感觉是最稳妥的办法,那就是 setInterval 大法!

setInterval 平常总是被嫌弃,就连实现倒计时,也要用 setTimeout 代替它。没想到最后它解决了我的问题,而且非常稳健!

思路非常简单:
app.globalData 中声明一个变量,比如叫 launched,初始值是 false,用来表示 app.onLaunch 是否执行完毕。然后,在 app.onLaunch 的最后,把它设置为 true
与此同时,页面中的 onLoad 或者 onShow 中设置一个 interval,一旦检查到 getApp().globalData.launched === true,则取消定时器,并执行初始化。

由于 uni-app 使用的是 Vue,所以可以在 main.js 中添加全局的 mixin 来执行 onLoad/onShow,若符合条件,则在 onLoad/onShow 中执行页面中约定好的方法,例如,若 mixin 中的 onLoad 检测到小程序已经执行完 onLaunch,则进一步检查当前页面中是否有约定好的方法,如 _onLoad,若有则执行。这样,就不用在每个页面中的 onLoad/onShow 中写重复的代码了。

由于比较简单,就不上代码了。

以前觉得写小程序简单,因为只需要在小程序那“简单的”框架中写代码,所能做的东西有限。现在终于明白,想要实现一些在 H5 中普通的功能,小程序需要更多的尝试。


End

有时候需要修改页面对象(PageObject)中的 data,直接修改是无效的:

const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];

currentPage.bar = 1; //PageObject上不存在bar
currentPage.data.bar = 1; //不会触发页面渲染

正确修改 data 的方式是使用setData

currentPage.setData({ bar: 1 })

End

如果你平时老是需要编写各种列表,例如商品列表、图片列表,不妨自己写一个通用的列表组件,只需要传入几个参数,就能帮我们自动分列、自动调整间距。下面就是一个实现起来非常简单,但真的很好用的自定义列表组件。

定义

直接上完整的代码:

<script setup>
const props = defineProps({
    data: {
        type: Array,
        default: []
    },
    columnCount: {
        type: Number,
        default: 2
    },
    gap: {
        type: Number,
        default: 10
    }
});

const emit = defineEmits(['select']);
</script>

<template>
    <div class="list" :style="{ gap: `${gap}px` }">
        <template v-for="i in data">
            <div class="item" :style="{ 'flex-basis': `calc((100% - ${columnCount - 1} * ${gap}px) / ${columnCount})` }" @click="emit('select', i)">
                <slot :row="i"></slot>
            </div>
        </template>
    </div>
</template>

<style lang="scss" scoped>
.list {
    display: flex;
    flex-wrap: wrap;

    >.item {
        flex-shrink: 0;
        flex-grow: 0;
        cursor: pointer;
    }
}
</style>

在 script 部分,这个组件定义了 3 个属性,data是需要传入的数组形式的数据,就像 el-table 的 data 属性。columnCount是列数,可以是任意整数列,默认值为 2,这也是最常用的列数。gap是每一项与其他项的边距。如果你需要实现其他特定,可以非常容易地自己增加其他参数。

模板部分,列表使用了 flex 布局,根据参数自动计算每一列的宽度。还提供了一个默认插槽,用来自定义每一项里面的内容。

使用

使用起来非常简单,假设我们将上面的代码保存到一个叫 CommonMultiColumnList.vue 的文件中,首先引入这个组件:

import CommonMultiColumnList from 'CommonMultiColumnList.vue';

然后在模板中使用:

<CommonMultiColumnList :data="someData" :column-count="1" :gap="15">
    <template #default="{row}">
        <!-- 这里是自定义的每一项的结构 -->
    </template>
</CommonMultiColumnList>

现在只需要编写每一项的结构和样式,列表就会展示出来了。
如果你希望更加方便,也可以在组件中写好每一项的样式,而不是提供插槽,并多建立几个每一项样式不同的组件。


End

最近在开发 uni-app 过程中,发现页面布局会闪烁:页面先是显示错乱,然后又恢复正常。

刚开始以为是页面过于复杂,导致性能降低,渲染速度变慢,但由于时间关系,没有来得及详细研究。今天偶然间发现了导致页面布局闪烁的元凶:image 组件。

我新建了一个很简单的页面,只有一个简单列表,每个列表项包含一个标题和一个子列表。其中标题包含两个 image 组件,用来显示图标,子列表高度设置为0,用来实现高度动画。

在调试过程中,发现每次刷新页面,布局都会先错乱,一闪而过后变得正常。实际上这个问题严重影响用户体验,所以我打算研究一下是什么导致了这个现象。

打开 Chrome 浏览器的性能工具,录制页面刷新过程,出现了布局偏移(Layout Shift)一栏,鼠标移动到代表布局便宜的紫色条上,发现页面上的标题部分会高亮。

布局偏移(Layout Shift)指的是元素突然改变位置,会严重影响体验。

标题的结构如下:

<view class="title">
    <image mode="heightFix" src="xxx.svg"></image>
    <text>Title</text>
    <image mode="heightFix" src="xxx.svg"></image>
</view>

其中有两个 SVG 图片,只给它们设置了高度,其宽度自适应:

.title {
    image {
        height: 20px;
    }
}

显示结果是符合预期的,但正是这两个 image 导致了布局偏移!

因为水平有限,具体原因还没有详细研究,不过可以猜测:image 在应用我们自己的 CSS 之前,应该经历了 uni-app 的处理,导致其位置和样式变化。如果有大神了解其中原理,请不吝赐教。

解决办法就是,为 image 设置明确的宽高:

.title {
    image {
        height: 20px;
        width: 20px;
    }
}

End

本文以封装一个旋转的 Loading 图标为例,讲解如何自定义 Message 动态(这里的“动态”的意思是"会动的")图标。

Element-Plus Message 提供了一个icon属性,用来自定义图标。icon属性接受Component或者String类型的参数。这个时候,就会想到可以引入 Element-Plus 自带的图标组件:

...
import { Loading } from '@element-plus/icons-vue'

ElMessage({
    icon:  Loading,
    ...
});
...

可惜上面这个图标不会动。

Element-Plus 提供了is-loadingCSS Class,只要添加在el-icon组件上,就能够这个组件旋转起来。
那么我们自己封装旋转的el-icon成一个自定义组件,就能够让 Message 的图标动起来了:

//新建组件loading.vue
<script setup>
import { Loading } from '@element-plus/icons-vue';
</script>

<template>
    <el-icon class="is-loading"><Loading /></el-icon>
</template>

使用上面的组件:

//javascript
import MyLoading from 'loading.vue';

ElMessage({
    icon:  MyLoading,
    ...
});

这样就实现了目标,不足就是需要建立一个组件文件。

如果不希望为了实现旋转效果而封装一个自定义组件,还有其他办法可以实现目标。
查看 Message 的源代码(位置在你的项目\node_modules\element-plus\es\utils\vue\icon.mjs),可以发现,Message 的icon属性可以接受 3 种类型:

const iconPropType = definePropType([
  String,
  Object,
  Function
]);

String 接受一个不能有特殊字符的字符串,经过 Vue 的 CreateNode 函数处理,会在页面中生成一个以这个字符串命名的标签,例如:

ElMessage({
    icon:  'MyTag',
    ...
});

会在图标应该出现的位置生成一个自定义 HTML 标签:

<MyTag></MyTag>

但暂时没有找到为它添加属性的方法。如果有明白的同学,欢迎通过站内联系方式告诉我,感谢!

最后,还可以传递 Function 类型。Element Message 文档中提到的icon属性可以是Component,也就是组件。组件可以是一个符合 Vue 组件格式的 JavaScript 对象,也可以是一个函数。前面的文章(戳这里)讲到了函数式组件,这种组件是一个函数。

所以,可以直接创建一个函数,该函数使用渲染函数h返回一个组件,最终代码如下:

import { Loading } from '@element-plus/icons-vue';

export const loading = ElMessage({
    ...
    icon: () => {
        return h(
            'div',
            //给任意 HTML 元素增加下面的两个 Class,都会应用旋转效果
            //这两个 Class 是 Element-Plus 提供的
            //这里这样用仅仅为了方便,也可以自己写动画
            { class: 'el-icon is-loading' },
            [h(Loading)]
        );
    },
    //不自动关闭,通过 Message 实例的 close 方法手动关闭
    duration: 0,
    ...
});

这样,一个会显示加载动画的自定义 Message 就实现了。下面是完整的代码,还顺便封装成了一个更方便的工具:

//新建 msg.js
import { h } from 'vue';
import { ElMessage } from "element-plus";
import { Loading } from "@element-plus/icons-vue";

//可以在全局样式中修改 Message 的样式
const customClass = 'el-message-custom';

const duration = 4000;
function msg(message, type = 'message') {
    if (type === 'loading') {
        return ElMessage({
            icon: () => {
                return h(
                    'div',
                    { class: 'el-icon is-loading' },
                    [h(Loading)]
                );
            },
            message,
            customClass,
            duration: 0,
        });
    }

    ElMessage({ message, type, customClass, duration });
}

export default {
    message: message => {
        msg(message);
    },
    success: message => {
        msg(message, 'success');
    },
    warning: message => {
        msg(message, 'warning');
    },
    error: message => {
        msg(message, 'error');
    },
    loading: message => {
        return msg(message, 'loading');
    }
};

使用:

import msg from 'msg.js'

const loading = msg.loading('提交中,请稍候');
setTimeout(()=>{
    loading.close();
    msg.success('提交成功');
}, 3000)

End

在 Element plus 中,除了对整个表单进行验证,还可以对单独的表单项进行验证。
查看 Form 文档,在 FormItem Exposes 节,可以看到 FormItem 提供了validate方法。该方法的使用方式与 Formrefvalidate方法基本相同,不同的就是需要获取 FormItemref

//javascript
const mobileRef = ref(null);

//template
...
<el-form-item prop="mobile" ref="mobileRef">...</el-form-item> 
...

接着,就能够调用该 ref 的 validate方法了:

//javascript
const submit = async () => {
    const result = await mobileRef.value.validate();
}

validate方法返回一个Promise,如果验证通过,上面代码中的result将会等于true。如果验证不通过,相应的表单将会显示在el-form上设置的rules中提供的报错信息。

End

0、前言

渲染函数h()取自 hyperscript 的第一个字母,意思是“能够生成 HTML 的 JavaScript”,用来创建 vnodes。

JSX是 React 最早使用的,类似 XML 的 JavaScript 扩展,能够用这样的方式写代码:

const vnode = <h1>Hi!</h1>

渲染函数和 JSX 都比模板语法灵活得多,所以可以用来创建高自由度的组件,实现复杂逻辑。

函数式组件实际上就是一种轻量化的组件,去掉了大多数组件的属性,仅支持有限的属性,用来创建功能简单的组件:接收 props,返回 vnodes。由于轻量化,所以性能比正常组件高。

下面介绍如何创建这些组件。

1、使用渲染函数创建组件

由于渲染函数是使用纯 JavaScript 创建组件,所以将其放在一个 .js.ts文件中。
创建Component1.js,并添加如下代码:

import { h } from 'vue';

export default {
    setup() {
        return () => h('div', 'Hi!');
    }
}

这样就完成了一个最简单的组件的创建。h函数的参数,以及 props 的接收、数据的处理请参见文档。

2、使用 JSX 创建组件

JSX 组件的文件后缀是 .jsx。如果你使用 TypeScript的话,后缀则为.tsx

创建Component2.jsx并添加如下代码:

export default {
    render() {
        return <h1>Hi!</h1>;
    }
}

JSX 组件创建完成。同样地,具体细节请参考文档。

3、在Component1中使用Component2

修改 Component1.js 的代码如下:

import { h } from 'vue';
import Component2 from './Component2.jsx';

export default {
    setup() {
        return () => h('div', h(Component2));
    }
}

如果提示React is not defined,可能是 Vite 的版本过低,没有自带@vitejs/plugin-vue-jsx,请更新 Vite,或者安装 @vitejs/plugin-vue-jsx,不过,由于版本原因,安装过程中会报错,请根据提示强制安装,或者更新 Vite。

4、创建函数式组件

函数式组件实际上就是一个函数,其接收一些参数,这些参数和setup()完全相同。
在 Component1.js 中创建一个函数式组件:

const Component3 = props => {
    return h('div', props.msg);
}

5、在 Component1 中使用函数式组件

修改 Component1.js:

//...
setup() {
    return () => h('div', h(Component3, { msg: 'Hi!' }));
}
//...

6、总结

注意, JSX 组件和函数式组件都需要经过渲染函数h()来渲染。

熟练创建文中各种类型的组件,需要了解渲染函数的参数,渲染函数以及 JSX 的语法,setup()函数的参数。掌握这些知识,对于提高程序灵活性、提高程序性能会有很大的帮助。

End

0、前言

uni-app 项目经过编译、压缩后的打包资源,如果其大小超出了 40M,就需要付费,每次云打包需要花费 10 元,如果打包频繁,这是一笔不小的花费,所以,配置本地离线打包才是最终的选择。

配置本地打包一共有两个主要步骤:

  1. 配置 Android Studio
  2. 从 XBuilder 导出打包资源

未完待续

注意,这里使用的是 HTML+ 提供的 SQLite 方法。

const dbName = 'dbName';
const tableName = 'tablename';
const sql = `select count(1) as count from sqlite_master where type='table' and name='${tableName}'`;

plus.sqlite.selectSql({
    name: dbName,
    sql,
    success(r) {
        //r是一个数组,如果没有查询的表,则是空数组
    },
    fail(err) {
        //do something
    }
});