Vite Learning의 심층 분석 '의존성 스캔'
이 글에서는 Vite의 종속성 검색 구현 세부 사항에 대해 자세히 설명합니다. 최종 검색 결과는 여러 모듈의 이름을 포함하는 개체입니다. 사전 구축 프로세스나 사전 구축 방법은 포함되지 않습니다. 제품이 사용됩니다.
Vite를 처음 실행하면 Vite는 CommonJS 및 UMD와 호환되고 성능을 향상하기 위해 종속성 사전 구축을 수행합니다. [관련 권장 사항: vuejs 비디오 튜토리얼]
종속성을 사전 빌드하려면 먼저 다음 두 가지 질문을 이해해야 합니다.
사전 빌드된 콘텐츠가 무엇인가요? / 어떤 모듈을 사전 구축해야 합니까?
미리 구축해야 하는 모듈을 어떻게 찾을 수 있나요?
이 두 가지 문제는 실제로 스캔 내용과 구현 방법에 따라 다릅니다.
이 문서에서는 종속성 검색의 구현 세부 사항을 자세히 설명합니다. 최종 검색 결과는 여러 모듈의 이름을 포함하는 개체이며 사전 구축 프로세스나 사전 구축된 제품의 사용 방법은 포함되지 않습니다. 이 부분의 내용에 관심이 있으시면 저를 팔로우하시고 후속 기사를 기다리시면 됩니다.
사전 구축된 콘텐츠에 따라 다름
프로젝트에는 많은 모듈이 있지만 모든 모듈이 사전 구축되지는 않습니다. 베어 가져오기(베어 종속성)만 종속성 사전 구축을 수행합니다.
베어 가져오기란 무엇인가요?
아래 예를 직접 보세요
// vue 是 bare import import xxx from "vue" import xxx from "vue/xxx" // 以下不是裸依赖 import xxx from "./foo.ts" import xxx from "/foo.ts"
간단히 나눌 수 있습니다:
- 이름으로 접근하는 모듈은 베어 모듈입니다
- bare import가 아닌 경로로 접근하는 모듈
사실 Vite도 판단합니다 이 방법으로.
다음은 일반적인 Vue 프로젝트
의 모듈 종속성 트리입니다. 종속성 스캔 결과는 다음과 같습니다.
[ "vue", "axios" ]
왜 bare import만 미리 빌드되어 있나요?
Node.js는 베어 가져오기의 주소 지정 메커니즘을 정의합니다 - 현재 디렉터리의 node_modules에서 검색합니다. 찾을 수 없으면 디렉터리가 루트 경로가 될 때까지 상위 디렉터리의 node_modules로 이동합니다.
bare import는 일반적으로 npm에 의해 설치되는 모듈이며, 일반적인 상황에서는 수정되지 않습니다. 따라서 이 부분을 빌드하는 것이 좋습니다. 모듈을 미리 성능을 향상시키세요.
반대로 개발자가 작성한 코드가 미리 빌드되어 있고 프로젝트가 청크 파일로 패키징되어 있는 경우,개발자가 코드를 수정하면 빌드를 다시 실행한 다음 청크 파일로 패키징해야 합니다. . 이 프로세스는 성능에 영향을 미칩니다.
monorepo 아래의 모듈도 사전 빌드되나요?아니요. 모노레포의 경우, 일부 모듈은 베어 임포트되기는 하지만 이러한 모듈 역시 개발자가 직접 작성하고 제3자 모듈이 아니기 때문에 Vite는 이러한 모듈에 대해 사전 구축을 수행하지 않습니다. 실제로 Vite는 모듈의 실제 경로가 node_modules에 있는지
결정합니다.
실제 경로가 node_modules에 있는 모듈은 사전 빌드됩니다. 이것은 타사 모듈입니다.- 실제 경로가 node_modules에 없음은 모듈이 파일 링크(monorepo의 구현 방법)를 통해 node_modules에 연결되어 있음을 증명합니다. 개발자가 직접 작성한 코드이며 사전 빌드를 수행하지 않습니다
구현 아이디어살펴보기 이 모듈 종속성 트리를 살펴보세요.
모든 베어 임포트를 스캔하려면 전체 종속성 트리를 순회해야 하며, 여기에는
가 포함됩니다. 우리는 트리 순회에 대해 논의할 때 일반적으로 다음 두 가지 사항에 주의를 기울입니다.
언제 깊이 들어가는 것을 멈추나요?- 리프 노드를 처리하는 방법은 무엇입니까?
현재 리프 노드가 심층 순회를 계속할 필요가 없는 상황:
- When CSS, SVG 등과 같은 다른 JS 독립적 모듈을 만나면 JS 코드가 아니기 때문에 계속 깊이 탐색할 필요가 없습니다
의 결과입니다. 스캐닝에 의존하는 구현 아이디어는 실제로 이해하기 매우 쉽지만 실제 처리는 간단하지 않습니다.
리프 노드 처리를 살펴보겠습니다.
- bare import
는 모듈 ID로 판단할 수 있습니다. 모듈 ID가 경로가 아닌 모듈은 bare import입니다. 이러한 모듈을 발견하면 종속성을 기록하고 더 이상 깊이 탐색하지 마세요.
- 다른 JS 독립적 모듈
은 모듈의 접미사 이름으로 판단 할 수 있습니다. 예를 들어, *.css
모듈을 발견하면 *.css
的模块,无需任何处理,不再深入遍历。
- JS 模块
要获取 JS 代码中依赖的子模块,就需要将代码转成 AST,获取其中 import 语句引入的模块,或者正则匹配出所有 import 的模块,然后继续深入遍历这些模块
- HTML 类型模块
这类模块比较复杂,例如 HTML 或 Vue,里面有一部分是 JS,需要把这部分 JS 代码提取出来,然后按 JS 模块进行分析处理,继续深入遍历这些模块。这里只需要关心 JS 部分,其他部分不会引入模块。
具体实现
我们已经知道了依赖扫描的实现思路,思路其实不复杂,复杂的是处理过程,尤其是 HTML、Vue 等模块的处理。
Vite 这里用了一种比较巧妙的办法 —— 用 esbuild 工具打包
为什么可以用 esbuild 打包代替深度遍历的过程?
本质上打包过程也是个深度遍历模块的过程,其替代的方式如下:
深度遍历 | esbuild 打包 |
---|---|
叶子节点的处理 | esbuild 可以对每个模块(叶子节点)进行解析和加载 可以通过插件对这两个过程进行扩展,加入一些特殊的逻辑 例如将 html 在加载过程中转换为 js |
不深入处理模块 | esbuild 可以在解析过程,指定当前解析的模块为 external 则 esbuild 不再深入解析和加载该模块。 |
深入遍历模块 | 正常解析模块(什么都不做,esbuild 默认行为),返回模块的文件真实路径 |
这块暂时看不懂没有关系,后面会有例子
各类模块的处理
例子 | 处理 | |
---|---|---|
bare import | vue |
在解析过程中,将裸依赖保存到 deps 对象中,设置为 external |
其他 JS 无关的模块 | less文件 |
在解析过程中,设置为 external |
JS 模块 | ./mian.ts |
正常解析和加载即可,esbuild 本身能处理 JS |
html 类型模块 |
index.html 、app.vue 처리가 필요하지 않습니다. 심층 순회 | 가 필요하지 않습니다. JS 모듈 |

