前端工程化
- 目标:利用技术进步&经验积累带来的各种方案,解决项目中开发,测试,维护中遇到的各种低效和繁琐的问题
- 技术:不断演变和进步,服务于工程化思想
- 原因:提效,提高生产力,体现在项目的开发,测试和维护阶段
规范化
git flow 规范
- master 主干分支,上线的代码
- develop 开发分支,
- hotfix 紧急修复bug
- release 是上线之前的临时分支
- feature 新增功能,是从develop临时复制的一个分支,开发完合并
相关操作
初始化
git flow init 自动生成master develop分支
git branch
git checkout develop
开发feature
git pull origin develop
git flow feature start f1
# git checkout develop
# git checkout -b feature/f1 这两条是上面flow feature start 的简写
git commit -am 'commit'
git push origin feature/f1 或者 git flow feature publish f1
git checkout feature/f1
git flow feature finish f1
# git checkout out develop
# git merge feature/f1 同样是上面的简写,现在在develop分支上
git push origin develop
上线
上线要先推送到release分支进行检查
git checkout develop
git flow release start 0.01
git commit -am "commit"
git push origian release/v0.0.1
检查无误,正式上线。上线的版本会同时合并到masterand develop
git checkout master
git pull origin master
git checkout release/0.0.1
git flow release finish 0.0.1
# git checkout develop 上面这一条同时进行了下面的全部动作
# git merge release/0.0.1
# git checkout master
# git merge release/0.0.1
# git tag 0.0.1
# git branch -D release/0.0.1
git push origin master
git push origin 0.0.1
hotfix
hotfix存在的意义是减少流程,快速上线
git checkout master
git flow hotfix start fix1
# git checkout master
# git checkout -b hotfix/fix1
git flow hotfix finish fix1 同时合并到`master`and `develop`
模块化
前端中一般是逻辑相关的代码放在一个文件,当作一个模块。只要关注模块内逻辑的实现,无需考虑变量污染的问题,模块之间可以相互调用。
CSS模块化
核心思想:样式生效规则来避免冲突 有四种基本的方案
- scoped: DOM节点添加data-v-version属性,.selector=>.selector[data-v-version]
- Css in JS: 以脚本模块来写样式,甚至可以封装常用样式。可以按规则生成唯一的selector
- CSS MODULES: 借助预编译使样式成为脚本中的变量
- BEM Block_ELement-Modifier 是一种命名规范。或者借助SASS
- shadow root:比较现代的方法,兼容性堪忧
JS模块化
- CommonJS
- ES6 module
组件化
模块化和组件化的核心思想都在于分治,好处就是团队协作效率和项目可维护性的提升。组件可以分为Presentational和container组件,分别用于展示UI和维护逻辑,方便复用。UI组件一般会包含逻辑组件。
自动化(重点与核心)
- 自动初始化 vue-cli
- 自动构建打包 webpack
- 自动测试 karma,jest
- 自动部署 Jenkins
自动化测试
JS动画
- 定时器改变对象的属性
- 根据新的属性重新渲染动画
- 优点(相对CSS/SVG动画) 灵活性,可控性,性能
- 缺点 易用性
通用化
function update({target},count){
target.style.tranform = `rotate(${count++}deg)`;
}
class Ticker{
tick(update,context){
let count = 0;
requestAnimationFrame(function next(){
if(update(context,++count) !== false){
requestAnimationFrame(next);
}
})
}
}
const ticker = new Ticker();
ticker.tick(update,{target:block});
这个版本里面实现了简易旋转的动画,但是要精细的控制时间,可以使用下面这个版本,做到了恒速旋转。
function update({target}, {time}) {
target.style.transform = `rotate(${360 * time / 2000}deg)`;
}
class Ticker {
tick(update, context) {
let count = 0;
let startTime = Date.now();
requestAnimationFrame(function next() {
count++;
const time = Date.now() - startTime;
if(update(context, {count, time}) !== false) {
requestAnimationFrame(next);
}
});
}
}
const ticker = new Ticker();
ticker.tick(update, {target: block});
上面两个例子都是直接操作DOM的style,那么如何使用H5的新特性 canvas呢
function update({context}, {time}) {
context.clearRect(0, 0, 512, 512);
context.save();
context.translate(100, 100);
context.rotate(time * 0.005);
context.fillStyle = '#00f';
context.fillRect(-50, -50, 100, 100);
context.restore();
}
class Ticker {
tick(update, context) {
let count = 0;
let startTime = Date.now();
requestAnimationFrame(function next() {
count++;
const time = Date.now() - startTime;
if(update(context, {count, time}) !== false) {
requestAnimationFrame(next);
}
});
}
}
一个更强大的Timing控制器
class Timing {
constructor({duration, easing} = {}) {
this.startTime = Date.now();
this.duration = duration;
this.easing = easing || function(p){return p};
}
get time() {
return Date.now() - this.startTime;
}
get p() {
return this.easing(Math.min(this.time / this.duration, 1.0));
}
}
class Ticker {
tick(update, context, timing) {
let count = 0;
timing = new Timing(timing);
requestAnimationFrame(function next() {
count++;
if(update(context, {count, timing}) !== false) {
requestAnimationFrame(next);
}
});
}
}
匀速运动
比如在两秒内匀速运动200px, 那么timing的duration参数就是2000, 每次更新 translate 200*timing.p 就可以
自由落地运动
根据公式,我们知道距离和时间成平方关系,200*timing.p的200这个时候就变成了a/2,timing.p要有一个平方的映射,可以用easing来处理
const ticker = new Ticker();
ticker.tick(update, {target: block}, {
duration: 2000,
easing: p => p ** 2,
});
摩擦力
easing可以有一个x(T-x)的映射
平抛
同时考虑x,y两个轴上的运动,translate要在两个参数上都设置
椭圆轨迹
x = a.cost(a)
y = b.sin(a)
来控制translate的x,y方向的位移。
周期运动
加上iterations参数控制周期数,使得timing.p始终在0-1之间重复iterations次
class Timing {
constructor({duration, easing, iterations = 1} = {}) {
this.startTime = Date.now();
this.duration = duration;
this.easing = easing || function(p){return p};
this.iterations = iterations;
}
get time() {
return Date.now() - this.startTime;
}
get finished() {
return this.time / this.duration >= 1.0 * this.iterations;
}
get op() {
let op = Math.min(this.time / this.duration, 1.0 * this.iterations);
if(op < 1.0) return op;
op -= Math.floor(op);
return op > 0 ? op : 1.0;
}
get p() {
return this.easing(this.op);
}
}
连续运动
class Ticker {
tick(update, context, timing) {
let count = 0;
timing = new Timing(timing);
return new Promise((resolve) => {
requestAnimationFrame(function next() {
count++;
if(update(context, {count, timing}) !== false && !timing.finished) {
requestAnimationFrame(next);
} else {
resolve(timing);
}
});
});
}
}
使用promise的callback处理周期,直到时间到了,才resolve promise使其从callback中跳出来变成fulfill状态
线性插值
lerp函数对值进行操作,然后setter到目标里面。
function lerp(setter, from, to) {
return function({target}, {timing}) {
const p = timing.p;
const value = {};
for(let key in to) {
value[key] = to[key] * p + from[key] * (1 - p);
}
setter(target, value);
}
}
弹跳的小球
const down = lerp(setValue, {top: 100}, {top: 300});
const up = lerp(setValue, {top: 300}, {top: 100});
(async function() {
const ticker = new Ticker();
// noprotect
while(1) {
await ticker.tick(down, {target: block},
{duration: 2000, easing: p => p * p});
await ticker.tick(up, {target: block},
{duration: 2000, easing: p => p * (2 - p)});
}
})();
平稳变速
function forward(target, {y}) {
target.style.top = `${y}px`;
}
(async function() {
const ticker = new Ticker();
await ticker.tick(
lerp(forward, {y: 100}, {y: 200}),
{target: block},
{duration: 2000, easing: p => p * p});
await ticker.tick(
lerp(forward, {y: 200}, {y: 300}),
{target: block},
{duration: 1000, easing: p => p});
await ticker.tick(
lerp(forward, {y: 300}, {y: 350}),
{target: block},
{duration: 1000, easing: p => p * (2 - p)});
}());
逐帧动画
<style type="text/css">
.sprite {
display:inline-block;
overflow:hidden;
background-repeat: no-repeat;
background-image:url(https://p.ssl.qhimg.com/t01f265b6b6479fffc4.png);
}
.bird0 {width:86px; height:60px; background-position: -178px -2px}
.bird1 {width:86px; height:60px; background-position: -90px -2px}
.bird2 {width:86px; height:60px; background-position: -2px -2px}
#bird{
position: absolute;
left: 100px;
top: 100px;
zoom: 0.5;
}
</style>
<div id="bird" class="sprite bird1"></div>
<script type="text/javascript">
var i = 0;
setInterval(function(){
bird.className = "sprite " + 'bird' + ((i++) % 3);
}, 1000/10);
</script>
Web Animation API(Working Draft)
element.animate(keyframes, options);
target.animate([
{backgroundColor: '#00f', width: '100px', height: '100px', borderRadius: '0'},
{backgroundColor: '#0a0', width: '200px', height: '100px', borderRadius: '0'},
{backgroundColor: '#f0f', width: '200px', height: '200px', borderRadius: '100px'},
], {
duration: 5000,
fill: 'forwards',
});
Web Animation API
对一个元素直接调用 animate方法,传入关键帧,比如from,50%,to; duration和fill可以表示时间和结束时动画状态。很简单但是要详细控制还是要使用之前介绍的方法。
前端性能优化
RAIL模型
Response
- 要把延迟控制在100ms内最好,1s之内是正常的。
- 50ms内处理用户输入事件,确保100ms内反馈用户可视的响应
- 开销大的任务可分割任务处理,或者放到worker进程中执行,避免影响用户交互
- 超过50ms的操作,始终给予反馈(进度条)
Animation
- 10ms或者更短时间内生成1帧,因为渲染要大概6ms左右
- 要考虑视觉平滑
- 处理动画逻辑尽可能的少
Idle
- 要最大化空闲时间以增加页面在100ms内响应用户输入的几率
- 利用空闲时间完成推迟的工资
- 即使要加载一些任务,用户可能的交互要优先考虑
Load
- 首屏时间对于普通中档设备应当在5s内呈现交互内容
- 非首屏加载应该在2s内完成
- 优化关键渲染路径以解除阻止渲染
- 影响加载性能的因素:网络/CPU/解析JS
工具篇
- lighthouse
- window.performancetiming
- webpagetest
- chrome devtools
实战篇
浏览器渲染场景
- JavaScript 实现动画/操作DOM
- Style(Render Tree)
- Layout 盒模型,确切的位置和大小
- Paint 栅格化
- Composite 渲染层合并
有些属性会触发Layout和Paint,有些则不会。
csstriggers.com可以看到详细信息。