webpack基本配置

734 阅读10分钟

npm 相关知识

  1. 版本号 1.2.3

    • 1 大版本号: 代码重构
    • 2 中版本号: 代码加功能
    • 3 小版本号: bug修复
  2. 版本号前缀 ^ ~ * 不加前缀符号就装指定版本的依赖

  3. -D-S 的区别

    • -D: dependencies
    • -S: devDependencies
    • 将包发布以后 别人装这个包只会装dependencies的依赖
    npm install --only=prod
    npm install --only=dev
    npm install // 全装
    
  4. npm install过程

    • 寻找包版本信息文件package-lock.json, 依照它来进行安装
    • 查package.json中的依赖, 并检查项目中其他的版本信息文件
      • 如果不存在包版本信息文件 就完全按照package.json来安装 并且生成一个版本信息文件
      • 如果存在版本信息文件 则只会安装package.json中有而版本信息中没有的哪些包
    • 如果发现了新包, 就更新版本信息文件
  5. 如果发现某个包和预想的不一致 应该:

    • 看版本信息中该包的来源和版本 因为在安装的过程中 它的优先级是最高的

webpack

前端模块化
不进行特殊配置 只能处理js(es5及以下)
灵活 loader plugin可插拔

  1. webpack本身只能处理es6以下版本的js
  2. loader顺序
  3. plugin使用

作用域

概念: 代码运行时,变量、函数、对象的可访问性

全局作用域

在文件中开始写 javascript代码 就已经在全局作用域了
在js执行中 只有一个全局作用域 全局作用域的变量和资源都会挂载在全局对象
在浏览器中 这个全局对象是window;在node中,这个全局对象是global

// 在浏览器中
a = 1 // window.a = 1
var b = 2 // window.b = 2
let c = 3 // es6 let 块作用域 window.c = undefined

局部作用域

块作用域

模块化

作用域封装
重用性
解除耦合 (把系统 分解为一个个模块 拆分一个庞大的系统解除更个部分的耦合 当系统的某个部分发生改变 模块可以帮助我们快速的定位问题 模块(把功能的实现封装在自己的内部) 只要模块暴露的接口不变 模块内部逻辑的变化并不会影响其他模块)

一个页面有多个功能 会把多个功能对应的js通过外链 如下代码:
这三个js文件共用了一个全局作用域
在任何一个文件中 进行顶层作用域的变量或函数声明 都会暴露在全局之中 使得其他脚本可能获取 它并不想要的变量 当应用的规模和复杂度上升 这些脚本之间就很容易发生命名冲突 从而导致不可预知的问题

<script src="./moduleA.js"></script>
<script src="./moduleB.js"></script>
<script src="./moduleC.js"></script>
// moduleA.js:
var name = 'Lucy'

// moduleB.js:
var name = 'Jack'

// moduleC.js:
// moduleC里想用moduleA中的name变量 但是moduleB中把moduleA中的name变量覆盖了 所以拿到的name是错误的
console.log(name)

初步解决方案: 加上命名空间

缺点: 依然不能解决文件里的变量方法被随意访问和修改(用闭包去解决)

// moduleA.js:
var a = {
  name: 'Lucy',
  tell () {
    console.log('我的名字是', this.name)
  }
}
// moduleB.js:
var b = {
  name: 'Jack',
  tell () {
    console.log('我的名字是', this.name)
  }
}
// moduleC.js:
console.log(a.name)
a.name = 'Beta'

早起模块化的写法

// moduleA.js:
var moduleA = (function() {
  var name = 'Lucy'
  return {
    // name,
    tell () {
      console.log(name)
    }
  }
})()

// 更进一步改写 立即函数为标准模块的实现
(function(window) {
  var name = 'Lucy'
  function tell() {
    console.log(name)
  }
  window.moduleA = { tell }
})(window)

模块化的演变

从立即执行函数实现方法之后 模块化方案经历了很长一段时间演化 三个比较重要的阶段: AMD COMMONJS ES6-MODULE

AMD

Asynchronous Module Definition 异步模块定义
amd规范成型较早 现在应用并不广发
使用defined定义模块 接收三个参数:

  • 当前模块的id(给模块起名字)
  • 当前模块的依赖
  • 可以是函数/对象
    • 如果是函数 将函数返回值作为定义模块的接口导出
    • 如果是对象 这个对象就是当前模块的导出值 好处:
  • 显示的表达了每个模块依赖的其他模块有哪些
  • 模块的定义不在绑定在全局对象上 增强了模块的安全性 不必担心在别的地方被篡改
define('getSum', ['math'], function(math) {
  return function(a, b) {
    console.log('sum:', math.sum(a + b))
  }
})

COMMONJS

