github 地址: githubcss
demo實例:demohtml
起初,在公司作一些活動頁的時候,常常須要用到截圖分享的千人千面的功能,並且這種需求並不止一兩次,而是常常會出如今各類各樣的截圖場景。第一次碰到這種需求的時候,基本上都會去手擼canvas
API去作渲染功能,這種狀況的步驟大體以下:vue
dom template
標籤template
成dom
標籤dom
元素,繪製canvas
canvas
渲染圖片面臨的主要問題是複用性太差,其次是性能上也有問題,用戶看到的界面不必定和正式渲染出的界面一致,可能存在渲染差別。 由於我工做中主要使用的是vue
,對vue
核心思想也有必定研究,vue
經過vnode
實現了對不一樣端的渲染工做,那有沒有可能經過vnode
實現對canvas的渲染呢?也就是說,沒有vnode -> html -> canvas
而是直接vnode -> canvas
。 同時利用vue
的數據驅動,來達到繪製的數據驅動。想法有了,下面開始實施。node
這篇文章對此有詳細的介紹:60 FPS on the mobile web 這裏簡單的歸納一下:webpack
canvas是一種當即模式的渲染方式,不會存儲額外的渲染信息。Canvas 受益於當即模式,容許直接發送繪圖命令到 GPU。但若用它來構建用戶界面,須要進行一個更高層次的抽象。例如一些簡單的處理,好比當繪製一個異步加載的資源到一個元素上時會出現問題,如在圖片上繪製文本。在HTML中,因爲元素存在順序,以及 CSS 中存在 z-index,所以是很容易實現的。 dom渲染是一種保留模式,保留模式是一種聲明性API,用於維護繪製到其中的對象的層次結構。保留模式 API 的優勢是,對於你的應用程序,他們一般更容易構建複雜的場景,例如 DOM。一般這都會帶來性能成本,須要額外的內存來保存場景和更新場景,這可能會很慢。git
canvas 的渲染其實也是一種嘗試,既然前人以及作了充分的實踐,那麼咱們便站在巨人的肩膀上去基於vue
來實現一個數據驅動的canvas渲染。說作就作!github
熟悉Vue源碼的應該都知道,Vue經過render
函數,傳入createElement
方法來構造出一個vnode
,經過發佈--訂閱
模式來實現對數據的監聽,從新生成vnode
。咱們要作的就是在vnode
這一層開始。因此,咱們基於Vue源碼的方式,實現一個監聽函數,並混入Vue實例中:web
Vue.mixin({
// ...
created() {
if (this.$options.renderCanvas) {
// ...
// 監聽vnode中引用的變化,從新渲染
this.$watch(this.updateCanvas, this.noop)
// ...
}
},
methods: {
updateCanvas() {
// 模擬Vue render 函數
// 尋找實例中定義的 renderCanvas 方法,並傳入createElement方法
let vnode = this.$options.renderCanvas.call(this._renderProxy, this.$createElement)
}
})
複製代碼
這樣咱們就能夠愉快的在組件內部使用:canvas
renderCanvas (h) {
return h(...)
}
複製代碼
render的vnode咱們須要作額外的一些約束,好比dom
的div
應該對應canvas
裏面的什麼,dom
裏面的文本,對應canvas
裏面的什麼... 也就是說咱們能夠這樣作一些約束:性能優化
自定義標籤 | 繪製形式 | 類比dom |
---|---|---|
view/scrollView/scrollItem | rect | div |
text | text | span |
image | img | img |
其中這些元素類分別都繼承於一個Super類,而且因爲它們各有不一樣的展現方式,所以它們分別實現本身的draw方法,作定製化的展現。
繪製 canvas 佈局最基礎的寫法是爲canvas 元素傳入一系列座標點和相關的基礎寬高,這樣寫到實際項目中多是這樣的:
renderCanvas(h) {
return h('view', {
style: {
left: 10,
top: 10,
width: 100,
height: 100
}
})
}
複製代碼
這樣寫確實有點不方便維護,目前有好幾種解決方案,一種是使用css-layout
去作管理。css-layout
支持的轉換屬性以下:
這樣也只是作了一層轉換,幫咱們更好的用css思惟去寫canvas,可是若是咱們很不爽css in js
的寫法,其實咱們還能夠寫一個webpack loader 來加載外部css:
const css = require('css')
module.exports = function (source, other) {
let cssAST = css.parse(source)
let parseCss = new ParseCss(cssAST)
parseCss.parse()
this.cacheable();
this.callback(null, parseCss.declareStyle(), other);
};
class ParseCss {
constructor(cssAST) {
this.rules = cssAST.stylesheet.rules
this.targetStyle = {}
}
parse () {
this.rules.forEach((rule) => {
let selector = rule.selectors[0]
this.targetStyle[selector] = {}
rule.declarations.forEach((dec) => {
this.targetStyle[selector][dec.property] = this.formatValue(dec.value)
})
})
}
formatValue (string) {
string = string.replace(/"/g, '').replace(/'/g, '')
return string.indexOf('px') !== -1 ? parseInt(string) : string
}
declareStyle (property) {
return `window.${property || 'vStyle'} = ${JSON.stringify(this.targetStyle)}`
}
}
複製代碼
主要也就是將 css 文件轉成AST
語法樹,以後再對語法樹作轉換,轉成canvas
須要的定義形式。並以變量的形式注入到組件中。
若是咱們的元素不少,須要滾動時,咱們必須解決canvas
內部元素滾動的問題。這裏我選擇了使用Zynga Scroller 來模擬用戶滾動方法,經過他返回的滾動座標點,來對canvas進行重繪。
詳細的參考這裏
對於click,touch
等dom事件的模擬,咱們採用的方案是根據點擊區域進行檢測,並找出最底層的元素,遞歸尋找父元素並觸發對應事件處理程序,從而模擬事件冒泡。
詳細的實現能夠參考這裏
canvas繪製頁面也是一種創新的嘗試,但願這裏的研究對你有啓發,也歡迎您的PR。這裏也作了不少性能優化,限於篇幅不在贅述了,有興趣也能夠一塊兒探討。
最後:它並不意味着徹底取代基於DOM的渲染,這仍然須要文本輸入,複製/粘貼,可訪問性和SEO。 出於這些緣由,咱們可使用canvas和基於DOM的渲染的組合。