搭建一个前端项目原来有这么多讲究---Webpack5(一)

5/1/2022 工程化 5/4/2022

# 搭建一个前端项目原来有这么多讲究(一)

前端领域目前搭建项目的方式多种多样,如最出名的webpackVite等等,还有一系列脚手架如vue-clicreate-react-app等等。最原始的项目架构是一个 html 文件,通过link或者script来引入各种 css、js 文件,放在现在看来无疑是十分繁琐且令人心烦的...废话不多说,让我们来了解一下搭建项目的种种讲究吧!!

# 使用 Webpack5 的(最小化)搭建过程 ( With Babel )

首先我们需要知道,Webpack 最牛的地方就是会自动将entry文件以及被引用到的文件全都打包成一个 bundle js 文件,我们并不需要关注从 html 引用各种资源的过程。

# 安装/下载 & 配置 Webpack

首先我们需要下载 webpackwebpack-dev-serverwebpack-cli 三件套。

// use Npm

npm install --save-dev webpack webpack-dev-server webpack-cli
1
2
3

其次我们的最简单的启动方式如下:

webpeck serve --mode development
1

但这种方式只会运行/dist/index.html,此时并没有任何打包过的 js 文件被引入。这也是自然的,毕竟我们并没有去配置入口文件。

我们可以做以下操作:

webpeck serve --config ./webpack.config.js --mode development

1
2
// in ./webpack.config.js
export.modules={
    entry:path.resolve(__dirname,"./src/index.js"),
    output:{
        path:path.resolve(__dirname,"./dist"),
        filename:path.resolve(__dirname,"bundle.js")
    },
    devServer:{
        static:path.resolve(__dirname,"./dist")
    }
}
1
2
3
4
5
6
7
8
9
10
11

# 安装 & 配置 Babel

Babel 允许使用大多数浏览器还不支持的 JavaScript 特性编写代码。也许您听说过 JavaScript ES6 (ES2015)、ES7 和其他针对 JavaScript 语言的 ECMAScript 规范版本。在阅读本文时,JavaScript 语言中可能已经包含了各种版本。

  • 安装
npm install --save-dev @babel/core @babel/preset-env
1

此外,如果你有 Webpack 来打包你的 JavaScript 应用程序,你必须为 Babel 安装一个 Webpack Loader:

npm install --save-dev babel-loader
1
// 可以在.babelrc中配置
// package.json

"babel":{
    "presets":[
        "@babel/preset-env"
    ]
}
1
2
3
4
5
6
7
8
// webpack.config.js

module: {
  rules: [
    {
      test: "/.(js)$/",
      exclude: "/./node_modules/",
      use: ["babel-loader"],
    },
  ];
}
1
2
3
4
5
6
7
8
9
10
11

# 继续完成一个较 Advanced 的 Webpack5 项目

在前面的过程中,我们知道了 Webpack 除了可以自动帮我们打包所有的 js 文件、通过 Babel 来转译最新的 JavaScript 特性之外,还能通过 Dev Server 来启动开发环境下的项目。

然而 Webpack 还能干更多的事情,比如在发布的时候,Webpack 可以帮助您构建一个生产就绪包,其中包含对您的源代码的所有优化。

# Webpack 生产环境&开发环境的构建

前面的 Dev server 能够帮我们启动开发环境下的项目、并且实时绑定、热更新。但此时 bundle.js 文件是不可见的。

现在我们想要构建出生产环境中的项目,就需要通过:

webpack --config ./webpack.config.js --mode production
1

等到命令执行完毕,我们可以看到 dist/bundle.js 文件不是即时生成的,而是在 dist/ 文件夹中真实创建的。

另请注意,Webpack 开发和生产模式都有自己的默认配置。开发模式创建源代码文件时考虑到了改进的开发人员体验,而生产构建对源代码进行了所有优化。

# 如何管理你的 Webpack build folder

