Vue.js 原始碼構建

September發表於2019-08-27

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

構建指令碼

通常一個基於 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,接下來我們來看看它實際是怎麼構建的。

構建過程

我們對於構建過程分析是基於原始碼的,先開啟構建的入口 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

Runtime Only VS Runtime+Compiler

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

· Runtime Only

我們在使用 Runtime Only 版本的 Vue.js 的時候,通常需要藉助如 webpack 的 vue-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。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
日照香爐生紫煙

相關文章