这是我的第一篇掘金博客,开启掘金写作之路。
大家好,我是刚加入掘金的"有两只臭猫"(公众号:咪仔和汤圆,欢迎关注~)
未经授权,禁止转载~
开篇
这次和大家探讨如何真正的从零开始搭建一个现代化前端工程,总结其中的要点。整个过程中,会用到很多工具、库、框架,也会有很多需要自己书写的地方。在文章中,更多的是偏向于如何搭建以及如何运作,所以某一些工具的详细配置,可能就不是那么精细了,还需要自己去探索。
适合有一些前端入门知识的人,如果是新手朋友,当不知道某一项是做什么的时候,就查资料,查到自己明白了他的作用为止(也花不了多少时间)~
再强调一遍,如果不知道,就去查资料,very very重要~
文中使用技术均为当前时间段(2022年1月)下的最新版,如果时间比较久了配置可能会有出入。
本文可能比较长,还请心平气和慢慢看(大侠别翻篇),最好实操一遍
再次谢谢大家~
(掘金的这个编辑器图片我明明设置了大小的,好像不生效哇~ 后面图片看着不太对,还请忽略)
目录和关键技术
首先把整个要用到的技术大致列出来一下,也供想了解的朋友先看一下:
HTML、CSS、JS、Webpack、Babel、TS、Less、Postcss、React、Redux、Axios、Eslint、Husky、Git
大致上是这些了~
好,那我们进入正题:joy:
一、工程运行原理
首先需要明白一个项目或者一个工程的本质,目前前端工程的本质运行逻辑还是没有变化的:读取HTML
、CSS
文件进行解析和渲染,加载其中的JS进行执行。所以搭建工程的目的就是万年不变的整出来这老三样,放在UserAgent
中运行。
目前流行的项目管理方式就是node
的包管理机制,目前有很多种管理机制:npm
、cnpm
、yarn
、pnpm
等,我们就选用目前最熟知的npm
就行了。
任何项目前提都是新建文件夹,首先我们新建一个文件夹,初始化为npm
项目:
mkdir demo && cd demo
npm init
初始化会要求你填入一些信息,这里我们选填一些工程的属性就可以了,里面具体的字段含义如果不懂可以查一下node
文档。初始化以后,项目就形成了,记住这个时候我们就已经是一个项目了。在mkdir demo
创建文件夹以后我们就是一个项目了,可以在里面写html、css、js
文件来运行。后面初始化为npm
包的形式只是为了跟上现代前端的步伐。
整篇完
开个玩笑~
初始化为工程以后,做什么?回想一下一个普通的项目,当我们运行npm run start
的时候,会启动一个本地服务器拉起我们写的代码,并且我们写的代码是承载在一个HTML
模板上的。其中做了以下的工作:
node
通过npm
运行package.json
中的命令,这个命令通常是脚手架自定义命令或者自己配置的打包工具命令。- 添加的打包工具或者脚手架接收到命令后,读取打包工具配置,进行打包生成产物。
- 打包的产物完以后会以一个入口的形式挂在在我们的模板文件下,这个时候我们就可以浏览器预览了。
- 加个本地服务器,用本地域名拉起我们的模板文件,给你营造家的感觉,起飞🛫。
二、实操 先跑起来
在新建的demo
文件夹中,新建一个index.html
,随便写一点内容。安装一个打包工具,并且按照这个打包工具的规则进行一些配置。我们选用常用的打包工具webpack
,直接打开文档的指南webpack(这个文档相对官网是更新最好的,翻译也比较ok,建议看这个。):
npm install webpack webpack-cli --save-dev
当安装完webpack
以后,如果不是很熟悉,可以按照官方给的文档走一遍。webpack
本质就是处理项目中用到的各种资源,打包压缩后将产物挂在html
模板上。因为我们是刚开始搭建工程,这里我们就手动先将产物挂载到模板文件上。
这个/dist/main.js
就是我们打包出来的东西,这里贴一下目前的工程目录:
几个比较重要的文件(这里吐槽一下掘金的编辑器太难用了,太难用了,2202年了还有这么难用的~):
注意📢:后面贴代码的部分,可能只会贴有改动的地方。
webpack.config.js
是我们打包工具的配置,这里我们只添加最基础的配置:
// webpack.config.js
// webpack最新的版本你甚至可以不写配置,有默认配置了
const path = require('path');
module.exports = {
mode: 'development',
entry: path.join(__dirname, 'src', 'index'),
output: {
path: path.join(__dirname, 'dist'),
}
};
index.html
就是我们的入口文件了,我们的逻辑也很简单,手动挂载js
,然后一个点击按钮,触发我们挂在的js
中的方法:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>从零开始一个工程</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="">
</head>
<body>
<script src="./dist/main.js"></script>
<h1>从零开始的一个工程</h1>
<button onclick="change()">点击</button>
<div id="app"></div>
</body>
</html>
src
下的index.js
,相当于我们的业务文件,逻辑就一个函数:
function say(){
console.log("say hello")
}
function change(){
console.log("调用了change")
const app = document.getElementById('app')
app.innerHTML = "改变App文案"
}
package.json
,我们安装完webpack
以后,只需要在scripts
下添加一个打包命令来启动webpack
,而webpack
的打包命令就是webpack
:
{
"name": "demo",
"version": "1.0.0",
"description": "一个示例工程",
"main": "index.js",
"scripts": {
"start": "webpack"
},
"author": "awefeng",
"license": "MIT",
"devDependencies": {
"webpack": "^4.29.0",
"webpack-cli": "^3.2.1"
}
}
三、配置环境和改变入口
这一步骤比较简单但是还是需要做。
虽然webpack
支持直接传--env
,但考虑到后续工程里面的环境判断,我们还是在scripts
命令传入环境变量。增加个依赖包cross-env
来抵消掉不同平台环境(windows
、linux
、macOS
等)运行命令时的问题:
npm install --save-dev cross-env
// package.json
"scripts": {
"start": "cross-env ENV=dev webpack",
"build:test": "cross-env ENV=test webpack",
"build": "cross-env ENV=prod webpack"
},
webpack
里面配置mode
为运行命令传入的环境参数,webpack
会将配置文件中mode
的值自动设置为process.env.NODE_ENV
的值。
设置webpack
的打包入口,注意这里我们把入口名改为了app
:
entry: {
app: resolve(__dirname, '../src', 'app') // multi: xxxx 多个入口类似
},
四、添加CSS、图片等处理配置
首先我们添加css
预处理器,这里选用less
(最好不用选sass
,国内的垃圾网络+sass
自身的实现问题+sass
的版本兼容问题...):
npm install --save-dev style-loader css-loader less-loader less
css-loader
:会对 @import
和 url()
进行处理,就像 js
解析 import/require()
一样,默认生成一个字符串数组存放存放处理后的样式字符串,并将其导出。
style-loader
:作用就是把 CSS
插入到 DOM
中,就是处理css-loader
导出的模块数组,然后将样式通过style
标签或者其他形式插入到DOM
中。
less-loader
:less
出的针对webpack
的loader
,less-loader
中会用到核心库less
。
webpack
的loader
是实行的链式流水线调用,并且是从右向左,所以我们这么配置:
module: {
rules: [
{
test: /.less$/i,
use: [
'style-loader',
'css-loader',
'less-loader',
]
}
]
}
到这里简单的webpack
处理less
的配置就完成了,我们在工程中写入一个less
文件,在index.js
中进行引入,然后在我们的函数运用:
// src/index.js
import './index.less'
function change(){
const app = document.getElementById('app')
app.innerHTML = "改变App文案"
app.classList.add("container")
}
change()
// index.less
.container{ width: 100px; height: 100px; border: 1px solid black;}
这时候我们运行npm run start
命令,等待打包完成以后,浏览器打开html
:
进行优化:
- 添加
css module
- 将
css
和js
分开(生产环境) - 压缩
css
css
进行按需引入- 浏览器兼容
css
这里就简单的先把每一项的方案、解决办法说一下,然后在最后贴上配置文件,相关的详细配置还需要自己去详细研究。
添加CSS Module
两种方案:
- 全部
less
、css
文件都模块化 - 部分文件(比如只有
.module.(le/c)ss
后缀的)才模块化
因为在css module
中可以添加:global
来对样式全局化,所以我们采用第一种方案。
将CSS和JS分开
webpack
打包生成的bundle
,是将css
和js
混合在一起的。我们需要添加webpack
的插件mini-css-extract-plugin
来处理打包后的bundle
,将css
和js
分开。
mini-css-extract-plugin
和style-loader
不能共用,在dev
模式下style-loader
更具优势。所以这里判断下环境。在dev
模式下使用style-loader
;在prod
环境下使用mini-css-extract-plugin
。入口文件的css
引用需要我们用html-webpack-plugin
去自动引入,加入了html-wbpack-plugin
以后,需要指定这个插件从对应的模板html
生成新的index.html
。
mini-css-extract-plugin
支持css
的按需加载并且无需配置,这里额外安装了clean-webpack-plugin
来清除每次打包前,上一次打包遗留的bundle
。
npm install --save-dev mini-css-extract-plugin html-wbpack-plugin clean-webpack-plugin
压缩CSS
npm install css-minimizer-webpack-plugin --save-dev
浏览器兼容CSS
配置浏览器兼容需要提供.browserslistrc
文件,这个文件主要是提供浏览器基准信息(查一下资料),像postcss
、babel
这类工具都需要参考这些浏览器信息。
npm install --save-dev postcss-loader postcss postcss-preset-env
说完less、css的处理,接着说图片的处理,图片的处理我们直接使用webpack的asset module功能(查一下资料)。
整个配置
<!-- index.html -->
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>从0开始搭建一个工程</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
<!-- 添加一个节点 -->
<!-- 不需要再手动引入js 因为我们使用了html-webpack-plugin -->
<div id="app"></div>
</body>
</html>
// index.js
import styles from './index.less' // css module
import temp from './temp.css' // 测试CSS文件是否被postcss处理
import tans from './asset/image/tans.jpeg' //asset module
function change(){
const app = document.getElementById('app')
app.innerHTML = "改变App文案"
app.classList.add(styles.container)
app.classList.add(temp.temp)
const img = document.createElement('img')
img.src = tans
app.parentElement.append(img)
}
change()
// index.less
.container{
width: 100px;
height: 100px;
border: 1px solid black;
background-image: url("./asset/image/tans.jpeg");
background-size: cover;
user-select: none;
}
// webpack.config.js
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const path = require('path')
const {ProgressPlugin} = require('webpack')
const config = require("./config/index")
/**
* 获取当前环境的枚举
* @returns development test production
*/
function getEnv() { return config.ENV_ENUM[process.env.ENV]}
const isProd = getEnv() === config.ENV_ENUM.prod
// webpack插件
const plugins = [
// 进度条
new ProgressPlugin(),
// 打包之前清理dist
new CleanWebpackPlugin(),
// 从模板自动生成html
new HtmlWebpackPlugin({template: 'index.html'}),
// 提取CSS文件 按需加载CSS
new MiniCssExtractPlugin({
filename: "./css/[name].[contenthash].css",
}),
]
module.exports = {
// 环境配置
mode: isProd ? config.ENV_ENUM.prod : config.ENV_ENUM.dev,
// 入口
entry: {
app: path.join(__dirname, 'src', 'index')
},
// 输出
output: {
filename: "[name].[hash].js",
path: path.join(__dirname, 'dist'),
},
module: {
rules: [
// less css 文件的处理
{
test: /.(le|c)ss$/i,
exclude: /node_modules/,
use: [
// 生产环境进行压缩 本地dev直接使用style-loader
isProd ? MiniCssExtractPlugin.loader : 'style-loader',
{
loader: "css-loader",
/**
* 开启css module
* css文件处理之前先加载上一个loader 即postcss-loader 来处理兼容问题
*/
options: {
importLoaders: 1,
modules: true
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
// 自动添加不同浏览器浅醉 并处理新特性
plugins: ['postcss-preset-env']
}
}
},
"less-loader"
],
},
{
test: /.(png|svg|jpg|jpeg|gif|woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: "[file]"
}
},
]
},
plugins,
optimization: {
minimizer: [
// 压缩css
new CssMinimizerPlugin(),
],
},
}
五、搭建开发环境
首先开启source-map
,可选值太多了,上网搜一下。推荐dev
环境eval-cheap-module-source-map
,生产环境nosources-source-map
,在webpack
中配置devtool
:
module.exports = {
devtool: isProd ? "nosources-source-map" : "eval-cheap-module-source-map",
}
每次运行的时候手动npm run start
,再去刷新,很麻烦。所以我们添加上开发工具(本地服务器),webpack
上用的比较多的是web-dev-server
:
npm install --save-dev webpack-dev-server
module.exports = {
devServer: {
static: resolve(__dirname, '../dist'),
compress: true,
hot: true,
port: 8080
},
}
// 修改下package.json中的启动命令
"start": "cross-env ENV=dev webpack serve --open",
重新npm run start
,项目会自动拉起,修改代码浏览器会自动刷新。同时我们debugger
的时候,看到的是我们源码(因为我们配置的devtool
)。
六、配置HMR
webpack-dev-server
是带有模块热替换的,我们要做的就是开启它。开启方法在devServer
配置中添加hot: true
即可。这个时候我们新建一个js
文件,随便写点东西在里面,然后在index.js
中去引用。index.js
中引用的模块更新了,需要用module.hot.accpet
去获取,也就是需要手动去写个钩子。
//添加一个 student.js
export function studentSay(){
console.log('学生', "说", "不想考试")
}
// index.js
import {studentSay} from './student.js'
if (module.hot) {
module.hot.accept('./student.js', function() {
console.log('Accepting the updated student module!');
studentSay()
})
}
项目npm run start
以后,随便更改下student.js
,就会看见热更新了。
至于为什么需要手动写钩子,可以参考下这个大佬文章,不过好在后面我们要用框架,框架实现了loader
来处理,所以不用担心。
七、Tree Shaking
tree shaking
就是“摇树”,把树上多余枯叶摇下来。也就是把工程里面用不到的代码剔除,让打包后的bundle
体积更小。工作原理是静态结构分析,所以只能用于ESM
。对于全局引用的文件,要注意是否有副作用,比如全局css
、全局js
、一些polyfill
、IIFE
文件等,这些就是有副作用的。webpack
需要在设置中排除掉这些文件。
webpack
默认开启了tree shaking
,如果你的文件没有副作用,tree shaking
就会生效,删除不要的代码。但是如果你的文件中只要有一个地方被webpack
检测到认为有副作用,这个时候就不会对这个文件进行tree shaking
。他不会去删除没用的代码,也不符合逻辑和事实。因此,如果你能明确知道哪些文件有副作用,哪些文件没有副作用,可以手动去设置,这也就是官方说的:
因为是默认开启,所以我们暂时不用配置,我们首先去还原下有副作用的场景:新建math-effect.js
和math-no-effect.js
,一个表示有副作用,一个表示没有副作用:
// math-effect.js
// console.log 万一打印打印的这句话是业务需求的呢
// 所以这句话在webpack看来引起了副作用
console.log("aaa")
export function addNum(num1, num2){ return num1 + num2}
// math-no-effect.js
export const name = 'awe'
export function calcNameLen(){ return name.length }
// index.js 中只引用 不进行任何调用
import "./math-effect"
import './math-no-effect'
这个时候我们npm run start
,浏览器里找到app.xxxx.js
,会发现没有删除这两个文件中的任何代码,这是因为开发环境下不会做tree shaking
。
运行npm run build
,检查生成的生产环境的dist
里的app.xxx.js
,会发现只打入了math-effect.js
,这时候就是tree shaking
对没有副作用的文件生效了。
实际项目情况中,会存在全局css
、全局js
、polyfill
、IIFE
等情况,webpack
提供了一个配置,可以在package.json
中显式的配置副作用: sideEffects: true
(所有文件都有)`false(所有都没有)\
['xxx.js, *.css'](符合配置项的才有副作用),因为我们知道我们写的
math-effect.js`其实是没有业务副作用的,我们配置个空的:
// package.json
{
"sideEffects": [],
}
重新打包查看生成的dist
会发现也没有打包math-effect.js
了。
需要注意的是这样写就表示所有文件都没有副作用,需要你很明确知道项目其他文件真的没有副作用。
还有一种配置方法是在loader里面进行配置,这个查下资料就知道了。
一般来说在项目里面不用去特殊处理,因为第一个是全局css
、全局js
、polyfill
、IIFE
等有副作用的文件不会占比很多,第二个是webpack
默认会去tree shaking
那些没有副作用的文件。
其实到这就差不多了,官方的旧版本文档建议再安装个压缩插件,应该算性能优化了。
npm install terser-webpack-plugin --save-dev
// webpack.config.js
module.exports = {
optimization: {
minimize: true,
minimizer: [new TerserPlugin()]
},
}
八、webpack配置文件优化
因为我们是把所有的webpack
配置都写在webpack.config.js
中的,但是有些配置在某些环境下我并不想要(比如我不想开发环境还在压缩我的代码),所以我们把不同环境的配置分成不同文件,然后用webpack-merge
来合并通用配置就行了。
npm install --save-dev webpack-merge
创建webpack.prod.js和webpack.dev.js就够了,test环境只是需要在webpack.dev.js里面做代理的区分,改造后的文件:
// webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
const { ProgressPlugin} = require('webpack')
const config = require("./index")
const { merge} = require('webpack-merge')
const devConfig = require("./webpack.dev")
const prodConfig = require("./webpack.prod")
/**
* 获取当前环境的枚举
* @returns development test production
*/
function getEnv() { return config.ENV_ENUM[process.env.ENV] }
const isProd = getEnv() === config.ENV_ENUM.prod
const commonConfig = {
// 入口
entry: {
app: path.join(__dirname, '../src', 'index')
},
// 输出
output: {
filename: "[name].[hash].js",
path: path.join(__dirname, '../dist'),
},
module: {
rules: [
{
test: /.(png|svg|jpg|jpeg|gif|woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: "[file]"
}
},
]
},
plugins: [
new ProgressPlugin(),
// 打包之前清理dist
new CleanWebpackPlugin(),
// 从模板自动生成html
new HtmlWebpackPlugin({ template: 'index.html' })
]
}
module.exports = () =>{
if(isProd){
return merge(commonConfig, prodConfig)
}
return merge(commonConfig, devConfig)
}
// webpack.dev.js
const config = require('./index')
const { HotModuleReplacementPlugin } = require('webpack')
const path = require('path')
const { merge } = require('webpack-merge')
const isTestEnv = config.ENV_ENUM[process.env.ENV] === config.ENV_ENUM.test
const devConfig = {
mode: config.ENV_ENUM.dev,
devtool: 'eval-source-map',
module: {
rules: [
// less css 文件的处理
{
test: /\.(le|c)ss$/i,
exclude: /node_modules/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: {
localIdentName: '[path][name]__[local]'
}
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['postcss-preset-env']
}
}
},
'less-loader'
]
}
]
},
plugins: [new HotModuleReplacementPlugin()]
}
const getConfig = () => {
if (isTestEnv) return devConfig
return merge(devConfig, {
devServer: {
static: path.join(__dirname, '../dist'),
compress: true,
hot: true,
port: 8080
}
})
}
module.exports = getConfig()
// webpack.prod.js
const config = require('./index')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
mode: config.ENV_ENUM.prod,
devtool: 'nosources-source-map',
module: {
rules: [
// less css 文件的处理
{
test: /\.(le|c)ss$/i,
exclude: /node_modules/,
use: [
// 生产环境进行压缩 本地dev直接使用style-loader
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
/**
* 开启css module
* css文件处理之前先加载上一个loader 即postcss-loader 来处理兼容问题
*/
options: {
importLoaders: 1,
modules: {
localIdentName: '[hash:base64]'
}
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
// 自动添加不同浏览器前缀 并处理新特性
plugins: ['postcss-preset-env']
}
}
},
'less-loader'
]
}
]
},
optimization: {
minimize: true,
minimizer: [
// 压缩css
new CssMinimizerPlugin(),
// 压缩JS
new TerserPlugin()
]
},
plugins: [
new MiniCssExtractPlugin({
filename: './css/[name].[contenthash].css'
})
]
}
// package.json中命令修改"
scripts": {
"start": "cross-env ENV=dev webpack serve --open --config ./config/webpack.config.js",
"build:test": "cross-env ENV=test webpack --config ./config/webpack.config.js",
"build": "cross-env ENV=prod webpack --config ./config/webpack.config.js"
}
九、配置bundle分析
通常打包后需要知道哪些资源占用比较多,这里我们配置一条分析命令,对生产环境打出来的包进行分析,我们采用webpack-bundle-analyzer
这个插件来完成这个工作。
在config
下面新增一个webpack.analyzer.js
的配置,这个配置在prod
的配置基础上增加一个插件配置就可以了:
// scripts中添加一条命令来启动analyzer
// 之所以不在webpack.config里添加这个插件是因为这个加了这个插件以后就会运行一个devserver来显示你的bundle统计
"scripts": {
"analyzer": "cross-env ENV=prod webpack --config ./config/webpack.analyzer.js"
},
//webpack.analyzer.js
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer')
const { merge} = require('webpack-merge')
const getConfig = require("./webpack.config")
const analyzerConfig = {
plugins: [
new BundleAnalyzerPlugin()
]
}
// 注意我们优化以后webpack.config是导出的一个函数
module.exports = merge(getConfig(), analyzerConfig)
这个时候我们npm run analyzer
,就会启动一个界面来分析打包后的bundle
的情况,更多关于此插件的信息查看这个地址。
十、配置代码分离
webpack
打包生成的产物叫bundle
,我们写的代码是叫module
,而从module
到bundle
的这个过程中,webpack会生成一个中间产物叫chunk
,整个打包过程是module
到chunk
到bundle
的过程。chunk
意思是块,就好比是造房子用的砖一样,代码的某处逻辑用到了“砖”,只去加载这部分就行了。把代码拆分成比较小的chunk
,有需要用到某一个chunk
的时候才去按需引用,这就是代码分离的由来,代码分离使得我们能获得更小更细的bundle
,然后可以按需加载或者并行加载。
代码分离有三种方式:动态引入、入口起点、防止重复等方式。webpack
动态引入需要反人类书写代码,不太能让人读懂;入口起点分离只是增多了入口,防止不了重复引入的问题;所以我们采用防止重复这种方式,去抽取公共依赖为一个bundle
就可以了。
防止重复里面又可以分两种:一种是入口依赖的方式,在每个入口下面指定依赖的项,然后配置依赖的项的具体内容,详见这个地址;第二种是通过SplitChunksPlugin
插件来完成,这个插件被集成在webpack
里了,能够拆分一些重复的依赖,但是能够自动拆分的也不是很多:
我们这里就配置一个很简单的就行了,要搞得够好,需要去深入研究。
// webpack.prod.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
},
},
}
十一、修改为TS项目
TypeScript
主要有两个功能:一是提供类型扩展、进行静态检查(强类型语言前端真的该去写写,比如java
),这个功能我们只需要开发依赖中安装上typescript
包就可以了;二是提供编译能力(tsc
),将高版本语法编译为低版本语法以兼容不同时代浏览器或者其他运行环境。因此第二个能力也就和babel
形成了竞品。
babel
相对于tsc
来说,生态要丰富一些,编译产物也更细腻一些,可以去查些资料对比一下(简单甩一篇文章。
所以我们只用TS
的静态检查和类型扩展,对于编译我们还是用babel
,后面会做详细配置。
npm i --save-dev typescript @types/node
因为我们想把webpack
的文件配置也改为TS
,所以我们还需要装上webpack
相关的types
包。并且我们要直接运行TS
(webpack.config.ts
等),所以还需要要装上ts-node
。
npm install --save-dev ts-node @types/webpack @types/webpack-dev-server
添加tsconfig.json
第一步我们在项目中添加上tsconfig.json
,这样从定义的角度上,项目就已经是TS项目了。再去改造我们原来写的文件(只注释了几个比较重要的,没用过TS
的建议还是先去看看)。
{
"compilerOptions": {
"noEmit": true, // 只做静态类型 不编译内容
"module": "esnext",
"target": "es5",
"lib": [
"dom",
"esnext"
],
"baseUrl": ".",
"sourceMap": true, // ts中启用source-map必须配置
"allowJs": true, // 也允许写js文件
"checkJs": true, // 允许对js文件做检查
"noImplicitAny": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"incremental": true,
"isolatedModules": true
},
"ts-node": { // ts-node对module的重载 后面会讲到
"compilerOptions": {
"module": "CommonJS"
}
},
// 起作用的范围
"include": [
"src/**/*",
"config/**/*",
],
// 排除哪些
"exclude": [
"node_modules",
"build",
"dist",
"scripts",
]
}
webpack添加处理ts
第二步改造我们的文件,包括一些业务配置文件、环境配置文件、webpack
配置文件等。
需要注意的两个地方:
我们项目变成TS
项目以后,有些自定义变量比如process.env.ENV
这种,是检查不到定义的,以及像.less
、.css
、.png
等资源文件,从定义上TS
不认为是模块。所以我们需要写个.d.ts
来扩展(一般是在src
下写个typing.d.ts
, cra
是src/react-app-env.d.ts
)。
因为我们把webpack
的配置文件改为了.ts
,所以需要用ts-node
去运行他。ts-node
只是个node上能运行.ts
的扩展,并且node
环境是CommonJS
规范。在tsconfig.json
中我们指定的module
是esnext
,所以需要对ts-node
重载一个配置(见上面tsconfig.json
中ts-node
的配置),如果不做这一步,ts-node
看见配置里写的import {xxx} from 'xxx'
语法是识别不出来到底啥意思的,根本不知道啥玩意。
这里我把webpack
配置也顺便调整了一下。
// src/typing.d.ts
// 图片上的typing.d.ts代码有一些问题
// 下面是修正后的
declare module '*.less'
declare module '*.css'
declare module '*.png'
declare module '*.svg'
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.gif'
declare module '*.bmp'
declare module '*.tiff'
declare namespace NodeJS {
interface ProcessEnv {
ENV: "dev" | "test" | "prod";
}
}
// config/index.ts
// 环境枚举
export const ENV_LIST = {
DEV: "dev",
TEST: "test",
PROD: "prod"
}
// config/webpack.analyzer.ts
import {BundleAnalyzerPlugin} from 'webpack-bundle-analyzer'
import { merge } from 'webpack-merge'
import {Configuration} from 'webpack'
import prodConfig from './webpack.prod'
const analyzerConfig: Configuration = {
plugins: [
new BundleAnalyzerPlugin()
]
}
export default merge<Configuration>(prodConfig, analyzerConfig)
// config/webpack.common.ts
import {CleanWebpackPlugin} from 'clean-webpack-plugin'
import HtmlWebpackPlugin from 'html-webpack-plugin'
import { join} from 'path'
import {Configuration, ProgressPlugin} from 'webpack'
const commonConfig: Configuration = {
// 入口
entry: {
app: join(__dirname, '../src', 'index')
},
// 输出
output: {
filename: "[name].[chunkhash].js",
path: join(__dirname, '../dist'),
},
module: {
rules: [
{
test: /\.(png|svg|jpg|jpeg|gif|woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: "[file]"
}
},
]
},
plugins: [
new ProgressPlugin(),
// 打包之前清理dist
new CleanWebpackPlugin(),
// 从模板自动生成html
new HtmlWebpackPlugin({ template: 'index.html' })
]
}
export default commonConfig
// webpack.dev.ts
import { HotModuleReplacementPlugin, Configuration } from 'webpack'
import { join } from 'path'
import { merge } from 'webpack-merge'
import commonConfig from './webpack.common'
// in case you run into any typescript error when configuring `devServer`
import 'webpack-dev-server'
//https://github.com/DefinitelyTyped/DefinitelyTyped/issues/27570
const devConfig: Configuration & { devServer: { [key: string]: any } } = {
mode: 'development',
devtool: 'eval-source-map',
devServer: {
static: join(__dirname, '../dist'),
compress: true,
hot: true,
port: 8080
},
module: {
rules: [
// less css 文件的处理
{
test: /\.(le|c)ss$/i,
exclude: /node_modules/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: {
localIdentName: '[path][name]__[local]'
}
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['postcss-preset-env']
}
}
},
'less-loader'
]
}
]
},
plugins: [new HotModuleReplacementPlugin()]
}
export default merge<Configuration>(commonConfig, devConfig)
// webpack.prod.ts
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'
import TerserPlugin from 'terser-webpack-plugin'
import { Configuration } from 'webpack'
import commonConfig from './webpack.common'
import { merge } from 'webpack-merge'
const prodConfig: Configuration = {
mode: 'production',
devtool: 'nosources-source-map',
module: {
rules: [
// less css 文件的处理
{
test: /\.(le|c)ss$/i,
exclude: /node_modules/,
use: [
// 生产环境进行压缩 本地dev直接使用style-loader
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
/**
* 开启css module
* css文件处理之前先加载上一个loader 即postcss-loader 来处理兼容问题
*/
options: {
importLoaders: 1,
modules: {
localIdentName: '[hash:base64]'
}
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
// 自动添加不同浏览器前缀 并处理新特性
plugins: ['postcss-preset-env']
}
}
},
'less-loader'
]
}
]
},
optimization: {
splitChunks: {
chunks: 'all'
},
minimize: true,
minimizer: [
// 压缩css
new CssMinimizerPlugin(),
// 压缩JS
new TerserPlugin()
]
},
plugins: [
new MiniCssExtractPlugin({
filename: './css/[name].[contenthash].css'
})
]
}
export default merge<Configuration>(commonConfig, prodConfig)
"scripts": {
"start": "cross-env ENV=dev webpack serve --open --config ./config/webpack.dev.ts", "start:prod": "cross-env ENV=prod webpack serve --open --config ./config/webpack.dev.ts",
"build:test": "cross-env ENV=test webpack --config ./config/webpack.prod.ts",
"build": "cross-env ENV=prod webpack --config ./config/webpack.prod.ts",
"analyzer": "cross-env ENV=prod webpack --config ./config/webpack.analyzer.ts"
},
其他.js
文件也挨着改为.ts
就好了,给这些文件添加一些ts
的类型,不要让.ts
文件只改一个后缀名为.js
就能继续运行(后面会用来验证,这一步需要做)。一些配置文件,比如根目录下以.js
结尾的配置文件是不能更改的。
这个时候我们运行npm run start
,但是发现项目是跑不起来的:
项目找不到src
下的index
,因为webpack
默认不支持TypeScript
,他不认识.ts
。在官方的模块和模块解析(这里要去看下)也有说明。
所以这个时候我们首先要做两个步骤:
第一个步骤是添加解析的扩展名,让webpack
能够在找我们引用的模块的路径时,能够找到,所以我们在webpack.common.ts
中加上配置:
resolve: { extensions: [ '.tsx', '.ts', '.jsx', '.js', '.less', '.css' ],},
这个时候如果我们在引用的地方去加上.ts
后缀名,而不是省略后缀名,会报个错误:
这是一个很有意思的问题,谷歌搜索TS 2691
,你会发现官方开摆这里,这个没有去修,意思就是让我们省略后缀名。行吧,那就摆烂吧~
项目到这里以后npm run start
发现还是跑步起来,因为webpack
认识.ts
了,但是不知道怎么处理,上一步骤只是加了个解析规则而已,因此有了我们第二步。
第二个步骤是我们需要让webpack
知道怎么去处理.ts
。回顾webpack
的原理可以知道,我们需要添加loader
让webpack
能处理自己不认识的资源,所以我们需要添加处理.ts
的loader
。前面也说了,有tsc
和babel
两个选择,我们选择的是babel
,下一节我们就配置babel-loader
。
十二、添加babel
最开始的时候,babel
只是处理js
,没有处理.ts
的能力,所以那个阶段很难受,混用babel
和TS
,然后增加各种配置。先使用tsc
让ts
转为js
,再用babel
处理成低版本js
。到babel7
以后,解决了这个问题,解决办法就是babel
直接抹去所有的ts
,还是按照js
那样编译。babel
本身做编译,所以他们的思想是你TS
做好你自己的类型定义、静态检查就完事了,到编译代码这一步的时候,我也不去管你的类型啊什么的,全部去掉后按js
来编译就行了。
npm install --save-dev babel-loader @babel/core @babel/preset-env @babel/preset-typescript
项目根目录中添加babel.config.json
// babel.config.json
// preset-typescript 是官方针对ts的一个推荐集合
// preset-env 是利用.browserslistrc针对目标环境来生成对应的低版本代码 可以查下资料
{ "presets": ["@babel/preset-typescript", "@babel/preset-env"]}
这个时候我们npm run start
,项目再一次跑起来了,当然你可以再装一些babel
的plugin
,来继续优化。
十三、添加React
安装React
依赖包,React
把核心和渲染环境进行了区分,核心包为react
(叫react-core
可能语义上更好一点),渲染环境包为react-dom
、react-native
等,我们这个demo
是开发web
,所以我们使用react-dom
。
npm i react react-dom
npm i -D @types/react @types/react-dom
然后我们改造项目,将根目录上的index.ts
改为app.tsx
,index.less
改为global.less
,去除掉前面验证功能后留下的冗余文件,增加views
视图文件夹并写一个界面welcome
,目录结构:
这里着重强调下.tsx
扩展语法和vue
那种模板写法的区别,请明白这一点tsx
是扩展语法,语法。这里搞清楚了后面就会很自然。
//app.tsx
import ReactDOM from 'react-dom'
import React from 'react'
import './global.less'
import Welcome from './views/welcome'
ReactDOM.render(
<React.StrictMode>
<Welcome />
</React.StrictMode>,
document.getElementById('app')
)
// src/welcome/index.tsx
import React, { FC } from 'react'
import styles from './index.less'
// index.less中随便写样式
const Welcome: FC = () => {
return <div className={styles.welcome}>欢迎 主页</div>
}
export default Welcome
配置tsconfig.json
和babel.config.json
支持React
:
//tsconfig.json 在compilerOptions添加jsx转换
{
"compilerOptions": {
"jsx": "react-jsx",
}
}
// 添加react的babel集
npm install --save-dev @babel/preset-react
// 配置babel.config.json
{
"presets": ["@babel/preset-react", "@babel/preset-typescript", "@babel/preset-env"]
}
修改webpack
的配置支持tsx
、jsx
,修改入口为app.tsx
:
// 入口 修改为app
entry: {
app: join(__dirname, '../src', 'app')
},
// 添加.tsx .jsx扩展
resolve: {
extensions: [ '.ts', '.tsx' , '.js','.jsx', '.less', '.css' ],
},
module: {
rules: [
{
// 添加扩展tsx jsx扩展 添加presets
test: /\.(tsx?|jsx?)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ['@babel/preset-react', '@babel/preset-env', '@babel/preset-typescript']
}
}
},
]
},
十四、添加UI库
UI
库根据自身选择就可以了,我们拿用的比较多的antd
来举例,打开他们官网的指导文档。
注意的是有两个问题:一是样式文件,antd
说是直接引用,但是我们在配置webpack
对less
、css
文件处理的时候主动忽略了node_modules
,所以需要在webpack
中单独处理;二是antd
这种组件库的样式文件都是编译好了的,不适用于我们配置的css module
,所以单独配置的时候要去掉css module
:
//webpack.dev.ts 和webpack.prod.ts中修改一下less-loader配置
{
loader: "less-loader",
options: {
lessOptions: {
javascriptEnabled: true
}
}
}
// wepack.common.ts中单独处理node_modules中的样式文件
{
test: /\.(le|c)ss$/i,
include: /node_modules/,
use: [
'style-loader',
{
loader: "css-loader",
options: {
importLoaders: 1,
}
},
{
loader: "less-loader",
options: {
lessOptions: {
javascriptEnabled: true
}
}
}
]
},
// app.tsx中引入antd的样式文件
import 'antd/dist/antd.less';
十五、添加状态管理
这里我们就以redux
为例,当然你有其他喜欢的状态管理库也可以添加其他的。
添加redux
、react-redux
、@reduxjs/toolkit
:redux
是状态管理库,可以用于react
,可以用于vue
,可以用于原生js
获者其他框架。react-redux
是为了方便在react
中使用redux
,而开发的一个中间件。又因为redux
写法有点累赘,遂开发了@reduxjs/toolkit
(这东西有点像dva
,人称小dva
)来简化写法,就是这么回事。
这里提一个以前说的,如果包是用ts
写的,就会自带类型文件,如果不是,再去下载@types
对应的包。
npm i redux react-redux @reduxjs/toolkit
安装好以后我们在src
下新建文件夹store
,里面新建index.ts
、rootReducer.ts
和我们的业务模块state
,这里我们就举个用户模块例子(注意这里需要一定的react-redux
和@reduxjs/toolkit
基础,如果写着费劲,就先去简单看一下。)
// state/index.ts
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './rootReducer';
const store = configureStore({
reducer: rootReducer,
});
export type AppDispatch = typeof store.dispatch;
export const dispatch = store.dispatch;
export default store;
// state/rootReducer.ts
import { combineReducers } from '@reduxjs/toolkit';
import userReducer from './user';
const rootReducer = combineReducers({
user: userReducer,
});
export type RootState = ReturnType<typeof rootReducer>;
declare module 'react-redux' {
// 为了方便在业务中能直接读到对应业务模块state类型
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface DefaultRootState extends RootState {}
}
export default rootReducer;
// state/user.ts
import type { PayloadAction } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit'
export interface UserStateProps {
userId: string
name: string
phone: string
}
// 初始化的值
const initState = (): UserStateProps => {
return {
userId: '',
name: '',
phone: ''
}
}
const userSlice = createSlice({
name: 'user',
initialState: initState(),
reducers: {
init(state) {
const initS = initState()
Object.keys(initS).forEach((key) => {
state[key] = initS[key]
})
},
setUserInfo(state, action: PayloadAction<Partial<UserStateProps>>) {
const inputState = action.payload
Object.keys(inputState).forEach((key) => {
state[key] = inputState[key]
})
}
}
})
export const { init, setUserInfo } = userSlice.actions
export default userSlice.reducer
然后在React
中挂在store
,改造下app.tsx
:
// src/app.tsx
import ReactDOM from 'react-dom'
import React from 'react'
import store from './store'
import './global.less'
import 'antd/dist/antd.less'
import Welcome from './views/welcome'
import { Provider } from 'react-redux'
ReactDOM.render(
<React.StrictMode>
// 挂载store
<Provider store={store}>
<Welcome />
</Provider>
</React.StrictMode>,
document.getElementById('app')
)
业务组件上我们测试一下:
import { Button, message } from 'antd'
import React, { FC, Fragment } from 'react'
import styles from './index.less'
import { useDispatch, useSelector } from 'react-redux'
import { setUserInfo } from '../../store/user'
const Welcome: FC = () => {
const dispath = useDispatch()
const { name } = useSelector((state) => state.user)
return (
<Fragment>
<Button
type='primary'
danger
onClick={() => {
dispath(setUserInfo({ name: 'awefeng' }))
message.success('登录成功')
}}
>
登录 awefeng
</Button>
<div className={styles.welcome}>欢迎: {name}</div>
</Fragment>
)
}
export default Welcome
可以看到设置读取都木得问题,并且reduxdev
也能看见过程。
十六、添加Router
前端路由在我公众号里面以前已经说过一次,react-ruoter
也分为了核心和运行环境,因为我们主要是在浏览器中使用,例子中我们就安装react-router-core
和react-router-dom
。但是今天我不准备说这里,因为路由版块比较大,涉及到配置式书写、权限控制、图标、layout
菜单、懒加载等。所以后面单独开一个系列写项目中的router设计,到时候就用react-router
,所以在这里先埋个坑。
react-router
的基础使用也比较简单,可以看看他们官网。
十七、添加请求处理文件
添加一个统一请求文件用来处理请求,一般会选取某一个库然后结合业务,写入一定的处理逻辑,这里我们选用axios
来写一个简单的例子:
npm install axios
src
目录下新建文件夹request
,新建index.ts
。我这里就直接贴书写步骤,然后给个简单的例子文件。
- 首先写一个大体的空心架子,请求的时候我们填入url、填入请求方法、查询参数、数据、其他配置等。
- 然后我们去完善传入的axios的配置,这个时候我们也需要加上类型。
- 根据前后端对齐的API规范文档去处理axois的返回。
- 继续完善config传入的参数,对请求进行完善(包括鉴权、headers、超时、拦截、取消、并发等)。
- 最后根据不同的环境(开发、测试、预发、线上)抽离出不同的配置文件。
- 优化点是可以抽出多个axios实例,来供不同的场景调用。
// src/request/index.ts
// 一个简单的例子 不一定准确
import axios from 'axios';
import type { Method, AxiosRequestConfig, AxiosResponse, } from 'axios';
import { notification } from 'antd';
const fetch = <R>(
url: string,
{
method,
baseUrl = '',
params = undefined,
data = undefined,
then = undefined,
...others
}: { method: Method; baseUrl?: string; params?: Object; data?: any; then: Function, [key: string]: any },
): Promise<R> => {
/**
* 1. 是否有自定义的header 或者其他从config传入的配置
* 从这里进行完善
*/
let headers = {
'Content-Type': 'application/json',
//TODO 鉴权?
};
if (others.headers) {
headers = Object.assign(headers, others.headers);
}
// 2. 定义请求的Config
const requestConfig: AxiosRequestConfig = {
method,
url,
timeout: 10000,
data,
headers,
...others,
};
// 3. 根据环境和传参决定baseurl
// 优化点 抽出不同的axios实例
if (baseUrl) {
requestConfig.baseURL = baseUrl;
} else {
// 开发环境根据 devServer proxy来替换
// 其他环境从配置读取
if (process.env.ENV !== ENV.DEV) {
requestConfig.baseURL = config.baseUrl;
}
}
// 如果是自己传入了后续处理方法 则用后续的处理方法
if (then) {
return axios(requestConfig).then(then);
}
return axios(requestConfig)
.then((res) => {
return new Promise((resolve, reject) => {
// 根据API规范文档进行处理
const { code } = res.data;
if (code === 0 ) {
resolve(res.data as R);
} else {
reject(res);
}
});
})
.catch(async (res: AxiosResponse) => {
// 统一的错误请求处理
// TODO 登录过期 API规范文档规定的报错code 错误兜底 等
const { status, data } = res;
if (status === 401) {
await new Promise<void>((resolve) => {
notification.error({ message: data?.msg ?? '鉴权过期,请重新登录!' });
setTimeout(() => {
resolve();
}, 2000);
}).then(logOut);
} else {
// TODO 其他情况
// 抛出业务错误信息
throw new Error(data?.msg ?? '请求错误~,服务器开小差了');
}
});
};
export default fetch;
然后我们请求的时候在src
下建好api
文件夹统一归档,写一个请求例子:
// src/api/user.ts
import fetch from '../request/index'
// 获取用户信息
export function getUserInfo(
userId: string
): Promise<{ data: { userId: string; name: string; blabala: any } }> {
return fetch('/user/info', { method: 'GET', params: { userId } })
}
十八、统一npm源、版本
统一npm
源很简单,工程目录下添加.npmrc
,考虑到CI/CD
中runner
打包以及墙的原因等,建议还是用taobao
镜像:
.npmrcregistry=https://registry.npm.taobao.org/
@company:registry=https://registry.npm.company.com/
node
的统一是很有必要的,node
的更新,不管大小,可能都会有改动;某一些依赖包,可能会要求node
版本,为了保证项目在每个人(甚至服务器)上的一致,所以保持node
统一、npm
版本统一是很有必要的。关系到安装依赖的规则,可以查一下package-lock.json
,npm
版本的高低,package-lock.json
的内容可能是不一样的(是不是经常出现这种情况:为什么同事安装依赖跑得好好的,到你电脑上就不行😶)。
至于怎么统一,你可以写脚本约束,可以强制规定(或者你就摆烂,不管这些,能跑就行),nvm
管理起来。
十九、路径别名
这个其实应该在webpack
配置那一节就顺便做了,在webpack
的解析中配置好别名以后还需要在tsconfig.json
中也配置好解析路径:
// config/webpack.common.ts
resolve: {
alias: {
"@": resolve(__dirname, '../src'),
"#": resolve(__dirname, '../config')
},
extensions: [ '.ts', '.tsx' , '.js','.jsx', '.less', '.css' ],
},
// tsconfig.json
{
"compilerOptions": {
"paths": {
"@/*": [
"./src/*"
],
"#/*": [
"./config/*"
]
}
}
}
这个时候我们就可在代码中使用了:
// 举个例子
import { setUserInfo } from "@/store/user";
import {ENV_LIST} from "#/index"
二十、添加.editorconfig和.gitignore
.editorconfig
配置文件是对IDE
的编码约束,并且需要IDE
的支持(支持的IDE
以及需要装插件才能支持的IDE
,在官网有详细的展示)。比如我使用的是VS Code
,需要在插件中安装EditorConfig for VS Code
插件,才能使用此配置。
// 根目录下 .editorconfig
# see http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab
git
的忽略文件,配置工程中需要在提交的时候忽略的文件,git
官方针对每种不同的开发环境推荐了不同的配置,可以参考下文档
# see https://github.com/github/gitignore/blob/main/Node.gitignore
/dist
.DS_Store
*.lock
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Dependency directories
node_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
二十一、添加eslint、prettier和stylelint
eslint
主要用来规范代码质量,需要我们自己安装包
npm i --save-dev eslint
先写一个初始配置放在根目录上
// .eslintrc.js
module.exports = {
// 设定当前目录为eslint根目录
root: true,
// 设定eslint的env
env: {
es6: true,
browser: true,
node: true
},
parserOptions: {
ecmaVersion: 6,
sourceType: "module",
jsx: true
},
extends: ["eslint:recommended"],
rules: {
//暂时不写
}
};
我使用的IDE
是VS Code
,需要安装插件ESLint
,在控制台查看eslint server
是否启动成功和配置文件是否报错:
这个时候我们在根目录下新建一个测试用的js
文件,写一点代码,eslint
是能检测到的(这里有个坑是以.
开头的文件是默认忽略了,和node_modules
一样,但是我没在官方找到说明,有兴趣的查一查)。
接着继续修改上面的配置,以支持React
和ts
,给两个参考链接:
npm i -D @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react
在rules
中添加一些自定义规则,修改后的配置如下:
module.exports = {
// 设定当前目录为eslint根目录
root: true,
// 设定eslint的env
env: {
es2021: true,
browser: true,
node: true
},
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
},
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'react'],
extends: [
'eslint:recommended',
'plugin:react/recommended',
// 工程里面用了最新react-jsx模式 所以要加这个
'plugin:react/jsx-runtime',
'plugin:@typescript-eslint/recommended'
],
settings: {
react: {
pragma: 'React',
version: 'detect'
}
},
rules: {
// 规则具体含义看下官网
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-inner-declarations': 'warn',
'block-scoped-var': 'error',
'default-case': 'error',
'no-caller': 'error',
'no-eval': 'error',
'array-element-newline': ['warn', { multiline: true, minItems: 4 }],
'comma-dangle': ['error', 'never'],
'max-len': [
'error',
{
code: 100,
tabWidth: 2,
comments: 80
}
],
semi: ['error', 'never'],
'padding-line-between-statements': [
'error',
// 给代码加上必要的空行风格
{
blankLine: 'always',
prev: ['const', 'let', 'var', 'block', 'block-like'],
next: '*'
},
{
blankLine: 'always',
prev: ['import'],
next: ['const', 'let', 'var', 'block', 'block-like', 'expression']
},
{ blankLine: 'never', prev: ['import'], next: ['import'] },
{ blankLine: 'never', prev: ['const'], next: ['const'] }
]
}
}
配置好忽略文件:
# .eslintignore
# 目前就三个就可以了
# 也可以不写node_modules eslint会默认忽略
node_modules
/dist
/src/asset
prettier
前面也说了作用了,我们需要在eslint
中融入prettier
,并且让和prettier
冲突的都以prettier
为准,给个参考链接:
npm i -D eslint-plugin-prettier prettier eslint-config-prettier
改造eslint
支持prettier
:
// 只贴改动部分
module.exports = {
// 插件新增prettier
plugins: ["@typescript-eslint", "react", "prettier"],
// 按官方推荐的新的方式配置
// 使用 "plugin:prettier/recommended"
extends: [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
]
}
新建.prettier.js
,来配置prettier
,配置选项见官方文档
// .prettier.js
module.exports = {
printWidth: 80, //一行的字符数,如果超过会进行换行,默认为80
tabWidth: 2, //一个tab代表几个空格数,默认为80
useTabs: false, //是否使用tab进行缩进,默认为false,表示用空格进行缩减
singleQuote: true, //字符串是否使用单引号,默认为false,使用双引号
jsxSingleQuote: true, // 在JSX中是否使用单引号
semi: false, //行位是否使用分号,默认为true
trailingComma: 'none', //是否使用尾逗号,有三个可选值"<none|es5|all>"
bracketSpacing: true, //对象大括号直接是否有空格,默认为true,效果:{ foo: bar }
parser: 'typescript' //代码的解析引擎
}
同样配置好忽略文件:
# .prettierignore
package.json
/dist
.DS_Store
.eslintignore
*.png
.editorconfig
.gitignore
.prettierignore
添加styleint
这个留给你们自己去做了~(写不动了~)
二十二、格式化
配置格式化命令
完成上面的工作后,工程上代码规范有了,但是只是提示,更改的时候需要自己运行命令,因此我们在package.json
中添加一条命令,用来修复eslint
检查出的问题:
"scripts": {
"lint": "eslint --fix --ext .js,.jsx,.ts,.tsx ."
},
这里有个坑就是eslint
对于node
的版本是有要求的,如果你运行这条命令出错的话,先看看node
版本对不对,这也是上面我们说要控制版本的原因之一。
运行完命令以后,你会发现有些地方可能还是没有修复,这个时候鼠标指着报错处查看原因。有一种情况是prettier
的规则和我们自己在rules
写的eslint
规则冲突了。
// 比如这条规则 和prettier是冲突
'array-element-newline': ['warn', { multiline: true, minItems: 4 }]
原因是这样的:prettier
的某一些配置和我们自己写的配置冲突,然后prettier
是不会以我们自己写的优先的(也就是大家说的prettier
有偏见),目前官方给的解决办法就是你自己在上面加个忽略(开摆:啊对对对,这个可以查一下,我记得有人github
提了issue
)。所以,如果真正的要做到自己或者团队定制化,prettier
只可以作为个参考。
IDE自动格式化
这个就需要基于不同的IDE
进行不同的配置了。有些IDE
可能默认支持,不需要配置,检测到你有eslint
配置就会按照配置进行格式化,有些IDE
可能还是需要你写个工作区配置文件。我拿VS Code
来举例,VS Code
是需要配置的,工作区配置有两种方法:第一种是根目录下新建.vscode
,添加settings.json
手动编写;第二种是菜单-code-首选项-设置-工作区
,进行勾选设置。
// .vscode/settings.json
{
"eslint.alwaysShowStatus": true,
"editor.codeActionsOnSave": {
"source.fixAll": true
}
}
这个时候我们在写完代码保存的时候,就会自动格式化了。
还有个优化点是,每次提交的时候不需要再把全部文件去格式化一下,只需要格式化我们有改动的文件即可,这一部分留个作业自己去做(lint-staged
)。
二十三、Git约束
如果团队成员的IDE
需要手动配置或者不支持,或者团队成员也没有配置。这个时候他写的代码可能还没有经过格式化,就推到远端仓库去了。为了避免这种情况出现,需要在git
提交之前运行我们的命令来格式化代码,并且能够做到规范提交信息是最好的。基于这两点需求,我们召唤“哈士奇”,husky
是一个git
钩子库,可以在提交前用来lint
代码,规范提交信息、跑测试等。husky
在7.0版本以后采用的是配置文件脚本方式,我们用官方的安装指引:
npx husky-init
修改pre-commit
钩子里的内容,需要在pre-commit
里完成重新格式化并且重新提交工作,所以pre-commit
脚本改造下,对应的我们的执行命令也改造下:
// .husky/pre-commit 简单例子仅供参考
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
echo '1. 执行eslint'
npm run lint
echo '2. 执行prettier'
npx prettier --write
echo '执行完成,重新添加文件'
git add -A
// package.json
{
"scripts": {
"lint": "npm run lint:js && npm run lint:style",
"lint:js":"eslint --fix --ext .js,.jsx,.ts,.tsx .",
"lint:style": "echo '写不动了,此处配置styleling命令去格式化less css'",
}
}
这时候我们走一遍提交流程:
第一个工作格式化完成后,第二个工作我们需要检测提交信息,防止乱写,保持统一。同样的,按照官方给的示例添加一个commit-msg
钩子:
npx husky add .husky/commit-msg
然后我们这里编辑commit-msg
脚本,去检测提交信息是否符合我们的需求:
// /husky/commit-msg
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# 本人bash脚本不太熟
# 所以用node运行我们写的限制脚本
node ./commit-msg.js $1
// 根目录 commit-msg.js
/**
* 根目录下 commonjs环境
* chalk5是ESM commonjs中需要使用chalk4.x
* npm i -D chalk@4.1.2
* 记得eslint下忽略这个文件
*
* 我抄的umijs fabric的检测
* 他也是抄的别人的
*/
const chalk = require("chalk")
const msgPath = process.argv[2]
const msg = require('fs').readFileSync(msgPath, 'utf-8').trim()
const commitRE =
/^(((\ud83c[\udf00-\udfff])|(\ud83d[\udc00-\ude4f\ude80-\udeff])|[\u2600-\u2B55]) )?(revert: )?(feat|fix|docs|UI|refactor|⚡perf|workflow|build|CI|typos|chore|tests|types|wip|release|dep|locale): .{1,50}/
if (!commitRE.test(msg)) {
console.error(
` ${chalk.bgRed.white(' ERROR ')} ${chalk.red(`提交日志不符合规范`)}\n\n${chalk.red(
` 合法的提交日志格式如下(emoji 选填):\n\n`,
)}
${chalk.green(`💥 feat: 添加了个很棒的功能`)}
${chalk.green(`🐛 fix: 修复了一些 bug`)}
${chalk.green(`📝 docs: 更新了一下文档`)}
${chalk.green(`🌷 UI: 修改了一下样式`)}
${chalk.green(`🏰 chore: 对脚手架做了些更改`)}
${chalk.green(`🌐 locale: 为国际化做了微小的贡献`)}
${chalk.red(`更多提交前缀查看commit-msg.js\n`)}`
)
process.exit(1)
}
总结
到这里我们搭建的就差不多了,圆润圆润就可以使用了,至此整个从零搭建工程就搞定了,整个系列内容可能有出入、有错误、有未配置、疏漏的地方,请海涵、指出,谢谢。
完整的工程目录:
总结下大致步骤和思路:
- 新建文件夹并初始化为npm项目
- 引入打包工具并写一个最最基础的配置文件
- 配置环境以及对应的打包脚本命令
- 不同环境的打包配置文件区分开
- 打包配置文件中添加资源文件处理
- 搭建开发环境(devServer)
- 配置HMR、Tree Shaking、Code Splitting等
- 增加打包后的产物分析(webpack-bundle-analyzer)
- 项目修改为ts项目
- 添加babel-loader处理ts
- 添加react,修改配置以让工程支持tsx、jsx
- 添加UI库
- 添加状态管理
- 添加路由
- 添加请求处理
- 添加工程约束文件
- 重新整理打包配置文件,进行提取优化,抽离配置文件等
- 添加自动构建/自动部署
- 添加一个工程说明文件(工程干什么的、用了哪些技术、怎么启动怎么玩的、提交检查、工作流程、部署说明等等)
这些配置一点都不难,只是需要你知道每种技术是干什么的,然后去找相应的配置就行了。整个从零构建一个工程,除了路由部分,应该都是完成了。我放了个demo
在github
上,大家可以参考一下:
欢迎star(多点一点⭐️吧,各位老爷)
欢迎关注我的公众号:咪仔和汤圆
我是”有两只臭猫“,一个养了两只猫,和大家一起学习前端的朋友~