沐鳴:_Vue.js vs React vs Angular 深度對比

這個頁面無疑是最難編寫的,但我們認為它也是非常重要的。或許你曾遇到了一些問題並且已經用其他的框架解決了。你來這裏的目的是看看 Vue 是否有更好的解決方案。這也是我們在此想要回答的。

客觀來說,作為核心團隊成員,顯然我們會更偏愛 Vue,認為對於某些問題來講用 Vue 解決會更好。如果沒有這點信念,我們也就不會整天為此忙活了。但是在此,我們想盡可能地公平和準確地來描述一切。其他的框架也有顯著的優點,例如 React 龐大的生態系統,或者像是 Knockout 對瀏覽器的支持覆蓋到了 IE6。我們會嘗試着把這些內容全部列出來。

我們也希望得到的幫助,來使文檔保持最新狀態,因為 JavaScript 的世界進步的太快。如果你注意到一個不準確或似乎不太正確的地方,請提交問題讓我們知道。

React

React 和 Vue 有許多相似之處,它們都有:

  • 使用 Virtual DOM
  • 提供了響應式(Reactive)和組件化(Composable)的視圖組件。
  • 將注意力集中保持在核心庫,伴隨於此,有配套的路由和負責處理全局狀態管理的庫。

由於有着眾多的相似處,我們會用更多的時間在這一塊進行比較。這裏我們不只保證技術內容的準確性,同時也兼顧了平衡的考量。我們需要指出 React 比 Vue 更好的地方,像是他們的生態系統和豐富的自定義渲染器。

React社區為我們準確進行平衡的考量提供了非常积極地幫助,特別感謝來自 React 團隊的 Dan Abramov 。他非常慷慨的花費時間來貢獻專業知識,幫助我們完善這篇文檔,最後我們對最終結果都十分滿意。

性能簡介

到目前為止,針對現實情況的測試中,Vue 的性能是優於 React 的。如果你對此表示懷疑,請繼續閱讀。我們會解釋為什麼會這樣(並且會提供一個與 React 團隊共同約定的比較基準)。

渲染性能

在渲染用戶界面的時候,DOM 的操作成本是最高的,不幸的是沒有庫可以讓這些原始操作變得更快。
我們能做到的最好效果就是:

  1. Minimize the number of necessary DOM mutations. Both React and Vue use virtual DOM abstractions to accomplish this and both implementations work about equally well.
  2. Add as little overhead (pure JavaScript computations) as possible on top of those DOM manipulations. This is an area where Vue and React differ.

The JavaScript overhead is directly related to the mechanisms of computing the necessary DOM operations. Both Vue and React utilizes Virtual DOM to achieve that, but Vue’s Virtual DOM implementation (a fork of snabbdom) is much lighter-weight and thus introduces less overhead than React’s.

Vue 和 React 也提供功能性組件,這些組件因為都是沒有聲明,沒有實例化的,因此會花費更少的開銷。當這些都用於關鍵性能的場景時,Vue 將會更快。為了證明這點,我們建立了一個簡單的參照項目,它負責渲染 10,000 個列表項 100 次。我們鼓勵你基於此去嘗試運行一下。然而在實際上,由於瀏覽器和硬件的差異甚至 JavaScript 引擎的不同,結果都會相應有所不同。

如果你懶得去做,下面的數值是來自於一個 2014 年產的 MacBook Air 並在 Chrome 52 版本下運行所產生的。為了避免偶然性,每個參照項目都分別運行 20 次並取自最好的結果:

Vue React
Fastest 23ms 63ms
Median 42ms 81ms
Average 51ms 94ms
95th Perc. 73ms 164ms
Slowest 343ms 453ms

更新性能

In React, when a component’s state changes, it triggers the re-render of the entire component sub-tree, starting at that component as root.

To avoid unnecessary re-renders of child components, you need to implementshouldComponentUpdate everywhere and use immutable data structures. In Vue, a component’s dependencies are automatically tracked during its render, so the system knows precisely which components actually need to re-render.

這意味着,未經優化的 Vue 相比未經優化的 React 要快的多。由於 Vue 改進過渲染性能,甚至全面優化過的 React 通常也會慢於開箱即用的 Vue。

開發中

顯然,在生產環境中的性能是至關重要的,目前為止我們所具體討論的便是針對此環境。但開發過程中的表現也不容小視。不錯的是用 Vue 和 React 開發大多數應用的速度都是足夠快的。

然而,假如你要開發一個對性能要求比較高的數據可視化或者動畫的應用時,你需要了解到下面這點:在開發中,Vue 每秒最高處理 10 幀,而 React 每秒最高處理不到 1 幀。

Both Vue and React remain fast enough in development for most normal applications. However, when prototyping high frame-rate data visualizations or animations, we’ve seen cases of Vue handling 10 frames per second in development while React dropping to about 1 frame per second.

這是由於 React 有大量的檢查機制,這會讓它提供許多有用的警告和錯誤提示信息。我們同樣認為這些是很重要的,但是我們在實現這些檢查時,也更加密切地關注了性能方面。

HTML & CSS

在 React 中,它們都是 JavaScript 編寫的,聽起來這十分簡單和優雅。然而不幸的事實是,JavaScript 內的 HTML 和 CSS 會產生很多痛點。在 Vue 中我們採用 Web 技術並在其上進行擴展。接下來將通過一些實例向你展示這意味的是什麼。

JSX vs Templates

在 React 中,所有的組件的渲染功能都依靠 JSX。JSX 是使用 XML 語法編寫 Javascript 的一種語法糖。這有一個通過React社區審核過的例子:

render () { let { items } = this.props let children if ( items.length > 0 ) { children = ( <ul> {items.map( item => <li key={item.id}>{item.name} </li> )} </ul> ) } else { children = <p>No items found.</p> } return ( <div className = ‘list-container’> {children} </div> ) }

JSX 的渲染功能有下面這些優勢:

  • 你可以使用完整的編程語言 JavaScript 功能來構建你的視圖頁面。
  • 工具對 JSX 的支持相比於現有可用的其他 Vue 模板還是比較先進的(比如,linting、類型檢查、編輯器的自動完成)。

在 Vue 中,由於有時需要用這些功能,我們也提供了渲染功能 並且支持了 JSX。然而,對於大多數組件來說,渲染功能是不推薦使用了。

在這方面,我們提供的是更簡單的模板:

<template> <div class=“list-container”> <ul v-if=“items.length”> <li v-for=“item in items”> {{ item.name }} </li> </ul> <p v-else>No items found. </p> </div> </template>

優點如下:

  • 在寫模板的過程中,樣式風格已定並涉及更少的功能實現。
  • 模板總是會被聲明的。
  • 模板中任何 HTML 語法都是有效的。
  • 閱讀起來更貼合英語(比如,for each item in items)。
  • 不需要高級版本的 JavaScript 語法,來增加可讀性。

這樣,不僅開發人員更容易編寫代碼,設計人員和開發人員也可以更容易的分析代碼和貢獻代碼。

這還沒有結束。Vue 擁抱 HTML,而不是用 JavaScript 去重塑它。在模板內,Vue 也允許你用預處理器比如 Pug(原名 Jade)。

div.list-container ul(v-if=”items.length”) li(v-for=”item in items”) {{ item.name }} p(v-else) No items found.

CSS 的組件作用域

除非你把組件分佈在多個文件上(例如 CSS Modules),要不在 React 中作用域內的 CSS 就會產生警告。非常簡單的 CSS 還可以工作,但是稍微複雜點的,比如懸停狀態、媒體查詢、偽類選擇符等要麼通過沉重的依賴來重做要麼就直接不能用。

而 Vue 可以讓你在每個單文件組件中完全訪問 CSS。

<style scoped> @ media (min-width: 250px) { .list-container :hover { background: orange; } } </style>

這個可選 scoped 屬性會自動添加一個唯一的屬性(比如 data-v-21e5b78)為組件內 CSS 指定作用域,編譯的時候 .list-container:hover 會被編譯成類似.list-container[data-v-21e5b78]:hover

最後,就像 HTML 一樣,你可以選擇自己偏愛的 CSS 預處理器編寫 CSS。這可以讓你圍繞設計為中心展開工作,而不是引入專門的庫來增加你應用的體積和複雜度。

規模

向上擴展

Vue 和 React 都提供了強大的路由來應對大型應用。React 社區在狀態管理方面非常有創新精神(比如Flux、Redux),而這些狀態管理模式甚至 Redux 本身也可以非常容易的集成在 Vue 應用中。實際上,Vue 更進一步地採用了這種模式(Vuex),更加深入集成 Vue 的狀態管理解決方案 Vuex 相信能為你帶來更好的開發體驗。

兩者另一個重要差異是,Vue 的路由庫和狀態管理庫都是由官方維護支持且與核心庫同步更新的。React 則是選擇把這些問題交給社區維護,因此創建了一個更分散的生態系統。但相對的,React 的生態系統相比 Vue 更加繁榮。

最後,Vue 提供了Vue-cli 腳手架,能讓你非常容易地構建項目,包含了 Webpack,Browserify, 甚至 no build system。React 在這方面也提供了create-react-app,但是現在還存在一些局限性:

  • 它不允許在項目生成時進行任何配置,而 Vue 支持 Yeoman-like 定製。
  • 它只提供一個構建單頁面應用的單一模板,而 Vue 提供了各種用途的模板。
  • 它不能用用戶自建的模板構建項目,而自建模板對企業環境下預先建立協議是特別有用的。

而要注意的是這些限制是故意設計的,這有它的優勢。例如,如果你的項目需求非常簡單,你就不需要自定義生成過程。你能把它作為一個依賴來更新。如果閱讀更多關於不同的設計理念。

向下擴展

React 學習曲線陡峭,在你開始學 React 前,你需要知道 JSX 和 ES2015,因為許多示例用的是這些語法。你需要學習構建系統,雖然你在技術上可以用 Babel 來實時編譯代碼,但是這並不推薦用於生產環境。

就像 Vue 向上擴展好比 React 一樣,Vue 向下擴展后就類似於 jQuery。你只要把如下標籤放到頁面就可以運行:

<script src="https://unpkg.com/vue/dist/vue.js"></script>

然後你就可以編寫 Vue 代碼並應用到生產中,你只要用 min 版 Vue 文件替換掉就不用擔心其他的性能問題。

由於起步階段不需學 JSX,ES2015 以及構建系統,所以開發者只需不到一天的時間閱讀指南就可以建立簡單的應用程序。

本地渲染

ReactNative 能使你用相同的組件模型編寫有本地渲染能力的 APP(IOS 和 Android)。能同時跨多平台開發,對開發者是非常棒的。相應地,Vue 和 Weex 會進行官方合作,Weex 是阿里的跨平台用戶界面開發框架,Weex 的 JavaScript 框架運行時用的就是 Vue。這意味着在 Weex 的幫助下,你使用 Vue 語法開發的組件不僅僅可以運行在瀏覽器端,還能被用於開發 IOS 和 Android 上的原生應用。

在現在,Weex 還在积極發展,成熟度也不能和 ReactNative 相抗衡。但是,Weex 的發展是由世界上最大的电子商務企業的需求在驅動,Vue 團隊也會和 Weex 團隊积極合作確保為開發者帶來良好的開發體驗。

MobX

Mobx 在 React 社區很流行,實際上在 Vue 也採用了幾乎相同的反應系統。在有限程度上,React + Mobx 也可以被認為是更繁瑣的 Vue,所以如果你習慣組合使用它們,那麼選擇 Vue 會更合理。

Angular 1

Vue 的一些語法和 Angular 的很相似(例如 v-if vs ng-if)。因為 Angular 是 Vue 早期開發的靈感來源。然而,Augular 中存在的許多問題,在 Vue 中已經得到解決。

複雜性

在 API 與設計兩方面上 Vue.js 都比 Angular 1 簡單得多,因此你可以快速地掌握它的全部特性並投入開發。

靈活性和模塊化

Vue.js 是一個更加靈活開放的解決方案。它允許你以希望的方式組織應用程序,而不是在任何時候都必須遵循 Angular 1 制定的規則,這讓 Vue 能適用於各種項目。我們知道把決定權交給你是非常必要的。
這也就是為什麼我們提供 Webpack template,讓你可以用幾分鐘,去選擇是否啟用高級特性,比如熱模塊加載、linting、CSS 提取等等。

