Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

我是这样搭建Typescript+React项目环境的!(2.7w字详解) #14

Open
vortesnail opened this issue Aug 11, 2020 · 29 comments
Open
Labels

Comments

@vortesnail
Copy link
Owner

vortesnail commented Aug 11, 2020

前言

现在我们开发一个 React 项目最快的方式便是使用 Facebook 官方开源的脚手架 create-react-app ,但是随着业务场景的复杂度提升,难免会需要我们再去添加或修改一些配置,这个时候如果对 webpack 不够熟练的话,会比较艰难,那种无力的感觉,就好像是女朋友在旁边干扰你打游戏一样,让人焦灼且无可奈何。

这篇文章的主要目的是让大家(新手)对webpack 构建 react + typescript 项目开发环境有一个很感性的认知,以及 会配合使用 rollup 打包组件并发布至 npm 全流程,坦白说,相关的文章真的很多了,但是我仍然想再写一篇属于我自己风格的文章,什么风格呢?

1.从零开始搭建至完整的项目开发环境流程!
2.尽量做到每一步操作、每一行代码都能尽量解释给读者!
3.若完全跟着做下来,一定能实现同样的功能!

你能学到什么?

希望在你阅读本篇文章之后,不会觉得浪费了时间。如果你跟着读下来,你将会学到:

  • 🍋 项目中常用配置文件的作用及配置方式
  • 🍊 eslint、stylelint 及 prettier 的配置
  • 🍉 代码提交规范的第三方工具强制约束方式实现
  • 🍓 webpack 配置 react + typescript 开发与生产环境及优化
  • 🍑 rollup 构建组件打包环境并发布至 npm 的全流程
  • 🍏 利用 react-testing-library 对 react 组件进行测试
  • 🥝 持续集成(CI)、Github Actions

项目初始化及配置

大家对 github 一定很熟悉了,各式各样的开源工具一定也是经常被大家用到,用久了自己也想对开源社区做一些贡献,奈何各种配置太过繁琐,劝退了一大部分热心的开发者,我当初就是有很多想法,但是只会写代码,看别人的开源项目一堆配置文件,看的头皮发麻,再想想自己全都看不懂,想想就算开发出来了,别人也会觉得不专业,就抱着这种心态直接放弃了~

image.png

别慌,看完这篇文章,该会的都会了!
那我们现在就从 github 新建一个开发脚手架项目开始吧~

这一步只需要在 github 主页右上角点击“+”然后 New repository 之后进行项目名字及项目描述的填写,选择一个开源协议即可确定创建完成(比如我新建的一个项目便为 react-ts-quick-starter ,欢迎大家 pr 以及 star🌟。),进入到项目主页之后,点击绿油油的 Code 大按键,复制 SSH 链接,回到我们的桌面,打开终端(控制台),切换到你想要的目录下,执行命令:

# 注意以下的 ssh 连接要是自己项目下复制的
git clone [email protected]:vortesnail/react-ts-quick-starter.git

当 clone 完成之后,使用编辑器打开项目文件夹,我们的 vscode 该上场了!
我个人比较习惯于使用 vscode 自带的终端,打开默认的终端快捷键为 ctrl + 反引号 ,当前目录默认就为项目目录。

1. package.json

每一个项目都需要一个 package.json 文件,它的作用是记录项目的配置信息,比如我们的项目名称、包的入口文件、项目版本等,也会记录所需的各种依赖,还有很重要的 script 字段,它指定了运行脚本命令的 npm 命令行缩写。

通过以下命令就能快速生成该文件:

npm init -y

你也可以使用 yarn 来进行生成,但是我个人还是对 npm 更习惯些,所以我之后都会用 npm 来进行依赖包的安装。

通过修改生成的默认配置,现在的内容如下:

{
  "name": "react-ts-quick-starter",
  "version": "1.0.0",
  "description": "Quickly create react + typescript project development environment and scaffold for developing npm package components",
  "main": "index.js",
  "scripts": {},
  "repository": {
    "type": "git",
    "url": "git+https://github.com/vortesnail/react-ts-quick-starter.git"
  },
  "keywords": ["react-project", "typescript-project", "react-typescript", "react-ts-quick-starter"],
  "author": {
    "name": "vortesnail",
    "url": "https://github.com/vortesnail",
    "email": "[email protected]"
  },
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/vortesnail/react-ts-quick-starter/issues"
  },
  "homepage": "https://github.com/vortesnail/react-ts-quick-starter#readme"
}

暂时修改了以下配置:

  • description :增加了对该项目的描述,github 进行 repo 搜索时,关键字匹配会使你的项目更容易被搜索到。
  • scripts :把默认生成的删了,没啥用。
  • keywords :增加了项目关键字,其他开发者在 npm 上搜索的时候,适合的关键字能你的包更容易被搜索到。
  • author :添加了更具体的作者信息。
  • license :修改为MIT协议。

2. LICENSE

我们在建仓库的时候会有选项让我们选择开源协议,我当时就选了MIT协议,如果没选的也不要紧,去网站 choosealicense 选择合适的 license(一般会选宽松的 MIT 协议),复制到项目根目录下的 LICENSE 文件内即可,然后修改作者名和年份,如下:

MIT License

Copyright (c) 2020 chen xin

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights...

3. .gitignore

该文件决定了项目进行 git 提交时所需要忽略掉的文件或文件夹,编辑器如 vscode 也会监听 .gitignore 之外的所有文件,如果没有进行忽略的文件有所变动时,在进行 git 提交时就会被识别为需要提交的文件。

node_modules 是我们安装第三方依赖的文件夹,这个肯定要添加至 .gitignore 中,且不说这个文件夹里面成千上万的文件会给编辑器带来性能压力,也会给提交至远端的服务器造成不小损失,另外就是这个文件夹中的东西,完全可以通过简单的 npm install 就能得到~

所以不需要上传至 git 仓库的都要添加进来,比如我们常见的 build 、 dist 等,还有操作系统默认生成的,比如 MacOs 会生成存储项目文件夹显示属性的 DS_Store 文件。

image.png

那么这些系统或编辑器自动生成的文件,但是又不被我们很容易查知的该怎么办呢?使用 vscode 的 gitignore 插件,下载安装该插件之后, ctrl+shift+p 召唤命令面板,输入 Add gitignore 命令,即可在输入框输入系统或编辑器名字,来自动添加需要忽略的文件或文件夹至 .gitignore 中。

123.gif

我添加了以下: Node 、 Windows 、 MacOS 、 SublimeText 、 Vim 、 Vscode ,大家酌情添加吧。如果默认中没有的,可自行手动输入至 .gitignore 中,比如我自己加了 dist/ 和 build/ ,用于忽略之后webpack 打包生成的文件。

4. .npmrc

大家一开始使用 npm 安装依赖包时,肯定感受过那挤牙膏般的下载速度,上网一查只需要将 npm 源设置为淘宝镜像源就行,在控制台执行一下以下命令:

npm config set registry https://registry.npm.taobao.org

从此过上了速度七十迈,心情是自由自在的生活。

但是大家想想,万一某个同学克隆了你的项目之后,准备在他本地开发的时候,并没有设置淘宝镜像源,又要人家去手动设置一遍,我们作为项目的发起者,就先给别人省下这份时间吧,只需要在根目录添加一个 .npmrc 并做简单的配置即可:

# 创建 .npmrc 文件
touch .npmrc
# 在该文件内输入配置
registry=https://registry.npm.taobao.org/

5. README.md

你只要上 github 找任何一个项目,点进去之后往下拉一点,看到的对项目的直接说明就是 README.md 所呈现的,这个文件无比重要,一个好的开源项目必须!必须!必须!有一个简明且美观的 README.md ,不过文章写到现在为止,我们的这个脚手架并没有任何实质性的内容,之后完全配置完之后,会再好好书写一下。

后续我还会再对这部分内容做补充,现在大家先 touch README.md 创建文件,然后随意写点东西先看着~

规范代码与提交

多人共同开发一个项目的很大问题就是每个开发者代码风格都有所差异,随着版本不断迭代,维护人员不断更换,这个项目将会变得越来越难维护,因为后人基本不可能再看懂了。比如小迈、小克、小尔三个开发者的风格如下:

// 小迈 紧凑型
const add=(a,b)=>{
  return a+b;
}

// 小克 规范型
const add = (a, b) => {
    return a + b
}

// 小尔 松紧皆可型
var add = (a,b) => {
  return a+b
}

请问如果你刚加入一个团队,所要参与的项目中有这几种代码风格,你会不会觉得“人间不值得”

image.png

如果我们一开始就有手段能够约束大家的代码风格,使其趋于统一,将会极大地增强代码的可维护性,很重要的一点是能提高我们开发的幸福度。

当然了,作为开源项目,代码的提交规范也是很有必要遵守的,这个我们也可以通过第三方工具来强制约束,不要太美滋滋啊,既能使项目的提交更加规范,还能不断地锻炼自己的**规范性思维,**这对于无论是开源项目还是团队项目,都是大有裨益的。

1. EditorConfig

.editorconfig 是跨编辑器维护一致编码风格的配置文件,有的编辑器会默认集成读取该配置文件的功能,但是 vscode 需要安装相应的扩展 EditorConfig For vs Code 。

image.png

安装完此扩展后,在 vscode 中使用快捷键 ctrl+shift+p 打开命令台,输入 Generate .editorcofig 即可快速生成 .editorconfig 文件,当然,有时候 vscode 抽风找不到命令也是可能的,比如我就经常遇到输入该命令没用,需要重启才会重新出现,那么就手动创建该文件也是没问题的。

该文件的配置特别简单,就少许的几个配置,比如我的配置如下:

root = true

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

[*.md]
trim_trailing_whitespace = false

扩展装完,配置配完,编辑器就会去首先读取这个配置文件,对缩进风格、缩进大小在换行时直接按照配置的来,在你 ctrl+s 保存时,就会按照里面的规则进行代码格式化。以下是上述配置的简单介绍:

  • indent_style :缩进风格,可选配置有 tab 和 space 。
  • indent_size :缩进大小,可设定为 1-8 的数字,比如设定为 2 ,那就是缩进 2 个空格。
  • charset :编码格式,通常都是选 utf-8 。
  • trim_trailing_whitespace :去除多余的空格,比如你不小心在尾巴多打了个空格,它会给你自动去掉。
  • insert_final_newline :在尾部插入一行,个人很喜欢这个风格,当最后一行代码很长的时候,你又想对该行代码比较靠后的位置编辑时,不要太好用哦,建议大家也开上。
  • end_of_line :换行符,可选配置有 lf ,cr ,crlf ,会有三种的原因是因为各个操作系统之间的换行符不一致,这里有历史原因,有兴趣的同学自行了解吧,许多有名的开源库都是使用 lf ,我们姑且也跟跟风吧。

因为 markdown 语法中,我想要换行需要在上一行多打 2 个以上的空格,为了不影响该语法,故 .md 文件中把去除多余空格关掉了。

2. Prettier

如果说 EditorConfig 帮你统一编辑器风格,那 Prettier 就是帮你统一项目风格的。 Prettier 拥有更多配置项(实际上也不多,数了下二十个),且能在发布流程中执行命令自动格式化,能够有效的使项目代码风格趋于统一。

在我们的项目中执行以下命令安装我们的第一个依赖包:

npm install prettier -D

安装成功之后在根目录新建文件 .prettierrc ,输入以下配置:

{
  "trailingComma": "all",
  "tabWidth": 2,
  "semi": false,
  "singleQuote": true,
  "endOfLine": "lf",
  "printWidth": 120,
  "bracketSpacing": true,
  "arrowParens": "always"
}

其实 Prettier 的配置项很少,大家可以去 Prettier Playground 大概把玩一会儿,下面我简单介绍下上述的配置:

  • trailingComma :对象的最后一个属性末尾也会添加 , ,比如 { a: 1, b: 2 } 会格式为 { a: 1, b: 2, } 。
  • tabWidth :缩进大小。
  • semi :分号是否添加,我以前从C++转前端的,有一段时间非常不能忍受不加分号的行为,现在香的一匹。
  • singleQuote :是否单引号,绝壁选择单引号啊,不会真有人还用双引号吧?不会吧!😏
  • jsxSingleQuote :jsx 语法下是否单引号,同上。
  • endOfLine :与 .editorconfig 保持一致设置。
  • printWidth :单行代码最长字符长度,超过之后会自动格式化换行。
  • bracketSpacing :在对象中的括号之间打印空格, {a: 5} 格式化为 { a: 5 } 。
  • arrowParens :箭头函数的参数无论有几个,都要括号包裹。比如 (a) => {} ,如果设为 avoid ,会自动格式化为 a => {} 。

那我们现在也配置好了,但是咋用的呢?

  • 一个是我们可以通过命令的形式去格式化某个文件下的代码,但是我们基本不会去使用,最终都是通过 ESlint 去检测代码是否符合规范。
  • 二是当我们编辑完代码之后,按下 ctrl+s 保存就给我们自动把当前文件代码格式化了,既能实时查看格式化后的代码风格,又省去了命令执行代码格式化的多余工作。

