【源码库】跟着 Vue3 学习前端模块化

news2025/7/20 9:39:55

Vue3为了支持不同的用户群体,提供了多种模块化方案,这样使得我们在使用的Vue的使用可以有很多种方式;

例如我们可以直接在html中使用script标签引入Vue,也可以前端工程化工具,例如webpackrollup等打包工具,将Vue打包到我们的项目中,甚至还可以在nodejs中使用服务端渲染的方式来使用Vue

今天就跟着Vue3的源码,一起来看看Vue3是如何支持这些模块化方案的,以及我们在使用Vue的时候,应该如何选择合适的模块化方案;

大家好,这里是田八的【源码&库】系列,Vue3的源码阅读计划,Vue3的源码阅读计划不出意外每周一更,欢迎大家关注。

如果想一起交流的话,可以点击这里一起共同交流成长

系列章节:

  • 【源码&库】跟着 Vue3 学习前端模块化
  • 【源码&库】在调用 createApp 时,Vue 为我们做了那些工作?

准备工作

这是第一篇关于Vue3源码的文章,所以我们需要先准备一些工作,才能开始我们的源码学习之旅;

源码 clone

找到Vue3的源码:https://github.com/vuejs/core

源码下载的方式有很多种,这里建议大家下载到本地,方便后面的调试和学习;

image.png

可以直接在github上下载,也可以使用git命令下载:

git clone https://github.com/vuejs/core.git

还可以fork到自己的仓库,然后使用git命令下载,git和上面相同,这里就不再赘述;

环境准备

Vue3的包管理工具使用的是pnpm,这个可以在Vue3的源码中的package.json文件中找到:

{
  "version": "3.2.45",
  "packageManager": "pnpm@7.1.0",
  "engines": {
    "node": ">=16.11.0"
  }
}

上面列举的就是Vue3的开发环境,截取自Vue3package.json文件;

  • versionVue3的版本号
  • packageManagerVue3的包管理工具,这里是pnpm,版本号是7.1.0
  • enginesVue3的开发环境,这里是node,版本号是>=16.11.0

所以我们在学习Vue3的源码之前,需要先安装pnpmnode,并且需要注意它们的版本号;

这里怎么安装pnpmnode,大家自行查询,应该不会有什么问题;

  • pnpm installation
  • nodejs

环境准备好了之后,我们通过pnpm安装Vue3的依赖:

cd [源码目录]

pnpm install

Vue3 模块化方案

上面的准备工作做完了之后,我们就可以开始学习Vue3的源码了;

我们今天需要了解的是Vue3的模块化实现,模块化离不开构建工具,Vue使用的rollup作为构建工具;

先不管使用的是什么构建工具,最终是一定要通过构建工具来打包我们的代码,而打包的命令通常是封装在package.json中的;

所以我们直接看一下Vue3package.json,看看里面封装的命令有哪些:

{
  "scripts": {
    "dev": "node scripts/dev.js",
    "build": "node scripts/build.js",
    "size": "run-s size-global size-baseline",
    "size-global": "node scripts/build.js vue runtime-dom -f global -p",
    "size-baseline": "node scripts/build.js runtime-dom runtime-core reactivity shared -f esm-bundler && cd packages/size-check && vite build && node brotli",
    "lint": "eslint --cache --ext .ts packages/*/{src,__tests__}/**.ts",
    "format": "prettier --write --cache --parser typescript "packages/**/*.ts?(x)"",
    "format-check": "prettier --check --cache --parser typescript "packages/**/*.ts?(x)"",
    "test": "run-s "test-unit {@}" "test-e2e {@}"",
    "test-unit": "jest --filter ./scripts/filter-unit.js",
    "test-e2e": "node scripts/build.js vue -f global -d && jest --filter ./scripts/filter-e2e.js --runInBand",
    "test-dts": "node scripts/build.js shared reactivity runtime-core runtime-dom -dt -f esm-bundler && npm run test-dts-only",
    "test-dts-only": "tsc -p ./test-dts/tsconfig.json && tsc -p ./test-dts/tsconfig.build.json",
    "test-coverage": "node scripts/build.js vue -f global -d && jest --runInBand --coverage --bail",
    "release": "node scripts/release.js",
    "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
    "dev-esm": "node scripts/dev.js -if esm-bundler-runtime",
    "dev-compiler": "run-p "dev template-explorer" serve",
    "dev-sfc": "run-s dev-sfc-prepare dev-sfc-run",
    "dev-sfc-prepare": "node scripts/pre-dev-sfc.js || npm run build-compiler-cjs",
    "dev-sfc-serve": "vite packages/sfc-playground --host",
    "dev-sfc-run": "run-p "dev compiler-sfc -f esm-browser" "dev vue -if esm-bundler-runtime" "dev server-renderer -if esm-bundler" dev-sfc-serve",
    "serve": "serve",
    "open": "open http://localhost:5000/packages/template-explorer/local.html",
    "build-sfc-playground": "run-s build-compiler-cjs build-runtime-esm build-ssr-esm build-sfc-playground-self",
    "build-compiler-cjs": "node scripts/build.js compiler reactivity-transform shared -af cjs",
    "build-runtime-esm": "node scripts/build.js runtime reactivity shared -af esm-bundler && node scripts/build.js vue -f esm-bundler-runtime && node scripts/build.js vue -f esm-browser-runtime",
    "build-ssr-esm": "node scripts/build.js compiler-sfc server-renderer -f esm-browser",
    "build-sfc-playground-self": "cd packages/sfc-playground && npm run build",
    "preinstall": "node ./scripts/preinstall.js",
    "postinstall": "simple-git-hooks"
  }
}

