<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet type="text/xsl" href="/linen-theme/css/rss-en.xsl"?>
<rss version="2.0">
<channel>
  <title>Crispin&#39;s Blog</title>
  <link>http://example.com</link>
  <description></description>
  <lastBuildDate>Thu, 16 Apr 2026 12:21:01 GMT</lastBuildDate>
  <image>
      <url>http://example.comundefined</url>
      <title>Crispin&#39;s Blog</title>
      <link>http://example.com</link>
    </image>
  
    <item>
      <title>春节江苏三城记：苏州、淮安、常州</title>
      <link>http://example.com/2026/02/05/spring-festival-jiangsu-suzhou-huaian-changzhou/</link>
      <guid>http://example.com/2026/02/05/spring-festival-jiangsu-suzhou-huaian-changzhou/</guid>
      <pubDate>Feb 5, 2026</pubDate>
      <description><![CDATA[<p>春节七天，开车陪着家人在江苏转了三座城市。苏州、淮安、常州，三座城，三种气质，却都带着过年特有的那种松弛。</p>
<hr>
<h2 id="苏州：园林里的年味"><a href="#苏州：园林里的年味" class="headerlink" title="苏州：园林里的年味"></a>苏州：园林里的年味</h2><p>苏州是第一站。</p>
<p>春节期间的苏州，游客比平时少了不少，反而多了一份难得的安静。拙政园的游廊上挂着红灯笼，倒映在水面里，晃晃悠悠的。</p>
<p>平江路是必去的。老街两侧的店铺大多还开着，卖桂花糕、苏式糖果、手工团扇。买了一盒玫瑰猪油年糕，甜而不腻，带回去当伴手礼刚好。</p>
<p>苏州的早饭值得专门说一说。找了一家老苏州面馆，点了一碗枫镇大肉面。汤是白色的，用猪骨和酒酿吊出来的，鲜得很克制，不抢戏。面条细而有劲，大肉酥烂，筷子一碰就脱骨。这碗面，吃完之后整个上午都是满足的。</p>
<p>虎丘塔在春节期间有灯会，晚上亮起来，塔身的轮廓在夜色里很好看。没有挤进人群，就在远处的桥上看了一会儿，也够了。</p>
<hr>
<h2 id="淮安：一座被低估的城市"><a href="#淮安：一座被低估的城市" class="headerlink" title="淮安：一座被低估的城市"></a>淮安：一座被低估的城市</h2><p>淮安是意外之喜。</p>
<p>很多人去江苏，不会把淮安列进行程。但这座城市有一种别处没有的从容——不赶时髦，不刻意打造网红景点，就是安安静静地过自己的日子。</p>
<p>河下古镇是淮安最值得待的地方。青砖小巷，石板路，巷子里偶尔传来鞭炮声，年味比苏州更浓。镇上有一家做淮扬菜的小馆子，软兜长鱼、平桥豆腐、文思豆腐，每一道都是功夫菜。软兜长鱼是用鳝鱼做的，口感嫩滑，酱汁咸鲜，配着白米饭能吃两碗。</p>
<p>淮安是周恩来的故乡，周恩来纪念馆在春节期间人不多，安静地走了一圈，看了很多历史照片，心里有些沉。<br><img loading="lazy" src="/images/202603/IMG_7733.png" alt="周恩来纪念馆" title="周恩来纪念馆"></p>
<p>运河边的夜晚很适合散步。灯光打在水面上，对岸是老城区的轮廓，没有什么特别的景点，就是走走，也很好。</p>
<p>西游记主题公园是带小孩的好去处。春节期间有特别的表演，孙悟空、猪八戒、沙僧都穿着节日的服装，表演得很卖力。孩子们看得目不转睛，大人也被逗得哈哈大笑。</p>
<p><img loading="lazy" src="/images/202603/IMG_7773.png" alt="西游记主题公园" title="西游记主题公园"></p>
<hr>
<h2 id="常州：恐龙和青果巷"><a href="#常州：恐龙和青果巷" class="headerlink" title="常州：恐龙和青果巷"></a>常州：恐龙和青果巷</h2><p>常州是最后一站，也是节奏最轻松的一站。</p>
<p>中华恐龙园是带小孩来的好地方。春节期间有特别的灯光秀，恐龙模型在彩灯里显得格外有气势。孩子们跑来跑去，大人跟在后面，累但是开心。</p>
<p>青果巷是常州的老城区，修缮得很好，没有过度商业化。巷子里有几家老字号，卖大麻糕、芝麻糖、萝卜干。大麻糕是常州特产，外皮酥脆，内馅咸香，趁热吃最好，凉了就差了一截。</p>
<p>天宁寺在春节期间香火很旺。寺庙不大，但香客很多，烟雾缭绕，钟声厚重。在这里待了半个小时，什么都没想，就是站着，听钟声。<br><img loading="lazy" src="/images/202603/IMG_7789.png" alt="天宁寺" title="天宁寺"></p>
<hr>
<h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>春节出行，前两天的行程是最舒适的，因为人少，更容易让人放松下来。</p>
<p>三座城市，各有各的好。如果只能选一座再去一次，我会选淮安。不是因为它最出名，而是因为它最让我意外。</p>
]]></description>
    </item>
    <item>
      <title>国庆·九宫山</title>
      <link>http://example.com/2025/10/11/jiugong-mountain-national-day/</link>
      <guid>http://example.com/2025/10/11/jiugong-mountain-national-day/</guid>
      <pubDate>Oct 11, 2025</pubDate>
      <description><![CDATA[<p>国庆的假期就算过完了。以前总觉得八天很长，真的放下来的时候才发现，也就那么一眨眼。</p>
<p>节前规划了很久的行程，最终因为怕人多，怕堵在路上停留在了纸上。</p>
<p>想起以前的自己喜欢热闹，哪里人多就往哪里跑。</p>
<p>年纪大了以后就真的变了，喜欢清净，喜欢人少的地方，最后选择了去九宫山上待了两天。</p>
<p>从武汉出发到九宫山差不多三个小时，盘山公路就占了一个小时。</p>
<p>山路其实没那么难跑，无外乎多给点油，开慢一点。但是不断想超车、占道的司机让上山之路变得如履薄冰。</p>
<p>下午四点多到达，山上的风一吹，心里那口闷气也松了。老婆说：”人好少啊，这不像国庆的景区。”我笑着说：”看来来对了。”或许是那一段山路劝退了大多数人，或许是大家来过一次，就不愿意再折腾第二回。</p>
<p>我们在山顶的小湖边走了一圈。日出和日落，只选一个。我果断放弃了日出——4点半起床，这种事太反人性。反而是日落刚刚好，光线正柔，风也温柔。</p>
<p>随后坐摆渡车来到山顶。山顶的天，层层叠叠。我们坐在石阶上，背靠着背，她靠在我肩上，我拿出手机拍了一张。</p>
<p>“有颗流星，你拍到了吗？”她突然问。我低头一看，真有。流星从夕阳的头顶划过。</p>
<p>那一刻，脑海里突然回响起那首歌：”总会梦见云层之上飞过子午线，分不起是黑夜还是白天。”</p>
<p>假期就这样结束了。没出远门，也没拍什么大片，但我知道，这次的”没去哪里”，好像比去很多地方更让我安静。</p>
<p><img loading="lazy" src="/images/202510/18903d7e026398ee6938e25480791df1.png" alt="jiugong-mountain" title="九宫山"></p>
]]></description>
    </item>
    <item>
      <title>从零搭建金融贷款低代码前端框架：xxx-editor 实践总结</title>
      <link>http://example.com/2024/12/16/quark-editor-monorepo-fintech/</link>
      <guid>http://example.com/2024/12/16/quark-editor-monorepo-fintech/</guid>
      <pubDate>Dec 16, 2024</pubDate>
      <description><![CDATA[<p>最近有点忙，在做一个面向金融贷款业务的前端框架项目 <strong>xxx-editor</strong>，整体采用 pnpm Monorepo 架构，涵盖低代码编辑器、多个 Next.js 子应用和一套完整的组件库。这篇文章记录一下整体架构设计和几个值得分享的技术点。</p>
<h2 id="项目背景"><a href="#项目背景" class="headerlink" title="项目背景"></a>项目背景</h2><p>金融贷款类 H5 应用有几个典型特点：</p>
<ol>
<li><strong>页面结构高度相似</strong>：注册、登录、申请步骤、还款等页面在不同产品线之间大量复用</li>
<li><strong>多地区多品牌</strong>：同一套业务逻辑需要支持菲律宾、香港等不同市场，UI 和文案各有差异</li>
<li><strong>安全要求高</strong>：请求加密、活体检测、证件上传等功能必不可少</li>
</ol>
<p>基于这些特点，我们决定用 Monorepo 把所有子应用和共享包统一管理，并在此基础上做一个低代码编辑器，让运营和产品可以快速配置页面。</p>
<h2 id="Monorepo-结构设计"><a href="#Monorepo-结构设计" class="headerlink" title="Monorepo 结构设计"></a>Monorepo 结构设计</h2><pre><code>xxx-editor/
├── packages/
│   ├── core/        # 低代码编辑器核心（Designer + Renderer）
│   ├── widgets/     # 基础 UI 组件库（40+ 组件）
│   ├── templates/   # 页面级模板（Login、Register、MePage 等）
│   ├── components/  # 增强型表单组件
│   └── utils/       # 工具函数（HTTP、加密、格式化、缓存、i18n）
├── server/          # 主业务 Next.js 应用
├── ph-life/         # 菲律宾生活服务应用
├── ph-life-website/ # 菲律宾官网
└── max-cash-portal/ # Max Cash 多语言门户
</code></pre>
<p>包管理用的是 pnpm workspaces，<code>pnpm-workspace.yaml</code> 配置如下：</p>
<pre><code class="language-yaml">packages:
  - &quot;packages/*&quot;
  - &quot;server&quot;
  - &quot;ph-life&quot;
  - &quot;ph-life-website&quot;
  - &quot;max-cash-portal&quot;
</code></pre>
<p>各子应用通过 workspace 协议引用内部包，例如：</p>
<pre><code class="language-json">{
  &quot;dependencies&quot;: {
    &quot;@xxx/widgets&quot;: &quot;workspace:*&quot;,
    &quot;@xxx/utils&quot;: &quot;workspace:*&quot;
  }
}
</code></pre>
<p>这样改动组件库后，所有子应用都能实时感知，不需要发布 npm 包。</p>
<h2 id="HTTP-层的设计"><a href="#HTTP-层的设计" class="headerlink" title="HTTP 层的设计"></a>HTTP 层的设计</h2><p>HTTP 层是整个框架里最复杂的部分之一，封装在 <code>packages/utils/src/http/axios</code> 下。核心是一个 <code>VAxios</code> 类，支持：</p>
<ul>
<li><strong>请求&#x2F;响应拦截</strong>：统一处理 Token 注入、错误码映射</li>
<li><strong>请求取消</strong>：通过 <code>AxiosCanceler</code> 管理 pending 请求，页面切换时自动取消</li>
<li><strong>自动重试</strong>：<code>AxiosRetry</code> 对网络抖动场景做指数退避重试</li>
<li><strong>请求加密</strong>：敏感接口的请求体走 AES 加密</li>
</ul>
<pre><code class="language-ts">// 创建实例时注入 transform 钩子
const http = createAxios({
  transform: {
    beforeRequestHook(config, options) {
      // 拼接时间戳、加密 body 等
    },
    responseInterceptors(res) {
      // 统一解包 { code, data, msg }
    },
    responseInterceptorsCatch(error) {
      // checkStatus 映射错误码到提示文案
    },
  },
});
</code></pre>
<p>Token 刷新用的是经典的”请求队列”方案：刷新期间的所有请求挂起，刷新成功后统一重放。</p>
<h2 id="组件库的-Variant-模式"><a href="#组件库的-Variant-模式" class="headerlink" title="组件库的 Variant 模式"></a>组件库的 Variant 模式</h2><p><code>packages/widgets</code> 里的组件大量使用了 <strong>Variant（变体）</strong> 模式。以 <code>Card</code> 为例：</p>
<pre><code>Card/
├── Basic/          # 基础卡片
├── VariantOne/     # 贷款产品卡片
├── VariantSecond/  # 还款信息卡片
├── VariantRecord/  # 账单记录卡片
└── VariantSeventh/ # 推广活动卡片
</code></pre>
<p>每个 Variant 有独立的 <code>definition.ts</code>（props 类型定义）、<code>styled.ts</code>（样式）和主组件文件，互不干扰。这种结构的好处是：</p>
<ul>
<li>新增变体不影响已有变体，改动范围可控</li>
<li>Storybook 可以为每个变体单独写 story，方便 QA 验收</li>
<li>低代码编辑器可以把每个变体作为独立的”积木块”注册</li>
</ul>
<h2 id="低代码编辑器核心"><a href="#低代码编辑器核心" class="headerlink" title="低代码编辑器核心"></a>低代码编辑器核心</h2><p><code>packages/core</code> 实现了一个轻量的低代码运行时，主要由三部分组成：</p>
<ul>
<li><strong>Designer</strong>：拖拽画布，左侧 WidgetPanel 展示可用组件，右侧 SettingPanel 配置属性</li>
<li><strong>Renderer</strong>：根据 JSON Schema 渲染页面，通过 <code>WidgetListRender</code> 递归渲染组件树</li>
<li><strong>register</strong>：组件注册机制，将 widgets 中的组件映射到编辑器可识别的 key</li>
</ul>
<pre><code class="language-ts">// 注册一个组件
register({
  key: &quot;CardVariantOne&quot;,
  component: CardVariantOne,
  defaultProps: { title: &quot;贷款产品&quot;, amount: 0 },
});
</code></pre>
<p>JSON Schema 的结构大致如下：</p>
<pre><code class="language-json">{
  &quot;widgets&quot;: [
    {
      &quot;key&quot;: &quot;CardVariantOne&quot;,
      &quot;props&quot;: { &quot;title&quot;: &quot;快速贷&quot;, &quot;amount&quot;: 50000 }
    },
    {
      &quot;key&quot;: &quot;Button&quot;,
      &quot;props&quot;: { &quot;text&quot;: &quot;立即申请&quot;, &quot;type&quot;: &quot;primary&quot; }
    }
  ]
}
</code></pre>
<h2 id="加密工具的分层设计"><a href="#加密工具的分层设计" class="headerlink" title="加密工具的分层设计"></a>加密工具的分层设计</h2><p><code>packages/utils/src/encrypted</code> 里的加密工具做了比较清晰的分层：</p>
<pre><code>encrypted/
├── implForStorage.ts  # 工厂类，支持 AES / MD5 / SHA256 / SHA512 / Base64
├── standard.ts        # 对外暴露的标准 API
└── index.ts
</code></pre>
<p>工厂类用单例模式管理各加密实例，避免重复初始化：</p>
<pre><code class="language-ts">class AesEncryption {
  private static instance: AesEncryption;
  static getInstance(options: AesEncryptionOptions) {
    if (!this.instance) this.instance = new AesEncryption(options);
    return this.instance;
  }
  encrypt(value: string) {
    /* ... */
  }
  decrypt(value: string) {
    /* ... */
  }
}
</code></pre>
<p>对外只暴露 <code>encryptByAES</code>、<code>decryptByAES</code>、<code>encryptByMd5</code> 等函数，调用方不需要关心内部实现。</p>
<h2 id="多语言方案"><a href="#多语言方案" class="headerlink" title="多语言方案"></a>多语言方案</h2><p><code>max-cash-portal</code> 是一个多语言门户，用的是 Next.js App Router 的 <code>[lang]</code> 动态路由方案：</p>
<pre><code>app/
└── [lang]/
    ├── layout.tsx   # 根据 lang 加载对应字典
    └── page.tsx
