wuqiren / blog

记录自己的学习过程
1 stars 0 forks source link

使用webpack5 搭建React项目 #1

Open wuqiren opened 3 years ago

wuqiren commented 3 years ago

前言

之前创建React或者Vue项目一直是使用官方推荐的脚手架, 在公司也是一直使用的是前辈实现的脚手架,未能深入了解脚手架所涉及的知识,这次决定学习下脚手架的知识,巩固一下Webpack以及学习最新的Webpack5知识。

这篇文章我会一步一步搭建,中间会讲解各个步骤的原因,后续会推出视频版本

注意事项

关于状态管理库我这里不会使用Redux或者Mobx,自从React引入hooks之后,对于Redux等依赖其实已经很少了,很多小型项目基本是不需要使用Redux,不过为了项目的完整性,我这里会使用一个小型的状态管理库(zustand)

目前接触过很多的后台管理项目,项目的简单程度完全不需要使用Redux这种非常大的状态管理库,非常影响项目的体积,各位读者可以回想下,你所接触的后台管理系统,各个页面数据交流真的多吗?

一个真正的易用的状态管理工具不需要过多复杂的概念,但这往往是设计过程中就要去思考到的,但我觉得使用者对于工具的设计感和创造者对于工具的设计感有时候会出现冲突,往往后者会沉溺于复杂的构建中,逐渐也就淡化了用户的感受。

预备知识

开始搭建

新建一个项目

进入项目的根目录,创建默认的package.json,

yarn init -y
或者
npm init -y

这里是可以使用npm 也可以使用yarn。我这里统一用yarn 进行安装

目录结构
|--package.json
package.json
{
  "name": "template",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
}

安装Webpack

yarn add webpack webpack-cli webpack-merge -D

为了区分开发与生产环境,方便维护和特殊处理,就不把所有的内容配置到webpack.config.js这一个文件里面。区分webpack的配置还是非常有必要的,可以加快开发环境的打包速度,有时候遇到开发环境打包慢,可以排查下是否配置有问题(如开发环境开启了代码压缩等),最终的整合使用webpack-merge工具

在根目录新建workflow目录,里面有三个代码文件

目录结构
|--package.json
|--workflow
|  |--webpack.base.config.js   通用配置
|  |--webpack.dev.config.js    开发环境配置
|  |--webpack.prod.config.js   生产环境配置

每当我们对webpack进行修改的时候,都需要验证下,对package.json做下修改,

package.json
{
  "name": "template",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "webpack": "^5.58.2",
    "webpack-cli": "^4.9.1",
    "webpack-merge": "^5.8.0"
   },
  "scripts": {
    "build": "webpack --config ./workflow/webpack.dev.config.js"
  }
}

这里有个坑 如果你的webpack的配置文件不是在最外层(或者你的名字不是webpack.config.js),而是按照我们这种写法来写,但是你的package.json 的script中的build命令写法按照webpack官网写的话(如下代码),你会发现你的webpack配置未能生效

pageage.json
  ....
  "scripts": {
    "build": "webpack"
  }

当你的webpack后面没有任何配置,默认是读取webpack.config.js这个文件,但是前面我们为了区分开发和生产环境,并不推荐这种使用方式,所以你在写build命令的时候,需要加上 --config ./workflow/webpack.dev.config.js

配置webpack

Entry

入口文件,webpack会首先从这里开始编译

// webpack.base.config.js
const path = require('path');

module.exports = {
  entry: {
    main: './src/index.js',
  },
};

Output

定义打包后输出的位置以及对应的文件名,[name]是占位符,这话字段是根据entry中定义的key值来定的,即为main

// webpack.base.config.js

const path = require('path');

module.exports = {
  entry: {
    main: './src/index.js',
  },
  output:{
    path: path.resolve(__dirname, '../dist'), //这里可以不写,默认生成的路径和workflow同级
    filename: '[name].bundle.js',
  }
};
// webpack.dev.config.js
const {merge} = require('webpack-merge');
const commonConfig = require('./webpack.base.config');

