简答题

1、Webpack 的构建流程主要有哪些环节?如果可以请尽可能详尽的描述 Webpack 打包的整个过程。

答:

(1)初始化参数
根据用户在命令窗口输入的参数以及 webpack.config.js 文件的配置,得到最后的配置。

(2)开始编译
根据上一步得到的最终配置初始化得到一个 compiler 对象,注册所有的插件 plugins,插件开始监听 webpack 构建过程的生命周期的环节(事件),不同的环节会有相应的处理,然后开始执行编译。

(3)确定入口
根据 webpack.config.js 文件中的 entry 入口,开始解析文件构建 AST 语法树,找出依赖,递归下去。

(4)编译模块
递归过程中,根据文件类型和 loader 配置,调用相应的 loader 对不同的文件做不同的转换处理,再找出该模块依赖的模块,然后递归本步骤,直到项目中依赖的所有模块都经过了本步骤的编译处理。

编译过程中,有一系列的插件在不同的环节做相应的事情,比如 UglifyPlugin 会在 loader 转换递归完对结果使用 UglifyJs 压缩覆盖之前的结果;再比如 clean-webpack-plugin ,会在结果输出之前清除 dist 目录等等。

(5)完成编译并输出
递归结束后,得到每个文件结果,包含转换后的模块以及他们之间的依赖关系,根据 entry 以及 output 等配置生成代码块 chunk。

(6)打包完成
根据 output 输出所有的 chunk 到相应的文件目录。

2、Loader 和 Plugin 有哪些不同?请描述一下开发 Loader 和 Plugin 的思路。

答:

Loader 和 Plugin 的不同点:

  • Loader 专注实现资源模块的转换和加载(编译转换代码、文件操作、代码检查)
  • Plugin 解决其他自动化工作(打包之前清除 dist 目录、拷贝静态文件、压缩代码等等)

开发 Loader 的思路:

  • 可以直接在项目根目录新建 test-loader.js (完成后也可以发布到 npm 作为独立模块使用)
  • 这个文件需要导出一个函数,这个函数就是我们的 loader 对所加载到的资源的处理过程
  • 函数输入为 加载到的资源,输出为 加工后的结果
  • 输出结果可以有两种形式:第一,输出标准的 JS 代码,让打包结果的代码能正常执行;第二,输出处理结果,交给下一个 loader 进一步处理成 JS 代码
  • 在 webpack.config.js 中使用 loader,配置 module.rules ,其中 use 除了可以使用模块名称,也可以使用模块路径

开发 Plugin 的思路:

  • plugin 是通过钩子机制实现的,我们可以在不同的事件节点上挂载不同的任务,就可以扩展一个插件
  • 插件必须是一个函数或者是一个包含 apply 方法的对象
  • 一般可以把插件定义为一个类型,在类型中定义一个 apply 方法
  • apply 方法接收一个 compiler 参数,包含了这次构建的所有配置信息,通过这个对象注册钩子函数
  • 通过 compiler.hooks.emit.tap 注册钩子函数(emit也可以为其他事件),钩子函数第一个参数为插件名称,第二个参数 compilation 为此次打包的上下文,根据 compilation.assets 就可以拿到此次打包的资源,做一些相应的逻辑处理

编程题

1、使用 Webpack 实现 Vue 项目打包任务

具体任务及说明:

先下载任务的基础代码:https://github.com/lagoufed/f...

这是一个使用 Vue CLI 创建出来的 Vue 项目基础结构

有所不同的是这里我移除掉了 vue-cli-service(包含 webpack 等工具的黑盒工具)

这里的要求就是直接使用 webpack 以及你所了解的周边工具、Loader、Plugin 还原这个项目的打包任务

尽可能的使用上所有你了解到的功能和特性

答:
参考 vue-app-base 项目,项目地址:https://github.com/luxiancan/...

# Project setup
yarn

# Compiles and hot-reloads for development
yarn serve

# Compiles and minifies for production
yarn build

# Lints files
yarn lint

# Lints and fixes files
yarn lintfix

学习笔记

模块化开发

当下最重要的前端开发范式,“模块化”是一种思想

模块化演变过程

