工程化

定义工程化不是工具,而是一套解决问题的思想

主要解决软件开发环节中存在的效率和质量问题

典型的效率和质量问题:

  • 为兼容老平台而不得不使用老的开发技术
  • 存在许多重复手工操作(比如编译打包、刷新界面看效果、运行测试、上传部署等)
  • 团队开发人员的代码风格五花八门
  • 前端等后端给接口
  • …….等等

诸如以上问题都会造成开发效率、及产品质量的低下,最后导致开发成本的上升

做法

凡是能提高效率、降低成本、保证质量的手段、都属于工程化范畴,比如:

  • 通过制定规章制度、操作流程
  • 目前最流行的工程化思路:将一切具有重复性的工作都自动化

前端工程化

一切能提升前端的开发、测试、部署效率的方法都属于前端工程化的范畴
Webpack基础 - 图1

实施前 实施后
花费较多时间纯手工搭建新项目 一条命令即可创建新项目
为浏览器兼容性考虑而使用老版本 JS 语法 尽情享受最新版 JS 的语法特性
在 HTML 中使用 <link>
<script>
引用 css、js 文件;没有模块机制、或只有简陋的模块机制,一不小心就会出现冲突
使用 js、css 模块化机制,不用担心代码冲突问题
每次改完代码都要手动刷新页面来查看最新效果 改完代码即刻能在多种浏览器上看到最新效果
不同开发人员编写的代码风格迥异 通过代码检查工具来统一代码格式,降低潜在问题、提高代码质量
代码发布时不做文件合并和压缩等优化 根据不同环境进行打包、压缩优化、添加文件版本、并生成协助调试的 SourceMaps

工具

Webpack
  • 自身定位:资源打包器、聚焦于对“资源”的转换处理(将一种文件变成另一种文件,浏览器只认识 html、js,不认识 vue、jsx 等,所以这类文件需要转换成 html、js 文件)
  • 使用风格:声明式(以编写 js 对象的形式来描述对某种资源的处理方法)
  • 具备优点:使用简洁方便、社区庞大、插件众多

Webpack的介绍

官方对 webpack 的定位是: 模块打包器(module bundler),它将项目中依赖的各种文件视作 “模块” 来进行打包处理。

该打包处理工作主要涉及:

  • 创建、删除文件
  • 复制、移动文件
  • 修改、转换文件(是 webpack 的核心功能)

当然,webpack 也可用于非文件处理性质的任务

webpack 进行文件转换是怎么样的一个过程?

image.png

  1. 读取原文件的内容
  2. 将读取的内容组织成 js 形式的 “webpack 模块”,并记录模块间的依赖关系
  3. 对 “webpack 模块” 的内容做进一步加工处理
  4. 对加工后的内容封装定型,封装后就不再允许修改
  5. 输出这些内容到目标文件(chunk)

webpack 依靠什么完成这些文件处理工作?

要点:
webpack 默认只认识 js、json 文件,其他格式的文件,一律都需要资源加载器来进行识别和处理

Webpack基础 - 图3

image.png
每个文件都可经由一个或多个 loader 和 plugin 的处理
每个 loader 和 plugin 的功能都保持单一,保证它们的可复用性,当要完成复杂处理时可组合使用。

Webpack基础

安装

npm i -g webpack webpack-cli webpack-dev-server

查看

webapck --version

简单示例

核心内容:

  1. 编写一个简单的页面代码
  2. 编写 webpack 配置文件
  3. 运行 webpack 进行打包

    具体步骤

1、创建一个空目录、编写一个传统的页面

index.html

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>Document</title>
  8. </head>
  9. <body>
  10. <div id="app"></div>
  11. <script src="./src/util.js"></script>
  12. <script src="./src/index.js"></script>
  13. </body>
  14. </html>

src/util.js

function toUpperCase(str) {
  return str ? str.toUpperCase() : ''
}

src/index.js

const el = document.getElementById('app')
el.innerHTML = toUpperCase('hello,world')

