最近在研究如何在浏览器上实现代码在线编译(就类似于地图那种,左边写代码,右边实时运行),然后就发现官方也在用@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的代码和例图

vue 在线编辑 javascript vue 在线编译_javascript

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下所有的文件

vue 在线编辑 javascript vue 在线编译_文件名_02

 因为目前我没有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,不过目前还没有得到反馈。

vue 在线编辑 javascript vue 在线编译_前端_03

但是我们大部分个人开发者,想部署到线上,怎么会去使用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方法,希望大家都能去实现哦