乐趣区

关于前端:源码库跟着-Vue3-学习前端模块化

Vue3为了反对不同的用户群体,提供了多种模块化计划,这样使得咱们在应用的 Vue 的应用能够有很多种形式;

例如咱们能够间接在 html 中应用 script 标签引入 Vue,也能够前端工程化工具,例如webpackrollup 等打包工具,将 Vue 打包到咱们的我的项目中,甚至还能够在 nodejs 中应用服务端渲染的形式来应用Vue

明天就跟着 Vue3 的源码,一起来看看 Vue3 是如何反对这些模块化计划的,以及咱们在应用 Vue 的时候,应该如何抉择适合的模块化计划;

这里是源码浏览第一篇文章,感兴趣的小伙伴能够点击退出群聊一起独特成长提高,文章在掘金是首发,所以图片会带掘金的水印,无任何引流的意思。

筹备工作

这是第一篇对于 Vue3 源码的文章,所以咱们须要先筹备一些工作,能力开始咱们的源码学习之旅;

源码 clone

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

源码下载的形式有很多种,这里倡议大家下载到本地,不便前面的调试和学习;

能够间接在 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 命令;

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

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

await buildAll(allTargets)
checkAllSizes(allTargets)

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

它获取的中央是通过 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
  • compiler-core.cjs.prod.js
  • compiler-core.esm-bundler.js

模块化的标准不同的方面就不多说了,最直观的了解就是导入和导出的形式不同,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
  • compiler-core.cjs.prod.js__DEV__false
  • compiler-core.esm-bundler.js__DEV__process.env.NODE_ENV !== 'production'

而依据不同的替换值,最终生成的代码也不雷同,就比方下面的示例。

参考: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 的源码,可能会有一些谬误或者了解不到位的中央,欢送大家斧正,也欢送大家一起来学习,一起来交换,一起来提高;

目前也创立了一个群提供一个平台,大家有什么倡议或者问题都能够在群里提出来,一起来探讨,一起来提高,点击退出群聊一起独特成长提高。

退出移动版