概述:
本文旨在通过从0开始搭建一套完整的React开发框架来掌握如webpack
、react
、ts
、loader
、babel
、eslint
、prettier
、husky
、lint-staged
等各个部分基础是如何协同编译开发的,进而去了解creat-react-app
之类的CI都做了哪些事情。
webpack中文网
1 初始化项目
npm init
yarn init & npm init 都是ok的,都是为了初始化一个package.json文件
2 构建核心打包环境
npm i webpack webpack-cli webpack-dev-server -D
-
依赖说明
| 模块名 | 说明 | 版本 |
| — | — | — |
| webpack | 模块化打包工具,打包代码时的核心依赖 | ^5.72.1 |
| webpack-cli | 支持在命令行中执行webpack的工具 | ^4.9.2 |
| webpack | 开启本地开发服务器 | ^4.9.0 |
| webpack-merge | 合并webpack-config文件 | ^5.8.0 | -
webpack.config.js配置文件
此文件是webpack默认的配置文件,也可以在命令行中通过–config或者-c指定配置文件,一般项目里会区分production、development的配置文件,这里我们配置一个公共base/dev/prod,之后用命令区分。
webpack.config.base.js
const path = require('path');
module.export = {
// 入口文件
entry: {
main: path.resolve(__dirname, "./index.js"),
},
// 输出
output: {
// 文件名称
filename: "[name].[contenthash].js",
// 输出目录
path: path.resolve(__dirname, "./dist"),
// 每次编译输出的时候,清空dist目录 - 这里就不需要clean-webpack-plugin了
clean: true,
// 所有URL访问的前缀路径
publicPath: "/",
},
resolve: {
// 定义了扩展名之后,在import文件时就可以不用写后缀名了,会按循序依次查找
extensions: [".js", ".jsx", ".ts", ".tsx", ".json", ".css", ".less"],
// 设置链接
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
module: {
rules: [
{
// 匹配js/jsx
test: /\.jsx?$/,
// 排除node_modules
exclude: /node_modules/,
use: {
// 确定使用的loader
loader: "babel-loader",
// 参数配置
options: {
presets: [
[
// 预设polyfill
"@babel/preset-env",
{
// polyfill 只加载使用的部分
useBuiltIns: "usage",
// 使用corejs解析,模块化
corejs: "3",
},
],
// 解析react
"@babel/preset-react",
],
// 使用transform-runtime,避免全局污染,注入helper
plugins: ["@babel/plugin-transform-runtime"],
},
},
},
],
},
}
webpack.config.dev.js
// merge,合并两个或多个webpack配置文件
const { merge } = require("webpack-merge");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const path = require("path");
const isProd = process.env.NODE_ENV === "prod";
// 导入公共配置文件
const webpackConfigBase = require("./webpack.config.base");
// dev环境下相关配置
module.exports = merge(webpackConfigBase, {
// 指定环境
mode: "development",
// 输出source-map的方式,增加调试。eval是默认推荐的选择,build fast and rebuild fast!
devtool: "eval",
// 本地服务器配置
devServer: {
// 启动GZIP压缩
compress: true,
// 设置端口号
port: 3000,
// 代理请求设置
proxy: {
"/api": {
// 目标域名
target: "http://xxxx.com:8080",
// 允许跨域了
changeOrigin: true,
// 重写路径 - 根据自己的实际需要处理,不需要直接忽略该项设置即可
pathRewrite: {
// 该处理是代码中使用/api开头的请求,如/api/userinfo,实际转发对应服务器的路径是/userinfo
"^/api": "",
},
// https服务的地址,忽略证书相关
secure: false,
},
},
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './public/index.html')
}),
new MiniCssExtractPlugin({
// 输出的每个css文件名称
filename: isProd ? "[name].[contenthash].css" : "[name].css",
// 非入口的chunk文件名 - 通过import()加载异步组件中样式
chunkFilename: isProd ? "[id].[contenthash].css" : "[id].css",
}),
],
module: {
rules: [
{
test: /\.(css|less)$/,
use: [
// 生产环境下直接分离打包css
isProd ? MiniCssExtractPlugin.loader : "style-loader",
{
loader: "css-loader",
},
"less-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
// 浏览器前缀自动补全
plugins: ["autoprefixer"],
},
},
},
],
},
]
}
});
webpack.config.prod.js
const { merge } = require("webpack-merge");
const webpackConfigBase = require("./webpack.config.base");
module.exports = merge(webpackConfigBase, {
// 指定打包环境
mode: "production",
});
- 在package.json中配置命令
scripts: {
"dev": "webpakc serve -c webpack.config.dev.js",
"build": "webpack build -c webpack.config.prod.js"
}
3 添加模板文件
npm i html-webpack-plugin -D
可以新建目录public/index.html创建模板文件
webpack配置如图中:
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
// HTML模板文件
template: path.resolve(__dirname, "./public/index.html"),
// 收藏夹图标
favicon: path.resolve(__dirname, "./public/logo.ico"),
}),
]
// ...
}
4 解析React
- 下载react依赖
npm i react react-dom
- 下载解析react的依赖
npm i babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime @babel/preset-react core-js@3 -D
- 依赖块说明
模块名 | 说明 | 版本 |
---|---|---|
react | 核心代码 | ^18.1.0 |
react-dom | 浏览器端实现 | ^18.1.0 |
babel-loader | 识别ES6语法,编译js | ^8.2.5 |
@babel/core | babel处理的核心逻辑 | ^7.18.2 |
@babel/preset-env | 根据预设的目标值转化js语法,会打包一些polyfill | ^7.18.2 |
@babel/preset-react | 编译jsx语法 | ^7.17.12 |
@babel/plugin-transform-runtime | 按需打包polyfill,解决polyfill会污染全局 | ^7.18.2 |
core-js@3 | polyfill的核心实现 | 3 |
- 添加webpack的loader配置
loader是webpack的文件处理器,让webpack能够处理其他类型的文件,并转化为有效的模块。
webpack配置如图:
module.exports = {
// ...
module: {
rules: [
{
// 匹配js/jsx
test: /\.jsx?$/,
// 排除node_modules
exclude: /node_modules/,
use: {
// 确定使用的loader
loader: "babel-loader",
// 参数配置
options: {
presets: [
[
// 预设polyfill
"@babel/preset-env",
{
// polyfill 只加载使用的部分
useBuiltIns: "usage",
// 使用corejs解析,模块化
corejs: "3",
},
],
// 解析react
"@babel/preset-react",
],
// 使用transform-runtime,避免全局污染,注入helper
plugins: ["@babel/plugin-transform-runtime"],
},
},
}
]
}
// ...
}
- 创建React组件
创建App.jsx文件
import React, { useState } from "react";
export default function App () {
return <div className="app">
<h1>Hello Webpack-React</h1>
</div>;
}
入口文件index.jsx
import React from "react";
// 注意这里最新版的ReactDOM是从client中导出的
import ReactDOM from "react-dom/client";
// 因为设置了extensions,所以可以不加扩展名
import App from './App';
// 创建app根节点
const appEl = document.createElement("div");
// 设置id
appEl.id = "app";
// 追加节点到body中
document.body.appendChild(appEl);
// 最新版本使用的是ReactDOM.createRoot
// 如果使用ReactDOM.render()控制台会报warnning错误
const root = ReactDOM.createRoot(appEl);
// 渲染
root.render(<App />);
运行项目:效果肯定是杠杠的
5 解析CSS和CSS预处理器
CSS在webpack中也是作为一个资源来被识别的,需要配置相关的loader来解析。CSS的预处理器如less/sass/stylus/postcss都可以被loader识别。
- 添加相关依赖
npm i css-loader less less-loader style-loader postcss postcss-loader mini-extract-plugin cross-env autoprefixer css-minimizer-webpack-plugin -D
- 依赖块说明
模块 | 说明 | 版本 |
---|---|---|
css-loader | 解析CSS | ^6.7.1 |
less | less语法 | ^4.1.2 |
less-loader | 解析less | ^11.0.0 |
style-loader | 将解析的CSS追加到head中 | ^3.3.1 |
postcss | css插件,压缩、自动补全css | ^8.4.14 |
postcss-loader | 解析postcss | ^7.0.0 |
mini-extract-plugin | 分离css | ^2.6.0 |
cross-env | 设置环境变量配置 | ^7.0.3 |
autoprefixer | 自动补全css前缀 | ^10.4.7 |
css-minimizer-webpack-plugin | 生产环境,压缩css | ^4.0.0 |
3.webpack.config.dev.js相关配置
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const isProd = process.env.NODE_ENV === "prod";
module.exports = {
// ...
plugins: [
new MiniCssExtractPlugin({
// 输出的每个css文件名称
filename: isProd ? "[name].[contenthash].css" : "[name].css",
// 非入口的chunk文件名 - 通过import()加载异步组件中样式
chunkFilename: isProd ? "[id].[contenthash].css" : "[id].css",
}),
],
module: {
rules: [
{
test: /\.(css|less)$/,
use: [
// 生产环境下直接分离打包css
isProd ? MiniCssExtractPlugin.loader : "style-loader",
{
loader: "css-loader",
},
"less-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
// 浏览器前缀自动补全
plugins: ["autoprefixer"],
},
},
},
],
},
]
}
// ...
}
值得注意的是,在webpack的loader中,加载顺序是从右向左依次处理,css/less的处理顺序是:postcss-loader->less-loader->css-loader->style-loader/MiniExtractPlugin.loader
在生产环境里将css压缩
webpack.config.prod.js
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = merge(webpackConfigBase, {
// ...
optimization: {
minimizer: [
new CssMinimizerPlugin()
]
},
// ...
})
- 在package.json里添加环境变量命令,cross-env解决跨平台环境变量的参数。
"scripts": {
"dev": "cross-env NODE_ENV=dev webpack serve --config config/webpack.config.dev",
"build": "cross-env NODE_ENV=prod webpack build --config config/webpack.config.prod"
},
6 配置TS
- 安装ts和ts-loader
npm i typescript ts-loader -D
值得注意的是,如果已经在使用 babel-loader 来转换代码,则可以使用 @babel/preset-typescript 并让 Babel 处理 JavaScript 和 TypeScript 文件,而不是使用额外的加载器。请记住,与 ts-loader 相反,底层的 @babel/plugin-transform-typescript 插件不执行任何类型检查。
2.安装React类型校验
npm i @types/react @types/react-dom -D
3.配置webpack
module.exports = merge(webpackConfigBase, {
// ...
modeule: {
rules: [
{
test: /\.(ts|tsx)?$/,
use: ['ts-loader']
}
]
}
// ...
})
或者在.babelrc中配置
options: {
//...
presets: ['@babel/preset-typescript']
//...
}
- 增加tsconfig.json文件:tsconfig.json指定了编译项目所需的根目录下的文件以及编译选项。
TypeScript配置
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "es5", "es6", "es7", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": false,
"jsx": "react-jsx",
"downlevelIteration": true
},
// "compilerOptions": {
// "target": "es2015",
// "module": "esnext",
// "jsx": "react-jsx",
// "lib": [
// "esnext",
// "dom"
// ],
// "types": [
// "node",
// "react",
// ],
// "noEmit": true,
// "checkJs": false,
// "moduleResolution": "node",
// "allowSyntheticDefaultImports": true,
// "experimentalDecorators": true,
// "emitDecoratorMetadata": true,
// "skipLibCheck": true,
// "useDefineForClassFields": false,
// "strict": false,
// "noImplicitAny": false,
// "baseUrl": ".",
// "allowJs": true,
// "esModuleInterop": true,
// "forceConsistentCasingInFileNames": true,
// "noFallthroughCasesInSwitch": true,
// "resolveJsonModule": true,
// "isolatedModules": true,
// "paths": {
// "@core": [
// "./packages/shared/core"
// ],
// "@shared/*": [
// "./packages/shared/*"
// ]
// }
// },
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
7 增加Eslint和Prettier
- 配置Eslint
ESLint属于一种QA工具,是一个ECMAScript/JavaScript语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码,这里先简单提一下 ESLint 的工作原理:ESLint 会先通过词法分析器把你写的代码进行拆分(AST),然后再按某种规则组合起来,接着把新组合起来的代码和你写的代码进行比对,如果有差异,就会在控制台提示报错信息 Eslint中文网
npm i eslint -D
让eslint识别TypeScrpit
npm i @typescript-eslint/parser @typescript-eslint/eslint-plugin -D
依赖 | 说明 |
---|---|
eslint | Eslint核心代码 |
@typescript-eslint/parser | ESLint的解析器,用于解析typescript,从而检查和规范Typescript代码 |
@typescript-eslint/eslint-plugin | 这是一个ESLint插件,包含了各类定义好的检测Typescript代码的规范 |
在根目录下新建.eslintrc.js文件用来配置eslint, 一个比较简单的输出如下
module.exports = {
parser: '@typescript-eslint/parser', //定义ESLint的解析器
extends: ['plugin:@typescript-eslint/recommended'],//定义文件继承的子规范
plugins: ['@typescript-eslint'],//定义了该eslint文件所依赖的插件
env:{ //指定代码的运行环境
browser: true,
node: true,
}
}
更多配置->
- 在ts项目中必须执行解析器为@typescript-eslint/parser,才能正确的检测和规范TS代码
- env环境变量配置,形如console属性只有在browser环境下才会存在,如果没有设置支持browser,那么可能报console is undefined的错误
- 增加prettier
在Esllint中,还有很多的规则不能自动修复,而prettier是一个流行的代码格式化的工具,结合ESLint来使用。
npm i prettier eslint-config-prettier eslint-plugin-prettier -D
依赖 | 说明 |
---|---|
prettier | prettier核心插件 |
eslint-config-prettier | 当eslint和prettier规则冲突时,以prettier为准 |
eslint-plugin-prettier | 将prettier作为eslint规则来用 |
在根目录上新建.prettierrc.js文件用来配置prettier, 如下简易版:
module.exports = {
"printWidth": 120,
"semi": false,
"singleQuote": true,
"trailingComma": "all",
"bracketSpacing": false,
"jsxBracketSameLine": true,
"arrowParens": "avoid",
"insertPragma": true,
"tabWidth": 4,
"useTabs": false
};
在.eslilntrc.js上新增配置
module.exports = {
parser: '@typescript-eslint/parser',
extends:[
'prettier/@typescript-eslint',
'plugin:prettier/recommended'
],
settings: {
"react": {
"pragma": "React",
"version": "detect"
}
},
parserOptions: {
"ecmaVersion": 2019,
"sourceType": 'module',
"ecmaFeatures":{
jsx:true
}
},
env:{
browser: true,
node: true,
}
- prettier/@typescript-eslint:使得@typescript-eslint中的样式规范失效,遵循prettier中的样式规范
- plugin:prettier/recommended:使用prettier中的样式规范,且如果使得ESLint会检测prettier的格式问题,同样将格式问题以error的形式抛出
- 使用Eslint和prettier插件的方式可以在开发代码保存或者变更的时候进行自动格式化
安装完这两个插件之后,在项目根目录上新建.vscode文件夹,新建setting.json文件
{
"eslint.enable": true, //是否开启vscode的eslint
"eslint.autoFixOnSave": true, //是否在保存的时候自动fix eslint
"eslint.options": { //指定vscode的eslint所处理的文件的后缀
"extensions": [
".js",
".vue",
".ts",
".tsx"
]
},
"eslint.validate": [ //确定校验准则
"javascript",
"javascriptreact",
{
"language": "html",
"autoFix": true
},
{
"language": "vue",
"autoFix": true
},
{
"language": "typescript",
"autoFix": true
},
{
"language": "typescriptreact",
"autoFix": true
}
]
}
- eslint.options中可以通过configFile属性来执行eslint规范的绝对路径,默认会向上查找,在根路径中指定。
- eslint.validate中必须通过{ language: XXX}的形式来指定typescript和typescriptreact
8 husky和lint-staged构建代码工作流
husky 是一个 Git Hook 工具,它可以在代码提交前允许我们做一些事情,从而防止一些不好的代码被提交上去。 lint-staged 是针对工作区修改的文件,这对我们只希望处理将要提交的文件将会非常有用
npm i husky lint-staged -D
我们需要在代码提交前对代码做一下格式化并且如果代码不符合规范就不让提交,简单的做法就是在husky
的pre-commit
钩子去运行 lint-staged
,lintstaged
主要就干了三件事:
第一件就是调用eslint --fix
修复不合符eslint规范的代码。
第二件prettier --write
美化代码格式。
最后如果都通过了就允许代码commit
packages.json
//...
"scripts": {
"lint": "eslint --ext .tsx,.ts,.js --fix ./src",
"fix": "prettier --write ./src"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*{.ts,.js}":[
"eslint --ext .tsx,.ts --fix ./src",
"prettier --write",
"git add"
]
}
//...
写在最后:真实的场景可能是需要在vue或者react以及其他的框架中具体配置husky、lint-staged、Typescripe等,本文基于一些基础的工作流来总结一个项目相关包一开始是如何配合工作,后续也会就初始项目并且提交更新到GitHub Docu,同时非常感谢您阅读这篇文章,有任何问题或反馈请给我留言,后续本文章也会持续更新。