🎜깊은 순회 프로세스를 대체하기 위해 esbuild 패키징을 사용할 수 있는 이유는 무엇입니까? 🎜🎜본질적으로🎜패키징 프로세스는 모듈의 심층 탐색 프로세스이기도 합니다🎜. 대체 방법은 다음과 같습니다.🎜
깊이 탐색 | esbuild 패키징 | 🎜||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
예 | 처리 중 | 🎜|||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
例子 | 处理 | |
---|---|---|
bare import | vue |
在解析过程中,将裸依赖保存到 deps 对象中,设置为 external |
其他 JS 无关的模块 | less文件 |
在解析过程中,设置为 external |
JS 模块 | ./mian.ts |
正常解析和加载即可,esbuild 本身能处理 JS |
html 类型模块 |
index.html 、app.vue
|
在加载过程中,将这些模块加载成 JS |
JS 模块
esbuild 本身就能处理 JS 语法,因此 JS 是不需要任何处理的,esbuild 能够分析出 JS 文件中的依赖,并进一步深入处理这些依赖。
其他 JS 无关的模块
// external urls build.onResolve({ filter: /^(https?:)?\/\// }, ({ path }) => ({ path, external: true })) // external css 等文件 build.onResolve( { filter: /\.(css|less|sass|scss|styl|stylus|pcss|postcss|json|wasm)$/ }, ({ path }) => ({ path, external: true } ) // 省略其他 JS 无关的模块
这部分处理非常简单,直接匹配,然后 external 就行了
bare import
build.onResolve( { // 第一个字符串为字母或 @,且第二个字符串不是 : 冒号。如 vite、@vite/plugin-vue // 目的是:避免匹配 window 路径,如 D:/xxx filter: /^[\w@][^:]/ }, async ({ path: id, importer, pluginData }) => { // depImports 为 if (depImports[id]) { return externalUnlessEntry({ path: id }) } // 将模块路径转换成真实路径,实际上调用 container.resolveId const resolved = await resolve(id, importer, { custom: { depScan: { loader: pluginData?.htmlType?.loader } } }) // 如果解析到路径,证明找得到依赖 // 如果解析不到路径,则证明找不到依赖,要记录下来后面报错 if (resolved) { if (shouldExternalizeDep(resolved, id)) { return externalUnlessEntry({ path: id }) } // 如果模块在 node_modules 中,则记录 bare import if (resolved.includes('node_modules')) { // 记录 bare import depImports[id] = resolved return { path, external: true } } // isScannable 判断该文件是否可以扫描,可扫描的文件有 JS、html、vue 等 // 因为有可能裸依赖的入口是 css 等非 JS 模块的文件 else if (isScannable(resolved)) { // 真实路径不在 node_modules 中,则证明是 monorepo,实际上代码还是在用户的目录中 // 是用户自己写的代码,不应该 external return { path: path.resolve(resolved) } } else { // 其他模块不可扫描,直接忽略,external return { path, external: true } } } else { // 解析不到依赖,则记录缺少的依赖 missing[id] = normalizePath(importer) } } )
- 如果文件在 node_modules 中,才认为是 bare import,记录当前模块
- 文件不在 node_modules 中,则是 monorepo,是用户自己写的代码
- 如果这些代码 isScanable 可扫描(即含有 JS 代码),则继续深入处理
- 其他非 JS 模块,external
html 类型模块
如: index.html
、app.vue
const htmlTypesRE = /\.(html|vue|svelte|astro)$/ // html types: 提取 script 标签 build.onResolve({ filter: htmlTypesRE }, async ({ path, importer }) => { // 将模块路径,转成文件的真实路径 const resolved = await resolve(path, importer) if (!resolved) return // 不处理 node_modules 内的 if (resolved.includes('node_modules'){ return } return { path: resolved, // 标记 namespace 为 html namespace: 'html' } })
解析过程很简单,只是用于过滤掉一些不需要的模块,并且标记 namespace 为 html
真正的处理在加载阶段:
// 正则,匹配例子: <script></script> const scriptModuleRE = /(<script>]*type\s*=\s*(?:"module"|'module')[^>]*>)(.*?)<\/script>/gims // 正则,匹配例子: <script></script> export const scriptRE = /(<script>]*>|>))(.*?)<\/script>/gims build.onLoad( { filter: htmlTypesRE, namespace: 'html' }, async ({ path }) => { // 读取源码 let raw = fs.readFileSync(path, 'utf-8') // 去掉注释,避免后面匹配到注释 raw = raw.replace(commentRE, '<!---->') const isHtml = path.endsWith('.html') // scriptModuleRE: <script type=module></script> // scriptRE: <script></script> // html 模块,需要匹配 module 类型的 script,因为只有 module 类型的 script 才能使用 import const regex = isHtml ? scriptModuleRE : scriptRE // 重置正则表达式的索引位置,因为同一个正则表达式对象,每次匹配后,lastIndex 都会改变 // regex 会被重复使用,每次都需要重置为 0,代表从第 0 个字符开始正则匹配 regex.lastIndex = 0 // load 钩子返回值,表示加载后的 js 代码 let js = '' let scriptId = 0 let match: RegExpExecArray | null // 匹配源码的 script 标签,用 while 循环,因为 html 可能有多个 script 标签 while ((match = regex.exec(raw))) { // openTag: 它的值的例子: <script> // content: script 标签的内容 const [, openTag, content] = match // 正则匹配出 openTag 中的 type 和 lang 属性 const typeMatch = openTag.match(typeRE) const type = typeMatch && (typeMatch[1] || typeMatch[2] || typeMatch[3]) const langMatch = openTag.match(langRE) const lang = langMatch && (langMatch[1] || langMatch[2] || langMatch[3]) // 跳过 type="application/ld+json" 和其他非 non-JS 类型 if ( type && !( type.includes('javascript') || type.includes('ecmascript') || type === 'module' ) ) { continue } // esbuild load 钩子可以设置 应的 loader let loader: Loader = 'js' if (lang === 'ts' || lang === 'tsx' || lang === 'jsx') { loader = lang } else if (path.endsWith('.astro')) { loader = 'ts' } // 正则匹配出 script src 属性 const srcMatch = openTag.match(srcRE) // 有 src 属性,证明是外部 script if (srcMatch) { const src = srcMatch[1] || srcMatch[2] || srcMatch[3] // 外部 script,改为用 import 用引入外部 script js += `import ${JSON.stringify(src)}\n` } else if (content.trim()) { // 内联的 script,它的内容要做成虚拟模块 // 缓存虚拟模块的内容 // 一个 html 可能有多个 script,用 scriptId 区分 const key = `${path}?id=${scriptId++}` scripts[key] = { loader, content, pluginData: { htmlType: { loader } } } // 虚拟模块的路径,如 virtual-module:D:/project/index.html?id=0 const virtualModulePath = virtualModulePrefix + key js += `export * from ${virtualModulePath}\n` } } return { loader: 'js', contents: js } } )</script>
加载阶段的主要做有以下流程:
- 读取文件源码
- 正则匹配出所有的 script 标签,并对每个 script 标签的内容进行处理
- 外部 script,改为用 import 引入
- 内联 script,改为引入虚拟模块,并将对应的虚拟模块的内容缓存到 script 对象。
- 最后返回转换后的 js
srcMatch[1] || srcMatch[2] || srcMatch[3] 是干嘛?
我们来看看匹配的表达式:
const srcRE = /\bsrc\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/im
因为 src 可以有以下三种写法:
- src="xxx"
- src='xxx'
- src=xxx
三种情况会出现其中一种,因此是三个捕获组
虚拟模块是如何加载成对应的 script 代码的?
export const virtualModuleRE = /^virtual-module:.*/ // 匹配所有的虚拟模块,namespace 标记为 script build.onResolve({ filter: virtualModuleRE }, ({ path }) => { return { // 去掉 prefix // virtual-module:D:/project/index.html?id=0 => D:/project/index.html?id=0 path: path.replace(virtualModulePrefix, ''), namespace: 'script' } }) // 之前的内联 script 内容,保存到 script 对象,加载虚拟模块的时候取出来 build.onLoad({ filter: /.*/, namespace: 'script' }, ({ path }) => { return scripts[path] })
虚拟模块的加载很简单,直接从 script 对象中,读取之前缓存起来的内容即可。
这样之后,我们就可以把 html 类型的模块,转换成 JS 了
扫描结果
下面是一个 depImport 对象的例子:
{ "vue": "D:/app/vite/node_modules/.pnpm/vue@3.2.37/node_modules/vue/dist/vue.runtime.esm-bundler.js", "vue/dist/vue.d.ts": "D:/app/vite/node_modules/.pnpm/vue@3.2.37/node_modules/vue/dist/vue.d.ts", "lodash-es": "D:/app/vite/node_modules/.pnpm/lodash-es@4.17.21/node_modules/lodash-es/lodash.js" }
- key:模块名称
- value:模块的真实路径
总结
依赖扫描是预构建前的一个非常重要的步骤,这决定了 Vite 需要对哪些依赖进行预构建。
本文介绍了 Vite 会对哪些内容进行依赖预构建,然后分析了实现依赖扫描的基本思路 —— 深度遍历依赖树,并对各种类型的模块进行处理。然后介绍了 Vite 如何巧妙的使用 esbuild 实现这一过程。最后对这部分的源码进行了解析:
- 最复杂的就是 html 类型模块的处理,需要使用虚拟模块;
- 当遇到 bare import 时,需要判断是否在 node_modules 中,在的才记录依赖,然后 external。
- 其他 JS 无关的模块就直接 external
- JS 模块由于 esbuild 本身能处理,不需要做任何的特殊操作
最后获取到的 depImport 是一个记录依赖以及其真实路径的对象
위 내용은 Vite Learning의 심층 분석 '의존성 스캔'의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

AI Hentai Generator
AI Hentai를 무료로 생성하십시오.

인기 기사

뜨거운 도구

메모장++7.3.1
사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전
중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기
강력한 PHP 통합 개발 환경

드림위버 CS6
시각적 웹 개발 도구

SublimeText3 Mac 버전
신 수준의 코드 편집 소프트웨어(SublimeText3)

뜨거운 주제











vue.js에서 JS 파일을 참조하는 세 가지 방법이 있습니다. & lt; script & gt; 꼬리표;; mounted () 라이프 사이클 후크를 사용한 동적 가져 오기; Vuex State Management Library를 통해 수입.

vue.js의 시계 옵션을 사용하면 개발자가 특정 데이터의 변경 사항을들을 수 있습니다. 데이터가 변경되면 콜백 기능을 트리거하여 업데이트보기 또는 기타 작업을 수행합니다. 구성 옵션에는 즉시 콜백을 실행할지 여부와 DEEP를 지정하는 즉시 포함되며, 이는 객체 또는 어레이에 대한 변경 사항을 재귀 적으로 듣는 지 여부를 지정합니다.

vue.js에서 bootstrap 사용은 5 단계로 나뉩니다 : Bootstrap 설치. main.js.의 부트 스트랩 가져 오기 부트 스트랩 구성 요소를 템플릿에서 직접 사용하십시오. 선택 사항 : 사용자 정의 스타일. 선택 사항 : 플러그인을 사용하십시오.

vue.js에서 게으른 로딩을 사용하면 필요에 따라 부품 또는 리소스를 동적으로로드 할 수 있으므로 초기 페이지로드 시간을 줄이고 성능을 향상시킵니다. 특정 구현 방법에는 & lt; keep-alive & gt를 사용하는 것이 포함됩니다. & lt; 구성 요소는 & gt; 구성 요소. 게으른 하중은 FOUC (Splash Screen) 문제를 일으킬 수 있으며 불필요한 성능 오버 헤드를 피하기 위해 게으른 하중이 필요한 구성 요소에만 사용해야합니다.

HTML 템플릿의 버튼을 메소드에 바인딩하여 VUE 버튼에 함수를 추가 할 수 있습니다. 메소드를 정의하고 VUE 인스턴스에서 기능 로직을 작성하십시오.

CSS 애니메이션 또는 타사 라이브러리를 사용하여 VUE에서 Marquee/Text Scrolling Effects를 구현하십시오. 이 기사는 CSS 애니메이션 사용 방법을 소개합니다. & lt; div & gt; CSS 애니메이션을 정의하고 오버플로를 설정하십시오 : 숨겨진, 너비 및 애니메이션. 키 프레임을 정의하고 변환을 설정하십시오 : Translatex () 애니메이션의 시작과 끝에서. 지속 시간, 스크롤 속도 및 방향과 같은 애니메이션 속성을 조정하십시오.

Vue DevTools를 사용하여 브라우저 콘솔에서 vue 탭을 보면 VUE 버전을 쿼리 할 수 있습니다. npm을 사용하여 "npm list -g vue"명령을 실행하십시오. package.json 파일의 "종속성"객체에서 vue 항목을 찾으십시오. Vue Cli 프로젝트의 경우 "vue -version"명령을 실행하십시오. & lt; script & gt에서 버전 정보를 확인하십시오. vue 파일을 나타내는 html 파일의 태그.

vue.js는 이전 페이지로 돌아갈 수있는 네 가지 방법이 있습니다. $ router.go (-1) $ router.back () 사용 & lt; router-link to = & quot;/quot; Component Window.history.back () 및 메소드 선택은 장면에 따라 다릅니다.
