插件概览
- 插件入口
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
钩子
- 如果 id === ssrRegisterHelperId,返回
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
返回我们的代码``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’,
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
}
}
....
}
} }
<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 中