我的掘金

前端开发开发环境系列文章的 github 在这,如果您在看的过程中发现了什么不足和错误,感谢您能指出!

虽然目前市面上有很多的前端脚手架以及一体化的框架,比如create-react-app、umi等。但是作为一个程序员,自己写过更有助于提升在开发过程中发现问题和解决问题的能力。

Webpack基础

Webpack是一个静态模块打包器,它将所有的资源都看作模块。通过一个或多个入口,构建一个依赖图谱(dependency graph)。然后将所有模块组合成一个或多个bundle

<div align="center">
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f6c514b8cdf54d19b073a85dc297390a~tplv-k3u1fbpfcp-zoom-1.image" width = "300" alt="" align=center />
</div>

可以通过一个简单的例子来初步了解Webpack

比如: 我们想要使用es6的箭头函数来写一个功能,但是有的浏览器不支持(IE6-11或其他老版本浏览器)。那么这个用户在加载这个js资源的时候就会报错。

但这显然不是我们想要的结果,这时候就需要用到webpack或像gulp这样的构建工具来帮助我们将es6的语法转化成低版本浏览器可兼容的代码。

那么用webpack来配置一个构建工具时如下:

  1. 创建一个目录,并yarn init初始化一个包管理器
  2. 安装webpack yarn install webpack webpack-cli -D
  3. 想要将es6转化为es5语法,需要用到babel插件对代码进行编译,所以需要安装babel和相应的loader yarn add @babel/core @babel/preset-env babel-loader -D
  4. 配置.babelrc

    {
     "presets": [
         [
             "@babel/preset-env",
             {
                 "modules": false
             }
         ]
     ]
    }
  5. 创建src/index.js 入口

    const sum = (a, b) => a + b;
    console.log(sum(1, 2))
  6. 创建输出文件 dist/html

    <!DOCTYPE html>
    <html lang="en">
    <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>Document</title>
    </head>
    <body>
     <script src="./bundle.js"></script>
    </body>
    </html>
  7. 然后就是配置webpack.config.js

    const webpack = require('webpack');
    const path = require('path');
    
    const config = {
      entry: './src/index.js',
      output: {
     path: path.resolve(__dirname, 'dist'),
     filename: 'bundle.js'
      },
      module: {
     rules: [
       {
         test: /\.js$/,
         use: 'babel-loader',
         exclude: /node_modules/
       }
     ]
      }
    };
    
    module.exports = config;
  8. 最后通过构建命令./node_modules/.bin/webpack --config webpack.config.js --mode development 运行配置,会生成一个dist/bundle.js文件,这就是转换后的js文件
/*
 * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
 * This devtool is neither made for production nor for readable output files.
 * It uses "eval()" calls to create a separate source file in the browser devtools.
 * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
 * or disable the default devtool with "devtool: false".
 * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
 */
/******/ (() => { // webpackBootstrap
/******/     var __webpack_modules__ = ({

/***/ "./src/index.js":
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
/***/ (() => {

eval("var sum = function sum(a, b) {\n  return a + b;\n};\nconsole.log(sum(1, 2));\n\n//# sourceURL=webpack://webpack-config/./src/index.js?");

/***/ })

/******/     });
/************************************************************************/
/******/     
/******/     // startup
/******/     // Load entry module and return exports
/******/     // This entry module can't be inlined because the eval devtool is used.
/******/     var __webpack_exports__ = {};
/******/     __webpack_modules__["./src/index.js"]();
/******/     
/******/ })()
;

上面这个例子就使用了webpack的几个核心概念

  1. 入口 entry

在webpack的配置文件中通过配置entry告诉webpack所有模块的入口在哪里

  1. 输出 output

output配置编译后的文件存放在哪里,以及如何命名

  1. loader

loader其实就是一个pure function,它帮助webpack通过不同的loader处理各种类型的资源,我们这里就是通过babel-loader处理js资源,然后通过babel的配置,将输入的es6语法转换成es5语法再输出

  1. 插件 plugin

