手摸手打造类码上掘金在线IDE(六)——沙箱编译(二)

2022-12-02 16:49:32 浏览数 (1)

前言

我们上回书说道沙箱编译的vue编译部分,很多jym以为我会就此金盆洗手, 等着东家发完盒饭踏实回家搬砖。

甚至有Jy 略带嘲讽的给我评论道:

我能从他们的字里行间体会到他们在质问我,就这?我那啥都那啥了你就给我看这?

而由于行文是从丘处机路过牛家村开始,略显墨迹,阅读量,点赞量,可谓惨不忍睹。

发生这种情况,我以为有三个原因

  • 1、本身沙箱编译内容不是流量密码,不是真正干过这个的人,很难产生兴趣和好奇心!
  • 2、我这篇小作文写的确实枯燥,既没有讲原理,也没有讲心得,而是讲科普。
  • 3、对于他们的日常开发没有任何帮助

亲爱的jym啊,我怎么会让自己晚节不保呢?我怎么能让自己这么没有深度呢?

当然还有后续啊,今天我们就来讲讲原理,毕竟原理才是技术圈的流量密码

本着帮人帮到底 送佛送到西的优良品质,也本着绝不认输,不点赞不断更的态度(主要是一个点赞都没有脸上实在挂不住了)。

我们今天就来细致的讲一下vue模板 在到底是如何编译的。

也能让大家能理解,vue项目的整个编译流程,这样就能在工作中更好的学以致用,这样也能在面试官的面前游刃有余

废话少说,我们正式开始!

当然国际惯例,讲编译原理之前,我们还是要从丘处机路过牛家村开始

正常的vue模板编译流程

在介绍正常的vue模板编译流程,我们需要一些前置支持,我们知道的代码编译分为两种

  • 1、html内容直接编译
  • 2、sfc单文件组件编译

html内容编译

html 的编译其实就非常简单了说白了就是利用全量vue 版本,拿到html的字符串进行编译即可

举个例子:

代码语言:javascript复制
<head>
    <script src="./vue.global.js"></script>
</head>
<div id="app">
    <div>
        {{message}}
    </div>
</div>
<script>
    const app = Vue.createApp({
        setup() {
            const { ref } = Vue
            const message = ref('hello world')
            return {
                message
            }
        }
    })
    app.mount('#app')
</script>Ï
<body>
</body>

以上代码他最后就会在vue.global.js的加持下解析 idapp的字符串模板

这也是vue能够在行业内屹立不倒的原因,小而美,上手简单,开箱即用。

而反观react,相信干过的都知道,你想要使用他的语法,光引入一个js 文件那是远远不够的!

而他的实现原理也非常简单,仅仅在初始化的时候将模板内容拿到,然后调用 中的@vue/compiler 执行编译即可!

由于我们这期编译原理为主,运行时我们暂时按下不表

我们来看源代码:

代码语言:javascript复制
import { compile } from '@vue/compiler-dom'
// 初始化编译函数
function compileToFunction(
  template: string | HTMLElement,
  options?: CompilerOptions
): RenderFunction {
  // 判断了是否是字符串,因为在初始化的时候,可以使用字符
  if (!isString(template)) {
    if (template.nodeType) {
      template = template.innerHTML
    } else {
      __DEV__ &amp;&amp; warn(`invalid template option: `, template)
      return NOOP
    }
  }

  const key = template
  // 作者还机制的使用了缓存,如果已经编译过了,就直接返回
  const cached = compileCache[key]
  if (cached) {
    return cached
  }
  // 开始根据id拿到模板
  if (template[0] === '#') {
    const el = document.querySelector(template)
    if (__DEV__ &amp;&amp; !el) {
      warn(`Template element not found or is empty: ${template}`)
    }
    // 此处已经拿到模板了
    template = el ? el.innerHTML : ``
  }

  // 拿到编译后的代码
  const { code } = compile(template)

  const render = (
    __GLOBAL__ ? new Function(code)() : new Function('Vue', code)(runtimeDom)
  ) as RenderFunction
  // 生成render 函数
  return (compileCache[key] = render)
}

以上vue3的源码中,我们就能清楚的看出来,是在初始化的时候,引用模板编译模块,来生成render 函数。

这个render函数相信大家都不陌生,毕竟面试常考,我也就不再赘述

接下来,就是sfc单文件组件编译

sfc单文件组件编译

上期我们说过sfc单文件组件,他从本质上来说,就是只适用于vue的一种规范,既然,是适用于vue规范,那么必然不行业公认的,于是他就需要转义,给他变成浏览器能跑起来的代码