这里面有很多命令,通常情况下打包的命令是build,去掉build之后的命令,我们可以看到Vue3的打包命令是:

{
  "scripts": {
    "build": "node scripts/build.js",
    "build-sfc-playground": "run-s build-compiler-cjs build-runtime-esm build-ssr-esm build-sfc-playground-self",
    "build-compiler-cjs": "node scripts/build.js compiler reactivity-transform shared -af cjs",
    "build-runtime-esm": "node scripts/build.js runtime reactivity shared -af esm-bundler && node scripts/build.js vue -f esm-bundler-runtime && node scripts/build.js vue -f esm-browser-runtime",
    "build-ssr-esm": "node scripts/build.js compiler-sfc server-renderer -f esm-browser",
    "build-sfc-playground-self": "cd packages/sfc-playground && npm run build"
  }
}

这里面有很多的命令,先简单解释一下这些命令的意思:

  • build:打包所有的包,等会主要分析这个命令;
  • build-sfc-playground:打部署Vue3SFCplayground:https://sfc.vuejs.org
  • build-compiler-cjs:打包compilercjs包,这个包是Vue3的编译器
  • build-runtime-esm:打包runtimeesm包,这个包是Vue3的运行时
  • build-ssr-esm:打包ssresm包,这个包是Vue3的服务端渲染
  • build-sfc-playground-self:这个是单独打包Vue3SFCplayground的包

可以看到这些命令都是node scripts/build.js,除了build-sfc-playground-self,其他的命令后面都会带上一些参数,这些参数是什么意思呢?我们来看一下scripts/build.js的代码。

scripts/build.js

这个文件不到200行,我们来看一下这个文件的代码,这个文件的代码如下:

const fs = require('fs-extra')
const path = require('path')
const chalk = require('chalk')
const execa = require('execa')
const { gzipSync } = require('zlib')
const { compress } = require('brotli')
const { targets: allTargets, fuzzyMatchTarget } = require('./utils')

const args = require('minimist')(process.argv.slice(2))
const targets = args._
const formats = args.formats || args.f
const devOnly = args.devOnly || args.d
const prodOnly = !devOnly && (args.prodOnly || args.p)
const sourceMap = args.sourcemap || args.s
const isRelease = args.release
const buildTypes = args.t || args.types || isRelease
const buildAllMatching = args.all || args.a
const commit = execa.sync('git', ['rev-parse', 'HEAD']).stdout.slice(0, 7)

run()

async function run() {
    // ...
}

async function buildAll(targets) {
    // ...
}

async function runParallel(maxConcurrency, source, iteratorFn) {
    // ...
}

async function build(target) {
    // ...
}

function checkAllSizes(targets) {
    // ...
}

function checkSize(target) {
    // ...
}

function checkFileSize(filePath) {
    // ...
}

方便查看,这里删除函数中的代码,只保留函数的声明

可以看到这里是直接执行的run函数,这个函数的代码如下:

async function run() {
  if (isRelease) {
    // remove build cache for release builds to avoid outdated enum values
    await fs.remove(path.resolve(__dirname, '../node_modules/.rts2_cache'))
  }
  if (!targets.length) {
    await buildAll(allTargets)
    checkAllSizes(allTargets)
  } else {
    await buildAll(fuzzyMatchTarget(targets, buildAllMatching))
    checkAllSizes(fuzzyMatchTarget(targets, buildAllMatching))
  }
}

这里有两个判断,使用了定义的全局变量,一个是isRelease,一个是targets,先来看一下这两个变量是怎么获取的。

const args = require('minimist')(process.argv.slice(2))
const targets = args._

const isRelease = args.release

这里是用过minimist包来获取process.argv的参数;

minimist是一个解析命令行参数的包,可以将命令行参数解析成一个对象,比如node build.js --release,这里的release就是一个参数,可以通过minimist来解析成一个对象,这个对象的key就是参数的名字,value就是参数的值

targets指向的是args._isRelease指向的是args.release,这两个变量的值是什么呢?我们直接开启调试模式看一下,这里使用的是build命令;

image.png

因为build命令是没有参数的,所以这两个变量都是是没有值的,但是tarets指向的args._是一个是空数组,isRelease是正常的undefined

所以build命令会执行下面的两个函数:

await buildAll(allTargets)
checkAllSizes(allTargets)

这里又出现了一个全局变量allTargets,乘着还在调试模式,我们来看一下这个变量的值;

image.png

它获取的地方是通过require引入的utils文件,这个文件的代码如下:

// 引用的代码,通过解构赋值的方式获取,而且还对这些变量进行了重命名
const { targets: allTargets, fuzzyMatchTarget } = require('./utils')

