【新手向】Vue.js + Node.js(koa) 合體指南

小蘑菇哥哥發表於2019-01-02

webpack 大法好

Webpack 是大家熟知的前端開發利器,它可以搭建包含熱更新的開發環境,也可以生成壓縮後的生產環境程式碼,還擁有靈活的擴充套件性和豐富的生態環境。但它的缺點也非常明顯,那就是配置項又多又複雜,隨便拿出某一個配置項(例如 rulespluginsdevtool等等)都夠寫上一篇文章來說明它的 N 種用法,對新手造成極大的困擾。Vue.js(以下簡稱 Vue)絕大部分情況使用 webpack 進行構建,間接地把這個問題丟給了 Vue 的新手們。不過不論是 Vue 還是 webpack,其實他們都知道配置問題的癥結所在,因此他們也想了各自的辦法來解決這個問題,我們先看看他們的努力。

Vue cli 2.x - 提供開箱即用的配置

在之前一長段時間中,我們要初始化一個 Vue 專案,一般是使用 vue-cli 提供的 vue init 命令(這也是 Vue cli 的 v2 版本,之後簡稱 Vue cli 2.x)。而且通常一些比較有規模的專案都會使用 vue init webpack my-project 來使用 webpack 模板,那麼上面提到的配置問題就來了。

為了解決這個問題, Vue 的做法是提供開箱即用的配置,即通過 vue init 出來的專案,預設生成的巨多的配置檔案,截圖如下:

Vue cli 2 build 目錄

開箱即用是保證了,但一旦要修改,就相當於是進入了一個黑盒,開發者對於一堆檔案,一堆 JSON 望洋興嘆。

webpack 4 - 極大地簡化配置

webpack 4 推出也有一年左右了,它的核心改動之一是極大地簡化配置。它新增了 mode,把一些顯而易見的配置做成內建的。因此例如 NoEmitOnErrorsPlugin(), UglifyJSPlugin() 等等都不必寫了;分包用的 CommonsChunkPlugin() 也濃縮成了一個配置項 optimization.splitChunks,並且已有能適應絕大部分情況的預設值。

據說 webpack 4 構建出來的程式碼的體積還更小了,因此這次升級顯然是必要的。

Vue cli 3.x - 升級 webpack,還搞出了外掛

大約小半年前,Vue cli 推出了 v3 版本,也是一個顛覆性的升級。它把核心精簡為 @vue/cli,把 webpack 搞成了 @vue/cli-service, 把其他東西抽象為“外掛”。這些外掛包括 babel, eslint, Vuex, Unit Testing 等等,還允許自定義編寫和釋出。我不在這裡介紹 Vue cli 3.x 的用法和生態,但從結果看,現在通過 vue create 建立的的 Vue 專案清爽了不少。

Vue cli 3 目錄

所以現在的問題是什麼?

如果我們單純開發一個前端 Vue 專案,webpack-dev-server 能幫助我們啟動一個 nodejs 伺服器並支援熱載入,非常好用。可如果我們要開發的是一個 nodejs + Vue 的全棧專案呢?兩者是不可能啟動在同一個埠的。那我們能做的只是讓 nodejs 啟動在埠 A,讓 Vue (webpack-dev-server) 啟動在埠 B。而如果 Vue 需要傳送請求訪問 nodejs 提供的 API 時,還會遇上跨域問題,雖然可以通過配置 proxy 解決,但依然非常繁瑣。而實質上,這是一整個專案的前後端而已,我們應該使用一條命令,一個埠來啟動它們。

拋開 Vue,此類需求 webpack 本身其實是支援的。因為它除了提供 webpack-dev-server 之外,還提供了 webpack-dev-middleware。它以 express middleware 的方式,同樣整合了熱載入的功能。因此如果我們的 nodejs 使用的是 express 作為服務框架的話,我們可以以 app.use 的方式引入這個中介軟體,就可以達成兩者的融合了。