你所需要做的是先安装扩展 Prettier - Code formatter

image.png

当安装结束后, 在项目根目录新建一个文件夹 .vscode ,在此文件下再建一个 settings.json 文件:

image.png

该文件的配置优先于 vscode 全局的 settings.json ,这样别人下载了你的项目进行开发,也不会因为全局 setting.json 的配置不同而导致 Prettier 或之后会说到的 ESLint 、 StyleLint 失效,接下来在该文件内输入以下代码:

{ 
  // 指定哪些文件不参与搜索
  "search.exclude": {
    "**/node_modules": true,
    "dist": true,
    "yarn.lock": true
  },
  "editor.formatOnSave": true,
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[javascriptreact]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[json]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[html]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[markdown]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[css]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[less]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[scss]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  }
}

"editor.formatOnSave" 的作用是在我们保存时,会自动执行一次代码格式化,而我们该使用什么格式化器?接下来的代码便是设置默认的格式化器,看名字大家也能看得出来了吧!

在遇到 .js 、 .jsx 、.ts 、.tsx 、.json 、.html 、.md 、 .css 、 .less 、 .scss 为后缀的文件时,都会去使用 Prettier 去格式化代码,而格式化的规则就是我们配置的 .prettierrc 决定的!

12.gif

.editorconfig 配置文件中某些配置项是会和 Prettier 重合的,例如 指定缩进大小 两者都可以配置。

那么两者有什么区别呢?

我们可以看到 EditorConfig 的配置项都是一些不涉及具体语法的,比如 缩进大小、文移除多余空格等。

Prettier 是一个格式化工具,要根据具体语法格式化,对于不同的语法用单引号还是双引号,加不加分号,哪里换行等,当然,肯定也有缩进大小。

即使缩进大小这些共同都有的设置,两者也是不冲突的,设置 EditorConfig 的 indent_size  为 4 , Prettier 的 tabWidth 为 2 。

12.gif

可以看到,在我们新起一行时,根据 .editorconfig 中的配置,缩进大小为 4 ,所以光标直接跳到了此处,但是保存时,因为我们默认的格式化工具已经在 .vscode/settings.json 中设置为了 Prettier ,所以这时候读取缩进大小为 2 的配置,并正确格式化了代码。

当然,我还是建议大家两个都配置文件重合的地方都保持一致比较好~

3. ESLint

在上面我们配置了 EditorConfig 和 Prettier 都是为了解决代码风格问题,而 ESLint 是主要为了解决代码质量问题,它能在我们编写代码时就检测出程序可能出现的隐性BUG,通过 eslint --fix 还能自动修复一些代码写法问题,比如你定义了 var a = 3 ,自动修复后为 const a = 3 。还有许多类似的强制扭转代码最佳写法的规则,在无法自动修复时,会给出红线提示,强迫开发人员为其寻求更好的解决方案。

prettier 代码风格统一支持的语言更多,而且差异化小,eslint 一大堆的配置能弄出一堆风格,prettier 能对 ts js html css json md做风格统一,这方面 eslint 比不过。 --来自“三元小迷妹”

我们先把它用起来,直观感受一下其带来的好处!

首先在项目中安装 eslint :

 npm install eslint -D

安装成功后,执行以下命令:

npx eslint --init

上述命令的功能为初始化 ESLint 的配置文件,采取的是问答的形式,特别人性化。不过在我们介绍各个问答之前先来看看这句命令中 npx 是什么。

实际上,要达到以上命令的效果还有两种方式。

一是直接找到我们项目中安装的 eslint 的可执行文件,如下图:

image.png

然后根据该路径来执行命令:

./node_modules/.bin/eslint --init

二是先全局安装 eslint ,直接执行以下命令即可:

# 全局安装 eslint
npm install eslint -g

# eslint 配置文件初始化
eslint --init

现在让我们来说下这两种方式的缺点:

  • 针对第一种,其实本质上来说和我们所推荐的 npx 形式没有区别,缺点是该命令太过于繁琐。
  • 针对第二种,我们需要先全局进行 eslint 的安装,这会占据我们电脑的硬盘空间,且会将安装文件放到挺隐蔽的地方,个人有心里洁癖,非常接受不了这种全局安装的方式,特别是越来越多全局包的时候。再有一个比较大的问题是,因为我们执行 eslint --init 是使用全局安装的版本去初始化的,这有可能会和你现在项目中的 eslint 版本不一致。这个问题我就出现了,记得很久以前装的全局 eslint ,版本好低。

那么 npx 的作用就是抹掉了上述两个缺点,其是 npm v5.2.0 引入的一条命令,它在上述命令执行时:

  • 会先去本地 node_modules 中找 eslint 的执行文件,如果找到了,就直接执行,相当于上面所说的第一种方式;
  • 如果没有找到,就去全局找,找到了,就相当于上述第二种方式;
  • 如果都没有找到,就下载一个临时的 eslint ,用完之后就删除这个临时的包,对本机完全无污染。

image.png

已经执行 npx eslint --init 的小伙伴现在会依次遇到下面问题,请跟我慢慢看来:

  • How would you like to use ESLint?

    果断选择第三条 To check syntax, find problems, and enforce code style ,检查语法、检测问题并强制代码风格。

  • What type of modules does your project use?

    项目非配置代码都是采用的 ES6 模块系统导入导出,选择 JavaScript modules (import/export) 。

  • Which framework does your project use?

    显而易见,选择 React 。

  • Does your project use TypeScript?

    果断用上 Typescript 啊,还记得我们文章的标题吗?选择 Yes 后生成的 eslint 配置文件会给我们默认配上支持 Typescript 的 parse 以及插件 plugins 等。

  • Where does your code run?

Browser 和 Node 环境都选上,之后可能会编写一些 node 代码。

  • How would you like to define a style for your project?

    选择 Use a popular style guide ,即使用社区已经制定好的代码风格,我们去遵守就行。

  • Which style guide do you want to follow?

    选择 Airbnb 风格,都是社区总结出来的最佳实践。

  • What format do you want your config file to be in?

    选择 JavaScript ,即生成的配置文件是 js 文件,配置更加灵活。

  • Would you like to install them now with npm?

    当然 Yes 了~

在漫长的安装结束后,项目根目录下多出了新的文件 .eslintrc.js ,这便是我们的 eslint 配置文件了。其默认内容如下:

module.exports = {
  env: {
    browser: true,
    es2020: true,
    node: true,
  },
  extends: ['plugin:react/recommended', 'airbnb'],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 11,
    sourceType: 'module',
  },
  plugins: ['react', '@typescript-eslint'],
  rules: {},
}

各个属性字段的作用可在 Configuring ESLint 仔细了解,可能会比较迷惑的地方是 extends 和 plugins 的关系,其实 plugins 就是插件的意思,都是需要 npm 包的安装才可以使用,只不过默认支持简写,官网都有说;至于 extneds 其实就是使用我们已经下载的插件的某些预设规则。

现在我们对该配置文件作以下修改:

  • 根据 eslint-config-airbnb 官方说明,如果要开启 React Hooks 的检查,需要在 extends 中添加一项 'airbnb/hooks' 。
  • 根据 @typescript-eslint/eslint-plugin 官方说明,在 extends 中添加 'plugin:@typescript-eslint/recommended' 可开启针对 ts 语法推荐的规则定义。
  • 需要添加一条很重要的 rule ,不然在 .ts 和 .tsx 文件中引入另一个文件模块会报错,比如:

image.png

添加以下规则到 rules 即可:

rules: {
  'import/extensions': [
    ERROR,
    'ignorePackages',
    {
      ts: 'never',
      tsx: 'never',
      json: 'never',
      js: 'never',
    },
  ],
}

在之后我们安装 typescript 之后,会出现以下的怪异错误:

image.png

大家先添加以下配置,毕竟之后一定要安装 typscript 的,把最常用的扩展名排在最前面,这里寻找文件时最快能找到:

  settings: {
    'import/resolver': {
      node: {
        extensions: ['.tsx', '.ts', '.js', '.json'],
      },
    },
  },

接下来安装 2 个社区中比较火的 eslint 插件:

  • eslint-plugin-promise :让你把 Promise 语法写成最佳实践。
  • eslint-plugin-unicorn :提供了更多有用的配置项,比如我会用来规范关于文件命名的方式。

执行以下命令进行安装:

npm install eslint-plugin-promise eslint-plugin-unicorn -D

在添加了部分规则 rules 后,我的配置文件修改之后如下:

const OFF = 0
const WARN = 1
const ERROR = 2

module.exports = {
  env: {
    browser: true,
    es2020: true,
    node: true,
  },
  extends: [
    'airbnb',
    'airbnb/hooks',
    'plugin:react/recommended',
    'plugin:unicorn/recommended',
    'plugin:promise/recommended',
    'plugin:@typescript-eslint/recommended',
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 11,
    sourceType: 'module',
  },
  settings: {
    'import/resolver': {
      node: {
        extensions: ['.tsx', '.ts', '.js', '.json'],
      },
    },
  },
  plugins: ['react', 'unicorn', 'promise', '@typescript-eslint'],
  rules: {
    // 具体添加的其他规则大家可查看我的 github 查看
    // https://github.com/vortesnail/react-ts-quick-starter/blob/master/.eslintrc.js
  },
}

在之后的配置过程中,我们可能还会需要对该文件进行更改😛,比如添加解决 eslint 和 prettier 的规则冲突处理插件,请大家期待一下下。

大家新建一个 hello.ts 文件,在里面打上以下代码:

var add = (a, b) => {
  console.log(a + b)
  return a + b
}

export default add

你会发现没有任何的错误提示,很明显上面的代码违反了不能使用 var 定义变量的规则,理论上来说一定会报一堆红线的~

这时候按下图看我们的 ESLint 输出:

image.png

原来是 @typescript-eslint/eslint-plugin 这个插件需要安装 typescript ,虽然我们这部分内容应该在之后再讲的,但是现在为了让大家写点代码测试看下 eslint 是否好用,我们就先安装一下吧:

npm install typescript -D

安装完之后,你再回头看看刚才那个 hello.ts 文件内的代码,是不是一堆爆红了!

image.png

我们知道 eslint 由编辑器支持是有自动修复功能的,首先我们需要安装扩展:

image.png

再到之前创建的 .vscode/settings.json 中添加以下代码:

{
  "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
  "typescript.tsdk": "./node_modules/typescript/lib", // 代替 vscode 的 ts 语法智能提示
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true,
  },
}

这时候我们保存时,就会开启 eslint 的自动修复,完美!

12.gif

不过有时候我们并不希望 ESLint 或 Prettier 去对某些文件做任何修改,比如某个特定的情况下我想去看看打包之后的文件内容,里面的内容一定是非常不符合各种 lint 规则的,但我不希望按保存时对其进行格式化,此时就需要我们添加 .eslintignore 和 .prettierignore ,我一般会使这两个文件的内容都保持一致:

/node_modules
/build
/dist

先添加以上三个需要忽略的文件目录好了,之后大家视情况而添加就行。

4. StyleLint

经过上面的一顿操作,我们的 js 或 ts 代码已经能有良好的代码风格了,但可别忘了还有样式代码的风格也需要统一啊!这个真的很有必要啊,有时候去调试其他人的样式代码,这里一坨那里一坨,看着属实难受。

image.png

根据 stylelint 官网介绍,我们先安装两个基本的包:

npm install stylelint stylelint-config-standard -D

然后在项目根目录新建 .stylelintrc.js 文件,输入以下内容:

module.exports = {
  extends: ['stylelint-config-standard'],
  rules: {
    'comment-empty-line-before': null,
    'declaration-empty-line-before': null,
    'function-name-case': 'lower',
    'no-descending-specificity': null,
    'no-invalid-double-slash-comments': null,
    'rule-empty-line-before': 'always',
  },
  ignoreFiles: ['node_modules/**/*', 'build/**/*'],
}

同样,简单介绍下配置上的三个属性:

  • extends :其实和 eslint 的类似,都是扩展,使用 stylelint 已经预设好的一些规则。
  • rules :就是具体的规则,如果默认的你不满意,可以自己决定某个规则的具体形式。
  • ignoreFiles :不像 eslint 需要新建 ignore 文件, stylelint 配置就支持忽略配置字段,我们先添加 node_modules 和 build ,之后有需要大家可自行添加。

其中关于 xxx/**/* 这种写法的意思有不理解的,大家可在 google (或百度)glob模式

eslint 一样,想要在编辑代码时有错误提示以及自动修复功能,我们需要 vscode 安装一个扩展:

image.png

并且在 .vscode/settings.json 中增加以下代码:

{
	// 使用 stylelint 自身的校验即可
  "css.validate": false,
  "less.validate": false,
  "scss.validate": false,
  
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true,
    "source.fixAll.stylelint": true
  },
}

这时候随便建一个 .less 文件测试下,已经有错误提示和保存时自动修复功能了。

