Before starting this article, let’s show you the js-Encoder, an online compiler I made myself:

Click here to preview

I came up with the idea of making an online compiler about three or four months ago. Before that, I had been working with a lot of online compilers, CodePen, JsBin, JsFiddle, etc., all of which were excellent compilers with a large user base.

I have strong interest for the realization of the online compiler, the online compiler support many kinds of language, color code, many shortcuts and some personalized Settings, this makes the online compiler looks and we download the compiler software locally also won’t have too big difference, I don’t know how to implement these complex function, So I looked at the CodePen and JsBin code and saw that they both used a tool called Codemirror.

codemirror

Codemirror is a versatile text editor for a JavaScript implementation for browsers. It is dedicated to editing code and comes with a number of language patterns and plug-ins for more advanced editing capabilities.

It turns out that these compilers rely on Codemirror, a tool so complex that it took me two days to get familiar with its configuration items. Codemirror itself is a direct manipulation of the DOM, but my project is built using Vue + Webpack, which violates Vue’s data-driven purpose, so I found vue-Codemirror on NPM. Build the code editor using Vue

Codemirror has many configuration items. I used the following configuration items in my own project. If you want to see all of them, you can see them here

cmOptions: {
        // codemirror config
        flattenSpans: false.// By default, CodeMirror merges two spans using the same class into one. Disable this feature by setting this item to false
        tabSize: 2.// TAB indents Spaces
        mode: ' './ / mode
        theme: 'monokai'./ / theme
        smartIndent: true.// Whether to indent intelligently
        lineNumbers: true.// Display the line number
        matchBrackets: true.// Match symbol
        lineWiseCopyCut: true.// If no text is selected during copying or clipping, the entire line where the cursor is located is automatically manipulated
        indentWithTabs: true.// Whether to replace n* TAB Spaces with N TAB characters in indentation
        electricChars: true.// Whether to re-indent input when it may change the current indent
        indentUnit: 2.// Indent unit, default 2
        autoCloseTags: true.// Automatically close the label
        autoCloseBrackets: true.// Automatically enter brackets
        foldGutter: true.// Allow line positions to collapse
        cursorHeight: 1.// Cursor height
        keyMap: 'sublime'.// Set of shortcut keys
        extraKeys: {
          'Ctrl-Alt': 'autocomplete'.'Ctrl-Q': cm= > {
            cm.foldCode(cm.getCursor())
          }
        }, // Intelligent prompt
        gutters: ['CodeMirror-linenumbers'.'CodeMirror-foldgutter'].// add extra gutter
        styleActiveLine: true // Activate the current row style
      },
Copy the code

These configurations are only a small part of what I want, but they are enough to do what I want

Mode indicates the language currently used by the editor

Theme refers to the color scheme used by the editor. There are many colors supported officially, but there is no color preview, so I used monokai which I am familiar with as the theme, because I prefer the color scheme of vscode, so I found the monokai.css file and modified many styles. It ended up being a bit different from the real vscode theme, but I did my best 😭

Keymap I set to sublime, and most of the shortcuts are available on Sublime

The rest of the configuration should have been explained in the comments, so I won’t explain it here

Codemirror works fine

With the Codemirror artifact, it’s safe to say that the hardest problems have been solved, but there are a lot of smaller problems that need to be solved

layout

There are a lot of references to JsBin in terms of layout, because I think the interface looks simple and comfortable

JsBin’s layout is aunt Jiang’s:

It is divided into five Windows. You can drag the mouse to change the size of the window by placing it on the boundary of two Windows

Dragging the mouse will increase the width of one window and decrease the width of the other, but the sum of the widths of the two Windows will not change

Here’s my idea:

The width of two adjacent Windows is obtained when the border is clicked, and the horizontal movement distance of the mouse is calculated when the mouse is dragged, and the width of the two Windows is increased or decreased accordingly

Since these five Windows are all sub-components of the same level, it is difficult for one window to obtain the width of another window, so I store the width of these five Windows in Vuex for use, and the width of each window changes with the width information in Vuex

Successful realization effect:

I set min-width: 100px; The style of the

In addition to the problem of two Windows, make sure that all window widths change with browser widths:

This effect is also easy to achieve by adding or subtracting the width of each window as the browser widths change/the number of Windows

Iframe

This was the first time I really touched iframe. It may be very simple, but I did put a lot of effort into it

I’ve solved the window drag problem, but it doesn’t work for iframe, and I’ve been confused and can’t figure out why, until it hits me:

Iframe is a separate new page, and events that are triggered outside of the IFrame do not affect the iframe itself. When I drag a border with the mouse, if the mouse enters the iframe, then the drag event is invalid. Therefore, it is necessary to add a transparent mask layer to the iframe before dragging, so that there will be no drag problems

When the user does not enter any characters for a period of time or directly clicks the run button, the HTML, CSS and JavaScript code in the editor should be put into the iframe, and the iframe will show the final effect, so the contents of the editor will be put into Vuex

compile

