最近在研究如何在浏览器上实现代码在线编译(就类似于地图那种,左边写代码,右边实时运行),然后就发现官方也在用@vue/repl的依赖,于是我也去研究了下,目前是大概搞出了一部分,我就分享出来,咱不遮遮掩掩,全部分享!
1、repl 是啥?
这其实你百度都可以知道,分别是 读取-求值-输出-循环(Read-Eval-Print-Loop),实现在线代码编译。
2、vue中官方的repl是如何做的?
vue2中有现成的在线代码编译,我没具体去用过,有用vue2的可以去试试:vue-code-view
我着重讲一下vue3的在线代码编译,vue3就是将 他内核中浏览器编译模块的源码拿过来用,你可以看到不管是vue官方的repl库还是其他的,都要引用 vue的vue.esm-browser.js的cdn,其实就是实现内核编译的主要模块,下面我展示repl的代码和例图
2.1、官方repl源码demo
你直接在你自己项目里引用@vue/repl 依赖其实也是可以的, 我在官方基础上稍微改了下,代码如下,h 就是jsx语法,这我就不细讲了,不了解jsx其实也没关系,你常规写法也行,ReplStore类实际就是总的编译预览组件的类, 里面的defaultVueRuntimeURL其实就是调用浏览器编译内核cdn的属性,sfcOptions就是相关编译规则配置,包括script,style, template,这是不是和我们vue模板三个一模一样。
import { createApp, h, version } from 'vue'
import { Repl, ReplStore } from '@vue/repl'
const App = {
setup() {
const store = new ReplStore({
defaultVueRuntimeURL: `https://unpkg.com/vue@${version}/dist/vue.esm-browser.js`
})
const sfcOptions = {
script: {
reactivityTransform: true
}
}
return () =>
{
const file = `<script setup>
import { ref } from 'vue'
const msg = ref('hello world!')
</script>
<template>
<h1>{{ msg }}</h1>
<input v-model="msg">
</template>`
store.setFiles({
'App.vue': file
})
return h(Repl, {
store:store,
showCompileOutput:true,
autoResize:true,
sfcOptions:sfcOptions,
clearConsole:false
})
}
}
}
createApp(App).mount('#app')
2.2、ReplStore类方法源码解析
其实这类由尤大大写的,但是你自己去看看源码,就会觉得其实没有想象中的那么优美,我这里就不说缺点了,我来解析几个常用方法:
①、addFile
单个添加文件到编译模块中,最上面例图左边编辑区,不是有个加号吗,就类似这种常规添加,然后就是参数判断,是文件名还是说直接是文件,但是最后都会添加到this.state对象中,这个state就类似于vue2的data,vue3中的setup中return的数据,一般情况下都是直接将你添加的文件作为当前文件
addFile(fileOrFilename: string | File): void {
const file =
typeof fileOrFilename === 'string'
? new File(fileOrFilename)
: fileOrFilename
this.state.files[file.filename] = file
if (!file.hidden) this.setActive(file.filename)
}
②、deleteFile
单个文件删除的,最上面例图左边编辑区,移到文件名那里,不是有个叉吗,就是这种删除,要是当前删除文件正好是你所看到的的文件,那么会将默认未见切换到目前你所看到的地方,默认文件是展示 Hello World!,所以你看到当前你编辑区代码变成Hello World!了,那要么是你ctrl+z一直回退到默认代码了(ps:ctrl + y 为前进快捷键),要么就是你删除完了文件。
deleteFile(filename: string) {
if (confirm(`Are you sure you want to delete ${filename}?`)) {
if (this.state.activeFile.filename === filename) {
this.state.activeFile = this.state.files[this.state.mainFile]
}
delete this.state.files[filename]
}
}
③、getFiles
这跟字面意思一样,就是获取你添加在this.state,files下所有的文件
因为目前我没有setFiles文件,所以为默认的代码,返回包括俩部分,分别是文件名和import-map.json,前者就是内置的默认代码(Hello World),后者是上面说的 defaultVueRuntimeURL参数,不过都是字符格式化过后的了。
getFiles() {
const exported: Record<string, string> = {}
for (const filename in this.state.files) {
exported[filename] = this.state.files[filename].code
}
return exported
}
④、setFiles
这就是我常用的方法,设置文件,因为我最近做地图文档,只需要添加一个文件就可以了,不需要addFile来连续添加,setFiles就类似于直接覆盖this.state.files的意思, mainFile就是文件名的意思,defaultMainFile是默认文件名App.vue,newFiles就是你所添加的文件,这里我没研究透,目前我就用的代码字符串形式来添加的。
async setFiles(newFiles: Record<string, string>, mainFile = defaultMainFile) {
const files: Record<string, File> = {}
if (mainFile === defaultMainFile && !newFiles[mainFile]) {
files[mainFile] = new File(mainFile, welcomeCode)
}
for (const filename in newFiles) {
files[filename] = new File(filename, newFiles[filename])
}
for (const file in files) {
await compileFile(this, files[file])
}
this.state.mainFile = mainFile
this.state.files = files
this.initImportMap()
this.setActive(mainFile)
}
那如何添加呢?就类似于下面,当然文件名你可以任意用自己想要的,不一定用默认的App.vue,
但是你要是想改成自定义的文件名, 就要像我下面截图这种,保持那俩参数一致(文件名为中英文都可以)。
const file = `<script setup>
import { ref } from 'vue'
const msg = ref('你好 林大大哟!')
</script>
<template>
<h1>{{ msg }}</h1>
<input v-model="msg">
</template>`
// 官方默认文件名
store.setFiles({
'App.vue': file
})
// 自定义文件名
store.setFiles({
'林大大哟.vue': file
}, '林大大哟.vue')
⑤、setVueVersion
设置vue内核版本的方法,我们不是调用了vue浏览器在线编译的cdn吗,这个方法就是用来设置我们调用哪个版本内核的。
async setVueVersion(version: string) {
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
this.pendingCompiler = null
this.state.vueRuntimeURL = runtimeUrl
const importMap = this.getImportMap()
;(importMap.imports || (importMap.imports = {})).vue = runtimeUrl
this.setImportMap(importMap)
console.info(`[@vue/repl] Now using Vue version: ${version}`)
}
⑥、resetVueVersion
使用默认的vue内核版本方法,这默认就是我们在初始化ReplStore类时候传的参数
resetVueVersion() {
this.compiler = defaultCompiler
this.state.vueRuntimeURL = this.defaultVueRuntimeURL
}
我上面吧官方主要用的几个方法源码分析了,那我就来说说官方repl源码的缺陷吧!
2.3、官方repl源码缺陷
为啥会说是缺陷呢,因为在每次更换文件,都会触发 compileFile 编译文件操作,然后在加密文件名时候(ps:我也不知道为啥要对文件名进行加密),会调用一个 hashId 函数:
async function hashId(filename: string) {
const msgUint8 = new TextEncoder().encode(filename) // encode as (utf-8) Uint8Array
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8) // hash the message
const hashArray = Array.from(new Uint8Array(hashBuffer)) // convert buffer to byte array
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('') // convert bytes to hex string
return hashHex.slice(0, 8)
}
然后重点来了,会使用crypto加密,要是不知道crypto的,也不需要咋了解,他就是种加密方法,然后官方用的是web 标准的 crypto,但这种加密方法,有致命缺点,就是只能运行在安全环境下(localhost、127.0.0.1或者https等),在非安全环境下,crypto的subtle属性就会不存在,这就奠定了官方repl例子只能本机运行,关于这问题,我也在vue官方repl库提出了issue,不过目前还没有得到反馈。
但是我们大部分个人开发者,想部署到线上,怎么会去使用https呢(ps:有点贵还不值得),所以我对repl进行了部分修改,然后就可以随便部署到http线上环境啦。
3、部分重写的repl模块
上面说了,官方不管,但是我们要用啊,所以我做法先是将官方repl源码拿下来,毕竟大部分就是要用官方的呀!然后主要步骤就是重写 transform 文件里的 hashId 函数:
async function hashId(filename: string) {
return encodeURIComponent(string).replace(/[!'()*]/g, function(c) {
return '%' + c.charCodeAt(0).toString(16).toUpperCase();
});
}
其实就是将crypto加密改成普通的加密了,然后这一步拦截就过了,这是最简单的部分修改,当然还有其他修改,我后面有时间再发出来(最近我正在做地图代码在线编辑,类似于高德地图,但相比肯定差远了哈,手动狗头~)
其他的一些参数添加:
// main.ts
window.VUE_DEVTOOLS_CONFIG = {
defaultSelectedAppId: 'id:repl'
}
全局主体颜色配置
// index.html
window.process = { env: {} }
const saved = localStorage.getItem('vue-sfc-playground-prefer-dark')
if (
saved !== 'false' ||
window.matchMedia('(prefers-color-scheme: dark)').matches
) {
document.documentElement.classList.add('dark')
}
其实目前来看是差不多了,核心就是重写下hashId方法,希望大家都能去实现哦