再說回 Vue cli 3。它通過 vue-cli-service 命令,把 webpack 和 webpack-dev-server 包裹起來,這樣使用者就看不到配置檔案了,達成了簡潔的目的。不過實質上,配置檔案依然存在,只是移動到了 node_modules/@vue/cli-service/webpack.config.js 而已。當然為了個性化需求,它也支援使用者通過配置物件 (configureWebpack) 或者鏈式呼叫 (chainWebpack) 兩種間接的方式,但不再提供直接修改配置檔案的方式了。

然而致命的是,即便它提供了足夠的方式修改配置,但它不能把 webpack-dev-server 變成 webpack-dev-middleware。這表示使用 Vue cli 3 建立的 Vue 部分和 nodejs(express) 部分是不能融合的。

怎麼解決?

說了這麼多,其實這就是我最近實際碰到的問題以及分析問題的思路。鑑於 Vue cli 3 黑盒的特性,我們無法繼續使用它了(可能以後有升級能解決這個問題,至少目前不行)。而使用 Vue cli 2 又因為它內建的是 webpack 3 且配置檔案一大堆,也讓人無所適從。這麼看,唯一剩下的路就只能自行使用並配置 webpack 4了,這也是本文的內容所在。

技術棧

nodejs 部分

目前比較主流的構建 nodejs 部分的 Web 框架是 express,且不說它的語法有多優雅,使用有多廣泛等等,最主要的原因是剛才提過的 webpack-dev-middleware 就是一個 express 的中介軟體,因此兩者可以無縫銜接。

可惜的是,在我實際的專案中,我使用了 koa 作為了我的 nodejs 框架。其實要說它比 express 好在哪裡我也說不上來,也不是本文的重點。可能出於嚐鮮的目的,或者團隊技術棧統一的目的,或者其他鬼使神差的巧合,反正我用了它,而且開始時還沒意識到有這個融合的問題,直到後來發現 webpack-dev-middleware 和 koa 是不相容的,我內心有過一絲後悔……當然這是後話了。

本文以 koa 為基準。如果您使用的是 express,其實大同小異,而且更加簡單。

Vue 部分

Vue 沒什麼好多說的,就一個版本,不存在 express / koa / 其他的選擇。只是這裡我沒有使用 SSR,而是普通的 SPA 專案(單頁應用,前端渲染)。

目錄結構

既然是兩個專案合體,總有一個目錄結構的安排問題。這裡我不談每個專案內部需要如何組織,那是 Vue / koa 本身的問題,也是個人喜好的問題。我想談的是這兩者之間的組織方式,不外乎以下 3 種:(實際上也是個人喜好問題,見仁見智,這裡只是統一一下表述,避免後續的混淆)

以下截圖中的前後端專案均為獨立專案,即融合之前的,可以單獨執行的那種,所以能看到兩份 package.json 和 package-lock.json

後端專案為基礎,前端專案為子目錄

【新手向】Vue.js + Node.js(koa) 合體指南

除了紅框中的 vue 目錄外,其他都是 nodejs 的程式碼。而且因為我只是做個示意,所以 nodejs 程式碼其實也僅僅包含兩個 index.js,public 目錄和兩個 package.json。實際的 nodejs 專案應該會有更多的程式碼,例如 actions(把每個路由處理單獨到一個目錄),middlewares(過所有路由的中介軟體)等等。

這個安排的思路是認為前端是整個專案的一部分(頁面展示部分),所以 Vue 單獨放在一個目錄裡面。我採用的就是這種結構。

前端專案為基礎,後端專案為子目錄

【新手向】Vue.js + Node.js(koa) 合體指南

這就和前面一種相反,紅框中的是後端程式碼。這麼安排的理由可能是因為我們是前端開發者,所以把前端程式碼位於基礎位置,後端提供的 API 輔助 Vue 的程式碼執行。

中立,不偏向任何人