而编译sfc单文件组件,就需要node环境,因为node 能做文件io操作!

使用上其实很简单,我们利用node读取vue单文件组件,然后将其中内容,分开编译输出,打包为浏览器可以运行的代码!

然而,在这个前端纷繁复杂生态繁荣的年代!我们干事情千万不要从0开始,我们要从1到10,我们要站在巨人的肩膀上!

众所周知,在前端基建领域的巨人,非webpack莫属!

他就能实现我们要做的所有事情,我们只需要付出少量心血写个插件即可

于是Vue Loader诞生了!

Vue Loader

Vue Loader 在上回书,我们也说道过, 是一个 webpack 的 loader,它允许你以一种名为单文件组件 (SFCs)的格式撰写 Vue 组件

简而言之,Vue Loader 在webpack的基础上建立了灵活且极其强大的前端工作流,来帮助撰你写 Vue.js 应用。

他的使用方式非常简单,在webpack中配置即可

代码语言:javascript复制
// webpack.config.js
const VueLoaderPlugin = require('vue-loader/lib/plugin')

module.exports = {
  module: {
    rules: [
      // ... 其它规则
      {
        test: /.vue$/,
        loader: 'vue-loader'
      }
    ]
  },
  plugins: [
  
     // 这个插件使 .vue 中的各类语言块匹配相应的规则
    new VueLoaderPlugin()
  ]
}

而在开箱即用的vue-cli中直接内置了,我么你甚至都不需要引用!下载相关脚手架即可开始开发!

webpack 中的Vue Loader插件到底做了什么事情呢?

一图胜千言,但还是简单的说一下吧!

总体上来说,Vue Loader 对于.vue文件的处理分为那么几步关键点:

  • 1、通过 parse方法 生成 descriptor描述文件,描述符中包含了vue解析后的各个结果,比如template、style、script
  • 2、处理过后的type 区别并缓存内容提高编译性能
  • 3、配合compiler-sfc生成 code代码

如上图所示,我们可以简单的看下他编译后的代码!

那么接下下来我们来探究一下vue-loader的原理了,细致的探究一下他是怎么实现的。

