搭建react+Redux+ts+antd+less+webpack项目(不使用脚手架)

1,185 阅读4分钟

首先

yarn init

创建package.json文件,

{
  "name": "demo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT"
}

react

安装reactreact-dom

yarn add react react-dom

其中React负责描述特性,提供Reract Api,比如函数组件、hooks等;react-dom负责渲染dom

创建src文件夹,存放源文件。

src下创建index.html文件:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width">
    <title>reactDemo</title>
</head>

<body>
    <div id="root"></div>
</body>

</html>

src下创建index.js文件:

import React ,{Component} from 'react'
import ReactDom from 'react-dom'

class App extends Component{
    render(){
        return <div>hello1 Jerry</div>
    }
}

ReactDom.render(<App />, document.getElementById('root'));

webpack

安装webpack,webpack-cli ,webpack-dev-server

yarn add webpack webpack-cli webpack-dev-server

  • webpack:依赖包
  • webpack-cli:wabpack的命令行工具
  • webpack-dev-server:启动一个服务

创建config文件夹,用于存放不同环境下的webpack配置。

并且基本配置:

// config/webpack.config.base.js
import path from 'path'
const __dirname=path.resolve()

export default {
  entry: "./src/index.js",
  mode:'development',
  output: {
    path:  path.resolve(__dirname, '../dist/')
  },
  devServer:{
    hot: true,
    host: "localhost",
    port: "8081",
  },
};

package.json文件中添加命令:

  "scripts": {
    "start": "webpack-dev-server --config ./config/webpack.config.base.js"
  }

尝试执行:

yarn start

报错:

import path from 'path' ^^^^^^

SyntaxError: Cannot use import statement outside a module

package.json中添加:

  "type": "module",

这时候报错:

ERROR in ./src/index.js 6:15
Module parse failed: Unexpected token (6:15)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| class App extends Component{
|     render(){
>         return <div>hello1 Jerry</div>
|     }
| }

这是由于不能识别react代码

支持ES6

安装 babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime @babel/runtime。

yarn add babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime @babel/runtime -D

config/webpack.config.base.js更新为:

import path from "path";
const __dirname = path.resolve();

export default {
  entry: "./src/index.js",
  mode: "development",
  output: {
    path: path.resolve(__dirname, "../dist/"),
  },
  // add start
  module: {
    rules: [
      {
        test: /\.js$/, // 以js结尾的文件
        exclude: /node_modules/, //文件不在node_modules文件夹下
        loader: "babel-loader", //babel是js和babel的桥梁,但是并不完成代码转化
      },
    ],
  },
  // add end
  devServer: {
    hot: true,
    host: "localhost",
    port: "8081",
  },
};

创建.babelrc文件,配置babel:

{
    "plugins": [
        [
            "@babel/plugin-transform-runtime",
            {
                "corejs": 2,
                "helpers": true,
                "regenerator": true,
                "useESModules": false
            }
        ]
    ],
      "presets": [
        [
            "@babel/preset-env",
            {
                "targets": {
                    "chrome":67
                }, //可以将此处删除,改用.browserslistrc文件
                "useBuiltIns":"usage"
            }
        ],
    ]
}

下面详细讲解下这部分用到的内容:

  • babel-loader:将ES6代码转化为ES5
  • @babel/core:把 js 代码分析成 ast ,方便各个插件分析语法进行相应的处理。有些新语法在低版本 js 中是不存在的,如箭头函数,rest 参数,函数默认值等,这种语言层面的不兼容只能通过将代码转为 ast,分析其语法后再转为低版本 js
  • @babel/preset-env:该预设除了包含所有稳定的转码插件,还可以根据我们设定的目标环境进行针对性转码。还可以通过设置参数项进行针对性语法转换以及polyfill的部分引入。
  • @babel/plugin-transform-runtime:自动移除语法转换后内联的辅助函数(inline Babel helpers),使用@babel/runtime/helpers里的辅助函数来替代。这样就减少了手动引入的麻烦。
  • @babel/runtime:把所有语法转换会用到的辅助函数都集成在了一起。减小体积。

@babel/preset-env

  "browserslist": [
    "> 1%",
    "not ie <= 8"
  ]

