前端模块化开发-Webpack(笔记)

前端模块化开发-Webpack(笔记)

小技巧:折起代码,command+k+0

加载器loader

webpack,是一个打包儿的工具,具体的某项功能一般通过不同类型的加载器,或者插件实现,一般分为这几类:

  • 编译转换类;
  • 文件操作类;
  • 代码检查类

分割

提示:babel只是一个转换新特性的工具集合,具体使用还要指定插件

webpack加载资源的方式,以下都支持:

  • 遵循 ES Modules 标准的import声明;
  • 遵循 CommonJS 标准的require函数;
  • 遵循 AMD 标准的 define函数和require函数;
  • *样式代码中的@import指令和url函数;
  • *html代码中图片标签的src属性;

但是使用时最好统一使用一种,别混用,不利于代码的维护和使用;

一个简单loader的封装

每一个loader都想一个管道,将一个输入处理加工然后输出
处理markdown文件的loader;

const marked = require('marked');
// loader文件,负责资源文件的从输入到输出的转换,
// 他也是一种管道的概念,可以将这个loader的输出交给下一个loader去输入处理,可以依次执行多个loader
module.exports = source => {
  let html = marked(source);
  html = JSON.stringify(html); //可以转移"号
  console.log(html);
  // 必须返回一段js代码,会直接放入模块儿中
  return `module.exports = ${html}`
  
}

插件机制

帮我实现处理加载器loader模块之外的其他工作;

  • 清楚dist打包目录;
  • 拷贝静态文件至输出目录;
  • 压缩输出代码;
  • Webpack+Plugin无所不能的实现了前端工程化的几乎所有东西;
常用插件

1、clean-webpack-plugin,清除打包目录

const { CleanWebpackPlugin } = require('clean-webpack-plugin')
...
plugins:[
  new CleanWebpackPlugin()
]

2、html-webpack-plugin,生成html文件

const HtmlWebpackPlugin = require('html-webpack-plugin')
...
new HtmlWebpackPlugin({
   filename: 'about.html',
   template: './src/index.html'
})

3、copy-webpack-plugin,复制文件

const CopyWebpackPlugin = require('copy-webpack-plugin')
...
new CopyWebpackPlugin([
   // 'public/**'
  'public'
])

开发一个插件

Plugin通过钩子机制实现
Webpack几乎给每一个过程都添加了钩子函数,第三方插件就可以通过这些钩子来给webpack挂在不同的任务,
Webpack要求插件必须是一个函数或者是一个包含apply方法的对象;

需求去除bundle.js中的无用js
webpack插件

