世界是平的

Think & Talk

社区团购乱战,如何杀出重围?

编辑导语:社区团购的出现让用户可以更加便利地获得所需菜品,因此,其用户规模也在不断上升,各玩家也纷纷入局。不过,伴随着竞争愈发激烈,社区团购未来也需要寻找一个更合适的发展模式。本文探索了社区团购市场改变的方案,并试图验证社区团购线下直销的可行性,一起来看一下。

社区团购平台拥有便利、低价等良好的特性,受众群体不断增加。大量团购平台层出不穷,用户可选择余地较大。

近期在团长手上拿菜过程中,发现团长在进行平台商品线下售卖,其运用自身私欲流量,从平台以低价购买进货,线下加价售卖,以此引出了对社区团购加线下直销的构思。

本文从产品角度探索社区团购市场改变的方案,验证社区团购线下直销的可行性。

一、背景

疫情过后,社区团购的观念深入人心。随着互联网大佬纷纷入局,各种社区团购软件层出不穷,现今已经是千亿级别市场。如拼多多的多多买菜、淘宝的淘宝买菜、美团的美团买菜、支付宝的盒马生鲜等等,众所周知的互联网新贵以及元老都冲向了“菜篮子”,价格战打的不可开交。

随着国家政策的约束,如在2020年12月22日,市场监管总局联合商务部召开规范社区团购秩序行政指导会,要求互联网平台企业严格遵守“九不得”,垄断“菜篮子”的方式成为了泡沫。想要掌握了与上游商家的定价权,通过极低的价格压榨线下商家的生存空间,最终开始收割用户的可操作性也更加难以实现。

二、迭代方向

一方面是政策高压,一方面是竞争激烈,同时社区团购的前途也充满了未知。在现今的基础上如何给社区团购做加减法成为了难题。如美团的菜谱、兴盛优选的看视频等新的特色功能,都在瞄准更加精确的用户群体,培养用户习惯。

但是在多方价格战的竞争之下,用户都流向了真正低价的一方,难以培养出“忠实”的用户。如多多买菜,作为较后入场的新贵,结合其市场定位,在社区团购的基础上做减法,运用单页面加大量红包的方式,减少用户的抉择时间。

但是无论页面如何缩减,社区团购的用户的第一标签还是寻找便宜商品这一点,通过功能添加,页面减少等方式依然是难以解决核心难点。

对社区团购用户需求进行分析,可以清晰地发现,他们在寻找便宜好物。如现今社区团购的主攻点“生鲜”,用户可以很快找到便宜的生鲜产品。但是随着购买频次增加,出现“低价坏物”的情况也经常出现。并且在社区团购中出现生鲜坏物的情况,往往就是完全不能使用,极大地影响了用户体验感。因此被大量用户贴上了品质差的标签,这一点也是现今社区团购最大的问题。

现今在社区团购市场上还未拥有平台能够解决品质这一难题,一但拥有平台能够真正的解决这个问题,成为寡头将成为可能。

三、产品实现

分析商品从上游商家走向用户手中这个过程,冻品以及成品在这个过程中损耗率较低,用户不接受的较大原因这方面产品品质难以把控的问题。生鲜产品,在运输过程中损耗率较大,且储存拥有储存周期,时间超过商品会损坏。生鲜到用户的时间难以掌控,这也是用户购买生鲜时好时坏的重要原因。

在社区团购平台中,拥有大量物美价廉的商品,用户难以验证商品的好坏,那么现今的问题是如何向用户证明成品的品质,证明生鲜的新鲜。思考线下产品的买卖场景,用户通过自己的视觉、触觉、嗅觉以及听觉来判断产品好坏,让产品出现在用户面前用产品证明产品。那社区团购有没有方式让产品放在用户面前让用户选择了?

结论肯定是有的,社区团购的最后一个环节,团长到用户这个过程。用户会在团长建设的环境拿自己的商品。那如何让团长把社区团购的商品放在自家的场景进行售卖,成为了难点。

再次结合线下,线下商家会自己进货卖卖,自己获取其中的差价。社区团购的百分之十左右的提成过低,且用户购买商品需要成本,过程中存在损耗的风险。这时出现几点问题:

  1. 用户购买社区团购商品,以原价自卖自销收益过低;
  2. 存在商品买入,卖不出去的风险;
  3. 存在绝大部分团购用户共性问题,对买到的商品质量无法掌控;
  4. 好货自己挑选时间成本以及经济成本都较难掌控。

那么事情逐渐明朗化。让用户购买商品拥有自我溢价权,或者说用户从购买商品到用户手里本身就具备溢价的权利。

第二点,提供最新到来的商品售卖以及当天未卖完货品召回的渠道,铺货渠道反向召回刚好能解决问题。用户不存在积攒商品成本问题。

第三点,好的商品内部推荐,或刚到的新鲜蔬菜直接提供,以平台价格直销。平台方只要真正出现优质商品挑选的这个过程加上团长作为副业的直接售卖,线下直销将成为可能。

同时平台方在进行这个过程中存在如下几个问题

  1. 团长供货多少?
  2. 商品损毁率会不会提高?
  3. 其它平台模仿,重新回同一起跑线?
  4. 口碑是否会增加?
  5. 多少商户具备线下直销的能力,影响多大的群体?

探索这几个问题,不难发现一个是对用户提供商品量的问题,一个是用户资质的问题,另外是做这件事情能达到什么样效果的问题。平台方只要设立好完整的规范,如使用评级的方式、线下走访筛选的方式等,都能较好地解决问题。

四、解决方案

根据总的问题与解决方式,可以尝试构建平台迭代的版本,如下。

一期:在平台原有基础上,对某一区域进行试点,推出内测版本,探索直销方案能否增加平台销量,增加利润。

1)用户端

功能

线下团长:优质团长可以申请成为线下团长,获取召回资质。

精选商品:了解最新到达的生鲜产品咨询,以及内部人员评分较高商品的信息。同步可提供线下指导价推荐。

解决团上线下直销,有积货成本以及选货花费大量的时间成本,让线下团长在商品售卖中存在较高的利润,增加收入。

2)平台端

功能

商家审核:审核商家资质、店面水平、日销量等评级,根据商家评级,规范可召回量。

同时根据线下团长的周销量,对评级进行变动。可规定小区线下团长数量,如两名形成竞争,数据较差下团长进行更替。

二期:根据试点区域状况,判断是否推广。

五、总结

“菜篮子”城市居民刚需,随我国城镇化加速,需求量会呈现递增趋势。社区团购乱战之下,各方平台定位都及其相似。

在此局面下寻找破局之法需要从用户生活习惯入手,用户线下买菜的时代都未曾结束。一味地注重于价格竞争,只会获取暂时性的流量,服务化转变才是大局。将社区团购最后环节“团长”的作用放大,通过线下的方式打通用户对于团购商品的难以掌控,提升用户信任与口碑,走服务化方向道路。

 

本文由 @划船不用桨 原创发布于人人都是产品经理,未经许可,禁止转载

题图来自 Unsplash,基于 CC0 协议

使用 acme.sh 免费申请 HTTPS 证书

前言

因为 Google Chrome 和运营商劫持干扰访问者体验的努力推动了大型网站加速应用全站 HTTPS,而 Let’s Encrypt 这个项目通过自动化把配置和维护 HTTPS 变得更加简单,Let’s Encrypt 设计了一个 ACME 协议目前版本是 v2,并在 2018 年支持通配符证书Wildcard Certificate Support is Live。官网主推的客户端是Certbot,任何人都可以基于 ACME 协议实现一个客户端,比如大名鼎鼎的acme.sh。本文主要记录了使用 acme.sh 基于 dns-api 协议生成证书的过程。

更新历史

2020 年 10 月 23 日 – 初稿

阅读原文 – https://wsgzao.github.io/post/acme/


基础知识

关于 HTTPS

引维基百科的说法

超文本传输安全协议(英语:Hypertext Transfer Protocol Secure,缩写:HTTPS)是一种网络安全传输协议。在计算机网络上,HTTPS 经由超文本传输协议进行通信,但利用 SSL/TLS 来对数据包进行加密。HTTPS 开发的主要目的,是提供对网络服务器的身份认证,保护交换数据的隐私与完整性

HTTPS 的主要思想是在不安全的网络上创建一安全信道,并可在使用适当的加密包和服务器证书可被验证且可被信任时,对窃听和中间人攻击提供合理防护。

HTTPS 的信任继承基于预先安装在浏览器中的证书颁发机构(如 Symantec、Comodo、GoDaddy 和 GlobalSign 等)(意即“我信任证书颁发机构告诉我应该信任的”)。因此,一个到某网站的 HTTPS 连接可被信任,当且且当:

  • 用户相信他们的浏览器正确实现了 HTTPS 且安装了正确的证书颁发机构;
  • 用户相信证书颁发机构仅信任合法的网站;
  • 被访问的网站提供了一个有效的证书,意即,它是由一个被信任的证书颁发机构签发的(大部分浏览器会对无效的证书发出警告);
  • 该证书正确地验证了被访问的网站(如,访问 https://example.com 时收到了给 example.com 而不是其他组织的证书);
  • 或者互联网上相关节点是值得信任的,或者用户相信本协议的加密层(TLS 或 SSL)不能被窃听者破坏。

HTTP 和 HTTPS 区别

HTTP 协议传输的数据都是未加密的,也就是明文的,因此使用 HTTP 协议传输隐私信息非常不安全,为了保证这些隐私数据能加密传输,于是网景公司设计了 SSL(Secure Sockets Layer)协议用于对 HTTP 协议传输的数据进行加密,从而就诞生了 HTTPS。简单来说,HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,要比 HTTP 协议安全。

HTTPS 和 HTTP 的区别主要如下:

  • HTTPS 协议需要到 CA 申请证书,一般免费证书较少,因而需要一定费用。
  • HTTP 是超文本传输协议,信息是明文传输,HTTPS 则是具有安全性的 SSL 加密传输协议。
  • HTTP 和 HTTPS 使用的是完全不同的连接方式,用的端口也不一样,前者是 80,后者是 443。
  • HTTP 的连接很简单,是无状态的;HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全。

Types of SSL Certificates for a Secure Business Website

关于 TLS/SSL

传输层安全协议(英语:Transport Layer Security,缩写:TLS),及其前身安全套接层(Secure Sockets Layer,缩写:SSL)是一种安全协议,目的是为互联网通信,提供安全及数据完整性保障

为什么要部署 HTTPS

说到底,就是 HTTPS 更安全。甚至为了安全,一个专业可靠的网站, HTTPS 是必须的。 Firefox 和 Chrome 都计划将没有配置 SSL 加密的 HTTP 网站标记为不安全(貌似 Firefox 50 已经这么干了),目前它们也正在联合其他相关的基金会与公司推动整个互联网 HTTPS 化,现在大家访问的一些主要的网站。如 Google 多年前就已经全部启用 HTTPS ,国内的淘宝、搜狗、知乎、百度等等也全面 HTTPS 了。甚至 Google 的搜索结果也正在给予 HTTPS 的网站更高的排名和优先收录权。

怎么部署 HTTPS

你只需要有一张被信任的 CA ( Certificate Authority )也就是证书授权中心颁发的 SSL 安全证书,并且将它部署到你的网站服务器上。一旦部署成功后,当用户访问你的网站时,浏览器会在显示的网址前加一把小绿锁,表明这个网站是安全的,当然同时你也会看到网址前的前缀变成了 HTTPS ,不再是 HTTP 了。

怎么获得 SSL 安全证书

理论上,我们自己也可以签发 SSL 安全证书,但是我们自己签发的安全证书不会被主流的浏览器信任,所以我们需要被信任的证书授权中心( CA )签发的安全证书。而一般的 SSL 安全证书签发服务都比较贵,比如 Godaddy 、 GlobalSign 等机构签发的证书一般都需要 20 美金一年甚至更贵,不过为了加快推广 HTTPS 的普及, EEF 电子前哨基金会、 Mozilla 基金会和美国密歇根大学成立了一个公益组织叫 ISRG ( Internet Security Research Group ),这个组织从 2015 年开始推出了 Let’s Encrypt 免费证书。这个免费证书不仅免费,而且还相当好用,所以我们就可以利用 Let’s Encrypt 提供的免费证书部署 HTTPS 了

Let’s Encrypt 简介

Let’s Encrypt 是 一个叫 ISRG ( Internet Security Research Group ,互联网安全研究小组)的组织推出的免费安全证书计划。参与这个计划的组织和公司可以说是互联网顶顶重要的先驱,除了前文提到的三个牛气哄哄的发起单位外,后来又有思科(全球网络设备制造商执牛耳者)、 Akamai 加入,甚至连 Linux 基金会也加入了合作,这些大牌组织的加入保证了这个项目的可信度和可持续性。

部署 HTTPS 网站的时候需要证书,证书由 CA 机构签发,大部分传统 CA 机构签发证书是需要收费的,这不利于推动 HTTPS 协议的使用。

Let’s Encrypt 也是一个 CA 机构,但这个 CA 机构是免费的!!!也就是说签发证书不需要任何费用。

Let’s Encrypt 由于是非盈利性的组织,需要控制开支,他们搞了一个非常有创意的事情,设计了一个 ACME 协议,目前该协议的版本是 v1。

那为什么要创建 ACME 协议呢,传统的 CA 机构是人工受理证书申请、证书更新、证书撤销,完全是手动处理的。而 ACME 协议规范化了证书申请、更新、撤销等流程,只要一个客户端实现了该协议的功能,通过客户端就可以向 Let’s Encrypt 申请证书,也就是说 Let’s Encrypt CA 完全是自动化操作的。

任何人都可以基于 ACME 协议实现一个客户端,官方推荐的客户端是 Certbot 。

Let’s Encrypt 通配符证书

在没有出现通配符证书之前,Let’s Encrypt 支持两种证书。

1)单域名证书:证书仅仅包含一个主机。

2)SAN 证书:一张证书可以包括多个主机(Let’s Encrypt 限制是 20),也就是证书可以包含下列的主机:www.example.com、www.example.cn、blog.example.com 等等。

证书包含的主机可以不是同一个注册域,不要问我注册域是什么?注册域就是向域名注册商购买的域名。

对于个人用户来说,由于主机并不是太多,所以使用 SAN 证书完全没有问题,但是对于大公司来说有一些问题:

  • 子域名非常多,而且过一段时间可能就要使用一个新的主机。
  • 注册域也非常多。