上面的配置含义是,目标环境是市场份额大于1%的浏览器并且不考虑IE8及以下的IE浏览器。Browserslist叫做目标环境配置表,除了写在package.json里,也可以单独写在工程目录下.browserslistrc文件里。我们用browserslist来指定代码最终要运行在哪些浏览器或node.js环境。Autoprefixer、postcss等就可以根据我们的browserslist,来自动判断是否要增加CSS前缀(例如'-webkit-')。我们的Babel也可以使用browserslist,如果使用了@babel/preset-env这个预设,此时Babel就会读取browserslist的配置。

如果@babel/preset-env不设置任何参数,Babel就会完全根据browserslist的配置来做语法转换。如果没有browserslist,那么Babel就会把所有ES6的语法转换成ES5版本。

比如:我们有一个箭头函数,然后设置:

 "browserslist": [
    "chrome 60"
  ]

这时转换后的代码依旧是箭头函数,这是因为chrome 60已经实现了箭头函数的语法。当我们换成chrome 38时,箭头函数会转换为ES5的语法,因为chrome 38不支持箭头函数语法。

注意,Babel使用browserslist的配置功能依赖于@babel/preset-env,如果Babel没有配置任何预设或插件,那么Babel对转换的代码会不做任何处理,原封不动生成和转换前一样代码。

targets

该参数项可以取值为字符串、字符串数组或对象,不设置的时候取默认值空对象。

如果我们对@babel/preset-env的targets参数项进行了设置,那么就不使用browserslist的配置,而是使用targets的配置。如不设置targets,那么就使用browserslist的配置(推荐)。如果targets不配置,browserslist也没有配置,那么@babel/preset-env就对所有ES6语法转换成ES5的。

useBuiltIns

useBuiltIns项取值可以是"usage" 、 "entry" 或 false。如果该项不进行设置,则取默认值false。

这个参数项主要和polyfill的行为有关。在我们没有配置该参数项或是取值为false的时候,polyfill会全部引入到最终的代码里。取值为"entry"或"usage"的时候,会根据配置的目标环境找出需要的polyfill进行部分引入。

useBuiltIns:"entry"

使用:需要在入口文件中使用import '@babel/polyfill'(只能import polyfill一次,一般都是在入口文件。如果进行多次import,会发生错误。)

结果:将所有目标环境不支持的API特性引入。

useBuiltIns:"usage"

使用:不需要在入口文件中引入polyfill,babel 会自动进行polyfill引入。

结果:会将所有目标环境不支持且代码中使用到的API引入

corejs

该参数项的取值可以是2或3,没有设置的时候取默认值为2。

这个参数项只有useBuiltIns设置为'usage'或'entry'时,才会生效。

取默认值或2的时候,Babel转码的时候使用的是core-js@2版本(即core-js2.x.x)。因为某些新API只有core-js@3里才有,例如数组的flat方法,我们需要使用core-js@3的API模块进行补齐,这个时候我们就把该项设置为3。

需要注意的是,corejs取值为2的时候,需要安装并引入core-js@2版本,或者直接安装并引入polyfill也可以。如果corejs取值为3,必须安装并引入core-js@3版本才可以,否则Babel会转换失败并提示。

modules

这个参数项的取值可以是"amd"、"umd" 、 "systemjs" 、 "commonjs" 、"cjs" 、"auto" 、false。在不设置的时候,取默认值"auto"。

该项用来设置是否把ES6的模块化语法改成其它模块化语法。

我们常见的模块化语法有两种:

  • ES6的模块法语法用的是import与export;
  • commonjs模块化语法是require与module.exports。

在该参数项值是'auto'或不设置的时候,会发现我们转码前的代码里import都被转码成require了。

如果我们将参数项改成false,那么就不会对ES6模块化进行更改,还是使用import引入模块。

使用ES6模块化语法有什么好处呢。在使用Webpack一类的打包工具,可以进行静态分析,从而可以做tree shaking 等优化措施。

@babel/runtime

比如有文件使用了ES6的class语法,babel转化后,会在文件头部添加许多函数声明,称之为辅助函数。@babel/preset-env在做语法转换的时候,注入了这些函数声明,以便语法转换后使用。

