如何使用 webpack 启动一个 react + typescript 项目

初始化目录

我们要从一个空目录开始,先新建这个目录,做一些必要的初始化工作:

$ mkdir my-react
$ cd my-react

$ git init
$ npm init新建如下目录结构:

react-project

  • config 打包配置
  • public 静态文件夹
  • index.html
  • favicon.ico
  • src 源码目录

规范 git 提交

协作开发时,git 提交的内容如果没有规范,就不好管理项目,我们用 husky + commitlint 来规范 git 提交。我们先在根目录下建立 .gitignore 文件,忽略不需要要的文件。然后安装工具:

$ npm i -D husky
$ npm i -D @commitlint/cli

husky 会为 git 增加钩子,在 commit 时执行一系列操作, commitlint 可以检查 git message 是否符合规则。 在 package.json 中增加配置如下:

"husky": {
  "hooks": {
    "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
  }
},

在根目录新建文件 .commitlintrc.js ,根据具体情况配置:

module.exports = {
  parserPreset: {
    parserOpts: {
      headerPattern: /^(\w*)(?:\((.*)\))?:\s(.*)$/,
      headerCorrespondence: ["type", "scope", "subject"],
    },
  },
  rules: {
    "type-empty": [2, "never"],
    "type-case": [2, "always", "lower-case"],
    "subject-empty": [2, "never"],
    "type-enum": [
      2,
      "always",
      [
        "feat",
        "fix",
        "update",
        "docs",
        "style",
        "refactor",
        "test",
        "chore",
        "release",
        "revert",
      ],
    ],
  },
};

这样即可完成配置,具体的使用方法参考 commitlint 文档

React hello, world

安装 react,写一个 react hello, world

现在让主角 React 登场:

$ npm i react react-dom

新建一个 hello, world 结构,这里直接用 ts 书写:

// src/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import "./style.css";

ReactDOM.render(<App />, document.getElementById("root"));
// src/App.tsx
import React from "react";
import "./app.css";

const App: React.FC = () => {
  return <div>hello, world</div>;
};

export default App;

我们还需要一个 html 模板:

<! DOCTYPE html>

<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<title>react-app</title>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
完整的结构参考 代码示例

团队规范

eslint

我们通常使用 lint 工具来检查代码不规范的地方,以下是将 eslinttypescript webpack 结合使用的例子。

首先安装依赖:

$ npm i -D eslint babel-eslint eslint-loader eslint-plugin-jsx-control-statements

$ npm i -D eslint-plugin-react @typescript-eslint/parser @typescript-eslint/eslint-plugin

然后在根目录新建 eslint 配置文件 .eslintrc.js

module.exports = {
  root: true,
  env: {
    browser: true,
    node: true,
    es6: true,
    // "jquery": true
    jest: true,
    "jsx-control-statements/jsx-control-statements": true, // 能够在jsx中使用if,需要配合另外的babel插件使用
  },
  parser: "@typescript-eslint/parser",
  parserOptions: {
    sourceType: "module",
    ecmaFeatures: {
      jsx: true,
      experimentalObjectRestSpread: true,
    },
  },
  globals: {
    // "wx": "readonly",
  },
  extends: [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:jsx-control-statements/recommended", // 需要另外配合babel插件使用
  ],
  settings: {
    react: {
      version: "detect", // 自动读取已安装的react版本
    },
  },
  plugins: ["@typescript-eslint", "react", "jsx-control-statements"],
  rules: {
    "no-extra-semi": 0, // 禁止不必要的分号
    quotes: ["error", "single"], // 强制使用单引号
    "no-unused-vars": 0, // 不允许未定义的变量
    // ...你自己的配置
  },
};

我们可能希望检查或不检查某些特定的文件,可以在根目录新建.eslintignore,以下配置不检查 src 目录以外的 js 文件:

**/*.js
!src/**/*.js

还需要配置 webpack ,才能在开发时启用 eslint

// webpack.base.js
module: {
  rules: [
    // 把这个配置放在所有loader之前
    {
      enforce: "pre",
      test: /\.tsx?$/,
      exclude: /node_modules/,
      include: [APP_PATH],
      loader: "eslint-loader",
      options: {
        emitWarning: true, // 这个配置需要打开,才能在控制台输出warning信息
        emitError: true, // 这个配置需要打开,才能在控制台输出error信息
        fix: true, // 是否自动修复,如果是,每次保存时会自动修复可以修复的部分
      },
    },
  ];
}

prettier

除了约束开发时的编码规范外,我们一般还希望在提交代码时自动格式化代码,但我们只希望处理当前提交的代码,而不是整个代码库,否则会把提交记录搞得乱七八糟, prettierlint-staged 可以完成这项任务。

先安装工具:

$ npm i -D prettier eslint-plugin-prettier eslint-config-prettier

$ npm i -D lint-staged

在根目录增加 prettier 配置 .prettierrc.js ,同样的也可以增加忽略配置 .prettierignore (建议配置为与 lint 忽略规则一致):

