从零开发一套完整的react项目开发环境

10,852 阅读13分钟

不管是工作需要还是面试加分,除了Vue相关技术以外,React技术栈也已经成为了前端开发工程师必备的技术点。接下来,我将从零开发一套完整的React全家桶项目开发环境,提供给需要的同行小伙伴观看也方便自己以后复习。篇幅很长,请需要的小伙伴耐心阅读完必有所收获!

快上车

仓库地址

项目源码地址:github.com/tangmengche…

目标

我们本次需要通过 webpack4.x完成那些目标:

  1. ES6/7/8/9等高级语法转换成ES5
  2. stylus/less/scss等css预处理器转换成css
  3. 解析字体font、图片(jpg、png...)等静态资源
  4. 压缩js、css等文件
  5. 自动添加css各大浏览器厂商前缀
  6. 定义环境变量
  7. 抽离公共代码
  8. 项目热更新和懒加载
  9. 区别生产环境和开发环境
  10. 每次打包删除上一次打包记录
  11. 集成React、React-Router-Dom、Redux等周边生态
  12. 添加express服务接口、通过axios打通前后端
  13. ...

后续有时间,楼主将 Antd UI 框架也集成进来,完完整整的从零一行行代码开发一个完整的React全家桶项目。

项目初始化

  1. 检查 node 环境配置

先本地全局安装node环境,react的运行是依赖于nodenpm的管理工具来实现的,node下载地址。下载好node之后,打开cmd管理工具,输入node -v,回车,查看node版本号,出现版本号则说明安装成功

node -v  npm -v

效果图

  1. 初始化项目目录

在命令行依次输入:

mkdir my-react-project 新建项目目录
cd my-react-project/ 切换到项目目录
npm init 生成项目的一些信息,最终会生成一个package.json文件。注意:可以输入npm init -y可以不用按回车
  1. 安装 webpack

webpack 是一个模块打包机,自动分析项目依赖的模块以及一些浏览器不能直接转换的高级语法等转换成浏览器可以解析的 js、css文件等。在项目根目录本地安装webpack, 本项目将使用webpack4.x版本

npm install webpack webpack-cli -D

效果图

  1. 初始化项目目录和文件

在项目根目录新建一下文件:

src: 存放项目源码的目录
index.js: 需要被 webpack 编译的文件
build:存放项目的 webpack 配置文件
webpack.config.js 项目的webpack核心配置文件
index.html: 项目打包后自动将打包的文件添加在该文件里面

效果图

添加webpack配置文件的基本信息

  1. mode: 模式, development开发环境、production生产环境
  2. entry: 项目的打包的入口文件
  3. output: 项目的打包后输出文件
  4. module: 模块, 在webpack中所有文件皆模块, 解析css、js、图片以及字体图标等
  5. plugins: 插件, 用来扩展webpack功能
  6. ...

在package.json文件 scripts 属性中添加 运行 npm run build 即可打包

"build": "webpack --config ./build/webpack.config.js"

在index.js中添加测试代码验证webpack打包是否正确

function sum(a, b) {
    return a + b;
}
var sum = sum(1, 2)
console.log(sum)

如果项目dist目录生成了一个bundle.js文件,说明webpack打包正确. 效果图

配置核心功能

ES6/7/8/9等高级语法转换成ES5

在index.js中添加ES6/7/8等高级语法的代码测试代码验证webpack是否能将其转换为ES5等让浏览器能够解析的低级语法

  1. 安装相关依赖
    npm install babel-loader @babel/core @babel/preset-env -D
    
    babel-loader 是将ES6等高级语法转换为能让浏览器能够解析的低级语法
    @babel/core 是babel的核心模块,编译器。提供转换的API
    @babel/preset-env 可以根据配置的目标浏览器或者运行环境来自动将ES2015+的代码转换为es5
    @babel/preset-react 用于解析 JSX
  1. 在项目根目录创建babel.config.js文件

如果不创建该文件的话,也可以在webpack配置文件对应的规则中添加options即可。

babel配置文件

  1. 修改核心配置文件webpack.config.js文件

    {
        // src目录下面以.js结尾的文件都要使用babel解析
        // cacheDirectory是用来缓存的,下次编译加速
        test: /\.js$/,
        use: ['babel-loader?cacheDirectory=true'],
        include: path.join(__dirname, '../src')
    },
    

    然后运行npm run build,就可以看到我们输入的ES6+等高级语法被转换为ES5了。 注意:babel-loader只会将 ES6/7/8等高级语法转换为ES5语法,但是对新api并不会转换。比如Promise、Iterator、Set、Proxy、Symbol等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。此时,我们必须使用babel-polyfill,为当前环境提供一个垫片。

    npm install @babel/polyfill -S
    
    babel-polyfill是解决babel-loader不能对新的api进行转换为当前环境添加一个垫片

