打造最简洁的 typescript + react + webp
前言
作为一枚前端开发者来说,与时俱进是少不了的。
近些年来的各种前端开发工具层出不穷,让人眼花缭乱。
虽然单纯的写一个页面,用 html、css、JavaScript 三者就够了,甚至有的简单的页面,连 JavaScript 也不需要,单凭 html + css 就足以胜任。
但是对于大型的项目来说,不用前端框架开发,项目简直就是灾难现场。
一般而言,大型项目都不会是由一个人单独开发完成的,而是需要多人协作开发。每个人的代码风格都不尽相同,而且每个人的水平都参差不齐,这就导致会碰到各种问题,最后往往搞得大家苦不堪言。
得益于 typescript 的兴起以及近些年的蓬勃发展,前端项目的开发也越来越规范化了。
本文的目的就是记录下,怎么配置最简单的 typescript + react + webpack + eslint 前端的开发环境。
1. 初始化项目
这一步是初始化项目必不可少的,也没什么高深的地方。打开控制台,执行以下命令就好了。
mkdir demo && cd demo && git init && npm init -y
echo node_modules/ >> .gitignore
mkdir src && touch src/app.js src/index.html
依次执行完以上命令以后,我们会得以下目录结构:
$ tree
.
├── package.json
└── src
├── app.js
└── index.html
下面我们来先补充下 index.html
和 app.js
里面的内容。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
</head>
<body>
<div id="root"></div>
<script src="./app.js" type="module"></script>
</body>
</html>
app.js
const root = document.querySelector('#root');
const h1 = document.createElement('h1');
h1.innerText = "Hello, typescript + react + webpack + eslint.";
root.appendChild(h1);
在浏览器打开 index.html
页面,出现以下结果:

2. 添加 webpack
添加 webpack,然后创建配置文件
npm install --save-dev webpack webpack-cli webpack-dev-server webpack-merge clean-webpack-plugin
mkdir build && touch webpack.dev.js build/webpack.prod.js build/webpack.common.js
执行完以上命令以后,目录结构会变成如下所示

