项目背景
随着前端业务的不断发展,前端对设计稿的还原程度也成为了影响用户对产品体验的一个关键指标,作为最靠近用户侧的研发,前端工程师通常需要和设计师同学通力配合来提升用户体验。其中,设计走查是设计同学最常见的测试前端同学是否完美还原了自己设计理念的方式,本文旨在通过设计走查平台在前端侧的实践总结下在前端上游研发链路中的一些基础建设,以期能够为其他有相关需要的同学提供一些实践思路。
方案
一个前端工程师的主要目标是寻找一款贴近浏览器原生的框架,svelte 是你不二的选择。
前端架构选型,从整体的产品业务形态来看,属于较为简单的单一页面形态,并且考虑到要支持浏览器的插件生态,因而前端部分选择使用了svelte的框架方案。选择 svelte
作为本业务的前端技术选型主要考虑到以下两方面原因:一是考虑到业务形态较为简单,只有一个上传图片的页面;二是由于浏览器插件相关的编写还是更加偏向原生js一些,使用大型框架有些大材小用。综合近几年 svelte
的迅猛发展,小型业务还是考虑使用它作为一个框架使用的,其在编译时利用位掩码做的脏值检查的思路其实还是可以作为框架开发者借鉴的一个思路的(ps:对这个感兴趣的同学,可以看一下新兴前端框架 Svelte 从入门到原理这篇文章的介绍),但是目前发展相对还是比较初期,整个生态相对还不够完善,同时也是给广大开发者提供了很好的蓝海空间,比如:目前还没有出现类似 Element UI
和 Ant Design
这种十分好用的组件库系统,虽然有几个,但是个人感觉很不好用,作者在本项目中对需要用到的几个组件做了简单的封装,有需要的同学可以参考借鉴一下。
目录
public
- build
- bg.jpeg
- favicon.png
- global.css
- index.html
- manifest.json
scripts
- setupCrxScript.js
- setupTypeScript.js
src
components
- Button.svelte
- Dialog.svelte
- Icon.svelte
- Input.svelte
- Message.svelte
- Tooltip.svelte
- Upload.svelte
utils
- function.js
- image.js
- index.js
- App.svelte
- main.js
- rollup.config.js
实践
设计走查平台提供了管理平台及对应的Chrome插件,可提供给测试及UI同学用于图片的比对,提升研发效率
源码
svelte作者--前端著名的轮子哥Rich Harris,同时也是rollup的作者,因而本项目中就选择了 rollup
作为打包构建的工具,同时为了将Chrome插件发布到内网中(ps:本项目主要用于项目内部基建应用,因而未在公有云及Chrome官方平台去发布,服务端涉及到了图片的比对计算);在scripts目录下内置了两个脚本,一个用于生成ts,一个用于向云平台发送压缩包;由于 svelte
的组件库生态相对不是特别丰富(ps:业界常见已经开源的 svelte
组件库可以参看这篇文章Svelte的UI组件库),对比了业界几个相关的组件库后,决定自己实现下业务中需要用到的组件,具体组件放在了components目录下
rollup.config.js
import svelte from 'rollup-plugin-svelte';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import livereload from 'rollup-plugin-livereload';
import { terser } from 'rollup-plugin-terser';
import css from 'rollup-plugin-css-only';
const production = !process.env.ROLLUP_WATCH;
function serve() {
let server;
function toExit() {
if (server) server.kill(0);
}
return {
writeBundle() {
if (server) return;
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
stdio: ['ignore', 'inherit', 'inherit'],
shell: true
});
process.on('SIGTERM', toExit);
process.on('exit', toExit);
}
};
}
export default {
input: 'src/main.js',
output: {
sourcemap: true,
format: 'iife',
name: 'app',
file: 'public/build/bundle.js'
},
plugins: [
svelte({
compilerOptions: {
// enable run-time checks when not in production
dev: !production
}
}),
// we'll extract any component CSS out into
// a separate file - better for performance
css({ output: 'bundle.css' }),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte']
}),
commonjs(),
// In dev mode, call `npm run start` once
// the bundle has been generated
!production && serve(),
// Watch the `public` directory and refresh the
// browser on changes when not in production
!production && livereload('public'),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser()
],
watch: {
clearScreen: false
}
};
scripts
setupCrxScript.js
通过minio这个库来进行私有云平台的对象存储库上传,archiver这个主要用于压缩
const fs = require('fs');
const path = require('path');
const archiver = require('archiver');
const Minio = require('minio');
const minio = new Minio.Client({
endPoint: '',
port: 80,
useSSL: false,
accessKey: '',
secretKey: ''
})
const output = fs.createWriteStream(path.resolve(__dirname,'../pixelpiper.zip'));
const archive = archiver('zip', {
zlib: { level: 9 }
});
output.on('close', function() {
console.log(archive.pointer() + ' total bytes');
console.log('archiver has been finalized and the output file descriptor has closed.');
// 压缩完成后向 cdn 中传递压缩包
const file = path.resolve(__dirname, '../pixelpiper.zip');
fs.stat(file, function(error, stats) {
if(error) {
return console.error(error)
}
minio.putObject('cdn', 'pixelpiper.zip', fs.createReadStream(file), stats.size, 'application/zip', function(err, etag) {
return console.log(err, etag) // err should be null
})
})
});
output.on('end', function() {
console.log('Data has been drained');
});
archive.on('warning', function(err) {
if (err.code === 'ENOENT') {
} else {
throw err;
}
});
archive.on('error', function(err) {
throw err;
});
archive.pipe(output);
archive.directory(path.resolve(__dirname, '../public'), false);
archive.finalize();
setupTypeScript.js
// @ts-check
/** This script modifies the project to support TS code in .svelte files like:
As well as validating the code for CI.
*/
/** To work on this script:
rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template
*/
const fs = require("fs")
const path = require("path")
const { argv } = require("process")
const projectRoot = argv[2] || path.join(__dirname, "..")
// Add deps to pkg.json
const packageJSON = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8"))
packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, {
"svelte-check": "^1.0.0",
"svelte-preprocess": "^4.0.0",
"@rollup/plugin-typescript": "^8.0.0",
"typescript": "^4.0.0",
"tslib": "^2.0.0",
"@tsconfig/svelte": "^1.0.0"
})
// Add script for checking
packageJSON.scripts = Object.assign(packageJSON.scripts, {
"validate": "svelte-check"
})
// Write the package JSON
fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(packageJSON, null, " "))
// mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too
const beforeMainJSPath = path.join(projectRoot, "src", "main.js")
const afterMainTSPath = path.join(projectRoot, "src", "main.ts")
fs.renameSync(beforeMainJSPath, afterMainTSPath)
// Switch the app.svelte file to use TS
const appSveltePath = path.join(projectRoot, "src", "App.svelte")
let appFile = fs.readFileSync(appSveltePath, "utf8")
appFile = appFile.replace("
Dialog.svelte
{#if visible}
{ title }
{/if}
Icon.svelte
常用的icon主要通过iconfont来引入
Input.svelte
Message.svelte
{#if show}
{/if}
Tooltip.svelte
{#if tooltip}
{content}
{/if}
Upload.svelte
utils
通用工具库主要封装了图片及函数式编程需要用到的一些工具函数
function.js
export const curry = (fn, arr = []) => (...args) => (
arg => arg.length === fn.length ?
fn(...arg) :
curry(fn, arg)
)([...arr, ...args]);
export const compose = (...args) => args.reduce((prev, current) => (...values) => prev(current(...values)));
image.js
export const getBase64 = file => {
const reader = new FileReader();
reader.readAsDataURL(file);
return new Promise((resolve) => {
reader.onload = () => {
resolve(reader.result);
};
});
};
export const getPixel = img => {
const image = new Image();
image.src = img;
return new Promise((resolve) => {
image.onload = () => {
const width = image.width;
const height = image.height;
resolve({ width, height });
};
});
}
App.svelte
x
{#each uploaders as uploader}
{#if !uploader.url}
{:else}
{/if}
{uploader.title}
{/each}
{#if uploaders.filter(f => !!f.filename).length == 2}
注:请在两分钟内进行图片对比!!
{/if}
{#if flag == 'success'}
{successMsg}
{:else if flag == 'error'}
{errorMsg}
{/if}
总结
作为前端工程师,我们是距离用户侧最近的研发同学,不仅仅在于完成业务的代码实现,而且不同于其他研发同学,我们也承担着产品用户体验的重要职责,而这其中页面的还原程度是一项重要的指标,能够让设计同学认可我们完成的工作也是评价大家前端能力的一个维度,毕竟上下游通力合作,才能把产品体验做到最佳,共勉!!!