數據綁定

Angular 1 使用雙向綁定,Vue 在不同組件間強制使用單向數據流。這使應用中的數據流更加清晰易懂。

指令與組件

在 Vue 中指令和組件分得更清晰。指令只封裝 DOM 操作,而組件代表一個自給自足的獨立單元 —— 有自己的視圖和數據邏輯。在 Angular 中兩者有不少相混的地方。

性能

Vue 有更好的性能,並且非常非常容易優化,因為它不使用臟檢查。

在 Angular 1 中,當 watcher 越來越多時會變得越來越慢,因為作用域內的每一次變化,所有 watcher 都要重新計算。並且,如果一些 watcher 觸發另一個更新,臟檢查循環(digest cycle)可能要運行多次。Angular 用戶常常要使用深奧的技術,以解決臟檢查循環的問題。有時沒有簡單的辦法來優化有大量 watcher 的作用域。

Vue 則根本沒有這個問題,因為它使用基於依賴追蹤的觀察系統並且異步隊列更新,所有的數據變化都是獨立觸發,除非它們之間有明確的依賴關係。

有意思的是,Angular 2 和 Vue 用相似的設計解決了一些 Angular 1 中存在的問題。

Angular 2

我們單獨將 Augluar 2 作分類,因為它完全是一個全新的框架。例如:它具有優秀的組件系統,並且許多實現已經完全重寫,API 也完全改變了。

TypeScript

Angular 1 面向的是較小的應用程序,Angular 2 已轉移焦點,面向的是大型企業應用。在這一點上 TypeScript 經常會被引用,它對那些喜歡用 Java 或者 C# 等類型安全的語言的人是非常有用的。

Vue 也十分適合製作企業應用,你也可以通過使用官方類型或用戶貢獻的裝飾器來支持 TypeScript,這完全是自由可選的。

大小和性能

在性能方面,這兩個框架都非常的快。但目前尚沒有足夠的數據用例來具體展示。如果你一定要量化這些數據,你可以查看第三方參照,它表明 Vue 2 相比 Angular2 是更快的。

在大小方面,雖然 Angular 2 使用 tree-shaking 和離線編譯技術使代碼體積減小了許多。但包含編譯器和全部功能的 Vue2(23kb) 相比 Angular 2(50kb) 還是要小的多。但是要注意,用 Angular 2 的 App 的體積縮減是使用了 tree-shaking 移除了那些框架中沒有用到的功能,但隨着功能引入的不斷增多,尺寸會變得越來越大。

靈活性

Vue 相比於 Angular 2 則更加靈活,Vue 官方提供了構建工具來協助你構建項目,但它並不限制你去如何構建。有人可能喜歡用統一的方式來構建,也有很多開發者喜歡這種靈活自由的方式。

學習曲線

開始使用 Vue,你使用的是熟悉的 HTML、符合 ES5 規則的 JavaScript(也就是純 JavaScript)。有了這些基本的技能,你可以快速地掌握它(指南)並投入開發 。

Angular 2 的學習曲線是非常陡峭的。即使不包括 TypeScript,它的開始指南中所用的就有 ES2015 標準的 JavaScript,18個 NPM 依賴包,4 個文件和超過 3 千多字的介紹,這一切都是為了完成個 Hello World。而Vue’s Hello World就非常簡單。甚至我們並不用花費一整個頁面去介紹它。

Ember

Ember 是一個全能框架。它提供了大量的約定,一旦你熟悉了它們,開發會變得很高效。不過,這也意味着學習曲線較高,而且並不靈活。這意味着在框架和庫(加上一系列鬆散耦合的工具)之間做權衡選擇。後者會更自由,但是也要求你做更多架構上的決定。

也就是說,我們最好比較的是 Vue 內核和 Ember 的模板與數據模型層:

  • Vue 在普通 JavaScript 對象上建立響應,提供自動化的計算屬性。在 Ember 中需要將所有東西放在 Ember 對象內,並且手工為計算屬性聲明依賴。
  • Vue 的模板語法可以用全功能的 JavaScript 表達式,而 Handlebars 的語法和幫助函數相比來說非常受限。
  • 在性能上,Vue 甩開 Ember 幾條街,即使是 Ember 2.0 的最新 Glimmer 引擎。Vue 能夠自動批量更新,而 Ember 在關鍵性能場景時需要手動管理。

Knockout

Knockout 是 MVVM 領域內的先驅,並且追蹤依賴。它的響應系統和 Vue 也很相似。它在瀏覽器支持以及其他方面的表現也是讓人印象深刻的。它最低能支持到 IE6,而 Vue 最低只能支持到 IE9。

隨着時間的推移,Knockout 的發展已有所放緩,並且略顯有點老舊了。比如,它的組件系統缺少完備的生命周期事件方法,儘管這些在現在是非常常見的。以及相比於 Vue 調用子組件的接口它的方法顯得有點笨重。

如果你有興趣研究,你還會發現二者在接口設計的理念上是不同的。這可以通過各自創建的simple Todo List 體現出來。或許有點主觀,但是很多人認為 Vue 的 API 接口更簡單結構更優雅。

Polymer

Polymer 是另一個由谷歌贊助的項目,事實上也是 Vue 的一個靈感來源。Vue 的組件可以粗略的類比於 Polymer 的自定義元素,並且兩者具有相似的開發風格。最大的不同之處在於,Polymer 是基於最新版的 Web Components 標準之上,並且需要重量級的 polyfills 來幫助工作(性能下降),瀏覽器本身並不支持這些功能。相比而言,Vue 在支持到 IE9 的情況下並不需要依賴 polyfills 來工作,。

在 Polymer 1.0 版本中,為了彌補性能,團隊非常有限的使用數據綁定系統。例如,在 Polymer 中唯一支持的表達式只有布爾值否定和單一的方法調用,它的 computed 方法的實現也並不是很靈活。

Polymer 自定義的元素是用 HTML 文件來創建的,這會限制使用 JavaScript/CSS(和被現代瀏覽器普遍支持的語言特性)。相比之下,Vue 的單文件組件允許你非常容易的使用 ES2015 和你想用的 CSS 預編譯處理器。

在部署生產環境時,Polymer 建議使用 HTML Imports 加載所有資源。而這要求服務器和客戶端都支持 Http 2.0 協議,並且瀏覽器實現了此標準。這是否可行就取決於你的目標用戶和部署環境了。如果狀況不佳,你必須用 Vulcanizer 工具來打包 Polymer 元素。而在這方面,Vue 可以結合異步組件的特性和 Webpack 的代碼分割特性來實現懶加載(lazy-loaded)。這同時確保了對舊瀏覽器的兼容且又能更快加載。

而 Vue 和 Web Component 標準進行深層次的整合也是完全可行的,比如使用 Custom Elements、Shadow DOM 的樣式封裝。然而在我們做出嚴肅的實現承諾之前,我們目前仍在等待相關標準成熟,進而再廣泛應用於主流的瀏覽器中。

Riot

Riot 2.0 提供了一個類似於基於組件的開發模型(在 Riot 中稱之為 Tag),它提供了小巧精美的 API。Riot 和 Vue 在設計理念上可能有許多相似處。儘管相比 Riot ,Vue 要顯得重一點,Vue 還是有很多顯著優勢的:

  • 根據真實條件來渲染,Riot 根據是否有分支簡單显示或隱藏所有內容。
  • 功能更加強大的路由機制,Riot 的路由功能的 API 是極少的。
  • 更多成熟工具的支持。Vue 提供官方支持Webpack、Browserify和SystemJS,而 Riot 是依靠社區來建立集成系統。
  • 過渡效果系統。Riot 現在還沒有提供。
  • 更好的性能。Riot 儘管聲稱其使用了虛擬 DOM,但實際上用的還是臟檢查機制,因此和 Angular 1 患有相同的性能問題。

沐鳴總代_實用教程——CSS3 Flexbox 屬性可視化指南

Flexbox 布局(國內很多人稱為彈性布局)正式的全稱為 CSS Flexible Box布局模塊,它是CSS3新增的一種布局模式。它可以很方便地用來改善動態或未知大小的元素的對齊,方向和順序等等。flex容器的主要特性是它可以調整其子元素的寬度或高度去填充可用的空白區,以最優的方式達到兼容不同屏幕大小。

很多設計人員和開發人員發現使用Flexbox來布局很容易,可以使用更少的代碼,更簡單的方式實現更複雜的布局,也使整個開發過程更為簡單。Flexbox布局算法不同於那些基於垂直或水平的塊或內聯布局。Flexbox布局應該用於小的應用程序組件之中,而新的CSS 網格布局模塊正在興起,來處理大規模布局。

本指南將以可視化的方式重點介紹 flex 屬性是如何影響布局的,不是解釋 flex 屬性是如何工作。

基礎

在我們開始介紹 flexbox屬性之前,讓我們先稍微介紹一下 flexbox模型。flex布局由被稱為 flex container(flex容器) 的父容器 和被稱為 flex item(flex項)的直接子元素構成。

在上面的圖片中,您可以看到用於描述 flex container(flex容器) 及其子元素的屬性和術語。關於他們意義的更多信息,請閱讀 W3C flexbox模型 的官方文檔。

愚人碼頭注:
要是看英文比較吃力,你可以查看 《Flexbox布局(CSS3 彈性布局,Flexible Box)之 基本概念解析》 這篇文章。

從2009年的最初草案開始,flexbox布局經歷了多次迭代和幾個版本的語法變更,所以為了避免混亂,我們將只使用最新的工作草案(2014年9月)中的語法。如果你需要維護舊的瀏覽器兼容性,你可以閱讀這篇文章,了解如何以最好的方式做到這一點。

愚人碼頭注:
目前解決flexbox版本兼容性問題,一般可以使用Autoprefixer后處理程序來實現css3代碼自動補全;
如果你使用 Sass,也可以使用 @mixin (混入)來解決 flexbox版本兼容,推薦一個github項目:sass-flex-mixin。

瀏覽器對最新flexbox規範的支持情況:

  • Chrome 29+
  • Firefox 28+
  • Internet Explorer 11+
  • Opera 17+
  • Safari 6.1+ (加前綴 -webkit-)
  • Android 4.4+
  • iOS 7.1+ (加前綴 -webkit-)

如果你需要查看更多瀏覽器兼容性,你可以看這裏。

使用

使用flexbox布局只要在父級HTML元素上設置display屬性:

css 代碼:
  1. .flex-container {
  2. display: -webkit-flex;/* Safari */
  3. display: flex;
  4. }

或者如果你想將它像一個內聯元素显示使用:

css 代碼:
  1. .flex-container {
  2. display: -webkit-inline-flex;/* Safari */
  3. display: inline-flex;
  4. }

注意:這是需要你在父容器上設置的唯一屬性,並且其所有直接子元素將自動成為 flex item(flex項)。

有幾種方法可以將 flexbox 屬性進行分組,迄今為止,我發現的了解 flexbox 選項的最簡單的方法是將 flexbox 屬性分成 flex container(flex容器)和 flex item(flex項) 兩組。下面,我們將按照這兩個分組解釋每個flexbox 屬性是如何在視覺上影響布局的。

Flexbox container 屬性

flex-direction

這個屬性通過設置 flex container(flex容器) 的主軸方向,來指定 flex item(flex項) 在 flex container(flex容器) 中是如何布局的。flex item(flex項) 可以在兩個主軸方向上排列布局,水平橫向和垂直縱向兩種。

屬性值:

css 代碼:
  1. .flex-container {
  2. -webkit-flex-direction: row;/* Safari */
  3. flex-direction: row;
  4. }

值為 row 方向,那麼在ltr上下文中,所有 flex item(flex項) 從左到右排成一行。如圖:

css 代碼:
  1. .flex-container {
  2. -webkit-flex-direction: row-reverse;/* Safari */
  3. flex-direction: row-reverse;
  4. }

值為 row-reverse 方向,那麼在ltr上下文中,所有 flex item(flex項) 從右到左排成一行。( 愚人碼頭注:reverse 為相反方向,很好記憶吧)如圖:

css 代碼:
  1. .flex-container {
  2. -webkit-flex-direction: column;/* Safari */
  3. flex-direction: column;
  4. }

值為 column 方向,那麼所有 flex item(flex項) 從上到下排成一列。如圖:

css 代碼:
  1. .flex-container {
  2. -webkit-flex-direction: column-reverse;/* Safari */
  3. flex-direction: column-reverse;
  4. }

值為 column-reverse 方向,那麼所有 flex item(flex項) 從下到上排成一列。如圖:

默認值: row

注意:rowrow-reverse的排列方式取決於書寫模式,所以如果是在rtl上下文中,它們會反向排序。

flex-wrap

默認情況下,flexbox 理念是將容器中的所有子元素設置在一行中。flex-wrap屬性可以控制 flex container(flex容器) 以單行還是多行布局其flex item(flex項),以及在新行上的排列方向。

屬性值:

css 代碼:
  1. .flex-container {
  2. -webkit-flex-wrap: nowrap;/* Safari */
  3. flex-wrap: nowrap;
  4. }

flex item(flex項) 會显示成一行,默認情況下,它們會自動伸縮,以適應 flex container(flex容器)的寬度。如圖:

css 代碼:
  1. .flex-container {
  2. -webkit-flex-wrap: wrap;/* Safari */
  3. flex-wrap: wrap;
  4. }

flex item(flex項)會在flex container(flex容器)中, 按照從左到右,從上到下的順序显示成多行。如圖:

css 代碼:
  1. .flex-container {
  2. -webkit-flex-wrap: wrap-reverse;/* Safari */
  3. flex-wrap: wrap-reverse;
  4. }

flex item(flex項)會在flex container(flex容器)中, 按照從左到右,從下到上的順序显示成多行。如圖:

默認值: nowrap

注意:這些屬性取決於書寫模式,所以如果是在rtl上下文中,它們會反向排序。

flex-flow

這個屬性是設置flex-directionflex-wrap屬性的速記寫法(簡寫或縮寫)。

屬性值:

css 代碼:
  1. .flex-container {
  2. -webkit-flex-flow:||;/* Safari */
  3. flex-flow:||;
  4. }

默認值: row nowrap

justify-content

justify-content屬性用來指定 flex item(flex項)在當前 flex container(flex容器)主軸線(main axis)上的對齊方式。當所有flex item(flex項)都在同一行並且不可伸縮,或是可伸縮但已經達到它們最大尺寸的時候,它可以分配主軸線(main axis)上的剩餘空間。

屬性值:

css 代碼:
  1. .flex-container {
  2. -webkit-justify-content: flex-start;/* Safari */
  3. justify-content: flex-start;
  4. }

在ltr上下文中, flex item(flex項)會在 flex container(flex容器)中靠左對齊。如圖:

css 代碼:
  1. .flex-container {
  2. -webkit-justify-content: flex-end;/* Safari */
  3. justify-content: flex-end;
  4. }

在ltr上下文中, flex item(flex項)會在 flex container(flex容器)中靠右對齊。如圖:

css 代碼:
  1. .flex-container {
  2. -webkit-justify-content: center;/* Safari */
  3. justify-content: center;
  4. }

flex item(flex項)會在 flex container(flex容器)中靠中間對齊(居中對齊)。如圖:

css 代碼:
  1. .flex-container {
  2. -webkit-justify-content: space-between;/* Safari */
  3. justify-content: space-between;
  4. }

flex item(flex項)之間以相等的間距显示,第一個和最後一個flex item(flex項)會在 flex container(flex容器)中靠兩側邊緣對齊(愚人碼頭注:類似於常說的兩端對齊)。如圖:

css 代碼:
  1. .flex-container {
  2. -webkit-justify-content: space-around;/* Safari */
  3. justify-content: space-around;
  4. }

flex item(flex項)以相同的空間間隔显示,甚至第一個和最後一個flex item(flex項)。如圖:

默認值: flex-start

愚人碼頭注:
特別說明的是:上面都是flex container(flex容器)主軸線 橫向的例子,即flex-direction:row;。縱向的例子可以使用flex-direction:column;試試…。

align-items

flex item(flex項)可以在 flex container(flex容器)的當前交叉軸(cross axis)上對齊方式,類似於justify-content,但是在垂直方向。
此屬性設置所有flex item(flex項)的默認對齊方式,並且包括匿名元素。

愚人碼頭注:
justify-content是設置flex item(flex項)在主軸線(main axis)上的對齊方式;
align-items是設置flex item(flex項)在交叉軸(cross axis)上對齊方式;

屬性值:

css 代碼:
  1. .flex-container {
  2. -webkit-align-items: stretch;/* Safari */
  3. align-items: stretch;
  4. }

flex item(flex項)會從cross startcross end結束,填滿整個 flex container(flex容器)的高度(或寬度)。如圖:

愚人碼頭注:
這裏解釋一下填滿整個 flex container(flex容器)的寬度的情況:
當 flex-direction:column; 或者 flex-direction:column-reverse;時,將flex item(flex項)的寬度設置auto或者100%就可以了。

css 代碼:
  1. .flex-container {
  2. -webkit-align-items: flex-start;/* Safari */
  3. align-items: flex-start;
  4. }

flex item(flex項)會以flex container(flex容器) 的交叉軸起始(cross start)線對齊。如圖:

css 代碼:
  1. .flex-container {
  2. -webkit-align-items: flex-end;/* Safari */
  3. align-items: flex-end;
  4. }

flex item(flex項)會以flex container(flex容器) 的交叉軸結尾(cross end)線對齊。如圖:

css 代碼:
  1. .flex-container {
  2. -webkit-align-items: center;/* Safari */
  3. align-items: center;
  4. }

flex item(flex項)會以flex container(flex容器) 的交叉軸(cross axis)的中間對齊。如圖:

css 代碼:
  1. .flex-container {
  2. -webkit-align-items: baseline;/* Safari */
  3. align-items: baseline;
  4. }

flex item(flex項)會按照她們的文本基線(baseline)對象。

愚人碼頭注:
科普:文本基線(英語:Baseline)指的是多数字母排列的基準線。詳細說明請查看維基百科中關於Baseline的說明

默認值: stretch

注意:有關於文本基線計算的更多說明可以查看這裏。

align-content

當flex container(flex容器)的交叉軸(cross axis)上存在多餘空間(愚人碼頭注:未被填充滿)時,align-content 屬性可以控制 flex item(flex項)在 flex container(flex容器)的交叉軸(cross axis)上的對齊方式。類似於justify-content在主軸(main axis)方向上各個 flex item(flex項)的對齊。

屬性值:

css 代碼:
  1. .flex-container {
  2. -webkit-align-content: stretch;/* Safari */
  3. align-content: stretch;
  4. }
  5.  

flex item(flex項)在每行flex item(flex項)的後面都显示分佈式空間。如圖:

css 代碼:
  1. .flex-container {
  2. -webkit-align-content: flex-start;/* Safari */
  3. align-content: flex-start;
  4. }

flex item(flex項)會堆砌在flex container(flex容器)的交叉軸(cross axis)的起始位置上。如圖:

css 代碼:
  1. .flex-container {
  2. -webkit-align-content: flex-end;/* Safari */
  3. align-content: flex-end;
  4. }

flex item(flex項)會堆砌在 flex container(flex容器)的交叉軸(cross axis)的結束位置上。如圖:

css 代碼:
  1. .flex-container {
  2. -webkit-align-content: center;/* Safari */
  3. align-content: center;
  4. }

flex item(flex項)會堆砌在flex container(flex容器)的交叉軸(cross axis)的居中位置上。如圖:

css 代碼:
  1. .flex-container {
  2. -webkit-align-content: space-between;/* Safari */
  3. align-content: space-between;
  4. }

flex item(flex項)的行與行之間以相等的間隔显示,第一行和最後一行分別显示在flex container(flex容器)的交叉軸(cross axis)的兩端。如圖:

css 代碼:
  1. .flex-container {
  2. -webkit-align-content: space-around;/* Safari */
  3. align-content: space-around;
  4. }

flex item(flex項)的每一行以相等的間隔显示。如圖:

默認值: stretch

注意:這個屬性只有當 flex container(flex容器)有多行 flex item(flex項)時才生效,如果只有一行,那麼這個屬性沒有效果。

flex container(flex容器)的注意點

  • 所有的column-*屬性在flex container(flex容器)上都不會有效果
  • ::first-line 與 ::first-letter偽類在flex container(flex容器)上不會被應用。

flex item(flex項)的屬性

order

order屬性用來控制 flex container(flex容器)中 flex item(flex項)的排列順序。默認情況下,以它們被添加到 flex container(flex容器)中的順序排序(愚人碼頭注:可以理解為以HTML結構的順序排序)。

屬性值:

css 代碼:
  1. .flex-item {
  2. -webkit-order:;/* Safari */
  3. order:;
  4. }

flex item(flex項)可以使用這個簡單的屬性重新排序,而不需要重新修改HTML代碼。

默認值: 0

flex-grow

當 flex container(flex容器)存在剩餘空間時,可以用flex-grow這個屬性來確定 flex item(flex項)的拉伸比例。

屬性值:

css 代碼:
  1. .flex-item {
  2. -webkit-flex-grow:;/* Safari */
  3. flex-grow:;
  4. }

如果所有 flex item(flex項)具有相同的flex-grow值,那麼所有 flex item(flex項)在 flex container(flex容器)中具有相同的大小。

第二個 flex item(flex項) 相對於其他 flex item(flex項) 佔用更多空間。

默認值: 0

注意:負數值無效。

flex-shrink

當 flex container(flex容器)空間不足時,可以用flex-shrink這個屬性來確定 flex item(flex項)的收縮比例。

屬性值:

css 代碼:
  1. .flex-item {
  2. -webkit-flex-shrink:;/* Safari */
  3. flex-shrink:;
  4. }

默認情況下,所有 flex item(flex項)都可以收縮,但如果我們將它設置為 0(不收縮),它們將保持原來的大小。

默認值: 1

注意:負數值無效。

flex-basis

這個屬性看作widthheight屬性相同的值,並在 flex 伸縮比例分配剩餘空間之前,指定 flex item(flex項)的初始主要大小(愚人碼頭注: flex-grow 和 flex-shrink 伸縮比例分配剩餘空間之前)。

屬性值:

css 代碼:
  1. .flex-item {
  2. -webkit-flex-basis: auto |<width>;/* Safari */
  3. flex-basis: auto |<width>;
  4. }


flex-basis
指定了第4個 flex item(flex項)元素的初始大小。如圖:

默認值: 1

注意: auto 值存在一個問題,將在未來解決。

flex

這個屬性是flex-growflex-shrinkflex-basis屬性的速記寫法(簡寫或縮寫)。 除了其他值,它也可以設置為auto(即:1 1 auto)和none(即:0 0 auto)。

css 代碼:
  1. .flex-item {
  2. -webkit-flex: none | auto |[?||];/* Safari */
  3. flex: none | auto |[?||];
  4. }

默認值: 0 1 auto

注意:W3C鼓勵使用 flex 簡寫,而不是每個屬性都分開寫,因為速記正確地重置了未指定的屬性,以適應常見用途。

align-self

這個align-self屬性允許覆蓋單個的 flex item(flex項)默認對齊方式(或由align-items指定的值)。請參閱 flex container(flex容器)的align-items說明以了解可用的值。

屬性值:

css 代碼:
  1. .flex-item {
  2. -webkit-align-self: auto | flex-start | flex-end | center | baseline | stretch;/* Safari */
  3. align-self: auto | flex-start | flex-end | center | baseline | stretch;
  4. }

第3和第4個 flex item(flex項)通過align-self屬性覆蓋了其默認的對齊方式。如圖:

默認值: auto

注意:align-self 取值為auto值時,其值被計算為父級元素的align-items屬性的值,或者如果元素沒有父元素,那麼其值為stretch

flex item(flex項)的注意點

float,clearvertical-align在flex item(flex項)上都不會起作用,也不會讓它脫離文檔流。

Flexbox 演習場

這是一個flex 演習場,在這裏你可以玩完不同的flex屬性,並探索flexbox布局的力量。你可以結合多個flex屬性來獲取更加複雜的布局。

