предисловие
Если вы недавно следили за динамикой Vue, вы найдете новые инструменты, с которыми авторы Vue в последнее время возятся.vite. vite 1.0 теперь входит в версию rc, и скоро будет официально выпущена версия 1.0. Несколько месяцев назад Ю Юйси уже представил vite на Weibo, сервере разработки, основанном на собственном ESM браузера.
Когда Webpack впервые появился, чтобы решить проблему, заключающуюся в том, что браузеры с низкими версиями не поддерживают модуляризацию ESM, он объединил различные разбросанные модули JavaScript в один файл и объединил несколько файлов сценариев JavaScript в один файл, чтобы уменьшить количество HTTP-запросов. , что помогает повысить скорость первого посещения страницы. На более позднем этапе Webpack воспользовался победой и представил механизмы загрузчика и плагина для предоставления различных возможностей, связанных с созданием (побег от Babel, слияние css, сжатие кода), заменив Browserify и Gulp в тот же период.
Сегодня, с преобладанием HTTP/2 и предстоящим выпуском HTTP/3 в сочетании с коммерческим использованием сетей 5G, уменьшение количества HTTP-запросов мало что дает, и новая версия браузера в основном поддерживает ESM (<script module>
).
начать
Вите пришло вместе со своей исторической миссией. Поскольку процесс упаковки опущен, можно сказать, что при первом запуске vite его можно открыть за считанные секунды. Вы можете взглянуть на Gif, который я записал, и вы можете войти в разработку, не дожидаясь вообще.
Если вы хотите попробовать vite, вы можете напрямую передать следующую команду:
$ npm init vite-app <project-name>
$ cd <project-name>
$ npm install
$ npm run dev
npm init vite-app
команда будет выполненаnpx create-vite-app
, взято из нпмcreate-vite-appмодуль, а затем сгенерировать файл шаблона в указанную папку через соответствующий шаблон.
{
"name": "vite-app",
"version": "0.0.1",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"vue": "^3.0.0-rc.1"
},
"devDependencies": {
"vite": "^1.0.0-rc.1",
"@vue/compiler-sfc": "^3.0.0-rc.1"
}
}
В настоящее время vite используется с vue 3. Если вы хотите использовать vite в vue 2, по оценкам, вам придется дождаться выпуска официальной версии. Конечно, независимо от того, можете ли вы перейти на vue 3 или vue 3, vue 3 намного превосходит vue 2 с точки зрения производительности, размера пакета и благословения ts. В дополнение к vue, vite также предоставляет шаблоны, связанные с реакцией и предварительными настройками.
Структура каталогов сгенерированного проекта vue выглядит следующим образом.
Заявка на проект естьindex.html
, собственный ESM браузера (type="module"
) способность. Для ознакомления с возможностями браузера ESM вы можете прочитать мою предыдущую статью«Нынешняя жизнь фронтальной модуляризации».
<script type="module" src="/src/main.js"></script>
После того, как все файлы js будут обработаны vite, путь к модулю их импорта будет изменен, добавьте впереди/@modules/
. Когда браузер запрашивает модуль импорта, vitenode_modules
Найдите соответствующий файл и верните его.
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
createApp(App).mount('#app')
Таким образом, процесс упаковки исключается, что значительно повышает эффективность разработки. Конечно, vite также предоставляет производственный режим, в котором для сборки используется Rollup.
говорить о снежном покрове
Первым инструментом, использующим встроенные в браузер возможности ESM, является не vite, а инструмент под названиемsnowpackИнструмент. До того, как Snowpack был выпущен 1.0, он назывался@pika/web
.
pikaКоманда сделала снежный покров, потому что pika стремится ускорить работу веб-приложений на 90%.
Поскольку многие современные веб-приложения построены на основе разных модулей с открытым исходным кодом, и эти модули с открытым исходным кодом упаковываются в один пакет с помощью инструментов упаковки, таких как веб-пакет, если все эти модули с открытым исходным кодом находятся с одного адреса CDN и поддерживают междоменное кэширование, то эти модули с открытым исходным кодом нужно загрузить только один раз, и другие веб-сайты используют те же модули с открытым исходным кодом, их не нужно загружать снова, а локальный кеш считывается напрямую.
Например, Taobao и Tmall разработаны на основе реакции + редукса + antd + loadsh.После того, как я открою Taobao, мне не нужно повторно загружать эти модули с открытым исходным кодом при входе в Tmall, мне нужно только загрузить некоторые службы, связанные на страницу Tmall. Подойдет код. С этой целью pika построила CDN (skypack) используется для загрузки некоторых модулей esm в npm.
Позже, когда был освобожден SnowPack, команда Pika опубликовала статью под названием «Путь».«Будущее без Webpack»В статье всем рассказывается, что можно попробовать отказаться от webpack и изменить жизнь webpack.
В README vite также упоминается, что в некоторых аспектах упоминается снежный покров, и перечислены сходства и различия между vite и снежным покровом.
Snowpack теперь выпущен для v2, мы можем найти исходный код периода v1, чтобы увидеть раннюю реализацию Snowpack.
Анализ исходного кода
На github можно найти версию snowpack v1.0.0 по тегу git.После скачивания он немного глючит.При чтении исходного кода рекомендуется переходить на v1.2.0(GitHub.com/pikapikalight/скажи нет…).
существуетpackage.json
Как видно в@pika/pack
Для упаковки этот инструмент передает процесс упаковки, который чем-то похож на gulp. Если вам интересно, вы можете узнать об этом. Здесь основное внимание уделяется принципу снежного покрова.
{
"scripts": {
"build": "pika build"
},
// snowpack 的构建工具
"@pika/pack": {
"pipeline": [
[
"@pika/plugin-ts-standard-pkg"
],
[
"@pika/plugin-copy-assets"
],
[
"@pika/plugin-build-node"
],
[
"@pika/plugin-simple-bin",
{
// 通过 snowpack 运行命令
"bin": "snowpack"
}
]
]
}
}
Здесь мы берем проект vue в качестве примера и используем Snowpack для запуска проекта vue 2. Структура каталогов следующая:
Если вы хотите внедрить снежный покров в проект, вам нужно добавить снежный покров в проектpackage.json
добавить конфигурацию, связанную снегопадом, тем более важной конфигурацией этоsnowpack.webDependencies
, указывающий на зависимости текущего проекта, эти два файла будут упакованы Snowpack вweb_modules
содержание.
{
"scripts": {
"build": "snowpack",
"start": "serve ./"
},
"dependencies": {
"http-vue-loader": "^1.4.2",
"vue": "^2.6.12"
},
"devDependencies": {
"serve": "^11.3.2",
"snowpack": "~1.2.0"
},
"snowpack": {
"webDependencies": [
"http-vue-loader",
"vue/dist/vue.esm.browser.js"
]
}
}
бегатьnpm run build
После этого новыйweb_modules
каталог, файлы в этом каталоге - это то, что у нас есть вsnowpack.webDependencies
Два файла js, объявленные в .
Когда снежный покров запущен, он вызывает sourcesrc/index.ts
Способ CLI In, сокращенная версия метода выглядит следующим образом:
// 精简了部分代码,如果想看完整版建议去 github
// https://github.com/pikapkg/snowpack/blob/v1.2.0/src/index.ts
const cwd = process.cwd();
export async function cli(args: string[]) {
// 解析命令行参数
const { dest = 'web_modules' } = yargs(args);
// esm 脚本文件的输出目录,默认为 web_modules
const destLoc = path.resolve(cwd, dest);
// 获取 pkg.json
const pkgManifest: any = require(path.join(cwd, 'package.json'));
// 获取 pkg.json 中的依赖模块
const implicitDependencies = [
...Object.keys(pkgManifest.dependencies || {}),
...Object.keys(pkgManifest.peerDependencies || {}),
];
// 获取 pkg.json 中 snowpack 相关配置
const { webDependencies } = pkgManifest['snowpack'] || {
webDependencies: undefined
};
const installTargets = [];
// 需要被安装的模块,如果没有该配置,会尝试安装所有 dependencies 内的模块
if (webDependencies) {
installTargets.push(...scanDepList(webDependencies, cwd));
} else {
installTargets.push(...scanDepList(implicitDependencies, cwd));
}
// 模块安装
const result = await install(installTargets, installOptions);
}
Этот метод считывает элементpackage.json
файл, если естьsnowpack.webDependencies
Конфигурация, будет установлена первойsnowpack.webDependencies
Модуль, объявленный в , если такой конфигурации нет, то поставитdependencies
а такжеdevDependencies
модули установлены. Все имена модулей будут переданы черезscanDepList
, преобразуется в определенный формат и будетglob
Имя модуля грамматики послеglob
Восстановить в один файл.
import path from 'path';
function createInstallTarget(specifier: string): InstallTarget {
return {
specifier,
named: [],
};
}
export function scanDepList(depList: string[], cwd: string): InstallTarget[] {
// 获取 node_modules 路径
const nodeModules = path.join(cwd, 'node_modules');
return depList
.map(whitelistItem => {
// 判断文件名是否为 glob 语法 (e.g. `vue/*.js`)
if (!glob.hasMagic(whitelistItem)) {
return [createInstallTarget(whitelistItem)];
} else {
// 转换 glob 路径
return scanDepList(glob.sync(whitelistItem,{cwd: nodeModules}), cwd);
}
})
// 将所有文件合并成一个数组
.reduce((flat, item) => flat.concat(item), []);
}
Наконец, все модули будут установлены через install.
// 移除 .js、.mjs 后缀
function getWebDependencyName(dep: string): string {
return dep.replace(/\.m?js$/i, '');
}
// 获取模块的类型以及绝对路径
function resolveWebDependency(dep: string): {
type: 'JS' | 'ASSET';
loc: string;
} {
var packagePattern = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$')
// 如果带有扩展名,且非 npm 模块,直接返回
if (path.extname(dep) && !packagePattern.test(dep)) {
const isJSFile = ['.js', '.mjs', '.cjs'].includes(path.extname(dep));
return {
type: isJSFile ? 'JS' : 'ASSET',
// 还原绝对路径
loc: require.resolve(dep, {paths: [cwd]}),
};
}
// 如果是 npm 模块,需要查找模块对应的 package.json 文件
const manifestPath = `${cwd}/node_modules/${dep}/package.json`;
const manifestStr = fs.readFileSync(manifestPath, {encoding: 'utf8'});
const depManifest = JSON.parse(manifestStr);
// 然后读取 package.json 中的 module属性、browser属性
let foundEntrypoint: string =
depManifest['browser:module'] || depManifest.module || depManifest.browser;
if (!foundEntrypoint) {
// 如果都不存在就取 main 属性
foundEntrypoint = depManifest.main || 'index.js';
}
return {
type: 'JS',
// 还原绝对路径
loc: path.join(`${cwd}/node_modules/${dep}`, foundEntrypoint),
};
}
// 模块安装
function install(installTargets, installOptions) {
const {
destLoc
} = installOptions;
// 使用 set 将待安装模块进行一次去重
const allInstallSpecifiers = new Set(installTargets.map(dep => dep.specifier));
// 模块查找转化
for (const installSpecifier of allInstallSpecifiers) {
// 移除 .js、.mjs 后缀
const targetName = getWebDependencyName(installSpecifier);
// 获取文件类型,以及文件绝对路径
const {type: targetType, loc: targetLoc} = resolveWebDependency(installSpecifier);
if (targetType === 'JS') {
// 脚本文件
const hash = await generateHashFromFile(targetLoc);
// 添加到脚本依赖对象
depObject[targetName] = targetLoc;
importMap[targetName] = `./${targetName}.js?rev=${hash}`;
installResults.push([installSpecifier, true]);
} else if (targetType === 'ASSET') {
// 静态资源
// 添加到静态资源对象
assetObject[targetName] = targetLoc;
installResults.push([installSpecifier, true]);
}
}
if (Object.keys(depObject).length > 0) {
// 通过 rollup 打包文件
const packageBundle = await rollup.rollup({
input: depObject,
plugins: [
// rollup 插件
// 这里可以进行一些 babel 转义、代码压缩之类的操作
// 还可以将一些 commonjs 的模块转化为 ESM 模块
]
});
// 文件输出到 web_modules 目录
await packageBundle.write({
dir: destLoc,
});
}
// 拷贝静态资源
Object.entries(assetObject).forEach(([assetName, assetLoc]) => {
mkdirp.sync(path.dirname(`${destLoc}/${assetName}`));
fs.copyFileSync(assetLoc, `${destLoc}/${assetName}`);
});
return true;
}
Основной принцип проанализирован, давайте посмотрим на реальный случай. Переходим в htmltype="module"
Введен тег scriptindex.js
как входной файл.
<!DOCTYPE html>
<html lang="en">
<title>snowpack-vue-httpvueloader</title>
<link rel="stylesheet" href="./assets/style.css">
<body>
<h1>snowpack - Vue Example</h1>
<div id="app"></div>
<script type="module" src="./js/index.js"></script>
</body>
</html>
затем вindex.js
, импорт вwebDependenies
Два js-файла, объявленные и добавленные перед/web_modules
.
import Vue from '/web_modules/vue/dist/vue.esm.browser.js'
import httpVueLoader from '/web_modules/http-vue-loader.js'
Vue.use(httpVueLoader)
new Vue({
el: '#app',
components: {
app: 'url:./components/app.vue',
},
template: '<app></app>',
})
наконец прошлоnpm run start
,использоватьserve
Запустите службу узла, и вы сможете получить к ней обычный доступ.
Видно, что функции SnowPack V1 являются относительно простыми в целом, но модули, которые необходимо зависеть от, извлекаются из Node_Modules в Web_Modules, а компиляция выполняется через свертание в середине. Rollup представлен здесь в основном для сжимания и оптимизации кода JS и преобразовать некоторые модули Commonjs в модули ESM.
Но в итоге для запуска службы узла нужно использовать сторонние модули.В то время официалы тоже с энтузиазмом рассказывали, какие сторонние модули можно выбрать для предоставления услуг.
Версия v2 уже поддерживает включение внутреннего сервера узла для разработки, не прибегая к горячим обновлениям. Конечно, версия v2 обеспечивает поддержку модулей css в дополнение к модулям js.
принцип жизни
Разобравшись с исходным кодом Snowpack v1, давайте вернемся к принципу vite. Или так же, как раньше, возвращаясь кvite v0.1.1, когда количество кода невелико, присмотритесь к идее vite.
При запуске vite внутри запускается http-сервер для перехвата файла сценария страницы.
// 精简了热更新相关代码,如果想看完整版建议去 github
// https://github.com/vitejs/vite/blob/a4f093a0c3/src/server/server.ts
import http, { Server } from 'http'
import serve from 'serve-handler'
import { vueMiddleware } from './vueCompiler'
import { resolveModule } from './moduleResolver'
import { rewrite } from './moduleRewriter'
import { sendJS } from './utils'
export async function createServer({
port = 3000,
cwd = process.cwd()
}: ServerConfig = {}): Promise<Server> {
const server = http.createServer(async (req, res) => {
const pathname = url.parse(req.url!).pathname!
if (pathname.startsWith('/__modules/')) {
// 返回 import 的模块文件
return resolveModule(pathname.replace('/__modules/', ''), cwd, res)
} else if (pathname.endsWith('.vue')) {
// 解析 vue 文件
return vueMiddleware(cwd, req, res)
} else if (pathname.endsWith('.js')) {
// 读取 js 文本内容,然后使用 rewrite 处理
const filename = path.join(cwd, pathname.slice(1))
const content = await fs.readFile(filename, 'utf-8')
return sendJS(res, rewrite(content))
}
serve(req, res, {
public: cwd,
// 默认返回 index.html
rewrites: [{ source: '**', destination: '/index.html' }]
})
})
return new Promise((resolve, reject) => {
server.on('listening', () => {
console.log(`Running at http://localhost:${port}`)
resolve(server)
})
server.listen(port)
})
}
При доступе к сервису vite по умолчанию возвращается index.html.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
обрабатывать js-файлы
html файл запросит/src/main.js
, когда служба vite вернет файл js, она будет использоватьrewrite
Метод заменяет содержимое файла js один раз.
if (pathname.endsWith('.js')) {
// 读取 js 文本内容,然后使用 rewrite 处理
const filename = path.join(cwd, pathname.slice(1))
const content = await fs.readFile(filename, 'utf-8')
return sendJS(res, rewrite(content))
}
// 精简了部分代码,如果想看完整版建议去 github
// https://github.com/vitejs/vite/blob/a4f093a0c3/src/server/moduleRewriter.ts
import { parse } from '@babel/parser'
export function rewrite(source: string, asSFCScript = false) {
// 通过 babel 解析,找到 import from、export default 相关代码
const ast = parse(source, {
sourceType: 'module',
plugins: [
'bigInt',
'optionalChaining',
'nullishCoalescingOperator'
]
}).program.body
let s = source
ast.forEach((node) => {
if (node.type === 'ImportDeclaration') {
if (/^[^\.\/]/.test(node.source.value)) {
// 在 import 模块名称前加上 /__modules/
// import { foo } from 'vue' --> import { foo } from '/__modules/vue'
s = s.slice(0, node.source.start)
+ `"/__modules/${node.source.value}"`
+ s.slice(node.source.end)
}
} else if (asSFCScript && node.type === 'ExportDefaultDeclaration') {
// export default { xxx } -->
// let __script; export default (__script = { xxx })
s = s.slice(0, node.source.start)
+ `let __script; export default (__script = ${
s.slice(node.source.start, node.declaration.start)
})`
+ s.slice(node.source.end)
s.overwrite(
node.start!,
node.declaration.start!,
`let __script; export default (__script = `
)
s.appendRight(node.end!, `)`)
}
})
return s.toString()
}
запрос файла html/src/main.js
После лечения вите результаты следующие:
- import { createApp } from 'vue'
+ import { createApp } from '/__modules/vue'
import App from './App.vue'
createApp(App).mount('#app')
Работа с модулями npm
После того, как браузер проанализирует main.js, он прочитает модуль импорта и сделает запрос. запрошенный файл, если/__modules/
В начале указано, что это модуль npm, который будет использовать viteresolveModule
способ обработки.
// fetch /__modules/vue
if (pathname.startsWith('/__modules/')) {
// 返回 import 的模块文件
return resolveModule(pathname.replace('/__modules/', ''), cwd, res)
}
// 精简了部分代码,如果想看完整版建议去 github
// https://github.com/vitejs/vite/blob/a4f093a0c3/src/server/moduleResolver.ts
import path from 'path'
import resolve from 'resolve-from'
import { sendJSStream } from './utils'
import { ServerResponse } from 'http'
export function resolveModule(id: string, cwd: string, res: ServerResponse) {
let modulePath: string
modulePath = resolve(cwd, 'node_modules', `${id}/package.json`)
if (id === 'vue') {
// 如果是 vue 模块,返回 vue.runtime.esm-browser.js
modulePath = path.join(
path.dirname(modulePath),
'dist/vue.runtime.esm-browser.js'
)
} else {
// 通过 package.json 文件,找到需要返回的 js 文件
const pkg = require(modulePath)
modulePath = path.join(path.dirname(modulePath), pkg.module || pkg.main)
}
sendJSStream(res, modulePath)
}
Обрабатывать vue-файлы
В дополнение к получению рамочного кода Main.js также импортирует Vue компонент. если.vue
конец файла, vite пройдетvueMiddleware
способ обработки.
if (pathname.endsWith('.vue')) {
// 解析 vue 文件
return vueMiddleware(cwd, req, res)
}
// 精简了部分代码,如果想看完整版建议去 github
// https://github.com/vitejs/vite/blob/a4f093a0c3/src/server/vueCompiler.ts
import url from 'url'
import path from 'path'
import { parse, SFCDescriptor } from '@vue/compiler-sfc'
import { rewrite } from './moduleRewriter'
export async function vueMiddleware(
cwd: string, req, res
) {
const { pathname, query } = url.parse(req.url, true)
const filename = path.join(cwd, pathname.slice(1))
const content = await fs.readFile(filename, 'utf-8')
const { descriptor } = parse(content, { filename }) // vue 模板解析
if (!query.type) {
let code = ``
if (descriptor.script) {
code += rewrite(
descriptor.script.content,
true /* rewrite default export to `script` */
)
} else {
code += `const __script = {}; export default __script`
}
if (descriptor.styles) {
descriptor.styles.forEach((s, i) => {
code += `\nimport ${JSON.stringify(
pathname + `?type=style&index=${i}`
)}`
})
}
if (descriptor.template) {
code += `\nimport { render as __render } from ${JSON.stringify(
pathname + `?type=template`
)}`
code += `\n__script.render = __render`
}
sendJS(res, code)
return
}
if (query.type === 'template') {
// 返回模板
}
if (query.type === 'style') {
// 返回样式
}
}
После анализа,.vue
Когда файл будет возвращен, он будет разделен на три части: сценарий, стиль и шаблон.
// 解析前
<template>
<div>
<img alt="Vue logo" src="./assets/logo.png" />
<HelloWorld msg="Hello Vue 3.0 + Vite" />
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld.vue";
export default {
name: "App",
components: {
HelloWorld
}
};
</script>
// 解析后
import HelloWorld from "/src/components/HelloWorld.vue";
let __script;
export default (__script = {
name: "App",
components: {
HelloWorld
}
})
import {render as __render} from "/src/App.vue?type=template"
__script.render = __render
Содержимое в шаблоне будет преобразовано в метод рендеринга с помощью vue. О том, как шаблоны vue компилируются в методы рендеринга, вы можете прочитать в другой моей статье:"Принцип компиляции шаблона Vue".
import {
parse,
SFCDescriptor,
compileTemplate
} from '@vue/compiler-sfc'
export async function vueMiddleware(
cwd: string, req, res
) {
// ...
if (query.type === 'template') {
// 返回模板
const { code } = compileTemplate({
filename,
source: template.content,
})
sendJS(res, code)
return
}
if (query.type === 'style') {
// 返回样式
}
}
И стиль шаблона
import {
parse,
SFCDescriptor,
compileStyle,
compileTemplate
} from '@vue/compiler-sfc'
export async function vueMiddleware(
cwd: string, req, res
) {
// ...
if (query.type === 'style') {
// 返回样式
const index = Number(query.index)
const style = descriptor.styles[index]
const { code } = compileStyle({
filename,
source: style.content
})
sendJS(
res,
`
const id = "vue-style-${index}"
let style = document.getElementById(id)
if (!style) {
style = document.createElement('style')
style.id = id
document.head.appendChild(style)
}
style.textContent = ${JSON.stringify(code)}
`.trim()
)
}
}
Обработка стиля не сложная, получите содержимое тега стиля, а затем js добавит стиль в тег заголовка, создав тег стиля.
резюме
Вот простой анализ того, как vite перехватывает запрос и потом возвращает нужный файл, опуская код горячего обновления. В дополнение к запуску сервисов для разработки, vite v1, который будет выпущен, также поддерживает накопительную упаковку и возможность вывода кода рабочей среды.
Суммировать
Когда vite был только что выпущен, его можно было использовать только как вспомогательный инструмент для vue, а теперь он поддерживает ряд возможностей, таких как JSX, TypeScript, Web Assembly, PostCSS и т. д. Давайте просто спокойно дождемся выхода официальных версий vue3 и vite.Сможем ли мы произвести революцию в веб-пакете или нет, зависит от воли Божией.
Кстати, vite, как и vue, происходит от французского, а по-китайски означает «быстрый».