- JS中的二进制数据处理
前言 在现有的计算机中,二进制常常以字节数组的形式存在于程序当中。例如在C#里面,就用byte[],标准C里面没有byte类型,但可以通过typedef把byte定义为unsigned char的别名,效果是一样的。JS设计之初似乎就没想过要处理二进制,对于字节的概念可以说是非常非常的模糊。如果要表达字节数组,那么似乎只能用一个普通数组来表示。 然而随着业务需求的逐渐发展,出现了WebGL这样的技术。所谓WebGL,就是指浏览器与显卡之间的通信接口。为了满足JavaScript与显卡之间大量的、实时的数据交换,它们之间的数据通信必须是二进制的,而不能是传统的文本格式。类型化数组(Typed Array)就是在这种背景下诞生的。而类型化数组是建立在ArrayBuffer对象的基础上的。下面介绍一下Arraybuffer。 一、Arraybuffer1.1 基本概念 ArrayBuffer 对象是 ES6 才纳入正式 ECMAScript 规范,是 JavaScript 操作二进制数据的一个接口。ArrayBuffer 对象是以数组的语法处理二进制数据,也称二进制数组。它不能直接读写,只能通过视图(TypedArray视图和DataView视图)来读写。 ❝ArrayBuffer 简单说是一片内存,但是你不能直接用它。这就好比你在 C 里面,malloc 一片内存出来,你也会把它转换成 unsigned_int32 或者 int16 这些你需要的实际类型的数组/指针来用。这就是 JS 里的 TypedArray 的作用,那些 Uint32Array 也好,Int16Array 也好,都是给 ArrayBuffer 提供了一个 “View”,MDN 上的原话叫做 “Multiple views on the same data”,对它们进行下标读写,最终都会反应到它所建立在的 ArrayBuffer 之上。❝1.2 基本操作「语法」 new ArrayBuffer(length) 参数:length 表示要创建的 ArrayBuffer 的大小,单位为字节;返回值:ArrayBuffer 对象;异常:如果 length 大于 Number.MAX_SAFE_INTEGER(>= 2 ** 53)或为负数,则抛出一个 RangeError 异常;「示例」 const buffer = new ArrayBuffer(32); buffer.byteLength; // 32 const v = new Int32Array(buffer); ArrayBuffer.isView(v) // true const buffer2 = buffer.slice(0, 1); 上面代码表示实例对象 buffer 占用 32 个字节。 它有实例属性 byteLength ,表示当前实例占用的内存字节长度。 它拥有一个静态方法isView(),这个方法可以用来判断是否为TypedArray实例或DataView实例。 它拥有实例方法 slice(),用来复制一部分内存,使用方式同数组的slice方法。 除了slice方法,ArrayBuffer对象不提供任何直接读写内存的方法,只允许在其上方建立视图,然后通过视图读写。 二、视图2.1 TypedArray TypedArray一共包含九种类型,每一种都是一个构造函数。(DataView视图不支持Uint8ClampedArray,其他都支持) 名称描述长度(字节)Int8Array8位有符号整数1Uint8Array8位无符号整数1Uint8ClampedArray8位无符号整型固定数组(数值在0~255之间)1Int16Array16位有符号整数2Uint16Array16位无符号整数2Int32Array32位有符号整数4Uint32Array32 位无符号整数4Float32Array32 位 IEEE 浮点数4Float64Array64 位 IEEE 浮点数8 每一种视图都有一个BYTES_PER_ELEMENT常数,表示这种数据类型占据的字节数。 Int8Array.BYTES_PER_ELEMENT // 1 Uint8Array.BYTES_PER_ELEMENT // 1 Int16Array.BYTES_PER_ELEMENT // 2 Uint16Array.BYTES_PER_ELEMENT // 2 Int32Array.BYTES_PER_ELEMENT // 4 Uint32Array.BYTES_PER_ELEMENT // 4 Float32Array.BYTES_PER_ELEMENT // 4 Float64Array.BYTES_PER_ELEMENT // 8 这 9 个构造函数生成的数组,统称为TypedArray视图。它们很像普通数组,都有length属性,普通数组的操作方法和属性,对TypedArray 数组完全适用。 普通数组与 TypedArray 数组的差异主要在以下方面: [图片] TypedArray和Array之间也可以互相转换 const typedArray = new Uint8Array([1, 2, 3, 4]); const normalArray = Array.apply([], typedArray); 「建立TypedArray视图」 // 创建一个8字节的ArrayBuffer const a = new ArrayBuffer(8); // 创建一个指向a的Int32视图,开始于字节0,直到缓冲区的末尾 const a1 = new Int32Array(a); // 创建一个指向a的Uint8视图,开始于字节4,直到缓冲区的末尾 const a2 = new Uint8Array(a, 4); // 创建一个指向a的Int16视图,开始于字节4,长度为2 const a3 = new Int16Array(a, 4, 2); 上面代码在一段长度为 8 个字节的内存(a)之上,生成了三个视图:a1、a2和a3。 视图的构造函数可以接受三个参数: 第一个参数(必选):视图对应的底层ArrayBuffer对象;第二个参数:视图开始的字节序号,默认从 0 开始;第三个参数:视图包含的数据个数,默认直到本段内存区域结束; 建立了视图以后,就可以进行各种操作了。这里需要明确的是,视图其实就是普通数组,语法完全没有什么不同,只不过它直接针对内存进行操作,而且每个成员都有确定的数据类型。所以,视图就被叫做“类型化数组”。 「TypedArray视图操作」 const buffer = new ArrayBuffer(8); const int16View = new Int16Array(buffer); for (let i = 0; i < int16View.length; i++) { int16View[i] = i * 2; } console.log(int16View) // [0, 2, 4, 6] 上面代码生成一个8字节的ArrayBuffer对象,然后在它的基础上,建立了一个16位整数的视图。由于每个字节占据8位,那么16位就占据了2个字节(1个字节等于8位),所以一共可以写入4个整数,依次为0,2,4,6。 如果在这段数据上接着建立一个8位整数的视图,则可以读出完全不一样的结果。 const int8View = new Int8Array(buffer); for (let i = 0; i < int8View.length; i++) { int8View[i] = i; } console.log(int8View) // [0, 0, 2, 0, 4, 0, 6, 0] 首先整个ArrayBuffer对象会被分成8段。然后,由于x86体系的计算机都采用小端字节序(具体概念理解请自主查询),相对重要的字节排在后面的内存地址,相对不重要字节排在前面的内存地址,所以就得到了上面的结果。还可以看到下面这个例子 const buffer = new ArrayBuffer(4); const v1 = new Uint8Array(buffer); v1[0] = 10; v1[1] = 3; v1[2] = 11; v1[3] = 8; console.log(v1) // [10, 3, 11, 8] const uInt16View = new Uint16Array(buffer); // [0xa, 0x3, 0xb, 0x8] console.log(uInt16View) // 计算机采用小端字节序 [0x030a, 0x080b] => [778, 2059] 如果一段数据是大端字节序(大端字节序主要用于数据传输),TypedArray 数组将无法正确解析,因为它只能处理小端字节序!为了解决这个问题,JavaScript 引入DataView对象,可以设定字节序。 2.2 DataView DataView 视图是一个可以从二进制 ArrayBuffer 对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的字节序问题。 ❝ 字节顺序,又称端序或尾序(英语:Endianness),在计算机科学领域中,指存储器中或在数字通信链路中,组成多字节的字的字节的排列顺序。字节的排列方式有两个通用规则。例如,一个多位的整数,按照存储地址从低到高排序的字节中,如果该整数的最低有效字节(类似于最低有效位)在最高有效字节的前面,则称小端序;反之则称大端序。在网络应用中,字节序是一个必须被考虑的因素,因为不同机器类型可能采用不同标准的字节序,所以均按照网络标准转化。例如假设上述变量 x 类型为int,位于地址 0x100 处,它的值为 0x01234567,地址范围为 0x100~0x103字节,其内部排列顺序依赖于机器的类型。大端法从首位开始将是:0x100: 01, 0x101: 23,..。而小端法将是:0x100: 67, 0x101: 45,..。❝「语法」 new DataView(buffer [, byteOffset [, byteLength]]) 相关的参数说明如下: buffer:ArrayBuffer 对象 或 SharedArrayBuffer 对象;byteOffset(可选):此 DataView 对象的第一个字节在 buffer 中的字节偏移。如果未指定,则默认从第一个字节开始;异常:此 DataView 对象的字节长度。如果未指定,这个视图的长度将匹配 buffer 的长度;「示例」 const buffer = new ArrayBuffer(16); const view = new DataView(buffer, 0); view.setInt8(1, 68); view.getInt8(1); // 68 如果一次操作(get或者set)两个或两个以上字节,就必须明确数据的存储方式,到底是小端字节序还是大端字节序。DataView的操作方法默认使用大端字节序解读数据,如果需要使用小端字节序解读,必须在操作方法中指定参数为true(get方法的第二个参数和set方法的第三个参数)。 const buffer = new ArrayBuffer(24); const dv = new DataView(buffer); // 1个字节,默认大端字节序 const v1 = dv.getUint8(0); // 小端字节序 const v1 = dv.getUint16(1, true); // 大端字节序 const v2 = dv.getUint16(3, false); // 在第5个字节,以小端字节序写入值为11的32位整数 dv.setInt32(4, 11, true); 对于直接处理ArrayBuffer对象的业务场景不是特别多,特别是写页面比较多的同学。笔者深刻认识并运用的场景,主要是在处理比较复杂且数据量比较大的点云数据,前端接收到的点云数据已经是原始采集数据转换过的二进制数据,前端需要对二进制数据进行解析,运用的解析方法就是上述提到的各种方法。下面介绍一下业务场景中比较常见到的一种二进制表示类型——Blob。 三、Blob3.1 基本介绍 Blob 对象比较常用于文件上传、文件读写操作等。在对文件读写的时候,我们更多的时候只是操作File对象,而File继承了所有Blob的属性。所以在我们看来,File对象可以看作一种特殊的Blob对象。 而Blob 对象与 ArrayBuffer 的区别在于,Blob 对象用于操作二进制文件, ArrayBuffer 用于直接操作内存,所以他们有如下图的关系: [图片] 「语法」 const blob = new Blob(array [, options]); 相关的参数说明如下: array:字符串或二进制对象,表示新生成的Blob实例对象的内容;options(可选):比较常用的属性 type,表示数据的 MIME 类型,默认空字符串;「示例」 const array = ['Hello World! ']; const blob = new Blob(array, {type : 'text/html'}); 「属性和方法」 [图片] 由上图可以看到,Blob对象拥有size和type两个属性,以及多种自有方法。比较常用的方法slice、arrayBuffer等;slice方法主要用来拷贝原来的数据,返回的也是一个Blob实例,这个方法可以用来做切片上传。arrayBuffer方法返回一个 Promise 对象,包含 blob 中的数据,并在 ArrayBuffer 中以二进制数据的形式呈现。 const blob = new Blob([]); blob.slice(0, 1); blob.arrayBuffer().then(buffer => /* 处理 ArrayBuffer 数据的代码……*/); 3.2 运用场景通过window.URL.createObjectURL方法可以把一个blob转化为一个Blob URL,并且用做文件下载或者图片显示的链接。 Blob URL所实现的下载或者显示等功能,仅仅可以在单个浏览器内部进行。而不能在服务器上进行存储,亦或者说它没有在服务器端存储的意义。 下面是一个Blob的例子,可以看到它很短 blob:d3958f5c-0777-0845-9dcf-2cb28783acaf 和冗长的Base64格式的Data URL相比,Blob URL的长度显然不能够存储足够的信息,这也就意味着它只是类似于一个浏览器内部的“引用“。从这个角度看,Blob URL是一个浏览器自行制定的一个伪协议。 「文件下载」 [图片] 「图片显示」 [图片] 「切片上传」 [图片] 「本地文件读取」 [图片]
2021-05-10 - map 组件 如何给自定义气泡(customCallout)添加事件?
[图片]要实现的效果,第一个是导航,第二个是进入一个简介详情 [图片] 这是代码,点击没有触发任何事件,控制台无输出
2020-12-23 - WXWebAssembly胶水代码在小程序内的使用场景和实例化踩坑记录
1、AR项目 对计算帧率达到毫秒级要求的AR项目,识别,美妆,实时渲染计算都可以使用 2、AI项目 一些需要大型重复计算绘制渲染需要使用gpu提升算法时长的项目 关于WXWebAssembly的坑,在微信8.0.1之前 ,WebAssembly也存在,但是之后WebAssembly就删除了 WXWebAssembly.instantiate(obsoluteUrl,importObjet)该api需要开发工具版本1.0.5以上,否则一直报错,且该api与原生web端的第一个参数更是相差甚远
2021-04-09 - 小程序CSS-JS Shared variable,共享变量,减少CDN文件路径变量重复定义
CSS变量与JS共享: CSS Module的:export方法,功能上类似ES6的export关键字,即导出一个变量对象 首先定义一份sass文件如下: $COMMON_ARROW_RIGHT: 'https://you.domain.com/123dfdds768fkasfhja3.png'; $INDEX_OLD: 'https://you.domain.com/s7812312312dsvasty8jkassd.png'; $MINE_USER_AVATAR: 'https://you.domain.com/79jksadsuek32423saasfh4.png'; // :export { COMMON_ARROW_RIGHT: $COMMON_ARROW_RIGHT; INDEX_OLD: $INDEX_OLD; MINE_USER_AVATAR: $SERVICE_WARNING; } 然后定义cloud.js文件如下: /** * CDN,图片云,资源云,CSS-JS 变量共享 */ import urls from './index.module.scss'; const clouds = {}; for(let i in urls) clouds[i] = urls[i].split('"')[1]; export default clouds; CSS中使用 @import "~@cloud/index.module.scss"; .container { position: relative; width: 100%; height: 100vh; background: #f3f7f9 url($INDEX_OLD) top center no-repeat; } JS中使用: JS中使用: import Clouds from '@cloud/index'; const { MINE_USER_AVATAR } = Clouds; // Render函数部分伪代码: <Image className={'global-user-avatar'} src={MINE_USER_AVATAR} />
2021-04-29 - 关于Taro小程序,主包太大,无法预览上传的问题
1、Taro的echart组件,无论怎么处理都蛮大,450kb左右,放到主包中,肯定会超大小,所以放到分包页面、配置echart文件目录的alisa之后,只要有两个不同的分包页面引入该echart就会导致eachrt的分包策略失败,抽离到common.js中去了,这个体积大概是500kb 如果一定要主包中展示echart不妨换个思路,把echart模块当做一个插件发布,然后引入插件 2、如果多个页面引入了同样的小图片,可能会被处理成base64打到venders或者commons.js中去,虽然不多,四五张小图标就好几十kb了,最好是放到分包中或者走cdn 3、一些比较重的库momentjs可以换做dayjs,或者仅仅用到一些库中的部分方法,可以考虑只拷贝其中的函数出来,减少体积 4、开启编译打包分析,默认是关闭的 mini: { webpackChain (chain) { chain.plugin('analyzer') .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, []) } }
2021-04-28 - 🔥Vue 转 React不完全指北
老东家 Vue,加实习写了两年半~,期间只是简单学过 React,没怎么写过。新东家用 React + Typescript,用了一个半月,写了写Demo 在线预览。 这里简单总结下和vue的区别,如果你也是在Vue转React阶段,欢迎点击http://github.crmeb.net/u/yi了解更多 有不同见解,欢迎评论区指教🤓 一、横向对比 1、Vue 官方对比 Vue 官方对比 React 2、个人的理解 一般 H5 的,或者一些做不大的系统,首选 Vue。因为 Vue 简单,开发效率比较高。同时 Vue 包的体积也更小,在移动端网络差异大的情况下,资源体积是非常重要的。 像一些后台系统,会越做越大的,就用 React。解决方案更多,后期也更方便迭代与维护。(本人有幸开发过 Vue 大项目,webpack 热更新一下 3mins+) 二、核心思想 Vue 早期定位是尽可能的[代码]降低前端开发的门槛[代码](这跟 Vue 作者是独立开发者也有关系)。所以 Vue [代码]推崇[代码]灵活易用(渐进式开发体验),数据可变,双向数据绑定(依赖收集)。 React 早期口号是 [代码]Rethinking Best Practices[代码](重新思考最佳实践)。背靠大公司 Facebook 的 React,从开始起就不缺关注和用户,而且 React 想要做的是用更好的方式去颠覆前端开发方式(事实上跟早期 jquery 称霸前端,的确是颠覆了)。所以 React [代码]推崇[代码]函数式编程(纯组件),数据不可变以及单向数据流。函数式编程最大的好处是其稳定性(无副作用)和可测试性(输入相同,输出一定相同),所以通常大家说的 React 适合大型应用,根本原因还是在于其函数式编程。 由于两者核心思想的不同,所以导致 Vue 和 React 许多外在表现不同(从开发层面看)。 引用这位大哥写的 理解 Vue 和 React 区别 三、生命周期 Vue Vue 生命周期官方图解 [图片] React 大神绘图 React 生命周期 点击生命周期即可跳转官网解读 [图片] 综合对比 生命周期这块基本都是围绕着[代码]挂载、更新、卸载[代码]三个方面 Vue 提供的比较多,但是常用的: [代码]created/mounted/destroyed[代码]React 新版废弃了一些,常用的: [代码]componentDidMount/componentDidUpdate/componentWillUnmount[代码],Hooks 更是没有 四、数据流 Vue 双向绑定,单向数据流:vue2.x 通过 [代码]v-model[代码] 实现双向绑定,可以不关心受控组件,v-model 相当于 onChange 的语法糖 <input v-model="value" /> React 单向数据流:[代码]万物皆 Props[代码],主要通过 [代码]onChange/setState()[代码]的形式该更新数据,需要所以在 react 中需要关注受控组件的写法 // 会报错,props的值不可修改 // 在onChange调用setState修改数据,需要调用setState修改绑定数据 受控组件 [图片] 五、组件 1、组件封装 Vue // 父组件 import Child from './Child' export default { name: 'Father', components: { Child }, data() { return { text: '接收到了父组件数据' } } } // 子组件 export default { name: 'child', props: ['text'], data() { return { children: '子组件自己的数据' } } } React import React, { useState, useEffect } from "react"; function Child({ onClick }) { const [list, setList] = useState([]); useEffect(() => { setList([1, 2, 3]); }, [onClick]); return ( {list.map((item, index) => { return {item} ; })} ); } function Father() { const show = () => { return [4, 5, 6]; }; return ( ); } export default Father; 2、组件通信 Vueprops/emitprovide/injectvuex(双向数据绑定,响应式)event bus Reactprops(子传父通过[代码]props.function[代码])contextredux(单向数据流) 3、组件嵌套 Vue:slot 插槽 // index.vue ; import Test from "./test"; // test.vue ; React: props.children // 父组件 import Test from "./test"; 组件嵌套 ; // 子组件 import * as React from "react"; const Test: React.FC = (props) => { return ( <> 测试props.children {props.children} ); }; export default Test; 六、总体感受 1、一些区别vue [代码]更简单,更方便[代码],熟悉了 api 以后,实现某些简单功能更快。react 写法更偏向于[代码]原生 JS[代码],Class 的写法不是很舒服,个人更喜欢 [代码]hooks[代码]。熟悉了 [代码]hooks[代码] 以后,写起来很自由,不用关心 vue 中固定的 [代码]options api[代码]react 做中后台优势更大,有大厂加持,生态更好,组件库功能也更多,解决方案也更多vue2.x 对 typescript 不太友好,[代码]react + typescript[代码] 更加舒适,两者写起来风格差距较大。react JSX 写起来还是不够熟练,[代码]onClick、style、className[代码] 等等,没有 [代码]v-if,v-for,All in JS[代码]。Vue 则推崇 [代码]html、js、css 分离[代码]的写法,当然 vue 也可以写 JSXvue 的 prop 必须在[代码]子组件 props 字段里声明[代码]。React 的 prop 不强制声明,直接使用,如果用 TS 的话还是要声明的 2、学习很多人说 vue 转 react 很简单,一周熟练上手。我比较菜,感觉适应起来还是[代码]有成本的[代码],但是也没有很难,最主要的还是要多动手,不懂就深挖为什么通读一遍 react 官网,对着例子多敲敲,好好理解,做做笔记。B 站 React技术全家桶 学习视频,可以不敲,[代码]快速过一遍[代码],毕竟都不是小白了。然后自己搞个项目,[代码]去实现一些自己感兴趣的东西[代码]基础知识过完以后,[代码]查缺补漏[代码],找各种博文读一读,不理解的[代码]再次[代码]进行学习[代码]总结[代码]自己的学习成果,react已经学了一段时间了,后面再整理一下,发出来为了提高熟练度,用公司的组件库(zent)自己动手写了写,有兴趣的老哥参考下:在线预览:俊劫学习系统 Github 源码:基于 react + typescript 欢迎[代码]start[代码] [图片] 七、参考文章「Vue」与「React」--使用上的区别从 Vue 转 React 的一些体验Vue 转 React 指南,看这篇文章就够了理解 Vue 和 React 区别 八、最后 [图片] 作者:俊劫 感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi
2021-04-23 - 【问题排查】小程序闪退
在使用小程序的时候,偶然会发生闪退。这里来讲一下闪退的问题该如何排查。 版本排查 发生闪退的时候,首先,要确认下 版本 是不是最新的。如果不是,建议更新版本再重试。旧版本的问题会在新版本进行修复哦。 微信版本: 微信官网 基础库版本:基础库更新日志小程序自查 确认版本都是最新情况下,还是有闪退的问题的话,建议先进行小程序自查~ 一般情况下,闪退是因为内存使用过多导致的,小程序侧可以通过基础库提供 wx.onMemoryWarning 接口来监听内存不足的告警,当收到告警时,通过回收一些不必要资源避免进一步加剧内存紧张。 反馈官方 如果问题还是会出现的话建议反馈给官方处理,需要附带上以下信息点协助排查(划重点:完整的提供信息才可以加速问题处理进度哦!!!) 示例: 系统及微信版本号:安卓7.0.17、IOS 7.0.17(出现问题的时候,建议两端都测试,给出有问题的case)必现 or 偶现:必现可复现场景:代码片段 或者 线上小程序复现步骤:进入首页,点击添加按钮等等,推荐录制复现的 视频(重点)进行上传。上传日志:提供微信号,复现时间点(操作步骤:手机微信那里上传下日志: 我 -> 设置 -> 帮助与反馈:右上角扳手 -> 上报日志,选择出现问题的日期,上传日志)
2020-11-03 - 微信、支付宝小程序CI尝鲜
背景微信上传代码流程 PR -> 审核通过 -> 切到master分支 -> git pull -> 切到ide -> 点击上传代码 -> 填入备注和版本号 CI PR -> 审核通过 -> CI上传 利用CI后除了一开始的人工提PR和审核后,不需要进行任何操作,并且会给出开始上传和上传完成的通知,解决了之前上传代码流程较长的人工链路,释放自己,不用一直做无意义且无趣的事。 执行过程的效果图如下: [图片] 上传成功后 我们需要去小程序后台提交审核信息。 [图片] 钉钉自定义机器人文档:钉钉webHooks,码云webHook对钉钉的支持,钉钉自定义机器人SDK 需要设置群机器人群设置 -> 智能群助手 -> 添加机器人 -> 自定义机器人 -> 勾选安全设置加签使用access_token和secret完成自定义机器人初始化[图片] 码云WebHooks文档:码云webHooks,码云webHook推送数据格式说明 在URL处填入刚刚的接口,比如https://xxx.com/upload勾选Pull Request,然后点击更新即可,这里需要注意他会发送好几次,比如创建PR发送一次,审核通过发送一次,合并发送一次,所以在接口里需要判断一下只有合并完成的时候才执行后续逻辑(state === 'merged'[图片] 微信小程序准备工作微信官方CI API 密钥:微信公众号 -> 开发 -> 开发设置,下载代码上传密钥,然后在调用CI时设置好privateKeyPath白名单配置:微信公众号 -> 开发 -> 开发设置,配置白名单(必须IP)开发先来个流程图,让大家了解一下接口的逻辑 [图片]判断是否是合并到master(十一后可能要改为main了)值得一提的是目前码云的webHook的推送数据格式是Request Payload,所以我们需要利用koa2-formidable插件对数据进行处理后就能直接使用ctx.request.body获取数据了 router.post('/api/upload', async ctx => { const body = ctx.request.body // 合并状态并且合到master才会自动上传到后台 if (body.state === 'merged' && body.target_branch === 'master') { ... } } 钉钉初始化const Robot = require("dingtalk-robot-sdk") const robot = new Robot({ accessToken: config.robotAccessToken, secret: config.robotSecret }); const text = new Robot.Text('开始上传') robot.send(text) 是否存在项目const xcxPath = path.join(process.cwd(), config[type].publicProject) // 判断是否存在xcx项目文件夹,没有就去clone,有就pull最新代码 if (fs.existsSync(xcxPath)) { child_process.execSync('git pull', { cwd: xcxPath }) } else { child_process.execSync(`git clone ${config[type].git}`, { cwd: path.join(process.cwd(), config.publicRoot) }) } 项目初始化和上传// 创建项目对象 const project = new ci.Project({ appid: config.wx.appid, type: 'miniProgram', projectPath: publicProject, privateKeyPath: path.join(process.cwd(), config.publicRoot, '../wx.key'), // wx.key为小程序appSecret ignores: ['node_modules/**/*'], }) const year = new Date().getFullYear() - 2000 let month = new Date().getMonth() + 1 const day = new Date().getDate() // 根据年月日当版本号 const version = '2.5.' + year + (month < 10 ? '0' + month : month) + (day < 10 ? '0' + day : day); // 上传 const previewResult = await ci.upload({ project, version, desc: body.title, setting: { es6: true, minify: true, autoPrefixWXSS: true, minifyWXML: true, minifyWXSS: true, minifyJS: true } }) 钉钉通知完成const markdown = new Robot.Markdown() .setTitle('上传完成!!!!') .add(`### [上传完成](https://mp.weixin.qq.com)\n`) .add(`1. version:${version}`) .add(`2. size:${fullSize}`) .add(`3. ${JSON.stringify(previewResult)}`) robot.send(markdown) 支付宝小程序准备工作文档:支付宝官方CLI文档,支付宝官方SDK文档 开发其余逻辑均与上面的微信小程序开发一致,只有项目初始化和上传是不一致的,所以这里我们只说一下项目初始化和上传 项目初始化使用alipaydev key create -w 生成的私钥和工具id完成项目初始化 // 初始化项目 alipaydev.setConfig({ toolId: config.ali.toolId, privateKey: config.ali.privateKey, }) 上传version不传的话默认线上包版本自增0.0.1,所以我们不需要传version const uploadResult = await alipaydev.miniUpload({ appId: config.ali.appid, clientType: 'alipay', project: publicProject, experience: true // 设置体验版(不是主账户,所以其实目前没用) })
2020-09-29 - 如何写一个自己的脚手架 - 一键初始化项目
如何写一个自己的脚手架 - 一键初始化项目 介绍 脚手架的作用:为减少重复性工作而做的重复性工作 即为了开发中的:编译 es6,js 模块化,压缩代码,热更新等功能,我们使用[代码]webpack[代码]等打包工具,但是又带来了新的问题:初始化工程的麻烦,复杂的[代码]webpack[代码]配置,以及各种配置文件,所以就有了一键生成项目,0 配置开发的脚手架 本文项目代码地址 本文以我司的脚手架工具 简化之后为基础 本系列分 3 篇,详细介绍如何实现一个脚手架: 一键初始化项目 0 配置开发环境与打包 一键上传服务器 首先说一下个人的开发习惯 在写功能前我会先把调用方式写出了,然后一步一步的从使用者的角度写,现将基础功能写好后,慢慢完善 例如一键初始化项目功能 我期望的就是 在命令行执行输入 [代码]my-cli create text-project[代码],回车后直接创建项目并生成模板,还会把依赖都下载好 我们下面就从命令行开始入手 创建项目 [代码]my-cli[代码],执行 [代码]npm init -y[代码]快速初始化 bin [代码]my-cli[代码]: 在 [代码]package.json[代码] 中加入: [代码]{ "bin": { "my-cli": "bin.js" } } [代码] [代码]bin.js[代码]: [代码]#!/usr/bin/env node console.log(process.argv); [代码] [代码]#!/usr/bin/env node[代码],这一行是必须加的,就是让系统动态的去[代码]PATH[代码]目录中查找[代码]node[代码]来执行你的脚本文件。 命令行执行 [代码]npm link[代码] ,创建软链接至全局,这样我们就可以全局使用[代码]my-cli[代码]命令了,在开发 [代码]npm[代码] 包的前期都会使用[代码]link[代码]方式在其他项目中测试来开发,后期再发布到[代码]npm[代码]上 命令行执行 [代码]my-cli 1 2 3[代码] 输出:[代码][ '/usr/local/bin/node', '/usr/local/bin/my-cli', '1', '2', '3' ][代码] 这样我们就可以获取到用户的输入参数 例如[代码]my-cli create test-project[代码] 我们就可以通过数组第 [2] 位判断命令类型[代码]create[代码],通过第 [3] 位拿到项目名称[代码]test-project[代码] commander [代码]node[代码]的命令行解析最常用的就是[代码]commander[代码]库,来简化复杂[代码]cli[代码]参数操作 (我们现在的参数简单可以不使用[代码]commander[代码],直接用[代码]process.argv[3][代码]获取名称,但是为了之后会复杂的命令行,这里也先使用[代码]commander[代码]) [代码]#!/usr/bin/env node const program = require("commander"); const version = require("./package.json").version; program.version(version, "-v, --version"); program .command("create <app-name>") .description("使用 my-cli 创建一个新的项目") .option("-d --dir <dir>", "创建目录") .action((name, command) => { const create = require("./create/index"); create(name, command); }); program.parse(process.argv); [代码] [代码]commander[代码] 解析完成后会触发[代码]action[代码]回调方法 命令行执行:[代码]my-cli -v[代码] 输出:[代码]1.0.0[代码] 命令行执行: [代码]my-cli create test-project[代码] 输出:[代码]test-project[代码] 创建项目 拿到了用户传入的名称,就可以用这么名字创建项目 我们的代码尽量保持[代码]bin.js[代码]整洁,不将接下来的代码写在[代码]bin.js[代码]里,创建[代码]create[代码]文件夹,创建[代码]index.js[代码]文件 [代码]create/index.js[代码]中: [代码]const path = require("path"); const mkdirp = require("mkdirp"); module.exports = function(name) { mkdirp(path.join(process.cwd(), name), function(err) { if (err) console.error("创建失败"); else console.log("创建成功"); }); }; [代码] [代码]process.cwd()[代码]获取工作区目录,和用户传入项目名称拼接起来 (创建文件夹我们使用[代码]mkdirp[代码]包,可以避免我们一级一级的创建目录) 修改[代码]bin.js[代码]的[代码]action[代码]方法: [代码]// bin.js .action(name => { const create = require("./create") create(name) }); [代码] 命令行执行: [代码]my-cli create test-project[代码] 输出:[代码]创建成功[代码] 并在命令行所在目录创建了一个[代码]test-project[代码]文件夹 模板 首先需要先列出我们的模板包含哪些文件 一个最基础版的[代码]vue[代码]项目模板: [代码]|- src |- main.js |- App.vue |- components |- HelloWorld.vue |- index.html |- package.json [代码] 这些文件就不一一介绍了 我们需要的就是生成这些文件,并写入到目录中去 模板的写法后很多种,下面是我的写法: 模板目录: [代码]|- generator |- index-html.js |- package-json.js |- main.js |- App-vue.js |- HelloWorld-vue.js [代码] [代码]generator/index-html.js[代码] 模板示例: [代码]module.exports = function(name) { const template = ` { "name": "${name}", "version": "1.0.0", "description": "", "main": "index.js", "scripts": {}, "devDependencies": { }, "author": "", "license": "ISC", "dependencies": { "vue": "^2.6.10" } } `; return { template, dir: "", name: "package.json" }; }; [代码] [代码]dir[代码]就是目录,例如[代码]main.js[代码]的[代码]dir[代码]就是[代码]src[代码] [代码]create/index.js[代码]在[代码]mkdirp[代码]中新增: [代码]const path = require("path"); const mkdirp = require("mkdirp"); const fs = require("fs"); module.exports = function(name) { const projectDir = path.join(process.cwd(), name); mkdirp(projectDir, function(err) { if (err) console.error("创建失败"); else { console.log(`创建${name}文件夹成功`); const { template, dir, name: fileName } = require("../generator/package")(name); fs.writeFile(path.join(projectDir, dir, fileName), template.trim(), function(err) { if (err) console.error(`创建${fileName}文件失败`); else { console.log(`创建${fileName}文件成功`); } }); } }); }; [代码] 这里只写了一个模板的创建,我们可以用[代码]readdir[代码]来获取目录下所有文件来遍历执行 下载依赖 我们平常下载[代码]npm[代码]包都是使用命令行 [代码]npm install / yarn install[代码] 这时就需要用到 [代码]node[代码] 的 [代码]child_process.spawn[代码] api 来调用系统命令 因为考虑到跨平台兼容处理,所以使用 cross-spawn 库,来帮我们兼容的操作命令 我们创建[代码]utils[代码]文件夹,创建[代码]install.js[代码] [代码]utils/install.js[代码]: [代码]const spawn = require("cross-spawn"); module.exports = function install(options) { const cwd = options.cwd || process.cwd(); return new Promise((resolve, reject) => { const command = options.isYarn ? "yarn" : "npm"; const args = ["install", "--save", "--save-exact", "--loglevel", "error"]; const child = spawn(command, args, { cwd, stdio: ["pipe", process.stdout, process.stderr] }); child.once("close", code => { if (code !== 0) { reject({ command: `${command} ${args.join(" ")}` }); return; } resolve(); }); child.once("error", reject); }); }; [代码] 然后我们就可以在创建完模板后调用[代码]install[代码]方法下载依赖 [代码]install({ cwd: projectDir }); [代码] 要知道工作区为我们项目的目录 至此,解析 cli,创建目录,创建模板,下载依赖一套流程已经完成 基本功能都跑通之后下面就是要填充剩余代码和优化 优化 当代码写的多了之后,我们看上面[代码]create[代码]方法内的回调嵌套回调会非常难受 [代码]node 7[代码]已经支持[代码]async,await[代码],所以我们将上面代码改成[代码]Promise[代码] 在[代码]utils[代码]目录下创建,[代码]promisify.js[代码]: [代码]module.exports = function promisify(fn) { return function(...args) { return new Promise(function(resolve, reject) { fn(...args, function(err, ...res) { if (err) return reject(err); if (res.length === 1) return resolve(res[0]); resolve(res); }); }); }; }; [代码] 这个方法帮我们把回调形式的[代码]Function[代码]改成[代码]Promise[代码] 在[代码]utils[代码]目录下创建,[代码]fs.js[代码]: [代码]const fs = require(fs); const promisify = require("./promisify"); const mkdirp = require("mkdirp"); exports.writeFile = promisify(fs.writeFile); exports.readdir = promisify(fs.readdir); exports.mkdirp = promisify(mkdirp); [代码] 将[代码]fs[代码]和[代码]mkdirp[代码]方法改造成[代码]promise[代码] 改造后的[代码]create.js[代码]: [代码]const path = require("path"); const fs = require("../utils/fs-promise"); const install = require("../utils/install"); module.exports = async function(name) { const projectDir = path.join(process.cwd(), name); await fs.mkdirp(projectDir); console.log(`创建${name}文件夹成功`); const { template, dir, name: fileName } = require("../generator/package")(name); await fs.writeFile(path.join(projectDir, dir, fileName), template.trim()); console.log(`创建${fileName}文件成功`); install({ cwd: projectDir }); }; [代码] 结语 关于进一步优化: 更多功能与健壮 例如指定目录创建项目,目录不存在等情况 [代码]chalk[代码]和[代码]ora[代码]优化[代码]log[代码],给用户更好的反馈 通过[代码]inquirer[代码]问询用户得到更多的选择:模板[代码]vue-router[代码],[代码]vuex[代码]等更多初始化模板功能,[代码]eslint[代码] 更多的功能: 内置 webpack 配置 一键发布服务器 其实要学会善用第三方库,你会发现我们上面的每个模块都有第三方库的身影,我们只是将这些功能组装起来,再结合我们的想法进一步封装 虽然有[代码]vue-cli[代码],[代码]create-react-app[代码]这些已有的脚手架,但是我们还是可能在某些情况下需要自己实现脚手架部分功能,根据公司的业务来封装,减少重复性工作,或者了解一下内部原理
2019-09-26 - AR小程序持续踩坑指南
为了兼容安卓帧率问题,转而采用此方案,文档被删除,文档被删除,文档被删除,通过报错信息和朋友得知使用方法 初始化:<开发者工具限定1.0.5及其以上> WXWebAssembly.instantiate(obsoluteWasmFilePath,importObject) 微信初始化胶水代码算法文件方式: WebAssembly:可以加载本地文件,微信8.0.2起WebAssembly对象被删除 WebAssembly.compile() // 已废弃,文档也已删除,先compile加载wasm文件,再instantiate实例化,其instantiate与WXWebAssembly.instantiate完全不同,传入的是一个arraybuffer对象 WXWebAssembly:没有compile方法,初始化仅支持加载代码包内的wasm绝对路径文件,如/pages/index/index.wasm; WXWebAssembly.instantiate(obsoluteWasmFilePathath,importObject) // obsoluteWasmFilePathath为非wxfile://和http开头且以.wasm结尾的绝对路径文件 // importObject为wasm算法模型文件中初始化引入的方法 wasm模型文件: 其中包括算法部分和模型部分<人脸检测,生物检测等等等等>,以及渲染素材,比如眉毛,腮红眼影等 可分拆为单独的data模型文件部分:人脸检测模型,渲染素材 鉴于人脸算法模型文件很容易就超出2M了,可做如下拆分: Wasm文件可以缩减为仅包含核心算法库和渲染库部分,模型文件抽离成model.data文件,通过WXWebAssembly.instantiate实例化之后导入的方法远程加载模型 模型文件在tensflowjs内包含,model.json和model.bin文件,json文件加载bin文件,c++也是类似机制,一个完整的单独wasm文件包含了从c或者c++转译来的 微信文件系统分为: 代码包文件:仅支持绝对路径读取,且不允许动态增删改,如:/pages/index/index.wasm; 本地文件:通过网络方式下载的文件或缓存文件,其文件路径以wxfile://或http开头,且不允许硬编码为绝对路径,通过wx.env.USER_DATA_PATH也无法编译成绝对路径 本地文件的文件路径均为以下格式: {{协议名}}://文件路径 其中,协议名在 iOS/Android 客户端为 "wxfile",在开发者工具上为 "http",开发者无需关注这个差异,也不应在代码中去硬编码完整文件路径。 相关阅读:微信小程序文件系统说明文档 以上是使用胶水代码小程序初始化过程中遇到相对棘手问题,分享出来,希望在文档出来前对大家有所帮助,持续更新,欢迎留言
2021-04-14