每一次当你去运行npm run build的时候,Webpack 都会创建一个全新版本的 ./dist/bundle.js 文件。最终,你的 dist 文件夹就会变得很复杂,而且你不知道哪一个 bundle.js 是你最新一次构建得。

因此我们需要在每一次 build 的时候先清空 dist 文件夹。(还需要重新创建 index.html) 这就需要html-webpack-plugin

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
  entry: path.resolve(__dirname, "./src/index.js"),
  output: {
    path: path.resolve(__dirname, "./dist"),
    filename: "bundle.js",
  },
  devServer: {
    static: path.resolve(__dirname, "./dist"),
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: ["babel-loader"],
      },
    ],
  },
  plugins: [new HtmlWebpackPlugin()],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

现在运行 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="defer" src="bundle.js"></script>
  </head>
  <body></body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12

现在我想要去自定义一下 html 模板,那我可以去自己定义一个 html 文件,并且添加以下配置。

...
plugins: [new HtmlWebpackPlugin({
    title: "Hello HtmlWebpackPlugin",
    template: path.resolve(__dirname, "./src/index.html")
  })]
...
1
2
3
4
5
6
<!DOCTYPE html>
<html>
  <head>
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body></body>
</html>
1
2
3
4
5
6
7

而且我们也不需要去手动引用 bundle.js!!!

下一步我们需要使用clean-webpack-plugin来去清空 dist 文件夹。

const { CleanWebpackPlugin } = require("clean-webpack-plugin");
...
plugins: [
    new HtmlWebpackPlugin({
      title: "Hello HtmlWebpackPlugin2",
      template: path.resolve(__dirname, "./src/index.html")
    }),
    new CleanWebpackPlugin()
  ]
1
2
3
4
5
6
7
8
9

# Webpack Source Map

Webpack 帮我们打包文件固然很好,但也有缺陷。当我们发现一个 bug 并且想要从浏览器控制台查看的时候,无法去追踪 bug 位置,因为文件被打包过了。

此外在生产环境上,一旦您单击 bundle.js 文件,您只会看到 Webpack 捆绑的用于生产的 JavaScript,其格式不可读。

为了解决这个问题,source maps 可以给了 Webpack 一个至源代码的引用,即可以从打包后代码追溯到源代码上。

./webpack.config.js
...
devtool:'source-map'
1
2
3

再 build 之后,有一个名为 dist/bundle.js.map 的新文件用于执行来自 src/ 的实际源代码与 dist/bundle.js 中捆绑的 JavaScript 之间的映射。而且在生产环境中去点击控制台的 bug 也会跳转至源代码具体位置中去。

# Webpack 生产 / 开发环境的两种配置

最终示例地址:https://github.com/1360151219/minimal-webpack5-setup/tree/webpack-distinct-configuration

我们应该将生产、开发环境的两种配置文件分开。

{
  ...
  "scripts": {
    "start": "webpack serve --config ./webpack.dev.js",
    "build": "webpack --config ./webpack.prod.js",
    "test": "echo \"Error: no test specified\" && exit 0"
  },
  ...
}
1
2
3
4
5
6
7
8
9

然后将之前的配置文件复制出两份:

webpack.dev.js;

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = {
  mode: "development",
  entry: path.resolve(__dirname, "./src/index.js"),
  devtool: "source-map",
  module: {
    rules: [
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: ["babel-loader"],
      },
    ],
  },
  resolve: {
    extensions: ["*", ".js"],
  },
  output: {
    path: path.resolve(__dirname, "./dist"),
    filename: "bundle.js",
  },
  devServer: {
    static: path.resolve(__dirname, "./dist"),
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: "Hello HtmlWebpackPlugin2",
      template: path.resolve(__dirname, "./src/index.html"),
    }),
    new CleanWebpackPlugin(),
  ],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
webpack.prod.js;

module.exports = {
  ...
  mode: "production",
  ...
}
1
2
3
4
5
6
7

