Vite 源码分析 - client (上)| 8月更文挑战

830 阅读3分钟

这是我参与8月更文挑战的第19天,活动详情查看:8月更文挑战

前言

上篇关于 Vite 的介绍中说过,Vite 的核心代码包括 clientnode 双端,本篇主要介绍一下 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 的值需要经过条件判断才能确定,这里使用一个自执行函数包裹其中判断部分的代码。 依次从 globalThisselfwindowFunction('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 组件,attachShadowmode: '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 组件,它主要用来承载错误信息的展示。

小结

本篇主要介绍了 clientenvoverlay 两个文件,基本都是一些细小的知识点,而 client 最主要的处理逻辑都在 client/client.ts 中,这部分下篇继续分析。