2、改造:对 js 代码使用模块化

src/util.js

export function toUpperCase(str) {
  return str ? str.toUpperCase() : ''
}

src/index.js

import { toUpperCase } from './util'

const el = document.getElementById('app')
el.innerHTML = toUpperCase('hello,world')

3、改造:让 html 文件引用之后通过 webpack 打包产生的 js 文件
<script src="./dist/bundle.js"></script>

4、编写 webpack 配置文件

webpack.config.js

const path = require('path')

module.exports = {
  // 环境模式:开发环境 (webpack 针对不同环境会做相应的优化)
  mode: 'development',

  // 入口配置
  entry: './src/index.js',

  // 输出配置
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  }
}

5、在根目录下执行命令,生成打包结果目录 dist
# 打包命令
webpack build

# 快捷形式
webpack

# 监听模式:可实时监听文件修改,重新打包
webpack --watch

处理CSS样式

核心内容:

  1. 为页面添加 css 样式
  2. 安装并配置 css-loaderstyle-loader,处理 css 文件

具体步骤

1、在之前代码的基础上,向页面中添加一个设置了样式类的元素

src/index.js

import { toUpperCase } from './util'

const el = document.getElementById('app')
el.innerHTML = `
    <h1 class="title">
        ${toUpperCase('hello,world!!!')}
    </h1>
`

2、创建样式文件

src/assets/main.css

.title {
  color: red;
}

3、将样式文件通过 import 模块语法导入 src/index.js

说明:有别于传统方式,使用 webpack 后一般不直接通过 <link> 引入样式文件,而是将样式文件当成 ”模块“ 导入 js:

import './assets/main.css'

此时打包会出错,因为 webpack 当前不认识 css 文件:

image.png

4、安装并配置 css-loader``style-loader
npm i css-loader style-loader -D

webpack.config.js

// ...
module.exports = {
  // ...

  // 模块的处理配置
  module: {

    // 模块的匹配规则
    rules: [
      // 后缀名 为 .css 的文件都通过 css-loader 处理
      {
        // 文件名匹配规则
        test: /\.css$/,

        // 使用多个 loader 时使用数组形式(注意 loader 从右向左顺序执行)
        //css-loader 读取.css文件的内容
        //style-loader 将读取的内容以 style 表签插到 html 标签内
        use: ['style-loader','css-loader'],
      }
    ]
  }
}

上面这样使用style-loader,当有多个css文件时,无法汇合到一起

将样式文件提取到独立文件

核心内容:

安装并配置 mini-css-extract-plugin 中提供的 loaderplugin,将 css 从 js 中分离

具体步骤

1、安装并配置 mini-css-extract-plugin
npm i mini-css-extract-plugin -D

webpack.config.js

const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
  // 模式
  mode: 'development',
  // 代码入口
  entry: './src/index.js',
  // 打包结果目录
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  // 针对不同类型模块文件的处理
  module: {
    rules: [{
      // 匹配所有以.css结尾的文件
      test: /\.css$/i,
      // 多个loader的执行顺序是从后往前的
      // css-loader 是读取样式文件内容,把它放到打包后的js文件中
      // style-loader 是将读取后的内容以 style 标签的格式插入到 html 标签中
      // use: ['style-loader', 'css-loader']
      use: [MiniCssExtractPlugin.loader, 'css-loader']
    }]
  },
  plugins: [
    // MiniCssExtractPlugin 可以接受参数使css文件名变成hash值,并且css可以自动拆分文件 
    new MiniCssExtractPlugin({
      //文件名字
      filename:'[name].[contenthash].css',
      //自动拆分的文件名字//自动拆分的过程会生成 id
      chunkFilename:'[id].[contenthash].css'
    })
  ]
}

打包后可在 dist下看到独立的 css 文件

2、通过 <link>将 css 文件引入 index.html
<link rel="stylesheet" href="dist/main.css">

处理 less/sass 样式

核心内容:

  1. 编写 less/sass 文件( 以 less 为例 )
  2. 安装并配置 less-loader 及相关依赖

