这篇文章主要阅读开发需要用到的命令和工具,即以下脚本涉及的部分。
{
"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 (process.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 (process.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-rendererjson{ "./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 } 16import 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 } 16const 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 = ageconst 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 = ageObject.defineProperty(exports, '__esModule', { value: true });
const name = 'leo'
const age = 16
module.exports.default = name
module.exports.age = ageTIP
如果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' // import.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' // import.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命令的用途
vue3-reading