这看似重复引用,那我们为什么要这样去做呢?在一个逐步完善迭代的 Webpack 项目中,我们将会去引用各种 plugins、loaders、sourcemap,这些在生产环境和开发环境中会表现得不同。就以 Source Map 为例,为一个大型项目去构建 source map 映射是一件很繁重的任务,为了让开发者能够有更快更高效的开发体验,您希望开发中的源映射不像生产构建中的源映射那样 100% 有效。

因此以下操作只在 webpack.dev.js 中进行:

module.exports={
  ...
  devtool: "eval-source-map";
  ...
}
1
2
3
4
5
  1. 源码映射 会单独生成一个 sourcemap 文件,代码出错时,会标识当前报错的列和行(大而全): devtool:‘source-map’
  2. 不会产生单独的文件,但是可以显示行和列: devtool:‘eval-source-map’
  3. 不会产生列,但是是一个单独的映射文件: devtool:‘cheap-module-source-map’
  4. 不会生成文件,集成在打包后的文件中,不会产生列: devtool:‘cheap-module-eval-source-map’

# Webpack 合并配置文件

最终示例地址:https://github.com/1360151219/minimal-webpack5-setup/tree/webpack-automaticly-merge-configuration

现在我们的 Webpack 两种配置文件其实有着很多相同的代码配置,为什么我们不将公共部分提取出来使用,只根据环境的不同选择特定的配置呢?

很简单,我们只需要在运行时命令中去动态传一个 env 参数即可。

"scripts": {
    "start": "webpack serve --config build-utils/webpack.config.js --env env=dev",
    "build": "webpack --config build-utils/webpack.config.js --env env=prod",
  },
1
2
3
4

我们在新建了一个build-utils文件夹,并且引用了同一个文件,它接收一个 env 参数并且能够动态去与特定环境的 configuration 合并。这里通过webpack-merge这个第三方包来实现合并。

/webpack.config.js

const { merge } = require("webpack-merge");
const commonConfig = require("./webpack.common.js");
module.exports = ({ env }) => {
  const envConfig = require(`./webpack.${env}.js`);
  return merge(envConfig, commonConfig);
};
1
2
3
4
5
6
7
8
/webpack.common.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
    entry: path.resolve(__dirname, '../src/index.js'),
    module: {
        rules: [
            {
                test: /\.(js)$/,
                exclude: /node_modules/,
                use: ['babel-loader'],
            },
        ],
    },
    resolve: {
        extensions: ['*', '.js'],
    },
    output: {
        path: path.resolve(__dirname, '../dist'),
        filename: 'bundle.js',
    },
    devServer: {
        static: path.resolve(__dirname, '../dist'),
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: "Hello HtmlWebpackPlugin2",
            template: path.resolve(__dirname, "../src/index.html")
        }),
        new CleanWebpackPlugin()
    ]
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/webpack.dev.js

module.exports = {
  mode: "production",
  devtool: 'eval-source-map',
};
1
2
3
4
5
6

可以发现在公共配置文件中再也没有mode以及devtool属性了。它们被分配在了特定环境的 configuration 中。

# Webpack 环境变量的定义

最终示例地址:https://github.com/1360151219/minimal-webpack5-setup/tree/webpack-definition-env-variables

有时候你也许需要在源代码中去获取当前处于的环境。那么我们可以通过 Webpack 来声明不同环境下的环境变量。下面是一个例子:

const { DefinePlugin } = require("webpack");

