基础-搭建react项目教程

2,164 阅读10分钟

前言

本文详细的介绍如何从头开始搭建一个react项目,帮助新人了解项目搭建流程和打包流程。
需要注意包版本问题,不同版本代表着不同的代码。如果包版本不同启动项目时可能会出现未知错误。

目录说明

└─config                             #webpack配置文件
    │  webpack.build.config.js       #webpack生产配置文件
    │  webpack.dev.config.js         #webpack开发配置文件
│  dist                              #打包后的文件
│  node_modules                      #npm下载的包文件
└─server                             #json-server 的数据来源
    │  db.json			             #数据
└─src                                #项目源码
    │  images                        #图片文件      
    ├─pages                          #页面目录
        │  home.css                  #样式文件
        │  Home.js                   #页面组件
        │  home.less                     
        │  Page1.jsx                       
        │  Page2.tsx                 #ts 文件
        │  Thunk.js                      
    ├─store                          #redux store配置
        │  action.js                 
        │  actionsUser.js            
        │  reducer.js                     
    ├─types 
    	│  types.d.ts                #自定义 ts类型
    │ index.html
    │ index.js                       #入口文件
    │ router.js                      #路由文件
│ .gitignore                         #git 上传过滤
│ babel.config.json                  #babel 配置文件 配置插件使用 
│ package-lock.json                  #包版本
│ package.json                       #npm 配置文件
│ postcss.config.js                  #postcss 配置文件
│ README.md                          #简介
│ tsconfig.json                      #ts配置检查文件
    
    

node

Node.js 是能够在服务器端运行 JavaScript 的开放源代码、跨平台 JavaScript 运行环境。
安装node教程
版本测试 --通过命令行测试版本号

npm

npm(全称 Node Package Manager,即“node包管理器”)是Node.js默认的、用JavaScript编写的软件包管理系统。NPM 使用介绍
如网速不好时可以使用淘宝npm镜像,安装成功后只需要把npm修改为cnpm执行就可以了。

npm install cnpm -g --registry=https://registry.npm.taobao.org

创建项目

创建项目根目录。可通过命令行也可自己创建文件夹。

mkdir react-obj 

初始化项目,后续命令都是进入根目录后执行。(可一直回车生)

npm init // 按照提示填写项目基本信息 生成 package.json 文件

webpack

安装webpackwebpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。

npm install --save-dev webpack
npm install --save-dev webpack@<version> // 安装指定版本
npm install --save-dev webpack-cli

npm 命令简写
install 安装命令 简写:i
--save-dev 开发时要依赖的东西 简写:-D
--save 发布时要依赖的东西 简写:-S
-g 全局安装包(加载命令最后)
注意:在webpack4之后 webpack-cli 和 webpack 就分为两个包

基础的配置 webpack文档, 新建config目录放置webpack开发配置webpack.dev.config.js

mkdir config 
cd config 
echo. > webpack.dev.config.js // window 创建文件
// webpack.dev.config.js
const path = require('path');

module.exports = {
    /* 入口 */
    entry: path.join(__dirname, '../src/index.js'),
    /* 输出到dist目录,输出文件名字为bundle.js */
    output: {
        path: path.join(__dirname, '../dist'),
        filename: 'bundle.js'
    }
};

新建index.js入口文件

mkdir src 
cd src 
echo. > index.js 
// index.js
document.getElementById('root').innerHTML = "Hello React";

修改 package.json 文件中 "scripts" 的值

{
  "name": "react-obj",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack --config ./config/webpack.dev.config.js"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^5.11.0",
    "webpack-cli": "^4.2.0"
  }
}

执行打包命令

npm run build
  • 全局安装 webpack 和 webpack-cli
  • 也可以直接执行打包命令 webpack --config ./config/webpack.dev.config.js

执行完成后可以看到生成了dist文件夹和bundle.js。内容就是index.js中的内容表示打包成功。

Babel

Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。Babel文档

  • @babel/core 调用Babel的API进行转码
  • @babel/preset-env 用于解析 ES6+
  • @babel/preset-react 用于解析 JSX
  • babel-loader 加载器