但样这做存在一个问题。在我们正常的前端工程开发的时候,少则几十个js文件,多则上千个。如果每个文件里都使用了class类语法,那会导致每个转换后的文件上部都会注入这些相同的函数声明。这会导致我们用构建工具打包出来的包非常大。

那么怎么办?一个思路就是,我们把这些函数声明都放在一个npm包里,需要使用的时候直接从这个包里引入到我们的文件里。这样即使上千个文件,也会从相同的包里引用这些函数。通过webpack这一类的构建工具打包的时候,我们只会把使用到的npm包里的函数引入一次,这样就做到了复用,减少了体积。

@babel/runtime就是上面说的这个npm包,@babel/runtime把所有语法转换会用到的辅助函数都集成在了一起。

但是我们需要手动将@babel/runtime集中后的辅助函数包引入,这时候@babel/plugin-transform-runtime就登场了。

@babel/plugin-transform-runtime

@babel/plugin-transform-runtime有三大作用:

  • 自动移除语法转换后内联的辅助函数(inline Babel helpers),使用@babel/runtime/helpers里的辅助函数来替代。这样就减少了我们手动引入的麻烦。
  • 当代码里使用了core-js的API,自动引入@babel/runtime-corejs3/core-js-stable/,以此来替代全局引入的core-js/stable;
  • 当代码里使用了Generator/async函数,自动引入@babel/runtime/regenerator,以此来替代全局引入的regenerator-runtime/runtime;

helpers

该项是用来设置是否要自动引入辅助函数包,这个当然要引入了,这是@babel/plugin-transform-runtime的核心用途。该项取值是布尔值,我们设置为true,其默认值也是true,所以也可以省略不填。

corejs和regenerator

这两项是用来设置是否做API转换以避免污染全局环境,regenerator取值是布尔值,corejs取值是false、2和3。在前端业务项目里,我们一般对corejs取false,即不对Promise这一类的API进行转换。而在开发JS库的时候设置为2或3。regenerator取默认的true就可以

useESModules

该项用来设置是否使用ES6的模块化用法,取值是布尔值。默认是fasle,在用webpack一类的打包工具的时候,我们可以设置为true,以便做静态分析。

支持React

安装 @babel/preset-react

yarn add @babel/preset-react -D

更新.babelrc:

{
    "plugins": [
        [
            "@babel/plugin-transform-runtime",
            {
                "corejs": 2,
                "helpers": true,
                "regenerator": true,
                "useESModules": false
            }
        ]
    ],
    "presets": [
        "@babel/preset-react"
    ]
}

执行yarn start,成功!!!

但是打开http://localhost:8081/ 发现显示 Cannot GET /

html-webpack-pluginclean-webpack-plugin

安装:

yarn add html-webpack-plugin clean-webpack-plugin -D

更新:

import path from "path";
const __dirname = path.resolve();
//start
import HtmlWebpackPlugin from  "html-webpack-plugin"
import { CleanWebpackPlugin }  from 'clean-webpack-plugin'
//end

export default {
  entry: "./src/index.js",
  mode: "development",
  output: {
    path: path.resolve(__dirname, "../dist/"),
  },
  module: {
    rules: [
      {
        test: /\.js$/, // 以js结尾的文件
        exclude: /node_modules/, //文件不在node_modules文件夹下
        loader: "babel-loader", //babel是js和babel的桥梁,但是并不完成代码转化
      },
    ],
  },
  devServer: {
    hot: true,
    host: "localhost",
    port: "8081",
  },
  // start
  plugins: [
    new HtmlWebpackPlugin({
      filename: "index.html",
      template: "./src/index.html",
      inject: true,
      path: "/dist/",
    }),
    new CleanWebpackPlugin()
  ],
  // end
};

至此,运行命令,打开网址,successfully!!!

html-webpack-plugin:当使用 webpack 打包时,创建一个 html 文件,并把 webpack 打包后的静态文件自动插入到这个 html 文件当中。

clean-webpack-plugin:是一个清除文件的插件。 在每次打包后,磁盘空间会存有打包后的资源,在再次打包的时候,我们需要先把本地已有的打包后的资源清空,来减少它们对磁盘空间的占用。 插件clean-webpack-plugin就可以帮我们做这个事情。

webpack-merge

