Vue 是如何用 Rollup 打包的?

Rollup 是一個 JavaScript 模塊打包器,它將小塊的代碼編譯併合併成更大、更復雜的代碼,比如打包一個庫或應用程序。它使用的是 ES Modules 模塊化標準,而不是之前的模塊化方案,如 CommonJS 和 AMD。ES 模塊可以讓你自由、無縫地使用你最喜愛庫中那些最有用的獨立函數,而讓你的項目無需包含其他未使用的代碼。

近期在團隊內組織學習 Rollup 專題,在着重介紹了 Rollup 核心概念和插件的 Hooks 機制後,爲了讓小夥伴們能夠深入瞭解 Rollup 在實際項目中的應用。我們就把目光轉向了優秀的開源項目,之後就選擇了尤大的 Vue/Vite/Vue3 項目,接下來本文將先介紹 Rollup 在 Vue 中的應用。

dev 命令

vue-2.6.14 項目根目錄下的 package.json 文件中,我們可以找到 scripts 字段,在該字段內定義瞭如何構建 Vue 項目的相關腳本。

{
  "name""vue",
  "version""2.6.14",
  "sideEffects": false,
  "scripts"{
    "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 命令爲例,來介紹一下與 rollup 相關的配置項:

dev 命令可知 rollup 的配置文件是 scripts/config.js

// scripts/config.js
// 省略大部分代碼
if (process.env.TARGET) {
  module.exports = genConfig(process.env.TARGET)
} else {
  exports.getBuild = genConfig
  exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}

觀察以上代碼可知,當 process.env.TARGET 有值的話,就會根據 TARGET 的值動態生成打包配置對象。

// scripts/config.js
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)
      }
    }
  }
  // 省略部分代碼
  return config
}

genConfig 函數內部,會從 builds 對象中獲取當前目標對應的構建配置對象。當目標爲 'web-full-dev' 時,它對應的配置對象如下所示:

// scripts/config.js
const builds ={ 
  'web-runtime-cjs-dev'{ ... },
  'web-runtime-cjs-prod'{ ... },
  // 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
  },
}

在每個構建配置對象中,會定義 entry(入口文件)、dest (輸出文件)、format(輸出格式)等信息。當獲取構建配置對象後,就根據 rollup 的要求生成對應的配置對象。

需要注意的是,在 Vue 項目的根目錄中是沒有 web 目錄的,該項目的目錄結構如下所示:

├── BACKERS.md
├── LICENSE
├── README.md
├── benchmarks
├── dist
├── examples
├── flow
├── package.json
├── packages
├── scripts
├── src
├── test
├── types
└── yarn.lock

那麼 web/entry-runtime-with-compiler.js 入口文件的位置在哪呢?其實是利用了 rollup 的 @rollup/plugin-alias 插件爲地址取了個別名。具體的映射規則被定義在 scripts/alias.js 文件中:

// scripts/alias.js
const path = require('path')
const resolve = p => path.resolve(__dirname, '../', p)

module.exports = {
  vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
  compiler: resolve('src/compiler'),
  core: resolve('src/core'),
  shared: resolve('src/shared'),
  web: resolve('src/platforms/web'),
  weex: resolve('src/platforms/weex'),
  server: resolve('src/server'),
  sfc: resolve('src/sfc')
}

根據以上的映射規則,我們可以定位到 web 別名對應的路徑,該路徑對應的文件結構如下:

├── compiler
├── entry-compiler.js
├── entry-runtime-with-compiler.js
├── entry-runtime.js
├── entry-server-basic-renderer.js
├── entry-server-renderer.js
├── runtime
├── server
└── util

到這裏結合前面介紹的 builds 對象,相信你也知道了 Vue 是如何打包不同類型的文件,以滿足不同場景的需求,比如含有編譯器和不包含編譯器的版本。分析完 dev 命令的處理流程,下面我來分析 build 命令。

build 命令

同樣,在根目錄下 package.jsonscripts 字段,我們可以找到 build 命令的定義:

{
  "name""vue",
  "version""2.6.14",
  "sideEffects": false,
  "scripts"{
    "build""node scripts/build.js",
    ...
}

當你運行 build 命令時,會使用 node 應用程序執行 scripts/build.js 文件:

// scripts/build.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
  })
}

build(builds)

scripts/build.js 文件中,會先獲取所有的構建目標,然後根據進行過濾操作,最後再調用 build 函數進行構建操作,該函數的處理邏輯也很簡單,就是遍歷構建列表,然後調用 buildEntry 函數執行構建操作。

// scripts/build.js
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 函數執行時,就會開始調用 buildEntry 函數,在該函數內部就是根據傳入了配置對象調用 rollup.rollup API 進行構建操作:

// scripts/build.js
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)
      }
    })
}

當打包完成後,下一個環節就是生成文件。在 buildEntry 函數中是通過調用 write 函數來生成文件:

// scripts/build.js
const fs = require('fs')

function write (dest, code, zip) {
  return new Promise((resolve, reject) ={
    function report (extra) {
      console.log(blue(path.relative(process.cwd(), dest)) 
     + ' ' + getSize(code) + (extra || ''))
      resolve()
    }

    fs.writeFile(dest, code, err ={
      if (err) return reject(err)
      if (zip) {
        zlib.gzip(code, (err, zipped) ={
          if (err) return reject(err)
          report(' (gzipped: ' + getSize(zipped) + ')')
        })
      } else {
        report()
      }
    })
  })
}

write 函數內部是通過 fs.writeFile 函數來生成文件,該函數還支持 zip 參數,用於輸出經過 gzip 壓縮後的大小。現在我們已經分析完了 devbuild 命令,最後我們來簡單介紹一下構建過程中所使用的一些核心插件。

rollup 插件

package.json  文件中,我們可以看到 Vue2 項目中用到的 rollup 插件:

// package.json
{
  "name""vue",
  "version""2.6.14",
  "devDependencies"{
    "rollup-plugin-alias""^1.3.1",
    "rollup-plugin-buble""^0.19.6",
    "rollup-plugin-commonjs""^9.2.0",
    "rollup-plugin-flow-no-whitespace""^1.0.0",
    "rollup-plugin-node-resolve""^4.0.0",
    "rollup-plugin-replace""^2.0.0",
  }
}

其中,"rollup-plugin-alias" 插件在前面我們已經知道它的作用了。而其他插件的作用如下:

除了以上的插件,在實際的項目中,你也可以使用 Rollup 官方倉庫提供的插件,來實現對應的功能,具體如下圖所示(僅包含部分插件):

(來源:https://github.com/rollup/plugins)

總結

本文只是簡單介紹了 Rollup 在 Vue 2 中的應用,很多細節並沒有展開介紹,感興趣的小夥伴可以自行學習一下。如果遇到問題的話,歡迎跟我一起交流哈。另外,你們也可以自行分析一下在 Vue 3 和 Vite 項目中是如何利用 Rollup 進行打包的。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/qU741y8QfEPsDhqvWAZTIA