uni-app+vue3+vant开发微信小程序探路...

12,343 阅读10分钟

一、简单说说准备工作:

创建项目

  • 工具: HBuilder最新稳定版, uni-app官网可下载
  • 菜单栏:文件-> 新建 -> 项目,创建一个空白的基于vue3版本的模板目录即可 image.png

调试

  • 工具:预先下载安装 微信开发者工具
  • HBuilder中选中当前项目的任何一个文件
  • image.png
  • 菜单栏:运行-> 运行到小程序模拟器 ->微信开发者工具
  • 运行成功会自动启动微信开发者工具,等待编译完成会看到测试界面
  • image.png

【坑】开发模式进行真机调试会白屏,需要在发行模式下进行真机调试

【坑】如出现微信开发者工具console里面爆红,可以试试点一下微信开发者工具上方菜单里面的重新编译,说不定就好了(* ̄︶ ̄)

发布

  • 菜单栏:发行-> 小程序-微信
  • 此时需要一个小程序AppId,需在微信公众平台上开通

二、一些技巧和坑

1.引用vant组件

引入组件目录

  1. /pages 同级创建 /wxcomponents/vant 目录
  2. 下载微信小程序版本的vant代码
  3. 解压代码,把/dist目录内的文件拷贝进去新建的/wxcomponents/vant目录中
  4. 开发过程中应参考对应版本的文档:vant-contrib.gitee.io/vant-weapp 但需要把对应的引用语法改成vue的语法,如:
<van-cell-group>
    <van-field value="{{ value }}" placeholder="请输入用户名" border="{{ false }}"
    bind:change="onChange" />
</van-cell-group>

改为:

<van-cell-group>
    <van-field :value="value" placeholder="请输入用户名" :border="false"
    @change="onChange" />
</van-cell-group>

引用

修改pages.json文件

全局引用

"globalStyle":{} 属性下加入以下片段,可按需引入具体需要全局引入的组件,引入规则如下:

"usingComponents": {
   "van-cell-group": "/wxcomponents/vant/cell-group/index",
   "van-field": "/wxcomponents/vant/field/index",
   //...
}

image.png

单页引用

把上述的代码放在对应的页面配置内,如:

image.png

【坑】报错??

页面【wxcomponents/vant/info/index]错误:
 Error: module 'wxcomponents/vant/info/index.js' is not defined,
 require args is 'wxcomponents/vant/info/index.js'

image.png

WAServiceMainContext.js?t=wechat&s=1666746784000&v=2.27.0:1 SyntaxError: 
Cannot use import statement outside a module(env: Windows,mp,1.06.2209190; lib: 2.27.0)

image.png

【填坑】

这是因为微信开发者工具没有启用ES6转ES5功能而报错

微信开发者工具右上角:详情-> 本地设置 -> 勾选“将JS编译成ES5”即可 image.png

但可能重新编译或热重载后,该选项“将JS编译成ES5”又不勾选了,此时可以直接去修改项目文件manifest.json, 勾选微信小程序配置-> ES6转ES5 即可

image.png

如打开的是manifest.json的源码,可以在对应的位置增加以下配置:

image.png

【坑】引用van-toast组件无效或报错

1.未找到van-toast 节点,请确认 selector 及 context 是否正确

组件内引用切记参考官方文档,需要给页面引入一个van-toast组件,才能在script内使用 Toast() 的方法

<van-toast id="van-toast" />

注意:只要可能会调用Toast()的界面,都需要插入该组件

2.在非组件的js文件中引用Toast(),正常使用可能会像如下的方式:

image.png

开发模式下是可以正常调用,但是在发行模式下,会报错

vendor.js? [sm]:1 TypeError: s.Toast is not a function

image.png

TypeError: Cannot read property 'success' of undefined

image.png

如果把Toast打印出来,会发现为null, 原因是Toast并未正常引入

理由揭秘:

打开/wxcomponents/vant/toast/toast.js,你会发现,最后一句代码为:

export default Toast;

开发模式下,初次编译,在微信开发者工具打开这个文件,发现这句话没有变,编译过程并没有把它进行处理

而当回去修改了一下代码,热重载后,这句话就变成了:

exports.Toast = Toast;

估计就是微信开发者工具进行了再次编译,把一些组件目录的代码进行转换了,而uni-app没有对这些代码进行处理

发布模式下,不存在热重载,在微信开发者工具打开这个文件,就会发现它一直是

export default Toast;

而编译后引用这个文件的代码始终是一样的:

image.png

都是通过[module].Toast()来进行引用。

这就会导致一个问题,我们什么都不改直接引用的话,会出现热重载后是能正常,初始加载和发布都不能正常使用,因为 exports.Toastexport default Toast我们引入后的内容是不一样的

详细原理可参考这个地址

【填坑】

/wxcomponents/vant/toast/toast.js的最后一句导出语句修改为:

exports.Toast = Toast;

引入语句修改为:

const Toast = require('../wxcomponents/vant/toast/toast.js').Toast

export function loadInfo() {
	Toast('加载成功')
}

大功告成!😀

若有更优美的解决方案欢迎交流~

2.自定义顶部导航栏,以便于修改顶栏样式(还原UI设计图;加背景图、Logo之类..)

修改pages.json文件

{
    ...,
    "globalStyle": {
        "navigationStyle": "custom",
        ...
     }
}

创建对应的自定义组件: components/commonHeader/commonHeader.vue, 在界面中引入即可(可按下方第4点的方式进行按需引入的设置)

私人tips

获取系统状态栏和菜单高度,用于设置自定义顶部导航栏的高度

修改App.vue文件

onLaunch: function() {
    console.log('App Launch')
    uni.getSystemInfo({
        success: function(e) {
            // #ifdef MP-WEIXIN
            uni.$StatusBarHeight = e.statusBarHeight;
            // getMenuButtonBoundingClientRect 用于获取页面右上角圆角按钮的位置信息
            let custom = wx.getMenuButtonBoundingClientRect();
            uni.$CustomBarHeight = custom.bottom - e.statusBarHeight + 8
            // #endif

            //uni 为全局对象,可以挂载全局参数,在其他组件可以直接使用
        }
    })
}

私人tips2: 监听页面滚动动态修改顶部导航栏样式

场景:可用于全屏背景图,滚动后需要修改顶部导航栏的背景和文字颜色的场景

// page代码
onPageScroll(e) {
        const { scrollTop } = e
        // pageTop 可指定滚动高度
        const pageTop = 100
        if (scrollTop > pageTop) {
                // headerBg, headerColor 可以是自定义导航栏的一些props, 按需使用
                this.headerBg = '#fff'
                this.headerColor = 'black'
                
                // 修改状态栏样式(顶部的时间日期等内容)
                uni.setNavigationBarColor({
                        frontColor: '#000000',
                        backgroundColor: '' // 必填项,不能不传,但可以为空字符串
                })
        } else {
                this.headerBg = 'transparent'
                this.headerColor = 'white'
                uni.setNavigationBarColor({
                        frontColor: '#ffffff',
                        backgroundColor: ''
                })
        }
}

3.自定义底部菜单栏tabbar

修改pages.json文件

{
    ...,
    "tabBar": {
        "custom": true,
        "list": [{
                        "pagePath": "pages/index/index",
                        "text": "首页"
                },
                {
                        "pagePath": "pages/passed/index",
                        "text": "主页面1"
                },
                {
                        "pagePath": "pages/my/index",
                        "text": "个人中心"
                },
                ...
        ]
    }
}

创建对应的自定义组件: components/commonTabbar/commonTabbar.vue, 在对应主界面中引入即可,可以使用vanttabbar组件,可参考如下:

<template>
    <van-tabbar :active="active" active-color="#00C84A" inactive-color="#CDCDCD" :border="false" @change="onChange">
        <van-tabbar-item v-for="item in tabs" :key="item.name">
                <image slot="icon" :src="item.iconNormal" mode="contain" style="width: 48rpx; height: 48rpx;" />
                <image slot="icon-active" :src="item.iconActive" mode="contain" style="width: 48rpx; height: 48rpx;" />
                {{item.name}}
        </van-tabbar-item>
    </van-tabbar>
</template>
<script>
export default {
    props: {
        current: {
            type: Number,
            default: 1
        }
    },
    data() {
        return {
           active: 1,
           tabs: [{
                    name: '主页面1',
                    iconNormal: '/static/passed.png',
                    iconActive: '/static/passed-active.png',
                    path: '/pages/passed/index'
                },
                {
                    name: '首页',
                    iconNormal: '/static/index.png',
                    iconActive: '/static/index-active.png',
                    path: '/pages/index/index'
                },
                {
                    name: '个人中心',
                    iconNormal: '/static/my.png',
                    iconActive: '/static/my-active.png',
                    path: '/pages/my/index'
                }
            ]
        }
    },
    watch: {
        current: {
            immediate: true,
            handler: function(val) {
                    this.active = val
            }
        }
    },
    methods: {
        onChange(e) {
            const current = e.detail
            uni.switchTab({
                    url: this.paths[current]
            })
        }
    }
}

私人tips

为了使界面主要内容不受tabbar覆盖,可以在van-tabbar组件前面加入一个空节点,如下:

    <view class="footer-empty">&nbsp;</view>
    //css 样式
     .footer-empty {
	height: 100rpx;
	padding-bottom: env(safe-area-inset-bottom);
    }

4.使用easycom自动按需加载自定义组件(为什么我的组件总是无法自动识别?)

修改pages.json文件

{
    ...,
    "easycom": {
            "autoscan": true,
            "custom": {
                "^uni-(.*)": "@/components/uni-$1.vue", // 匹配components目录内的vue文件
                "^vue-file-(.*)": "packageName/path/to/vue-file-$1.vue" // 匹配node_modules内的vue文件
            }
    }
}

只要开启了autoscan, 会自动扫描目录规则符合/components/组件名称/组件名称.vue的所有文件,自动进行引入,无需再在custom中引入

引入后page中直接使用组件即可

注意:如果新建的组件引用无效,在HBuilder中重新编译项目试试

5.引用富文本显示和编辑组件mp-html

官方文档

mp-html 默认只有显示富文本功能,需要支持编辑状态的话,要额外代码, 参考 editable模式设置文档 小程序示例代码

其实引用组件不麻烦,参照demo,可以按照自己的需求修改菜单icon之类的,比较方便

6.使用微信原生小程序提供的wxml-to-canvas进行动态海报生成以及保存到本地

官方文档

为了方便改造,不采用npm的方法进行安装,直接打开代码片段

image.png

在代码片段中对应的两个目录,拷贝到自己的项目中/wxcomponents目录下

image.png

改造目的和内容:

1.wxml-to-canvas通常用于生成海报,而海报又通常通过popup的形式弹出,所以需要改造渲染时机,等popup弹出后再渲染

默认引用即渲染,但是canvas没法在一个隐藏节点中渲染,所以把默认的渲染时机改为手动(通过自定义代码调用渲染)

修改wxml-to-canvas/index.js文件

  • 在methods中新增一个方法:
methods: {
    initCanvas() {
        ...
    }
}
  • attached中的渲染代码剪切进去

image.png

  • canvas需要先初始化,初始化结束后再调用renderToCanvas进行绘制, 否则会报错 网上很多方案都是用setTimeout来进行延迟调用, 但是我觉得这是不稳妥的, 延迟时间设长了设短了都不好, 于是我把initCanvas写成一个异步函数,如下:
methods: {
    initCanvas() {
        return new Promise((resolve,reject) => {
            const dpr = wx.getSystemInfoSync().pixelRatio
            const query = this.createSelectorQuery()
            this.dpr = dpr
            query.select('#canvas')
              .fields({node: true, size: true})
              .exec(res => {
                const canvas = res[0].node
                const ctx = canvas.getContext('2d')
                canvas.width = res[0].width * dpr
		canvas.height = res[0].height * dpr
                ctx.scale(dpr, dpr)
                this.ctx = ctx
                this.canvas = canvas
                resolve(canvas)
             })
        })
    },
    ...
},

  • 页面代码中:

插入组件

<van-popup :show="showPopup" :close-on-click-overlay="true" custom-style="background:transparent" @close="showPopup = false" @after-enter="opened">
    <view>
      <wxml-to-canvas ref="widget" class="widget"></wxml-to-canvas>
    </view>
</van-popup>

在popup弹出后,调用方法

methods: {
    async opened() {
        const canvas = await this.$refs.widget.initCanvas()
        Toast.loading({
            message: '加载中',
            forbidClick: true
        });
        const obj = { wxml: '...', styles: '...'}
        this.$refs.widget.renderToCanvas(obj).then(res => {
            Toast.clear()
        })
    }
}