上面的例子暂时没有用到,不过也很好理解,plugin就是loader的增强版,loader只能用来转换不同类型的模块,而plugin能执行的任务更广。包括打包优化、资源管理、注入环境变量等。简单来说就是loader能做的plugin可以做,loader不能做的plugin也能做

以上就是webpack的核心概念了

添加Webpack配置

解析React + TS

了解了Webpack的基础后进行下面的操作

  1. 首先是安装需要的库

yarn add react react-dom react-hot-loader -S

yarn add typescript ts-loader @hot-loader/react-dom -D

  1. 修改babel
{
  presets: [
    [
      '@babel/preset-env',
      {
        modules: false
      }
    ],
    '@babel/preset-react'
  ],
  plugins: [
    'react-hot-loader/babel'
  ]
}
  1. 配置tsconfig.json

    {
     "compilerOptions": {
         "outDir": "./dist/",
         "sourceMap": true,
         "strict": true,
         "noImplicitReturns": true,
         "noImplicitAny": true,
         "module": "es6",
         "moduleResolution": "node",
         "target": "es5",
         "allowJs": true,
         "jsx": "react",
     },
     "include": [
         "./src/**/*"
     ]
    }
  2. 然后就是配置解析react和ts的 loader

webpack.config.js

const config = {
    ...
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/, // 新增加了jsx,对React语法的解析
        use: 'babel-loader',
        exclude: /node_modules/
      },
      {
        test: /\.ts(x)?$/, // 对ts的解析
        loader: 'ts-loader',
        exclude: /node_modules/
      }
    ]
  },
  ...
};

module.exports = config;

解析图片和字体

1. 下载loader

yarn add file-loader url-loader -D

2. 修改webpack配置

const config = {
    ...
  module: {
    rules: [
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/, // 解析字体资源
        use: 'file-loader'
      },
      {
        test: /\.(png|jpg|jpeg|gif)$/, // 解析图片资源,小于10kb的图解析为base64
        use: [
            {
                loader: 'url-loader',
                options: {
                    limit: 10240
                }
            }
        ]
      },
    ]
  },
  ...
};

解析css、less,使用MiniCssExtractPlugin将js中的css分离出来,形成单独的css文件,并使用postcss-loader生成兼容各浏览器的css

1. 安装loader

yarn add css-loader style-loader less less-loader mini-css-extract-plugin postcss-loader autoprefixer -D

2. 配置postcss.config.js

module.exports = {
  plugins: [
    require('autoprefixer')
  ]
};

3. 配置webpack

这里写了两个差不多的css-loader的配置,因为在项目中会同时遇到使用全局样式和局部(对应页面的)css样式。所以,配置了两个,使用exclude和css-loader中的options.modules: true来区分, 当创建的css文件名中带有module的就表示为局部css,反之为全局样式

// 引入plugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const config = {
    ...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1
            }
          },
          'postcss-loader'
        ],
        exclude: /\.module\.css$/
      },
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
              modules: true
            }
          },
          'postcss-loader'
        ],
        include: /\.module\.css$/
      },
      {
        test: /\.less$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'less-loader'
        ]
      }
    ]
  },

  plugins: [
    new MiniCssExtractPlugin()
  ],

  ...
};

使用文件指纹策略(hash、chunkhash、contenthash)

为什么会有这些配置呢?因为浏览器会有缓存,该技术是加快网站的访问速度。但是当我们用webpack生成js和css文件时,内容虽然变化了,但是文件名没有变化。所以浏览器默认的是资源并没有更新。所以需要配合hash生成不同的文件名。下面就介绍一下这三种有什么不同

fullhash

该计算是跟整个项目的构建相关,就是当你在用这个作为配置时,所有的js和css文件的hash都和项目的构建hash一样

chunkhash

hash是根据整个项目的,它导致所有文件的hash都一样,这样就会发生一个文件内容改变,使整个项目的hash也会变,那所有的文件的hash都会变。这就导致了浏览器或CDN无法进行缓存了。

而chunkhash就是解决这个问题的,它根据不同的入口文件,进行依赖分析、构建对应的chunk,生成不同的哈希

