基于webpack打造前端在线编译器

需求

公司内部的 UI 组件使用示例一直是仅仅以 markDown 格式展示代码,在比较复杂的组件中示例代码就会非常冗长,难以使用文件目录的结构来展示代码。所以该项目一直以来就有这样的一个需求:

  • 组件的示例代码能够按照一定的目录结构组织与展示
  • 用户能够修改示例代码并实时查看其效果

首先分析需求:两者综合起来就是一个针对前端开发者的在线开发平台,类似于 codepen,只是我们需要更好的将其集成到当前的网站中来。

为什么我们还需要自己实现这样的工具?

现在的工具已经不少了,再比如webpackbincodeSandBoxjsfiddlejsbin等,除了嵌入模式的支持度不是很好,最大的问题是我们的组件尚未开源,也就是编译的过程需要我们自己控制,在这一点上就决定了我们要自行开发。

确定主要思路

丑陋的交互图

默认只能展示代码,并且可以切换文件。当点击编辑按钮之后编辑器变为可写模式,同时生成唯一的链接保存与展示用户代码,cmd/ctrl+S操作与点击save按钮都能触发编译过程,并且会异步的将最新代码保存到数据库中。

技术方案

整体的数据流动如上图所示,整个开发过程感触较深的有以下几点:

  • 为什么需要管理前端状态?
  • 在哪儿存储用户的临时文件以能更高效地实现编译过程?
  • 错误处理:编译时的错误要能够展示到网页
  • 如何根据当前浏览的组件页面确定代码区的数据获取?
  • ...

为什么需要管理前端状态

在最初搭建基本环境的时候,我是没有将这一部分直接加进项目中的。开发时,能实现需求时肯定是越简单越好。可是后来我发现,如果想要在全局得到/共享/监听某个状态,像当前页面是否可编辑这个状态,就影响着右上角的按钮显示edit还是'save', 编辑区是否可以读写。当前页面是否有数据,没有就需要去指定接口拉取。就要自己去实现,比如事件机制,或者发布订阅模式,实现不同组件间的通信。可是当需要监听的状态多了以后,用于监听的代码就散落在各个组件中,自己有时也想不清该去哪个组件中修改了。

这开发的时候都蒙圈,后边维护修改就更难了,看来需要一个工具来帮我们集中管理页面的状态。

mobx VS redux

因为以前了解过 redux,最先想到的就是 redux,但是当想要动手时,一想到要写 action, reducer 还要将组件与状态连接,将相应的状态注入到组件中,实在是太麻烦了。正巧,搜了一下除了 redux之外的其他工具,发现了 mobx, 语法很简洁,然后翻阅了他的文档,发现用起来也比较简单,基于订阅的模式来修改影响到的节点的更新机制也很高效。(PS:具体的使用网上资源众多,在此就略过了)

如果直接说 mobx就比 redux 好用,我想这样是不负责任的,任何脱离了使用场景来谈技术的都是耍流氓。 那么这两者的差别是什么呢?redux基于不可变数据,通过定义action与纯函数的reducer使得状态是可预测的,适合大项目,尤其是多人协作开发,结合devtools很容易就能知道数据的流向,相对繁琐的流程就是为了限制或者约束一些非法的行为,可以说redux是在开发效率与维护成本之间达成了一个比较好的权衡。而mobx基于可变数据(也可以结合不可变数据,如mobx-state-tree),很像一个功能完备的观察者工具,很适合小项目或者是不同页面只有较少共用数据的情况,当然mobx最大的优势就是不用像redux那样写一堆繁琐的模板代码以及无需再为了异步请求引入一堆中间件。和现实世界一样,不同规模的组织,肯定有着不同的管理方式。所以当我们进行这种类似的技术选择的时候,如果在一个不大的项目,使用一个工具用起来很累,很大概率是选择错了。

总结:所谓的状态管理,其实就是为了在如今高度模块化、组件化的应用中,实现组件间简单方便的通信。倘若不同页面(容器)中有很多共用数据,可能单一store与数据流向清晰的redux依然会是你的最爱。但我建议在引入redux之前,都要再问一句“我真的需要redux吗?”

