Vuex 4源码学习笔记 - 通过build我们能学习到什么(八)

241 阅读4分钟

这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战

在上一篇笔记中:Vuex 4源码学习笔记 - 通过单元测试来看工具函数(七)

我们通过单元测试学习了Vuex所用到的工具函数。

今天我们通过Vuex的npm run build,看看能学习到什么

首先我们通过package.json中的scripts可以看到,实际是使用node运行了scripts/build.js文件

"scripts": {
  //...
  "build": "node scripts/build.js",
  //...
}

打开scripts/build.js文件

// fs-extra 是原生fs的替代品。 fs中的所有方法都附加到fs-extra。如果回调未通过,所有fs方法都会返回Promise。
const fs = require('fs-extra')
// chalk 用来在终端内输出带有样式的字符串
const chalk = require('chalk')
// execa 用来在Node.js中运行各种命令
const execa = require('execa')
// Gzip压缩文件
const { gzipSync } = require('zlib')
// Brotli压缩文件算法
const { compress } = require('brotli')

const files = [
  'dist/vuex.esm-browser.js',
  'dist/vuex.esm-browser.prod.js',
  'dist/vuex.esm-bundler.js',
  'dist/vuex.global.js',
  'dist/vuex.global.prod.js',
  'dist/vuex.cjs.js'
]

async function run() {
  await Promise.all([build(), copy()])
  checkAllSizes()
}

async function build() {
  await execa('rollup', ['-c', 'rollup.config.js'], { stdio: 'inherit' })
}

async function copy() {
   await fs.copy('src/index.mjs', 'dist/vuex.mjs')
 }

function checkAllSizes() {
  console.log()
  files.map((f) => checkSize(f))
  console.log()
}

function checkSize(file) {
  const f = fs.readFileSync(file)
  const minSize = (f.length / 1024).toFixed(2) + 'kb'
  const gzipped = gzipSync(f)
  const gzippedSize = (gzipped.length / 1024).toFixed(2) + 'kb'
  const compressed = compress(f)
  const compressedSize = (compressed.length / 1024).toFixed(2) + 'kb'
  console.log(
    `${chalk.gray(
      chalk.bold(file)
    )} size:${minSize} / gzip:${gzippedSize} / brotli:${compressedSize}`
  )
}

run()

这个文件主要依赖的NPM包有:

  • fs-extra:fs-extra 是原生fs的替代品。 fs中的所有方法都附加到fs-extra。如果回调未通过,所有fs方法都会返回Promise。
  • chalk:用来在终端内输出带有样式的字符串
  • execa:用来在Node.js中运行各种命令
  • zlib:Gzip压缩文件
  • brotli:Brotli压缩文件算法

这个文件主要做了下面三件事情:

  • build()函数用来使用rollup来进行打包
  • copy()函数用来将src/index.mjs文件拷贝到dist/vuex.mjs
  • checkAllSizes()函数用来检查每个打包后的文件的大小

接下来就是重要的使用rollup进行将源代码进行打包,我们来查看rollup配置文件rollup.config.js

import buble from '@rollup/plugin-buble'
import replace from '@rollup/plugin-replace'
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import { terser } from 'rollup-plugin-terser'
import pkg from './package.json'

const banner = `/*!
 * vuex v${pkg.version}
 * (c) ${new Date().getFullYear()} Evan You
 * @license MIT
 */`

const configs = [
  // 浏览器引入ES Module的开发版本
  { input: 'src/index.js', file: 'dist/vuex.esm-browser.js', format: 'es', browser: true, env: 'development' },
  // 浏览器引入ES Module的生产版本
  { input: 'src/index.js', file: 'dist/vuex.esm-browser.prod.js', format: 'es', browser: true, env: 'production' },
  // ES模块方式引入 import Vuex from 'vuex'
  { input: 'src/index.js', file: 'dist/vuex.esm-bundler.js', format: 'es', env: 'development' },
  // 浏览器CDN方式引入的开发版本
  { input: 'src/index.cjs.js', file: 'dist/vuex.global.js', format: 'iife', env: 'development' },
  // 浏览器CDN方式引入的生产版本
  { input: 'src/index.cjs.js', file: 'dist/vuex.global.prod.js', format: 'iife', minify: true, env: 'production' },
  // commonjs模块方式引入 require('vuex')
  { input: 'src/index.cjs.js', file: 'dist/vuex.cjs.js', format: 'cjs', env: 'development' }
]

