vite preview 命令用于在本地启动一个静态服务器,来模拟生产环境,预览你项目构建后的最终效果。
命令参数有哪些?
js
cli
.command('preview [root]', 'locally preview production build')
.option('--host [host]', `[string] specify hostname`, { type: [convertHost] })
.option('--port <port>', `[number] specify port`)
.option('--strictPort', `[boolean] exit if specified port is already in use`)
.option('--open [path]', `[boolean | string] open browser on startup`)
.option('--outDir <dir>', `[string] output directory (default: dist)`)
.action(){
}

配置文件 preview 选项 有哪些?
js
interface CommonServerOptions {
port?: number;
strictPort?: boolean;
host?: string | boolean;
allowedHosts?: string[] | true;
https?: HttpsServerOptions;
open?: boolean | string;
/**
* Configure custom proxy rules for the dev server. Expects an object
* of `{ key: options }` pairs.
* Uses [`http-proxy-3`](https://github.com/sagemathinc/http-proxy-3).
* Full options [here](https://github.com/sagemathinc/http-proxy-3#options).
*
* Example `vite.config.js`:
* ``` js
* module.exports = {
* proxy: {
* // string shorthand: /foo -> http://localhost:4567/foo
* '/foo': 'http://localhost:4567',
* // with options
* '/api': {
* target: 'http://jsonplaceholder.typicode.com',
* changeOrigin: true,
* rewrite: path => path.replace(/^\/api/, '')
* }
* }
* }
* ```
*/
proxy?: Record<string, string | ProxyOptions>;
/**
* Configure CORS for the dev server.
* Uses https://github.com/expressjs/cors.
*
* When enabling this option, **we recommend setting a specific value
* rather than `true`** to avoid exposing the source code to untrusted origins.
*
* Set to `true` to allow all methods from any origin, or configure separately
* using an object.
*
* @default false
*/
cors?: CorsOptions | boolean;
headers?: OutgoingHttpHeaders;
}
示例 有 base 路径
访问根路径 http://localhost:4173/ 会重定向到 http://localhost:4173/vue3-vite-cube/
js
base: '/vue3-vite-cube/',

访问非根路径 http://localhost:4173/log

