项目背景

随着前端业务的不断发展,前端对设计稿的还原程度也成为了影响用户对产品体验的一个关键指标,作为最靠近用户侧的研发,前端工程师通常需要和设计师同学通力配合来提升用户体验。其中,设计走查是设计同学最常见的测试前端同学是否完美还原了自己设计理念的方式,本文旨在通过设计走查平台在前端侧的实践总结下在前端上游研发链路中的一些基础建设,以期能够为其他有相关需要的同学提供一些实践思路。

方案

一个前端工程师的主要目标是寻找一款贴近浏览器原生的框架,svelte 是你不二的选择。

前端架构选型,从整体的产品业务形态来看,属于较为简单的单一页面形态,并且考虑到要支持浏览器的插件生态,因而前端部分选择使用了svelte的框架方案。选择 svelte 作为本业务的前端技术选型主要考虑到以下两方面原因:一是考虑到业务形态较为简单,只有一个上传图片的页面;二是由于浏览器插件相关的编写还是更加偏向原生js一些,使用大型框架有些大材小用。综合近几年 svelte 的迅猛发展,小型业务还是考虑使用它作为一个框架使用的,其在编译时利用位掩码做的脏值检查的思路其实还是可以作为框架开发者借鉴的一个思路的(ps:对这个感兴趣的同学,可以看一下新兴前端框架 Svelte 从入门到原理这篇文章的介绍),但是目前发展相对还是比较初期,整个生态相对还不够完善,同时也是给广大开发者提供了很好的蓝海空间,比如:目前还没有出现类似 Element UIAnt 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 productiondev: !production}}),// we'll extract any component CSS out into// a separate file - better for performancecss({ 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/commonjsresolve({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), minifyproduction && 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:<script lang="ts">export let name: string;</script>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("<script>", '<script lang="ts">')
appFile = appFile.replace("export let name;", 'export let name: string;')
fs.writeFileSync(appSveltePath, appFile)// Edit rollup config
const rollupConfigPath = path.join(projectRoot, "rollup.config.js")
let rollupConfig = fs.readFileSync(rollupConfigPath, "utf8")// Edit imports
rollupConfig = rollupConfig.replace(`'rollup-plugin-terser';`, `'rollup-plugin-terser';
import sveltePreprocess from 'svelte-preprocess';
import typescript from '@rollup/plugin-typescript';`)// Replace name of entry point
rollupConfig = rollupConfig.replace(`'src/main.js'`, `'src/main.ts'`)// Add preprocessor
rollupConfig = rollupConfig.replace('compilerOptions:','preprocess: sveltePreprocess({ sourceMap: !production }),\n\t\t\tcompilerOptions:'
);// Add TypeScript
rollupConfig = rollupConfig.replace('commonjs(),','commonjs(),\n\t\ttypescript({\n\t\t\tsourceMap: !production,\n\t\t\tinlineSources: !production\n\t\t}),'
);
fs.writeFileSync(rollupConfigPath, rollupConfig)// Add TSConfig
const tsconfig = `{"extends": "@tsconfig/svelte/tsconfig.json","include": ["src/**/*"],"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
}`
const tsconfigPath =  path.join(projectRoot, "tsconfig.json")
fs.writeFileSync(tsconfigPath, tsconfig)// Delete this script, but not during testing
if (!argv[2]) {// Remove the scriptfs.unlinkSync(path.join(__filename))// Check for Mac's DS_store file, and if it's the only one left remove itconst remainingFiles = fs.readdirSync(path.join(__dirname))if (remainingFiles.length === 1 && remainingFiles[0] === '.DS_store') {fs.unlinkSync(path.join(__dirname, '.DS_store'))}// Check if the scripts folder is emptyif (fs.readdirSync(path.join(__dirname)).length === 0) {// Remove the scripts folderfs.rmdirSync(path.join(__dirname))}
}// Adds the extension recommendation
fs.mkdirSync(path.join(projectRoot, ".vscode"), { recursive: true })
fs.writeFileSync(path.join(projectRoot, ".vscode", "extensions.json"), `{"recommendations": ["svelte.svelte-vscode"]
}
`)console.log("Converted to TypeScript.")if (fs.existsSync(path.join(projectRoot, "node_modules"))) {console.log("\nYou will need to re-run your dependency manager to get started.")
}

components

项目中用到了一些通用组件,借鉴了下Element UI的组件样式和思路,主要封装了 Button(按钮)Dialog(对话框)Icon(图标)Input(输入框)Message(消息)Tooltip(提示工具)Upload(上传) 几个组件