Codemirror can do a lot of things, but it doesn’t compile. Compilers like JsBin and CodePen don’t just support plain HTML, CSS, and JavaScript, they also support a lot of pre-processing languages for all three

For example, if I choose TypeScript as the preprocessing language, the compiler will need to convert TypeScript to JavaScript before passing it to iframe

Since js-encoder is a compiler with no background at all, it is necessary to introduce NPM packages and files of other pre-processing languages to compile. For example, in the implementation of Sass and Scss compilation, I introduce sass.js and sass.worker.js to compile:

async function compileSass(code) {
  // scss&sass
  if(! loadFiles.get('sass')) {
    const Sass = await require('./sass')
    Sass.setWorkerUrl('static/js/sass.worker.js')
    loadFiles.set('sass', Sass)
  }

  const defSass = loadFiles.get('sass')
  const sass = new defSass()
  
  return new Promise((resolve, reject) = > {
    sass.compile(code, result => {
      if (result.status === 0) resolve(result.text)
      else reject(new Error('fail to get result'))})})}Copy the code

LoadFiles is only used to determine whether or not these files have been imported. I see this compilation method in the official documentation

Js-encoder currently supports MarkDown, Sass, Scss, Less, Stylus, TypeScript and CoffeeScript. LiveScript and JSX(React) support will be considered later.

The console

Now that WE’ve covered HTML, CSS, JavaScript, and iframe, we’re left with the Console window

The Console window is used to display the contents of the iframe Console. If you want to display the contents on the page, you need to retrieve the contents when the user fires these methods. I override Console and window.onerror:

Note: The following js code is written inside the iframe

let consoleInfo = []

/ / rewrite the console
if (console) {
  const ableMethods = ['log'.'info'.'debug'.'warn'.'error']
  for (let item of ableMethods) {
    console[item] = function (data) {
      consoleInfo.push({ data, type: item })
    }
  }
}
/ / rewrite window. The error
window.onerror = function (msg, url, row, col) {
  consoleInfo.push({ data: { msg, url, row, col }, type: 'error' })
  return false // return false prevents errors from being reported in the browser console
}
Copy the code

The Console object is much more than these methods, and I’ve just overwritten some common ones

The consoleInfo for the iframe element is then retrieved from the component:

const consoleInfo = this.$refs.iframeBox.contentWindow.consoleInfo
Copy the code

Set up the

In addition to the selection of preprocessor languages, there are the following Settings in JS-Encoder

  • Delayed execution time
    • I’ve set up every editable windowwatchThe change of the monitoring value and the frequent input will lead to the frequent triggering of the method. Therefore, I set the anti-shake function, and the code will be executed only when the user does not input any characters within the set delay time
  • Convert a space the width of TAB to TAB
  • CDN
    • External can be addedCDN, this will be executedJavaScriptBefore I introduceCDN
  • CSS
    • External can be addedCSS, this will be executedCSSBefore you go throughlinkThe introduction of

shortcuts

Codemirror also supports keyboard shortcuts. We selected Sublime as our KeyMap configuration, which means that most of the keyboard shortcuts we use on Sublime are available in the online editor. On the website, You can see all of the supported shortcuts, except for the common Tab shortcut, because Codemirror only has indentation for the Tab key

You can use Tab to write HTML on many compilers:

This function comes from a tool called emmet, which is a very useful tool for codemirror, so I found the codemiror-emmet tool on NPM. Here is how it is used:

First import codemirror and codemiror-emmet

import CodeMirror from 'codemirror'
import codeMirrorEmmet from 'codemirror-emmet'
Copy the code

Codemirror -emMet returns a Promise object:

codeMirrorEmmet.then(emmet= > {
    emmet(CodeMirror) // Merge emmet functionality into codemirrorcmOptions.extraKeys = { ... cmOptions.extraKeys,// Since I already set the default extraKeys, I use the object extender to merge the previous configuration with TAB
      Tab: cm= > {
        if (cm.somethingSelected()) {// Select the text and press TAB to indent the entire text
          cm.indentSelection('add')}else if (cm.getOption('mode').indexOf('html') > - 1) {// When the current mode is HTML, execute the command emmetexpandprecedence
          try {
            cm.execCommand('emmetExpandAbbreviation')}catch (err) {
            console.error(err)
          }
        } else {// If the first two conditions are not met, press TAB to indent normally
          const spaces = Array(cm.getOption('indentUnit') + 1).join(' ')
          cm.replaceSelection(spaces, 'end'.'+input')}},Enter: 'emmetInsertLineBreak'
    }
    cmOptions.emmet = {// Configure the emmet entry
      markupSnippets: {
        'script:unpkg': 'script[src="https://unpkg.com/"]'.'script:jsd': 'script[src="https://cdn.jsdelivr.net/npm/"]'}}})Copy the code

That’s how it works on the picture

conclusion

Js-encoder has been officially developed for two months now. Due to academic reasons, there is not much time to invest in the development. At present, JS-Encoder is still a semi-finished product. In addition to some basic functions, there are still many functions that are not implemented or are being implemented. If you are interested, you can follow this project on Github. I’ll keep updating this post as more features are implemented.