重点:当我们执行打包后,打包的文件里含有大量的重复代码,那么我们需要提供统一的模块化的helper来减少这些helper函数的重复输出。

    npm install @babel/runtime @babel/plugin-transform-runtime @babel/plugin-syntax-dynamic-import -D
    
    @babel/runtime 就是提供统一的模块化的helper, 使用能大大减少打包编译后的体积
    @babel/plugin-transform-runtime它会帮我自动动态require @babel/runtime中的内容
    注意:还有一些常见的babel:
    @babel/plugin-proposal-decorators将es6+中更高级的特性转化---装饰器
    @babel/plugin-proposal-class-properties将es6中更高级的API进行转化---类

效果图

  1. 在index.js中编写ES6+等高级语法
    let fn = () => {
        console.log('箭头函数')
    }
    fn()
    
    let promise = new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(123)
        }, 1000)
    })
    promise.then(res => {
        console.log(res);
    })

stylus/less/scss等css预处理器转换成css

以下就以less预处理器为例,详细介绍下其用法,其余两种类似:

  1. 安装相关依赖
    npm install stylus stylus-loader less less-loader sass-loader node-sass css-loader style-loader -D
    
    css-loader主要的作用是解析css文件, 像@import等动态语法
    style-loader主要的作用是解析的css文件渲染到html的style标签内
    stylus、less、sass是CSS的常见预处理器
    stylus-loader、less-loader、sass-loader主要是将其对应的语法转换成css语法
  1. 修改核心配置文件webpack.config.js

    效果图

  2. 添加index.less文件

    @color: red;
    #div1 {
        color: @color;
        font-size: 36px;
    }

效果图 注意:CSS3 的许多特性来说,需要添加各种浏览器兼容前缀,开发过程中,这样加太麻烦,postcss 帮你自动添加各种浏览器前缀

    npm install postcss-loader autoprefixer -D
    
    postcss-loader autoprefixer 处理浏览器兼容,自动为CSS3的某些属性添加前缀

效果图

解析字体font、图片(jpg、png...)等静态资源

项目中通常会使用图片、字体等静态资源,不使用对应的loader项目会报错

  1. 安装相关依赖
    npm install file-loader url-loader -D
    
    file-loader可以用来帮助webpack打包处理一系列的图片文件;比如:.png 、 .jpg 、.jepg等格式的图片。打包的图片会给每张图片都生成一个随机的hash值作为图片的名字
    url-loader封装了file-loader,它的工作原理:1.文件大小小于limit参数,url-loader将会把文件转为Base64;2.文件大小大于limit,url-loader会调用file-loader进行处理,参数也会直接传给file-loader
  1. 修改核心配置文件webpack.config.js

    效果图

压缩打包后的js、css等文件

由于项目打包后会生成很多js文件,代码之间有很多空格、引号等,如果我们将其去掉,这样会大大减少打包的体积

  1. 安装相关依赖
    npm install mini-css-extract-plugin -D
    // or 或
    npm install extract-text-webpack-plugin@next -D // 不推荐使用
    npm install optimize-css-assets-webpack-plugin -D
    npm install uglifyjs-webpack-plugin -D
    // 扩展 消除未使用的css
    npm install purify-webpack purify-css -D

注意:在生产模式下,webpack自动将JS进行压缩。MiniCssExtractPlugin 推荐只用于生产环境,因为该插件在开发环境下会导致HMR功能缺失,所以日常开发中,还是用style-loader。

  1. 修改核心配置文件webpack.config.js

    修改配置

抽离公共代码

在我们写好代码后打包,由于不管是css还是js都有很多公共的部分。如果不将其抽离的话会使打包的项目体积过大,影响页面性能

注意:在webpack4以后的版本都使用内置的SplitChunksPlugin插件来进行公共部分的代码提取。

// 核心配置
optimization: {
    splitChunks: {
        cacheGroups: {
            //打包公共模块
            commons: {
                chunks: 'initial', //initial表示提取入口文件的公共部分
                minChunks: 2, //表示提取公共部分最少的文件数
                minSize: 0, //表示提取公共部分最小的大小
                name: 'commons' //提取出来的文件命名
            }
        }
    }
}

添加resolve选项