123.gif

我们可以在社区下载一些优秀的 stylelint extends 和 stylelint plugins :

1.Positioning   2.Box Model    3.Typography    4.Visual    5.Animation    6.Misc
{ display: inline; width: 100px; }

我们来一波安装:

npm install stylelint-order stylelint-config-rational-order stylelint-declaration-block-no-ignored-properties -D

现在更改以下我们的配置文件:

module.exports = {
  extends: ['stylelint-config-standard', 'stylelint-config-rational-order'],
  plugins: ['stylelint-order', 'stylelint-declaration-block-no-ignored-properties'],
  rules: {
    'plugin/declaration-block-no-ignored-properties': true,
    'comment-empty-line-before': null,
    'declaration-empty-line-before': null,
    'function-name-case': 'lower',
    'no-descending-specificity': null,
    'no-invalid-double-slash-comments': null,
    'rule-empty-line-before': 'always',
  },
  ignoreFiles: ['node_modules/**/*', 'build/**/*'],
}

至此, stylelint 就配置完成了,具体的规则以及插件大家都可以在其官网进行浏览或查找,然后添加一些自己希望的规则定义。

5. lint命令

我们在 package.json 的 scripts 中增加以下三个配置:

{
	scripts: {
  	"lint": "npm run lint-eslint && npm run lint-stylelint",
    "lint-eslint": "eslint -c .eslintrc.js --ext .ts,.tsx,.js src",
    "lint-stylelint": "stylelint --config .stylelintrc.js src/**/*.{less,css,scss}"
  }
}

在控制台执行 npm run lint-eslint 时,会去对 src/ 下的指定后缀文件进行 eslint 规则检测, lint-stylelint 也是同理, npm run lint 会两者都按顺序执行。

其实我个人来说,这几个命令我是都不想写进 scripts 中的,因为我们写代码的时候,不规范的地方就已经自动修复了,只要保持良好的习惯,看到有爆红线的时候想办法去解决它,而不是视而不见,那么根本不需要对所有包含的文件再进行一次命令式的规则校验。

但是对于新提交缓存区的代码还是有必要执行一次校验的,这个后面会说到。

6. ESLint、Stylelint 和 Prettier 的冲突

有时候 eslint 和 stylelint 的自定义规则和 prettier 定义的规则冲突了,比如在 .eslintrc.js 中某个 extends 的配置设置了缩进大小为 4 ,但是我 .prettierrc 中我设置的缩进为 2 ,那就会出现我们保存时,先是 eslint 的自动修复缩进大小为 4 ,这个时候 prettier 不开心了,又强制把缩进改为了 2 ,好了, eslint 不开心,代码直接爆红!
12.gif
那么我们如何解决这部分冲突呢?

其实官方提供了很好的解决方案,查阅 Integrating with Linters 可知,针对 eslint 和 stylelint 都有很好的插件支持,其原理都是禁用与 prettier 发生冲突的规则。

安装插件 eslint-config-prettier ,这个插件会禁用所有和 prettier 起冲突的规则:

npm install eslint-config-prettier -D

添加以下配置到 .eslintrc.js 的 extends 中:

{
  extends: [
    // other configs ...
   	'prettier',
    'prettier/@typescript-eslint',
    'prettier/react',
    'prettier/unicorn',
  ]
}

这里需要注意, 'prettier' 及之后的配置要放到原来添加的配置的后面,这样才能让 prettier 禁用之后与其冲突的规则。

stylelint 的冲突解决也是一样的,先安装插件 stylelint-config-prettier

npm install stylelint-config-prettier -D

添加以下配置到 .stylelintrc.js 的 extends 中:

{  
	extends: [
  	// other configs ...
    'stylelint-config-prettier'
  ]
}

7. lint-staged

在项目开发过程中,每次提交前我们都要对代码进行格式化以及 eslint 和 stylelint 的规则校验,以此来强制规范我们的代码风格,以及防止隐性 BUG 的产生。

那么有什么办法只对我们 git 缓存区最新改动过的文件进行以上的格式化和 lint 规则校验呢?答案就是 lint-staged

我们还需要另一个工具 husky ,它会提供一些钩子,比如执行 git commit 之前的钩子 pre-commit ,借助这个钩子我们就能执行 lint-staged 所提供的代码文件格式化及 lint 规则校验!

图片名称

赶紧安装一下这两个插件吧:

npm install husky lint-staged -D

随后在 package.json 中添加以下代码(位置随意,我比较习惯放在 repository 上面):

{
	"husky": {
    "hooks": {
      "pre-commit": "lint-staged",
    }
  },
  "lint-staged": {
    "*.{ts,tsx,js}": [
      "eslint --config .eslintrc.js"
    ],
    "*.{css,less,scss}": [
      "stylelint --config .stylelintrc.js"
    ],
    "*.{ts,tsx,js,json,html,yml,css,less,scss,md}": [
      "prettier --write"
    ]
  },
}

首先,我们会对暂存区后缀为 .ts .tsx .js 的文件进行 eslint 校验, --config 的作用是指定配置文件。之后同理对暂存区后缀为 .css .less .scss 的文件进行 stylelint 校验,注意⚠️,我们没有添加 --fix 来自动修复不符合规则的代码,因为自动修复的内容对我们不透明,你不知道哪些代码被更改,这对我来说是无法接受的。

但是在使用 prettier 进行代码格式化时,完全可以添加 --write 来使我们的代码自动格式化,它不会更改语法层面上的东西,所以无需担心。

可能大家搜索一些文章的时候,会发现在 lint-staged 中还配置了一个 git add ,实际上在 v10 版本之后任何被修改了的原 staged 区的文件都会被自动 git add,所以无需再添加。

8. commitlint + changelog

在多人协作的项目中,如果 git 的提交说明精准,在后期协作以及 bug 处理时会变得有据可查,项目的开发可以根据规范的提交说明快速生成开发日志,从而方便开发者或用户追踪项目的开发信息和功能特性。

建议阅读 Commit message 和 Change log 编写指南(阮一峰)

commitlint 可以帮助我们进行 git commit 时的 message 格式是否符合规范,conventional-changelog 可以帮助我们快速生成 changelog ,至于在命令行中进行可视化的 git commit 插件 commitizen 我们就不配了,有兴趣的同学可以自行了解~

首先安装 commitlint 相关依赖:

npm install @commitlint/cli @commitlint/config-conventional -D

@commitlint/config-conventional 类似 eslint 配置文件中的 extends ,它是官方推荐的 angular 风格的 commitlint 配置,提供了少量的 lint 规则,默认包括了以下除了我自己新增的 type 。

随后在根目录新建文件 .commitlintrc.js ,这就是我们的 commitlint 配置文件,写入以下代码:

module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [
      2,
      'always',
      ['build', 'ci', 'chore', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test', 'anno'],
    ],
  },
}

我自己增加了一种 anno ,目的是表示一些注释的增删改的提交。

/**
 * build : 改变了build工具 如 webpack
 * ci : 持续集成新增
 * chore : 构建过程或辅助工具的变动
 * feat : 新功能
 * docs : 文档改变
 * fix : 修复bug
 * perf : 性能优化
 * refactor : 某个已有功能重构
 * revert : 撤销上一次的 commit
 * style : 代码格式改变
 * test : 增加测试
 * anno: 增加注释
 */

随后回到 package.json 的 husky 配置,增加一个钩子:

{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
      "commit-msg": "commitlint --config .commitlintrc.js -E HUSKY_GIT_PARAMS"
    }
  },
}

-E HUSKY_GIT_PARAMS 简单理解就是会拿到我们的 message ,然后 commitlint 再去进行 lint 校验。

接着配置生成我们的 changelog ,首先安装依赖:

npm install conventional-changelog-cli -D

package.json 的 scripts 下增加一个命令:

{
  "scripts": {
    "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
  },
}

之后就可以通过 npm run changelog 生成 angular 风格的 changelog ,需要注意的是,上面这条命令产生的 changelog 是基于上次 tag 版本之后的变更(feat、fix 等等)所产生的。

现在就来测试一下我们上面的工作有没有正常运行吧!执行以下提交信息不规范(chore 写成 chora)的命令:

# 提交所有变化到缓存区
git add -A
# 把暂存区的所有修改提交到分支 
git commit -m "chora: add commitlint to force commit style"

像预期中的一致,出现了以下报错:

image.png

那我们现在进行我们的提交,把故意写错的改回来:

git commit -m "chore: add commitlint to force commit style"

这时候我们成功 commit ,再执行以下命令提交到远端:

git push origin master

经历了漫长的配置,我们“初步”形成了一个完善的项目开发环境,接下来就开始进入 Webpack 的世界吧!

image.png

Webpack 基本配置

我们最终的配置要支持 React 和 Typescript 的开发与生产,现在的我们的思路是将对这两个部分的支持放到最后去配置,一开始先把必要的都配好,这样大家能有一个很直观的印象,什么时候该做什么?怎么做?

对于 Webpack 的配置,我会尽量地去解释清楚每一个新增的配置都有什么用,希望大家耐心阅读~

⚠️ 目前讲解的 webpack 版本为 4+

1. 开始

想要使用 webpack,这两个包你不得不装:

npm install webpack webpack-cli -D
  • webpack :这不必多说,其用于编译 JavaScript 模块。
  • webpack-cli :此工具用于在命令行中运行 webpack。

紧接着我们在根目录下新建文件夹 scripts ,在之下再建一个文件夹 config ,在 config 中再建一个 .js 文件 webpack.common.js ,此结构如下:

scripts/
    config/
    webpack.common.js

为什么会是这样的目录结构,主要考虑到之后讲了 webpack-merge 之后,会把 webpack 的核心配置文件放到 config 下,其余的例如导出文件路径的文件模块放到 config 同级。总之大家先这样搞着,之后咱慢慢解释。

2. input、output

**入口(input)出口(output)**是 webpack 的核心概念之二,从名字就能大概感知他们是干什么的:指定一个(或多个)入口文件,经过一系列的操作之后转换成另一个(或多个)文件

接下来在 webpack.common.js 中输入以下代码:

const path = require('path')

module.exports = {
  entry: {
    app: path.resolve(__dirname, '../../src/app.js'),
  },
  output: {
    filename: 'js/[name].[hash:8].js',
    path: path.resolve(__dirname, '../../dist'),
  },
}

webpack 配置是标准的 Node.js 的 CommonJS 模块,它通过 require 来引入其他模块,通过 module.exports 导出模块,由 webpack 根据对象定义的属性进行解析。

  • entry :定义了入口文件路径,其属性名 app 表示引入文件的名字。
  • output :定义了编译打包之后的文件名以及所在路径。

这段代码的意思就是告诉 webpack,入口文件是根目录下的 src 下的 app.js 文件,打包输出的文件位置为根目录下的 dist 中,注意到 filename 为 js/[name].[hash:8].js ,那么就会在 dist 目录下再建一个 js 文件夹,其中放了命名与入口文件命名一致,并带有 hash 值的打包之后的 js 文件。

接下来在根目录创建 src 文件夹,新建 app.js 文件,输入以下代码:

const root = document.querySelector('#root')
root.innerHTML = 'hello, webpack!'

现在我们尝试使用刚才的 webpack 配置对其进行打包,那如何操作呢?
打开 package.json ,为其添加一条 npm 命令:

{
  "scripts": {
+   "build": "webpack --config ./scripts/config/webpack.common.js",
    "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
    "lint": "npm run lint-eslint && npm run lint-stylelint",
    "lint-eslint": "eslint -c .eslintrc.js --ext .ts,.tsx,.js src",
    "lint-stylelint": "stylelint --config .stylelintrc.js src/**/*.{less,css,scss}"
  },
}

--config 选项来指定配置文件

然后在控制台输入:

npm run build

等待一两秒后,你会发现根目录下真的多出了一个 dist 文件夹,里面的内容和我们 webpack 配置所想要达到的效果是一样的:一个 js 文件夹以及下面的(比如) app.e406fb9b.js 的文件。

至此,我们已经初步使用 webpack 打了一个包,接下来我们逐步开始扩展其他的配置以及相应优化吧!~

3. 公用变量文件

在上面简单的 webpack 配置中,我们发现有两个表示路径的语句:

path.resolve(__dirname, '../../src/app.js')
path.resolve(__dirname, '../../dist')
  • path.resolve :node 的官方 api,可以将路径或者路径片段解析成绝对路径。
  • __dirname :其总是指向被执行 js 文件的绝对路径,比如在我们 webpack 文件中访问了 __dirname ,那么它的值就是在电脑系统上的绝对路径,比如在我电脑上就是:
/Users/RMBP/Desktop/react-ts-quick-starter/scripts/config

所以我们上面的写法,大家可以简单理解为, path.resolve 把根据当前文件的执行路径下而找到的想要访问到的文件相对路径转换成了:该文件在系统中的绝对路径!

比如我的就是:

/Users/RMBP/Desktop/react-ts-quick-starter/src/app.js