【新手向】Vue.js + Node.js(koa) 合體指南

看了前面兩種,自然能想到這第三種辦法。不過我認為這種辦法純粹沒事兒找事兒,因為根據 npm 的要求,package.json 是必須放在根目錄的,所以實際上想把兩者完全分離並公平對待是弊大於利的(例如各類呼叫路徑都會多幾層),適合強迫症患者。

改造 Vue 部分

Vue 部分的改造點主要是:

  1. package.json 融合到根目錄(nodejs) 的 package.json 裡面去。這裡主要包括依賴 (dependencydevDependency)以及執行命令(scripts)兩部分。其餘的如 browserslist, engine 等 babel 可能用到的欄位,因為 nodejs 程式碼不需要 babel,所以可以直接複製過去,不存在融合。

  2. 編寫 webpack.config.js。(因為 Vue cli 3 是自動生成且隱藏的,這個就需要自己寫)

下面詳細來看。

融合 package.json

剛才有提到過,像 browserslist, engine 這類 babel 等使用的欄位,因為 nodejs 端是不需要的,所以簡單的複製過去即可。需要動腦的是依賴和命令。

依賴方面,其實前後端共用的依賴也基本不存在,所以實際上也是一個簡單的複製。需要注意的是類似 vue, vue-router, webpack, webpack-cli 等等都是 devDependency,而不是 dependency。真正需要放到 dependency 的,其實只有 @babel/runtime 這一個(因為使用了 plugin-transform-runtime)。

命令方面,本身 Vue 必備的是“啟動開發環境”和“構建”兩條命令(可選的還有測試,這個我這裡先不討論)。因為開發環境需要和 nodejs 融合,所以這條我們放到 nodejs 部分說。剩下的是構建命令,常規操作是通過設定 NODE_ENVproduction 來讓 webpack 走入線上構建的情況。另外值得注意的是,因為現在 package.json 和 webpack.config.js 不在同級目錄了,所以需要額外指定目錄,命令如下:(cross-env 是一個相當好用的跨平臺設定環境變數的工具)

{
  "scripts": {
    "build": "cross-env NODE_ENV=production webpack --config vue/webpack.config.js"
  }
}
複製程式碼

編寫 webpack.config.js

本文的重點不是 webpack 的配置方式,因此這裡比較簡略,不詳細講述每個配置項的含義

webpack.config.js 本質上是一個返回 JSON 的配置檔案,我們會用到其中的幾個 key。如果要了解 webpack 全部的配置項,可以檢視 webpack 的中文網站介紹。另外如果不想分段檢視,你可以在這裡找到完整的 webpack.config.js。

mode

webpack 4 新增配置項,常規可選值 'production''development'。這裡我們根據 process.env.NODE_ENV 來確定值。

let isProd = process.env.NODE_ENV === 'production'

module.exports = {
  mode: isProd ? 'production' : 'development'
}
複製程式碼

entry

定義 webpack 的入口。我們需要把入口設定為建立 Vue 例項的那個 JS,例如 vue/src/main.js

{
  entry: {
    "app": [path.resolve(__dirname, './src/main.js')]
  }
}
複製程式碼

output

定義 webpack 的輸出配置。在開發狀態下,webpack-dev-middleware(以下簡稱 wdm)並不會真的去生成這個 dist 目錄,它是通過一個記憶體檔案系統,把檔案輸出到記憶體。所以這個目錄僅僅是一個標識而已。

{
  output: {
    filename: '[name].[hash:8].js',
    path: isProd ? resolvePath('../vue-dist') : resolvePath('dist'),
    publicPath: '/'
  }
}
複製程式碼

resolve

主要定義兩個東西:webpack 處理 import 時自動新增的字尾順序和供快速訪問的別名。

{
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': resolvePath('src'),
    }
  }
}
複製程式碼

module(重點)

