插件概览

  • 插件入口 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 会进行处理

      1. export type PluginItem =
      2. | ConfigItem
      3. | PluginObj<any>
      4. | PluginTarget
      5. | [PluginTarget, PluginOptions]
      6. | [PluginTarget, PluginOptions, string | undefined];
    • 如果是 tsx 那么我们需要加入 @babel/plugin-transform-typescript 来处理文件

    • 通过 babel.transformSync 来得到 ast 代码
    • 如果不是 ssr hmr 返回我们的代码 ``javascript 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’,

  1. config(config) {
  2. return {
  3. // only apply esbuild to ts files
  4. // since we are handling jsx and tsx now
  5. esbuild: {
  6. include: /\.ts$/
  7. },
  8. define: {
  9. __VUE_OPTIONS_API__: true,
  10. __VUE_PROD_DEVTOOLS__: false,
  11. ...config.define
  12. }
  13. }
  14. },
  15. configResolved(config) {
  16. needHmr = config.command === 'serve' && !config.isProduction
  17. needSourceMap = config.command === 'serve' || !!config.build.sourcemap
  18. root = config.root
  19. },
  20. resolveId(id) {
  21. if (id === ssrRegisterHelperId) {
  22. return id
  23. }
  24. },
  25. load(id) {
  26. if (id === ssrRegisterHelperId) {
  27. return ssrRegisterHelperCode
  28. }
  29. },
  30. transform(code, id, opt) {
  31. const ssr = typeof opt === 'boolean' ? opt : (opt && opt.ssr) === true
  32. const {
  33. include,
  34. exclude,
  35. babelPlugins = [],
  36. ...babelPluginOptions
  37. } = options
  38. const filter = createFilter(include || /\.[jt]sx$/, exclude)
  39. if (filter(id)) {
  40. const plugins = [importMeta, [jsx, babelPluginOptions], ...babelPlugins]
  41. if (id.endsWith('.tsx')) {
  42. plugins.push([
  43. require('@babel/plugin-transform-typescript'),
  44. // @ts-ignore
  45. { isTSX: true, allowExtensions: true }
  46. ])
  47. }
  48. const result = babel.transformSync(code, {
  49. babelrc: false,
  50. ast: true,
  51. plugins,
  52. sourceMaps: needSourceMap,
  53. sourceFileName: id,
  54. configFile: false
  55. })
  56. }
  57. if (!ssr && !needHmr) {
  58. return {
  59. code: result.code,
  60. map: result.map
  61. }
  62. }
  63. ....
  64. }

} }

<a name="HfMqf"></a>
# HMR
一共做了两件事

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

是和框架高度绑定的实现

<a name="jwOF6"></a>
## 处理 ast 
在线解析ast :[https://astexplorer.net/](https://astexplorer.net/)<br />主要是判断 `export`,判断被 `export` 的是不是一个组件,

- `result.ast.program.body` 有对每一个语句的解析
- `VariableDeclaration`
   - 如果是这个类型,判断下是不是组件类型,如果是,放到 `declaredComponents` 中
   - 通过这些判断辨识这个代码 `defineComponent` ,所以 vue3 中需要定义 `defineComponent` 声明代码是个 vue 组件
```javascript
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__ ```javascript 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` 相关代码
```javascript
 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
    }
    
  • 打印处理后的代码 ```javascript 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`
```javascript
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 中