前端工程化(模块化开发下2-2)

Charon

babfile## 模块化开发
前面说了gulp工作流
这边现在来讲下模块化开发
模块化开发是当下最重要的开发范式
模块化只是思想。
模块化的演变过程

  1. 文件划分的方式(原始方式完全靠约定)
    缺点:
    1.污染全局作用域
    2.命名冲突
    3.无法管理模块依赖关系

2.命名空间方式(放到一个对象,然后再统一使用)
3.然后就是AMD和CMD规范(社区逐渐提出的规范,历史本章不作为重点讲)
然后现在最后就是模块化规范的最佳实践:
node.js的CommonJS和eES Modules (官方推出)

Es Modules基本特性:

  • 自动采用严格模式,忽略"use strict"
  • 每个ESM模块都是单独的私有作用域
  • ESM是通过CORS去请求外部js模块 (需要允许CORS资源请求)
  • ESM的script标签会延迟执行脚本(不会像正常script标签立即执行)

image.png
这里导入的是一个只读性引用,所以模块外部不可更改,多个地方导入,模块内部值变化都会影响导入地方。
import 是一个 导入声明,在头部使用,不可出现在条件中
属于是静态阶段确定依赖,可以按需引入依赖项(通过webpack摇树 tree-shaking去除不需要的),动态执行,实际执行的时候去模块内取值。
动态导入import()返回一个promsie。
export {}是固定语法,不是导出对象。
和commonjs互相导入的支持情况
image.png

CommonJS特点:

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。
  • 属于动态加载,一导入会把其他模块导出的东西,整体导入

对module对象的描述
1.module.exports属性
module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。
2.exports变量
node为每一个模块提供了一个exports变量(可以说是一个对象),指向 module.exports。这相当于每个模块中都有一句这样的命令 var exports = module.exports;

这样,在对外输出时,可以在这个变量上添加方法。例如  exports.add = function (r){return Math.PI r r};注意:不能把exports直接指向一个值,这样就相当于切断了 exports 和module.exports 的关系。例如 exports=function(x){console.log(x)};
一个模块的对外接口,就是一个单一的值,不能使用exports输出,必须使用 module.exports输出。module.exports=function(x){console.log(x);};
用阮老师的话来说,这两个不好区分,那就放弃 exports,只用 module.exports 就好(手动机智)

在浏览器我们是不适合使用CommonJS规范的,动态读取时查看到这个require模块,然后发请求返回文件,这会程序会卡在那里,这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。

所以浏览器使用的是ESM,但是因为它是es6的模块,有部分浏览器还不支持image.png
在浏览器中js的ESM 的工作样子是:
image.png
通过一个script标签中的type设置为module,就可以表明它是一个模块了。
image.png
app.js的内容

基于我们现在项目不同类型文件过多,过于分散,网络请求频繁,所以我们现在设想所有的前端资源都需要模块化。
前面说的ESM和CommonJs都是对js的模块化。
我们现在需要做的是对所有资源的模块化:
image.png
image.png
image.png

  • 新特性代码的编译
  • 模块化javascript打包
  • 支持不同类型的资源模块

下面就是介绍我们的明星级产品WebPack

webpack

本质上,_webpack_ 是一个现代 JavaScript 应用程序的_静态模块打包器(module bundler)_。当 webpack 处理应用程序时,它会递归地构建一个_依赖关系图(dependency graph)_,其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 _bundle_。
image.png
从官网看也可以看出来,它是把所有的文件打包成js文件导出
webpack的配置文件默认使用CommonJs规范

什么是 webpack 模块

对比 Node.js 模块,webpack _模块_能够以各种方式表达它们的依赖关系,几个例子如下:

  • ES2015 import 语句
  • CommonJS require() 语句
  • AMD definerequire 语句
  • css/sass/less 文件中的 @import 语句
  • 样式(url(...))或 HTML 文件(<img src=...>)中的图片链接(image url)

以上这些文件的载入都是会触发webpack打包机制的,检测到对应的文件类型就会触发对应的loader去进行处理成javascript代码输出。

这样它基本对我们常用的资源文件都可以做模块化转成js处理了,它提供的功能可以完美的满足我们的需求了。(webpack !=前端工程化,前端工程化是一个概念)
下面就让我们一起来了解它,先看官方说法:

入口(entry)

webpack 应该使用哪个模块,来作为构建其内部_依赖图_的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。

每个依赖项随即被处理,最后输出到称之为 bundles 的文件中

输出(output)
output 属性告诉 webpack 在哪里输出它所创建的 _bundles_,以及如何命名这些文件,默认值为 ./dist。基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。你可以通过在配置中指定一个 output 字段

loader
loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。

插件(plugins)
loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。

模式
通过选择 developmentproduction 之中的一个,来设置 mode 参数,你可以启用相应模式下的 webpack 内置的优化
生产模式下,自动优化打包结果。
开发模式下,自动优化打包速度,添加一些调试过程中的辅助。
none模式下,运行最原始的打包不做任何额外处理。

下面就让我们深入的了解下webpack的工作机制(4):
首先我们来安装webpack和它的脚手架webpack-cli(此工具用于在命令行中运行 webpack,可以再node_modules的bin目录下查看它的.cmd文件中命令执行)
默认执行 webpack入口值是./src,然后根据package.json里的main属性指定的文件为入口文件。
出口输出文件默认值就是上面有写
从 webpack v4.0.0 开始,可以不用引入一个配置文件。
直接命令行执行webpack --mode=production 启动生产模式优化,这样输出到dist文件夹下,就完成了一个我们基本的打包。

下面主要介绍的是主流使用方式,也就是配置文件的方式。
使用yarn init生成一个项目下载webpack基本依赖:
image.png
然后开始写我们的webpack.config.js

const path = require('path')

module.exports = {
  // 这个属性有三种取值,分别是 production、development 和 none。
  // 1. 生产模式下,Webpack 会自动优化打包结果;
  // 2. 开发模式下,Webpack 会自动优化打包速度,添加一些调试过程中的辅助;
  // 3. None 模式下,Webpack 就是运行最原始的打包,不做任何额外处理;
  mode: 'none',
  entry: './src/main.js',//支持多入口,官方查看
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist')//需要写入绝对路径,借助node的path
  }
}

mian.js的代码结构

import createHeading from './heading.js'

const heading = createHeading()

document.body.append(heading)

heading.js的代码结构

export default () => {
  const element = document.createElement('h2')

  element.textContent = 'Hello world'
  element.addEventListener('click', () => {
    alert('Hello webpack')
  })

  return element
}

然后执行webpack none命令打包操作,我们看一下最后weboack的生成内容。
打开bundle.js进行代码折叠
image.png
一个立即执行函数(webpackBootstrap)引导函数,组织各模块之间的引用关系,这是webpack的入口,接收一个modules参数,然后调用时传入了一个数组,
image.png
数组里每项都是个参数列表相同的函数,这些函数里就是我们源代码中的模块代码,然后我们的模块都会被包裹在这样的函数中,从而去实现模块的私有作用域,看注释和代码可以看出下标0就是我们的main.js,然后1位置的就是heading.js。
然后再展开webpack工作入口函数(立即执行函数),
image.png
最开始是定义了一个缓存模块的对象,把加载过的模块缓存起来,下面又定义了一个载入模块的require函数,require函数的内部就是传入参数(模块对象,导出成员对象,require函数)调用我们载入的模块,最后返回模块的exports导出
然后就是require函数挂载一些工具函数
image.png
webpack工作入口最后调用了webpack_require函数,传入的id参数为0,来加载我们传入数组的第一个模块(开始加载入口模块)。
image.png
继续往下看
image.png

模块0开始调用了一个__webpack_require__.r工具函数,这个函数就是给我们模块添加一个标记,标明它是一个__esModule。然后就是再继续调用__webpack_require__函数传入id为1,这会加载的就是我们mian.js导入的heading.js了,然后根据heading导出方式export default来给__webpack_require__传入的导出成员对象赋值,完成之后跳回模块0代码也就是main.js,__webpack_require__会返回传入模块id的导出成员, 然后再去调用从模块1导出来的成员。
这样我们的所有模块就加载完了。
这就是一个大概的运行过程。

下面再来了解一下loader,webpack中loader的作用就是把其他类型的文件内容转成webpack能处理的js模块放入最后应用程序的依赖图js中(bundle)。
拿css-loader举例