// utils 文件中的 targets 获取
const targets = (exports.targets = fs.readdirSync('packages').filter(f => {
    if (!fs.statSync(`packages/${f}`).isDirectory()) {
        return false
    }
    const pkg = require(`../packages/${f}/package.json`)
    if (pkg.private && !pkg.buildOptions) {
        return false
    }
    return true
}))

这里是通过fs.readdirSync来读取packages文件夹下的文件,然后通过filter过滤;

fs.readdirSync是同步读取文件夹下的文件,返回一个数组,数组中的每一项都是文件夹下的文件名

然后使用filter过滤不是文件夹的文件,然后再拿到这个文件夹中的package.json文件;

通过package.json文件中的privatebuildOptions来判断是否是一个需要打包的文件夹,如果是的话,就将这个文件夹的名字添加到targets数组中;

通过上面的截图再对比一下源码的packages文件夹,可以看到targets数组中的每一项都是一个文件夹的名字,并且也知道哪些文件是不需要打包的,比如sfc-playground工程并不是vue源码发布包含的部分,所以它是不需要打包的;

继续往下看,buildAll函数的代码如下:

async function buildAll(targets) {
  await runParallel(require('os').cpus().length, targets, build)
}

async function runParallel(maxConcurrency, source, iteratorFn) {
  const ret = []
  const executing = []
  for (const item of source) {
    const p = Promise.resolve().then(() => iteratorFn(item, source))
    ret.push(p)

    if (maxConcurrency <= source.length) {
      const e = p.then(() => executing.splice(executing.indexOf(e), 1))
      executing.push(e)
      if (executing.length >= maxConcurrency) {
        await Promise.race(executing)
      }
    }
  }
  return Promise.all(ret)
}

buildAll函数只是包装了一下runParallel函数;

runParallel 并发执行

runParallel函数的作用是并发执行build函数,可以简单的看一下runParallel函数的代码;

这里的maxConcurrency是通过os.cpus().length获取的,也就是获取当前电脑的 CPU 核心数;

source就是targets数组,也就是packages文件夹下的文件夹名字;

iteratorFn就是build函数;

通过Promise.resolve()创建一个微任务的异步函数,然后将build函数放入then中等待执行;

然后将这个异步函数放入ret数组中,ret数组的作用是存放所有的异步函数;

如果source的长度小于CPU的核心数,那么就直接使用Promise.all(ret)来并发执行所有的异步函数;

如果大于CPU的核心数,那么就极限发挥多核的能力,下面就是关键代码:

if (maxConcurrency <= source.length) {
    // 对异步函数进行包装,包装的目的是在异步函数执行完毕后,将这个异步函数从 executing 数组中移除
    const e = p.then(() => executing.splice(executing.indexOf(e), 1))
    
    // 将包装后的异步函数放入 executing 数组中
    executing.push(e)
    
    // 如果达到最大并发数,那么就等待一个异步函数执行完毕,然后再执行下一个异步函数
    if (executing.length >= maxConcurrency) {
        // Promise.race 会在最先执行完毕的异步函数执行完毕后,将这个异步函数的执行结果
        await Promise.race(executing)
    }
}

这里的实现可以说是非常巧妙了,通过Promise.race来实现并发执行异步函数,而Promise.race的特性就是只要有一个异步函数执行完毕,那么Promise.race就会有执行结果;

而在这些异步函数执行完毕后,会将这些异步函数从executing数组中移除,这样就会空出一个位置,通过检测executing数组的长度,就可以知道是否达到了最大并发数,这样就可以保持最大并发数的异步函数在执行;

而这个代码的最后还是会调用一次Promise.all,这是因为Promise.race只能保证最先执行完毕的异步函数执行完毕,但是并不能保证所有的异步函数都执行完毕,所以这里还是需要调用一次Promise.all来保证所有的异步函数都执行完毕;

build 正式开始构建

上面的并发执行最终都是为了执行build函数,现在就来看看build函数的代码;

async function build(target) {
    // 获取当前构建的包的路径
    const pkgDir = path.resolve(`packages/${target}`)

    // 获取当前构建的包的 package.json
    const pkg = require(`${pkgDir}/package.json`)

    // if this is a full build (no specific targets), ignore private packages
    // 如果是全量构建,那么就忽略私有包
    if ((isRelease || !targets.length) && pkg.private) {
        return
    }

    // if building a specific format, do not remove dist.
    // 如果是构建指定的格式,那么就不要删除 dist 目录
    if (!formats) {
        await fs.remove(`${pkgDir}/dist`)
    }

    // 构建目标生成的环境变量
    const env = (pkg.buildOptions && pkg.buildOptions.env) || (devOnly ? 'development' : 'production')

    // 执行 rollup 构建
    await execa(
        'rollup',
        [
            '-c',
            '--environment',
            [
                `COMMIT:${commit}`,
                `NODE_ENV:${env}`,
                `TARGET:${target}`,
                formats ? `FORMATS:${formats}` : ``,
                buildTypes ? `TYPES:true` : ``,
                prodOnly ? `PROD_ONLY:true` : ``,
                sourceMap ? `SOURCE_MAP:true` : ``
            ]
                .filter(Boolean)
                .join(',')
        ], {
            stdio: 'inherit'
        }
    )

    // if 里面的代码不会执行,因为执行 npm run build 的时候没有任何参数,所以下面的代码省略
    if (buildTypes && pkg.types) {
          // 这里主要是建构建类型声明文件
    }
}

