拥抱在线编程,去他娘的构建!
本文由 小茗同学 发表于 2024-01-09 浏览(188)
最后修改 2024-04-14 标签:

怀念啊我们的青春啊

不知从何时开始,脚手架构建似乎成了前端绕不过去的一个步骤,本来HTML+JS+CSS组合只需一个带文本高亮的简易编辑器就可以直接开始写代码了,现在却要天天和nodepythonnpmwebpack(vite)、babelsass(less)等一堆的玩意儿打交道,有点情怀的“老程序员”可能都会怀念那个没有构建、一个Ctrl+S就可以直接保存生效甚至直接去发布的感觉。越来越臃肿的脚手架、越来越复杂的各种lint规则、越来越慢的构建速度、越来越多的根目录文件越来越影响我们的开发体验,有时候不禁纳闷:我就写个简单的后台页面而已,真的至于这么费劲么?真的有必要一定要构建么?或者说一定要在发布前进行预构建么?都2024年了,浏览器的兼容性越来越好了,而且很多时候页面的受众可能只是内部用户,根本就无需关注兼容性问题,那么我仍在在构建的目的是什么呢?

undefined

什么叫在线编程?

抛出上面的问题后我们再来回答什么是在线编程。在线编程 = 没有脚手架 + 没有预构建 + 保存即发布,有点类似将大家熟知的codepenjsfiddle等搬上生产环境!

在线编程适合的场景

在线编程虽然很香,但是需要注意适用场景,一般而言,至少同时满足以下条件才可以尝试在线编程方案:

  • 不需要关注浏览器兼容性;
  • 当下以及可预见的未来不需要多人协作;
  • 页面逻辑不会太复杂;

语言选型

都2023年了,虽然原生JS语法越来越强,但如果说我们还是基于原生JS去开发一些重DOM交互的页面,那效率真是太低了,成熟框架的使用可以让我们事半功倍,react和vue都有非常成熟的在线编程环境支持,大胆拥抱即可。

下面分别简单介绍React和Vue的在线编程实践。

React篇

最简单示例

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>最简单在线react编程示例</title>
</head>
<body>
	<div id="root"></div>
	<script src="https://g.alicdn.com/code/lib/react/16.9.0/umd/react.production.min.js"></script>
	<script src="https://g.alicdn.com/code/lib/react-dom/16.9.0/umd/react-dom.production.min.js"></script>
	<!-- 虽然浏览器原生支持es6,但是还需要引入babel以便支持jsx语法 -->
	<script src="https://g.alicdn.com/code/lib/babel-standalone/7.22.5/babel.min.js"></script>
	<script type="text/babel">
		class App extends React.Component {
			state = {
				count: 0,
			};

			add = () => {
				this.setState({ count: this.state.count + 1 });
			}

			render() {
				const { count } = this.state;
				return <div className="demo-page">
					<button type="primary" onClick={this.add}>测试</button>
					计数:{count}
				</div>;
			}
		}
		ReactDOM.render(<App />, document.getElementById('root'));
	</script>
</body>
</html>

babel-standalone 会查找页面上所有类型为 text/babel 的 <script> 标签,并将其中的 JSX 代码转换为普通的 JavaScript 代码。

支持scss语法

<style type="text/scss">
body {
	-webkit-font-smoothing: antialiased;
	background-color: #f5f7fa;
	color: #333;
	font-size: 14px;
	#root {
		width: 1400px;
	}
}
</style>
<script src="https://g.alicdn.com/code/lib/sass.js/0.11.0/sass.sync.min.js"></script>
<script>
	// 编译页面所有scss,可重复调用
	function compileScss() {
		[...document.querySelectorAll('style[type="text/scss"]')].forEach(style => {
			const scss = style.textContent;
			const newStyle = document.createElement('style');
			style.parentNode.insertBefore(newStyle, style); // 插在原位置前面
			style.parentNode.removeChild(style); // 删除旧元素
			Sass.compile(scss, result => newStyle.textContent = result?.text);
		});
	}
	// 立即调用一次
	compileScss();
	// 页面加载完再调用一次
	document.addEventListener("DOMContentLoaded", compileScss);
</script>

