Потратьте десять минут, чтобы понять принцип Vite

JavaScript
Потратьте десять минут, чтобы понять принцип Vite

1 Обзор

Viteлегче и быстрееwebИнструменты разработки приложений для современных браузеров. Нижняя часть основана наECMAScriptСтандартная система родного модуляES Moduleвыполнить.他的出现是为了解决webpackВремя холодного пуска слишком велико иWebpack HMRОтвет на горячее обновление медленный и другие проблемы.

по умолчаниюViteСозданный проект является обычнымVue3приложение, по сравнению сVue-cliПриложение создается с гораздо меньшим количеством файлов конфигурации и зависимостей.

ViteСозданный проект требует очень мало зависимостей разработки, толькоVite和@vue/compiler-sfc. здесьViteэто беговой инструмент,compiler-sfcсостоит в том, чтобы скомпилировать.vueОднофайловый компонент в конце. Также можно поддерживать использование других фреймворков, указав разные шаблоны при создании проекта, такого какReact. После создания проекта его можно запустить и упаковать двумя командами.

# 开启服务器
vite serve
# 打包
vite build

просто такViteначалwebСлужбу не нужно компилировать и упаковывать, поэтому скорость запуска очень высока.Большая часть кода, работающего на этапе отладки, — это код, который вы пишете в редакторе.webpackРендеринг после компиляции действительно намного быстрее. Конечно, производственную среду еще нужно упаковать, ведь мы много раз используем самые последниеESспецификация пока не поддерживается в браузерах,Viteпроцесс упаковки иwebpackТочно так же все файлы будут скомпилированы и упакованы вместе. Необходимость вырезания кодаViteОн реализован с помощью собственного динамического импорта, поэтому результат упаковки может поддерживать только современные браузеры.Если вам нужна совместимость со старыми браузерами, вы можете импортироватьPolyfill.

В дополнение к использованию Webpack для упаковки, поскольку среда браузера не поддерживает модульность и новый синтаксис, существует также тот факт, что файлы модулей будут генерировать многоhttpпросить. Если вы разрабатываете модульным способом, на странице будут десятки или даже десятки модулей, и часто их будет несколько.kbфайл, откройте страницу для загрузки десятковjsресурсы Это явно необоснованно.

ViteСозданные проекты, требующие небольшой дополнительной настройки, уже поддерживаются по умолчанию.TS,Less, Sass,Stylus,postcss, но вам нужно установить соответствующий компилятор отдельно, и он также поддерживает дефолтныйjsxа такжеWeb Assembly.

ViteПреимущество заключается в повышении опыта разработчика в процессе разработки,webСервер разработки можно запустить сразу, без ожидания, горячее обновление модулей происходит практически в режиме реального времени, а необходимые файлы компилируются по запросу, чтобы избежать компиляции неиспользуемых файлов. и из коробки избежатьloaderа такжеpluginsКонфигурация.

ViteОсновные функции включают в себя открытие статическогоwebсервер, способный компилировать однофайловые компоненты и предоставлятьHMRФункция. при запускеviteКогда текущий каталог проекта используется в качестве корневого каталога статического сервера, статический сервер будет перехватывать некоторые запросы, компилировать в реальном времени, когда запрашивается один файл, и обрабатывать модули, которые другие браузеры не могут распознать.websocketвыполнитьhmr.

2. Внедрить статический тестовый сервер

Сначала реализуйте функцию, которая включает статическиеwebИнструмент командной строки для сервера.vite1.xВнутренне используетсяKoaреализовать статический сервер. (ps: Инструмент командной строки узла может просмотреть мою предыдущую статью, которая не будет здесь представлена, просто вставьте код напрямую).

npm init
npm install koa koa-send -D

инструментbinВходной файл установлен как локальныйindex.js

#!/usr/bin/env node

const Koa = require('koa')
const send = require('koa-send')

const app = new Koa()

// 开启静态文件服务器
app.use(async (ctx, next) => {
    // 加载静态文件
    await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html'})
    await next()
})

app.listen(5000)

console.log('服务器已经启动 http://localhost:5000')

Это напишетnodeИнструменты для статических серверов.

3. Работа со сторонними модулями

Наш подход заключается в том, что сторонние модули (node_modulesфайл), вы можете дать ему идентификатор, изменив путь стороннего модуля, а затем получить этот идентификатор в сервере для обработки этого модуля.

Во-первых, вам нужно изменить путь к стороннему модулю, для реализации которого требуется новое промежуточное ПО. Определить, является ли файл, возвращенный в данный момент в браузер,javascript, просто посмотрите на заголовок ответаcontent-type. еслиjavascriptНеобходимо найти путь к модулю, указанный в этом файле.ctx.bodyЭто файл содержимого, возвращаемый браузеру. Данные здесь представляют собойstream, который необходимо преобразовать в строку для обработки.


const stream2string = (stream) => {
    return new Promise((resolve, reject) => {
        const chunks = [];
        stream.on('data', chunk => {chunks.push(chunk)})
        stream.on('end', () => { resolve(Buffer.concat(chunks).toString('utf-8'))})
        stream.on('error', reject)
    })
}

