前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >实用的VUE系列——每天在用的Vue-SFC-Playground你真的了解吗?

实用的VUE系列——每天在用的Vue-SFC-Playground你真的了解吗?

作者头像
用户7413032
发布2024-03-23 07:50:44
2020
发布2024-03-23 07:50:44
举报
文章被收录于专栏:佛曰不可说丶佛曰不可说丶

声明:本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

前言

上回书说到我到底应该选择拥抱vue还是react,能看懂VUE源码到底是不是高人一等。 好吧,其实就是水了一篇

但我发下宏愿,说要讲点实用的 vue 系列那绝对是相当真诚的!

因为从毕业开始,接触 vue五年有余,一直以来,我总是想体系的梳理一下 vue 相关内容,至于目的嘛 ,无非是为名为利

今年时机终于到了,但愿我能坚持下去!

写到这里,相信已经有jym 抄起板砖,咬牙切齿,身体作投掷状,

那个,我知道你们迫不及待了,马上正式开始~

vue生态周边到底藏着多少知识点?

Playground 翻译过来,演练场,也就是在线 vue 运行环境

这是一个非常实用的项目,要技术深度有技术深度,要知识点有知识点,要解决方案,有解决方案,

综上所述,他是一个非常值得研究的项目!

对你的架构能力,设计能力,代码能力,知识点理解,都有巨大的帮助

不信?

带你看看~

但是带你看之前~

我得先找到项目,可以说这个 vue 项目相当难找,我本来以为,他是在 vue3 的工程文件里, 因为 core(vue 源码工程)项目里,有一个sfc-playground

然后,我找了半天,对不起没有,然后我猜他一定有包引用,于是千回百转

在这 repl

当然,还有我 fork 注释版

vue-sfc-playground

项目下载了,知识点在哪呢?

接下来,开始!!!!

1、 如何在浏览器端如果不经过打包 引用 通过 import 引用 在线vue

es6 语法出来有很多年import 我们也经常在用,但相信很多刚入行的 jym 都会很好奇,但凡我们想要使用 ES Module 都要离不开 webpackrullop 等工程化的打包工具。

这是为什么呢?

其实这个原因很简单,浏览器不方便支持,注意,不是不能支持,而是不方便支持。

之所以会产生这种滑稽的现象是因为,制订标准的人,和开发浏览器的人不是一波人

我们知道ES6 是由ECMA组织制定的标准,而浏览器一般是由谷歌主导开发

自古以来,上有政策,下有对策,制定是制定,执行是执行(职场老油条 dddd)。

所以,谷歌的态度很简单,你可以制定,但我可以选择性执行。

于是他们选择性加入了 es6 的一些特性,比如:

  • 1、块级作用域变量(let和const)
  • 2、箭头函数
  • 3、模板字符串
  • 4、解构赋值
  • 5、默认参数
  • 6、扩展运算符
  • 7、类和继承
  • 8、Promise

而最重要的,模块化语法并没有加入。

我猜原因很简单,谷歌浏览器作为历史悠久的头部浏览器,他们有历史包袱,他们需要兼容老代码,开辟新代码。

注意这是非常难的,相信做过老项目的都知道, 这种项目我们要遵循的唯一原则就是有一个能跑就行

当然,谷歌不是一般的老项目,而是历史悠久的老项目,所以,他必须要做取舍。

他舍弃了直接兼容模块化新特性,我相信他们内部其实做了一个痛苦的抉择;

有可能还在为实现方式大大出手!!

因为制定标准总是很容易的(上嘴皮下嘴皮一碰的事),做事总是很不容易的。

他们虽然没有直接实现标准,还是曲线救国的间接实现了。

代码如下:

代码语言:javascript
复制
<html  lang="en">
    <body>
        <div id="container">my name is {name}</div>
        <script type="module">
           import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.esm.browser.js'
           new Vue({
             el: '#container',
             data:{
                name: 'Bob'
             }
           })
        </script>
    </body>
</html>

我们需要 type="module" 即可,利用这个特性,甚至还诞生了大名鼎鼎 vite

当然除了谷歌浏览器 其他的浏览器也纷纷效仿,ES modules的支持情况如下:

然而,这样还是不够的,因为我们日常引用 vue 是这样的:

代码语言:javascript
复制
import Vue from 'vue'

怎么办呢?

于是继续开始,他定义了import.map 来前置引入vue链接,代码如下 :

代码语言:javascript
复制
//html
<script type="importmap">
  {
    "imports":{
      "vue":"https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.esm.browser.js"
    }
  }
</script>
<script type="module" src="/index.js"></script>

//index.js
 import Vue from 'vue'
 new Vue({
    el: '#container',
    data:{
    name: 'Bob'
    }
 })

有了这个特性,我们的沙箱构建的第一步就有了,因为沙箱中,要编译执行 vue 代码,就要有包的引用,如此一来,我们就能简单的引用 vue

2、怎样构造与浏览器宿主环境一致的沙箱实现

什么是沙箱

也称作:“沙盒/沙盘”。沙箱是一种安全机制,为运行中的程序提供隔离环境。通常是作为一些来源不可信、具破坏力或无法判定程序意图的程序提供实验之用。沙箱能够安全的执行不受信任的代码,且不影响外部实际代码影响的独立环境。

其实我就是就是一个不受外部影响的干净的执行环境

沙箱这个名字,虽然听起来比较玄乎 但其实,在我们的日常开发中,无不在使用沙箱

比如:

IIFE

JavaScript 中目前有三种作用域: 全局作用域、函数作用域、ES6 新增的块级作用域。通过给一段代码包裹一层函数可以实现作用的隔离,这通常基于 IIFE 立即执行函数来实现,也被称作自执行匿名函数。

这个在我们的打包过程中,相信大家经常遇到,他的特性就可以用来做 js 的隔离沙箱

代码语言:javascript
复制
 (function (window) {
  var sum = 0
  for (var i = 1; i <= 100; i++) {
    sum += i
  }
  window.total=sum
})(window)
console.log(total)
console.log(sum) // 无法获取
eval

eval() 函数会将传入的字符串当做 JavaScript 代码进行执行,而我们在一个沙箱函数中,传入需要的上下文环境,eval 中执行字符串,依赖执行上下文环境,从而避免影响外部程序,代码如下:

代码语言:javascript
复制
    // 执行上下文环境
const ctx = {
  func: (v) => {
    console.log(v);
  },
  foo: "foo",
};

function sandbox(code, ctx) {
// 取
  eval(code); 
}
..
const code = `
    ctx.foo = 'bar'
    ctx.func(ctx.foo)
`;

sandbox(code, ctx); // bar

当然,诸如 new Functionwith特定的程序设计proxy、也能实现类似的沙箱环境,但都是不完美的,因为js 语言的设计特性 他总会沿着作用于链,往上找,直到可能污染全局变量

Web Worker

Web Worker 是 HTML5 标准的一部分,这一规范定义了一套 API,允许我们在 js 主线程之外开辟新的 Worker 线程,并将一段 js 脚本运行其中,它赋予了开发者利用 js 操作多线程的能力。

既然是多线程,所以,他还是一个天然的 js 沙箱环境 ,我们要做的,建立与Web Worker的通信即可。

其实实现通信很简单,因为一个原因,人类都是很懒惰的,一旦发明了一种套路,就要往死了用,直到用不动了为止

还记得 iframe吗? 没错,通信方式一模一样

代码如下:

代码语言:javascript
复制
// main.js(主线程)

const myWorker = new Worker('/worker.js'); // 创建worker

myWorker.addEventListener('message', e => { // 接收消息
    console.log(e.data); // Greeting from Worker.js,worker线程发送的消息
});

// 这种写法也可以
// myWorker.onmessage = e => { // 接收消息
//    console.log(e.data);
// };

myWorker.postMessage('Greeting from Main.js'); // 向 worker 线程发送消息,对应 worker 线程中的 e.data


// worker.js(worker线程)
self.addEventListener('message', e => { // 接收到消息
    console.log(e.data); // Greeting from Main.js,主线程发送的消息
    self.postMessage('Greeting from Worker.js'); // 向主线程发送消息
});

不同的沙箱,要用在不同的场景,比如,jq 为了隐藏细节,选用 iife , 微前端,为了不同项目之间的 js 隔离,

