基於Webpack/TypeScript/Koa的環境配置

TypeScript是一種開源編程語言,在軟件開發社區中愈來愈受歡迎。TypeScript帶來了可選的靜態類型檢查以及最新的ECMAScript特性。
做爲Javascript的超集,它的類型系統經過在鍵入時報告錯誤來加速和保障咱們的開發,同時愈來愈多對的庫或框架提供的types文件可以讓這些庫/框架的API一目瞭然。我對這門語言垂涎已久,可是遲遲沒法找到練手的地方。
很顯然的,我的博客又一次的成了個人學習試驗田😸。我放棄了上一版Vue單頁面的框架,改成基於TypeScript/Koa的多頁面應用。在改造的過程當中,我試着將服務端(Koa)代碼以及前端代碼都使用TypeScript來開發,中間使用了webpack做爲開發時先後端的橋樑。javascript


目錄結構

.
├── .babelrc
├── bin
│   ├── dev.server.ts
│   ├── pm2.json
│   └── app.ts
├── config                                       # 配置目錄
│   ├── dev.ts
│   └── prod.ts
├── nodemon.json
├── package.json
├── postcss.config.js
├── scripts
│   └── webpack.config.js
├── src                                          # 源碼
│   ├── assets                                   # 靜態資源
│   │   ├── imgs
│   │   ├── scss
│   │   └── ts
│   ├── entries                                  # webpack入口
│   │   ├── blog.ts
│   │   └── index.ts
│   └── views                                    # 模板(文件名與入口一一對應)
│       ├── blog.html
│       ├── index.html
│       └── layout                               # 模板佈局
│           ├── footer.html
│           └── header.html
├── server                                       # 服務端
│   ├── app.ts
│   └── middleware
│       └── webpack-dev-middleware.ts
├── test                                         # 單元測試
│   └── .gitkeep                                      
├── tsconfig.front.json
└── tsconfig.json

安裝項目依賴

npm i --save koa koa-{router,bodyparser,static,ejs}

npm i -D typescript ts-node nodemon @types/{node,koa,koa-router,koa-bodyparser}

開發環境(development)流程

開發環境流程圖

ts-node啓動項目後,整個流程分爲兩部分,藍色線條的表明純服務端代碼的編譯過程。服務端代碼是純typeScript文件,能夠經過ts-node直接編譯運行。前端代碼包含了ejs渲染所須要的模板文件(html),以及模板中所引用的靜態資源(ts, scss, img),這部分須要經過webpack來編譯。css

// path: bin/dev.server.ts
import webpack = require('webpack')
// 引入項目主模塊
import app from '../server/app'
// webpack-dev-middleware中間件
import devMiddleware from '../server/middleware/webpack-dev-middleware'
// webpack配置文件
const webpackConfig = require('../scripts/webpack.config.js')

// https://webpack.docschina.org/api/compiler
const compiler = webpack(webpackConfig)

app.use(devMiddleware(compiler, {
  // 很重要,提供了靜態資源的路徑, 該路徑與webpackConfig中的output.publicPath 對應
  publicPath: '/' 
}))