css-loader

const path = require('path')

module.exports = {
  mode: 'none',
  entry: './src/main.css',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      }
    ]
  }
}

在 webpack 的配置中 loader 有两个目标:

  1. test 属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。
  2. use 属性,表示进行转换时,应该使用哪个 loader。

这俩也和我们最开始打包例子一样,会把我们loader转换的模块放到webpack引导函数调用的数组里,在我们的css文件中,使用了两个loader,css-loader是把我们编写的css转换成js模块,然后style-loader是用来把我们css-loader转换的结果通过style标签形式 放入界面中。
lader的执行也是从下到上执行,类似于函数组合的从右到左。
use数组里的loader类似一个管道,前一个执行的结果会传给后一个,最后一个执行的loader一定是返回一段js代码。
下面看一些常用的loader:

file-loader

通过file-loader来处理字体文件和图片文件,代码如下

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
这样file-loader检测到这个导入引用图片的时候会把这个图片拷贝到我们的输出目录,然后把文件路径返回。
因为本人是测试使用的 npm serve这个库测试
image.png
项目结构是这样,默认访问index.html,输出地址是dist所以资源路径需要配置一下, publicPath: 'dist/' (publicPath可以配合CDN和hash可以查看官网).

配置了publicPath后打包后文件路径就会拼接上,如下
image.png
image.png

这种file-loader的做法是物理拷贝,还有一种方式是Data Urls的方式表示文件,传统方式是服务器上有一个对应文件,我们通过请求这个地址得到这个文件,而Data Urls是当前地址可以直接表示内容的方式,也就是说这种url中的文本就已经包含了文件的内容:
image.png
image.png
image.png
image.png
image.png
使用这种url时就是发送任何的http请求,就可以根据这个url直接解析出来内容。
二进制文件会进行base64编码,使用base64来表示内容,然后在webpack中我们就借助url-loader来转换文件为Data Url的方式来展示图片。

url-loader

如下:

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: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 // 10 KB
          }
        }
      }
    ]
  }
}

上面这种配置,碰到图片文件就不会使用物理copy的方式了,就会使用Data Url方式转换为base64来进行展示。
image.png
然后我们都知道图片越大转化出来的base64就会越长,太大的图片就不适合放在代码中了,更适合做CDN静态资源。所以我们给url-loader添加一个配置limit配置文件大小,超过这个大小的就做base64得转换了。
所以总结就是:

  • 小文件使用Data Urls,减少请求次数(url-loader)
  • 大文件单独提取存放,提高加载速度(file-loader)

这样的话url-loader就必须也要下载file-loader了,因为对于超出url-loader设置大小的文件还是会调用file-loader.来进行文件copy.

webpack 的loader分类

  • 编译转换类(加载到模块转换为javascript代码)
    例如:css-loader
  • 文件操作类(把文件copy到我们的输出目录,并且把访问路径向外导出)
    例如:file-loader
  • 代码检查类(对代码进行校验)
    例如:eslint-loader

babel-loader

再说一种常用的loader,转换es6,也就是babel-loader
下载依赖 babel-loader(转换平台,配置中再具体使用特性转换插件),以及babel的核心模块@babel/core,以及所有新特性转换插件@babel/preset-env
配置:

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: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            //babel-polyfill按需引入
           presets: [['@babel/preset-env', { useBuiltIns: 'usage', corejs: 3 }]]
          }
        }
      },
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.png$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 // 10 KB
          }
        }
      }
    ]
  }
}

这样就完成了对es6的编译。
babel得优化
https://zhuanlan.zhihu.com/p/139359864
corejs3 polyfill原型方法

之前最开始说过了几种会触发webpack触发模块loader处理机制的几种情况,还有两种html中情况需要特殊处理一下,一个是图片的src一个是a标签的href需要配置html-loader
image.png
image.png
image.png

loader机制是webpack的核心
我们来实现一个自己的loader,来处理.md文件
首先创建markdown-loader.js文件来处理.md文件,结构如下
image.png
about.md中是一段文字
然后我们来编写markdown-loader.js

const marked = require('marked')

