前端工程化
前端工程化
模块化
背景:
在实际开发时,经常会遇到变量名称
或函数名称
一样的情况。这不仅容易造成命名冲突,还会污染全局变量。
基于此,JS
也引入模块化
的概念:
- 早期的模块化不是真正的模块化,
立即调用函数表达式
(简称IIFE
)就是一个在定义时可立即执行的函数
var result = (function () {
var name = "Barry";
return name;
})();
// IIFE 执行后返回的结果:
result; // "Barry"
- 后期的模块化才算是真正:目前
Web开发
倾向于ESM
,Node开发
倾向于CJS
。 - 在实际开发中,一个模块就是一个文件。
同步加载
包括IIFE
与CJS
,异步加载
包括ESM
。浏览器可兼容IIFE
,服务器可兼容CJS
,浏览器与服务器都兼容ESM
。
- | CJS | ESM |
---|---|---|
语法类型 | 动态 | 静态 |
关键声明 | require |
export 与import |
加载方式 | 运行时加载 | 编译时加载 |
加载行为 | 同步加载 | 异步加载 |
书写位置 | 任何位置 | 顶层位置 |
指针指向 | this 指向当前模块 |
this 指向undefined |
执行顺序 | 首次引用时加载模块 再次引用时读取缓存 |
引用时生成只读引用 执行时才是正式取值 |
属性引用 | 基本类型属于复制不共享 引用类型属于浅拷贝且共享 |
所有类型属于动态只读引用 |
属性改动 | 工作空间可修改引用的值 | 工作空间不可修改引用的值 但可通过引用的方法修改 |
-
运行时加载指整体加载模块生成一个对象,再从对象中获取所需的属性方法去加载。最大特性是
全部加载
,只有运行时才能得到该对象,无法在编译时做静态优化。 -
编译时加载指直接从模块中获取所需的属性方法去加载。最大特性是
按需加载
,在编译时就完成模块加载,效率比其他方案高,无法引用模块本身(本身不是对象
),但可拓展JS
高级语法(宏与类型校验
)。 -
使用
type
指定模块方案- 在
package.json
中指定type
为commonjs
,则使用CJS
- 在
package.json
中指定type
为module
,则使用ESM
- 在
mjs文件
使用ESM
解析,cjs文件
使用CJS
解析,js文件
使用基于package.json
指定的type
解析(type=commonjs
使用CJS
,type=module
使用ESM
)。
主流浏览器都能通过<script type=”module”>
标签支持ECMAScript 模块 (ES modules) 。
ESM
不再提供Node
某些特性与不能灵活引用json文件
了,因此__dirname
、__filename
、require
、module
和exports
这几个特性将无法使用。
__filename
与__dirname
可用import.meta
对象重建require
、module
和exports
可用import
与export
代替json文件
的引用可用Fs模块
的readFileSync
与JSON.parse()
代替
CJS
的循环依赖关系已通过缓存各个模块的module.exports
对象解决,但ESM
用了所谓的绑定。简而言之,ESM
模块不会导出导入值而是引用值。
- 导入引用模块可访问该引用但无法修改它。
- 导出引用模块可为引用该模块的模块重新分配值且该值由导入引用模块使用
四个babel
子包
- @babel/cli:提供支持
@babel/core
的命令运行环境 - @babel/core:提供转译函数
- @babel/node:提供支持
ESM
的命令运行环境 - @babel/preset-env:提供预设语法转换集成环境
Stylelint、Eslint
Lint
其实就是编辑器中运行的一个脚本进程,将代码解析为抽象语法树
,遍历抽象语法树
并通过预设规则做一些判断与改动,再将新的抽象语法树
转换为正确代码。
Tslint官方
已宣布废弃tslint
,改用eslint
代替其所有校验功能eslint
部分配置与prettier
部分配置存在冲突且互相影响,为了保障格式化性能就放弃接入prettier
所以VSCode
只需安装Stylelint
与Eslint
两个插件。
# stylelint及其依赖
npm i stylelint stylelint-config-standard stylelint-order postcss-html postcss-scss postcss-less
# eslint及其依赖
npm i @babel/core @babel/eslint-parser @babel/preset-react eslint eslint-config-standard eslint-plugin-html eslint-plugin-import eslint-plugin-n eslint-plugin-promise eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-vue vue-eslint-parser
# typescript-eslint及其依赖
npm i -D @typescript-eslint/eslint-plugin @typescript-eslint/parser typescript eslint-config-standard-with-typescript
安装完毕需配置多份文件,
- CSS类型有
html/css/scss/less/vue文件
, - JS类型有
html/js/ts/jsx/tsx/vue文件
。 - 查看插件文档,
Stylelint
的配置文件可同时校验html/css/scss/less/vue文件
, Eslint
需配置不同文件分别校验html/js/ts/jsx/tsx/vue文件
。- 两个插件可在
settings.json
中通过指定字段覆盖默认配置。 - 配置文件中的
rules
可根据自身编码规范
与编码风格
适当调整 - 配置Stylelint可查看Stylelint规则
配置Eslint可查看Eslint规则
配置TypeScriptEslint可查看TypeScriptEslint规则
配置VueEslint可查看VueEslint规则
Webpack
缩减范围
-
配置include/exclude缩小Loader对文件的搜索范围,好处是
避免不必要的转译
-
include/exclude
通常在各大Loader
中配置,src
文件夹通常作为源码目录
export default {
// ...
module: {
rules: [{
exclude: /node_modules/,
include: /src/,
test: /\.js$/,
use: "babel-loader"
}]
}
};
缓存副本
- 配置cache缓存Loader对文件的编译副本,好处是
再次编译时只编译变动的文件
- 很多
Loader/Plugin
都会提供一个可用编译缓存的选项,通常包括cache
字眼
import EslintPlugin from "eslint-webpack-plugin";
export default {
// ...
module: {
rules: [{
// ...
test: /\.js$/,
use: [{
loader: "babel-loader",
options: { cacheDirectory: true }
}]
}]
},
plugins: [
// ...
new EslintPlugin({ cache: true })
]
};
定向搜索
-
配置resolve提高文件的搜索速度,好处是
定向指定所需文件路径
。 -
若某些第三方库以默认形式引用可能报错或希望程序自动索引指定类型文件都可通过该方式解决
-
alias
表示映射路径,extensions
表示文件后缀,noParse
表示过滤无依赖文件。 -
通常配置
alias
与extensions
就足够。
export default {
// ...
resolve: {
alias: {
"#": AbsPath(""), // 根目录快捷方式
"@": AbsPath("src"), // src文件夹快捷方式
swiper: "swiper/js/swiper.min.js"
}, // 导入模块快捷方式
extensions: [".js", ".ts", ".jsx", ".tsx", ".json", ".vue"] // 导入模块省略后缀
}
};
摇树优化
删除项目中未被引用代码,好处是删除重复代码与未使用代码
摇树优化
只对ESM
生效,对其他模块规范
失效。摇树优化
针对静态结构分析,只有import/export
才能提供静态的导入/导出
功能,因此在编写业务代码时必须使用ESM
才能让摇树优化
删除重复代码与未使用代码。
在webpack
中只需将打包环境设置为生产环境
就能让摇树优化
生效,同时业务代码使用ESM
编写,使用import
导入模块,使用export
导出模块。
export default {
// ...
mode: "production"
};
按需加载
将路由页面/触发性功能单独打包为一个文件,使用时才加载,好处是减轻首屏渲染的负担
。因为项目功能越多其打包体积越大,导致首屏渲染速度越慢。
首屏渲染时只需首页的JS代码
而无需其他网页的JS代码
,所以可用按需加载
实现。webpack v4+
提供模块按需切割加载功能,配合import()
可做到首屏渲染减包的效果,以加快首屏渲染速度。只有当触发某些功能时才会加载当前功能的JS代码
。
const Login = () => import( /* webpackChunkName: "login" */ "../../views/login");
const Logon = () => import( /* webpackChunkName: "logon" */ "../../views/logon");
作用提升
分析模块间依赖关系,把打包好的模块合并到一个函数中,好处是减少函数声明与内存花销
。
在未开启作用提升
前,构建好的代码会存在大量函数闭包。因为模块依赖,通过webpack
打包后会转换为IIFE
,大量函数闭包包裹代码会导致打包体积增大,模块越多越明显。在运行代码时创建的函数作用域变多,导致更大的内存开销。
在开启作用提升
后,构建好的代码会根据引用顺序放到一个函数作用域中,通过适当重命名某些变量以防止变量名冲突,以减少函数声明与内存花销。
在webpack
中只需将打包环境设置为生产环境
就能让作用提升
生效,或显式设置concatenateModules
。
export default {
// ...
mode: "production"
};
// 显式设置
export default {
// ...
optimization: {
// ...
concatenateModules: true
}
};
压缩资源
压缩HTML/CSS/JS代码,压缩字体/图像/音频/视频,好处是更有效减少打包体积
。极致地优化代码都有可能不及优化一个资源文件的体积更有效。
针对HTML代码,使用html-webpack-plugin开启压缩功能。
import HtmlPlugin from "html-webpack-plugin";
export default {
// ...
plugins: [
// ...
HtmlPlugin({
// ...
minify: {
collapseWhitespace: true,
removeComments: true
} // 压缩HTML
})
]
};
单元测试
expect与test
单元测试
有两个很重要的概念函数,分别是expect()
与test()
。expect()
表示期望得到的运行结果,简称期望结果;test()
表示测试结果是否通过预期,简称通过状态。
Learn
lerna
显式地改变项目结构,把多个子包合并为一个仓库,然后使用packages
文件夹存放每个子包,以使用一个仓库管理多个子包。
掌用命令
命令 | 功能 | 描述 |
---|---|---|
lerna init |
初始项目 | |
lerna boostrap |
安装依赖 | 自动解决子包间的依赖关系 子包内部互相依赖会使用软链接处理 |
lerna clean |
卸载依赖 | 只能卸载每个子包的node_modules 不能卸载根目录的node_modules |
lerna create <name> |
新增子包 | 在packages 文件夹中创建由lerna 管理的子包 |
lerna add <pkg> |
安装模块 | 为所有子包安装模块 可通过--scope=pkg 安装指定子包模块 |
lerna run <script> |
执行命令 | 为所有子包执行命令 可通过--scope=pkg 执行指定子包命令 |
lerna version |
标记版本 | 标记存在修改行为的子包 |
lerna publish [bump] |
发布子包 | 发布全部private 不为true 的子包 |