前言

大概在 2019 年,自己搭建 React 开发环境的想法萌芽,到目前为止,公司的很多项目上,也在使用中,比较稳定。为什么要自己造轮子?起初是因为自己并不满意市面上的脚手架。另外,造轮子对于自己也有一些技术上的帮助,学别人二次封装的东西,不如直接使用底层的库,这样也有助于自己系统的学习一遍知识,废话不多说,直接进入正文,如何搭建自己的开发环境。

初始化

创建文件夹并进入:

$ mkdir tristana && cd tristana

初始化 package.json

$ npm init

安装 Webpack

$ npm install webpack webpack-cli --save-dev

创建以下目录结构、文件和内容:

project

tristana|- package.json|- /dist
   |- index.html|- /script
   |- webpack.config.js|- index.html|- /src
   |- index.js

src/index.js

document.getElementById("root").append("React");

index.html && dist/index.html

<!DOCTYPE html><html>
    <head>
        <meta charset="utf-8" />
        <title>tristana</title>
    </head>
    <body>
        <script src="../src/index.js"></script>
        <div id="root"></div>
    </body></html>

script/webpack.config.js

module.exports = {
    mode: "development",
    entry: "./src/index.js",
};

package.json

{    // ...
    "scripts": {        "build": "webpack --mode=development --config script/webpack.config.js"
    },
}

然后根目录终端输入:npm run build

在浏览器中打开 dist 目录下的 index.html,如果一切正常,你应该能看到以下文本:'React'

index.html 目前放在 dist 目录下,但它是手动创建的,下面会教你如何生成 index.html 而非手动编辑它。

Webpack 核心功能

Babel

$ npm install @babel/cli @babel/core babel-loader @babel/preset-env --save-dev

script/webpack.config.js

module.exports = {    // ...
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                loader: "babel-loader",
                exclude: /node_modules/,
            },
        ],
    },
};

.babelrc

在根目录下添加 .babelrc 文件:

{    "presets": ["@babel/preset-env", "@babel/preset-react"]
}

样式

$ npm install style-loader css-loader less less-loader --save-dev

script/webpack.config.js

module.exports = {    // ...    module: {        rules: [
            {                test: /\.(css|less)$/,                use: [
                    {                        loader: "style-loader",
                    },
                    {                        loader: "css-loader",                        options: {                            importLoaders: 1,
                        },
                    },
                    {                        loader: "less-loader",                        lessOptions: {                            javascriptEnabled: true,
                        },
                    },
                ],
            },
        ],
    },
};

图片字体

$ npm install file-loader --save-dev

script/webpack.config.js

module.exports = {    // ...
    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif|jpeg)$/,
                loader: 'file-loader'
            },
            {
                test: /\.(woff|woff2|eot|ttf|otf)$/,
                loader: 'file-loader'
            }
        ],
    },
};

HTML

$ npm install html-webpack-plugin --save-dev

script/webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = {    // ...
    plugins: {
        html: new HtmlWebpackPlugin({
            title: 'tristana',
            template: 'public/index.html'
        }),
    }
};

index.html

<!DOCTYPE html><html>
    <head>
        <meta charset="utf-8" />
        <title>tristana</title>
    </head>
    <body>
        <div id="root"></div>
    </body></html>

开发服务

$ npm install webpack-dev-server --save-dev

script/webpack.config.js

const path = require("path");const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = {    // ...
    devServer: {        contentBase: path.resolve(__dirname, "dist"),        hot: true,        historyApiFallback: true,        compress: true,
    },
};

package.json

