⚠️ 不要在家尝试这个:CSS 作为后端 - 引入级联服务器表!

2025-05-28

⚠️ 不要在家尝试这个: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


Enter fullscreen mode Exit fullscreen mode

太棒了!现在让我们创建一个小应用程序,并添加一个启动脚本到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"
  }
}


Enter fullscreen mode Exit fullscreen mode

在 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)


Enter fullscreen mode Exit fullscreen mode

快速测试表明,除小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>';
}


Enter fullscreen mode Exit fullscreen mode

您将获得以下内容:

一个打开检查器的浏览器窗口,显示上面提到的确切内容。

嗯。那我们为什么不直接用浏览器来做呢?浏览器确实会以某种方式评估这些东西,对吧?唯一的问题是,我们把问题转移了。Node 有CSS的实现。它们提供计算样式,而我们使用的浏览器也只会提供同样的东西,对吧?要是能有办法让电脑“读”屏幕上的内容就好了。

理想情况下,浏览器会加载 CSS 文件,我们不会内联任何内容;否则我们就无法真正使用类似@import. 所以我们需要另一个加载 CSS 文件的控制器。

无论如何,这听起来很像“未来的我”的问题。我们先来介绍一下 puppeteer,让它执行 CSS。

添加木偶

直截了当:



npm i -s puppeteer


Enter fullscreen mode Exit fullscreen mode

要加载 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>
  `
}


Enter fullscreen mode Exit fullscreen mode

请注意我们已经将 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)
})


Enter fullscreen mode Exit fullscreen mode

然后,请求/_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!
}


Enter fullscreen mode Exit fullscreen mode

让我们尝试一下基本的小方法index.css



body::after {
  content: '<h1>Hello, World!</h1>';
}


Enter fullscreen mode Exit fullscreen mode

瞧!成功了!Puppeteer 执行 CSS 并显示结果:

浏览器将上述 CSS 显示为渲染文本。

很棒的副作用:改成headless: true这样false我们就可以调试 CSS 了。一个开箱即用的调试器绝对是个好东西。

提取内容

还记得“未来的我”的问题吗?是的。

我们知道,无法使用计算样式来获取任何元素的content,尤其是当它包含变量或计数器时。我们也无法选择并复制/粘贴渲染后的文本,因为 Chromium 无法做到这一点。那么,我们如何获取渲染后的、求值的文本呢?

曾经下载过网站的 PDF 文件吗?执行后的文本是可选的。Puppeteer 可以从网站创建 PDF 文件吗?是的,可以。我们能以某种方式解析 PDF 文件来获取文本吗?当然可以



npm i -s pdf-parse


Enter fullscreen mode Exit fullscreen mode

这个库允许我们解析任何给定的 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)
}


Enter fullscreen mode Exit fullscreen mode

最后一步,让我们调整 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)
  })
})


Enter fullscreen mode Exit fullscreen mode

应该可以解决问题。

演示时间!

让我们来测试一下这个东西。

使用表单的计算器

基本的“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>';
}


Enter fullscreen mode Exit fullscreen mode

该计算器使用多种功能:

  • 对 GET 和 POST 的反应
  • 做数学
  • 显示结果

那么,这实际上有什么作用?

我们渲染一个标题和一个包含两个输入字段的表单,分别为num1num2。如果这个“应用”遇到 POST 请求,它会显示结果,该结果由 CSS 计数器计算得出。CSS 计数器首先设置为num1,然后增加num2,得出两个数字的和。因此:一个基本的加法计算器。

这有效吗?确实有效:

浏览器窗口显示一个简单的计算器,左侧的终端显示 DOM。

同一个计算器,显示结果

带有导航的简单两页应用程序

让我们将一些页眉和页脚抽象到一个globals.css文件中:



:root {
    --navigation: '<ul><li><a href="/">Home</a></li><li><a href="/about">About</a></li></ul>';
    --footer: '<footer>&copy; 2022</footer>';
}


Enter fullscreen mode Exit fullscreen mode

然后我们可以像index.css这样使用它:



@import "./globals.css";

body::after {
    content: var(--navigation) '<h1>Hello, World!</h1>' var(--footer);
}


Enter fullscreen mode Exit fullscreen mode

效果非常好:

一个简单的页面,带有导航、标题和页脚,没有样式。

呼。真是一段奇妙的旅程。

编辑:既然这显然引起了一些困惑,那就让我解释一下为什么我在这个项目中主要使用了 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
PREV
感觉自己像个秘密特工:用隐写术在图像中隐藏信息🖼️🕵️‍♀️
NEXT
为 Vue 构建自己的所见即所得 Markdown 编辑器 📝👀 入门 将“富文本”放入“富文本” 添加 Markdown 总结想法