vue3入门51 - Vite 进阶 - vite-vue3-jsx 源码解析

358 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第30天,点击查看活动详情

前言

这一节我们来对vite-vue3-jsx做一下源码分析

插件概览

  • 插件入口 vueJsxPlugin
  • config 钩子中处理增加一些配置
    • esbuild,使用 esbuild 编译 ts 文件,我们这个插件处理了 jsx 和 tsx,所以不需要编译
    • difine,添加了 vue 相关的一些环境变量
  • configResolved 钩子
    • 判断我们项目运行,是 dev 环境还是 production 环境
    • dev 环境需要 hmr
    • dev 环境需要 sourceMap,如果我们没有设置false,同样为 true
    • 设置 项目根目录
  • resolveId 钩子
    • 如果 id === ssrRegisterHelperId 这个变量,就返回这个变量
    • ssrRegisterHelperId 是 /__vue-jsx-ssr-register-helper, 后面 ssr 会用到
  • load 钩子
    • 如果 id === ssrRegisterHelperId,返回 ssrRegisterHelperCode
    • ssrRegisterHelperCode 是返回的一段代码片段,后面 ssr 会用到
    • 当想增加一些特定的引用代码时,就可以使用resolveId load 钩子
  • transform 钩子
    • 可以接收到 code 源码,id 文件id,opt ssr 配置选项
    • 读取 include exclude babelPlugins babelPluginOptions
    • babelPlugins ast 转换过程需要用到 babelPlugins
    • babelPluginOptions 其他选项,都会传给 @vue/babel-plugin-jsx
    • createFilter 是 rollup 插件开发工具,判断我们的文件是否需要进行处理
    • [jsx, babelPluginOptions] 意思是 jsx 插件 和 jsx 的选项,babel plugins 会进行处理
export type PluginItem =
    | ConfigItem
    | PluginObj<any>
    | PluginTarget
    | [PluginTarget, PluginOptions]
    | [PluginTarget, PluginOptions, string | undefined];
  • 如果是 tsx 那么我们需要加入 @babel/plugin-transform-typescript 来处理文件
  • 通过 babel.transformSync 来得到 ast 代码
  • 如果不是 ssr hmr 返回我们的代码
const ssrRegisterHelperId = '/__vue-jsx-ssr-register-helper'
const ssrRegisterHelperCode =
  `import { useSSRContext } from "vue"\n` +
  `export ${ssrRegisterHelper.toString()}`

/**
 * This function is serialized with toString() and evaluated as a virtual
 * module during SSR
 * @param {import('vue').ComponentOptions} comp
 * @param {string} filename
 */
function ssrRegisterHelper(comp, filename) {
  const setup = comp.setup
  comp.setup = (props, ctx) => {
    // @ts-ignore
    const ssrContext = useSSRContext()
    ;(ssrContext.modules || (ssrContext.modules = new Set())).add(filename)
    if (setup) {
      return setup(props, ctx)
    }
  }
}

function vueJsxPlugin(options = {}) {
  let root = ''
  let needHmr = false
  let needSourceMap = true

  return {
    name: 'vite:vue-jsx',
    
    config(config) {
      return {
        // only apply esbuild to ts files
        // since we are handling jsx and tsx now
        esbuild: {
          include: /\.ts$/
        },
        define: {
          __VUE_OPTIONS_API__: true,
          __VUE_PROD_DEVTOOLS__: false,
          ...config.define
        }
      }
    },
    configResolved(config) {
      needHmr = config.command === 'serve' && !config.isProduction
      needSourceMap = config.command === 'serve' || !!config.build.sourcemap
      root = config.root
    },

    resolveId(id) {
      if (id === ssrRegisterHelperId) {
        return id
      }
    },

    load(id) {
      if (id === ssrRegisterHelperId) {
        return ssrRegisterHelperCode
      }
    },
    
    transform(code, id, opt) {
      const ssr = typeof opt === 'boolean' ? opt : (opt && opt.ssr) === true
      const {
        include,
        exclude,
        babelPlugins = [],
        ...babelPluginOptions
      } = options

      const filter = createFilter(include || /\.[jt]sx$/, exclude)

      if (filter(id)) {
        const plugins = [importMeta, [jsx, babelPluginOptions], ...babelPlugins]
        if (id.endsWith('.tsx')) {
          plugins.push([
            require('@babel/plugin-transform-typescript'),
            // @ts-ignore
            { isTSX: true, allowExtensions: true }
          ])
        }

        const result = babel.transformSync(code, {
          babelrc: false,
          ast: true,
          plugins,
          sourceMaps: needSourceMap,
          sourceFileName: id,
          configFile: false
        })
      }
      
      if (!ssr && !needHmr) {
          return {
            code: result.code,
            map: result.map
          }
        }
      ....
    }
  }
}

HMR

一共做了两件事

  1. 把 jsx 代码通过 babel 转译成了 js 代码
  2. 加入了这个文件所有 export 出去的组件,热更新的代码

是和框架高度绑定的实现

处理 ast