baseMiddleware 中间件
js
function baseMiddleware(
rawBase: string,
middlewareMode: boolean,
): Connect.NextHandleFunction {
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
return function viteBaseMiddleware(req, res, next) {
const url = req.url!
const pathname = cleanUrl(url)
const base = rawBase
if (pathname.startsWith(base)) {
// rewrite url to remove base. this ensures that other middleware does
// not need to consider base being prepended or not
// 重写 URL,移除 base路径
req.url = stripBase(url, base)
return next()
}
// skip redirect and error fallback on middleware mode, #4057
if (middlewareMode) {
return next()
}
// 根路径访问 ------ 302 重定向到 base
if (pathname === '/' || pathname === '/index.html') {
// redirect root visit to based url with search and hash
res.writeHead(302, {
Location: base + url.slice(pathname.length),
})
res.end()
return
}
// non-based page visit
// 非 base 的页面访问 ------ 404 友好提示
const redirectPath =
withTrailingSlash(url) !== base ? joinUrlSegments(base, url) : base
if (req.headers.accept?.includes('text/html')) {
res.writeHead(404, {
'Content-Type': 'text/html',
})
res.end(
`The server is configured with a public base URL of ${base} - ` +
`did you mean to visit <a href="${redirectPath}">${redirectPath}</a> instead?`,
)
return
} else {
// not found for resources
res.writeHead(404, {
'Content-Type': 'text/plain',
})
res.end(
`The server is configured with a public base URL of ${base} - ` +
`did you mean to visit ${redirectPath} instead?`,
)
return
}
}
}
htmlFallbackMiddleware 中间件
js
/**
* 将浏览器对页面路径的请求回退(fallback)到对应的 HTML 文件
* @param root 项目根目录(开发)或 distDir(预览),用于在文件系统查找 HTM
* @param spaFallback 是否启用 SPA 回退(找不到任何匹配时,最终回退到 /index.html)
* @param clientEnvironment 客户端环境实例,用于访问内存中的文件
* @returns
*/
export function htmlFallbackMiddleware(
root: string,
spaFallback: boolean,
clientEnvironment?: DevEnvironment,
): Connect.NextHandleFunction {
const memoryFiles =
clientEnvironment instanceof FullBundleDevEnvironment
// 全量打包环境,如预览或 SSR 优化场景),优先从 memoryFiles 这个内存 Map 中查找
? clientEnvironment.memoryFiles
: undefined
function checkFileExists(relativePath: string) {
return (
memoryFiles?.has(
relativePath.slice(1), // 去掉前导 /
// fs.existsSync 在磁盘 root 下查找
) ?? fs.existsSync(path.join(root, relativePath))
)
}
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
return function viteHtmlFallbackMiddleware(req, _res, next) {
if (
// Only accept GET or HEAD, 方法必须是 GET 或 HEAD(POST/PUT 等不处理)
(req.method !== 'GET' && req.method !== 'HEAD') ||
// Exclude default favicon requests
// 排除 /favicon.ico(浏览器自动请求的图标,不需要 HTML 回退)
req.url === '/favicon.ico' ||
// Require Accept: text/html or */*
!(
req.headers.accept === undefined || // equivalent to `Accept: */*`
req.headers.accept === '' || // equivalent to `Accept: */*`
req.headers.accept.includes('text/html') ||
req.headers.accept.includes('*/*')
)
) {
return next()
}
// cleanUrl 去掉 ?query 与 #hash
const url = cleanUrl(req.url!)
let pathname
try {
// 解码 URL 编码
pathname = decodeURIComponent(url)
} catch {
// ignore malformed URI
return next()
}
// .html files are not handled by serveStaticMiddleware
// so we need to check if the file exists
// .html 结尾 ------ 直接验证存在性
if (pathname.endsWith('.html')) {
if (checkFileExists(pathname)) {
debug?.(`Rewriting ${req.method} ${req.url} to ${url}`)
req.url = url
return next()
}
}
// trailing slash should check for fallback index.html
// 以 / 结尾 ------ 查找目录下的 index.html
else if (pathname.endsWith('/')) {
if (checkFileExists(joinUrlSegments(pathname, 'index.html'))) {
const newUrl = url + 'index.html'
debug?.(`Rewriting ${req.method} ${req.url} to ${newUrl}`)
req.url = newUrl
return next()
}
}
// non-trailing slash should check for fallback .html
// 无尾斜杠 ------ 查找同名 .html 文件
else {
if (checkFileExists(pathname + '.html')) {
const newUrl = url + '.html'
debug?.(`Rewriting ${req.method} ${req.url} to ${newUrl}`)
req.url = newUrl
return next()
}
}
// SPA 终极回退 ------ 找不到任何匹配时,最终回退到 /index.html
if (spaFallback) {
debug?.(`Rewriting ${req.method} ${req.url} to /index.html`)
req.url = '/index.html'
}
next()
}
}
示例 请求 http://localhost:4173/vue3-vite-cube/home, req.url = /home
- 检查是否以
.html结尾 - 检查 是否
/根路径 - 检查文件
/home.html是否存在 - 回退, url 设置
/index.html