在哪儿存储用户的临时文件以能更高效地实现编译过程?

每次用户修改代码触发保存操作(cmd+S or click 'save' button )时,都会将代码提交,保存到临时文件夹中,然后开始编译流程。每次提交的代码都存到硬盘?太慢了,尤其是多个用户同时编辑时,想一想这个流程,一个用户编辑代码,创建一个专属的文件夹,用户提交代码,后台收到请求,创建文件并写入内容,编译,又生成2个文件(html,bundle.js),再次写入硬盘,然后程序读取内容发送回页面,涉及了多次对硬盘的读写操作,而所有的操作都受限于硬盘的读写速度。速度慢,开销大。

如果曾经有一个优秀的方法出现过,那么其他的方法就都成了将就。

想一下 webpack-dev-server 是怎么做的吧,入口文件从本地读取,但是编译结果是放到内存中的,因为我们每次的保存操作都会触发一次编译,会不断的生成一些临时的打包文件,将这些文件放入内存中,有以下好处:

  • 基于内存比从硬盘读写快,本地单一用户可能区别不是很明显,但是部署上线之后多用户同时操作时差别就会比较大了。
  • 也没有必要添加.gitignore,当应用挂掉重启时没有东西需要清理。
  • 也不用太担心内存不够用,一个上百 M 的文件可能在哪儿都不适合。

那么我们的需求就是 entries(用户提交的代码文件)和 output 基于内存,但是 loaders 还是从本地也就是 node_modules 里找。

发现 webpack提供了 custom file system ,那么根据我们的需求配置如下:

compiler.inputFileSystem = memoryFs;
compiler.outputFileSystem = memoryFs;
compiler.resolvers.normal.fileSystem = memoryFs;
//context 需要和inputFileSystem保持一致
compiler.resolvers.context.fileSystem =fs;
compiler.resolvers.loader.fileSystem =fs;

但是发现,loader 仍然是从 memory-fs 中找,那么当然是找不到 node_modules的内容。而node_modules文件很大,在机器配置有限的情况下,不建议将其也放到内存中,所以当前的问题是待编译文件存于内存,但是依赖项都在物理硬盘中。经过一番探索,发现一个至少能解决问题的答案:重写 memory-fs 中的读文件的方法,当找不到时就去物理硬盘上去找,参考这里

当仅仅是基于 react 的组件进行编译时,速度还可以接受 不到2s 即可完成编译,但是当要编译基于我们公司内部的组件时,时间就要爆炸。每次都要10+s,这还是我们在把所有的第三方包提前打到了 dll 文件中的情况下。那么为什么会这么慢呢,我们的包中依赖太多了,在编译时需要查找依赖时就要经历从 memory-fs 到physical disk 的切换,在一个组件编译过程中,要经历这种文件系统的切换达5700多次!但是对此我们却难以解决,或许可以搞清在不断切换文件系统的过程中,程序到底是在找什么,或许通过提前帮助它进行路径选择,会缩短一些时间。但是,我们是不会考虑完备所有的情况的,而且对于路径的决策也只能是点到为止。为此,我们需要换一个思路。

在我一筹莫展的时候,一个同学提到了 redis,想一想它也不能满足我们的需求,因为为了满足 webpack 的编译我们才要把用户的内容写到文件中的,所以只能是存储文件,但是,redis 给了我思路。为什么提起缓存他就想起了 redis?它基于内存实现数据缓存的,速度极快。要知道,基于内存对文件进行读写要比 SSD 还快上数百甚至上千倍。所以该项目的重点是实现基于内存读写!为什么因为我见到了 webpack基于 js 实现的 memeory-fs 我就抓着不放手了呢? 思路打开之后,开始浏览各种内存相对的文章,最终 linux 自带的 tmpfs 成了我的目标,基于此我们可以十分简单的直接利用系统提供的内存系统进行读写操作。

所以最终我的解决方案是: 基于 linux系统的 tmpfs 对用户的临时文件进行读写,只要将 entry 和 output 设为该 tmpfs 所在的路径即可,并且webpack 对此无感知。唯一可能需要注意的就是:在配置 webpack 时,尤其是 options 中的插件时,需要使用 require.resolve('')的形式,否则会去entry 设置的路径中查找,当然肯定也会找不到。比如对于 babel:

module:{
    rules:[
        {
            test: /\.(jsx|js)$/,
            exclude: /node_modules/,
            use: [{
                loader:'babel-loader?cacheDirectory=true',
                options: {
                    presets: ['babel-preset-es2015', 'babel-preset-react','babel-preset-stage-0'].map(require.resolve),
                    plugins:[[require.resolve('babel-plugin-transform-react-jsx'),{
                      pragma: "require('react').createElement"
                     }]]
                }
            }]
        }
    ]
}

Tips:
tmpfs 是虚拟内存文件系统,存储空间在 VM,由 real memory(RM) 和 swap 组成。RM 就是物理内存,swap 是通过硬盘虚拟出来的内存空间,读写速度相对 RM 要慢很多。当没有足够的 RM 时就会把 RM 里不常用的一下数据交换到 Swap 中去,重新使用时再次交换到 RM 中。增加 Swap 交换分区。tmpfs 的配置大小只是最多占用空间,实际占用空间是根据使用情况动态调整的。tmpfs 默认是 RM的一半。1、直接挂载到需要的目录: mount -t tmpfs -o size=500m tmpfs /tmp。2、写入/etc/fstab,这样重启后也有效。

tmpfs 只支持 linux,在 mac上没有直接的命令可用,但是聪明的程序员们早已实现了‘曲线救国’的壮举

错误处理:编译时的错误要能够展示到网页

继续我们的编译流程,当用户提交代码后,我们将其存储到临时文件夹,开始编译,并将编译结果作为用户提交代码的 response,编译正确时会生成 html 文件,我们直接将其返回给用户即可,当编译过程发生错误呢?此时程序不应该挂掉,并且应该像返回正常结果一样,返回一个包含错误信息的 html。(基本代码如下,有删减)
关于 webpack 中的 compiler 模块,可见官网介绍

//在 webpack的编译过程中,最终返回一个 promise 对象
return new Promise(function(resolve, reject) {
  compiler.run(function(err, stats) {
    if (err || stats.hasErrors()) {
      resolve({
        assets: stats.compilation.errors,
        hasError: true
      });
    } else {
      resolve({
        assets: stats.compilation.assets,
        hasError: false
      });
    }
  });
});

因为编译错误时返回的只是错误信息,而不是 html,为了能够正确的显示到 playground 的预览区,我们为其添加 html 模板:

export default function generateErrorTemplate(err) {
    const strToHtml = str => {
        return (str || "")
            .replace(/&/g, "&")
            .replace(/</g, "<")
            .replace(/>/g, ">")
            .replace(/"/g, """)
            .replace(/'/g, "'")
            .replace(/\[(\d+)m/g, "")
            .replace(/ /g, " ")
            .replace(/\n/g, "<br />");
    };
    let template = `
            <!DOCTYPE html> 
            <html>
            <head>
            </head>
            <body>
              <div>
                ${strToHtml(err.toString()) || ""}
              </div>
            </body>
            </html>`;
    return template;
}

在 routerHandler 中: response.html = generateErrorTemplate(err) ;

运行时的错误展示

当编译的资源返回前端之后,也会有运行时的错误,通常这些错误只会在Console(Chrome中的调试工具)中显示。但是作为一个前端代码的编译器,我们应当提供能够展示运行时错误的能力,否则出现runtime error时,展示区仍处于loading 的状态而无任何显示,会使人疑惑是否是源码尚未编译完成。

尚未捕获的error通常使用两种event listener:

  • 给window 添加error事件来监听未捕获的错误

    window.addEventListener('error', function(err) {
      displayErrorMsg(err.error.stack)
    });
    
  • 给window添加unhandledrejection 事件来监听promise中未捕获的错误

    window.addEventListener('unhandledrejection', function(err){
      displayErrorMsg(err.reason.stack)
    })
    

从报错信息中抽取关键词进行展示:

function displayErrorMsg(error){
  const lines = error.split('\n');
  let formatError;
  if (lines.length>2) {
    const errorLocation = lines[1].replace(/\(.*\)/,"").trim();
    formatError = `${lines[0]} ${errorLocation}`;
  } else {
    formatError = error;
  }
  preview.innerHTML = `<div style="color: red;">${formatError}</div>`;
  // stop the loading status
  window.parent.postMessage("compileEnd", window.location.origin);
};

如何根据当前浏览的组件页面确定代码区的数据获取?

本次的任务主要是实现嵌入模式下的展示,所以更多的精力将放到将 playground 与现有网站的集成中。不难想象,我们的 playground 必然是作为 iframe嵌入到主站中,但是在 playground 中如何才能根据主站打开的页面自动去获取相应的数据(即 代码)呢?

当在网站 A 中以 iframe 的形式打开网站 B 时,向网站 B 请求数据时的 request header 中会有referer 表明调用的网站网址,那么依据此信息,我们可以通过正则匹配获取到主站页面url 中的有用信息,然后再 B 网站中依据此参数获取相应的数据即可。(此处比较容易,不再代码展示)

update on 24 Jan 2018: referrer项在不同浏览器中的表现不同,在 Firefox 中通过 Hash改变路由时,浏览器不会更新 referrer 的信息,所以在 FireFox中妄图通过 referrer 来监听路由的变化很不靠谱,所以还是首先从页面路由中获取有效信息,然后作为参数传到 iframe.src 中去。

当多用户时,如何尽可能降低机器的内存占用?

前面提到,对于用户提交的代码我们将其保存在了 挂载到tmpfs 下的文件夹,也就是放到了内存中,基于内存的读写效率很高,当然其空间也是很宝贵的,我们应该及时有效地清理该文件夹中的无用数据。每个用户在进入不同组件页面修改代码时都会以 Unique id 为名创立文件夹,当该文件夹下的内容无活动时(理解为用户已离线,因为该系统为内部使用且目前没有加入用户管理系统),将该文件夹删除。因为项目中没有 添加session,所以设置定时器,在最后一次编译完成后的30分钟删除掉该文件夹。(PS: 不过也正因为此设计,在用户修改代码提交时我们必须是将文件夹全量提交,否则就会出现用户离开页面一段时间后再次提交代码时编译结果报错的问题。当然因为每个组件的示例代码最多也不过几个文件夹,测试中发现全量提交代码与增量提交已修改的代码文件对整个编译时间的影响最多也不会超过几十毫秒,这也全仰仗于基于内存的读写速度之快)。在设计删除任务时,要针对每个 id 单独记录其删除任务,最好通过建立 gcTask={} 的对象,以 unique id 为 key, 定时任务作为 value,在任务结果后通过delete gcTask[id] 删除掉该 key,以减轻全局对象 gcTask 的负担。

此外,在每个文件夹内,用户可能针对一个组件做出很多次保存的操作,也就是会生成多个编译结果文件bundle.[hash].js。对于此,我们在控制编译的 webpack.config.js 中最好添加clean-webpack-plugin,通过new CleanWebpackPlugin(`${tmpPath}/${id}/bundle.*.js`,cleanOptions)在每次编译前删除之前的编译结果。正如世界上没有十全十美,这样粗暴的删除文件,也会使得上次的编译结果不再缓存可用,比如只是修改了一处代码,编译之后报错,撤销修改,再次编译,此种情况下是应该可以直接利用上次的编译结果的,只是这种使用场景概率比较低,且影响较小。根据不同的情况做出权衡吧,用户多就选择尽力保证机器的内存空间能够得到有效释放,用户少的话就力求编译速度快就好了。基于我们的实际情况,并没有在每次编译时删除之前的编译结果,因为我们的用户少,且无操作30分钟后就将整个临时文件夹删除了。

如何更新预览区的内容?

因为在设计 playground 时没有把预览区单独拿出来作为一个应用,并且也没有设计其指向单独的页面。那么更新预览区的内容时就只能通过更新子 iframe 的方式。

refreshIframe(html) {
    let frame = document.querySelector("#preview-iframe");
    let iframe =
        frame.contentWindow ||
        frame.contentDocument.document ||
        frame.contentDocument;
    iframe = iframe.document;
    iframe.open("text/html");
    iframe.write(html);
    iframe.close();

这种更新方式都会带来一瞬间的闪烁,因为内容是先清空再重写。如果将 iframe 部分独立成一个应用,通过 postMessage 等方法接受到待更新的内容后,可以直接通过document.body.html=toPreviewHtml修改预览区的内容,并且不会造成闪烁。当前只能通过添加 loading 动画来美化预览区更新时因为页面空白带来的闪烁问题。

update on 24 Jan 2018: 在 stackoverflow 上看到有回答使用的是iframe.open("text/htmlreplace"),它在 chrome 上没有问题,但是在 Firefox 上会导致 iframe 内容无显示。所以要使用通用的标准iframe.open("text/html)

继续考虑:如果在代码中含有<a href="http://www.baidu.com">baidu</a>这种链接,在跳转之后,如何保证在浏览器后退到编译自己页面的时候还能显示内容?

简述一下之前的逻辑:

每次展示区展开时、用户行为(键盘保存事件/点击编译按钮)都触发 postCode 方法,让 top window 直接重写 iframe 内的 document。 前文已经提到这种模式带来的问题,即不能支持 history 的前进后退行为,每次从其他页面回来都会导致展示区空白。那么考虑:是不是在展示区页面每次 Init的时候我都去触发一次编译过程就好了呢?页面的每次 init 是包括了打开展示区面板和从其他页面跳转回来的情况的。现在的模式其实就只有“推”,不能满足我们的需求,而如果页面每次加载时能够主动拉取代码,那就可以支持任意链接的跳转了。

修改后的流程

display.html 的主动拉取代码过程涉及到 iframe 与 parent 的通信,postMessage可以很容易的解决这件事。
targetElement.postMessage(msg, targetOrigin)

但是倘若此时展示区已经跳转到了其他网站,用户触发编译行为时是无法通过 postMessage到达 display.html 的。此时,就需要我们在 postMessage 之前,去验证此时iframe内是否还是 display.html,如果不是,先将 iframe内的页面跳转回dispaly页面。window.myIframe.location.href = document.querySelector('#previewIframe').src。可以使用 window.myIframeName.location.href值来验证当前 iframe 内页面是否为本站的 dispaly.html,如果仍是在自己的网站,这个值是可以获得的,但如果当前 iframe 内是其他网站的页面,则由于同源策略会导致报错。所以此处使用 try,catch进行判断操作。可以说,在这点上的设计当前没有任何一个类似的网站(无论是 jsfiddle还是 codesandbox)做到。jsfiddle 是直接从缓存中获取历史版本页面,如果disable cache则失效;codesandbox则直接报错。(* 发现codesandbox现在也能正确响应浏览历史的前进/后退了,update at July 07 2018*)

let frameEle = document.querySelector("#preview-iframe");
let iframe =
    frameEle.contentWindow ||
    frameEle.contentDocument.document ||
    frameEle.contentDocument;

try{
    const iframeHref = window.myIframe.location.href;
}catch(err){
    window.myIframe.location.href = frameEle.src
}
postMessage(xxx,xxx)

Tips: querySelector('#iframeId') 与 window.iframeName 的区别

querySelector()/getElementById()都是 document 的方法;iframeName 是通过 window 来访问的。以下3种方式都能实现 iframe 的重新加载。

document.querySelector("#iframeId").src=`${new url}`
document.querySelector("#iframeId").contentWindow.location.href
window.iframeName.location.href=`${new url}'

但是 document.querySelector("#iframeId").src 通常是在我们引用 iframe 时就已经写好了的,window.iframeName.location.href是会随着 iframe内连接的跳转而改变的,正如前文所说,跳到其他网站之后在 iframe 外部是无法取到 iframe 内的location.href 的。

如何拥有保存/查找功能

这个系统是没有连接公司的用户系统的,那么我们如何实现用户能够保存代码并能查找到呢?

  • 另存为:用户可以对修改后的文件命名保存。为了便于查找自己保存过的文件,可以指定统一的命名格式:yourName / fileName;
  • 搜索:需要提供即时搜索,当我输入了 yourName 前几个字母时,基本就能列出我曾经保存过的文件列表来了。

推荐功能强大的 Algolia:它拥有支持各种编程语言的包,即时搜索功能极其强大,配合 autocomplete.js 使用完全可以实现我们的预期。

使用方法

  • 注册 algolia, 它会提供3种 KEY:ALGOLIA_ID, ALGOLIA_SEARCH_KEY, ALGOLIA_ADMIN_KEY
  • 初始化数据,将数据提交到 Algolia,此后才可利用其提供的云搜索能力。
    const yourIndex = client.initIndex('yourIndex');
    // data is commly got from db
    // should be Array type
    const data = getCodesFromDB();
    
    yourIndex.clearIndex(function(clearErr) {
      if (clearErr) {
        throw clearErr;
      }
      yourIndex.addObjects(data, function(addErr) {
        if (addErr) {
          throw addErr;
        }
        yourIndex.setSettings({
          searchableAttributes:['component'],
          customRanking: [ 'asc(order)' ],
          attributesToSnippet: [ 'content:8' ]
        }, function(settingsErr) {
          if (settingsErr) {
            throw settingsErr;
          }
          console.info(`Algolia index '${name}' reloaded with new data`);
        });
      });
    });
    
    关于 process.env中的变量,建议使用dotenv管理,它也提供了 webpack 插件,这样便可以将所有变量统一放到一个文件中管理,并同样可以在前端拿到 process.env 值。
  • 搜索。初始化,指定搜索的源。
    algoliaInit(ALGOLIA_ID,ALGOLIA_SEARCH_KEY,selector){
        const client = algoliasearch(ALGOLIA_ID, ALGOLIA_SEARCH_KEY);
        const index = client.initIndex('yourIndex');
        const _this=this;
        autocomplete(selector, { hint:true }, [
          {
            source:autocomplete.sources.hits(index, {hitsPerPage: 5}),
            displayKey:'yourAttribute',
            templates:{
              suggestion:function(suggestion){
                return suggestion._highlightResult.yourAttribute.value;
              }
            }
          }
        ]).on('autocomplete:selected',function(event,suggestion,dataset){
          // your action
        })
      }
      algoliaInit(ALGOLIA_ID, ALGOLIA_SEARCH_KEY, '#search-input');
      <input
        type="text"
        id="search-input"
        placeholder="Search your saved examples..."
      />
    

主要思路如上,详细的使用还需查看文档。

对集成时问题的补充

  • 初始化示例代码到数据库

    playground 中的数据都是从数据库中读取的。用户修改后的代码提交时直接保存到了数据库,组件页面打开时初始展示的代码呢?这些代码是 ui 组的同事为了帮助开发者了解组件使用写的示例代码,每次从网站打开组件的使用时都是默认要展示这些代码的,所以在网站启动时需要从 git 仓库中读取这些示例组件的代码并将其存到数据库。

  • 通过 webhook 自动更新示例代码

    当 UI 组的同事更新了组件的示例之后,playground 中的数据应该也自动得到及时的更新。通过webhook 实现对示例代码所在仓库的动态跟踪,在有关组件的示例代码更改后更新数据库中该组件的代码。

  • 将待编译项目的依赖文件单独配置

    因为嵌入的 playground 就是用来对公司内部组件进行编译的,为了防止这些组件的依赖与开发 playground 的依赖发生冲突,特将用于编译的依赖文件独立到一个文件夹下,防止在同一个 package.json 中因为版本不同带来的某些问题。比如我在 playground 中使用了 react-router-dom@4,但是公司前端组件中依赖的是 react-router@3。

  • 对 playground 中文件目录的实现独立成文:打造在线编译器 之 对文件目录的操作

  • 最终项目目录如下:

    .
    |--compile //对私有组件库的依赖
    |   |
    |   |--site-helper //some extra files to support the server, alias/getDependencies function
    |   |--package.json(extracted dependencies from spark-ui)
    |
    |--config //针对webpack的配置文件,主要有两套:1是项目本身,2是实时编译用户代码的配置
    |   |-- project_config 
    |   |   |-- webpack.common.config.js
    |   |   |-- webpack.dev.config.js
    |   |   |-- webpack.prd.config.js
    |   |   |-- webpack.dll.config.js
    |   |
    |   |-- compile_config 
    |        |-- webpack.config.js
    |        |-- webpack.dll.config.js
    |
    |--model //初始化数据库,定义数据模型,封装CRUD方法
    |--src 
    |   |--app
    |   |   |--components //各种组件
    |   |   |--pages //page级别的几个部分,用于组装最终页面
    |   |   |--app.js //layout, router, provider
    |   |   |--index.js //container & Hot Module entry
    |   |--embed
    |   |   |-- --app.js
    |   |   |-- --index.js
    |   |--display //效果展示页基础页面搭建&相关资源加载(html, js)
    |   |   |--preview.html //html 模板,已经引入了基本的css/js文件
    |   |   |--previewEntry.js //为用户react代码提供统一的render入口,封装render(<App />, document.getElementById('preview'));`,开启debug方法;
    |   |--store // 基于mobx的状态管理文件
    |
    |--utils //各种功能函数
    |   |-- getDirectoryTree.js //给定某个组件示例的path, 得到其代码树
    |   |-- saveCodeToDB.js // 接收path , 调用getDirectoryTree,对代码文件排序(文件在前,文件夹在后), save to DB
    |   |-- initializeExampleCodes.js //遍历所有examples, 调用saveCodeToDB,存到数据库
    |   |-- compileCode.js //控制编译流程,对待编译文件write To Memory(tmpfs), 以及webpack(config(uid))
    |   |-- generateErrorTemplate.js //错误美化
    |   |-- addAssetsToPreoview.js //postMessage事件监听相关js代码、preview要用到的css文件, ugligy, minify. 这些静态资源没有使用htmlWebpackPlugin在每次编译前插入到模板,而是项目部署时在相关资源构建完毕之后调用该方法,一次性地注入到模板中,节省了每次编译时调用的插件。
    |   |-- addCodesToAlgolia //要借助algolia实现即时搜索
    |
    |--public //打包文件的输出目录,serve静态资源
    |--app.js // server入口文件
    |--router.js //路由
    |--webhook.js // 再起一个server,通过gitlab的hook监听私有库的代码改动,及时更新playground中相关依赖。
    |--pm2.conf.js //以pm2启动时的配置文件
    |--package.json //所有的命令均在scripts串联
    |--其他无关文件//如 nodemon.json, postcss.config.js, .babelrc,  .env(.example) .eslintrc等
    

写在最后

这是很有意思的一个项目,后续还可以有很多工作可做,比如对该项目运行状态的监控(并发时的内存占用等)、数据库读写效率与空间占用、编译速度的进一步提升等。举个例子,为了提高编译用户代码时的速度,我放弃使用了html webpack plugin,而是转而使用统一的html模板,并预先把必须的assets(都已经打包成dll文件)加载了,当编译结果完成后不再需要处理编译后的hook事件(这儿特指生成html),而是直接将编译的单个js文件返回即可,减少了编译后的处理任务,也减小了资源下载量。

PS:该 playground 是基于 react+mobx+koa2+webpack3进行的开发,也提取出了一套个人感觉还算不错的脚手架:koa-react-scaffold 。其中对webpack的配置已经尽可能做了优化(包括dll,区分 dev与 prod 模式)、所有代码(包括 node 部分)都可以使用 ES6编写,也包含了针对 mongoose 的 crud 方法,如果喜欢,欢迎使用以及 star~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 1 Webpack 1.1 概念简介 1.1.1 WebPack是什么 1、一个打包工具 2、一个模块加载工具 3...
    Kevin_Junbaozi阅读 6,548评论 0 16
  • 在线阅读 http://interview.poetries.top[http://interview.poetr...
    程序员poetry阅读 113,837评论 24 450
  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,937评论 3 118
  • 我又来到了这里 灰色的天地, 只有地板,能映出我的身影。像镜子一样,映出我淡淡的忧伤。 我努力冲着地面挤出一抹干瘪...
    玄德_3dc3阅读 474评论 0 1
  • 雪还在掉着 你躺在雪地里 月光和着这雪把这里映的通明 我低下头,看着雪不着痕迹的撒在你脸上 亲爱的你啊,你告诉我 ...
    慕狱阅读 259评论 0 2