发明了诸如基于 proxy 的 legacySandbox(单例沙箱) proxySandbox(多例沙箱)

自然的,想要实现,一个干净的样式,js执行环境, 当然是 iframe最合适

iframe沙箱

于是有人问了,既然iframe沙箱 这么好,既能实现天然的 js 隔离,又能实现css样式隔离,为啥所有的微前端都不选择用这个方案呢?

这个问题,我可以回答:体验问题

iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。

  1. url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
  2. UI 不同步,DOM 结构不共享
  3. 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
  4. 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。

理论上来说,iframe 有这么多缺点,应该不能成为沙箱环境的第一选择。

但历史告诉我们,凡事皆有例外,例外从Vue-SFC-Playground开始

因为我们渲染的代码的时候,这些缺点却成了邪门的优点,因为他天然的隔离性,我们可以在里面随意的渲染代码,而不用担心影响到外部的代码或者样式,并且只需要初始化一次,后续通过相互通信来实现热更新 ,而相互通信虽然麻烦,但这才是考研各位jym的能耐的地方不是吗?

3、怎样和沙箱环境通信

接下来,考验各位jym能力的时候到了,各位自行设计通信之前,我们现分析分析Vue-SFC-Playground 是怎么做的,让大佬给我们打个样

开始之前,我们先温习一下怎样实现通信

iframe想要实现通信,有两种情况

  • 1、同源
  • 2、跨域

同源

同源策略会限制 窗口(window) 和 frame 之间的通信,因此首先要知道同源策略。

所谓同源策略 就是两个 URL 具有相同的协议,域,和端口

在同源的情况下,我们能轻而易举的获取如下信息:

  • iframe.contentWindow 来获取 中的 window
  • iframe.contentDocument 来获取 中的 document

跨域

跨域状态下,我们就要用到 postMessage,无论它们来自什么源

想要发送消息的窗口需要调用接收窗口的 postMessage 方法。换句话说,如果我们想把消息发送给 win,我们应该调用 win.postMessage(data, targetOrigin)。

为了接收消息,目标窗口应该在 message 事件上有一个处理程序。当 postMessage 被调用时触发该事件 ,注意要使用 addEventListener 绑定事件

代码如下:

代码语言:javascript
复制
// 1、父页面向子页面发送消息
let data = { type: 'answerResult', data: jsonData.data }
this.$refs.iframe.contentWindow.postMessage(data, '*')

// 2、子页面向父页面发送消息
let parentData = { type: 'passDataBack', data: passData }
window.parent.postMessage(parentData, '*')

// 3、接收消息方法
window.addEventListener('message', function (e) {})

基本的 api 我们理解了,接下来就看大佬是怎么封装的吧

大佬的封装分为这么几步:

  • 1、创建沙箱环境(也就是 iframe)
  • 2、挂载沙箱
  • 3、与沙箱建立通信
  • 4、区分通信类型

1、创建沙箱环境(也就是 iframe)和挂载沙箱

创建沙箱环境,大佬为了代码的封装, 采用动态创建的方式,并且引入了沙箱中的执行代码

代码如下:

