• TypeScript在react项目中的实践


    前段时间有写过一个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.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
    
    1. 然后是配置webpackalias属性,用于webpack能够正确的找到其路径
    // client-src/webpack/base.js
    module.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.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的规则:

    "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.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$/,
        ]),
      ]
    }
    

    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
    同时还存在了reacttypescript的两个衍生版本。

    关于ESLint的配置文件.eslintrc,在本项目中存在两份。一个是根目录的blued-typescript,另一个是client-src下的blued-react + blued-typescript
    因为根目录的更多用于node项目,所以没必要把react什么的依赖也装进来。

    # .eslintrc
    extends: blued-typescript
    
    # client-src/.eslintrc
    extends: 
      - blued-react
      - blued-typescript
    

    一个需要注意的小细节
    因为我们的reacttypescript实现版本中都用到了parser
    react使用的是babel-eslinttypescript使用的是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.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',
    }))
    

    如果是多个页面,那就创建多个用来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的过程中有什么问题、或者有什么更好的想法,欢迎来沟通讨论。

  • 相关阅读:
    css子元素水平垂直居中
    js 防抖节流
    NOIP 游记
    flash player播放器用法
    android设备连接不上电脑的解决方法
    AndroidStudio自动下载gradle失败问题解决
    3组Alpha冲刺5/6
    3组Beta冲刺2/5
    3组Beta冲刺5/5
    3组Beta冲刺1/5
  • 原文地址:https://www.cnblogs.com/jiasm/p/9542253.html
Copyright © 2020-2023  润新知