module 在 webpack 中主要確定如何處理專案中不同型別的模組。我們這裡採用最常用的配法,即告訴 webpack,什麼樣的字尾檔案用什麼樣的 loader 來處理。

{
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /\.js?$/,
        loader: 'babel-loader',
        exclude: file => (
          /node_modules/.test(file) && !/\.vue\.js/.test(file)
        )
      },
      {
        test: /\.less$/,
        use: [
          isProd ? MiniCssExtractPlugin.loader : 'vue-style-loader',
          'css-loader',
          'less-loader'
        ]
      },
      {
        test: /\.css$/,
        use: [
          isProd ? MiniCssExtractPlugin.loader : 'vue-style-loader',
          'css-loader'
        ]
      }
    ]
  }
}
複製程式碼

上述配置了 4 種檔案的處理方式,它們分別是:

  1. /\.vue$/

    處理 Vue 檔案,使用 Vue 專門提供的 vue-loader。這個處理器做的事情就是把 Vue 裡面的 <script><style> 的部分獨立出來,讓它們可以繼續由下面的 rules 分別處理。否則一個 .vue 檔案是不可能進入 .js 或者 .css 的處理流程的。另外如果 <style>lang 屬性,還可以進入例如 .less, .styl 等其他處理流程。

    它還需要專門的外掛 VueLoaderPlugin(),之後可以看到,不要漏掉。

  2. /\.js?$/

    表面上是處理字尾為 .js 的檔案,但實質上在這裡也用來處理 Vue 裡面 <script> 的內容。在這裡我們要做的是使用 babel-loader 對程式碼中的高階寫法轉譯為相容低版本瀏覽器的寫法。具體轉譯規則使用 .babelrc 檔案配置。另外這裡還忽略了 node_modules(因為其他包在釋出時都已經轉碼過了,不用再處理徒增時間)。不過得確保 node_modules 裡的單體 Vue 檔案依然參與轉譯,這也是 Vue 官方文件 的推薦寫法。

  3. /\.less$/

    我的專案中使用 less 作為樣式前處理器,因此在每個 Vue 檔案都使用了 <style lang="less">。這樣通過 vue-loader,就能讓這條規則中配置的幾個 loader 來處理 Vue 檔案中的樣式了。這幾個 loader 分別做的事情是:

    1. vue-style-loader

      把樣式以 <style> 標籤的形式插入在頁面頭部,只在開發狀態下使用

      它和 style-loader 的差異並不大,但既然 Vue 官方文件建議使用這個,那我們就用這個吧。

    2. mini-css-extract-plugin 的 loader

      把樣式抽成一個單獨的 css 檔案並在 <head> 標籤中以 <link rel="stylesheet"> 的方式引用,取代原來 webpack 3.x 的 extract-text-webpack-plugin只在生產狀態下使用

      它同樣需要在外掛中增加 MiniCssExtractPlugin() 以配合使用。

    3. css-loader

      支援通過隱式的方式載入資源。例如如果在 JS 檔案中編寫 import 'style.css',或者在樣式檔案中編寫 url('image.png'),經過 css-loader 可以把 style.cssimage.png 都引入到 webpack 的處理流程中。這樣針對 css 的所有 loader 就可以處理 style.css,而針對所有圖片的 loader(例如尺寸很小就自動轉為 base64 的 url-loader)都可以處理 image.png 了。

    4. less-loader

      使用 less 前處理器必須載入的 loader,用以把 less 語法轉化為普通的 css 語法。

      基本上每個前處理器都有對應的 loader,例如 stylus-loader, sass-loader 等等,可以按需使用。

  4. /\.css$/

    .js 規則相似,這條規則可以同時應用於 .css 的字尾檔案以及 Vue 中的 <style> (且沒有寫 lang 的)部分。

plugins(重點)

外掛和規則類似,也是對載入進入 webpack 的資源進行處理。不過和規則不同,它並不以正則(多數為字尾名)決定是否進入,而是全部進入後通過外掛自身的 JS 寫法來確定處理哪些,因此更加靈活。