</code></pre>
<p><code>middleware.ts</code> 负责检测用户语言并重定向：</p>
<pre><code class="language-ts">export function middleware(request: NextRequest) {
  const locale = getLocale(request); // 从 Accept-Language 或 cookie 读取
  return NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url));
}
</code></pre>
<p>字典文件放在 <code>dictionaries/</code> 下，按语言分文件，<code>generateStaticParams</code> 在构建时生成所有语言的静态页面。</p>
<h2 id="Storybook-组件文档"><a href="#Storybook-组件文档" class="headerlink" title="Storybook 组件文档"></a>Storybook 组件文档</h2><p><code>packages/stories</code> 是整个组件库的 Storybook 工程，独立作为 <code>@xxx/storybook</code> 包存在，运行在 6006 端口。<br>这也是我认为架构设计里比较成功的一点，Storybook 和组件库虽然紧密相关，但职责不同，分开管理更清晰并且方便 UI 和 QA 访问而不需要跑整个 Next.js 应用。</p>
<h3 id="配置要点"><a href="#配置要点" class="headerlink" title="配置要点"></a>配置要点</h3><p>Storybook 使用的是 <code>@storybook/react-webpack5</code>，在 <code>webpackFinal</code> 里配置了 workspace 包的路径别名，让 stories 可以直接 import 内部包的源码而不是构建产物：</p>
<pre><code class="language-js">// packages/.storybook/main.js
webpackFinal: async (config) =&gt; {
  config.resolve.alias = {
    &quot;@xxx/widgets&quot;: path.resolve(__dirname, &quot;../widgets/src&quot;),
    &quot;@xxx/utils&quot;: path.resolve(__dirname, &quot;../utils/src&quot;),
    &quot;@xxx/components&quot;: path.resolve(__dirname, &quot;../components/src&quot;),
  };
  return config;
};
</code></pre>
<p>这样改了组件源码后，Storybook 热更新可以直接生效，不需要先 build 包。</p>
<p>样式方面配置了完整的 Less &#x2F; Sass &#x2F; CSS Modules 支持，因为 widgets 里的组件混用了多种样式方案。</p>
<h3 id="Story-的写法"><a href="#Story-的写法" class="headerlink" title="Story 的写法"></a>Story 的写法</h3><p>项目里的 story 统一用 <code>StoryFn</code> 模板模式，以 <code>Guide</code> 组件为例：</p>
<pre><code class="language-tsx">const Template: StoryFn&lt;GuideProps&gt; = (args) =&gt; &lt;Guide {...args} /&gt;;

export const Default = Template.bind({});
Default.args = {
  title: &quot;Item List&quot;,
  items: [{ imageSrc: &quot;...&quot;, itemTitle: &quot;Item 1&quot;, description: &quot;...&quot; }],
};

export const CustomStyles = Template.bind({});
CustomStyles.args = {
  // 通过 cssClassnames + cssStyles 注入自定义样式
  cssStyles: `.xxx-guide img { width: 32px; }`,
};
</code></pre>
<p>每个组件至少有 <code>Default</code> 和边界场景（如 <code>Empty</code>）两个 story，复杂组件还会有 <code>CustomStyles</code> 等变体，方便 QA 和设计师对照验收。</p>
<p><code>Form</code> 组件的 story 比较特殊，用了 <code>argTypes</code> 的 <code>control: &#39;check&#39;</code> 让测试人员在 Storybook 面板上勾选需要展示的表单字段，动态渲染表单，不需要改代码就能验证各种字段组合：</p>
<pre><code class="language-tsx">argTypes: {
  fields: {
    control: &#39;check&#39;,
    options: xxx_FORM_FIELDS.map((field) =&gt; field.label),
  },
}
</code></pre>
<h3 id="启动与构建"><a href="#启动与构建" class="headerlink" title="启动与构建"></a>启动与构建</h3><pre><code class="language-bash"># 开发模式
pnpm dev:storybook