// 修改第三方模块路径
app.use(async (ctx, next) => {
    if (ctx.type === 'application/javascript') {
        const contents = await stream2string(ctx.body);
        // 将body中导入的路径修改一下,重新赋值给body返回给浏览器
        // import vue from 'vue', 匹配到from '修改为from '@modules/
        ctx.body = contents.replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/');
    }
})

Затем начните загружать сторонний модуль.Здесь также необходимо промежуточное программное обеспечение, чтобы определить, начинается ли путь запроса с модифицированного @module.Если да, перейдите в node_modules, чтобы загрузить соответствующий модуль и вернуть его в браузер. Это промежуточное программное обеспечение должно быть размещено перед статическим сервером.

// 加载第三方模块
app.use(async (ctx, next) => {
    if (ctx.path.startsWith('/@modules/')) {
        // 截取模块名称
        const moduleName = ctx.path.substr(10);
    }
})

После получения имени модуля вам необходимо получить входной файл модуля.ES ModuleВходной файл модуля, вам нужно сначала найти входной файл модуляpackage.jsonа потом получить этоpackage.jsonсерединаmoduleЗначение поля также является файлом ввода.

// 找到模块路径
const pkgPath = path.join(process.pwd(), 'node_modules', moduleName, 'package.json');
const pkg = require(pkgPath);
// 重新给ctx.path赋值,需要重新设置一个存在的路径,因为之前的路径是不存在的
ctx.path = path.join('/node_modules', moduleName, pkg.module);
// 执行下一个中间件
awiat next();

Таким образом, хотя запрос браузера поступает по пути @modules, путь пути изменяется перед загрузкой вnode_modulesв пути, чтобы при загрузке он переходил наnode_modulesПолучите файл в формате , и ответьте на загруженный контент в браузере.

// 加载第三方模块
app.use(async (ctx, next) => {
    if (ctx.path.startsWith('/@modules/')) {
        // 截取模块名称
        const moduleName = ctx.path.substr(10);
        // 找到模块路径
        const pkgPath = path.join(process.pwd(), 'node_modules', moduleName, 'package.json');
        const pkg = require(pkgPath);
        // 重新给ctx.path赋值,需要重新设置一个存在的路径,因为之前的路径是不存在的
        ctx.path = path.join('/node_modules', moduleName, pkg.module);
        // 执行下一个中间件
        awiat next();
    }
})

4. Однофайловая обработка компонентов

Я уже говорил, что браузер с этим не справится..vueресурс, браузер может распознать толькоjs,cssи другие общие ресурсы, поэтому другие типы ресурсов необходимо обрабатывать на стороне сервера. Когда запрашивается однофайловый компонент, однофайловый компонент необходимо скомпилировать в js-модуль на сервере и вернуть в браузер.

Итак, когда браузер впервые запрашиваетApp.vue, сервер компилирует однофайловый компонент в объект, сначала загружает компонент, а затем создает объект.

import Hello from './src/components/Hello.vue'
const __script = {
    name: "App",
    components: {
        Hello
    }
}

Затем перейдите к загрузке файла записи, на этот раз сервер скомпилирует шаблон этого однофайлового компонента и вернет функцию рендеринга. Затем смонтируйте функцию рендеринга в только что созданный объект параметров компонента и, наконец, экспортируйте объект параметров.

import { render as __render } from '/src/App.vue?type=template'
__script.render = __render
__script.__hmrId = '/src/App.vue'
export default __script

то естьviteБудут отправлены два запроса, первый запрос скомпилирует однофайловый файл, а второй запрос скомпилирует однофайловый шаблон и вернетrenderфункция.

  1. Параметры компиляции одного файла

Во-первых, давайте реализуем случай запроса одного файла в первый раз. Однофайловый компонент нужно скомпилировать в вариант, который здесь тоже реализован промежуточным ПО. Эта функция необходима после обработки статического сервера и перед обработкой путей сторонних модулей.

Во-первых, вам нужно скомпилировать однофайловые компоненты с помощьюcompiler-sfc.

// 处理单文件组件
app.use(async (ctx, next) => {
    if (ctx.path.endsWith('.vue')) {
        // 获取响应文件内容,转换成字符串
        const contents = await streamToString(ctx.body);
        // 编译文件内容
        const { descriptor } = compilerSFC.parse(contents);
        // 定义状态码
        let code;
        // 不存在type就是第一次请求
        if (!ctx.query.type) {
            code = descriptor.script.content;
            // 这里的code格式是, 需要改造成我们前面贴出来的vite中的样子
            // import Hello from './components/Hello.vue'
            // export default {
            //      name: 'App',
            //      components: {
            //          Hello
            //      }
            //  }
            // 改造code的格式,将export default 替换为const __script =
            code = code.relace(/export\s+default\s+/g, 'const __script = ')
            code += `
                import { render as __render } from '${ctx.path}?type=template'
                __script.rener = __render
                export default __script
            `
        }
        // 设置浏览器响应头为js
        ctx.type = 'application/javascript'
        // 将字符串转换成数据流传给下一个中间件。
        ctx.body = stringToStream(code);
    }
    await next()
})