杏耀註冊平台官網_使用 Vue 2.0 實現服務端渲染的 HackerNews

Vue 2.0 支持服務端渲染 (SSR),並且是流式的,可以做組件級的緩存,這使得極速渲染成為可能。vue-hackernews-2.0 是 Vue 作者在GitHub上面放的 Vue 2.0 的一個示例項目,結合 Express、vue-router & vuex 來構建,是很好的學習案例。

 

Features

  • Server Side Rendering
    • Vue + vue-router + vuex working together
    • Server-side data pre-fetching
    • Client-side state & DOM hydration
  • Single-file Vue Components
    • Hot-reload in development
    • CSS extraction for production
  • Real-time List Updates with FLIP Animation

Architecture Overview

Build Setup

Requires Node.js 6+

# install dependencies
npm install

# serve in dev mode, with hot reload at localhost:8080
npm run dev

# build for production
npm run build

# serve in production mode
npm start

 

沐鳴註冊平台_經典文摘:餓了么的 PWA 升級實踐

很榮幸在今年 2 月到 5 月的時間里,以顧問的身份加入餓了么,參与 PWA 的相關工作。這篇文章其實最初是在以英文寫作發表在 medium 上的:Upgrading Ele.me to Progressive Web Apps,獲得了一定的關注。所以也決定改寫為中文版本再次分享出來,希望能對你有所幫助

本文首發於 CSDN 與《程序員》2017 年 7 月刊,同步發佈於 餓了么前端 – 知乎專欄、Hux Blog,轉載請保留鏈接。

自 Vue.js 官方推特第一次公開到現在,我們就一直在進行着將餓了么移動端網站升級為 Progressive Web App 的工作。直到近日在 Google I/O 2017 上登台亮相,才終於算告一段落。我們非常榮幸能夠發布全世界第一個專門面向國內用戶的 PWA,但更榮幸的是能與 Google、UC 以及騰訊合作,一起推動國內 web 與瀏覽器生態的發展。

多頁應用、Vue、PWA?

對於構建一個希望達到原生應用級別體驗的 PWA,目前社區里的主流做法都是採用 SPA,即單頁面應用模型(Single-page App)來組織整個 web 應用,業內最有名的幾個 PWA 案例 Twitter Lite、 Flipkart Lite、Housing Go 與 Polymer Shop 無一例外。

然而餓了么,與很多國內的電商網站一樣,青睞多頁面應用模型(MPA,Multi-page App)所能帶來的一些好處,也因此在一年多將移動站從基於 Angular.js 的單頁應用重構為目前的多頁應用模型。團隊最看重的優點莫過於頁面與頁面之間的隔離與解耦,這使得我們可以將每個頁面當做一個獨立的“微服務”來看待,這些服務可以被獨立迭代,獨立提供給各種第三方的入口嵌入,甚至被不同的團隊獨立維護。而整個網站則只是各種服務的集合而非一個巨大的整體。

與此同時,我們仍然依賴 Vue.js 作為 JavaScript 框架。Vue 除了是 React/Angular 這種“重型武器”的競爭對手外,其輕量與高性能的優點使得它同樣可以作為傳統多頁應用開發中流行的 “jQuery/Zepto/Kissy + 模板引擎” 技術棧的完美替代。Vue 提供的組件系統、聲明式與響應式編程更是提升了代碼組織、共享、數據流控制、渲染等各個環節的開發效率。Vue 還是一個漸進式框架,如果網站的複雜度繼續提升,我們可以按需、增量地引入 Vuex 或 Vue-Router 這些模塊。萬一哪天又要改回單頁呢?(誰知道呢……)

2017 年,PWA 已經成為 web 應用新的風潮。我們決定試試,以我們現有的“Vue + 多頁”的架構,能在升級 PWA 的道路上走多遠,達到怎樣的效果。

實現 “PRPL” 模式

“PRPL”(讀作 “purple”)是 Google 的工程師提出的一種 web 應用架構模式,它旨在利用現代 web 平台的新技術以大幅優化移動 web 的性能與體驗,對如何組織與設計高性能的 PWA 系統提供了一種高層次的抽象。我們並不準備從頭重構我們的 web 應用,不過我們可以把實現 “PRPL” 模式作為我們的遷移目標。“PRPL”實際上是 Push/Preload、Render、Precache、Lazy-Load 的縮寫,我們會在下文中展開它們的具體含義。

1. PUSH/PRELOAD,推送/預加載初始 URL 路由所需的關鍵資源。

無論是 HTTP2 Server Push 還是 <link rel="preload">,其關鍵都在於,我們希望提前請求一些隱藏在應用依賴關係(Dependency Graph)較深處的資源,以節省 HTTP 往返、瀏覽器解析文檔、或腳本執行的時間。比如說,對於一個基於路由進行 code splitting 的 SPA,如果我們可以在 webpack 清單、路由等入口代碼(entry chunks)被下載與運行之前就把初始 URL,即用戶訪問的入口 URL 路由所依賴的代碼用 Server Push 推送或 <link rel="preload"> 進行提前加載。那麼當這些資源被真正請求時,它們可能已經下載好並存在在緩存中了,這樣就加快了初始路由所有依賴的就緒。

在多頁應用中,每一個路由本來就只會請求這個路由所需要的資源,並且通常依賴也都比較扁平。餓了么移動站的大部分腳本依賴都是普通的 <script> 元素,因此他們可以在文檔解析早期就被瀏覽器的 preloader 掃描出來並且開始請求,其效果其實與顯式的 <link rel="preload"> 是一致的。

我們還將所有關鍵的靜態資源都伺服在同一域名下(不再做域名散列),以更好的利用 HTTP2 帶來的多路復用(Multiplexing)。同時,我們也在進行着對 API 進行 Server Push 的實驗。

2. RENDER,渲染初始路由,儘快讓應用可被交互

既然所有初始路由的依賴都已經就緒,我們就可以儘快開始初始路由的渲染,這有助於提升應用諸如首次渲染時間、可交互時間等指標。多頁應用並不使用基於 JavaScript 的路由,而是傳統的 HTML 跳轉機制,所以對於這一部分,多頁應用其實不用額外做什麼。

3. PRE-CACHE,用 Service Worker 預緩存剩下的路由

這一部分就需要 Service Worker 的參与了,Service Worker 是一個位於瀏覽器與網絡之間的客戶端代理,它以可攔截、處理、響應流經的 HTTP 請求,使得開發者得以從緩存中向 web 應用提供資源而聞名。不過,Service Worker 其實也可以主動發起 HTTP 請求,在“後台” 預請求與預緩存我們未來所需要的資源。

我們已經使用 Webpack 在構建過程中進行 .vue 編譯、文件名哈希等工作,於是我們編寫了一個 webpack 插件來幫助我們收集需要緩存的依賴到一個“預緩存清單”中,並使用這個清單在每次構建時生成新的 Service Worker 文件。在新的 Service Worker 被激活時,清單里的資源就會被請求與緩存,這其實與 SW-Precache 這個庫的運行機制非常接近。

實際上,我們只對我們標記為“關鍵路由”的路由進行依賴收集。你可以將這些“關鍵路由”的依賴理解為我們整個應用的 “App Shell” 或者說“安裝包”。一旦它們都被緩存,或者說成功安裝,無論用戶是在線離線,我們的 web 應用都可以從緩存中直接啟動。對於那些並不那麼重要的路由,我們則採取在運行時增量緩存的方式。我們使用的 SW-Toolbox 提供了 LRU 替換策略與 TTL 失效機制,可以保證我們的應用不會超過瀏覽器的緩存配額。

4. LAZY-LOAD 按需懶加載、懶實例化剩下的路由

懶加載與懶實例化剩下的路由對於 SPA 是一件相對麻煩點兒的事情,你需要實現基於路由的 code splitting 與異步加載。幸運的是,這又是一件不需要多頁應用擔心的事情,多頁應用中的各個路由天生就是分離的。

值得說明的是,無論單頁還是多頁應用,如果在上一步中,我們已經將這些路由的資源都預先下載與緩存好了,那麼懶加載就幾乎是瞬時完成的了,這時候我們就只需要付出實例化的代價。

這四句話即是 PRPL 的全部了。有趣的是,我們發現多頁應用在實現 PRPL 這件事甚至比單頁還要容易一些。那麼結果如何呢?

根據 Google 推出的 Web 性能分析工具 Lighthouse(v1.6),在模擬的 3G 網絡下,用戶的初次訪問(無任何緩存)大約在 2 秒左右達到“可交互”,可以說非常不錯。而對於再次訪問,由於所有資源都直接來自於 Service Worker 緩存,頁面可以在 1 秒左右就達到可交互的狀態了。

但是,故事並不是這麼簡單得就結束了。在實際的體驗中我們發現,應用在頁與頁的切換時,仍然存在着非常明顯的白屏空隙,由於 PWA 是全屏運行的,白屏對用戶體驗所帶來的負面影響甚至比以往在瀏覽器內更大。我們不是已經用 Service Worker 緩存了所有資源了嗎,怎麼還會這樣呢?

從首頁點擊到發現頁,跳轉過程中的白屏

多頁應用的陷阱:重啟開銷

與 SPA 不同,在多頁應用中,路由的切換是原生的瀏覽器文檔跳轉(Navigating across documents),這意味着之前的頁面會被完全丟棄而瀏覽器需要為下一個路由的頁面重新執行所有的啟動步驟:重新下載資源、重新解析 HTML、重新運行 JavaScript、重新解碼圖片、重新布局頁面、重新繪製……即使其中的很多步驟本是可以在多個路由之間復用的。這些工作無疑將產生巨大的計算開銷,也因此需要付出相當的時間成本。

圖中為我們的入口頁(同時也是最重的頁面)在 2 倍 CPU 節流模擬下的 profile 數據。即使我們可以將“可交互時間”控制在 1 秒左右,我們的用戶仍然會覺得這對於“僅僅切換個標籤”來說實在是太慢了。

巨大的 JavaScript 重啟開銷

根據 Profile,我們發現在首次渲染(First Paint)發生之前,大量的時間(900 毫秒)都消耗在了 JavaScript 的運行上(Evaluate Script)。幾乎所有腳本都是阻塞的(Parser-blocking),不過因為所有的 UI 都是由 JavaScript/Vue 驅動的,倒也不會有性能影響。這 900ms 中,約一半是消耗在包括 Vue 運行時、組件、庫等依賴的運行上,而另一半則花在了業務組件實例化時 Vue 的啟動與渲染上。從軟件工程角度來說,我們需要這些抽象,所以這裏並不是想責怪 JavaScript 或是 Vue 所帶來的開銷。

但是,在 SPA 中,JavaScript 的啟動成本是均攤到整個生命周期的: 每個腳本都只需要被解析與編譯一次,諸如生成 Virtual DOM 等較重的任務可以只執行一次,像 Vue 的 ViewModel 或是 Virtual DOM 這樣的大對象也可以被留在內存里復用。可惜在多頁應用里就不是這樣了,我們每次切換頁面都為 JavaScript 付出了巨大的重啟代價。

瀏覽器的緩存啊,能不能幫幫忙?

能,也不能。

V8 提供了代碼緩存(code caching),可以將編譯后的機器碼在本地拷貝一份,這樣我們就可以在下次請求同一個腳本時一次省略掉請求、解析、編譯的所有工作。而且,對於緩存在 Service Worker 配套的 Cache Storage 中的腳本,會在第一次執行后就觸發 V8 的代碼緩存,這對於我們的多頁切換能提供不少幫助。

另外一個你或許聽過的瀏覽器緩存叫做“進退緩存”,Back-Forward Cache,簡稱 bfcache。瀏覽器廠商對其的命名各異,Opera 稱之為 Fast History Navigation,Webkit 稱其為 Page Cache。但是思路都一樣,就是我們可以讓瀏覽器在跳轉時把前一頁留存在內存中,保留 JavaScript 與 DOM 的狀態,而不是全都銷毀掉。你可以隨便找個傳統的多頁網站在 iOS Safari 上試試,無論是通過瀏覽器的前進後退按鈕、手勢,還是通過超鏈接(會有一些不同),基本都可以看到瞬間加載的效果。