const PORT: number = Number(process.env.PORT) || 3000
app.listen(PORT)
  • 項目運行時經過 webpack-dev-middleware 中間件來調用webpack,以便 ctx.render 時渲染的就是編譯後的模板文件;
  • 經過glob模塊來遍歷src/entries/*.ts下的入口文件,生成webpack的entry配置項config.entry;這也是Webpack多頁面配置必不可少的一步;
  • 經過ts-loader/babel-loader等來編譯入口文件以及入口文件中所引用的ts/js模塊;
  • 經過css-loader/sass-loader等來編譯入口文件中所引用的scss/css模塊,而且直接經過MiniCssExtractPlugin.loader來獨立生成css文件;
  • 經過url-loader等來編譯引用的資源文件,如image;
  • 遍歷config.entry來查找對應的模板文件,生成多頁面的HtmlWebpackPlugin配置;
經過 webpack-dev-middleware編譯後的文件都在 內存中, 可是 ejs渲染所須要的模板文件都必須爲真實的物理文件。所以須要有兩個 output,一個將靜態資源放置在內存中,一個則直接編譯後生成物理文件放置在 dist/views中(方案見[ejs模板文件沒法使用內存文件的解決方法]章節)。

實現Koa webpack-dev-middleware中間件

webpack-dev-middleware 是一個封裝器(wrapper),它能夠把 webpack 處理過的文件發送到一個 server。

webpack-dev-middleware是一個標準的express中間件,其一個重要做用就是將通過webpack編譯打包的文件生成在內存中,以便下一個中間件使用。不少Cli使用的webpack-dev-server就是基於express+webpack-dev-middleware的實現。html

因爲webpack-dev-middleware是一個標準的express中間件,在Koa中不能直接使用它,所以須要將webpack-dev-middleware封裝一下,以便Koa可以直接使用。前端

安裝依賴
npm i -D webpack-dev-middleware @types/webpack-dev-middleware
koa-webpack-dev-middleware
// path: server/middleware/webpack-dev-middleware.ts
// opts 配置同 webpack-dev-middleware

import * as WebpackDevMiddleware from 'webpack-dev-middleware'
import * as Koa from 'koa'
import { NextHandleFunction } from 'connect'
import webpack = require('webpack')

const devMiddleware = (compiler: webpack.ICompiler, opts: WebpackDevMiddleware.Options) => {
  const middleware = WebpackDevMiddleware(compiler, opts)
  return async (ctx: Koa.Context, next: NextHandleFunction) => {
    await middleware(ctx.req, {
      // @ts-ignore
      end: (content:string) => {
        ctx.body = content
      },
      setHeader: (name, value: any) => {
        ctx.set(name, value)
      }
    }, next)
  }
}

export default devMiddleware

glob遍歷目錄生成webpack入口

webpack 要實現一個多頁面的配置,須要配置多個入口。隨着深刻的開發,入口每每是動態不定的,所以要實現一個動態獲取入口的方法。java

glob是一個容許正則匹配文件路徑的模塊,藉助glob模塊,很容易遍歷某個目錄下的全部文件來生成一個入口的map。node

// path: scripts/webpack.config.js
// ...

// 獲取入口文件
const entries = () => {
  // 經過 globa.sync 方法獲取 src/entries/下的全部 .ts 文件
  const entriesFile = glob.sync(path.resolve(__dirname, '../src/entries/*.ts'))
  /**
   * 入口字典
   * {
   *    index: 'src/entries/index.ts',
   *    blog: 'src/entries/blog.ts',
   *    // ...
   * }
   */
  const map = Object.create(null)
  // 遍歷匹配到的文件列表
  for (let i = 0; i < entriesFile.length; i++) {
    const filePath = entriesFile[i]
    // 提取文件名
    const match = filePath.match(/entries\/([a-zA-Z0-9-_]+)\.ts$/)
    // 將文件名做爲 key, 存入map
    // 如: src/entries/index.ts , src/entries/blog.ts 將分別做爲 index / blog 兩個入口
    map[match[1]] = filePath
  }

  return map
}

// webpack config
const webpackConfig = {
  entry: entries(),
  // ...
}

module.exports = webpackConfig

入口文件映射模板文件

因爲前端源碼使用的typescript/es6/scss,這些文件必須通過編譯後才能被瀏覽器識別。同時,對資源文件的版本處理(加版本號),也須要藉助HtmlWebpackPlugin這個插件注入到對應模板上。就像流程圖中示意的那樣,當訪問路由時(如 localhost:3000/blog),ejs 加載的並非 src/views 下的模板,而是編譯後(此時 css/js的引用已經注入到頁面中)的位於 dist/views下的新的模板文件。多入口對應多個模板,每一個模板文件和入口文件應該有個映射關係,這個關係能夠經過維護一個map來實現(不利於增改),也能夠經過文件命名規則來實現。這裏採用命名規則來實現,這樣更有利於自動化。react

// path: scripts/webpack.config.js
// ...

// 遍歷webpackConfig入口, key 對應了模板的文件名,這個命名規則能夠更復雜些,好比增長對子目錄的支持
// {
//   index: 'views/index.html',
//   blog: 'views/blog.html'
// }

const isProduction = process.env.NODE_ENV === 'production'

Object.keys(webpackConfig.entry).forEach(entry => {
  // 在 plugins 配置中增長了多個 HtmlWebpackPlugin 實例
  webpackConfig.plugins.push(new HtmlWebpackPlugin({
    filename: 'views/' + entry + '.html',
    template: path.resolve(__dirname, `../src/views/${entry}.html`),
    chunks: [entry],  // 將入口文件打包後的文件注入到對應的頁面中
    alwaysWriteToDisk: true,  // 該配置項說明見 [ejs模板文件沒法使用內存文件的解決方法] 章節
    minify: {
      removeComments: isProduction,
      collapseWhitespace: isProduction,
      removeAttributeQuotes: false,
      minifyCSS: isProduction,
      minifyJS: isProduction
    },
  }))
})

ejs模板文件沒法使用內存文件的解決方法

