1. 程式人生 > >Vue.js原始碼目錄及構建

Vue.js原始碼目錄及構建

1.Vue.js 目錄結構

下面是Github上Vue原始碼的目錄結構

其中src為原始碼部分,結構如下:

src
├── compiler        # 編譯相關 
├── core            # 核心程式碼 
├── platforms       # 不同平臺的支援
├── server          # 服務端渲染
├── sfc             # .vue 檔案解析
├── shared          # 共享程式碼

compiler

      compiler 目錄包含 Vue.js 所有編譯相關的程式碼。它包括把模板解析成

ast 語法樹,ast 語法樹優化,程式碼生成等功能。

      編譯的工作可以在構建時做(藉助 webpack、vue-loader 等輔助外掛);也可以在執行時做,使用包含構建功能的 Vue.js。顯然,編譯是一項耗效能的工作,所以更推薦前者——離線編譯。

core

       core 目錄包含了 Vue.js 的核心程式碼,包括內建元件、全域性 API 封裝,Vue 例項化、觀察者、虛擬 DOM、工具函式等等。

這裡的程式碼可謂是 Vue.js 的靈魂,也是我們之後需要重點分析的地方。

platform

       Vue.js 是一個跨平臺的 MVVM 框架,它可以跑在 web 上,也可以配合 weex 跑在 natvie 客戶端上。platform 是 Vue.js 的入口,2 個目錄代表 2 個主要入口,分別打包成執行在 web 上和 weex 上的 Vue.js。

       我們會重點分析 web 入口打包後的 Vue.js,對於 weex 入口打包的 Vue.js

server

       Vue.js 2.0 支援了服務端渲染,所有服務端渲染相關的邏輯都在這個目錄下。注意:這部分程式碼是跑在服務端的

Node.js,不要和跑在瀏覽器端的 Vue.js 混為一談。

      服務端渲染主要的工作是把元件渲染為伺服器端的 HTML 字串,將它們直接傳送到瀏覽器,最後將靜態標記"混合"為客戶端上完全互動的應用程式。

sfc

      通常我們開發 Vue.js 都會藉助 webpack 構建, 然後通過 .vue 單檔案的編寫元件。

      這個目錄下的程式碼邏輯會把 .vue 檔案內容解析成一個 JavaScript 的物件。

shared

      Vue.js 會定義一些工具方法,這裡定義的工具方法都是會被瀏覽器端的 Vue.js 和服務端的 Vue.js 所共享的。

 

2.Vue.js 原始碼構建


Vue.js 原始碼是基於 Rollup 構建的,它的構建相關配置都在 scripts 目錄下。

 

3.構建指令碼


       通常一個基於 NPM 託管的專案都會有一個 package.json 檔案,它是對專案的描述檔案,它的內容實際上是一個標準的 JSON 物件。

       我們通常會配置 script 欄位作為 NPM 的執行指令碼,Vue.js 原始碼構建的指令碼如下:

{
  "script": {
    "build": "node scripts/build.js",
    "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
    "build:weex": "npm run build --weex"
  }
}

       這裡總共有 3 條命令,作用都是構建 Vue.js,後面 2 條是在第一條命令的基礎上,新增一些環境引數。

       當在命令列執行 npm run build 的時候,實際上就會執行 node scripts/build.js,接下來我們來看看它實際是怎麼構建的。

 

4.構建過程


       我們對於構建過程分析是基於原始碼的,先開啟構建的入口 JS 檔案,在 scripts/build.js 中:

let builds = require('./config').getAllBuilds()

// filter builds via command line arg
if (process.argv[2]) {
  const filters = process.argv[2].split(',')
  builds = builds.filter(b => {
    return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
  })
} else {
  // filter out weex builds by default
  builds = builds.filter(b => {
    return b.output.file.indexOf('weex') === -1
  })
}

build(builds)

       這段程式碼邏輯非常簡單,先從配置檔案讀取配置,再通過命令列引數對構建配置做過濾,這樣就可以構建出不同用途的 Vue.js 了。接下來我們看一下配置檔案,在 scripts/config.js 中:

const builds = {
  // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
  'web-runtime-cjs': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.js'),
    format: 'cjs',
    banner
  },
  // Runtime+compiler CommonJS build (CommonJS)
  'web-full-cjs': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.common.js'),
    format: 'cjs',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime only (ES Modules). Used by bundlers that support ES Modules,
  // e.g. Rollup & Webpack 2
  'web-runtime-esm': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.esm.js'),
    format: 'es',
    banner
  },
  // Runtime+compiler CommonJS build (ES Modules)
  'web-full-esm': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.js'),
    format: 'es',
    alias: { he: './entity-decoder' },
    banner
  },
  // runtime-only build (Browser)
  'web-runtime-dev': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.js'),
    format: 'umd',
    env: 'development',
    banner
  },
  // runtime-only production build (Browser)
  'web-runtime-prod': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.min.js'),
    format: 'umd',
    env: 'production',
    banner
  },
  // Runtime+compiler development build (Browser)
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime+compiler production build  (Browser)
  'web-full-prod': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.min.js'),
    format: 'umd',
    env: 'production',
    alias: { he: './entity-decoder' },
    banner
  },
  // ...
}

       這裡列舉了一些 Vue.js 構建的配置,關於還有一些服務端渲染 webpack 外掛以及 weex 的打包配置就不列舉了。

       對於單個配置,它是遵循 Rollup 的構建規則的。其中 entry 屬性表示構建的入口 JS 檔案地址,dest 屬性表示構建後的 JS 檔案地址。format 屬性表示構建的格式,cjs 表示構建出來的檔案遵循 CommonJS 規範,es 表示構建出來的檔案遵循 ES Module 規範。 umd 表示構建出來的檔案遵循 UMD 規範。

       以 web-runtime-cjs 配置為例,它的 entry resolve('web/entry-runtime.js'),先來看一下 resolve 函式的定義。

       原始碼目錄:scripts/config.js

const aliases = require('./alias')
const resolve = p => {
  const base = p.split('/')[0]
  if (aliases[base]) {
    return path.resolve(aliases[base], p.slice(base.length + 1))
  } else {
    return path.resolve(__dirname, '../', p)
  }
}

      這裡的 resolve 函式實現非常簡單,它先把 resolve 函式傳入的引數 p 通過 / 做了分割成陣列,然後取陣列第一個元素設定為 base。在我們這個例子中,引數 p  web/entry-runtime.js,那麼 base 則為 webbase 並不是實際的路徑,它的真實路徑藉助了別名的配置,我們來看一下別名配置的程式碼,在 scripts/alias 中:

const path = require('path')

module.exports = {
  vue: path.resolve(__dirname, '../src/platforms/web/entry-runtime-with-compiler'),
  compiler: path.resolve(__dirname, '../src/compiler'),
  core: path.resolve(__dirname, '../src/core'),
  shared: path.resolve(__dirname, '../src/shared'),
  web: path.resolve(__dirname, '../src/platforms/web'),
  weex: path.resolve(__dirname, '../src/platforms/weex'),
  server: path.resolve(__dirname, '../src/server'),
  entries: path.resolve(__dirname, '../src/entries'),
  sfc: path.resolve(__dirname, '../src/sfc')
}

       很顯然,這裡 web 對應的真實的路徑是 path.resolve(__dirname, '../src/platforms/web'),這個路徑就找到了 Vue.js 原始碼的 web 目錄。然後 resolve 函式通過 path.resolve(aliases[base], p.slice(base.length + 1)) 找到了最終路徑,它就是 Vue.js 原始碼 web 目錄下的 entry-runtime.js。因此,web-runtime-cjs 配置對應的入口檔案就找到了。

       它經過 Rollup 的構建打包後,最終會在 dist 目錄下生成 vue.runtime.common.js

 

5.Runtime Only VS Runtime+Compiler


       通常我們利用 vue-cli 去初始化我們的 Vue.js 專案的時候會詢問我們用 Runtime Only 版本的還是 Runtime+Compiler 版本。下面我們來對比這兩個版本。

  • Runtime Only

       我們在使用 Runtime Only 版本的 Vue.js 的時候,通常需要藉助如 webpackvue-loader 工具把 .vue 檔案編譯成 JavaScript,因為是在編譯階段做的,所以它只包含執行時的 Vue.js 程式碼,因此程式碼體積也會更輕量。

  • Runtime+Compiler

       我們如果沒有對程式碼做預編譯,但又使用了 Vue 的 template 屬性並傳入一個字串,則需要在客戶端編譯模板,如下所示:

// 
需要編譯器的版本
new Vue({
  template: '<div>{{ hi }}</div>'
})

// 
這種情況不需要
new Vue({
  render (h) {
    return h('div', this.hi)
  }
})

       因為在 Vue.js 2.0 中,最終渲染都是通過 render 函式,如果寫 template 屬性,則需要編譯成 render 函式,那麼這個編譯過程會發生執行時,所以需要帶有編譯器的版本。

       很顯然,這個編譯過程對效能會有一定損耗,所以通常我們更推薦使用 Runtime-Only Vue.js

 

6.總結


       通過分析,我們可以瞭解到 Vue.js 的構建打包過程,也知道了不同作用和功能的 Vue.js 它們對應的入口以及最終編譯生成的 JS 檔案。儘管在實際開發過程中我們會用 Runtime Only 版本開發比較多,但為了分析 Vue 的編譯過程,我們這門課重點分析的原始碼是 Runtime+CompilerVue.js