diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 5c05d921..6b26570d 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -1,4 +1,5 @@ import { defineConfig, DefaultTheme } from 'vitepress' +import { transformerTwoslash } from '@shikijs/vitepress-twoslash' import { buildEnd } from './buildEnd.config' const ogDescription = 'Next Generation Frontend Tooling' @@ -386,5 +387,19 @@ export default defineConfig({ ], }, }, + transformPageData(pageData) { + const canonicalUrl = `${ogUrl}/${pageData.relativePath}` + .replace(/\/index\.md$/, '/') + .replace(/\.md$/, '/') + pageData.frontmatter.head ??= [] + pageData.frontmatter.head.unshift([ + 'link', + { rel: 'canonical', href: canonicalUrl }, + ]) + return pageData + }, + markdown: { + codeTransformers: [transformerTwoslash()], + }, buildEnd, }) diff --git a/.vitepress/theme/composables/sponsor.ts b/.vitepress/theme/composables/sponsor.ts index ea8a180a..78fd30a1 100644 --- a/.vitepress/theme/composables/sponsor.ts +++ b/.vitepress/theme/composables/sponsor.ts @@ -1,4 +1,4 @@ -import { ref, onMounted } from 'vue' +import { ref, onMounted, onUnmounted } from 'vue' interface Sponsors { special: Sponsor[] @@ -13,6 +13,10 @@ interface Sponsor { name: string img: string url: string + /** + * Expects to also have an **inversed** image with `-dark` postfix. + */ + hasDark?: true } // shared data across instances so we load only once. @@ -58,12 +62,40 @@ const viteSponsors: Pick = { name: 'Transloadit', url: 'https://transloadit.com/?utm_source=vite&utm_medium=referral&utm_campaign=sponsorship&utm_content=website', img: '/transloadit.svg', + hasDark: true, }, ], } +function toggleDarkLogos() { + if (data.value) { + const isDark = document.documentElement.classList.contains('dark') + data.value.forEach(({ items }) => { + items.forEach((s: Sponsor) => { + if (s.hasDark) { + s.img = isDark + ? s.img.replace(/(\.\w+)$/, '-dark$1') + : s.img.replace(/-dark(\.\w+)$/, '$1') + } + }) + }) + } +} + export function useSponsor() { onMounted(async () => { + const ob = new MutationObserver((list) => { + for (const m of list) { + if (m.attributeName === 'class') { + toggleDarkLogos() + } + } + }) + ob.observe(document.documentElement, { attributes: true }) + onUnmounted(() => { + ob.disconnect() + }) + if (data.value) { return } @@ -72,6 +104,7 @@ export function useSponsor() { const json = await result.json() data.value = mapSponsors(json) + toggleDarkLogos() }) return { diff --git a/.vitepress/theme/index.ts b/.vitepress/theme/index.ts index aa8b1648..0fddcd6d 100644 --- a/.vitepress/theme/index.ts +++ b/.vitepress/theme/index.ts @@ -1,6 +1,8 @@ import { h } from 'vue' import type { Theme } from 'vitepress' import DefaultTheme from 'vitepress/theme' +import TwoslashFloatingVue from '@shikijs/vitepress-twoslash/client' +import '@shikijs/vitepress-twoslash/style.css' import './styles/vars.css' import HomeSponsors from './components/HomeSponsors.vue' import AsideSponsors from './components/AsideSponsors.vue' @@ -19,6 +21,7 @@ export default { }, enhanceApp({ app }) { app.component('SvgImage', SvgImage) + app.use(TwoslashFloatingVue) }, } satisfies Theme diff --git a/.vitepress/tsconfig.json b/.vitepress/tsconfig.json new file mode 100644 index 00000000..20b9618d --- /dev/null +++ b/.vitepress/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noImplicitOverride": true, + "noUnusedLocals": true, + "esModuleInterop": true, + "noEmit": true + }, + "exclude": ["cache", "dist"] +} diff --git a/_data/team.js b/_data/team.js index 9b37798b..525e5208 100644 --- a/_data/team.js +++ b/_data/team.js @@ -64,54 +64,6 @@ export const core = [ ], sponsor: 'https://github.com/sponsors/sapphi-red', }, - { - avatar: 'https://github.com/sodatea.png', - name: 'Haoqun Jiang', - title: 'Developer', - org: 'Vue.js', - orgLink: 'https://vuejs.org/', - desc: 'Vue/Vite core team member. Full-time open sourcerer.', - links: [ - { icon: 'github', link: 'https://github.com/sodatea' }, - { icon: 'twitter', link: 'https://twitter.com/haoqunjiang' }, - { icon: 'mastodon', link: 'https://elk.zone/m.webtoo.ls/@haoqun' }, - ], - sponsor: 'https://github.com/sponsors/sodatea', - }, - { - avatar: 'https://github.com/Shinigami92.png', - name: 'Shinigami', - title: 'Maintainer', - org: 'Faker', - orgLink: 'https://fakerjs.dev', - desc: 'Passionate TypeScript enthusiast working extensively with Vue SPA and pug.', - links: [ - { icon: 'github', link: 'https://github.com/Shinigami92' }, - { icon: 'mastodon', link: 'https://elk.zone/mas.to/@Shini92' }, - ], - sponsor: 'https://github.com/sponsors/Shinigami92', - }, - { - avatar: 'https://i.imgur.com/KMed6rQ.jpeg', - name: 'Alec Larson', - title: 'Entrepreneur', - desc: 'Dabbling in social ecommerce, meta frameworks, and board games', - links: [ - { icon: 'github', link: 'https://github.com/aleclarson' }, - { icon: 'twitter', link: 'https://twitter.com/retropragma' }, - ], - sponsor: 'https://github.com/sponsors/aleclarson', - }, - { - avatar: 'https://github.com/poyoho.png', - name: 'yoho', - title: 'Frontend Developer', - desc: 'Frontend. Vite team member.', - links: [ - { icon: 'github', link: 'https://github.com/poyoho' }, - { icon: 'twitter', link: 'https://twitter.com/yoho_po' }, - ], - }, { avatar: 'https://github.com/ArnaudBarre.png', name: 'Arnaud Barré', @@ -147,9 +99,56 @@ export const core = [ ], sponsor: 'https://github.com/sponsors/sheremet-va', }, + { + avatar: 'https://github.com/Shinigami92.png', + name: 'Shinigami', + title: 'Maintainer', + org: 'Faker', + orgLink: 'https://fakerjs.dev', + desc: 'Passionate TypeScript enthusiast working extensively with Vue SPA and pug.', + links: [ + { icon: 'github', link: 'https://github.com/Shinigami92' }, + { icon: 'mastodon', link: 'https://elk.zone/mas.to/@Shini92' }, + ], + sponsor: 'https://github.com/sponsors/Shinigami92', + }, + { + avatar: 'https://github.com/sodatea.png', + name: 'Haoqun Jiang', + title: 'Developer', + org: 'Vue.js', + orgLink: 'https://vuejs.org/', + desc: 'Vue/Vite core team member. Full-time open sourcerer.', + links: [ + { icon: 'github', link: 'https://github.com/sodatea' }, + { icon: 'twitter', link: 'https://twitter.com/haoqunjiang' }, + { icon: 'mastodon', link: 'https://elk.zone/m.webtoo.ls/@haoqun' }, + ], + sponsor: 'https://github.com/sponsors/sodatea', + }, ] export const emeriti = [ + { + avatar: 'https://i.imgur.com/KMed6rQ.jpeg', + name: 'Alec Larson', + title: 'Entrepreneur', + desc: 'Dabbling in social ecommerce, meta frameworks, and board games', + links: [ + { icon: 'github', link: 'https://github.com/aleclarson' }, + { icon: 'twitter', link: 'https://twitter.com/retropragma' }, + ], + }, + { + avatar: 'https://github.com/poyoho.png', + name: 'yoho', + title: 'Frontend Developer', + desc: 'Frontend. Vite team member.', + links: [ + { icon: 'github', link: 'https://github.com/poyoho' }, + { icon: 'twitter', link: 'https://twitter.com/yoho_po' }, + ], + }, { avatar: 'https://github.com/ygj6.png', name: 'ygj6', diff --git a/config/build-options.md b/config/build-options.md index 4cf1c0c1..38d5964c 100644 --- a/config/build-options.md +++ b/config/build-options.md @@ -48,13 +48,22 @@ type ResolveModulePreloadDependenciesFn = ( `resolveDependencies` 函数将为每个动态导入调用,同时带着一个它所依赖的 chunk 列表。并且它还会为每个在入口 HTML 文件中导入的 chunk 调用。 可以返回一个新的依赖关系数组,可能被过滤后变少了,也可能有更多依赖注入进来了,同时它们的路径也被修改过。`deps` 路径是相对于 `build.outDir` 的。若在注入该模块到 HTML head 时使用 `new URL(dep, import.meta.url)` 获取绝对路径,则对于 `hostType === 'js'`,允许返回一个相对于 `hostId` 的路径。 -```js + +```js twoslash +/** @type {import('vite').UserConfig} */ +const config = { + build: { +// ---cut-before--- modulePreload: { resolveDependencies: (filename, deps, { hostId, hostType }) => { return deps.filter(condition) - } + }, +}, +// ---cut-after--- + }, } ``` + 解析得到的依赖路径可以再在之后使用 [`experimental.renderBuiltUrl`](../guide/build.md#advanced-base-options) 更改。 diff --git a/config/dep-optimization-options.md b/config/dep-optimization-options.md index 6b5e4e2e..07f000a1 100644 --- a/config/dep-optimization-options.md +++ b/config/dep-optimization-options.md @@ -19,7 +19,9 @@ :::warning CommonJS CommonJS 的依赖不应该排除在优化外。如果一个 ESM 依赖被排除在优化外,但是却有一个嵌套的 CommonJS 依赖,则应该为该 CommonJS 依赖添加 `optimizeDeps.include`。例如: -```js +```js twoslash +import { defineConfig } from 'vite' +// ---cut--- export default defineConfig({ optimizeDeps: { include: ['esm-dep > cjs-dep'], @@ -37,7 +39,9 @@ export default defineConfig({ **实验性:** 如果你使用的是一个有很多深层导入的库,你也可以指定一个尾部的 glob 模式来一次性地预构建所有深层导入。这将避免在使用新的深层导入时不断地预构建。可以在此 [提供反馈](https://github.com/vitejs/vite/discussions/15833)。例如: -```js +```js twoslash +import { defineConfig } from 'vite' +// ---cut--- export default defineConfig({ optimizeDeps: { include: ['my-lib/components/**/*.vue'], diff --git a/config/index.md b/config/index.md index e1b99d38..2bb79134 100644 --- a/config/index.md +++ b/config/index.md @@ -50,7 +50,9 @@ Vite 也直接支持 TS 配置文件。你可以在 `vite.config.ts` 中使用 ` 如果配置文件需要基于(`dev`/`serve` 或 `build`)命令或者不同的 [模式](/guide/env-and-mode) 来决定选项,亦或者是一个 SSR 构建(`isSsrBuild`)、一个正在预览的构建产物(`isPreview`),则可以选择导出这样一个函数: -```js +```js twoslash +import { defineConfig } from 'vite' +// ---cut--- export default defineConfig(({ command, mode, isSsrBuild, isPreview }) => { if (command === 'serve') { return { @@ -65,7 +67,7 @@ export default defineConfig(({ command, mode, isSsrBuild, isPreview }) => { }) ``` -需要注意的是,在 Vite 的 API 中,在开发环境下 `command` 的值为 `serve`(在 CLI 中, `vite dev` 和 `vite serve` 是 `vite` 的别名),而在生产环境下为 `build`(`vite build`)。 +需要注意的是,在 Vite 的 API 中,在开发环境下 `command` 的值为 `serve`(在 CLI 中, `vite dev` 和 `vite serve` 是 [`vite`](/guide/cli#vite) 的别名),而在生产环境下为 `build`([`vite build`](/guide/cli#vite-build))。 `isSsrBuild` 和 `isPreview` 是额外的可选标志,用于区分 `build` 和 `serve` 命令的类型。一些加载 Vite 配置的工具可能不支持这些标志,而会传递 `undefined`。因此,建议使用 `true` 和 `false` 的显式比较。 @@ -73,7 +75,9 @@ export default defineConfig(({ command, mode, isSsrBuild, isPreview }) => { 如果配置需要调用一个异步函数,也可以转而导出一个异步函数。这个异步函数也可以通过 `defineConfig` 传递,以便获得更好的智能提示: -```js +```js twoslash +import { defineConfig } from 'vite' +// ---cut--- export default defineConfig(async ({ command, mode }) => { const data = await asyncFunction() return { @@ -88,7 +92,7 @@ export default defineConfig(async ({ command, mode }) => { 注意 Vite 默认是不加载 `.env` 文件的,因为这些文件需要在执行完 Vite 配置后才能确定加载哪一个,举个例子,`root` 和 `envDir` 选项会影响加载行为。不过当你的确需要时,你可以使用 Vite 导出的 `loadEnv` 函数来加载指定的 `.env` 文件。 -```js +```js twoslash import { defineConfig, loadEnv } from 'vite' export default defineConfig(({ command, mode }) => { diff --git a/config/server-options.md b/config/server-options.md index 951749ec..d030f7a1 100644 --- a/config/server-options.md +++ b/config/server-options.md @@ -18,10 +18,10 @@ 你可以设置 [`dns.setDefaultResultOrder('verbatim')`](https://nodejs.org/api/dns.html#dns_dns_setdefaultresultorder_order) 来禁用这个重新排序的行为。Vite 会将地址打印为 `localhost`。 -```js +```js twoslash // vite.config.js import { defineConfig } from 'vite' -import dns from 'dns' +import dns from 'node:dns' dns.setDefaultResultOrder('verbatim') @@ -152,6 +152,8 @@ export default defineConfig({ 设置 `server.hmr.overlay` 为 `false` 可以禁用开发服务器错误的屏蔽。 +`protocol` 是用于设置 HMR 连接使用的 WebSocket 协议的选项,可以是 `ws`(WebSocket)或者 `wss`(WebSocket Secure)。 + `clientPort` 是一个高级选项,只在客户端的情况下覆盖端口,这允许你为 websocket 提供不同的端口,而并非在客户端代码中查找。如果需要在 dev-server 情况下使用 SSL 代理,这非常有用。 当 `server.hmr.server` 被定义后,Vite 将会通过所提供的的服务器来处理 HMR 连接。如果不是在中间件模式下,Vite 将尝试通过已有服务器处理 HMR 连接。这在使用自签证书或想通过网络在某端口暴露 Vite 的情况下,非常有用。 @@ -236,7 +238,7 @@ Vite 服务器的文件监听器默认会监听 `root` 目录,同时会跳过 - **示例:** -```js +```js twoslash import express from 'express' import { createServer as createViteServer } from 'vite' @@ -356,9 +358,9 @@ export default defineConfig({ // 添加到忽略列表中。 sourcemapIgnoreList(sourcePath, sourcemapPath) { return sourcePath.includes('node_modules') - } - } -}; + }, + }, +}) ``` ::: tip 注意 diff --git a/config/shared-options.md b/config/shared-options.md index 57c4a4cc..b26aace5 100644 --- a/config/shared-options.md +++ b/config/shared-options.md @@ -163,7 +163,14 @@ Vite 有一个“允许的情景”列表,并且会匹配列表中第一个情 - **相关:** [esbuild#preserve-symlinks](https://esbuild.github.io/api/#preserve-symlinks),[webpack#resolve.symlinks ](https://webpack.js.org/configuration/resolve/#resolvesymlinks) -## css.modules {#css-modules} +## html.cspNonce + +- **类型:** `string` +- **相关:** [内容安全策略(CSP)](/guide/features#content-security-policy-csp) + +一个在生成脚本或样式标签时会用到的 nonce 值占位符。设置此值还会生成一个带有 nonce 值的 meta 标签。 + +## css.modules - **类型:** ```ts @@ -408,7 +415,7 @@ export default defineConfig({ 使用自定义 logger 记录消息。可以使用 Vite 的 `createLogger` API 获取默认的 logger 并对其进行自定义,例如,更改消息或过滤掉某些警告。 -```js +```ts twoslash import { createLogger, defineConfig } from 'vite' const logger = createLogger() diff --git a/config/ssr-options.md b/config/ssr-options.md index 32e08315..d5b7c21a 100644 --- a/config/ssr-options.md +++ b/config/ssr-options.md @@ -18,7 +18,7 @@ 这个选项可以防止列出的依赖项在服务端渲染(SSR)时被外部化,这些依赖项将会在构建过程中被打包。默认情况下,只有软链接的依赖项不会被外部化(这是为了HMR)。如果你希望将软链接的依赖项也外部化,可以将其名称传给 `ssr.external` 选项。 -如果这个选项设置为 `true`,那么没有任何依赖项会被外部化。然而,如果你在 `ssr.external` 中明确列出了一些依赖项(使用 `string[]` 类型),那么这些依赖项可以优先被外部化。 +如果这个选项设置为 `true`,那么没有任何依赖项会被外部化。然而,如果你在 `ssr.external` 中明确列出了一些依赖项(使用 `string[]` 类型),那么这些依赖项可以优先被外部化。如果设置了 `ssr.target: 'node'`,那么 Node.js 的内置模块也会被默认外部化。 需要注意的是,如果 `ssr.noExternal: true` 和 `ssr.external: true` 都被设置了,那么 `ssr.noExternal` 将优先生效,没有任何依赖项会被外部化。 diff --git a/guide/api-hmr.md b/guide/api-hmr.md index 277502a1..64321028 100644 --- a/guide/api-hmr.md +++ b/guide/api-hmr.md @@ -8,15 +8,15 @@ Vite 通过特殊的 `import.meta.hot` 对象暴露手动 HMR API。 -```ts +```ts twoslash +import type { ModuleNamespace } from 'vite/types/hot.d.ts' +import type { InferCustomEventPayload } from 'vite/types/customEvent.d.ts' + +// ---cut--- interface ImportMeta { readonly hot?: ViteHotContext } -type ModuleNamespace = Record & { - [Symbol.toStringTag]: 'Module' -} - interface ViteHotContext { readonly data: any @@ -32,7 +32,6 @@ interface ViteHotContext { prune(cb: (data: any) => void): void invalidate(message?: string): void - // `InferCustomEventPayload` provides types for built-in Vite events on( event: T, cb: (payload: InferCustomEventPayload) => void, @@ -66,7 +65,9 @@ Vite 在 [`vite/client.d.ts`](https://github.com/vitejs/vite/blob/main/packages/ 要接收模块自身,应使用 `import.meta.hot.accept`,参数为接收已更新模块的回调函数: -```js +```js twoslash +import 'vite/client' +// ---cut--- export const count = 1 if (import.meta.hot) { @@ -89,7 +90,13 @@ Vite 要求这个函数的调用在源代码中显示为 `import.meta.hot.accept 模块也可以接受直接依赖项的更新,而无需重新加载自身: -```js +```js twoslash +// @filename: /foo.d.ts +export declare const foo: () => void + +// @filename: /example.js +import 'vite/client' +// ---cut--- import { foo } from './foo.js' foo() @@ -115,7 +122,9 @@ if (import.meta.hot) { 一个接收自身的模块或一个期望被其他模块接收的模块可以使用 `hot.dispose` 来清除任何由其更新副本产生的持久副作用: -```js +```js twoslash +import 'vite/client' +// ---cut--- function setupSideEffect() {} setupSideEffect() @@ -131,7 +140,9 @@ if (import.meta.hot) { 注册一个回调,当模块在页面上不再被导入时调用。与 `hot.dispose` 相比,如果源代码更新时自行清理了副作用,你只需要在模块从页面上被删除时,使用此方法进行清理。Vite 目前在 `.css` 导入上使用此方法。 -```js +```js twoslash +import 'vite/client' +// ---cut--- function setupOrReuseSideEffect() {} setupOrReuseSideEffect() @@ -149,7 +160,9 @@ if (import.meta.hot) { 注意,不支持对 `data` 本身的重新赋值。相反,你应该对 `data` 对象的属性进行突变,以便保留从其他处理程序添加的信息。 -```js +```js twoslash +import 'vite/client' +// ---cut--- // ok import.meta.hot.data.someValue = 'hello' @@ -167,7 +180,9 @@ import.meta.hot.data = { someValue: 'hello' } 请注意,你应该总是调用 `import.meta.hot.accept`,即使你打算随后立即调用 `invalidate`,否则 HMR 客户端将不会监听未来对接收自身模块的更改。为了清楚地表达你的意图,我们建议在 `accept` 回调中调用 `invalidate`,例如: -```js +```js twoslash +import 'vite/client' +// ---cut--- import.meta.hot.accept((module) => { // 你可以使用新的模块实例来决定是否使其失效。 if (cannotHandleUpdate(module)) { @@ -204,3 +219,9 @@ import.meta.hot.accept((module) => { 如果在连接前调用,数据会先被缓存、等到连接建立好后再发送。 查看 [客户端与服务器的数据交互](/guide/api-plugin.html#client-server-communication) 一节获取更多细节。 + +## 推荐阅读 {#further-reading} + +如果你想深入了解如何使用 HMR API,以及它的内部运作机制,可以参考以下资源: + +- [热模块替换其实很简单](https://bjornlu.com/blog/hot-module-replacement-is-easy) diff --git a/guide/api-javascript.md b/guide/api-javascript.md index d614d791..8e1c85d2 100644 --- a/guide/api-javascript.md +++ b/guide/api-javascript.md @@ -12,26 +12,24 @@ async function createServer(inlineConfig?: InlineConfig): Promise **使用示例:** -```js -import { fileURLToPath } from 'url' +```ts twoslash +import { fileURLToPath } from 'node:url' import { createServer } from 'vite' const __dirname = fileURLToPath(new URL('.', import.meta.url)) -;(async () => { - const server = await createServer({ - // 任何合法的用户配置选项,加上 `mode` 和 `configFile` - configFile: false, - root: __dirname, - server: { - port: 1337, - }, - }) - await server.listen() +const server = await createServer({ + // 任何合法的用户配置选项,加上 `mode` 和 `configFile` + configFile: false, + root: __dirname, + server: { + port: 1337, + }, +}) +await server.listen() - server.printUrls() - server.bindCLIShortcuts({ print: true }) -})() +server.printUrls() +server.bindCLIShortcuts({ print: true }) ``` ::: tip 注意 @@ -44,7 +42,7 @@ const __dirname = fileURLToPath(new URL('.', import.meta.url))
示例 -```ts +```ts twoslash import http from 'http' import { createServer } from 'vite' @@ -57,16 +55,17 @@ const vite = await createServer({ // 提供父 http 服务器以代理 WebSocket server: parentServer, }, - }, - proxy: { - '/ws': { - target: 'ws://localhost:3000', - // Proxying WebSocket - ws: true, + proxy: { + '/ws': { + target: 'ws://localhost:3000', + // Proxying WebSocket + ws: true, + }, }, }, }) +// @noErrors: 2339 parentServer.use(vite.middlewares) ``` @@ -178,12 +177,24 @@ interface ViteDevServer { */ close(): Promise /** - * Bind CLI shortcuts + * 绑定 CLI 快捷键 */ bindCLIShortcuts(options?: BindCLIShortcutsOptions): void + /** + * 调用 `await server.waitForRequestsIdle(id)` 会等待所有的静态导入 + * 都被处理完。如果这个函数是从一个加载或转换的插件钩子中被调用的,那么你需要 + * 把 id 作为参数传入,以避免死锁。在模块图的第一个静态导入部分被处理之后 + * 调用这个函数,它将立即返回。 + * @实验性 + */ + waitForRequestsIdle: (ignoredId?: string) => Promise } ``` +:::info +`waitForRequestsIdle` 的设计初衷是作为一种应急措施,以改善那些无法按照 Vite 开发服务器按需加载特性来实现的功能的开发体验。像 Tailwind 这样的工具可以在启动期间使用它,以便在应用代码被加载之前延迟生成应用的 CSS 类,从而避免样式的闪烁变化。当这个函数在加载或转换钩子中被使用,并且使用的是默认的 HTTP1 服务器时,六个 http 通道中的一个将被阻塞,直到服务器处理完所有的静态导入。Vite 的依赖优化器目前使用这个函数来避免在缺少依赖项时进行全页刷新,它通过延迟加载预打包的依赖项,直到从静态导入的源收集到所有的导入依赖项。在未来的主要版本中,Vite 可能会采取不同的策略,将 `optimizeDeps.crawlUntilStaticImports: false` 设置为默认值,以避免在大型应用程序在冷启动期间出现性能下降。 +::: + ## `build` {#build} **类型签名:** @@ -196,24 +207,22 @@ async function build( **使用示例:** -```js -import path from 'path' -import { fileURLToPath } from 'url' +```ts twoslash +import path from 'node:path' +import { fileURLToPath } from 'node:url' import { build } from 'vite' const __dirname = fileURLToPath(new URL('.', import.meta.url)) -;(async () => { - await build({ - root: path.resolve(__dirname, './project'), - base: '/foo/', - build: { - rollupOptions: { - // ... - }, +await build({ + root: path.resolve(__dirname, './project'), + base: '/foo/', + build: { + rollupOptions: { + // ... }, - }) -})() + }, +}) ``` ## `preview` {#preview} @@ -226,20 +235,19 @@ async function preview(inlineConfig?: InlineConfig): Promise **示例用法:** -```js +```ts twoslash import { preview } from 'vite' -;(async () => { - const previewServer = await preview({ - // 任何有效的用户配置项,将加上 `mode` 和 `configFile` - preview: { - port: 8080, - open: true, - }, - }) - previewServer.printUrls() - previewServer.bindCLIShortcuts({ print: true }) -})() +const previewServer = await preview({ + // 任何合法的用户配置选项,加上 `mode` 和 `configFile` + preview: { + port: 8080, + open: true, + }, +}) + +previewServer.printUrls() +previewServer.bindCLIShortcuts({ print: true }) ``` ## `PreviewServer` @@ -314,7 +322,17 @@ function mergeConfig( 你可以使用 `defineConfig` 工具函数将回调形式的配置与另一个配置合并: -```ts +```ts twoslash +import { + defineConfig, + mergeConfig, + type UserConfigFnObject, + type UserConfig, +} from 'vite' +declare const configAsCallback: UserConfigFnObject +declare const configAsObject: UserConfig + +// ---cut--- export default defineConfig((configEnv) => mergeConfig(configAsCallback(configEnv), configAsObject), ) diff --git a/guide/api-plugin.md b/guide/api-plugin.md index 066c7d66..837a9a42 100644 --- a/guide/api-plugin.md +++ b/guide/api-plugin.md @@ -401,6 +401,7 @@ Vite 插件也可以提供钩子来服务于特定的 Vite 目标。这些钩子 ### `handleHotUpdate` {#handlehotupdate} - **类型:** `(ctx: HmrContext) => Array | void | Promise | void>` +- **参见:** [HMR API](./api-hmr) 执行自定义 HMR 更新处理。钩子接收一个带有以下签名的上下文对象: @@ -422,10 +423,31 @@ Vite 插件也可以提供钩子来服务于特定的 Vite 目标。这些钩子 - 过滤和缩小受影响的模块列表,使 HMR 更准确。 - - 返回一个空数组,并通过向客户端发送自定义事件来执行完整的自定义 HMR 处理(示例使用了在 Vite 5.1 中引入的 `server.hot`,如果你想支持较低版本,建议使用 `server.ws`): + - 返回一个空数组并进行全面刷新: + + ```js + handleHotUpdate({ server, modules, timestamp }) { + // 如果需要,也可使用 `server.ws.send` 来支持 Vite <5.1 版本 + server.hot.send({ type: 'full-reload' }) + // 手动使模块失效 + const invalidatedModules = new Set() + for (const mod of modules) { + server.moduleGraph.invalidateModule( + mod, + invalidatedModules, + timestamp, + true + ) + } + return [] + } + ``` + + - 返回一个空数组,并通过向客户端发送自定义事件,来进行完全自定义的 HMR处理: ```js handleHotUpdate({ server }) { + // 如果需要,也可使用 `server.ws.send` 来支持 Vite <5.1 版本 server.hot.send({ type: 'custom', event: 'special-update', @@ -556,7 +578,9 @@ export default defineConfig({ 在客户端侧,使用 [`hot.on`](/guide/api-hmr.html#hot-on-event-cb) 去监听事件: -```ts +```ts twoslash +import 'vite/client' +// ---cut--- // client side if (import.meta.hot) { import.meta.hot.on('my:greetings', (data) => { diff --git a/guide/assets.md b/guide/assets.md index 221cd060..d51576e5 100644 --- a/guide/assets.md +++ b/guide/assets.md @@ -7,7 +7,9 @@ 服务时引入一个静态资源会返回解析后的公共路径: -```js +```js twoslash +import 'vite/client' +// ---cut--- import imgUrl from './img.png' document.getElementById('hero-img').src = imgUrl ``` @@ -30,11 +32,25 @@ document.getElementById('hero-img').src = imgUrl - 默认情况下,TypeScript 不会将静态资源导入视为有效的模块。要解决这个问题,需要添加 [`vite/client`](./features#client-types)。 +::: tip 通过 `url()` 内联 SVG +当在 JS 中手动构造 `url()` 并传入一个 SVG 的 URL 时,应该用双引号将变量包裹起来。 + +```js twoslash +import 'vite/client' +// ---cut--- +import imgUrl from './img.svg' +document.getElementById('hero-img').style.background = `url("${imgUrl}")` +``` + +::: + ### 显式 URL 引入 {#explicit-url-imports} 未被包含在内部列表或 `assetsInclude` 中的资源,可以使用 `?url` 后缀显式导入为一个 URL。这十分有用,例如,要导入 [Houdini Paint Worklets](https://houdini.how/usage) 时: -```js +```js twoslash +import 'vite/client' +// ---cut--- import workletURL from 'extra-scalloped-border/worklet.js?url' CSS.paintWorklet.addModule(workletURL) ``` @@ -43,7 +59,9 @@ CSS.paintWorklet.addModule(workletURL) 资源可以使用 `?raw` 后缀声明作为字符串引入。 -```js +```js twoslash +import 'vite/client' +// ---cut--- import shaderString from './shader.glsl?raw' ``` @@ -51,19 +69,25 @@ import shaderString from './shader.glsl?raw' 脚本可以通过 `?worker` 或 `?sharedworker` 后缀导入为 web worker。 -```js +```js twoslash +import 'vite/client' +// ---cut--- // 在生产构建中将会分离出 chunk import Worker from './shader.js?worker' const worker = new Worker() ``` -```js +```js twoslash +import 'vite/client' +// ---cut--- // sharedworker import SharedWorker from './shader.js?sharedworker' const sharedWorker = new SharedWorker() ``` -```js +```js twoslash +import 'vite/client' +// ---cut--- // 内联为 base64 字符串 import InlineWorker from './shader.js?worker&inline' ``` diff --git a/guide/backend-integration.md b/guide/backend-integration.md index f767411f..3490c464 100644 --- a/guide/backend-integration.md +++ b/guide/backend-integration.md @@ -8,7 +8,9 @@ 1. 在你的 Vite 配置中配置入口文件和启用创建 `manifest`: - ```js + ```js twoslash + import { defineConfig } from 'vite' + // ---cut--- // vite.config.js export default defineConfig({ build: { @@ -66,7 +68,8 @@ "isEntry": true, "dynamicImports": ["views/foo.js"], "css": ["assets/main.b82dbe22.css"], - "assets": ["assets/asset.0ab0f9cd.png"] + "assets": ["assets/asset.0ab0f9cd.png"], + "imports": ["_shared.83069a53.js"] }, "views/foo.js": { "file": "assets/foo.869aea0d.js", @@ -75,7 +78,8 @@ "imports": ["_shared.83069a53.js"] }, "_shared.83069a53.js": { - "file": "assets/shared.83069a53.js" + "file": "assets/shared.83069a53.js", + "css": ["assets/shared.a834bfc3.css"] } } ``` @@ -83,12 +87,56 @@ - 清单是一个 `Record` 结构的对象。 - 对于 入口 或动态入口 chunk,键是相对于项目根目录的资源路径。 - 对于非入口 chunk,键是生成文件的名称并加上前缀 `_`。 - - Chunk 将信息包含在其静态和动态导入上(两者都是映射到清单中相应 chunk 的键),以及任何与之相关的 CSS 和资源文件。 + - Chunk 将信息包含在其静态和动态导入上(两者都是映射到清单中相应 chunk 的键),以及任何与之相关的 CSS 和资源文件。 + +4. 你可以利用这个文件来渲染带有哈希文件名的链接或预加载指令。 + + 这是一个用来渲染正确链接的 HTML 模板示例。这里的语法仅用于解释, + 你需要用你的服务器模板语言来替换。`importedChunks` 函数只是 + 用来说明,并不是 Vite 提供的。 + + ```html + + + + + + + + + + + + + + ``` + + 具体来说,一个生成 HTML 的后端在给定 manifest 文件和一个入口文件的情况下, + 应该包含以下标签: + + - 对于入口文件 chunk 的 `css` 列表中的每个文件,都应包含一个 `` 标签。 + - 递归追踪入口文件的 `imports` 列表中的所有 chunk,并为每个导入的 chunk 的每个 css 文件 + 包含一个 `` 标签。 + - 对于入口文件 chunk 的 `file` 键的标签(对于 Javascript 是 + ` + + + ``` - 你可以使用这个文件来渲染链接或者用散列文件名预加载指令(注意:这里的语法只是为了解释,实际使用时请你的服务器模板语言代替): + 而对于入口文件 `views/foo.js`,应该包含以下标签: ```html - - - + + + + ``` diff --git a/guide/build.md b/guide/build.md index 7270d863..8cbcbad8 100644 --- a/guide/build.md +++ b/guide/build.md @@ -34,7 +34,6 @@ 构建过程可以通过多种 [构建配置选项](/config/#build-options) 来自定义构建。具体来说,你可以通过 `build.rollupOptions` 直接调整底层的 [Rollup 选项](https://rollupjs.org/configuration-options/): ```js -// vite.config.js export default defineConfig({ build: { rollupOptions: { @@ -68,9 +67,9 @@ export default defineConfig({ 当 Vite 加载动态导入失败时,会触发 `vite:preloadError` 事件。`event.payload` 包含原始的导入错误信息。如果调用 `event.preventDefault()`,则不会抛出错误。 -```js +```js twoslash window.addEventListener('vite:preloadError', (event) => { - window.reload() // 例如,刷新页面 + window.location.reload() // 例如,刷新页面 }) ``` @@ -111,7 +110,7 @@ export default defineConfig({ 在构建过程中,你只需指定多个 `.html` 文件作为入口点即可: -```js +```js twoslash // vite.config.js import { resolve } from 'path' import { defineConfig } from 'vite' @@ -138,7 +137,7 @@ export default defineConfig({ 当这个库要进行发布构建时,请使用 [`build.lib` 配置项](/config/build-options.md#build-lib),以确保将那些你不想打包进库的依赖进行外部化处理,例如 `vue` 或 `react`: -```js +```js twoslash // vite.config.js import { resolve } from 'path' import { defineConfig } from 'vite' @@ -251,32 +250,45 @@ dist/my-lib.umd.cjs 0.30 kB / gzip: 0.16 kB 单个静态的 [基础路径](#public-base-path) 在这种场景中就不够用了。Vite 在构建时为更高级的基础路径选项提供了实验性支持,可以使用 `experimental.renderBuiltUrl`。 -```ts + +```ts twoslash +import type { UserConfig } from 'vite' +const config: UserConfig = { +// ---cut-before--- experimental: { - renderBuiltUrl(filename: string, { hostType }: { hostType: 'js' | 'css' | 'html' }) { + renderBuiltUrl(filename, { hostType }) { if (hostType === 'js') { return { runtime: `window.__toCdnUrl(${JSON.stringify(filename)})` } } else { return { relative: true } } - } + }, +}, +// ---cut-after--- } ``` + 如果 hash 后的资源和公共文件没有被部署在一起,可以根据该函数的第二个参数 `context` 上的字段 `type` 分别定义各个资源组的选项: -```ts -experimental: { - renderBuiltUrl(filename: string, { hostId, hostType, type }: { hostId: string, hostType: 'js' | 'css' | 'html', type: 'public' | 'asset' }) { - if (type === 'public') { - return 'https://www.domain.com/' + filename - } - else if (path.extname(hostId) === '.js') { - return { runtime: `window.__assetsPath(${JSON.stringify(filename)})` } - } - else { - return 'https://cdn.domain.com/assets/' + filename - } - } +```ts twoslash +import type { UserConfig } from 'vite' +import path from 'node:path' +const config: UserConfig = { + // ---cut-before--- + experimental: { + renderBuiltUrl(filename, { hostId, hostType, type }) { + if (type === 'public') { + return 'https://www.domain.com/' + filename + } else if (path.extname(hostId) === '.js') { + return { runtime: `window.__assetsPath(${JSON.stringify(filename)})` } + } else { + return 'https://cdn.domain.com/assets/' + filename + } + }, + }, + // ---cut-after--- } ``` + +请注意,传递的 `filename` 是一个已解码的 URL,如果函数返回了一个 URL 字符串,那么它也应该是已解码的。当 Vite 渲染 URL 时会自动处理编码。如果返回的是一个带有 `runtime` 的对象,就需要在必要的地方自行处理编码,因为运行时的代码将会按照原样呈现。 diff --git a/guide/cli.md b/guide/cli.md index 4a3b9c74..5543cc07 100644 --- a/guide/cli.md +++ b/guide/cli.md @@ -4,7 +4,7 @@ ### `vite` {#vite} -在当前目录下启动 Vite 开发服务器。 +在当前目录下启动 Vite 开发服务器。`vite dev` 和 `vite serve` 是 `vite` 的别名。 #### 使用 {#usage} diff --git a/guide/dep-pre-bundling.md b/guide/dep-pre-bundling.md index c660fe4f..6b262654 100644 --- a/guide/dep-pre-bundling.md +++ b/guide/dep-pre-bundling.md @@ -37,7 +37,9 @@ 然而,这需要被链接的依赖被导出为 ESM 格式。如果不是,那么你可以在配置里将此依赖添加到 [`optimizeDeps.include`](/config/dep-optimization-options.md#optimizedeps-include) 和 [`build.commonjsOptions.include`](/config/build-options.md#build-commonjsoptions) 这两项中。 -```js +```js twoslash +import { defineConfig } from 'vite' +// ---cut--- export default defineConfig({ optimizeDeps: { include: ['linked-dep'], diff --git a/guide/features.md b/guide/features.md index 202d0c27..4f2b8926 100644 --- a/guide/features.md +++ b/guide/features.md @@ -65,7 +65,7 @@ export type { T } 你必须在 `tsconfig.json` 中的 `compilerOptions` 下设置 `"isolatedModules": true`。如此做,TS 会警告你不要使用隔离(isolated)转译的功能。 -然而,一些库(如:[`vue`](https://github.com/vuejs/core/issues/1228))不能很好地与 `"isolatedModules": true` 共同工作。你可以在上游仓库修复好之前暂时使用 `"skipLibCheck": true` 来缓解这个错误。 +如果一个依赖项和 `"isolatedModules": true` 不兼容的话,你可以在上游仓库修复好之前暂时使用 `"skipLibCheck": true` 来缓解这个错误。 #### `useDefineForClassFields` @@ -176,7 +176,7 @@ Vue 用户应使用官方提供的 [@vitejs/plugin-vue-jsx](https://github.com/v 如果不是在 React 或 Vue 中使用 JSX,自定义的 `jsxFactory` 和 `jsxFragment` 可以使用 [`esbuild` 选项](/config/shared-options.md#esbuild) 进行配置。例如对 Preact: -```js +```js twoslash // vite.config.js import { defineConfig } from 'vite' @@ -192,7 +192,7 @@ export default defineConfig({ 你可以使用 `jsxInject`(这是一个仅在 Vite 中使用的选项)为 JSX 注入 helper,以避免手动导入: -```js +```js twoslash // vite.config.js import { defineConfig } from 'vite' @@ -230,7 +230,9 @@ Sass 和 Less 文件也支持 `@import` 别名和 URL 变基(具体请参阅 [ } ``` -```js +```js twoslash +import 'vite/client' +// ---cut--- import classes from './example.module.css' document.getElementById('foo').className = classes.red ``` @@ -239,7 +241,9 @@ CSS modules 行为可以通过 [`css.modules` 选项](/config/shared-options.md# 如果 `css.modules.localsConvention` 设置开启了 camelCase 格式变量名转换(例如 `localsConvention: 'camelCaseOnly'`),你还可以使用按名导入。 -```js +```js twoslash +import 'vite/client' +// ---cut--- // .apply-color -> applyColor import { applyColor } from './example.module.css' document.getElementById('foo').className = applyColor @@ -274,7 +278,9 @@ Vite 为 Sass 和 Less 改进了 `@import` 解析,以保证 Vite 别名也能 自动注入 CSS 内容的行为可以通过 `?inline` 参数来关闭。在关闭时,被处理过的 CSS 字符串将会作为该模块的默认导出,但样式并没有被注入到页面中。 -```js +```js twoslash +import 'vite/client' +// ---cut--- import './foo.css' // 样式将会注入页面 import otherStyles from './bar.css?inline' // 样式不会注入页面 ``` @@ -305,29 +311,39 @@ npm add -D lightningcss 导入一个静态资源会返回解析后的 URL: -```js +```js twoslash +import 'vite/client' +// ---cut--- import imgUrl from './img.png' document.getElementById('hero-img').src = imgUrl ``` 添加一些特殊的查询参数可以更改资源被引入的方式: -```js +```js twoslash +import 'vite/client' +// ---cut--- // 显式加载资源为一个 URL import assetAsURL from './asset.js?url' ``` -```js +```js twoslash +import 'vite/client' +// ---cut--- // 以字符串形式加载资源 import assetAsString from './shader.glsl?raw' ``` -```js +```js twoslash +import 'vite/client' +// ---cut--- // 加载为 Web Worker import Worker from './worker.js?worker' ``` -```js +```js twoslash +import 'vite/client' +// ---cut--- // 在构建时 Web Worker 内联为 base64 字符串 import InlineWorker from './worker.js?worker&inline' ``` @@ -338,7 +354,9 @@ import InlineWorker from './worker.js?worker&inline' JSON 可以被直接导入 —— 同样支持具名导入: -```js +```js twoslash +import 'vite/client' +// ---cut--- // 导入整个对象 import json from './example.json' // 对一个根字段使用具名导入 —— 有效帮助 treeshaking! @@ -349,7 +367,9 @@ import { field } from './example.json' Vite 支持使用特殊的 `import.meta.glob` 函数从文件系统导入多个模块: -```js +```js twoslash +import 'vite/client' +// ---cut--- const modules = import.meta.glob('./dir/*.js') ``` @@ -375,7 +395,9 @@ for (const path in modules) { 匹配到的文件默认是懒加载的,通过动态导入实现,并会在构建时分离为独立的 chunk。如果你倾向于直接引入所有的模块(例如依赖于这些模块中的副作用首先被应用),你可以传入 `{ eager: true }` 作为第二个参数: -```js +```js twoslash +import 'vite/client' +// ---cut--- const modules = import.meta.glob('./dir/*.js', { eager: true }) ``` @@ -395,7 +417,9 @@ const modules = { 第一个参数可以是一个 glob 数组,例如: -```js +```js twoslash +import 'vite/client' +// ---cut--- const modules = import.meta.glob(['./dir/*.js', './another/*.js']) ``` @@ -403,7 +427,9 @@ const modules = import.meta.glob(['./dir/*.js', './another/*.js']) 同样也支持反面 glob 匹配模式(以 `!` 作为前缀)。若要忽略结果中的一些文件,你可以添加“排除匹配模式”作为第一个参数: -```js +```js twoslash +import 'vite/client' +// ---cut--- const modules = import.meta.glob(['./dir/*.js', '!**/bar.js']) ``` @@ -418,7 +444,9 @@ const modules = { 也可能你只想要导入模块中的部分内容,那么可以利用 `import` 选项。 -```ts +```ts twoslash +import 'vite/client' +// ---cut--- const modules = import.meta.glob('./dir/*.js', { import: 'setup' }) ``` @@ -432,7 +460,9 @@ const modules = { 当与 `eager` 一同存在时,甚至可以对这些模块进行 tree-shaking。 -```ts +```ts twoslash +import 'vite/client' +// ---cut--- const modules = import.meta.glob('./dir/*.js', { import: 'setup', eager: true, @@ -451,7 +481,9 @@ const modules = { 设置 `import` 为 `default` 可以加载默认导出。 -```ts +```ts twoslash +import 'vite/client' +// ---cut--- const modules = import.meta.glob('./dir/*.js', { import: 'default', eager: true, @@ -472,7 +504,9 @@ const modules = { 你也可以使用 `query` 选项来提供对导入的自定义查询,比如,可以将资源 [作为字符串引入](/guide/assets#importing-asset-as-string) 或者 [作为 URL 引入](/guide/assets#importing-asset-as-url) : -```ts +```ts twoslash +import 'vite/client' +// ---cut--- const moduleStrings = import.meta.glob('./dir/*.svg', { query: '?raw', import: 'default', @@ -497,7 +531,9 @@ const moduleUrls = { 你还可以为其他插件提供定制化的查询参数: -```ts +```ts twoslash +import 'vite/client' +// ---cut--- const modules = import.meta.glob('./dir/*.js', { query: { foo: 'bar', bar: true }, }) @@ -527,7 +563,9 @@ const module = await import(`./dir/${file}.js`) 预编译的 `.wasm` 文件可以通过 `?init` 来导入。 默认导出一个初始化函数,返回值为所导出 [`WebAssembly.Instance`](https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Instance) 实例对象的 Promise: -```js +```js twoslash +import 'vite/client' +// ---cut--- import init from './example.wasm?init' init().then((instance) => { @@ -537,7 +575,10 @@ init().then((instance) => { `init` 函数还可以将传递给 [`WebAssembly.instantiate`](https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/instantiate) 的导入对象作为其第二个参数: -```js +```js twoslash +import 'vite/client' +import init from './example.wasm?init' +// ---cut--- init({ imports: { someFunc: () => { @@ -560,7 +601,9 @@ init({ 如果需要访问 `Module` 对象,例如将它多次实例化,可以使用 [显式 URL 引入](./assets#explicit-url-imports) 来解析资源,然后执行实例化: -```js +```js twoslash +import 'vite/client' +// ---cut--- import wasmUrl from 'foo.wasm?url' const main = async () => { @@ -580,7 +623,9 @@ main() 以下是一种替代方案,假设项目根目录在当前目录: -```js +```js twoslash +import 'vite/client' +// ---cut--- import wasmUrl from 'foo.wasm?url' import { readFile } from 'node:fs/promises' @@ -620,7 +665,9 @@ const worker = new Worker(new URL('./worker.js', import.meta.url), { 你可以在导入请求上添加 `?worker` 或 `?sharedworker` 查询参数来直接导入一个 web worker 脚本。默认导出会是一个自定义 worker 的构造函数: -```js +```js twoslash +import 'vite/client' +// ---cut--- import MyWorker from './worker?worker' const worker = new MyWorker() @@ -630,18 +677,44 @@ const worker = new MyWorker() 默认情况下,worker 脚本将在生产构建中编译成单独的 chunk。如果你想将 worker 内联为 base64 字符串,请添加 `inline` 查询参数: -```js +```js twoslash +import 'vite/client' +// ---cut--- import MyWorker from './worker?worker&inline' ``` 如果你想要以一个 URL 的形式读取该 worker,请添加 `url` 这个 query: -```js +```js twoslash +import 'vite/client' +// ---cut--- import MyWorker from './worker?worker&url' ``` 关于如何配置打包全部 worker,可以查看 [Worker 选项](/config/worker-options.md) 了解更多相关细节。 +## 内容安全策略(CSP) {#content-security-policy-csp} + +由于 Vite 的内部机制,为了部署 CSP 必须设置某些指令或配置。 + +### [`'nonce-{RANDOM}'`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/Sources#nonce-base64-value) + +当设置了 [`html.cspNonce`](/config/shared-options#html-cspnonce) 时,Vite 会在输出的脚本标签和样式表的链接标签中添加一个带有指定值的 nonce 属性。请注意,Vite 不会将 nonce 属性添加到其他标签中,例如 `