代码语言:javascript
复制
//创建沙箱
function createSandbox() {
 
 
  // 初始化 iframe
  sandbox = document.createElement('iframe')
  // 添加必要属性

  // 1. sandbox=""
  //   应用所有限制

  // 2. sandbox="allow-same-origin"
  //   允许 iframe 内容被视为与包含文档有相同的来源。

  // 3. sandbox="allow-top-navigation"
  //   允许 iframe 内容从包含文档导航(加载)内容。
  //   可用于禁用外部网站的JS跳转、target="_parent"、target="_top"等

  // 4. sandbox="allow-forms"
  //   允许表单提交。

  // 5. sandbox="allow-scripts"
  //   允许脚本执行,即允许iframe运行脚本(但不创建弹出窗口)。
  //   可用于禁用外部网站的JS

  // 6. sandbox="allow-popups"
  //   允许弹出窗口(如window.open,target="_blank")。

  // 5. sandbox="allow-scripts"
  //   允许弹出窗口逃离沙箱:允许一个沙箱文件打开新窗口不强制使用沙盒。
  sandbox.setAttribute(
    'sandbox',
    [
      'allow-forms',
      'allow-modals',
      'allow-pointer-lock',
      'allow-popups',
      'allow-same-origin',
      'allow-scripts',
      'allow-top-navigation-by-user-activation',
    ].join(' '),
  )
  // 引入文件vue,和引入 vue-ssr
  const importMap = store.getImportMap()
  console.log(importMap)
  // 替换主题等,内容
  // 动态添加沙箱中的运行代码
  const sandboxSrc = srcdoc
    .replace(
      /<html>/,
      `<html class="${previewTheme.value ? theme.value : ''}">`,
    )
    .replace(/<!--IMPORT_MAP-->/, JSON.stringify(importMap)) // 使用importMap模式导入vue 以及vuessr的包
    .replace(
      /<!-- PREVIEW-OPTIONS-HEAD-HTML -->/,
      previewOptions?.headHTML || '', // 占位符head
    )
    .replace(
      /<!--PREVIEW-OPTIONS-PLACEHOLDER-HTML-->/, // 占位符内容
      previewOptions?.placeholderHTML || '',
    )
  //赋值 iframe-container
  sandbox.srcdoc = sandboxSrc
  //挂在 iframe
  container.value.appendChild(sandbox)

  //iframe 加载完毕
  sandbox.addEventListener('load', () => {
    // 为a标签啥的绑定时事件,并且阻止a标签的默认行为,防止页面内打开跳转
    proxy.handle_links()
    // 监听变动
    stopUpdateWatcher = watchEffect(updatePreview)
    // 设置主题
    switchPreviewTheme()
  })
}

与沙箱建立通信和区分通信类型

建立通信,当然也要封装 ,通过初始化一个通信用到的实例,传入对应参数,来设计整体的代码结构

代码语言:javascript
复制
  /// 初始化沙箱,建立通信
  proxy = new PreviewProxy(sandbox, {
    on_fetch_progress: (progress: any) => {
      // pending_imports = progress;
    },
    // 执行错误回调
    on_error: (event: any) => {
      const msg =
        event.value instanceof Error ? event.value.message : event.value
      if (
        msg.includes('Failed to resolve module specifier') ||
        msg.includes('Error resolving module specifier')
      ) {
        runtimeError.value =
          msg.replace(/\. Relative references must.*$/, '') +
          `.\nTip: edit the "Import Map" tab to specify import paths for dependencies.`
      } else {
        runtimeError.value = event.value
      }
    },
    on_unhandled_rejection: (event: any) => {
      let error = event.value
      if (typeof error === 'string') {
        error = { message: error }
      }
      runtimeError.value = 'Uncaught (in promise): ' + error.message
    },
    // console的回调事件
    on_console: (log: any) => {
      // console的回调事件
      if (log.duplicate) {
        return
      }
      if (log.level === 'error') {
        if (log.args[0] instanceof Error) {
          runtimeError.value = log.args[0].message
        } else {
          runtimeError.value = log.args[0]
        }
      } else if (log.level === 'warn') {
        if (log.args[0].toString().includes('[Vue warn]')) {
          runtimeWarning.value = log.args
            .join('')
            .replace(/\[Vue warn\]:/, '')
            .trim()
        }
      }
    },
    on_console_group: (action: any) => {
      // group_logs(action.label, false);
    },
    on_console_group_end: () => {
      // ungroup_logs();
    },
    on_console_group_collapsed: (action: any) => {
      // group_logs(action.label, true);
    },
  })

如此封装的好处,就是我们全局只需要有一个通信事件,通过类型来区分消息类型,再通过传入函数来灵活执行代码,其实他可能还有一个通俗的叫法策略模式

果然,高端的代码,总是有着通俗且悠久的基础原理

具体代码如下:

代码语言:javascript
复制
export class PreviewProxy {
  iframe: HTMLIFrameElement
  handlers: Record<string, Function>
  pending_cmds: Map<
    number,
    { resolve: (value: unknown) => void; reject: (reason?: any) => void }
  >
  handle_event: (e: any) => void

