前端工程化是一個很是普遍的議題,包含的技術和解決方案也是很是豐富的。一個前端工程的生命週期能夠大體劃分爲這四個過程:
css
前端工程的生命週期html
任何在這四個過程當中應用的系統化、嚴格約束、可量化的方法均可以稱之爲工程化。工程化的程度越高,在工做中因人的個體差別性致使的缺陷或者短板就會越少,項目質量能夠獲得更有效的保障。對上面四個過程的工程化並非徹底分隔的,而是相輔相成,好比開發階段的優化也會對測試、部署和維護產生很大的影響。
前端
下面從模塊化、組件化、規範化和自動化這四個方面進行具體介紹。node
模塊化react
模塊化能夠對複雜邏輯進行有效分割,每一個模塊更關注自身的功能,模塊內部的數據和實現是私有的,經過向外部暴露一些接口來實現各模塊間的通訊。開發階段前端須要關注JS、CSS和HTML,下面咱們將分別對JS、CSS、HTML的模塊化進行簡單介紹。jquery
JS模塊化是一個逐漸演變的過程,開始的namespace概念實現了簡單對象封裝,約定私有屬性使用_開頭,到後來的IIFE模式,利用匿名函數閉包的原理解決模塊的隔離與引用,下面介紹如今比較流行的幾種模塊化標準。android
Nodejs中的模塊化方案,就是基於CommonJS規範實現的。一個文件就是一個模塊,有本身的做用域,沒有export的變量和方法都是私有的,不會污染全局做用域,模塊的加載是運行時同步加載的。CommonJS能夠細分爲CommonJS1和CommonJS2,兩者的模塊導出方式不一樣,CommonJS2兼容CommonJS1,增長了module.exports的導出方式,如今通常所指的都是CommonJS2。webpack
每一個文件一個模塊,有本身的做用域,不會污染全局;git
使用require同步加載依賴的其餘模塊,經過module.exports導出須要暴露的接口;es6
屢次require的同一模塊只會在第一次加載時運行,並將運行結果緩存,後續直接讀取緩存結果,若是須要從新執行,須要先清理緩存;
Nodejs環境下能夠直接運行,各個模塊按引入順序依次執行。
module.exports.add = function (a, b) { return a + b;}
exports.add = function (a, b) { return a + b;}
const sum = require('sum');sum.add(1, 2);
瀏覽器加載js文件須要進行網絡請求,而網絡請求的耗時是不可預期的,這使得CommonJS同步加載模塊的機制在瀏覽器端並不適用,咱們不能由於要加載某個模塊js而一直阻塞瀏覽器繼續執行下面的代碼。AMD規範則採用異步的方式加載模塊,容許指定回調函數,這很是適合用於瀏覽器端的模塊化場景。
使用define定義一個模塊,使用require加載模塊;
異步加載,能夠並行請求依賴模塊;
原生JavaScript運行環境沒法直接執行AMD規範的模塊代碼,須要引入第三方庫支持,如requirejs等;
// 定義一個模塊define(id ? , dependencies ? , factory); // 引用一個模塊require([module], callback)
相似於AMD規範,是應用在瀏覽器端的JS模塊化方案,由sea.js提出,詳見 https://www.zhihu.com/question/20351507 。
UMD規範兼容AMD和CommonJS,在瀏覽器和Nodejs中都可以運行。
(function (root, factory) { if (typeof define === 'function' && define.amd) { define(['jquery', 'underscore'], factory); } else if (typeof exports === 'object') { module.exports = factory(require('jquery'), require('underscore')); } else { root.returnExports = factory(root.jQuery, root._); }}(this, function ($, _) { function a() {}; function b() {}; function c() {};
return { b: b, c: c }}));
ES6從語言標準的層面上實現了模塊化,是ECMA提出的模塊化標準,後續瀏覽器和Nodejs都宣佈會原生支持,愈來愈受開發者青睞。
使用import引入模塊,export導出模塊;
與CommonJS的執行時機不一樣,只是個只讀引用,只會在真正調用的地方開始執行,而不是像CommonJS那樣,在require的時候就會執行代碼;
支持度暫不完善,須要進行代碼轉換成上面介紹的某一種模塊化規範。
在瀏覽器中能夠經過下面的方式引入es6規範的模塊js:
<script type="module" src="foo.mjs"></script> <script type="module" src="foo.mjs" defer></script>
defer和async不一樣,它會阻塞DomContentLoaded事件,每一個模塊js會根據引入的順序依次執行。
隨着更多瀏覽器對ES6的支持,如今有一些方案開始提出直接使用ES2015+的代碼在瀏覽器中直接執行來提升運行效果,這篇文章《Deploying ES2015+ Code in Production Today》中有詳細的介紹,能夠結合這份性能測試報告綜合評估ES6在node以及各類瀏覽器環境下的執行效率對比。
CSS 自誕生以來,基本語法和核心機制一直沒有本質上的變化,它的發展幾乎全是表現力層面上的提高。不一樣於JS,CSS自己不具備高級編程屬性,沒法使用變量、運算、函數等,沒法管理依賴,全局做用域使得在編寫CSS樣式的時候須要更多人工去處理優先級的問題,樣式名還有壓縮極限的問題,爲此,出現了不少「編譯工具」和「開發方案」爲CSS賦予「編程能力」。
隨着頁面愈來愈複雜,爲了便於開發和維護,咱們經常會將CSS文件進行切分,而後再將須要的文件進行合併。諸如LESS、SASS、Stylus等預處理器爲CSS帶來了編程能力,咱們可使用變量、運算、函數,@import指令能夠輕鬆合併文件。但各類預處理器並不能徹底解決全局做用域的問題,須要結合namespace的思想去命名。
OOCSS和SMACSS都是有關css的方法論。OOCSS(Object Oriented CSS)即面向對象的CSS,旨在編寫高可複用、低耦合和高擴展的CSS代碼,有兩個主要原則,它們都是用來規定應該把什麼屬性定義在什麼樣式類中。
Separate structure and skin(分離結構和主題)
Separate container and content(分離容器和內容)
SMACSS(Scalable and Modular Architecture for CSS)是可擴展模塊化的CSS,它的核心就是結構化CSS代碼,則有三個主要規則:
Categorizing CSS Rules (CSS分類規則):將CSS分紅Base、Layout、Module、State、Theme這5類。
Naming Rules(命名規則):考慮用命名體現樣式對應的類別,如layout-這樣的前綴。
Minimizing the Depth of Applicability(最小化適配深度):下降對特定html結構的依賴。
/* 依賴html結構,不提倡 */.sidebar ul h3 { }
/* 建議直接定義 */.sub-title { }
BEM是一種CSS命名規範,旨在解決樣式名的全局衝突問題。BEM是塊(block)、元素(element)、修飾符(modifier)的簡寫,咱們經常使用這三個實體開發組件。
塊(block):一種佈局或者設計上的抽象,每個塊擁有一個命名空間(前綴)。
元素(element):是.block的後代,和塊一塊兒造成一個完整的實體。
修飾符(modifier):表明一個塊的狀態,表示它持有的一個特定屬性。
在選擇器中,BEM要求只使用類名,不容許使用id,由如下三種符號來表示擴展的關係:
中劃線( - ) :僅做爲連字符使用,表示某個塊或者某個子元素的多單詞之間的鏈接記號。
雙下劃線( __ ):雙下劃線用來鏈接塊和塊的子元素。
單下劃線( _ ):單下劃線用來描述一個塊或者塊的子元素的一種狀態。
.type-block__element_modifier {}
從上面BEM的命名要求能夠看到,類名都很長,這就致使在對CSS文件進行壓縮的時候,咱們沒法獲得更大的優化空間。並且BEM僅僅是一種規範,須要團隊中的開發者自行遵照,在可靠性上沒法獲得有效保障,並且還可能和第三方庫的命名衝突。
CSS in JS是一種比較激進的方案,完全拋棄了CSS,徹底使用JS來編寫CSS,又用起了行內樣式(inline style),它的發展得益於React的出現,具體的緣由能夠參見組件化這部份內容。
解決全局命名污染的問題;
更貼近Web組件化的思想;
能夠在一些沒法解析CSS的運行環境下執行,好比React Native等;
JS賦予CSS更多的編程能力,實現了CSS和JS間的變量共享;
支持CSS單元測試,提升CSS的安全性;
原生JS編寫CSS沒法支持到不少特性,好比僞類、media query等,須要引入額外的第三方庫來支持,各類庫的對比詳見css-in-js;
有運行時損耗,性能比直接class要差一些;
不容易debug;
下面以styled-components爲例:
import React from 'react';import styled from 'styled-components';
const Container = styled.div` text-align: center;`;
const App = () => ( <Container> It is a test! </Container>);
render(<App />, document.getElementById('content'));
構建後的結果以下,咱們發現不會再有.css文件,一個.js文件包含了組件相關的所有代碼:
var _templateObject = _taggedTemplateLiteral(['\n text-align: center;\n'], ['\n text-align: center;\n']);
function _taggedTemplateLiteral(strings, raw) { return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } }));}
var Container = _styledComponents2.default.div(_templateObject);
var App = function App() { return _react2.default.createElement( Container, null, 'It is a test!' );};
CSS module則最大化地結合了現有CSS生態和JS模塊化的能力,之前用於CSS的技術均可以繼續使用。CSS module最終會構建出兩個文件:一個.css文件和一個.js。
解決全局命名污染的問題;
默認是局部的,能夠用:global聲明全局樣式;
受CSS的限制,只能一層嵌套,和JS沒法共享變量;
能支持如今全部的CSS技術。
以webpack爲例,使用css-loader就能夠實現CSS module:
module.exports = { ... module: { rules: [ ... { loader: 'css-loader', options: { importLoaders: 1, modules: { localIdentName: "[name]__[local]--[hash:base64:5]" },
} } ... ] } ...}
下面是一個組件開發的例子:
/* style.css */.color { color: green;}
:local .className .subClass :global(.global-class-name) { color: blue;}
/* component.js */import styles from './style.css';elem.outerHTML = `<h1 class=${styles.color}>It is a test title</h1>`;
構建運行後生成的dom結構以下:
<h1 class="style__color--rUMvq">It is a test title</h1>
component.js中styles變量的值以下,咱們看到聲明成:global的類名.global-class-name沒有被轉換,具備全局做用域。
const styles = { "color": "style__color--rUMvq", "className": "style__className--3n_7c", "subClass": "style__subClass--1lYnt"}
說明:React對樣式如何定義並無明確態度,不管是BEM規範,仍是CSS in JS或者CSS module都是支持的,選擇何種方案是開發者自行決定的。
組件化
最初,網頁開發通常都會遵循一個原則」關注點分離」,各個技術只負責本身的領域,不能混合在一塊兒,造成耦合。HTML只負責結構,CSS負責樣式,JS負責邏輯和交互,三者徹底隔離,不提倡寫行內樣式(inline style)和行內腳本(inline script)。React的出現打破了這種原則,它的考慮維度變成了一個組件,要求把組件相關的HTML、CSS和JS寫在一塊兒,這種思想能夠很好地解決隔離的問題,每一個組件相關的代碼都在一塊兒,便於維護和管理。
咱們回想一下原有引用組件的步驟:
引入這個組件的JS;
引入這個組件的樣式CSS(若是有);
在頁面中引入這個組件的;
最後是編寫初始化組件的代碼。
這種引入方式很繁瑣,一個組件的代碼分佈在多個文件裏面,並且做用域暴露在全局,缺少內聚性容易產生衝突。
組件化就是將頁面進行模塊拆分,將某一部分獨立出來,多個組件能夠自由組合造成一個更復雜的組件。組件將數據、視圖和邏輯封裝起來,僅僅暴露出須要的接口和屬性,第三方能夠徹底黑盒調用,不須要去關注組件內部的實現,很大程度上下降了系統各個功能的耦合性,而且提升了功能內部的聚合性。
React、Vue、Angular等框架的流行推進了Web組件化的進程。它們都是數據驅動型,不一樣於DOM操做是碎片的命令式,它容許將兩個組件經過聲明式編程創建內在聯繫。
<!-- 數據驅動的聲明式Declarative--><pagination current={current} total={maxCount/20} on-nav={this.nav(1)}></pagination>
<!-- DOM操做的命令式Imprective --><pagination id='pagination'></pagination><script>// 獲取元素var pagination = document.querySelector('#pagination');
// 綁定事件pagination.addEventListener('pagination-nav', function(event){ ...})
// 設置屬性$.ajax('/blogs').then(function( json ){ pagination.setAttribute('current', 0) pagination.setAttribute('total', json.length / 20)})</script>
從上面的例子能夠看到,聲明式編程讓組件更簡單了,咱們不須要去記住各類DOM相關的API,這些所有交給框架來實現,開發者僅僅須要聲明每一個組件「想要畫成什麼樣子」。
JSX vs 模板DSL
React使用JSX,很是靈活,與JS的做用域一致。Vue、Angular採用模板DSL,可編程性受到限制,做用域和JS是隔離的,但也是這個缺點使得咱們能夠在構建期間對模板作更多的事情,好比靜態分析、更好地代碼檢查、性能優化等等。兩者都沒有瀏覽器原生支持,須要通過Transform才能運行。
Web Component是W3C專門爲組件化建立的標準,一些Shadow DOM等特性將完全的、從瀏覽器的層面解決掉一些做用域的問題,並且寫法一致,它有幾個概念:
Custom Element: 帶有特定行爲且用戶自命名的 HTML 元素,擴展HTML語義;
<x-foo>Custom Element</x-foo>
/* 定義新元素 */var XFooProto = Object.create(HTMLElement.prototype);
// 生命週期相關XFooProto.readyCallback = function() { this.textContent = "I'm an x-foo!";};
// 設置 JS 方法XFooProto.foo = function() { alert('foo() called'); };
var XFoo = document.register('x-foo', { prototype: XFooProto });
// 建立元素var xFoo = document.createElement('x-foo');
Shadow DOM:對標籤和樣式的一層 DOM 封裝,能夠實現局部做用域;當設置{mode: closed}後,只有其宿主纔可定義其表現,外部的api是沒法獲取到Shadow DOM中的任何內容,宿主的內容會被Shadow DOM掩蓋。
var host = document.getElementById('js_host');var shadow = host.attachShadow({mode: 'closed'});shadow.innerHTML = '<p>Hello World</p>';
Chrome調試工具:DevTool > Settings > Preferences> Show user agent shadow DOM
Chrome調試工具查看shadow DOM
HTML Template & Slots: 可複用的 HTML 標籤,提供了和用戶自定義標籤相結合的接口,提升組件的靈活性。定義了template的標籤,相似咱們常常用的<script type='tpl'>,它不會被解析爲dom樹的一部分,template的內容能夠被塞入到Shadow DOM中而且反覆使用;template中定義的style只對該template有效,實現了隔離。
<template id="tpl"> <style> p { color:red; }</style> <p>hello world</p></template>
<script> var host = document.getElementById('js_host'); var shadow = host.attachShadow({mode: 'open'}); var tpl = document.getElementById("tpl").content.cloneNode(true); shadow.appendChild(tpl);</script>
dom樹中的template標籤,不解析:
HTML template-1
最終插入的影子節點效果:
HTML template-2
因爲Shadow DOM中宿主元素的內容會被影子節點掩蓋,若是想將宿主中某些內容顯示出來的話就須要藉助slot,它是定義在宿主和template中的一個插槽,用來「佔位」。
<div id="host"> <span>Test1</span> <span slot="s1">slot1</span> <span slot="s2">slot2</span> <span>Test2</span></div><template id="tpl"> <span>tpl1</span> <slot name="s1"></slot> <slot name="s2"></slot> <span>tpl2</span></template>
宿主元素中設置了slot屬性的節點被「保留」了下來,而且插入到了template中定義的slot的位置。
slot的示例
HTML Imports: 打包機制,將HTML代碼以及Web Componnet導入到頁面中,這個規範目前已經不怎麼推進了,在參考了ES6 module的機制後,FireFox團隊已經不打算繼續支持。
<link rel="import" href="/path/to/imports/stuff.html">
Polymer
Polymer是基於Web Componet的一種數據驅動型開發框架,可使用ES6 class來定義一個Web Component,因爲如今瀏覽器對Web Component的支持度還不是很好,須要引入一些polyfill才能使用。
React和Web Component並非對立的,它們解決組件化的角度是不一樣,兩者能夠相互補充。與Web Component不一樣的是React中的HTML標籤運行在Virtual DOM中,在非標準的瀏覽器環境,React的這種機制能夠更好地實現跨平臺,Web Component則更有可能實現瀏覽器大統一,是瀏覽器端更完全的一種解決方案。
規範化
規範化是保障項目質量的一個重要環節,能夠很好地下降團隊中個體的差別性。
代碼規範是一個老生常談的話題,咱們須要制定一些原則來統一代碼風格,雖然不遵照規範的代碼也是能夠運行的,可是這會對代碼的維護帶來不少麻煩。
根據維基百科的介紹,首先看一下lint的定義:
lint最初是一個特定程序的名稱,它在C語言源代碼中標記了一些可疑的和不可移植的構造(多是bug)。這個術語(lint或者linter)如今通常用於稱呼那些能夠標記任何計算機語言編寫的軟件中可疑用法的工具,這些工具一般執行源代碼的靜態分析。
通常代碼的Linter工具提供下面兩大類的規則:
格式化規則:好比 max-len, no-mixed-spaces-and-tabs等等,這些規則只是用來統一書寫格式的。
代碼質量規則:好比 no-unused-vars, no-extra-bind, no-implicit-globals等等,這些規則能夠幫助提高代碼質量,減小bug。
在實際的項目中能夠引入lint的機制來提高代碼質量,能夠參考GitHub 官方出品的 Lint 工具列表 ,下面簡單介紹幾個經常使用工具。
Prettier
Prettier是一個代碼格式化工具,能夠統一團隊中的書寫風格,比下面Eslint這類工具的功能要弱,由於只是對格式上的約束,沒法對代碼質量進行檢測。
ESlint
ESLint是一款很是經常使用的JS編程規範庫,固然還有不少其餘的lint工具。下面的表格裏簡單介紹了3種經常使用的規範標準,能夠在ESLint中配置選擇哪種標準,每一種標準都會包含不少編程規則。各個標準沒有絕對的孰優孰劣,選擇適用於團隊的編程風格和規範就好。
標準 | 簡介 |
---|---|
Airbnb JavaScript Style Guide | 目前最受歡迎的JS編程規範之一,對不少JS框架都有支持,好比React等。 |
Google JavaScript Style Guide | Google Style的JS編程規範。 |
JavaScript Standard Style Guide | 很強大,自帶linter和自動代碼糾正,無需配置,自動格式化代碼。不少知名公司所採用,好比 Nodejs、npm、express、GitHub、mongoDB 等。 |
husky
若是咱們把Lint放在了持續集成CI階段,就會遇到這樣一個問題:CI系統在Lint時發現了問題致使構建失敗,這個時候咱們須要根據錯誤從新修改代碼,而後重複這個過程直到Lint成功,整個過程可能會浪費掉很多時間。針對這個問題,咱們發現只在CI階段作Lint是不夠的,須要把Lint提早到本地來縮短整個修改鏈路。可是將Lint放在本地僅僅依靠開發者的自覺遵照是不夠的,咱們須要更好的方案,須要依靠流程來保障而不是人的自覺性。
Lint的問題
husky能夠註冊git hooks,攔截一些錯誤的提交,好比咱們就能夠在pre-commit這個hook中增長Lint的校驗,這裏能夠查看支持的git hooks。
lint-staged
經過husky註冊的git hook會對倉庫中的所有文件都執行設置的npm命令,但咱們僅僅須要對提交到staged區的文件進行處理來減小校驗時間,lint-staged能夠結合husky實現這個功能,在package.json中的示例:
{ "husky": { "hooks": { "pre-commit": "lint-staged", } }, "lint-staged": { "src/**/*.js": "eslint" }}
JavaScript是很是靈活的,這得益於它的弱類型語言特色,但也是由於這個緣由,咱們只有在運行時才知道變量究竟是什麼類型,沒法在編譯階段做出任何類型錯誤的提示,同時因爲函數參數類型的不肯定性,編譯器的編譯結果極可能沒法被複用,好比下面的例子中,在執行add(1,2)時對add函數的編譯結果沒法直接被下面的add('1', '2')複用,第二次調用必須得再從新編譯一次,這對性能也是有很大影響。
function add(a, b) { return a + b;}add(1, 2);add('1', '2');
類型檢查可讓咱們編寫出更高質量的代碼,減小類型錯誤的bug,同時明確了類型也讓代碼更好維護。
PropTypesReact在15.5的版本後將類型檢查React.PropTypes移除後使用prop-types庫代替,它是一種運行時的類型檢測機制,包含一整套驗證器,可用於確保組件屬性接收的數據是正確的類型。
import React, { Component } from 'react';import PropTypes from 'prop-types';
class App extends Component {
}
App.propTypes = { title: PropTypes.string.isRequired}
Flow和PropTypes不一樣,Flow是一種靜態類型檢查器,由Facebook開源,賦予JS強類型的能力,在編譯階段就能夠檢測出是否有類型錯誤,能夠被用於任何JavaScript項目。
Flow主要有兩個工做方式:
function split(str) { return str.split(' ')}split(11);
function square(n: number): number { return n * n;}square("2");
Flow風格的代碼不能直接在JS運行環境中執行,須要使用babel進行轉換。就目前的發展和生態而言,Flow離TypeScript的差距已經愈來愈遙遠了,Vue在2.0版本開始使用flow.js,但從3.0起已經替換成了TypeScript。
TypeScriptTypeScript則是一種JavaScript語言的超集,強類型、支持靜態類型檢查,更像是一門「新語言」。Deno已經支持直接運行tcs了,不須要進行轉換。
interface Person { firstName: string; lastName: string;}
function greeter(person: Person) { return "Hello, " + person.firstName + " " + person.lastName;}
高質量的項目文檔能夠提升團隊協做效率,便於後期優化維護。維護文檔很重要,可是也很繁瑣,咱們常常會看到代碼和文檔南轅北轍互相矛盾,下面介紹幾種文檔構建工具,它們能夠很好地幫助咱們構建文檔,而對於React、Vue等組件而言,使用MDX能夠很是便捷構建demo,極大減小人工保證代碼和文檔一致性的工做:
當團隊在開發時,一般會使用版本控制系統來管理項目,經常使用的有svn和git,如何合併代碼、如何發佈版本都須要相應的流程規範,這可讓咱們規避不少問題,好比合並代碼後出現代碼丟失,又或者將別人未經測試的代碼發佈出去等等。下面主要介紹幾種基於git的協做開發模式:
以部署爲中心的開發模式,持續且高速安全地進行部署,具體流程以下:
github-flow的最大特色就是簡單,只有一個master長期分支,可是因爲要持續部署,當一個部署還未完成的時候,每每下一個Pull Request已經完成,這就致使在開發速度愈來愈快的時候,必需要讓部署所需的一系列流程都是自動化的,好比有自動化測試、接入CI等。
有兩個長期分支master和develop,這意味着不要直接在這兩個分支上進行push操做,全部的開發都在feature分支上進行,詳見文檔。
git-flow工做流
功能開發:首先從develop分支建立feature分支,而後和上面github-flow的流程相似,開發測試完畢後向develop分支發起Pull Request,其餘開發者review完畢後將這次PR合併至develop分支。
管理Release:當develop分支能夠release的時候,首先建立一個release/版本號分支,而後對這個release分支打上tag後再合併到develop和master中去。
hotfix:當出現了緊急bug的時候,須要開啓「hotfix」流程,和release不一樣的是,這個hotfix分支是基於master建立的,修復bug後提交到這個hotfix分支,而後又和release分支的處理很是相似,改動會被同時合併到develop和master中去,最後這個hotfix分支被刪除掉。
github-flow有一個問題,它要求master分支和生產環境是徹底一致,一旦PR經過被合併到了master分支,就要馬上部署發佈到生成環境,可是每每受限於產品發佈時間,master分支極可能會先於生產環境,這個時候不能依靠master分支來追蹤線上版本。git-flow的流程比較複雜,須要維護兩個長期分支master和develop,開發過程要以develop分支爲準,可是不少開發工具將master當作默認分支,就須要頻繁切換分支。git-flow的模式是基於「版本發佈」,這對一些持續發佈部署的項目不太適用。gitlab-flow則是上面兩個工做流的綜合,推出一個「上游優先」的最大原則,即只存在一個master主分支,它是全部分支的上游,只有合併到master上的代碼才能應用到其餘分支,詳見文檔。
持續發佈對於這種模式的項目,master分支對應開發環境,而後須要再建立pre-production和production兩個分支,它們的上游鏈路依次是:master分支—>pre-production分支—>production分支,只有合併進入master分支的代碼修改才能依次應用合併到」下游」。
版本發佈在這種模式下,首先基於master分支建立某個版本的stable分支,而後將代碼改動合併進master分支,當須要發版本的時候,將master分支使用cherry-pick合併到stable分支中去,而後基於stable分支進行項目的發佈部署。
自動化
在前端項目開發中咱們使用了模塊化的方案,有可能還引入了組件化的機制,依賴一些開發框架,這個時候就須要對項目進行構建,構建通常能夠包括這幾個步驟:
代碼轉換:容許使用更高級的JavaScript語法,好比ES六、TypeScript等等,這對代碼的開發和可維護性來講是很是有好處的。
模塊合併:按模塊化開發的代碼須要進行打包合併。
文件優化:常見的有代碼壓縮和Code Splitting,使用ES6 module的模塊化機制的還能夠考慮構建工具的Tree Shaking功能,進一步減小代碼體積。
自動刷新:在開發過程當中支持file watch和HMR都是能夠很好地提高開發效率。
在軟件的生命週期中,不一樣的測試階段,針對的測試問題是不同的:
JavaScript 單元測試,咱們真的須要嗎?答案是須要結合項目看實際狀況。若是是基礎庫或者公共組件這樣的項目,單元測試仍是頗有必要的。而對於那種就上線幾天的活動頁,寫詳細的單元測試可能真的會有點入不敷出。引用這篇文章結尾處是思考:
「怎麼單元測試寫起來這麼麻煩」
——說明項目模塊之間存在耦合度高,依賴性強的問題。
「怎麼要寫這麼長的測試代碼啊」
——這是一勞永逸的,而且每次需求變動後,你均可經過單元測試來驗證,邏輯代碼是否依舊正確。
「個人模塊沒問題的,是你的模塊出了問題」
——程序中每一項功能咱們都用測試來驗證的它的正確性,快速定位出現問題的某一環。
「上次修復的 bug 怎麼又出現了 」
——單元測試可以避免代碼出現迴歸,編寫完成後,可快速運行測試。
TDD (測試驅動開發Test-Driven Development)和 BDD (行爲驅動開發Behavior Driven Development)是兩種開發模式,並非單單指如何進行代碼測試,它們定義了一種軟件開發模式來將用戶需求、開發人員和測試人員進行有效的聯合,減小三者之間的脫節。TDD要求開發者先寫測試用例,而後根據測試用例的結果再寫真正實現功能的代碼,接下來繼續運行測試用例,再根據結果修復代碼,該過程重複屢次,直到每一個測試用例運行正確。BDD則是對TDD的一種補充,咱們沒法保證在TDD中的測試用例能夠徹底達到用戶的指望,那麼BDD就以用戶指望爲依據,從用戶的需求出發,強調系統行爲。具體區別能夠詳見文章The Difference Between TDD and BDD。
前端如何作單元測試?
和後端不一樣,前端有運行環境的差別性,須要考慮兼容性,如何模擬瀏覽器環境,如何支持到BOM API的調用,這些都是須要考慮的。能夠考慮如下幾種測試環境的解決方案:
運行環境 | 特色 |
jsdom | node端直接運行,僞瀏覽器環境,速度快,內置BOM對象,目前也有了對sessionStorage、localStorage和cookie的支持。 |
puppeteer | 在真實的瀏覽器中運行測試,很方便,可是運行速度會慢一點。 |
phantomjs | 無頭瀏覽器,在puppeteer發佈後,做者已經宣佈不維護了。 |
測試框架就是運行測試用例的工具,常見的有Macha、Jasmine、Jest、AVA等等。
斷言庫主要提供語義化方法,用於對參與測試的值作各類各樣的判斷。這些語義化方法會返回測試的結果,要麼成功、要麼失敗。Node內置斷言庫assert,常見的斷言庫還有有chai.js、should.js。斷言庫能夠支持不一樣的開發模式,好比chai.js就是一個BDD/TDD模式的斷言庫。
測試覆蓋率工具是用於統計測試用例對代碼的測試狀況,生成相應的報表,如Istanbul(Jest內置集成)。
Karma是一個測試平臺,能夠在多種真實瀏覽器(e.g Chrome Firefox Safari IE等等)中運行JavaScript代碼,能夠和不少測試框架集成,好比Mocha、Jasmine等等,還可使用Istanbul自動生成覆蓋率報告。
首先先看一張圖片,來理解Agile(敏捷開發)、CI(持續集成),CD(持續交付/部署)和DevOps(開發運維一體化)涵蓋的生命週期範圍。CI/CD並不等同於DevOps,它們只是DevOps的部分流程中的一種解決方案。
DevOps是Development和Operations的組合,是一種方法論,是一組過程、方法與系統的統稱,用於促進應用開發、應用運維和質量保障(QA)部門之間的溝通、協做與整合。以期打破傳統開發和運營之間的壁壘和鴻溝。
各個術語涵蓋的生命週期範圍
持續集成(Continuous Integration) 中開發人員須要頻繁地向主幹提交代碼,這些新提交的代碼在最終合併到主幹前,須要通過編譯和自動化測試(一般是單元測試)進行驗證。
CI的好處在於能夠防止分支偏離主幹過久,這種持續集成能夠實現產品快速迭代,可是因爲要頻繁集成,因此須要支持自動化構建、代碼檢查和測試,實現這些自動化流程是CI的核心。持續集成
持續交付(Continuous Delivery)指的是,頻繁地將軟件的新版本,交付給質量團隊或者用戶,以供評審。若是評審經過,代碼就進入生產階段。
CD是CI的下一步,它的目標是擁有一個可隨時部署到生產環境的代碼庫。
持續交付
持續部署是持續交付的延伸,實現自動將應用發佈到生產環境。 持續部署
公司內部經常使用的解決方案有:藍盾DevOps平臺 、orange-ci、QCI,各花入各眼,詳情能夠閱讀這篇文章CI工具哪家強。
這些CI平臺是怎樣將git倉庫中的代碼變更和自動化構建流程相關聯起來的呢?答案就是Webhook,它與異步編程中「訂閱-發佈模型」很是相似,一端觸發事件,一端監聽執行。
在web開發過程當中的Webhook,是一種經過一般的callback,去增長或者改變web page或者web app行爲的方法。這些callback能夠由第三方用戶和開發者維持當前,修改,管理,而這些使用者與網站或者應用的原始開發沒有關聯。Webhook這個詞是由Jeff Lindsay在2007年在計算機科學hook項目第一次提出的。
CI自動化構建只是應用Webhook的一個案例,Webhook的應用遠不止這些,因爲webhook使用HTTP協議,所以能夠直接被集成到web service,有時會被用來構建消息隊列服務,例如一些RESTful的例子:IronMQ和RestMS。
咱們的項目構建現狀
這是目前團隊移動端基礎庫的項目結構:主要有9個模塊,其中3個UI組件依賴框架。
基礎庫項目結構
咱們團隊在移動端基礎庫的開發中,最初採用的是IIFE模式。從嚴格意義上來講,這並非一種標準的模塊化方式,只是經過閉包實現了私有數據,將數據和行爲封裝到一個函數內部, 經過給全局對象window.M添加屬性來向外暴露接口,咱們沒法確認每一個模塊間的依賴關係,模塊合併時還要關注依賴順序。在新的方案中,咱們引入了ES6的模塊化標準來解決這個問題。
因爲業務特色,對於一些快速上線的活動頁使用Zepto庫,而對常駐頁面進行了技術升級,社交團隊使用了Preact框架,這致使基礎庫的開發有了兩個版本,分別在不一樣的代碼倉庫維護,但實際上兩者90%+的代碼都是同樣的,僅僅是三個UI組件不一樣。在基於TSW的同構直出項目中,有些基礎庫方法又要在node端執行,這個時候也是複製粘貼了一份m.js放到了該項目目錄中。在新的方案中,咱們使用差別化的構建在一份代碼倉庫中分別構建出多個版本。
對於組件的樣式,咱們是有專門的重構組進行開發維護的,他們遵循BEM規範,開發組件的時候當字符串引入:
var css ='.qui_dialog__mask{position:fixed;top:0;left:0;bottom:0;right:0;}...';appendToHead(css);
這種模式對CSS的開發維護很不友好,雖然咱們不須要關注樣式的細節,但仍是每次要把重構發給咱們的.css文件中的樣式copy出來。新方案中,咱們引入CSS module的方案。
構建工具的選擇,主要對比了Webpack四、Rollupjs和Parcel,由於基礎庫的構建文件只有js,並且從構建體積來講,rollupjs是有絕對優點的,因此選擇了rollupjs。
主流構建工具對比
因爲CSS in JS須要引入額外的依賴,在對比了CSS Module和CSS in JS後,咱們選擇CSS Module的方案。 CSS模塊化方案對比
單元測試框架咱們選擇了Jest,主要是由於開箱即用,不須要再引入斷言庫,生態也很好,較多用於React項目,並且組內的UI自動化測試系統是支持Jest的,這篇文章Migrating from Mocha to Jest中介紹了Airbnb的嘗試。
單元測試框架對比
因爲接入了CI系統進行lint自動化檢查,爲了減小「無效」的commit,咱們選擇了husky+lint-staged來進行本地代碼提交前的lint。
Lint方案
各類工做流中,首先須要在各自的開發分支進行開發測試,而後將代碼合併到追蹤生成環境的長期分支進行持續地發佈部署,這意味着對這個長期分支要有完善的自動化測試能力,由於誰也不能保證merge的代碼就必定不會有問題,目前新的方案引入了單元測試,對UI組件引入了基於puppeteer的截圖測試,但一些功能缺少在更多設備、更多平臺上的自動化驗證,所以咱們認爲在自動化測試方面的建設還不是很是完善,因此新方案接入了CI,可是對發佈外鏈基礎庫music.js這種會直接影響到全量業務的並無接入,仍是使用ARS發佈,除非緊急bug,其餘的代碼更改會在測試環境驗證一段時間(通常2-3天)後纔會發佈外網。
咱們的工程化實踐
首先能夠看一下新舊構建方案的對比,在新方案中推廣使用ES6,增長了對代碼質量的控制:代碼檢查+單元測試,並接入了CI系統。
新舊方案對比
這是咱們總體的打包方案,核心是一份源碼開發維護,經過構建工具的差別化配置實現多種版本的構建。
打包方案
這是總體的開發流程,本地開發使用package.json管理項目依賴,規範代碼格式,接入單元測試;提交以前git hook設置保證代碼檢查和測試經過後才能提交成功;使用QCI自動進行項目的構建、檢查和測試,經過後將JSDOC文檔推送到文檔服務器,併發布npm包,外鏈js仍是使用ars發佈。
開發流程
咱們選擇react-styleguide做爲UI組件開發調試工具以及文檔生成器,這是一個組件的MD文件示例:
### 組件式引入- 能夠提早插入dom結構,若是浮層中有圖片的話會先加載;- 屬性中的 `visible` 控制組件是否可見。```jsximport Button from '../../basic/Button/Button'import QMDialog from './QMDialog';
class QMDialogExample extends React.Component { constructor(props) { super(props); this.state = {visible1: false} }
render() { const {visible1} = this.state; return (
<div> <Button onClick={() => { this.setState({ visible1: true }) }}>基本使用</Button> <Button onClick={() => { this.setState({ visible2: true }) }}>帶頭圖的浮層</Button> <Button onClick={() => { this.setState({ visible3: true }) }}>傳入一個react節點</Button>
<QMDialog visible={visible1} title="QQ音樂" message="這是一段描述" btn={'我知道了'} handleTap={index => { if(index === -1) { this.setState({ visible1: false }) } else { console.log('我知道了按鈕被點擊,index=', index) } }} /> </div>
) }}<QMDialogExample />```
react-styleguide會根據組件的源碼和這個md文件生成文檔和demo,開發調試階段支持webpack配置HMR,很是方便。
demo文檔截圖
Jest能夠設置全局的Setup,會在全部test執行以前運行,也能夠設置全局Teardown,會在全部test執行完畢以後運行,好比這裏就能夠設置一些測試須要的Global對象、運行環境等等。describe能夠將測試用例進行分組,beforeEach、afterEach、beforeAll、afterAll這些方法能夠定義在測試用例以前或者以後運行的方法。
根據上面介紹的打包方案和業務特色,基礎庫須要分別運行在node端和瀏覽器端,所以須要考慮到不一樣運行環境下的測試結果。
module.exports = { clearMocks: true, coverageDirectory: "jest-coverage/coverage-music-node", preset: null, rootDir: '../../', testEnvironment: "jest-environment-jsdom-fourteen", testMatch: [ "**/tests/music-node/**/*.test.[jt]s?(x)", ], testURL: "https://y.qq.com/m/demo.html", transformIgnorePatterns: []};
node端和瀏覽器端的不一樣在於運行環境testEnvironment不一樣,jest提供jest-environment-node,咱們爲node端單獨配置了music-node.jest.config.js。
Jest支持對React App的測試,能夠採用截圖測試(Snapshot Testing)、模擬DOM操做(DOM Testing)等方法詳見文檔。在組件文檔和demo這一章節中咱們已經有了組件示例,並構建了文檔頁,能夠直接接入團隊的自動化測試系統,結合使用puppeteer進行截圖對比。
下面是對QMDialog組件的測試用例,首先準備一張基準圖片,而後寫測試流程:打開頁面——點擊按鈕觸發組件——截圖對比。screeshotDiff方法的實現參考了這篇KM文件經過puppeteer實現頁面監控,圖片diff核心算法由pixelmatch庫實現。
const iPhone = devices['iPhone 6'];await page.emulate(iPhone);
await log("進入頁面");await page.goto('http://[host]/reactui/index.html#/QMDialog', { waitUntil: 'load'});
await timeout(3000);let dom = await page.$('#QMPreload-container .rsg--preview-35 .button');
await dom.click();
await timeout(200)let diff = await screenshotDiff({ img: 'https://y.gtimg.cn/music/common/upload/t_cm3_photo_publish/1677163.png'});
if (diff > 10) { fail(); return;}
success();
這是一次測試運行結果,從左到右依次是:基準圖、測試截圖、diff結果圖,screeshotDiff根據第三張圖片返回差別點的佔比,因爲QMPreload組件的特色,加載進度受網絡影響,設置閾值爲10%,即只要差別率在10%之內就能夠認爲是正常的。
QMPreload測試結果
和上面QMPreload不一樣,對QMDialog組件的判斷則是須要差別值爲0,以下面第三張圖所示,沒有差別點。QMDialog測試結果
這是咱們參照官網的文檔接入的mock示例,這裏須要注意__mock__的目錄結構,詳見文檔。
.├── config├── src│ ├── music│ │ ├── utils│ │ │ ├── __mock__│ │ │ └── loadUrl.js│ │ └── loadUrl.js├── node_modules├── ...└── tests
loadURL方法用來動態加載js,使用jest.fn().mockImplementation對loadUrl進行mock,並mock了window.pgvMain和window.pgvSendClick。
export const loadUrl = jest.fn().mockImplementation((url, callback) => { if (/ping.js/.test(url)) { let pvCount = 0; window.pgvMain = jest.fn().mockImplementation( (p1, p2) => { expect(p1).toBe(''); expect(p2.virtualDomain).toBe('y.qq.com'); if (pvCount === 1) { expect(p2.ADTAG).toBe('all'); } pvCount++; }) window.pgvSendClick = jest.fn().mockImplementation( (p) => { expect(p.hottag).toEqual(expect.stringContaining('.android')); }); } callback();});
export default loadUrl;
由於使用了ES module的import,須要jest.mock對整個模塊進行mock。對於mock的函數才能調用toHaveBeenCalledTimes的斷言。
import tj from '../../src/music/tj';import loadUrl from '../../src/music/utils/loadUrl'
jest.mock('../../src/music/utils/loadUrl');
describe('【tj.js】點擊上報', () => { test('tj.pv tj.sendClick', () => { expect(typeof window.pgvMain).toBe('undefined'); expect(loadUrl).toHaveBeenCalledTimes(0); tj.pv(); expect(loadUrl).toHaveBeenCalledTimes(1); expect(typeof window.pgvMain).toBe('function'); expect(window.pgvMain).toHaveBeenCalledTimes(1); tj.sendClick(); tj.sendClick('tjtag.click'); window.tj_param = { ADTAG: 'all' } tj.pv(); expect(loadUrl).toHaveBeenCalledTimes(1); expect(window.pgvSendClick).toHaveBeenCalledTimes(1); });})
這是某一次的測試報告,上面有每一個模塊詳細的測試覆蓋率。爲了便於對各個模塊靈活處理,咱們將每一個函數細分拆成一個文件,以下面的src/music/type目錄下的各個文件。
測試覆蓋率-1 測試覆蓋率-2
測試覆蓋率-3
這些都是咱們經過單元測試發現的以前一些函數的bug,僅舉例一部分:
測試用例 | 錯誤輸出 | 正確輸出 |
M.type(undefined) | "nan" | "undefined" |
M.isPlainObject(Object.creact({})) | false | true |
Mozilla/5.0 (Linux; U; en-us; KFTT Build/IML74K) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.21 Safari/535.19 Silk-Accelerated=true<br />M.os.tablet | false | true |
M.param({a: 1, b: {c: 1}}) | "a=1&b=c%3D1" | "a=1&b%5Bc%5D=1" |
聲明pkg.module可讓構建工具利用到ES Moudle的不少特性來提升打包性能,好比利用Tree Shaking的機制減小文件體積,這篇文章package.json中的Module字段是幹嗎的有詳細介紹。
Tree Shaking能夠在構建的時候去除冗餘代碼,減小打包體積,但這是一個很是危險的行爲,在webpack4中,能夠在package.json中明確聲明該包/模塊是否包含sideEffects(反作用),從而指導webpack4做出正確的行爲。若是在package.json中設置了sideEffects: false,webpack4會將import {a} from 'moduleName'轉換爲import a from 'moduleName/a',從而自動修剪掉沒必要要的import,做用機制同babel-plugin-import。這個功能親測是頗有效的
對於rollupjs來講,有時候Tree Shaking並不有效,這是官網的一段解釋,大意就是靜態代碼分析很難,爲了安全rollupjs可能會沒法應用Tree Shaking,這個時候建議最好仍是明確import的PATH,這裏能夠結合適應上面的babel-plugin-import插件。Tree-Shaking Doesn't Seem to Be Working
這個插件能夠避免每個js文件分別引入膠水代碼,而是整個構建文件引入一份膠水代碼,減小代碼體積。
對eslint的錯誤輸出進行格式化,方便查看和定位問題。
因爲運行時的性能緣由,RN已經在production模式下移除了PropTypes,咱們引入這個babel插件在生產模式中移除組件屬性的類型校驗相關的代碼。
在將外鏈js用rollupjs構建成umd規範的時候,咱們設置了--noConflict,能夠解決全局變量M衝突的問題,相似於jQuery.noConflict()。
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = global || self, (function () { var current = global.M; var exports = global.M = {}; factory(exports); exports.noConflict = function () { global.M = current; return exports; }; }()));