前面提過,有些功能需要 loader 和 plugins 配合使用,因此也需要宣告在這裡,比如 VueLoaderPlugin()MiniCssExtractPlugin()

在我們的專案中,外掛分為兩類。一類是不論環境(開發還是生產)都要使用的,這一類有 2 個:

{
  "plugins": [
    // 和 vue-loader 配合使用
    new VueLoader(),

    // 輸出 index.html 到 output
    new HtmlwebpackPlugin({
      template: resolvePath('index.html')
    })
  ]
}
複製程式碼

另外一類是生產環境才需要使用的,也有 2 個:

if (isProd) {
  webpackConfig.plugins.push(
    // 每次 build 清空 output 目錄
    new CleanWebpackPlugin(resolvePath('../vue-dist'))
  )
  webpackConfig.plugins.push(
    // 分離單獨的 CSS 檔案到 output,和 MiniCssExtractPlugin.loader 配合使用
    new MiniCssExtractPlugin({
      filename: 'style.css',
    })
  )
}
複製程式碼

optimization

optimization 是一個 webpack 4 新增的配置項,主要處理生產環境下的各類優化(例如壓縮,提取公共程式碼等等),所以大部分的優化都在 mode === 'production' 時會使用。這裡我們只使用它的一個功能,即分包,以前的寫法是 new webpack.optimize.CommonChunkPlugin(),現在只要配置就可以了,配置方法也很簡單:

{
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
}
複製程式碼

這樣,從 node_modules 來的程式碼會打包到一起,命名為 vendors~app.[hash].js,這裡面可能包含了 Vue, Vuex, Vue Router 等等第三方的程式碼。這些程式碼不會經常修改,所以獨立出來並新增長時間的強制快取能顯著提升站點訪問速度。

看看編譯完成的產物

通過執行 npm run build,能夠呼叫 webpack-cli 來執行剛才編寫的配置檔案。編譯成功後,會在根目錄下生成一個 vue-dist 目錄,裡面存放的內容如下:(如果做了 Vue 的路由懶載入,即 const XXX = () => import('@/XXX.vue'),檔案會根據路由分割,因此數量會更多)

vue-dist

總共 4 個檔案

  1. index.html 存放唯一的 HTML 入口,裡面包含對各 JS, CSS 檔案的引用,並定義了容器節點。使用靜態伺服器啟動後,由於 JS 的執行,可以執行前端渲染。

  2. style.css 存放所有的樣式。這些樣式都是從每個 Vue 檔案的 <style lang="less"> 部分中抽出來合併而成的。

  3. app.[hash].js 存放所有的自定義 JS,即每個 Vue 檔案的 <script> 部分,以及如 app.js, router.js, store.js 的程式碼等等。

  4. vendors~app.[hash].js 如上所述,存放所有類庫 JS,如 vue-router, vuex 本身的程式碼。

對 nodejs 來說,需要關心的只是這個 index.html 而已,其他 3 個都會由它負責引入。那麼我們接下來看看如何改造 nodejs 部分。

改造 nodejs(koa) 部分

koa 部分需要我們改造的點主要有:

  1. package.json

    nodejs 專案的標配,記錄依賴,指令碼,專案資訊等等。我們需要在這裡和 Vue 端的 package.json 進行合併,尤其是 npm run dev 指令碼的合併。

  2. index.js

    nodejs 的程式碼入口,通過命令 node index.js 啟動整個專案。在這裡可以註冊路由規則,註冊中介軟體,註冊 wdm 等等,絕大部分邏輯都在這裡。

    因為開發環境和生產環境的行為不盡相同(例如開發環境需要 wdm 而生產環境不需要),因此可以分為兩個檔案(index.dev.jsindex.prod.js),也可以在一個檔案中通過環境變數判斷,這個因人而異。

    雖然 koa 的路由規則和中介軟體都可以寫在這裡,但通常只要是略有規模的專案,都會把路由處理和中介軟體分別獨立成 actionsmiddlewares 目錄分開存放(名字怎麼起看自己喜好)。配置檔案(例如配置啟動埠號)也通常會獨立成 config.js 或者 config 目錄。其他的例如 util 目錄等也都按需建立。

    我們需要在這裡統一前後路由,並使用 wdm 等