比如 a.87b39097.js -> 1a3b44b6.js 都是使用chunkhash生成的文件
那么当b.js里面内容发生变化时,只有b的hash会发生变化,a文件还是a.87b39097.js
b文件可能就变成了2b3c66e6.js

contenthash

再细化,a.js和a.css同为一个chunk (87b39097),a.js内容发生变化,但是a.css没有变化,打包后它们的hash却全都变化了,那么重新加载css资源就是对资源的浪费。

而contenthash则会根据资源内容创建出唯一的hash,也就是内容不变,hash就不变

所以,根据以上我们可以总结出在项目中hash是不能用的,chunkhash和contenthash需要配合使用

webpack配置如下


const config = {
  output: {
    path: path.resolve(__dirname, 'dist'),
    // chunkhash根据入口文件进行依赖解析
    filename: '[name].[chunkhash:8].js'
  },
  module: {
    rules: [
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        type: "asset/resource",
        generator: {
          filename: 'fonts/[hash:8].[ext].[query]'
        },
        // use: 'file-loader',
      },
      {
        test: /\.(png|jpg|jpeg|gif)$/,
        // webpack5中使用资源模块代替了url-loader、file-loader、raw-loader
        type: "asset",
        generator: {
          filename: 'imgs/[hash:8].[ext].[query]'
        },
        parser: {
          dataUrlCondition: {
            maxSize: 4 * 1024 // 4kb
          }
        }
        // use: [
        //   {
        //     loader: 'url-loader',
        //     options: {
        //       // 文件内容的hash,md5生成
        //       name: 'img/[name].[hash:8].[ext]',
        //       limit: 10240,
        //     },
        //   },
        // ],
      },
    ]
  },
  plugins: [
    ...
    new MiniCssExtractPlugin({
        filename: `[name].[contenthash:8].css`
    }),
    ...
  ],
};

module.exports = (env, argv) => {
  if (argv.hot) {
    // Cannot use 'contenthash' when hot reloading is enabled.
    config.output.filename = '[name].[fullhash].js';
  }

  return config;
};

增加第三方库

React Router

React Router一共有6种 Router Components,分别是BrowserRouter、HashRouter、MemoryRouter、NativeRouter、Router、StaticRouter。
详细请看这里

安装&配置React-router

  1. 安装react-routeryarn add react-router-dom
  2. 安装@babel/plugin-syntax-dynamic-import来支持动态import yarn add @babel/plugin-syntax-dynamic-import -D
  3. 将动态导入插件添加到babel中

    {
     "plugins": ["@babel/plugin-syntax-dynamic-import"]
    }

webpack配置

在这篇文章中,我们要做的是一个单页应用,所以使用的是React Router6 BroserRouter, 它是基于html5规范的window.history来实现路由状态管理的。
它不同于使用hash来保持UI和url同步。使用了BrowserRouter后,每次的url变化都是一次资源请求。所以在使用时,需要在Webpack中配置,以防止加载页面时出现404

webpack.config.js


const config = {
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js',
    // 配置中的path是资源输出的绝对路径,而publicPath则是配置静态资源的相对路径
    // 也就是说 静态资源最终访问路径 = output.publicPath + 资源loader或插件等配置路径 
    // 所以,上面输出的main.js的访问路径就是{__dirname}/dist/dist/main.js
    publicPath: '/dist',
    ...
  },
  devServer: {
    // 将所有的404请求redirect到 publicPath指定目录下的index.html上
    historyApiFallback: true,
    ...
  },
}

关于publicPath请看这里

添加相关代码

编写react-router配置,使用React.lazy 和React.Suspense来配合import实现动态加载, 它的本质就是通过路由来分割代码成不同组件,Promise来引入组件,实现只有在通过路由访问某个组件的时候再进行加载和渲染来实现动态导入

config/routes.tsx

import React from 'react';

const routes = [
  {
    path: '/',
    component: React.lazy(() => import('../src/pages/Home/index')),
  },
  {
    path: '/mine',
    component: React.lazy(() => import('../src/pages/Mine/index')),
    children: [
      {
        path: '/mine/bus',
        component: React.lazy(() => import('../src/pages/Mine/Bus/index')),
      },
      {
        path: '/mine/cart',
        component: React.lazy(() => import('../src/pages/Mine/Cart/index')),
      },
    ],
  },
];