class MyPlugin {
  apply(compiler) { // apply方法
    console.log('my Plugin');
    // emit钩子资源输出到打包目录前
    compiler.hooks.emit.tap('myplugin', compilation => {
      let source = compilation.assets;
      // 循环出所有资源
      for( let name in source){
      	// 找到js资源
        if(name.endsWith('.js')){
          let content = source[name].source();
          // 替换/******/信息
          let content2 = content.replace(/\/\*\*+\*\//g, '');
          source[name] = {
            source: () => content2,
            size: () => content2.length
          }
        }
      }
    })
  }
}

原理就是,通过在生命周期的钩子中挂载函数实现扩展;

webpack 关键钩子
关键钩子钩子类型钩子参数作用
beforeRunAsyncSeriesHookCompiler运行前的准备活动,主要启用了文件读取的功能。
runAsyncSeriesHookCompiler“机器”已经跑起来了,在编译之前有缓存,则启用缓存,这样可以提高效率。
beforeCompileAsyncSeriesHookparams开始编译前的准备,创建的ModuleFactory,创建Compilation,并绑定ModuleFactory到Compilation上。同时处理一些不需要编译的模块,比如ExternalModule(远程模块)和DllModule(第三方模块)。
compileSyncHookparams编译了
makeAsyncParallelHookcompilation从Compilation的addEntry函数,开始构建模块
afterCompileAsyncSeriesHookcompilation编译结束了
shouldEmitSyncBailHookcompilation获取compilation发来的电报,确定编译时候成功,是否可以开始输出了。
emitAsyncSeriesHookcompilation输出文件了
afterEmitAsyncSeriesHookcompilation输出完毕
doneAsyncSeriesHookStats无论成功与否,一切已尘埃落定。

搭建开发环境

1、webpack-dev-server 开发服务器插件

// 服务配置,
  devServer: {
  	// 静态资源目录,可以配置数组,webpack会一个一个去找
    contentBase: './public', // 公共资源目录,可以是数组一个一个找
    proxy: {
      '/api': {
        // http://localhost:8080/api/users -> https://api.github.com/api/users
        target: 'https://api.github.com', //请求的代理地址
        // http://localhost:8080/api/users -> https://api.github.com/users
        pathRewrite: { //匹配目录替换
          '^/api': ''
        },
        // 不能使用 localhost:8080 作为请求 GitHub 的主机名 设为true后就会以后目标域名及https://api.github.com请求
        changeOrigin: true
      }
    }
  },
注意:webpack-dev-server 下载完后可能会运行报错,可以将其三个一起安装
npm install webpack webpack-cli webpack-dev-server -D

2、添加 Source Map,资源地图,方便调试;
Webpack中的Source Map有多钟模式,没有种模式都有不同的效果;

devtool: 'eval',
  • eval模式,不会生成.map文件,编译很快,但效果不好,只能定位文件;原理就是通过eval函数去执行,每个模块的代码,在通过 //# sourceMappingURL=XXX.js.map指定资源文件,//# sourceMappingYRL;
  • webpack模式多,一般通过其命名便可以推断其主要功能和区分;
  • 一般建议选择cheap-module-eval-source-map模式开发;

3、自动刷新的问题:
问题核心:自动刷新导致的页面状态丢失问题;
是否可以:页面不刷新的前提下,模块也可以及时更新;

Webpack中的HMR,模块儿热替换,在不影响应用运行的状态下,更新模块儿变化;
HMR是Webpack中最强大的功能之一,极大程度的提高了开发者的工作效率;
使用;通过 webpack-dev-server --hot,添加–hot参数
或者

const webpack = require('webpack')
...
  devServer :{
    hot:true,  //模块热替换
  },
  plugins:{
  	new webpack.HotModuleReplacementPlugin()
  }

我们还需要手动处理JS模块更新后的热替换,具体方法可以在模块代码中手动处理

import hello from './module1.js'

module.hot.accept('./module1.js', () => {
  // 这里处理热更新模式
})

注意事项:
1、热替换代码如果出现错误,是不容易发现的,建议使用hotOnly配置

hotOnly: true

2、没启用HMR的情况下,HMR API报错,使用前需要先判断

if(module.hot){
  module.hot.accept('./module1.js', () => {
     // 这里处理热更新模式
  })
}

3、代码中多了一些与业务无关的代码
打包配置,不设置HMR时,代码中的module.hot会判断为false。
webpack打包时就会去除,不会打包儿到生成环境。

手动处理热替换会比较麻烦,小型项目不适使用,大型项目或者长期项目较为使用,一般大型框架都会自己封装现成的HMR,像vue-cli、react-cli等

配置不同的生成和开发环境

webpack.config.js文件也可以导出一个函数返回配置

module.exports = (env, argv) => {
  // env 环境变量 argv这个环境的参数
  // 开发环境配置
  const config = {
    mode: 'development',
    entry: './src/main.js',
    output: {
      filename: 'js/bundle.js'
    },
    devtool: 'cheap-eval-module-source-map',
    devServer: {
      hot: true,
      contentBase: 'public'
    },
    module: {
      rules: [
        {
          test: /\.css$/,
          use: [
            'style-loader',
            'css-loader'
          ]
        },
        {
          test: /\.(png|jpe?g|gif)$/,
          use: {
            loader: 'file-loader',
            options: {
              outputPath: 'img',
              name: '[name].[ext]'
            }
          }
        }
      ]
    },
    plugins: [
      new HtmlWebpackPlugin({
        title: 'Webpack Tutorial',
        template: './src/index.html'
      }),
      new webpack.HotModuleReplacementPlugin()
    ]
  }

  if (env === 'production') {
    // 生产环境配置
    config.mode = 'production'
    config.devtool = false
    config.plugins = [
      ...config.plugins,
      new CleanWebpackPlugin(),
      new CopyWebpackPlugin(['public'])
    ]
  }
  // 返回配置
  return config
}

调用

webpack --env production

大型项目中,一般使用不同环境对应不同配置文件,多配置文件
webpack.common.js
webpack.dev.js
webpack.prod.js

// webpack.dev.js
const webpack = require('webpack')
const merge = require('webpack-merge') //专门合拼webpack配置的插件
const common = require('./webpack.common')
// 将开发配置合拼入公共配置
module.exports = merge(common, {
  mode: 'development',
  devtool: 'cheap-eval-module-source-map',
  devServer: {
    hot: true,
    contentBase: 'public'
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
})

调用

webpack --config webpack.dev.js

Webpack的默认配置

1、DefinePlugin默认插件,为我们注入了全局变量,默认注入了process.env.NODE_ENV

  plugins: [
    new webpack.DefinePlugin({
      // 值要求的是一个代码片段,API_BASE_URL就可以再业务代码中调用
      API_BASE_URL: JSON.stringify('https://api.example.com')
    })
  ]
console.log(API_BASE_URL)

2、Tree Shaking”摇树“,去除代码中的无用代码(未被引用的代码dead-code),冗余代码,webpack生成模式自动启动,其他模式要使用可以配置

  optimization: { //配置webpack内部的优化配置的
    usedExports: true, //只导出引用到的模块
    concatenateModules: true, //尽可能合并每一个模块到一个函数中,进一步减少代码体积,合并模块
    minimize: true // 最小压缩
  }

Tree Shaking与Babel的问题;
Tree Shaking是基于ES6的ESModules的,而Babel在编译时会将ESModules转为->CommonJS,致使Tree Shaking不能生效及usedExports: true不能生效;
解决;
最新版的Babel已经自动用关闭了ESModules到CommonJS转换的插件,所以使用最新的是可以使Tree Shaking生效的
或者使用

  module:{
    rules:[
      {
        test: /.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              // 如果 Babel 加载模块时已经转换了 ESM,则会导致 Tree Shaking 失效
              // ['@babel/preset-env', { modules: 'commonjs' }]
              // ['@babel/preset-env', { modules: false }]
              // 也可以使用默认配置,也就是 auto,这样 babel-loader 会自动关闭 ESM 转换
              ['@babel/preset-env', { modules: 'auto' }]
            ]
          }
        }
      }
    ]
  },

