«Эта статья участвовала в мероприятии Haowen Convocation Order, щелкните, чтобы просмотреть:Двойные заявки на внутреннюю и внешнюю стороны, призовой фонд в 20 000 юаней ждет вас, чтобы бросить вызов!"
Vite — это инструмент веб-разработки, разработанный автором Vue Ю Юйси, Ю Юйси дал краткое представление о Vite при его продвижении на Weibo:
Vite, сервер разработки, основанный на импорте ES из браузера. Используйте браузер для анализа импорта, компиляции и возврата по запросу на стороне сервера, полностью пропуская концепцию упаковки, и сервер можно использовать по мере необходимости. При этом он не только поддерживает файлы Vue, но и обрабатывает горячие обновления, причем скорость горячих обновлений не будет замедляться по мере увеличения количества модулей. Для рабочих сред тот же код можно упаковать с помощью Rollup. Несмотря на то, что пока это относительно грубо, я думаю, что у этого направления есть потенциал, и если оно будет сделано хорошо, оно может полностью решить проблему полдня горячих обновлений, таких как изменение строки кода.
Мы можем извлечь некоторую ключевую информацию из этого отрывка.
- Vite основан на ESM, поэтому он реализует быстрый запуск и возможность мгновенного горячего обновления модуля;
- Vite реализует компиляцию по запросу на стороне сервера.
Итак, скажем более прямо:В среде разработки нет процесса упаковки и сборки Vite.
Синтаксис импорта ESM, прописанный разработчиком в коде, будет отправлен непосредственно на сервер, и сервер также будет напрямую обрабатывать содержимое модуля ESM и отправлять его в браузер. Далее современные браузеры делают HTTP-запросы для каждого импортированного модуля, анализируя модуль сценария, а сервер продолжает обрабатывать эти HTTP-запросы и отвечать на них.
Интерпретация принципа реализации Vite
Строительство окружающей среды
Идея Vite относительно проста для понимания и несложна в реализации. Далее давайте проанализируем исходный код Vite.
Сначала мы создаем среду обучения, создаем приложение на основе Vite и запускаем:
$ yarn global add vite
$ npm init vite-app vite-app
$ cd vite-app
$ yarn
$ yarn dev
Вы получите структуру каталогов, как показано ниже:
Стартовый проект:
$ yarn dev
Где браузер запрашивает: **http://localhost:3000/**, полученный контент является контентом в нашем проекте приложениясодержимое index.html.
Введите исходный код
Вытащитьисходный код,существуетОткройте часть реализации командной строки,
cli
.command('[root]') // default command
.alias('serve')
.option('--host [host]', `[string] specify hostname`)
.option('--port <port>', `[number] specify port`)
.option('--https', `[boolean] use TLS + HTTP/2`)
.option('--open [path]', `[boolean | string] open browser on startup`)
.option('--cors', `[boolean] enable CORS`)
.option('--strictPort', `[boolean] exit if specified port is already in use`)
.option('-m, --mode <mode>', `[string] set env mode`)
.option(
'--force',
`[boolean] force the optimizer to ignore the cache and re-bundle`
)
.action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {
// output structure is preserved even after bundling so require()
// is ok here
const { createServer } = await import('./server')
try {
const server = await createServer({
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) {
createLogger(options.logLevel).error(
chalk.red(`error when starting dev server:\n${e.stack}`)
)
process.exit(1)
}
})
пройти черезcreateServerдля запуска службы http, отвечающей на запросы браузера.
const { createServer } = await import('./server')
createServerРеализация метода, код выглядит следующим образом
export async function createServer(inlineConfig) {
// 配置文件处理
const config = await resolveConfig(inlineConfig, 'serve', 'development')
const root = config.root
const serverConfig = config.server
const httpsOptions = await resolveHttpsConfig(config)
let { middlewareMode } = serverConfig
// 以中间件模式创建 vite 服务器,不使用 vite 创建的服务器
if (middlewareMode === true) {
middlewareMode = 'ssr'
}
const middlewares = connect()
// 创建一个 http 实例,注意,这里如果 middlewareMode = 'ssr' 则使用中间件来创建服务器
const httpServer = middlewareMode
? null
: await resolveHttpServer(serverConfig, middlewares, httpsOptions)
// HMR 连接
const ws = createWebSocketServer(httpServer, config, httpsOptions)
const { ignored = [], ...watchOptions } = serverConfig.watch || {}
// 文件监听
const watcher = chokidar.watch(path.resolve(root), {
ignored: ['**/node_modules/**', '**/.git/**', ...ignored],
ignoreInitial: true,
ignorePermissionErrors: true,
disableGlobbing: true,
...watchOptions
})
const plugins = config.plugins
const container = await createPluginContainer(config, watcher)
const moduleGraph = new ModuleGraph(container)
const closeHttpServer = createServerCloseFn(httpServer)
// eslint-disable-next-line prefer-const
let exitProcess
const server = {
config: config,
middlewares,
get app() {
config.logger.warn(
`ViteDevServer.app is deprecated. Use ViteDevServer.middlewares instead.`
)
return middlewares
},
httpServer,
watcher,
pluginContainer: container,
ws,
moduleGraph,
transformWithEsbuild,
transformRequest(url, options) {
return transformRequest(url, server, options)
},
transformIndexHtml: null,
ssrLoadModule(url) {
if (!server._ssrExternals) {
server._ssrExternals = resolveSSRExternal(
config,
server._optimizeDepsMetadata
? Object.keys(server._optimizeDepsMetadata.optimized)
: []
)
}
return ssrLoadModule(url, server)
},
ssrFixStacktrace(e) {
if (e.stack) {
e.stack = ssrRewriteStacktrace(e.stack, moduleGraph)
}
},
listen(port, isRestart) {
return startServer(server, port, isRestart)
},
async close() {
process.off('SIGTERM', exitProcess)
if (!middlewareMode && process.env.CI !== 'true') {
process.stdin.off('end', exitProcess)
}
await Promise.all([
watcher.close(),
ws.close(),
container.close(),
closeHttpServer()
])
},
_optimizeDepsMetadata: null,
_ssrExternals: null,
_globImporters: {},
_isRunningOptimizer: false,
_registerMissingImport: null,
_pendingReload: null
}
server.transformIndexHtml = createDevHtmlTransformFn(server)
exitProcess = async () => {
try {
await server.close()
} finally {
process.exit(0)
}
}
// 如果收到终止信号句柄,停止服务
process.once('SIGTERM', exitProcess)
if (!middlewareMode && process.env.CI !== 'true') {
process.stdin.on('end', exitProcess)
}
watcher.on('change', async (file) => {
file = normalizePath(file)
// invalidate module graph cache on file change
moduleGraph.onFileChange(file)
if (serverConfig.hmr !== false) {
try {
await handleHMRUpdate(file, server)
} catch (err) {
ws.send({
type: 'error',
err: prepareError(err)
})
}
}
})
watcher.on('add', (file) => {
handleFileAddUnlink(normalizePath(file), server)
})
watcher.on('unlink', (file) => {
handleFileAddUnlink(normalizePath(file), server, true)
})
// 插件处理
// apply server configuration hooks from plugins
const postHooks = []
for (const plugin of plugins) {
if (plugin.configureServer) {
postHooks.push(await plugin.configureServer(server))
}
}
// 下面是一些中间件的处理
// Internal middlewares ------------------------------------------------------
// request timer
// 请求时间调试
if (process.env.DEBUG) {
middlewares.use(timeMiddleware(root))
}
// cors (enabled by default)
const { cors } = serverConfig
if (cors !== false) {
middlewares.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors))
}
// proxy
const { proxy } = serverConfig
if (proxy) {
middlewares.use(proxyMiddleware(httpServer, config))
}
// base
if (config.base !== '/') {
middlewares.use(baseMiddleware(server))
}
// open in editor support
middlewares.use('/__open-in-editor', launchEditorMiddleware())
// hmr reconnect ping
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
middlewares.use('/__vite_ping', function viteHMRPingMiddleware(_, res) {
res.end('pong')
})
//decode request url
middlewares.use(decodeURIMiddleware())
// serve static files under /public
// this applies before the transform middleware so that these files are served
// as-is without transforms.
if (config.publicDir) {
middlewares.use(servePublicMiddleware(config.publicDir))
}
// main transform middleware
middlewares.use(transformMiddleware(server))
// serve static files
middlewares.use(serveRawFsMiddleware(server))
middlewares.use(serveStaticMiddleware(root, config))
// spa fallback
if (!middlewareMode || middlewareMode === 'html') {
middlewares.use(
history({
logger: createDebugger('vite:spa-fallback'),
// support /dir/ without explicit index.html
rewrites: [
{
from: /\/$/,
to({ parsedUrl }) {
const rewritten = parsedUrl.pathname + 'index.html'
if (fs.existsSync(path.join(root, rewritten))) {
return rewritten
} else {
return `/index.html`
}
}
}
]
})
)
}
// run post config hooks
// This is applied before the html middleware so that user middleware can
// serve custom content instead of index.html.
postHooks.forEach((fn) => fn && fn())
if (!middlewareMode || middlewareMode === 'html') {
// transform index.html
middlewares.use(indexHtmlMiddleware(server))
// handle 404s
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
middlewares.use(function vite404Middleware(_, res) {
res.statusCode = 404
res.end()
})
}
// error handler
middlewares.use(errorMiddleware(server, !!middlewareMode))
const runOptimize = async () => {
if (config.cacheDir) {
server._isRunningOptimizer = true
try {
server._optimizeDepsMetadata = await optimizeDeps(config)
} finally {
server._isRunningOptimizer = false
}
server._registerMissingImport = createMissingImporterRegisterFn(server)
}
}
if (!middlewareMode && httpServer) {
// overwrite listen to run optimizer before server start
const listen = httpServer.listen.bind(httpServer)
httpServer.listen = (async (port, ...args) => {
try {
await container.buildStart({})
await runOptimize()
} catch (e) {
httpServer.emit('error', e)
return
}
return listen(port, ...args)
})
httpServer.once('listening', () => {
// update actual port since this may be different from initial value
serverConfig.port = (httpServer.address()).port
})
} else {
await container.buildStart({})
await runOptimize()
}
return server
}
Код очень длинный, вкратце он делает следующие вещи:
- Создайте сервер, который действует как статический сервер и отвечает на запросы приложений.
- Создайте веб-сокет, предоставьте HMR
- Используйте chokidar для включения мониторинга файлов и обработки изменений файлов.
- Работа с плагинами
- Слушайте дескриптор и останавливайте службу, если она встречает стоп-сигнал.
Роль запуска сервера
браузер посещаетhttp://localhost:3000/После этого у меня получилось следующее:
<body>
<di v id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
В соответствии с реализацией спецификации ESM в теге сценария браузера для<script type="module" src="./bar.js"></script>
Содержание: при предъявленииscript
Этикеткаtype
собственностьmodule
, браузер отправит соответствующее содержимое модуля HTTP-запроса. Обрабатывается сервером Vite.
Мы видим, что после обработки Vite Serverhttp://localhost:3000/src/main.jsПосле запроса наконец возвращается содержимое вышеуказанной картинки. Однако этот контент и наш проект./src/main.js
есть разница
Исходный код такой
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
createApp(App).mount('#app')
После Вайта стало так
import { createApp } from '/@modules/vue.js'
import App from '/src/App.vue'
import '/src/index.css?import'
createApp(App).mount('#app')
Здесь мы разделяем его на две части.
вimport { createApp } from 'vue'
изменить наimport { createApp } from '/@modules/vue.js'
, по понятным причинам:import
Соответствующий путь поддерживает только"/""./"
или"../"
Контент в начале, используйте имя модуля напрямуюimport
, об ошибке будет сообщено немедленно.
Поэтому, когда Vite Server обрабатывает запрос, разрешите этот плагин, чтобы датьimport from 'A' 的 A
Добавить к/@module/
с префиксомfrom '/@modules/A'
,Соответствует части исходного кода.
Весь процесс и ссылка на вызов длиннее, я простую резюме для метода импорта обработки Vite:
-
Получить содержимое тела, соответствующее пути запроса в createServer;
-
Разобрать ресурс AST через es-module-lexer и получить содержимое импорта;
-
Если установлено, что импортированный ресурс является абсолютным путем, можно считать, что ресурс является модулем npm, и возвращается обработанный путь к ресурсу. Например, в приведенном выше коде
vue → /@modules/vue
.
Для таких форм, как:import App from './App.vue'
а такжеimport './index.css'
Обработка аналогична вышеописанной:
-
Получить содержимое тела, соответствующее пути запроса в createSercer;
-
Разобрать ресурс AST через es-module-lexer и получить содержимое импорта;
-
Если установлено, что импортированный ресурс является относительным путем, этот ресурс можно рассматривать как ресурс в приложении проекта, и возвращается обработанный путь к ресурсу. Например, в приведенном выше коде
./App.vue → /src/App.vue
.
Далее браузер запрашивает отдельно по содержимому main.js:
/@modules/vue.js
/src/App.vue
/src/index.css?import
для/@module/
Запросы класса проще, нам нужно всего лишь выполнить следующие три шага:
-
Получите содержимое тела, соответствующее пути запроса в промежуточном программном обеспечении createServer;
-
Определите, начинается ли путь с /@module/, и если да, удалите имя пакета (здесь vue.js);
-
Перейдите к файлу node_modules, чтобы найти соответствующую библиотеку npm и вернуть содержимое.
Вышеуказанные шаги используются в ViteresolveРеализация промежуточного программного обеспечения.
Тогда правильно/src/App.vue
Запросы класса обрабатываются, что задействует возможности компиляции сервера Vite.
Давайте сначала посмотрим на результаты.По сравнению с App.vue в проекте результаты, полученные по запросу браузера, очевидно, сильно изменились:
Фактически,App.vue
Такая однофайловая компонента соответствуетscript、style
а такжеtemplate
, при обработке Vite Server сервер будетscript、style 和 template
Три части обрабатываются отдельно, и соответствующее промежуточное ПО@vitejs/plugin-vue
. Реализация этого плагина очень проста, т.е..vue
Запрос файла обрабатывается, компонент с одним файлом анализируется методом parseSFC, иcompileSFCMain
Метод разбивает однофайловый компонент на содержимое, как показано выше, а ключевое содержимое соответствующего промежуточного программного обеспечения можно найти в исходном коде vuePlugin. исходный код, включающийparseSFC
Что конкретно делается, так это звонить@vue/compiler-sfc
Выполнение парсинга компонентов одного файла. Сжато до моей собственной логики, чтобы помочь вам понять:
В общем, каждый.vue
Однофайловые компоненты разбиваются на несколько запросов. Например, в соответствии с приведенным выше сценарием после того, как браузер получит фактический контент, соответствующий App.vue, он отправляетHelloWorld.vue
так же какApp.vue?type=template
запрос (представленный типом этого запроса является шаблоном или стилем).createServer
Обрабатываются и возвращаются отдельно, эти запросы по-прежнему отдельно упоминаются выше.@vitejs/plugin-vue
Обработка плагинов: дляtemplate
запросы, которые использует сервис@vue/compiler-dom
компилироватьtemplate
и вернуть содержимое.
для вышеупомянутогоhttp://localhost:3000/src/index.css?import
запрос немного особенный, вcss
плагин, черезcssPostPlugin
объектtransform
реализовать синтаксический анализ:
transform(css, id, ssr) {
if (!cssLangRE.test(id) || commonjsProxyRE.test(id)) {
return
}
const modules = cssModulesCache.get(config)!.get(id)
const modulesCode =
modules && dataToEsm(modules, { namedExports: true, preferConst: true })
if (config.command === 'serve') {
if (isDirectCSSRequest(id)) {
return css
} else {
// server only
if (ssr) {
return modulesCode || `export default ${JSON.stringify(css)}`
}
return [
`import { updateStyle, removeStyle } from ${JSON.stringify(
path.posix.join(config.base, CLIENT_PUBLIC_PATH)
)}`,
`const id = ${JSON.stringify(id)}`,
`const css = ${JSON.stringify(css)}`,
`updateStyle(id, css)`,
// css modules exports change on edit so it can't self accept
`${modulesCode || `import.meta.hot.accept()\nexport default css`}`,
`import.meta.hot.prune(() => removeStyle(id))`
].join('\n')
}
}
// build CSS handling ----------------------------------------------------
// record css
styles.set(id, css)
return {
code: modulesCode || `export default ${JSON.stringify(css)}`,
map: { mappings: '' },
// avoid the css module from being tree-shaken so that we can retrieve
// it in renderChunk()
moduleSideEffects: 'no-treeshake'
}
},
передачаcssPostPlugin
серединаtransform
метод:
Этот метод будет выполняться в браузереupdateStyle
метод, какhttp://localhost:3000/src/index.css?import
Исходный код выглядит следующим образом:
import { createHotContext as __vite__createHotContext } from "/@vite/client";import.meta.hot = __vite__createHotContext("/src/components/HelloWorld.vue?vue&type=style&index=0&scoped=true&lang.css");import { updateStyle, removeStyle } from "/@vite/client"
const id = "/Users/study/vite-app/src/components/HelloWorld.vue?vue&type=style&index=0&scoped=true&lang.css"
const css = "\nh1[data-v-469af010] {\n font-size:18px;\n}\n"
updateStyle(id, css)
import.meta.hot.accept()
export default css
import.meta.hot.prune(() => removeStyle(id))
Наконец-то закончил вставлять стили в браузер.
На данный момент мы проанализировали и перечислили больше исходного контента. Вышеупомянутый контент необходимо разобрать шаг за шагом, и настоятельно рекомендуется открыть исходный код Vite и проанализировать его самостоятельно. Если у вас все еще есть «облака и туман», когда вы видите это, не теряйте терпения.Объедините это с диаграммой ниже и прочитайте ее еще раз, я думаю, это будет более полезно.
Сравните с вебпаком
Идея вебпака без пакетов
Идея Vite без комплектов:
Суммировать
-
Vite использует функцию, которую браузеры изначально поддерживают ESM, опускает упаковку модулей и не требует создания пакетов, поэтому первоначальный запуск выполняется быстрее, а функция HMR удобна.
-
В режиме разработки Vite при запуске сервера Node перезапись модуля (например, синтаксический анализ и компиляция одного файла и т. д.) и обработка запросов выполняются на стороне сервера для достижения реальной компиляции по требованию.
-
Вся логика Vite Server в основном зависит от реализации промежуточного программного обеспечения. Это промежуточное ПО после перехвата запроса выполняет следующее:
-
Обработка синтаксиса ESM, например преобразование путей импорта сторонних зависимостей в бизнес-коде в пути зависимостей, распознаваемые браузером;
-
Своевременная компиляция .ts, .vue и других файлов;
-
Скомпилируйте модули Sass/Less, которые необходимо предварительно скомпилировать;
-
Установите сокетное соединение с браузером для реализации HMR.
-