webpack笔记

Webpack

功能包括css

  • 模块打包器(Module bundler)
  • 模块加载器(Loader)
  • 代码拆分(Code Splitting)模块增量加载,渐进式加载
  • 资源模块(Asset Module)

打包工具解决的是前端总体的模块化,并非单指JavaScript模块化html

安装,由于是基于npm的包,因此前端

  • yarn init --yes
  • yarn add webpack webpack-cli --dev (^4.40.2, ^3.3.9)
  • yarn webpack --version 查看版本
  • yarn webpack 开始打包,webpack自动从src/index.js开始打包

4.0之后webpack执行支持零配置打包,将src/index.js做为打包入口 -> dist/main.js生成地址node

添加配置文件 webpack.config.js,运行在node环境的js文件,按CommonJs方式编写代码webpack

image.png

# webpack.config.js

const path = require('path')

module.exports = {
  entry:'./src/main.js', //项目打包入口文件路径
  output:{               //输出文件配置,是个对象
    filename:'bundle.js', //输出文件名称
    path:path.join(__dirname,'output')         //输出文件所在目录,必须是absolute path!
  },
  mode:'development' //webpack4以上 工做模式(不一样环境的几组预设配置)
  //默认是production,自动启动优化,优化打包结果
  //development 优化打包速度,添加一些调试过程须要的辅助
  //none 原始打包不会进行额外处理
  // 使用yarn webpack --mode development执行
}

资源打包

非js代码经过loader加载,Webpack内部的loader默认只能处理js,json文件
image.pnggit

yarn add css-loader --dev (^3.2.0)es6

yarn add style-loader --dev (^1.0.0)github

const path = require('path')

module.exports = {
  entry:'./src/main.css', //项目打包入口文件路径
  output:{               //输出文件配置,是个对象
    filename:'bundle.js', //输出文件名称
    path:path.join(__dirname,'dist')         //输出文件所在目录,必须是absolute path!
  },
  mode:'none',//production,development
  module: {
    rules:[ //除js外其余资源模块加载规则配置
      {
        test:/.css$/,
        use:['style-loader','css-loader'] //匹配到的文件使用的loader
      }
    ]
  }
}

loader内的rules,use从后向前执行,经过style标签挂载到html上web

image.png

导入资源模块

上面导入css经过修改entry入口文件,可是通常状况下入口文件仍是js文件,打包的入口就至关于运行的入口,JS驱动整个前端应用业务,
经过入口文件引入 css的方式也能达到目的npm

# main.js

import createHeading from './heading.js'

import './main.css'

const heading = createHeading()

document.body.append(heading)

根据代码须要动态导入资源,例如js中引入css

为何不是js,css分离的方式?

js能够比做驱动文件,而css和样式是辅助js进行美化的,所以在js中引入css逻辑更合理,并且js确实须要这些资源文件

确保上线资源不缺失,都是必要的

文件资源加载器

文件图片资源加载方式经过yarn add file-loader --dev (^4.2.0)

const path = require('path')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    // publicPath: 'dist/' //默认值''网站的根目录
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /.png$/,
        use: 'file-loader'
      }
    ]
  }
}

然而图片资源并无被加载出来,查看下加载路径

image.png

默认加载网站根目录下的资源(应该加载dist下的资源),这是因为index.html没有生成到dist目录,而是放在项目的根目录下,因此把项目根目录做为网站根目录,
而webpack会默认认为全部打包结果都放在网站的根目录下面,解决方法配置publicPath: 'dist/'

webpack打包时遇到图片文件,根据配置文件配置,匹配到文件加载器,文件加载器开始工做,先将导入的文件copy到输出目录,而后将文件copy到输出目录事后的路径,做为当前模块的返回值返回,这样资源就被发布出来了。不理解能够看下bundle.js文件

URL加载器

data urls是特殊的url协议,能够用来直接表示一个文件,传统url要求服务器有一个对应文件,而后咱们经过请求这个地址获得这个对应文件。 而data url是一种当前url就能够直接表示文件内容的方式,这种url中的文本包含文件内容

使用时不会发送任何http请求

data:text/html;charset=UTF-8,<h1>xxx</h1> //html内容,编码utf-8

对于图片,字体没法直接经过文本去表示的二进制类型文件,经过将文件内容进行base64编码,编号后的字符串表示文件内容

data:image/png;base64,iVBORw0KGgoAAAANSUhE... //png类型文件,编码base64,编码

webpack打包静态资源模块时,经过data urls咱们能够用代码形式表示任何类型文件了

yarn add url-loader --dev (^2.2.0)

module: {
    rules: [
      {
        test: /.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /.png$/,
        use: 'url-loader'//将其转化为 dataurl 形式
      }
    ]
  }

适合体积比较小的资源,小文件使用data urls减小请求次数,大资源会致使生成的文件体积过大,影响加载效率,所以大文件用file-loader单独提取存放,提升加载速度 ,所以为loader添加options

module: {
  rules: [
    {
      test: /.css$/,
      use: [
        'style-loader',
        'css-loader'
      ]
    },
    {
      test: /.png$/,
      // use: 'url-loader'//将其转化为 dataurl 形式
      use:{
        loader:'url-loader',
        options:{ //loader配置选项
          limit:10*1024 //10kb
        }
      }
    }
  ]
}

须要注意的若是你使用url-loader的话,必定要同时安装file-loader,url-loader对于超出10kb的文件仍是会调用file-loader

经常使用加载器分类

  1. 编译转换类
  2. 文件操做类
  3. 代码检查类

对写的代码进行校验的加载器,目的统一代码风格,提升代码质量,通常不会修改生产环境代码

webpack与es2015

webpack由于模块打包须要,因此处理了import和export,可是并不能转化代码中其余的es6特性

yarn add babel-loader @babel/core @babel/preset-env --dev (^8.2.2, ^7.14.3, ^7.14.2)

module: {
  rules: [
    {
      test:/.js$/,
      use:{
        loader:'babel-loader',
        options:{
          presets:['@babel/preset-env']
        }
      }
    },
    {
      test: /.css$/,
      use: [
        'style-loader',
        'css-loader'
      ]
    },
    {
      test: /.png$/,
      // use: 'url-loader'//将其转化为 dataurl 形式
      use:{
        loader:'url-loader',
        options:{ //loader配置选项
          limit:10*1024 //10kb
        }
      }
    }
  ]
}

webpack只是打包工具,loader加载器能够用来编译转换代码

模块加载方式

模块加载的方式有遵循ES Modules标准的import声明,遵循CommonJS 标准的require函数,遵循AMD标准的define函数和require函数,建议不要混着用,会下降项目可维护性

除了上面方式,触发模块加载方式还有css样式代码中的url函数和@import指令,html代码中图片标签的src属性

css样式文件中 url 触发模块加载

# main.css

body {
  min-height: 100vh;
  background: #f4f8fb;
  background-image: url(background.png);
  /* url触发了模块加载,注意下url-loader版本 2.2.0,如今4不显示 */
  background-size: cover;
}

@import url(reset.css)
# reset.css

* {
  margin:0;
  padding:0;
}
# main.js

import 'main.css'

html中的src属性触发模块加载

yarn add html-loader --dev (^0.5.5)

# footer.html

<footer>
  <img class="lazy" referrerpolicy="no-referrer" data-src="better.png" alt="">
</footer>
# main.js

//html文件默认会将html代码做为字符串导出,还须要为html模块配置loader
import footerHtml from './footer.html' 
document.write(footerHtml)
# webpack.config.js

{
  test:/.html$/,
  use:{
    loader:'html-loader'
  }
}

可是html-loader只能处理html 下 img:src 属性,其余额外属性经过attrs 配置

{
  test:/.html$/,
  use:{
    loader:'html-loader', //默认只能处理html img src属性,其余额外属性经过attrs配置
    options:{ 
      attrs:['img:src','a:href']
    }
  }
}

这个文件能够被处理了

# footer.html

<footer>
  <!-- <img src="better.png" alt=""> -->
  <a href="better.png">download png</a>
</footer>

核心工做原理

项目中散落各类各样代码及资源,webpack会根据咱们的配置,找到其中一个文件做为打包的入口,通常状况下这个文件都是.js文件,而后他会顺着咱们入口文件中的代码,根据代码中出现的import或require之类的语句,解析推断出这个文件所依赖的资源模块,而后分别去解析每个资源模块对应的依赖,最后造成了依赖关系的依赖树,webpack会递归这个依赖树,找到每一个节点对应的资源文件,最后根据配置文件中的rules属性,去找到模块对应的加载器,交给对应的加载器去加载这个模块,最后将加载到的结果放入到bundle.js中,从而实现整个项目打包。

整个过程当中,loader机制事webpack的核心

开发一个loader

建立一个markdown-loader, 引入.md文件,输出转化后的html

先在根文件夹下建立配置和loader文件

image.png

每一个webpack loader都须要导出一个函数,这个函数就是loader对加载到的资源的处理过程

输入就是加载到的资源文件的内容,输出就是这次加工事后的结果

# markdonw-loader.js

module.exports = source => {

  // console.log(source);

  //webpack加载资源过程相似于工做管道,能够在加载过程当中依次使用多个loader,可是最终结果必须是js代码
  // return 'hello ~'   //x
  // return 'console.log("hello ~")'  //√


  //yarn add marked --dev (markdown解析模块)
  const html = marked(source) //返回值是html字符串

  
  // return html //面临上面一样问题,正确作法把他变成js代码

  // 方式1
  // html做为模块导出的字符串,经过module.exports = 这样一个字符串,
  // 可是简单拼接的话,html中存在的换行符或者引号拼接一块儿会形成语法错误

  // return `module.exports = ${html}` //x

  // return `module.exports = ${JSON.stringify(html)}` //webpack会解析模板字符串中的js代码

  // return `export default  ${JSON.stringify(html)}` //可使用esm语法

  //方式2
  // 返回html 字符串,而后再向管道中添加一个loader处理字符串
  // 交给下一个loader处理,须要安装 html-loader (组合loader的形式)

  return html
}
# webpack.config.js

const path = require('path')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    publicPath: 'dist/'
  },
  module:{
    rules:[
      {
        test:/.md$/,
        use:[  
          'html-loader',
          './markdown-loader' //模块名称或文件路径,相似nodejs的require
        ]
      }
    ]
  }
}

固然也能够把自定义loader发布到npm包上

总结

loader负责资源文件从输入到输出的转换,其实是一种管道概念,对同一资源能够依次使用多个loader

插件机制

加强webpack自动化能力

loader专一实现资源模块加载,plugin解决其余自动化工做

e.g. 清除dist目录

e.g. 拷贝静态文件至输出目录

e.g. 压缩输出代码
webpack+plugin 实现了绝大多数前端工程化工做

自动清除目录插件

yarn add clean-webpack-plugin --dev第三方插件

# webpack.config.js

const {CleanWebpackPlugin} = require('clean-webpack-plugin')

plugins:[
  new CleanWebpackPlugin()
]

自动生成使用bundle.js的HTML

yarn add html-webpack-plugin --dev第三方插件(^3.2.0)

dist下生成html,解决之前根下html中的引入路径发生改变须要硬编码的问题(经过 publicpath解决的),如今经过动态注入。

# webpack.config.js

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

plugins:[
    new HtmlWebpackPlugin()
]

会在dist/生成 index.html文件。注意要去掉publicpath,由于此时index在dist下,咱们默认把根目录设置成了dist/ ,访问时浏览器会把index.html所在的路径设置为根路径,至此咱们就能够删除项目中的index.html了,经过webpack自动生成

改进html-webpack-plugin生成结果

默认生成在index.html,若是想自定义修改经过配置。例如修改html的title,自定义html基础dom结构,html-webpack-plugin参数参考文章

# webpack.config.js

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

plugins:[
  new HtmlWebpackPlugin({
    title:'Webpack Plugin Sample',
    meta:{  //设置页面中元数据标签
      viewport:'width=device-width'
    },
    template:'./src/index.html'//对于大量的配置经过建立模板文件,根据模板生成页面
  })
]

模板文件

# ./src/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Webpack</title>
</head>
<body>
  <div class="container">
    <!-- 访问插件配置数据 -->
    <h1><%= htmlWebpackPlugin.options.title %></h1>
  </div>
</body>
</html>

同时输出多个页面文件

除非是单页面应用,不然须要输出多个html,经过添加多个实例对象到plugins中

# webpack.config.js

  plugins:[
    //用于生成index.html
    new HtmlWebpackPlugin({
      title:'Webpack Plugin Sample',
      meta:{ 
        viewport:'width=device-width'
      },
      template:'./src/index.html'
    }),

    //用于生成about.html
    new HtmlWebpackPlugin({
      filename:'about.html' //默认的filename是index.html
    })
  ]

静态文件拷贝插件 copy-webpack-plugin

yarn add copy-webpack-plugin --dev (^5.0.4)

拷贝根目录下 public/xx.ico静态文件至dist

# webpack.config.js

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

plugins:[
  //参数数组,指定拷贝的文件路径(通配符,目录,相对路径)
  new CopyWebpackPlugin([ 
    // 'public/**'
    'public'
  ])
]

总结

每一个项目用到的,常见的plugin

  • clean-webpack-plugin
  • html-webpack-plugin
  • copy-webpack-plugin

经过github查看特性,作到心中有数。其余插件经过github特殊使用时特殊查找,例如imagemin-webpack-plugin 图片压缩

插件机制工做原理

相比于loader(加载模块时候),plugin(涉及webpack工做每一个环节)拥有更宽的能力范围

webpack plugin机制就是软件开发中最多见的钩子机制,经过钩子机制实现。

钩子机制相似于web中的事件。在webpack工做过程当中会有不少环节,为了便于插件的扩展,webpack几乎给每个环节都埋下了钩子,这样咱们在开发插件时,能够经过往这些不一样的节点上去挂载不一样任务,就能够轻松扩展webpack的能力。具体有哪些预先定义好的钩子参考文档

image.png

webpack开发一个插件

定义一个插件往钩子上挂载任务,这个插件用于清除bundle.js中无用的注释//

webpack要求插件必须是一个函数或者是一个包含apply方法的对象,
通常咱们都会把插件定义为一个类型,而后再这个类型中定义一个apply方法
使用时经过类型构建实例去使用

# webpack.config.js

class MyPlugin{

  apply (compiler){ // 此方法在webpack启动时自动被调用,compile配置对象,配置信息

    console.log('MyPlugin 启动');

    // 经过hooks属性访问钩子emit 
    // 参考:https://webpack.docschina.org/api/compiler-hooks/
    // tap方法注册钩子函数(参数1:插件名称,参数2:挂载到钩子上的函数)

    compiler.hooks.emit.tap('MyPlugin',compilation=>{

      // compilation 能够理解为这次打包的上下文,全部打包过程产生的结果都会放到这个对象中
      // compilation.assets属性是个对象,用于获取即将写入目录当中的资源文件信息

      for(const name in compilation.assets) 
      {

        // console.log("文件名称:",name); 如图
        // console.log("文件内容:",compilation.assets[name].source());
        
        if(name.endsWith(".js")){

          //获取内容
          const contents = compilation.assets[name].source();

          //替换内容
          const withoutComments = contents.replace(/\/\*\*+\*\//g,'')

          //覆盖老内容
          compilation.assets[name] = {

            source:()=> withoutComments,
            size:()=> withoutComments.length //返回内容的大小,webpack要求必须加
          }
        }
      }
    }) 
  }
}

// 使用自定义插件
plugins:[
  new MyPlugin()
]

从上面的类咱们了解了插件是经过在生命周期的钩子中挂载函数扩展