使用Fusion组件

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>完整在线react编程示例</title>
</head>
<body>
	<link rel="stylesheet" href="https://g.alicdn.com/code/lib/alifd__next/1.26.9/next.min.css">
	<style type="text/scss">
	body {
		-webkit-font-smoothing: antialiased;
		background-color: #f5f7fa;
		color: #333;
		font-size: 14px;
	}
	</style>
	<script src="https://g.alicdn.com/code/lib/sass.js/0.11.0/sass.sync.min.js"></script>
	<script>
		// 编译页面所有scss,可重复调用
		function compileScss() {
			[...document.querySelectorAll('style[type="text/scss"]')].forEach(style => {
				const scss = style.textContent;
				const newStyle = document.createElement('style');
				style.parentNode.insertBefore(newStyle, style); // 插在原位置前面
				style.parentNode.removeChild(style); // 删除旧元素
				Sass.compile(scss, result => newStyle.textContent = result?.text);
			});
		}
		// 立即调用一次
		compileScss();
		// 页面加载完再调用一次
		document.addEventListener("DOMContentLoaded", compileScss);
	</script>
	<div id="root"></div>

	<script src="https://g.alicdn.com/code/lib/react/16.9.0/umd/react.production.min.js"></script>
	<script src="https://g.alicdn.com/code/lib/react-dom/16.9.0/umd/react-dom.production.min.js"></script>
	<script src="https://g.alicdn.com/code/lib/babel-standalone/7.22.5/babel.min.js"></script>
	<script src="https://g.alicdn.com/code/lib/alifd__next/1.26.9/next.min.js"></script>
	<script src="https://g.alicdn.com/code/lib/bizcharts/4.1.22/BizCharts.min.js"></script>
	<script src="https://g.alicdn.com/code/lib/echarts/5.4.3/echarts.min.js"></script>

	<script type="text/babel">
		const { Button } = Next;
		class App extends React.Component {
			state = {
				count: 0,
			};

			componentDidMount() {
				// this.query(1);
			}

			add = () => {
				this.setState({ count: this.state.count + 1 });
			}

			render() {
				const { count } = this.state;
				return <div className="demo-page">
					<Button type="primary" onClick={this.add}>测试</Button>
					计数:{count}
				</div>;
			}
		}
		ReactDOM.render(<App />, document.getElementById('root'));
	</script>
</body>
</html>

模块化

虽然在线编程不太适合编写太复杂代码,但是如果有模块化的加持肯定是最好的,毕竟把全部代码都写在一个文件也很难维护。

为了方便后面演示,假设服务器上有如下3个JS文件:

采用JSX编写的/demo/react-component-demo-1.js

const { Button } = Next;
export default class Demo1 extends React.Component {
	render() {
		return <div className="test-class">
			<Button type="secondary">测试React组件1</Button>
		</div>;
	}
}

采用原生JS编写的/demo/react-component-demo-2.js

const { Button } = Next;
export default function Demo2() {
	return React.createElement(Button, { type: 'secondary' }, '测试React组件2');
};

采用JSX编写的无模块化/demo/react-component-demo-3.js

const { Button } = Next;
class Demo3 extends React.Component {
	render() {
		return <div className="test-class">
			<Button type="secondary">测试React组件1</Button>
		</div>;
	}
}

全局变量

严格来讲这个不属于模块化,但是确实最容易实现的引入外部代码方案。默认情况下babel在编译时并不会进行模块化处理,前面一个script中的变量可以直接在后一个script中获取到:

<script type="text/babel">
	const aaa = 123;
</script>
<script type="text/babel">
	console.log('aaa:', aaa); // 可以正常输出
</script>

所以,我们得到了最简单的“模块化”方案:

<script type="text/babel" src="/demo/react-component-demo-3.js"></script>
<script type="text/babel">
	class App extends React.Component {
		render() {
			return <Demo3 />;
		}
	}
	ReactDOM.render(<App />, document.getElementById('root'));
</script>

由于这种方式很容易出现变量互相冲突覆盖,所以谨慎使用。

浏览器原生模块化

假如我们在上述React环境下写如下测试代码:

<script type="text/babel">
	import Demo1 from '/demo/react-component-demo-1.js';
	class App extends React.Component {
		render() {
			return <Demo1 />;
		}
	}
	ReactDOM.render(<App />, document.getElementById('root'));
