2021年从零开发前端项目指南

之前翻译过一篇 前端工程化发展历史 的文章,WebpackBabelEslint 现在基本上就是前端项目的标配了。

但工作以后一般很少接触这些配置,都是在前人配置好的基础上去写业务代码。即使有机会从零配置一个项目,一般也不会自己手动建这些配置文件,直接用 create-react-appAnt Design Pro 等自动帮我们生成各个目录和配置文件就可以了,省时省力。

这篇文章的话就从零手动去配置一个前端项目,会涉及到 WebpackReactBabelTypeScriptAnt DesignSassEslintPrettier,本文的话就本着「不求甚解」的态度,主要过一下各个模块的使用,适合从零一步一步跟着操作。

前端工程化项目是建立在 node.js 环境下的,之后需要安装各个 npm 包,所以首先电脑必须已经配置好了 node 环境。

新建一个目录然后执行 npm init 来初始化一个项目。

1
npm init

然后一路回车就可以,只是生成了 package.json 文件,后续想改的话也能改。

img

Webpack

前端不断发展,但很多特性浏览器不一定会支持,ES6 模块,CommonJs 模块、Scss/lessjsx 等等,通过 Webpack 我们可以将所有文件进行打包、压缩混淆,最终转换为浏览器识别的代码。

除了安装 Webpack ,我们需要安装对应的命令行工具 webpack-cli,以及实现了热加载,也就是自动监听我们文件变化然后刷新网页的 webpack-dev-server

由于这些工具只在开发阶段使用,所以我们安装的时候可以加上 -D(--save-dev) 命令,这样开发环境就不会打包了。

1
npm i -D webpack webpack-cli webpack-dev-server

安装之后 package.json 会自动记录我们安装的 node 包,对应版本如下,如果安装的和我不一样的话,后边的一些配置可能略有不同。

1
2
3
4
5
6
7
8
{
...
"devDependencies": {
"webpack": "^5.51.1",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.0.0"
}
}

接下来在根目录新建 webpack.config.js 进行项目的配置,主要配置入口文件,打包输目录,以及 devServer 的目录。

1
2
3
4
5
6
7
8
9
10
11
const path = require('path')
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'bundle.js'
},
devServer: {
static: path.resolve(__dirname, './dist')
}
}

新建一下上边相应的文件。

main.js 文件主要实现在网页写 hello world

1
2
// /src/main.js
document.write('hello world')

新建 dist 目录,在里边新建 index.html 文件,引入 <script src="bundle.js"></script>

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>前端工程化</title>
</head>
<body>
<div id="app" />
<script src="bundle.js"></script>
</body>
</html>

最后在 package.json 新建两条命令,默认的 test 命令可以直接删掉了。

1
2
3
4
5
6
...
"scripts": {
"dev": "webpack-dev-server --mode development --open",
"build": "webpack --mode production"
},
...

执行 npm run dev ,此时会自动打开 http://localhost:8080/

img

React

React 可以让我们专注于构建用户界面,而不需要再手动维护 dom 元素的更新,当然还可以用 VUE

安装核心库 react ,以及渲染 Webreact-dom

1
npm i react react-dom

修改 src/main.js 体验一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// /src/main.js
import React from 'react';
import ReactDOM from 'react-dom';

class Hello extends React.Component {
render() {
return React.createElement('div', null, `Hello ${this.props.toWhat}`);
}
}

ReactDOM.render(
React.createElement(Hello, { toWhat: 'World by React' }, null),
document.getElementById('app')
);

npm run dev 看下效果:

img

这里会发现上边都调用了 React.createElement 来创建元素,如果页面复杂的的话,那一层套一层就太繁琐了,React 为我们提供了 JSX 语法来简化写法。

让我们改写一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// /src/main.js
import React from 'react';
import ReactDOM from 'react-dom';

class Hello extends React.Component {
render() {
return <div>Hello {this.props.toWhat}</div>;
}
}

