"SyntaxError: Invalid or unexpected token" - How to SSR images/css with react

"SyntaxError: Invalid or unexpected token" - How to SSR images/css with react

我正在使用 SSR 渲染一个应该导入图像的 React 组件。我添加了:

declare module '*.ico' {
  const value: any
  export default value
}

哪个有效。然后当我尝试做时:

import Favicon from '/path/to/favicon.ico'

我收到错误“SyntaxError:无效或意外的标记”。即使我什至不尝试使用它,我也会收到此错误,它会在导入本身中断:

SyntaxError: Invalid or unexpected token
    at wrapSafe (internal/modules/cjs/loader.js:979:16)
    at Module._compile (internal/modules/cjs/loader.js:1027:27)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
    at Module.load (internal/modules/cjs/loader.js:928:32)
    at Function.Module._load (internal/modules/cjs/loader.js:769:14)
    at Module.require (internal/modules/cjs/loader.js:952:19)
    at require (internal/modules/cjs/helpers.js:88:18)

我会问我如何使用它,但在我尝试使用它之前,我首先坚持只是导入它。

注意,在渲染单个组件服务器端时,我不想重新运行 webpack 或类似的东西,所以我如何在不使用 webpack 的情况下使用带有导入的图像。

我开始认为实现此目的的唯一方法是手动使用 fs.writeFile 并将静态 URL 手动分配给图像源,但我希望有更简单的解决方案除此之外,我认为 webpack 正在做所有这些事情。

编辑:我以为我找到了解决这个问题的方法,但仍然没有运气。

你可以有这样的东西,它可以渲染一个页面:

export const component = async (
  request: Request,
  response: Response,
): Promise<void> => {
  console.log(request.url)

  const stream = renderToNodeStream(<Component />)

  stream.pipe(response)

  stream.on('end', () => {
    response.end()
  })
}

几乎每个博客 post 和 Internet 上的示例都停在那里并说“好的,工作完成”,但事实并非如此,这甚至还不到故事的一半。

当您将以上内容与以下内容一起使用时:

import Favicon from '/path/to/favicon.ico'

在那个组件里面,然后它会崩溃,因为打字稿无法解析任何图像,这是有道理的。它也会在任何 CSS 或任何其他没有反应的情况下中断。这意味着您不能重用任何客户端组件并在服务器上重新呈现它们,如果它们包含任何图像或任何 CSS 或任何不纯的 typescript/react.[=26 则不行=]

您可以使用:

import webpack from 'webpack'

import config.js from './config'

...

const compiler = webpack(config('production'))

compiler.run((error, stats) => {
  compiler.close()
})

使用它您可以使用 webpack 编译和捆绑特定文件并以编程方式调用它,但这并不能真正解决问题。我不明白为什么那里的每个示例都只显示 renderToStaticMarkuprenderToString 而其中没有一个告诉您“哦,顺便说一句,在您拥有的任何东西上使用它都会破坏。”如果您不使用 webpack 和 运行 在每个预呈现的 HTML 上使用 webpack 编译器预编译它,则不可能以任何有用的方式 运行 或使用或利用 react-dom/server fs.readFile'ing *.html 并将其提供给 React 的渲染器。

你也不能只是手动使用 <img src='/path/to/image'/> 因为那样你的服务器和客户端代码将不一样所以 hydrate 会抱怨,因为 webpack 渲染图像的方式不同于你是手动做的,更不用说你不能用 import Component from './component' 之类的东西重用相同的组件,你必须复制整个代码库,一次用于 webpack 客户端,一次用于手动编写图像路径。

那么你怎么能简单地使用 renderToString 渲染图像,我不明白那里怎么没有人,没有博客 post 也没有论坛或什么都没有这方面的任何信息? !不仅如此,为什么每个人都简单地说“哦,只需使用 renderToString 瞧”,而它显然不适用于 images/css 或其他任何东西。

我遗漏了什么拼图,我是不是遗漏了一些非常愚蠢或明显的东西?

我终于想通了,好像我有拼图,但我以错误的方式将它们拼在一起。我会 post 完整的解决方案,以防有人需要它。

首先要提到的是 在没有像 webpack 这样的捆绑器的情况下使用任何反应服务器功能是不可能的,或者它是可能的但没有意义因为你会有图像和 css 以及打字稿无法解析的东西。

您首先需要的是您习惯的普通 webpack 配置:

const config = {
  output: {
    path: path.join(__dirname, 'public'),
    filename: '[contenthash].js',
    assetModuleFilename: '[contenthash].[ext]',
    publicPath: '/',
    libraryTarget: 'umd',
    clean: false,
  },
  target: 'web',
  resolve: {
    extensions: ['.js', '.ts', '.jsx', '.tsx'],
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: [/node_modules/],
      },
      {
        test: /\.(jpg|png|ico)$/,
        type: 'asset/resource',
        generator: {
          filename: '[contenthash].[ext]',
        },
      },
    ],
  },
  ...

这不是完整的配置,但它会给出一个大概的概念。在那里你可以使用所有你习惯的普通插件和东西,即 file-loaderstyle-loader 或其他任何东西。

之后假设您的目录结构如下:

- webpack.config.js
- ui/route // we want all files here to be SSR and client side rendered

然后您可以像往常一样捆绑所有这些,并使用您的常规 webpack 设置在客户端提供服务。但是除此之外,您还需要某种编译器来编写 .js 文件,以便被 React 服务器函数使用。这是关键部分。

import fs from 'fs'
import path from 'path'
import webpack from 'webpack'

import config from '../webpack.config.js'

const configuration = config({production: true})

const files = fs.readdirSync(path.join(__dirname, 'ui/route'))

for (const file of files) {
  if (file.indexOf('.map') === -1) {
    configuration.target = 'node'
    configuration.entry = path.join(__dirname, 'ui/route', file)
    configuration.output.path = path.join(__dirname, '../public')
    configuration.output.filename = file // [contenthash].js

    const compiler = webpack(configuration)
    compiler.run((error) => {
      console.log(error)

      compiler.close((error) => {
        console.log(error)
      })
    })
  }
}

该代码非常原始,仍然可以通过许多不同的方式进行优化,但它向您展示了您可以做什么。您可以单独处理每个文件,也可以创建某种 glob 来读取特定目录中的所有内容(或递归搜索),然后以编程方式调用 webpack 编译器并分别输出每个文件(页面(路由))到它们的目录为 .js 个文件,每个文件都有自己的 CSS/Images/whatever 已经用 webpack 编译。这里需要注意的是,不要无缘无故地创建不同的webpack配置,你只需要改变节点的目标,入口和输出,你可以使用原始的webpack配置来做。

然后您可以在构建步骤中包含此编译器,因此每次启动服务器时都使用 tsc && node lib/compiler.js && lib/server.js 之类的东西。这将构建你所有的东西,之后它将调用所有这些东西的 webpack 并输出这些文件,然后你启动服务器。你也可以动态地做,这取决于你。这里重要的是 typescript 应该只编译你的服务器端文件,而 webpack(使用 ts-loader 调用 typescript)应该编译你的 UI 文件。

之后你可以使用:

// api/example.tsx

import HTML from './html' // the html.js file we just compiled with webpack

export const index = async (
  request: Request,
  response: Response,
): Promise<void> => {
  console.log(request.url)

  const stream = renderToNodeStream(<HTML />)

  stream.pipe(response)

  stream.on('end', () => {
    response.end()
  })
}

使用 React 呈现 webpack 输出文件并将它们作为流发送到请求客户端。请注意,在上述情况下 HTML 应该是使用 webpack 的编译器输出的 .js 文件, 这很重要 ,不要尝试包含 typescript/original javascript 组件在这里。所以你想用 webpack 编译它,然后将 javascript 输出提供给 API 中的 React 服务器函数。注意上面是快递服务器。

这是使用 webpack 编译之前的原始 HTML typescript 组件:

import React from 'react'

const HTML = (): JSX.Element => {
  return (
    <>
      <html>
        <head>
          <meta charSet='utf-8' />
          <meta name='robots' content='index, follow' />
          <meta name='viewport' content='width=device-width, initial-scale=1' />
          <title>Title</title>
          <script src='/script.js' defer></script>
        </head>
        <body>
          <div id='ui'></div>
        </body>
      </html>
    </>
  )
}

export default HTML

然后唯一剩下的就是hydrate原始客户端webpack UI.

import React from 'react'
import {hydrate} from 'react-dom'

import HTML from './html'

const UI = () => {
  return <HTML/>
}

hydrate(<UI />, document.getElementById('ui'))

或者你可以这样做:

import React from 'react'
import {render} from 'react-dom'

const UI = () => {
  return <>Hello World!</>
}

render(<UI />, document.getElementById('ui'))

现在,当我请求 '/' 时,服务器将开始将 HTML string 流式传输到 UI,之后它将获取 script.js 文件,这是带有 hydrate 的客户端 webpack 捆绑包,一旦它到达客户端的浏览器,它将对之前流式传输的标记进行 hydrate。

恭喜,现在您有一个 API 服务 HTML,没有任何预呈现的 html 文件!

这里的关键是使用 webpack 创建自定义编译器并覆盖入口和输出文件,然后在 react 服务器函数中使用这些文件 after 他们已经建成了。

编辑:编译器性能略有改进。您可以 运行 多个这样的编译器,每个编译器都有自己的配置。

var webpack = require('webpack');

var config1 = {
  entry: './index1.js',
  output: {filename: 'bundle1.js'}
}
var config2 = {
  entry: './index2.js',
  output: {filename:'bundle2.js'}
}

webpack([config1, config2], (err, stats) => {
  process.stdout.write(stats.toString() + "\n");
})

如果您想更进一步,您可以使用 https://www.npmjs.com/package/memfs:

将文件写入内存而不是磁盘
import {fs} from 'memfs'

...

compiler.outputFileSystem = fs

...

const content = fs.readFileSync('...')

https://webpack-v3.jsx.app/api/compiler/#watching

这就是 webpack-dev-server 在幕后使用的东西。

如果我以前不是 webpack 的狂热者,我现在是...这打开了一个全新的 webpack 真正能力的蠕虫罐头。