前言

模块化开发确实很好的解决了我们在开发当中的一些代码组织问题,但是我们所使用的ESModules存在环境兼容问题,其次模块文件过多,网络请求过于频繁,我们所有的前端资源都需要模块化,比如我们的html,css也需要模块化同样会面临着相同的问题,我们要解决的问题:

  1. 将我们开发阶段使用的es6代码转换成es5代码 —- 新特性代码编译
  2. 将我们散装的模块文件打包到一个文件里面 —- 模块化javascript打包
  3. 需要支持不同种类的前端资源类型,比如.css .png都当作模块去使用,这样以来就有了统一的模块管理方案了 —- 支持不同类型的资源模块

前端模块打包工具

比较主流的前端模块打包工具有 webpack,parcel,RollUp

webpack介绍

webpack很好的解决了我们前言中要解决的一些问题,它可以把一些零散的文件打包到同一个文件中,对于打包过程中有环境兼容问题的代码,我们就可以通过模块加载器(loader)对其进行编译转换,其次webpack还带有代码拆分(code spliting)的能力,它将所有的代码都按照我们的需要去打包,这样的话就避免了由于把所有代码打包到一起会使得文件过大的问题,webpack还支持引入任意类型的文件,我们直接可以通过import引入css等文件,打包工具解决的是前端整体的模块化,并不是只是javascript模块化,让我们更好的享受模块化带来的优势,也不必担心生产环境的兼容性问题。

webpack快速上手

webpack之前的模块化代码:

  1. 使用了es6提供的模块话方式的代码

image.png
image.png

  1. 在index.html中引入模块化代码

image.png

  1. 在终端中执行 serve,在浏览器中就可以看到我们想要的结果了

image.png
image.png
使用了webpack的代码:

  1. 我们首先使用yarn init —yes 初始化一个package.json文件,在安装一下webpack 和 webpack-cli

image.png

  1. 通过执行 yarn webpack,它会自动找到我们src目录下的文件进行打包,打包到dist目录下面,dist目录下面的main.js就是我们打包好的文件

image.png

  1. 我们修改一下index.html文件,在浏览器中看到正常输出

image.png

webpack配置文件

webpack配置文件在webpack.config.js中配置

  1. const path = require('path')
  2. module.exports={
  3. entry:'./src/index.js',//入口文件路径
  4. output:{
  5. filename:'bundle.js',//指定打包后的文件名
  6. path:path.join(__dirname,'temp'),//指定打包的路径名,这里要用到绝对路径所以使用的node中的path模块
  7. }
  8. }

webpack 工作模式

webpack新增了一个工作模式的用法,这种用法大大的降低了webpack的复杂程度,可以理解为针对于不同环境的几组预设的配置,webpack一共有三种工作模式,production,development,none
当我们在不指定工作模式的情况下,我们的webpack会默认使用production工作模式,在这个模式下面,webpack会自动启动一些优化插件,例如代码压缩,
image.png
我们可以通过 —mode来切换我们想要的命令,比如 yarn webpack —mode development 切换到开发模式,开发模式会加快我们的打包的速度,添加一些调试过程中需要的辅助,通过 yarn webpack —mode none切换到none模式,这个模式是最原始状态的打包,webpack没有去做任何额外的处理,
我们可以在配置文件中添加mode属性来指定我们的工作模式:

  1. const path = require('path')
  2. module.exports={
  3. mode:'development',
  4. entry:'./src/index.js',//入口文件路径
  5. output:{
  6. filename:'bundle.js',//指定打包后的文件名
  7. path:path.join(__dirname,'temp'),//指定打包的路径名,这里要用到绝对路径所以使用的node中的path模块
  8. }
  9. }

资源模块加载 loader

loader是webpack的核心特性,通过不同的loader我们可以加载任何类型的资源
以加载css类型的资源为例:

  1. 首先安装 yarn add css-loader —dev,css-loader的作用就是把我们的css文件转换为js模块,在src文件夹下新建一个main.css文件

    1. //mian.css
    2. body{
    3. background:yellow;
    4. font-size: 20px;
    5. }
  2. 在webpack.config.js中配置

    1. //webpack.config.js
    2. const path = require('path')
    3. module.exports={
    4. mode:'development',
    5. entry:'./src/main.css',//入口文件路径
    6. output:{
    7. filename:'bundle.js',//指定打包后的文件名
    8. path:path.join(__dirname,'dist'),//指定打包的路径名,这里要用到绝对路径所以使用的node中的path模块
    9. },
    10. module:{
    11. rules:[
    12. {
    13. test:/.css$/,
    14. use:'css-loader'
    15. }
    16. ]
    17. }
    18. }
  3. 打开浏览器以后,我们发现我们写的样式并没有挂载到body标签上面,这时我们还需要一个style-loader,它的作用就是把我们打包好的css模块通过style标签的形式追加页面上,通过安装yarn add style-loader —dev

