- 实战分享: 小程序云开发玩转订阅消息(一)
[图片] 微信官方为提升小程序模板消息能力的使用体验,对模板消息的下发条件进行了调整。原有的小程序模板消息接口于 2020 年 1 月 10 日下线,届时将无法使用旧的小程序模板消息接口发送模板消息,取而代之的是新的一次性订阅消息和长期订阅消息。 订阅消息给小程序开发者带来了更好的触达用户的能力,在具体实施过程中,开发者如何把模板消息换成新的订阅消息,是否需要购买服务器来实现服务器鉴权,怎样才能在用户订阅之后一段时间后,给用户发送长期或一次性订阅消息呢? 小程序·云开发最近支持了通过云调用免 access_token 发送订阅消息,还新增支持了在定时触发器中实现云调用,这些能力可以帮助开发者轻松玩转小程序订阅消息。 我们今天会利用小程序·云开发进行一个小程序中实现订阅开课提醒的实战,帮助大家了解如何基于小程序·云开发快速接入小程序订阅消息。 [图片]整体时序图[图片]开课提醒订阅消息时序图环境准备注册小程序帐号[1]开通云开发服务[2]获取订阅消息模板 ID在微信小程序管理后台中,新增一个订阅消息的模板,这里我们新增了一个开课提醒的模板。 [图片]新增模板引导用户订阅微信小程序提供了[代码]wx.requestSubscribeMessage[代码] 接口来发起申请订阅权限界面。 [图片]微信申请订阅权限界面在 "订阅开课提醒" 的按钮上绑定 tap 事件,事件处理器我们这里用的 [代码]onSubscribe[代码] index.wxml [代码]<button class="btn" data-item="{{ item }}" bindtap="onSubscribe" hover-class="btn-hover" > 订阅开课提醒 </button> [代码]在 [代码]onSubscribe[代码] 函数内,我们会调用微信 API [代码]wx.requestSubscribeMessage[代码] 申请发送订阅消息权限,当用户在弹窗同意订阅之后,我们会收到 [代码]success[代码] 回调,将订阅的课程信息调用云函数 [代码]subscribe[代码] 存入云开发数据库,云函数 [代码]subscribe[代码] 的实现在下文会讲。 index.js [代码]onSubscribe: function(e) { // 获取课程信息 const item = e.currentTarget.dataset.item; // 调用微信 API 申请发送订阅消息 wx.requestSubscribeMessage({ // 传入订阅消息的模板id,模板 id 可在小程序管理后台申请 tmplIds: [lessonTmplId], success(res) { // 申请订阅成功 if (res.errMsg === 'requestSubscribeMessage:ok') { // 这里将订阅的课程信息调用云函数存入云开发数据 wx.cloud .callFunction({ name: 'subscribe', data: { data: item, templateId: lessonTmplId, }, }) .then(() => { wx.showToast({ title: '订阅成功', icon: 'success', duration: 2000, }); }) .catch(() => { wx.showToast({ title: '订阅失败', icon: 'success', duration: 2000, }); }); } }, }); [代码][代码] },[代码] 文章字数超出 50000 字,后半部分链接 《实战分享: 小程序云开发玩转订阅消息(二)》
2019-10-23 - 随手写的一个css头像轮播,代码较菜,不喜勿喷.哈哈哈
[图片] 大概就长这样样子,可以向左自动轮播的那种,主要就是用到了css的延迟transition-delay这个属性,然后配合定时器做的一个css的轮播.哈哈哈,然后没了,欢迎各位改造,指出里面的错误.觉得想用的朋友就拿去用吧 代码链接:https://developers.weixin.qq.com/s/ox93u6m17W9X
2019-06-13 - 使用Puppeteer搭建统一海报渲染服务
背景介绍 有赞微商城包括了 PC 端、H5 端和小程序端,每个端都有绘制分享海报的需求。最早的时候我们是在每个端通过canvas API来绘制的,通过canvas绘制有很多痛点,与本文要讲的海报渲染服务做了一个对比: [图片] 正是因为这些痛点问题,有同事就提出基于Puppeteer实现一个公共的海报渲染服务,使用方只需传入海报图片的html,海报渲染服务绘制一张对应的图片作为返回结果,解决了canvas绘制的各种痛点问题。 一、Puppeteer 是什么 Puppeteer是谷歌官方团队开发的一个 Node 库,它提供了一些高级 API 来通过DevTools协议控制Headless Chrome或Chromium。通俗的说就是提供了一些 API 用来控制浏览器的行为,比如打开网页、模拟输入、点击按钮、屏幕截图等操作,通过这些 API 可以完成很多有趣的事情,比如本文要讲的海报渲染服务,它用到的就是屏幕截图的功能。 二、Puppeteer 能做什么 Puppeteer几乎能实现你能在浏览器上做的任何事情,比如: 生成页面的屏幕截图或pdf 自动化提交表单、模拟键盘输入、自动化单元测试等 网站性能分析:可以抓取并跟踪网站的执行时间轴,帮助分析效率问题 抓取网页内容,也就是我们常说的爬虫 三、海报渲染服务 3.1方案设计 首先我们来看一下海报渲染服务的流程图: [图片] 其实整个流程还是比较简单的,当有一个绘制请求时,首先看之前是否已经绘制过相同的海报了,如果绘制过,就直接从Redis里取出海报图片的 CDN 地址。如果海报未曾绘制过,则先调用Headless Chrome来绘制海报,绘制完后上传到 CDN,最后 CDN 上传完后返回 CDN 地址。整个流程的大致代码实现如下: [代码]const crypto = require('crypto'); const PuppeteerProvider = require('../../lib/PuppeteerProvider'); const oneDay = 24 * 60 * 60; class SnapshotController { /** - 截图接口 * - @param {Object} ctx 上下文 */ async postSnapshotJson(ctx) { const result = await this.handleSnapshot(); ctx.json(0, 'ok', result); } async handleSnapshot() { const { ctx } = this; const { html } = ctx.request.body; // 根据 html 做 sha256 的哈希作为 Redis Key const htmlRedisKey = crypto.createHash('sha256').update(html).digest('hex'); try { // 首先看海报是否有绘制过的 let result = await this.findImageFromCache(htmlRedisKey); // 命中缓存失败 if (!result) { result = await this.generateSnapshot(htmlRedisKey); } return result; } catch (error) { ctx.status = 500; return ctx.throw(500, error.message); } } /** - 判断kv中是否有缓存 * - @param {String} htmlRedisKey kv存储的key */ async findImageFromCache(htmlRedisKey) { } /** - 生成截图 * - @param {String} htmlRedisKey kv存储的key */ async generateSnapshot(htmlRedisKey) { const { ctx } = this; const { html, width = 375, height = 667, quality = 80, ratio = 2, type: imageType = 'jpeg', } = ctx.request.body; this.validator .required(html, '缺少必要参数 html') .required(operatorId, '缺少必要参数 operatorId'); let imgBuffer; try { imgBuffer = await PuppeteerProvider.snapshot({ html, width, height, quality, ratio, imageType }); } catch (err) { // logger } let imgUrl; try { imgUrl = await this.uploadImage(imgBuffer, operatorId); // 将海报图片存在 Redis 里 await ctx.kvdsClient.setex(htmlRedisKey, oneDay, imgUrl); } catch (err) { } return { img: imgUrl || '', type: IMAGE_TYPE_MAP.CDN, }; } /** - 上传图片到七牛 * - @param {Buffer} imgBuffer 图片buffer */ async uploadImage(imgBuffer) { // upload image to cdn and return cdn url } } module.exports = SnapshotController; [代码] 3.2遇到的问题 2.3.1 Chromium启动和执行流程 最开始一个版本我们是直接Puppeteer.launch()返回一个浏览器实例,每次绘制会用单独的一个浏览器实例,这个在使用过程中发现绘制海报会很慢,后面优化时找到了这篇文章:Puppeteer性能优化与执行速度提升,这篇文章提到了两个优化点:1. 优化Chromium启动项;2. 优化Chromium执行流程。 先说优化Chromium启动项,这个就是为了我们启动一个最小化可用的浏览器实例,其他不需要的功能都禁用掉,这样会大大提升启动速度。 [代码]const browser = await puppeteer.launch({ args: [ '–disable-gpu', '–disable-dev-shm-usage', '–disable-setuid-sandbox', '–no-first-run', '–no-sandbox', '–no-zygote', '–single-process' ] }); [代码] 再来说说浏览器的执行流程,最开始我们是每次绘制都会用单独一个浏览器,也就是一对一,这个在压测的时候发现CPU和内存飙升,最后我们改用了复用浏览器标签的方式,每次绘制新建一个标签来绘制。 [代码]const page = await browser.newPage(); page.setContent(html, { waitUntil: 'networkidle0' }); const imageBuffer = await page.screeshot(options); [代码] 3.3.2 networkidle0 最开始我们的海报服务绘制海报时有时候会偶尔出现图片展示不出来的情况,我们排查后发现是因为我们setContent时,使用的是默认的load事件来判断设置内容成功,而我们期望的是所有网络请求成功后才算设置内容成功。 [代码]page.setContent(html); [代码] Puppeteer在setContent和goto等方法里提供了一个waitUntil的参数,它就是用来配置这个判断成功的标准,它提供了四个可选值: load:默认值,load事件触发就算成功 domcontentloaded:domcontentloaded事件触发就算成功 networkidle0:在500ms内没有网络连接时就算成功 networkidle2:在500ms内有不超过2个网络连接时就算成功 我们这里需要用到的就是networkidle0: [代码]page.setContent(html, { waitUntil: 'networkidle0' }); [代码] 当改成networkidle0后,使用方给我们反馈说整个绘制服务变慢了很多,随随便便都2s以上。变慢主要是因为加上networkidle0后,至少需要等待500ms以上,加上绘制的一些其他开销,基本上就需要2s了。所以我们期望这个500ms是可配置的,因为500ms实在太长了,我们的分享海报一般只有几张图片,不需要这么久。但是Puppeteer没有提供相关的参数,还好在issue中早已经有人提出了这个问题:Control networkidle wait time [代码]function waitForNetworkIdle(page, timeout, maxInflightRequests = 0) { page.on('request', onRequestStarted); page.on('requestfinished', onRequestFinished); page.on('requestfailed', onRequestFinished); let inflight = 0; let fulfill; let promise = new Promise(x => fulfill = x); let timeoutId = setTimeout(onTimeoutDone, timeout); return promise; function onTimeoutDone() { page.removeListener('request', onRequestStarted); page.removeListener('requestfinished', onRequestFinished); page.removeListener('requestfailed', onRequestFinished); fulfill(); } function onRequestStarted() { ++inflight; if (inflight > maxInflightRequests) clearTimeout(timeoutId); } function onRequestFinished() { if (inflight === 0) return; --inflight; if (inflight === maxInflightRequests) timeoutId = setTimeout(onTimeoutDone, timeout); } } // Example await Promise.all([ page.goto('https://google.com'), waitForNetworkIdle(page, 500, 0), // equivalent to 'networkidle0' ]); [代码] 利用这个函数我们就可以传入自己想要的超时时间了。 3.2.3 Chromium定时刷新机制 为什么需要定时刷新Chromium呢?总不可能一直用同一个Chromium实例吧,万一变卡或者crash了,就会影响海报的绘制。所以我们需要定时的去刷新当前的浏览器实例。 [代码]class PuppeteerProvider { constructor() { this.browserList = []; } /** - 初始化`puppeteer`实例 */ initBrowserInstance() { Array.from({ length: browserConcurrency }, () => { this.checkBrowserInstance(); }); // 每隔30分钟刷新一下浏览器 this.refreshTimer = setTimeout(() => this.refreshOneBrowser(), thrityMinutes); } /** - 检查是否还需要浏览器实例 */ async checkBrowserInstance() { if (this.needBrowserInstance) { this.browserList.push(this.launchBrowser()); } } /** - 定时刷新浏览器 */ refreshOneBrowser() { clearTimeout(this.refreshTimer); const browserInstance = this.browserList.shift(); this.replaceBrowserInstance(browserInstance); this.checkBrowserInstance(); // 每隔30分钟刷新一下浏览器 this.refreshTimer = setTimeout(() => this.refreshOneBrowser(), thrityMinutes); } /** - 替换单个浏览器实例 * - @param {String} browserInstance 浏览器promise - @param {String} retries 重试次数,超过这个次数直接关闭浏览器 */ async replaceBrowserInstance(browserInstance, retries = 2) { const browser = await browserInstance; const openPages = await browser.pages(); // 因为浏览器会打开一个空白页,如果当前浏览器还有任务在执行,一分钟后再关闭 if (openPages && openPages.length > 1 && retries > 0) { const nextRetries = retries - 1; setTimeout(() => this.replaceBrowserInstance(browserInstance, nextRetries), oneMinute); return; } browser.close(); } launchBrowser(opts = {}, retries = 1) { return PuppeteerHelper.launchBrowser(opts).then(chrome => { return chrome; }).catch(error => { if (retries > 0) { const nextRetries = retries - 1; return this.launchBrowser(opts, nextRetries); } throw error; }); } } [代码] 这里还有一个点,我们给replaceBrowserInstance这个方法加了个重试次数的限制,当超出这个限制后不管有没有任务在进行都会关闭浏览器。这个是防止在某些特殊情况不能关闭掉浏览器,导致内存无法释放的情况。 四、展望 目前海报渲染服务的问题就是qps比较低,因为Chromium消耗最多的资源是CPU,当并发数变高时,CPU也随之变高,就会导致后面的绘制变慢。在4核8G的情况,大概是20qps左右。后面的主要精力就是如何去提升单机的qps,应该还有比较大的空间。还有就是看看能不能增加定时任务,在凌晨机器比较闲的时候提前绘制好一些常用的海报,这样当需要海报时就是直接从redis里取出来了,充分利用了机器的性能,也可以减少海报服务白天的压力。 五、相关链接: Puppeteer 性能优化与执行速度提升:https://blog.it2048.cn/article-puppeteer-speed-up/ Control networkidle wait time:https://github.com/GoogleChrome/puppeteer/issues/1353
2019-06-13 - TypeScript入门完全指南(基础篇)
[TOC] 为什么JS需要类型检查 TypeScript的设计目标在这里可以查看到,简单概括为两点: 为JavaScript提供一个可选择的类型检查系统; 为JavaScript提供一个包含将来新特性的版本。 TypeScript的核心价值体现在第一点,第二点可以认为是TypeScript的向后兼容性保证,也是TypeScript必须要做到的。 那么为什么JS需要做静态类型检查呢?在几年前这个问题也许还会存在比较大的争议,在前端日趋复杂的今天,经过像Google、Microsoft、FaceBook这样的大公司实践表明,类型检查对于代码可维护性和可读性是有非常大的帮助的,尤其针对于需要长期维护的规模性系统。 TypeScript优势 在我看来,TypeScript能够带来最直观上的好处有三点: 帮助更好地重构代码; 类型声明本身是最好查阅的文档。 编辑器的智能提示更加友好。 一个好的代码习惯是时常对自己写过的代码进行小的重构,让代码往更可维护的方向去发展。然而对于已经上线的业务代码,往往测试覆盖率不会很高,当我们想要重构时,经常会担心自己的改动会产生各种不可预知的bug。哪怕是一个小的重命名,也有可能照顾不到所有的调用处造成问题。 如果是一个TypeScript项目,这种担心就会大大降低,我们可以依赖于TypeScript的静态检查特性帮助找出一个小的改动(如重命名)带来的其他模块的问题,甚至对于模块文件来说,我们可以直接借助编辑器的能力进行[代码]“一键重命名”[代码]操作。 另外一个问题,如果你接手过一个老项目,肯定会头痛于各种文档的缺失和几乎没有注释的代码,一个好的TypeScript项目,是可以做到代码即文档的,通过声明文件我们可以很好地看出各个字段的含义以及哪些是前端必须字段: [代码]// 砍价用户信息 export interface BargainJoinData { curr_price: number; // 当前价 curr_ts: number; // 当前时间 init_ts: number; // 创建时间 is_bottom_price: number; // 砍到底价 } [代码] TypeScript对开发者是友好的 TypeScript在设计之初,就确定了他们的目标并不是要做多么严格完备的类型强校验系统,而是能够更好地兼容JS,更贴合JS开发者的开发习惯。可以说这是MS的商业战略,也是TS能够成功的关键性因素之一。它对JS的兼容性主要表现为以下三个方面: 隐式的类型推断 [代码]var foo = 123; foo = "456"; // Error: cannot assign `string` to `number` [代码] 当我们对一个变量或函数等进行赋值时,TypeScript能够自动推断类型赋予变量,TypeScript背后有非常强大的自推断算法帮助识别类型,这个特性无疑可以帮助我们简化一些声明,不必像其他语言那样处处是声明,也可以让我们看代码时更加轻松。 结构化的类型 TypeScript旨在让JS开发者更简单地上手,因此将类型设计为“结构化”(Structural)的而非“名义式”(Nominal)的。 什么意思呢?意味着TypeScript的类型并不根据定义的名字绑定,只要是形似的类型,不管名称相不相同,都可以作为兼容类型(这很像所谓的duck typing),也就是说,下面的代码在TypeScript中是完全合法的: [代码]class Foo { method(input: string) { /* ... */ } } class Bar { method(input: string) { /* ... */ } } let test: Foo = new Bar(); // no Error! [代码] 这样实际上可以做到类型的最大化复用,只要形似,对于开发者也是最好理解的。(当然对于这个示例最好的做法是抽出一个公共的interface) 知名的JS库支持 TypeScript有强大的DefinitelyTyped社区支持,目前类型声明文件基本上已经覆盖了90%以上的常用JS库,在编写代码时我们的提示是非常友好的,也能做到安全的类型检查。(在使用第三方库时,可以现在这个项目中检索一下有没有该库的TS声明,直接引入即可) 回顾两个基础知识 在进入正式的TS类型介绍之前,让我们先回顾一下JS的两个基础: 相等性判断 我们都知道,在JS里,两个等号的判断会进行隐式的类型转换,如: [代码]console.log(5 == "5"); // true console.log(0 == ""); // true [代码] 在TS中,因为有了类型声明,因此这两个结果在TS的类型系统中恒为false,因此会有报错: [代码]This condition will always return 'false' since the types '5' and '"5"' have no overlap. [代码] 所以在代码层面,一方面我们要避免这样两个不同类型的比较,另一方面使用全等来代替两个等号,保证在编译期和运行期具有相同的语义。 对于TypeScript而言,只有[代码]null[代码]和[代码]undefined[代码]的隐式转换是合理的: [代码]console.log(undefined == undefined); // true console.log(null == undefined); // true console.log(0 == undefined); // false console.log('' == undefined); // false console.log(false == undefined); // false [代码] 类(Class) 对于ES6的Class,我们本身已经很熟悉了,值得一提的是,目前对于类的静态属性、成员属性等有一个提案——proposal-class-fields已经进入了Stage3,这个提案包含了很多东西,主要是类的静态属性、成员属性、公有属性和私有属性。其中,私有属性的提案在社区内引起了非常大的争议,由于它的丑陋和怪异遭受各路人马的抨击,现TC39委员会已决定重新思考该提案。 现在让我们来看看TypeScript对属性访问控制的情况: 可访问性 public protected private 类本身 是 是 是 子类 是 是 否 类的实例 是 否 否 可以看到,TS中的类成员访问和其他语言非常类似: [代码]class FooBase { public x: number; private y: number; protected z: number; } [代码] 对于类的成员构造函数初始化,TS提供了一个简单的声明方式: [代码]class Foo { constructor(public x:number) { } } [代码] 这段代码和下面是等同的: [代码]class Foo { x: number; constructor(x:number) { this.x = x; } } [代码] TS类型系统基础 基本性准则 在正式了解TypeScript之前,首先要明确两个基本概念: TypeScript的类型系统设计是可选的,意味着JavaScript就是TypeScript。 TypeScript的报错并不会阻止JS代码的生成,你可以渐进式地将JS逐步迁移为TS。 基本语法 [代码]:<TypeAnnotation> [代码] TypeScript的基本类型语法是在变量之后使用冒号进行类型标识,这种语法也揭示了TypeScript的类型声明实际上是可选的。 原始值类型 [代码]var num: number; var str: string; var bool: boolean; [代码] TypeScript支持三种原始值类型的声明,分别是[代码]number[代码]、[代码]string[代码]和[代码]boolean[代码]。 对于这三种原始值,TS同样支持以它们的字面量为类型: [代码]var num: 123; var str: '123'; var bool: true; [代码] 这类字面量类型配合上联合类型还是十分有用的,我们后面再讲。 数组类型 对于数组的声明也非常简单,只需要加上一个中括号声明类型即可: [代码]var boolArray: boolean[]; [代码] 以上就简单地定义了一个布尔类型的数组,大多数情况下,我们数组的元素类型是固定的,如果我们数组内存在不同类型的元素怎么办? 如果元素的个数是已知有限的,可以使用TS的元组类型: [代码]var nameNumber: [string, number]; [代码] 该声明也非常的形象直观,如果元素个数不固定且类型未知,这种情况较为罕见,可直接声明成any类型: [代码]var arr: any[] [代码] 接口类型 接口类型是TypeScript中最常见的组合类型,它能够将不同类型的字段组合在一起形成一个新的类型,这对于JS中的对象声明是十分友好的: [代码]interface Name { first: string; second: string; } var personName:Name = { first: '张三' } // Property 'second' is missing in type '{ first: string; }' but required in type 'Name' [代码] 上述例子可见,TypeScript对每一个字段都做了检查,若未定义接口声明的字段(非可选),则检查会抛出错误。 内联接口 对于对象来说,我们也可以使用内联接口来快速声明类型: [代码]var personName:{ first: string, second: string } = { first: '张三' } // Property 'second' is missing in type '{ first: string; }' but required in type 'Name' [代码] 内联接口可以帮助我们快速声明类型,但建议谨慎使用,对于可复用以及一般性的接口声明建议使用interface声明。 索引类型 对于对象而言,我们可以使用中括号的方式去存取值,对TS而言,同样支持相应的索引类型: [代码]interface Foo { [key:string]: number } [代码] 对于索引的key类型,TypeScript只支持[代码]number[代码]和[代码]string[代码]两种类型,且Number是string的一种特殊情况。 对于索引类型,我们在一般化的使用场景上更方便: [代码]interface NestedCSS { color?: string; nest?: { [selector: string]: NestedCSS; } } const example: NestedCSS = { color: 'red', nest: { '.subclass': { color: 'blue' } } } [代码] 类的接口 对于接口而言,另一个重要作用就是类可以实现接口: [代码]interface Point { x: number; y: number; z: number; // New member } class MyPoint implements Point { // ERROR : missing member `z` x: number; y: number; } [代码] 对类而言,实现接口,意味着需要实现接口的所有属性和方法,这和其他语言是类似的。 函数类型 函数是TypeScript中最常见的组成单元: [代码]interface Foo { foo: string; } // Return type annotated as `: Foo` function foo(sample: Foo): Foo { return sample; } [代码] 对于函数而言,本身有参数类型和返回值类型,都可进行声明。 可选参数 对于参数,我们可以声明可选参数,即在声明之后加一个问号: [代码]function foo(bar: number, bas?: string): void { // .. } [代码] void和never类型 另外,上述例子也表明,当函数没有返回值时,可以用[代码]void[代码]来表示。 当一个函数永远不会返回时,我们可以声明返回值类型为[代码]never[代码]: [代码]function bar(): never { throw new Error('never reach'); } [代码] callable和newable 我们还可以使用接口来定义函数,在这种函数实现接口的情形下,我们称这种定义为[代码]callable[代码]: [代码]interface Complex { (bar?: number, ...others: boolean[]): number; } var foo: Complex; [代码] 这种定义方式在可复用的函数声明中非常有用。 callable还有一种特殊的情况,该声明中指定了[代码]new[代码]的方法名,称之为[代码]newable[代码]: [代码]interface CallMeWithNewToGetString { new(): string } var foo: CallMeWithNewToGetString; new foo(); [代码] 这个在构造函数的声明时非常有用。 函数重载 最后,一个函数可以支持多种传参形式,这时候仅仅使用可选参数的约束可能是不够的,如: [代码]unction padding(a: number, b?: number, c?: number, d?: number) { if (b === undefined && c === undefined && d === undefined) { b = c = d = a; } else if (c === undefined && d === undefined) { c = a; d = b; } return { top: a, right: b, bottom: c, left: d }; } [代码] 这个函数可以支持四个参数、两个参数和一个参数,如果我们粗略的将后三个参数都设置为可选参数,那么当传入三个参数时,TS也会认为它是合法的,此时就失去了类型安全,更好的方式是声明函数重载: [代码]function padding(all: number); function padding(topAndBottom: number, leftAndRight: number); function padding(top: number, right: number, bottom: number, left: number); function padding(a: number, b?: number, c?: number, d?: number) { //... } [代码] 函数重载写法也非常简单,就是重复声明不同参数的函数类型,最后一个声明包含了兼容所有重载声明的实现。这样,TS类型系统就能准确的判断出该函数的多态性质了。 使用[代码]callable[代码]的方式也可以声明重载: [代码]interface Padding { (all: number): any (topAndBottom: number, leftAndRight: number): any (top: number, right: number, bottom: number, left: number): any } [代码] 特殊类型 any [代码]any[代码]在TypeScript中是一个比较特殊的类型,声明为[代码]any[代码]类型的变量就像动态语言一样不受约束,好像关闭了TS的类型检查一般。对于[代码]any[代码]类型的变量,可以将其赋予任何类型的值: [代码]var power: any; power = '123'; power = 123; [代码] [代码]any[代码]对于JS代码的迁移是十分友好的,在已经成型的TypeScript项目中,我们要慎用[代码]any[代码]类型,当你设置为[代码]any[代码]时,意味着告诉编辑器不要对它进行任何检查。 null和undefined [代码]null[代码]和[代码]undefined[代码]作为TypeScript的特殊类型,它同样有字面量的含义,之前我们已经了解到。 值得注意的是,[代码]null[代码]和[代码]undefined[代码]可以赋值给任意类型的变量: [代码]var num: number; var str: string; // 赋值给任意类型的变量都是合法的 num = null; str = undefined; [代码] void和never 在函数类型中,我们已经介绍了两种类型,专门修饰函数返回值。 readonly [代码]readonly[代码]是只读属性的修饰符,当我们的属性是只读时,可以用该修饰符加以约束,在类中,用[代码]readonly[代码]修饰的属性仅可以在构造函数中初始化: [代码]class Foo { readonly bar = 1; // OK readonly baz: string; constructor() { this.baz = "hello"; // OK } } [代码] 一个实用场景是在[代码]react[代码]中,[代码]props[代码]和[代码]state[代码]都是只读的: [代码]interface Props { readonly foo: number; } interface State { readonly bar: number; } export class Something extends React.Component<Props,State> { someMethod() { this.props.foo = 123; // ERROR: (props are immutable) this.state.baz = 456; // ERROR: (one should use this.setState) } } [代码] 当然,[代码]React[代码]本身在类的声明时会对传入的[代码]props[代码]和[代码]state[代码]做一层[代码]ReadOnly[代码]的包裹,因此无论我们是否在外面显式声明,赋值给[代码]props[代码]和[代码]state[代码]的行为都是会报错的。 注意,[代码]readonly[代码]听起来和[代码]const[代码]有点像,需要时刻保持一个概念: [代码]readonly[代码]是修饰属性的 [代码]const[代码]是声明变量的 泛型 在更加一般化的场景,我们的类型可能并不固定已知,它和[代码]any[代码]有点像,只不过我们希望在[代码]any[代码]的基础上能够有更近一步的约束,比如: [代码]function reverse<T>(items: T[]): T[] { var toreturn = []; for (let i = items.length - 1; i >= 0; i--) { toreturn.push(items[i]); } return toreturn; } [代码] [代码]reverse[代码]函数是一个很好的示例,对于一个通用的函数[代码]reverse[代码]来说,数组元素的类型是未知的,可以是任意类型,但[代码]reverse[代码]函数的返回值也是个数组,它和传入的数组类型是相同的,对于这个约束,我们可以使用泛型,其语法是尖括号,内置泛型变量,多个泛型变量用逗号隔开,泛型变量名称没有限制,一般而言我们以大写字母开头,多个泛型变量使用其语义命名,加上[代码]T[代码]为前缀。 在调用时,可以显示的指定泛型类型: [代码]var reversed = reverse<number>([1, 2, 3]); [代码] 也可以利用TypeScript的类型推断,进行隐式调用: [代码]var reversed = reverse([1, 2, 3]); [代码] 由于我们的参数类型是[代码]T[][代码],而传入的数组类型是一个[代码]number[][代码],此时[代码]T[代码]的类型被TypeScript自动推断为[代码]number[代码]。 对于泛型而言,我们同样可以作用于接口和类: [代码]interface Array<T> { reverse(): T[]; // ... } [代码] 联合类型 在JS中,一个变量的类型可能拥有多个,比如: [代码]function formatCommandline(command: string[]|string) { var line = ''; if (typeof command === 'string') { line = command.trim(); } else { line = command.join(' ').trim(); } } [代码] 此时我们可以使用一个[代码]|[代码]分割符来分割多种类型,对于这种复合类型,我们称之为[代码]联合类型[代码]。 交叉类型 如果说联合类型的语义等同于[代码]或者[代码],那么交叉类型的语义等同于集合中的[代码]并集[代码],下面的[代码]extend[代码]函数是最好的说明: [代码]function extend<T, U>(first: T, second: U): T & U { let result = <T & U> {}; for (let id in first) { result[id] = first[id]; } for (let id in second) { if (!result.hasOwnProperty(id)) { result[id] = second[id]; } } return result; } [代码] 该函数最终以[代码]T&U[代码]作为返回值值,该类型既包含了[代码]T[代码]的字段,也包含了[代码]U[代码]的字段,可以看做是两个类型的[代码]并集[代码]。 类型别名 TypeScript为类型的复用提供了更便捷的方式——类型别名。当你想复用类型时,可能在该场景下要为已经声明的类型换一个名字,此时可以使用type关键字来进行类型别名的定义: [代码]interface state { a: 1 } export type userState = state; [代码] 我们同样可以使用type来声明一个类型: [代码]type Text = string | { text: string }; type Coordinates = [number, number]; type Callback = (data: string) => void; [代码] 对于type和interface的取舍: 如果要用交叉类型或联合类型,使用type。 如果要用extend或implement,使用interface。 其余情况可看个人喜好,个人建议type更多应当用于需要起别名时,其他情况尽量使用interface。 枚举类型 对于组织一系列相关值的集合,最好的方式应当是枚举,比如一系列状态集合,一系列归类集合等等。 在TypeScript中,枚举的方式非常简单: [代码]enum Color { Red, Green, Blue } var col = Color.Red; [代码] 默认的枚举值是从0开始,如上述代码,[代码]Red=0[代码],[代码]Green=1[代码]依次类推。 当然我们还可以指定初始值: [代码]enum Color { Red = 3, Green, Blue } [代码] 此时[代码]Red=3[代码], [代码]Green=4[代码]依次类推。 大家知道在JavaScript中是不存在枚举类型的,那么TypeScript的枚举最终转换为JavaScript是什么样呢? [代码]var Color; (function (Color) { Color[Color["Red"] = 0] = "Red"; Color[Color["Green"] = 1] = "Green"; Color[Color["Blue"] = 2] = "Blue"; })(Color || (Color = {})); [代码] 从编译后的代码可以看到,转换为一个key-value的对象后,我们的访问也非常方便: [代码]var red = Color.Red; // 0 var redKey = Color[0]; // 'Red' var redKey = Color[Color.Red]; // 'Red' [代码] 既可以通过key来访问到值,也可以通过值来访问到key。 Flag标识位 对于枚举,有一种很实用的设计模式是使用位运算来标识(Flag)状态: [代码]enum EnvFlags { None = 0, QQ = 1 << 0, Weixin = 1 << 1 } function initShare(flags: EnvFlags) { if (flags & EnvFlags.QQ) { initQQShare(); } if (flags & EnvFlags.Weixin) { initWeixinShare(); } } [代码] 在我们使用标识位时,可以遵循以下规则: 使用 [代码]|=[代码] 增加标志位 使用 [代码]&=[代码] 和 [代码]~[代码]清除标志位 使用 [代码]|[代码] 联合标识位 如: [代码]var flag = EnvFlags.None; flag |= EnvFlags.QQ; // 加入QQ标识位 Flag &= ~EnvFlags.QQ; // 清除QQ标识位 Flag |= EnvFlags.QQ | EnvFlags.Weixin; // 加入QQ和微信标识位 [代码] 常量枚举 在枚举定义加上[代码]const[代码]声明,即可定义一个常量枚举: [代码]enum Color { Red = 3, Green, Blue } [代码] 对于常量枚举,TypeScript在编译后不会产生任何运行时代码,因此在一般情况下,应当优先使用常量枚举,减少不必要代码的产生。 字符串枚举 TypeScript还支持非数字类型的枚举——字符串枚举 [代码]export enum EvidenceTypeEnum { UNKNOWN = '', PASSPORT_VISA = 'passport_visa', PASSPORT = 'passport', SIGHTED_STUDENT_CARD = 'sighted_tertiary_edu_id', SIGHTED_KEYPASS_CARD = 'sighted_keypass_card', SIGHTED_PROOF_OF_AGE_CARD = 'sighted_proof_of_age_card', } [代码] 这类枚举和我们之前使用JavaScript定义常量集合的方式很像,好处在于调试或日志输出时,字符串比数字要包含更多的语义。 命名空间 在没有模块化的时代,我们为了防止全局的命名冲突,经常会以命名空间的形式组织代码: [代码](function(something) { something.foo = 123; })(something || (something = {})) [代码] TypeScript内置了[代码]namespace[代码]变量帮助定义命名空间: [代码]namespace Utility { export function log(msg) { console.log(msg); } export function error(msg) { console.error(msg); } } [代码] 对于我们自己的工程项目而言,一般建议使用ES6模块的方式去组织代码,而命名空间的模式可适用于对一些全局库的声明,如jQuery: [代码]namespace $ { export function ajax(//...) {} } [代码] 当然,命名空间还可以便捷地帮助我们声明静态方法,如和[代码]enum[代码]的结合使用: [代码]enum Weekday { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday } namespace Weekday { export function isBusinessDay(day: Weekday) { switch (day) { case Weekday.Saturday: case Weekday.Sunday: return false; default: return true; } } } const mon = Weekday.Monday; const sun = Weekday.Sunday; console.log(Weekday.isBusinessDay(mon)); // true console.log(Weekday.isBusinessDay(sun)); // false [代码] 关于命名规范 变量名、函数和文件名 推荐使用驼峰命名。 [代码]// Bad var FooVar; function BarFunc() { } // Good var fooVar; function barFunc() { } [代码] 类、命名空间 推荐使用帕斯卡命名。 成员变量和方法推荐使用驼峰命名。 [代码]// Bad class foo { } // Good class Foo { } // Bad class Foo { Bar: number; Baz() { } } // Good class Foo { bar: number; baz() { } } [代码] Interface、type 推荐使用帕斯卡命名。 成员字段推荐使用驼峰命名。 [代码]// Bad interface foo { } // Good interface Foo { } // Bad interface Foo { Bar: number; } // Good interface Foo { bar: number; } [代码] 关于模块规范 [代码]export default[代码]的争论 关于是否应该使用[代码]export default[代码]在这里有详尽的讨论,在AirBnb规范中也有[代码]prefer-default-export[代码]这条规则,但我认为在TypeScript中应当尽量不使用[代码]export default[代码]: 关于链接中提到的重命名问题, 甚至自动import,其实export default也是可以做到的,借助编辑器和TypeScript的静态能力。所以这一点还不是关键因素。 不过使用一般化的[代码]export[代码]更让我们容易获得智能提示: [代码]import /* here */ from 'something'; [代码] 在这种情况下,一般编辑器是不会给出智能提示的。 而这种: [代码]import { /* here */ } from 'something'; [代码] 我们可以通过智能提示做到快速引入。 除了这一点外,还有以下几点好处: 对CommonJS是友好的,如果使用export default,在commonJS下需要这样引入: [代码]const {default} = require('module/foo'); [代码] 多了个default无疑感觉非常奇怪。 对动态import是友好的,如果使用export default,还需要显示的通过default字段来访问: [代码]const HighChart = await import('https://code.highcharts.com/js/es-modules/masters/highcharts.src.js'); Highcharts.default.chart('container', { ... }); // 注意 `.default` [代码] 对于[代码]re-exporting[代码]是友好的,如果使用export default,那么进行[代码]re-export[代码]会比较麻烦: [代码]import Foo from "./foo"; export { Foo } [代码] 相比之下,如果没有[代码]export default[代码],我们可以直接使用: [代码]export * from "./foo" [代码] 实践中的一些坑 实践篇即将到来,敬请期待~
2019-03-18 - 小程序蓝牙打印爬坑之旅
因为公司要在小程序上加蓝牙打印标签功能,所以就开始接触小程序的蓝牙打印,看文档还是蛮详细的,而且还有demo,顺着demo,一步一步下来还是蛮顺畅的,原以为很快就能完成。没想到坑来了,由于demo中writeBLECharacteristicValue只是写入了一个16进制的数据,而现实中是需要发送字符串的,而且小程序必须要是arrayBuffer,就必须将字符串转arrayBuffer了,好,网上搜下,准备打印了吱吱吱咦,怎么有乱码啊,怎么中文都乱码了。。这下可糟了!于是就去各种找答案。最后知道问题了:原来是因为我们公司用的打印机是智能支持GB2312编码格式的二进制的,但是字符串是utf-8,诶,又得爬坑。经过一天的努力,终于找到解决方法啦,感谢csdn的大大们。实现的代码如下 //计算arraybuffer的长度 sumStrLength(str) { var length = 0; var data = str.toString(); for (var i = 0; i < data.length; i++) { if (this.isCN(data[i])) { //是中文 length += 2; } else { length += 1; } } return length; }, //混杂 hexStringToBuff(str) { //str=‘中国:WXHSH’ const buffer = new ArrayBuffer((this.sumStrLength(str)) + 1); const dataView = new DataView(buffer) var data = str.toString(); var p = 0; //ArrayBuffer 偏移量 for (var i = 0; i < data.length; i++) { if (this.isCN(data[i])) { //是中文 //调用GBK 转码 var t = gbk.$URL.encode(data[i]); for (var j = 0; j < 2; j++) { var temp = parseInt(t[j * 2] + t[j * 2 + 1], 16) dataView.setUint8(p++, temp) } } else { var temp = parseInt(data.charCodeAt(i).toString(16), 16) dataView.setUint8(p++, temp) } } console.log(String.fromCharCode.apply(null, new Uint8Array(buffer))); return buffer; }, //js正则验证中文 isCN(str) { if (/[1]+$/.test(str)) { return true; } else { return false; } }, 将中文转化为GB2312编码格式再转成arrayBuffer就大功告成啦,把这个文章记录下来,希望可以帮助到其他小程程们。如有需要,加我Q:786914253 \u3220-\uFA29 ↩︎
2019-03-19 - 十分钟理解 Git 原理
历史 2002 年,Linus(Linux 作者)决定使用 BitKeeper 作为 Linux 内核主要的版本控制系统。Linus 曾考虑过采用现成软件作为版本控制系统,但这些软件都存在一些问题,比如:性能不佳。 2005 年,Andrew(Samba 作者)写了一个简单程序,可以连接 BitKeeper 的存储库,BitKeeper 作者反对这种逆向工程的行为,因此收回了无偿使用 BitKeeper 的许可。Linux 内核开发团队与其磋商无果后,Linus 决定自行开发版本控制系统替代 BitKeeper,然后用了十天的时间编写出 Git。 Git(英国俚语“混帐”)名字来源于 Linus 的自嘲: I’m an egotistical bastard, and I name all my projects after myself. First Linux, now Git. 基本原理 因为 Git 是一个分布式版本控制系统,因此 Git 的操作大部分都是在本地的,除非明确说明,下面的原理或命令都是本地操作。 每个 Git 项目的根目录下有一个 [代码].git[代码] 目录,它是 Git 默默进行版本控制时读写的“数据库”。有几个概念需要提一下: 工作区:代码所在目录; 暂存区: [代码].git/index[代码] 文件; 本地仓库: [代码].git[代码] 目录; 一个典型的工作流程如下图,绿色部分为工作区(Working Directory),对它进行任何修改(包括:新建文件、删除文件、文件重命名等)都和单纯的修改文件一样,不会涉及到版本控制。 只有当你把工作区的修改提交(commit)到仓库([代码].git[代码] 目录)中,Git 才会真正的进行版本控制。 [图片] 暂存区是一个包含文件索引的目录树([代码].git/index[代码] 文件),记录了文件的元数据(文件名、文件长度、修改时间等),而文件内容则存放在 [代码].git/objects[代码] 目录下。 用 Git 进行版本控制,实际上就是在工作区、暂存区、仓库三个地方进行文件信息的记录。 [图片] Git 将提交(commit)、文件、目录统统视为对象。对象以 [代码]SHA1[代码] 值作为指纹,与其他对象相区分。Git 命令操作的最小单位是对象。 Git 会将文件的副本存放在 [代码].git[代码] 文件夹下,每个文件都根据文件内容进行操作。以下图为例: [代码]98ca9[代码] 对象是一次提交,它记录了本次提交的元信息以及 [代码]92ec2[代码] 树对象; [代码]92ec2[代码] 树对象记录了文件名和对象的印射关系; 本次提交修改的三个文件一一对应了一个对象; [图片] Git 项目的文件始终在四种状态之间迁移,如下图所示: [图片] 如果是新文件,典型的操作流程如下: 创建一个新文件 [代码]foo.txt[代码],此时它处于未跟踪(Untracked)的状态,未被 Git 进行版本控制; [图片] 通过 [代码]add[代码] 命令将它纳入 Git 管理,此时 [代码]foo.txt[代码] 变为已暂存(Staged)状态; [图片] 提交此次操作,[代码]foo.txt[代码] 转变为未修改(Unmodified)状态; [图片] 如果是修改文件,典型的操作流程如下: 修改 [代码]foo.txt[代码] 文件,它转变为已修改(Modified)状态; [图片] 通过 [代码]add[代码] 命令将它的修改记录到暂存区,为已暂存(Staged)状态; [图片] 提交此次操作,[代码]foo.txt[代码] 转变为未修改(Unmodified)状态; [图片] 如果是删除文件,典型的操作流程如下: 删除 [代码]foo.txt[代码] 文件,它转变为已修改(Modified)状态; [图片] 通过 [代码]add[代码] 命令将操作记录到暂存区,为已暂存(Staged)状态; [图片] 提交此次操作,[代码]foo.txt[代码] 转变为未修改(Unmodified)状态(文件历史版本依然在 [代码].git[代码] 中被记录着); [图片] 常用命令 下面列举一下常用的 Git 命令,方便速查。更详细的速查可以看 快速回忆你用过的 Git。 查看信息 [代码]# 列出处于未跟踪、已修改或已暂存状态的文件 git status # 查看提交日志 git log # 查看已暂存文件和最后一次提交相比的详细修改 git diff --staged [代码] 状态变更 [代码]# 暂存或添加 git add . # 提交 git commit -m "commit description" [代码] 远程仓库(Remote Repository) [代码]# 下载到本地 git clone http://git.code.oa.com/<USER>/<PROJECT>.git # 更新代码 git pull # 将本地仓库的 commit 提交到远程仓库 git push origin master # 显示远程仓库信息,比如:url(不涉及网络通信) git remote -v [代码]
2019-03-19 - “共享拍照”请求微信集成这个功能。(个人原创架构)
生活中爱美自拍、旅游远行、各种聚会,经常困扰我们的是如何拍出一张理想的照片。“共享拍照”可以让您不受空间限制获得理想照片,或许还能成就一段美丽的姻缘。废话不多说,实例说明。 场景应用 1、美女在某建筑前摆好造型,自拍模式无法展现建筑全貌,此时可以打开“共享拍照”,发出请求,远处帅哥接受挑战,顺利完成任务,获得美女加分。 [图片] 2、一群小伙伴历尽千辛万苦爬到某处,无奈拍张合影这么难。 一家人欢快出游,泛舟湖中,真心希望岸边好心人拍张照片。 [图片][图片] 3、“共享拍照”可以让附近彼此陌生的两个人建立联系,共享对方手机,互助拍照,各取所需。 [图片] 功能优势:不限空间限制,共享手机硬件资源。一些高档手机有多个摄像头,配合各种互动,可拓展业务各类。 技术构想与实现: 方案1:需要手机端安装“共享拍照”APP(这一方案实施比较困难,花费巨大。) 方案2:集成到微信或者MIUI小米系统、华为手机系统等。(需要人家同意) 现在简单介绍一下它的工作原理,假如新版微信集成了这个“共享拍照”插件,场景1的美女打开这个插件呼叫附近的人,附近的人收到呼叫请求后,可以查看到呼叫者个人公开资料(比如长相),并显示双方地图方位、相对距离等。两人确定连接后,双方根据软件提示的方向,俯仰角度确定能够目测到对方。 [图片] 接下来主拍方(附近的人)接受图像传输协议,听从受拍方指挥。 受拍方(美女)可在手机端预览图像,并发出角度调整指令。 主拍方(附近的人)根据软件提示调整方向角度,并保持稳定。 受拍方(美女)可随时按下拍照键,获得主拍方(附近的人)的手机镜头图像。并对主拍方评论打分、打赏等。 主拍方(附近的人)亦可请求互换角色。 技术风险:与其说“共享拍照”,实则“互助拍照”,依赖网络,距离较近时可以采用wlan或者蓝牙技术。隐私保护,账户安全问题等 前景展望: 只需一台手机,不出门就可以全世界拍照,网店场景应用等。 声明:“共享拍照”由个人原创,架构方案知识产权应得到保护。转载请注明出处。谢谢!
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 - 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 - 云开发,实现【最近搜索】和【大家在搜】的存储和读取逻辑
小程序【搜索】功能是很常见的,在开发公司的一个电商小程序的时候想尝试使用“云开发”做系统的后端,而首页顶端就有个搜索框,所以第一步想先解决【搜索】的一些前期工作:【最近搜索】和【大家在搜】关键词的存储和读取逻辑。 效果图如下: [图片] 效果视频请点击链接查看: https://v.vuevideo.net/share/post/-2263996935468719531 或者微信扫码观看: [图片] 为什么需要这两个功能? 【最近搜索】:可以帮助用户快速选择历史搜索记录(我这里只保存10个),搜索相同内容时减少打字操作,更加人性化; 【大家在搜】:这部分的数据可以是从所有用户的海量搜索中提取的前n名,也可以是运营者想给用户推荐的商品关键词,前者是真实的“大家在搜”,后者更像是一种推广。 具体实现 流程图: [图片] 可以结合效果图看流程图,用户触发操作有三种形式: 输入关键词,点击搜索按钮; 点击【大家在搜】列举的关键词; 点击【最近搜索】的关键词 这里发现1和2是一样的逻辑,而3更简单,所以把1和2归为一类(左边流程图),3单独一类(右边流程图) 两个流程图里都含有相同的片段(图中红色背景块部分):“找出这条记录”->“更新其时间”。故这部分可以封装成函数:updateTimeStamp()。 更新时间指的是更新该条记录的时间戳,每条记录包含以下字段:_id、keyword、openid、timeStamp。其中_id是自动生成的,openid是当前用户唯一标识,意味着关键词记录与人是绑定的,每个用户只能看到自己的搜索记录,timeStamp是触发搜索动作时的时间戳,记录时间是为了排序,即最近搜索的排在最前面。 代码结构: [图片] 云函数: cloudfunctions / searchHistory / index.js [代码]const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() exports.main = async (event, context) => { try { switch (event.type) { // 根据openid获取记录 case 'getByOpenid': return await db.collection('searchHistory').where({ openid: event.openid }).orderBy('timeStamp', 'desc').limit(10).get() break // 添加记录 case 'add': return await db.collection('searchHistory').add({ // data 字段表示需新增的 JSON 数据 data: { openid: event.openid, timeStamp: event.timeStamp, keyword: event.keyword } }) break // 根据openid和keyword找出记录,并更新其时间戳 case 'updateOfOpenidKeyword': return await db.collection('searchHistory').where({ openid: event.openid, keyword: event.keyword }).update({ data: { timeStamp: event.timeStamp } }) break // 根据openid和keyword能否找出这条记录(count是否大于0) case 'canFindIt': return await db.collection('searchHistory').where({ openid: event.openid, keyword: event.keyword }).count() break // 根据openid查询当前记录条数 case 'countOfOpenid': return await db.collection('searchHistory').where({ openid: event.openid }).count() break // 找出该openid下最早的一条记录 case 'getEarliestOfOpenid': return await db.collection('searchHistory').where({ openid: event.openid }).orderBy('timeStamp', 'asc').limit(1).get() break // 根据最早记录的id,删除这条记录 case 'removeOfId': return await db.collection('searchHistory').where({ _id: event._id }).remove() break // 删除该openid下的所有记录 case 'removeAllOfOpenid': return await db.collection('searchHistory').where({ openid: event.openid }).remove() break } } catch (e) { console.error('云函数【searchHistory】报错!!!', e) } } [代码] cloudfunctions / recommendedKeywords / index.js [代码]const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() exports.main = async (event, context) => await db.collection('recommendedKeywords') .orderBy('level', 'asc') .limit(10) .get() [代码] cloudfunctions / removeExpiredSearchHistory / config.json [代码]// 该定时触发器被设置成每天晚上23:00执行一次 index.js (删除3天前的数据) { "triggers": [ { "name": "remove expired search history", "type": "timer", "config": "0 0 23 * * * *" } ] } [代码] cloudfunctions / removeExpiredSearchHistory / index.js [代码]const cloud = require('wx-server-sdk') cloud.init() const db = cloud.database() const _ = db.command let timeStamp = new Date().getTime() // 删除3天前的数据 let duration = timeStamp - 1000 * 60 * 60 * 24 * 3 exports.main = async (event, context) => { try { let arr = await db.collection('searchHistory').where({ timeStamp: _.lt(duration) }).get() let idArr = arr.data.map(v => v._id) console.log('idArr=', idArr) for (let i = 0; i < idArr.length; i++) { await db.collection('searchHistory').where({ _id: idArr[i] }).remove() } } catch (e) { console.error(e) } } [代码] search.js 关键代码: // 输入关键词,点击搜索 [代码] tapSearch: function () { let that = this if (that.data.openid) { // 只为已登录的用户记录搜索历史 that.tapSearchOrRecommended(that.data.keyword, that.data.openid) } else { // 游客直接跳转 wx.navigateTo({ url: `../goods-list/goods-list?keyword=${that.data.keyword}`, }) } }, [代码] // 点击推荐的关键词 [代码]tabRecommended: function (e) { let that = this let keyword = e.currentTarget.dataset.keyword that.setData({ keyword }) if (that.data.openid) { // 只为已登录的用户记录搜索历史 that.tapSearchOrRecommended(keyword, that.data.openid) } else { // 游客直接跳转 wx.navigateTo({ url: `../goods-list/goods-list?keyword=${keyword}`, }) } }, [代码] // 点击历史关键词 [代码]tabHistory: function (e) { let that = this let keyword = e.currentTarget.dataset.keyword wx.navigateTo({ url: `../goods-list/goods-list?keyword=${keyword}`, }) that.updateTimeStamp(keyword, that.data.openid) }, [代码] // 获取历史记录和推荐 [代码]getHistoryAndRecommended: function () { let that = this try { // 只为已登录的用户获取搜索历史 if (that.data.openid) { let searchHistoryNeedUpdate = app.globalData.searchHistoryNeedUpdate const searchHistory = wx.getStorageSync('searchHistory') // 如果本地有历史并且还没有更新的历史 if (searchHistory && !searchHistoryNeedUpdate) { console.log('本地有搜索历史,暂时不需要更新') that.setData({ searchHistory }) } else { console.log('需要更新(或者本地没有搜索历史)') wx.cloud.callFunction({ name: 'searchHistory', data: { type: 'getByOpenid', openid: that.data.openid }, success(res) { console.log('云函数获取关键词记录成功') let searchHistory = [] for (let i = 0; i < res.result.data.length; i++) { searchHistory.push(res.result.data[i].keyword) } that.setData({ searchHistory }) wx.setStorage({ key: 'searchHistory', data: searchHistory, success(res) { console.log('searchHistory本地存储成功') // wx.stopPullDownRefresh() app.globalData.searchHistoryNeedUpdate = false }, fail: console.error }) }, fail: console.error }) } } // 获取推荐关键词 wx.cloud.callFunction({ name: 'recommendedKeywords', success(res) { console.log('云函数获取推荐关键词记录成功') let recommendedKeywords = [] for (let i = 0; i < res.result.data.length; i++) { recommendedKeywords.push(res.result.data[i].keyword) } that.setData({ recommendedKeywords }) }, fail: console.error }) } catch (e) { fail: console.error } }, [代码] // 添加该条新记录(tapSearchOrRecommended()要用到它两次) [代码]addRecord: function (keyword, timeStamp) { let that = this // 【添加】 wx.cloud.callFunction({ name: 'searchHistory', data: { type: 'add', openid: that.data.openid, keyword, timeStamp }, success(res) { console.log('云函数添加关键词成功') app.globalData.searchHistoryNeedUpdate = true }, fail: console.error }) }, [代码] // 根据openid和keyword找出记录,并更新其时间戳 [代码]updateTimeStamp: function (keyword, openid) { this.setData({ keyword }) let timeStamp = new Date().getTime() wx.cloud.callFunction({ name: 'searchHistory', data: { type: 'updateOfOpenidKeyword', openid, keyword, timeStamp, }, success(res) { console.log('云函数更新关键词时间戳成功') app.globalData.searchHistoryNeedUpdate = true }, fail: console.error }) }, [代码] // 输入关键词,点击搜索或者点击推荐的关键词 [代码]tapSearchOrRecommended: function (keyword, openid) { let that = this if (!keyword) { wx.showToast({ icon: 'none', title: '请输入商品关键词', }) setTimeout(function () { that.setData({ isFocus: true }) }, 1500) return false } wx.navigateTo({ url: `../goods-list/goods-list?keyword=${keyword}`, }) let timeStamp = new Date().getTime() // 【根据openid和keyword能否找出这条记录(count是否大于0)】 wx.cloud.callFunction({ name: 'searchHistory', data: { type: 'canFindIt', openid, keyword }, success(res) { console.log('res.result.total=', res.result.total) if (res.result.total === 0) { // 集合中没有 // 【根据openid查询当前记录条数】 wx.cloud.callFunction({ name: 'searchHistory', data: { type: 'countOfOpenid', openid }, success(res) { // 记录少于10条 if (res.result.total < 10) { // 【添加】 that.addRecord(keyword, timeStamp) } else { // 【找出该openid下最早的一条记录】 wx.cloud.callFunction({ name: 'searchHistory', data: { type: 'getEarliestOfOpenid', openid }, success(res) { console.log('云函数找出最早的一条关键词成功', res.result.data[0]) let _id = res.result.data[0]._id // 【根据最早记录的id,删除这条记录】 wx.cloud.callFunction({ name: 'searchHistory', data: { type: 'removeOfId', _id }, success(res) { console.log('云函数删除最早的一条关键词成功') // 【添加】 that.addRecord(keyword, timeStamp) }, fail: console.error }) }, fail: console.error }) } }, fail: console.error }) } else { // 【根据openid和keyword找出记录,并更新其时间戳】 that.updateTimeStamp(keyword, openid) } }, fail: console.error }) } [代码]
2019-03-28 - 用150行代码实现Vuex 80%的功能
作者: 殷荣桧@腾讯 本文地址,欢迎查看 本文github仓库代码地址,欢迎star,谢谢。 如果你对自己用少量代码实现各个框架感兴趣,那下面这些你都可以一看: build-your-own-react build-your-own-flux build-your-own-redux 目录: 一.完成最简单的通过vuex定义全局变量,在任何一个页面可以通过this.$store.state.count可以直接使用 二.vuex中的getter方法的实现 三.mutation和commit方法的实现 四.actions和dispatch方法的实现 五.module方法的实现 六.实现:Vue.use(Vuex) 先来看一下用自己实现的的vuex替代真实的vuex的效果,看看能否正常运行,有没有报错: [图片] 从运行结果来看,运行正常,没有问题。接下来看看一步一步实现的过程: <h4>一. 完成最简单的通过vuex定义全局变量,在任何一个页面可以通过this.$store.state.count可以直接使用</h4> main.js代码如下: [代码]let store = new Vuex.Store({ state: { count: 0 } }, Vue); new Vue({ store, render: h => h(App), }).$mount('#app') [代码] store.js的代码如下: [代码]export class Store { constructor(options = {}, Vue) { this.options = options; Vue.mixin({ beforeCreate: vuexInit }); } get state () { return this.options.state; } } function vuexInit () { const options = this.$options if (options.store) { // 组件内部设定了store,则优先使用组件内部的store this.$store = typeof options.store === 'function' ? options.store() : options.store } else if (options.parent && options.parent.$store) { // 组件内部没有设定store,则从根App.vue下继承$store方法 this.$store = options.parent.$store } } [代码] 界面代码如下: [代码]<script> export default { name: 'app', created() { console.log('打印出this.$store.state.count的结果',this.$store.state.count); }, } </script> [代码] 运行结果: 成功打印出this.$store.state.count的值为0 <h4>二. vuex中的getter方法的实现</h4> main.js代码如下: [代码]let store = new Vuex.Store({ state: { count: 0 }, getters: { getStatePlusOne(state) { return state.count + 1 } } }, Vue); new Vue({ store, render: h => h(App), }).$mount('#app') [代码] store.js的代码如下: [代码]export class Store { constructor(options = {}, Vue) { this.options = options; this.getters = {} Vue.mixin({ beforeCreate: vuexInit }); forEachValue(options.getters, (getterFn, getterName) => { registerGetter(this, getterName, getterFn); }) } get state() { return this.options.state; } } function registerGetter(store, getterName, getterFn) { Object.defineProperty(store.getters, getterName, { get: () => { return getterFn(store.state) } }) } // 将对象中的每一个值放入到传入的函数中作为参数执行 function forEachValue(obj, fn) { Object.keys(obj).forEach(key => fn(obj[key], key)); } function vuexInit() { const options = this.$options if (options.store) { // 组件内部设定了store,则优先使用组件内部的store this.$store = typeof options.store === 'function' ? options.store() : options.store } else if (options.parent && options.parent.$store) { // 组件内部没有设定store,则从根App.vue下继承$store方法 this.$store = options.parent.$store } } [代码] 界面代码如下: [代码]<script> export default { name: 'app', created() { console.log('打印出this.$store.getters.getStatePlusOne的结果',this.$store.getters.getStatePlusOne); }, } </script> [代码] 运行结果: 成功打印出this.$store.getters.getStatePlusOne的值为1 <h4>三. mutation和commit方法的实现</h4> main.js代码如下: [代码]let store = new Vuex.Store({ state: { count: 0 }, mutations: { incrementFive(state) { // console.log('初始state', JSON.stringify(state)); state.count = state.count + 5; } }, getters: { getStatePlusOne(state) { return state.count + 1 } } }, Vue); [代码] store.js的代码如下: [代码]export class Store { constructor(options = {}, Vue) { Vue.mixin({ beforeCreate: vuexInit }) this.options = options; this.getters = {}; this.mutations = {}; const { commit } = this; this.commit = (type) => { return commit.call(this, type); } forEachValue(options.getters, (getterFn, getterName) => { registerGetter(this, getterName, getterFn); }); forEachValue(options.mutations, (mutationFn, mutationName) => { registerMutation(this, mutationName, mutationFn) }); this._vm = new Vue({ data: { state: options.state } }); } get state() { // return this.options.state; // 无法完成页面中的双向绑定,所以改用this._vm的形式 return this._vm._data.state; } commit(type) { this.mutations[type](); } } function registerMutation(store, mutationName, mutationFn) { store.mutations[mutationName] = () => { mutationFn.call(store, store.state); } } function registerGetter(store, getterName, getterFn) { Object.defineProperty(store.getters, getterName, { get: () => { return getterFn(store.state) } }) } // 将对象中的每一个值放入到传入的函数中作为参数执行 function forEachValue(obj, fn) { Object.keys(obj).forEach(key => fn(obj[key], key)); } function vuexInit() { const options = this.$options if (options.store) { // 组件内部设定了store,则优先使用组件内部的store this.$store = typeof options.store === 'function' ? options.store() : options.store } else if (options.parent && options.parent.$store) { // 组件内部没有设定store,则从根App.vue下继承$store方法 this.$store = options.parent.$store } } [代码] 界面代码如下: [代码]<script> export default { name: 'app', created() { console.log('打印出this.$store.getters.getStatePlusOne的结果',this.$store.getters.getStatePlusOne); }, mounted() { setTimeout(() => { this.$store.commit('incrementFive'); console.log('store state自增5后的结果', this.$store.state.count); }, 2000); }, computed: { count() { return this.$store.state.count; } } } </script> [代码] 运行结果:成功在2秒之后输出count自增5后的结果5 <h4>四. actions和dispatch方法的实现</h4> main.js代码如下: [代码]let store = new Vuex.Store({ state: { count: 0 }, actions: { countPlusSix(context) { context.commit('plusSix'); } }, mutations: { incrementFive(state) { // console.log('初始state', JSON.stringify(state)); state.count = state.count + 5; }, plusSix(state) { state.count = state.count + 6; } }, getters: { getStatePlusOne(state) { return state.count + 1 } } }, Vue); [代码] store.js的代码如下: [代码]export class Store { constructor(options = {}, Vue) { Vue.mixin({ beforeCreate: vuexInit }) this.options = options; this.getters = {}; this.mutations = {}; this.actions = {}; const { dispatch, commit } = this; this.commit = (type) => { return commit.call(this, type); } this.dispatch = (type) => { return dispatch.call(this, type); } forEachValue(options.actions, (actionFn, actionName) => { registerAction(this, actionName, actionFn); }); forEachValue(options.getters, (getterFn, getterName) => { registerGetter(this, getterName, getterFn); }); forEachValue(options.mutations, (mutationFn, mutationName) => { registerMutation(this, mutationName, mutationFn) }); this._vm = new Vue({ data: { state: options.state } }); } get state() { // return this.options.state; // 无法完成页面中的双向绑定,所以改用this._vm的形式 return this._vm._data.state; } commit(type) { this.mutations[type](); } dispatch(type) { return this.actions[type](); } } function registerMutation(store, mutationName, mutationFn) { store.mutations[mutationName] = () => { mutationFn.call(store, store.state); } } function registerAction(store, actionName, actionFn) { store.actions[actionName] = () => { actionFn.call(store, store) } } function registerGetter(store, getterName, getterFn) { Object.defineProperty(store.getters, getterName, { get: () => { return getterFn(store.state) } }) } // 将对象中的每一个值放入到传入的函数中作为参数执行 function forEachValue(obj, fn) { Object.keys(obj).forEach(key => fn(obj[key], key)); } function vuexInit() { const options = this.$options if (options.store) { // 组件内部设定了store,则优先使用组件内部的store this.$store = typeof options.store === 'function' ? options.store() : options.store } else if (options.parent && options.parent.$store) { // 组件内部没有设定store,则从根App.vue下继承$store方法 this.$store = options.parent.$store } } [代码] 界面代码如下: [代码]export default { name: 'app', created() { console.log('打印出this.$store.getters.getStatePlusOne的结果',this.$store.getters.getStatePlusOne); }, mounted() { setTimeout(() => { this.$store.commit('incrementFive'); console.log('store state自增5后的结果', this.$store.state.count); }, 2000); setTimeout(() => { this.$store.dispatch('countPlusSix'); console.log('store dispatch自增6后的结果', this.$store.state.count); }, 3000); }, computed: { count() { return this.$store.state.count; } } } [代码] 运行结果: 成功在3秒之后dipatch自增6输出11 <h4>五. module方法的实现</h4> main.js代码如下: [代码]const pageA = { state: { count: 100 }, mutations: { incrementA(state) { state.count++; } }, actions: { incrementAAction(context) { context.commit('incrementA'); } } } let store = new Vuex.Store({ modules: { a: pageA }, state: { count: 0 }, actions: { countPlusSix(context) { context.commit('plusSix'); } }, mutations: { incrementFive(state) { // console.log('初始state', JSON.stringify(state)); state.count = state.count + 5; }, plusSix(state) { state.count = state.count + 6; } }, getters: { getStatePlusOne(state) { return state.count + 1 } } }, Vue); [代码] store.js的代码如下: [代码]let _Vue; export class Store { constructor(options = {}, Vue) { _Vue = Vue Vue.mixin({ beforeCreate: vuexInit }) this.getters = {}; this._mutations = {}; // 在私有属性前加_ this._wrappedGetters = {}; this._actions = {}; this._modules = new ModuleCollection(options) const { dispatch, commit } = this; this.commit = (type) => { return commit.call(this, type); } this.dispatch = (type) => { return dispatch.call(this, type); } const state = options.state; const path = []; // 初始路径给根路径为空 installModule(this, state, path, this._modules.root); this._vm = new Vue({ data: { state: state } }); } get state() { // return this.options.state; // 无法完成页面中的双向绑定,所以改用this._vm的形式 return this._vm._data.state; } commit(type) { this._mutations[type].forEach(handler => handler()); } dispatch(type) { return this._actions[type][0](); } } class ModuleCollection { constructor(rawRootModule) { this.register([], rawRootModule) } register(path, rawModule) { const newModule = { _children: {}, _rawModule: rawModule, state: rawModule.state } if (path.length === 0) { this.root = newModule; } else { const parent = path.slice(0, -1).reduce((module, key) => { return module._children(key); }, this.root); parent._children[path[path.length - 1]] = newModule; } if (rawModule.modules) { forEachValue(rawModule.modules, (rawChildModule, key) => { this.register(path.concat(key), rawChildModule); }) } } } function installModule(store, rootState, path, module) { if (path.length > 0) { const parentState = rootState; const moduleName = path[path.length - 1]; _Vue.set(parentState, moduleName, module.state) } const context = { dispatch: store.dispatch, commit: store.commit, } const local = Object.defineProperties(context, { getters: { get: () => store.getters }, state: { get: () => { let state = store.state; return path.length ? path.reduce((state, key) => state[key], state) : state } } }) if (module._rawModule.actions) { forEachValue(module._rawModule.actions, (actionFn, actionName) => { registerAction(store, actionName, actionFn, local); }); } if (module._rawModule.getters) { forEachValue(module._rawModule.getters, (getterFn, getterName) => { registerGetter(store, getterName, getterFn, local); }); } if (module._rawModule.mutations) { forEachValue(module._rawModule.mutations, (mutationFn, mutationName) => { registerMutation(store, mutationName, mutationFn, local) }); } forEachValue(module._children, (child, key) => { installModule(store, rootState, path.concat(key), child) }) } function registerMutation(store, mutationName, mutationFn, local) { const entry = store._mutations[mutationName] || (store._mutations[mutationName] = []); entry.push(() => { mutationFn.call(store, local.state); }); } function registerAction(store, actionName, actionFn, local) { const entry = store._actions[actionName] || (store._actions[actionName] = []) entry.push(() => { return actionFn.call(store, { commit: local.commit, state: local.state, }) }); } function registerGetter(store, getterName, getterFn, local) { Object.defineProperty(store.getters, getterName, { get: () => { return getterFn( local.state, local.getters, store.state ) } }) } // 将对象中的每一个值放入到传入的函数中作为参数执行 function forEachValue(obj, fn) { Object.keys(obj).forEach(key => fn(obj[key], key)); } function vuexInit() { const options = this.$options if (options.store) { // 组件内部设定了store,则优先使用组件内部的store this.$store = typeof options.store === 'function' ? options.store() : options.store } else if (options.parent && options.parent.$store) { // 组件内部没有设定store,则从根App.vue下继承$store方法 this.$store = options.parent.$store } } [代码] 主界面代码如下: [代码]<template> <div id="app"> ==============主页================<br> 主页数量count为: {{count}}<br> pageA数量count为: {{countA}}<br> ==========以下为PageA内容==========<br> <page-a></page-a> </div> </template> <script> import pageA from './pageA'; export default { name: 'app', components: { pageA }, created() { console.log('打印出this.$store.getters.getStatePlusOne的结果',this.$store.getters.getStatePlusOne); }, mounted() { setTimeout(() => { this.$store.commit('incrementFive'); console.log('store state自增5后的结果', this.$store.state.count); }, 2000); setTimeout(() => { this.$store.dispatch('countPlusSix'); console.log('store dispatch自增6后的结果', this.$store.state.count); }, 3000); }, computed: { count() { return this.$store.state.count; }, countA() { return this.$store.state.a.count; } } } </script> [代码] pageA页面如下: [代码]<template> <div> 页面A被加载 </div> </template> <script> export default { name: 'pageA', mounted() { setTimeout(() => { this.$store.dispatch('incrementAAction'); }, 5000) }, } </script> [代码] 运行结果: 在5秒后A页面触发incrementAAction,主界面中的countA变化为101,成功 <span style=“color: red”>自此:基本用了150行左右的代码实现了vuex 80%左右的功能了,其中还有namespace等不能够使用,其他基本都和源代码语法相同,如果你有兴趣仔细再看看,可以移步github仓库代码,代码是建立在阅读了vuex源代码之后写的,所以看完了本文的代码,再去看vuex的代码,相信你一定会一目了然</span> <h4>六. 实现:Vue.use(Vuex)</h4> 最后为了和vuex源代码做到最相似,同样使用Vue.use(Vuex),使用如下的代码进行实现: [代码]export function install(_Vue) { Vue = _Vue; Vue.mixin({ beforeCreate: function vuexInit() { const options = this.$options; if (options.store) { this.$store = options.store; } else if (options.parent && options.parent.$store) { this.$store = options.parent.$store; } } }) } [代码] 部门正在招新,为腾讯企业产品部,隶属CSIG事业群。福利不少,薪水很高,就等你来。有兴趣请猛戳下方两个链接。 https://www.lagou.com/jobs/5210396.html https://www.zhipin.com/job_detail/2876d4cc2cdebe2c1XNz2NW0ElU~.html 参考资料: Build a Vuex Module How does a minimal Vuex implementation look like? 从0开始写一个自己的Vuex vuex 源码:如何实现一个简单的 vuex Vue 源码(三) —— Vuex 浅谈Vue.use Vuex官方文档 vuex Github仓库
2019-03-27 - 小程序自定义组件知多少
自定义组件 why 代码的复用 在起初小程序只支持 Page 的时候,就会有这样蛋疼的问题:多个页面有相同的组件,每个页面都要复制粘贴一遍,每次改动都要全局搜索一遍,还说不准哪里改漏了就出翔了。 组件化设计 在前端项目中,组件化是很常见的方式,某块通用能力的抽象和设计,是一个必备的技能。组件的管理、数据的管理、应用状态的管理,这些在我们设计的过程中都是需要去思考的。当然你也可以说我就堆代码就好了,不过一个真正的码农是不允许自己这么随便的! 所以,组件化是现代前端必须掌握的生存技能! 自定义组件的实现 一切都从 Virtual DOM 说起 前面《解剖小程序的 setData》有讲过,基于小程序的双线程设计,视图层(Webview 线程)和逻辑层(JS 线程)之间通信(表现为 setData),是基于虚拟 DOM 来实现数据通信和模版更新的。 自定义组件一样的双线程,所以一样滴基于 Virtual DOM 来实现通信。那在这里,Virtual DOM 的一些基本知识(包括生成 VD 对象、Diff 更新等),就不过多介绍啦~ Shadow DOM 模型 基于 Virtual DOM,我们知道在这样的设计里,需要一个框架来支撑维护整个页面的节点树相关信息,包括节点的属性、事件绑定等。在小程序里,Exparser 承担了这个角色。 前面《关于小程序的基础库》也讲过,Exparser 的主要特点包括: 基于 Shadow DOM 模型 可在纯 JS 环境中运行 Shadow DOM 是什么呢,它就是我们在写代码时候写的自定义组件、内置组件、原生组件等。Shadow DOM 为 Web 组件中的 DOM 和 CSS 提供了封装。Shadow DOM 使得这些东西与主文档的 DOM 保持分离。 简而言之,Shadow DOM 是一个 HTML 的新规范,其允许开发者封装 HTML 组件(类似 vue 组件,将 html,css,js 独立部分提取)。 例如我们定义了一个自定义组件叫[代码]<my-component>[代码],你在开发者工具可以见到: [图片] [代码]#shadow-root[代码]称为影子根,DOM 子树的根节点,和文档的主要 DOM 树分开渲染。可以看到它在[代码]<my-component>[代码]里面,换句话说,[代码]#shadow-root[代码]寄生在[代码]<my-component>[代码]上。[代码]#shadow-root[代码]可以嵌套,形成节点树,即称为影子树(Shadow Tree)。 像这样: [图片] Shadow Tree 拼接 既然组件是基于 Shadow DOM,那组件的嵌套关系,其实也就是 Shadow DOM 的嵌套,也可称为 Shadow Tree 的拼接。 Shadow Tree 拼接是怎么做的呢?一切又得从模版引擎讲起。 我们知道,Virtual DOM 机制会将节点解析成一个对象,那这个对象要怎么生成真正的 DOM 节点呢?数据变更又是怎么更新到界面的呢?这大概就是模版引擎做的事情了。 《前端模板引擎》里有详细描述模版引擎的机制,通常来说主要有这些: DOM 节点的创建和管理:[代码]appendChild[代码]/[代码]insertBefore[代码]/[代码]removeChild[代码]/[代码]replaceChild[代码]等 DOM 节点的关系(嵌套的处理):[代码]parentNode[代码]/[代码]childNodes[代码] 通常创建后的 DOM 节点会保存一个映射,在更新的时候取到映射,然后进行处理(通常包括替换节点、改变内容[代码]innerHTML[代码]、移动删除新增节点、修改节点属性[代码]setAttribute[代码]) 在上面的图我们也可以看到,在 Shadow Tree 拼接的过程中,有些节点并不会最终生成 DOM 节点,例如[代码]<slot>[代码]这种。 但是,常用的前端模版引擎,能直接用在小程序里吗? 双线程的难题 自定义组件渲染流程 双线程的设计,给小程序带来了很多便利,安全性管控力都拥有了,当然什么鬼东西都可以比作一把双刃剑,双线程也不例外。 我们知道,小程序分为 Webview 和 JS 双线程,逻辑层里是没法拿到真正的 DOM 节点,也没法随便动态变更页面的。那在这种情况下,我们要怎么去使用映射来更新模版呢(因为我们压根拿不到 Webview 节点的映射)? 所以在双线程下,其实两个线程都需要保存一份节点信息。这份节点信息怎么来的呢?其实就是我们需要在创建组件的时候,通过事件通知的方式,分别在逻辑层和视图层创建一份节点信息。 同时,视图层里的组件是有层级关系的,但是 JS 里没有怎么办?为了维护好父子嵌套等节点关系,所以我们在 逻辑层也需要维护一棵 Shadow Tree。 那么我们自定义组件的渲染流程大概是: 组件创建。 逻辑层:先是 wxml + js 生成一个 JS 对象(因为需要访问组件实例 this 呀),然后是 JS 其中节点部分生成 Virtual DOM,拼接 Shadow Tree 什么的,最后通过底层通信通知到 视图层 视图层:拿到节点信息,然后吭哧吭哧开始创建 Shadow DOM,拼接 Shadow Tree 什么的,最后生成真实 DOM,并保留下映射关系 组件更新。 这时候我们知道,不管是逻辑层,还是视图层,都维护了一份 Shadow Tree,要怎么保证他们之间保持一致呢? 让 JS 和 Webview 的组件保持一致 为了让两边的 Shadow Tree 保持一致,可以使用同步队列来传递信息。(这样就不会漏掉啦) 同步队列可以,每次变动我们就往队列里塞东西就好了。不过这样还会有个问题,我们也知道 setData 其实在实际项目里是使用比较频繁的,要是像 Component 的 observer 里做了 setData 这类型的操作,那不是每次变动会导致一大堆的 setDate?这样通信效率会很低吧? 所以,其实可以把一次操作里的所有 setData 都整到一次通信里,通过排序保证好顺序就好啦。 Page 和 Component Component 是 Page 的超集 事实上,小程序的页面也可以视为自定义组件。因而,页面也可以使用[代码]Component[代码]构造器构造,拥有与普通组件一样的定义段与实例方法。但此时要求对应 json 文件中包含[代码]usingComponents[代码]定义段。 来自官方文档-Component 所以,基于 Component 是 Page 的超集,那么其实组件的渲染流程、方式,其实跟页面没多大区别,应该可以一个方式去理解就差不多啦。 页面渲染 既然页面就是组件,那其实页面的渲染流程跟组件的渲染流程基本保持一致。 视图层渲染,可以参考7.4 视图层渲染说明。 结束语 其实很多新框架新工具出来的时候,经常会让人眼前一亮,觉得哇好厉害,哇好高大上。 但其实更多时候,我们需要挖掘新事物的核心,其实大多数都是在原有的事物上增加了个新视角,从不一样的视角看,看到的就不一样了呢。作为一名码农,我们要看到不变的共性,变化的趋势。
2019-04-01 - CSS3 Animation动画的十二原则
作为前端的设计师和工程师,我们用 CSS 去做样式、定位并创建出好看的网站。我们经常用 CSS 去添加页面的运动过渡效果甚至动画,但我们经常做的不过如此。 [代码] 动效是一个有助于访客和用户理解我们设计的强有力工具。这里有些原则能最大限度地应用在我们的工作中。 迪士尼经过基础工作练习的长时间累积,在 1981 年出版的 The Illusion of Life: Disney Animation 一书中发表了动画的十二个原则 ([] (https://en.wikipedia.org/wiki/12_basic_principles_of_animation)) 。这些原则描述了动画能怎样用于让观众相信自己沉浸在现实世界中。 [代码] 在本文中,我会逐个介绍这十二个原则,并讨论它们怎样运用在网页中。你能在 Codepen 找到它们[] (https://codepen.io/collection/AxKOdY/)。 挤压和拉伸 (Squash and stretch) [图片] 这是物体存在质量且运动时质量保持不变的概念。当一个球在弹跳时,碰击到地面会变扁,恢复的时间会越来越短。 [代码] 创建对象的时候最有用的方法是参照实物,比如人、时钟和弹性球。 当它和网页元件一起工作时可能会忽略这个原则。DOM 对象不一定和实物相关,它会按需要在屏幕上缩放。例如,一个按钮会变大并变成一个信息框,或者错误信息会出现和消失。 尽管如此,挤压和伸缩效果可以为一个对象增加实物的感觉。甚至一些形状上的小变化就可以创造出细微但抢眼的效果。 HTML [代码] [代码] <h1>Principle 1: Squash and stretch</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle one"> <div class="shape"></div> <div class="surface"></div> </article> [代码] CSS [代码].one .shape { animation: one 4s infinite ease-out; } .one .surface { background: #000; height: 10em; width: 1em; position: absolute; top: calc(50% - 4em); left: calc(50% + 10em); } @keyframes one { 0%, 15% { opacity: 0; } 15%, 25% { transform: none; animation-timing-function: cubic-bezier(1,-1.92,.95,.89); width: 4em; height: 4em; top: calc(50% - 2em); left: calc(50% - 2em); opacity: 1; } 35%, 45% { transform: translateX(8em); height: 6em; width: 2em; top: calc(50% - 3em); animation-timing-function: linear; opacity: 1; } 70%, 100% { transform: translateX(8em) translateY(5em); height: 6em; width: 2em; top: calc(50% - 3em); opacity: 0; } } body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 预备动作 (Anticipation) [图片] 运动不倾向于突然发生。在现实生活中,无论是一个球在掉到桌子前就开始滚动,或是一个人屈膝准备起跳,运动通常有着某种事先的累积。 [代码] 我们能用它去让我们的过渡动画显得更逼真。预备动作可以是一个细微的反弹,帮人们理解什么对象将在屏幕中发生变化并留下痕迹。 例如,悬停在一个元件上时可以在它变大前稍微缩小,在初始列表中添加额外的条目来介绍其它条目的移除方法。 [代码] HTML [代码]<h1>Principle 2: Anticipation</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle two"> <div class="shape"></div> <div class="surface"></div> </article> [代码] CSS [代码].two .shape { animation: two 5s infinite ease-out; transform-origin: 50% 7em; } .two .surface { background: #000; width: 8em; height: 1em; position: absolute; top: calc(50% + 4em); left: calc(50% - 3em); } @keyframes two { 0%, 15% { opacity: 0; transform: none; } 15%, 25% { opacity: 1; transform: none; animation-timing-function: cubic-bezier(.5,.05,.91,.47); } 28%, 38% { transform: translateX(-2em); } 40%, 45% { transform: translateX(-4em); } 50%, 52% { transform: translateX(-4em) rotateZ(-20deg); } 70%, 75% { transform: translateX(-4em) rotateZ(-10deg); } 78% { transform: translateX(-4em) rotateZ(-24deg); opacity: 1; } 86%, 100% { transform: translateX(-6em) translateY(4em) rotateZ(-90deg); opacity: 0; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 演出布局 (Staging) [图片] 演出布局是确保对象在场景中得以聚焦,让场景中的其它对象和视觉在主动画发生的地方让位。这意味着要么把主动画放到突出的位置,要么模糊其它元件来让用户专注于看他们需要看的东西。 [代码] 在网页方面,一种方法是用 model 覆盖在某些内容上。在现有页面添加一个遮罩并把那些主要关注的内容前置展示。 另一种方法是用动作。当很多对象在运动,你很难知道哪些值得关注。如果其它所有的动作停止,只留一个在运动,即使动得很微弱,这都可以让对象更容易被察觉。 [代码] 还有一种方法是做一个晃动和闪烁的按钮来简单地建议用户比如他们可能要保存文档。屏幕保持静态,所以再细微的动作也会突显出来。 HTML [代码]<h1>Principle 3: Staging</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle three"> <div class="shape a"></div> <div class="shape b"></div> <div class="shape c"></div> </article> [代码] CSS [代码].three .shape.a { transform: translateX(-12em); } .three .shape.c { transform: translateX(12em); } .three .shape.b { animation: three 5s infinite ease-out; transform-origin: 0 6em; } .three .shape.a, .three .shape.c { animation: threeb 5s infinite linear; } @keyframes three { 0%, 10% { transform: none; animation-timing-function: cubic-bezier(.57,-0.5,.43,1.53); } 26%, 30% { transform: rotateZ(-40deg); } 32.5% { transform: rotateZ(-38deg); } 35% { transform: rotateZ(-42deg); } 37.5% { transform: rotateZ(-38deg); } 40% { transform: rotateZ(-40deg); } 42.5% { transform: rotateZ(-38deg); } 45% { transform: rotateZ(-42deg); } 47.5% { transform: rotateZ(-38deg); animation-timing-function: cubic-bezier(.57,-0.5,.43,1.53); } 58%, 100% { transform: none; } } @keyframes threeb { 0%, 20% { filter: none; } 40%, 50% { filter: blur(5px); } 65%, 100% { filter: none; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 连续运动和姿态对应 (Straight-Ahead Action and Pose-to-Pose) [图片] 连续运动是绘制动画的每一帧,姿态对应是通常由一个 assistant 在定义一系列关键帧后填充间隔。 [代码] 大多数网页动画用的是姿态对应:关键帧之间的过渡可以通过浏览器在每个关键帧之间的插入尽可能多的帧使动画流畅。 [代码] 有一个例外是定时功能step。通过这个功能,浏览器 “steps” 可以把尽可能多的无序帧串清晰。你可以用这种方式绘制一系列图片并让浏览器按顺序显示出来,这开创了一种逐帧动画的风格。 HTML [代码]<h1>Principle 4: Straight Ahead Action and Pose to Pose</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle four"> <div class="shape a"></div> <div class="shape b"></div> </article> [代码] CSS [代码].four .shape.a { left: calc(50% - 8em); animation: four 6s infinite cubic-bezier(.57,-0.5,.43,1.53); } .four .shape.b { left: calc(50% + 8em); animation: four 6s infinite steps(1); } @keyframes four { 0%, 10% { transform: none; } 26%, 30% { transform: rotateZ(-45deg) scale(1.25); } 40% { transform: rotateZ(-45deg) translate(2em, -2em) scale(1.8); } 50%, 75% { transform: rotateZ(-45deg) scale(1.1); } 90%, 100% { transform: none; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 跟随和重叠动作 (Follow Through and Overlapping Action) [图片] 事情并不总在同一时间发生。当一辆车从急刹到停下,车子会向前倾、有烟从轮胎冒出来、车里的司机继续向前冲。 [代码] 这些细节是跟随和重叠动作的例子。它们在网页中能被用作帮助强调什么东西被停止,并不会被遗忘。例如一个条目可能在滑动时稍滑微远了些,但它自己会纠正到正确位置。 要创造一个重叠动作的感觉,我们可以让元件以稍微不同的速度移动到每处。这是一种在 iOS 系统的视窗 (View) 过渡中被运用得很好的方法。一些按钮和元件以不同速率运动,整体效果会比全部东西以相同速率运动要更逼真,并留出时间让访客去适当理解变化。 [代码] 在网页方面,这可能意味着让过渡或动画的效果以不同速度来运行。 HTML [代码]<h1>Principle 5: Follow Through and Overlapping Action</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle five"> <div class="shape-container"> <div class="shape"></div> </div> </article> [代码] CSS [代码].five .shape { animation: five 4s infinite cubic-bezier(.64,-0.36,.1,1); position: relative; left: auto; top: auto; } .five .shape-container { animation: five-container 4s infinite cubic-bezier(.64,-0.36,.1,2); position: absolute; left: calc(50% - 4em); top: calc(50% - 4em); } @keyframes five { 0%, 15% { opacity: 0; transform: translateX(-12em); } 15%, 25% { transform: translateX(-12em); opacity: 1; } 85%, 90% { transform: translateX(12em); opacity: 1; } 100% { transform: translateX(12em); opacity: 0; } } @keyframes five-container { 0%, 35% { transform: none; } 50%, 60% { transform: skewX(20deg); } 90%, 100% { transform: none; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 缓入缓出 (Slow In and Slow Out) [图片] 对象很少从静止状态一下子加速到最大速度,它们往往是逐步加速并在停止前变慢。没有加速和减速,动画感觉就像机器人。 [代码] 在 CSS 方面,缓入缓出很容易被理解,在一个动画过程中计时功能是一种描述变化速率的方式。 [代码] 使用计时功能,动画可以由慢加速 (ease-in)、由快减速 (ease-out),或者用贝塞尔曲线做出更复杂的效果。 HTML [代码]<h1>Principle 6: Slow in and Slow out</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle six"> <div class="shape a"></div> </article> [代码] CSS [代码].six .shape { animation: six 3s infinite cubic-bezier(0.5,0,0.5,1); } @keyframes six { 0%, 5% { transform: translate(-12em); } 45%, 55% { transform: translate(12em); } 95%, 100% { transform: translate(-12em); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 弧线运动 (Arc) [图片] 虽然对象是更逼真了,当它们遵循「缓入缓出」的时候它们很少沿直线运动——它们倾向于沿弧线运动。 我们有几种 CSS 的方式来实现弧线运动。一种是结合多个动画,比如在弹力球动画里,可以让球上下移动的同时让它右移,这时候球的显示效果就是沿弧线运动。 HTML [代码]<h1>Principle 7: Arc (1)</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle sevena"> <div class="shape-container"> <div class="shape a"></div> </div> </article> [代码] CSS [代码].sevena .shape-container { animation: move-right 6s infinite cubic-bezier(.37,.55,.49,.67); position: absolute; left: calc(50% - 4em); top: calc(50% - 4em); } .sevena .shape { animation: bounce 6s infinite linear; border-radius: 50%; position: relative; left: auto; top: auto; } @keyframes move-right { 0% { transform: translateX(-20em); opacity: 1; } 80% { opacity: 1; } 90%, 100% { transform: translateX(20em); opacity: 0; } } @keyframes bounce { 0% { transform: translateY(-8em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 15% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 25% { transform: translateY(-4em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 32.5% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 40% { transform: translateY(0em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 45% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 50% { transform: translateY(3em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 56% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 60% { transform: translateY(6em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 64% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 66% { transform: translateY(7.5em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 70%, 100% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] [图片] 另外一种是旋转元件,我们可以设置一个在对象之外的原点来作为它的旋转中心。当我们旋转这个对象,它看上去就是沿着弧线运动。 HTML [代码]<h1>Principle 7: Arc (2)</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle sevenb"> <div class="shape a"></div> <div class="shape b"></div> </article> [代码] CSS [代码].sevenb .shape.a { animation: sevenb 3s infinite linear; top: calc(50% - 2em); left: calc(50% - 9em); transform-origin: 10em 50%; } .sevenb .shape.b { animation: sevenb 6s infinite linear reverse; background-color: yellow; width: 2em; height: 2em; left: calc(50% - 1em); top: calc(50% - 1em); } @keyframes sevenb { 100% { transform: rotateZ(360deg); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 次要动作 (Secondary Action) [图片] 虽然主动画正在发生,次要动作可以增强它的效果。这就好比某人在走路的时候摆动手臂和倾斜脑袋,或者弹性球弹起的时候扬起一些灰尘。 在网页方面,当主要焦点出现的时候就可以开始执行次要动作,比如拖拽一个条目到列表中间。 HTML [代码]<h1>Principle 8: Secondary Action</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle eight"> <div class="shape a"></div> <div class="shape b"></div> <div class="shape c"></div> </article> [代码] CSS [代码].eight .shape.a { transform: translateX(-6em); animation: eight-shape-a 4s cubic-bezier(.57,-0.5,.43,1.53) infinite; } .eight .shape.b { top: calc(50% + 6em); opacity: 0; animation: eight-shape-b 4s linear infinite; } .eight .shape.c { transform: translateX(6em); animation: eight-shape-c 4s cubic-bezier(.57,-0.5,.43,1.53) infinite; } @keyframes eight-shape-a { 0%, 50% { transform: translateX(-5.5em); } 70%, 100% { transform: translateX(-10em); } } @keyframes eight-shape-b { 0% { transform: none; } 20%, 30% { transform: translateY(-1.5em); opacity: 1; animation-timing-function: cubic-bezier(.57,-0.5,.43,1.53); } 32% { transform: translateY(-1.25em); opacity: 1; } 34% { transform: translateY(-1.75em); opacity: 1; } 36%, 38% { transform: translateY(-1.25em); opacity: 1; } 42%, 60% { transform: translateY(-1.5em); opacity: 1; } 75%, 100% { transform: translateY(-8em); opacity: 1; } } @keyframes eight-shape-c { 0%, 50% { transform: translateX(5.5em); } 70%, 100% { transform: translateX(10em); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 时间节奏 (Timing) [图片] 动画的时间节奏是需要多久去完成,它可以被用来让看起来很重的对象做很重的动画,或者用在添加字符的动画中。 [代码] 这在网页上可能只要简单调整 animation-duration 或 transition-duration 值。 [代码] 这很容易让动画消耗更多时间,但调整时间节奏可以帮动画的内容和交互方式变得更出众。 HTML [代码]<h1>Principle 9: Timing</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle nine"> <div class="shape a"></div> <div class="shape b"></div> </article> [代码] CSS [代码].nine .shape.a { animation: nine 4s infinite cubic-bezier(.93,0,.67,1.21); left: calc(50% - 12em); transform-origin: 100% 6em; } .nine .shape.b { animation: nine 2s infinite cubic-bezier(1,-0.97,.23,1.84); left: calc(50% + 2em); transform-origin: 100% 100%; } @keyframes nine { 0%, 10% { transform: translateX(0); } 40%, 60% { transform: rotateZ(90deg); } 90%, 100% { transform: translateX(0); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 夸张手法 (Exaggeration) [图片] 夸张手法在漫画中是最常用来为某些动作刻画吸引力和增加戏剧性的,比如一只狼试图把自己的喉咙张得更开地去咬东西可能会表现出更恐怖或者幽默的效果。 在网页中,对象可以通过上下滑动去强调和刻画吸引力,比如在填充表单的时候生动部分会比收缩和变淡的部分更突出。 HTML [代码]<h1>Principle 10: Exaggeration</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle ten"> <div class="shape"></div> </article> [代码] CSS [代码].ten .shape { animation: ten 4s infinite linear; transform-origin: 50% 8em; top: calc(50% - 6em); } @keyframes ten { 0%, 10% { transform: none; animation-timing-function: cubic-bezier(.87,-1.05,.66,1.31); } 40% { transform: rotateZ(-45deg) scale(2); animation-timing-function: cubic-bezier(.16,.54,0,1.38); } 70%, 100% { transform: rotateZ(360deg) scale(1); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 扎实的描绘 (Solid drawing) [图片] 当动画对象在三维中应该加倍注意确保它们遵循透视原则。因为人们习惯了生活在三维世界里,如果对象表现得与实际不符,会让它看起来很糟糕。 如今浏览器对三维变换的支持已经不错,这意味着我们可以在场景里旋转和放置三维对象,浏览器能自动控制它们的转换。 HTML [代码]<h1>Principle 11: Solid drawing</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle eleven"> <div class="shape"> <div class="container"> <span class="front"></span> <span class="back"></span> <span class="left"></span> <span class="right"></span> <span class="top"></span> <span class="bottom"></span> </div> </div> </article> [代码] CSS [代码].eleven .shape { background: none; border: none; perspective: 400px; perspective-origin: center; } .eleven .shape .container { animation: eleven 4s infinite cubic-bezier(.6,-0.44,.37,1.44); transform-style: preserve-3d; } .eleven .shape span { display: block; position: absolute; opacity: 1; width: 4em; height: 4em; border: 1em solid #fff; background: #2d97db; } .eleven .shape span.front { transform: translateZ(3em); } .eleven .shape span.back { transform: translateZ(-3em); } .eleven .shape span.left { transform: rotateY(-90deg) translateZ(-3em); } .eleven .shape span.right { transform: rotateY(-90deg) translateZ(3em); } .eleven .shape span.top { transform: rotateX(-90deg) translateZ(-3em); } .eleven .shape span.bottom { transform: rotateX(-90deg) translateZ(3em); } @keyframes eleven { 0% { opacity: 0; } 10%, 40% { transform: none; opacity: 1; } 60%, 75% { transform: rotateX(-20deg) rotateY(-45deg) translateY(4em); animation-timing-function: cubic-bezier(1,-0.05,.43,-0.16); opacity: 1; } 100% { transform: translateZ(-180em) translateX(20em); opacity: 0; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 吸引力 (Appeal) [图片] 吸引力是艺术作品的特质,让我们与艺术家的想法连接起来。就像一个演员身上的魅力,是注重细节和动作相结合而打造吸引性的结果。 [代码] 精心制作网页上的动画可以打造出吸引力,例如 Stripe 这样的公司用了大量的动画去增加它们结账流程的可靠性。 [代码] HTML [代码]<h1>Principle 12: Appeal</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle twelve"> <div class="shape"> <div class="container"> <span class="item one"></span> <span class="item two"></span> <span class="item three"></span> <span class="item four"></span> </div> </div> </article> [代码] CSS [代码].twelve .shape { background: none; border: none; perspective: 400px; perspective-origin: center; } .twelve .shape .container { animation: show-container 8s infinite cubic-bezier(.6,-0.44,.37,1.44); transform-style: preserve-3d; width: 4em; height: 4em; border: 1em solid #fff; background: #2d97db; position: relative; } .twelve .item { background-color: #1f7bb6; position: absolute; } .twelve .item.one { animation: show-text 8s 0.1s infinite ease-out; height: 6%; width: 30%; top: 15%; left: 25%; } .twelve .item.two { animation: show-text 8s 0.2s infinite ease-out; height: 6%; width: 20%; top: 30%; left: 25%; } .twelve .item.three { animation: show-text 8s 0.3s infinite ease-out; height: 6%; width: 50%; top: 45%; left: 25%; } .twelve .item.four { animation: show-button 8s infinite cubic-bezier(.64,-0.36,.1,1.43); height: 20%; width: 40%; top: 65%; left: 30%; } @keyframes show-container { 0% { opacity: 0; transform: rotateX(-90deg); } 10% { opacity: 1; transform: none; width: 4em; height: 4em; } 15%, 90% { width: 12em; height: 12em; transform: translate(-4em, -4em); opacity: 1; } 100% { opacity: 0; transform: rotateX(-90deg); width: 4em; height: 4em; } } @keyframes show-text { 0%, 15% { transform: translateY(1em); opacity: 0; } 20%, 85% { opacity: 1; transform: none; } 88%, 100% { opacity: 0; transform: translateY(-1em); animation-timing-function: cubic-bezier(.64,-0.36,.1,1.43); } } @keyframes show-button { 0%, 25% { transform: scale(0); opacity: 0; } 35%, 80% { transform: none; opacity: 1; } 90%, 100% { opacity: 0; transform: scale(0); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码]
2019-03-21