读者可以思考下,对于大企业来说,SAN 证书可能并不能满足需求,类似于 sina 这样的网站,所有的主机全部包含在一张证书中,而使用 Let’s Encrypt 证书是无法满足的。

通配符证书就是证书中可以包含一个通配符,比如 .example.com、.example.cn,读者很快明白,大型企业也可以使用通配符证书了,一张证书可以防止更多的主机了。

这个功能可以说非常重要,从功能上看 Let’s Encrypt 和传统 CA 机构没有什么区别了,会不会触动传统 CA 机构的利益呢?

如何申请 Let’s Encrypt 通配符证书

为了实现通配符证书,Let’s Encrypt 对 ACME 协议的实现进行了升级,只有 v2 协议才能支持通配符证书。

也就是说任何客户端只要支持 ACME v2 版本,就可以申请通配符证书了,是不是很激动。

在了解该协议之前有几个注意点:

客户在申请 Let’s Encrypt 证书的时候,需要校验域名的所有权,证明操作者有权利为该域名申请证书,目前支持三种验证方式:

  • dns-01:给域名添加一个 DNS TXT 记录。
  • http-01:在域名对应的 Web 服务器下放置一个 HTTP well-known URL 资源文件。
  • tls-sni-01:在域名对应的 Web 服务器下放置一个 HTTPS well-known URL 资源文件。

而申请通配符证书,只能使用 dns-01 的方式

acme.sh 介绍

acme.sh 实现了 acme 协议, 可以从 letsencrypt 生成免费的证书.

  • 一个纯粹用 Shell(Unix shell)语言编写的 ACME 协议客户端。
  • 完整的 ACME 协议实施。 支持 ACME v1 和 ACME v2 支持 ACME v2 通配符证书
  • 简单,功能强大且易于使用。你只需要 3 分钟就可以学习它。
  • Let’s Encrypt 免费证书客户端最简单的 shell 脚本。
  • 纯粹用 Shell 编写,不依赖于 python 或官方的 Let’s Encrypt 客户端。
  • 只需一个脚本即可自动颁发,续订和安装证书。 不需要 root/sudoer 访问权限。
  • 支持在 Docker 内使用,支持 IPv6

An ACME Shell script: acme.sh

  • An ACME protocol client written purely in Shell (Unix shell) language.
  • Full ACME protocol implementation.
  • Support ACME v1 and ACME v2
  • Support ACME v2 wildcard certs
  • Simple, powerful and very easy to use. You only need 3 minutes to learn it.
  • Bash, dash and sh compatible.
  • Purely written in Shell with no dependencies on python or the official Let’s Encrypt client.
  • Just one script to issue, renew and install your certificates automatically.
  • DOES NOT require root/sudoer access.
  • Docker friendly
  • IPv6 support
  • Cron job notifications for renewal or error etc.

It’s probably the easiest & smartest shell script to automatically issue & renew the free certificates from Let’s Encrypt.

Wiki: https://github.com/acmesh-official/acme.sh/wiki

安装 acme.sh

安装很简单, 一个命令:

1
curl  https://get.acme.sh | sh

 

普通用户和 root 用户都可以安装使用.
安装过程进行了以下几步:

1) 把 acme.sh 安装到你的 home 目录下:

1
~/.acme.sh/

并创建 一个 bash 的 alias, 方便你的使用: alias acme.sh=~/.acme.sh/acme.sh

2). 自动为你创建 cronjob, 每天 0:00 点自动检测所有的证书, 如果快过期了, 需要更新, 则会自动更新证书.

更高级的安装选项请参考: https://github.com/Neilpang/acme.sh/wiki/How-to-install

安装过程不会污染已有的系统任何功能和文件 , 所有的修改都限制在安装目录中: ~/.acme.sh/

生成证书

acme.sh 实现了 acme 协议支持的所有验证协议.
一般有两种方式验证: http 和 dns 验证.

  1. http 方式需要在你的网站根目录下放置一个文件, 来验证你的域名所有权, 完成验证. 然后就可以生成证书了.
1
acme.sh  --issue  -d mydomain.com -d www.mydomain.com  --webroot  /home/wwwroot/mydomain.com/

只需要指定域名, 并指定域名所在的网站根目录. acme.sh 会全自动的生成验证文件, 并放到网站的根目录, 然后自动完成验证. 最后会聪明的删除验证文件. 整个过程没有任何副作用.

如果你用的 apache 服务器, acme.sh 还可以智能的从 apache 的配置中自动完成验证, 你不需要指定网站根目录:

1
acme.sh --issue  -d mydomain.com   --apache

 

如果你用的 nginx 服务器, 或者反代, acme.sh 还可以智能的从 nginx 的配置中自动完成验证, 你不需要指定网站根目录:

1
acme.sh --issue  -d mydomain.com   --nginx

 

注意, 无论是 apache 还是 nginx 模式, acme.sh 在完成验证之后, 会恢复到之前的状态, 都不会私自更改你本身的配置. 好处是你不用担心配置被搞坏, 也有一个缺点, 你需要自己配置 ssl 的配置, 否则只能成功生成证书, 你的网站还是无法访问 https. 但是为了安全, 你还是自己手动改配置吧.

如果你还没有运行任何 web 服务, 80 端口是空闲的, 那么 acme.sh 还能假装自己是一个 webserver, 临时听在 80 端口, 完成验证:

1
acme.sh  --issue -d mydomain.com   --standalone

更高级的用法请参考: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert

  1. 手动 dns 方式, 手动在域名上添加一条 txt 解析记录, 验证域名所有权.

这种方式的好处是, 你不需要任何服务器, 不需要任何公网 ip, 只需要 dns 的解析记录即可完成验证.
坏处是,如果不同时配置 Automatic DNS API,使用这种方式 acme.sh 将无法自动更新证书,每次都需要手动再次重新解析验证域名所有权。

1
acme.sh  --issue  --dns   -d mydomain.com

然后, acme.sh 会生成相应的解析记录显示出来, 你只需要在你的域名管理面板中添加这条 txt 记录即可.

等待解析完成之后, 重新生成证书:

1
acme.sh  --renew   -d mydomain.com

 

注意第二次这里用的是 --renew

dns 方式的真正强大之处在于可以使用域名解析商提供的 api 自动添加 txt 记录完成验证.

acme.sh 目前支持 cloudflare, dnspod, cloudxns, godaddy 以及 ovh 等数十种解析商的自动集成.

以 dnspod 为例, 你需要先登录到 dnspod 账号, 生成你的 api id 和 api key, 都是免费的.
然后:

1
2
3
4
5
export DP_Id="1234"

export DP_Key="sADDsdasdgdsf"

acme.sh   --issue   --dns dns_dp   -d aa.com  -d www.aa.com

 

证书就会自动生成了. 这里给出的 api id 和 api key 会被自动记录下来, 将来你在使用 dnspod api 的时候, 就不需要再次指定了.
直接生成就好了:

1
acme.sh  --issue   -d  mydomain2.com   --dns  dns_dp

更详细的 api 用法: https://github.com/Neilpang/acme.sh/blob/master/dnsapi/README.md

copy / 安装 证书

前面证书生成以后, 接下来需要把证书 copy 到真正需要用它的地方.

注意, 默认生成的证书都放在安装目录下: ~/.acme.sh/, 请不要直接使用此目录下的文件, 例如: 不要直接让 nginx/apache 的配置文件使用这下面的文件. 这里面的文件都是内部使用, 而且目录结构可能会变化.

正确的使用方法是使用 --install-cert 命令, 并指定目标位置, 然后证书文件会被 copy 到相应的位置,
例如:

1
2
3
4
5
6
7
# Apache example:

acme.sh --install-cert -d example.com \
--cert-file      /path/to/certfile/in/apache/cert.pem  \
--key-file       /path/to/keyfile/in/apache/key.pem  \
--fullchain-file /path/to/fullchain/certfile/apache/fullchain.pem \
--reloadcmd     "service apache2 force-reload"
1
2
3
4
5
6
# Nginx example:

acme.sh --install-cert -d example.com \
--key-file       /path/to/keyfile/in/nginx/key.pem  \
--fullchain-file /path/to/fullchain/nginx/cert.pem \
--reloadcmd     "service nginx force-reload"

(一个小提醒, 这里用的是 service nginx force-reload, 不是 service nginx reload, 据测试, reload 并不会重新加载证书, 所以用的 force-reload)

Nginx 的配置 ssl_certificate 使用 /etc/nginx/ssl/fullchain.cer ,而非 /etc/nginx/ssl/<domain>.cer ,否则 SSL Labs 的测试会报 Chain issues Incomplete 错误。

--install-cert 命令可以携带很多参数, 来指定目标文件. 并且可以指定 reloadcmd, 当证书更新以后, reloadcmd 会被自动调用, 让服务器生效.

详细参数请参考: https://github.com/Neilpang/acme.sh#3-install-the-issued-cert-to-apachenginx-etc

值得注意的是, 这里指定的所有参数都会被自动记录下来, 并在将来证书自动更新以后, 被再次自动调用.

更新证书

目前证书在 60 天以后会自动更新, 你无需任何操作. 今后有可能会缩短这个时间, 不过都是自动的, 你不用关心.

更新 acme.sh

目前由于 acme 协议和 letsencrypt CA 都在频繁的更新, 因此 acme.sh 也经常更新以保持同步.

升级 acme.sh 到最新版 :

1
acme.sh --upgrade

 

如果你不想手动升级, 可以开启自动升级:

1
acme.sh  --upgrade  --auto-upgrade

之后, acme.sh 就会自动保持更新了.

你也可以随时关闭自动更新:

1
acme.sh --upgrade  --auto-upgrade  0

出错怎么办

如果出错, 请添加 debug log:

1
acme.sh  --issue  .....  --debug

或者:

1
acme.sh  --issue  .....  --debug  2

 

请参考: https://github.com/Neilpang/acme.sh/wiki/How-to-debug-acme.sh

关于 Nginx 证书配置的解释

文件名 内容
cert.pem 服务端证书
chain.pem 浏览器需要的所有证书但不包括服务端证书,比如根证书和中间证书
fullchain.pem 包括了 cert.pem 和 chain.pem 的内容
privkey.pem 证书的私钥

Nginx 官方是这么介绍的

Some browsers may complain about a certificate signed by a well-known certificate authority, while other browsers may accept the certificate without issues. This occurs because the issuing authority has signed the server certificate using an intermediate certificate that is not present in the certificate base of well-known trusted certificate authorities which is distributed with a particular browser. In this case the authority provides a bundle of chained certificates which should be concatenated to the signed server certificate. The server certificate must appear before the chained certificates in the combined file…

SSL Certificate Chains

Let’s encrypt 提供的 fullchain.pem 文件,其实是把 cert.pem 和 chain.pem 文件粘贴到了一起。如果 cert.pem 出于某种原因不被认可,那么 chain.pem 文件就可以解围。因此在 ssl_certificate 的配置中使用 fullchain.pem 确实更为合适。

不过经过我在 https://www.ssllabs.com/ 上的测试,各大平台完全支持使用 cert.pem,ssllabs 给出的测试结果里就会少一条 warning:“This server’s certificate chain is incomplete. ”

nginx.conf 配置文件的修改

你需要再加上一个 server 条目用于 HTTPS 服务。改完之后的结果是这个样子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
http {
    server {
        listen 80; 
        listen [::]:80;
        server_name example.com www.example.com;
        location / {
            root /var/www/html;
        }
        ...
    }
    server {
        listen 443 ssl;
        server_name example.com www.example.com;
        ssl_certificate /etc/letsencrypt/live/example.com/cert.pem;
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
        location / {
            root /var/www/html;
        }
        ...
    }
    ...
}

HTTPS 跳转

现在,你的服务器同时接受 HTTP 和 HTTPS 请求。如果你希望只受理 HTTPS 请求,可以在 nginx.conf 中添加一个 301 跳转规则,告知浏览器将 HTTP 变成 HTTPS.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
http {
    server {
        listen 80; 
        listen [::]:80;
        server_name example.com www.example.com;
        return 301 https://$server_name$request_uri;
    }
    server {
        listen 443 ssl;
        server_name example.com www.example.com;
        ssl_certificate /etc/letsencrypt/live/example.com/cert.pem;
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
        location / {
            root /var/www/html;
        }
        ...
    }
    ...
}

你也可以参考NGINXConfig

The easiest way to configure a performant, secure, and stable NGINX server.

参考文章

Let’s Encrypt Documentation

acme.sh

使用 acme.sh 管理 Let’s Encrypt Wildcard SSL 证书

Getting started with acme.sh Let’s Encrypt SSL client

国外产品经理相关的网站推荐

除了facebook,twitter、youtube,产品经理们浏览国外网站还能看些啥呢?今天找了几个国外产品经理经常浏览的网址,给那些总是对国外产品经理充满好奇心的小伙伴们提供些参考,内容基本自己组织的,如有不合适的请多批评指教。

1 http://www.svpg.com/(国内产品经理提到较多的网站)

硅谷产品集团(SVPG)由来自硅谷的四位行业的资深人士创办的,他们都曾主导产品的开发工作并担任过高管职位,网站内容主要是分享成功和失败的产品教训,并为企业提供产品策略、产品管理、产品设计、产品开发、用户体验、产品架构和产品营销相关的信息

2 https://www.productschool.com/

产品学堂,产品经理收费培训机构,包括线上和线下,类似国内的产品100吧。

3 http://www.techmeme.com/

Techmeme 是一个科技新闻聚合网站Techmeme是一个能生成,过滤,存档实时新闻概要的页面,并展开话题讨论,主要以科技话题为主,帮助人们了解科技产业发展情况。

4 https://medium.com/

Medium 是一个博客发布平台,由 Twitter 联合创办人埃文·威廉姆斯比兹·斯通2012年8月创办。Medium 于网上编辑时提供一个所见即所得的用户界面,以及提供文字格式设置。当文章发布后,它可以被其他人推荐或分享,类似社交平台 Twitter。Medium强调用主题为核心的内容组织方式来聚合内容、用用户投票的形式进一步精选内容、用精美的网站模版来加强页面的结构化。据说国内的十五言、简书、蒹葭是模仿丰medium。

5 https://www.quora.com/

不说了,知乎跟他很相似,在上面能搜到很多相关product management的话题,有兴趣的小伙伴可以看一下。

6 http://www.sachinrekhi.com/

个人博客网站,Sachin是一个在硅谷工作十多年的产品经理和创业者,他爱分享产品管理和创业相关的经验教训,文章质量不错,更新也快,值得看。

7 https://www.productmanagerhq.com/

