前端工程化浅析
1.前言:什么是前端工程化
1.1目标
在前端领域,利用技术不断进步和经验逐步积累带来的各种方案,来解决在项目的开发、测试、维护阶段中遇到的种种低效和繁琐的问题。
1.2技术
工程化是一种思想,技术是一种实践。技术会随着时代进步不断地演进和改变,在不同时期,都会有不同地技术来承载和践行着工程化地思想。
1.3原因
前端工程化就是为了提效。这个提效体现在项目地开发、测试及维护阶段。
前端工程化的好处
规范化、模块化、组件化、自动化
2.规范化
规范化是项目可维护的基石
- 版本管理及开发流程规范
- 编写规范
- 脚本
- 样式
- 目录结构
版本规范化的开发过程
git
版本管理/代码仓库
git flow
- 基于git/简化了git的操作
-
活动模型/行为规范
git flow的开发流程如下图所示
流程如下:
git flow init //初始化一个项目
git branch //生成分支,一个master,一个develop
git checkout develop //切换到develop分支进行开发
切换完成后输入以下命令
git pull origin develop
//基于develop新建一个叫f1的
feature分支(是git checkout develop git checkout -b feature/f1的缩写)
git flow festure start f1
//开发完成后提交代码
git commit -am 'ADD#PRO-01#new func'
git push origin feature/f1
//将f1上新增的合并到develop上
git flow feature finish f1
开发完成后可以把develop上的内容合并到
git checkout master
git pull origin master
//先将develop分支上的内容放到release上,若这时候发现了错误可以进行修改,修改完成提交后会将修改同步到master和develop上
git checkout release/0.0.1
git flow release finish 0.0.1
当线上有一些紧急的bug时可以放到hotfix上去修改
git checkout master
git flow hotfix start fix1
//修改完成后用git finish 可以将修改保存到master和develop
3.模块化
一般将逻辑相关的代码放到同一个文件中,当作一个模块。
只需关注模块内逻辑的实现,无需考虑变量污染等问题,模块之间可互相调用。
3.1 CSS模块化解决方案
核心思想通过样式生效规则来避免冲突
scoped
它的原理就是给DOM节点添加data-v-version属性
.selector =>.selector[data-v-version]
CSS in JS
这个是一种思想。以脚本模块来写样式,甚至有封装好的样式模块可以直接使用。
样式 => 按规则生成的唯一selector
CSS MODULES
借助预编译使样式成为脚本中的变量
.selector => Object.selector|.selector => .main__sub__hash
BEM(Block__Element-Modifier)
按照规则,手写css,并在模板内增加相应class
优雅的使用BEM
Shadow DOM
为元素建宇shadow root ,使内部样式与外部样式完全隔离
3.2js模块化解决方案
有两个成熟的框架。一个是nodejs,带来了comminJs规范。
还有一个是从二手开始的Moudle-loader规范
4.组件化
组件化和模块化的核心思想都在于分治,实际带啦的好处就是团队协作效率和项目可维护性的提升
组件化开发时Web开发的趋势
4.1什么是组件
4.1.1UI为主
页面上的一个UI块可以封装成一个组件。比如页面的头部,封装成一个Header组件后,我希望它的脚本、样式和模板可以放在一个文件夹中,到时候便于维护。
4.1.2 逻辑为主
某一个功能逻辑也可以封装成一个组件。封装成一个组件后,我希望它的脚本、样式和模板可以放在一个文件夹中,可以一处封装,多处任意使用。
在Web前端领域,可以将由特定逻辑和UI进行的高内聚,低耦合的封装体称为一个组件。
侧重UI进行封装的组件:代码结构清晰,组件内的模块就近放置,方便进行修改和维护。这种组件具备高内聚,低耦合的特性,但普适性不高。
侧重逻辑进行封装的组件:除了具备上述优点外,还有很高的普适性,更方便组件重用
组件内可以包含组件:偏UI的组件往往都是包含有偏逻辑的组件。
5.自动化
核心思想:能由机器自动完成的事情,绝不让人来做。自动化是前端工程化的核心
- 自动初始化eg.:vue-cli
- 自动构建(打包)eg.:webpack
- 自动测试 eg.:karma,jest
- 自动部署eg.:Jenkins
5.1自动化测试
这个图当中越往上与逻辑越不相关,越往下与逻辑越相关
5.2自动化部署
5.3自动化初始化
通过脚手架自动完成项目初始化,迅速搭建一个项目。
5.4自动化构建
工具有webpack、PARCEL
5.5自动化示例:360搜索专题页开发工具
这个工具的诉求如下:
为实现上述需求,开发一个CLI,专门负责项目初始化和上线发布
配置一个支持多项目打包的webpack工程,满足预编译的需求
开发一个基于webpack4的插件,将静态资源上传至公司CDN
写一个基于Node.js的CLI
用以下命令捕获用户输入的参数和命令,并获得参数触发回调
const programe = require('commander')
program.on('--help',_=>{})
program.command('init').action((name,options) => {})
通过以下代码触发询问与用户交互
const inquirer = require('inquirer');
inquirer.prompt({
type:'confirm',
name:'name',
message:'是否将产品发布至线上',
default:true
}).then(anser =>{})
通过以下代码帮助执行命令,例如发送HTTP请求
const child_process = require('child_process');
const HTTP = require('http');
增强交互效果
const chalk = require('chalk');
console.log(chalk.redBright('专题名称已被使用,请重新输入'));
const ora = require('ora');
const spinner = ora('正在加载中').start();
setTimeout(_ => {
spinner.text = '加载完成';
spinner.succeed();
},1000);
使用webpack4进行项目构建
建议写法
- 将不同环境的配置进行区分
- 集成进来的工具的插件配置单独放置
- evn配置使用.browserslistrc文件单独放置
前端动画还可以这样玩
1.JS动画的基本原理
1.定时器改变对象的属性
2.根据新的属性重新渲染动画
function update(context) {
// 更新属性
}
const ticker = new Ticker();
ticker.tick(update, context);
动画的种类
1.JavaScript 动画
- 操作DOM
- Canvas
2.CSS 动画
- transition
- animation
- SVG 动画
- SMIL
JS动画的优缺点
优点:
- 灵活度
- 可控性
- 性能
缺点:
- 易用性差
简单动画
通过以下代码实现小方块的旋转
let rotation = 0;
requestAnimationFrame(function update() {
block.style.transform = `rotate(${rotation++}deg)`;
requestAnimationFrame(update);
});
这样存在一个问题不能很好的精确控制速度
另一个版本
let rotation = 0;
let startTime = null;
const T = 2000;
requestAnimationFrame(function update() {
if(!startTime) startTime = Date.now();
const p = (Date.now() - startTime)/T;
block.style.transform = `rotate(${360 * p}deg)`;
requestAnimationFrame(update);
});
通用化
function update({target}, count) {
target.style.transform = `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});
通用化2
既可以用target实现又可以用time实现
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});
通用化3
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);
}
});
匀速运动
实现2s内向右匀速运动200px
function update({target}, {timing}) {
target.style.transform = `translate(${200 * timing.p}px, 0)`;
}
const ticker = new Ticker();
ticker.tick(update,
{target: block},
{duration: 2000}
);
自由落体运动实现
速度从0开始增加的一个加速运动
function update({target}, {timing}) {
target.style.transform = `translate(0, ${200 * timing.p}px)`;
}
const ticker = new Ticker();
ticker.tick(update, {target: block}, {
duration: 2000,
easing: p => p ** 2,
});
摩擦力实现
把速度从一个开始的值减到0
function update({target}, {timing}) {
target.style.transform = `translate(${200 * timing.p}px, 0)`;
}
const ticker = new Ticker();
ticker.tick(update, {target: block}, {
duration: 2000,
easing: p => p * (2 - p),
});
平抛
把x轴和y轴的速度区分开
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 op() {
return Math.min(this.time / this.duration, 1.0);
}
get p() {
return this.easing(this.op);
}
}
function update({target}, {timing}) {
target.style.transform =
`translate(${200 * timing.op}px, ${200 * timing.p}px)`;
}
旋转+平抛
function update({target}, {timing}) {
target.style.transform = `
translate(${200 * timing.op}px, ${200 * timing.p}px)
rotate(${720 * timing.op}deg)
`;
}
贝塞尔轨迹
function bezierPath(x1, y1, x2, y2, p) {
const x = 3 * x1 * p * (1 - p) ** 2 + 3 * x2 * p ** 2 * (1 - p) + p ** 3;
const y = 3 * y1 * p * (1 - p) ** 2 + 3 * y2 * p ** 2 * (1 - p) + p ** 3;
return [x, y];
}
function update({target}, {timing}) {
const [px, py] = bezierPath(0.2, 0.6, 0.8, 0.2, timing.p);
target.style.transform = `translate(${100 * px}px, ${100 * py}px)`;
}
const ticker = new Ticker();
ticker.tick(update, {target: block}, {
duration: 2000,
easing: p => p * (2 - p),
});
bezier-easing
- B(px) 作为输入, B(py) 作为输出
- 通过牛顿迭代,从B(px)求p,从p求B(py)
function update({target}, {timing}) {
target.style.transform = `translate(${100 * timing.p}px, 0)`;
}
const ticker = new Ticker();
ticker.tick(update, {target: block}, {
duration: 2000,
easing: BezierEasing(0.5, -1.5, 0.5, 2.5),
});
bezier-easing 轨迹
function update({target}, {timing}) {
target.style.transform =
`translate(${100 * timing.p}px, ${100 * timing.op}px)`;
}
const ticker = new Ticker();
ticker.tick(update, {target: block}, {
duration: 2000,
easing: BezierEasing(0.5, -1.5, 0.5, 2.5),
});
椭圆轨迹
周期运动
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);
}
}
椭圆周期运动
小球转10周停止
function update({target}, {timing}) {
const x = 150 * Math.cos(Math.PI * 2 * timing.p);
const y = 100 * Math.sin(Math.PI * 2 * timing.p);
target.style.transform = `
translate(${x}px, ${y}px)
`;
}
const ticker = new Ticker();
ticker.tick(update, {target: block},
{duration: 2000, iterations: 10});
连续运动
返回一个promise,用await来逐步执行
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);
}
});
});
}
}
function left({target}, {timing}) {
target.style.left = `${100 + 200 * timing.p}px`;
}
function down({target}, {timing}) {
target.style.top = `${100 + 200 * timing.p}px`;
}
function right({target}, {timing}) {
target.style.left = `${300 - 200 * timing.p}px`;
}
function up({target}, {timing}) {
target.style.top = `${300 - 200 * timing.p}px`;
}
(async function() {
const ticker = new Ticker();
await ticker.tick(left, {target: block},
{duration: 2000});
await ticker.tick(down, {target: block},
{duration: 2000});
await ticker.tick(right, {target: block},
{duration: 2000});
await ticker.tick(up, {target: block},
{duration: 2000});
})();
线性插值(lerp)
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);
}
}
可以调用这个函数更方便的实现前面的功能
function setValue(target, value) {
for(let key in value) {
target.style[key] = `${value[key]}px`;
}
}
const left = lerp(setValue, {left: 100}, {left: 300});
const down = lerp(setValue, {top: 100}, {top: 300});
const right = lerp(setValue, {left: 300}, {left: 100});
const up = lerp(setValue, {top: 300}, {top: 100});
(async function() {
const ticker = new Ticker();
await ticker.tick(left, {target: block},
{duration: 2000});
await ticker.tick(down, {target: block},
{duration: 2000});
await ticker.tick(right, {target: block},
{duration: 2000});
await ticker.tick(up, {target: block},
{duration: 2000});
})();
弹跳的小球
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)});
}
})();
弹跳的小球2
给弹跳加一个衰减
(async function() {
const ticker = new Ticker();
let damping = 0.7,
duration = 2000,
height = 300;
// noprotect
while(height >= 1) {
let down = lerp(setValue, {top: 400 - height}, {top: 400});
await ticker.tick(down, {target: block},
{duration, easing: p => p * p});
height *= damping ** 2;
duration *= damping;
let up = lerp(setValue, {top: 400}, {top: 400 - height});
await ticker.tick(up, {target: block},
{duration, easing: p => p * (2 - p)});
}
})();
滚动
const roll = lerp((target, {left, rotate}) => {
target.style.left = `${left}px`;
target.style.transform = `rotate(${rotate}deg)`;
},
{left: 100, rotate: 0},
{left: 414, rotate: 720});
const ticker = new Ticker();
ticker.tick(roll, {target: block},
{duration: 2000, easing: p => 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)});
}());
甩球
function circle({target}, {timing}) {
const p = timing.p;
const rad = Math.PI * 2 * p;
const x = 200 + 100 * Math.cos(rad);
const y = 200 + 100 * Math.sin(rad);
target.style.left = `${x}px`;
target.style.top = `${y}px`;
}
function shoot({target}, {timing}) {
const p = timing.p;
const rad = Math.PI * 0.2;
const startX = 200 + 100 * Math.cos(rad);
const startY = 200 + 100 * Math.sin(rad);
const vX = -100 * Math.PI * 2 * Math.sin(rad);
const vY = 100 * Math.PI * 2 * Math.cos(rad);
const x = startX + vX * p;
const y = startY + vY * p;
target.style.left = `${x}px`;
target.style.top = `${y}px`;
}
(async function() {
const ticker = new Ticker();
await ticker.tick(circle, {target: block},
{duration: 2000, easing: p => p, iterations: 2.1});
await ticker.tick(shoot, {target: block},
{duration: 2000});
}());
逐帧动画
使用background-position来改变图片位置
使用SetInterval()每隔一段时间换一次class
<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)
传入关键帧,与CSS是对应的
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',
});
封装成promise,达到逐个小球运动的效果
function animate(target, keyframes, options) {
const anim = target.animate(keyframes, options);
return new Promise((resolve) => {
anim.onfinish = function() {
resolve(anim);
}
});
}
(async function() {
await animate(ball1, [
{top: '10px'},
{top: '150px'},
], {
duration: 2000,
easing: 'ease-in-out',
fill: 'forwards',
});
await animate(ball2, [
{top: '200px'},
{top: '350px'},
], {
duration: 2000,
easing: 'ease-in-out',
fill: 'forwards',
});
await animate(ball3, [
{top: '400px'},
{top: '550px'},
], {
duration: 2000,
easing: 'ease-in-out',
fill: 'forwards',
});
}());
一起优化前端性能
原因
与用户体验相关,决定了用户去留。希望发现网站的性能瓶颈,从而提升用户体验
1.RAIL模型
1.1 RAIL模型的概念
它是一个以用户为中心的性能模型,将用户行为分为4个方面:
- Response
- Animation
- ldle
- Load
每个网络应用都具有与其生命周期相关的4个方面,而这些方面以不同的方式影响着性能。
它的内容有两个部分: - 目标
是一种恒定性的指标,因为人类对外界的感知是恒定的 - 指导意见
是一些针对性能的评估标准。这些标准往往依赖当时的硬件等因素。
延迟与用户反应:
100ms 以内用户会感觉可以立即获得结果。
超过1s用户注意力会离开他们正在执行的任务。
响应:50ms处理事件
目标
在100ms内响应用户输入
指导
- 50ms内处理用户输入事件,确保100ms内反馈用户可视的响应
- 对于开销大的任务可分隔任务处理,或放到worker进程中执行,避免影响用户交互
- 处理时间超过50ms的操作,始终给予反馈(进度和活动指示器)
动画:10ms处理事件
目标
- 10ms或更短时间内生成一帧
- 视觉平滑
指导
- 在动画这样的高压点,尽量不要处理逻辑。提高达到60fps的机会
- 动画类型
- 滚动
- 视觉动画
- 拖拽动画
空闲时间最大化
目标
最大化空闲时间以增加页面在100ms内响应用户输入的几率
指导
- 利用空闲时间完成推迟的工作
- 空闲时间期间用户交互优先级最高
关键指标
1.响应:在100ms内响应用户输入
2.动画:动画或滚动时,10ms产生一帧
3.空闲时间:主线程空闲时间最大化
4.加载:在1000ms内呈现交互内容
5.以用户为中心
2.工具篇
Lighthouse
可以选择是移动端还是客户端。它进行评估后会给出一些性能优化的建议
WebPageTest
是一个在线的网站
Chorme DEvTools
3.实战篇
3.1浏览器渲染场景
csstriggers.com可以查看每个属性影响的范围
3.2浏览器渲染流程
- JS(实现动画,操作DOM)
- Style(产出渲染树)
- Layout(盒模型,确切的位置和大小)
- Paint(栅格化,完整显示)
-
Composite(渲染层合并)
- 在sources中可以查看代码的耗时情况
- 优化方向:尽量不要在设置样式之后读取它的样式属性
- 用transform属性来移动元素
3.3性能优化方向
- 加载
- 资源效率优化
- 图片优化
- 字体优化
- 关键渲染路径优化
- 渲染
- JS执行优化
- 避免大型复杂的布局
- 渲染层合并