webpack-dev-middleware 的一個重要特性就是生成的文件都位於內存中,是一個內存型的文件系統。而koa-ejs做爲渲染引擎只能加載真實的物理文件,當它加載 dist/vies/*.html時會報文件未找到的錯。所以,對模板文件的編譯就不能再像其餘資源同樣生成於內存中,而是要把模板文件真真切切的生成爲文件。HtmlWebpackHarddiskPlugin 這個webpack插件能夠完美解決。webpack

npm i -D html-webpack-harddisk-plugin
// path: scripts/webpack.config.js
// ...
const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin')
// ... 見 [入口文件映射模板文件] 章節
webpackConfig.plugins.push(new HtmlWebpackPlugin({
  // 增長該配置項
  alwaysWriteToDisk: true, 
}))
// ...

// 應用 HtmlWebpackHarddiskPlugin 插件
webpackConfig.plugins.push(new HtmlWebpackHarddiskPlugin())

先後端typescript配置文件的衝突

Server端和前端可能在typescript的配置上有所不一樣,尤爲是在一些編譯選項上。此時須要兩個不一樣的配置文件。tsconfig.json是默認的TypeScript配置文件, 這裏就做爲Server端的配置項,根目錄新建 tsconfig.front.json 做爲前端的配置文件:git

// ./tsconfig.front.json
{
  "compilerOptions": {
    "outDir": "./dist/",
    "noImplicitAny": true,
    "module": "es6",
    "target": "es5",
    "jsx": "react",
    "allowJs": true
  },
  "include":[
    "src/assets/**/*",
    "src/entries/**/*"
  ],
  "exclude": [
    "node_modules"
  ]
}

同時,須要在webpack配置文件中指定配置文件路徑:es6

// path: scripts/webpack.config.js
// ...
 webpackConfig.module = {
    rules: [{
        test: /\.tsx?$/,
        include: [
          path.resolve(__dirname, '../src/')
        ],
        use: [{
          loader: 'ts-loader',
          options: {
            // 指定配置文件
            configFile: '../tsconfig.front.json'
          }
        }],
      },
      // ...
    ],
  },
// ...

至此,基於基於WEBPACK/TYPESCRIPT/KOA的先後端多頁面開發環境配置完畢。配置nodemon, nodemon將監視啓動目錄中的文件,若是有任何文件更改,nodemon將自動從新啓動node應用程序。

運行 npm start, 其實是運行 nodemon, nodemon將根據 nodemon.json配置項來啓動 npm run dev命名。當src目錄下的文件有任何變化時,它將重啓應用程序。
// ./nodemon.json
{
  "watch": ["src", "server"],
  "exec": "npm run dev",
  "ext": "ts"
}

package.jsonscripts中加入運行腳本方便一鍵啓動。

// ./package.json
{
  "scripts": {
    "start": "nodemon",
    "dev": "rm -rf dist && cross-env NODE_ENV=development ts-node bin/dev.server.ts",
  }
}

生產環境(production)流程

生產環境流程

相對而言,生產環境的配置就簡單多了。當運行npm run build時,仍是分兩步走;

  • 經過 tsc 命令將 server 下的服務端代碼所有編譯到 dist/server目錄;
  • 經過 webpack 命令將 src 下的前端代碼所有編譯到 dist/* 相應目錄;
  • 當經過 pm2 restart ./bin/pm2.json 或者 node ./bin/app.js (須要設置環境變量爲production) 啓動服務時,實際上已經運行的是編譯後的代碼。這裏須要注意兩點:

    • static 目錄指向了 dist/static
    • views 目錄指向了 dist/views
// ./server/app.ts
// 獲取環境變量
const env = process.env.NODE_ENV || 'development'
const isDev = env === 'development'
require('koa-ejs')(app, {
  // root 爲通過webpack編譯後的真實模板路徑
  // 生產環境下,server已經在dist目錄,修改以下:
  root: path.resolve(__dirname, isDev ? '../dist/views' : '../views'),
})
// ./bin/app.js
// 引用了編譯後的 app.js 主文件
const app = require('../dist/server/app')
const path = require('path')
// 設置靜態資源目錄
app.use(require('koa-static')(path.resolve(__dirname, '../dist')))

此時,dist目錄結構以下:

.
├── server
│   ├── app.js
│   └── middleware
│       └── webpack-dev-middleware.js
├── static
│   ├── css
│   │   ├── blog.4dcddae.css
│   │   └── index.4dcddae.css
│   └── js
│       ├── blog.4dcddae.js
│       └── index.4dcddae.js
└── views
    ├── blog.html
    └── index.html

小結

至此,基於webpack/koa/typescript的多頁面服務端渲染的項目以及開發和生產環境的配置已經搭建完畢。其中webpack-dev-middleware在開發環境中提供了橋樑的做用。TypeScript做爲JavaScript的超集,不只能夠有效杜絕由變量類型引發的誤用問題,並且經過@types和如vscode等編輯器的配合,能夠更方便快速的讓開發者瞭解一些庫/框架的API。

完整webpack配置

webpack.config.js

項目地址

GitHub地址

相關閱讀

相關文章
相關標籤/搜索