Button.svelte

<script>import Icon from './Icon.svelte';import { createEventDispatcher } from 'svelte';const dispatch = createEventDispatcher();export let icon, type='default';function handleClick() {dispatch('click')}function computedButtonClass(type) {switch (type) {case 'primary':return 'button button-primary';case 'default':return 'button button-default';case 'text':return 'button button-text';default:return 'button button-default';}}
</script><style>.button {border: 0;border-radius: 2px;}.button:hover {cursor: pointer;}.button-primary {background-color: rgb(77, 187, 41, 1);color: white;border: 1px solid rgb(77, 187, 41, 1);}.button-primary:hover {background-color: rgba(77, 187, 41,.8);}.button-default {background-color: white;color: #999;border: 1px solid #e0e0e0;}.button-default:hover {background-color: rgba(77, 187, 41,.1);color: rgba(77, 187, 41,.6);border: 1px solid #e0e0e0;}.button-text {background-color: transparent;border: none;}
</style><button class={computedButtonClass(type)} on:click={handleClick}>{#if icon}<Icon name="{icon}" />{/if}<span><slot></slot></span>
</button>

Dialog.svelte

<script>
import Icon from "./Icon.svelte";
import Button from "./Button.svelte";
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let title, visible = false;
function handleClose() {visible = false;
}
function handleShade() {visible = false;
}
function handleCancel() {visible = false;
}
function handleSubmit() {dispatch('submit')
}
</script><style>
.dialog-wrapper {width: 100vw;height: 100vh;position: absolute;z-index: 100000;background-color: rgba(0, 0, 0, .3);
}.dialog {width: 400px;height: max-content;background-color: white;box-shadow: 0 0 10px #ececec;position: absolute;z-index: 100001;border-radius: 2px;margin: auto;top: 0;bottom: 0;left: 0;right: 0;
}
.dialog-header {display: flex;align-items: center;justify-content: space-between;border-bottom: 1px solid #ececec;padding: 10px;
}.dialog-header .dialog-title {font-size: 16px;
}.dialog-body {padding: 10px;
}.dialog-footer {display: flex;justify-content: right;padding: 10px;
}
</style>{#if visible}
<div class="dialog-wrapper" on:click={handleShade}></div>
<div class="dialog"><div class="dialog-header"><slot name="title"><span class="dialog-title">{ title }</span></slot><Button type="text" on:click={handleClose}><Icon name="iconclose"  /></Button></div><div class="dialog-body"><slot></slot></div><div class="dialog-footer"><div class="dialog-button-group"><Button on:click={handleCancel}>取消</Button><Button type="primary" on:click={handleSubmit}>确定</Button></div></div>
</div>
{/if}

Icon.svelte

常用的icon主要通过iconfont来引入

<script>export let name;
</script><i class="{`iconfont ${name}`}"></i>

Input.svelte

<script>export let value;import { createEventDispatcher } from 'svelte';const dispatch = createEventDispatcher();function handleChange() {dispatch('input')}
</script><style>.input {border: 1px solid rgb(77, 187, 41, 1);width: 100px;}.input:focus {border: 1px solid rgb(77, 187, 41, .5);outline: none;}
</style><input class="input" bind:value={value} on:change={handleChange} />

Message.svelte

<script>
import Icon from './Icon.svelte';
export let type = 'info', show = false;const computedMessageClass = type => {switch (type) {case 'success':return 'message message-success';case 'warning':return 'message message-warning';case 'info':return 'message message-info';case 'error':return 'message message-error';default:return 'message message-info';}
}const computedIconName = type => {switch (type) {case 'success':return 'iconsuccess';case 'warning':return 'iconwarning';case 'info':return 'iconinfo';case 'error':return 'iconerror';default:return 'iconinfo';}
}
</script><style>
.message {position: absolute;z-index: 100000;width: 200px;height: max-content;left: 0;right: 0;margin: auto;padding: 4px 10px;animation: show 2s ease-in-out forwards;display: flex;justify-content: left;align-items: center;border-radius: 4px;
}.message-content {margin-left: 10px;
}@keyframes show {from {opacity: 1;top: 0;}to {opacity: 0;top: 100px;}
}.message-success {background-color: rgba(77, 187, 41, .2);color: rgba(77, 187, 41, 1);
}.message-info {background-color: rgb(144, 147, 153, .2);color: rgb(144, 147, 153, 1);
}.message-warning {background-color: rgb(230, 162, 60, .2);color: rgb(230, 162, 60, 1);
}.message-error {background-color: rgb(245, 108, 108, .2);color: rgb(245, 108, 108, 1);
}
</style>{#if show}
<div class={computedMessageClass(type)}><Icon name={computedIconName(type)} /><p class="message-content"><slot></slot></p>
</div>
{/if}

Tooltip.svelte

<script>export let content, tooltip;</script><style>
.tooltip {position: relative;
}.tooltip .tip-container {position: absolute;background: #666;padding: 0 10px;border-radius: 4px;right: -180px;top: 50%;margin-top: -24px;
}.tip-container .tip-triple {width: 0;height: 0;border: 8px solid transparent;border-right-color: #666;position: absolute;left: -16px;top: 16px;
}.tip-container .tip-content {line-height: 24px;font-size: 12px;color: white;
}
</style><div class="tooltip"><slot class="tip-component"></slot>{#if tooltip}<div class="tip-container"><div class="tip-triple"></div><p class="tip-content">{content}</p></div>{/if}
</div>

Upload.svelte

<script>export let action, onSuccess, beforeUpload, id;function ajax(options) {const xhr = new XMLHttpRequest();const action = options.action;let fd = new FormData();fd.append(options.filename, options.file);xhr.onerror = function (err) {console.error(err)}xhr.onload = function() {const text = xhr.responseText || xhr.response;console.log('text', text)text && options.success(JSON.parse(text))}xhr.open('post', action, true);xhr.send(fd);return xhr;}function post(rawFile) {const options = {id: id,file: rawFile,filename: 'img',action: action,success: res => onSuccess(res, rawFile, id)}const req = ajax(options);if(req && req.then) {req.then(options.onSuccess)}}async function handleChange(e) {const rawFile = e.target.files[0];if(!beforeUpload) {return post(rawFile)}let flag = await beforeUpload(rawFile, id);if(flag) post(rawFile)}function handleClick() {const plus = document.getElementById(id);plus.value = '';plus.click()}
</script><style>.upload {width: 250px;height: 250px;border: 1px dashed #ececec;text-align: center;display: flex;justify-content: center;align-items: center;}.upload:hover {cursor: pointer;}.native-input {display: none;}
</style><div class="upload" on:click={handleClick}><div class="upload-slot" ><slot></slot></div><input {id} class="native-input" type="file" multiple="false" accept="image/png" on:change={handleChange} />
</div>

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

<script>import Icon from './components/Icon.svelte';import Button from './components/Button.svelte';import Upload from './components/Upload.svelte';import Input from './components/Input.svelte';import Message from './components/Message.svelte';import Tooltip from './components/Tooltip.svelte';import axios from 'axios';import { getBase64, getPixel } from './utils';const bgUrl = './bg.jpeg',logoUrl = './favicon.png',actionUrl = '',compareUrl = '',downloadUrl = '',crxUrl = '';let width = 0, height = 0, flag, compareName, errorMsg, successMsg, show, tooltip;let uploaders = [{id: 'design',title: '设计图',filename: '',url: ''},{id: 'code',title: '实现图',filename: '',url: ''}];const handleCompare = () => {show = false;const len = uploaders.filter(f => !!f.filename).length;if(len == 2) {axios.post(compareUrl, {designName: uploaders[0]['filename'],codeName: uploaders[1]['filename'],}).then(res => {console.log('compare', res)if(res.data.success) { compareName = res.data.data.compareName;return true} else {flag = 'error';show = true;errorMsg = res.data.data} }).then(c => {if(c) {flag = 'success';successMsg = '对比成功';show = true;handleDownload()handleDelete()}})} else if(len == 1) {window.alert('设计图或开发图缺少,请确认已全部上传后再进行比较!')} else {window.alert('必须有图片才能进行比较!')}};const handleBeforeUpload = async function(rawFile, id) {const fileBase64 = await getBase64(rawFile);const res = await getPixel(fileBase64);// console.log('res', res)if(res.width == width && res.height == height) {switch (id) {case 'design':uploaders[0]['url'] = fileBase64break;case 'code':uploaders[1]['url'] = fileBase64break;default:break;}return true;} else {window.alert('上传图片不符合分比率要求');return false;}}const handleSuccess = (response, rawFile, id) => {console.log('response', response, rawFile, id);if(response.success) {switch (id) {case 'design':uploaders[0]['filename'] = response.data.filenamebreak;case 'code':uploaders[1]['filename'] = response.data.filenamebreak;default:break;}}}function handleDownload() {axios({method: 'POST',url: downloadUrl,responseType: 'blob',data: {compareName: compareName}}).then(res => {console.log('download', res)if(res.status == 200) {var blob = new Blob([res.data]);// 创建一个URL对象var url = window.URL.createObjectURL(blob);console.log('url', url)// 创建一个a标签var a = document.createElement("a");a.href = url;a.download = compareName;// 这里指定下载文件的文件名a.click();// 释放之前创建的URL对象window.URL.revokeObjectURL(url);}})}function handleDelete() {uploaders = [{id: 'design',title: '设计图',filename: '',url: ''},{id: 'code',title: '实现图',filename: '',url: ''}];}
</script><style>.pixel-piper {width: 100%;height: 100%;display: flex;justify-content: center;align-items: center;}.main {width: 600px;margin: 0 auto;padding: 20px;border: 1px solid #eee;background-color: #fff;border-radius: 4px;box-shadow: 0 0 10px #e0e0e0;}.main .logo-container {display: flex;justify-content: center;align-items: center;}.logo-container .logo:hover {opacity: 90%;cursor: pointer;}.main .select-container {display: flex;align-items: center;align-content: center;justify-content: center;justify-items: center;line-height: 40px;}.main .upload-container {display: flex;padding: 0 0 10px 0;justify-content: space-between;text-align: center;}.main .button-container {display: flex;justify-content: center;align-items: center;}.main .info-container {text-align: center;color: red;font-size: 12px;margin: 10px 0;}
</style><div class="pixel-piper" style="{`background: url(${bgUrl}) no-repeat; background-size: cover`}"><section class="main"><div class="logo-container"><Tooltip content="点击logo可下载chrome插件" {tooltip}><a href={crxUrl}><img class="logo" src={logoUrl} alt="logo" width="100px" on:mouseenter={() => tooltip=true} on:mouseleave={() => tooltip=false}></a></Tooltip></div><div class="select-container"><p class="select-name"><Input bind:value={width} /> x <Input bind:value={height} /> </p> </div><div class="upload-container">{#each uploaders as uploader}<div class="uploader"><Uploadid={uploader.id}onSuccess={handleSuccess} beforeUpload={handleBeforeUpload} action={actionUrl}>{#if !uploader.url}<Icon name="iconplus" />{:else}<img class="uploader-image" style="object-fit: contain;" width="250" height="250" src={uploader.url} alt={uploader.id} />{/if}</Upload><span class="uploader-title">{uploader.title}</span></div>{/each}</div><div class="info-container">{#if uploaders.filter(f => !!f.filename).length == 2}<span class="info-tips">注:请在两分钟内进行图片对比!!</span>{/if}</div><div class="button-container"><Button icon="iconposition" on:click={handleCompare} type="primary">对比</Button>{#if uploaders.filter(f => !!f.filename).length == 2}<div style="margin-left: 10px"><Button  icon="icondelete" on:click={handleDelete} type="default">清除图片</Button></div>{/if}</div></section>{#if flag == 'success'}<Message type="success" {show} >{successMsg}</Message>{:else if flag == 'error'}<Message type="error" {show} >{errorMsg}</Message>{/if}
</div>

总结

作为前端工程师,我们是距离用户侧最近的研发同学,不仅仅在于完成业务的代码实现,而且不同于其他研发同学,我们也承担着产品用户体验的重要职责,而这其中页面的还原程度是一项重要的指标,能够让设计同学认可我们完成的工作也是评价大家前端能力的一个维度,毕竟上下游通力合作,才能把产品体验做到最佳,共勉!!!

参考

  • Svelte 中文文档
  • 一文详解 Svelte
  • 干货 | 携程机票前端Svelte生产实践
  • Svelte 原理浅析与评测
  • 新兴前端框架 Svelte 从入门到原理
  • 被称为“三大框架”替代方案,Svelte如何简化Web开发工作
  • 设计小姐姐都说好的视觉还原对比利器
  • 使用Svelte开发Chrome Extension
  • Chrome插件manifest.json文件详解
  • 30分钟开发一款抓取网站图片资源的浏览器插件
  • 一天学会Chrome插件开发

前端设计走查平台实践(前端篇)相关推荐

  1. 前端设计走查平台实践(后端篇)

    项目背景 随着业务的不断发展,研发链路的效能提升也是一个至关重要的指标,其中对前端工程基建而言,其上游部分主要是和设计师同学打交道,而在整个研发链路中,通常会有设计走查的流程来让设计师同学辅助测试同学 ...

  2. vivo浏览器的快速开发平台实践-总览篇

    一.什么是快速开发平台 快速开发平台,顾名思义就是可以使得开发更为快速的开发平台,是提高团队开发效率的生产力工具.近一两年,国内很多公司越来越注重研发效能的度量和提升,基于软件开发的特点,覆盖管理和优 ...

  3. 微前端在平台级管理系统中的最佳实践

    微前端在平台级管理系统中的最佳实践 一.什么是微前端 二.什么是通用管理端工程 三.当管理端工程遇上微前端 四.未来展望 作者:杨朋飞 一.什么是微前端 近十年来,前端技术有了长足发展,各种概念与框架 ...

  4. 监控平台前端SDK开发实践

    监控是提高故障处理能力和保障服务质量必需的一环,它需要负责的内容包括:及时上报错误.收集有效信息.提供故障排查依据. 及时上报错误:发生线上问题后,经由运营或者产品反馈到开发人员,其中流转过程可能是几 ...

  5. 解密国内BAT等大厂前端技术体系-腾讯篇(长文建议收藏)

    1 引言 为了了解当前前端的发展趋势,让我们从国内各大互联网大厂开始,了解他们的最新动态和未来规划.这是解密大厂前端技术体系的第三篇,前两篇已经讲述了阿里和百度在前端技术这几年的技术发展.这一篇从腾讯 ...

  6. 转载《美团点评金融平台Web前端技术体系》

    复制代码 作者:禹霖 原文地址: tech.meituan.com/2018/03/16/- 背景 随着美团点评金融业务的高速发展,前端研发数量从 2015 年的 1 个人,扩张到了现在横跨北上两地 ...

  7. 死磕前端架构之整洁架构在前端的应用实践【稀缺资源】

    在2202年的今天,前端应用走向了 MV* 的架构方案,有了一层很重的 View.随着业务场景的越来越专业化和复杂化,大型 SPA 应用的流行,前端承担的职责也越来越多.即使在精心设计过的架构,也很容 ...

  8. 生鲜 B2B 技术平台的前端团队该如何搭建

    此文写于 1 年前,转载至此,大家可以加 Scott 微信: codingdream 成为朋友圈的朋友,聊南聊北,哈哈哈. ![图片描述][1] 线下越重,线上需要越轻,这个轻指的是轻便轻巧和简洁易用 ...

  9. web前端开发最佳实践_学习前端Web开发的最佳方法

    web前端开发最佳实践 为什么要进行网站开发? (Why web development?) Web development is a field that is not going anywhere ...

最新文章

  1. mysql的count报错_mysql的floor()报错注入方法详细分析
  2. mysql 5.7配置多线程复制,MySQL5.7复制功能实战,基于事务的复制,多源复制和多线程复制配置...
  3. 基本概念_复杂网络基本概念
  4. oracle服务器错误,oracle 11g数据库维护中错误总结
  5. eos和以太坊有什么关系_比特币、以太坊、柚子三者的关系
  6. android变量要不要附空值,android-如何在使用Parcelable时序列化空值
  7. java获取类的信息
  8. wordpress胖鼠采集去限制版
  9. php smtp邮件类,php利用smtp类发送邮件
  10. 【JAVA SE】第十六章 进程、线程、同步锁和线程锁的简介
  11. antd vue form表单 子组件调用父组件的方法没反应_前几天推了Vue,今天给React疯狂打call...
  12. hadoop ubantu环境搭建_创帆云大数据教程系列1-搭建基于docker的hadoop环境安装规划、容器通信及zookeeper...
  13. ubuntu美化之conky美化
  14. [BZOJ1597]土地购买
  15. NoteExpress 自定义参考文献输出样式
  16. Power BI 精美的可视化图表
  17. hololens共享视野的例子记录
  18. C语言LMS双麦克风消噪算法,芯片内部的噪声抑制算法,语音芯片来说也是一样(双麦克风降噪理念)...
  19. 自动驾驶道路曲率计算
  20. 10招有效预防电脑辐射

热门文章

  1. 新能源汽车,火力全开
  2. 对BitMap和布隆过滤器的理解
  3. 中国互联网走在“灰度”上
  4. 玩转SAP-无物料费用单的采购订单的配置方法
  5. JavaScript正反选
  6. 【捷哥浅谈PHP】第三弹---使用二分查找法查找数组中的元素位置
  7. kratos mysql_Kratos
  8. 最炫民族风70个版本大合集
  9. 基于MATLAB碎片拼接
  10. 【Windows】运行