npm i @babel/core @babel/preset-env @babel/preset-react babel-loader -D

在根目录添加babel的配置文件 babel.config.json

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

修改 webpack.dev.config.js 文件

module.exports = {
  ...
  /* cacheDirectory是用来缓存编译结果,下次编译加速 */
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        use: ["babel-loader?cacheDirectory=true"],
        include: path.join(__dirname, "../src"),
      },
    ],
  }
  
};

修改 src/index.js

var tem = str => {
    document.getElementById('root').innerHTML = str;
};
tem('我现在在使用ES6!');

在dist文件夹下创建index.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
<div id="root"></div>
<script type="text/javascript" src="./bundle.js"></script>
</body>
</html>

执行打包命令

npm run build

用浏览器打开index.html文件看到 我现在在使用ES6! 打包成功。

React

npm i react react-dom -S

修改 src/index.js

import React from 'react';
import ReactDom from 'react-dom';

class Hello extends React.Component  {
    render() {
        return (
            <div>
                开始使用React!
            </div>
        )
    }
}

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

执行打包命令

npm run build

打开inde.html文件 看到 开始使用! 打包成功.

react-router

在 Web 前端单页应用 SPA(Single Page Application)中,路由描述的是 URL 与 UI (--react组件) 之间的映射关系,这种映射是单向的,即 URL 变化引起 UI 更新(无需刷新页面)。
react-router 也是使用javascript自带的 hashhistory api实现的。

npm i react-router-dom -S

现在开始使用路由。在src下创建文件夹pages和文件router.js,创建文件Home.jsPage1.jsPage2.js

cd src
mkdir pages 
echo. > router.js
cd pages
echo. > Home.js
echo. > Page1.js
echo. > Page2.js
// src/psges/Home.js
import React from 'react';

export default class Hello extends React.Component  {
    render() {
        return (
            <div>
                我是首页
            </div>
        )
    }
}
// src/psges/Page1.js
import React from "react";

export default function () {
  return <div>我是页面1</div>;
}
// src/psges/Page2.js
import React from "react";

export default function () {
  return <div>我是页面2</div>;
}
// src/router.js
import React from "react";
// 引入路由组件
import { BrowserRouter, Route, Switch, Link } from "react-router-dom";
import Home from "./pages/Home";
import Page1 from "./pages/Page1";
import Page2 from "./pages/Page2";

// 添加404页面
function NotFound(){
    return <div>404</div>;
}

export default function () {
  return (
    <BrowserRouter>
      {/* 开始使用路由 */}
      <div>
        <div>
          {/* 通过 Link 修改浏览器的url Switch组件 通过url 来判断展示那个Route 包裹的组件页面 */}
          <Link to="/">首页</Link>
        </div>
        <div>
          <Link to="/page1">Page1</Link><br></br>
          <Link to="/page2">page2</Link>
        </div>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route path="/page1" component={Page1} />
          <Route path="/page2" component={Page2} />
          {/* 路由找不到 加载404组件 */}
          <Route component={NotFound} />
        </Switch>
      </div>
    </BrowserRouter>
  );
}

// 修改 src/index.js
import React from 'react';
import ReactDom from 'react-dom';
import Root from './router';

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

现在执行打包命令,打开index.html看到页面能正常输出。但是路由是不能跳转的,因为现在是根目录打开无法跳转路由。
需要安装 webpack-dev-server,是webpack官方提供的一个小型Express服务器。

npm i webpack-dev-server -D
// 修改 package.json
"scripts": {
  "start": "webpack-dev-server --config ./config/webpack.dev.config.js",
  "build": "webpack --config ./config/webpack.dev.config.js"
},
//在 `webpack.dev.config.js` 添加配置
module.exports = {
  ...
  
  devServer: {
    contentBase: path.join(__dirname, '../dist'), 
    compress: true,  // gzip压缩
    host: '0.0.0.0', // 允许ip访问
    hot:true, // 热更新
    historyApiFallback:true, // 解决启动后刷新404
    port: 8000 // 端口
  },

};

