搭建自己的React+Typescript环境(一)

10,310 阅读9分钟

前言

前阵子在自己学习React,最开始上手使用的creat-react-app来创建自己的项目,2版本之后的create-react-app已经支持了很多功能,比如sass、数据mock、typescript支持等等,也升级了相关依赖babel、webpack到一个最新的版本,具体可以参照Create React App 中文文档,但是它将项目的webpack配置等内容给藏起来了,想要自己配置的话还要npm run eject才可见,不过对于我这种初学者已经足够了,但是本着折腾的精神,在掘金看了好多大佬的配置文章,终于折腾出一个自己的项目模板,如果有什么问题或者不对的地方,希望大佬们能及时指出,最后有项目地址~

第二篇生产开发环境配置已经写完:搭建自己的React+Typescript环境(二)

项目简介

主要的依赖以及版本

  • webpack4+
  • babel8+
  • typescript3.7+
  • react16.12+
  • antd3+
  • react-router5+
  • eslint6+

初始化项目

  1. 创建一个目录,名字按自己喜好来
    mkdir react-ts-template
    cd react-ts-template
    
  2. 初始化项目,填写项目信息
    yarn init -y 或者 npm init -y
    

安装webpack

yarn add webpack -D 或者 npm i webpack -D
yarn add webpack-cli -D 或者 npm i webpack-cli -D
  • webpack也可以全局安装,不过要注意配置PATH
  • webpack4将命令行相关的操作抽离到了webpack-cli中,比如init、migrate、serve等等,不过都没用过

安装完毕后在根目录新建build文件夹,并新建一个webpack.common.js文件,用来存放webpack的公共配置

mkdir build
cd build
touch webapck.common.js

然后在webpack.common.js中简单的配置入口(entry)跟输出(output)。

const path = require('path');
module.exports={
  entry: path.join(__dirname, '../src/index.js'),
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, '../dist')
  }
}

接着在根目录下再新建src文件夹,用来存放主要代码,并新建index.js,随便写点东西。

console.log('Hello World');

在package.json中加入一个脚本,并在控制台中运行它npm run build

"scripts": {
    "build": "webpack --config build/webpack.common.js"
}

之后会发现生成了一个dist文件夹,并且还有一个bundle.js,同时控制台还会有报错

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/

webpack4中提供了 mode 配置选项,告知 webpack 使用相应模式的内置优化,上面这个警告写着如果不提供mode,webpack将会使用production模式。我们把scripts修改一下。

"scripts": {
    "build": "webpack --config build/webpack.common.js --mode production"
}

这样的webpack简单的打包功能就有了。

Babel

Babel这里使用的是7版本,与之前版本不同的是安装依赖时的包名,像babel-core、babel-preset-env、babel-polyfill等,名字已经更换成了@babel/core、@babel/preset-env、@babel/polyfill,这里先安装主要的包。

yarn add @babel/core @babel/preset-env @babel/preset-react babel-loader -D

然后在根目录下新建一个babel.config.js,这个配置文件跟.babelrc.js是有区别的,根据官网来看babel.config.js是项目级别的一个配置,详细信息可以参照官网 Config Files,在其中添加如下内容:

module.exports = {
  presets: [
    '@babel/preset-env',
    '@babel/preset-react'
  ],
  plugins: []
}

修改webpack.common.js,增加 js 文件的 loader 配置,之后还会改。

  module: {
    rules: [{
        test: /\.js$/,
        use: ['babel-loader'],
        include: path.join(__dirname, '../src')
    }]
  }

React

接下来加入React,也是最重要的部分。

yarn add react react-dom

修改 src/index.js 中的内容

import React from 'react';
import ReactDom from 'react-dom';

ReactDom.render(<div>Hello React!</div>, document.getElementById('root'));

然后在根目录下新建一个public文件夹,并在里面新建一个index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>React-TS-Tempalte</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>

想要 webpack 能以这个html为模板,还需要一个html-webpack-plugin插件,安装它yarn add html-webpack-plugin -D并在webpack.common.js中增加如下配置,这也是用到的第一个插件:

const HtmlWebpackPlugin = require('html-webpack-plugin');
...
plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'public/index.html',
      inject: true
    })
]

让我们打包后打开dist下的index.html看看效果,成功地展示了Hello React。

开发环境配置

接下来安装 webpack-dev-server 来启动一个简单的服务器。

yarn add webpack-dev-server -D

修改webpack.common.config.js,增加webpack-dev-server的配置。

  devServer: {
    host: 'localhost',
    port: 3000,
    historyApiFallback: true,
    overlay: {
      //当出现编译器错误或警告时,就在网页上显示一层黑色的背景层和错误信息
      errors: true
    },
    inline: true,
    hot: true
  }

接下来需要在package.json中增加一个script

"scripts": {
    "dev": "webpack-dev-server --config build/webpack.common.js --mode development --open"
}

在控制台中输入npm run dev,便可以在http://localhost:3000 中看到启动的项目。

Typescript

下面加入Typescript依赖,关键的依赖包就是 typescript,不过还需要安装对应的types:@types/react、@types/react-dom。

yarn add typescript @types/react @types/react-dom -D

接下来需要把之前的 js、jsx 文件替换成对应的 ts、tsx,同时还需要对应的loader,可以使用 ts-loader 以及之前安装过的 babel-loader,这里使用之前安装的 babel-loader,在webpack.common.js中添加配置:

rules: [
  {
    test: /\.(j|t)sx?$/,
    include: [resolve('../src')],
    use: [
      {
        loader: 'babel-loader'
      }
    ],
    // 排除node_modules底下的
    exclude: /node_modules/
  }
]

修改 webpack 的入口配置

module.exports={
  entry: path.join(__dirname, '../src/index.tsx')
}

不要忘记把src下的index.js改成index.tsx

配置tsconfig.json,这个文件也是使用ts时很关键的一个文件,下面是官网的推荐配置。

{
  "compilerOptions": {
    /* Basic Options */
    "target": "es5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
    "module": "esnext",                       /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],                                        /* Specify library files to be included in the compilation. */
    "allowJs": true,                          /* Allow javascript files to be compiled. */
    "jsx": "react",                           /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
    "sourceMap": true,                        /* Generates corresponding '.map' file. */
    "outDir": "./dist",                       /* Redirect output structure to the directory. */
    "isolatedModules": true,                  /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
    "resolveJsonModule": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "strict": true,                           /* Enable all strict type-checking options. */
    "noImplicitThis": true,                   /* Raise error on 'this' expressions with an implied 'any' type. */
    "noImplicitReturns": true,                /* Report error when not all code paths in function return a value. */
    "moduleResolution": "node",               /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    "baseUrl": ".",                       /* Base directory to resolve non-absolute module names. */
    "paths": {},                                        /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
    "allowSyntheticDefaultImports": true,     /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    "esModuleInterop": true,                  /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    "experimentalDecorators": true,           /* Enables experimental support for ES7 decorators. */
  },
  "include": [
    "src"
  ],
  "exclude": [
    "node_modules"
  ]
}

CSS配置

这里我们需要用到 style-loadercss-loader,先安装它们

  • css-loader使你能够使用类似@import 和 url(...)的方法实现 require()的功能;
  • style-loader将所有的计算后的样式加入页面中; 二者组合在一起使你能够把样式表嵌入webpack打包后的JS文件中。
yarn add style-loader css-loader -D

然后在webpack.common.js中添加相应的规则

{
    test: /\.css$/, // 正则匹配文件路径
    exclude: /node_modules/,
    use: [
      // 注意loader生效是从下往上的
      'style-loader',
      'css-loader'
    ]
 }

接着在webpack.common.js中配置resolve.extensions,来自动解析确定的扩展。

  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.jsx']
  }

在根目录下新建一个index.css,以及App.tsx,并在index.tsx中引入它们

// index.css
.app {
    background-color: red;
}

// App.tsx
import * as React from 'react'

