webpack搭建

4/13/2022 webpack

# 1.初始化项目

# 1.1 创建项目并初始化package.json

npm init -y

# 1.2 安装webpack和webpack-cli

npm i webpack webpack-cli -D

# 1.3 在根目录下创建默认的webpack配置入口文件webpack.config.js

该文件在执行webpack打包构建项目时会被读取里边的配置,所有的loader、plugin以及其他都在这里配置。若要修改默认配置文件的入口,可执行以下指令

webpack --config webpack.dev.config.js
// webpack.config.js ==> webpack.dev.config.js

# 1.4 在package.json文件中配置webpack命令脚本

"scripts":{
  "build": "webpack --config webpack.config.js"
}

在命令行执行如下命令,即可打包构建当前项目

npm run build

# 2.Webpack配置入口和出口

webpack.config.js文件中

  • entry字段对应入口文件
  • output.path字段对应出口文件所在目录路径
  • output.filename字段对应出口文件名
// webpack.config.js
let path = require('path')

let config = {
  entry:'./src/main.js', // 入口文件
  output:{ // 出口
    path:path.resolve(__dirname,'dist'),
    filename:'bundle.js'
  }
}

module.exports = config

在根目录下执行以下指令,会将src/main.js作为入口文件开始编译打包输出为dist目录下的bundle.js文件

webpack
// 或者
npm run build

或者不用在webpack.config.js中配置entryoutput,在package.json文件中定义脚本(不推荐)

"scripts":{
  "build": "webpack --entry ./src --output-path ./dist"
}

# 3.Loader

Loader用于预编译及其处理依赖关系,通常是在webpack.config.js文件中的module.rules字段配置

graph LR
A(SASS LESS) -- sass-loader less-loader -->B(CSS)
graph LR
A(ES6/7/8/9/10) -- babel-loader -->B(JS)
graph LR
A(TS) -- ts-loader -->B(JS)
graph LR
A(图片 MP3 MP4等资源) -- file-loader url-loader -->B(JS)

通过loader还可以将所有类型文件都统一转化成模块,即 “万物皆模块”的前端工程化思想,从而可以根据ESModule或者CommonJS模块化规范引入任何类型的文件

// 1.ESModule规范
export { xx } // 导出
import xx from 'xx.js' // 导入
import 'yy.css' // 导出yy.css里的代码,前提需要css-loader处理

// 2.CommonJS规范
module.exports = { xx } // 导出
let xx = require('xx.png') // 导入

# 3.1 css相关loader

# 3.1.1 css-loader

用于编译css文件,使其可以作为一个模块以import 'xx.css'的方式引入,首先需要安装css-loader

npm i css-loader -D

webpack.config.json文件中module.rules中配置对应的loader

 module:{ // 模块
    rules:[ // 打包规则
      {
        test:/\.css$/, // 正则匹配到的以.css结尾的文件
        use:['css-loader'] // 用css-loader预编译

       // use其他写法
       // use:[
       //   { loader:'css-loader'} // 'css-loader'是{loader:'css-loader'}简写
       // ]

       // loader:'css-loader' 若只有一个loader那么不用use
      }
    ]
  }

# 3.1.2 style-loader

style-loader用于将编译好的css文件以style标签的方式插入html文件中

安装

npm i style-loader -D

配置

 module:{
    rules:[
      {
        test:/\.css$/,
        use:['style-loader','css-loader']
        // 注意数组项的顺序,这里会先css-loader处理后再交给style-loader处理,
        // 这样css样式才会最终应用于html文件
      }
    ]
  }

# 3.1.3 less-loader和sass-loader

less-loadersass-loader用于将less、sass文件编译成css文件

安装

npm i less-loader less sass-loader -D

补充:less插件可以将指定less文件转译为css文件,指令如下

npm less ./style/index.less ./style/index.css

但是这样是通过命令行手动一个一个编译的,不方便,故需要安装less-loader,通过webpack配置,让less-loader自动调用less插件实现自动编译

配置

  module:{
    rules:[
      {
        test:/\.less$/,
        use:['style-loader','css-loader','less-loader'] // less --> css --> 插入style标签
      },
      {
        test:/\.(scss|sass)$/,
        use:['style-loader','css-loader','sass-loader']
      }
    ]
  }

# 3.2 JS相关loader

# 3.2.1 ☆babel

babel可以将jsxtses6+vue等转译为浏览器能够识别的js,并做兼容处理(根据配置的browserslist条件)

# babel-loader