build函数最终就是为了执行rollup命令;

这里的rollup命令是通过execa来执行的,execa是一个可以执行命令的库;

这里的execa的第一个参数就是要执行的命令;

第二个参数就是要传递给命令的参数;

第三个参数就是execa的配置,这里的stdio配置就是让execa的输出和rollup的输出保持一致;

关键点在于rollup命令的参数,而使用npm run build命令的时候,没有传递任何参数,所以formatsbuildTypesprodOnlysourceMap都是undefined

所以最终的rollup命令就是:

rollup -c --environment COMMIT:xxx,NODE_ENV:production,TARGET:xxx

rollup.config.js

既然最终执行的是rollup命令,那么就得走到rollup.config.js这个文件了;

上面逐行分析的了sripts/build.js文件,这里就不再逐行分析了,直接看整体的代码,逐行分析的直接写在注释里面了;

// @ts-check
import { createRequire } from 'module'
import { fileURLToPath } from 'url'
import path from 'path'
import ts from 'rollup-plugin-typescript2'
import replace from '@rollup/plugin-replace'
import json from '@rollup/plugin-json'
import chalk from 'chalk'
import commonJS from '@rollup/plugin-commonjs'
import polyfillNode from 'rollup-plugin-polyfill-node'
import { nodeResolve } from '@rollup/plugin-node-resolve'
import terser from '@rollup/plugin-terser'

// 必须有 TARGET 参数
if (!process.env.TARGET) {
  throw new Error('TARGET package must be specified via --environment flag.')
}

// 创建 require 函数,import.meta.url 指向的是当前文件的路径
const require = createRequire(import.meta.url)
// 没有使用 node 自带的 __dirname,而是使用了 fileURLToPath 来获取当前文件的路径
const __dirname = fileURLToPath(new URL('.', import.meta.url))

// 获取 package 的版本号
const masterVersion = require('./package.json').version
// 模板引擎整合库
const consolidatePkg = require('@vue/consolidate/package.json')

// packages 目录路径
const packagesDir = path.resolve(__dirname, 'packages')
// 当前构建工程包的路径
const packageDir = path.resolve(packagesDir, process.env.TARGET)

// 简单封装一个 resolve 函数,方便后面使用
const resolve = p => path.resolve(packageDir, p)
// 通过 resolve 函数获取当前构建工程包的 package.json
const pkg = require(resolve(`package.json`))
// 当前构建工程包的 package.json 的 buildOptions 配置
const packageOptions = pkg.buildOptions || {}
// 工程名,通过  package.json 的 buildOptions 配置来的,如果没有就是工程的包名
const name = packageOptions.filename || path.basename(packageDir)

// ensure TS checks only once for each build
// 否则执行 ts 检查的标识,确保只会执行一次检查
let hasTSChecked = false

// 输出模块化的配置,包含了 cjs、esm、iife
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'
  }
}

// 默认的输出配置,对应上面的 outputConfigs
const defaultFormats = ['esm-bundler', 'cjs']
// 通过参数传递的构建包的格式,使用 npm run build 命令没有这个参数
const inlineFormats = process.env.FORMATS && process.env.FORMATS.split(',')
// 最终包构架的格式,优先使用参数传递的格式,其次使用 package.json 的 buildOptions.formats 配置,最后使用默认的格式
const packageFormats = inlineFormats || packageOptions.formats || defaultFormats
// 最终包构架的格式的配置,通过 packageFormats 过滤出 outputConfigs 中的配置
const packageConfigs = process.env.PROD_ONLY
  ? []
  : packageFormats.map(format => createConfig(format, outputConfigs[format]))

// 如果是生产环境,那么就添加一个生产环境的配置
if (process.env.NODE_ENV === 'production') {
  packageFormats.forEach(format => {
    // 如果 package.json 中的 buildOptions.prod 确定为 false,那么就不会添加生产环境的配置
    if (packageOptions.prod === false) {
      return
    }
    // cjs 的配置会增加的配置,通过 createProductionConfig 函数来创建
    if (format === 'cjs') {
      packageConfigs.push(createProductionConfig(format))
    }
    
    // 浏览器环境包增加的配置,通过 createMinifiedConfig 函数来创建
    if (/^(global|esm-browser)(-runtime)?/.test(format)) {
      packageConfigs.push(createMinifiedConfig(format))
    }
  })
}

// 导出 rollup 的配置
export default packageConfigs

function createConfig(format, output, plugins = []) {
    // ...
}

function createReplacePlugin(
  isProduction,
  isBundlerESMBuild,
  isBrowserESMBuild,
  isBrowserBuild,
  isGlobalBuild,
  isNodeBuild,
  isCompatBuild,
  isServerRenderer
) {
    // ...
}

function createProductionConfig(format) {
  // ...
}