const devConfig = {
  mode: 'development',
  devtool:"source-map", //会生成map文件,能够清晰的展示报错信息的位置,视频会详解,后面可看source-map内容
};
module.exports = merge(commonConfig, devConfig);

在终端运行

yarn run build

即可以在根目录看到生成的dist目录

模式(mode)

上面的代码中涉及一个字段mode,它是用来告知webpack使用相应模式的内置优化,你在开发环境和生产环境进行的操作不一定都是相同的。

mode有development和production两个数值,一般你本地启动服务的时候,mode是使用development,而我们部署到生产环境(线上环境)的时候,都是使用production。

source-map

当 webpack 打包源代码时,可能会很难追踪到 error(错误) 和 warning(警告) 在源代码中的原始位置。例如,如果将三个源文件(a.js, b.js 和 c.js)打包到一个 bundle(bundle.js)中,而其中一个源文件包含一个错误,那么堆栈跟踪就会直接指向到 bundle.js。你可能需要准确地知道错误来自于哪个源文件,所以这种提示这通常不会提供太多帮助。

为了更容易地追踪 error 和 warning,JavaScript 提供了 source maps 功能,可以将编译后的代码映射回原始源代码。如果一个错误来自于 b.js,source map 就会明确的告诉你。

因此本地开发的时候我们需要定位到具体的错误地址devtool的数值为source-map,而生产环境devtool我们一般设置为cheap-module-source-map 这种不会显示源码,而且会对空格或者空行进行删除,减少文件体积大小

// webpack.prod.config.js
const {merge} = require('webpack-merge');
const commonConfig = require('./webpack.base.config');

const devConfig = {
  mode: 'production',
  devtool:"cheap-module-source-map", 
};
module.exports = merge(commonConfig, devConfig);

Plugins

webpack的插件机制使得webpack拥有很多丰富的功能 https://www.webpackjs.com/plugins/

模板文件

当我们构建一个Web的时候,我们需要一个HTML,该HTML再引入JavaScript代码。HtmlWebpackPlugin简化了HTML文件的创建,以便为你的webpack包提供服务。这对于在文件名中包含每次会随着编译而发生变化哈希的 webpack bundle 尤其有用。

yarn add html-webpack-plugin -D

修改webpack.base.config.js文件,添加html-webpack-plugin

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    app: './src/index.js',
  },
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: '[name].bundle.js',
  },

  plugins: [
    new HtmlWebpackPlugin(),
  ],
};

运行 yarn run build 命令就默认在dist生成一个index.html文件,并且自动给你引入打包好的文件

//  dist/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Webpack App</title>
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <script defer src="app.bundle.js"></script>
  </head>
  <body>
  </body>
</html>

这个index模板是可以由我们来定义的

在根目录创建public目录,该目录下使我们存放一些公共文件的地方,创建模板index.html

public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

代码中的htmlWebpackPlugin字段是在webpack中定义的

修改webpack.base.config.js

 ....
 plugins: [
    new HtmlWebpackPlugin({
      title: '欢迎fishfan同学',
      template: path.resolve(__dirname, '../public/index.html'),
      filename: 'fish.html',
    }),
  ],

运行yarn run build ,dist目录下回生成一个fish.html 这个文件名字就是由filename来决定的。默认为index.html,后续我们会删除这个filename

打包前清除dist

webpack5 自带有clear:true,这里可以使用也可以不使用

clear-webpack-plugin 插件打包前用来清理dist目录之前打包的文件,如果不清理每次打包,dist目录下都会存在上次打包的文件,特别是生成的文件是由hash值来命名的时候,即

  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: '[name].[chunkhash].js'
  },

安装

yarn add clear-webpack-plugin -D

修改webpack.base.config.js文件

