Vue
源码是选用了rollup
作为bundler
,看Vue
的源码时发现:npm script
对应了不同的构建选项。这也对应了最后打包构建后产出的不同的包。
不同于其他的library
,Vue
为什么要在最后的打包构建环节输出不同类型的包呢?接下来我们通过Vue
的源码以及对应的构建配置中简单的去分析下。
由于Vue
是基于rollup进行构建的,我们先来简单了解下rollup
这个bundler
:rollup
是默认使用ES Module
规范而非CommonJS
,因此如果你在你的项目中使用rollup
作为构建工具的话,那么可以放心的使用ES Module
规范,但是如果要引入只遵循了CommonJs
规范的第三包的话,还需要使用相关的插件,插件会帮你将CommonJs
规范的代码转为ES Module
。得益于ES Module
,rollup
在构建前进行静态分析,进行tree-shaking
。关于tree-shaking
的描述请戳我。在构建输出环节,rollup
提供了多种文件输出类型:
iife
: 立即执行函数cjs
: 遵循CommonJs Module
规范的文件输出amd
: 遵循AMD Module
规范的文件输出umd
: 支持外链
/CommonJs Module
/AMD Module
规范的文件输出es
: 将多个遵循ES6 Module
的文件编译成1个ES6 Module
接下来我们就看看Vue
的使用rollup
进行构建的几个不同的版本(使用于browser
的版本)。
npm run dev 对应
rollup -w -c build/config.js --environment TARGET:web-full-dev
rollup
对应的配置信息为:
// Runtime+compiler development build (Browser)
'web-full-dev': {
entry: resolve('web/runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
开发环境下输出的umd
格式的代码,入口文件是runtime-with-compiler.js
,这个入口文件中是将Vue
的构建时
和运行时
的代码都统一进行打包了,通过查看这个入口文件,我们注意到
...
import { compileToFunctions } from './compiler/index'
...
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function () {
}
我们发现,这个文件当中,首先将原来定义的Vue.prototype.$mount
方法缓存起来,然后将这个方法进行重写,重写后的方法当中,首先判断是否有自定义的render
函数,如果有自定义的render
函数的话,Vue
不会通过自带的compiler
对模板进行编译并生成render
函数。但是如果没有自定义的render
函数,那么会调用compiler
对你定义的模板进行编译,并生成render
函数,所以通过这个rollup
的配置构建出来的代码既支持自定义render
函数,又支持template
模板编译:
// 将模板编译成render函数,并挂载到vm实例的options属性上
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
delimiters: options.delimiters
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
...
// 调用之前缓存的mount函数,TODO: 关于这个函数里面发生了什么请戳我
return mount.call(this, el, hydrating)
接下来看第二种构建方式:
npm run dev:cjs 对应的构建脚本
rollup -w -c build/config.js --environment TARGET:web-runtime-cjs
rollup
对应的配置信息为:
// Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
'web-runtime-cjs': {
entry: resolve('web/runtime.js'),
dest: resolve('dist/vue.runtime.common.js'),
format: 'cjs',
banner
}
最后编译输出的文件是遵循CommonJs Module
同时只包含runtime
部分的代码,它能直接被webpack 1.x
和Browserify
直接load
。它对应的入口文件是runtime.js
:
import Vue from './runtime/index'
export default Vue
这里没有重写Vue.prototye.$mount
方法,因此在vm
实例的生命周期中,进行到beforeMount
阶段时:
// vm挂载的根元素
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// vm.$el为真实的node
vm.$el = el
// 如果vm上没有挂载render函数
if (!vm.$options.render) {
// 空节点
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
// 钩子函数
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`${name} patch`, startTag, endTag)
}
} else {
// updateComponent为监听函数, new Watcher(vm, updateComponent, noop)
updateComponent = () => {
// Vue.prototype._render 渲染函数
// vm._render() 返回一个VNode
// 更新dom
// vm._render()调用render函数,会返回一个VNode,在生成VNode的过程中,会动态计算getter,同时推入到dep里面
// 在非ssr情况下hydrating为false
vm._update(vm._render(), hydrating)
}
}
// 新建一个_watcher对象
// vm实例上挂载的_watcher主要是为了更新DOM
// 在实例化watcher的过程中,就会执行updateComponent,完成对依赖的变量的收集过程
// vm/expression/cb
vm._watcher = new Watcher(vm, updateComponent, noop)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
首先判断vm
实例上是否定义了render
函数。如果没有,那么就会新建一个新的空vnode
并挂载到render
函数上。此外,如果页面的渲染是通过传入根节点
的形式:
new Vue({
el: '#app'
})
Vue
便会打出log
信息:
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.'
意思就是你当前使用的是只包含runtime
打包后的代码,模板的编译器(即构建时)的代码并不包含在里面。因此,你不能通过挂根节点或者是声明式模板的方式去组织你的html
内容,而只能使用render
函数去书写模板内容。不过报错信息里面也给出了提示信息就是,你还可以选择pre-compile
预编译工具去将template
模板编译成render
函数(vue-loader
就起到了这个作用)或者是使用包含了compiler
的输出包,也就是上面分析的即包含compiler
,又包含runtime
的包。
第三种构建方式:
npm run dev:esm 对应的构建脚本为:
rollup -w -c build/config.js --environment TARGET:web-runtime-esm
入口文件及最后构建出来的代码内容和第二种一样,只包含runtime
部分的代码,但是输出代码是遵循ES Module
规范的。可以被支持ES Module
的bundler
直接加载,如webpack2
和rollup
。
第四种构建方式:
npm run dev:compiler 对应的构建脚本为:
rollup -w -c build/config.js --environment TARGET:web-compiler
不同于前面3种构建方式:
// Web compiler (CommonJS).
'web-compiler': {
entry: resolve('web/compiler.js'),
dest: resolve('packages/vue-template-compiler/build.js'),
format: 'cjs',
external: Object.keys(require('../packages/vue-template-compiler/package.json').dependencies)
},
这一构建对应于将关于Vue
模板编译的成render
函数的compiler.js
单独进行打包输出。最后输出的packages/vue-template-compiler/build.js
文件会单独作为一个node_modules
进行发布,在你的开发过程中,如果使用了webpack
作为构建工具,以及vue-loader
,在开发构建环节,vue-loader
便会通过web compiler
去处理你的*.vue
文件中的模板<template>
当中的内容,将这些模板字符串编译为render
函数。