- 你的小程序总共引入了多少组件?【下篇】
你的小程序总共引入了多少组件?【下篇】 前言 在【上篇】中,我对小程序项目中的代码文件做了一次简单的解析,将页面文件中引入的组件以对象的键值对形式输出到 csv 格式的文件中,数据结构格式如下所示: [代码]{ file: 'xxx.page', components: [ { name: 'xxx', path: 'yyy' } ] } [代码] 但是,我还没有对页面对应的 json 文件所引入组件列表做进一步的分析,也就是对页面的 wxml 文件是否 都全部使用到该组件列表里面的组件 做进一步的分析。 这样做的目的有以下几点: 1. 分析页面的无用组件引入,做进一步的删除代码优化,减少小程序包体积; 2. 统一项目中的组件ui样式,为后期的项目组件化打下根基(尤其是一些老旧项目的代码,错综复杂,代码过于冗余和繁琐)。 准备工作 思路 我在这里简单阐述一下这次编码过程的思路: 在【上篇】文章中我已经可以获取到了每个页面所对应的组件数组列表; 读取 wxml 文件内容字符串,将当前的字符串通过 [代码]html2json[代码] 工具库转换成 json 结构的对象; 遍历组件数组列表,然后递归查找当前 wxml文件转化后的 json 对象,判断是否存在组件的标签字符,存在就输出为true,不存在就输出为false。 我们可以简单看看wxml文件转化后的json对象结构,如下所示: [代码]{ node: 'root', child: [ { node: 'element', tag: 'div', attr: { id: '1', class: 'foo' }, child: [ { node: 'element', tag: 'h2', child: [ { node: 'text', text: 'sample text with ' }, { node: 'element', tag: 'code', child: [{ node: 'text', text: 'inline tag' }] } ] }, { node: 'element', tag: 'pre', attr: { id: 'demo', class: ['foo', 'bar'] }, child: [{ node: 'text', text: 'foo' }] }, { node: 'element', tag: 'pre', attr: { id: 'output', class: 'goo' }, child: [{ node: 'text', text: 'goo' }] }, { node: 'element', tag: 'input', attr: { id: 'execute', type: 'button', value: 'execute' } } ] } ] } [代码] 安装工具包 安装 html2json 包 [代码]npm install --save-dev html2json [代码] 编码 【上篇】文章已经讲解过的逻辑就不再阐述了,有需要的老哥可以移步到这里看看,我主要针对这次主要的 递归比对json对象的逻辑阐述一下: 1. 比对页面json对象是否存在某标签字符 [代码]/** * 判断当前页面是否引入该组件标签 * @param {*} pageJsonData * @param {*} tagName */ const isWxmlImportComponent = (pageJsonData, tagName) => { if (!pageJsonData.child) return false tagName = tagName.toLowerCase() for (let item of pageJsonData.child) { // 判断标签名是否一致 if (item.tag === tagName) { return true } // 递归判断子节点的标签 if (item.child) { const flag = isWxmlImportComponent(item, tagName) if (flag) { return true } } } return false } [代码] 这是这次分享的核心代码,从 [代码]html2json[代码] 对象解析出来的对象我们可以发现,如果父标签有子标签的话,会有一个 [代码]child[代码] 的数组属性,于是我们就要对父标签进行递归操作: [代码] // 递归判断子节点的标签 if (item.child) { const flag = isWxmlImportComponent(item, tagName) if (flag) { return true } } [代码] 并且每个标签都会有一个 [代码]tag[代码] 的属性,属性值就是标签名,于是由此得知,如果当前传入的标签名如果和当前标签一样,就可以退出当前的查找流程: [代码] // 判断标签名是否一致 if (item.tag === tagName) { return true } [代码] 2. 组装拼接文件输出对象数组 我们来看看完整的脚本文件: [代码]const path = require('path'); const { isWxmlImportComponent } = require('./tool/parseUtils'); const { getAllFiles, getFilterFiles, listComponents, getFileJsonData } = require('./tool/fileUtils'); const ObjectsToCsv = require('objects-to-csv'); (async function() { // 解析入口目录 const entryDir = path.resolve(__dirname + '/../dist/pages') const allFiles = getAllFiles(entryDir) if (allFiles.length === 0) return const filterFiles = getFilterFiles(allFiles, ['wxml', 'json']) // 组装导出对象数组数据 const pageWithComponents = filterFiles.reduce((acc, { jsonFile }) => { const current = path.basename(jsonFile, '.json') const currentDir = path.dirname(jsonFile) const components = listComponents(jsonFile) || [] if (components.length == 0) { return [...acc, { page: current, directory: currentDir, }] } else { // 输入wxml地址,转化为json标签对象 const fileJsonData = getFileJsonData(currentDir + `/${current}.wxml`) const childs = components.reduce((childAcc, { name, path }) => { const used = isWxmlImportComponent(fileJsonData, name) return [...childAcc, { page: current, directory: currentDir, component: name, componentPath: path, used: used ? 'true' : 'false', }] }, []) return [...acc, ...childs] } }, []) // 导出csv文件 const csv = new ObjectsToCsv(pageWithComponents) await csv.toDisk(__dirname + '/component_status_stat.csv') })() [代码] 文件导出效果 我这次对导出的数据结构做了一下优化,相比较【上篇】文章的导出格式更为直观易懂,一目了然,大家可以看看: [图片] 项目地址 项目地址:https://github.com/csonchen/wx-mall-components 有需要的同学可以自行下载脚本文件,然后修改一下入口目录的路径,在本地用 node 命令执行脚本文件就可以跑起来了,这样就可以分析出你当前的小程序项目每个页面引用到的组件路径。 总结 关于本次对 如何分析小程序项目引入组件 的分享就到此结束了,这是我曾经接手一个老旧项目的时候所总结下来的一点小小经验; 当你要分析一个项目的时候,可以先通过编写脚本的方式去分析一下里面的代码逻辑,组件引入情况,这样可以避免你在不熟悉业务的前提下,对项目有一个整体的认知,为后期的项目优化提供一份资料作为参考。 欢迎大家继续关注。
2020-11-11 - 全自动、全面化收集 formid 思路,开启脑洞时刻
formid 基础知识 formid 是啥 小程序给用户发消息的唯一途径,所以前端要尽量多收集 formid 保存到服务端; formid 怎么收集 小程序要求必须用户提交表单才能得到formid; form 增加 report-submit 属性,并通过 button submit,前端 formSubmit 事件就能获得 formid ,如下方代码所示: [代码]<form bindsubmit="formSubmit" report-submit class="layout"> <button formType="submit" class="button" hover-class="none"> 。。。 </button> </form> [代码] formid 组件封装(初期思路) 我们将form与button 封装成组件(fomids),通过slot,可以显示任意元素,模拟form submit; 把页面上需要点击的元素用fomids包起来,fomids 将收集的 formid 放入cache,然后通过网络请求将 fomid 传给服务端; 大数小程序都是这么做的,这样做也没有问题,就是略微麻烦,form 与button 组合的样式有时不太好控制 formid 组件封装(进阶思路) 此思路是上面思路的延申,我们把 formids 这个组件做成layout级,把整个页面包在里面,利用事件自动冒泡的特性,只要有点击事件,就能收集到 formid 新建组件layout [代码]<!--components/layout.wxml--> <form bindsubmit="formSubmit" report-submit class="layout"> <button formType="submit" class="button" hover-class="none"> <view class="fixed"><slot></slot></view> </button> </form> [代码] [代码]/* components/layout.wxss */ .layout { display: inline-block; padding-left: 0; padding-right: 0; box-sizing: border-box; font-size: inherit; text-align: left; text-decoration: none; line-height: inherit; -webkit-tap-highlight-color: transparent; color: inherit; width: 100%; position: relative; } .layout .button{ display: inline-block; padding-left: 0; padding-right: 0; box-sizing: border-box; font-size: inherit; text-align: left; text-decoration: none; line-height: inherit; -webkit-tap-highlight-color: #000; color: inherit; width: 100%; position: relative; } .layout .button .fixed{ position:relative; z-index: 9999; width: 100%; } .layout .button:before,.layout .button:after{ border-width: 0; } [代码] [代码]// components/layout.js Component({ methods:{ formSubmit: function(e) { console.log('layout.formids',e.detail.formId) if("the formId is a mock one"!=e.detail.formId){ let formids=wx.getStorageSync('formids') || []; formids.push(e.detail.formId); formids=[...new Set(formids)]; wx.setStorage({key:'formids',data:formids}); } }, }, }) [代码] 将 layout 添加为全局组件 app.json 中增加 [代码]"usingComponents":{ "layout":"/components/layout" }, [代码] 在页面wxml中使用 [代码]<layout> <view class="pages">...</view> </layout> [代码] 怎么将 formid 提给服务端 首先你必须将wx.requxest 进行封装为myRequxest,页面上都是用myRequxest 进行网络请求; myRequxest 中 header 增加formids,从cache中获得放到header中;并删除formid缓存;(需要服务端从header中获得formids并存起来) [代码]function myRequxest(....){ let formids = wx.getStorageSync('formids'); if(formids){ wx.removeStorage({key:'formids'}); } let openid='';//用户openid wx.request({ ... header: { formids:formids.toString(), openid:openid, }, ... }) } [代码] 延申思考 从 写 form 收集 -> 封装 formids 组件 -> 封装 layout(formids) 组件,跳出思维固化,可以做更多可能; 希望这个思路能给你一些触发
2019-07-31 - 小程序原生高颜值组件库--ColorUI
[图片] 简介 ColorUI是一个Css类的UI组件库!不是一个Js框架。相比于同类小程序组件库,ColorUI更注重于视觉交互! 浏览GitHub:https://github.com/weilanwl/ColorUI [图片] 如何使用? 先下载源码包 → Github 引入到我的小程序 将 /demo/ 下的 colorui.wxss 和 icon.wxss 复制到小程序的根目录下 在 app.wxss 引入两个文件 [代码]@import "icon.wxss"; @import "colorui.wxss"; [代码] 使用模板全新开发 复制 /template/ 文件夹并重命名为你的项目,微信开发者工具导入为小程序就可以使用ColorUI了 体验沉浸式导航 [图片] App.js 获取系统参数并写入全局参数。 [代码]//App.js App({ onLaunch: function() { wx.getSystemInfo({ success: e => { this.globalData.StatusBar = e.statusBarHeight; let custom = wx.getMenuButtonBoundingClientRect(); this.globalData.Custom = custom; this.globalData.CustomBar = custom.bottom + custom.top - e.statusBarHeight; } }) } }) [代码] Page.js 页面配置获取全局参数。 [代码]//Page.js const app = getApp() Page({ data: { StatusBar: app.globalData.StatusBar, CustomBar: app.globalData.CustomBar, Custom: app.globalData.Custom } }) [代码] Page.wxml 页面构造导航。更多导航样式请下载Demo查阅 操作条组件。 [代码]<view class="cu-custom" style="height:{{CustomBar}}px;"> <view class="cu-bar fixed bg-gradual-pink" style="height:{{CustomBar}}px;padding-top:{{StatusBar}}px;"> <navigator class='action border-custom' open-type="navigateBack" delta="1" hover-class="none" style='width:{{Custom.width}}px;height:{{Custom.height}}px;margin-left:calc(750rpx - {{Custom.right}}px)'> <text class='icon-back'></text> <text class='icon-homefill'></text> </navigator> <view class='content' style='top:{{StatusBar}}px;'>操作条</view> </view> </view> [代码] 自定义系统Tabbar [图片] 按照官方 自定义 tabBar 配置好Tabbar (开发工具和版本库请使用最新版)。 使用ColorUI配置Tabbar只需要更改 Wxml 页的内容即可。 更多Tabbar样式请下载Demo查阅 操作条组件。 /custom-tab-bar/index.wxml [代码] <view class="cu-bar tabbar bg-white shadow"> <view class="action" wx:for="{{list}}" wx:key="index" data-path="{{item.pagePath}}" data-index="{{index}}" bindtap="switchTab"> <view class='icon-cu-image'> <image src='{{selected === index ? item.selectedIconPath : item.iconPath}}' class='{{selected === index ? "animation" : "animation"}}'></image> </view> <view class='{{selected === index ? "text-green" : "text-gray"}}'>{{item.text}}</view> </view> </view> [代码] 作者叨叨 ColorUI是一个高度自定义的Css样式库,包含了开发常用的元素和组件,元素组件之间也能相互嵌套使用。我也会不定期更新一些扩展到源码。 其实大家都在催我写文档,但这个库源码就在这,所见即所得,粘贴复制就可以得到你想要的页面。当然,文档我还是要写的,也希望大家多多提意见。 现在前端的开发方向基本都是奔着Js方向的,布局和样式大家讨论的有点少。以后我会在开发者社区多聊一聊关于开发中的布局和样式。 [图片] 感谢阅读。
2019-02-26 - 「分享」高性能双列瀑布流极简实现(附示例)❤️
前言 在日常开发过程中,经常会有双列瀑布流场景的需求出现,如商品列表、文章列表等,本文将简单介绍这种情景下如何高效、精准的实现双列瀑布流场景,支持刷新、加载更多等,实现效果如下。 [图片] [图片] 开发思路 瀑布流视图有一种参差的美感,常规列表布局如 flex wrap 等由于存在行高度限制,无法让第二行的 item 对齐上一行最矮处,因此,瀑布流布局时采用双列 scrollview 的 flex 布局。 参差布局的实现,采用代码计算左右两列的高度,然后对左右两列总高度进行比较,新加入的 item 总是排在总高度较小的那列后面。 计算时可以尽可能的缓存高度,例如左右两列高度在每次计算时都缓存起来,有新的 item 加入列表时直接增加左右两列高度即可,不需要重新从头计算。 index.js [代码]const tplWidth = (750 - 24 - 8) / 2; const tplHeight = 595; // plWidth * 1.66 newPhotos.forEach(photo => { const { height, width } = photo let photoHeight = tplWidth if (height > width) { photoHeight = tplHeight photo.display = 'long' } else { photo.display = 'short' } if (leftHeight < rightHeight) { leftList.push(photo) leftHeight += photoHeight } else { rightList.push(photo) rightHeight += photoHeight } }) [代码] index.wxml [代码]<!-- list --> <view class='list'> <!-- left --> <view class='left-list'> <block wx:for="{{leftList}}" wx:key="{{item._id}}"> <cell photo="{{item}}" bindclick='onCellClicked' /> </block> </view> <!-- right --> <view class='right-list'> <block wx:for="{{rightList}}" wx:key="{{item._id}}"> <cell photo="{{item}}" bindclick='onCellClicked' /> </block> </view> </view> [代码] index.css [代码].list { display: flex; flex: 1; position: relative; flex-direction: row; justify-content: space-between; padding-left: 12rpx; padding-right: 12rpx; padding-top: 8rpx; } .left-list { display: flex; position: relative; flex-direction: column; width: 359rpx; } .right-list { display: flex; position: relative; flex-direction: column; width: 359rpx; } [代码]
2019-02-27 - 轻松生成小程序分享海报
小程序海报组件 https://github.com/jasondu/wxa-plugin-canvas 需求 小程序分享到朋友圈只能使用小程序码海报来实现,生成小程序码的方式有两种,一种是使用后端方式,一种是使用小程序自带的canvas生成;后端的方式开发难度大,由于生成图片耗用内存比较大对服务端也是不小的压力;所以使用小程序的canvas是一个不错的选择,但由于canvas水比较深,坑比较多,还有不同海报需要重现写渲染流程,导致代码冗余难以维护,加上不同设备版本的情况不一样,因此小程序海报生成组件的需求十分迫切。 在实际开发中,我发现海报中的元素无非一下几种,只要实现这几种,就可以通过一份配置文件生成各种各样的海报了。 海报中的元素分类 [图片] 要解决的问题 单位问题 canvas隐藏问题 圆角矩形、圆角图片 多段文字 超长文字和多行文字缩略问题 矩形包含文字 多个元素间的层级问题 图片尺寸和渲染尺寸不一致问题 canvas转图片 IOS 6.6.7 clip问题 关于获取canvas实例 单位问题 canvas绘制使用的是px单位,但不同设备的px是需要换算的,所以在组件中统一使用rpx单位,这里就涉及到单位怎么换算问题。 通过wx.getSystemInfoSync获取设备屏幕尺寸,从而得到比例,进而做转换,代码如下: [代码]const sysInfo = wx.getSystemInfoSync(); const screenWidth = sysInfo.screenWidth; this.factor = screenWidth / 750; // 获取比例 function toPx(rpx) { // rpx转px return rpx * this.factor; } function toRpx(px) { // px转rpx return px / this.factor; }, [代码] canvas隐藏问题 在绘制海报过程时,我们不想让用户看到canvas,所以我们必须把canvas隐藏起来,一开始想到的是使用display:none; 但这样在转化成图片时会空白,所以这个是行不通的,所以只能控制canvas的绝对定位,将其移出可视界面,代码如下: [代码].canvas.pro { position: absolute; bottom: 0; left: -9999rpx; } [代码] 圆角矩形、圆角图片 由于canvas没有提供现成的圆角api,所以我们只能手工画啦,实际上圆角矩形就是由4条线(黄色)和4个圆弧(红色)组成的,如下: [图片] 圆弧可以使用canvasContext.arcTo这个api实现,这个api的入参由两个控制点一个半径组成,对应上图的示例 [代码]canvasContext.arcTo(x1, y1, x2, y2, r) [代码] 接下来我们就可以非常轻松的写出生成圆角矩形的函数啦 [代码]/** * 画圆角矩形 */ _drawRadiusRect(x, y, w, h, r) { const br = r / 2; this.ctx.beginPath(); this.ctx.moveTo(this.toPx(x + br), this.toPx(y)); // 移动到左上角的点 this.ctx.lineTo(this.toPx(x + w - br), this.toPx(y)); // 画上边的线 this.ctx.arcTo(this.toPx(x + w), this.toPx(y), this.toPx(x + w), this.toPx(y + br), this.toPx(br)); // 画右上角的弧 this.ctx.lineTo(this.toPx(x + w), this.toPx(y + h - br)); // 画右边的线 this.ctx.arcTo(this.toPx(x + w), this.toPx(y + h), this.toPx(x + w - br), this.toPx(y + h), this.toPx(br)); // 画右下角的弧 this.ctx.lineTo(this.toPx(x + br), this.toPx(y + h)); // 画下边的线 this.ctx.arcTo(this.toPx(x), this.toPx(y + h), this.toPx(x), this.toPx(y + h - br), this.toPx(br)); // 画左下角的弧 this.ctx.lineTo(this.toPx(x), this.toPx(y + br)); // 画左边的线 this.ctx.arcTo(this.toPx(x), this.toPx(y), this.toPx(x + br), this.toPx(y), this.toPx(br)); // 画左上角的弧 } [代码] 如果是画线框就使用[代码]this.ctx.stroke();[代码] 如果是画色块就使用[代码]this.ctx.fill();[代码] 如果是圆角图片就使用 [代码]this.ctx.clip(); this.ctx.drawImage(***); [代码] clip() 方法从原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内(不能访问画布上的其他区域)。可以在使用 clip() 方法前通过使用 save() 方法对当前画布区域进行保存,并在以后的任意时间对其进行恢复(通过 restore() 方法)。 多段文字 如果是连续多段不同格式的文字,如果让用户每段文字都指定坐标是不现实的,因为上一段文字的长度是不固定的,这里的解决方案是使用[代码]ctx.measureText[代码] (基础库 1.9.90 开始支持)Api来计算一段文字的宽度,记住这里返回宽度的单位是px(坑),从而知道下一段文字的坐标。 超长文字和多行文字缩略问题 设置文字的宽度,通过[代码]ctx.measureText[代码]知道文字的宽度,如果超出设定的宽度,超出部分使用“…”代替;对于多行文字,经测试发现字体的高度大约等于字体大小,并提供lineHeight参数让用户可以自定义行高,这样我们就可以知道下一行的y轴坐标了。 矩形包含文字 这个同样使用[代码]ctx.measureText[代码]接口,从而控制矩形的宽度,当然这里用户还可以设置paddingLeft和paddingRight字段; 文字的垂直居中问题可以设置文字的基线对齐方式为middle([代码]this.ctx.setTextBaseline('middle');[代码]),设置文字的坐标为矩形的中线就可以了;水平居中[代码]this.ctx.setTextAlign('center');[代码]; [图片] 多个元素间的层级问题 由于canvas没有Api可以设置绘制元素的层级,只能是根据后绘制层级高于前面绘制的方式,所以需要用户传入zIndex字段,利用数组排序(Array.prototype.sort)后再根据顺序绘制。 图片尺寸和渲染尺寸不一致问题 绘制图片我们使用[代码]ctx.drawImage()[代码]API; 如果使用[代码]drawImage(dx, dy, dWidth, dHeight)[代码],图片会压缩尺寸以适应绘制的尺寸,图片会变形,如下图: 在基础库1.9.0起支持[代码]drawImage(sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)[代码],sx和sy是源图像的矩形选择框左上角的坐标,sWidth和sHeight是源图像的矩形选择框的宽度和高度,如下图: [图片] 如果绘制尺寸比源图尺寸宽,那么绘制尺寸的宽度就等于源图宽度;反之,绘制尺寸比源图尺寸高,那么绘制尺寸的高度等于源图高度; 我们可以通过[代码]wx.getImageInfo[代码]Api获取源图的尺寸; canvas转图片 在canvas绘制完成后调用[代码]wx.canvasToTempFilePath[代码]Api将canvas转为图片输出,这样需要注意,[代码]wx.canvasToTempFilePath[代码]需要写在[代码]this.ctx.draw[代码]的回调中,并且在组件中使用这个接口需要在第二个入参传入this(坑),如下 [代码]this.ctx.draw(false, () => { wx.canvasToTempFilePath({ canvasId: 'canvasid', success: (res) => { wx.hideLoading(); this.triggerEvent('success', res.tempFilePath); }, fail: (err) => { wx.hideLoading(); this.triggerEvent('fail', err); } }, this); }); [代码] IOS 6.6.7 clip问题 在IOS 6.6.7版本中clip方法连续裁剪图片时,只有第一张有效,这是微信的bug,官方也证实了(https://developers.weixin.qq.com/community/develop/buglist) 关于获取canvas实例 我们可以使用[代码]wx.createCanvasContext[代码]获取小程序实例,但在组件中使用切记第二个参数需要带上this,如下 [代码]this.ctx = wx.createCanvasContext('canvasid', this); [代码] 如何使用组件 https://github.com/jasondu/wxa-plugin-canvas
2019-02-22