早期在没有工具和规范的情况下,对模块化的落地方式

  1. Stage 1 - 文件划分方式
  • 污染全局作用域
  • 命名冲突
  • 无法管理模块依赖关系
  • 早期模块化完全依靠约定
  1. Stage 2 - 命名空间方式,每个模块只暴露一个全局对象,所有模块成员都挂载到这个对象中
  • 模块成员可以被修改
  1. Stage 3 - IIFE,使用立即执行函数表达式(Immediately-Invoked Function Expression)为模块提供私有空间

模块化规范的出现

模块化标准 + 模块加载器

CommonJS 规范

  • 一个文件就是一个模块
  • 每个模块都有单独的作用域
  • 通过 module.exports 导出成员
  • 通过 require 函数载入模块
  • CommonJS 是以同步模式加载模块

AMD(Asynchronous Module Definition), require.js

  • define 函数,定义一个模块
  • require 函数,载入一个模块
  • 目前绝大多数第三方库都支持 AMD 规范
  • AMD 使用起来相对复杂
  • 模块 JS 文件请求频繁

Sea.js + CMD(Common Module Definition)

  • Sea.js,淘宝团队推出的库,类似 CommonJS 规范
  • 使用上有点类似 require.js

模块化标准规范

模块化的最佳实践

  • node.js 环境中,遵循 CommonJS 规范
  • 浏览器环境中,遵循 ES Modules 规范

ES Modules 基本特性

  • 自动采用严格模式,忽略 'use strict'
  • 每个 ESM 模块都是单的私有作用域
  • ESM 是通过 CORS 去请求外部 JS 模块的
  • ESM 的 script 标签会延迟执行脚本

ES Modules 注意事项

  • export {} 这是一个固定的语法,不是 es6 中的对象简写
  • import {} 这是一个固定的语法,不是 es6 中的对象解构
  • 导出得到的是对值的引用,模块内部修改了值外部也会跟着改变
  • 导入的成员是只读的成员

ES Modules 导出和导入

  • export 注意有无 default 关键字
  • 不能省略文件后缀名,不能省略 ./
  • 执行某个模块,不需要提取模块中的成员 import './module.js'
  • 动态导入模块,可以用全局函数 import()

ES Modules in Node.js

支持情况

  • 执行文件时,node --experimental-modules index.mjs
  • ES Module 中可以导入 CommonJS 模块
  • CommonJS 中不能导入 ES Module 模块
  • CommonJS 始终只会导出一个默认成员
  • 注意 import 不是解构导出对象

与 CommonJS 模块的差异

  • ESM 中没有 CommonJS 中的那些模块全局成员了(require module exports __filename __dirname)
  • 利用 import 和 url path 模块实现
import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
console.log(__filename);

const __dirname = dirname(__filename);
console.log(__dirname);

新版本的 node 进一步支持 ESM

  • babel是基于插件机制实现的,核心模块并不会转换代码
  • 具体转换代码是通过插件来做的()
  • @babel/preset-env 是一个插件的集合,它包含了最新的JS标准中的所有新特性
  • @babel/plugin-transform-modules-commonjs 这才是一个具体的插件

Webpack 打包

打包工具解决的是前端整体的模块化,并不单指 JavaScript 模块化

webpack 工作模式

mode: 'production',
mode: 'development',
mode: 'none',

webpack 资源模块加载

  • JS file => Default Loader
  • Other file => Other Loader

webpack 导入资源模块

  • JavaScript 驱动整个前端应用
  • 在 js 中导入相关资源模块,逻辑合理,JS 确实需要这些资源文件
  • 确保上线资源不缺失,都是必要的
学习一个新事物,不是学会它的所有用法就能提高,掌握新事物的思想才是突破点。能够搞明白这些新事物为什么这样设计,那就基本上算是出道了。

webpack 文件资源加载器

  • JS file => Default Loader => Bundle.js
  • 图片、字体等资源文件 => File Loader => 文件路径 => Bundle.js

webpack URL 加载器

协议 媒体类型和编码 文件内容

data:<mediatype>,<data>

data:text/html;charset-UTF-8,<h1>content</h1>

data:image/png;base64,iVBORw0KGg...SuQmCC

