在vscode中开发uniapp

使用命令行创建uniapp项目

此处是vue3+ts。

1
npx degit dcloudio/uni-preset-vue#vite-ts my-vue3-project

安装插件

  • uni-create-view
  • uni-helper
  • uniapp小程序扩展

TS相关

在根目录创建 tsconfig.json 文件,并进行个性化配置,个性化配置是可选的,没有tsconfig.json时会自动使用默认配置运行。推荐配置如下:

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
// tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"moduleResolution": "node",
"esModuleInterop": true,
"sourceMap": true,
"skipLibCheck": true,
"importHelpers": true,
"allowSyntheticDefaultImports": true,
"useDefineForClassFields": true,
"resolveJsonModule": true,
"lib": [
"esnext",
"dom",
],
"types": [
"@dcloudio/types",
"@types/wechat-miniprogram",
"@uni-helper/uni-app-types",
"@uni-helper/uni-ui-types",
],
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
},
},
"vueCompilerOptions": {
"nativeTags": [
"block",
"component",
"template",
"slot"
]
},
"exclude": [
"node_modules",
"unpackage",
"src/**/*.nvue"
]
}

vueCompilerOptions以及nativeTags的配置是必须的。否则uniapp的标签比如view会出现类型错误

baseUrl 和 paths 用于映射@/开头的 import 路径。详情参考TypeScript: TSConfig

安装TS类型支持包。前者是微信小程序组件的TS类型,后者是uniapp组件的TS类型。这些类型已在上方配置文件中的types添加。

1
npm i -D @types/wechat-miniprogram @uni-helper/uni-app-types

json文件相关

在 uniapp 项目中,我们可能会在 json 文件中有注释,vscode 识别 json 文件时,不允许有注释在其中,会报错,如下图所示。

json文件内注释报错

解决方法:在 vscode 的设置中,搜索 associations ,添加文件关联键值对,即 xxx.json   jsonc 。如下图所示,报错已消失。

注:.json 文件是标准的 json 格式,除了不允许有注释以外,还有其它的一些严格规则。.jsonc 文件则是拓展的 json 格式,能有注释在其中,以及可以省略 key 的引号等等。像 package.json 这种文件,最好还是不要加注释在里面。

vscode文件关联

uni-ui

uni-ui 介绍 | uni-app官网 (dcloud.net.cn)

安装 uni-ui 所需的 sass 预处理器和加载器

安装 sass

1
npm i sass -D   或   yarn add sass -D  

安装 sass-loader

1
npm i sass-loader@10.1.1 -D   或   yarn add sass-loader@10.1.1 -D

安装uni-ui

1
npm i @dcloudio/uni-ui   或   yarn add @dcloudio/uni-ui