具体步骤

1、基于之前的代码,为页面添加一些元素
<ul class="my-list">
  <li class="list-item">项目1</li>
  <li class="list-item">项目2</li>
  <li class="list-item">项目3</li>
  <li class="list-item">项目4</li>
</ul>

2、编写less文件 src/assets/main2.less
.my-list {
  list-style: none;
  padding: 0;

  .list-item {
    border-bottom: 1px solid #ccc;
    margin-top: 10px;
  }
}

3、在 src/index.js 中导入 less 样式
import './assets/main2.less'

4、安装 less-loaderless ,并进行配置
npm i less less-loader -D

webpack.config.js 中添加针对 *.less 文件的规则:

{
  test: /\.less$/,
    use: [
      MiniCssExtractPlugin.loader, 
      'css-loader', 
      'less-loader'
    ]
}

打包后观察到 less 文件中的样式都已合并到 dist/main.css

处理图片和字体

核心内容:

  1. 通过 css-loader 处理:css 代码中 url() 引入的图片或字体
  2. 通过 webpack内置的资源模块 处理:js 代码中 import 引入的图片或字体

具体步骤:

示例1:在样式中引用图片

1、基于之前配置过 css-loader 的项目,找一些图片放入 src/assets/images/目录

2、为页面添加一个元素,并通过样式设置背景图
<div class="my-img"></div>
.my-img {
  width: 200px;
  height: 200px;
  background: url('./images/pic1.png') center center no-repeat;
  background-size: contain;
}

3、执行打包,能看到引用的图片已被放入 dist ,并自动生成了随机文件名

image.png

以上功能都是由 css-loader 完成的,它可以处理通过 url()引用的资源。

示例2:使用字体图标资源

1、下载若干字体图标,并将下载内容中的 iconfont.css 放入项目的 src/assets/fonts/目录

2、在 src/assets/main.css 中引用 iconfont.css
/* 在样式文件中引入样式文件可以使用 @import url() 指令  */
@import url('./fonts/iconfont.css');

3、在 index.html中测试字体图标
<!-- 使用字体图标 -->
<span class="iconfont icon-Apple"></span>

4、打包后访问页面,查看是否能成功看到字体图标

由于字体文件也是通过 url()来引用,因此也会经由 css-loader处理。

注意:资源文件被打包时,可能会以不同的形式被处理:

  • 文件尺寸较大时:以独立文件形式放在 dist目录中
  • 文件尺寸较小时:以 base64 字符串形式内嵌到样式文件中

示例3:在 js 中 import 图片资源

1、在 index.html 中添加元素
<img id="img2" src="" width="200" height="200" />

2、在 src/index.js 中导入图片资源,并设置到 img 元素上
// 导入一张图片
import pic2 from './assets/images/pic2.png'

// 将导入的图片设置到 img 标签上
const img2 = document.getElementById('img2')
img2.src = pic2

3、处理 import 导入的资源文件,需使用 webpack 内置的特殊 loader:[Asset Modules](https://webpack.js.org/guides/asset-modules/)

webpack.config.js 中添加规则:

// 通过 Asset Modules 处理以下后缀名的图片或字体文件
{
  test: /\.(png|jpe?g|gif|svg|eot|ttf|woff|woff2)$/i,
  type: "asset"
}

打包后能在 dist 中看到处理后的图片。

Asset Moduls 的说明】

它其实是 webpack 内置的一种专用于处理静态资源的 loader。type 属性可设置为下列值:

  • asset/resource 将资源处理成单独文件,并得到它的 URL 路径(文件较大时候会被处理成单独文件)
  • asset/inline 将资源处理成 data URI 字符串(base64位字符串,文件较小的时候会被处理成base64位)
  • asset/source 将资源处理成它的原始内容
  • asset 根据文件大小,在处理成 data URI 字符串和单独文件之间自动选择(自动选择是base64还是单独文件)

处理 es6 语法

通过 webpack 调用 Babel,实现将使用了 ES6 及更高版本语法的 JS 代码进行降级处理

需要说明的是:webpack 本身并不会对 JS 代码做语法层面的转换处理,即编写时采用什么语法,打包后仍保持该语法。如果想在打包后将高版本语法降级,可在 webpack 中使用 Babel

核心内容:

  1. 编写一些使用了 ES6+ 语法的 JS 代码
  2. 安装并配置 babel-loader 及 Babel 相关的依赖包

具体步骤

1、在 src/index.js 中编写 ES6+ 语法的代码
// 一个能返回 Promise 的函数
function asyncTask(name) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const result = `hello,${name}`
      resolve(result)
    }, 2000)
  })
}