module.exports = source => {
  // console.log(source)
  // return 'console.log("hello ~")'
  const html = marked(source)
 
  // return `module.exports = "${html}"`
  // return `export default ${JSON.stringify(html)}`

  // 返回 html 字符串交给下一个 loader 处理
  return html
}

第一种操作时,遵循最后一个loader中返回js代码的规则,所以我们return 'js代码',使用JSON.stringify是因为 html中特殊字符进行转义。

另一种操作就是,我们把这段转换过后html代码返回给下一个loader惊醒处理,像之前说的,loader的处理类似管道。
webpack配置如下

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'
        ]
      }
    ]
  }
}

这段配置中我们先用markdown-loader把.md文件中的文字转换成html代码,然后再把这段代码交给html-loader进行处理。

webpack插件机制(plugins)

插件目的在于解决 loader 无法实现的其他事
由于插件可以携带参数/选项,你必须在 webpack 配置中,向 plugins 属性传入 new 实例。
我们现在记录一些常用的plugins
webpack.config.js

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

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: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 // 10 KB
          }
        }
      }
    ]
  },
  plugins: [
    new webpack.ProgressPlugin(),
    new CleanWebpackPlugin(),
    // 用于生成 index.html
    new HtmlWebpackPlugin({
      title: 'Webpack Plugin Sample',
      meta: {
        viewport: 'width=device-width'
      },
      template: './src/index.html'
    }),
    new CopyWebpackPlugin([
      // 'public/**'
      'public'
    ])
  ]
}

webpack.ProgressPlugin用来监控各个hook执行的进度percentage,输出各个hook的名称和描述。
CleanWebpackPlugin用来每次打包清空dist文件夹。
HtmlWebpackPlugin 指定使用哪个html作为模板生成,里面可以填一些配置信息。<%= htmlWebpackPlugin.options.title %> html内部这样取值.
CopyWebpackPlugin用来copy静态资源文件(文件夹)到,指定的输出目录。
因为这会我们把html生成输出到dist同级目录中了,所以publicPath就注释去掉了。
我们对插件有所了解之后,来实现一个我们自己的插件。
webpack 插件是一个具有 apply 属性的 JavaScript 对象。apply 属性会被 webpack compiler(编译) 调用,并且 compiler 对象可在整个编译生命周期访问。

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

class MyPlugin {
  apply (compiler) {
    console.log('MyPlugin 启动')

    compiler.hooks.emit.tap('MyPlugin', compilation => {
      // compilation => 可以理解为此次打包的上下文
      for (const name in compilation.assets) {
        // console.log(name)
        // console.log(compilation.assets[name].source())
        if (name.endsWith('.js')) {//模块后缀是js
          const contents = compilation.assets[name].source()//通过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'),
    // publicPath: 'dist/'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.png$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 // 10 KB
          }
        }
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    // 用于生成 index.html
    new HtmlWebpackPlugin({
      title: 'Webpack Plugin Sample',
      meta: {
        viewport: 'width=device-width'
      },
      template: './src/index.html'
    }),
    // 用于生成 about.html
    new HtmlWebpackPlugin({
      filename: 'about.html'
    }),
    new CopyWebpackPlugin([
      // 'public/**'
      'public'
    ]),
    new MyPlugin()
  ]
}

我们写的apply方法会在webpack编译期调用,compiler 对象代表了完整的 webpack 环境配置(更多信息可以查看官方文档)。我们可以通过它来获取,webpack编译期间的hooks生命周期钩子函数(具体生命周期可以看管方手册),compiler hook 的 tap 方法的第一个参数,应该是驼峰式命名的插件名称。建议为此使用一个常量,以便它可以在所有 hook 中复用。
webpack钩子
我们再edit(生成资源到 output 目录之前�触发,这是一个异步串行 AsyncSeriesHook 钩子)这个钩子中注册了一个MyPlugin的插件,然后通过compilation对象(代表了一次资源版本构建,一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。)来获取模块的信息,如果Chunk是js,通过

compilation.assets[name].source()

获取内容加工处理后,重写当前模块的 对应source,size方法。

这样我们就实现了一个我们自己的插件(更复杂的插件可以查看官方手册,这里只做一个简单的说明)。

webpack通过 --watch 工作的话不会理解结束,会监视我们的文件变化,除非我们手动借宿。

webpack-dev-server

为你提供了一个简单的 web 服务器,并且能够自动编译,实时重新加载(live reloading),提供代理url的功能。
简单例子:

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
  mode: 'development',
  entry: './src/main.js',
  output: { //默认执行webpack打包输出目录 ./dist,
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist')
  },
  devtool:'cheap-module-eval-source-map',
  devServer: {
    contentBase: './public',
    hot: true,
    // hotOnly: true // 只使用 HMR,不会 fallback 到 live reloading
    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
      }
    }
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.png$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 // 10 KB
          }
        }
      }
    ]
  },
  plugins: [
    //new CleanWebpackPlugin(),
    // 用于生成 index.html
    new HtmlWebpackPlugin({
      title: 'Webpack Tutorials',
      meta: {
        viewport: 'width=device-width'
      },
      template: './src/index.html'
    }),
    // // 开发阶段最好不要使用这个插件
    // new CopyWebpackPlugin(['public'])
    new webpack.HotModuleReplacementPlugin()
  ]
}

