⚠️ 不要在家尝试这个:CSS 作为后端 - 引入级联服务器表!
又来了!又来了!我保证,看完之后你一定会怀疑我的理智。
我当时正要买些杂货。正要沿着街道走向附近的商店,突然想到了什么。层叠……服务员床单!
今天,我们将使用 CSS 作为服务器端语言。没错。用 CSS 来声明路由、进行数学运算,甚至用 CSS 来创建模板!而且我们不会使用 SASS 或 LESS 之类的语言(噗,我们才不需要那些讨厌的循环!),而是使用普通的 CSS。
啥??为什么??
SMBC 最近对此进行了很好的阐述,尽管这只是一部关于量子计算机的漫画的一部分:
想象一下用哈勃望远镜换轮胎。这可不太现实,不是吗?不过,如果你真的做到了,那感觉该有多棒啊!而这正是我想要的。嘿,也许我在这里引领了一种新的潮流,谁知道呢!即使这种潮流只是嘲笑我的愚蠢想法,从此再也不把我当回事了。
你可能听说过这样一句话:“人们太执着于自己是否能做到,以至于忘了问问自己是否应该做到。” 我很清楚自己可能不应该这么做,但问题是,我能做到吗?
我绝对不会在生产环境中使用这个工具,亲爱的读者,你也别用。拜托了。好了,我已经警告过你了。
好的,它是 Cascading St...服务器表。
首先,我们来定义一下这个东西到底是怎么工作的。我之前考虑过一个 Express 接口。基本上就是在 Express 中定义一个 catch-all 路由,加载 CSS 文件,解析并解释样式(我猜这部分应该会很有趣),然后捕获通过网络传输过来的任何 DOM。
为此,我们首先安装 Express。请注意,我在这里使用nvm在 Node 版本之间切换。
echo "14" > .nvmrc
nvm use
npm init # Hit enter a few times
npm i express
太棒了!现在让我们创建一个小应用程序,并添加一个启动脚本到package.json
:
{
"name": "css-server",
"version": "1.0.0",
"description": "A bad idea.",
"main": "index.js",
"scripts": {
"start": "node ./css-server.js"
},
"author": "Pascal Thormeier",
"license": "donttrythisathome",
"dependencies": {
"express": "^4.17.2"
}
}
在 express 应用中,我们定义了一个 catch-all 路由,它会尝试判断给定的路由是否对应一个 CSS 文件。如果存在,则直接返回该文件的内容;如果不存在,则抛出 404 错误。
const express = require('express')
const bodyParser = require('body-parser')
const path = require('path')
const fs = require('fs')
const app = express()
// Allows to get POST bodies as JSON
app.use(bodyParser.urlencoded({ extended: true }))
// Catch-all route
app.use((req, res) => {
let cssFile = req.path
// So `index.css` works.
if (cssFile.endsWith('/')) {
cssFile += 'index'
}
const cssFilePath = path.resolve('./app' + cssFile + '.css')
try {
const css = fs.readFileSync(cssFilePath, 'utf8')
res.send(css)
} catch (e) {
// Any error of the file system will
// be caught and treated as "not found"
res.sendStatus(404)
}
})
app.listen(3000)
快速测试表明,除小index.css
文件外,所有内容都会产生 404;CSS 文件会显示出来。
评估 CSS - 大声思考
好的,有趣的部分来了。我们需要弄清楚如何在服务器端执行 CSS,并将其输出作为应用程序的响应。
关于渲染,首先想到的就是直接使用 CSScontent
规则来渲染内容。它可以使用 CSS 变量和计数器,所以从技术上来说,我们甚至可以用它来做数学运算。但有一个问题:浏览器会动态地计算计数器和变量,所以我们不能直接计算 CSS,直接取其中的内容content
并输出。所以,“计算样式”的方法行不通。(相信我,我试过了……)
基本上,您将获得在开发工具的“CSS”选项卡中看到的内容。
想象一下这段 CSS:
body {
--num1: 12;
--num2: 13;
counter-set: sum 15;
}
body::before {
content: '<h1>The sum is ' counter(sum) '</h1>';
}
您将获得以下内容:
嗯。那我们为什么不直接用浏览器来做呢?浏览器确实会以某种方式评估这些东西,对吧?唯一的问题是,我们把问题转移了。Node 有CSS的实现。它们提供计算样式,而我们使用的浏览器也只会提供同样的东西,对吧?要是能有办法让电脑“读”屏幕上的内容就好了。
理想情况下,浏览器会加载 CSS 文件,我们不会内联任何内容;否则我们就无法真正使用类似@import
. 所以我们需要另一个加载 CSS 文件的控制器。
无论如何,这听起来很像“未来的我”的问题。我们先来介绍一下 puppeteer,让它执行 CSS。
添加木偶
直截了当:
npm i -s puppeteer
要加载 CSS,我们需要一些 HTML。我们可以动态创建 HTML,将加载的 CSS 注入为<link>
,对整个 blob 进行 base64 编码,然后让浏览器解析:
const escapeVarValue = value => {
if (!isNaN(value)){
return value
}
return `'${value}'`
}
const createDOM = (cssFilePath, method, args) => {
const varifiedArgs = Object.entries(args).map(([key, value]) => `--${key}: ${escapeVarValue(value)};\n`).join("\n")
const dataifiedArgs = Object.entries(args).map(([key, value]) => `data-${key}="${value}"`).join(' ')
return `
<!DOCTYPE html>
<html data-http-method="${method.toUpperCase()}">
<head>
<style>
:root {
${varifiedArgs}
}
</style>
<!-- Load the actual CSS -->
<link rel="stylesheet" href="${cssFilePath}">
</head>
<body ${dataifiedArgs}>
</body>
</html>
`
}
请注意我们已经将 HTTP 方法添加为数据属性,并将任何参数添加为 CSS 变量和数据属性。
接下来,我们将_internal
路由添加到我们的 express 应用程序,以提供所请求的 CSS 文件:
app.get('/_internal/*', (req, res) => {
const appPath = req.path.replace('_internal', 'app')
if (appPath.includes('..') || !appPath.endsWith('.css')) {
res.send('Invalid file')
return
}
const internalFilePath = path.resolve('.' + appPath)
res.sendFile(internalFilePath)
})
然后,请求/_internal/index.css
就会加载app/index.css
并执行。Puppeteer 现在可以加载并执行我们的应用代码了。我们可以在这里进行更多验证,但为了简单起见,我在这里只进行了简单的验证。
现在让木偶师加入游戏:
const getContent = async (cssPath, method, args) => {
const dom = createDOM(cssPath, method, args)
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
})
const page = await browser.newPage()
const base64Html = Buffer.from(dom).toString('base64')
await page.goto('data:text\/html;base64;charset=UTF-8,' + base64Html, {
waitUntil: 'load',
timeout: 300000,
waitFor: 30000,
})
// Magic!
}
让我们尝试一下基本的小方法index.css
:
body::after {
content: '<h1>Hello, World!</h1>';
}
瞧!成功了!Puppeteer 执行 CSS 并显示结果:
很棒的副作用:改成headless: true
这样false
我们就可以调试 CSS 了。一个开箱即用的调试器绝对是个好东西。
提取内容
还记得“未来的我”的问题吗?是的。
我们知道,无法使用计算样式来获取任何元素的content
,尤其是当它包含变量或计数器时。我们也无法选择并复制/粘贴渲染后的文本,因为 Chromium 无法做到这一点。那么,我们如何获取渲染后的、求值的文本呢?
曾经下载过网站的 PDF 文件吗?执行后的文本是可选的。Puppeteer 可以从网站创建 PDF 文件吗?是的,可以。我们能以某种方式解析 PDF 文件来获取文本吗?当然可以!
npm i -s pdf-parse
这个库允许我们解析任何给定的 PDF 并提取其中的文本。我们不会在这里对图像、布局之类的进行任何改动。我们只会将普通的 HTML 渲染为未解析的字符串。我们可以复制/粘贴以下内容:
const pdf = require('pdf-parse')
const getContent = async (cssPath, method, args) => {
const dom = createDOM(cssPath, method, args)
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
})
const page = await browser.newPage()
const base64Html = Buffer.from(dom).toString('base64')
await page.goto('data:text\/html;base64;charset=UTF-8,' + base64Html,{
waitUntil: 'load',
timeout: 300000,
waitFor: 30000,
})
// Get a PDF buffer
const pdfBuffer = await page.pdf()
// Parse the PDF
const renderedData = await pdf(pdfBuffer)
// Get the PDFs text
return Promise.resolve(renderedData.text)
}
最后一步,让我们调整 catch-all 路由以获取文本:
// Catch-all route
app.use((req, res) => {
let cssFile = req.path
// So `index.css` works.
if (cssFile.endsWith('/')) {
cssFile += 'index'
}
cssFile += '.css'
// File doesn't exist, so we break here
if (!fs.existsSync(path.resolve('./app/' + cssFile))) {
res.sendStatus(404)
return
}
const cssFilePath = 'http://localhost:3000/_internal' + cssFile
getContent(cssFilePath, req.method, {
...req.query, // GET parameters
...req.body, // POST body
}).then(content => {
res.send(content)
})
})
这应该可以解决问题。
演示时间!
让我们来测试一下这个东西。
使用表单的计算器
基本的“Hello World”已经足够简单了。让我们来构建一个 CSS 计算器:
body {
--title: '<h1>Calculator:</h1>';
--form: '<form method="POST" action="/"><div><label for="num1">Number 1</label><input id="num1" name="num1"></div><div><label for="num2">Number 2</label><input id="num2" name="num2"></div><button type="submit">Add two numbers</button></form>';
}
[data-http-method="POST"] body {
counter-set: sum var(--num1, 0) val1 var(--num1, 0) val2 var(--num2, 0);
}
[data-http-method="GET"] body::before {
content: var(--title) var(--form);
}
[data-http-method="POST"] body::before {
--form: '<form method="POST" action="/"><div><label for="num1">Number 1</label><input id="num1" name="num1" value="' counter(val1) '"></div><div><label for="num2">Number 2</label><input id="num2" name="num2" value="' counter(val2) '"></div><button type="submit">Add two numbers</button></form>';
counter-increment: sum var(--num2, 0);
content: var(--title) var(--form) '<div>Result: ' counter(sum) '</div>';
}
该计算器使用多种功能:
- 对 GET 和 POST 的反应
- 做数学
- 显示结果
那么,这实际上有什么作用?
我们渲染一个标题和一个包含两个输入字段的表单,分别为num1
和num2
。如果这个“应用”遇到 POST 请求,它会显示结果,该结果由 CSS 计数器计算得出。CSS 计数器首先设置为num1
,然后增加num2
,得出两个数字的和。因此:一个基本的加法计算器。
这有效吗?确实有效:
带有导航的简单两页应用程序
让我们将一些页眉和页脚抽象到一个globals.css
文件中:
:root {
--navigation: '<ul><li><a href="/">Home</a></li><li><a href="/about">About</a></li></ul>';
--footer: '<footer>© 2022</footer>';
}
然后我们可以像index.css
这样使用它:
@import "./globals.css";
body::after {
content: var(--navigation) '<h1>Hello, World!</h1>' var(--footer);
}
效果非常好:
呼。真是一段奇妙的旅程。
编辑:既然这显然引起了一些困惑,那就让我解释一下为什么我在这个项目中主要使用了 JS,即使标题里说的是 CSS。每种编程语言的执行都会经过用其他语言编写的解释器或编译器。例如,NodeJS 最初是用 C/C++ 编写的。我在这里构建的 CSS 服务器与之类似:我使用 JS 来执行 CSS。CSS 是 CSS 服务器的用户态代码,就像 JS 是 Node 的用户态代码一样。
希望你喜欢阅读这篇文章,就像我喜欢写它一样!如果喜欢,请留下❤️或🦄 !我空闲时间会写科技文章,偶尔也喜欢喝咖啡。
如果你想支持我的努力, 可以请我喝杯咖啡☕ ,或者 在推特上关注我🐦 ! 你也可以直接通过PayPal支持我!
文章来源:https://dev.to/thormeier/dont-try-this-at-home-css-as-the-backend-what-3oih