头图

文件路径:VUE 3.0 源码 /rollup.config.js

上一篇文章中,单独介绍了各种类型的输出文件的差异,今天这篇文章将完整的介绍一下该配置文件

一、rollup.config.js 配置信息时如何生效的?

scripts/dev.js | scripts/build.js 文件中都有这么一个函数:

execa( 'rollup',  [ '-c', '--environment', [ // ... ], ])

关键就在 '-c' 这个参数上:作用是rollup打包指定使用配置文件 rollup.config.js

二、整体描述一下该配置文件的执行过程:整体围绕生成 packageConfigs 配置信息的创建展开,创建完成后:export default packageConfigs,具体细节如下:

1、dev非正式环境下回执行函数createConfig()创建配置信息,prod环境会调用createProductionConfig() 和 createMinifiedConfig() 两个函数生成相应的配置信息。

2、createConfig过程:

(1)会配置input入口文件。
(2)external 外部依赖的名称。
(3)plugins 插件信息。
(4)调用 createReplacePlugin 函数:配置替换插件中需要替换的变量,打包过程中替换文件中的字符串。
(5)配置output输出信息。
(6)设置onwarn 拦截警告消息.如果未配置,警告信息将被去重并打印到控制台。
(7)配置treeshake降低代码体积。

三、上源码,内容有点多,根据上面梳理的思路,可以快速浏览一下各个环节的代码:

/**
 * path是node的默认库我们在启动项目的时候使用的是node的环境所以path库就可以直接使用了,而不需要在安装了,
 * 这就是为什么package.json中没有path的原因
 */
import path from 'path'
/** 带有typescript编译器错误的的rollup插件,它可以打印出typescript语法和语义诊断消息*/
import ts from 'rollup-plugin-typescript2'
/** 替换文件中的目标字符串 */
import replace from '@rollup/plugin-replace'
/**
 * 允许 Rollup 从 JSON 文件中导入数据
 * 例如:import {version} from '../package.json';
 */
import json from '@rollup/plugin-json'

// 环境变量中目标模块为设置异常处理,由于命令行中输入的模块名跟 packages 目录项模块不匹配导致
if (!process.env.TARGET) {
  throw new Error('TARGET package must be specified via --environment flag.')
}

// 根目录下 package.json 中定义的版本号
const masterVersion = require('./package.json').version
// 根目录下 packages 路径
const packagesDir = path.resolve(__dirname, 'packages')
// 命令行中输入的模块名称在 packages 目录中所匹配的模块路径
const packageDir = path.resolve(packagesDir, process.env.TARGET)
// packages 目录下某模块名称
const name = path.basename(packageDir)
const resolve = p => path.resolve(packageDir, p)
// 引入当前模块下的 package.json 文件
const pkg = require(resolve(`package.json`))
// 引入当前模块下的 package.json 文件 中设定的 buildOptions 属性
const packageOptions = pkg.buildOptions || {}

// ensure TS checks only once for each build
// 全局记录TS检测状态,完成一次检测后记录状态,防止多次检测,出现错误。
let hasTSChecked = false

/**
 * 一、全局打包: vue(.runtime).global(.prod).js:【使用 CDN 或没有构建工具】
 *
 * 1、全局打包不是 UMD 构建的,它们被打包成 IIFEs,并且仅用于通过 <script src="..."> 直接使用。
 *    format: `iife`
 *    https://developer.mozilla.org/en-US/docs/Glossary/IIFE
 *    iife 的全称是 Immediately Invoked Function Expression ,即"立即调用的函数表达式",
 *    可以很容易的用 JS 来表达:(function () {}())
 *    用于通过 <script src="..."> 直接使用。
 * 2、若要通过浏览器中的 <script src="..."> 直接使用,则暴露 Vue 全局。
 * 3、vue.global.js 是包含编译器和运行时的“完整”构建版本,因此它支持动态编译模板。
 * 4、vue.runtime.global.js 只包含运行时,并且需要在构建步骤期间预编译模板。
 *
 * 二、vue(.runtime).esm-browser(.prod).js:【使用 CDN 或没有构建工具】
 *
 * 1、用于通过原生 ES 模块导入使用 (在浏览器中通过 <script type="module"> 来使用)。
 * 2、与全局构建版本共享相同的运行时编译、依赖内联和硬编码的 prod/dev 行为。
 *
 * 三、vue(.runtime).esm-bundler.js:【使用构建工具】
 *
 * 1、vue.runtime.esm-bundler.js (默认) 仅运行时,并要求所有模板都要预先编译。
 *    这是构建工具的默认入口 (通过 package.json 中的 module 字段),
 *    因为在使用构建工具时,模板通常是预先编译的 (例如:在 *.vue 文件中)。
 * 2、vue.esm-bundler.js 包含运行时编译器。
 *    如果你使用了一个构建工具,但仍然想要运行时的模板编译 (例如,DOM 内 模板或通过内联 JavaScript 字符串的模板),
 *    请使用这个文件。你需要配置你的构建工具,将 vue 设置为这个文件
 *
 * 四、vue.cjs(.prod).js:【服务端渲染】
 *
 * 1、通过 require() 在 Node.js 服务器端渲染使用。
 * 2、如果你将应用程序与带有 target: 'node' 的 webpack 打包在一起,并正确地将 vue 外部化,则将加载此文件。
 * 3、dev/prod 文件是预构建的,但是会根据 process.env.NODE_ENV 自动加载相应的文件。
 */
const outputConfigs = {
  'esm-bundler': {
    file: resolve(`dist/${name}.esm-bundler.js`),
    format: `es`
  },
  'esm-browser': {
    file: resolve(`dist/${name}.esm-browser.js`),
    format: `es`
  },
  cjs: {
    file: resolve(`dist/${name}.cjs.js`),
    format: `cjs`
  },
  global: {
    file: resolve(`dist/${name}.global.js`),
    format: `iife`
  },

  // runtime-only builds, for main "vue" package only
  'esm-bundler-runtime': {
    file: resolve(`dist/${name}.runtime.esm-bundler.js`),
    format: `es`
  },
  'esm-browser-runtime': {
    file: resolve(`dist/${name}.runtime.esm-browser.js`),
    format: 'es'
  },
  'global-runtime': {
    file: resolve(`dist/${name}.runtime.global.js`),
    format: 'iife'
  }
}

// 模块默认打包方式
const defaultFormats = ['esm-bundler', 'cjs']
/**
 * 命令行中输入的 -f | --formats 信息会在 scripts/dev.js 中赋值给环境变量 env.FORMATS。
 * 如 'esm-bundler,cjs' 多个值时中间可用逗号分隔.
 * process.env.FORMATS 默认值:'global'
 */

const inlineFormats = process.env.FORMATS && process.env.FORMATS.split(',')

/**
 * 模块打包类型优先级:
 * 第1优先级: inlineFormats 即命令行输入的 格式类型
 * 第2优先级: packageOptions.formats 即命/packages目录下子模块 package.json 中 buildOptions 属性定义的formats
 * 第2优先级: defaultFormats 即默认打包方式 ['esm-bundler', 'cjs']
 */
const packageFormats = inlineFormats || packageOptions.formats || defaultFormats

/**
 * 正式环境打包时(非dev开发环境),需要添加 --prodOnly | -p 参数,
 * -- 在 scripts/build.js 中,会检查命令行参数设置,参数正确时会设置 env.PROD_ONLY 环境变量。
 * 1、正式环境 打包配置信息为 [];
 * 2、其他非正式环境 通过函数 createConfig 创建配置信息;
 */
const packageConfigs = process.env.PROD_ONLY
  ? []
  : packageFormats.map(format => createConfig(format, outputConfigs[format]))

if (process.env.NODE_ENV === 'production') {
  packageFormats.forEach(format => {
    if (packageOptions.prod === false) {
      return
    }
    if (format === 'cjs') {
      packageConfigs.push(createProductionConfig(format))
    }
    if (/^(global|esm-browser)(-runtime)?/.test(format)) {
      packageConfigs.push(createMinifiedConfig(format))
    }
  })
}

export default packageConfigs

/**
 * 生成非正式环境模块打包配置信息
 * @param {*} format 当前模块 formats 中的一个,如: /packages/vue/package.json buildOptions 属性中 formats 列表元素
 * @param {*} output 定义输出配置信息 类型:{ file: string, format: string } 具体可参考变量: outputConfigs 元素属性值
 * @param {*} plugins
 * @returns
 */
function createConfig(format, output, plugins = []) {
  // 没有定义package输出相关配置信息:打印警告信息 & 线程退出
  if (!output) {
    console.log(require('chalk').yellow(`invalid format: "${format}"`))
    process.exit(1)
  }

  // 根据全局环境变量SOURCE_MAP信息,给 output 变量设置 sourcemap 属性。
  // 受控于命令行参数 --formats | -f 的影响
  output.sourcemap = !!process.env.SOURCE_MAP
  /**
   * 给 output 变量设置 externalLiveBindings 属性,默认值(true)
   * 当设置为false时,Rollup不会生成代码来支持外部导入的模块(假设导出的模块不会随时间而改变的前提下)。
   * 这将允许Rollup生成更优化的代码。请注意,当存在涉及外部依赖项的循环依赖项时,这可能会导致问题。
   * 这将避免大多数情况下Rollup在代码中生成getter,因此在许多情况下可用于使代码IE8兼容。
   */
  output.externalLiveBindings = false

  /**
   * 判断是否是正式环境打包:根据环境变量__DEV__ || 打包文件的输出路径名称是否以 '.prod.js' 结尾
   * __DEV__ 什么时候被赋值的 (大概率跟 process.env.NODE_ENV 相关)
   */
  const isProductionBuild =
    process.env.__DEV__ === 'false' || /\.prod\.js$/.test(output.file)

  /* esm-bundler esm-browser cjs global 类型 详见 38~71 行注释 */
  const isBundlerESMBuild = /esm-bundler/.test(format)
  const isBrowserESMBuild = /esm-browser/.test(format)
  const isNodeBuild = format === 'cjs'
  const isGlobalBuild = /global/.test(format)

  /**
   * 全局打包时,读取模块下 package.json 文件 中设定的 buildOptions 属性中的name属性赋值给output.name
   */
  if (isGlobalBuild) {
    output.name = packageOptions.name
  }

  // 判断是否生成 .d.ts 和 .d.ts.map 文件
  const shouldEmitDeclarations = process.env.TYPES != null && !hasTSChecked

  /** 配置TS 插件 */
  const tsPlugin = ts({
    // check: 设置为false可避免对代码进行任何诊断检查。
    check: process.env.NODE_ENV === 'production' && !hasTSChecked,
    /**
     * tsconfig的路径.json。如果您的tsconfig在项目目录中有其他名称或相对位置,请设置此选项。
     * 默认情况下,将尝试加载/tsconfig.json,但如果文件丢失,则不会失败,除非明确设置该值。
     */
    tsconfig: path.resolve(__dirname, 'tsconfig.json'),
    /* 缓存的路径。默认为node_modules目录下的一个的文件夹 */
    cacheRoot: path.resolve(__dirname, 'node_modules/.rts2_cache'),
    /**
     * 覆盖原有的 tsconfigDefaults 配置信息 = merge tsconfig.json to tsconfigDefaults;
     */
    tsconfigOverride: {
      compilerOptions: {
        sourceMap: output.sourcemap,
        /**
         * 为项目中TypeScript和JavaScript生成.d.ts文件
         */
        declaration: shouldEmitDeclarations,
        /**
         * 开启 --declarationMap ,编译器会同时生成 .d.ts 和 .d.ts.map 文件。
         * 语言服务现在能够正确识别这些映射文件,并且使用它们来映射到源码。
         * 也就是说,在使用“跳到定义之处”功能时,会直接跳转到源码文件,而不是 .d.ts 文件。
         */
        declarationMap: shouldEmitDeclarations
      },
      // 指定解析 include 属性配置信息时,应该跳过的文件名称
      exclude: ['**/__tests__', 'test-dts']
    }
  })
  // we only need to check TS and generate declarations once for each build.
  // it also seems to run into weird issues when checking multiple times
  // during a single build.

  // 防止出现问题,每次构建只做一次 TS 检测
  hasTSChecked = true

  // 定义模块入口文件
  const entryFile = /runtime$/.test(format) ? `src/runtime.ts` : `src/index.ts`

  // 指出应将哪些模块视为外部模块,不会与你的库打包在一起。
  const external =
    isGlobalBuild || isBrowserESMBuild
      ? packageOptions.enableNonBrowserBranches
        ? // externalize postcss for @vue/compiler-sfc
          // because @rollup/plugin-commonjs cannot bundle it properly
          ['postcss']
        : // normal browser builds - non-browser only imports are tree-shaken,
          // they are only listed here to suppress warnings.
          ['source-map', '@babel/parser', 'estree-walker']
      : // Node / esm-bundler builds. Externalize everything.
        [
          ...Object.keys(pkg.dependencies || {}),
          ...Object.keys(pkg.peerDependencies || {}),
          ...['path', 'url'] // for @vue/compiler-sfc
        ]

  // the browser builds of @vue/compiler-sfc requires postcss to be available
  // as a global (e.g. http://wzrd.in/standalone/postcss)

  /**rollup通过`external` + `output.globals`来标记外部依赖 */
  output.globals = {
    postcss: 'postcss'
  }

  /**
   * node 插件
   * 1. @rollup/plugin-node-resolve插件:使用节点解析算法在 node_modules 中查找第三方模块
   * 2. @rollup/plugin-commonjs插件:将CommonJS模块转换为ES6,以便它们可以包含在Rollup bundle中
   * 3. rollup-plugin-node-builtins插件: 允许 node 内置模块 to be required/imported.
   * 4. rollup-plugin-node-globals插件: 用于嵌入 node 全局信息,包括用于browserify的代码,即使它使用进程或缓冲区,也应该可以工作
   */
  const nodePlugins =
    packageOptions.enableNonBrowserBranches && format !== 'cjs'
      ? [
          require('@rollup/plugin-node-resolve').nodeResolve({
            // 如果'true',插件将更优先使用内置模块(例如'fs','path`)。如果'false',插件将查找本地安装的同名模块。
            preferBuiltins: true
          }),
          require('@rollup/plugin-commonjs')({
            sourceMap: false
          }),
          require('rollup-plugin-node-builtins')(),
          require('rollup-plugin-node-globals')()
        ]
      : []

  return {
    // 入口文件
    input: resolve(entryFile),
    // Global and Browser ESM builds inlines everything so that they can be
    // used alone.
    // 外部依赖的名称
    external,
    plugins: [
      // 包含 JSON 插件
      json({
        // 为JSON对象的每个属性生成命名导出。默认为true
        namedExports: false
      }),
      tsPlugin,
      // 配置替换插件中需要替换的变量,打包过程中替换文件中的字符串。
      createReplacePlugin(
        isProductionBuild,
        isBundlerESMBuild,
        isBrowserESMBuild,
        // isBrowserBuild?
        (isGlobalBuild || isBrowserESMBuild || isBundlerESMBuild) &&
          !packageOptions.enableNonBrowserBranches,
        isGlobalBuild,
        isNodeBuild
      ),
      ...nodePlugins,
      ...plugins
    ],
    output,
    // 拦截警告消息.如果未配置,警告信息将被去重并打印到控制台。
    onwarn: (msg, warn) => {
      if (!/Circular/.test(msg)) {
        warn(msg)
      }
    },
    treeshake: {
      // code from imported modules will only be retained if at least one exported value is used
      // 如果引入的模块代码想要保留,至少需要有一个该模块元素被使用过。
      moduleSideEffects: false
    }
  }
}

/**
 * 配置 Replace 插件
 */
function createReplacePlugin(
  isProduction,
  isBundlerESMBuild,
  isBrowserESMBuild,
  isBrowserBuild,
  isGlobalBuild,
  isNodeBuild
) {
  // 打包文件时替换文件中的字符串。
  const replacements = {
    __COMMIT__: `"${process.env.COMMIT}"`,
    __VERSION__: `"${masterVersion}"`,
    __DEV__: isBundlerESMBuild
      ? // preserve to be handled by bundlers
        `(process.env.NODE_ENV !== 'production')`
      : // hard coded dev/prod builds
        !isProduction,
    // this is only used during Vue's internal tests
    __TEST__: false,
    // If the build is expected to run directly in the browser (global / esm builds)
    __BROWSER__: isBrowserBuild,
    __GLOBAL__: isGlobalBuild,
    __ESM_BUNDLER__: isBundlerESMBuild,
    __ESM_BROWSER__: isBrowserESMBuild,
    // is targeting Node (SSR)?
    __NODE_JS__: isNodeBuild,

    // feature flags
    __FEATURE_SUSPENSE__: true,
    __FEATURE_OPTIONS_API__: isBundlerESMBuild ? `__VUE_OPTIONS_API__` : true,
    __FEATURE_PROD_DEVTOOLS__: isBundlerESMBuild
      ? `__VUE_PROD_DEVTOOLS__`
      : false,
    ...(isProduction && isBrowserBuild
      ? {
          'context.onError(': `/*#__PURE__*/ context.onError(`,
          'emitError(': `/*#__PURE__*/ emitError(`,
          'createCompilerError(': `/*#__PURE__*/ createCompilerError(`,
          'createDOMCompilerError(': `/*#__PURE__*/ createDOMCompilerError(`
        }
      : {})
  }
  // allow inline overrides like
  //__RUNTIME_COMPILE__=true yarn build runtime-core
  Object.keys(replacements).forEach(key => {
    if (key in process.env) {
      replacements[key] = process.env[key]
    }
  })
  return replace(replacements)
}

/**
 * Prod 正式环境:format === 'cjs' 生成服务端渲染包时 额外配置信息
 * @param {*} format
 * @returns
 */
function createProductionConfig(format) {
  return createConfig(format, {
    file: resolve(`dist/${name}.${format}.prod.js`),
    format: outputConfigs[format].format
  })
}

/**
 * Prod 正式环境: global-runtime|esm-browser-runtime 的额外配置
 * @param {*} format
 * @returns
 */
function createMinifiedConfig(format) {
  // minify generated es bundle 压缩 ES code
  const { terser } = require('rollup-plugin-terser')
  return createConfig(
    format,
    {
      file: outputConfigs[format].file.replace(/\.js$/, '.prod.js'),
      format: outputConfigs[format].format
    },
    [
      terser({
        module: /^esm/.test(format), // true is set when format is esm or es
        compress: {
          // 5 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020;
          ecma: 2015,
          pure_getters: true
        }
      })
    ]
  )
}

如果您对 “前端源码” 情有独钟,可以微信扫码关注下面的公众号二维码,内容一直持续更新中!
当前 VUE3.0 源码正在解析中,欢迎捧场!
image.png


老罗
1 声望0 粉丝

IT小伙儿