修改配置文件webpack.config.js

  1. //webpack.config.js
  2. const path = require('path')
  3. module.exports={
  4. mode:'development',
  5. entry:'./src/main.css',//入口文件路径
  6. output:{
  7. filename:'bundle.js',//指定打包后的文件名
  8. path:path.join(__dirname,'dist'),//指定打包的路径名,这里要用到绝对路径所以使用的node中的path模块
  9. },
  10. module:{
  11. rules:[
  12. {
  13. test:/.css$/,
  14. use:['style-loader','css-loader']
  15. 如果这里我们配置了多个loader,这里的执行顺序是从后往前执行,因为我们要先执行css-loader,
  16. 再执行style-loader,所以我们要把css-loader放在后面
  17. }
  18. ]
  19. }
  20. }

文件资源加载器

css-loader styles-loader用来处理css文件,那我们的图片字体文件就是要用到文件资源加载器file-loader ,通过yarn add file-loader —dev 后在webpack.config.js中配置如下:

  1. const path = require('path')
  2. module.exports={
  3. mode:'development',
  4. entry:'./src/main.css',//入口文件路径
  5. output:{
  6. filename:'bundle.js',//指定打包后的文件名
  7. path:path.join(__dirname,'dist'),//指定打包的路径名,这里要用到绝对路径所以使用的node中的path模块
  8. + publicPath:'dist/' 当我们想要浏览器访问dist目录下面的静态资源目录时我们可以添加这个属性,dist目录为我们打包好的存放图片的目录,dist后面的斜杠不可以省略,
  9. },
  10. module:{
  11. rules:[
  12. {
  13. test:/.css$/,
  14. use:['style-loader','css-loader']
  15. 如果这里我们配置了多个loader,这里的执行顺序是从后往前执行,因为我们要先执行css-loader,
  16. 再执行style-loader,所以我们要把css-loader放在后面
  17. },
  18. {
  19. + test:/.png$/,
  20. + use:'file-loader'
  21. }
  22. ]
  23. }
  24. }

文件加载器的工作过程:

  1. webpack在打包时遇到了我们的图片文件,然后根据我们配置文件当中的配置,找到对应的文件加载器
  2. 文件加载器将我们导入的文件copy到输出的目录
  3. 然后将我们copy文件的输出路径作为当前模块的返回值返回,这样我们的应用需要的资源就发布出来了,这样我们的模块导出成员就可以拿到路径

url加载器

我们对 图片的处理也可以使用url-loader
image.png
使用url-loader打包之后并没有把匹配到的文件复制一份到打包之后的文件夹中,但是页面还是可以正常显示这些图片,控制台可以发现,这里其实是将这些文件转化为base64,这样做的好处是减少这些文件的http请求;但是这样做又会带来新的问题,如果这个图片特比大,那么生成的js文件也会特别的大,那么就会增加加载js的时间;
image.png
通过file-loader和url-loader的对比总结出:

  1. 小文件使用url-loaders,减少请求次数
  2. 大文件单独提取存放,提高加载速度

为了让file-loader和url-loader很好的配合,我们可以在url-loader配置的地方增加一个options属性,在options属性中增加一个limit属性,用来限制图片的大小,具体配置:

  1. const path = require('path')
  2. module.exports={
  3. entry:'./src/index.js',//入口文件路径
  4. output:{
  5. filename:'bundle.js',//指定打包后的文件名
  6. path:path.join(__dirname,'dist'),//指定打包的路径名,这里要用到绝对路径所以使用的node中的path模块
  7. },
  8. module:{
  9. rules:[
  10. {
  11. test:/.css$/,
  12. use:['style-loader','css-loader']
  13. },
  14. {
  15. test:/.png$/,
  16. use:{
  17. loader:'url-loader',
  18. options:{
  19. limit:10*1024 //10kb
  20. esModule:false //file-loader在新版本中esModule默认为true,因此手动设置为false
  21. }
  22. },
  23. }
  24. ]
  25. }
  26. }

注意:我们上图中的配置必须要安装file-loader的情况下才成立,超出限制大小默认去找file-loader加载器,如果我们没有安装file-loader的话,当文件超出限制大小会报出一个找不到file-loader的错误

常用的加载器分类

1.编译转换类加载器

这种加载器会将我们的资源模块转换为javascript代码,例如我们用到的css-loader,它是将我们的css代码转换为了bundle.js中的一个javascript模块,然后通过javascript代码来运行我们的css代码,

2.文件操作类型的加载器

这种加载器会把我们加载到的资源模块copy到输出的目录,同时将这个文件的访问路径向外导出,例如我们用到的file-loader

3. 代码质量检查的加载器

这种加载器对我们写好的代码进行校验的加载器,这种类型的加载器是为了统一我们的代码风格,提高我们的代码质量,

webpack与es6、bable-loader记载器

