Vue系列---源碼構建過程(四)

在瞭解源碼如何構建以前,咱們有必要了解下 項目中一個簡單的目錄結構以下:vue

|---- vue
|  |---- dist               # 打包後的存放文件目錄
|  |---- scripts            # 存放構建相關的代碼
|  |  |--- alias.js
|  |  |--- build.js
|  |  |--- config.js        # 配置文件
|  |  |--- ..... 其餘的更多
|  |---- src                # src目錄是vue核心代碼庫
|  |  |--- compiler
|  |  |--- core
|  |  |--- platforms
|  |  | |--- web            # web平臺
|  |  | | |--- compiler
|  |  | | |--- runtime
|  |  | | |--- server
|  |  | | |--- util
|  |  | | |--- entry-runtime-with-compiler.js  # 運行+模板編譯的入口文件
|  |  | |--- weex
|  |  |--- server
|  |  |--- sfc
|  |  |--- shared
|  |---- package.json

如上只是一個很是簡單的一個目錄,爲了節約篇幅,只是把入口構建的相關的目錄畫出來。node

咱們看任何庫相關的代碼的第一步先把視線轉移到 package.json 中來。而後看下 "scripts" 這個,以下:git

{
  ......
  "scripts": {
    "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev --sourcemap",
    "dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-cjs-dev",
    "dev:esm": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-esm",
    "dev:test": "karma start test/unit/karma.dev.config.js",
    "dev:ssr": "rollup -w -c scripts/config.js --environment TARGET:web-server-renderer",
    "dev:compiler": "rollup -w -c scripts/config.js --environment TARGET:web-compiler ",
    "dev:weex": "rollup -w -c scripts/config.js --environment TARGET:weex-framework",
    "dev:weex:factory": "rollup -w -c scripts/config.js --environment TARGET:weex-factory",
    "dev:weex:compiler": "rollup -w -c scripts/config.js --environment TARGET:weex-compiler ",
    "build": "node scripts/build.js",
    "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
    "build:weex": "npm run build -- weex",
    "test": "npm run lint && flow check && npm run test:types && npm run test:cover && npm run test:e2e -- --env phantomjs && npm run test:ssr && npm run test:weex",
    "test:unit": "karma start test/unit/karma.unit.config.js",
    "test:cover": "karma start test/unit/karma.cover.config.js",
    "test:e2e": "npm run build -- web-full-prod,web-server-basic-renderer && node test/e2e/runner.js",
    "test:weex": "npm run build:weex && jasmine JASMINE_CONFIG_PATH=test/weex/jasmine.js",
    "test:ssr": "npm run build:ssr && jasmine JASMINE_CONFIG_PATH=test/ssr/jasmine.js",
    "test:sauce": "npm run sauce -- 0 && npm run sauce -- 1 && npm run sauce -- 2",
    "test:types": "tsc -p ./types/test/tsconfig.json",
    "lint": "eslint src scripts test",
    "flow": "flow check",
    "sauce": "karma start test/unit/karma.sauce.config.js",
    "bench:ssr": "npm run build:ssr && node benchmarks/ssr/renderToString.js && node benchmarks/ssr/renderToStream.js",
    "release": "bash scripts/release.sh",
    "release:weex": "bash scripts/release-weex.sh",
    "release:note": "node scripts/gen-release-note.js",
    "commit": "git-cz"
  },
  .....
}

這邊咱們只要關注 "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev --sourcemap", 這塊就能夠了,其餘的命令也是相似的。如上使用的 rollup 進行打包,而後咱們會看到命令中有 scripts/config.js 這個配置文件,所以咱們須要把視線找到 這個 scripts/config.js 這個文件上來。web

scripts/config.js 代碼以下:npm

......

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)
  }
}

const builds = {
  .....
  '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
  },
  .....
};

function genConfig (name) {
  const opts = builds[name]
  const config = {
    input: opts.entry,
    external: opts.external,
    plugins: [
      flow(),
      alias(Object.assign({}, aliases, opts.alias))
    ].concat(opts.plugins || []),
    output: {
      file: opts.dest,
      format: opts.format,
      banner: opts.banner,
      name: opts.moduleName || 'Vue'
    },
    onwarn: (msg, warn) => {
      if (!/Circular/.test(msg)) {
        warn(msg)
      }
    }
  }

  .... 

  return config
}
if (process.env.TARGET) {
  module.exports = genConfig(process.env.TARGET)
} else {
  exports.getBuild = genConfig
  exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}

而後把視線移到最後的代碼,if條件判斷 process.env.TARGET 是否存在,存在的話,就執行 getConfig(process.env.TARGET) 這個函數,最後把結果導出 module.exports = genConfig(process.env.TARGET);  從命令行中,咱們能夠看到 process.env.TARGET 值爲:'web-full-dev'; 所以 const opts = builds['web-full-dev']; 所以最後 opts的值變爲以下:json

const opts = {
  entry: resolve('web/entry-runtime-with-compiler.js'),
  dest: resolve('dist/vue.js'),
  format: 'umd',
  env: 'development',
  alias: { he: './entity-decoder' },
  banner
}

再看看 resolve 函數以下:api

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 函數,首先會獲取基路徑,好比 'web/entry-runtime-with-compiler.js' 的基路徑就是 'web',所以 base = 'web'; 而後判斷 if (aliases[base]) {} aliases 是否有 key爲web的,若是有的話,直接返回:return path.resolve(aliases[base], "entry-runtime-with-compiler.js"); 同理其餘的也同樣。bash

