共计 14272 个字符,预计需要花费 36 分钟才能阅读完成。
我的掘金
前端开发开发环境系列文章的 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 来配置一个构建工具时如下:
- 创立一个目录,并
yarn init
初始化一个包管理器 - 装置 webpack
yarn install webpack webpack-cli -D
- 想要将 es6 转化为 es5 语法,须要用到 babel 插件对代码进行编译, 所以须要装置 babel 和相应的 loader
yarn add @babel/core @babel/preset-env babel-loader -D
-
配置.babelrc
{ "presets": [ [ "@babel/preset-env", {"modules": false} ] ] } -
创立 src/index.js 入口
const sum = (a, b) => a + b; console.log(sum(1, 2)) -
创立输入文件 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> -
而后就是配置 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; - 最初通过构建命令
./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 的几个外围概念
- 入口 entry
在 webpack 的配置文件中通过配置 entry 通知 webpack 所有模块的入口在哪里
- 输入 output
output 配置编译后的文件寄存在哪里,以及如何命名
- loader
loader 其实就是一个 pure function,它帮忙 webpack 通过不同的 loader 解决各种类型的资源,咱们这里就是通过 babel-loader 解决 js 资源,而后通过 babel 的配置,将输出的 es6 语法转换成 es5 语法再输入
- 插件 plugin
下面的例子临时没有用到,不过也很好了解,plugin 就是 loader 的增强版,loader 只能用来转换不同类型的模块,而 plugin 能执行的工作更广。包含打包优化、资源管理、注入环境变量等。简略来说就是 loader 能做的 plugin 能够做,loader 不能做的 plugin 也能做
以上就是 webpack 的外围概念了
增加 Webpack 配置
解析 React + TS
理解了 Webpack 的根底后进行上面的操作
- 首先是装置须要的库
yarn add react react-dom react-hot-loader -S
yarn add typescript ts-loader @hot-loader/react-dom -D
- 批改 babel
{ | |
presets: [ | |
[ | |
'@babel/preset-env', | |
{modules: false} | |
], | |
'@babel/preset-react' | |
], | |
plugins: ['react-hot-loader/babel'] | |
} |
-
配置 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/**/*"] } - 而后就是配置解析 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
- 装置 react-router
yarn add react-router-dom
- 装置 @babel/plugin-syntax-dynamic-import 来反对动静 import
yarn add @babel/plugin-syntax-dynamic-import -D
-
将动静导入插件增加到 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)。它的益处如下:
- 防止一个属性层层传递,代码凌乱
- view(视图)和 model(模型)的拆散,使得逻辑更清晰
- 多个组件共享一个数据,如用户信息、一个父组件与多个子组件
能够看一下这篇对于 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',}, | |
}, | |
} |
到这里,一个根底的前端开发环境就搭建实现了,咱们总结一下
总结
- 咱们通过一个简略的 webpack 例子理解了 entry、output、loader、plugin 这 4 个外围概念
- 通过 webpack 和 babel 配合来解析 react 代码,并将不同的文件作为了一个 module 进行打包
- 通过减少第三方库对我的项目进行扩大
其余文章
- 开发一个 react+ ts+webpack 的前端开发环境(二)—— 配置 eslint、prettier、husky 等代码标准查看工具
- 开发一个 react+ ts+webpack 的前端开发环境(三)——webpack 配置优化