  constructor(iframe: HTMLIFrameElement, handlers: Record<string, Function>) {
    // iframe 实例
    this.iframe = iframe
    //回调函数
    this.handlers = handlers

    this.pending_cmds = new Map()
    // 初始化回调
    this.handle_event = (e) => this.handle_repl_message(e)
    //监听事件,实现和 iframe 的通信
    window.addEventListener('message', this.handle_event, false)
  }
  // 销毁监听
  destroy() {
    window.removeEventListener('message', this.handle_event)
  }

  iframe_command(action: string, args: any) {
    return new Promise((resolve, reject) => {
      const cmd_id = uid++

      this.pending_cmds.set(cmd_id, { resolve, reject })
      // 通知子 iframe 发送事件
      this.iframe.contentWindow!.postMessage({ action, cmd_id, args }, '*')
    })
  }

  handle_command_message(cmd_data: any) {
    let action = cmd_data.action
    let id = cmd_data.cmd_id
    let handler = this.pending_cmds.get(id)

    if (handler) {
      this.pending_cmds.delete(id)
      if (action === 'cmd_error') {
        let { message, stack } = cmd_data
        let e = new Error(message)
        e.stack = stack
        handler.reject(e)
      }

      if (action === 'cmd_ok') {
        handler.resolve(cmd_data.args)
      }
    } else if (action !== 'cmd_error' && action !== 'cmd_ok') {
      console.error('command not found', id, cmd_data, [
        ...this.pending_cmds.keys(),
      ])
    }
  }

  handle_repl_message(event: any) {
    if (event.source !== this.iframe.contentWindow) return

    const { action, args } = event.data
 // 错误成功开始结束的等一些事件回调
    switch (action) {
     
      case 'cmd_error':
      case 'cmd_ok':
        return this.handle_command_message(event.data)
      case 'fetch_progress':
        return this.handlers.on_fetch_progress(args.remaining)
      case 'error':
        return this.handlers.on_error(event.data)
      case 'unhandledrejection':
        return this.handlers.on_unhandled_rejection(event.data)
      // console类型
      case 'console':
        return this.handlers.on_console(event.data)
      case 'console_group':
        return this.handlers.on_console_group(event.data)
      case 'console_group_collapsed':
        return this.handlers.on_console_group_collapsed(event.data)
      case 'console_group_end':
        return this.handlers.on_console_group_end(event.data)
    }
  }
  // 执行代码
  eval(script: string | string[]) {
    //debugger
    return this.iframe_command('eval', { script })
  }

  handle_links() {
    return this.iframe_command('catch_clicks', {})
  }
}

而在沙箱中,我们之前讲过,是通过动态加载一个srcdoc 来执行代码,他的原理很简单,只是拿到父环境编译后的代码执行即可

代码如下:

代码语言:javascript
复制
  if (action === 'eval') {
            //debugger
            try {
              if (scriptEls.length) {
                scriptEls.forEach((el) => {
                  document.head.removeChild(el)
                })
                scriptEls.length = 0
              }
              //拿到 js 加载执行
              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')
                // send ok in the module script to ensure sequential evaluation
                // of multiple proxy.eval() calls
                const done = new Promise((resolve) => {
                  window.__next__ = resolve
                })
                scriptEl.innerHTML = script + `\nwindow.__next__()`
                // 挂在 js
                document.head.appendChild(scriptEl)
                scriptEl.onerror = (err) => send_error(err.message, err.stack)
                scriptEls.push(scriptEl)
                await done
              }
              // 成功以后,通知事件
              send_ok()
            } catch (e) {
              send_error(e.message, e.stack)
            }
          }

总结

讲了这么多,本质上就是简单的讲了一下Vue-SFC-Playground 简单的实现原理,如何通信,如果执行代码,如何展示渲染,当然,他还有热更新代码编辑器 ,如何编译,等等就要靠大家揣摩了

加了注释的源码奉上:vue-sfc-playground

欢迎帮助完善

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2024-03-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • vue生态周边到底藏着多少知识点?
    • 1、 如何在浏览器端如果不经过打包 引用 通过 import 引用 在线vue
      • 2、怎样构造与浏览器宿主环境一致的沙箱实现
        • 什么是沙箱
        • iframe沙箱
      • 3、怎样和沙箱环境通信
        • 同源
        • 跨域
      • 1、创建沙箱环境(也就是 iframe)和挂载沙箱
        • 与沙箱建立通信和区分通信类型
        • 总结
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档