前言:从6月初的实习到现在也有一段时间了,这段时间学习了很多,有个比较困扰我的点就是公司的项目基础模版的搭建,都有现成的解决方案,而缺少0-1的宏观了解,而市面上主流creat-react-app
的效果又不尽人意,想要有自己的扩展eject之后感觉Webpack
水平不够限制又太多,难以学习,于是抽空之余阅读众多博主文章,模仿学习着从0-1,建出一个完整的属于自己的TypeScript+React
开发环境。
我的开发环境:mac+vscode+node(v16.16.0)
仓库地址:https://github.com/hyh-op/react_ts_hyh
项目中常用的配置文件
1.package.json的配置
大多的项目都是从:新建项目文件 => 初始化项目 => 配置文件 package.json开始,这里也不例外。
// 新建项目文件夹
mkdir react_ts_hyh
// 切换工作路径到项目文件夹
cd $_
// 初始化项目,生成项目配置文件
yarn init -y
mkdir
是在当前路径下新建文件, react_ts_hyh
是对应的文件名字,cd
是进入,$_
表示上一条命令的最后参数,yarn
是包管理工具,主流的有npm,yarn,pnpm,之间各有优劣,这里我习惯性使用yarn。
然后通过修改默认的项目配置文件后,内容如下:
{
"name": "react_ts_hyh",
"version": "1.0.0",
"main": "index.js",
"scripts": {},
"keywords": [
"react+ts"
],
"author": {
"name": "hyh",
"url": "http://luobiop.com/"
},
"license": "MIT"
}
- scripts :把默认生成的test删除,后续自己配置,
- keywords:项目的关键字,当项目 publish 到 npm 后,便于搜索,
- author:项目的作者信息
- license:项目的协议(一般没有特别的需求,都会选择较为宽松的 MIT 协议)
2. git 初始化
// 初始化本地仓库
git init
之后我们的代码推到云端仓库的时候,总是需要忽略一些文件的,这时候.gitignore
就派上用场了,首先肯定要添加的是node_modules
,我们安装的第三方依赖包,里面成千上万的文件都是应该由开发者自己下载的,推到远端会给服务器造成很大压力,完全没必要的事情,常见添加的有build,dist,package-lock.json等,我们可以通过vscode的gitignore
插件来生成.gitignore
文件的常用配置。
安装好插件后,ctrl + shift + p 唤出 VSCode 的命令面板,输入 Add gitignore 命令,自动生成一些具体的配置规则。
3. 切换源镜像
由于东方的神秘力量,国内的github
或者npm
的连接速度都很感人,一般的通用做法是切换到国内的taobao源镜像,提升安装速度。而切换需要依赖包源管理器通常是nrm
。
但是!这仅仅是在本地的源镜像配置,如果有一天你的项目被人clone,那个同学看了眼package.json的启动项没啥问题,README.md文档里也没有相应的注释,那么正常来说就是安装依赖,然后没有配置的话还是默认国外的源安装,速度上可能有较大的block,从便利性上应创建.npmrc
配置文件(yarn也会遵循该配置),在上帝视角为后续开发者提供保障。
// 根目录下,创建.npmrc文件
touch .npmrc
// 该文件配置内容
registery https://registery.npm.taobao.org/
- README.md 就是项目说明书,由开发者提供一份启动指南也好,注意事项也罢,总之就是方便后续同学使用,避免踩坑。
统一代码风格和规范配置
这一步的代码风格和规范配置
是非常重要的,特别是在团队协作和大型项目中,随着项目迭代和人员流动,代码上手和维护难度会随之上升,而统一的代码风格和规范配置能在一定程度上减缓难度。
与此著名的开源项目是Prettier
。
- Prettier 插件是用于替换 VSCode 默认的 Formatter,开发者可以在编码的期间使用 VSCode 快捷键 alt + shift + f 触发格式化操作。
通过读取 .prettierrc 配置文件,或者 package.json 项目配置文件中 prettier 属性里配置的格式化规则来设置其格式化风格。
1.Prettier 配置
在我们的项目中执行以下命令安装 Prettier 命令行工具
yarn add -D prettier
- -D是--save-dev简写,表示为以本地开发依赖安装
安装完后在根目录新建.prettierrc
配置文件,可以参考如下配置,感兴趣的话也可以自行查阅更多相关配置:{ "trailingComma": "es5", "tabWidth": 2, "semi": true, "singleQuote": true, "printWidth": 100, "arrowParens": "always", "useTabs": false }
- trailingComma:默认“es5”
- tabWidth:指定每个缩进级别的空格数,默认 2
- semi:句尾添加分号。 默认 true
- singleQuote:使用单引号而不是双引号,默认false
- printWidth: 超过多少换行, 默认 80
- arrowParens :"always"- 如果超过宽度,则换行
- useTabs : 使用制表符而不是空格缩进, 默认 false
官网:https://www.prettier.cn/docs/options.html
此外,我们在vscode安装对应的Prettier - Code formatter 插件:
安装完后,我们在项目的根目录新建一个.vscode
文件夹,然后在此文件夹下再新建一个settings.json
文件,该文件优先级会高于 VSCode 全局的 settings.json,这样当别人 clone 了项目进行协同开发,也不会因为全局的 settings.json 的配置不同而导致 prettier 或后面会讲到的 eslint 或 stylelint 失效,接下来我们暂时添加以下内容到文件中:{ // 指定哪些文件不参与搜索 "search.exclude": { "**/node_modules": true, "dist": true, "yarn.lock": true }, // 指定哪些文件不被VSCode监听,预防启动VSCode时扫描的文件太多,导致CPU占用过高 "files.watcherExclude": { "**/.git/objects/**": true, "**/.git/subtree-cache/**": true, "**/node_modules/*/**": true, "**/dist/**": true }, // 保存时,自动进行一次代码风格格式化 "editor.formatOnSave": true, // 配置VSCode使用Prettier插件替代默认的Formatter "[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" }, "[jsonc]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[html]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[css]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[less]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[scss]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[yaml]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[markdown]": { "editor.defaultFormatter": "esbenp.prettier-vscode" } }
同时把细节做好,
.vscode
文件夹下新建extensions.json
文件,在文件中配置一些用于向别人推荐的 VSCode 插件,如下配置:{ "recommendations": ["esbenp.prettier-vscode"] }
2.ESLint 配置
Prettier 是为了
解决代码风格格式化
的工具,从解决的层面来说前者是解决通用代码风格,后者是解决具体语言代码风格
。而 ESLint 则是用作代码风格、代码质量的规范化工具,虽然各种 lint 工具有一定的格式化代码的能力。但是其主要功能并不是负责代码格式化,所以代码格式化的工作就交由前面两个专门进行代码风格格式化的工具进行处理了。
在项目里安装所需依赖包yarn add -D eslint
- -D 有些包全局本地都可以,但没办法确保别人在开发这个项目的时候也全局安装了依赖包,以本地开发以来或生产依赖安装还能确保大家使用的版本一致,避免不必要的麻烦。
安装成功后,执行一下命令,之后进行配置
npx eslint --init
- npx npm 5.2 自带的一个命令,其中的 x 就是和文件类型描述符的那个 x 一样表示 execute 执行的意思。该命令的意思是,如果本地安装了 eslint 就会使用本地的 eslint,否则就会去全局中查找,若全局中也没有找到,就在临时目录下载 eslint,执行完毕后删除
安装的时候是交互式配置生成,执行以下命令
- 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?
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?
选择 默认第一个风格就行,都是社区里总结出来的最佳实践
- What format do you want your config file to be in?
选择 JavaScript,即生成的配置文件是 js 文件,配置更加灵活,虽然灵活。但是 js 格式没办法使用 VSCode 提供的 JSON Validate 功能
Would you like to install them now with npm?
选择 Yes
安装后,项目根目录应该会长出了新的文件 .eslintrc.js,这便是 eslint 的配置文件了。
为了让 eslint-plugin-import
能够正确解析 ts tsx json 等后缀名。我们还需要指定允许的后缀名,添加 settings
属性,加入以下配置:
settings: {
'import/reslover': {
node: {
// 指定eslint-plugin-import解析的后缀名,出现频率高的文件类型放在前面
extensions: ['.ts', '.tsx', '.js', '.json'],
},
},
}
因为 eslint-plugin-import
与 TypeScript 搭配存在 Bug,需要添加一条很重要的 rule
,不然在 .ts 和 .tsx 模块文件中引入另一个模块会报错,添加以下规则到 rules 即可:
/ eslint-plugin-import和typescript搭配不能正确处理后缀名bug
rules: {
'import/extensions': [
ERROR,
'ignorePackage',
{
ts: 'never',
tsx: 'never',
json: 'never',
js: 'never',
},
],
}
接下来安装 3 个社区中比较优秀的 eslint 插件:
- eslint-plugin-promise 可以帮助你把 Promise 语法写成最佳实践
- eslint-plugin-unicorn 提供了循环依赖检测,文件名大小写风格约束等非常使用的规则集合
- eslint-plugin-eslint-comments 用于检测出无用的 eslint-disable 注释
执行以下命令安装:
yarn add -D eslint-plugin-promise eslint-plugin-unicorn eslint-plugin-eslint-comments
修改后,配置文件如下:
const OFF = 0;
const WARN = 1;
const ERROR = 2;
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
"plugin:eslint-comments/recommended",
"plugin:react/recommended",
"plugin:unicorn/recommended",
"plugin:promise/recommended",
"plugin:@typescript-eslint/recommended",
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 12,
sourceType: "module",
},
settings: {
"import/resolver": {
node: {
// 指定eslint-plugin-import解析的后缀名,出现频率高的文件类型放在前面
extensions: [".ts", ".tsx", ".js", ".json"],
},
},
},
plugins: ["react", "unicorn", "promise", "@typescript-eslint"],
rules: {
// eslint-plugin-import和typescript搭配不能正确处理后缀名bug
"import/extensions": [
ERROR,
"ignorePackages",
{
ts: "never",
tsx: "never",
json: "never",
js: "never",
},
],
},
};
此时新建一个hello.ts
文件,在文件内敲入以下代码:
var add = (a, b) => {
console.log(a + b);
return a + b;
};
export default add;
此时是没有任何错误提示的,很明显代码里使用了违反规则的变量定义符号 var,理论上来说 eslint 应该要报错,但没有报错。我们可以通过 OUTPUT 窗口得知,其实是因为 @typescript-eslint/eslint-plugin
这个插件需要我们安装 TypeScript 依赖,为了 eslint 插件现在能够正常工作,我们就先执行以下命令安装 TypeScript 依赖:
yarn add -D typescipt
之后再看hello.ts
文件的内容,就有一堆红色波浪线报错了!
此外,这种错误其实是支持自动修复的,需要安装ESLint
插件
任何到之前创建的.vscode/settings.json
配置文件中添加以下配置信息
{
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
// 代替VSCode的TS语法只能提示
"typescript.tsdk": "./node_modules/typescript/lib",
// 保存时,自动进行一次eslint代码修复
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
这时候ts文件保存时,就会通知eslint
根据.eslintrc.js
配置文件修复
但有一个问题值得我们思考,是不是所有的代码不符合eslint规范的就应该修复呢?
答案是否定的,典型的想打包产物,依赖包等就不该修复,这就需要我们创建两个配置文件.eslintignore
和 .prettierignore
,一般会保持这两个配置文件的内容一致,内容如下
/node_modules
/build
/dist
后续可以根据自己的代码情况忽略更多文件
3.StyleLint 配置
经过上述操作,我们的js+ts代码已经有良好的代码风格和规范了,但项目里的代码可远远不止这些类型,样式代码的风格统一也是很重要滴。
同理,先安装以来
yarn add -D stylelint stylelint-config-standard
然后再项目根目录新建 .stylelintrc 配置文件,输入
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,
},
ignoreFiles: ["node_modules/**/*"],
};
- extend:和 eslint 配置里的类似,都是扩展,使用 stylelint 已经预设好的一些规则
- rules:就是具体的规则,如果对默认的规则不满意,可以自己做具体的修改
- ignoreFiles:与 eslint 需要单独创建 ignore 配置文件不同,stylelint 配置文件里就支持忽略的属性字段。这里我们先添加 node_modules 和 build,后面视情况再做添加,和
eslint
一样,stylelint
也有自动修复功能,需要对应的stylelint
插件
安装完后,我们对先前的 .vscode/settings.json 配置文件,添加
{
// 保存时,自动进行一次代码风格格式化
"editor.formatOnSave": true,
// 保存时,自动进行一次eslint和styleline代码修复
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.fixAll.stylelint": true
},
// 使用stylelint自身的校验即可
"css.validate": false,
"less.validate": false,
"scss.validate": false
}
这时候可以自己创建一个.less
文件校验一下,可以看到里面是有错误提示的,此外还可以再下载社区里一些比较优秀的 stylelint
扩展和插件
先安装依赖
yarn add -D stylelint-order stylelint-config-rational-order stylelint-declaration-block-no-ignored-properties
- stylelint-order : 强制你按照某个顺序编写css, 例如先写定位,再写盒模型,再写内容区样式,最后写CSS3 相关属性。 这样可以极大的保证我们代码的可读性
- stylelint-config-rational-order : css属性顺序的规则,配合stylelint-order使用
- stylelint-declaration-block-no-ignored-properties : 用于提示我们写出的矛盾样式,避免我们犯一些书写样式时的低级错误
安装完之后,把 .stylelintrc.js
配置文件修改成
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,
},
ignoreFiles: ["node_modules/**/*"],
};
这样一来, stylelint 的相关配置就暂告一段落啦!其实以上关于 stylelint 所配置的信息都是参考 ant-design 的 stylelint 配置文件来进行配置的。但是,对于具体的规则以及插件大家都可以在其官网浏览查找,然后添加自己想要的规则定义。
4. 命令行中lint的配置
在之前的配置里,都是以 VSCode 插件的形式来使用到 lint,也提到过 lint 可以使用 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,scss,css}"
}
}
- scripts : 定义脚本命令
推荐阮一峰老师的博客:https://www.ruanyifeng.com/blog/2016/10/npm_scripts.html
在控制台执行yarn lint-eslint
的时候,项目就会以.eslintrc.js
配置文件为规范,到src文件夹下指定后缀的代码检测,yarn lint-stylelint
同理,yarn lint
则是按顺序合并执行前两个指令。
5. lint 与 Prettier 的冲突
虽然 lint 工具有一定的格式化代码的能力,但是其主要功能并不是负责格式化代码,所以格式化代码的工作就交由prettier专门进行代码风格格式化的工具进行处理了。所以有时候 eslint 和 stylelint 的格式化规则会与 prettier 的格式化规则产生冲突,比如在配置文件 .eslintrc.js 中属性 extends 中的配置设置了缩进为 4,但是在配置文件 .prettierrc 中设置了缩进为 2,那就会出现保存时,先是 eslint 自定修复缩进为 4,然后 prettier 又不开心的把缩进改为 2,然后 eslint 也不开心,直接报错!
针对 eslint 和 stylelint 都有相应的插件支持解决该类问题,其原理基本都是禁用其与 prettier 发生冲突的规则,从而达到解决冲突的效果。
解决 ESLint 与 Prettier 的冲突
安装依赖
yarn add -D eslint-config-prettier
安装完之后,再到 .eslintrc.js
配置文件的 extends
属性中,添加以下配置
{
extends: [
// ...
'prettier',
],
}
新增的配置信息需要放在原来 extends
属性配置信息的最后面
,这样才能覆盖掉冲突的规则
解决 Stylelint 与 Prettier 的冲突
安装依赖
yarn add -D stylelint-config-prettier
安装完之后,再到 .stylelintrc .js
配置文件的 extends
属性中,添加以下配置
{
extends: [
// ...
'stylelint-config-prettier',
],
}
新增的配置信息需要放在原来 extends
属性配置信息的最后面
,这样才能覆盖掉冲突的规则
6. lint-staged配置
本地的代码保存格式化规范化已经完成,但是有一个问题,coommit代码到暂存区的时候,之后再git push 到远端,而暂存区的代码可能有漏网之鱼,而lint-staged
可以配合eslint
, stylelint
, prettier
一起使用格式化暂存区,而为了让每次git commit执行后,代码提交到暂存区前都自动格式化,我们需要给git命令配置钩子,依靠husky
可以完成,先安装依赖
yarn add -D husky lint-staged
- husky 安装 husky 时 husky 会在 .git\hooks 文件夹里配置 git 的钩子脚本,如果还未初始化 git 本地仓库就没有 .git\hooks 文件夹,就会导致 husky 配置钩子脚本失败,所以要
git init
之后再安装,已经安装的可以先卸载初始化。
之后在package.json
配置文件,添加{ "husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "*.{ts,tsx,js}": ["eslint -c .eslintrc.js"], "*.{less,scss,css}": ["stylelint --config .stylelintrc.js"], "*.{ts,tsx,js,json,html,yml,less,scss,css,md}": ["prettier --write"] } }
- pre-commit 执行钩子,在
git commit
执行后,提交前执行,执行内容对应lint-staged
- lint-staged 自定义的
lint
内容,- 先对 git stage 区中后缀为 .ts .tsx .js 的文件进行 eslint 规范校验
- 对后缀为 .less .scss .css 的文件进行 stylelint 校验,
--config
的作用是指定配置文件 - 对对应后缀的文件,使用
--write
来格式化
此外,在大型项目合作中,后期协作以及 bug 排查十分依赖提交日志,关于git的代码提交规范也有一些相关依赖,方便开发者或者用户追踪项目的开发信息和功能特性。推荐阮一峰老师的博客:https://www.ruanyifeng.com/blog/2016/01/commit_message_change_log.html
Webpack4 基本配置
前面的内容按照流程实践一遍,应该对项目整体流程有了大概的了解,但是!上面只是基本配置,后面的 Webpack
的配置才是重中之重~
使用版本
yarn add -D webpack@4 webpack-cli@3
- webpack 前端项目常用的项目构建打包工具
- webpack-cli Webpack 的命令行工具,用于在命令行中使用 Webpack
下载完之后,在项目根目录下创建一个新的文件夹 scripts
,在文件夹内再创建一个文件夹 config
,在 config 文件夹里头创建一个 .js 文件 webpack.common.js
目的:分为开发构建环境和生产构建环境,不同构建环境的Webpack
脚本配置有重合也有差异,相同的部分我们一般都放在所创建的 webpack.common.js
,不同的部分就另外分别创建两个环境的构建打包配置文件。然后后续会把这两个环境的配置文件和通用配置文件中的配置信息通过 webapck-merge
依赖包进行合并处理。
1. input, output的配置
input(入口)和 output(出口)是 Webpack 的核心概念之一,顾名思义就大概能猜到他们是用来干嘛的,入口就是给 Webpack 指定一个或多个构建打包的入口及其对应的出口,Webpack 经过一系列的构建操作打包操作后就把产出物输出到对应的出口中。(注:可以多个出口 js 文件对应一个 html 模板,也可以多个出口 js 文件对应多个 html 模板,需要通过 Webpack 的 html-webpack-plugin
插件配合处理)
接下来在刚创建的通用构建打包配置文件 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"),
},
};
- require : 标准的
Node.js
的CommonJS
模块,通过require
来引入其他模块,通过module.exports
来导出模块,由 Webpack 根据对象定义的属性进行解析。 - entry : 定义了入口文件路径,其值支持字符串、数组、对象类型,对象值类型支持定义多入口。而上面以
对象
类型作为其值,且属性名app
可用于出口文件名字 - output : 定义了构建打包之后的出口文件名及其所在路径
- __dirname : Node.js中总是指向被执行 js 文件的绝对路径
- path.resolve() : 路径解析方式,各个参数的作用可以参考:https://www.jianshu.com/p/3a713442b70b
这段代码的意思就是告诉 Webpack,入口文件是项目根目录下的 src
文件夹里的 app.js
代码文件。构建打包产出的文件位置为项目根目录下的 dist
文件夹,其中 filename
属性值为 js/[name].[hash:8].js
就表示会在 dist
文件夹内创建一个新的 js
文件夹,并把产出的文件以入口属性名以及带有八位的 hash
值作为文件名输出到 js
文件夹中。
配置完之后我们可以测试一下这个配置是否生效,于是在根目录创建 src
文件夹,并创建 app.js
代码文件,其内容为:
const root = document.querySelector("#app");
root.innerHTML = "hello world!";
然后在配置文件 package.json
中的 scripts
属性,添加一条 Webpack 的打包命令
{
"scripts": {
// ...
"build": "webpack --config ./scripts/config/webpack.common.js"
}
}
测试命令和配置是否生效
yarn build
执行完过一会,项目根目录下会多出一个 dist
文件夹,里面的文件和我们在 Webpack 构建打包配置文件中,所配置的预期是一致。
2.公共变量文件配置
上面简单的 Webpack
配置中,我们发现有两个表示路径的代码语句
path.resolve(__dirname, "../../src/app.js");
path.resolve(__dirname, "../../dist");
这里的路径拼接写法是否有些冗余,__dirname值总是指向被执行 js 文件的绝对路径,那么我们要不断的../../回到根目录,如果层级再加深,更复杂,那么是否可以把跟路径提炼出来为公共变量,同时也简化了路径写法
我们在 scripts
文件夹下创建 constants.js
代码文件,用于存放我们的公共常量,在里面定义我们的常量
const path = require("path");
const { ModuleResolutionKind } = require("typescript");
const PROJECT_PATH = path.resolve(__dirname, "../");
const PROJECT_DIRNAME = path.parse(PROJECT_PATH).name;
module.exports = {
PROJECT_DIRNAME,
PROJECT_PATH,
};
- PROJECT_PATH 项目的根目录
- PROJECT_DIRNAME 项目目录名称
然后我们修改 webpack.common.js 配置文件,修改完如下
const path = require("path");
const { PROJECT_PATH } = require("../constants");
module.exports = {
entry: {
app: path.resolve(PROJECT_PATH, "./src/app.js"),
},
output: {
filename: "js/[name].[hash:8].js",
path: path.resolve(PROJECT_PATH, "./dist"),
},
};
整个看起来舒服多了,之后在yarn build
验证一下,芜湖,成功了
3. 开发和生产构建环境
我们在生产构建环境中并不需要启用 dev server
的功能或者构建打包出代码的 .map
文件。又或者说在开发构建环境中,我们并不需要太过苛刻的代码或者文件压缩,此时就需要为两个环境做相应配置。
但分别配置也不是重写,而是提炼不同的部分,所以我们把基础的配置写在 webpack.common.js
配置文件中,然后合并配置的问题就交给 webpack-merge
依赖包解决。
安装依赖
yarn add -D webpack-merge
然后在scripts/config
文件夹下创建配置文件 webpack.dev.js
作为开发构建的配置文件,且内容为
const { merge } = require("webpack-merge");
const common = require("./webpack.common");
module.exports = merge(common, {
mode: "development",
});
- mode : Webpack 默认地为我们设置了 mode: production,所以我们之前打包后的 js 代码文件里的内容都无法直视。因为在 production 模式下,Webpack 会使用 production 默认的 Webapck 配置去丑化、压缩代码等等。
在 scripts/config
文件夹创建配置文件 webpack.prod.js
作为生产配置文件,且内容为
const { merge } = require("webpack-merge");
const common = require("./webpack.common");
module.exports = merge(common, {
mode: "production",
});
那么随之而来产生的问题便是:你怎么知道这是开发环境还是生产环境?而痛点是不同操作系统设置环境变量的方式都各不相同,所以这就造成了一些麻烦
- Mac 中需要通过
export NODE_ENV=development
来设置环境变量 - Windows 中需要通过
set NODE_ENV=development
来设置环境变量
但我们不必辛苦的重复造轮子,这些早已有现成的解决方案,cross-env
是一个可跨平台设置和使用环境变量的 Node.js
命令行依赖包工具,所以有了这个利器,我们就无需考虑操作系统的差异性,统一使用他提供的 api 来进行环境变量的设置啦
安装依赖
yarn add -D cross-env
然后到 package.json
配置文件中的 scripts
属性里添加以下代码
{
"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"
修改 scripts/constants.js
文件,增加一个用于环境判断的布尔变量 ISDEV
const ISDEV = process.env.NODE_ENV !== "production";
module.exports = {
// ...
ISDEV,
};
现在有了这个环境的判断变量,我们可以用他来做点事情啦!
还记得先前在公共配置中,我们给出口脚本文件的名字生成规则中配置了 [name].[hash\:8].js。那为什么要配置上 [hash\:8] 呢?我们可以想一下,如果不这么配置,当用户在第一次访问页面时,浏览器根据缓存策略,缓存下了名字为 app.js 的出口脚本文件。然后我们进行了代码修改后,再次使用 Webpack 构建打包,并且发布部署。此时,用户再次访问页面时,浏览器发现本地存在 app.js 脚本文件的缓存,并且该缓存还在有效期内,就直接使用了本地缓存的 app.js 脚本文件。这样就会造成用户无法及时获得脚本文件的更新,最终可能导致用户页面出现异常。
然而加上了[hash\:8]之后就不同了,当用户在第一次访问页面时,浏览器根据缓存策略,缓存下了名字为 app.1a2b3c4d.js 的出口脚本文件。然后当我们进行了代码修改后,再次使用 Webpack 构建打包,并且发布部署。新的出口脚本文件名就会根据 hash 运算后就进行更新,如 app.5e6f7g8h.js。那么用户再次访问页面时,由于其浏览器只缓存过 app.1a2b3c4d.js 并没有缓存过新的 app.5e6f7g8h.js 脚本出口文件,所以浏览器就会向服务器获取该新的出口脚本文件并再次缓存。
不过,这个 hash 值在开发构建环境中并不需要,所以我们修改 webpack.common.js
配置文件
- const { PROJECT_PATH } = require('../constants');
+ const { PROJECT_PATH, ISDEV } = require('../constants');
module.exports = {
output: {
- filename: 'js/[name].[hash:8].js',
+ filename: `js/[name]${ISDEV ? '' : '.[hash:8]'}.js`,
path: path.resolve(PROJECT_PATH, './dist'),
},
};
推荐缓存协议的文章:https://zhuanlan.zhihu.com/p/60950750
4.本地服务实时查看页面
在过往的项目中,我们经常看到启动之后会自动打开一个localhost的本地服务展示页面信息,而这是怎么做的呢?
借助 webpack-dev-server
和 html-webpack-plugin
这两个依赖包就能实现,执行以下命令安装
yarn add -D webpack-dev-server@3 html-webpack-plugin
- webpack-dev-server@3 : 版本与先前安装的
Webpack CLI
保持一致,可以在本地开启一个 http 服务,可以指定端口和开启热更新功能。 - html-webpack-plugin : 每一个页面一定是要有 html 文件的,而这个插件可以帮助我们把构建打包产出的出口脚本文件自动的引入 html 模板文件中,并把经过处理的 html 模板文件输出到出口文件夹中(毕竟我们不可能每次构建打包都自己把出口脚本文件手动引入 html 模板文件中)
然后,我们在根目录创建一个 public
文件夹,里面是用于存放一些公共的静态资源文件,现在我们在里面创建一个 index.html
的 html 模板文件,内容如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Webapck TypeScript React Practice</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
而html-webpack-plugin
在开发和生产环境我们都需要配置,所以我们打开 webpack.common.js
配置文件,增加以下内容
// ...
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(PROJECT_PATH, "./public/index.html"),
filename: "index.html",
cache: false, // 防止之后使用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 模板文件,并且针对生产构建环境,还对该 html 文件进行了代码压缩、去除注释、去除空格等配置。
- Plugin : Webpack 的核心功能之一,他丰富了 Webpack 本身,针对的是 Loader 处理结束后,Webpack 打包的整个过程,他并不直接操作文件,而是基于事件机制工作。他会监听 Webpack 打包过程中的某些节点,执行较为宏观的任务。
然后,我们到 webpack.dev.js
配置文件中增加本地服务的配置, 如下
...
+const { SERVER_HOST, SERVER_PORT } = require("../constants");
module.exports = merge(common, {
...
+ devServer: {
+ host: SERVER_HOST, // 指定host,不设置的话默认为localhost
+ port: SERVER_PORT, // 指定端口,不设置的话默认为8080
+ stats: "errors-only", // 重点仅打印error
+ compress: true, // 是否启用gzip压缩
+ open: true, // 打开默认浏览器
+ hot: true, // 热更新
+ },
});
在上面使用到了 scripts/constants.js
中的两个常量 SERVER_HOST
和 SERVER_PORT
,所以我们需要在 constants.js
里定义他们
// ...
const SERVER_HOST = "127.0.0.1";
const SERVER_PORT = 5000; // 默认3000,我本地服务较多,另起了端口不与本地服务冲突
module.exports = {
// ...
SERVER_HOST,
SERVER_PORT,
};
确认一下,我们先前编写的src/app.js
中的代码
const root = document.querySelector("#app");
root.innerHTML = "hello world!";
把 html 模板中 id 为 app 的 div 标签内的内容替换为 hello world! 字符串,执行 yarn start
自动打开浏览器,并且浏览器还打开了一个页面,而这个页面就是我们期待的那个页面。屏幕中出现了 hello world!,我们查看控制台,发现 html 文件真的就自动引入了我们使用 Webpack 构建打包后的出口文件 app.js
5.devtool 配置
devtool 中的一些设置,可以帮我们将构建打包(编译)后的代码映射回原始的源代码,即我们经常所听到的 sourcemap
。如:Webpack, TypeScript, babel, powser-assert 等转换代码的工具都有提供 sourcemap 的功能,否则源代码被压缩、混淆、polyfill 后,就根本就没办法调试定位问题了。
所以 sourcemap
对于开发时调式起到了极其重要的作用,而不同类型的 sourcemap 会明显的影响到构建和重新构建的速度。
我在开发构建环境一般会选择 cheap-module-eval-source-map
,而在生产构建环境则会选择 cheap-module-source-map
。
然后在webpack.dev.js
配置文件中添加以下代码
module.exports = merge(common, {
mode: 'development',
+ devtool: 'cheap-module-eval-source-map',
});
在webpack.prod.js
配置文件中添加以下代码
module.exports = merge(common, {
mode: 'production',
+ devtool: 'cheap-module-source-map',
});
通过上面的配置后,我们在本地进行开发时,如果代码出现了错误,那么 Console 界面的错误提示就会精确的告诉我们错误的文件、位置等信息。
具体sourcemap的宏观理解,推荐文章:https://juejin.cn/post/6960941899616092167
6.dist文件夹配置
这时候我们思考一个问题,我们打包完dist文件夹下有打包的内容,这时候再次yarn build
之后,dist\js
文件夹会重复打包出口文件,因为原理就是每次打包,Webpack 都是直接把产物直接丢进去的,我们最直观的做法就是:每次打包前把原来的删了,这样当然可以,可是能自动化的事情就不要无意义的重复劳动。
执行以下命令安装依赖包
yarn add -D clean-webpack-plugin
到 webpack.prod.js
配置文件,增加以下代码
...
+const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = merge(common, {
...
+ plugins: [new CleanWebpackPlugin()],
});
- 作用 : 每次构建打包前,
clean-webpack-plugin
会自动的找到 output 属性中的 path 的值进行清除。
7.样式文件配置
如果现在我们在
src/
文件夹下创建一个app.css
样式文件,给选择器#app
随便添加一个样式,app.js 代码文件中通过import './app.css'
,再进行生产构建或者开发构建,Webpack 就会直接报错。因为 Webpack 只认识.js
的代码模块文件,他不支持直接处理.css
.less
.scss
等文件,所以我们需要借助 Webpack 另一个核心的东西,他的名字叫Loader
。 - Loader : 用于对模块的源代码进行转换,Loader 可以使你在 import 模块文件时,处理文件内容。因此,Loader 类似于其他构件工具中的“任务(Task)”,并提供了处理前端构建步骤的强大方法。Loader 可以将文件从不同的语言,如 TypeScript 转换为 JavaScript,或将内联图像文件转换为 dataURL。Loader 甚至可以让你在 JavaScript 模块中 import CSS 样式文件,把 CSS 样式文件当做 JavaScript 模块使用。
1. CSS 样式文件
处理 .css 样式文件,我们需要安装 style-loader
和 css-loader
两个 Webpack Loader 依赖包
yarn add -D style-loader css-loader
然后,到 webpack.common.js
配置文件中,加入以下代码
module.exports = {
entry: { ... },
ouput: { ... },
module:{
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
sourceMap: ISDEV, // 开启后与devtool设置一致
importLoaders: 0 // 指定在css-loader处理前使用的loader数量
}
}
]
}
]
},
plugins: [ ... ]
};
- test : 匹配文件后缀类型正则表达式,针对符合规则的文件进行所配置的 Loader 处理
- use : 属性的值有几种写法
- 字符串:例如我们只使用 style-loader 的话,那就可以写成 use: 'style-loader'
- 数组:例如我们不对 css-loader 做 options 参数配置,那就可以写成 use: ['style-loader', 'css-loader']。若需要对某个 Loader 做参数配置可以参考上面的配置
- 对象:例如只使用 css-loader 的话且需要进行 options 参数配置,那就可以写成 use: { loader: 'css-loader', options: { importLoaders: 0 } }
Loader 是有顺序的,Webpack 肯定是先将 css 模块依赖解析完得到 css 代码字符串,才能把 css 代码字符串内容以 style 标签的形式插入到 html 模板的 head 标签中。
所以在上面的配置中 style-loader 放在了 css-loader 的前面,也就说明了Webpack Loader 的执行顺序是从右到左,从下到上的
。
2. LESS 样式文件
处理 .less 样式文件,我们还需要另外安装 less
和 less-loader
两个依赖包
yarn add -D less less-loader
然后,到 webpack.common.js 配置文件中
module.exports = {
entry: { ... },
ouput: { ... },
module:{
rules: [
...
{
test: /\.less$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
sourceMap: ISDEV, // 开启后与devtool设置一致
importLoaders: 1, // 需要先被less-loader处理,所以这里设置为1
},
},
{
loader: 'less-loader',
options: {
sourceMap: ISDEV, // 开启后与devtool设置一致
},
},
],
}
]
},
plugins: [ ... ]
};
3. SASS 样式文件
处理 .sass 样式文件,我们还需要另外安装 node-sass
和 sass-loader
两个依赖包
yarn add -D node-sass sass-loader
然后在在 webpack.common.js 配置文件中
module.exports = {
entry: { ... },
ouput: { ... },
module:{
rules: [
...
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
sourceMap: ISDEV, // 开启后与devtool设置一致
importLoaders: 1, // 需要先被less-loader处理,所以这里设置为1
},
},
{
loader: 'sass-loader',
options: {
sourceMap: ISDEV, // 开启后与devtool设置一致
},
},
],
},
]
},
plugins: [ ... ]
};
通过上面的配置之后,我们再把 src/app.css
改为 app.less
或 app.sass
,执行命令 yarn start
,我们会发现报错了
**TypeError: this.getOptions is not a function**
原因就是我们使用的Webpack 4,而随着版本的迭代,新的的loader也会同步新版的更新内容,而没有锁版本的依赖包会自动下载最新版的,导致版本对不上,这是很常见的错误。Webpack 5 发布后,在 loader 的上下文中,会带有内置的 this.getOptions 方法。这对于那些使用之前推荐 schema-utils 中的 getOptions 方法的 loader 而言,这是一个重大更新。所以我们要把对应的loader的版本锁在支持Webpack 4的版本,具体的版本可以参考这篇文章:https://blog.arcto.me/frontend/webpack-4-loader/
后续操作就是卸载对应的依赖包,再安装对应的版本,例如:
yarn remove css-loader
yarn add -D css-loader@5.2.7
完成对应的版本锁定之后,再执行yarn start
,就会发现我们写的样式已经可以正常加载出来了。
4. PostCSS 处理浏览器兼容问题
以前在写网页样式的时候,在涉及到 CSS3 的相关的样式语法时,都需要在前面加上浏览器的前缀做兼容性处理,当时就对 css 产生一种不好的印象,太麻烦了!而 PostCSS 就是来帮我们根据实际情况自动加上浏览器前缀的工具,大大减少了我们在编写样式时的顾虑。
- PostCSS 是 css 后处理器工具,因为有了 css,PostCSS 才能去处理他,所以叫后处理器
- less/sass 是 css 预处理器工具,因为他们需要把 .less 和 .scss 处理成 .css,所以叫预处理器
执行以下命令,安装相关依赖包
yarn add -D postcss postcss-loader postcss-flexbugs-fixes postcss-preset-env autoprefixer postcss-normalize
- postcss-flexbugs-fixes : 用于修复一些和 flex 布局相关的 bug
- postcss-preset-env : 将最新的 css 语法转换为目标浏览器环境能够理解的 css 语法,目的是使开发者在编写样式代码时不用考虑浏览器的兼容性问题
- autoprefixer : 自动添加浏览器前缀,处理样式语法兼容问题
- postcss-normalize : 从 browserslist 中自动导入所需要的 normalize.css 内容
然后,再到 webpack.common.js
配置文件中,把 postcss-loader
的配置放在刚刚上面置完的 css
less
scss
里的 css-loader
后面,配置如下
{
loader: 'postcss-loader',
options: {
// 这里要注意配置是包裹在postcssOptions属性中
postcssOptions: {
ident: 'postcss',
plugins: [
// 修复一些和flex布局相关的bug
require('postcss-flexbugs-fixes'),
// 参考browserslist的浏览器兼容表自动对那些还不支持的css语法做转换
require('postcss-preset-env')({
// 自动添加浏览器前缀
autoprefixer: {
// will add prefixes only for final and IE versions of specification
flexbox: 'no-2009',
},
stage: 3,
}),
// 根据browserslist自动导入需要的normalize.css内容
require('postcss-normalize'),
],
},
// 开启后与devtool设置一致
sourceMap: ISDEV,
},
}
但是,如果我们要为每一个之前所配置的样式 Loader 中都加一段这个代码的话,整个 webpack.common.js
配置文件会显得非常冗余,并且如果需要做修改的话,三个地方都得同步修改。所以我们可以把这里的公共部分的配置抽成一个函数,与 CRA 一致,命名为 getCssLoaders
,因为新增了 postcss-loader
,所以我们还需要修改 css-loader
的 importLoaders
参数的值,于是我们的 webpack.common.js
就变成如下这样
const getCssLoaders = (importLoaders) => [
'style-loader',
{
loader: 'css-loader',
options: {
sourceMap: ISDEV, // 开启后与devtool设置一致
importLoaders, // 指定在css-loader处理前使用的laoder数量
},
},
{
loader: 'postcss-loader',
options: {
// 这里要注意配置是包裹在postcssOptions属性中
postcssOptions: {
ident: 'postcss',
plugins: [
// 修复一些和flex布局相关的bug
require('postcss-flexbugs-fixes'),
// 参考browserslist的浏览器兼容表自动对那些还不支持的css语法做转换
require('postcss-preset-env')({
// 自动添加浏览器前缀
autoprefixer: {
// will add prefixes only for final and IE versions of specification
flexbox: 'no-2009',
},
stage: 3,
}),
// 根据browserslist自动导入需要的normalize.css内容
require('postcss-normalize'),
],
},
// 开启后与devtool设置一致
sourceMap: ISDEV,
},
}
];
module.exports = {
entry: { ... },
output: { ... },
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"]
}
现在,我们写的样式就会在打包后自动添加对应的浏览器前缀了,可以自己写点样式打包测试一下
8.图片和字体文件处理
我们使用 file-loader 和 url-loader 来处理本地资源文件,比如:图片、字体文件等。而 url-loader 是对 file-loader 的封装,具有 file-loader 的所有功能,并且还提供了将低于阈值体积的图片装换成 base64 嵌入到页面中。但是,url-loader 并不依赖于 file-loader,所以一般我们只安装 url-loader 即可!
安装依赖
yarn add -D url-loader
然后,我们到 webpack.common.js 中继续在 modules.rules 中添加以下代码
module.exports = {
entry: { ... },
ouput: { ... },
module: {
rules: [
...
{
test: [/\.bmp/, /\.gif/, /\.jpe?g/, /\.png/],
use: [
{
loader: 'url-loader',
options: {
limit: 10 * 1024, // 图片低于10k会被转换成base64格式的dataUrl
name: '[name].[contenthash:8].[ext]', // [hash]占位符和[contenthash]是相同的含义,都是表示文件内容的摘要值,默认是使用md5 hash算法
outputPath: 'assets/imgaes', // 构建打包输出到出口文件夹的assets/images文件夹下
},
},
],
},
{
test: /\.(ttf|woff|woff2|eot|otf)$/,
use: [
{
loader: 'url-loader',
options: {
name: '[name].[contenthash:8].[ext]',
outputPath: 'assets/fonts',
},
},
],
},
]
},
plugins: [ ... ]
}
- name : 属性的值表示输出的文件名为
原来文件名.8位文件内容摘要.文件拓展名
,有了这个 8 位的文件内容摘要,可以防止图片更新后导致的缓存问题 - limit : 属性的值表示,如果图片文件小于
1024b
,即10kb
,那就使用 url-loader 把图片转为 base64 嵌入到网页中,否则就转而使用 file-loader 让图片保持独立文件 - outputPath : 属性的值是以
output.path
的值为基准,既我们这里是以dist
文件夹为基准,把图片或者字体文件通过构建打包流程后输出到dist/assets/images
和dist/assets/fonts
文件夹下
然后,我可以可以随便把本地一张图片放入项目的 src 文件夹中,并创建一个文件 demo.tsx 然后在其中通过 import 引入该图片,你会发现报错了?
原因是你的编译器不认识这个图片的类型,那么你需要定义类型告诉它,引入的这个是什么东西,在 src
下创建 TypeScript 类型定义文件 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 的基本配置,具备了 Webpack 的基本功能。但是核心的问题是,我们的标题叫:从0-1搭建自己的TypeScript+React开发环境,TypeScript支持吗?React支持吗?这还有待后续的配置
支持 React配置
接下来就到我们熟悉的react环节了,首先肯定是相关包的引入
yarn add -S react react-dom
然后,我们就已经开始可以使用 jsx
语法了,我们在 src
文件夹下创建 index.js
代码文件,且内容改为如下代码
import React from "react";
import ReactDOM from "react-dom";
import App from "./app";
ReactDOM.render(<App />, document.querySelector("#app"));
把 src/app.js
中的内容修改为以下代码:
import React from "react";
function App() {
return <div className="app-content">Hello World!</div>;
}
export default App;
同时,也要把 webpack.common.js
配置文件中 entry
属性里的入口文件修改为 index.js
module.exports = {
entry: {
+ app: path.resolve(PROJECT_PATH, './src/index.js'),
- app: path.resolve(PROJECT_PATH, './src/app.js')
}
}
执行yarn start
测试一下,什么?又报错了?
原因是你写jsx
你是便捷了,舒服了,可是浏览器只认识js,这就需要 babel-loader 这位“翻译官”进行预处理
- Babel : 是一个 JS 编译器,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
yarn add -D babel-loader @babel/core @babel/preset-react
- babel-loader : 使用 Babel 解析代码文件
- @babel/core : Babel 的核心依赖
- @babel/preset-react : 是转译 jsx 语法的预设集合(Babel 插件的预设集合)。
然后在项目根目录创建 .babelrc 配置文件,并输入以下代码:
{
"presets": ["@babel/preset-react"]
}
- presets : 是 Babel 的一些预设插件集合
接下来,我们到 webpack.common.js
配置文件,增加一下代码
module.exports = {
entry: { ... },
ouput: { ... },
module: {
rules: [
...
{
test: /\.(tsx?|js)$/,
loader: 'babel-loader',
options: { cacheDirectory: true },
exclude: /node_modules/,
},
]
},
plugins: [ ... ]
}
- options : 配置属性Babel 的
cacheDirectory: true
参数,让 Babel 在运行时把这些公共文件缓存起来,下次编译的时候就会快很多。
我们匹配的代码文件后缀只有 .ts
.tsx
.js
,把 .jsx
的后缀排除在外了。因为,我们不可能在 ts 环境下再使用 .jsx 代码文件来写代码了
弄完之后yarn start
测试一下效果吧
支持 TypeScript配置
Webpack 的模块系统只能识别 js 代码文件及其语法,遇到 jsx 语法、tsx 语法、图片、字体等文件就需要相应的 Loader 对其进行预处理,像图片、字体我们在上文中已经配置过了。为了支持 React,我们使用了 babel-loader 以及对应的 Babel 预设集合,如果现在要支持 TypeScript 我们也需要对应的 Babel 预设集合
yarn add -D @babel/preset-typescript
TypeScript 之前配置 eslint的时候安装了
然后在.babelrc
配置文件,进行以下修改
{
"presets": ["@babel/preset-react", "@babel/preset-typescript"]
}
注意,Babel 配置文件里的 presets 的执行顺序与 Webpack 配置文件里的 Loader 是一样的,都是从右往左,从下往上的执行,但 Babel 的 plugins 就与其相反。
在src
文件夹下创建index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./app";
ReactDOM.render(<App name="hyh" age={23} />, document.querySelector("#app"));
在src
文件夹下创建app.tsx
import React from "react";
interface IProps {
name: string;
age: number;
}
function App(props: IProps) {
const { name, age } = props;
return (
<div className="app-content">
<span>{`Hello my name is {name},{age} years old.`}</span>
</div>
);
}
export default App;
这个时候如果我们执行命令 yarn start
,是会报错的。我们应该还得到 webpack.common.js
配置文件,做如下修改
module.exports = {
entry: {
app: path.resolve(PROJECT_PATH, './src/index.tsx'),
},
...
resolve: {
extensions: ['.tsx', '.ts', '.js', '.json'], // 经常被import的文件后缀放在前面
}
}
- entry : 修改了 entry 属性中的入口代码文件后缀,改为了 .tsx
- resolve : 新增了 resolve 属性,并在
resolve.extensions
中定义好了文件后缀名,这样当我们在 import 某个代码文件的时候,就可以省略后缀名了
Webpack 会按照 resolve.extensions
中所定义的后缀名顺序依次查找代码文件,比如我们定义了 ['.tsx', '.ts', '.js', '.json'],那么 Webpack 会先把 import 的代码文件名尝试加上 .tsx 后缀在文件夹下查找,若找不到就会依次尝试查找。所以我们在配置时应该尽可能的把常用的后缀放在前面,这样可以缩短 Webpack 的查找时间,提升构建打包的效率
而用上了 TypeScript,那 React 的 ts 类型声明自然就不能少,执行以下命令安装相关依赖包
yarn add -D @types/react @types/react-dom
之后yarn start
测试就正常了
同时注意,每个 TypeScript 项目都需要有一个 tsconfig.json
配置文件,其作用简单的解释就是:编译指定的文件 和 定义了编译选项
一般都会把 tsconfig.json
配置文件放在项目根目录下,我们执行以下命令来生成该配置文件
npx tsc --init
打开默认的tsconfig.json
配置文件,可以替换成如下内容:
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
"target": "es5", // 编译成哪个版本的ES
"module": "ESNext", // 指定生成哪个模块系统的代码
"lib": ["dom", "dom.iterable", "esnext"], // 编译过程中需要引入的库文件列表
"allowJs": true, // 允许编译js文件
"jsx": "react", // 在.tsx代码文件里支持jsx
"isolatedModules": true,
/* Strict Type-Checking Options */
"strict": true, // 启用所有严格类型检查选项
/* Module Resolution Options */
"moduleResolution": "node", // 指定模块解析策略
"baseUrl": "./", // 根路径
"paths": {
"Src/*": ["src/*"],
"Components/*": ["src/components/*"],
"Utils/*": ["src/utils/*"]
}, // 路径解析
"esModuleInterop": true, // 支持CommonJS和ES模块之间的互操作性
/* Experimental Options */
"experimentalDecorators": true, // 启用实验性的ES装饰器
"emitDecoratorMetadata": true, // 给源码里的装饰器声明加上设计类型元数据
/* Advanced Options */
"skipLibCheck": true, // 忽略所有的声明文件(*.d.ts)的类型检查
"forceConsistentCasingInFileNames": true // 禁止对同一个文件的不一致的引用
},
"exclude": ["node_modules", "scripts", "dist"]
}
关于各个字段的作用都有说明
更多 Babel 配置
到这里,我们已经支持了React 和 TypeScript 的语法了,也有了基本环境,但是我们在项目中编写的 ES6+ 语法,在 Webpack 构建打包后还是会被原样输出,然而并不是所有的浏览器环境都支持 ES6+ 语法,这时候就需要 Babel 的 @babel/preset-env
预设集合来帮我们做高级语法转译的这个苦力活了。他会根据设置的目标浏览器环境,也就是我们上面在 package.json 配置文件里设置过的 browserslist
属性里的配置信息,根据这个配置信息找出所需要的插件去转译 ES6+ 语法,比如 const
或 let
转译为 var
。
但是,遇到 Promise
或者 Array.prototype.map
这类新的 api 时,是没有办法用直接转译来解决的。除非我们用低版本的代码实现此类新的 api ,然后再把实现的 api 代码注入到构建打包后的代码文件中填充所缺失的 api。虽然 @babel/preset-env
可以做这项工作,但是他在做这项工作时会直接在原生对象上挂载实现的代码,这样就会造成原生对象污染的问题发生。还好 Babel 还有一个宝藏插件 @babel/plugin-transform-runtime
,他和 @babel/preset-env
预设集合一样都能提供新 api 的垫片,都可以实现按需加载,但前者不会污染原型链
另外,Babel 在编译每一个模块在需要的时候他会插入一些辅助函数,例如 _extend
。每一个需要的模块都会插入这么一些辅助函数,这就很明显的造成了代码的冗余了,而 Babel 的 @babel/plugin-transform-runtime
插件会将所有的辅助函数都从 @babel/runtime
中导入(我们下面使用 @babel/runtime-corejs3),从而减少此类代码的冗余
执行以下代码,安装他们的依赖包
yarn add -D @babel/preset-env @babel/plugin-transform-runtime
yarn add -S @babel/runtime-corejs3
修改 .babelrc
配置文件
{
"presets": [
[
"@babel/preset-env",
{
"modules": false // 防止Babel将任何模块都转译成CommonJS类型,导致Webpack的tree-shaking失效
}
],
"@babel/preset-react",
"@babel/preset-typescript"
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": {
"version": 3,
"proposals": true
},
"useESModules": true
}
]
]
}
到这里,我们 TypeScript + React 的项目开发环境已经可以用于正常的项目开发了
Webpack 公共构建环境优化
1. 拷贝公共静态资源
这时候,如果我们执行命令yarn build
,可以看到 dist
文件夹下是没有 favicon.ico
文件的,那么 html 文件中的引入肯定也就没办法生效了。于是我们就希望有一个手段,能够在我们构建打包时把 public
文件夹下的静态资源通通都复制到我们出口文件夹 dist
中,反正我不会考虑手动去复制,所以我们还是使用 Webpack 的 copy-webpack-plugin
插件吧
执行以下命令,安装他的依赖包
yarn add -D copy-webpack-plugin
然后,我们到 webpack.common.js
配置文件中,增加以下代码:
module.exports = {
...
plugins: [
...
new CopyWebpackPlugin({
patterns: [
{
context: path.resolve(PROJECT_PATH, './public'),
from: '*',
to: path.resolve(PROJECT_PATH, './dist'),
toType: 'dir',
},
],
}),
]
...
}
再次执行,dist
下对应的资源就打包好啦~
2. 显示构建打包进度
如果我们现在执行yarn start
和 yarn build
命令,控制台是没有任何信息能提示我们现在的进度到底怎么样。而一般来说构建打包的速度往往都需要一些时间,如果不是很熟悉项目的人,基本都会认为是不是卡住了,从而大大地提升了焦虑感。所以,显示进度是比较重要的,这是对开发者积极的正向反馈。
我们可以使用 webpackbar
来显示进度,执行以下命令来安装他的依赖包
yarn add -D webpackbar
到 webpack.common.js
配置文件中,增加以下代码
module.exports = {
...
plugins: [
...
new Webpackbar({
name: ISDEV ? '正在启动' : '正在构建打包',
}),
]
...
}
现在我们再重新执行以下命令,就会发现有进度条提示了啦~
4,编译时的 TypeScript 类型检查
我们之前在配置 Babel 的时候提过,为了编译效率,Babel 在编译 TypeScript 时会直接将 ts 类型去掉,并不会对 ts 类型做检查。但是,此时如果你执行命令 yarn start
或 yarn build
的话,是可以正常的构建打包的。所以,可能在某一时刻某一个开发人员犯了这样的错误,但没有去处理这个问题,直到别人接手这个项目后,也不知道有这么一个问题,然后在执行构建打包命令时,就把这样的问题代码也一同给构建打包出来了!这样就完全丧失了 TypeScript 类型声明所带来的优势
所以,我们需要借助 Webpack
的 fork-ts-checker-webpack-plugin
插件力量,他无论是在开发还是生产的构建打包时都会给我们进行 ts 类型的检查,并在检查到错误的时候给出准确的错误提示
yarn add -D fork-ts-checker-webpack-plugin
然后,到 webpack.common.js
配置文件中,增加以下代码
module.exports = {
...
plugins: [
...
new ForkTsCheckerWebpackPlugin({
typescript: {
configFile: path.resolve(PROJECT_PATH, './tsconfig.json'),
},
}),
]
...
}
这样再执行打包命令时就能出现错误提示啦~
4. 加速二次构建打包速度
有一个 Webpack 的神器插件就能大大滴提高二次构建打包的速度,他为程序中的模块(如:loadash)提供了一个中间缓存,并且把缓存放在了项目的 node_modules/.cache/hard-source
文件夹下,而这个神器插件就是 hard-source-webpack-plugin
。在使用这个插件后,首次构建打包时可能耗时会比原来多一点,因为他要进行一些缓存工作,但在首次构建打包缓存后,之后的每次一构建打包都会变得快很多
yarn add hard-source-webpack-plugin
然后,我们到 webpack.common.js
配置文件,增加以下代码
module.exports = {
...
plugins: [
...
new HardSourceWebpackPlugin(),
]
...
}
弄完之后,我们可以自己打包两次对比一下速度
此外,能配置的还有很多,像减少打包体积的依赖,抽离公共代码,组件懒加载等
同时,在开发环境里,像热更新的配置,后端接口代理等;在生产环节里,css拆分,去除无用代码,代码压缩,css压缩,tree-shaking,bundle 分析等都是值得我们深入思考的。
总结
到这里,想必你也跟着配置好了属于自己的开发环境,虽然内容较长,但也很有成就感不是嘛。对于我一个实习生来说,配置的过程有很多懵懵懂懂的地方,先啃下来,后续再回味消化也不失为一个好办法,从宏观的了解再细化到具体的点,这还需要后续的实践探索!
同时,在前端飞速发展的现在,文章的时效性随着各个依赖的迭代可能也会随之改变,遇到问题不要慌,根据错误提示对症下药,一步一个脚印。此外,这样的搭建思维也可以自己升级优化,更好了依赖包?不同的框架语言?各取所需!
仓库地址: https://github.com/hyh-op/react_ts_hyh
参考文章
整体参考:https://www.aaronlam.xyz/2020/02/15/step-by-step-build-ts-react-dev-env/
prettier设置参考: https://www.prettier.cn/docs/options.html
脚本命令scripts推荐:https://www.ruanyifeng.com/blog/2016/10/npm_scripts.html
Git commit知识推荐:https://www.ruanyifeng.com/blog/2016/01/commit_message_change_log.html
path.resolve()函数参考:https://www.jianshu.com/p/3a713442b70b
缓存协议文章推荐:https://zhuanlan.zhihu.com/p/60950750
sourcemap的宏观理解推荐:https://juejin.cn/post/6960941899616092167
文章评论