最佳实践

  • 小文件使用 Data URLs,减少请求次数
  • 大文件单独提取存放,提高加载速度
{
    test: /.png$/,
    use: {
        loader: 'url-loader',
        options: {
            limit: 10 * 1024 // 10 KB
        }
    }
}
  • 超出 10KB 文件单独提取存放
  • 小于 10KB 文件转换为 Data URLS 嵌入代码中

webpack 常用加载器分类

  • 编译转换类(css-loader => 以 JS 形式工作的我 CSS 模块)
  • 文件操作类(file-loader => 导出文件访问路径)
  • 代码检查类(eslint-loader => 检查通过/不通过)

webpack 加载资源的方式

  • 遵循 ES Modules 标准的 import 声明
  • 遵循 CommonJS 标准的 require 函数
  • 遵循 AMD 标准的 define 函数和 require 函数
    • 样式代码中的 @import 执行和 url 函数
    • HTML 代码中图片标签的 src 属性

webpack 核心工作原理

Loader 机制是 Webpack 的核心

webpack 插件机制

  • Loader 专注实现资源模块加载
  • Plugin 解决其他自动化工作

Plugin 用途:

  • 打包之前清除 dist 目录
  • 拷贝静态文件至输出目录
  • 压缩输出代码

常用的插件:

  • clean-webpack-plugin 打包之前清除 dist 目录
  • html-webpack-plugin 用于生成 index.html 文件
  • copy-webpack-plugin 拷贝静态文件至输出目录

开发一个插件:插件是通过在生命周期的钩子中挂载函数实现扩展

如何增强 webpack 开发体验

  • 自动编译
  • 自动刷新浏览器
  • webpack-dev-server:继承了以上特性的工具

Source Map

  • 运行代码与源代码之间完全不同
  • 如果需要调试应用,或者运行应用过程中出现了错误,错误信息无法定位
  • 调试和报错都是基于运行代码
  • Source Map 解决了源代码与运行代码不一致所产生的问题

Source Map 的方式

webpack 支持 12 种不同的 source-map 方式,每种方式的效率和效果各不相同

不同 devtool 之间的差异

  • eval - 是否使用 eval 执行模块代码
  • cheap - Source Map 是否包含行信息
  • module - 是否能够得到 Loader 处理之前的源代码

选择合适的 Source Map

开发模式:cheap-module-eval-source-map

  • 我的代码每行不会超过 80 个字符
  • 我的代码经过 Loader 转换过后的差异较大
  • 首次打包速度慢无所谓,重新打包速度较快

生产环境:none / nosources-source-map

  • 安全隐患,source-map 会暴露源代码
  • 调试是开发阶段的事情
  • 没有绝对的选择,理解不同模式的差异,适配不同的环境

HMR 体验

HMR(Hot Module Replacement): 模块热替换

  • 应用运行过程中实时替换某个模块
  • 应用运行状态不受影响
  • 自动刷新会导致页面状态丢失
  • 热替换只将修改的模块实时替换至应用中

开启 HMR

集成在 webpack-dev-server 中

  • webpack-dev-server --hot
  • 也可以通过配置文件开启

HMR 的疑问

  • webpack 中的 HMR 并不可以开箱即用
  • webpack 中的 HMR 需要手动处理模块热替换逻辑
  • 为什么样式文件的热更新开箱即用?因为样式经过了 loader 处理,然后只需要替换掉某段 <style></style> 就可以实现
  • 我的项目没有手动处理,JS 照样可以热替换?因为使用了框架,框架下的开发,每种文件都是有规律的
  • 通过脚手架创建的项目内部都集成了 HMR 方案

总结:我们需要手动处理 JS 模块更新后的热替换

Webpack 生产环境优化

  • 生产环境跟开发环境有很大差异
  • 生产环境注重运行效率,开发环境注重开发效率
  • 模式(mode),为不同的工作环境创建不同的配置

Webpack Tree Shaking

  • 尽可能的将所有模块合并输出到一个函数中
  • 既提升了运行效率,又减少了代码体积
  • Tree Shaking 又被称为 Scope Hoisting 作用域提升

Webpack Tree Shaking 与 Babel

  • Tree Shaking 前提是 ES Modules
  • 由 Webpack 打包的代码必须使用 ESM
  • 为了转换代码中的 ECMAScript 新特性而使用 babel-loader ,就有可能导致 ESM => CommonJS,这取决我们有没有使用转换 ESM 的插件

