中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Vue中怎么搭建一個服務端渲染項目

發布時間:2021-07-21 13:51:52 來源:億速云 閱讀:186 作者:Leah 欄目:web開發

這篇文章給大家介紹Vue中怎么搭建一個服務端渲染項目,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。

客戶端渲染過程

  1.  訪問客戶端渲染的網站。

  2.  服務器返回一個包含了引入資源語句和 <div id="app"></div> 的 HTML 文件。

  3.  客戶端通過 HTTP 向服務器請求資源,當必要的資源都加載完畢后,執行 new Vue() 開始實例化并渲染頁面。

服務端渲染過程

  1.  訪問服務端渲染的網站。

  2.  服務器會查看當前路由組件需要哪些資源文件,然后將這些文件的內容填充到 HTML 文件。如果有 asyncData() 函數,就會執行它進行數據預取并填充到 HTML 文件里,最后返回這個 HTML 頁面。

   3.  當客戶端接收到這個 HTML 頁面時,可以馬上就開始渲染頁面。與此同時,頁面也會加載資源,當必要的資源都加載完畢后,開始執行 new Vue() 開始實例化并接管頁面。

從上述兩個過程中,可以看出,區別就在于第二步。客戶端渲染的網站會直接返回 HTML 文件,而服務端渲染的網站則會渲染完頁面再返回這個 HTML 文件。

這樣做的好處是什么?是更快的內容到達時間 (time-to-content)。

假設你的網站需要加載完 abcd 四個文件才能渲染完畢。并且每個文件大小為 1 M。

這樣一算:客戶端渲染的網站需要加載 4 個文件和 HTML 文件才能完成首頁渲染,總計大小為 4M(忽略 HTML 文件大小)。而服務端渲染的網站只需要加載一個渲染完畢的 HTML 文件就能完成首頁渲染,總計大小為已經渲染完畢的 HTML 文件(這種文件不會太大,一般為幾百K,我的個人博客網站(SSR)加載的 HTML 文件為 400K)。這就是服務端渲染更快的原因。

客戶端接管頁面

對于服務端返回來的 HTML 文件,客戶端必須進行接管,對其進行 new Vue() 實例化,用戶才能正常使用頁面。

如果不對其進行激活的話,里面的內容只是一串字符串而已,例如下面的代碼,點擊是無效的:

<button @click="sayHi">如果不進行激活,點我是不會觸發事件的</button>

那客戶端如何接管頁面呢?下面引用一篇文章中的內容:

客戶端 new Vue() 時,客戶端會和服務端生成的DOM進行Hydration對比(判斷這個DOM和自己即將生成的DOM是否相同(vuex store 數據同步才能保持一致)

如果相同就調用app.$mount('#app')將客戶端的vue實例掛載到這個DOM上,即去“激活”這些服務端渲染的HTML之后,其變成了由Vue動態管理的DOM,以便響應后續數據的變化,即之后所有的交互和vue-router不同頁面之間的跳轉將全部在瀏覽器端運行。

如果客戶端構建的虛擬 DOM 樹與服務器渲染返回的HTML結構不一致,這時候,客戶端會請求一次服務器再渲染整個應用程序,這使得SSR失效了,達不到服務端渲染的目的了

小結

不管是客戶端渲染還是服務端渲染,都需要等待客戶端執行 new Vue() 之后,用戶才能進行交互操作。但服務端渲染的網站能讓用戶更快的看見頁面。

從零開始搭建 SSR 項目

配置 weback

webpack 配置文件共有 3 個:

  1. 鴻蒙官方戰略合作共建——HarmonyOS技術社區

  2.  webpack.base.config.js,基礎配置文件,客戶端與服務端都需要它。

  3.  webpack.client.config.js,客戶端配置文件,用于生成客戶端所需的資源。

  4.  webpack.server.config.js,服務端配置文件,用于生成服務端所需的資源。

webpack.base.config.js 基礎配置文件

const path = require('path')  const { VueLoaderPlugin } = require('vue-loader')  const isProd = process.env.NODE_ENV === 'production'  function resolve(dir) {      return path.join(__dirname, '..', dir)  }  module.exports = {      context: path.resolve(__dirname, '../'),      devtool: isProd ? 'source-map' : '#cheap-module-source-map',      output: {          path: path.resolve(__dirname, '../dist'),          publicPath: '/dist/',          // chunkhash 同屬一個 chunk 中的文件修改了,文件名會發生變化           // contenthash 只有文件自己的內容變化了,文件名才會變化          filename: '[name].[contenthash].js',          // 此選項給打包后的非入口js文件命名,與 SplitChunksPlugin 配合使用          chunkFilename: '[name].[contenthash].js',      },      resolve: {          extensions: ['.js', '.vue', '.json', '.css'],          alias: {              public: resolve('public'),              '@': resolve('src')          }      },      module: {          // https://juejin.im/post/6844903689103081485          // 使用 `mini-css-extract-plugin` 插件打包的的 `server bundle` 會使用到 document。          // 由于 node 環境中不存在 document 對象,所以報錯。          // 解決方案:樣式相關的 loader 不要放在 `webpack.base.config.js` 文件          // 將其分拆到 `webpack.client.config.js` 和 `webpack.client.server.js` 文件          // 其中 `mini-css-extract-plugin` 插件要放在 `webpack.client.config.js` 文件配置。          rules: [              {                  test: /\.vue$/,                  loader: 'vue-loader',                  options: {                      compilerOptions: {                          preserveWhitespace: false                      }                  }              },              {                  test: /\.js$/,                  loader: 'babel-loader',                  exclude: /node_modules/              },              {                  test: /\.(png|svg|jpg|gif|ico)$/,                  use: ['file-loader']              },              {                  test: /\.(woff|eot|ttf)\??.*$/,                  loader: 'url-loader?name=fonts/[name].[md5:hash:hex:7].[ext]'              },          ]      },      plugins: [new VueLoaderPlugin()],  }

基礎配置文件比較簡單,output 屬性的意思是打包時根據文件內容生成文件名稱。module 屬性配置不同文件的解析 loader。

webpack.client.config.js 客戶端配置文件

const webpack = require('webpack')  const merge = require('webpack-merge')  const base = require('./webpack.base.config')  const CompressionPlugin = require('compression-webpack-plugin')  const WebpackBar = require('webpackbar')  const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')  const MiniCssExtractPlugin = require('mini-css-extract-plugin')  const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')  const isProd = process.env.NODE_ENV === 'production'  const plugins = [      new webpack.DefinePlugin({          'process.env.NODE_ENV': JSON.stringify(              process.env.NODE_ENV || 'development'          ),          'process.env.VUE_ENV': '"client"'      }),      new VueSSRClientPlugin(),      new MiniCssExtractPlugin({          filename: 'style.css'      })  ]  if (isProd) {      plugins.push(          // 開啟 gzip 壓縮 https://github.com/woai3c/node-blog/blob/master/doc/optimize.md          new CompressionPlugin(),          // 該插件會根據模塊的相對路徑生成一個四位數的hash作為模塊id, 用于生產環境。          new webpack.HashedModuleIdsPlugin(),          new WebpackBar(),      )  }  const config = {      entry: {          app: './src/entry-client.js'      },      plugins,      optimization: {          runtimeChunk: {              name: 'manifest'          },          splitChunks: {              cacheGroups: {                  vendor: {                      name: 'chunk-vendors',                      test: /[\\/]node_modules[\\/]/,                      priority: -10,                      chunks: 'initial',                  },                  common: {                      name: 'chunk-common',                      minChunks: 2,                      priority: -20,                      chunks: 'initial',                      reuseExistingChunk: true                  }              },          }      },      module: {          rules: [              {                  test: /\.css$/,                  use: [                      {                          loader: MiniCssExtractPlugin.loader,                          options: {                              // 解決 export 'default' (imported as 'mod') was not found                              // 啟用 CommonJS 語法                              esModule: false,                          },                      },                      'css-loader'                  ]              }          ]      },  }  if (isProd) {      // 壓縮 css      config.optimization.minimizer = [          new CssMinimizerPlugin(),      ]  }  module.exports = merge(base, config)

客戶端配置文件中的 config.optimization 屬性是打包時分割代碼用的。它的作用是將第三方庫都打包在一起。

其他插件作用:

  1. 鴻蒙官方戰略合作共建——HarmonyOS技術社區

  2.  MiniCssExtractPlugin 插件, 將 css 提取出來單獨打包。

  3.  CssMinimizerPlugin 插件,壓縮 css。

  4.  CompressionPlugin 插件,將資源壓縮成 gzip 格式(大大提升傳輸效率)。另外還需要在 node 服務器上引入 compression 插件配合使用。

  5.  WebpackBar 插件,打包時顯示進度條。

webpack.server.config.js 服務端配置文件

const webpack = require('webpack')  const merge = require('webpack-merge')  const base = require('./webpack.base.config')  const nodeExternals = require('webpack-node-externals') // Webpack allows you to define externals - modules that should not be bundled.  const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')  const WebpackBar = require('webpackbar')  const plugins = [      new webpack.DefinePlugin({          'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),          'process.env.VUE_ENV': '"server"'      }),      new VueSSRServerPlugin()  ]  if (process.env.NODE_ENV == 'production') {      plugins.push(          new WebpackBar()      )  }  module.exports = merge(base, {      target: 'node',      devtool: '#source-map',      entry: './src/entry-server.js',      output: {          filename: 'server-bundle.js',          libraryTarget: 'commonjs2'      },      externals: nodeExternals({          allowlist: /\.css$/ // 防止將某些 import 的包(package)打包到 bundle 中,而是在運行時(runtime)再去從外部獲取這些擴展依賴      }),      plugins,      module: {          rules: [              {                  test: /\.css$/,                  use: [                      'vue-style-loader',                      'css-loader'                  ]              }          ]      },  })

服務端打包和客戶端不同,它將所有文件一起打包成一個文件 server-bundle.js。同時解析 css 需要使用 vue-style-loader,這一點在官方指南中有說明:

Vue中怎么搭建一個服務端渲染項目

配置服務器

生產環境

pro-server.js 生產環境服務器配置文件

const fs = require('fs')  const path = require('path')  const express = require('express')  const setApi = require('./api')  const LRU = require('lru-cache') // 緩存  const { createBundleRenderer } = require('vue-server-renderer')  const favicon = require('serve-favicon')  const resolve = file => path.resolve(__dirname, file)  const app = express()  // 開啟 gzip 壓縮 https://github.com/woai3c/node-blog/blob/master/doc/optimize.md  const compression = require('compression')  app.use(compression())  // 設置 favicon  app.use(favicon(resolve('../public/favicon.ico')))  // 新版本 需要加 new,舊版本不用  const microCache = new LRU({      max: 100,      maxAge: 60 * 60 * 24 * 1000 // 重要提示:緩存資源將在 1 天后過期。  })  const serve = (path) => {      return express.static(resolve(path), {          maxAge: 1000 * 60 * 60 * 24 * 30      }) }  app.use('/dist', serve('../dist', true))  function createRenderer(bundle, options) {      return createBundleRenderer(          bundle,          Object.assign(options, {              basedir: resolve('../dist'),              runInNewContext: false          })      )  } function render(req, res) {      const hit = microCache.get(req.url)      if (hit) {          console.log('Response from cache')          return res.end(hit)      }      res.setHeader('Content-Type', 'text/html')      const handleError = err => {          if (err.url) {              res.redirect(err.url)          } else if (err.code === 404) {              res.status(404).send('404 | Page Not Found')          } else {              res.status(500).send('500 | Internal Server Error~')              console.log(err)          }      }      const context = {          title: 'SSR 測試', // default title          url: req.url     }      renderer.renderToString(context, (err, html) => {          if (err) {              return handleError(err)          }          microCache.set(req.url, html)          res.send(html)      })  }  const templatePath = resolve('../public/index.template.html')  const template = fs.readFileSync(templatePath, 'utf-8')  const bundle = require('../dist/vue-SSR-server-bundle.json')  const clientManifest = require('../dist/vue-SSR-client-manifest.json') // 將js文件注入到頁面中  const renderer = createRenderer(bundle, {      template,      clientManifest  })  const port = 8080  app.listen(port, () => {      console.log(`server started at localhost:${ port }`)  })  setApi(app)  app.get('*', render)

從代碼中可以看到,當首次加載頁面時,需要調用 createBundleRenderer() 生成一個 renderer,它的參數是打包生成的 vue-SSR-server-bundle.json 和 vue-SSR-client-manifest.json 文件。當返回 HTML 文件后,頁面將會被客戶端接管。

在文件的最后有一行代碼 app.get('*', render),它表示所有匹配不到的請求都交給它處理。所以如果你寫了 ajax 請求處理函數必須放在前面,就像下面這樣:

app.get('/fetchData', (req, res) => { ... })  app.post('/changeData', (req, res) => { ... })  app.get('*', render)

否則你的頁面會打不開。

開發環境

開發環境的服務器配置和生產環境沒什么不同,區別在于開發環境下的服務器有熱更新。

一般用 webpack 進行開發時,簡單的配置一下 dev server 參數就可以使用熱更新了,但是 SSR 項目需要自己配置。

由于 SSR 開發環境服務器的配置文件 setup-dev-server.js 代碼太多,我對其進行簡化后,大致代碼如下:

// dev-server.js  const express = require('express')  const webpack = require('webpack')  const webpackConfig = require('../build/webpack.dev') // 獲取 webpack 配置文件  const compiler = webpack(webpackConfig)  const app = express()  app.use(require('webpack-hot-middleware')(compiler))  app.use(require('webpack-dev-middleware')(compiler, {      noInfo: true,      stats: {          colors: true      }  }))

同時需要在 webpack 的入口文件加上這一行代碼 webpack-hot-middleware/client?reload=true。

// webpack.dev.js  const merge = require('webpack-merge')  const webpackBaseConfig = require('./webpack.base.config.js') // 這個配置和熱更新無關,可忽略  module.exports = merge(webpackBaseConfig, {      mode: 'development',      entry: {          app: ['webpack-hot-middleware/client?reload=true' , './client/main.js'] // 開啟熱模塊更新      },      plugins: [new webpack.HotModuleReplacementPlugin()]  })

然后使用 node dev-server.js 來開啟前端代碼熱更新。

熱更新主要使用了兩個插件:webpack-dev-middleware 和 webpack-hot-middleware。顧名思義,看名稱就知道它們的作用,

webpack-dev-middleware 的作用是生成一個與 webpack 的 compiler 綁定的中間件,然后在 express 啟動的 app 中調用這個中間件。

這個中間件的作用呢,簡單總結為以下三點:通過watch mode,監聽資源的變更,然后自動打包; 快速編譯,走內存;返回中間件,支持express 的 use 格式。

webpack-hot-middleware 插件的作用就是熱更新,它需要配合 HotModuleReplacementPlugin 和 webpack-dev-middleware 一起使用。

打包文件 vue-SSR-client-manifest.json 和 vue-SSR-server-bundle.json

webpack 需要對源碼打包兩次,一次是為客戶端環境打包的,一次是為服務端環境打包的。

為客戶端環境打包的文件,和以前我們打包的資源一樣,不過多出了一個 vue-SSR-client-manifest.json 文件。服務端環境打包只輸出一個 vue-SSR-server-bundle.json 文件。

vue-SSR-client-manifest.json 包含了客戶端環境所需的資源名稱:

Vue中怎么搭建一個服務端渲染項目

從上圖中可以看到有三個關鍵詞:

  1. 鴻蒙官方戰略合作共建——HarmonyOS技術社區

  2.  all,表示這是打包的所有資源。

  3.  initial,表示首頁加載必須的資源。

  4.  async,表示需要異步加載的資源。

vue-SSR-server-bundle.json 文件: 

Vue中怎么搭建一個服務端渲染項目

  1. 鴻蒙官方戰略合作共建——HarmonyOS技術社區

  2. entry, 服務端入口文件。

  3. files,服務端依賴的資源。

填坑記錄

1. [vue-router] failed to resolve async component default: referenceerror: window is not defined

由于在一些文件或第三方文件中可能會用到 window 對象,并且 node 中不存在 window 對象,所以會報錯。

此時可在 src/app.js 文件加上以下代碼進行判斷:

// 在 app.js 文件添加上這段代碼,對環境進行判斷  if (typeof window === 'undefined') {      global.window = {}  }

2. mini-css-extract-plugin 插件造成 ReferenceError: document is not defined

使用 mini-css-extract-plugin 插件打包的的 server bundle, 會使用到 document。由于 node 環境中不存在 document 對象,所以報錯。

解決方案:樣式相關的 loader 不要放在 webpack.base.config.js 文件,將其分拆到 webpack.client.config.js 和 webpack.client.server.js 文件。其中 mini-css-extract-plugin 插件要放在 webpack.client.config.js 文件配置。

base

module: {      rules: [          {              test: /\.vue$/,              loader: 'vue-loader',              options: {                  compilerOptions: {                      preserveWhitespace: false                  }              }          },          {              test: /\.js$/,              loader: 'babel-loader',              exclude: /node_modules/          },          {              test: /\.(png|svg|jpg|gif|ico)$/,              use: ['file-loader']          },          {              test: /\.(woff|eot|ttf)\??.*$/,              loader: 'url-loader?name=fonts/[name].[md5:hash:hex:7].[ext]'          },      ]  }

client

module: {      rules: [          {              test: /\.css$/,              use: [                  {                      loader: MiniCssExtractPlugin.loader,                      options: {                          // 解決 export 'default' (imported as 'mod') was not found                          esModule: false,                      },                  },                  'css-loader'              ]          }      ]  }

server

module: {      rules: [          {              test: /\.css$/,              use: [                  'vue-style-loader',                  'css-loader'              ]          }      ]  }

3. 開發環境下跳轉頁面樣式不生效,但生產環境正常。

由于開發環境使用的是 memory-fs 插件,打包文件是放在內存中的。如果此時 dist 文件夾有剛才打包留下的資源,就會使用 dist 文件夾中的資源,而不是內存中的資源。并且開發環境和打包環境生成的資源名稱是不一樣的,所以就造成了這個 BUG。

解決方法是執行 npm run dev 時,刪除 dist 文件夾。所以要在 npm run dev 對應的腳本中加上 rimraf dist。

"dev": "rimraf dist && node ./server/dev-server.js --mode development",

4. [vue-router] Failed to resolve async component default: ReferenceError: document is not defined

不要在有可能使用到服務端渲染的頁面訪問 DOM,如果有這種操作請放在 mounted() 鉤子函數里。

如果你引入的數據或者接口有訪問 DOM 的操作也會報這種錯,在這種情況下可以使用 require()。因為 require() 是運行時加載的,所以可以這樣使用:

<script>  // 原來報錯的操作,這個接口有 DOM 操作,所以這樣使用的時候在服務端會報錯。 import { fetchArticles } from '@/api/client'  export default {    methods: {      getAppointArticles() {        fetchArticles({          tags: this.tags,          pageSize: this.pageSize,          pageIndex: this.pageIndex,        })        .then(res => {            this.$store.commit('setArticles', res)        })      },    }  }  </script>

修改后:

<script>  // 先定義一個外部變量,在 mounted() 鉤子里賦值  let fetchArticles  export default {    mounted() {      // 由于服務端渲染不會有 mounted() 鉤子,所以在這里可以保證是在客戶端的情況下引入接口        fetchArticles = require('@/api/client').fetchArticles    },    methods: {      getAppointArticles() {        fetchArticles({          tags: this.tags,          pageSize: this.pageSize,          pageIndex: this.pageIndex,        })        .then(res => {            this.$store.commit('setArticles', res)        })      },    }  }  </script>

修改后可以正常使用。

5. 開發環境下,開啟服務器后無任何反應,也沒見控制臺輸出報錯信息。

這個坑其實是有報錯信息的,但是沒有輸出,導致以為沒有錯誤。

在 setup-dev-server.js 文件中有一行代碼 if (stats.errors.length) return,如果有報錯就直接返回,不執行后續的操作。導致服務器沒任何反應,所以我們可以在這打一個 console.log 語句,打印報錯信息。

關于Vue中怎么搭建一個服務端渲染項目就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

vue
AI

平湖市| 固镇县| 安新县| 通海县| 三原县| 丰原市| 惠安县| 伊通| 防城港市| 罗田县| 和静县| 金溪县| 石嘴山市| 龙泉市| 新河县| 长葛市| 仁怀市| 乌拉特前旗| 浮梁县| 信阳市| 陆河县| 永城市| 太康县| 开封县| 沅江市| 东至县| 衡东县| 乌海市| 旬阳县| 遂溪县| 阿城市| 辛集市| 芦山县| 阿巴嘎旗| 车致| 蒲江县| 建平县| 兴宁市| 洪洞县| 丰都县| 香格里拉县|