从零搭建一个 webpack 脚手架工具(一)

  • 2019 年 12 月 5 日
  • 筆記

webpack 是一个现代 JavaScript 应用程序的静态模块打包器,已经成为前端开发不可获取的工具。特别是在开发大型项目时,项目太大,文件过多导致难以维护,或者是优化网络请求时,webpack 都是不可获取的利器。但是 webpack 配置并没有那么容易,webpack 配置项繁多,繁多的背后是配置的灵活性。许多的框架都是由 webpack 搭建而成,因此学会使用 webpack 可以让自己更好的理解脚手架搭建过程,甚至自己写一个灵活高效的脚手架工具。

从最基础的开始,使用的 webpack 版本是^4.39.2,搭建时会用到以下技术:

  1. 从单页面到多页面
  2. 代码切片
  3. 热更新
  4. 热替换
  5. CSS 分离
  6. HTML 模板
  7. babel 的使用
  8. 支持 img、sass、jsx、css、typescript 等工具
  9. webpack 一些插件的使用
  10. postcss 的简单配置
  11. 不同开发环境的配置
  12. 配置 react 开发环境

首先,配置 webpack,大致的骨架是这样的,这是最基本的配置内容:

{      entry: "",      // 入口配置      mode: 'development',    // 环境配置      output: {},         // 打包输出配置      module: {},        // loader 配置项      plugins: [],        // 插件配置项      devServer: {},      // 服务器配置  }

那么就开始一一进行配置。

安装 webpack,设置程序打包命令

首先是安装:yarn add webpack --dev 或者 npm install webpack --save-dev 或者 yarn add webpack -D npm i webpack -D在开发环境安装。 安装完时候,来到 package.json 文件,在 scripts 项中写入一下命令:

"build": "webpack"

当运行命令 npm run build 后,webpack 默认会在项目根目录下查找一个叫 webpack.config.js 的文件,然后进行打包。当文件名不想叫这个或者不想在根目录下创建这个文件时,可以在后面的 --config 字段之后写上文件的所在路径。例如:

"build" "webpack --config config/webpack.config.dev.js"

这样,当运行时,webpack 就从 项目根路径/config/webpack.config.dev.js 这个路径查找配置文件。

在运行命令时,可能会提醒安装 webpack-cli 输入 yes 即可。

entry 入口配置(必须的)

entry 大致有四种写法,分别是字符串的形式、数组形式、函数形式和对象形式。代表的含义分别是:

形式

含义

举例

字符串的形式

这种表示单个入口

例如:entry: "path(__dirname,"../src/index.js")"

数组形式

这种也是表示单个入口

例如:entry: "["./01.js","./index.js"]"

函数形式

可以是单入口也可以是多入口

该函数应该返回一个字符路径、数组或对象作为打包入口

对象形式

这种表示多个入口

例如:entry: {app: './src/app.js',vendors: './src/vendors.js'}

注意!:第一种和第二种都表示单入口,但含义不同。使用数组的作用是将多个资源预先 合并,在打包的时候, webpack 会将数组的最后一项作为实际的入口路径。

modules.exports = {      entry: ["babel-polyfill","./src/index.js"]  }

就相当于:

// webpack.config.js  modules.exports = {      entry: "./src/index.js"  }    // 在打包后的文件中,会包含数组中所有的路径对应的文件  import "babel-polyfill";    // 以及 ./src/index.js 文件

output 配置

output 配置项很多,有两个是必须的:

  • path 指定文件输出时的文件夹(不存在时会自动创建);
  • filename 指定文件输出时文件的名字;

单页面

{      entry: path.join(__dirname,"../src/index.js"),      // 入口配置      output: {          path: path.join(__dirname,"../build"),          filename: "index.js"      },         // 打包输出配置  }

运行后,就会在 build 文件夹下创建一个 index.js 的打包文件。

多页面

{      entry: {          index: path.join(__dirname,"../src/index.js"),          demo: path.join(__dirname,'../src/demo.js')      },      // 入口配置      output: {          path: path.join(__dirname,"../build"),          filename: "[name].js"      },         // 打包输出配置  }

运行后,在 build 文件夹下就会多出两个文件。注意 output 中 filename 的文件名 —— [name] 这个 name 对应的就是 entry 对象的键。当然,也可以为 filename 指定别的字段,但也是要用 [] 包裹。可以指定的字段有:

  • [name] 当前 chunk 的名字
  • [hash] 此次打包所有资源生成的 hash
  • [id] 指代当前 chunk 的 id
  • [chunkhash] 指代当前 chunk 内容的 hash
  • [query] 模块的 query,例如,文件名 ? 后面的字符串

表中 hashchunkhash 的作用:当下一次请求时,请求到的资源会被立刻下载新的版本,而不会用本地缓存。query 也有类似的效果,只是需要人为指定。

publicPath

publicPath 是 output 中的一个配置项,不是必须的。但是它是一个非常重要的配置项。 与 path 属性不同,publicPath 用来指定资源的请求位置,而 path 是用来指定资源的输出位置(打包后文件的所在路径)。在 HTML 页面中,我们可能会通过 <script> 标签来加载 JS 代码,标签中的 src 路径就是一个请求路径(不光是 HTML 中的 JS 文件,也可能是 CSS 中的图片、字体等资源、HTML 中的图片、CSS 文件等)。publicPath 的作用就是指定这部分间接资源的请求位置。 publicPath 有三种形式,分别是:

  1. 与 HTML 相关,在请求这些资源时会以当前页面 HTML 所在路径加上相对路径,构成实际请求的 URL。
  2. Host 相关,若 publicPath 的值以 ‘/’开始,则代表此时 publicPath 是以当前页面的 hostname 为基础路径的。
  3. CDN 相关,上面两种都是相对路径,而这个是 绝对路径。如:publicPath 为:"https://www.example.com/",代表当前路径是 CDN 相关。 举个例子,当使用第一种形式时,当我们使用 html-webpack-plugin 插件动态生成一个 HTML,并打包到 build 文件夹后,JS 文件(指定的 entry)会自动插入到 HTML 中。当我们指定 publicPath: '/',后就会变成:

当没有指定 publicPath 时,默认是 "",即:

而如果是 "/static" 时,HTML 引入的资源路径前都将有一个 "/static"。这个路径是相对于项目根路径的。

html-webpack-plugin 插件

这是一个很实用的插件,在上面的例子中,都没有提到 html,而这个插件可以动态生成 html。下载: npm install html-webpack-plugin -D 或者 yarn add npm install html-webpack-plugin -D。配置:

const HtmlWebpackPlugin  = require('html-webpack-plugin');    {      plugins: [          new HtmlWebpackPlugin({              // 以下都是可选的配置项:              title: "hello world!",   // html 的 title 标签内容              // html 模板路径              template: path.join(__dirname,'../public/index.html'),              // title favicon 路径              favicon: path.join(__dirname,'../public/favicon.ico'),              // 指定引入的 js 代码 插入到哪里(默认是body最底部,即:true)              // 也可以指定字符串:"body" 或 "head"              inject: false,              // 指定 打包输出后,文件的名字(不指定的话还是原来的名字)              filename: "hello.html",              // 还有许多配置,这是常用的几个          })      ]  }

多个 HTML 页面的配置

有时候,想要配置多页面应用,这时就要多实例化几个这个插件。这时就可能会用到别的一个配置属性 —— chunks

{      entry: {          index: path.join(__dirname,"../src/index.js"),          demo: path.join(__dirname,'../src/demo.js')      },      // 入口配置      output: {          path: path.join(__dirname,"../build"),          filename: "[name].js"      },         // 打包输出配置        plugins: [          new HtmlWebpackPlugin({              chunks: ['index'],      // 指定 chunk 的名字(一般就是 entry 对象的键)              // html 模板路径              template: path.join(__dirname,'../public/index.html'),              favicon: path.join(__dirname,'../public/favicon.ico'),              inject: "body",              filename: "index.html",          }),            new HtmlWebpackPlugin({              chunks: ['demo'],              // html 模板路径              template: path.join(__dirname, '../public/demo.html'),              favicon: path.join(__dirname, '../public/favicon.ico'),              inject: "body",              filename: "demo.html",          }),      ],        // 插件配置项  }

mode 环境变量

mode 的选项一般是这两者其一:developmentproduction,即:开发模式或生产模式。生产模式的代码一般是压缩过的。 单纯的指定 mode 值,可能不能满足我们的需要,这时可以使用另一种办法来设置 mode 值。就是在 package.json 文件的 'scripts' 命令中传入参数。

// ....  "scripts": {      "build": "cross-env NODE_ENV='development' webpack --config config/webpack.config.dev.js"  }  // ....

可以注意到,在之前的命令中,我们在前面又添加了一部分内容:cross-env NODE_ENV='development 这是给 Node.js 的全局变量 process 的 env 属性传入了一个值,前面 cross-env 是一个 npm 包,主要为了解决在 Windows 系统下不支持传值命令。 这样,在 webpack 配置文件中,就可以接收到这个值:

var mode = process.env.NODE_ENV;    // development

这样就可以根据传入的值,来对配置文件作进一步的改进:

const mode = process.env.NODE_ENV;  const isDev = mode === 'development';    const config = {      // 公共配置项,比如 loader、mode、entry 和 output 中相同的配置项      mode: mode,      entry: "xxx",      // 等等  }    if(isDve){      // 是开发模式时的配置,比如:      config.devServer = {          // 对 webpack-dev-server 的配置      }  }else{      // 是生产模式时的配置  }    // 最后导出:  module.exports = config;

module 配置

这一部分比较多,主要是配置各种 loader,比如 css-loader,babel-loader,sass-loader等等。而这些配置存在于 module.rules 这个配置项中。所有的 loader 都是需要安装的。通过 npm install xxx-loaderyarn add xxx-loader 的形式进行安装。

style-loadercss-loader

两者有很大不同,css-loader 的作用仅仅是处理 CSS 的各种加载语法,例如 @importurl() 等。而 style-loader 才是真正让样式起作用的 loader。因此这两个一般配合使用:

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

还需要注意的是:webpack 打包时是按数组从后往前的顺序将资源交给 loader 处理的,因此要把最后生效的放在前面。

loader options

有时候使用一个 loader 时,可能要对它进行一些配置,例如 babel-loader babel 的一些配置就可以写在 options 里,当然也可以建一个 .babelrc 文件进行配置。当一个 loader 需要配置时,它就不能在 use 属性里是个单纯的字符串了,而是一个对象。

{      module: {          rules: [              {                  test: /.css$/,                  use: ['style-loader',{                      // 对 css-loader 配置时,是个对象                      loader: 'css-loader',                      options: {                          // css-loader 的配置项                      }                  }]              }          ]      }  }

需要注意的是,css 中还不能书写背景图片路径(例如:background: url())。不然会报错。因为加载的不是样式,而是图片,在 webpack 中,想要加载图片,还需要使用 file-loader,之后会介绍。

sass-loaderless-loader

sassless 是 CSS 的预处理器,需要安装。而且最终会编译成 CSS,因此我们还需要 style-loadercss-loader。而且还要安装编译 Sass 的包:node-sass(不然会报错)。

module: {      rules: [          {              test: /.(sass|scss)$/,              use: ["style-loader", "css-loader","sass-loader"]          },          {              test: /.less$/,              use: ["style-loader", "css-loader", "less-loader"]          }      ]  }

html-loader

有了这个 loader,我们可以将一个 html 文件通过 JS 加载进来。比如这样:

rules: [      {          test: /.html$/,          use: 'html-loader'      }  ]    // loader.html  <h1>Hello World!</h1>    // index.js  import html from './loader.html';  document.write(html);

file-loaderurl-loader

file-loader 用于打包文件类型的资源。比如 CSS 的背景图片和字体、HTML 的 img 标签中的 src 路径等。

rules: [      {          test: /.(png|jpg|gif)$/,          use: "file-loader",      }  ]

这样就可以对 png、jpg、gif 类型的图片文件进行打包,而且可以在 JS 中加载图片。

file-loader 中的 options

主要有两个配置项:

  1. name,指定打包后文件的名字,默认是 hash 值加上文件后缀。也可以制定成:[name].[ext] 表示原来的名字和文件后缀。
  2. publicPath 这里的 publicPath 与 output 中的 publicPath 一样,在这里指定后,会覆盖原有的 output.publicPath。比如:
rules: [      {          test: /.(png|jpg|gif)/,          use: {              loader: "file-loader",              options: {                  name: '[name].[ext]',                  publicPath: "",              }          }      }  ]

url-loader

file-loader 作用类似,唯一的不同是:url-loader 可以设置一个文件大小的阈(yù)值。当大于该阈值时与 file-loader 一样返回 publicPath,而小于阈值时则返回文件的 base64 形式编码。比如:

{      test: /.(png|jpg|gif)$/,      use: {          loader: "url-loader",          options: {              // 当文件小于这个值时,使用 base64 编码形式              // 大于该值时,使用 publicPath              // 这个属性在 file-loader 中是没有的。              limit: 10240,              name: '[name].[ext]'          }      }  }

ts-loader

使用 ts-loader 可以让我们使用 typescript 来编写 js 代码。安装该 loader 后,还要安装 typescript。

yarn add ts-loader typescript
rules: [      {          test: /.ts$/,          use: "ts-loader",      }  ]

babel-loader

babel-loader 很重要,使用 babel-loader 可以让我们写的 JS 代码更加兼容浏览器环境。配置 babel-loader 时需要下载好几个其他的包。yarn add babel-loader @babel/core @babel/preset-env -D 。这三个是最核心的模块。主要作用如下:

  • babel-loader 它是 babel 与 webpack 协同工作的模块;
  • @babel/core babel 编译器的核心模块;
  • @babel/preset-env 它是官方推荐的预置器,可根据用户设置的目标环境自动添加所需的插件和补丁来编译 ES6+ 代码。 具体配置如下:
rules: [      {          test: /.js$/,          // 不要编译 node_modules 下面的代码          exclude: path.join(__dirname,'../node_modules'),          use: {              loader: "babel-loader",              options: {                  // 当为 true 时,会启动缓存机制,                  // 在重复打包未改变过的模块时防止二次编译                  // 这样做可以加快打包速度                  "cacheDirectory": true,              }          }      }  ]

对于 options 其它部分,可以在项目根目录下新建一个 .babelrc 文件。.babelrc 文件相当于一个 json 文件。它的配置项大概是这样的:

{      "presets": [],      "plugins": [],  }

比如要配置的一个内容:

{      "presets": [          ["@babel/env",      // 每一个 preset 就是数组的每一项              // 当有的 preset 需要配置时,这一项将也是一个数组              // 数组的第一项是 preset 名称,第二项是该 preset 的配置内容,是一个对象              {   // @babel/preset-env 会将 ES6 module 转成 CommonJS 的形式                  // 将 mudules 设置成 false,可以禁止模块语句的转化                  // 而将 ES6 module 的语法交给 webpack 本身处理                  "mudules": false,                  // targets 可以指定兼容的各个环境的最低版本                  "targets": {                      "edge": "17",                      "firefox": "60",                      "chrome": "67",                      "safari": "11.1"                  }              }          ]      ],      "plugins": []  }

env 的 targets 属性,可以配置的环境名称有:chromeoperaedgefirefoxsafariieiosandroidnodeelectron。当然 targets 的值也可以是一个字符串,例如:"targets": "> 0.25%, not dead" 表示仅包含浏览器具有> 0.25%市场份额的用户所需的 polyfill 和代码转换。

处理 react jsx 语法:@babel/preset-react

下载: yarn add @babel/preset-react -D。当然,如果想使用 react,也要下载。在 .babelrc 的 presets 项中添加一个 preset:

{      "presets": [          ["@babel/env",              {                  "modules": false,                  "targets": {                      "ie": 9                  }              }          ],          "@babel/react"      ]  }

这个时候就可以愉快的使用 react 了!

处理 .jsx 的文件

用 react 写的文件不光可以使用 .js后缀,也可以使用 .jsx 文件后缀。但想要使用,这需要配置,不然会报错。来到 webpack 配置文件,添加一个 loader 项:

{      test: /.jsx$/,      use: "babel-loader",  }

当然,也可以与 js 配置写在一起:

test: /.(js|jsx)$/,  use: {      // ...  }

postcss-loader

下载:npm install postcss-loader 配置:

// 不需要再次创建新的 loader 对象,应该在之前的 style-loader css-loader 之后直接添加 postcss-loader 即可  {      test: /.css$/,      // 顺序很重要      use: ['style-loader','css-loader','postcss-loader'],  }

配置 PostCSS

这里需要创建一个文件 —— postcss.config.js 在项目根目录下。

自动添加后缀 —— autoprefixer

const autoprefixer = require('autoprefixer');    module.exports = {      plugins: [          autoprefixer({              // 需要支持的特性(这里添加了 grid 布局)              grid: true,              // 浏览器兼容              overrideBrowserList: [                  '>1%',  // 浏览器份额 大于 1% 的。                  'last 3 versions',  // 兼容最后三个版本                  'android 4.2',                  'ie 8'              ],          })      ]  };

postcss-preset-env 插件

这个插件可以让我们在应用中使用最新的 CSS 语法特性。同样需要下载: yarn add postcss-preset-env。使用:

// postcss.config.js    const autoprefixer = require('autoprefixer');  const postcssPresetEnv = require('postcss-preset-env');    module.exports = {      plugins: [          autoprefixer({              grid: true,              overrideBrowserList: [                  '>1%',                  'last 3 versions',                  'android 4.2',                  'ie 8'              ],          }),            postcssPresetEnv({              state: 3,              features: {                  'color-mod-function': {                      unresolved: 'warn'                  },                  browsers: 'last 2 versions'              }          })      ]  };

resolve 配置项

这是一个可选的配置项,配置 resolve 用来设置模块如何被解析。几个常见的配置项:

1. resolve.alias

这个属性是给路径添加别名的,当使用 import 或者 require 去引用别的模块时,文件路径可能会比较长,这个时候就可以使用 alias 来简化路径。也可以在给定对象的键后的末尾添加 $,以表示精准匹配。比如:

// 在 webpack 中配置 resolve.alias  module.exports = {      // ....      resolve: {          alias: {              xyz$: path.resolve(__dirname, 'path/to/file.js'),              xyz: path.resolve(__dirname, 'path/to/file.js')          }      }      // ....  }    // 引用:index.js  import App1 from 'xyz';     // 精准匹配,会解析到 path/to/file.js 中的 js 文件  import App2 from 'xyz/index.js';  // 非精准匹配,匹配 path/to/file.js/index.js 中的内容

resolve.extensions

这个配置项设置后会自动解析确定的扩展。默认值为 extensions: ['.wasm', '.mjs', '.js', '.json']。还可以做更改,比如 添加 jsx 文件:

{      resolve: {          extensions: ['.wasm', '.mjs', '.js', '.json','.jsx']      }  }

resolve 配置项还有许多,上面两个应该是比较常用的。其他的可以参看官网:webpack 中文文档[1]webpack 英文文档[2]

最后

下一节还是 webpack 的配置,之后将要配置 devServer、配置优化和生产环境的配置。最后再说一下 create-react-app 脚手架的改造。

参考资料

[1]

webpack中文文档: https://webpack.docschina.org/configuration/resolve/

[2]

webpack英文文档: https://webpack.js.org/configuration/resolve/