问题描述:

前段时间有写过一个TypeScript在node项目中的实践。
在里边有解释了为什么要使用TS,以及在Node中的一个项目结构是怎样的。
但是那仅仅是一个纯接口项目,碰巧赶上近期的另一个项目重构也由我来主持,经过上次的实践以后,尝到了TS所带来的甜头,毫不犹豫的选择用TS+React来重构这个项目。
这次的重构不仅包括Node的重构(之前是Express的项目),同时还包括前端的重构(之前是由jQuery驱动的多页应用)。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
        <script>
            window.onload = function() {
                var oldColor;
                var tblEle = document.getElementById("tbl");
                var len = tblEle.tBodies[0].rows.length;
                for(var i = 0;i<len;i++) {
                    if(i%2==0) {
                        tblEle.tBodies[0].rows[i].style.backgroundColor = "pink";
                        //添加鼠标经过事件
                        tblEle.tBodies[0].rows[i].onmouseover = function() {
                            oldColor = this.style.backgroundColor;
                            this.style.backgroundColor = "blue";
                        }
                        // 添加鼠标离开事件
                        tblEle.tBodies[0].rows[i].onmouseout = function() {
                            this.style.backgroundColor = oldColor;
                        }
                    }else{
                        tblEle.tBodies[0].rows[i].style.backgroundColor = "gold";
                        //添加鼠标经过事件
                        tblEle.tBodies[0].rows[i].onmouseover = function() {
                            oldColor = this.style.backgroundColor;
                            this.style.backgroundColor = "blue";
                        }
                        // 添加鼠标离开事件
                        tblEle.tBodies[0].rows[i].onmouseout = function() {
                            this.style.backgroundColor = oldColor;
                        }
                    }
                }
            }
        </script>
    </head>
    <body>
        <table id="tbl" border="1" border-collapse="collapse" align="center" cellspacing="0" cellpadding="5" width="400" height="20">
            <thead>
                <tr>
                    <th>编号</th><th>姓名</th><th>年龄</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td>1</td>
                    <td>张三</td>
                    <td>12</td>
                </tr>
                <tr>
                    <td>2</td>
                    <td>李四</td>
                    <td>22</td>
                </tr>
                <tr>
                    <td>3</td>
                    <td>王五</td>
                    <td>13</td>
                </tr>
                <tr>
                    <td>4</td>
                    <td>马六</td>
                    <td>14</td>
                </tr>
                <tr>
                    <td>5</td>
                    <td>伍六七</td>
                    <td>17</td>
                </tr>
                <tr>
                    <td>6</td>
                    <td>梅花十三</td>
                    <td>17</td>
                </tr>
            </tbody>
        </table>
    </body>
</html>

图片 1

项目结构

因为目前项目是没有做前后分离的打算的(一个内部工具平台类的项目),所以大致结构就是基于上次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

图片 2

环境的搭建

如果使用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"
  ]
}

在 js 的语法中,如果语句独占一行,通常可以省略句末的分号

ESLint的配置

最近这段时间,我们团队基于airbnbESLint规则进行了一些自定义,创建了自家的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
    

一个需要注意的小细节
因为我们的reacttypescript实现版本中都用到了parser
react使用的是babel-eslint,typescript使用的是typescript-eslint-parser。
但是parser只能有一个,从option的命名中就可以看出extendspluginsrules,到了parser就没有复数了。
所以这两个插件在extends中的顺序就变得很关键,babel现在并不能理解TS的语法,但好像babel开发者有支持TS的意愿。
但就目前来说,一定要保证react在前,typescript在后,这样parser才会使用typescript-eslint-parser来进行覆盖。

发表评论

电子邮件地址不会被公开。 必填项已用*标注