代码语言:javascript复制
// 默认导出的loader函数注意loader本质上就是个函数
export default function loader(
  this: webpack.loader.LoaderContext,// webpack的loader上下文
  source: string// 源码
) {
  const loaderContext = this
  //拿到上下文中的相关内容
  const {
    mode,
    target,
    sourceMap,
    rootContext,
    resourcePath,
    resourceQuery: _resourceQuery = '',
  } = loaderContext
  //一些前置内容的处理,比如loaderUtils获取配置对象,传入参数处理等,不是我们本次关心的重点
  const rawQuery = _resourceQuery.slice(1)
  const incomingQuery = qs.parse(rawQuery)
  const resourceQuery = rawQuery ? `&amp;${rawQuery}` : ''
  const options = (loaderUtils.getOptions(loaderContext) ||
    {}) as VueLoaderOptions

  const isServer = options.isServerBuild ?? target === 'node'
  const isProduction =
    mode === 'production' || process.env.NODE_ENV === 'production'

  const filename = resourcePath.replace(/?.*$/, '')
  // 通过vue/compiler-sfc 分离内容
  const { descriptor, errors } = parse(source, {
    filename,
    sourceMap,
  })

  const asCustomElement =
    typeof options.customElement === 'boolean'
      ? options.customElement
      : (options.customElement || /.ce.vue$/).test(filename)

  // 缓存当前编译内容,防止下次编译
  setDescriptor(filename, descriptor)


  // 作用域CSS和热重载的处理,生成唯一id
  const rawShortFilePath = path
    .relative(rootContext || process.cwd(), filename)
    .replace(/^(..[/\]) /, '')
  const shortFilePath = rawShortFilePath.replace(/\/g, '/')
  const id = hash(
    isProduction
      ? shortFilePath   'n'   source.replace(/rn/g, 'n')
      : shortFilePath
  )
  //vue-loader 推导策略
  // 这里主要就是通过vue插件来处理编译分离后的内容
  // 主要就是生成引用的js、render函数,css等内容
  //比如'?vue&amp;type=script&amp;lang=js' 就会走js 的处理逻辑
  // 分别通过插件styleInlineLoader,stylePostLoader。templateLoader 来处理
  if (incomingQuery.type) {
    return selectBlock(
      descriptor,
      id,
      options,
      loaderContext,
      incomingQuery,
      !!options.appendExtension
    )
  }

  // 前置处理css scoped 
  const hasScoped = descriptor.styles.some((s) => s.scoped)
  const needsHotReload =
    !isServer &amp;&amp;
    !isProduction &amp;&amp;
    !!(descriptor.script || descriptor.scriptSetup || descriptor.template) &amp;&amp;
    options.hotReload !== false

  const propsToAttach: [string, string][] = []

  // 处理script
  let scriptImport = `const script = {}`
  let isTS = false
  const { script, scriptSetup } = descriptor
  if (script || scriptSetup) {
    const lang = script?.lang || scriptSetup?.lang
    isTS = !!(lang &amp;&amp; /tsx?/.test(lang))
    const src = (script &amp;&amp; !scriptSetup &amp;&amp; script.src) || resourcePath
    const attrsQuery = attrsToQuery((scriptSetup || script)!.attrs, 'js')
    //拼接下次请求的query
    const query = `?vue&amp;type=script${attrsQuery}${resourceQuery}`
    const scriptRequest = stringifyRequest(src   query)
    // 生成代码
    scriptImport =
      `import script from ${scriptRequest}n`  
      // support named exports
      `export * from ${scriptRequest}`
  }

  // 处理模板template
  let templateImport = ``
  let templateRequest
  const renderFnName = isServer ? `ssrRender` : `render`
  const useInlineTemplate = canInlineTemplate(descriptor, isProduction)
  if (descriptor.template &amp;&amp; !useInlineTemplate) {
    const src = descriptor.template.src || resourcePath
    const idQuery = `&amp;id=${id}`
    const scopedQuery = hasScoped ? `&amp;scoped=true` : ``
    const attrsQuery = attrsToQuery(descriptor.template.attrs)
    const tsQuery =
      options.enableTsInTemplate !== false &amp;&amp; isTS ? `&amp;ts=true` : ``
    // 同样的处理模板内容
    const query = `?vue&amp;type=template${idQuery}${scopedQuery}${tsQuery}${attrsQuery}${resourceQuery}`
    templateRequest = stringifyRequest(src   query)
    // 生成代码 
    templateImport = `import { ${renderFnName} } from ${templateRequest}`
    propsToAttach.push([renderFnName, renderFnName])
  }

  // 处理styles内容
  let stylesCode = ``
  let hasCSSModules = false
  const nonWhitespaceRE = /S /
  if (descriptor.styles.length) {
    descriptor.styles
      .filter((style) => style.src || nonWhitespaceRE.test(style.content))
      .forEach((style, i) => {
        const src = style.src || resourcePath
        const attrsQuery = attrsToQuery(style.attrs, 'css')
        const idQuery = !style.src || style.scoped ? `&amp;id=${id}` : ``
        const inlineQuery = asCustomElement ? `&amp;inline` : ``
        const query = `?vue&amp;type=style&amp;index=${i}${idQuery}${inlineQuery}${attrsQuery}${resourceQuery}`
        const styleRequest = stringifyRequest(src   query)
        if (style.module) {
          if (asCustomElement) {
            loaderContext.emitError(
              `<style module> is not supported in custom element mode.`
            )
          }
          if (!hasCSSModules) {
            stylesCode  = `nconst cssModules = {}`
            propsToAttach.push([`__cssModules`, `cssModules`])
            hasCSSModules = true
          }
          // 如果有热更新,拼接添加css 代码 添加热更新等内容
          stylesCode  = genCSSModulesCode(
            id,
            i,
            styleRequest,
            style.module,
            needsHotReload
          )
        } else {
          // 否则直接拼接
          if (asCustomElement) {
            stylesCode  = `nimport _style_${i} from ${styleRequest}`
          } else {
            stylesCode  = `nimport ${styleRequest}`
          }
        }
        // TODO SSR critical CSS collection
      })
  }

  let code = [templateImport, scriptImport, stylesCode]
    .filter(Boolean)
    .join('n')

  // attach scope Id for runtime use
  if (hasScoped) {
    propsToAttach.push([`__scopeId`, `"data-v-${id}"`])
  }

  // 拼接处最后的代码段
  if (!propsToAttach.length) {
    code  = `nnconst __exports__ = script;`
  } else {
    code  = `nnimport exportComponent from ${exportHelperPath}`
    code  = `nconst __exports__ = /*#__PURE__*/exportComponent(script, [${propsToAttach
      .map(([key, val]) => `['${key}',${val}]`)
      .join(',')}])`
  }

  //生成代码最终返回
  code  = `nnexport default __exports__`
  return code
}