由于我们的webpack默认就能处理我们代码中的import和export ,所以很多人会认为webpack会自动编译我们的es6代码,其实并不是这样的,webpack仅仅是对模块完成打包工作,因为打包的需要它才会对我们的export和import做一些相应的转换,但是它并不能转换我们代码中的其他的es6的代码
下面举例:

  1. //在我们的入口文件中写入
  2. export const fun = () =>{
  3. return 'my name is xuke'
  4. }

打包后的bundle.js
image.png
我们发现webpack并没有帮我们的箭头函数做转换,这时我们要借助我们的babel-loader来给我的代码做转换
yarn add babel-loader @babel/core @babel/preset-env —dev 由于@babel/core @babel/preset-env是我们的babel所依赖的两个模块
安装完成后在webpack.config.js中module配置加入一条配置

  1. {
  2. test:/.js$/,
  3. use:{
  4. loader:'babel-loader',
  5. options:{
  6. presets:['@babel/preset-env']
  7. }
  8. }
  9. },

最后需要强调一下:webpack只是打包工具,不会去处理es6等新特性,如果需要处理新特性,我们可以通过加载器来编译转换代码。

webpack 核心工作原理

在我们的前端项目当中会散落着各种各样的代码集资源文件,webpack会根据我们的配置找到一个文件作为我们的入口文件,一般情况下这个文件都会是一个js文件,然后会顺着我们的代码导入导出的文件解析推断出这个文件所依赖的资源模块,分别解析每一个模块对应的依赖,最后会形成一个依赖树,webpack会遍历递归这个依赖树,找到每个节点对应的资源文件,然后再根据我们配置文件当中的rules属性,找到对应的加载器,加载对应的资源模块,最后把加载到的结果放入到bundle.js中,从而实现整个项目的打包,其中 loader机制是webpack的核心 没有loader的话webpack就没有办法实现各种资源文件的加载,对于webpack来说它是一个打包或者合并代码的工具
image.png

自己开发一个loader

需求是自己开发一个 markdown-loader 的加载器

  1. 首先在src目录下面新建一个入口文件 main.js 和md文件 about.md
    1. //main.js
    2. import about from './about.md'
    3. console.log(about)
    ```javascript //about.md

    我是谁

  2. 我是许可 ```

  3. 然后新建一个 markdowm-loader.js 作为自己写的加载器 ```javascript //markdowm-loader.js

module.exports = source =>{ console.log(source) return ‘hello~’ }

  1. 3. webpack.config.js 配置我们写好的loader
  2. ```javascript
  3. const path = require('path')
  4. module.exports = {
  5. mode:'none',
  6. entry:'./src/main.js',
  7. output:{
  8. filename:'bundle.js',
  9. path:path.join(__dirname,'dist')
  10. },
  11. module:{
  12. rules:[
  13. {
  14. test:/.md$/,
  15. use:'./markdown-loader'
  16. }
  17. ]
  18. }
  19. }

在控制台中执行 yarn webpack ,会出现下图的信息,可以看出我们的md文件中的内容是可以正常打印出来的,下面报出了一个错误:意思是我们的 loader 返回的内容必须是一个 javascript 代码,
image.png

  1. 我们把返回的 hello 改成 console.log(‘hello~’) ```javascript //markdowm-loader.js

module.exports = source =>{ console.log(source) return ‘console.log(“hello~”)’ }

  1. 再次执行 yarn webpack,控制台可以正常输出,我们的 bundle.js 中也是可以正常显示出返回的结果<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/2135644/1598152243368-e4e7e775-d76a-4a2f-886a-dc48de0aba1f.png#align=left&display=inline&height=471&margin=%5Bobject%20Object%5D&name=image.png&originHeight=942&originWidth=1207&size=133151&status=done&style=none&width=603.5)
  2. 5. 第一种解析方法:我们使用 marked 来解析我们的 markdown 文件,先安装 yarn add marked --dev,修改我们的 markdown-loader.js 文件
  3. ```javascript
  4. const marked = require('marked')
  5. module.exports = source =>{
  6. const html = marked(source)
  7. // return `module.exports = ${JSON.stringify(html)}`
  8. 或者
  9. return `export default ${JSON.stringify(html)}`
  10. 这里我们用到 JSON.stringify 是因为如果我们直接使用html的话,html中的换行符和引号有可能会造成语法上的错误
  11. 所以这里我们把它转换成字符串进行拼接
  12. }

再次执行yarn webpack 会正常执行,在bundle.js中会看到我们marked转换后的结果
image.png

  1. 第二种解析方法:我们安装一个 html-loader 让它来帮我们解析 yarn add html-loader —dev

修改 markdown-loader.js

  1. const marked = require('marked')
  2. module.exports = source =>{
  3. const html = marked(source)
  4. // return `module.exports = ${JSON.stringify(html)}`
  5. // return `export default ${JSON.stringify(html)}`
  6. return html
  7. }

修改 webpack.config.js

  1. module:{
  2. rules:[
  3. {
  4. test:/.md$/,
  5. use:['html-loader','./markdown-loader']
  6. 注意:这里的加载器是从右向左加载的 所以html-loader写前面 后加载
  7. }
  8. ]
  9. }

