Modern frontend teams typically use internal build platforms; our team uses Cloud Chang for CI/CD. Cloud Chang automates tasks like code fetching, dependency installation, building, CDN upload of static assets, Docker image creation, and K8s deployment.
Scenario Analysis
For security, some clients require full intranet deployment. Key considerations:
- Direct external resource access in frontend code
- External resource URLs returned by backend
- Types of CDN resources: e.g., at.alicdn.com, luban.zcycdn.com, sitecdn.zcycdn.com
Examples of hardcoded CDN URLs:
<link rel='stylesheet' href='//at.alicdn.com/t/fontnm.css' />
<img src="https://sitecdn.zcycdn.com/f2e/8.png" alt="recipient"/>
<!-- CSS font files -->
src: url(https://sitecdn.zcycdn.com/t/font_148178j4i.eot);
src: url(https://sitecdn.zcycdn.com/t/font1_4i.woff);
Solution Evaluation
Option 1: DNS Resolution Forwarding
Use DNS to resolve external CDN domain names to local server IP addresses. This avoids code changes but requires client DNS configuration and annual SSL certificate updates.
Key Limitations:
- HTTPS requires valid certificates for resolved domains
- Client must maintain DNS and certificate configurations
- Tested locally with DNS server and static asset server, confirming HTTP works but HTTPS fails without valid certificates
Option 2: Build-Time Static Asset Replacement
Scan and replace external CDN URLs in code during build, download assets from external CDNs, and upload them to the client's local server.
Advantages:
- No client-side configuration needed
- Handles new CDN domains automatical in future builds Disadvantages:
- Requires code modification during build
Implementation Steps for Option 2
We break the process in to 4 phases:
1. Pre-Environment Verification
// Reusable command executor
const runCmd = (cmd, args, options = {}, before, after) => {
return new Promise((resolve, reject) => {
console.log(before || `Executing ${cmd} ${args.join(' ')}`)
const spawn = require('child_process').spawn(cmd, args, {
cwd: process.env.WORKSPACE || process.cwd(),
stdio: 'inherit',
shell: true,
...options
})
spawn.on('error', reject)
spawn.on('close', (code) => {
if (code !== 0) {
return reject(`Command failed: ${cmd} ${args.join(' ')}`)
}
after && console.log(after)
resolve()
})
})
}
// Switch to internal NPM registry
await runCmd('nrm', ['use', 'zcy-internal'], {},
'Switching NPM registry to internal',
'NPM registry switched to internal successfully'
)
// Install dependencies
await runCmd('npm', ['install', '--unsafe-perm'], {},
'Installing project dependencies',
'Dependencies installed successfully'
)
2. Compilation
Dynamically modify Webpack's publicPath for different environments:
const fs = require('fs')
const updatePublicPath = (configFile, targetPath) => {
const content = fs.readFileSync(configFile, 'utf8')
const updatedContent = content.replace(
/assetsPublicPath:.*,/g,
`assetsPublicPath: '${targetPath}',`
)
fs.writeFileSync(configFile, updatedContent, 'utf8')
}
// Example: Set development public path
updatePublicPath('config/webpack.dev.conf.js', 'http://192.168.1.100:8080')
// Build the project
await runCmd('npm', ['run', 'build'], {},
'Building project',
'Project built successfully'
)
3. Static Asset Replacement
const fs = require('fs')
const path = require('path')
const url = require('url')
const replaceStaticAssets = (filePath, localCdn, projectName, assetsCollector) => {
let content = fs.readFileSync(filePath, 'utf8')
const cdnPatterns = [
/(https?:)?\/\/at\.alicdn\.com\/[-\w@:%+.~#?&\/=]+\.[-\w@:%+.~#?&\/=]+/g,
/(https?:)?\/\/sitecdn\.zcycdn\.com\/[-\w@:%+.~#?&\/=]+\.[-\w@:%+.~#?&\/=]+/g,
/(https?:)?\/\/cdn\.zcycdn\.com\/[-\w@:%+.~#?&\/=]+\.[-\w@:%+.~#?&\/=]+/g
]
cdnPatterns.forEach(pattern => {
content = content.replace(pattern, (match) => {
let fullUrl = match.startsWith('http') ? match : `https:${match}`
const parsed = url.parse(fullUrl)
const fileName = path.basename(parsed.pathname)
assetsCollector({ src: fullUrl, fileName })
const newPath = `/${projectName}${parsed.path}${parsed.hash || ''}`
return `${localCdn}${newPath}`
})
})
fs.writeFileSync(filePath, content, 'utf8')
}
// Collect and download assets
const downloadAssets = async (assets, targetDir) => {
const fs = require('fs')
const path = require('path')
const http = require('https')
const downloadDir = path.join(targetDir, 'static-assets')
if (!fs.existsSync(downloadDir)) {
fs.mkdirSync(downloadDir, { recursive: true })
}
const downloadFile = (url, savePath) => {
return new Promise((resolve, reject) => {
const dir = path.dirname(savePath)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
const fileStream = fs.createWriteStream(savePath)
http.get(url, (res) => {
res.pipe(fileStream)
res.on('end', () => {
fileStream.close()
resolve()
})
}).on('error', reject)
})
}
for (const asset of assets) {
const savePath = path.join(downloadDir, asset.fileName)
await downloadFile(asset.src, savePath)
console.log(`Downloaded ${asset.fileName} from ${asset.src}`)
}
}
// Usage example
const assets = []
replaceStaticAssets('dist/index.html', 'http://192.168.1.100:8080', 'my-project', (asset) => assets.push(asset))
downloadAssets(assets, 'dist')
4. Upload to OSS
const OSS = require('ali-oss')
const uploadToOSS = async (localPath, remotePath) => {
const client = new OSS({
region: process.env.OSS_REGION,
accessKeyId: process.env.OSS_ACCESS_KEY,
accessKeySecret: process.env.OSS_ACCESS_SECRET,
bucket: process.env.OSS_BUCKET
})
try {
const result = await client.put(remotePath, localPath)
console.log(`File uploaded to ${result.url}`)
return result.url
} catch (error) {
console.error('Upload failed:', error)
throw error
}
}
// Upload downloaded assets
const uploadStaticAssets = async (assetsDir, projectName) => {
const fs = require('fs')
const path = require('path')
const files = fs.readdirSync(assetsDir, { recursive: true })
for (const file of files) {
const localPath = path.join(assetsDir, file)
if (fs.lstatSync(localPath).isFile()) {
const remotePath = `${projectName}/static-assets/${file}`
await uploadToOSS(localPath, remotePath)
}
}
}
uploadStaticAssets('dist/static-assets', 'my-project')