前段時間有寫過一個TypeScript在node項目中的實踐。
在裏邊有解釋了爲何要使用TS
,以及在Node
中的一個項目結構是怎樣的。
可是那僅僅是一個純接口項目,碰巧遇上近期的另外一個項目重構也由我來主持,通過上次的實踐之後,嚐到了TS
所帶來的甜頭,堅決果斷的選擇用TS
+React
來重構這個項目。
此次的重構不只包括Node
的重構(以前是Express
的項目),同時還包括前端的重構(以前是由jQuery
驅動的多頁應用)。javascript
由於目前項目是沒有作先後分離的打算的(一個內部工具平臺類的項目),因此大體結構就是基於上次Node
項目的結構,在其之上添加了一些FrontEnd
的目錄結構:css
. ├── README.md ├── copy-static-assets.ts ├── nodemon.json ├── package.json + ├── client-dist + │ ├── bundle.js + │ ├── bundle.js.map + │ ├── logo.png + │ └── vendors.dll.js ├── dist ├── src │ ├── config │ ├── controllers │ ├── entity │ ├── models │ ├── middleware │ ├── public │ ├── app.ts │ ├── server.ts │ ├── types + │ ├── common │ └── utils + ├── client-src + │ ├── components + │ │ └── Header.tsx + │ ├── conf + │ │ └── host.ts + │ ├── dist + │ ├── utils + │ ├── index.ejs + │ ├── index.tsx + │ ├── webpack + │ ├── package.json + │ └── tsconfig.json + ├── views + │ └── index.ejs ├── tsconfig.json └── tslint.json
其中標綠(也多是一個+
號顯示)的文件爲本次新增的。
其中client-dist
與views
都是經過webpack
生成的,實際的源碼文件都在client-src
下。就這個結構拆分先後分離其實沒有什麼成本
在下邊分了大概這樣的一些文件夾:html
dir/file | desc |
---|---|
index.ejs |
項目的入口html 文件,採用ejs 做爲渲染引擎 |
index.tsx |
項目的入口js 文件,後綴使用tsx ,緣由有二:1. 咱們會使用 ts 進行React 程序的開發 2. .tsx 文件在vs code上的icon 比較好看 :p |
tsconfig.json |
是用於tsc 編譯執行的一些配置文件 |
components |
組件存放的目錄 |
config |
各類配置項存放的位置,相似請求接口的host 或者各類狀態的map 映射之類的(能夠理解爲枚舉對象們都在這裏) |
utils |
一些公共函數存放的位置,各類可複用的代碼都應該放在這裏 |
dist |
各類靜態資源的存放位置,圖片之類文件 |
webpack |
裏邊存放了各類環境的webpack 腳本命令以及dll 的生成 |
實際上邊還漏掉了一個新增的文件夾,咱們在src
目錄下新增了一個common
目錄,這個目錄是存放一些公共的函數和公共的config
,不一樣於utils
或者config
的是,這裏的代碼是先後端共享的,因此這裏邊的函數必定要是徹底的不包含任何環境依賴,不包含任何業務邏輯的。前端
相似的數字千分位,日期格式化,抑或是服務監聽的端口號,這些不包含任何邏輯,也對環境沒有強依賴的代碼,咱們均可以放在這裏。
這也是沒有作先後分離帶來的一個小甜頭吧,先後能夠共享一部分代碼。java
要實現這樣的配置,基於上述項目須要修改以下幾處:node
src
下的utils
和config
部分代碼遷移到common
文件夾下,主要是用於區分是否可先後通用node
結構方面的影響降至最低,咱們須要在common
文件夾下新增一個index.ts
索引文件,並在utils/index.ts
下引用它,這樣對於node
方面使用來說,並不須要關心這個文件是來自utils
仍是common
// src/common/utils/comma.ts export default (num: number): string => String(num).replace(/\B(?=(\d{3})+$)/g, ',') // src/common/utils/index.ts export { default as comma } from './comma' // src/utils.index.ts export * from '../common/utils' // src/app.ts import { comma } from './utils' // 並不須要關心是來自common仍是來自utils console.log(comma(1234567)) // 1,234,567
webpack
的alias
屬性,用於webpack
可以正確的找到其路徑// client-src/webpack/base.js module.exports = { resolve: { alias: { '@Common': path.resolve(__dirname, '../../src/common'), } } }
tsconfig.json
用於vs code
能夠找到對應的目錄,否則會在編輯器中提示can't find module XXX
// client-src/tsconfig.json { "compilerOptions": { "paths": { // 用於引入某個`module` "@Common/*": [ "../src/common/*" ] } } }
client-src/utils/index.ts
寫上相似server
端的處理就能夠了// client-src/utils/index.ts export * from '@Common/utils' // client-src/index.tsx import { comma } from './utils' console.log(comma(1234567)) // 1,234,567
若是使用vs code
進行開發,並且使用了ESLint
的話,須要修改TS
語法支持的後綴,添加typescriptreact
的一些處理,這樣纔會自動修復一些ESLint
的規則:react
"eslint.validate": [ "javascript", "javascriptreact", { "language": "typescript", "autoFix": true }, { "language": "typescriptreact", "autoFix": true } ]
由於在前端使用了React
,按照目前的主流,webpack
確定是必不可少的。
並無選擇成熟的cra
(create-react-app)來進行環境搭建,緣由有下:webpack
webpack
更新到4之後並無嘗試過,想本身耍一耍TS
以及公司內部的東西,會有一些自定義配置狀況的出現,擔憂二次開發太繁瑣可是其實也沒有太多的配置,本次重構選用的UI框架爲Google Material的實現:material-ui
而他們採用的是jss 來進行樣式的編寫,因此也不會涉及到以前慣用的scss
的那一套loader
了。git
webpack
分了大概以下幾個文件:es6
file | desc |
---|---|
common.js |
公共的webpack 配置,相似env 之類的選項 |
dll.js |
用於將一些不會修改的第三方庫進行提早打包,加快開發時編譯效率 |
base.js |
能夠理解爲是webpack 的基礎配置文件,通用的loader 以及plugins 在這裏 |
pro.js |
生產環境的特殊配置(代碼壓縮、資源上傳) |
dev.js |
開發環境的特殊配置(source-map ) |
dll
是一個很早以前的套路了,大概須要修改這麼幾處:
webpack
文件,用於生成dll
文件webpack
文件中進行引用生成的dll
文件// dll.js { entry: { // 須要提早打包的庫 vendors: [ 'react', 'react-dom', 'react-router-dom', 'babel-polyfill', ], }, output: { filename: 'vendors.dll.js', path: path.resolve(__dirname, '../../client-dist'), // 輸出時不要少了這個option library: 'vendors_lib', }, plugins: [ new webpack.DllPlugin({ context: __dirname, // 向外拋出的`vendors.dll.js`代碼的具體映射,引用`dll`文件的時候經過它來作映射關係的 path: path.join(__dirname, '../dist/vendors-manifest.json'), name: 'vendors_lib', }) ] } // base.js { plugins: [ new webpack.DllReferencePlugin({ context: __dirname, manifest: require('../dist/vendors-manifest.json'), }), ] }
這樣在watch
文件時,打包就會跳過verdors
中存在的那些包了。
有一點要注意的,若是最終須要上傳這些靜態資源,記得連帶着verdors.dll.js
一併上傳
在本地開發時,vendors
文件並不會自動注入到html
模版中去,因此咱們有用到了另外一個插件,add-asset-html-webpack-plugin。
同時在使用中可能還會遇到webpack
無限次數的從新打包,這個須要配置ignore
來解決-.-:
// dev.js const HtmlWebpackPlugin = require('html-webpack-plugin') const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin') { plugins: [ // 將`ejs`模版文件放到目標文件夾,並注入入口`js`文件 new HtmlWebpackPlugin({ template: path.resolve(__dirname, '../index.ejs'), filename: path.resolve(__dirname, '../../views/index.ejs'), }), // 將`vendors`文件注入到`ejs`模版中 new AddAssetHtmlPlugin({ filepath: path.resolve(__dirname, '../../client-dist/vendors.dll.js'), includeSourcemap: false, }), // 忽略`ejs`和`js`的文件變化,避免`webpack`無限從新打包的問題 new webpack.WatchIgnorePlugin([ /\.ejs$/, /\.js$/, ]), ] }
TS
的配置分了兩塊,一個是webpack
的配置,另外一個是tsconfig
的配置。
首先是webpack
,針對ts
、tsx
文件咱們使用了兩個loader
:
{ rules: [ { test: /\.tsx?$/, use: ['babel-loader', 'ts-loader'], exclude: /node_modules/, } ], resolve: { // 必定不要忘記配置ts tsx後綴 extensions: ['.tsx', '.ts', '.js'], } }
ts-loader
用於將TS
的一些特性轉換爲JS
兼容的語法,而後執行babel
進行處理react/jsx
相關的代碼,最終生成可執行的JS
代碼。
而後是tsconfig
的配置,ts-loader
的執行是依託於這裏的配置的,大體的配置以下:
{ "compilerOptions": { "module": "esnext", "target": "es6", "allowSyntheticDefaultImports": true, // import的相對起始路徑 "baseUrl": ".", "sourceMap": true, // 構建輸出目錄,但由於使用了`webpack`,因此這個配置並無什麼卵用 "outDir": "../client-dist", // 開啓`JSX`模式, // `preserve`的配置讓`tsc`不會去處理它,而是使用後續的`babel-loader`進行處理 "jsx": "preserve", "strict": true, "moduleResolution": "node", // 開啓裝飾器的使用 "experimentalDecorators": true, "emitDecoratorMetadata": true, // `vs code`所須要的,在開發時找到對應的路徑,真實的引用是在`webpack`中配置的`alias` "paths": { "@Common": [ "../src/common" ], "@Common/*": [ "../src/common/*" ] } }, "exclude": [ "node_modules" ] }
最近這段時間,咱們團隊基於airbnb
的ESLint
規則進行了一些自定義,建立了自家的eslint-config-blued
同時還存在了react和typescript的兩個衍生版本。
關於ESLint
的配置文件.eslintrc
,在本項目中存在兩份。一個是根目錄的blued-typescript
,另外一個是client-src
下的blued-react
+ blued-typescript
。
由於根目錄的更多用於node
項目,因此不必把react
什麼的依賴也裝進來。
# .eslintrc extends: blued-typescript # client-src/.eslintrc extends: - blued-react - blued-typescript
一個須要注意的小細節
由於咱們的react
與typescript
實現版本中都用到了parser
。
react
使用的是babel-eslint,typescript
使用的是typescript-eslint-parser。
可是parser
只能有一個,從option
的命名中就能夠看出extends
、plugins
、rules
,到了parser
就沒有複數了。
因此這兩個插件在extends
中的順序就變得很關鍵,babel
如今並不能理解TS
的語法,但好像babel
開發者有支持TS
的意願。
但就目前來講,必定要保證react
在前,typescript
在後,這樣parser
纔會使用typescript-eslint-parser
來進行覆蓋。
除了上邊提到的兩端公用代碼之外,還須要添加一個controller
用於吐頁面,由於使用的是routing-controllers
這個庫,渲染一個靜態頁面被封裝的很是棒,僅僅須要修改兩個頁面,一個用於設置render
模版的根目錄,另外一個用來設置要吐出來的模版名稱:
// controller/index.ts import { Get, Controller, Render, } from 'routing-controllers' @Controller('/') export default class { @Get('/') @Render('index') // 指定一個模版的名字 async router() { // 渲染頁面時的一些變量 // 相似以前的 ctx.state = XXX return { title: 'First TypeScript React App', } } } // app.ts import koaViews from 'koa-views' // 添加模版所在的目錄 // 以及使用的渲染引擎、文件後綴 app.use(koaViews(path.join(__dirname, '../views'), { options: { ext: 'ejs', }, extension: 'ejs', }))
若是是多個頁面,那就建立多個用來Render
的ts
文件就行了
目前的routing-controller
對於Koa
的支持還不是很好,(原做者對Koa
並非很瞭解,致使Render
對應的接口被請求一次之後,後續全部的其餘的接口都會直接返回該模版文件,緣由是在負責模版渲染的URL
觸發時,本應返回數據,可是目前的處理倒是添加了一箇中間件到Koa
中,因此任何請求都會將該模版文件做爲數據來返回)因此@Render
並不能適用於Koa
驅動。
不過我已經提交了PR了,跑通了測試用例,坐等被合併代碼,可是這是一個臨時的修改方案,涉及到這個庫針對外部中間件註冊的順序問題,因此對於app.ts
還要有額外的修改纔可以實現。
// app.ts 的修改 import 'reflect-metadata' import Koa from 'koa' import koaViews from 'koa-views' import { useKoaServer } from 'routing-controllers' import { distPath } from './config' // 手動建立koa實例,而後添加`render`的中間件,確保`ctx.render`方法會在請求的頭部就被添加進去 const koa = new Koa() koa.use(koaViews(path.join(__dirname, '../views'), { options: { ext: 'ejs', }, extension: 'ejs', })) // 使用`useKoaServer`而不是`createKoaServer` const app = useKoaServer(koa, { controllers: [`${__dirname}/controllers/**/*{.js,.ts}`], }) // 後續的邏輯就都同樣了 export default app
固然,這個是新版發出之後的邏輯了,基於現有的結構也能夠繞過去,可是就不能使用@Render
裝飾器了,拋開koa-views
直接使用內部的consolidate:
// controller/index.ts // 這個修改不須要改動`app.ts`,能夠直接使用`createKoaServer` import { Get, Controller, } from 'routing-controllers' import cons from 'consolidate' import path from 'path' @Controller() export default class { @Get('/') async router() { // 直接在接口返回時獲取模版渲染後的數據 return cons.ejs(path.resolve(__dirname, '../../views/index.ejs'), { title: 'Example For TypeScript React App', }) } }
目前的示例代碼採用的上邊的方案
至此,一個完整的TS先後端項目架構就已經搭建完成了(剩下的任務就是往骨架裏邊填代碼了)。
我已經更新了以前的typescript-exmaple 在裏邊添加了本次重構所使用的一些前端TS
+React
的示例,還包括針對@Render
的一些兼容。
TypeScript
是一個很棒的想法,解決了N多javaScript
種使人詬病的問題。
使用靜態語言來進行開發不只可以提升開發的效率,同時還能下降錯誤出現的概率。
結合着強大的vs code
,Enjoy it.
若是在使用TS
的過程當中有什麼問題、或者有什麼更好的想法,歡迎來溝通討論。