再次执行 yarn webpack ,bundle.js中正常输出
image.png
总解:从上面的自己封装的饿loader中可以看出,loader 内部的工作原理很简单,loader负责资源文件输入到输出的转换,loader 其实是一个管道的概念,对于同一个资源可以依次使用多个 loader,

webpack 插件机制

webpack 的插件机制是为了增强 webpack 的自动化能力,loader 主要专注实现项目当中资源模块的加载,从而实现整体项目的打包,plugin 主要解决项目中其他的自动化工作,比如实现打包之前自动清除 dist 目录,拷贝静态文件到输出目录,压缩我们输出的代码,总之 webpack + plugin 实现大多前端工程化工作,

webpack 自动清除目录的插件

在之前的webpack打包体验中,我们会发现我们每次打包时,webpack都会覆盖我们的dist目录中的同名的文件,之前的其余文件还是依旧存在,其实之前的文件我们并不需要了,这时候我们就会用到 webpack 中自动清除目录的插件 clean-webpack-plugin

  1. 首先安装 yarn add clean-webpack-plugin —dev
  2. 在 webpack.config.js 中配置

    1. const path = require('path')
    2. const {CleanWebpackPlugin} = require('clean-webpack-plugin')
    3. module.exports={
    4. mode:'none',
    5. entry:'./src/index.js',//入口文件路径
    6. output:{
    7. filename:'bundle.js',//指定打包后的文件名
    8. path:path.join(__dirname,'dist'),//指定打包的路径名,这里要用到绝对路径所以使用的node中的path模块
    9. publicPath: 'dist/'
    10. },
    11. module:{
    12. rules:[...]
    13. },
    14. plugins:[ //plugins 是webpack中配置插件的地方
    15. //绝大多数插件导出的是一个类型,我们创建一个插件的实例
    16. new CleanWebpackPlugin()
    17. ]
    18. }

    自动生成html插件

    我们的html都是通过硬编码的方式单独存放在项目的根目录下,这种方式会存在两个问题,一个就是我们发布的时候要同时发布src下面的html文件和dist目录下面的所有的打包结果,而且我们还要保证html页面的路径引用都是正确的,这样的话会相对麻烦,第二个就是如果我们输出的目录发生了变化,那么html中引入的js路径也需要我们手动去修改它,解决这个方法是我们通过 webpack 输出html文件,这些配置和html文件都是通过配置注入进来的,不需要我们硬编码,我们借助 html-webpack-plugin 插件

  3. yarn add html-webpack-plugin —dev 安装插件

  4. 在 webpack.config.js 中配置 ```javascript const path = require(‘path’)
  • const HtmlWebpackPlugin = require(‘html-webpack-plugin’) module.exports = { mode:’none’, entry:’./src/main.js’, output:{
    1. filename:'bundle.js',
    2. path:path.join(__dirname,'dist')
    }, module:{
    1. rules:[
    2. ...
    3. ]
    }, plugins:[
  • new HtmlWebpackPlugin() ] }
    1. 执行 yarn webpack 命令,在dist目录下面会自动生成一个index.html文件<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/2135644/1598166147075-67ed2d52-b33a-43fb-8e40-20e5b2d8323b.png#align=left&display=inline&height=265&margin=%5Bobject%20Object%5D&name=image.png&originHeight=530&originWidth=1353&size=87650&status=done&style=none&width=676.5)<br />这时生成的index.html文件的标题等信息都是写死的,我们可以在 HtmlWebpackPlugin() 中去配置它<br />配置如下:
    2. ```javascript
    3. plugins:[
    4. new HtmlWebpackPlugin({
    5. title:'my name is xuke',
    6. meta:{
    7. viewport:'width=device-width'
    8. }
    9. })
    10. ]
    再次执行yarn webpack,dist目录下生成的index.html就是我们配置好的信息
    image.png
    如果我们相对这个html文件进行大量的自定义的话,更好的做法就是在源代码当中添加一个去 生成html的模板,然后让 html-webpack-plugin 根据这个模板生成页面 ,我们可以在 src 目录下面新建一个html模板,
    image.png
    修改我们的 webpack.config.js 文件
    1. plugins:[
    2. new HtmlWebpackPlugin({
    3. title:'my name is xuke',
    4. meta:{
    5. viewport:'width=device-width'
    6. },
    7. template:'./src/index.html'
    8. })
    9. ]
    执行 yarn webpack ,在dist目录下面生成了 index.html 就是我们自定义的模板文件中内容
    image.png
    有时我们页面需要同时输出多个文件,修改我们的 webpack.config.js 文件
    1. plugins:[
    2. new HtmlWebpackPlugin({
    3. title:'my name is xuke',
    4. meta:{
    5. viewport:'width=device-width'
    6. },
    7. template:'./src/index.html'
    8. }),
    9. //用于配置生成其他的html文件
    10. new HtmlWebpackPlugin({
    11. filename:'about.html'
    12. })
    13. ]

    自己开发一个插件

    相比于loader,plugin拥有更广的能力范围,plugin通过钩子机制实现,webpack要求我们的插件必须是一个函数或者是一个包含apply方法的对象,我们可以把这个插件定义一个类型,这个类型中定义一个apply方法,我们在这里写一个移除bundle.js中注释webpack插件的一个方法,我们在 webpack.config.js 中定义自己的插件 ```javascript const path = require(‘path’) const HtmlWebpackPlugin = require(‘html-webpack-plugin’)

class MyPlugin { apply(compiler){ compiler.hooks.emit.tap(‘my plugin’,compilation =>{ for(const name in compilation.assets){ 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 } } } }) } }

module.exports = { mode:’none’, entry:’./src/main.js’, output:{ filename:’bundle.js’, path:path.join(__dirname,’dist’) }, module:{ rules:[ … ] }, plugins:[ … new MyPlugin() ] }

  1. 执行 yarn webpack 命令生成的bundle.js中就没了注释,插件是通过往webpack声明周期里面的钩子函数里面挂载我们的任务函数来去实现的,
  2. <a name="1FTd8"></a>
  3. #### webpack dev server
  4. 集成了自动刷新浏览器和自动编译等功能,通过 yarn add webpack-dev-server 安装,然后通过yarn webpack-dev-server 就可以完成项目的启动了,并且我们修改项目中的东西还会引起浏览器的自动刷新,这一点跟gulpwatch模式很像,webpack-dev-server为了提高我们的工作效率,并没有将打包结果写入到磁盘当中,它是将打包结果暂时存放在内存当中,而httpserver就是在内存当中把这些文件读取出来,发送给浏览器,这样会减少很多不必要的磁盘读写操作,从而大大的提高我们的构建效率,<br />通过在 webpack.config.js 中配置contentBase 来为webpack-dev-server 额外指定一个指定查找静态资源目录,
  5. ```javascript
  6. const path = require('path')
  7. const HtmlWebpackPlugin = require('html-webpack-plugin')
  8. class MyPlugin {
  9. apply(compiler){
  10. compiler.hooks.emit.tap('my plugin',compilation =>{
  11. for(const name in compilation.assets){
  12. if(name.endsWith('.js')){
  13. const contents = compilation.assets[name].source()
  14. const withoutComments = contents.replace(/\/\*\*+\*\//g,'')
  15. compilation.assets[name] = {
  16. source:()=>withoutComments,
  17. size:()=>withoutComments.length
  18. }
  19. }
  20. }
  21. })
  22. }
  23. }
  24. module.exports = {
  25. mode:'none',
  26. entry:'./src/main.js',
  27. output:{
  28. filename:'bundle.js',
  29. path:path.join(__dirname,'dist')
  30. },
  31. + devServer:{
  32. + contentBase:'./public'
  33. + },
  34. module:{
  35. rules:[
  36. ...
  37. ]
  38. },
  39. plugins:[
  40. ...
  41. ]
  42. }

webpack-dev-server 代理api —proxy

我们在日常的开发过程中总会遇到跨域的问题,比如我们要在本地访问 https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json
我们在 devServer中添加 proxy属性

  1. devServer:{
  2. contentBase:'./public',
  3. proxy:{
  4. '/api':{
  5. target:'https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json',
  6. pathRewrite:{ //表示重写路径,将以api开头的路径替换为空
  7. '^/api':''
  8. },
  9. changeOrigin:true
  10. }
  11. }
  12. },

我们 yarn webpack-dev-server之后 在浏览器中请求localhost:8080/api 会访问到这个json中的数据

Source Map

通过构建编译之类的操作,我们可以将开发阶段的源代码转换为能够在生产环境当中运行的代码,这意味着我们的开发环境中的代码和我们生成环境中的代码有很大的差异,这种情况如果我们需要去调试我们的应用,那么我们将无法定位错误地方在哪,因为调试和报错都是基于运行代码(转换过后的代码)运行的,source map(源代码地图)就是用来解决这个问题的,它来映射我们运行代码和源代码之间的关系,我们通过 source map 文件逆向得到我们的源代码,sourcemap主要解决了源代码和运行代码不一致所产生的问题,

  1. 在webpack中配置sourcemap,添加devtool属性(配置我们开发过程当中的辅助工具) ```javascript const path = require(‘path’) const HtmlWebpackPlugin = require(‘html-webpack-plugin’)

module.exports = { mode:’none’, entry:’./src/main.js’, output:{ filename:’bundle.js’, path:path.join(__dirname,’dist’) },

  • devtool:’source-map’,//配置我们开发过程中的辅助工具 module:{
    1. rules:[
    2. ...
    3. ]
    }, plugins:[ … ] } ``` sourcemap模式:
    image.png
  1. source map模式的选择

在开发环境下会选择 cheap-module-eval-source-map,原因

  • 自己的代码风格每一行不超过80个字符(sourcemap 帮忙定位到行就够了,能够找到相应的位置)
  • 代码经过loader转换后差异较大,
  • 虽然cheap-module-eval-source-map首次打包速度慢无所谓,后面重写打包相对较快

在生产环境中会选择none,原因:

  1. sourcemap会暴露源代码
  2. 调试是开发阶段的事情,开发阶段就应该把问题解决完,而不应该带到生产环境中

webpack HMR体验

HMR(hot module replacement)为模块热替换,在计算机行业我们经常听到一个叫做热拔插的名词,它指的是在一个运行的机器上随时插拔设备,运行状态不会受插拔设备的影响,而且插上的设备可以立即开始工作,例如usb端口,热替换和热拔插是一个道理,他们都是在运行过程中的及时变化,webpack中的模块热替换指的就是应用运行过程中替换某一个模块,我们的运行状态不会收到影响,

使用

HMR已经集成在了webpack-dev-server中,我们不需要安装其他得插件了直接可以使用,我们通过 webpack-dev-server —hot去开启这个特性,也可以通过配置文件开启,在webpack.config.js中配置

  1. const path = require('path')
  2. const HtmlWebpackPlugin = require('html-webpack-plugin')
  3. + const webpack = require('webpack')
  4. module.exports = {
  5. mode:'none',
  6. entry:'./src/main.js',
  7. output:{
  8. filename:'bundle.js',
  9. path:path.join(__dirname,'dist')
  10. },
  11. devtool:'source-map',//配置我们开发过程中的辅助工具
  12. devServer:{
  13. + hot:true,
  14. },
  15. module:{
  16. ...
  17. },
  18. plugins:[
  19. + new webpack.HotModuleReplacementPlugin()
  20. ]
  21. }

我们按照上面得配置配置完后,我们通过 yarn webpack-dev-server 启动浏览器,我们改样式后发现页面会自动更新,但是我们修改js页面却没有自动刷新,这是因为style是一个统一得模块比较好区别,js比较杂乱,不同的模块有不同的逻辑,webpack不好找到统一得规则去更新他,我们必须通过其他得方法实现js得热更新替换

js热更新初体验

在src下面新建一个main.js文件,并引入 create.js

  1. //main.js
  2. import create from './create.js'
  3. console.log(create.name)
  4. module.hot.accept('./create.js',()=>{
  5. console.log("create更新拉")
  6. })
  7. //create.js
  8. export default {
  9. name:'需咳咳咳'
  10. }

这时当我们修改了create.js中得内容得时候,浏览器会自动更新我们得js代码
图片的热替换:

  1. //main.js
  2. import create from './create.js'
  3. import background from './11.png'
  4. const img = new Image()
  5. console.log(create.name)
  6. // js的热替换
  7. module.hot.accept('./create.js',()=>{
  8. console.log("create更新拉")
  9. })
  10. // 图片的热替换
  11. module.hot.accept('./11.png',()=>{
  12. console.log("create更新拉")
  13. img.src = background
  14. console.log(background)
  15. })

webpack 不同环境下的配置

  • 配置文件根据环境不同导出不同的配置
  • 一个环境对应一个配置文件

第一种 :配置文件根据环境不同导出不同的配置
实现思路:

  1. 我们先把 webpack.config.js中的配置抽取出来写到一个 config 变量中
  2. module.exports 中可以导出一个函数,函数中接收两个参数,一个是env(代表我们传递的环境名参数,以何种模式进行开发),一个是argv(指的是我们运行cli过程中传递的所有参数)
  3. 我们根据这两个参数进行判断 ```javascript const path = require(‘path’) const HtmlWebpackPlugin = require(‘html-webpack-plugin’) const webpack = require(‘webpack’) const { CleanWebpackPlugin } = require(‘clean-webpack-plugin’) const CopyWebpackPlugin = require(‘copy-webpack-plugin’) class MyPlugin { apply(compiler) {
    1. compiler.hooks.emit.tap('my plugin', compilation => {
    2. for (const name in compilation.assets) {
    3. if (name.endsWith('.js')) {
    4. const contents = compilation.assets[name].source()
    5. const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
    6. compilation.assets[name] = {
    7. source: () => withoutComments,
    8. size: () => withoutComments.length
    9. }
    10. }
    11. }
    12. })
    } } // 把module.exports中的配置东西抽取出来 const config = { mode: ‘none’, entry: ‘./src/main.js’, output: {
    1. filename: 'bundle.js',
    2. path: path.join(__dirname, 'dist')
    }, devtool: ‘source-map’,//配置我们开发过程中的辅助工具 devServer: {
    1. hot: true,
    2. contentBase: './public',
    3. proxy: {
    4. '/api': {
    5. target: 'https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json',
    6. pathRewrite: { //表示重写路径,将以api开头的路径替换为空
    7. '^/api': ''
    8. },
    9. changeOrigin: true
    10. }
    11. }
    }, module: {
    1. rules: [
    2. {
    3. test: /.md$/,
    4. use: ['html-loader', './markdown-loader']
    5. }
    6. ]
    }, plugins: [
    1. new HtmlWebpackPlugin({
    2. title: 'my name is xuke',
    3. meta: {
    4. viewport: 'width=device-width'
    5. },
    6. template: './src/index.html'
    7. }),
    8. //用于配置生成其他的html文件
    9. new HtmlWebpackPlugin({
    10. filename: 'about.html'
    11. }),
    12. new MyPlugin(),
    13. new webpack.HotModuleReplacementPlugin()
    ] }

module.exports = (env, argv) => { // env 代表我们传递的环境名参数,以何种模式进行开发 // argv 指的是我们运行cli过程中传递的所有参数 if (env === ‘production’) { // 如果是生成环境 config.mode = ‘production’ config.devtool = false config.plugins = [ …config.plugins, //添加两个开发阶段可以省略的插件 new CleanWebpackPlugin(), new CopyWebpackPlugin({ patterns: [ { from: path.join(__dirname, ‘public’), to: ‘public’ } ], }), ] } return config }

  1. 执行 yarn webpack 命令,会自动按照开发模式的配置打包<br />执行 yarn webpack --env production 命令,会按照我们的生产环境去打包
  2. 第二种:一个环境对应一个配置文件<br />根据环境名参数返回不同的对象这种方式只适用于中小型项目,一旦我们的项目变的复杂,我们的配置文件也会变得复杂起来,对于大型的项目,比较推荐不同的环境配置不同的配置文件,这种方式的话一般我们的项目会有三个配置文件,其中两个用来适配不同的环境,另外一个是一个公共的配置,用来抽取两个文件中相同的配置
  3. 1. webpack.config.js 改为webpack.common.js 作为公共配置文件
  4. 1. 新建一个 webpack.prod.js(生产环境的配置) webpack.dev.js (开发环境的配置)
  5. 1. 在生产环境中写入下面配置
  6. ```javascript
  7. // webpack.common.js
  8. const path = require('path')
  9. const HtmlWebpackPlugin = require('html-webpack-plugin')
  10. const webpack = require('webpack')
  11. class MyPlugin {
  12. apply(compiler) {
  13. compiler.hooks.emit.tap('my plugin', compilation => {
  14. for (const name in compilation.assets) {
  15. if (name.endsWith('.js')) {
  16. const contents = compilation.assets[name].source()
  17. const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
  18. compilation.assets[name] = {
  19. source: () => withoutComments,
  20. size: () => withoutComments.length
  21. }
  22. }
  23. }
  24. })
  25. }
  26. }
  27. module.exports = {
  28. mode: 'none',
  29. entry: './src/main.js',
  30. output: {
  31. filename: 'bundle.js',
  32. path: path.join(__dirname, 'dist')
  33. },
  34. devtool: 'source-map',//配置我们开发过程中的辅助工具
  35. devServer: {
  36. hot: true,
  37. contentBase: './public',
  38. proxy: {
  39. '/api': {
  40. target: 'https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json',
  41. pathRewrite: { //表示重写路径,将以api开头的路径替换为空
  42. '^/api': ''
  43. },
  44. changeOrigin: true
  45. }
  46. }
  47. },
  48. module: {
  49. rules: [
  50. {
  51. test: /.md$/,
  52. use: ['html-loader', './markdown-loader']
  53. }
  54. ]
  55. },
  56. plugins: [
  57. new HtmlWebpackPlugin({
  58. title: 'my name is xuke',
  59. meta: {
  60. viewport: 'width=device-width'
  61. },
  62. template: './src/index.html'
  63. }),
  64. //用于配置生成其他的html文件
  65. new HtmlWebpackPlugin({
  66. filename: 'about.html'
  67. }),
  68. new MyPlugin(),
  69. new webpack.HotModuleReplacementPlugin()
  70. ]
  71. }
  72. // webpack.prod.js
  73. const common = require('./webpack.common.js')
  74. const { CleanWebpackPlugin } = require('clean-webpack-plugin')
  75. const CopyWebpackPlugin = require('copy-webpack-plugin')
  76. const {merge} = require('webpack-merge')
  77. const path = require('path')
  78. module.exports = merge(common,{ // webpack-merge 插件用来合并common中的代码
  79. mode :'production',
  80. devtool: false,
  81. plugins : [
  82. new CleanWebpackPlugin(),
  83. new CopyWebpackPlugin({
  84. patterns: [
  85. {
  86. from: path.join(__dirname, 'public'),
  87. to: 'public'
  88. }
  89. ],
  90. }),
  91. ]
  92. })
  93. // webpack.dev.js
  94. const common = require('./webpack.common.js')
  95. module.exports = common

执行 yarn webpack —config webpack.prod.js 会出现打包为生产环境的 dist 目录
执行 yarn webpack —config webpack.dev.js 会出现打包为开发环境的 dist 目录

  1. 我们还可以修改 package.json 文件 让我们执行的命令简化
    1. "scripts": {
    2. "build":"webpack --config webpack.prod.js",
    3. "dev":"webpack --config webpack.dev.js"
    4. },
    直接执行 yarn build 或者 yarn dev 可以生成相应环境的代码

    webpack DefinePlugin

    defineplugin 是为我们的代码注入全局成员的,来「启用/禁用」「生产/开发」构建中的功能。在production模式下,这个插件会默认启用起来,并且像我们的代码中注入了 process.env.NODE_ENV 常量,很多的第三方的模块都是根据这个成员判断当前的环境,来执行像打印日志这样一类的操作,使用: ```javascript const common = require(‘./webpack.common.js’) const {merge} = require(‘webpack-merge’) const webpack = require(‘webpack’)

module.exports = merge(common,{ plugins:[ new webpack.DefinePlugin({ //对象当中的每一个键值都会被注入到我们的代码当中 API_BASE_URL:’”https://api.example.com"'// 我们的api服务器地址 因为这个插件直接执行文本替换,给定的值必须包含字符串本身内的实际引号。 通常,有两种方式来达到这个效果,使用 ‘“production”‘, 或者使用 JSON.stringify(‘production’) }) ] })

  1. <a name="R9l5v"></a>
  2. #### webpack Tree Shaking
  3. 1. tree shaking 用来处理项目中 未被引用的,多余的,冗余的代码,在生产模式下自动启用
  4. 1. tree shaking不是指 webpack 中某一个配置选项,他是一组功能搭配使用后的优化效果,
  5. 1. tree shaking的实现必须依赖我们的 esmodules 模块
  6. 1. 在开发模式下,或者none模式下,手动开启的方法
  7. ```javascript
  8. const path = require('path');
  9. module.exports = {
  10. entry: './src/index.js',
  11. output: {
  12. filename: 'bundle.js',
  13. path: path.resolve(__dirname, 'dist'),
  14. },
  15. + mode: 'development',
  16. + optimization: {
  17. + usedExports: true,// 表示只导出那些外部使用过的成员,并标记未使用的成员
  18. + minimize:true, // 表示去除未使用的成员
  19. + },
  20. };

webpack 合并模块

concatenateModules这个属性用来表示合并模块,表示尽可能的将所有的函数合并到一个函数中,这样既提升了运行效率,又减少了代码的体积,这个特性又被成为 scope hoisting (作用域提升),
webpack.config.js 中配置如下:

  1. const path = require('path');
  2. module.exports = {
  3. entry: './src/index.js',
  4. output: {
  5. filename: 'bundle.js',
  6. path: path.resolve(__dirname, 'dist'),
  7. },
  8. mode: 'development',
  9. optimization: {
  10. usedExports: true,// 表示只导出那些外部使用过的成员,并标记未使用的成员
  11. minimize:true, // 表示去除未使用的成员
  12. + concatenateModules:true // 表示尽可能的将所有的函数合并到一个函数中,这样既提升了运行效率,又减少了代码的体积,
  13. + //这个特性又被成为 scope hoisting (作用域提升),
  14. },
  15. };

webpack setEffects

配置后 webpack 打包会标识我们的代码是否有副作用,从而为treeshaking提供更大的压缩空间,副作用是指模块执行时除了导出模块之外所作出的事情,它一般用于npm包标记是否有副作用,
使用:

  1. 在 webpack.config.js 中给 optimization 属性添加 sideEffects:true,这个属性在 production 模式下自动开启 ```javascript const path = require(‘path’);

module.exports = { entry: ‘./src/index.js’, output: { filename: ‘bundle.js’, path: path.resolve(__dirname, ‘dist’), }, mode: ‘development’, optimization: {

  • sideEffects:true, usedExports: true,// 表示只导出那些外部使用过的成员,并标记未使用的成员 minimize:true, // 表示去除未使用的成员 concatenateModules:true // 表示尽可能的将所有的函数合并到一个函数中,这样既提升了运行效率,又减少了代码的体积,
    1. //这个特性又被成为 scope hoisting (作用域提升),
    }, }; ```
  1. 在package.json 中添加 “sideEffects”:false 来表示我们的代码是没有副作用的代码,这时候webpack.config.js 中的 sideEffects 就可以发挥作用了,移除掉我们代码中的副作用代码
    1. // package.json
    2. {
    3. ...
    4. "sideEffects":false
    5. }

    code splitting

    webpack打包会把我们项目中的所有代码都打包到一起,这时会存在一个弊端,当项目过大,由于我们的代码要运行在浏览器端,会影响运行的效率和浪费流量和带宽,并不是每一个模块在启动时都是由必要的,正确的做法是把我们的打包结果分离到多个bundle中,按需加载,实现分包的方式一共有两种:
    1. 多入口打包 根据我们的业务配置不同的打包入口,可输出多个打包结果
    2. 采用 esmodules 的动态导入的功能来实现模块的按需加载

1. 多入口打包

多入口打包使用于多页面应用程序,最常见的划分规则是一个页面对应一个打包入口,公共部分单独提取,