不熟悉的朋友可能不知道,我叫老骥,前端切图仔,单位内卷,疯狂加班
最近几个月一直在跟在线IDE打交道,当然,高端一点咱也可以叫他低代码平台,毕竟这个词是流量密码
,因为听着高端,看着大气,闻着。。。额,没味
经过这几个月的摸索,只能感叹一句,就这。。。。
时至今日,我终于理解了,什么叫术业有专攻
,之所以,这个在线ide 显得高端,不是因为他难,而是干的人少,干这个的人, 只是在这个领域的时间长,有相关的工作经验而已,并不是他厉害,因为我们并不是创造者,我们属于仿写者,所有的思想,理念,结构是,都是别人的,我们仅仅理解改进而已。
真的要论起来,他跟写业务的区别并不大,说不好还没有写业务的复杂度高
不信?
那我就手摸手带您,解开他的神秘面纱,还原技术原理的本质
作为在线IDE
,就是在浏览器端的编辑器,属于比较新鲜的玩意儿,虽然在开发体验上,跟传统的IDE相差甚远,但是我相信,这个一定会是未来的趋势
相比传统IDE有以下劣势
基础的代码处理
,比如格式化,可能无法实现,代码支持单一,可能每个在线IED只是给每一个单独的语开发到极致, 比如我们今天要讲的js的语种插件机制
。对于本地 IDE,一般都会有插件系统来满足不同需求,并且多年积累下插件种类丰富,而都是各家自己的定制,很难出现统一的标准插件机制大型项目无法满足
。项目规模提升对网络的考验加大,再加上 WebIDE 性能受限于编译运行容器所获取的资源,这些资源有时候还比不上本地机器。有劣势,那么就会有优势 ,在线IED的优势
免安装,所见即所得
,快速开发,随处可用,在前端领域我们vscode 大家应该都用过, 在每次换一个新电脑,大家有多难受,自己心里都有点数代码安全性能保障
, 由于所有的东西都是在线的, 那么就会有用户和权限的概念,如此一来代码的安全性就会有极大保障,妈妈再也不用担心代码被盗了环境统一
相信大家都遇见过辛辛苦苦开发一下午提交了代码,在别人的电脑上死活跑步起来 ,而web 版本就不会有这个问题,他们的环境,完全来自云端,保证了环境的统一性,别人使用你的代码时候,就不会出现尴尬的问题协作编辑
这是一个非常诱人的功能,大家可以类比现在的在线word文档,你就知道有多丝滑了,这也是在线IDE核心竞争力,有了它你就能抛弃那令人作呕的git代码冲突了,还有可能一个在线的聊天窗口,这样一来一边干活,一边摸鱼岂不痛快,领导还不能说啥在我们前端圈子,充斥着很多在线的编辑器, 他们专为js这个语种设计,可能内置了很多个框架的模板,也可能为单独的框架定制开发,使得我们今天能够在网上顺畅的交流,共同进步。
接下来就让我来跟着大家一块揭开前端领域的在线IDE的原理
在揭开ide原理之前,我们先得了解一下目前市面上的一些主流的在线ide,所谓知己知彼,百战百胜
CodeSandbox 一个即时可用的,功能齐全的在线 IDE,可在具有浏览器的任何设备上进行Web 开发。使您能够快速启动新项目并快速创建原型。 并且主流的脚手架都支持,例如 create-react-app
、 vue-cli
、parcel
等等。
现在这个时候,在线ide一哥毋庸置疑,也是我常用并且天天研究的在线IDE,但是他有一个致命的缺点,国内用户访问太慢了
每次超过半分钟的等待时间,真是不厌其烦。
他的好处就是全,煎炒烹炸,闷溜熬炖
全有,然而我钻研过他的源码,耦合性太高,代码结构分的不是很清楚,可读性极差(也有可能是我太菜),不是在这个编辑器里面泡个十年八年的,你很难搞明白他写的是个啥
CodeSandbox的整个结构包含 三个部分,代码编辑器
,文件目录系统
, 和沙箱运行渲染环境
基本的运行原理如下(感谢前辈大佬们画的图)
代码编辑器这一部分其实很好理解,就是一个在线的编辑器 ,可以编辑代码,目前市面上做的比较好的有很多,比如monaco-editor
、codemirror
等都可以满足我们的需求
其实所谓的目录系统,那是官话 ,他本质就是个treeList
,也就是我们常用的element-ui
中常用的tree
只不过我们需要对于他做一些改造,来满足自己的业务需求,他的本质其实非常简单,就是一个递归组件,利用递来实现目录目录层级的结构
之前我也实现过一个,在系列文章开篇科普中就不再赘述,后续详细跟大家一起实现,如有兴趣请查看我写的 treelist
沙箱运行环境,是整个项目中最难的一部分他相当于在浏览器端实现了一个webpack的运行环境,通过配置,来模拟webpack的运行流程
export class ReactPreset extends Preset {
defaultHtmlBody = '<div id="root"></div>';
constructor() {
super('react');
}
async init(bundler: Bundler): Promise<void> {
await super.init(bundler);
await Promise.all([
this.registerTransformer(new BabelTransformer()),
this.registerTransformer(new ReactRefreshTransformer()),
this.registerTransformer(new CSSTransformer()),
this.registerTransformer(new StyleTransformer()),
this.registerTransformer(new ScssTransformer()),
this.registerTransformer(new UrlTransformer()),
]);
}
mapTransformers(module: Module): Array<[string, any]> {
if (/^(?!\/node_modules\/).*\.(((m|c)?jsx?)|tsx)$/.test(module.filepath)) {
return [
[
'babel-transformer',
{
presets: [
[
'react',
{
runtime: 'automatic',
},
],
],
plugins: [['react-refresh/babel', { skipEnvCheck: true }]],
},
],
['react-refresh-transformer', {}],
];
}
if (/\.(m|c)?(t|j)sx?$/.test(module.filepath) && !module.filepath.endsWith('.d.ts')) {
return [
[
'babel-transformer',
{
presets: [
[
'react',
{
runtime: 'automatic',
},
],
],
},
],
];
}
if (/\.css$/.test(module.filepath)) {
return [
['css-transformer', {}],
['style-transformer', {}],
];
}
if (/\.s(c|a)ss$/.test(module.filepath)) {
return [
['scss-transformer', {}],
['css-transformer', {}],
['style-transformer', {}],
];
}
if (/\.(png|jpeg|svg)$/.test(module.filepath)) {
return [
['url-transformer', {}],
];
}
throw new Error(`No transformer for ${module.filepath}`);
}
augmentDependencies(dependencies: DepMap): DepMap {
if (!dependencies['react-refresh']) {
dependencies['react-refresh'] = '^0.11.0';
}
dependencies['core-js'] = '3.22.7';
return dependencies;
}
}
以上就是一个react 的代码预设,你会发现跟webpack真的很像
而有了这个配置,自然而然的,就能调用不同的loader去处理文件
我们知道在在node
环境中webpack
编译之后就会将代码发送到浏览器中来执行,而此时,我们的代码就是在浏览器中编译的,这时候就用到了一个函数,eval
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;
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})`;
// @ts-ignore
// 使用eaval 执行函数
(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;
}
}
好了,CodeSandbox 的基本结构讲解完毕了,后续我们将跟大家一起实现一个类似的功能,揭晓整个沙箱的原理!
StackBlitz 也是一款在线的IDE 功能和CodeSandbox 类似也支持主流的脚手架。
这其实在功能上和CodeSandbox 已经重了, 那为啥还能这么火爆呢?
原因是他可以在浏览器端跑node,这是CodeSandbox 不具备的,所以他才能杀出来一条血路,当然这是CodeSandbox 现在在追赶并且支持了
比较可惜的是,他们俩的最新代码都没有开源!
可以说他俩现在功能基本重合了,但是他们的实现原理,大相径庭
我们之前说 CodeSandbox 的实现基于在浏览器中构建了webpack
,而StackBlitz则是使用了web container
CodeSandbox 如果要想运行nodejs代码, 则需要在远程服务器上运行整个开发环境,并将将结果传输回给浏览器。
这种方式有一个一些缺陷,首先非常慢
,并且没有任何安全优
势,开发体验极差
, 往往启动都需要几分钟,而且还容易出现网络延迟, 没有办法离线工作
, 如果网速没有保障,是那么就会经常网络超时
而所谓 WebContainer 其实本质就是可以在浏览器中模拟 Node.js 环境 ,从而直接 浏览器端快速启动项目,可在几毫秒内启动并立即处于在线状态,可以通过链接共享——只需单击一下。它也完全在浏览器中运行,这会产生下列这些关键的好处:
yarn/npm
快 20%,包安装完成速度 >= 5 倍。
Node.js
应用可以在浏览器中调试。 与 Chrome DevTools 的无缝集成支持本地后端调试,无需安装或扩展。
远程虚拟机
或本地二进制文件上。
而他的实现思路具有几个简单的步骤:
他的真正实现其实比较复杂,并且对于兼容性具有较高要求,因为其中还需要设计很多底层的东西以及一些新新的技术 比如Service Worker
、webassembly
接下来我们简单的介绍一下
Service workers 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。这个 API 旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。
一句话总结就是 一个服务器与浏览器之间的中间人角色
,是一种类型的 Web Worker ,具备了同网络拦截,缓存的能力。他具备以下特点
有了这些能耐,那可就是一个天然的node沙箱,node环境就有了基础
WebAssembly 是一种运行在现代网络浏览器中的新型代码,并且提供新的性能特性和效果。它设计的目的不是为了手写代码而是为诸如 C、C++和 Rust 等低级源语言提供一个高效的编译目标
以上是mdn的表达,用人话来说 WebAssembly 是一个可移植、体积小、加载快并且兼容 Web 的全新格式
WebAssembly 它具备以下特点:
相信看到这,大家依然不明白他是个什么玩意?
其实说白了就是一种字节码类型的代码,他本身不是给我们写代码用的,因为他的可读性极差
那他是干啥的呢? 用来大幅度提高 Javascript 的性能,同时也不损失安全性
其实就是为了打破js的现有的性能瓶颈, 那么使用它,我们就能将node的一些能力移植到浏览器上来
如此一来我们就能在浏览器中高性能的运行node应用了。
这一部分其实已经被StackBlitz开源了webcontainer-core
CodePen是什么?
官方是这么介绍的
CodePen
是一个完全免费的前端代码托管服务与GitHub Pages 相比,它最重要的优势有:
上述讲了他的主要功能,其实在我看来,他本质上就是一个简单的IDE,功能单一,目标明确,就是为了写个dome 互相交流,仅此而已,无法承载大型项目
码上掘金
,jym再熟悉不过了,他和CodePen功能类似, 但是有一个非常大的好处就是他能嵌入在掘金的编辑器中,这算是自家项目开的后门
VueSFC 就更简单了,主要专为vue设计,并且目前来看只兼容vue3
核心原理就是利用vue3的compiler-sfc
实现编译 并且同样的使用eval 实现渲染
,并且源代码非常适合阅读。
他的编译原理拆开了主要就是处理.vue 文件
import { Store, File } from './store'
import {
SFCDescriptor,
BindingMetadata,
shouldTransformRef,
transformRef,
CompilerOptions
} from 'vue/compiler-sfc'
// 编译ts
import { transform } from 'sucrase'
// 生成id
import hashId from 'hash-sum'
// 导出名字
export const COMP_IDENTIFIER = `__sfc__`
// 编译ts
async function transformTS(src: string) {
return transform(src, {
transforms: ['typescript', 'imports']
}).code
}
// 编译文件
export async function compileFile(
store: Store,
{ filename, code, compiled }: File
) {
if (!code.trim()) {
store.state.errors = []
return
}
if (filename.endsWith('.css')) {
compiled.css = code
store.state.errors = []
return
}
if (filename.endsWith('.js') || filename.endsWith('.ts')) {
if (shouldTransformRef(code)) {
code = transformRef(code, { filename }).code
}
if (filename.endsWith('.ts')) {
code = await transformTS(code)
}
compiled.js = compiled.ssr = code
store.state.errors = []
return
}
if (!filename.endsWith('.vue')) {
store.state.errors = []
return
}
const id = hashId(filename)
// 拆分.vue文件
const { errors, descriptor } = store.compiler.parse(code, {
filename,
sourceMap: true
})
if (errors.length) {
store.state.errors = errors
return
}
if (
descriptor.styles.some((s) => s.lang) ||
(descriptor.template && descriptor.template.lang)
) {
store.state.errors = [
`lang="x" pre-processors for <template> or <style> are currently not ` +
`supported.`
]
return
}
const scriptLang =
(descriptor.script && descriptor.script.lang) ||
(descriptor.scriptSetup && descriptor.scriptSetup.lang)
const isTS = scriptLang === 'ts'
if (scriptLang && !isTS) {
store.state.errors = [`Only lang="ts" is supported for <script> blocks.`]
return
}
const hasScoped = descriptor.styles.some((s) => s.scoped)
let clientCode = ''
let ssrCode = ''
const appendSharedCode = (code: string) => {
clientCode += code
ssrCode += code
}
// 处理script
const clientScriptResult = await doCompileScript(
store,
descriptor,
id,
false,
isTS
)
if (!clientScriptResult) {
return
}
const [clientScript, bindings] = clientScriptResult
clientCode += clientScript
// script ssr only needs to be performed if using <script setup> where
// the render fn is inlined.
if (descriptor.scriptSetup) {
const ssrScriptResult = await doCompileScript(
store,
descriptor,
id,
true,
isTS
)
if (ssrScriptResult) {
ssrCode += ssrScriptResult[0]
} else {
ssrCode = `/* SSR compile error: ${store.state.errors[0]} */`
}
} else {
// when no <script setup> is used, the script result will be identical.
ssrCode += clientScript
}
// template
// only need dedicated compilation if not using <script setup>
if (
descriptor.template &&
(!descriptor.scriptSetup || store.options?.script?.inlineTemplate === false)
) {
const clientTemplateResult = await doCompileTemplate(
store,
descriptor,
id,
bindings,
false,
isTS
)
if (!clientTemplateResult) {
return
}
clientCode += clientTemplateResult
const ssrTemplateResult = await doCompileTemplate(
store,
descriptor,
id,
bindings,
true,
isTS
)
if (ssrTemplateResult) {
// ssr compile failure is fine
ssrCode += ssrTemplateResult
} else {
ssrCode = `/* SSR compile error: ${store.state.errors[0]} */`
}
}
if (hasScoped) {
// 梳理scoped
appendSharedCode(
`\n${COMP_IDENTIFIER}.__scopeId = ${JSON.stringify(`data-v-${id}`)}`
)
}
if (clientCode || ssrCode) {
appendSharedCode(
`\n${COMP_IDENTIFIER}.__file = ${JSON.stringify(filename)}` +
`\nexport default ${COMP_IDENTIFIER}`
)
compiled.js = clientCode.trimStart()
compiled.ssr = ssrCode.trimStart()
}
// styles
let css = ''
// 处理css
for (const style of descriptor.styles) {
if (style.module) {
store.state.errors = [
`<style module> is not supported in the playground.`
]
return
}
// 处理scoped
const styleResult = await store.compiler.compileStyleAsync({
...store.options?.style,
source: style.content,
filename,
id,
scoped: style.scoped,
modules: !!style.module
})
if (styleResult.errors.length) {
// postcss uses pathToFileURL which isn't polyfilled in the browser
// ignore these errors for now
if (!styleResult.errors[0].message.includes('pathToFileURL')) {
store.state.errors = styleResult.errors
}
// proceed even if css compile errors
} else {
css += styleResult.code + '\n'
}
}
if (css) {
compiled.css = css.trim()
} else {
compiled.css = '/* No <style> tags present */'
}
// clear errors
store.state.errors = []
}
// 编译script模块
async function doCompileScript(
store: Store,
descriptor: SFCDescriptor,
id: string,
ssr: boolean,
isTS: boolean
): Promise<[string, BindingMetadata | undefined] | undefined> {
if (descriptor.script || descriptor.scriptSetup) {
try {
const expressionPlugins: CompilerOptions['expressionPlugins'] = isTS
? ['typescript']
: undefined
// 编译script 文件
const compiledScript = store.compiler.compileScript(descriptor, {
inlineTemplate: true,
...store.options?.script,
id,
templateOptions: {
...store.options?.template,
compilerOptions: {
...store.options?.template?.compilerOptions,
}
}
})
let code = ''
if (compiledScript.bindings) {
code += `\n/* Analyzed bindings: ${JSON.stringify(
compiledScript.bindings,
null,
2
)} */`
}
// script 中Default导出 处理
code +=
`\n` +
store.compiler.rewriteDefault(
compiledScript.content,
COMP_IDENTIFIER,
expressionPlugins
)
// 处理ts
if ((descriptor.script || descriptor.scriptSetup)!.lang === 'ts') {
code = await transformTS(code)
}
return [code, compiledScript.bindings]
} catch (e: any) {
store.state.errors = [e.stack.split('\n').slice(0, 12).join('\n')]
return
}
} else {
return [`\nconst ${COMP_IDENTIFIER} = {}`, undefined]
}
}
// 编译模板
async function doCompileTemplate(
store: Store,
descriptor: SFCDescriptor,
id: string,
bindingMetadata: BindingMetadata | undefined,
ssr: boolean,
isTS: boolean
) {
// 编译模板
const templateResult = store.compiler.compileTemplate({
...store.options?.template,
source: descriptor.template!.content,
filename: descriptor.filename,
id,
scoped: descriptor.styles.some((s) => s.scoped),
slotted: descriptor.slotted,
ssr,
ssrCssVars: descriptor.cssVars,
isProd: false,
compilerOptions: {
...store.options?.template?.compilerOptions,
bindingMetadata,
expressionPlugins: isTS ? ['typescript'] : undefined
}
})
if (templateResult.errors.length) {
store.state.errors = templateResult.errors
return
}
const fnName = ssr ? `ssrRender` : `render`
// 拼接代码
let code =
`\n${templateResult.code.replace(
/\nexport (function|const) (render|ssrRender)/,
`$1 ${fnName}`
)}` + `\n${COMP_IDENTIFIER}.${fnName} = ${fnName}`
if ((descriptor.script || descriptor.scriptSetup)?.lang === 'ts') {
code = await transformTS(code)
}
return code
}
基本的代码如下,后续我们详细解析
打造在线IDE 系列文章基础篇到此结束,主要介绍了一下各个在线IDE的优势,以及一些使用的简单的原理