Howdy
theme-lighttheme-dark

开发OnlineCodeEditor过程的一些记录

March 20, 2021

OnlineCodeEditor是笔者基于Vue3 + Typescript开发的一个类似Codepen的开源项目,本文记录一些项目中使用到技术及实现原理等。

Feature

✅ 纯前端项目静态部署(利用iframe与postMessage生成实时预览子页)

✅ 响应式布局,布局可弹性伸缩支持拖拽更改宽度、折叠。

✅ HTML/CSS Emmet技术,按Tab键快速生成代码

✅ 支持引入外部CDN样式和JS.

✅ 增加SCSS解析模块. (基于在线转换API:sassmeister.com)

主要原理

使用codemirror搭建HTML、CSS、JS三种代码块编译器,构建一个Iframe网页用于展示效果。然后利用Postmessage向Iframe传入代码数据并替换旧代码。同时支持传入jsCDN与CssCDN路径,也是利用新增或更新动态标签实现。注意为了防止一直更改dom浪费内存,我们可以使用debounce防抖等让其在代码停止编辑一定时间才进行刷新。

通过简单的改写html/css/js实现不刷新页面更新页面效果。

function loadPage (htmlCode, cssCode, jsCode) {
  const _html = document.querySelector('#customHTML')
  if (_html) document.body.removeChild(_html)
  const html = document.createElement('div')
  html.id = 'customHTML'
  html.innerHTML = htmlCode
  document.body.appendChild(html)

  const _css = document.querySelector('#customCSS')
  if (_css) document.head.removeChild(_css)
  const css = document.createElement('style')
  css.id = 'customCSS'
  css.innerHTML = cssCode
  document.head.appendChild(css)

  const _script = document.querySelector('#customJS')
  if (_script) document.body.removeChild(_script)
  const script = document.createElement('script')
  script.id = 'customJS'
  script.innerHTML = jsCode
  document.body.appendChild(script)
}

当然有一些场景是需要手动刷新页面的,例如:添加JS监听器、更新CDN路径等。这时为iframe更新时间戳,就可以实现页面刷新。

async function sendMessage(refresh = false) {
  if (refresh) {
    state.iframeURL.value = `./iframe.html?t=${+new Date()}`
    await new Promise((resolve) => {
      setTimeout(() => {
        resolve(1)
      }, 2000)
    })
  }
  ...
}

Emmet

emmet技术,可以让开发者速写html代码。官方文档参考:https://docs.emmet.io/

emmet有提供npm包,可以让我们在浏览器中使用其API。

import expand from 'emmet'
// 传入word与模式即可获取到速写后的代码
const result = expand(word, { type: mode }) 

同时需要注意,在编写代码时,我们需要提取出速写前的单词,我们可以以空格为界限进行切割,可参考以下函数

function getWord (line: string, ch: number): [string, number] {
  const getNearTagChar = (str: string): string => {
    for (let i = str.length - 1; i > 0; i--) {
      if (str[i] === '>' || str[i] === '<') return str[i]
    }
    return str[0] || '<'
  }
  // 光标位于行末或单词末尾
  if (ch === line.length || (line.length > ch + 1 && (/\s/.test(line[ch]) || line[ch] === '<'))) {
    let i
    for (i = ch - 1; i >= 0; i--) {
      if (/\s/.test(line[i]) || (line[i] === '>' && getNearTagChar(line.slice(0, i)) === '<')) {
        break
      }
    }
    return [line.slice(i + 1, ch), i + 1]
  }
  return ['', 0]
}

当我们输入完关键词后,按Tab键时可快速匹配到相应代码块,当前支持htmlcss的速写。

Emmet

支持编写SASS代码

项目中支持切换到SCSS预编译器编写CSS。

由于SASS是一般基于node-sassdart-sass,这两个可以通过某些技术让其在浏览器中运行,但是占用的文件大小会较大,所以此次仅采用了线上API实现。

线上SASS转CSS地址: sassmeister.com

export async function scss2css (scss) {
  try {
    const res = await fetch('https://api.sassmeister.com/compile', {
      method: 'POST',
      body: JSON.stringify({
        input: scss,
        outputStyle: 'expanded',
        syntax: 'SCSS',
        compiler: 'dart-sass/1.26.11'
      }),
      headers: {
        'Content-Type': 'application/json'
      }
    })
    if (res.status === 200) {
      return await res.json()
    } else {
      const json = await res.json()
      return Promise.reject(json)
    }
  } catch (e) {
    return Promise.reject(e)
  }
}

Webpack配置相关

  • 由于生成iframe页,项目变成了多页面应用,需要设置2个入口
  • 定义全局配色scss变量,为了减去手动引入,使用css-loader时采用了全局注入的方式
  • 配置CDN路径减轻网站带宽压力(但由于codemirror内部结构问题,这次只将css文件放在了CDN)
// vue.config.js
const isProduction = process.env.NODE_ENV === 'production'
const assetsCDN = {
  css: [
    'https://cdn.bootcdn.net/ajax/libs/codemirror/5.58.1/codemirror.min.css',
    'https://cdn.bootcdn.net/ajax/libs/codemirror/5.58.1/theme/material-darker.min.css'
  ],
  js: []
}
const isHashMode = process.env.VUE_APP_ROUTER_MODE === 'hash'
const publicPath = isHashMode ? './' : '/coder'
module.exports = {
  pages: {    index: 'src/main.ts',    iframe: 'src/iframe.ts'  },
  chainWebpack: config => {
    config.plugin('html-index').tap(args => {
      args[0].cdn = assetsCDN
      return args
    })
  },
  css: {
    loaderOptions: {
      scss: {        prependData: '@import "~@/assets/variable.scss";'      }
    }
  },
  productionSourceMap: !isProduction,
  publicPath
}

兼容手机模式(响应式设计)

项目中代码编辑器布局在PC端中支持自定义拉伸与折叠,使用了笔者开源插件@howdyjs/resize

实现响应式设计一般采用CSS3的媒体查询实现,但由于布局问题,手机端下不适用拉伸布局,需要额外添加一个手机端下的标签页,所以这次需要使用js辅助实现。

响应式

利用Github Workflows实现自动构建

由于该项目是纯前端项目,打包后直接部署就行,所以很适合使用github page部署。

使用Github workflows可以构建任务,让其在代码push时自动执行打包,并部署到github page分支。

目前已经社区已经有很多成熟的常用任务,不需要自己编写。这次主要用到了github-pages-deploy-action可以直接帮我们把代码发布到github page分支。

# .github/workflows/main.deploy.yml
name: Deploy Doc Website
on:
  push:
    branches:
      - main
jobs:
  main-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
        with:
          persist-credentials: false
      - name: Setup node
        uses: actions/setup-node@v2
      - name: Install dependencies
        run: yarn
      - name: Build Demo
        run: yarn build:hash
      - name: Deploy
        uses: JamesIves/github-pages-deploy-action@3.7.1
        with:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          BRANCH: gh-pages
          FOLDER: dist

需要注意github page目前不支持history的路由模式

Todo

😴 Javascript Babel模式
😴 引入账号系统同步代码
😴 线上代码展示模式


to-top