--这里就遇到了包版本问题 前面安装webpack-cli@4 版本 和 webpack-dev-server@3 版本 是不兼容的 启动就会报错。这里把webpack-cli降低了版本。

npm uninstall webpack-cli -D
npm i webpack-cli@3 -D

启动服务,打开http://localhost:8000 就能开始使用路由了

npm start

devtool

在开发时需要定位错误,打包后的代码比较难以理解,这时候只需要在webpack.dev.config.js添加devtool 就能在错误的时候定位到自己写的代码。

devtool: 'inline-source-map',

React-Redux

Redux 其实就是找个,不能直接操作的对象存放一些共享数据。只能通过特殊的方式来获取和修改数据。我们这里使用的React-Redux其实就是对Redux进行的一次封装,把那个对象放在了props上。

npm i redux react-redux -S

下面我们来吧Home.js修改为使用react-redux的计数器。

// Home.js
import React from "react";
// 引入 react-redux 中的高阶函数 绑定获取和修改的特殊方法
import { connect } from 'react-redux';
// 定义 store的修改类型
import { addAction, reduceAction } from "../store/action";

function Home(props) {
  const { count, onAddClick, onReduceClick } = props;
  return (
    <div>
      <span>我是首页计数器{count}</span>
      <button onClick={onAddClick}></button>
      <button onClick={onReduceClick}></button>
    </div>
  );
}
// 获取store中的值到当前组件
function mapStateToProps(state) {
  return {
    count: state.count,
  };
}
// 修改store中的值
function mapDispatchToProps(dispatch) {
  // 获取action.js 中定义的特殊类型
  return {
    onAddClick: () => dispatch(addAction),
    onReduceClick: () => dispatch(reduceAction),
  };
}
// connect接收两个参数 叫什么都行
// 第一个参数获取 reducer.js 中 counter 定义的state的值
// 第二个参数通过 回调 dispatch 传入指定类型 来修改 reducer.js 中 counter 定义的state
export default connect(mapStateToProps, mapDispatchToProps)(Home);

在根目录创建store文件夹,在其中创建action.jsreducer.js

cd src
mkdir store 
cd store
echo. > action.js
echo. > reducer.js
// store/reducer.js 创建redux中的store对象,设置修改对象的类型。
import { createStore } from 'redux';

function counter(state = { count: 0 }, action) {
  const count = state.count;
  switch (action.type) {
    case "add":// 约定的类型
      return { count: count + 1 };
    case "reduce":
      return { count: count - 1 };
    default:
      return state;
  }
}

// 创建 Store
const store = createStore(counter)
// 导出要使用的Store
export default store
// store/action.js 
// 根据reducer.js 设置的约定类型 定义修改store的 公用action对象
const addAction = { type: 'add' }
const reduceAction = { type: 'reduce' }

export { addAction, reduceAction}
// 修改router.js 
// 配置store对象 放入组件最顶级的props中
...
import { Provider } from 'react-redux'
import Home from "./pages/Home";
import Page1 from "./pages/Page1";
import Page2 from "./pages/Page2";
import store from "./store/reducer";// 引入store

...

export default function () {
  return (
    <Provider store={store}>
      {/* 把store放入props中让所有子组件都能获取到  */}
      ...
    </Provider>
  );
}
// 修改pages/Page1.js 测试 --当在其他组件中修改store的值后 每个组件对应的值都修改了
import React from "react";
import { connect } from 'react-redux'

function Home(props) {
  const { count } = props;
  return <div>我是页面1--{count}</div>;
}
// 获取store中的值
export default connect((state)=>({count: state.count}))(Home);

启动项目,当Home.js中修改计数值,切换路由到Page1.js计数值跟着修改。这样Redux就创建成功了。
在这里梳理一下我们做了什么。
1.通过 reducer 创建我们需要的store对象和修改store对象的类型,这里可以创建多个reducer合并在一起。
2.通过 action 设置公用的约定类型对象。
3.通过 Provider 把store 放入顶部props。
4.通过 connect 第一个参数获取store的值,通过第二个参数传入action对象修改store。

