yuritu / yuritu.github.io Goto Github PK
View Code? Open in Web Editor NEW个人博客
个人博客
想要解决的问题:
项目是否要 换 3.0
如何从webpack1.x 向 3.0过渡
相关兼容问题
TL;DR
对打包体积优化差强人意,但是速度优化让人惊喜
有一些API的改变,总体上可以无痛升级
webpack3前段时间推出了,我对新版本进行了一些探索。
判断标准就是:想解决的问题webpack能帮你以低成本解决吗
我想解决的问题是:
关于技术选型我觉得不同项目需要不同对待,我的理解是
个人项目怎么激进怎么来
集体项目在保守的大方向下前进
如果是简单项目,只需要一个人来完成,比如说简单的活动页,后台管理页,这种需求往往一个人就做完了,不涉及配合的问题,试错成本小,那么激进无妨。
如果是集体项目,如果使用新技术涉及到团队的学习成本问题,成员间的沟通问题,而且非常容易牵一发而动全身,以前代码的兼容怎么办,用了太新的技术以后不好招人怎么办,都是需要考虑的问题,所以已经成熟的项目,前进要慎重。
我在一个自己负责的小项目中尝试了webpack的3.0版本,最直观感觉就是打包速度快了,之后我开始在主要项目中进行测试,在开发时,我需要起一个本地server
我主项目entry是20个文件,大小各异,样本多点我们好看对比。
我的主项目使用的是1.13.1
毕竟涉及把所有文件写到虚拟内存里,有点慢
这里启开发环境不涉及压缩混淆一类的就是过一下loader,差距还是很明显的;
所以每次改完bug打包都能喝个茶了...
但是!
时间快了20%所右
webpack3.0的新特性之一是 Scope Hoisting 译为 作用域提升
特别是这张图传的神乎其神
近50%的体积减小,惊不惊喜,意不意外!这还有什么好犹豫的快上车啊!
(╯‵□′)╯︵┻━┻ 这不对啊,你就优化了这1%?
webpack3优化的原因是加入了Scope Hoisting 译为 作用域提升,我们来一起看看他到底做了什么
我们的实验如下,es6module对照组与webpack版本对照组,webpack3版本增加作用于提升插件
//test1
var foo = require("./module1")
foo();
//module1
module.exports = function foo () {
console.log("hello world")
}
//test2.js
import foo from "./module2"
foo();
//module2
export default function foo() {
console.log("hello world")
}
//webpack.config for 1&2
module.exports = {
entry:{
"es5":"./webpacktest/test1",
"es6":"./webpacktest/test2"
},
output:{
filename:"[name].bundle.js"
},
plugins: [
new webpack.optimize.ModuleConcatenationPlugin()//for webpack3
]
}
首先我们一般都知道webapck处理依赖是将其封在一个独立闭包函数里面, 来完成依赖的独立作用域
然后ES5 与 ES6 的实现还有区别我们之后看
我们先说ES6module
webpack2 的打包情况
这里是2
这里是3
敲黑板!!
这里我们看出来webpack2为每一个闭包都加了一个
(function(module, __webpack_exports__, __webpack_require__) {
...
})
而webpack3把所有的依赖都打在了一个闭包里,那么你想,你一个组件要是个单页面应用那怎么不得十几个依赖,那就是十几个(function (module)...)
然后依赖再依赖别的这得多少啊...
Webpack 3 + ModuleConcatenationPlugin() 帮你全干掉,这就是为什么打包体积小了。
我们可以从下图看出来,es6.js 从3.17k 减少到了2.75k
那有同学就问了,你说的这是2和3对比,1呢,我们来看实验
这不对啊,怎么1反而是最小的,这还升什么级,(╯‵□′)╯︵┻━┻。
我们来看1里面打出来是什么
ES6module
ES5module
这就要说到ES6module 的问题了,我们可以看到webpack1对es6module根本没有处理,那么这样的ES6代码,很多浏览器是不支持的,而2、3版本都对ES6module进行了处理
原因是webpack1对es6module支持的很差,在2.1.0版本中的,才开始支持,其bugfixes写到
**It's now possible to combine webpack ES2015 modules with babel ES2015 modules
**
所以,在webpack1中传统的es5的方式,webpack对他进行了处理,和我们之前的结果一样,使用了闭包来保证独立作用域。
那为什么webpack1实验结果体积最小呢
是因为webpack在包里新加了一些getter setter方法用于优化依赖的加载
所以虽然在我们的实验中webpack1更小,但是在实际项目的结果中,webapck3的体积要比1要小。
而在我的项目中因为兼容原因没有使用ES6module所以优化效果比较差,所以如果项目中完全使用es6的话,那么就可以完全发挥ES6的威力。
我们在这张图中可以看到
在webpack3中依赖存储于一个闭包,那么问题来了,在没有闭包的情况下,他们在同一个块级作用域,如果我在一个依赖里声明了一个变量,其他模块岂不是也能拿到?
我们来搞事情
//test2.js
import foo from "./module2"
foo();
console.log(other)
//module2
var other = "The World"
export default function foo() {
console.log("hello world")
}
我们可以看出来,虽然没有了闭包,但是使用了另外的名字来保证了作用域。
如果你决定升级了那么首先就要对配置文件进行一些更改,主要是 loader 与 plugins 的一些改变,语法上有一些不兼容。
另外webpack3的文档写的比1好多了,读起来清晰多了,所以基本的 api在这里不再赘述,这里列出来我遇到的一些问题
在打包或者建立服务器的时候可能会报找不到一些node的模块比如fs,这个时候需要在webpack文件里面这么设置
node:{
console: true,
fs: 'empty',
net: 'empty',
tls: 'empty',
},
babel-loader 官方推荐更新到7.1+
loader得到的souce从String改为了buffer对象,
所以说,总体上来说升级有一定收益,webpack官方也说webpack2可以直接升级webpack3,而webapck1升级也仅仅是一些api名字的变化。
如果用了es6那么升级就可以发挥他的威力。
而且webpack github上的issues 关于升级后的兼容性问题非常少,如果有时间的同学不妨一试。
参考:
Releases · webpack_webpack
🍾🚀 webpack 3_ Official Release!! 🚀🍾 – webpack – Medium
lishengzxc/bblog#34
这近两个月异常的忙,主要有两个原因一个是并发产品线数量过多,整个5月份我身负了3条产品线之多;第二个是时间衔接紧张,导致排期没有错峰。一忙就会导致身心上的压力与焦虑,进一步降低了工作效率;而且个人发展时间压缩。
这个月承担了一点点的偏管理任务,同时也在这里做个case study。
近期我负责和4个小伙伴做一个后端迁移任务,我姑且算是承担一个项目经理和小组长的责任,
小伙伴们暂时不直接像leader汇报。
这个项目有几个难点
这样一个项目,我和小伙伴的任务是 去除不需要组件 比如消息队列、一些sql上的特殊处理、把私有轮子换位厂内的公开轮子等等,且完成迁移。
1、初期
每个同学按照自己喜好领取完任务后,我对自己的定位模型进行这样的构思。
由于这个项目是我负责对接搭建环境的,目前只有一个开发机上的跑不通的qa环境,所以我的第一要务就是帮其他同学高速了解整个项目,对整个项目有个大概认知,明确自己负责的部分进行重点打击,其他项目部分作为黑盒看待即可。
我在项目也有执行任务是 缩减冗余代码,其实这是个辅助和需要了解全局的任务,也符合我的定位,于是我再梳理功能与代码的时候,绘制了方便其他同学理解的关系图,并且告知相关同学,其负责功能的代码位置,使得其他同学不要做重复劳动,能作为黑盒处理的就不要关注具体实现。
由于如果需要改代码只能去开发机改,版本混乱且登录跳板机费时,我做了点工具实现了两件事:
2、中期
由于有前面的普通其他同学工作进行也不错,但是问题也慢慢来了
这里和leader 沟通了一下,主要有三点需要注意
3、后期
后期我反了非常大的错误,导致整体进度险些崩盘,这里复盘一下。
我在处理环境流程时,遇到了一个账号体系的问题,找对接的同学也没有解决,一家伙卡了4天。直接导致1. 我的管理工作没有进行,我全部精力都投到解决问题上了,这段时间leader代劳 2. 拖累了项目进度
这里的原因本质上是两点
错把流程问题当成技术问题。作为一个开发习惯于看日志打断点寻找bug,但是作为成熟的厂内流程的东西,可靠性非常高。最后也发现其实是一些权限没有申请,我错当成了技术问题,一味求解浪费了时间。
没有及时沟通。理论上我一个下午没有搞定时就应该和leader交流一下,看看他有什么思路或者方向,但是我有些钻牛角尖了,对自己的判断过于自信。而且也有点害怕麻烦别人,结果最后通过leader找到了专门负责体系的同学,非常快就解决了。及时主动的沟通非常重要,这点我管理其他同学时也清除,但我自己做的不好。
4、革命尚未成功
目前这个需求,还没有搞定,虽然艰辛异常,但是收获很多。革命尚未成功、同志仍需努力。
做为一个工程师,我一般都秉持着对用户负责、对代码负责的要求。
但是理想很美好,现实没那么容易。操守都需要时间来保证。
对用户负责这点已经被大老板打脸了,没想到对代码负责这点也不是绝对的。
我有一个3d动画需求,用webgl做的,比较费cpu,优化了一版之后,在提测阶段后端要求我继续优化,理由是现在性能耗费过大,影响用户体验。
从操守的角度来说,能不能优化?肯定能啊,但这是个边际效应问题,我花3个月再优化30%肯定可以,但是工期不允许,哪怕我愿意加班搞,leader 也不会同意的。
另一个事情是有个支援性质的需求上线了之后一直有些小css的问题,本着自己的代码负责到底的操守,我一直都会去处理。有次听大老板谈话,说到了支持有效期的问题,就是说我作为一个fe支援,一般只支持到上线后一周,一周后非重大功能性问题不支持。咋一看有点不负责任其实很有道理,首先本质是一个支援性质的任务,我的人不可能长期投在这里;其次从个人发展来说,什么都做这是分不清主次的表现,大老板说的有道理 —— 需求是做不完的。
tl;dr 对于开发者而言最大的影响是MIT协议的改变,其次是传送门对开发方式的优化以及ssr方面的巨大提升。
React16 在国庆前发布了,这次版本迭代带来了许多令人激动的新特性。
前不久的React在业界引起了不小的波澜,更有国外WordPress与国内大厂百度的封杀React,以至于不少开发者认为Vue或成最大赢家。
终于FB将React16改为MIT协议,对于不升级的开发者也有MIT协议的15.6.2的旧版本可以选择。不过之前此举多少伤害了开发者对其的信任,FB是否再会出尔反尔,也影响这技术负责人对技术栈的选取。
长久以来全局的模态框、dialog、tooltips等等全局组件的DOM位置问题,终于有了优雅的解决方案。
当我们需要一个全局的提示框时,作为通用组件,它是脱离我们当前开发的业务组件的,因为从state的设计上,提示框的显隐是一个私有变量,而文案内容等一般由当前业务组件的props来决定,这就导致在业务组件中添加了一个无关组件,一来影响我们关注点分离的**,二来难以复用。
另一方面由于DOM层级的影响css的规则与书写有会造成困难,你无法控制Module父元素的css状态,从而导致无法良好的抽象。
举个栗子:
理想的状态是这样
我们的模态框可以复用,等待数据流灌入即可。
let = jsx = (
<body>
<Main/>
<Other/>
<Module/>
</body>
)
可是实际上开发却是这样
let jsx = (
<body>
<Main>
<components/>
<Module/>
</Main>
<Other>
<components/>
<Module>
</Other>
</body>
)
当然是用Redux可以完成第一种的设计,可是单单为了一个提示框就如此大弄干戈真的有必要吗?
React16提供了一种 将组件内部一些DOM元素绑定在外部组件的DOM上的方法,简单来说就是 在组件内部render一个组件,却能把这个render 的结果挂载在别的DOM上。从而完成我们所说的组件结构的优化与复用。
而如果我们使用portals 开发时组件结构就可以不变
let jsx = (
<body>
<Main>
<components/>
<Module/ ...props> // 接受数据与事件冒泡
</Main>
<Other>
<components/>
<Module ...props> // 接受数据与事件冒泡
</Other>
<Module>// 真实渲染 选择挂载位置为根元素
</body>
)
我们在业务组件中,控制公共组件的state数据,进而指定公共组件的渲染位置,最后达到良好的复用。
从代码上来说,我们的业务组件可以这么写
//以前我们渲染会这么写
render() {
reutrn (
<div>{this.props.children}</div>
)
}
//如果我们需要制定Module的挂载位置
render () {
return ReactDOM.createPortal(
<Module ...props.module>,//这里是我们要给传送门的东西
this.props.node//需要挂载的地方
)
}
而且传送门能够实现事件的冒泡,你不用担心开发时的父组件无法监听相关的事件,它的表现会像一个正常的组件一样,无论你把全局组件挂载到哪里。
这东西厉害了,这是react提出的一个很先进的概念,由于React16使用了新的Fiber架构来进行项目构建(这个Fiber架构具体是什么,博客里说的很难我没看懂),从而开发了异步渲染。
异步渲染是一种 与浏览器进行协作 以 定期执行对渲染任务的排序的策略(这块不太好理解 原文是 a strategy for cooperatively scheduling rendering work by periodically yielding execution to the browser. )
通过异步渲染,以及避免react占用主线程,从而让页面动画更加流畅,可以看一下这个例子 demo
主要注意右上角那个黑方块,他一直在旋转占用着主线程,然后我们去进行请求,请求后会渲染页面,导致进程占用,页面重新渲染,方块旋转停止。
如果我们采用异步渲染就可以完成 在不占用主进程的情况进行渲染操作,从而不影响其他的元素。
这个功能并没有在React16实装,但是在博客中提到了未来几个月后可能会推出。
你再也不用给jsx带套了,render
函数接受更加多样的数据结构。
一般我们写一个列表可能是这样
const renderChildren = (arr) => (
<ul>
{arr.map( i => (<li>{i}</li>) )}
</ul>
)
我们干什么都要给加个父元素的contianer,这样一方面结构不明朗,另一面有可能会有多余的元素,而现在我们可以直接输出数组或者字符串了。
render() {
return [
<li key="A">First item</li>,
<li key="B">Second item</li>,
<li key="C">Third item</li>,
];
}
// or
render() {
return 'Look ma, no spans!';
}
相对于上个版本有30%的减少
名称 | 新版本(kb) | 压缩后(kb) | 旧版本(kb) | 压缩后 (kb) |
---|---|---|---|---|
react | 5.3 | 2.2 | 20.7 | 6.9 |
react-dom | 103.7 | 32.6 | 141 | 42.9 |
react+react-dom | 109 | 34.8 | 161.7 | 49.8 |
React文档上说由于对打包方式的改变,React 采用 Rollup打包,所产生的bundle不会受到工具的影响,比如webpack、Browserify、UMD或者其他的工具。这个Rollup是一个模块打包器,它能把小的代码打进比较大的或者更加复杂的库,是一个比较新的库。
另一个方面是 react自持自定义DOM属性了,由于不用再设置属性的白名单,一定程度上减少了文件大小。这个特性主要是为ssr服务的。
这是新版本重点更新的特性之一,笔者SSR经验比较少,体会不是很深,感觉React16比较明显的提升是性能上的。
React16对ssr主要是性能提升了3倍以上,如果是最新版本node实验环境下提升更是达到了数量级的提升。
还有一个比较大的区别是在html到达客户端之后,react并不会去比较初次的渲染结果与服务端结果,react会尽量去重用相同的DOM元素,这也是为了优化ssr中的“注水”过程,从而提升ssr的性能。因为一般情况下我们的服务端与客户端也不会渲染不同的内容,除非是做时间戳的一些东西。
笔者认为对开发者影响比较大的特性就是以上这些了,总体来说新的特性能够解决一些日常开发的痛点,开发体验也越来越好了,不过React16因为用了ES6的一些集合数据结构例如Map
和Set
所以,兼容性要求比较高,如果需要兼容老式浏览器需要上polyfill比如core.js
和 babel-polyfill
。
还有一些特性比如 错误捕捉的优化、api更新与打包的改变有兴趣的同学可以关注一下React的博客来了解。
近期给内网准入的部门做了个官网,谈一下动画项目的代码组织方式。
由于内网限定,gif预览:image
从项目整体来看分为 M 数据 V 视图 C 生命周期控制
目录结构如下
├── config.js (M 所有的数据来源)
├── animator
│ ├── Animator.js (C 动画生命周期控制)
│ ├── Fir.js (V 视图)
│ ├── For.js
│ ├── Sec.js
│ └── Thi.js
└── module (mesh封装)
│ ├── boxBase.js
│ ├── particleBase.js
│ └── particleLine.js
├── index.js (环境初始化)
MV 组装过后成为独立组件,暴露出的接口由C进行调用,C直接控制每个组件和整个动画的生命周期、action触发与销毁,以及和父级组件的沟通,我们主要来看一下C层的构成
class Animator {
//使用MV组装组件
const fir = new Fir (M)
//给环境中的 raf 暴露接口
update(){
switch(status.get()){
case 1: fir.mount();
break;
case 2:fir.render();
break;
case 3:fir.unmount()
break;
//...
}
}
}
//状态机,暴露接口
class Status {}
//时间管理,提供时间与组件循环状态的对应
class Timer {}
核心就是由一个有限状态机来进行生命周期的管理,代码组织也是围绕这一层来进行的。
接下来看一下V层,V层的主要功能是使用M的数据来组装自定义的mesh模块,从而进行显示,暴露相关的action与hook以供C层使用。
class Fir extends ViewBase {
// 使用公用的mesh模块对需要的模型机型组装
const mesh1 = new CustomizedMesh(M)
// 添加mesh模块,进行渐入动画
mount(){}
//动画loop
render(){}
//销毁模块,进行渐出动画
unmount() {}
}
//复用通用模块,建立自定义模块,
class CustomizedMesh extends CommonMesh{}
这样组织代码的主要优点是
通过借助k8s进行微服务的搭建,使前端也能够进行高并发服务的维护与开发,前后端代码分离部署分离的难度大大降低。提高了服务的可用性可维护性,维护难度降低对fe更加友好。
在公司这几年的’全应用上云‘的政策加持下,公司基础设施有了很大的进步,hife团队自身也有许多开发独立应用和服务的需求,作为在server方向的探索者,我也进行了相关实践,最后结合团队梯队组成与现状,探索出了基于kubernetes的微服务方案。
问卷系统是fe团队全栈负责的一个产品。是一个厂内用于用户调查的平台,大到手百、全面小视频等等千万级起步的流量大户,也有仅用于厂内的行政人力调查需求,所以有着以下的特点
由于用户调查采用的是应用推送或出现于信息流中的方式,作为高危操作,都是需要提前进行相关资源位的配置,导致用户接受到的时间非常集中,会非常明显的造成流量的峰值,在实际的运维中也发现60%的流量一般会集中在前一个小时,而在前一个小时中约10%的流量会直接在前5分钟爆发。
前文提到问卷不仅承接外部调查,也承担内网调查,这造成整个月份来看系统的pv、uv并不高,所以在对接运维部门进行资源配置的时候,结合预算要求在日常需求仅维持了6实例的配置,但是在千万级流量的面前这些是远远不够。k8s的pod调度可以让运维者秒级完成横向的扩容操作,非常符合我们的需求。
由于流量断崖的存在所以一方面设备的自动横向与纵向伸缩是非常重要的,另一方面在流控措施层面我们也添加了基于qps的限制,在极限特殊情况下,优先保证大多数用户的体验。
这个比较好理解,用户同时打开非常正常,但是用户的填答率折损是一方面另一方面由于题目的差异,写的流量往往会被拉伸,不会有特别明显的峰值。目前已经做了主从分离,但是还是有一定风险存在,我们打算后续增加读写分离,增加分担读流量的从库。由于本质上属于无状态服务,这个成本是非常小的。
这个严格来说不算技术问题,更多是产品和业务的理解了,在运维过程中我也遇到的达到估算流量的10倍多的情况,导致了部分用户打开页面排队需要10s之久。结合历史数据和业务表现,我们总结了以下的公式 日活 * 时间系数 * 业务方打开率 * 应用相性系数 = 日流量估值。
这里来说一下系统对于请求的处理流程,如题图。核心是使用了k8s中的负责均衡机制。k8s中提供4种负载均衡的实现,我们使用Service中提供的cluster内部负载均衡就可以解决问题了。
当请求到主机的时候,本质上是请求到了 k8s server,视业务而定我们会有一个或多个cluster,由于经过service,pod的endpoint已经在service中建立了链接,那么这时service 起到两个作用 1. 路由 2. 负载均衡
经过service 的路由我们可以完成前后端部署分离的问题,作为fe不需要关心be的核心有几个实例,具体访问哪一个,也不用管对应的be的状态。完全由service来抽象以解耦。
这里谈一个老问题:为什么要前后端分离。
经过service之后,对应的模块承载对应的服务,通过MQ、redis进一步的减少server端的瞬间压力,从而达到业务方的需要。
在产品矩阵中,问卷其实只是一个基础“平台”,他同样肩负着支撑其他业务的作用。即本身是一种服务也是一个能力提供平台。团队也借助其基础能力开发了一些需要系统基础能力支持的应用。它们整体是一套小而自治的服务集合。
从产品设计的角度来讲,系统本身提供的是‘信息收集’的能力,依据这个能力可以衍生出一套闭环方案。从内聚性角度上来说,没有必要重新开启新服务。但是如果采用传统的方案,全部耦合在一个服务中,就会出现即使有衍生服务的小改动,也要整体部署服务。这样会导致部署的步长越来越大,迭代速度越来越慢而且,新旧代码的差异越大,出错的可能性也越大。
所以需要的是 能够修改一个服务并对其进行部署,而不影响其他任何服务。
虽然微服务让项目的学习成本有所增加,但是其带来的好处也是显而易见的。
由于服务间的解耦,其实不用关系其他服务的技术栈,对于一些老项目也可以非常方便的迁移,新项目也不用收到其的掣肘。比如说问卷最开始一部分服务是php,在后续团队有了node的同学,我们也渐渐把一些核心模块用node改造或者扩展,并不会影响其他服务。
由于一些外部原因,有的时候会发生一些组织变动从而会拆解、合并一些业务,由于微服务的分离性,这些服务在交接的时候并不会有很大的成本。新人并不需要了解详细的业务底层,拆分后也不会影响核心模块。
问卷在一年多的改造中也慢慢从几千pv成长到十几万pv 了,并且逐渐可以支持外部流量需求,最高一次一个小时保障了50万流量的正常访问。也证明了改造方案的正确性,让团队的小伙伴们有了信心,接下来还有许多优化点需要继续,在后续文章会详细说明。
最新有些需求涉及到一些前端软解的问题,WXInlinePlayer 是一个非常好的例子,它来自于前全民直播CTO创建的新公司,技术实力有保证,这里我们就来一起看下~
主要步骤为
http range request ——>controller ——> codec——> viewer
在点击调用play() 方法后,播放器开始初始化设置:
实例化 通过emscripten编译好的解码器 H264Codec 类
实例化用于封装解码器的worker。
创建基于webgl的view层
完成之后worker.postmessage 通知controler(prcessor.js)开始请求视频数据,通过http range request请求chunksize所规定的byte。
请求返回后将拿到的二进制数据流交给worker 中的解码器开始解码。
解码
软解码中使用 openH264 或 tinyh264作为解码器,具体看加载哪个解码器文件。
ready —— 解码器初始化完成
header —— 文件元信息
mediaInfo —— 视频分片信息
video —— 获得视频数据
audio —— 获得音频数据
decode —— 帧解码完成,准备渲染、播放
(在数据请求完成之前重复 video ——> audio——>decode——>video——...的过程)
complete —— 数据全部解析完成
对应生命周期会发送同名message 通知controler(processor)
先通过node判断了需要打包编译版本,baseline 版本的编码器只有tinyH264 all版本才包括openH264,这里会影响cmake的编译路径。
编译开始后从根目录开始BFS搜索cmake文件开始编译,
首先根据选择使用openH264还是tinyH264的结果,开始设置emscripten 编译参数 核心语句是
"-O3 生成wasm的优化等级 optimized
-s js代码生成所需要的option specified
ENVIRONMENT="web,worker" 产出在web和webworker环境中运行
-s SINGLE_FILE=1 合并js和wasm的代码
-s WASM=1 启用wasm
-s FETCH=0 不需要使用fetchapi
-s DISABLE_EXCEPTION_CATCHING=0 生成对应代码补货异常
-s ERROR_ON_UNDEFINED_SYMBOLS=0 允许未定义符号的错误
-s NO_EXIT_RUNTIME=0 main()结束时运行时退出
-s FILESYSTEM=0 不构建文件系统支持
-s INVOKE_RUN=0 不自动调用main 而采用Module.callMain
-s ASSERTIONS=1 添加运行时断言 用于检查堆栈状态
-s TOTAL_MEMORY=67108864 能用多少内存,扩展堆很费性能
-s EXPORTED_FUNCTIONS= 显式导出的api
--closure 1 使用闭包编译器
--memory-init-file 0 嵌入base64表示c语言
--llvm-lto 3 链接时间优化等级
此处参数 参照 emscripten-core
打包编译这里 用emscripten把 cmake 和make 封装了一下本质上还是用cmake来打包。
编译完成,生成 prod.js 改为 all.asm.js
通过wrapper.js 函数 将整个asm 包装成为一个worker
worker 的postmessage在glue.js中定义。onmessage在processsor.js中完成从而完成wasm与普通业务逻辑的交互
controler(process.js) 通过worker将请求到的buffer和其对应的length传递给解码器H264Codec之后,会到达 lib/codec/src/main.cpp
main.cpp 是对外暴露的解码器接口, 他的作用是 对于 openH264和tinyH264 的API做了统一的封装,进行了媒体文件数据的pre-format。
其内部方法最后都会变成 process.js codecH264实例中的内部方法
经过main.cpp 的入口,之后处理buffer的是 codec.cpp作为解码器方法的具体实现。
开始进行demuxer的相关处理。
demuxer核心代码位于 decoder.cpp,在其中进行类似渲染管线的处理
首先进行header的decode 位于 header.cpp 通过对buffer数据的切分,获得对应的 签名、版本、是否存在音频视频轨
// 切分buffer的过程
_signature = buffer->slice(0, 3);
_version = buffer->read_uint8(3);
uint8_t flags = buffer->read_uint8(4);
_hasAudio = (flags & 4) >> 2 == 1;
_hasVideo = (flags & 1) == 1;
_offset = buffer->read_uint32_be(5);
结合flv的二进制文件和flv规范 spec
signature 是flv version 是1 hasAudio = hasvideo = 1 offset 是9;
之后会通过 codec_factor.cpp 调用emscripten 中的brige,把对应的数据向外抛出,触发外部worker的onmessage回调,使得controll(processer.js)获得对应的数据
切分剩下的数据会继续向下个任务传递
进入 body.cpp
接下来的数据是 size-tag的类似gl中的arraybuffer的交错结构
为 「上一个tag 的size - 当前tag」
size是int32 所以第一个是0 没用切掉
shared_ptr<Buffer> body = make_shared<Buffer>(buffer->slice(4));
接下来的数据是 对应的tag 进入 tag.cpp模块
这个tag是 “12 00 16xxxx”
先拿到当前tag的值和size
_type = buffer->read_uint8(0);
_size = buffer->read_uint24_be(1);
12 当前type是一个script data 接下来size "16ba" = 5818
timestamp 0 timestampextentd 0 steamid 规范为0 “0200 xxx”后面全是当前tagdata的数据
根据tag 的类型 demuxer进行switchcase来分别处理
“00af” 这里开始
_soundFormat = (uint32_t) ((buffer->read_uint8(0) & 240) >> 4);
_soundRate = (uint32_t) ((buffer->read_uint8(0) & 12) >> 2);
_soundSize = (uint32_t) ((buffer->read_uint8(0) & 2) >> 1);
_soundType = (uint32_t) (buffer->read_uint8(0) & 1);
这里取audiodata的对应信息
_soundFormat = 10 // aac
_soundRate = 3 // 44khz
_soundSize = 1 // snd16Bit
_soundType = 1 // sndStereo
去掉开头的2字节信息之后,剩下的数据进入下个管道
value.data = make_shared<Buffer>(buffer->slice(2, size));
在codec_factor.cpp中进行了一顿猛(mei)如(kan)虎(dong)的位操作,然后就返回给worker了...
_frameType = (uint32_t) ((buffer->read_uint8(0) & 240) >> 4);
_codecId = (uint32_t) (buffer->read_uint8(0) & 15);
“0017”这里是video data数据的开始
先取 帧类型 _frameType = keyframe === 1
codecId 编码id === 7 = avc
对于是avc的codec需要向后去4个字节
_AVCPacketType = buffer->read_uint8(1);
_compositionTime = (uint32_t) (buffer->read_int32_be(2) >> 8);
获得 _AVCPacketType = 0 === avc sequence
_compositionTime = 0
value.data = make_shared<Buffer>(buffer->slice(5, size));
切掉前面的5个字节 剩下 的 data数据 作为AVCDecoderConfigurationRecord 进入下个管道处理
tinyH264部分
经过codec_factor.cpp的一层分发 开始对AVCDecoderConfigurationRecord中的 sps和pps (tinyH264部分
经过codec_factor.cpp的一层分发 开始对AVCDecoderConfigurationRecord中的 sps和pps 进行数据的提取) 进行数据的提取
uint8_t configurationVersion = unit->read_uint8(0);
uint8_t AVCProfileIndication = unit->read_uint8(1);
uint8_t profileCompatibility = unit->read_uint8(2);
uint8_t AVCLevelIndication = unit->read_uint8(3);
最后将相关的 sps pps数据交给 tinyH264部分
h264bsdDecode(_codec->storage, _codec->sps->get_buf_ptr(), _codec->sps->get_length(), &picPtr, &width, &height);
h264bsdDecode(_codec->storage, _codec->pps->get_buf_ptr(), _codec->pps->get_length(), &picPtr, &width, &height);
处理完sps pps之后,其他_AVCPacketType 不是0 的video tag 会生成一个用于承载图片数据的contaienr
通过tinyH264的处理最后
uint32_t retCode = h264bsdDecode(_codec->storage, nalus->get_buf_ptr(), nalus->get_length(), &picPtr, &width, &height);
将我们传入的container赋予 生成图片的相关数据以及width和height
有了数据之后,就通过 emscripten构造的brige 把对应的 timestamp、宽高buffer数据等传给worker 返回processor.js
一次videotag的数据解码传输就完成了。
script里面主要是一些方法调用的封装,在流数据中比较多
有几种细分类型
ScriptDataString
ScriptDataLongString
ScriptDataValue
ScriptDataAvariable
ScriptDataAvariableend
对应不同的解码规则
在解码器完成decode 的全部工作后,通过worker的message通知 controller(processor.js)
view层开始使用之前获得的video tag的对应的yuv颜色数据进行渲染。
由于采用yuv420p的色彩抽样,webgl使用三个texture对应yuv通道,交替进行texture渲染。
并在fragment shader中进行yuv转rgb的矩阵换算 完成一帧图片的处理。
在解码器完成decode 的全部工作后,依靠AudioContext 获得audiobuffer 获得currentTime和baseLatency 从而控制音频的时间戳
总体来说,由于安卓、浏览器厂商等的一些限制和魔改,前端软解码的需求愈发重要,WXInlinePlayer 提供了一种非常好范例,它支持了flv格式一般对业务的支持比较有限,但是我们可以通过这个范例,自行扩展mp4,hls等等等等
2019.12.06 业务团队分享
抖音早期让大众印象最深刻的功能就是它的美颜滤镜,其功能之强大让用户的感官体验得到了极大提升;作为提高用户体验的重要功能,这里将讨论:
滤镜的本质是什么?
滤镜是如何工作的?
高分辨率下前端如何实现滤镜
图像滤镜用于改变图像颜色、阴影或其他特征;其中最明显的感知就是颜色的变化,通过一种滤镜,可以达到更改人对图片的感受,究其原因是因为人对不同的颜色有着不同的感知,产生的刺激也是不同的。所以滤镜的根本目的是改变人对颜色的感知。
颜色通过色彩模式来表示,就像坐标系一样,同一个物体可以有多重表示的方式,除去常见的rgb、hex,也有HSL\HSV等其他的色彩模式。色彩模式的改变反应色彩属性的改变,颜色包含多种的属性,比如从情感上来说的色温:红色橙色给人温暖的感觉,蓝色给人感觉寒冷;饱和度则用于量化在颜色中灰色占据多少分量。从功能上滤镜用于改变这些属性,从而完成对用户感知的处理。
从实现上滤镜可以通过卷积矩阵来描述。
卷积矩阵,分成两个部分:卷积、矩阵。
卷积是一种数学操作,用于使用两个function 生成第三个function。目的是为了表示出两者之间是如何互相影响的。
矩阵也叫卷积核,比较常见的是33矩阵,也有55和7*7的。
滤镜在工作的时候,会根据每个像素的周围像素,来决定处理后的像素的颜色,所以需要遍历图片上的每一个像素。
原始数据通过卷积计算获得一个新的值,应用于图片上,可以类比为对于rgb 三个通道都做类似的处
理,比如下面的图,可以类比为是一个只有r通道的图片,获得目标像素周围的颜色数据之后,与矩阵的对应项目进行相乘,最后求得新的颜色值,后进行下一次计算。
为什么是这些数字?为什么这些数字就能获得我们的效果,这些数字是怎么来的?
我们首先看这些数字的意义可以理解为kernel中,对应像素点在新颜色值中的比重情况
如上,这样会得到图片整个位移一个像素的图片,因为其表达的意思就是除了上方的点其他的点都不做影响。如果换一个情况
[
1,1,1,
1,1,1,
1,1,1,
]
意思就是新的点的颜色值和周围一圈的像素点都有关系,每个点的比重也是一样的,它的结果是周边像素点的平均,所以这是一个模糊的滤镜。
还有一些比较实用的滤镜,比如边缘检测(egle delect),它是磨皮美颜等强大滤镜的基础,一个基础的边缘检测滤镜是
[
0,1,0,
1,-4,1,
0,1,0
]
具体数学证明可见边缘检测,在实际开发中一般使用特定的卷积核就足够日常开发需要了,一般不需要自行进行相关的拟合。
在了解了对应原理之后,有两种对应的实现方式,webgl和canvas的两种方案
通过片元着色器可以使用gpu并行计算能力,在大图情况下计算速度得到保证。
核心 fragment shader 为:
void main(){
vec2 onePixel = vec2(1.0,1.0) / u_textureSize; // 一个像素的offset
vec4 colorSum =
texture2D(sampler, t_coord + vec2(-1,-1) * onePixel ) * u_kernel[0] +
texture2D(sampler, t_coord + vec2( 0,-1) * onePixel ) * u_kernel[1] +
texture2D(sampler, t_coord + vec2( 1,-1) * onePixel ) * u_kernel[2] +
texture2D(sampler, t_coord + vec2(-1, 0) * onePixel ) * u_kernel[3] +
texture2D(sampler, t_coord + vec2( 0, 0) * onePixel ) * u_kernel[4] +
texture2D(sampler, t_coord + vec2( 1, 0) * onePixel ) * u_kernel[5] +
texture2D(sampler, t_coord + vec2(-1, 1) * onePixel ) * u_kernel[6] +
texture2D(sampler, t_coord + vec2( 0, 1) * onePixel ) * u_kernel[7] +
texture2D(sampler, t_coord + vec2( 1, 1) * onePixel ) * u_kernel[8];
gl_FragColor = vec4( (colorSum / u_kernelWeight).rgb, 1.0);
}
这段意思是 从图片上,去除当前坐标点的周围的点,与3*3矩阵相乘,最后获得颜色值 / kernel_weight
3*3 滤镜的kernel_weight(滤镜内所有数字相加) 必须是1 所以要么在传入滤镜的时候,给的就是小于1的,或者给任意值,但是在处理的时候最后 矩阵相乘结果 / weight。
canvas实现相对更加适合小图以及兼容要求高的情况
卷积的实现如下:
for (let y = 0; y < img_height; y++) {
for (let x = 0; x < img_width; x++) { // 遍历整个图像
const sy = y;
const sx = x;
const dstOff = (y * w + x) * 4; // 获得当前像素的imageData对应index
let r = 0; let g = 0; let b = 0; let
a = 0;
for (let cy = 0; cy < kernel_side_length; cy++) {
for (let cx = 0; cx < kernel_side_length; cx++) { // 遍历整个卷积核
const scy = sy + cy - halfSide;
const scx = sx + cx - halfSide; // 判断是kernel size 是3*3 还是5或7
if (scy >= 0 && scy < sh && scx >= 0 && scx < sw) { // 超过图片边界的忽略
const srcOff = (scy * sw + scx) * 4;
const wt = kernel[cy * side + cx]; // 查找卷积核中对应的项
// src 是图片原本的imageData
r += src[srcOff] * wt;
g += src[srcOff + 1] * wt;
b += src[srcOff + 2] * wt;
a += src[srcOff + 3] * wt;
}
}
}
// 获得新的imageData
dst[dstOff] = r;
dst[dstOff + 1] = g;
dst[dstOff + 2] = b;
dst[dstOff + 3] = a + alphaFac * (255 - a);
}
}
最近做一个需求有了对lottie-web的魔改需要,于是看了一下lottie的源码,线性流程看着非常舒服。
如果需要修正相关的效果,需要在json文件中进行相关的处理,这里也给出对应的解释。
l-web会对json文件进行解析 AnimationITem对于每一个layer层进行解析,根据调用栈,对于每一个layer层,创建对应的 tag标签 || canvas语法 || svg标签 从而建立对应效果
对应关系如下
ty
tips:docs/json 中有部分对应的解释
{
"ddd": 0, // si 3d
"ind": 1, // index
"ty": 2, // type
"nm": "wall.jpg",// name
"cl": "jpg",// classname
"refId": "image_0", // 对应asset 中的id 不对应则无此项
"sr": 1,// 拉伸因子
"ks": {},// transform 配置
"ao": 0,// ao auto Oriented 自动朝向
"ip": 0,// 该图层开始关键帧
"op": 900.000036657751,// 该图层结束关键帧
"st": 0,//offsetTime 延迟时间
"bm": 0// bm mix-blend-mode 颜色混合模式
},
{
...image,// 与image类型相同
t:{ // 文字数据
"d": {
"a": [] // 所有者 可无
"k": [ // keyframe具体数据
{
"s": {
"s": 36, // font size
"f": "AdobeHeitiStd-Regular", // font family
"t": "hello", // text value
"j": 0, // justification 代码里面没用
"tr": 0, // text tracking
"lh": 43.2, // line height
"ls": 0,
"fc": [ // color
0.92,
0.92,
0.92
]
},
"t": 0 // keyframe 关键帧开始时间
}
]
},
"p": {}, // 字符路径 number 可无
"m": { // 其他配置
"g": 1, // 对齐情况
"a": { // 锚点,不用改
"a": 0,
"k": [
0,
0
],
"ix": 2 // propertyIndex 表达式标记
}
},
}
}
表达式对应关系
11 opacity
1 anchor point
2 position
6 scale
10 rotation
{
...image,
// 木有对应的refid
}
这个子类过多,大同小异,具体看需求再补充
{
...image,
"shapes": [
{
"ty": "gr", // group shape 大部分都这个
"it": [ // item 具体的图形
{
"d": 1, // 方向,一般固定的
"ty": "el", // Ellipse 类型 椭圆
"s": { // size
"a": 0, // anchon point
"k": [ // value
249.414,
245.07
],
"ix": 2
},
"p": { // position 这点存疑🤔
"a": 0,
"k": [
0,
0
],
"ix": 3
},
"nm": "Ellipse Path 1",
"mn": "ADBE Vector Shape - Ellipse",
"hd": false
},
{
"ty": "st", // stroke
"c": { //color
"a": 0,
"k": [
1,
1,
1,
1
],
"ix": 3
},
"o": { // opacition
"a": 0,
"k": 100,
"ix": 4
},
"w": { // width
"a": 0,
"k": 2,
"ix": 5
},
"lc": 1, //line cap
"lj": 1, // line join
"ml": 4, // miter limit 一般用不着
"nm": "Stroke 1",
"mn": "ADBE Vector Graphic - Stroke",
"hd": false // hidden
},
{
"ty": "fl", // fill
"c": { // colr
"a": 0,
"k": [
1,
0,
0,
1
],
"ix": 4
},
"o": { // opacity
"a": 0,
"k": 100,
"ix": 5
},
"r": 1, // 代码里没用,默认值
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill",
"hd": false
},
{
"ty": "tr", // transform
"p": {
"a": 0,
"k": [
-117.293,
-307.465
],
"ix": 2
},
"a": {
"a": 0,
"k": [
0,
0
],
"ix": 1
},
"s": {
"a": 0,
"k": [
100,
100
],
"ix": 3
},
"r": { // rotation
"a": 0,
"k": 0,
"ix": 6
},
"o": { // opacity
"a": 0,
"k": 100,
"ix": 7
},
"sk": { // skew
"a": 0,
"k": 0,
"ix": 4
},
"sa": { // skew axis
"a": 0,
"k": 0,
"ix": 5
},
"nm": "Transform"
}
],
"nm": "Ellipse 1", // name
"np": 3, // 代码中对应的原型index
"cix": 2, // 代码里面没有
"ix": 1,
"mn": "ADBE Vector Group", // match name 和代码调用有关
"hd": false
}
],
}
ks 是对应的动画设置位于
player/js/utils/TransformProperty.js:151
进行相应的配置
"ks": { // 变换。对应AE中的变换设置
"o": { // 透明度
"a": 0, // 是否存在关键帧
"k": 100, // 对应值 有关键帧时为数组
"ix": 11
},
"r": { // 旋转
"a": 0,
"k": 0,
"ix": 10
},
"p": { // 位置
"a": 0,
"k": [-167, 358.125, 0],
"ix": 2
},
"a": { // 锚点
"a": 0,
"k": [667, 375, 0],
"ix": 1
},
"s": { // 缩放
"a": 0,
"k": [100, 100, 100],
"ix": 6
},
"sk":{},
"rx":{}
}
"a": 1, // 是否存在关键帧数量
"k": [ // property value 储存变量的地方
{
"i": { // 贝塞尔曲线 入值 这里是一个一次函数曲线
"x": [
0.833
],
"y": [
0.833
]
},
"o": { // 贝塞尔曲线 出值
"x": [
0.167
],
"y": [
0.167
]
},
"n": [
"0p833_0p833_0p167_0p167" // 猜测是备注,代码里没用
],
"t": 0, // 当前关键帧的开始时间
"s": [ // 开始的opacity
100
],
"e": [ // 结束的opacity
57
]
},
{
"t": 89.0000036250443 // 持续的帧数
}
],
"ix": 11 // propertyIndex 表达式标记
{
"a": 1, // same up
"k": [
{
"i": {
"x": [ // 这里肯定是x轴value emm 为什么有多个🤔 我也不知道
0.833,
0.833,
0.833
],
"y": [
0.833,
0.833,
0.833
]
},
"o": {
"x": [
0.167,
0.167,
0.167
],
"y": [
0.167,
0.167,
0.167
]
},
"n": [
"0p833_0p833_0p167_0p167",
"0p833_0p833_0p167_0p167",
"0p833_0p833_0p167_0p167"
],
"t": 0,
"s": [ // start 的scale值,对应xyz轴
100,
100,
100
],
"e": [ // end scale
300,
300,
100
]
},
{
"t": 84.0000034213901
}
],
"ix": 6
}
视频的type是9 所以默认的l-web是不支持的
但是canvas 的imageITem 最后的实现api是drawimage
这个是可以输入video congtext的,所以接下来就需要进行canvas的魔改
原则上集成imageITem重写部分方法即可
how about dom
从源码上看直接使用video和audio标签也是可以的
但是canvas 掌控能力更好
会更合适一些
基本content重写,继承于CVImage类,已经验证lottie-web的支持video是可行的.
function CVVideoElement(data, globalData, comp){
this.assetData = globalData.getAssetData(data.refId);
this.initElement(data,globalData,comp);
}
extendPrototype([CVImageElement], CVVideoElement);
CVVideoElement.prototype.createContent = function(){
let canvas = createTag('canvas');
canvas.width = this.assetData.w;
canvas.height = this.assetData.h;
let ctx = canvas.getContext('2d');
this.video = document.createElement('video');
this.video.src = this.assetData.u + this.assetData.p;
this.video.autoplay = true;
this.video.mute = true;
this._timeOUtRender = () => {
ctx.drawImage(this.video,0,0);
setTimeout(this._timeOUtRender, 1000 / 30);
}
this._timeOUtRender();
this._offscreenCanvas = canvas;
};
CVVideoElement.prototype.renderInnerContent = function(parentMatrix){
this.canvasContext.drawImage(this._offscreenCanvas, 0, 0);
};
效果见图:
可以支持视频的播放和相关transform处理,好啦我要去赚pr啦😁
最近为了给以后的运营需求做技术积累,对3D动画在组内的落地进行了一些研究。切入点是粒子动画,这是最容易实现的3D动画。
最后做出了一些还可以的效果。把其中的经验与坑记录一下
由于只在内网上线,所以暂时用gif来表示(gif掉帧比较大,真实还是非常流畅的)。
这里不再赘述 webGL底层与图形学,我们这里说three的基础
three.js构成一个物体的本质是三样
1.Geometry 几何 负责物体长什么样子
2.material 材料 负责物体对光线的交互
3.mesh 网格 geo与mat的承载者
粒子动画的粒子构成有三种思路
最常用的一种,一个父级对象作为粒子系统,子为坐标点矢量
好处是方便建立,开一个随机数组,xyz三值赋值即可
缺点是不方便为控制每一个粒子,如果想对每个粒子的颜色大小等进行控制无法达到,因为Vector对象本质上是矢量,不包含这些东西,矢量擅长的事情是位移。
这种方式的优点是
可以操控所有的粒子属性,大而全,就像一个IDE,你能做的事情都帮你做了,建立起来也和Vector非常相似,十分容易。
缺点是非常耗费性能,2D动画的性能一般由计算和渲染决定,而由于GPU太擅长矩阵计算了,计算耗费相对于渲染微不足道。渲染性能耗费主要就在于GPU要计算多少面,而每一个Mesh对象又是独立的,如果采用MergeGeometry的方法则无法控制每一个粒子。
使用缓冲区来降低性能消耗,还能完整控制每个属性。
BufferGeometry要求Float32Array,为3 * n矩阵来确定位置信息,颜色与大小等则同样为 1* n的矩阵。与上面两种直接给实例的方式不一样。
举个🌰
const v = new THREE.Vector3();
const length = n;
let rs = new Array(length * 3);
for (let i = 0; i < length; i++) {
v.x = x
v.y = y;
v.z = z;
v.toArray(rs, i * 3)
}
return rs
这里我们用一个Vector3.toArray来帮我们来完成矩阵排列,当然这样就是要你不要
用 index index + 1 index + 2 的方式来表示xyz,因为在真实开发环境中可能有复杂情况让你不是那么好算。
这里返回一个普通array 你可以直接用 new Float32Array()或者用原型链方法from来进行array 到 float。
ShaderMaterial就更麻烦了,因为他涉及到我们需要自己写用GLSL语言来写着色器,它可以让你充分的发挥GPU的威力但是对于习惯了写JS的一些fe来说,突然去写类C的这样一种语言还是会有一些不适应的。
three.js帮你把底层基本上能做的事,都做了,封装的也非常好,虽说官方文档有点云里雾里的但是跟着看看源码也就明白了,作者的源码写的是真心话简单朴实不炫技。
那么对于我们开发者的难度就在于算法上了,算法有两个问题
1.1 物体建模
有UI支撑帮忙建模自然最好,但是从经验来看还是靠不住的;我们需要自己用算法实现物体的效果,比如说一个渐变缩小的金字塔,他的每个粒子之间的的关系方程式,需要我们花费时间去处理。这里面就涉及到调参的问题,如何快速的精确调整与展示结果,直接影响到我们的开发效率,这里three官方用的是 dat.gui,能解决我们所需要的快速展示结果的需求,问题就是需要花费时间添加代码。理想来说应该可以集成在一个框架中,来减少开发成本,但是目前生态不算完善,可以考虑自己造轮子。
1.2 数据生成
随机数据的生成以及对数据状态的变化,也是一个需要解决的部分,建立好的模型,怎么去进行动画。位移、大小、颜色、形变等等,都需要一个规律的算法来实现,简单的补间动画我们可以使用TWEEN来实现,但是真正那些好看的效果,还是需要我们通过各种三角函数、矩阵变换等来实现,这里暂时没有什么太好的处理方法,只能说是多看实例,积累算法。
在动画各幕对象差很多的情况该怎么办?
在我的动画中,四幕粒子数约为 10k, .5k, 2k, 3k ,那么这里涉及到第一幕多出来的粒子怎么处理?让他飞出去在销毁?毕竟多这么多挺费内存的。但是我没有这么干,而是循环原样摆放,也就是说我的10k个粒子,在第二幕其实摆了20遍,只不过他们重合了,看起来像只有很少。
有两个原因:
GPU前向算法会将不出现的物体不进行渲染。10k粒子对GPU不算啥事。
如果我反复操作粒子总量更加困难不说,也容易出bug延误工期,现在都敏捷开发,一天出80分的活,也不会让你一周干到100分,毕竟可以后续再迭代。
本质上来说Three.js像是Jq,他的定位是一个库,3D的开发模式还是比较传统,没有React等这样的革新开发模型的框架出现,也不想2D有CreateJS这样的容错很高的框架存在,整个生态还是比较原始的一片处女地,在错误捕捉、性能提升、开发体验上暂时还都需要开发者自己来完成。
我自己把日常工作中沉淀了一下,做一个专为动画设计的工具库,Christina.js目前还很不完善, 只是集成了一些我觉得常用到的操作与算法,希望能对开发效率上有一定的提升。
总体上这次开发还是挺仓促的,一共10/人/天,性能上没有任何优化,在内存上也有很多浪费和需要处理的地方,这都需要在后续的开发中再优化,在提高开发效果和快速成果展示上也还需要继续优化。但总体上我还是满意的。
希望本文能给你一些灵感。
这次开发的Y项目,犯了一些错误,踩了一些坑,在这里总结一下。
这次开发自3.12-3.30,实际工作日18天,13号拿到ui图,加班3天,原定23号开发完成,28上线。最后delay到30号,勉强达到上线状态,项目整体delay。
12号我搭了页面框架和类库,13号拿到设计图之后共5页,我觉得非常简单,单纯的fullpage滑动效果+css动画就能搞定,当天我就写好了3页的基本dom,剩下比较多的切图活和css。
于是我就去改其他项目的bug了(我们就叫D项目吧)...13-17号,我都在D项目的bug,为了能在19号上线结束D项目。本质上我从18号才开始全部精力投入到Y项目,后续各种加班赶进度也证明了这次在时间管理上错误决策。
后续的突发情况指那些呢
不熟悉Native + webview模式下的各端联调,浪费时间
总结:
不要太相信端上,做好防御性设计。
我以前和在小公司的朋友聊,他说信任成本非常高,因为大家互相 觉得: ‘我觉得你菜,你也觉得我菜,所以你说话我得掂量掂量’,所以很多时候,一个谁做都可以、就看谁方便的事,搞得大家得撕很久,效率很低。
我那会觉得在大公司应该是 ‘你觉得我牛逼,我也觉得你牛逼,所以大家互相信任’,结果发并不是这样...
结果是扯了半天还得去找leader决断解决。
所以这里牵扯到一个沟通的分寸问题。
这个分寸是指,到底对于什么程度的问题,我们需要采用怎么样的沟通方式?
一般我们有三种方式,效率层层递进
自己去找相关人员沟通 —— 让pm去沟通(带pm去沟通)—— 直接找对方leader沟通
时间往往都浪费在 1、2步,我们多次尝试无果后,最终选择3,结果几分钟就解决了。
但是肯定不能说是事无巨细都找人家leader去。那么如何避免1、2步浪费非常多的时间呢?(如何预判出某个问题需要直接找相关leader沟通)这就是一个需要分寸拿捏的地方。
这是对人沟通的情况。
除了对人沟通,我们还有对物沟通的工作内容。
百度hi开放了开发者接入,提供接口等等功能,本质上是要做一个开放平台的定位。
什么是开放平台
开放平台本质是 点线面定位模型的一个表现,它负责承载 点(角色)线(行为),点线的增加,导致面的扩大和发展。
1.平台规范
(不适合对外发布,故删除)
一个开发平台的发展,本质上是 点(角色)和线(行为)的增加,这样平台才能扩展。
点和线的自增加是一方面,另一方面是平台要给点线赋能,这点在hi上看不到。
举个栗子:
hi平台现在本质只能完成一件事 发送接收消息,没了,其他的啥也干不了,用户想发送消息给站点还得等我们上线了智能消息才能,那么可以看到这个平台里面,点很少,线也很少。
什么是赋能
给点更多能力、让点能做更多的事,我们的智能消息就是一个例子,但是本质还是 发送与接收消息 你赋不出来别的能,所以点和线就不会增多,这个平台也只能这么大、没有扩张性。
pm老大在新风会上提了个问题,大意是说 百度的产品做得非常老气非常死,他猜测之一是 是不是pm年龄太大的原因。
我觉得hi平台也有这个问题,我们没有啥新的能力。
我这里举个例子,说明为啥赋能才能让平台发展。不做具体建议只是举例。
我们在数学里面都学过面面相交成线,那么我们是不是能结合其他平台,从而产生新的点或者新的面,比如做语音平台的度秘,
有了音箱这个新的点,那我们的消息能不能去控制音箱、甚至会议室的一切开关等等等等
就是说我们需要平台更多的赋能,这个平台才能继续扩展。
主要体现在这个设计方案,ui设计完了就给我了,没和产品沟通,那我拿到了图自然默认各方都同意了,然后肯定赶紧做,结果做出来pm和各路老板都不满意,改的很多,结果就是班白加,工白做,还得返工,辛苦加了班,却啥成绩都没有出,影响士气。
开发流程本质还是线性的,比如fe的上游是ui,这个需求11个部门,ui完全可以出了主视觉就给我,有了主视觉我们可以干不少事了,这里出了一半我才开工。
举个例子:
ui图很多图需要我自己切或者稍微偏了几个像素,这都可以接受ui比较忙嘛,咱们互相理解。但是这图有的直接不居中,我ps自己现学现用摆了半天...但我这边开发时间也非常紧
作为fe我的代码里面写一些妥协性的代码,比如说,后端返回的图片,有可能是个死链,那图就挂了,我最开始说那后端排查一下呗,这个图片以后肯定还用,不能一直挂啊。结果是我前端去捕捉异常,替换调这个图片,你说可不可以fe做,可以,应不应该,不应该。
这些事其实不难,也是谁方便谁做的范畴,可是做到最后我发现fe承担了所有的妥协,时间充裕这没什么,这时间不充裕的时候,那就是雪上加霜,但是这些本来都是可以避免的。
结论:
开发去承担因流程问题带来的工作量是非常浪费和不应该的。
以后开发还是应该尽量避免因流程产生的问题以及自身不熟悉导致的延期。
对于非技术方向,我们需要在沟通在流程上留心,‘两耳不闻窗外事,一心只想写代码’的理想状态可遇而不可求,通过硬实力技术实力在提高工作效率,用软实力来保证高效率的时间长度,才能真正的达到高效。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.