module.exports = {
  mode: "development",
  // additions
  plugins: [
    new DefinePlugin({
      "process.env": {
        NODE_ENV: JSON.stringify("development"),
      },
    }),
  ],
  devtool: "eval-source-map",
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { DefinePlugin } = require("webpack");

module.exports = {
  mode: "production",
  plugins: [
    new DefinePlugin({
      "process.env": {
        NODE_ENV: JSON.stringify("production"),
      },
    }),
  ],
  devtool: "source-map",
};
1
2
3
4
5
6
7
8
9
10
11
12
13

之后我们就可以在./src/index.js以及引用到的 js 文件中去通过process.env.NODE_ENV来获取我们定义的环境变量啦。

# Webpack 环境变量的安全处理 (.ENV)

最终示例地址:https://github.com/1360151219/minimal-webpack5-setup/tree/webpack-dotenv-webpack

现在我们的环境变量相当于是明牌的,每一个人都可以直接在 Webpack configuration 中看到,但是如果我们想要去定义一些比较隐私的环境变量,那要怎么做呢?

这里可以用到dotenv-webpack,我们在项目根目录去定义两个文件.env.development.env.production,然后写入:

// .env.development
NODE_ENV=development
1
2
// .env.production
NODE_ENV=production
1
2

然后在对应的 Webpack configuration 中利用dotenv-webpack去读取:

const DotEnv = require("dotenv-webpack");
plugins: [
  new DotEnv({
    path: path.resolve(__dirname, "..", "./.env.development"),
  }),
];
1
2
3
4
5
6

# Webpack 插件

最终示例地址:https://github.com/1360151219/minimal-webpack5-setup/tree/webpack-bundleAnalyzer

Webpack 有着一个很庞大的插件生态,在我们前面的 Webpack 使用中其实也引用了一些插件。然而还有别的 Webpack 插件是可以增强我们打包体验的。例如,我们来看一个可以让我们可视化分析 Webpack 打包的插件。

首先我们先引入一个script命令:

"build:analyze": "npm run build -- --env addon=bundleanalyze",
1

在这里,addon作为参数被传进了 Webpack configuration 中,接下来我们在build-utils/webpack.config.js中去使用它:

const { merge } = require("webpack-merge");

const commonConfig = require("./webpack.common.js");

const getAddons = (addonsArgs) => {
  const addons = Array.isArray(addonsArgs) ? addonsArgs : [addonsArgs];
  return addons
    .filter(Boolean)
    .map((name) => require(`./addons/webpack.${name}.js`));
};

module.exports = ({ env, addon }) => {
  const envConfig = require(`./webpack.${env}.js`);
  return merge(commonConfig, envConfig, ...getAddons(addon));
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

现在除了把 common 和特定环境的配置文件合并起来以外,还把我们将要放进build-utils/addons文件夹中的 addons 也合并起来。

让我们来看一下build-utils/addons/webpack.bundleanalyze.js

const path = require("path");
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: "static",
      reportFilename: path.resolve(__dirname, "..", "..", "./dist/report.html"),
      openAnalyzer: false,
    }),
  ],
};
1
2
3
4
5
6
7
8
9
10
11
12

最后当我们去运行npm run build:analyze的时候,可以发现dist中多了个report.html,它展示了我们项目的一个体积结构。

# 使用 Webpack5 搭建 React 项目

# React with babel

我们需要使用 Babel 去编译.jsx文件,这里就需要使用到:

npm install --save-dev @babel/preset-react
1

以及在.babelrc文件中去使用:

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}
1
2
3

还需要在webpack.config.js中配置:

 module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: ['babel-loader'],
            },
        ],
    },
    resolve:{
      extension:['*','.js','.jsx']
    }
1
2
3
4
5
6
7
8
9
10
11
12

# 使用 Hot Module Replacement

使用react-hot-loader对你的项目开发效率是一个极大的提升。它可以让你不需要刷新页面的前提下进行热更新。

npm install --save-dev react-hot-loader
1

配置如下:

const webpack = require('webpack');
...
plugins: [new webpack.HotModuleReplacementPlugin()],
devServer: {
  static: path.resolve(__dirname, './dist'),
  hot: true,
},
1
2
3
4
5
6
7
// src/index.js
import React from "react";
import ReactDOM from "react-dom";

const title = "React with Webpack and Babel";

ReactDOM.render(<div>{title}</div>, document.getElementById("app"));

module.hot.accept();
1
2
3
4
5
6
7
8
9

这里如果不加上module.hot.accept()的话,当代码变化之后浏览器还是会进行刷新.