Vue2
공식 문서 와 비교하여 Vue3
온라인에 추가 사항이 있습니다 Playground
.
다음과 같이 엽니다.
온라인에서 단일 파일 구성 요소를 작성하고 실행하는 것과 같습니다 Vue
.물론 이것도 오픈 소스이며 패키지로 게시되며 그 npm
자체가 구성 요소이므로 Vue
프로젝트에서 쉽게 사용할 수 있습니다 Vue
.
<script setup>
import {
Repl } from '@vue/repl'
import '@vue/repl/style.css'
</script>
<template>
<Repl />
</template>
여전히 작성 및 공유 에 demo
매우 좋으며 특히 Vue
관련 프로젝트를 기반으로 한 온라인 프로젝트 에 적합합니다 demo
. 현재 많은 Vue3
구성 요소 라이브러리가 사용됩니다. 창고 주소: @vue/repl .
@vue/repl
의 데이터 저장, 다중 파일 생성 지원 등 사람(나)을 빛나게 하는 기능도 있고 , 전처리 언어 사용 url
만 지원하고 지원하지 않는 등의 제약도 있지만 , . _Vue3
CSS
ts
다음으로 처음부터 구현 원리를 탐색하도록 안내하겠습니다.설명해야 할 것은 ssr
관련 항목과 같은 일부 항목을 선택적으로 무시한다는 것입니다.이 측면을 이해해야 하는 경우 소스 코드를 직접 읽을 수 있습니다.
먼저 프로젝트를 다운로드한 다음 테스트 페이지의 항목 파일을 찾습니다.
// test/main.ts
const App = {
setup() {
// 创建数据存储的store
const store = new ReplStore({
serializedState: location.hash.slice(1)
})
// 数据存储
watchEffect(() => history.replaceState({
}, '', store.serialize()))
// 渲染Playground组件
return () =>
h(Repl, {
store,
sfcOptions: {
script: {
}
}
})
}
}
createApp(App).mount('#app')
먼저 에 저장된 파일 데이터를 꺼낸 다음 클래스의 인스턴스를 생성하면 모든 파일 데이터가 이 전역에 저장되고 모니터링된 파일 데이터가 변경되고 변경 url
사항 이 실시간으로 반영됩니다 . 실시간 저장소, 마지막으로 구성 요소를 렌더링 하고 전달합니다 .hash
ReplStore
store
store
store
url
Repl
store
먼저 클래스를 살펴보겠습니다 ReplStore
.
데이터 저장고
// 默认的入口文件名称
const defaultMainFile = 'App.vue'
// 默认文件的内容
const welcomeCode = `
<script setup>
import { ref } from 'vue'
const msg = ref('Hello World!')
</script>
<template>
<h1>{
{ msg }}</h1>
<input v-model="msg">
</template>
`.trim()
// 数据存储类
class ReplStore {
constructor({
serializedState = '',
defaultVueRuntimeURL = `https://unpkg.com/@vue/runtime-dom@${
version}/dist/runtime-dom.esm-browser.js`,
}) {
let files: StoreState['files'] = {
}
// 有存储的数据
if (serializedState) {
// 解码保存的数据
const saved = JSON.parse(atou(serializedState))
for (const filename in saved) {
// 遍历文件数据,创建文件实例保存到files对象上
files[filename] = new File(filename, saved[filename])
}
} else {
// 没有存储的数据
files = {
// 创建一个默认的文件
[defaultMainFile]: new File(defaultMainFile, welcomeCode)
}
}
// Vue库的cdn地址,注意是运行时版本,即不包含编译模板的代码,也就是模板必须先被编译成渲染函数才行
this.defaultVueRuntimeURL = defaultVueRuntimeURL
// 默认的入口文件为App.vue
let mainFile = defaultMainFile
if (!files[mainFile]) {
// 自定义了入口文件
mainFile = Object.keys(files)[0]
}
// 核心数据
this.state = reactive({
mainFile,// 入口文件名称
files,// 所有文件
activeFile: files[mainFile],// 当前正在编辑的文件
errors: [],// 错误信息
vueRuntimeURL: this.defaultVueRuntimeURL,// Vue库的cdn地址
})
// 初始化import-map
this.initImportMap()
}
}
주로 반응형 개체를 핵심 저장소 개체로 만드는 데 사용되며 reactive
, 저장된 데이터에는 일반적으로 루트 구성 요소인 항목 파일의 이름 mainFile
, 모든 파일 데이터 files
및 현재 편집 중인 파일 개체가 포함됩니다 activeFile
.
데이터가 URL에 저장되는 방식
hash
데이터를 복호화하는 데 사용되는 중간에서 추출한 데이터에 serializedState
대해 위의 메소드가 호출되고 해당 데이터를 인코딩하는 데 사용되는 atou
해당 메소드가 있음을 알 수 있습니다 .utoa
모든 사람은 최대 길이 제한이 있다는 것을 어느 정도 들었을 것입니다 url
. 따라서 우리의 일반적인 생각에 따르면 데이터는 확실히 서버에 저장되지 않지만 url
일부 hash
는 영향을 받지 않아야 하며 hash
데이터는 다음 주소로 전송되지 않습니다. 서버.
그래도 @vue/repl
저장하기 전에 압축을 하게 되는데 결국 url
공유용으로 사용하는 경우가 많고 너무 길면 불편하다.
store.serialize()
먼저 처음에 언급한 파일 데이터를 직렬화하고 저장하는 데 사용되는 메서드를 살펴보겠습니다 url
.
class ReplStore {
// 序列化文件数据
serialize() {
return '#' + utoa(JSON.stringify(this.getFiles()))
}
// 获取文件数据
getFiles() {
const exported: Record<string, string> = {
}
for (const filename in this.state.files) {
exported[filename] = this.state.files[filename].code
}
return exported
}
}
getFiles
파일 이름과 파일 내용을 꺼내기 위해 호출 한 다음 utoa
문자열로 변환한 후 메서드를 호출합니다.
import {
zlibSync, strToU8, strFromU8 } from 'fflate'
export function utoa(data: string): string {
// 将字符串转成Uint8Array
const buffer = strToU8(data)
// 以最大的压缩级别进行压缩,返回的zipped也是一个Uint8Array
const zipped = zlibSync(buffer, {
level: 9 })
// 将Uint8Array重新转换成二进制字符串
const binary = strFromU8(zipped, true)
// 将二进制字符串编码为Base64编码字符串
return btoa(binary)
}
압축은 가장 빠르고 가장 작고 다재다능한 순수 압축 및 압축 해제 라이브러리 라고 주장하는 fflate 를 사용합니다.JavaScript
strFromU8
메서드의 두 번째 매개 변수가 전달됨을 알 수 있으며 true
이는 바이너리 문자열로 변환됨을 의미합니다.이는 js
기본 제공 sum btoa
메서드가 문자열을 atob
지원하지 않기 때문에 필요하며 Unicode
코드 내용은 분명히 ASCII
문자 만 사용할 수 없습니다 256
. 그래서 직접 btoa
인코딩을 사용할 때 오류가 보고됩니다.
세부 정보: https://base64.guru/developers/javascript/examples/unicode-strings .
압축 방법을 읽은 후 해당 압축 해제 방법을 살펴보겠습니다 atou
.
import {
unzlibSync, strToU8, strFromU8 } from 'fflate'
export function atou(base64: string): string {
// 将base64转成二进制字符串
const binary = atob(base64)
// 检查是否是zlib压缩的数据,zlib header (x78), level 9 (xDA)
if (binary.startsWith('\x78\xDA')) {
// 将字符串转成Uint8Array
const buffer = strToU8(binary, true)
// 解压缩
const unzipped = unzlibSync(buffer)
// 将Uint8Array重新转换成字符串
return strFromU8(unzipped)
}
// 兼容没有使用压缩的数据
return decodeURIComponent(escape(binary))
}
utoa
마지막 줄과는 조금 다릅니다.마지막 줄도 결국 컴포넌트이기 fflate
때문에 압축을 사용하지 않는 상황 과 호환되며 @vue/repl
, 사용자가 초기에 전달한 데이터는 fflate
압축을 사용하지 않고 전송 방법은 다음과 같습니다 base64
.
function utoa(data) {
return btoa(unescape(encodeURIComponent(data)));
}
파일 클래스File
files
개체 에 저장된 파일은 일반 텍스트 콘텐츠가 아니라 File
클래스에서 만든 파일 인스턴스입니다.
// 文件类
export class File {
filename: string// 文件名
code: string// 文件内容
compiled = {
// 该文件编译后的内容
js: '',
css: ''
}
constructor(filename: string, code = '', hidden = false) {
this.filename = filename
this.code = code
}
}
이 클래스는 매우 간단합니다.파일 이름과 파일 내용을 저장하는 것 외에도 주로 파일의 컴파일된 내용을 저장합니다.파일이면 js
컴파일된 내용이 저장되며 compiled.js
당연히 css
저장됩니다 compiled.css
. vue
가 단일 파일 인 경우 컴파일 script
되고 에 저장template
되며 스타일이 추출되어 에 저장됩니다.js
compiled.js
compiled.css
이 컴파일 로직은 나중에 자세히 소개하겠습니다.
가져오기 맵 사용
브라우저에서 직접 구문을 사용하면 ESM
기본 가져오기가 지원되지 않습니다. 즉, 다음이 작동하지 않습니다.
import moment from "moment";
가져오기 소스가 합법적이어야 하므로 import-map 제안이 url
나타났습니다 .물론 현재 호환성이 좋지는 않지만 import-maps 는 다음과 같습니다 .polyfill
이러한 방식으로 베어 임포트를 다음과 같은 방식으로 사용할 수 있습니다.
<script type="importmap">
{
"imports": {
"moment": "/node_modules/moment/src/moment.js",
}
}
</script>
<script type="importmap">
import moment from "moment";
</script>
ReplStore
그럼 이 메서드가 무엇을 하는지 살펴보겠습니다 initImportMap
.
private initImportMap() {
const map = this.state.files['import-map.json']
if (!map) {
// 如果还不存在import-map.json文件,就创建一个,里面主要是Vue库的map
this.state.files['import-map.json'] = new File(
'import-map.json',
JSON.stringify(
{
imports: {
vue: this.defaultVueRuntimeURL
}
},
null,
2
)
)
} else {
try {
const json = JSON.parse(map.code)
// 如果vue不存在,那么添加一个
if (!json.imports.vue) {
json.imports.vue = this.defaultVueRuntimeURL
map.code = JSON.stringify(json, null, 2)
}
} catch (e) {
}
}
}
사실 내용을 저장하기 위해 import-map.json
파일이 생성됩니다 .import-map
다음으로 우리의 주인공 컴포넌트를 입력하겠습니다 Repl.vue
.템플릿 부분은 말할 것도 없습니다.주로 왼쪽과 오른쪽의 두 부분으로 나뉩니다.왼쪽의 편집기가 사용되고 codemirror
오른쪽의 iframe
미리보기가 사용됩니다. script
주요 부분 살펴보기 :
// ...
props.store.options = props.sfcOptions
props.store.init()
// ...
핵심은 이 두줄인데 component 사용시 넘겨주는 속성 sfcOptions
으로 저장 되어 추후에 파일 컴파일시 사용하게 됩니다 . 을 실행하면 파일이 열립니다.store
options
store
init
파일 편집
class ReplStore {
init() {
watchEffect(() => compileFile(this, this.state.activeFile))
for (const file in this.state.files) {
if (file !== defaultMainFile) {
compileFile(this, this.state.files[file])
}
}
}
}
현재 편집 중인 파일을 컴파일합니다. 기본값은 이며 App.vue
현재 편집 중인 파일이 변경되면 컴파일을 다시 트리거합니다. 또한 초기에 여러 파일이 있는 경우 다른 파일도 컴파일을 위해 트래버스됩니다.
컴파일 하는 방법 compileFile
이 비교적 길기 때문에 천천히 살펴보도록 하겠습니다.
CSS 파일 컴파일
export async function compileFile(
store: Store,
{
filename, code, compiled }: File
) {
// 文件内容为空则返回
if (!code.trim()) {
store.state.errors = []
return
}
// css文件不用编译,直接把文件内容存储到compiled.css属性
if (filename.endsWith('.css')) {
compiled.css = code
store.state.errors = []
return
}
// ...
}
@vue/repl
현재 전처리 언어의 사용이 지원되지 않기 css
때문에 스타일은 css
파일을 만드는 것만 가능하며 당연히 css
컴파일이 필요하지 않으며 컴파일 결과 개체에 직접 저장할 수 있습니다.
js 및 ts 파일 컴파일
계속하다:
export async function compileFile(){
// ...
if (filename.endsWith('.js') || filename.endsWith('.ts')) {
if (shouldTransformRef(code)) {
code = transformRef(code, {
filename }).code
}
if (filename.endsWith('.ts')) {
code = await transformTS(code)
}
compiled.js = code
store.state.errors = []
return
}
// ...
}
shouldTransformRef
그리고 transformRef
두 가지 방법은 @vue/reactivity-transform 패키지에 있는 방법들입니다.그들은 무엇을 위해 사용됩니까?사실, Vue3
실험적인 제안이 있습니다.우리 모두는 그것을 사용하여 ref
원래 값의 반응형 데이터를 생성할 수 있다는 것을 알고 있습니다. 그러나 액세스 시간이 경과해야 하므로 .value
이 제안은 이것을 제거하는 것입니다 .value
. 방법은 사용하는 것이 아니라 ref
사용하는 것 입니다 $ref
. 예를 들면 다음과 같습니다.
// $ref都不用导出,直接使用即可
let count = $ref(0)
console.log(count)
또한 다음과 같은 ref
몇 가지 다른 기능이 지원됩니다 api
.
따라서 shouldTransformRef
이 실험적 구문이 사용되는지 여부를 확인하는 방법을 사용하고 transformRef
이를 일반 구문으로 변환하는 방법을 사용합니다.
파일 인 경우 다음 방법을 ts
사용하여 transformTS
컴파일됩니다 .
import {
transform } from 'sucrase'
async function transformTS(src: string) {
return transform(src, {
transforms: ['typescript']
}).code
}
sucrase를 사용 하여 ts
구문을 변환 합니다 . 컴파일러가 초고속 이기 때문에 걱정이 덜 된다면 결과 브라우저 호환성을 사용할 수 있습니다 .ts
ts
babel
sucrase
Vue 단일 파일 컴파일
compileFile
방법 으로 돌아가서 계속하십시오 .
import hashId from 'hash-sum'
export async function compileFile(){
// ...
// 如果不是vue文件,那么就到此为止,其他文件不支持
if (!filename.endsWith('.vue')) {
store.state.errors = []
return
}
// 文件名不能重复,所以可以通过hash生成一个唯一的id,后面编译的时候会用到
const id = hashId(filename)
// 解析vue单文件
const {
errors, descriptor } = store.compiler.parse(code, {
filename,
sourceMap: true
})
// 如果解析出错,保存错误信息然后返回
if (errors.length) {
store.state.errors = errors
return
}
// 接下来进行了两个判断,不影响主流程,代码就不贴了
// 判断template和style是否使用了其他语言,是的话抛出错误并返回
// 判断script是否使用了ts外的其他语言,是的话抛出错误并返回
// ...
}
vue
하나의 파일을 컴파일하기 위한 패키지는 @vue/compiler-sfc 입니다 . 버전부터는 이 패키지가 패키지 3.2.13
에 내장됩니다 . 이 패키지는 설치 후 바로 사용할 수 있습니다. 이 패키지는 업그레이드와 함께 업그레이드 되므로 죽음에 기록되지는 않지만 수동으로 구성할 수 있습니다.vue
vue
vue
@vue/repl
import * as defaultCompiler from 'vue/compiler-sfc'
export class ReplStore implements Store {
compiler = defaultCompiler
vueVersion?: string
async setVueVersion(version: string) {
this.vueVersion = version
const compilerUrl = `https://unpkg.com/@vue/compiler-sfc@${
version}/dist/compiler-sfc.esm-browser.js`
const runtimeUrl = `https://unpkg.com/@vue/runtime-dom@${
version}/dist/runtime-dom.esm-browser.js`
this.pendingCompiler = import(/* @vite-ignore */ compilerUrl)
this.compiler = await this.pendingCompiler
// ...
}
}
기본적으로 현재 웨어하우스가 사용되지만 compiler-sfc
지정된 버전의 합계는 store.setVueVersion
메서드를 호출하여 설정할 수 있습니다 .vue
compiler
App.vue
콘텐츠가 다음과 같다고 가정합니다 .
<script setup>
import {
ref } from 'vue'
const msg = ref('Hello World!')
</script>
<template>
<h1>{
{ msg }}</h1>
<input v-model="msg">
</template>
<style>
h1 {
color: red;
}
</style>
compiler.parse
메서드는 이를 다음 결과로 구문 분석합니다.
사실 , , 세 부분의 내용을 분석하는 script
것 template
입니다 style
.
compileFile
방법 으로 돌아가서 계속하십시오 .
export async function compileFile(){
// ...
// 是否有style块使用了scoped作用域
const hasScoped = descriptor.styles.some((s) => s.scoped)
// 保存编译结果
let clientCode = ''
const appendSharedCode = (code: string) => {
clientCode += code
}
// ...
}
clientCode
최종 컴파일 결과를 저장할 때 사용합니다.
컴파일 스크립트
compileFile
방법 으로 돌아가서 계속하십시오 .
export async function compileFile(){
// ...
const clientScriptResult = await doCompileScript(
store,
descriptor,
id,
isTS
)
// ...
}
doCompileScript
메서드의 컴파일된 부분을 호출합니다 script
. 실제로 구문을 template
사용하지 않거나 수동으로 구성하지 않는 한 부분도 함께 컴파일됩니다. <script setup>
다음을 수행하지 마십시오.
h(Repl, {
sfcOptions: {
script: {
inlineTemplate: false
}
}
})
doCompileScript
이 상황을 무시하고 메서드 구현을 살펴보겠습니다 .
export const COMP_IDENTIFIER = `__sfc__`
async function doCompileScript(
store: Store,
descriptor: SFCDescriptor,
id: string,
isTS: boolean
): Promise<[string, BindingMetadata | undefined] | undefined> {
if (descriptor.script || descriptor.scriptSetup) {
try {
const expressionPlugins: CompilerOptions['expressionPlugins'] = isTS
? ['typescript']
: undefined
// 1.编译script
const compiledScript = store.compiler.compileScript(descriptor, {
inlineTemplate: true,// 是否编译模板并直接在setup()里面内联生成的渲染函数
...store.options?.script,
id,// 用于样式的作用域
templateOptions: {
// 编译模板的选项
...store.options?.template,
compilerOptions: {
...store.options?.template?.compilerOptions,
expressionPlugins// 这个选项并没有在最新的@vue/compiler-sfc包的源码中看到,可能废弃了
}
}
})
let code = ''
// 2.转换默认导出
code +=
`\n` +
store.compiler.rewriteDefault(
compiledScript.content,
COMP_IDENTIFIER,
expressionPlugins
)
// 3.编译ts
if ((descriptor.script || descriptor.scriptSetup)!.lang === 'ts') {
code = await transformTS(code)
}
return [code, compiledScript.bindings]
} catch (e: any) {
store.state.errors = [e.stack.split('\n').slice(0, 12).join('\n')]
return
}
} else {
return [`\nconst ${
COMP_IDENTIFIER} = {}`, undefined]
}
}
이 함수는 주로 세 가지 일을 하는데, 하나씩 살펴보겠습니다.
1. 스크립트 컴파일
compileScript
메서드 컴파일을 호출합니다 . 이 메서드는 구문, 변수 주입 및 기타 기능을 script
처리합니다 . 변수 주입은 레이블 에서 바인딩된 구성 요소 데이터의 사용 을 나타냅니다 . [CSS의 v-bind]( 단일 파일 구성 요소 CSS 함수| Vue.js) 에 대해 자세히 알아보세요. ).<script setup>
css
css
style
v-bind
data
구문을 사용 <script setup>
하고 inlineTemplate
옵션을 전달하면 true
부품이 template
렌더링 함수로 컴파일되고 setup
동시에 함수에 인라인됩니다. 그렇지 않으면 template
별도로 컴파일해야 합니다.
id
매개변수는 블록에서 사용하거나 구문을 사용할 scoped id
때 로 사용되며 이를 사용하여 고유한 클래스 이름, 스타일 이름을 생성 해야 합니다 .style
scoped
v-bind
id
class
컴파일 결과는 다음과 같습니다.
setup
템플릿 부분이 렌더링 기능으로 컴파일되고 구성 요소의 기능 에 인라인되고 export default
기본 내보내기 구성 요소가 사용되는 것을 볼 수 있습니다 .
2. 기본 내보내기 변환
이 단계는 이전에 얻은 기본 내보내기 문을 변수 정의 형식으로 변환합니다. 메소드를 사용하여 rewriteDefault
이 메소드는 변환할 내용, 변수 이름, 플러그인 배열의 세 가지 매개변수를 받습니다. 이 플러그인 배열은 사용하도록 전달됩니다 babel
. , 그래서 사용하는 경우 ts
전달됩니다 ['typescript']
.
변환 결과는 다음과 같습니다.
변수로 만들면 어떤 점이 좋은가요 사실 객체에 다른 속성을 추가하는 것이 편리합니다 폼에 있는 경우 export default {}
이 객체에 일부 속성을 확장하려면 어떻게 해야 할까요? 정규 경기? ast
나무 로 변신 ? 가능한 것 같지만 소스 내용을 조작해야 하기 때문에 그다지 편리하지는 않지만 변수로 변환하는 것은 매우 간단합니다. 암호:
__sfc__.xxx = xxx
소스 콘텐츠가 무엇인지 알 필요 없이 직접 추가하려는 특성을 추가하기만 하면 됩니다.
3. ts 컴파일
마지막 단계는 사용 여부를 결정 ts
하고, 그렇다면 위에서 언급한 방법을 사용하여 transformTS
컴파일합니다.
컴파일 후 메서드 script
로 돌아갑니다 compileFile
.
export async function compileFile(){
// ...
// 如果script编译没有结果则返回
if (!clientScriptResult) {
return
}
// 拼接script编译结果
const [clientScript, bindings] = clientScriptResult
clientCode += clientScript
// 给__sfc__组件对象添加了一个__scopeId属性
if (hasScoped) {
appendSharedCode(
`\n${
COMP_IDENTIFIER}.__scopeId = ${
JSON.stringify(`data-v-${
id}`)}`
)
}
if (clientCode) {
appendSharedCode(
`\n${
COMP_IDENTIFIER}.__file = ${
JSON.stringify(filename)}` +// 给__sfc__组件对象添加了一个__file属性
`\nexport default ${
COMP_IDENTIFIER}`// 导出__sfc__组件对象
)
compiled.js = clientCode.trimStart()// 将script和template的编译结果保存起来
}
// ...
}
export default
변수 정의로 변환하면 새 속성을 쉽게 추가할 수 있다는 이점이 있습니다 .
마지막으로 export default
내보내기로 정의된 변수를 사용합니다.
컴파일 템플릿
앞에서도 여러 번 언급했듯이 <script setup>
구문을 사용하고 inlineTemplate
옵션을 설정하지 않으면 수동 false
으로 컴파일할 필요가 없으며 template
직접 컴파일하려면 메서드를 호출하여 compiler.compileTemplate
컴파일하면 됩니다. 사실, 이것은 compiler.compileScript
우리가 이것을 조정하는 데 도움이 된 방법 내부에 있습니다 그것은 단지 방법일 뿐입니다. 이 방법은 template
그것을 렌더링 함수로 컴파일할 것입니다. 우리는 또한 이 렌더링 함수 문자열을 그것에 연결 하고 clientCode
구성 요소 옵션 개체에 속성 을 추가합니다 . 이전 단계에서 컴파일된 개체 script
이며 값은 이 렌더링 함수입니다.__sfc__
render
let code =
`\n${
templateResult.code.replace(
/\nexport (function|const) render/,
`$1 render`
)}` + `\n${
COMP_IDENTIFIER}.render = render
컴파일 스타일
방법 으로 돌아가서 단일 파일의 합계가 compileFile
여기에서 컴파일되었으며 다음 부분이 처리됩니다.vue
script
template
style
export async function compileFile(){
// ...
let css = ''
// 遍历style块
for (const style of descriptor.styles) {
// 不支持使用CSS Modules
if (style.module) {
store.state.errors = [
`<style module> is not supported in the playground.`
]
return
}
// 编译样式
const styleResult = await store.compiler.compileStyleAsync({
...store.options?.style,
source: style.content,
filename,
id,
scoped: style.scoped,
modules: !!style.module
})
css += styleResult.code + '\n'
}
if (css) {
// 保存编译结果
compiled.css = css.trim()
} else {
compiled.css = '/* No <style> tags present */'
}
}
간단합니다. compileStyleAsync
컴파일 블록 메서드를 사용하면 이 메서드가 , 및 구문을 style
처리하는 데 도움이 됩니다 .scoped
module
v-bind
이 시점에서 파일 컴파일 부분이 소개됩니다. 요약하자면 다음과 같습니다.
- 스타일 파일은 기본적으로만 사용할 수 있으므로
css
컴파일할 필요가 없습니다. js
파일은 원래 컴파일할 필요는 없으나 실험적인$ref
구문을 사용할 수 있으므로 판단 및 처리가 필요합니다.사용할 경우ts
컴파일해야 합니다.vue
@vue/compiler-sfc
단일 파일이 컴파일 되고 일부는 구문, 변수 주입 및 기타 기능을script
처리합니다 .이를 사용하면 컴파일도됩니다 .최종 결과는 실제로 구성 요소 개체입니다. 함께 컴파일 하든 별도로 컴파일하든 상관없이 렌더링 함수로 컴파일되어 컴포넌트 객체에 마운트, 부분 컴파일 후 바로 저장 가능setup
css
ts
ts
template
script
style
시사
파일이 컴파일된 후 직접 미리 볼 수 있습니까? 안타깝게도 이전 파일은 일반 모듈을 얻기 위해 컴파일되기 때문에 , ESM
즉 가져오기 및 내보내기를 import
통해 예를 들어 파일을 만든 다음export
App.vue
Comp.vue
App.vue
// App.vue
import Comp from './Comp.vue'
./Comp.vue
언뜻 보기에는 문제가 없어 보이지만 문제는 서버에 파일이 없다는 것입니다. , 그리고 우리의 시뮬레이션에 의해 생성된 파일은 결국 태그를 통해 페이지에 하나씩 삽입 되므로 다른 형식으로 변환 <script type="module">
해야 합니다 .import
export
아이프레임 만들기
미리보기 섹션은 먼저 다음을 생성합니다 iframe
.
onMounted(createSandbox)
let sandbox: HTMLIFrameElement
function createSandbox() {
// ...
sandbox = document.createElement('iframe')
sandbox.setAttribute(
'sandbox',
[
'allow-forms',
'allow-modals',
'allow-pointer-lock',
'allow-popups',
'allow-same-origin',
'allow-scripts',
'allow-top-navigation-by-user-activation'
].join(' ')
)
// ...
}
요소 를 만들고 프레임에서 페이지의 일부 동작이 허용되는지 여부를 제어할 수 있는 속성을 iframe
설정합니다 . 자세한 내용은 arrt-sand-box 입니다 .sandbox
iframe
import srcdoc from './srcdoc.html?raw'
function createSandbox() {
// ...
// 检查importMap是否合法
const importMap = store.getImportMap()
if (!importMap.imports) {
importMap.imports = {
}
}
if (!importMap.imports.vue) {
importMap.imports.vue = store.state.vueRuntimeURL
}
// 向框架页面内容中注入import-map
const sandboxSrc = srcdoc.replace(
/<!--IMPORT_MAP-->/,
JSON.stringify(importMap)
)
// 将页面HTML内容注入框架
sandbox.srcdoc = sandboxSrc
// 添加框架到页面
container.value.appendChild(sandbox)
// ...
}
srcdoc.html
미리 보기에 사용되는 페이지로 컨텐츠를 먼저 주입한 다음 페이지를 import-map
생성 iframe
하여 렌더링합니다 .
let proxy: PreviewProxy
function createSandbox() {
// ...
proxy = new PreviewProxy(sandbox, {
on_error:() => {
}
// ...
})
sandbox.addEventListener('load', () => {
stopUpdateWatcher = watchEffect(updatePreview)
})
}
PreviewProxy
다음으로 클래스의 인스턴스를 생성 하고 마지막으로 iframe
로딩이 완료되면 사이드 이펙트 함수를 등록하는 updatePreview
방식으로 파일을 처리하고 미리보기 작업을 수행하게 됩니다.
iframe과 통신
PreviewProxy
이 클래스는 주로 다음 iframe
과 통신하는 데 사용됩니다.
export class PreviewProxy {
constructor(iframe: HTMLIFrameElement, handlers: Record<string, Function>) {
this.iframe = iframe
this.handlers = handlers
this.pending_cmds = new Map()
this.handle_event = (e) => this.handle_repl_message(e)
window.addEventListener('message', this.handle_event, false)
}
}
message
이벤트는 다음 메서드에서 정보를 수신 iframe
하고 iframe
정보를 보낼 수 있습니다 postMessage
.
export class PreviewProxy {
iframe_command(action: string, args: any) {
return new Promise((resolve, reject) => {
const cmd_id = uid++
this.pending_cmds.set(cmd_id, {
resolve, reject })
this.iframe.contentWindow!.postMessage({
action, cmd_id, args }, '*')
})
}
}
이 방법 을 통해 에게 iframe
메시지를 보내고 하나 promise
를 id
반환 할 수 있습니다 promise
. 부모 창에 응답하고 이를 다시 보내면 부모 창은 이 검색을 통해 작업 실행의 성공 또는 실패에 따라 호출할 창을 결정할 수 있습니다.resolve
reject
id
id
iframe
iframe
id
id
resove
reject
iframe
부모에게 정보를 보내는 방법:
// srcdoc.html
window.addEventListener('message', handle_message, false);
async function handle_message(ev) {
// 取出任务名称和id
let {
action, cmd_id } = ev.data;
// 向父级发送消息
const send_message = (payload) => parent.postMessage( {
...payload }, ev.origin);
// 回复父级,会带回id
const send_reply = (payload) => send_message({
...payload, cmd_id });
// 成功的回复
const send_ok = () => send_reply({
action: 'cmd_ok' });
// 失败的回复
const send_error = (message, stack) => send_reply({
action: 'cmd_error', message, stack });
// 根据actiion判断执行什么任务
// ...
}
미리보기를 위해 모듈 컴파일
다음으로 메서드를 살펴보자 updatePreview
이 메서드에서는 파일을 다시 컴파일하여 모듈 목록을 가져온 js
다음 실제로 코드인 모듈 목록을 로 전송 iframe
하고 태그를 iframe
동적으로 생성하여 script
에 삽입합니다. 이러한 모듈 코드는 iframe
미리보기를 위해 페이지를 업데이트하는 효과를 얻습니다.
async function updatePreview() {
// ...
try {
// 编译文件生成模块代码
const modules = compileModulesForPreview(store)
// 待插入到iframe页面中的代码
const codeToEval = [
`window.__modules__ = {}\nwindow.__css__ = ''\n` +
`if (window.__app__) window.__app__.unmount()\n` +
`document.body.innerHTML = '<div id="app"></div>'`,
...modules,
`document.getElementById('__sfc-styles').innerHTML = window.__css__`
]
// 如果入口文件时Vue文件,那么添加挂载它的代码!
if (mainFile.endsWith('.vue')) {
codeToEval.push(
`import { createApp } as _createApp } from "vue"
const _mount = () => {
const AppComponent = __modules__["${
mainFile}"].default
AppComponent.name = 'Repl'
const app = window.__app__ = _createApp(AppComponent)
app.config.unwrapInjectedRef = true
app.config.errorHandler = e => console.error(e)
app.mount('#app')
}
_mount()
)`
}
// 给iframe页面发送消息,插入这些模块代码
await proxy.eval(codeToEval)
} catch (e: any) {
// ...
}
}
codeToEval
배열은 미리보기의 원리를 드러내며, codeToEval
배열의 내용은 iframe
마지막에 페이지로 전송되고 동적으로 script
태그를 생성하여 페이지에 삽입하여 작동시킵니다.
먼저 다른 파일을 추가합니다 Comp.vue
.
<script setup>
import {
ref } from 'vue'
const msg = ref('我是子组件')
</script>
<template>
<h1>{
{ msg }}</h1>
</template>
그런 다음 App.vue
구성 요소를 가져옵니다.
<script setup>
import {
ref } from 'vue'
import Comp from './Comp.vue'// ++
const msg = ref('Hello World!')
</script>
<template>
<h1>{
{ msg }}</h1>
<input v-model="msg">
<Comp></Comp>// ++
</template>
<style>
h1 {
color: red;
}
</style>
이때 앞선 [파일 컴파일]에서 처리한 후 Comp.vue
컴파일 결과는 다음과 같다.
App.vue
컴파일 결과는 다음과 같습니다.
compileModulesForPreview
각 파일은 주로 다음 작업을 수행하기 위해 다시 컴파일됩니다.
1. 모듈의 내보내기 문을 export
특성 추가 문으로 변환합니다. 즉, window.__modules__
개체에 모듈을 추가합니다.
const __sfc__ = {
__name: 'Comp',
// ...
}
export default __sfc__
다음으로 변환:
const __module__ = __modules__["Comp.vue"] = {
[Symbol.toStringTag]: "Module" }
__module__.default = __sfc__
2. 객체에서 지정된 모듈을 얻을 수 있도록 import
상대 경로가 있는 모듈 ./
의 명령문을 할당 명령문으로 변환합니다 .__modules__
import Comp from './Comp.vue'
다음으로 변환:
const __import_1__ = __modules__["Comp.vue"]
3. 마지막으로 가져온 구성 요소가 사용되는 위치를 변환합니다.
_createVNode(Comp)
다음으로 변환:
_createVNode(__import_1__.default)
4. 구성 요소에 스타일이 있는 경우 window.__css__
문자열에 추가합니다.
if (file.compiled.css) {
js += `\nwindow.__css__ += ${
JSON.stringify(file.compiled.css)}`
}
이때 codeToEval
배열의 내용을 보면 매우 명확합니다.먼저 전역 객체 window.__modules__
와 전역 문자열을 생성합니다. 인스턴스 가 window.__css__
이전에 이미 존재한다면 __app__
업데이트라는 의미입니다.그런 다음 이전 구성 요소를 먼저 제거하고, 그런 다음 구성 요소를 마운트하는 데 id
사용 만든 다음 메서드 컴파일에 의해 반환된 모듈 배열을 추가하여 이러한 구성 요소의 전역 변수가 실행 중일 때 정의되고 구성 요소가 스타일을 추가 모든 구성 요소가 실행 중이 면 페이지에 스타일을 추가하십시오.app
div
Vue
compileModulesForPreview
window.__css__
window.__css__
마지막으로 엔트리 파일이 구성 요소인 경우 인스턴스화 및 마운팅 코드의 다른 섹션이 Vue
추가됩니다 .Vue
compileModulesForPreview
방법은 비교적 길다.할 일은 대략적으로 엔트리 파일에서 시작하여 이전 4점에 따라 파일을 변환한 다음 모든 종속 구성 요소를 재귀적으로 변환합니다.구체적인 변환 방법은 모듈을 트리로 변환한 다음 매직스트링을 이용해서 babel
수정 AST
하세요 소스코드, 이런 코드는 아시는 분들은 아주 간단하지만 트리 작업을 접해보지 않으신 분들 은 어려워서 AST
구체적인 코드는 올리지 않겠습니다. 특정 구현을 보고 싶다면 moduleCompiler.ts 를 클릭하면 됩니다 .
codeToEval
배열 콘텐츠가 준비되면 iframe
미리보기에 메시지를 보낼 수 있습니다.
await proxy.eval(codeToEval)
iframe
메시지를 받은 후 이전에 추가한 script
레이블이 먼저 삭제된 다음 새 레이블이 생성됩니다.
// scrdoc.html
async function handle_message(ev) {
let {
action, cmd_id } = ev.data;
// ...
if (action === 'eval') {
try {
// 移除之前创建的标签
if (scriptEls.length) {
scriptEls.forEach(el => {
document.head.removeChild(el)
})
scriptEls.length = 0
}
// 遍历创建script标签
let {
script: scripts } = ev.data.args
if (typeof scripts === 'string') scripts = [scripts]
for (const script of scripts) {
const scriptEl = document.createElement('script')
scriptEl.setAttribute('type', 'module')
const done = new Promise((resolve) => {
window.__next__ = resolve
})
scriptEl.innerHTML = script + `\nwindow.__next__()`
document.head.appendChild(scriptEl)
scriptEls.push(scriptEl)
await done
}
}
// ...
}
}
모듈을 하나씩 순서대로 추가하기 위해 하나를 생성 promise
하고 resove
메서드를 전역 속성에 할당한 __next__
다음 각 모듈의 끝에서 호출 코드를 연결하여 태그 script
가 삽입하면 태그의 코드가 메서드를 실행하고 window.__next__
현재 메서드를 종료 하고 플러그인의 promise
다음 탭으로 들어갑니다 . 여전히 매우 영리합니다.script
요약하다
이 글은 컴포넌트의 구현을 소스코드의 관점에서 살펴본다 실제로 관련 내용, 엔트리 파일로 사용 , 정보 출력 등 @vue/repl
많은 내용이 무시된다 관심 있는 분들은 소스코드를 읽어보면 된다 당신 자신.ssr
html
이 구성 요소는 running 을 지원하지 않기 때문에 Vue2
동료 중 한 명이 fork
수정하여 Vue2
새 버전을 만들었습니다.필요한 경우 vue2-repl 에주의하십시오 .
마지막으로 제 오픈소스 프로젝트도 추천하는데 역시 온라인이고 단일 파일을 Playground
지원 하지만 좀 더 일반적이지만 다중 파일 생성을 지원하지 않습니다 .Vue2
Vue3