集成TypeScript

npm i typescript ts-loader -D

typescript 基础安装包
ts-loader 解析.ts文件 为 es5的.js文件

修改package.jsonscripts,然后 npm run tsc 初始化 TypeScript ,生成 tsconfig.json 文件,管理工程配置,例如你想包含哪些文件和进行哪些检查。

  "scripts": {
    ...
    "tsc": "tsc --init"
  },

webpack.dev.config.js的 module -> rules 中添加

module: {
    rules: [
      ...
      {
        test:/\.(ts|tsx)?$/,
        use:'ts-loader',
        include: path.join(__dirname, "../src"),
        exclude: /node_modules/
      },
    ],
},
// webpack.dev.config.js 中添加
// import导入文件 无需后缀配置
module.exports = {
  ...
  resolve: {
    extensions: [".js", ".jsx", ".json",".ts",".tsx"]
  },
}

修改 Page2.jsPage2.tsx,发现文件报错。
安装 react@types/react 获取ts类型,修改tsconfig.json 添加 "jsx": "react" 设置jsx验证。
当遇见框架没有@types包时在src下创建types -> types.d.ts在文件中自定义类型。

npm i @types/react -D
// tsconfig.json
{
  "compilerOptions": {
    "jsx": "react",
    ...
  },
  "include": [  // 所需要用ts的文件
    "src/**/*",
  ],
}
cd src
mkdir types 
cd types
echo. > types.d.ts
// types.d.ts
declare module "react-redux" {
    export {}
}
// Page2.tsx
import React from "react";

// 使用ts设置 强类型
interface pageProps{
  num?:number,
  text:string,
}

function Test(props:pageProps){
  return <div>{props.text}{props.num}</div>;
}

export default function () {

  return <Test text="使用Ts。你好我是页面" num={2}/>;
}

重新启动页面正常加载,配置成功,就可以正常使用ts了。

添加css

npm i css-loader style-loader -D

css-loader 解析类似@import 和 url(...)等方法实现。
style-loader 解析css文件,打包后加入页面。

webpack.dev.config.js的 module -> rules 中添加

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

在pages中创建 home.css

echo. > home.css
// home.css
.home {
    border: 1px solid rgb(108, 92, 153);
    display: flex;
}
// 修改`Home.js`
...
import "./home.css"

function Home(props) {
  ...
  return (
    <div className="home">
      ...
    </div>
  );
}
...

集成PostCSS优化

集成PostCSS 后,可以自动给css属性加浏览器前缀。
postcss-cssnext 允许你使用未来的 CSS 特性(包括 autoprefixer)。

npm i postcss-loader postcss-cssnext -D

修改webpack.dev.config.jsmodule -> rules

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

然后在根目录下新建postcss.config.js

module.exports = {
    plugins: {
        'postcss-cssnext': {}
    }
};

启动服务npm start查看css是否加上前缀,加上表示成功。

添加less

npm i less less-loader -D

less 基础安装包
less-loade 把.less解析为.css文件 在webpack.dev.config.js的 module -> rules 中添加

{
  test: /\.less$/,
  use: ["style-loader","css-loader",
    {
      loader: "less-loader",
      options: {
        lessOptions: {
          strictMath: true,
        },
      },
    },
    "postcss-loader",
  ],
},

在pages中创建 home.less

echo. > home.less
.home {
    .hLess{
        color: #379adb;
    }
}
// 修改`Home.js`
...
import "./home.less";

function Home(props) {
  ...
  return (
    <div className="home">
     <span className="hLess">--我是首页计数器{count}</span>
      ...
    </div>
  );
}
...

启动服务npm start看见 我是首页计数器 改变颜色表示加载成功。

添加图片

npm i url-loader file-loader -D

在src中添加images文件夹放入图片,修改Home.js

cd src
mkdir images
// Home.js
...
import timg from "../images/timg.jpg"

function Home(props) {
  ...
  return (
    <div className="home">
      ...
      <img src={timg} alt="图"/>
    </div>
  );
}
...

webpack.dev.config.js module -> rules 中添加