社区性质,课程学习,注重初级产品经理的成长体系。当然也有一些产品经理相关的免费干货。

8 http://www.mindtheproduct.com/

“一个了解同行的产品管理、与产品人沟通最佳的途径”,包括产品经理博客内容、视频、话题的分享,也包括产品经理线下活动的组织和课程的培训等。

9 http://www.creativebloq.com/

创意blog,艺术设计灵感的来源,包括平面设计、网页设计、插图、艺术、3d、数码艺术等的建议、点评、灵感等内容,是设计师设计创意群体学习、交流的平台。

10 https://www.lynda.com/

在线视频课程学习平台,可免费学习10天,有商业、设计和信息技术三大板块,信息技术中包括大数据、软件开发、网站开发等方面,我看了一下用户体验相关的视频,有视频还配有文字。

11 https://www.smashingmagazine.com/

提供代码、网站设计、移动端设计、图片处理、UX设计等相关的书籍、电子书、线下交流活动及互联网招聘信息。

12 https://www.usability.gov/

官方标语是改善用户体验。为用户提供改善用户体验的工具和方法策略,创建有用、易用的网站。网站内容涉及用户体验的基础知识,内容策略、项目管理、视觉设计相关的内容,以及改善用户体验的方法和工具。

产品经理常用的网站

作为一个产品经理,经常逛一些内容好的网站,或使用一些效率高的工具,往往能够少走很多弯路,成长更快一点。为此,我整理了一些互联网产品经理常用的一些网站。

介绍一个产品经理导航网站:PMhome123(产品经理之家 | 产品经理网址导航 | 优质网址入口),是一个比较干净简洁的垂直导航之家。

1.产品社区类

人人都是产品经理(人人都是产品经理 | 产品经理、产品爱好者学习交流平台) 最活跃的产品经理交流社区

PMCaff(人人都是产品经理 | 产品经理、产品爱好者学习交流平台) 最专业的产品经理交流社区

产品100(产品壹佰 | 最真实的产品经理学习网站) 最真实的产品经理学习网站

产品邦(产品邦-互联网产品经理门户网站 – 产品邦 – 帮助产品经理成长) 需求文档、Axure部件库、设计规范文档

产品中国(pmtoo.com/) 报道国内外互联网创业公司,评论产品

互联网早读(互联网早读课丨提倡慢阅读丨专注产品设计丨交互体验丨用户研究) 提倡慢阅读,专注交互体验、用户研究

2.资讯新闻

虎嗅网(虎嗅网) 捕获互联网每个重要时刻

36kr(36氪 | 让创业更简单) 互联网最新最热新闻资讯

爱范儿(爱范儿 · 让未来触手可及) 国内聚焦创新和科技领域的第一媒体

钛媒体(钛媒体_引领未来商业与生活新知) 最具影响力的科技消费生态服务商

雷锋网(雷锋网_读懂智能&未来) 国内最早关注人工智能和智能硬件

品玩(PingWest 品玩-有品好玩的科技,一切与你有关) 关注科技创新、数字文化、有趣资讯

梅花网(梅花网-营销者的信息中心) 营销专业领域内容最丰富网站

投资界(投资界_中国创业与投资专业门户_清科旗下网站) 创业与投资第一门户

3.数据分析

谷歌趋势(trends.google.com/trend) 了解世界各地的用户在搜索什么

Similarweb(similarweb.com/) 查看全球网站排名和流量分析

西瓜数据(西瓜数据-公众号大数据服务商) 微信公众号数据分析

友盟(【友盟+】全球领先的第三方全域数据服务商) App统计和分析工具

谷歌统计(marketingplatform.google.com) 全球最大的网站统计和分析工具

百度指数(百度指数) 以百度海量网民行为数据为基础

AppAnnie(App Annie -应用分析和应用数据业界标杆) 全球领先的 App 市场数据供应商

七麦数据ASO100(七麦数据(原ASO100)- 专业移动推广数据分析平台 – ASO优化|ASM竞价广告|iOS|App Store|苹果应用商店|安卓市场|榜单) 中国专业的移动推广数据分析平台

4.行业报告

易观智库(易观 – 数据驱动精益成长) 数据分析平台,涵盖行业报告、APP数据等

艾瑞网(艾瑞网_互联网数据资讯聚合平台) 互联网数据资讯聚合平台

199IT(互联网数据资讯中心-199IT | 发现数据的价值-199IT | 中文互联网数据研究资讯中心-199IT) 中文互联网数据资讯中心

企鹅智库(企鹅智酷) 深度研究性栏目,洞见科技背后的商业逻辑

移动观象台(TalkingData移动观象台-全球领先免费公开数据实时查询平台) 权威的互联网行业报告

IT桔子(itjuzi.com/) 查公司数据,查行业报告,上桔子

前瞻数据(前瞻网 – 发现趋势 预见未来) 中国行业数据中心,数据分析

手游那些事儿(手游那点事 | 关注手机游戏运营和手机游戏推广) 关于手游的行业报告平台

5.常用工具

Xmind(xmind.net/) 国产思维导图软件

百度脑图(百度脑图 – 便捷的思维工具) 一款完全免费的在线脑图

Teambition(Teambition | Team Collaboration Solutions) 国产团队协作工具,看板式管理

伙伴办公(伙伴办公(原“伙伴云表格”) – 全流程定制) 强大的在线表格工具,拥有海量模板

石墨文档(石墨文档) 小而美的在线文档和表格工具

腾讯问卷(腾讯问卷 – 免费好用的问卷调查系统,调查问卷,免费,简单,模板) 腾讯出品的免费简约问卷系统

吐个槽(吐个槽-免费便捷的用户意见反馈服务平台) 腾讯出品用户反馈收集平台

产品大牛(产品大牛 | 让产品工作更简单,原型托管,在线PRD,需求池) 启发产品、设计工作方式的协作平台

天眼查(tianyancha.com/) 公司靠不靠谱就用天眼查搜一下

站长之家(站长工具 – 站长之家) SEO分析工具、网站信息查询

Alexa中国(Alexa中国_网站流量全球综合排名_中文网站排行榜) 网站流量全球综合排名,网站排行榜

6.原型设计

Axure原创教程(Axure原创教程网专业 • 认真 • 原创 • 分享) 小楼教你用Axure画原型图

Axure下载(Prototypes, Specifications, and Diagrams in One Tool) 最强大的原型图设计软件

墨刀教程(墨刀 – Tutorials) 墨刀官方教程

墨刀(墨刀 – 强大易用的原型设计与协同工具) 国产原型设计工具

Sketch中文教程(Sketch中文用户手册) Sketch官方中文教程

Sketch(The digital design toolkit) 简单、高效的UI设计软件

Mockplus教程(| 摹客,让设计更快更简单) Mockplus官方教程

Mockplus(摹客(Mockplus),让设计更快更简单) 优秀的快速原型设计工具和平台提供者

7.设计规范

苹果设计规范(设计 – Apple Developer) IOS应用的设计规范

谷歌设计规范(Introduction) 适用于网页设计跟APP设计

蚂蚁设计规范(A UI Design Language) 阿里巴巴蚂蚁金服的设计规范

优设网(优秀设计联盟-SDC-优设网-设计师交流学习平台-听讲座,聊设计,找素材,尽在优设网) 设计师交流学习平台

UI中国(UI中国-专业用户体验设计平台) 人气最高的图形界面交互设计平台

安卓设计规范(developer.android.com/d) 安卓官方设计指南

微软设计指南(Microsoft Design) 微软官方设计指南

屏幕尺寸对照表(material.io/devices/) 谷歌出品的各种设备屏幕对照表

8.名人博客

纯银(纯银V – 简书) 前网易网站产品部总监,文艺范~

唐杰(互联网产品经理@唐杰) 《杰出产品经理》作者

苏杰(人人都是产品经理–iamsujie) 《人人都是产品经理》作者

网易GUX(gux.163.com/) 网易游戏用户体验中心

百度MUX(百度用户体验中心) 百度官方用户体验中心

京东JDC(JDC | 京东设计中心) 京东官方用户体验中心

腾讯CDC(腾讯CDC) 腾讯CDC官方博客

阿里UED博客(阿里巴巴(中国站)用户体验设计部博客U一点设计 UED团队) 阿里官方UED博客

9.学习平台

三节课(三节课-专注于“能力提升”的互联网人在线大学【官网】-三节课) 专注PM、产品运营新人成长

起点学院(起点学院-产品经理培训 | 产品经理培训课程 | 致力于为IT从业者提供专业、系统的产品经理学习服务,目标成为产品经理黄埔军校) 产品经理的起点学院

网易公开课(网易公开课) 免费观看国内外名校公开课视频

W3Schools(w3schools.com/) 全球最大的Web开发学习平台

W3School中国(w3school 在线教程) 中国最大的Web在线学习平台

 

WordPress 直接保存图片到本地服务器 代码实现

前几天,突然收到七牛的一封邮件“[七牛云服务] 账户冻结预警”,内容如下图:

WordPress 实现文章中远程(外链)图片自动本地化的方法 wordpress

账户余额为负,威胁冻结服务…说好的免费服务呢~

之前用的贴图库,不过貌似已经死了…现在七牛又来这么一出,看来这些外链图床不是很靠谱!在 112 兄那发现了一个外链图片本地化的方法,觉得非常不错,为以后的摆脱外链图床做准备!

WordPress 文章中远程图片自动保存到本地服务器,最大的好处就是复制粘贴,方便对文章的转载复制。就现实中而言,并不是所有网站都会全部写原创内容或者想伪原创一下,有些好文章想转载,但是对方网站有可能对图片进了防盗链,而图文比较多的情况下,那么就相当麻烦,而此代码功能可有效的帮助你转载复制。不过,无论载转与否,建议保留出处,这是对原作者的尊重,毕竟人家写得辛苦,编辑也不容易。假使是你自己的原创作品,被人盗去并盗用了作者出处,你一定也会很恼火的。所以,将心比心。

好了,话不多说,进入正题。虽说有不少插件能实现这个功能,比如《WordPress 直接粘贴图片到文章编辑器插件 Imagepaste》,但是,有可能插件太多了,会影响网站的性能或者拖累服务器,降低网站的运行速度。但凡能代码实现的,我们都比较推荐使用代码,集成到 wordpress 主题功能中。

实现方法:

复制下面的代码,然后粘贴到你当前 WordPress 主题的模版函数(functions.php)文件中保存即可。

//自动本地化外链图片
add_filter('content_save_pre', 'auto_save_image');
function auto_save_image($content) {
    $upload_path = '';
    $upload_url_path = get_bloginfo('url');
    //上传目录
    if (($var = get_option('upload_path')) !=''){
        $upload_path = $var;
    } else {
        $upload_path = 'wp-content/uploads';
    }
    if(get_option('uploads_use_yearmonth_folders')) {
        $upload_path .= '/'.date("Y",time()).'/'.date("m",time());
    }
    //文件地址
    if(($var = get_option('upload_url_path')) != '') {
        $upload_url_path = $var;
    } else {
        $upload_url_path = bloginfo('url');
    }
    if(get_option('uploads_use_yearmonth_folders')) {
        $upload_url_path .= '/'.date("Y",time()).'/'.date("m",time());
    }
    require_once ("../wp-includes/class-snoopy.php");
    $snoopy_Auto_Save_Image = new Snoopy;
    $img = array();
    //以文章的标题作为图片的标题
    if ( !empty( $_REQUEST['post_title'] ) )
        $post_title = wp_specialchars( stripslashes( $_REQUEST['post_title'] ));
    $text = stripslashes($content);
    if (get_magic_quotes_gpc()) $text = stripslashes($text);
    preg_match_all("/ src=(\"|\'){0,}(http:\/\/(.+?))(\"|\'|\s)/is",$text,$img);
    $img = array_unique(dhtmlspecialchars($img[2]));
    foreach ($img as $key => $value){
        set_time_limit(180); //每个图片最长允许下载时间,秒
        if(str_replace(get_bloginfo('url'),"",$value)==$value&&str_replace(get_bloginfo('home'),"",$value)==$value){
            //判断是否是本地图片,如果不是,则保存到服务器
            $fileext = substr(strrchr($value,'.'),1);
            $fileext = strtolower($fileext);
            if($fileext==""||strlen($fileext)>4)
                $fileext = "jpg";
            $savefiletype = array('jpg','gif','png','bmp');
            if (in_array($fileext, $savefiletype)){
                if($snoopy_Auto_Save_Image->fetch($value)){
                    $get_file = $snoopy_Auto_Save_Image->results;
                }else{
                    echo "error fetching file: ".$snoopy_Auto_Save_Image->error."<br>";
                    echo "error url: ".$value;
                    die();
                }
                $filetime = time();
                $filepath = "/".$upload_path;//图片保存的路径目录
                !is_dir("..".$filepath) ? mkdirs("..".$filepath) : null;
                //$filename = date("His",$filetime).random(3);
                $filename = substr($value,strrpos($value,'/'),strrpos($value,'.')-strrpos($value,'/'));
                //$e = '../'.$filepath.$filename.'.'.$fileext;
                //if(!is_file($e)) {
                // copy(htmlspecialchars_decode($value),$e);
                //}
                $fp = @fopen("..".$filepath.$filename.".".$fileext,"w");
                @fwrite($fp,$get_file);
                fclose($fp);
                $wp_filetype = wp_check_filetype( $filename.".".$fileext, false );
                $type = $wp_filetype['type'];
                $post_id = (int)$_POST['temp_ID2'];
                $title = $post_title;
                $url = $upload_url_path.$filename.".".$fileext;
                $file = $_SERVER['DOCUMENT_ROOT'].$filepath.$filename.".".$fileext;
                //添加数据库记录
                $attachment = array(
                    'post_type' => 'attachment',
                    'post_mime_type' => $type,
                    'guid' => $url,
                    'post_parent' => $post_id,
                    'post_title' => $title,
                    'post_content' => '',
                );
                $id = wp_insert_attachment($attachment, $file, $post_parent);
                $text = str_replace($value,$url,$text); //替换文章里面的图片地址
            }
        }
    }
    $content = AddSlashes($text);
    remove_filter('content_save_pre', 'auto_save_image');
    return $content;
}
function mkdirs($dir){
    if(!is_dir($dir)){
        mkdirs(dirname($dir));
        mkdir($dir);
    }
    return ;
}
function dhtmlspecialchars($string) {
    if(is_array($string)) {
        foreach($string as $key => $val) {
            $string[$key] = dhtmlspecialchars($val);
        }
    }else{
        $string = str_replace('&', '&', $string);
        $string = str_replace('"', '"', $string);
        $string = str_replace('<', '<', $string);
        $string = str_replace('>', '>', $string);
        $string = preg_replace('/&(#\d;)/', '&\1', $string);
    }
    return $string;
}