viteAssetMiddleware 中间件
js
const viteAssetMiddleware = (...args: readonly [any, any?, any?]) =>
// 每次请求都会重新调用 sirv(...),创建一个新的 sirv 实例
sirv(distDir, {
etag: true, // 启用 ETag 缓存协商,客户端缓存有效时返回 304,减少带宽
dev: true, // 开发模式,禁用长期缓存头
extensions: [], // 不自动补全扩展名
ignores: false, // 不忽略任何文件
setHeaders(res) {
// 自定义响应头
if (headers) {
for (const name in headers) {
res.setHeader(name, headers[name]!)
}
}
},
shouldServe(filePath) {
// 自定义是否服务的判断
return shouldServeFile(filePath, distDir)
},
})(...args)
app.use(viteAssetMiddleware)
sirv
sirv 是一个轻量级的静态文件服务中间件,专为 Node.js 的 Connect/Express 风格服务器设计。
js
module.exports = function (dir, opts={}) {
dir = resolve(dir || '.');
let isNotFound = opts.onNoMatch || is404;
let setHeaders = opts.setHeaders || noop;
// 默认查找 html/htm,用于"无扩展名 URL → 自动找 index.html"的推断。
let extensions = opts.extensions || ['html', 'htm'];
// 如果开启了 gzip/brotli 预压缩,则构造对应的预压缩扩展名列表(如 html.gz、gz
let gzips = opts.gzip && extensions.map(x => `${x}.gz`).concat('gz');
let brots = opts.brotli && extensions.map(x => `${x}.br`).concat('br');
// 生产模式下用来缓存"文件名 → 文件元信息"的映射表。/
const FILES = {};
let fallback = '/';
let isEtag = !!opts.etag;
let isSPA = !!opts.single;
// 是字符串(如 "app.html"),则去掉扩展名拼成 /app 作为回退路径
if (typeof opts.single === 'string') {
let idx = opts.single.lastIndexOf('.');
fallback += !!~idx ? opts.single.substring(0, idx) : opts.single;
}
let ignores = [];
if (opts.ignores !== false) {
// "任何带扩展名的文件",即带扩展名的请求不回退到 index.html(说明是请求具体资源)
ignores.push(/[/]([A-Za-z\s\d~$._-]+\.\w+){1,}$/); // any extn
// 忽略点文件(.xxx)
if (opts.dotfiles) ignores.push(/\/\.\w/);
// 忽略 .well-known
else ignores.push(/\/\.well-known/);
[].concat(opts.ignores || []).forEach(x => {
ignores.push(new RegExp(x, 'i'));
});
}
// 根据 maxAge 生成 Cache-Control: public,max-age=N
let cc = opts.maxAge != null && `public,max-age=${opts.maxAge}`;
// 追加 ,immutable(适合带 hash 的静态资源,长期缓存)
if (cc && opts.immutable) cc += ',immutable';
// must-revalidate,强制每次校验
else if (cc && opts.maxAge === 0) cc += ',must-revalidate';
// 生产模式预扫描文件
if (!opts.dev) {
totalist(dir, (name, abs, stats) => {
if (/\.well-known[\\+\/]/.test(name)) {} // keep
else if (!opts.dotfiles && /(^\.|[\\+|\/+]\.)/.test(name)) return;
let headers = toHeaders(name, stats, isEtag);
if (cc) headers['Cache-Control'] = cc;
FILES['/' + name.normalize().replace(/\\+/g, '/')] = { abs, stats, headers };
});
}
let lookup = opts.dev ? viaLocal.bind(0, dir + sep, isEtag) : viaCache.bind(0, FILES);
return function (req, res, next) {
let extns = [''];
let pathname = parse(req).pathname;
let val = req.headers['accept-encoding'] || '';
if (gzips && val.includes('gzip')) extns.unshift(...gzips);
if (brots && /(br|brotli)/i.test(val)) extns.unshift(...brots);
// 候选扩展名优先级数组:brotli > gzip > 原始无扩展 > html/htm
extns.push(...extensions); // [...br, ...gz, orig, ...exts]
// 对含 % 的路径做 URI 解码(如 %20 → 空格),解码失败则静默忽略(保留原值)
if (pathname.indexOf('%') !== -1) {
try { pathname = decodeURI(pathname) }
catch (err) { /* malform uri */ }
}
let data = lookup(pathname, extns) || isSPA && !isMatch(pathname, ignores) && lookup(fallback, extns);
if (!data) return next ? next() : isNotFound(req, res);
// 客户端发来的 If-None-Match 与文件 ETag 一致 → 直接返回 304 Not Modified,不传输 body,节省带宽
if (isEtag && req.headers['if-none-match'] === data.headers['ETag']) {
res.writeHead(304);
return res.end();
}
// 启用了预压缩时设置 Vary: Accept-Encoding
if (gzips || brots) {
res.setHeader('Vary', 'Accept-Encoding');
}
setHeaders(res, pathname, data.stats);
send(req, res, data.abs, data.stats, data.headers);
};
}
function viaLocal(dir, isEtag, uri, extns) {
// 如 uri="/about"、extns=['','html'] 会生成
// ['/about', '/about/index', '/about.html', '/about/index.html']
let i=0, arr=toAssume(uri, extns);
let abs, stats, name, headers;
for (; i < arr.length; i++) {
abs = normalize(
join(dir, name=arr[i])
);
if (abs.startsWith(dir) && fs.existsSync(abs)) {
stats = fs.statSync(abs);
if (stats.isDirectory()) continue;
headers = toHeaders(name, stats, isEtag);
headers['Cache-Control'] = isEtag ? 'no-cache' : 'no-store';
return { abs, stats, headers };
}
}
}
浏览器请求 http://localhost:4173/vue3-vite-cube/public/index-CHsDdILU.js