{
  test: /\.(png|jpg|gif)$/,
  use: [
    {
      loader: "url-loader",
      options: {
        // 控制小于10K的图片会被转成base64编码,直接插入HTML中.
        limit: 10240,
      },
    },
  ],
},

添加Ajax请求

现在没有后端服务,我们使用 json-server 来模拟后端服务,然后使用axios发起ajax请求。

npm i json-server -D
npm i axios -S

创建server文件夹,在里面创建db.json

mkdir server
cd server
echo. > db.json
{
  "users": [
    {
      "name": "a",
      "id": 1
    },
    {
        "name": "b",
        "id": 2
      }
  ]
}

修改package.jsonscripts

"server":"json-server server/db.json --watch --port 3000"

使用时需要分别启动两个服务,启动json-server后可直接查看数据。
因为启用了两个服务发送请求时需要跨域,需要设置代理proxy修改webpack.dev.config.js

devServer: {
    ...
    proxy: { // 配置服务代理
      '/api': {
           target: 'http://localhost:3000',
           // 转换请求 /api/users 为 http://localhost:3000/users
           pathRewrite: {'^/api' : ''},  
           changeOrigin: true
      }
    },
  },

修改Home.js

import React,{useEffect,useState} from "react";
// 引入 react-redux 中的高阶函数 绑定获取和修改的特殊方法
import { connect } from "react-redux";
import axios from "axios";
// 定义 store的修改类型
import { addAction, reduceAction } from "../store/action";
import "./home.css";
import timg from "../images/timg.jpg";

function Home(props) {
  const { count, onAddClick, onReduceClick } = props;

  // 使用HOOK state
  const [state,setState] = useState([]);

  // 使用HOOK 副作用函数 --第二个参数为空表示 子执行一次 
  // 有值 表示对应的值修改 就在执行一次
  useEffect(()=>{
    // 发送请求
    axios.get("/api/users").then((res) => {
      let data = JSON.parse(res.request.responseText);
      setState(data)
    });
  },[])

  return (
    <>
      <div className="home">
        <span className="hLess">{state[0]?.name}--我是首页计数器{count}</span>
        <button onClick={onAddClick}></button>
        <button onClick={onReduceClick}></button>
      </div>
      <img src={timg} alt="图" />
    </>
  );
}
// 获取store中的值到当前组件
function mapStateToProps(state) {
  return {
    count: state.count,
  };
}
// 修改store中的值
function mapDispatchToProps(dispatch) {
  // 获取action.js 中定义的特殊类型
  return {
    onAddClick: () => dispatch(addAction),
    onReduceClick: () => dispatch(reduceAction),
  };
}
// connect接收两个参数 叫什么都行
// 第一个参数获取 reducer.js 中 counter 定义的state的值
// 第二个参数通过 回调 dispatch 传入指定类型 来修改 reducer.js 中 counter 定义的state
export default connect(mapStateToProps, mapDispatchToProps)(Home);

分别启动两个服务

npm start
npm run server

redux-thunk中间件使用

在使用redux中需要通过请求获取数据修改store对象。但是redux的action必须是个对象不能直接传入函数。所以需要使用中间件来实现异步修改数据。

npm i redux-thunk -S

先创建user数据的reducer、action 和 测试页面。

// store/actionsUser.js

import axios from 'axios';

export const INFO = "user/INFO";
// 异步修改 store 的 state 
// connect函数 中的第二个参数需要的值
export function getUser() {
    return dispatch => {
        axios.get('/api/users').then((res)=>{
            let data = JSON.parse(res.request.responseText);
            dispatch({
                type: INFO,
                payload: data
            });
        })
    }
}
// 修改 store/reducer.js