通常,开发环境和生产环境需要配置不同的webpack配置,但是也有相同的配置,一般情况下:

  • webpack.config.base.js:存放公共webpack配置
  • webpack.config.dev.js:存放开发环境特有webpack配置
  • webpack.config.prod.js:存放生产环境特有webpack配置

但是需要将公共配置和开发环境特有的配置结合起来构成全部开发环境的配置。这是webpack-merge就登场了。

安装:

yarn add webpack-merge -D

使用:

import baseConfig from "./webpack.config.base.js";
import {merge} from 'webpack-merge';
import configs from "./configs.js";
const { dev } = configs;

export default merge(baseConfig, {
  devServer: {
    hot: true,
    host: dev.host,
    port: dev.port,
  },
});

package.json的scripts改变为:

  "scripts": {
    "start": "webpack-dev-server --config ./config/webpack.config.dev.js"
  },

支持typescript

安装:

yarn add typescript ts-loader

yarn add ts-loader @types/react @types/react-dom -D

创建配置文件tsconfig.json:

{
    "compilerOptions": {
        "outDir": "./dist/",
        "noImplicitAny": true,
        "module": "es6",
        "target": "es5",
        "jsx": "react",
        "allowJs": true,
        "moduleResolution": "node"
    }
}

配置webpack.config.base.js

  module:{
  	rulse:[
  		{
        test: /\.(tsx|ts)$/,
        use: ["babel-loader", "ts-loader"],
        exclude: /node_modules/,
      },
  	]
  },
  resolve: {
    extensions: [".tsx", ".ts", ".js"],
  },

resolve

extensions

其中extensions的作用是在使用import引入时可以不添加文件后缀(当添加了type:modules到package.json中时,仅在非js文件中支持不添加后缀)

alias

支持import引入时使用路径别名。仅在js文件中起作用。

  resolve: {
    extensions: ['.js', '.jsx','.ts','.tsx'],
    alias: {
      "@": path.resolve(__dirname, 'src'),
      "@com":path.resolve(__dirname,'src/components')
    },
  },

若想在tsx文件中使用路径别名,需要在tsconfig.json中配置:

//compilerOptions 下 	
"paths": {
        "@/*": ["./src/*"],
        "@com/*": ["./src/components/*"]
   }

支持antd

安装 antd:

yarn add antd

antd组件可以正常使用,但是样式没有生效。

解决办法一:全局引入样式

import 'antd/dist/antd.css';

解决办法二:按需引入样式

安装:

yarn add babel-plugin-import -D

.babelrc中配置:

"plugins": [
   ["import", {
     "libraryName": "antd",
     "libraryDirectory": "es",
     "style": "css"
   }, "import-antd"]
],

支持less

安装:

yarn add less less-loader -D

配置webpack的rules:

      {
        test: /\.less$/,
        use: [
          'style-loader',
          'css-loader',
          'less-loader',
        ],
      },

根目录下创建typings文件,用来存放ts的描述文件

typings文件夹下创建global.d.ts并且写入:

declare module '*.less';
declare module '*.css';

问题一:控制台报错Cannot read properties of undefined “con”(con是less文件中定义的className)

style-loader版本是 3.3.1,降版本到2.0.0之后控制台不再报错。

问题二:控制台不报错但是样式不生效

解决方案一:将webpack.config.base.js中的定义更新:

      {
        test: /\.less$/,
        use: [
          "style-loader",
          "css-loader",
          {
            loader: "less-loader",
            options: {
              javascriptEnabled: true,
            },
          },
        ],
      },
      // 更新为
      {
        test: /\.less$/,
        use: [
          "style-loader",
          {
            loader: 'css-loader',
            options: {
              modules: {
                mode: 'local',
                localIdentName: '[name]__[local]-[hash:base64:5]'
              }
            }
          },
          {
            loader: "less-loader",
            options: {
              javascriptEnabled: true,
            },
          },
        ],
      },