在线解析ast :astexplorer.net/ 主要是判断 export,判断被 export 的是不是一个组件,

  • result.ast.program.body 有对每一个语句的解析
  • VariableDeclaration
    • 如果是这个类型,判断下是不是组件类型,如果是,放到 declaredComponents
    • 通过这些判断辨识这个代码 defineComponent ,所以 vue3 中需要定义 defineComponent 声明代码是个 vue 组件
const Comp = defineComponent()
  • ExportNamedDeclaration
    • 如果是这个类型,判断下 是否有 node.declaration,并且是 VariableDeclaration 类型,
    • 如果是 vue 组件,放到 hotComponents
export const Comp = defineComponent()
  • specifiers
    • 如果 node.specifiers.length 有值,遍历数组
    • 判断 spec.type === 'ExportSpecifier' &&spec.exported.type === 'Identifier'
    • 如果是的话,从 declaredComponents 中找有没有 spec.local.name,是的话放到 hotComponents
    • 不是所有声明的组件,都会被 hot,只有 export 的组件,才会被 hot
const Comp = defineComponent()
export {Comp,logo}
  • ExportDefaultDeclaration
    • 如果是 node.declaration.type === 'Identifier',就去 declaredComponents 找是不是需要 hot 的组件
export default defineComponent({})
  • isDefineComponentCall
    • 如果是这个类型,就添加到 hot 组件中
    • 设置 hasDefault 为 true
    • 由于没有变量名,我们需要定义 __default__, export default

增加 hot 的代码

  • 如果是 hasDefault,那么就会推一行代码(特殊处理)
  • /export default defineComponent/g 替换成 const __default__ = defineComponent`) + `\nexport default __default__
result.code.replace(
                /export default defineComponent/g,
                `const __default__ = defineComponent`
              ) + `\nexport default __default__`

类似于
export default defineComponent({})
变成了
const __default__ = defineComponent({})
export default __default__
  • 如果需要 hmr 的
    • 循环 hotComponents,给每一个组件加一段代码
    • \n${local}.__hmrId = "${id}" 创造全局唯一的 id
    • createRecord vue3 创建一个 hmr 的代码
    • reload vue3 执行 reload 的代码
    • 最后添加 import.meta.hot.accept 相关代码
 code +=
  `\n${local}.__hmrId = "${id}"` +
  `\n__VUE_HMR_RUNTIME__.createRecord("${id}", ${local})`
callbackCode += `\n__VUE_HMR_RUNTIME__.reload("${id}", __${exported})`


code += `\nimport.meta.hot.accept(({${hotComponents
  .map((c) => `${c.exported}: __${c.exported}`)
  .join(',')}}) => {${callbackCode}\n})`

最终编译到浏览器上会多出这部分代码

const __default__ = defineComponent({
	...
})
  
export default __default__
__default__.__hmrId = "4e3a8c6e"
__VUE_HMR_RUNTIME__.createRecord("4e3a8c6e", __default__)
import.meta.hot.accept(({default: __default}) => {
__VUE_HMR_RUNTIME__.reload("4e3a8c6e", __default)
})
function vueJsxPlugin(options = {}) {
	transform(code, id, opt) {
      const ssr = typeof opt === 'boolean' ? opt : (opt && opt.ssr) === true
     ......

        // check for hmr injection
        /**
         * @type {{ name: string }[]}
         */
        const declaredComponents = []
        /**
         * @type {{
         *  local: string,
         *  exported: string,
         *  id: string,
         * }[]}
         */
        const hotComponents = []
        let hasDefault = false

        for (const node of result.ast.program.body) {
          if (node.type === 'VariableDeclaration') {
            const names = parseComponentDecls(node, code)
            if (names.length) {
              declaredComponents.push(...names)
            }
          }

          if (node.type === 'ExportNamedDeclaration') {
            if (
              node.declaration &&
              node.declaration.type === 'VariableDeclaration'
            ) {
              hotComponents.push(
                ...parseComponentDecls(node.declaration, code).map(
                  ({ name }) => ({
                    local: name,
                    exported: name, // export 的名字
                    id: hash(id + name) // 文件路径 + 文件名 做 hash
                  })
                )
              )
            } else if (node.specifiers.length) {
              for (const spec of node.specifiers) {
                if (
                  spec.type === 'ExportSpecifier' &&
                  spec.exported.type === 'Identifier'
                ) {
                  const matched = declaredComponents.find(
                    ({ name }) => name === spec.local.name
                  )
                  if (matched) {
                    hotComponents.push({
                      local: spec.local.name,
                      exported: spec.exported.name,
                      id: hash(id + spec.exported.name)
                    })
                  }
                }
              }
            }
          }

          if (node.type === 'ExportDefaultDeclaration') {
            if (node.declaration.type === 'Identifier') {
              const _name = node.declaration.name
              const matched = declaredComponents.find(
                ({ name }) => name === _name
              )
              if (matched) {
                hotComponents.push({
                  local: node.declaration.name,
                  exported: 'default',
                  id: hash(id + 'default')
                })
              }
            } else if (isDefineComponentCall(node.declaration)) {
              hasDefault = true
              hotComponents.push({
                local: '__default__',
                exported: 'default',
                id: hash(id + 'default')
              })
            }
          }
        }

        if (hotComponents.length) {
          if (hasDefault && (needHmr || ssr)) {
            result.code =
              result.code.replace(
                /export default defineComponent/g,
                `const __default__ = defineComponent`
              ) + `\nexport default __default__`
          }

          if (needHmr && !ssr && !/\?vue&type=script/.test(id)) {
            let code = result.code
            let callbackCode = ``
            for (const { local, exported, id } of hotComponents) {
              code +=
                `\n${local}.__hmrId = "${id}"` +
                `\n__VUE_HMR_RUNTIME__.createRecord("${id}", ${local})`
              callbackCode += `\n__VUE_HMR_RUNTIME__.reload("${id}", __${exported})`
            }

            code += `\nimport.meta.hot.accept(({${hotComponents
              .map((c) => `${c.exported}: __${c.exported}`)
              .join(',')}}) => {${callbackCode}\n})`

            result.code = code
          }
					.....
	
        }

        return {
          code: result.code,
          map: result.map
        }
      }
    }
}