import { createStore,applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { INFO } from "./actionsUser";// 获取约定类型

// 计数器
function counter(state = { count: 0 }, action) {
  switch (action?.type) {
    case "add":
      return { ...state,count: state.count + 1 };
    case "reduce":
      return { ...state,count: state.count - 1 };
    default:
      return state;
  }
}

// 异步修改
function reducer(state = { user:{} }, action) {
  switch (action?.type) {
      case INFO:
          return {
              ...state,
              user: action.payload,
          };
      default:
          return state;
  }
}

// 合并 reducer
function combineReducers(state = {}, action) {
  return {
      counter: counter(state.counter, action),
      reducer: reducer(state.user, action)
  }
}

// 创建store 对象
const store = createStore(combineReducers,applyMiddleware(thunkMiddleware));

// 导出要使用的Store
export default store

创建 pages/Thunk.js

import React from "react";
// 引入 react-redux 中的高阶函数 绑定获取和修改的特殊方法
import { connect } from "react-redux";
// 定义 store的修改类型
import { getUser } from "../store/actionsUser";

function Home(props) {
  const { user = [], getUser} = props;

  return (
    <div>
      <span>我是异步修改数据{user[0]?.name}</span>
      <button onClick={getUser}>修改</button>
    </div>
  );
}
// 获取store中的值到当前组件
function mapStateToProps(state) {
  return {
    user: state.reducer.user,
  };
}

export default connect(mapStateToProps, {getUser})(Home);

因为reducer.js修改,修改Home.jsPage1.js获取正常数据。

...
// 获取store中的值到当前组件
function mapStateToProps(state) {
  return {
    count: state.counter.count,
  };
}
...
...
// 获取store中的值
export default connect((state)=>({count: state.counter.count,}))(Home);

最后在router.js中添加组件路由。

...
import Thunk from "./pages/Thunk.js";
...

return (
  ...
  <div>
    <Link to="/thunk">thunk页</Link>
  </div>
  <Switch>
      ...
      <Route exact path="/thunk" component={Thunk} />
  </Switch>
  ...
)

npm start 重新启动,进入路由/thunk点击修改,能获取到数据,表示异步加载数据成功。

缓存优化

浏览器自带文件缓存,当第一次加载了bundle.js,第二次就不会下载这个文件,需要客户清空本地缓存后才会重新下载。
所以我们生成文件时都生成不同的名字。修改webpack.dev.config.jsoutputfilename 中自动生成文件的参数。

module.exports = {
  ...
  output: {
      path: path.join(__dirname, "../dist"),
      filename: '[name].[chunkhash].js',
  }
  ...
}

提取公共包

我们通过npm下载的公共库中的代码是不会修改的,所以不需要打包在bundle.js中,需要我们提取为公共代码。
webpack4之前使用的CommonsChunkPlugin插件,在webpack4之后改为optimization.splitChunks

module.exports = {
  ...
  // 优化项配置
  optimization: { 
    // 分割代码块
    splitChunks: { 
      // -默认作用于异步chunk,值为all/initial/async/function(chunk),
      // -值为function时第一个参数为遍历所有入口chunk时的chunk模块,
      // -chunk._modules为chunk所有依赖的模块,通过chunk的名字和所有依赖模块的resource可以自由配置,
      // -会抽取所有满足条件chunk的公有模块,以及模块的所有依赖模块,包括css
      // chunks: "all"
      // 设置缓存组用来抽取满足不同规则的chunk
      cacheGroups: {
        // 抽取第三方模块
        vendor: { 
          name: 'vendor',// 模块名
          test: /node_modules/, // 如果你多次引用了node_modules第三方模块,就抽取出来
          chunks: "all"
        }
      }
    }
  },
  ...
}

HtmlWebpackPlugin优化

npm i html-webpack-plugin -D

每次自动把js插入到模板index.html里面。新建src/index.html,把dist文件夹删除。

cd src
echo. > index.html
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

修改webpack.dev.config.js,增加plugin

...
var HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
	...
    plugins: [
      new HtmlWebpackPlugin({
        filename: "index.html",
        template: path.join(__dirname, "../src/index.html"),
      }),
    ],
  ...
}

重新运行npm start如果能正常访问就表示成功。

文件压缩

使用uglifyjs-webpack-plugin来压缩文件,使打包出来的文件体积更小。

npm i uglifyjs-webpack-plugin -D
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
  plugins: [
    new UglifyJSPlugin()
  ]
}

