Background of the project
With the continuous development of the front-end business, the degree of restoration of the design draft by the front-end has also become a key indicator that affects the user's experience of the product. As the research and development closest to the user side, front-end engineers usually need to cooperate with designers and classmates to improve the user experience. . Among them, design walk-through is the most common way for design students to test whether front-end students have perfectly restored their design concepts. This article aims to summarize some infrastructure in the front-end upstream R&D link through the practice of the design walk-through platform on the front-end side. , in order to provide some practical ideas for other students who have related needs.
Program
The main goal of a front-end engineer is to find a framework that is close to the native browser, and svelte is your best choice.
The front-end architecture selection, from the perspective of the overall product business form, is a relatively simple single-page form, and considering the plug-in ecology to support the browser, the front-end part chose to use the svelte framework solution. The selection svelte
as the front-end technology selection of this business mainly considers the following two reasons: one is that the business form is relatively simple, and there is only one page for uploading pictures; It is more inclined to native js, and the use of large frameworks is somewhat overkill. Considering the rapid development of svelte
in recent years, small businesses still consider using it as a framework. The idea of using bitmasks to check dirty values during compilation can actually be used as a reference for framework developers. (ps: For students who are interested in this, you can look at the introduction of the emerging front-end framework Svelte from entry to principle ), but the current development is still relatively early, and the entire ecology is relatively incomplete. The vast number of developers provide a good blue ocean space. For example, there are no similar Element UI
and Ant Design
such a very useful component library system, although there are several, but personal It feels very difficult to use. The author has made a simple encapsulation of several components that need to be used in this project. Students who need it can refer to it for reference.
Table of contents
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
practice
The design walk-through platform provides a management platform and corresponding Chrome plug-ins, which can be provided to test and UI students for image comparison, improving R&D efficiency
source code
The author of svelte -- Rich Harris , the well-known front-end wheel brother, is also the author of rollup , so in this project, he chose rollup
as a tool for packaging and building, and at the same time, in order to publish the Chrome plug-in to the intranet (ps : This project is mainly used for the internal infrastructure application of the project, so it is not released on the public cloud and the official Chrome platform, and the server side involves the comparison and calculation of pictures); two scripts are built in the scripts directory, one is used to generate ts, One is used to send compressed packages to the cloud platform; because the component library ecology of svelte
is relatively not particularly rich (ps: common in the industry has been open source svelte
component library can refer to this article Svelte's UI component library ), after comparing several related component libraries in the industry, I decided to implement the components that need to be used in the business. The specific components are placed in the components directory
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
The object repository upload of the private cloud platform is performed through the minio library, and the archiver is mainly used for compression
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 script
fs.unlinkSync(path.join(__filename))
// Check for Mac's DS_store file, and if it's the only one left remove it
const 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 empty
if (fs.readdirSync(path.join(__dirname)).length === 0) {
// Remove the scripts folder
fs.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(上传)
several components
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
Commonly used icons are mainly introduced through 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
The general tool library mainly encapsulates some tool functions needed for pictures and functional programming
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'] = fileBase64
break;
case 'code':
uploaders[1]['url'] = fileBase64
break;
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.filename
break;
case 'code':
uploaders[1]['filename'] = response.data.filename
break;
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">
<Upload
id={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>
Summarize
As front-end engineers, we are the closest R&D students to the user side, not only in completing the code implementation of the business, but also different from other R&D students, we also undertake the important responsibility of product user experience, and the degree of page restoration is one It is an important indicator that allows design students to recognize the work we have completed and is also a dimension to evaluate everyone's front-end capabilities. ! !
refer to
- Svelte Chinese Documentation
- A detailed explanation of Svelte
- Dry goods | Ctrip ticket front-end Svelte production practice
- Svelte principle analysis and evaluation
- Emerging front-end framework Svelte from entry to principle
- Known as the "Big Three" alternative, how Svelte simplifies web development
- The good visual restoration and contrast tool that the design ladies and sisters all said
- Developing Chrome Extensions with Svelte
- Detailed explanation of the manifest.json file of the Chrome plugin
- 30 minutes to develop a browser plug-in that captures website image resources
- Learn Chrome plugin development in one day
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。