Bfcache 其實非常適合多頁應用。但不幸的是,Chrome 由於內存開銷與其多進程架構等原因目前並不支持。Chrome 現階段僅僅只是用了傳統的 HTTP 磁盤緩存,來稍稍簡化了一下加載過程而已。對於 Chromium 內核霸佔的 Android 生態來說,我們沒法指望了。

為“感知體驗”奮鬥

儘管多頁應用面臨着現實中的不少性能問題,我們並不想這麼快就妥協。一方面,我們嘗試盡可能減少在頁面達到可交互時間前的代碼執行量,比如減少/推遲一些依賴腳本的執行,還有減少初次渲染的 DOM 節點數以節省 Virtual DOM 的初始化開銷。另一方面,我們也意識到應用在感知體驗上還有更多的優化空間。

Chrome 產品經理 Owen 寫過一篇 Reactive Web Design: The secret to building web apps that feel amazing,談到兩種改進感知體驗的手段:一是使用骨架屏(Skeleton Screen)來實現瞬間加載;二是預先定義好元素的尺寸來保證加載的穩定。跟我們的做法可以說不謀而合。

為了消除白屏時間,我們同樣引入了尺寸穩定的骨架屏來幫助我們實現瞬間的加載與佔位。即使是在硬件很弱的設備上,我們也可以在點擊切換標籤后立刻渲染出目標路由的骨架屏,以保證 UI 是穩定、連續、有響應的。我錄了兩個視頻放在 Youtube 上,不過如果你是國內讀者,你可以直接訪問餓了么移動網站來體驗實地的效果 最終效果如下圖所示。

在添加骨架屏后,從發現頁點回首頁的效果

這效果本該很輕鬆的就能實現,不過實際上我們還費了點功夫。

在構建時使用 Vue 預渲染骨架屏

你可能已經想到了,為了讓骨架屏可以被 Service Worker 緩存,瞬間加載並獨立於 JavaScript 渲染,我們需要把組成骨架屏的 HTML 標籤、CSS 樣式與圖片資源一併內聯至各個路由的靜態 *.html 文件中。

不過,我們並不準備手動編寫這些骨架屏。你想啊,如果每次真實組件有迭代(每一個路由對我們來說都是一個 Vue 組件)我們都需要手動去同步每一個變化到骨架屏的話,那實在是太繁瑣且難以維護了。好在,骨架屏不過是當數據還未加載進來前,頁面的一個空白版本而已。如果我們能將骨架屏實現為真實組件的一個特殊狀態 —— “空狀態”的話,我們理論上就可以從真實組件中直接渲染出骨架屏來。

而 Vue 的多才多藝就在這時體現出來了,我們真的可以用 Vue.js 的服務端渲染模塊 來實現這個想法,不過不是用在真正的服務器上,而是在構建時用它把組件的空狀態預先渲染成字符串並注入到 HTML 模板中。你需要調整你的 Vue 組件代碼使得它可以在 Node 上執行,有些頁面對 DOM/BOM 的依賴一時無法輕易去除得,我們目前只好額外編寫一個 *.shell.vue 來暫時繞過這個問題。

關於瀏覽器的繪製(Painting)

HTML 文件中有標籤並不意味着這些標籤就能立刻被繪製到屏幕上,你必須保證頁面的關鍵渲染路徑是為此優化的。很多開發者相信將 script 標籤放在 body 的底部就足以保證內容能在腳本執行之前被繪製,這對於能渲染不完整 DOM 樹的瀏覽器(比如桌面瀏覽器常見的流式渲染)來說可能是成立的。但移動端的瀏覽器很可能因為考慮到較慢的硬件、電量消耗等因素並不這麼做。不僅如此,即使你曾被告知設為 async 或 defer 的腳本就不會阻塞 HTML 解析了,但這可不意味着瀏覽器就一定會在執行它們之前進行渲染。

首先我想澄清的是,根據 HTML 規範 Scripting 章節,async 腳本是在其請求完成后立刻運行的,因此它本來就可能阻塞到解析。只有 defer(且非內聯)與最新的 type=module 被指定為“一定不會阻塞解析”。(不過 defer 目前也有點小問題……我們稍後會再提到)

而更重要的是,一個不阻塞 HTML 解析的腳本仍然可能阻塞到繪製。我做了一個簡化的“最小多頁 PWA”(Minimal Multi-page PWA,或 MMPWA)來測試這個問題,:我們在一個 async(且確實不阻塞 HTML 解析)腳本中,生成並渲染 1000 個列表項,然後測試骨架屏能否在腳本執行之前渲染出來。下面是通過 USB Debugging 在我的 Nexus 5 真機上錄製的 profile:

是的,出乎意料嗎?首次渲染確實被阻塞到腳本執行結束后才發生。究其原因,如果我們在瀏覽器還未完成上一次繪製工作之前就過快得進行了 DOM 操作,我們親愛的瀏覽器就只好拋棄所有它已經完成的像素,且一直要等待到 DOM 操作引起的所有工作結束之後才能重新進行下一次渲染。而這種情況更容易在擁有較慢 CPU/GPU 的移動設備上出現。

黑魔法:利用 setTimeout() 讓繪製提前

不難發現,骨架屏的繪製與腳本執行實際是一個競態。大概是 Vue 太快了,我們的骨架屏還是有非常大的概率繪製不出來。於是我們想着如何能讓腳本執行慢點,或者說,“懶”點。於是我們想到了一個經典的 Hack: setTimeout(callback, 0)。我們試着把 MMPWA 中的 DOM 操作(渲染 1000 個列表)放進 setTimeout(callback, 0) 里……

噹噹!首次渲染瞬間就被提前了。如果你熟悉瀏覽器的事件循環模型(event loop)的話,這招 Hack 其實是通過 setTimeout 的回調把 DOM 操作放到了事件循環的任務隊列中以避免它在當前循環執行,這樣瀏覽器就得以在主線程空閑時喘息一下(更新一下渲染)了。如果你想親手試試 MMPWA 的話,你可以訪問 github.com/Huxpro/mmpwa 或 huangxuan.me/mmpwa/訪問代碼與 Demo。我把 UI 設計為了 A/B Test 的形式並改為渲染 5000 個列表項來讓效果更誇張一些。

回到餓了么 PWA 上,我們同樣試着把 new Vue() 放到了 setTimeout 中。果然,黑魔法再次顯靈,骨架屏在每次跳轉后都能立刻被渲染。這時的 Profile 看起來是這樣的:

現在,我們在 400ms 時觸發首次渲染(骨架屏),在 600ms 時完成真實 UI 的渲染並達到頁面的可交互。你可以拉上去詳細對比下優化前後 profile 的區別。

被我 “defer” 的有關 defer 的 Bug

不知道你發現沒有,在上圖的 Profile 中,我們仍然有不少腳本是阻塞了 HTML 解析的。好吧讓我解釋一下,由於歷史原因,我們確實保留了一部分的阻塞腳本,比如侵入性很強的 lib-flexible,我們沒法輕易去除它。不過,profile 里的大部分阻塞腳本實際上都設置了 defer,我們本以為他們應該在 HTML 解析完成之後才被執行,結果被 profile 打了一臉。

我和 Jake Archibald 聊了一下,果然這是 Chrome 的 Bug:defer 的腳本被完全緩存時,並沒有遵守規範等待解析結束,反而阻塞了解析與渲染。Jake 已經提交在 crbug 上了,一起給它投票吧~

最後,是優化后的 Lighthouse 跑分結果,同樣可以看到明顯的性能提升。需要說明的是,能影響 Lighthouse 跑分的因素有很多,所以我建議你以控制變量(跑分用的設備、跑分時的網絡環境等)的方式來進行對照實驗。

最後附上一張圖,這張圖當時是做給 Addy Osmani 的 I/O 演講用的,描述了餓了么 PWA 是如何結合 Vue 來實現多頁應用的 PRPL 模式,可以作為一個架構的參考與示意圖。

一些感想

多頁應用仍然有很長的路要走

Web 是一個極其多樣化的平台。從靜態的博客,到電商網站,再到桌面級的生產力軟件,它們全都是 Web 這個大家庭的第一公民。而我們組織 web 應用的方式,也同樣只會更多而不會更少:多頁、單頁、Universal JavaScript 應用、WebGL、以及可以預見的 Web Assembly。不同的技術之間沒有貴賤,但是適用場景的差距確是客觀存在的。

Jake 曾在 Chrome Dev Summit 2016 上說過 “PWA !== SPA”。可是儘管我們已經用上了一系列最新的技術(PRPL、Service Worker、App Shell……),我們仍然因為多頁應用模型本身的缺陷有着難以逾越的一些障礙。多頁應用在未來可能會有“bfcache API”、Navigation Transition 等新的規範以縮小跟 SPA 的距離,不過我們也必須承認,時至今日,多頁應用的局限性也是非常明顯的。

而 PWA 終將帶領 web 應用進入新的時代

即使我們的多頁應用在升級 PWA 的路上不如單頁的那些來得那麼閃亮,但是 PWA 背後的想法與技術卻實實在在的幫助我們在 web 平台上提供了更好的用戶體驗。

PWA 作為下一代 Web 應用模型,其嘗試解決的是 web 平台本身的根本性問題:對網絡與瀏覽器 UI 的硬依賴。因此,任何 web 應用都可以從中獲益,這與你是多頁還是單頁、面向桌面還是移動端、是用 React 還是 Vue 無關。或許,它還終將改變用戶對移動 web 的期待。現如今,誰還覺得桌面端的 web 只是個看文檔的地方呢?

還是那句老話:讓我們的用戶,也像我們這般熱愛 web 吧。

最後,感謝餓了么的王亦斯、任光輝、題恭弘=叶 恭弘,Google 的 Michael Yeung、DevRel 團隊, UC 瀏覽器團隊,騰訊 X5 瀏覽器團隊在這次項目中的合作。感謝尤雨溪、陳蒙迪和 Jake Archibald 在寫作過程中給予我的幫助。

沐鳴登錄測速_深入視角:變態的靜態資源緩存與更新

這是一個非常有趣的 非主流前端領域,這個領域要探索的是如何用工程手段解決前端開發和部署優化的綜合問題,入行到現在一直在學習和實踐中。 在我的印象中,facebook是這個領域的鼻祖,有興趣、有梯子的同學可以去看看facebook的頁面源代碼,體會一下什麼叫工程化。 接下來,我想從原理展開講述,多圖,較長,希望能有耐心看完。