package.json

在“改造 Vue 部分”的 package.json 中曾經講過,Vue 專案的依賴都直接複製到外層的 package.json 中來,還增加了一條 npm run build 命令。這裡會再列出兩條命令,達成最基本的的需求。

我為了區分執行環境,把 index.js 拆解為了 index.dev.jsindex.prod.js。如上面所說,你也可以就在一個檔案裡用 process.env.NODE_ENV 來判斷執行環境。

啟動開發伺服器

常規的 koa 服務,一般我們通過 node index.js 來啟動。但 nodejs 預設沒有熱載入,因此修改了 nodejs 程式碼需要重啟伺服器,比較麻煩。以前我會使用 chokidar 模組監聽檔案系統的變化,並在變化時執行 delete require.cache[path] 來實現簡單的熱載入機制。但現在有一款更方便的工具幫我們做了這個事情,那就是 nodemon

"nodemon -e js --ignore vue/ index.dev.js"
複製程式碼

它的使用方式也很簡單,把 node index.js 換成 nodemon index.js,他就會監聽以這個入口執行的所有檔案的變化,並自動重啟。但我們這裡還額外使用了兩個配置項。-e 表示指定副檔名,這裡我們只監聽 js。 --ignore 指定忽略項,因為 vue/ 目錄中有 webpack 幫我們執行熱載入,因此它的修改可以忽略。其他可用的配置項可以參考 nodemon 的主頁

啟動線上環境伺服器

這個就簡單了,直接執行 node 命令即可。所以最終的指令碼部分如下:

{
  "scripts": {
    "dev": "nodemon -e js --ignore vue/ index.dev.js",
    "build": "cross-env NODE_ENV=production webpack --config vue/webpack.config.js",
    "start": "node index.prod.js"
  }
}
複製程式碼

index.js

這個檔案是 koa 的啟動入口,它的大致結構如下(我使用了 koa-router 來管理路由,且只列舉最最簡單的骨架):

// 引用基本類庫
const Koa = require('koa')
const Router = require('koa-router')
const koaStatic = require('koa-static')

// 初始化
const app = new Koa()
const router = new Router()

// 常規專案可能有中介軟體,即處理所有路由的邏輯,如驗證登入,記錄日誌等等,這裡省略

// 註冊路由到 koa-router。
// 常規專案路由很多,應該獨立到一個目錄去一個個註冊
router.get('/api/hello', ctx => {
  ctx.body = {message: 'Greeting from koa'}
})

// koa-router 以中介軟體的形式註冊給 koa
// 就理解為固定寫法
app.use(router.routes());
app.use(router.allowedMethods());
// 為 public 目錄啟動靜態服務,可以放圖片,字型等等。Vue 部分打包的資源在 vue-dist,不在這裡。
app.use(koaStatic('public'));

// 實際專案可能埠還要寫到配置檔案,這裡隨意了
app.listen(8080)
複製程式碼

我們從開發環境和線上環境兩個方面來討論對這個檔案的改造方式。

開發環境

首先是合併前後端路由。

Vue 端使用的是 history 路由模式,因此本身就需要 nodejs 來配合,Vue 官方推薦的是 connect-history-api-fallback 中介軟體,不過那是針對 express 的。我找到了一個給 koa 使用的相同功能的中介軟體,名為 koa2-history-api-fallback

不論是哪一個中介軟體,原理是一樣的。因為 SPA 只生成一個 index.html,因此所有的 navigate 路由都必須定向到這個檔案才行,否則例如 /user/index 這樣的路由,瀏覽器會去找 /user/index.html,顯然是找不到的。

