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}
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模板文件沒法使用內存文件的解決方法]章節)。
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
// 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
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 }, })) })
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())
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.json
的scripts
中加入運行腳本方便一鍵啓動。
// ./package.json { "scripts": { "start": "nodemon", "dev": "rm -rf dist && cross-env NODE_ENV=development ts-node bin/dev.server.ts", } }
相對而言,生產環境的配置就簡單多了。當運行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。