讓我們返璞歸真,從原始的前端開發講起。上圖是一個“可愛”的index.html頁面和它的樣式文件a.css,用文本編輯器寫代碼,無需編譯,本地預覽,確認OK,丟到服務器,等待用戶訪問。前端就是這麼簡單,好好玩啊,門檻好低啊,分分鐘學會有木有! 然後我們訪問頁面,看到效果,再查看一下網絡請求,200!不錯,太™完美了!那麼,研發完成。。。。了么? 等等,這還沒完呢!對於大公司來說,那些變態的訪問量和性能指標,將會讓前端一點也不“好玩”。 看看那個a.css的請求吧,如果每次用戶訪問頁面都要加載,是不是很影響性能,很浪費帶寬啊,我們希望最好這樣: 利用304,讓瀏覽器使用本地緩存。但,這樣也就夠了嗎?不成!304叫協商緩存,這玩意還是要和服務器通信一次,我們的優化級別是變態級,所以必須徹底滅掉這個請求,變成這樣: 強制瀏覽器使用本地緩存(cache-control/expires),不要和服務器通信。好了,請求方面的優化已經達到變態級別,那問題來了:你都不讓瀏覽器發資源請求了,這緩存咋更新? 很好,相信有人想到了辦法:通過更新頁面中引用的資源路徑,讓瀏覽器主動放棄緩存,加載新資源。好像這樣: 下次上線,把鏈接地址改成新的版本,就更新資源了不是。OK,問題解決了么?!當然沒有!大公司的變態又來了,思考這種情況: 頁面引用了3個css,而某次上線只改了其中的a.css,如果所有鏈接都更新版本,就會導致b.css,c.css的緩存也失效,那豈不是又有浪費了?! 重新開啟變態模式,我們不難發現,要解決這種問題,必須讓url的修改與文件內容關聯,也就是說,只有文件內容變化,才會導致相應url的變更,從而實現文件級別的精確緩存控制。 什麼東西與文件內容相關呢?我們會很自然的聯想到利用 數據摘要要算法 對文件求摘要信息,摘要信息與文件內容一一對應,就有了一種可以精確到單個文件粒度的緩存控制依據了。好了,我們把url改成帶摘要信息的: 這回再有文件修改,就只更新那個文件對應的url了,想到這裏貌似很完美了。你覺得這就夠了么?大公司告訴你:圖樣圖森破! 唉~~~~,讓我喘口氣 現代互聯網企業,為了進一步提升網站性能,會把靜態資源和動態網頁分集群部署,靜態資源會被部署到CDN節點上,網頁中引用的資源也會變成對應的部署路徑: 好了,當我要更新靜態資源的時候,同時也會更新html中的引用吧,就好像這樣: 這次發布,同時改了頁面結構和樣式,也更新了靜態資源對應的url地址,現在要發布代碼上線,親愛的前端研發同學,你來告訴我,咱們是先上線頁面,還是先上線靜態資源?

  1. 先部署頁面,再部署資源:在二者部署的時間間隔內,如果有用戶訪問頁面,就會在新的頁面結構中加載舊的資源,並且把這箇舊版本的資源當做新版本緩存起來,其結果就是:用戶訪問到了一個樣式錯亂的頁面,除非手動刷新,否則在資源緩存過期之前,頁面會一直執行錯誤。
  2. 先部署資源,再部署頁面:在部署時間間隔之內,有舊版本資源本地緩存的用戶訪問網站,由於請求的頁面是舊版本的,資源引用沒有改變,瀏覽器將直接使用本地緩存,這種情況下頁面展現正常;但沒有本地緩存或者緩存過期的用戶訪問網站,就會出現舊版本頁面加載新版本資源的情況,導致頁面執行錯誤,但當頁面完成部署,這部分用戶再次訪問頁面又會恢復正常了。 好的,上面一坨分析想說的就是:先部署誰都不成!都會導致部署過程中發生頁面錯亂的問題。所以,訪問量不大的項目,可以讓研發同學苦逼一把,等到半夜偷偷上線,先上靜態資源,再部署頁面,看起來問題少一些。

但是,大公司超變態,沒有這樣的“絕對低峰期”,只有“相對低峰期”。So,為了穩定的服務,還得繼續追求極致啊! 這個奇葩問題,起源於資源的 覆蓋式發布,用 待發布資源 覆蓋 已發布資源,就有這種問題。解決它也好辦,就是實現 非覆蓋式發布。 看上圖,用文件的摘要信息來對資源文件進行重命名,把摘要信息放到資源文件發布路徑中,這樣,內容有修改的資源就變成了一個新的文件發布到線上,不會覆蓋已有的資源文件。上線過程中,先全量部署靜態資源,再灰度部署頁面,整個問題就比較完美的解決了。 所以,大公司的靜態資源優化方案,基本上要實現這麼幾個東西:

  1. 配置超長時間的本地緩存 —— 節省帶寬,提高性能
  2. 採用內容摘要作為緩存更新依據 —— 精確的緩存控制
  3. 靜態資源CDN部署 —— 優化網絡請求
  4. 更資源發布路徑實現非覆蓋式發布 —— 平滑升級

全套做下來,就是相對比較完整的靜態資源緩存控制方案了,而且,還要注意的是,靜態資源的緩存控制要求在 前端所有靜態資源加載的位置都要做這樣的處理 。是的,所有!什麼js、css自不必說,還要包括js、css文件中引用的資源路徑,由於涉及到摘要信息,引用資源的摘要信息也會引起引用文件本身的內容改變,從而形成級聯的摘要變化,大概示意圖就是: 好了,目前我們快速的學習了一下前端工程中關於靜態資源緩存要面臨的優化和部署問題,新的問題又來了:這™讓工程師怎麼寫碼啊!!! 要解釋優化與工程的結合處理思路,又會扯出一堆有關模塊化開發、資源加載、請求合併、前端框架等等的工程問題,以上只是開了個頭,解決方案才是精髓,但要說的太多太多,有空再慢慢展開吧。

總之,前端性能優化絕逼是一個工程問題!

以上不是我YY的,可以觀察 百度 或者 facebook 的頁面以及靜態資源源代碼,查看它們的資源引用路徑處理,以及網絡請中靜態資源的緩存控制部分。再次讚歎facebook的前端工程建設水平,跪舔了。 建議前端工程師多多關注前端工程領域,也許有人會覺得自己的產品很小,不用這麼變態,但很有可能說不定某天你就需要做出這樣的改變了。而且,如果我們能把事情做得更極致,為什麼不去做呢? 另外,也不要覺得這些是運維或者後端工程師要解決的問題。如果由其他角色來解決,大家總是把自己不關心的問題丟給別人,那麼前端工程師的開發過程將受到極大的限制,這種情況甚至在某些大公司都不少見! 媽媽,我再也不玩前端了。。。。5555

業界實踐

Assets Pipeline

Rails中的Assets Pipeline完成了以上所說的優化細節,對整個靜態資源的管理上的設計思考也是如此,了解rails的人也可以把此答案當做是對rails中assets pipeline設計原理的分析。 rails通過把靜態資源變成erb模板文件,然後加入<%= asset_path ‘image.png’ %>,上線前預編譯完成處理,fis的實現思路跟這個幾乎完全一樣,但我們當初確實不知道有rails的這套方案存在。 相關資料:

  • 英文版:http://guides.rubyonrails.org/asset_pipeline.html
  • 中文版:http://guides.ruby-china.org/asset_pipeline.html

FIS的解決方案

用 F.I.S 包裝了一個小工具,完整實現整個回答所說的最佳部署方案,並提供了源碼對照,可以感受一下項目源碼和部署代碼的對照。

  • 源碼項目:https://github.com/fouber/static-resource-digest-project
  • 部署項目:https://github.com/fouber/static-resource-digest-project-release

部署項目可以理解為線上發布后的結果,可以在部署項目里查看所有資源引用的md5化處理。 這個示例也可以用於和assets pipeline做比較。fis沒有assets的目錄規範約束,而且可以以獨立工具的方式組合各種前端開發語言(coffee、less、sass/scss、stylus、markdown、jade、ejs、handlebars等等你能想到的),並與其他後端開發語言結合。 assets pipeline的設計思想值得獨立成工具用於前端工程,fis就當做這樣的一個選擇吧。

沐鳴總代_npm的package.json字段含義中文文檔

簡介

本文檔有所有package.json中必要的配置。它必須是真正的json,而不是js對象。

本文檔中描述的很多行為都受npm-config(7)的影響。

默認值

npm會根據包內容設置一些默認值。

  • "scripts": {"start": "node server.js"}如果包的根目錄有server.js文件,npm會默認將start命令設置為node server.js
  • "scripts":{"preinstall": "node-waf clean || true; node-waf configure build"}如果包的根目錄有wscript文件,npm會默認將preinstall命令用node-waf進行編譯。
  • "scripts":{"preinstall": "node-gyp rebuild"}如果包的根目錄有binding.gyp文件,npm會默認將preinstall命令用node-gyp進行編譯。
  • "contributors": [...]如果包的根目錄有AUTHORS文件,npm會默認逐行按Name <email> (url)格式處理,郵箱和url是可選的。#號和空格開頭的行會被忽略。

name

在package.json中重要的就是name和version字段。他們都是必須的,如果沒有就無法install。name和version一起組成的標識在假設中是唯一的。改變包應該同時改變version。

name是這個東西的名字。注意:

  • 不要把node或者js放在名字中。因為你寫了package.json它就被假定成為了js,不過你可以用”engine”字段指定一個引擎(見後文)。
  • 這個名字會作為在URL的一部分、命令行的參數或者文件夾的名字。任何non-url-safe的字符都是不能用的。
  • 這個名字可能會作為參數被傳入require(),所以它應該比較短,但也要意義清晰。
  • 在你愛上你的名字之前,你可能要去npm registry查看一下這個名字是否已經被使用了。http://registry.npmjs.org/

version

在package.json中重要的就是name和version字段。他們都是必須的,如果沒有就無法install。name和version一起組成的標識在假設中是唯一的。改變包應該同時改變version。