const { CleanWebpackPlugin } = require('clean-webpack-plugin');
 ....
 plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: '欢迎fishfan同学',
      template: path.resolve(__dirname, '../public/index.html'),
    }),
  ],

命令友好提示

Friendly-errors-webpack-plugin识别某些类别的webpack错误,并清理,聚合和优先级,以提供更好的开发人员体验。具体效果可以访问下面链接查看 https://www.npmjs.com/package/friendly-errors-webpack-plugin

yarn add friendly-errors-webpack-plugin -D

修改webpack.base.config.js文件

const { CleanWebpackPlugin } = require('clean-webpack-plugin');
 ....
 plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: '欢迎fishfan同学',
      template: path.resolve(__dirname, '../public/index.html'),
    }),
    new FriendlyErrorsWebpackPlugin(),
  ],

Loaders

为什么需要loader?

在使用webpack的时候,并不是所有类型的文件都能够处理的,因此我们需要通过loader进行转换

什么是loader?

所谓 loader 只是一个导出为函数的 JavaScript 模块,可以把它理解成不同的打包方案,针对不同的类型的文件使用不同的打包方案。

目前我们项目中只有一个HTML和一个index.js文件,是没有什么作用的,我们还需要webpack做一些事情。

css

我们需要能够在JavaScript中使用CSS,就需要使用css-loader和style-loader两个loader。

css-loader能够让webpack解析的时候识别css语法,而style-loader是将css注入DOM中,两者缺一不可

修改webpack.base.config.js文件

  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'], // 执行顺序从右到左,因此这俩loader的顺序不能改变
      },
    ],
  },
  plugins: [
   ...
  ],

添加css文件

src/css/index.css

.title {
  color: red;
}

修改index.js文件

/src/index.js

import './css/index.css';
function login() {
  const oH2 = document.createElement('h2');
  oH2.innerHTML = 'fish-cli';
  oH2.className = 'title';
  return oH2;
}
document.body.appendChild(login());

现在的文件目录结构

|--package.json
|--dist
|  |--index.html
|  |--app.bundle.js
|  |--app.bundle.js.map
|--public
|  |--index.html
|--workflow
|  |--webpack.base.config.js   通用配置
|  |--webpack.dev.config.js    开发环境配置
|  |--webpack.prod.config.js   生产环境配置
|--src
|  |--index.js
|  |--css
|  |  |--index.css

运行打包命令yarn run build 后打开dist下的index.html会发现我们配置的css生效了

截图1

less

在实际项目中,我们都会借助CSS预处理语言,而不是去写原生的CSS,最常用的有less

安装less-loader

yarn add less less-loader -D

修改webpack.base.config.js文件

 module: {
     rules: [
          {
            test: /\.css$/,
            use: ['style-loader', 'css-loader'],
          },
          {
            test: /\.less$/,
            use: ['style-loader', 'css-loader', 'less-loader'],
          },
      ]
 }

在css文件下,建立一个index.less文件

// index.less

@bgColor: '#123456';
@fontsize: 100px;
.title {
  background-color: @bgColor;
  font-size: @fontsize;
}

在src/index.js文件下引入index.less

import '../css/index.css';
import '../css/index.less';
function login() {
  const oH2 = document.createElement('h2');
  oH2.innerHTML = 'fish-cli';
  oH2.className = 'title';
  return oH2;
}
document.body.appendChild(login());

运行yarn run build 查看结果没有问题

样式的兼容

我们在使用webpack对css或者less打包之后,要保证最终生成的CSS代码能够在不同的平台运行,比如不同的浏览器或者不同版本的浏览器,这时候就需要某些工具来处理

首先我们要订一个标准,让Webpack按照该标准来进行打包。

修改package.json文件,添加browserslist字段

//package.json
   ...
   ...
  "browserslist": [
    ">1%",
    "last 2 versions",
    "not ie <=10"
  ]

针对CSS样式的补全(不同的浏览器CSS类需要加不同的前缀,例如-ms-transform的-ms-前缀),我们需要接入postcss工具

