起因
在公司入职也很久了,发现很多下面的同学都不理解我们所用开发框架是如何搭建起来的,为了加强小伙伴们的基础功底,所以有了这篇文章。
此次我会带领大家一步步的来搭建一个开发框架,一共分四篇文章来写。
- 基础React开发框架的搭建
- 基础的node端抽取
- cli集成
- 开发规范
本文会实现什么
从0到1搭建一个基于webpack的react应用
搭建
准备工作
先来介绍一下我的开发环境
MacBook
node@15.5.0
yarn@1.22.10
在进行下一步前为了加快我们的下载速度,请先执行如下命令
npm install -g yarn
npm install -g nrm
npm install -g n
nrm use taobao
# 设置依赖安装过程中内部模块下载Node的淘宝镜像
npm config set disturl https://npm.taobao.org/mirrors/node/
# 设置常用模块的淘宝镜像
npm config set sass_binary_site https://npm.taobao.org/mirrors/node-sass/
npm config set sharp_dist_base_url https://npm.taobao.org/mirrors/sharp-libvips/
npm config set electron_mirror https://npm.taobao.org/mirrors/electron/
npm config set puppeteer_download_host https://npm.taobao.org/mirrors/
npm config set phantomjs_cdnurl https://npm.taobao.org/mirrors/phantomjs/
npm config set sentrycli_cdnurl https://npm.taobao.org/mirrors/sentry-cli/
npm config set sqlite3_binary_site https://npm.taobao.org/mirrors/sqlite3/
npm config set python_mirror https://npm.taobao.org/mirrors/python/
本文会用到的全局的node模块
npm install -g http-server
代码篇
首先我们先来创建一个简单的开发项目
cd ~/Documents
mkdir react-base && cd react-base
yarn init -y
mkdir src config types public
我们现在的目录结构是这样的
├── config 存放webpack配置文件
├── package.json
├── public 存放与业务无关的静态资源文件
├── src 存放入口文件和业务相关代码
└── types 存放ts声明文件
我们先来安装一下我们的模块化打包工具webpack
yarn add webpack webpack-cli -D
在src
目录下创建main.js
文件当做入口文件,执行如下命令
echo "console.log('Hello World');" > src/index.js
此时运行yarn webpack
,会发现在dist目录下生成一个main.js
文件,可以看到如下是webpack
的工作机制。
因为webpack
只对模块化语法export
和import
提供了支持,对于es6
的其他语法并没有提供支持,此处我们要编译es6
和jsx
语法,需要使用babel
,webpack
的工作机制是先通过loader
对静态资源进行转换后输出到目标文件,此处引入babel-loader
,并在根目录下创建.babelrc
文件提供配置,如下是babel
为我们完成的转换工作。
yarn add babel-loader @babel/core @babel/preset-env @babel/preset-react -D
yarn add react react-dom
touch .babelrc
在.babelrc文件中配置如下
{
"presets": [
"@babel/preset-env", "@babel/preset-react"
]
}
创建src/App.js
,内容如下:
import React from 'react';
const App = () => {
return <div>Hello World</div>
}
export default App;
修改index.js
,内容如下
import React from 'react';
import ReactDom from 'react-dom';
import App from './App';
ReactDom.render(<App />, document.getElementById('root'))
在项目根目录下创建webpack.config.mjs
文件,此处使用mjs
后缀是为了避免在同一个项目中既使用CommonJs
也是用ES Module
,内容如下
import path from 'path';
const config = {
entry: './src/index.js',
output: {
path: path.resolve(path.resolve(), 'dist'),
filename: 'bundle.js'
},
module: {
rules: [{
test: /\.m?jsx?$/,
exclude: /node_modules/,
use: 'babel-loader'
}],
}
};
export default config;
要想让代码能够跑起来,我们还需要一个html
文件,在dist
目录下,创建index.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>
<div id="root"></div>
<script src="bundle.js"></script>
</body>
</html>
此时我们执行yarn webpack --mode none
,可以看到有文件dist/bundle.js
文件生成,将该文件进行折叠,可以看到是一个立即执行函数,有兴趣的小伙伴可以仔细看一下里面的内容,此时我们通过上面安装的http-server
来启动一个http
服务器,执行命令http-server dist
,可以看到已经为我们启动了8080
端口。
通过浏览器打开http://127.0.0.1:8080
,我们可以看到是白屏的,打开console
可以看到如下报错
我们分析一下打包后的代码,可以看到会根据环境变量的配置来判断引入是引入开发版本的react
还是生产版本的react
,此时process
变量没有被定义。
我们可以借助于webpack
的插件机制来解决该问题, 我们在webpack.config.mjs
的module
节点下添加如下代码
+ import webpack from 'webpack';
const config = {
...,
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development')
})
]
}
重新打包yarn webpack --mode none
,可以看到process.env.NODE_ENV === 'production'
已经被替换成false
。重新运行清空缓存刷新一下,可以看到页面上已经显示出Hello World
,代表我们的程序没有问题。
此处有一个问题就是html
文件和bundle
文件都是我们写死的,我们可以使用webpack
的另外一个插件html-webpack-plugin
,来根据模板来生成index.html
文件,并动态注入打包后的js
文件。
创建文件src/index.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>
<div id="root"></div>
</body>
</html>
安装依赖
yarn add html-webpack-plugin -D
更改webpack.config.mjs
,添加如下内容
...
+ import HtmlWebpackPlugin from 'html-webpack-plugin';
const config = {
...
plugins: [
...
+ new HtmlWebpackPlugin({
+ template: './src/index.html',
+ inject: 'body'
+ })
]
};
运行yarn webpack --mode none
,可以看到生成的在dist
目录下生成的index.html
文件中已经自动注入了bundle.js
。
引入typescript
同样的我们的框架也要支持typescript
,我们将src/app.js
更改为src/app.tsx
,内容不变,此处我们要添加ts
相关的依赖的内容。
yarn add typescript ts-loader -D
yarn add @types/react @types/react-dom -D
yarn tsc --init
此时根目录下会生成一个tsconfig.json
文件,我们不需要做过多的配置,将jsx
节点放开,并配置为react
,如下
"jsx": "react",
在webpack.config.mjs
文件的rules
节点下添加如下内容
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: 'ts-loader'
},
在output
节点下添加如下内容,此处的内容是为了避免添加后缀。
resolve: {
extensions: ['.mjs', '.js', '.json', ".ts", ".tsx"],
},
现在目录结构如下
├── config
├── dist
│ ├── bundle.js
│ └── index.html
├── package.json
├── public
├── src
│ ├── App.tsx
│ ├── index.html
│ └── index.js
├── tsconfig.json
├── types
├── webpack.config.mjs
└── yarn.lock
执行yarn webpack --mode none
同样可以正常显示。
添加css/less/scss支持
添加所用loader
yarn add style-loader css-loader less-loader sass-loader less sass -D
添加测试文件src/index.css
,src/index.less
,src/index.scss
。
src/index.css
.css {
background-color: #f00;
}
.less {
background-color: #0f0;
}
.scss {
background-color: #00f;
}
src/App.tsx
代码如下
import React from 'react';
import './index.css';
import './index.less';
import './index.scss';
const App = () => {
return <div>
<h1 className="css">Hello CSS</h1>
<h1 className="less">Hello Less</h1>
<h1 className="scss">Hello SCSS</h1>
</div>
}
export default App;
webpack.config.mjs
的rules
节点下添加如下内容
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader'],
},
{
test: /\.s[ac]ss$/i,
use: [
"style-loader",
"css-loader",
"sass-loader",
],
},
使用yarn webpack --mode none
进行打包,然后用http-server dist
启动,访问http://127.0.0.1:8080/
发现已经可以正常显示了。
简单介绍一下各个loader
的作用,sass-loader
和less-loader
是将.scss
和.less
代码转换成.css
代码,而css-loader
的作用是将css
的代码转换成js脚本
,style-loader
的作用是将生成的css
文件嵌入到style
标签中,简单看一下打包后的代码可以看到webpack
将css
代码当做一个模块放到了IIFE
中。
静态资源
我们在开发过程中也会使用到一些静态资源,比方说图片,此处为图片添加依赖,在webpack@5
版本下,我们一般会使用file-loader
和url-loader
,file-loader
来处理一些比较大的文件内容,而url-loader
则处理一些小的文件内容。在webpack@5
版本中功能已经被优化,请看如下描述。
我们新建一个页面src/pages/Home.tsx
,内容如下
import React from 'react';
import git from '@static/img/git.png';
const Home = () => {
return <div>
<h1>Home</h1>
<img src={git} />
</div>
}
export default Home;
添加图片src/static/img/git.png
。
此时我们可以看到已经编译报错了,错误信息如下:
找不到模块“@static/img/git.png”或其相应的类型声明。
这个地方是因为ts
没有办法解析.png
的类型声明,我们在创建types/static.d.ts
文件,内容如下
declare module '*.svg'
declare module '*.png'
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.gif'
declare module '*.bmp'
declare module '*.tiff'
此处因为我们不想一直通过相对路径的方式去找文件,所以我们对文件路径定义了别名,更改webpack.config.mjs
,在extensions
节点下添加如下配置
alias: {
'@pages': path.resolve(path.resolve(), './src/pages/'),
'@components': path.resolve(path.resolve(), './src/components/'),
'@static': path.resolve(path.resolve(), './src/static/'),
},
更改tsconfig.json
,添加如下配置
"baseUrl": "./",
"paths": {
"@pages/*": [
"./src/pages/"
],
"@static/*": [
"./src/static/"
],
"@components/*": [
"./src/components/"
]
},
然后我们添加loader
对图片文件进行处理,在webpack.config.mjs
的rules
节点下添加如下内容。
{
test: /\.png/,
type: 'asset/resource'
}
再运行打包,我们可以发现已经有一个图片文件被打包到了dist
目录下。
简单分析一下打包后的文件,我们发现loader
只是对文件名进行了处理,然后放入到了模块中。
另外一种比较常规的静态资源打包是如下的方式,这个时候webpack
会将文件转换成data:url
的形式放入到bundle.js
文件中,这种方式只建议小文件去使用,若太大则会影响到bundle.js
文件的体积,如下图,可以看到是一个很长的字符串。
{
test: /\.svg/,
type: 'asset/inline',
}
加快我们的开发步骤
上面的操作都是我们手动去完成的,目的是为了加深大家对打包过程的理解,真实开发过程中我们不会把生成文件直接写入到磁盘上,而是会放到内存中,加快编译速度。
webpack-dev-server
会我们的开发过程提供了更好的体验,他的便利如下
- 提供
http-server
- 提供热更新
- 提供代理
下面让我们来一步步的使用webpack-dev-server
提供http-server
安装依赖
yarn add webpack-dev-server -D
更改package.json
脚本,在跟节点下添加如下脚本
"scripts": {
"start": "webpack serve --mode development"
},
此时浏览器已经为我们开启了8080
端口
有些静态资源例如favicon.ico
放入到public
目录下,想要让应用访问到这个文件,就需要为webpack-dev-server
添加一个静态资源的路径转发,在webpack.config.mjs
的config
对象上添加如下属性
devServer: {
contentBase: path.join(path.resolve(), './public'),
},
此时运行yarn start
可以看到webpack
已经开始长期监听文件的变化,并自动进行了编译,打开http://localhost:8080/
可以看到favicon.ico
也已经被正常加载了。
提供热更新
在我们的开发过程中,不想每次更改代码后都要手工去刷新浏览器,那webpack-dev-server
为我们提供了自动刷新浏览器的配置,我们在webpack.config.mjs
的devServer
属性上添加hotOnly:true
,此时我们去修改css
文件,可以发现浏览器已经自动的刷新了。
这个地方有的同学可能会好奇为什么js
文件就没有提供这样的功能呢,这是因为css
的处理是比较common
的,所以style-loader
已经进行了处理。
而js
则需要我们根据使用框架的不同来定制化处理,好在社区已经有了现成的方案,我们选用react-hot-loader
,下面我们来安装一下
yarn add react-hot-loader @hot-loader/react-dom
在.babelrc
中添加plugins
,如下
{
"plugins": ["react-hot-loader/babel"]
}
对src/App.tsx
进行调整,调整后代码如下
+ import { hot } from 'react-hot-loader/root';
import React from 'react';
import './index.css';
import './index.less';
import './index.scss';
import Home from '@pages/Home';
const App = () => {
return <div>
<h1 className="css">Hello CSS</h1>
<h1 className="less">Hello Less</h1>
<h1 className="scss">Hello SCSS</h1>
<Home />
</div>
}
- export default App;
+ export default hot(App);
停掉服务重新运行yarn start
一下,此时我们在App
组件中添加一个输入框,然后在页面上输入内容123
,然后在App
组件中在添加一个输入框,发现第一个输入框的内容并没有消失,第二个输入框也同样被添加上去了,代表热重载成功。
提供代理
一个基于React
的SPA
应用,一般都会向服务端发起请求,而浏览器的安全策略经常会让我们遇到跨域的问题,此处我们使用github
的/users
接口来模拟一个简单的应用。
先安装所需依赖
yarn add axios
创建文件src/pages/Users/index.tsx
,文件内容如下
import React, { useEffect, useState } from 'react';
import axios from 'axios';
interface User {
login: string;
}
const Users = () => {
const [users, setUsers] = useState<Array<User>>([])
useEffect(() => {
axios.get('/api/users').then(res => {
setUsers(res.data)
})
}, []);
return <>
<ul>
{users.map(item => <li key={item.login}>{item.login}</li>)}
</ul>
</>
}
export default Users;
此时我们需要在webpack.config.mjs
文件为其配置代理,内容如下:
devServer {
...
proxy: {
'/api': {
target: 'https://api.github.com/',
pathRewrite: { '^/api': '' },
changeOrigin: true
}
}
}
在App.tsx
文件中引入该组件,并放到Home
组件后,重启后可以发现user
列表已经正常的展现出来了。
src/App.tsx
...
import Users from '@pages/Users';
const App = () => {
return (
...
<Users />
);
}
生产环境配置
现在我么已经有了一套可以在开发环境使用的框架,但是在开发环境和生产环境我们的打包策略是不一样的,此时我们对webpack
配置文件进行抽取,将公共各部分放到webpack.base.mjs
文件中,通过webpack-merge
对配置进行结合,更改我们现有的文件结构,如下
├── config
│ ├── webpack.base.mjs
│ ├── webpack.dev.mjs
│ └── webpack.prd.mjs
├── package.json
├── public
│ └── favicon.ico
├── src
│ ├── App.tsx
│ ├── index.css
│ ├── index.html
│ ├── index.js
│ ├── index.less
│ ├── index.scss
│ ├── pages
│ │ ├── Home
│ │ │ └── index.tsx
│ │ └── Users
│ │ └── index.tsx
│ └── static
│ └── img
│ └── git.png
├── tsconfig.json
├── types
│ └── static.d.ts
└── yarn.lock
安装所需依赖
yarn add webpack-merge -D
调整后config/webpack.base.mjs
文件内容如下
import path from 'path';
import HtmlWebpackPlugin from 'html-webpack-plugin';
const config = {
entry: './src/index.js',
output: {
path: path.resolve(path.resolve(), './dist'),
filename: 'bundle.js'
},
resolve: {
extensions: ['.mjs', '.js', '.json', ".ts", ".tsx"],
alias: {
'@pages': path.resolve(path.resolve(), './src/pages/'),
'@components': path.resolve(path.resolve(), './src/components/'),
'@static': path.resolve(path.resolve(), './src/static/'),
},
},
module: {
rules: [
{
test: /\.m?jsx?$/,
exclude: /node_modules/,
use: 'babel-loader'
},
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: 'ts-loader'
},
{
test: /\.png/,
type: 'asset/inline'
}
],
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
inject: 'body'
})
],
};
export default config;
调整后config/webpack.dev.mjs
内容如下
import { merge } from 'webpack-merge';
import base from './webpack.base.mjs';
import path from 'path';
import webpack from 'webpack';
export default merge(base, {
mode: 'development',
devtool: 'eval-cheap-module-source-map',
devServer: {
contentBase: path.join(path.resolve(), './public'),
hotOnly: true,
proxy: {
'/api': {
target: 'https://api.github.com/',
pathRewrite: { '^/api': '' },
changeOrigin: true
}
}
},
resolve: {
alias: {
'react-dom': '@hot-loader/react-dom',
},
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader'],
},
{
test: /\.s[ac]ss$/i,
use: [
"style-loader",
"css-loader",
"sass-loader",
],
},
]
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development')
}),
]
})
优化后的config/webpack.prd.mjs
内容如下
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
import { merge } from 'webpack-merge';
import CopyPlugin from "copy-webpack-plugin";
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
import base from './webpack.base.mjs';
import webpack from 'webpack';
export default merge(base, {
mode: 'production',
devtool: false,
module: {
rules: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
{
test: /\.less$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'],
},
{
test: /\.s[ac]ss$/i,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
"sass-loader",
],
},
],
},
optimization: {
minimize: true,
minimizer: [
// For webpack@5 you can use the `...` syntax to extend existing minimizers (i.e. `terser-webpack-plugin`), uncomment the next line
`...`,
new CssMinimizerPlugin(),
],
},
plugins: [
new CleanWebpackPlugin(),
new CopyPlugin({
patterns: [
{ from: "public", to: "." },
]
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
}),
new MiniCssExtractPlugin(),
],
})
此次添加了几个插件, 简单来介绍一下作用。
clean-webpack-plugin
在每次打包前清空dist
目录下内容copy-webpack-plugin
打包的时候将public
目录下的内容拷贝到dist
目录下mini-css-extract-plugin
将bundle.js
中的样式文件抽取到css
文件中css-minimizer-webpack-plugin
对抽取的css
文件进行压缩。
更改package.json
中的scripts
"start": "webpack serve --config config/webpack.dev.mjs",
"build:prd": "webpack --config config/webpack.prd.mjs"
安装依赖并执行构建
yarn add clean-webpack-plugin -D
yarn add copy-webpack-plugin -D
yarn add mini-css-extract-plugin -D
yarn add css-minimizer-webpack-plugin -D
yarn build:prd
下图是打包后的内容,可以看到未通过gzip
压缩的bundle.js
大小为143k
。
按需加载
当我们的项目越来越大后,bundle.js
文件的大小也会随之增大,那么页面的加载速度也会变慢,这个时候我们可以引入按需加载,也就是lazy load
,当我们需要用到一个组件的时候,再去加载这个页面的内容。
让我们来改进一下我们的项目,此处我们使用的是@loadable/component
,为了体现出效果引入react-router
yarn add react-router react-router-dom @loadable/component
yarn add @types/react-router @types/react-router-dom @types/loadable__component -D
修改src/App.js
为src/App.tsx
,目前发现以.tsx
为后缀的文件实现按需加载,此问题后续进行补充。
src/App.js
内容如下
import { hot } from 'react-hot-loader/root';
import React from 'react';
import './index.css';
import './index.less';
import './index.scss';
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom';
import loadable from "@loadable/component";
const Loading = () => {
return <div>Loading...</div>
}
const Home = loadable(() => import("@pages/Home"), {
fallback: <Loading />
});
const Users = loadable(() => import("@pages/Users"), {
fallback: <Loading />
});
const App = () => {
return <div>
<h1 className="css">Hello CSS</h1>
<h1 className="less">Hello Less</h1>
<h1 className="scss">Hello SCSS</h1>
return <>
<Router>
<div>
<nav>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/users">Users</Link></li>
</ul>
</nav>
<main>
<input />
<Switch>
<Route path="/" exact>
<Home />
</Route>
<Route path="/users">
<Users />
</Route>
</Switch>
</main>
</div>
</Router>
</>
</div>
}
export default hot(App);
.babelrc
添加plugin
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
"react-hot-loader/babel",
"@babel/plugin-syntax-dynamic-import"
]
}
执行yarn build:prd
,可以看到每个路由都进行了切分
此时dist
目录下的内容我们就可以和服务端代码部署到一起使用了。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!