使用@babel/preset-env 的不同配置来关闭ESModules的转换

3、sideEffects(副作用)
副作用:指模块执行时除了导出成员之外所作的其他事情;
sideEffects一般用于npm包标记是否有副作用;

// extend.js
// 为 Number 的原型添加一个扩展方法,没有导出成员
Number.prototype.pad = function (size) {
  // 将数字转为字符串 => '8'
  let result = this + ''
  // 在数字前补指定个数的 0 => '008'
  while (result.length < size) {
    result = '0' + result
  }
  return result
}
// 除了导出成员的代码都属于副作用
// 像这样的模块都属于副作用样式文件属于副作用模块
import './global.css'
// extend都属于副作用模块
import './extend'

副作用要慎用,确保你的代码没有副作用
使用:

optimization: { //配置webpack内部的优化配置的
    sideEffects: true //副作用生成模式下会自动开启,开启之后,会先去当前项目的package.json查看当前代码是否有副作用,没有的话,就不会打包儿无用的模块。
  }
// package.json
"sideEffects": false //当为false没有副作用可以使用sideEffects,当为true时,有副作用不可用sideEffects。
// 或者配置某些文件不参与副作用
"sideEffects": [
  "./src/extend.js",
  "*.css"
]
注意:Tree Shaking和sideEffects,有相同的部分,但是不完全一样,Tree Shaking是未被引用的代码,sideEffects是未被导出的其他部分代码,sideEffects要慎用。

Webpack代码分割/代码分包

多入口打包

webpack配置

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

module.exports = {
  mode: 'none',
  // 配置多个入口
  entry: {
    index: './src/index.js',
    album: './src/album.js'
  },
  output: {
    filename: '[name].bundle.js' //[name]可动态指定文件名
  },
  optimization: { //webpack内部配置
    splitChunks: {
      // 自动提取所有的公共模块到单独的 bundle中
      chunks: 'all'
    }
  }
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/index.html',
      filename: 'index.html',
      chunks: ['index']  //不同的页面注入不同的js文件块儿
    }),
    new HtmlWebpackPlugin({
      title: 'Multi album',
      template: './src/album.html',
      filename: 'album.html',
      chunks: ['album']  //不同的页面注入不同的js文件块儿
    })
  ]
}
动态载入

需要动态载入模块时,需要引入模块儿时,使用import函数

import posts from './posts/posts'
import album from './album/album'
// -------替换为------------
import('./posts/posts').then(({default: posts}) => {
	//posts.....后续事情
})
import('./album/album').then(({default: album}) => {
	//album.....后续事情
})

魔法注释:
通过在函数中添加/* webpackChunkName: ‘components’ */,可以为每个模块命名,并且相同名字的模块就会被打包到一起。

import(/* webpackChunkName: 'posts' */'./posts/posts').then(({default: posts}) => {
	//posts.....后续事情
})
import(/* webpackChunkName: 'album' */'./album/album').then(({default: album}) => {
	//album.....后续事情
})

mini-css-extract-plugin插件,提取样式为单独的css文件,建议css较大时使用

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // 'style-loader', // 将样式通过 style 标签注入
          // 使用MiniCssExtractPlugin时,这里就不是通过style标签注入了,而是通过link标签引入,所以这里使用MiniCssExtractPlugin通过的loader来处理
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin()
  ]
...

optimize-css-assets-webpack-plugin插件,压缩css文件,webapck默认只会压缩js代码,css不会压缩,所以使用这个插件

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
...
  optimization: {
    minimizer: [
      new TerserWebpackPlugin(),
      new OptimizeCssAssetsWebpackPlugin()//官方建议添加到这里可以根据默认配置,生产环境时再压缩,但是也有问题,就是配置了minimizer属性之后,webpack会认为你要自定义压缩模式,就不会压缩js代码了。
      // 这里你要自己添加webpack内置的js压缩插件terser-webpack-plugin
    ]
  },
  plugins: [
    new MiniCssExtractPlugin(),
    // new OptimizeCssAssetsWebpackPlugin() //添加到这里无论在那种环境下都会压缩css
  ]
...
文件名Hash值

1、一般的filename都支持Hash值
‘[name]-[hash].bundle.js’ 项目级别hash整个项目hash都会变

  output: {
    filename: '[name]-[hash].bundle.js'
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name]-[hash].bundle.css'
    })
  ]

2、’[name]-[chunkhash].bundle.js’ 模块儿hash,相互关联的模块儿hash相同
3、’[name]-[contenthash].bundle.js’ 根据模块内容生成的hash,不同的内容不同的hash
4、’[name]-[contenthash:8].bundle.js’ 通过:8可以指定hash长度,一般就是8位就可以了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值