他的步骤其实本质上其实就是在开发环境下来拼接生成esmodule代码, 然后代码就会拼接成如下这样:

当然这只是第一步,因为你发现他又引入了单独拆分后的文件。 接下来,我们就要对每个单独拆分后的类型文件做处理,此时的处理就要依赖于vue-loader这个包中的一个webpack插件来做下一步。

VueLoaderPlugin

VueLoaderPlugin,他的源代码非常简单,主要就是兼容了webpack4webpack5

代码语言:javascript复制
import webpack = require('webpack')
declare class VueLoaderPlugin implements webpack.Plugin {
  static NS: string
  apply(compiler: webpack.Compiler): void
}

let Plugin: typeof VueLoaderPlugin
// 兼容webpack4和webpack5
if (webpack.version &amp;&amp; webpack.version[0] > '4') {

  Plugin = require('./pluginWebpack5').default
} else {

  Plugin = require('./pluginWebpack4').default
}

export default Plugin

接下来,我们就以webpack5为例讲讲这个插件怎么处理剩余的内容。

至于为啥将webpack5,就跟买东西一样啊,买新不加旧!Ï众所周知,webpack插件本质上是个class类

那我们只需要看看这个类里面干了什么事情即可

pluginWebpack5

我们之前说了 pluginWebpack5本质上是个类,这个类由于能拿到webpack编译的参数,于是,他便可以动态的改变他的配置对象,从而注入新的loader来实现拆分后文件的解析,这也是我们引入插件后就能解析内容的原理

代码如下:

代码语言:javascript复制
class VueLoaderPlugin implements Plugin {
  static NS = NS

  apply(compiler: Compiler) {
    //拿到编译之后的一些模块
    const normalModule = compiler.webpack.NormalModule || NormalModule

    //相当于做一些出错误处理,日志输出啥的
    compiler.hooks.compilation.tap(id, (compilation) => {
      normalModule
        .getCompilationHooks(compilation)
        .loader.tap(id, (loaderContext: any) => {
          loaderContext[NS] = true
        })
    })
    // 此处省略无关紧要的一些代码
    //...
    //...
    // 开始注册编译模板loader
    const templateCompilerRule = {
      loader: require.resolve('./templateLoader'),
      resourceQuery: (query?: string) => {
        if (!query) {
          return false
        }
        const parsed = qs.parse(query.slice(1))
        return parsed.vue != null &amp;&amp; parsed.type === 'template'
      },
      options: vueLoaderOptions,
    }


    //pitcher注册除了模板之外的剩余内容
    const pitcher = {
      loader: require.resolve('./pitcher'),
      resourceQuery: (query?: string) => {
      
        if (!query) {
          return false
        } 
        // 解析 query 上带有 vue 标识的资源
        const parsed = qs.parse(query.slice(1))
        return parsed.vue != null
      },
    }

    // 重写loader规则以便能够解析vue文件剩余内容
    compiler.options.module!.rules = [
      pitcher,
      templateCompilerRule,
       ...rules,
    ]
  }
}

以上简写代码中,我们能很清楚的看到,他重写了rules 也就是之前那个webpack的配置表

接下来就水到渠成了,由于vue-loader返回了拼接后的文件,那么他就会去处理拼接后的文件,也就是我们前面那张截图

然后就会根据正则规则触发那两个新的loader 从而实现编译

接下来我们也来简单介绍一下这两个loader

templateLoader

代码语言:javascript复制
// 模板的处理其实就是调用vue/compiler-sfc的compileTemplate方法
const TemplateLoader: webpack.loader.Loader = function (source, inMap) {
  source = String(source)
  const loaderContext = this
  // 前置处理
  const options = (loaderUtils.getOptions(loaderContext) ||
    {}) as VueLoaderOptions

  const isServer = options.isServerBuild ?? loaderContext.target === 'node'
  const isProd =
    loaderContext.mode === 'production' || process.env.NODE_ENV === 'production'
  const query = qs.parse(loaderContext.resourceQuery.slice(1))
  const scopeId = query.id as string
  const descriptor = getDescriptor(loaderContext.resourcePath)
  const script = resolveScript(
    descriptor,
    query.id as string,
    options,
    loaderContext
  )

  let templateCompiler: TemplateCompiler | undefined
  if (typeof options.compiler === 'string') {
    templateCompiler = require(options.compiler)
  } else {
    templateCompiler = options.compiler
  }
  // 主要就是这里,调用vue/compiler-sfc的compileTemplate方法
  const compiled = compileTemplate({
    source,
    filename: loaderContext.resourcePath,
    inMap,
    id: scopeId,
    scoped: !!query.scoped,
    slotted: descriptor.slotted,
    isProd,
    ssr: isServer,
    ssrCssVars: descriptor.cssVars,
    compiler: templateCompiler,
    compilerOptions: {
      ...options.compilerOptions,
      scopeId: query.scoped ? `data-v-${scopeId}` : undefined,
      bindingMetadata: script ? script.bindings : undefined,
      ...resolveTemplateTSOptions(descriptor, options),
    },
    transformAssetUrls: options.transformAssetUrls || true,
  })

  // tips
  if (compiled.tips.length) {
    compiled.tips.forEach((tip) => {
      loaderContext.emitWarning(tip)
    })
  }
  // 返回结果,让下一个loader处理
  const { code, map } = compiled
  loaderContext.callback(null, code, map)
}