version必須能被node-semver解析,它被包在npm的依賴中。(要自己用可以執行npm install semver

可用的“数字”或者“範圍”見semver(7).

description

放簡介,字符串。方便屌絲們在npm search中搜索。

keywords

關鍵字,數組、字符串。還是方便屌絲們在npm search中搜索。

homepage

項目官網的url。

注意:這和“url”一樣。如果你放一個“url”字段,registry會以為是一個跳轉到你發布在其他地方的地址,然後喊你滾粗。

嗯,滾粗,沒開玩笑。

bugs

你項目的提交問題的url和(或)郵件地址。這對遇到問題的屌絲很有幫助。

差不多長這樣:

{ "url" : "http://github.com/owner/project/issues"
, "email" : "project@hostname.com"
}

你可以指定一個或者指定兩個。如果你只想提供一個url,那就不用對象了,字符串就行。

如果提供了url,它會被npm bugs命令使用。

license

你應該要指定一個許可證,讓人知道使用的權利和限制的。

最簡單的方法是,假如你用一個像BSD或者MIT這樣通用的許可證,就只需要指定一個許可證的名字,像這樣:

{ "license" : "BSD" }

如果你又更複雜的許可條件,或者想要提供給更多地細節,可以這樣:

"licenses" : [
  { "type" : "MyLicense"
  , "url" : "http://github.com/owner/project/path/to/license"
  }
]

在根目錄中提供一個許可證文件也蠻好的。

people fields: author, contributors

author是一個人。contributors是一堆人的數組。person是一個有name字段,可選的有url、email字段的對象,像這樣:

{ "name" : "Barney Rubble"
, "email" : "b@rubble.com"
, "url" : "http://barnyrubble.tumblr.com/"
}

或者可以把所有的東西都放到一個字符串里,npm會給你解析:

"Barney Rubble <b@rubble.com> (http://barnyrubble.tumblr.com/)

email和url在兩種形式中都是可選的。

也可以在你的npm用戶信息中設置一個頂級的maintainers字段。

files

files是一個包含項目中的文件的數組。如果命名了一個文件夾,那也會包含文件夾中的文件。(除非被其他條件忽略了)

你也可以提供一個.npmignore文件,讓即使被包含在files字段中得文件被留下。其實就像.gitignore一樣。

main

main字段配置一個文件名指向模塊的入口程序。如果你包的名字叫foo,然後用戶require("foo"),main配置的模塊的exports對象會被返回。

這應該是一個相對於根目錄的文件路徑。

對於大多數模塊,它是非常有意義的,其他的都沒啥。

bin

很多包都有一個或多個可執行的文件希望被放到PATH中。npm讓媽媽再也不用擔心了(實際上,就是這個功能讓npm可執行的)。

要用這個功能,給package.json中的bin字段一個命令名到文件位置的map。初始化的時候npm會將他鏈接到prefix/bin(全局初始化)或者./node_modules/.bin/(本地初始化)。

比如,npm有:

{ "bin" : { "npm" : "./cli.js" } }

所以,當你初始化npm,它會創建一個符號鏈接到cli.js腳本到/usr/local/bin/npm

如果你只有一個可執行文件,並且名字和包名一樣。那麼你可以只用一個字符串,比如:

{ "name": "my-program"
, "version": "1.2.5"
, "bin": "./path/to/program" }

結果和這個一樣:

{ "name": "my-program"
, "version": "1.2.5"
, "bin" : { "my-program" : "./path/to/program" } }

man

指定一個單一的文件或者一個文件數組供man程序使用。

如果只提供一個單一的文件,那麼它初始化后就是man <pkgname>的結果,而不管實際的文件名是神馬,比如:

{ "name" : "foo"
, "version" : "1.2.3"
, "description" : "A packaged foo fooer for fooing foos"
, "main" : "foo.js"
, "man" : "./man/doc.1"
}

這樣man foo就可以用到./man/doc.1文件了。

如果文件名不是以包名開頭,那麼它會被冠以前綴,下面的:

{ "name" : "foo"
, "version" : "1.2.3"
, "description" : "A packaged foo fooer for fooing foos"
, "main" : "foo.js"
, "man" : [ "./man/foo.1", "./man/bar.1" ]
}

會為man fooman foo-bar創建文件。

man文件需要以数字結束,然後可選地壓縮后以.gz為後綴。The number dictates which man section the file is installed into.

{ "name" : "foo"
, "version" : "1.2.3"
, "description" : "A packaged foo fooer for fooing foos"
, "main" : "foo.js"
, "man" : [ "./man/foo.1", "./man/foo.2" ]
}

會為man fooman 2 foo創建。

directories

CommonJS Packages規範說明了幾種方式讓你可以用directorieshash標示出包得結構。如果看一下npm’s package.json,你會看到有directories標示出doc, lib, and man。

在未來,這個信息可能會被用到。

directories.lib

告訴屌絲們你的庫文件夾在哪裡。目前沒有什麼特別的東西需要用到lib文件夾,但確實是重要的元信息。

directories.bin

如果你指定一個“bin”目錄,然後在那個文件夾中得所有文件都會被當做”bin”字段使用。

如果你已經指定了“bin”字段,那這個就無效。

directories.man

一個放滿man頁面的文件夾。貼心地創建一個“man”字段。
A folder that is full of man pages. Sugar to generate a “man” array by
walking the folder.

directories.doc

將markdown文件放在這裏。最後,這些會被很好地展示出來,也許,某一天。
Put markdown files in here. Eventually, these will be displayed nicely,
maybe, someday.

directories.example

將事例腳本放在這裏。某一天,它可能會以聰明的方式展示出來。

repository

指定你的代碼存放的地方。這個對希望貢獻的人有幫助。如果git倉庫在github上,那麼npm docs命令能找到你。

這樣做:

"repository" :
  { "type" : "git"
  , "url" : "http://github.com/isaacs/npm.git"
  }

"repository" :
  { "type" : "svn"
  , "url" : "http://v8.googlecode.com/svn/trunk/"
  }

URL應該是公開的(即便是只讀的)能直接被未經過修改的版本控製程序處理的url。不應該是一個html的項目頁面。因為它是給計算機看的。

scripts

“scripts”是一個由腳本命令組成的hash對象,他們在包不同的生命周期中被執行。key是生命周期事件,value是要運行的命令。

參見 npm-scripts(7)

config

“config” hash可以用來配置用於包腳本中的跨版本參數。在實例中,如果一個包有下面的配置:

{ "name" : "foo"
, "config" : { "port" : "8080" } }

然後有一個“start”命令引用了npm_package_config_port環境變量,用戶可以通過npm config set foo:port 8001來重寫他。

參見 npm-config(7) 和 npm-scripts(7)。

dependencies

依賴是給一組包名指定版本範圍的一個hash。這個版本範圍是一個由一個或多個空格分隔的字符串。依賴還可以用tarball或者git URL。

請不要將測試或過渡性的依賴放在dependencieshash中。見下文的devDependencies

詳見semver(7).

  • version 必須完全和version一致
  • >version 必須比version
  • >=version 同上
  • <version 同上
  • <=version 同上
  • ~version 大約一樣,見semver(7)
  • 1.2.x 1.2.0, 1.2.1, 等,但不包括1.3.0
  • http://... 見下文’依賴URL’
  • * 所有
  • "" 空,同*
  • version1 - version2 同 >=version1 <=version2.
  • range1 || range2 二選一。
  • git... 見下文’依賴Git URL’
  • user/repo 見下文’GitHub URLs’

比如下面都是合法的:

{ "dependencies" :
  { "foo" : "1.0.0 - 2.9999.9999"
  , "bar" : ">=1.0.2 <2.1.2"
  , "baz" : ">1.0.2 <=2.3.4"
  , "boo" : "2.0.1"
  , "qux" : "<1.0.0 || >=2.3.1 <2.4.5 || >=2.5.2 <3.0.0"
  , "asd" : "http://asdf.com/asdf.tar.gz"
  , "til" : "~1.2"
  , "elf" : "~1.2.3"
  , "two" : "2.x"
  , "thr" : "3.3.x"
  }
}

依賴URL

可以指定一個tarball URL,這個tarball將在包被初始化的時候下載並初始化。

依賴Git URL

Git urls 可以是下面幾種形式:

git://github.com/user/project.git#commit-ish
git+ssh://user@hostname:project.git#commit-ish
git+ssh://user@hostname/project.git#commit-ish
git+http://user@hostname/project/blah.git#commit-ish
git+https://user@hostname/project/blah.git#commit-ish

commit-ish是可以被git checkout的任何tag、sha或者branch。默認為master

GitHub URLs

1.1.65版后,你可以僅僅用“user/foo-project”引用GitHub urls,比如:

{
  "name": "foo",
  "version": "0.0.0",
  "dependencies": {
    "express": "visionmedia/express"
  }
}

devDependencies

如果有人要使用你的模塊,那麼他們可能不需要你開發使用的外部測試或者文檔框架。

在這種情況下,最好將這些附屬的項目列在devDependencies中。

這些東西會在執行npm link或者npm install的時候初始化,並可以像其他npm配置參數一樣管理。詳見npm-config(7)。

對於非特定平台的構建步驟,比如需要編譯CoffeeScript,可以用prepublish腳本去實現,並把它依賴的包放在devDependency中。(譯者注:prepublish定義了在執行npm publish的時候先行執行的腳本)

比如:

{ "name": "ethopia-waza",
  "description": "a delightfully fruity coffee varietal",
  "version": "1.2.3",
  "devDependencies": {
    "coffee-script": "~1.6.3"
  },
  "scripts": {
    "prepublish": "coffee -o lib/ -c src/waza.coffee"
  },
  "main": "lib/waza.js"
}

prepublish腳本會在publishing前運行,這樣用戶就不用自己去require來編譯就能使用。並且在開發模式中(比如本地運行npm install)會運行這個腳本以便更好地測試。

peerDependencies

在一些場景中,如在一個host中不必須進行require時候,你想表現你的package與一個host工具或者庫的兼容關鍵。這一般用來引用插件。尤其是你的模塊可能要暴露一個特定的接口,並由host文檔來預期和指定。

比如:

{
  "name": "tea-latte",
  "version": "1.3.5"
  "peerDependencies": {
    "tea": "2.x"
  }
}

這能保證你的package可以只和tea的2.x版本一起初始化。npm install tea-latte可能會產生下面的依賴關係

├── tea-latte@1.3.5
└── tea@2.2.0

試圖初始化另一個有會衝突的依賴的插件將導致一個錯誤。因此,確保你的插件的需求約束越弱越好,而不要去把它鎖定到一個特定的版本。

假設這個host遵守semver規範,只改變這個package的主版本會打破你的插件。因此,如果你在package中用過每個1.x版本,就用”^1.0″或者”1.x”來表示。如果你依賴於功能介紹1.5.2,用”>= 1.5.2 < 2″。

bundledDependencies

一組包名,他們會在發布的時候被打包進去。

拼成"bundleDependencies"(缺d)也可以。

optionalDependencies

如果一個依賴可用,但你希望在它安裝錯誤的時候npm也能繼續初始化,那麼你可以把它放在optionalDependencies hash中。這是一個包名到版本或者url的map,就像dependencies hash一樣。只是它運行錯誤。

處理缺乏依賴也是你的程序的責任。比如像這樣:

try {
  var foo = require('foo')
  var fooVersion = require('foo/package.json').version
} catch (er) {
  foo = null
}
if ( notGoodFooVersion(fooVersion) ) {
  foo = null
}

// .. then later in your program ..

if (foo) {
  foo.doFooThings()
}

optionalDependencies會覆蓋dependencies中同名的項,所以通常比只放在一個地方好。

engines

你可以指定工作的node的版本:

{ "engines" : { "node" : ">=0.10.3 <0.12" } }

並且,像dependensies一樣,如果你不指定版本或者指定“*”作為版本,那麼所有版本的node都可以。

如果指定一個“engines”字段,那麼npm會需要node在裏面,如果“engines”被省略,npm會假定它在node上工作。

你也可以用“engines”字段來指定哪一個npm版本能更好地初始化你的程序,如:

{ "engines" : { "npm" : "~1.0.20" } }

記住,除非用戶設置engine-strict標記,這個字段只是建議值。

engineStrict

如果你確定你的模塊一定不會運行在你指定版本之外的node或者npm上,你可以在package.json文件中設置"engineStrict":true。它會重寫用戶的engine-strict設置。

除非你非常非常確定,否則不要這樣做。如果你的engines hash過度地限制,很可能輕易讓自己陷入窘境。慎重地考慮這個選擇。如果大家濫用它,它會再以後的npm版本中被刪除。

os

你可以指定你的模塊要運行在哪些操作系統中:

"os" : [ "darwin", "linux" ]

你也可以用黑名單代替白名單,在名字前面加上“!”就可以了:

"os" : [ "!win32" ]

操作系統用process.platform來探測。

雖然沒有很好地理由,但它是同時支持黑名單和白名單的。

cpu

如果你的代碼只能運行在特定的cpu架構下,你可以指定一個:

"cpu" : [ "x64", "ia32" ]

就像os選項,你也可以黑一個架構:

"cpu" : [ "!arm", "!mips" ]

cpu架構用process.arch探測。

preferGlobal

如果包主要是需要全局安裝的命令行程序,就設置它為true來提供一個warning給只在局部安裝的人。

它不會真正的防止用戶在局部安裝,但如果它沒有按預期工作它會幫助防止產生誤會。

private

如果你設置"private": true,npm就不會發布它。

這是一個防止意外發布私有庫的方式。如果你要確定給定的包是只發布在特定registry(如內部registry)的,用publishConfighash的描述來重寫registry的publish-time配置參數。

publishConfig

這是一個在publish-time使用的配置集合。當你想設置tag或者registry的時候它非常有用,所以你可以確定一個給定的包沒有打上“lastest”的tag或者被默認發布到全局的公開registry。

任何配置都可以被重寫,但當然可能只有“tag”和“registry”與發布的意圖有關。

參見npm-config(7)有可以被重寫的列表。

沐鳴測速登錄地址_12月份30個輕量的 JavaScript 庫和插件

下面這個列表中的免費 JavaScript 插件都是今年發布的,沒有臃腫的一體化的框架,它們提供輕量級的解決方案,幫助 Web 開發過程更容易和更快。提供的插件可以創建滑塊、響應式菜單、模態窗口、相冊和許多其他常見的組件。

baguetteBox.js

baguetteBox.js is a simple and easy to use responsive image lightbox script with swipe gesture support on mobile devices. it has been written in pure JavaScript.

ScrollReveal

The ScrollReveal plugin makes it super-easy to create web page scroll animations for both desktop and mobile browsers.

Marginotes

Marginotes takes your jQuery selection and adds notes to the margin with the text provided in HTML attributes. If jQuery is not your thing, there’s also a version available without it.

Loud Links

Loud Links is a lightweight JavaScript library for adding interaction sounds to your website. It does this by creating an HTML5 audio element and using it to play MP3 or OGG audio files.

Bricks.js

Bricks.js is a ‘blazing fast’ masonry layout generator for fixed width elements.

MediumEditor

Written using vanilla JavaScript, MediumEditor is a lightweight (28kb) Medium.com WYSIWYG inline editor toolbar clone. There are also a selection of Mediumditor extensions and themes available.

Philter

Available as either a jQuery plugin or as vanilla JavaScript, the Philter gives you the means to control CSS filters with HTML attributes.

SuperEmbed.js

SuperEmbed.js is a JavaScript ibrary that detects embedded videos on webpages and makes them responsive.

Substance

Substance is a JavaScript library for web-based content editing. It gives you all of the tools you need for creating custom text editors and web-based publishing systems.

List.js

List.js is a lightweight and fast vanilla JavaScript script that adds search, sort, filters and flexibility to lists, tables, or anything HTML.

jqGifPreview

jqGifPreview is a simple jQuery plugin for creating GIF previews just like you see on Facebook.

Datedropper.js

Datedropper.js is a jQuery plugin that gives you a an easy way to manage dates for input fields.

jfMagnify

jfMagnify is a jQuery plugin that creates a magnify glass effect on any HTML element, not just images.

jQuery formBuilder

jQuery formBuilder is a new jQuery plugin for quick, drag & drop form creation.

Popper.js

Popper.js is a lightweight (4kb minified) library for managing poppers, tooltips and popovers. You can quickly and easily position tooltips with just a single line code.

Image Blur Plugin

Image Blur Plugin is a lightweight cross-browser jQuery plugin for blurring images.

InlineTweet.js

InlineTweet.js allows you to easily create tweetable links out of any text on a webpage. All you have to do is wrap the tweetable text in a container with data-inline-tweet.

iMissYou.js

iMissYou.js is a handy little jQuery plugin for changing the title & favicon of the page when the user moves away from your website.

SweetAlert2

SweetAlert2 is a beautiful and customizable replacement for JavaScript’s popup boxes.

Turntable.js

Turntable.js is a responsive jQuery slider that will flip through a selection of images as your mouse moves across a container.

Force.js

Force.js is a JavaScript library that makes it simple to animate HTML elements and navigate around a web page.

Push.js

Push.js is a cross-browser solution for getting up and running with Javascript desktop notifications.

Bideo.js

Bideo.js is a JavaScript library that makes it very easy to add fullscreen background videos to web pages.

Microlight.js

Microlight.js is a lightweight code highlighting library, for any programming language, that greatly improves readability.

Algolia Places

Algolia Places is an easy way to use an address search autocomplete JavaScript library on your website.

flatpickr

Written in vanilla JavaScript, flatpickr is lightweight datetimepicker and calendar solution.

Slidebars

Slidebars is a jQuery Framework for adding off-canvas menus and sidebars to your website or web application.

anime.js

anime.js is a flexible and lightweight JavaScript animation library. It works with CSS, Individual Transforms, SVG, DOM attributes and JS Objects.

Cleave.js

Cleave.js – Format your <input/> content when you are typing.

Skippr

Skippr is a super simple and lightweight slideshow plugin for jQuery

iziModal.js

iziModal.js is an elegant, responsive, flexible and lightweight modal jQuery plugin.

Lightgallery.js

Lightgallery.js is a fully featured JavaScript lightbox gallery with no dependencies.

 

沐鳴怎麼當代理?_用 Vue.js 模仿一個簡單的網易雲音樂

前言:自己學習VUEJS也一段時間,但一直沒有做出來一東西。我自己一直喜歡用網易雲音樂app,於是乎就做了這個app。

技術棧

  • vue全家桶 (vue vue-router vuex)
  • axios
  • Muse-UI(一個基於Vue2.x的material design 風格UI框架)

功能與思路分析

我之前學習JS的時候對Html5 audio研究過,也寫過一些例子,那時的功能並不是很全面。在寫這個程序之前,我好好的查閱了當前的HTML5中的audio標籤,發現園子上一位園友總結的很不錯(這裏)。於是就先把網易雲音樂最基本的功能實現,歌單部分(這也是我喜歡網易雲音樂的原因這一),然後實現音樂的上一曲、下一曲,播放、暫停。列表功能。

後台

後台採用.net做為後台提供系統請求所用的API(源碼),原理很簡單就是用.net偽裝成一個客戶端去訪問網易雲音樂的API然後,把返回的json數據轉發出來。同時服務端做下跨域處理。

核心代碼:

/// <summary>
/// 請求網易雲音樂接口
/// </summary>
/// <typeparam name="T">要請求的接口類型</typeparam>
/// <param name="config">要請求的接口類型的對象</param>
/// <returns>請求結果(JSON)</returns>
public static string Request<T>(T config) where T : RequestData, new()
{
    // 請求URL
    string requestURL = config.Url;
    // 將數據包對象轉換成QueryString形式的字符串
    string @params = config.FormData.ParseQueryString();
    bool isPost = config.Method.Equals("post", StringComparison.CurrentCultureIgnoreCase);

    if (!isPost)
    {
        // get方式 拼接請求url
        string sep = requestURL.Contains('?') ? "&" : "?";
        requestURL += sep + @params;
    }

    HttpWebRequest req = (HttpWebRequest)WebRequest.Create(requestURL);
    req.Accept = "*/*";
    req.Headers.Add("Accept-Language", "zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4");
    // 如果服務端啟用了GZIP,那麼下面必須解壓,否則一直亂碼。
    // 參見:http://www.crifan.com/set_accept_encoding_header_to_gzip_deflate_return_messy_code/
    req.Headers.Add("Accept-Encoding", "gzip,deflate,sdch");
    req.ContentType = "application/x-www-form-urlencoded";
    req.KeepAlive = true;
    req.Host = "music.163.com";
    req.Referer = "http://music.163.com/search/";
    req.UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537";            
    // 設置cookies
    req.Headers["Cookie"] = "appver=1.5.2";
    req.Method = config.Method;
    req.AutomaticDecompression = DecompressionMethods.GZip;
    if (isPost)
    {
        // 寫入post請求包
        byte[] formData = Encoding.UTF8.GetBytes(@params);
        // 設置HTTP請求頭 參考:https://github.com/darknessomi/musicbox/blob/master/NEMbox/api.py 
        req.GetRequestStream().Write(formData, 0, formData.Length);
    }            
    // 發送http請求 並讀取響應內容返回
    return new StreamReader(req.GetResponse().GetResponseStream(), Encoding.GetEncoding("UTF-8")).ReadToEnd();
}

vuejs部分

項目結構

├── index.html
├── main.js
├── api
│   └── ... # 抽取出API請求
├── components
│   ├── playBar.vue
│   └── ...
└── store
│    └── index.js        # 整個項目的vuex部分
└── router
│   └── router.js        # 整個項目的路由
└── utils                # 一些工具類模塊
│
└── views                # 項目中的一些route-view

說項目的路由之前,先來看一張效果圖

對於整個項目來說:視圖區別在於頂部導航,下面的bar的是否出來取決於,當前系統列表中是否有歌曲,如果有就會出現。

router.js核心部分

const router = new VueRouter({
  mode: 'history',
  routes: [{
    path: '/index',
    component: require('../views/index'),
    children: [
      {
        path: 'rage',
        component: require('../views/rage')
      },
      {
        path: 'songList',
        component: require('../views/songList')
      },
      {
        path: 'leaderBoard',
        component: require('../views/leaderBoard')
      },
      {
        path: 'hotSinger',
        component: require('../views/hotSinger')
      }
    ]
  }, {
    name: 'playerDetail',
    path: '/playerDetail/:id',
    component: require('../views/playerDetail')
  }, {
    path: '/playListDetail/:id',
    name: 'playListDetail',
    component: require('../views/playListDetail')
  }, {
    path: '*', redirect: '/index/rage'
  }],
  // 讓每個頁面都滾動到頂部,改變模式為mode: history
  scrollBehavior (to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    } else {
      return { x: 0, y: 0 }
    }
  }
})