配置uni-ui组件自动导入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// pages.json
{
"easycom": {
"autoscan": true,
"custom": {
// uni-ui 规则如下配置
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
},

// 其他内容
pages:[
// ...
]
}

安装 uni-ui 的 ts 类型包

1
npm i -D @uni-helper/uni-ui-types

然后在 tsconfig.json 中的 types 里写上对应的类型。现在就可以在 uni-ui 的组件标签上看到对应的类型了。

Pinia

pinia本体

本人采用 vue 版本 3.4.21, pinia 版本 2.0.36,不会发生依赖冲突以及各种报错。

1
npm i pinia@2.0.36

创建 pinia 实例。

1
2
3
4
5
6
7
8
9
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia()
const app = createApp(App)

app.use(pinia)
app.mount('#app')
常用操作

使用 storeToRefs() 取出 store 中的响应式数据。

1
2
3
4
5
import { useUserStore } from '../../stores/piniaStores'
import { storeToRefs } from 'pinia';

const userStore = useUserStore();
const {nickName} = storeToRefs(userStore);

pinia的持久化插件

Home | pinia-plugin-persistedstate (prazdevs.github.io)

1
npm i pinia-plugin-persistedstate

注意:是 pinia-plugin-persistedstate 这个插件,而非 pinia-plugin-persist 这个坑比插件。后者使用自定义 Storage 的时候,比如我是用的 uni 的storage,会报错说没有sessionStorage,这让我研究了一下午自己的官方文档,怀疑是自己的问题,最后得出结论是插件官方的问题。

在 main.ts 中配置持久化。

1
2
3
4
5
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

我配置完成后是如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { createSSRApp } from "vue";
import App from "./App.vue";
import * as Pinia from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

export function createApp() {
const app = createSSRApp(App);
const pinia = Pinia.createPinia();
pinia.use(piniaPluginPersistedstate);
app.use(pinia);

return {
app,
Pinia,
};
}

因为是为了 uniapp 采用的 pinia ,而 uniapp 没有 sessionStorage ,所以在定义 store 的时候需要自定义 storage。参考代码如下。

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
import { defineStore } from 'pinia'
import { StorageLike } from 'pinia-plugin-persistedstate';
import { ref } from 'vue'


const uniStorege: StorageLike = {
setItem(key, value) {
uni.setStorageSync(key, value);
},
getItem(key) {
return uni.getStorageSync(key);
},
}

// 你可以对 `defineStore()` 的返回值进行任意命名,但最好使用 store 的名字,同时以 `use` 开头且以 `Store` 结尾。(比如 `useUserStore`,`useCartStore`,`useProductStore`)
// 第一个参数是你的应用中 Store 的唯一 ID。
export const useUserStore = defineStore('user', () => {
const nickName = ref("Kahvia");

function setName(name: string) {
nickName.value = name;
}

return { nickName, setName };
}, {
persist: {
storage: uniStorege,
},
})

关于插件的其它可选配置,可以参照官方文档。

为 uniapp 的请求添加拦截器

拦截器的作用是,在请求发送前拦截请求,或收到请求结果后拦截结果,对请求或结果作一定的处理后,再放行。常用于简化操作,比如发送请求前统一添加域名前缀,或收到请求结果后提取关键数据、展示错误信息等。减少了代码的冗余。ps:比如我以前做项目的时候,都是在一个 js 文件中定义常量 baseUrl,在有请求的页面引入该 js 文件,采用 baseUrl + ‘/user/upload’ 这种形式的代码,这意味着我有多少个请求,我就写了多少个这样的拼接代码,的确写了很多无用代码。

以下代码块则展示了如何添加请求发送前的拦截器。

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
//utils/http.ts
import { useUserStore } from "@/stores/piniaStores";


const baseUrl: string = "https://www.kahvia.cn";

const httpInterceptor = {
invoke(options: UniApp.RequestOptions) {
//请求添加前缀
if (!options.url.startsWith("http"))
options.url = baseUrl + options.url;
//设置超时时间
options.timeout = 10000;
//设置请求头
options.header = {
//保留原有请求头
...options.header,
//添加请求来源
'source-client': 'miniapp'
};
const userStore = useUserStore();
const token = userStore.profile?.token;
if (token)
options.header.Authorization = token;

},
}

uni.addInterceptor("request", httpInterceptor);
uni.addInterceptor("uploadFile", httpInterceptor);

uniapp 中没有关于请求结果的拦截器,得用 Promise 手动封装。

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
//utils/http.ts
//接上个拦截器代码块

//自己封装的数据类型
interface Data<T> {
code: number,
msg: string,
data: T
}

//封装uni-request,对数据结果进行统一处理,即拦截请求结果。上面的interceptor是请求发送前拦截。
export const http = <T>(options: UniApp.RequestOptions) => {
return new Promise<Data<T>>((resolve, reject) => {
uni.request({
...options,
//服务器有响应结果,不管状态码是200还是401亦或是500,都会走success函数。
success(res) {
//状态码正常,则resolve,axios内部也是如此实现的。
if (res.statusCode >= 200 && res.statusCode < 300) {
//用as对返回类型进行断言
resolve(res.data as Data<T>);
} else if (res.statusCode == 401) {
//401代表token无效,身份验证失败
console.log("Authorization error");
uni.showToast({ title: "身份验证失效,请重新登录" });
reject(res);
} else {
uni.showToast({ title: "服务器异常" });
reject(res);
}
},
//服务器没有返回结果,代表请求未成功发送,即网络异常。
fail(err) {
uni.showToast({ title: "网络异常,请检查网络" })
reject(err);
},
})
})
}

这样一来,自己封装的网络请求模块就完成了。在需要的地方导入使用即可。

注:泛型 T 的使用是为了更好地处理数据。每个请求返回的 data 部分都是变化的,有的是字符数组,有的是对象数组,根据选择使用。

1
2
3
4
5
6
7
8
9
10
import {http} from '@/utils/http'

async function clickTest(){
userStore.setName("Adong");
const res = await http<string[]>({
method:"GET",
url:"/test"
})
console.log(res);
}

当请求的结果有效,即 resolve 后,该 Promise 对象的状态会变成 fulfilled(已兑现),变量 res 有了有效值,继续执行下面的 log 语句。当请求的结果无效,即 reject 后,该 Promise 对象的状态变为 rejected(已拒绝),此时异步函数直接结束,不会执行下面的 log 语句。

注:Promise 对象的初始状态为 pending(待定)。

uniapp知识点

svg

uniapp 中不支持 svg 标签,所以涉及到的 svg 图片可以转换成 base64 进行使用。

SVG在线工具。通过这个工具我们可以实现上述操作。我就用这个工具将 iconfont 上面的图标转换成了 base64 ,用 img 标签进行了插入。

1
<img class="searchIcon"           src="data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCI+PHBhdGggZD0iTTEwMjQgOTYzLjdMNzU2LjggNjk2LjVjNjAuMi03My42IDk2LjUtMTY3LjUgOTYuNS0yNjkuOEM4NTMuMyAxOTEuNCA2NjEuOSAwIDQyNi43IDBTMCAxOTEuNCAwIDQyNi43czE5MS40IDQyNi43IDQyNi43IDQyNi43YzEwMi4zIDAgMTk2LjMtMzYuMyAyNjkuOC05Ni41TDk2My43IDEwMjRsNjAuMy02MC4zek00MjYuNyA3NjhDMjM4LjUgNzY4IDg1LjMgNjE0LjkgODUuMyA0MjYuN1MyMzguNSA4NS4zIDQyNi43IDg1LjMgNzY4IDIzOC41IDc2OCA0MjYuNyA2MTQuOSA3NjggNDI2LjcgNzY4eiIgZmlsbD0iI2ZmZiIvPjwvc3ZnPg==">

自定义导航栏

设置 navigationStyle 为 custom 以取消默认导航栏。然后手写导航栏组件添加到页面中使用即可。

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
{
"easycom": {
"autoscan": true,
"custom": {
// uni-ui 规则如下配置
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
},
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
//取消默认导航栏
"navigationStyle": "custom",
"navigationBarTitleText": "首页",
"navigationBarTextStyle": "white"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "uni-app",
// "navigationBarBackgroundColor": "#F8F8F8",
// "backgroundColor": "#F8F8F8"
}
}

image标签

uniapp 中不能使用 img 标签。使用 img 标签后,会被解析成 image 标签,导致为 img 写的 css 样式失效。

样式隔离

在某些情况下,比如我写了一个组件,里面用到了 uniapp 提供的内置组件,如 uni-file-picker ,当我尝试通过 vue 的样式穿透去修改内置组件的样式时,怎么改都不行,用上 !important 也不行,在调试的时候看穿透的样式根本没修改成功。这时候或许就是 uniapp 自带的样式隔离起了作用。使用以下代码进行样式隔离的取消,可以解决问题。

1
2
3
4
5
6
7
<script lang="ts">
export default {
options: {
styleIsolation: 'shared'
}
}
</script>

非常傻逼的一个问题。样式隔离。这个文档是微信官方文档,因此推断是微信小程序的特有设定。

禁止手指滑动

在自定义的加载遮罩显示时,需要禁止手指滑动,以防下面的页面被滑动。

在最外层添加 @touchmove.stop.prevent=”() => { }” 。

1
2
3
4
5
6
7
<template>
<div @touchmove.stop.prevent="() => { }" class="loading" :class="{ alignTop: ifAlignTop, alignBottom: !ifAlignTop }"
v-if="isLoading">
<uni-load-more icon-type="circle" color="white" status="loading" :icon-size="30"
:content-text="{ contentrefresh: content, contentdown: '', contentnomore: '' }" />
</div>
</template>

自定义遮罩的位置问题

在 uniapp 中,App.vue 文件中并不能写 template ,所以全局遮罩并不方便实现。 所以需要定义一个遮罩组件,在每个页面分别引入使用。

然而,遮罩放在每个页面的根节点下有时也并不方便。举个例子,遮罩的高度一般是100vh,假设我通过绝对定位对页面进行上左对齐,当页面可以滚动的时候,即页面高度大于100vh的时候,如果我在页面底部触发了遮罩,由于遮罩高度不够,页面底部就是无遮罩状态。如果我对遮罩的高度预设成300vh,可以达到遮住底部的效果,但是遮罩中间的加载用的提示信息又跑到了别处,而不在屏幕中间。想要动态控制遮罩的高度或者其中的文字位置是不现实的,每个页面的高度都不一样,而 uniapp 中又不容易获取滚动高度。

因此,固定遮罩的高度为100vh,尝试调整遮罩的引入位置以及对齐位置才是更合理的做法。比如我在页面底部触发了从底部弹出的弹出层,那么封装一个组件,引入弹出层和遮罩层,将自定义组件标记为相对定位,遮罩层标记为绝对定位,并使遮罩下左对齐。

scroll-view 滚动条问题

在微信小程序中,仅设置 show-scrollbar = “false” 是没用的,同时还要设置 enhanced = “true”。

1
2
3
4
5
6
<scroll-view :scroll-y="true" class="cats" :show-scrollbar="false" :enhanced="true">
<div :key="index" v-for="(cat, index) in categories" class="cat" :class="{ catActive: catIsActive(index) }"
@click=setActive(index)>
<text>{{ cat.name }}</text>
</div>
</scroll-view>

别用uniapp的这个组件写 flex 布局。因为flex布局中的 align-content 会无效,调了一下午没吊用。🐕都不用。自己写的 flex 布局盒子好用的一批。