網站搜尋

reactjs - "SyntaxError: 無效或意外的符號" - 如何在 react 中對圖片/css 進行伺服器端渲染 (SSR)


我正在使用 SSR 來渲染一個應該導入圖片的 React 元件。我已經添加了:

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

這有效。然後當我嘗試做:

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

我收到一個錯誤「SyntaxError: Invalid or unexpected token」。即使我根本沒有嘗試使用它,僅僅是在導入時就會出現這個錯誤:

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 的情況下使用 import 來使用圖片。

我開始覺得,實現這個的唯一方法就是手動使用 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 或其他非 React 的東西,它也會出錯。這意味著你不能重複使用任何客戶端組件並在服務器上重新渲染它們,如果它們包含任何圖片、任何 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 預編譯它,並在每個預渲染的 HTML 上運行 webpack 編譯器,在 fs.readFile 該 *.html 並將其輸入到 react 的渲染之前,根本不可能以任何有用的方式運行或使用 react-dom/server。

你也不能只是手動使用 <img src='/path/to/image'/>,因為那樣你的伺服器和客戶端代碼就不一致,所以 hydrate 會報錯,因為 webpack 渲染圖片的方式跟你手動做的不一樣,更不用說你不能用像 import Component from './component' 這樣的方式重用相同的組件,你將不得不重複整個代碼庫,一次為 webpack 客戶端,一次為手動寫圖片路徑。

那麼你如何簡單地使用 renderToString 來呈現圖像呢,我不明白為什麼外面沒有任何人、沒有任何博客文章、討論論壇或任何東西有關於這個的資訊?!更重要的是,為什麼每個人都只是說「哦,只要使用 renderToString 就好了」,而實際上它明顯對圖像/CSS 或其他任何東西都不起作用。

我缺少的拼圖是什麼,我是不是錯過了一些非常愚蠢或明顯的東西?

答案#1

我終於弄清楚這個了,看起來我手上有拼圖的碎片,但我一直在以完全錯誤的方式將它們拼在一起。我會發布完整的解答,以防有人以後想要它。

首先要提到的是,在沒有像 webpack 這樣的打包工具的情況下使用任何 React 伺服器功能是不可能的,或者說是可能的,但這樣做沒有意義,因為你會有 TypeScript 無法解析的圖片、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-loader 或 style-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 檔案,每個檔案都有自己已用 webpack 編譯好的 CSS/圖片/任意資源。這裡需要注意的是,不要無緣無故地創建不同的 webpack 配置,你只需要更改目標為 node、入口和輸出,即可使用原有的 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()
  })
}

將 webpack 輸出的檔案與 React 一起渲染,並以串流方式發送給請求的客戶端。請注意,在上述情況中,HTML 應該是一個使用 webpack 編譯器輸出的 .js 檔案,這很重要,不要試圖在這裡包含 TypeScript 或原始的 JavaScript 元件。因此,你需要先使用 webpack 編譯它,然後將該 JavaScript 輸出傳遞給 API 中的 React 伺服器功能。需要注意的是,上述範例是一個 Express 伺服器。

這是使用 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

那麼唯一剩下要做的就是對原本的客戶端 webpack UI 進行 hydrate。

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 bundle,一旦這個文件到達客戶端的瀏覽器,它將對先前流式傳輸的標記進行 hydrate。

恭喜,你現在有一個提供 HTML 的 API,而不需要任何預先渲染的 HTML 檔案!

關鍵在於使用 webpack 創建自訂的編譯器,並覆寫入口和輸出檔案,然後在這些檔案被構建完成 之後,在 React 伺服器函數中使用它們。

編輯:編譯器性能略有提升。你可以這樣運行多個編譯器,每個都有自己的配置。

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 真正能力的一整罐新蟲子。

答案#2

要解決伺服器端 node.js 關於 CSS 的錯誤「SyntaxError: Invalid or unexpected token」,請以開發依賴安裝 css-modules-transform

npm install css-modules-transform --save-dev

並在 .bablerc 中添加

{
"plugins": ["css-modules-transform"]
}

要解決伺服器端 node.js 中與檔案相關的錯誤「SyntaxError: Invalid or unexpected token」,請以開發依賴方式安裝 babel-plugin-file-loader

npm install babel-plugin-file-loader --save-dev

並在 .bablerc 中添加

{
"plugins": ["css-modules-transform", "file-loader"]
}