自从接触前端以来,每次创建一个新的项目时,为了图方便几乎都是使用create-react-app
脚手架来搭建的。到了工作中时,发现公司的项目都是大佬们已经搭建配置好的,根本不需要我去关心项目如何搭建的,以及内部的一些配置细节。虽然,现在市面上一些热门的打包工具(例如webpack、vite)都有看过,但总是看了就忘,没有亲身实践,总无法理解其中真正的意义。所以本文便借助网上资料自己手动搭建一个React项目。
一、初始化项目
首先创建一个项目文件夹,本文将其命名为 zero-react-app,用于存放项目文件,如下图:
然后进入 cmd 终端界面,切换到该文件夹的路径下:
然后在cmd中输入命令 npm init
初始化一个项目:
npm init
运行后界面如下所示:
需要输入一些项目相关信息:
- package name: 项目名称
- version:项目版本
- description:项目描述
- entry point:入口文件
- test command:测试命令
- git repository:Git仓库地址
- keywords:关键字
- license:版本信息
这些信息可以直接回车使用默认值,也可自定义设置。
npm init
命令运行成功后,项目文件夹中会创建一个 package.json
文件,里面存放的就是创建项目时指定的基础信息:
package.json文件是什么?
package.json 位于项目根目录,是项目的配置文件,例如配置项目启动、打包命令,声明依赖包等。
具体配置项可见npm doc 或 Github上的介绍。
二、安装webpack
首先安装webpack
npm install webpack webpack-cli -D
安装好后,项目文件夹如下所示,多了 node_modules 文件夹和 package-lock.json 文件。
node_modules文件夹中存放着所有安装的依赖包。
package.json 和 package-lock.json的区别
package.json:运行 npm init 时生成,主要作用是描述项目以及记录项目依赖的模块信息(模块名称、大版本信息等)
package-lock.json:运行 npm install 时生成,用于记录所有模块的详细信息,包括版本信息、模块来源以及依赖的小版本信息等。
之所以引入 package.json 文件,主要用途在于:
- 描述依赖关系树的单一表示,以便保证团队成员、部署和持续集成安装完全相同的依赖关系。(即锁定所有依赖的版本号)
- npm install 重新安装全部依赖时,可以通过 package-lock.json 文件指定的下载地址和相关依赖,相对加快下载速度。
有关package-lock.json文件中具体配置,可以见官方说明
那为什么要安装 webpack 呢?
按官方定义,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 >webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个依赖图(dependency graph),然后>将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。
安装好webpack后,需要在根目录下创建一个webpack.config.js
文件,这是webpack的配置文件。该文件配置主要包含以下几部分:
- 入口(entry):指示 webpack 应该使用哪个模块来作为构建其内部依赖图的开始。默认值是
./src/index.js
。具体配置可见官方文档
module.exports = {
entry: './path/to/my/entry/file.js',
};
- 输出(output):告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。主要输出文件的默认值是
./dist/main.js
,其他生成文件默认放置在./dist
文件夹中。
const path = require('path');
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
// bundle生成位置
path: path.resolve(__dirname, 'dist'),
// bundle文件名
filename: 'my-first-webpack.bundle.js',
},
};
- loader:webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中。loader主要用于转换源代码,可以使你在
import
或 “load(加载)” 模块时预处理文件。通常有两个属性:test
属性,识别出哪些文件会被转换。use
属性,定义出在进行转换时,应该使用哪个 loader
module.rules
允许你在 webpack 配置中指定多个 loader。
loader 支持链式调用。链中的每个 loader 会将转换应用在已处理过的资源上。一组链式的 loader 将按照相反的顺序执行。链中的第一个 loader 将其结果传递给下一个 loader,依此类推。最后,链中的最后一个 loader,返回 webpack 所期望的 JavaScript。
module.exports = {
module: {
rules: [
{
test: /.css$/,
use: [
// [style-loader](/loaders/style-loader)
{ loader: 'style-loader' },
// [css-loader](/loaders/css-loader)
{
loader: 'css-loader',
options: {
modules: true
}
},
// [sass-loader](/loaders/sass-loader)
{ loader: 'sass-loader' }
]
}
]
}
};
- 插件(plugin):loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。
webpack 插件是一个具有apply
方法的 JavaScript 对象。apply
方法会被 webpack compiler 调用,并且在 整个 编译生命周期都可以访问 compiler 对象。
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); // 访问内置的插件
const path = require('path');
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
filename: 'my-first-webpack.bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /.(js|jsx)$/,
use: 'babel-loader',
},
],
},
plugins: [
new webpack.ProgressPlugin(),
new HtmlWebpackPlugin({ template: './src/index.html' }),
],
};
- 模式(mode):主要有开发模式(
development
)和生产模式(production
)
三、创建项目目录结构
- 在项目根目录下创建
src
文件夹。src
文件夹用于存放项目代码文件.- 在其中创建一个
index.js
文件作为项目的入口文件。
- 在其中创建一个
- 在项目根目录下创建public文件夹。
- 在
public
文件夹下创建一个index.html
文件,作为生成HTML文件的模板。
- 在
初始项目目录结构如下图:
紧接着向 webpack.config.js 中写入一些基本的webpack配置:
const path = require('path');
module.exports = {
mode: 'development', // 开发模式
entry: './src/index.js', // 入口文件
output: {
// 必须是绝对路径
path: path.resolve(__dirname, 'dist'), //打包后文件的输出位置
filename: 'main.js', // 打包后的文件名
clean: true // 打包之前清理dist文件夹
},
}
我们在入口文件 ./src/index.js
文件中写入一段 JS 代码,测试是否能够打包成功:
然后运行 npx webpack --config webpack.config.js
进行打包,可以看到打包后输出到了./dist/main.js
。
注意:
npx webpack --config webpack.config.js
可以简写为npx webpack
,因为当存在webpack.config.js
文件时webpack会默认选择该文件,--config
参数表明可以传递任何名称的配置文件。
四、安装 HtmlWebpackPlugin 插件
该插件将自动生成一个 HTML5 文件, 在 body 中使用 script
标签引入你所有 webpack 生成的 bundle。
npm install -D html-webpack-plugin
安装好后,我们可以在webpack.config.js
文件中进行如下配置。
const htmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
...
plugins: [
new htmlWebpackPlugin({
template: './public/index.html', // 生成HTML文件的模板文件
filename: 'index.html', // 生成的HTML文件名
inject: 'body' // <script>标签插入的地方
})
],
...
}
然后我们每次使用 webpack 进行打包时,都会自动在dist文件夹中自动生成一个 index.html
文件。
五、加载样式文件
项目中存在各种各样的样式文件,我们可以通过loader
来加载。通常我们需要安装以下loader
:
style-loader
将模块导出的内容作为样式并添加到 DOM 中css-loader
加载 CSS 文件并解析 import 的 CSS 文件,最终返回 CSS 代码less-loader
加载并编译 LESS 文件sass-loader
加载并编译 SASS/SCSS 文件postcss-loader
使用 PostCSS 加载并转换 CSS/SSS 文件stylus-loader
加载并编译 Stylus 文件
以加载less文件为例;
首先安装相关loader 以及 less:
npm install -D style-loader css-loader less-loader less
然后在webpack.config.js
文件中添加如下配置:
module.exports = {
...
module: {
rules: [
{
test: /\.(css|less)$/,
use: ['style-loader', 'css-loader', 'less-loader']
}
]
}
...
}
当 webpack 打包 less 文件时,会依次调用'less-loader', 'css-loader', 'style-loader'
对less文件进行解析。['style-loader', 'css-loader', 'less-loader']
的书写顺序和调用顺序(从后往前)相反。
抽离和压缩CSS
MiniCssExtractPlugin
:会将CSS提取到单独的文件中,为每个包含CSS的JS文件(打包后的)创建一个CSS文件,并且支持CSS和SourceMaps的按需加载
npm install -D mini-css-extract-plugin
在 webpack.config.js
中添加如下配置:
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
...
plugins: [
new MiniCssExtractPlugin({
filename: './styles/[contenthash].css'
})
],
module: {
rules: [
{
test: /\.(css|less)$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
}
]
}
...
}
CssMinimizerWebpackPlugin
:这个插件使用cssnano优化和压缩CSS
npm install -D css-minimizer-webpack-plugin
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin')
module.exports = {
...
mode: 'production',
optimization: {
minimizer: new CssMinimizerWebpackPlugin()
}
...
}
六、静态资源打包
资源模块(asset module)是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外 loader。
在 webpack 5 之前,通常使用:
raw-loader
将文件导入为字符串url-loader
将文件作为 data URI 内联到 bundle 中file-loader
将文件发送到输出目录
资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:
asset/resource
发送一个单独的文件并导出 URL。之前通过使用file-loader
实现。asset/inline
导出一个资源的 data URI。之前通过使用url-loader
实现。asset/source
导出资源的源代码。之前通过使用raw-loader
实现。asset
在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用url-loader
,并且配置资源体积限制实现。
例如打包 .png
格式的图片:
module.exports = {
...
module: {
rules: [
{
test: /\.png$/,
type: 'asset/resource',
generator: {
filename: 'images/[contenthash][ext]'
}
},
]
}
...
}
此外 webpack 也可以打包 .csv
.xml
格式的文件,不过需要借助以下 loader:
- csv-loader:将 csv 文件内容转换成数组
- xml-loder:将 xml 文件内容转换成对象
这类静态资源如何打包,可根据官方文档以及具体项目需求进行配置。
七、支持 ES6 语法(babel-loader)
对于某些浏览器而言,并不支持 ES6 语法,所以需要使用babel-loader
将代码转换成 ES5 语法。假设我们在index.js
文件写入ES6的箭头函数:
然后我们看webpack打包后的文件:
可以看到,webpack 并未对其做任何转换。然后我们安装 babel-loader
npm install -D babel-loader @babel/core @babel/preset-env
- babel-loader :webpack中用babel解析ES6的桥梁
- @babel/core:babel的核心模块
- @babel/preset-env:babel预设,一组babel插件的集合
然后在webpack.config.js
文件中添加如下配置:
module.exports = {
...
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
...
}
然后再进行打包,可以看到箭头函数转换成了ES中的函数声明:
此外还需要安装regeneratorRuntime
插件,这是webpack打包生成的全局辅助函数,由babel生成,用于兼容async/await
语法。所以需要安装和配置插件:
// 该包中含有 regeneratorRuntime
npm install --save @babel/runtime
// 该插件会在需要regeneratorRuntime的地方自动require导入
npm install --save-dev @babel/plugin-transform-runtime
然后将配置文件修改如下:
module.exports = {
...
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
plugins: [
["@babel/plugin-transform-runtime"]
]
}
}
}
]
}
...
}
至此,babel相关配置已经完成。当然,实际项目中会根据实际要求进行更复杂的配置,可参考Babel官网。
八、集成React
本文目的在于搭建一个 React 项目,上述已经完成了基本配置,紧接着我们将引入React。首先安装React:
npm install -D react react-dom
此外,我们仍然需要安装@babel/preset-react
用于将 jsx
转换成 ES5.
npm install -D @babel/preset-react
然后在 webpack.config.js
中添加如下配置:
{
test: /\.(js|jsx)$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react'
],
plugins: [
// 主要改变在此处
["@babel/plugin-transform-runtime"]
]
}
}
}
然后我们在index.js
中写入如下代码:
HTML 模板文件也需要进行如下修改:
然后执行打包命令,打包后的文件在浏览器中显示如下:
至此,一个简单的React项目就搭建完成了。
九、模块热更新
在实际开发过程中,如果每次修改后都执行一次npx webpack
打包命令,操作过于繁琐。对此,我们引入webpack-dev-server
,提供一个本地的web服务,同时具有live reloading(实时重新加载)功能。
npm install -D webpack-dev-server
然后在 webpack.config.js
中添加如下配置:
module.exports = {
...
devServer: {
hot: true,
static: './dist'
},
...
}
然后在 package.json
文件的script
中添加启动命令:"start": "webpack-dev-server --open-app-name chrome"
然后我们就可以在命令行中执行npm start
启动服务了。
十、引入 TypeScript
目前大多数前端项目开发中使用的都是 TypeScript 。TypeScript 是 JavaScript 的超集,为其增加了类型系统,可以编译为普通 JavaScript 代码。若想在项目中使用 TypeScript 进行编码,首先我们需要安装TypeScript compiler 和 loader:
npm install -D typescript ts-loader
然后在根目录下添加一个tsconfig.json
文件,然后输入基本配置:
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "es6",
"target": "es5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node"
}
}
关于 TypeScript 更细致的配置,可查看 TypeScript 官方文档 了解更多关于 tsconfig.json
的配置选项。
到此,一个简单的React项目就搭建好了,当然在实际开发中还有更加复杂的配置,大家可参考 webpack官方文档进行配置。
本文是自己学习手动搭建React项目的过程记录,当中存在错误或者不合理的地方,欢迎大家指正。
参考: