Vue3
为了支持不同的用户群体,提供了多种模块化方案,这样使得我们在使用的Vue
的使用可以有很多种方式;
例如我们可以直接在html
中使用script
标签引入Vue
,也可以前端工程化工具,例如webpack
、rollup
等打包工具,将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
的开发环境,截取自Vue3
的package.json
文件;
version
:Vue3
的版本号packageManager
:Vue3
的包管理工具,这里是pnpm
,版本号是7.1.0
engines
:Vue3
的开发环境,这里是node
,版本号是>=16.11.0
所以我们在学习Vue3
的源码之前,需要先安装pnpm
和node
,并且需要注意它们的版本号;
这里怎么安装pnpm
和node
,大家自行查询,应该不会有什么问题;
环境准备好了之后,我们通过pnpm
安装Vue3
的依赖:
cd [源码目录]
pnpm install
Vue3 模块化方案
上面的准备工作做完了之后,我们就可以开始学习Vue3
的源码了;
我们今天需要了解的是Vue3
的模块化实现,模块化离不开构建工具,Vue
使用的rollup
作为构建工具;
先不管使用的是什么构建工具,最终是一定要通过构建工具来打包我们的代码,而打包的命令通常是封装在package.json
中的;
所以我们直接看一下Vue3
的package.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
:打部署Vue3
的SFC
的playground
:https://sfc.vuejs.orgbuild-compiler-cjs
:打包compiler
的cjs
包,这个包是Vue3
的编译器build-runtime-esm
:打包runtime
的esm
包,这个包是Vue3
的运行时build-ssr-esm
:打包ssr
的esm
包,这个包是Vue3
的服务端渲染build-sfc-playground-self
:这个是单独打包Vue3
的SFC
的playground
的包
可以看到这些命令都是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
文件中的private
和buildOptions
来判断是否是一个需要打包的文件夹,如果是的话,就将这个文件夹的名字添加到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
命令的时候,没有传递任何参数,所以formats
、buildTypes
、prodOnly
、sourceMap
都是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
包的配置,其他包的配置类似:
这里只是列出配置,不用看懂,只需要关心input
和output
的配置,还有最终生成配置结构,由于我这里是直接导出的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
指向的都是同一个入口文件,不同的是output
的file
和format
不同。
前端模块化
回头看看我这篇文章的标题,跟着Vue3
来学习前端模块化,这里我们已经粗窥Vue3
的模块化了;
上面生成的配置文件中有主要生成的esm
和commonjs
两种模块化的文件,先简单介绍下这两种模块化的区别:
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
模块化的规范不同的方面就不多说了,最直观的理解就是导入和导出的方式不同,esm
是import
导入,commonjs
是require
导入,iife
是script
标签导入。
而通过上面的三个文件的对比,直接看defaultOnWarn
函数的区别:
compiler-core.cjs.js
:defaultOnWarn
函数是直接使用console.warn
打印内容的;compiler-core.cjs.prod.js
:defaultOnWarn
函数里面是空的,没有任何内容;compiler-core.esm-bundler.js
:defaultOnWarn
函数多了一个环境检测,如果是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
规范的,通常是提供给webpack
、vite
等打包工具使用的,所以__DEV__
的值为process.env.NODE_ENV !== 'production'
,这样就给这些打包工具提供了一种处理机制,让打包工具来决定是否移除;
除了上述的这两种打包方案,vue
还提供了一种iife
的打包方案,这种打包方案通常是用来在浏览器环境下使用的;
在这种方案下__DEV__
的值会是什么呢?而最终生成的文件和上面的两种有什么区别呢?这里就留给感兴趣的同学自己动动手,编译一下vue
,输出一些日志信息,看看最终生成的文件有什么区别。
总结
通过分析vue
源码的package.json
的scripts
配置,我们了解到vue
是通过nodejs
的execa
包,对rollup
的命令进行了封装,从而实现了对rollup
的命令和配置动态化;
execa
是基于child_process
的封装,可以让我们更加方便的执行命令,vue
利用这个特性,使用多线程的方式,同时执行多个rollup
命令,从而加快了打包速度;
rollup
的配置文件是通过rollup.config.js
来进行配置的,而vue
通过各种参数的封装,实现了对rollup
配置的动态化,从而实现了对不同的打包方案的适配;
考虑到vue
可能会在不同的环境下运行,vue
通过配置rollup
的output.format
参数,可以实现对不同的打包方案的适配,比如cjs
、esm
、iife
等,从而实现对浏览器环境、node
环境、webpack
、vite
等打包工具的适配;
考虑到产物最终可能需要区分开发环境和生产环境,vue
通过配置rollup
的replace
插件,可以实现对__DEV__
的动态替换,从而实现对开发环境和生产环境的适配;
宣传
这是我写的关于vue3
源码系列的第一章,从打包开始,后面的章节会一步步深入到vue3
的源码中,逐步了解vue3
的实现原理;
目前的节奏准备是一周一篇,学习趋势是后面会跟着官网的api
文档,看看每个api
后面是怎么实现的,下一篇文章会就是正式开始,从createApp
开始,也是我们使用vue3
的入口;
个人也不知道大家更能接受什么方式理解源码,然后这篇文章第一部分逐行分析scripts/build.js
的代码,第二部分是对rollup.config.js
的直接分析,不知道大家更能接受那种方式;
自己现在也是在学习vue3
的源码,可能会有一些错误或者理解不到位的地方,欢迎大家指正,也欢迎大家一起来学习,一起来交流,一起来进步;
目前也创建了一个群提供一个平台,大家有什么建议或者问题都可以在群里提出来,一起来讨论,一起来进步,点击加入群聊一起共同成长进步。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。