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/repl .Vue
demo
Vue3
@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
メソッドの 2 番目のパラメーターが渡されていることがわかりますtrue
, これは、バイナリ文字列に変換されることを意味します. これは、js
組み込みの sumbtoa
メソッドが文字列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)));
}
ファイルクラスファイル
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
. テンプレート部分については何も言うことはありません. 大きく分けて左右の2つに分かれています. 左側のエディタを使用し, 右側のプレビューを使用しcodemirror
ますiframe
.script
主要部分を見てください:
// ...
props.store.options = props.sfcOptions
props.store.init()
// ...
sfcOptions
The core is these two lines, which will be saved to the attributes store
passed in when using the component options
, and will be used when Compiling the file later. もちろん、デフォルトでは何も渡されず、空のオブジェクトだけが渡されますstore
。init
ファイル compile を開きます。
ファイルのコンパイル
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
2 つのメソッドは@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
(余談ですが、私がソース コードを見るのが好きな理由の 1 つは、ソース コードから便利なライブラリやツールをいつでも見つけることができるからです)。通常は、公式ツールまたは を使用し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
、の 3 つの部分の内容を分析することです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]
}
}
この関数は主に 3 つのことを行います。1 つずつ見ていきましょう。
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
。使用されている場合は、上記の方法を使用して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
メソッド compileブロックを使用します。このメソッドは、構文を処理するの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
一見問題ないように見えますが、問題はサーバー上にファイルがないことです.このファイルは当方がフロントエンドでシミュレートしただけなので、ブラウザがこのモジュールリクエストを直接送信すると、間違いなく失敗します. 、そして私たちのシミュレーションによって作成されたファイルは最終的に タグを介して 1 つずつページに挿入さ、他の形式に変換する<script type="module">
必要があります。import
export
iframe を作成する
プレビュー セクションでは、最初に次のものが作成されます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
設定します。詳細はart-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 ステートメントを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__
, それは更新であることを意味します. 次に, まず以前のコンポーネントをアンインストールします.次に、It is used to id
mount components の要素を作成し、メソッドのコンパイルによって返されたモジュール配列を追加して、これらのコンポーネントのグローバル変数が実行時に定義され、コンポーネントがそれらにスタイルを追加できるすべてのコンポーネントが実行されている場合、スタイルをページに追加します。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
}
}
// ...
}
}
モジュールを1つずつ順番に追加するために、1つ作成されpromise
、resove
メソッドがグローバル属性に割り当てられ__next__
、呼び出しコードが各モジュールの最後に接合されるため、タグscript
が挿入すると、タグのコードが実行されますwindow.__next__
メソッドを実行すると、現在のメソッドが終了し、プラグインのpromise
次のタブに入ります。それでも非常に巧妙です。script
要約する
この記事では、コンポーネントの実装をソース コードの観点から見ていきます. 実際には、関連するもの@vue/repl
、エントリ ファイルとして使用されるもの、情報出力として使用されるものなど、多くのコンテンツが無視されています. 興味がある場合は、ソース コードを読むことができます.あなた自身。ssr
html
このコンポーネントは実行をサポートしていないためVue2
、同僚の 1 人がfork
変更してVue2
新しいバージョンを作成しました. 必要な場合は、vue2-replに注意してください.
最後に、私のオープン ソース プロジェクトもお勧めします。こちらもオンラインで、単一ファイルPlayground
をサポートしていますVue2
が、より一般的ですが、複数ファイルの作成はサポートしていません。興味がある場合は、 code-runVue3
に注意してください。