vue 快速入門 系列 —— Vue(自身) 專案結構

彭加李發表於2022-01-16

其他章節請看:

vue 快速入門 系列

Vue(自身) 專案結構

前面我們已經陸續研究了 vue 的核心原理:資料偵測模板虛擬 DOM,都是偏底層的。本篇將和大家一起來看一下 vue 自身這個專案,瞭解它的目錄結構,以及構建過程。

vue 的目錄結構

vue 專案 下載到本地 git clone git@github.com:vuejs/vue.git vuev2.5.20

- vuev2.5.20
    - dist                      // 構建後的檔案
    - examples                  // 有幾個用 vue 寫的示例,直接是通過 <script> 方式。例如有經典的 todo,還有 markdown
    - flow                      // flow 相關。flow 是 JAVASCRIPT 的靜態型別檢查器
    - packages                  // 這 4 個包在 npm 中都能搜尋到
        - vue-server-renderer
        - vue-template-compiler
        - weex-template-compiler
        - weex-vue-framework
    - scripts                   // 構建相關的指令碼和配置檔案。還有 gitHooks
    - src
        - compiler              // 編譯器。與模板編譯相關的程式碼,例如解析器、優化器、生成器等。從 core 中分離出來或許是因為有的版本不需要它。
        - core                  // vue 的核心程式碼
            - components        // 有 keep-alive 元件
            - global-api        // 全域性 api 的程式碼。例如 Vue.set
            - instance          // vue 的建構函式和例項方法。例如 Vue.prototype.$set
            - observer          // 偵測資料變化相關程式碼
            - util              // 工具相關。例如 env.js、error.js、next-tick.js
            - vdom              // 虛擬 dom
        - platforms             // 平臺相關
            - web               
            - weex              // 阿里巴巴發起的跨平臺使用者介面開發框架
        - sfc                   // 將單檔案元件 (*.vue) 檔案解析為 SFC 描述符物件
            - parser.js
        - shared                // 公用的工具程式碼。在 vscode 中搜尋 `shared/`,可發現有 76 個檔案引用了它
            - util.js           // 工具模組
    - test                      // 測試相關
    - types                     // TypeScript 相關

Tip: flowgithooks 就不節外生枝了

構建版本

dist 目錄下有很多版本的 vue,我們需要了解一下它們的差異。

完整版:有 vue.jsvue.esm.jsvue.common.js等。

執行時版本:包含 runtime 的,例如 vue.runtime.jsvue.runtime.esm.jsvue.runtime.common.js

完整版包括執行時和編譯器,而執行時基本上就是完整版除去編譯器的其它一切。

Tip編譯器,用來將模板字串編譯成為 JavaScript 渲染函式的程式碼。在 模板 一文中已介紹。

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

// 不需要編譯器
new Vue({
  render (h) {
    return h('div', this.hi)
  }
})

UMD版本:umd 版本的檔案通過 <script> 標籤直接在瀏覽器中使用。有vue.jsvue.runtime.jsvue.min.jsvue.runtime.min.js

CommonJS 版本:包含 common 的,例如 vue.common.jsvue.runtime.common.js。主要給的打包工具使用,入 webpack 1。

ES Module 版本:包含 esm 的,例如 vue.esm.jsvue.runtime.esm.js。主要配合(或現代)的打包工具,比如 webpack 2 或 Rollup。

Tip:有關構建版本更詳細的介紹請看 官網

使用 vue 的哪個版本(import 'vue')

現代打包工具,通過 importrequire 引入 vue,使用的都是 vue.runtime.esm.js

為什麼是這樣?請看實驗。

準備一個專案,有 webpack,通過 npm 安裝 vue,最後能打包就好了。

Tip:webpack 的簡單使用可以看 初步認識 webpack

index.js 中就寫一行程式碼:

import 'vue'

然後構建生成 main.js:

test-project> npx webpack --mode development
Hash: e9412c758fa785a2fd70
Version: webpack 4.46.0
Time: 289ms
Built at: 2022/01/16 上午9:55:41
  Asset     Size  Chunks             Chunk Names
main.js  250 KiB    main  [emitted]  main
Entrypoint main = main.js
[./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {main} [built]
[./src/index.js] 12 bytes {main} [built]
    + 4 hidden modules
// main.js
...
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm.js\");\n\n\n//# sourceURL=webpack:///./src/index.js?");

我們在main.js 中發現 vue.runtime.esm.js

如果改為 require('vue'),仍然是 vue.runtime.esm.js

eval("__webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm.js\")\n\n//# sourceURL=webpack:///./src/index.js?");

如果我們刪除 vuepackage.json 的一行程式碼,再次打包:

// node_modules/vue/package.json
{
  "name": "vue",
  "version": "2.6.14",
  "description": "Reactive, component-oriented view layer for modern web interfaces.",
  "main": "dist/vue.runtime.common.js",
- "module": "dist/vue.runtime.esm.js",

會發現 main.js 中引入的變成 vue.runtime.common.js

eval("__webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.common.js\")\n\n//# sourceURL=webpack:///./src/index.js?");

:在 vue-cli 的專案中,即使只刪除 "module": "dist/vue.runtime.esm.js",,使用 vue 版本也不會變成 vue.runtime.common.js

構建分析

dist 目錄下有很多版本的 vue。每次執行 npm run build 就會重新生成一遍:

vuev2.5.20> npm run build

> vue@2.6.14 build
> node scripts/build.js

dist\vue.runtime.common.dev.js 227.52kb
dist\vue.runtime.common.prod.js 63.60kb (gzipped: 22.98kb)
dist\vue.common.dev.js 326.08kb
dist\vue.common.prod.js 91.81kb (gzipped: 33.41kb)
dist\vue.runtime.esm.js 231.45kb
dist\vue.esm.js 331.88kb
dist\vue.esm.browser.js 321.26kb
dist\vue.esm.browser.min.js 91.26kb (gzipped: 33.38kb)
dist\vue.runtime.js 242.70kb
dist\vue.runtime.min.js 63.76kb (gzipped: 23.04kb)
dist\vue.js 347.56kb
dist\vue.min.js 91.98kb (gzipped: 33.47kb)
packages\vue-template-compiler\build.js 145.59kb
packages\vue-template-compiler\browser.js 253.25kb
packages\vue-server-renderer\build.dev.js 254.91kb
packages\vue-server-renderer\build.prod.js 79.47kb (gzipped: 28.99kb)
packages\vue-server-renderer\basic.js 340.69kb
packages\vue-server-renderer\server-plugin.js 4.00kb
packages\vue-server-renderer\client-plugin.js 4.02kb

Tipnpm run build 來自 package.json,執行前需要安裝依賴 npm i

只生成 vue.runtime.esm.js

如何讓 npm run build 只生成 vue.runtime.esm.js 這一個檔案?我們先分析:

首先,執行 npm run build 就是執行 node scripts/build.js,也就是執行 vuev2.5.20/scripts/build.js 這個檔案。

如果我們將這個檔案內容替換成 console.log('i am build.js'),再次編譯,發現什麼事都不會去做,僅僅輸出 i am build.js

vuev2.5.20> npm run build    

> vue@2.6.14 build
> node scripts/build.js

i am build.js

於是我們知道應該從 build.js 入手。核心程式碼如下:

// build.js

// 現代構建工具
const rollup = require('rollup')

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

// 構建
build(builds)

裡面提到 config.jsgetAllBuilds 方法:

exports.getAllBuilds = () => Object.keys(builds).map(genConfig)

最後定位到 builds 變數:

const builds = {
  ...
  // Runtime only ES modules build (for bundlers)
  'web-runtime-esm': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.esm.js'),
    format: 'es',
    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
  },
  ...
}

修改 builds 並重新打包:

// 只保留一個
const builds = {
  // Runtime only ES modules build (for bundlers)
  'web-runtime-esm': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.esm.js'),
    format: 'es',
    banner
  }
}
vuev2.5.20> npm run build

> vue@2.6.14 build
> node scripts/build.js

dist\vue.runtime.esm.js 231.45kb

至此,每次編譯,則只會生成一個檔案。

構建 vue.runtime.esm.js 的過程

web/entry-runtime.js

從下面這段程式碼,我們猜測 vue.runtime.esm.js 的入口是 web/entry-runtime.js:

'web-runtime-esm': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.esm.js'),
    format: 'es',
    banner
  }
// vuev2.5.20/src/platforms/web/entry-runtime.js 全部內容:

/* @flow */

import Vue from './runtime/index'

export default Vue

替換 entry-runtime.js 的內容如下,重新構建:

// entry-runtime.js
const Vue = function () { }
export default Vue
vuev2.5.20> npm run build

> vue@2.6.14 build
> node scripts/build.js

dist\vue.runtime.esm.js 0.13kb
// dist/vue.runtime.esm.js 全部內容:

/*!
 * Vue.js v2.6.14
 * (c) 2014-2022 Evan You
 * Released under the MIT License.
 */
var Vue = function () { };

export default Vue;

根據打包後的內容,說明 web/entry-runtime.js 確實就是入口。

runtime/index

根據上文的分析,我們已知曉 vue.runtime.esm.js 的構建的過程就是在 runtime/index 中定義的。以下是與 Vue 相關的程式碼:

// vuev2.5.20/src/platforms/web/runtime/index.js

/* @flow */

import Vue from 'core/index'

// install platform specific utils
// 安裝平臺特定的工具
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

export default Vue

關鍵程式碼就是第一行 import Vue from 'core/index',也即是引入 vue 的核心程式碼。

core/index
// vuev2.5.20/src/core/index.js 全部程式碼:

// 返回 Vue 建構函式,並準備好例項方法
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'

// 初始化全域性 api
initGlobalAPI(Vue)

Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})

Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})

// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})

Vue.version = '__VERSION__'

export default Vue

關鍵程式碼是 instance/index(建構函式和例項方法) 和 global-api/index(全域性方法):

// vuev2.5.20/src/core/instance/index.js 全部程式碼:

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
// 建構函式
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
// 狀態相關
stateMixin(Vue)
// 事件相關
eventsMixin(Vue)
// 生命週期相關
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue
// vuev2.5.20/src/core/global-api/index.js 

/* @flow */

import config from '../config'
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'
import { observe } from 'core/observer/index'

import {
  warn,
  extend,
  nextTick,
  mergeOptions,
  defineReactive
} from '../util/index'

// 初始化全域性 api
export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  // 定義全域性 api:set、delete、nextTick...
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 explicit observable API
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }

  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)

  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)
}

其他章節請看:

vue 快速入門 系列

相關文章