Лян Сяоин, главный инженер отдела передовых технологий WeDoctor, — девочка-свинка, которая любит плавать и читать.
Проанализируйте версию Vite: 2.2.3, пойдем со мной в путешествие по исследованию сервера vite~(❦ω❦)
1. Что делает начальная служба запуска cli?
Bin package.json указывает исполняемый файл:
"bin": {
"vite": "bin/vite.js"
}
При установке пакета vite с полем bin исполняемый файл будет связан с ./node_modules/.bin текущего проекта, поэтому npm создаст файл vite.js из файла vite.js в /usr/local/bin/vite. Симлинки (которые позволяют запускать vite прямо из командной строки) демонстрируются следующим образом:В локальном проекте также очень удобно использовать npm для выполнения скриптов (скрипты в файле package.json можно запускать напрямую: 'node node_modules/.bin/vite')
Так что же делает vite.js?
cli.ts — это настоящая служба запуска, и выполните соответствующую настройку команды cli:
import { cac } from 'cac' // 是一个用于构建 CLI 应用程序的 JavaScript 库
const cli = cac('vite')
cli
.option('-c, --config <file>', `[string] use specified config file`) // 明确的 config 文件名称,默认 vite.config.js .ts .mjs
.option('-r, --root <path>', `[string] use specified root directory`) // 根路径,默认是当前路径 process.cwd()
.option('--base <path>', `[string] public base path (default: /)`) // 在开发或生产中使用的基本公共路径,默认'/'
.option('-l, --logLevel <level>', `[string] silent | error | warn | all`) // 日志级别
.option('--clearScreen', `[boolean] allow/disable clear screen when logging`) // 打日志的时候是否允许清屏
.option('-d, --debug [feat]', `[string | boolean] show debug logs`) // 配置展示 debug 的日志
.option('-f, --filter <filter>', `[string] filter debug logs`) // 筛选 debug 日志
// dev 的命令[这是我们的讨论重点 -- devServer]
cli
.command('[root]') // default command
.alias('serve') // 别名,即为 `vite serve`命令 = `vite`命令
.option('--host [host]', `[string] specify hostname`)
.option('--port <port>', `[number] specify port`) // --host 指定 port (默认值:3000)
.option('--https', `[boolean] use TLS + HTTP/2`) // --https 使用 https (默认值:false)
.option('--open [path]', `[boolean | string] open browser on startup`) // --open 在服务器启动时打开浏览器
.option('--cors', `[boolean] enable CORS`) // --cors 启动跨域
.option('--strictPort', `[boolean] exit if specified port is already in use`)
.option('-m, --mode <mode>', `[string] set env mode`) // --mode 指定环境模式
.option(
'--force',
`[boolean] force the optimizer to ignore the cache and re-bundle` // 优化器有缓存,--force true 强制忽略缓存,重新打包
)
.action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {
const { createServer } = await import('./server')
try {
const server = await createServer({ // 创建了 server,接下来我们重点讨论 server 做了什么
root,
base: options.base,
mode: options.mode,
configFile: options.config,
logLevel: options.logLevel,
clearScreen: options.clearScreen,
server: cleanOptions(options) as ServerOptions
})
await server.listen()
} catch (e) {
.......
}
})
// build 的命令:生产环境构建
cli
.command('build [root]')
。。。。。。。
// preview 的命令:预览构建效果
cli
.command('preview [root]')
// optimize 的命令:预优化
cli
.command('optimize [root]')
。。。。。。。
Проще говоря, когда мы выполняем команду cli из npm run dev, мы выполняем /node_modules/vite/dist/node/cli.js, вызываем метод createServer и передаем vite.config.js или пользовательскую конфигурацию на cli, чтобы создать экземпляр viteDevServer. Далее, каков производственный процесс, с помощью которого мы можем построить viteDevServer~
Во-вторых, состав devServer
5 Основные модули + 15 промежуточных программ:
Попади в точку! ! ! Прежде чем анализировать эти части исходного кода, чтобы облегчить понимание, братья отлаживают их~
- местный код пряжи
- Узел --inspect-brk ломает точку для отладки нашей логики на стороне сервера или отладчика в скрипте, --inspect, а затем yarn inspect запускает службу
"inspect": "node --inspect-brk ./node_modules/.bin/vite --debug lxyDebug"
- Браузер открывает chrome://inspect для отладки:конкретная операция
Три, пять модулей
Прежде всего, мы кратко разберем функции 5 модулей и взаимодействие между каждым модулем.Для более глубокого понимания, пожалуйста, ожидайте последующих статей~
1. WebSocketServer
в основном используютws package, создал новый сервис websocketnew WebSocket.Server()
Используется для отправки информации и прослушивания подключений. Это в основном играет роль отправки различных сообщений в горячем обновлении HRM, и статья HRM будет посвящена описанию ~
2. watcher--FSWatcher
полезно использоватьchokidarВ этой кроссплатформенной библиотеке мониторинга файлов используемые в ней методы также просты для понимания.Если вам интересно, перейдите на Kangkang~
это в основном прослушиваниеadd
unlink
change
, то есть следить за добавлением, удалением и обновлением файлов, тем самым обновляя диаграмму модуляmoduleGraph
, синхронное горячее обновление. [То же, что и выше, в основном для горячих обновлений~]
3. ModuleGraph
График модуля, который отслеживает отношения импорта, сопоставления URL-адреса с файлом и статус hmr. Говоря человеческими словами, этот класс является складом, который может выполнять добавления, удаления, изменения и проверки. Новые данные добавляются в соответствии с зависимостями, обновляются и могут быть найдены в соответствии с идентификатором разрешения, URL-адресом, именем файла и т. д. Цель состоит в том, чтобы обрабатывать зависимости модуля для вас ~
4. pluginContainer
На основе контейнера плагинов Rollup предоставляются некоторые хуки: например, следующие
-
pluginContainer.watchChange
: Всякий раз, когда отслеживаемый файл изменяется, плагин будет уведомлен, и будет выполнена соответствующая обработка. -
pluginContainer.resolveId
: обработать оператор импорта ES6 и, наконец, нужно вернуть идентификатор модуля. -
pluginContainer.load
: выполнить метод загрузки каждого подключаемого модуля объединения, вывести данные ast и т. д. для последующего преобразования pluginContainer.transform. -
pluginContainer.transform
: Каждый подключаемый модуль объединения предоставляет метод преобразования, который выполняется в этом хуке для преобразования различных кодов файлов, например plugin-vue, который преобразует файл vue в код нового формата после выполнения.
Подводя итог, эти хуки создаются с целью преобразования [наш код => новый код в соответствии с правилами, созданными vite] и служат в качестве базовой службы для других модулей.
5. httpServer
Экземпляр http-сервера собственного узла, согласноhttp
https
http2
Обрабатываются разные ситуации. использовалselfsignedПакет генерирует самоподписанный сертификат x509, обеспечивающий сертификацию CA для обеспечения безопасной передачи https.
После прочтения основных модулей давайте посмотрим, какую детальную обработку выполняет промежуточное ПО и какая последовательность рабочих процессов существует~
4. 15 промежуточного программного обеспечения
Посмотрите на исходный код каждого промежуточного программного обеспечения в сочетании с комментариями ниже😄, число немного велико, ключевое промежуточное программное обеспечение, такое как transformMiddleware, вы можете выбрать некоторые ключевые моменты, чтобы увидеть ~
1. timeMiddleware
Под командой --debug начните печать, и промежуточное ПО времени сможет распечатать наше общее время запуска.
// 文件: /server/index.ts
if (process.env.DEBUG) {
middlewares.use(timeMiddleware(root))
}
// 文件:/middleware/time.ts:
const logTime = createDebugger('vite:time')
export function timeMiddleware(root: string): Connect.NextHandleFunction {
return (req, res, next) => {
const start = Date.now()
const end = res.end
res.end = (...args: any[]) => {
// 打印【时间 相对路径】 -- e.g.: 1ms /src/App.vue?vue&type=style&index=0&lang.css
logTime(`${timeFrom(start)} ${prettifyUrl(req.url!, root)}`)
// @ts-ignore
return end.call(res, ...args)
}
next()
}
}
2. corsMiddleware
Промежуточное ПО для междоменной обработки. vite.config.js передает параметр cors как corsOptions пакету cors для реализации различных междоменных сценариев конфигурации.
// 文件: /server/index.ts
// CORS 用于提供可用于通过各种选项启用 CORS 的 Connect / Express 中间件。
import corsMiddleware from 'cors'
// cors (默认启用)
const { cors } = serverConfig
if (cors !== false) {
middlewares.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors))
}
3. proxyMiddleware
Обработка прокси. vite.config.js передает параметр прокси, а базовый пакет http-proxy реализует функцию прокси.
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
port: 3001,
host: 'liang.web.com',
open: true, // 自动打开浏览器
cors: true,
base: '/mybase',
proxy: {
// 字符串简写写法
'/foo1': 'http://liang.web.com:3001/foo2',
// 选项写法
'/api': {
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
},
// 正则表达式写法
'^/fallback/.*': {
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/fallback/, '')
},
'/sunny': {
bypass: (req, res, options) => {
console.log(options)
res.end('sunny hhhhhh')
},
},
'^/404/.*': {
forward: 'http://localhost:3001/',
bypass: (req, res, options) => {
return false // 默认服务器返回是 res.end(404)
}
}
}
}
})
// 文件: /server/index.ts
const { proxy } = serverConfig
if (proxy) { // 启用代理配置
middlewares.use(proxyMiddleware(httpServer, config))
}
// 文件:/middleware/proxy.ts:
// node-http-proxy 是一个支持 websocket 的 HTTP 可编程代理库。它适用于实现诸如反向代理和负载平衡器之类的组件。
import httpProxy from 'http-proxy'
export function proxyMiddleware(
httpServer: http.Server | null,
config: ResolvedConfig
): Connect.NextHandleFunction {
const options = config.server.proxy!
...
const proxy = httpProxy.createProxyServer(opts) as HttpProxy.Server // 创建代理服务器
proxy.on('error', (err) => {...})
if (opts.configure) { // 执行传递的 config 方法
opts.configure(proxy, opts)
}
if (httpServer) {
// 监听 `upgrade` 事件并且代理 WebSocket 请求
httpServer.on('upgrade', (req, socket, head) => {
const url = req.url!
for (const context in proxies) {
if (url.startsWith(context)) { // 如果当前 URL 匹配上要代理的 url
const [proxy, opts] = proxies[context]
if (
(opts.ws || opts.target?.toString().startsWith('ws:')) &&
req.headers['sec-websocket-protocol'] !== HMR_HEADER // 不是 HRM 的 websocket 请求
) {
if (opts.rewrite) {
req.url = opts.rewrite(url)
}
proxy.ws(req, socket, head) // 代理 websocket 方法
}
}
}
})
}
return (req, res, next) => {
const url = req.url!
for (const context in proxies) { // 循环处理传递来的 proxy 对象配置,context 如【'^/fallback/.*'】
if (
(context.startsWith('^') && new RegExp(context).test(url)) ||
url.startsWith(context)
) { // 正则匹配上的 URL 或 字符匹配上的 URL
const [proxy, opts] = proxies[context]
const options: HttpProxy.ServerOptions = {}
if (opts.bypass) { // 执行配置传递的 bypass 方法 - 记录 debug
const bypassResult = opts.bypass(req, res, opts)
......
}
if (opts.rewrite) { // 执行传递的 rewrite 方法
req.url = opts.rewrite(req.url!)
}
proxy.web(req, res, options) // 代理 web 请求
return
}
}
next()
}
}
4. baseMiddleware
базовая обработка путей
// 文件: /server/index.ts
if (config.base !== '/') {
middlewares.use(baseMiddleware(server))
}
// 文件 /middlewares/base.ts
import { parse as parseUrl } from 'url'
export function baseMiddleware({
config
}: ViteDevServer): Connect.NextHandleFunction {
const base = config.base
return (req, res, next) => {
const url = req.url!
const parsed = parseUrl(url)
const path = parsed.pathname || '/'
if (path.startsWith(base)) {
req.url = url.replace(base, '/') // 删除 base..这确保其他中间件不需要考虑是否在 base 上加了前缀
} else if (path === '/' || path === '/index.html') {
res.writeHead(302, { // 302 重定向到 base 路径
Location: base
})
res.end()
return
} else if (req.headers.accept?.includes('text/html')) {
// non-based page visit
res.statusCode = 404
res.end(xxx)
return
}
next()
}
}
5. launchEditorMiddleware
Откройте файл с номерами строк в редакторе Node.js.
import launchEditorMiddleware from 'launch-editor-middleware'
middlewares.use('/__open-in-editor', launchEditorMiddleware())
6. pingPongMiddleware
HMR повторно подключил обнаружение сердцебиения
middlewares.use('/__vite_ping', (_, res) => res.end('pong'))
7. decodeURIMiddleware
Промежуточное программное обеспечение sirv находит URL-адрес файла, который необходимо декодировать, поэтому необходимо заранее декодировать ключ, соответствующий значению объекта parsedUrl.
// decode 请求 URL
middlewares.use(decodeURIMiddleware())
8. servePublicMiddleware
// 在/ public 下提供静态文件
// 这在转换中间件之前应用,以便提供这些文件就像没有变换一样。
middlewares.use(servePublicMiddleware(config.publicDir))
// 文件 /server/middleware/static.ts
import sirv from 'sirv'
export function servePublicMiddleware(dir: string): Connect.NextHandleFunction {
const serve = sirv(dir, sirvOptions) // 这个插件可以处理静态服务
return (req, res, next) => {
// 跳过 import 的请求,如 /src/components/HelloWorld.vue?import&t=1620397982037
if (isImportRequest(req.url!)) {
return next()
}
serve(req, res, next)
}
}
9. transformMiddleware
cacheDir: по умолчанию /node_modules/.vite в пути к проекту.
// 核心转换器 middleware
middlewares.use(transformMiddleware(server))
Основная логика: добавьте текущий URL-адрес запроса в поддерживаемый moduleGraph и верните обработанный новый код; Основной метод --transformRequesт: Этот метод выполняет кэширование, запрос ресурсов, синтаксический анализ, загрузку и операции преобразования. Если кеш попал, результат преобразования будет возвращен напрямую, в противном случае будут выполнены следующие операции:
- pluginContainer.resolveId(url)?.id: получить новый идентификатор разрешения
- pluginContainer.load(id): Согласно полученному выше идентификатору, хук создает карту [информация об исходной карте] и код [возвращает код клиента]
- Поместите новый модуль в moduleGraph и используйте наблюдатель для мониторинга module.file.
- Обработайте на карте информацию, связанную с sourceMap, например вставьте содержимое исходного кода:
injectSourceContent
- Объединение информации в возвращаемый объект
mod.transformResult = {
code, // plugin.transform 后返回给客户端的代码
map, // 处理后的 sourceMap 信息
etag: getEtag(code, { weak: true }) // etag 插件生成
}
Исходный код слишком много, сделай сам~1) Обработка запросов js: /src/main.js:Просмотр возвращенного результата кода после преобразования:
2) Ручка? Импорт запросов: Сценарий: обновить линию код HelloWorld.Vue, горячего обновления входящих запросов3) Обработка запросов css
10. serveRawFsMiddleware
иметь дело с/@fs/
URL для получения исходного пути
// 文件 /server/middleware/static.ts
export function serveRawFsMiddleware(): Connect.NextHandleFunction {
const isWin = os.platform() === 'win32'
const serveFromRoot = sirv('/', sirvOptions)
return (req, res, next) => {
let url = req.url!
if (url.startsWith(FS_PREFIX)) { // 以`/@fs/`开头的 URL
url = url.slice(FS_PREFIX.length) // 取原有的路径
if (isWin) url = url.replace(/^[A-Z]:/i, '')
req.url = url
serveFromRoot(req, res, next)
} else {
next()
}
}
}
11. serveStaticMiddleware
// 文件 /server/middleware/static.ts
export function serveStaticMiddleware(
dir: string,
config: ResolvedConfig
): Connect.NextHandleFunction {
const serve = sirv(dir, sirvOptions) // 传递 dir=root, 根路径下的静态服务
return (req, res, next) => {
const url = req.url!
// 仅在不是 html 请求的情况下处理文件,以便 html 请求可以进入我们的 html 中间件特殊处理
if (path.extname(cleanUrl(url)) === '.html') {
return next()
}
// 也将别名应用于静态请求
let redirected: string | undefined
for (const { find, replacement } of config.resolve.alias) {
const matches =
typeof find === 'string' ? url.startsWith(find) : find.test(url)
if (matches) {
redirected = url.replace(find, replacement)
break
}
}
if (redirected) {
// dir 已预先标准化为 posix 样式
if (redirected.startsWith(dir)) {
redirected = redirected.slice(dir.length)
}
req.url = redirected
}
serve(req, res, next)
}
}
12. spaMiddleware
Обработка SPA: укажите index.html по пути, соответствующему URL-адресу, по умолчанию используется файл /index.html.
// 该中间件通过指定的索引页代理请求,对于使用 HTML5 history API 的单页应用程序非常有用。
import history from 'connect-history-api-fallback'
if (!middlewareMode) {
middlewares.use(
history({
logger: createDebugger('vite:spa-fallback'),
// 支持/ dir /,没有明确的 index.html
rewrites: [
{
from: /\/$/,
to({ parsedUrl }: any) {
const rewritten = parsedUrl.pathname + 'index.html'
if (fs.existsSync(path.join(root, rewritten))) {
return rewritten
} else {
return `/index.html`
}
}
}
]
})
)
}
13. indexHtmlMiddleware
if (!middlewareMode) {
// 转换入口文件 index.html
middlewares.use(indexHtmlMiddleware(server))
}
14. 404Middleware
if (!middlewareMode) {
// 处理 404
middlewares.use((_, res) => {
res.statusCode = 404
res.end()
})
}
15. errorMiddleware
// error handler
middlewares.use(errorMiddleware(server, middlewareMode))
// 文件 /server/middleware/error.ts
export function errorMiddleware(
server: ViteDevServer,
allowNext = false // 是否允许程序进行,否则返回错误状态码 500
): Connect.ErrorHandleFunction {
// 请注意,必须保留 4 个 arg 才能进行 connect,以将其视为错误中间件
return (err: RollupError, _req, res, next) => {
const msg = buildErrorMessage(err, [
chalk.red(`Internal server error: ${err.message}`)
])
server.config.logger.error(msg, { // 日志记录错误
clear: true,
timestamp: true
})
server.ws.send({ // websocket 发送错误
type: 'error',
err: prepareError(err)
})
if (allowNext) {
next()
} else {
res.statusCode = 500 // 返回 500 服务错误
res.end()
}
}
}
Пять, резюме createServer
Это имеет cli всоздать серверметод~ В заключение:
Эта статья начинается с команды cli на первом этапе, что делает команда vite, а затем поможет вам найти запись createServer.
Во-вторых, мы глубоко изучаем "внутренние органы", необходимые для createServer. Есть 5 модулей, таких как websocketServer, fsWatcher и moduleGraph для поддержки необходимых частей. Согласно разделению труда 15 промежуточного программного обеспечения, весь процесс обработки связан последовательно , и наконец наш devServer отполирован. .
Мы видим, есть ли какие-либо следы пакетов во всем процессе, vite очень хорошо использует esmodule для достижения своевременной и эффективной горячей перезагрузки модулей, быстрого холодного запуска и большого опыта разработки 👌🏻