// 通过 async/await 语法执行以上函数
async function start() {
  const result = await asyncTask('张三')
  console.log(result)
}

// 执行
start()

此时执行打包,不会产生任何错误,只不过打包后的代码还保留着 ES6+ 语法。

2、安装和配置 babel-loader及 babel 依赖包

需要安装的包和说明:

  • @babel/core - Babel 核心功能包
  • @babel/runtime - 支持 async/await 的运行时(generator-runtime)
  • @babel/preset-env - 标准预设配置
  • @babel/plugin-transform-runtime - 运行时转换插件
npm i babel-loader @babel/core @babel/preset-env @babel/runtime @babel/plugin-transform-runtime -D

3、然后在 webpack.config.js 中添加规则:
{
  // 处理 js、mjs 文件
  test: /\.m?js$/i,

    // 配置 babel-loader
    use: {
      // loader 的名称
      loader: 'babel-loader',

        // loader 的配置
        options: {
          // babel 的预设配置
          presets: ['@babel/preset-env'],
            // babel 的插件
            plugins: ['@babel/plugin-transform-runtime']
        }
    },

      // 忽略处理那些通过包管理器安装的代码目录,否则会非常缓慢
      exclude: /(node_modules|bower_components)/
}

打包后观察目标代码的语法是否已降级,且是否能成功执行该异步任务。

处理 html 文件

让 html 文件也进行打包处理,放到 dist 目录下

目前为止,项目中的 js、css、图片等文件都已通过 webpack 打包,唯独 index.html 还未经处理:

  • dist 目录下并没有将 index.html 包含进去
  • index.html 中还通过写死的 <script> 标签来引用代码文件

核心内容:

通过 html-webpack-plugin 插件,实现以指定 html 文件为模板,生成一份新的 html 并自动注入对打包后的 js、css 的引用

具体步骤

1、删除 html 文件中对 dist/main.cssdist/bundle.js 的引用

2、安装并配置插件: html-webpack-plugin
npm i html-webpack-plugin -D

3、在 webpack.config.js 中添加 plugins 插件配置:
const HtmlWebpackPlugin = require('html-webpack-plugin')
// ...
module.exports = {
  // ...
  plugins: [
    // ...
    //,面试题:webpack如何做版本锁定:使用script标签,版本固定
    new HtmlWebpackPlugin({
      // 页面模板
      template: 'index.html'
    })
  ]
}

4、打包后看到 dist 下生成了 index.html ,且已自动注入经过处理的 js 和 css:

image.png

处理在 html 文件中通过<img />标签引用的图片

虽然 webpack 处理资源的功能非常强大,但仍存在一个问题:在 HTML 中直接用<img src="..."> 来加载图片,该图片默认将不会被 webpack 处理。
但我们可以借助插件来完成该场景下的图片处理工作。

核心内容:

使用 html-withimg-loader 来解决以上问题

具体步骤

1、在页面中通过 <img> 标签引入一张图片
<img src="./src/assets/images/avatar.jpeg" alt="" width="100" height="100" />

尝试打包后,能观察到在 dist 目录下并不会包含该图片。

2、安装 html-withimg-loader 并配置
npm i html-withimg-loader -D

3、在 webpack.config.js 中添加规则:
{
  test: /\.(htm|html)$/i,
  loader: 'html-withimg-loader'
}