ReactDOM.render(
<Hello toWhat="World by jsx" />,
document.getElementById('app')
);

但此时会发现项目跑不起来了

img

现在,我们就需要 Babel 了。

Babel

babel 可以为我们把各种语法、新功能转换为浏览器所能识别的 js 。这里我们先安装一下 babel 以及在 webpack 中使用的 babel-loader

1
npm i -D @babel/core babel-loader

然后在 webpack 中引入 babel-loader ,用来对 js 进行转换,更改 webpack.config.js 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const path = require('path')
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.(js)x?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
],
},
devServer: {
static: path.resolve(__dirname, './dist')
}
}

然后我们来安装 @babel/preset-react 来转换 jsx 语法。

1
npm i -D @babel/preset-react

在根目录新建 babel 的配置文件 babel.config.json

1
2
3
4
5
6
// babel.config.json
{
"presets": [
"@babel/preset-react"
]
}

此时再运行 npm run dev 就发现项目成功跑起来了!

然后我们还可以安装一些其他 babel 以便使用最新的 ES 语法,比如箭头函数、async await、问号表达式等等, 需要什么就可以配置什么。当浏览器不支持这些特性时,babel 可以帮我们实现 polyfill 进行降级。

@babel/preset-env 包含了许多 ES 的新特性,core-js 实现 ployfill,通过这两个 babel 各种 ES 最新的特性就都可以放心使用了,如果有不满足的我们可以单独配置 babel 的插件。

1
npm i -D @babel/preset-env core-js

然后我们再修改下 babel 的配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// babel.config.json
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
],
"@babel/preset-react"
],
"plugins": [
]
}

其中 useBuiltIns": "usage" 代表自动判断每个文件是否引入 ployfillcorejs: 3 是指定版本。

TypeScript

越来越多的项目引入了 TypeScript ,尤其是规模比较大的项目,通过 ts 可以让一些 bug 提前暴露,平时自己开发的话也可以引入 ts,提前了解学习。

项目引入 ts 的话有两种方式:

  1. 使用 TypeScript Compiler (TSC)ts 编译为 ES5 以便能够在浏览器中运行。并且使用 TSC 进行类型检查。
  2. 使用 Babel 翻译 TS,使用 TSC 进行类型检查。

这里的话使用第二种方式,让 BabelTSC 各司其职。

首先安装 TypeScript 以及 Reacttype

1
npm i -D typescript @types/react @types/react-dom

根目录新建 tsconfig.json 进行 ts 的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// tsconfig.json
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"lib": [
"dom"
],
"jsx": "react",
"noEmit": true,
"sourceMap": true,
/* Strict Type-Checking Options */
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
},
"include": [
"src"
]
}

"noEmit": true, 表明 ts 只做类型检查,不进行编译输出。

然后我们将 src/main.js 修改为 src/main.tsx,并且加上类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// /src/main.js
import * as React from 'react';
import * as ReactDOM from 'react-dom';

type Props = {
toWhat: string;
};
type State = {

};

class Hello extends React.Component<Props, State> {
render() {
return <div>Hello {this.props.toWhat}</div>;
}
}

ReactDOM.render(
<Hello toWhat="World by jsx" />,
document.getElementById('app')
);

接下来进行 babel 的配置,安装 @babel/preset-typescript,将我们代码从 ts 转为 js

1
npm i -D @babel/preset-typescript

babel 配置文件中加入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// babel.config.json
{
"presets": [
"@babel/preset-typescript",
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
],
"@babel/preset-react"
],
"plugins": [
]
}

最后在 webpack.config.jsbabel 匹配的路径中加入 tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const path = require('path')
module.exports = {
entry: './src/main.tsx',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.(js|ts)x?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
],
},
resolve: {
// 引入模块的时候可以省略这些后缀
extensions: ['.tsx', '.ts', '.jsx', '.js'],
},
devServer: {
static: path.resolve(__dirname, './dist')
}
}

我们可以全局安装一下 typescript ,便于使用 tsc 命令进行类型检查。

