前言
大概两年前有学习过vue源码,当时学的比较粗糙,学习到的东西也比较少,差不多都快忘完了。最近打算再次捡起来,同时希望通过博客的方式加深理解和记忆,更希望能遇到一起交流的小伙伴~
Flow
本次分析的 2.6
版本是使用 flow 作为类型检查工具的,其主要的作用是对 JS 变量进行类型注释和类型推断,弥补JS作为一门弱类型语言的不足。感兴趣的朋友可以在官网上进行学习和了解。在 vue 中,有专门的 flow 文件夹对不同的变量类型进行定义,比如我们后面在源码中常见的 VNodeData
// flow/vnode.js declare interface VNodeData { key?: string | number; slot?: string; ref?: string; is?: string; pre?: boolean; tag?: string; staticClass?: string; class?: any; staticStyle?: { [key: string]: any }; style?: string | Array<Object> | Object; normalizedStyle?: Object; props?: { [key: string]: any }; attrs?: { [key: string]: string }; domProps?: { [key: string]: any }; hook?: { [key: string]: Function }; on?: ?{ [key: string]: Function | Array<Function> }; nativeOn?: { [key: string]: Function | Array<Function> }; transition?: Object; show?: boolean; // marker for v-show inlineTemplate?: { render: Function; staticRenderFns: Array<Function>; }; directives?: Array<VNodeDirective>; keepAlive?: boolean; scopedSlots?: { [key: string]: Function }; model?: { value: any; callback: Function; }; }; 复制代码
构建入口
我们可以先看看 package.json
中的构建脚本命令
// package.json { "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev", "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" } 复制代码
太多了有木有,根本学不过来。我们主要简单地了解下vue使用的打包工具不是 webpack
,而是 rollup,rollup
作为一个轻量级的打包工具,可以很好的处理JS代码打包,使其成为很多JS库打包首选。还有一个知识点就是,vue可以打包成多种不同版本,包含 weex平台运行版本,SSR服务端渲染版本,esm模块化版本,带编译的版本等等。
接下来我们大概看看vue的打包过程,是如何实现的
// scripts/build.js // 首先从 `config.js` 获取所有的打包版本配置 let builds = require('./config').getAllBuilds() // 接着对命令行参数进行解析处理,通过命令行参数匹配过滤得到需要打包的版本 // filter builds via command line arg if (process.argv[2]) { const filters = process.argv[2].split(',') builds = builds.filter(b => { return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1) }) } else { // filter out weex builds by default builds = builds.filter(b => { return b.output.file.indexOf('weex') === -1 }) } 复制代码
关于版本配置我们也可以简单看看
// scripts/config.js // 可以很清楚地看到不同版本的入口文件 `entry` 和打包路径 `dest` const builds = { // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify 'web-runtime-cjs-dev': { entry: resolve('web/entry-runtime.js'), dest: resolve('dist/vue.runtime.common.dev.js'), format: 'cjs', env: 'development', banner }, 'web-runtime-cjs-prod': { entry: resolve('web/entry-runtime.js'), dest: resolve('dist/vue.runtime.common.prod.js'), format: 'cjs', env: 'production', banner }, // Runtime+compiler CommonJS build (CommonJS) 'web-full-cjs-dev': { entry: resolve('web/entry-runtime-with-compiler.js'), dest: resolve('dist/vue.common.dev.js'), format: 'cjs', env: 'development', alias: { he: './entity-decoder' }, banner }, 'web-full-cjs-prod': { entry: resolve('web/entry-runtime-with-compiler.js'), dest: resolve('dist/vue.common.prod.js'), format: 'cjs', env: 'production', alias: { he: './entity-decoder' }, banner }, // 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 ES modules build (for bundlers) 'web-full-esm': { entry: resolve('web/entry-runtime-with-compiler.js'), dest: resolve('dist/vue.esm.js'), format: 'es', alias: { he: './entity-decoder' }, banner }, // Runtime+compiler ES modules build (for direct import in browser) 'web-full-esm-browser-dev': { entry: resolve('web/entry-runtime-with-compiler.js'), dest: resolve('dist/vue.esm.browser.js'), format: 'es', transpile: false, env: 'development', alias: { he: './entity-decoder' }, banner }, // Runtime+compiler ES modules build (for direct import in browser) 'web-full-esm-browser-prod': { entry: resolve('web/entry-runtime-with-compiler.js'), dest: resolve('dist/vue.esm.browser.min.js'), format: 'es', transpile: false, env: 'production', alias: { he: './entity-decoder' }, banner }, // runtime-only build (Browser) 'web-runtime-dev': { entry: resolve('web/entry-runtime.js'), dest: resolve('dist/vue.runtime.js'), format: 'umd', env: 'development', banner }, // runtime-only production build (Browser) 'web-runtime-prod': { entry: resolve('web/entry-runtime.js'), dest: resolve('dist/vue.runtime.min.js'), format: 'umd', env: 'production', 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 }, // Runtime+compiler production build (Browser) 'web-full-prod': { entry: resolve('web/entry-runtime-with-compiler.js'), dest: resolve('dist/vue.min.js'), format: 'umd', env: 'production', alias: { he: './entity-decoder' }, banner }, // Web compiler (CommonJS). 'web-compiler': { entry: resolve('web/entry-compiler.js'), dest: resolve('packages/vue-template-compiler/build.js'), format: 'cjs', external: Object.keys(require('../packages/vue-template-compiler/package.json').dependencies) }, // Web compiler (UMD for in-browser use). 'web-compiler-browser': { entry: resolve('web/entry-compiler.js'), dest: resolve('packages/vue-template-compiler/browser.js'), format: 'umd', env: 'development', moduleName: 'VueTemplateCompiler', plugins: [node(), cjs()] }, // Web server renderer (CommonJS). 'web-server-renderer-dev': { entry: resolve('web/entry-server-renderer.js'), dest: resolve('packages/vue-server-renderer/build.dev.js'), format: 'cjs', env: 'development', external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies) }, 'web-server-renderer-prod': { entry: resolve('web/entry-server-renderer.js'), dest: resolve('packages/vue-server-renderer/build.prod.js'), format: 'cjs', env: 'production', external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies) }, 'web-server-renderer-basic': { entry: resolve('web/entry-server-basic-renderer.js'), dest: resolve('packages/vue-server-renderer/basic.js'), format: 'umd', env: 'development', moduleName: 'renderVueComponentToString', plugins: [node(), cjs()] }, 'web-server-renderer-webpack-server-plugin': { entry: resolve('server/webpack-plugin/server.js'), dest: resolve('packages/vue-server-renderer/server-plugin.js'), format: 'cjs', external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies) }, 'web-server-renderer-webpack-client-plugin': { entry: resolve('server/webpack-plugin/client.js'), dest: resolve('packages/vue-server-renderer/client-plugin.js'), format: 'cjs', external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies) }, // Weex runtime factory 'weex-factory': { weex: true, entry: resolve('weex/entry-runtime-factory.js'), dest: resolve('packages/weex-vue-framework/factory.js'), format: 'cjs', plugins: [weexFactoryPlugin] }, // Weex runtime framework (CommonJS). 'weex-framework': { weex: true, entry: resolve('weex/entry-framework.js'), dest: resolve('packages/weex-vue-framework/index.js'), format: 'cjs' }, // Weex compiler (CommonJS). Used by Weex's Webpack loader. 'weex-compiler': { weex: true, entry: resolve('weex/entry-compiler.js'), dest: resolve('packages/weex-template-compiler/build.js'), format: 'cjs', external: Object.keys(require('../packages/weex-template-compiler/package.json').dependencies) } } 复制代码
接着我们继续分析下获取配置文件后的打包流程。
// scripts/build.js build(builds) // 对不同的打包版本进行遍历调用buildEntry function build (builds) { let built = 0 const total = builds.length const next = () => { buildEntry(builds[built]).then(() => { built++ if (built < total) { next() } }).catch(logError) } next() } // 在buildEntry中可以看到rollup开始打包工作,具体rollup的打包配置及打包原理这边不作学习 function buildEntry (config) { const output = config.output const { file, banner } = output const isProd = /(min|prod)\.js$/.test(file) return rollup.rollup(config) .then(bundle => bundle.generate(output)) .then(({ output: [{ code }] }) => { if (isProd) { // 打包后的生产代码插入banner,就是在文件头部包含版本号,日期的那段注释,可以看看 // scripts/config.js // const banner = // '/*!\n' + // ` * Vue.js v${version}\n` + // ` * (c) 2014-${new Date().getFullYear()} Evan You\n` + // ' * Released under the MIT License.\n' + // ' */' const minified = (banner ? banner + '\n' : '') + terser.minify(code, { toplevel: true, output: { ascii_only: true }, compress: { pure_funcs: ['makeMap'] } }).code return write(file, minified, true) } else { return write(file, code) } }) } 复制代码
打包的流程我们就分析到这,主要是了解下 vue 打包不同版本的实现。
目录结构
vue的核心源码在src目录下,我们先来看看源码的主要目录结构
src ├── compiler # 编译相关 ├── core # 核心代码 (组件化,全局API,实例,VDOM,数据监听) ├── platforms # 不同平台的支持(包括web/weex) ├── server # 服务端渲染 ├── sfc # .vue 文件解析 ├── shared # 共享代码 复制代码
准备工作
在学习源码之前,我们先准备好我们本地的调试环境吧,首先通过 vue create xxx
新建vue项目,同时通过 git clone 在项目中下载 vue 项目源码, 然后再修改 main.js
// src/main.js // 在入口文件中我们修改 Vue 的来源,可以引用 node_modules 中的 vue/dist 目录,也可以像我一样引用自己 clone 下来的 vue 库 import Vue from '../vue/dist/vue' import App from './App.vue' Vue.config.productionTip = false new Vue({ render: h => h(App), }).$mount('#app') 复制代码
修改引用文件为我自己 clone 下来的 vue 库的时候,遇到一些坑
- 首先就是我的编辑器 vscode 打开 vue 源码的时候会报 ts 校验的错误,通过修改 vscode 的配置文件处理好了
"typescript.validate.enable": false, "javascript.validate.enable": false 复制代码
- 其次就是一直报找不到模块
eslint-plugin-flowtype
的错误,按理说这个错误是在vue/.eslintrc.js
中的报的,但是后面我在外面的 vue 项目中也安装上了eslint-plugin-flowtype
才解决的报错,怀疑是它找的是外层项目的 node_modules,具体原因也没去了解 - 在解决报错的过程中,我也为 vscode 中安装了一些支持 flow 的插件,同时屏蔽了项目 eslint 对于 vue 源码项目的检查
// .eslintignore vue 复制代码
现在项目的目录结构是
xxx ├── node_modules ├── public ├── src ├── vue # vue为我们自己 clone 下来的源码 ├── .eslintignore ├── package.js ├── ... 复制代码
至此,我们可以通过 npm install
安装好依赖,再开启运行项目 npm run serve
,运行 vue 源码,npm run dev
来进行源码调试了