我的掘金

前端开发开发环境系列文章的 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,反之为全局款式

// 引入pluginconst 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;}// 初始stateconst 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 functionexport 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. 通过减少第三方库对我的项目进行扩大

其余文章

  • 开发一个react+ ts+webpack的前端开发环境(二)------配置eslint、prettier、husky等代码标准查看工具
  • 开发一个react+ ts+webpack的前端开发环境(三)------webpack配置优化