contentBase告诉服务器从哪里提供内容。只有在你想要提供静态文件时才需要。devServer.publicPath 将用于确定应该从哪里提供 bundle,并且此选项优先。
publicPath此路径下的打包文件可在浏览器中访问。
假设服务器运行在 http://localhost:8080 并且 output.filename 被设置为 bundle.js。默认 publicPath"/",所以你的包(bundle)可以通过 http://localhost:8080/bundle.js 访问。

hot启动热更新,注意,webpack。完全启用HMR需要使用HotModuleReplacementPlugin。如果webpack或webpack-dev-server以——hot选项启动,这个插件将自动添加,所以您可能不需要将它添加到您的webpack.config.js中,在这里是做了手动添加的。

我们在开发富文本编辑器时,js模块热更新,输入的内容也会清空掉,因为它并不能确定你的js返回了什么。
这个时候我们也可以通过module.hot来控制热更新。
例如:

import createEditor from './editor'
import background from './better.png'
import './global.css'

const editor = createEditor()
document.body.appendChild(editor)

const img = new Image()
img.src = background
document.body.appendChild(img)

// ============ 以下用于处理 HMR,与业务代码无关 ============

// console.log(createEditor)

if (module.hot) {
  let lastEditor = editor
  module.hot.accept('./editor', () => {
    // console.log('editor 模块更新了,需要这里手动处理热替换逻辑')
    // console.log(createEditor)

    const value = lastEditor.innerHTML
    document.body.removeChild(lastEditor)
    const newEditor = createEditor()
    newEditor.innerHTML = value
    document.body.appendChild(newEditor)
    lastEditor = newEditor
  })

  module.hot.accept('./better.png', () => {
    img.src = background
    console.log(background)
  })
}

正常来说我们通过module.hto控制的模块报错后,还是会走默认的热更新,所以hotOnly配置它来阻止。
其他的一些dev-server配置信息可查看:
webpack-dev-server配置
然后我们把webpack-dev-server 命令添加到npm scripts 中就可以运行了

devtool

image.png
根据需要开发阶段个人感觉:cheap-module-eval-source-map,eval-source-map会方便点,首次构建慢,重新构建还可以接受。

接下来我们做下生产环境和开发环境的区分
首先是一个公共的配置webpack.common.js

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

module.exports = {
  entry: './src/main.js',
  output: {
    filename: 'js/bundle.js'
  },
  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'
    })
  ]
}

webpack.dev.js 开发环境的配置

const webpack = require('webpack')
const merge = require('webpack-merge')
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.prod.js生产环境的配置

const merge = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const common = require('./webpack.common')

module.exports = merge(common, {
  mode: 'production',
  plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin(['public'])
  ]
})

最后给package.json添加scripts命令

 "build": "webpack --config webpack.prod.js",
 "start": "webpack-dev-server --config webpack.dev.js"

webpack优化

然后就是一些webpack优化相关的知识了。
optimization 对象
webpack不同的mode会进行不同的优化,但是这些优化也还都是可以手动配置的
webpck中mode模式的优化