既然 Vue 需求的是所有的 navigate 路由,顯然它不能註冊在 koa 的路由之前,否則 koa 的路由將永遠無法生效。因此路由註冊順序就很自然了:先後端再前端。另外這裡說的 navigate 路由,指的是請求 HTML 的第一個請求,靜態資源的請求不在其內,因此例如上述的 public 靜態路由和 Vue 的中介軟體的前後順序就無所謂了。

所以路由部分融合後大概是這樣:

// 後端(koa)路由
// koa-router 的單個註冊部分省略
app.use(router.routes());
app.use(router.allowedMethods());

// 前端(vue)路由
// 所有 navigate 請求重定向到 '/',因為 webpack-dev-middleware 只服務這個路由
app.use(history({
  htmlAcceptHeaders: ['text/html'],
  index: '/'
}));
app.use(koaStatic('public'));
複製程式碼

其次就是問題的最開端,webpack-dev-middleware 的使用。

和 Vue 的中介軟體類似,webpack-dev-middleware 也是隻支援 express 的(這些都表明 express 的生態更好),然後我也找了個 koa 版本的替代方案,叫做 koa-webpack

使用起來倒也不麻煩,如下:

const koaWebpack = require('koa-webpack')
const webpackConfig = require('./vue/webpack.config.js')

// 注意這裡是個非同步,所以和其他 app.use 以及最終的 app.listen 必須在一起執行
// 可以使用 async/await 或者 Promise 保證這一點
koaWebpack({
  config: webpackConfig,
  devMiddleware: {
    stats: 'minimal'
  }
}).then(middleware => {
  app.use(middleware)
})
複製程式碼

一個完整的 index.dev.js 可以檢視這裡

線上環境

線上環境和開發環境有兩處不同,我們著重講一下這兩個不同點。

首先,線上環境不使用 webpack-dev-middleware (koa-webpack),因此這部分程式碼不需要了。

其次,因為構建後的 Vue 程式碼全部位於 vue-dist 目錄,而我們需要的 HTML 入口以及其他 JS, CSS檔案都在其中,因此我們需要把 vue-dist 目錄新增到靜態服務中可供訪問,另外 history fallback 的目標也有所改變,如下:

// 後端(koa)路由
// koa-router 的單個註冊部分省略
app.use(router.routes());
app.use(router.allowedMethods());

// 前端(vue)路由
// 所有 navigate 請求重定向到 /vue-dist/index.html 這個檔案,配合下面的 koaStatic('vue-dist'),這裡只要填到 '/index.html' 即可。
app.use(history({
  htmlAcceptHeaders: ['text/html'],
  index: '/index.html'
}));
app.use(koaStatic('vue-dist'));
app.use(koaStatic('public'));
複製程式碼

一個完整的 index.prod.js 可以檢視這裡

這麼多配置新手看了還是很懵怎麼辦?

雖然我們討論了這麼多,但不要害怕,實際上的重點只有三個,我們來總結一下:

  1. 我們需要自己編寫 Vue 的 webpack.config.js,處理 loader, plugins 等等

  2. 我們需要合併前後端的兩個 package.json,把兩方的依賴合併,並編寫三條指令碼 (dev, build, start)

  3. 我們需要改動 index.js,處理路由順序,並在開發環境呼叫 webpack-dev-middleware

為了簡單上手,我把專案中的業務程式碼抽離,留下了一個骨架,可以作為 Vue + koa 專案的啟動模板,放在 easonyq/vue-nodejs-template。不過我覺得我們還是應當掌握配置方法和原理,這樣以後如果技術棧的某一塊發生了變化(例如 webpack 出了 5),我們也能夠自己研究修改,而不是每次都以解決任務為最優先,能跑起來就不管了。

願我們大家在前端道路上都能越走越順!

相關文章