webpack.common.js
基本的配置
const path = require('path');
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: './src/app.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, '../dist'),
publicPath: './',
},
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html",
}),
],
};
webpack.dev.js
开发环境的配置
const path = require('path');
const webpackMerge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = webpackMerge(common, {
mode: 'development',
devtool: 'inline-source-map',
devServer: {
contentBase: path.join(__dirname, '../dist'),
publicPath: '/',
compress: true,
port: 9000,
},
});
webpack.prod.js
生产环境的配置
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
module.exports = merge(common, {
mode: 'production',
devtool: 'source-map',
plugins: [
new CleanWebpackPlugin(),
],
watchOptions: {
poll: 1000, // 轮询间隔时间
aggregateTimeout: 500, // 防抖(在输入时间停止刷新计时)
ignored: /node_modules/,
},
});
修改 index.html
</head>
<body>
<div id="root"></div>
- <script src="./app.js" type="module"></script>
</body>
</html>
朝 package.json 添加脚本
"description": "",
"main": "index.js",
"scripts": {
+ "start": "webpack-dev-server --open --config ./build/webpack.dev.js",
+ "watch": "webpack --watch --config ./build/webpack.prod.js",
+ "build": "webpack --config ./build/webpack.prod.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
运行脚本
npm start
$ npm start
> demo@1.0.0 start /Users/root1/Desktop/demo
> webpack-dev-server --open --config ./build/webpack.dev.js
ℹ 「wds」: Project is running at http://localhost:9000/
ℹ 「wds」: webpack output is served from /
ℹ 「wds」: Content not from webpack is served from /Users/root1/Desktop/demo/dist
ℹ 「wdm」: wait until bundle finished: /
ℹ 「wdm」: Hash: e989ba9f2efc6598e907
Version: webpack 4.43.0
Time: 540ms
Built at: 2020/06/21 下午2:26:14
Asset Size Chunks Chunk Names
bundle.js 876 KiB main [emitted] main
index.html 249 bytes [emitted]
Entrypoint main = bundle.js
[0] multi (webpack)-dev-server/client?http://localhost:9000 ./src/app.js 40 bytes {main} [built]
[./node_modules/ansi-html/index.js] 4.16 KiB {main} [built]
[./node_modules/html-entities/lib/index.js] 449 bytes {main} [built]
[./node_modules/loglevel/lib/loglevel.js] 8.41 KiB {main} [built]
[./node_modules/url/url.js] 22.8 KiB {main} [built]
[./node_modules/webpack-dev-server/client/index.js?http://localhost:9000] (webpack)-dev-server/client?http://localhost:9000 4.29 KiB {main} [built]
[./node_modules/webpack-dev-server/client/overlay.js] (webpack)-dev-server/client/overlay.js 3.51 KiB {main} [built]
[./node_modules/webpack-dev-server/client/socket.js] (webpack)-dev-server/client/socket.js 1.53 KiB {main} [built]
[./node_modules/webpack-dev-server/client/utils/createSocketUrl.js] (webpack)-dev-server/client/utils/createSocketUrl.js 2.91 KiB {main} [built]
[./node_modules/webpack-dev-server/client/utils/log.js] (webpack)-dev-server/client/utils/log.js 964 bytes {main} [built]
[./node_modules/webpack-dev-server/client/utils/reloadApp.js] (webpack)-dev-server/client/utils/reloadApp.js 1.59 KiB {main} [built]
[./node_modules/webpack-dev-server/client/utils/sendMessage.js] (webpack)-dev-server/client/utils/sendMessage.js 402 bytes {main} [built]
[./node_modules/webpack-dev-server/node_modules/strip-ansi/index.js] (webpack)-dev-server/node_modules/strip-ansi/index.js 161 bytes {main} [built]
[./node_modules/webpack/hot sync ^\.\/log$] (webpack)/hot sync nonrecursive ^\.\/log$ 170 bytes {main} [built]
[./src/app.js] 174 bytes {main} [built]
+ 18 hidden modules
Child HtmlWebpackCompiler:
1 asset
Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
[./node_modules/html-webpack-plugin/lib/loader.js!./src/index.html] 455 bytes {HtmlWebpackPlugin_0} [built]
ℹ 「wdm」: Compiled successfully.
npm run build
$ npm run build
> demo@1.0.0 build /Users/root1/Desktop/demo
> webpack --config ./build/webpack.prod.js
Hash: 82d149378677fd82d8b9
Version: webpack 4.43.0
Time: 337ms
Built at: 2020/06/21 下午2:27:55
Asset Size Chunks Chunk Names
bundle.js 1.09 KiB 0 [emitted] main
bundle.js.map 4.89 KiB 0 [emitted] [dev] main
index.html 228 bytes [emitted]
Entrypoint main = bundle.js bundle.js.map
[0] ./src/app.js 174 bytes {0} [built]
Child HtmlWebpackCompiler:
1 asset
Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
[0] ./node_modules/html-webpack-plugin/lib/loader.js!./src/index.html 455 bytes {0} [built]
npm run watch
$ npm run watch
> demo@1.0.0 watch /Users/root1/Desktop/demo
> webpack --watch --config ./build/webpack.prod.js
webpack is watching the files…
Hash: 82d149378677fd82d8b9
Version: webpack 4.43.0
Time: 130ms
Built at: 2020/06/21 下午2:31:00
Asset Size Chunks Chunk Names
bundle.js 1.09 KiB 0 [emitted] main
bundle.js.map 4.89 KiB 0 [emitted] [dev] main
index.html 228 bytes [emitted]
Entrypoint main = bundle.js bundle.js.map
[0] ./src/app.js 174 bytes {0} [built]
Child HtmlWebpackCompiler:
1 asset
Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
[0] ./node_modules/html-webpack-plugin/lib/loader.js!./src/index.html 455 bytes {0} [built]
2. 添加 react
添加 react 开发依赖以及类型文件。
npm install --save react react-dom
npm install --save-dev @types/react @types/react-dom
单纯只是添加了这些的话,我们暂时还无法编译运行。
因为 jsx 语法,需要我们配置 babel 才能正确的编译 js 代码。
我们知道的是,babel 配置一直是 webpack 配置里面的“玄学”,特别是对于初学者而言,着实不友好。
但是如果我们往下看,当我们朝项目中配置了 typescript 以后,就不需要配置 babel 就能直接编译运行了。
因为 typescript 不能直接运行在浏览器上,需要编译为 js 才能运行,如果我们写的是 tsx 文件,那么在由 typescript 编译为 js 的过程中,jsx 语法直接就被编译好了,不需要我们自己操心了。
3. 添加 typescript
添加 typescript 依赖以及配置文件。
npm install --save-dev typescript ts-loader source-map-loader
touch tsconfig.json
下面是 tsconfig.json
配置文件里面的内容:
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es5",
"jsx": "react",
"allowJs": true,
"baseUrl": ".",
"esModuleInterop": true,
},
"include": [
"./src/**/*"
],
"exclude": [
"dist",
"node_modules"
]
}
接下来,我们就修改 webpack.common.js
文件,让其支持 typescript 的编译
template: "./src/index.html",
}),
],
+
+ resolve: {
+ extensions: ['.ts', '.tsx', '.js', '.json'],
+ },
+
+ module: {
+ rules: [
+ {
+ test: /\.ts(x?)$/,
+ exclude: /node_modules/,
+ use: [
+ {
+ loader: 'ts-loader',
+ },
+ ],
+ },
+ {enforce: 'pre', test: /\.js$/, loader: 'source-map-loader'},
+ ],
+ },
};
接下来,我们讲 app.js
文件重命名为 app.tsx
,然后改一下里面的代码,改成 react 的写法
import React, { FC } from "react";
import ReactDom from "react-dom";
const Hello: FC<{title: string}> = ({ title }) => {
return <h1>{title}</h1>;
};
ReactDom.render(
<Hello title="Hello, typescript + react + webpack + eslint." />,
document.querySelector("#root"),
);
然后改一下 webpack.common.js 里面的入口文件
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
- entry: './src/app.js',
+ entry: './src/app.tsx',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, '../dist'),
然后运行 npm start ,发现项目能正常运行

4. 添加 eslint
输入下列命令,为项目添加 eslint 规则约束,这一项是可选的,但是为了项目代码的统一,建议还是用上。
唯一不好的地方是,用了以后,在写代码的过程中,经常会发现,自己写的不符合规范,还得花时间研究下,为啥自己写的不符合规范。
运行以下命令
npm install eslint -g && eslint --init
之后会出现让你进行选择的选项,类似下面的情况:
$ eslint --init
? How would you like to use ESLint? (Use arrow keys)
To check syntax only
❯ To check syntax and find problems
To check syntax, find problems, and enforce code style
不要着急,一路选择下去
$ 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? JavaScript modules (import/export)
? Which framework does your project use? React
? Does your project use TypeScript? Yes
? Where does your code run? (Press <space> to select, <a> to toggle all, <i> to
invert selection)Browser
? How would you like to define a style for your project? Use a popular style gui
de
? Which style guide do you want to follow? Google (https://github.com/google/esl
int-config-google)
? What format do you want your config file to be in? JavaScript
Checking peerDependencies of eslint-config-google@latest
Local ESLint installation not found.
The config that you've selected requires the following dependencies:
eslint-plugin-react@latest @typescript-eslint/eslint-plugin@latest eslint-config-google@latest eslint@>=5.16.0 @typescript-eslint/parser@latest
? Would you like to install them now with npm? (Y/n) y
最后输入 y 回车,eslint 相关配置就会自动安装到项目中去了
最后朝 package.json 中新增一个 fix 脚本
"start": "webpack-dev-server --open --config ./build/webpack.dev.js",
"watch": "webpack --watch --config ./build/webpack.prod.js",
"build": "webpack --config ./build/webpack.prod.js",
+ "lint": "eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx src/ ",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
运行 npm run lint
开始自动修复我们的 src 文件里的代码格式
$ npm run lint
> demo@1.0.0 lint /Users/root1/Desktop/demo
> eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx src/
/Users/root1/Desktop/demo/src/app.tsx
1:8 error 'React' is defined but never used no-unused-vars
1:16 error 'FC' is defined but never used no-unused-vars
4:7 error 'Hello' is assigned a value but never used no-unused-vars
✖ 3 problems (3 errors, 0 warnings)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! demo@1.0.0 lint: `eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx src/ `
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the demo@1.0.0 lint script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm ERR! A complete log of this run can be found in:
npm ERR! /Users/root1/.npm/_logs/2020-06-21T07_23_07_803Z-debug.log
可知道,fix 失败,这说明,有些错误,是不能通过此命令为我们自动修复的,我们必须手动修复或者忽略掉这个约束规则。
从报错,我们可知,是因为我们的代码不符合 no-unused-vars
这个规则,那么我们可以去掉这个约束或者修改我们的代码。
分析代码可以,这几个变量 我们都不能去掉,否则代码就不能正常运行了,那么我们只需要在 .eslintrc.js
文件里面的 rules 里加一行规则就行了,忽略掉这个规则