—- 取自boke112

以后你发表文章时就不用去管文章中的外链图片了,因为上面的代码会自动将文章中包含的外链图片自动保存到本地,是不是很方便的 wordpress 技巧呀。

使用GoAdmin极速搭建golang应用管理后台

GoAdmin介绍

GoAdmin是一个基于golang的数据可视化后台搭建框架,内置了管理后台的rbac权限系统,登录以及一个crud逻辑与视图生成的插件。支持不同主题更换,支持添加插件形式添加不同应用进行功能扩展。

官网:https://www.go-admin.cn
github地址:https://github.com/GoAdminGro…
在线demo:https://demo.go-admin.cn
文档地址:https://book.go-admin.cn/zh

上手

这里直接实战介绍如何上手,最小化的实现一个真实应用的数据管理后台。我从github搜索到了这样一个golang的web例子:eddycjy/go-gin-example,我们以这个简单例子为例来搭建这个应用的管理后台。

准备工作

第一步

首先,把这个应用的sql导入进数据库:

第二步

接着我们安装一下GoAdmin的命令行工具:

GO111MODULE=on GOPROXY=https://goproxy.cn go install github.com/GoAdminGroup/go-admin/adm

注意:这里使用了go module的方式加载依赖,不了解go module的话先百度一下。同时设置了代理,加快依赖的下载。

安装完后,你应该可以在mac或linux的终端或windows的cmd成功执行以下命令:

> adm -V
GoAdmin CLI v1.2.7

注意:如果不成功,检查一下你是否有将$GOPATH/bin这个路径加入到你的环境变量路径中,如果你不知道什么是环境变量路径可以百度一下先,再进行后面步骤

第三步

导入GoAdmin所需的sql文件进数据库中。

到这里准备工作完毕,开始写代码。

生成数据模型文件

我们在任意位置创建我们的项目文件夹,比如叫:go-gin-example-admin,然后进入文件夹中,执行以下命令:

> adm generate

接着会出现几个菜单,首先让你选择数据库驱动,我们用的是mysql,因此选择mysql,按回车进行选择。

? choose a driver  [Use arrows to move, type to filter, enter to select]
> mysql
  postgresql
  sqlite
  mssql

然后我们填写好对应的数据信息:

? choose a driver mysql
? sql address 127.0.0.1
? sql port 3306
? sql username root
? sql password ****
? sql database name gin-example-blogs

接着选择要管理的表格,我们按空格选择全部,然后回车:

? choose table to generate  [Use arrows to move, space to select, type to filter]
> [x]  [select all]
  [ ]  blog_article
  [ ]  blog_auth
  [ ]  blog_tag

接着设置好文件的包名,数据模型文件的对应数据库连接名(默认是default)按回车即可,以及输出路径,我们都直接回车使用默认值。

? set package name main
? set connection name default
? set file output path ./

然后就可以看到在文件夹下生成了几个文件:

.
├── tables.go
├── blog_article.go
├── blog_auth.go
└── blog_tag.go

编写main.go

生成完数据模型文件后,我们在文件夹下创建main.go,内容如下:

package main

import (
    _ "github.com/GoAdminGroup/go-admin/adapter/gin"               // 适配器
    _ "github.com/GoAdminGroup/go-admin/modules/db/drivers/mysql" // sql 驱动
    _ "github.com/GoAdminGroup/themes/adminlte"                    // ui主题

    "github.com/GoAdminGroup/go-admin/engine"
    "github.com/GoAdminGroup/go-admin/examples/datamodel"
    "github.com/GoAdminGroup/go-admin/modules/config"
    "github.com/GoAdminGroup/go-admin/modules/db"
    "github.com/GoAdminGroup/go-admin/modules/language"
    "github.com/GoAdminGroup/go-admin/template"
    "github.com/GoAdminGroup/go-admin/template/chartjs"
    "github.com/gin-gonic/gin"
    "io/ioutil"
)

func main() {
    r := gin.Default()

    gin.SetMode(gin.ReleaseMode)
    gin.DefaultWriter = ioutil.Discard

    eng := engine.Default()

    template.AddComp(chartjs.NewChart())

    if err := eng.AddConfig(config.Config{
        Databases: config.DatabaseList{
            "default": {
                Host:       "127.0.0.1",
                Port:       "3306",
                User:       "root",
                Pwd:        "root",
                Name:       "gin-example-blogs",
                MaxIdleCon: 50,
                MaxOpenCon: 150,
                Driver:     db.DriverMysql,
            },
        },
        UrlPrefix: "admin",
        IndexUrl:  "/",
        Debug:     true,
        Language:  language.CN,
    }).
        AddGenerators(Generators).
        Use(r); err != nil {
        panic(err)
    }

    r.Static("/uploads", "./uploads")

    eng.HTML("GET", "/admin", datamodel.GetContent)

    _ = r.Run(":9033")
}

这里简单的解释一下:我们实例化了一个GoAdmin引擎对象eng,然后调用AddConfig方法传入配置,然后使用AddGenerators方法传入数据模型文件,接着调用Use挂载到gin框架上面。

现在我们尝试运行一下:

> go run .

[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

GoAdmin is now running.
Running in "debug" mode. Switch to "release" mode in production.

看到GoAdmin is now running意味着运行成功了,接着我们访问一下:http://localhost:9033/admin/login

可以看到已经运行起来:

默认账号密码都是:admin

设置菜单与介绍数据模型文件

登录进去后,访问菜单设置页,我们需要设置一下菜单,才能从菜单进入我们的表格管理页面。这时我们需要看一下我们文件夹下的tables.go文件。

package main

import "github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"

// The key of Generators is the prefix of table info url.
// The corresponding value is the Form and Table data.
//
// http://{{config.Domain}}:{{Port}}/{{config.Prefix}}/info/{{key}}
//
// example:
//
// "blog_article" => http://localhost:9033/admin/info/blog_article
// "blog_auth" => http://localhost:9033/admin/info/blog_auth
// "blog_tag" => http://localhost:9033/admin/info/blog_tag
//
// example end
//
var Generators = map[string]table.Generator{
    "blog_article": GetBlogArticleTable,
    "blog_auth":    GetBlogAuthTable,
    "blog_tag":     GetBlogTagTable,

    // generators end
}

这个文件声明了一个map变量,这个变量的key是我们数据表管理路由的前缀,对应的值就是我们的数据模型生成函数。所以我们明白了,我们菜单需要设置的地址:

新建完成对应的几个菜单后,我们强制刷新一下页面,就可以看到左边已经出现了对应的菜单:

我们点博客用户的菜单,进入用户的管理页面。

我们想要对这个页面进行一些设置,比方说页面的标题Blog_auth,页面表格的表头字段名。这时我们需要改生成的数据模型文件,点开文件夹下的文件blog_auth.go,我们将其如下改动:

package main

import (
    "github.com/GoAdminGroup/go-admin/context"
    "github.com/GoAdminGroup/go-admin/modules/db"
    "github.com/GoAdminGroup/go-admin/plugins/admin/modules/table"
    "github.com/GoAdminGroup/go-admin/template/types/form"
)

func GetBlogAuthTable(ctx *context.Context) table.Table {

    blogAuthTable := table.NewDefaultTable(table.DefaultConfigWithDriver("mysql"))

    info := blogAuthTable.GetInfo()

    info.AddField("ID", "id", db.Int).FieldFilterable()
    info.AddField("用户名", "username", db.Varchar)
    info.AddField("密码", "password", db.Varchar)

    info.SetTable("blog_auth").SetTitle("博客用户").SetDescription("博客用户")

    formList := blogAuthTable.GetForm()

    formList.AddField("ID", "id", db.Int, form.Default).FieldNotAllowAdd()
    formList.AddField("用户名", "username", db.Varchar, form.Text)
    formList.AddField("密码", "password", db.Varchar, form.Password)

    formList.SetTable("blog_auth").SetTitle("博客用户").SetDescription("博客用户")

    return blogAuthTable
}

然后重新运行程序,再访问一下博客用户的管理页面,可以看到标题等内容已经被改变:

是不是很简单~这样就完成了数据表最基础的管理后台的搭建。更多细节,留待大家在文档中去发现!

像写 Vue 3 一样写小程序

Vue 3 带来了许多令人兴奋的新特性,这其中最令人期待的莫过于 Composition API,它带来了更灵活的代码组织方式,更好的 TS 支持,以及更强大的逻辑复用能力。如今 Vue 3 已经发布半年有余,各项周边配套也已基本完善,可以说它已经准备好被用于生产了。你或许早已摩拳擦掌跃跃欲试,但作为国内开发者你却可能面临着一个很尴尬的处境,那就是你的主要工作也许是写小程序,这一切令人激动的消息好像与你并没有什么关系。但是如果你能用 Vue 3 的 Composition API 写小程序呢?

介绍

向大家隆重介绍 Vue Mini,Vue Mini 是一个基于 Vue 3 的小程序库,它能让你用 Composition API 写小程序。Vue Mini 底层直接且仅依赖 Vue 3 的响应式核心 @vue/reactivity,它完整继承了 Vue 3 的响应式数据,并且设计了与 Composition API 一模一样的 API,当然也拥有一模一样的能力。用 Vue Mini 写小程序就像在写 Vue 3 一样。

动机

小程序本就自带一个框架,为什么还要大费周章的移植别的框架呢?这是因为小程序自身的框架在几年前或许还差强人意,但如今却很难让人满意了。现在 TS 越来越受欢迎,而小程序自身对 TS 的支持却很羸弱。并且当小程序组件变大时,你也不能按逻辑关注点来组织代码,这可能会导致组件难以阅读和理解。最后小程序提供的逻辑复用方案 Behavior 其实就是 Mixin,存在着与 Mixin 相同的限制,例如容易发生冲突,不能接收参数等。而这些问题恰巧也是 Vue 3 添加 Composition API 所解决的问题。所以 Vue Mini 将 Composition API 移植到小程序,从而解决了这些问题。它提供了非常好的 TS 支持,并且有很好的类型推导,很多时候你并不需要手写类型注解。你可以按逻辑关注点来组织代码,让复杂的组件更容易阅读和理解。你也能将任意逻辑提取为一个组合函数,你可以在任何组件中调用组合函数,你也可以向组合函数传递任何参数来改变它的逻辑,这大大提高了抽象逻辑的灵活性。

Dogfooding

Vue Mini 其实已经在我们公司内部孵化一年多的时间了,它已经先后被用于创新项目和正式的生产项目。之所以最近才开始宣传是因为在经过真实生产项目验证之前,我并不能确定它是可行的,所以过去一年我都在 Dogfooding(使用自己的产品)。非常幸运,在团队小伙伴的信任和支持下,我们用 Vue Mini 写的第一个生产小程序最近已经成功上线了,最新上线的版本一共包含 58 个页面,几乎全都是用 Vue Mini 写的。我现在十分确定 Vue Mini 是可行的,在经过生产项目打磨后,它的质量已经比较可靠了,API 也比较稳定了。我认为现在是时候让更多的人知道它的存在了,它或许也能解决你的一些问题。如果你对 Vue Mini 感兴趣,想将其引入到你个人或工作的小程序中,一个保险也更具说服力的做法是先在小程序的部分页面或组件中小规模使用,等你或你的小伙伴逐渐信任它之后,再慢慢扩大使用范围。如果你在使用过程中遇到任何问题或是 Bug,欢迎提交 Issue,我将第一时间解决修复。

比较

如果你想要了解 Vue Mini 跟其他一些小程序框架的区别,可以参考文档的比较章节。

尝试一下!

使用以下命令,你可以快速搭起一个基于 Vue Mini 的小程序:

npm install -g sao@beta
# Then
sao vue-mini/template new-miniprogram

最后欢迎分享、转载本文,让更多的人知道 Vue Mini。

发布于 04-09

手写vue版mini源码分析框架,优势特性总结,vue-cli知识点,以及vue项目的二次封装,mini项目源码附送

前言

在前端三大框架并存的今天,vue已经是前端必须掌握的一部分。而对于很多入门者,或者转行前端的小伙伴们,个人觉得vue是一个非常适合入门的框架的之一。笔者个人觉得,无论从api的易学的角度出发,还是从原理层面解析,vue还是比react的简单一些。记得某个大神的面试分享:如果面试官没有vue跟react方向的要求,尽量往vue的方向扯,个人觉得是个非常优秀的意见哈哈哈。

身处跳槽涨薪的年代,相信很多同行们都已经背了很多面经。(虽然内心有点鄙视背题库的人,面试神一样,工作zhu一样)。 长久的发展,还是得扎扎实实的打好基础。如果面试官不再追问面经,反过来请你介绍vue,你想好怎么介绍你的vue项目吗?

本文的重点,如何介绍如何搭建vue项目,介绍你的vue项目。

此外,文章为个人源码第一篇,后续会陆续送上源码,以下是个人计划:

| 序号 | 博客主题 | 相关链接 | |—–|——|————|- | 1 | 手写vue_mini源码解析(即本文) | juejin.im/post/684790… | | 2 | 手写react_mini源码解析 | juejin.im/post/685457… | | 3 | 手写webpack_mini源码解析 | juejin.im/post/685457… | | 4 | 手写jquery_mini源码解析 | juejin.im/post/685457… | | 5 | 手写vuex_mini源码解析 | juejin.im/post/685705… | | 6 | 手写vue_router源码解析 | 预计8月 | | 7 | 手写diff算法源码解析 | 预计8月 | | 8 | 手写promis源码解析 | 预计8月 | | 9 | 手写原生js源码解析(手动实现常见api) | 预计8月 | | 10 | 手写react_redux,fiberd源码解析等 | 待定,本计划先出该文,整理有些难度 | | 11 | 手写koa2_mini | 预计9月,前端优先 |

文章适合人群

半年~三年经验的vue开发者。 如未接触过vue,建议从官方文档:vuejs.org/ 学习搭建先。

该文章重点为普及知识点,以及部分知识点的解析。

从mini源码了解什么vue

Vue.js是一套构建用户界面的渐进式框架。 他最大的优势,也是单页面最大的优势,数据驱动与组件化。

首先我们mini的源码了解vue如何完成数据驱动。如图:

从图我们就可以简单的分析出什么叫MVVM。

MVVM, 实际上为M + V + VM。vue的框架就是一个内置的VM状态,而M就是我们的MODLE, V即是我们的视图。而通过我们的M,就能实现对V的控制,就是我们所说的数据驱动(模型控制视图)。ViewModel 的内容会实时展现在 View 层,前端开发者再也不必低效又麻烦,还耗性能(因为没有diff算法)地通过操纵 DOM 去更新视图。这就是一个从根源上,MVVM框架比传统MVC框架的优势。

我们进一步手写Mini版来了解vue,从源码了解什么是数据劫持。

首先构造一个vue实例。写过vue初始化的都知道,初始化时需要传入data,以及绑定元素标记el。我们把它储存起来。

class wzVue {
	constructor(options){
		this.$options = options;
		this.$data = options.data;
		this.$el = options.el;
	}
}
复制代码

首先看一下Observer的实现,以vue2.0为例,我们都知道数据劫持是通过Object.defineProperty。它自带监听get,set方法,我们可以用他实现一个简单的绑定。

obsever(this.$data);

function obsever(){
      Object.defineProperty( obj, key, {
		get(){
			
		},
		set( newValue ){
			value = newValue;
		}
	})
}
复制代码

这里很简单,如果还是不明白怎么双向绑定,举个简单的栗子:

<input type="text" v-modle="key" id="key"/>

// script
var data = {
    key: 5
    key2: 8
}
obsever(data);
data.key=6;//

function obsever(obj){
      Object.defineProperty( obj, key, {
		get(){
			
		},
		set( newValue ){
		    document.getElementById('key').val(newValue);//写死'key'先,下文会讲解
		}
	})
}

//写死'key'先,下文会讲解
document.getElementById('key').addEventListener( 'click', false, function(e){
    obj.key = e.target.value;
})
复制代码

这样实现了双向绑定,如果对象obj.key赋值,就会触发set方法,同步input的数据;如果页面手动输入值,则通过监听触发set,同步到对象obj的值。此时你可能有一个疑问,我们在vue赋值的时候,是直接修改上下文data数据的,并不是修改对对象的值, 也就是this.key=6。是的,vue源码中,先对data对象的数据进行了一次本地的数据劫持。如下文的proxyData。这样的:

this.key —-> data.key(触发) —>实现数据劫持

observer( data ){//监听data数据,双向绑定
    if( !data || typeof(data) !== 'object'){
		return;
	}
	Object.keys( data ).forEach( key => {
		this.observerData(key, data, data[key]);//监听data对象
		this.proxyData( key );
	})
}

observerData( key, obj, value ) {
	this.observer(key);
	const dep = new Dep();
	Object.defineProperty( obj, key, {
		get(){
		},
		set( newValue ){ //通知变化
		}
	})
}

proxyData(key){
	Object.defineProperty( this, key, {
		get(){
			return this.$data[key];
		},
		set( newValue ){	
			this.$data[key] = newValue;
		}			
	})
}
复制代码

两点需要强调的地方:

1)遍历data的属性,vue的源码是用了Object.keys。它能按顺序遍历出不同的属性,但是不同的浏览器中可能执行顺序不一样。

