chenfaxiang / chenfaxiang.github.io

Issues、学习记录。
https://www.chenfx.com.cn
5 stars 1 forks source link

2 - 源码构建、Vue.js 中的 Vue 定义 #2

Open chenfaxiang opened 6 years ago

chenfaxiang commented 6 years ago

源码构建

Vue.js 的源码是通过 Rollup 构建,然而,在第一部分学习目录结构学习时有一个 scripts 文件夹,此文件夹下面存放的都是构建相关的配置文件。

构建命令

基于 NPM 托管的项目都会有一个 package.json 文件,在使用 node 的构建过程中都会调用 package.json 中的 script 配置项作为执行脚本,而 Vue.js 在构建的过程中调用的脚本文件如下3条配置:

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

在当前目录的命令行中运行: npm run build 实际执行后面的 node 命令进行构建 npm run build:ssr 实际执行后面的 node 命令进行服务端构建 npm run build:weex 实际执行后面的 node 命令进行移动端平台的代码构建

构建过程

// filter builds via command line arg // process.argv 返回一个数组,获取命令行参数 // process.argv 第一个元素为 process.execPath(它返回启动node.js进程的可执行文件所在绝对路径) // process.argv 第二个元素为当前执行的 js 文件绝对路径 // process.argv 剩下的其他元素为命令行后跟的参数 if (process.argv[2]) { // 当前命令行存在参数 const filters = process.argv[2].split(',') builds = builds.filter(b => { // some 会遍历 builds 的所有数据,当表达式返回 true ,剩余的不会再执行;否则返回 false // 满足下面条件的 builds 数据才会组成新的数组 return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1) }) } else { // filter out weex builds by default // 当前命令行没有参数 // filter 返回一个新数组 builds = builds.filter((b, index) => { // config 文件中处理后的 builds 中是否是存在 weex(移动端) 路径 // 将路径中不存在 weex 的数据返回成一个新数组 return b.output.file.indexOf('weex') === -1 }) }

build(builds)

这段代码是先从配置文件中读取配置信息,再根据不同的命令行参数进行构建配置过滤,构建出不同平台用途的 Vue.js;下面看一下构建配置文件 scripts/config.js,它里面的构建配置项(篇幅太长,只列举两个例子):
``` javascript
const builds = {
  // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
  'web-runtime-cjs': {
    // entry: 找到 src/platforms/web下的入口文件 entry-runtime.js
    entry: resolve('web/entry-runtime.js'),
    // dest: dist 在 alias.js(别名配置) 中没有配置,则走 resolve 函数 else 条件,直接重新在文件夹 dis 下生成当前命名文件
    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
  },
}

这里包含了基本构建、服务端渲染 webpack 插件及 weex 的打包配置,基于单个配置,它是遵循 Rollup 构建规则的,其中 entry 表示构建的入口文件地址,dest 表示构建后的 js 文件地址,format 表示构建的格式,而 format 中的值有 cjs(CommonJS 规范)es(ES Module 规范)umd(UMD 规范)

// 这里把相应变量处理成对应的绝对地址 module.exports = { // vue: __dirname/src/platforms/web/entry-runtime-with-compiler vue: resolve('src/platforms/web/entry-runtime-with-compiler'), // 下同 vue: '' 变量值 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'), entries: resolve('src/entries'), sfc: resolve('src/sfc') }

通过这里看出 `web` 对应的真是路径,然后 `config.js` 中的 `resolve` 函数通过 `path.resolve` 得到最终路径,就是 Vue.js 源码 web 目录下 `entry-runtime.js`,因此 `web-runtime-cjs` 配置对应的入口文件就找到了,同理 `web-runtime-cjs` 的 `dest` 配置通过上述跟踪最终会在 `dist` 目录下生成 `vue.runtime.common.js` 文件。

### Vue.js 中的 Vue 到底是什么

在 WEB 应用下,学习 Vue.js 入口文件之前,我们进入 Vue.js 核心代码文件夹 `src/platforms/web` 下,有 5 个构建文件,分别是`entry-runtime.js`、`entry-runtime-with-compiler.js`、`entry-compiler.js`、`entry-server-basic-renderer.js`、`entry-server-renderer.js`;很明显能识别出是在客户端运行时编译和服务端的渲染文件,针对客户端,这里只需要分析 Rumtime Only 和 Runtime + Compiler 构建出来的 Vue.js,分别对应 `entry-runtime.js` 和 `entry-runtime-with-compiler.js` ,那什么是 `Runtime Only` 和 `Runtime + Compiler` 呢?

- Runtime Only & Runtime Compiler

我们在使用 vue-cli 初始化 vue 项目的时候会询问是选用 Runtime Only 还是 Runtime Compiler 版本,那它们究竟是干什么的?区别又是什么?
1. Runtime Only (只包含运行时)
在使用 Runtime Only 版本 Vue.js 的时候,我们需要借助 webpack 的 vue-loader 工具把 .vue 文件编译成 JavaScript,因为是在编译阶段做的,所以它只包含运行时的 Vue.js 代码,代码体积也更轻量。
2. Runtime Compiler (编译并运行)
如果对代码没有做预编译,但是又使用了 Vue 的 `template` 属性传入了一些模板字符串,则这种情况需要在客户端编译模板,如下:
``` javascript
// 需要编译器的版本
new Vue({
  template: '<div>{{ hi }}</div>'
})

// 这种情况不需要
new Vue({
  render (h) {
    return h('div', this.hi)
  }
})

虽然在 Vue2.* 中最终渲染都是通过 render 函数,如果是写 template 属性,则需要编译成 render 函数,然而这个编译过程会发生在运行时,因此需要带编译器的版本,显然,这种在客户端操作对性能会有一定损耗,一般都是用 Runtime Only.

在前面找到了 web 平台在构建过程中使用的文件 entry-runtime.jsentry-runtime-with-compiler.js,它们有一个共同的特点是:

* other code*
import Vue from './runtime/index'
* other code*
export default Vue

然而,我们在使用 Vue 到项目的时候需要执行代码 import Vue from 'vue',这个 vue 其实就是从这里暴露出去的,那它底层究竟是什么?

在文件 entry-runtime.jsentry-runtime-with-compiler.js 中引入了 ./runtime/index,找到文件:

import Vue from 'core/index'
* other code*

这里又引入了另外一个文件夹的代码并命名为 Vue,继续找 core/index

// 又引入了一个文件,它初始化一个 Vue 构造函数,也就是 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'

// 扩展 Vue 构造函数自身的全局的静态方法及属性
initGlobalAPI(Vue)

* other code*

这里又对 Vue 进行了再次引入 instance/index,并引入了初始化的全局 API global-api/index 文件,再更进一层文件夹找到文件 instance/index 代码,终于找到了 Vue 的定义:

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) {
  // process.env 返回一个包含用户环境信息的对象
  // instanceof 用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性,只能检测 new 出来的构造函数实例
  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

终于找到了,最底层原来就是一个构造函数,我们在使用时用 new Vue 去实例它;其他的 **Mixin 方法都是给 Vue 的 prototype 上扩展方法,好吧,最底层的 Vue 函数已经找到了,再来看看前面说的全局 API initGlobalAPI

其实,Vue 在整个初始化的过程中,除了给原型上扩展方法外,它还给自身扩展很多全局的静态方法和属性,就定义在 global-api/index 文件中,如下:

// 没粘贴出所有的代码
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
  }

  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  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 最底层的代码,虽然学习还需更进一步,但是已经开始了。。。go go go。。。