打包后能看到引用图片已被打包到 dist 中。

使用开发服务器(热更新)

借助 webpack 提供的本地开发服务器,实现修改代码后自动实时预览最新效果的功能

核心内容:

开发服务器模块 webpack-dev-server 的安装和配置

具体步骤

1、安装

如果你的 webpack 是全局安装的,那最好 webpack-dev-server 也全局安装

npm i -g webpack-dev-server

2、配置
// ...
module.exports = {
  // ...

  // 开发服务器
  devServer: {
    // 资源目录
    static: {
      directory: path.join(__dirname, 'dist')
    },

    // 服务器端口
    port: 9000,
  }
}

3、启动
webpack serve 
或者
webapck serve --open

成功启动后,将自动打开浏览器访问首页。

当修改了项目代码后,页面也会自动更新。(注意:修改 index.html 后并不会自动更新,需手动刷新)

生成调试文件

在打包时生成用于辅助调试代码的 sourcemap 文件

在开发过程中,难免遇到需要调试代码的时候。
浏览器支持读取 Source Map 文件来解析打包后的代码,还原出原始代码的样子,这对调试非常有帮助。

核心内容:

通过 webpack 配置项[devtool](https://webpack.js.org/configuration/devtool/#devtool) 生成 Source Map文件

具体步骤

1、在 webpack.config.js 中添加 devtool 配置
module.exports = {
  devtool: 'source-map',
  // ...
}

打包后即可在 dist 中找到以 .map 为后缀的 Source Map 文件。

清理打包文件

每当开始新一次打包时,先删除上一次打包所生成的所有文件,防止无用文件残留

目前问题:
默认情况下,每次执行打包时不会删除 dist 中的文件,只会对同名文件进行覆盖。这导致 dist 中可能残留无用的文件,让打包后的代码变得越来越臃肿。

解决方案:
每次打包前先自动删除 dist目录下的所有文件。

核心内容:

安装并配置 clean-webpack-plugin 插件,实现在打包前的清理 dist 目录

具体步骤

1、安装clean-webpack-plugin
npm i clean-webpack-plugin -D

2、配置webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
// ...

module.exports = {
    // ...
    // 插件
    plugins: [
        // 生成 clean-webpack-plugin 实例
        new CleanWebpackPlugin()
    ]
}

现在执行打包,就会先删除 dist 下的所有文件后再进行打包。

(提示:可在 dist 放一些额外的文件来辅助测试观察)

打包多页面应用

使用 webpack 打包拥有多个 html 入口页面的前端项目

核心内容:

  1. 编写一个简单的多页应用
  2. 通过配置多个 entryHtmlWebpackPlugin 实例,完成多 html 项目的打包

具体步骤

1、新建项目并添加三个 html 文件
  • home.html
  • product.html
  • about.html

2、新建 src目录并添加三个 js 文件
  • src/home.js
  • src/product.js
  • src/about.js

3、新建 webpack 配置文件

webpack.config.js

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

module.exports = {

  mode: 'development',

  entry: {
    // 多个入口,即打成三个独立的 js 包
    index: './src/home.js',
    product: './src/product.js',
    introduce: './src/about.js'
  },

  output: {
    path: path.join(__dirname, 'dist'),
    // 为最终生成的入口 js 设置不同文件名
    filename: '[name].bundle.js'
  },

  plugins: [
    // 生成首页 html
    new HtmlWebpackPlugin({
      template: 'home.html',
      filename: 'home.html',
      // 在页面中注入指定 entry 名的 js 文件
      chunks: ['home']
    }),

    // 生成产品页 html
    new HtmlWebpackPlugin({
      template: 'product.html',
      filename: 'product.html',
      chunks: ['product']
    }),

    // 生成介绍页 html
    new HtmlWebpackPlugin({
      template: 'about.html',
      filename: 'about.html',
      chunks: ['about']
    })
  ]
}

打包后看到 dist 中生成了三个 html 文件,且都已注入对应的 js 文件。