【问题标题】:"SyntaxError: Invalid or unexpected token" - How to SSR images/css with react“SyntaxError: Invalid or unexpected token” - 如何对 SSR 图像/css 做出反应
【发布时间】:2021-08-11 09:57:48
【问题描述】:

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

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()
  })
}

互联网上几乎所有的博客文章和示例都停在那里说“好吧,工作完成了”,但事实并非如此,这甚至还不到故事的一半。

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

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

在该组件内部,它会中断,因为 typescript 无法解析任何图像,这是有道理的。它也会破坏任何 CSS 或其他任何没有反应的东西。这意味着您不能重用任何客户端组件并在服务器上重新渲染它们,如果它们包含任何图像或任何 CSS 或任何不是纯 typescript/react 的东西,则不能。

你可以使用:

import webpack from 'webpack'

import config.js from './config'

...

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

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

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

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

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

我错过了什么拼图,我错过了什么非常愚蠢或明显的东西吗?

【问题讨论】:

    标签: reactjs typescript server-side-rendering


    【解决方案1】:

    我终于想通了,似乎我有拼图,但我以所有错误的方式将它们拼凑在一起。我会发布完整的解决方案,以防有人需要。

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

    您首先需要的是您习惯的常规 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-loader或style-loader或其他任何东西。

    然后假设你有一个这样的目录结构:

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

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

    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/已经使用 webpack 编译的任何内容。这里需要注意的是不要无缘无故地创建不同的 webpack 配置,你需要改变的只是节点的目标、入口和输出,你可以使用原始的 webpack 配置来做到这一点。

    然后您可以在构建步骤中包含此编译器,因此每次启动服务器时都使用tsc &amp;&amp; node lib/compiler.js &amp;&amp; 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 文件,这很重要,不要尝试在此处包含打字稿/原始 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 文件,这是客户端 webpack 与水合物的捆绑包,一旦命中客户端的浏览器,它将对之前流式传输的标记进行水合。

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

    这里的关键是使用 webpack 创建自定义编译器并覆盖入口和输出文件,然后在构建它们之后在反应服务器函数中使用这些文件。

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

    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 爱好者,我现在...

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-02-17
      • 1970-01-01
      • 1970-01-01
      • 2017-02-21
      • 1970-01-01
      相关资源
      最近更新 更多