什么是webcontainer
webcontainer 是基于 WebAssembly 的操作系统,这使得 Node.js 能够完全在浏览器内部运行,并且可以在浏览器内部直接执行操作系统的命令。这也就是说,之前有些我们需要前后端合作才能完成的功能,借助 webcontainer你就可以端了后端的饭碗,顺利地晋升为“全干工程师”。
官方解释
WebContainers是一种基于浏览器的运行时,用于在浏览器标签内完全执行Node.js应用程序和操作系统命令。在WebContainers中,先前需要云虚拟机来执行用户代码的应用程序可以完全在客户端运行,并且相对于传统的云虚拟机,具有许多优点。详细请进官网了解
了解 WebAssembly
WebAssembly 是一种运行在现代网络浏览器中的新型代码,并且提供新的性能特性和效果。它设计的目的不是为了手写代码而是为诸如 C、C++和 Rust 等低级源语言提供一个高效的编译目标。
WebAssembly 是一门低级的类汇编语言。它有一种紧凑的二进制格式,使其能够以接近原生性能的速度运行,并且为诸如 C++和 Rust 等拥有低级的内存模型语言提供了一个编译目标以便它们能够在网络上运行。
深入了解 WebAssembly
webcontainer/api 的使用
目前 stackblitz 已经将 webcontainer/api 提供给公众使用,这个 npm 包可以让我们使用到 webcontainer中的一些核心的功能,比如其提供了访问虚拟文件系统的接口进行文件系统操作,还提供了接口来执行各种命令。
体验 webcontainer/api
我们可以通过 stackblitz 编辑器来体验一下 webcontainer/api。
首先我们将会进入这个界面,这是一个使用 vite 作为构建工具,使用 webcontainer/api 来简单实现将node.js代码运行在浏览器环境的项目,接下来我会仔细来解读其中的配置,以及 webcontainer/api 的使用。
files.js(浏览器需要运行的 node.js 代码)
我们先来看看本次在浏览器中运行什么样的node.js代码,没错,就是这个 files.js 文件,从files.js中的 index.js 和 package.json 文件可以看出这是一个简单的express 项目,我们接下来就是需要通过 webcontainer/api 来将这段 node.js 代码运行在浏览器环境中。
// files.js
export const files = {
'index.js': {
file: {
contents: `
import express from 'express';
const app = express();
const port = 3111;
app.get('/', (req, res) => {
res.send('Welcome to a WebContainers app! ?');
});
app.listen(port, () => {
console.log(`App is live at http://localhost:${port}`);
});`,
},
},
'package.json': {
file: {
contents: `
{
"name": "example-app",
"type": "module",
"dependencies": {
"express": "latest",
"nodemon": "latest"
},
"scripts": {
"start": "nodemon index.js"
}
}`,
},
},
};
index.html
主要提供了一个 id 为 app 的容器,以及加载了 main.js 文件,主要的功能都在 main.js 文件中实现。
main.js
1. 挂载元素
我们可以看到实例页面主要分为左右两个区域,左边是代码编辑区域,右边是页面展示区域。所以在 main.js 中定义了这两个区域,一个是输入框,另一个是 iframe,用来展示页面。
document.querySelector('#app').innerHTML = `
<div class="container">
<div class="editor">
<textarea>I am a textarea</textarea>
</div>
<div class="preview">
<iframe src="loading.html"></iframe>
</div>
</div>
`
/** @type {HTMLIFrameElement | null} */
const iframeEl = document.querySelector('iframe');
/** @type {HTMLTextAreaElement | null} */
const textareaEl = document.querySelector('textarea');
2. 加载 webcontainer 实例
页面加载后,我们首先需要加载 webconainer 实例,然后将 files.js 中的代码挂载到实例之中。
import { WebContainer } from '@webcontainer/api';
import { files } from './files';
window.addEventListener('load', async () => {
webcontainerInstance = await WebContainer.boot();
await webcontainerInstance.mount(files);
const exitCode = await installDependencies();
if (exitCode !== 0) {
throw new Error('Installation failed');
};
startDevServer();
});
3. 下载依赖
加载完代码之后,我们就需要通过代码中的 package.json 来下载代码所需的依赖了,我们需要通过webcontainer/api 提供的 spawn 方法来执行 npm 命令。
async function installDependencies() {
// Install dependencies
const installProcess = await webcontainerInstance.spawn('npm', ['install']);
installProcess.output.pipeTo(new WritableStream({
write(data) {
console.log(data);
}
}))
// Wait for install command to exit
return installProcess.exit;
}
与此同时,我们可以顺便学习一下 spawn 方法执行命令的其他例子。
1)cd 命令
cd hello-world
webcontainerInstance.spawn('cd', ['hello-world']);
2)ls 命令
ls src -l
webcontainerInstance.spawn('ls', ['src', '-l']);
4. 运行服务
依赖下载完成之后,我们就可以运行服务了,也就是前文有的 startDevServer 方法。
首先会执行 npm run start 命令,webcontainer 公开了server-ready事件,我们可以监听在服务器准备好接受请求时将 url 配置到 iframe 上面。
async function startDevServer() {
// Run `npm run start` to start the Express app
await webcontainerInstance.spawn('npm', ['run', 'start']);
// Wait for `server-ready` event
webcontainerInstance.on('server-ready', (port, url) => {
iframeEl.src = url;
});
}
此时 iframe 就可以展示你在 files 文件中配置的内容了。
5. 编辑和保存文件
输入框中需要展示 files.js 中的 index.js 中的文件内容,并且当我们修改输入框中的内容时,页面需要同步更改。
首先页面加载成功后,我们需要给输入框赋值为 files.js 中的 index.js 中的文件内容。其次,我们需要监听输入框的输入,当其改动时,我们需要同步更新 files.js 中保存的文件内容。
window.addEventListener('load', async () => {
textareaEl.value = files['index.js'].file.contents;
textareaEl.addEventListener('input', (e) => {
writeIndexJS(e.currentTarget.value);
});
});
async function writeIndexJS(content) {
await webcontainerInstance.fs.writeFile('/index.js', content);
}
vite.config.js(header 设置)
为避免跨域问题,WebContainers 要求我们的页面(即使在开发阶段)也需要使用这两个标头:
- Cross-Origin-Embedder-Policy: require-corp
- Cross-Origin-Opener-Policy: same-origin
import { defineConfig } from 'vite';
export default defineConfig({
server: {
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
},
},
});
配置调试终端
下载 xterm 依赖包
终端执行命令下载 xterm
npm install xterm
引入 xterm 样式
在 main.js 中引入终端样式
// main.js
import 'xterm/css/xterm.css';
加入终端元素 terminal
document.querySelector('#app').innerHTML = `
<div class="container">
<div class="editor">
<textarea>I am a textarea</textarea>
</div>
<div class="preview">
<iframe src="loading.html"></iframe>
</div>
<div class="terminal"></div>
</div>
`
/** @type {HTMLTextAreaElement | null} */
const terminalEl = document.querySelector('.terminal');
初始化终端
在 main.js 中初始化终端
import { Terminal } from 'xterm';
// convertEol设置为的原因true是强制光标始终从下一行的开头开始
const terminal = new Terminal({
convertEol: true,
});
window.addEventListener('load', async () => {
terminal.open(terminalEl);
});
将输出发送到终端
async function installDependencies() {
// Install dependencies
const installProcess = await webcontainerInstance.spawn('npm', ['install']);
installProcess.output.pipeTo(
new WritableStream({
write(data) {
terminal.write(data);
},
})
);
// Wait for install command to exit
return installProcess.exit;
}
async function startDevServer() {
// Run `npm run start` to start the Express app
const serverProcess = await webcontainerInstance.spawn('npm', [
'run',
'start',
]);
serverProcess.output.pipeTo(
new WritableStream({
write(data) {
terminal.write(data);
},
})
);
// Wait for `server-ready` event
webcontainerInstance.on('server-ready', (port, url) => {
iframeEl.src = url;
});
}
后记
本文是 webcontainer 的初次探究,了解怎样使用 webcontainer 将 node.js 代码运行在浏览器上,并且通过webcontainer/api 在浏览器中操作文件系统并执行命令。通过这次实践,我们可以对 webcontainer 的强大功能有一个大概的了解。后期我将会继续使用 webcontainer/api 实现更多的业务功能,有兴趣的同学可以持续关注一下。