2)因为Object.defineProperty只能监听一层结构,所以,对于多层级的Object结构来讲,需要遍历去一层一层往下监听。

那如果连续赋值的,例如this.key = 1; this.key2 = 2; 上边的双向绑定代码是写死了“key”。

这时候是否发生了两次赋值?那么我们怎么知道,它触发的对象是哪个呢?这时候,vue的设计是设计了dep的概念,来存放每个监听对象的值。

class Dep{
	constructor(){
		this.deps = [];
	}
	
	addDep(dep){
		this.deps.push(dep);
	}
	
	notiyDep(){
		this.deps.forEach(dep => {
			dep.update();
		})
	}
}
复制代码

这里不难理解。addDep既是为了有数据变化时,插入的“对象”,表示需要劫持。 notiyDep即是该对象,已经需要被更新,执行对应的update方法。

那么插入的对象是什么呢(数组的单体)?单体肯定,需要包含一个“dom”对象,还有对应监听的“data”对象,两者关系绑定,才能实现数据同步。这个“单体”,我们称呼它为“watcher”。

class Watcher{

	constructor( vm, key, initVal, cb ){
		this.vm = vm;//保存vue对象实例
		this.key = key;//保存绑定的key
		this.cb = cb;//同步两者的回调函数
		this.initVal = initVal;//初始化值
		this.vm[this.key];//触发对象的get方法
	}
	
	update(){
		this.cb.call( this.vm, this.vm[this.key], this.initVal );
	}

}
复制代码

截至目前为止,obsever还是没有跟Watcher关联上。在讲他们怎么关联上之前,我们再看看vue的设计思维,它是由Watcher添加订阅者,再由Dep添加变化。那么Watcher是怎么来的?从图中的关系,我们可以看出由页面解析出来的。这就是我们要讲的 Compile。 

Compile,首先有一个“初始化视图”的动作。

class Compile{
	
	constructor( el, vm ){
		this.$el  =  document.querySelector(el);
		this.$vm = vm;
		if (this.$el) {
			this.$fragment = this.getNodeChirdren( this.$el );
			this.$el.appendChild(this.$fragment);
		}
	}
	
	getNodeChirdren( el ){
		const frag = document.createDocumentFragment();
		let child;
		while( (child = el.firstChild )){
			frag.appendChild( child );
		}
		return frag;
	}
}
复制代码

这里应该不难理解,拿到template对象的id,遍历完之后,赋值显示在我们的el元素中。接下来我们重点讲Compile产生的Watcher。我们在Compile的原型中添加this.compile( this.$fragment);方法。对刚才拿到template的模版进行继续,看他用到哪些属性。

compile( el ){
	const childNodes = el.childNodes;
	Array.from(childNodes).forEach( node => {
		if( node.nodeType == 1 ) {//1为元素节点
			const nodeAttrs = node.attributes;
			Array.from(nodeAttrs).forEach( attr => {
				const attrName = attr.name;//属性名称
				const attrVal = attr.value;//属性值
				if( attrName == "v-modle"){
				   	this.zDir_model( node, attrVal );
				}
			})
		} else if( node.nodeType == 2 ){//2为属性节点
			console.log("nodeType=====22");
		} else if( node.nodeType == 3 ){//3为文本节点
			this.compileText( node );
		}
		// 递归子节点
		if (node.childNodes && node.childNodes.length > 0) {
			this.compile(node);
		}
	})
}
复制代码

如果你对childNodes,nodeType,nodeList还是一脸懵逼,建议移步到: 关于DOM和BOM知识点汇总: juejin.im/post/684668…

从上边的mini源码可以看出,compile遍历el的所有子元素,如果是文本类型,我们就进行文本解析compileText。如果是input需要双向绑定,我们就进行zDir_model解析。

compileText( node ){
	if( typeof( node.textContent ) !== 'string' ) {
		return "";
	}
	const reg = /({{(.*)}})/;
	const reg2 = /[^/{/}]+/;
	const key = String((node.textContent).match(reg)).match(reg2);//获取监听的key
	const initVal = node.textContent;//记录原文本第一次的数据
    updateText( node, this.$vm[key], initVal );
}

updateText( node, value, initVal ){
	var reg = /{{(.*)}}/ig;
	var replaceStr = String( initVal.match(reg) );
	var result = initVal.replace(replaceStr, value );
	node.textContent = result;
	new Watcher( this.$vm, key, initVal, function( value, initVal ){
		updateText( node, value, initVal  );
	});
}
复制代码

我们再看看compileText的源码,大概意思为,获取到文本例如“我的名字{{name}}”的key,即为name。然后name进行初始化赋值updateText, updateText的初始化结束后,添加订阅数据变化,绑定更新函数Watcher。

而Watcher,正是绑定dep跟compile的桥梁。我们修改一下添加到dep跟Watcher的代码:

observerData( key, obj, value ) {
	this.observer(key);
	const dep = new Dep();
	Object.defineProperty( obj, key, {
		get(){
		    Dep.target && dep.addDep(Dep.target);//添加的代码+++++++++++++++++
			return value;
		},
		set( newValue ){ //通知变化
		    if (newValue === value) {
			  return;
			}
			value = newValue;
			//通知变化
			dep.notiyDep();//添加的代码+++++++++++++++++
		}
	})
}


class Watcher{

	constructor( vm, key, initVal, cb ){
		this.vm = vm;
		this.key = key;
		this.cb = cb;
		this.initVal = initVal;
		Dep.target = this;//添加的代码+++++++++++++++++
		this.vm[this.key];
		Dep.target = null;
	}
	
	update(){
		this.cb.call( this.vm, this.vm[this.key], this.initVal );
	}

}
复制代码

这样的话,我们在新增一个Watcher的过程中,将此时的整个Watcher的this对象赋值到Dep.target中。这时候我们再调用一下this.vm[this.key]。vm即是vue实例对象,所以,Watcher的this.vm[this.key],即是vue实例中的,this.key。而我们的key已经通过Object.defineProperty监听,此时就会进入到Object.defineProperty的get方法中, Dep.target 此时不为空,所以dep.addDep(Dep.target),即是watcher添加订阅者到dep中。

这时候如果数据发生变化,即调用set方法,然后dep.notiyDep,notiyDep就会通知,由文本解析的例如{{key}}的watcher重新更新一遍值,即完成了双向绑定。

如果是v-modle的话,即在解析时,每个对象多加一个监听,然后主动调用set方法。

node.addEventListener("input", e => {
	  vm[value] = e.target.value;
	});
	
复制代码

这就是vue整个双向绑定的大致流程,所谓的数据驱动。

然后他有一个很大的缺陷,这个缺陷是,他知道驱动对象,却无法对数组进行驱动 (实际上也行) 。这里vue的作者用了另外一种思维去解决这个问题。他重写了数组的原型,把数组的’push’, ‘pop’, ‘shift’, ‘unshift’, ‘splice’, ‘sort’, ‘reverse’的方法重写了一遍。也就是当你数组使用了这7个方法时,vue重写的方法,会帮你变化中放入dep中。

这 个 (实际上也行) 其实也很有学问。上述说,vue2.0无法对数组进行数据监听,其实真实的的测试中,Object.defineProperty是可以监听到数组变化的。但是只能在已有的长度中,不能对其加长的长度。那你这时候可能会有疑问,那我们重写array的push方法就够了,为什么要重写7个呢?好吧,我也曾经有这样的疑惑。后续,曾在帖子上,看到过vue的笔者回复过,印象中是这么说的:Object.defineProperty对数组的监听,消耗性能大于效果。也就是说,本来Object.defineProperty,为了提升效率而产生,现在用在数组上,反而降低了效率,那不如干脆拒绝使用他。

于是,又有了vue-cli3.0数据劫持的改造。

那么vue3.0是怎么实现数据劫持的呢?

3.0中双向绑定已经不再是使用Object.defineProperty。而是proxy。proxy的引入,更高的效率,一方面解决了数组方面的问题,我们可以简单看一下mini源码的改造:

proxyData( data ){//监听data数据,双向绑定
	if( !data || typeof(data) !== 'object'){
		return;
	}
	const _this = this;
	const handler  = {
		set( target, key, value ) {
			const rest = Reflect.set(target, key, value);
			_this.$binding[key].map( item =>  {
				item.update();
			})
			return rest;
		}
	}
	this.$data = new Proxy( data, handler );
}
复制代码

vue的mini源码解析到此为止,如还有不明白的地方可留言。 可需要源码可进入github查看:github.com/zhuangweizh…

vue的特性是什么

双向绑定

由上述mini源码,我们可以知道vue的数据驱动。MVVM相比MVC模式, 没有频繁的操作dom值,在开发中无疑时更高效的灵活页面的触发。可让我们专注与逻辑js的抒写,而具体的页面变化,交给VM区处理。

diff算法

我们都知道,js执行的效率高于dom渲染的效率。如果我们能提前通过js算出不一致的地方,再最后去“渲染”最终的差异。明显的增加效益。

我们列出diff算法的三步曲:

  • 1)通过虚拟dom渲染对象
  • 2)对比两个虚拟的差异
  • 3)根据差异进行渲染

全局混入mixins

mixins 选项接收一个混入对象的数组。而vue正是利用他来扩展vue的实例。

我们的全局方法等,都可以利用mixins快速的套入vue实例。

完善的生命周期

十一个生命周期,create, mount, update, activated, destroyed。分别前后。最后还有v2.5.0版本的errorCaptured。 完善的生命周期的更适合,程序顺序的正确执行。

丰富的组件传递

props, emit, slot,provide/inject,attrs/listeners,EventBus emit/on,parent / children与 ref

vue的优势是什么

也是你为什么选择vue的原因

易学上手

笔者曾是一名jq的前端小杂,入门这些玩意,个人觉得他们的难度级别(仅限于api):

jq < 原生小程序 < vue系列 < angurle系列 < react系列

vue是刚开始一边看着api就可以撸出来的项目。

活跃的社区

也许每个框架都有自身的bug。有bug不可怕,怕的是没有解决方案。而vue中,你卡到问题点,但自己没有能力解决时,活跃的社区会给你答案。

完善的第三方插件

支持axios, webpack, sass,elemnt-ui,vuex, router 等第三方插件。

支持客户端全家桶

vue有着脚手架,ssr的nuxt框架,app版本的weex, 小程序多端开发uniapp。

可谓学好vue,吃遍前端全家桶。


最后:也许你觉得,上述react都支持。好的吧,的确是,晚些汇总完react的文章,再写一篇对比。

vue-cli包含了什么

vue-cli脚手架,帮我们做了什么。vue-cli3.0开始,已经成为可选择性的插件。我们分析一下各个插件的作用。

webpack

blog.guowenfh.com/2016/03/24/…

webpack,打包所有的“脚本”。脚手架已经帮我们通过webpack做了很多默认的loader。

我们项目中,不同的文件,经过编译输出最终的html,js,css,都是经过webpack。

例如,编译 ES2015 或 TypeScript 模块成 ES5 CommonJS 的模块;

再例如:编译 SASS 文件成 CSS,然后把生成的CSS插入到 Style 标签内,然后再转译成 JavaScript 代码段,处理在 HTML 或 CSS 文件中引用的图片文件,根据配置路径把它们移动到任意位置,根据 MD5 hash 命名。

因此,我们可以不同文件,找在webpack不同的编译器,如vue有vue-loader,脚手架帮我们引入了。如sass有sass-loader,基本npm或者yarn生态圈中,已经有前端你所有见过的loader。也许还有没有?没关系,我们可以自己写一个。

来个简单的需求:开发环境过滤掉所有的打印。