class App extends React.Component {
  render() {
    return (
      <div className="app">
        Hello React
      </div>
    )
  }
}
export default App

// index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import './index.css'

ReactDOM.render(<App />, document.getElementById('root'))

启动后便可以看到设置的红色背景

加入Sass

如果想要在编写样式时使用sass的语法,就需要安装相应的loader。

yarn add sass-loader node-sass -D

注意:node-sass安装时可能会遇到网络问题,需要使用淘宝镜像源。

安装完成后在webpack.common.js中加入 .scss 文件的规则

{
    test: /\.scss$/,
    include: path.join(__dirname, '../src'), // 只让loader解析我们src底下自己写的文件
    use: [
      'style-loader',
      'css-loader',
      'sass-loader'
    ]
}

接下来我们把根目录下的index.css改成index.scss,不要忘记index.tsx中引入的文件后缀也要修改,项目启动后发现可以成功解析scss文件。

配置公共sass属性

既然已经可以使用sass进行更加简便的css代码编写,那么我们也可以将常用的一些样式代码和sass变量写入公共文件中,当使用的时候就可以直接引入使用。

在src目录下新建styles文件夹,然后新建一个var.scss文件用于存放样式变量。 之后在var.scss文件里写入一个颜色变量和一个样式:

$red: red;
@mixin ellipsis {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

然后在刚才的index.scss文件中引入它。

@import './styles/var.scss';

.app{  
  background: $red;
  .aps {
    width: 50px;
    @include ellipsis;
  }
}

这样配置之后还存在一个优化上的问题,如果需要在不同的层级引入var.scss就要根据每个文件夹的路径相对来引入非常麻烦,那么我们能否做到只需要@import var.scss就行呢?答案是可以的,我们可以通过配置loader的属性includePaths进行路径优化,修改webpack.common.js。

{
    test: /\.scss$/,
    include: path.join(__dirname, '../src'),
    use: [
      'style-loader',
      'css-loader',
      {
        loader: 'sass-loader',
        include: resolve('../src'),
        options: {
          // 这个在最新的scss版本中已经不能用了2020-01-05更新,额其实一两个月前就不能这样写了
          // includePaths: [path.join(__dirname, '../src/styles')]
          // 应换成下面的
          sassOptions: {
            includePaths: [path.join(__dirname, '../src/styles')]
          }
        }
      }
    ]
}

这样之后我们在引入的时候只需要写文件名称即可。

@import 'var.scss';

加入PostCSS

什么是PostCSS呢?借用官方的话:

PostCSS 是一个允许使用 JS 插件转换样式的工具。 这些插件可以检查(lint)你的 CSS,支持 CSS Variables 和 Mixins, 编译尚未被浏览器广泛支持的先进的 CSS 语法,内联图片,以及其它很多优秀的功能。

它提供了很多常用的插件

  • 提前使用先进的 CSS 特性
    • autoprefixer 添加了 vendor 浏览器前缀,它使用 Can I Use 上面的数据。
    • postcss-preset-env 允许你使用未来的 CSS 特性。
  • 更佳的 CSS 可读性
    • precss 囊括了许多插件来支持类似 Sass 的特性,比如 CSS 变量,套嵌,mixins 等。
  • 图片和字体
    • postcss-assets 可以插入图片尺寸和内联文件。
    • postcss-sprites 能生成雪碧图。

...还有很多,具体可以查看PostCSS中文Readme

这里主要用autoprefixer,首先安装postcss-loader

yarn add postcss-loader autoprefixer -D

之后在根目录下新建一个postcss.config.js文件,并写入:

module.exports = {
  plugins: [
    require('autoprefixer')
  ]
}

最后需要在webpack.common.js的样式相关插件的 css-loader 之后加上配置,以scss为例

{
    test: /\.scss$/,
    include: path.join(__dirname, '../src'),
    use: [
      'style-loader',
      'css-loader',
      'postcss-loader', // 加了这一行
      {
        loader: 'sass-loader',
        options: {
          includePaths: [path.join(__dirname, '../src/styles')]
        }
      }
    ]
 }

随便写点样式,然后在谷歌控制台可以发现,会自动帮你添加 -webkit- 的前缀。

注意如果使用了 postcss-preset-env 这个的话,它会自动安装 autoprefixer,并且配置了它,就不再需要配置 autoprefixer

const postcssPresetEnv = require('postcss-preset-env');
module.exports = {
  plugins: [
    postcssPresetEnv(/* pluginOptions */)
  ]
}

CSS Modules优化

CSS Modules 是为了加入局部作用域和模块依赖,这里我没加入它,可以代替它的方案也有,比如scoped,以及bem命名方式等,这里我选择了bem命名方法,掘金也有关于它的介绍,可以去看看。

图片字体等资源加载

首先安装处理这类资源的加载器

yarn add url-loader file-loader -D

然后在webpack.common.js中加入相关的规则配置

  {
    test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
    use: [
      {
      loader: 'url-loader',
      options: {
        //1024 == 1kb  
        //小于10kb时打包成base64编码的图片否则单独打包成图片
        limit: 10240,
        name: path.join('img/[name].[hash:7].[ext]')
      }
    }]
  },
  {
    test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
    use: [{
      loader: 'url-loader',
      options: {
        limit: 10240,
        name: path.join('font/[name].[hash:7].[ext]')
      }
    }]
  }

ESlint

关于typescript代码的规范校验,可选的有tslint和eslint,不过最近官方也推荐往eslint上转了,所以我这里使用eslint,安装相关依赖。

yarn add eslint eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/eslint-plugin @typescript-eslint/parser

如果不使用 hook 的话,就不用装 eslint-plugin-react-hooks 这个插件了

在根目录下新建.eslintrc.js,并写入配置:

module.exports = {
  extends: [
    "eslint:recommended",
    "plugin:react/recommended"
  ],
  parserOptions: {
    "ecmaVersion": 2019,
    "sourceType": "module"
  },
  env: {
    node: true,
    browser: true,
    commonjs: true,
    es6: true
  },
  parser: '@typescript-eslint/parser',
  plugins: [
    "@typescript-eslint",
    "react-hooks"
  ],
  globals: {
    // 这里填入你的项目需要的全局变量
    // 这里值为 false 表示这个全局变量不允许被重新赋值,比如:
    // React: false,
    // ReactDOM: false
  },
  settings: {
    react: {
        pragma: "React",
        version: "detect"
    }
  },
  rules: {
    // 这里填入你的项目需要的个性化配置,比如:
    //
    // // @fixable 一个缩进必须用两个空格替代
    semi: ['error', 'never'],
    'no-console': 'off',
    'no-unused-vars': [
      'warn',
      {
        vars: 'all',
        args: 'none',
        caughtErrors: 'none'
      }
    ],
    'max-nested-callbacks': 'off',
    'react/no-children-prop': 'off',
    'typescript/member-ordering': 'off',
    'typescript/member-delimiter-style': 'off',
    'react/jsx-indent-props': 'off',
    'react/no-did-update-set-state': 'off',
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn",
    indent: [
      'off',
      2,
      {
        SwitchCase: 1,
        flatTernaryExpressions: true
      }
    ]
  }
}

用 VS Code 开发时,应该还需要配置settings.json

    "eslint.autoFixOnSave": true,
    "eslint.validate": [
        "javascript",
        "javascriptreact",
        {
            "language": "html",
            "autoFix": true
        },
        {
            "language": "vue",
            "autoFix": true
        },
        {
            "language": "typescript",
            "autoFix": true
        },
        {
            "language": "typescriptreact",
            "autoFix": true
        },
    ]

AntDesign

antd是阿里家的一款UI组件库,官方文档中关于如何引入使用讲的很清楚,我们来配置一下,先安装需要的依赖,babel-plugin-import 用于按需引入:

yarn add antd
yarn add babel-plugin-import less less-loader -D 

在babel.config.js中增加配置

  plugins: [
    ...
    ['import', { 
      libraryName: 'antd',
      libraryDirectory: 'lib',
      style: true 
    }]
  ]

还需要在webpack.common.js中配置less规则

  {
    // for ant design
    test: /\.less$/,
    include: resolve('../node_modules'),
    use: [
      'style-loader',
      'css-loader',
      'postcss-loader',
      {
        loader: 'less-loader',
        options: {
          lessOptions: { // 如果使用less-loader@5,请移除 lessOptions 这一级直接配置选项。
            modifyVars: theme,
            javascriptEnabled: true,
          },
        }
      }
    ]
   }

注意 modifyVars: theme 这个是根据官网来的 自定义主题,需要新建一个 theme.js,这个文件的名字自己定义,这里修改了一下主要颜色,以及 border-radius-base 的值,还有很多配置具体可以查看官网。

module.exports = {
  'primary-color': 'black',
  'border-radius-base': '10px'
}

之后便可以按照官网实例愉快的使用了。不过打包之后存在一个问题,icons.js占了很大的一部分,这里使用github上的大佬给出的解决方案。

在src下新建一个icons.ts,里面只写用到的icon

export { default as DownOutline } from '@ant-design/icons/lib/outline/DownOutline'

然后配置别名,这里就将在ts中使用别名的方式一块介绍了。在 webpack.common.js 中的 resolve 项中增加下面配置,跟icon有关的是第二行。

alias: {
  '@': resolve('../src'),
  "@ant-design/icons/lib/dist$": resolve('../src/icons.ts'),
  '@components': resolve('../src/components'),
  '@img': resolve('../src/assets/img')
}

还需要在tsconfig.json中增加配置:

"paths": {
  "@/*": ["src/*"],
  "@ant-design/icons/lib/dist$": ["src/icons.js"],
  "@components/*": ["src/components/*"],
  "@img/*": ["src/assets/img/*"]
}

更新babel配置

之前加了 typescript 等依赖,现在来更新一下 babel 的配置,来支持我们后面可能会用到的功能,比如装饰器以及路由的动态引入。

yarn add @babel/preset-typescript @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/plugin-syntax-dynamic-import -D

修改 babel.config.js

module.exports = {
  presets: [
    '@babel/preset-env',
    '@babel/preset-typescript',
    '@babel/preset-react'
  ],
  plugins: [
    ['import', { 
      libraryName: 'antd',
      libraryDirectory: 'lib',
      style: true 
    }],
    ['@babel/plugin-proposal-decorators', { legacy: true }],
    ['@babel/plugin-proposal-class-properties', { loose: true }],
    '@babel/plugin-syntax-dynamic-import'
  ]
}

React-router

router使用最新的5版本,然后路由懒加载使用官方例子中的loadable,首先还是安装依赖

yarn add react-router-dom
yarn add @loadable/component @types/loadable__component @types/react-router-dom -D

让我们在 App.tsx 中使用它们

import * as React from 'react'
import { HashRouter as Router, Route, Link } from "react-router-dom"
import loadable from '@loadable/component'

const HomeComponent = loadable(() => import(/* webpackChunkName: "home" */ './views/Home'))
const AboutComponent = loadable(() => import(/* webpackChunkName: "about" */ './views/About'))

class App extends React.Component {
  
  
  render() {

    return (
      <div className="app">
        <Router>
          <ul>
            <li>
              <Link to="/">To Home</Link>
            </li>
            <li>
              <Link to="/about">To About</Link>
            </li>
          </ul>
          <Route exact path='/' component={HomeComponent}></Route>
          <Route path='/about' component={AboutComponent}></Route>
        </Router>
        <p className="aps">hahahaahhahhahahaha</p>
      </div>
    )
  }
}

export default App

更多

到这里基本的功能已经具备了,接下来还需要一些优化以及拆分开发和生产环境的配置,篇幅有点长了,放到下一篇文章里写吧。

最后附上地址 项目地址,如果有不对的地方希望各位指出,感谢。