Vue2 源码构建

519 阅读4分钟

一、构建方式

Vue2 源码是基于 Rollup 构建的,适合构建 JavaScript 库,无法识别其它资源类型。如果需要识别其它资源类型,则需借助插件才能让 Rollup 识别。而对于 Webpack 构建工具,更适合在项目中应用。

二、构建流程

build-flow.png

结合上面的流程图,来说说 Vue2 源码是如何构建的?

通常我们会在文件 package.json 配置 script 字段作为执行 npm 脚本,配置如下:

{
  "script": {
    "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 时,实际上是执行目录 scripts 下文件 build.js

从上面流程图可知,文件 scripts/build.js 所做的工作主要有三点:

  • 从文件 scripts/config.js 加载构建配置(builds)
  • 根据命令行参数对构建配置进行过滤
  • 调用函数 build 执行构建

那么下面来说说这三点其内部是如何实现的?

加载构建配置(builds)

在文件 scripts/build.js 中,加载构建配置的代码如下:

let builds = require('./config').getAllBuilds()

从代码中可看出,调用文件 scripts/config.js 里函数 getAllBuilds ,代码实现如下:

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

函数的作用是返回构建配置,但是在返回之前对构建配置 builds 进行遍历,按照 Rollup 构建规范调用函数 genConfig 生成配置。

Vue 可构建出不同用途的版本,具体配置在变量 builds 里,先通过一张图来看可构建出哪些版本?

build-version.jpeg

那么来看下具体的配置,然后举例子说明:

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
    },
    ...
}

web-runtime-cjs-prod 为例说下配置参数:

  • entry:表示构建的入口 JS 文件地址
  • dest:表示构建后的 JS 文件地址
  • format:表示构建出来的文件遵循 CommonJS 格式
  • env:指定构建环境
  • banner:构建说明

enterdest 用到函数 resolve ,那么该函数究竟是什么呢?代码实现如下:

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

函数的作用是根据传过来的参数 p 获取完整的文件路径。

对于 genConfig ,其内部实现也挺简单,主要是根据 Rollup 规范重新生成配置,用于后续打包构建,代码实现如下:

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

    // built-in vars
    const vars = {
            __WEEX__: !!opts.weex,
            __WEEX_VERSION__: weexVersion,
            __VERSION__: version
        }
        // feature flags
    Object.keys(featureFlags).forEach(key => {
            vars[`process.env.${key}`] = featureFlags[key]
        })
        // build-specific env
    if (opts.env) {
        vars['process.env.NODE_ENV'] = JSON.stringify(opts.env)
    }
    config.plugins.push(replace(vars))

    if (opts.transpile !== false) {
        config.plugins.push(buble())
    }

    Object.defineProperty(config, '_name', {
        enumerable: false,
        value: name
    })

    return config
}

过滤构建配置

基于以上获取到的构建配置,根据命令行参数对构建配置进行过滤,其实现逻辑也挺简单的,代码实现如下:

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

进行构建

基础工作做好了,可开始构建,核心逻辑实现就一行代码:

build(builds)

所有的构建实现都在函数 build 里,来看看其代码实现:

function build(builds) {
    let built = 0
    const total = builds.length
    const next = () => {
        buildEntry(builds[built]).then(() => {
            built++
            if (built < total) {
                next()
            }
        }).catch(logError)
    }
​
    next()
}

函数内部定义嵌套函数 next ,其作用是调用函数 buildEntrybuilds 各个版本进行构建,最终打包构建出不同用途的 Vue.js 产物。buildEntry 实现如下:

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

函数的作用是先调用 Rollup 方法 rollup 打包构建,其输出是 bundle ,然后再执行 bundle.generate(output) 生成产物,根据不同的环境对产物进行处理。

如果是生产环境,则使用 terser 对代码进行压缩,再调用函数 write 将产物写入到输出目录;如果是开发环境,则直接调用函数 write 将产物写入到输出目录,不作任何处理。

三、Runtime Only VS Runtime + Compiler

从上面的分析可知,Vue 可构建出 Runtime Only 和 Runtime + Compiler 版本,那么这两个版本有什么区别呢?

Runtime Only

仅运行时版本,只能通过 render 函数解析,无法识别 template 属性。

vue-cli 3.0+ 默认使用 Runtime Only,那么在 Vue 项目中之所以能跑起来,是因为借助 Webpack 的 vue-template-compiler 在编译阶段将 template 编译成 render 函数。这一层转换操作发生在编译阶段,所以只需包含运行时代码,其体积相对比较小。

对于使用 vue-cli 3.0+ 的项目,如果要使用 Runtime + Compiler 版本,则可在配置文件 vue.config.js 设置属性 runtimeCompilertrue

module.exports = {
  runtimeCompiler: true
}

runtimeCompiler

  • Type: boolean

  • Default: false

    是否使用包含运行时编译器的 Vue 构建版本。设置为 true 后你就可以在 Vue 组件中使用 template 选项了,但是这会让你的应用额外增加 10kb 左右。

Runtime + Compiler

如果项目中是通过 CDN 方式引入 Vue.js,并且没有使用 Webpack 等构建工具;同时又使用 Vue 属性 template 传入一个字符串,则需要在客户端编译模板,转换成 render 函数,浏览器才能解析。所以,需要使用带有编译器的 Vue 版本,即 Runtime + Compiler。

由于这时的编译过程是发生在运行时,对性能有一定的损耗,所以官方推荐使用我们使用 Runtime Only Vue.js 版本。

参考链接