Webpack 代码分割

代码分包

  • 所有代码最终都被打包到一起,bundle 体积过大
  • 并不是每个模块在启动时都是必要的
  • 模块打包是必要的,但是应用越来越大之后,需要进行分包,按需加载
  • 有两种方式:多入口打包;ESM 动态导入

多入口打包

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

动态导入

  • 按需加载,需要用到某个模块时,再加载这个模块
  • 可以极大地节省带宽和流量
  • 无需配置任何地方,只需要按照 ESM 动态导入的方式去导入模块,webpack 内部会自动处理分包和按需加载
  • 使用单页应用开发框架(React/Vue),在项目中的路由映射组件就可以通过动态导入实现按需加载

Webpack 魔法注释

  • 使用魔法注释可以为动态导入最终打包出来的文件命名
  • 命名相同的模块最终会被打包到一起

Webpack 输出文件名 Hash

  • 一般我们部署前端资源文件时,都会采用服务器的静态资源缓存
  • 开启缓存的问题:缓存时间过短-效果不明显,缓存过期时间较长-应用发生了更新重新部署后客户端因为缓存得不到更新
  • 解决上面问题,建议生产模式下,文件名使用 Hash,文件名不同也就是新的请求,解决了缓存的问题,服务器可以将缓存过期时间设置足够长

三种 Hash 方式

  • hash: 整个项目级别的,项目中任意一个地方改动,重新打包之后的 hash 值都会改变
  • chunkhash: chunk 级别的,同一路的打包 chunkhash 都是相同的
  • contenthash: 文件级别的hash,根据文件内容生成的hash值,不同的文件就有不同的值

解决缓存问题的最佳 hash 方式 [contenthash:8]

Rollup

Rollup 概述

  • Rollup 与 Webpack作用类似
  • Rollup 更为小巧
  • 仅仅是一款 ESM 打包器
  • Rollup 中并不支持类似 HMR 这种高级特性
  • Rollup 的初衷是提供一个充分利用 ESM 各项特性的高效打包器

Rollup 快速上手

# 安装依赖
yarn add rollup --dev

# 指定打包的入口文件、打包输出格式、输出结果路径,执行打包
yarn rollup ./src/index.js --format iife --file dist/bundle.js

Rollup 配置文件

  • 在项目根目录创建 rollup.config.js
export default {
  input: 'src/index.js',
  output: {
    file: 'dist/bundle.js',
    format: 'iife'
  }
}
  • 执行命令 yarn rollup --config 完成打包,也可以在命令最后跟上文件名

Rollup 使用插件

  • 想加载其他类型的资源模块
  • 想导入 CommonJS 模块、编译 ECMAScript 新特性
  • Rollup 支持使用插件的方式扩展,插件是 Rollup 唯一扩展途径
  • rollup-plugin-json 加载 json 文件的插件
  • rollup-plugin-node-resolve 加载 npm 模块的插件
  • rollup-plugin-commonjs 加载 CommonJS 模块

Rollup 代码拆分

使用 Dynamic Imports 动态导入实现模块按需加载,实现代码拆分/分包

rollup.config.js 修改为:

export default {
  input: 'src/index.js',
  output: {
    // file: 'dist/bundle.js',
    // format: 'iife'
    dir: 'dist',
    format: 'amd'
  } 
}

Rollup 多入口打包

  • 将 rollup.config.js 文件中的 input 改为一个数组 或者 对象
  • 对于以 amd 格式输出的文件,不能直接引入到页面上,需要配合 Require.js 这样的库使用

Rollup VS Webpack 选用原则

优点:

  • 输出结果更加扁平
  • 自动移除未引用的代码
  • 打包结果依然完全可读

缺点:

  • 加载非 ESM 的第三方模块比较复杂
  • 模块最终都被打包到一个函数中,无法实现 HMR
  • 浏览器环境中,代码拆分功能依赖 AMD

选用原则:

  • 如果我们正在开发应用程序 => webpack
  • 如果我们正在开发框架或者类库 => rollup
  • 大多数知名框架 / 库都在使用 rollup
  • 社区中希望二者共存,webpack 大而全,rollup 小而美

