前段时间有写过一个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
的过程当中有什么问题、或者有什么更好的想法,欢迎来沟通讨论。