web构建cli
web 构建 cli…
模块化
.mjs 是什么文件呢?很明显因为 ESM 和 CJS 的加载方式不同,为了更高的区分这两种不同的加载方式,于是创建了 .mjs(Module JavaScript).mjs 就是表示当前文件用 ESM 的方式进行加载,如果是普通的 .js 文件,则采用 CJS 的方式加载。
发展历程
- IIFE: 使用自执行函数来编写模块化,特点:在一个单独的函数作用域中执行代码,避免变量冲突。
- AMD: 使用 requireJS 来编写模块化,特点:依赖必须提前声明好。
- CMD: 使用 seaJS 来编写模块化,特点:支持动态引入依赖文件。
- CommonJS: nodejs 中自带的模块化。
- UMD:兼容 AMD,CommonJS 模块化语法。
- webpack(require.ensure):webpack 2.x 版本中的代码分割。
- ES Modules: ES6 引入的模块化,支持 import 来引入另一个 js 。
SystemJS 是一个通用的模块加载器,它能在浏览器或者 NodeJS 上动态加载模块,并且支持 CommonJS、AMD、全局模块对象和 ES6 模块。通过使用插件,它不仅可以加载 JavaScript,还可以加载 CoffeeScript 和 TypeScript。
js 各种模块写法
- UMD
其是 amd 和 commonjs 的统一规范,支持两种规范,即写一套代码,可用于多种场景。其用法如下:
前后端通用
与 CJS 或 AMD 不同,UMD 更像是一种配置多个模块系统的模式。
UMD 在使用诸如 Rollup/ Webpack 之类的 bundler 时通常用作备用模块
UMD 希望提供一个前后端跨平台的解决方案(支持 AMD 与 CommonJS 模块方式)
代码结构:
- 先判断是否支持 Node.js 模块格式(exports 是否存在),存在则使用 Node.js 模块格式
- 再判断是否支持 AMD(define 是否存在),存在则使用 AMD 方式加载模块
- 前两个都不存在,则将模块公开到全局(window 或 global)
1 | (function (root, factory) { |
- AMD
amd(Asynchronous Module Definitions)规范因 RequireJS 而出名,其适用于浏览器端
commonjs 常被用于 node 中,当然其也可通过 Browserify 用于浏览器中。
异步加载
其被提出,主要用于客户端 browser
语法不直观,没有 commonjs 便于书写
1 | // 文件名: foo.js |
- CJS(commonjs)
语法类似于 node,因为 node 使用 commonjs 规范
commonjs 导入模块是同步导入
主要用于后端,客户端用的话需要通过 Browserify
书写直观
1 | // 暴露公共方法(一个) |
- ESM(ES6 Modules)
- esm 的浏览器写法
1 | <script type="module" src="./index.js"> |
1 | // 导出方式 |
- 原生 ES 导入不支持下面这样的裸模块导入
1 | import { someMethod } from "my-dep"; |
ESM 和 CommonJs 的差异
语法上面,CommonJS 模块使用 require()加载和 module.exports 输出,ES6 模块使用 import 和 export。
用法上面,require()是同步加载,后面的代码必须等待这个命令执行完,才会执行。import 命令则是异步加载,或者更准确地说,ES6 模块有一个独立的静态解析阶段,依赖关系的分析是在那个阶段完成的,最底层的模块第一个执行。
esm 和 common 混用
- CommonJS 模块加载 ES6 模块
1 | (async () => { |
require()不支持 ES6 模块的一个原因是,它是同步加载,而 ES6 模块内部可以使用顶层 await 命令,导致无法被同步加载。
顶层 await 是 es2021 的新特性,目前只有 chrome 浏览器高于 89 版本才支持(2021 年后),及 node15+默认支持无需携带试验参数。在低版本使用还是需要借助 babel 帮忙的。
- ES6 模块加载 CommonJS 模块
1 | // 正确 |
ES6 模块需要支持静态代码分析,而 CommonJS 模块的输出接口是 module.exports,是一个对象,无法被静态分析,所以只能整体加载。
- package.json 指定入口
1 | "exports":{ |
在 package.json 文件的 exports 字段,指明两种格式模块各自的加载入口
esm 的循环引用问题
esm 的 import 命令是在编译阶段就执行,优先于自身内容执行。
esm 并不关心是否存在循环引用,只是生成一个指向被加载模块的引用,代码未执行时,这个引用的值就是 undefined。
tree-shaking
在使用 Tree shaking 的时候,有一个需要特别注意的地方,就是“副作用(side effects)”。
有些模块的代码可能会在导入时执行一些副作用,例如改变全局变量、改变导入模块的状态等。这种情况下,即使模块中的部分导出没有被使用,由于其副作用,也不能被 Tree shaking 移除。否则,可能会导致程序运行出错。
webpack
魔法注释
1 | /* webpackChunkName: " " */ |
webpack require.ensure
/
1 | import Vue from "vue"; |
require.ensure 可实现按需加载资源,包括 js,css 等。他会给里面 require 的文件单独打包,不会和主文件打包在一起。 有四个入参
- 第一个参数是数组,表明第二个参数里需要依赖的模块,这些会提前加载。
- 第二个是回调函数,在这个回调函数里面 require 的文件会被单独打包成一个 chunk,不会和主文件打包在一起,这样就生成了两个 chunk,第一次加载时只加载主文件。
- 第三个参数是错误回调。
- 第四个参数是单独打包的 chunk 的文件名
import 和 require 的比较(了解)
import 是解构过程并且是编译时执行
require 是赋值过程并且是运行时才执行,也就是异步加载
require 的性能相对于 import 稍低,因为 require 是在运行时才引入模块并且还赋值给某个变量
webpack require.context
webpack 构建期间运行的 function,可以实现自动化导入
1 | require.context(directory, useSubdirectories, regExp); |
1 | const requireApi = require.context(".", true, /\.js$/); |
vue-cli webpack 解析
1 | node --inspect-brk=9229 node_modules/@vue/cli-service/bin/vue-cli-service.js build |
过程
解析依赖,loader 处理相应文件
loader 里面配 include,让 loader 只针对特殊资源。另一种办法是让你的第三方库 noparse
vue-loader 入手
file-loader 和 url-loader
hook 在 babel-loader 和 vue-loader 之前
webpack 的 loader 和 plugin 的区别
基于 tapable 的插件架构,扩展性强,众多的 loader 或者 plugin 让 webpack 稍显复杂。
在你编写自定义 webpack 扩展前你需要想明白到底是要做一个 loader 还是 plugin 呢?可以这样判断:
如果你的扩展是想对一个个单独的文件进行转换那么就编写 loader 剩下的都是 plugin。
其中对文件进行转换可以是像:
babel-loader 把 es6 转换成 es5
file-loader 把文件替换成对应的 URL
raw-loader 注入文本文件内容到代码里去
编写 webpack loader
请好好利用 loader-utils 包,它提供了很多有用的工具,最常用的一个就是获取传入 loader 的 options。除了 loader-utils 之外包还有 schema-utils 包,我们可以用 schema-utils 提供的工具,获取用于校验 options 的 JSON Schema 常量,从而校验 loader options
我们在函数中处理 source 的转化,最终返回处理好的值。当然返回值的数量和返回方式依据 a-loader 的需求来定。一般情况下可以通过 return 返回一个值,也就是转化后的值。如果需要返回多个参数,则须调用 this.callback(err, values…) 来返回。在异步 loader 中你可以通过抛错来处理异常情况。Webpack 建议我们返回 1 至 2 个参数,第一个参数是转化后的 source,可以是 string 或 buffer。第二个参数可选,是用来当作 SourceMap 的对象。
webpack 规定 use 数组中 loader 的执行顺序是从最后一个到第一个
- 顺序最后的 loader 第一个被调用,它拿到的参数是 source 的内容
- 顺序第一的 loader 最后被调用, webpack 期望它返回 JS 代码,source map 如前面所说是可选的返回值。
- 夹在中间的 loader 被链式调用,他们拿到上个 loader 的返回值,为下一个 loader 提供输入。
1 | module: { |
1 | var Minimize = require("minimize"); |
编写 webpack plugin
1 | class EndWebpackPlugin { |
loader 的入口需要导出一个 class, 在 new EndWebpackPlugin()的时候通过构造函数传入这个插件需要的参数,在 webpack 启动的时候会先实例化 plugin 再调用 plugin 的 apply 方法,插件需要在 apply 函数里监听 webpack 生命周期里的事件,做相应的处理。
webpack plugin 里有 2 个核心概念:
- Compiler: 从 webpack 启动到推出只存在一个 Compiler,Compiler 存放着 webpack 配置
- Compilation: 由于 webpack 的监听文件变化自动编译机制,Compilation 代表一次编译。
Compiler 和 Compilation 都会广播一系列事件。
webpack 生命周期里有非常多的事件可以在event-hooks和Compilation里查到。以上只是一个最简单的 demo,更复杂的可以查看 how to write a plugin或参考web-webpack-plugin。
vue-cli3 下的 webpack
1 | 用法:vue-cli-service build [options] [entry|pattern] |
配置输出
1 | vue inspect>output.js |
构建包分析
1 | vue-cli-service build --report |
vue-cli3 下的 webpack 打包优化
webpack.DllPlugin 方式
- 配置 webpack.dll.js 针对第三方库打包
- vue.config.js 中配置 plugin
- html 中引入 dll 打包出来的 js 文件。(一般采用部署 CDN 的方式)
webpack 拆包
SplitChunksPlugin
chunks:可以自定义 chunk 输出分组。设置 test 对模块进行过滤,符合条件的模块分配到相同的组
cacheGroups:可以自定义 chunk 输出分组。设置 test 对模块进行过滤,符合条件的模块分配到相同的组
maxInitialRequests(入口的最大并行请求数) 和 maxAsyncRequests(异步请求中的最大并发请求数)
CommonsChunkPlugin
TurboPack
vite
vite 介绍
1 | import { createServer, ViteDevServer } from "vite"; |
vite 只是应用于开发、构建、阶段,打包依赖于 rollup 工具
vite 解析实现过程如下:
开发服务器使用 http + connect 构建服务
通过多种中间件处理请求资源;也会遍历 html 节点,将元素的 src 属性添加上 base 前缀,将内联脚本转换为网络请求
会通过 es-module-lexer 这个库分析出所有 import 语句
vite 会在请求后添加 query 参数的方式,用来标识当前请求的处理逻辑
vitest
- toBe 和 toEqual 和 toStrictEqual
对象引用,基本类型用 toBe,会有层级克隆引用问题的 toEqual
rollup 插件
- @rollup/plugin-alias 设置别名
1 | // 最后是导出一个插件函数 |
esbuild 插件
1 | // build.js |
通用钩子
开发时,vite dev server 创建一个插件容器按照 rollup 调用创建钩子的规则请求各个钩子函数
- 构建阶段
服务器启动调用一次
- options(options) 获取操纵 rollup 选项
- buildStart(options) 每次开始构建时调用
- resolveId(source,importer,options)每个传入模块请求时被调用,创建自定义确认函数,,可以用来定位第三方依赖
- load(id) 自定义加载器
- transform(code,id) 转换已加载的模块内容
服务器关闭调用一次 - buildEnd
- 输出阶段
- outputOptions(options):接受输出参数;
- renderStart(outputOptions, inputOptions):每次 bundle.generate 和 bundle.write 调用时都会被触发;
- augmentChunkHash(chunkInfo):用来给 chunk 增加 hash;
- renderChunk(code, chunk, options):转译单个的 chunk 时触发。rollup 输出每一个 chunk 文件的时候都会调用;
- generateBundle(options, bundle, isWrite):在调用 bundle.write 之前立即触发这个 hook;
- writeBundle(options, bundle):在调用 bundle.write 后,所有的 chunk 都写入文件后,最后会调用一次 writeBundle;
- closeBundle():在服务器关闭时被调用
vite 特有钩子
config(config,env) 修改 vite 配置
configResolved(resolvedConfig) vite 配置确认
configureServer(server) 用于配置 dev server
transformIndexHtml(html) 转换 index.html 的专用钩子。钩子接收当前的 HTML 字符串和转换上下文
handleHotUpdate(ctx) 执行自定义 HMR 更新,可以通过 ws 往客户端发送自定义的事件
钩子调用顺序
- 配置解析相关
- 构建阶段通用钩子
- 输出阶段的通用钩子
config–>configResolved–>options–>configureServer–>buildStart–>resolvedId–>load–>transform–>buildEnd–>outputOptions–>renderStart–>augmentChunkHash–>renderChunk–>generateBundle–>transformIndexHtml–>writeBundle–>closeBudle
插件执行顺序
- 别名处理 Alias
- 用户插件设置 enforce: ‘pre’
- vite 核心插件
- 用户插件未设置 enforce
- vite 构建插件
- 用户插件设置 enforce: ‘post’
- vite 构建后置插件(minify, manifest, reporting)
vite 插件执行机制
可以通过变量控制 vite 插件执行时机,类似 loader 可以控制执行顺序
1 | // 插件执行是个函数,传参 |
热更新 hmr
- 监听 import.meta.hot 和常见钩子
1 | // 可能不存在 hot, vite build,这段会被 treeshaking |
参考
(vite 插件)[https://juejin.cn/post/7103165205483356168#heading-16]
构建工具
- snowpack
- rollup
- webpack
- gulp 常用 function
1 | import { src, dest, series, parallel } from 'gulp' |
Rollup 是基于 ES6 实现的代码模块化工具
可以使用 rollup 来编译出符合所有类型规范的代码
1 | // src/rollup.config.js |
修改 package.json
1 | { |
package:
- rollup-plugin-css-porter
- 测试 esm 类型
1 | import { method1, method2 } from "./lib"; |
- 测试 cjs 类型
1 | // mock 一个 nodejs 项目来测试 cjs 规范 |
- 测试 umd 类型
1 |
|
cli 工具开发
- #! /usr/bin/env node
执行文件 bin/cli.js 里要加上这句
意思就是用 node 来执行此文件,node 怎么来呢,就去用户(usr)的安装根目录(bin)下的 env 环境变量中去找,简单的说就是如果在 windows 上面,就去安装 node 的 bin 目录去找 node 执行器,一般我们都放在环境变量中,所以就能正确找到 node 来执行
- package.json 的 bin 字段
bin 字段是命令名到本地文件名的映射。在安装时,npm 会将文件符号链接到 prefix/bin 以进行全局安装或./node_modules/.bin/本地安装。
当我们使用 npm 或者 yarn 命令安装包时,如果该包的 package.json 文件有 bin 字段,就会在 node_modules 文件夹下面的 .bin 目录中复制了 bin 字段链接的执行文件。我们在调用执行文件时,可以不带路径,直接使用命令名来执行相对应的执行文件。