问题
最近在研究OJ(OnlineJudge,即在线运行代码自动判题的平台,类似leetcode、ACM和OI的比赛平台),考虑让Vue和React组件的代码能够支持OJ的JudgeServer,所以动手实现了一个简单的命令行交互可运行React和Vue组件的小工具 github.com/xitu/showca…
因为我的主要使用场景是特定的(机器判题),所以不需要考虑浏览器兼容性,因此不选择复杂的构建工具,而是追求以简单的方式让代码可以运行在最新的Chrome浏览器和Puppeteer环境即可。
借着实现这个工具,我深入研究了一下零构建快速搭建Vue、React非生产环境的方法,将它总结下来,对希望用简单的环境学习框架和希望自己实现和扩展构建工具的同学们也许会有帮助。
不依赖构建工具的React环境
作为流行的前端框架,React和Vue都提供了不依赖构建的运行环境来帮助新人快速上手。
React本身在设计上可以不依赖编译工具,完全使用原生的JS写法。
例如:
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<div id="root"></div>
<script type="text/javascript">
class Hello extends React.Component {
render() {
return React.createElement('div', null, `Hello ${this.props.toWhat}`);
}
}
ReactDOM.render(
React.createElement(Hello, {toWhat: 'World'}, null),
document.getElementById('root')
);
</script>
复制代码
当然,如果你这么使用的话,就不能使用jsx语法了,所以一般我们也不会这么用,即使是出于学习目的。
但是,React可以使用浏览器的Babel环境来实时编译,我们改一下上面的代码:
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<div id="root"></div>
<script type="text/babel">
class Hello extends React.Component {
render() {
return <div>Hello {this.props.toWhat}</div>;
}
}
ReactDOM.render(
<Hello toWhat="World"/>,
document.getElementById('root')
);
</script>
复制代码
但这么做还是有美中不足之处,那就是我们不能用import来加载多个组件。
不过我们还是有办法做到这一点的,在新的浏览器上我们可以用原生的ESModule:
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="importmap">
{
"imports": {
"react": "https://unpkg.com/@esm-bundle/react/esm/react.development.js",
"react-dom": "https://unpkg.com/@esm-bundle/react-dom/esm/react-dom.development.js"
}
}
</script>
<div id="root"></div>
<script type="text/babel" data-type="module">
import React from 'react';
import ReactDOM from 'react-dom';
import Greeting from './greeting.js';
class Hello extends React.Component {
render() {
return <div>Hello {this.props.toWhat} <Greeting name="akira"/></div>;
}
}
ReactDOM.render(
<Hello toWhat="World"/>,
document.getElementById('root')
);
</script>
复制代码
当然,这个还是需要被依赖的组件如上面代码里的./greeting.js
是编译过的jsx或者不使用jsx语法,所以依然有一定的局限性,这算是一个小遗憾。
实际上上面这个问题也不是不能解决,比如我们可以在http服务层用Babel做编译,所以有方案可以解决,留待后续话题探讨。
不依赖构建工具的Vue环境
与React类似的,Vue3本身也可以不依赖编译,直接运行。
官网推荐的ESModule写法,如下:
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
<div id="app">{{ message }}</div>
<script type="module">
import { createApp } from 'vue'
createApp({
data() {
return {
message: 'Hello Vue!'
}
}
}).mount('#app')
</script>
复制代码
但这样使用的话,依赖的模块也只能用JS,不能用Vue支持的SFC写法,这样也比较遗憾。
在Vue社区里,提供了一个sfc-loader,可以实时加载sfc文件,是一个相对比较理想的解决方案。
<div id="app"></div>
<script src="https://unpkg.com/vue@next"></script>
<script src="https://cdn.jsdelivr.net/npm/vue3-sfc-loader/dist/vue3-sfc-loader.js"></script>
<script>
const options = {
moduleCache: {
vue: Vue
},
async getFile(url) {
const res = await fetch(url);
if ( !res.ok )
throw Object.assign(new Error(res.statusText + ' ' + url), { res });
return {
getContentData: asBinary => asBinary ? res.arrayBuffer() : res.text(),
}
},
addStyle(textContent) {
const style = Object.assign(document.createElement('style'), { textContent });
const ref = document.head.getElementsByTagName('style')[0] || null;
document.head.insertBefore(style, ref);
},
}
const { loadModule } = window['vue3-sfc-loader'];
const app = Vue.createApp({
components: {
'my-component': Vue.defineAsyncComponent( () => loadModule('./myComponent.vue', options) )
},
template: '<my-component></my-component>'
});
app.mount('#app');
</script>
复制代码
此外,还有其他的一些解决方案,比如用相对简单的esbuild来构建,这也是我在showcase工具中采用的主要方案,具体的情况,在下一个话题里再详细介绍吧。