function createMinifiedConfig(format) {
  // ...
}

上面的代码逐行给出了注释,这里就不再赘述,最终就是为了导出rollup的配置,这些配置最终长什么样子?我们可以通过上面分析build函数最后执行的命令来看看:

rollup -c --environment COMMIT:1fa3d95,NODE_ENV:production,TARGET:compiler-core

rollup 配置

通过上面的命名出来的配置长下面的样子,这里只展示了compiler-core包的配置,其他包的配置类似:

这里只是列出配置,不用看懂,只需要关心inputoutput的配置,还有最终生成配置结构,由于我这里是直接导出的json,会导致函数处理部分的缺失。

[
  {
    "input": "C:\workspace\vue3-progress\core\packages\compiler-core\src\index.ts",
    "external": [
      "@vue/shared",
      "@babel/parser",
      "estree-walker",
      "source-map",
      "path",
      "url",
      "stream",
      "source-map",
      "@babel/parser",
      "estree-walker"
    ],
    "plugins": [
      {
        "name": "json"
      },
      {
        "name": "rpt2"
      },
      {
        "name": "replace"
      }
    ],
    "output": {
      "file": "C:\workspace\vue3-progress\core\packages\compiler-core\dist\compiler-core.esm-bundler.js",
      "format": "es",
      "exports": "named",
      "sourcemap": false,
      "externalLiveBindings": false
    },
    "treeshake": {
      "moduleSideEffects": false
    }
  },
  {
    "input": "C:\workspace\vue3-progress\core\packages\compiler-core\src\index.ts",
    "external": [
      "@vue/shared",
      "@babel/parser",
      "estree-walker",
      "source-map",
      "path",
      "url",
      "stream",
      "source-map",
      "@babel/parser",
      "estree-walker"
    ],
    "plugins": [
      {
        "name": "json"
      },
      {
        "name": "rpt2"
      },
      {
        "name": "replace"
      },
      {
        "name": "commonjs",
        "version": "23.0.2"
      },
      {
        "name": "node-resolve",
        "version": "15.0.1",
        "resolveId": {
          "order": "post"
        }
      }
    ],
    "output": {
      "file": "C:\workspace\vue3-progress\core\packages\compiler-core\dist\compiler-core.cjs.js",
      "format": "cjs",
      "exports": "named",
      "sourcemap": false,
      "externalLiveBindings": false
    },
    "treeshake": {
      "moduleSideEffects": false
    }
  },
  {
    "input": "C:\workspace\vue3-progress\core\packages\compiler-core\src\index.ts",
    "external": [
      "@vue/shared",
      "@babel/parser",
      "estree-walker",
      "source-map",
      "path",
      "url",
      "stream",
      "source-map",
      "@babel/parser",
      "estree-walker"
    ],
    "plugins": [
      {
        "name": "json"
      },
      {
        "name": "rpt2"
      },
      {
        "name": "replace"
      },
      {
        "name": "commonjs",
        "version": "23.0.2"
      },
      {
        "name": "node-resolve",
        "version": "15.0.1",
        "resolveId": {
          "order": "post"
        }
      }
    ],
    "output": {
      "file": "C:\workspace\vue3-progress\core\packages\compiler-core\dist\compiler-core.cjs.prod.js",
      "format": "cjs",
      "exports": "named",
      "sourcemap": false,
      "externalLiveBindings": false
    },
    "treeshake": {
      "moduleSideEffects": false
    }
  }
]

上面的没必要全看,简化后如下:

[
  {
    "input": "C:\workspace\vue3-progress\core\packages\compiler-core\src\index.ts",
    "output": {
      "file": "C:\workspace\vue3-progress\core\packages\compiler-core\dist\compiler-core.esm-bundler.js",
      "format": "es",
      "exports": "named",
      "sourcemap": false,
      "externalLiveBindings": false
    }
  },
  {
    "input": "C:\workspace\vue3-progress\core\packages\compiler-core\src\index.ts",
    "output": {
      "file": "C:\workspace\vue3-progress\core\packages\compiler-core\dist\compiler-core.cjs.js",
      "format": "cjs",
      "exports": "named",
      "sourcemap": false,
      "externalLiveBindings": false
    }
  },
  {
    "input": "C:\workspace\vue3-progress\core\packages\compiler-core\src\index.ts",
    "output": {
      "file": "C:\workspace\vue3-progress\core\packages\compiler-core\dist\compiler-core.cjs.prod.js",
      "format": "cjs",
      "exports": "named",
      "sourcemap": false,
      "externalLiveBindings": false
    }
  }
]

看到这些配置不难发现它们长的很想,input指向的都是同一个入口文件,不同的是outputfileformat不同。

前端模块化

回头看看我这篇文章的标题,跟着Vue3来学习前端模块化,这里我们已经粗窥Vue3的模块化了;

上面生成的配置文件中有主要生成的esmcommonjs两种模块化的文件,先简单介绍下这两种模块化的区别:

  • esm:指的是es6出的模块化规范,它是js原生支持的模块化规范,它的特点是动态加载,可以在运行时加载模块,而且可以通过import()动态加载模块,它的优点是可以按需加载,减少了打包后的文件体积;
  • commonjs:指的是node出的模块化规范,它的特点是静态加载,只能在编译时加载模块,而且只能通过require()加载模块,它的优点是可以同步加载,不需要考虑加载顺序;
  • iife:指的是IIFE模块化规范,它的特点是静态加载,只能在编译时加载模块,而且只能通过script标签加载模块,它的优点是可以同步加载,不需要考虑加载顺序;

这里补上iife的模块化的简短说明,这里就不详细介绍,网上有很多关于前端模块化的文章,感兴趣可以自行查阅。

我们来看看生成的这三个文件的区别:

  • compiler-core.cjs.js

image.png

  • compiler-core.cjs.prod.js

image.png

  • compiler-core.esm-bundler.js

image.png

模块化的规范不同的方面就不多说了,最直观的理解就是导入和导出的方式不同,esmimport导入,commonjsrequire导入,iifescript标签导入。

而通过上面的三个文件的对比,直接看defaultOnWarn函数的区别:

  • compiler-core.cjs.jsdefaultOnWarn函数是直接使用console.warn打印内容的;
  • compiler-core.cjs.prod.jsdefaultOnWarn函数里面是空的,没有任何内容;
  • compiler-core.esm-bundler.jsdefaultOnWarn函数多了一个环境检测,如果是process.env.NODE_ENV !== 'production'就不打印内容;

这一部分就是通过rollup.config.js中的createReplacePlugin函数生成的,来看看这个函数的实现:

function createReplacePlugin(
  isProduction,
  isBundlerESMBuild,
  isBrowserESMBuild,
  isBrowserBuild,
  isGlobalBuild,
  isNodeBuild,
  isCompatBuild,
  isServerRenderer
) {
  // 替换的关键字和内容对象的映射
  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,
    // need SSR-specific branches?
    __SSR__: isNodeBuild || isBundlerESMBuild || isServerRenderer,

    // for compiler-sfc browser build inlined deps
    ...(isBrowserESMBuild
      ? {
          'process.env': '({})',
          'process.platform': '""',
          'process.stdout': 'null'
        }
      : {}),

    // 2.x compat build
    __COMPAT__: isCompatBuild,

    // 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]
    }
  })
    
  // replace 插件,用于替换代码中的关键字,由 rollup-plugin-replace 提供
  return replace({
    // @ts-ignore
    values: replacements,
    preventAssignment: true
  })
}

可以先把上面的代码简化一下:

function createReplacePlugin(isBundlerESMBuild) {
    // 替换的关键字和内容对象的映射
    const replacements = {
        __DEV__: isBundlerESMBuild
            ? // 工程化打包处理机制
            `(process.env.NODE_ENV !== 'production')`
            : // 硬编码 dev/prod 构建方式
            !isProduction,
    }
    
    // 可以通过命令参数覆盖
    //__RUNTIME_COMPILE__=true yarn build runtime-core
    Object.keys(replacements).forEach(key => {
        if (key in process.env) {
            replacements[key] = process.env[key]
        }
    })

    return replace({
        values: replacements,
        preventAssignment: true
    })
}

这个函数的主要作用就是生成replacements对象,这个对象就是用来替换代码中的关键字的,比如__DEV__就是用来替换代码中的__DEV__关键字的,这个关键字在vue的代码中是这样使用的:

if (__DEV__) {
  // do something
}

而这个关键字根据上面三个输出文件来看:

  • compiler-core.cjs.js__DEV__true

image.png

  • compiler-core.cjs.prod.js__DEV__false

image.png

  • compiler-core.esm-bundler.js__DEV__process.env.NODE_ENV !== 'production'

image.png

而根据不同的替换值,最终生成的代码也不相同,就比如上面的示例。

参考:https://github.com/rollup/plugins/tree/master/packages/replace

场景适配

通过上面的配置可以看到,vue中使用这种方式的变量有非常多,而__DEV__只是其中的一种,也是我们最为熟悉的一种;

__DEV__的作用就是用来判断当前的环境是开发环境还是生产环境,同时还会考虑各种环境的适配,就比如上面的三个文件:

  • compiler-core.cjs.js:这个文件是commonjs规范的,通常是提供给node环境使用的,所以__DEV__的值为true,这样就可以在开发环境下输出一下内部的日志信息,方便开发者调试;
  • compiler-core.cjs.prod.js:这个文件也是commonjs规范的,文件名中带有.prod,通常是在生产环境下使用的,所以__DEV__的值为false,在生产环境下移除掉一些内部的日志信息,减少打包体积;
  • compiler-core.esm-bundler.js:这个文件是esm规范的,通常是提供给webpackvite等打包工具使用的,所以__DEV__的值为process.env.NODE_ENV !== 'production',这样就给这些打包工具提供了一种处理机制,让打包工具来决定是否移除;

除了上述的这两种打包方案,vue还提供了一种iife的打包方案,这种打包方案通常是用来在浏览器环境下使用的;

在这种方案下__DEV__的值会是什么呢?而最终生成的文件和上面的两种有什么区别呢?这里就留给感兴趣的同学自己动动手,编译一下vue,输出一些日志信息,看看最终生成的文件有什么区别。