但是大家也看出来了,这种写法需要不断的 ../../ ,这个在文件层级较深时,很容易出错且很不优雅。那我们就换个思路,都从根目录开始找所需的文件路径不久很简单了吗,相当于省略了 ../../ 这一步。

scripts 下新建一个 constant.js 文件,专门用于存放我们的公用变量(之后还会有其他的):

scripts/
	config/
  	webpack.common.js
+ constant.js

在里面定义我们的变量:

const path = require('path')

const PROJECT_PATH = path.resolve(__dirname, '../')
const PROJECT_NAME = path.parse(PROJECT_PATH).name

module.exports = { 
  PROJECT_PATH,
  PROJECT_NAME
}
  • PROJECT_PATH :表示项目的根目录。
  • PROJECT_NAME :表示项目名,目前不用,但之后的配置会用到,我们就先定义好吧~

上面两个简单的 node api 大家可以自己简单了解一下,不想了解也可以,只要明白其有啥作用就行。

然后在 webpack.common.js 中引入,修改代码:

const { resolve } = require('path')
const { PROJECT_PATH } = require('../constants')

module.exports = {
  entry: {
    app: resolve(PROJECT_PATH, './src/app.js'),
  },
  output: {
    filename: 'js/[name].[hash:8].js',
    path: resolve(PROJECT_PATH, './dist'),
  },
}

好了,现在是不是看起来清爽多了,大家可以 npm run build 验证下自己代码是不是有写错或遗漏啥的~🐶

4. 区分开发/生产环境

在 webpack 中针对开发环境与生产环境我们要分别配置,以适应不同的环境需求,比如在开发环境中,报错要能定位到源代码的具体位置,而这又需要打出额外的 .map 文件,所以在生产环境中为了不牺牲页面性能,不需要添加此功能,毕竟,没人会在生产上调试代码吧?

虽然都要分别配置,但是又有挺多基础配置是开发和生产都需要且相同的,那我们不可能写两份文件,写两次基础配置吧?这也太冗余了,不过不用担心,webpack-merge 为我们都想好了。

安装它:

npm install webpack-merge -D

scripts/config 下新建文件 webpack.dev.js 作为开发环境配置,并输入以下代码:

const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')

module.exports = merge(common, {
  mode: 'development',
})

同样地,在 scripts/config 下新建文件 webpack.prod.js 作为生产环境配置,并输入以下代码:

const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')

module.exports = merge(common, {
  mode: 'production',
})

在我使用 require('webpack-merge') 时,给我报了以下 eslint 的报错:
'webpack-merge' should be listed in the project's dependencies, not devDependencies.
只需要在 .eslintrc.js 中添加以下规则即可解决:
'import/no-extraneous-dependencies': [ERROR, { devDependencies: true }] 

虽然都分开了配置,但是在公共配置中,还是可能会出现某个配置的某个选项在开发环境和生产环境中采用不同的配置,这个时候我们有两种选择:

  • 一是分别在 dev 和 prod 配置文件中写一遍,common 中就不写了。
  • 二是设置某个环境变量,根据这个环境变量来判别不同环境。

显而易见,为了使代码最大的优雅,采用第二种。

cross-env 可跨平台设置和使用环境变量,不同操作系统设置环境变量的方式不一定相同,比如 Mac 电脑上使用 export NODE_ENV=development ,而 Windows 电脑上使用的是 set NODE_ENV=development ,有了这个利器,我们无需在考虑操作系统带来的差异性。

安装它:

npm install cross-env -D

然后在 package.json 中添加修改以下代码:

{
  "scripts": {
+   "start": "cross-env NODE_ENV=development webpack --config ./scripts/config/webpack.dev.js",
+   "build": "cross-env NODE_ENV=production webpack --config ./scripts/config/webpack.prod.js",
-   "build": "webpack --config ./scripts/config/webpack.common.js",
  },
}

修改 srcipt/constants.js 文件,增加一个公用布尔变量 isDev :

const isDev = process.env.NODE_ENV !== 'production'

module.exports = {
  isDev,
	// other
}

我们现在就使用这个环境变量做点事吧!记得之前配的公共配置中,我们给出口文件的名字配了 hash:8 ,原因是在生产环境中,即用户已经在访问我们的页面了,他第一次访问时,请求了比如 app.js 文件,根据浏览器的缓存策略会将这个文件缓存起来。然后我们开发代码完成了一版功能迭代,涉及到打包后的 app.js 发生了大变化,但是该用户继续访问我们的页面时,如果缓存时间没有超出或者没有人为清除缓存,那么他将继续得到的是已缓存的 app.js ,这就糟糕了。

于是,当我们文件加了 hash 后,根据入口文件内容的不同,这个 hash 值就会发生非常夸张的变化,当更新到线上,用户再次请求,因为缓存文件中找不到同名文件,就会向服务器拿最新的文件数据,这下就能保证用户使用到最新的功能。

不过,这个 hash 值在开发环境中并不需要,于是我们修改 webpack.common.js 文件:

- const { PROJECT_PATH } = require('../constants')
+ const { isDev, PROJECT_PATH } = require('../constants')

module.exports = {
	// other...
  output: {
-   filename: 'js/[name].[hash:8].js',
+   filename: `js/[name]${isDev ? '' : '.[hash:8]'}.js`,
    path: resolve(PROJECT_PATH, './dist'),
  },
}

5. mode

在我们没有设置 mode 时,webpack 默认为我们设为了 mode: 'prodution' ,所以之前打包后的 js 文件代码都没法看,因为在 production 模式下,webpack 默认会丑化、压缩代码,还有其他一些默认开启的配置。

我们只要知道,不同模式下 webpack 为为其默认开启不同的配置,有不同的优化,详细可见 webpack.mode

然后接下来大家可以分别执行以下命令,看看分别打的包有啥区别,主要感知下我们上面所说的:

# 开发环境打包
npm run start

# 生产环境打包
npm run build

6. 本地服务实时查看页面

说了这么多,我们到现在甚至连个页面都看不到,使用过各种脚手架的朋友一定很熟悉 npm run start ,它直接起一个本地服务,然后页面就出来了。而我们现在执行这个命令却只能简单的打个包,别急,我们借助 webpack-dev-serverhtml-webpack-plugin 就能实现,现在先把它们安装下来:

npm install webpack-dev-server html-webpack-plugin -D

简单介绍一下两个工具的作用:

  • html-webpack-plugin :每一个页面是一定要有 html 文件的,而这个插件能帮助我们将打包后的 js 文件自动引进 html 文件中,毕竟你不可能每次更改代码后都手动去引入 js 文件。
  • webpack-dev-server :可以在本地起一个 http 服务,通过简单的配置还可指定其端口、热更新的开启等。

现在,我们先在项目根目录下新建一个 public 文件夹,里面存放一些公用的静态资源,现在我们先在其中新建一个 index.html ,写入以下内容:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React+Typescript 快速开发脚手架</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

注意⚠️:里面有一个 div 标签,id 值为 root

因为 html-webpack-plugin 在开发和生产环境我们都需要配置,于是我们打开 webpck.common.js 增加以下内容:

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: {...},
  output: {...},
  plugins: [
  	new HtmlWebpackPlugin({
      template: resolve(PROJECT_PATH, './public/index.html'),
      filename: 'index.html',
      cache: fale, // 特别重要:防止之后使用v6版本 copy-webpack-plugin 时代码修改一刷新页面为空问题。
      minify: isDev ? false : {
        removeAttributeQuotes: true,
        collapseWhitespace: true,
        removeComments: true,
        collapseBooleanAttributes: true,
        collapseInlineTagWhitespace: true,
        removeRedundantAttributes: true,
        removeScriptTypeAttributes: true,
        removeStyleLinkTypeAttributes: true,
        minifyCSS: true,
        minifyJS: true,
        minifyURLs: true,
        useShortDoctype: true,
      },
    }),
  ]
}

可以看到,我们以 public/index.html 文件为模板,并且在生产环境中对生成的 html 文件进行了代码压缩,比如去除注释、去除空格等。

plugin 是 webpack 的核心功能,它丰富了 webpack 本身,针对是 loader 结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听 webpack 打包过程中的某些节点,执行广泛的任务。

随后在 webpack.dev.js 下增加本地服务的配置:

const { SERVER_HOST, SERVER_PORT } = require('../constants')

module.exports = merge(common, {
  mode: 'development',
  devServer: {
    host: SERVER_HOST, // 指定 host,不设置的话默认是 localhost
    port: SERVER_PORT, // 指定端口,默认是8080
    stats: 'errors-only', // 终端仅打印 error
    clientLogLevel: 'silent', // 日志等级
    compress: true, // 是否启用 gzip 压缩
    open: true, // 打开默认浏览器
    hot: true, // 热更新
  },
})

我们定义了两个新的变量 SERVER_HOST 和 SERVER_PORT ,在 constants.js 中定义它们:

const SERVER_HOST = '127.0.0.1'
const SERVER_PORT = 9000

module.exports = {
  SERVER_HOST,
  SERVER_PORT,
	// ...
}

其中提高开发幸福度的配置项:

  • stats :当设为 error-only 时,终端中只会打印错误日志,这个配置个人觉得很有用,现在开发中经常会被一堆的 warn 日志占满,比如一些 eslint 的提醒规则,编译信息等,头疼的很。
  • clientLogLevel :设为 slient 之后,原来的三条信息会变为只有一条。

image.png

  • hot :这个配置开启后,之后在配合其他配置,可以开启热更新,我们之后再说。

现在配置好了本地服务的相关配置,我们还需要回到 package.json 中修改 start 命令:

{
  "scripts": {
+   "start": "cross-env NODE_ENV=development webpack-dev-server --config ./scripts/config/webpack.dev.js",
-   "start": "cross-env NODE_ENV=development webpack --config ./scripts/config/webpack.dev.js",
  },
}

然后确认一下, src/app.js 中的代码如下:

const root = document.querySelector('#root')
root.innerHTML = 'hello, webpack!'

很简单,就是往之前在 html 文件中定义的 id 为 root 的 div 标签下加了一个字符串。
现在,执行以下命令:

npm run start

你会发现浏览器默认打开了一个页面,屏幕上出现了期待中的 hello, webpack! 。查看控制台,发现 html 文件真的就自动引入了我们打包后的文件~

image.png

至此,我们已经能利用本地服务实时进行页面更新了!当然,这远远是不够的,我们会一步一步继续,尽可能的去完善。

7. devtool

devtool 中的一些设置,可以帮助我们将编译后的代码映射回原始源代码,即大家经常听到的 source-map ,这对于调试代码错误的时候特别重要,而不同的设置会明显影响到构建和重新构建的速度。所以选择一个适合自己的很重要。

它都有哪些值可以设置,官方 devtool 说明中说的很详细,我就不具体展开了,**在这里我非常非常无敌强烈建议大家故意写一些有错误的代码,然后使用每个设置都试试看!**在开发环境中,我个人比较能接受的是 eval-source-map ,所以我会在 webpack.dev.js 中添加以下代码:

module.exports = merge(common, {
  mode: 'development',
+ devtool: 'eval-source-map',
})

在生产环境中我直接设为 none ,不需要 source-map 功能,在 webpack.prod.js 中添加以下代码:

module.exports = merge(common, {
  mode: 'production',
+ devtool: 'none',
})

通过上面配置,我们本地进行开发时,代码出现了错误,控制台的错误日志就会精确地告诉你错误的代码文件、位置等信息。比如我们在 src/app.js 中第 5 行故意写个错误代码:

const root = document.querySelector('#root')
root.innerHTML = 'hello, webpack!'

const a = 5
a = 6

其错误日志提示我们:你的 app.js 文件中第 5 行出错了,具体错误原因为 balabala.... ,赶紧看看吧~

image.png

完美!完美了吗?

image.png

如果你已经执行过多次 npm run build ,你会发现事情不简单:

image.png

妈蛋,多出了那么多 app.xxxxxxxx.js ,为了我们最终打包后没有前一次打包出来的多余文件,得想个办法处理这个问题。

8. 打包编译前清理 dist 目录

我们发现每次打出来的文件都会继续残留在 dist 目录中,当然如果你足够勤快,可以每次打包前手动清理一下,但是这种勤劳是毫无意义的。

借助 clean-webpack-plugin 可以实现每次打包前先处理掉之前的 dist 目录,以保证每次打出的都是当前最新的,我们先安装它:

npm install clean-webpack-plugin -D

打开 webpack.prod.js 文件,增加以下代码:

const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
	// other...
  plugins: [
    new CleanWebpackPlugin(),
  ],
}

它不需要你去指定要删除的目录的位置,会自动找到 output 中的 path 然后进行清除。
现在再执行一下 npm run build ,看看打出来的 dist 目录是不是干净清爽了许多?

9. 样式文件处理

如果你现在在 src/ 目录下新建一个 app.css 文件,给 #root 随便添加一个样式, app.js 中通过 import './app.css' ,再进行打包或本地服务启动,webpack 直接就会报错,因为 webpack 只会编译 .js 文件,它是不支持直接处理 .css 、 .less 或 .scss 文件的,我们需要借助 webpack 中另一个很核心的东西:**loader **。

