十二、通用上传组件开发以及使用
1、导学
* 开发通用上传组件
- 通过TDD的方式,开发一个通用上传组件,然后将组件添加到编辑器中进行使用,从
这个过程中衍生出很多的相关知识点
* 主要内容
- 模拟真实开发场景,使用TDD的方式,一步步开发一个通用上传组件
- 分析Element Plus中Uploader的源代码
- 将上传组件应用到编辑器中
- 对于知识点的发散和总结
~ Vue3中实例的类型
~ Vue3中组件通信方法
~ 预览本地图片的两种方法
~ HTMLImageElement家族的一系列关系
~ JSDOM是什么?jest是怎样采用它模拟浏览器环境的
* 关键词
- TDD
- 上传组件
- 代码重构
- ELement Plus源代码
- 发散和总结的思维方式
* 学习方法
- 真实场景,拥抱TDD和普通流程的混合开发方式,感受它的奇妙
- 对于一个知识点的发散学习,追根溯源很有意思
2、上传组件需求以及开发流程
* 现在面临的问题
- 没有很好的体现测试的优势
- 缺少复杂组件的开发经验
* 解决办法
- 一个通用上传组件
- 原因:
~ 复杂的逻辑交互
~ 属性、方法、生命周期都有很多
~ 适合测试驱动开发
* 上传组件的定义
- 上传是将信息通过网页或者上传工具发布到远程服务器上的过程。
- 传统的form表单的方式
########
<form>
<input type="file" name="myFile"/>
<button type="submit">提交</button>
</form>
########
- ajax发送异步请求的方式
* 将任务拆分
- 将大块任务,量化成一系列Todo List,使用测试驱动,然后把任务划掉
- 对工作目标非常清晰,并且可进行细微的调整
- 随着时间的进行,对完成时间越来越清晰可控
* 上传组件的需求
- 基本上传流程-点击按钮选择,完成上传
- 支持上传文件列表
~ 显示文件名称
~ 状态
~ 可删除
~ 显示上传进度
~ 有可能有更丰富的显示支持?
- 自定义模板
~ 支持初始容器自定义
~ 支持上传完毕后自定义
- 支持一系列生命周期钩子事件,上传事件
~ beforeUpload
~ onProgress
~ onSuccess
~ onError
~ onChange
- 拖拽上传支持
- 等等
3、上传文件的基本方式
* 传统模式
########
<form method="post" action="http://local.test:7001/api/upload" enctype="multipart/form-data">
<input type="file">
<button type="submit">Submit</button>
</form>
########
* 推荐一个抓包工具mac下
- https://proxyman.io/
- 特别注意enctype
~ 表单默认:application/x-www-form-urlencoded
~ 如果要有二进制数据:multipart/form-data
* 从Input获得Files
- e.target.files是FileList对象
https://developer.mozilla.org/zh-CN/docs/Web/API/FileList
~ 它是一个array-like object,不是真的数组
- files[索引]拿到对应的文件,它是File独享
https://developer.mozilla.org/zh-CN/docs/Web/API/File
- FormData针对XHR2设计的数据结构,可以完美模拟HTML的<form>
https://developer.mozilla.org/zh-CN/docs/Web/API/FormData
4、uploader重构的基本步骤
* 需求带动重构
- 现在需要手动触发上传
- 上传过程和DOM事件是耦合在一起的
- 一开始想设计一个完美的代码结构是不可能的
* 用流程图来理清代码逻辑
* 重构的基本要点
- 变量命名-准确,言简意赅
- 代码逻辑-抽象通用逻辑,更细粒度
5、扩展知识,Vue3中的实例
* 每个Vue应用都是new Vue函数创建的一个新的实例
* 创建的时候将data作为property添加到响应式系统中
* https://v2.cn.vuejs.org/v2/guide/instance.html
- Vue3-Application Instance
* createApp创建一个**Application Instance**
* 应用实例用来注册应用中的全局内容
* 大多数方法支持链式调用,返回**应用实例**
* https://cn.vuejs.org/guide/essentials/application.html
* 为什么有这样的修改?
########
const app = createApp(App)
########
* createApp传递的那个组件,称之为root component
* mount方法用来:
- 将应用实例挂载到DOM节点上
- 返回的不是**应用实例**,而是**组件实例**(和Vue2那个一样)
########
const vm = app.mount('#app')
console.log(vm)
########
* 关于组件实例
- 组件中的所有属性(methods,props,computed,setup...)都会在实例上平铺展示
- 还有一系列内置或者全局的属性,比如($attrs,$refs)内置,($message,$confirm)全局注册
* 所以测试中的wrapper.vm属于组件实例
* 通过ref拿到的子组件实例属于组件实例
- 组件内部实例-Internal Component Instance
* getCurrentInstance()
* 这是一个神奇的混合实例
* proxy属性-可以拿到**组件实例**上面的内容
* appContext-可以拿到**应用实例**上的部分属性
########
setup() {
const internal = getCurrentInstance()
console.log(internal?.proxy)
console.log(internal?.appContext)
}
########
6、组件之间互相访问的方法
* 这里的实例说的都是组件实例(component instance)
* $refs
* 在composition API中,使用template refs,在setup中创建ref对象返回,
在template中添加同名的ref属性
* https://v3.vuejs.org/guide/composition-api-template-refs.html#template-refs
* 在当前组件实例上,有一个特殊的属性称之为$parent,它可以拿到父组件实例
* **注意**,直接调用父组件上的方法和属性,这是一种很不好的做法,应该保持单向数据流,子
组件发送特定的事件去触发父组件的改变
* $parent可以继续往上嵌套调用比如$parent.$parent一直可以到rootComponent
- 使用Provide/Inject完成子组件到父组件的多级访问
* 属性传递和$parent访问在多级传递的时候都会非常繁琐
* 使用provide和inject完成跨级传递
* 响应式对象也可以被provide
* 文档地址:https://v3.vuejs.org/guide/component-provide-inject.html
* 有一些特殊的情形,我们需要使用事件监听器完成父子通讯
* 比如:父组件中有slot,子组件是以slot形式存在的,没法添加ref
########
// parent.vue
<div><slot></slot></div>
// app.vue
<parent>
<child/>
</parent>
########
* 在vue2中的$on,$off等事件监听器,已经被废除,需要使用第三方库。
* 官方推荐mitt作为事件监听器,https://github.com/developit/mitt
7、Element Plus Uploader源码分析
* 面对一个知识点,动用发散性思维,总结和他相关的一系列知识点
* 代码实现冗长的时候,逻辑代码的拆分
* 组件结构上的拆分
- UploadList
- Dragger
- 分析element plus Uploader的源代码
* https://github.com/element-plus/element-plus/blob/dev/packages/upload/src
* https://v3.vuejs.org/guide/composition-api-template-refs.html
* vnode ref属性等于render对象中的响应式对象
* 对应的DOM节点或者组件实例就会被赋值给这个响应式对象
* 在patch或者mount时候发生的,所以初次渲染完毕才能拿到这个值
* 举例来看看
8、图片预览的方式
* 不要等上传完毕再显示,需要一种快速本地预览图片的方法
* 一个静态方法,创建一个DOMString,返回一个URL,URL和document绑定,表示指定的File对象
* 文档地址:https://developer.mozilla.org/zh-CN/docs/Web/API/URL/createObjectURL
- FileReader.readAsDataURL()
* 一个FileReader上面的实例方法,读取指定的File对象,读取完成的时候触发回调,返回URL
格式的字符串(base64)
* 文档地址:https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader/readAsDataURl
* 返回值
- FileReader.readAsDataURL(file)可以得到一段base64的字符串
- URL.createObjectURL(file)可以得到当前文件的一个内存URL
* 执行机制
- FileReader.readAsDataURL(file)通过回调的形式返回,异步执行
- URL.createObjectURL(file)直接返回,同步执行
* 内存清理
- FileReader.readAsDataURL(file)依照JS垃圾回收机制自动从内存中清理
- URL.createObjectURL(file)存在于当前document内,清除方式只有unload()事件或
revokeObjectURL()手动清除
* URL.createObjectURL(file)得到本地内存容器的URL地址,同步使用,比较方便快捷,多次使用
需要注意手动释放内存的问题,性能优秀。
* FileReader.readAsDataURL(file)胜在直接转为base64格式,可以直接用于业务,无需二次转换
格式。
9、图片获取真实大小的方法
- 平平无奇的
* 它的DOM节点有个神奇的类型称之为HTMLImageElement,它是标准WebAPI的一部分,还
有很多类似的HTML标签类型
- HTMLInputElement
- HTMLDivElement
- 文档地址:https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLImageElement
* 追根溯源:HTMLImageElement继承了HTMLElement
* 还能往上吗?:HTMLElement继承了Element
- HTMLElement
- SVGElement
* 停不下来:Element继承了Node
- 一个基本的抽象类
- 它是一个抽象类,所以没有一个真正的Node对象
- 所有对象实现的都是基于它的子类
~ Document
~ Element
~ DocumentFragment
* 最终的头目:EventTarget
- 是一个最基本的DOM接口
- 可以接受事件,创建监听器等实现
~ Element,Document,Window
~ XHLHttpRequest
* HTMLImageElement → HTMLElement → Element → Node → EventTarget
* 请自己参阅文档https://developer.mozilla.org/zh-CN/docs/Web/API/MouseEvent
10、通过Image构造函数获取图片的原始大小
export const getImageDimensions = (url: string | File) => {
return new Promise<{ width: number; height: number }>((resolve, reject) => {
const img = new Image()
img.src = typeof url === 'string' ? url : URL.createObjectURL(url)
img.addEventListener('load', () => {
const { naturalWidth: width, naturalHeight: height } = img
resolve({ width, height })
})
img.addEventListener('error', () => {
reject(new Error('There was some problem with the image.'))
})
})
}
11、本周总结
* Uploader真实开发流程-TDD和普通开发混合
- 文件基本上传原理
- 组件实现基本上传流程(TDD)
- 上传列表的实现(TDD)
- 自定义模本的实现(TDD)
- 自定义事件的实现(TDD)
- 拖动文件上传
- 添加实例方法编码
- 添加图片列表的图片预览功能
* 扩展知识
- Vue3的三种实例
~ App Instance应用实例-createApp
~ Component Instance组件实例-ref或者app.mount()返回
~ Component Internal Instance组件内部实例-getInternalInstance()
- Vue3组件通信的四种方法
~ 父组件实例中获取子组件实例-ref
~ 子组件实例中获取父组件实例-$parent
~ 使用Provider/Inject自上而下跨层传递数据
~ 使用emitter事件发射器的方式跨层传递数据
- Element Plus Upload源码分析
~ 组件的分割-Index,Uploader,UploadList,Dragger
~ 数据操作的分割-useHandler
- 本地读取图片并且展示的两种方法
~ URL.createObjectURL()
~ FileReader.readAsDataURL()
- Jest运行的环境之谜:使用JSDOM
* 上传组件添加到编辑器,并且完成对应功能
- 添加LImage组件
- 添加至左侧组件列表并且相应上传成功
- 根据上传数据
* 扩展知识
- HTMLImageElement家族传承
~ HTMLImageElement → HTMLElement → Element → Node → EventTarget
- 获取图片真实大小的方法
* 选择正确的开发方式,来完成对应的功能
* 对之前的功能更有信心,不会怕新功能搞坏了老功能
* 重构更方便了
* 按照自己的想法,继续完成对Uploader组件的开发过程
* 根据MDN文档,分析Event对象的家族关系
* 完成ImageProcesser的组件开发,并且为其写测试
- 处理图片组件的Src属性,传入一个图片地址,可以重新上传改变图片地址
- 注意组件属性处理组价的标准
十三、业务组件库打包、发布、添加CI-CD
1、导学
* 目前的组件库是和主项目混在一起的,这不是完美的形态,业务组件库作为要为两
个项目所服务的公共组件。是时候将它抽取出来发布成至npm,让两个项目可以共
享。组件库打包有很多新的知识点,让我们来一起学习。
* 主要内容
- javascript模块以及打包工具
~ AMD → Common.js → ES modules
~ Webpack vs Rollup
~ Snowpack
- 创建业务组件库代码
- 添加Rollup配置以及完成打包
~ Rollup配置文件
~ Rollup插件使用
~ Rollup插件简单原理
~ Element Plus打包过程分析
- 发布到NPM以及使用Travis CI完成CI/CD
~ NPM简介和发布
~ 发布前验证代码质量
~ Travis完成CI和CD两个流程
* 关键词
- Module-模块
- Bundler-打包工具
- Rollup
- NPM
- CI/CD
- Travis CI
* 学习方法
- 实践出真知,这节课知识点众多,并且涉及的工具较多,请大家一定要动起手
来,只有实践以后才能加深理解掌握。
2、javascript模块发展历史
* 模块(modules)是什么?
########
from package import function
########
########
package main
import (
"fmt"
)
########
* 模块化的优点
- 可维护性
- 可复用性
* ES6之前没有模块的年代
########
// 使用backbone.js的方法
<script src="spec/support/jquery.js"></script>
<script src="spec/support/underscore.js"></script>
<script src="spec/support/backbone.js"></script>
<script src="backbone.localStorage.js"></script>
<script src="todos.js"></script>
########
* 全局变量+命名空间(namespace)
########
// IIFE自执行函数,创建一个封闭的作用域,赋值给一个全局变量
var namesCollection = (function() {
// private members
var objects = [];
// Public Method
function addobject(object) {
objects.push(object);
printMessage(object);
}
// Private Method
function printMessage(object) {
console.log("Object successfully added:", object);
}
// public members, exposed with return statement
return {
addName: addObject,
};
})();
namesCollection.addName('viking')
########
* 缺点
- 依赖全局变量,污染全局作用域,不安全
- 依赖约定命名空间来避免冲突,可靠性不高
- 需要手动管理依赖并控制执行顺序,容易出错
- 需要在最终上线前手动合并所有用到的模块
* Common.js
########
const bar = require('./bar')
module.exports = function() {
}
########
- 没法在浏览器里直接运行
* AMD-(Asynchronous module definition)
- 采用异步方式加载模块
- 仅仅需要在全局环境定义require与define,不需要其他的全局变量
- 通过文件路径或模块自己声明的模块名定位模块
- 提供了打包工具自动分析依赖并合并
- 配合特定的AMD加载器使用,RequireJS
- 同时还诞生了很多类似的模块标准CMD
########
define(function(require) {
// 通过相对路径获取依赖模块
const bar = require('./bar')
// 模块产出
return function() {
}
})
########
* ES6 modules
########
// 通过相对路径获取依赖模块
import bar from './bar'
// 模块产出
export default function() {
}
########
- 引入和暴露的方式更加多样
- 支持复杂的静态分析
3、Bundler是什么?
* 诞生原因
- 使用import export这种同步加载的方式在大多数浏览器中无法使用
* Bundler-打包工具
- 将浏览器不支持的模块进行编译,转换,合并最后生成的代码可以在浏览器端良好
的运行的工具。
* 大家最熟悉的-Webpack
- 对于web应用来说:一般采用单javascript文件入口
- https://webpack.js.org/
- 举例时间
########
npx webpack main.js
########
* 后起之秀-Rollup
- https://rollupjs.org/guide/en/
########
npx rollup main.js --file dist/bundle.js --format iife
########
4、Webpack vs Rollup
* Webpack
- 大型SPA项目的模块化构建,也就是我们常说的web应用。
~ 通过各种Loader处理各种各样的静态资源
~ 通过各种插件Plugins对整体文件进行一些处理。
~ Code splitting将公共模块进行提取。
~ 提供一个webpack-dev-server,进行本地开发。
~ 支持HMR模块热替换。
* Rollup
- Rollup设计之初就是面向ES module的,构建出结构扁平,性能出众的类库。
- ES module的规则
~ import只能作为模块顶层的语句出现,不能出现在function里面或是if里面。
~ ES import的模块名只能是字符串常量。
~ 不管import的语句出现的位置在哪里,在模块初始化的时候所有的import都
必须已经导入完成。
- 使用工具静态分析的过程
~ AST(抽象语法树)
~ Tree shaking机制-摇树!让死了的叶子掉下来。
~ 目的就是将es modules打包生产特定的JS模块文件,并减小它的体积。
* Webpack vs Rollup
- 通过以上的对比可以得出,构建App应用时,webpack比较合适,如果是类库(纯
js项目),rollup更加适合。
- Webpack的优势
~ 强大的生态插件
~ 面向开发应用的特性支持HMR,按需加载,公共模块提取
~ 简化Web开发的环节,图片自动转base64,资源的缓存(添加chunkid)
- Rollup
~ 构建高性能的模块文件,这正是类库所需要的。
~ 编译出来的代码可读性好,内容更小,执行效率更高。
~ 配置比较简单。
5、打包什么类型的文件?
* Commonjs,es6 modules-需要特殊的module bundler支持
* AMD已经有点过时了-需要使用特殊的Loader-require.js
* 浏览器中直接使用-UMD(Universal Module Definition)
- 通用的一种JavaScript格式
- 兼容common.js,AMD,浏览器
- https://github.com/umdjs/umd
- Vue和React都提供了这样的格式
- 不是一种推荐的格式,太大了!不支持tree shaking
########
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['b'], factory);
} else if (typeof define === 'object' && module.exports) {
// Node. Does not work with string CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory(require('b'));
} else {
// Browser globals (root is window)
root.returnExports = factory(root.b);
}
}(typeof self !== 'undefined' ? self : this, function (b) {
return {};
}))
########
* 结论
- 首要格式-ES Modules,并且提供支持typescript的type文件。
- 备选方案-UMD
6、另辟蹊径-简介Snowpack
* Bundler的问题
- 当资源越来越多的时候,打包速度越来越慢。
- 大中型项目,启动时间可能达到好几分钟。
* 另辟蹊径-Snowpack
- https://www.snowpack.dev/
- 利用新版浏览器支持es modules的特性。
- 不会被打包。
- 每个文件编译一次,永久被缓存。
- 当一个文件修改的时候,只需要重新build那一个文件。
* 处理Node_modules中的模块
- 它扫描node_modules中的模块。找到使用的模块。
- 将每个模块都分别转换成单个js文件。
- 这些单个文件都是esm模块,可以被最新的浏览器直接使用。
########
node_modules/react/**/* ->
http://localhost:3000/web_modules/react.js
node_modules/react-dom/**/* ->
http://localhost:3000/web_modules/react-dom.js
########
* 为生产环节Build代码
- 默认情况下,和开发环境生成的代码是几乎一致的。
- 当你想用一个bundler你可以自己选择去用,而不是你必须要用。
- 提供了插件,生成bundle以后全浏览器兼容的代码。
7、Vue3的插件系统
* 一段代码给vue应用实例添加全局功能。它的格式是一个object暴露出一个
install()方法,或者一个function
* 它没有严格的限制,一般有以下几种功能
- 添加全局方法或者属性
- 添加全局资源:指令,过滤器等
- 通过全局混入来添加一些一些组件选项
- 通过config.globalProperties来添加app实例方法
* 写一个插件试试
8、组件库入口文件的设计
* 所有组件一次性全部导入并且作为插件使用
########
import LegoComponents from 'lego-components'
app.use(LegoComponents)
########
- 建立一个入口文件index.ts
- 将所有组件导入,作为一个数组,创建一个install函数,循环调用app.component
- 默认导出一个插件(这个install函数)
* 单个组件导入并且作为插件使用
########
import { LText } from 'lego-components'
app.use(LText)
// 或者
app.component(LText.name, LText)
########
- 每个组件新建一个文件夹,并且创建一个单独的index.ts文件
- 每个组件设计成一个插件(一个object拥有install方法)
- 在全局入口文件导出
9、尝试打包入口文件
* https://www.npmjs.com/package/@rollup/plugin-node-resolve
* npm依赖的分类
- dependencies
~ 运行项目业务逻辑需要依赖的第三方库
~ npm install‘模块名称’的时候都会被解析,下载
- devDependencies
~ 开发模式工作流下依赖的第三方库
~ 单元测试,语法转换,lint工具,程序构建,本地开发等等
- peerDependencies
~ 需要核心依赖库,不能脱离依赖库单独使用。
* rollup external字段
- https://rollupjs.org/guide/en/#external
10、NPM简介,以及package.json的信息更新
* NPM的主要功能
- 从npm服务器下载别人编写的第三方包到本地,比如vue
- 从npm服务器下载并安装别人编写的命令行程序到本地使用,比如vue-cli
- 允许用户将自己编写的包或命令行程序上传到npm服务器供别人使用
* 语义化版本-semver
- 说明网址:https://semver.org/lang/zh-CN/
- 版本格式:主版本号.次版本号.修订号(1.0.0),版本号递增规则如下:
~ 主版本号:当你做了不兼容的API修改,
~ 次版本号:当你做了向下兼容的功能性新增,
~ 修订号:当你做了向下兼容的问题修正
* npm files字段
- 默认忽略掉gitingore中的内容
- 指示npm publish的时候需要上传的内容
- package.json/README.md/CHANGLOG.md/LICENSE都会包含在其中
11、npm scripts
* Pre&Post scripts
- 你script的名称前面加上pre或者post,那么当运行这个命令的时候,pre和
post会自动在这个命令之前或者之后运行。
########
"scripts": {
"precompress": "{{ executes BEFORE the `compress` script }}",
"compress": "{{ run command to compress files }}",
"postcompress": "{{ executes AFTER `compress` script }}"
}
########
* Life Cycle Scripts
- prepare
- prepublish(即将废弃)
- prePublishOnly
12、CI/CD的概念
* 业务组件库的开发和发布是随着一系列任务进化的
- 本地commit钩子函数完成commit验证
- 代码push到远端以后
- 跑特定的test(不仅仅是本机的unit test,也可能有时间很长的E2E test)
- test通过以后检查是否有新的tag,假如有就自动publish一个新的版本
- 甚至还有更多,自动部署文档站点等等。
* 这些任务如果手动操作,费时费力,不是很好的解决方案
* CI(Continuous integration)-持续集成
- 持续集成指的是,频繁地(一天多次)将代码集成到主干。一旦开发人员对应用所
做的更改被合并,系统就会通过自动构建应用并运行不同级别的自动化测试(通常
是单元测试和集成测试)来验证这些更改,确保这些更改没有对应用造成破坏。
~ 快速发现错误
~ 防止分支大幅偏离主干
- 持续集成的目的,就是让产品可以快速迭代,同时还能保持高质量。
* CD(Continuous Delivery)-持续交付
- 持续交付(Continuous delivery)指的是,频繁地将软件的新版本,交付给质量
团队或者用户,以供评审。
* CD(Continuous Deployment)-持续部署
- 持续部署(continuous deployment)是持续交付的下一步,指的是代码通过评
审以后,自动部署到生产环境。
* 两大服务
- Github Action(https://github.com/features/actions)
- Travis(https://www.travis-ci.com/)
13、本周总结
* 过程回顾和要点
- JavaScript模块化历史
~ 使用自执行函数挂载到全局对象
~ Common.js
~ AMD、CMD
~ UMD
~ ES Modules
- 打包工具
~ 可以使用现代的模块系统进行开发并且兼容浏览器环境
~ Webpack
~ **Rollup**
> Tree shaking
~ Snowpack
> Bundleless,不打包,利用浏览器对ESM的支持
- 配置Rollup
~ Rollup配置文件
~ Rollup插件
> vue plugin
> typescript plugin
> css-only plugin
~ 生成ESM和UMD两种格式
~ 分析Element Plus的打包过程
- NPM发布
~ npm scripts中的一些钩子函数
~ 发布前运行lint和test
~ 使用husky添加提交前检查
~ npm publish的原理
- CI/CD
~ CI/CD的概念
~ 使用Travis CI完成CI-代码提交后运行测试
~ 使用Travis CI完成CD-提交特定tag以后自动发布
* 本周特别注意
- 工具和平台
- 看文档
- 亲手写代码
- 举一反三