规范化标准

规范化标准介绍

规范化是我们践行前端工程化中重要的一部分

为什么要有规范会标准?

  • 软件开发需要多人协同
  • 不同开发者具有不同的编码习惯和喜好
  • 不同的喜好会增加项目的维护成本
  • 每个项目或者团队需要明确统一的标准

哪里需要规范化标准?

  • 代码、文档、甚至是提交日志
  • 开发过程中人为编写的成果物
  • 代码标准化规范最为重要

实施规范化的方法

  • 编码前人为的标准约定
  • 通过工具实现 Lint

常见的规范化实现方式

  • ESLint 工具使用
  • 定制 ESLint 校验规则
  • ESLint 对 TypeScript 的支持
  • ESLint 结合自动化工具或者 Webpack
  • 基于 ESLint 的衍生工具
  • StyleLint 工具的使用

ESLint 介绍

  • 最为主流的 JavaScript Lint 工具,检测 JS 代码质量
  • ESLint 很容易统一开发者的编码风格
  • ESLint 可以帮助开发者提升编码能力

ESLint 快速上手

  • 初始化项目,安装 ESLint 模块为开发依赖 npm install eslint -D
  • 编写“问题”代码,使用 eslint 执行检测 npx eslint ./01-prepare.js 加上参数 --fix 可以自动修复格式问题
  • 当代码中存在语法错误时,eslint 没法检查问题代码
  • 完成 eslint 使用配置

结合自动化工具

  • 集成之后,ESLint 一定会工作
  • 与项目统一,管理更加方便
  • 结合 gulp 使用,通过 .pipe(plugins.eslint()) 让其工作

ESLint 结合 Webpack

  • Webpack 可以通过 loader 机制实现 eslint 的检测工作
  • 安装 eslint eslint-loader
  • 在 webpack.config.js 文件配置 eslint-loader 应用在 .js 文件中
  • 安装相关插件,如:eslint-plugin-react
  • 修改 .eslintrc.js 的配置

ESLint 检查 TypeScript

  • 初始化项目
  • 安装 eslint typescript
  • 初始化 .eslintrc.js 配置文件,注意当询问 use TypeScript ? 是要选择 yes
  • 执行 npx eslint .\index.ts

Stylelint 认识

  • 提供默认的代码检查规则
  • 提供 CLI 工具,快速调用
  • 通过插件支持 Sass Less PostCSS
  • 支持 Gulp 或 Webpack 集成

快速上手

  • 安装 stylelint npm i stylelint -D
  • 安装 standard 插件 npm i stylelint-config-standard -D
  • 创建 .stylelintrc.js 配置文件,并修改 extends 字段
module.exports = {
    extends: 'stylelint-config-standard'
}
  • 执行 npx stylelint ./index.css,加上参数 --fix 可以自动修复部分格式问题
  • 检查 sass 文件,执行 npm i stylelint-config-sass-guidelines -D,修改 .stylelintrc.js 文件中的 extends 为数组,添加 sass 插件

Prettier 的使用

  • Prettier 几乎可以完成所有类型文件的格式化工作
  • 安装, npm i prettier -D
  • 检查某个文件并输出检查结果,npx prettier style.css
  • 检查并格式化某个文件,npx prettier style.css --write
  • 检查并格式化项目所有文件,npx prettier . --write

ESLint 结合 Git Hooks

Git Hooks

  • 代码提交至仓库之前未执行 lint 工作
  • 使用 lint 的目的就是保证提交到仓库的代码是没有问题的
  • 通过 Git Hooks 在代码提交前强制 lint
  • Git Hooks 也称为 git 钩子,每个钩子都对应一个任务
  • 通过 shell 脚本可以编写钩子任务触发时要具体执行的操作

快速上手

  • 很多前端开发者并不擅长使用 shell
  • Husky 可以实现 Git Hooks 的使用需求 npm i husky -D,然后在 package.json 中添加如下配置
    "husky": {
        "hooks": {
            "pre-commit": "npm run lint"
        }
    }
  • 配合 lint-stage 使用,npm i lint-staged -D
    "lint-staged": {
        "*.js*": [
            "eslint",
            "git add"
        ]
    }

lxcan
337 声望32 粉丝