TypeScript在react項目中的實踐

來源:互聯網
上載者:User

前段時間有寫過一個TypeScript在node項目中的實踐。
在裡邊有解釋了為什麼要使用TS,以及在Node中的一個項目結構是怎樣的。
但是那僅僅是一個純介面項目,碰巧趕上近期的另一個項目重構也由我來主持,經過上次的實踐以後,嘗到了TS所帶來的甜頭,毫不猶豫的選擇用TS+React來重構這個項目。
這次的重構不僅包括Node的重構(之前是Express的項目),同時還包括前端的重構(之前是由jQuery驅動的多頁應用)。

項目結構

因為目前項目是沒有做前後分離的打算的(一個內部工具平台類的項目),所以大致結構就是基於上次Node項目的結構,在其之上添加了一些FrontEnd的目錄結構:

  .  ├── 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-distviews都是通過webpack產生的,實際的源碼檔案都在client-src下。就這個結構拆分前後分離其實沒有什麼成本
在下邊分了大概這樣的一些檔案夾:

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的是,這裡的代碼是前後端共用的,所以這裡邊的函數一定要是完全的不包含任何環境依賴,不包含任何商務邏輯的。

類似的數字千分位,日期格式化,抑或是服務監聽的連接埠號碼,這些不包含任何邏輯,也對環境沒有強依賴的代碼,我們都可以放在這裡。
這也是沒有做前後分離帶來的一個小甜頭吧,前後可以共用一部分代碼。

要實現這樣的配置,基於上述項目需要修改如下幾處:

  1. src下的utilsconfig部分代碼遷移到common檔案夾下,主要是用於區分是否可前後通用
  2. 為了將對之前node結構方面的影響降至最低,我們需要在common檔案夾下新增一個index.ts索引檔案,並在utils/index.ts下引用它,這樣對於node方面使用來講,並不需要關心這個檔案是來自utils還是common
// src/common/utils/comma.tsexport default (num: number): string => String(num).replace(/\B(?=(\d{3})+$)/g, ',')// src/common/utils/index.tsexport { default as comma } from './comma'// src/utils.index.tsexport * from '../common/utils'// src/app.tsimport { comma } from './utils' // 並不需要關心是來自common還是來自utilsconsole.log(comma(1234567)) // 1,234,567
  1. 然後是配置webpackalias屬性,用於webpack能夠正確的找到其路徑
// client-src/webpack/base.jsmodule.exports = {  resolve: {    alias: {       '@Common': path.resolve(__dirname, '../../src/common'),    }  }}
  1. 同時我們還需要配置tsconfig.json用於vs code可以找到對應的目錄,不然會在編輯器中提示can't find module XXX
// client-src/tsconfig.json{  "compilerOptions": {    "paths": {      // 用於引入某個`module`      "@Common/*": [        "../src/common/*"      ]    }  }}
  1. 最後在client-src/utils/index.ts寫上類似server端的處理就可以了
// client-src/utils/index.tsexport * from '@Common/utils'// client-src/index.tsximport { comma } from './utils'console.log(comma(1234567)) // 1,234,567
環境的搭建

如果使用vs code進行開發,而且使用了ESLint的話,需要修改TS文法支援的尾碼,添加typescriptreact的一些處理,這樣才會自動修複一些ESLint的規則:

"eslint.validate": [  "javascript",  "javascriptreact",  {    "language": "typescript",    "autoFix": true  },  {    "language": "typescriptreact",    "autoFix": true  }]
webpack的配置

因為在前端使用了React,按照目前的主流,webpack肯定是必不可少的。
並沒有選擇成熟的cra(create-react-app)來進行環境搭建,原因有下:

  1. webpack更新到4以後並沒有嘗試過,想自己耍一耍
  2. 結合著TS以及公司內部的東西,會有一些自訂配置情況的出現,擔心二次開發太繁瑣

但是其實也沒有太多的配置,本次重構選用的UI架構為Google Material的實現:material-ui
而他們採用的是jss 來進行樣式的編寫,所以也不會涉及到之前慣用的scss的那一套loader了。

webpack分了大概如下幾個檔案:

file desc
common.js 公用的webpack配置,類似env之類的選項
dll.js 用於將一些不會修改的第三方庫進行提前打包,加快開發時編譯效率
base.js 可以理解為是webpack的基礎設定檔,通用的loader以及plugins在這裡
pro.js 生產環境的特殊配置(代碼壓縮、資源上傳)
dev.js 開發環境的特殊配置(source-map

dll是一個很早之前的套路了,大概需要修改這麼幾處:

  1. 建立一個單獨的webpack檔案,用於產生dll檔案
  2. 在普通的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.jsconst 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$/,    ]),  ]}
TypeScript相關的配置

TS的配置分了兩塊,一個是webpack的配置,另一個是tsconfig的配置。

首先是webpack,針對tstsx檔案我們使用了兩個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"  ]}
ESLint的配置

最近這段時間,我們團隊基於airbnbESLint規則進行了一些自訂,建立了自家的eslint-config-blued
同時還存在了react和typescript的兩個衍生版本。

關於ESLint的設定檔.eslintrc,在本項目中存在兩份。一個是根目錄的blued-typescript,另一個是client-src下的blued-react + blued-typescript
因為根目錄的更多用於node項目,所以沒必要把react什麼的依賴也裝進來。

# .eslintrcextends: blued-typescript# client-src/.eslintrcextends:   - blued-react  - blued-typescript

一個需要注意的小細節
因為我們的reacttypescript實現版本中都用到了parser
react使用的是babel-eslint,typescript使用的是typescript-eslint-parser。
但是parser只能有一個,從option的命名中就可以看出extendspluginsrules,到了parser就沒有複數了。
所以這兩個外掛程式在extends中的順序就變得很關鍵,babel現在並不能理解TS的文法,但好像babel開發人員有支援TS的意願。
但就目前來說,一定要保證react在前,typescript在後,這樣parser才會使用typescript-eslint-parser來進行覆蓋。

node層的修改

除了上邊提到的兩端公用代碼以外,還需要添加一個controller用於吐頁面,因為使用的是routing-controllers這個庫,渲染一個靜態頁面被封裝的非常棒,僅僅需要修改兩個頁面,一個用於設定render模版的根目錄,另一個用來設定要吐出來的模版名稱:

// controller/index.tsimport {  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.tsimport koaViews from 'koa-views'// 添加模版所在的目錄// 以及使用的渲染引擎、檔案尾碼app.use(koaViews(path.join(__dirname, '../views'), {  options: {    ext: 'ejs',  },  extension: 'ejs',}))

如果是多個頁面,那就建立多個用來Renderts檔案就好了

深坑,注意

目前的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的過程中有什麼問題、或者有什麼更好的想法,歡迎來溝通討論。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.