模块打包器-Webpack

引入模块化后,解决了大体量项目的开发问题,但是又带来了一些新问题。比如:
ES Module还存在兼容性问题
模块文件过多,网络请求频繁
所有前端资源都需要模块化,不仅仅是JS,还需要css,html图片等静态资源

所以需要工具满足我们以下设想:


es6=>es5

能将散落的模块化文件再次打包到一起,因为生产阶段不需要分散的模块

还能支持多种类的前端资源类型

所以出现了前端模块化打包工具

webpack

常见工具:webpack,RollUp,Parcel
webpack,模块打包器,在打包过程中,可以通过模块加载器(Loader)对新特性进行编译转换,它还具备代码拆分(code splitting)的能力,将模块按我们的需要去分组打包,用渐进式加载解决文件太碎或文件太大,这两个极端的问题,还支持资源模块(Asset Module),支持载入任意资源类型的文件,比如import一个css文件。
打包工具解决的是前端整体的模块化,并不单指JS的模块化

打包结果运行原理:
webpack打包过后的代码并不会特别复杂,只是把所有模块翻到了同一文件当中,还提供了基础代码,让模块与模块间相互依赖的关系还可以保持原有状态。

几个关键概念

// webpack.config.js
const path = require('path')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          'style-loader', // 把样式转化成style标签插进html
          'css-loader' // 把css转换成js模块
        ]
      }
    ]
  }
}
  • webpack.config.js 是配置webpack的文件,运行在node上,所以我们需要遵循commonJS
  • entry: 入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack 会递归找出有哪些模块和库是入口起点(直接和间接)依赖的
  • output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件
  • mode:通过选择 development 或 production 或 none;none不会开启任何默认插件。
  • loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
    注意,loader 能够 import 导入任何类型的模块(例如 .css 文件),这是 webpack 特有的功能。
    以上配置中,对一个单独的 module 对象定义了 rules 属性,里面包含两个必须属性:test 和 use。这告诉 webpack 编译器(compiler) 如下信息:
    “嘿,webpack 编译器,当你碰到「在 require()/import 语句中被解析为 '.txt' 的路径」时,在你对它打包之前,先使用 raw-loader 转换一下。”

webpack只是打包工具,loader用来转换编译代码。

loader

常见的资源加载器,有file-loader,url-loader(Data Urls形式)
最佳实践为:小文件用Data Urls,减小请求次数,大文件还是file-loader,提高加载速度。

{
        test: /.png$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 // 10 KB  大于10自动调用 file-loader
          }
        }
      }

常用Loader分类:
编译转换类型:css-loader,babel-loader
文件操作类型:file-loader,
代码检查类:eslint-loader

babel-loader

es6=>es5的编译要安装三个东西
babel-loader @babel/core @babel/present-env


image.png

babel-loader是一个开关,只是触发@babel/core去工作,它是转换的核心,但是他的转换需要插件来指定,比如babel/classes是针对es6中class新语法的,@babel/present-env就是插件的集合,把所有新特性都支持了

webpack加载资源的方式

webpack加载资源的方式

几乎所有代码中所要引用的资源,都会被webpack找出来,根据我们的配置交给不同的loadel去处理,最后将处理结果整体打包到输出目录。

webpack核心工作原理

官网首页

官网首页其实就很好的解释了工作原理。
从入口文件开始,webpack递归的找到所有依赖,形成了一个依赖树,根据依赖树和配置文件中的rules属性,找到其对应的加载器(loader),交给对应的加载器去处理,最后将处理结果整体打包到输出文件中。其中Loader机制是webpack的核心。

开发一个Loader

我们来实现一个markDown-loader

// m-loader.js
// 首先loader要运行在node上要遵循commonJS
// Loader就是一个函数,函数的参数source就是拿到的源代码
// Loader工作的结果必须是js代码
module.exports = source => {
  console.log(source)
  // return "hello ~" // 返回的不是js 不行
  return 'console.log("hello ~")' // 可以
}

或者可以交给其他loader处理


image.png
// m-loader.js
const marked = require('marked')
module.exports = source => {
  const html = marked(source)
  // 返回 html 字符串交给下一个 loader 处理
  return html
}
// webpack.config.js
{
        test: /.md$/,
        use: [
          'html-loader',   // 交给你
          './m-loader'
        ]
      }

plugin插件

loader专注资源的加载,plugin解决其他自动化工作。
比如:压缩代码,清除dist目录,拷贝静态文件到输出目录等
webpack+plugin 实现了大多数前端工程化的工作,所以让大家以为webpack就是前端工程化。

常用插件