{    // ...
    "scripts": {        "start": "webpack serve --mode=development --config script/webpack.config.js"
    },    // ...}

清理 dist

$ npm install clean-webpack-plugin --save-dev

script/webpack.config.js

const { CleanWebpackPlugin } = require('clean-webpack-plugin');module.exports = {    // ...
    plugins: {        new CleanWebpackPlugin()
    }
};

Tips

由于 webpack 使用的是^5.21.2 版本,在使用该插件时,会提示clean-webpack-plugin: options.output.path not defined. Plugin disabled...,暂时还未解决。

环境变量

$ npm install cross-env --save-dev

package.json

{    // ...
    "scripts": {        "start": "cross-env ENV_LWD=development webpack serve  --mode=development --config script/webpack.config.js",        "build": "cross-env ENV_LWD=production webpack --mode=production --config script/webpack.config.js"
    },    // ...}

.jsx 文件

安装依赖

$ npm install @babel/preset-react react react-dom --save-dev

.babelrc

{  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

src/App.jsx

在 src 目录下,新增 App.jsx 文件:

import React, { Component } from "react";class App extends Component {
    render() {        return (
            <div>
                <h1> Hello, World! </h1>
            </div>
        );
    }
}

export default App;

src/index.js

import React from "react";import ReactDOM from "react-dom";import App from "./App.jsx";
ReactDOM.render(<App />, document.getElementById("root"));

React Router

安装依赖

$ npm install react-router history --save

src/index.js

import React from "react";import ReactDOM from "react-dom";import { Router, Route, Link } from "react-router";import { createBrowserHistory } from "history";import App from "./App.jsx";const About = () => {    return <>About</>;
};

ReactDOM.render(    <Router history={createBrowserHistory()}>
        <Route path="/" component={App} />
        <Route path="/about" component={About} />
    </Router>,    document.getElementById("root")
);

MobX

安装依赖

$ npm install mobx mobx-react babel-preset-mobx --save

.babelrc

{  "presets": ["@babel/preset-env", "@babel/preset-react", "mobx"]
}

src/store.js

在 src 目录下新建 store.js

import { observable, action, makeObservable } from "mobx";class Store {

    constructor() {
        makeObservable(this);
    }

    @observable
    count = 0;

    @action("add")    add = () => {        this.count = this.count + 1;
    };

    @action("reduce")    reduce = () => {        this.count = this.count - 1;
    };
}export default new Store();

index.js

import { Provider } from "mobx-react";import Store from "./store";// ...ReactDOM.render(    <Provider store={Store}>
        <Router history={createBrowserHistory()}>
        <Route path="/" component={App} />
        <Route path="/about" component={About} />
        </Router>
    </Provider>,    document.getElementById("root")
);

src/App.jsx

import React, { Component } from "react";import { observer, inject } from "mobx-react";@inject("store")@observerclass App extends Component {    render() {        return (            <div>
                <div>{this.props.store.count}</div>
                <button onClick={this.props.store.add}>add</button>
                <button onClick={this.props.store.reduce}>reduce</button>
            </div>
        );
    }
}export default App;

Ant Design

安装依赖

$ npm install antd babel-plugin-import --save

.babelrc

{    // ...
    "plugins": [
        [            "import",
            {                "libraryName": "antd",                "libraryDirectory": "es",                "style": true
            }
        ]
    ]
}

src/App.jsx

// ...import { DatePicker } from "antd";import "antd/dist/antd.css";@inject("store")@observerclass App extends Component {
    render() {        return (
            <div>
                <DatePicker />
            </div>
        );
    }
}

export default App;

TypeScript

安装依赖

$ npm install typescript @babel/preset-typescript --save-dev

.babelrc

{    "presets": [        // ...
        "@babel/preset-typescript"
    ]
}

tsconfig.json

在根目录下,新增 tsconfig.json 文件:

{    "compilerOptions": {        "emitDecoratorMetadata": true,        "experimentalDecorators": true,        "target": "ES5",        "allowSyntheticDefaultImports": true,        "strict": true,        "forceConsistentCasingInFileNames": true,        "allowJs": true,        "outDir": "./dist/",        "esModuleInterop": true,        "noImplicitAny": false,        "sourceMap": true,        "module": "esnext",        "moduleResolution": "node",        "isolatedModules": true,        "importHelpers": true,        "lib": ["esnext", "dom", "dom.iterable"],        "skipLibCheck": true,        "jsx": "react",        "typeRoots": ["node", "node_modules/@types"],        "rootDirs": ["./src"],        "baseUrl": "./src"
    },    "include": ["./src/**/*"],    "exclude": ["node_modules"]
}

src/App.jsx

更换文件后缀 App.jsx -> App.tsx

import React, { Component } from "react";import { observer, inject } from "mobx-react";import { DatePicker } from "antd";import "antd/dist/antd.css";@inject("store")@observerclass App extends Component {    props: any;    render() {        return (            <div>
                <DatePicker />
                <div>{this.props.store.count}</div>
                <button onClick={this.props.store.add}>add</button>
                <button onClick={this.props.store.reduce}>reduce</button>
            </div>
        );
    }
}export default App;

代码规范

代码校验、代码格式化、Git 提交前校验、Vscode配置、编译校验

ESLint

安装依赖

$ npm install @typescript-eslint/parser eslint eslint-plugin-standard @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-promise --save-dev

.eslintrc.js

在根目录下,新增 .eslintrc.js 文件:

module.exports = {    extends: ["eslint:recommended", "plugin:react/recommended"],    env: {        browser: true,        commonjs: true,        es6: true,
    },    globals: {
        $: true,        process: true,        __dirname: true,
    },    parser: "@typescript-eslint/parser",    parserOptions: {        ecmaFeatures: {            jsx: true,            modules: true,
        },        sourceType: "module",        ecmaVersion: 6,
    },    plugins: ["react", "standard", "promise", "@typescript-eslint"],    settings: {        "import/ignore": ["node_modules"],        react: {            version: "latest",
        },
    },    rules: {        quotes: [2, "single"],        "no-console": 0,        "no-debugger": 1,        "no-var": 1,        semi: ["error", "always"],        "no-irregular-whitespace": 0,        "no-trailing-spaces": 1,        "eol-last": 0,        "no-unused-vars": [        1,
        {            vars: "all",            args: "after-used",
        },
        ],        "no-case-declarations": 0,        "no-underscore-dangle": 0,        "no-alert": 2,        "no-lone-blocks": 0,        "no-class-assign": 2,        "no-cond-assign": 2,        "no-const-assign": 2,        "no-delete-var": 2,        "no-dupe-keys": 2,        "use-isnan": 2,        "no-duplicate-case": 2,        "no-dupe-args": 2,        "no-empty": 2,        "no-func-assign": 2,        "no-invalid-this": 0,        "no-redeclare": 2,        "no-spaced-func": 2,        "no-this-before-super": 0,        "no-undef": 2,        "no-return-assign": 0,        "no-script-url": 2,        "no-use-before-define": 2,        "no-extra-boolean-cast": 0,        "no-unreachable": 1,        "comma-dangle": 2,        "no-mixed-spaces-and-tabs": 2,        "prefer-arrow-callback": 0,        "arrow-parens": 0,        "arrow-spacing": 0,        camelcase: 0,        "jsx-quotes": [1, "prefer-double"],        "react/display-name": 0,        "react/forbid-prop-types": [        2,
        {            forbid: ["any"],
        },
        ],        "react/jsx-boolean-value": 0,        "react/jsx-closing-bracket-location": 1,        "react/jsx-curly-spacing": [        2,
        {            when: "never",            children: true,
        },
        ],        "react/jsx-indent": ["error", 4],        "react/jsx-key": 2,        "react/jsx-no-bind": 0,        "react/jsx-no-duplicate-props": 2,        "react/jsx-no-literals": 0,        "react/jsx-no-undef": 1,        "react/jsx-pascal-case": 0,        "react/jsx-sort-props": 0,        "react/jsx-uses-react": 1,        "react/jsx-uses-vars": 2,        "react/no-danger": 0,        "react/no-did-mount-set-state": 0,        "react/no-did-update-set-state": 0,        "react/no-direct-mutation-state": 2,        "react/no-multi-comp": 0,        "react/no-set-state": 0,        "react/no-unknown-property": 2,        "react/prefer-es6-class": 2,        "react/prop-types": 0,        "react/react-in-jsx-scope": 2,        "react/self-closing-comp": 0,        "react/sort-comp": 0,        "react/no-array-index-key": 0,        "react/no-deprecated": 1,        "react/jsx-equals-spacing": 2,
    },
};

.eslintignore

在根目录下,新增 .eslintignore 文件:

src/assets

.vscode

在根目录下新增 .vscode 文件夹,然后新增 .vscode/settings.json

{    "eslint.validate": [        "javascript",        "javascriptreact",        "typescript",        "typescriptreact"
    ]
}

Perttier

安装依赖

$ npm install prettier --save-dev

prettier.config.js

在根目录下,新增 prettier.config.js 文件:

module.exports = {    // 一行最多 100 字符
    printWidth: 100,    // 使用 4 个空格缩进
    tabWidth: 4,    // 不使用缩进符,而使用空格
    useTabs: false,    // 行尾需要有分号
    semi: true,    // 使用单引号
    singleQuote: true,    // 对象的 key 仅在必要时用引号
    quoteProps: 'as-needed',    // jsx 不使用单引号,而使用双引号
    jsxSingleQuote: false,    // 末尾不需要逗号
    trailingComma: 'none',    // 大括号内的首尾需要空格
    bracketSpacing: true,    // jsx 标签的反尖括号需要换行
    jsxBracketSameLine: false,    // 箭头函数,只有一个参数的时候,也需要括号
    arrowParens: 'avoid',    // 每个文件格式化的范围是文件的全部内容
    rangeStart: 0,
    rangeEnd: Infinity,    // 不需要写文件开头的 @prettier
    requirePragma: false,    // 不需要自动在文件开头插入 @prettier
    insertPragma: false,    // 使用默认的折行标准
    proseWrap: 'preserve',    // 根据显示样式决定 html 要不要折行
    htmlWhitespaceSensitivity: 'css',    // 换行符使用 lf
    endOfLine: 'lf'};

stylelint

安装依赖

$ npm install stylelint stylelint-config-standard stylelint-config-prettier --save-dev

stylelint.config.js

在根目录下,新增 stylelint.config.js 文件:

module.exports = {
    extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
    ignoreFiles: [        '**/*.ts',        '**/*.tsx',        '**/*.png',        '**/*.jpg',        '**/*.jpeg',        '**/*.gif',        '**/*.mp3',        '**/*.json'
    ],
    rules: {        'at-rule-no-unknown': [
            true,
            {
                ignoreAtRules: ['extends', 'ignores']
            }
        ],
        indentation: 4,        'number-leading-zero': null,        'unit-allowed-list': ['em', 'rem', 's', 'px', 'deg', 'all', 'vh', '%'],        'no-eol-whitespace': [
            true,
            {
                ignore: 'empty-lines'
            }
        ],        'declaration-block-trailing-semicolon': 'always',        'selector-pseudo-class-no-unknown': [
            true,
            {
                ignorePseudoClasses: ['global']
            }
        ],        'block-closing-brace-newline-after': 'always',        'declaration-block-semicolon-newline-after': 'always',        'no-descending-specificity': null,        'selector-list-comma-newline-after': 'always',        'selector-pseudo-element-colon-notation': 'single'
    }
};

lint-staged、pre-commit

安装依赖

$ npm install lint-staged prettier eslint pre-commit --save-dev

package.json

{    // ...
    "scripts": {        "lint:tsx": "eslint --ext .tsx src && eslint --ext .ts src",        "lint:css": "stylelint --aei .less .css src",        "precommit": "lint-staged",        "precommit-msg": "echo 'Pre-commit checks...' && exit 0"
    },    "pre-commit": [        "precommit",        "precommit-msg"
    ],    "lint-staged": {        "*.{js,jsx,ts,tsx}": [            "eslint --fix",            "prettier --write",            "git add"
        ],        "*.{css,less}": [            "stylelint --fix",            "prettier --write",            "git add"
        ]
    }
}

eslint-webpack-plugin

安装依赖

$ npm install eslint-webpack-plugin --save-dev

script/webpack.config.js

const ESLintPlugin = require('eslint-webpack-plugin');module.exports = {    // ...
    plugins: [new ESLintPlugin()],
};

总结

搭建这个的过程,也是遇到了不少坑,收获也是蛮多的,希望这个教程能够帮助更多的同学,少采点坑,完整的 React 开发环境可以看这个tristana,求点赞,求关注!