├── .gitignore ├── Dockerfile ├── README.md ├── cdn.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── server.js ├── src ├── App.css ├── App.js ├── App.test.js ├── baseCode.js ├── icon │ ├── browser.png │ ├── css.png │ ├── dot.png │ ├── github.png │ ├── js.png │ └── loading.png ├── index.css ├── index.js └── serviceWorker.js ├── views ├── error.ejs └── index.ejs └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 设置基础镜像,如果本地没有该镜像,会从Docker.io服务器pull镜像 2 | FROM node:8.16.2-alpine 3 | ARG OSS_KEY_SECRET 4 | # 设置时区 5 | RUN apk --update add tzdata \ 6 | && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 7 | && echo "Asia/Shanghai" > /etc/timezone \ 8 | && apk del tzdata 9 | # 创建app目录 10 | RUN mkdir -p /usr/src/node-app 11 | # 设置工作目录 12 | WORKDIR /usr/src/node-app 13 | # 拷贝package.json文件到工作目录 14 | # !!重要:package.json需要单独添加。 15 | # Docker在构建镜像的时候,是一层一层构建的,仅当这一层有变化时,重新构建对应的层。 16 | # 如果package.json和源代码一起添加到镜像,则每次修改源码都需要重新安装npm模块,这样木有必要。 17 | # 所以,正确的顺序是: 添加package.json;安装npm模块;添加源代码。 18 | COPY package.json /usr/src/node-app/package.json 19 | # 安装npm依赖(使用淘宝的镜像源) 20 | # 如果使用的境外服务器,无需使用淘宝的镜像源,即改为`RUN npm i`。 21 | RUN npm i --registry=https://registry.npm.taobao.org 22 | 23 | COPY . /usr/src/node-app 24 | 25 | RUN npm run build 26 | 27 | RUN node cdn.js $OSS_KEY_SECRET 28 | 29 | EXPOSE 3001 30 | 31 | ENTRYPOINT ["node", "server.js"] 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 近几年前端发展迅速,很多概念性的想法渐渐地变成了现实,比如前端微服务、Serverless 在前端的应用、在线IDE编码等,越来越多“工程”级别的项目选择使用 Web 浏览器作为宿主平台,前端发展可谓如日中天,本篇文章选择在线IDE作为切入点,介绍其实现原理,并且动手写一个简易版本的 React 在线IDE。 4 | 5 | ## CodeSandBox 6 | 7 | [CodeSandBox](https://codesandbox.io/) 是目前为止最为强大的在线 IDE 工具之一,他实现了 90% 的本地前端 IDE 工具的功能,以下是比较令人惊叹的几个特性: 8 | 9 | 1. 完全浏览器在线打包,实现了可在浏览器运行的 webpack 工具。 10 | 2. 可使用 npm 包 11 | 3. 支持文件系统 12 | 4. 可离线使用 13 | 5. 可在线发布应用 14 | 15 | 是的,你没有看错,这一切都是发生在浏览器上,更为惊叹的是,CodeSanBox 是个人项目,并不是由专业的公司团队开发完成。其强大功能证明了,浏览器能够做的事情是无穷无尽的,下面我将简单讲解其结构以及实现方案。 16 | 17 | ![image](http://static4.vince.xin/WeChate987f08313e53d34abc458997a652daf.png) 18 | 19 | 其主要结构分为以下几个部分: 20 | 21 | - Editor: 编辑器,这里集成了 VsCode,包含了大部分桌面版 VsCode 的主要功能(自动填充、光标提示、快捷键等),当文件变动时,会通知 SandBox 进行编译。 22 | - SandBox:这部分是 CodeSandBox 最核心的部分,它负责代码的转译,也就是最核心的 Webpack 在浏览器上的打包实现方案,用户编写的代码与用户所使用到的 npm 包源码,注入到转译的 Complier 中,转译完成的代码会注入 Iframe 中预览。 23 | - Packager:包管理器,相当于浏览器版本的 npm、yarn 包管理器。 24 | - Iframe:最后面是用来预览项目的内嵌 Iframe。 25 | 26 | 看似简单的实现方案,但是由于 CodeSandBox 的强大功能,因此其实现过程是非常复杂且具有挑战性的,因为浏览器并没有 Node 环境,并且 webpack 、npm、babel 对浏览器端使用并不是完全覆盖,甚至只是提供最基本的功能,还有需要克服的文件系统,这一切都需要大量的精力与时间,非常佩服与赞叹其作者,假如你想知道其具体的实现方案,以下资料推荐给你: 27 | 28 | - 国内首篇讲解:[CodeSandbox 浏览器端的webpack是如何工作的? 上篇](https://juejin.im/post/5d1e0dea51882514bf5bedfa#heading-1) 29 | - 作者的博客: [Ives van Hoorne](https://hackernoon.com/@compuives) 30 | - 项目开源地址:[codesandbox](https://github.com/codesandbox) 31 | 32 | ## 简易IDE需求 33 | 34 | 简单分析完 CodeSandBox 的实现方案,我们也能试着动手做一个简单的在线 IDE,我们需要实现以下基础功能: 35 | 36 | 1. 可编写 ES6、JSX 的 React 代码并预览 37 | 2. 可编写 CSS 38 | 3. 可保存源码数据 39 | 4. 可发布应用 40 | 41 | 这里,我放出我完成的项目,读者有兴趣可以进入下面地址试玩一下:[Online Editor](http://online.vince.xin/) 42 | 43 | 项目源码: [react-online-editor](https://github.com/Vincedream/react-online-editor) 44 | 45 | ## 实现方案 46 | 47 | 完成需求分析后,我列出了在我完成该 IDE 过程中给你遇到几个棘手的问题: 48 | 49 | 1. 在浏览器端如何转译 ES6、JSX 代码? 50 | 2. 如何实现预览功能 51 | 3. 如何完成编辑器的样式 52 | 4. 需要保证用户编写的代码数据持久化 53 | 5. 即时发布应用 54 | 55 | ### 总览分析 56 | 57 | 在解决上述问题,我们先对 Online Editor 的结构做一个讲解: 58 | 59 | ![image](http://static4.vince.xin/WeChat10f42ef16c117e1e72dd14a76599d0b0.png) 60 | 61 | 这是项目主要的结构,首先,用户编写代码后,Cmd/Ctrl + S 保存代码,触发 @babel/standalone 转译,这里将 ES6、JSX 的代码转译成 ES5 的可执行代码以及可可执行的 CSS 代码,注入到浏览器内嵌的 Iframe 中,使得 Iframe 刷新重新运行新的代码,同时,这一步我们会将用户编写的 Js、Css 代码以字符串的方式以一个唯一的 uuid 作为标识存入到 OSS 中,只要用户持有当前的 url,遍能访问到之前写的代码,这里我们便解决了上述的第1、2、4个问题。 62 | 63 | 用户编写完后,需要将其当前页面发布到线上环境,供其他人访问,这里我们看下面的分析结构图: 64 | 65 | ![image](http://static4.vince.xin/WeChatc0a10bf2c12d97c4517fc91605caaa92.png) 66 | 67 | 当我们点击发布按钮时,将转译后的 js 与 Css 代码存入到 OSS 中,以 ` 151 | 152 | 153 | 154 | 155 |
loading...
156 | 157 | 158 | 159 | ``` 160 | 161 | 我们需要注意的是,这里预置了 `react` 、 `react-dom`,这里只需要将转译后的业务代码注入即可。 162 | 163 | 完整的源码: [react-online-editor](https://github.com/Vincedream/react-online-editor) 164 | 165 | ## 接口 Serverless 化 166 | 167 | 这里我们用到了上传源码文件的接口,为了避免 OSS 的密钥直接暴露在前端,这里我做了一个接口在后端进行统一处理,后端接收到文件名和文件字符串后,会生成相应的文件,然后传入到 OSS 中,由于其不具备依赖性、用完即走的特性,我将其接口直接写在了阿里云的云函数中,也就是我们说的 **Serverless**,可实现随用随调、可承受高并发、按需收费等优良特性。 168 | 169 | ## TODO 170 | 171 | 通过上面的讲解,我们完成了一个非常简配的在线 IDE ,实现了非常基础的功能,当然还是有非常多不足的地方,也是接下来需要优化的 TodoList: 172 | 173 | - 通过分析源码的 AST ,来支持浏览器的文件引用 174 | - 通过 unpkg、Systemjs 等方案实现可引入 npm 包功能 175 | - 实现 Css 的预处理功能(Scss、less...) 176 | 177 | ## 总结 178 | 179 | 本篇博文并没有讲解很高深的知识,只是给了读者一种实现 IDE 方案,希望能够给到读者一些启示,实现一个完整的浏览器 IDE 工程量是巨大的,虽然如此,我们还是有必要去了解其基本的实现方案,对将来遇到的需求也不会一脸懵的尴尬情况。 180 | 181 | **项目源码**: [react-online-editor](https://github.com/Vincedream/react-online-editor) 182 | 183 | **Refs**:[CodeSandbox 浏览器端的webpack是如何工作的? 上篇](https://juejin.im/post/5d1e0dea51882514bf5bedfa#heading-1) 184 | -------------------------------------------------------------------------------- /cdn.js: -------------------------------------------------------------------------------- 1 | const OSS = require('ali-oss'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | 5 | const OSS_KEY_SECRET = process.argv.splice(2)[0]; 6 | 7 | const client = new OSS({ 8 | region: 'oss-cn-beijing', 9 | accessKeyId: 'LTAIzLWlLvM1A0bd', 10 | accessKeySecret: OSS_KEY_SECRET, 11 | bucket: 'officespace2', 12 | }); 13 | 14 | const jsDirPath = path.resolve(__dirname, './build/static/js') 15 | const cssDirPath = path.resolve(__dirname, './build/static/css') 16 | const OSS_URL = 'http://officespace2.oss-cn-beijing.aliyuncs.com/'; 17 | 18 | const findJsFile = () => { 19 | return new Promise((resolve, reject) => { 20 | fs.readdir(jsDirPath, (err, files) => { 21 | if (err) { 22 | reject(err); 23 | } else { 24 | const fileList = []; 25 | files.forEach(fileName => { 26 | const fileExtension = fileName.split('.').pop(); 27 | if (fileExtension === 'js') { 28 | fileList.push(`/static/js/${fileName}`); 29 | } 30 | }) 31 | resolve(fileList) 32 | } 33 | }) 34 | }) 35 | } 36 | 37 | const findCssFile = () => { 38 | return new Promise((resolve, reject) => { 39 | fs.readdir(cssDirPath, (err, files) => { 40 | if (err) { 41 | reject(err); 42 | } else { 43 | const fileList = []; 44 | files.forEach(fileName => { 45 | const fileExtension = fileName.split('.').pop(); 46 | if (fileExtension === 'css') { 47 | fileList.push(`/static/css/${fileName}`); 48 | } 49 | }) 50 | resolve(fileList) 51 | } 52 | }) 53 | }) 54 | } 55 | 56 | const findAllNeedUploadFileList = () => { 57 | return Promise.all([findJsFile(), findCssFile()]) 58 | } 59 | 60 | const htmlPath = path.resolve(__dirname, `./build/index.html`); 61 | 62 | findAllNeedUploadFileList().then(res => { 63 | let originFileList = [...res[0], ...res[1]] 64 | const upLoadRequestList = []; 65 | originFileList.forEach(filePath => { 66 | const fileName = filePath.split('/').pop(); 67 | const filePathResolve = path.resolve(__dirname, `./build/.${filePath}`) 68 | console.log(fileName) 69 | console.log(filePath) 70 | console.log(filePathResolve) 71 | upLoadRequestList.push(client.put(fileName, filePathResolve)) 72 | }) 73 | 74 | Promise.all(upLoadRequestList).then(res => { 75 | console.log(res); 76 | const htmlData = fs.readFileSync(htmlPath, 'utf8') 77 | let changeTtmlData = htmlData; 78 | originFileList.forEach(item => { 79 | if (htmlData.indexOf(item) !== -1) { 80 | console.log('888') 81 | changeTtmlData = changeTtmlData.replace(item, `${OSS_URL}${item.split('/').pop()}`) 82 | } 83 | }) 84 | console.log(changeTtmlData) 85 | fs.writeFile(htmlPath, changeTtmlData, 'utf8', function (err) { 86 | if (err) { 87 | console.log(err) 88 | } else { 89 | console.log('wancheng') 90 | } 91 | }); 92 | }) 93 | }) 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-online-editor", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@babel/standalone": "^7.7.1", 7 | "ali-oss": "^6.1.1", 8 | "axios": "^0.19.0", 9 | "classnames": "^2.2.6", 10 | "codemirror": "^5.49.2", 11 | "compression": "^1.7.4", 12 | "ejs": "^2.7.1", 13 | "express": "^4.17.1", 14 | "lodash": "^4.17.15", 15 | "qs": "^6.9.1", 16 | "react": "^16.11.0", 17 | "react-codemirror2": "^6.0.0", 18 | "react-dom": "^16.11.0", 19 | "react-scripts": "3.2.0" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": "react-app" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vincedream/react-online-editor/189c65ee87d926c3b71353ec09cef701e797e03a/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Online Editor 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vincedream/react-online-editor/189c65ee87d926c3b71353ec09cef701e797e03a/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vincedream/react-online-editor/189c65ee87d926c3b71353ec09cef701e797e03a/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); //express框架模块 2 | const path = require('path'); //系统路径模块 3 | const app = express(); 4 | const compression = require('compression'); 5 | 6 | const port = 3001; //端口 7 | 8 | app.use(compression()); 9 | app.use(express.static(path.join(__dirname, 'build'))); //指定静态文件目录 10 | // view engine setup 11 | app.set('views', path.join(__dirname, 'views')); 12 | app.set('view engine', 'ejs'); 13 | 14 | app.get('/share/:pageId', (req, res, next) => { 15 | console.log(req.params.pageId) 16 | res.render('index', {pageId: req.params.pageId}) 17 | }) 18 | 19 | app.listen(port, function() { 20 | console.log(`服务器运行在http://:${port}`); 21 | }); -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | overflow: hidden; 3 | } 4 | .app-page { 5 | display: flex; 6 | height: 100vh; 7 | width: 100vw; 8 | overflow: hidden; 9 | } 10 | .code-layout { 11 | width: 50vw; 12 | height: 100vh; 13 | display: flex; 14 | flex-direction: column; 15 | } 16 | 17 | .tab-wrap { 18 | position: relative; 19 | display: flex; 20 | width: 100%; 21 | height: 36px; 22 | background: rgb(38, 38, 38); 23 | color: #fff; 24 | } 25 | 26 | .tab-item { 27 | display: flex; 28 | width: 145px; 29 | line-height: 36px; 30 | font-size: 14px; 31 | box-sizing: border-box; 32 | padding: 0 10px; 33 | align-items: center; 34 | background: rgb(68, 68, 68); 35 | cursor: pointer; 36 | } 37 | 38 | .tab-item--selected { 39 | background: rgb(30, 30, 30); 40 | } 41 | 42 | .tab-icon { 43 | width: 12px; 44 | height: 12px; 45 | margin-right: 5px; 46 | } 47 | 48 | .dot-icon { 49 | width: 36px; 50 | height: 36px; 51 | margin-left: 25px; 52 | } 53 | 54 | .reload-tips { 55 | position: absolute; 56 | right: 5px; 57 | top: 10px; 58 | font-size: 13px; 59 | color: #b5b5b5; 60 | } 61 | 62 | .code-wrap { 63 | flex: 1; 64 | overflow: scroll; 65 | } 66 | .code-container > .CodeMirror, 67 | .code-container{ 68 | width: 100%; 69 | height: 100%; 70 | font-size: 14px; 71 | } 72 | 73 | .browser-layout { 74 | display: flex; 75 | width: 50vw; 76 | height: 100vh; 77 | flex-direction: column; 78 | } 79 | 80 | .browser-header { 81 | display: flex; 82 | height: 36px; 83 | width: 100%; 84 | background: rgb(38, 38, 38); 85 | color: #fff; 86 | align-items: center; 87 | padding: 0 5px; 88 | box-sizing: border-box; 89 | } 90 | 91 | .browser-icon { 92 | width: 22px; 93 | height: 22px; 94 | margin-right: 8px; 95 | } 96 | 97 | .github-icon { 98 | width: 22px; 99 | height: 22px; 100 | margin-left: 12px; 101 | margin-right: 8px; 102 | cursor: pointer; 103 | opacity: 0.7; 104 | transition: all 0.1s ease-in-out; 105 | vertical-align: bottom; 106 | } 107 | 108 | .github-icon:hover { 109 | opacity: 1; 110 | } 111 | 112 | .url-container { 113 | position: relative; 114 | flex: 1; 115 | height: 28px; 116 | line-height: 26px; 117 | background: rgb(85, 85, 85); 118 | border-radius: 4px; 119 | padding: 0 5px; 120 | font-size: 13px; 121 | margin-right: 8px; 122 | } 123 | 124 | .loading-icon { 125 | position: absolute; 126 | right: 5px; 127 | width: 26px; 128 | height: 26px; 129 | animation: donut-spin 1.2s linear infinite; 130 | } 131 | 132 | .share-button { 133 | height: 28px; 134 | padding: 0 15px; 135 | line-height: 28px; 136 | cursor: pointer; 137 | background: rgb(85, 85, 85); 138 | font-size: 13px; 139 | font-weight: 600; 140 | border-radius: 2px; 141 | } 142 | .share-button:hover { 143 | background: #6b6b6b; 144 | } 145 | 146 | .browser-wrap { 147 | flex: 1; 148 | width: 100%; 149 | overflow: scroll; 150 | } 151 | 152 | .preview-browser { 153 | border: 0; 154 | width: 100%; 155 | height: 100%; 156 | } 157 | 158 | @keyframes donut-spin { 159 | 0% { 160 | transform: rotate(0deg); 161 | } 162 | 100% { 163 | transform: rotate(360deg); 164 | } 165 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Controlled as CodeMirror} from 'react-codemirror2'; 3 | import axios from 'axios'; 4 | import classnames from 'classnames'; 5 | import * as Babel from '@babel/standalone'; 6 | import qs from 'qs'; 7 | import { baseScriptCode, baseJsxCode, baseCssCode } from './baseCode'; 8 | import jsIcon from './icon/js.png'; 9 | import cssIcon from './icon/css.png'; 10 | import browserIcon from './icon/browser.png'; 11 | import loadingIcon from './icon/loading.png'; 12 | import dotIcon from './icon/dot.png'; 13 | import githubIcon from './icon/github.png'; 14 | 15 | import './App.css'; 16 | require('codemirror/lib/codemirror.css'); 17 | require('codemirror/theme/material.css'); 18 | require('codemirror/mode/javascript/javascript.js'); 19 | require('codemirror/mode/jsx/jsx.js'); 20 | require('codemirror/mode/css/css.js'); 21 | 22 | 23 | const getUUid = () => Number(Math.random().toString().substr(2, 5) + Date.now()).toString(36); 24 | const getEditId = () => qs.parse(window.location.search.slice(1)).editId || null; 25 | 26 | const uploadFileToOSSUrl = 'https://1556981199176880.cn-shanghai.fc.aliyuncs.com/2016-08-15/proxy/react-online-edit/createFileAndUploadToOSS/'; 27 | 28 | const jsxCodeTransform = (input) => { 29 | return Babel.transform(input, { presets: ['react', 'es2015'] }).code; 30 | }; 31 | 32 | const TabList = [ 33 | { 34 | key: 'jsxCode', 35 | iconImg: jsIcon, 36 | title: 'App.js' 37 | }, 38 | { 39 | key: 'cssCode', 40 | iconImg: cssIcon, 41 | title: 'App.css' 42 | }, 43 | ] 44 | 45 | class App extends React.Component { 46 | constructor(props) { 47 | super(props); 48 | this.state = { 49 | jsxCode: baseJsxCode, 50 | cssCode: baseCssCode, 51 | initTransFormCode: '', // 初始注入到 Iframe 的代码 52 | cssCodeSaved: true, 53 | jsxCodeSaved: true, 54 | tabSelected: TabList[0].key, // 55 | isPublished: false, // 是否已经发布过 56 | isPublishLoading: false, // 是否正在发布页面 57 | } 58 | } 59 | componentDidMount() { 60 | this.loadCodeFromOss(); 61 | } 62 | 63 | // 加载初始代码 64 | initRunCode = () => { 65 | const { jsxCode, cssCode } = this.state; 66 | const transFormJsxCode = this.transFormJsxCode(jsxCode); 67 | const initTransFormCode = ` 68 | 69 | ${baseScriptCode} 70 | 71 | `; 72 | this.setState({ 73 | initTransFormCode 74 | }); 75 | } 76 | 77 | // 获取远程源代码加载 78 | loadCodeFromOss = () => { 79 | const editId = getEditId(); 80 | // 当进入初始url时 81 | if (!editId) { 82 | const uuid = getUUid(); 83 | window.history.pushState(null, null, `?editId=${uuid}`); 84 | this.initRunCode(); 85 | } else { 86 | // 当 editId 不为空,请求数据 87 | axios({ 88 | method: 'get', 89 | url: `http://officespace2.oss-cn-beijing.aliyuncs.com/${editId}.json` 90 | }).then(res => { 91 | const { jsxCode, cssCode } = res.data; 92 | this.setState({ 93 | jsxCode, 94 | cssCode 95 | }, () => { 96 | this.initRunCode(); 97 | }) 98 | }).catch(() => { 99 | this.initRunCode(); 100 | }) 101 | } 102 | } 103 | 104 | // 将源代码(未经编译)传入OSS 105 | uploadOriginCodeToOss = () => { 106 | const editId = getEditId(); 107 | const { jsxCode, cssCode } = this.state; 108 | const fileName = `${editId}.json`; 109 | this.uploadFile(fileName, JSON.stringify({ 110 | jsxCode, 111 | cssCode 112 | })).then((res) => { 113 | console.log(res); 114 | }) 115 | } 116 | 117 | // 编译 Jsx 代码 118 | transFormJsxCode = (input) => { 119 | try { 120 | const outputCode = jsxCodeTransform(input); 121 | return outputCode 122 | } catch(e) { 123 | console.log(e); 124 | } 125 | } 126 | 127 | // 保存代码:1. 传入到iframe;2.保存到OSS 128 | handleSaveCode = (e, type) => { 129 | if (e.keyCode === 83 && (navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey)) { 130 | e.preventDefault(); 131 | this.postCodeToIframe(type); 132 | this.uploadOriginCodeToOss(); 133 | this.setState({ 134 | [`${type}Saved`]: true 135 | }) 136 | } 137 | } 138 | 139 | // 绑定 input 140 | handleInputCode = (value, type) => { 141 | this.setState({ 142 | [type]: value, 143 | [`${type}Saved`]: false 144 | }) 145 | } 146 | 147 | // 将 js、css 传入 Iframe 中 148 | postCodeToIframe = (type) => { 149 | const { cssCode, jsxCode } = this.state; 150 | const transFormJsxCode = this.transFormJsxCode(jsxCode); 151 | if (type === 'jsxCode') { 152 | document.getElementById("preview").contentWindow.postMessage({ 153 | type: 'jsxCode', 154 | content: transFormJsxCode 155 | }, "*"); 156 | } else if (type === 'cssCode') { 157 | document.getElementById("preview").contentWindow.postMessage({ 158 | type: 'cssCode', 159 | content: cssCode 160 | }, "*"); 161 | } 162 | } 163 | 164 | // 发布页面 165 | handleSharePage = () => { 166 | this.setState({ 167 | isPublishLoading: true 168 | }) 169 | const editId = getEditId(); 170 | const filePreName = `${editId}`; 171 | const { cssCode, jsxCode } = this.state; 172 | const transformJsxCode = this.transFormJsxCode(jsxCode); 173 | const sharePageUrl = `${window.location.origin}/share/${editId}`; 174 | Promise.all([this.uploadFile(`${filePreName}.js`, transformJsxCode), this.uploadFile(`${filePreName}.css`, cssCode)]).then(res => { 175 | this.setState({ 176 | isPublished: true, 177 | isPublishLoading: false 178 | }); 179 | const clickTarget = document.createElement('a'); 180 | clickTarget.id="clickTarget"; 181 | clickTarget.target="_blank"; 182 | clickTarget.href=sharePageUrl; 183 | document.body.append(clickTarget) 184 | document.getElementById('clickTarget').click() 185 | }) 186 | } 187 | 188 | // 上传文件 189 | uploadFile = (fileName, fileContent) => { 190 | return axios({ 191 | method: 'post', 192 | url: uploadFileToOSSUrl, 193 | data: { 194 | fileName: fileName, 195 | content: fileContent 196 | } 197 | }) 198 | } 199 | 200 | handleChangeTab = (key) => { 201 | this.setState({ 202 | tabSelected: key 203 | }) 204 | } 205 | render() { 206 | const { isPublished, isPublishLoading, jsxCode, cssCode, tabSelected, cssCodeSaved, jsxCodeSaved, initTransFormCode } = this.state; 207 | const editId = getEditId(); 208 | const sharePageUrl = `${window.location.origin}/share/${editId}`; 209 | return ( 210 |
211 |
212 |
213 | {TabList.map(item => ( 214 |
{ this.handleChangeTab(item.key) }} className={classnames("tab-item", { "tab-item--selected": item.key === tabSelected})}> 215 | 216 | {item.title} 217 | {((item.key === 'jsxCode' && !jsxCodeSaved) || (item.key === 'cssCode' && !cssCodeSaved)) && } 218 |
219 | ))} 220 |
Tips: Cmd/Ctrl + S to reload
221 |
222 |
223 | {tabSelected === 'jsxCode' && { 232 | this.handleInputCode(value, 'jsxCode') 233 | }} 234 | onKeyDown={(editor, event) => { this.handleSaveCode(event, 'jsxCode') }} 235 | />} 236 | {tabSelected === 'cssCode' && { 245 | this.handleInputCode(value, 'cssCode') 246 | }} 247 | onKeyDown={(editor, event) => { this.handleSaveCode(event, 'cssCode') }} 248 | />} 249 |
250 |
251 |
252 |
253 | 254 |
255 | {isPublished ? sharePageUrl : 'http://localhost (click publish button to show url)'} 256 | {isPublishLoading && } 257 |
258 | 发布 259 | 260 |
261 |
262 |