babel-loader是一个非常非常非常重要的编译JS的插件,其中@babel/preset-env提供了全套的预置功能用以增强babel-loader例如:

  • @babel/plugin-transform-arrow-functions(转译箭头函数)
  • @babel/plugin-transform-block-function(转译const、let语法)

安装

npm i babel-loader @babel/core @babel/preset-env -D

配置

module: {
  rules: [
    {
      test:/\.js$/,
      use:[
        loader:'babel-loader',
        options:{
          presets:['@babel/preset-env']
         // plugins:[
         //  '@babel/plugin-transform-arrow-functions',
         //  '@babel/plugin-transform-block-function',
         // ]
        }
      ],
      exclude:/node_modules/ // 排除node_modules目录
    }
  ]
}
# polyfill

polyfill是用于做兼容的JS代码填充插件,会根据js源码以及broserslist条件填充相应的代码,是作为@bable/preset-env的一个补丁。

例如源码中使用到Promise这个来自ES6的新玩意,而某些浏览器不会识别Promise这个对象,polyfill会在打包构建项目时填充Promise的实现代码

安装

npm i core-js regenerator-runtime -D

core-js/stable用于填充符合ES标准的新语法特性,regenerator-runtime用于编译ES6后新添加的Generator函数

将babel插件从webpack配置中独立出来,在根目录下创建babel.config.js

module.exports={
  presets:[
    [
      '@babel/preset-env',
      {
        useBuiltIns:'usage',
        // 默认为false,即不对js做填充处理
        // usage,根据兼容浏览器及JS源码,按需填充(推荐)
        // entry,根据兼容浏览器全填充,
        // 坑:若为entry需要在入口js文件开头导入core-js/stable和regenerator-runtime/runtime
        corejs:3, // 坑必须指定corejs版本
      }
    ]
  ]
}

或者在根目录下创建.babelrc文件

{
  "presets": [
    "@babel/preset-env",
    { useBuildIns: 'usage', corejs:3 }
  ]
}

# 3.2.2 vue-loader

vue-loader@15版本之前,配置

{
  test:/\.vue$/,
  use:['vue-loader']
}

vue-loader@15版本后,需要加入plugin

let VueLoaderPlugin = require('vue-loader/lib/plugin')

plugins:[
  new VueLoaderPlugin ()
]

# 3.2.3 ts-loader

ts-loader可以编译ts文件,但是推荐统一使用babel-loader,在.babelrc中配置@babel/preset-typescript即可

安装

npm i @babel/preset-typescript

配置

{
  presets:[
    [
      '@babel/preset-env',
      {
        useBuildIns:'usage',
        corejs:3
      }
   ],
   [ '@babel/preset-typescript' ]
  ]
}

# 3.3 兼容处理

# 3.3.1 browserslist浏览器兼容

在根目录下创建.browserslistrc文件,使得项目构建时能够根据设置的条件做浏览器兼容处理

例如:配置了预设babel-preset-envbabel-loader、配置了预设postcss-preset-envpostcss-loader,会根据.broswerslistrc文件中的条件做兼容编译

配置

> 1%   // 市场使用度超过1%的浏览器版本
last 2 version   // 该浏览器最新的两个版本
not dead   // 还未淘汰(两年之内有过更新)

推荐在package.json文件中配置browserslist字段,无需创建.browserslistrc文件

"browserslist":[
    ">1%",
    "last 2 versions",
    "not dead"
]

补充:webpack包下内置browserslist包,执行以下命令会输出打印要兼容的浏览器版本列表

npm browserslist

# 3.3.2 postcss样式兼容

postcss是利用js转换css样式的工具,根据配置的browserslist条件自动补充css属性前缀

安装

  • postcss
  • autoprefixer自动补充前缀插件(扩展postcss-loader功能的插件)
  • postcss-loader
  • postcss-preset-env(postcss-loader配置预设集)
npm i postcss postcss-loader postcss-preset-env autoprefixer -D

配置

{
  test: /\.css$/,
  use: [
    'style-loader',
    'css-loader',
    {
      loader:'postcss-loader',
      options:{
        postcssOptions:{ // 配置postcss-loader选项
          plugins:[
            'postcss-preset-env' // 简写
            // require('autoprefixer'), // 传入autoprefixer插件用于补充css属性前缀
            // reuire('postcss-preset-env') // 预设包含了autoprefixer
          ]
        }
      }
    },
  ]
}
// 注意:postcss-loader要在css-loader之前对css代码进行兼容处理,
// 之后再把处理好的css文件传入给css-loader进行依赖处理,问题是
// 在less、sass的loader配置中又得再CV一遍,冗余且耦合

推荐在根目录下创建postcss.config.js文件,对postcss-loader进行全局配置

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

这样一来,在use里只需传入'postcss-loader'即可,无需再一个一个地重复配置

# 3.3.3 babel

使用@babel/preset-env以及做polyfill填充即可


# 3.4 其他loader

# 3.4.1 import-loader

import-loader负责处理各模块之间的依赖关系,为什么需要这个loader呢?例如有一个index.css文件

// index.css
@import './common.css'
#app {
  color:pink;
}

loader顺序为postcss => css => style,在postcss-loaderindex.css文件处理时并不能解析@import './common.css'这个字段,所以只是处理了index.css里的css代码,然后到css-loader后可以解析@import './common.css'./common.css代码合并,最终结果是postcss-loader并未对./common.css进行处理,故需要在postcss-loader之前配置一个import-loader处理@import字段

配置如下:

use:[
  'style-loader',
  {
    loader: 'css-loader',
    options: {
      importLoaders:1
      // 当有@import时将导入的文件退回给上一个loader进行处理,具体数值看场景
      esModule:false
      // 解析background-img:url('xx.png')时,将url('..')作为esModule处理,
      // 将其关闭避免引入路径出错bug,即不作为esMoudule处理
    }
  },
 'postcss-loader'
]

# 3.4.2 file-loader

file-loader顾名思义就是文件资源的loader

const img = require('./assets/imgs/flower.png')
const music = require('./assets/medias/凤舞九天劲爆DJ.mp3')
const mp4 = require('./assets/medias/乡村爱情故事第三季1.mp4')

配置

{
  test:/\.(png|gif|svg|jpe?g|mp3|mp4)$/,
  use:['file-loader']
}

通过配置options字段管理打包后的资源名

{
/**
 * [ext]:扩展名
 * [name]:文件名
 * [hash]:文件内容
 * [hash:<length>]:限制hash长度
*/
  test: /\.(png|gif|svg|jpe?g|mp3|mp4)$/,
  use: [
    {
      loader:'file-loader',
      options:{
        name:'img/[name].[hash:7].[ext]'
      }
    },
  ]
}

# 3.4.3 url-loader

file-loader功能类似,但是url-loader可以将文件资源路径转化成base64的形式,从而减少网络请求,而file-loader是将资源打包至指定目录下,分开请求

补充:base64是将二进制文件以data uri字符串的形式来表示,也就是基于64个可打印字符来表示二进制数据,是网络上常用的用于传输8Bit字节码的编码方式

url-loader内部也可以调用file-loader,最佳实践配置如下

use: [
  {
    loader:'url-loader',
    options:{
      name:'img/[name].[hash:7].[ext]',
      limit:25*1024 // 小于25kb使用base64,否则调用file-loader
    }
  }
]

# 3.4.4 asset

asset是webpack内置的插件模块,可以替代file-loader、url-loader、raw-loader如下:

asset/resource --> file-loader
asset/inline --> url-loader
asset/source --> raw-loader
asset

配置

output:{
  // ...
  assetModuleFilename:'img/[name].[hash:7][ext]' // 指定资源存放路径及名称
},
module:{
  rules:[
    {
      test: /\.(png|gif|svg|jpe?g|mp3|mp4)$/,
      type:'asset/resource' // 替代file-loader
    }
  ]
}

推荐这样

{
  test: /\.(png|gif|svg|jpe?g|mp3|mp4)$/,
  type:'asset/resource',
  generator:{
    filename:"img/[name].[hash:7][ext]" // 指定资源存放路径及名称
  }
}

最佳实践

{
   test: /\.(png|gif|svg|jpe?g|mp3|mp4)$/,
   type:'asset', // 设置type为asset
   generator:{
     filename:'img/[name].[hash:7][ext]'
   },
   parser:{
     dataUrlCondition:{
       maxSize:25*1024 // 小于25kb使用base64解析为data uri
     }
   }
 }

# 3.4.5 字体图标

字体图标文件通常后缀名为.ttf.woff,在css文件中通过@font-face的方式引入,例如

@font-face{
  font-family:'iconfont';
  src:url('iconfont.ttf?t=15123458') format('truetype'),
      url('iconfont.woff?t=15123458') format('woff'),
      url('iconfont.woff2?t=15123458') format('woff2');
}

但是前面提过css-loader处理url字段时把它当作esModule处理,此时需要指定对应的loader,可以使用asset/resource

配置

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

# 4. Plugin

plugin可以在webpack工作的任意生命周期注入,从而做一些干预,类似生命周期钩子

graph LR
A(项目源码) -- plugins干预构建流程 -->B(构建后的项目)

plugins数组字段中传入插件实例即可

# 4.1 clean-webpack-plugin

clean-webpack-plugin可以在打包前自动删除打包后的目录,而无需每次手动删除

安装

npm i clean-webpack-plugin -D

配置

// 引入
let {CleanWebpackPlugin} = require('clean-webpack-plugin')

module.exports={
  // ...
  plugins:[ // 定义插件集合
    new CleanWebpackPlugin(), // 传入插件实例
  ]
}

# 4.2 html-webpack-plugin

html-webpack-plugin可以基于自己创建html模板在打包后的dist目录里动态添加index.html

配置

let HtmlWebpackPlugin = require('html-webpack-plugin')

plugins:[
  new HtmlWebpackPlug({
    ititle:'项目名', // 字段值会在打包时传入<title></title>
    template:'./public/index.html', // 定义模板文件
  })
]

public目录下创建html模板文件index.html,例如vue的html模板

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title><%= htmlWebpackPlugin.options.title %></title>
  <!-- 这里为ejx语法 -->
</head>
<body>
  <div id="app"></div>
</body>
</html>

执行打包构建命令npm run build,获得打包构建后的./dist/index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>项目名</title>
  <script defer="defer" src="index.js"></script>
</head>
<body>
  <div id="app"></div>
</body>
</html>

# 4.3 DefinePlugin

DefinePlugin是webpack内置的用于定义常量的插件(无须npm install了),通过键值对的方式定义

配置

let {DefinePlugin} = require('webpack')

plugins:[
  new DefinePlugin({
    BASE_URL:'"./"'
    // 注意这里是一个坑,插件在编译时会原封不动地将'...'里的
    // 东西赋值给BASE_URL这个变量,因此传入字符串需要为'"./"'
    // 即 BASE_URL = "./",若为对象则传入格式为'{...}'
  })
]

vue默认的html模板为

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link rel="icon" href="<%= BASE_URL %>favicon.ico">
  <title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
  <div id="app"></div>
</body>
</html>

此时定义了BASE_URL这个常量,模板打包编译后的./dist/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link rel="icon" href="./favicon.ico">
  <title>项目名</title>
  <script defer="defer" src="index.js"></script>
</head>
<body>
  <div id="app"></div>
</body>
</html>

# 4.4 copy-webpack-plugin

copy-webpack-plugin是webpack内置的插件,可自动将静态资源目录拷贝进打包后的目录里

配置

let CopyWebpackPlugin = require('copy-webpack-plugin')

plugins:[
 new CopyWebpackPlugin({
   patterns:[
     {
       from:'public',
      //to:path.resolve(__dirname,'dist') // 推荐省略to字段,默认会找webpack定义的出口path
       globOptions:{
         ignore:['**/index.html'] // 忽略不需要copy的文件
       }
     }
   ]
 })
]

# 5. 搭建本地服务器server

webpack配置watch字段为true,结合vs-code的live server服务器插件可以实现修改源码自动打包更新的效果,但是只修改一个地方,不能实现局部更新,wepack要重新再将所有源码打包编译,这种频繁读写文件的操作性能极差

# 5.1 webpack-dev-server

webpack-dev-server是webpack生态下的一个live server本地服务器,会将项目编译打包后存放在本地虚拟内存之中,默认为localhost:8080,即默认在本地服务器的8080端口

安装

npm i webpack-dev-server -D

package.json文件中配置启动指令webpack serve即可

"scripts": {
  "serve":"webpack serve"
},

运行以下命令,并访问localhost:8080

npm run serve

在webpack配置文件中,对应devServer字段可配置服务器

# 5.2 webpack-dev-middleware

webpack-dev-middleware可以将webpack打包构建后的文件作为中间件传递给一个自定义的服务器

安装

npm i webpack-dev-middleware -D

结合express开启服务器使用,在根目录下创建一个serve.js文件

let express = require('express')
let webpackDevMiddleware = require('webpack-dev-middleware')
let webpack = require('webpack')

const config = require('./webpack.config') // 获取配置文件
const compiler = webpack(config) // 编译打包

const app = express() // 创建server服务
app.use(webpackDevMiddleware(compiler)) // 作为中间件传递
app.listen(3000,()=>{
  console.log(`服务器运行在: localhost:3000`);
})

执行命令,开启服务后,访问localhost:3000即可

node serve.js

# 5.3 HMR

HMR即Hot Module Replacement,模块热更新,可以实现局部模块更新,其他的原封不动,极大地提升了构建性能。

HMR是基于本地开发服务器实现的即webpack-dev-server,对于webpack-dev-middleware将项目打包作为中间件传递给其他服务器,要实现HMR的办法目前我还不知道

配置

// 在webpack.config.js文件下配置devServer字段即可
devServer:{
  hot:true
}

指定热更新模块

// ./src/a.js
function fn() {
  console.log('a');
}
export{
  fn
}

// ./src/b.js
function fn2() {
  console.log('b');
}
export{
  fn2
}

// ./src/index.js
import { fn } from "./a";
import { fn2 } from "./b";

fn()
fn2()

if(module.hot){ // 如果开启热更新
  module.hot.accept(['./a']) // 热更新a.js文件,修改b.js则重新编译而非热更新(页面会重新刷新)
}

# 5.4 path

output为出口路径配置,属性配置如下:

  • path: 打包目录
  • filename: 出口文件名
  • publicPath:index.html内部引用路径= 域名+publicPath+filename
  • assetModuleFilename: asset-loader打包资源后存放地址及命名

项目构建打包部署到服务器后,域名解析即为打包后的项目根目录(默认为dist)

devServer下的publicPath指定的是本地服务所在的目录,默认为dist上一级即源码根目录

resolve定义webpack对路径的解析规则

resolve:{
  extensions:['.js','.json','.ts','.vue'], // 识别扩展名
  alias:{
    '@':path.resolve(__dirname,src) // 路径别名
  }
},

# 5.5 Proxy代理配置

proxy可以在开发阶段解决跨域问题,因为服务器与服务器之间不存在跨域的问题,proxy核心思想就是基于devServer服务器作为客户端与接口服务器之间的代理转发,从而实现跨域,因为devServer与客户端是同源

graph LR
A(客户端) -- Proxy -->B(服务器) -- Proxy --> A(客户端)

配置

devServer:{
  hot:true, // 开启热更新
  port:3000, // 项目部署于本地的3000端口
  proxy:{
    '/api':{
      target:'https://api.somewhere.com', // 用'/api'代理target
       // 若访问/api/users会代理的是https://api.somewhere.com/api/user
      // 而实际想要访问的是https://api.somewhere.com/users,那么需要重写覆盖
      pathRewrite:{'^/api':''}
      changeOrigin:true // 改变请求头里的host,设置为target同名的,以实现同源
    },
    '/api2':{
      target:'https://api.somewhere2.com'
    }
  }
}

# 5.6 source-map

source-map即源码映射,使得开发时能直接定位到源码,方便调试

在webpack配置文件中定义devtool字段即可

devtool:'source-map'

会在构建项目时生成index.js.map文件,是出口文件的一个map映射

# 5.7 配置区分开发环境和生产环境

配置package.json文件里的script字段

 "scripts": {
   "build": "webpack --env prd",
   "serve:dev":"webpack serve --env dev",
   "serve:prd":"webpack serve --env prd"
 },

之后在执行脚本构建项目时,会将prd或者dev属性挂载到prossess.env对象上,可以在webpack.config.js文件中访问到env变量

// 方法1
let { prd: isPrd,dev: isDev} = prossess.env

// 方法2
module.exports = (env)=>{
  let { prd: isPrd,dev: isDev} = env
}

// 下面展示如何根据环境区分配置
let path = require('path')
let { merge } = require('webpack-merge')

let defaultConfig = {
    // 默认配置
    entry:'./src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename:'index.js'
    },
    module: {
        rules: [
            // ...
        ]
    },
    plugins: [
        // ...
    ]
}
let devConfig = {
    // 开发环境配置
}
let prdConfig = {
    // 生产环境配置
}
module.exports = (env) => {
    let { prd: isPrd, dev: isDev } = env
    let config;
    if(isDev) config = merge(defaultConfig,devConfig)
    if (isPrd) config = merge(defaultConfig, prdConfig)
    return config
}

# 6. resolve

该选项用于配置模块如何解析,即在执行require('xx')或者import xx from 'xx'语句时会使用到该规则

配置

resolve: {
  alias: { // 路径别名
    Utilities: path.resolve(__dirname, 'src/utilities/'),
    Templates: path.resolve(__dirname, 'src/templates/')
  },
  extensions: ['.js', '.json'], // 文件拓展 优先级从左到右
  modules: [path.resolve(__dirname, 'src'), 'node_modules'] // 默认为['node_modules'],规定webpack解析模块时应该搜索的目录
  mainFields: ['browser', 'module', 'main'], // 规定要解析模块目录下package.json文件中的哪个字段为模块路径
}

// loader解析去掉后缀
resolveLoader: {
  moduleExtensions: ['-loader']
}
    人生如梦,
    我投入了的却是真情,
    世界先爱了我,
    我不能不爱它。
    红莲华
    x
    loading...