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

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标签立即执行)

前端工程化(模块化开发下2-2)_第1张图片

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的模块,有部分浏览器还不支持前端工程化(模块化开发下2-2)_第2张图片
app.js的内容

基于我们现在项目不同类型文件过多,过于分散,网络请求频繁,所以我们现在设想所有的前端资源都需要模块化。
前面说的ESM和CommonJs都是对js的模块化。
我们现在需要做的是对所有资源的模块化:
前端工程化(模块化开发下2-2)_第3张图片

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

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

webpack

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

什么是 webpack 模块

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

以上这些文件的载入都是会触发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基本依赖:
前端工程化(模块化开发下2-2)_第5张图片
然后开始写我们的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进行代码折叠
前端工程化(模块化开发下2-2)_第6张图片

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

前端工程化(模块化开发下2-2)_第7张图片
项目结构是这样,默认访问index.html,输出地址是dist所以资源路径需要配置一下, publicPath: 'dist/' (publicPath可以配合CDN和hash可以查看官网).

配置了publicPath后打包后文件路径就会拼接上,如下
前端工程化(模块化开发下2-2)_第8张图片

这种file-loader的做法是物理拷贝,还有一种方式是Data Urls的方式表示文件,传统方式是服务器上有一个对应文件,我们通过请求这个地址得到这个文件,而Data Urls是当前地址可以直接表示内容的方式,也就是说这种url中的文本就已经包含了文件的内容:
前端工程化(模块化开发下2-2)_第9张图片
使用这种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来进行展示。
前端工程化(模块化开发下2-2)_第10张图片
然后我们都知道图片越大转化出来的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
前端工程化(模块化开发下2-2)_第11张图片

loader机制是webpack的核心
我们来实现一个自己的loader,来处理.md文件
首先创建markdown-loader.js文件来处理.md文件,结构如下
前端工程化(模块化开发下2-2)_第12张图片
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

前端工程化(模块化开发下2-2)_第13张图片
根据需要开发阶段个人感觉: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' }]
            ]
          }
        }
      }
    ]
  }

这会我们会碰到另外一个问题
例如:
前端工程化(模块化开发下2-2)_第14张图片
index.js中引入的css和js原型方法都没有导出,只是原型做了扩展。这样我们上面的usedExports就不会标识,最后摇树 的时候就会把他们去掉,这样和我们的需求不符。

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

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

通过指定sideEffects为false可以告诉webpack的编译器,我们的代码是纯的,可以安全地进行摇树优化。有两种方式:
1.设置sideEffects属性为false
前端工程化(模块化开发下2-2)_第15张图片
这里是指定那个那类文件(有副作用),就不会被摇掉,不包含里面的引用文件。

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

总结

要使用摇树优化:

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

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

然后就是配置多入口和提取公共模块:
前端工程化(模块化开发下2-2)_第16张图片
这样俩文件的公共模块就会被提取出来。

这样就会涉及到:

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

小于webpack4的版本我们 分离app和第三方代码 webpack\<4
前端工程化(模块化开发下2-2)_第17张图片

webpack4分离公共代码
webpack4抽离公共代码方式
前端工程化(模块化开发下2-2)_第18张图片

还有一种可以动态引入的方式就是通过es6的import()
例如:
前端工程化(模块化开发下2-2)_第19张图片

最后一部分就是webpack生产文件的hash,js的压缩,css的压缩相关信息,如下:
前端工程化(模块化开发下2-2)_第20张图片
最后需要了解一下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配置策略,实现前端工程化环境极致优化

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

你可能感兴趣的:(webpack,模块化,工程化)