而编译后的结果在babelsourceMap的加持下变成了这样

我们可以很清楚的看到render函数

pitcher

pitcher本质上就是处理除了模板以外的情况

代码语言:javascript复制
// 处理css 内容,js 内容可以用babel 处理
const stylePostLoaderPath = require.resolve('./stylePostLoader')
const styleInlineLoaderPath = require.resolve('./styleInlineLoader')
// pitcher-loader 是个空壳子
const pitcher: webpack.loader.Loader = (code) => code
// pitcher这个loader 中的 pitch 方法才是真正的pitcher
//loader 总是从右到左被调用。有些情况下,loader 只关心 request 后面的 元数据(metadata),
//并且忽略前一个 loader 的结果。在实际(从右到左)执行 loader 之前,会先从左到右调用 loader 上的 pitch 方法。
export const pitch = function () {
  const context = this as webpack.loader.LoaderContext
  const rawLoaders = context.loaders.filter(isNotPitcher)
  let loaders = rawLoaders

  if (loaders.some(isNullLoader)) {
    return
  }
  // 接受参数
  const query = qs.parse(context.resourceQuery.slice(1))
  // 省略无用代码
  //......
  // 处理css 内容
  if (query.type === `style`) {
    const cssLoaderIndex = loaders.findIndex(isCSSLoader)
    if (cssLoaderIndex > -1) {
      const afterLoaders =
        query.inline != null
          ? [styleInlineLoaderPath]
          : loaders.slice(0, cssLoaderIndex   1)
      const beforeLoaders = loaders.slice(cssLoaderIndex   1)
      return genProxyModule(
        [...afterLoaders, stylePostLoaderPath, ...beforeLoaders],
        context,
        !!query.module || query.inline != null
      )
    }
  }
  // 处理其他情况,最后将生成代码再次放到下一个loader 中处理
  return genProxyModule(loaders, context, query.type !== 'template')
}

到这我们就能很清楚的理解他loader对于整个vue文件的解析了。

总体上来说,他就是解析了内容之后,生成目标代码,再通过,别的loader 去解析处理,最终形成浏览器可以使用的代码

好了,说到这,我们整个vue模板在node端,和weback 的加持下,算是解析完成了。

接下来就轮到我们的沙箱了

沙箱中的vue 编译流程

在我们的浏览器中,由于没有io操作,以及webpack的加持,我们将这笨重的webpack移植到浏览器上,略显费劲。

于是在大佬们的不断探索下,他们换了个思路,我们可以在浏览器端实现一个类似loader的东西,来转换代码不就行了吗。

node vue-loader不就是干这个用的吗?

在开始之前我们可以从结果以及目的,来倒推过程和写法!

我们知道,我们的目的,就是将一个vue模板代码 变成浏览器可执行代码然后通过eval 来执行?

那我们构造一个可以通过eval执行的函数不就可以了吗

以上代码其实就是我们要构建的结果,只不过和node环境不同的是,我们需要生成一个函数来整体执行。

这样一来,我们就能确定我们生成代码需要什么基础设施了babel@vue/compiler-sfcscss预处理器即可

这样一来他的原理就呼之欲出了,我们只需要有相应的实现然后执行即可。

上回书只说到了vue3的编译内容,如有兴趣传送门在此

这一回,我们雨露均沾

我们先说怎么编译vue模板

说起编译vue模板 我们还是仿照上一部分的步骤

  • 1、loader处理
  • 2、@vue/compiler-sfc 编译
  • 3、babel 编译,处理css
  • 4、eval执行

loader处理