SSR

添加 server.js server-entry.jsx, 使用 node 服务启动 ssr

import { createSSRApp } from 'vue'

import App from './App'
import { renderToString } from '@vue/server-renderer'

export async function render(url, mainfest) {
  const app = createSSRApp(App)
  const ctx = {}
  const html = await renderToString(app, ctx)
  return html
}
// server.js
const express = require('express')
const fs = require('fs')
const app = express()

app.use(express.static('dist/client')) // 静态资源目录映射

const { createServer: createViteServer } = require('vite')

createViteServer({
  server: {
    middlewareMode: 'ssr', // 启动 ssr
  },
}).then((vite) => {
  app.use(vite.middlewares) // vite 中间件

  app.get('*', async (req, res) => {
    // 读取文件
    let template = fs.readFileSync('index.html', 'utf-8')
    template = await vite.transformIndexHtml(req.url, template)
    // 获取渲染函数
    const { render } = await vite.ssrLoadModule('/src/server-entry.jsx')
    // 渲染路由对应的 html
    const html = await render(req.url)
    // 替换字符串为 html 模版
    const responseHtml = template.replace('<!-- APP_HTML -->', html)
    res.set('content-type', 'text/html').send(responseHtml)
    // res.set('content-type', 'text/html').send(html)
  })

  app.listen(4000)
})

  • 源码
if (ssr) {
  const normalizedId = normalizePath(path.relative(root, id))
  let ssrInjectCode =
    `\nimport { ssrRegisterHelper } from "${ssrRegisterHelperId}"` +
    `\nconst __moduleId = ${JSON.stringify(normalizedId)}`
  for (const { local } of hotComponents) {
    ssrInjectCode += `\nssrRegisterHelper(${local}, __moduleId)`
  }
  result.code += ssrInjectCode
}
  • 打印处理后的代码
import { createSSRApp } from 'vue';
import { App } from './App';
import { renderToString } from '@vue/server-renderer';
export async function render(url, mainfest) {
  const app = createSSRApp(App);
  const ctx = {};
  const html = await renderToString(app, ctx);
  return html;
}
import { createVNode as _createVNode, createTextVNode as _createTextVNode } from "vue";
import { defineComponent } from 'vue';
import '@styles/index.css';
import classes from '@/styles/test.module.css';
import '@/styles/test.scss';
import logo from '@/assets/logo.png'; // import test from '@/test?raw'
// console.log(test) //

const App = defineComponent({
  setup() {
    const name = 'test';
    return () => {
      return _createVNode("div", {
        "class": `root ${classes.moduleClass}`
      }, [_createVNode("p", null, [_createTextVNode("Hello vue3 jsx")]), _createVNode("img", {
        "src": logo,
        "alt": ""
      }, null), _createVNode("p", null, [name])]);
    };
  }

});
export { App };

if (import.meta.hot) {
  import.meta.hot.on('test', val => {
    console.log(val);
  });
}
import { ssrRegisterHelper } from "/__vue-jsx-ssr-register-helper"
const __moduleId = "src/App.jsx"
ssrRegisterHelper(App, __moduleId)

编译后的代码加入了 ssrRegisterHelper, ssrRegisterHelper 中,替换了 setup,并添加了 useSSRContext

const ssrRegisterHelperId = '/__vue-jsx-ssr-register-helper'
const ssrRegisterHelperCode =
  `import { useSSRContext } from "vue"\n` +
  `export ${ssrRegisterHelper.toString()}`

/**
 * This function is serialized with toString() and evaluated as a virtual
 * module during SSR
 * @param {import('vue').ComponentOptions} comp
 * @param {string} filename
 */
function ssrRegisterHelper(comp, filename) {
  const setup = comp.setup
  comp.setup = (props, ctx) => {
    // @ts-ignore
    const ssrContext = useSSRContext()
    ;(ssrContext.modules || (ssrContext.modules = new Set())).add(filename)
    if (setup) {
      return setup(props, ctx)
    }
  }
}

useSSRContext 的源码在 vue@next 中