</script>

直接运行发现会报错如下:

Uncaught ReferenceError: require is not defined

这是因为默认情况下babel会将import转化为require来处理,而我们没有引入require环境。通过给script设置type=module可以启用浏览器原生esModule,但是由于type已经被babel占用了,我们需要设置data-type="module"来标识这是一个原生模块(这个是babel提供的语法),修改如下:

<script type="text/babel" data-type="module">
	import Demo1 from '/demo/react-component-demo-1.js';
	class App extends React.Component {
		render() {
			return <Demo1 />;
		}
	}
	ReactDOM.render(<App />, document.getElementById('root'));
</script>

运行后发现仍然报错:

Uncaught SyntaxError: Unexpected token '<' (at react-component-demo-1.js:5:16)

这是因为通过浏览器自带的import导入的外部JS不会被babel编译,babel只会编译当前script的内联代码。通过network面板可以看到浏览器通过GET成功引入了这个JS:

此时如果我们把引入的js换成原生编写的/demo/react-component-demo-2.js就没问题了:

<script type="text/babel" data-type="module">
	import Demo2 from '/demo/react-component-demo-2.js';
	class App extends React.Component {
		render() {
			return <Demo2 />;
		}
	}
	ReactDOM.render(<App />, document.getElementById('root'));
</script>

运行正常:

由于babel无法劫持原生import编译导入的外部文件,故如果要支持带jsx的js的话,此路不通。

全局变量+原生模块化

虽然全局变量容易出现命名冲突问题,虽然原生模块化无法劫持import,但是我们可以组合二者一起使用,通过type=module增加对环境隔离的支持,通过全局变量实现导入导出:

假设有/demo/react-component-demo-4.js

const { Button } = Next;
class Demo3 extends React.Component {
	render() {
		return <div className="test-class">
			<Button type="secondary">测试React组件1</Button>
		</div>;
	}
}
// 相当于 export
window.Demo3 = Demo3;
<script type="text/babel" src="/demo/react-component-demo-4.js" data-type="module"></script>
<script type="text/babel" data-type="module">
	class App extends React.Component {
		render() {
			return <Demo3 />;
		}
	}
	ReactDOM.render(<App />, document.getElementById('root'));
</script>

这个方案中规中矩,比上不足比下有余。

服务端加持

在服务端通过模板引擎组合各种代码片段是模块化的另一种尝试,假设有如下代码片段:

<!-- test1.htm -->
<script type="text/babel" data-type="module">
const { Button } = Next;
class Demo3 extends React.Component {
	render() {
		return <div className="test-class">
			<Button type="secondary">测试React组件1</Button>
		</div>;
	}
}
// 相当于 export
window.Demo3 = Demo3;
</script>

然后在另一个html中引入这段文件,假设使用的是ejs语法:

<% include test1.htm %>
<script type="text/babel" data-type="module">
	class App extends React.Component {
		render() {
			return <Demo3 />;
		}
	}
	ReactDOM.render(<App />, document.getElementById('root'));
</script>

可以看到服务端加持的优势并不是特别明显,一般不太采用。

babel模块化

关于babel的模块化,默认为全局变量,添加data-plugins="transform-modules-umd"后变成umd模块:

<script type="text/babel">
	const aaa = 111;
</script>
<script type="text/babel">
	console.log('aaa:', aaa); // 输出 111
</script>
<script type="text/babel" data-plugins="transform-modules-umd">
	const bbb = 222;
</script>
<script type="text/babel" data-plugins="transform-modules-umd">
	console.log('bbb:', bbb); // bbb is not defined
</script>
<script type="text/babel" data-plugins="transform-modules-umd" data-module="Test">
	const ccc = 333;
	export default ccc;
	export const ddd = 444;
</script>
<script type="text/babel" data-plugins="transform-modules-umd">
	import ccc, * as Test from 'Test';
	console.log('ccc:', ccc); // 输出 333
	console.log('Test:', Test); // 输出 { ddd: 444, default: 333 }
</script>
<script type="text/babel">
	const { default: ccc } = Test;
	console.log('ccc', ccc);
	console.log('Test2:', Test); // 其实默认 Test 已经注入全局变量了,可以直接使用
