回到最初:開發不須要「編譯」 的 WebApp

目前開發 WebApp 最流行的方式就是使用 React, Vue, Webpack 或者相似的工具,他們解決的最大的問題是組件式開發。但使用他們帶來很大的開發成本:他們更新速度很快,你須要不斷的學習他們,並且靈活度也受到了框架的限制。那有沒有一件法寶讓咱們學了就能夠一直使用,專一於開發應用而沒必要關心工具呢?下面我使用最簡單的方式就像最初咱們沒用這些工具同樣來開發一款現代 WebApp。html

這個 WebApp 我如今稱他爲「MT Music Player」(GitHub 地址),他是一個簡單的單頁應用,作的事情也至關簡單,就是上傳音頻並播放他們。麻雀雖小,但五臟俱全:因爲有自定義列表,因此須要具有路由的功能;因爲多處 UI 須要響應同一個狀態,因此須要具有全局數據管理;最主要的是,他使用組件式開發,方便協做維護。能夠點擊這裏體驗。node

桌面端

模塊化

主流瀏覽器都已經支持 ES6 的所有特性(尾遞歸優化除外),包括 ES Modules,Classes,我能夠直接使用這些特性而沒必要經過 Webpack 來編譯,而後使用 HTTP2 直接加載他們。git

組件化

React, Vue 都提供一套完整的組件化方案。如今,Web Components 在除了 Edge 外其餘主流瀏覽器中都已經獲得支持,<template> 元素提供了可重複使用的模版,Shadow DOM 提供了組件的界限。github

React 使用 JSX 來編寫模版,優勢是直觀,可編程。如今有了 ES6 的模版字符串,我可使用模版字符串來編寫 HTML 代碼,計算後將其解析成 DOM 插入或者替代文檔中的某個元素便可。編程

// `variable` 更新後從新調用
template = `<span>${variable}</span>`
document.body.innerHTML = template
複製代碼

但實際上並不能這麼作,由於實例化模版插入文檔後還有須要更新他,這個方式將更新整個組件,性能太差。React 使用 Virtual DOM 的方式來更新組件,他計算出整個組件的 Virtual DOM 表示並進行 Diff 獲得須要更新的部分 DOM 以後再更新他們,須要更新的 DOM 一般只佔整個組件的一小部分,因此這個的更新方式相比上面的方式要快得多。瀏覽器

想象一下,若是組件中的某個數據變化須要更新組件中的某個 DOM ,咱們不使用 Virtual DOM ,不使用 Diff ,而是直接獲得這個 DOM 直接進行更新,這樣不是沒有了性能問題嗎?只須要把 Node 和數據綁定就能夠作到這一點。app

回到 ES 的模版字符串,他可使用一個標籤函數來計算最終字符串,如今能夠利用他來進行 Node 和數據綁定:替換模版字符串中的變量後解析到 <template> 元素中,再使用 DOM 相關 API 查詢到對應的 Node。下面是一段拙劣的代碼:框架

const tempsMap = new Map

const html = (strings, ...values) => {
    // 注:同一個模版字符串 strings 相同
    let result = tempsMap.get(strings)
    if (!result) {
        const temp = document.createElement('template')
        temp.innerHTML = strings.reduce((p, c, i) => {
            return p + '<!---->' + `{{placeholder-${i}}}` + '<!---->' + c
        })
        tempsMap.set(strings, {strings, values, temp})
    }
    return {...tempsMap.get(strings), values}
}

const instances = new Map

const render = (result, container) => {
    let instance = instances.get(container)
    if (instance) {
        // 更新
        instance.setValue(result)
    } else {
        // 首次渲染
        const instance = result.temp.content.cloneNode(true)
        container.append(instance)

        const nodes = result.values.map((v, i) => {
            const xpr = `//node()[contains(text(),'{{placeholder-${i + 1}}}')]/text()`
            return document.evaluate(xpr, document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null).snapshotItem(0)
        })
        instance.setValue = (result) => {
            result.values.forEach((value, i) => {
                nodes[i].data = value
            })
        }
        instance.setValue(result)
        instances.set(container, instance)
    }
}

// 重複調用就能夠更新組件
setInterval(() => {
    render(html`<span>${Date.now()}</span>`, document.body)
}, 1000)
複製代碼

基於這個思想有一個很完善的實現—— lit-html點擊這裏查看一個典型的組件。jsp

全局數據管理

不論是 React 仍是 Vue 都有一個數據管理的庫,他們有個共同點是數據綁定到視圖,當數據更新時,能馬上反應到視圖上。React Redux 經過 React 的 props 來更新視圖,我想訂閱的方式可能更適合上面提到的組件式方案:組件訂閱一個數據對象,當這個數據對象更新時通知組件更新。Proxy 很容易作到這一點:ide

const handles = new Map()

const handler = {
    get(target, key) {
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      const listeners = handles.get(key)
      listeners.forEach(/* 更新組件 */)
      return true
    },
};

// 全局數據對象
const store = new Proxy({
    appState: {}
}, handler)

export const connect = (page, func) => {
  const listeners = handles.get(page)
  if (!func.connectedPage) func.connectedPage = new Set()
  func.connectedPage.add(page)
  listeners.add(func)
}
複製代碼

當咱們實例化組件時,組件經過 connect 訂閱一份數據如 appState,當從新賦值 store.appState 時,就能夠調用訂閱該數據的組件,執行回調函數,通過包裝後,最後是這樣使用的:

export default class AppState extends Component {
  constructor() {
    super();
    this.state = store.appState
    this.clickHandle = () => this.setState({date: new Date})
  }

  render() {
    return html` <span @click="${this.clickHandle}">${this.state.date}</span> `
  }
}

customElements.define('app-state', AppState)
複製代碼

路由

一個完整的應用離不開路由,他讓應用易於傳播,即 Native App 所說的深度連接,另外支持路由外,也間接的支持了 Android 的返回鍵。History API 能很快的爲單頁應用建立路由功能:

window.addEventListener('popstate', () => {
        // 由用戶代理更新歷史棧時觸發,如點擊後退/前進鍵
    })
    
    // 更新歷史棧
    window.history.pushState(state, title, pathname);
複製代碼

另外,將歷史棧對象與全局數據管理結合,能夠保證歷史棧修改的適合更新全局數據 store ,以更新訂閱該數據的組件(完整代碼),如 <app-route>

總結

解決上面三個問題後,就能夠開發現代 WebApp 了。但還存在不少問題:

  • 模塊不少時,HTTP2 加載並無想象中的快
  • 沒法進行服務端渲染,可能須要單獨生成一份 SEO 友好的文檔
  • 樣式不能穿透 ShadowDOM,可能須要寫不少重複的樣式
  • 依賴管理比較棘手,目前 jspm 是較好的一個方案

當使用這種不須要「編譯」的方式開發 WebApp 時,喪失了一些目前經常使用的開發工具:

  • 熱更新
  • 類型系統支持

最後,我仍是以爲應該用如今的開發方案結合 Web Components,由於 Web Components 特別適合那種獨立封閉的組件,如視頻播放器 —— <video>

相關文章
相關標籤/搜索