loader 用于对模块的源代码进行转换。loader 可以使你在 import 或"加载"模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的强大方法。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript,或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS文件!

CSS 样式文件处理

处理 .css 文件我们需要安装 style-loadercss-loader

npm install style-loader css-loader -D
  • 遇到后缀为 .css 的文件,webpack 先用 css-loader 加载器去解析这个文件,遇到 @import 等语句就将相应样式文件引入(所以如果没有 css-loader ,就没法解析这类语句),计算后生成css字符串,接下来 style-loader 处理此字符串生成一个内容为最终解析完的 css 代码的 style 标签,放到 head 标签里。

  • loader 是有顺序的,webpack 肯定是先将所有 css 模块依赖解析完得到计算结果再创建 style 标签。因此应该把 style-loader 放在 css-loader 的前面(webpack loader 的执行顺序是从右到左,即从后往前)。

于是,打开我们的 webpack.common.js ,写入以下代码:

module.exports = {
	// other...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              modules: false, // 默认就是 false, 若要开启,可在官网具体查看可配置项
              sourceMap: isDev, // 开启后与 devtool 设置一致, 开发环境开启,生产环境关闭
              importLoaders: 0, // 指定在 CSS loader 处理前使用的 laoder 数量
            },
          },
        ],
      },
    ]
  },
}

test 字段是匹配规则,针对符合规则的文件进行处理。

use 字段有几种写法:

  • 可以是一个字符串,假如我们只使用 style-loader ,只需要 use: 'style-loader' 。
  • 可以是一个数组,假如我们不对 css-loader 做额外配置,只需要 use: ['style-loader', 'css-loader']
  • 数组的每一项既可以是字符串也可以是一个对象,当我们需要在webpack 的配置文件中对 loader 进行配置,就需要将其编写为一个对象,并且在此对象的 options 字段中进行配置。比如我们上面要对 css-loader 做配置的写法。

LESS 样式文件处理

处理 .less 文件我们需要安装 lessless-loader

npm install less less-loader -D
  • 遇到后缀为 .less 文件, less-loader 会将你写的 less 语法转换为 css 语法,并转为 .css 文件。
  • less-loader 依赖于 less ,所以必须都安装。

继续在 webpack.common.js 中写入代码:

module.exports = {
	// other...
  module: {
    rules: [
      { /* ... */ },
      {
        test: /\.less$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              modules: false,
              sourceMap: isDev,
              importLoaders: 1, // 需要先被 less-loader 处理,所以这里设置为 1
            },
          },
          {
            loader: 'less-loader',
            options: {
              sourceMap: isDev,
            },
          },
        ],
      },
    ]
  },
}

SASS 样式文件处理

处理 .scss 文件我们需要安装 node-sasssass-loader

npm install node-sass sass-loader -D
  • 遇到 .scss 后缀的文件, sass-loader 会将你写的 sass 语法转为 css 语法,并转为 .css 文件。
  • 同样地, sass-loader 依赖于 node-sass ,所以两个都需要安装。( node-sass 我不用代理就没有正常安装上过,还好我们一开始就在配置文件里设了淘宝镜像源)

继续在 webpack.common.js 中写入代码:

module.exports = {
	// other...
  module: {
    rules: [
      { /* ... */ },
      {
        test: /\.scss$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              modules: false,
              sourceMap: isDev,
              importLoaders: 1, // 需要先被 sass-loader 处理,所以这里设置为 1
            },
          },
          {
            loader: 'sass-loader',
            options: {
              sourceMap: isDev,
            },
          },
        ],
      },
    ]
  },
}

现在,通过以上配置之后,你再把 src/app.css 改为 app.less 或 app.scss ,执行 npm run start ,你会发现咱们的样式正常加载了出来,开心噢~

PostCSS 处理浏览器兼容问题

postcss 一种对 css 编译的工具,类似 babel 对 js 一样通过各种插件对 css 进行处理,在这里我们主要使用以下插件:

  • postcss-flexbugs-fixes :用于修复一些和 flex 布局相关的 bug。
  • postcss-preset-env :将最新的 CSS 语法转换为目标环境的浏览器能够理解的 CSS 语法,目的是使开发者不用考虑浏览器兼容问题。我们使用 autoprefixer 来自动添加浏览器头。
  • postcss-normalize :从 browserslist 中自动导入所需要的 normalize.css 内容。

安装上面提到的所需的包:

npm install postcss-loader postcss-flexbugs-fixes postcss-preset-env autoprefixer postcss-normalize -D

postcss-loader 放到 css-loader 后面,配置如下:

{
  loader: 'postcss-loader',
  options: {
    ident: 'postcss',
    plugins: [
      require('postcss-flexbugs-fixes'),
      require('postcss-preset-env')({
        autoprefixer: {
          grid: true,
          flexbox: 'no-2009'
        },
        stage: 3,
      }),
      require('postcss-normalize'),
    ],
    sourceMap: isDev,
  },
},

但是我们要为每一个之前配置的样式 loader 中都要加一段这个,这代码会显得非常冗余,于是我们把公共逻辑抽离成一个函数,与 cra 一致,命名为 getCssLoaders ,因为新增了 postcss-loader ,所以我们要修改 importLoaders ,于是我们现在的 webpack.common.js 修改为以下这样:

const getCssLoaders = (importLoaders) => [
  'style-loader',
  {
    loader: 'css-loader',
    options: {
      modules: false,
      sourceMap: isDev,
      importLoaders,
    },
  },
  {
    loader: 'postcss-loader',
    options: {
      ident: 'postcss',
      plugins: [
        // 修复一些和 flex 布局相关的 bug
        require('postcss-flexbugs-fixes'),
        require('postcss-preset-env')({
          autoprefixer: {
            grid: true,
            flexbox: 'no-2009'
          },
          stage: 3,
        }),
        require('postcss-normalize'),
      ],
      sourceMap: isDev,
    },
  },
]

module.exports = {
	// other...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: getCssLoaders(1),
      },
      {
        test: /\.less$/,
        use: [
          ...getCssLoaders(2),
          {
            loader: 'less-loader',
            options: {
              sourceMap: isDev,
            },
          },
        ],
      },
      {
        test: /\.scss$/,
        use: [
          ...getCssLoaders(2),
          {
            loader: 'sass-loader',
            options: {
              sourceMap: isDev,
            },
          },
        ],
      },
    ]
  },
  plugins: [//...],
}

最后,我们还得在 package.json 中添加 browserslist (指定了项目的目标浏览器的范围):

{
  "browserslist": [
    ">0.2%",
    "not dead", 
    "ie >= 9",
    "not op_mini all"
  ],
}

现在,在如果你在入口文件(比如我之前一直用的 app.js )随便引一个写了 display: flex 语法的样式文件, npm run start 看看是不是自动加了浏览器前缀了呢?快试试吧!

10. 图片和字体文件处理

我们可以使用 file-loader 或者 url-loader 来处理本地资源文件,比如图片、字体文件,而 url-loader 具有 file-loader 所有的功能,还能在图片大小限制范围内打包成 base64 图片插入到 js 文件中,这样做的好处是什么呢?别急,我们先安装所需要的包(后者依赖前者,所以都要安装):

npm install file-loader url-loader -D

然后在 webpack.common.js 中继续在 modules.rules 中添加以下代码:

module.exports = {
  // other...
  module: {
    rules: [
      // other...
      {
        test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 10 * 1024,
              name: '[name].[contenthash:8].[ext]',
              outputPath: 'assets/images',
            },
          },
        ],
      },
      {
        test: /\.(ttf|woff|woff2|eot|otf)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              name: '[name].[contenthash:8].[ext]',
              outputPath: 'assets/fonts',
            },
          },
        ],
      },
    ]
  },
  plugins: [//...],
}
  • [name].[contenthash:8].[ext] 表示输出的文件名为 原来的文件名.哈希值.后缀 ,有了这个 hash 值,可防止图片更换后导致的缓存问题。
  • outputPath 是输出到 dist 目录下的路径,即图片目录 dist/assets/images 以及字体相关目录 dist/assets/fonts 下。
  • limit 表示如果你这个图片文件大于 10240b ,即 10kb ,那我 url-loader 就不用,转而去使用 file-loader ,把图片正常打包成一个单独的图片文件到设置的目录下,若是小于了 10kb ,就将图片打包成 base64 的图片格式插入到打包之后的文件中,这样做的好处是,减少了 http 请求,但是如果文件过大,js文件也会过大,得不偿失,这是为什么有 limit 的原因!

接下来大家引一下本地的图片并放到 img 标签中,或者去 iconfont 下个字体图标试试吧~

不幸的是,当你尝试引入一张图片的时候,会有以下 ts 的报错(如果你安装了 ts 的话):

image.png

这个时候在 src/ 下新建以下文件 typings/file.d.ts ,输入以下内容即可:

declare module '*.svg' {
  const path: string
  export default path
}

declare module '*.bmp' {
  const path: string
  export default path
}

declare module '*.gif' {
  const path: string
  export default path
}

declare module '*.jpg' {
  const path: string
  export default path
}

declare module '*.jpeg' {
  const path: string
  export default path
}

declare module '*.png' {
  const path: string
  export default path
}

其实看到现在已经很不容易了,不过我相信大家仔细跟到现在的话,也会收获不少的,上面的 webpack 基本配置只是配置了最基本的功能,接下来我们要达到支持 React,TypeScript 以及一堆的开发环境和生产环境的优化,大家加油噢~

image.png

支持 React

终于来到我们 React 的支持环节了,美好的开始就是安装 react 和 react-dom :

npm install react react-dom -S

-S 相当于 --save , -D 相当于 --save-dev 。

其实安装了这两个包就已经能使用 jsx 语法了,我们在 src/index.js 中输入以下代码:

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

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

src/app.js 中输入以下示例代码:

import React from 'react'

function App() {
  return <div className='App'>Hello World</div>
}

export default App

然后修改 webpack.common.js 中 entry 字段,修改入口文件为 index.js :

module.exports = {
  entry: {
+   app: resolve(PROJECT_PATH, './src/index.js'),
-   app: resolve(PROJECT_PATH, './src/app.js'),
  },
}

如果这时候,你无论尝试 npm run start 还是 npm run build ,结果都会报错:

image.png

诶!为啥啊,我不是都安装了 react 了吗,咋还不行啊?
因为 webpack 根本识别不了 jsx 语法,那怎么办?使用 babel-loader 对文件进行预处理。

在此,强烈建议大家先阅读一篇关于 babel 写的很好的文章:不容错过的 Babel7 知识,绝对的收获满满,我知道在自己文章中插入一个链接,让读者去阅读再回来接着读这种行为挺让人反感的,我看别人文章时也有这种感觉,但是在这里我真的不得不推荐,一定要读!一定要读!一定要读!

好了,安装该有的包:

npm install babel-loader @babel/core @babel/preset-react -D

babel-loader 使用 babel 解析文件;@babel/core 是 babel 的核心模块;@babel/preset-react 转译 jsx 语法。

在根目录下新建 .babelrc 文件,输入以下代码:

{
  "presets": ["@babel/preset-react"]
}

presets 是一些列插件集合。比如 @babel/preset-react 一般情况下会包含 @babel/plugin-syntax-jsx 、 @babel/plugin-transform-react-jsx 、 @babel/plugin-transform-react-display-name 这几个 babel 插件。

接下来打开我们的 webpack.common.js 文件,增加以下代码:

module.exports = {
	// other...
  module: {
    rules: [
      {
        test: /\.(tsx?|js)$/,
        loader: 'babel-loader',
        options: { cacheDirectory: true },
        exclude: /node_modules/,
      },
      // other...
    ]
  },
  plugins: [ //... ],
}

注意,我们匹配的文件后缀只有 .tsx 、.ts 、 .js ,我把 .jsx 的格式排除在外了,因为我不可能在 ts 环境下建 .jsx 文件,实在要用 jsx 语法的时候,用 .js 不香吗?

babel-loader 在执行的时候,可能会产生一些运行期间重复的公共文件,造成代码体积大冗余,同时也会减慢编译效率,所以我们开启 cacheDirectory 将这些公共文件缓存起来,下次编译就会加快很多。

建议给 loader 指定 include 或是 exclude,指定其中一个即可,因为 node_modules 目录不需要我们去编译,排除后,有效提升编译效率。

现在,我们可以 npm run start 看看效果了!其实 babel 还有一些其他重要的配置,我们先把 TS 支持了再一起搞!

支持 TypeScript

webpack 模块系统只能识别 js 文件及其语法,遇到 jsx 语法、tsx 语法、文件、图片、字体等就需要相应的 loader 对其进行预处理,像图片、字体这种我们上面已经配置了,为了支持 React,我们使用了 babel-loader 以及对应的插件,现在如果要支持 TypeScript 我们也需要对应的插件。

1. 安装对应 babel 插件

@babel/preset-typescript 是 babel 的一个 preset,它编译 ts 的过程很粗暴,它直接去掉 ts 的类型声明,然后再用其他 babel 插件进行编译,所以它很快。

废话补多少,先来安装它:

npm install @babel/preset-typescript -D

注意:我们之前因为 Eslint 的配置地方需要先安装 Typescript,所以之前安装过的就不用再安装一次了。

然后修改 .babelrc :

{
  "presets": ["@babel/preset-react", "@babel/preset-typescript"]
}

presets 的执行顺序是从后到前的。根据以上代码的 babel 配置,会先执行 @babel/preset-typescript ,然后再执行 @babel/preset-react 。

2. tsx 语法测试

src/ 有以下两个 .tsx 文件,代码分别如下:

index.tsx :

import React from 'react'
import ReactDOM from 'react-dom'
import App from './app'

ReactDOM.render(
  <App name='vortesnail' age={25} />,
  document.querySelector('#root')
)

app.tsx :

import React from 'react'

interface IProps {
  name: string
  age: number
}

function App(props: IProps) {
  const { name, age } = props
  return (
    <div className='app'>
      <span>{`Hello! I'm ${name}, ${age} years old.`}</span>
    </div>
  )
}

export default App

很简单的代码,在 <App /> 中输入属性时因为 ts 有了良好的智能提示,比如你不输入 name 和 age ,那么就会报错,因为在 <App /> 组件中,这两个属性时必须值!

但是这个时候如果你 npm run start ,发现是编译有错误的,我们修改 webpack.common.js 文件:

module.exports = {
  entry: {
    app: resolve(PROJECT_PATH, './src/index.tsx'),
  },
  output: {//...},
  resolve: {
    extensions: ['.tsx', '.ts', '.js', '.json'],
  },
}

一来修改了 entry 中的入口文件后缀,变为 .tsx 。

二来新增了 resolve 属性,在 extensions 中定义好文件后缀名后,在 import 某个文件的时候,比如上面代码:

import App from './app'

就可以不加文件后缀名了。webpack 会按照定义的后缀名的顺序依次处理文件,比如上文配置 ['.tsx', '.ts', '.js', '.json'] ,webpack 会先尝试加上 .tsx 后缀,看找得到文件不,如果找不到就依次尝试进行查找,所以我们在配置时尽量把最常用到的后缀放到最前面,可以缩短查找时间。

这个时候再进行 npm run start ,页面就能正确输出了。

既然都用上了 Typescript,那 React 的类型声明自然不能少,安装它们:

npm install @types/react @types/react-dom -D

3. tsconfig.json 详解

每个 Typescript 都会有一个 tsconfig.json 文件,其作用简单来说就是:

  • 编译指定的文件
  • 定义了编译选项

一般都会把 tsconfig.json 文件放在项目根目录下。在控制台输入以下代码来生成此文件:

npx tsc --init

打开生成的 tsconfig.json ,有很多注释和几个配置,有点点乱,我们就将这个文件的内容删掉吧,重新输入我们自己的配置。

此文件中现在的代码为:

{
  "compilerOptions": {
    // 基本配置
    "target": "ES5",                          // 编译成哪个版本的 es
    "module": "ESNext",                       // 指定生成哪个模块系统代码
    "lib": ["dom", "dom.iterable", "esnext"], // 编译过程中需要引入的库文件的列表
    "allowJs": true,                          // 允许编译 js 文件
    "jsx": "react",                           // 在 .tsx 文件里支持 JSX
    "isolatedModules": true,
    "strict": true,                           // 启用所有严格类型检查选项

    // 模块解析选项
    "moduleResolution": "node",               // 指定模块解析策略
    "esModuleInterop": true,                  // 支持 CommonJS 和 ES 模块之间的互操作性
    "resolveJsonModule": true,                // 支持导入 json 模块
    "baseUrl": "./",                          // 根路径
    "paths": {																// 路径映射,与 baseUrl 关联
      "Src/*": ["src/*"],
      "Components/*": ["src/components/*"],
      "Utils/*": ["src/utils/*"]
    },

    // 实验性选项
    "experimentalDecorators": true,           // 启用实验性的ES装饰器
    "emitDecoratorMetadata": true,            // 给源码里的装饰器声明加上设计类型元数据

    // 其他选项
    "forceConsistentCasingInFileNames": true, // 禁止对同一个文件的不一致的引用
    "skipLibCheck": true,                     // 忽略所有的声明文件( *.d.ts)的类型检查
    "allowSyntheticDefaultImports": true,     // 允许从没有设置默认导出的模块中默认导入
    "noEmit": true														// 只想使用tsc的类型检查作为函数时(当其他工具(例如Babel实际编译)时)使用它
  },
  "exclude": ["node_modules"]
}

compilerOptions 用来配置编译选项,其完整的可配置的字段从这里可查询到; exclude 指定了不需要编译的文件,我们这里是只要是 node_modules 下面的我们都不进行编译,当然,你也可以使用 include 去指定需要编译的文件,两个用一个就行。

接下来对 compilerOptions 重要配置做一下简单的解释:

  • target 和 module :这两个参数实际上没有用,它是通过 tsc 命令执行才能生成对应的 es5 版本的 js 语法,但是实际上我们已经使用 babel 去编译我们的 ts 语法了,根本不会使用 tsc 命令,所以它们在此的作用就是让编辑器提供错误提示。

  • isolatedModules :可以提供额外的一些语法检查。

比如不能重复 export :

import { add } from './utils'
add()

export { add } // 会报错

比如每个文件必须是作为独立的模块:

const print = (str: string) => { console.log(str) } // 会报错,没有模块导出

// 必须有 export
export print = (str: string) => { 
  console.log(str) 
}
  • esModuleInterop :允许我们导入符合 es6 模块规范的 CommonJS 模块,下面做简单说明。

比如某个包为 test.js :

// node_modules/test/index.js
exports = test

使用此包:

// 我们项目中的 app.tsx
import * as test from 'test'
test()

开启 esModuleInterop 后,直接可如下使用:

import test from 'test'
test()

接下来我们着重讲下 baseUrl 和 paths ,这两个配置真的是提升开发效率的利器啊!它的作用就是快速定位某个文件,防止多层 ../../../ 这种写法找某个模块!比如我现在的 src/ 下有这么几个文件:

image.png

我在 app.js 中要引入 src/components 下的 Header 组件,以往的方式是:

import Header from './components/Header'

大家可能觉得,蛮好的啊,没毛病。但是我这里是因为 app.tsx 和 components 是同级的,试想一下如果你在某个层级很深的文件里要用 components ,那就是疯狂 ../../../.. 了,所以我们要学会使用它,并结合 webpack 的 resolve.alias 使用更香。

但是想用它麻烦还蛮多的,咱一步步拆解它。

首先 baseUrl 一定要设置正确,我们的 tsconfig.json 是放在项目根目录的,那么 baseUrl 设为 ./ 就代表了项目根路径。于是, paths 中的每一项路径映射,比如 ["src/*"] 其实就是相对根路径。

如果大家像上面一样配置了,并自己尝试用以下方式开始进行模块的引入:

import Header from 'Components/Header'

因为 eslint 的原因,是会报错的:

image.png

这个时候需要改 .eslintrc.js 文件的配置了,首先得安装 eslint-import-resolver-typescript

npm install eslint-import-resolver-typescript -D

然后在 .eslintrc.js 文件的 setting 字段修改为以下代码:

settings: {
  'import/resolver': {
    node: {
      extensions: ['.tsx', '.ts', '.js', '.json'],
    },
    typescript: {},
  },
},

是的,只需要添加 typescript: {} 即可,这时候再去看已经没有报错了。
但是上面我们完成的工作仅仅是对于编辑器来说可识别这个路径映射,我们需要在 webpack.common.js 中的 resolve.alias 添加相同的映射规则配置:

module.exports = {
  // other...
  resolve: {
    extensions: ['.tsx', '.ts', '.js', '.json'],
    alias: {
      'Src': resolve(PROJECT_PATH, './src'),
      'Components': resolve(PROJECT_PATH, './src/components'),
      'Utils': resolve(PROJECT_PATH, './src/utils'),
    }
  },
  module: {//...},
  plugins: [//...],
}

现在,两者一致就可以正常开发和打包了!可能有的小伙伴会疑惑,我只配置 webpack 中的 alias 不就行了吗?虽然开发时会有报红,但并不会影响到代码的正确,毕竟打包或开发时 webpack 都会进行路径映射替换。是的,的确是这样,但是在 tsconfig.json 中配置,会给我们增加智能提示,比如我打字打到 Com ,编辑器就会给我们提示正确的 Components ,而且其下面的文件还会继续提示。

如果你参与过比较庞大、文件层级会很深的项目你就能明白智能提示真的很香。

image.png

更多 babel 配置

之前我们已经使用 babel 去解析 react 语法和 typescript 语法了,但是目前我们所做的也仅仅如此,你在代码中用到的 ES6+ 语法编译之后依然全部保留,然而不是所有浏览器都能支持 ES6+ 语法的,这时候就需要@babel/preset-env 来做这个苦力活了,它会根据设置的目标浏览器环境(browserslist)找出所需的插件去转译 ES6+ 语法。比如 const 或 let 转译为 var 。

但是遇到 Promise 或 .includes 这种新的 es 特性,是没办法转译到 es5 的,除非我们把这中新的语言特性的实现注入到打包后的文件中,不就行了吗?我们借助 @babel/plugin-transform-runtime 这个插件,它和 @babel/preset-env 一样都能提供 ES 新API 的垫片,都可实现按需加载,但前者不会污染原型链。

另外,babel 在编译每一个模块的时候在需要的时候会插入一些辅助函数例如 _extend ,每一个需要的模块都会生成这个辅助函数,显而易见这会增加代码的冗余,@babel/plugin-transform-runtime 这个插件会将所有的辅助函数都从 @babel/runtime-corejs3 导入(我们下面使用 corejs3),从而减少冗余性。

安装它们:

npm install @babel/preset-env @babel/plugin-transform-runtime -D
npm install @babel/runtime-corejs3 -S

注意: @babel/runtime-corejs3 的安装为生产依赖。

修改 .babelre 如下:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        // 防止babel将任何模块类型都转译成CommonJS类型,导致tree-shaking失效问题
        "modules": false
      }
    ],
    "@babel/preset-react",
    "@babel/preset-typescript"
  ],
  "plungins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": {
          "version": 3,
          "proposals": true
        },
        "useESModules": true
      }
    ]
  ]
}

ok,搞定!

到此为止,我们的 react+typescript 项目开发环境已经可行了,就是说现在已经可以正常进行开发了,但是针对开发环境和生产环境,我们能做的优化还有很多,大家继续加油!

公共(common)环境优化

这部分主要针对无论开发环境还是生产环境都需要的公共配置优化。

1. 拷贝公共静态资源

大家有没有注意到,到目前为止,我们的开发页面还是没有 icon 的,就下面这个东西:
image.png
create-react-app 一样,我们将 .ico 文件放到 public/ 目录下,比如我就复制了一个 cra 的 favicon.ico 文件,然后在我们的 index.html 文件中加入以下标签:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
+   <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React+Typescript 快速开发脚手架</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

这时候你 npm run build 打个包,我们看到 dist 目录下是没有 favicon.ico 文件的,那么 html 文件中的引入肯定就无法起效了。于是我们希望有一个手段,在打包时能把 public/ 文件夹下的静态资源复制到我们打包后生成的 dist 目录中,除非你想每次打包完手动复制,不然就借助 copy-webpack-plugin 吧!

安装它:

npm install copy-webpack-plugin -D

修改 webpack.common.js 文件,增加以下代码:

const CopyPlugin = require('copy-webpack-plugin')

module.exports = {
	plugins: [
    // 其它 plugin...
  	new CopyPlugin({
      patterns: [
        {
          context: resolve(PROJECT_PATH, './public'),
          from: '*',
          to: resolve(PROJECT_PATH, './dist'),
          toType: 'dir',
        },
      ],
    }),
  ]
}

然后你重新 npm run start ,再清下页面缓存,你会看到我们的小图标就出来了,现在你可以替换成你自己喜欢的图标了。

image.png

同样地,其它的静态资源文件,大家只要往 public/ 目录下丢,打包之后都会自动复制到 dist/ 目录下。

特别注意⚠️:在讲基础配置配置 html-webpack-plugin 时,注释中特别强调过要配置 cache: false ,如果不加的话,你代码修改之后刷新页面,html 文件不会引入任何打包出来的 js 文件,自然也没有执行任何 js 代码,特别可怕,我搞了好久,查了 copy-webpack-plugin 官方 issue 才找到的解决方案。

2. 显示编译进度

我们现在执行 npm run start 或 npm run build 之后,控制台没有任何信息能告诉我们现在编译的进度怎么样,在大型项目中,编译打包的速度往往需要很久,如果不是熟悉此项目尿性的人,基本都会认为是不是卡住了,从而极大地增强了焦虑感。。。所以,显示打包的进度是非常重要的,这是对开发者积极的正向反馈。