2019年被提出 最开始是为了定义服务端的模块标准 而不是用于浏览器环境
之后nodeJs采用并实现了它的部分规范 在模块系统上做了以下调整
一般来说 不会严格区分COMMONJS和nodeJs的模块标准
(两种标准之间有何区别)

在CommonJS中每个文件就是一个模块 并且拥有属于它自己的作用域和上下文

// 模块的依赖通过require函数来引入
const math = require('./math')
// 通过exports将其导出
exports.getSum = function(a, b) {
  return a + b
}

amd和commonJs具有同样的特性:
强调模块的依赖必须显示引入 这样做方便在维护复杂模块时 可以不必操心各个模块间引入顺序的问题

ES6 Module 推荐的模块化写法

// import 导入
import math from './math'
// export 导出
export function sum(a, b) {
  return a + b
}

webpack打包

  • webpack立即执行函数之间的关系
  • 打包的核心逻辑
// 安装依赖
/*打包*/
npm webpack -g
npm webpack-cli -g
/*监听工程目录文件改动*/
npm webpack-dev-serve -g // 监听工程目录文件改动 当修改源文件 会动态实时的重新打包 并且自动刷新浏览器

  1. webpack预置了一些配置 即使在没有配置的情况下 执行webpack命令

    D:\study\webpackTest>webpack // 找到当前项目目录的src/index.js开始打包 出口是<projectDir>/dist/main
    
  2. webpack运行的时候会找webpack.config.js 就会按照该文件的配置去打包

  3. webpack-dev-server

    // "webpack-cli": "^4.7.2" // webpack-cli版本高了 不兼容webpack-dev-server 改为"webpack-cli": "^3.3.12",
    D:\study\webpackTest\react-demo>webpack-dev-server
    internal/modules/cjs/loader.js:883
      throw err;
      ^
    
    Error: Cannot find module 'webpack-cli/bin/config-yargs'
    
    
  4. 常见loader loader加载顺序 先进后执行

    // cssloader
    npm install css-loader
    npm install style-loader
    
    
    
    /**
     * babel 将高版本的es代码转为低版本的
     */
    
    // src/test.js:
    [1, 2, 3].map(e => console.log(e))
    
    // 使用: babel src/test.js
    npm i @babel/core @babel/cli -g
    
    // 转换规则; 这个包核心功能转换高版本的es代码为es5 
    // 早些年用babel会用一系列的插件 但这样比较琐碎 babel官方会把一些插件集成到一个包里
    // 使用: babel src/test.js --presets=@babel/preset-env // 在控制台打印出来
    npm i @babel/preset-env
    
    /*babel支持配置文件*/
    
  5. 使用babel, babel支持配置文件:

    • 在package.json里配置
    {
     "babel": {
       "presets": ["@babel/preset-env"]
     }
    }
    
    • 在.babelrc里配置 优先级最高 (babel会自动在找有没有.babelrc)
  6. webpack中使用babel

    • 需要使用babel-loader 它依赖于@babel/core @babel/cli
       npm i babel-loader -D
       npm i @babel/core @babel/cli -D // 如果已经在全局装了可以不装
      
       // 指定规则将高版本es代码转为低版本
       npm i @babel/preset-env -D
       // 如果是react项目 转义jsx代码
       npm i @babel/preset-react -D
      
    • 在webpack.config.js中配置
      {
        module: {
          rules: [
            {
              test: /.jsx?/,
              exclude: /node_modules/, // 排除在外
              include: [],
              use: {
                loader: "babel-loader" // babel-loader依赖@babel/core @babel/cli
              }
            }
          ]
        }
      }
      
  7. 配置html-webpack-plugin

    const htmlWebpackPlugin = require('html-webpack-plugin')
    const path = require('path')
    module.exports = {
    plugins: [
     new htmlWebpackPlugin({
       // template: '/react-demo/src/index.html' // 需要处理的文件绝对路径
       template: path.resolve(__dirname, '/react-demo/src/index.html'), // 需要处理的文件绝对路径
       filename: 'index.html' // 打包过后的目标文件名称
     })
    ]
    }
           
    
  8. webpack-dev-server详细配置
    启动webpack-dev-server服务 可以监听工程目录文件改动
    当修改源文件 会动态实时的重新打包(并不会真的生成打包文件 可以理解为存在缓存中 ) 并且自动刷新浏览器
    可以配置不去刷新浏览器 就自动更新内容 -- 热更新

    
    const webpack = require('webpack')
    
    module.exports = {
      devServer: {
        open: true, // 自动打开浏览器
        hot: true // 热更新
      },
      plugins: [
        new webpack.HotModuleReplacementPlugin()
      ]
    }
    
    // 热更新出错提示
    // index.jsx:
    if (module.hot) {
      module.hot.accept(err => {
        console.log('热更新出BUG:', err)
      })
    }
    
    1. webpack-dev-server有自己默认配置项 也可以指定配置
       /**
        * open 指定是否自动打开浏览器
        * config 指定文件路径 不写的话 默认是找webpack.config.js
        */
       webpack-dev-server --open // 自动打开浏览器
       webpack-dev-server --config // 读webpack.config.js里的devServer配置
       // 可能会将生产环境和开发环境的配置分成两个文件来配置 需要指定文件
       webpack-dev-server --config build/webpack.dev.conf.js // 读build/webpack.config.dev.js里的devServer配置
      