1
npm install -g typescript

可以运行一下 tsc -w 实时进行类型检查。

img

Ant Design

引入组件库,方便更快的开发。

1
npm install antd

顺便可以按照习惯把 main.tsx 中的 hello 组件抽离出来并且命名为 app.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// /src/App.tsx
import * as React from 'react';
import { DatePicker } from 'antd';

type Props = {
toWhat: string;
};
type State = {

};

class App extends React.Component<Props, State> {
render(): JSX.Element {
return <div>
Hello {this.props.toWhat}
<div>
<DatePicker></DatePicker>
</div>
</div>;
}
}

export default App;

然后我们在 main.tsx 引入 antdcss 文件。

1
2
3
4
5
6
7
8
9
10
// /src/main.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import 'antd/dist/antd.css';
import App from './App'

ReactDOM.render(
<App toWhat="World by jsx" />,
document.getElementById('app')
);

此时就需要在 webpack.config.js 配置文件中补上 cssloader ,先安装一下。

1
npm i -D style-loader css-loader

css-loader 可以让我们在 js 中引入 cssstyle-loader 帮我们将 cssstyle 标签的形式插入到页面。

安装好后进行配置 loader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const path = require('path')
module.exports = {
entry: './src/main.tsx',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.(js|ts)x?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
},
resolve: {
// 引入模块的时候可以省略这些后缀
extensions: ['.tsx', '.ts', '.jsx', '.js'],
},
devServer: {
static: path.resolve(__dirname, './dist')
}
}

然后就成功引入日期选择器了。

img

Sass

Sasscss 的预编译器,可以让我们写样式更顺手,具体特性可以参考 官网,我用的最多的就是可以嵌套形式写 css,很方便。

我们安装一下 Sass 以及它的 loader

1
npm install sass-loader sass  --save-dev

然后在 webpack.config.js 配置一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const path = require('path');
module.exports = {
entry: './src/main.tsx',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.(js|ts)x?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.s[ac]ss$/i,
use: [
// 将 JS 字符串生成为 style 节点
'style-loader',
// 将 CSS 转化成 CommonJS 模块
'css-loader',
// 将 Sass 编译成 CSS
'sass-loader',
],
},
],
},
resolve: {
// 引入模块的时候可以省略这些后缀
extensions: ['.tsx', '.ts', '.jsx', '.js'],
},
devServer: {
static: path.resolve(__dirname, './dist'),
},
};

App.jsx 加几个类名,引入 App.scss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// /src/App.tsx
import * as React from 'react';
import { DatePicker } from 'antd';
import './App.scss';

type Props = {
toWhat: string;
};
type State = {};

class App extends React.Component<Props, State> {
render(): JSX.Element {
return (
<div className="app">
<div className="text">Hello</div>
<div>{this.props.toWhat}</div>
<div>
<DatePicker></DatePicker>
</div>
</div>
);
}
}

export default App;

新建 App.scss,添加颜色实验一下。

1
2
3
4
5
.app {
.text {
color: #f00;
}
}

npm run dev 看下效果

img

Eslint

可以配置 eslint 来进行语法上静态的检查,也可以对 ts 进行检查。

1
npm i eslint -D

可以全局安装一下 npm i -g npx 命令,能够更方便的运行 node_modules/.bin 目录下的命令.

不然的话我们要执行 eslint 命令的话需要执行 ./node_modules/.bin/eslint --version 才能取到。或者像上边为了执行 tsc 命令,全局安装了 typescript。或者在 package.json 里边添加一个自定义命令。不过还是 npx 是最方便的。

让我们初始化 eslint.

1
npx eslint --init

然后按照项目需要选择对应的选项,最后自动安装相应的依赖。

img

然后 eslint 就自动为我们生成了 .eslintrc.js 配置文件,顺便补一个 "node": true,不然的话 module.exports 直接报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
module.exports = {
"env": {
"browser": true,
"es2021": true,
"node": true,
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"react",
"@typescript-eslint"
],
"rules": {
}
};