在我看来,人活着,心中希望真的很重要。

我们可以借助 webpackbar 来完成此项任务,安装它:

npm install webpackbar -D

webpack.common.js 增加以下代码:

const WebpackBar = require('webpackbar')

module.exports = {
	plugins: [
    // 其它 plugin...
  	new WebpackBar({
      name: isDev ? '正在启动' : '正在打包',
      color: '#fa8c16',
    }),
  ]
}

现在我们本地起服务还是打包都有进度展示了,是不是特别舒心呢?我真的很喜欢这个插件。

3. 编译时的 Typescirpt 类型检查

我们之前配置 babel 的时候说过,为了编译速度,babel 编译 ts 时直接将类型去除,并不会对 ts 的类型做检查,来看一个例子,大家看我之前创建的 src/app.tsx 文件下,我故意解构出一个事先没有声明的类型:
image.png
如上所示,我尝试解构的 wrong 是没有在我们的 IProps 中声明的,在编辑器中肯定会报错的,但是重点是,在某一刻某一个人某种情况下就是犯了这样的错误,而它没有去处理这个问题,我们接手这个项目之后,并不知道有这么个问题,然后本地开发或打包时,依然可以正常进行,这完全丧失了 typescript 类型声明所带来的优势以及带来了重大的隐性 bug!

所以,我们需要借助 fork-ts-checker-webpack-plugin ,在我们打包或启动本地服务时给予错误提示,那就安装它吧:

npm install fork-ts-checker-webpack-plugin -D

webpack.common.js 中增加以下代码:

const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')

module.exports = {
	plugins: [
    // 其它 plugin...
  	new ForkTsCheckerWebpackPlugin({
      typescript: {
        configFile: resolve(PROJECT_PATH, './tsconfig.json'),
      },
    }),
  ]
}

现在,我们执行 npm run build 看看,会有以下错误提示:

image.png

发现问题之后我们就可以去解决它了,而不是啥都不知道任由其隐性 bug 存在。

4. 加快二次编译速度

这里所说的“二次”意思为首次构建之后的每一次构建。

有一个神器能大大提高二次编译速度,它为程序中的模块(如 lodash)提供了一个中间缓存,放到本项目 node_modules/.cache/hard-source 下,就是 hard-source-webpack-plugin ,首次编译时会耗费稍微比原来多一点的时间,因为它要进行一个缓存工作,但是再之后的每一次构建都会变得快很多!我们先来安装它:

npm install hard-source-webpack-plugin -D

webpack.common.js 中增加以下代码:

const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')

module.exports = {
	plugins: [
    // 其它 plugin...
  	new HardSourceWebpackPlugin(),
  ]
}

这时候我们执行两次 npm run start 或 npm run build ,看看花费时间对比图:

image.png

随着项目变大,这个速度差距会更明显。

5. external 减少打包体积

到目前为止,我们无论是开发还是生产,都要先经过 webpack 将 react、react-dom 的代码打进我们最终生成的代码中,试想一下,当这种第三方包变得越来也多的时候,最后打出的文件将会很大,用户每次进入页面需要下载一个那么大的文件,带来的就是白屏时间变长,将会严重影响用户体验,所以我们将这种第三方包剥离出去或者采用 CDN 链接形式。

修改 webpack.common.js ,增加以下代码:

module.exports = {
	plugins: [
    // 其它 plugin...
  ],
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
  },
}

在开发时,我们是这样使用 react 和 react-dom 的:

import React from 'react'
import ReactDOM from 'react-dom'

那么,我们最终打完的包已经不注入这两个包的代码了,肯定得有另外的方式将其引入,不然程序都无法正确运行了,于是我们打开 public/index.html ,增加以下 CDN 链接:

<!DOCTYPE html>
<html lang="en">
  <body>
    <div id="root"></div>
+   <script crossorigin src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
+   <script crossorigin src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
  </body>
</html>

它们各自的版本可在 package.json 去确定!

然后我们对比一下添加 externals 前后的打包体积会发现相差很多。

这个时候大家就疑惑了,我无论添不添加 externals,最终需要下载的文件大小其实并没有变啊,只不过一个是一次性下载一个文件,另一个是一次性下载三个文件,大小都不变,时间应该也不变啊?其实它有以下优势:

  • http 缓存:当用户第一次下载后,之后每次进入页面,根据浏览器的缓存策略,都不需要再重新下载 react 和 react-dom。
  • webpack 编译时间减少:因为少了一步打包编译 react 和 react-dom 的工作,因此速度会提升。

跟大家说明下,关于 externals 的配置,如果是用在自己的项目里,这样配完全没问题,但是如果用该脚手架开发 react 组件,并需要发布到 npm 上的,那如果你把 react 这种依赖没有打进最终输出的包里,那么别人下载了你这个包就需要 npm install [email protected] -S ,这其实是有问题的,你无法保证别人的 react 版本和你一致,这个问题我们之后会再说,现在先提个醒~

6. 抽离公共代码

我们先来讲一下ES6中的懒加载

懒加载是优化网页首屏速度的利器,下面演示一个简单的例子,让大家明白有什么好处。

一般情况下,我们引入某个工具函数是这样的:

import { add } from './math.js';

如果这样引入,在打包之后, math.js 这个文件中的代码就会打进最终的包里,**即使这个 ****add** **方法不一定在首屏就会使用!**那么带来的坏处显而易见,我都不需要在首屏使用它,却要承担下载这个目前的多余代码的响应速度变慢的后果!

但是,如果现在我们以下面的方式进行引入:

import("./math").then(math => {
  console.log(math.add(16, 26))
})

webpack 就会自动解析这个语法,进行代码分割,打包出来之后, math.js 中的代码会被自动打成一个独立的 chunk 文件,只有我们在页面交互时调用了这个方法,页面才会下载这个文件,并执行调用的方法。

同理,我们也可以对 React 组件进行这样的懒加载,只需借助 React.lazy 和 React.Suspense 即可,下面做个简单的演示:

src/app.tsx :

import React, { Suspense, useState } from 'react'

const ComputedOne = React.lazy(() => import('Components/ComputedOne'))
const ComputedTwo = React.lazy(() => import('Components/ComputedTwo'))

function App() {
  const [showTwo, setShowTwo] = useState<boolean>(false)

  return (
    <div className='app'>
      <Suspense fallback={<div>Loading...</div>}>
        <ComputedOne a={1} b={2} />
        {showTwo && <ComputedTwo a={3} b={4} />}
        <button type='button' onClick={() => setShowTwo(true)}>
          显示Two
        </button>
      </Suspense>
    </div>
  )
}

export default App

src/components/ComputedOne/index.tsx :

import React from 'react'
import './index.scss'
import { add } from 'Utils/math'

interface IProps {
  a: number
  b: number
}

function ComputedOne(props: IProps) {
  const { a, b } = props
  const sum = add(a, b)

  return <p className='computed-one'>{`Hi, I'm computed one, my sum is ${sum}.`}</p>
}

export default ComputedOne

ComputedTwo 组件代码与 ComputedOne 组件代码相似, math.ts 是简单的求和函数,就不贴代码了。

接下来,我们 npm run start ,并打开控制台的 Network,会发现以下动态加载 chunk 文件:
12.gif
以上演示便是实现了组件的懒加载方式。接下来,执行一下 npm run build 看看打包出来了以下文件:
image.png
红线框住的文件就是两个组件( ComputedOne 和 ComputedTwo )的代码,这样带来的好处很明显:

  • 若通过懒加载引入的组件,若该组件代码不变,打出的包名也不会变,部署到生产环境后,因为浏览器缓存原因,用户不需要再次下载该文件,缩短了网页交互时间。
  • 防止把所有组件打进一个包,降低了页面首屏时间。

懒加载带来的优势不可小觑,我们沿着这个思维模式向外延伸思考,如果我们能把一些引用的第三方包也打成单独的 chunk,是否也会具有同样的优势呢?

image.png

答案是肯定的,因为第三方依赖包只要版本锁定,代码是不会有变化的,那么每一次项目代码的迭代,都不会影响到依赖包 chunk 文件的文件名,那么就会同样具有以上优势!

其实 webpack4 默认就开启该功能,所以以上演示的懒加载才会打出独立 chunk 文件,但是要将第三方依赖也打出来独立 chunk,我们需要在 webpack.common.js 中增加以下代码:

module.exports = {
	// other...
  externals: {//...},
  optimization: {
    splitChunks: {
      chunks: 'all',
      name: true,
    },
  },
}

这个时候我们 npm run build ,就会发现多了这么一个包:

image.png

这个 chunk 里放了一些我们没有通过 externals 剔除的第三方包的代码,若大家不想通过 cdn 形式引入 react 和 react-dom ,这里也可以进行相应的配置将它们单独抽离出来;另一方面,若是多页应用,还需要配置把公共模块也抽离出来,这里因为我们是搭建单页应用开发环境,就不演示了。

给大家推荐两个学习 splitChunks 配置的地方:1. webpack官方介绍;2. 理解webpack4.splitChunks

开发(dev)环境优化

这部分主要针对无论开发环境还是开发环境都需要的公共配置优化。

1. 热更新

如果你开发时忍受过稍微改一下代码,页面就会重新刷新的痛苦,那么热更新一定得学会了!可能小项目你觉得没什么,都一样快,但是项目大了每一次编译都是直击内心的痛!

image.png

所谓的热更新其实就是,页面只会对你改动的地方进行“局部刷新”,这个说法可能不严谨,但是想必大家能理解什么意思。打开 webpack.dev.js ,执行以下三个步骤即可使用:

第一步:将 devServer 下的 hot 属性设为 true 。
image.png

第二步:新增 webpack.HotModuleReplacementPlugin 插件:

const webpack = require('webpack')

module.exports = merge(common, {
  devServer: {//...},
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
  ]
})

这个时候,你 npm run start 并尝试改变局部的代码,保存后发现整个页面还是会进行刷新,如果你希望得到上面所说的“局部刷新”,需要在项目入口文件加以下代码。

第三步:修改入口文件,比如我就选择 src/index.js 作为我的入口文件:

import React from 'react'
import ReactDOM from 'react-dom'
import App from './app'

if (module && module.hot) {
  module.hot.accept()
}

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

这时候因为 ts 的原因会报错:
image.png
我们只需要安装 @types/webpack-env 即可:

npm install @types/webpack-env -D

现在,我们在重新 npm run start ,在页面上随便修改个代码看看,是不是不会整体刷新了?舒服~

2. 跨域请求

一般来说,利用 devServer 本来就有的 proxy 字段就能配置接口代理进行跨域请求,但是为了使构建环境的代码与业务代码分离,我们需要将配置文件独立出来,可以这样做:

第一步:在 src/ 下新建一个 setProxy.js 文件,并写入以下代码:

const proxySettings = {
  // 接口代理1
  '/api/': {
    target: 'http://198.168.111.111:3001',
    changeOrigin: true,
  },
  // 接口代理2
  '/api-2/': {
    target: 'http://198.168.111.111:3002',
    changeOrigin: true,
    pathRewrite: {
      '^/api-2': '',
    },
  },
  // .....
}

module.exports = proxySettings

配置完成,我们要在 webpack.dev.js 中要引入,并正确放大 devServer 的 proxy 字段。

第二步:简单的引入及解构下就行:

const proxySetting = require('../../src/setProxy.js')

module.exports = merge(common, {
  devServer: {
    //...
    proxy: { ...proxySetting }
  },
})

可以了!就这么简单!接下来安装我们最常用的请求发送库 axios :

npm install axios -S

src/app.tsx 中简单发个请求,就可以自己测试了,这里大家要找测试接口的话可以找下 github 的公用 api,这里我就直接蹭公司的了~

生产(prod)环境优化

这部分主要针对无论开发环境还是生产环境都需要的公共配置优化。

1. 抽离出 css 样式

抽离出单独的 chunk 文件的优势在上面“抽离公共代码”一节已经简单描述过,现在我们写的所有样式打包后都打进了 js 文件中,如果这样放任下去,该文件会变得越来越大,抽离出样式文件势在必行!

借助 mini-css-extract-plugin 进行 css 样式拆分,先安装它:

npm install mini-css-extract-plugin -D

webpack.common.js 文件(注意⚠️,是 common 文件)中增加和修改以下代码:

const MiniCssExtractPlugin = require('mini-css-extract-plugin')

const getCssLoaders = (importLoaders) => [
  isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
  // ....
]

module.exports = {
	plugins: [
    // 其它 plugin...
    !isDev && new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',
      chunkFilename: 'css/[name].[contenthash:8].css',
      ignoreOrder: false,
    }),
  ]
}

我们修改了 getCssLoaders 这个方法,原来无论在什么环境我们使用的都是 style-loader ,因为在开发环境我们不需要抽离,于是做了个判断,在生产环境下使用 MiniCssExtractPlugin.loader

我们随便写点样式,然后执行以下 npm run build ,再到 dist 目录下看看:

image.png
可以看到成功拆出来了样式 chunk 文件,享用了至尊级待遇!

2. 去除无用样式

我在样式文件中故意为某个不会用到的类名加了个样式:
image.png
结果我执行打包,找到这个分离出的样式文件点进去一看:
image.png
它默认还是保留这个样式了,这显然是无意义的代码,所以我们要想办法去除它,所幸有 purgecss-webpack-plugin 这个利器,让我们先安装它及路径查找利器 node-glob

npm install purgecss-webpack-plugin glob -D

然后在 webpack.prod.js 中增加以下代码:

const { resolve } = require('path')
const glob = require('glob')
const PurgeCSSPlugin = require('purgecss-webpack-plugin')
const { PROJECT_PATH } = require('../constants')

module.exports = merge(common, {
	// ...
  plugins: [
    new PurgeCSSPlugin({
      paths: glob.sync(`${resolve(PROJECT_PATH, './src')}/**/*.{tsx,scss,less,css}`, { nodir: true }),
    }),
  ],
})

简单解释下上面的配置:
glob 是用来查找文件路径的,我们同步找到 src 下面的后缀为 .tsx 、 .(sc|c|le)ss 的文件路径并以数组形式返给 paths ,然后该插件就会去解析每一个路径对应的文件,将无用样式去除; nodir 即去除文件夹的路径,加快处理速度。为了直观给大家看下路径数组,打印出来是这个样子:

[
  '/Users/RMBP/Desktop/react-ts-quick-starter/src/app.scss',
  '/Users/RMBP/Desktop/react-ts-quick-starter/src/app.tsx',
  '/Users/RMBP/Desktop/react-ts-quick-starter/src/components/ComputedOne/index.scss',
  '/Users/RMBP/Desktop/react-ts-quick-starter/src/components/ComputedOne/index.tsx',
  '/Users/RMBP/Desktop/react-ts-quick-starter/src/components/ComputedTwo/index.scss',
  '/Users/RMBP/Desktop/react-ts-quick-starter/src/components/ComputedTwo/index.tsx',
  '/Users/RMBP/Desktop/react-ts-quick-starter/src/index.tsx'
]

大家要注意⚠️:一定也要把引入样式的 tsx 文件的路径也给到,不然无法解析你没有哪个样式类名,自然也无法正确剔除无用样式了。

现在再看看我们打包出来的样式文件,已经没有了那个多余的代码,简直舒服!

3. 压缩 js 和 css 代码

在生产环境,压缩代码是必须要做的工作,其打包出的文件体积能减少一大半呢!

js 代码压缩

webpack4 中 js 代码压缩神器 terser-webpack-plugin 可谓是无人不知了吧?它支持对 ES6 语法的压缩,且在 mode 为 production 时默认开启,是的,webpack4 完全内置,不过我们为了能对它进行一些额外的配置,还是需要先安装它的:

npm install terser-webpack-plugin -D

webpack.common.js 文件中的 optimization 增加以下配置:

module.exports = {
	// other...
  externals: {//...},
  optimization: {
    minimize: !isDev,
    minimizer: [
      !isDev && new TerserPlugin({
        extractComments: false,
        terserOptions: {
          compress: { pure_funcs: ['console.log'] },
        }
      })
    ].filter(Boolean),
    splitChunks: {//...},
  },
}

首先增加了 minimize ,它可以指定压缩器,如果我们设为 true ,就默认使用 terser-webpack-plugin ,设为 false 即不压缩代码。接下来在 minimize 中判断如果是生产环境,就开启压缩。

  • extractComments 设为 false 意味着去除所有注释,除了有特殊标记的注释,比如 @preserve 标记,后面我们会利另一个插件来生成我们的自定义注释。
  • pure_funcs 可以设置我们想要去除的函数,比如我就将代码中所有 console.log 去除。

css 代码压缩

同样也是耳熟能详的 css 压缩插件 optimize-css-assets-webpack-plugin ,直接安装它:

npm install optimize-css-assets-webpack-plugin -D

在我们上面配置过的 minimizer 新增一段代码即可:

module.exports = {
  optimization: {
    minimizer: [
      // terser
      !isDev && new OptimizeCssAssetsPlugin()
    ].filter(Boolean),
  },
}

4. 添加包注释

上面我们配置 terser 时说过,打包时会把代码中所有注释去除,除了一些有特殊标记的比如 @preserve 这种就会保留。我们希望别人在使用我们开发的包时,可以看到我们自己写好的声明注释(比如 react 就有),就可以使用 webpack 内置的 BannerPlugin ,无需安装!

webpack.prod.js 文件中增加以下代码,并写入自己想要的声明注释即可:

const webpack = require('webpack')

module.exports = merge(common, {
  plugins: [
    // ...
    new webpack.BannerPlugin({
      raw: true,
      banner: '/** @preserve Powered by react-ts-quick-starter (https://github.com/vortesnail/react-ts-quick-starter) */',
    }),
  ],
})

这时候打个包去 dist 目录下看看出口文件:

image.png

5. tree-shaking

tree-shaking 是 webpack 内置的打包代码优化神器,在生产环境下,即 mode 设置为 production 时,打包后会将通过 ES6 语法 import 引入的未使用的代码去除。下面我们简单举个例子:

src/utils/math.ts 中写入以下代码:

export function add(a: number, b: number) {
  console.info('I am add func')
  return a + b
}

export function minus(a: number, b: number) {
  console.info('I am minus func')
  return a - b
}

回到我们的 src/app.tsx 中,清除以前的内容,写入以下代码:

import React from 'react'
import { add, minus } from 'Utils/math'

function App() {
  return <div className='app'>{add(5, 6)}</div>
}

export default App

可以看到,我们同时引入来 add 和 minus 方法,但是实际使用时只使用了 add 方法,这时候我们 build 一下,打开打包后的文件搜索 console.info('I am minus func') 是搜不到的,但却搜到了 console.info('I am add func') 意味着这个方法因为没有被使用导致被删除,这就是 tree-shaking 的作用!

在我开发的项目时,我不会去 package.json 中配置 sideEffects: false ,因为我写的模块我能保证没有副作用。

这里大家有必要回忆一下,在 .babelrc 中我们在 @babel/preset-env 下配置了 module: false ,目的在于不要将 import 和 export 关键字处理成 commonJS 的模块导出引入方式,比如 require ,这样的话才能支持 tree-shaking,因为我们上面说了,在 ES6 模块导入方式下才会有效。

6. 打包分析

有时候我们想知道打出的包都有哪些,具体多大,只需借助 webpack-bundle-analyzer 即可,我们安装它:

npm install webpack-bundle-analyzer -D

打开 webpack.prod.js 增加以下 plugin 即可:

const webpack = require('webpack')

module.exports = merge(common, {
  plugins: [
    // ...
    new BundleAnalyzerPlugin({
      analyzerMode: 'server',					// 开一个本地服务查看报告
      analyzerHost: '127.0.0.1',			// host 设置
      analyzerPort: 8888,							// 端口号设置
    }),
  ],
})

这时候我们 npm run build 完成后,就会打开默认浏览器,出现一下 bundle 分析页面:

image.png
尽情想用吧!~

前半部分结语

大家跟着读到这里,或者跟着做到这里,相信大家感觉一定不虚此行了吧?现在完成的配置已经是可以进行正常的开发了,至于项目中经常用到的 react-router-dom 、 react-redux 、 mobx 等更多的库大家就按照正常开发时安装使用就可以。

接下来后半部分我想以两个案例讲解使用现用我们搭出来的架子开发 React 组件和常规工具并发布至 npm 的全流程,内容分别如下:

  • 利用 rollup 和 tsc 打包工具包并发布至 npm 全流程
  • 利用 rollup 和 tsc 打包开发的 react 组件并发布至 npm 全流程

这一部分能讲的东西也是满多的,我会新起另一篇文章讲解,大家敬请期待吧!这篇文章我前后花了大概一个月时间,都是利用工作之余时间写的,希望大家能给予一点小小的鼓励,只需要给我的github/blog一个小小的 star✨ 即可让我元气满满!球球了🙏!!!

image.png

@Failymiss
Copy link

👍,受益匪浅

@jingjing20
Copy link

好文啊!666

@vortesnail
Copy link
Owner Author

好文啊!666

谢谢哦~

@oceanxy
Copy link

oceanxy commented Nov 9, 2020

后半部分何时发呢

@vortesnail
Copy link
Owner Author

感谢作者能提供那么好的文章,看着跟着做了一遍受益匪浅!
不知道能否做转载?我发现文中有一些地方有些小错误,想在自己博客上转载,并且把错误的地方做的小修改。感谢!

随意转载,标明来源即可

@vortesnail
Copy link
Owner Author

有个问题折腾我几个小时就是在配置了webpack的alias还有tsconfig的paths路径后,eslint里貌似也要进行相应的配置,否则在git commit时就会出现import/no-unresolved错误,具体配置可以参考这里:https://my.oschina.net/someok/blog/2050469

我这里没有问题,可能是版本问题。
本身这篇文章也是提供个配置流程和思路,具体过程中出现的问题就相应去解决即可~

@Mustang-Galaxy
Copy link

小姐姐,prettier这里并没有自动修复 var => const 的错误啊

@mpv945
Copy link

mpv945 commented Jan 29, 2021

可以来一版react17 + webpack5 + typescript吗

@vortesnail
Copy link
Owner Author

可以来一版react17 + webpack5 + typescript吗

master 分支已经是 react17 + webpack5 + typescript 版本,README.md 已更新了配置差异,欢迎查看。

@FutureXZC
Copy link

妙啊!!!赞!

@guoyianlin
Copy link

受益匪浅,不虚此行!作者辛苦啦!

@vortesnail
Copy link
Owner Author

受益匪浅,不虚此行!作者辛苦啦!

谢谢支持~对你有所帮助我很高兴

@bey1995
Copy link

bey1995 commented May 12, 2021

感谢,受益匪浅

@willwei954
Copy link

大佬真的🐂 🍺, 爱你

@willwei954
Copy link

不过 好像file.d.ts是不是应该放在根目录而不是src/typings/file.d.ts?

@Pzx1997
Copy link

Pzx1997 commented Jun 2, 2021

很感谢作者提供的文章 照着敲了一边受益匪浅
配置 husky和commitlint 的时候发现 commit 不能检测 =-=
查阅资料后发现 因为更新了 6.x 的版本 所以以前的配置方法不生效了
踩坑 可以参考这篇文档解决 最后再次感觉作者的文章 太nice了
https://blog.csdn.net/qq_21567385/article/details/116429214?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-5.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-5.control

@vortesnail
Copy link
Owner Author

很感谢作者提供的文章 照着敲了一边受益匪浅
配置 husky和commitlint 的时候发现 commit 不能检测 =-=
查阅资料后发现 因为更新了 6.x 的版本 所以以前的配置方法不生效了
踩坑 可以参考这篇文档解决 最后再次感觉作者的文章 太nice了
https://blog.csdn.net/qq_21567385/article/details/116429214?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-5.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-5.control

嘿嘿,这个我搭 webpack5 版本的也是遇到了,不过我还是选择使用旧版的,因为 6.x 好像开源协议有所变动。

@Pzx1997
Copy link

Pzx1997 commented Jun 3, 2021

image
plungins 应该修改为 plugins

@vortesnail
Copy link
Owner Author

image
plungins 应该修改为 plugins

嗯嗯,打错字了,问题不大。

@Pzx1997
Copy link

Pzx1997 commented Jun 3, 2021

image

这种写法 好像会报错 =-= 提示智能是 obj 或者 function

@vortesnail
Copy link
Owner Author

image

这种写法 好像会报错 =-= 提示智能是 obj 或者 function

第一:确定版本是否一致,不一致要自己根据版本情况修改,因为可能 api 不一样了。 第二:关闭 ts 对于该文件的检查

@jingjing20
Copy link

jingjing20 commented Jun 8, 2021

有个问题想请教下楼主,在 ESLint、Stylelint 和 Prettier 的冲突 这部分内容我是跟着文章操作的,但是在缩进上还是有问题。试了新版的解决方法也没有用。是我理解有问题吗?
我看你的代码是把 eslint 缩进的配置注释了,是这样操作吗?

@wkylin
Copy link

wkylin commented Jul 20, 2021

受益良多....
本人也搭建了一个项目:https://github.com/wkylin/promotion-web, 未升级到TS,以后打算参考作者的项目也升级一下。
多多交流...

@yoneyy
Copy link

yoneyy commented Sep 5, 2021

rollup 文章是在那个位置呀?

@fishcoderman
Copy link

总结很全面👍🏻

@whenTheMorningDark
Copy link

很强

@narakuR
Copy link

narakuR commented Aug 24, 2022

👍👍👍

@xiazhaohui
Copy link

刚看了一段,先赞为敬👍👍👍

@lomolomo2
Copy link

多谢。我按照这个文章,搭建了react18+ 的环境! https://github.com/lomolomo2/WebPhotoViewer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests