- 小程序粘性布局组件实现
一、前言 开发中,我们经常会遇需要让组件在屏幕范围内时,按照正常布局排列,而组件滚出屏幕范围时,让其始终固定在屏幕顶部的情况,也就是常说的粘性布局。今天我们就一起用小程序来实现一个适用于不同场景下的粘性布局组件。 二、demo演示 如图,实现的组件主要适用于以下几种场景: 吸顶页面最上方; 吸顶与页面有固定距离的位置; 在指定容器内吸顶; 嵌套在scroll-view中吸顶。 [图片] 三、代码演示 其中,粘性组件通过<weimob-sticky></weimob-sticky>调用,参数信息用法如下: 参数 说明 类型 默认值 offset-top 吸顶时与顶部的距离,单位px number 0 z-index 吸顶时的 z-index number 99 container 一个函数,返回容器对应的 NodesRef 节点 function - scroll-top 当前滚动区域的滚动位置,非 null 时会禁用页面滚动事件的监听 number - 滚动时触发scroll函数,其中isFixed为是否吸顶,scrollTop为距离顶部的位置。详细代码如下。 3.1 页面代码 3.1.1 基础用法 [代码]<view class="weimob-block"> <view class="weimob-title">基础用法</view> <view class="weimob-body"> <weimob-sticky> <!-- 需要粘性的部分 --> <button class="margin-left-base" size="mini"> 基础用法 </button> </weimob-sticky> </view> </view> [代码] 3.1.2 吸顶距离 [代码]<view class="weimob-block"> <view class="weimob-title">吸顶距离</view> <view class="weimob-body"> <!-- 吸顶时与顶部的距离,单位px --> <weimob-sticky offset-top="{{ 50 }}"> <!-- 需要粘性的部分 --> <button class="margin-left-top" type="primary" size="mini"> 吸顶距离 </button> </weimob-sticky> </view> </view> [代码] 3.1.3 指定容器 [代码]<view class="weimob-block"> <view class="weimob-title">指定容器</view> <view class="weimob-body"> <!-- 这里需要固定高度 --> <view id="container" style="height: 300rpx;background-color: #fff"> <weimob-sticky container="{{ container }}"> <button size="mini" class="margin-left-special"> 指定容器 </button> </weimob-sticky> </view> </view> </view> [代码] 3.1.4 嵌套在scroll-view使用 [代码]<view class="weimob-block"> <view class="weimob-title">嵌套在 scroll-view 内使用</view> <!-- 这里需要固定高度,scroll-view里的元素高度需要大于其高度 --> <scroll-view bind:scroll="onScroll" scroll-y id="scroller" style="height: 400rpx; background-color: #fff;margin-top: 40rpx;" > <view style="height: 800rpx"> <weimob-sticky scroll-top="{{ scrollTop }}" offset-top="{{ offsetTop }}" > <button size="mini" class="margin-left-scoll"> 嵌套在 scroll-view 内 </button> </weimob-sticky> </view> </scroll-view> </view> [代码] 页面js [代码]Page({ data: { container: null, //一个函数,返回容器对应的 NodesRef 节点 scrollTop: 60, // 当前滚动区域的滚动位置,非null时会禁用页面滚动事件的监听 offsetTop: 0 // 吸顶时与顶部的距离,单位px }, onReady() { // 页面渲染完,获取节点信息 this.setData({ container: () => wx.createSelectorQuery().select('#container'), }); }, onScroll(event) { // 容器滚动时获取节点信息 wx.createSelectorQuery() .select('#scroller') .boundingClientRect((res) => { this.setData({ scrollTop: event.detail.scrollTop, offsetTop: res.top, }); }) .exec(); } }); [代码] 3.2 组件代码 组件wxml [代码]<wxs src="./index.wxs" module="computed" /> <view class="weimob-sticky" style="{{ computed.containerStyle({ fixed, height, zIndex }) }}" > <view class="{{ fixed ? 'weimob-sticky-wrap--fixed' : ''}}" style="{{ computed.wrapStyle({ fixed, offsetTop, transform, zIndex }) }}" > <slot /> </view> </view> [代码] 组件wxs 这里使用使用小程序的wxs对吸顶元素的transform,top,height,z-index元素进行实时渲染,ios设备在滚动监听时性能会优于在js 2-20倍,androd设备效率暂无差异。 [代码]function wrapStyle(data) { var style = ""; if (data.transform) { style += 'transform: translate3d(0, ' + data.transform + 'px, 0);' } if (data.fixed) { style += 'top: ' + data.offsetTop + 'px;' } if (data.zIndex) { style += 'z-index: ' + data.zIndex + ';' } return style; } function containerStyle(data) { var style = ""; if (data.fixed) { style += 'height: ' + data.height + 'px;' } if (data.zIndex) { style += 'z-index: ' + data.zIndex + ';' } return style; } module.exports = { wrapStyle: wrapStyle, containerStyle: containerStyle } [代码] 组件js [代码]import pageScrollMixin from "./page-scroll"; const ROOT_ELEMENT = ".weimob-sticky"; Component({ options: { multipleSlots: true }, properties: { zIndex: { type: Number, value: 99 }, offsetTop: { type: Number, value: 0, observer: "onScroll" }, disabled: { type: Boolean, observer: "onScroll" }, container: { type: null, observer: "onScroll" }, scrollTop: { type: null, observer(val) { this.onScroll({ scrollTop: val }); } } }, data: { height: 0, fixed: false, transform: 0 }, behaviors: [pageScrollMixin(function pageScrollMixinCallback(event) { // 非null时会禁用页面滚动事件的监听 if (this.data.scrollTop != null) { return; } this.onScroll(event); })], lifetimes: { attached() { this.onScroll(); } }, methods: { onScroll({ scrollTop } = {}) { const { container, offsetTop, disabled } = this.data; if (disabled) { this.setDataAfterDiff({ fixed: false, transform: 0 }); return; } this.scrollTop = scrollTop || this.scrollTop; if (typeof container === "function") { // 情况一:指定容器下时,吸顶距离+吸顶元素高度>容器高度+容器距顶部距离,随页面滚动; // 情况二:指定容器下时,吸顶距离>吸顶元素高度,元素固定; // 情况三:元素初始化。 // this.getRect获取节点ROOT_ELEMENT相对于显示区域的top,height等信息,通过root获取 // this.getContainerRect获取父容器相对于显示区域的top,height等信息,通过container获取 Promise.all([this.getRect(ROOT_ELEMENT), this.getContainerRect()]).then( ([root, container]) => { if (offsetTop + root.height > container.height + container.top) { this.setDataAfterDiff({ fixed: false, transform: container.height - root.height }); } else if (offsetTop >= root.top) { this.setDataAfterDiff({ fixed: true, height: root.height, transform: 0 }); } else { this.setDataAfterDiff({ fixed: false, transform: 0 }); } }); return; }else{ this.getRect(ROOT_ELEMENT).then(root => { // 吸顶时与顶部的距离小于可视区域的top距离时,随着滚动条滚动,否则吸顶 if (offsetTop >= root.top) { this.setDataAfterDiff({ fixed: true, height: root.height }); this.transform = 0; } else { this.setDataAfterDiff({ fixed: false }); } return Promise.resolve(); }); } }, setDataAfterDiff(data) { // 比较数据是否与上次相同,不同则触发父组件scroll事件更新isFixed,scrollTop。 wx.nextTick(() => { const diff = Object.keys(data).reduce((prev, key) => { const prevCopy = prev; if (data[key] !== this.data[key]) { prevCopy[key] = data[key]; } return prevCopy; }, {}); this.setData(diff); this.triggerEvent("scroll", { scrollTop: this.scrollTop, isFixed: data.fixed || this.data.fixed }); }); }, getContainerRect() { const nodesRef = this.data.container(); return new Promise(resolve => nodesRef.boundingClientRect(resolve).exec()); }, getRect(selector) { return new Promise(resolve => { wx.createSelectorQuery().in(this).select(selector).boundingClientRect(rect => { resolve(rect); }).exec(); }); } } }); [代码] page-scroll.js 滚动事件在页面进入和离开时共享的pageScrollMixin函数。 [代码]function getCurrentPage() { const pages = getCurrentPages(); return pages[pages.length - 1] || {}; } function onPageScroll(event) { const { weimobPageScroller = [] } = getCurrentPage(); weimobPageScroller.forEach(scroller => { if (typeof scroller === "function" && event) { // @ts-ignore scroller(event); } }); } const pageScrollMixin = scroller => Behavior({ attached() { const page = getCurrentPage(); if (Array.isArray(page.weimobPageScroller)) { page.weimobPageScroller.push(scroller.bind(this)); } else { page.weimobPageScroller = typeof page.onPageScroll === "function" ? [page.onPageScroll.bind(page), scroller.bind(this)] : [scroller.bind(this)]; } page.onPageScroll = onPageScroll; }, detached() { const page = getCurrentPage(); page.weimobPageScroller = (page.weimobPageScroller || []).filter(item => item !== scroller); } }); export default pageScrollMixin; [代码] 总结 最后,我将上述代码放在了代码片段中供大家使用了解,https://developers.weixin.qq.com/s/qiym3wmr7znx ,希望能够帮到小伙伴们,欢迎评论区建议或指教哦~
2021-01-26 - Webpack学习《接入Typescript》
TypeScript | 是 JavaScript 的一个超集,主要提供了类型系统和对 ES6 的支持,设计目标是开发大型应用,它可以编译成纯 JavaScript,编译出来的 JavaScript 可以运行在任何浏览器上。 传统调试TypeScript Typescript 代码需要通过构建将它转换成 JavaScript 代码后才能运行。 // show .ts 为 show 函数增加类型检查 export function show (content : string) { console.log('Hello ,' + content) } // main.ts 通过 ES6 模块规范导如 show 函数 import {show) from './show '; // 执行 show 函数 show ('Webpack '); 我们需要在当前项目的根目录下新建一个用于配置编译选项的 tsconfig.json 文件,编译器默认会读取和使用这个文件,配置文件的内容大致如下: [图片] 通过 npm install -g typescript 安装编译器到全局后,可以通过 tsc main. ts 命令编译 器到全局后,可以通过 tsc main. ts 命令编译 集成 Webpack 通过 Loader TypeScript 转换成 JavaScriptWebpack 在寻找模块对应的文件时需要尝试 ts 后缀。在运行构建前 npm i -D typescript awesome-typescript-loader 配置如下: const path = require('path') module.exports = { entry: './main', output: { filename: 'bundle.js', path: path.resolve(_dirname,'./dist') }, resolve: { // 先尝试以 ts 为后缀的 Typescript 源码文件 extensions : [ '.ts', '.js'] }, module: { rules: [ { test : /\.ts$/, loader: 'awesome-typescript-loader' } ] }, devtool:'so urce-map', // 输出 ur ce Map 以方便在浏览器里调试 Types cript 代码 } 总结由于对webpack比较感兴趣, 所以看了一些资料 《深入浅出Webpack.pdf--吴浩麟》后,决定打包配置引入TypeScript,小试一把。
2020-01-25 - 小程序webview使用
一、使用原因 由于小程序包大小8M的限制 (之前版本)我们组做的是公共活动、工具,业务方会拉取我们的代码, 新活动会不断添加H5小程序都做,维护成本会比较高二、webview引入思路 webview 安全域名申请访问原先小程序活动重定向到webview容器页面并且在url后面追加一些参数(h5页面需要)实现公共的webview页面,需要功能:分享回调、h5路径转发、身份token透传、刷新H5页面H5页面实现功能兼容, 一些活动配置项、分享标题、分享图片、打点(跟小程序和h5环境相关)三、代码实现 原小程序页面重定向webview页面方法包装 [图片] 小程序webview代码实现 Page({ /** * 页面的初始数据 */ data: { url: '', // webview 地址 shareData: {}, // 分享参数 }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { // TODO 登录信息获取 }, /** * 用户点击右上角分享 */ onShareAppMessage: function (options) { // 这里把网址编码了下,用户分享的卡片进去的时候会在onload中得到webview的网址,解码后赋值给src属性就行了 let that = this const shareInfo = { title: that.data.shareData.title, path: `/pages/hudong/common/pages/hdview/index?url=${encodeURIComponent(that.data.shareData.url)}`, imageUrl: that.data.shareData.imgUrl } return shareInfo }, /** * 分享回调 */ onShareback() { let that = this // 分享成功后 通知h5页面(h5有配置xxxback的话), 调用之后需要手动删除 xxxback that.refreshH5(that.data.shareData.xxxback) delete that.data.shareData.xxxback }, /** * 初始化信息 */ initData(options, userInfo = {}) { let url = decodeURIComponent(options.url); const { openid, appid, token } = userInfo; let statType = '' // 记录当前options的链接 this.data.shareData.url = url // h5链接xxxxxxx=xxxxxxxxx 标识 const navUrl = `${url}${url.indexOf('?') > -1 ? '&' : '?'}xxxxxxx=${token}&dotStatType=${statType}` this.setData({ url: navUrl }) }, // 接收h5发送的数据 getMessage(e) { const that = this const data = e.detail.data let action = '' // 注意:每次发送数据后数据会添加到e.detail.data中的数组, // 并不会清除掉上一次传递的数据,所以我们取数组的最后一位 if(data.length > 0) { let item = data[data.length - 1] action = item.action switch(action) { case 'share': that.dealH5Share(item) break case 'saveFormId': // TODO that.dealSaveFormId(item) break } } }, // 处理接收到h5数据 分享操作 dealH5Share(data) { const that = this if(data) { that.data.shareData = data } }, // 处理接收到h5数据 保存formId操作 (待拓展) dealSaveFormId() { // TODO debugger }, // 刷新H5页面当前 refreshH5(xxxback) { if(typeof xxxback != 'object') { return false } const oldUrl = this.data.url let newUrl = oldUrl const oldParams = ParseURL(oldUrl) // 删除历史 oldParams.xxxback && (newUrl = newUrl.replace(`xxxback=${oldParams.xxxback}`, ``)) newUrl += `&xxxback=${(JSON.stringify(xxxback))}` this.setData({ url: newUrl }) } H5分享相关改造 /** * 获取指定的URL参数值 * 参数:paramName URL参数 * 调用方法:getQueryParam("name") * 返回值:tyler */ function _getQueryParam(paramName) { paramValue = "", isFound = !1; if (window.location.search.indexOf("?") == 0 && window.location.search.indexOf("=") > 1) { arrSource = decodeURIComponent(window.location.search).substring(1, window.location.search.length).split("&"), i = 0; while (i < arrSource.length && !isFound) arrSource[i].indexOf("=") > 0 && arrSource[i].split("=")[0].toLowerCase() == paramName.toLowerCase() && (paramValue = arrSource[i].split("=")[1], isFound = !0), i++ } return paramValue == "" && (paramValue = null), paramValue } // 判断是否是小程序环境 function IsMiniProgramEnv(fun) { // 通过navigator.userAgent 判断 var bl = navigator.userAgent && navigator.userAgent.indexOf('miniProgram') > -1 if (typeof fun == 'function') { fun(bl) } } // 初始化执行 IsMiniProgramEnv(function (bl) { if (bl) { IsInMiniProgram = true // 判断是否是小程序环境 _initMiniData() } }) // 设置小程序分享 function SetMiniProgramShare(url, title, imgUrl, fun) { if (title && url && imgUrl) { IsMiniProgramEnv(function (bl) { if (bl) { // 向小程序发送消息 var data = { action: 'share', // 触发修改分享参数动作 url: url, title: title, imgUrl: imgUrl } if(typeof fun === 'function') { data.xxxback = fun() || {} } wx.miniProgram.postMessage({ data: data }) } }) } } // H5跳转小程序路径 function _NavigateToMiniProgram(url) { var newUrl = '/pages/hudong/common/pages/bridge/index?url=' + encodeURIComponent(url) wx.miniProgram.navigateTo({ url: newUrl }) } function _initMiniData() { } // 重新包装链接跳转, 用于跳转业务方的小程序地址或者h5页面地址 function HrefToBusiness(url) { if(!url) { return false; } // 是小程序环境, 并且不是h5链接 if(IsInMiniProgram && url.split("?")[0].indexOf('http') == -1) { _NavigateToMiniProgram(url) } else { window.location.href = url } } webview改造原有小程序项目利弊 优点 减少小程序包大小提升开发、迭代效率大多数需求可省去审核流程,h5最新的功能就是小程序功能缺点 用户体验不如原生小程序不能用复杂的动画交互,简单页面还可以权限限制比较多,例如不能 埋点弹起授权手机号授权等h5、小程序通讯限制太多,具体可参考微信文档
2020-01-25