咱們再結合下面的 alias.js 代碼weex

alias.js 代碼以下:dom

const path = require('path');

const resolve = p => path.resolve(__dirname, '../', p); // 到項目的根目錄下

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

由代碼可知:alias.js 代碼能夠理解爲以下:

module.exports = {
  vue: '項目的根目錄' + '/src/platforms/web/entry-runtime-with-compiler',
  compiler: '項目的根目錄' + '/src/compiler',
  core: '項目的根目錄' + '/src/core',
  shared: '項目的根目錄' + '/src/shared',
  web: '項目的根目錄' + '/src/platforms/web',
  weex: '項目的根目錄' + '/src/platforms/weex',
  server: '項目的根目錄' + '/src/server',
  sfc: '項目的根目錄' + '/src/sfc'
};

分析可知最後的opts對象變爲以下:

const opts = {
  entry: '項目的根目錄' + '/src/platforms/web/entry-runtime-with-compiler.js',
  dest: '項目的根目錄' + '/dist/vue.js',
  format: 'umd',
  env: 'development',
  alias: { he: './entity-decoder' },
  banner
};

所以 genConfig 函數內的config對象值變爲以下:

const config = {
  input: '項目的根目錄' + '/src/platforms/web/entry-runtime-with-compiler.js',
  external: '',
  plugins: [
    flow(),
    alias(Object.assign({}, aliases, opts.alias))
  ].concat(opts.plugins || []),
  output: {
    file: '項目的根目錄' + '/dist/vue.js',
    format: 'umd',
    banner: '',
    name: opts.moduleName || 'Vue'
  },
  onwarn: (msg, warn) => {
    if (!/Circular/.test(msg)) {
      warn(msg)
    }
  }
}; 

如上代碼打包的含義能夠理解爲以下:

找到 '項目的根目錄' + '/src/platforms/web/entry-runtime-with-compiler.js', 路徑下的js文件 打包到'項目的根目錄' + '/dist/vue.js',目錄下的 vue.js 文件。所以咱們須要把視線轉移到 '/src/platforms/web/entry-runtime-with-compiler.js' 文件內了。該文件就是咱們的vue的入口文件。

entry-runtime-with-compiler.js 基本的代碼以下:

/* @flow */

import config from 'core/config'
import { warn, cached } from 'core/util/index'
import { mark, measure } from 'core/util/perf'

import Vue from './runtime/index'
import { query } from './util/index'
import { compileToFunctions } from './compiler/index'
import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'

....

const mount = Vue.prototype.$mount;

Vue.prototype.$mount = function() {
  .....
};

....

export default Vue;

如上其餘的代碼,咱們這邊先無論,咱們先看的 import Vue from './runtime/index' 這句代碼,爲何要看這句代碼呢,那是由於 它引入了該文件,而且直接使用 export default Vue; 導出該 Vue.所以咱們會找到 src/platforms/web/runtime/index.js 代碼以下:

import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser } from 'core/util/index'

import {
  query,
  mustUseProp,
  isReservedTag,
  isReservedAttr,
  getTagNamespace,
  isUnknownElement
} from 'web/util/index'

import { patch } from './patch'
import platformDirectives from './directives/index'
import platformComponents from './components/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

.....

export default Vue;

該文件的代碼也是同樣,先引入 import Vue from 'core/index' 文件後,而後導出 export default Vue;

所以咱們繼續找到 src/core/index.js 代碼以下:

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'

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;

如上代碼,咱們主要看 import Vue from './instance/index'; 和一些全局API import { initGlobalAPI } from './global-api/index' 的代碼。

首先咱們看 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;

如上代碼,咱們終於看到Vue的構造函數了,咱們在Vue頁面初始化 new Vue({}); 這樣調用的時候,就會調用該構造函數,而咱們傳入的參數就傳給了options。該函數首先會判斷是否是正式環境 及 是否使用 new 來實列Vue。
最後會調用 this._init(options) 該函數。該函數在 src/core/instance/init.js 裏面,也就是咱們下面的initMixin(Vue) 函數調用初始化了。它會把一些方法掛載到Vue的原型上,好比 _init()方法,以下代碼:
Vue.prototype._init = function() {} 這樣的。

下面咱們繼續來看下該方法,在 src/core/instance/init.js 代碼以下:

/* @flow */

import config from '../config'
import { initProxy } from './proxy'
import { initState } from './state'
import { initRender } from './render'
import { initEvents } from './events'
import { mark, measure } from '../util/perf'
import { initLifecycle, callHook } from './lifecycle'
import { initProvide, initInjections } from './inject'
import { extend, mergeOptions, formatComponentName } from '../util/index'

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    .......
  }
}

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  .....
}

export function resolveConstructorOptions (Ctor: Class<Component>) {
  ....
}

如上就是 init.js 代碼。

initGlobalAPI

下面咱們再看下 src/core/global-api/index.js, Vue在初始化過程當中,不只給他的原型prototype上擴展方法,還會給Vue這個對象自己擴展不少全局的靜態方法。那麼擴展的全局的靜態方法和屬性就是在該函數內作的。在 src/core/global-api 其實有以下js文件

|--- vue
| |--- src
| | |--- core
| | | |--- global-api
| | | | |--- assets.js
| | | | |--- extends.js
| | | | |--- index.js
| | | | |--- mixin.js
| | | | |--- use.js

src/core/global-api/index.js 源碼能夠去看vue(v2.6.10)上去看了。在後面咱們會逐漸講解掛載了哪些全局屬性和原型方法的。

相關文章
相關標籤/搜索