这要是在传统的项目,没有经过编译器,这是有多大的工作量。当有了我们的webpack或者gulp等,他仅仅只是几句代码的问题。我们来看一下webpack的实现:

配置文件:

const fs = require('fs');

function wzJsLoader(source) {
    /*过滤输出*/
    const mode = process.env.NODE_ENV === 'production'
    if( mode ){//正式环境
        source = source.replace(/console.log\(.*?\);/ig, value => "" );
    }
    return source;
};

module.exports = wzJsLoader;
复制代码

这样,我们就轻松定了一个自己的loader。在wepback.config.js,加上我们对应的loader,轻松解决问题

{
    test: /\.js$/, //js文件加载器
    exclude: /node_modules/,
    use: [
        {
          loader: 'babel-loader?cacheDirectory=ture',
          options: {
            presets: ['@babel/preset-env']
          }
        },
        {
            loader: require.resolve("./myWebPackConfig/wz-js-loader"),//添加的
            options: { name: __dirname  },
        },
    ]
  },
  
复制代码

webpack是个很有难度的东西,本文就不继续简介,简单了解webpack的配置,以及如何写好Loader跟plugins等。如果你还有精力深入,webpack的执行机制,如何打包成文件,他的生命周期等,都可以深入挑战,如果研究透彻,相信你的实力不一般。

axios

axios,网络请求工具。提到网络请求工具,你肯定了解从$.ajax,fetch、axios。下边此次讲一下他们的发展史以及优缺点(具体什么时候,会在下文的“vue项目的二次封装”中讲解)

$.ajax,相信早期进入前端领域的人,都大为喜欢。他基于jquery,对原生XHR的封装,还支持JSONP,非常方便。 他的有点包括,无需要通过刷新页面更新数据,支持异步与服务器通信,而且规范被广泛支持。

当年可谓如“诺基亚”一般存在。可惜“诺基亚”后来跌下神坛,$.ajax在网络请求中也遭受的同样的待遇。

那么淘汰$.ajax的根本原因是什么呢?

因为引入的单页面框架,如vue的mvvn架构,或者是只有m的react,他们都属于js驱动Html。这涉及到控制dom刷新的过程。es5可以利用callback, 或者generater的迭代器模式进行处理。但是还不理想。所以es6引入了promise的概念。

所以,以返回promise的单位的异步控制进程逐步发展。

一方面,.ajax没有改进,他依然我行我素的不支持promise。这对“新”前端的理念很不符,我们无法用.ajax来完成异步操作(除非回调地狱,写过大项目的都知道定位问题太难了)。

另一方面,他还需要引入jquery来实现。我们都知道新框架,都基本脱离了jq。

SO,fetch就这样产生了。解决了ajax无法返回promise的问题。开始让人抛弃$.ajax。

fetch号称是$.ajax的替代品,它的API是基于Promise设计的,旧版本的浏览器不支持Promise,需要使用polyfill es6-promise

然而,fetch貌似是为解决返回Promise而产生的,并没有注意其他网络请求工具该做的细节,他虽然支持promise, 但暴露了太多的问题:

1)fetch只对网络请求报错,对400,500都当做成功的请求,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。

2)fetch默认不会带cookie,需要添加配置项: fetch(url, {credentials: ‘include’})

3)fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费

4)fetch没有办法原生监测请求的进度,而XHR可以

因此,axios正式入场。他重新基于xhr封装,支持返回Promise, 也解决了fetch的弊端。

反问:知道jquery,fetch,axios的区别了吗?

vue-router

在没有“路由”的概念时,我们通常讲“页面路径”。如果你经历过spring mvc通过action映射到html页面的时代,那么恭喜 ,你已经使用过路由。他属于后台路由。后台的路由,可以简单的理解成一个路径的映射。

那么有后台路由,就会有前端路由。没错,带来质的改变,就是前端路由。那么他带来的优势是什么。

前端路由,又分hash模式跟history模式。我们用两张图来简单的说明一下,前端路由的原理:

hash模式

hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页,也就是说hash 出现在 URL 中,但不会被包含在 http 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。hash 模式的原理是 onhashchange 事件(监测hash值变化),可以在 window 对象上监听这个事件。

优势呢?是不是很明显?如果没有使用异步加载,我们的已经可以不需要经过后台,直接仅是页面的“锚点”切换。

history模式

history模式充分利用了html5 history interface 中新增的 pushState() 和 replaceState() 方法。这两个方法应用于浏览器记录栈,在当前已有的 back、forward、go 基础之上,它们提供了对历史记录修改的功能。只是当它们执行修改时,虽然改变了当前的 URL ,但浏览器不会立即向后端发送请求。

history模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。

**此外,**vue的路由,还支持嵌套(多级)路由,支持路由动态配置,命名视图(同一页面多个路由),路由守卫, 过渡动态效果等,可谓功能十分之强大,考虑比较齐全,在此每个列举一个简单的栗子:

路由动态配置:

const router = new VueRouter({
  routes: [
   动态路径参数 以冒号开头
    { path: '/detail/:id', component: Detail }
  ]
})
复制代码

嵌套(多级)路由: const router = new VueRouter({

routes: [
        { path: '/detail/', component: User,
              children: [
                {
                  path: 'product',
                  component: Product  //二级嵌套路由
                },
              ]
        }
   ]
})
复制代码

命名视图:

<router-view></router-view>
<router-view name="a"></router-view>
<router-view name="b"></router-view>

const router = new VueRouter({
  routes: [
    {
      path: '/',
      components: {
        default: componentsDefulat,
        a: componentsA,
        b: componentsB
      }
    }
  ]
})
复制代码

路由守卫:

router.beforeEach((to, from, next) => {
  // ...
})
复制代码

动态效果:

<transition>
  <router-view></router-view>
</transition>
复制代码

sass/less

sass跟less,两者都是CSS预处理器的佼佼者。

为什么要使用CSS预处理器?

CSS有具体以下几个缺点

1.语法不够强大,比如无法嵌套书写,导致模块化开发中需要书写很多重复的选择器;

2.没有变量和合理的样式复用机制,使得逻辑上相关的属性值必须以字面量的形式重复输出,导致难以维护。

Less和Sass在语法上有些共性,比如下面这些:

  • 1、混入(Mixins)——class中的class;
  • 2、参数混入——可以传递参数的class,就像函数一样;
  • 3、嵌套规则——Class中嵌套class,从而减少重复的代码;
  • 4、运算——CSS中用上数学;
  • 5、颜色功能——可以编辑颜色;
  • 6、名字空间(namespace)——分组样式,从而可以被调用;
  • 7、作用域——局部修改样式;
  • 8、JavaScript 赋值——在CSS中使用JavaScript表达式赋值。

再说一下两者的区别:

  • 1.Less环境较Sass简单,使用起来较Sass简单
  • 2.从功能出发,Sass较Less略强大一些 (1) sass有变量和作用域。

    (2) sass有函数的概念;

    (3) sass可以进行进程控制。例如: -条件:@if @else; -循环遍历:@for @each @while

    (4) sass又数据结构类型: -list类型=数组; -map类型=object; 其余的也有string、number、function等类型

  • 3.Less与Sass处理机制不一样
  • 前者是通过客户端处理的,后者是通过服务端处理,相比较之下前者解析会比后者慢一点。而且sass会产生服务器压力。

vuex

vuex官方概念:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。

看到这里你可能会有疑问,我们传统的框架上,localstore, session , cookies,不就以及解决问题了么。

没错。他们是解决了本地存储的问题。但是vue是单页面架构,需要数据驱动。 session , cookies无法触发数据驱动。这时候不得引入一个可以监听的容易。小型项目可能直接用store,或者页面与页面直接可以用props传递

我们在使用Vue.js开发复杂的应用时,经常会遇到多个组件共享同一个状态,亦或是多个组件会去更新同一个状态,在应用代码量较少的时候,我们可以组件间通信去维护修改数据,或者是通过事件总线来进行数据的传递以及修改。但是当应用逐渐庞大以后,代码就会变得难以维护,从父组件开始通过prop传递多层嵌套的数据由于层级过深而显得异常脆弱,而事件总线也会因为组件的增多、代码量的增大而显得交互错综复杂,难以捋清其中的传递关系。

那么为什么我们不能将数据层与组件层抽离开来呢?把数据层放到全局形成一个单一的Store,组件层变得更薄,专门用来进行数据的展示及操作。所有数据的变更都需要经过全局的Store来进行,形成一个单向数据流,使数据变化变得“可预测”。

简单说一下他的工作流程:

图文相信已经非常清晰vuex的工作流程。简单的简述一下api:

state 简单的理解就是vuex数据的储存对象。

getters getter 会暴露为 state 对象,你可以以属性的形式访问这些值:

actions Action 类似于 mutation,不同在于: Action 提交的是 mutation,而不是直接变更状态。 Action 可以包含任意异步操作。

mutations 每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。 mutations可以直接改变state的状态。 mutations 不可以包含任意异步操作

module Vuex 太大时,允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter。

vuex的使用,很简单。但是灵活时候,可能还需要进一步的了解源码。vuex的原理其实跟vue有点像。

如需要看vuex源码,可通过:github.com/zhuangweizh…

element ui/vux

即UI库的选择

vue的火热,离不开vue社区的火热。就常规的项目,如果公司不是要求特别高,基本各种UI库,已经不需要你写样式(前端最烦的就是写样式没意见吧)。

这里就不做UI库如何搭建的文章,有兴趣可以关注,后续我会写一篇专门搭建UI库的。

这里介绍一下vue火热的UI库吧。

其中,移动端笔者推荐vant,管理后台推荐element。

vue项目的二次封装

axios的封装

上文讲解过axios的由来以及优缺点,这里谈谈axios在vue项目的使用。

1)请求拦截

比如我们的请求接口,全局都需要做token验证。我们可以在请求钱做好token雁阵。如果存在,则请求头自动添加token。

axios.interceptors.request.use(    
    config => {        
        // 每次发送请求之前判断vuex中是否存在token        
        // 如果存在,则统一在http请求的header都加上token,这样后台根据token判断你的登录情况
        // 即使本地存在token,也有可能token是过期的,所以在响应拦截器中要对返回状态进行判断 
        const token = store.state.token;        
        token && (config.headers.token = token);        
        return config;    
    },    
    error => {        
        return Promise.error(error);    
})
复制代码

2)返回拦截

当程序异常的时候呢,接口有时候在特定的场景,或者是服务器异常的情况下,是否就让用户白白等待? 如果有超时,错误返回机制,及时告知用户的,是不是用户好一点?这就是返回的拦截。

axios.interceptors.response.use(    
    response => {   
        // 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据     
        // 否则的话抛出错误
        if (response.status === 200) {            
            return Promise.resolve(response);        
        } else {            
            return Promise.reject(response);        
        }    
    },    
    // 服务器状态码不是2开头的的情况
    // 这里可以跟你们的后台开发人员协商好统一的错误状态码    
    // 然后根据返回的状态码进行一些操作,例如登录过期提示,错误提示等等
    // 下面列举几个常见的操作,其他需求可自行扩展
    error => {    
            alert("数据异常,请稍后再试或联系管理员");
            return Promise.reject(error.response);
        }
    }    
});
复制代码

3)以get为栗子

export function get(url, params){    
    return new Promise((resolve, reject) =>{        
        axios.get(url, {            
            params: params        
        }).then(res => {
            resolve(res.data);
        }).catch(err =>{
            reject(err.data)        
    })    
});
复制代码

此外,对axios的使用还有想法的,建议查看一下axios全攻略: ykloveyxk.github.io/2017/02/25/…

编译器改进

上文曾提到,vue-cli自带webpack。那么我们如何通过他,来改进我们的项目呢。

从环境区分,自带的引入,已经帮我们区分了环境,然后帮我们导入不同的loader跟Pulger等,基本已经是一个非常完善的编译器。

我们见到看一下dev的源码(添加了注释),dev环境,实际上会运行dev-server.js文件该文件以express作为后端框架

// nodejs环境配置
var config = require('../config')
if (!process.env.NODE_ENV) {
  process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
}
var opn = require('opn') //强制打开浏览器
var path = require('path')
var express = require('express')
var webpack = require('webpack')
var proxyMiddleware = require('http-proxy-middleware') //使用代理的中间件
var webpackConfig = require('./webpack.dev.conf') //webpack的配置

var port = process.env.PORT || config.dev.port //端口号
var autoOpenBrowser = !!config.dev.autoOpenBrowser //是否自动打开浏览器
var proxyTable = config.dev.proxyTable //http的代理url

var app = express() //启动express
var compiler = webpack(webpackConfig) //webpack编译

//webpack-dev-middleware的作用
//1.将编译后的生成的静态文件放在内存中,所以在npm run dev后磁盘上不会生成文件
//2.当文件改变时,会自动编译。
//3.当在编译过程中请求某个资源时,webpack-dev-server不会让这个请求失败,而是会一直阻塞它,直到webpack编译完毕
var devMiddleware = require('webpack-dev-middleware')(compiler, {
  publicPath: webpackConfig.output.publicPath,
  quiet: true
})

//webpack-hot-middleware的作用就是实现浏览器的无刷新更新
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
  log: () => {}
})
//声明hotMiddleware无刷新更新的时机:html-webpack-plugin 的template更改之后
compiler.plugin('compilation', function (compilation) {
  compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
    hotMiddleware.publish({ action: 'reload' })
    cb()
  })
})

//将代理请求的配置应用到express服务上
Object.keys(proxyTable).forEach(function (context) {
  var options = proxyTable[context]
  if (typeof options === 'string') {
    options = { target: options }
  }
  app.use(proxyMiddleware(options.filter || context, options))
})

//使用connect-history-api-fallback匹配资源
//如果不匹配就可以重定向到指定地址
app.use(require('connect-history-api-fallback')())

// 应用devMiddleware中间件
app.use(devMiddleware)
// 应用hotMiddleware中间件
app.use(hotMiddleware)

// 配置express静态资源目录
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
app.use(staticPath, express.static('./static'))

var uri = 'http://localhost:' + port

//编译成功后打印uri
devMiddleware.waitUntilValid(function () {
  console.log('> Listening at ' + uri + '\n')
})
//启动express服务
module.exports = app.listen(port, function (err) {
  if (err) {
    console.log(err)
    return
  }
  // 满足条件则自动打开浏览器
  if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
    opn(uri)
  }
})
复制代码

可见,webpack的编译,以及相对完善。我们也可以去优化一下对应的插件,比如:

plugins: [
  new webpack.DefinePlugin({ // 编译时配置的全局变量
    'process.env': config.dev.env //当前环境为开发环境
  }),
  new webpack.HotModuleReplacementPlugin(), //热更新插件
  new webpack.NoEmitOnErrorPlugin(), //不触发错误,即编译后运行的包正常运行
  new HtmlWebpackPlugin({  //自动生成html文件,比如编译后文件的引入
    filename: 'index.html', //生成的文件名
    template: 'index.html', //模板
    inject: true
  }),
  new FriendlyErrorsPlugin() //友好的错误提示
]
复制代码

最后讲一下webpack的相关优化:

构建速度的优化:
  • 1.HappyPack 基于webpack的编译模式本是单线程,时间占时最多的Loader对文件的转换。开启HappyPack,可以讲任务分解成多个进程去并行处理。

    简单配置:

    new HappyPack({// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件 id: ‘babel’,// 如何处理 .js 文件,用法和 Loader 配置中一样 loaders: [‘babel-loader?cacheDirectory’],// … 其它配置项 }),

    详细可参考:www.fly63.com/nav/1472

  • 2.DllPlugin 可将一些Node_moudle一些编译好的库,常用而且不变的库,抽出来。这样就无需重新编译。
  • 3.Loader 记录配置搜索范围,include,exclude 如:

    { test: /.js$/, //js文件加载器 exclude: /node_modules/, use: [ { loader: ‘babel-loader?cacheDirectory=ture’, options: { presets: [‘@babel/preset-env’] }, include: Path2D.resolve(__dirname, ‘src’) } ] }

优化打包大小:
  • 1.tree shaking写法。(webpack 4 已自动引入) 即“摇树”。即只引入所需要引入部分,其余的代码讲会过滤。
  • 2.压缩代码 当前最程愫的压缩工具是UglifyJS。HtmlWebpackPlugin也可配置minify等。
  • 3.文件分离 多个文件加载,速度更快。 例如:mini-css-extract-plugin,将css独立出来。这样还有利于,部分“不变”的文件做缓存。
  • 4.Scope Hoisting 开启后,分细分出模块直接的依赖关系,会自动帮我们合并函数。简单的配置:

    module,exports={ optimization:{   concatenateModules:true } }

组件化

任何框架,团队都需要自己的组件化。(当然,有些团队,怕人员的流动性,全部不组件化,最简单的写法,笔者也遇过这种公司)。

一般来说,组件大致可以分为三类:

  • 1)与业务无关的独立组件。
  • 2)页面级别的组件。
  • 3)业务上可复用的基础组件。

关于1),可以理解成现在的UI库(如element/vant),这里暂时不做独立组件分析。(晚些可能会写一篇如何写独立组件的文章,上传到npm。)

关于2),貌似当某一个模块,页面需要多次重复使用时候,就可以写成独立组件,这个貌似没什么好分析。

这里重点分析一下:** 3)业务上可复用的基础组件 ** 。

笔者写过的vue项目,都基本会封装20~30个业务通用组件。例如截图的my-form,my-table。如下: 

这里我以为myTable

emelent的table插件,的确已经很强大了。但是笔者虽然用上了emelent ui,但是业务代码却没有任何emelent的东西。

如果有一天,公司不再喜欢element ui的table,那so easy,我把我的mytable修改一下,所有页面即将同步。这就是组件化的魅力。

下边我以my-table为栗子,记录一下我组件化的要点: 1.合并封装分页,是表格不再关心分页问题。 2.统一全局表格样式(后期可随时修改) 3.业务脱离,使业务上无需再关心element的api如何定义,且可随时替换掉element。 4.自定义类型,本文提供select跟text控制,配置对象即可实现。 5.统一自定义缺省处理。 6.统一搜索按钮,搜索框。配置对象即可实现。

这些优势,以及对全局的拓展性,是不是比传统直接用的,有很大的优势?

当然,不好的地方,插件应该相对完善,考虑周全,需要一个全局统筹的人。对人员的流动的公司,的确很不友好。

下边是源码提供,可参考:

<template>
  <div>
    <h3 class="t_title">{{tName}}</h3>
    <div class="t_content">
      <el-form :inline="true" class="serach_form" >
        <el-form-item  v-for="(item, index) in tSerachList" :label="item.name" :key="index" v-if="tSerachList.length > 0 ">
          <div v-if="item.type == 'text'" >
            <el-input  :placeholder="item.name" v-model="tSerachList[index].value" ></el-input>
          </div>
          <div v-else-if="item.type == 'select'" >
            <el-select v-model="tSerachList[index].value"  :placeholder="item.name">
              <el-option  v-for="(cItem, cIndex) in item.list" :key="cIndex" :label="cItem.name" :value="cItem.value" ></el-option>
            </el-select>
          </div>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="onSubmit" >查询</el-button>
        </el-form-item>
      </el-form>
      <!--按钮操作模块-->
      <el-row class="t_button_tab" v-for="(item,index) in tBtnOpeList" :key="index">
        <el-button  :type="item.type" @click="btnOpeHandle(item.opeList)" :render="item.render">{{item.label}}</el-button>
      </el-row>
    </div>

    <div class="t_table">
      <el-table
        :data="tableData"
        style="width: 100%">
        <el-table-column v-for="(item, index) in tableList" :key="index" v-bind="item">
          <template slot-scope="scope" >
            <my-table-render v-if="item.render" :row="scope.row" :render="item.render" ></my-table-render>
            <span  v-else>{{scope.row[item.key]}}</span>
          </template>
        </el-table-column>
      </el-table>
    </div>

    <div class="t_pagination">
      <el-pagination
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page.sync="currentPage"
        :page-size="tPageSize"
        layout="prev, pager, next, jumper"
        :total="tTotal">
      </el-pagination>
    </div>
  </div>

</template>

<script>

  import MyTableRender from './my-table-render.vue'

  export default {
    props: {
      tTablecolumn: { //展示的列名
        type: Array,
        required: true
      },
      tName: { //页面的名称
        type: String,
          required: true
      },
      tUrl: { //请求的URL
        type: String,
        required: true
      },
      tParam: { //请求的额外参数
        type: Object,
        required: true
      },
      tSerachList: { //接口的额外数据
        type: Array,
        required: true
      },
      tBtnOpeList: {
        type: Array,
        required: false
      }
    },
    data () {
      return {
        arrea: "",
        currentPage: 1,
        tableData: [],
        tableList: [],
        tTotal: 0,
        tPageSize: 10,
        serachObj: {} //搜索的文本数据
      }
    },
    created () {
      this.getTableList()
      this.reloadTableList()
    },
    methods: {
      async getTableList () {
        var Obj = { pageNum: this.currentPage, pageSize: this.tPageSize }
        var that = this;
        var url = this.tUrl;
        var param = Object.assign(this.tParam, Obj, this.serachObj)
        const res = await this.utils.uGet({ url:url, query:param })
        var list = res.data.dataList
        that.tableData = list
        that.tTotal = res.data.total
      },
      // 提交
      reloadTableList () {
        var tableList = this.tTablecolumn
        for (var i = 0; i < tableList.length; i++) {
          tableList[i].prop = tableList[i].key
          tableList[i].label = tableList[i].name
        }
        this.tableList = tableList
      },
      onSubmit ( res ) {
        var that = this;
        const status = this.$store.getters.getUserStatus;
        if( status == 4 ){
          this.utils.uErrorAlert("临时用户无权限哦");
        } else {
          this.utils.uLoading(800);
          var tSerachList = this.tSerachList;
          var obj = {}
          for (var i = 0; i < tSerachList.length; i++) {
            obj[ tSerachList[i].key ] = tSerachList[i].value;
          }
          this.serachObj = obj;
          this.currentPage = 1;
          this.getTableList();
        }
      },
      handleSizeChange () {
      },
      handleCurrentChange (obj) {
        this.currentPage = obj
        this.getTableList()
      },
      btnOpeHandle(params){
        const status = this.$store.getters.getUserStatus;
        if( status == 4 ){
          this.utils.uErrorAlert("临时用户无权限哦");
        } else {
          this.$emit('handleBtn', params);
        }
      }
    },
    components: {
      MyTableRender
    }
  }
</script>


<style  lang="scss">

  @import '@/assets/scss/element-variables.scss';
  .serach_form{
    background: $theme-light;
    text-align: left;
    padding-top: 18px;
    padding-left: 20px;
  }
  .t_title{
    /*float: left;*/
    /*padding: 20px;*/
    /*font-size: 23px;*/
    color:$theme;
    text-align: left;
    border-left: 3px solid $theme;
    padding-left: 5px;
  }

  .t_content{
    clear: both;
  }

  .t_table{
    clear: both;
    padding: 20px;
  }

  .t_pagination{
    margin-top: 20px;
    float: right;
    margin-right: 20px;
  }
  .t_button_tab{
    text-align: left;
    margin-top: 18px;
  }

</style>
复制代码

mini项目源码

最后送上个人手写的mini版本vue源码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>wz手写vue源码</title>
</head>
<body>
  <div id="app" class="body" >
	<div class="b_header" >
		<img class="b_img" src="https://user-gold-cdn.xitu.io/2020/7/10/173344e271bf85af?w=400&h=400&f=png&s=3451" /><span>wz手写vue源码</span>
	</div>
	<div class="b_content" >
		<div class="n_name" >姓名:{{name}}</div>
		<div class="box" >
			<span>年龄:{{age}}</span>
		</div>
		<div>{{content}}</div>
		<div>
			<input type="text" wz-model="content" placeholder="请输入自我介绍"  />
		</div>
		<div wz-html="htmlSpan" ></div>
		<button @click="changeName" >点击提示</button>
	</div>
  </div>
  <style>
  .body{
	 text-align: left;
	 width: 300px;
	 margin: 0 auto;
	 margin-top: 100px;
  }
  .body .b_header{
	display:  flex;
	justify-item: center;
	justify-content: center;
	align-items: center;
	align-content: center;
	margin-bottom: 20px;
  }
  .body .b_header span{
	font-size: 21px;
  }
  .body .b_img{
	display: inline-flex;
	width: 20px;
	height: 20px;
	align-item: center;
  }
  .body .b_content{
	
  }
  .body div{
	margin-top: 10px;
	min-height: 20px;
  }
  button{
	margin-top: 20px;
  }
  </style>
  <script src="./wzVue.js"></script>
  <script>
    const w = new wzVue({
      el: '#app',
      data: {
        "name": "加载中...",
        "age": '加载中...',
		"content": "我是一枚优秀的程序员",
		"htmlSpan": '<a href="http://wwww.zhuangweizhan.com">点击欢迎进入个人主页 </a>'
      },
      created() {
        setTimeout(() => {
          this.age = "25岁";
		  this.name = "weizhan";
        }, 800);
      },
	  methods: {
		changeName() {
			alert("欢迎进入个人主页: http://www.zhuangweizhan.com");
		}
	  }
    })
  </script>
</body>
</html>


// js文件
/*
	本代码来自weizhan
*/
class wzVue {
	constructor(options){
		this.$options = options;
		console.log("this.$options===" + JSON.stringify(this.$options) );
		this.$data = options.data;
		this.$el = options.el;
		this.observer( this.$data );//添加observer监听
		new wzCompile( options.el, this);//添加文档解析
		if ( options.created ) {
			options.created.call(this);
		}
	}
	
	observer( data ){//监听data数据,双向绑定
		if( !data || typeof(data) !== 'object'){
			return;
		}
		Object.keys(data).forEach(key => {//如果是对象进行解析
			this.observerSet(key, data, data[key]);//监听data对象
			this.proxyData(key);//本地代理服务
		});
	}
	
	observerSet( key, obj, value ){
		this.observer(key);
		const dep = new Dep();
		Object.defineProperty( obj, key, {
			get(){
				Dep.target && dep.addDep(Dep.target);
				return value;
			},
			set( newValue ){
				if (newValue === value) {
				  return;
				}
				value = newValue;
				//通知变化
				dep.notiyDep();
			}
		})
	}
	
	proxyData(key){
		Object.defineProperty( this, key, {
			get(){
				return this.$data[key];
			},
			set( newVal ){
				this.$data[key] = newVal;
			}
		})	
	}
	
}

//存储数据数组
class Dep{
	constructor(){
		this.deps = [];
	}
	
	addDep(dep){
		this.deps.push(dep);
	}
	
	notiyDep(){
		this.deps.forEach(dep => {
			dep.update();
		})
	}
}

//个人编译器
class wzCompile{
	constructor(el, vm){
		this.$el = document.querySelector(el);
		
		this.$vm = vm;
		if (this.$el) {
			this.$fragment = this.getNodeChirdren( this.$el );
			this.compile( this.$fragment);
			this.$el.appendChild(this.$fragment);
		}
	}
	
	getNodeChirdren(el){
		const frag = document.createDocumentFragment();
		
		let child;
		while( (child = el.firstChild )){
			frag.appendChild( child );
		}
		return frag;
	}
	
	compile( el ){
		const childNodes = el.childNodes;
		Array.from(childNodes).forEach( node => {
			if( node.nodeType == 1 ) {//1为元素节点
				const nodeAttrs = node.attributes;
				Array.from(nodeAttrs).forEach( attr => {
					const attrName = attr.name;//属性名称
					const attrVal = attr.value;//属性值
					if( attrName.slice(0,3) === 'wz-' ){
						var tagName = attrName.substring(3);
						switch( tagName ){
							case "model":
								this.wzDir_model( node, attrVal );
							break;
							case "html":
								this.wzDir_html( node, attrVal );
							break;
						}
					}
					if( attrName.slice(0,1) === '@'  ){
						var tagName = attrName.substring(1);
						this.wzDir_click( node, attrVal );
					}
				})
			} else if( node.nodeType == 2 ){//2为属性节点
				console.log("nodeType=====22");
			} else if( node.nodeType == 3 ){//3为文本节点
				this.compileText( node );
			}
			
			// 递归子节点
			if (node.childNodes && node.childNodes.length > 0) {
				this.compile(node);
			}
		})
	}
	
	wzDir_click(node, attrVal){
		var fn = this.$vm.$options.methods[attrVal];
		node.addEventListener( 'click', fn.bind(this.$vm));
	}
	
	wzDir_model( node, value ){
		const vm = this.$vm;
		this.updaterAll( 'model', node, node.value );
		node.addEventListener("input", e => {
		  vm[value] = e.target.value;
		});
	}
	
	wzDir_html( node, value ){
		this.updaterHtml( node, this.$vm[value] );
	}
	