webpack 性能调优

  • 打包结果优化
  • 构建过程优化
  • Tree-Shaking

打包结果优化

  1. 打包生产环境时 webpack会自动压缩代码

    webpack --mode production
    
  2. webpack也可以自定义打包工具 比如webpack官方提供了terser-webpack-plugin

     // 因为考虑到压缩这个环节非常重要 webpack有专门的属性(optimization) 来存放和压缩有关的东西
     const TerserPlugin = require('terser-webpack-plugin')
    
     module.exports = {
       optimization: {
         minimizer: [
           new TerserPlugin({
             // cache: true, // 开启缓存 属性被移除
             parallel: true, // 开启多线程; 压缩是比较耗时的
             terserOptions: {
               compress: {
                 unused: true, // 自动剔除无用代码
                 drop_debugger: true, // 去除debugger
                 drop_console: true // 去除console
               }
             },
           })
         ]
       } 
     }
    
     /**
      * 之前有个Ugligy.js在压缩es5方面做得很优秀 但在es6的代码压缩上做得不够好
      * 所以后来有一个Uglify-es的项目 处理es6; 由于后来没有人维护Uglify-es
      * tenrser-webpack-plugin 就是uglify-es项目拉的一个分支来继续去维护
      */
    
  3. webpack打包结果分析
    从构建体积大的文件 下手去做优化

        // 安装: npm i webpack-bundle-analyzer -D
    
       const BundleAnalyzerPlugin = require('webpack-bundle-plugin').BundleAnalyzerPlugin
    
       module.exports = {
        plugins: [
          new BundleAnalyzerPlugin()
        ]
       }
    

构建过程优化

构建速度和构建体积往往是耦合在一起的 打包的时候可以

  1. 开启缓存
  2. 开启多线程 HappyPackthread-loader

nodejs单线程模型的, 所以运行在nodejswebpack单线程的 所以webpack需要处理的事得一件一件的做, 要webpack多件事情一起做 需要多核cpu发挥作用
使用插件可以让webpack支持多个线程去打包; 而简单的项目使用多线程打包 会浪费更多的cpu资源 这样不仅不能加快 还会降低打包速度

webpack中实现多线程打包 它都是从线程的维度来做这个事情的; happypack,thread-loader也好,它都是在创建线程池;
因为webpack,node都是单线程的 所以要发挥多线程优势 必须要借助进程这个维度的数量; 但是在webpack上体现出来的只有多线程 背后利用的是多进程

happyPack: 多进程模型 插件可以开启多进程 他会把任务分解成多个子进程 并发的去执行 子进程执行完后 把结果返回给主进程
thread-loader: 它是针对loader进行优化的, 它会把loader放在一个线程池里(worker) 达到多线程构建的目的 使用是必须放在所有loader之前(loader是栈顺序)

thread-loader: 构建过程中哪些是比较耗时间:

  1. 解析
    webpack在进行文件打包时, 会对文件进行递归处理;
    像jquery、echarts库 它们非常大又没有模块化标准
    webpack解析这些文件耗时且没有意义 像这种解析不动的可以不解析 配置noParse

    被忽略的文件 不应该包括 import require defined这样的模块化的一些语句, 否则构建出来的文件 包含不能被浏览器环境下执行的语句

     module.exports = {
       noParse: /node_modules/(jquery.js)/
     } 
    
  2. 查找:

    • 在rules匹配文件
        module.exports = {
          rules: [
            {
              test: /.jsx?/,
              exclude: /node_modules/, // 排除node_modules
              include: path.resolve('src'), // 匹配指定文件(可以是正则) include的优先级比exclude和test大 所以这三者冲突时 include优先级最高
            }   
          ] 
        }   
      

其他思路:

  • 还可以通过预编译来构建优化
  • 还可以在loader上下手 fast-sass-loader 并行的处理sass文件 比sass-loader快很多倍
  • sourceMap耗时严重 设置为false

Tree-Shaking DCE的一种实现

消除无用代码 DCE
首先 webpack自己会分析es6的modules的引入情况 去除不使用的import引入
然后借助一些工具 比如tenser-plugin 对无用的模块进行删除(这些工具只有在mode 为production时会去掉无用的代码 为development时 只会去掉import引用)