postcss

postcss是什么?

postcss就是一个运行JavaScript插件转换样式的工具,这些插件可以检查你的CSS,编译尚未被浏览器广泛支持的先进的CSS语法,内联图片等其他很多优秀功能
postcss
安装

yarn add postcss postcss-loader  postcss-preset-env autoprefixer postcss-nested -D   

关于postcss插件的具体含义和用法可以查看下面地址 https://github.com/postcss/postcss/blob/main/docs/README-cn.md

新建postcss文件(和src目录同级别),文件名固定必须是postcss.config.js,配置如下

module.exports = {
  plugins: [
    require('autoprefixer'),
    require('postcss-nested'),
    require('postcss-preset-env'),
  ],
};

修改webpack.base.config.js文件

  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader', 'postcss-loader'],
      },
       {
        test: /\.less/,
        use: ['style-loader', 'css-loader', 'postcss-loader','less-loader'],
      },
     ]
   }

importLoaders

如何我们的css文件存在这样的形式

// x.css
@import './index.css'

需要对css-loader增加一些选项,修改webpack.base.config.js文件

  {
    test: /\.css$/,
    use: [
      'style-loader',
      {
        loader: 'css-loader',
        options: {
          importLoaders: 1,
        },
      },
      'postcss-loader',
    ],
  },

Babel

Babel是一个JavaScript编译器,能将ES7或者ES6语法的代码转换成ES5的(浏览器可识别),让你使用最新的语言特性而不用担心兼容问题

yarn add babel-loader @babel/core @babel/preset-env -D

yarn add @babel/plugin-transform-runtime @babel/runtime-corejs3 -D //在JS的情况下来使用异步
{
  "presets": ["@babel/preset-env"],
  "plugins": [
    [ 
      "@babel/plugin-transform-runtime", {
        "corejs": 3
      }
    ]
  ]
}

修改webpack.base.config.js 在rules中添加

  {
        test: /\.m?js$/,
        exclude: /(node_modules|bower_components)/,
        use: ['babel-loader'],
      },

运行yarn run build 可以看到生成的js文件里面都是转译过后的代码

图片和字体

webpack5之前,在处理图片或者文本类文件的时候都是使用file-loader或者是url-loader,在webpack5之后可以使用Asset Modules(资源模块),就不需要配置loader

具体详情可点击此处

图片

修改webpack.base.config.js配置

   {
    test: /\.(png|svg|gif|jpe?g)$/,
    type: 'asset',
    generator: {
      filename: 'img/[name].[hash:4]:[ext]',
    },
    parser: {
      dataUrlCondition: {
        maxSize: 10 * 1024,
      },
    },
  },

现在webpack 将按照默认条件,自动地在 resource 和 inline 之间进行选择:小于 10kb 的文件,将会视为 inline 模块类型,否则会被视为 resource 模块类型。

在src文件创建img文件夹,里面存放一张图片名字为fish.jpeg

截图2

修改src/index.js文件

import ImgSrc from './img/fish.jpeg';
function login() {
  const IMG = document.createElement('img');
  IMG.src = ImgSrc;
  return IMG;
}
document.body.appendChild(login());

运行打包命令yarn run build 可以看到在打包生成后的dist目录中有img目录,图片的名称和我们设置的名字格式一致

截图3

字体

修改webpack.base.config.js文件

 {
    test: /\.(eot|svg|ttf|woff|woff2?)$/,
    type: 'asset/resource',
    generator: {
      filename: 'font/[name].[hash:3]:[ext]',
    },
  },

热模块替换HMR

当我们改动代码的时候,希望能够自动重新编译,webpack-dev-server就可以帮助我们在代码发生变化的时候自动编译代码

yarn add webpack-dev-server -D

我们这里的webpack-dev-server使用过的是最新的4.4版本,v3x版本有很多不一样,3和4版本不同的地方可以查看点击此处

