模块化开发
当下最重要的前段开发范式,“模块化”是一种思想
模块化演变过程
早期在没有工具和规范的情况下,对模块化的落地范式
- 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:
data:text/html;charset-UTF-8,content
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 处理,然后只需要替换掉某段 就可以实现
- 我的项目没有手动处理,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"
]
}