- 部分安卓机型不可以识别普通二维码?
出现问题的图片: [图片] 出现问题的手机长按截图 [图片]
2020-12-01 - 微信小程序调用oss上传至阿里云
1.首先创建oss密钥 这里我新建了一个名字 命名为config.js var fileHost = "https://*****************/"; //你的阿里云地址最后面跟上一个/ 在你当前小程序的后台的uploadFile 合法域名也要配上这个域名 var config = { //aliyun OSS config uploadImageUrl: `${fileHost}`, // 默认存在根目录,可根据需求改 AccessKeySecret: '*******', // AccessKeySecret 去你的阿里云上控制台上找 OSSAccessKeyId: '********', // AccessKeyId 去你的阿里云上控制台上找 timeout: 87600 //这个是上传文件时Policy的失效时间 }; module.exports = config 2.创建上传配置文件 我这里命名为upload.js const env = require('config.js'); //配置文件,在这文件里配置你的OSS keyId和KeySecret,timeout:87600; // 以下算法在https://gitee.com/chenkuo1997/oss-wx.git 复制到同upload.js目录下即可 const base64 = require('base64.js');//Base64,hmac,sha1,crypto相关算法 require('hmac.js'); require('sha1.js'); const Crypto = require('crypto.js'); /* *上传文件到阿里云oss *@param - filePath :图片的本地资源路径 *@param - dir:表示要传到哪个目录下 *@param - successc:成功回调 *@param - failc:失败回调 */ const uploadFile = function (filePath, dir, successc, failc) { if (!filePath || filePath.length < 9) { wx.showModal({ title: '图片错误', content: '请重试', showCancel: false, }) return; } console.log('上传图片.....'); //图片名字 可以自行定义, 这里是采用当前的时间戳 + 150内的随机数来给图片命名的 console.log(dir) const aliyunFileKey = dir+ new Date().getTime() + Math.floor(Math.random() * 150) + '.png'; const aliyunServerURL = env.uploadImageUrl;//OSS地址,需要https const accessid = env.OSSAccessKeyId; const policyBase64 = getPolicyBase64(); const signature = getSignature(policyBase64);//获取签名 console.log(env) wx.uploadFile({ url: aliyunServerURL,//开发者服务器 url filePath: filePath,//要上传文件资源的路径 name: 'file',//必须填file formData: { 'key': aliyunFileKey, 'policy': policyBase64, 'OSSAccessKeyId': accessid, 'signature': signature, 'success_action_status': '200', }, success: function (res) { console.log(res) if (res.statusCode != 200) { failc(new Error('上传错误:' + JSON.stringify(res))) return; } successc(aliyunServerURL+aliyunFileKey); }, fail: function (err) { err.wxaddinfo = aliyunServerURL; failc(err); }, }) } const getPolicyBase64 = function () { let date = new Date(); date.setHours(date.getHours() + env.timeout); let srcT = date.toISOString(); const policyText = { "expiration": srcT, //设置该Policy的失效时间,超过这个失效时间之后,就没有办法通过这个policy上传文件了 "conditions": [ ["content-length-range", 0, 5 * 1024 * 1024] // 设置上传文件的大小限制,5mb ] }; const policyBase64 = base64.encode(JSON.stringify(policyText)); return policyBase64; } const getSignature = function (policyBase64) { const accesskey = env.AccessKeySecret; const bytes = Crypto.HMAC(Crypto.SHA1, policyBase64, accesskey, { asBytes: true }); const signature = Crypto.util.bytesToBase64(bytes); return signature; } module.exports = uploadFile; 3.小程序页面调用的时候 const uploadImage = require('../../utils/upload'); /** * 上传图片 */ uploadImage() { let that = this; let applyRefundImgList = that.data.applyRefundImgList wx.chooseImage({ count: 5 - applyRefundImgList.length, //处理图片上传数量 (可根据自身需求配置) success: function(res) { wx.showLoading({ title: '上传中', mask: true }) for (let index = 0; index < res.tempFilePaths.length; index++) { //applyrefund 则为bucket下的文件夹路径 可根据自身需求进行分类 uploadImage(res.tempFilePaths[index], `applyrefund/${shopUuid}/`, function(res) { wx.hideLoading() applyRefundImgList.push(res) that.setData({ applyRefundImgList }) }, function(res) { wx.hideLoading() } ) } } }) },
2021-07-06 - scroll-view在自定义头部的时候 锚点点击的时候 取到的值乱了
- 当前 Bug 的表现(可附上截图) - 预期表现 - 复现路径 - 提供一个最简复现 Demo 自定义了头部scroll-view 锚点点击的时候 取到的scrolltop值乱了 而且有时候会出现滚动头部黑屏的问题 求解决方案[图片] [图片] 滚动的时候 拿到的值是没问题的 [图片] 然后我点击锚点让他跳转最上边 接下来问题来了 ???黑色的背景是什么?? [图片] 好吧我重新在来一遍 我又滚动到了最底下 再次点击锚点 让他跳转到最上面 也就是scrolltop为0 然后scrolltop取到的值是乱的 怎么回事??我在期间考虑用过事件节流 和 touchend事件 并没有很好的解决了这个问题 最后补充下 这个问题在iphone手机端才会出现 [图片]
2019-07-16 - 锚点判断高亮 会出现闪动
[图片] e.scrllTop是滚动取到的值,indeBoxTop commentBoxTop 分别是某个view 距离顶部的值(在onLoad的时候计算的),custombar是胶囊的大小 (因为这个页面是自定义的头部), 滚动条滚动的时候 会出现高亮抖动[图片] 点击评价的锚点 下边的横线会先跳动评论在跳到详情在跳到评价 这个问题要怎么解决呢?
2019-07-18 - 关于医疗咨询的小程序审核时需要选择什么类目?以及对应的资质
我们准备做一个儿科医生兼职咨询的小程序平台,需要准备哪些资质呢?
2020-02-19 - 直播多店铺,但同属于一个小程序?
你好 我们平台下会有多个店铺,但是同属于一个小程序,类似于京东,淘宝这样的平台。用户进入的时候首页直播列表我想做个筛选,筛选出当前店铺对应的直播间,并非看到所有的直播列表。这个功能怎么实现呢?
2020-03-09 - 微信小程序自定义tarbra的坑,动态适配iphoneX iphone11 带安全区域的手机
微信小程序虽然开放了自定义的tabbar 因为他用的是fixed定位布局 导致每个tabbar页都要去动态计算padding-bottom 或者bottom值,之前尝试过 wx.getSystemInfo({ success: function(res) { console.log(res) if (res.model.search('iPhone X') != -1) { that.globalData.isIphoneX = true } }, }) 在app.js中判断是不是iphone X ok这个时候是完美适配的 但是有一天测试同学拿着iphone 11 pro max找我 说页面的padding-bottom值会盖住,在我的排查中发现res.model.search('iPhone X') != -1 这句代码拿到的结果为-1 我之前是这么处理的 我判断机型为iphonex的时候 tabbar 页面的padding-bottom为100rpx+64rpx 但是iphone 11pro 系列手机在这个判断中无效 经过排查并反复改 终于拿到了完美适配的方案!!!!我们只需要在外层的view padding-bottom: calc(100rpx + env(safe-area-inset-bottom))就好了 有需要的同学点个关注吧!!! 对了 再次说明下 custom-tab-bar.wxss 中.tar-bar里的height我自己改成了100rpx 微信官方的是50px
2020-03-19 - 微信小程序性能优化总结
1.关于onshow优化方案,onshow每次页面切换都会调用接口,但是有种场景下的确要用到onshow,比如说登陆成功之后刷新首页接口,但是吧,刷新完成之后你又不想在onshow调用接口,你可以在onload调用一波接口,这个时候你可以在全局app.js设置一个flag开关默认为false,登陆成功之后将全局的flag开关重置为true,当前页面onshow判断if(app.globalData.flag){/*调用接口相关操作*/,}调用完接口之后在将flag重置为false,这样就减少了onshow的每次切换接口都会请求相关没必要操作。 2.关于setData,一些没必要的参数重置可以不用setData,比如说商品列表页面跳转商品详情页面需要传入一个商品唯一标示,这个时候很多人习惯在onload里面判断if(options&&options.itemUuid){ /*itemUuid你可以理解为商品唯一标示*/ this.setData({ itemUuid }) this.getItemList() },这个时候itemUuid不需要动态更新,你可以这样做减少setData调用(不懂的setData小伙伴可以了解下下diff算法), if(options&&options.itemUuid){ /*itemUuid你可以理解为商品唯一标示*/ this.data. itemUuid = options.itemUuid this.getItemList() } 3.关于接口请求等待优化,可以做一些骨架屏、gif动画类似,微信开发者工具最新版右下角又三个点点,打开生成骨架屏会在当前文件夹生成一套wxss,wxml的相关骨架屏文件,开发者只需要在wxml和wxss引入骨架屏的样式就可以,还是蛮方便的哟 今天先到这里,改天再来更新!!!!
2020-05-12 - 小程序图片懒加载终极方案
效果图 既然来了,把妹子都给你。 [图片] 定义懒加载,前端人都知道的一种性能优化方式,简单的来说,只有当图片出现在浏览器的可视区域内时,才设置图片正真的路径,让图片显示出来。这就是图片懒加载。 实现原理监听页面的[代码]scroll[代码]事件,判读元素距离页面的[代码]top[代码]值是否是小于等于页面的可视高度 判断逻辑代码如下 [代码]element.getBoundingClientRect().top <= document.documentElement.clientHeight ? 显示 : 默认[代码] 我们知道小程序页面的脚本逻辑是在JsCore中运行,JsCore是一个没有窗口对象的环境,所以不能在脚本中使用window,也无法在脚本中操作组件。 所以关于图片懒加载就需要在数据上面做文章了。 页面页面上面只需要根据数据的某一个字段来判断是否显示图片就可以了,字段为Boolean类型,当为false的时候显示默认图片就行了。 代码大概长成这样 <view wx:for="{{list}}" class='item item-{{index}}' wx:key="{{index}}"> <image class="{{item.show ? 'active': ''}}" src="{{item.show ? item.src : item.def}}"></image> </view> 布局跟简单,[代码]view[代码]组件里面有个图片,并循环[代码]list[代码],有多少就展示多少 [代码]image[代码]组件的[代码]src[代码]字段通过每一项的[代码]show[代码]来进行绑定,[代码]active[代码]是加了个透明的过渡 样式 image{ transition: all .3s ease; opacity: 0; } .active{ opacity: 1; } 逻辑 本位主要讲解懒加载,所以把数据写死在页面上了 数据结构如下: [图片] 我们使用两种方式来实现懒加载,准备好没有,一起来快乐的撸码吧。 WXML节点信息 小程序支持调用createSelectQuery创建一个[代码]SelectorQuery[代码]实例,并使用[代码]select[代码]方法来选择节点,并通过[代码]boundingClientRect[代码]来获取节点信息。 wx.createSelectorQuery().select('.item').boundingClientRect((ret)=>{ console.log(ret) }).exec() 显示结果如下 [图片] 悄悄告诉你,小程序里面有个[代码]onPageScroll[代码]函数,是用来监听页面的滚动的。 还有个[代码]getSystemInfo[代码]函数,可以获取获取系统信息,里面包含屏幕的高度。 接下来,思路就透彻了吧。还是上面的逻辑, 扒拉扒拉直接写代码就行了,这里只写下主要的逻辑,完整代码请戳文末github showImg(){ let group = this.data.group let height = this.data.height // 页面的可视高度 wx.createSelectorQuery().selectAll('.item').boundingClientRect((ret) => { ret.forEach((item, index) => { if (item.top <= height) { 判断是否在显示范围内 group[index].show = true // 根据下标改变状态 } }) this.setData({ group }) }).exec() } onPageScroll(){ // 滚动事件 this.showImg() } 至此,我们完成了一个小程序版的图片懒加载,只是思维转变了下,其实并没有改变实现方式。我们来学些新的东西吧。 节点布局相交状态 节点相交状态是啥?它是一个新的API,叫做[代码]IntersectionObserver[代码], 本文只讲解简单的使用,了解更多请猛戳没错,就是点我 小程序里面给它的定义是节点布局交叉状态API可用于监听两个或多个组件节点在布局位置上的相交状态。这一组API常常可以用于推断某些节点是否可以被用户看见、有多大比例可以被用户看见。 里面设计的概念主要有五个,分别为 参照节点:以某参照节点的布局区域作为参照区域,参照节点可以有多个,多个话参照区域取它们的布局区域的交集目标节点:监听的目标,只能是一个节点相交区域:目标节点与参照节点的相交区域相交比例:目标节点与参照节点的相交比例阈值:可以有多个,默认为[0], 可以理解为交叉比例,例如[0.2, 0.5]关于它的API有五个,依次如下 1、[代码]createIntersectionObserver([this], [options])[代码],见名知意,创建一个IntersectionObserver实例 2、[代码]intersectionObserver.relativeTo(selector, [margins])[代码], 指定节点作为参照区域,margins参数可以放大缩小参照区域,可以包含top、left、bottom、right四项 3、[代码]intersectionObserver.relativeToViewport([margin])[代码],指定页面显示区域为参照区域 4、[代码]intersectionObserver.observer(targetSelector, callback)[代码],参数为指定监听的节点和一个回调函数,目标元素的相交状态发生变化时就会触发此函数,callback函数包含一个result,下面再讲 5、[代码]intersectionObserver.disconnect()[代码] 停止监听,回调函数不会再触发 然后说下callback函数中的result,它包含的字段为 [图片] 我们主要使用[代码]intersectionRatio[代码]进行判断,当它大于0时说明是相交的也就是可见的。 先来波测试题,请说出下面的函数做了什么,并且log函数会执行几次 1、 wx.createIntersectionObserver().relativeToViewport().observer('.box', (result) => { console.log('监听box组件触发的函数') }) 2、 wx.createIntersectionObserver().relativeTo('.box').observer('.item', (result) => { console.log('监听item组件触发的函数') }) 3、 wx.createIntersectionObserver().relativeToViewport().observer('.box', (result) => { if(result.intersectionRatio > 0){ console.log('.box组件是可见的') } }) duang,揭晓答案。 第一个以当前页面的视窗监听了[代码].box[代码]组件,log会触发两次,一次是进入页面一次是离开页面 第二个以[代码].box[代码]节点的布局区域监听了[代码].item[代码]组件,log会触发两次,一次是进入页面一次是离开页面 第三个以当前页面的视窗监听了[代码].box[代码]组件,log只会在节点可见的时候触发 好了,题也做了,API你也掌握了,相信你已经可以使用[代码]IntersectionObserver[代码]来实现图片懒加载了吧,主要逻辑如下 let group = this.data.group // 获取图片数组数据 for (let i in this.data.group){ wx.createIntersectionObserver().relativeToViewport().observe('.item-'+ i, (ret) => { if (ret.intersectionRatio > 0){ group[i].show = true } this.setData({ group }) }) } 最后 至此,我们使用两种方式实现了小程序版本的图片懒加载,可以发现,使用[代码]IntersectionObserver[代码]来实现不要太酸爽
2020-05-12 - 微信小程序仿拼多多、京东实现商品详情页顶部导航滚动渐变
1.这里使用了自定义导航,然后通过监听动态来改变background的rgba透明值达到效果 2.滚动监听setData是一件很消耗性能的问题,在这里使用了事件节流,及判断了scrollTop 最终特效[图片][图片][图片][图片] 代码片段:https://developers.weixin.qq.com/s/KyKkpmmb7hhi 时间紧迫前期只做了动态实现顶部导航渐变,后面会完善动态锚点及锚点监听 原理也很简单 依据 <scroll-view scroll-into-view="{{toView}}" scroll-with-animation="true" scroll-y="true"> 既可以实现
2020-05-19 - 用微前端的方式搭建类单页应用
微前端由ThoughtWorks 2016年提出,将后端微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。 美团已经是一家拥有几万人规模的大型互联网公司,提升整体效率至关重要,这需要很多内部和外部的管理系统来支撑。由于这些系统之间存在大量的连通和交互诉求,因此我们希望能够按照用户和使用场景将这些系统汇总成一个或者几个综合的系统。 我们把这种由多个微前端聚合出来的单页应用叫做“类单页应用”,美团HR系统就是基于这种设计实现的。美团HR系统是由30多个微前端应用聚合而成,包含1000多个页面,300多个导航菜单项。对用户来说,HR系统是一个单页应用,整个交互过程非常顺畅;对开发者同学来说,各个应用均可独立开发、独立测试、独立发布,大大提高了开发效率。 接下来,本文将为大家介绍“微前端构建类单页应用”在美团HR系统中的一些实践。同时也分享一些我们的思考和经验,希望能够对大家有所启发。 HR系统的微前端设计因为美团的HR系统所涉及项目比较多,目前由三个团队来负责。其中:OA团队负责考勤、合同、流程等功能,HR团队负责入职、转正、调岗、离职等功能,上海团队负责绩效、招聘等功能。这种团队和功能的划分模式,使得每个系统都是相对独立的,拥有独立的域名、独立的UI设计、独立的技术栈。但是,这样会带来开发团队之间职责划分不清、用户体验效果差等问题,所以就迫切需要把HR系统转变成只有一个域名和一套展示风格的系统。 为了满足公司业务发展的要求,我们做了一个HR的门户页面,把各个子系统的入口做了链接归拢。然而我们发现HR门户的意义非常小,用户跳转两次之后,又完全不知道跳到哪里去了。因此我们通过将HR系统整合为一个应用的方式,来解决以上问题。 一般而言,“类单页应用”的实现方式主要有两种: iframe嵌入微前端合并类单页应用其中,iframe嵌入方式是比较容易实现的,但在实践的过程中带来了如下问题: 子项目需要改造,需要提供一组不带导航的功能iframe嵌入的显示区大小不容易控制,存在一定局限性URL的记录完全无效,页面刷新不能够被记忆,刷新会返回首页iframe功能之间的跳转是无效的iframe的样式显示、兼容性等都具有局限性考虑到这些问题,iframe嵌入并不能满足我们的业务诉求,所以我们开始用微前端的方式来搭建HR系统。 在这个微前端的方案里,有几个我们必须要解决的问题: 一个前端需要对应多个后端提供一套应用注册机制,完成应用的无缝整合构建时集成应用和应用独立发布部署只有解决了以上问题,我们的集成才是有效且真正可落地的,接下来详细讲解一下这几个问题的实现思路。 一个前端对应多个后端HR系统最终线上运行的是一个单页应用,而项目开发中要求应用独立,因此我们新建了一个入口项目,用于整合各个应用。在我们的实践中,把这个项目叫做“Portal项目”或“主项目”,业务应用叫做“子项目”,整个项目结构图如下所示: [图片] 项目结构图 “Portal项目”是比较特殊的,在开发阶段是一个容器,不包含任何业务,除了提供“子项目”注册、合并功能外,还可以提供一些系统级公共支持,例如: * 用户登录机制 * 菜单权限获取 * 全局异常处理 * 全局数据打点 “子项目”对外输出不需要入口HTML页面,只需要输出的资源文件即可,资源文件包括js、css、fonts和imgs等。 HR系统在线上运行了一个前端服务(Node Server),这个Server用于响应用户登录、鉴权、资源的请求。HR系统的数据请求并没有经过前端服务做透传,而是被Nginx转发到后端Server上,具体交互如下图所示: [图片] 前后端分离图 转发规则上限制数据请求格式必须是 [代码]系统名+Api做前缀[代码] 这样保障了各个系统之间的请求可以完全隔离。其中,Nginx的配置示例如下: server { listen 80; server_name xxx.xx.com; location /project/api/ { set $upstream_name "server.project"; proxy_pass http://$upstream_name; } ... location / { set $upstream_name "web.portal"; proxy_pass http://$upstream_name; } } 我们将用户的统一登录和认证问题交给了SSO,所有的项目的后端Server都要接入SSO校验登录状态,从而保障业务系统间用户安全认证的一致性。 在项目结构确定以后,应用如何进行合并呢?因此,我们开始制定了一套应用注册机制。 应用注册机制“Portal项目”提供注册的接口,“子项目”进行注册,最终聚合成一个单页应用。在整套机制中,比较核心的部分是路由注册机制,“子项目”的路由应该由自己控制,而整个系统的导航是“Portal项目”提供的。 路由注册路由的控制由三部分组成:权限菜单树、导航和路由树,“Portal项目”中封装一个组件App,根据菜单树和路由树生成整个页面。路由挂载到DOM树上的代码如下: let Router = ReactDOM.render(Router,document.querySelector("#app")); Router是在react-router的基础上做了一层封装,通过menu和routes最后生成一个如下所示的路由树: 具体注册使用了全局的[代码]window.app.routes[代码],“Portal项目”从[代码]window.app.routes[代码]获取路由,“子项目”把自己需要注册的路由添加到[代码]window.app.routes[代码]中,子项目的注册如下: let app = window.app = window.app || {}; app.routes = (app.routes || []).concat([ { code:'attendance-record', path: '/attendance-record', component: wrapper(() => async(require('./nodes/attendance-record'), 'kaoqin')), }]); 路由合并的同时也把具体的功能做了引用关联,再到构建时就可以把所有的功能与路由管理起来。项目的作用域要怎么控制呢?我们要求“子项目”间是彼此隔离,要避免样式污染,要做独立的数据流管理,我们用项目作用域的方式来解决这些问题。 项目作用域控制在路由控制的时候我们提到了 [代码]window.app[代码],我们也是通过这个全局App来做项目作用域的控制。[代码]window.app[代码]包含了如下几部分: let app = window.app || {}; app = { require:function(request){...}, define:function(name,context,index){...}, routes:[...], init:function(namespace,reducers){...} }; window.app主要功能: define 定义项目的公共库,主要用来解决JS公共库的管理问题require 引用自己的定义的基础库,配合define来使用routes 用于存放全局的路由,子项目路由添加到window.app.routes,用于完成路由的注册init 注册入口,为子项目添加上namesapce标识,注册上子项目管理数据流的reducers子项目完整的注册,如下所示: import reducers from './redux/kaoqin-reducer'; let app = window.app = window.app || {}; app.routes = (app.routes || []).concat([ { code:'attendance-record', path: '/attendance-record', component: wrapper(() => async(require('./nodes/attendance-record'), 'kaoqin')), // ... 其他路由 }]); function wrapper(loadComponent) { let React = null; let Component = null; let Wrapped = props => ( ); return async () => { await window.app.init('namespace-kaoqin',reducers); React = require('react'); Component = await loadComponent(); return Wrapped; }; } 其中做了这几件事情: 把路由添加到window.app中业务第一次功能被调用的时候执行 [代码]window.app.init(namespace,reducers)[代码],注册项目作用域和数据流的reducers对业务功能的挂载节点包装一个根节点:[代码]Component[代码]挂载在[代码]className[代码]为[代码]namespace-kaoqin[代码]的[代码]div[代码]下面这样就完成了“子项目”的注册,“子项目”的对外输出是一个入口文件和一系列的资源文件,这些文件由webpack构建生成。 CSS作用域方面,使用webpack在构建阶段为业务的所有CSS都加上自己的作用域,构建配置如下: //webpack打包部分,在postcss插件中 添加namespace的控制 config.postcss.push(postcss.plugin('namespace', () => css => css.walkRules(rule => { if (rule.parent && rule.parent.type === 'atrule' && rule.parent.name !== 'media') return; rule.selectors = rule.selectors.map(s => `.namespace-kaoqin ${s === 'body' ? '' : s}`); }) )); CSS处理用到postcss-loader,postcss-loader用到postcss,我们添加postcss的处理插件,为每一个CSS选择器都添加名为[代码].namespace-kaoqin[代码]的根选择器,最后打包出来的CSS,如下所示: .namespace-kaoqin .attendance-record { height: 100%; position: relative } .namespace-kaoqin .attendance-record .attendance-record-content { font-size: 14px; height: 100%; overflow: auto; padding: 0 20px } ... CSS样式问题解决之后,接下来看一下,Portal提供的init做了哪些工作。 let inited = false; let ModalContainer = null; app.init = async function (namespace,reducers) { if (!inited) { inited = true; let block = await new Promise(resolve => { require.ensure([], function (require) { app.define('block', require.context('block', true, /^\.\/(?!dev)([^\/]|\/(?!demo))+\.jsx?$/)); resolve(require('block')); }, 'common'); }); ModalContainer = document.createElement('div'); document.body.appendChild(mtfv3ModalContainer); let { Modal} = block; Modal.getContainer = () => ModalContainer; } ModalContainer.setAttribute('class', `${namespace}`); mountReducers(namepace,reducers) }; init方法主要做了两件事情: 挂载“子项目”的reducers,把“子项目”的数据流挂载了redux上“子项目”的弹出窗全部挂载在一个全局的div上,并为这个div添加对应的项目作用域,配合“子项目”构建的CSS,确保弹出框样式正确上述代码中还看到了[代码]app.define[代码]的用法,它主要是用来处理JS公共库的控制,例如我们用到的组件库Block,期望每个“子项目”的版本都是统一的。因此我们需要解决JS公共库版本统一的问题。 JS公共库版本统一为了不侵入“子项目”,我们采用构建过程中替换的方式来做,“Portal项目”把公共库引入进来,重新定义,然后通过[代码]window.app.require[代码]的方式引用,在编译“子项目”的时候,把引用公共库的代码从[代码]require('react')[代码]全部替换为[代码]window.app.require('react')[代码],这样就可以将JS公共库的版本都交给“Portal项目”来控制了。 define 的代码和示例如下: /** * 重新定义包 * @param name 引用的包名,例如 react * @param context 资源引用器 实际上是 webpackContext(是一个方法,来引用资源文件) * @param index 定义的包的入口文件 */ app.define = function (name, context, index) { let keys = context.keys(); for (let key of keys) { let parts = (name + key.slice(1)).split('/'); let dir = this.modules; for (let i = 0; i < parts.length - 1; i++) { let part = parts[i]; if (!dir.hasOwnProperty(part)) { dir[part] = {}; } dir = dir[part]; } dir[parts[parts.length - 1]] = context.bind(context, key); } if (index != null) { this.modules[name]['index.js'] = this.modules[name][index]; } }; //定义app的react //定义一个react资源库:把原来react根目录和lib目录下的.js全部获取到,绑定到新定义的react中,并指定react.js作为入口文件 app.define('react', require.context('react', true, /^.\/(lib\/)?[^\/]+\.js$/), 'react.js'); app.define('react-dom', require.context('react-dom', true, /^.\/index\.js$/)); “子项目”的构建,使用webpack的externals(外部扩展)来对引用进行替换: /** * 对一些公共包的引用做处理 通过webpack的externals(外部扩展)来解决 */ const libs = ['react', 'react-dom', "block"]; module.exports = function (context, request, callback) { if (libs.indexOf(request.split('/', 1)[0]) !== -1) { //如果文件的require路径中包含libs中的 替换为 window.app.require('${request}'); //var在这儿是声明的意思 callback(null, `var window.app.require('${request}')`); } else { callback(); } }; 这样项目的注册就完成了,还有一些需要“子项目”自己改造的地方,例如本地启动需要把“Portal项目”的导航加载进来,需要做mock数据等等。 项目的注册完成了,我们如何发布部署呢? 构建后集成和独立部署在HR系统的整合过程中,开发阶段对“子项目”是“零侵入”,而在发布阶段,我们也希望如此。 我们的部署过程,大概如下: [图片] 第一步:在发布机上,获取代码、安装依赖、执行构建; 第二步:把构建的结果上传到服务器; 第三步:在服务器执行 [代码]node index.js[代码] 把服务启动起来。 “Portal项目”构建之后的文件结构如下: [图片] “子项目”构建后的文件结构如下: [图片] 线上运行的文件结构如下: [图片] 把“子项目”的构建文件上传到服务器对应的“子项目”文件目录下,然后对“子项目”的资源文件进行集成合并,生成.dist目录中的文件,提供给用户线上访问使用。 每次发布,我们主要做以下三件事情: 发布最新的静态资源文件重新生成entry-xx.js和index.html(更新入口引用)重启前端服务如果是纯静态服务,完全可以做到热部署,动态更新一下引用关系即可,不需要重启服务。因为我们在Node服务层做了一些公共服务,所以选择了重启服务,我们使用了公司的基础服务和PM2来实现热启动。 对于历史文件,我们需要做版本控制,以保障之前的访问能够正常运行。此外,为了保证服务的高可用性,我们上线了4台机器,分别在两个机房进行部署,最终来提高HR系统的容错性。 总结以上就是我们使用React技术栈和微前端方式搭建的“类单页应用”HR业务系统,回顾一下这个技术方案,整个框架流程如下图所示: [图片] 在产品层面上,“微前端类单页应用”打破了独立项目的概念,我们可以根据用户的需求自由组装我们的页面应用,例如:我们可以在HR门户上把考勤、请假、OA审批、财务报销等高频功能放在一起。甚至可以让用户自己定制功能,让用户真的感受到我们是一个系统。 “微前端构建类单页应用”方案是基于React技术栈开发,如果把路由管理机制和注册机制抽离出来作为一个公共的库,就可以在webpack的基础上封装成一个业务无关性的通用方案,而且使用起来非常的友好。 截止目前,HR系统已经稳定运行了1年多的时间,我们总结了以下三个优点: 单页应用的体验比较好,按需加载,交互流畅项目微前端化,业务解耦,稳定性有保障,项目的粒度易控制项目的健壮性比较好,项目注册仅仅增加了入口文件的大小,30多个项目目前只有12K文章转载于 https://tech.meituan.com/2018/09/06/fe-tiny-spa.html
2020-05-20 - 一张页面引起的项目架构思考(rax+Typescript+hooks)
前言“好的书本分章节、好的代码分模块,那么好的架构该如何定义呢?咳咳,不要意思,题目起大了~~ 小生之辈,岂敢以架构而论。 不过话说来,很多人都认为前端无非就是 [代码]HTML[代码]+[代码]CSS[代码]+[代码]JS[代码],一个目录一类文件,有何架构可言。但是我想说。。。。你说的都对! [图片] 但是,笔者一直在探索不同的页面架构组织形式,鄙人愚见,好的架构,能够方便拓展和开发以及后期的项目维护。 在笔者刚开始接触前端的时候,就一直在思考怎么样的架构比较舒服易于扩展,且能装 B。React-Full-Dianping-Demo里面就有写到对于[代码]react[代码]+[代码]react-redux[代码]+[代码]soga[代码]的一些列代码组织的思考:react技术栈项目结构探究 (详见 github/Nealyang) 一直还在学习,本文也只是拿来探讨下本次我开发一个页面时,我个人的一些代码组织方式。抛个砖~ 望各位大佬不啬赐教。 [图片] 项目架构[图片] src ├─ action-log │ ├─ constants.ts │ └─ index.ts ├─ app.js ├─ app.json ├─ common │ ├─ animation-utils.ts │ ├─ business-utils.ts │ ├─ constants.ts │ ├─ detail-utils.ts │ ├─ mtop-utils.ts │ ├─ net-utils.ts │ ├─ price-utils.ts │ ├─ storage-utils.ts │ ├─ string-utils.ts │ ├─ time-utils.ts │ ├─ type.ts │ ├─ url-utils.ts │ └─ utils.ts ├─ components │ ├─ loading-page │ │ ├─ index.css │ │ └─ index.tsx │ └─ pm-bottom │ ├─ index.css │ └─ index.tsx ├─ document │ └─ index.jsx ├─ event │ └─ EVENTS.ts ├─ modules │ ├─ bottom-action │ │ ├─ index.css │ │ └─ index.tsx │ └─ page-container │ ├─ base │ ├─ decorator │ ├─ index.tsx │ └─ libs └─ pages ├─ buyer-identity │ ├─ components │ ├─ constants │ ├─ customized-hooks │ ├─ index.tsx │ ├─ types │ └─ utils 或许上面看起来并不是很直观,截图解释下 [图片] [图片] 大概的看下,脑海中有个大概的位置和每个文件的作用。下面我们再来细品 目录职责其实划分了这么多的目录,无非就是为了最大可能的复用。其中也包括对于组件状态的抽离、hooks 特性的利用。 pages 层以外的公共逻辑毕竟是[代码]MPA[代码]应用,所以一切还都是围绕着 [代码]pages[代码] 展开。 action-log首先这里的[代码]action-log[代码]目录就不多说了,因为没有太多可借鉴性。大概就是返回一个 [代码]ActionLog[代码]对象,来进行一些业务上的埋点、信息收集等逻辑的处理。所以这里如果大家有一些公共的基础类封装,都是可以放这里的。 commoncommon ├─ animation-utils.ts ├─ business-utils.ts ├─ constants.ts ├─ detail-utils.ts ├─ mtop-utils.ts ├─ net-utils.ts ├─ price-utils.ts ├─ storage-utils.ts ├─ string-utils.ts ├─ time-utils.ts ├─ type.ts ├─ url-utils.ts └─ utils.ts 由于该项目的比较复杂,业务逻辑相对较多。所以这里我将 [代码]utils[代码]按照类别,区分出来了以上几种。方面后期开发中的维护和扩展,也便于查找。 除了一些从命名可以区分出来的[代码]utils[代码] 以外,这里还放了一个 [代码]type.ts[代码]和[代码]constants.ts[代码],用途自如其名。 components相信框架使用者对于 [代码]components[代码] 的命名都不为陌生.是的,就是对于一些公共组件的封装,比如我这里放的两个组件[代码]loading-page[代码],[代码]pm-bottom[代码]等公共组件。components 相对来说是比较“小”的概念,划分依据这这个项目中也比较简单,就是是否为“木偶组件”(虽然 hooks 了以后,咱不太适合这么说), modulesmodules ├─ bottom-action │ ├─ index.css │ └─ index.tsx └─ page-container ├─ base │ ├─ base.tsx │ ├─ error.tsx │ └─ scrollBase.tsx ├─ decorator │ └─ withError.tsx ├─ index.tsx └─ libs ├─ displayName.ts ├─ navbarTransparent.ts ├─ spm.ts └─ title.ts 更具有模块的概念,这里最典型的[代码]page-contaienr[代码]的模块,作用就是每一个页面的通用底层容器,早在之前的文章中其实有介绍到这个容器,如何用 Decorator 装饰你的 Typescript,所以这里就不再赘述了,其实就是一些基础功能的封装。所以也就是解释了[代码]event[代码]的目录存在。 而这里[代码]modules[代码]和[代码]conponents[代码]最大的区别就是,复杂度和内部状态管理。如果内部状态较为复杂,且有很多的交互,那么我们就称之为 [代码]module[代码].是的,这里的界限,我们划分较为模糊。 但是当你拿到一份设计稿的时候,估计就能明白我的良苦用心了~ [图片] “红色框就可以理解为 module,绿色框可以理解为 componentspage 的组织针对单个页面里面的组织,其实都大同小异。(突然发现前端架构没有太多可言) [图片] 目录区分的并不是很多,但是也都较为清晰。简单介绍下每个区域的分工,需要展开的,我们在后续展开介绍 [代码]index.tsx[代码] 页面的入口文件,但是本身里面不会编写太多业务逻辑[代码]utils[代码] 该页面的工具函数,包括接口的请求、数据的 [代码]format[代码]等[代码]customized-hooks[代码] 自定义[代码]hooks[代码],这里有两个,初始化 UI 所需要的数据(边距等),业务请求的数据。[代码]constants[代码] 页面的常量,包括请求的 [代码]api[代码]、[代码]spm[代码] 埋点、固定的一些该页面业务数据等[代码]components[代码] 该页面的组件(注意这里没有 module,因为太多了真的容易混乱),页面的 [代码]components[代码],有简单的,也有复杂的。[图片] 以上就是一些目录结构和代码组织的交代。其实还是比较简单清晰的。下面介绍下 页面数据流向和管理规则“碎碎叨叨道不到个明明白白[图片] 因为是业务代码,所以这里就不会粘贴太多代码了 简单的解释下上面的流程 初始化 UI 的逻辑比较偏于业务,其实没有太多可借鉴的。这里我代码里面的工作也就是适配 [代码]iPhone X[代码]的一些UI。 重点说下初始化接口数据的过程吧。其实也就是各个页面中的 [代码]components[代码] 的状态初始化 interface首先我们需要定义每一个模块的 [代码]props[代码],毕竟是因为用的 [代码]ts[代码],注释即文档。所以我们将每一个 [代码]components[代码] 的 [代码]props[代码] 都定义到 [代码]type[代码] 目录中,毕竟很多时候接口返回的数据,需要我们做一次 [代码]format[代码],而这个 [代码]format[代码] 的目的就是为了 [代码]components[代码] 更好的使用。换句话说,这些接口,可复用! 那必然定义到外面 [图片] 注意接口上都要写注释啊!!!!理由如下: [图片] [图片] 将所有数据处理的方法,全部放到 [代码]utils[代码] 中(注意数据兜底的处理,这里我所有的数据处理都写好工具函数,并添加充分的单元测试) [图片] 真正的做到对 [代码]components[代码] 而言,开箱即用。 因为有 [代码]type[代码] 的定义和 [代码]components[代码] 之间的约束,所以无论是[代码]componemts[代码] 内部的数据使用还是 [代码]index.tsx[代码] 里面的模块引入时 [代码]props[代码] 的注入,都有很好的约束 [图片] “编写时候的提醒[图片] “漏写时候的报错组件通信由于我们使用了 [代码]hooks[代码],且相对隔离的组件划分,原则上,组件通信其实并不是很多。当然,也必然是有的。 其实这方面的约束主要归结于业务的复杂度,如果数据逻辑比较复杂,且通信较多。那么可以考虑使用 [代码]useContext[代码] 和 [代码]useReducer[代码] 说下这次需求中涉及到的通信。 原则:组件尽可能值管理自己的状态。 遵循如上原则,最终的业务交互逻辑都是由组件内部管理,涉及到的同级通信则通过父组件操作。而父组件操作的原则就是只拿数据,不做任何业务处理。(尽可能的撇清关系) 约束[代码]index[代码]尽可能不写业务逻辑UI 初始化和模块数据初始化需自定义 [代码]hooks[代码]状态尽可能抽离。[代码]component[代码] 过于复杂需额外抽离 [代码]component[代码] 、 [代码]utils[代码] 和 [代码]customized-hooks[代码] 等。参照上文[代码]component[代码] 的 [代码]props[代码] 需抽离复用公共 [代码]utils[代码] 方法编写充分的单元测试公共 [代码]utils[代码] 的方法导出需单独导出([代码]bundle[代码] 大小),且编写注释(调用时候的提醒)尽可能定义 [代码]interface[代码],并且编写注释.毕竟注释即文档以上约束后期应该都会编写相应的 [代码]Eslint[代码] 来进行强约束(咳咳,程序猿基本素养不可靠) 最后看下我正在补充的单元测试,编写单元测试过程中,的确发现了不少工具函数的边缘情况处理的有问题 [图片] [图片] 结束语按照如上的 [代码]page[代码] 代码组织后面又写了一个页面,感觉代码的组织和状态的管理还是较为清晰的。后续会编写相应的 cli 来自动生成页面基础架构,比如 [代码]pmCli add page[代码] or [代码]pmCli add com[代码] 原创 Nealyang 全栈前端精选 最后,本文只做一个抛转,并非定义一种规范。更多的约束和组织,希望大家多多交流,互相学习。
2020-05-21 - 直播现已支持 评论管理 创建直播间 创建商品啦!
1.评论管理评论管理功能,与单用户禁言。之前只支持整个直播间禁止评论,目前已上线通过小助手对单个用户禁言,而且可以设置敏感词,有敏感词的评论将不出现在评论列表(官方已经添加了常见的禁用词)。 [图片] 2.创建直播间 该接口仅供商家后台调用,调用限额 1 万次/天,1万次我的天!!!然而当天貌似只能创建50个直播间,后期做到无限制就好了,哪怕收费也可以滴! [图片] 3.商品添加并提审接口说明 调用此接口上传并提审需要直播的商品信息,审核通过后商品录入【小程序直播】商品库 注意:开发者必须保存【商品ID】与【审核单ID】,如果丢失,则无法调用其他相关接口 调用频率 调用额度:500次/一天 请求方法 POST 请求URL https://api.weixin.qq.com/wxaapi/broadcast/goods/add?access_token=access_token 请求参数示例: json { "goodsInfo": { "coverImgUrl": "ZuYVNKk9sMP1X4m7FXdcDCKra251KDZTjS502UTV7gwalgLZXcrOhG6oNYX6c7AR", "name":"TIT茶杯", "priceType":1, "price":"111", "price2":"", "url":"pages/index/index" } } 请求参数含义 参数说明coverImgUrl填入mediaID(mediaID获取后,三天内有效);图片mediaID的获取,请参考以下文档: https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/New_temporary_materials.html);图片规则:图片尺寸最大300像素*300像素;name商品名称,最长17个汉字,1个汉字相当于2个字符priceType价格类型,1:一口价,2:价格区间,3:显示折扣价;1:一口价,只需要传入price,price2不传;2:价格区间,price字段为左边界,price2字段为右边界,price和price2必传。3:折扣价,price字段为原价,price2字段为现价, price和price2必传price数字,最多保留两位小数,单位元price2数字,最多保留两位小数,单位元url商品详情页的小程序路径 正确返回示例 { "goodsId": 51, "auditId": 525022786, "errcode": 0 } 返回参数含义 参数说明goodsId商品IDauditId审核单ID 4.撤回审核接口说明 调用此接口,可撤回直播商品的提审申请,消耗的提审次数不返还 调用频率 调用额度:500次/一天 请求方法 POST 请求URL https://api.weixin.qq.com/wxaapi/broadcast/goods/resetaudit?access_token=access_token 请求参数示例: json { "auditId": 525022184, "goodsId": 9 } 请求参数含义 参数说明goodsId商品IDauditId审核单ID 正确返回示例 { "errcode": 0 } 5.重新提交审核接口说明 调用此接口可以对已撤回提审的商品再次发起提审申请 调用频率 调用额度:500次/一天(与接口1共用500次限制) 请求方法 POST 请求URL https://api.weixin.qq.com/wxaapi/broadcast/goods/audit?access_token=access_token 请求参数示例: json { "goodsId": 9 } 请求参数含义 参数说明goodsId商品ID 正确返回示例 { "errcode": 0, "auditId": 525022184 } 返回参数含义 参数说明auditId审核单ID 6.删除商品接口说明 调用此接口,可删除【小程序直播】商品库中的商品,删除后直播间上架的该商品也将被同步删除,不可恢复; 调用频率 调用额度:1000次/一天 请求方法 POST 请求URL https://api.weixin.qq.com/wxaapi/broadcast/goods/delete?access_token=access_token 请求参数示例: json { "goodsId": 9 } 请求参数含义 参数说明goodsId商品ID 返回参数 { "errcode": 0, } 7.更新商品接口说明 调用此接口可以更新商品信息,审核通过的商品仅允许更新价格类型与价格,审核中的商品不允许更新,未审核的商品允许更新所有字段, 只传入需要更新的字段。 调用频率 调用额度:1000次/一天 请求方法 POST 请求URL https://api.weixin.qq.com/wxaapi/broadcast/goods/update?access_token=access_token 请求参数示例: json { "goodsInfo": { // 需要更新哪个字段就传入哪个字段,goodsId 必传 "coverImgUrl": "ZuYVNKk9sMP1X4m7FXdcDCKra251KDZTjS502UTV7gwalgLZXcrOhG6oNYX6c7AR", "name":"TIT茶杯", "priceType":1, "price": "1111", "price2":"", "url": "pages/index/index", "goodsId": 9 } } 请求参数含义 参数说明coverImgUrl填入mediaID(mediaID获取后,三天内有效);图片mediaID的获取,请参考以下文档: https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/New_temporary_materials.html);图片规则:图片尺寸最大300像素*300像素;name商品名称,最长17个汉字,1个汉字相当于2个字符priceType价格类型,1:一口价,2:价格区间,3:显示折扣价;1:一口价,只需要传入price,price2不传;2:价格区间,price字段为左边界,price2字段为右边界,price和price2必传。3:折扣价,price字段为现价,price2字段为原价, price和price2必传price数字,最多保留两位小数,单位元price2数字,最多保留两位小数,单位元url商品详情页的小程序路径goodsId商品ID 返回参数 { "errcode": 0, } 8.获取商品状态接口说明 调用此接口可获取商品的信息与审核状态 调用频率 调用额度:1000次/一天 请求方法 POST 请求URL https://api.weixin.qq.com/wxa/business/getgoodswarehouse?access_token=access_token 请求参数示例: json { "goods_ids": [1] } 请求参数含义 参数说明goods_ids商品ID 返回参数 { "errcode":0, "errmsg":"ok", "goods": [ { "goods_id":9, "cover_img_url":"xxxx", "name":"xxxxx" "price":12300, "url":"xxxxxxx", "price_type":1, "price2":0, "audit_status":1, "third_party_tag":0 } ], "total":0 } 返回参数含义 参数说明goods_id商品IDname商品名称priceType1:一口价,此时读price字段,2:价格区间,此时price字段为左边界,price2字段为右边界,3:折扣价,此时price字段为原价,price2字段为现价;price价格左区间,单位“元”price2价格右区间,单位“元”audit_status0:未审核,1:审核中,2:审核通过,3审核失败third_party_tag2:表示是为api添加商品,否则不是api添加商品 9.获取商品列表接口说明 调用此接口可获取商品列表 调用频率 调用额度:10000次/一天 请求方法 GET 请求URL https://api.weixin.qq.com/wxaapi/broadcast/goods/getapproved?access_token=[access_token] URL query 参数 参数说明offset分页条数起点limit分页大小,默认30,不超过100status商品状态,0:未审核。1:审核中,2:审核通过,3:审核驳回 返回参数 { "errcode":0, "total":68, "goods": [ { "goodsId":9, "coverImgUrl":"xxxx", "name":"xxxxx" "price":12300, "url":"xxxxxxx", "priceType":1, "price2":0, "thirdPartyTag":0 } ] } 返回参数含义 参数说明goodsId商品IDname商品名称priceType1:一口价,此时读price字段,2:价格区间,此时price字段为左边界,price2字段为右边界,3:折扣价,此时price字段为原价,price2字段为现价;price价格左区间,单位“元”price2价格右区间,单位“元”thirdPartyTag1,2:表示是为api添加商品,否则是再MP添加商品total商品数量url商品小程序路径coverImgUrl商品图片链接 10.往指定直播间导入已入库商品接口说明 调用此接口往指定直播间导入已入库的商品 调用频率 调用额度:10000次/一天 请求方法 POST 请求URL https://api.weixin.qq.com/wxaapi/broadcast/room/addgoods?access_token= 请求参数示例: json { "ids": [9,11], "roomId":223 } 请求参数含义 参数说明ids数组列表,可传入多个,里面填写 商品IDroomId房间id 返回参数 { "errcode":0, }
2020-05-25