plugins: [
    new webpack.ProgressPlugin(),
    new CleanWebpackPlugin(), // 清除dist
 // HtmlWebpackPlugin 自动生成使用打包结果bundle.js的html文件
    // 用于生成index.html
    new HtmlWebpackPlugin({
        template: './src/index.html' // 以谁为模版
    })
    // 多页面应用,再生成个其他的html
    new HtmlWebpackPlugin({
      filename: 'about.html'
    })
    // 静态文件拷贝(其实上线前才会用,开发阶段不要用这个,开销大)
    new CopyWebpackPlugin(['public']),
  ]

想找插件就去 github上搜索 关键字[ plugin webpack image mini ]

自己实现一个插件

相比loader,plugin有着更宽的一个能力范围,plugin通过钩子机制实现

class MyPlugin {
// 必须有apply方法
  apply (compiler) {
    console.log('MyPlugin 启动')
// 自己上官网找钩子时机
    compiler.hooks.emit.tap('MyPlugin', compilation => {
      // compilation => 可以理解为此次打包的上下文
      for (const name in compilation.assets) {
         // ......
      }
    })
  }
}

webpack的开发体验

如果只是完成上线任务,那么上面的功能足够用了,但是实际中,你本地的开发环节时间是远大于你上线的那几分钟的,好的开发体验才能事半功倍,我希望我的开发环境能:

  • 项目可以以http serve来运行,才能使用ajax,如果以文件形式运行那是不支持的。
  • 边开发,边自动编译,边自动刷新
  • 提供SourceMap支持,便能在浏览器调试,定位问题源码

那么对于上述功能,webpack都以实现

自动编译

watch工作模式
webpack --watch 用于观察依赖文件的变化,一旦有变化,则可以重新执行构建流程

自动刷新浏览器

npm install browser-sync
browser-sync dist --files"*/"
browser-sync会监听dist文件下的变化,有变化就刷新。
这样就实现了自动编译+自动刷新,但是效率还是低,因为要用到两个工具,还需要磁盘的写入后读取,还有更好的解决办法。

Webpack Dev Server

Webpack官方提供的工具,它提供了http server,集成了自动编译自动刷新浏览器功能等对开发友好的功能

npm install webpack-dev-server --dev
webpack-dev-server // 启动啦

webpack-dev-server为了提高效率,并没有使用磁盘,它将打包结果暂时存放在内存中,http server也是从内存中把这些文件读出来发给浏览器。

静态资源访问
dev-server默认会将构建结果输出的文件,全部作为开发服务器的资源文件,也就是说只要能够作为webpack打包输出的文件,都可以被正常访问到,如果有些静态资源也需要被访问的话(假如图片没有经过webpack构建),就需要额外告诉webpack

// webpack.config.js
// contentBase额外为httpserver指定查找资源目录
module.exports = {
    devServer: {
      contentBase: ['./public'] // 指定了public文件为额外的资源路径
    },
}

代理API
开发阶段的接口跨域问题

解决问题最好的方法

通过开发服务器代理,因为服务器与服务器间的请求是不跨域的。
dev server支持配置代理
我们实现代理github的一个接口

  devServer: {
    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 的主机名
        changeOrigin: true
      }
    }
  },

Source Map

在开发阶段,调试和报错我们都希望基于开发阶段的代码而不是编译过后的。Source Map(源代码地图)就是最好的一种方法,它是一种映射源代码与编译后代码的关系地图,转换后的代码通过source map逆向解析,就可得到源代码。
source map固定格式
在编译后文件最后一行添加 //# sourceMappingURL=xx.js.map

webpack配置source map

module.exports = {
     devtool: 'source-map',
}

webpack支持12种sourcemap风格,每种方式的效果和速度各不同。官网有表格对比差异
选择合适自己的sourcemap,一般来说
开发环境下:eval-cheap-module-source-map (loader转换前,能定位到行)
生产环境下:nosources-source-map

webpack自动刷新问题

自动刷新带来了一个问题就是,会把输入框中你输入的文字刷新掉,还是麻烦一点,最好保留着。
问题核心:自动刷新导致页面状态丢失。最好的解决办法是,在页面不刷新的前提下,模块也可以及时更新。

HMR(模块热更新)

热更新:应用程序运行的过程中,实时的替换掉应用中的某个模块,应用运行状态并不受影响。
HMR是webpack最强大的特性之一,极大提高了开发效率。
开启,

