SegmentFault 太极剑舞最新的文章
2019-03-23T18:06:06+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
Taro 小程序实战技术摘要
https://segmentfault.com/a/1190000018626813
2019-03-23T18:06:06+08:00
2019-03-23T18:06:06+08:00
newsning
https://segmentfault.com/u/newsning
3
<h2>Taro 小程序实战技术摘要</h2>
<h4>React</h4>
<p>一个用于构建用户界面的 JAVASCRIPT 库。采用声明范式,可以轻松描述应用。通过对DOM的模拟,最大限度地减少与DOM的交互。组件复用,能够很好的应用在大项目的开发中。单向响应的数据流,使得开发变得清晰。</p>
<h4>Taro</h4>
<p>Taro 是一套遵循 React 语法规范的 多端开发 解决方案。现如今市面上端的形态多种多样,Web、React-Native、微信小程序等各种端大行其道,当业务要求同时在不同的端都要求有所表现的时候,针对不同的端去编写多套代码的成本显然非常高,这时候只编写一套代码就能够适配到多端的能力就显得极为需要。</p>
<p>使用 Taro,我们可以只书写一套代码,再通过 Taro 的编译工具,将源代码分别编译出可以在不同端(微信/百度/支付宝/字节跳动小程序、H5、React-Native 等)运行的代码。</p>
<h4>TypeScript</h4>
<p>TypeScript 是 JavaScript 的一个超集,主要提供了类型系统和对 ES6 的支持,它由 Microsoft 开发,代码开源于 GitHub 上。</p>
<h4>GitFlow</h4>
<p>git-flow 是一个 git 扩展集,按 Vincent Driessen 的分支模型提供高层次的库操作<br><img src="/img/remote/1460000018626816" alt="" title=""></p>
<h4>VsCode + 插件</h4>
<p>Visual Studio Code(简称VsCode)是一个轻量且强大的代码编辑器,支持Windows,OS X和Linux。内置JavaScript、TypeScript和Node.js支持,而且拥有丰富的插件生态系统,可通过安装插件来支持C++、C#、Python、PHP等其他语言。以及更多丰富便捷的插件库。<br><img src="/img/remote/1460000018626817?w=2874&h=1784" alt="" title=""></p>
<h4>代码检查</h4>
<p>代码检查主要是用来发现代码错误、统一代码风格。</p>
<p>在 JavaScript 项目中,我们一般使用 ESLint 来进行代码检查。它通过插件化的特性极大的丰富了适用范围,搭配 typescript-eslint-parser 之后,甚至可以用来检查 TypeScript 代码。</p>
<p>TSLint 与 ESLint 类似,不过除了能检查常规的 js 代码风格之外,TSLint 还能够通过 TypeScript 的语法解析,利用类型系统做一些 ESLint 做不到的检查。</p>
<h5>为什么需要代码检查</h5>
<p>有人会觉得,JavaScript 非常灵活,所以需要代码检查。而 TypeScript 已经能够在编译阶段检查出很多问题了,为什么还需要代码检查呢?</p>
<p>因为 TypeScript 关注的重心是类型的匹配,而不是代码风格。当团队的人员越来越多时,同样的逻辑不同的人写出来可能会有很大的区别:</p>
<ul>
<li>缩进应该是四个空格还是两个空格?</li>
<li>是否应该禁用 var?</li>
<li>接口名是否应该以 I 开头?</li>
<li>是否应该强制使用 === 而不是 ==?</li>
</ul>
<p>这些问题 TypeScript 不会关注,但是却影响到多人协作开发时的效率、代码的可理解性以及可维护性。</p>
<p>下面来看一个具体的例子:</p>
<pre><code class="typescript">let myName = 'Tom';
console.log(`My name is ${myNane}`);
console.log(`My name is ${myName.toStrng()}`);
console.log(`My name is ${myName}`)
// tsc 报错信息:
//
// index.ts(3,27): error TS2552: Cannot find name 'myNane'. Did you mean 'myName'?
// index.ts(4,34): error TS2551: Property 'toStrng' does not exist on type 'string'. Did you mean 'toString'?
//
//
//
// eslint 报错信息:
//
// /path/to/index.ts
// 3:27 error 'myNane' is not defined no-undef
// 5:38 error Missing semicolon semi
//
// ✖ 2 problems (2 errors, 0 warnings)
// 1 errors, 0 warnings potentially fixable with the `--fix` option.
//
//
//
// tslint 报错信息:
//
// ERROR: /path/to/index.ts[5, 36]: Missing semicolon</code></pre>
<p><img src="/img/remote/1460000018626818?w=1606&h=378" alt="" title=""></p>
<p>下图表示了 tsc, eslint 和 tslint 能覆盖的检查:<br><img src="/img/remote/1460000018626819" alt="" title=""></p>
<p>上图中,<strong>tsc</strong>, <strong>eslint</strong> 和 <strong>tslint</strong> 之间互相都有重叠的部分,也有各自独立的部分。</p>
<p>虽然发现代码错误比统一的代码风格更重要,但是<strong>当一个项目越来越庞大,开发人员也越来越多的时候,代码风格的约束还是必不可少的</strong>。</p>
<h4>UI设计协作</h4>
<p>蓝湖是一款设计图共享平台,帮助互联网团队管理设计图。蓝湖可以自动生成标注,与团队共享设计图,展示页面之间的跳转关系。支持从Sketch一键分享、在线讨论、自动为设计图生成标注,而且只需简单几步就能将设计图变成一个可以点击的演示原型,支持分享给同事,让他也可以在手机中查看设计效果。蓝湖已经成为新一代产品设计的工作方式。</p>
<p><img src="/img/remote/1460000018626820" alt="" title=""></p>
<p>直接在线编辑和预览的方式,让产品与设计师的交流反馈得到极大的提升,同时开发者可以得到详细的标注,以及不同平台的支持提示,使得开发效率变快。</p>
<p><img src="/img/remote/1460000018626821" alt="" title=""></p>
<h4>框架搭建</h4>
<h6>全局配置文件</h6>
<p><strong>(ps: 截图中的秘钥配置和配置都是测试,大家看一下大概配置就好)</strong><br>通过 Config 文件,区分开发、生产环境,动态的编译代码后,使用相应正确的配置,还有小程序不同的标识,同时保存了微信、支付宝、神策等第三方平台的密钥。统一的配置文件能让后期更好维护。<br><img src="/img/remote/1460000018626822" alt="" title=""></p>
<h6>通过Swagger文档生成API文件</h6>
<p>在需求评审后,前后端人员将会在一起制定接口结构,这点体现在Swagger文档之中,随后前端使用脚手架工具把Swagger文档yaml文件生成可调用的fetch请求文件集成到项目,避免了手动编写接口代码的错误风险,这点前提是对Swagger有严格的要求,基于接口规范编码。</p>
<ol><li>放入Swagger文件</li></ol>
<p><img src="/img/remote/1460000018626823" alt="" title=""></p>
<ol><li>执行转化命令</li></ol>
<pre><code class="shell">sudo api-cli MINI js</code></pre>
<ol><li>得到api请求配置文件</li></ol>
<p><img src="/img/remote/1460000018626824" alt="" title=""></p>
<ol><li>代码中使用</li></ol>
<p><img src="/img/remote/1460000018626825" alt="" title=""></p>
<h6>开发适配</h6>
<p>在自动生成api请求文件后,需要更改一些配置才能在于小程序之中运行,所以我们做了以下适配:</p>
<ul>
<li>
<p>小程序请求</p>
<ul><li>Taro.request 适配 微信、支付宝小程序请求</li></ul>
</li>
<li>
<p>全局请求头添加</p>
<ul>
<li>userToken 用来识别用户身份信息</li>
<li>branch 后端服务路径分支名称</li>
<li>clientType 小程序平台名称</li>
<li>version 当前程序版本</li>
</ul>
</li>
<li>
<p>测试调试工具</p>
<ul>
<li>baseURL 更改全局请求地址</li>
<li>branch 更改请求头分支名</li>
<li>clear 清除缓存数据</li>
</ul>
</li>
</ul>
<p>以上适配让小程序的可拓展、可维护性得到大大提高。</p>
<h6>资源优化</h6>
<p>由于官方的限制,一个小程序的代码包括资源文件大小不能超过2M,在真实迭代情况下,随着业务的增加,功能的改变,2M大小对我们来说可能很快就要超出,我们采用以下方案:</p>
<ol><li>代码分包</li></ol>
<p>此方式是官方推荐的方法,通过把不同业务代码资源分离出来,主要的功能先行下载运行,如:主页商铺列表、商铺详情、订单列表等,而相对低频的功能可以后续异步下载运行,如:退款列表/详情、会员权益说明等,但是官方还是限制了最多为4个分离包,每个大小限制也为2M。</p>
<ol><li>图片资源远端保存</li></ol>
<p>这是一种常用的包大小减小体积的方案,超过30k大小的文件放入阿里云OSS资源仓库保存,代码中使用远程路径方式引入,但同时要考虑首屏渲染的平衡</p>
<ol><li>代码压缩</li></ol>
<p>代码运行终究是机器做的事,只是顺便让我们程序员看看,所以在我们编译代码的同时对其js、css等文件内容压缩,去除运行中不必要的注释、打印、多余空格等,同时用编译环境控制是否开启压缩功能,这样平时开发调试的时候可以关闭压缩功能,更好的进行调试、测试、排错。</p>
<h6>线上代码报错日志 fundebug</h6>
<p>在真实线上的环境,难免会遇到很隐性的BUG,通过集成fundebug的方式。来检测到线上程序报错的信息。 </p>
<p>fundebug 可以快速复现出错场景,记录出错前点击、页面跳转、网络请求,控制台打印等信息。通过 Source Map 还原生产环境中的压缩代码,提供完整的堆栈信息,准确定位到出错源码,帮助我们快速修复Bug。<br><img src="/img/remote/1460000018626826" alt="" title=""></p>
<p>一键还原出错代码 </p>
<p><img src="/img/remote/1460000018626827" alt="" title=""></p>
<h6>数据采集</h6>
<p>Sensors(神策分析)针对企业级客户的自助式用户行为分析平台。实时数据采集、建模、分析,驱动市场营销、产品优化、用户运营、管理监控。</p>
<p><img src="/img/remote/1460000018626828" alt="" title=""></p>
<p>神策官方提供了小程序的SDK,但是我们需要配合产品提供的事件和逻辑,自己编写埋点。</p>
<ul><li>埋点事件模型和抽象类</li></ul>
<pre><code class="typescript">
// 点击优惠券列表
export interface IClickCouponMerchantList extends IClickCoupon {}
// 点击兑换优惠券
export interface IClickToExChange {}
// 使用商品
export interface IConsume extends IOrder, IMerchant, ICommodity {}
// 点击小程序首页banner
export interface IClickMiniBanner {}
// ...
export interface IStatisEvents {
ClickCouponMerchantList?: (params: IClickCouponMerchantList) => any
ClickToExChange?: (params: IClickToExChange) => any
Consume?: (params: IConsume) => any
ClickMiniBanner?: (params: IClickMiniBanner) => any
// ...
}</code></pre>
<ul><li>数据采集封装</li></ul>
<pre><code class="typescript">
interface IStatis extends IStatisEvents {
sensors: any;
init: () => any;
defaultTrack: (eventName: string, eventParams: any) => any;
}
const loadStatisEvent = (statis: Statis): IStatis => {
return new Proxy(statis, {
get(obj, prop) {
const target = obj[prop];
if (obj.hasOwnProperty(prop)) {
return target;
}
if (!SaConfig.enable) {
return noop;
}
if (typeof target === "function") {
return target.bind(obj);
}
// 默认采集
return obj.defaultTrack.bind(obj, prop);
}
});
};
class Statis implements IStatis {
public sensors: any;
constructor(sensors) {
this.sensors = sensors;
}
init() {
this.sensors.init();
}
defaultTrack(eventName: string, eventParams: any) {
this.sensors.track(eventName, eventParams);
}
Login(id: string) {
this.sensors.login(id);
}
RegisterApp(params: IRegisterApp) {
const param = Object.assign(
{
platform: AppConfig.CLENT_TYPE
},
params
);
this.sensors.registerApp(param);
}
}
const sa = loadStatisEvent(new Statis(sensors_));</code></pre>
<ul><li>事件采集</li></ul>
<p><img src="/img/remote/1460000018626829" alt="" title=""></p>
<h4>结语</h4>
<p>微信/支付宝 双端发布,虽说是一套代码,但是做了满多兼容,主要在两端授权用户信息不太一样,需要封装统一,再一个是样式写的不规范,两端表现的也不一致。 <br>swagger API文件生成 可调用的 fetch 文件、统一请求封装和自定义请求头、处理多端授权用户信息等,还有很多技术和流程的细节,容我后续再跟大家慢慢讲解。</p>
React 简介
https://segmentfault.com/a/1190000018626718
2019-03-23T17:52:20+08:00
2019-03-23T17:52:20+08:00
newsning
https://segmentfault.com/u/newsning
1
<h2>React 介绍</h2>
<blockquote>React是Facrbook内部的一个JavaScript类库并开源,可用于创建Web用户交互界面。它引入了一种新的方式来处理浏览器DOM。那些需要手动更新DOM、费力地记录每一个状态的日子一去不复返。React使用很新颖的方式解决了这些问题。你只需要声明地定义各个时间点的用户界面,而无序关系在数据变化时,需要更新哪一部分DOM。在任何时间点,React都能以最小的DOM修改来更新整个应用程序。</blockquote>
<p><strong>React主要有四个主要概念构成,下面分别来简单介绍一下</strong></p>
<h5>Virtual DOM</h5>
<ul>
<li>之所以引入虚拟DOM,一方面是性能的考虑。Web应用和网站不同,一个Web应用 中通常会在单页内有大量的DOM操作,而这些DOM操作很慢</li>
<li>在React中,应用程序在虚拟DOM上操作,这让React有了优化的机会。简单说, React在每次需要渲染时,会先比较当前DOM内容和待渲染内容的差异, 然后再决定如何最优地更新DOM</li>
<li>除了性能的考虑,React引入虚拟DOM更重要的意义是提供了一种一致的开发方式来开发服务端应用、Web应用和手机端应用</li>
</ul>
<p><img src="/img/remote/1460000018626721?w=1210&h=778" alt="avatar" title="avatar"></p>
<blockquote>因为有了虚拟DOM这一层,所以通过配备不同的渲染器,就可以将虚拟DOM的内容 渲染到不同的平台。而应用开发者,使用JavaScript就可以通吃各个平台了。相当棒的思路!且<strong>虚拟DOM是内存数据,性能是极高的,而对实际DOM进行操作的仅仅是 Diff部分,因而能达到提高性能的目的</strong>
</blockquote>
<h5>React组件</h5>
<p>对于React而言,则完全是一个新的思路,开发者从功能的角度出发,将<strong>UI分成不同的组件,每个组件都独立封装</strong>。<br>在React中,你按照界面模块自然划分的方式来组织和编写你的代码,对于评论界面而言,整个UI是一个通过小组件构成的大组件,每个组件只关心自己部分的逻辑,彼此独立。</p>
<p><img src="/img/remote/1460000018626722" alt="avatar" title="avatar"><br><img src="/img/remote/1460000018626723" alt="avatar" title="avatar"></p>
<ul><li>组件化开发特性:</li></ul>
<p>React认为一个组件应该具有如下特征:</p>
<p>1.<strong>可组合(Composeable)</strong>:一个组件易于和其它组件一起使用,或者嵌套在另一个组件内部。如果一个组件内部创建了另一个组件,那么说父组件拥有(own)它创建的子组件,通过这个特性,一个复杂的UI可以拆分成多个简单的UI组件;</p>
<p>2.<strong>可重用(Reusable)</strong>:每个组件都是具有独立功能的,它可以被使用在多个UI场景;</p>
<p>3.<strong>可维护(Maintainable)</strong>:每个小的组件仅仅包含自身的逻辑,更容易被理解和维护;</p>
<p>4.<strong>可测试(Testable)</strong>:因为每个组件都是独立的,那么对于各个组件分别测试显然要比对于整个UI进行测试容易的多。</p>
<ul><li>组件定义</li></ul>
<p>在React中定义一个组件也是相当的容易,组件就是一个 实现预定义接口的JavaScript类:</p>
<ol><li>组件渲染</li></ol>
<blockquote>ReactDOM.render 是 React 的最基本方法,用于将模板转为 HTML 语言,并插入指定的 DOM 节点。</blockquote>
<pre><code class="javascript">ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('example')
);</code></pre>
<p>而这个方法, 必须而且只能返回一个有效的React元素。这意味着,如果你的组件是由多个元素构成的,那么你必须在外边包一个顶层 元素,然后返回这个顶层元素。比如我们创建一个布局组件:</p>
<pre><code class="javascript">render:function(){
return React.createElement(
"div",null,
React.createElement("div",null,"header"),
React.createElement("div",null,"content"),
React.createElement("div",null,"footer")
);
}</code></pre>
<ol><li>ES5方式定义组件</li></ol>
<pre><code class="javascript">"use strict";
var HelloMessage = React.createClass({
displayName: "HelloMessage",
render: function render() {
return React.createElement(
"div",
null,
"Hello ",
this.props.name
);
}
});</code></pre>
<ol><li>ES6方式定义组件</li></ol>
<pre><code class="javascript">import './Hello.css';
import './Hello.scss';
import React, {Component} from 'react';
// 内联样式
let style={
backgroundColor:'blue'
}
export default class Hello extends Component {
constructor(props) {
super(props);
this.state = { count: 'es6'};
}
render() {
return (
<div>
<h1 style={style}>Hello world{this.state.count}</h1>
<br/>
<image/>
</div>
)
}
}</code></pre>
<ol><li>JSX方式定义组件</li></ol>
<pre><code class="javascript">var HelloMessage = React.createClass({
render: function() {
return <div>Hello {this.props.name}</div>;
}
});
ReactDOM.render(<HelloMessage name="John" />, mountNode);</code></pre>
<h5>Jsx语法</h5>
<ul><li>什么是jsx</li></ul>
<blockquote>在用React写组件的时候,通常会用到JSX语法,粗看上去,像是在Javascript代码里直接写起了XML标签,实质上这只是一个语法糖,每一个 XML标签都会被JSX转换工具转换成纯Javascript代码,当然你想直接使用纯Javascript代码写也是可以的,只是<strong>利用JSX,组件的结构和组件之间的关系看上去更加清晰</strong>
</blockquote>
<ul><li>Jsx语法使用</li></ul>
<p>HTML 语言直接写在 JavaScript 语言之中,不加任何引号,这就是 JSX 的语法,它允许 HTML 与 JavaScript 的混写。</p>
<pre><code class="javascript">var names = ['Alice', 'Emily', 'Kate'];
ReactDOM.render(
<div>
{
names.map(function (name) {
return <div>Hello, {name}!</div>
})
}
</div>,
document.getElementById('example')
);</code></pre>
<p><strong>上面代码体现了 JSX 的基本语法规则:遇到 HTML 标签(以 < 开头),就用 HTML 规则解析;遇到代码块(以 { 开头),就用 JavaScript 规则解析。</strong></p>
<hr>
<p>JSX 允许直接在模板插入 JavaScript 变量。如果这个变量是一个数组,则会展开这个数组的所有成员</p>
<pre><code class="javascript">var arr = [
<h1>Hello world!</h1>,
<h2>React is awesome</h2>,
];
ReactDOM.render(
<div>{arr}</div>,
document.getElementById('example')
);</code></pre>
<p>上面代码的arr变量是一个数组,结果 JSX 会把它的所有成员,添加到模板,运行结果如下。</p>
<p><img src="/img/remote/1460000018626724?w=1222&h=470" alt="helloword" title="helloword"></p>
<h5>Data Flow(单向数据流)</h5>
<ul><li>单向数据流</li></ul>
<p>先来了解一下 Flux 的核心“单向数据流“怎么运作的:<br>Action -> Dispatcher -> Store -> View<br>更多时候 View 会通过用户交互触发 Action,所以一个简单完整的数据流类似这样:<br><img src="/img/remote/1460000018626725" alt="Flux" title="Flux"></p>
<p>整个流程如下:</p>
<p>1.首先要有 action,通过定义一些 action creator 方法根据需要创建 Action 提供给 dispatcher<br>2.View 层通过用户交互(比如 onClick)会触发 Action<br>3.Dispatcher 会分发触发的 Action 给所有注册的 Store 的回调函数<br>4.Store 回调函数根据接收的 Action 更新自身数据之后会触发一个 change 事件通知 View 数据更改了<br>5.View 会监听这个 change 事件,拿到对应的新数据并调用 setState 更新组件 UI<br>所有的状态都由 Store 来维护,通过 Action 传递数据,构成了如上所述的单向数据流循环,所以应用中的各部分分工就相当明确,高度解耦了。<br>这种单向数据流使得整个系统都是透明可预测的。</p>
Taro 简介
https://segmentfault.com/a/1190000018626680
2019-03-23T17:50:39+08:00
2019-03-23T17:50:39+08:00
newsning
https://segmentfault.com/u/newsning
4
<h2>Taro 介绍</h2>
<blockquote>在互联网不断发展的今天,前端程序员们也不断面临着新的挑战,在这个变化多端、不断革新自己的领域,每一年都有新的美好事物在发生。从去年微信小程序的诞生,到今年的逐渐火热,以及异军突起的轻应用、百度小程序等的出现,前端可以延伸的领域已经越来越广,当然也意味着业务在不断扩大。这时候,如何通过<strong>技术手段来提升开发效率,应对不断增长的业务</strong>,京东前端凹凸实验室开源多 Taro 就此诞生。</blockquote>
<h4>让人又爱又恨的微信小程序</h4>
<p>自 <font color=red>2017-1-9</font> 微信小程序(以下简称小程序)诞生以来,就伴随着赞誉与争议不断。从发布上线时的不被大多数人看好,到如今的逐渐火热,甚至说是如日中天也不为过,小程序用时间与实践证明了自己的价值。同时于开发者来说,小程序的生态不断在完善,许多的坑已被踩平,虽然还是存在一些令人诟病的问题,但已经足见微信的诚意了。这个时候要是还没有上手把玩过小程序,就显得非常OUT了。 </p>
<p>小程序对于前端程序员来说应该算得上是福音了,用前端相关的技术,获得丝般顺滑的 Native 体验。小程序给前端程序员打开了一扇新的大门,大家都应该感谢微信,但是从开发的角度来说,小程序的开发体验就非常值得商榷了,不仅语法上显得有些不伦不类,而且有些莫名其妙的坑也经常让人不经意间感叹,从市面上层出不穷的小程序开发框架就可见一斑。以下就盘点部分小程序开发的痛点。</p>
<h5>代码组织与语法</h5>
<p>在小程序中,一个页面 page 可能拥有 page.js、page.wxss、page.wxml 、page.json 四个文件</p>
<p><img src="/img/remote/1460000018626683?w=970&h=746" alt="wxfile" title="wxfile"></p>
<p>这样在开发的时候就需要来回进行文件切换,尤其是在同时开发模板和逻辑的时候,切来切去会显得尤其麻烦,影响开发效率。 </p>
<p>而在语法上,小程序的语法可以说既像 <strong>React</strong> ,又像 <strong>Vue</strong>,不能说显得有点不伦不类,但在使用上总是感觉有些别扭,对于开发者来说,等于又要学习一套新的语法,提升了学习成本。而且,小程序的模板由于没有编辑器插件的支持,书写的时候也没有智能提示与 lint 检查,书写起来显得有些麻烦。</p>
<h5>命名规范</h5>
<p>在小程序中到处可见规范不统一的情况</p>
<p>例如组件的属性,以最简单的 <strong>button</strong> 组件为例,在小程序官方文档中,该组件的属性部分截图如下</p>
<p><strong>button</strong> 组件 <br><img src="/img/remote/1460000018626684" alt="" title=""></p>
<p>属性名既有以中划线分割多个单词的情况 <strong>session-form</strong>,也有多个单词连写的情况 <strong>bindgetphonenumber</strong>。当然这也不是最严重的,你可以说事件绑定的规范就是 <strong>bind + 事件名</strong> ,而其他属性的规范就是中划线分割单词,然而这并不是作为标准 </p>
<p><strong>progress</strong> 组件<br><img src="/img/remote/1460000018626685" alt="" title=""></p>
<p>这不统一的标准使得开发者体验极其难受。 </p>
<p>同样的情况也出现在 <strong>页面</strong> 与 <strong>组件</strong> 的生命周期方法中,<strong>页面</strong> 的生命周期方法有 <strong>onLoad</strong>、<strong>onReady</strong>、<strong>onUnload</strong> 等,但到了 <strong>组件</strong> 中则是 <strong>created</strong>、<strong>attached</strong> 、<strong>ready</strong> 等,这样规范又不统一了,为啥 页面 的生命周期方法是 <strong>on+Xxx</strong> 的格式,但到了 组件 里却不一样了呢,让人费解。</p>
<h4>开发方式</h4>
<p>小程序官方提供了 <strong>微信开发工具</strong> 作为开发编译工具,而对于代码本身没有提供一个类似 <strong>webpack</strong> 的工程化开发工具,来解决开发中的一些问题,所以小程序原生的开发方式显得不那么现代化,这也是很多小程序开发框架致力于解决的问题。例如,在小程序开发中</p>
<ul>
<li>
<strong>不能使用 npm 管理依赖</strong>,在小程序中需要手动把第三方代码文件下载到本地,然后再 <strong>reuqire</strong> 进行使用,显得不那么优雅</li>
<li>
<strong>不能使用 Sass 等 CSS 预处理器</strong>,由于没有预编译的概念,小程序开发中无法使用市面上流行的 CSS 预处理器,这样会使得样式代码难以管理</li>
<li>
<strong>不完整的 ES Next 语法支持</strong>,小程序默认只能支持极少一部分 ES6 规范的语法,而 ES 是不断往前发展的,一些非常优秀的新语法特性就不能使用了</li>
<li>
<strong>手动的文件处理</strong>,像图片压缩、代码压缩等等的一些文件操作,必须手工来处理,显得有些繁琐</li>
</ul>
<p>以上就是从开发者的角度看到的一些小程序的开发问题,不过纵然有千般困难,我们总要面对,作为新时代的前端开发工程师,我们不能一味忍受问题,要保持技术的头脑,以技术作为武器,用技术手段去提升的我们开发体验。</p>
<h4>能不能用React来写小程序</h4>
<p>目前前端界言及前端框架,必离不开依然保持着统治地位的 <strong>React</strong> 与 <strong>Vue</strong>,这两个都是非常优秀的前端 UI 框架,而且在网上也经常能看到两个框架的粉丝之间热情交流,碰撞出一些思想火花,显得社区异常活跃。 </p>
<p>而我们团队也在去年勇敢地抛弃了历史包袱,非常荣幸地引入了 <strong>React</strong> 开发方式,让我们团队丢掉了煤油灯,开始通上了电。而且也研发出了一款优秀的类 <strong>React</strong> 框架 <strong>Nerv</strong> ,让我们和 <strong>React</strong> 开发思想结合得更深。 </p>
<p>与小程序的开发方式相比,<strong>React</strong> 明显显得更加现代化、规范化,而且 <strong>React</strong> 天生组件化更适合我们的业务开发,<strong>JSX</strong> 也比字符串模板有更强的表现力。那么这时候我们就在思考,我们能不能用 <strong>React</strong> 来写小程序?</p>
<h6>理性地探索</h6>
<ol><li><strong>类比</strong></li></ol>
<p>通过对比体验 小程序和 <strong>React</strong> ,我们还是能发现两者之间相似的地方</p>
<ol><li><strong>生命周期</strong></li></ol>
<p>小程序的生命周期和 <strong>React</strong> 的生命周期,在很大程度上是类似的,我们甚至能找到他们之间的对应关系<br>app 及页面的生命周期<br><img src="/img/remote/1460000018626686" alt="" title=""><br>可以看出,对于 <strong>app</strong> 及 <strong>页面</strong> 来说,除了 <strong>onShow</strong> 与 <strong>onHide</strong> 两个方法,其他方法都能在 <strong>React</strong> 中找到对应。</p>
<ol><li><strong>数据更新方式</strong></li></ol>
<p>在 <strong>React</strong> 中,组件的内部数据是用 <strong>state</strong> 来进行管理的,而在小程序中组件的内部数据都是用 <strong>data</strong> 来进行管理,两者具有一定相似性。而同时在 <strong>React</strong> 中,我们更新数据使用的是 <strong>setState</strong> 方法,传入新的数据或者生成新数据的函数,从而更新相应视图。在小程序中,则对应的有 <strong>setData</strong> 方法,传入新的数据,从而更新视图。 <br>两者都是以数据驱动视图的方式进行更新,而且 <strong>api</strong> 神似。</p>
<ol><li>事件绑定</li></ol>
<p>小程序中绑定事件使用的是 <strong>bind + 事件名</strong> 的方式,例如点击事件,小程序中是 <strong>bindtap</strong></p>
<pre><code class="jsx"><view bindtap="handlClick">1</view></code></pre>
<p>而在 <strong>React</strong> 里,则是 <strong>on + 事件名</strong> 的方式,例如点击事件, <strong>React web</strong> 中是 <strong>onClick</strong></p>
<pre><code class="jsx"><View onClick={this.handlClick}>1</View></code></pre>
<p>虽然看上去不一样,但其实是可以类比的,我们只需要在编译时将 <strong>on + 事件名</strong> 的形式编译成 <strong>bind + 事件名</strong> 的形式就可以了。</p>
<blockquote>如此看来,两者之间有些相似,用 React 来写小程序貌似是可行的,但接下来我们就发现了巨大的差异。</blockquote>
<h5>巨大的差异</h5>
<p><strong>React</strong> 与小程序之间最大的差异就是他们的模板了,在 <strong>React</strong> 中,是使用 <strong>JSX</strong> 来作为组件的模板的,而小程序则与 <strong>Vue</strong> 一样,是使用字符串模板的。这样两者之间就有着巨大的差异了。 </p>
<p>JSX</p>
<pre><code class="jsx">render () {
return (
<View className='index'>
{this.state.list.map((item, idx) => (
<View key={idx}>{item}</View>
))}
<Button onClick={this.goto}>走你</Button>
</View>
)
}</code></pre>
<p>小程序模板</p>
<pre><code class="html"><view class="index">
<view wx:key={idx} wx:for="{{list}}" wx:for-item="item" wx:for-index="idx">{{item}}</view>
<view bindtap="goto">走你</view>
</view></code></pre>
<p>众所周知,<strong>JSX</strong> 其实本质上就是 <strong>JS</strong>,我们可以在里面写任意的逻辑代码,这样一来就比字符串模板的表现力与操作性要强多了,况且,小程序的字符串模板功能比较羸弱,只有一些比较基本的功能。那这样的话,要如何来实现用 <strong>JSX</strong> 来写小程序模板呢。</p>
<h4>编译原理的力量</h4>
<p>我们可以仔细来分析我们的需求,我们期望使用 <strong>JSX</strong> 来书写小程序模板,但小程序显然是不支持执行 <strong>JSX</strong> 代码的(要是支持的话,<strong>Taro</strong> 应该也就不存在了吧),我们也不能期望微信能给我们开个后门来跑 <strong>JSX</strong>。那么这个时候我们就想,我们要是能够将 <strong>JSX</strong> 编译成小程序模板就好了。 </p>
<p>事实上在我们平时的开发中,这种编译的操作到处可见,<strong>babel</strong> 就是我们最常用的 <strong>JS</strong> 代码编译器,一般浏览器是不能支持一些非常新的语法特性的,但我们又想使用它们,这个时候就可以借助 <strong>babel</strong> 来将我们的高版本的 <strong>ES</strong> 代码,编译成浏览器可以运行的 <strong>ES</strong> 代码。而我们像要将 <strong>JSX</strong> 编译成小程序模板,也是同样的道理。我们首先来了解一下 <strong>Babel</strong> 的运行机制。 </p>
<p><strong>Babel</strong> 作为一个 代码编译器 ,能够将 <strong>ES6/7/8</strong> 的代码编译成 <strong>ES5</strong> 的代码,其核心利用的就是计算中非常基础的编译原理知识,将输入语言代码,通过编译器执行,输出目标语言的代码。编译原理的一般过程就是,输入源程序,经过词法分析、语法分析,构造出语法树,再经过语义分析,理解程序正确与否,再对语法树做出需要的操作与优化,最终生成目标代码。</p>
<p><img src="/img/remote/1460000018626687" alt="" title=""></p>
<p><strong>Babel</strong> 的编译过程亦是如此,主要包含三个阶段</p>
<ul>
<li>解析过程,在这个过程中进行词法、语法分析,以及语义分析,生成符合 <strong>ESTree</strong> 标准 虚拟语法树 <strong>AST</strong>
</li>
<li>转换过程,针对 <strong>AST</strong> 做出已定义好的操作,<strong>babel</strong> 的配置文件 <strong>.babelrc</strong> 中定义的 <strong>preset</strong> 、 <strong>plugin</strong> 就是在这一步中执行并改变 <strong>AST</strong> 的</li>
<li>生成过程,将前一步转换好的 <strong>AST</strong> 生成目标代码的字符串</li>
</ul>
<p>为了更好地理解这些过程,大家可以利用 <a href="https://link.segmentfault.com/?enc=XptnygCPJxxsPFdG%2B9KjDA%3D%3D.RLnu37inXrdGhBjPTDc43qBMoL3wX33ASBo4dVefBI8%3D" rel="nofollow">Ast Explorer</a> 这个网站接一下自己的代码,感受一下每一部分代码所对应的 <strong>AST</strong> 结构。</p>
<p><img src="/img/remote/1460000018626688" alt="" title=""></p>
<p>可以看到,一份源码经过编译器解析后,会变成类似如下的结构</p>
<pre><code class="javascript">{
type: "Program",
start: 0,
end: 78,
loc: { start, end }
sourceType: "module",
body: [
{ type: "VariableDeclaration", ... },
{ type: "VariableDeclaration", ... },
{ type: "FunctionDeclaration", ... },
{ type: "ExpressionStatement", ... }
]
...
}</code></pre>
<p>其中,<strong>body</strong> 里包含的就是我们示例代码的语法树结构,第一个 <strong>VariableDeclaration</strong> 对应的是 <strong>const a = 1</strong>,第三个 <strong>FunctionDeclaration</strong> 对应的则是 <strong>function sum (a, b) { }</strong>,分别就是 <strong>JS</strong> 中的变量定义与函数定义,每一个树节点里都会包含许多子节点,这样就形成了一个树形结构,更多的节点类型,请参考 <strong>babel types</strong>。<br>当然我们在这儿只是简单介绍下编译原理与 <strong>babel</strong>,编译原理是一门非常深奥的课程, <strong>babel</strong> 也是一个非常优秀的工具,希望在后续的文章中能和大家再详细探讨这一部分内容。<br>再次回到我们的需求,将 <strong>JSX</strong> 编译成小程序模板,非常幸运的是 <strong>babel</strong> 的核心编译器 <strong>babylon</strong> 是支持对 <strong>JSX</strong> 语法的解析的,我们可以直接利用它来帮我们构造 <strong>AST</strong>,而我们需要专注的核心就是如何对 <strong>AST</strong> 进行转换操作,得出我们需要的新 <strong>AST</strong>,再将新 <strong>AST</strong> 进行递归遍历,生成小程序的模板。</p>
<p>JSX 代码</p>
<pre><code class="jsx"><View className='index'>
<Button className='add_btn' onClick={this.props.add}>+</Button>
<Button className='dec_btn' onClick={this.props.dec}>-</Button>
<Button className='dec_btn' onClick={this.props.asyncAdd}>async</Button>
<View>{this.props.counter.num}</View>
<A />
<Button onClick={this.goto}>走你</Button>
<Image src={sd} />
</View></code></pre>
<p>编译生成小程序模板</p>
<pre><code class="html"><import src="../../components/A/A.wxml" />
<block>
<view class="index">
<button class="add_btn" bindtap="add">+</button>
<button class="dec_btn" bindtap="dec">-</button>
<button class="dec_btn" bindtap="asyncAdd">async</button>
<view>{{counter.num}}</view>
<template is="A" data="{{...$$A}}"></template>
<button bindtap="goto">走你</button>
<image src="{{sd}}" />
</view>
</block></code></pre>
<p>以上仅仅是转换规则的冰山一角,<strong>JSX</strong> 的写法极其灵活多变,通过穷举的方式,将常用的、<strong>React</strong> 官方推荐的写法作为转换规则加以支持,而一些比较生僻的,或者是不那么推荐的写的写法则不做支持,转而以 <strong>eslint</strong> 插件的方式,提示用户进行修改。目前我们支持的 <strong>JSX</strong> 转换规则,大致能覆盖到 <strong>JSX</strong> <strong>80%</strong> 的写法操作。</p>
<h5>多端发布</h5>
<p>输入一份源代码,针对不同的端设定好对应的转换规则,再一键转换出对应端的代码。而且由于我们已经遵循 <strong>React</strong> 语法了,那我们再转成 <strong>H5</strong> 端(使用 <strong>Nerv</strong>)与 <strong>RN</strong> 端(使用 <strong>React</strong>)也就有了天然的优势</p>
<p><img src="/img/remote/1460000018626689" alt="" title=""></p>
<h5>设计思路</h5>
<p>但是仔细思考我们又会发现,仅仅将代码按照对应语法规则转换过去后,还远远不够,因为不同端会有自己的原生组件,端能力 <strong>API</strong> 等等,代码直接转换过去后,可能不能直接执行。例如,小程序中普通的容器组件用的是 <strong>view</strong> ,而在 H5 中则是 <strong>div</strong>;小程序中提供了丰富的端能力 <strong>API</strong>,例如网络请求、文件下载、数据缓存等,而在 <strong>H5</strong> 中对应功能的 <strong>API</strong> 则不一致。 </p>
<p>所以,为了弥补不同端的差异,<strong>Taro</strong> 需要订制好一个统一的组件库标准,以及统一的 <strong>API</strong> 标准,在不同的端依靠它们的语法与能力去实现这个组件库与 <strong>API</strong>,同时还要为不同的端编写相应的运行时框架,负责初始化等等操作。通过以上这些操作,就能实现一份一键生成多端的需求了。在 <strong>Taro</strong> 最初的设计中,我们组件库与 <strong>API</strong> 的标准就是源自小程序的,既然已经有定义好的组件库与 <strong>API</strong> 标准,那为啥不直接拿来使用呢,这样不仅省去了定制标准的冥思苦想,同时也省去了为小程序开发组件库与 <strong>API</strong> 的麻烦,只需要让其他端来向小程序靠齐就好。 </p>
<p>可能有些人会有疑问,既然是为不同的端实现了对应的组件库与端能力 <strong>API</strong> (小程序除外,因为组件库和 <strong>API</strong> 的标准都是源自小程序),那么是怎么能够只写一份代码就够了呢?因为我们有编译的操作,在书写代码的时候,只需要引入标准组件库 <strong>@tarojs/components</strong> 与运行时框架 <strong>@tarojs/taro</strong> ,代码经过编译之后,会变成对应端所需要的库。</p>
<p><img src="/img/remote/1460000018626690" alt="" title=""></p>
<p>既然组件库以及端能力都是依靠不同的端做不同实现来抹平差异,那么同样的,如果想为 <strong>Taro</strong> 引入更多的功能支持的话,有时候也需要按照这个套路来。例如,为了提升开发便利性,我们为 <strong>Taro</strong> 加入了 <strong>Redux</strong> 支持,做法就是,在小程序端,实现 <strong>@tarojs/redux</strong> 这个库来作为小程序的 <strong>Redux</strong> 辅助库,并且以他作为基准库,它具有和 <strong>react-redux</strong> 一致的 <strong>API</strong>,在书写代码的时候,引用的都是 <strong>@tarojs/redux</strong> ,经过编译后,在 <strong>H5</strong> 端会替换成 <strong>nerv-redux</strong>(<strong>Nerv</strong>的 <strong>Redux</strong> 辅助库),在 <strong>RN</strong> 端会替换成 <strong>react-redux</strong>。这样就实现了 <strong>Redux</strong> 在 <strong>Taro</strong> 中的多端支持。</p>
<p><img src="/img/remote/1460000018626691" alt="" title=""></p>
<p>Taro 项目官网:<a href="https://link.segmentfault.com/?enc=OqO46Ga%2FHIjd01WzdVsQPw%3D%3D.gZu832mXdwQtODGahKHmmMTVm9WLhY0kr7xrMjIytPY%3D" rel="nofollow">https://taro.aotu.io/</a> <br>Taro GitHub:<a href="https://link.segmentfault.com/?enc=RfaBqsNmz6gIdsx%2Bp7pkHw%3D%3D.ZKOBMHqnZZwyZ1j5FjaLxvNdpzTjktItOhDI9WRfMio%3D" rel="nofollow">https://github.com/NervJS/taro</a></p>
【5+App】第三方分享的思路
https://segmentfault.com/a/1190000018573496
2019-03-19T16:13:47+08:00
2019-03-19T16:13:47+08:00
newsning
https://segmentfault.com/u/newsning
2
<h2>大家好</h2>
<p>2016-12-25 圣诞节这天 我失恋了</p>
<h2>现实</h2>
<p>1)App能分享一篇文章,能分享一个活动等,到第三方平台<br>2)展示相应的标题、内容以及链接<br>3)进入相应的链接可以唤醒App,不存在客户端时展示App下载页<br>4)被唤醒App后,展示相应的分享详细内容</p>
<h2>1</h2>
<p>利用5+的第三方分享,可以把想分享的信息发送到第三方平台<br>相关文章:<br><a href="https://link.segmentfault.com/?enc=mivcoXxo7Zqf7XVUHWcwLQ%3D%3D.4yqDyB5IWCG22NIZDHr8D1B7VdN0xv%2FHI4pzNuhwYEy1oyvupR4DDPIRF%2BakuKMB" rel="nofollow">分享插件开发指南</a><br><a href="https://link.segmentfault.com/?enc=2gba8wjTGWrtKBB79Bp2Hg%3D%3D.nvzAC3RdEtz1TInpWEx3GRIfAD7yGM%2Fm%2BVFTx%2BUHcfNsxcgYsb%2BwzSTm2yUFikgG" rel="nofollow">在Dcould社区搜索【分享】相关文章</a></p>
<p>我也简单的写过一个es6版本share</p>
<pre><code>let __shares = (function () {
let shares
return newShares => {
if (newShares) {
shares = newShares
}
return shares
}
})()
let __shareKv = {
wxhy: 'WXSceneSession', //微信好友
wxpyq: 'WXSceneTimeline', //微信朋友圈
qq: 'qq', //QQ好友
sinaweibo: 'sinaweibo', //新浪微博
}
class Share {
constructor(type, fn, op, context) {
this.config = {
type, //分享平台
fn, //结果回调
op, //需要分享的内容配置
context, //上下文
}
this._initCallback()
this.start()
}
_initCallback() {
this.ShareCallBack = function (...arg) {
return this.config.fn.apply(this.config.context, arg)
}
}
start() {
this.getService(this.config.type, (idShare) => {
this.sendShare(idShare, () => {
this.ShareCallBack(null, idShare)
})
})
}
sendShare(share, sendCallBack) {
let message = this._getShareInfo(share)
share.send(message, () => {
sendCallBack()
}, (err) => {
this.ShareCallBack(err, share)
})
}
//分享信息
_getShareInfo(share) {
let op = {
extra: {
scene: __shareKv[this.config.type]
},
href: this.config.op.href,
title: this.config.op.title, //
content: this.config.op.content, //
pictures: [this.config.op.img],
thumbs: [this.config.op.img],
}
if (!op.href) {
delete op.title
delete op.content
}
return op
}
//获取授权
_getAuth(share, authCallBack) {
if (!share.authenticated) {
share.authorize(() => {
authCallBack()
}, (...err) => {
return this.ShareCallBack.apply(null, err)
});
} else {
authCallBack()
}
}
//通过id 获取服务
_getService(id, CallBack) {
if(!!~id.toString().indexOf('wx')){
id = 'weixin'
}
let shares = __shares()
for (let i in shares) {
console.log(JSON.stringify(shares[i]))
if (id === shares[i].id) {
CallBack && CallBack(shares[i])
return shares[i]
}
}
}
//获取服务
getService(id, Callback) {
if (__shares()) {
return this._getService(id, Callback)
}
this._getHtml5PlusServices((data) => {
__shares(data)
this._getService(id, Callback)
})
}
//获取设备分享服务列表
_getHtml5PlusServices(CallBack) {
plus.share.getServices((services) => {
CallBack(services)
}, (err) => {
this.ShareCallBack(err, null)
})
}
}
var sendShare = function (...arg) {
return new Share(...arg)
}
export {
sendShare
}</code></pre>
<h2>2</h2>
<p>大概这样用</p>
<pre><code> //qq wxhy(微信好友) wxpyq(微信朋友圈) sinaweibo
sendShare('qq',function(err, data){
if(err){
data = err
}
console.log(JSON.stringify(data))
},{
img: '_www/img/vhp.png',//图片地址
href: 'https://github.com/zhaomenghuan/vue-html5plus',//分享的超链接
title: '分享标题',//当且仅当href存在时有效
content: '分享内容'//当且仅当href存在时有效
})</code></pre>
<p>ok,在这里我并没有配置分享的appkey,因为我用HBuilder的真机测试,大家如果打包发布时请一一填补</p>
<p><img src="/img/bVG9j2?w=1089&h=499" alt="clipboard.png" title="clipboard.png"></p>
<h2>3</h2>
<p>分享出去的信息,如果才能唤醒App呢?<br>鸽了。</p>
<p>2019-03-19<br>笔者因为工作原因没有太多时间更新该系列文章,技术栈也有所变化,且近来Dcloud的中心到放到了uni-app之上,所以希望大家能坚持初心,且适应变化。开头还写了我失恋了,没想到都快过了3年了,诶</p>
【5+】Webview页面之间的数据交流
https://segmentfault.com/a/1190000012762453
2018-01-09T12:15:16+08:00
2018-01-09T12:15:16+08:00
newsning
https://segmentfault.com/u/newsning
5
<p>一个App,其中大部分是要对页面之间的数据进行交互。<br><img src="/img/bVZA6P?w=54&h=51" alt="1DBFB83E6BDA9816D35E4796DE55BB0B" title="1DBFB83E6BDA9816D35E4796DE55BB0B"></p>
<p>碧如:A打开B页面,B页面执行一些代码,再通知回A页面。</p>
<p>这可能是h5+er们遇到最常见的一个场景了。</p>
<p>ok,我们将问题实例化:</p>
<p>A页面有个选择地区的按钮,需要打开B页面选择一个地区,然后获取到选取结果返回给A页面并展示。</p>
<p>我们看看用mui.fire怎么来实现这个功能</p>
<p><img src="/img/bV1H0X?w=105&h=110" alt="e9fbd9d5a31f5e16" title="e9fbd9d5a31f5e16"></p>
<p>A页面</p>
<p><img src="/img/bV1HYx?w=348&h=245" alt="clipboard.png" title="clipboard.png"></p>
<pre><code> <header class="mui-bar mui-bar-nav">
<h1 class="mui-title">A</h1>
</header>
<div class="mui-content">
<input type="text" readonly placeholder="未选择">
<button type="button" class="mui-btn mui-btn-blue">选取地区</button>
</div>
<script src="js/mui.min.js"></script>
<script type="text/javascript">
mui.init();
// 自定义监听select事件
document.addEventListener('select', function(e){
var text = e.detail.text;
document.querySelector("input").value = text;
});
// 按钮点击事件
document.querySelector(".mui-btn-blue").addEventListener('tap', function(){
// 打开B页面,选取一个结果
mui.openWindow('B.html');
});
</script></code></pre>
<p>B页面</p>
<p><img src="/img/bV1HYM?w=346&h=229" alt="clipboard.png" title="clipboard.png"></p>
<pre><code> <header class="mui-bar mui-bar-nav">
<a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left"></a>
<h1 class="mui-title">B</h1>
</header>
<div class="mui-content">
<ul class="mui-table-view">
<li class="mui-table-view-cell">
<a class="mui-navigate-right">
上海
</a>
</li>
<li class="mui-table-view-cell">
<a class="mui-navigate-right">
深圳
</a>
</li>
<li class="mui-table-view-cell">
<a class="mui-navigate-right">
北京
</a>
</li>
</ul>
</div>
<script src="js/mui.min.js"></script>
<script type="text/javascript">
mui.init();
mui('ul').on('tap', 'li', function() {
// 获取当前选择的内容
var text = this.innerText;
// 通知上个页面
var w = plus.webview.currentWebview();
var opener = w.opener();
mui.fire(opener, "select",{
text: text
});
// 关闭本页面
w.close();
});</code></pre>
<p>真机调试一下,o98k。</p>
<p><img src="/img/bV1H17?w=527&h=468" alt="clipboard.png" title="clipboard.png"></p>
<p>但是!我个人还是建议脱离mui.js来实现这个功能</p>
<p><img src="/img/bV1H3L?w=109&h=108" alt="clipboard.png" title="clipboard.png"></p>
<p>可以借用咱们之前文章里面的讲过的,利用webview对象的evalJS方法</p>
<p><a href="https://segmentfault.com/a/1190000008844889">【5+】跨webview多页面 触发事件(一)</a><br>【5+】跨webview多页面 触发事件(一)</p>
<p>感觉用Broadcast.js有点小题大做</p>
<p><img src="/img/bV1If1?w=109&h=72" alt="clipboard.png" title="clipboard.png"></p>
<p>那咱们就写一个类似Android中的onActivityResult和setResult方法</p>
<p>新建一个app.js,作为一个自己的插件,里面实现两个方法 onActivityResult 和 setResult</p>
<pre><code>
(function(app){
/**
* 打开一个页面
* @param {String} url 页面路径
* @param {String} id 页面id
* @param {Object} ex 参数
* @param {Function} callback
*/
app.onActivityResult = function(url, id, ex, callback){
};
/**
* 返回创建者页面数据
* @param {Object} data 需要返回的数据
* @return {Webview} w 当前webview
*/
app.setResult = function(data){
};
}(window.app || (window.app = {})));
</code></pre>
<p>我们一步步来,先看看setResult如何触发上个页面的函数</p>
<pre><code> /**
* 返回创建者页面数据
* @param {Object} data 需要返回的数据
* @return {Webview} w 当前webview
*/
app.setResult = function(data){
// 获取当前webview
var indexW = plus.webview.currentWebview();
// 获取创建者的webview
var opener = indexW.opener();
// 执行js字符串
opener.evalJS();// ??????
};</code></pre>
<p>卧槽,那么,问题来了,evalJS该执行什么呢? </p>
<p>如果我在A页面的window对象下定一个函数</p>
<pre><code>window.test = function(data){
alert(JSON.stringify(data));
}</code></pre>
<p>那么,我们在evalJS里面就该这么写</p>
<pre><code> // 执行js字符串
var jsstr = "window.test && window.test(" + JSON.stringify(data) + ")";
opener.evalJS(jsstr); </code></pre>
<p>好吧,考虑到一个页面可能通过这个方式打开多个页面,那么我们这个test函数就得改一个不重复唯一的名称,并且定义放到onActivityResult方法里面</p>
<pre><code> var _id = 0,
_tempName = '',
ow,cw;
/**
* 打开一个页面
* @param {String} url 页面路径
* @param {String} id 页面id
* @param {Object} ex 参数
* @param {Function} callback
*/
app.onActivityResult = function(url, id, ex, callback) {
// 生成唯一回调函数名称
_tempName = 'APP_RESULT_FUN_' + _id++;
// 定义函数
window[_tempName] = function(data){
// 执行自定义回调
callback(data);
};
// 传递函数名称到目标页面
ex.callbackName = _tempName;
// 显示菊花
cw = plus.nativeUI.showWaiting();
// 创建目标页面
ow = plus.webview.create(url, id, {
render: "always"
}, ex);
// title更新时显示 页面
ow.addEventListener('titleUpdate', function(){
// 关闭菊花
cw && (cw.close(),cw = null);
// 显示页面
ow.show('pop-in');
});
// 页面关闭时,注销window下此次事件
ow.addEventListener('close', function(){
setTimeout(function(){
window[_tempName] = null;
});
});
};</code></pre>
<p>生成特殊一个函数,并把函数名通过extras的方式传参到目标页面,<br>相应的,setResult方法也需要少许更改</p>
<pre><code> /**
* 返回创建者页面数据
* @param {Object} data 需要返回的数据
* @return {Webview} w 当前webview
*/
app.setResult = function(data) {
// 获取当前webview
var indexW = plus.webview.currentWebview();
// js字符串
var jsstr = "";
// 如果存在自定义回调函数名
if(indexW.callbackName){
// 拼接js字符串
jsstr = "window." + indexW.callbackName;
jsstr = jsstr + "&&" + jsstr + "(" + JSON.stringify(data) + ")";
// 执行
indexW.opener().evalJS(jsstr);
}
// 返回当前页面
return indexW;
};</code></pre>
<p>试试引用后在AB页面测试一下</p>
<pre><code> // A页面 按钮点击事件
document.querySelector(".mui-btn-blue").addEventListener('tap', function(){
// 打开B页面,选取一个结果
app.onActivityResult('B.html', 'B', {}, function(data){
// 修改内容
document.querySelector("input").value = data.text;
});
});
// B页面 选项点击事件
mui('ul').on('tap', 'li', function() {
// 获取当前选择的内容
var text = this.innerText;
// 通知上个页面 并关闭本页面
app.setResult({
text: text
}).close();
});</code></pre>
<p>卧槽666。</p>
<pre><code>class Man{
constructor(){
this.name = 'newsning'
}
say(){
console.log('天行健, 君子以自强不息. ')
}
}</code></pre>
【5+】跨webview多页面 触发事件(二)
https://segmentfault.com/a/1190000008857298
2017-03-28T10:42:48+08:00
2017-03-28T10:42:48+08:00
newsning
https://segmentfault.com/u/newsning
1
<h5>上一章我们了解到通过webview evalJS的方法来跨页面通知事件,但是在其中还是有需要优化的地方,接下来我们慢慢的来分析。</h5>
<p>上节回顾:<a href="https://segmentfault.com/a/1190000008844889">【5+】跨webview多页面 触发事件(一)</a><br>代码:</p>
<pre><code>//页面通知
class Broadcast{
/**
* 构造器函数
*/
constructor(){
}
/**
* 事件监听
* @param {String} eventName 事件名称
* @param {Function} callback 事件触发后执行的回调函数
* @return {Broadcast} this
*/
on(eventName, callback){
document.addEventListener(eventName, e => {
callback.call(e, e.detail)
})
return this
}
/**
* 事件触发
* @param {String} eventName 事件名称
* @param {Object} data 参数
* @return {Broadcast} this
*/
emit(eventName, data){
// 获取所有的webview
var all = plus.webview.all()
// 遍历全部页面
for(var w in all){
// 挨个来evalJS
all[w].evalJS(`document.dispatchEvent(new CustomEvent('${eventName}', {
detail:JSON.parse('${JSON.stringify(data)}'),
bubbles: true,
cancelable: true
}));`)
}
return this
}
}</code></pre>
<h3>自定义需要通知页面</h3>
<p>可以看到,之前我们emit发送通知时,是对所有的webview进行获取通知,但是有时候我们并不想通知所有的页面,而且通知别人的时候也不想通知自己啊,怎么办,在这里我们在emit方法参数多加一个配置项</p>
<pre><code> /**
* 事件触发
* @param {String} eventName 事件名称
* @param {Object} data 传参参数值
* @param {Object} options 其它配置参数
*/
emit(eventName, data, {
self = false, // 是否通知自己,默认不通知
views = [], // 为空数组时,默认通知全部,为string数组时,认为是id,为object时,认为是webview对象
} = {}) {
//code...
}</code></pre>
<p>然后我们针对传进来的拓展参数,进行逻辑判断,得到最终我们需要通知的webview list</p>
<pre><code> /**
* 事件触发
* @param {String} eventName 事件名称
* @param {Object} data 传参参数值
* @param {Object} options 其它配置参数
*/
emit(eventName, data, {
self = false, // 是否通知自己,默认不通知
views = [], // 为空数组时,默认通知全部,为string数组时,认为是id,为object时,认为是webview对象
} = {}) {
let all = []
// 获取 特定 webview 数组
if(views.length > 0) {
// 如果是string 类型,则统一处理获取为 webview对象
all.map(item => typeof item === 'string' ? plus.webview.getWebviewById(item) : item)
} else {
// 不特定通知的webview数组时,直接获取全部已存在的webview
all = plus.webview.all()
}
// 如果不需要通知到当前webview 则过滤
if(!self) {
let v = plus.webview.currentWebview()
all = all.filter(item => item.id !== v.id)
}
// 遍历所有需要通知的页面
for(let v of all) {
v.evalJS(`document.dispatchEvent(new CustomEvent('${eventName}', {
detail:JSON.parse('${JSON.stringify(data)}'),
bubbles: true,
cancelable: true
}));`)
}
}
</code></pre>
<p>如何调用</p>
<pre><code>new Broadcast().emit('say',{
name: 'newsning',
age: 26
},{
self: true, // 通知当前页面 默认不通知
views: ['A.html','C.html'] // 默认通知所有页面,但不包括当前页面
})
// 如上代码就只通知到了3个页面, 当前页面, A页面, C页面</code></pre>
<hr>
<h3>事件 - [ <strong>订阅</strong> | <strong>发布</strong> | <strong>取消</strong> ]</h3>
<p>如果你遇到那种还需要移除监听事件,亦或者Once只监听一次的事件,再或是你看个代码不爽<br><img src="/img/bVLj8S?w=684&h=110" alt="clipboard.png" title="clipboard.png"></p>
<p>ok!我们来撸一套简单的 守望先锋模式,哦不,是观察者模式</p>
<h4>事件订阅</h4>
<p>瞧瞧我们之前的代码,on方法是直接把传进来的函数作为调用,这样子在外部调用时移除事件就没路子了,包括Once也很是蛋疼</p>
<pre><code> /**
* 事件监听
* @param {String} eventName 事件名称
* @param {Function} callback 事件触发后执行的回调函数
* @return {Broadcast} this
*/
on(eventName, callback){
document.addEventListener(eventName, e => {
callback.call(e, e.detail)
})
return this
}</code></pre>
<p>我们先来定义好2个专门放置事件的存储对象,碧如 :</p>
<pre><code> // 事件列表
const events = {
// 事件名称 : 事件方法数组
},
// 单次事件列表
events_one = {
}</code></pre>
<p>之后我们修改一下on方法,并新增一个once方法</p>
<pre><code> /**
* 事件监听
* @param {String} eventName 事件名称
* @param {Function} callback 事件触发后执行的回调函数
*/
on(eventName, callback) {
// 获取已存在的事件列表
if(!events[eventName]) {
events[eventName] = []
}
// 添加至数组
events[eventName].push(callback)
}
/**
* 事件监听 (单次)
* @param {String} eventName 事件名称
* @param {Function} callback 事件触发后执行的回调函数
*/
once(eventName, callback) {
// 获取已存在的单次事件列表
if(!events_one[eventName]) {
events_one[eventName] = []
}
// 添加至数组
events_one[eventName].push(callback)
}</code></pre>
<p>酱紫,每次添加事件时,都会放入我们的事件列表中,但是!我们并没有给任何dom添加事件,而仅仅是放入所对应的事件列表中,奇怪了,看看我们之前的添加事件方法</p>
<p><img src="/img/bVLkbX?w=487&h=126" alt="clipboard.png" title="clipboard.png"></p>
<p>给document监听一个事件</p>
<p><img src="/img/bVLj8S?w=684&h=110" alt="clipboard.png" title="clipboard.png"></p>
<p>触发document事件</p>
<p>nonono , 我们不这么借助document亦或者其它dom的事件监听,还记得上一章的 evalJS('faqme()')么?我们就用亲切的函数来触发事件</p>
<h4>事件发布</h4>
<p>在事件订阅当中,我们仅仅只是把事件放入了事件列表中,我们该如何触发? </p>
<p>编写一个静态方法,用来触发当前页面的事件, 然后通过</p>
<pre><code> static _emitSelf(eventName, data) {
if(typeof data === 'string') {
data = JSON.parse(data)
}
// 获取全部事件列表 和 单次事件列表,并且合并
let es = [...(events[eventName] || []), ...(events_one[eventName] || [])]
// 遍历触发
for(let f of es) {
f && f.call(f, data)
}
// 单次事件清空
events_one[eventName] = []
}</code></pre>
<p>再配合修改一下 emit 里面的 evalJS</p>
<pre><code> /**
* 事件触发
* @param {String} eventName 事件名称
* @param {Object} data 传参参数值
* @param {Object} options 其它配置参数
*/
emit(eventName, data, {
self = false, // 是否通知自己,默认不通知
views = [], // 为空数组时,默认通知全部,为string数组时,认为是id,为object时,认为是webview对象
} = {}) {
let all = []
// 获取 特定 webview 数组
if(views.length > 0) {
// 如果是string 类型,则统一处理获取为 webview对象
all.map(item => typeof item === 'string' ? plus.webview.getWebviewById(item) : item)
} else {
// 不特定通知的webview数组时,直接获取全部已存在的webview
all = plus.webview.all()
}
// 如果不需要通知到当前webview 则过滤
if(!self) {
let v = plus.webview.currentWebview()
all = all.filter(item => item.id !== v.id)
}
// 遍历所有需要通知的页面
for(let v of all) {
/////////////////////////
////////////////这里是重点, 调用Broadcast的静态方法
/////////////////////////
v.evalJS(`Broadcast && Broadcast._emitSelf && Broadcast._emitSelf('${eventName}', '${JSON.stringify(data)}')`)
}
}
</code></pre>
<p>这样子,就巧妙的触发了每个webview页面 相对应的事件,并且单次事件也得到了清除</p>
<h4>事件移除</h4>
<p>我们知道前面的事件订阅只是将事件存起来了,事件移除相应的就是把事件列表清空</p>
<pre><code> static _offSelf(eventName) {
//清空事件列表
events[eventName] = []
events_one[eventName] = []
}</code></pre>
<h3>最后收尾</h3>
<p>所定义的2个静态方法,触发 和 移除 事件,我们在内部代理2个相应的方法</p>
<pre><code> /**
* 当前页面事件触发
* @param {String} eventName 事件名称
* @param {Object} data 传参参数值
*/
emitSelf(eventName) {
Broadcast._emitSelf(eventName, data)
}
/**
* 清空当前页面事件
* @param {String} eventName 事件名称
*/
offSelf(eventName) {
Broadcast._offSelf(eventName)
}</code></pre>
<p>最后,成果已经出现</p>
<p>A.html</p>
<pre><code> var b = new Broadcast()
b.on('say', function(data){
alert(JSON.stringify(data))
// 删除本页面say事件
//b.offSelf('say')
})
b.once('say', function(data){
//单次
alert('单次:'+JSON.stringify(data))
})</code></pre>
<p>B.html</p>
<pre><code> new Broadcast().emit('say', {
from: '我是B啊',
id: 666
})</code></pre>
<p>最后附上源码:</p>
<pre><code>/**
* 5+ Broadcast.js by NewsNing 宁大大
*/
// 获取当前webview
const getIndexView = (() => {
// 缓存
let indexView = null
return(update = false) => {
if(update || indexView === null) {
indexView = plus.webview.currentWebview()
}
return indexView
}
})(),
// 获取全部webview
getAllWebview = (() => {
// 缓存
let allView = null
return(update = false) => {
if(update || allView === null) {
allView = plus.webview.all()
}
return allView
}
})()
// 事件列表
const events = {
},
// 单次事件列表
events_one = {
}
//页面通知类
class Broadcast {
/**
* 构造器函数
*/
constructor() {
}
/**
* 事件监听
* @param {String} eventName 事件名称
* @param {Function} callback 事件触发后执行的回调函数
*/
on(eventName, callback) {
// 获取已存在的事件列表
if(!events[eventName]) {
events[eventName] = []
}
// 添加至数组
events[eventName].push(callback)
}
/**
* 事件监听 (单次)
* @param {String} eventName 事件名称
* @param {Function} callback 事件触发后执行的回调函数
*/
once(eventName, callback) {
// 获取已存在的单次事件列表
if(!events_one[eventName]) {
events_one[eventName] = []
}
// 添加至数组
events_one[eventName].push(callback)
}
/**
* 事件触发
* @param {String} eventName 事件名称
* @param {Object} data 传参参数值
* @param {Object} options 其它配置参数
*/
emit(eventName, data, {
self = false, // 是否通知自己,默认不通知
views = [], // 为空数组时,默认通知全部,为string数组时,认为是id,为object时,认为是webview对象
} = {}) {
let jsstr = `Broadcast && Broadcast._emitSelf && Broadcast._emitSelf('${eventName}', '${JSON.stringify(data)}')`
this._sendMessage(jsstr, self, views)
}
/**
* 当前页面事件触发
* @param {String} eventName 事件名称
* @param {Object} data 传参参数值
*/
emitSelf(eventName) {
Broadcast._emitSelf(eventName, data)
}
/**
* 事件关闭移除
* @param {String} eventName 事件名称
* @param {Object} options 其它配置参数
*/
off(eventName, {
self = false, // 是否通知自己,默认不通知
views = [] // 为空数组时,默认通知全部,为string数组时,认为是id,为object时,认为是webview对象
} = {}) {
let jsstr = `Broadcast && Broadcast._offSelf && Broadcast._offSelf('${eventName}')`
this._sendMessage(jsstr, self, views)
}
/**
* 清空当前页面事件
* @param {String} eventName 事件名称
*/
offSelf(eventName) {
Broadcast._offSelf(eventName)
}
/**
* 页面通知
* @param {String} jsstr 需要运行的js代码
* @param {Boolean} self 是否通知自己,默认不通知
* @param {Array} views 为空数组时,默认通知全部,为string数组时,认为是id,为object时,认为是webview对象
*/
_sendMessage(
jsstr = '',
self = false,
views = []
) {
let all = []
// 获取 特定 webview 数组
if(views.length > 0) {
// 如果是string 类型,则统一处理获取为 webview对象
all.map(item => typeof item === 'string' ? plus.webview.getWebviewById(item) : item)
} else {
// 不特定通知的webview数组时,直接获取全部已存在的webview
all = getAllWebview(true)
}
// 如果不需要通知到当前webview 则过滤
if(!self) {
let v = getIndexView()
all = all.filter(item => item.id !== v.id)
}
// 遍历全部页面
for(let v of all) {
v.evalJS(jsstr)
}
}
static _emitSelf(eventName, data) {
if(typeof data === 'string') {
data = JSON.parse(data)
}
// 获取全部事件列表 和 单次事件列表,并且合并
let es = [...(events[eventName] || []), ...(events_one[eventName] || [])]
// 遍历触发
for(let f of es) {
f && f.call(f, data)
}
// 单次事件清空
events_one[eventName] = []
}
static _offSelf(eventName) {
//清空事件列表
events[eventName] = []
events_one[eventName] = []
}
}</code></pre>
<p>您也可以通过babel在线转化成es5 <a href="https://link.segmentfault.com/?enc=qnIfwfJlISiaAfQM8tm56w%3D%3D.ccvlNGfSmx%2FvMgC3%2FP5BI0iJ7C5hdqrl7fcfExYkZ%2BE%3D" rel="nofollow">在线转换地址</a></p>
<p><img src="/img/bVLkld?w=1586&h=722" alt="clipboard.png" title="clipboard.png"></p>
<p>最后您还可以在github上看到一些其它5+ Api封装的源码 <a href="https://link.segmentfault.com/?enc=BuRejK6fn3V%2BwMF%2BOCrBBw%3D%3D.bH8nevdQXpVmA8%2BaymGAsUKtaqF5Jzq4RJ7jRof77M0%3D" rel="nofollow">5+ api整合</a></p>
<pre><code>class Man{
constructor(){
this.name = 'newsning'
}
say(){
console.log('天行健, 君子以自强不息. ')
}
}</code></pre>
【5+】跨webview多页面 触发事件(一)
https://segmentfault.com/a/1190000008844889
2017-03-27T13:30:49+08:00
2017-03-27T13:30:49+08:00
newsning
https://segmentfault.com/u/newsning
7
<p>在日常撸功能中,很多情况都需要用到通知页面,mui呢给我们已经内置写好啦,当当当,就是 <a href="https://link.segmentfault.com/?enc=hCgQ2yetkGoLUe7HzUMVew%3D%3D.kzWGyrqprJaaDK4tRvxc1rI0MTdp74ilBDMl1EbwFQiuXP17%2BUAHDR%2FlmKlEuZsL" rel="nofollow">mui.fire</a><br>我们来看看之前所写的用法</p>
<h3>mui.fire</h3>
<ul>
<li><a href="https://link.segmentfault.com/?enc=iPqzWYa7u7Z5%2Bj5ZiDlY3w%3D%3D.BBVNekzyglzbM3JHt8pnwct3ZBvCfQB3s7t86mSwwWqoIHY%2Fa85ReJA9Mweb69CW" rel="nofollow">Mui官方文档</a></li>
<li><a href="https://segmentfault.com/a/1190000007186287">【MUI】跨webview多页面 触发事件</a></li>
</ul>
<p><em>耶?这时候有童鞋就会问了,咋了之前不是写了mui.fire的文章了吗?为啥又有了这一篇捏?</em><br><strong>哈哈,这篇文章我们主要来讲解5+的实现方案,不用mui.fire,就相当于自己写一个通知的js功能,让大家更明白其原理,以及更好地不止是拘束于非得用到mui.js</strong></p>
<hr>
<h3>原理介绍</h3>
<h5>关键词</h5>
<ul>
<li>plus.webview.evalJS</li>
<li>dispatchEvent</li>
</ul>
<hr>
<p>在B页面通知A页面,我们暂时不管通知A页面的内容,只是希望B页面调用一段代码,能让A页面弹出一个alert<br>这时候我们就要用到webview的evalJS方法了</p>
<p>A.html</p>
<pre><code> <header class="mui-bar mui-bar-nav">
<h1 class="mui-title">我是A页面</h1>
</header>
<div class="mui-content">
<button type="button" class="mui-btn mui-btn-blue">打开B页面</button>
</div>
<script src="js/mui.min.js"></script>
<script type="text/javascript">
mui.init()
mui.plusReady(function(){
document.querySelector("button").addEventListener('tap',function(){
mui.openWindow('B.html')
})
})
</script></code></pre>
<p>B.html</p>
<pre><code> <header class="mui-bar mui-bar-nav">
<a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left"></a>
<h1 class="mui-title">我是B页面</h1>
</header>
<div class="mui-content">
<button type="button" class="mui-btn mui-btn-blue">通知A页面</button>
</div>
<script src="js/mui.min.js"></script>
<script type="text/javascript">
mui.init()
mui.plusReady(function(){
document.querySelector("button").addEventListener('tap',function(){
// 通知A页面的方法
})
})
</script></code></pre>
<p>那么,怎么才能通知A弹出框呢?我们需要用到关键词所提到的 <strong><a href="https://link.segmentfault.com/?enc=%2FlzKS1X5WWGihz5WaVGlCA%3D%3D.CQT0CZjF%2F0iROgnTRLs%2FWYqvcxFy%2F07yt5TGTXKQzG7HYlJNHmEmYZ4MjdK11fT8IymUhrSDhjOxWYRuC1advOmoOopuRQbCW0TOMgxPxRLaH4Dt6%2BQqTK6%2F4Bf1IxRl" rel="nofollow">plus.webview.evalJS</a></strong> <br>嘿嘿, 我们现在B获取到A的Webview对象,然后通过evalJS来向A页面发送一段代码让其执行</p>
<pre><code>// 通知A页面的方法
var A = plus.webview.getLaunchWebview()
A.evalJS('alert("我是被B的")')</code></pre>
<p>点击一下按钮,果不其然,我们的弹出框就显示出来,当然我们也还可以定义其它的函数来接收响应</p>
<p>A</p>
<pre><code>function faqme(){
alert('啊,乖乖站好!')
}</code></pre>
<p>B</p>
<pre><code>// 通知A页面的方法
var A = plus.webview.getLaunchWebview()
A.evalJS('faqme()')</code></pre>
<p>当然,A页面执行了faqme函数,弹出了乖乖站好</p>
<p><strong>其实,mui.fire的内部实现就是其原理</strong><br>我们可以看一下其代码</p>
<p><img src="/img/bVLg2p?w=981&h=384" alt="clipboard.png" title="clipboard.png"></p>
<p><img src="/img/bVLg2s?w=523&h=313" alt="clipboard.png" title="clipboard.png"></p>
<p><img src="/img/bVLg2n?w=512&h=289" alt="clipboard.png" title="clipboard.png"></p>
<p>这个 <strong>dispatchEvent</strong> 是什么呢?<br>我们可以将这个方法理解为用来触发dom事件<br>相关详细文档:<br><a href="https://link.segmentfault.com/?enc=bzrfrzjr49SiC%2BMYTdeGgg%3D%3D.XnONJ0T1Sm8bXtBJ%2Bnt1dt%2F6a5uRa55UOPxhNv4q1aPRPxG2tifXU97kgU%2BtAorsIezOYhc%2Bfc8OyNhN5rtmyA%3D%3D" rel="nofollow">事件触发器-----dispatchEvent</a></p>
<p>这下条理就很清楚拉!<br>A页面自定义事件 => B页面触发A页面事件回调并传参</p>
<p>嘿嘿,就是这么简单,但是本文章还没有结束,既然都到这了,干脆我们来自己封装一下这个通知功能吧!</p>
<h3>造轮子</h3>
<p>我们新建一个文件,美其名曰:Broadcast.js<br>在这里我采用ES6 Class的方式编写</p>
<pre><code>
//页面通知
class Broadcast{
/**
* 构造器函数
*/
constructor(){
}
}
</code></pre>
<p>我们先来实现最基础的两个功能</p>
<ul>
<li>监听事件(订阅)</li>
<li>触发事件(发布)</li>
</ul>
<pre><code>
//页面通知
class Broadcast{
/**
* 构造器函数
*/
constructor(){
}
/**
* 事件监听
* @param {String} eventName 事件名称
* @param {Function} callback 事件触发后执行的回调函数
* @return {Broadcast} this
*/
on(eventName, callback){
document.addEventListener(eventName, e => {
callback.call(e, e.detail)
})
return this
}
/**
* 事件触发
* @param {String} eventName 事件名称
* @param {Object} data 参数
* @return {Broadcast} this
*/
emit(eventName, data){
// 获取所有的webview
var all = plus.webview.all()
// 遍历全部页面
for(var w in all){
// 挨个来evalJS
all[w].evalJS(`document.dispatchEvent(new CustomEvent('${eventName}', {
detail:JSON.parse('${JSON.stringify(data)}'),
bubbles: true,
cancelable: true
}));`)
}
return this
}
}
</code></pre>
<p>ok, 我们在页面中引用并尝试用一下</p>
<p>A</p>
<pre><code> <header class="mui-bar mui-bar-nav">
<h1 class="mui-title">我是A页面</h1>
</header>
<div class="mui-content">
<button type="button" class="mui-btn mui-btn-blue">打开B页面</button>
</div>
<script src="js/mui.min.js"></script>
<script type="text/javascript" src="js/Broadcast.js" ></script>
<script type="text/javascript">
mui.init()
mui.plusReady(function(){
document.querySelector("button").addEventListener('tap',function(){
mui.openWindow('B.html')
})
})
new Broadcast().on('say', function(data){
alert(JSON.stringify(data))
})
</script></code></pre>
<p>B</p>
<pre><code> <header class="mui-bar mui-bar-nav">
<a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left"></a>
<h1 class="mui-title">我是B页面</h1>
</header>
<div class="mui-content">
<button type="button" class="mui-btn mui-btn-blue">通知A页面</button>
</div>
<script src="js/mui.min.js"></script>
<script type="text/javascript" src="js/Broadcast.js" ></script>
<script type="text/javascript">
mui.init()
mui.plusReady(function(){
document.querySelector("button").addEventListener('tap',function(){
// 通知A页面的方法
//var A = plus.webview.getLaunchWebview()
//A.evalJS('alert("我是被B的")')
new Broadcast().emit('say', {
from: '我是B啊',
id: 666
})
})
})
</script></code></pre>
<p>点击B页面的按钮</p>
<p><img src="/img/bVLg6S?w=385&h=693" alt="clipboard.png" title="clipboard.png"></p>
<p>哇哈哈,基础功能已经实现了怎么样,<br>当然,这只是最基础的实现了监听,触发而已,后续还需要更多的优化,以及管理,辣么,下章见</p>
<p>下一章节:<a href="https://segmentfault.com/a/1190000008857298">【5+】跨webview多页面 触发事件(二)</a></p>
<pre><code>class Man{
constructor(){
this.name = 'NewsNing'
}
say(){
console.log('天行健, 君子以自强不息. ')
}
}</code></pre>
vue 多页面 + HBuilder 打包 + HTML5PLUS
https://segmentfault.com/a/1190000008176524
2017-01-20T11:39:19+08:00
2017-01-20T11:39:19+08:00
newsning
https://segmentfault.com/u/newsning
7
<h2>vue 多页面 + HBuilder 打包 + HTML5PLUS 特性</h2>
<blockquote>A Vue + H5Plus project</blockquote>
<p>使用UI : <a href="https://link.segmentfault.com/?enc=phdziF8EuihDhxfV8Tjujg%3D%3D.Qx8ti3vdBj5QBSejE38JfJ3T3LlqNhvdBuSZ8ppHVBE%3D" rel="nofollow">Muse-UI</a> 基于 Vue 2.0 和 Material Desigin 的 UI 组件库<br>真机调试 : <a href="https://link.segmentfault.com/?enc=t9Pda2EJyXspa7790r7z%2FQ%3D%3D.yF9gnJYSwDHIXTZd1x9kCXlHIE%2Bum9lYIr5AF7yCjr8%3D" rel="nofollow">HBuilder</a> HBuilder-飞速编码的极客工具</p>
<p>关键字:</p>
<p>1)换肤(支持webview多页面换肤)<br>2)简易机器人聊天<br>3)技术性文章浏览<br>4)支持流应用版本</p>
<h5>2017年2月3日 18:33:02</h5>
<p>新增主页webview拖拽切换(需要 hbuilder 8.0 版本)</p>
<p><a href="https://link.segmentfault.com/?enc=7wFWdgYREwPZSqRfyLgH%2Bg%3D%3D.wOQv6S7DNedzpVkVOPGvCi%2FFoF7OMDreslP5sJyjgMz0tTbJfHBdUyRIVJj093Yu" rel="nofollow">github源码地址</a></p>
<h3>开始</h3>
<pre><code class="bash"># 安装依赖
npm install
# 在服务器上调试 localhost:8080/module/index.html
npm run dev
## 然后在HBuilder中起始页设置为 本地服务器:端口号/module/index.html
### 如 192.168.11.102:8080/module/index.html
## 手机连接电脑, 在统一局域网下, 开启真机调试
# 你也可以先 打包
npm run build
## 然后把 dist 文件下的目录放入 HBuilder 项目中
## 设置起始页为 module/index.html 真机调试</code></pre>
<p>扫描二维码<br>在流应用中快速预览<br><img src="/img/remote/1460000008176527?w=144&h=145" alt="测试二维码" title="测试二维码"></p>
<h3>部分截图</h3>
<p><img src="/img/remote/1460000008176528?w=540&h=960" alt="主页" title="主页"></p>
<p><img src="/img/remote/1460000008176529?w=1440&h=2560" alt="机器人聊天" title="机器人聊天"></p>
<p><img src="/img/remote/1460000008176530?w=540&h=960" alt="侧栏" title="侧栏"></p>
<p><img src="/img/remote/1460000008176531?w=540&h=960" alt="其它" title="其它"></p>
【MUI】Android监听电话呼入呼出
https://segmentfault.com/a/1190000007421972
2016-11-08T21:11:20+08:00
2016-11-08T21:11:20+08:00
newsning
https://segmentfault.com/u/newsning
4
<h2>序章</h2>
<p>还记得吗?在前段时间,我们撸过了h5的video标签元素,和h5+的plus特性编写了一个小小的播放器。至今位置改了一些代码运用到了项目中,却发现出现了一个蛋疼的问题。</p>
<p>Android : 后台,锁屏,来电都不能自动暂停,哭瞎 T-T。<br>ios : 后台,锁屏,来电,都会自动暂停。得益于苹果的系统限制,以上情况下都会暂停音视频的播放。</p>
<h2>搞起</h2>
<p>先来分析一波,后台事件,5+的文档中早已出现过。</p>
<pre><code> document.addEventListener('pause',function(){
//code...
//暂停播放
});</code></pre>
<p>这里需要跟大家提醒下<strong><em>document.addEventListener</em></strong>千万不要写成<strong><em>window.addEventListener</em></strong>因为我就写错了一次,我说怎么调试都不成功。哈哈</p>
<p>再来再来,下一个下一个,<strong>锁屏</strong>是吧,我一开始在官方文档中翻了一会儿并没有发现有这个事件,后来仔细想想,锁屏的时候,不就正是另一个叫做<em>‘锁屏’</em>的应用程序切换到了<em>前台</em>,而我们的App切换到了<em>后台</em>吗?<br>果不其然,不需要增加额外的事件,监听了上面的后台事件即可,也会触发。</p>
<p>SO,视频播放中,来电话了,视频并不会暂停? Excuse Me?如果是我还好,看一些美剧什么的(<br><strong><em> 强势推荐大家看:《闪电侠》 </em></strong><br>)<br>咳咳,要是别人在看一些那啥怎么办,orz 。这就很尴尬了,当初并没有想到Android来电不会自动暂停H5 video的播放。</p>
<p>一时间找文档也没有发现 <strong><em>来电事件</em></strong></p>
<p>好吧,拿起Native.js大宝剑</p>
<p>1:打开 Native.js 文档 <a href="https://link.segmentfault.com/?enc=hlycqlrfItvzHNdvtofujg%3D%3D.sUzzp%2FC0HY2jVrnWqMqcplq3I6jDJf1%2FoHPX3Bvzq573VDL0QTjcO91f3OY5tn54" rel="nofollow">Native.js</a><br>2:打开 Native.js 范例 <a href="https://link.segmentfault.com/?enc=k%2FENqpbg%2FRWDcV5PaOCcUA%3D%3D.pkU7haPBsVcE2mU4x98fDnVo6JkJakdzoJCU3jAcb0RYCFwlmseEgQyPeSdUYD1D" rel="nofollow">Native范例汇总</a><br>3:打开 baidu.com 搜索 关键字 <a href="https://link.segmentfault.com/?enc=gi9W1k8d7GLMz%2FzWcKMjAQ%3D%3D.Znd8CQhCNm7q7JmO86px5Du1PF%2BjgiZ1FHQTaBxUp8t4yq5DvZvSjypyFqaInQ1r%2FkbPbtKpatv9EWiqFJqvC%2F75K1Px8oqz5zvY%2BLfaWWA%3D" rel="nofollow">android 来电监听</a></p>
<p>差不多就这些了,就算我这样不怎么懂原生的菜鸟,还是照葫芦画瓢,约摸着写出来点。</p>
<pre><code>var Native = (function($) {
var native = {};
var receiver, main, context, TelephonyManager;
native.listenTelPhone = function(callback) {
$.plusReady(function() {
context = plus.android.importClass('android.content.Context'); //上下文
TelephonyManager = plus.android.importClass('android.telephony.TelephonyManager'); //通话管理
main = plus.android.runtimeMainActivity(); //获取activity
receiver = plus.android.implements('io.dcloud.feature.internal.reflect.BroadcastReceiver', {
onReceive: doReceive //实现onReceiver回调函数
});
var IntentFilter = plus.android.importClass('android.content.IntentFilter');
var Intent = plus.android.importClass('android.content.Intent');
var filter = new IntentFilter();
filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED); //监听电话状态
main.registerReceiver(receiver, filter); //注册监听
});
function doReceive(context, intent) {
plus.android.importClass(intent);
var phoneNumber = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER),
telephony = context.getSystemService(context.TELEPHONY_SERVICE),
state = telephony.getCallState();
switch(state) {
case TelephonyManager.CALL_STATE_RINGING:
callback && callback(1,phoneNumber);
//console.log("[Broadcast]等待接电话=" + phoneNumber);
break;
case TelephonyManager.CALL_STATE_IDLE:
callback && callback(0,phoneNumber);
console.log("[Broadcast]电话挂断=" + phoneNumber);
break;
case TelephonyManager.CALL_STATE_OFFHOOK:
callback && callback(2,phoneNumber);
console.log("[Broadcast]通话中=" + phoneNumber);
break;
}
}
},
native.removeListenTelPhone = function(){
if(receiver){
main = plus.android.runtimeMainActivity(); //获取activity
main.unregisterReceiver(receiver); //删除监听
receiver = null;
}
}
return native;
}(mui));
</code></pre>
<p>暴露出2个方法,添加来电监听广播,以及删除。</p>
<pre><code> //监听电话 code 1来电 0挂断 2童话中 number 对方电话号码
mui.os.android && Native.listenTelPhone(function(code, number) {
if(code === 1) { //来电
//暂停播放
//code...
}
});
//去除电话广播监听
mui.os.android && Native.removeListenTelPhone();</code></pre>
<p>嘿嘿嘿嘿,嘿嘿嘿嘿</p>
【MUI】跨webview多页面 触发事件
https://segmentfault.com/a/1190000007186287
2016-10-17T00:14:02+08:00
2016-10-17T00:14:02+08:00
newsning
https://segmentfault.com/u/newsning
1
<h2>前言</h2>
<p>好久没写文章了,这段时间太忙了</p>
<h2>干货</h2>
<p>直接上代码吧,语言组织的不行,没时间想好怎么写,先去休息。</p>
<p>app.js</p>
<pre><code>var App = (function($, undefined) {
var app = {};
//通过参数个数的不同实现重载
app.addMethod = function(name, fn) {
var ofn = app[name];
app[name] = function() {
if(fn.length === arguments.length) {
return fn.apply(this, arguments);
} else if(typeof ofn === 'function') {
return ofn.apply(this, arguments);
}
};
};
return app;
}(mui));
//本地存储
(function(app) {
var keyName = 'store', //方法名称
store = window.localStorage,
nkey = function(k) {
return '_news_' + k + '_ning_';
},
storeFn = function(fn) {
app.addMethod(keyName, fn);
};
//获取
storeFn(function(key) {
var str = store.getItem(nkey(key)) || '{}';
return JSON.parse(str);
});
//全部覆盖
storeFn(function(key, val) {
var str = JSON.stringify(val) || '{}';
store.setItem(nkey(key), str);
});
//追加并覆盖key
storeFn(function(key, val, isAppend) {
var oval = storeFn(key);
for(var i in val) {
oval[i] = val[i];
}
storeFn(key, oval);
});
}(App));
//多页面 监听事件
(function(app, $) {
var storeKeyName = '_NEWSNING_EVENTS_LISTENER_';
var eList = function(){
arguments[0] = storeKeyName + arguments[0];
return app.store.apply(this,arguments);
},
eMethod = function(key, fn, isMore) {
var arg = arguments,
keyArr = eList(key);
if($.isEmptyObject(keyArr)) {
keyArr = [];
}
if(fn) { //如果是增添事件
$.plusReady(function() {
//获取当前webview的id
var wid = plus.webview.currentWebview().id;
if(keyArr.length > 0 && keyArr.indexOf(wid) >= 0){
console.log('页面:'+ wid +'已存在事件:'+key);
//如果已经监听 则不做任何
return;
}
//添加事件
window.addEventListener(key, function _newsning_event_(e) {
fn.apply(fn, arguments);
//是否可以多次触发
isMore || (window.removeEventListener(key, _newsning_event_));
});
console.log('页面:'+ wid +'添加事件:' + key);
//增添至事件数组
keyArr.push({
id:wid,
once:!isMore
});
//保存至本地
eList(key, keyArr);
});
} else { //触发事件
var i = 0,
l = keyArr.length,
tempW,temp,nArr=[];
$.plusReady(function() {
for(; i < l; i++) {
temp = keyArr[i];
//获取需要触发事件的webview
tempW = plus.webview.getWebviewById(temp.id);
$.fire(tempW, key, isMore);
temp.once || nArr.push(temp);
}
//覆盖
eList(key,nArr);
});
}
};
app.events = {
//添加事件 once :是否只监听一次 默认监听一次
add: function(kname, fn, more) {
eMethod(kname, fn, more);
},
//触发事件
fire: function(kname, data) {
eMethod(kname, null, data);
},
remove: function(kname) {
eList(kname, []);
}
}
}(App, mui));</code></pre>
<p>index.html</p>
<pre><code>mui.init();
App.events.remove('demolistener');
//监听
App.events.add('demolistener', function(e) {
var data = e.detail.data;
console.log(JSON.stringify(data));
}, true); //false 时 为只被触发一次
mui.ready(function() {
document.getElementById("open").addEventListener('tap', function() {
mui.openWindow('index2.html');
});
});</code></pre>
<p>index2.html</p>
<pre><code>mui.init();
mui.ready(function(){
document.getElementById("fireBtn").addEventListener('tap',function(){
App.events.fire('demolistener',{
'message': 'i\'m coming back !'
});
});
});</code></pre>
<p>各位看官自己试试,还有些地方没来得及优化</p>
【MUI】5+之SuperAD(超级广告栏)
https://segmentfault.com/a/1190000006783194
2016-08-31T23:35:50+08:00
2016-08-31T23:35:50+08:00
newsning
https://segmentfault.com/u/newsning
1
<h2>占位</h2>
<p>时间不够,我先休息,我称之为超级广告,利用plus.nativeObj.view跨跨跨webview显示在最最最顶端的 SuperAD</p>
<p>纯属无聊之作</p>
<h2>先看看图</h2>
<p><img src="/img/bVCCMr?w=540&h=960" alt="clipboard.png" title="clipboard.png"><br><img src="/img/bVCCMt?w=540&h=960" alt="clipboard.png" title="clipboard.png"><br><img src="/img/bVCCMs?w=540&h=960" alt="clipboard.png" title="clipboard.png"></p>
<p>未完待续...</p>
【MUI】5+ 预截原动画 打开webview案例
https://segmentfault.com/a/1190000006731711
2016-08-27T00:25:49+08:00
2016-08-27T00:25:49+08:00
newsning
https://segmentfault.com/u/newsning
2
<h2>讲几句</h2>
<p>每次官方的案例不是太多,包括有段时间出世的plus.nativeObj.View<br>可以先看看官方的文章<br><a href="https://link.segmentfault.com/?enc=YULVt16BQkjbmSc%2B6GLFJQ%3D%3D.7b1mOh%2BxP82AM8qYag%2BQJ0KIME%2BbBPftVPR7hrgSdoCqmPEWH0Mi2b06VTft55Vq" rel="nofollow">plus.nativeObj.View解析</a><br><a href="https://link.segmentfault.com/?enc=2j%2Btp6Tdo6D1mGdfrj4wDQ%3D%3D.J%2F8uRvZrFAKKDRPG14jky8kA57jXD%2FESf4l67nx4oQkUOuukxwwl0QGmsdXxrrFr" rel="nofollow">HTML5+ 官方 API</a></p>
<p>不说别的,咱们来看看官方人员的这一段解释</p>
<p><img src="/img/bVCpk7?w=778&h=177" alt="clipboard.png" title="clipboard.png"></p>
<p>看起来很高大上啊!来来来,趁着晚上的时间折腾一下。</p>
<h2>WebView截图</h2>
<p>新建一个bitmap类型的变量(其实是原生的一种映射变量)</p>
<pre><code>var bitmap = new plus.nativeObj.Bitmap('nwbitmap');//nwbitmap为bitmap的id</code></pre>
<p>然后将webview内容绘制到Bitmap对象中</p>
<pre><code> var ws = plus.webview.currentWebview();
ws.draw(bitmap,function(){
console.log('截屏绘制图片成功');
},function(e){
console.log('截屏绘制图片失败:'+JSON.stringify(e));
});</code></pre>
<p>为了能获取到每一个webview的截图,我们封装一下</p>
<pre><code> //webview截图
var drawWebView = function(webview, bitmap, callback) {
bitmap = bitmap || new plus.nativeObj.Bitmap('defultBitMap');
webview.draw(bitmap, function() {
callback && callback(bitmap);
}, function(err) {
callback && callback();
console.log('截图错误:' + JSON.stringify(err))
});
}</code></pre>
<h2>原生动画</h2>
<p>原生动画这块的资料太多,我这里不做多解释,大家可以查阅资料<a href="https://link.segmentfault.com/?enc=6NKZ4wWBTOJsK%2F6sViYN4Q%3D%3D.XsWq8KxKShvWUvoZK23YRax5MkR%2Bagg%2F6rJOdKF1McrkBLQPj%2BhXK2GZ8TI2VY1gff%2BRsMJAt%2Fw56jo7qRW34KCBcAZXS7e8Fy3JLX3Mx9Vlhkebza8cxVy%2FTNB9bOKu" rel="nofollow">5+开始原生动画</a></p>
<pre><code> //开始原生动画
var startAnimation = function(type, bitmap, callback) {
plus.nativeObj.View.startAnimation({
type: type || 'pop-in'//pop-in从右到左划入 pop-out从右到左划出
}, {}, {
bitmap: bitmap
}, function() {
console.log('动画结束');
callback && callback();
//关闭原生动画 在这里我注释,选择在回调函数callback内处理
//plus.nativeObj.View.clearAnimation();
});
}</code></pre>
<h2>对比</h2>
<p>直接上干货吧<br>app.js</p>
<pre><code>var App = (function($) {
var app = {};
app.openWindow = function(url, id, op, data) {
var nw = plus.webview.create(url, id, op, data),
bitmap = new plus.nativeObj.Bitmap('nwbitmap');
//开始原生动画
var startAnimation = function(type, bitmap, callback) {
plus.nativeObj.View.startAnimation({
type: type || 'pop-in'//默认
}, {}, {
bitmap: bitmap
}, function() {
console.log('动画结束');
callback && callback();
//关闭原生动画
//plus.nativeObj.View.clearAnimation();
});
}
//webview截图
var drawWebView = function(webview, bitmap, callback) {
bitmap = bitmap || new plus.nativeObj.Bitmap('defultBitMap');
webview.draw(bitmap, function() {
callback && callback(bitmap);
}, function(err) {
callback && callback();
console.log('截图错误:' + JSON.stringify(err))
});
}
//webview onloaded事件
nw.onloaded = function(e) {
//开始截图open
drawWebView(nw, bitmap, function(bitmap) {
if(!bitmap) {
nw.show('pop-in', 300);
return;
}
//播放页面打开的动画
startAnimation('pop-in', bitmap, function() {
//动画播放完毕后
//显示webview
nw.show('none', 0, function() {
//当webview关闭时
nw.onclose = function() {
//播放页面关闭动画
startAnimation('pop-out', bitmap, function() {
//关闭页面原生动画
plus.nativeObj.View.clearAnimation();
});
}
//关闭页面原生动画
plus.nativeObj.View.clearAnimation();
})
});
});
}
}
return app;
}(mui))</code></pre>
<p>main.html</p>
<pre><code><!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<link href="css/mui.min.css" rel="stylesheet" />
</head>
<body>
<script src="js/mui.min.js"></script>
<script src="js/app.js" ></script>
<script>
mui.init();
mui.ready(function(){
var url = 'feedback.html';
document.querySelectorAll("button")[0].addEventListener('tap',function(){
App.openWindow(url,'hehe');
});
document.querySelectorAll("button")[1].addEventListener('tap',function(){
mui.openWindow(url,url,{
show:{
duration:300
},
waiting:{
autoShow:false
}
});
});
});
</script>
<button type="button" class="mui-btn mui-btn-blue mui-btn-block">截图方式 打开页面</button>
<button type="button" class="mui-btn mui-btn-blue mui-btn-block">MUI默认 打开页面</button>
</body>
<script>
</script>
</html></code></pre>
<p>看看效果</p>
<p><img src="/img/bVCpmk?w=364&h=557" alt="clipboard.png" title="clipboard.png"></p>
<p>可能动态图不是很清晰,在这里我总结一下<br>用预截图做动画打开webview 的方式 我简称 <strong>预截原动画</strong><br>用默认webview的位移、渐变的效果打开webview 的方式 我简称 <strong>渐变帧动画</strong></p>
<p><strong>预截原动画</strong><br>预截原动画是通过先把要打开的webview创建,然后监听onloaded事件,截一张webview的bitmap图 给 原生动画 调用<br>无疑这种方式的渲染打开是很稳的,然而可能是我代码逻辑问题,也可能是受webview默认的打开动画影响,在关闭时会瞬间闪屏。我也不多做解释。<br>预截原动画,真的很稳,打开新的页面,可以看得出,不掉帧,mui默认渐变帧动画的方式会稍微掉帧,如果页面dom js 复杂可能效果差异更大</p>
<p><strong>渐变帧动画</strong><br>渐变帧动画是给webview的位置以及样式作出调整,从而达到一个动画的目的,但是当你的页面dom复杂,js加载量大,尤其是图片加载较多的话,那么在这种方式的Webview动画就会掉帧啦</p>
<p>不过可以尝试<strong>截图原动画</strong>的方式去处理较复杂页面,否则,我们用了这么久的<strong>渐变帧动画</strong>完全够用了啦。</p>
<p>晚安,我要睡了,卧槽,这么晚了</p>
<h2>返回动画修复</h2>
<p>前面说过了,动态图也看到了,用<strong>预截原动画</strong>打开的页面,返回,也就是关闭的时候会闪屏一下,现在有新的解决方案,虽然马虎点</p>
<p>1)使用mui的预加载 新页面</p>
<p>2)截图新页面<br>截图成功就监听</p>
<p>loaded事件:为播放打开动画<br>show事件:为关闭动画 <br>hide事件:再次截图新页面<br>![图片上传中...]<br>,并播放关闭动画,且播放完毕后,关闭页面,关闭动画</p>
<p>截图失败 就使用<strong> 渐变帧动画 </strong>打开新页面</p>
<p>js改动如下</p>
<pre><code>var App = (function($) {
var app = {};
//开始原生动画
function startAnimation(type, bitmap, callback) {
plus.nativeObj.View.startAnimation({
type: type || 'pop-in' //默认
}, {}, {
bitmap: bitmap
}, function() {
callback && callback(plus.nativeObj.View.clearAnimation);
});
}
//webview截图
function drawWebView(webview, bitmap, callback) {
bitmap = bitmap || new plus.nativeObj.Bitmap('defultBitMap');
webview.draw(bitmap, function() {
callback && callback(bitmap);
}, function(err) {
callback && callback();
console.log('截图错误:' + JSON.stringify(err))
});
}
app.openWindow = function(op) {
if(op.id && plus.webview.getWebviewById(op.id)) {
return;
}
var nw = $.preload(op),
bitmap = new plus.nativeObj.Bitmap('nwbitmap');
//webview onloaded事件
nw.onloaded = function(e) {
//开始截图open
drawWebView(nw, bitmap, function(bitmap) {
if(!bitmap) {
//截图失败就降级用默认打开
nw.show('pop-in');
return;
}
nw.addEventListener('show', function() {
plus.nativeObj.View.clearAnimation();
})
nw.addEventListener('hide', function() {
drawWebView(nw, bitmap, function(bitmap) {
if(!bitmap) {
//截图失败就降级用默认打开
nw.close('pop-out');
return;
}
//播放页面关闭动画
startAnimation('pop-out', bitmap, function(close) {
nw.close();
//关闭页面原生动画
close();
});
bitmap.clear();
bitmap = null;
});
})
//播放页面打开的动画
startAnimation('pop-in', bitmap, function(close) {
//动画播放完毕后
//显示webview
nw.show('none');
//close();
});
});
}
}
return app;
}(mui))</code></pre>
<p>html页面多增加了几个图片,更好的对比渲染情况</p>
<pre><code><!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<link href="css/mui.min.css" rel="stylesheet" />
</head>
<body>
<script src="js/mui.min.js"></script>
<script src="js/app.js" ></script>
<script>
mui.init();
mui.ready(function(){
var url = 'feedback.html';
document.querySelectorAll("button")[0].addEventListener('tap',function(){
App.openWindow({
url:url,
id:'hehe'
});
});
document.querySelectorAll("button")[1].addEventListener('tap',function(){
mui.openWindow(url,url,{
waiting:{
autoShow:false
}
});
});
});
</script>
<button type="button" class="mui-btn mui-btn-blue mui-btn-block">截图方式 打开页面</button>
<button type="button" class="mui-btn mui-btn-blue mui-btn-block">MUI默认 打开页面</button>
</body>
<script>
</script>
</html></code></pre>
<p>效果图</p>
<p>效果图2.5m被吃了,尼玛 <a href="https://link.segmentfault.com/?enc=lAqpfUihncEIdEB6X3slxA%3D%3D.iHUrrGVVQMpdE%2BoIzFuQVNdOaQdDPRPAcyz40YOQXvUMo70EOgbMoJw7JO2NS4lOMX9NFVZVy4tiAjLxsjrbfoTRT7WMPgwGDj9t%2F8Fscao%3D" rel="nofollow">动态图</a></p>
<p>本文源码<a href="https://link.segmentfault.com/?enc=v8Q8M7tTm2SZ%2BWVUB70peQ%3D%3D.0%2BuRGFuFsyFxdnxNgzo5DCP48YS2Lpbknrkfw0459J6CLYj%2Fhz3GcMNS7xnA31LBzeatGQxk09KXUGRiTezv9Q%3D%3D" rel="nofollow">5+ 预截原动画</a></p>
【MUI】利用Native.js弹出软键盘打开WIFI
https://segmentfault.com/a/1190000006720078
2016-08-25T21:41:25+08:00
2016-08-25T21:41:25+08:00
newsning
https://segmentfault.com/u/newsning
0
<h2>痛点</h2>
<p>很多时候我们会遇到这样一个细节,需要打开一个webview时:<br>1)输入框自动获取焦点<br>2)系统弹出软键盘</p>
<h2>准备</h2>
<p>新建一个html文件,我暂且命名为input.html</p>
<p><img src="/img/bVCmdz?w=1052&h=393" alt="clipboard.png" title="clipboard.png"></p>
<p>新建一个js文件,我暂且命名为app.js</p>
<p><img src="/img/bVCmdM?w=328&h=174" alt="clipboard.png" title="clipboard.png"></p>
<p>要输入框自动获取焦点,js很简单,但是为了兼容ios以及android,我们加上setTimeout</p>
<pre><code> var iem;
mui.ready(function(){
iem = document.querySelector("input");
setTimeout(function(){
iem.focus();
},200);
})</code></pre>
<h2>Native.js自动弹出软键盘</h2>
<p>ok,那么弹出软键盘怎么办?这里我们将用到native.js来编写代码映射来调用原生,什么是<a href="https://link.segmentfault.com/?enc=E6TTxqNaqNLnZC5ieMDtRg%3D%3D.pyB9mCWYlJF8h2b%2FcSF7BkdKUSPqw8R1vs5fokqIf54rl%2FO9wphtfJt4VSD%2BfID7" rel="nofollow">Native.js</a>?</p>
<p>我们在app.js写上弹出软键盘函数</p>
<pre><code>window.App = (function($) {
var app = {};
/**
* Input获取焦点 弹出软键盘
* @param {HTMLInputElement} inputElem
*/
app.showSoftInput = function(inputElem) {
var nativeWebview, imm, InputMethodManager;
var initNativeObjects = function() {
if($.os.android) {//android
var main = plus.android.runtimeMainActivity();
var Context = plus.android.importClass("android.content.Context");
InputMethodManager = plus.android.importClass("android.view.inputmethod.InputMethodManager");
imm = main.getSystemService(Context.INPUT_METHOD_SERVICE);
plus.android.importClass(nativeWebview);
nativeWebview.requestFocusFromTouch();
//显示软键盘
imm.toggleSoftInput(0, InputMethodManager.SHOW_FORCED);
} else {//ios
nativeWebview.plusCallMethod({
"setKeyboardDisplayRequiresUserAction": false
});
}
};
$.plusReady(function() {
nativeWebview = plus.webview.currentWebview().nativeInstanceObject();
initNativeObjects();
setTimeout(function() {
inputElem.focus();
}, 200);
});
}
return app;
}(mui))</code></pre>
<p>上面这个代码应该大家很熟悉了,在<a href="https://link.segmentfault.com/?enc=Qr7uqzrpUt%2BkIf9VA6L3ig%3D%3D.lf2aXcYbykJBgH8vEd8A%2BE7K7SEoaUsaYLzGDshOq0ehRSq3thPm6wzT3bdRj9SN" rel="nofollow">MUI问答社区</a>有很多技术问答,当然也包括以上这些代码,但是呢?在ios设备上确实挺快的弹出了键盘,<strong>然而在中端左右的android设备上,延迟总会延迟那么一两秒,带来不好的用户体验,甚至用户第一次使用时不知道是自动弹出的,往往会在input上按下她们的手指,这时键盘弹出来了,可是我们写的自动弹出也并发了,导致键盘又弹缩回去了</strong>,我擦,怎么办?</p>
<p>解决方案1)</p>
<pre><code>去除android的弹出键盘功能</code></pre>
<p>解决方案2)</p>
<pre><code>如果input已经被用户点击,即Input已经获取到了焦点的话,直接return;不执行自动弹出</code></pre>
<p>第二种方案看起来可以接受的样子</p>
<pre><code> /**
* Input获取焦点 弹出软键盘
* @param {HTMLInputElement} inputElem
*/
app.showSoftInput = function(inputElem) {
if(!inputElem) {//如果没有传入input则return
return;
}
if(!inputElem.id) {//如果当前input没有id,就给个默认的id
this.input.id = inputElem.id = 'InputDefultId';
}
var nativeWebview, imm, InputMethodManager;
var initNativeObjects = function() {
if($.os.android) {
var main = plus.android.runtimeMainActivity();
var Context = plus.android.importClass("android.content.Context");
InputMethodManager = plus.android.importClass("android.view.inputmethod.InputMethodManager");
imm = main.getSystemService(Context.INPUT_METHOD_SERVICE);
plus.android.importClass(nativeWebview);
nativeWebview.requestFocusFromTouch();
//强制显示软键盘
//imm.showSoftInput(nativeWebview,InputMethodManager.SHOW_FORCED);
imm.toggleSoftInput(0, InputMethodManager.SHOW_FORCED);
} else {
nativeWebview.plusCallMethod({
"setKeyboardDisplayRequiresUserAction": false
});
}
};
$.plusReady(function() {
if(document.activeElement.id === inputElem.id) {
//如果当前活动,也就是获取到了焦点的是input的话,那么就不执行自动弹出键盘
return;
}
nativeWebview = plus.webview.currentWebview().nativeInstanceObject();
initNativeObjects();
setTimeout(function() {
inputElem.focus();
}, 200);
});
}
</code></pre>
<p>嘿嘿,在这里我将再安利大家一个小技巧</p>
<p>native调用的是原生,语法上是跟原生差不多的,这里用android来讲解,例如您要实现弹出软键盘的功能,我们打开百度搜索(android 弹出软键盘)</p>
<p>先看看native实现android弹出软键盘的代码</p>
<p><img src="/img/bVCmhO?w=682&h=117" alt="clipboard.png" title="clipboard.png"></p>
<p>再来看看我们百度的 android原生实现的代码</p>
<p><img src="/img/bVCmh1?w=664&h=68" alt="clipboard.png" title="clipboard.png"></p>
<p>是不是tm好像啊?哈哈</p>
<h2>Native.js打开WIFI</h2>
<p>我们再来一个栗子<img src="/img/bVCmin" alt="u=1556826177,1292336595&fm=116&gp=0.jpg" title="u=1556826177,1292336595&fm=116&gp=0.jpg"></p>
<p>百度一下 (android 打开wifi)</p>
<p><img src="/img/bVCmkS?w=572&h=325" alt="clipboard.png" title="clipboard.png"></p>
<p>楼主老实说,android我只入了个门,像打开wifi的代码我都还没写过<img src="/img/bVCmi2?w=50&h=50" alt="clipboard.png" title="clipboard.png"></p>
<p>但是并不要紧,我们开始造轮子!!!</p>
<p>编写js代码</p>
<pre><code> /**
* 设置wifi状态
*/
app.setWifi = function(isEnable){
$.plusReady(function(){
if($.os.android){
isEnable = !!isEnable;
//获取上下文
var Context = plus.android.importClass("android.content.Context"),
//导入wifi管理模块
WifiManager = plus.android.importClass("android.net.wifi.WifiManager"),
//获取wifi服务
wifiManager = plus.android.runtimeMainActivity().getSystemService(Context.WIFI_SERVICE);
wifiManager.setWifiEnabled(isEnable);//false 为关闭
}
});
}</code></pre>
<p>对比一下,是不是很清真?哈哈,ios打开wifi的话,目前是这种解决方案<br>跳转到wifi设置状态下</p>
<pre><code>if($.os.ios){
plus.runtime.openURL("prefs:root=WIFI");//ios需要手动打开
}</code></pre>
<h2>Native.js好玩吗?</h2>
<p>好不好玩,您不试试怎么知道?</p>
H5打造属于自己的视频播放器(JS篇2)
https://segmentfault.com/a/1190000006604046
2016-08-16T23:28:17+08:00
2016-08-16T23:28:17+08:00
newsning
https://segmentfault.com/u/newsning
38
<h2>回顾</h2>
<p>算了不回顾了 <img src="/img/bVBQyx?w=77&h=76" alt="clipboard.png" title="clipboard.png"><br>直接搞起,打开JS1中写的bvd.js</p>
<h2>播放视频</h2>
<ol>
<li>播放按钮隐藏</li>
<li>
<p>视频开始播放<br> 当点击播放按钮的时候,播放按钮将会隐藏,播放视频,这个不难,在JS1中我们就已经实现。但我们改变一下思维,给视频添加点击tap事件,使视频播放,再触发播放事件,从而让播放按钮隐藏</p>
<pre><code>pro.initEvent = function(){
var that = this;
//给播放按钮图片添加事件
this.vimg.addEventListener('tap',function(){
that.video.play();
})
//视频点击暂停或播放事件
this.video.addEventListener('tap',function(){
if(this.paused || this.ended) {
//暂停时点击就播放
if(this.ended) {//如果播放完毕,就重头开始播放
this.currentTime = 0;
}
this.play();
} else {
//播放时点击就暂停
this.pause();
}
})
//视频播放事件
this.video.addEventListener('play',function(){
that.vimg.style.display = 'none';
})
//获取到元数据
this.video.addEventListener('loadedmetadata',function(){
that.vC.querySelector('.duration').innerHTML = stom(this.duration);
});
}</code></pre>
</li>
<li>
<p>下方控制条渐渐隐藏<br> 隐藏并不是难点,重要的是渐渐的隐藏,在这里我们有这么几种解决方案:</p>
<ol>
<li>定时器</li>
<li>css3 动画帧</li>
</ol>
</li>
</ol>
<p>在这里我们2种结合起来使用<img src="/img/bVBQGQ?w=61&h=61" alt="clipboard.png" title="clipboard.png"></p>
<p>首先我们先定义好一组动画</p>
<pre><code>@keyframes vhide {0% {opacity: 1;}100% {opacity: 0;}}
@-webkit-keyframes vhide {0% {opacity: 1;}100% {opacity: 0;}}
.vhidden {
animation: vhide 3.5s ease-in;
-webkit-animation: vhide 3.5s ease-in;
}</code></pre>
<p>其作用就是透明度3.5秒内1=>0,ease-in 就是 由慢到快 的过度效果。有不懂css动画可以问问度娘哦<br>然后我们给视频开始播放事件的时候给<strong>控制条</strong>添加vhidden样式类</p>
<pre><code>//视频播放事件
this.video.addEventListener('play',function(){
that.vC.classList.add('vhidden');
})</code></pre>
<p>测试效果,果然3.5s内,控制条 慢慢透明,问题是3.5s后,透明度又回到了1,这里我讲解一下,是因为动画帧默认是回弹的,我们可以加个样式</p>
<pre><code>.vhidden {
animation: vhide 3.5s ease-in;
-webkit-animation: vhide 3.5s ease-in;
animation-fill-mode:forwards;
-webkit-animation-fill-mode: forwards;
}</code></pre>
<p>CSS3 属性 animation-fill-mode 用来定义元素在动画结束后的样子。</p>
<p>animation-fill-mode 的默认值是 none,也就是在动画结束之后不做任何改动,如果把animation-fill-mode 改成 forwards 则动画结束后元素的样式会变成动画最后一个关键帧所规定的样式。</p>
<p>加上这个样式后,果然3.5s后,动画不再回弹了,但是这里要留意一下,控制条并不是不在了而是透明了,如果这时我们有写控制条的点击时间,那么在控制条位置点击,还是会触发事件,所以呢,我们还可以写上一段setTimeout来,让控制条3.5s后隐藏,这个大家可以自行取舍</p>
<pre><code>//视频播放事件
this.video.addEventListener('play',function(){
that.vimg.style.display = 'none';
that.vC.classList.add('vhidden');
that.vCt = setTimeout(function(){
that.vC.style.visibility = 'hidden';
},3400);
})</code></pre>
<p>为什么动画过程是3.5s,然而js是是3.4s后执行,这里只是在未写animation-fill-mode:forwards的情况下做个保险</p>
<p><img src="/img/bVBQUI?w=313&h=237" alt="clipboard.png" title="clipboard.png"></p>
<h2>正在播放中</h2>
<p>嘿嘿,视频可以播放啦!那么现在我们该考虑一下播放中有哪些事要做呢?</p>
<p><strong>1. 控制条进度条慢慢增长</strong></p>
<p>我们需要给视频添加一条<strong>timeupdate</strong>音视频播放位置发生改变时的事件</p>
<p>我们先在获取视频元数据事件中,把视频的长度给拿下来</p>
<pre><code>//获取到元数据
this.video.addEventListener('loadedmetadata',function(){
that.vDuration = this.duration;
that.vC.querySelector('.duration').innerHTML = stom(that.vDuration);
});</code></pre>
<p>再从视频播放进度更新事件中计算比例,设置进度条的宽度</p>
<pre><code>//视频播放中事件
this.video.addEventListener('timeupdate', function() {
var currentPos = this.currentTime;//获取当前播放的位置
//更新进度条
var percentage = 100 * currentPos / that.vDuration;
//设置宽度
that.vC.querySelector('.timeBar').style.width = percentage + '%';
});</code></pre>
<p><img src="/img/bVBRgG?w=810&h=373" alt="clipboard.png" title="clipboard.png"></p>
<p>可以看到我们的进度条君越来越膨胀了。</p>
<p><strong>2. 当前播放时间变化</strong></p>
<p>同时,我们的当前播放时间显示也在<strong>timeupdate</strong>事件中设置</p>
<pre><code>//视频播放中事件
this.video.addEventListener('timeupdate', function() {
var currentPos = this.currentTime;//获取当前播放的位置
//更新进度条
var percentage = 100 * currentPos / that.vDuration;
that.vC.querySelector('.timeBar').style.width = percentage + '%';
//更新当前播放时间
that.vC.querySelector('.current').innerHTML = stom(currentPos);
});</code></pre>
<p><img src="/img/bVBRiq?w=297&h=32" alt="clipboard.png" title="clipboard.png"></p>
<h2>暂停 or 停止</h2>
<p>当我们点击视频时,如果是暂停,那就开始播放,并触发播放事件,反之视频在播放中,点击视频就会暂停,并触发暂停事件。</p>
<p><strong>0. 时间定格</strong></p>
<p>啦啦啦,暂停播放,<strong>timeupdate</strong>事件自然就不触发啦,所以进度条和当前播放时间就不会变啦。</p>
<p><strong>1. 播放按钮显示</strong></p>
<p>在暂停的时候,显示出按钮就行啦</p>
<pre><code>//暂停or停止
this.video.addEventListener('pause',function(){
that.vimg.style.display = 'block';
});</code></pre>
<p><strong>2. 下方控制条显示</strong></p>
<p>控制条显示,直接去除那个vhidden样式类就好啦</p>
<pre><code>//暂停or停止
this.video.addEventListener('pause',function(){
that.vimg.style.display = 'block';
that.vC.classList.remove('vhidden');
that.vC.style.visibility = 'visible';
});</code></pre>
<p>这样写看样子是没错啦,但是,如果大家在之前隐藏控制条的时候写了setTimeout的话,这个时候就要清除掉哦。</p>
<pre><code>//暂停or停止
this.video.addEventListener('pause',function(){
that.vimg.style.display = 'block';
that.vC.classList.remove('vhidden');
that.vC.style.visibility = 'visible';
that.vCt && clearTimeout(that.vCt);
});</code></pre>
<h2>快进快退</h2>
<p>一个叼叼哒的小视频播放器怎么可能少的了可进可退能屈能伸呢?</p>
<p>来,我们先为video添加左滑右滑事件</p>
<pre><code>//视频手势右滑动事件
this.video.addEventListener('swiperight',function(e){
this.currentTime += 5;
});
//视频手势左滑动事件
this.video.addEventListener('swipeleft',function(e){
this.currentTime -= 5;
});</code></pre>
<p>可能在电脑上调试会直接进度变0,一开始我也纳闷呢,后来发现手机上webview中好像是可行的。</p>
<p>关于<strong> 进度条拖动改变视频进度 </strong> 我暂时不打算写,因为我还没写。<img src="/img/bVBRMQ?w=58&h=55" alt="clipboard.png" title="clipboard.png"></p>
<h2>全屏播放</h2>
<p>可能大家会比较关注这个吧:</p>
<p>ios端:去除video标签webkit-playsinline属性即可,因为ios对h5的video标签支持还是比较不错的</p>
<pre><code>//调用原生方式 全屏播放
pro.nativeMax = function(){
if(!window.plus){
//非html5+环境
return;
}
if($.os.ios){
console.log('ios')
this.video.removeAttribute('webkit-playsinline');
}else if($.os.android){
console.log('android');
var url = this.video.querySelector('source').src;
var Intent = plus.android.importClass("android.content.Intent");
var Uri = plus.android.importClass("android.net.Uri");
var main = plus.android.runtimeMainActivity();
var intent = new Intent(Intent.ACTION_VIEW);
var uri = Uri.parse(url);
intent.setDataAndType(uri, "video/*");
main.startActivity(intent);
}
}</code></pre>
<p>在initEvent中添加点击 全屏 事件</p>
<pre><code>this.vC.querySelector('.fill').addEventListener('tap',function(){
that.nativeMax();
});</code></pre>
<p>这样做有点鸡肋啊,就不能来点通用的?</p>
<p>确实这个问题我想了一晚上,决定再拿点干货来。</p>
<p>先给个状态,默认为mini播放</p>
<pre><code>var bvd = function(dom) {
var that = this;
$.ready(function() {
//获取视频元素
that.video = document.querySelector(dom || 'video');
//获取视频父元素
that.vRoom = that.video.parentNode;
//元素初始化
that.initEm();
//事件初始化
that.initEvent();
//记录信息
that.initInfo();
//当前播放模式 false 为 mini播放
that.isMax = false;
})
}
//记录信息
pro.initInfo = function() {
var that = this;
//在onload状态下,offsetHeight才会获取到正确的值
window.onload = function(){
that.miniInfo = {//mini状态时的样式
width: that.video.offsetWidth + 'px',
height: that.video.offsetHeight + 'px',
position: that.vRoom.style.position,
transform: 'translate(0,0) rotate(0deg)'
}
var info = [
document.documentElement.clientWidth || document.body.clientWidth,
document.documentElement.clientHeight || document.body.clientHeigth
],
w = info[0],
h = info[1],
cha = Math.abs(h - w) / 2;
that.maxInfo = {//max状态时的样式
width: h + 'px',
height: w + 'px',
position: 'fixed',
transform: 'translate(-' + cha + 'px,' + cha + 'px) rotate(90deg)'
}
}
}
//全屏 mini 两种模式切换
pro.switch = function() {
var vR = this.vRoom;
//获取需要转换的样式信息
var info = this.isMax ? this.miniInfo : this.maxInfo;
for(var i in info) {
vR.style[i] = info[i];
}
this.isMax = !this.isMax;
}
//全屏按钮
this.vC.querySelector('.fill').addEventListener('tap', function() {
//that.nativeMax();
that.switch();
});</code></pre>
<p>瞧一瞧拉,看一看拉</p>
<p><img src="/img/bVBR8W?w=302&h=518" alt="clipboard.png" title="clipboard.png"></p>
<p>看起来感觉很不错呢,利用css3的位移和旋转,让视频全屏在了屏幕前,但是问题也随之而来了</p>
<ul><li>播放按钮 以及 控制条 在全屏下 似乎隐藏了,其实是video标签盖在了父元素之上,我们作出相应的调整</li></ul>
<p>css</p>
<pre><code>.bad-video {
position: relative;
/*overflow: hidden;*/
background-color: #CCCCCC;
}</code></pre>
<p>js<br>max配置当中,设置zIndex值</p>
<pre><code> that.maxInfo = {//max状态时的样式
zIndex:99,
width: h + 'px',
height: w + 'px',
position: 'fixed',
transform: 'translate(-' + cha + 'px,' + cha + 'px) rotate(90deg)'
}</code></pre>
<p><img src="/img/bVBXPj?w=322&h=554" alt="clipboard.png" title="clipboard.png"></p>
<ul><li>横向全屏后,左右滑动事件没有跟着方向改变</li></ul>
<pre><code> //视频手势右滑动事件
this.video.addEventListener('swiperight', function(e) {
console.log('right');
this.currentTime += 5;
});
//视频手势左滑动事件
this.video.addEventListener('swipeleft', function(e) {
console.log('left');
this.currentTime -= 5;
});</code></pre>
<p><img src="/img/bVBXQH?w=636&h=558" alt="clipboard.png" title="clipboard.png"></p>
<p>这TM就很尴尬了,难道我全屏后,手机横放,还去上下快进快退?</p>
<p>这时候怎么办呢,不要方</p>
<h2>手势滑动事件</h2>
<p>我们先给video注册一个事件列表</p>
<pre><code> var events = {};
//增加 或者删除事件
pro.eve = function(ename, callback, isF) {
if(callback && typeof(callback) == 'function') {
isF && arguments.callee(ename);
events[ename] = callback;
this.video.addEventListener(ename, events[ename]);
console.log('添加事件:' + ename);
return;
}
var fun = events[ename] || function(){};
this.video.removeEventListener(ename, fun);
console.log('删除事件:' + ename);
return fun;
}</code></pre>
<p>给video事件添加一个代理来删除添加事件,isF就是在新增这个事件是否删除之前的这个相同的事件,因为添加事件用匿名函数的话,是不能删除的,这样设置一个代理就可以把动态添加的事件记录在events里面,便于操作</p>
<p>这时我们补上修改当前播放进度和音量的功能</p>
<pre><code> //跳转视频进度 单位 秒
pro.setCurrentTime = function(t){
this.video.currentTime += t;
}
//设置音量大小 单位 百分比 如 0.1
pro.setVolume = function(v){
this.video.volume+= v;
}</code></pre>
<p>再通过代理给video添加左右上下滑动的事件</p>
<pre><code> //视频手势右滑动事件
this.eve('swiperight',function(){
that.setCurrentTime(5);
});
//视频手势左滑动事件
this.eve('swipeleft', function(e) {
that.setCurrentTime(-5);
});
//视频手势上滑动事件
this.eve('swipeup',function(){
that.setVolume(0.2);
});
//视频手势下滑动事件
this.eve('swipedown', function(e) {
that.setCurrentTime(-0.2);
});</code></pre>
<p>ok,四个方向的滑动事件已经添加过去了,但这是mini模式播放时的事件,在全屏播放下,四个方向事件并没有跟着video元素方向的改变而改变,这下需要再通过最最最笨的方式判断是否全屏从而触发的事件</p>
<pre><code> //视频手势右滑动事件
this.eve('swiperight',function(){
if(that.isMax){
return that.setVolume(0.2);
}
that.setCurrentTime(5);
});
//视频手势左滑动事件
this.eve('swipeleft', function() {
if(that.isMax){
return that.setVolume(-0.2);
}
that.setCurrentTime(-5);
});
//视频手势上滑动事件
this.eve('swipeup',function(){
if(that.isMax){
return that.setCurrentTime(-5);
}
that.setVolume(0.2);
});
//视频手势下滑动事件
this.eve('swipedown', function() {
if(that.isMax){
return that.setCurrentTime(5);
}
that.setVolume(-0.2);
});</code></pre>
<p>怎么样,虽然看起来有点stupid,但是很实用呢</p>
<h2>5+客户端全屏解决方案</h2>
<p>虽说在5+客户端,android可以调用原生的方式播放,但还是差强人意,我们可以再来看一套解决方案</p>
<p><strong>初始化时,记录mini时的样式,全屏时,通过修改视频宽度为屏幕高度,视频高度修改为视频宽度,再利用5+的屏幕旋转,设置全屏,隐藏状态栏</strong></p>
<p>0)去除手势事件判断</p>
<pre><code>因为现在是准备改变移动设备的方向,所以,手势方向会跟着设备方向改变
</code></pre>
<p><img src="/img/bVB8qs?w=316&h=285" alt="clipboard.png" title="clipboard.png"></p>
<p>1)去除 css3 旋转以及位移</p>
<pre><code>
//记录信息
pro.initInfo = function() {
var that = this;
//在onload状态下,offsetHeight才会获取到正确的值
window.onload = function() {
that.miniInfo = { //mini状态时的样式
zIndex: 1,
width: that.video.offsetWidth + 'px',
height: that.video.offsetHeight + 'px',
position: that.vRoom.style.position
}
that.maxInfo = { //max状态时的样式
zIndex: 99,
width: '100%',
height: that.sw + 'px',
position: 'fixed'
}
}
}
</code></pre>
<p>2)该用5+的设置全屏以及隐藏状态栏</p>
<pre><code> //全屏 mini 两种模式切换
pro.switch = function() {
var vR = this.vRoom;
//获取需要转换的样式信息
var info = this.isMax ? this.miniInfo : this.maxInfo;
for(var i in info) {
vR.style[i] = info[i];
}
this.isMax = !this.isMax;
plus.navigator.setFullscreen(this.isMax);
if(this.isMax) {
//横屏
plus.screen.lockOrientation("landscape-primary");
} else {
//竖屏
plus.screen.lockOrientation("portrait-primary");
}
}</code></pre>
<p>3)全屏状态下,android端返回键,触发退出全屏</p>
<pre><code>pro.initEvent = function() {
//.......省略其他代码
this.oback = $.back;
//监听安卓返回键
$.back = function() {
if(that.isMax) {
that.switch();
return;
}
that.oback();
}
}</code></pre>
<p>效果图<br><img src="/img/bVB320" alt="1.gif" title="1.gif"></p>
<h2>5+重力感应切换全屏</h2>
<p>嘿嘿,一个在移动端的播放器怎么能少得了 自动切换 横竖屏呢?<br>在个小节当中就讲了如何手动切换全屏,接下来重力感应切换横屏,需要用到5+的API <a href="https://link.segmentfault.com/?enc=fHS9vguk%2FO0%2BHBXFnXliSw%3D%3D.pVAaopFquFYAccGxGF%2FkSTJaj9DLZgS8aRyaanLvgImwQIeJNx9h3PiGBbK%2BO2Ry%2BgQIZTSEDTkugzkM3txEoQ%3D%3D" rel="nofollow"><strong>Accelerometer</strong></a> 加速度感应</p>
<pre><code>简单说:重力加速度感应可以想象成一个小球在坐标系中
三个方向上的加速度。永远以手机屏幕为准</code></pre>
<p>啥是加速度?额,就是物理书上的</p>
<pre><code>手机水平放置向上是y轴正向
向右是x轴正向,向外是z轴正向</code></pre>
<p>啥是xyz轴?额,就是高数书上的</p>
<p>哎呀,你把手机竖屏正直的放在地上,你人正直走上去,现在你站在你的手机的屏幕上,然后你的右手打开伸直,这就是x轴,你现在看着前面,这就是y轴,你的头顶就是z轴。这样讲明白了不,但是并不是真的要你踩手机,23333</p>
<p>您也可以选择查看其他讲解:<a href="https://link.segmentfault.com/?enc=Q1hnC9aX5eWGtf5XYUaTCw%3D%3D.loQYuagWget4ow45mzOapr2NDwCwnBya4GdClUTZOav7TsxrwfkQSxiS0RZYGMSPR8KGt0DiRSZR1J71WaRVa3pRfcDus79G%2B1OEWSoZozQ%3D" rel="nofollow">Android-传感器开发-方向判断 </a></p>
<ol>
<li>x,y轴变化:<p>手机屏幕向上水平放置时: (x,y,z) = (0, 0, -9.81)<br> 当手机顶部抬起时: y减小,且为负值<br> 当手机底部抬起时: y增加,且为正值<br> 当手机右侧抬起时: x减小,且为负值<br> 当手机左侧抬起时: x增加,且为正值</p>
</li>
<li>z轴的变化:<br> 手机屏幕向上水平放置时,z= -9.81<br> 手机屏幕竖直放置时, z= 0<br> 手机屏幕向下水平放置时,z= 9.81</li>
<li>屏幕横竖切换条件<br> y<=-5时, 切换为竖向<br> x<=-5时, 换为横向</li>
</ol>
<p>ok,我们新增2个方法,用于打开和关闭设备监控</p>
<pre><code> //开启方向感应
pro.startWatchAcc = function(){
var that = this;
this.watchAccFun = plus.accelerometer.watchAcceleration(function(a) {
if(that.getIsMax()){
//当前为全屏状态
//判断是否满足竖屏Mini状态
a.yAxis>=5 && that.setIsMax(false);
}else{
//当前为Mini状态
//判断是否满足全屏Max状态
Math.abs(a.xAxis) >=5 && that.setIsMax(true);
}
}, function(e) {
//出错了大不了 不自动旋转呗 让它手动 切换
console.log("Acceleration error: " + e.message);
that.clearWatchAcc();
},{
frequency:1200
});
}
//关闭方向感应
pro.clearWatchAcc = function(){
this.watchAccFun && plus.accelerometer.clearWatch(this.watchAccFun);
}
</code></pre>
<p>然后在初始化的时候默认打开方向监控</p>
<pre><code> var bvd = function(dom) {
var that = this;
$.ready(function() {
//...
})
$.plusReady(function() {
that.startWatchAcc();
})
}</code></pre>
<p>再把横向全屏改为,可双向横屏</p>
<p><img src="/img/bVB8oQ?w=502&h=339" alt="clipboard.png" title="clipboard.png"></p>
<p>真机调试看看</p>
<p><img src="/img/bVB8qi" alt="ziong.gif" title="ziong.gif"></p>
<p>嘿嘿,我们再给全屏播放时添加一个锁定按钮,让设备不监控 重力感应,也不响应视频的点击播放暂停事件</p>
<p>先做一个锁定按钮</p>
<p><img src="/img/bVB8JJ?w=990&h=176" alt="clipboard.png" title="clipboard.png"></p>
<p>当然,锁定图片,地址也改成用base64,最好也用js动态生成标签</p>
<p><img src="/img/bVB8JT?w=1023&h=283" alt="clipboard.png" title="clipboard.png"></p>
<p>设置它的基本样式,靠右,上下垂直居中,默认隐藏</p>
<pre><code> .lock {
padding: .3rem;
width: 3rem;
height: 3rem;
position: absolute;
right: .5rem;
top: 50%;
transform: translateY(-50%);
-webkit-transform: translateY(-50%);
visibility: hidden;
}</code></pre>
<p><img src="/img/bVB8J0?w=602&h=398" alt="clipboard.png" title="clipboard.png"></p>
<p>好,我们来整理一下逻辑,</p>
<p>1)默认在mini播放时,lock隐藏<br>2)全屏播放时,lock显示,但是也会跟着控制条 在4s内向右隐藏<br>3)全屏暂停时,lock也跟着控制条 一直显示<br>4)点击lock锁定时,提示已锁定,控制条立即隐藏,lock4s内向右隐藏,视频点击事件更换为显示lock图标,android返回键事件改为不做任何,关闭重力监控<br>5)点击lock解锁时,提示已解锁,android返回键改为 切换为mini状态,开启重力监控</p>
<p>我擦,其实做起来还是挺郁闷的,主要是逻辑处理比较痛苦</p>
<p>0)添加一个向右移动的动画,3s延迟后 1s内 执行完动画</p>
<pre><code>@keyframes lockhide {0% {transform: translate(0%,-50%);}100% {transform: translate(120%,-50%);}}
webkit-keyframes lockhide {0% {transform: translate(0%,-50%);}100% {transform: translate(120%,-50%);}}
.lockhidden {
animation: lockhide 1s 3s linear;
-webkit-animation: lockhide 1s 3s linear;
animation-fill-mode:forwards;
-webkit-animation-fill-mode: forwards;
}
</code></pre>
<p>1)全屏时显示lock</p>
<pre><code> pro.switch = function() {
//...
//全屏时 显示锁定 图标
this.vlock.style.visibility = this.isMax ? 'visible' : 'hidden';
}</code></pre>
<p>2)全屏播放时,lock显示,但是也会跟着控制条 在4s内向右隐藏<br>我们在播放时添加lock的隐藏动画,</p>
<p><img src="/img/bVB8LC?w=420&h=183" alt="clipboard.png" title="clipboard.png"></p>
<p>3)全屏暂停时,lock也跟着控制条 一直显示</p>
<p><img src="/img/bVB8LE?w=395&h=164" alt="clipboard.png" title="clipboard.png"></p>
<p>4)点击lock锁定时,提示已锁定,控制条立即隐藏,lock4s内向右隐藏,视频点击事件更换为显示lock图标,android返回键事件改为不做任何,关闭重力监控<br>5)点击lock解锁时,提示已解锁,android返回键改为 切换为mini状态,开启重力监控</p>
<pre><code> //锁定屏幕
pro.lockScreen = function() {
$.toast('锁定屏幕');
var that = this;
//更换video点击事件为 显示 lock图标,并保存 video之前的事件
this.videoTapFn = this.eve('tap', function() {
that.lockT = setTimeout(function(){
that.vlock.classList.add('lockhidden');
},500);
//重新开始播放样式
that.vlock.classList.remove('lockhidden');
that.vlock.style.visibility = 'visible';
}, true);
//隐藏控制条
this.vC.style.visibility = 'hidden';
//给Lock图标增加 隐藏样式类
this.vlock.classList.add('lockhidden');
//锁定屏幕时,不监控重力感应
this.clearWatchAcc();
//标识当前更改的Lock状态
this.isLock = true;
}
//解锁屏幕
pro.unlockScreen = function() {
$.toast('解锁屏幕');
//替换回video之前的点击事件
this.eve('tap', this.videoTapFn, true);
//给Lock图标清楚 隐藏样式类
this.vlock.classList.remove('lockhidden');
//不锁定屏幕时,监控重力感应
this.startWatchAcc();
//标识当前更改的Lock状态
this.isLock = false;
}
</code></pre>
<p>666)最后给我们亲爱的lock图标增加一枚抚摸事件,以及android返回键的事件更改</p>
<pre><code> //全屏 时 锁定点击事件
this.vlock.addEventListener('tap', function() {
if(that.isLock) {
that.unlockScreen();
return;
}
that.lockScreen();
});
this.oback = $.back;
//监听安卓返回键
$.back = function(){
if(that.isMax){
if(!that.isLock){
//全屏状态下 按下返回键 时,1s内不监控重力,防止返回Mini状态时和重力感应并发事件
setTimeout(function(){
that.startWatchAcc();
},1000);
that.clearWatchAcc();
that.switch();
}
return;
}
that.oback();
}
}</code></pre>
<p>好了!本文5+全屏demo <a href="https://link.segmentfault.com/?enc=G4cNHsKnX31jbhMOQt5mhg%3D%3D.cijZVy7Y9B1CwTaRW%2F89V5yjPqxB10uK3bcIbent8GZZ7Psbd%2BFhtt1%2FeYoIchcz" rel="nofollow">源码地址</a></p>
<p>写博客不易,但是那种分享的心情是很不错的,何尝不是另一种温习和进步呢?</p>
<p>谢谢各位。</p>
<p>本文相关文章:<a href="https://segmentfault.com/blog/248">H5打造属于自己的视频播放器 专栏</a></p>
H5打造属于自己的视频播放器(JS篇1)
https://segmentfault.com/a/1190000006569543
2016-08-16T13:36:29+08:00
2016-08-16T13:36:29+08:00
newsning
https://segmentfault.com/u/newsning
10
<h2>回顾</h2>
<p>1)<a href="https://segmentfault.com/a/1190000006461476">H5打造属于自己的视频播放器(HTML篇)</a><br>2)<a href="https://segmentfault.com/a/1190000006477658">H5打造属于自己的视频播放器(逻辑篇)</a></p>
<h2>前言</h2>
<p>在HTML篇当中我们写好了样式,今天我们为video新增一个样式</p>
<pre><code>.bad-video video {
width: 100%;
height: 100%;
display: block;
object-fit:fill;
}</code></pre>
<p>高,宽,内容,都充父元素</p>
<p>乳此一来,我们的HMTL元素内容为</p>
<pre><code> <div class="bad-video">
<video webkit-playsinline>
<source src='xx.mp4' type="video/mp4"></source>
<p>设备不支持</p>
</video>
<img src="img/play.png" class="vplay" />
<div class="controls">
<div>
<div class="progressBar">
<div class="timeBar"></div>
</div>
</div>
<div><span class="current">00:00</span>/<span class="duration">00:00</span></div>
<div><span class="fill">全屏</span></div>
</div>
</div></code></pre>
<h2>吹着口哨写JS</h2>
<p>幸好我们在写JS之前,先整理好了逻辑思绪,并写好了笔记<a href="https://segmentfault.com/a/1190000006477658">H5打造属于自己的视频播放器(逻辑篇)</a></p>
<p>以下文章将要用到 号称 <strong>最接近原生APP体验的高性能前端框架</strong> 的 <a href="https://link.segmentfault.com/?enc=xUEYOAp3i3D6gnlIr0MU0A%3D%3D.0Slr3wn0MQnHfc3mBcudSWmXqo4KK96Nm8fyIVv%2FdrU%3D" rel="nofollow"><strong>MUI</strong></a> </p>
<p>首先,让我们来新建一个js文件,比如,bvd.js 并注册一下全局的一个入口</p>
<pre><code>(function($){
var bvd = function(dom) {
this.dom = document.querySelector(dom || 'video');
}
var pro = bvd.prototype;
pro.test = function(){
console.log(this.dom)
}
var nv = null;
$.bvd = function(dom) {
return nv || (nv = new bvd(dom));
}
}(mui))
</code></pre>
<p>让我们在video.html引入bvd.js 并且来测试一下吧!</p>
<pre><code><scrpit src="js/mui.min.js"></script>
<script src="js/bvd.js"></script>
<script>
var v = mui.bvd();
v.test();
</script></code></pre>
<p><img src="/img/bVBHMm?w=714&h=307" alt="clipboard.png" title="clipboard.png"></p>
<p>看来是ok的,与此同时,为了html页的简洁美观,我们将<strong>播放按钮</strong>和<strong>控制条</strong>用JS动态生成</p>
<pre><code>(function($){
var bvd = function(dom) {
var that = this;
$.ready(function(){
//获取视频元素
that.video = document.querySelector(dom || 'video');
//获取视频父元素
that.vRoom = that.video.parentNode;
//元素初始化
that.initEm();
})
}
var pro = bvd.prototype;
pro.initEm = function(){
//先添加播放按钮
this.vimg = document.createElement("img");
this.vimg.src = 'img/play.png';
this.vimg.className = 'vplay';
this.vRoom.appendChild(this.vimg);
}
var nv = null;
$.bvd = function(dom) {
return nv || (nv = new bvd(dom));
}
}(mui))
</code></pre>
<p>这里有个小细节,如果img的src设置为本地资源的话,那么以后使用会出现很多问题,比如,页面层级发生变化时,你要去修改bvd.js,以免夜长梦多,我们将图片转为base64</p>
<p>再添加控制条</p>
<pre><code>pro.initEm = function(){
//先添加播放按钮
this.vimg = document.createElement("img");
this.vimg.src = 'data:image/png;base64,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';
this.vimg.className = 'vplay';
this.vRoom.appendChild(this.vimg);
//添加控制条
this.vC = document.createElement("div");
this.vC.classList.add('controls');
this.vC.innerHTML = '<div><div class="progressBar"><div class="timeBar"></div></div></div><div><span class="current">00:00</span>/<span class="duration">00:00</span></div><div><span class="fill">全屏</span></div>';
this.vRoom.appendChild(this.vC);
}</code></pre>
<p>再把 video.html中的 播放按钮 和 控制条 注释,看看我们写的代码效果如何</p>
<p><img src="/img/bVBH8b?w=989&h=364" alt="clipboard.png" title="clipboard.png"><br>大家可以看到,红色是我们注释的,绿色的是我们js生成的,奈斯</p>
<h2>开始播放</h2>
<p>真正开始我们js篇的第一步啦</p>
<p>初始化事件,先写个播放按钮播放视频事件测试一下</p>
<pre><code> //元素初始化
that.initEm();
//事件初始化
that.initEvent();
----------
pro.initEvent = function(){
var that = this;
//给播放按钮图片添加事件
this.vimg.addEventListener('tap',function(){
that.video.play();
})
}
</code></pre>
<p><em>tap事件,是MUI封装好的一个适合移动端点击的事件哦,移动端click事件延迟300ms,建议大家多用tap,关于tap事件是如何模拟的,请看<a href="https://link.segmentfault.com/?enc=puuB3uMbHF4VA3hFHUgOWQ%3D%3D.ebj09kD8Wf2Ww2Y%2B7LNACBQKHlwldforMDG8MVBBZI9zDxFeqj3DuOMPuow53LP3" rel="nofollow">HTML5触摸事件演化tap事件介绍</a></em></p>
<p>哟西,视频开始播放拉!!!</p>
<p><img src="/img/bVBIHV?w=441&h=295" alt="clipboard.png" title="clipboard.png"></p>
<p>回顾一下<a href="https://segmentfault.com/a/1190000006477658">H5打造属于自己的视频播放器(逻辑篇)</a><br>温习一下<a href="https://link.segmentfault.com/?enc=09f%2FcUtCxcA%2B5si5EIz%2BlQ%3D%3D.scxAZFNfhzF712J84KMGLTge0P1i8macYvR5P6zShKzieYED0GuWHqv6NwtB3LMW" rel="nofollow">移动端HTML5<video>视频播放优化实践</a></p>
<h2>初始化</h2>
<p>1)封面 在这里我没啥好图,就不设置了,大家可以试试在video标签加入属性 poster="图片地址"<br>2)获取视频长度,ok,我们给视频添加一个"<strong>loadedmetadata</strong>"获取到元数据事件,什么事元数据?</p>
<p>举个栗子:一部电影30个G,名字叫《我将带头冲锋》,视频长300小时</p>
<p>大小,名称,长度,等这些应该就算是元数据了。</p>
<p>我们给视频添加一个获取到元数据事件</p>
<pre><code> pro.initEvent = function(){
var that = this;
//给播放按钮图片添加事件
this.vimg.addEventListener('tap',function(){
this.style.display = 'none';
that.video.play();
})
//获取到元数据
this.video.addEventListener('loadedmetadata',function(){
that.vC.querySelector('.duration').innerHTML = this.duration;
});
}</code></pre>
<p><img src="/img/bVBIZ3?w=363&h=43" alt="clipboard.png" title="clipboard.png"></p>
<p>果然出现了视频的长度,可是这是按秒计算的啊。。。</p>
<p>这时我们可以写一个时间转换的函数</p>
<pre><code> function stom(t) {
var m = Math.floor(t / 60);
m < 10 && (m = '0' + m);
return m + ":" + (t % 60 / 100).toFixed(2).slice(-2);
}</code></pre>
<p>转换一下</p>
<p><img src="/img/bVBI33?w=371&h=52" alt="clipboard.png" title="clipboard.png"></p>
<p>可以,这很清真,今天我们暂时讲到这,消化消息,也让楼主整顿一下思绪</p>
<p>本章节JS1源码放在<a href="https://link.segmentfault.com/?enc=OkF%2BwCTyiucDS5rwzrvKoQ%3D%3D.FnC26NVQKugnJK0KdPEijMnYJ%2BG%2F48EtkQMY%2FCwAf35Qi63G%2FAgLeD7S7NiwkKWs" rel="nofollow">这里</a></p>
<p><a href="https://segmentfault.com/a/1190000006604046">H5打造属于自己的视频播放器(JS篇2)</a></p>
帧绘玩=>使用video与canvas玩耍的伪“人体感应”
https://segmentfault.com/a/1190000006492676
2016-08-15T20:53:33+08:00
2016-08-15T20:53:33+08:00
newsning
https://segmentfault.com/u/newsning
3
<h2>写在前面</h2>
<p>感谢各位在前端一直努力奋斗的程序猿们,希望以后的HelloWorld会更美好。</p>
<p>感谢<a href="https://link.segmentfault.com/?enc=FXGx8mdwryy3qNaXgSTBag%3D%3D.UkYlX%2FV%2F3RrM3QeGTJxLkcEVRLpQ0wQITGGUAiWDhfXA2xrZhIuDouRF6HpbytR9" rel="nofollow">来看看机智的前端童鞋怎么防盗</a>作者的分享</p>
<p>感谢停了一天的网终于恢复</p>
<h2>干货</h2>
<p>先来分享2张图<br>1)王的愤怒<br><img src="/img/bVBoIK" alt="64380cd7912397dd0b00143f5182b2b7d0a28794.jpg" title="64380cd7912397dd0b00143f5182b2b7d0a28794.jpg"><br>2) 程序猿的愤怒<br><img src="/img/bVBoLP?w=1328&h=611" alt="clipboard.png" title="clipboard.png"></p>
<h2>原理</h2>
<p>通过启用浏览器摄像的方式,把每一帧的图映射到canvas上,通过比较上一帧与当前帧的差异,算出来的差异占的百分比,超过某个百分比就触发函数。</p>
<p>栗子:我摇动我的双手,两个帧的手的位置不一样,从而差异占的比例就挺大的,就触发了写好的回调函数{</p>
<pre><code>给水杯添加一个动画
给body更换一个背景颜色</code></pre>
<p>}<br>程序猿的愤怒是通过在<a href="https://link.segmentfault.com/?enc=N2KkMZaPuvotBHlL6qwnnQ%3D%3D.C7S6X8MBCLWXxK4MNw7hxCXWIAJ5Yz4g3G5C8j3fDa4%2B1NJVjpitcIrs5ailz3EV" rel="nofollow">来看看机智的前端童鞋怎么防盗</a>借鉴的代码实现的,<br><a href="https://link.segmentfault.com/?enc=ybXeC56lAQhOwCZdcpMGVQ%3D%3D.MxR%2BGfDFReufd10ejIP8wRq%2F8QB%2BE6X%2B3cTNDGyAtvB0NEZd5kvKstwQHWG2juHe" rel="nofollow">原作者项目GitHub地址</a><br><img src="/img/bVBoTV?w=44&h=46" alt="clipboard.png" title="clipboard.png"><br>同时,在不破坏原作者源码的基础下,愤怒的程序猿代码如下:</p>
<pre><code><!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>愤怒的程序猿</title>
<style type="text/css">
.room{
text-align: center;
margin: 200px 10px;
position: relative;
height: 240px;
width: 320px;
display: inline-block;
}
.room>*{
float: left;
position: absolute;
bottom: 0;
left: 100px;
}
.room .beizi{
width: 50px;
height: 100px;
border-radius: 10px;
background-color: darkolivegreen;
color: white;
line-height: 100px;
}
.room>span{
width: 100%;
left: 80px; color: white;
}
canvas{
/*display: none;*/
}
@keyframes tiao{0%{transform: translateY(0);}50%{transform: translateY(-100%);}100%{transform: translateY(0);}}
@-webkit-keyframes tiao{0%{transform: translateY(0);}50%{transform: translateY(-100%);}100%{transform: translateY(0);}}
.tiao{
animation: tiao 1s ease-in-out .1s;
-webkit-animation: tiao 1s ease-in-out .1s;
}
</style>
</head>
<body>
<div class="room">
<video autoplay width="320" height="240"></video>
<div class="beizi">水杯</div>
<span>您可以尝试敲打桌面</span>
</div>
<div class="room" style="display: none;">
<canvas width="320" height="240"></canvas>
<span>每帧截取</span>
</div>
<div class="room">
<canvas width="320" height="240"></canvas>
<span>上一帧与这一帧的对比差异</span>
</div>
<div class="room">
<canvas width="320" height="240"></canvas>
<span>检测到动了一定幅度的截图</span>
</div>
<script>
var w = 320,
h = 240;
var video = document.querySelector('video'),
beizi = document.querySelector(".beizi"),
canvas = document.querySelectorAll('canvas'),
canvasForDiff = canvas[1];
canvasPhoto = canvas[2];
canvas = canvas[0];
navigator.getUserMedia || navigator.webkitGetUserMedia// || navigator.mozGetUserMedia//ie chrome firefox
({video:true}, function(stream) {
video.src = window.URL.createObjectURL(stream);
video.play();
}, function(err) {
alert('出错: ' + err)
});
//canvas
var context = canvas.getContext('2d'),
diffCtx = canvasForDiff.getContext('2d'),
photo = canvasPhoto.getContext('2d');
//将第二个画布混合模式设为“差异”
diffCtx.globalCompositeOperation = 'difference';
var preFrame, //前一帧
curFrame; //当前帧
var diffFrame; //存放差异帧的imageData
//捕获并保存帧内容
function captureAndSaveFrame(){
preFrame = curFrame;
context.drawImage(video, 0, 0, w, h);
curFrame = canvas.toDataURL(); //转为base64并保存
}
//绘制base64图像到画布上
function drawImg(src, ctx){
ctx = ctx || diffCtx;
var img = new Image();
img.src = src;
ctx.drawImage(img, 0, 0, w, h);
}
//渲染前后两帧差异
function renderDiff(){
if(!preFrame || !curFrame) return;
diffCtx.clearRect(0, 0, w, h);
drawImg(preFrame);
drawImg(curFrame);
diffFrame = diffCtx.getImageData( 0, 0, w, h ); //捕获差异帧的imageData对象
}
//计算差异
function calcDiff(){
if(!diffFrame) return 0;
var cache = arguments.callee,
count = 0;
cache.total = cache.total || 0; //整个画布都是白色时所有像素的值的总和
for (var i = 0, l = diffFrame.width * diffFrame.height * 4; i < l; i += 4) {
count += diffFrame.data[i] + diffFrame.data[i + 1] + diffFrame.data[i + 2];
if(!cache.isLoopEver){ //只需在第一次循环里执行
cache.total += 255 * 3; //单个白色像素值
}
}
cache.isLoopEver = true;
count *= 3; //亮度放大
//返回“差异画布高亮部分像素总值”占“画布全亮情况像素总值”的比例
return Number(count/cache.total).toFixed(2);
}
var t,d;
//定时捕获
function timer(delta){
d = 0;
setTimeout(function(){
captureAndSaveFrame();
renderDiff();
setTimeout(function(){
if((d=calcDiff())>=0.13){
handel();
}
}, 16.7);
timer(delta)
}, delta || 500);
}
function handel(){
if(t){return}
photo.drawImage(video, 0, 0, w, h);
beizi.classList.add('tiao');
canvasForDiff.classList.add('tiao');
document.body.style.backgroundColor = (function(){
return ['red','yellow','black','blue','green','white'][Math.floor(Math.random()*6)];
}());
t = setTimeout(function(){
canvasForDiff.classList.remove('tiao');
beizi.classList.remove('tiao');
t = null;
},1000);
}
timer();
</script>
</body>
</html>
</code></pre>
<ul>
<li>Q:在谷歌浏览器上运行的,表示页面一片空白,啥都没有,请问这是为什么?</li>
<li>A:通过谷歌的文档可以得知,这是为了安全性考虑,非 HTTPS 的服务端请求都不能接入摄像头,简单来说,chrome通过文件的方式打开一个html文件,是无法开启摄像头的,可以在本地服务端打开哦!如:localhost</li>
<li>还要注意写兼容哦!楼主只写了chrome的</li>
</ul>
<h2>嗨起来</h2>
<p>程序猿们怎么可能就地止步,让我们运用代码,自己来写一个自动拍照吧!</p>
<ul><li>AV.html<img src="/img/bVBoTV?w=44&h=46" alt="clipboard.png" title="clipboard.png">
</li></ul>
<pre><code><!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<style>
div{
text-align: center;
width: 250px;
margin: 10px 0;
}
</style>
<body>
<video></video>
<div>
<button onclick="paipaipai()">手动拍照</button>
<button onclick="auto()">自动拍照</button>
<button onclick="debug()">调试模式</button>
</div>
<img/>
<script src="av.js"></script>
<script>
var img = document.querySelector("img");
var av = Av({
//style: {
// width: 320,//视频 画布 宽
// height: 240//高
//},
//deg: 0.12,//灵敏度 触发动作幅度
//die: 500,//dwon机时间,触发事件后500ms内不触发
//delta:300,//取帧间隔 300ms 获取一次视频帧
//sw:true//开关,默认为开
fn:function(data){//触发函数
img.src = data;
}
});
function paipaipai(){
av.switchAV(false);
img.src = av.getCurFrame();
}
function auto(){
av.switchAV(true);
}
function debug(){
[].map.call(document.querySelectorAll("canvas"),function(c){
c.style.display = 'inline';
});
}
</script>
</body>
</html>
</code></pre>
<ul><li>AV.js<img src="/img/bVBoTV?w=44&h=46" alt="clipboard.png" title="clipboard.png">
</li></ul>
<pre><code>(function() {
var av = function(option) {
option = option || {};
this.op = (function(o) {
for(var i in option) {
o[i] = option[i];
}
return o;
}({
style: {
width: 320,//视频 画布 宽
height: 240//高
},
deg: 0.12,//灵敏度 触发动作幅度
die: 500,//dwon机时间,触发事件后500ms内不触发
delta:300,//取帧间隔 300ms 获取一次视频帧
sw:true//开关,默认为开
}));
this.initEm();
this.initMedia();
this.initCanvas();
this.switchAV();
}
var avpro = av.prototype;
//初始化基础元素
avpro.initEm = function() {
//video元素
this.video = document.querySelector(this.op.el || 'video');
this.video.setAttribute('autoplay', '');
this.video.style.objectFit = 'fill';
//初始化canvas元素
var canvas = document.createElement("canvas");
canvas.style.display = 'none';
canvas.style.backgroundColor = this.video.style.backgroundColor = 'grey';
canvas.style.width = this.video.style.width = (this.w = canvas.width = this.op.style.width) + 'px';
canvas.style.height = this.video.style.height = (this.h = canvas.height = this.op.style.height) + 'px';
//动作画布克隆,映射视频 ac 动作Action 便于记忆
var acCanvas = canvas.cloneNode(true);
//对比画布克隆,比较差异 bw 表示黑白 便于记忆
var bwCanvas = canvas.cloneNode(true);
//清除原体释放资源
canvas = null;
//添加至页面
document.body.appendChild(acCanvas);
document.body.appendChild(bwCanvas);
this.canvas = acCanvas;
this.acCanvas = acCanvas.getContext('2d');
this.bwCanvas = bwCanvas.getContext('2d');
}
//初始化摄像头
avpro.initMedia = function() {
var tv = this.video;
navigator.getUserMedia || navigator.webkitGetUserMedia //ie chrome
({
video: true
}, function(se) {
tv.src = window.URL.createObjectURL(se);
tv.play();
}, function(err) {
console.log('err:' + err);
});
}
//初始化画布,帧数
avpro.initCanvas = function() {
//将第二个画布混合模式设为“差异”
this.bwCanvas.globalCompositeOperation = 'difference';
// 前一帧 当前帧 差异帧
this.preFrame = this.curFrame = this.diffFrame = null;
}
//开始AV
avpro.startAv = function() {
var tv = this;
var call = arguments.callee;
tv.zt = setTimeout(function() {
tv.avSaveFrame();
tv.renderDiff();
setTimeout(function() {
if(tv.calcDiff() >= tv.op.deg) {
//触发事件
tv.handel();
}
}, 16.7);
call.call(tv);
},tv.op.delta);
}
//设置开关 和 回调函数
avpro.switchAV = function(sw,fn){
if(sw == undefined ? this.op.sw:sw){
this.startAv();
}else{
this.stopAv();
}
fn && (this.op.fn = fn);
}
avpro.stopAv = function(){
this.zt && clearTimeout(this.zt);
}
//触发事件
avpro.handel = function() {
var tv = this;
if(tv.t) {
return;
}
console.log(tv.fn);
tv.op.fn && tv.fn(tv.curFrame);
tv.t = setTimeout(function() {
tv.t = null;
},tv.op.die);
}
avpro.getCurFrame = function(){
return this.curFrame;
}
//捕获并保存帧
avpro.avSaveFrame = function() {
//帧替换
this.preFrame = this.curFrame;
this.acCanvas.drawImage(this.video, 0, 0, this.w, this.h);
//转为base64并保存当前帧
this.curFrame = this.canvas.toDataURL();
}
//绘制base64图像到画布上
avpro.drawImg = function(src, ctx) {
ctx = ctx || this.bwCanvas;
var img = new Image();
img.src = src;
ctx.drawImage(img, 0, 0 ,this.w, this.h);
}
//渲染前后两帧差异
avpro.renderDiff = function() {
if(!this.preFrame || !this.curFrame) return;
this.bwCanvas.clearRect(0, 0, this.w, this.h);
this.drawImg(this.preFrame);
this.drawImg(this.curFrame);
//捕获差异帧的imageData对象
this.diffFrame = this.bwCanvas.getImageData(0, 0, this.w, this.h);
}
//计算差异
avpro.calcDiff = function() {
if(!this.diffFrame) return 0;
var cache = arguments.callee,
count = 0;
cache.total = cache.total || 0; //整个画布都是白色时所有像素的值的总和
for(var i = 0, l = this.diffFrame.width * this.diffFrame.height * 4; i < l; i += 4) {
count += this.diffFrame.data[i] + this.diffFrame.data[i + 1] + this.diffFrame.data[i + 2];
if(!cache.isLoopEver) { //只需在第一次循环里执行
cache.total += 255 * 3; //单个白色像素值
}
}
cache.isLoopEver = true;
count *= 3; //亮度放大
//返回“差异画布高亮部分像素总值”占“画布全亮情况像素总值”的比例
return Number(count / cache.total).toFixed(2);
}
var nav = null;
window.Av = function(op) {
return nav || (nav = new av(op));
};
}())</code></pre>
<h2>遗言</h2>
<p>写前圣成佛:感觉自己马上要写出一篇无与伦比的文章。<br>写后呆成魔:写的跟流水账样的。</p>
<p>最后希望宝强能够好起来。</p>
H5打造属于自己的视频播放器(逻辑篇)
https://segmentfault.com/a/1190000006477658
2016-08-15T17:54:04+08:00
2016-08-15T17:54:04+08:00
newsning
https://segmentfault.com/u/newsning
9
<h2>回顾</h2>
<p><a href="https://segmentfault.com/a/1190000006461476">H5打造属于自己的视频播放器(HTML篇)</a><br>在上一章节之中,已经把HTML篇给大致样式显现了出来,接下来应该是JS篇了,可是在写之前有必要先整理一下思绪,盲目乱写是不对的,喝杯茶,撩撩妹,生活多美妙,写起代码来自然心情好思路正。</p>
<p><img src="/img/bVBkEE?w=373&h=192" alt="clipboard.png" title="clipboard.png"></p>
<h2>初始化</h2>
<p>1)设置好封面,一个好的小视频,自然封面非常给力,让人忍不住点击<br> 下面就是一些不错的栗子<br><img src="/img/bVBkM1?w=278&h=325" alt="clipboard.png" title="clipboard.png"></p>
<p><img src="/img/bVBkOg?w=400&h=382" alt="clipboard.png" title="clipboard.png"></p>
<p>2)获取视频长度,一个友好的播放器,会给用户显示这个视频总共多少分钟</p>
<pre><code>当然,您可以不用标注的太明显,但不能没有哦</code></pre>
<h2>播放</h2>
<p>当点下播放的时候,世界将会改变。确实</p>
<p>1)播放按钮隐藏<br>2)下方控制条渐渐隐藏<br>3)视频开始播放</p>
<p>ps:排名不分先后 你321倒过来也行</p>
<h2>播放中</h2>
<p>在播放中会发生什么变化呢?</p>
<p>1)控制条进度条慢慢增长<br>2)当前播放时间变化</p>
<h2>暂停 or 停止</h2>
<p>在播放当中按暂停或者视频播放完毕时</p>
<p>0)时间定格(所指的是 video ui都静止,但不包括广告哟,哈哈,您可以尝试在播放按钮上放置一些诱人的广告)</p>
<p><img src="/img/bVBkYT?w=702&h=392" alt="clipboard.png" title="clipboard.png"></p>
<p>1)播放按钮显示<br>2)下方控制条显示</p>
<h2>总结</h2>
<p>1)在点击播放时,是播放按钮的点击事件,触发视频播放<br> 而在点击暂停时,是视频的点击事件,触发按钮显示</p>
<p>2)在点击播放后,下方控制条渐渐隐藏时,却还没有完全隐藏的时候<br>但又点击了暂停,这时需要清除控制条的隐藏的状态,如下图</p>
<p>灵魂画师来袭</p>
<p><img src="/img/bVBlfc?w=800&h=479" alt="clipboard.png" title="clipboard.png"></p>
<p>3)哎呀我跌倒了,没有三五八万起不来<img src="/img/bVBlgR?w=34&h=32" alt="clipboard.png" title="clipboard.png"></p>
<p><a href="https://segmentfault.com/a/1190000006569543">H5打造属于自己的视频播放器(JS篇1)</a><br><a href="https://segmentfault.com/a/1190000006604046">H5打造属于自己的视频播放器(JS篇2)</a></p>
H5打造属于自己的视频播放器(HTML篇)
https://segmentfault.com/a/1190000006461476
2016-08-15T13:21:01+08:00
2016-08-15T13:21:01+08:00
newsning
https://segmentfault.com/u/newsning
18
<h2>前言</h2>
<p>众所周知,16年无疑是直播行业的春天,同时也是H5的一次高潮。<br>so,到现在用H5技术在移动端做网页直播也是见怪不怪了,但是!!!<br>今天我们的主角是webApp下播放视频<br>参考文献:<br>1)<a href="https://link.segmentfault.com/?enc=qsqQK7Bc4nwBTZvIyYIKpg%3D%3D.nUsuulPUJ4ag%2FmfkA4MSb4o%2F8S1z7N8Ub5Q65ozM61htQyy6QonwV2F%2Bc5fGF0uSnpxRKZ5lGRNlqyXhxSXIXg%3D%3D" rel="nofollow">HTML5+CSS3+JQuery打造自定义视频播放器</a><br>2)<a href="https://link.segmentfault.com/?enc=2RCyA27U%2Bxylmvb1g6uLqg%3D%3D.0I6CSpdEHd1CgmNRtzDCIuFSLu73PoJcSZghhKvNa04Cff%2B6705lcByhcaTeApyC" rel="nofollow">mui Html5 Video 实现方案</a><br>3)<a href="https://link.segmentfault.com/?enc=CfLhTe8Fqx%2Bw12vyc2%2B%2FGA%3D%3D.VADWaMxSCXlY%2BiyA7z0t9e5qdsCUmVx83UJUc6FhVz5BWlBvzonozk16oJU1%2FMK2" rel="nofollow">移动端HTML5<video>视频播放优化实践</a></p>
<h2>搬好凳子看HTML</h2>
<p>首先我们在HB下创建一个新的app项目,名称为 欠债</p>
<p><img src="/img/bVBe1P?w=276&h=101" alt="clipboard.png" title="clipboard.png"></p>
<p>新建一个video.html</p>
<p><img src="/img/bVBfG0?w=580&h=108" alt="clipboard.png" title="clipboard.png"></p>
<p>webkit-playsinline : 在ios中,加入此属性,可以关闭自动全屏播放<br>object-fit:fill : 视频充满video容器的大小<br>详细理由请看参考文献2or3</p>
<p>在此我们向项目里放置一个mp4格式的视频,视频内容不限,可以是小动画,也可以是<br><img src="/img/bVBfjr?w=121&h=75" alt="clipboard.png" title="clipboard.png"></p>
<p>ps:要在meta中加上,否则视频会扩充变形哦</p>
<pre><code><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /></code></pre>
<p>OK,现在布局已经完成,一个视频已经在页面中了</p>
<p>旁白:尼玛,点了没反应,那这怎么播放?</p>
<p>楼主:你们这群家伙看别的小视频等个1小时都行。。。</p>
<p>旁白:一个简单的播放器,至少要有 暂停/播放,进度条,视频时长,全屏等控件吧</p>
<p>楼主:来来来,不要急,先来个播放按钮写在video标签后面</p>
<pre><code> <div class="bad-video">
<video class="" webkit-playsinline style="object-fit:fill;">
<source src='xx.mp4' type="video/mp4"></source>
<p>设备不支持</p>
<video>
<img src="img/play.png"/>
</div></code></pre>
<p>写好样式、</p>
<pre><code> .bad-video {
position: relative;
overflow: hidden;
background-color: #CCCCCC;
}
.bad-video .vplay{
position: absolute;
width: 15%;
z-index: 99;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}</code></pre>
<p>楼主:当当当</p>
<p><img src="/img/bVBfLT?w=446&h=303" alt="clipboard.png" title="clipboard.png"></p>
<p>再在后面加一个控制条</p>
<pre><code> <img src="img/play.png" class="vplay" />
<div class="controls">
<div>
<div class="progressBar">
<div class="timeBar"></div>
</div>
</div>
<div><span class="current">00:00</span>/<span class="duration">00:00</span></div>
<div><span class="fill">全屏</span></div>
</div></code></pre>
<pre><code>.bad-video .controls {
width: 100%;
height: 2rem;
line-height: 2rem;
font-size: 0.8rem;
color: white;
display: block;
position: absolute;
bottom: 0;
background-color: rgba(0, 0, 0, .55);
display: -webkit-flex;
display: flex;
}
.bad-video .controls>* {
flex: 1;
}
.bad-video .controls>*:nth-child(1) {
flex: 6;
}
.bad-video .controls>*:nth-child(2) {
flex: 2;
text-align: center;
}
.bad-video .controls .progressBar {
margin: .75rem 5%;
position: relative;
width: 90%;
height: .5rem;
background-color: rgba(200, 200, 200, .55);
border-radius: 10px;
}
.bad-video .controls .timeBar {
position: absolute;
top: 0;
left: 0;
width: 0;
height: 100%;
background-color: rgba(99, 110, 225, .85);
border-radius: 10px;
}</code></pre>
<p>总算有个看起来像样的了<br><img src="/img/bVBgie?w=328&h=262" alt="clipboard.png" title="clipboard.png"></p>
<p>旁白:楼主,可是还是不能播放啊<br>楼主:叫你别急,要不你先去撸一把,我写好了文字@你<br>旁白:好啊,早说嘛,我先走了,记得@我<br>楼主:你走,省的我精神分裂码两个人的字</p>
<p>好,现在Html元素已经基本上弄好啦,看起来不是那么low了</p>
<p><a href="https://segmentfault.com/a/1190000006477658">H5打造属于自己的视频播放器(逻辑篇)</a><br><a href="https://segmentfault.com/a/1190000006569543">H5打造属于自己的视频播放器(JS篇1)</a><br><a href="https://segmentfault.com/a/1190000006604046">H5打造属于自己的视频播放器(JS篇2)</a></p>