const stringToStream = text => {
    const stream = new Readable();
    stream.push(text);
    stream.push(null);
    return stream;
}
npm install @vue/compiler-sfc -D

Затем мы обработаем второй запрос однофайлового компонента, второй запросurlпринесуtype=templateпараметр, шаблон однофайлового компонента необходимо скомпилировать вrenderфункция.

Во-первых, вам нужно определить, есть ли какой-либо текущий запросtype=template.

if (!ctx.query.type) {
    ...
} else if (ctx.query.type === 'template') {
    // 获取编译后的对象 code就是render函数
    const templateRender = compilerSFC.compileTemplate({ source: descriptor.template.content })
    // 将render函数赋值给code返回给浏览器
    code = templateRender.code
}

Здесь нам также нужно разобраться с инструментами вprocess.env, потому что эти коды вернутся в браузер для запуска, если они не обработаны, по умолчаниюnodeПривести к провалу. Изменить третье лицо может изменить путь к модулю промежуточного программного обеспечения, а затем добавить После изменения пути модификацияprocess.env.

// 修改第三方模块路径
app.use(async (ctx, next) => {
    if (ctx.type === 'application/javascript') {
        const contents = await stream2string(ctx.body);
        // 将body中导入的路径修改一下,重新赋值给body返回给浏览器
        // import vue from 'vue', 匹配到from '修改为from '@modules/
        ctx.body = contents.replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/').replace(/process\.env\.NODE_ENV/g, '"development"');
    }
})

Пока упрощенная версияvite, конечно, здесь мы только демонстрируем.vueфайл, дляcss,lessДругие ресурсы не обрабатывались, но методы аналогичны, и заинтересованные студенты могут реализовать это самостоятельно.

#!/usr/bin/env node

const path = require('path')
const { Readable } = require('stream)
const Koa = require('koa')
const send = require('koa-send')
const compilerSFC = require('@vue/compiler-sfc')

const app = new Koa()

const stream2string = (stream) => {
    return new Promise((resolve, reject) => {
        const chunks = [];
        stream.on('data', chunk => {chunks.push(chunk)})
        stream.on('end', () => { resolve(Buffer.concat(chunks).toString('utf-8'))})
        stream.on('error', reject)
    })
}

const stringToStream = text => {
    const stream = new Readable();
    stream.push(text);
    stream.push(null);
    return stream;
}

// 加载第三方模块
app.use(async (ctx, next) => {
    if (ctx.path.startsWith('/@modules/')) {
        // 截取模块名称
        const moduleName = ctx.path.substr(10);
        // 找到模块路径
        const pkgPath = path.join(process.pwd(), 'node_modules', moduleName, 'package.json');
        const pkg = require(pkgPath);
        // 重新给ctx.path赋值,需要重新设置一个存在的路径,因为之前的路径是不存在的
        ctx.path = path.join('/node_modules', moduleName, pkg.module);
        // 执行下一个中间件
        awiat next();
    }
})

// 开启静态文件服务器
app.use(async (ctx, next) => {
    // 加载静态文件
    await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html'})
    await next()
})

// 处理单文件组件
app.use(async (ctx, next) => {
    if (ctx.path.endsWith('.vue')) {
        // 获取响应文件内容,转换成字符串
        const contents = await streamToString(ctx.body);
        // 编译文件内容
        const { descriptor } = compilerSFC.parse(contents);
        // 定义状态码
        let code;
        // 不存在type就是第一次请求
        if (!ctx.query.type) {
            code = descriptor.script.content;
            // 这里的code格式是, 需要改造成我们前面贴出来的vite中的样子
            // import Hello from './components/Hello.vue'
            // export default {
            //      name: 'App',
            //      components: {
            //          Hello
            //      }
            //  }
            // 改造code的格式,将export default 替换为const __script =
            code = code.relace(/export\s+default\s+/g, 'const __script = ')
            code += `
                import { render as __render } from '${ctx.path}?type=template'
                __script.rener = __render
                export default __script
            `
        } else if (ctx.query.type === 'template') {
            // 获取编译后的对象 code就是render函数
            const templateRender = compilerSFC.compileTemplate({ source: descriptor.template.content })
            // 将render函数赋值给code返回给浏览器
            code = templateRender.code
        }
        // 设置浏览器响应头为js
        ctx.type = 'application/javascript'
        // 将字符串转换成数据流传给下一个中间件。
        ctx.body = stringToStream(code);
    }
    await next()
})

// 修改第三方模块路径
app.use(async (ctx, next) => {
    if (ctx.type === 'application/javascript') {
        const contents = await stream2string(ctx.body);
        // 将body中导入的路径修改一下,重新赋值给body返回给浏览器
        // import vue from 'vue', 匹配到from '修改为from '@modules/
        ctx.body = contents.replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/').replace(/process\.env\.NODE_ENV/g, '"development"');
    }
})

app.listen(5000)

console.log('服务器已经启动 http://localhost:5000')