修改webpack.dev.config.js文件

// webpack.dev.config.js

const devConfig = {
  mode: 'development',
  devtool: 'source-map',
  devServer: {
    historyApiFallback: true, // 所有的404都会连接到index.html
    open: true,
    hot: 'only',// 只刷新我们更新改动的模板 在构建失败时不刷新页面作为回退
    port: 8080,
  },
};

注意使用webpack-dev-server的时候,HTML的模板名字一般是index.html,因此我们在使用HtmlWebpackPlugin的时候,filename不用写,如果写的话,数值为index.html

运行yarn run dev,浏览器会自动打开一个页面,这时候我们修改代码,浏览器会自动实现刷新功能

支持TypeScript

yarn add typescript esbuild-loader -D

这里为了提高性能,并没有使用传统过的ts-loader 选择最新的esbuild-loader

修改webpack.base.config.js

 {
        test: /\.(js|ts|jsx|tsx)$/,
        include: path.appSrc,
        use: [
          {
            loader: 'esbuild-loader',
            options: {
              loader: tsx,
              target: '2015',
            },
          },
        ],
      },

之前针对js新特的解析的babel-loader就会失效

为了兼容TypeScript文件,新增typescript配置文件tsconfig.json(和src同等级)

// tsconfig.json
{
    "compilerOptions": {
        "outDir": "./dist/",
        "noImplicitAny": true,
        "module": "es6",
        "target": "es5",
        "jsx": "react",
        "allowJs": true,
        "moduleResolution": "node",
        "allowSyntheticDefaultImports": true,
        "esModuleInterop": true,
    "paths": {
            "@/*": [
                "src/*"
            ]
    }
      }
}

支持React

yarn add react react-dom  @types/react @types/react-dom -D

由于对JSX语法在支持Typescript部分已经做了配置,React这里就无须进行配置

我们这里验证一下React语法是否成功

创建index.tsx文件在src下

// src/index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
const Index = () => {
  return <div id='root'>欢迎fishfan</div>;
};
ReactDOM.render(<Index />, document.getElementById('root'));

修改入口文件配置

entry: {
    app: './src/index.tsx',
  },

运行yarn run dev,我们所配置的代码已经成功

resolve

alias

创建 import 或 require 的别名,来确保模块引入变得更简单,例如,一些位于 src/ 文件夹下的常用模块: 在不使用resolve的时候你会这么写

import {method} from '../../../src/utils'

使用resolve的alias之后

import {method} from '@/src/utils'
extensions

能够使用户在引入模块时不带扩展,例如

在不使用resolve的时候你会这么写

import {method} from './src/utils.js'

使用resolve的extensions之后

import {method} from './src/utils'

修改webpack.base.config.js

 ....
 output: {
    path: path.resolve(__dirname, '../dist'),
    filename: '[name].bundle.js',
    clean: true, // webpack5 自带的清空dist目录功能
  },
 resolve: {
    alias: {
      '@': resolve('src'),
      ReactDOM: path.resolve('./node_modules/react-dom'), // 保证全局只有一个react,否则会导致hook无法使用
      React: path.resolve('./node_modules/react'), // 保证全局只有一个react,否则会导致hook无法使用
      // https://github.com/facebook/react/issues/13991
    },
    extensions: ['.js', '.jsx', '.ts', '.tsx'], //没有写后缀时进行补全
  },

区分打包环境

具体webpack的环境区分代码请点击下方链接查看

修改下打包命令

 "scripts": {
    "build": "webpack --config ./workflow/webpack.prod.config.js",
    "dev": "webpack serve --config ./workflow/webpack.dev.config.js"
  },

结束语

关于zustand后续会出一个新的章节来介绍这个工具库

代码地址(template目录)

代码地址

wuqiren commented 3 years ago

修复在使用JS 和JSX的时候,无法使用异步的情况,安装 @babel/plugin-transform-runtime @babel/runtime-corejs3