总结

通过分析vue源码的package.jsonscripts配置,我们了解到vue是通过nodejsexeca包,对rollup的命令进行了封装,从而实现了对rollup的命令和配置动态化;

execa是基于child_process的封装,可以让我们更加方便的执行命令,vue利用这个特性,使用多线程的方式,同时执行多个rollup命令,从而加快了打包速度;

rollup的配置文件是通过rollup.config.js来进行配置的,而vue通过各种参数的封装,实现了对rollup配置的动态化,从而实现了对不同的打包方案的适配;

考虑到vue可能会在不同的环境下运行,vue通过配置rollupoutput.format参数,可以实现对不同的打包方案的适配,比如cjsesmiife等,从而实现对浏览器环境、node环境、webpackvite等打包工具的适配;

考虑到产物最终可能需要区分开发环境和生产环境,vue通过配置rollupreplace插件,可以实现对__DEV__的动态替换,从而实现对开发环境和生产环境的适配;

宣传

这是我写的关于vue3源码系列的第一章,从打包开始,后面的章节会一步步深入到vue3的源码中,逐步了解vue3的实现原理;

目前的节奏准备是一周一篇,学习趋势是后面会跟着官网的api文档,看看每个api后面是怎么实现的,下一篇文章会就是正式开始,从createApp开始,也是我们使用vue3的入口;

个人也不知道大家更能接受什么方式理解源码,然后这篇文章第一部分逐行分析scripts/build.js的代码,第二部分是对rollup.config.js的直接分析,不知道大家更能接受那种方式;

自己现在也是在学习vue3的源码,可能会有一些错误或者理解不到位的地方,欢迎大家指正,也欢迎大家一起来学习,一起来交流,一起来进步;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/395423.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023年湖北一级技师二级技师报名时间、考试时间是什么时候?

2023年湖北一级技师二级技师报名时间、考试时间是什么时候&#xff1f; 技师是属于职业资格证书&#xff0c;是各行业的高级技术人员中设置的技术职务。技师是技能高超的技术人员、能工巧匠&#xff0c;具有丰富的实践经验&#xff0c;能在本工种难度较大的工艺加工、复杂设备、…

数据治理之数据梳理

目录 1.定义 2.用途作用 3.实施方法 3.1自上而下 3.1.1数据域梳理 3.1.2数据主题梳理 3.1.3 数据实体梳理 3.1.4设计数据模型 3.1.5优点 3.1.5缺点 3.2自下而上 3.2.1需求分析 3.2.2展现 3.2.3分析逻辑 3.2.4数据建模 3.2.5优点 3.2.6缺点 1.定义 “数据梳理”即对…

C语言例程:用二维数组实现矩阵转置

用二维数组实现矩阵转置 本实例将输入的 34 矩阵转置为 43 矩阵&#xff0c;并输出结果。通过本实例&#xff0c;可以学习如何使用二 维数组。 实例解析 二维数组的定义 二维数组定义的一般形式为&#xff1a; 第一部分 基础篇 X2277 类型说明符 数组名[常量表达式][常量表…

STM32移植FreeRTOS操作系统

一、FreeRTOS源码下载&#xff08;1&#xff09;移植钱得准备前菜对吧&#xff0c;我们先来去官网瞄一瞄网址&#xff1a;https://freertos.org/zh-cn-cmn-s/ 第一步&#xff1a;点击下载FreeRTOS第二步&#xff1a;选择版本下载&#xff08;我选择稳定版本&#xff09;注&…

git 双因子身份登录遇到的问题及解决方法

git 双因子身份登录描述&#xff1a; 一般我们登录账号和密码的时候都是通过git的账号和密码直接登录即可&#xff0c;但是如果我们用双因子身份进行登录的时候发现用账号和密码登录一直登录不了&#xff0c;&#xff0c;&#xff0c;&#xff0c;有点emo了。。 git 双因子身份…

CentOS8基础篇13:yum与dnf软件包管理器

一、YUM工具的使用 yum可以说是一个管理rpm软件包的前端工具&#xff0c;其基于rpm软件包进行管理&#xff0c;能够从指定服务器自动下载rpm软件包并进行安装&#xff0c;可以自动处理依赖关系&#xff0c;并一次安装所有需要的软件包。在RHEL本地操作系统中设置相应的软件仓库…

SAP smartforms打印图片

注意&#xff1a;SAP只能上传打印bmp格式图片 1.标准程序上传 T-CODE:SE78 2.程序代码上传 DATA: P_FILENAME TYPE RLGRAP-FILENAME,P_NAME TYPE STXBITMAPS-TDNAME,P_TITLE LIKE BAPISIGNAT-PROP_VALUE,P_DOCID TYPE STXBITMAPS-DOCID,P_RESOLUTION TYPE …

案例分析之——理由Mybatis动态SQL实现复用

无复用思想的做法&#xff1a; 在没有复用思想的时候&#xff0c;就只顾着实现功能。比如开发过程中涉及到两个表的更新功能&#xff0c;每需要更新一处&#xff0c;就写一个接口&#xff0c;结果出现了写了11个接口的情况。 这样虽然功能实现了&#xff0c;可是可能自…

