简答题
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
学习笔记
模块化开发
当下最重要的前端开发范式,“模块化”是一种思想
模块化演变过程
早期在没有工具和规范的情况下,对模块化的落地方式
- Stage 1 - 文件划分方式
- 污染全局作用域
- 命名冲突
- 无法管理模块依赖关系
- 早期模块化完全依靠约定
- Stage 2 - 命名空间方式,每个模块只暴露一个全局对象,所有模块成员都挂载到这个对象中
- 模块成员可以被修改
- 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"
]
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。