webpack4+中添加了 模式(mode) 设置模式为production 会自动安装这个插件

module.exports = {
  mode:"production"
}

清空上一次的打包文件

webpack 每次打包,都会生成新的文件放入dist文件夹下,我们可以使用clean-webpack-plugin在打包前清除上一次的文件。

npm i clean-webpack-plugin -D
// 清空上一次的打包文件
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

...
plugins: [
    ...
    new CleanWebpackPlugin()
],
...

生产坏境构建

开发项目和打包发版时使用webpack的插件是不一样的。
所以我们创建一个和 webpack.dev.config.js 内容一样的 webpack.build.config.js,根据不同环境修改配置。

// package.json 修改 build 时调用 webpack.build.config.js
"scripts": {
    "start": "webpack-dev-server --config ./config/webpack.dev.config.js",
    "build": "webpack --config ./config/webpack.build.config.js",
    "server": "json-server server/db.json --watch --port 3000",
    "tsc": "tsc --init"
},
// 删除开发时才需要的配置
const path = require("path");
// html自动打包
var HtmlWebpackPlugin = require("html-webpack-plugin");
// 清空上一次的打包文件
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  // 模式 开始模式 会自动预设安装插件 模式不同安装插件不同
  // 可以使用 node 自带的 process.env.NODE_ENV 来获取所在的环境
  mode: 'production',// production 生产模式  development 开发模式

  /* 入口 */
  entry:  path.join(__dirname, "../src/index.js"),

  /* 输出到dist目录,输出文件名字为bundle.js */
  output: {
    path: path.join(__dirname, "../dist"),
    filename: '[name].[chunkhash].js',
  },
  // 加载打包前的代码
  devtool: "cheap-module-source-map",

  /* cacheDirectory是用来缓存编译结果,下次编译加速 */
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        use: ["babel-loader?cacheDirectory=true"],
        include: path.join(__dirname, "../src"),
      },
      {
        test:/\.(ts|tsx)?$/,
        use:'ts-loader',
        include: path.join(__dirname, "../src"),
        exclude: /node_modules/
      },
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader","postcss-loader"],
      },
      {
        test: /\.(png|jpg|gif)$/,
        use: [
          {
            loader: "url-loader",
            options: {
              // 控制小于10K的图片会被转成base64编码,直接插入HTML中.
              limit: 10240,
            },
          },
        ],
      },
      {
        test: /\.less$/,
        use: ["style-loader","css-loader",
          {
            loader: "less-loader",
            options: {
              lessOptions: {
                strictMath: true,
              },
            },
          },
          "postcss-loader",
        ],
      },
    ],
  },

  plugins: [
    new HtmlWebpackPlugin({
      /* html压缩 */
      minify: true,
      template: path.join(__dirname, "../src/index.html"),
    }),
    new CleanWebpackPlugin()
  ],
  
  // 优化项配置
  optimization: { 
    // 分割代码块
    splitChunks: { 
      // -默认作用于异步chunk,值为all/initial/async/function(chunk),
      // -值为function时第一个参数为遍历所有入口chunk时的chunk模块,
      // -chunk._modules为chunk所有依赖的模块,通过chunk的名字和所有依赖模块的resource可以自由配置,
      // -会抽取所有满足条件chunk的公有模块,以及模块的所有依赖模块,包括css
      // chunks: "all"
      // 设置缓存组用来抽取满足不同规则的chunk
      cacheGroups: {
        // 抽取第三方模块
        vendor: { 
          name: 'vendor',// 模块名
          test: /node_modules/, // 如果你多次引用了node_modules第三方模块,就抽取出来
          chunks: "all"
        }
      }
    }
  },
  // 导入文件 无需后缀
  resolve: {
    extensions: [".js", ".jsx", ".json",".ts",".tsx"]
  },
};

参考资料

菜鸟教程
Babel教程
webpackjs教程
Web 前端路由原理解析和实现
Redux 入门教程 TypeScript教程
搭建React全家桶框架教程

源码地址: github.com/nie-ny/reac…