Chanx's Blog前端开发 | 陈同学技术博客 | 代码破破烂烂我在缝缝补补https://chanx.techzh-CNSat, 20 Dec 2025 02:58:29 +0800Astro RSS 2.0 GeneratorCopyright 2025 Chanx's Blog[email protected] (Chanx)[email protected] (Chanx)- 如何实现自定义域名功能https://chanx.tech/blog/learn-custom-domainhttps://chanx.tech/blog/learn-custom-domain深入拆解自定义域名功能的技术实现原理:DNS 配置与验证、Nginx 动态配置与 SNI、Let's Encrypt 证书自动申请、域名状态管理、完整访问链路设计。通过 Nginx + Node.js 示例代码,帮助理解建站平台、CMS 系统的自定义域名实现机制。Thu, 18 Dec 2025 00:00:00 GMT<![CDATA[<h2>一、引言</h2>
<p>在建站产品、CMS 或低代码平台中,自定义域名功能涉及<strong>完整的访问链路管理</strong>。</p>
<p>域名不仅是一个配置字段,更是浏览器请求、DNS、网关、应用和 HTTPS 证书的交汇点。</p>
<p>本文将从工程视角拆解自定义域名功能。</p>
<hr>
<h2>二、概念</h2>
<ul>
<li><strong>自定义域名</strong>:用户为建站产品中的站点配置的专属域名,用于访问站点内容。 </li>
<li><strong>流量路由(A / CNAME)</strong>:DNS 记录类型,用于将用户浏览器请求解析到平台入口。 <ul>
<li><strong>A 记录</strong>:将域名指向平台的 IP 地址。 </li>
<li><strong>CNAME 记录</strong>:将域名指向平台的另一个域名。</li>
</ul>
</li>
<li><strong>所有权验证 / 配置声明(TXT)</strong>:DNS TXT 记录,用于校验用户对域名的控制权,或作为证书 DNS-01 校验的一部分。 </li>
<li><strong>Nginx / 网关层</strong>:处理 TLS 终端、SNI、Host 路由和流量转发到应用层。 </li>
<li><strong>SNI(Server Name Indication)</strong>:TLS 扩展,用于根据客户端请求的域名选择对应证书。 </li>
<li><strong>Node.js 应用层</strong>:处理业务逻辑和 Host 映射,根据请求 Host 决定访问的站点内容。 </li>
<li><strong>HTTPS / TLS 证书</strong>:为每个域名提供独立加密证书,用于浏览器验证和安全传输。 </li>
<li><strong>动态配置维护</strong>:通过模板、脚本或配置中心生成、更新和热加载 Nginx 配置,实现多域名自动化管理。 </li>
<li><strong>周期检测与资源回收</strong>:后台定时检查域名状态,处理异常或迁移域名,回收对应证书和 Nginx 配置。</li>
</ul>
<hr>
<h2>三、流程拆解</h2>
<ol>
<li>用户在控制台添加域名 </li>
<li>平台生成域名记录,状态为 <code>pending</code> </li>
<li>用户配置 DNS(A / CNAME 或 TXT) </li>
<li>后台轮询验证 DNS 指向 </li>
<li>DNS 生效 → 域名状态置为 <code>active</code> </li>
<li>后台异步申请 HTTPS 证书 </li>
<li>动态生成或更新 Nginx 配置 </li>
<li>流量进入入口层(Nginx / CDN) </li>
<li>应用层根据 Host 映射站点 </li>
<li>周期检测 DNS → 异常下线或回收</li>
</ol>
<hr>
<h2>四、状态机与数据结构</h2>
<h3>4.1 域名状态机</h3>
<p>自定义域名从创建到回收,经历多个状态流转。清晰的状态机设计是系统稳定运行的基础。</p>
<pre><code> ┌─────────────────────────────────────┐
│ ▼
┌─────────┐ ┌────────────┐ ┌────────┐ ┌───────────────┐
│ pending │───▶│ verifying │───▶│ active │───▶│ cert_pending │
└─────────┘ └────────────┘ └────────┘ └───────────────┘
│ │ │
│ │ ▼
│ │ ┌─────────────┐
│ │ │ cert_issued │
│ │ └─────────────┘
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ error │◀─────│ expired │◀─────│ deleted │
└─────────┘ └─────────┘ └─────────┘
</code></pre>
<table>
<thead>
<tr>
<th>状态</th>
<th>含义</th>
<th>触发条件</th>
</tr>
</thead>
<tbody><tr>
<td><code>pending</code></td>
<td>域名已创建,等待用户配置 DNS</td>
<td>用户添加域名</td>
</tr>
<tr>
<td><code>verifying</code></td>
<td>正在验证 DNS 指向</td>
<td>后台开始轮询检测</td>
</tr>
<tr>
<td><code>active</code></td>
<td>DNS 验证通过,域名已激活</td>
<td>DNS 解析指向平台</td>
</tr>
<tr>
<td><code>cert_pending</code></td>
<td>等待证书申请</td>
<td>域名激活后入队</td>
</tr>
<tr>
<td><code>cert_issued</code></td>
<td>证书已签发,完全可用</td>
<td>ACME 申请成功</td>
</tr>
<tr>
<td><code>error</code></td>
<td>验证失败或证书申请失败</td>
<td>DNS 错误/证书失败</td>
</tr>
<tr>
<td><code>expired</code></td>
<td>证书过期或 DNS 失效</td>
<td>周期检测发现异常</td>
</tr>
<tr>
<td><code>deleted</code></td>
<td>用户删除或系统回收</td>
<td>手动删除/自动回收</td>
</tr>
</tbody></table>
<h3>4.2 数据结构</h3>
<pre><code class="language-ts">/** 域名状态 */
type DomainStatus =
| 'pending' // 等待配置
| 'verifying' // 验证中
| 'active' // 已激活
| 'cert_pending' // 等待证书
| 'cert_issued' // 证书已签发
| 'error' // 错误
| 'expired' // 已过期
| 'deleted'; // 已删除
/** 站点记录 */
interface Site {
id: string;
cnameKey: string; // 随机生成的 CNAME 标识,如 cname-a3f8x2k9m4n7p1
// ...其他字段
}
/** 域名记录 */
interface Domain {
id: string;
siteId: string; // 关联站点
domain: string; // 用户自定义域名
status: DomainStatus;
dnsType?: 'A' | 'CNAME';
verifiedAt?: Date;
errorMessage?: string;
retryCount: number;
createdAt: Date;
updatedAt: Date;
}
/** 证书记录 */
interface Certificate {
id: string;
domainId: string;
domain: string; // 冗余,便于查询
certPath: string; // 证书路径
keyPath: string; // 私钥路径
issuer: string; // 颁发机构
issuedAt: Date; // 签发时间
expiresAt: Date; // 过期时间
autoRenew: boolean; // 自动续期
lastRenewedAt?: Date;
createdAt: Date;
}
/** 操作日志(可选) */
interface DomainLog {
id: string;
domainId: string;
action: 'create' | 'verify' | 'activate' | 'cert_issue' | 'cert_renew' | 'error' | 'recover' | 'delete';
fromStatus?: DomainStatus;
toStatus?: DomainStatus;
message?: string;
operator: string; // 'system' | userId
createdAt: Date;
}
</code></pre>
<h3>4.3 缓存结构设计</h3>
<p>为提高访问性能,使用 Redis 缓存域名映射关系:</p>
<pre><code class="language-js">// Key 设计
`domain:${domain}` → { siteId, status, certIssued }
`site:${siteId}:domains` → Set<domain> // 站点下所有域名
`cert:renew:queue` → SortedSet<domain, expiresAt> // 续期队列
// 示例数据
{
"domain:www.example.com": {
"siteId": "site_abc123",
"status": "cert_issued",
"certIssued": true
}
}
</code></pre>
<p><strong>缓存更新策略</strong>:</p>
<ul>
<li>域名状态变更时同步更新 Redis</li>
<li>设置合理的 TTL,防止脏数据</li>
<li>使用 Pub/Sub 同步多节点缓存</li>
</ul>
<hr>
<h2>五、DNS 配置</h2>
<p>用户添加自定义域名后,需要在其 DNS 服务商处配置解析记录,将域名流量指向平台。DNS 配置是连接用户域名与平台服务的桥梁。</p>
<p><strong>三种记录类型</strong>:</p>
<table>
<thead>
<tr>
<th>类型</th>
<th>用途</th>
<th>适用场景</th>
<th>示例</th>
</tr>
</thead>
<tbody><tr>
<td><strong>A</strong></td>
<td>域名 → IP</td>
<td>裸域名(<code>example.com</code>)</td>
<td><code>example.com → 1.2.3.4</code></td>
</tr>
<tr>
<td><strong>CNAME</strong></td>
<td>域名 → 域名</td>
<td>子域名(<code>www.example.com</code>)</td>
<td><code>www → site.platform.com</code></td>
</tr>
<tr>
<td><strong>TXT</strong></td>
<td>文本声明</td>
<td>所有权验证、证书校验</td>
<td><code>_acme → xxx</code></td>
</tr>
</tbody></table>
<p><strong>选择</strong>:</p>
<ul>
<li>裸域名只能用 A 记录(DNS 协议限制,裸域名不能设置 CNAME)</li>
<li>子域名优先用 CNAME,平台 IP 变更时用户无需修改</li>
<li>TXT 不参与流量路由,仅用于验证和声明</li>
</ul>
<p><strong>CNAME 目标生成</strong>:</p>
<p>平台为每个站点生成独立的 CNAME 目标(如 <code>cname-a3f8x2k9m4n7p1.site.platform.com</code>),需配置泛域名解析 <code>*.site.platform.com → 平台 IP</code>。</p>
<pre><code class="language-ts">import { nanoid } from 'nanoid';
const generateCnameKey = () => `cname-${nanoid(16)}`;
</code></pre>
<p><strong>DNS 校验实现</strong>:</p>
<pre><code class="language-ts">import dns from 'dns/promises';
const PLATFORM_CNAME = 'site.platform.com';
const PLATFORM_IPS = ['1.2.3.4', '5.6.7.8'];
/** 弱校验:检查域名是否指向平台 */
export async function verifyDomain(domain: string): Promise<boolean> {
// 优先检查 CNAME
try {
const cnames = await dns.resolveCname(domain);
if (cnames.some(c => c.endsWith(PLATFORM_CNAME))) return true;
} catch {}
// 兜底检查 A 记录
try {
const ips = await dns.resolve4(domain);
if (ips.some(ip => PLATFORM_IPS.includes(ip))) return true;
} catch {}
return false;
}
/** 周期轮询更新状态 */
async function pollDomains() {
const domains = await db.domains.findMany({
where: { status: { in: ['pending', 'verifying'] } }
});
for (const d of domains) {
const ok = await verifyDomain(d.domain);
await db.domains.update({
where: { id: d.id },
data: { status: ok ? 'active' : d.status }
});
}
}
</code></pre>
<hr>
<h2>六、访问链路</h2>
<p>从用户发起请求到最终渲染页面,自定义域名功能涉及完整的访问链路。本章从<strong>流量接入 → 路由分发 → 证书管理 → 配置维护 → 监控回收</strong>的视角,拆解各层技术实现。</p>
<h3>6.1 入口层</h3>
<p>当用户通过自定义域名访问时,流量首先到达 Nginx 网关层。Nginx 通过 <strong>SNI(Server Name Indication)</strong> 识别请求的域名,为每个域名加载独立的 TLS 证书。</p>
<h4>多域名 SNI + 独立证书配置</h4>
<pre><code class="language-nginx"># 域名 www.example.com
server {
listen 80;
server_name www.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 443 ssl;
server_name www.example.com;
ssl_certificate /etc/nginx/certs/www.example.com.crt;
ssl_certificate_key /etc/nginx/certs/www.example.com.key;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# 域名 blog.example.com
server {
listen 80;
server_name blog.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 443 ssl;
server_name blog.example.com;
ssl_certificate /etc/nginx/certs/blog.example.com.crt;
ssl_certificate_key /etc/nginx/certs/blog.example.com.key;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
</code></pre>
<hr>
<h3>6.2 应用层</h3>
<p>流量经 Nginx 转发到 Node.js 应用层后,需要根据请求的 <strong>Host</strong> 头确定访问的站点。</p>
<p>注意:用户配置 <code>www.example.com CNAME cname-xxx.site.platform.com</code> 后,访问时 Host 头仍是 <code>www.example.com</code>,CNAME 只影响 DNS 解析过程,不影响 HTTP 请求头。</p>
<pre><code class="language-ts">// ACME HTTP-01 验证路由
app.get('/.well-known/acme-challenge/:token', async (req, res) => {
const { token } = req.params;
const host = req.headers.host;
if (!host) return res.status(400).send('Missing host header');
// 从缓存读取该域名的 challenge
const challenge = await cache.get(`acme:${host}:${token}`);
if (!challenge) return res.status(404).send('Not found');
res.type('text/plain').send(challenge);
});
async function getSiteIdByHost(host: string): Promise<string | null> {
// 1. 先查缓存
const cached = await cache.get(`domain:${host}`);
if (cached) return JSON.parse(cached).siteId;
// 2. 缓存未命中,查数据库
const domain = await db.domains.findUnique({
where: { domain: host, status: 'cert_issued' }
});
if (!domain) return null;
// 3. 回填缓存
await cache.set(`domain:${host}`, JSON.stringify({ siteId: domain.siteId }), 'EX', 3600);
return domain.siteId;
}
app.use(async (req, res, next) => {
const host = req.headers.host;
if (!host) return res.status(400).send('Missing host header');
const siteId = await getSiteIdByHost(host);
if (!siteId) return res.status(404).send('Site not found');
req.siteId = siteId;
next();
});
// 根据 siteId 渲染站点内容
app.get('*', async (req, res) => {
const siteId = req.siteId;
const content = await db.sites.findOne({ where: { id: siteId } });
res.send(renderTemplate(content));
});
</code></pre>
<hr>
<h3>6.3 证书管理</h3>
<p>为保证访问安全,每个自定义域名需要独立的 HTTPS 证书。证书申请是异步流程,不应阻塞域名激活。</p>
<h4>异步证书申请与部署流程</h4>
<ol>
<li>域名状态 <code>active</code> → 入队申请证书任务</li>
<li>调用 ACME API(如 Let's Encrypt)申请 TLS 证书</li>
<li>保存证书文件到指定目录</li>
<li>更新 Nginx 配置引用新证书</li>
<li>校验配置(<code>nginx -t</code>)</li>
<li>平滑重载(<code>nginx -s reload</code>)</li>
</ol>
<h4>代码实现示例</h4>
<pre><code class="language-ts">import acme from 'acme-client';
import fs from 'fs';
import { exec } from 'child_process';
async function issueCertificate(domain: string) {
const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.production
});
const [key, csr] = await client.createCsr({ commonName: domain });
const cert = await client.auto({
csr,
email: '[email protected]',
termsOfServiceAgreed: true,
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
// HTTP-01: 将验证内容写入缓存
if (challenge.type === 'http-01') {
await cache.set(
`acme:${domain}:${challenge.token}`,
keyAuthorization,
'EX',
600 // 10分钟过期
);
}
},
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
// 移除缓存
if (challenge.type === 'http-01') {
await cache.del(`acme:${domain}:${challenge.token}`);
}
},
});
await fs.promises.writeFile(`/etc/nginx/certs/${domain}.crt`, cert.cert);
await fs.promises.writeFile(`/etc/nginx/certs/${domain}.key`, key.toString());
// 校验 Nginx 配置
await execAsync('nginx -t');
// 校验通过后重载
await execAsync('nginx -s reload');
}
function execAsync(cmd: string): Promise<void> {
return new Promise((resolve, reject) => {
exec(cmd, (error) => {
if (error) reject(error);
else resolve();
});
});
}
</code></pre>
<hr>
<h3>6.4 动态配置</h3>
<p>随着自定义域名数量增长,手动维护 Nginx 配置不可行。需要通过<strong>模板化 + 自动化脚本</strong>实现配置的动态生成、更新和热加载。</p>
<h4>配置模板化</h4>
<p>使用 Handlebars 或 Mustache 等模板引擎渲染配置:</p>
<pre><code class="language-js">import Handlebars from 'handlebars';
const template = Handlebars.compile(`
server {
listen 443 ssl;
server_name {{domain}};
ssl_certificate /etc/nginx/certs/{{domain}}.crt;
ssl_certificate_key /etc/nginx/certs/{{domain}}.key;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
}
}`);
// 渲染配置
const config = template({ domain: 'www.example.com' });
await fs.promises.writeFile(`/etc/nginx/conf.d/www.example.com.conf`, config);
</code></pre>
<p><strong>自动化流程</strong>:</p>
<ul>
<li>渲染模板 → 写入 <code>/etc/nginx/conf.d/{{domain}}.conf</code></li>
<li>校验语法(<code>nginx -t</code>)→ 热加载(<code>nginx -s reload</code>)</li>
</ul>
<h4>域名生命周期管理</h4>
<ul>
<li><strong>新增域名</strong>:生成配置 → 写入文件 → 热加载 Nginx</li>
<li><strong>删除域名</strong>:删除配置文件 → reload → 清理证书和缓存</li>
</ul>
<h4>高可用方案</h4>
<ul>
<li><strong>配置中心</strong>:使用 Consul / Etcd 同步多节点 Nginx 配置</li>
<li><strong>CDN / LB</strong>:在边缘层处理 SNI 和证书,减轻源站网关压力</li>
</ul>
<hr>
<h3>6.5 监控与回收</h3>
<p>后台需要周期性检测域名状态,处理异常域名并回收相关资源(证书、配置、缓存),确保系统一致性和安全性。</p>
<h4>异常域名清理</h4>
<pre><code class="language-js">async function cleanExpiredDomains() {
const domains = await db.domains.findMany({ where: { status: 'error' } });
for (const d of domains) {
try {
// 使用异步方法删除文件
await fs.promises.unlink(`/etc/nginx/conf.d/${d.domain}.conf`);
await fs.promises.unlink(`/etc/nginx/certs/${d.domain}.crt`);
await fs.promises.unlink(`/etc/nginx/certs/${d.domain}.key`);
console.log(`Cleaned up domain: ${d.domain}`);
} catch (error) {
// 文件可能已被删除或不存在,记录错误但继续处理其他域名
console.error(`Failed to clean domain ${d.domain}:`, error.message);
}
}
// 校验并重载 Nginx
try {
await execAsync('nginx -t');
await execAsync('nginx -s reload');
} catch (error) {
console.error('Nginx reload failed:', error);
throw error;
}
}
</code></pre>
<p><strong>监控策略</strong>:</p>
<ul>
<li><strong>周期检测</strong>:定时检查域名 DNS 指向是否正常</li>
<li><strong>异常标记</strong>:DNS 解析失败或指向错误时标记 <code>error</code></li>
<li><strong>资源回收</strong>:删除无效域名的配置、证书和缓存,防止资源泄露</li>
</ul>
<p><strong>完整访问链路总结</strong>:</p>
<pre><code>用户请求 → DNS 解析 → Nginx (SNI + 证书) → Node.js (Host 路由) → 站点渲染
↑ ↓
周期检测 ← 资源回收 ← 异常处理 ← 动态配置 ← 证书管理
</code></pre>
<hr>
<h2>七、域名冲突处理</h2>
<p>同一域名可能被多个用户尝试绑定到不同站点。弱校验模式下,采用 <strong>DNS 仲裁</strong>:谁的 DNS 解析生效谁获得使用权。</p>
<pre><code class="language-ts">async function addDomain(userId: string, siteId: string, domain: string) {
// 1. 检查是否已被其他站点绑定且激活
const existing = await db.domains.findFirst({
where: { domain, status: { in: ['active', 'cert_issued'] } }
});
if (existing && existing.siteId !== siteId) {
// 已被其他站点激活,检查当前 DNS 是否指向该用户的 CNAME
const site = await db.sites.findUnique({ where: { id: siteId } });
const isPointingToMe = await checkDnsPointsTo(domain, site.cnameKey);
if (!isPointingToMe) {
throw new Error('该域名已被其他站点使用');
}
// DNS 指向当前用户,标记原记录为过期,创建新记录
await db.domains.update({
where: { id: existing.id },
data: { status: 'expired' }
});
}
// 2. 创建新的域名记录
return db.domains.create({
data: { siteId, domain, status: 'pending' }
});
}
async function checkDnsPointsTo(domain: string, cnameKey: string): Promise<boolean> {
try {
const cnames = await dns.resolveCname(domain);
return cnames.some(c => c.startsWith(cnameKey));
} catch {
return false;
}
}
</code></pre>
<p><strong>数据库约束</strong>:</p>
<pre><code class="language-sql">-- Domain 表不设置 domain 字段的唯一约束
-- 而是通过部分唯一索引:只有一个激活状态的域名记录
CREATE UNIQUE INDEX idx_active_domain
ON domains(domain)
WHERE status IN ('active', 'cert_issued');
</code></pre>
<p>这样可以保证:</p>
<ul>
<li>同一域名可以有多个 pending 记录(不同用户尝试绑定)</li>
<li>但只能有一个激活状态的记录(DNS 验证通过的用户)</li>
</ul>
]]>技术分析技术Chanx ([email protected])
- 2024 年度报告:你好,大模型https://chanx.tech/blog/2024-rewindhttps://chanx.tech/blog/2024-rewind记录 2024 年的工作经历、技术成长、对于大模型以及生活的一些感想Fri, 11 Apr 2025 00:00:00 GMT<![CDATA[<p>记录 2024 年的工作经历、技术成长、对于大模型以及生活的一些感想</p>
<!-- more -->
<p><img src="https://static.chanx.tech/image/AQHcbm85ForTuYxaD5cc2RPlnGr.jpg" alt="AQHcbm85ForTuYxaD5cc2RPlnGr"></p>
<blockquote>
<p>系统提示词:你是一个有三年经验精通写缺陷的辣鸡前端开发,你需要对过去的 2024 年进行总结。参考《2023 年度报告》编写完整的文章,使用 Markdown 格式,文字需要像正常人类一样,请直接输出文章内容。</p>
</blockquote>
<h1>前言</h1>
<blockquote>
<p>差不多冬至 一早一晚还是有雨 -- 《葡萄成熟时》歌词</p>
</blockquote>
<p>按惯例更新年度报告,属于年更博主的觉悟(目标管理)</p>
<p>冬至就新建文档,希望尽早完结,大概率是不行的(预期管理)</p>
<p>今年师弟的年度报告咕咕咕,我不咕就是赢(横向对比)</p>
<h1>聊聊工作</h1>
<blockquote>
<p>引用 2024 的工作签名:Imagine and make magic</p>
</blockquote>
<h2><strong>你看我这饼,又大又圆</strong></h2>
<p>**讲故事都是从「很久很久以前」开始的。**刚来新公司的第一天,飞书就冒出需求讨论的日程,有着前司入职即干活的先例,这都不是事~会议室里两个陌生人给我叽里咕噜地说了一大堆话,问我这个饼大不大香不香。那程序员标准话术得用上吧,我只能先跟他们讲「<strong>你这个很难,需要再看看</strong>」,再不行来个「你说得有道理,下来我们再对对」。</p>
<p>同一天,+1 也给我拉了个会,给我讲了一个他是他,但他不是他的故事,问我想做什么,我说我不知道。后面才知道是两个同事重名,HR 给我分配错直属上级(摊手无奈)。第二天喜提甲流,一测体温 40 度,正所谓入职即巅峰。大晚上烧麻了,蠕动到医院抽血开药,回去直接躺了四天,同事大概也许可能觉得我要提桶跑路。</p>
<p>我们需要做:大模型来生成网站,高情商叫 AI 建站,低情商叫 AI 套壳。这个时间点模型编码能力并不强,v0 此时还在走对话生成 React 组件。想要生成一个整站项目,天方夜谭。至于究竟要做到什么程度,服务什么人群这些,团队内其实还是模糊的,探索会议一轮接一轮。摸不清前面的路,那就先走一步是一步,船到桥头自然直。敲下第一行代码,原始版的示例程序就开始形成模样。虽然 demo 不是很完整,但确实能看出来这个饼,确实又大又圆。</p>
<h2><strong>你好,Wegic</strong></h2>
<p>一群人在封闭会议室里酷酷干,这个新项目就这么跌跌撞撞地跑起来。</p>
<p>产品名字的来源也挺有意思,web + magic = wegic,前端切图仔升咖成网页魔术师。</p>
<p>团队内优秀的设计师带来了三个可爱的吉祥物,整个产品瞬间生动形象起来。</p>
<p><img src="https://static.chanx.tech/image/Mzklbe18io8Sb6x3nDdchegqnab.jpg" alt="Mzklbe18io8Sb6x3nDdchegqnab"></p>
<p>2024 年 5 月 14 日,产品在 Product Hunt 发布。这是我第一次知道有这么一个平台,每天都有创意的点子在上线。</p>
<p><img src="https://static.chanx.tech/image/HiqMbL9tRoLtsMxKWjTcSMHmnrF.jpg" alt="HiqMbL9tRoLtsMxKWjTcSMHmnrF"></p>
<p>同一天 GPT-4o 发布,编码能力较 GPT-3.5 有显著提升。团队收到消息后立马就准备更换底层模型和调整宣传物料,把这个带着当时最新模型能力的产品推送上线。整个打榜过程属于是惊心动魄,靠着新模型的曝光流量和团队的努力,最终有惊无险拿下 Product Hunt 当天日榜第一。</p>
<p><img src="https://static.chanx.tech/image/V2nVbpdc0oh9SexGkuSctllHnzc.jpg" alt="V2nVbpdc0oh9SexGkuSctllHnzc"></p>
<p>那天转发的宣发视频,底下评论都是:好快!<strong>总而言之,这真是天时地利人和</strong>。</p>
<p>在此之前,我也会反复地想这个还不成熟的产品能不能得到用户认可。</p>
<p>回想起在前司团队做的一项业务,花费比较长时间在打磨细节,在获取内外部目标用户反馈上做得不太好,最后的结果平平无奇。</p>
<p>当然,用户反馈是混乱的,还要提取关键信息,找到合适的方向,不要忘记出发点。</p>
<blockquote>
<p>原文:<a href="https://www.kitze.io/posts/saddest-just-ship-it-story-ever">https://www.kitze.io/posts/saddest-just-ship-it-story-ever</a>
译文:<a href="https://www.ruanyifeng.com/blog/2024/07/weekly-issue-308.html">https://www.ruanyifeng.com/blog/2024/07/weekly-issue-308.html</a></p>
</blockquote>
<p><img src="https://static.chanx.tech/image/XCZlbbrtKoNSNbxTNpbcjYqMnFc.jpg" alt="XCZlbbrtKoNSNbxTNpbcjYqMnFc"></p>
<h2><strong>同志,你是干什么工作的</strong></h2>
<h3><strong>日常切图</strong></h3>
<p>实际上推进新业务的过程并不顺利,不像大厂有成熟的配套基础设施建设,我在公司的知识库转了一圈发现什么也没有。不仅是基建没有,甚至团队的知识库也是空空如也。</p>
<p>第一个坑是组件库。看着设计稿比较简单,不需要太长的排期。但没有现成可用的基础组件库,一切都需要从零开始。开始先引入 arco 把功能做了,然后再慢慢覆盖样式做业务的设计风格。产品内有一套成熟的基础组件库真的很重要,至少沟通和开发的成本已经减少很多。</p>
<p>第二个是交互细节。简单的比如鼠标移动、悬浮、单击、双击,工具栏弹出、定位、碰撞、动画,甚至各种交互的复杂互斥共存条件。这种交互细节不同于技术方案,很难在一开始就全面枚举,需要持续地迭代。最好的方式就是以用户视角深度体验产品,慢慢就会发现很多用户体验的问题。这里分享一篇前端开发在用户体验细节实践的博客:<a href="https://innei.in/posts/design/design-concepts-in-follow-app">浅谈 Follow 中的设计理念 | 静かな森</a>。</p>
<p>第三个是还原样式。切图离不开样式,设计总能想出来各种好玩的效果。同样一个属性在不同浏览器表现不一致,同一浏览器版本之间的 不一致,同一浏览器不同终端也可以不一致。兼容性让你每天都学会一个小细节,在这里<strong>我愿称 safari 为现代 IE 浏览器。</strong></p>
<p><img src="https://static.chanx.tech/image/R3JJbFuXMoA3Zdxb7TVcm3D1nac.jpg" alt="R3JJbFuXMoA3Zdxb7TVcm3D1nac"></p>
<h3><strong>搞搞工程</strong></h3>
<p>敲下第一行代码!新项目没有技术债务,选型上会相对自由。</p>
<p>技术方案的选择有很多,合适的才是最重要的:</p>
<ul>
<li><p>主要技术栈直接跟公司内部业务保持一致:减少协作成本,方便摇人</p>
</li>
<li><p>依赖版本拉到最新稳定版:减少依赖的历史债务,获得新特性和漏洞修复</p>
</li>
<li><p>业务选择 Reactjs 而不是 Nextjs:当前业务没有强诉求,选择开发坑少点的框架</p>
</li>
<li><p>yarn 改成 pnpm 工具链:当前 pnpm 更友好的 DX ,也为接入 pnpm workspace 做铺垫</p>
</li>
<li><p>接入 i18n 框架:前期接入成本远远低于后期接入成本,长期迭代的项目可以先做</p>
</li>
<li><p>代码规范和流水线检查:代码劣化是光速的,多人协作必须做,永远不要相信同事提交的代码</p>
</li>
</ul>
<p>**第一个是包管理工具。**我直接选择 pnpm 起手,它有更友好的开发体验。依赖工具的变化,存量项目需要检查依赖关系及产物,新项目没有这一方面的担心。这里额外推荐一篇文作为解释:<a href="https://juejin.cn/post/6932046455733485575">为什么现在我更推荐 pnpm 而不是 npm/yarn?</a></p>
<p>问题在于内部全面使用 yarn 做依赖管理,推进新的包管理工具存在挑战,整个研发流程都需要进行改变。首先要进行改造和宣讲,让同事感受到 pnpm 带来的温暖,也要上 <code>only-allow</code> 进行额外的限制,避免同事和<code>yarn</code>纠缠不清。其次还要更新部署镜像,保证开发和构建全流程的环境一致。</p>
<p>**第二个是持续集成。**新项目在开始就要把这个事情做好,多人协作的场景下,代码劣化真是光速的。本地的代码提交检查大致有: <code>Eslint + Prettier</code>、<code>TypeScript 类型</code>、<code>Husky + lint-staged + commitlint</code>。当然还要防止同事偷偷地使用逃逸大法绕过本地检查,还需要搭建远程仓库的流水线检查。内部用的<code>Gitlab</code>,需要找运维协调<code>Gitlab Runner</code>资源。</p>
<p>这里比较好玩的是,研发团队对于持续集成这件事并不重视。我在推进这件事的时候遇到的有趣观点:存量代码不好处理、代码检查前端不会配置、<code>Gitlab Runner</code>需要额外的机器、修复代码检查的错误需要时间,种种原因导致项目没有代码检查。<strong>在我的角度看来,一个长期多人协作的前端项目连 eslint 确实是匪夷所思。</strong></p>
<h3><strong>搞搞代码</strong></h3>
<p>项目里的持续集成是保代码下限的,代码上限则需要代码设计来做到。</p>
<p>「好的架构是进化来的,不是设计来的」</p>
<p>「好的架构也应该让业务尽量感知不到“架构”的存在」</p>
<p>尽管项目的复杂度远远没有到架构设计一说,但是很多常见的地方还是可以注意的。</p>
<ul>
<li><p>长函数拆解</p>
</li>
<li><p>静态代码如常量类型定义和函数的代码拆分</p>
</li>
<li><p>业务逻辑按模块拆分,高内聚低耦合</p>
</li>
<li><p>组件逻辑封装注意拆解视图和非视图逻辑</p>
</li>
<li><p>还有像配置化、组合式、设计模式等等一些实用的方法</p>
</li>
</ul>
<p>个人推荐《重构,改善既有代码设计》这本书,看完后会有不错的认识。</p>
<h3><strong>搞搞新东西</strong></h3>
<p>下半年团队给了一段时间做功能探索,算是一个小型的黑客松。</p>
<p>跟几个不同方向的同事组成小队,组合各种稀奇古怪的功能做些好玩的东西。</p>
<p>这段时间每天都在点各种平台和文档,体验 coze、dify、fastgpt 等新产品。</p>
<ul>
<li><p>用向量知识库做问答助手,可以是网页、文档的智能客服,也可以是某个风格的数字人</p>
</li>
<li><p>通过生图应用,根据主题生成多张同风格的图片嵌入到网页内,提高美观度</p>
</li>
<li><p>结合爬虫和大模型做网页信息提取,挖掘页面内的关键文本信息,做进一步处理</p>
</li>
</ul>
<p>每天都在体验新功能,感受新技术带来更易用的功能,更低的集成成本。</p>
<p>我的学生时代为什么没有大模型,我真的不想翻文档学习 API(笑。</p>
<h2><strong>成为 10x 程序员</strong></h2>
<p>内部经过一轮业务调整,下半年建站业务的队伍异常壮大。</p>
<p>认识不少新的小伙伴,他们有不一样的经验或观点,在需求合作过程互相学习。</p>
<p>日常交流过程,也给团队内校招同学分享了一些我自己觉得很不错的工作经验。</p>
<ul>
<li><p>**有效的信息留痕。**日常工作留下有效的文档或者结论,既能提高效率,也能在工作中保护自己</p>
</li>
<li><p>**工作二八定律。**每天工作时间只有 80%,要留出一些缓冲时间处理其他事情,比如会议、值班等</p>
</li>
<li><p>**提问的艺术。**提出问题或者发起会议给对方描述清楚问题或背景,减少无效沟通</p>
</li>
</ul>
<p>在大模型的加持下,研发的工作方式正在发生巨大变化,就像是产研流程全链路按下了加速键,各个阶段都插上了大模型做的翅膀。<strong>懒就是第一生产力,多使用工具解决的事情,把自己从低效的事情释放出来</strong>。</p>
<p>用代码迁移场景举例,最笨的方法就是每行代码人工迁移;传统方法可能是人工抽象一个 AST 脚本做转换,但现在可以直接用大模型完成 AST 脚本的编写,甚至直接用大模型完成代码迁移。</p>
<p>在开发需求的过程,还能抽空让 AI 写点好用的工具,降低排查问题的成本。</p>
<p><strong>当然,好用的工具可以分享给团队,给团队带上 10x 的效率加成。</strong></p>
<h2>AI,让世界充满爱</h2>
<p><strong>如果说 2023 年是大模型元年,那 2024 年就是大模型梦想年。</strong></p>
<p>过去十五年,我们讲数字化,互联网+**,**移动生态,网络更新。</p>
<p>下一个十五年,我们讲智能化,AI+,设备更新,智能生态。</p>
<p>2024 年已经出现不少富有想象力的应用,让我们看到大模型的魅力。</p>
<p>或许明年,大模型就从互联网走到各行各业,影响到更多普通人,我们拭目以待。</p>
<p>大模型的落地,也催生了程序员行业已死的观点。</p>
<p><strong>我认为,AI 不是替代者,而是辅助者,我们的核心能力是辨别和使用工具。</strong></p>
<p>在自身专业能力靠谱的前提下,我们才能使用大模型进一步提升目标达成的效率与质量;</p>
<p>相反,它也能让我们更便捷地获取信息,轻松实现跨界探索,但真实性就需要打上问号了。</p>
<p><img src="https://static.chanx.tech/image/IMhub54eioUwh5xmGGgcLi6tnCH.jpg" alt="IMhub54eioUwh5xmGGgcLi6tnCH"></p>
<h1>折腾怪</h1>
<blockquote>
<p>保持好奇心、保持想象力、保持创造力</p>
</blockquote>
<p>成年人有成年人自己的玩具,记录下我折腾的东西,有物理玩具,也有数字玩具。</p>
<p>今年入手了些新设备,全力拉动 GDP 增长,成为经济发展的小螺丝钉!</p>
<h2><strong>绿联 NAS</strong></h2>
<p>入手动力:腾讯视频会员不能看 4K 视频,还得整个 SVIP 那我为什么要年费?</p>
<p>体验:变成 BT下载 + 夸克 + 4K观影的电子仓鼠,随时随地掌控播放进度</p>
<p><img src="https://static.chanx.tech/image/OmiDbsVbFovlToxeRW0c67Wfn7g.jpg" alt="OmiDbsVbFovlToxeRW0c67Wfn7g"></p>
<h2><strong>ipad mini</strong></h2>
<p>入手动力:京东补贴,看剧看小说的过渡设备...娱乐是生产力好吧</p>
<p>体验:上手很舒服,平时拿来看微信读书和看剧,使用频率不高,建议打折低价入手</p>
<h2><strong>Macbook Pro</strong></h2>
<p>入手动力:国家补贴太香了,刚好需要一个生产力设备,用了一段时间 wsl 很多问题</p>
<p>体验:高频使用,随时随地大小编,边娱乐边看 AI 写代码,丝滑的翻盖开关和长续航</p>
<p><img src="https://static.chanx.tech/image/Oj5Zb8tApopUFsx684Ic4A8xnwc.jpg" alt="Oj5Zb8tApopUFsx684Ic4A8xnwc"></p>
<h2><a href="https://larkdocs2md.chanx.tech/"><strong>larkdocs2md</strong></a></h2>
<p>去年写的文档导出工具,想把自己个人号的飞书文档导出来放博客上面</p>
<p>一个半成品玩具,只能搞搞简单的文档,没想到有几个外国人用,甚至发现还有同事 fork了一下</p>
<p>花时间整了好看的样式,但是看网上有几个功能完整的成品,我这个半成品玩具很尴尬(笑</p>
<p><img src="https://static.chanx.tech/image/AUdFbyJHlofeNnxOQ9ScrOATnKb.jpg" alt="AUdFbyJHlofeNnxOQ9ScrOATnKb"></p>
<h2><strong>ohmyinterview</strong></h2>
<p>去年社招时体验牛客网的智能面试,深度吐槽什么破功能,纯画饼</p>
<p>今年想起来大模型跟这个场景的匹配度还蛮高,搞了个好玩的玩具</p>
<p>核心流程:给定领域和题目、在线录音、语音转文字,大模型评分</p>
<p>还模仿「推特性格总结」写了个提示词,这个阴阳怪气的回复比主流程更有意思</p>
<p><img src="https://static.chanx.tech/image/W3YLb1fhuo8wAdxi6NFc6DmCnhc.jpg" alt="W3YLb1fhuo8wAdxi6NFc6DmCnhc"></p>
<p>玩具体验中也发现核心卡点在语音识别上,中英混杂以及行业术语极大影响识别率</p>
<p>或许接入更强的语音识别、微调识别关键词、直接使用多模态模型这些方法能解决</p>
<p>评分标准上需要调整提示词,更精确的话可以组一个八股文的知识库做 RAG</p>
<p>完善一下这个链路感觉是个不错的小产品,高校在读或在找工作的同学应该有不少诉求</p>
<h2><strong>书签管理工具</strong></h2>
<p>作为电子仓鼠,巨多的浏览器书签一直困扰我</p>
<p>于是用 cursor 搞了一个 web 网页管理书签,希望接入大模型打标分类</p>
<p>可能画的饼不够清晰, cursor 的 composer 模式在一通乱搞,代码改不动了</p>
<p>不过我相信以目前模型的迭代速度,重新填坑不会太遥远</p>
<h2><a href="https://follow.is/"><strong>Follow</strong></a></h2>
<p>浏览器收藏不少博客,也订阅了不少期刊,各种信息渠道需要聚合过滤一下</p>
<p>看见 rsshub 的仓库感觉还蛮惊喜,打算自己动手搞个 rss 阅读器</p>
<p>没想到一个月之后 Follow 就出内测了,很幸运拿到一枚邀请码提前体验到了</p>
<p>每天打开必看大模型动态、前端新闻、博客更新,跟看报纸差不多(哈哈</p>
<p>前期也给 Follow 提了几个 PR,梳理了一遍简中/繁中文案,修了一些样式问题</p>
<p>我只能说:在贡献榜短暂地露脸,赢麻了</p>
<p><img src="https://static.chanx.tech/image/BJScbipxAoB2WuxDrs6cVvD2nZf.jpg" alt="BJScbipxAoB2WuxDrs6cVvD2nZf"></p>
<h1>零零碎碎</h1>
<blockquote>
<p>记忆的碎片,拼成回不去的过往</p>
</blockquote>
<p>「今天黄昏这么美,不拍一张吗」</p>
<p><img src="https://static.chanx.tech/image/JcvybSr6ioTAoWxShuCcgy2nn9f.jpg" alt="JcvybSr6ioTAoWxShuCcgy2nn9f"></p>
<p>「专家说,写代码的同时看飞机可以有效降低缺陷率」</p>
<p><img src="https://static.chanx.tech/image/YZjcbpEX9oy5qXxZFRCcODdUnWb.jpg" alt="YZjcbpEX9oy5qXxZFRCcODdUnWb"></p>
<p><img src="https://static.chanx.tech/image/ZArDbOPGWoOJpxxMruecEjbfnRh.jpg" alt="ZArDbOPGWoOJpxxMruecEjbfnRh"></p>
<p>「天空是蔚蓝色,窗外有千纸鹤」</p>
<p><img src="https://static.chanx.tech/image/EneObLsp9o6NNmxSrdqcXc5KnTf.jpg" alt="EneObLsp9o6NNmxSrdqcXc5KnTf"></p>
<p>「工作之余来场黑客松」</p>
<p><img src="https://static.chanx.tech/image/XE3xbb6ENoctyDxJb4bcuUlRn1c.jpg" alt="XE3xbb6ENoctyDxJb4bcuUlRn1c"></p>
<p>「震惊!上班竟遭后勤同事白眼...」</p>
<p><img src="https://static.chanx.tech/image/Wp56bTHWXoTVhcxIpMQcvmSOnBg.jpg" alt="Wp56bTHWXoTVhcxIpMQcvmSOnBg"></p>
<p>「好像来过,又好像没走过」</p>
<p><img src="https://static.chanx.tech/image/BboubljYkoAbJnxVIX2ceyNRnjc.jpg" alt="BboubljYkoAbJnxVIX2ceyNRnjc"></p>
<p>「纵有狂风拔地起,我亦乘风破万里」</p>
<p><img src="https://static.chanx.tech/image/N2fpbmmwxonre3xOQqOcbPF1nDc.jpg" alt="N2fpbmmwxonre3xOQqOcbPF1nDc"></p>
<p><img src="https://static.chanx.tech/image/GhgzbI6bQoUn6LxJHH8c9RQonaf.jpg" alt="GhgzbI6bQoUn6LxJHH8c9RQonaf"></p>
<p>「冬天到了,春天也就来了」</p>
<p><img src="https://static.chanx.tech/image/ZRKOb7Gg9oWZY6xv3sicbzqbnfc.jpg" alt="ZRKOb7Gg9oWZY6xv3sicbzqbnfc"></p>
<p>「网易云十级了,毕业!」</p>
<p><img src="https://static.chanx.tech/image/UfptbFSKLoZqLjxsxM3cXBXKnTe.jpg" alt="UfptbFSKLoZqLjxsxM3cXBXKnTe"></p>
<p>「网易云没周杰伦版权啊,只能听医生的话」</p>
<p><img src="https://static.chanx.tech/image/Pu1fbity5o1n32xEucccEaDznld.jpg" alt="Pu1fbity5o1n32xEucccEaDznld"></p>
<h1>小小的我</h1>
<blockquote>
<p>整理旧电脑发现一段 2015 年写的作文,标题是《年风年味年不同》
你成熟了 不会失去格调吧 -- 《给十年后的我》歌词</p>
</blockquote>
<p>春节,是中华民族最为隆重、最具特色的传统节日,是家人团圆的美好时刻,也是中华民族的传统文化。人们在春节都尽可能地回到家里和亲人团聚吃饭和祭祖拜神,来表达对未来一年的热切期盼和对新一年生活的美好祝福。</p>
<p>在春节这样如此隆重的传统节日中,自然也少不了许多春节的风俗习惯。比如说在春节前夕需要扫尘,家家户户都要打扫环境,清洗各种器具,拆洗被褥窗帘,清洁多尘地方,到处洋溢着欢欢喜喜搞卫生、干干净净迎新春的欢乐气氛。然后便是办年货,贴春联了,王安石《元日》中就说到了:“爆竹声中一岁除,春风送暖入屠苏。千门万户曈曈日,总把新桃换旧符。”在这个时候,无论城市还是农村,家家户户都要精选一幅大红春联贴于门上,春联红红的颜色更是为节日增加喜庆气氛,示意新一年红红火火。</p>
<p>到了除夕夜的夜幕降临,万家灯火通明,家人团聚一起吃饭,一起守岁迎接新年到来。夜晚十二点钟声响起,漆黑的夜里传来一阵一阵的烟花爆竹声,这样春节就到了。正月初一,大家都带着礼物和红包互相上门祝贺拜年,场面十分之热闹。</p>
<p>随着社会的日益发展,春节与往年年有新花样,年年有不同。在以往,每逢春节都要烧不少烟花爆竹,伴着声声炮响,年味也是浓浓的,可如今由于环境污染,这样的做法开始减少。交通的发达和方便了,现在更多的人热衷于家庭大团聚,以前只是一家小几口团聚,现在却是一家几代十几口人一起聚会,因为有些人的家里亲戚比较多,一家一家地走,直到上班前一天才能全部走完,天天都在奔波中。到现在大家一起聚会,这样的话,往年需要四天做的事情现在只要一天就完成了,剩下的三天时间就可以自由发挥,而且这一天人到的特别齐,虽然说人多吃饭可能会拥挤,可是大家团圆在一起趁这个机会畅所欲言,谈谈一年来的工作情况,诉一诉生活上的苦衷和感慨,彼此之间没有半点隔膜,心与心之间贴得格外的近,场面也特别热闹。</p>
<p>随着科技的进步、互联网的普及,过年问候也多了。各种各样的新年活动也随之兴起,像QQ、微信这样的聊天软件越来越多,功能也越来越全,视频,语音等等,就算作客他乡,随时随地都可以发祝福问候好友。亲戚间也不用见面才发红包,只要大家同在一个聊天群就可以抢红包,也不用说拿着个口袋专门装红包。</p>
<p>在快节奏时代,虽然很多传统观念不断被颠覆,年风年味每一年都在不断地改变当中,改变使得年过得既“健康方便”又“绿色环保”也不失年味,但回家过年的脚步依然没有停止,因为只有大家团聚,这样过年才会更加有意义。 </p>
<p>好好好,都玩大模型了,不得让大模型打个分,感谢 Deepseek - R1 跨越时间的评价。</p>
<blockquote>
<p>评分建议:二类上(45-48/60)
亮点: 主题明确,传统年俗(扫尘、春联、爆竹)与现代变迁(线上红包、云聚会)对比清晰,结构完整;引用《元日》、环保与科技素材展现文化传承与时代感。
提升点: ①传统与现代融合的思辨不足,如环保与传统习俗的矛盾可深入;②语言偶有赘述;③情感共鸣可增强(如具体团圆场景描写)。
简评: 选材典型,结构工整,但议论稍浅。若加强细节描写与思辨深度,可冲击一类文。</p>
</blockquote>
<p>让 R1 大哥再来个升华主题:成长的过程,总让我们对年味有了不同的感受。年味是否浓烈并不取决于外在的热闹,而源于我们是否真正发现并珍惜自己,找到属于自己的生活节奏和价值。我们或许可以学着用自己的方式去发现、去创造属于自己的“年味”。</p>
<h1>结束语</h1>
<p>2024 年北京的冬天没有雪花飘飘,只有风和落叶。</p>
<p>树叶乘风而起,一些在天上,一些在地上,一些在路上。</p>
<p>灯笼微亮,照亮漆黑的路。</p>
<p>走过这个路口,是新年。</p>
<p>那下个路口,又是什么。</p>
<p><strong>你好,2025!我正在路上。</strong></p>
<p><img src="https://static.chanx.tech/image/HgLGbRB6foMl64x1PgPcn6nOn9f.jpg" alt="HgLGbRB6foMl64x1PgPcn6nOn9f"></p>
]]>年度报告随笔Chanx ([email protected])
- 编程导航 1024 活动记录https://chanx.tech/blog/2024-codefather-1024https://chanx.tech/blog/2024-codefather-10242024年编程导航 1024 活动记录Thu, 24 Oct 2024 00:19:56 GMT<![CDATA[<blockquote>
<p>程序员鱼皮弄的编程导航 1024 活动 <a href="https://1024.codefather.cn/">https://1024.codefather.cn/</a></p>
</blockquote>
<p><img src="https://cdn.chanx.tech/image/CRGbbQGn3oaZxsxJdhZcsnTbnAh.jpg" alt="CRGbbQGn3oaZxsxJdhZcsnTbnAh"></p>
<p>经典开局 hello world</p>
<p><img src="https://cdn.chanx.tech/image/PhzwbgXGnopjZixyvYjc5g8xnGb.jpg" alt="PhzwbgXGnopjZixyvYjc5g8xnGb"></p>
<p>程序员必备的两个cv键</p>
<p><img src="https://cdn.chanx.tech/image/VdXRbSVxToJwQXxCT42ctwcYn9g.jpg" alt="VdXRbSVxToJwQXxCT42ctwcYn9g"></p>
<p>奇怪的url带上了参数?answer=codefather</p>
<p><img src="https://cdn.chanx.tech/image/O289bY4Jbo28GYxOxTAcS7Xcnrc.jpg" alt="O289bY4Jbo28GYxOxTAcS7Xcnrc"></p>
<p>#4e80ee</p>
<p><img src="https://cdn.chanx.tech/image/AaPhbUzLBo7DGexXqq8cgPZ8nBh.jpg" alt="AaPhbUzLBo7DGexXqq8cgPZ8nBh"></p>
<p>二进制呢 8</p>
<p><img src="https://cdn.chanx.tech/image/HJoYbiOcHo0XbuxiqYycCSlCnvh.jpg" alt="HJoYbiOcHo0XbuxiqYycCSlCnvh"></p>
<p>php是最好的语言(不,nodejs才是</p>
<p><img src="https://cdn.chanx.tech/image/ZMv0bnLfioUvA1xZkckc2KGNnKh.jpg" alt="ZMv0bnLfioUvA1xZkckc2KGNnKh"></p>
<title>1 Byte = ? bit</title>
<p><img src="https://cdn.chanx.tech/image/GzhFbiQqBo9CarxsFvXc4sFDnUe.jpg" alt="GzhFbiQqBo9CarxsFvXc4sFDnUe"></p>
<p>#e37a3f</p>
<p><img src="https://cdn.chanx.tech/image/VTxHbpqVroBplfxORptcoaKhn2d.jpg" alt="VTxHbpqVroBplfxORptcoaKhn2d"></p>
<p>时间戳代表的时间是「疯狂星期四」</p>
<p>刚好1970年1月1日那天是周四,所以填-28000~57599之间的任意数字都可以通过</p>
<p><img src="https://cdn.chanx.tech/image/MGZkbJSaEofJ0fxdHljcJvl8naf.jpg" alt="MGZkbJSaEofJ0fxdHljcJvl8naf"></p>
<p>填写图片原体积:9.4kb</p>
<p><img src="https://cdn.chanx.tech/image/PViqbCEIGonVtlxw5IlcFRPznyc.jpg" alt="PViqbCEIGonVtlxw5IlcFRPznyc"></p>
<p>红色的rgb值255,0,0</p>
<p><img src="https://cdn.chanx.tech/image/DHabb9h1vo37cWxxJVzcZBSTnng.jpg" alt="DHabb9h1vo37cWxxJVzcZBSTnng"></p>
<p>移除马赛克,图片上标注了 yupi</p>
<p><img src="https://cdn.chanx.tech/image/N9yQbOpP6oIUjXxIuDEc9CyEnCg.jpg" alt="N9yQbOpP6oIUjXxIuDEc9CyEnCg"></p>
<p>cookie里藏了一个值 kanbujianwo=codefather.cn</p>
<p><img src="https://cdn.chanx.tech/image/BX74br8LSoCr6IxW8ivcL1Yvned.jpg" alt="BX74br8LSoCr6IxW8ivcL1Yvned"></p>
<p>程序卡死了,当然要刷新(笑</p>
<p><img src="https://cdn.chanx.tech/image/PzQNb7fBBoRoUAxrgNZcqmtInPb.jpg" alt="PzQNb7fBBoRoUAxrgNZcqmtInPb"></p>
<p>控制台报了 connection error 错误</p>
<p><img src="https://cdn.chanx.tech/image/ZzDKbMDcsoy6Y6xxs1vcsxcPnBe.jpg" alt="ZzDKbMDcsoy6Y6xxs1vcsxcPnBe"></p>
<pre><code class="language-python">window.a
'\x001'
window.b
'1'
</code></pre>
<p><img src="https://cdn.chanx.tech/image/UX73bJu9QoC0yGxbUMtc3Yr1n6g.jpg" alt="UX73bJu9QoC0yGxbUMtc3Yr1n6g"></p>
<p>这个下载的txt文件实际上是png图片,里面写着「ctrl」</p>
<p><img src="https://cdn.chanx.tech/image/QcpZbfbmgoNgESxCUVHcf1egnMh.jpg" alt="QcpZbfbmgoNgESxCUVHcf1egnMh"></p>
<p><img src="https://cdn.chanx.tech/image/CoQhbI7GjopQ64xUrIbcfQj0nle.jpg" alt="CoQhbI7GjopQ64xUrIbcfQj0nle"></p>
<p>Codefather 或者 mianshiya</p>
<p><img src="https://cdn.chanx.tech/image/SJ5gbdCvnoFQ1nxXWowcYdeKnZe.jpg" alt="SJ5gbdCvnoFQ1nxXWowcYdeKnZe"></p>
<p><img src="https://cdn.chanx.tech/image/F0E6bFFYGoLYf5xBELTcsmntnrg.jpg" alt="F0E6bFFYGoLYf5xBELTcsmntnrg"></p>
<p><img src="https://cdn.chanx.tech/image/BQBWbwwLRoh296xDfU2cxFX1noc.jpg" alt="BQBWbwwLRoh296xDfU2cxFX1noc"></p>
<p><img src="https://cdn.chanx.tech/image/YZkkbPqqlo6rp0xpD2LcOFGWnJe.jpg" alt="YZkkbPqqlo6rp0xpD2LcOFGWnJe"></p>
<p><img src="https://cdn.chanx.tech/image/DcJ0bt6Izo69BBxVzfUcyJD6nHb.jpg" alt="DcJ0bt6Izo69BBxVzfUcyJD6nHb"></p>
<p><img src="https://cdn.chanx.tech/image/TLbYbFKmnoPXBrx9hYRcSmHVn6f.jpg" alt="TLbYbFKmnoPXBrx9hYRcSmHVn6f"></p>
<p>控制台打印了「答案已复制到剪切板」 ,所以答案是「学编程就来编程导航~」</p>
<p><img src="https://cdn.chanx.tech/image/SFj2bDVm7oDKNxxyKR9cmlIfnrh.jpg" alt="SFj2bDVm7oDKNxxyKR9cmlIfnrh"></p>
<p><img src="https://cdn.chanx.tech/image/DSQRblWh2ofvEuxe8xfcQU2Mn7e.jpg" alt="DSQRblWh2ofvEuxe8xfcQU2Mn7e"></p>
<pre><code class="language-python">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
</code></pre>
<p><img src="https://cdn.chanx.tech/image/Y8kjbRWjUoi63hx6XFrc1Sh2nmh.jpg" alt="Y8kjbRWjUoi63hx6XFrc1Sh2nmh"></p>
<p>\u7f8e</p>
<p><img src="https://cdn.chanx.tech/image/Y0PdbZbLuoGQzZxFzGqclJkinLe.jpg" alt="Y0PdbZbLuoGQzZxFzGqclJkinLe"></p>
<p><img src="https://cdn.chanx.tech/image/HWhHbfBZcoBl5UxrFcBc8B9rnre.jpg" alt="HWhHbfBZcoBl5UxrFcBc8B9rnre"></p>
<p><img src="https://cdn.chanx.tech/image/Ekf3bg8GEoeqpNxrY1dcwmLAnlf.jpg" alt="Ekf3bg8GEoeqpNxrY1dcwmLAnlf"></p>
<p>4132</p>
<p><img src="https://cdn.chanx.tech/image/J990b38QVo4zNTx9091cjWRrnKd.jpg" alt="J990b38QVo4zNTx9091cjWRrnKd"></p>
<p><img src="https://cdn.chanx.tech/image/Xc9ebREbuo4re3x5dy3cmCvhnef.jpg" alt="Xc9ebREbuo4re3x5dy3cmCvhnef"></p>
<p><img src="https://cdn.chanx.tech/image/J2BYb164xoLrVVx2gATc8xaLnff.jpg" alt="J2BYb164xoLrVVx2gATc8xaLnff"></p>
<p>8</p>
<p><img src="https://cdn.chanx.tech/image/TKMRbcDcCo3jzgxgXeVcukgmnWg.jpg" alt="TKMRbcDcCo3jzgxgXeVcukgmnWg"></p>
<p>0xdeadbeef 是一个16进制魔术数字</p>
<p><img src="https://cdn.chanx.tech/image/Urx5bG5nkoRFBhxg897ca0bRnue.jpg" alt="Urx5bG5nkoRFBhxg897ca0bRnue"></p>
<p><img src="https://cdn.chanx.tech/image/DuVbbqAXno9qWSxTKfHcIrWYnAg.jpg" alt="DuVbbqAXno9qWSxTKfHcIrWYnAg"></p>
<p><img src="https://cdn.chanx.tech/image/ChEFbIwJAoV3CexvqmacrbVunkf.jpg" alt="ChEFbIwJAoV3CexvqmacrbVunkf"></p>
<p><a href="http://tool.chacuo.net/cryptrc4">http://tool.chacuo.net/cryptrc4</a> 在线RC4加密解密 => you_good</p>
<p><img src="https://cdn.chanx.tech/image/BNh4btqlBoXa2BxG5VbceCclnOg.jpg" alt="BNh4btqlBoXa2BxG5VbceCclnOg"></p>
<p>图片是一个压缩包,下载下来改成zip打开存在一张二维码图片</p>
<pre><code class="language-python">1024666 学编程,就来编程导航:codefather.cn
</code></pre>
<p><img src="https://cdn.chanx.tech/image/YaVLb5c5noUCmnxr6dEcCyubnRg.jpg" alt="YaVLb5c5noUCmnxr6dEcCyubnRg"></p>
<p>yupi_thanks_you</p>
<p>这个真没扫出来,直接前端代码逆向吧哈哈哈哈</p>
<p><img src="https://cdn.chanx.tech/image/VtZuboTCBoezVHx424hcyjyDnmb.jpg" alt="VtZuboTCBoezVHx424hcyjyDnmb"></p>
<p><img src="https://cdn.chanx.tech/image/SSy3bQ0I5oyzdLxpx3ectWern7j.jpg" alt="SSy3bQ0I5oyzdLxpx3ectWern7j"></p>
<pre><code class="language-python">This is codefather.cn
</code></pre>
<p><img src="https://cdn.chanx.tech/image/BS8tbC1LFoePKPxmO5dcunf1nYb.jpg" alt="BS8tbC1LFoePKPxmO5dcunf1nYb"></p>
<p><img src="https://cdn.chanx.tech/image/LLQ9b6mn2oD5ACxxr4kc0oL2nLc.jpg" alt="LLQ9b6mn2oD5ACxxr4kc0oL2nLc"></p>
<p><img src="https://cdn.chanx.tech/image/VIfZbb5n5oKtSKxlmaJcKvjAnmb.jpg" alt="VIfZbb5n5oKtSKxlmaJcKvjAnmb"></p>
]]>1024其他Chanx ([email protected])
- 2023 年度报告:再见,同学https://chanx.tech/blog/2023-rewindhttps://chanx.tech/blog/2023-rewind记录了毕业一年半的工作经历,分为“摸索前行”、“拥抱变化”、“重新出发”、“一路向北”四个阶段Fri, 09 Feb 2024 00:00:00 GMT<![CDATA[<p>记录我过去的一年:「<strong>摸索前行</strong>」、「<strong>拥抱变化</strong>」、「<strong>重新出发</strong>」、「<strong>一路向北</strong>」</p>
<!-- more -->
<h1>前言</h1>
<p>虽然时常感慨生活的酸甜苦辣,但也很少用文字来记录。<strong>笔者不善文</strong>,这次更新的动力完全来源于师弟的「<a href="https://anyview.fun/2023/12/28/2023%E5%B9%B4%E5%BA%A6%E6%80%BB%E7%BB%93%EF%BC%9A%E6%88%90%E9%95%BF%E3%80%81%E6%8E%A2%E7%B4%A2%E4%B8%8E%E6%B2%9F%E9%80%9A/">2023 年度总结:成长、探索与沟通</a>」加上今年的一些新变化。题目虽然定了「2023年度报告」,但还是想回顾一下<strong>本科毕业后工作这一年半的时间</strong>。在码字前我在纠结,我该用什么方式或者什么角度去记录,不希望主观情绪影响到文字。最后我决定先大体跟着时间顺序随便写点,这些文字可能更像生活记录而不是技术文章。过去的这段时间可以先用几个关键词来总结一下:「<strong>摸索前行</strong>」、「<strong>拥抱变化</strong>」、「<strong>重新出发</strong>」、「<strong>一路向北</strong>」。</p>
<h1><strong>摸索前行</strong></h1>
<h2>自动化测试</h2>
<p>毕业后我进入了一家还不错的头部互联网公司,在获得这份 offer 前还有一些有趣的故事,在这里不再展开。刚毕业希望在工作中做出点什么成绩,但其实大多数人的工作都是很普通的,所以才有了**「面试造火箭,入职拧螺丝」**的笑谈。正式入职后就被调整到新部门,是负责基建工程化的方向。在前端复杂度极高的应用场景里,做基建工程化相关的事情也是有意思的事情。「质量」这个关键词开始在研发部门的 OKR 上出现,也预示着后续的一系列调整方向。</p>
<p>在质量建设的大目标下,<strong>笔者分到了自动化测试的方向</strong>,希望能用自动化测试来帮助整体项目的迭代更加顺利。很遗憾,我在这个方向一窍不通,我只能一点点地去摸索和落地。更不巧的是,我的同事在这个方向也没有太多实践,我俩只能想着「<strong>跟优秀的人做有挑战的事</strong>」,一起在这个方向进步。</p>
<p>旧项目的自动化测试属于历史产物无人维护状态,甚至因为拉长流水线时间和不稳定的原因人见人不爱。新项目的工程又是啥也没有,<strong>怎么在这个方向落地成为一大难题</strong>。经过一段时间的调研讨论开会,反反复复最后才确定下来落地的方式和节奏。框架集成、基础用例编写、流水线执行,只要一步步往下推进,事情似乎就变得好起来了。我的同事告诉我他要转岗到其他业务去,于是我一个人得接住这一块事情。<strong>我心想,嘿,完蛋了</strong>。</p>
<p>从一个辅助角色突然切换到负责角色,转变来的很突然。尽管同事给我留了一些后续的规划,零经验的我对后面的工作还是一片迷茫。<strong>作为 owner 的我只能到处偷师</strong>,查阅各种资料,包括但不限于公司内文档、竞品做法、测试领域的书籍。有了大致的了解后我开始规划起自动化测试的蓝图,立下小目标「<strong>会写测试的前端</strong>」,希望在这个方向做出成绩。</p>
<p>「<strong>理想很丰满,现实很骨感</strong>」。在落地的时候总会遇到很多问题。人力投入、编写用例、规范制定、流程卡点、数据统计等等一整个链路都是问题,慢慢地我带着这艘有些破烂的小船扬帆起航。流程上线后看着数据看板里单测覆盖率增加和测试成功拦截的数字,忐忑了一段时间的心才下来。<strong>我还在摸索规划,希望缝缝补补让测试小船走得更快更远</strong>。幸运的是,测试终于决定长期投入做这件事,我和几位测试同学在端对端测试上做了进一步的应用。自动化测试落地过程中建立了流水线相关的看板,我关注到项目流水线速度慢和失败率高的问题。而持续集成这块一直没有人管,我主动揽下来做了些小优化,算是额外小惊喜。</p>
<p>再回过头看这个方向的结果,我觉得是成功且有效的,但还有很多改进的空间,只是没有机会和时间。后面我多次跟别人提起这段项目经历,他们都会问我「<strong>你到底是测试还是前端</strong>」。这个方向我持续投入了半年多的时间,几乎没有写前端的业务需求代码,我也同样不断问自己这个问题,内心迷茫。零基础不断摸索前进的过程虽然很痛苦,但不管在技术还是协作上都确实积累了不少干货。作为 owner 要去思考的东西要更多,方案间的权衡选择、规范和流程的制定等等。</p>
<h2>万物 AI</h2>
<p>跟元宇宙画饼不太一样,ChatGPT横空出世后,大量业务开始智能化,都在探索怎么跟AI结合,跟紧新一代的技术。一开始我也是跟大多数人一样没觉得能有多大变化,直到工作中偶然使用到 AI 解决问题,直接就是整个人被通用大模型震惊到,这才慢慢意识到 <strong>AI 要开始影响各行各业</strong>了。</p>
<p>我所在的业务跟进 AI 的速度并不快,甚至说落后于行业内的同行。直到 Notion AI 发布后,大家才意识到AI已经可以这么玩,从这个时候开始追赶已经是落后于别人了,这也不得不说有时候国外业务创造力是大于国内的。我所在团队内一开始定了十分激进的目标,要在几个月内完成功能复刻,探索创新功能并全量上线。某天我也被调整到 AI 方向上,全新的方向没有一个人能说明白该怎么做,充满无限想象力,不断试错不断创新。</p>
<p>于是每天除了做一直在变化的产品需求,我也时常在想 AI 可以改变我生活中哪些方面。作为前端,只需要明白如何调用 OpenAI 的接口,代表已经掌握大模型的使用方法。再后面就是将<strong>需求描述(Prompt)<strong>给大模型,一切交给大模型处理,这也是所谓的</strong>提示词工程</strong>。</p>
<h1><strong>拥抱变化</strong></h1>
<p>平平无奇的工作日,HR临时约了个日程告诉我要协商离职。绩效后有心理准备,也算是意料之中的事情。只是毕业后第一家公司没有呆满三年,还没有好好的做事情,有些遗憾。虽然有一个月的缓冲期,但是业务的压力蛮大,全身心投入到最后的需求里,直到最后半个月才开始慢下来,<strong>想起来自己快要lastday了,但好像我才刚毕业不久</strong>,有些不敢相信。</p>
<p>离职之后只想休息好好躺平充电,甚至想不等35原地退休。一起被裁员的同事说要不要一起去川西,那就说走就走。第二周从深圳坐船到珠海跟同事会合,在珠海金湾机场搭上自己的首飞航班。第一次坐飞机的感觉还是很奇妙的,看到在眼前的大飞机,感受起飞的推背感,像个什么都感兴趣的小孩。</p>
<p>川西的五天亚丁行程从成都开始,每天有接近十小时的车程,一路上手机没有信号,有的是一路海拔爬升缺氧带来的困意,还有<strong>充足的时间思考人生,俗称发呆</strong>。前三天还是比较有意思的,看到了很多很多高原才有的风景,蓝天、白云、牦牛、马、高山、雪山、堵车……就是高反作用下人会比较累</p>
<p><img src="https://static.chanx.tech/image/SK1Fb49jyowzKQxqryxcG8l3nLe.jpg" alt="SK1Fb49jyowzKQxqryxcG8l3nLe"></p>
<p><img src="https://static.chanx.tech/image/EnvbbD05qo7IjfxxGgUctb48nSh.jpg" alt="EnvbbD05qo7IjfxxGgUctb48nSh"></p>
<p><img src="https://static.chanx.tech/image/KYJgb5no9oaZnyx0AI0cqpk3njd.jpg" alt="KYJgb5no9oaZnyx0AI0cqpk3njd"></p>
<p><img src="https://static.chanx.tech/image/RYAsbKVKfobAqCxEttfcB8K1n7c.jpg" alt="RYAsbKVKfobAqCxEttfcB8K1n7c"></p>
<h1><strong>重新出发</strong></h1>
<p>停下来的感觉很舒服,没有工作的压力,每天一觉睡到自然醒,仿佛回到秋招后毕业前的那段时间。唯一不同的是,那时候对工作充满希望,感觉美好的一切都刚刚开始。九月份出去吃炸鸡,看见深圳的学生上下学,感觉自己需要找一个新的目标。开始重新准备刷题,捡起来遗忘的八股文,修改破碎的自动化测试简历(笑。</p>
<p><img src="https://static.chanx.tech/image/D19Nb6x8Qo68inxf8decnQ1unLe.jpg" alt="D19Nb6x8Qo68inxf8decnQ1unLe"></p>
<p>重新跑起来之后开始每天都在关注招聘岗位的变动,犹豫要投递哪些岗位。不像秋招,社招很少有可以交流的同学,很多决定都要自己拍板。于是初步定了个大目标:先找广州和深圳匹配的岗位,其次杭州上海,我想我肯定最远只会到江浙沪一带了。九月中的时候我觉得做好心理准备面试了,开始投递简历,实际上我没有完全海投,这也是后面我一段时间内焦虑的原因。沟通好几个岗位基本都是没有面试机会,九月份没有任何收获。</p>
<p><img src="https://static.chanx.tech/image/PHehbhbuCoq4bexg3TZcvbpVnLh.jpg" alt="PHehbhbuCoq4bexg3TZcvbpVnLh"></p>
<p>幸运的是,国庆节后开始有岗位提供面试机会。一边准备面试一边约面试,为防止自己忘记时间,我在用日历记录一些比较正式的面试沟通,回头看大概一天一场的面试频率。<strong>社招还有个特点就是,面试没有时间点或者节奏可言,全看招聘方想不想推进,即使你上一轮通过了。</strong></p>
<p>技术面试过程常问的几个问题:<strong>#1,离职后的一段时间在干嘛,<strong>真实感受到中国人不能gap;</strong>#2,为什么选择跳槽</strong>,面试官不相信我说的裁员,因为我的业务还在疯狂招人;#3,为什么前端在写自动化测试,是不是不熟悉业务代码,这个我也没法解释,只能说一下大概背景。另外学校不是985/211,学历是本科,经验刚满一年,这些都像叠起来的debuff,面试过程中我甚至在想是不是我小时候学习努力一点就不会这样。</p>
<p>十月底的时候慢慢进入找工作面试的状态,目标也有些转变:拿保底offer和感兴趣offer两手抓,准备新一轮的岗位投递,投递了有且只有一个北京的岗位。十一月面试渐入佳境,有几个岗位推进好几轮的流程,也有到手的offer,每天都是充满希望的一天。钉钉推进的流程很快,一周三面,已经做好去杭州的心理准备,没有竞业去竞品也说得过去。还记得那天钉钉二面完高兴的时候,一个北京的电话打过来,我一度以为诈骗电话,接了之后才发现自己还投了一家北京的公司。11月9号从早到晚一天三个三面,社招来最开心也是紧张的一天。</p>
<p>再后面钉钉三面因为非技术原因流程结束,我再三考虑接受了一份来自北京首都的offer。对于一个常驻广东的华南F3选手,去广东以北到北京工作真的不敢想的事情。</p>
<h1>一路向北</h1>
<p>还记得21年的时候,背着装满衣服的书包坐着高铁从广州去深圳实习;同样23年是拖着装满衣服的行李箱坐着飞机从深圳飞到北京工作。因为坐过飞机去四川,所以流程基本熟悉,托运、安检、登机没有任何问题。感谢发达的互联网,在深圳的我还没买机票就在自如上看好了要租的房子。</p>
<p>11月23号落地找了酒店过夜,第二天就直奔看房,光速签约。落地北京的第一晚,刚好是北京冷空气到达的时间。走出地铁站,我才感受到什么叫刮大风,零度的冷风吹得人站不住。幸好租的房子集体供暖刚用起来,没有被这大冬天冷死,就是刚来北京被子枕头都没有,深夜还是冷的哆嗦。哆哆嗦嗦冻了两个晚上,不经意看到遥控器才想起来空调也有制暖,开了制暖睡了个懒觉。</p>
<p>11月29日,新公司入职,熟悉的Macbook Pro,熟悉的办公软件。第二天上班感觉不太舒服,找行政拿了个体温计一量39度,吓得立马请假回去躺着。<strong>躺到晚上感觉好多了,再拿温度计一测40.2度</strong>,好家伙比新冠发烧还厉害。晚上犹豫再三还是打车去医院,初来北京什么流程都不熟悉,医院发烧门诊一堆人,看着别人怎么操作自己跟着操作,挂号抽血问诊一条龙,确诊甲流拿奥司他韦回家吃了睡觉。</p>
<p>还没有工资,先交房租再交医药费,烧得迷迷糊糊,在想是不是要离开北京...</p>
<h1>欢迎来到对抗路</h1>
<p>由于组织架构的变动,我的汇报线调整到「Z同学」团队。在我离开我毕业的第一家团队之前的三个月,我和我的+1即Z同学有一些比较有趣的聊天对话。当然,我清楚这只是他管理团队的一套话术。</p>
<ol>
<li><strong>我也不知道要做什么,你可以想想</strong></li>
</ol>
<p>Z同学带队有个习惯,每隔两周会主动找团队内同学进行一对一的沟通,以了解一线工作的精神状况(开玩笑</p>
<ul>
<li><p>谈及业务方向,Z同学说:要多花时间想想业务方向,了解客户的诉求,最好能签下一些客户</p>
</li>
<li><p>谈及团队氛围,Z同学说:嗯...那你觉得应该怎么做呢</p>
</li>
<li><p>谈及职业规划,Z同学说:这个东西不能我来告诉你,你应该多了解一下身边高职级同学多想想</p>
</li>
<li><p>谈及下一次沟通,Z同学说:下次准备好议题,我们照着文档聊</p>
</li>
</ul>
<p>综上所述,可以提炼出一套话术:我知道/不知道怎么做,但我还是希望你能主动思考,多想想</p>
<p>于是每当Z同学说大家有事记得找我,我在想...我还是自己多想一想吧哈哈</p>
<ol>
<li><strong>九点下班有点早,要我看见你的付出</strong></li>
</ol>
<p>日常工作中没有特别紧急重要的事情,我一般会在九点就准备下班。即使有工作需要处理,我也会在九点带上电脑回家再干。我自认为互联网九点下班已经算是正常的工作时间,直到Z同学在沟通中说出「九点下班有点早,不够积极」。我说有紧急的事情我会在家处理,比如某一段时间为服务上线,我常常在家加班到晚上一点。Z同学说「要在公司努力,让我看见你的付出」</p>
<ol>
<li><strong>校招生要多花业余时间做产出</strong></li>
</ol>
<p>我发现一个奇怪的定律,每隔三个月必定会调整一次,甚至是一个全新的业务方向。作为刚毕业的校招生,我能做到的就是积极适应工作的变化,先做好本职工作。对于其他业务模块、横向的性能或架构,甚至是新的业务方向确实没有更多的精力。Z同学说,自己可以多花点业余时间特别是周末去做一些产出,比如做一下新功能MVP验证、业务架构的升级优化。可能我水平比较低,周末我是一定要给自己休息充电的。</p>
<ol>
<li><strong>要学会质疑,做 ROI 高的事情</strong></li>
</ol>
<p>在新业务上线的前一个月,团队内对齐一个OKR目标:内部打磨产品功能,提升外部用户体验。于是源源不断的反馈从团队内部提出,从产品、设计再到研发都在处理这些反馈,每天开完日会就是在处理事情,忙忙碌碌。上线后一段时间新业务的数据没有达到预期,Z同学质疑我一个月的品质问题修复对业务毫无产出。但搞笑的是,新业务是全程日会 + 周会汇报,汇报过程中Z同学是全程知晓的。</p>
<ol>
<li><strong>优秀的人会花时间把事情做好</strong></li>
</ol>
<p>源自一个因为客户端团队无人力支持的低优需求,Z同学说我可以花时间学习一下原生的安卓和IOS代码,独自完成这个需求。这个就是不讲道理的PUA了,要我这跨技术栈的活都干,怎么不给我开大前端的工资。</p>
<h1>结束语</h1>
<p>比较遗憾的是没有在毕业第一家也是自己比较喜欢的公司呆下去,感谢一路上认识的新同学。</p>
<p>结束也是新的开始,北京欢迎你,为你开天辟地~</p>
]]>年度报告随笔Chanx ([email protected])
- 2024 探索云开发及其应用https://chanx.tech/blog/2024-serverlesshttps://chanx.tech/blog/2024-serverless探索2024Serverless云开发的现状及其应用Thu, 11 Jan 2024 00:00:00 GMT<![CDATA[<blockquote>
<p>原来这就是云开发! Serverless?!</p>
</blockquote>
<!-- more -->
<h1>探索云开发及其应用</h1>
<blockquote>
<p>开始想分享一篇 AI 相关的文章,但是比较尴尬的是很多了解都是在应用层面,理论知识又不够丰富;</p>
<p>后面在写 AI Demo 的时候想到部署这块,恰好在云开发上有些体验,结合个人理解编写本文</p>
</blockquote>
<h1>云开发</h1>
<h2>概述</h2>
<blockquote>
<p>引用腾讯云云开发的介绍</p>
</blockquote>
<p>「云开发」是云原生一体化开发环境和工具平台,为开发者提供高可用、自动弹性扩缩的后端云服务,包含计算、存储、托管等 Serverless 化能力,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),帮助开发者统一构建和管理后端服务和云资源,避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。</p>
<h2>优劣势</h2>
<h3>优势</h3>
<ul>
<li><p><strong>弹性伸缩</strong>: 根据应用负载的变化,自动扩展或缩减计算资源,提高了稳定性,满足并发场景。</p>
</li>
<li><p><strong>无服务器架构(Serverless)</strong>: 云开发采用无服务器架构,开发者只需关注代码的编写,无需关心服务器的维护和管理,降低了运维成本。Serverless 并非表示没有服务器,而是指开发者无需关心底层的服务器管理。</p>
</li>
<li><p><strong>多环境快速部署</strong>: 云开发可以将应用部署在全球范围内的服务器上,提供低延迟和高可用性的服务</p>
</li>
<li><p><strong>性价比高</strong>: 按使用量计费,无需支付闲置资源费用</p>
</li>
</ul>
<h3>劣势</h3>
<ul>
<li><p><strong>平台强相关</strong>: 使用云开发平台意味着对云服务提供商的依赖,依赖于平台提供的环境和功能进行开发</p>
</li>
<li><p><strong>定制化能力不足</strong>: 一些云开发平台为了提供简便性和快速开发,可能限制了一些高度定制的选项,对于一些特殊需求的应用可能不够灵活</p>
</li>
<li><p><strong>数据隐私和合规</strong>: 对于一些行业或地区的应用,特别关注数据隐私和合规性。使用云开发平台时,需要确保选择的平台符合相关法规和标准</p>
</li>
<li><p><strong>网络延迟</strong>: 尽管云服务提供商通常有全球性的数据中心,但仍然可能存在一些网络延迟,对于某些对延迟非常敏感的应用可能会成为一个问题</p>
</li>
</ul>
<h3>应用场景</h3>
<ul>
<li><p>轻量应用后端: 通过无服务器框架构建灵活的后端服务</p>
</li>
<li><p>事件驱动处理: 处理异步事件,如消息队列、文件上传</p>
</li>
<li><p>定时任务: 周期性执行</p>
</li>
</ul>
<h2>选择</h2>
<p>云开发平台那么多,选择一个平台需要考虑哪些方面?几个常见的考虑点:</p>
<ul>
<li><p>功能多少:是否提供满足业务场景的能力或者说可以定制化开发的能力</p>
</li>
<li><p>迭代速度:平台开发功能是否稳定,直接影响开发者体验</p>
</li>
<li><p>环境稳定:决定线上生产环境是否稳定,直接影响应用稳定性和用户体验</p>
</li>
<li><p>费用核算:付费方式及各种计费资源的价格</p>
</li>
</ul>
<p>通常找到一个大公司背书的云开发平台就能解决以上问题,但受制于多方面的因素影响,还是需要找到更适合当前场景的平台并做好备份迁移等措施。</p>
<h1>产品分析</h1>
<blockquote>
<p>这里列举的主要是云开发平台,部分平台只是函数计算</p>
</blockquote>
<h2>海外</h2>
<h2>Google Firebase</h2>
<p>官网地址:<a href="https://firebase.google.com/">https://firebase.google.com/</a></p>
<p>Firebase 是一家实时后端数据库创业公司,它能帮助开发者很快的写出Web端和移动端的应用。自2014年10月Google 收购 Firebase以来,用户可以在更方便地使用Firebase的同时,结合Google的云服务。</p>
<p>虽然Firebase本身是一个应用开发的平台,但一个亮点功能是 Analytics(分析),所以面向的不只是研发人员,还有运营人员,包括一些市场投放人员。</p>
<p><img src="https://static.chanx.tech/image/JWQcbntmfooUNsxpT23cb1gun7e.jpg" alt="JWQcbntmfooUNsxpT23cb1gun7e"></p>
<h2>Google Cloud</h2>
<p>官网地址:<a href="https://cloud.google.com/compute">https://cloud.google.com/compute</a></p>
<p>谷歌提供的云函数计算服务,结合平台提供的其他云服务能力实现应用开发</p>
<h2>AWS Lambda</h2>
<p>官网地址:<a href="https://aws.amazon.com/lambda">https://aws.amazon.com/lambda</a></p>
<p>亚马逊提供的云函数计算服务,结合平台提供的其他云服务能力实现应用开发</p>
<h2>Azure Function</h2>
<p>官网地址:<a href="https://azure.microsoft.com/en-us/products/functions/">https://azure.microsoft.com/en-us/products/functions/</a></p>
<p>微软提供的云函数计算服务,结合平台提供的其他云服务能力实现应用开发</p>
<h2>国内</h2>
<h3>阿里云 - 云开发(已下线)</h3>
<blockquote>
<p>云开发平台即将停止对外服务,如果需要云开发相关的功能,您可以根据自己的需求使用阿里云其他云产品提供的同类工具:函数计算应用管理、云效AppStack、SAE应用管理</p>
</blockquote>
<p>官网地址:<a href="https://workbench.aliyun.com/">https://workbench.aliyun.com/</a></p>
<p>云开发平台下线,但是其他云服务可以满足同样的能力,服务对个人和企业可用</p>
<p><img src="https://static.chanx.tech/image/QD2ObgxqkoxcW5xmLUBciTqOnAc.jpg" alt="QD2ObgxqkoxcW5xmLUBciTqOnAc"></p>
<h3>字节跳动 - 轻服务(已下线)</h3>
<blockquote>
<p>轻服务将从2022年5月14日起停止服务创建,2022年6月14日23点59分停止服务并删除所有资源</p>
</blockquote>
<p>官网地址:<a href="https://qingfuwu.cn/">https://qingfuwu.cn/</a></p>
<p>字节跳动旗下云服务平台「<a href="https://www.volcengine.com/">火山引擎</a>」提供函数服务,但服务对象是企业,<strong>不面向个人开发者</strong></p>
<p><img src="https://static.chanx.tech/image/WHEBb3JkDoohshxOdd9cc6JTnD4.jpg" alt="WHEBb3JkDoohshxOdd9cc6JTnD4"></p>
<p><img src="https://static.chanx.tech/image/LcO9biKA2onnuSxkvwUcgMYhned.jpg" alt="LcO9biKA2onnuSxkvwUcgMYhned"></p>
<p><img src="https://static.chanx.tech/image/EryobiPP9oDzxBxahapcMniFnVf.jpg" alt="EryobiPP9oDzxBxahapcMniFnVf"></p>
<h3>腾讯云 - 云开发</h3>
<p>官网地址:<a href="https://cloud.tencent.com/product/tcb">https://cloud.tencent.com/product/tcb</a></p>
<p>目前应该是国内大厂还在提供云开发平台服务的,从免费到商业化,经历过多轮价格调整。</p>
<p><img src="https://static.chanx.tech/image/DLiybELu7oOO47xDThsc2xpNnig.jpg" alt="DLiybELu7oOO47xDThsc2xpNnig"></p>
<p><img src="https://static.chanx.tech/image/X7I6b3V6YovhBWx8TVecdzFhnwb.jpg" alt="X7I6b3V6YovhBWx8TVecdzFhnwb"></p>
<h3>Aircode(已下线)</h3>
<p>官网地址:<a href="https://aircode.io/">https://aircode.io/</a></p>
<p>开发文档地址:<a href="https://docs.aircode.io/">https://docs.aircode.io/</a></p>
<p>开源组织:<a href="https://github.com/AirCodeLabs">https://github.com/AirCodeLabs</a></p>
<p>不知道什么名字的团队在孵化的项目,提供一些的基础能力,还在功能快速迭代期,刚进入商业化不久</p>
<p>提供三个免费项目额度,超出需要购买收费套餐</p>
<p><img src="https://static.chanx.tech/image/NIGSbN5aYoZ3EOxoduacSjmcnCc.jpg" alt="NIGSbN5aYoZ3EOxoduacSjmcnCc"></p>
<p><img src="https://static.chanx.tech/image/PcigbynyToMAORxjFxLc8t5Xngo.jpg" alt="PcigbynyToMAORxjFxLc8t5Xngo"></p>
<h3>Laf</h3>
<p>官网地址: <a href="https://laf.run/"><u>laf.run</u></a> (国内版) <a href="https://laf.dev/"><u>laf.dev</u></a> (海外版)</p>
<p>开发文档地址:<a href="https://doc.laf.run/zh/">https://doc.laf.run/zh/</a></p>
<p>开源组织:<a href="https://github.com/labring/laf">https://github.com/labring/laf</a></p>
<p>国内企业环界云计算孵化的项目,目前进入商业化阶段,新用户赠送一定余额体验</p>
<p>应该是国内除了大厂外做的最好的(或者说云开发不是腾讯云就是Laf?哈哈)</p>
<p>亮点:</p>
<ul>
<li><p><strong>代码开源,拥抱社区,函数市场</strong></p>
</li>
<li><p><strong>支持私有化部署</strong></p>
</li>
<li><p>在线协作</p>
</li>
<li><p>支持 Websocket 链接</p>
</li>
</ul>
<p><img src="https://static.chanx.tech/image/AwXfbU1eXo3ZxKxVBvYcktuInxf.jpg" alt="AwXfbU1eXo3ZxKxVBvYcktuInxf"></p>
<p><img src="https://static.chanx.tech/image/H5YEbStSsoQSVxxdDuVc0PqIncc.jpg" alt="H5YEbStSsoQSVxxdDuVc0PqIncc"></p>
<h2>小结</h2>
<p>可以看到上述云服务厂商或平台提供了主要的云能力:<strong>函数计算、数据库、存储、CDN</strong></p>
<ul>
<li><p>海外厂商基本都只提供单独的云服务能力,没有将服务聚合成云开发平台进行销售</p>
</li>
<li><p>国内大部分厂商都对云开发平台进行下线处理,将用户引流到自家的各种云服务能力</p>
</li>
<li><p>腾讯云考虑继续将服务聚合成云开发平台,笔者猜测原因是:</p>
</li>
<li><p>新的云开发平台收获好评,拥抱开源社区,发展势头强劲</p>
</li>
</ul>
<p>以前比较火的还有「新浪 SAE」 ,不知道什么原因在国内市场渐渐消失。</p>
<p><img src="https://static.chanx.tech/image/IRBXbrit7o5pUFxhJ6EcZvqVnVd.jpg" alt="IRBXbrit7o5pUFxhJ6EcZvqVnVd"></p>
<h1>使用体验</h1>
<p>实际上云开发平台功能的使用上基本一致,这里主要拿 **laf 平台 **举例。</p>
<p>30s 完成 helloword 接口的创建、开发、发布,如下图示例。</p>
<p>基于上述路径,我们可以编写「<strong>云函数</strong>」,实现自己的逻辑,快速完成上线</p>
<p><img src="https://static.chanx.tech/image/OMoSb7qPDoCk3UxpCq1c5WVFnXd.jpg" alt="OMoSb7qPDoCk3UxpCq1c5WVFnXd"></p>
<p>需要注意的是,每次函数的执行都是在新的环境里,也就是两次函数的状态本身是不共享的,即无状态</p>
<p>但是一些数据需要进行持久存储,这时候需要使用到 「<strong>数据库</strong>」** **能力</p>
<p>平台本身封装数据库的能力并注入到函数的上下文当中,即下面代码中提供的<code>@lafjs/cloud</code></p>
<pre><code class="language-javascript">// 官方文档示例
import cloud from '@lafjs/cloud'
const db = cloud.mongo.db
export default async function () {
const ret = await db.collection('users').insertOne({
name: '王小波',
age: 44,
books: ['黄金时代', '白银时代', '青铜时代']
})
console.log(ret)
}
</code></pre>
<p>继续进行应用开发,可能遇到一些非结构化的数据或者说大文件的存储,数据库不能满足诉求了</p>
<p>这时候需要「<strong>文件存储</strong>」来解决这个问题,平台也把存储能力集成到 SDK 里</p>
<pre><code class="language-javascript">// 官方文档示例
import cloud from '@lafjs/cloud'
export default async function (ctx: FunctionContext) {
// 获取存储桶
const bucket = cloud.storage.bucket('data')
// 写文件,假设是一个大文件
const content = 'hello, laf'
await bucket.writeFile('laf.html', content)
}
</code></pre>
<p>云开发三件套组合技一使用,貌似应用后端的雏形就出来了,前端同学可以大声说:**<del>后端不是有手就行?**</del></p>
<p>确实,云开发平台大大降低了开发的成本,只关注逻辑即可完成开发,一个人也可以是一支军队</p>
<p>除了上面说的三件套,平台也会提供其他的一些功能:</p>
<ul>
<li><p>大多数平台都会提供的触发器,满足部分定时任务的场景</p>
</li>
<li><p>腾讯云额外提供兼容 Redis 协议的缓存数据库,满足高速缓存的场景</p>
</li>
<li><p>Laf 平台提供处理 WebSocket 连接的云函数能力</p>
</li>
</ul>
<p>尽管平台提供再全面的功能,但是云开发不是万金油,也会有不能满足的场景,这时候就要考虑其他方案</p>
<h1>技术分析</h1>
<blockquote>
<p>感兴趣可以了解关于 laf 的一些题外话
<a href="https://github.com/labring/laf/discussions/178">laf 的发展背景和方向讨论 · labring/laf · Discussion #178</a>
<a href="https://github.com/labring/laf/discussions/352">laf next 重构计划说明 · labring/laf · Discussion #352</a>
<a href="https://github.com/labring/laf/discussions/354">laf product key & roadmap · labring/laf · Discussion #354</a></p>
</blockquote>
<p>从个人使用和理解的角度出发,对云开发平台使用到的核心能力进行简单梳理。</p>
<p>怎么从零开始跑通一个最简单云开发平台流程呢?</p>
<p>首先,核心能力分为云函数、数据库、文件存储三个方面</p>
<h2>云函数</h2>
<p>云函数,首先是一个函数,写在函数内的逻辑会被执行,实际上就是一段 ESM 代码</p>
<p>编写的函数需要默认导出,平台后续会导入并调用</p>
<pre><code class="language-javascript">import cloud from '@lafjs/cloud'
export default async function (ctx: FunctionContext) {
console.log('Hello World')
return { data: 'hi, laf' }
}
</code></pre>
<h3>平台能力注入</h3>
<p>将各种实例封装成 SDK 引入到每一个云函数文件里,实现开箱即用,即<code>@lafjs/cloud</code></p>
<pre><code class="language-javascript">// https://github.com/labring/laf/blob/main/runtimes/nodejs/src/support/cloud-sdk.ts
/**
* Create a new Cloud SDK instance
*
* @returns
*/
export function createCloudSdk() {
const cloud: CloudSdkInterface = {
database: () => getDb(DatabaseAgent.accessor),
invoke: invokeInFunction,
shared: _shared_preference,
getToken: getToken,
parseToken: parseToken,
mongo: {
client: DatabaseAgent.client as any,
db: DatabaseAgent.db as any,
},
sockets: WebSocketAgent.clients,
appid: Config.APPID,
get env() {
return process.env
},
storage: null,
}
/**
* Ensure the database is connected, update its Mongo object, otherwise it is null
*/
DatabaseAgent.ready.then(() => {
cloud.mongo.client = DatabaseAgent.client as any
cloud.mongo.db = DatabaseAgent.accessor.db as any
})
return cloud
}
</code></pre>
<h3>云函数相互调用</h3>
<ul>
<li><p>正常编写 import 语句,使用相对路径导入</p>
</li>
<li><p>使用SDK的<code>invoke</code>方法。运行时会由<code>FunctionModule</code>加载并执行</p>
</li>
</ul>
<pre><code class="language-javascript">// https://github.com/labring/laf/blob/main/runtimes/nodejs/src/support/cloud-sdk.ts
async function invokeInFunction(name: string, ctx?: FunctionContext) {
const mod = FunctionModule.get(name)
const func = mod?.default || mod?.main
if (!func) {
throw new Error(`invoke() failed to get function: ${name}`)
}
ctx = ctx ?? ({} as any)
ctx.__function_name = name
ctx.requestId = ctx.requestId ?? 'invoke'
ctx.method = ctx.method ?? 'call'
return await func(ctx)
}
</code></pre>
<h3>三方依赖</h3>
<p>环境初始化后,执行命令安装 NPM 包;运行时通过<code>FunctionModule</code>加载依赖并执行</p>
<pre><code class="language-javascript">// https://github.com/labring/laf/blob/main/runtimes/nodejs/src/init.ts
/**
* Install packages
* @param packages
* @returns
*/
export function installPackages() {
const deps = process.env.DEPENDENCIES
if (!deps) {
return
}
const flags = Config.NPM_INSTALL_FLAGS
logger.info('run command: ', `npm install ${deps} ${flags}`)
const r = execSync(`npm install ${deps} ${flags}`)
console.log(r.toString())
}
</code></pre>
<h3>沙箱执行</h3>
<p>代码需要在沙箱环境执行,这里可以使用 <strong>Node.js vm 模块</strong></p>
<p>Laf 函数调用的分析:<a href="https://forum.laf.run/d/1146">开源 Serverless 框架 Laf 性能优化实践</a></p>
<pre><code class="language-javascript">// https://github.com/labring/laf/blob/main/runtimes/nodejs/src/support/engine/module.ts
import * as vm from 'vm'
export class FunctionModule {
protected static cache: Map<string, any> = new Map()
private static customRequire = createRequire(
CUSTOM_DEPENDENCY_NODE_MODULES_PATH,
)
static get(functionName: string): any {
const moduleName = `@/${functionName}`
return this.require(moduleName, [])
}
static require(moduleName: string, fromModule: string[], filename = ''): any {
// 加载依赖的逻辑,处理缓存、循环引用等
// #1 平台封装的sdk
// #2 云函数
// #3 其他模块
}
/**
* Compile function module
*/
static compile(){
...
// #1 包装代码
const wrapped = this.wrap(code)
// #2 创建全局对象上下文
const sandbox = this.buildSandbox(
functionName,
fromModules,
consoleInstance,
)
// #3 通过vm创建script脚本
const script = this.createScript(wrapped, {})
// #4 在沙箱内执行脚本,返回对应模块
return script.runInNewContext(sandbox, options)
}
static deleteCache(): void {
FunctionModule.cache.clear()
}
protected static wrap(code: string): string {
return `
const require = (name) => {
__from_modules.push(__filename)
return __require(name, __from_modules, __filename)
}
${code}
module.exports;
`
}
/**
* Create vm.Script
*/
protected static createScript(){
const script = new vm.Script(code, _options)
return script
}
/**
* Build function module global sandbox
*/
protected static buildSandbox(){
const sandbox: FunctionModuleGlobalContext = {
__filename: functionName,
module: _module,
exports: _module.exports,
console: fConsole,
__require: this.require.bind(this),
Buffer: Buffer,
setImmediate: setImmediate,
clearImmediate: clearImmediate,
Float32Array: Float32Array,
setInterval: setInterval,
clearInterval: clearInterval,
setTimeout: setTimeout,
clearTimeout: clearTimeout,
process: process,
URL: URL,
fetch: globalThis.fetch,
global: null,
__from_modules: [...__from_modules],
ObjectId: ObjectId,
}
sandbox.global = sandbox
return sandbox
}
</code></pre>
<h3>函数调用</h3>
<p>每一个函数对应着发布上线后的一个接口,有唯一的调用地址,<code>/:name</code>即是函数名称</p>
<p>请求调用的上下文,比如请求的 body 等,即<code>ctx: FunctionContext</code></p>
<pre><code class="language-javascript">// https://github.com/labring/laf/blob/main/runtimes/nodejs/src/handler/router.ts
import { Router } from 'express'
export const router = Router();
/**
* Invoke cloud function through HTTP request.
* @method *
*/
// router.all('/:name', uploader.any(), handleInvokeFunction)
router.all('*', uploader.any(), (req, res) => {
let func_name = req.path
// remove the leading slash
if (func_name.startsWith('/')) {
func_name = func_name.slice(1)
}
// check length
if (func_name.length > 256) {
return res.status(500).send('function name is too long')
}
req.params['name'] = func_name
handleInvokeFunction(req, res)
})
</code></pre>
<pre><code class="language-javascript">// https://github.com/labring/laf/blob/main/runtimes/nodejs/src/handler/invoke.ts
export async function handleInvokeFunction(req: IRequest, res: Response) {
const name = req.params?.name
const ctx: FunctionContext = {
__function_name: name,
requestId: req.requestId,
query: req.query,
files: req.files as any,
body: req.body,
headers: req.headers,
method: req.method,
auth: req['auth'],
user: req.user,
request: req,
response: res,
}
...
return await invokeFunction(ctx, useInterceptor)
}
async function invokeFunction() {
const name = ctx.__function_name
// 获取函数
let func = FunctionCache.get(name)
if (!func) {
func = FunctionCache.get(DEFAULT_FUNCTION_NAME)
if (!func) {
return ctx.response.status(404).send('Function Not Found')
}
}
try {
const executor = new FunctionExecutor(func)
const result = await executor.invoke(ctx, useInterceptor)
if (result.error) {
logger.error(result.error)
return ctx.response.status(500).send({
error: 'Internal Server Error',
requestId,
})
}
}
</code></pre>
<pre><code class="language-javascript">// https://github.com/labring/laf/blob/main/runtimes/nodejs/src/support/engine/executor.ts
export class FunctionExecutor {
async invoke(){
try {
const mod = this.getModule()
const main = mod.default || mod.main
// 执行函数
data = await main(context)
return {
data,
time_usage: timeUsage,
}
} catch (error) {
return {
error,
time_usage: timeUsage,
}
}
}
</code></pre>
<h2>数据库</h2>
<p>一般使用 <strong>NoSQL</strong> 数据库,比如 <strong>MongoDB</strong></p>
<ul>
<li><p>比较自由,不用设置固定的数据结构</p>
</li>
<li><p>在水平扩展上也有比较好的优势,便于做弹性伸缩</p>
</li>
</ul>
<h2>文件存储</h2>
<p>一般是<strong>对象存储</strong>方案,比如开源的 <strong>MinIO</strong>(<a href="https://github.com/minio/minio%EF%BC%89">https://github.com/minio/minio)</a></p>
<p>对象存储是以对象的形式存储和组织数据,每个对象通常包括数据、元数据和唯一的标识符,该标识符用于在存储系统中检索对象。相比传统的文件系统或块存储,对象存储的优势:</p>
<ul>
<li><p>更灵活: 采用扁平的命名空间,不受目录结构限制;文件用元数据来描述</p>
</li>
<li><p>可伸缩:采用水平扩展的架构,允许通过添加更多的存储节点来增加存储容量和性能</p>
</li>
<li><p>高度可用:分布式存储,数据通常会被冗余存储在多个节点或多个数据中心</p>
</li>
</ul>
<h2>小结</h2>
<p>从上面的简单介绍来看,核心能力的选用都离不开「弹性伸缩」,根据资源使用情况进行实时调度扩容,提高整个系统的吞吐量,这也算是云计算的一个特点之一。</p>
<h1>应用案例</h1>
<blockquote>
<p>云开发 x 开放接口 = 任意组合 = 无限可能</p>
</blockquote>
<h2>对接飞书开放平台</h2>
<p><img src="https://static.chanx.tech/image/MoLNb1uohoxblox2iNicfNebnxh.jpg" alt="MoLNb1uohoxblox2iNicfNebnxh"></p>
<h3>定时发送技术文章</h3>
<p>通过接口、页面解析等方式对信息源做过滤和聚合,调用飞书消息相关的接口,定时将数据发送到飞书上</p>
<p><img src="https://static.chanx.tech/image/RLuhbStRso3fOdxSjurcEXkfnbd.jpg" alt="RLuhbStRso3fOdxSjurcEXkfnbd"></p>
<p><img src="https://static.chanx.tech/image/MPjcbPAWzoR8pnxYsjrc06GinNI.jpg" alt="MPjcbPAWzoR8pnxYsjrc06GinNI"></p>
<h3>云文档周报管理</h3>
<ul>
<li><p>基于周报模版文档每周定时创建新的周报文档,并给团队成员授权文档编辑权限;</p>
</li>
<li><p>自动将新周报文档归档到指定知识库指定节点;</p>
</li>
<li><p>通过群机器人发送群消息,通知所有人更新周报内容</p>
</li>
</ul>
<p><img src="https://static.chanx.tech/image/KKL8bN0elo0r1JxSQnpcjeOJnvc.jpg" alt="KKL8bN0elo0r1JxSQnpcjeOJnvc"></p>
<p><img src="https://static.chanx.tech/image/MnBLbqPaNoR81uxIrcNcczNln1d.jpg" alt="MnBLbqPaNoR81uxIrcNcczNln1d"></p>
<h3>可视化数据后台</h3>
<blockquote>
<p><a href="https://open.feishu.cn/community/articles/7271149634339438594">解放社畜~基于飞书API实现next.js网站内容自动生成实践 - 开发者广场</a></p>
</blockquote>
<p>人事维护飞书表格,基于飞书表格 API 自动化生成招聘网页内容(飞书表格 -> Markdown -> Next.js)</p>
<p><img src="https://static.chanx.tech/image/ATPlbV1RBoLDRQxUbwzc3b54nce.jpg" alt="ATPlbV1RBoLDRQxUbwzc3b54nce"></p>
<h2>对接 OpenAI 实现 AI 应用</h2>
<blockquote>
<p>Openai 提供的Node SDK:<a href="https://github.com/openai/openai-node">https://github.com/openai/openai-node</a></p>
</blockquote>
<h3>Chat GPT 应用</h3>
<p>Laf函数示例:<a href="https://laf.dev/market/templates/64c8b75c644db038c51d174e">https://laf.dev/market/templates/64c8b75c644db038c51d174e</a></p>
<p>实践案例:<a href="https://icloudnative.io/posts/build-chatgpt-web-using-laf/">用 Laf 云开发搭建一个 ChatGPT Web 演示网页</a></p>
<p><img src="https://static.chanx.tech/image/Tzc8b2ioHozaEgxpdyec1nZSnAb.jpg" alt="Tzc8b2ioHozaEgxpdyec1nZSnAb"></p>
<h3>ChatMind</h3>
<p>官网地址:<a href="https://chatmind.tech/">https://chatmind.tech/</a></p>
<p>通过与 AI 对话,快速创建和完善思维导图,该产品后续被 Xmind 收购。(据说是大学生基于 Laf 平台编写的项目</p>
<p><img src="https://static.chanx.tech/image/IWINb672moBvyqxG702cdCdtnPc.jpg" alt="IWINb672moBvyqxG702cdCdtnPc"></p>
<h2>对接 Gitlab 实现自动化工作流</h2>
<p>结合 Gitlab 的 Open API 或者 Webhook 实现自动化工作流</p>
<ul>
<li><p>Gitlab API:<a href="https://docs.gitlab.com/ee/api/api_resources.html">https://docs.gitlab.com/ee/api/api_resources.html</a></p>
</li>
<li><p>Webhook:<a href="https://docs.gitlab.com/ee/user/project/integrations/webhooks.html">https://docs.gitlab.com/ee/user/project/integrations/webhooks.html</a></p>
</li>
</ul>
<p>发挥想象力:</p>
<ul>
<li><p>主分支合入/上线群通知提醒</p>
</li>
<li><p>自动切出版本分支/打tag</p>
</li>
<li><p>自动生成changelog文档</p>
</li>
<li><p>GPT + Merge Diff = 自动代码审查</p>
</li>
</ul>
]]>ServerlessJavascript前端Chanx ([email protected])
- 前端开发环境手册(Windows 篇)https://chanx.tech/blog/fe-landing-log-windowshttps://chanx.tech/blog/fe-landing-log-windows前端开发环境手册-WindowsSat, 08 Jul 2023 00:00:00 GMT<![CDATA[<h1>Git</h1>
<h2>安装</h2>
<p>访问<a href="https://git-scm.com/download/win">下载地址</a>,下载独立安装程序,默认配置点点点就好</p>
<p><img src="https://static.chanx.tech/image/FzzrbWQv1oK29vxsmsTcltK6nPH.png" alt=""></p>
<h2>基本配置</h2>
<p>打开 <code>Git Bash</code>,执行以下命令进行全局配置</p>
<blockquote>
<p>Git Bash 不能使用快捷键粘贴,请使用<strong>右键菜单</strong>进行复制粘贴</p>
</blockquote>
<ul>
<li><code>git config --global core.autocrlf input</code>(统一换行符为 LF)</li>
<li><code>git config --global --replace core.editor "code --wait"</code>(用 VSCode 编辑,默认是 Vim)</li>
<li><code>git config --global user.name "``此处填用户名``"</code></li>
<li><code>git config --global user.email "``此处填邮箱``"</code>(邮箱是和 GitHub/Gitea/Gitlab 账号一致的邮箱)</li>
<li><code>git config --global --list</code>(查看上述配置结果)</li>
</ul>
<h2>配置 SSH</h2>
<blockquote>
<p>git clone 支持 https 和 ssh 两种方式克隆代码,当使用 ssh 方式时如果没有配置过 ssh key,会有如下错误提示:</p>
<pre><code>
</code></pre>
</blockquote>
<p><a href="mailto:[email protected]">[email protected]</a>:Permission denied(publickey).
fatal: Could not read from remote repository.</p>
<pre><code>
### 生成公钥和私钥
1. 打开`Git Bash`并执行以下命令,邮箱使用上一步配置的
```javascript
ssh-keygen -t rsa -C "此处填邮箱"
</code></pre>
<p>执行上面命令后需要进行几次确认,<strong>一般直接回车就行,可能需要输入 Y 进行覆盖</strong></p>
<ul>
<li>确认秘钥的保存路径(如果不需要改路径则直接回车)</li>
<li>如果上一步保存路径下已经有秘钥文件,则需要确认是否覆盖(输入 Y 进行覆盖)</li>
<li>创建密码(如果不需要密码则直接回车)</li>
<li>确认密码(如果不需要密码则直接回车)</li>
</ul>
<p>命令执行完会在** 。ssh 文件夹路径(这个后面有用)**下会生成 2 个文件:(如图为 <code>c/Users/chanx/.ssh</code>)</p>
<ul>
<li><code>id_rsa</code> 私钥文件</li>
<li><code>id_rsa.pub</code> 公钥文件</li>
</ul>
<ol>
<li>打开 <code>Git Bash</code> 继续执行命令</li>
</ol>
<pre><code class="language-bash">cat ~/.ssh/id_rsa.pub
</code></pre>
<p>执行命令后输出的 ssh-rsa 开头文本即为公钥</p>
<p><img src="https://static.chanx.tech/image/SUJFbKxDfoBpkBx2h4OcDyLvncB.png" alt=""></p>
<h3>远程仓库配置公钥(Github 为例)</h3>
<p>打开 GitHub,进入到个人账号设置页配置公钥: Settings -> SSH and GPG keys -> New SSH key</p>
<p>将上一步骤生成的公钥复制到 Key 下面的文本框,Title 根据实际情况命名,然后点保存即可</p>
<p><img src="https://static.chanx.tech/image/TvkFb6k18ot4N9xEms8ccWY2naf.png" alt=""></p>
<p><strong>一般其他远程<strong><strong>仓库</strong></strong>到这已经配置完成了</strong></p>
<p>但是 Github 还需要另外配置:</p>
<p>打开上一步提到的 。ssh 文件夹路径 <code>/c/Users/chanx/.ssh</code>(因人而异)</p>
<p>给其中名字为 config 文件添加以下内容并保存</p>
<pre><code class="language-javascript">Host github.com
Hostname ssh.github.com
Port 443
User git
</code></pre>
<p><code>Git Bash</code> 执行以下命令,中间过程输入 <code>yes</code> 进行二次确认,看见如图提示即为配置完成</p>
<pre><code class="language-bash">ssh -T [email protected]
</code></pre>
<p><img src="https://static.chanx.tech/image/NkosbmGFhoGkaGx3zelcdXy9nsC.png" alt=""></p>
<h1>Node.js</h1>
<h2>为什么不直接安装某个版本的 Node.js?</h2>
<p>nvm 可以控制管理 Node.js 版本,<strong>快速切换版本以支持不同的场景</strong>,比如:</p>
<ul>
<li>开发时需要使用稳定的 LTS 版本,不同项目需要使用不同的 Node.js 版本</li>
<li>其他场景可以使用最新的 latest 版本,体验 Node.js 新特性</li>
</ul>
<h2>安装 nvm-windows</h2>
<p>访问<a href="https://github.com/coreybutler/nvm-windows/releases">下载地址</a>,下载最新版本的 <strong>nvm-setup.exe</strong>,下载完成直接运行安装即可</p>
<h3>配置镜像地址</h3>
<p>nvm 默认的下载地址是 <a href="http://nodejs.org/dist/">http://nodejs.org/dist/ </a>,这是国外的服务器,在国内下载速度很慢</p>
<p>但是来自阿里的国内镜像站拯救了我们,感谢阿里(笑</p>
<p><strong>方法一(推荐):</strong></p>
<p>打开任意控制台终端(比如 CMD),执行以下命令</p>
<ul>
<li><code>nvm node_mirror ``https://npmmirror.com/mirrors/node/</code></li>
<li><code>nvm npm_mirror ``https://registry.npmmirror.com</code></li>
</ul>
<p><strong>方法二:</strong>
在 nvm 的安装路径下,找到 settings.txt** **打开,加上</p>
<pre><code>node_mirror: https://npmmirror.com/mirrors/node/
npm_mirror: https://registry.npmmirror.com
</code></pre>
<h2>安装 Node.js</h2>
<p>上一步已经安装 nvm,下面执行 nvm 的命令完成 Node.js 的安装</p>
<ol>
<li><code>nvm list available</code> 获取当前可用的 node 版本</li>
<li><code>nvm install 版本号</code> 就可以安装指定版本的 node(根据项目安装对应的版本</li>
<li><code>nvm use 版本号</code> 就可以切换到该版本的 node 环境</li>
<li><code>nvm list</code> 获取当前已安装的 node 版本列表</li>
<li><code>nvm -v</code> 获取当前 nvm 的版本</li>
</ol>
<p>这里有个小技巧快速安装某个大版本的 node(比如 node18)</p>
<ul>
<li><code>nvm install v18</code></li>
<li><code>nvm use v18</code></li>
</ul>
<p><img src="https://static.chanx.tech/image/CForbTHAwotIXyxgn3PcTRPzn5d.png" alt=""></p>
<h2>npm 包管理器</h2>
<p>Nodejs 安装时默认会附带安装 npm,此时镜像源是国外的</p>
<pre><code class="language-bash"># 更换国内源
npm config set registry https://registry.npmmirror.com
# 查询配置是否成功
npm config get registry
</code></pre>
<p>目前实际使用中,更多会使用到 <code>yarn</code> 和 <code>pnpm</code> 两个包管理器,也需要安装和配置</p>
<pre><code class="language-bash"># 安装yarn
npm i -g yarn
# 查看 yarn 版本
yarn -v
# 更换国内源
yarn config set registry https://registry.npmmirror.com
# 查询配置是否成功
yarn config get registry
</code></pre>
<pre><code class="language-bash"># 安装 pnpm
npm i -g pnpm
# 查看 pnpm 版本
pnpm -v
# 更换国内源
pnpm config set registry https://registry.npmmirror.com
# 查询配置是否成功
pnpm config get registry
</code></pre>
<h3>常见问题</h3>
<ol>
<li>遇到如图报错“禁止运行脚本”,执行以下指令更改策略,<a href="https://learn.microsoft.com/zh-cn/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.3">详情可见</a></li>
</ol>
<p><img src="https://static.chanx.tech/image/ULZMblvT5oKlZkx3zGQcYssznzf.png" alt=""></p>
<pre><code class="language-shell">Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
</code></pre>
<ol>
<li><code>npm i</code> 安装长时间卡在 idealTree buildDeps 这一步不动</li>
</ol>
<p>按一下顺序尝试:<code>npm cache clean</code> 清除 npm 缓存、nvm 重新安装 node、nvm 更换 node 版本</p>
<h1>VS Code</h1>
<p>访问<a href="https://code.visualstudio.com/"> VSCode 官网</a>,下载安装包,正常运行安装即可</p>
<h2>推荐安装字体</h2>
<p>JetBrains Mono: <a href="https://www.jetbrains.com/lp/mono/">https://www.jetbrains.com/lp/mono/</a></p>
<ol>
<li>下载解压后,双击 **ttf **文件内任一打开,点击左上角安装按钮即可</li>
</ol>
<p>区别是文字的粗细和斜体,我目前使用的是 **JetBrainsMono-Regular.ttf **</p>
<p><img src="https://static.chanx.tech/image/Sa9FbCphgoDuCExMyfOcWc8Hn2f.gif" alt=""></p>
<ol>
<li>安装后打开 VSCode -> 设置 -> Editor: Font Family,在前面加入 <code>JetBrains Mono,</code> 或直接使用以下内容覆盖</li>
</ol>
<pre><code class="language-bash">JetBrains Mono, Consolas, 'Courier New', monospace
</code></pre>
<p><img src="https://static.chanx.tech/image/RLtWbNMUUoPvEwx3jwpcLu3gnAc.gif" alt=""></p>
<h2>推荐安装插件</h2>
<table>
<thead>
<tr>
<th>序号</th>
<th>插件名</th>
<th>描述</th>
<th>备注</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td><a href="https://marketplace.visualstudio.com/items?itemName=MS-CEINTL.vscode-language-pack-zh-hans">Chinese (Simplified) (简体中文) Language Pack for Visual Studio Code</a></td>
<td>官方的汉化包,老玩家直接默认英文</td>
<td></td>
</tr>
<tr>
<td>2</td>
<td><a href="https://marketplace.visualstudio.com/items?itemName=bierner.color-info">Color Info</a></td>
<td>代码显示色值信息</td>
<td></td>
</tr>
<tr>
<td>3</td>
<td><a href="https://marketplace.visualstudio.com/items?itemName=vscode-icons-team.vscode-icons">vscode-icons</a></td>
<td>更多图标</td>
<td></td>
</tr>
<tr>
<td>4</td>
<td><a href="https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint">ESLint</a></td>
<td>代码静态检查和修复工具</td>
<td></td>
</tr>
<tr>
<td>5</td>
<td><a href="https://marketplace.visualstudio.com/items?itemName=mkxml.vscode-filesize">filesize</a></td>
<td>显示当前文件的体积</td>
<td></td>
</tr>
<tr>
<td>6</td>
<td><a href="https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens">GitLens</a></td>
<td>显示 git 相关的信息,常用的是显示当前行的提交记录</td>
<td></td>
</tr>
<tr>
<td>7</td>
<td><a href="https://marketplace.visualstudio.com/items?itemName=christian-kohler.npm-intellisense">npm Intellisense</a></td>
<td>import 语句中自动补充 npm 模块名</td>
<td></td>
</tr>
<tr>
<td>8</td>
<td><a href="https://marketplace.visualstudio.com/items?itemName=ionutvmi.path-autocomplete">Path Autocomplete</a></td>
<td>提供代码路径自动补全</td>
<td></td>
</tr>
<tr>
<td>9</td>
<td><a href="https://marketplace.visualstudio.com/items?itemName=Vue.volar">Vue Language Features (Volar)</a></td>
<td>Vue、Vitepress 和 petite-vue 构建的语言支持扩展</td>
<td></td>
</tr>
<tr>
<td>10</td>
<td><a href="https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin">TypeScript Vue Plugin (Volar)</a></td>
<td>为。vue 文件提供 ts 检查</td>
<td></td>
</tr>
<tr>
<td>11</td>
<td><a href="https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker">Code Spell Checker</a></td>
<td>检查代码的命名错误</td>
<td></td>
</tr>
</tbody></table>
<h1>WebStorm</h1>
<p><a href="https://www.jetbrains.com/webstorm/">https://www.jetbrains.com/webstorm/</a></p>
<p>WebStorm 也是较为常见的前端开发工具,相比于 VSCode 来说会更加智能</p>
<p>因常用编辑为 VSCode,这里不赘述。</p>
]]>前端Chanx ([email protected])
- 如何入门开源文档翻译https://chanx.tech/blog/open-source-document-translationhttps://chanx.tech/blog/open-source-document-translation开源文档翻译入门指南,帮助你了解如何参与开源项目的文档翻译工作Thu, 25 Aug 2022 00:00:00 GMT<![CDATA[<h1>为什么要翻译?</h1>
<h2>给正版文档贡献</h2>
<blockquote>
<p>Jest 中文文档、Yarn 中文文档、npm 中文文档...</p>
</blockquote>
<h3>虚假的 Jest 文档</h3>
<p>(来自百度第一
优点:百度排名第一,中文;缺点:除了优点都是缺点</p>
<p><img src="https://static.chanx.tech/image/nr088v_0.png" alt="Pasted image 20230715143445">
<img src="https://static.chanx.tech/image/nr5uo7_0.png" alt="image.png">
<img src="https://static.chanx.tech/image/nr74y5_0.png" alt="image.png"></p>
<h3>真实的 Jest 文档</h3>
<p>优点:谷歌排名第一、信息最新;缺点:百度排名不是第一,中文不一定对
<img src="https://static.chanx.tech/image/nsvdab_0.png" alt="image.png">
<img src="https://static.chanx.tech/image/nswhcf_0.png" alt="image.png">
<img src="https://static.chanx.tech/image/nsxkpl_0.png" alt="image.png"></p>
<h2>省钱小妙招</h2>
<p><img src="https://static.chanx.tech/image/ntdin8_0.png" alt="image.png">
<img src="https://static.chanx.tech/image/ntenrp_0.png" alt="image.png"></p>
<h2>拯救下一个同学</h2>
<p>比如更新滞后的MDN中文文档,按照中文的信息来得加半天的debug时间(笑
<img src="https://static.chanx.tech/image/nth2hf_0.png" alt="image.png">
<img src="https://static.chanx.tech/image/nthydx_0.png" alt="image.png"></p>
<h1>哪里可以贡献翻译?</h1>
<p><img src="https://static.chanx.tech/image/nu26l8_0.png" alt="image.png"></p>
<h2>独立维护文档站点</h2>
<p>独立站点一般是直接整个网站镜像,然后翻译。</p>
<p>也有的是按自己的思路来翻译,这种一般多为GitBook</p>
<blockquote>
<p>下图分别是印记中文,官网。镜像的方式,长得一样</p>
</blockquote>
<p><img src="https://static.chanx.tech/image/nu80vw_0.png" alt="image.png"></p>
<p><img src="https://static.chanx.tech/image/nujwht_0.png" alt="image.png"></p>
<h2>基于MR形式</h2>
<blockquote>
<p><a href="https://github.com/reactjs/zh-hans.reactjs.org/issues">Issues · reactjs/zh-hans.reactjs.org</a></p>
<p><a href="https://github.com/vuejs-translations/docs-zh-cn">GitHub - vuejs-translations/docs-zh-cn: Vue 文档官方中文翻译 | Official Chinese translation for Vue docs</a></p>
</blockquote>
<p>翻译步骤:</p>
<ol>
<li><p>维护团队将原文档进行划分</p>
</li>
<li><p>贡献者提交<strong>该模块翻译****MR</strong></p>
</li>
<li><p>严格的 review 环节</p>
</li>
<li><p>代码合入,翻译完成</p>
</li>
</ol>
<p>比如<a href="https://github.com/reactjs/zh-hans.reactjs.org/pull/870">https://github.com/reactjs/zh-hans.reactjs.org/pull/870</a></p>
<p><img src="https://static.chanx.tech/image/nvxi0k_0.png" alt="image.png">
<img src="https://static.chanx.tech/image/nvylvy_0.png" alt="image.png"></p>
<h2>基于第三方平台</h2>
<p><a href="https://crowdin.com/">https://crowdin.com/</a></p>
<p>crowdin是一个文案翻译平台,按句子的粒度切割文案进行翻译。</p>
<p>在某些情况下这是个缺点,比如语言习惯不同、基于语境的翻译,表述不一定跟原文句子一致。
<img src="https://static.chanx.tech/image/nw1ben_0.png" alt="image.png"></p>
<p><img src="https://static.chanx.tech/image/nwc6jp_0.png" alt="image.png"></p>
<p>翻译步骤:</p>
<ol>
<li><p><code>Crowdin</code>平台加入对应项目</p>
</li>
<li><p>选择翻译语言后<strong>按句翻译</strong></p>
</li>
<li><p>保存,翻译完成</p>
</li>
</ol>
<h1>翻译中需要注意什么?</h1>
<blockquote>
<p>翻译实际上是译者根据原文进行的二次创作</p>
</blockquote>
<h2><strong>遣词</strong></h2>
<ul>
<li><p>正确使用专业术语</p>
<ul>
<li><p>合理地使用常见术语可以降低沟通成本</p>
</li>
<li><p>不要使用过于小众或自创的术语</p>
</li>
<li><p>必要时提供对照的英文术语以方便理解</p>
</li>
<li><p>避免无上下文的缩略词</p>
</li>
</ul>
</li>
</ul>
<blockquote>
<p><img src="https://static.chanx.tech/image/nxry49_0.png" alt="image.png">
赛普拉斯?Cypress!傀儡师?Puppeteer!笑话?Jest!</p>
<p>争议:<a href="https://zhuanlan.zhihu.com/p/245223836?utm_source=wechat_session">《JavaScript高级程序设计(第4版)》的“期约”败笔</a></p>
</blockquote>
<ul>
<li><p>省略程度副词</p>
<ul>
<li>不管作者意图为何, “非常重要” 和 “重要” 在读者看来大同小异</li>
</ul>
</li>
<li><p>不要使用过于生僻的词汇,不要过度使用书面语</p>
</li>
</ul>
<h2><strong>造句</strong></h2>
<ul>
<li><p>尽量使用短句,不要使用多从句的复杂句式</p>
</li>
<li><p>去掉无意义的修饰,去掉试图缓和语气的从句</p>
<blockquote>
<p>反例:“我们可以看到, 数据库在一定程度上可以满足我们对事务支持的需求。”</p>
<p>修改后:“数据库支持事务”</p>
</blockquote>
<blockquote>
<p>反例: “MR 提交信息作为读者查阅修改历史时第一时间看到的信息,其重要性不言而喻。”</p>
<p>修改后:“读者查阅修改历史时会首先关注 MR 提交信息。”</p>
</blockquote>
</li>
<li><p>语气要冷静。避免过于口语化</p>
<ul>
<li><p>不要加顺口溜</p>
</li>
<li><p>不要使用语气词</p>
</li>
</ul>
</li>
<li><p>准确并客观地描述事实,避免加入主观情绪</p>
</li>
</ul>
<h2>排版</h2>
<blockquote>
<p>中文文案排版指北<a href="https://github.com/sparanoid/chinese-copywriting-guidelines">GitHub - sparanoid/chinese-copywriting-guidelines: Chinese copywriting guidelines for better written</a></p>
<p><a href="https://github.com/reactjs/zh-hans.reactjs.org/wiki/React-%E4%B8%AD%E6%96%87%E6%96%87%E6%A1%A3%E8%AF%91%E6%96%87%E6%8E%92%E7%89%88%E6%8C%87%E5%8D%97">React 中文文档译文排版指南 · reactjs/zh-hans.reactjs.org Wiki</a></p>
</blockquote>
<p><strong>中英文之间需要增加空格</strong></p>
<p>正确:</p>
<blockquote>
<p>在 LeanCloud 上,数据存储是围绕 <code>AVObject</code> 进行的。</p>
</blockquote>
<p>错误:</p>
<blockquote>
<p>在LeanCloud上,数据存储是围绕<code>AVObject</code>进行的。</p>
<p>在 LeanCloud上,数据存储是围绕<code>AVObject</code> 进行的。</p>
</blockquote>
<p><strong>中文与数字之间需要增加空格</strong></p>
<p>正确:</p>
<blockquote>
<p>今天出去买菜花了 5000 元。</p>
</blockquote>
<p>错误:</p>
<blockquote>
<p>今天出去买菜花了 5000元。</p>
<p>今天出去买菜花了5000元。</p>
</blockquote>
<p><strong>数字与单位之间需要增加空格</strong></p>
<p>正确:</p>
<blockquote>
<p>我家的光纤入屋宽带有 10 Gbps,SSD 一共有 20 TB</p>
</blockquote>
<p>错误:</p>
<blockquote>
<p>我家的光纤入屋宽带有 10Gbps,SSD 一共有 20TB</p>
</blockquote>
<p>例外:度数/百分比与数字之间不需要增加空格</p>
<h1>我的翻译小插曲</h1>
<ol>
<li><strong>结合上下文</strong></li>
</ol>
<p>原文:</p>
<p>The <code>expect</code> function is used every time you want to test a value. You will rarely call <code>expect</code> by itself. Instead, you will use <code>expect</code> along with a "matcher" function to assert something about a value.</p>
<p>机翻:</p>
<p>每次要测试一个值,你需要使用<code>expect</code>函数。你大概率不需要自己调用<code>expect</code>。相反,你通常会使用<code>expect</code>配合<code>matcher</code>函数来断言某个值</p>
<p>尝试修改:</p>
<p><code>expect</code>用于测试一个值。通常会结合<code>matcher</code>函数来断言某个值,而不是单独使用<code>expect</code>。</p>
<ol start="2">
<li><strong>特有名词</strong></li>
</ol>
<p><code>Fake Timers</code>、<code>Mock</code>该不该进行翻译</p>
<h1>收获</h1>
<ol>
<li><p>提高阅读全英文档的能力</p>
</li>
<li><p>提高技术文档写作的能力</p>
</li>
<li><p>发现不常用但有用的配置</p>
</li>
</ol>
]]>开源翻译文档其他Chanx ([email protected])
- 网页性能优化 - 加载速度https://chanx.tech/blog/web-performance-optimization-loading-speedhttps://chanx.tech/blog/web-performance-optimization-loading-speed网页性能优化-加载速度Thu, 09 Dec 2021 00:00:00 GMT<![CDATA[<p>测试工具:</p>
<ol>
<li><a href="http://webpagetest.org/">http://webpagetest.org/</a></li>
<li>Chrome-devtools-lighthouse</li>
<li>...</li>
</ol>
<h2><strong>性能关键词</strong></h2>
<ol>
<li>First Contentful Paint:<strong>FCP</strong>,首屏渲染时间</li>
<li>Largest Contentful Paint : <strong>LCP</strong>,最大内容渲染时间</li>
<li>Speed Index:代表页面内容渲染所消耗的时间</li>
<li>Time to Interactive:TTI,用户与页面可交互时间</li>
<li>Total Blocking Time:TBT,衡量从FCP到TTI之间主线程被阻塞时长的总和</li>
</ol>
<blockquote>
<p>主线程执行的任务分为长任务和短任务。规定持续时间超过50ms的任务为长任务,低于50ms的任务为短任务。长任务中超过50ms的时间被认为是“阻塞”的,因此,TBT是所有长任务中阻塞时间的总和。</p>
</blockquote>
<ol start="6">
<li>Cumulative Layout Shift:累计布局偏移,指网页布局在加载期间的偏移量</li>
</ol>
<p><strong>并发可以加快速度?</strong></p>
<p>一个文件通过一个连接传输快,还是通过多个连接传输快?显然在「多连接传输的收益 > 建立连接的成本」条件成立下,必然是多连接传输快,也就是<strong>并发</strong>。</p>
<p>浏览器对同源HTTP/1.1连接的并发个数有限制,典型值是6,测试表明Chrome和Firefox都是这个值。根据实际情况充分利用最大连接数,可以让速度更快,文件分片上传/下载就是常见的情况。</p>
<blockquote>
<p>解决最大连接数有常见几种方案</p>
<ul>
<li>域名分片(就是多搞些不同的域名,打破同源条件)</li>
<li>websocket(限制数相对较高)</li>
<li><a href="https://sanyuan0704.github.io/my_blog/http/017.html#%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8">HTTP/2多路复用</a></li>
</ul>
</blockquote>
<h2>说明</h2>
<p>下面评测结果均为Chrome浏览器开发者工具内置的<strong>LightHouse</strong>得出</p>
<p>Q: 不同服务器硬件条件不一,LightHouse如何保证结果的稳定?</p>
<p>A: 它会模拟出一个尽可能相同的环境,然后自动模拟用户访问。但是受当时的服务器状态和网络条件影响,仍然会有测试误差。</p>
<h2>FCP 优化</h2>
<h3>Gzip动态压缩</h3>
<ol>
<li>这是未开启压缩进行测试的结果截图</li>
</ol>
<p><img src="https://static.chanx.tech/image/tecgj_0.png" alt="img"></p>
<p><img src="https://static.chanx.tech/image/tea40_0.png" alt="img"></p>
<p>需要加载的各个文件都比较大,首次加载十分缓慢;二次加载时有缓存的支持,表现相对较好。</p>
<p>一般网页加载使用的文件大小在200kb左右,以便利用<strong>并发请求</strong>加快网页的加载速度(HTTP/1.1)。</p>
<p>查看<code>Network</code>可以看到需要处理的文件<code>Coding.js</code>、<code>student.js</code>、<code>Personal.js</code>;</p>
<p>查看产物目录下<code>report.html</code>,使用<code>Gzip</code>压缩可以大幅度减少文件的体积</p>
<p><img src="https://static.chanx.tech/image/te9fs_0.png" alt="img" style="zoom:50%;" /><img src="https://static.chanx.tech/image/teazg_0.png" alt="img" style="zoom:50%;" /></p>
<ol start="2">
<li>进行Nginx的配置,开启<code>Gzip</code>压缩</li>
</ol>
<pre><code class="language-nginx">http {
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.1;
gzip_comp_level 2;
gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml;
gzip_vary on;
gzip_proxied expired no-cache no-store private auth;
gzip_disable "MSIE [1-6]\.";
}
</code></pre>
<ol start="3">
<li>配置后,查看是否生效</li>
</ol>
<img src="https://static.chanx.tech/image/vsekn_0.png" alt="image-20220612005339741" style="zoom:50%;" />
<ol start="4">
<li><code>LightHouse</code>进行测试</li>
</ol>
<p><img src="https://static.chanx.tech/image/tea1q_0.png" alt="img"></p>
<p><img src="https://static.chanx.tech/image/tei5k_0.png" alt="img"></p>
<p>开启Gzip压缩后测试:文件的体积大幅度减小,首屏加载时间FCP也从<strong>4s变为了1.2s</strong></p>
<h3>Gzip静态压缩</h3>
<p>上面提及了Gzip的动态压缩,在请求到达的时候匹配到相应的压缩规则进行压缩,尽可能的降低文件大小来提高加载速度,实际上就是一个用服务器<strong>计算性能换取网络性能</strong>的操作。</p>
<p>假设缓存失效,大量访问涌入服务器将会占用大量的计算资源用以压缩;而静态资源文件没有变化,无需每次访问都重新压缩,于是就可以有「一次压缩,多次使用」的方法,即「<strong>静态压缩</strong>」。</p>
<ol>
<li>配置Nginx服务器,开启静态压缩</li>
</ol>
<pre><code class="language-nginx">http {
gzip_static on;
}
</code></pre>
<ol start="2">
<li>配置webpack,输出经过gzip压缩的产物</li>
</ol>
<p>安装<code>compression-webpack-plugin</code>插件<code>yarn add compression-webpack-plugin -D</code></p>
<pre><code class="language-javascript">// vue-cli项目配置vue.config.js
const CompressionWebpackPlugin = require("compression-webpack-plugin");
const productionGzipExtensions = /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i;
configureWebpack: {
plugins: [
new CompressionWebpackPlugin({
filename: "[path][base].gz", // 输出的文件名称
algorithm: "gzip", // 压缩算法
test: productionGzipExtensions, // 文件名匹配规则
threshold: 10240, // 压缩的文件最小值
minRatio: 0.8, // 压缩的最小压缩率,压缩率较低的不压缩
deleteOriginalAssets: false, // 压缩后删除源文件
}),
],
}
</code></pre>
<p>注意:出现<code>Cannot read property 'tapPromise' of undefined</code>错误提示的请<strong>降低插件的版本</strong></p>
<ol start="3">
<li>部署服务器,查看是否生效</li>
</ol>
<p><img src="https://static.chanx.tech/image/teemm_0.png" alt="img"></p>
<ol start="4">
<li>使用<code>LightHouse</code>进行测试</li>
</ol>
<p>加载速度没有明显的变化,想想应该是存在并发量才能观察出来差异,这里不再深入</p>
<h3>第三方库按需引入</h3>
<p>使用element-ui、echarts等第三方库时,可以根据相应的文档使用按需引入,减少无用代码打包</p>
<h3>第三方库使用CDN引入</h3>
<p><code>student.js</code>的加载直接影响FCP,而<code>Coding.js</code>和<code>Personal.js</code>通过<code>prefetch</code>进行预加载</p>
<p>所以想进一步提高首次加载速度,需要考虑优化<code>student.js</code></p>
<p>vue-cli下自带prefetch和preload的优化,优化时可以<a href="https://cli.vuejs.org/zh/guide/html-and-static-assets.html#prefetch">关闭相应的优化</a>便于查看首次加载文件</p>
<p>单页应用时:config.plugins.delete('prefetch')</p>
<p>多页应用时:config.plugins.delete('prefetch-XXX') 关闭对应页面XXX的prefetch插件</p>
<p><img src="https://static.chanx.tech/image/tep2m_0.png" alt="img"></p>
<ol>
<li>查看<code>report.html</code>中<code>student.js</code>的构成</li>
</ol>
<p>可以看到<code>element-ui</code>的体积占用较高;结合项目实际使用情况,项目内已使用的组件较多,按需加载优化效果不明显;而组件库作为必要依赖且一般不会变化,可以考虑抽离<strong>使用模板HTML引入(又称CDN引入)</strong>。</p>
<blockquote>
<p>有什么作用?使用外部免费CDN,加载速度较快,可以减少服务器的负载;公共依赖,利用缓存可以提高加载速度;但是需要注意的是外部免费CDN存在宕机(见附录)等隐患,可以通过切换备用CDN恢复,若不能接受此类情况,也可以把文件放在自己的服务器上;</p>
</blockquote>
<pre><code class="language-html"><!-- 引入多个CDN进行备份 -->
<script src="https://cdn.staticfile.org/vue/2.6.14/vue.min.js"></script>
<script src="https://cdn.staticfile.org/element-ui/2.15.7/index.min.js"></script>
<script>
window.Vue ||
document.write(
'<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js"><\/script>'
);
</script>
<script>
window.ELEMENT ||
document.write(
'<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/index.min.js"><\/script>'
);
</script>
</code></pre>
<ol start="2">
<li>配置webpack,在<code>build</code>的时候不打包<code>element-ui</code></li>
</ol>
<pre><code class="language-javascript">// 入口文件
import ELEMENT from "element-ui";
Vue.use(ELEMENT);
// vue-cli项目下配置vue.config.js文件
configureWebpack: {
externals: {
"element-ui": "ELEMENT",
}
}
// 模板HTMl文件
<head>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/index.js"></script>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/lib/theme-chalk/index.css" rel="stylesheet" type="text/css" />
</head>
</code></pre>
<p>此时需要注意项目中<strong>非Vue组件</strong>下引入组件库的方式都需要<strong>改为全部引入</strong>,避免webpack识别不了</p>
<pre><code class="language-javascript">// 某js文件原来
import Message from "element-ui/packages/message/src/main";
import { MessageBox } from "element-ui";
// 修改后
import ElEMENT from "element-ui";
const { MessageBox, Message } = ElEMENT;
// 样式通过
</code></pre>
<ol start="3">
<li>再次<code>build</code>,查看<code>report.html</code>文件,组件库成功抽离</li>
</ol>
<p><img src="https://static.chanx.tech/image/teqcm_0.png" alt="img"></p>
<ol start="4">
<li>部署到服务器上,使用<code>LightHouse</code>进行测试</li>
</ol>
<p><img src="https://static.chanx.tech/image/tef6b_0.png" alt="img"></p>
<p>利用外部CDN加载,首次加载速度FCP又有部分提高</p>
<blockquote>
<p>假设不使用外部CDN引入,加载速度会受限于服务器条件</p>
</blockquote>
<h3></h3>
<h3>路由懒加载</h3>
<p><code>build</code>时路由的每个组件各自打包,使用**<a href="https://webpack.js.org/guides/code-splitting/#dynamic-imports">动态导入</a>**</p>
<pre><code class="language-javascript">// router/index.js
const routes = [
{
path: "/login",
name: "Login",
component: () => import(/* webpackChunkName: 'Login' */ "@student/views/HomePage/Login"),
},
{
path: "/register",
name: "Register",
component: () => import(/* webpackChunkName: 'Register' */ "@student/views/HomePage/Register"),
},
]
</code></pre>
<h3>异步组件</h3>
<p><a href="https://cn.vuejs.org/v2/guide/components-dynamic-async.html#%E5%BC%82%E6%AD%A5%E7%BB%84%E4%BB%B6">动态组件 & 异步组件 — Vue.js</a> 实际上也是上面提到的动态导入</p>
<blockquote>
<p>以Coding页面为例</p>
</blockquote>
<img src="https://static.chanx.tech/image/v85up_0.png" alt="image-20220612005244299" style="zoom:50%;" />
<p>,需要对这个文件进行下一步的优化;</p>
<p>查看<code>report.html</code>,观察文件内各个内容的占用;</p>
<p><img src="https://static.chanx.tech/image/teoje_0.png" alt="img"></p>
<p>首先看到整个文件可以大致分为四块:<code>sv.js</code>、<code>ace-builds</code>、<code>swiper</code>、<code>splitpanes</code>;分别代表的是可视化面板、编辑器、测试数据面板、分割面板组件。</p>
<p><img src="https://static.chanx.tech/image/texwq_0.png" alt="img"></p>
<p><code>Coding.js</code>中引入可视化面板、堆栈面板、监视面板。它们在调试状态下并不会被使用到,也就是说页面首次加载时加载了部分无用内容。使用<strong>异步组件</strong>将这几个面板拆出来,让它们在进入调试状态下才去加载。</p>
<pre><code class="language-javascript">const DebugPane = () =>
import(/* webpackChunkName: 'debug-pane' */ "@student/components/debug-button-bar.vue");
const VisualPane = () => ({
component: import(
/* webpackChunkName: 'visual-pane' */ "@student/components/visual-components/visual-pane.vue"
),
loading: LoadingComponent,
});
const stackVariateShow = () => ({
component: import(
/* webpackChunkName: 'stack-pane' */ "@student/components/stack-variate/data-controller.vue"
),
loading: LoadingComponent,
});
const WatchPane = () => ({
component: import(/* webpackChunkName: 'watch-pane' */ "@student/components/watch-pane.vue"),
loading: LoadingComponent,
});
</code></pre>
<p><img src="https://static.chanx.tech/image/tet47_0.png" alt="img"></p>
<p>拆完包之后我们打开页面验证一下:</p>
<p><img src="https://static.chanx.tech/image/teqza_0.png" alt="img"></p>
<p><img src="https://static.chanx.tech/image/teq7g_0.png" alt="img"></p>
<p>可以看到在进入页面时多加载了几个js文件,单个文件的体积比较小,没有超过200K</p>
<p>Coding页面的首次加载优化已经算完成了,继续看看调试状态下的加载</p>
<p><img src="https://static.chanx.tech/image/ter49_0.png" alt="img"></p>
<p>点击调试后开始加载这几个组件,<code>visual-pane</code>因为<code>sv.js</code>的存在所以体积还是相对较大;</p>
<p>后续还可以对<code>sv.js</code>进行优化:使用CDN引入、在<code>sv.js</code>构建的时候拆成多个包</p>
<blockquote>
<p>这里没有对<code>sv.js</code>进行下一步优化的原因是:</p>
<ul>
<li><code>sv.js</code>处于快速迭代状态,为了方便该模块开发者测试,所以暂不考虑构建成多个包;</li>
<li>使用CDN引入,一般适合变化不多的静态资源
当然,还是有方案解决的,感兴趣可以了解一下<code>monorepo</code>或者其他,这里不再赘述</li>
</ul>
</blockquote>
<p>拆包时需要注意拆出的模块是否存在代码耦合,比如说加载完成后执行某个函数,这是需要进行处理的:import("XXX").then(() => { console.log("加载完成执行的函数"); })</p>
<p>部署后使用LighntHouse进行测试</p>
<p><img src="https://static.chanx.tech/image/tev2g_0.png" alt="img"></p>
<p>比较优化前后的测试结果:FCP从1.3s到1.0s,LCP从8s到2.7s</p>
<h3>使用HTTP/2</h3>
<p>HTTP/2下二进制和流的特性可以加快网站的访问速度,需要服务器和浏览器的支持</p>
<p>目前主流浏览器均已支持HTTP/2,服务器完成支持即可,需要配置<strong>HTTPS</strong>、<strong>Nginx</strong></p>
<h3>SourceMap</h3>
<p>生产环境把<code>sourcemap</code>关了也能减小部分文件大小</p>
<h2>总结</h2>
<p>全文主要围绕网页加载速度,选定<strong>单页应用</strong>(SPA)常见的<strong>首屏渲染</strong>(FCP)问题作为优化点。</p>
<p>上面的几个点基本都在围绕「文件大小」进行优化,尽可能的减少单个文件体积,实际上还有隐藏在打包阶段进行的<code>tree shaking(删除无用代码)</code>、<code>uglily(压缩)</code>、内联资源等进行的优化;</p>
<p>另外就是算法、网络通信上提升传输效率,像上面提到的gzip、http2、cdn,还有dns、缓存等。</p>
<p>更进一步,网页加载速度不只是资源文件的下载速度,其实还包括下载后资源文件的解析、渲染等,这些牵扯到浏览器的运行机制,优化难度更大更复杂。</p>
<h2>思考</h2>
<ol>
<li>CDN利用缓存提高速度,而不同CDN之间的缓存不能共用,有没有什么好的办法?</li>
</ol>
<blockquote>
<p>若浏览器支持类似contenthash形式的缓存,那么所有第三方缓存将会打通,所有引入第三方的网站访问速度都会提升。</p>
</blockquote>
<p>路由懒加载和异步组件实际上都是将单文件变为多文件,即「拆包」;</p>
<ol>
<li>那是不是首页变快了,后面每次点击都需要等待加载?用户体验不是更差?</li>
</ol>
<blockquote>
<p>这个跟应用实际情况有关系。拆包之后按需加载确实加快了首次访问,后续要等待加载这个却是可以优化的。<a href="https://cli.vuejs.org/zh/guide/html-and-static-assets.html#prefetch">Vue-cli项目打包默认加载prefetch和preload插件</a>,利用<code>**prefetch**</code>和<code>**preload**</code>属性,浏览器可以在后台进行无感的预加载,后续加载的时候就是使用<code>cache</code>了,反而让用户体验变得更好。当然,不排除某些情况下预加载没完成,这个时候做一个<code>loading</code>状态也不会影响体验。</p>
</blockquote>
<ol>
<li>拆包那么爽,那我全拆了不就行了吗?</li>
</ol>
<blockquote>
<p>拆包前先考虑原来的代码是否已经优化过,删除无用的代码和模块,无必要的拆包会增加文件数量和维护成本。如果拆出的包过小,反而会影响加载速度(HTTP/1.1),不然为什么小图片加载会有<strong>雪碧图</strong>方案呢?</p>
</blockquote>
<h2>附录</h2>
<p>2021-12-20</p>
<p>jsdelivr挂掉<a href="https://www.v2ex.com/t/823281">https://www.v2ex.com/t/823281</a></p>
<img src="https://static.chanx.tech/image/tf5yq_0.png" alt="img" style="zoom: 33%;" />
<p>国内节点挂了至少八小时,影响了BootCDN(笑)、echarts、部分npm包等...很严重</p>
]]>性能优化前端工程化前端Chanx ([email protected])
- 网页DarkModehttps://chanx.tech/blog/darkmodehttps://chanx.tech/blog/darkmode网页暗黑模式的实现方案与问题探讨Mon, 22 Nov 2021 00:00:00 GMT<![CDATA[<h2>目标</h2>
<p>主题化功能长期存在,可供用户前端自行切换</p>
<ul>
<li>LightMode:日间模式、浅色模式、白色模式</li>
<li>DarkMode:夜间模式、深色模式、黑色模式</li>
</ul>
<blockquote>
<p>中文表达较多,下文默认使用英文表达</p>
</blockquote>
<p>色板:</p>
<p><a href="https://materialui.co/colors">https://materialui.co/colors</a></p>
<p><a href="https://tailwindcolor.com/">Tailwind Color Palette</a></p>
<p><a href="https://antv.vision/zh/docs/specification/language/palette">https://antv.vision/zh/docs/specification/language/palette</a></p>
<p><strong>Ant Design</strong>关于配色的</p>
<p><a href="https://ant.design/docs/spec/colors-cn">https://ant.design/docs/spec/colors-cn</a></p>
<p><a href="https://ant.design/docs/spec/dark-cn">https://ant.design/docs/spec/dark-cn</a></p>
<p><a href="https://github.com/ant-design/ant-design/blob/5ab2783ff00d4b1da04bb213c6b12de43e7649eb/components/style/color/colors.less">https://github.com/ant-design/ant-design/blob/5ab2783ff00d4b1da04bb213c6b12de43e7649eb/components/style/color/colors.less</a></p>
<p>Leetcode是如何做Darkmode的?</p>
<p>CSS中<code>var()</code>和<code>:root</code>。root设置默认色板,切换成darkmode时添加<code>.dark</code>类名,该类下有主题色板,优先级覆盖默认色板,达到切换效果。</p>
<blockquote>
<p>叫法:C - s - s,var(哇!)</p>
</blockquote>
<h2>现有方案</h2>
<p>可以按变量出现的时间分为<strong>运行时</strong>和<strong>编译时</strong>两种类型的方案</p>
<h3>运行时方案</h3>
<ul>
<li>使用<code>css var</code>方式</li>
<li>使用<code>less</code>运行时方式,可以在线实时编译<code>less</code>代码</li>
<li>使用<code>js</code>操作样式代码</li>
</ul>
<h3>编译时方案</h3>
<p>使用<code>css预处理器</code>,使用<code>scss/sass</code>、<code>less</code>等预处理器在项目构建时生成多套样式代码</p>
<h2>预期问题</h2>
<ol>
<li>项目内无样式规范,已有样式不统一</li>
<li><code>ElementUI</code>样式处理</li>
<li>是否存在特殊组件不希望被主题切换影响</li>
<li>主题化方案,及色板</li>
</ol>
]]>JavascriptCSSDarkMode前端Chanx ([email protected])
- 前端工程化之Jenkinshttps://chanx.tech/blog/frontend-jenkinshttps://chanx.tech/blog/frontend-jenkins前端工程化之JenkinsSun, 14 Nov 2021 00:00:00 GMT<![CDATA[<blockquote>
<p>下文安装通过「war安装包」安装,使用Docker安装方式可忽略本文</p>
</blockquote>
<p><img src="https://static.chanx.tech/image/nvzng_0.png" alt="img"></p>
<h1>安装运行</h1>
<ol>
<li>安装Java环境</li>
<li>下载Jenkins的war包,<a href="https://www.jenkins.io/zh/download/">下载地址</a></li>
<li>运行war包<code>java -jar jenkins.war --httpPort=8080</code></li>
<li>打开<code>http://localhost:8080</code>,等待Jenkins启动</li>
<li>初次进入页面会要求输入初始密码,初始密码可以启动时在<code>shell</code>看到</li>
</ol>
<p><img src="https://static.chanx.tech/image/nw0j5_0.png" alt="img"></p>
<ol>
<li>跟随提示进行设置。设置管理员账户和密码、安装插件、设置网址等</li>
<li>完成</li>
</ol>
<h1>配置</h1>
<h3>推荐插件</h3>
<table>
<thead>
<tr>
<th><a href="https://plugins.jenkins.io/envinject">Environment Injector Plugin</a></th>
<th>用于注入环境变量</th>
</tr>
</thead>
<tbody><tr>
<td><a href="https://plugins.jenkins.io/git-parameter">Git Parameter Plug-In</a></td>
<td>构建时选择git参数</td>
</tr>
<tr>
<td><a href="https://plugins.jenkins.io/publish-over-ftp">Publish Over FTP</a></td>
<td>执行FTP相关操作</td>
</tr>
<tr>
<td><a href="https://plugins.jenkins.io/publish-over-ssh">Publish Over SSH</a></td>
<td>执行Shell相关操作</td>
</tr>
<tr>
<td><a href="https://plugins.jenkins.io/nodejs">NodeJS Plugin</a></td>
<td>安装Nodejs环境</td>
</tr>
</tbody></table>
<h1></h1>
<h3>配置Git</h3>
<ol>
<li>Jenkins服务器生成Git密钥,<a href="https://git-scm.com/book/zh/v2/%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%B8%8A%E7%9A%84-Git-%E7%94%9F%E6%88%90-SSH-%E5%85%AC%E9%92%A5">Git - 生成 SSH 公钥</a></li>
<li>将<code>id_dsa</code>私钥配置到Jenkins凭证当中</li>
</ol>
<p><img src="https://static.chanx.tech/image/nw3sc_0.png" alt="img"></p>
<ol>
<li>将<code>id_dsa.pub</code>公钥配置到Git Server中</li>
</ol>
<p><img src="https://static.chanx.tech/image/nw2z6_0.png" alt="img"><img src="https://static.chanx.tech/image/nw2rp_0.png" alt="img"></p>
<h3>配置FTP Server</h3>
<p><img src="https://static.chanx.tech/image/nw3bb_0.png" alt="img"><img src="https://static.chanx.tech/image/nvyl8_0.png" alt="img"></p>
<h3>配置Node环境</h3>
<p><img src="https://static.chanx.tech/image/nw8qr_0.png" alt="img"><img src="https://static.chanx.tech/image/nwa3q_0.png" alt="img"></p>
<h1>尝试新建一个构建项目</h1>
<ol>
<li><p>新建构建项目,并进入配置页面</p>
</li>
<li><p>配置「构建前选择代码分支」(需要安装插件)</p>
</li>
</ol>
<p><img src="https://static.chanx.tech/image/nwc4b_0.png" alt="img"></p>
<ol start="3">
<li>配置Git仓库地址,没有凭证则需要添加</li>
</ol>
<p><img src="https://static.chanx.tech/image/nw8ep_0.png" alt="img"></p>
<ol start="4">
<li>配置构建命令,通过Shell命令执行</li>
</ol>
<p><img src="https://static.chanx.tech/image/nw6ru_0.png" alt="img"></p>
<ol start="5">
<li>构建后进行打包保存和FTP服务器部署</li>
</ol>
<p><img src="https://static.chanx.tech/image/nwgrz_0.png" alt="img"></p>
<ol start="6">
<li>保存配置,执行构建</li>
</ol>
<p><img src="https://static.chanx.tech/image/nwhz9_0.png" alt="img"></p>
<ol start="7">
<li>等待构建完成</li>
</ol>
<p>一个简单的前端项目构建流程就是上面这些,除此之外可以通过Shell、插件、Jenkins自带的环境变量等实现更定制化的功能。比如<code>node_modules</code>的缓存提高构建速度、通过构建参数执行不同环境的打包。</p>
<pre><code class="language-bash"> # 通过判断package.json的md5来确认是否需要yarn安装依赖
CACHE_FOLDER=${HOME}/md5_cache/md5
echo "EXECUTOR_NUMBER: ${EXECUTOR_NUMBER}"
MD5_FILE_NAME=package-json_${EXECUTOR_NUMBER}.md5sum
[ -d ${CACHE_FOLDER} ] || mkdir -p ${CACHE_FOLDER}
ls ${CACHE_FOLDER}
if [ -f ${CACHE_FOLDER}/${MD5_FILE_NAME} ];then
cp ${CACHE_FOLDER}/${MD5_FILE_NAME} ${MD5_FILE_NAME}
md5sum package.json
cat ${MD5_FILE_NAME}
md5sum -c ${MD5_FILE_NAME} || yarn
else
echo "No md5sum backup"
yarn
fi
echo "create new md5sum backup"
md5sum package.json
md5sum package.json > ${MD5_FILE_NAME}
cp ${MD5_FILE_NAME} ${CACHE_FOLDER}
</code></pre>
<p><strong>总结:相比于Gitlab,Jenkins在代码管理并不擅长,更多是侧重于构建部署。</strong></p>
]]>前端工程化前端Chanx ([email protected])
- Linux下部署Minecraft服务器https://chanx.tech/blog/linux-minecraft-serverhttps://chanx.tech/blog/linux-minecraft-server详细介绍如何在Linux系统上搭建和配置Minecraft服务器,包括Java环境配置、服务端安装和常见问题解决方案Sun, 24 Oct 2021 14:19:56 GMT<![CDATA[<p><a href="https://blog.csdn.net/moqianmoqian/article/details/105210028">xftp、xshell连接远程服务器,所选的用户密钥未在远程主机上注册_墨浅-CSDN博客</a></p>
<ol>
<li>连接<code>shell</code>和<code>ftp</code></li>
<li>下载<code>JAVA</code>并上传安装</li>
</ol>
<p>注意安装jdk8,高版本不兼容</p>
<ul>
<li>执行解压<code>tar -zxvf jdk-linux-x64.tar.gz</code></li>
<li>执行<code>vim /etc/profile</code>进入环境变量编辑,末尾加入下面配置并保存</li>
</ul>
<pre><code class="language-shell">JAVA_HOME=/root/jdk1.8.0_131
CLASSPATH=$JAVA_HOME/lib/
PATH=$PATH:$JAVA_HOME/bin
export PATH JAVA_HOME CLASSPATH
</code></pre>
<ul>
<li>重新加载配置<code>source /etc/profile</code></li>
<li>验证是否安装成功<code>java -version</code></li>
</ul>
<ol>
<li>上传服务端</li>
</ol>
<pre><code>问题:These libraries failed to download. Try again.
</code></pre>
<p>下载依赖不成功,需要单独下载相应的包</p>
]]>MinecraftLinux服务器其他Chanx ([email protected])
- 重新认识 window.open()https://chanx.tech/blog/reintroducing-window-openhttps://chanx.tech/blog/reintroducing-window-open重新认识 window.open()Sun, 15 Aug 2021 00:00:00 GMT<![CDATA[<h1>window.open的返回值</h1>
<p>重新认识一下这个函数<code>window.open(url, ``*windowName, windowFeatures)*</code></p>
<p>执行<code>window.open</code>函数,我们可以打开一个新的标签页</p>
<p><strong>其实,它是有返回值的,它返回新窗口的引用</strong><code>**WindowProxy**</code></p>
<p>通过返回值这个新窗口的引用,我们可以做一些符合同源策略的操作</p>
<blockquote>
<p>If the window couldn't be opened, the returned value is instead <code>null</code>. The returned reference can be used to access properties and methods of the new window as long as it complies with <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy">Same-origin policy</a> security requirements.</p>
</blockquote>
<p>那么,我们可以这么操作(A标签页下通过<code>window.open</code>打开B标签页):</p>
<ul>
<li>A和B标签页同源的情况下,在A标签页通过<strong>新窗口引用</strong>操作B的<code>localStorage</code></li>
</ul>
<p><img src="https://static.chanx.tech/image/12osuh7_0.png" alt="img"></p>
<ul>
<li>A和B标签页不同源的情况下,在A标签页通过<strong>新窗口引用</strong>操作B的<code>localStorage</code></li>
</ul>
<p><img src="https://static.chanx.tech/image/12ot31t_0.png" alt="img"></p>
<p>学到了学到了,但是好像并没有什么用...</p>
<p>尝试一下改变<code>location</code>?<strong>你会发现标签页进行了跳转。</strong></p>
<p><img src="https://static.chanx.tech/image/12ot89f_0.png" alt="img"></p>
<h1>window.opener</h1>
<p>上面讲的是「父标签页」操作「子标签页」,这里讲的就是相反的。</p>
<p>标签页在打开的时候会在<code>window.opener</code>存着对父标签页的引用</p>
<p>跟前面所讲到的一样,通过引用可以执行一些操作,比如修改父标签页的<code>location</code>达到重定向的效果</p>
<p>如果<code>window.open</code>打开的是一个恶意网址B,B修改A的<code>location</code>,就可以偷偷的把你的A转到钓鱼网址</p>
<h1>多种打开新标签页的方式</h1>
<ol>
<li>带上<code>_blank</code>属性</li>
</ol>
<pre><code class="language-html"><a href="https://www.feishu.cn/" target="_blank">打开新的标签页</a>
</code></pre>
<ol>
<li>带上<code>_blank</code> 属性并且带 <code>opener</code></li>
</ol>
<pre><code class="language-html"><a href="https://www.feishu.cn/" rel="opener" target="_blank">打开新的标签页</a>
</code></pre>
<ol>
<li>带上 <code>_blank</code> 属性并且带<code>noopener</code></li>
</ol>
<pre><code class="language-html"><a href="https://www.feishu.cn/" rel="noopener" target="_blank">打开新的标签页</a>
</code></pre>
<ol>
<li><code>window.open()</code> 且不清除 <code>opener</code> 的值</li>
</ol>
<pre><code class="language-typescript"><span class="link" onclick="openNewTabWithOpener()">打开新的标签页</span>
// js
function openNewTabWithOpener(){
window.open("真实地址");
}
</code></pre>
<ol>
<li><code>window.open()</code>后再清除 <code>opener</code></li>
</ol>
<pre><code class="language-typescript"><span class="link" onclick="openNewTabWithoutOpener()">打开新的标签页</span>
// js
function openNewTabWithoutOpener(){
window.open("");
window.opener = null;
window.location = "真实地址";
}
</code></pre>
<ol>
<li><code>window.open()</code>时带 <code>noopener</code>属性</li>
</ol>
<pre><code class="language-typescript"><span class="link" onclick="openNewTabWithoutOpener()">打开新的标签页</span>
// js
function openNewTabWithoutOpener(){
window.open("真实地址","_blank","noopener")
}
</code></pre>
<h1><code>noopener</code>一定要加上吗?</h1>
<ol>
<li><code>chrome88</code>版本对超链接的<code>rel</code>默认设置为<code>noopener</code></li>
</ol>
<blockquote>
<p>舒舒服服,默认就有,不用加了</p>
</blockquote>
<ol>
<li><code>chrome89</code>版本<code>noopener</code>终止<code>clone sessionStorage</code></li>
</ol>
<blockquote>
<p>要是登录态存在session,那<code>noopener</code>打开一个相同网址的标签页就要重新登录</p>
</blockquote>
<blockquote>
<p>上述新标签页打开方式3和6就受这条更新的影响</p>
</blockquote>
<p>所以,<code>noopener</code>并不是随便加的。在不确认新链接指向哪的时候,加上<code>noopener</code>比较稳妥;而在明确的可信链接指向下,加不加就看心情。</p>
<p>但是需要注意,由于<code>chrome89</code>版本的这一特性,标签页使用<code>a标签</code>还需要考虑加上<code>rel="opener"</code>。</p>
<h1>补充</h1>
<p>在代码中执行<code>window.open</code>经常会被浏览器或者是一些插件认为上不友好的行为,然后被拦截</p>
<blockquote>
<p>想起被弹窗广告支配的时代吗?</p>
</blockquote>
<p>怎么优雅地用<code>JavaScript</code>打开一个新标签页?</p>
<p><strong>新建一个</strong><code>**<a>**</code><strong>并加上</strong><code>**noopener**</code><strong>属性,再触发点击</strong></p>
<pre><code class="language-typescript"><span class="link" onclick="openNewTab()">打开新的标签页</span>
// js
function openNewTab(){
const link = document.createElement("a");
link.href = "真实地址";
link.rel = "noopener"; // 这个视情况决定
link.target = "_blank";
link.click();
}
</code></pre>
<p>当然,除了<code>noopener</code>这一属性外,还有<code>noreferrer</code>属性来设置新标签页请求的<code>referer</code></p>
]]>JavascriptHTML前端Chanx ([email protected])
- 前端函数或请求的聚合https://chanx.tech/blog/frontend-function-aggregationhttps://chanx.tech/blog/frontend-function-aggregation前端函数或请求的聚合Wed, 31 Mar 2021 00:00:00 GMT<![CDATA[<h2>场景</h2>
<ul>
<li>某个请求被重复触发</li>
<li>某个函数被重复触发</li>
</ul>
<p>多次触发合并成一次,或一段时间内触发合并成一次。思路其实与<strong>节流</strong>类似</p>
<p>为什么?</p>
<ul>
<li>具体业务场景需要。如<code>element-ui</code>中上传组件多选文件会多次触发上传钩子,需要合并成一次上传</li>
<li>性能优化考虑。某个高频的http请求进行聚合,降低请求频率</li>
</ul>
<p><strong>先举个例子,某个上传函数被多次触发</strong></p>
<pre><code class="language-js">const upload = (filename) => {
console.log("正式上传", filename);
}
upload("a");
upload("b");
upload("c");
</code></pre>
<blockquote>
<p>正式上传 a 正式上传 b 正式上传 c</p>
</blockquote>
<p>需求: 希望多个文件能放在一次正式上传</p>
<h2>宏任务</h2>
<pre><code class="language-js">const file = [];
const upload = (() => {
let collect = null;
return function(filename){
file.push(filename); // 记录要上传的文件
if(!collect){
collect = setTimeout(()=>{
console.log("正式上传", file);
collect = null;
},0);
}
}
})();
upload("a");
upload("b");
upload("c");
</code></pre>
<blockquote>
<p>正式上传 ["a", "b", "c"]</p>
</blockquote>
<p><strong>思路:三次上传钩子实际上是同步代码,宏任务会在同步代码执行完成后再去触发。利用闭包记录下是否已经设置宏任务和需要上传的文件</strong></p>
<h2>微任务</h2>
<pre><code class="language-js">const file = [];
const upload = (() => {
let collect = null;
return function(filename){
file.push(filename);
if(!collect){
collect = Promise.resolve().then(()=>{
console.log("正式上传", file);
collect = null;
})
}
}
})();
upload("a");
upload("b");
upload("c");
</code></pre>
<blockquote>
<p>正式上传 ["a", "b", "c"]</p>
</blockquote>
<p><strong>思路:其实跟宏任务一样,只不过<code>promise.then</code>会在同步代码后,<code>setTimeout</code>前触发</strong></p>
<h2>小结</h2>
<p>总的来说,就是对原有的上传钩子进行了<strong>封装</strong>,或者叫<strong>代理</strong>。并利用<strong>事件循环</strong>的执行顺序,在合适的时机再去执行上传操作。</p>
<blockquote>
<p>Q:那是不是只有以上两种方案呢?当然不是。思路一致,但是实现各有不同。</p>
<p>A:比如说,知道触发次数,可以考虑使用计数器在最后一次触发;标记变量可以使用其他写法来实现;</p>
<p>Q:我只想聚合一部分,怎么办?</p>
<p>A:<code>setTimeout</code>设置时间;或者设置临界值再推几个宏/微任务,注意记录文件做相应修改</p>
<p>Q:那异步触发的怎么去做聚合?</p>
<p>A:我可能是利用一下<code>Promise.all</code>吧。没有深入想,此处不多说。</p>
<p>说到聚合 + 性能优化,其实还有后端接口的聚合。利用中间层进行接口聚合,前端再去请求聚合后的接口,也能优化前端体验</p>
</blockquote>
]]>Javascript设计模式及应用Element性能优化前端Chanx ([email protected])
- JS实现拖拽移动元素https://chanx.tech/blog/drag-drophttps://chanx.tech/blog/drag-dropJS实现拖拽移动元素Mon, 23 Nov 2020 00:00:00 GMT<![CDATA[<p><strong>实现的效果如图</strong></p>
<img src="https://static.chanx.tech/image/a7gx3_0.gif" alt="录制_2020_11_23_20_10_35_670" style="zoom:50%;" />
<p><strong>实现步骤:</strong></p>
<ul>
<li>鼠标按下时进入拖拽状态<code>onmousedown</code></li>
<li>鼠标移动时,如果是拖拽状态,则元素跟随移动<code>onmousemove</code></li>
<li>鼠标弹起或鼠标离开元素范围时退出拖拽状态<code>onmouseup</code>和<code>onmouseleave</code></li>
</ul>
<img src="https://static.chanx.tech/image/a7f7o_0.png" style="zoom:50%;" />
<p><strong>如何跟随移动?计算两个鼠标指针的距离差,然后应用到目标元素上</strong></p>
<p>需要注意的是(我没做的):</p>
<ul>
<li>若是对鼠标点击有要求,需要判断是否为左键</li>
<li>若是对移动范围有要求,需要做相应处理禁止移动</li>
<li>若是对性能有要求,记得做<strong>防抖</strong>处理</li>
</ul>
<p>简单的实现拖拽代码如下:</p>
<pre><code class="language-html"><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>拖拽移动元素</title>
<style>
#tools {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
height: 100px;
width: 100px;
background-color: cadetblue;
cursor: move;
}
</style>
</head>
<body>
<div id="tools"></div>
<script>
let start = null; // 鼠标位置记录
let isDrag = false; // 拖拽状态
const tools = document.getElementById("tools");
// 鼠标按下的时候记录初始鼠标位置
tools.onmousedown = function (e) {
start = {
x: e.x,
y: e.y,
};
isDrag = true;
}
// 鼠标抬起时结束拖拽
tools.onmouseup = function (e) {
isDrag = false;
}
// 鼠标移出目标元素范围自动结束拖拽
tools.onmouseleave = function (e) {
isDrag = false;
}
// 鼠标移动时移动目标元素
tools.onmousemove = function (e) {
if (isDrag) {
// 计算两次鼠标的位置差
let distance = {
x: e.x - start.x,
y: e.y - start.y,
}
// 移动目标元素
tools.style.left = tools.offsetLeft + distance.x + 'px';
tools.style.top = tools.offsetTop + distance.y + 'px';
// 保存本次鼠标位置
start.x = e.x;
start.y = e.y;
}
}
</script>
</body>
</html>
</code></pre>
]]>Javascript前端Chanx ([email protected])
- HTML+CSS实现打字机效果https://chanx.tech/blog/typewriter-effecthttps://chanx.tech/blog/typewriter-effectHTML+CSS实现打字机效果Sat, 21 Nov 2020 00:00:00 GMT<![CDATA[<p>看到了一些Hexo博客首页用到了打字机效果。于是思考不使用Javascript,如何实现打字机效果呢?</p>
<!-- more -->
<p><img src="https://static.chanx.tech/image/6oau0_0.gif" alt=""></p>
<p>一个字总结:<strong>丑</strong>。</p>
<p><strong>实现:利用css中<code>animiation</code>实现关键帧组成循环动画。<code>width</code>减少模仿字体被删除;<code>border-right</code>黑白变色模仿光标闪烁</strong></p>
<p>直接上源码:</p>
<pre><code class="language-html"><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#demo {
white-space: nowrap;
font-family: 'Courier New', Courier, monospace;
width: 250px;
font-size: 2em;
font-weight: 700;
overflow: hidden;
animation: first 6s step-end infinite;
display: inline-block
}
#key {
margin-top: -200px;
line-height: 50px;
font-size: 1em;
color: blue;
}
@keyframes first {
0% {
width: 250px;
border-right: 3px solid black;
}
5% {
width: 250px;
border-right: 3px solid white;
}
10% {
width: 250px;
border-right: 3px solid black;
}
20% {
width: 200px;
border-right: 3px solid white;
}
25% {
width: 200px;
border-right: 3px solid black;
}
30% {
width: 200px;
border-right: 3px solid white;
}
40% {
width: 150px;
border-right: 3px solid black;
}
45% {
width: 150px;
border-right: 3px solid white;
}
50% {
width: 150px;
border-right: 3px solid black;
}
60% {
width: 100px;
border-right: 3px solid white;
}
80% {
width: 50px;
border-right: 3px solid black;
}
100% {
width: 0px;
border-right: 3px solid white;
}
}
</style>
</head>
<body>
<span id="demo">THIS DEMO</span>
</body>
</html>
</code></pre>
]]>前端Chanx ([email protected])
- 前端AES加密的简单应用https://chanx.tech/blog/aes-encryptionhttps://chanx.tech/blog/aes-encryption前端AES加密的简单应用Sat, 21 Nov 2020 00:00:00 GMT<![CDATA[<p>本文并不会系统地讲AES及其应用,只是遇到案例学习了然后记录一下</p>
<!-- more -->
<p>再次说明,本文并不是为了系统学习和比较各种加密方式...(我还不会)</p>
<h2>什么是AES</h2>
<p>高级加密标准(AES,Advanced Encryption Standard)为最常见的对称加密算法。对称加密算法也就是<strong>加密和解密用相同的密钥</strong></p>
<h2>如何利用AES</h2>
<p>首先AES这家伙只要你知道密钥就能加密解密的,而前端代码一般是公开的,或者说是破解可能需要一定成本。最终也只是为了数据不要明文存着让人一眼看出这个样子罢了。遇到了两个例子,分享一下。</p>
<h3>数据传输加密</h3>
<blockquote>
<p>背景:遇上了某qt应用,学校用其发布活动,但各种功能极其不人性化,于是想通过前端代码找到接口做个定时器抓取数据,过程中遇到一些小困难(知识点)。</p>
</blockquote>
<p>如题,前端后端交互的时候数据会被丢来丢去。要是中途被人截胡,明文的话可能会导致某些数据的泄露,加个密就能让数据难看一点。</p>
<h4>固定密钥</h4>
<p>固定密钥就是前后端约定好一个密钥,然后写死在代码里面就完事。我用密钥<code>“123456”</code>加密,你用<code>“123456”</code>解密,你说这好吗?</p>
<h4>变化密钥</h4>
<p>比固定密钥高大上一点点...就是密钥能通过逻辑生成,每次不是同一个</p>
<p>但是是同一套逻辑,花点时间搞懂就也就能生成密钥了(不然哪里来的本文</p>
<p>密钥是变化的,那么前后端如何都知道密钥是什么呢?</p>
<blockquote>
<p> 张三:这个我知道!把密钥放在请求里送给后端</p>
<p>我:那密钥明文放在请求里我加密有什么用?</p>
<p>李四:那就把密钥放在密文里</p>
<p>我:???后端不知道密钥,看着密文拿头去解密是吧</p>
<p>张三李四:有道理,那怎么办?</p>
<p>我:整点看起来比较正常而且经常变化的<strong>数据</strong>去生成密钥不就得了,然后同时把<strong>数据</strong>发给后端</p>
</blockquote>
<p>以上提到的一个数据实例便是:<strong>时间戳</strong></p>
<p>时间戳这种看起来普普通通的东西 => 好东西</p>
<p><strong>前端利用时间戳生成密钥并对数据进行加密,再把时间戳附带在请求头里</strong></p>
<p><strong>后端接收到数据密文的时候,提取请求头的时间戳生成密钥,再去解密</strong></p>
<p>如果直接就用时间戳当密钥的话,那真的是不太行噢。那我们就对时间戳做些简单处理,要是有人拿时间戳去碰碰,也能让他碰一脸灰!</p>
<hr>
<p><strong>下面就是案例具体代码,仅提供思路,不能直接跑。敏感部分已去掉</strong></p>
<p>涉及到的js库有:<a href="https://github.com/emn178/js-md5">js-md5</a>、<a href="https://github.com/brix/crypto-js">crypto-js</a></p>
<p><strong>时间戳混淆/获取密钥</strong></p>
<pre><code class="language-javascript">function u(t) {
var e = kmd5("5%^&#@*321!`~,;:" + t + "TT_APP000000';-="), //加长版时间戳利用md5生成固定长度字符串
n = parseInt(t / 1e3);
e = e.split("");
// 再做处理,为了生成密钥
let i = (e = n % 2 == 0 ? e.filter(function(t, e) {
return e % 2 == 0
}) : e.filter(function(t, e) {
return e % 2 != 0
})).join("");
return CryptoJS.enc.Utf8.parse(i)
}
</code></pre>
<p><strong>参数加密</strong></p>
<p>参数t为字符串类型,请用<code>JSON.stringify</code>处理相关类型的数据,e为请求头中的时间戳</p>
<pre><code class="language-javascript">function encryptHttp(t, e) {
let n = u(e),
i = CryptoJS.enc.Utf8.parse(t);
return CryptoJS.AES.encrypt(i, n, {
mode: CryptoJS.mode.ECB
}).toString()
}
</code></pre>
<p>加密后还需做相关处理(视具体情况</p>
<pre><code class="language-javascript">let miwen = encryptHttp(data,time)
miwen = encodeURI(miwen);
miwen = miwen.replace("+", "%2B");
// 最后把密文放进参数里
{
params:miwen
}
</code></pre>
<p><strong>响应解密</strong></p>
<p>参数t为响应的密文,e为请求头中的时间戳</p>
<pre><code class="language-javascript">function decryptHttp(t, e) {
let n = u(e);
let i = CryptoJS.AES.decrypt(t, n, {
mode: CryptoJS.mode.ECB
});
return CryptoJS.enc.Utf8.stringify(i).toString()
}
</code></pre>
<h3>数据持久化加密</h3>
<p>数据持久化保存到浏览器<code>storage</code>里,明文保存可能也不太好。那就加个密再丢进去吧!</p>
<h4>固定密钥</h4>
<p>一般也就用这个办法了。比如说<code>Vuex持久化</code>,在刷新前保存数据,刷新后读取数据。</p>
<h4>变化密钥</h4>
<p>没仔细想过,要实现的话估计也就是上面那个思路,找些普普通通的当变化源</p>
<hr>
<p>下面是<code>Vuex持久化</code>一个案例,仅提供思路,不保证能使用:</p>
<pre><code class="language-js"> // 不使用持久化插件:插件每次操作vuex都会触发storage保存,vuex操作比较多的时候有一定影响且插件保存数据为明文信息
// 页面加载时读取缓存数据到vuex
if (sessionStorage.getItem("miwen")) {
// 取缓存中经过加密的密文
const saveData = JSON.parse(CryptoJS.AES.decrypt(sessionStorage.getItem("miwen"), "123456").toString(CryptoJS.enc.Utf8));
// 数据导入到vuex
this.$store.replaceState(Object.assign(this.$store.state, saveData));
}
// 在页面刷新时将vuex里的信息保存到sessionStorage里
window.addEventListener("beforeunload", () => {
// 选择需要保存的数据
const saveKeys = ["user", "token"];
const saveData = {};
const state = this.$store.state;
saveKeys.forEach(key => {
saveData[key] = state[key];
});
// 取需要保存的数据并进行AES加密
const ciphertext = CryptoJS.AES.encrypt(JSON.stringify(saveData), "123456").toString();
sessionStorage.setItem("miwen", ciphertext);
});
</code></pre>
]]>前端Chanx ([email protected])
- 奇奇怪怪的Javascripthttps://chanx.tech/blog/weird-javascripthttps://chanx.tech/blog/weird-javascript奇奇怪怪的JavascriptWed, 18 Nov 2020 00:00:00 GMT<![CDATA[<p>Javascript是最好的语言(滑稽.jpg</p>
<h2>浮点数精度</h2>
<h2><code>navigator.onLine</code>真的可以实现断网检测吗?</h2>
<p>摘录自红宝书:浏览器会跟踪网络连接状态并以两种方式暴露这些信息:<strong>连接事件和<code>navigator.onLine</code>属性</strong>。在设备连接导网络时,浏览器会记录这个事实并在<code>window</code>对象上触发<code>online</code>事件。相应地,断开网络连接后,浏览器会在<code>window</code>对象上触发<code>offline</code>事件。任何时候,都可以通过<code>navigator.onLine</code>属性来确定浏览器的联网状态。这个属性返回一个布尔值,表示浏览器是否联网。</p>
<blockquote>
<p>哦吼!那我们直接利用他们不就达到想要的效果了吗?</p>
</blockquote>
<p>红宝书后面补充了这么一句话:<strong>到底怎么才算联网取决于浏览器与系统实现。有些浏览器可能会认为只要连接到局域网就算"在线",而不管是否真正接入了互联网</strong>。</p>
<blockquote>
<p>这么说来,这玩意还是不靠谱。看浏览器具体实现,那我还不如拿头去给你断网检测。回到重点,我对谷歌和火狐进行了同样的尝试,发现两者表现一致。能在网线拔掉和插入的时候触发相应事件,但我没有对有线连接局域网这一方式进行尝试。另外我发现在无线网络的条件下,断开连接网络貌似不会触发事件,真的是越来越奇怪了</p>
</blockquote>
<blockquote>
<p>由于websocket断网时有超时断开,所以最后我断网检测利用的是ws中open和close事件实现,其中websocket有实现心跳机制</p>
</blockquote>
<h2>时间戳真的是准确的吗?</h2>
<p>利用<code>new Date().getTime()</code>去获取一个最新的时间戳,<strong>实际上它跟系统的时间设置有关</strong>。假设用户的时间并不准确,对于一些基于时间戳的逻辑操作而言,是存在非常大的隐患的。<strong>举例来说</strong>,一个用时间戳判定实现的倒计时器,是可以通过修改系统时间来实现延长时间的这么一个功能;基于时间戳实现的节流操作,会出现时间戳的差为负数的情况,导致节流失效并烂掉</p>
<p>那么,我们修改了系统时间,他就会立刻反应到浏览器上的吗?</p>
<p>经过我的尝试,谷歌浏览器应该是在系统到达新的一分钟的时候会更新到浏览器上(简单尝试,不保证如此</p>
]]>Javascript前端Chanx ([email protected])
- 你可能需要Windows Terminalhttps://chanx.tech/blog/windows-terminalhttps://chanx.tech/blog/windows-terminal介绍Windows Terminal终端工具的安装配置和使用技巧,帮助你提高命令行操作效率Tue, 27 Oct 2020 00:00:00 GMT<![CDATA[<p>一款新的windows终端程序</p>
<!-- more -->
<p>不说废话,先上图</p>
<p><img src="https://static.chanx.tech/image/8jfb5_0.png" alt="image-20201027195830982"></p>
<p><img src="https://static.chanx.tech/image/8k1vk_0.png" alt="image-20201027195415759"></p>
<p>经常使用vscode跑服务,终端不想看的时候需要进行拖拉,不然遮挡代码</p>
<p>直接打开cmd跑吧,多窗口不好管理</p>
<p><strong>那就用Windows Terminal!Ohhhhhhhhhhhhhhhh</strong></p>
<h2>Windows Terminal作为主要命令行工具</h2>
<p>首先从微软商店下载好,并确认可以使用</p>
<p><a href="https://www.microsoft.com/zh-cn/p/windows-terminal-preview/9n0dx20hk701?activetab=pivot:overviewtab#">https://www.microsoft.com/zh-cn/p/windows-terminal-preview/9n0dx20hk701?activetab=pivot:overviewtab#</a></p>
<h3>右键打开Terminal</h3>
<p>你可能还想更方便地打开,如</p>
<img src="https://static.chanx.tech/image/8ksri_0.png" alt="image-20201027200001700" style="zoom: 50%;" />
<ul>
<li><p><code>win + R</code>打开运行窗口,输入<code>regedit</code></p>
</li>
<li><p>打开<code>计算机\HKEY_CLASSES_ROOT\Directory\Background\shell</code></p>
</li>
<li><p>右键新建项命名为<code>wt</code>,修改默认值为你想要的名字如<code>在此打开 Terminal</code></p>
</li>
<li><p>如有需要的话可以将这个命令置顶,则添加新字符串值<code>Position</code>,值为<code>Top</code></p>
</li>
<li><p>新建一个项<code>command</code>,默认值<code>%USERPROFILE%\AppData\Local\Microsoft\WindowsApps\wt.exe</code></p>
<p>注意:如失效,可尝试将%USERPROFILE%改为指定用户目录如<code>C:\Users\<username></code>,并重启,<code><username></code>记得改成自己的用户名</p>
</li>
</ul>
<p>一般出现<code>explorer.exe</code>错误多是没有用管理员权限写入导致的</p>
<h3>在当前目录打开</h3>
<p>添加一行配置<code>"startingDirectory": null,</code></p>
<p>目的是从目录右键打开时终端路径在你当前的目录下</p>
<p><img src="https://static.chanx.tech/image/8ly7m_0.png" alt="image-20201031111710347"></p>
<p><img src="https://static.chanx.tech/image/8mjgl_0.png" alt="image-20201031111732159"></p>
<p>其他配置文件可以查看<a href="https://aka.ms/terminal-profile-settings">官方文档</a></p>
]]>Windows终端工具其他Chanx ([email protected])
- vue多页面应用配置https://chanx.tech/blog/vue-pageshttps://chanx.tech/blog/vue-pagesvue多页面应用Tue, 27 Oct 2020 00:00:00 GMT<![CDATA[<p>首先搭建一个脚手架项目,<code>vuecli4 + vue2默认配置 + router + vuex</code>。此时得到一个原始的<strong>vue单页面应用</strong>项目,去掉组件。</p>
<h2>多页面应用结构</h2>
<h3>了解单页面</h3>
<p>在项目根目录下新建<code>vue.config.js</code>文件,我们先看看默认配置了解一下单页应用</p>
<p><img src="https://static.chanx.tech/image/aghmr_0.png" alt="image-20201027173459149"></p>
<pre><code class="language-javascript">module.exports = {
publicPath: "/",
productionSourceMap: false,
// 页面入口
pages:{
index: {
entry: "src/main.js",
title: "标题",
},
},
devServer: {
open: false, //关闭自动打开浏览器
}
}
</code></pre>
<p>此时打包后的<code>index.html</code>会使用<code>main.js</code>作为入口,可以理解为初始化的执行文件</p>
<p>当然,我们不想让他叫<code>index.html</code>,给他换个名字是吧,比如<code>demo.html</code></p>
<p>我们修改一下页面对象的名字,如下</p>
<pre><code class="language-javascript">module.exports = {
publicPath: "/",
productionSourceMap: false,
// 页面入口
pages:{
demo: {
entry: "src/main.js",
title: "标题",
},
},
devServer: {
open: false, //关闭自动打开浏览器
}
}
</code></pre>
<p>此时打包后的页面文件为<code>demo.html</code>,它会使用<code>main.js</code>作为入口</p>
<p>当然,我们可能还要加入<strong>模板页面</strong>,打包的时候以它为模板</p>
<p>我们在根目录新建一个HTML文件,就叫<code>template.html</code>吧</p>
<pre><code class="language-html"><!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
<meta name=viewport content="width=device-width,initial-scale=1">
<title>
<%= htmlWebpackPlugin.options.title %>
</title>
</head>
<body>
<div id=app></div>
</body>
</html>
</code></pre>
<p><code><%= htmlWebpackPlugin.options.title %></code>对应的是页面对象的<code>title</code>属性,当然还有其他一些属性</p>
<p>此时,<code>vue.config.js</code>需要修改一下</p>
<pre><code class="language-javascript">module.exports = {
publicPath: "/",
productionSourceMap: false,
// 页面入口
pages:{
index: {
entry: "src/main.js",
// 模板文件
template: "template.html",
// 在 dist/index.html 的输出
filename: "index.html",
// 当使用 title 选项时,
// template 中的 title 标签需要是 <title><%= htmlWebpackPlugin.options.title %></title>
title: "标题",
},
},
devServer: {
open: false, //关闭自动打开浏览器
}
}
</code></pre>
<p>此时打包后会输出<code>index.html</code>文件</p>
<blockquote>
<p>问题来了,修改页面对象的名字跟页面对象中的filename属性有什么关系呢?</p>
<p>我认为在filename为空的情况下,打包时会使用页面对象的名字</p>
<p>总的来说,就是filename属性优先</p>
</blockquote>
<p>这么一来,<code>vue单页面应用</code>的结构好像摸得差不多了</p>
<p><strong>访问HTML文件,然后通过入口JS文件进行初始化(如实例化Vue),把<code>App.vue</code>挂到网页上</strong></p>
<h3>基于单页面改造多页面</h3>
<p>基于单页面应用,如何改造成多页面应用?</p>
<p>上面我们知道,一个<code>Vue单页面应用</code>入口由三部分组成:网页模板<code>.html</code>、入口文件<code>.js</code>、Vue页面<code>.vue</code></p>
<blockquote>
<p>网页模板不是必须提供的,但打包时会生成默认的模板</p>
</blockquote>
<p>那么每一个页面都是这样子的咯,我们开始改造?</p>
<p><img src="https://static.chanx.tech/image/apwha_0.png" alt="image-20201027181144590"></p>
<p>根据两个页面对象的<code>filename</code>,我们知道打包后是两个页面文件<code>app1.html</code>和<code>app2.html</code></p>
<p>那<code>index.html</code>是什么?</p>
<p><img src="https://static.chanx.tech/image/cr4xb_0.png" alt="image-20201027181312761"></p>
<p>可以看到<code>index.html</code>里面并没有逻辑代码,所以纯粹就是输出了一个默认的网页模板</p>
<p>打开浏览器,看下我们的成果</p>
<p><img src="https://static.chanx.tech/image/aqb4q_0.png" alt="image-20201027181619615"></p>
<p><strong>多页面应用get!!!</strong></p>
<blockquote>
<p>多页面应用中各页面是相互独立的, 因为他们各自拥有自己的Vue实例和router实例和vuex实例</p>
<p>可以说是两个独立的项目</p>
</blockquote>
<h3>自动生成页面对象</h3>
<pre><code class="language-javascript">const glob = require("glob");
// 获取文件信息 => 生成pages对象
function handleEntry(entry) {
const entries = {};
let entryBaseName = "";
let entryPathName = "";
let entryTemplate = "";
glob.sync(entry).forEach(item => {
entryBaseName = path.basename(item, path.extname(item));
entryTemplate = item.split("/").splice(-3);
entryPathName = entryBaseName;
/**
* entry为pages下页面文件夹名字的js文件,如pages/app1/app1.js
* template为pages下页面文件夹的html文件
*/
entries[entryPathName] = {
entry: `src/${entryTemplate[0]}/${entryTemplate[1]}/${entryTemplate[1]}.js`,
template: `src/${entryTemplate[0]}/${entryTemplate[1]}/${entryTemplate[2]}`,
title: entryTemplate[2],
filename: entryTemplate[2],
};
});
return entries;
}
const pages = handleEntry("./src/pages/**?/*.html"); // 调用函数扫描pages文件夹
module.exports = {
pages,
}
</code></pre>
<p><strong>注意: 用此方法生成页面对象时必须保证pages下文件夹与相应入口js文件同名, 如pages/app1/app1.js</strong></p>
<h2>隐藏html后缀</h2>
<p>然后我们兴奋地打包文件部署到生产环境<code>Nginx</code>上,然后访问</p>
<p><img src="https://static.chanx.tech/image/arf0c_0.png" alt="image-20201027183545808"></p>
<p>一脸问号???那我们换个方式....</p>
<blockquote>
<p>index.html 是默认网页,在服务器上比如访问'/'时,服务器会指向index.html。故不会发生这种情况</p>
</blockquote>
<p><img src="https://static.chanx.tech/image/as3v1_0.png" alt="image-20201027183640128"></p>
<p>啊这...每次都要加个html后缀才能访问,甲方会不会炸的?</p>
<blockquote>
<p><strong>注意:路由模式为History的请往下看,此方法不适用</strong></p>
</blockquote>
<p>我们配置一下<code>nginx</code>让他在没有后缀名的情况下也能找到文件,配置文件为<code>XX.conf</code></p>
<pre><code> location / {
if (!-f $request_filename){
rewrite (.*)$ $1.html last;
break;
}
}
</code></pre>
<p>大功告成</p>
<p><strong>注意: 在隐藏html后缀的情况下,url不允许"."的出现; 即参数不能出现点, 同时页面跳转无需加后缀<code><a href="./app2"></a></code></strong></p>
<h2>路由使用history</h2>
<p>甲方可能又要说了:你这个网址怎么每次都有一个<code>#</code>呀,去掉去掉</p>
<p>噢!万能的工具人此时应该想到了路由里面的<code>mode: "history"</code></p>
<p>我们把路由改成<code>history</code>模式,热情高涨地进行开发</p>
<h3>路由跳转路径有误</h3>
<p>尝试切换路由,不对劲 => <code>/app1</code>应该切换到<code>/app1/about</code>的,但实际上是切成<code>/about</code></p>
<p>好活!路由加个前缀应该就没问题了</p>
<pre><code class="language-javascript">const router = new VueRouter({
mode: "history",
base:"/app1/",
routes
})
</code></pre>
<h3>刷新路由丢失</h3>
<p>开发环境中,路由跳转后刷新页面可能会出现404错误。<strong>因为刷新页面时访问的资源找不到,因为<code>vue-router</code>设置的路径不是真实存在的路径。</strong><code>vue.config.js</code>加入以下配置</p>
<pre><code class="language-javascript"> devServer: {
open: false, //关闭自动打开浏览器
historyApiFallback: {
verbose: true,
rewrites: [
{ from: /^\/app1\/.*$/, to: "/app1.html" },
{ from: /^\/app2\/.*$/, to: "/app2.html" },
],
},
}
</code></pre>
<p>既然本地开发的服务端要做配置,那么生产环境的<code>nginx</code>服务器也需配置</p>
<pre><code>location / {
try_files $uri $uri/ @router;
index index.html;
}
location @router {
# rewrite ^.*$ /index.html last;
# 多页面 时刷新配置如下
# rewrite ^((?!/(app1|app2)/*).)*$ /index.html last;
rewrite ^/app1/* /app1.html last;
rewrite ^/app2/* /app2.html last;
}
</code></pre>
<h2>其他错误</h2>
<h3>Uncaught SyntaxError:Unexpected token</h3>
<p><code>vue.config.js</code>配置</p>
<pre><code class="language-javascript">module.exports = {
publicPath: "/",
}
</code></pre>
]]>Vue前端Chanx ([email protected])
- JS重难点梳理https://chanx.tech/blog/js-keypointshttps://chanx.tech/blog/js-keypointsJS重难点梳理Thu, 22 Oct 2020 20:38:32 GMT<![CDATA[<h2>第6章 面向对象的程序设计</h2>
<h3>理解对象</h3>
<h4>属性类型</h4>
<h4>定义多个属性</h4>
<h4>读取属性的特性</h4>
<h3>创建对象</h3>
<h4>工厂模式</h4>
<h4>构造函数模式</h4>
<h4>原型模式</h4>
<h4>组合使用构造函数模式和原型模式</h4>
<h4>动态原型模式</h4>
<h4>寄生构造函数模式</h4>
<h4>稳妥构造函数模式</h4>
<h3>继承</h3>
<h4>原型链</h4>
<h4>借用构造函数</h4>
<h4>组合继承</h4>
<h4>原型式继承</h4>
<h4>寄生式继承</h4>
<h4>寄生组合式继承</h4>
<h2>第7章 函数表达式</h2>
<h3>递归</h3>
<h3>闭包</h3>
<p><strong>闭包</strong>:函数中有权访问另一个函数作用域中的变量</p>
<h4>闭包与变量</h4>
<pre><code class="language-javascript">function createFunctions(){
var result = new Array();
for(var i = 0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}
</code></pre>
<p>上述例子每个函数都引用着保存变量i的同一个变量对象,所以每一个函数内部i的值都是10</p>
<pre><code class="language-javascript">function createFunctions(){
var result = new Array();
for(var i = 0; i < 10; i++){
result[i] = (function(num){
return function(){
return num;
}
})(i);
}
return result;
}
</code></pre>
<p>这个例子中我们没有直接把闭包赋值给数组,二十定义一个匿名函数并立即执行。由于函数参数式<strong>按值传递</strong>的,所以就会将变量i的当前值复制给参数num。而在这个匿名函数内部,又创建并返回了一个访问num的闭包。这样一来,result数组中的每个函数都有自己的num变量的一个副本,因此就可以返回各自不同的数值</p>
<h4>关于this对象</h4>
<pre><code class="language-javascript">var name = "The Window";
var object = {
name : "My Object",
getNameFunc: function(){
return function(){
return this.name;
}
}
};
alert(object.getNameFunc()()); //"The Window"(在非严格模式下)
</code></pre>
<p>每个函数在被调用的时候,其活动对象都会自动取得两个特殊变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量</p>
<pre><code class="language-javascript">var name = "The Window";
var object = {
name : "My Object",
getNameFunc: function(){
var _this = this;
return function(){
return _this.name;
}
}
};
alert(object.getNameFunc()()); //"My Object"
</code></pre>
<p>另外,在几种特殊情况下,this的值可能会意外地改变。</p>
<pre><code class="language-javascript">var name = "The Window";
var object = {
name : "My Object",
getName: function(){
return this.name;
}
};
object.getName(); //"My Object"
(object.getName)(); //"My Object"
(object.getName = object.getName)(); //"The Window",在非严格模式下
</code></pre>
<p>第一和第二行代码其实是一样的,第三行代码先执行赋值语句再调用赋值后的结果。因为这个赋值表达式的值是函数本身,所以this的值得不到维持。常见情况下并不会出现二三两种写法,只是为了更好地说明细微的语法变化可能会导致this的值发生改变。</p>
<h4>内存泄漏</h4>
<p><code>IE9</code>之前的版本对<code>JScript</code>对象和<code>COM</code>对象使用不同的垃圾收集例程,因此闭包在IE的这些版本里会导致一些特殊问题。具体来说,如果闭包的作用域链中保存着一个<code>HTML</code>元素,那么就意味着该元素将会无法被销毁</p>
<pre><code class="language-javascript">function assignHandler(){
var element = document.getElementById("someElement");
element.onclick = function(){
alert(element.id)
};
}
</code></pre>
<p>以上代码创建了一个作为<code>element</code>元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用。由于匿名函数保存了一个对<code>assignHandler()</code>的活动对象的引用,因此就会导致无法减少element的引用数。只要匿名函数存在,<code>element</code>的引用数至少也是1,因此它所占用的内存就永远不会被回收</p>
<pre><code class="language-javascript">function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){
alert(id)
};
element = null;
}
</code></pre>
<p>在上面的代码中,通过把<code>element.id</code>的一个副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用。但仅仅做到这一步还是不能解决内存泄漏的问题。必须要记住:<strong>闭包会引用包含函数的整个活动对象</strong>,而其中包含着<code>element</code>。即使闭包不直接引用<code>element</code>,包含函数的活动对象中也仍然会保存一个引用。因此,有必要把<code>element</code>设置为<code>null</code>。这样就能接触对<code>DOM</code>对象的引用,顺利减少其因引用数,确保正常回收其占用的内存。</p>
<h3>模仿块级作用域</h3>
<p>在Javascript中没有块级作用域的概念(ES6中出现了<code>let</code>)</p>
<pre><code class="language-javascript">(function output(){
for(var i = 0; i < 5; i++){
alert(i);
}
alert(i); //"5"
})();
</code></pre>
<p>在Java、C++等语言中,变量i只会在for循环的语句中有定义,循环一旦结束,变量i就被销毁。</p>
<p>可是在Javascript中变量i是定义在<code>output()</code>的活动对象中的,因此从它有定义开始,就可以在函数内部的任意处访问它</p>
<p><strong>匿名函数可以用来模仿块级作用域(又称私有作用域)并避免这个问题</strong></p>
<pre><code class="language-javascript">function(){
//这里是块级作用域
}(); //出错
</code></pre>
<p>Javascript将function关键字当作一个函数声明的开始,而函数声明后面不能跟圆括号。然而,<strong>函数表达式的后面可以跟圆括号</strong></p>
<pre><code class="language-javascript">(function(){
//这里是块级作用域
})();
</code></pre>
<p>无论在什么地方,要临时使用变量就可以使用私有作用域</p>
<pre><code class="language-javascript">function output(count){
(function(){
for(var i = 0; i < count; i++){
alert(i);
}
})();
alert(i); //导致一个错误
}
</code></pre>
<p>在for循环的外部插入了一个私有作用域。在匿名函数中定义的任何变量都会在执行结束时被销毁,因此变量i只能在循环中使用。而在私有作用域中能够访问变量count,是因为这个匿名函数是一个闭包,它能够访问包含作用域内的所有变量</p>
<h3>私有变量</h3>
<h4>静态私有变量</h4>
<h4>模块模式</h4>
<h4>增强的模块模式</h4>
<h3>小结</h3>
]]>Javascript笔记Chanx ([email protected])
- JS原型链那些事https://chanx.tech/blog/js-prototype-chainhttps://chanx.tech/blog/js-prototype-chainJS原型链那些事Tue, 22 Sep 2020 00:00:00 GMT<![CDATA[<p>在使用JavaScript的时候,可能会经常看见控制台输出信息里有<code>__proto__</code>或者<code>prototype</code>;又或者是你在使用字符串的时候用到一些方法,你会好奇它究竟是写在哪里;又或者是你对JavaScript里面没有类的产生疑惑</p>
<h2>几种函数</h2>
<h3>作为普通函数</h3>
<pre><code class="language-javascript">//定义函数
function foo(){}
//调用函数
foo();
</code></pre>
<h3>作为构造函数</h3>
<p>当一个函数被用来创建新对象的时候,我们会叫他为<code>构造函数</code></p>
<pre><code class="language-javascript">//按照惯例,作为构造函数的函数名首字母需要大写
function Foo(){}
const obj = new Foo();
//当我们使用new操作符的时候,实际上会进行下面几个步骤
//创建一个新的对象
const obj = {};
//原型链连接/对象关联
obj.__proto__ = Foo.prototype;
//把新对象作为函数的上下文
Foo.apply(obj, arguments);
//Foo不返回对象,所以返回新对象
return obj;
</code></pre>
<h3>作为对象</h3>
<pre><code class="language-javascript">function foo(){}
foo.name = "tom";
foo['age'] = 20;
</code></pre>
<p>以上这三种使用方式中,以普通函数来调用的方式十分常见,不再赘述。下面要讲的就是当一个函数<strong>被作为构造函数</strong>来使用和<strong>被作为对象</strong>来使用的时候,分别是什么样的,以及它们之间与原型链的关系是什么样的</p>
<h2><code>__proto__</code>和<code>prototype</code></h2>
<p><strong>对象</strong>有一个特殊的<code>__proto__</code>的内置属性,<strong>其实它就是对其他对象的引用</strong>。几乎所有的对象在创建这个属性的时候都会被赋予一个非空的值。**需要注意的是,这个属性可以为空,虽然少见。**它是实现原型链的关键,因为它会指向构造函数的原型对象,即 prototype 属性上的对象,而构造函数原型对象上的<code>__proto__</code> 又指向上一级,即:<code>构造函数.prototype.__proto__ === 上一级构造函数.prototype</code>,以此类推层层往上,就形成了我们所说的原型链。</p>
<p><code>prototype</code>是<strong>函数</strong>特有的一个属性,它是构造函数的原型对象。JavaScript 是一种基于原型的语言,每个对象都拥有一个原型对象,对象以其原型为模板,从原型继承方法和属性。</p>
<p>先来看看下面的代码</p>
<pre><code class="language-javascript">var obj = {
a = 2;
};
obj.a; //2
</code></pre>
<p>相信大家都能一下子把结果说出来,结果是2</p>
<p>我们再来看看</p>
<pre><code class="language-javascript">var anotherObj = {
a = 2;
};
//创建一个关联到anotherObj的对象
var obj = Object.create(anotherObj);
obj.a; //2
</code></pre>
<p>显然,此时<code>obj</code>上并没有<code>a</code>属性,那么这个值是哪里来的?这就是我们即将要说的<strong>原型链</strong></p>
<p>当你试图引用对象的属性时会触发<code>get</code>操作:</p>
<ol>
<li>对于默认的<code>get</code>操作,第一步是检查对象本身是否有这个属性,如果有的话就直接使用。</li>
<li>但是如果<code>a</code>属性不在对象本身,就会继续访问原型链(<code>???.__proto__</code>)找到这个属性直至跑完整条原型链</li>
</ol>
<p><strong>哪里是原型链的尽头</strong></p>
<p>由于所有普通(内置)对象都源于<code>Object.prototype</code>,所以他们的原型链最终都会指向<code>Object.prototype</code>。而<code>Object.prototype.__proto__</code>为<code>null</code>,这不就断了吗。</p>
<h2>函数和对象</h2>
<h3>基本</h3>
<p>首先我们要知道下面几个关系</p>
<ol>
<li>所有的原型对象都是由<code>Object</code>创建出来的</li>
<li>所有函数对象都是由<code>Function</code>创建出来的。Function是一个构造函数,通过new调用可以生成函数对象,即我们一般自定义的那种函数。所以<code>Fucntion</code>这个构造函数的<code>prototype</code>是所有函数的<code>__proto__</code></li>
</ol>
<p>另外,<code>__proto__</code>是对象的属性;<code>prototype</code>是函数的属性</p>
<pre><code class="language-javascript">Function.prototype.__proto__ === Object.prototype
Object.__proto__ === Function.prototype
</code></pre>
<p>分析以上两行代码:</p>
<ol>
<li>看到<code>Function.prototype</code>,说明此时<code>Function</code>是当成构造函数来使用的。前面说过构造函数都由<code>Object</code>创建出来的</li>
<li>看到<code>Object.__proto__</code>,说明此时<code>Object</code>是被当前对象来使用的,前面说过函数对象都是由<code>Function</code>创建出来的</li>
</ol>
<h3>特例</h3>
<p>Object和Function既是对象,又是函数,两者内部同时含有<code>__proto__</code>和<code>prototype</code>属性,他们关系较为复杂,以下做归纳。</p>
<blockquote>
<p>Function.prototype指向"内置函数"。而Object.prototype指向"根源对象"</p>
</blockquote>
<pre><code class="language-javascript">Object.__proto__ === Function.prototype //true
Object.__proto__ === Function.__proto__//true
Object.prototype === Function.prototype.__proto__ // true
//因此
Function instanceof Object //true
Object instanceof Function //true
</code></pre>
<p><img src="https://static.chanx.tech/image/aa0xz_0.png" alt="image-20200922205901071"></p>
<h3>梳理</h3>
<p><img src="https://static.chanx.tech/image/aa9xs_0.png" alt="proto"></p>
<ol>
<li><p>函数<code>prototype</code>属性指向原型对象</p>
</li>
<li><p>所有的原型对象都是由<code>Object</code>创建出来的</p>
<ul>
<li><code>Foo.prototype.__proto__ === Object.prototype</code></li>
<li><code>Function.prototype.__proto__ === Object.prototype</code></li>
</ul>
</li>
<li><p>所有函数对象都是由<code>Function</code>创建出来的</p>
<ul>
<li><code>Foo.__proto__ === Function.prototype</code></li>
<li><code>Function.__proto__ === Function.prototype</code></li>
<li><code>Object.__proto__ === Function.prototype</code></li>
</ul>
</li>
<li><p>普通对象指向构造函数的原型</p>
<ul>
<li><code>Obj.__proto__ === Foo.prototype</code></li>
</ul>
</li>
</ol>
<p>::: right</p>
<p>部分内容参考自<a href="https://blog.csdn.net/qq_36470086/article/details/82599604">不要做切图仔</a>、<a href="https://blog.csdn.net/dingpanqing3307/article/details/101261244?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param">dingpanqing3307</a>、<a href="http://blod.wxinxianyun.com/views/category1/2019/091205.html#%E8%A7%A3%E5%86%B3%E9%A2%84%E7%95%99%E9%97%AE%E9%A2%98">山水有轻音</a></p>
<p>:::</p>
]]>Javascript前端Chanx ([email protected])
- 使用Cron表达式https://chanx.tech/blog/cronhttps://chanx.tech/blog/cronCron表达式的详细解释和使用示例Sun, 20 Sep 2020 14:50:56 GMT<![CDATA[<h3>Cron 表达式</h3>
<p>Cron 表达式有七个必需字段,按空格分隔。</p>
<table>
<thead>
<tr>
<th align="left">第一位</th>
<th align="left">第二位</th>
<th align="left">第三位</th>
<th align="left">第四位</th>
<th align="left">第五位</th>
<th align="left">第六位</th>
<th align="left">第七位</th>
</tr>
</thead>
<tbody><tr>
<td align="left">秒</td>
<td align="left">分钟</td>
<td align="left">小时</td>
<td align="left">日</td>
<td align="left">月</td>
<td align="left">星期</td>
<td align="left">年</td>
</tr>
</tbody></table>
<p>其中,每个字段都有相应的取值范围:</p>
<table>
<thead>
<tr>
<th align="left">字段</th>
<th align="left">值</th>
<th align="left">通配符</th>
</tr>
</thead>
<tbody><tr>
<td align="left">秒</td>
<td align="left">0-59 的整数</td>
<td align="left">, - * /</td>
</tr>
<tr>
<td align="left">分钟</td>
<td align="left">0-59 的整数</td>
<td align="left">, - * /</td>
</tr>
<tr>
<td align="left">小时</td>
<td align="left">0-23 的整数</td>
<td align="left">, - * /</td>
</tr>
<tr>
<td align="left">日</td>
<td align="left">1-31 的整数(需要考虑月的天数)</td>
<td align="left">, - * /</td>
</tr>
<tr>
<td align="left">月</td>
<td align="left">1-12 的整数 或 JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC</td>
<td align="left">, - * /</td>
</tr>
<tr>
<td align="left">星期</td>
<td align="left">0-6 的整数 或 MON,TUE,WED,THU,FRI,SAT,SUN;其中 0 指星期一,1 指星期二,依次类推</td>
<td align="left">, - * /</td>
</tr>
<tr>
<td align="left">年</td>
<td align="left">1970~2099 的整数</td>
<td align="left">, - * /</td>
</tr>
</tbody></table>
<p><strong>通配符</strong></p>
<table>
<thead>
<tr>
<th align="left">通配符</th>
<th align="left">含义</th>
</tr>
</thead>
<tbody><tr>
<td align="left">, (逗号)</td>
<td align="left">代表取用逗号隔开的字符的并集。例如:在“小时”字段中 1,2,3 表示1点、2点和3点</td>
</tr>
<tr>
<td align="left">- (破折号)</td>
<td align="left">包含指定范围的所有值。例如:在“日”字段中,1-15 包含指定月份的 1 号到 15 号</td>
</tr>
<tr>
<td align="left">* (星号)</td>
<td align="left">表示所有值。在“小时”字段中,* 表示每个小时</td>
</tr>
<tr>
<td align="left">/ (正斜杠)</td>
<td align="left">指定增量。在“分钟”字段中,输入 1/10 以指定从第一分钟开始的每隔十分钟重复。例如,第 11 分钟、第 21 分钟和第 31 分钟,依此类推</td>
</tr>
</tbody></table>
<p><strong>注意事项</strong></p>
<ul>
<li>在 Cron 表达式中的“日”和“星期”字段同时指定值时,两者为“或”关系,即两者的条件分别均生效。</li>
</ul>
<p><strong>示例</strong></p>
<p>下面展示了一些 Cron 表达式和相关含义的示例:</p>
<ul>
<li><code>*/5 * * * * * *</code> 表示每5秒触发一次</li>
<li><code>0 0 2 1 * * *</code> 表示在每月的1日的凌晨2点触发</li>
<li><code>0 15 10 * * MON-FRI *</code> 表示在周一到周五每天上午10:15触发</li>
<li><code>0 0 10,14,16 * * * *</code> 表示在每天上午10点,下午2点,4点触发</li>
<li><code>0 */30 9-17 * * * *</code> 表示在每天上午9点到下午5点内每半小时触发</li>
<li><code>0 0 12 * * WED *</code> 表示在每个星期三中午12点触发</li>
</ul>
<p>::: tip
内容来自<a href="https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/functions/triggers.html">微信开放文档</a>
:::</p>
]]>Cron定时任务调度其他Chanx ([email protected])
- AceEditor开发的坑https://chanx.tech/blog/ace-editor-developmenthttps://chanx.tech/blog/ace-editor-developmentAceEditor开发的坑Sun, 20 Sep 2020 00:00:00 GMT<![CDATA[<h2>禁止首行编辑</h2>
<p>使用到ace实现一个首行禁止编辑的功能,我们知道编辑操作可以这些:插入、删除、粘贴。只要阻止第一行的相关操作就可以了</p>
<p>但是,中文输入法输入会导致一些奇奇怪怪的问题。<strong>所以我们另外对光标进行处理,只要光标点击第一行我们就给他跳转到第二行</strong></p>
<p>show me the code...</p>
<pre><code class="language-javascript">//光标跳转
this.ace.getSession().selection.on('changeCursor', (e)=>{
if(this.ace.getSelectionRange().start.row == 0 || this.ace.getSelectionRange().end.row == 0){
this.ace.gotoLine(2);
}
});
//不能对第一行进行粘贴、删除操作
this.ace.commands.on("exec", (e)=>{
const hasFirst = this.ace.getSelectionRange().start.row == 0 ;
const command = e.command.description;
if ((hasFirst && ( command == "Paste" || command == "Backspace"))) {
e.preventDefault()
e.stopPropagation()
}
});
</code></pre>
<p>以上方法能解决大部分问题,但是<strong>移动文本能成功修改</strong>(2020-11-21)</p>
<p>下面是<code>stackoverflow</code>上面的一个相对来说比较完美解决办法(<a href="https://stackoverflow.com/questions/39640328/how-to-make-multiple-chunk-of-lines-readonly-in-ace-editor/39640987#39640987">传送门</a>)</p>
<p>重点代码是对原有编辑器方法的拦截修改</p>
<p>我进行了一些修改和注释:</p>
<pre><code class="language-js">/**
* Ace Editor 锁定或只读行封装
* @author CHANX 2020-11-21
* @param {ace} editor ace编辑器实例
* @param {number[]} [readOnlyLines = []] 需要锁定的行号
*/
function setReadOnly(editor, readOnlyLines) {
const session = editor.session;
const Range = editor;
const readOnlyRanges = [];
for (let i = 0; i < readOnlyLines.length; i += 1) {
const newRange = [readOnlyLines[i] - 1, 0, readOnlyLines[i], 0];
// 此处原有代码range是当前行头到下一行头,我修改为当前行头到当前行1000位置
// 防止下一行头无法被编辑
readOnlyRanges.push({
start: {
row: readOnlyLines[i] - 1,
column: 0,
},
end: {
row: readOnlyLines[i] - 1,
column: 1000,
},
});
}
function before(obj, method, wrapper) {
const orig = obj[method];
obj[method] = function (...arg) {
const args = Array.prototype.slice.call(arg);
return wrapper.call(this, () => orig.apply(obj, args), args);
};
return obj[method];
}
/**
* 当前选中范围和传入范围是否冲突
* @param {*} range
*/
function intersects(range) {
return editor.getSelectionRange().intersects(range);
}
function preventReadonly(next, args) {
for (let i = 0; i < readOnlyRanges.length; i += 1) { if (intersects(readOnlyRanges[i])) return; }
next();
}
/**
* 当前选中范围和不可编辑行范围是否冲突
* @param {*} newRange
*/
function intersectsRange(newRange) {
for (let i = 0; i < readOnlyRanges.length; i += 1) { if (newRange.intersects(readOnlyRanges[i])) return true; }
return false;
}
/**
* 是否在不可编辑行的末尾
* @param {*} position
*/
function onEnd(position) {
const row = position.row;
const column = position.column;
for (let i = 0; i < readOnlyRanges.length; i += 1) { if (readOnlyRanges[i].end.row === row && readOnlyRanges[i].end.column === column) return true; }
return false;
}
/**
* 是否在不可编辑行的范围外
* @param {*} position
*/
function outSideRange(position) {
const row = position.row;
const column = position.column;
for (let i = 0; i < readOnlyRanges.length; i += 1) {
if (readOnlyRanges[i].start.row < row && readOnlyRanges[i].end.row > row) { return false; }
if (readOnlyRanges[i].start.row === row && readOnlyRanges[i].start.column < column) {
if (readOnlyRanges[i].end.row !== row || readOnlyRanges[i].end.column > column) { return false; }
} else if (readOnlyRanges[i].end.row === row && readOnlyRanges[i].end.column > column) {
return false;
}
}
return true;
}
editor.keyBinding.addKeyboardHandler({
handleKeyboard(data, hash, keyString, keyCode, event) {
// 阻止不可编辑行行尾的回车
if (Math.abs(keyCode) === 13 && onEnd(editor.getCursorPosition())) {
return false;
}
if (hash === -1 || (keyCode <= 40 && keyCode >= 37)) return false;
for (let i = 0; i < readOnlyRanges.length; i += 1) {
if (intersects(readOnlyRanges[i])) {
return { command: "null", passEvent: false };
}
}
},
});
before(editor, "onPaste", preventReadonly);
before(editor, "onCut", preventReadonly);
const old$tryReplace = editor.$tryReplace;
editor.$tryReplace = function (range, replacement) {
return intersectsRange(range) ? null : old$tryReplace.apply(this, arguments);
};
/**
* 重写insert方法: 若postion在不可编辑范围内则不可插入
*/
const oldInsert = session.insert;
session.insert = function (position, text) {
return oldInsert.apply(this, [position, outSideRange(position) ? text : ""]);
};
/**
* 重写remove方法: 若range在不可编辑范围内则不可以删除
*/
const oldRemove = session.remove;
session.remove = function (range) {
return intersectsRange(range) ? false : oldRemove.apply(this, arguments);
};
/**
* 重写moveText方法: 若toPosition在不可编辑范围内则可不以移动
*/
const oldMoveText = session.moveText;
session.moveText = function (fromRange, toPosition, copy) {
if (intersectsRange(fromRange) || !outSideRange(toPosition)) return fromRange;
return oldMoveText.apply(this, arguments);
};
}
export default setReadOnly;
</code></pre>
<h2>初始内容可被撤销/重置撤销栈</h2>
<p>每次ace初始化完毕并赋值初始内容后,此时ctrl+z会发生内容消失的情况。因为赋值初始内容其实是一次输入(空 => 初始内容),此时撤销栈会压栈。</p>
<p>解决办法就是,在初始内容赋值完成后立即重置撤销栈。</p>
<pre><code class="language-javascript">//初始内容
editor.setValue("And now how can I reset the\nundo stack,-1");
//重置撤销栈UndoManager是保持所有历史的
editor.getSession().setUndoManager(new ace.UndoManager())
</code></pre>
<h2>ace添加自定义主题</h2>
<h3>1. 编辑样式代码</h3>
<pre><code class="language-css">.ace-vs-dark .ace_gutter {
background: #1e1e1e;
color: #858585;
overflow: hidden;
}
.ace-vs-dark .ace_print-margin {
width: 1px;
background: #e8e8e8;
}
.ace-vs-dark {
background-color: #1e1e1e;
color: #dcdcdc;
}
.ace-vs-dark .ace_cursor {
color: #dcdcdc;
}
.ace-vs-dark .ace_invisible {
color: #ffffff40;
}
.ace-vs-dark .ace_constant.ace_buildin {
color: #569cd6;
}
.ace-vs-dark .ace_constant.ace_language {
color: #b4cea8;
}
.ace-vs-dark .ace_constant.ace_library {
color: #b5cea8;
}
.ace-vs-dark .ace_invalid {
background-color: transparent;
color: #ff3333;
}
.ace-vs-dark .ace_fold {
}
.ace-vs-dark .ace_support.ace_function {
color: #dcdcdc;
}
.ace-vs-dark .ace_support.ace_constant {
color: #569cd6;
}
.ace-vs-dark .ace_support.ace_type,
.ace-vs-dark .ace_support.ace_class .ace-vs-dark .ace_support.ace_other {
color: #4ec9b0;
}
.ace-vs-dark .ace_variable.ace_parameter {
color: #dcdcdc;
}
.ace-vs-dark .ace_keyword.ace_operator {
color: #dcdcdc;
}
.ace-vs-dark .ace_comment {
color: #608b4e;
}
.ace-vs-dark .ace_comment.ace_doc {
color: #608b4e;
}
.ace-vs-dark .ace_comment.ace_doc.ace_tag {
color: #608b4e;
}
.ace-vs-dark .ace_constant.ace_numeric {
color: #b5cea8;
}
.ace-vs-dark .ace_variable {
color: #dcdcdc;
}
.ace-vs-dark .ace_xml-pe {
/**/
color: rgb(104, 104, 91);
}
.ace-vs-dark .ace_entity.ace_name.ace_function {
color: #dcdcdc;
}
.ace-vs-dark .ace_heading {
color: #569cd6;
}
.ace-vs-dark .ace_list {
color: #dcdcdc;
}
.ace-vs-dark .ace_marker-layer .ace_selection {
/**/
background: rgb(181, 213, 255);
}
.ace-vs-dark .ace_marker-layer .ace_step {
/**/
background: rgb(252, 255, 0);
}
.ace-vs-dark .ace_marker-layer .ace_stack {
/**/
background: rgb(164, 229, 101);
}
.ace-vs-dark .ace_marker-layer .ace_bracket {
/**/
margin: -1px 0 0 -1px;
border: 1px solid rgb(192, 192, 192);
}
.ace-vs-dark .ace_marker-layer .ace_active-line {
/**/
background: rgba(0, 0, 0, 0.07);
}
.ace-vs-dark .ace_gutter-active-line {
background-color: #0f0f0f;
}
.ace-vs-dark .ace_marker-layer .ace_selected-word {
/**/
background: rgb(250, 250, 255);
border: 1px solid rgb(200, 200, 250);
}
.ace-vs-dark .ace_storage,
.ace-vs-dark .ace_keyword,
.ace-vs-dark .ace_meta.ace_tag {
color: #569cd6;
}
.ace-vs-dark .ace_string.ace_regex {
/**/
color: rgb(255, 0, 0);
}
.ace-vs-dark .ace_string {
color: #d69d85;
}
.ace-vs-dark .ace_entity.ace_other.ace_attribute-name {
color: #92caf4;
}
</code></pre>
<h3>2. 编辑模块代码</h3>
<p>将css样式文本复制粘贴进去即可</p>
<pre><code class="language-javascript">/* eslint-disable */
ace.define("ace/theme/vs-dark",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
exports.isDark = true;
exports.cssClass = "ace-vs-dark";
exports.cssText = ".ace-vs-dark .ace_gutter {\
background: #1e1e1e;\
color: #858585;\
overflow: hidden;\
}\
.ace-vs-dark .ace_print-margin {\
width: 1px;\
background: #e8e8e8;\
}\
.ace-vs-dark {\
background-color: #1e1e1e;\
color: #dcdcdc;\
}\
.ace-vs-dark .ace_cursor {\
color: #dcdcdc;\
}\
.ace-vs-dark .ace_invisible {\
color: #ffffff40;\
}\
.ace-vs-dark .ace_constant.ace_buildin {\
color: #569cd6;\
}\
.ace-vs-dark .ace_constant.ace_language {\
color: #b4cea8;\
}\
.ace-vs-dark .ace_constant.ace_library {\
color: #b5cea8;\
}\
.ace-vs-dark .ace_invalid {\
background-color: transparent;\
color: #ff3333;\
}\
.ace-vs-dark .ace_fold {\
}\
.ace-vs-dark .ace_support.ace_function {\
color: #dcdcdc;\
}\
.ace-vs-dark .ace_support.ace_constant {\
color: #569cd6;\
}\
.ace-vs-dark .ace_support.ace_type,\
.ace-vs-dark .ace_support.ace_class .ace-vs-dark .ace_support.ace_other {\
color: #4ec9b0;\
}\
.ace-vs-dark .ace_variable.ace_parameter {\
color: #dcdcdc;\
}\
.ace-vs-dark .ace_keyword.ace_operator {\
color: #dcdcdc;\
}\
.ace-vs-dark .ace_comment {\
color: #608b4e;\
}\
.ace-vs-dark .ace_comment.ace_doc {\
color: #608b4e;\
}\
.ace-vs-dark .ace_comment.ace_doc.ace_tag {\
color: #608b4e;\
}\
.ace-vs-dark .ace_constant.ace_numeric {\
color: #b5cea8;\
}\
.ace-vs-dark .ace_variable {\
color: #dcdcdc;\
}\
.ace-vs-dark .ace_xml-pe {\
/**/\
color: rgb(104, 104, 91);\
}\
.ace-vs-dark .ace_entity.ace_name.ace_function {\
color: #dcdcdc;\
}\
.ace-vs-dark .ace_heading {\
color: #569cd6;\
}\
.ace-vs-dark .ace_list {\
color: #dcdcdc;\
}\
.ace-vs-dark .ace_marker-layer .ace_selection {\
/**/\
background: rgb(181, 213, 255);\
}\
.ace-vs-dark .ace_marker-layer .ace_step {\
/**/\
background: rgb(252, 255, 0);\
}\
.ace-vs-dark .ace_marker-layer .ace_stack {\
/**/\
background: rgb(164, 229, 101);\
}\
.ace-vs-dark .ace_marker-layer .ace_bracket {\
/**/\
margin: -1px 0 0 -1px;\
border: 1px solid rgb(192, 192, 192);\
}\
.ace-vs-dark .ace_marker-layer .ace_active-line {\
/**/\
background: rgba(0, 0, 0, 0.07);\
}\
.ace-vs-dark .ace_gutter-active-line {\
background-color: #0f0f0f;\
}\
.ace-vs-dark .ace_marker-layer .ace_selected-word {\
/**/\
background: rgb(250, 250, 255);\
border: 1px solid rgb(200, 200, 250);\
}\
.ace-vs-dark .ace_storage,\
.ace-vs-dark .ace_keyword,\
.ace-vs-dark .ace_meta.ace_tag {\
color: #569cd6;\
}\
.ace-vs-dark .ace_string.ace_regex {\
/**/\
color: rgb(255, 0, 0);\
}\
.ace-vs-dark .ace_string {\
color: #d69d85;\
}\
.ace-vs-dark .ace_entity.ace_other.ace_attribute-name {\
color: #92caf4;\
}\n";
});
</code></pre>
<h3>3. 引入自定义主题</h3>
<pre><code class="language-javascript">ace.config.setModuleUrl(
"ace/theme/vs-dark",
// eslint-disable-next-line import/no-webpack-loader-syntax
require("file-loader?esModule=false!./vs-dark.js")
);
ace.edit(context, {
fontSize: 15,
theme: "ace/theme/vs-dark",
}
</code></pre>
]]>Javascriptace-editor前端Chanx ([email protected])
- HTML+CSS实现选项卡效果https://chanx.tech/blog/tabs-effecthttps://chanx.tech/blog/tabs-effectHTML+CSS实现选项卡效果Sun, 13 Sep 2020 00:00:00 GMT<![CDATA[<p>不使用Javascript,如何实现选项卡效果呢?</p>
<!-- more -->
<p>具体效果:</p>
<img src="https://static.chanx.tech/image/a5otc_0.png" alt="Snipaste_2020-09-13_16-48-03" style="zoom:50%;" />
<p>本文将不会使用到js,使用html+css完成所需效果。你可能会有疑问不使用js的情况下,怎么实现点击切换的效果?</p>
<p>首先我们要知道,单选框选项组是怎么写的</p>
<pre><code class="language-html"><input type="radio" name="items" id="item1" value="1"/>
<input type="radio" name="items" id="item2" value="2"/>
<input type="radio" name="items" id="item3" value="3"/>
</code></pre>
<p>另外我们要知道一个非常重要的<code><label></code>的<code>for</code>属性</p>
<pre><code class="language-html"><input type="radio" name="items" id="item1" value="1"/>
<input type="radio" name="items" id="item2" value="2"/>
<input type="radio" name="items" id="item3" value="3"/>
<label for="item1">choose item1</label>
<label for="item2">choose item2</label>
<label for="item3">choose item3</label>
</code></pre>
<p>点击相应的<code><label></code>相应的单选框将会被选中</p>
<p>有了它,我们就可以进一步发挥想象的空间 => 一个<code>label</code>相当于一个<code>tab选项卡</code></p>
<p>然后就是我们选项卡的内容了,将每一个卡的内容视为一张卡片</p>
<p>我们把卡片叠到一起,只要其中一张不透明,其余卡片都透明就可以实现选择效果</p>
<p>如何实现透明度的切换,就需要利用到<code>radio</code>的<code>checked</code>这么一个CSS属性</p>
<p>剩余的浏览代码就可以明白啦</p>
<pre><code class="language-html"><div id="swiper">
<input id="item1" type="radio" name="1" value="1" style="display: none;" checked/>
<input id="item2" type="radio" name="1" value="2" style="display: none;"/>
<input id="item3" type="radio" name="1" value="3" style="display: none;"/>
<div class="pic">
<img id="pic1" src="./images/w01.jpg">
<img id="pic2" src="./images/w02.jpg">
<img id="pic3" src="./images/w03.jpg">
<div class="pic-btn">
<label id="btn1" for="item1"></label>
<label id="btn2" for="item2"></label>
<label id="btn3" for="item3"></label>
</div>
</div>
</div>
</code></pre>
<pre><code class="language-css">.pic img{
opacity: 0;/*默认所有卡片都透明*/
width: 100%;
height: 100%;
transition: all 0.6s;
position: absolute;
top: 0;
left: 0;
}
#item1:checked~.pic #pic1,
#item2:checked~.pic #pic2,
#item3:checked~.pic #pic3{
opacity: 1;//被选中的卡片透明度为1
transition: all 0.6s;
}
/*选项条居中*/
.pic-btn{
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
z-index: 1;
}
.pic-btn label{
width: 50px;
height: 10px;
background-color: #cecece;
display: inline-block;
margin: 5px;
border-radius: 10px;
}
/*给当前的选项点覆盖一个黑色,实现active效果*/
.pic #btn1::before,
.pic #btn2::before,
.pic #btn3::before{
content: ' ';
width: 50px;
height: 10px;
background-color:rgba(0, 0, 0, 0.6);
z-index: 2;
position: absolute;
opacity: 0;
transition: all 0.6s;
border-radius: 10px;
}
#item1:checked~.pic #btn1::before,
#item2:checked~.pic #btn2::before,
#item3:checked~.pic #btn3::before{
opacity: 1;
transition: all 0.6s;
}
</code></pre>
]]>前端Chanx ([email protected])
- 尝试开发课表和通知小程序https://chanx.tech/blog/schedule-notifierhttps://chanx.tech/blog/schedule-notifier尝试开发课表和通知小程序Fri, 04 Sep 2020 00:00:00 GMT<![CDATA[<p>一个简单的学校小程序,包含课程表、查看通知两个功能</p>
<!-- more -->
<h2>前言</h2>
<p>最近在学习小程序的开发,因为之前有简单的<code>html + css + js + vue </code>基础,所以直接看了下官方的快速开发模板。就开始上手弄demo,一开始是想做了一个简单的<code>Todolist</code>来练习数据交互。此时问题是没有后端,数据交互只能局限于前端,想用<code>Java</code>写但是觉得码量比较大且本人不太熟悉(课白上了)。用<code>nodejs</code>吧,虽然有js基础感觉学起来不太难,但是毕竟一边查一边学有点麻烦,于是作罢。这个时候我看到了<code>云开发</code>这个东西,js写完前端后端,简单操作数据库还不用学<code>SQL</code>语句,舒服了。查了几下文档就直接上手,另外前后端数据交互使用了<code>axios</code>。做完<code>todolist</code>之后,开始模仿学校两个比较多人使用的小程序进行深入学习。<strong>也发现了两款小程序的设计上的不一样(详细可见"准备工作</strong>"<strong>内容)</strong></p>
<h2>准备工作</h2>
<h3>整体分析</h3>
<p><img src="https://static.chanx.tech/image/962nv_0.png" alt="image-20200904231348892"></p>
<p>我的大概思路如下:</p>
<p><strong>课程表</strong>:第一次登录时,通过云开发向教务系统进行数据拉取并保存到数据库;随后的小程序访问都直接通过云开发拉取数据库信息,不再主动向源站(教务系统)拉取数据</p>
<p>**校内通知:**访问新闻通知网并拉取数据保存到数据库,随后小程序访问都直接通过云开发拉取数据库信息,不通过源站获取。另外还需注意的是,为了保证校内通知的及时性,需要对这个云函数<code>getNews()</code>设置触发器,实现定时拉取新数据。</p>
<hr>
<p>在我使用小程序和与开发者交流后,我发现两款小程序在某些方面是采取不同的做法:</p>
<p><strong>A小程序</strong>:课程数据从教务系统拉取后存入数据库,随后小程序访问实际上是小程序和数据库的交互(跟我的差不多</p>
<p><strong>B小程序</strong>:课程数据从教务系统拉取后本地缓存,随后小程序访问实际上是小程序和本地缓存的交互</p>
<p>采取A方式的能对多用户的数据做统一分析处理(如选某门课程的人数、大数据成绩分析),减少对源站的访问</p>
<p>采取B方式的能快速读取数据(读缓存)加速小程序速度,而且小程序后端本身不保存任何用户信息</p>
<p><strong>本文不对两种方式进行比较研究,采用A方式存入数据库,方便我进行开发学习</strong></p>
<h3>分析数据</h3>
<p>通过开发者工具查看相关网页的数据交互,尝试用<code>postman</code>或其他工具模拟获取数据的操作,获取数据并对数据进行解析以获取有用信息。我是用浏览器开发者工具一步步获取信息并分析,最后汇总相关接口达到爬虫效果。当然还有其他实现的方式这里不一一介绍</p>
<h2>开发</h2>
<h3>校内通知</h3>
<p>校内通知这个就比较简单了,也不需要太多花里胡哨的效果。</p>
<p>首先就是拿到数据,然后对数据进行过滤处理。</p>
<p><strong>我的实现是</strong>:后端云函数设置触发器(这里涉及到<code>Cron</code>表达式,很简单的一个东西),定时去拉取数据。然后把新拉取到的数据和数据库中的数据进行比较,新出现的就保存到数据库当中。当用户访问的时候,就可以直接拉数据库的数据。</p>
<p>你可以把数据分析过滤的逻辑全部写在前端页面上,这样子每次拉取都要过滤分析,我觉得不太好。</p>
<p>保存到自己的数据库一个好处也就是能实现更多花里胡哨的功能,比如说点赞?浏览记录?评论?甚至可以基于这些信息再整个推荐算法哈哈哈(别想了,你不会写</p>
<h3>课程表</h3>
<p>课程表就是拉数据处理数据啦,跟校内通知差不多</p>
<p><strong>我的实现是</strong>:拉取课程数据,然后将课程数据按周排列,每一周再按星期排列。至于课程表这个展示,就是利用<code>position:absolute</code>定位来实现的</p>
<p><img src="https://static.chanx.tech/image/96jv1_0.png" alt="image-20201121192149448"></p>
]]>微信小程序Javascript前端Chanx ([email protected])
- 微信小程序开发的坑https://chanx.tech/blog/wechat-mini-program-developmenthttps://chanx.tech/blog/wechat-mini-program-development微信小程序开发的坑Fri, 04 Sep 2020 00:00:00 GMT<![CDATA[<h2>Swiper高度自适应</h2>
<p>swiper组件设置高度100%无法生效,这时需要通过手动获取屏幕的高度并给swiper设置</p>
<pre><code class="language-javascript">data:{
swiperHeight: 0;
}
//监听页面加载
onLoad:function (option){
const _this = this;
wx.getSystemInfo({
success:functioin(res) {
const clientHeight = res.windowHeight;
const clientWidth = res.windowWidth;
const ratio = 750 / clientWidth;//计算为百分比
const rpxHeight = ratio * clientHeight;
_this.setData({
_this.swiperHeight: rpxHeight;//将计算好的高度给定义好的值
})
}
})
}
</code></pre>
<p>将获取到的屏幕高度给swiper设置上</p>
<pre><code class="language-html"><swiper style="height:{{swiperHeight}}rpx;"><swiper/>
</code></pre>
<p><strong>注意:style最后的rpx千万不能省略,否则不生效</strong></p>
<h2>小程序下载、预览文档</h2>
<p>把文件下载到临时的缓存,然后再打开。<strong>需要注意上线的项目中文件地址需要https</strong></p>
<pre><code class="language-javascript">onLoad: function(res){
var url = '文件地址';
wx.downloadFile({ //下载
url: url,
success: function(e){
const filePath = e.tempFilePath; // 临时文件地址
wx.openDocument({ // 预览
filePath: filePath,
success: function (ret) {
console.log('打开文档成功')
}
})
},
fail: function(r){
console.log(r)
}
})
}
</code></pre>
<h2>云开发中数据库的权限问题</h2>
<p>需要注意不同的权限导致api获取数据为空</p>
<h2>云函数中使用axios不能直接对返回值进行操作</h2>
<p>解决方案:使用<code>cheerio</code> + <code>utils</code></p>
<p>相关资料:<a href="https://www.cnblogs.com/CraryPrimitiveMan/p/3674421.html">cheerio</a></p>
<pre><code class="language-javascript">await axios.request({
...
}).then((res)=>{
let data = eval (util.inspect(res.data));
cookie = eval(util.inspect(res.headers["set-cookie"][0]));
const $ = cheerio.load(data);
$("#casLoginForm input").each((index,item)=>{
//解析html
...
})
})
</code></pre>
<h2>解析并渲染Markdown</h2>
<p><a href="https://github.com/ifanrx/wxParser-plugin">wxpraser-plugin使用</a></p>
]]>微信小程序前端Chanx ([email protected])
- Vue踩过的坑https://chanx.tech/blog/vue-developmenthttps://chanx.tech/blog/vue-developmentVue踩过的坑Sat, 15 Aug 2020 00:00:00 GMT<![CDATA[<h2>vue-router的beforeEach第一次打开页面不执行</h2>
<p>分析:<code>vue.use</code>的时候已经初始化了,挂载的时候hash值没有发生变化,所以不会执行<code>beforeEach</code></p>
<p>所以正确的代码顺序如下(先路由再初始化):</p>
<pre><code class="language-javascript">router.beforeEach((to, from, next) => {
//token校验
});
new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#app");
</code></pre>
<h2><code>form</code>只有一个<code>input</code>时按回车会自动提交</h2>
<p>W3C 标准中有如下<a href="https://www.w3.org/MarkUp/html-spec/html-spec_8.html#SEC8.2">规定</a>:</p>
<blockquote>
<p><em>When there is only one single-line text input field in a form, the user agent should accept Enter in that field as a request to submit the form.</em></p>
</blockquote>
<p>即:当一个 form 元素中只有一个输入框时,在该输入框中按下回车应提交该表单。如果希望阻止这一默认行为,可以在 <code><el-form></code> 标签上添加 <code>@submit.native.prevent</code>。</p>
]]>Vue前端Chanx ([email protected])
- 当Minecraft遇上Terrariahttps://chanx.tech/blog/terrariahttps://chanx.tech/blog/terraria当Minecraft遇上TerrariaThu, 06 Aug 2020 13:44:47 GMT<![CDATA[<p>Minecraft + Terraria = TerraCraft</p>
<!-- more -->
<h2>介绍</h2>
<p><strong>TerraCraft</strong>是一款将两个沙盒神作融合起来的<strong>同人游戏</strong>,由<a href="https://space.bilibili.com/183654671/"><strong>进击的蓝耀西(Bilibili账号)</strong></a>个人开发,使用C++以及自制游戏引擎编写,仅用于练习C++软件工程的开发实践。</p>
<p>TerraCraft是世界上第一款在单个地图中实现了<strong>无限边界</strong>的横板沙盒平台游戏,你可以在TerraCraft的世界中自由地搭建房子、挖掘方块、探索世界以及与敌人战斗,做任何你喜欢的事情!TerraCraft采用了末影传送门系统,你可以通过各种方式尽可能多地收集末影珍珠来开启更多的传送门,将探索区域拓展得更加广阔!目前TerraCraft实现了地表层、洞穴层和地狱层,同时实现了横向无边界。TerraCraft采用了独特的地层分级机制,将在未来的更新中添加更多的地层(下界绯红森林层、暮色森林层、水晶层、腐蚀层、辐射层、高压层、极压层等)以及更丰富的天空层、太空层、末地层,并使每个层次拥有独特的世界观,敬请期待未来的更新φ(≧ω≦*)♪</p>
<blockquote>
<p>作为MC玩家的我非常期待作者的进一步更新,但是由于作者是在校生,此作品当初也只是作者为了参赛而设计,更新的速度自然比不上正常的游戏更新。就目前放出的相对完整的版本,这个游戏也可以玩起来了。</p>
</blockquote>
<h2>演示视频(Bilibili)</h2>
<hr>
<p>当Minecraft变成Terraria(2020年7月25日发布):</p>
<iframe src="//player.bilibili.com/player.html?aid=926443273&bvid=BV1ZT4y1L7HM&cid=216505428&page=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" style="width:95%;min-height:500px;"> </iframe>
<p>我 的 泰 拉 世 界(2020年2月24日发布):</p>
<iframe src="//player.bilibili.com/player.html?aid=91225099&bvid=BV1H7411F7xv&cid=155758502&page=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" style="width:95%;min-height:500px;"> </iframe>
<p><strong>更详细的信息可以到<a href="http://blueyoshi.cn/terracraft">Terracraft官网</a>查看</strong></p>
]]>share随笔Chanx ([email protected])
- Swiper动态加载后无法滑动https://chanx.tech/blog/swiper-issueshttps://chanx.tech/blog/swiper-issuesSwiper动态加载后无法滑动Thu, 06 Aug 2020 00:00:00 GMT<![CDATA[<h2>说明</h2>
<p>Vue中使用Swiper,但是父组件会有状态切换,切换后Swiper出现异常(卡着其中一组不能滑动切换)</p>
<p><img src="https://static.chanx.tech/image/acoq3_0.png" alt="image-20200806133320474"></p>
<p>查资料找到解决办法,原因为<strong>swiper在初始化的时候会扫描swiper-wrapper下面的swiper-slide的个数,从而完成初始化,但是由于动态加载是在初始化之后的动作,所以导致无法滑动</strong></p>
<h2>解决方法1:在动态获取数据后,马上对swiper进行初始化</h2>
<p>每次获取数据后再对swiper进行初始化操作,说白了就是先后顺序不能错</p>
<pre><code class="language-javascript">$.ajax({
type:"get",
url:finalUrl,
dataType:"json",
success:function(data){
$("#reportList").empty();
for(var i=0;i<reportLength;i++){
var url="'"+"reportDetial.html"+location.search+"&noticeId="+reportList[i].id+"'";
reportHtml+='<div class="swiper-slide report-item" onclick="reportJump('+url+')">'
+'<div class="item-title">'+data.resp[i].title+'</div>'
+'<div class="item-content">'+data.resp[i].content+'</div>'
+'<div class="item-date">'+data.resp[i].createTime+'</div>'
+'</div>';
}
$("#reportList").append(reportHtml);
var swiper = new Swiper('.swiper-container', {
slidesPerView : 3
});
}
});
</code></pre>
<h2>解决方法2:修改Swiper参数实现监听变化</h2>
<p> observer:true, //修改swiper自己或子元素时,自动初始化swiper
observeParents:true, //修改swiper的父元素时,自动初始化swiper</p>
<pre><code class="language-js">var mySwiper = new Swiper ('.swiper-container', {
observer:true,//修改swiper自己或子元素时,自动初始化swiper
observeParents:true,//修改swiper的父元素时,自动初始化swiper
loop: true, // 循环模式选项
pagination: {
el: '.swiper-pagination',
},
})
</code></pre>
<p>::: tip</p>
<p>部分内容来自<a href="https://www.cnblogs.com/yangguoe/p/9857398.html">Carina</a></p>
<p>:::</p>
]]>SwiperVue前端Chanx ([email protected])
- 原生JS实现复制功能https://chanx.tech/blog/copy-with-vanilla-jshttps://chanx.tech/blog/copy-with-vanilla-js原生JS实现复制功能Thu, 06 Aug 2020 00:00:00 GMT<![CDATA[<p>使用<code>document.execCommand("Copy");</code>但是它只能是操作可编辑区域的内容,也就是意味着除了 <code><input></code>、<code><textarea></code> 这样的输入域以外,是无法使用这个方法的。但是我们可以这个样子实现其他元素的复制:</p>
<pre><code class="language-javascript">var text=document.getElementById("id").innerText;
var t = document.createElement('input');
t.style.display='none';//隐藏这个输入框
t.value = text;
document.body.appendChild(t);
t.select(); // 选择对象
document.execCommand("Copy"); // 执行浏览器复制命令
alert('复制成功');
</code></pre>
<p><strong>遇到的坑</strong></p>
<p>在Chrome下调试的时候,这个方法时完美运行的。然后到了移动端调试的时候,坑就出来了。</p>
<p>对,没错,就是你,ios...</p>
<p><strong>1、点击复制时屏幕下方会出现白屏抖动,仔细看是拉起键盘又瞬间收起</strong></p>
<p>知道了抖动是由于什么产生的就比较好解决了。既然是拉起键盘,那就是聚焦到了输入域,那只要让输入域不可输入就好了,在代码中添加<code>input.setAttribute('readonly', 'readonly');</code>使这个<code><input></code>是只读的,就不会拉起键盘了。</p>
<p><strong>2、无法复制</strong></p>
<p>这个问题是由于<code>input.select()</code>在ios下并没有选中全部内容,我们需要使用另一个方法来选中内容,这个方法就是 <code>input.setSelectionRange(0, input.value.length);</code></p>
<p>完整代码如下:</p>
<pre><code class="language-javascript">const btn = document.querySelector('#btn');
btn.addEventListener('click',() => {
const input = document.createElement('input');
input.setAttribute('readonly', 'readonly');
input.setAttribute('value', 'hello world');
document.body.appendChild(input);
input.setSelectionRange(0, 9999);
if (document.execCommand('copy')) {
document.execCommand('copy');
console.log('复制成功');
}
document.body.removeChild(input);
})
</code></pre>
<p>::: tip</p>
<p>部分内容来自<a href="https://github.com/axuebin/articles/issues/26">axuebin</a></p>
<p>:::</p>
]]>Javascript前端Chanx ([email protected])
- 天下无敌超级好用的东西https://chanx.tech/blog/awesome-toolshttps://chanx.tech/blog/awesome-tools分享一些超级好用的软件和工具Thu, 06 Aug 2020 00:00:00 GMT<![CDATA[<p><a href="https://voidtools.com/zh-cn/">搜索软件Everything</a></p>
<p><a href="https://zh.snipaste.com/">截图软件Snipaste</a></p>
<p><a href="https://www.caniuse.com/#home">前端函数兼容性查询</a></p>
]]>share工具随笔Chanx ([email protected])
- 使用CDN或对象存储加速网站访问https://chanx.tech/blog/website-speed-optimizationhttps://chanx.tech/blog/website-speed-optimization使用CDN或对象存储加速网站访问Thu, 23 Jul 2020 00:00:00 GMT<![CDATA[<h2>CDN和COS是什么</h2>
<p><strong>CDN</strong>:内容分发网络(Content Delivery Network,CDN),是在现有 Internet 中增加的一层新的网络架构,由遍布全球的高性能加速节点构成。这些高性能的服务节点都会按照一定的缓存策略存储您的业务内容,当您的用户向您的某一业务内容发起请求时,请求会被调度至最接近用户的服务节点,直接由服务节点快速响应,有效降低用户访问延迟,提升可用性。</p>
<p><strong>腾讯云叫COS,阿里云叫OSS。他们是同一个东西</strong></p>
<p><strong>COS</strong>:对象存储(Cloud Object Storage,COS)是腾讯云提供的一种存储海量文件的分布式存储服务,用户可通过网络随时存储和查看数据。腾讯云 COS 使所有用户都能使用具备高扩展性、低成本、可靠和安全的数据存储服务。</p>
<p>COS 通过控制台、API、SDK 和工具等多样化方式简单、快速地接入,实现了海量数据存储和管理。通过 COS 可以进行多格式文件的上传、下载和管理。腾讯云提供了直观的 Web 管理界面,同时遍布全国范围的 CDN 节点可以对文件下载进行加速。</p>
<p><strong>¥&……¥HGSDFS ,他在说什么啊</strong></p>
<h3>细说CDN</h3>
<p>首先我们要知道我们自己的网络服务器叫<strong>源站</strong>,腾讯云访问你服务器的过程叫<strong>回源</strong></p>
<p>腾讯云在全国各地有服务器,比如说东南西北各一台,然后中间有一台主的服务器</p>
<p>CDN是内容分发网络,他会访问一遍你的网站的,然后把你网站的一些内容缓存到主服务器上,分发给不同地区的服务器</p>
<p>当用户需要访问你的网站的时候,他可以从距离最近的服务器上获取资源</p>
<p><img src="https://static.chanx.tech/image/cjflp_0.png" alt="image-20200724211306537"></p>
<p><img src="https://static.chanx.tech/image/cjxmi_0.png" alt="image-20200724212250162"></p>
<p>联系日常生活来看,其实京东自营仓库跟这个就差不多</p>
<p>京东自营为什么能做到次日达,因为他在好几个地方建了仓库</p>
<p>比如他卖一个小饼干,他会在华南华北华东华西都分别储备一些小饼干</p>
<p>我广东地区的人买小饼干,他就直接在华南仓库发货</p>
<p>山东地区的人买小饼干,他就华北仓库发货</p>
<p>这样子就大大减少了物流时间,实现快速送货</p>
<p>而往往淘宝店都是些小店,他的店在广东,你在东北下单,商品不得不跨越大中国才到你的手里</p>
<h3>细说COS</h3>
<p>这个...我也不太懂啦</p>
<p>大概就是你只需要把重心放在文件上,如何安全地存储和传输文件都是他帮你做的</p>
<p>多样化的接入方式和多节点的存储,让你使用数据更加方便</p>
<p>前者就是不同的api文档啦,要用什么就看什么文档</p>
<p>后者就是什么多备份防止丢失、CDN加速等等</p>
<h2>利用CDN和COS</h2>
<p>COS可以用来放图片、文档什么的静态资源(就是不会变的那些</p>
<p>因为它云服务器的访问和传输速度当然比我自己的辣鸡小服务器快</p>
<p>我的辣鸡小服务器才5Mbps,放几张图片去访问就要等半天加载</p>
<p>如果是一张8M大图,那就呵呵呵呵呵呵呵呵呵</p>
<p>有了COS访问速度已经不错了,我们还可以用CDN进行优化</p>
<p>将COS的内容分发各地,能避免同一时间对COS的集中访问</p>
<p>对于我这种小破站来说,用COS或者CDN其中一个都是足够了</p>
<p>小破站主要是图片资源瞬间占用带宽严重,任意一个都能解决我目前遇到的问题</p>
<p>其实小破站也就自己访问比较多....等一会才多大事</p>
<p>但是!!!能白嫖的CDN和COS谁不喜欢呢?(滑稽)</p>
<p>CDN的使用大概就是把域名原来指向源站ip改成指向CDN服务器,然后在CDN里设置好源站IP和相关访问限制(如跨域、防盗链</p>
<p>COS最好是使用相关的客户端,因为我更多的时写Markdown时引入图片,所以我是配合<strong>Typora软件</strong>和<strong>PicGo自动上传图片</strong>使用</p>
<p>PicGo相关配置我应该会用一篇文章记录</p>
<p>::: tip
部分文章内容来自<a href="https://cloud.tencent.com/document/product">腾讯云</a>
:::</p>
]]>教程前端Chanx ([email protected])
- [转]Vuepress中Last Updated时间有误https://chanx.tech/blog/vuepress-last-updated-timehttps://chanx.tech/blog/vuepress-last-updated-timeVuepress中Last Updated时间有误Mon, 20 Jul 2020 00:00:00 GMT<![CDATA[<p>原文出处:<a href="https://ty-peng.gitee.io/views/note/ops/202004141352-vuepress-last-updated.html">ty-peng - VuePress Last Updated 时间有误的解决</a></p>
<!-- more -->
<h2><strong>此文章为转载</strong></h2>
<h2>正文</h2>
<p>之前一直以为文章<code>Last Updated</code>的时间每次都是取最新的时间是因为推送Pages分支时强制提交,只有一个最新提交的原因, 所以一直没去管,今天发觉按理编译之前<code>Last Updated</code>时间就已经确定了才对,于是找了下原因。</p>
<p>去VuePress的文档页面看了下,在默认主题的配置里有说明:</p>
<p>::: tip
VuePress的插件<code>last-updated</code>是基于<code>git</code>的,使用<code>git commit</code>的时间戳作为最后更新时间。
:::</p>
<p>我在本地编译运行了一下,发现<code>Last Updated</code>时间是对的, 但是通过GitHub Actions编译后的时间有误,都只是最新的时间。</p>
<p>检查workflow文件<code>main.yml</code>, 其中和之前本地部署不同的步骤应该只有<code>Checkout</code>那一步,</p>
<p>找到相关文档,查看配置项,其中有一项:</p>
<p>::: tip</p>
<pre><code class="language-yaml"># Number of commits to fetch. 0 indicates all history.
# Default: 1
fetch-depth: ''
</code></pre>
<p><code>fetch-depth</code>:要获取的提交数,0表示所有历史记录,默认为1。
:::</p>
<p>这就是问题的根源所在,在GitHub Actions中,按之前的配置,<code>fetch-depth</code>未配置取默认值1, 使用<code>Checkout</code>插件checkout代码只会fetch一个提交, 所以<code>Last Updated</code>插件获取不到其他文章的正确提交时间, 最后编译时都使用了最后一次提交时间作为最后更新时间。</p>
<p>修改如下:</p>
<pre><code class="language-yaml">name: Checkout 🛎️
uses: actions/checkout@v2
with:
persist-credentials: false
# Number of commits to fetch. 0 indicates all history.
fetch-depth: 0
</code></pre>
<p>提交推送远程仓库,自动部署,<code>Last Updated</code>时间恢复正常。</p>
]]>Vuepress前端Chanx ([email protected])
- 记一次笔记本加装内存条https://chanx.tech/blog/adding-memory-to-laptophttps://chanx.tech/blog/adding-memory-to-laptop<![CDATA[<img src="https://static.chanx.tech/image/b2hz9_0.png" alt="记一次笔记本加装内存条" /><br />第一次给笔记本加装内存条的经验分享]]>Mon, 20 Jul 2020 00:00:00 GMT<![CDATA[<p>第一次给笔记本加装内存条</p>
<!-- more -->
<h2>准备工作</h2>
<p>首先查看自己的电脑配置,看看需要什么类型的内存条</p>
<p>我搜了一下我的飞行(坠机)堡垒的配置,一开始没查到,后来在任务管理器看到相关信息</p>
<p><img src="https://static.chanx.tech/image/b2hz9_0.png" alt="image-20200720214431210"></p>
<p>然后就是去买内存条了,个人习惯京东。(记得准备螺丝刀,或者问问客服送不送</p>
<img src="https://static.chanx.tech/image/bca7l_0.png" alt="image-20200720214828662" />
<img src="https://static.chanx.tech/image/bc5bx_0.png" alt="image-20200720214907805" />
<h2>动手安装</h2>
<p>由于之前给笔记本换过硬盘,拆外壳有些经验(记得把螺丝放好,起壳的时候不要太暴力一般都ok</p>
<img src="https://static.chanx.tech/image/bej2f_0.png" alt="image-20200720215547078" />
<img src="https://static.chanx.tech/image/d544c_0.png" alt="image-20200720215743477" />
<img src="https://static.chanx.tech/image/bfb04_0.png" alt="image-20200720215947906" />
<p>对准插口和卡孔,倾斜约30°,向下压到两侧卡扣卡住即可。</p>
<img src="https://static.chanx.tech/image/d65ro_0.png" alt="image-20200720220231236" />
<h2>检查</h2>
<p>打开电脑,第一次进去可能比平时稍微久一点,应该是读新内存的信息</p>
<p>然后打开任务管理器,看到内存从7.9G变成15.9G,大功告成。</p>
<img src="https://static.chanx.tech/image/bhbnb_0.png" alt="Snipaste_2020-07-20_13-17-10" />
]]>硬件教程随笔Chanx ([email protected])
- Vuepress中使用Vue组件https://chanx.tech/blog/components-in-vuepresshttps://chanx.tech/blog/components-in-vuepressVuepress中使用组件Sat, 11 Jul 2020 00:00:00 GMT<![CDATA[<p>在Vuepress使用Vue组件</p>
<!-- more -->
<p>所有在<code>.vuepress/components</code>中找到的<code>*.vue</code>文件将会自动地被注册为全局的异步组件,如:</p>
<pre><code>.
└─ .vuepress
└─ components
├─ demo-1.vue
├─ OtherComponent.vue
└─ Foo
└─ Bar.vue
</code></pre>
<p>现在我们有一个组件是<code>Example.vue</code></p>
<h2>页面组件</h2>
<p>我们现在有一个页面是<code>Readme.md</code>,然后在Front Matter里填写字段<code>Layout: Example</code>,此时整个页面会被组件代替</p>
<h2>功能组件</h2>
<p>有时候我们只是想在页面里引入一个小组件,而不是想引入一个页面。那么可以直接在Markdown文件里写下<code><Example/></code>(markdown文件里支持vue的语法,而组件被全局注册,所以直接写就行,另外还有一些表达式什么的都可以)</p>
<p>详细介绍文档可以见:<a href="https://www.vuepress.cn/guide/using-vue.html">Vuepress官方文档</a></p>
]]>Vuepress前端Chanx ([email protected])
- 算法设计与分析基础https://chanx.tech/blog/algorithm-designhttps://chanx.tech/blog/algorithm-design算法设计与分析基础Thu, 18 Jun 2020 12:00:00 GMT<![CDATA[<p>专业课内容节选自《算法设计与分析基础》潘彦(译)</p>
<!-- more -->
<p>绪论:欧几里得算法的证明和实现(扩展:扩展欧几里得算法)、求最大公约数的多种算法(扩展:最小公倍数)、素数判定的多种算法、基本数据结构</p>
<p>算法效率分析基础:描述效率的渐进式符号、非递归和递归的效率分析</p>
<p>蛮力法:选择和冒泡排序、顺序查找和字符串匹配、最近对和凸包问题、穷举查找(旅行商问题、背包问题、分配问题、DFS和BFS)</p>
<p>减治法:插入排序、拓扑排序、生成组合对象的算法(生成排列、子集)、减常因子算法(折半查找、假币问题)、减可变规模算法:计算中值和选择问题、插值查找</p>
<p>分治法:合并排序、快速排序、二叉树遍历及其相关特性、大整数乘法和Strassen矩阵乘法、最近对和凸包问题</p>
<p>变治法:预排序、高斯消去法、AVL树四种旋转、堆排序、霍纳法则和二进制幂、问题化简(最小公倍数)</p>
<p>时空权衡:计数排序(比较、分布、字符串匹配:Horspool、Boyer-Moore)</p>
<p>动态规划:背包问题和记忆化功能、最优二叉查找树、Warshall和Floyd算法</p>
<p>贪婪技术:Prim算法、Kruskal算法、Dijkstra算法、哈夫曼树及其编码</p>
<p>部分练习题如下:</p>
<p><img src="https://static.chanx.tech/image/7ep46_0.png" alt="2020061801"></p>
<p><img src="https://static.chanx.tech/image/7enk4_0.png" alt="2020061802"></p>
]]>专业课笔记Chanx ([email protected])
- 欧几里得公式证明https://chanx.tech/blog/proof-of-euclidean-formulahttps://chanx.tech/blog/proof-of-euclidean-formula欧几里得算法(辗转相除法)的数学证明过程,包括最大公约数性质的推导与验证Mon, 08 Jun 2020 00:00:00 GMT<![CDATA[<p> <strong>先说结论:</strong><code>gcd(a,b) = gcd(b,a mod b)</code></p>
<p><strong>证明:</strong></p>
<p>写出式子:<code>a = b*q + a mod b</code>令<code>r = a mod b</code></p>
<p>① 假设d是(a,b)的一个公约数,则</p>
<p><code>d|a且d|b</code>,而<code>r = a - kb</code>,因此<code>d|r</code></p>
<p><u>因此d是(b,a mod b)的公约数</u></p>
<p>② 假设d是(b,a mod b)的公约数,则</p>
<p><code>d|b且d|r</code> ,但是<code>a = kb + r</code></p>
<p><u>因此d也是(a,b)的公约数</u></p>
<p><strong>因此(a,b)和(b,a mod b)的公约数是一样的,其最大公约数也必然相等,得证</strong></p>
<p><img src="https://static.chanx.tech/image/92a9p_0.png" alt="image-20200611125658094"></p>
]]>数据结构与算法其他Chanx ([email protected])
- Vuepress基于Valine的评论功能https://chanx.tech/blog/vuepress-valinehttps://chanx.tech/blog/vuepress-valineVuepress基于Valine的评论功能Mon, 08 Jun 2020 00:00:00 GMT<![CDATA[<h2>安装插件</h2>
<p>如果使用 <code>npm</code>:</p>
<pre><code class="language-sh">npm install --save vuepress-plugin-comment
</code></pre>
<p>如果使用 <code>yarn</code>:</p>
<pre><code class="language-sh">yarn add vuepress-plugin-comment -D
</code></pre>
<h2>配置插件</h2>
<h3>vuepress项目引用</h3>
<p>将 <code>vuepress-plugin-comment</code> 添加到vuepress项目的插件配置中:</p>
<pre><code class="language-javascript">module.exports = {
plugins: [
[
'vuepress-plugin-comment',
{
choosen: 'valine',
// options选项中的所有参数,会传给Valine的配置
options: {
el: '#valine-vuepress-comment',
appId: 'Your own appId',
appKey: 'Your own appKey'
}
}
]
]
}
</code></pre>
<h3>获取appId和appKey</h3>
<p>请先<a href="https://leancloud.cn/dashboard/login.html#/signin">登录</a>或<a href="https://leancloud.cn/dashboard/login.html#/signup">注册</a> <code>LeanCloud</code>, 进入<a href="https://leancloud.cn/dashboard/applist.html#/apps">控制台</a>后点击左下角<a href="https://leancloud.cn/dashboard/applist.html#/newapp">创建应用</a>:</p>
<p><img src="https://static.chanx.tech/image/atlyz_0.jpeg" alt="img"></p>
<p>应用创建好以后,进入刚刚创建的应用,选择左下角的<code>设置</code>><code>应用Key</code>,然后就能看到你的<code>APP ID</code>和<code>APP Key</code>了:</p>
<p><img src="https://static.chanx.tech/image/atxsc_0.jpeg" alt="img"></p>
<p>把获取到的appId和appKey填入vuepress的插件配置当中,就可以使用评论功能了。</p>
<h3>更多配置项</h3>
<h4>el</h4>
<ul>
<li>类型:<code>String</code></li>
<li>默认值:<code>null</code></li>
<li>必要性:<code>true</code></li>
</ul>
<p>Valine 的初始化挂载器。可以是一个<code>CSS 选择器</code>,也可以是一个实际的<code>HTML元素</code>。</p>
<h4>appId</h4>
<ul>
<li>类型:<code>String</code></li>
<li>默认值:<code>null</code></li>
<li>必要性:<code>true</code></li>
</ul>
<p>从<code>LeanCloud</code>的应用中得到的<code>appId</code>.</p>
<blockquote>
<p><a href="https://valine.js.org/quickstart.html">获取appId 和 appKey</a>。</p>
</blockquote>
<h4>appKey</h4>
<ul>
<li>类型:<code>String</code></li>
<li>默认值:<code>null</code></li>
<li>必要性:<code>true</code></li>
</ul>
<p>从<code>LeanCloud</code>的应用中得到的<code>appKey</code>.</p>
<blockquote>
<p><a href="https://valine.js.org/quickstart.html">获取appId 和 appKey</a>。</p>
</blockquote>
<h4>placeholder</h4>
<ul>
<li>类型:<code>String</code></li>
<li>默认值:<code>Just go go</code></li>
<li>必要性:<code>false</code></li>
</ul>
<p>评论框<code>占位提示符</code>。</p>
<h4>path</h4>
<ul>
<li>类型:<code>String</code></li>
<li>默认值:<code>window.location.pathname</code></li>
<li>必要性:<code>false</code></li>
</ul>
<p>当前<code>文章页</code>路径,用于区分不同的<code>文章页</code>,以保证正确读取该<code>文章页</code>下的评论列表。
可选值:</p>
<ul>
<li><code>window.location.pathname</code> (默认值,推荐)</li>
<li><code>window.location.href</code></li>
<li><code>自定义</code></li>
</ul>
<blockquote>
<ul>
<li>I. 请保证每个<code>文章页</code>路径的唯一性,否则可能会出现不同<code>文章页</code>下加载相同评论列表的情况。</li>
<li>II. 如果值为<code>window.location.href</code>,可能会出现随便加<code>不同参数</code>进入该页面,而被判断成新页面的情况。</li>
</ul>
</blockquote>
<h4>avatar</h4>
<ul>
<li>类型:<code>String</code></li>
<li>默认值:<code>mm</code></li>
<li>必要性:<code>false</code></li>
</ul>
<p><code>Gravatar</code> 头像展示方式。</p>
<p>可选值:</p>
<ul>
<li><code>''</code>(空字符串)</li>
<li><code>mp</code></li>
<li><code>identicon</code></li>
<li><code>monsterid</code></li>
<li><code>wavatar</code></li>
<li><code>retro</code></li>
<li><code>robohash</code></li>
<li><code>hide</code></li>
</ul>
<p>更多信息,请查看<a href="https://valine.js.org/avatar.html">头像配置</a>。</p>
<h4>meta</h4>
<ul>
<li>类型:<code>Array</code></li>
<li>默认值:<code>['nick','mail','link']</code></li>
<li>必要性:<code>false</code></li>
</ul>
<p>评论者相关属性。</p>
<h4>pageSize</h4>
<ul>
<li>类型:<code>Number</code></li>
<li>默认值:<code>10</code></li>
<li>必要性:<code>false</code></li>
</ul>
<p>评论列表分页,每页条数。</p>
<h4>lang</h4>
<ul>
<li>类型:<code>String</code></li>
<li>默认值:<code>zh-CN</code></li>
<li>必要性:<code>false</code></li>
</ul>
<p>多语言支持。</p>
<p>可选值:</p>
<ul>
<li><code>zh-CN</code></li>
<li><code>zh-TW</code></li>
<li><code>en</code></li>
<li><code>ja</code></li>
</ul>
<p>如需<code>自定义语言</code>,请参考<a href="https://valine.js.org/i18n.html">i18n</a>。</p>
<h4>visitor</h4>
<ul>
<li>类型:<code>Boolean</code></li>
<li>默认值:<code>false</code></li>
<li>必要性:<code>false</code></li>
</ul>
<p><a href="https://valine.js.org/visitor.html">文章访问量统计</a>。</p>
<h4>highlight</h4>
<ul>
<li>类型:<code>Boolean</code></li>
<li>默认值: <code>true</code></li>
<li>必要性: <code>false</code></li>
</ul>
<p><code>代码高亮</code>,默认开启,若不需要,请手动关闭</p>
<h4>avatarForce</h4>
<ul>
<li>类型: <code>Boolean</code></li>
<li>默认值: <code>false</code></li>
<li>必要性: <code>false</code></li>
</ul>
<p>每次访问<code>强制</code>拉取最新的<code>评论列表头像</code></p>
<blockquote>
<p>不推荐设置为<code>true</code>,目前的<code>评论列表头像</code>会自动带上<code>Valine</code>的版本号</p>
</blockquote>
<h4>recordIP</h4>
<ul>
<li>类型: <code>Boolean</code></li>
<li>默认值: <code>false</code></li>
<li>必要性: <code>false</code></li>
</ul>
<p>是否记录评论者IP</p>
<blockquote>
<pre><code>v1.3.5+
</code></pre>
</blockquote>
<h4>serverURLs</h4>
<ul>
<li>类型: <code>String</code></li>
<li>默认值: <code>http[s]://[tab/us].avoscloud.com</code></li>
<li>必要性: <code>false</code></li>
</ul>
<blockquote>
<p>⚠️ 该配置适用于国内<code>自定义域名</code>用户, <code>海外版本</code>会自动检测(无需手动填写) <code>v1.3.10+</code></p>
</blockquote>
<h4>emojiCDN</h4>
<ul>
<li>类型: <code>String</code></li>
<li>默认值: ``</li>
<li>必要性: <code>false</code></li>
</ul>
<p>设置<code>表情包CDN</code>,参考<a href="https://valine.js.org/emoji.html">自定义表情</a></p>
<blockquote>
<pre><code>v1.4.5+
</code></pre>
</blockquote>
<h4>emojiMaps</h4>
<ul>
<li>类型: <code>Object</code></li>
<li>默认值: <code>null</code></li>
<li>必要性: <code>false</code></li>
</ul>
<p>设置<code>表情包映射</code>,参考<a href="https://valine.js.org/emoji.html">自定义表情</a></p>
<blockquote>
<pre><code>v1.4.5+
</code></pre>
</blockquote>
<h4>enableQQ</h4>
<ul>
<li>类型: <code>Boolean</code></li>
<li>默认值: <code>false</code></li>
<li>必要性: <code>false</code></li>
</ul>
<p>是否启用<code>昵称框</code>自动获取<code>QQ昵称</code>和<code>QQ头像</code>, 默认关闭,需<code>博/网站主</code>主动启用</p>
<blockquote>
<pre><code>v1.4.6+
</code></pre>
</blockquote>
<h4>requiredFields</h4>
<ul>
<li>类型: <code>Array</code></li>
<li>默认值: <code>[]</code></li>
<li>必要性: <code>false</code></li>
</ul>
<p>设置<code>必填项</code>,默认<code>匿名</code>,可选值:</p>
<ul>
<li><code>['nick']</code></li>
<li><code>['nick','mail']</code></li>
</ul>
<blockquote>
<pre><code>v1.4.6+
</code></pre>
</blockquote>
<h2>扩展功能</h2>
<p><a href="https://valine.js.org/avatar.html">头像配置</a>、<a href="http://www.zhaojun.im/hexo-valine-admin/">邮件提醒</a>、<a href="https://valine.js.org/i18n.html">多语言支持</a>、<a href="https://valine.js.org/visitor.html">文章阅读量统计</a>、<a href="https://valine.js.org/emoji.html">自定义表情</a></p>
<p><u>其中,邮件提醒有几个小问题</u>:</p>
<ol>
<li><p><strong>国内版的绑定域名貌似需要备案</strong></p>
</li>
<li><p><strong>LeanCloud休眠调整,定时任务self_wake无法唤醒</strong> => 解决办法可以参考<a href="https://www.antmoe.com/posts/ff6aef7b/">小康博客</a></p>
<p><strong>我的解决办法</strong>是利用我windows服务器自带的计划任务定时访问评论后台达到唤醒效果:</p>
<p>首先新建一个bat文件(效果大概是自动打开IE浏览器访问指定网页,30秒后关闭浏览器)</p>
<blockquote>
<p>建好bat文件之后最好先自己访问一遍确定IE浏览器访问的时候能正常访问到网页</p>
<p>如果访问速度较慢,建议把30秒调成更长的时间来保证稳定</p>
</blockquote>
<pre><code class="language-sh">explorer.exe open=你的评论后台地址
Ping -n 30 127.1>nul
Taskkill /f /im "iexplore.exe"
</code></pre>
<p><img src="https://static.chanx.tech/image/avokr_0.png" alt="image-20200613101522289"></p>
<p><img src="https://static.chanx.tech/image/avz41_0.png" alt="image-20200613101209202"></p>
<p><img src="https://static.chanx.tech/image/avvcm_0.png" alt="image-20200613101238985"></p>
<p><img src="https://static.chanx.tech/image/awllk_0.png" alt="image-20200613101304356"></p>
</li>
<li><p><strong>Valine的管理员账户(部署后/sign-up页面出现Not Found)</strong></p>
<pre><code>LeanCloud结构化数据里_User表添加一行
必备三个字段email(此 email 必须为配置中的 SMTP_USER 或 TO_EMAIL)、username、password
然后进入到评论管理后台
账号为email字段输入的信息
密码为password字段输入的信息
</code></pre>
</li>
</ol>
<hr>
<p>::: tip
部分文字整理自<a href="https://valine.js.org/">Valine官网</a>、<a href="https://github.com/zhaojun1998/Valine-Admin">Valine-Admin</a>及其他相关网页
:::</p>
]]>Vuepress教程前端Chanx ([email protected])
- 原生JS实现简单计时器https://chanx.tech/blog/simple-timer-with-vanilla-jshttps://chanx.tech/blog/simple-timer-with-vanilla-js原生JS实现简单计时器Sat, 06 Jun 2020 00:00:00 GMT<![CDATA[<p>在线预览: <strong><a href="http://demo.chanx.tech/timer.html">点我查看demo</a></strong></p>
<!-- more -->
<h2>做什么</h2>
<p>用原生的js实现一个简单计时器。比如说记录编辑框他输入内容所用的时间。</p>
<h2>怎么做</h2>
<p><strong>HTML</strong></p>
<p>一个编辑框和一个时间显示器</p>
<p><strong>JS</strong></p>
<p>编辑框获得焦点时<code>setInterval()</code>进行计时,失去焦点时<code>clearInterval()</code>取消计时
另外利用<code>localStorage</code>可以进行数据的保存,刷新后能读取上次的时间</p>
<h2>代码</h2>
<pre><code class="language-html"><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>计时器</title>
</head>
<body>
<div id="container">
<h3 id="status">计时器案例 - 未做题</h3>
<textarea rows="5" cols="20" onfocus="inputFocus()" onblur="innputBlur()" placeholder="点击输入框开始计时"></textarea>
<h4 id="timer">做题时长: 0时0分0秒</h4>
<button onclick="resetTime()">重新计时</button>
</div>
<script>
var second = 0; //记录时间-秒
var timing; //定时器
//更新显示时间
function updateTime(){
let _hour = Math.floor(second /3600);
let _minute = Math.floor((second%3600) / 60);
let _second = (second%3600) %60;
document.getElementById("timer").innerText = "做题时长:"+_hour+"时"+_minute+"分"+_second+"秒";
}
function resetTime(){
window.localStorage.setItem("status",0);
second = 0;
updateTime();
}
//输入框获得焦点时开始计时
function inputFocus(){
document.getElementById("status").innerText = "计时器案例 - 做题中";
timing = setInterval(function(){
second ++;
updateTime();
},1000);
}
//输入框失去焦点取消计时
function innputBlur(){
document.getElementById("status").innerText = "计时器案例 - 未做题";
clearInterval(timing);
}
//网页加载完执行数据初始化
window.onload = function(){
let t = window.localStorage.getItem("time");
second = t?t:0; //无数据时默认为0
updateTime();
}
//网页刷新前进行数据缓存
window.onbeforeunload = function(){
innputBlur();
window.localStorage.setItem("time",second);
}
</script>
</body>
</html>
</code></pre>
<h2>思考</h2>
<p>是否可以利用两个开始和结束的两个时间戳获取一个时间段?但是需要注意的是我们<code>new Date()</code>获取的时间可能会受到系统时间的影响,导致获取的数据不合法。(2020-06-06) </p>
<p><strong><code>setTimeout</code>和<code>setInterval</code>究竟谁更适合实现计时?</strong></p>
<blockquote>
<p>看了红宝书,里面说了一句setInterval很少会出现在生产环境。于是想到了该计时器的实现。</p>
</blockquote>
<p><code>setInterval</code>到了时间就会把回调函数推进消息队列,所以不适合长时间和阻塞的循环任务,比如时间长的ajax不断被推进消息队列,然后消息队列越来越长,影响整个系统。与此同时,不能保证两次任务之间保持间隔。<code>setTimeout</code>实现循环任务就能保证任务有间隔,上一次任务成功回调才到下一次。</p>
<p>然而对于计时器这种比较简单的任务来说,不至于出现<code>setInterval</code>被塞爆的情况。而且计时器我们关注的是到时间就把回调推进消息队列,而不是两次时间增长之间保持间隔。</p>
<p><strong>所以我认为在这个场景下,<code>setInterval</code>是更适合计时器的。</strong>(2020-11-18)</p>
<p>另外,由于JS的运行机制。两种定时器的实现方式都是有微小误差的。而<code>performance</code>中的<code>计时api</code>适合于性能检测等高精度时间计算,不适合用在这</p>
]]>JavascriptDemo前端Chanx ([email protected])
- 计算机组成原理https://chanx.tech/blog/computer-organizationhttps://chanx.tech/blog/computer-organization计算机组成原理Thu, 04 Jun 2020 15:44:15 GMT<![CDATA[<h2>汉字在计算机中的表示方法</h2>
<h3>汉字的输入编码(输入)</h3>
<blockquote>
<p><strong>国标码 = 区位码(十六进制)+ 2020H</strong></p>
</blockquote>
<ol>
<li>数字编码
常用的是国标区位码,用数字串表示一个汉字的输入。</li>
<li>拼音码(拼音输入)</li>
<li>字形编码(五笔输入)</li>
</ol>
<h3>汉字内码(内部处理)</h3>
<blockquote>
<p><strong>汉字内码 = 国标码 + 8080H</strong></p>
</blockquote>
<p>用于汉字信息的存储、交换、检索等操作的机内代码,一般为双字节表示。</p>
<h3>汉字字模码(输出)</h3>
<p>用点阵来表示汉字字形,就是操作系统中的字体库文件。当显示输出或打印输出的时候才会检索字库。</p>
<h2>校验码</h2>
<h3>简单奇偶校验</h3>
<p>奇校验 ── 整个校验码(有效信息位和校验位)中“1”的个数为奇数
偶校验 ── 整个校验码中“1”的个数为偶数</p>
<blockquote>
<p>简单奇偶校验只能检测奇数个错误检测,无法检测偶数个错误,更无法识别错误的位置</p>
</blockquote>
<h3>交叉奇偶校验</h3>
<p>横向和纵向都有奇偶校验,提高检测到错误的概率</p>
<h3>海明校验码(多重奇偶校验)</h3>
<p><a href="https://blog.csdn.net/weixin_42426249/article/details/89428080">见CSDN博客</a></p>
<h3>循环冗余校验码</h3>
<p><a href="https://blog.csdn.net/T146lLa128XX0x/article/details/88968511">见CSDN博客(例题有误)</a></p>
]]>专业课笔记Chanx ([email protected])
- 2020阿里云备案过程记录https://chanx.tech/blog/beianhttps://chanx.tech/blog/beian详细记录在阿里云完成网站备案的全过程,包括工信部备案和公安联网备案的步骤与注意事项Sat, 30 May 2020 00:00:00 GMT<![CDATA[<p>历经<strong>大约一个月</strong>时间,成功完成备案的所有操作。</p>
<!-- more -->
<p>没上大学的时候偶尔也会买个虚拟空间自己瞎弄,但是后面觉得买虚拟空间太烦,而且便宜的经常性断连;上了大学后,用着 <a href="https://promotion.aliyun.com/ntms/act/campus2018.html">阿里云的学生云主机</a> 跑我的Java课程设计(一个简单的即时聊天程序)。后来发现闲置的云主机能用来跑些网页demo什么的,但是IP访问太不爽了。于是下定决心碰一下备案这块东西。</p>
<h2>备案初审1天</h2>
<p>在阿里云手机APP上进行网站和个人主体基本信息的填写,要进行人脸识别什么的。</p>
<p><strong>网站名称就写自己网站要干什么好了</strong></p>
<p>这个没什么关系,备完案之后想什么名字就什么名字(不违法就好</p>
<p>各种信息填写完成后,提交阿里云,他会帮你把材料看一遍,看看什么问题</p>
<p>当天下午阿里云打来电话,核对了身份证和域名信息之后</p>
<p>说我的网站名称不合格(XXX的个人网站),改一个跟内容相关的(前端技术学习笔记)</p>
<p>然后问了几个问题,关于你网站用途的</p>
<p>我说是记录一些学习笔记,个人博客这个样子</p>
<p>你有什么不懂的也能问问他</p>
<h2>管局审核7天</h2>
<p>4月28日,通过阿里云的初审之后,他会帮你把材料递给通信管理局</p>
<p>阿里云客服说管局审核期间要保持电话畅通,管局可能会有人打电话</p>
<p>然后就天天看手机,等等等等等,好几次房地产推销电话让我惊喜得要死...</p>
<p>阿里云那边给出得预测时间是12-20工作日内能通过审核</p>
<p>我经过五一假期后,<strong>5月9日下午邮件和短信同时收到了备案结果,没有收到电话</strong></p>
<p>欣喜若狂地把工信部备案号加到网站页脚</p>
<h2>公安联网备案14天</h2>
<p>阿里云备案系统提示:搞完工信部的备案之后,30天内要完成<a href="http://www.beian.gov.cn/">公安联网备案</a></p>
<ol>
<li>联网备案登录注册账号</li>
<li>开办主体管理填写个人信息</li>
<li>新办网站申请填写网站信息,信息填写可以参照<a href="https://help.aliyun.com/knowledge_detail/36981.html">这里</a></li>
</ol>
<p>网站服务类型如实填写就是了,我填的<strong>交互式-博客个人空间</strong></p>
<p>然后继续等等等等,<strong>等到5月25日收到公安平台的短信</strong></p>
<p>没有面审和电话</p>
<p>把公安备案号弄在网站页脚就完事了</p>
<p><strong>4月28日到5月25日,我的域名备案也就全部结束了,算是很舒服的</strong></p>
]]>教程网站备案其他Chanx ([email protected])
- vscode用户片段或代码模板的使用https://chanx.tech/blog/vscode-snippetshttps://chanx.tech/blog/vscode-snippets<![CDATA[<img src="/img/vscode-snippets.jpg" alt="vscode用户片段或代码模板的使用" /><br />介绍如何在VSCode中创建和使用代码片段(snippets)来提高编程效率,包括自定义Markdown博客模板的详细配置方法]]>Sat, 30 May 2020 00:00:00 GMT<![CDATA[<p>使用vscode的时候,肯定想自定义一些代码模板来提高效率</p>
<!-- more -->
<h2>配置模板</h2>
<p><strong>这里举一个我码博客文章的一个Markdown模板</strong></p>
<p><img src="https://static.chanx.tech/image/8xl1p_0.png" alt="image-20200530162713944"></p>
<p><u>打开配置文件:文件 - 首选项 - 用户片段 - markdown.json</u></p>
<p>可以看到一个默认的Example</p>
<pre><code class="language-json"> // Place your snippets for markdown here. Each snippet is defined under a snippet name and has a prefix, body and
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. Placeholders with the
// same ids are connected.
// Example:
// "Print to console": {
// "prefix": "log",
// "body": [
// "console.log('$1');",
// "$2"
// ],
// "description": "Log output to console"
// }
</code></pre>
<p>那么我的模板是什么呢?</p>
<pre><code class="language-json"> "Blog Template": { //用于区别的名称
"prefix": "blog", //使用模板的命令 比如这里就是输入blog就能打开该模板
"body": [ //body里面就是模板每一行的内容
"---",
"title: '$TM_FILENAME_BASE$1'", //$TM_FILENAME_BASE自动获取不带扩展名的文件名
"date: $CURRENT_YEAR-$CURRENT_MONTH-$CURRENT_DATE $CURRENT_HOUR:$CURRENT_MINUTE:$CURRENT_SECOND", //详细日期如2020-05-30 16:34:30
"# 永久链接",
"# permalink: '/hello-world'",
"# 文章访问密码",
"# keys: '123'",
"# 是否发布文章",
"# publish: false",
"# 置顶: 降序,可以按照 1, 2, 3, ... 来降低置顶文章的排列优先级",
"# sticky: 1",
"tags:",
"- '$2'",
"categories:",
"- '$TM_DIRECTORY$3'", //自动获取目录路径
"---",
"",
"$0",
],
"description": "blog file template." //模板的描述信息
}
</code></pre>
<p>其中有一些$或${}变量能让我们设置:</p>
<pre><code>TM_FILENAME 当前文件名
TM_FILENAME_BASE 当前文件名,不带扩展名
CURRENT_YEAR 当前年份
CURRENT_YEAR_SHORT 当前年份,最后两位数字
CURRENT_MONTH 当前月份数字形式,两位表示
CURRENT_MONTH_NAME 当前月份英文形式,如 July
CURRENT_MONTH_NAME_SHORT 当前月份英文缩写形式,如 Jul
CURRENT_DATE 当前日
CURRENT_DAY_NAME 当前星期,如 Monday
CURRENT_DAY_NAME_SHORT 当前星期缩写形式,如 Mon
CURRENT_HOUR 当前小时,24小时格式,两位表示
CURRENT_MINUTE 当前分钟,两位表示
CURRENT_SECOND 当前秒,两位表示
TM_DIRECTORY 当前文件所属目录的绝对路径
TM_FILEPATH 当前文件的绝对路径
</code></pre>
<p>更多的语法可以参考<a href="https://code.visualstudio.com/docs/editor/userdefinedsnippets">vscode文档</a></p>
<p>你可能会好奇$1,$2,$0这些是什么东西</p>
<pre><code>$1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. Placeholders with the same ids are connected.
他们是光标停止的地方,能让你快速切换至下一编辑点,$0是最后一个编辑点。${1:label}能设置占位符,id相同的占位符会被连接起来。
举例:打开模板后你的光标将会自动跳到$1处,按下tab光标跳到$2处,最后跳到$0。
</code></pre>
<h2>激活模板</h2>
<p>完成模板设置之后,你打开一个markdown文件,输入使用模板的命令如blog</p>
<p>发现居然没反应!</p>
<p><strong>你还需要打开vscode的配置文件setting.json</strong></p>
<p><img src="https://static.chanx.tech/image/8ydz5_0.png" alt="image-20200530164742943"></p>
<p><strong>添加如下代码</strong></p>
<pre><code class="language-json">"[markdown]":{
"editor.quickSuggestions": true
}
</code></pre>
<h2>享受快乐</h2>
<p>然后享受模板带来的快乐吧!</p>
<p><img src="https://static.chanx.tech/image/8ykhx_0.png" alt="image-20200530164913044"></p>
<p><img src="https://static.chanx.tech/image/8za6p_0.png" alt="image-20200530164906681"></p>
]]>教程VSCode效率工具其他Chanx ([email protected])
- 计算机网络https://chanx.tech/blog/computer-networkinghttps://chanx.tech/blog/computer-networking<![CDATA[<img src="/img/network.jpg" alt="计算机网络" /><br />计算机网络基础知识总结,包括网络架构、协议和通信原理等内容]]>Tue, 26 May 2020 00:00:00 GMT<![CDATA[<h2>概述</h2>
<p>互联网服务提供者 ISP(Internet Service Provider):中国电信、中国移动、中国联通</p>
<p>互联网交换点 IXP (Internet eXchange Point):允许两个网络直接相连并交换分组,而不需要再通过第三个网络来转发分组</p>
<p>边缘部分的端系统之间通信方式:</p>
<ol>
<li><p>客户-服务器方式(C/S - Client / Server)</p>
<blockquote>
<p>特殊的,浏览器-服务器方式(B/S - Browser / Server)</p>
</blockquote>
</li>
<li><p>对等方式(P2P - Peer to Peer)</p>
</li>
</ol>
<p>网络核心部分起特殊作用的是:<strong>路由器</strong>(实现分组交换的构件,任务是转发分组交换)</p>
<p><img src="https://static.chanx.tech/image/7v0v0_0.png" alt="image-20200526193011677"></p>
<p>不使用分组交换的话,两端通信需要占用整一段链路进行传输(图中黑色部分有4段)</p>
<p><strong>使用分组交换后,在哪段链路上传送才占用这段链路的通信资源</strong></p>
<p><img src="https://static.chanx.tech/image/7tzbk_0.png" alt="image-20200526193011677"></p>
]]>专业课计算机网络笔记笔记Chanx ([email protected])
- Taro-ui开发的坑https://chanx.tech/blog/taro-developmenthttps://chanx.tech/blog/taro-developmentTaro开发的坑Mon, 18 May 2020 00:00:00 GMT<![CDATA[<p>记录使用<a href="https://taro-ui.jd.com/#/docs/introduction">Taro-UI</a>开发的一些问题</p>
<!-- more -->
<h2>搜索框问题</h2>
<p>补充:这个搜索框问题是事后很久才补的,修修改改不知道是不是正确做法了,但是就是这么个思路。</p>
<h3>使用<code><input></code>原生开发</h3>
<pre><code class="language-html"><input type="search"/>
</code></pre>
<p>但是需求想把<strong>手机软键盘右下角换成搜索按钮</strong>,安卓上述方法可以实现,但是ios实现 '换行' 变 '搜索' </p>
<pre><code class="language-html"><form action="javascript:return true">
<input type="search" placeholder="请输入">
</form>
</code></pre>
<p>补充:ios真的是麻烦得飞起</p>
<pre><code class="language-html"><!--'换行'变'前往'-->
<form action="javascript:return true">
<input type="text" placeholder="请输入">
</form>
<!--直接唤醒数字九键键盘-->
<input type="text" pattern="[0-9]*" placeholder="请输入数字">
<!--显示26键数字键盘,带有标点符号-->
<form action="javascript:return true">
<input type="number" placeholder="请输入数字2">
</form>
</code></pre>
<p>另外<a href="https://www.cnblogs.com/ypppt/p/12846185.html">移动端H5开发软键盘的坑</a></p>
<h3>使用组件<code>AtSearchBar</code></h3>
<pre><code class="language-html"><AtSearchBar
value={this.state.value}
onChange={this.onChange.bind(this)}
onActionClick={this.onActionClick.bind(this)}
/>
</code></pre>
<p>套上了组件你以为可以了?不不不不</p>
<pre><code class="language-html"><Atform onSubmit={this.onActionClick.bind(this)}>
<AtSearchBar
value={this.state.value}
onChange={this.onChange.bind(this)}
onActionClick={this.onActionClick.bind(this)}
/>
</Atform>
</code></pre>
<p>这样子软键盘上的搜索按钮就能起作用了</p>
<p>那ios软键盘还有一个完成按钮怎么触发呢?</p>
<pre><code class="language-html"><Atform onSubmit={this.onActionClick.bind(this)}>
<AtSearchBar
value={this.state.value}
onChange={this.onChange.bind(this)}
onActionClick={this.onActionClick.bind(this)}
onBlur={this.onActionClick.bind(this)}
/>
</Atform>
</code></pre>
<p>采用失去焦点即搜索的办法即可</p>
<h2>置底输入框问题</h2>
<p>需求是想要一个置底(<code>position:absolute;</code>)的输入框,但是在ios里面点击输入框后,软键盘会完全遮挡输入框。</p>
<p>经过分析,是因为在安卓手机打开软键盘后,页面高度和显示高度取的是<strong>屏幕高度 - 软键盘高度</strong>,即大约原来一半</p>
<p>但是在ios手机上,此时页面高度还是屏幕高度,但是显示的区域只有原来一半,这时候taro的壳会自动出现滚动条,而且我们无法进行手动滚动</p>
<p>(啊!我也不知道怎么描述/不懂具体原因啊,反正就是不能实现手动滚动</p>
<p>于是我针对ios手机进行高度的特殊处理,将页面高度与显示区域进行同步,实现手动滚动</p>
<p>首先是判断手机类型,默认是安卓,防止意外情况。</p>
<pre><code class="language-javascript">var u = navigator.userAgent;
var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1; //android终端
var isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端
let type;
if(!isAndroid && isIOS){
type = "ios";
}else type = "android";
this.setState({phoneType:type});
</code></pre>
<p>然后监听输入框的获得焦点事件,触发下列行为</p>
<pre><code class="language-javascript">setTimeout(function () {
//ios高度特殊处理,-30px是为了页面样式
if(_this.state.phoneType == "ios"){
document.getElementsByClassName("taro_router")[0].style.minHeight = 'calc(50%-30px)';
document.getElementsByClassName("taro_router")[0].style.maxHeight = 'calc(50%-30px)';
}
//输入框滚动置底,底部与页面底部对齐
document.getElementsByClassName("item__footer")[0].scrollIntoViewIfNeeded(false);
}, 10);
</code></pre>
<p>记得失去焦点的时候进行恢复操作即可</p>
]]>TaroH5前端Chanx ([email protected])
- Javascript高级程序设计https://chanx.tech/blog/advanced-javascript-programminghttps://chanx.tech/blog/advanced-javascript-programmingJavascript高级程序设计Sat, 02 May 2020 00:00:00 GMT<![CDATA[<p>阅读《Javascript高级程序设计》红宝书的笔记记录,参杂一些个人的思考和想法。</p>
<!-- more -->
<h2>Javascript简介</h2>
<h3>JS和ES的关系</h3>
<p>就Javascript和ECMAscript来说,一般我们认为是同一个东西。</p>
<p>实际上,一个完整的Javascript应该由 <strong>核心(ECMAscript)、文档对象模型(Document Object Model)、浏览器对象模型(Browser Object Model)</strong> 三部分组成</p>
<p>由此看来,Javascript的范围比ECMAscript大得多</p>
<blockquote>
<p>个人理解ECMAscript是语法部分</p>
</blockquote>
<h2>基本概念</h2>
<h3>Typeof操作符</h3>
<p>基于ECMAscript是松散类型的,因此需要有一种手段来检测给定变量的数据类型</p>
<p>typeof就是负责提供这方面信息的<strong>操作符</strong>。</p>
<pre><code class="language-javascript">var message = "some string";
alert(typeof message); //"string"
alert(typeof (message));//"string"
alert(typeof 95); //"number"
</code></pre>
<p>是的,没错,它是一个操作符而不是一个函数。尽管例子中的圆括号可以使用,但是它并不是必需的。</p>
<h3>Undefined类型和Null类型</h3>
<p>Undefined类型只有一个值,即特殊的undefined。</p>
<pre><code class="language-javascript">var message;
alert(message == undefined); //true
var message = undefined;
alert(message == undefined); //true
</code></pre>
<p>以上两段代码实际上是等价的,在声明变量时未对变量进行初始化,它就会被隐式初始化为undefined</p>
<h2>Todo</h2>
<pre><code>P33 string字符串
p48 加性操作符
p51 相等操作符
p60 with语句
p64 js参数
(js无函数签名、不能重载、跟js数据类型也应该有关系)
p68 类型问题是否可以引出深浅拷贝这个概念(复制)
没有块级作用域
变量提升=>变量先声明
变量查询标识符从下往上找,故局部变量和外部变量同名,局部变量有效。另外还有局部变量与形参同名的情况
</code></pre>
]]>Javascript笔记Chanx ([email protected])
- Element-ui开发的坑https://chanx.tech/blog/element-ui-developmenthttps://chanx.tech/blog/element-ui-developmentelement开发的坑Wed, 22 Apr 2020 00:00:00 GMT<![CDATA[<p>记录一些常见不正确使用导致的错误</p>
<!-- more -->
<h2>[Vue][Element Warn][Form]model is required for validate to work!</h2>
<h3>1. 属性绑定错误(常见)</h3>
<p>确保使用<code>:model</code>,而不是<code>v-model</code></p>
<pre><code class="language-html"><el-form :model="form" ref="form" :rules="rules"
label-position="left" label-width="120px">
</el-form>
</code></pre>
<h3>2. ref重复</h3>
<p>检查是否在其他el-form中使用了相同的ref名,多个el-form组件ref命名要独立</p>
<h2>el-upload多选文件时触发多次上传钩子</h2>
<ol>
<li><p>上传钩子不做实际上传操作,另外设置上传按钮触发正式上传</p>
</li>
<li><p>对上传钩子进行聚合,多次触发合并一次。可见<a href="./%E5%89%8D%E7%AB%AF%E5%87%BD%E6%95%B0%E6%88%96%E8%AF%B7%E6%B1%82%E7%9A%84%E8%81%9A%E5%90%88.md">前端函数或请求的聚合</a>的思路</p>
</li>
</ol>
<h2>Todo</h2>
<pre><code><el-table-column>
<templete>
****
</templete>
</el-table-column>
自定义列时要加templete
<el-upload>的隐藏做法:官方思路和github两个实现方法
多选表格的状态保留:单页和多页
vue-quill-editor空格问题white-space
template or render function not defined 重新run dev
深浅拷贝
动态路由
</code></pre>
]]>ElementVue前端Chanx ([email protected])
- 从C到C++https://chanx.tech/blog/from-c-to-cpphttps://chanx.tech/blog/from-c-to-cppC++对C语言的扩展与改进,包括新增关键字、注释方式、类型转换等特性介绍Thu, 20 Feb 2020 00:00:00 GMT<![CDATA[<p><strong>本笔记根据《C/C++语言程序设计》龚尚福版以及个人理解总结修改,不能保证内容完全准确,仅供参考。</strong></p>
<p>第一次修改(2019年8月4日):C++对于C的一般扩充</p>
<p>第二次修改(2019年8月5日):C++中的函数</p>
<p>第三次修改(咕咕咕):C++的输入与输出流</p>
<!-- more -->
<hr>
<p>[前言]C++从C发展而来,它继承了C语言的优点,并引入了面向对象的概念,同时也增加了一些非面向对象的新特性,这些新特性使得C+ +比C更简洁、更安全。</p>
<h2>一、C++对于C的一般扩充</h2>
<p>[指南]主要介绍了C++ 对C的非面向对象特性的扩展,包括新增的关键字、注释、类型转换、灵活的变量声明、const、struct 、作用域分辨符、 C++ 的动态内存分配、引用、主函数、函数定义、内置函数、缺省参数值、重载函数、 C++的输入输出流等。</p>
<h3>1.新增的关键字</h3>
<p>C++在C语言的基础上增加了许多关键字 </p>
<blockquote>
<p>下面是一些常用的关键字:<br>asm catch class delete friend inline new operator private protected public template virtual try using</p>
</blockquote>
<h3>2. 注释方式</h3>
<p>C语言使用/* 和 */作为注释分界的符号,而C++ 除了保留这种原有的注释模式,还新加了单行注释符号 //</p>
<blockquote>
<p>对于这个问题我也感觉到十分诧异,在平时注释的时候并没有发现C不支持单行注释。经过百度后,发现C99才支持单行注释。所以之前的TC是不支持单行注释的。完美解释了我实验课用TC注释一直出错(滑稽)</p>
</blockquote>
<h3>3. 类型转换</h3>
<p>C++ 支持两种不同的类型转换方式</p>
<pre><code class="language-cpp">int i=0;
long n=(long)i; //C的类型风格
long m=long(i); //C++的类型风格
</code></pre>
<p>C++的这种新风格更像是调用了函数,可读性更好</p>
<h3>4.灵活的变量声明</h3>
<pre><code class="language-cpp">#include <stdio.h>
int main()
{
printf("100分!");//不使用变量
int score;//使用变量
score = 100;
printf("是%d分!",score);
return 0;
}
</code></pre>
<p>这段代码会因为score在第一个printf后报错</p>
<p>问题根源:编译器问题——C89和C99</p>
<p>C89规定,在任何执行语句之前,在块的开头声明所有局部变量。</p>
<p>在C99以及C++中则没有这个限制,即在首次使用之前,可在块的任何位置都可以声明变量。</p>
<blockquote>
<p>我在学习C的过程中接触的应该是C99标准。看到这里我感觉到了C99的强大...还有我是不是要换本书看。讲得太多我好像有点混乱(枯了)</p>
</blockquote>
<h3>5.const</h3>
<h4>1. const定义常量</h4>
<p>使用const定义常量可以避免define引起的歧义</p>
<pre><code class="language-cpp">#include <iostream>
int main()
{
int a=1;
#define T1 a+a
#define T2 T1-T1
cout<<"T2 is "<<T2<<endl;
return 0;
}
</code></pre>
<p>输出的是2而不是0</p>
<pre><code class="language-cpp">#include <iostream>
int main()
{
int a=1;
const T1 a+a
const T2 T1-T1
cout<<"T2 is "<<T2<<endl;
return 0;
}
</code></pre>
<p>输出结果为0</p>
<h4>2. const 修饰指针</h4>
<p><strong>首先存在 int b=500;</strong></p>
<p>a是一个指向常量的普通指针变量,不是常指针。</p>
<pre><code class="language-cpp">const int *a=&b;
int const *a=&b;
//所以a的值是可以改变的
int c=3; //√
a=&c;
//但是对于a指向的内容不能改变
*a=3; //×
</code></pre>
<p>指针本身是常量(常指针)而指针所指向的内容不是常量</p>
<pre><code class="language-cpp">int* const a=&b;
//不能对指针本身进行更改操作
a++; //×
//它指向的数据可以改变
*a=3; //√
</code></pre>
<p>指针本身和它指向的内容都是常量</p>
<pre><code class="language-cpp">const int* const a=&b;
//指针本身和指向内容都不能修改
a++; //×
*a=3; //×
</code></pre>
<h4>3. const 在函数中的应用</h4>
<p>const 还常用于限定函数的参数和返回值。函数参数如果使用const 声明,则在该函数中不能修改参数。例如:</p>
<pre><code class="language-cpp">float fun(const float x)
{
x=x*x; //非法
return x;
}
</code></pre>
<p>如果函数返回基本类型,则用const声明返回值并没有特别的意义,但是如果函数返回一个指针或引用(引用的概念稍后在第九节会讲),则使用const 声明返回值表示调用函数时不能用返回值来改变返回值所指向或引用的变量。例如:</p>
<pre><code class="language-cpp">const int *func()
{
static int x=1;
++x;
return &x;
}
int main()
{
int y;
y=*func(); //合法:将值x传给y
*func()=2; //非法:不能改变常量
return 0;
}
</code></pre>
<p>在这个例子中,函数func()的返回值使用了const声明,因此调用func() 函数时不能通过函数返回值来改变它所指向的变量x的值。</p>
<h3>6. struct</h3>
<p>在C++中,struct后的标识符可直接作为结构体类型名使用,所以定义变量比在C中更加直观。代码如下:</p>
<p>C语言</p>
<pre><code class="language-c">struct point
{
int x;
int y;
};
struct point p;
</code></pre>
<p>C++</p>
<pre><code class="language-cpp">struct point
{
int x;
int y;
};
point p;
</code></pre>
<p>对于union,也可以照此使用。以上两种方法在C++都适用。 </p>
<blockquote>
<p>不过我一直在用typedof的写法,现在看起来跟C++的写法差不多</p>
<pre><code class="language-cpp">typedof struct Point
{
int x;
int y;
}point;
point p;
</code></pre>
</blockquote>
<h3>7. 作用域分辨运算符::</h3>
<p>作用域分辨预算符“::”用于访问当前作用域中被隐藏的数据项。如果有两个重名的变量,一个是全局的,一个是局部的,那么局部变量作用域内具有优先权,同名的全局变量被隐藏无法被访问到。</p>
<pre><code class="language-cpp">#include <iostream>
int a=10; //全局变量
int main()
{
int a; //局部变量
a=25;
cout<<a<<endl;
return 0;
}
</code></pre>
<p>程序运行结果为25,说明了局部变量的较高优先权</p>
<p>如果希望在局部变量作用域内使用同名的全局变量,则可以在该变量前加上“::”,此时“::a”代表全局变量。</p>
<pre><code class="language-cpp">#include <iostream>
int a; //全局变量
int main()
{
int a; //局部变量
a=25;
::a=10;
cout<<"local is "<<a<<endl;
cout<<"global is "<<::a<<endl;
return 0;
}
</code></pre>
<p>程序运行结果为local is 25/global is 10</p>
<p>需要注意的是:作用域分辨符只能用来访问全局变量,不能用来访问一个在语句块外声明的同名局部变量。例如,下面代码是错误的:</p>
<pre><code class="language-cpp">int main()
{
int a; //语句块外局部变量
{
int a=25; //语句块内局部变量
::a=30; //非法
...
}
return 0;
}
</code></pre>
<h3>8. C++的动态内存分配</h3>
<p>在C语言中,动态分配内存是通过调用malloc()和free()来实现的</p>
<pre><code class="language-c">#include <stdio.h>
#include <malloc.h> //这里常用stdlib.h 它包含了malloc.h
int main()
{
int *p;
p=(int*)malloc(sizeof(int));
*p=8;
printf("%d",*p);
free(p);
return 0;
}
</code></pre>
<p>C++进行动态内存分配使用的是<strong>new</strong>和<strong>delete</strong></p>
<p>运算符new用于内存分配的使用形式为: </p>
<p>==<strong>指针变量 = new <数据类型>[<整形表达式>];</strong>==</p>
<p>其中<数据类型>可以是基本数据类型、结构等,它表示要分配与<数据类型>相匹配的内存空间;<整形表达式>表示要分配内存单元的个数,默认值为1,可以省略。new运算符返回分配内存单元的起始地址,因此需要把该返回值赋值给一个指针变量。如果当前内存没有足够的空间可以分配,则new运算符返回NULL,并抛出一个运行异常。所以在进行动态内存分配的时候需要检验分配是否成功。</p>
<p>运算符delete用于释放new分配的存储空间,它的使用形式为:</p>
<p>==<strong>delete <指针变量>;</strong>==</p>
<p>以下是C++中使用新方法进行动态内存分配的例子:</p>
<pre><code class="language-cpp">#include <iostream>
int main()
{
int *p=new int; //为指针p分配空间
*p=10;
cout<<*p<<endl;
delete p; //为指针p释放空间
return 0;
}
</code></pre>
<p>我们可以看到这种新方法中分配内存时不需要显式地计算int所占用地存储空间大小。</p>
<p><strong>new和delete的一些说明:</strong></p>
<ol>
<li>使用new可以为数组分配存储空间,但是需要在类型名后缀上数组的大小。为多维数组分配空间时需要给出每一维的大小,其中第一维的值可以是任何合法的整型表达式。</li>
</ol>
<pre><code class="language-cpp">int *p=new int[10];
int *p=new int[2][3][4];
int i=10;
int *p=new int[i][3][4];
</code></pre>
<ol start="2">
<li>new可以在为简单变量分配内存的同时进行初始化,但是不能对数组进行初始化。</li>
</ol>
<pre><code class="language-cpp">int *p=new int(99);
//分配了一个整形内存空间并赋值99
</code></pre>
<p>3.释放动态分配的数组时可用如下格式:</p>
<pre><code class="language-cpp">delete []p;
</code></pre>
<p>4.建议在使用new分配动态内存时进行检查,避免分配失败引起的程序错误。</p>
<h3>9. 引用</h3>
<p>引用的定义格式为</p>
<p>==<strong>数据类型 &变量名 = 初始值</strong>==</p>
<p>引用是一种能够自动间接引用的<strong>指针</strong>。自动间接引用就是不必使用间接引用运算符“*”就可以得到一个引用值,即指针所指向变量的值。++<strong>换句话说,引用就是某一变量的一个别名,对引用的操作就是对变量本身的操作。</strong>++</p>
<p>使用规则:</p>
<ol>
<li>定义引用时必须立即初始化</li>
</ol>
<pre><code class="language-cpp">int i=5;
int &j; //错误,没有立即初始化
j=i;
</code></pre>
<ol start="2">
<li>引用不可重新赋值</li>
</ol>
<pre><code class="language-cpp">int i=5;
int k=10;
int &j=i;
j=&k; //错误,重新赋值
</code></pre>
<p>3.引用不同于普通变量。以下声明是非法的:</p>
<pre><code class="language-cpp">int &b[3]; //不能建立引用数组
int &*p; //不能建立指向引用的指针
int &&r; //不能建立指向引用的引用
</code></pre>
<p>4.当使用&运算符取一个引用的地址时,其值为所引用变量的地址。</p>
<pre><code class="language-cpp">int num=50;
int &ref=num;
int *p=&ref;
//p中保存的是变量num的地址
</code></pre>
<p>引用作为一般变量几乎没什么意义,最大用处是作为函数形参。</p>
<p>通过两个例子比较能比较清楚地理解引用的意义</p>
<pre><code class="language-cpp">//这种方法是地址传递
#include <iostream>
void swap(int *m,int *n)
{
int t;
t=*m;
*m=*n;
*n=t;
}
int main()
{
int a=5,b=10;
cout<<"a= "<<a<<"b= "<<b<<endl;
swap(&a,&b);
cout<<"a= "<<a<<"b= "<<b<<endl;
return 0;
}
</code></pre>
<pre><code class="language-cpp">//这种方法是通过引用传递
#include <iostream>
void swap(int &m,int &n)
{
int t;
t=m;
m=n;
n=t;
}
int main()
{
int a=5,b=10;
cout<<"a= "<<a<<"b= "<<b<<endl;
swap(a,b);
cout<<"a= "<<a<<"b= "<<b<<endl;
return 0;
}
</code></pre>
<p>两种方法是等效的,区别就在引用不需要间接引用符“*”。</p>
<hr>
<h2>二、C++中的函数</h2>
<h3>1. 主函数</h3>
<p>C中对于main()函数的格式并无特殊规定,因为C通常不关心返回何种状态给操作系统。</p>
<p>然而,C++要求main()函数匹配下面两种原型之一:</p>
<pre><code class="language-cpp">void main()//无参数,无返回类型
int main(int argc,char *argv[]) //带参数,有返回类型,参数可省略
</code></pre>
<p>第二种写法中,形参argc是命令行总的参数个数,argv的元素个数即为argc,其中第0个为可执行文件名,后面是执行所带的参数。</p>
<pre><code>例如,test.exe在命令行执行:
C:\>test a.c b.c
则argc=3 argv[0]="test" argv[1]="a.c" argv[2]="b.c"
</code></pre>
<p>如果main()函数前不加返回类型则等价于int main()</p>
<h3>2. 函数定义</h3>
<p>C++函数定义中的参数说明必须放在函数名后面的括号内,不可将函数的参数说明放在函数说明部分与函数体之间。</p>
<pre><code class="language-c">void fun(a)
int a;
{ }
</code></pre>
<p>但是在C中,这种方法是允许的。</p>
<p><strong>学C的时候完全不知道有这种写法,个人觉得这种写法十分毒瘤,切勿使用。</strong></p>
<h3>3. 内置函数</h3>
<p>函数调用导致了一定数量的额外开销,如参数入栈出栈等。当函数定义由inline开头时,表明此函数为内置函数。编译时,可使用函数体中的代码来替代函数调用表达式,从而完成与函数调用相同的功能。例如:</p>
<pre><code class="language-cpp">inline int sum(int a,int b)
{
return a+b;
}
</code></pre>
<p>说明:(1)内置函数必须在它被调用之前被定义。
(2)若内置函数较长且调用频繁,编译后程序会加长许多。所以通常只有较短的函数定义为内置函数。</p>
<blockquote>
<p>这个内置函数感觉有点像define。函数体太长的话,每个调用的地方都会被替换成一个函数体,相当于多了一段一模一样的代码...</p>
</blockquote>
<h3>4. 缺省函数值</h3>
<p>C++对于C的改进之一就是可以为函数定义缺省参数值。</p>
<p>当函数调用时,编译器按从左向右的顺序将实参和形参结合,若未指定足够的实参,则编译器按顺序用函数原型的缺省值来补足缺少的实参。</p>
<pre><code class="language-cpp">int fun(int x=5,int y=10);
fun(1,2); //x=1,y=2
fun(1); //x=1,y=10
fun(); //x=5,y=10
fun(,5) //错误
</code></pre>
<p>最后一个调用错误的原因:一个函数可以有多个缺省值,但是缺省参数值必须连续放在后面。不允许出现某个参数值省略后,后一个参数有指定值。</p>
<blockquote>
<p>如果缺省值不是连续放在后面,按照从左至右结合可能会出现混乱。但是我有疑惑是,如果我缺省的时候把逗号补全不就让实参形参准确结合了吗?这样fun(,5)就可以算是一种正确的做法,是否还有另外的因素使得不能这样做?</p>
</blockquote>
<h3>5. 重载函数</h3>
<p>在C语言中,函数名必须是唯一的。也就是说,不允许出现同名的函数。如果我要编写一个求不同数据类型的三次方函数,我需要编写三个不同名的函数,同时要在名字标注数据类型的特点。调用时,尽管三个函数的功能相同,我还是需要<strong>手动</strong>为相应的数据类型调用相应的函数。例如:</p>
<pre><code class="language-cpp">Icube(int i); //求int的三次方
Fcube(float i); //求float的三次方
Dcube(double i); //求double的三次方
</code></pre>
<p>而在C++中,我们可以重载函数。只要函数参数类型不同或者参数的个数不同,两个或两个以上的函数就可以使用相同的函数名。==一般而言,重载函数应该实现相同的功能。==所以这能干什么呢?我们在算不同数据类型的三次方时可以调用同一个名字的函数cube(),而它会<strong>自动</strong>为相应的数据类型调用相应的重载函数。</p>
<pre><code class="language-cpp">#include <iostream>
int cube(int i){return i*i*i;}
float cube(float f){return f*f*f;}
double cube(double d){return d*d*d;}
int main()
{
int i=2;
float f=3.4;
double d=5.678;
cout<<i<<"*"<<i<<"*"<<i<<"="<<cube(i)<<endl;
cout<<f<<"*"<<f<<"*"<<f<<"="<<cube(f)<<endl;
cout<<d<<"*"<<d<<"*"<<d<<"="<<cube(d)<<endl;
return 0;
}
</code></pre>
<p>注意:重载函数需在参数个数或类型上有所不同,就算返回类型不同,编译程序也不知道调用哪一个重载函数。</p>
<pre><code>这种重载函数是错误的。
int fun(int x,int y);
double fun(int x,int y)
</code></pre>
<p>特例:同参数、同参数表的const和非const成员函数可以重载</p>
<pre><code class="language-cpp">int myclass::fun(int a,int b);
int myclass::fun(int a,int b) const;
</code></pre>
<p>关于成员函数会在类和对象中学到。</p>
<h2>三. C++的输入与输出流</h2>
<h3>1. C++的流式结构</h3>
<h3>2. 格式化I/O</h3>
]]>C++C其他Chanx ([email protected])
- [Maven] Maven基本知识https://chanx.tech/blog/maven-basicshttps://chanx.tech/blog/maven-basics[Maven] Maven基本知识Wed, 05 Feb 2020 00:00:00 GMT<![CDATA[<p><strong>Maven-tomcat插件好像跑不了9... 直接部署tomcat9启动即可</strong></p>
<p>本文记录Maven的基本概念和常用指令</p>
<!--more-->
<h2>Maven仓库概念</h2>
<p>就是能自动帮你引入jar包,减少不必要的操作</p>
<p>其中仓库有<strong>本地仓库,远程仓库,中央仓库</strong></p>
<p>本地仓库:本地的jar包(本机)</p>
<p>远程仓库:本地上传的或者联网缓存的,私服...(内网共用 <em>实际上并不一定是内网只是为了方便理解</em>)</p>
<p>中央仓库:集中存储的资源站(外网共用)</p>
<p>若实际开发中maven无法从三个仓库中找到相应jar包的坐标,则报错</p>
<h2>Maven工程</h2>
<pre><code>src/main/java 核心代码
src/main/resources 配置文件
src/test/java 测试代码
src/test/resources 测试配置
src/main/webapp 页面资源如js,css,image
</code></pre>
<h2>Maven常用命令</h2>
<pre><code class="language-html">compile 编译命令
将src/main/java下文件编译为class文件输出到target目录下
test 测试命令
clean 删除target文件夹
package 默认打包成war包放置于target
install 将maven编译打包放于本地仓库
</code></pre>
]]>Maven后端Chanx ([email protected])
- Java的MOOC编程练习https://chanx.tech/blog/java-moochttps://chanx.tech/blog/java-mooc浙江大学Java面向对象程序设计MOOC课程的编程练习题解Mon, 09 Dec 2019 00:00:00 GMT<![CDATA[<blockquote>
<p>课程地址:<a href="https://www.icourse163.org/course/ZJU-1001542001/">点击传送</a>
没有 Submit 给系统判题,所以代码不能保证完全正确。</p>
</blockquote>
<!-- more -->
<h2><strong>第一周:类与对象</strong></h2>
<p><strong>题目:分数</strong></p>
<p>设计一个表示分数的类Fraction。这个类用两个int类型的变量分别表示分子和分母。</p>
<p>这个类的构造函数是:</p>
<p>Fraction(int a, int b)</p>
<blockquote>
<p>构造一个a/b的分数。</p>
</blockquote>
<p><strong>这个类要提供以下的功能:</strong></p>
<p>double toDouble();</p>
<blockquote>
<p>将分数转换为double</p>
</blockquote>
<p>Fraction plus(Fraction r);</p>
<blockquote>
<p>将自己的分数和r的分数相加,产生一个新的Fraction的对象。注意小学四年级学过两个分数如何相加的哈。</p>
</blockquote>
<p>Fraction multiply(Fraction r);</p>
<blockquote>
<p>将自己的分数和r的分数相乘,产生一个新的Fraction的对象。</p>
</blockquote>
<p>void print();</p>
<blockquote>
<p>将自己以"分子/分母"的形式输出到标准输出,并带有回车换行。如果分数是1/1,应该输出1。当分子大于分母时,不需要提出整数部分,即31/30是一个正确的输出。</p>
</blockquote>
<p>==注意,在创建和做完运算后应该化简分数为最简形式。如2/4应该被化简为1/2==。</p>
<p>你写的类要和以下的代码放在一起,并请勿修改这个代码:</p>
<pre><code class="language-java">import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
Fraction a = new Fraction(in.nextInt(), in.nextInt());
Fraction b = new Fraction(in.nextInt(),in.nextInt());
a.print();
b.print();
a.plus(b).print();
a.multiply(b).plus(new Fraction(5,6)).print();
a.print();
b.print();
in.close();
}
}
</code></pre>
<p>注意,你的类的定义应该这样开始:</p>
<pre><code>class Fraction {
</code></pre>
<p>也就是说,在你的类的class前面不要有public。</p>
<p><strong>输入格式:</strong></p>
<p>程序运行时会得到四个数字,分别构成两个分数,依次是分子和分母。</p>
<p><strong>输出格式:</strong></p>
<p>输出一些算式。这些输入和输出都是由Main类的代码完成的,你的代码不要做输入和输出。</p>
<p><strong>输入样例:</strong></p>
<pre><code>2 4 1 3
</code></pre>
<p><strong>输出样例:</strong></p>
<pre><code>1/2
1/3
5/6
1
1/2
1/3
</code></pre>
<h4>代码</h4>
<pre><code class="language-java">class Fraction
{
private int a;
private int b;
Fraction(int a, int b)
{
this.a=a;
this.b=b;
toeasy();
}
//构造一个a/b的分数。
void toeasy()
{
int c = this.a;
int d = this.b;
int t=1;
while(d>0)
{
t=c%d;
c=d;
d=t;
}
this.a = this.a/c;
this.b = this.b/c;
}
//化简
double toDouble()
{
double ans = (double)a/(double)b;
return ans;
}
//将分数转换为double
Fraction plus(Fraction r)
{
int up = this.a*r.b+r.a*this.b;
int down = this.b*r.b;
Fraction ans = new Fraction(up,down);
ans.toeasy();
return ans;
}
//将自己的分数和r的分数相加,产生一个新的Fraction的对象。注意小学四年级学过两个分数如何相加的哈。
Fraction multiply(Fraction r)
{
Fraction ans = new Fraction(this.a*r.a,this.b*r.b);
ans.toeasy();
return ans;
}
//将自己的分数和r的分数相乘,产生一个新的Fraction的对象。
void print()
{
if(this.a==this.b)
System.out.println(1);
else if(this.a==0)
System.out.println(0);
else System.out.println(this.a+"/"+this.b);
}
// 将自己以"分子/分母"的形式输出到标准输出,并带有回车换行。如果分数是1/1,应该输出1。当分子大于分母时,不需要提出整数部分,即31/30是一个正确的输出。
//注意,在创建和做完运算后应该化简分数为最简形式。如2/4应该被化简为1/2。
}
</code></pre>
<hr>
<h2><strong>第二周:对象交互</strong></h2>
<p><strong>题目:有秒计时的数字时钟</strong></p>
<p>这一周的编程题是需要你在课程所给的时钟程序的基础上修改而成。但是我们并不直接给你时钟程序的代码,请根据视频自己输入时钟程序的 Display 和 Clock 类的代码,然后来做这个题目。</p>
<p>我们需要给时钟程序加上一个表示秒的 Display,然后为 Clock 增加以下 public 的成员函数:</p>
<p>public Clock(int hour, int minute, int second);</p>
<blockquote>
<pre><code>用 hour, minute 和 second 初始化时间。
</code></pre>
</blockquote>
<p>public void tick();</p>
<blockquote>
<pre><code>"嘀嗒" 一下,时间走 1 秒。
</code></pre>
</blockquote>
<p>public String toString();</p>
<blockquote>
<p> 返回一个 String 的值,以 "hh:mm:ss"的形式表示当前时间。这里每个数值都占据两位,不足两位时补 0。如 "00:01:22"。注意其中的冒号是西文的,不是中文的。</p>
</blockquote>
<p>提示:String.format () 可以用和 printf 一样的方式来格式化一个字符串。</p>
<p>另外写一个 Main 类,它的 main 函数为下面的样子,注意,必须原封不动地作为 Main 的 main 函数:</p>
<pre><code class="language-java">public static void main(String[] args) {
java.util.Scanner in = new java.util.Scanner(System.in);
Clock clock = new Clock(in.nextInt(), in.nextInt(), in.nextInt());
clock.tick();
System.out.println(clock);
in.close();
}
</code></pre>
<p>注意!在提交的时候,把 Main、Clock 和 Display 三个类的代码合并在一起,其中 Main 类是 public 的,而 Clock 和 Display 类是没有修饰符的。另外,千万注意第一行不能有 package 语句。</p>
<h4>代码</h4>
<pre><code class="language-java">public class Main{
public static void main(String[] args) {
java.util.Scanner in = new java.util.Scanner(System.in);
Clock clock = new Clock(in.nextInt(), in.nextInt(), in.nextInt());
clock.tick();
System.out.println(clock);
in.close();
}
}
class Display {
private int value = 0;
private int limit = 0;
Display(int limit){
this.limit = limit;
}
int getvalue() {
return this.value;
}
void increase() {
this.value++;
if(this.value == limit) {
this.value = 0;
}
}
void setvalue(int value) {
this.value = value;
}
}
class Clock{
Display h = new Display(24);
Display m = new Display(60);
Display s = new Display(60);
Clock(int hour,int minute,int second){
h.setvalue(hour);
m.setvalue(minute);
s.setvalue(second);
}
public void tick() {
s.increase();
if(s.getvalue()==0){
m.increase();
if(m.getvalue()==0)
h.increase();
}
}
public String toString() {
String str = String.format("%02d:%02d:%02d", h.getvalue(),m.getvalue(),s.getvalue());
return str;
}
}
</code></pre>
<hr>
<h2><strong>第三周:对象容器</strong></h2>
<p><strong>题目:查找里程</strong></p>
<p><strong>输入格式:</strong></p>
<p>首先,你会读到若干个城市的名字。每个名字都只是一个英文单词,中间不含空格或其他符号。当读到名字为 "###"(三个 #号)时,表示城市名字输入结束,## 并不是一个城市的名字。如果记读到的城市名字的数量为 n。</p>
<p>然后,你会读到 nxn 的一个整数矩阵。第一行的每一个数字,表示上述城市名单中第一个城市依次到另一个城市之间的里程。表中同一个城市之间的里程为 0。</p>
<p>最后,你会读到两个城市的名字。</p>
<p><strong>输出格式:</strong></p>
<p>输出这两个城市之间的距离。</p>
<p><strong>输入样例</strong>:</p>
<pre><code>Hagzou Hugzou Jigxng ###
0 1108 708
1108 0 994
708 994 0
Hagzou Jigxng
</code></pre>
<p><strong>输出样例</strong>:</p>
<pre><code>708
</code></pre>
<h4>代码</h4>
<pre><code class="language-java">import java.util.Scanner;
import java.util.ArrayList;
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
ArrayList<String> city = new ArrayList<String>();
//顺序容器存城市的名称
while(true) {
String temp = in.next();
if(temp.equals("###"))
break;
else city.add(temp);
}
int size = city.size();
//嵌套哈希表存二维数据表
HashMap<String,HashMap<String,Integer>> data = new HashMap();
int i=0,j=0;
for(i=0;i<size;i++){
String name = city.get(i);
for(j=0;j<size;j++){
int distance = in.nextInt();
if(data.get(name)== null) {
data.put(name,new HashMap());
}
else data.get(name).put(city.get(j),distance);
}
}
//读取城市并输出结果
String first = in.next();
String second = in.next();
System.out.println(data.get(first).get(second));
}
}
</code></pre>
<hr>
<h2><strong>第四周:继承与多态</strong></h2>
]]>Java练习面向对象后端Chanx ([email protected])