再次运行
npm run lint
$ npm run lint
> demo@1.0.0 lint /Users/root1/Desktop/demo
> eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx src/
发现,没有报错,根据 Unix 的 no news is good news 的设计哲学,我们的代码全部符合 eslint 的规则。

到此,我们的目标基本实现,最简洁的 typescript + react + webpack + eslint 开发环境已然搭建完成了。
最后来看下我们的项目目录:

5. 可选 webpack 插件
在实际项目中,只是添加以上一些模块是不够的,因为我们暂时还无法处理图片、css 等资源,为了让我们的开发环境更完善点,我们接下来需要朝项目中添加一些 webpack loader,让我们的开发环境更加的强大一些。
处理图片的 loader
添加 file loader
npm install --save-dev file-loader
修改 webpack.common.js
],
},
{enforce: 'pre', test: /\.js$/, loader: 'source-map-loader'},
+ {
+ test: /\.(png|jpg|gif)$/,
+ use: [
+ {
+ loader: 'file-loader',
+ options: {},
+ },
+ ],
+ },
],
},
};
然后我们尝试着,朝项目中引入一个 logo 图片。

然后改一下 app.tsx
里面的代码:
import React, {FC} from 'react';
import ReactDom from 'react-dom';
+import logo from './logo.jpg';
const Hello: FC<{title: string}> = ({title}) => {
- return <h1>{title}</h1>;
+ return <h1><img src={logo} />{title}</h1>;
};
ReactDom.render(
然后准备,运行的时候,却发现运行不了

别慌,原来是我们在 typescript 中引入图片的时候,需要自己定义以下其类型声明。
我们创建一个 typings 文件夹,然后新建一个 custom.d.ts
文件,自定义自己的类型声明。
mkdir typings && touch typings/custom.d.ts
custom.d.ts
内容
declare module '*.png' {
const content: any;
export = content;
}
declare module '*.jpg' {
const content: any;
export = content;
}
写到这里,还没完,还需要改下 tsconfig.json
,让我们这个文件可以正常加载出来。
"esModuleInterop": true,
},
"include": [
- "./src/**/*"
+ "./src/**/*",
+ "typings/*"
],
"exclude": [
"dist",
改完以后,重新 npm start
一下:

再次打开页面,可以看到,我们的 logo 也加载出来了。
处理 css 的 loader
添加 style-loader 和 css-loader
npm install --save-dev style-loader css-loader
修改 webpack.common.js
],
},
{enforce: 'pre', test: /\.js$/, loader: 'source-map-loader'},
+ {
+ test: /\.css$/,
+ use: [
+ 'style-loader',
+ {loader: 'css-loader', options: {url: false}},
+ ],
+ },
{
test: /\.(png|jpg|gif)$/,
use: [
添加一个 app.css
文件

在 app.tsx
里 import 一下
import React, {FC} from 'react';
import ReactDom from 'react-dom';
import logo from './logo.jpg';
+import './app.css';
const Hello: FC<{title: string}> = ({title}) => {
return <h1><img src={logo} />{title}</h1>;
重新 npm start
一下

打开页面,发现我们的 css 样式应用上去了。
其它有用的 loader
webpack 提供了很多 loader,处理各种不同的情景,如果你还有更多的需求,请参考页面:https://webpack.js.org/loaders/
6. 仓库地址
如果你想直接用我配置好的仓库,请访问地址:https://github.com/lovefengruoqing/demo,克隆或者下载下来以后,修改下 package.json
里面的信息即可使用。