</script>

如果同时和src一起使用时:

<script type="text/babel" data-plugins="transform-modules-umd" src="/demo/react-component-demo-1.js"></script>
<script type="text/babel" data-plugins="transform-modules-umd">
	import Demo from '/demo/react-component-demo-1.js';
	class App extends React.Component {
		render() {
			return <Demo />;
		}
	}
	ReactDOM.render(<App />, document.getElementById('root'));
</script>

已知data-modulesrc一起使用时data-module不生效。

终极方案

可以看到,前面方案使用上仍然不是特别方便,需要先用script单独引入,然后后又使用import再次引入。其实前面的script引入我们可以通过脚本自动完成,就像处理scss代码一样,将下面这段代码加在babel-standalone引入之前的任何位置即可,例如我们可以把它放在compileScss的后面:

// 预编译带 text/babel 的script标签,处理 import
function preCompileBabelScript() {
	// 相对路径转绝对路径
	function getAbsoluteSrc(src) {
		const a = document.createElement('a');
		a.href = src;
		return a.href;
	}
	const srcMap = {}; // 防止重复注入的map
	[...document.querySelectorAll('script[type="text/babel"]')]
		// 存储src集合
		.map(script => (srcMap[script.src] = script.src) || script)
		.forEach(script => {
			const { src, textContent, dataset } = script;
			// 遍历的script必须是内联代码,必须配置了 transform-modules-umd
			if (src || !textContent || !dataset?.plugins?.includes('transform-modules-umd')) {
				return;
			}
			// 自动检索类似 `import Demo from '/demo.js';` 这样的代码,并遍历出所有的src
			textContent.match(/(?<=import .+['"])(.+)(?=['"][;\n])/g)?.forEach(src => {
				// 由于上面 srcMap 存的是完整路径,这里需要转一下
				if (srcMap[getAbsoluteSrc(src)]) {
					return;
				}
				const ele = document.createElement('script');
				ele.src = src;
				ele.type = 'text/babel';
				ele.dataset.plugins = 'transform-modules-umd';
				script.parentNode.insertBefore(ele, script); // 依次插在原script前面
				srcMap[src] = true;
			});
		});
}
// 已知 babel 初始化也是在DOM初始化完成,这里刚好抢在 babel 初始化之前执行
document.addEventListener("DOMContentLoaded", preCompileBabelScript);

加上了上面这段代码后我们的模块引入就非常简单了:

<script type="text/babel" data-plugins="transform-modules-umd">
	import Demo from '/demo/react-component-demo-1.js';
	class App extends React.Component {
		render() {
			return <Demo />;
		}
	}
	ReactDOM.render(<App />, document.getElementById('root'));
</script>

完整终极代码:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>完整在线react编程示例</title>
</head>
<body>
	<link rel="stylesheet" href="https://g.alicdn.com/code/lib/alifd__next/1.26.9/next.min.css">
	<style type="text/scss">
	body {
		-webkit-font-smoothing: antialiased;
		background-color: #f5f7fa;
		color: #333;
		font-size: 14px;
	}
	</style>
	<script src="https://g.alicdn.com/code/lib/sass.js/0.11.0/sass.sync.min.js"></script>
	<script>
		// 编译页面所有scss,可重复调用
		function compileScss() {
			[...document.querySelectorAll('style[type="text/scss"]')].forEach(style => {
				const scss = style.textContent;
				const newStyle = document.createElement('style');
				style.parentNode.insertBefore(newStyle, style); // 插在原位置前面
				style.parentNode.removeChild(style); // 删除旧元素
				Sass.compile(scss, result => newStyle.textContent = result?.text);
			});
		}
		// 立即调用一次
		compileScss();
		// 页面加载完再调用一次
		document.addEventListener("DOMContentLoaded", compileScss);
		// 预编译带 text/babel 的script标签,处理 import
		function preCompileBabelScript() {
			// 相对路径转绝对路径
			function getAbsoluteSrc(src) {
				const a = document.createElement('a');
				a.href = src;
				return a.href;
			}
			const srcMap = {}; // 防止重复注入的map
			[...document.querySelectorAll('script[type="text/babel"]')]
				// 存储src集合
				.map(script => (srcMap[script.src] = script.src) || script)
				.forEach(script => {
					const { src, textContent, dataset } = script;
					// 遍历的script必须是内联代码,必须配置了 transform-modules-umd
					if (src || !textContent || !dataset?.plugins?.includes('transform-modules-umd')) {
						return;
					}
					// 自动检索类似 `import Demo from '/demo.js';` 这样的代码,并遍历出所有的src
					textContent.match(/(?<=import .+['"])(.+)(?=['"][;\n])/g)?.forEach(src => {
						// 由于上面 srcMap 存的是完整路径,这里需要转一下
						if (srcMap[getAbsoluteSrc(src)]) {
							return;
						}
						const ele = document.createElement('script');
						ele.src = src;
						ele.type = 'text/babel';
						ele.dataset.plugins = 'transform-modules-umd';
						script.parentNode.insertBefore(ele, script); // 依次插在原script前面
						srcMap[src] = true;
					});
				});
		}
		// 已知 babel 初始化也是在DOM初始化完成,这里刚好抢在 babel 初始化之前执行
		document.addEventListener("DOMContentLoaded", preCompileBabelScript);
	</script>
	<div id="root"></div>

	<script src="https://g.alicdn.com/code/lib/react/16.9.0/umd/react.production.min.js"></script>
	<script src="https://g.alicdn.com/code/lib/react-dom/16.9.0/umd/react-dom.production.min.js"></script>
	<script src="https://g.alicdn.com/code/lib/babel-standalone/7.22.5/babel.min.js"></script>
	<script src="https://g.alicdn.com/code/lib/alifd__next/1.26.9/next.min.js"></script>
	<script src="https://g.alicdn.com/code/lib/bizcharts/4.1.22/BizCharts.min.js"></script>
	<script src="https://g.alicdn.com/code/lib/echarts/5.4.3/echarts.min.js"></script>

	<script type="text/babel" data-plugins="transform-modules-umd">
		import Demo from '/demo/react-component-demo-1.js';
		class App extends React.Component {
			render() {
				return <Demo />;
			}
		}
		ReactDOM.render(<App />, document.getElementById('root'));
	</script>
</body>
</html>

需要注意的是上述方案仅适用于处理内联代码中的import,不支持多级嵌套处理。

Vue篇

最简单示例

得益于Vue的天生面向开发者友好,且由于vue没有jsx(默认情况下),可以无需引入babel,所以vue的在线编程就简单太多了:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>最简单在线vue编程示例</title>
</head>
<body>
	<div id="app">
		<button @click="count++">积攒功德</button>
		您的功德+{{count}}
	</div>
	<script src="https://g.alicdn.com/code/lib/vue/3.2.45/vue.global.js"></script>
	<script type="text/javascript">
		const { createApp } = Vue;
		const App = {
			data() {
				return {
					count: 0,
				};
			},
		};
		const app = createApp(App);
		app.mount('#app');
	</script>
</body>
</html>

不过由于Vue的模板代码在编译前会暴露,需要出一些处理,否则会出现一闪而过的情况:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>最简单在线vue编程示例</title>
	<style>
		#app { display: none }
	</style>
</head>
<body>
	<div id="app">
		<button @click="count++">积攒功德</button>
		您的功德+{{count}}
	</div>
	<script src="https://g.alicdn.com/code/lib/vue/3.2.45/vue.global.js"></script>
	<script type="text/javascript">
		const { createApp } = Vue;
		const App = {
			data() {
				return {
					count: 0,
				};
			},
			mounted() {
				document.getElementById('app').style.display = 'block';
			},
		};
		const app = createApp(App);
		app.mount('#app');
	</script>
</body>
</html>

支持scss

支持scss完全等同于react,这里省略。

引入element-plus

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>最简单在线vue编程示例</title>
	<style>
		#app { display: none }
	</style>
</head>
<body>
	<div id="app">
		<el-button @click="test">积攒功德</el-button>
		您的功德+{{count}}
	</div>
	<link rel="stylesheet" href="https://g.alicdn.com/code/lib/element-plus/2.4.4/index.css" />
	<script src="https://g.alicdn.com/code/lib/vue/3.2.45/vue.global.js"></script>
	<script src="https://g.alicdn.com/code/lib/element-plus/2.4.4/index.full.js"></script>
	<script type="text/javascript">
		const { createApp } = Vue;
		const App = {
			data() {
				return {
					count: 0,
				};
			},
			mounted() {
				document.getElementById('app').style.display = 'block';
			},
			methods: {
				test() {
					this.count++;
					this.$message('积攒功德成功!');
				},
			},
		};
		const app = createApp(App);
		app.use(ElementPlus);
		app.mount('#app');
	</script>
</body>
</html>

完整示例

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>最简单在线vue编程示例</title>
	<style>
		#app { display: none }
	</style>
</head>
<body>
	<div id="app">
		<el-button @click="test">积攒功德</el-button>
		您的功德+{{count}}
	</div>
	<style type="text/scss">
	body {
		-webkit-font-smoothing: antialiased;
		background-color: #f5f7fa;
		color: #333;
		font-size: 14px;
	}
	</style>
	<script src="https://g.alicdn.com/code/lib/sass.js/0.11.0/sass.sync.min.js"></script>
	<script>
		// 编译页面所有scss,可重复调用
		function compileScss() {
			[...document.querySelectorAll('style[type="text/scss"]')].forEach(style => {
				const scss = style.textContent;
				const newStyle = document.createElement('style');
				style.parentNode.insertBefore(newStyle, style); // 插在原位置前面
				style.parentNode.removeChild(style); // 删除旧元素
				Sass.compile(scss, result => newStyle.textContent = result?.text);
			});
		}
		// 立即调用一次
		compileScss();
		// 页面加载完再调用一次
		document.addEventListener("DOMContentLoaded", compileScss);
	</script>
	<link rel="stylesheet" href="https://g.alicdn.com/code/lib/element-plus/2.4.4/index.css" />
	<script src="https://g.alicdn.com/code/lib/vue/3.2.45/vue.global.js"></script>
	<script src="https://g.alicdn.com/code/lib/element-plus/2.4.4/index.full.js"></script>
	<script type="text/javascript">
		const { createApp } = Vue;
		const App = {
			data() {
				return {
					count: 0,
				};
			},
			mounted() {
				document.getElementById('app').style.display = 'block';
			},
			methods: {
				test() {
					this.count++;
					this.$message('积攒功德成功!');
				},
			},
		};
		const app = createApp(App);
		app.use(ElementPlus);
		app.mount('#app');
	</script>
</body>
</html>

模块化

同样由于Vue没有jsx,所以可以直接使用浏览器原生模块化,模块化简单很多。不过由于原生模块化只能导入JS文件,所以template部分代码必须放到JS里去。

假设服务器有文件/demo/vue-component-demo-1.js

const { defineComponent } = Vue;
export default defineComponent({
  name: 'Demo',
  data() {
	return {
	  count: 0
	};
  },
  methods: {
	test() {
	  this.count++;
	}
  },
  template: `
	<el-button @click="test">积攒功德</el-button>
	您的功德+{{ count }}
  `,
});

主页面代码如下:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>最简单在线vue编程示例</title>
	<style>
		#app { display: none }
	</style>
</head>
<body>
	<div id="app">
		测试组件引入:<demo></demo>
	</div>
	<link rel="stylesheet" href="https://g.alicdn.com/code/lib/element-plus/2.4.4/index.css" />
	<script src="https://g.alicdn.com/code/lib/vue/3.2.45/vue.global.js"></script>
	<script src="https://g.alicdn.com/code/lib/element-plus/2.4.4/index.full.js"></script>
	<script type="module">
		import Demo from '/demo/vue-component-demo-1.js';
		const { createApp } = Vue;
		const App = {
			components: { Demo },
			data() {
				return {
					count: 0,
				};
			},
			mounted() {
				document.getElementById('app').style.display = 'block';
			},
		};
		const app = createApp(App);
		app.use(ElementPlus);
		app.mount('#app');
	</script>
</body>
</html>

这个方案缺点是html代码只能以字符串的形式编写,无法直接高亮。

结语

还是那句话,事无绝对,免预构建在线编程虽然很香,但只适合单人开发的小项目,切勿贪杯。