[轉] 編譯輸出文件的區別
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 函數。
[轉] 編譯輸出文件的區別