2.canvas自适应宽高改造:

  • 获取实际屏幕宽度和设计稿的比例,计算每个元素的实际尺寸和边距等

  • 修改wxml-to-canvas/index.js文件

    • 修改attached()方法
    attached() {
        const sys = wx.getSystemInfoSync();
        //拿到设备的宽度,跟设计图宽度的比例
        const screenRatio = sys.screenWidth / 375;
        this.setData({
                screenRatio,
                width: this.data.width * screenRatio,
                height: this.data.height * screenRatio,
        });
    }
    
    • 修改initCanvas()方法
    initCanvas() {
        // ...省略
        // canvas.width = res[0].width * dpr
        // canvas.height = res[0].height * dpr
        canvas.width = this.data.width * dpr;
        canvas.height = this.data.height * dpr;
        // ...省略
    }
    
    • 修改renderToCanvas()方法
    renderToCanvas() {
        // const {wxml, style} = args
        const { wxml, fnGetStyle } = args
         // 通过fnGetStyle方法传入屏幕与设计稿的比例,用以计算样式
        const style = fnGetStyle(this.data.screenRatio)
        // ...省略
    }
    
    
  • 可新建一个文件进行canvas节点和样式的配置, 如canvasConfig.js:

// canvasConfig.js
// function 形式便于传入动态数据
export default function(info) {
    const wxml = `<view class="container">
        <view class="absolute mainBox"></view>
    </view>`
    const fnGetStyle = (screenRatio) => {
        return {
            // 编写样式, 名称需以驼峰命名
            absolute: {
               position: 'absolute'
            },
            container: {
                width: 300*screenRatio, //通过乘以倍率,计算出在当前屏幕分辨率下的实际宽度, 其他尺寸同理
                height: 471*screenRatio,
                position: 'relative'
            },
            mainBox: {
                width: 200*screenRatio,
                padding: 10*screenRatio,
                margin: 10*screenRatio,
                color: '#eeeeee'
            }
        }
    }
    return {
        wxml,
        fnGetStyle
    }
}

  • 修改页面中的组件调用方法:
import canvasConfig from './canvasConfig.js'
// ...省略
methods: {
    opened() {
        ...
        const info = { title: '标题' }
        const obj = canvasConfig(info)
        this.$refs.widget.renderToCanvas(obj).then(res => {
           Toast.clear()
        })
    }
}

生成图片并保存到本地

页面代码中:

methods: {
  //...
  extraImage() {
    this.$refs.widget.canvasToTempFilePath().then(res => {
        this.saveImg(res.tempFilePath)
    })
  }, 
  async saveImg (tempFilePath) {
    const _this = this
    uni.saveImageToPhotosAlbum({
        filePath: tempFilePath,
        success: async (res) => {
            uni.showModal({
                content: '图片已保存,分享给好友吧!',
                showCancel: false,
                confirmText: '好的',
                confirmColor: '#333',
                success: function (res) {
                    // ...
                },
                fail: function (res) {
                   // ...
                }
            });
        },
        fail: function (res) {
            Toast.fail('您取消了授权')
        }
    });
  }
}

【坑】明明添加了内容,页面却还是空白

  • wxml及样式的编写需要严格遵循使用规则,有些固定的要求及支持的样式,不遵循的话可能会导致渲染不出来

【坑】真机调试报错 fail canvas has not been created

  • 按照上述的方法应该不会报这个问题
  • 还报的话,检查是否canvas内的元素的渲染尺寸超出了canvas的实际宽高,超出则可能渲染失败

这次分享这么多,谢谢观看!

image.png

仍有无法解决的问题欢迎交流!

ps: 为了图快,本项目其实并没有用到vue3的一些新特性(懒得查文档), 就这么凑合吧哈哈


补充温馨提示:【大大大坑】

微信官方api wx.getUserProfile 也被砍了,就算用户点了授权,你也获取不到用户昵称和头像,只能获取到匿名信息和一个灰色的默认头像(官方说法是:没必要,见公告),需要使用微信头像的可以改为编辑用户信息,上传头像的时候,支持选择微信头像(当然交互上怎么提示大家看实际情况),具体参考这里

如果你发现你的 wx.getUserProfile还是能获取到非匿名信息,那可能是版本bug (* ̄︶ ̄)

image.png

一言以蔽之:你爱咋咋地呗┓( ´∀` )┏