- 云开发私人实时聊天室
云开发私人实时聊天室说明 在最开始开发小程序时,本人和团队成员实现小程序的聊天室时遇到一些困难,查阅了一些资料,有些讲得太泛,有些讲的太难,在一个阶段克服了这个困难后,收获了很多,对整个流程也熟悉了很多,在这里记录自己的一个思路,希望也能对开发新手有帮助。 项目基本配置 1.项目创建及云开发配置: 官方文档:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/quick-start/miniprogram.html PS:注意云函数目录是否为此样式:[图片] 若是普通目录样式记得在project.config.json中配置加入: [图片] 2.添加包colorui,用于样式使用,并在app.wxss中导入改包 [图片][图片] 3.在pages下新建文件夹index和新建page:index [图片] 聊天室静态页面 最终呈现的效果: 自己: [图片] 对方: [图片] 1. wxml 整体结构: [图片] 整一个页面说白了就是由一个scroll-view和一个回复框组成,scroll-view中由消息数组构成,消息的内容可以自己定义(时间,头像,消息内容等等) 具体源码: <!-- scroll-view来实现页面拖动 --> <scroll-view id='page' scroll-into-view="{{toView}}" upper-threshold="100" scroll-y="true" enable-back-to-top="true" class="message-list"> <!-- 每一条消息 --> <view class="cu-chat" wx:for="{{3}}" wx:key="index" id="row_{{index}}"> <!-- 自己发出的消息 --> <block wx:if="{{false}}"> <block wx:if="{{true}}"> <view class="datetime" style="width:100%">2021-11-16 18:10</view> </block> <view class="cu-item self" style="width: 750rpx; height: 120rpx; display: flex; box-sizing: border-box; left: 0rpx; top: 0rpx"> <view class="main"> <view class="content bg-green shadow" style="position: relative; left: 0rpx; top: 22rpx;border-radius: 10rpx"> <text style="font-size:33rpx">这是一条消息</text> </view> </view> <view class="cu-avatar radius center" style="background-image: url({{useravatar}}); width: 71rpx; height: 71rpx; display: flex; box-sizing: border-box; left: 0rpx; top: 0rpx" bindtap="go_myinfo"></view> </view> </block> <!-- 对方发出的消息 --> <block wx:else> <block wx:if="{{true}}"> <view class="datetime" style="width:100%">2021-11-16 19:10</view> </block> <view class="cu-item" style="width: 750rpx; height: 120rpx; display: flex; box-sizing: border-box; left: 0rpx; top: 0rpx"> <view class="cu-avatar radius center" style="background-image: url({{match_avatar}}); width: 71rpx; height: 71rpx; display: flex; box-sizing: border-box; left: 0rpx; top: 0rpx"> </view> <view class="main"> <view class="content bg-white shadow" style="position: relative; left: 0rpx; top: 22rpx;border-radius: 10rpx"> <text style="font-size:33rpx">这是对面的一条消息</text> </view> </view> </view> </block> </view> </scroll-view> <!-- 回复框 --> <view class="reply cu-bar"> <!-- 输入框 --> <view class="opration-area"> <input type="text" bindinput="getContent" value="{{textInputValue}}" maxlength="300" cursor-spacing="10" style="width: 544rpx; height: 64rpx; display: block; box-sizing: border-box;"></input> </view> <!-- 发送按钮 --> <button class="cu-btn bg-green shadow" bindtap='sendMsg' style="width: 150rpx; height: 64rpx; display: flex; box-sizing: border-box; left: -22rpx; top: 0rpx; position: relative">发送</button> </view> 2. wxss一些样式的配置,具体就不详细叙述了,见源码: /*消息窗口*/ .message-list { margin-bottom: 54px; } /*文本输入或语音录入*/ .reply .opration-area { flex: 1; padding: 8px; } /*回复文本框*/ .reply input { background: rgb(252, 252, 252); height: 36px; border: 1px solid rgb(221, 221, 221); border-radius: 6px; padding-left: 3px; } /*回复框*/ .reply { display: flex; flex-direction: row; justify-content: flex-start; align-items: center; position: fixed; bottom: 0; width: 100%; height: 108rpx; border-top: 1px solid rgb(215, 215, 215); background: rgb(245, 245, 245); } /*日期*/ .datetime { font-size: 10px; padding: 10px 0; color: #999; text-align: center; } 到此,静态的页面就已经做好啦,现在主要的难题也是数据部分,下面将先讲述数据库chatroom的设计及解释,最后进行js的代码编写。 数据库创建及设计1.数据库表创建:在编辑器打开云开发控制台,点击数据库,再点击集合名称右边加号,创建一个集合名称为chatroom的表。 [图片][图片] [图片] [图片] 2.chatroom设计具体页面如图: [图片] 其中, _id为记录创建时自动创建的标识属性,即主键 _openid和match_openid代表了自身和对方 records为一个对象数组,每个对象的属性分别是: msgText:消息属性(此案例中只有text属性,即文本,可自扩展为图片、音频等) openid:发送人的标识 sendTime:消息创建时间 sendTimeTS:消息创建时的时间戳(用于做时间比较,判断时间显示) showTime:消息是否显示时间 textContent:具体文本内容 其中, records:array类型, records中的记录:object类型 records中的sendTimeTS:number类型 records中的showTime:boolean类型 其余全为string类型 PS: 1、openid和match_openid可标识一个聊天室,是唯一不变的; 2、用户本身的openid是有可能在记录中的match_openid位置上的,谁发起了这个聊天室,openid这个位置就是那个发起用户的openid,所以在开发中,想要获取自己和所有其他人的聊天室,要查每条记录中的openid或者match_openid与自身openid是否匹配。 3.权限设置因为该表中的记录,非记录创建者也可以进行读写,这里的权限记得设置,不然会出问题: [图片] [图片] [图片] 具体功能实现(JS写法)1.先配置Page.data:6个属性,如有需要可自行扩展 [图片] chats存储数据库表中的records的所有信息; textInputValue是输入框内容。 2.绑定数据库表onChange函数: [图片] [图片] 这里的onChange输出e是这样的: [图片] type=init,获取了数据库表中该记录的所有内容,在这里将js中的chats进行赋值即可; 另外,当该记录内容变化时,type是update类型 3.wxml修改,wx-for将chats显示,以及一些判断和内容显示的设置: [图片] 到此,显示效果就有啦 [图片] 接下来,就是信息的添加了,下面将显示如何添加新信息到数据库 4.发送信息 先获取输入框内容: [图片] [图片] 发送函数: 增加一条信息,就是在records数组中加一条记录,所以在函数内部要对新纪录的属性进行一些赋值和判断等。 对showTime的处理: [图片] 消息空白处理: [图片] 对消息内的所有属性进行一个打包处理: [图片] 存储记录,并滑动页面: [图片] 最后,清空消息框内容 [图片] 发送一条消息,最终效果如图: [图片][图片] js源码const app = getApp() const db = wx.cloud.database() const _ = db.command const chatroomCollection = db.collection("chatroom") var util = require('../../utils/util.js'); Page({ data: { //这里的openid和match_openid应该是在上一级页面传进来的属性,这里由于只有聊天室所以暂时设置为一些固定值,用于测试 openid:'', match_openid:'', //这里的avatar是头像,具体传参方式自己设定,这里暂时设置为固定值,用于测试 useravatar:'', match_avatar:'', chats:[], textInputValue:'' }, onReady() { var that = this //查询openid和match_openid所标识的唯一聊天室 chatroomCollection.where({ _openid: _.or(_.eq(that.data.openid), _.eq(that.data.match_openid)), match_openid: _.or(_.eq(that.data.openid), _.eq(that.data.match_openid)) }) //绑定onChange,直观而言即表中该记录发生变动时,调用该函数 .watch({ onChange: this.onChange.bind(this), onError(err) { console.log(err) } }) }, //数据库表onchange绑定函数 onChange(e) { let that = this //type="init"的情况:初始化聊天窗口信息 if (e.type == "init") { that.initchats(e.docs[0].records) } //type="update"的情况:records中增加了一条记录 else { //在chats数组中增加该新消息 let i = that.data.chats.length const new_chats = [...that.data.chats] if (e.docs.length) new_chats.push(e.docs[0].records[i]) this.setData({ chats: new_chats }) } }, initchats(records) { this.setData({ chats: records }) //跳转到页面底部 this.goBottom() }, //获取输入文本 getContent(e) { this.data.textInputValue = e.detail.value }, sendMsg(){ let that = this //show代表了数据库表中的showTime属性,是否显示消息时间 var show = false //无记录时,true if (this.data.chats.length == 0) show = true //判断上下两条消息的时间差决定是否显示时间,这里设置了2分钟:120000毫秒,可自行修改 else { if (Date.now() - this.data.chats[this.data.chats.length - 1].sendTimeTS > 120000) show = true } const _ = db.command //消息空白处理 if (!that.data.textInputValue) { wx.showToast({ title: '不能发送空白信息', icon: 'none', }) return } //消息内容赋值 const doc = { openid: that.data.openid, msgText: "text", textContent: that.data.textInputValue, sendTime: util.formatTime(new Date()), sendTimeTS: Date.now(), showTime: show, } //添加数据库表中该记录的records数组,并跳转页面到底部 chatroomCollection.where({ _openid: _.or(_.eq(that.data.openid), _.eq(that.data.match_openid)), match_openid: _.or(_.eq(that.data.openid), _.eq(that.data.match_openid)) }) .update({ data: { records: _.push(doc) } }) .then(res => { that.goBottom() }) //消息设空 that.setData({ textInputValue: "" }) }, goBottom() { wx.createSelectorQuery().select('#page').boundingClientRect(function (rect) { if (rect) { // 使页面滚动到底部 wx.pageScrollTo({ scrollTop: rect.height + 4 }) } }).exec() }, }) 其中,util.js内容如下: const formatTime = date => { const year = date.getFullYear() const month = date.getMonth() + 1 const day = date.getDate() const hour = date.getHours() const minute = date.getMinutes() const second = date.getSeconds() return `${[year, month, day].map(formatNumber).join('/')} ${[hour, minute, second].map(formatNumber).join(':')}` } const formatNumber = n => { n = n.toString() return n[1] ? n : `0${n}` } module.exports = { formatTime } 一个简单的demo就完成了,大家有什么问题欢迎随时q我。 -----------完结撒花----------
2021-11-18 - 如何为老板节省几个亿之graphql介绍
写过接口和联调过接口的人想必都对接口的约定,文档维护和联调等问题经常头痛不已,因为我们的业务里有大量不同的应用和系统共同使用着许许多多的服务api,而随着业务的变化和发展,不同的应用对相同资源的不同使用方法最终会导致需要维护的服务api数量呈现爆炸式的增长,比如我试着跑了下我们自己业务里的接口数量,线上正在运行的就有超过1000多个接口,非常不利于维护。而另一方面,创建一个大而全的通用性接口又非常不利于移动端使用(流量损耗),而且后端数据的无意义聚合也对整个系统带来了很大的资源浪费。 总结一下,我们目前接口的开发遇到的主要痛点如下: 痛点 字段冗余 比如用户接口,刚开始时,返回的信息会比较少,例如只有 id,name,后来用户的信息增加了,就在用户接口中返回更多的信息,例如 id,name,age,city,addr,email,headimage,nick,但可能很多客户端只是想获取其中少部分信息,如name,headimage,却必须得到所有的用户信息,然后从中提取自己想要的,这个情况会增加网络传输量,并且也不利于客户端处理数据(为老板增加了很多不必要的开支😄。 参数类型校验 我们几乎每个接口都需要做基本的字段校验,比如某个参数是否必需,是不是数字,是不是数组,是不是布尔型,是不是字符串等等,而除了服务端要校验客户端传来的参数,客户端自己也需要去校验服务端返回的参数,比如客户端要的是数组,你有没有返回数组,试问有哪个前端想整天去写出[代码]var x = data?(data.obj?data.obj.name:null):null;[代码]这样的代码?我们业务自己线上出的很多问题也是因为客户端没有严格的校验服务端返回的数据是否符合预期造成的。比如下图是我们业务某天的js error错误日志上报: [图片] 文档与维护 这个可能是和接口打交道的人员感触最深的环节了,联调基本靠猜,维护历史遗留代码更是一种高深的猜一猜游戏。正常理论上来讲,写接口就要写对应的文档,接口一旦有变动,文档也一定要及时更新。但是,有不少团队因为种种原因,没有能及时维护文档甚至有不少连文档都没有,全部依赖口口相传和读源码。这个问题其实是困扰我个人在开发过程中最大的一个问题。 请求多个接口 我们经常会在某个需求中需要调用多个独立的API接口才能获取到足够的数据,例如客户端要去显示一篇文章的内容,同时也要显示评论、作者信息,那么就可能需要调用文章接口、评论接口、用户接口。这种方式非常的不灵活。另一个case是很多项目都会去拉一些不同的配置文件来决定展示什么,比如我们业务里的app,一打开就有10多个请求,非常不合理,不仅多个请求浪费了带宽,而且速度慢,客户端处理的请求也复杂。 如何解决? 对于上面的问题,我们来逐一攻破: **字段冗余?**那我可不可以客户端要什么字段,服务端就给什么字段的值? **参数类型校验?**那我可不可以定义一个返回数据格式与请求的数据格式的一个强类型的约束? **文档与维护?**这里的核心问题我觉的是因为我们写不写文档对于我们的代码没有约束力,所以我们写不写文档对代码的正常运行是没有任何影响的。所以我们能不能考虑和代码结合起来,我代码里怎么写的,文档就怎么生成的? **请求多个接口?**那我能不能客户端可以问服务端要1、2、3这些数据,服务端一次给我返回就行? graphql方案介绍 graphql的方案完美的解决了以上所有问题,连大名鼎鼎GitHub也抛弃了自己非常优秀的REST API接口,全面拥抱graphql了。 GraphQL是Facebook 在2012年开发的,2015年开源,2016年下半年Facebook宣布可以在生产环境使用,而其内部早就已经广泛应用了,用于替代 REST API。facebook的解决方案和简单:用一个“聪明”的节点来进行复杂的查询,将数据按照客户端的要求传回去,后端根据GraphQL机制提供一个具有强大功能的接口,用以满足前端数据的个性化需求,既保证了多样性,又控制了接口数量。 GraphQL并不是一门程序语言或者框架,它是描述你的请求数据的一种规范,是协议而非存储,GraphQL本身并不直接提供后端存储的能力,它不绑定任何的数据库或者存储引擎,它可以利用已有的代码和技术来进行数据源管理。 一个GraphQL查询是一个被发往服务端的字符串,该查询在服务端被解释和执行后返回JSON数据给客户端。 和Rest Api的对比 RESTful:服务端决定有哪些数据获取方式,客户端只能挑选使用,如果数据过于冗余也只能默默接收再对数据进行处理;而数据不能满足需求则需要请求更多的接口。 GraphQL:给客户端自主选择数据内容的能力,客户端完全自主决定获取信息的内容,服务端负责精确的返回目标数据。 为什么推荐用GraphQL方案 说人话就是:减少工作量 专业一点:提供一种更严格、可扩展、可维护的数据查询方式 优点 能为老板节省几个亿的流量(由前端定义需要哪些字段 再也不需要对接口的文档进行维护了(自动生成文档,代码里定义的结构就是文档 再也不用加班了(真正做到一个接口适用多个场景 再也不用改bug了(强类型,自动校验入参出参类型 新人再也不用培训了(所有的接口都在一颗数下,一目了然 再也不用前端去写假数据了(代码里定义好结构之后自动生成mock接口 再不用痛苦的联调了(代码里定义好结构之后,自动生成接口在线调试工具,直接在界面里写请求语句来调试返回,而且调试的时候各种自动补全 react/vue/express/koa无缝接入(relay方案/apollo方案 更容易写底层的工具去监控每个接口的请求统计信息(都在同一个端点的请求下 不限语言,除了官方提供的js实现,其他所有的语言都有社区的实现 生态是真的好啊,有各种方便易用的开发者工具 怎么用? [图片] 其实graphql的理念还算简单,总的来说,就如图中所示: 接口提供方定义好强类型的数据入参和返回的数据结构 客户端发送一个带有查询语句(graphql的查询协议)的请求,请求里定义了需要哪些数据。 服务端返回给客户端符合客户端预期的json字符串结果 github官方提供了github的graph api的在线调试工具,地址是https://developer.github.com/v4/explorer/,实际上这个工具是社区提供的,我们在写好graph服务的时候,也可以自动一键生成这样一个服务。你可以在github提供的这个调试工具里多试试查询语句。 在REST的API中,给哪些数据是服务端决定的,客户端只能从中挑选,如果A接口中的数据不够,再请求B接口,然后从他们返回的数据中挑出自己需要的,而在GraphQL 中,客户端直接对服务端说想要什么数据,服务端就负责精确的返回目标数据。 可以把GraphQL看成一棵树,GraphQL对外提供的其实就只有一个接口,所有的请求都通过这个接口去处理,相当于在graph服务内部做了针对不同请求的路由处理。 根节点下分为2大类,一类是Query类(主要是查询相关的,相当于get),一类是Mutation类(主要是非幂等操作,post,put,delete)。 下图是我写的一个微博类应用的demo,图是写完服务后用工具voyager生成的这个graph服务的数据结构图: [图片] 注:其中类型后面的感叹号表示这个属性必须提供。 GraphQL是一个强类型的协议,服务端在定义数据的时候必须指定每一个入参,出参的类型以及是否可选,这样的话,框架就可以自动帮我们去处理参数类型校验的问题了。GraphQL 的类型系统分为标量类型(Scalar Types,标量类型,也称为简单类型)和其他高级数据类型(复合类型),标量类型即可以表示最细粒度数据结构的数据类型,可以和 JavaScript的原始类型对应。 GraphQL 规范目前官方规定支持的标量类型有: Int : 整数,对应JavaScript的Number Float:浮点数,对应JavaScript的Number String:字符串,对应Javascript的String Boolean: 布尔值,对应JavaScript的Boolean ID:序列化后唯一的字符串,对应JavaScript的Symbol 高级数据类型包括:Object(对象)、Interface(接口,定义的时候继承这个接口的结构必须实现这个接口所有属性)、Union(联合类型,可以既是某个类型又是某个类型)、Enum(枚举类型)、Input Object(输入对象)、List(列表,对应js的数组)、Non-Null(不能为null) 这里不做详述,请参考官方指引 Schemas and Types。 查询示例 下面是对上面的graph服务的查询示例: [代码]// 获取单个用户信息的同时获取该位用户的文章列表,以及每篇文章对应的tag列表 { user(id: 1) { # 括号里的是参数,表示请求id为1的用户信息 id username:nickname # 这里可以给服务返回的字段设置别名,比如我们把服务返回的nickname设置为username gender avatar posts{ id source tagIds tags{ name } # tags也是一个数组,我们这里只需要数组里的tag里的name属性 } # posts的返回是一个数组,我们这里只需要定义数组里的项目对应的数据结构就可以 createdAt updatedAt } } //返回示例 { "data": { "user": { "id": "1c5c160c-b666-4728-b745-81c723e3e722", "username": "hello", "gender": "UNKNOW", "avatar": "http://qq.com", "posts": [ { "id": "ab9d26b9-23b8-4ca6-97e7-bb5f9091133e", "source": "黑队7首映归来!全程被卡黄发糖砸到懵逼!好不容易回过神了!被彩蛋砸了一顿玻璃渣!!!编剧我要给你寄刀片!!!!!!!! ", "tagIds": [ "a7475f18-4132-4155-be78-9d942de9fa89", "875d592a-a0f2-483f-b680-93c0db27b173" ], "tags": [ { "name": "任务1" }, { "name": "任务1" } ] }, { "id": "d96f7717-cd20-4ef5-b62e-01fbeb2e3b06", "source": "你好,世界", "tagIds": [ "14698f1e-2610-4d9c-baa0-8a94b3d8597a", "97557a0b-0bc5-48db-9338-3e864aa6b1c5" ], "tags": [ { "name": "任务1" }, { "name": "任务1" } ] } ], "createdAt": "2017-10-18T16:33:27.620Z", "updatedAt": "2017-10-18T16:33:27.620Z" } } } //下面是一个典型的创建文章示例 //需要指定为mutation类型,如果不写的话,默认是query类型 mutation { createPost(input:{source:"hello",sourceType:MARKDOWN,authorId:1,score:1}){ id,# 创建后返回id属性 createdAt # 创建后返回createdAt属性 } } //返回示例 { "data": { "createPost": { "id": "bcb1eafa-6440-4336-a4f0-5cafb334581f", "createdAt": "2017-10-18T16:35:48.297Z" } } } [代码] 除此之外,你还可以进行批量的请求,query类型的请求是并行的,mutation类的请求是顺序执行的。 服务端的数据结构定义 [代码]// 对应上面的GraphQL查询,GraphQL 服务需要建立以下自定义类型 type Query { # 单个用户 #号表示对这个字段的注释,感叹号表示必须传这个字段 user(id:ID!): User # User表示这个的返回类型的User } # 用户类 type User { # 用户uuid id: ID! # 昵称 nickname: String! # 性别 gender:Gender! # 头像地址 avatar:Url! # 注册时间 createdAt:DateTime! # 最后更新时间 updatedAt:DateTime! # 帖子列表 posts:[Post!]! } # 帖子 type Post { id: ID! # 帖子内容 source: String! # 作者ID authorId:ID! # 作者 author:User! # 帖子标签id数组 tagIds:[ID!]! # 帖子标签 tags:[Tag!]! # 创建时间 createdAt:DateTime! # 更新时间 updatedAt:DateTime! } # 标签 type Tag { # 标签uuid id: ID! # 标签名字 name:String! # 作者ID authorId:ID! # 作者 author:User! # 创建时间 createdAt:DateTime! # 最后更新时间 updatedAt:DateTime! } # 性别 enum Gender { # 男 MALE # 女 FEMALE # 未知 UNKNOW } [代码] GraphQL存在的问题 graphQl也不是没有缺点,主要有以下几个缺点: 改造成本 要使用GraphQL对数据源进行管理,相当于要对整个服务端进行一次换血。你需要考虑的不仅仅是需要针对现有数据源建立一套GraphQL的类型系统,同时需要改造服务端暴露数据的方式,这对业务久远的产品无疑是一场灾难,让人望而却步。 查询性能 GraphQL查询的每个字段如果都有自己的resolve方法,可能导致一次查询操作对数据库跑了大量了query,数据库里一趟select+join就能完成的事情在这里看来会产生大量的数据库查询操作,虽然网络层面的请求数被优化了,但是数据库查询可能会成为性能瓶颈。但是这个其实是可以优化,反正就算是用rest api,同时处理多个请求的时候,也总要运行那些数据库语句的。 一些有用的链接: facebook/graphql的github项目 — https://github.com/facebook/graphql GraphQl定义 — https://facebook.github.io/graphql/ GraphQ介绍网站,把graph讲的挺清楚的 http://graphql.org/ 谁在用 http://graphql.org/users/ 参考 http://imweb.io/topic/58499c299be501ba17b10a9e https://juejin.im/post/58fd6d121b69e600589ec740 https://segmentfault.com/a/1190000005766732 http://blog.kazaff.me/2016/01/01/GraphQL什么鬼/ https://neighborhood999.github.io/2017/01/12/Learning-GraphQL/
2019-03-20 - 模块化:Javascript模块化的演进
ES2015 在2015年6月正式发布,官方终于引入了对于模块的原生支持,如今 JS 的模块化开发非常的方便、自然,但这个新规范仅仅持续了3年。就在7年前,JS 的模块化还停留在运行时的支持;13年前,通过后端模版定义、注释定义模块依赖。对于经历过的人来说,历史的模块化方式还停留在脑海中,久久不能忘怀。 为什么需要模块化 模块,是为了完成某一功能所需的程序或者子程序。模块是系统中“职责单一”且“可替换”的部分。所谓的模块化就是指把系统代码分为一系列职责单一且可替换的模块。模块化开发是指如何开发新的模块和复用已有的模块来实现应用的功能。 那么,为什么需要模块化呢?主要是以下几个方面的原因: 传统的网页开发正在转变成 Web Apps 开发 代码复杂度在逐步增高,随着 Web 能力的增强,越来越多的业务逻辑和交互都放在 Web 层实现 开发者想要分离的JS文件/模块,便于后续代码的维护性 部署时希望把代码优化成几个 HTTP 请求 模块化的演进历程 直接定义依赖(1999) 最先尝试将模块化引入到 JS 中是在1999年,称作“直接定义依赖”。这种方式实现模块化简单粗暴 —— 通过全局方法定义、引用模块。Dojo 就是使用这种方式进行模块组织的,使用[代码]dojo.provide[代码] 定义一个模块,[代码]dojo.require[代码] 来调用在其它地方定义的模块。 我们在dojo 1.6中修改下示例: [代码]// greeting.js 文件 dojo.provide("app.greeting"); app.greeting.helloInLang = { en: 'Hello world!', es: '¡Hola mundo!', ru: 'Привет мир!' }; app.greeting.sayHello = function (lang) { return app.greeting.helloInLang[lang]; }; // hello.js 文件 dojo.provide("app.hello"); dojo.require('app.greeting'); app.hello = function(x) { document.write(app.greeting.sayHello('es')); }; [代码] 直接定义依赖的方式和commonjs非常类似,区别是它可以在任何文件中定义模块,模块不和文件进行关联。而在commonjs中,每一个文件即是一个模块。 namespace模式(2002) 最早,我们这么写代码: [代码]function foo() { } function bar() { } [代码] 很多变量和函数会直接在 global 全局作用域下面声明,很容易产生命名冲突。于是,命名空间模式被提出。比如: [代码] var app = {}; app.foo = function() { } app.bar = function() { } app.foo(); [代码] 匿名闭包 IIFE 模式(2003) 尽管 namespace 模式一定程度上解决了 global 命名空间上的变量污染问题,但是它没办法解决代码和数据隔离的问题。 解决这个问题的先驱是:匿名闭包 IIFE 模式。它最核心的思想是对数据和代码进行封装,并通过提供外部方法来对它们进行访问。一个基本的例子如下: [代码] var greeting = (function () { var module = {}; var helloInLang = { en: 'Hello world!', es: '¡Hola mundo!', ru: 'Привет мир!' }; module.getHello = function (lang) { return helloInLang[lang]; }; module.writeHello = function (lang) { document.write(module.getHello(lang)) }; return module; }()); [代码] 模板依赖定义(2006) 模板依赖定义是接下来的一个用于解决模块化问题的方案,它需要配合后端的模板语法一起使用,通过后端语法聚合 js 文件,从而实现依赖加载。 它最开始被应用到 Prototype 1.4 库中。05年的时候,Sam Stephenson 开始开发 Prototype 库,Prototype 当时是作为 Ruby on rails 的一个组成部分。由于 Sam 平时使用 Ruby 很多,这也难怪他会选择使用 Ruby 里的 erb 模板作为 JS 模块的依赖管理了。 模板依赖定义的具体做法是:对于某个 JS 文件而言,如果它依赖其它 JS 文件,则在这个文件的头部通过特殊的标签语法去指定以来。这些标签语法可以通过后端模板(erb, jinjia, smarty)去解析,和特殊的构建工具如 borshik 去识别。只得一提的是,这种模式只能作用于预编译阶段。 下面是一个使用了 borshik 的例子: [代码] // app.tmp.js 文件 /*borschik:include:../lib/main.js*/ /*borschik:include:../lib/helloInLang.js*/ /*borschik:include:../lib/writeHello.js*/ // main.js 文件 var app = {}; // helloInLang.js 文件 app.helloInLang = { en: 'Hello world!', es: '¡Hola mundo!', ru: 'Привет мир!' }; // writeHello.js 文件 app.writeHello = function (lang) { document.write(app.helloInLang[lang]); }; [代码] 注释定义依赖(2006) 注释定义依赖和1999年的直接定义依赖的做法非常类似,不同的是,注释定义依赖是以文件为单位定义模块了。模块之间的依赖关系通过注释语法进行定义。 一个应用如果想用这种方式,可以通过预编译的方式(Mootools),或者在运行期动态的解析下载下来的代码(LazyJS)。 [代码]// helloInLang.js 文件 var helloInLang = { en: 'Hello world!', es: '¡Hola mundo!', ru: 'Привет мир!' }; // sayHello.js 文件 /*! lazy require scripts/app/helloInLang.js */ function sayHello(lang) { return helloInLang[lang]; } // hello.js 文件 /*! lazy require scripts/app/sayHello.js */ document.write(sayHello('en')); [代码] 依赖注入(2009) 2004年,Martin Fowler为了描述Java里的组件之间的通信问题,提出了依赖注入概念。 五年之后,前Sun和Adobe员工Miško Hevery(JAVA程序员)开始为他的创业公司设计一个 JS 框架,这个框架主要使用依赖注入的思想却解决组件之间的通信问题。后来的结局大家都知道,他的项目被 Google 收购,Angular 成为最流行的 JS 框架之一。 [代码]// greeting.js 文件 angular.module('greeter', []) .value('greeting', { helloInLang: { en: 'Hello world!', es: '¡Hola mundo!', ru: 'Привет мир!' }, sayHello: function(lang) { return this.helloInLang[lang]; } }); // app.js 文件 angular.module('app', ['greeter']) .controller('GreetingController', ['$scope', 'greeting', function($scope, greeting) { $scope.phrase = greeting.sayHello('en'); }]); [代码] CommonJS模式 (2009) 09年 CommonJS(或者称作 CJS)规范推出,并且最后在 Node.js 中被实现。 [代码]// greeting.js 文件 var helloInLang = { en: 'Hello world!', es: '¡Hola mundo!', ru: 'Привет мир!' }; var sayHello = function (lang) { return helloInLang[lang]; } module.exports.sayHello = sayHello; // hello.js 文件 var sayHello = require('./lib/greeting').sayHello; var phrase = sayHello('en'); console.log(phrase); [代码] 这里我们发现,为了实现CommonJS规范,我们需要两个新的入口 – [代码]require[代码] 和 [代码]module[代码],它们提供加载模块和对外界暴露接口的能力。 值得注意的是,无论是 require 还是 module,它们都是语言的关键字。在 Node.js 中,由于是辅助功能,我们可以使用它。在将Node.js中的模块发送给 V8 之前,会通过下面的函数进行包裹: [代码](function (exports, require, module, __filename, __dirname) { // ... // 模块的代码在这里 // ... }); [代码] 就目前来看,CommonJS规范是最常见的模块格式规范。你不仅可以在 Node.js 中使用它,而且可以借助 Browserfiy 或 Webpack 在浏览器中使用。 AMD 模式(2009) CommonJS的工作在全力的推进,同时,关于模块的异步加载的讨论也越来越多。主要是希望解决 Web 应用的动态加载依赖,相比于 CommonJS,体积更小。 [代码]// lib/greeting.js 文件 define(function() { var helloInLang = { en: 'Hello world!', es: '¡Hola mundo!', ru: 'Привет мир!' }; return { sayHello: function (lang) { return helloInLang[lang]; } }; }); // hello.js 文件 define(['./lib/greeting'], function(greeting) { var phrase = greeting.sayHello('en'); document.write(phrase); }); [代码] 虽然 AMD 的模式很适合浏览器端的开发,但是随着 npm 包管理的机制越来越流行,这种方式可能会逐步的被淘汰掉。 ES2015 Modules(2015) ES2015 modules(简称 ES modules)就是我们目前所使用的模块化方案,它已经在Node.js 9里原生支持,可以通过启动加上flag [代码]--experimental-modules[代码]使用,不需要依赖 babel 等工具。目前还没有被浏览器实现,前端的项目可以使用 babel 或 typescript 提前体验。 [代码]// lib/greeting.js 文件 const helloInLang = { en: 'Hello world!', es: '¡Hola mundo!', ru: 'Привет мир!' }; export const greeting = { sayHello: function (lang) { return helloInLang[lang]; } }; // hello.js 文件 import { greeting } from "./lib/greeting"; const phrase = greeting.sayHello("en"); document.write(phrase); [代码] 总结 JS 模块化的演进历程让人感慨,感谢 TC 39 对 ES modules 的支持,让 JS 的模块化进程终于可以告一段落了,希望几年后所有的主流浏览器可以原生支持 ES modules。 JS 模块化的演进另一方面也说明了 Web 的能力在不断增强,Web 应用日趋复杂。相信未来 JS 的能力进一步提升,我们的开发效率也会更加高效。
2019-03-21 - Vue 移动端框架
1. vonic vonic 一个基于 vue.js 和 ionic 样式的 UI 框架,用于快速构建移动端单页应用,很简约。 中文文档 | github地址 | 在线预览 [图片] 2. vux vux 基于WeUI和Vue(2.x)开发的移动端UI组件库。基于webpack+vue-loader+vux可以快速开发移动端页面,配合vux-loader方便你在WeUI的基础上定制需要的样式。小编在开发微信公众号的时候使用过,欢迎来评论区吐槽。 中文文档 | github地址 | 在线预览 [图片] 3. Mint UI Mint UI 由饿了么前端团队推出的 Mint UI 是一个基于 Vue.js 的移动端组件库。 中文文档 | github地址 | 在线预览 [图片] 4. Muse-UI 基于 Vue 2.0 和 Material Design 的 UI 组件库 中文文档 | github地址 [图片] 5. Vant 是有赞前端团队基于有赞统一的规范实现的 Vue 组件库,提供了一整套 UI 基础组件和业务组件。 中文文档 | github地址 | 在线预览 [图片] 6. Cube-UI 滴滴 WebApp 团队 实现的 基于 Vue.js 实现的精致移动端组件库 中文文档 | github地址 | 在线预览 [图片] 7. vue-ydui Vue-ydui 是 YDUI Touch 的一个Vue2.x实现版本,专为移动端打造,在追求完美视觉体验的同时也保证了其性能高效。目前由个人维护。 中文文档 | github地址 | 在线预览 [图片] 8. Mand-Mobile 面向金融场景的Vue移动端UI组件库,丰富、灵活、实用,快速搭建优质的金融类产品。 中文文档 | github地址 | 在线预览 [图片] 9. v-charts 在使用 echarts 生成图表时,经常需要做繁琐的数据类型转化、修改复杂的配置项,v-charts 的出现正是为了解决这个痛点。基于 Vue2.0 和 echarts 封装的 v-charts 图表组件,只需要统一提供一种对前后端都友好的数据格式设置简单的配置项,便可轻松生成常见的图表。特别感谢 @书简_yu 的贡献。 中文文档 | github地址 | 在线预览 [图片] 10. Vue Carbon Vue Carbon 是基于 vue 开发的material design ui 库。 中文文档 | github地址 | 在线预览 [图片] 11. Quasar Quasar(发音为/kweɪ.zɑɹ/)是MIT许可的开源框架(基于Vue),允许开发人员编写一次代码,然后使用相同的代码库同时部署为网站、PWA、Mobile App和Electron App。使用最先进的CLI设计应用程序,并提供精心编写,速度非常快的Quasar Web组件。 中文文档 | github地址 [图片] 12. Vue-recyclerview 使用vue-recyclerview掌握大型列表。 github地址 | 在线预览 [图片] 13. Vue.js modal 易于使用,高度可定制,移动友好的Vue.js 2.0+ modal。 在线文档 | github地址 | 在线预览 [图片] 14. Vue Baidu Map Vue Baidu Map是基于Vue 2.x的百度地图组件。 中文文档 | github地址 | 在线预览 [图片] 15. Onsen UI 将Vue.js的强大功能和简单性带入混合和渐进式Web应用程序。 在线文档 | github地址 | 在线预览 [图片] 相关文章 Vue PC端框架 别走,还有后续呐······ 如果小伙伴们有比较好的移动端框架,欢迎在评论区留言砸场,我会持续更新在简书上,谢谢你的贡献。
2019-03-26 - Lottie-前端实现AE动效
项目背景 在海外项目中,为了优化用户体验加入了几处微交互动画,实现方式是设计输出合成的雪碧图,前端通过序列帧实现动画效果: [图片] 序列帧: [图片] 动画效果: [图片] 序列帧: [图片] 帧动画的缺点和局限性比较明显,合成的雪碧图文件大,且在不同屏幕分辨率下可能会失真。经调研发现,Lottie是个简单、高效且性能高的动画方案。 Lottie是可应用于Android, iOS, Web和Windows的库,通过Bodymovin解析AE动画,并导出可在移动端和web端渲染动画的json文件。换言之,设计师用AE把动画效果做出来,再用Bodymovin导出相应地json文件给到前端,前端使用Lottie库就可以实现动画效果。 [图片] Bodymovin插件的安装与使用 关闭AE 下载并安装ZXP installer https://aescripts.com/learn/zxp-installer/ 下载最新版bodymovin插件 https://github.com/airbnb/lottie-web/blob/master/build/extension/bodymovin.zxp 把下载好的bodymovin.zxp拖到ZXP installer [图片] 打开AE,在菜单首选项->常规中勾选☑️允许脚本写入文件和访问网络(否则输出JSON文件时会失败) [图片] 在AE中制作动画,打开菜单窗口->拓展->Bodymovin,勾选要输出的动画,并设置输出文件目录,点击render [图片] 打开输出目录会看到生成的JSON文件,若动画里导入了外部图片,则会在images中存放JSON中引用的图片 前端使用lottie 静态URL https://cdnjs.com/libraries/lottie-web NPM [代码]npm install lottie-web [代码] 调用loadAnimation [代码]lottie.loadAnimation({ container: element, // 容器节点 renderer: 'svg', loop: true, autoplay: true, path: 'data.json' // JSON文件路径 }); [代码] vue-lottie 也可以在vue中使用lottie [代码] import lottie from '../lib/lottie'; import * as favAnmData from '../../raw/fav.json'; export default { props: { options: { type: Object, required: true }, height: Number, width: Number, }, data () { return { style: { width: this.width ? `${this.width}px` : '100%', height: this.height ? `${this.height}px` : '100%', overflow: 'hidden', margin: '0 auto' } } }, mounted () { this.anim = lottie.loadAnimation({ container: this.$refs.lavContainer, renderer: 'svg', loop: this.options.loop !== false, autoplay: this.options.autoplay !== false, animationData: favAnmData, assetsPath: this.options.assetsPath, rendererSettings: this.options.rendererSettings } ); this.$emit('animCreated', this.anim) } } [代码] loadAnimation参数 参数名 描述 container 用于渲染动画的HTML元素,需确保在调用loadAnimation时该元素已存在 renderer 渲染器,可选值为’svg’(默认值)/‘canvas’/‘html’。svg支持的功能最多,但html的性能更好且支持3d图层。各选项值支持的功能列表在此 loop 默认值为true。可传递需要循环的特定次数 autoplay 自动播放 path JSON文件路径 animationData JSON数据,与path互斥 name 传递该参数后,可在之后通过lottie命令引用该动画实例 rendererSettings 可传递给renderer实例的特定设置,具体可看 Lottie动画监听 Lottie提供了用于监听动画执行情况的事件: complete loopComplete enterFrame segmentStart config_ready(初始配置完成) data_ready(所有动画数据加载完成) DOMLoaded(元素已添加到DOM节点) destroy 可使用addEventListener监听事件 [代码]// 动画播放完成触发 anm.addEventListener('complete', anmLoaded); // 当前循环播放完成触发 anm.addEventListener('loopComplete', anmComplete); // 播放一帧动画的时候触发 anm.addEventListener('enterFrame', enterFrame); [代码] 控制动画播放速度和进度 可使用anm.pause和anm.play暂停和播放动画,调用anm.stop则会停止动画播放并回到动画第一帧的画面。 使用anm.setSpeed(speed)可调节动画速度,而anm.goToAndStop(value, isFrame)和anm.goToAndPlay可控制播放特定帧数,也可结合anm.totalFrames控制进度百分比,比如可传anm.totalFrames - 1跳到最后一帧。 [代码]anm.goToAndStop(anm.totalFrames - 1, 1); [代码] 这样的好处是可以把相关联的JSON文件合并,通过anm.goToAndPlay控制动画状态的切换,如下图例中一个JSON文件包含了2个动画状态的数据: [图片] 图片资源 JSON文件里assets设置了对图片的引用: [图片] 若想统一修改静态资源路径或者设置成绝对路径,可在调用loadAnimation时传入assetsPath参数: [代码]lottie.loadAnimation({ container: element, renderer: 'svg', path: 'data.json', assetsPath: 'URL' // 静态资源绝对路径 }); [代码] 功能支持列表 即使用bodymovin成功输出了JSON文件(没有报错),也会出现动效不如预期的情况,比如这是在AE中构建的形象图: [图片] 但在页面中渲染效果是这样的: [图片] 这是因为使用了不支持的Merge Paths功能 [图片] 因此对设计师而言,创建Lottie动画和往常制作AE动画有所不同,此文档记录了Bodymovin支持输出的AE功能列表,动画制作前需跟设计师沟通好,根据动画加载平台来确认可使用的AE功能。 除此之外,尽量遵循官方文档里对设计过程的指导和建议: 动画简单化。创建动画时需时刻记着保持JSON文件的精简,比如尽可能地绑定父子关系,在相似的图层上复制相同的关键帧会增加额外的代码,尽量不使用占用空间最多的路径关键帧动画。诸如自动跟踪描绘、颤动之类的技术会使得JSON文件变得非常大且耗性能。 建立形状图层。将AI、EPS、SVG和PDF等资源转换成形状图层否则无法在Lottie中正常使用,转换好后注意删除该资源以防被导出到JSON文件。 设置尺寸。在AE中可设置合成尺寸为任意大小,但需确保导出时合成尺寸和资源尺寸大小保持一致。 不使用表达式和特效。Lottie暂不支持。 注意遮罩尺寸。若使用alpha遮罩,遮照的大小会对性能产生很大的影响。尽可能地把遮罩尺寸维持到最小。 动画调试。若输出动画破损,通过每次导出特定图层来调试出哪些图层出了问题。然后在github中附上该图层文件提交问题,选择用其他方式重构该图层。 不使用混合模式和亮度蒙版。 不添加图层样式。 全屏动画。设置比想要支持的最宽屏幕更宽的导出尺寸。 设置空白对象。若使用空白对象,需确保勾选可见并设置透明度为0%否则不会被导出到JSON文件。 预览效果 由于以上所说的功能支持问题会导致输出动画效果不确定性,设计师和前端之间有个动画效果联调的过程,为了提高联调效率,设计师可先进行初步的效果预览,再把文件交付给前端。 方法1:输出预览HTML文件 渲染前设置所要渲染的文件 [图片] 勾选☑️Demo选项 [图片] 在输出的文件目录中就可找到可预览的demo.html文件 方法2:LottieFiles分享平台 把生成的JSON文件传到LottieFiles平台,可播放、暂停生成文件的动画效果,可设置图层颜色、动画速度,也可以下载lottie preview客户端在iOS或Android机子上预览。 [图片] LottieFiles平台还提供了很多线上公开的Lottie动画效果,可直接下载JSON文件使用 [图片] 交互hack Lottie的不足之处是没有对应的API操纵动画层,若想做更细化的动画处理,只能直接操作节点来实现。比如当播放完左图动画进入惊讶状态后,若想实现右图随鼠标移动而控制动画层的简单效果: [图片][图片] 开启调试面板可以看到,lottie-web通过使用<g>标签的transform属性来控制动画: [图片] 当元素已添加到DOM节点,找到想要控制的<g>标签,提取其transform属性的矩阵值,并使用rematrix解析矩阵值。 [代码]onIntroDone() { const Gs = this.refs.svg.querySelectorAll('svg > g > g > g'); Gs.forEach((node, i) => { // 过滤需要修改的节点 ... // 获取transform属性值 const styleArr = node.getAttribute('transform').split(','); styleArr[0] = styleArr[0].replace('matrix(', ''); styleArr[5] = styleArr[5].replace(')', ''); const style = `matrix(${styleArr[0]}, ${styleArr[1]}, ${styleArr[2]}, ${styleArr[3]}, ${styleArr[4]}, ${styleArr[5]})`; // 使用Rematrix解析 const transform = Rematrix.parse(style); this.matrices.push({ node, transform, prevTransform: transform }); } } [代码] 监听鼠标移动,设置新的transform属性值。 [代码]onMouseMove = (e) => { this.mouseCoords.x = e.clientX || e.pageX; this.mouseCoords.y = e.clientY || e.pageY; let x = this.mouseCoords.x - (this.props.browser.width / 2); let y = this.mouseCoords.y - (this.props.browser.height / 2); const diffX = (this.mouseCoords.prevX - x); const diffY = (this.mouseCoords.prevY - y); this.mouseCoords.prevX = x; this.mouseCoords.prevY = y; this.matrices.forEach((matrix, i) => { let translate = Rematrix.translate(diffX, diffY); const product = [matrix.prevTransform, translate].reduce(Rematrix.multiply); const css = `matrix(${product[0]}, ${product[1]}, ${product[4]}, ${product[5]}, ${product[12]}, ${product[13]})`; matrix.prevTransform = product; matrix.node.setAttribute('transform', css); }) } [代码] 进一步优化 看到一个方法,在AE中将图层命名为[代码]#id[代码]格式,生成的SVG相应的图层id会被设置为id,命名为[代码].class[代码]格式,相应的图层class会被设置为class [图片] 试了下的确可以,如下图,因此可通过这个方法快速找到需要操作的动画层,进一步简化代码: [图片] 小结 Lottie的缺点在于若在AE动画制作的过程不注意规范,会导致数据文件大、耗内存和性能的问题;Lottie-web的官方文档不够详尽,例如assetsPath参数是在看源码的时候发现的;开放的API不够齐全,无法很灵活地控制动画层。 而优点也很明显,Lottie能帮助提高开发效率,精简代码,易于调试和维护;资源文件小,输出动画效果保真;跨平台——Android, iOS, Web和Windows通用。 总的来说,Lottie的引用可以替代传统的GIF和帧动画,灵活利用好提供的属性和方法可以控制动画的播放,但需注意规范设计和开发的流程,才可以更高效地完成动画的制作与调试。
2019-03-25 - Omi 6.0 - Store 的设计哲学
写在前面 Store 是 Omi 内置的中心化数据仓库,他解决了下面两个问题: 组件树数据共享 数据变更按需更新依赖的组件 [图片] 而这一切都是在运行时搞定。 一段代码完全上手 Store [代码]import { render, WeElement, define } from 'omi' define('my-counter', class extends WeElement { static use = [ { count: 'count' } ] add = () => this.store.add() sub = () => this.store.sub() addIfOdd = () => { if (this.use.count % 2 !== 0) { this.store.add() } } addAsync = () => { setTimeout(() => this.store.add(), 1000) } render() { return ( <p> Clicked: {this.use.count} times {' '} <button onClick={this.add}>+</button> {' '} <button onClick={this.sub}>-</button> {' '} <button onClick={this.addIfOdd}> Add if odd </button> {' '} <button onClick={this.addAsync}> Add async </button> </p> ) } }) render(<my-counter />, 'body', { data: { count: 0 }, sub() { this.data.count-- }, add() { this.data.count++ }, }) [代码] 这是一个简单的例子,说明了 store 体系的基本用法: 通过 [代码]static use[代码] 声明依赖的 path [代码]store[代码] 通过 render 的第三个参数从根节点注入到所有组件。 调用组件的方法或者直接改变组件的 data 进行视图更新 这里在书写过程中会出现两种方式,一种是将所有数据和逻辑放在 store 里,一种是将部分共享数据放在 store 里,这里没有强制要求使用哪种方式,omi 这两种能力都有,开发者偏爱哪种方式就使用哪种方式。 复杂的例子 Store 里的 data: [代码]{ count: 0, arr: ['china', 'tencent'], motto: 'I love omi.', userInfo: { firstName: 'dnt', lastName: 'zhang', age: 18 } } [代码] Static use: [代码]static use = [ 'count', //直接字符串,可通过 this.use[0] 访问 'arr[0]', //也支持 path,可通过 this.use[1] 访问 //支持 json { //alias,可通过 this.use.reverseMotto 访问 reverseMotto: [ 'motto', //path target => target.split('').reverse().join('') //computed ] }, { name: 'arr[1]' }, //{ alias: path },可通过 this.use.name 访问 { //alias,可通过 this.use.fullName 访问 fullName: [ ['userInfo.firstName', 'userInfo.lastName'], //path array (firstName, lastName) => firstName + lastName //computed ] }, ] [代码] 下面看看 JSX 中使用: [代码]... ... render() { return ( <div> <button onClick={this.sub}>-</button> <span>{this.use[0]}</span> <button onClick={this.add}>+</button> <div> <span>{this.use[1]}</span> <button onClick={this.rename}>rename</button> </div> <div>{this.use.reverseMotto}</div><button onClick={this.changeMotto}>change motto</button> <div>{this.use.name}</div> <div>{this.use[3]}</div> <div> {this.use.fullName} <button onClick={this.changeFirstName}>change first name</button> </div> </div> ) } ... ... [代码] 如果不带有 alias ,你也可以直接通过 [代码]this.store.data.xxx[代码] 访问。 Path 命中规则 当 [代码]store.data[代码] 发生变化,依赖变更数据的组件会进行更新,举例说明 Path 命中规则: Proxy Path(由数据更改产生) static use 中的 path 是否更新 abc abc 更新 abc[1] abc 更新 abc.a abc 更新 abc abc.a 不更新 abc abc[1] 不更新 abc abc[1].c 不更新 abc.b abc.b 更新 以上只要命中一个条件就可以进行更新! 总结: 只要注入组件的 path 等于 use 里声明 或者在 use 里声明的其中 path 子节点下就会进行更新! 解构赋值 [代码]import { define, WeElement } from 'omi' import '../my-list' define('my-sidebar', class extends WeElement { static css = require('./_index.css') static use = [ 'menus', 'sideBarShow', 'lan' ] render() { const [menus, sideBarShow, lan] = this.use return ( <div class={`list${sideBarShow ? ' show' : ''}`}> {menus[lan].map((menu, index) => ( <my-list menu={menu} index={index} /> ))} </div> ) } }) [代码] 这里举了个例子使用 ES2015+ 语法 [代码]const [xx, xxx] = xxxx[代码] 的语法快速赋值。上面是从 omi docs 的源码里截取的部分。感兴趣的可以看看源码。omi 官网已经使用 omi 6.0 重写了。 设计哲学 回顾 Omi 从 1.0 到 6.0: Omi 1.0 运行时动态模板引擎 Omi 2.0 拥抱虚拟 DOM 和运行时 scoped style Omi 3.0 提供 native 模块调用 bridge Omi 4.0 拥抱 Web Components Omi 5.0 纠正社区对 MVVM 误解 Omi 6.0 拥抱多端统一,迎来全新 path updating 的 store 体系 1.0 使用的动态模板引擎,是图灵完备的,可以表达一切你想表达的结构。由于是运行时,没法转虚拟 DOM,一定要转也可以,开销大,所以缺点很明显,视图更新开销大,依赖真实 DOM 之间的的 diff,另外一个缺点就是动态模板引擎(指令、模板语法都可以动态拼接)需要在脑海里二次转换,书写起来不够直观、智能提示也没有,不如 JSX 直接干净和智能。而为什么要这么设计,从整个发展历程来看离不开三个字: 运行时。 Omi 的设计 1.0 败也败在运行时,成也成在运行时。从 2.0 开始,除了 JSX 的部分(当然可以直接 hyperscript),其余全部 运行时 搞定: 运行时的 scoped style 运行时的 path updating 局部刷新 而到了 9210 年,JSX 也出现了运行时的替代方案:htm 。 为何如此偏爱运行时?而不交给编译器去做?这个仁者见仁,智者见智,而且有个权衡在里面。 当运行时的开销对于用户体验可以忽略不计,那么就选择运行时去做 运行时的好处非常明显,不需要任何构建工具、编译工具,就可以在浏览器、node、javascript core 或者任何 javascript 环境直接运行。凭什么让我学那么多构建工具、凭什么和一堆工具耦合在一起,我就是纯粹的 js,想在哪里跑都可以轻松复制粘贴或者直接 import/require 过去,而不强制带上任何工具。当然这里不是反对编译工具对前端带来的价值,omi-cli、omip、omi-mp 都大量使用了编译工具,只是没有编译工具,omi 也能运行良好,简单移植,比如 es module,比如 deno,直接 import 直接使用。 开始使用 omijs.org https://github.com/Tencent/omi
2019-03-25 - 拥抱更底层技术——从CSS变量到Houdini
0. 前言 平时写CSS,感觉有很多多余的代码或者不好实现的方法,于是有了预处理器的解决方案,主旨是write less &do more。其实原生css中,用上css变量也不差,加上bem命名规则只要嵌套不深也能和less、sass的嵌套媲美。在一些动画或者炫酷的特效中,不用js的话可能是用了css动画、svg的animation、过渡,复杂动画实现用了js的话可能用了canvas、直接修改style属性。用js的,然后有没有想过一个问题:“要是canvas那套放在dom上就爽了”。因为复杂的动画频繁操作了dom,违背了倒背如流的“性能优化之一:尽量少操作dom”的规矩,嘴上说着不要,手倒是很诚实地[代码]ele.style.prop = <newProp>[代码],可是要实现效果这又是无可奈何或者大大减小工作量的方法。 我们都知道,浏览器渲染的流程:解析html和css(parse),样式计算(style calculate),布局(layout),绘制(paint),合并(composite),修改了样式,改的环节越深代价越大。js改变样式,首先是操作dom,整个渲染流程马上重新走,可能走到样式计算到合并环节之间,代价大,性能差。然后痛点就来了,浏览器有没有能直接操作前面这些环节的方法呢而不是依靠js?有没有方法不用js操作dom改变style或者切换class来改变样式呢? 于是就有CSS Houdini了,它是W3C和那几个顶级公司的工程师组成的小组,让开发者可以通过新api操作CSS引擎,带来更多的自由度,让整个渲染流程都可以被开发者控制。上面的问题,不用js就可以实现曾经需要js的效果,而且只在渲染过程中,就已经按照开发者的代码渲染出结果,而不是渲染完成了再重新用js强行走一遍流程。 关于houdini最近动态可点击这里 上次CSS大会知道了有Houdini的存在,那时候只有cssom,layout和paint api。前几天突然发现,Animation api也有了,不得不说,以后很可能是Houdini遍地开花的时代,现在得进一步了解一下了。一句话:这是css in js到js in css的转变 1. CSS变量 如果你用less、sass只为了人家有变量和嵌套,那用原生css也是差不多的,因为原生css也有变量: 比如定义一个全局变量–color(css变量双横线开头) [代码]:root { --color: #f00; } [代码] 使用的时候只要var一下 [代码].f{ color: var(--color); } [代码] 我们的html: [代码]<div class="f">123</div> [代码] 于是,红色的123就出来了。 css变量还和js变量一样,有作用域的: [代码]:root { --color: #f00; } .f { --color: #aaa } .g{ color: var(--color); } .ft { color: var(--color); } [代码] html: [代码] <div className="f"> <div className="ft">123</div> </div> <div className=""> <div className="g">123</div> </div> [代码] 于是,是什么效果你应该也很容易就猜出来了: [图片] css能搞变量的话,我们就可以做到修改一处牵动多处的变动。比如我们做一个像准星一样的四个方向用准线锁定鼠标位置的效果: [图片] 用css变量的话,比传统一个个元素设置style优雅多了: [代码]<div id="shadow"> <div class="x"></div> <div class="y"></div> <div class="x_"></div> <div class="y_"></div> </div> [代码] [代码] :root{ --x: 0px; --y: 0px; } body{ margin: 0 } #shadow{ width: 50%; height: 600px; border: #000 1px solid; position: relative; margin: 0; } .x, .y, .x_, .y_ { position: absolute; border: #f00 2px solid; } .x { top: 0; left: var(--x); height: 20px; width: 0; } .y { top: var(--y); left: 0; height: 0; width: 20px; } .x_ { top: 600px; left: var(--x); height: 20px; width: 0; } .y_ { top: var(--y); left: 100%; height: 0; width: 20px; } [代码] [代码]const style = document.documentElement.style shadow.addEventListener('mousemove', e => { style.setProperty(`--x`, e.clientX + 'px') style.setProperty(`--y`, e.clientY + 'px') }) [代码] 那么,对于github的404页面这种内容和鼠标位置有关的页面,思路是不是一下子就出来了 2. CSS type OM 都有DOM了,那CSSOM也理所当然存在。我们平时改变css的时候,通常是直接修改style或者切换类,实际上就是操作DOM来间接操作CSSOM,而type om是一种把css的属性和值存在attributeStyleMap对象中,我们只要直接操作这个对象就可以做到之前的js改变css的操作。另外一个很重要的点,attributeStyleMap存的是css的数值而不是字符串,而且支持各种算数以及单位换算,比起操作字符串,性能明显更优。 接下来,基本脱离不了window下的CSS这个属性。在使用的时候,首先,我们可以采取渐进式的做法: [代码]if('CSS' in window){...}[代码] 2.1 单位 [代码]CSS.px(1); // 1px 返回的结果是:CSSUnitValue {value: 1, unit: "px"} CSS.number(0); // 0 比如top:0,也经常用到 CSS.rem(2); //2rem new CSSUnitValue(2, 'percent'); // 还可以用构造函数,这里的结果就是2% // 其他单位同理 [代码] 2.2 数学运算 自己在控制台输入CSSMath,可以看见的提示,就是数学运算 [代码]new CSSMathSum(CSS.rem(10), CSS.px(-1)) // calc(10rem - 1px),要用new不然报错 new CSSMathMax(CSS.px(1),CSS.px(2)) // 顾名思义,就是较大值,单位不同也可以进行比较 [代码] 2.3 怎么用 既然是新的东西,那就有它的使用规则。 获取值[代码]element.attributeStyleMap.get(attributeName)[代码],返回一个CSSUnitValue对象 设置值[代码]element.attributeStyleMap.set(attributeName, newValue)[代码],设置值,传入的值可以是css值字符串或者是CSSUnitValue对象 当然,第一次get是返回null的,因为你都没有set过。“那我还是要用一下getComputedStyle再set咯,这还不是和之前的差不多吗?” 实际上,有一个类似的方法:[代码]element.computedStyleMap[代码],返回的是CSSUnitValue对象,这就ok了。我们拿前面的第一部分CSS变量的代码测试一波 [代码]document.querySelector('.x').computedStyleMap().get('height') // CSSUnitValue {value: 20, unit: "px"} document.querySelector('.x').computedStyleMap().set('height', CSS.px(0)) // 不见了 [代码] 3. paint API paint、animation、layout API都是以worker的形式工作,具体有几个步骤: 建立一个worker.js,比如我们想用paint API,先在这个js文件注册这个模块registerPaint(‘mypaint’, class),这个class是一个类下面具体讲到 在html引入CSS.paintWorklet.addModule(‘worker.js’) 在css中使用,background: paint(mypaint) 主要的逻辑,全都写在传入registerPaint的class里面。paint API很像canvas那套,实际上可以当作自己画一个img。既然是img,那么在所有的能用到图片url的地方都适合用paint API,比如我们来自己画一个有点炫酷的背景(满屏随机颜色方块)。有空的话可以想一下js怎么做,再对比一下paint API的方案。 [图片] [代码]// worker.js class RandomColorPainter { // 可以获取的css属性,先写在这里 // 我这里定义宽高和间隔,从css获取 static get inputProperties() { return ['--w', '--h', '--spacing']; } /** * 绘制函数paint,最主要部分 * @param {PaintRenderingContext2D} ctx 类似canvas的ctx * @param {PaintSize} PaintSize 绘制范围大小(px) { width, height } * @param {StylePropertyMapReadOnly} props 前面inputProperties列举的属性,用get获取 */ paint(ctx, PaintSize, props) { const w = (props.get('--w') && +props.get('--w')[0].trim()) || 30; const h = (props.get('--h') && +props.get('--h')[0].trim()) || 30; const spacing = +props.get('--spacing')[0].trim() || 10; for (let x = 0; x < PaintSize.width / w; x++) { for (let y = 0; y < PaintSize.height / h; y++) { ctx.fillStyle = `#${Math.random().toString(16).slice(2, 8)}` ctx.beginPath(); ctx.rect(x * (w + spacing), y * (h + spacing), w, h); ctx.fill(); } } } } registerPaint('randomcolor', RandomColorPainter); [代码] 接着我们需要引入该worker: [代码]CSS && CSS.paintWorklet.addModule('worker.js');[代码] 最后我们在一个class为paint的div应用样式: [代码].paint{ background-image: paint(randomcolor); width: 100%; height: 600px; color: #000; --w: 50; --h: 50; --spacing: 10; } [代码] 再想想用js+div,是不是要先动态生成n个,然后各种计算各种操作dom,想想就可怕。如果是canvas,这可是canvas背景,你又要在上面放一个div,而且还要定位一波。 注意: worker是没有window的,所以想搞动画的就不能内部消化了。不过可以靠外面的css变量,我们用js操作css变量可以解决,也比传统的方法优雅 4. 自定义属性 支持情况 点击这里查看 首先,看一下支持度,目前浏览器并没有完全稳定使用,所以需要跟着它的提示:[代码]Experimental Web Platform features” on chrome://flags[代码],在chrome地址栏输入[代码]chrome://flags[代码]再找到[代码]Experimental Web Platform features[代码]并开启。 [代码]CSS.registerProperty({ name: '--myprop', //属性名字 syntax: '<length>', // 什么类型的单位,这里是长度 initialValue: '1px', // 默认值 inherits: true // 会不会继承,true为继承父元素 }); [代码] 说到继承,我们回到前面的css变量,已经说了变量是区分作用域的,其实父作用域定义变量,子元素使用该变量实际上是继承的作用。如果[代码]inherits: true[代码]那就是我们看见的作用域的效果,如果不是true则不会被父作用域影响,而且取默认值。 这个自定义属性,精辟在于,可以用永久循环的animation驱动一次性的transform。换句话说,我们如果用了css变量+transform,可以靠js改变这个变量达到花俏的效果。但是,现在不需要js,只要css内部消化,transform成为永动机。 [代码]// 我们先注册几种属性 ['x1','y1','z1','x2','y2','z2'].forEach(p => { CSS.registerProperty({ name: `--${p}`, syntax: '<angle>', inherits: false, initialValue: '0deg' }); }); [代码] 然后写个样式 [代码]#myprop, #myprop1 { width: 200px; border: 2px dashed #000; border-bottom: 10px solid #000; animation:myprop 3000ms alternate infinite ease-in-out; transform: rotateX(var(--x2)) rotateY(var(--y2)) rotateZ(var(--z2)) } [代码] 再来看看我们的动画,为了眼花缭乱,加了第二个改了一点数据的动画 [代码]@keyframes myprop { 25% { --x1: 20deg; --y1: 30deg; --z1: 40deg; } 50% { --x1: -20deg; --z1: -40deg; --y1: -30deg; } 75% { --x2: -200deg; --y2: 130deg; --z2: -350deg; } 100% { --x1: -200deg; --y1: 130deg; --z1: -350deg; } } @keyframes myprop1 { 25% { --x1: 20deg; --y1: 30deg; --z1: 40deg; } 50% { --x2: -200deg; --y2: 130deg; --z2: -350deg; } 75% { --x1: -20deg; --z1: -40deg; --y1: -30deg; } 100% { --x1: -200deg; --y1: 130deg; --z1: -350deg; } } [代码] html就两个div: [代码] <div id="myprop"></div> <div id="myprop1"></div> [代码] 效果是什么呢,自己可以跑一遍看看,反正功能很强大,但是想象力限制了发挥。 自己动手改的时候注意一下,动画关键帧里面,不能只存在1,那样子就不能驱动transform了,做不到永动机的效果,因为我的rotate写的是 rotateX(var(–x2))。接下来随意发挥吧 最后 再啰嗦一次 关于houdini最近动态可点击这里 关于houdini在浏览器的支持情况 ENJOY YOURSELF!!!
2019-03-25