Twelve: Universal Upload Component Development and Usage
1. Introduction to Upload Component Development
Developing a universal upload component requires understanding both the theoretical foundations and practical implementation details. The test-driven development approach provides a systematic way to build complex components while ensuring code quality and maintainability.
Core Learning Objectives
- Implement a complete upload workflow using TDD methodology
- Analyze the Element Plus Uploader source code for best practices
- Integrate the upload component into a larger editor application
- Understand Vue3 instance types and component communication patterns
Key Topics Covered
- TDD methodology applied to component development
- Vue3 instance types: Application, Component, and Internal instances
- Component communication strategies in Vue3
- Local image preview techniques using browser APIs
- The HTMLImageElement inheritance hierarchy
- JSDOM and browser environment simulation in Jest
2. Upload Component Requirements and Development Workflow
Current Development Challenges
Many development teams struggle with demonstrating the advantages of testing in their workflow. Complex component development often reveals gaps in testing practices and architectural planning. A universal upload component addresses these challenges by providing a well-defined scope with measurable outcomes.
Why Choose Upload Component for TDD
The upload component represents an ideal candidate for test-driven development due to several factors. Complex interaction logic creates numerous edge cases that benefit from upfront testing. The component includes multiple props, methods, and lifecycle hooks that require careful coordination. This complexity makes the upload component perfect for demonstrating how TDD produces more robust, maintainable code.
Traditional Upload Mechanisms
The HTML form-based approach represents the simplest upload mechanism:
<form method="post" action="/api/upload" enctype="multipart/form-data">
<input type="file" name="attachment">
<button type="submit">Submit</button>
</form>
Modern applications prefer asynchronous uploads using JavaScript, which provides better user experience and more control over the upload process.
Decomposing the Development Task
Breaking large tasks into manageable todo items enables precise tracking and incremental progress. Each todo item becomes a test case that either passes or fails, providing clear feedback on development status. This approach transforms vague requirements into concrete, verifiable outcomes.
Upload Component Requirements Specification
The component must support essential upload workflows while remaining flexible enough for custom implementations. Core requirements include file selection through a button click, automatic upload initiation, and progress tracking. The component should display a file list showing each file's name, current status, and progress percentage. Users must be able to remove files from the queue before or during upload.
Template customization enables integration into diverse design systems. The component should support custom initial containers and post-upload display templates. Lifecycle hooks provide extensibility points for custom behavior: beforeUpload for pre-upload validation, onProgress for real-time progress updates, onSuccess and onError for handling completion and failures, and onChange for tracking state changes. Drag-and-drop support enhances user experience for desktop applications.
3. Fundamental File Upload Mechanisms
Traditional Form Submission
The classic approach uses form submission with proper encoding:
<form method="post" action="http://api.example.com/upload" enctype="multipart/form-data">
<input type="file">
<button type="submit">Submit</button>
</form>
The enctype attribute critically affects how form data transmits. The default application/x-www-form-urlencoded encoding only handles text data. For binary file data, multipart/form-data becomes essential.
Working with File Objects
The input element's change event provides access to selected files through the event target's files property. This returns a FileList object, which resembles an array but lacks Array prototype methods. Individual files appear at specific indices:
const handleFileSelect = (event) => {
const fileList = event.target.files
const firstFile = fileList[0]
console.log(`Selected: ${firstFile.name}, Size: ${firstFile.size}`)
}
The FormData API, designed for XHR Level 2, provides a programmatic way to construct form data for asynchronous uploads:
const uploadFile = async (file) => {
const formData = new FormData()
formData.append('file', file)
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
})
return response.json()
}
4. Refactoring the Uploader Component
Drivers for Refactoring
Refactoring often emerges from new requirements that expose architectural limitations. When manual upload triggering becomes necessary, the original tight coupling between upload logic and DOM events becomes problematic. Accepting that initial implementations rarely achieve perfect architecture enables iterative improvement.
Using Flowcharts to Clarify Logic
Visual representations of component logic reveal hidden dependencies and opportunities for abstraction. Flowcharts expose complex conditional branches and state transitions that text descriptions obscure.
Refactoring Principles
Variable naming should precisely describe the data they contain while remaining concise. Code logic benefits from extracting reusable patterns into well-named functions. Each function should perform a single, well-defined task.
5. Vue3 Instance Types Deep Dive
Application Instance (Vue3)
Vue3 introduces a distinction between application instances and component instances. The createApp factory function creates an application instance that registers global configuration:
const application = createApp(AppRoot)
The application instance handles global component registration, directive registration, and plugin installation. Most methods return the application instance, enabling fluent chain calls. This architecture separates global configuration from individual component implementation.
Component Instance
The component passed to createApp becomes the root component. The mount method attaches the application to a DOM node and returns a component instance:
const componentInstance = application.mount('#app')
Component instances expose all component properties as direct properties. This includes methods, computed properties, props, and setup return values. Vue3 also places component instance properties and globally registered services directly on the instance for convenient access.
Internal Component Instance
The getCurrentInstance function provides access to the internal component instance during setup execution:
setup() {
const internal = getCurrentInstance()
// Access component instance properties
console.log(internal.proxy.$props)
// Access application context
console.log(internal.appContext.config)
}
The internal instance combines characteristics of both application and component instances through its proxy and appContext properties.
6. Component Communication Patterns
Parent Accessing Child Components
The ref directive enables parent components to access child component instances. In composition API, template refs require creating a ref object in setup and adding a matching ref attribute in the template:
// ChildComponent.vue
defineExpose({
childMethod() {
console.log('Child method called')
}
})
// ParentComponent.vue
setup() {
const childRef = ref(null)
const invokeChild = () => {
childRef.value?.childMethod()
}
return { childRef, invokeChild }
}
Child Accessing Parent Components
The $parent property provides direct access to the parent component instance from within a child. However, directly calling parent methods violates unidirectional data flow principles. Events should trigger parent-side changes instead:
setup(props, { emit }) {
const notifyParent = () => {
emit('custom-event', payload)
}
// Avoid: parentRef.value.parentMethod()
}
The $parent chain can extend through multiple parent levels to reach the root component.
Provider/Inject for Multi-Level Communication
Prop drilling through multiple component levels becomes unwieldy in deep hierarchies. The provide/inject pattern enables top-down data sharing without explicit prop chains:
// AncestorComponent.vue
setup() {
const sharedState = reactive({ message: 'Shared data' })
provide('sharedKey', sharedState)
}
// DescendantComponent.vue
setup() {
const received = inject('sharedKey')
}
Event Emitters for Cross-Component Communication
Slot-rendered components cannot receive refs from the parent, making traditional parent-child communication impossible. Event buses solve this communication gap. Vue2's deprecated $on/$off require migration to alternatives like mitt:
import mitt from 'mitt'
const emitter = mitt()
// Emit event
emitter.emit('custom-event', data)
// Listen for event
emitter.on('custom-event', (data) => {
console.log('Received:', data)
})
7. Element Plus Uploader Source Code Analysis
Architectural Patterns in Element Plus
Examining established component libraries reveals proven architectural patterns. The Element Plus Uploader demonstrates several refactoring opportunities for complex components.
Component Separation Strategy
The Element Plus Uploader splits into focused sub-components: UploadList handles file list display, Dragger implements drag-and-drop functionality, and the main Uploader component coordinates the overall workflow. This separation enables independent testing and maintenance of each concern.
Source Code Location
The implementation resides in the Element Plus repository under packages/upload/src, providing a comprehensive reference for upload component patterns.
Template Ref Implementation Details
Template refs in Vue3 connect reactive ref objects to their corresponding VNode references. During the patching process, Vue assigns the actual DOM node or component instance to the ref object. This assignment occurs during mounting, so ref values are only available after initial render completes.
8. Local Image Preview Techniques
The Need for Instant Preview
Waiting for upload completion before displaying images creates poor user experience. Local preview techniques display images immediately after selection without server round-trips.
URL.createObjectURL Approach
This static method creates a DOMString representing the specified File or Blob object. The returned URL references the object in memory:
const createPreviewUrl = (file) => {
return URL.createObjectURL(file)
}
The URL remains valid until explicitly revoked or the document unloads. Each call creates a new unique URL reference to the same underlying object.
FileReader.readAsDataURL Approach
This method asynchronously reads the file contents as a base64-encoded data URL:
const readAsDataUrl = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result)
reader.onerror = reject
reader.readAsDataURL(file)
})
}
Comparative Analysis
The URL.createObjectURL method executes synchronously and returns immediately, making it faster for quick previews. However, each created URL consumes memory that must be manually released through URL.revokeObjectURL when no longer needed.
The FileReader approach produces base64 strings compatible with any system expecting URL-formatted images. This format integrates easily with existing image handling pipelines without conversion. The asynchronous nature means callers must handle the result through callbacks or promises.
9. Determining Image Dimensions
HTMLImageElement Inheritance Chain
Understanding the DOM element hierarchy clarifies which interfaces provide dimension information. The HTMLImageElement type enables programmatic image manipulation:
graph TD
A[EventTarget] --> B[Node]
B --> C[Element]
C --> D[HTMLElement]
D --> E[HTMLImageElement]
This inheritance chain shows how browser DOM APIs build complex interfaces from foundational abstractions. EventTarget provides event handling capabilities, Node establishes the basic tree structure, Element adds tag-specific behavior, HTMLElement provides standard HTML element features, and HTMLImageElement adds image-specific functionality.
Related Element Types
Similar inheritance patterns apply to other HTML elements: HTMLInputElement for form inputs, HTMLDivElement for container elements, and so forth. Each type adds specialized properties and methods relevant to its specific use case.
10. Extracting Image Dimensions Programmatically
Using the Image Constructor
The Image constructor creates an img element without attaching it to the DOM. Setting the src property and waiting for load events provides access to natural dimensions:
const extractDimensions = (source) => {
return new Promise((resolve, reject) => {
const imageElement = new Image()
const imageSource = typeof source === 'string'
? source
: URL.createObjectURL(source)
imageElement.src = imageSource
imageElement.onload = () => {
const { naturalWidth: width, naturalHeight: height } = imageElement
resolve({ width, height })
}
imageElement.onerror = () => {
reject(new Error('Image loading failed'))
}
})
}
The naturalWidth and naturalHeight properties provide the intrinsic image dimensions, distinct fromm rendered dimensions affected by CSS.
Week Twelve Summary
Development Process Review
The upload component development followed a TDD methodology with interspersed traditional development phases. File selection and basic upload flow established the foundation. Upload list implementation enabled multi-file management. Template customization supported diverse UI requirements. Event handling enabled external integration. Drag-and-drop enhanced user interaction. Instance methods provided programmatic upload control. Image preview completed the feature set.
Expanded Knowledge
Vue3 presents three distinct instance types. Application instances from createApp handle global configuration. Component instances from mount or ref provide component-level access. Internal instances from getCurrentInstance expose internal APIs during setup.
Component communication employs four primary strategies. Refs enable parent-to-child access. $parent provides child-to-parent references. Provider/inject handles multi-level data sharing. Event emitters manage cross-component messaging.
The Element Plus Uploader demonstrates effective component decomposition with separate concerns for upload lists and drag functionality.
Implementation Integration
The upload component integrated into the editor application through the LImage component. This component appears in the component palette and responds to upload success events. Uploaded images become available for editor placement.
Thirteen: Component Library Packaging, Publishing, and CI/CD Integration
1. Introduction to Component Library Distribution
Component libraries shared between multiple projects benefit from independent distribution. Current co-location with the main project creates maintenance overhead and deployment complexity. Publishing to npm enables version-controlled sharing with clear dependency management.
Core Learning Objectives
- Understand JavaScript module evolution and bundling strategies
- Configure Rollup for library packaging
- Establish npm publication workflows
- Implement automated CI/CD pipelines
Key Topics
- Module history: AMD through ES modules
- Bundler comparison: Webpack and Rollup
- Snowpack's unbundled development approach
- Rollup configuraton for library distribution
- NPM publication procedures
- Travis CI for continuous integration and deployment
2. JavaScript Module Evolution
The Need for Modular Code
Modular programming addresses code organization challenges in large applications. Independent modules enhance maintainability by localizing related functionality. Reusability increases when modules encapsulate specific capabilities without external dependencies.
Pre-ES6 Module Patterns
Early JavaScript development relied on script tag inclusion with manual dependency ordering:
<script src="jquery.js"></script>
<script src="underscore.js"></script>
<script src="backbone.js"></script>
<script src="application.js"></script>
This approach required developers to manually track dependencies and ensure correct loading order.
Immediately Invoked Function Expressions
IIFEs created private scopes preventing global namespace pollution:
const collectionManager = (function() {
const storage = []
function addItem(item) {
storage.push(item)
console.log(`Added: ${item}`)
}
return { addItem }
})()
collectionManager.addItem('New Item')
Limitations of Early Patterns
Global variable dependency created security and maintainability risks. Namespace conventions provided unreliable conflict prevention. Manual dependency management introduced ordering errors. Pre-deployment bundling required external tooling.
CommonJS Module Format
Node.js popularized the synchronous module loading pattern:
const utility = require('./utility')
module.exports = function component() {
return { render: () => 'Component Output' }
}
This format never achieved browser compatibility without bundling transformation.
Asynchronous Module Definition
AMD emerged to address browser module loading through asynchronous patterns:
define(['./dependency'], function(dependency) {
return function myModule() {
return dependency.process()
}
})
RequireJS served as the primary AMD implementation. Similar patterns like CMD appeared in the Chinese development community.
ES6 Modules
The standardized module format provides native browser support and enhanced syntax:
import utility from './utility.js'
export default function component() {
return utility.process()
}
Static analysis enables tree shaking and optimization during bundling.
3. Understanding Module Bundlers
Why Bundling Remains Necessary
Despite browser support for ES modules, production applications typically require bundling for optimal performance. Native imports require network requests for each module, creating waterfalls that slow initial loading.
Bundler Functionality
Bundlers transform module-based source code into browser-compatible output. They perform compilation, transformation, and concatenation to produce runnable JavaScript bundles.
Webpack Introduction
Webpack remains the dominant bundler for web applications:
npx webpack entry.js --output-path dist/
Webpack handles diverse asset types through loaders and extends functionality through plugins.
Rollup Introduction
Rollup focuses on library building with efficient output:
npx rollup source.js --file dist/bundle.js --format iife
4. Webpack Versus Rollup
Webpack for Applications
Webpack excels at building single-page applications with complex requirements. Loaders transform various asset types—images, styles, fonts—into bundle-compatible formats. Plugins perform processing on the complete bundle. Code splitting extracts common dependencies for efficient caching. The development server provides hot module replacement for rapid iteration.
Rollup for Libraries
Rollup's design prioritizes ES module compatibility and output efficiency. Tree shaking removes unused exports during bundling, producing minimal bundles. The flat bundle structure eliminates internal module overhead.
Static Analysis in Rollup
Rollup's AST-based analysis enables aggressive dead code elimination. The ES module import rules—module-level imports only, constant module names, complete initialization—enable reliable static analysis.
Selection Guidelines
Web applications benefit from Webpack's feature-rich ecosystem. Library development favors Rollup's efficient output and simple configuration. Modern frameworks often provide optimized presets for each use case.
5. Output Format Considerations
ES Modules as Primary Format
Modern library distribution should prioritize ES module output:
// Library output in ESM format
export function component() { /* ... */ }
Tree shaking works optimally with ES modules, and browsers increasingly support native ESM imports.
UMD as Fallback
Universal Module Definition provides compatibility across environments:
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define(['dependency'], factory)
} else if (typeof module === 'object' && module.exports) {
module.exports = factory(require('dependency'))
} else {
root.library = factory(root.dependency)
}
}(typeof self !== 'undefined' ? self : this, function(dependency) {
return { /* ... */ }
}))
However, UMD bundles lack tree shaking compatibility and should serve as secondary outputs.
TypeScript Type Definitions
Publishing TypeScript type definitions alongside JavaScript enables type-safe consumption:
{
"types": "dist/index.d.ts"
}
6. Snowpack and Unbundled Development
Development Speed Challenges
Traditional bundling creates performance bottlenecks as projects grow. Large applications may require minutes for initial builds, slowing development velocity.
Snowpack's Approach
Snowpack leverages browser ES module support for development:
npx snowpack dev
Each source file builds once and caches indefinitely. Single file modifications require rebuilding only that file, enabling near-instant updates.
Module Resolution
Snowpack transforms node_modules dependencies into individual ESM files:
/node_modules/react/index.js → /web_modules/react.js
/node_modules/react-dom/index.js → /web_modules/react-dom.js
Modern browsers load these native modules without additional tooling.
Production Bundling
Snowpack supports optional production bundling for legacy browser compatibility or further optimization.
7. Vue3 Plugin System
Plugin Architecture
Vue plugins extend application functionality through a standardized interface. Plugins export either an install function or a function that receives the application instance:
const myPlugin = {
install(app, options) {
app.config.globalProperties.$util = utilityFunction
app.component('MyComponent', MyComponent)
app.directive('highlight', HighlightDirective)
}
}
Plugin Capabilities
Plugins enable global method addition, component registration, directive creation, and application configuration modification.
8. Component Library Entry Point Design
Automatic Global Registration
Single-import registration simplifies component library consumption:
// library-entry.js
import { LText } from './components/LText'
import { LImage } from './components/LImage'
const components = [LText, LImage]
const install = (app) => {
components.forEach(component => {
app.component(component.name, component)
})
}
export default { install }
Individual Component Import
Tree shaking requires each component to work independently:
// Component folder structure
src/
components/
LText/
index.ts
LImage/
index.ts
index.ts
Each component exports an object with an install method for plugin-based registration.
9. Configuring Rollup for Library Building
Dependency Classification
Package.json dependencies require careful categorization. Regular dependencies bundle with the library. Development dependencies remain development-only. Peer dependencies indicate requirements that consumers must provide.
External Dependency Configuration
Large dependencies like Vue should remain external:
// rollup.config.js
export default {
input: 'src/index.ts',
output: {
format: 'es',
file: 'dist/library.esm.js'
},
external: ['vue']
}
Consumers provide peer dependencies, reducing bundle size.
10. NPM Publication Fundamentals
NPM Ecosystem Role
NPM serves as the primary JavaScript package registry. Developers publish packages for community or organizational consumption. Package consumption occurs through npm install for libraries or npx for CLI tools.
Semantic Versioning
Version numbers follow major.minor.patch conventions:
- Major version: Breaking API changes
- Minor version: New features (backward compatible)
- Patch version: Bug fixes (backward compatible)
Package.json Configuration
The files field explicitly controls package publication contents:
{
"name": "component-library",
"version": "1.0.0",
"files": ["dist/", "src/"]
}
Git-ignored files remain excluded unless explicitly listed.
11. NPM Script Automation
Pre and Post Hooks
NPM scripts support lifecycle hooks through naming conventions:
{
"scripts": {
"prebuild": "echo 'Before build'",
"build": "rollup -c",
"postbuild": "echo 'After build'"
}
}
Hooks execute automatically before or after the named script.
Prepare Hook
The prepare script runs after package installation and version bumps, ideal for build tasks:
{
"scripts": {
"prepare": "npm run build"
}
}
12. Continuous Integration and Deployment
Manual Workflow Challenges
Repetitive tasks like testing, building, and publishing consume significant time when performed manually. Automation ensures consistent execution and faster feedback loops.
Continuous Integration Principles
CI involves frequent code integration with automated validation. Each integration triggers automated builds and tests that verify code quality. Early error detection prevents integration problems from accumulating:
# .travis.yml
language: node_js
node_js:
- '18'
script:
- npm run lint
- npm run test
Continuous Delivery Concepts
CD extends CI by automatically preparing releases. Passed tests trigger deployment-ready artifacts. Human approval gates production deployment in typical workflows.
Continuous Deployment Automation
Full automation proceeds from commit through production:
deploy:
provider: npm
email: $NPM_EMAIL
api_key: $NPM_TOKEN
on:
tags: true
Tag-based triggers control publication timing.
CI/CD Platform Options
GitHub Actions and Travis CI provide mature CI/CD integration with GitHub repositories. Both platforms support automated testing, building, and publishing based on repository events.
Week Thirteen Summary
Module System Evolution
JavaScript module capabilities evolved through global variable patterns, CommonJS synchronous loading, AMD asynchronous definition, UMD universal compatibility, and finally ES modules with native browser support. Each stage addressed specific limitations while enabling increasingly sophisticated applications.
Bundler Selection
Webpack provides application-oriented features including hot module replacement, code splitting, and extensive plugin ecosystems. Rollup focuses on library building with efficient tree shaking and minimal output. Snowpack introduces bundleless development that leverages native browser capabilities.
Library Distribution
Rollup configuration requires careful output format selection, external dependency management, and TypeScript integration. Element Plus demonstrates effective library configuration patterns worth emulating.
NPM Publication
Package publishing demands attention to version semantics, file inclusion, and pre-publication validation. Linting and testing should occur automatically before publication attempts.
CI/CD Pipeline
Continuous integration validates code on every change. Continuous deployment automatically publishes new versions. Tag-based triggers enable semantic versioning workflows. Platforms like Travis CI automate these processes based on repository events.
Critical Success Factors
Effective component library development requires familiarity with tooling ecosystems, documentation reading skills, hands-on implementation practice, and pattern recognition across similar solutions.