vuex部分

這部分,主要是歌曲這一塊,因為不同的頁面有不同的使用到了歌曲信息,把把這部分數據放到vuex中做統一的數據處理!sotre/index.js

const store = new Vuex.Store({
  state: {
    audio: {
      'id': 0,
      'name': '歌曲名稱',
      'singer': '演唱者',
      'albumPic': '/static/player-bar.png',
      'location': '',
      'album': ''
    },
    lyric: '正在加載中。。',
    currentIndex: 0, // 當前播放的歌曲位置
    playing: false, // 是否正在播放
    loading: false, // 是否正在加載中
    showDetail: false,
    songList: [],    // 播放列表
    currentTime: 0,
    tmpCurrentTime: 0,
    durationTime: 0,
    bufferedTime: 0,
    change: false   // 判斷是更改的時間還是播放的時間
  },
  getters: {
    audio: state => state.audio,
    playing: state => state.playing,
    loading: state => state.loading,
    showDetail: state => state.showDetail,
    durationTime: state => state.durationTime,
    currentIndex: state => state.currentIndex,
    bufferedTime: state => state.bufferedTime,
    tmpCurrentTime: state => state.tmpCurrentTime,
    songList: state => state.songList,
    change: state => state.change,
    currentTime: state => state.currentTime,
    prCurrentTime: state => {
      return state.currentTime / state.durationTime * 100
    },
    prBufferedTime: state => {
      return state.bufferedTime / state.durationTime * 100
    }
  },
  mutations: {
    play (state) {
      state.playing = true
    },
    pause (state) {
      state.playing = false
    },
    toggleDetail (state) {
      state.showDetail = !state.showDetail
    },
    setAudio (state) {
      state.audio = state.songList[state.currentIndex - 1]
    },
    setAudioIndex (state, index) {
      state.audio = state.songList[index]
      state.currentIndex = index + 1
    },
    removeAudio (state, index) {
      state.songList.splice(index, 1)
      state.audio = state.songList[index - 1]
      state.currentIndex = state.currentIndex - 1
      if (state.songList.length === 0) {
        state.audio = {
          'id': 0,
          'name': '歌曲名稱',
          'singer': '演唱者',
          'albumPic': '/static/player-bar.png',
          'location': '',
          'album': ''
        }
        state.playing = false
      }
    },
    setChange (state, flag) {
      state.change = flag
    },
    setLocation (state, location) {
      state.audio.location = location
    },
    updateCurrentTime (state, time) {
      state.currentTime = time
    },
    updateDurationTime (state, time) {
      state.durationTime = time
    },
    updateBufferedTime (state, time) {
      state.bufferedTime = time
    },
    changeTime (state, time) {
      state.tmpCurrentTime = time
    },
    openLoading (state) {
      state.loading = true
    },
    closeLoading (state) {
      state.loading = false
    },
    resetAudio (state) {
      state.currentTime = 0
    },
    playNext (state) { // 播放下一曲
      state.currentIndex++
      if (state.currentIndex > state.songList.length) {
        state.currentIndex = 1
      }
      state.audio = state.songList[state.currentIndex - 1]
    },
    playPrev (state) { // 播放上一曲
      state.currentIndex--
      if (state.currentIndex < 1) {
        state.currentIndex = state.songList.length
      }
      state.audio = state.songList[state.currentIndex - 1]
    },
    addToList (state, item) {
      var flag = false
      state.songList.forEach(function (element, index) { // 檢測歌曲重複
        if (element.id === item.id) {
          flag = true
          state.currentIndex = index + 1
        }
      })
      if (!flag) {
        state.songList.push(item)
        state.currentIndex = state.songList.length
      }
    },
    setLrc (state, lrc) {
      state.lyric = lrc
    }
  },
  // 異步的數據操作
  actions: {
    getSong ({commit, state}, id) {
      commit('openLoading')
      Axios.get(api.getSong(id)).then(res => {
        // 統一數據模型,方便後台接口的改變
        var url = res.data.data[0].url
        commit('setAudio')
        commit('setLocation', url)
      })
    },
    getLrc ({commit, state}, id) {
      commit('setLrc', '[txt](加載中。。。')
      Axios.get(api.getLrc(id)).then(res => {
        // 1、先判斷是否有歌詞
        if (res.data.nolyric) {
          commit('setLrc', '[txt](⊙0⊙) 暫無歌詞')
        } else {
          console.log(res.data.lrc.lyric)
          commit('setLrc', res.data.lrc.lyric)
        }
      })
    }
  }
})

最後上點項目截圖

github項目地址:https://github.com/javaSwing/NeteaseCloudWebApp

目前只完成app歌單部分,也是最核心的部分。這個項目會一直更新!如果覺的不錯就給個star吧

沐鳴娛樂_Bulma – 基於 Flexbox 的現代化的 CSS 框架

Bulma 是一個基於 Flexbox 的現代化的 CSS 框架,設計的初衷就是移動優先(Mobile First),模塊化設計,可以輕鬆用來實現各種簡單或者複製的內容布局,瀏覽器支持:瀏覽器支持:Chrome、Edge、Firefox、Internet Explorer (10+)、Opera 以及 Safari。

在線演示      免費下載

沐鳴網址_WebSlides – 製作漂亮的 HTML 幻燈片

WebSlides 是一個開源的 HTML 幻燈片項目,能夠幫助熟悉前端語言的開發者快速製作出效果精美的幻燈片。頁面中的每個 <section> 都是一個獨立的幻燈片,只需要很少的 CSS 代碼裝飾即可。趕緊來體驗一下吧。

在線演示      免費下載