怀念啊我们的青春啊
不知从何时开始,脚手架
和构建
似乎成了前端绕不过去的一个步骤,本来HTML+JS+CSS
组合只需一个带文本高亮的简易编辑器就可以直接开始写代码了,现在却要天天和node
、python
、npm
、webpack
(vite
)、babel
、sass
(less
)等一堆的玩意儿打交道,有点情怀的“老程序员”可能都会怀念那个没有构建、一个Ctrl+S
就可以直接保存生效甚至直接去发布的感觉。越来越臃肿的脚手架、越来越复杂的各种lint
规则、越来越慢的构建速度、越来越多的根目录文件越来越影响我们的开发体验,有时候不禁纳闷:我就写个简单的后台页面而已,真的至于这么费劲么?真的有必要一定要构建么?或者说一定要在发布前进行预构建么?都2024年了,浏览器的兼容性越来越好了,而且很多时候页面的受众可能只是内部用户,根本就无需关注兼容性问题,那么我仍在在构建的目的是什么呢?
什么叫在线编程?
抛出上面的问题后我们再来回答什么是在线编程。在线编程 = 没有脚手架 + 没有预构建 + 保存即发布,有点类似将大家熟知的codepen
、jsfiddle
等搬上生产环境!
在线编程适合的场景
在线编程虽然很香,但是需要注意适用场景,一般而言,至少同时满足以下条件才可以尝试在线编程方案:
- 不需要关注浏览器兼容性;
- 当下以及可预见的未来不需要多人协作;
- 页面逻辑不会太复杂;
语言选型
都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 代码。
支持通过data-presets=es2015
设置预设,可用预设如下:
支持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);
});
[...document.querySelectorAll('link[rel="stylesheet/scss"]')].forEach(async style => {
const href = style.href;
const newStyle = document.createElement('style');
newStyle.dataset.href = href;
style.parentNode.insertBefore(newStyle, style); // 插在原位置前面
style.parentNode.removeChild(style); // 删除旧元素
const scss = await fetch(href).then(resp => resp.text());
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);
});
[...document.querySelectorAll('link[rel="stylesheet/scss"]')].forEach(async style => {
const href = style.href;
const newStyle = document.createElement('style');
newStyle.dataset.href = href;
style.parentNode.insertBefore(newStyle, style); // 插在原位置前面
style.parentNode.removeChild(style); // 删除旧元素
const scss = await fetch(href).then(resp => resp.text());
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-module
和src
一起使用时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代码只能以字符串的形式编写,无法直接高亮。
结语
还是那句话,事无绝对,免预构建在线编程虽然很香,但只适合单人开发的小项目,切勿贪杯。