function createEntries () {
  return configs.map((c) => createEntry(c))
}

function createEntry (config) {
  const isGlobalBuild = config.format === 'iife'
  const isBundlerBuild = config.format !== 'iife' && !config.browser
  const isBundlerESMBuild = config.format === 'es' && !config.browser

  const c = {
    external: ['vue'], // 要排除的模块ID,排除vue依赖
    input: config.input, // 入口文件地址
    plugins: [], // 所有插件
    output: {
      banner, // 在打包后文件顶部插入的代码
      file: config.file, // 输出文件地址
      format: config.format, // 输出类型(amd、cjs、es、iife、umd、system)
      globals: { // 告诉Rollup,Vue 是外部依赖
        vue: 'Vue'
      }
      /*
      var MyBundle = (function (Vue) {
        // code goes here
      }(Vue));
      */
    },
    // 拦截警告消息的函数
    onwarn: (msg, warn) => {
      if (!/Circular/.test(msg)) {
        warn(msg)
      }
    }
  }

  if (isGlobalBuild) {
    // 对于导出的iife/umd包是必需的,它代表包的全局变量名称。
    c.output.name = c.output.name || 'Vuex'
  }

  if (!isGlobalBuild) {
    // 排除@vue/devtools-api依赖
    c.external.push('@vue/devtools-api')
  }

  // Rollup插件,在打包时替换文件中的目标字符串。
  c.plugins.push(replace({
    preventAssignment: true,
    __VERSION__: pkg.version,
    __DEV__: isBundlerBuild
      ? `(process.env.NODE_ENV !== 'production')`
      : config.env !== 'production',
    __VUE_PROD_DEVTOOLS__: isBundlerESMBuild
      ? '__VUE_PROD_DEVTOOLS__'
      : 'false'
  }))

  // 一个使用buble编译器转换ES2015+代码的Rollup插件。
  if (config.transpile !== false) {
    c.plugins.push(buble())
  }

  // 一个使用Node解析算法定位模块的Rollup插件,用于在node_modules中使用第三方模块
  c.plugins.push(resolve())
  // 一个Rollup插件,用于将CommonJS模块转换为ES6
  c.plugins.push(commonjs())

  // Rollup插件,用于缩小生成的es包。
  if (config.minify) {
    c.plugins.push(terser({ module: config.format === 'es' }))
  }

  return c
}

export default createEntries()

使用Rollup打包主要用到了下面的一些插件功能:

  • @rollup/plugin-buble:使用buble编译器转换ES2015+代码的Rollup插件。
  • @rollup/plugin-replace:在打包时替换文件中的目标字符串。
  • @rollup/plugin-node-resolve:使用Node解析算法定位模块的Rollup插件,用于在node_modules中使用第三方模块
  • @rollup/plugin-commonjs:用于将CommonJS模块转换为ES6
  • rollup-plugin-terser:用于缩小生成的es包。

打包的目标产物有以下六个:

  • dist/vuex.esm-browser.js:用于通过原生 ES 模块导入使用 (在浏览器中通过 <script type="module"> 来使用)。开发版本。

  • dist/vuex.esm-browser.prod.js:同上,作为生产版本。

  • dist/vuex.esm-bundler.js:ES模块方式引入 import Vuex from 'vuex',用于 webpackrollupparcel 等构建工具,不提供压缩版本 (打包后与其余代码一起压缩)。

  • dist/vuex.global.js:浏览器CDN方式引入的开发版本

  • dist/vuex.global.prod.js:浏览器CDN方式引入的生产版本

  • dist/vuex.cjs.js:commonjs模块方式引入 const Vuex = require('vuex'),通过 require() 在 Node.js 服务器端渲染使用。

每个产物对应着不同的使用平台。

一起学习更多前端知识,微信搜索【小帅的编程笔记】,每天更新