export default routes;

src/pates/root.tsx

import React, { Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import routes from '@/config/routes';

const Loading: React.FC = () => <div>loading.....</div>;

const CreateHasChildrenRoute = (route: any) => {
  return (
    <Route key={route.path} path={route.path}>
      <Route
        index
        element={
          <Suspense fallback={<Loading />}>
            <route.component />
          </Suspense>
        }
      />
      {RouteCreator(route.children)}
    </Route>
  );
};

const CreateNoChildrenRoute = (route: any) => {
  return (
    <Route
      key={route.path}
      path={route.path}
      element={
        <Suspense fallback={<Loading />}>
          <route.component />
        </Suspense>
      }
    />
  );
};

const RouteCreator = (routes: any) => {
  return routes.map((route: any) => {
    if (route.children && !!route.children.length) {
      return CreateHasChildrenRoute(route);
    } else {
      return CreateNoChildrenRoute(route);
    }
  });
};

const Root: React.FC = () => {
  return (
    <BrowserRouter>
      <Routes>{RouteCreator(routes)}</Routes>
    </BrowserRouter>
  );
};

export default Root;

App.tsx

import * as React from 'react';
import { sum } from '@/src/utils/sum';
import Header from './components/header';
import img1 from '@/public/imgs/ryo.jpeg';
import img2 from '@/public/imgs/乱菊.jpeg';
import img3 from '@/public/imgs/weather.jpeg';
import Root from './pages/root';

const App: React.FC = () => {
  return (
    <React.StrictMode>
      <Root />
    </React.StrictMode>
  );
};

export default App;

index.tsx

import * as React from 'react';
import { createRoot } from 'react-dom/client';

import App from './App';
import './styles.css';
import './styles.less';

const container = document.getElementById('app');
createRoot(container!).render(<App />);

redux

在一个中大型的项目中,统一的状态管理是必不可少的。尤其是在组件层级较深的React项目中,可以通过redux和react-redux来跨层级传输组件(通过实现react的Context)。它的好处如下:

  1. 避免一个属性层层传递,代码混乱
  2. view(视图)和model(模型)的分离,使得逻辑更清晰
  3. 多个组件共享一个数据,如用户信息、一个父组件与多个子组件

可以看一下这篇关于redux概念和源码分析的文章

使用官方推荐的@reduxjs/toolkit来统一管理在redux开发过程中经常使用到的middleware

1. 安装库

yarn add redux react-redux @reduxjs/toolkit

2. 创建一个redux的初始管理入口 src/core/store.ts

import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';

/**
 * 创建一个Redux store,同时自动的配置Redux DevTools扩展,方便在开发过程中检查
 **/
const store = configureStore({
});

// 定义RootState和AppDispatch是因为使用的是TS作为开发语言,
// RootState通过store来自己推断类型
export type RootState = ReturnType<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;
export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>;

export default store;

3. 在对应目录下创建不同组件的state

pages/mine/model/mine.ts

import { AppThunk, RootState } from '@/src/core/store';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { fetchCount } from '@/src/service/api';

// 为每个单独的store定义一个类型
interface CounterState {
  value: number;
}

// 初始state
const initialState: CounterState = {
  value: 0,
};

export const counterSlice = createSlice({
  name: 'mine',
  initialState,
  reducers: {
    increment: (state) => {
      // 在redux-toolkit中使用了immutablejs ,它允许我们可以在reducers中写“mutating”逻辑
      //(这里需要提一下redux的reducer本身是个纯函数,即相同的输入,总是会的到相同的输出,并且在执行过程中没有任何副作用。而这里的state.value+=1 实际就是state.value = state.value + 1,它修改了传入的值,这就是副作用。虽然例子中是简单类型,并不会修改源数据,但是如果存储的数据为引用类型时会给你的项目带来意想不到的bug),
      // 这就不符合redux对于reducer纯函数的定义了,所以使用immutablejs。让你可以写看似“mutating”的逻辑。但是实际上并不会修改源数据
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    // Use the PayloadAction type to declare the contents of `action.payload`
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
  },
});

// 支持异步dispatch的thunk,项目中比较常见,因为很多时候需要和后端交互,获取到后端数据,然后再保存到store中
export const asyncIncrement =
  (amount: number): AppThunk =>
  async (dispatch, getState) => {
    // selectCount(getState());
    const response = await fetchCount(amount);
    dispatch(incrementByAmount(response.data));
  };

// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actions;

// Other code such as selectors can use the imported `RootState` type
// export const selectCount = (state: RootState) => state.mine.value;

export default counterSlice.reducer;

4. 在视图中dispatch action 和使用state

mine/index.tsx

import React from 'react';
import { decrement, increment, asyncIncrement } from './model/mine';
import { useAppSelector, useAppDispatch } from '@/src/utils/typedHooks';

const Mine: React.FC = () => {
  // The `state` arg is correctly typed as `RootState` already
  const count = useAppSelector((state) => state.mine.value);
  const dispatch = useAppDispatch();

  return (
    <div>
      <button
        aria-label="Increment value"
        onClick={() => dispatch(increment())}
      >
        Increment
      </button>
      <span>{count}</span>
      <button
        aria-label="Decrement value"
        onClick={() => dispatch(decrement())}
      >
        Decrement
      </button>
      <button
        aria-label="Decrement value"
        onClick={() => dispatch(asyncIncrement(2))}
      >
        asyncIncrement
      </button>
    </div>
  );
};

export default Mine;

对axios进行二次封装

1.安装

yarn add axios

2.创建src/core/request.ts

import axios from 'axios';

// const { REACT_APP_ENV } = process.env;
const config: any = {
  // baseURL: 'http://127.0.0.1:8001',
  timeout: 30 * 1000,
  headers: {},
};

// 构建实例
const instance = axios.create(config);

// axios方法映射
const InstanceMaper = {
  get: instance.get,
  post: instance.post,
  delete: instance.delete,
  put: instance.put,
  patch: instance.patch,
};

const request = (
  url: string,
  opts: {
    method: 'get' | 'post' | 'delete' | 'put' | 'patch';
    [key: string]: any;
  }
) => {
  instance.interceptors.request.use(
    function (config) {
      // Do something before request is sent
      // 当某个接口需要权限时,携带token。如果没有token,重定向到/login
      if (opts.auth) {
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        config.headers.satoken = localStorage.getItem('satoken');
      }
      return config;
    },
    function (error) {
      // Do something with request error
      return Promise.reject(error);
    }
  );

  // Add a response interceptor
  instance.interceptors.response.use(
    function (response) {
      // Any status code that lie within the range of 2xx cause this function to trigger
      // Do something with response data
      console.log('response:', response);

      // http状态码
      if (response.status !== 200) {
        console.log('网络请求错误');
        return response;
      }

      // 后端返回的状态,表示请求成功
      if (response.data.success) {
        console.log(response.data.message);

        return response.data.data;
      }

      return response;
    },
    function (error) {
      // Any status codes that falls outside the range of 2xx cause this function to trigger
      // Do something with response error
      return Promise.reject(error);
    }
  );

  const method = opts.method;
  return InstanceMaper[method](url, opts.data);
};

export default request;

3. 配置webpack

在开发环境中,前端因为浏览器的同源限制,是不能跨域访问后端接口的。所以当我们以webpack作为开发环境的工具后,需要配置devServer的 proxy


module.exports = {

  devServer: {
    proxy: {
      '/api': 'http://127.0.0.1:8001',
    },
  },
}

到这里,一个基础的前端开发环境就搭建完成了,我们总结一下

总结

  1. 我们通过一个简单的webpack例子了解了entry、output、loader、plugin这4个核心概念
  2. 通过webpack和babel配合来解析react代码,并将不同的文件作为了一个module进行打包
  3. 通过增加第三方库对项目进行扩展

其他文章


lxnxbnq
301 声望103 粉丝