	updaterHtml( node, value ){
		node.innerHTML = value;
	}
	
	compileText( node ){
		if( typeof( node.textContent ) !== 'string' ) {
			return "";
		}
		console.log("node.textContent===" + node.textContent  );
		const reg = /({{(.*)}})/;
		const reg2 = /[^/{/}]+/;
		const key = String((node.textContent).match(reg)).match(reg2);//获取监听的key
		this.updaterAll( 'text', node, key );
	}
	
	updaterAll( type, node, key ) {
		switch( type ){
			case 'text':
				if( key ){
					const updater = this.updateText;
					const initVal = node.textContent;//记录原文本第一次的数据
					updater( node, this.$vm[key], initVal);
					new Watcher( this.$vm, key, initVal, function( value, initVal ){
						updater( node, value, initVal  );
					});
				}
				break;
			case 'model':
				const updater = this.updateModel;
				new Watcher( this.$vm, key, null, function( value, initVal ){
					updater( node, value );
				});
				break;
		}
	}

	updateModel( node, value ){
		node.value = value;
	}
	
	updateText( node, value, initVal ){
		var reg = /{{(.*)}}/ig;
		var replaceStr = String( initVal.match(reg) );
		var result = initVal.replace(replaceStr, value );
		node.textContent = result;
	}
	
}

class Watcher{
	
	constructor( vm, key, initVal, cb ){
		this.vm = vm;
		this.key = key;
		this.cb = cb;
		this.initVal = initVal;
		Dep.target = this;
		this.vm[this.key];
		Dep.target = null;
	}
	
	update(){
		this.cb.call( this.vm, this.vm[this.key], this.initVal );
	}

}
复制代码

文章结尾

文章均为原创手写,写一篇原创上万字的文章,明白了笔者的不易。

如有错误希望指出。

后续,我会继续react的总结。

如何搭建类似麦当劳店中需登录认证的wifi

如何搭建类似麦当劳店中需登录认证的wifi

July 24, 2020

日常生活中常能碰到一些商场或餐饮店提供一种需认证的wifi,这种wifi连接后不能立刻使用,往往还需要在一个页面上进一步认证操作才行,比如输入手机号填个验证码之类的。

作为一名前端开发,每当我去麦当劳店里吃饭,用手机连接wifi时,一直都很想搞清楚几个问题:

  • 这种wifi认证页面是如何搭建的?
  • 它的认证机制是怎样的?
  • 它跟正常的网站会有哪些不一样?

我工作地旁边的麦当劳wifi认证截图:

captive portal    1

captive portal    2

这种wifi的英文学名叫Captive Portal,在开源社区中早已存在一些组件可轻松搭建这种类型的wifi,比如wifiDog, CoovaChilli, nodogslash等。

为了一探究竟,我用nodogslash在树莓派上搭建了一个带认证功能的wifi,并且使用React创建了自定义认证页面,尝试搞清楚整个认证流程背后的原理。

最终认证页面效果如下,点击按钮即完成认证:

captive portal    wifi

wifi认证的机制和原理

每当设备连接wifi后,系统会自动做一个连通性校验,此校验的本质是发送一个HTTP请求。如果请求失败,则会触发相应机制要求用户输入登录凭证。如果请求成功,则表示网络已通,无任何回应,这个网络校验过程叫Captive Portal Detection (CPD)。

不同的操作系统校验时的请求地址不一样,比如我用手边的android和iphone手机分别做了测试,他们对应校验地址如下:

简单来说,wifi的认证过程通过一个HTTP GET请求即可完成。以我本文示例中使用的nodogsplash组件为例,其内部用C语言实现了一个服务器运行在2050端口。设备连上wifi时,wifi端会生成一个token,当设备被重定向到认证页面时,页面模板中包含此token,此时用户只需发送一个GET请求将此token传入到对应服务器的认证地址即可。

captive portal

如果你配置了FAS,也就是说设置了自定义认证机制,比如说你想添加了一个手机验证环节,需要用户填入手机和验证码才能完成认证。那么nodogsplash在重定向登录页面的时候会把一些重要参数附带在请求地址的后面,让你的自定义入口页能获取到这些认证凭证,比如token之类的参数。

captive portal      highlight

等你的自定义验证手机验证通过了,再选择将token以HTTP GET请求发送回原2050端口上的认证服务器,整个流程如下图所示:

captive portal FAS

认证站点的限制

需要注意的是,当用户设备连上wifi但还尚未通过验证时,网络访问是受限的,此时能访问的内容取决于防火墙的设置。比如我上面示例中,将站点配置在路由器上,网站端口是8080,依赖的后端服务器运行在端口8081上,此时必须在nodogsplash的防火墙规则中开放这两个端口,才能让未认证的用户设备访问的到。假如服务器配置在外网,就要将对应的域名或IP在防火墙中开放出来,具体配置方式参考nodogsplash关于FAS的文档

另外,wifi认证页面的实现上要有一些额外的安全考量。比如在nodogsplash的官网文档中建议网站遵循一下安全准则:

  1. 当认证成功后需立刻关闭浏览器
  2. 禁止使用链接
  3. 禁止文件下载功能
  4. 禁止执行javascript

关于第二条,可以使用表单提交的方式替换链接调整,而对于第四条,它的本意并不是禁止js功能,只是为了防止执行js语句引起的安全性问题。我在示例中搭建的网站使用了react框架,在android和ios上都能正常显示。

因此,在功能实现上相比较通常的前端站点,自定义的wifi认证网站部分功能受限,但影响并不大,可以使用你自己擅长的前端框架来搭建。

创建需认证的wifi

注意,如果你手边没有树莓派或Linux系统,或者对配置部分不感兴趣,直接跳过即可。

准备工作

  • 树莓派 4B
  • hostapd和dnsmasq (用于创建wifi热点)
  • nodogsplash (核心组件,管理wifi热点,提供认证功能)

nodogsplash可以安装OpenWrt和Linux中,前者是开源的智能路由器操作系统,国内的一些路由器厂商通常是基于此系统定制的,后者就不必多介绍了,这次的示例就是安装在Linux系统上,为了方便安装调试,我直接使用一个树莓派4作为载体,用网线连通网络,用无线创建热点wifi。

创建WIFI热点

在安装组件之前,首先将依赖包更新,然后安装hostapddnsmasq两个组件,前者用来创建wifi热点,后者用来处理DNS和DHCP等服务。

sudo aptget update

sudo aptget upgrade

sudo aptget install hostapd dnsmasq

修改配置并指定一个wifi网段,配置文件在/etc/dhcpcd.conf

sudo vi /etc/dhcpcd.conf

#

# 文件内容如下:

interface wlan0

static ip_address=192.168.220.1/24

nohook wpa_supplicant

其中wlan0是无线网卡的名称,可以通过ifconfig命令查询,IP地址可任意指定,只要不跟家中的wifi冲突即可,比如说此处设置的是192.168.220.*,而我家中的wifi网段是192.168.31.*。

重启一下服务,让配置生效:

sudo systemctl restart dhcpcd

修改hostapd配置,用于设置wifi的名称和密码,其中ssid表示此wifi的名称,wpa_passphrase表示此wifi的密码。

sudo vi /etc/hostapd/hostapd.conf

#

# 文件内容如下:

interface=wlan0

driver=nl80211

hw_mode=g

channel=6

ieee80211n=1

wmm_enabled=0

macaddr_acl=0

ignore_broadcast_ssid=0

auth_algs=1

wpa=2

wpa_key_mgmt=WPAPSK

wpa_pairwise=TKIP

rsn_pairwise=CCMP

# wif的名称

ssid=Pi4AP

# wifi的秘密

wpa_passphrase=pimylifeup
修改配置文件/etc/default/hostapd

这时还需要再修改两个配置文件,一个是hostapd启动时的加载文件,需要将配置文件字段DAEMON_CONF指定为上面的文件地址,默认情况下该字段是被注释掉的。

sudo nano /etc/default/hostapd

#

# 文件内容如下:

# 将#DAEMON_CONF=“” 修改为下面这行

DAEMON_CONF=“/etc/hostapd/hostapd.conf”
修改配置文件/etc/init.d/hostapd

另一个配置文件是系统服务配置,同意将上文的配置文件地址赋值给DAEMON_CONF字段。

sudo vi /etc/init.d/hostapd

#

# 文件内容如下:

# 将DAEMON_CONF=修改为下面这行

DAEMON_CONF=/etc/hostapd/hostapd.conf
修改配置文件/etc/dnsmasq.conf

在此文件配置自定义wifi的网段、dns服务器等信息。

sudo vi /etc/dnsmasq.conf

#

# 文件内容如下:

interface=wlan0 # 指定无线网卡名称

server=114.114.114.114 # 使用dns服务器

dhcprange=192.168.220.50,192.168.220.150,12h # 指定可用IP的网段范围和释放时间
无线网卡转发有线网卡

修改系统配置文件中的net.ipv4.ip_forward字段,激活转发功能,默认情况下,该字段是被注释掉的。

sudo vi /etc/sysctl.conf

#

# 文件内容如下:

# 将原#net.ipv4.ip_forward=1的注释符号去掉,修改为下面这行

net.ipv4.ip_forward=1

重启系统,让此修改生效。

sudo reboot

然后,通过iptables命令实现网卡之间的信息转发,其中eth0是有线网卡的名称,可通过ifconfig命令查询。

sudo iptables t nat A POSTROUTING o eth0 j MASQUERADE

最后,需要将当前iptables的配置保存下来,保证每次机器重启时该配置都能生效,先将配置保存到文件中/etc/iptables.ipv4.nat

# 将配置写入/etc/iptables.ipv4.nat文件

sudo sh c “iptables-save > /etc/iptables.ipv4.nat”

修改rc.local,保证每次启动时都会读取iptables配置。

sudo vi /etc/rc.local

#

# 文件内容如下:

#

# 在“exit 0”这一行之前添加下面命令读取iptables的配置

iptablesrestore < /etc/iptables.ipv4.nat

exit 0
启动wifi热点

最后,关于热点的配置终于配置完毕,运行一下命令启动服务:

sudo systemctl unmask hostapd

sudo systemctl enable hostapd

sudo systemctl start hostapd

sudo service dnsmasq start

这时,应该可以用手机检测到配置的wifi出现了Pi4-AP,该名称即上面配置的wifi名称,输入对应密码即可连上网络。此时可重启一下再连,确保重启后配置依然生效。

sudo reboot

安装NODOGSPLASH

首先安装对应依赖gitlibmicrohttpd-dev

sudo apt install git libmicrohttpddev

然后使用git直接将nodogsplash源码拿下来,直接安装。

git clone https://github.com/nodogsplash/nodogsplash.git

cd ./nodogsplash

make

sudo make install

添加NODOGSPLASH配置

添加配置到文件/etc/nodogsplash/nodogsplash.conf中,指定对应网卡、网关、最大连接用户数和认证过期时间。其中,wlan0是上面配置的无线网卡名,IP地址是上面配置的wifi热点的网关。

sudo vi /etc/nodogsplash/nodogsplash.conf

#

# 文件内容如下:

GatewayInterface wlan0

GatewayAddress 192.168.220.1

MaxClients 250

AuthIdleTimeout 480

配置完成后,启动nodogsplash。

sudo nodogsplash

此时,用手机连接创建的wifi并输入密码以后,即可看到以下弹窗,要求登录认证。

captive portal wifi popup

点击登录后进入认证页面。

captive portal splash page

配置自定义wifi认证页面

nodogsplash本身提供了自定义验证机制 – Forwarding Authentication Service (FAS),它可以指定自定义的认证页面和认证方式,通过简单配置对应服务器的IP和端口即可。

比如,我在同一台机器上开启一个react站点,端口为8080,若想把此站点设置为认证入口页,只需在配置文件中添加下面四行代码即可,其中fas_secure_enabled从0到3的多个等级值,从低到高会让安全性和复杂性递增,此处选了最简单等级用于做演示。

sudo vi /etc/nodogsplash/nodogsplash.conf

#

# 要添加的内容如下

fasport 8080

fasremoteip 192.168.220.1

faspath /

fas_secure_enabled 0

最后,呈现的样子如下,点击按钮即完成认证,顺利联网。

captive portal    wifi

备注:关于NODOGSPLASH的版本

nodogsplash源码中的master分支指向的3.3.5版本,而此时最新版是5.0.0(笔者写此文章时间2020.7),越新的版本其文档越完善,但要注意的是4.5.1版本是一个分水岭,因为从4.5.1之后该项目的自定义登录授权功能被剥离到一个独立项目openNDS

假如切换到v4.5.1版后碰到libmicrohttpd组件过时异常,可在配置文件中添加字段se_outdated_mhd 1避开此异常。

结语

带认证的wifi在商业活动中越来越常见,开源社区中,nodogsplash是其中一种实现方式,在少量限制的情况下,提供了足够的灵活性让你用熟悉的方式像搭建其他网站一样搭建wifi认证页面。最终,把你的前端能力延伸到路由器上。

参考文献

nodogsplash的github地址

nodogsplash的官方文档

captive-portal和rfc-7710文献关联

Captive-Portal的wiki

树莓派搭建wifi热点

树莓派搭建captive-portal

What We Do

What We DoHotspotSystem provides hotspot management and billing services for businesses or individuals who want to provide internet for their customers.

Live MapLook, people are logging into the hotspots of our clients right now!
Create your own hotspot today!

Get started in minutes. No credit card required.
  • Hotspot Software

    Hotspot Software

    Our Hotspot Software is running on the router, no computer is required.

    Hotspot Software Features

  • Control Center

    Control Center

    Cloud-based solution lets you manage unlimited hotspot locations from the Cloud.

    See how it works

  • Supported Devices

    Supported Devices

    Our Hotspot Software is compatible with many kinds of routers, access points and firmwares.

    Check out Supported Devices

Reseller Program for System IntegratorsHotspotsystem.com reseller program is designed for ISP and WISP companies, Multi-site, Hotels, Campgrounds, Retail Chains, Wireless Hotspot Installers, Network System Integrators.

You, as a reseller can provide hotspot solutions to your clients without having to operate servers, using our cloud-based hosted solution. With our White Label service you can sell the system as it was your own, using your brand.

Interested? Learn more!

 Wired Magazine recommends Hotspotsystem.comClick here to read the article!
Who is using HotspotSystem?HotspotSystem.com’s unique hotspot software is used by various small businesses and big venues. It is also used as a backend by large ISPs and service providers using our Reseller Program.

        

Page 1 of 5

Powered by WordPress & Theme by Anders Norén