express+websocket实现线上聊天

1.webSocket简介 WebSocket是一种通信协议&#xff0c;可在单个TCP连接上进行全双工通信。WebSocket使得客户端和服务器之间的数据交换变得更加简单&#xff0c;允许服务端主动向客户端推送数据。在WebSocket API中&#xff0c;浏览器和服务器只需要完成一次握手&#xff0c;两…

SpringCloud之 Hystrix服务熔断

文章目录一、Hystrix 服务熔断 (已过时)&#x1f349;1.1 服务降级&#x1f349;1.2 服务熔断&#x1f349;1.3 OpenFeign 实现降级二、hystrix-dashboard 监控&#x1f34b;2.1 监控页面部署&#x1f34b;2.2 观察现象提示&#xff1a;以下是本篇文章正文内容&#xff0c;Spri…

【PySide6】信号(signal)和槽函数(slot),以及事件过滤器

说明 在PYQT中&#xff0c;父控件可以通过两种方式响应子控件的事件&#xff1a; 通过信号(signal)和槽函数(slot)机制连接子控件和父控件父控件可以通过设置eventFilter()方法来监听响应子控件的事件 一、信号(signal)和槽函数(slot) 示例 在PYQT中&#xff0c;每个组件都…

传输线的物理基础(三):传输线的瞬时阻抗

每个信号都有一个上升时间 RT&#xff0c;通常是从 10% 到 90% 的电压电平测量的。当信号沿传输线向下移动时&#xff0c;前沿在传输线上展开并具有空间范围。如果我们可以冻结时间并观察电压分布向外移动时的大小&#xff0c;我们会发现类似下图的东西。传输线上上升时间的长度…

Python中的三器一闭(详细版)

python中的三器一闭迭代器什么是迭代什么是可迭代对象判断数据是否可迭代什么是迭代器迭代器的本质使用迭代器取数据自定义迭代器生成器创建生产器的方法关键字yieldnext和send装饰器装饰器的功能定义装饰器闭包什么是闭包函数、匿名函数、闭包、对象 当做实参时的区别迭代器 …

Blazor_WASM之3:项目结构

Blazor_WASM之3&#xff1a;项目结构 Blazor WebAssembly项目模板可选两种&#xff0c;Blazor WebAssemblyAPP及Blazor WebAssemblyAPP-Empty 如果使用Blazor WebAssemblyAPP模板&#xff0c;则应用将填充以下内容&#xff1a; 一个 FetchData 组件的演示代码&#xff0c;该…

蓝桥杯-最优清零方案(2022省赛)

蓝桥杯-最优清零方案1、问题描述2、解题思路3、代码实现1、问题描述 给定一个长度为 N 的数列 1,2,⋯,A1,A2,...,ANA_1,A_2,...,A_NA1​,A2​,...,AN​ 。现在小蓝想通过若干次操作将 这个数列中每个数字清零。 每次操作小蓝可以选择以下两种之一: 1. 选择一个大于 0 的整数, 将…

杂记——18.VSCode的下载及使用

这篇文章&#xff0c;我们来讲一下VSCode&#xff0c;讲一下如何下载及使用VSCode 目录 1.VSCode的下载 1.1VSCode的简介 1.2VSCode的下载与安装 1.2.1下载 1.2.2安装 2.VSCode的使用 2.1界面 2.2基础设置 2.3禁用自动更新 2.3自动保存设置 2.4Vscode更换主题 2.5…

Hive面试题-HQL转换MapReduce底层核心逻辑剖析

视频可查看&#xff1a;https://www.bilibili.com/video/BV1RV41147Tb/?spm_id_from333.999.0.0&vd_source3ba3c3ba31427f60d734ede7a948de4a 原文地址&#xff1a;Hive学习之路 &#xff08;二十&#xff09;Hive 执行过程实例分析 - 扎心了&#xff0c;老铁 - 博客园 (c…

K_A14_012基于STM32等单片机驱动GY-25倾斜度角度模块 串口与OLED0.96双显示

K_A14_012基于STM32等单片机驱动GY-25倾斜度角度模块 串口与OLED0.96双显示一、资源说明二、基本参数参数引脚说明三、驱动说明对应程序:四、部分代码说明1、接线引脚定义1.1、STC89C52RCGY-25倾斜度角度模块1.2、STM32F103C8T6GY-25倾斜度角度模块五、基础知识学习与相关资料下…

测试按方向的分类

按方向分(都是在系统测试阶段测试的) 功能测试&#xff1a;举例说明什么是功能 性能测试 ①压力测试&#xff1a;不断地增加压力&#xff0c;从而找到系统的极限 ②负载测试&#xff1a;系统在极限工作条件下&#xff0c;最多能持续多久——可能发生内存泄漏/溢出&#xff0c;导…

angular技术(持续更新)

css类绑定[class.color-blue]"isBlue()" 如果isBlue()返回为true 这里使用color-blue的class样式style样式绑定[style.background-color]"canclick ? blue: red" 组件与模块模块的元数据*declarations: 用于指定属于这个模块的视图类&#xff08;View Cla…