添加该选项只是为了方便我们开发者,比如文件的别名、文件的扩展名等

// 核心配置
resolve: {
    extensions: ['.js', '.jsx', '.json'],
    alias: {
        pages: path.join(__dirname, '../src/pages'),
        components: path.join(__dirname, '../src/components'),
        actions: path.join(__dirname, '../src/redux/actions'),
        reducers: path.join(__dirname, '../src/redux/reducers'),
        images: path.join(__dirname, '../src/images')
    }
}

代码热更新

  1. 安装依赖

    npm install webpack-dev-server -D
    npm install html-webpack-plugin -D
    html-webpack-plugin主要有两个作用: 
    1. 为html文件中引入的外部资源如scriptlink动态添加每次compile后的hash,防止引用缓存的外部文件问题       
    2. 可以生成创建html入口文件
    
  2. 修改核心配置文件webpack.config.js文件

    修改配置

  3. 在package.json添加:

    "dev": "webpack-dev-server --config ./build/webpack.config.js"
    

每次打包删除上一次打包记录

  1. 安装相关依赖
    npm install clean-webpack-plugin -D
    
    clean-webpack-plugin是删除webpack打包后的文件夹以及文件
  1. 修改核心配置文件webpack.config.js

    效果图

添加该插件后,每次只需打包时都会删除前一次的打包记录。

区别生产环境和开发环境

在我们开发不管是Vue项目还是React项目,都会分为开发环境和生产环境。这样是因为两个环境会有一些不同:

一、开发环境:

  1. 模块热更新
  2. sourceMap
  3. 接口代理
  4. 代码检查规范
  5. ...

二、生产环境:

  1. 提取公共代码
  2. 代码压缩
  3. 去除无用的代码
  4. ...

在webpack中通过mode选项来区分开发环境(development)和生产环境(production)

优秀

集成react全家桶

集成react步骤

  1. 安装依赖
npm install react react-dom -S
  1. 修改index.js,引入react
import React from 'react';
import ReactDom from 'react-dom';

ReactDom.render(
    <div>石小明</div>,
    document.getElementById('app')
)
  1. npm run dev 运行结果

运行结果

集成react-router-dom步骤

  1. 安装依赖

    npm install react-router-dom -S
    

在项目根目录添加一个page文件夹存放页面,在新建两个页面作为页面跳转后的文件

  1. 添加router.js文件
import React from 'react'
import { Route, Switch } from 'react-router-dom'

// 引入页面
import Home from './pages/home/index'
import Page from './pages/page/index'

// 路由
const getRouter = () => {
    <Switch>
        <Route exact path="/" component={Home} />
        <Route exact path="/page" component={Page} />
    </Switch>
}

export default getRouter;
  1. 添加一个主文件
import React from 'react'
import { Link } from 'react-router-dom'

export default () => {
    return (
        <div>
            <ul>
                <li><Link to="/">首页</Link></li>
                <li><Link to="/page">Page页</Link></li>
            </ul>
        </div>
    )
}

最后,运行npm run dev就可以看到我们两个页面都可以相互点击跳转了。

集成redux步骤

  1. 安装依赖

    npm install redux react-redux -S
    
    redux 是一个状态管理器,类似于Vuex
    react-redux 是连接Redux 与 React两个框架
    
  2. 新建核心配置文件store

第一、新建store核心配置文件

import {createStore, combineReducers} from 'redux'
import counter from 'reducers/counter'
import userInfo from 'reducers/userInfo'

let store = createStore(combineReducers({counter, userInfo}))

export default store

第二:将store集成在项目初始化的根目录

import ReactDom from 'react-dom'
import { Provider } from 'react-redux'

import store from './redux/store'

ReactDom.render(
    <Provider store={store}>
        ...
    </Provider>,
    document.getElementById('app')
)

注意:通过将跟组件注入store,所有的文件都可以获取的到store里的内容

  1. 新建actions 和reducers文件夹

注意:actions文件夹里存放是行为,而reducers文件夹存放的是具体的操作

以计数例子为例:

第一:在actions里新建counter.js

在该文件中,我们定义了几种type,分别代表加、减、清零。然后定义了几个action创建函数

// 防止action的type值重复,给相应的前面加个prefix
export const INCREMENT = "counter/INCREMENT"
export const DECREMENT = "counter/DECREMENT"
export const RESET = "counter/RESET"

export function increment() {
    return {
        type: INCREMENT
    }
}
export function decrement() {
    return {
        type: DECREMENT
    }
}
export function reset() {
    return {
        type: RESET
    }
}