module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  },
  optimization: {
    // 模块只导出被使用的成员
    usedExports: true,
    // 尽可能合并每一个模块到一个函数中
    concatenateModules: true,
    // 压缩输出结果
    // minimize: true
  }
}

也就是这样配置,我们就可以(usedExports配合minimize)只导出被使用的成员,usedExports标识导出被使用成员(导出名称会被处理做单个标记字符),然后minimize把无用代码筛除,然后压缩输出,这样就形成了摇树的功能。
waring:老的webpack版本有人碰到过使用babel后摇树无效的情况。
因为摇树要使用 ES2015模块语法,所以配置babel-loader

  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' }]
            ]
          }
        }
      }
    ]
  }

这会我们会碰到另外一个问题
例如:
image.png
image.png
index.js中引入的css和js原型方法都没有导出,只是原型做了扩展。这样我们上面的usedExports就不会标识,最后摇树 的时候就会把他们去掉,这样和我们的需求不符。

所以我们设置optimization中的sideEffects为true,然后在package.json中设置:
规则如下:

Mark the file as side-effect-free 标记副作用文件

通过指定sideEffects为false可以告诉webpack的编译器,我们的代码是纯的,可以安全地进行摇树优化。有两种方式:
1.设置sideEffects属性为false
image.png
2.若真的有副作用的文件,就在数组中指定出来,摇树时不要去除:
image.png
这里是指定那个那类文件(有副作用),就不会被摇掉,不包含里面的引用文件。

“副作用”定义为导入时执行特殊行为的代码,而不是公开一个或多个导出。比如 polyfills,它影响全局范围,通常不提供导出。
mode为production 或者设置optimize-minimize标志为true时都会进行一个摇树的优化

总结

要使用摇树优化:

  1. 使用 ES2015模块语法
  2. 确保没有编译器将 ES2015模块语法转换为 CommonJS 模块
  3. 在项目的 package.json 文件中添加“ sideEffects”属性
  4. 使用生产模式配置选项删除dead-code,达到摇树优化效果

您可以将应用程序想象为一棵树。 你实际使用的源代码和库代表了绿色的树叶,dead-code 代表着秋天里枯萎的树叶。为了摆脱枯叶,你不得不摇动树木,使它们掉下。

然后就是配置多入口和提取公共模块:
image.png
image.png
image.png
这样俩文件的公共模块就会被提取出来。

这样就会涉及到:

分离app(应用程序)和vendor(第三方库)入口

小于webpack4的版本我们 分离app和第三方代码 webpack\<4
image.png

webpack4分离公共代码
webpack4抽离公共代码方式
image.png

还有一种可以动态引入的方式就是通过es6的import()
例如:
image.png

最后一部分就是webpack生产文件的hash,js的压缩,css的压缩相关信息,如下:
image.png
最后需要了解一下runtimeChunk,属性。

设置runtimeChunk是将包含chunks 映射关系的 list单独从 app.js里提取出来,因为每一个 chunk 的 id 基本都是基于内容 hash 出来的,所以每次改动都会影响它,如果不将它提取出来的话,等于app.js每次都会改变。缓存就失效了。设置runtimeChunk之后,webpack就会生成一个个runtime~xxx.js的文件。
然后每次更改所谓的运行时代码文件时,打包构建时app.js的hash值是不会改变的。如果每次项目更新都会更改app.js的hash值,那么用户端浏览器每次都需要重新加载变化的app.js,如果项目大切优化分包没做好的话会导致第一次加载很耗时,导致用户体验变差。现在设置了runtimeChunk,就解决了这样的问题。所以这样做的目的是避免文件的频繁变更导致浏览器缓存失效,所以其是更好的利用缓存。提升用户体验。

上线优化方案

最后Rollup用来做框架会合适一点(了解)

parcel零配置前端应用打包器(了解,写demo可以使用)

后续下一篇讲下eslint和webpack的集成。

发现一遍webpack优化讲的很好,进一步优化可以参考
webpack4大结局:加入腾讯IM配置策略,实现前端工程化环境极致优化

本文内容借鉴于 拉钩大前端训练营

阅读 584

世界核平

34 声望
11 粉丝
0 条评论
你知道吗?

世界核平

34 声望
11 粉丝
宣传栏