// HMR已经集成在了dev-server中
const webpack = require('webpack')
module.exports = {
  devServer: {
    hot: true
    // hotOnly: true // 只使用 HMR,不会热替换失败又刷新浏览器
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
}

我们需要手动处理JS模块更新后的事情,CSS可以不用我们手动处理是因为css有规律可言,js没有,那react可以是因为react也是有规律可言的。

module.hot.accept('./editor', () => {// 处理逻辑})

webpack不同环境下的配置

开发环境注重开发效率,生产环境注重代码运行效率
如何解决呢:
配置文件根据环境不同导出不同配置 或 一个环境对应一个配置文件

// 配置文件根据环境不同导出不同配置
// webpack.config.js
module.exports = function(webpackEnv) {
    const isEnvDevelopment = webpackEnv === 'development';
    const isEnvProduction = webpackEnv === 'production';
    const config = {  ...   }
    return config
}

// 一个环境对应一个配置文件


image.png

DefinePlugin

mode = production模式下面,内部默认开启了很多功能。
比如DefinePlugin,为我们的代码注入全局成员,例如
process.env.NODE_ENV这样一个常量,很多第三方模块都会判断这么一个常量从而做不同操作

plugins: [
    new webpack.DefinePlugin({
      // 值要求的是一个代码片段
      API_BASE_URL: JSON.stringify('https://api.example.com')
    })
  ]

Tree Shaking

摇树,用来去除未引用代码,在生产模式下自动开启,我们来尝试在mode=none下开启Tree Shaking

module.exports = {
// optimization是集中去配置webpack内部优化功能的
  optimization: {
    // 模块只导出被使用的成员
    usedExports: true, 
    // 压缩输出结果
    minimize: true
    // 尽可能合并每一个模块到一个函数中
    concatenateModules: true,
  }
}

代码分割

默认webpack会把所有代码都打到一个包中,如果代码太多,bundle就会很大,并不是每个模块在启动时都是必要的,最好是分离到多个bundle中,根据应用按需加载。
代码分割有2种方式:

  • 多入口打包
  • 动态导入

多入口打包

适用于多页面应用程序,一个页面对应一个打包入口,公共部分单独提取

module.exports = {
  mode: 'none',
  entry: {
    index: './src/index.js',
    album: './src/album.js'
  },
  output: {
    filename: '[name].bundle.js' // name会取自输入文件name
  },
// 不同的打包接口中会有相同的代码出现,于是我们需要提取公共模块
  optimization: {
    splitChunks: {
      // 自动提取所有公共模块到单独 bundle
      chunks: 'all'
    }
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/index.html',
      filename: 'index.html',
      chunks: ['index'] // 指定需要的bundle
    }),
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/album.html',
      filename: 'album.html',
      chunks: ['album']
    })
  ]
}

动态导入

动态导入的模块会被自动分包,动态导入更灵活,它可以根据代码逻辑实现是否需要导入
原理是利用import() 和 魔法注释,webpack会自动分包。
注释的格式如下,是固定的,相同的名字就会打包到一起。
在react和vue中,就可以在路由分发模块中利用动态导入实现按需加载

const render = () => {
  const hash = window.location.hash || '#posts'
  const mainElement = document.querySelector('.main')
  mainElement.innerHTML = ''
  if (hash === '#posts') {
    // mainElement.appendChild(posts())
    import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => {
      mainElement.appendChild(posts())
    })
  } else if (hash === '#album') {
    // mainElement.appendChild(album())
    import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) => {
      mainElement.appendChild(album())
    })
  }
}

render()

window.addEventListener('hashchange', render)

MiniCssExtractPlugin

提取css到单个文件

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // 'style-loader', // 将样式通过 style 标签注入
          MiniCssExtractPlugin.loader, // 个人经验超过150k再单独提取
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin()
  ]
}

OptimizeCssAssetsWebpackPlugin

压缩Css,webpack内置的压缩只能压缩JS,其他的压缩需要其他插件支持。

module.exports = {
  optimization: {
    minimizer: [
      new TerserWebpackPlugin(),
      new OptimizeCssAssetsWebpackPlugin()
    ]
  },
}

minimizer,允许你通过提供一个或多个定制过的压缩插件,覆盖内置的minimize,所以webpack建议把压缩插件写在这里而不是plugin里

输出文件名Hash

生产模式下,文件名使用Hash,解决上线后缓存的问题
webpack及大多数插件的filename都支持用占位符实现hash。

'[name].[chunkhash:8].js'

hash:hash是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值,每一次构建后生成的哈希值都不一样,即使文件内容压根没有改变。

chunkhash:它根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值,由于采用chunkhash,所以项目主入口文件main.js及其对应的依赖文件main.css由于被打包在同一个模块,所以共用相同的chunkhash。
这样就会有个问题,只要对应css或则js改变,与其关联的文件hash值也会改变,但其内容并没有改变,所以没有达到缓存意义,所以js可以用chunkhash。

contenthash:contenthash表示由文件内容产生的hash值,内容不同产生的contenthash值也不一样,css文件最好使用contenthash。

Webpack vs Gulp

Webpack vs Gulp 谁会被拍死在沙滩上

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350