注意:在项目大的时候,我们需要将type类型的值单独存放在一个文件里,方便管理

第二:在reducers里新建counter.js

在该文件中,我们初始化了最初的状态,和定义了一个具体的处理逻辑的方法

import {INCREMENT, DECREMENT, RESET} from '../actions/counter';

// 初始化state
const initState = {
    count: 0
}

// reducer 接收两个参数,state,action 返回新的state
export default function reducer(state = initState, action) {
    switch (action.type) {
        case INCREMENT:
            return {
                count: state.count + 1
            };
        case DECREMENT:
            return {
                count: state.count - 1
            }
        case RESET:
            return {
                count: 0
            }
        default:
            return state
    }
}

注意:reducer是一个纯函数,它接收初始值和action,返回一个新的状态。如果没有找对对应的type,默认返回初始值

  1. 在项目中使用

我们在page目录下新建一个counter目录,用于存放测试redux功能的目录

import React, { Component } from 'react'
import { connect } from 'react-redux'
import { increment,decrement, reset } from 'actions/counter'

class Counter extends Component{

    render() {
        return (
            <div>
                <span>当前计数值为:{this.props.counter.count}</span>
                <button onClick={() => this.props.increment()}>增加</button>
                <button onClick={() => this.props.decrement()}>减少</button>
                <button onClick={() => this.props.reset()}>重置</button>
            </div>
        );
    }
}

export default connect(state => state, dispatch => ({
    increment: () => {
        dispatch(increment())
    },
    decrement: () => {
        dispatch(decrement())
    },
    reset: () => {
        dispatch(reset())
    }
}))(Counter)

注意:react-redux中提供了一个connect方法,用于连接react组件和redux。

通过该语法:connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])可知该方法各个参数的意思;

  1. mapStateToProps:意思是将Store中的状态State,以属性Props的方式传给组件
  2. mapDispatchToProps:意思是将disptach方法,以属性Props的方式传个组件
  3. mergeProps:不常用
  4. options:不常用

最后就可以执行npm run dev, 在界面上操作数字加减的例子了。

运行效果图

添加express服务接口、通过axios打通前后端

  1. 安装依赖

    npm install express axios -S
    
    express Express 是一个基于 Node.js 平台的极简、灵活的 web 应用开发框架
    axios 是一个基于PromiseHTTP网络请求库
    
  2. 新建server文件夹

改文件用于存放编写提供前台调用的接口

const express = require('express')
const app = express()

app.get('/api/user', (req, res) => {
    res.header('Access-Control-Allow-Origin', '*')
    res.send({
        name: 'tmc',
        age: 24
    })
})

app.listen(3000, () => {
    console.log('app listen port 3000')
})
  1. 启动服务

通过node来启动这个本地服务,也可以在webpack配置一条命令来启动。

执行:node server.js
在浏览器上就可以输入 http://localhost: 3000/api/user 来调用接口
  1. 在组件中调用该接口

新建一个/home/test.js 组件用于测试调用接口。

// home/test.js
import React, { Component } from 'react'
import axios from 'axios'

export default class Home extends Component{
    componentDidMount() {
	axios.get('http://localhost:3000/api/user').then(res => {
            console.log(res);
        })
    }

    render() {
        return (
            <div>
                this is home page.
            </div>
        )
    }
}

注意:在使用组件进行接口调用时,一定要在webpack配置文件中进行跨域的处理

// webpack.dev.config.js

devServer: {
    contentBase: path.join(__dirname, '../dist'),
    compress: true, // gzip压缩
    // host: '0.0.0.0', // 允许IP访问
    hot: true, // 热更新
    historyApiFallback: true, // 解决启动后刷新404
    port: 8888,
    proxy: { // 配置服务器代理 http://localhost:3000/api/user
        '/api': {
            target: 'http://localhost:3000',
            pathRewrite: {
                '^/api': '/api'
            },
            changeOrigin: true // 让target参数是域名
            // secure: false 设置支持https协议代理
        }
    }
}

最后重新运行npm run dev,打开浏览器,加载到刚才添加的这个组件后就可以看到调用接口返回的信息

接口返回结果

总结

通过从零一行一行配置一个完整的React项目开发环境,其实最重要的就是对Webpack的运用。了解并熟知Webpack的各项常见配置,不管是对个人能力的提升还是面试跳槽都有极大的好处。对希望提升自己的小伙伴一定要认真阅读,手动跟着上述文档一步一步敲一遍,当完成时,必定会有极大的收获!加油💪💪💪

加油