前端工程化淺析
1.前言:什么是前端工程化
1.1目標
在前端領域的妖,利用技術不斷進步和經(jīng)驗逐步積累帶來的各種方案兑燥,來解決在項目的開發(fā)鱼炒、測試泥彤、維護階段中遇到的種種低效和繁瑣的問題欲芹。
1.2技術
工程化是一種思想,技術是一種實踐吟吝。技術會隨著時代進步不斷地演進和改變菱父,在不同時期,都會有不同地技術來承載和踐行著工程化地思想剑逃。
1.3原因
前端工程化就是為了提效浙宜。這個提效體現(xiàn)在項目地開發(fā)、測試及維護階段蛹磺。
前端工程化的好處
規(guī)范化梆奈、模塊化、組件化称开、自動化
2.規(guī)范化
規(guī)范化是項目可維護的基石
- 版本管理及開發(fā)流程規(guī)范
- 編寫規(guī)范
- 腳本
- 樣式
- 目錄結(jié)構(gòu)
版本規(guī)范化的開發(fā)過程
git
版本管理/代碼倉庫
git flow
- 基于git/簡化了git的操作
-
活動模型/行為規(guī)范
git flow的開發(fā)流程如下圖所示
image.png
流程如下:
git flow init //初始化一個項目
git branch //生成分支亩钟,一個master乓梨,一個develop
git checkout develop //切換到develop分支進行開發(fā)
切換完成后輸入以下命令
git pull origin develop
//基于develop新建一個叫f1的
feature分支(是git checkout develop git checkout -b feature/f1的縮寫)
git flow festure start f1
//開發(fā)完成后提交代碼
git commit -am 'ADD#PRO-01#new func'
git push origin feature/f1
//將f1上新增的合并到develop上
git flow feature finish f1
開發(fā)完成后可以把develop上的內(nèi)容合并到
git checkout master
git pull origin master
//先將develop分支上的內(nèi)容放到release上,若這時候發(fā)現(xiàn)了錯誤可以進行修改清酥,修改完成提交后會將修改同步到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.模塊化
一般將邏輯相關的代碼放到同一個文件中扶镀,當作一個模塊。
只需關注模塊內(nèi)邏輯的實現(xiàn)焰轻,無需考慮變量污染等問題臭觉,模塊之間可互相調(diào)用。
3.1 CSS模塊化解決方案
核心思想通過樣式生效規(guī)則來避免沖突
scoped
它的原理就是給DOM節(jié)點添加data-v-version屬性
.selector =>.selector[data-v-version]
CSS in JS
這個是一種思想辱志。以腳本模塊來寫樣式蝠筑,甚至有封裝好的樣式模塊可以直接使用。
樣式 => 按規(guī)則生成的唯一selector
CSS MODULES
借助預編譯使樣式成為腳本中的變量
.selector => Object.selector|.selector => .main__sub__hash
BEM(Block__Element-Modifier)
按照規(guī)則揩懒,手寫css什乙,并在模板內(nèi)增加相應class
優(yōu)雅的使用BEM
Shadow DOM
為元素建宇shadow root ,使內(nèi)部樣式與外部樣式完全隔離
3.2js模塊化解決方案
有兩個成熟的框架。一個是nodejs,帶來了comminJs規(guī)范已球。
還有一個是從二手開始的Moudle-loader規(guī)范
4.組件化
組件化和模塊化的核心思想都在于分治臣镣,實際帶啦的好處就是團隊協(xié)作效率和項目可維護性的提升
組件化開發(fā)時Web開發(fā)的趨勢
4.1什么是組件
4.1.1UI為主
頁面上的一個UI塊可以封裝成一個組件。比如頁面的頭部智亮,封裝成一個Header組件后忆某,我希望它的腳本、樣式和模板可以放在一個文件夾中阔蛉,到時候便于維護弃舒。
4.1.2 邏輯為主
某一個功能邏輯也可以封裝成一個組件。封裝成一個組件后状原,我希望它的腳本棒坏、樣式和模板可以放在一個文件夾中,可以一處封裝遭笋,多處任意使用。
在Web前端領域徒探,可以將由特定邏輯和UI進行的高內(nèi)聚瓦呼,低耦合的封裝體稱為一個組件。
側(cè)重UI進行封裝的組件:代碼結(jié)構(gòu)清晰测暗,組件內(nèi)的模塊就近放置央串,方便進行修改和維護。這種組件具備高內(nèi)聚碗啄,低耦合的特性质和,但普適性不高。
側(cè)重邏輯進行封裝的組件:除了具備上述優(yōu)點外稚字,還有很高的普適性饲宿,更方便組件重用
組件內(nèi)可以包含組件:偏UI的組件往往都是包含有偏邏輯的組件厦酬。
5.自動化
核心思想:能由機器自動完成的事情,絕不讓人來做瘫想。自動化是前端工程化的核心
- 自動初始化eg.:vue-cli
- 自動構(gòu)建(打包)eg.:webpack
- 自動測試 eg.:karma,jest
- 自動部署eg.:Jenkins
5.1自動化測試
這個圖當中越往上與邏輯越不相關仗阅,越往下與邏輯越相關
5.2自動化部署
5.3自動化初始化
通過腳手架自動完成項目初始化,迅速搭建一個項目国夜。
5.4自動化構(gòu)建
工具有webpack减噪、PARCEL
5.5自動化示例:360搜索專題頁開發(fā)工具
這個工具的訴求如下:
為實現(xiàn)上述需求,開發(fā)一個CLI车吹,專門負責項目初始化和上線發(fā)布
配置一個支持多項目打包的webpack工程筹裕,滿足預編譯的需求
開發(fā)一個基于webpack4的插件,將靜態(tài)資源上傳至公司CDN
寫一個基于Node.js的CLI
用以下命令捕獲用戶輸入的參數(shù)和命令窄驹,并獲得參數(shù)觸發(fā)回調(diào)
const programe = require('commander')
program.on('--help',_=>{})
program.command('init').action((name,options) => {})
通過以下代碼觸發(fā)詢問與用戶交互
const inquirer = require('inquirer');
inquirer.prompt({
type:'confirm',
name:'name',
message:'是否將產(chǎn)品發(fā)布至線上'朝卒,
default:true
}).then(anser =>{})
通過以下代碼幫助執(zhí)行命令,例如發(fā)送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進行項目構(gòu)建
建議寫法
- 將不同環(huán)境的配置進行區(qū)分
- 集成進來的工具的插件配置單獨放置
- evn配置使用.browserslistrc文件單獨放置
前端動畫還可以這樣玩
1.JS動畫的基本原理
1.定時器改變對象的屬性
2.根據(jù)新的屬性重新渲染動畫
function update(context) {
// 更新屬性
}
const ticker = new Ticker();
ticker.tick(update, context);
動畫的種類
1.JavaScript 動畫
- 操作DOM
- Canvas
2.CSS 動畫
- transition
- animation
- SVG 動畫
- SMIL
JS動畫的優(yōu)缺點
優(yōu)點:
- 靈活度
- 可控性
- 性能
缺點:
- 易用性差
簡單動畫
通過以下代碼實現(xiàn)小方塊的旋轉(zhuǎn)
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實現(xiàn)又可以用time實現(xiàn)
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);
}
});
勻速運動
實現(xiàn)2s內(nèi)向右勻速運動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}
);
自由落體運動實現(xiàn)
速度從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,
});
摩擦力實現(xiàn)
把速度從一個開始的值減到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軸的速度區(qū)分開
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)`;
}
旋轉(zhuǎn)+平拋
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);
}
}
橢圓周期運動
小球轉(zhuǎn)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});
連續(xù)運動
返回一個promise,用await來逐步執(zhí)行
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);
}
}
可以調(diào)用這個函數(shù)更方便的實現(xiàn)前面的功能
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});
平穩(wěn)變速
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',
});
}());
一起優(yōu)化前端性能
原因
與用戶體驗相關,決定了用戶去留歹河。希望發(fā)現(xiàn)網(wǎng)站的性能瓶頸掩浙,從而提升用戶體驗
1.RAIL模型
1.1 RAIL模型的概念
它是一個以用戶為中心的性能模型,將用戶行為分為4個方面:
- Response
- Animation
- ldle
- Load
每個網(wǎng)絡應用都具有與其生命周期相關的4個方面秸歧,而這些方面以不同的方式影響著性能厨姚。
它的內(nèi)容有兩個部分: - 目標
是一種恒定性的指標,因為人類對外界的感知是恒定的 - 指導意見
是一些針對性能的評估標準键菱。這些標準往往依賴當時的硬件等因素谬墙。
延遲與用戶反應:
100ms 以內(nèi)用戶會感覺可以立即獲得結(jié)果。
超過1s用戶注意力會離開他們正在執(zhí)行的任務经备。
響應:50ms處理事件
目標
在100ms內(nèi)響應用戶輸入
指導
- 50ms內(nèi)處理用戶輸入事件拭抬,確保100ms內(nèi)反饋用戶可視的響應
- 對于開銷大的任務可分隔任務處理,或放到worker進程中執(zhí)行侵蒙,避免影響用戶交互
- 處理時間超過50ms的操作造虎,始終給予反饋(進度和活動指示器)
動畫:10ms處理事件
目標
- 10ms或更短時間內(nèi)生成一幀
- 視覺平滑
指導
- 在動畫這樣的高壓點,盡量不要處理邏輯纷闺。提高達到60fps的機會
- 動畫類型
- 滾動
- 視覺動畫
- 拖拽動畫
空閑時間最大化
目標
最大化空閑時間以增加頁面在100ms內(nèi)響應用戶輸入的幾率
指導
- 利用空閑時間完成推遲的工作
- 空閑時間期間用戶交互優(yōu)先級最高
關鍵指標
1.響應:在100ms內(nèi)響應用戶輸入
2.動畫:動畫或滾動時算凿,10ms產(chǎn)生一幀
3.空閑時間:主線程空閑時間最大化
4.加載:在1000ms內(nèi)呈現(xiàn)交互內(nèi)容
5.以用戶為中心
2.工具篇
Lighthouse
可以選擇是移動端還是客戶端份蝴。它進行評估后會給出一些性能優(yōu)化的建議
WebPageTest
是一個在線的網(wǎng)站
Chorme DEvTools
3.實戰(zhàn)篇
3.1瀏覽器渲染場景
csstriggers.com可以查看每個屬性影響的范圍
3.2瀏覽器渲染流程
- JS(實現(xiàn)動畫,操作DOM)
- Style(產(chǎn)出渲染樹)
- Layout(盒模型澎媒,確切的位置和大懈惴Α)
- Paint(柵格化,完整顯示)
-
Composite(渲染層合并)
性能面板 - 在sources中可以查看代碼的耗時情況
- 優(yōu)化方向:盡量不要在設置樣式之后讀取它的樣式屬性
- 用transform屬性來移動元素
3.3性能優(yōu)化方向
- 加載
- 資源效率優(yōu)化
- 圖片優(yōu)化
- 字體優(yōu)化
- 關鍵渲染路徑優(yōu)化
- 渲染
- JS執(zhí)行優(yōu)化
- 避免大型復雜的布局
- 渲染層合并