本文旨在通过从0开始搭建一套完整的React开发框架来掌握如webpack
、react
、ts
、loader
、babel
、eslint
、prettier
、husky
、lint-staged
等各个部分基础是如何协同编译开发的,进而去了解creat-react-app
之类的CI都做了哪些事情。
webpack中文网
npm init
yarn init & npm init 都是ok的,都是为了初始化一个package.json文件
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",
});
scripts: {
"dev": "webpakc serve -c webpack.config.dev.js",
"build": "webpack build -c webpack.config.prod.js"
}
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"),
}),
]
// ...
}
npm i react react-dom
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 |
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"],
},
},
}
]
}
// ...
}
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 />);
运行项目:效果肯定是杠杠的
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()
]
},
// ...
})
"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"
},
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']
//...
}
{
"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"]
}
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,
}
}
在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,
}
安装完这两个插件之后,在项目根目录上新建.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
}
]
}
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,同时非常感谢您阅读这篇文章,有任何问题或反馈请给我留言,后续本文章也会持续更新。