这一块其实很简单,我们只需要拿到模板code 代码然后调用loader处理即可!

代码语言:javascript复制
   // 有个函数相当于rules 匹配文件名,模仿webpack配置
    mapTransformers(module: Module): Array<[string, any]> {
        // 碰见js文件,就用babel转换
        if (/^(?!/node_modules/).*.(((m|c)?jsx?)|tsx)$/.test(module.filepath)) {
            return [
                [
                    'babel-transformer',
                    {
                        presets: ['solid'],
                        plugins: ['solid-refresh/babel'],
                    },
                ],
            ];
        }

        if (/.css$/.test(module.filepath)) {
            return [
                ['css-transformer', {}],
                ['style-transformer', {}],
            ];
        }
        //碰见vue文件,就用vue3-loader
        if (/.vue$/.test(module.filepath)) {
            return [
                ['vue3-transformer', {}],
            ];
        }
        //碰见图片,就用url-loader
        if (/.(png|jpeg|svg)$/.test(module.filepath)) {
            return [
                ['url-transformer', {}],
            ];
        }
        throw new Error(`No transformer for ${module.filepath}`);
    }

@vue/compiler-sfc 编译

这一步我们在上回书说道,如有兴趣传送门在此,我们不再赘述!

babel 编译,处理css

由于 compiler-sfc处理之后,是esmodule内容,所以我们还需要用在浏览器端的babel做一层转换

代码如下:

代码语言:javascript复制
import * as babel from '@babel/standalone';
//使用babel 编译代码
export async function babelTransform({ code, filepath, config }: ITransformData): Promise<any> {
    const requires: Set<string> = new Set();
    const presets = await getPresets(config?.presets ?? []);
    const plugins = await getPlugins(config?.plugins ?? []);
    plugins.push(collectDependencies(requires));
    // 传入一些配置,进行babel 转义
  
    const transformed = babel.transform(code, {
        filename: filepath,
        presets,
        plugins,

        ast: false,
        sourceMaps: 'inline',
        compact: /node_modules/.test(filepath),
    });
   
    if (!transformed.code) {
        transformed.code = 'module.exports = {};';
    }
    // 返回编译结果,并且拿到依赖包名字
    return Promise.resolve({
        code: transformed.code,
        dependencies: requires,
    })
}

注意这里的babel是浏览器端专用的,大家可以去babel官网自行翻阅!

scss的代码处理,自不用多说,在上回书也说道了,只需要用sass.js这个包即可

eval执行

拿到编译后的代码之后,我们就可以执行代码了!

代码语言:javascript复制
export default function (
  code: string,
  require: Function,
  context: { id: string; exports: any; hot?: any },
  env: Object = {},
  globals: Object = {}
) {
  const global = g;
  const process = {
    env: {
      NODE_ENV: 'development',
    },
  }; // buildProcess(env);
  // @ts-ignore
  g.global = global;
  // 构建函数中使用的变量!
  // 这里需要注意的是,我们之所以需要构建变量,是为了模拟nodejs中的require等方法,
  //因为babel 
  //所以我们需要在浏览器端模拟这些方法,来使程序跑起来
  const allGlobals: { [key: string]: any } = {
    require,
    module: context,
    exports: context.exports,
    process,
    global,
    swcHelpers,
    ...globals,
  };

  if (hasGlobalDeclaration.test(code)) {
    delete allGlobals.global;
  }

  const allGlobalKeys = Object.keys(allGlobals);
  const globalsCode = allGlobalKeys.length ? allGlobalKeys.join(', ') : '';
  const globalsValues = allGlobalKeys.map((k) => allGlobals[k]);
  try {
    // 构建函数
    const newCode = `(function $csb$eval(`   globalsCode   `) {`   code   `n})`;

    // 执行函数
    (0, eval)(newCode).apply(allGlobals.global, globalsValues);

    return context.exports;
  } catch (err) {
    logger.error(err);
    logger.error(code);

    let error = err;
    if (typeof err === 'string') {
      error = new Error(err);
    }
    // @ts-ignore
    error.isEvalError = true;

    throw error;
  }
}

最后

ok,到这里,我们就算是基本的讲了一个在线IDE的沙箱编译的基本原理流程,当然,整个项目要想跑起来,需要的知识点还有很多,篇幅有限,我们今天先到这里!

后续如果还有下回书,我们继续讲react的编译,以及怎样内置依赖包等能力!

JYM支持,让咱们还有下一回,今天这一回咱们打完收工,领盒饭去了!

0 人点赞