# 构建静态站点（可部署到 CDN 供设计师访问）
pnpm build:storybook
</code></pre>
<h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>整个项目下来，几个体会比较深的点：</p>
<ol>
<li><strong>Monorepo 的收益在中后期才明显</strong>：前期搭架子比较费时，但后期多个子应用共享组件和工具函数时，维护成本大幅降低</li>
<li><strong>Variant 模式适合业务组件</strong>：纯 UI 组件用 props 控制变体就够了，但业务组件的差异往往不只是样式，Variant 模式更清晰</li>
<li><strong>低代码不是银弹</strong>：低代码适合结构固定、配置频繁的场景，对于逻辑复杂的页面，还是直接写代码更高效</li>
<li><strong>加密要早规划</strong>：加密方案如果后期才加，改动面会很大，最好在 HTTP 层统一处理，业务代码无感知</li>
</ol>
]]></description>
    </item>
    <item>
      <title>开发一个 Chrome DevTools 接口解密插件</title>
      <link>http://example.com/2024/03/16/quark-network-chrome-extension/</link>
      <guid>http://example.com/2024/03/16/quark-network-chrome-extension/</guid>
      <pubDate>Mar 16, 2024</pubDate>
      <description><![CDATA[<p>在金融项目中，通常接口url和参数都会进行加密，所以在调试的时候，Network 面板里看到的全是密文，调试起来非常痛苦。这篇文章介绍我开发的 <strong>xxx Network</strong> —— 一个 Chrome DevTools 插件，能够自动拦截并解密 xxx 系统的加密接口，让调试回归正常。</p>
<!-- more -->

<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>xxx 系统的接口采用 AES-CBC 加密，请求体、响应体以及真实路径都是密文。打开 Chrome DevTools 的 Network 面板，看到的是这样的内容：</p>
<pre><code>request body:  &quot;U2FsdGVkX1+...&quot;
response body: &quot;U2FsdGVkX1+...&quot;
x_x_path:      &quot;U2FsdGVkX1+...&quot;
</code></pre>
<p>每次调试都要手动解密，效率极低。于是我决定写一个 DevTools 插件，把解密过程自动化。</p>
<h2 id="技术方案"><a href="#技术方案" class="headerlink" title="技术方案"></a>技术方案</h2><p>整体架构分两层：</p>
<ul>
<li><strong>Chrome Extension 层</strong>：利用 <code>chrome.devtools.network</code> API 监听网络请求，用 <code>chrome.scripting.executeScript</code> 从页面 <code>localStorage</code> 读取加密密钥（<code>xxxSalt</code>）。</li>
<li><strong>前端 UI 层</strong>：用 Vue 3 + Element Plus 构建 DevTools 面板，展示解密后的请求列表和响应内容，并支持语法高亮。</li>
</ul>
<pre><code>xxx-network/
├── manifest.json          # 插件配置
├── devtools.html          # DevTools 入口页
├── devtools.js            # 注册自定义面板
├── xxx-network/         # Vue3 前端项目
│   └── src/
│       ├── util.ts        # AES 解密核心逻辑
│       └── components/
│           └── Main.vue   # 主界面
└── chrome-plugin-xxx-network.sh  # 打包脚本
</code></pre>
<h2 id="核心：AES-CBC-解密"><a href="#核心：AES-CBC-解密" class="headerlink" title="核心：AES-CBC 解密"></a>核心：AES-CBC 解密</h2><p>加密算法是 AES-CBC + PKCS7 填充，IV 固定为 <code>xxx_mgr_item_a</code>，密钥由 <code>xxxSalt</code> 经过一次解密得到（两层嵌套）。</p>
<pre><code class="language-typescript">// util.ts
function getIvAndkey(xxxSalt: string, key: string) {
  return {
    iv: &quot;xxx_mgr_item_a&quot;,
    key: decrypt(xxxSalt, { iv: &quot;xxx_mgr_item_a&quot;, key }),
  };
}

function decrypt(str: string, { iv, key }: { iv: string; key: string }) {
  if (str.startsWith(&quot;{&quot;)) return formatJSONString(str); // 未加密，直接解析
  str = str.replace(/&quot;/gi, &quot;&quot;);
  const decrypted = CryptoJS.AES.decrypt(str, CryptoJS.enc.Utf8.parse(key), {
    iv: CryptoJS.enc.Utf8.parse(iv),
    padding: CryptoJS.pad.Pkcs7,
    mode: CryptoJS.mode.CBC,
  });
  const result = decrypted.toString(CryptoJS.enc.Utf8).trim();
  if (result.startsWith(&quot;{&quot;)) return formatJSONString(result);
  return result;
}
</code></pre>
<p><code>generate</code> 函数把一条原始网络请求转换成可读的结构：</p>
<pre><code class="language-typescript">export const generate = (network: any, { content, salt }) =&gt; {
  const token = network.request.headers.find((h) =&gt;
    [&quot;xxxtoken&quot;, &quot;token&quot;].includes(h.name.toLowerCase()),
  );
  const xxPath = network.request.headers.find(
    (h) =&gt; h.name.toLowerCase() === &quot;x_x_path&quot;,
  );

  const { iv, key } = getIvAndkey(salt, token?.value);

  return {
    status: network.response.status,
    url: network.request.url,
    path: decrypt(xxPath?.value, { iv, key }) || network.request.url, // 真实路径
    params: JSON.stringify(
      decrypt(network.request.postData?.text, { iv, key }),
    ),
    reponseObj: decrypt(content, { iv, key }), // 解密响应体
  };
};
</code></pre>
<h2 id="监听网络请求"><a href="#监听网络请求" class="headerlink" title="监听网络请求"></a>监听网络请求</h2><p>在 <code>Main.vue</code> 的 <code>onMounted</code> 中，通过 <code>chrome.devtools.network.onRequestFinished</code> 监听请求完成事件，过滤出 <code>/manager</code> 路径下的接口，再用 <code>chrome.scripting.executeScript</code> 注入脚本从页面读取 <code>xxxSalt</code>：</p>
<pre><code class="language-javascript">chrome.devtools.network.onRequestFinished.addListener((request) =&gt; {
  if (!request.request.url.includes(&quot;/manager&quot;)) return;

  request.getContent((content) =&gt; {
    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) =&gt; {
      chrome.scripting
        .executeScript({
          target: { tabId: tabs[0].id },
          function: () =&gt; localStorage.getItem(&quot;xxxSalt&quot;),
        })
        .then(([{ result: salt }]) =&gt; {
          requests.push(generate(request, { salt, content }));
        });
    });
  });
});
</code></pre>
<h2 id="UI-界面"><a href="#UI-界面" class="headerlink" title="UI 界面"></a>UI 界面</h2><p>面板左侧是请求列表，展示状态码、URL、解密后的真实路径和请求参数；右侧点击 “Response” 后显示格式化的 JSON 响应，通过 Prism.js 做语法高亮（字符串绿色、数字蓝色、布尔值红色）。</p>
<p>操作按钮只有两个，保持简洁：</p>
<ul>
<li><strong>Clear All</strong>：清空所有请求记录</li>
<li><strong>Copy Response</strong>：复制当前响应内容到剪贴板</li>
</ul>
<h2 id="构建与安装"><a href="#构建与安装" class="headerlink" title="构建与安装"></a>构建与安装</h2><pre><code class="language-bash"># 开发调试
cd xxx-network
npm i &amp;&amp; npm run dev

# 打包
npm run build
cd ..
bash ./chrome-plugin-xxx-network.sh
</code></pre>
<p>脚本会把 Vite 构建产物、<code>manifest.json</code>、<code>devtools.js</code> 等文件整合到 <code>chrome-xxx-network/</code> 目录，在 Chrome 的 <code>chrome://extensions/</code> 页面加载该目录即可。</p>
<p><code>manifest.json</code> 关键配置：</p>
<pre><code class="language-json">{
  &quot;manifest_version&quot;: 3,
  &quot;permissions&quot;: [&quot;tabs&quot;, &quot;activeTab&quot;, &quot;scripting&quot;],
  &quot;devtools_page&quot;: &quot;devtools.html&quot;,
  &quot;host_permissions&quot;: [&quot;http://localhost/*&quot;, &quot;*://*.xxx.com/*&quot;]
}
</code></pre>
<p><code>host_permissions</code> 限定了插件生效的域名，避免对无关站点产生影响。</p>
<h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>整个插件的核心思路很清晰：<strong>DevTools API 拦截请求 → 从页面上下文读取密钥 → AES 解密 → Vue UI 展示</strong>。代码量不大，但解决了实际痛点。</p>
<p>如果你的项目也有类似的接口加密场景，可以参考这个思路，把解密逻辑替换成自己的算法，快速定制一个专属的 DevTools 调试面板。</p>
]]></description>
    </item>
    <item>
      <title>抢茅子时，Python 和 Go 的多线程到底差在哪？</title>
      <link>http://example.com/2022/08/16/python-vs-golang-multithreading-seckill/</link>
      <guid>http://example.com/2022/08/16/python-vs-golang-multithreading-seckill/</guid>
      <pubDate>Aug 16, 2022</pubDate>
      <description><![CDATA[<p>茅子作为国酒，总有人写脚本抢购。Python 是大多数人的第一选择，但跑起来总感觉慢半拍。后面换成 Go 之后，明显快了。 所以很多人都说python是<code>假多线程</code>，这篇文章就来聊聊背后的原因。</p>
<h2 id="问题的本质：抢购是-I-O-密集型任务"><a href="#问题的本质：抢购是-I-O-密集型任务" class="headerlink" title="问题的本质：抢购是 I&#x2F;O 密集型任务"></a>问题的本质：抢购是 I&#x2F;O 密集型任务</h2><p>抢购脚本的核心逻辑大概是这样：</p>
<ol>
<li>轮询库存接口，等待开售</li>
<li>一旦有货，立刻发起下单请求</li>
<li>并发发多个请求，提高成功率</li>
</ol>
<p>整个过程 CPU 几乎不干活，大部分时间都在等网络响应。这是典型的 <strong>I&#x2F;O 密集型</strong>场景。</p>
<hr>
<h2 id="Python-的-GIL：多线程的天花板"><a href="#Python-的-GIL：多线程的天花板" class="headerlink" title="Python 的 GIL：多线程的天花板"></a>Python 的 GIL：多线程的天花板</h2><p>Python 有一个臭名昭著的东西叫 <strong>GIL（Global Interpreter Lock，全局解释器锁）</strong>。</p>
<p>它的存在是为了保证 CPython 解释器在同一时刻只有一个线程在执行 Python 字节码，避免内存管理出问题。</p>
<pre><code class="language-python">import threading
import requests

def buy():
    resp = requests.post(&quot;https://maotai.example.com/order&quot;, json={&quot;sku&quot;: &quot;53度飞天&quot;})
    print(resp.status_code)

threads = [threading.Thread(target=buy) for _ in range(10)]
for t in threads:
    t.start()
for t in threads:
    t.join()
</code></pre>
<p>上面这段代码开了 10 个线程，看起来是并发的。但实际上：</p>
<ul>
<li>当一个线程在执行 Python 代码时，其他线程被 GIL 挡在门外</li>
<li>只有当线程进入 I&#x2F;O 等待（比如等网络响应）时，GIL 才会释放，让其他线程有机会运行</li>
</ul>
<p>所以 Python 的多线程在 I&#x2F;O 密集型场景下<strong>能用，但有额外开销</strong>：线程切换、GIL 争抢，都是负担。</p>
<h3 id="asyncio：Python-的正确姿势"><a href="#asyncio：Python-的正确姿势" class="headerlink" title="asyncio：Python 的正确姿势"></a>asyncio：Python 的正确姿势</h3><p>对于 I&#x2F;O 密集型任务，Python 更推荐用 <code>asyncio</code> + <code>aiohttp</code>：</p>
<pre><code class="language-python">import asyncio
import aiohttp

async def buy(session):
    async with session.post(&quot;https://maotai.example.com/order&quot;, json={&quot;sku&quot;: &quot;53度飞天&quot;}) as resp:
        print(resp.status)

async def main():
    async with aiohttp.ClientSession() as session:
        tasks = [buy(session) for _ in range(10)]
        await asyncio.gather(*tasks)

asyncio.run(main())
</code></pre>
<p><code>asyncio</code> 是单线程的事件循环，没有 GIL 争抢，并发效率更高。但它是<strong>协作式调度</strong>——你必须在代码里主动 <code>await</code>，否则整个循环会被卡住。</p>
<hr>
<h2 id="Go-的-goroutine：真正的并发"><a href="#Go-的-goroutine：真正的并发" class="headerlink" title="Go 的 goroutine：真正的并发"></a>Go 的 goroutine：真正的并发</h2><p>Go 没有 GIL。它的并发单元是 <strong>goroutine</strong>，由 Go 运行时调度，而不是操作系统。</p>
<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
    &quot;net/http&quot;
    &quot;bytes&quot;
    &quot;sync&quot;
)

func buy(wg *sync.WaitGroup) {
    defer wg.Done()
    body := []byte(`{&quot;sku&quot;:&quot;53度飞天&quot;}`)
    resp, err := http.Post(&quot;https://maotai.example.com/order&quot;, &quot;application/json&quot;, bytes.NewBuffer(body))
    if err != nil {
        fmt.Println(&quot;error:&quot;, err)
        return
    }
    fmt.Println(resp.StatusCode)
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i &lt; 10; i++ {
        wg.Add(1)
        go buy(&amp;wg)
    }
    wg.Wait()
}
</code></pre>
<p>goroutine 的特点：</p>
<ul>
<li><strong>极轻量</strong>：初始栈只有 2KB，可以轻松开几万个</li>
<li><strong>抢占式调度</strong>：Go 运行时会主动调度 goroutine，不需要你手动 <code>await</code></li>
<li><strong>真正并行</strong>：在多核机器上，多个 goroutine 可以同时跑在不同的 CPU 核心上</li>
</ul>
<hr>
<h2 id="直接对比"><a href="#直接对比" class="headerlink" title="直接对比"></a>直接对比</h2><table>
<thead>
<tr>
<th></th>
<th>Python 多线程</th>
<th>Python asyncio</th>
<th>Go goroutine</th>
</tr>
</thead>
<tbody><tr>
<td>有无 GIL</td>
<td>有</td>
<td>有（单线程绕开）</td>
<td>无</td>
</tr>
<tr>
<td>调度方式</td>
<td>OS 线程 + GIL</td>
<td>事件循环（协作式）</td>
<td>Go runtime（抢占式）</td>
</tr>
<tr>
<td>内存开销</td>
<td>~1MB&#x2F;线程</td>
<td>极低</td>
<td>~2KB&#x2F;goroutine</td>
</tr>
<tr>
<td>并发数上限</td>
<td>数百</td>
<td>数万</td>
<td>数十万</td>
</tr>
<tr>
<td>代码复杂度</td>
<td>低</td>
<td>中（需要 async&#x2F;await）</td>
<td>低</td>
</tr>
<tr>
<td>I&#x2F;O 密集型性能</td>
<td>一般</td>
<td>好</td>
<td>好</td>
</tr>
</tbody></table>
<hr>
<h2 id="抢购场景下的实际差距"><a href="#抢购场景下的实际差距" class="headerlink" title="抢购场景下的实际差距"></a>抢购场景下的实际差距</h2><p>假设你要并发发 100 个请求，每个请求耗时 200ms：</p>
<ul>
<li><strong>Python 多线程</strong>：线程切换开销 + GIL 争抢，实际并发度受限，总耗时可能在 400~600ms</li>
<li><strong>Python asyncio</strong>：单线程事件循环，理论上接近 200ms，但受限于事件循环本身的调度延迟</li>
<li><strong>Go goroutine</strong>：100 个 goroutine 真正并发，总耗时接近单次请求的 200ms</li>
</ul>
<p>在抢购这种<strong>毫秒必争</strong>的场景下，这个差距是实质性的。</p>
<hr>
<h2 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h2><ul>
<li>如果你用 Python 写抢购脚本，<strong>用 asyncio + aiohttp，不要用多线程</strong></li>
<li>如果你追求极致并发和低延迟，<strong>Go 是更合适的选择</strong></li>
</ul>
<p>当然，抢到茅台还是要靠运气，祝大家都能抢到心仪的茅子。</p>
<p><img loading="lazy" src="/images/202208/143728_347.png" alt="maotai" title="Maotai"></p>
]]></description>
    </item>
    <item>
      <title>浅谈element-ui中的BEM范式实践</title>
      <link>http://example.com/2018/04/01/element-ui-bem-practice/</link>
      <guid>http://example.com/2018/04/01/element-ui-bem-practice/</guid>
      <pubDate>Apr 1, 2018</pubDate>
      <description><![CDATA[<p>日常的工作中，我们无时无刻不在和样式打交道。没有样式的页面就如同一部电影，被人随意地在不同地方做了截取。</p>
<p>BEM规范应该是对于我们现在前端组件开发中我觉得是最合适的一套范式了。所以，我在自己的日常工作中也是十分的推崇这样的一套CSS范式。</p>
<p>而自己最近也在看各种ui框架的源码，觉得ele对于这块还是处理的蛮好的，所以拿出来说说。</p>
<h2 id="1-BEM"><a href="#1-BEM" class="headerlink" title="1. BEM"></a>1. BEM</h2><p><a href="http://www.cnblogs.com/ChengWuyi/p/5667945.html">BEM是什么？</a></p>
<p>BEM范式我在以前自己的文章中简单的说过，就不再赘述了。</p>
<p>我们来看看饿了么在BEM这块有着怎样的实践。</p>
<pre><code class="language-scss">// element-ui
// config.scss
$namespace: &#39;el&#39;;
$element-separator: &#39;__&#39;;
$modifier-separator: &#39;--&#39;;
$state-prefix: &#39;is-&#39;;
</code></pre>
<p>element在config.scss里面定义了一些基础的配置项目，主要包括四个部分：</p>
<ol>
<li>整套样式的命名空间，命名空间可以带来不同系统样式的隔离，当然缺点就是我们的样式一定是带有一个namespace的前缀出现</li>
<li>B和E之间的连接符</li>
<li>E和M之间的连接符</li>
<li>状态的前缀，因为有很多的用户行为而带来的激活这样的效果。在js中我们会说到is和as，一个是类型的判定，一个是类型的模糊，这是多态的特性体现。所以，同理的话，is在css中代表的就是当前元素状态的判定，例如：<code>is-checked</code>（是否被选中）之类等等</li>
</ol>
<h2 id="2-“B”的定义"><a href="#2-“B”的定义" class="headerlink" title="2. “B”的定义"></a>2. “B”的定义</h2><pre><code class="language-scss">@mixin b($block) {
  $B: $namespace+&#39;-&#39;+$block !global;

  .#{$B} {
    @content;
  }
}
</code></pre>
<p>ele通过宏b来实现的BEM中B的定义。</p>
<p>这里通过radio来作为假设。最后我们在b中通过<code>!global</code>提升了一个<code>$B: el-radio</code>。这也是改良后的BEM。通过插值语句<code>#{ }</code>生成了<code>.el-radio</code>，然后通过<code>@content</code>向生成的B中导入内容。</p>
<p>导入的内容就是通过调用宏b生成的所有的样式：</p>
<pre><code class="language-scss">// 通过宏b生成的所有样式
@include b(radio) {
  color: $--radio-color;
  font-weight: $--radio-font-weight;
  line-height: 1;
  position: relative;
  cursor: pointer;
  display: inline-block;
  white-space: nowrap;
  outline: none;
  font-size: $--font-size-base;
  @include utils-user-select(none);

  @include when(bordered) {
    padding: $--radio-bordered-padding;
    border-radius: $--border-radius-base;
    border: $--border-base;
    box-sizing: border-box;
    height: $--radio-bordered-height;

    &amp;.is-checked {
      border-color: $--color-primary;
    }

    &amp;.is-disabled {
      cursor: not-allowed;
      border-color: $--border-color-lighter;
    }

    &amp; + .el-radio.is-bordered {
      margin-left: 10px;
    }
  }

  @include m(medium) {
    &amp;.is-bordered {
      padding: $--radio-bordered-medium-padding;
      border-radius: $--button-medium-border-radius;
      height: $--radio-bordered-medium-height;
      .el-radio__label { font-size: $--button-medium-font-size; }
      .el-radio__inner {
        height: $--radio-bordered-medium-input-height;
        width: $--radio-bordered-medium-input-width;
      }
    }
  }
  @include m(small) {
    &amp;.is-bordered {
      padding: $--radio-bordered-small-padding;
      border-radius: $--button-small-border-radius;
      height: $--radio-bordered-small-height;
      .el-radio__label { font-size: $--button-small-font-size; }
      .el-radio__inner {
        height: $--radio-bordered-small-input-height;
        width: $--radio-bordered-small-input-width;
      }
    }
  }
  @include m(mini) {
    &amp;.is-bordered {
      padding: $--radio-bordered-mini-padding;
      border-radius: $--button-mini-border-radius;
      height: $--radio-bordered-mini-height;
      .el-radio__label { font-size: $--button-mini-font-size; }
      .el-radio__inner {
        height: $--radio-bordered-mini-input-height;
        width: $--radio-bordered-mini-input-width;
      }
    }
  }

  &amp; + .el-radio {
    margin-left: 30px;
  }

  @include e(input) {
    white-space: nowrap;
    cursor: pointer;
    outline: none;
    display: inline-block;
    line-height: 1;
    position: relative;
    vertical-align: middle;

    @include when(disabled) {
      .el-radio__inner {
        background-color: $--radio-disabled-input-fill;
        border-color: $--radio-disabled-input-border-color;
        cursor: not-allowed;

        &amp;::after {
          cursor: not-allowed;
          background-color: $--radio-disabled-icon-color;
        }

        &amp; + .el-radio__label {
          cursor: not-allowed;
        }
      }
      &amp;.is-checked {
        .el-radio__inner {
          background-color: $--radio-disabled-checked-input-fill;
          border-color: $--radio-disabled-checked-input-border-color;

          &amp;::after {
            background-color: $--radio-disabled-checked-icon-color;
          }
        }
      }
      &amp; + span.el-radio__label {
        color: $--color-text-placeholder;
        cursor: not-allowed;
      }
    }

    @include when(checked) {
      .el-radio__inner {
        border-color: $--radio-checked-input-border-color;
        background: $--radio-checked-icon-color;

        &amp;::after {
          transform: translate(-50%, -50%) scale(1);
        }
      }

      &amp; + .el-radio__label {
        color: $--radio-checked-text-color;
      }
    }

    @include when(focus) {
      .el-radio__inner {
        border-color: $--radio-input-border-color-hover;
      }
    }
  }

  @include e(inner) {
    border: $--radio-input-border;
    border-radius: $--radio-input-border-radius;
    width: $--radio-input-width;
    height: $--radio-input-height;
    background-color: $--radio-input-fill;
    position: relative;
    cursor: pointer;
    display: inline-block;
    box-sizing: border-box;

    &amp;:hover {
      border-color: $--radio-input-border-color-hover;
    }

    &amp;::after {
      width: 4px;
      height: 4px;
      border-radius: $--radio-input-border-radius;
      background-color: $--color-white;
      content: &quot;&quot;;
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%) scale(0);
      transition: transform .15s cubic-bezier(.71,-.46,.88,.6);
    }
  }

  @include e(original) {
    opacity: 0;
    outline: none;
    position: absolute;
    z-index: -1;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    margin: 0;
  }

  &amp;:focus:not(.is-focus):not(:active) { /* 获得焦点时 样式提醒 */
    .el-radio__inner {
      box-shadow: 0 0 2px 2px $--radio-input-border-color-hover;
    }
  }

  @include e(label) {
    font-size: $--radio-font-size;
    padding-left: 10px;
  }
}
</code></pre>
<h2 id="3-“E”的定义"><a href="#3-“E”的定义" class="headerlink" title="3. “E”的定义"></a>3. “E”的定义</h2><p>完成了B以后，就要处理E了。不过在e中多做了两件事：</p>
<ol>
<li>通过<code>each</code>完成了”BE”的样式的生成，例如是input，那么<code>$currentSelector</code>就是<code>.el-radio__input</code></li>
<li>通过函数处理 <code>containsModifier($selector)</code>、<code>containWhenFlag($selector)</code>、<code>containPseudoClass($selector)</code> 三种情况</li>
</ol>
<pre><code class="language-scss">@mixin e($element) {
  $E: $element !global;
  $selector: &amp;;
  $currentSelector: &quot;&quot;;
  @each $unit in $element {
    $currentSelector: #{$currentSelector + &quot;.&quot; + $B + $element-separator + $unit + &quot;,&quot;};
  }

  @if hitAllSpecialNestRule($selector) {
    @at-root {
      #{$selector} {
        #{$currentSelector} {
          @content;
        }
      }
    }
  } @else {
    @at-root {
      #{$currentSelector} {
        @content;
      }
    }
  }
}
</code></pre>
<h2 id="4-“M”的定义"><a href="#4-“M”的定义" class="headerlink" title="4. “M”的定义"></a>4. “M”的定义</h2><p>最后是m的生成，基本上原理都和前面说到的是一样的了。</p>
<p>例如：<code>el-radio--medium</code>，用来描述radio的size属性。那么<code>$currentSelector</code>就是<code>.el-radio--medium</code>。</p>
<pre><code class="language-scss">@mixin m($modifier) {
  $selector: &amp;;
  $currentSelector: &quot;&quot;;
  @each $unit in $modifier {
    $currentSelector: #{$currentSelector + &amp; + $modifier-separator + $unit + &quot;,&quot;};
  }

  @at-root {
    #{$currentSelector} {
      @content;
    }
  }
}
</code></pre>
<h2 id="5-小结"><a href="#5-小结" class="headerlink" title="5. 小结"></a>5. 小结</h2><p>我在这也就是抛砖引玉的说下，精彩的内容还是要大家自己去源码里看，或者自己去试着写一下那就是最好了。</p>
<p>试着写一个vue或者react的组件用上BEM范式去管理类名，肯定也会和我一样，觉得在基于组件化开发的前端项目中，BEM范式绝对是我们管理css的一把利器。</p>
<p>当然，在以后的文章中我也会来说说OO和SMA范式。</p>
]]></description>
    </item>
    <item>
      <title>javascript中的null，对象系统还是非对象系统？</title>
      <link>http://example.com/2018/03/26/javascript-null-object-or-not/</link>
      <guid>http://example.com/2018/03/26/javascript-null-object-or-not/</guid>
      <pubDate>Mar 26, 2018</pubDate>
      <description><![CDATA[<h2 id="1-一直以来的认知"><a href="#1-一直以来的认知" class="headerlink" title="1.一直以来的认知"></a>1.一直以来的认知</h2><p>在我学习js的过程中，爱民老师的绿皮书里将js的类型系统分成了两类：<br><img loading="lazy" src="/images/201803/642545-20180325233759007-1345796346.png" alt="元类型系统和对象类型系统" title="Object Type System"><br>其一是元类型系统：由typeof运算来检测</p>
<p>其二是对象类型系统：是元类型的object的一个分支</p>
<p>而null这个关键字也被归类到了对象类型系统里面了：</p>
<ul>
<li>是属于对象系统的</li>
<li>对象是空值</li>
</ul>
<p>所以，当我们使用typeof去考察Null的话，会返回给我们”object”。</p>
<p>如果去用<code>for(... in null)</code>的方式试图去枚举Null里面的属性的时候。ES5会先行判断null和undefined，如果是这两个值的话，不会去执行循环体。</p>
<p>而我认为这一切都是null既无属性也无方法导致的。而且null也并无原型，也并不是Object()构造器或者其子类实例而来的。</p>
<p>而我是c#出身的前端，我觉得将null归类到对象系统里面是一个不错的选择！</p>
<h2 id="2-《你不知道的js》里面怎么说的？"><a href="#2-《你不知道的js》里面怎么说的？" class="headerlink" title="2.《你不知道的js》里面怎么说的？"></a>2.《你不知道的js》里面怎么说的？</h2><p>当我看到第三章的对象的时候，里面有这样一段话：</p>
<blockquote>
<p>null 有时会被当作一种对象类型，但是这其实只是语言本身的一个 bug，即对 null 执行<br>typeof null 时会返回字符串 “object”。实际上，null 本身是基本类型</p>
</blockquote>
<p>看到这里，完全颠覆我的认知啊！</p>
<p>按照”u dot know”里面的划分的话其实也是书里面很常见的一种划分方式。高程3中就是这样来划分js的类型系统的。但是，因为我觉得爱民老师这样的划分方式也没什么不妥。一切都是看待问题的角度不同导致的。而爱民老师将null归类到对象类型里面去，因为在js中除了undefined之外的都是对象。</p>
<p>而”u dot know”直接耳提面命的告诉我：你一直理解的js类型系统，是错的！</p>
<pre><code>// &#39;u dot know&#39;里的注解
// 原理是这样的，不同的对象在底层都表示为二进制，在 JavaScript 中二进制前三位都为 0 的话会被判
// 断为 object 类型，null 的二进制表示是全 0，自然前三位也是 0，所以执行 typeof 时会返回&quot;object&quot;。
</code></pre>
<h2 id="3-到底怎么理解？"><a href="#3-到底怎么理解？" class="headerlink" title="3.到底怎么理解？"></a>3.到底怎么理解？</h2><p>因为有了疑虑，所以我们要去探查真相！</p>
<p>终于，<a href="https://www.zhihu.com/question/24804474/answer/29040916">贺老给了我们一个很好的回答</a>！</p>
<p>hax主要说了以下几个点：</p>
<ul>
<li>爱民老师是按照自己的理解来分类的，而不是按照ECMA来照本宣科的，况且他写书的时候ES5&#x2F;ES6还没有出来，而ES5之前的ES规范其实都写得比较烂</li>
<li>现在比较普遍的认知是，typeof null返回”object”是一个历史错误（JS的发明者Brendan Eich自己也是这样说的），只是因为要保持语言的兼容性而维持至今。从ES5制定开始就有动议将typeof null改为返回”null”（如启动node加上”–harmony_typeof”参数，即是如此），但是当前ES6标准草案仍然维持了原样。</li>
<li>按照爱民的意见，也可在某种程度上理解为null实为object类型的一个特殊值。在诸如Java这样的语言中，一个变量若是primitive类型，均不可赋以null值，而若是 object，则均可赋以null值。因为在理解上来说，null实际是引用（reference）的特殊值（表示没有指向任何实际对象）</li>
<li>ECMA是如何划分概念，主要是依据ECMA的逻辑，而不是其他标准。所以从ECMA的逻辑看，类型系统是这样划分的，也是合理的</li>
</ul>
<h2 id="4-结论"><a href="#4-结论" class="headerlink" title="4.结论"></a>4.结论</h2><p>古人常云：尽信书不如无书。书中的知识也是作者按照自己对语言了解的深度和他自己涉猎的广度的一个综合的体现。而我们看书，其实就是和作者交流的过程。</p>
<p>所以，你说爱民老师说的对吗？<br>对也不对！狭义的看，如果按照ECMA来说，就不对。但是，广义的看，如果你觉得按照爱民老师的划分，让你对js的类型系统保持了你对”对象”一贯的理解，那就是对的。</p>
<p>那你说，ECMA是对的吗？<br>对也不对！狭义的看，标准一般都是对的。但是，广义的看，如果你觉得把null不归为对象系统，不符合你对高级语言的认知，那也可以说是不对。</p>
<p>那么，回到我这篇文章的标题，js中的null，你觉得是对象吗？<br>我可能并不会去正面回答我提出的问题了，但是，我想说的是，至少我现在并不会对于”u dot know”里面说到的这个问题而耿耿于怀了！</p>
]]></description>
    </item>
    <item>
      <title>从flexible.js引入高德地图谈起的移动端适配</title>
      <link>http://example.com/2018/02/28/flexible-amap-mobile-adaptation/</link>
      <guid>http://example.com/2018/02/28/flexible-amap-mobile-adaptation/</guid>
      <pubDate>Feb 28, 2018</pubDate>
      <description><![CDATA[<p>曾几何时，前端还仅仅是PC端的。随着移动时代的兴起，h5及css3的推陈出新。前端的领域慢慢的由传统的pc端转入了移动端，这也导致了前端这一职业在风口的一段时间出尽了风头。</p>
<p>从手写不同尺寸的媒体查询css到以手淘的flexible.js来进行移动端的适配，虽然过程曲折，不过效果也是十分的显著，因为有了成熟的体系以后，什么东西就有据可寻，适配也就没那么困难了。</p>
<p>但是，因为这次引入了高德地图，所以在适配上出现了一点意料之外的问题。</p>
<p>首先，我要说下视口这个东西，因为手淘的这个方案是严重依赖视口这个概念的。</p>
<h2 id="1-视口"><a href="#1-视口" class="headerlink" title="1. 视口"></a>1. 视口</h2><h3 id="1-1-视口的分类"><a href="#1-1-视口的分类" class="headerlink" title="1.1 视口的分类"></a>1.1 视口的分类</h3><p>ppk将视口分为三大类：布局视口、可视视口、理想视口。</p>
<p>那视口是什么呢？通俗点说就是浏览器上（也可能是一个app中的webview）用来显示网页的那部分区域，但viewport又不局限于浏览器可视区域的大小，它可能比浏览器的可视区域要大，因为为了正常的显示PC端的网页，浏览器会将自己的layout viewport设置为一个较大的值，结果就是会出现左右的滚动条。</p>
<p><img loading="lazy" src="/images/201802/642545-20180228115114056-1073633362.png" alt="视口分类示意图" title="视口分类示意图"></p>
<p>布局视口和可视视口我们作基本了解即可。在实际工作中，我们需要接触和处理的更多是ideal viewport。</p>
<p>而我们前端一直孜孜以求的移动端的适配，其实就是为了让用户的浏览器中呈现的是我们的理想视口。</p>
<p>ideal viewport并没有一个固定的尺寸，不同的设备拥有不同的ideal viewport。早期移动端开发，对于终端设备适配问题只属于Android系列，有320pt的，有360pt的，有384pt的等等。但随着iPhone6、iPhone6+的出现，从此终端适配问题不再是Android系列了，也从这个时候让移动端适配全面进入到”杂屏”时代。</p>
<p><a href="http://viewportsizes.com/">http://viewportsizes.com</a> 里面收集了众多设备的理想宽度。</p>
<h3 id="1-2-如何影响视口？"><a href="#1-2-如何影响视口？" class="headerlink" title="1.2 如何影响视口？"></a>1.2 如何影响视口？</h3><p>既然viewport这么重要，那我们怎么去控制他为我所用呢？这个时候，就轮到meta标签出场了。</p>
<pre><code class="language-html">&lt;meta
  name=&quot;viewport&quot;
  content=&quot;width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0&quot;
/&gt;
</code></pre>
<p>content里面的属性说明：</p>
<table>
<thead>
<tr>
<th>属性</th>
<th>说明</th>
</tr>
</thead>
<tbody><tr>
<td>width</td>
<td>设置 layout viewport 的宽度，为正整数或字符串 <code>device-width</code></td>
</tr>
<tr>
<td>height</td>
<td>设置 layout viewport 的高度，几乎不使用</td>
</tr>
<tr>
<td>initial-scale</td>
<td>设置页面的初始缩放值，相对于 ideal viewport 进行缩放</td>
</tr>
<tr>
<td>minimum-scale</td>
<td>允许用户的最小缩放值</td>
</tr>
<tr>
<td>maximum-scale</td>
<td>允许用户的最大缩放值</td>
</tr>
<tr>
<td>user-scalable</td>
<td>是否允许用户进行缩放，<code>no</code> 或 <code>yes</code></td>
</tr>
</tbody></table>
<pre><code class="language-html">&lt;!-- 将 layout viewport =&gt; ideal viewport --&gt;
&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width&quot; /&gt;
&lt;meta name=&quot;viewport&quot; content=&quot;initial-scale=1.0&quot; /&gt;
</code></pre>
<p>上面两种方式是殊途同归的。那么，为什么我们还要将两个都写上去呢？</p>
<ul>
<li><code>initial-scale=1.0</code>：处理在 iPhone、iPad 上，无论竖屏还是横屏，宽度都是竖屏时 ideal viewport 宽度的问题。</li>
<li><code>width=device-width</code>：处理在 Windows Phone 上的 IE 无论竖屏还是横屏都把宽度设为竖屏时 ideal viewport 宽度的问题。</li>
</ul>
<pre><code class="language-html">&lt;meta name=&quot;viewport&quot; content=&quot;width=400, initial-scale=1.0&quot; /&gt;
</code></pre>
<p>如果出现了上面这种冲突，浏览器会取两者中较大的那个值。总结起来就是”谁大谁先行”。</p>
<h2 id="2-引入高德后页面的表现"><a href="#2-引入高德后页面的表现" class="headerlink" title="2. 引入高德后页面的表现"></a>2. 引入高德后页面的表现</h2><p>在vue的spa项目中引入高德以后，我们发现在不同的dpr下，地图的显示效果差距非常大。</p>
<p><img loading="lazy" src="/images/201802/642545-20180228112156882-1384989867.png" alt="高德地图在dpr&#x3D;3下显示异常截图" title="高德地图在dpr&#x3D;3下显示异常截图"></p>
<p>在dpr&#x3D;3的时候，也就是plus的机型上，地图显得格外的小，几乎用肉眼是无法看清上面的字体。所以，基于flexible的适配方法肯定是有问题的了。</p>
<p>而出现这个问题的原因就是我们的viewport被缩放了。</p>
<pre><code class="language-js">if (!dpr &amp;&amp; !scale) {
  var isAndroid = win.navigator.appVersion.match(/android/gi);
  var isIPhone = win.navigator.appVersion.match(/iphone/gi);
  var devicePixelRatio = win.devicePixelRatio;
  if (isIPhone) {
    // iOS下，对于2和3的屏，用2倍的方案，其余的用1倍方案
    if (devicePixelRatio &gt;= 3 &amp;&amp; (!dpr || dpr &gt;= 3)) {
      dpr = 3;
    } else if (devicePixelRatio &gt;= 2 &amp;&amp; (!dpr || dpr &gt;= 2)) {
      dpr = 2;
    } else {
      dpr = 1;
    }
  } else {
    // 其他设备下，仍旧使用1倍的方案
    dpr = 1;
  }
  scale = 1 / dpr;
}
</code></pre>
<p>通过上面的代码计算出了viewport缩放的比率。当处于iPhone 6+ Plus的时候，<code>scale = 0.3333...</code></p>
<pre><code class="language-js">metaEl.setAttribute(
  &quot;content&quot;,
  &quot;initial-scale=&quot; +
    scale +
    &quot;, maximum-scale=&quot; +
    scale +
    &quot;, minimum-scale=&quot; +
    scale +
    &quot;, user-scalable=no&quot;,
);
</code></pre>
<p>最后写到页面上面的结果就是：</p>
<pre><code class="language-html">&lt;meta
  name=&quot;viewport&quot;
  content=&quot;initial-scale=0.3333333333333333, maximum-scale=0.3333333333333333, minimum-scale=0.3333333333333333, user-scalable=no&quot;
/&gt;
</code></pre>
<p>所以，iPhone Plus是414pt，通过flexible将viewport缩小了0.333…，<code>414 / 0.333... = 1242</code>，而正好高德地图通过canvas绘制的画布宽度也就是1242。</p>
<h2 id="3-如何解决这个问题"><a href="#3-如何解决这个问题" class="headerlink" title="3. 如何解决这个问题"></a>3. 如何解决这个问题</h2><p>处理这个问题的方法大致有三种。</p>
<h3 id="3-1-通过vue-router的路由守卫进行处理"><a href="#3-1-通过vue-router的路由守卫进行处理" class="headerlink" title="3.1 通过vue-router的路由守卫进行处理"></a>3.1 通过vue-router的路由守卫进行处理</h3><pre><code class="language-js">beforeMount() {
  this.$nextTick(() =&gt; {
    const dpr = document.documentElement.getAttribute(&#39;data-dpr&#39;)
    if (dpr &gt; 1) {
      window.tempViewport = document.querySelector(&#39;meta[name=&quot;viewport&quot;]&#39;).getAttribute(&#39;content&#39;);
      document.querySelector(&#39;meta[name=&quot;viewport&quot;]&#39;).setAttribute(&#39;content&#39;, &#39;width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no&#39;);
      window.tempDpr = dpr;
      document.documentElement.setAttribute(&#39;data-dpr&#39;, 1);
    }
  })
},
beforeRouteLeave(to, from, next) {
  if (window.tempViewport) {
    document.querySelector(&#39;meta[name=&quot;viewport&quot;]&#39;).setAttribute(&#39;content&#39;, window.tempViewport);
    delete window.tempViewport;
  }
  if (window.tempDpr) {
    document.documentElement.setAttribute(&#39;data-dpr&#39;, window.tempDpr);
    delete window.tempDpr;
  }
  next()
},
</code></pre>
<p>不过这样的方式不是很好，因为页面在过渡的时候会出现一瞬间样式的变形。而且如果在当前有地图的页面有其他结构的话，其他结构也会错乱。</p>
<blockquote>
<p>Tips：如果不是SPA的应用，而且整屏页面是地图占满的情况下，这个方案还是可行的。</p>
</blockquote>
<h3 id="3-2-通过css-scale属性"><a href="#3-2-通过css-scale属性" class="headerlink" title="3.2 通过css scale属性"></a>3.2 通过css scale属性</h3><p>这个方法在试验了以后，也存在问题。虽然地图的大小是正常了，但是在地图上进行点标记的时候，会出现地图位置的偏移。</p>
<blockquote>
<p>Tips：如果仅仅是展示，而并没有任何交互的情况下，这个方式也是可行的。</p>
</blockquote>
<h3 id="3-3-通过设置dpr-1（推荐）"><a href="#3-3-通过设置dpr-1（推荐）" class="headerlink" title="3.3 通过设置dpr&#x3D;1（推荐）"></a>3.3 通过设置dpr&#x3D;1（推荐）</h3><p>通过设置<code>dpr=1</code>，强制flexible布局对viewport不进行缩放。</p>
<pre><code class="language-html">&lt;meta name=&quot;flexible&quot; content=&quot;initial-dpr=1&quot; /&gt;
</code></pre>
<p>这样，最后写到页面上的meta标签就是：</p>
<pre><code class="language-html">&lt;meta
  name=&quot;viewport&quot;
  content=&quot;initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no&quot;
/&gt;
</code></pre>
<p>既然viewport没有缩放了，高德地图通过canvas绘制的地图也就是按照我们的ideal viewport来进行处理了。</p>
<p>不过这种方式会产生另外两个副作用：</p>
<ul>
<li>通过缩放来处理的”1px问题”需要重新去处理</li>
<li>通过dpr设置的不同dpr下的文本字号大小，可能会出现13px这样很奇葩的尺寸</li>
</ul>
<h2 id="4-结尾"><a href="#4-结尾" class="headerlink" title="4. 结尾"></a>4. 结尾</h2><p>大漠对于这个问题的解释是：flexible已经完成自己的使命，该功成身退了。他推荐使用vw、vh标准的新布局方式。</p>
<p>而到底用不用这套方案，作为前端的我们也是见仁见智了！</p>
]]></description>
    </item>
    <item>
      <title>vue-router项目实战总结</title>
      <link>http://example.com/2018/01/02/vue-router-project-summary/</link>
      <guid>http://example.com/2018/01/02/vue-router-project-summary/</guid>
      <pubDate>Jan 2, 2018</pubDate>
      <description><![CDATA[<p>今天来谈谈vue项目{vue,vue-router,component}三大神将之一的vue-router。作为我们前后端分离很重要的实践之一，router帮我们完成了SPA应用间的页面跳转。</p>
<p>并且，配合axios这样的第三方库，我们可以实现配合后台接口的拦截器功能。</p>
<p>对于一个小型项目而言，router这个文件夹里面就包含了一个router.js就足够了，但是，当我们的页面比较多的时候，我们就需要分出两个文件出来：一个定义我们的路由和组件，另一个实例化组件，并将路由挂载到vue的实例上。</p>
<p>基本的用法就不多赘述，大家可以看 <a href="https://router.vuejs.org/zh-cn/">vue-router</a> 的官网，认真过一遍的话，基本使用肯定没什么问题。</p>
<h2 id="1-为什么我的路由不起作用？"><a href="#1-为什么我的路由不起作用？" class="headerlink" title="1. 为什么我的路由不起作用？"></a>1. 为什么我的路由不起作用？</h2><p>这里有个非常重要的一点就是当我们去构造VueRouter的实例的时候，传入的参数的问题。</p>
<pre><code class="language-js">import routes from &quot;@/router/router&quot;;

const router = new VueRouter({
  routes, // （ES6语法）相当于 routes: routes
});

new Vue({
  router,
}).$mount(&quot;#app&quot;);
</code></pre>
<p>如果你这里引入的不是routes，你就要按照下面的方式来写：</p>
<pre><code class="language-js">import vRoutes from &quot;@/router/router&quot;;

const router = new VueRouter({
  routes: vRoutes,
});

new Vue({
  router,
}).$mount(&quot;#app&quot;);
</code></pre>
<h2 id="2-在路由中基于webpack实现组件的懒加载"><a href="#2-在路由中基于webpack实现组件的懒加载" class="headerlink" title="2. 在路由中基于webpack实现组件的懒加载"></a>2. 在路由中基于webpack实现组件的懒加载</h2><p>对于我们的vue项目，我们基本都是运用webpack打包的，如果没有懒加载，打包后的文件将会异常的大，造成首页白屏，延时严重，不利于用户体验。而运用懒加载则可以将页面进行划分，webpack将不同组件打包成很多个小的js文件，需要的时候再异步加载，优化用户的体验。</p>
<pre><code class="language-js">import App from &quot;@/App.vue&quot;;

const index = (r) =&gt;
  require.ensure([], () =&gt; r(require(&quot;@/pages/index/index&quot;)), &quot;index&quot;);

export default [
  {
    path: &quot;/&quot;,
    component: App,
    children: [
      {
        path: &quot;/index&quot;,
        name: &quot;index&quot;,
        component: index,
      },
    ],
  },
];
</code></pre>
<p>如果某个组件包含了嵌套路由，我们也可以将两个路由打包到一个js chunk中：</p>
<pre><code class="language-js">// 这两条路由被打包在相同的块中，访问任一路由都会延迟加载该路由组件
const orderUser = (r) =&gt;
  require.ensure([], () =&gt; r(require(&quot;@/pages/order/user&quot;)), &quot;order&quot;);
const payRecord = (r) =&gt;
  require.ensure([], () =&gt; r(require(&quot;@/pages/order/payRecord&quot;)), &quot;order&quot;);
</code></pre>
<h2 id="3-router的模式"><a href="#3-router的模式" class="headerlink" title="3. router的模式"></a>3. router的模式</h2><p>对于浏览器，我们的router分为两种模式。</p>
<p><strong>1. hash模式（默认）</strong></p>
<p>按照一个uri的基本结构来说，hash模式就是在一个基本的URI的片段进行的处理。如果抛开SPA的话，比较常见的应用场景就是我们在做pc商城的时候，会有比如说：商品详情、评论、商品参数这样的tab切换，就可以使用a标签配合id使用，加上一点运动的特效，效果甚佳。</p>
<p><img loading="lazy" src="/images/201801/642545-20180102135038424-1601781225.png" alt="hash模式URI结构示意图" title="hash模式URI结构示意图"></p>
<p>这也是router默认使用的路由方式。不过，这种方式有一个弊端，就是在接入第三方支付的时候，我们传入一个url给到第三方支付作为回调地址，但是在支付完成以后，有的第三方支付会把我们的<code>#</code>作为一个截取符号，仅保留第一个<code>#</code>符号前面的url内容，后面再添加相应的回调参数，导致支付完成以后无法跳转到相应的支付页面。</p>
<pre><code>传入的url:
http://xx.xx.com/#/pay/123

回调后的地址:
http://xx.xx.com/pay/123?data=xxxxx%xxxx
</code></pre>
<p><strong>2. history模式</strong></p>
<p>还有一种就是history的模式。它是使用h5的<code>history.pushState</code>来完成URL的跳转的。使用这种方式来处理跳转的好处就是，url和我们平常看到的没有什么区别，和hash模式作比较的话就是没有了<code>#</code>。不过使用history模式，我们在后台也要去做相应的处理，因为如果直接去访问一个地址，例如<code>http://www.xxxx.com/user/id</code>的时候，如果后端没有配置，后端就会返回404页面。</p>
<h2 id="4-router-link在循环中this-参数名-undefined"><a href="#4-router-link在循环中this-参数名-undefined" class="headerlink" title="4. router-link在循环中this.参数名&#x3D;undefined"></a>4. router-link在循环中this.参数名&#x3D;undefined</h2><p><code>&lt;router-link&gt;</code> 组件是我们在view层中需要用到的跳转组件。它替代了<code>&lt;a&gt;</code>标签需要做的事情，并且帮助我们做了更多的事情：</p>
<ul>
<li>无论是 h5 history 模式还是 hash 模式，它的表现行为一致，所以，当你要切换路由模式，或者在 IE9 降级使用 hash 模式，无须任何变动。</li>
<li>在 HTML5 history 模式下，<code>router-link</code> 会守卫点击事件，让浏览器不再重新加载页面。</li>
<li>当你在 HTML5 history 模式下使用 <code>base</code> 选项之后，所有的 <code>to</code> 属性都不需要写（基路径）了。</li>
</ul>
<p>不过当我们在<code>v-for</code>的循环中使用了<code>router-link</code>的时候，如果需要取一个我们在data中定义的值，我们是通过<code>this.foo</code>来取呢？还是通过<code>foo</code>来取呢？</p>
<p>这里的话，我们是<strong>不能</strong>通过<code>this.foo</code>来取的，因为这里的<code>this</code>不再是指向vue的实例了，而是指向了<code>[object Window]</code>。所以用<code>this.foo</code>来取的话，其实是<code>undefined</code>。</p>
<pre><code class="language-html">&lt;router-link
  tag=&quot;li&quot;
  :to=&quot;{path:`/user/${item.userID}`}&quot;
  v-for=&quot;(item, index) in userList&quot;
  :key=&quot;index&quot;
&gt;
  &lt;!-- this.foo 是 undefined，应直接用 foo --&gt;
  &lt;p&gt;{{this.foo}}&lt;/p&gt;
  &lt;p&gt;{{foo}}&lt;/p&gt;
&lt;/router-link&gt;
</code></pre>
<pre><code class="language-js">data(){
  return {
    foo: &#39;bar&#39;,
  }
}
</code></pre>
<h2 id="5-vue-router配合axios的使用"><a href="#5-vue-router配合axios的使用" class="headerlink" title="5. vue-router配合axios的使用"></a>5. vue-router配合axios的使用</h2><p>初次接触拦截器这个概念是在java中，通过拦截器，我们可以对用户的登录状态进行更加粒度的操作。而对于一个SPA的应用来说，没有了后台路由的介入，我们就需要在前端实现一套自己的登录状态的管理机制。</p>
<p>最直观的一点就是，通过用户的token来判断用户是否登录：</p>
<pre><code class="language-js">router.beforeEach((to, from, next) =&gt; {
  const NOW = new Date().getTime();
  if (to.matched.some((r) =&gt; r.meta.requireAuth)) {
    if (NOW &gt; store.state.deadLine) {
      store.commit(&quot;CLEAR_USERTOKEN&quot;);
    }
    if (store.state.message.login === true) {
      next();
    } else {
      next({
        path: &quot;/login&quot;,
        query: { redirect: to.fullPath },
      });
    }
  } else {
    next();
  }
});
</code></pre>
<p>上面的代码中，我们通过vue-router中的全局守卫，在导航触发的时候大致做了如下几件事：</p>
<ol>
<li>判断导航的页面是否需要登录</li>
<li>超过登录持久期限，清除持久化的登录用户token</li>
<li>没有超过登录期限，判断是否登录状态</li>
<li>没登录，重定向到登录页面</li>
</ol>
<p>但是，仅仅这样是不够的。因为用户直接不正常注销而直接后台运行网页是很正常的事情，这就导致虽然token是存在的，但是对于后台而言，这个token是无效的、过期的了。所以，我们需要axios配合后台给出的状态码来完善我们的拦截器。</p>
<pre><code class="language-js">import router from &quot;@/router/routes&quot;;

axios.interceptors.response.use(
  (success) =&gt; {
    switch (success.code) {
      case -100:
        router.replace({
          path: &quot;login&quot;,
          query: { redirect: router.currentRoute.fullPath },
        });
        console.warn(&quot;注意,登录已过期!&quot;);
        break;
    }
    return success;
  },
  (error) =&gt; {
    switch (error.code) {
      case 404:
        console.warn(&quot;请求地址有误或者参数错误!&quot;);
        break;
    }
    return Promise.reject(error.response.data);
  },
);
</code></pre>
<p>通过后端给到的登录过期状态码（这里以-100为例），我们可以用axios的响应拦截器实现，当我们的token过期的时候，将页面重定向到登录页面去。</p>
<h2 id="6-巧用replace替换push"><a href="#6-巧用replace替换push" class="headerlink" title="6. 巧用replace替换push"></a>6. 巧用replace替换push</h2><p>在项目中，有的同事就是一直<code>this.$router.push(...)</code>，从开始push到结尾。</p>
<p>碰到有的页面，比如说，在选择地址的时候需要知道用户当前所在的城市，如果没有的话，就重定向到城市列表页面去手动选取。选择完成以后再回到选择地址的页面，如果一直使用push的话，点击选择地址的后退时，就会回退到城市列表页，造成页面间的死循环。</p>
<p>这里如果使用<code>replace</code>来操作就没有什么问题了，问题就是我们不应该让城市列表页出现在我们的浏览历史里面。</p>
]]></description>
    </item>
    <item>
      <title>vue项目的骨架及常用组件介绍</title>
      <link>http://example.com/2017/12/23/vue-project-skeleton-and-common-components/</link>
      <guid>http://example.com/2017/12/23/vue-project-skeleton-and-common-components/</guid>
      <pubDate>Dec 23, 2017</pubDate>
      <description><![CDATA[<h2 id="vue项目基础结构"><a href="#vue项目基础结构" class="headerlink" title="vue项目基础结构"></a>vue项目基础结构</h2><p>一个vue的项目，我觉得最小的子集其实就是{vue,vue-router,component}，vue作为基础库，为我们提供双向绑定等功能。vue-router连接不同的”页面”，component作为样式或者行为输出，你可以通过这三个东西来实现最基本的静态SPA网站。当然我在这里不谈vue全家桶这样宽泛的概念，我会如数家珍的把主要的技术点一一列举。</p>
<ol>
<li>vue-cli：搭建基本的vue项目骨架，脚手架工具</li>
<li>sass-loader&amp;node-sass：使用sass作为样式的预编译工具，两者缺一不可</li>
<li>postcss：实现响应式布局的关键，px&#x3D;&gt;rem</li>
<li>vuex：管理复杂的数据流向，状态机工具，特化的Flux</li>
<li>vuex-persistedstate：将vuex中state持久化的工具</li>
<li>vue-router：实现SPA间”页面”之间的跳转</li>
<li>vue-lazyload：实现图片的懒加载，优化http传输性能</li>
<li>vue-awesome-swiper：轮播功能的实现及一些特殊切换效果的完成</li>
<li>better-scroll：实现列表滚动及父子组件间的滚动问题</li>
<li>axios：http工具，实现向API请求数据，以及拦截器的实现</li>
<li>fastclick：解决300ms延迟的库</li>
</ol>
<p>以上这些，都是我觉得一个中大型的vue项目需要用到的，还有一些比如我在实现图片上传中用到了jsx的语法，需要babel-jsx这样的东西，不具有普适性，就不例举了。</p>
<p>下面简述一下上面说到的这些东西，有的东西会单独的来出来细说：</p>
<h2 id="1-vue-cli"><a href="#1-vue-cli" class="headerlink" title="1. vue-cli"></a>1. vue-cli</h2><p><a href="https://github.com/vuejs/vue-cli">https://github.com/vuejs/vue-cli</a></p>
<p>脚手架工具，当我们选择vue作为我们的开发技术栈以后，就要开始为我们的项目搭建目录及开发的环境。安装好node以后，通过以下命令进行安装：</p>
<pre><code class="language-bash">npm install -g vue-cli          # 将vue-cli安装到全局环境
vue init webpack my-vue-demo    # 创建基于webpack模板的vue项目
</code></pre>
<p>这里的模板有6种，不过我们比较常用的就是webpack了。</p>
<p>期间你会看到有一些例如e2e这样的单元测试的工具和ESLint检测代码质量的工具，我觉得都是可以不必安装的。</p>
<p>那么，其实我们最关心的就是在src文件夹下面的内容了。可以看下图：</p>
<p><img loading="lazy" src="/images/201711/642545-20171223151348943-2006320399.png" alt="vue项目目录结构示意图" title="vue项目目录结构示意图"></p>
<p>上图就是一个在刨除vue-cli的基本结构，在项目上比较成熟的vue骨架了。</p>
<h2 id="2-3-sass-postcss"><a href="#2-3-sass-postcss" class="headerlink" title="2&amp;3. sass, postcss"></a>2&amp;3. sass, postcss</h2><p>直接写css的洪荒时代已经过去了，预编译的样式处理器帮助我们解放了生产力，提高了效率。sass、less、stylus各有优缺，也各有信徒。</p>
<p>要使用sass的话，你需要安装sass-loader和node-sass，不过node-sass不是很好装，被墙的厉害，建议还是用taobao的镜像。如果安装完成后还是报错无法解析的话，你可能就需要去<code>webpack.base.conf.js</code>里去看看是否设置好了对应的loader。</p>
<p>postcss的常用功能：</p>
<ul>
<li><strong>px2rem</strong>：可以帮助我们实现px到rem单位的转换，只需要你定义好相应的转换标准就可以了。</li>
<li><strong>autoprefixer</strong>：兼容性的处理postcss也可以帮我们处理好。</li>
</ul>
<pre><code class="language-js">// vue-loader.conf.js
module.exports = {
  loaders: utils.cssLoaders({
    sourceMap: isProduction
      ? config.build.productionSourceMap
      : config.dev.cssSourceMap,
    extract: isProduction,
  }),
  postcss: [
    require(&quot;autoprefixer&quot;)({
      browsers: [&quot;iOS &gt;= 7&quot;, &quot;Android &gt;= 4.1&quot;],
    }),
    require(&quot;postcss-px2rem&quot;)({ remUnit: 64 }),
  ],
};
</code></pre>
<h2 id="4-5-vuex-vuex-persistedstate"><a href="#4-5-vuex-vuex-persistedstate" class="headerlink" title="4, 5. vuex, vuex-persistedstate"></a>4, 5. vuex, vuex-persistedstate</h2><p><a href="https://github.com/robinvdvleuten/vuex-persistedstate">https://github.com/robinvdvleuten/vuex-persistedstate</a></p>
<p>一个中大型的vue项目，肯定有复杂的状态需要去管理。简单的event bus已经不再适用了。</p>
<p>特化的Flux架构，vuex就迎头顶上。简而言之：他就是我们处理无论是用户操作、API返回、URL变更等多重操作的状态管理工具。</p>
<p>用过vuex的人，会发现一个很痛苦的地方，就是vuex里面的state，只要我们去刷新，它就被释放掉了。vuex-persistedstate帮我们解决了这样的问题，它帮我们直接把state映射到了本地的缓存环境，我们可以在computed里面用vuex提供的mapState辅助函数，来动态的更新local里面的数据。而不需要持久化的state，我们依旧可以刷新来释放掉。</p>
<h2 id="6-vue-router"><a href="#6-vue-router" class="headerlink" title="6. vue-router"></a>6. vue-router</h2><p>当我们使用vue来构建SPA的应用时，就等于说我们完全的分离了前后端。后端仅仅提供数据，任何的逻辑都在前端实现。vue-router就帮我们做了这样的事情，他提供给了路由守卫给我们，我们可以设置全局的、组件内的路由守卫，来实现特定的业务逻辑，提供过渡动画等等。</p>
<h2 id="7-vue-lazyload"><a href="#7-vue-lazyload" class="headerlink" title="7. vue-lazyload"></a>7. vue-lazyload</h2><p><a href="https://github.com/hilongjw/vue-lazyload">https://github.com/hilongjw/vue-lazyload</a></p>
<p>实现图片的懒加载。这是前端性能优化的一个必须面对的问题：图片。懒加载可以减少请求的数量，而且在很直观的视觉上，也有一个良好的过渡。当然，图片我们也是需要去做一些处理的，使用webp格式来减小图片的质量，或者通过oss来对图片作处理。</p>
<h2 id="8-vue-awesome-swiper"><a href="#8-vue-awesome-swiper" class="headerlink" title="8. vue-awesome-swiper"></a>8. vue-awesome-swiper</h2><p><a href="https://github.com/surmon-china/vue-awesome-swiper">https://github.com/surmon-china/vue-awesome-swiper</a></p>
<p>通过它可以实现基本轮播、横轴的切换、横轴的列表滚动等。</p>
<p><img loading="lazy" src="/images/201711/642545-20171223165139818-1148541998.png" alt="vue-awesome-swiper tab切换示意图" title="vue-awesome-swiper tab切换示意图"></p>
<p>例如我要去实现四个tab切换这样的功能，但是简单的display这样的效果我又觉得不是很满意。那么我们就可以通过swiper来实现，每次tab里面的content都会对应swiper的一个swiper-item。切换的tab，其实就是swiper里面的next page或者before page。</p>
<pre><code class="language-js">data(){
  return{
    swiperOption: {
      slidesPerView: &#39;auto&#39;,
      direction: &#39;horizontal&#39;,
      freeMode: true,
      loop: false,
      spaceBetween: 20,
    },
  }
}
</code></pre>
<pre><code class="language-html">&lt;swiper :options=&quot;swiperOption&quot; ref=&quot;swiper&quot; v-if=&quot;list &amp;&amp; list.length !== 0&quot;&gt;
  &lt;swiper-slide v-for=&quot;(item,index) in list&quot; :key=&quot;index&quot; class=&quot;hot-item&quot;&gt;
    &lt;router-link
      :to=&quot;{name:&#39;quickCar&#39;,params:{carID:item.CarID}}&quot;
      class=&quot;description_car&quot;
    &gt;
      &lt;img
        v-lazy=&quot;item.Attachments.length !==0 &amp;&amp; item.Attachments[0].FilePath&quot;
      /&gt;
      &lt;span&gt;&amp;yen;{{item.price}}/日&lt;/span&gt;
    &lt;/router-link&gt;
  &lt;/swiper-slide&gt;
&lt;/swiper&gt;
&lt;p class=&quot;noData&quot; v-else&gt;&lt;/p&gt;
</code></pre>
<h2 id="9-better-scroll"><a href="#9-better-scroll" class="headerlink" title="9. better-scroll"></a>9. better-scroll</h2><p><a href="https://github.com/ustbhuangyi/better-scroll">https://github.com/ustbhuangyi/better-scroll</a></p>
<p>实现纵轴列表的滚动，以及当有嵌套的路由的时候，通过better-scroll来实现禁止父路由随着子路由滚动的问题。</p>
<p>better-scroll其实也可以去实现横轴的滚动，但是为什么不使用better-scroll来处理呢？这是因为在better-scroll实现横轴滚动的时候，我们无法在better-scroll的content的内容区域里去向下拉动我们的页面。所以导致的一个Bug就是，在better-scroll横轴滚动的区域里，页面动不了了。</p>
<p><img loading="lazy" src="/images/201711/642545-20171223165835834-909470123.png" alt="better-scroll横轴滚动bug示意图" title="better-scroll横轴滚动bug示意图"></p>
<p>如上图：横轴滚动下面还有内容，但是在图片所示的区域里面，无法向下拉动。所以横轴的滚动其实也是通过vue-awesome-swiper来实现的。</p>
<h2 id="10-axios"><a href="#10-axios" class="headerlink" title="10. axios"></a>10. axios</h2><p>基本功能就是通过axios来请求后台接口的数据。并且axios可以配合router更好的实现类似后台的拦截器的功能，例如处理token过期这样的问题。因为当token过期的时候，仅仅通过vue-router的<code>router.beforeEach</code>来处理就有点无能为力了。这时候就需要配合后台响应返回的code来进行url的处理。</p>
<h2 id="11-fastclick"><a href="#11-fastclick" class="headerlink" title="11. fastclick"></a>11. fastclick</h2><p>解决点透和点击延时的问题。</p>
<p>具体可以看钗神的源码分析：<a href="https://www.cnblogs.com/yexiaochai/p/3442220.html">https://www.cnblogs.com/yexiaochai/p/3442220.html</a></p>
]]></description>
    </item>
</channel>
</rss>