vite8 vite preview 命令做了什么?

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/homereq.url = /home

  1. 检查是否以 .html 结尾
  2. 检查 是否 / 根路径
  3. 检查文件 /home.html 是否存在
  4. 回退, 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

最后

  1. 配置项
相关推荐
米丘1 小时前
Vite 构建工具
vite
blanks202018 小时前
生成 公钥私钥 笔记
node.js
糖拌西瓜皮2 天前
Java开发者视角:深入理解Node.js异步编程模型
java·后端·node.js
智通3 天前
Node.js事件循环核心机制
node.js
初圣魔门首席弟子3 天前
Node.js 详细介绍(知识库版)
windows·qt·node.js·知识库
糖拌西瓜皮3 天前
Java 开发者如何快速上手 Node.js:一份从入门到进阶的学习路线
node.js
yspwf3 天前
NestJS 配置管理完整方案
后端·架构·node.js
网络点点滴3 天前
Node.js事件驱动架构
架构·node.js
weixin_471383033 天前
Node.js + Express 入门实战笔记-01-基础
node.js·lua·express