esbuild基本使用,及简单介绍vue3源码reactivity的打包 - 源码11
初始化环境
创建文件夹,开始试验
mkdir esbuild-demo
cd esbuild-demo
pnpm init
code .
生成
{
"name": "esbuild-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "zhm",
"license": "ISC",
"devDependencies": {
"esbuild": "0.19.3"
}
}
配置最简单打包
1. package.json加type
package.json加type是module,可以解析import
语法。
"main": "index.js",
"type": "module",
2. 配置packages文件夹
新建packages文件夹
,这个文件夹里可以新建n个包,这里新建add
的包,然后建一个index.ts
。
// packages/add/index.ts
export function add(a: number, b: number) {
return a + b
}
3. 配置scripts文件夹
新建scripts文件夹
,这个文件夹里主要是,打包脚本,这里新建dev.js
先安装esbuild
pnpm install --save-exact --save-dev esbuild
dev.js
内容如下:
import esbuild from 'esbuild';
let ctx = await esbuild.context({
// 入口
entryPoints: ['./packages/add/index.ts'],
// 输出文件
outfile: './packages/add/dist/add.js',
})
// 每次entryPoints变动,就自动生成新的文件
await ctx.watch()
4. 配置命令
package.json
里配置命令即可
"scripts": {
"dev": "node ./scripts/dev.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
然后运行
pnpm run dev
5.稍微重构下文件目录
正常一个包,通常的结构是index.js package.json src/index.ts src/...
生成package.json
cd packages/add
pnpm init
其他文件调整:
额外注意dev.js
里,需要配置bundle:true
,这样依赖文件会打包到一个文件中
let ctx = await esbuild.context({
// ...
// bundle是将所有的依赖打包到一个文件中
bundle:true,
})
不然打包出来的结果就是export { add } from './add';
说说format
format有三种
- iife ----- 自执行函数cjs是commonjs,esm是es6模块,xx.global.js
- cjs ----- commonjs(require module.exports),xx.cjs.js
- esm ----- es6模块(import export),xx.esm.js
多说下esm,esm我们常见到的其实还分为两种:ESM bundler 和 ESM browser ,主要差别在于目标环境。
-
ESM-Bundler: 这是一个为使用 JavaScript 打包工具(如 Webpack 和 Rollup)准备的 ESM 转译版代码包。如果你使用的项目使用了这些构建工具之一,那么使用 esm-bundler 是最好的选择。从这个版本打包可以更好地去除多余的代码,优化项目体积。依赖的包不会被打进输出文件里,因为被external掉了。
-
ESM-Browser: 这是一个为浏览器环境准备的 ESM 代码包,所有代码都已通过 Babel 转译,可以在浏览器环境中直接运行,不需要使用打包工具。这种情况通常在简单的单页应用中使用,或者在完全没有使用构建工具的开发环境中使用。依赖的包会被打进输出文件里。
所以如果在浏览器中使用那就是xx.esm-browser.js
。
打包vue的reactivity
安装插件@types/node和minimist
# 可以这么写,import { resolve } from 'node:path'
pnpm i -D @types/node
# 可以解析参数
pnpm i -D minimist
解析参数的多说说
如果运行下面的命令:
node ./scripts/dev.js a b c d -minify --format=iife`
用process.argv
获取,是这样的:
[
'/usr/local/bin/node',
'/Users/zhm/.../scripts/dev.js',
'a',
'b',
'c',
'd',
'-minify',
'--format=iife'
]
用minimist(process.argv)
获取,是这样的:
{
_: [
'/usr/local/bin/node',
'/Users/zhm/.../scripts/dev.js',
'a',
'b',
'c',
'd'
],
minify: true,
format: 'iife'
}
minimist会将命令行里-
,--
单独用一个键值表示。对于node命令来说,前两个基本都是node xx.ts
,所以一般取后面的minimist(process.argv.slice(2))
简单reactivity的打包
略微简化版的vue源码中的dev.js
import esbuild from 'esbuild'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { createRequire } from 'node:module'
import minimist from 'minimist'
const args = minimist(process.argv.slice(2))
const target = args._[0] || 'reactivity'
const format = args.f || 'global'
const require = createRequire(import.meta.url) // import.meta.url当前文件路径
const pkg = require(`../packages/${target}/package.json`) // pkg就是当前文件夹下的package.json的对象
const outputFormat = format.startsWith('global')
? 'iife'
: format === 'cjs'
? 'cjs'
: 'esm'
const postfix = format.endsWith('-runtime')
? `runtime.${format.replace(/-runtime$/, '')}`
: format
const __dirname = dirname(fileURLToPath(import.meta.url)) // __dirname当前文件的文件夹路径
const outfile = resolve(
__dirname,
`../packages/${target}/dist/${
target === 'vue-compat' ? `vue` : target
}.${postfix}.js`
)
esbuild
.context({
entryPoints: [resolve(__dirname, `../packages/${target}/src/index.ts`)],
outfile,
bundle: true,
sourcemap: true,
format: outputFormat,
globalName: pkg.buildOptions?.name || target,
platform: format === 'cjs' ? 'node' : 'browser',
// define是将代码里这些key替换成value
define: {
__VERSION__: `"${pkg.version}"`,
__DEV__: `true`,
}
})
.then(ctx => ctx.watch())
reactivity的目录结构如下:
运行node ./scripts/dev.js
之后,在dist文件,就已经生成了。
新建个index.html
,输入以下内容,浏览器正常运行
<script src="./reactivity.global.js"> </script>
<script>
const { reactive, effect } = VueReactivity
// debugger //加个debugger就能调试源码了
const state = reactive({ count: 0 })
effect(() => {
console.log(state.count)
})
const timer = setInterval(() => {
state.count++
if(state.count>6){
clearInterval(timer)
}
}, 1000)
</script>
如果执行,node ./scripts/dev.js reactivity -f=esm
,就会生成reactivity.esm.js
,index.html内容也可以换成module写法:
<script type="module">
import { reactive, effect } from './reactivity.esm.js'
const state = reactive({ count: 0 })
// ...
</script>
同样,如果执行node ./scripts/dev.js add
也是没问题的!
reactivity其他文件的代码
package.json
{
"name": "@vue/reactivity",
"version": "3.3.4",
"description": "@vue/reactivity",
"main": "index.js",
"module": "dist/reactivity.esm-bundler.js",
"types": "dist/reactivity.d.ts",
"unpkg": "dist/reactivity.global.js",
"jsdelivr": "dist/reactivity.global.js",
"files": [
"index.js",
"dist"
],
"sideEffects": false,
"repository": {
"type": "git",
"url": "git+https://github.com/vuejs/core.git",
"directory": "packages/reactivity"
},
"buildOptions": {
"name": "VueReactivity",
"formats": [
"esm-bundler",
"esm-browser",
"cjs",
"global"
]
},
"keywords": [
"vue"
],
"author": "Evan You",
"license": "MIT",
"bugs": {
"url": "https://github.com/vuejs/core/issues"
},
"homepage": "https://github.com/vuejs/core/tree/main/packages/reactivity#readme",
"dependencies": {
"@vue/shared": "3.3.4"
}
}
src/index.ts
export * from './reactive'
export * from './effect'
src/effect.ts
// track的时候,需要拿到effect,所以用下全局变量存放effect
export let activeEffect: ReactiveEffect | null = null;
// 建立类,方便存放fn,和运行
/**
* fn是函数,收集属性依赖,scheduler是函数,属性依赖变化的时候,执行
* 属性deps是个二维数组,结构是 [[_effect1,_effect2],[_effect3,_effect2],]
*/
export class ReactiveEffect {
// 是否主动执行
private active = true
// 新增deps
deps = []
parent
constructor(private fn, public scheduler) {
}
run() {
if (!this.active) {
const res = this.fn()
// 这里watch的时候,fn是函数返回字段,需要返回值
return res;
}
this.parent = activeEffect
activeEffect = this;
// 运行之前,清除依赖
clearupEffect(this);
const res = this.fn();
activeEffect = this.parent
this.parent && (this.parent = null);
return res
}
stop() {
if (this.active) {
// 清除依赖
clearupEffect(this);
// 标记不主动执行
this.active = false;
}
}
}
// 清除依賴
function clearupEffect(_effect) {
// deps结构是 [[_effect1,_effect2],[_effect3,_effect2],],假设去掉_effect2
const deps = _effect.deps
for (let i = 0; i < deps.length; i++) {
deps[i].delete(_effect)
}
// 同时deps置空,保证每次effect运行都是新的属性映射
_effect.deps.length = 0
}
// }
export function effect(fn, options) {
const _effect = new ReactiveEffect(fn, options?.scheduler);
_effect.run();
// runner是个函数,等同于_effect.run,注意绑定this
const runner = _effect.run.bind(_effect)
// runner还有effect属性,直接赋值就好
runner.effect = _effect
return runner
}
// 本质是找到属性对应的effect,但属性存在于对象里,所以两层映射
// 响应性对象 和 effect的映射,对象属性和effect的映射
// targetMap = { obj:{name:[effect],age:[effect]} }
export const targetMap: WeakMap<object, Map<string, Set<ReactiveEffect>>> = new WeakMap();
// 让属性 订阅 和自己相关的effect,建立映射关系
export function track(target, key) {
if (!activeEffect) {
return;
}
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
trackEffects(dep)
}
/**
* dep收集effect
*/
export function trackEffects(dep: Set<ReactiveEffect>) {
if (activeEffect && !dep.has(activeEffect)) {
// 收集effect
dep.add(activeEffect)
// effect同样收集下dep
// @ts-ignore
activeEffect?.deps?.push(dep)
}
}
/**
* dep执行触发effect
*/
export function triggerEffects(dep: Set<ReactiveEffect>) {
[...dep].forEach((effect) => {
const isRunning = activeEffect === effect
if (!isRunning) {
effect.scheduler ? effect.scheduler() : effect.run()
}
});
}
// 属性值变化的时候,让相应的effect执行
export function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) {
return;
}
const dep = depsMap.get(key);
if (!dep) {
return;
}
// 触发执行
triggerEffects(dep)
}
src/reactive.ts
// import { isObject } from './shared'
import { track, trigger } from './effect';
export const isObject = (param) => {
return typeof param === 'object' && param !== null
}
export const isFunction = (param) => {
return typeof param === 'function';
}
const __v_isReactive = '__v_isReactive'
// 是不是响应式对象
export const isReactive = (param) => param[__v_isReactive];
// 代理对象的映射
export const reactiveMap = new WeakMap()
export function reactive(target) {
// 如果不是对象,直接返回
if (!isObject(target)) {
return
}
// 如果已经代理过了,直接返回
if (reactiveMap.has(target)) {
return reactiveMap.get(target)
}
// 如果已经代理过了,__v_isReactive肯定是true,那直接返回
if (target[__v_isReactive]) {
return target
}
// 如果是ref对象,直接返回value
if (target.__v_isRef) {
return target.value
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
// 这里埋点,加上__v_isReactive属性,标识已经代理过了
if (key === __v_isReactive) {
return true
}
// Reflect将target的get方法里的this指向proxy上,也就是receiver
const res = Reflect.get(target, key, receiver);
// 依赖收集
track(target, key)
// 如果是对象,递归代理
if(isObject(res)) {
return reactive(res)
}
return res;
},
set(target, key, value, receiver) {
const oldValue = target[key]
const r = Reflect.set(target, key, value, receiver);
// 响应式对象发生变化的时候,触发effect执行
if(oldValue !== value) {
trigger(target, key)
}
return r;
},
})
// 如果没有代理过,缓存映射
reactiveMap.set(target, proxy)
return proxy
}
转载自:https://juejin.cn/post/7280436036948344866