然后我们在 package.json 中可以添加一个 lint 命令来修复代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"name": "fe-learn",
"version": "1.0.0",
"description": "前端工程化项目学习",
"main": "index.js",
"scripts": {
"dev": "webpack-dev-server --mode development --open",
"build": "webpack --mode production",
"lint": "eslint src --fix"
},
"author": "windliang",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.15.0",
"@babel/preset-env": "^7.15.0",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.15.0",
"@types/react": "^17.0.19",
"@types/react-dom": "^17.0.9",
"@typescript-eslint/eslint-plugin": "^4.29.2",
"@typescript-eslint/parser": "^4.29.2",
"babel-loader": "^8.2.2",
"core-js": "^3.16.2",
"eslint": "^7.32.0",
"eslint-plugin-react": "^7.24.0",
"typescript": "^4.3.5",
"webpack": "^5.51.1",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.0.0"
},
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}

然后执行 npm run lint 即可进行 eslint 的相关修复。

配合 Vscode 我们也可以做到边写代码边自动检测 eslint,以及保存的时候自动修复 eslint 相关错误。

可以安装 Eslint 插件,以及在 vscode 的设置中加入以下配置,点击下图的右上角可以直接进行配置的编辑。

img

1
2
3
4
5
6
{
"eslint.validate": ["javascript", "javascriptreact", "vue", "typescript", "typescriptreact"],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
}

为了使用更完善的 eslint 配置,我们也可以直接引用腾讯 Alloy 团队的推荐配置,参考 这里

Prettier

prettier 主要做代码风格上的检查,字符串双引号还是单引号?几个空格?类似这样的。

当然 eslint 也可以配置这些,但为了分离它们各自的职责,最好还是用 prettier 来格式化代码风格,先安装一下。

1
npm i -D prettier

然后新建一个配置文件 .prettierrc.js,这里直接引用 腾讯 Alloy 团队推荐的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// .prettierrc.js
module.exports = {
// max 120 characters per line
printWidth: 120,
// use 2 spaces for indentation
tabWidth: 2,
// use spaces instead of indentations
useTabs: false,
// semicolon at the end of the line
semi: true,
// use single quotes
singleQuote: true,
// object's key is quoted only when necessary
quoteProps: 'as-needed',
// use double quotes instead of single quotes in jsx
jsxSingleQuote: false,
// no comma at the end
trailingComma: 'all',
// spaces are required at the beginning and end of the braces
bracketSpacing: true,
// end tag of jsx need to wrap
jsxBracketSameLine: false,
// brackets are required for arrow function parameter, even when there is only one parameter
arrowParens: 'always',
// format the entire contents of the file
rangeStart: 0,
rangeEnd: Infinity,
// no need to write the beginning @prettier of the file
requirePragma: false,
// No need to automatically insert @prettier at the beginning of the file
insertPragma: false,
// use default break criteria
proseWrap: 'preserve',
// decide whether to break the html according to the display style
htmlWhitespaceSensitivity: 'css',
// vue files script and style tags indentation
vueIndentScriptAndStyle: false,
// lf for newline
endOfLine: 'lf',
// formats quoted code embedded
embeddedLanguageFormatting: 'auto',
};

同样的,为了保存的时候自动帮我们格式化,我们可以安装 VscodePrettier 插件,以及再修改 Vscode 的配置。

1
2
3
4
5
6
7
8
9
10
{
"files.eol": "\n",
"editor.tabSize": 2,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"eslint.validate": ["javascript", "javascriptreact", "vue", "typescript", "typescriptreact"],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}

总结

通过上边一系列的操作后,就可以开始愉快的开始写项目了,经验有限,上边有问题的地方还请大家指出。

上边的代码都比较零碎,可以在 github 上看整个代码。

上边每一块都是一个很大的地方,未来的话会继续边学习边总结,欢迎一起交流。

windliang wechat