这是我参与8月更文挑战的第19天,活动详情查看:8月更文挑战
前言
上篇关于 Vite 的介绍中说过,Vite 的核心代码包括 client 和 node 双端,本篇主要介绍一下 client 端的处理。
目录结构
关于 client 部分的代码目录如下:
- client
- client.ts
- env.ts
- overlay.ts
本篇先介绍一下后两个文件。
env.ts
在 env 中,首先定义了 context 的值。
const context = (() => {
if (typeof globalThis !== 'undefined') {
return globalThis
} else if (typeof self !== 'undefined') {
return self
} else if (typeof window !== 'undefined') {
return window
} else {
return Function('return this')()
}
})()
由于 context 的值需要经过条件判断才能确定,这里使用一个自执行函数包裹其中判断部分的代码。
依次从 globalThis、self、window、Function('return this')。
然后做了一个赋值的操作。
const defines = __DEFINES__
__DEFINES__ 在当前的上下文并不存在,这段赋值语句的目的是,在后续服务端返回内容时,会通过插件将 __DEFINES__ 进行字符串替换,类似于:
const code = `const defines = __DEFINES__`
code.replace('__DEFINES__', {})
由于 Vite 是使用 rollup 进行打包构建的,在没有配置其他插件的情况下,rollup 不会对 __DEFINES__ 进行特殊处理,所以可以理解为,经过打包构建后的代码仍然包含 __DEFINES__ 这一当时并不存在的变量。
在 env.ts 的最后一段:
Object.keys(defines).forEach((key) => {
const segments = key.split('.')
let target = context
for (let i = 0; i < segments.length; i++) {
const segment = segments[i]
if (i === segments.length - 1) {
target[segment] = defines[key]
} else {
target = target[segment] || (target[segment] = {})
}
}
})
在分析这段代码前,先假设 __DEFINES__ 的值是:
__DEFINES__ = {
'process.env.NODE_ENV': "production"
}
然后可以看到最后一段代码做的处理是:
先将 process.env.NODE_ENV 分割为 ['process', 'env', 'NODE_ENV'],并赋给 segments。
然后遍历 segments 数组,然后依次进行赋值: this['process']、this['process']['env']、this['process']['env']['NODE_ENV']。
比较巧妙的点在于,第一个 process 属性的取值方式是通过 context,即当前上下文的 this。
总结下来,其实就是将 __DEFINES__ 中的字符串赋值操作,转化成真的 javascript 变量赋值。
也可以理解为和 Webpack.definePlugin 的作用类似。
overlay.ts
首先看代码:
const tempalte = `
<style>
// 样式
</style>
<div class="window">
<pre>...</pre>
</div>
`
export class ErrorOverlay extends HTMLElement {
// ...
}
export const overlayId = 'vite-error-overlay'
!customElements.get(overlayId) && customElements.define(overlayId, ErrorOverlay)
首先定义了包含了 样式和模板的字符串 template。
然后导出了一个 WebComponents 组件、overlay 组件的 id、并且检查是否已经存在该组件,如果不存在就通过 customElements 注册 ErrorOverlay 组件。
通过 ErrorOverlay 组件的名称可以猜到,这个组件用来显示异常信息。
关于具体 ErrorOverlay 的细节:
export class ErrorOverlay extends HTMLElement {
constructor(err) {
super()
this.root = this.attachShadow({ mode: 'open' })
this.root.innerHTML = template
}
}
由于这是一个 WebComponents 组件,attachShadow 中 mode: 'open' 表示这个 shadow root 元素可以从 js 外部进行访问。
通过设置 innnerHTML 设置 WebComponnets 的模板内容。
const codeframeRE = /^(?:>?\s+\d+\s+\|.*|\s+\|\s*\^.*)\r?\n/gm
constructor(err) {
// ...
codeframeRE.lastIndex = 0
}
关于 lastIndex 的作用:
对于正则中使用了
/.../g全局匹配的用法,每次匹配成功都会修改lastIndex的值,导致后续的匹配位置>出现偏离,所以需要修改lastIndex=0进行修正。
具体可以看这个例子:
const reg = /\w/g
const value = 'gg'
reg.test(value) // true
reg.test(value) // true
reg.test(value) // false
之所以第三次的匹配会是 false 的原因,是因为在使用 //g 全局匹配的时候,每次都会从上次匹配到的地方重新开始匹配,即内部通过 lastIndex 来记录上次匹配到的位置,所以需要通过 reg.lastIndex = 0 进行修正。
this.text('.plugin', `[plugin:${err.plugin}] `)
text() {
const el = this.root.querySelector(selector)!
el.textContent = text
}
然后会调用 text 方法,依次传入 选择器、文本内容,text 方法内部会通过 textContent 进行 dom 文本的替换。
另外,在 this.root.querySelector(selector) 后的 ! 是一个 typescript 的语法,用于告诉 ts,!前面的变量一定存在,否则,调用 el.textContent 时会提示 el 可能不存在。
this.root.querySelector('.window')!.addEventListener('click', (e) => {
e.stopPropagation()
})
this.addEventListener('click', () => {
this.close()
})
close(): void {
this.parentNode?.removeChild(this)
}
这里对 ErrorOverlay 最外层的 window 选择器绑定 click 事件,阻止事件冒泡,通过点击当前组件时,会执行 close 方法, 通过调用 parentNode 上的 removeChild 进行当前元素的删除,并清除父节点与当前节点的绑定关系,用于释放内存引用。
可以看到,ErrorOverlay 其实是个 WebComponents 组件,它主要用来承载错误信息的展示。
小结
本篇主要介绍了 client 下 env、overlay 两个文件,基本都是一些细小的知识点,而 client 最主要的处理逻辑都在 client/client.ts 中,这部分下篇继续分析。