这篇文章主要阅读开发需要用到的命令和工具,即以下脚本涉及的部分。
{
"dev": "node scripts/dev.js",
"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"
}
{
"dev": "node scripts/dev.js",
"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"
}
首先我们会了解这些命令所涉及在 scripts/*里的脚本,接着在逐一分析每个命令所做的事情。
相关脚本
从以上命令可以看到涉及的脚本主要有两个:dev.js
和 pre-dev-sfc.js
。
dev.js
此脚本主要是通过esbuild对要开发的包进行监听打包,修改即 rebuild。
为什么开发用 esbuild,生产用 rollup?
我们知道 esbuild 是通过Go
编写的,可以直接使用编译后的打包代码进行打包处理,所以速度非常快,可以有更好的开发体验。但是 rollup 打包出来的体积更小,且有更好的tree-shaking。其实在生产环境的打包是用 rollup with esbuild的方式打包(结合rollup-plugin-esbuild)。
使用
在执行脚本时,直接提供要打包的包名(精准匹配)即可。e.g. node scripts/dev.js runtime-dom
。包名默认为vue
此外还可以提供以下参数,脚本是通过minimist处理参数的。
-f
: 指定打包的格式,包括global
,esm-bundler
,esm-browser
,cjs
以及vue
包额外的格式global-runtime
,esm-bundler-runtime
,esm-browser-runtime
;默认global
;具体每种格式的含义在分析打包相关命令脚本时再展开。-i
: (inline)是否将依赖包一起打包进去;不提供即false
;TIP
为
false
时只对cjs
和esm-bundler(-runtime)
格式会 external 掉所有的依赖包。 其他global
,esm-browser
格式都需要 inline 所有依赖包以可以单独使用。具体可看打包格式的介绍
$ node scripts/dev.js runtime-dom -if esm-bundler
# 包名的位置并不要紧
$ node scripts/dev.js -f cjs runtime-dom
$ node scripts/dev.js runtime-dom -if esm-bundler
# 包名的位置并不要紧
$ node scripts/dev.js -f cjs runtime-dom
最后通过pnpm link
(或者其他包管理工具)进行本地 link 调试。
# /Users/xxx/xxx/vue-core/packages/vue
$ pnpm dev vue -if esm-bundler
# 在用于调试的项目根目录下 e.g. /Users/xxx/xxx/my-project/
$ pnpm link /Users/xxx/xxx/vue-core/packages/vue
# /Users/xxx/xxx/my-project/下unlink
$ pnpm unlink /Users/xxx/xxx/vue-core/packages/vue
# /Users/xxx/xxx/vue-core/packages/vue
$ pnpm dev vue -if esm-bundler
# 在用于调试的项目根目录下 e.g. /Users/xxx/xxx/my-project/
$ pnpm link /Users/xxx/xxx/vue-core/packages/vue
# /Users/xxx/xxx/my-project/下unlink
$ pnpm unlink /Users/xxx/xxx/vue-core/packages/vue
这样vue
包下的代码的改动就可以实时地体现在调试项目中了。
开发时打包的注意点
- 在
pnpm link
之前,我们需要明确要开发的包的打包方式。比如上面例子我们是esm-bundler
&&inline
(所有其他依赖包一起打包进来了)的方式打包的。 那么我们在用于调试的项目中需要使用 esm 模块的方式且只需要安装vue
即可,无需处理其他的依赖包比如runtime-dom
等。 - 在开发某个包之前,请确保已经通过
pnpm build
将其他依赖包打包好,因为在引入依赖包时都会根据以下在package.json
的配置寻找对应的入口文件。
引包规则
在vue
包的package.json
中指定入口文件的部分如下所示:
部分 package.json
{
"main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js",
"types": "dist/vue.d.ts",
"unpkg": "dist/vue.global.js",
"jsdelivr": "dist/vue.global.js",
"files": [
"index.js",
"index.mjs",
"dist",
"compiler-sfc",
"server-renderer",
"jsx-runtime",
"jsx.d.ts",
"macros.d.ts",
"macros-global.d.ts",
"ref-macros.d.ts"
],
"exports": {
".": {
"types": "./dist/vue.d.ts",
"import": {
"node": "./index.mjs",
"default": "./dist/vue.runtime.esm-bundler.js"
},
"require": "./index.js"
},
"./server-renderer": {
"types": "./server-renderer/index.d.ts",
"import": "./server-renderer/index.mjs",
"require": "./server-renderer/index.js"
},
"./compiler-sfc": {
"types": "./compiler-sfc/index.d.ts",
"import": "./compiler-sfc/index.mjs",
"require": "./compiler-sfc/index.js"
},
"./jsx-runtime": {
"types": "./jsx-runtime/index.d.ts",
"import": "./jsx-runtime/index.mjs",
"require": "./jsx-runtime/index.js"
},
"./jsx-dev-runtime": {
"types": "./jsx-runtime/index.d.ts",
"import": "./jsx-runtime/index.mjs",
"require": "./jsx-runtime/index.js"
},
"./jsx": "./jsx.d.ts",
"./dist/*": "./dist/*",
"./package.json": "./package.json",
"./macros": "./macros.d.ts",
"./macros-global": "./macros-global.d.ts",
"./ref-macros": "./ref-macros.d.ts"
}
}
{
"main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js",
"types": "dist/vue.d.ts",
"unpkg": "dist/vue.global.js",
"jsdelivr": "dist/vue.global.js",
"files": [
"index.js",
"index.mjs",
"dist",
"compiler-sfc",
"server-renderer",
"jsx-runtime",
"jsx.d.ts",
"macros.d.ts",
"macros-global.d.ts",
"ref-macros.d.ts"
],
"exports": {
".": {
"types": "./dist/vue.d.ts",
"import": {
"node": "./index.mjs",
"default": "./dist/vue.runtime.esm-bundler.js"
},
"require": "./index.js"
},
"./server-renderer": {
"types": "./server-renderer/index.d.ts",
"import": "./server-renderer/index.mjs",
"require": "./server-renderer/index.js"
},
"./compiler-sfc": {
"types": "./compiler-sfc/index.d.ts",
"import": "./compiler-sfc/index.mjs",
"require": "./compiler-sfc/index.js"
},
"./jsx-runtime": {
"types": "./jsx-runtime/index.d.ts",
"import": "./jsx-runtime/index.mjs",
"require": "./jsx-runtime/index.js"
},
"./jsx-dev-runtime": {
"types": "./jsx-runtime/index.d.ts",
"import": "./jsx-runtime/index.mjs",
"require": "./jsx-runtime/index.js"
},
"./jsx": "./jsx.d.ts",
"./dist/*": "./dist/*",
"./package.json": "./package.json",
"./macros": "./macros.d.ts",
"./macros-global": "./macros-global.d.ts",
"./ref-macros": "./ref-macros.d.ts"
}
}
main: 指定
cjs
方式引入时对应的入口文件,值为index.js
。index.js
main
指定的index.js
里实际引入的就是打包好后的vue.cjs.(prod).js
文件。因此我们需要确保dist
目录下有对应的vue.cjs.(prod).js
文件。js// index.js 文件内容 'use strict' if (p
rocess.env. NODE_ENV === 'production') { module.exports = require('./dist/vue.cjs.prod.js') } else { module.exports = require('./dist/vue.cjs.js') }// index.js 文件内容 'use strict' if (p
rocess.env. NODE_ENV === 'production') { module.exports = require('./dist/vue.cjs.prod.js') } else { module.exports = require('./dist/vue.cjs.js') }module: 指定
esm
模块方式引入时对应的入口文件,值为dist/vue.runtime.esm-bundler.js
。unpkg: 这是一个为了支持UNPKG的 CDN 而使用的,可以看到其指定的文件就是通过iife的方式打包的
dist/vue.global.js
;这样只要上传到了 npm,我们就可以通过unpkg.com/:package@:version/:file
(e.g. https://unpkg.com/@vue/runtime-dom)的方式通过CDN去获取对应包的资源了。不过这里为啥不用dist/vue.global.prod.js
?jsdelivr: 和
unpkg
类似,也是为了支持jsdelivr。不过jsdelivr
会代码压缩后返回vue.global.js
。types: 指定包的类型声明文件。
files: 上传到 npm 需要包含的文件。这里解释的不错。
exports: 指定除了主入口文件之外还可以通过相对路径(相对包的根目录)指定引入其他的文件;其中可以通过
types
,import
和require
来进一步指定不同模块方式下的引入对应文件。例如对于./server-renderer
json{ "./server-renderer": { "types": "./server-renderer/index.d.ts", "import": "./server-renderer/index.mjs", "require": "./server-renderer/index.js" } }
{ "./server-renderer": { "types": "./server-renderer/index.d.ts", "import": "./server-renderer/index.mjs", "require": "./server-renderer/index.js" } }
js// esm: ./server-renderer/index.mjs import { renderToString } from 'vue/server-renderer' // cjs: ./server-renderer/index.js const { renderToString } = require('vue/server-renderer')
// esm: ./server-renderer/index.mjs import { renderToString } from 'vue/server-renderer' // cjs: ./server-renderer/index.js const { renderToString } = require('vue/server-renderer')
对__esModule 标识的补充
在打包出来的.cjs.js
中,我们可以看到前面有这么一句
Object.defineProperty(exports, '__esModule', { value: true })
Object.defineProperty(exports, '__esModule', { value: true })
这是因为在rollup
配置了output.esModule: true
,那么在打包时就会对打包的模块标记成es
模块。
为了弄清楚为什么要这个标记以及这个标记是给谁用的,我们来看一个例子:
TIP
以下是在 node 环境下跑的,为了使用es
模块化,可以设置 package.json 的 type 为 module,或者命名为.mjs 文件;下面我们就直接写.js 后缀表示了
我们有index.js
, es.js
和cjs.js
文件,现在希望在es.js
用es
模块化方式导出数据,在cjs.js
用CommonJs
方式导出数据,然后index.js
引入数据。
导出的数据有默认数据name
,以及具名数据age
。不过到这里应该就有个疑问:CommonJs
该如何以默认的方式导出数据?似乎没有。那我们就用module.exports.default
来暂且代替试试吧。
import defaultName, { age } from './es.js'
console.log(defaultName, age) // 'leo' 16
import defaultName, { age } from './cjs.js'
console.log(defaultName, age) // { default: 'leo', age: 16 } 16
import defaultName, { age } from './es.js'
console.log(defaultName, age) // 'leo' 16
import defaultName, { age } from './cjs.js'
console.log(defaultName, age) // { default: 'leo', age: 16 } 16
const name = 'leo'
const age = 16
export default name
export { age }
const name = 'leo'
const age = 16
export default name
export { age }
const name = 'leo'
const age = 16
module.exports.default = name
module.exports.age = age
const name = 'leo'
const age = 16
module.exports.default = name
module.exports.age = age
结果发现从cjs.js
导入的默认数据就是整个module.exports
对象。出现这样的差异,是因为CommonJS 方式没有提供默认导出的方式。
要解决这个问题,一些打包工具提出了用__esModule
属性来对cjs模块
进行标识,来告诉打包工具在用es
模块化引入该cjs模块
时,默认导出数据就是module.exports.default
上的数据。所以上面的两个问题就有了解答:
- 为什么要这个标记:
CommonJs
没有提供默认导出数据的方式,es
模块化导入时会产生差异 - 这个标记是给谁用的: 给打包工具识别的,所以本质是一种约定俗成的解决方案。
那么下面我们就用rollup
来对其进行打包,看看能不能让cjs.js
拥有默认导出的能力~
我们用到了@rollup/plugin-commonjs插件来处理es
引入cjs
的情况。
'use strict';
var cjs = {};
const name = 'leo';
const age = 16;
cjs.default = name;
var age_1 = cjs.age = age;
console.log(cjs, age_1); // { default: 'leo', age: 16 } 16
exports.age = age_1;
exports.name = cjs;
'use strict';
var cjs = {};
const name = 'leo';
const age = 16;
cjs.default = name;
var age_1 = cjs.age = age;
console.log(cjs, age_1); // { default: 'leo', age: 16 } 16
exports.age = age_1;
exports.name = cjs;
import commonjs from '@rollup/plugin-commonjs'
export default {
input: 'index.js',
output: {
file: 'bundle.js',
format: 'cjs' // 这个是最终打包成的格式,是什么对例子没有关系
// esModule: true 设为true就对最终打包结果进行es标识,默认为false
},
plugins: [commonjs()]
}
import commonjs from '@rollup/plugin-commonjs'
export default {
input: 'index.js',
output: {
file: 'bundle.js',
format: 'cjs' // 这个是最终打包成的格式,是什么对例子没有关系
// esModule: true 设为true就对最终打包结果进行es标识,默认为false
},
plugins: [commonjs()]
}
发现结果还是一样,因为还没有对cjs.js
加标识呢。我们看看加了标识后的打包结果:
'use strict';
var cjs = {};
Object.defineProperty(cjs, '__esModule', { value: true }); // 一起打包进来了。。。不过起作用了
const name = 'leo';
const age = 16;
var _default = cjs.default = name; // 取了default属性作为默认导出
var age_1 = cjs.age = age;
console.log(_default, age_1); // 'leo' 16
exports.age = age_1;
exports.name = _default;
'use strict';
var cjs = {};
Object.defineProperty(cjs, '__esModule', { value: true }); // 一起打包进来了。。。不过起作用了
const name = 'leo';
const age = 16;
var _default = cjs.default = name; // 取了default属性作为默认导出
var age_1 = cjs.age = age;
console.log(_default, age_1); // 'leo' 16
exports.age = age_1;
exports.name = _default;
Object.defineProperty(exports, '__esModule', { value: true });
const name = 'leo'
const age = 16
module.exports.default = name
module.exports.age = age
Object.defineProperty(exports, '__esModule', { value: true });
const name = 'leo'
const age = 16
module.exports.default = name
module.exports.age = age
TIP
如果cjs.js
里没有module.exports.default = name
这一句,那么即使标识了也不会有默认导出的,行为会和没有标识一样,因为打包工具找不到default
属性的值。
至此我们基本明白__esModule
的作用了。
实现
具体的脚本代码并不多,主要是明确 esbuild 的配置,接着调用 esbuild 提供的 api 即可。
其中有几点特殊处理的情况:
vue-compat
包打包的文件target
名是vue
而不是其本身。jsconst outfile = resolve( __dirname, `../packages/${target}/dist/${ target === 'vue-compat' ? `vue` : target }.${postfix}.js`
const outfile = resolve( __dirname, `../packages/${target}/dist/${ target === 'vue-compat' ? `vue` : target }.${postfix}.js`
- 在处理
externals
(打包时不包含的依赖包)时,compiler-sfc
包需要额外 external 掉@vue/consolidate
里的依赖包(devDependencies),但包含@vue/consolidate
包本身(打包格式不是cjs
和esm-bundler(-runtime)
格式时)。
脚本的书写方式
- 通过
// ts-check
可以让对应的 js 文件纳入 ts 检查
// @ts-check
let x = 3
x = '' // Type 'string' is not assignable to type 'number'.ts(2322)
// @ts-check
let x = 3
x = '' // Type 'string' is not assignable to type 'number'.ts(2322)
- 使用
esm
方式导入;有两种方式可以实现在node
环境使用esm
引包方式:1. 通过指定package.json
里的type
为module
;2. 通过命名文件后缀为.mjs
;以下是在node
环境使用esm
时一些区别调整:__dirname
不再适用,应该为import { dirname } from 'node:path'
里的dirname
函数方法jsimport { dirname } from 'node:path' import { fileURLToPath } from 'node:url' // i
mport.meta.url即当前执行脚本的fileUrl: e.g. file:///Users/xxx/xxx/index.js // fileURLToPath(import.meta.url) 之后就是 /Users/xxx/xxx/index.js const __dirname = dirname(fileURLToPath(import.meta.url))import { dirname } from 'node:path' import { fileURLToPath } from 'node:url' // i
mport.meta.url即当前执行脚本的fileUrl: e.g. file:///Users/xxx/xxx/index.js // fileURLToPath(import.meta.url) 之后就是 /Users/xxx/xxx/index.js const __dirname = dirname(fileURLToPath(import.meta.url))- 动态导入可以通过
createRequire
方法实现jsimport { createRequire } from 'node:module' const require = createRequire(import.meta.url) const pkg = require('./package.json')
import { createRequire } from 'node:module' const require = createRequire(import.meta.url) const pkg = require('./package.json')
pre-dev-sfc.js
此脚本主要是在执行dev-sfc
之前确保其所依赖的包都已经打包好了。否则就执行npm run build-compiler-cjs
以确保依赖包的打包工作已完成。
核心包开发命令
dev
对应于node scripts/dev.js
。
前面我们知道dev.js
默认开发的包是vue
,打包方式是global(iife)
,所以此命令就是用来开发global
打包的vue
的。
dev-esm
对应于node scripts/dev.js -if esm-bundler-runtime
。
即开发的包是vue
,打包方式是esm-bundler-runtime
,并且是将所有依赖包都打包进来的 inline 模式。
因为esm-bundler 格式下生产环境打包回external
掉所有依赖包的,而这里提供inline
选项就可以在开发时更好的调试。
辅助工具的开发
dev-compiler
对应于run-p \"dev template-explorer\" serve
,用于开发编译器转译工具。
其中run-p
是指并行运行pnpm dev template-explorer
和pnpm serve
命令,用的是npm-run-all这个工具。
- pnpm dev template-explorer:相当于
node scripts/dev.js template-explorer
所以此时使用global
格式打包的template-explorer
这个包。 - pnpm serve:基于项目根目录启动一个静态资源服务器,这样我们就可以直接访问本地进行开发
http://localhost:5000/packages/template-explorer/local.html
(对应的就是open
命令)如何访问 template-explorer
template-explorer
包下有两个 html 文件:index.html
和local.html
。 其两者的区别就是 local 引用的资源是本地的,而 index 是用 CDN 的资源。html<title>Vue Template Explorer</title> <link rel="stylesheet" data-name="vs/editor/editor.main" href="https://unpkg.com/monaco-editor@0.20.0/min/vs/editor/editor.main.css" // index href="./node_modules/monaco-editor/min/vs/editor/editor.main.css" // local /> <link rel="stylesheet" href="./style.css" /> <div id="header"></div> <div id="source" class="editor"></div> <div id="output" class="editor"></div> <script src="https://unpkg.com/monaco-editor@0.20.0/min/vs/loader.js"></script> // index <script src="./node_modules/monaco-editor/min/vs/loader.js"></script> // local <script> require.config({ paths: { vs: 'https://unpkg.com/monaco-editor@0.20.0/min/vs' // index vs: './node_modules/monaco-editor/min/vs' // local } }) </script> <script src="./dist/template-explorer.global.js"></script> <script> require(['vs/editor/editor.main'], init /* injected by build */) </script>
<title>Vue Template Explorer</title> <link rel="stylesheet" data-name="vs/editor/editor.main" href="https://unpkg.com/monaco-editor@0.20.0/min/vs/editor/editor.main.css" // index href="./node_modules/monaco-editor/min/vs/editor/editor.main.css" // local /> <link rel="stylesheet" href="./style.css" /> <div id="header"></div> <div id="source" class="editor"></div> <div id="output" class="editor"></div> <script src="https://unpkg.com/monaco-editor@0.20.0/min/vs/loader.js"></script> // index <script src="./node_modules/monaco-editor/min/vs/loader.js"></script> // local <script> require.config({ paths: { vs: 'https://unpkg.com/monaco-editor@0.20.0/min/vs' // index vs: './node_modules/monaco-editor/min/vs' // local } }) </script> <script src="./dist/template-explorer.global.js"></script> <script> require(['vs/editor/editor.main'], init /* injected by build */) </script>
引用的打包资源就是
./dist/template-explorer.global.js
。
dev-sfc
对应于run-s dev-sfc-prepare dev-sfc-run
,用于开发在线演练场。
其中run-s
是指串行执行命令。
- dev-sfc-prepare: 即在执行
dev-sfc
之前确保其所依赖的包都已经打包好了。否则就执行npm run build-compiler-cjs
。 - 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
。即并行执行开发sfc-playground
依赖的compiler-sfc
,vue
和server-renderer
这几个包,以及启动sfc-playground
的开发服务器。
总结
- 介绍了
dev.js
脚本的使用,可以监听并以指定参数打包对应包从而进行开发 - 介绍了基本的引包规则,主要包括
main
,module
,unpkg
和jsdelivr
等字段 - 补充说明了打包工具通过__esModule标识解决
CommonJs
没有默认导出的问题 - 仓库编写脚本的方式
- 简单介绍了每个
scripts
命令的用途