解决方案二:

  1. .less文件更改为.m.less文件

  2. typings/global.d.ts文件中添加:declare module '*.m.less';

  3. webpack.config.base.js文件中添加配置:

    		{
          	test: /\.less$/,
            exclude: /\.m\.less$/, //在原来的基础上添加这一行配置
            use: [
              "style-loader",
              "css-loader",
              {
                loader: "less-loader",
                options: {
                  javascriptEnabled: true,
                },
              },
            ],
          },      
          {
            test: /\.m\.less$/,
            use: [
              'style-loader',
              {
                loader: 'css-loader',
                options: {
                  modules: {
                    mode: 'local',
                    localIdentName: '[name]__[local]-[hash:base64:5]'
                  }
                }
              },
              'postcss-loader',
              'less-loader'
            ]
          },
    

    modules

    启用/禁用 CSS 模块规范并且设置基本的行为。

    mode:local:开启模块,将类名转换为唯一的hash值。

    localIdentName:允许配置生成的本地标识符

引入ReactRouter

安装react-router-dom:

$ yarn add react-router-dom

安装对应声明文件,使ts可以识别:

$yarn add @types/react-router-dom -D

路由配置信息:

// router/index.tsx
import * as React from "react";
import { Routes, Route } from "react-router-dom";
import { lazy } from "react";

const Com = lazy(
  () => import(/* webpackChunkName: "com" */ "../components/com")
);

const routes = [{ title: "页面标题", path: "/hello", component: Com }];

const RoutersConfig = () => {
  return (
    <React.Suspense fallback>
      <Routes>
        {routes.map((route, i) => {
          document.title = route.title || "";
          return (
            <Route path={route.path} key={i} element={<route.component />} />
          );
        })}
      </Routes>
    </React.Suspense>
  );
};

export default RoutersConfig;

注册全局路由

// index.js
import ReactDOM from "react-dom";
import React from "react";
import { BrowserRouter } from "react-router-dom";
import RoutersConfig from "./router/index.tsx";

ReactDOM.render(
  <BrowserRouter>
    <RoutersConfig />
  </BrowserRouter>,
  document.getElementById("app")
);

页面报错:Dynamic imports are only supported when the '--module' flag is set to 'es2020', 'es2022', 'esnext', 'commonjs', 'amd', 'system', 'umd', 'node12', or 'nodenext'.

修改配置:

// tsconfig.json
"module": "esnext",

这时候访问路由报错:connot GET /hello

修改配置:(参考连接:react-router设置path无效,错误信息Cannot GET /xxx_这个昵称没有被占用吧的博客-CSDN博客_cannot get

devServer: {
    // 添加下边这一行配置
    historyApiFallback: true,
    hot: true,
    host: dev.host,
    port: dev.port,
  },

引入Redux

安装依赖:

$ yarn add redux

$ yarn add @reduxjs/toolkit

$ yarn add react-redux

创建store

// store/app/rootStore
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit";
import counterReducer from "../com";

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>;

// store/app/hooks
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import { RootState, AppDispatch } from "./rootStore";

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

// store/com.ts
import { createSlice } from "@reduxjs/toolkit";
import { RootState } from "./app/rootStore";

export const counterSlice = createSlice({
  name: "counter",
  initialState: {
    value: 0,
  },
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
  },
});
export const { increment, decrement } = counterSlice.actions;
export const selectCount = (state: any) => state.counter.value;
export default counterSlice.reducer;

// 组件中使用
import * as React from "react";
import { Button } from "antd";
import _s from "../app.m.less";
import { increment, decrement, selectCount } from "src/store/com";
import { useAppDispatch, useAppSelector } from "src/store/app/hooks";

const Com = () => {
  const count = useAppSelector(selectCount);
  const dispatch = useAppDispatch();

  return (
    <div className={_s.con}>
      <h2>{count}</h2>
      <Button onClick={() => dispatch(increment())} type="primary">
        +
      </Button>
      <Button onClick={() => dispatch(decrement())} type="primary">
        -
      </Button>
    </div>
  );
};

export default Com;

                                

全局注入store

// 入口文件 src/index.js
import ReactDOM from "react-dom";
import React from "react";
import { BrowserRouter } from "react-router-dom";
import RoutersConfig from "./router/index.tsx";
import { Provider } from "react-redux";
import { store } from "./store/app/rootStore.ts";

ReactDOM.render(
  // 添加 Provider
  <Provider store={store}>
    <BrowserRouter>
      <RoutersConfig />
    </BrowserRouter>
  </Provider>,
  document.getElementById("app")
);