// 这个配置需要与eslint一致,否则在启用 eslint auto fix 的情况下会造成冲突
module.exports = {
    "printWidth": 120, //一行的字符数,如果超过会进行换行,默认为80
    "tabWidth": 2,
    "useTabs": false, // 注意:makefile文件必须使用tab,视具体情况忽略
    "singleQuote": true,
    "semi": true,
    "trailingComma": "none", //是否使用尾逗号,有三个可选值"<none|es5|all>"
    "bracketSpacing": true, //对象大括号直接是否有空格,默认为true,效果:{ foo: bar }
};
修改eslint配置.eslintrc.js:

module.exports = {
    "extends": [
        "eslint:recommended",
        "plugin:react/recommended",
        "plugin:jsx-control-statements/recommended", // 需要另外配合babel插件使用
        "prettier" // 注意顺序
    ],
    "plugins": ["@typescript-eslint", "react", "jsx-control-statements", "prettier"], // 注意顺序
    "rules": {
        "prettier/prettier": 2, // 这样prettier的提示能够以错误的形式在控制台输出
    }
};

然后我们要配置 lint-staged ,在提交代码时自动格式化代码。

修改 package.json

"husky": {
  "hooks": {
    "pre-commit": "lint-staged"
  }
},
"lint-staged": {
  "src/**/*.{jsx,js,tsx,ts}": [
    "prettier --write",
    "eslint --fix",
    "git add"
  ]
}

editorconfig 统一编辑器规范

有些编辑器能够根据配置提示会自动格式化代码,我们可以为各种编辑器提供一个统一的配置。

在根目录新建 .editorconfig 即可,注意不要与已有的 lint 规则冲突:

root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

使用 jest

使用 jest 可以帮助我们测试代码,在项目中使用 jest 的实现方式有很多种,文本不具体展开讨论,只提供一些必备的工具和配置。

必备工具: $ npm i -D jest babel-jest ts-jest @types/jest

参考配置 jest.config.js ,测试文件均放在test目录中:

module.exports = {
  transform: {
    "^.+\\.tsx?$": "ts-jest",
  },
  testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
  moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
};

美化 webpack 输出信息

webpack 在开发时的输出信息有一大堆,可能会干扰我们查看信息,以下提供一个美化、精简输出信息的建议。

精简以下开发服务器输出信息,修改 webpack.dev.js

// ...webpack configs
stats: {
    colors: true,
    children: false,
    chunks: false,
    chunkModules: false,
    modules: false,
    builtAt: false,
    entrypoints: false,
    assets: false,
    version: false
}

美化一下打包输出,安装依赖: $ npm i -D ora chalk

修改 config/build.js

const ora = require("ora");
const chalk = require("chalk"); // 如果要改变输出信息的颜色,使用这个,本例没有用到
const webpack = require("webpack");
const webpackConfig = require("./webpack.prod");

const spinner = ora("webpack编译开始...\n").start();

webpack(webpackConfig, function (err, stats) {
  if (err) {
    spinner.fail("编译失败");
    console.log(err);
    return;
  }
  spinner.succeed("编译结束!\n");

  process.stdout.write(
    stats.toString({
      colors: true,
      modules: false,
      children: false,
      chunks: false,
      chunkModules: false,
    }) + "\n\n"
  );
});

分别运行打包和开发命令,控制台界面是不是清爽多了?

路由的配置

本段提供一个 react-router 的实践。

安装依赖:

$ npm i react-router-dom react-router-config @types/react-router-dom @types/react-router-config

$ npm i @loadable/component

新建 src/router.ts

import loadable from "@loadable/component"; // 按需加载

export const basename = ""; // 如果访问路径有二级目录,则需要配置这个值,如首页地址为'http://tianzhen.tech/blog/home',则这里配置为'/blog'

export const routes = [
  {
    path: "/",
    exact: true,
    component: loadable(() =>
      import("@/pages/demo/HelloWorldDemo/HelloWorldDemoPage")
    ), // 组件需要你自己准备
    name: "home", // 自定义属性
    title: "react-home", // 自定义属性
    // 这里可以扩展一些自定义的属性
  },
  {
    path: "/home",
    exact: true,
    component: loadable(() =>
      import("@/pages/demo/HelloWorldDemo/HelloWorldDemoPage")
    ),
    name: "home",
    title: "HelloWorld",
  },
  // 404 Not Found
  {
    path: "*",
    exact: true,
    component: loadable(() => import("@/pages/demo/404Page/404Page")),
    name: "404",
    title: "404",
  },
];

改造 index.tsc ,启用路由:

import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';
import { routes, basename } from './router';
import '@/App.less';

const App: React.FC = () => {
  return <BrowserRouter basename={basename}>{renderRoutes(routes)}</BrowserRouter>;
};

export default App;

我们还可以利用路由为每个页面设置标题。

先写一个 hook:

import { useEffect } from "react";

export function useDocTitle(title: string) {
  useEffect(() => {
    const originalTitle = document.title;
    document.title = title;
    return () => {
      document.title = originalTitle;
    };
  });
}

hook 应用在需要修改标题的组件中即可:

import React from 'react';
import { useDocTitle } from '@/utils/hooks/useDocTitle';

import Logo from './react-logo.svg';
import './HelloWorldDemoPage.less';

const HelloWorldDemoPage: React.FC<Routes> = (routes) => {
  const { route } = routes; // 获取传入的路由配置
  useDocTitle(route.title); // 修改标题
  return <div className="App">hello, world</div>;
};

export default HelloWorldDemoPage;