政采云前端团队

政采云前端团队 查看完整档案

杭州编辑  |  填写毕业院校政采云有限公司  |  前端团队 编辑 www.zoo.team/ 编辑
编辑

Z 是政采云拼音首字母,oo 是无穷的符号,结合 Zoo 有生物圈的含义。寄望我们的前端 ZooTeam 团队,不论是人才梯队,还是技术体系,都能各面兼备,成长为一个生态,卓越且持续卓越。

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

个人动态

政采云前端团队 发布了文章 · 10月19日

敏感数据加密方案及实现

73 篇原创好文~
本文首发于政采云前端团队博客:敏感数据加密方案及实现

前言

现在是大数据时代,需要收集大量的个人信息用于统计。一方面它给我们带来了便利,另一方面一些个人信息数据在无意间被泄露,被非法分子用于推销和黑色产业。

2018 年 5 月 25 日,欧盟已经强制执行《通用数据保护条例》(General Data Protection Regulation,缩写作 GDPR)。该条例是欧盟法律中对所有欧盟个人关于数据保护和隐私的规范。这意味着个人数据必须使用假名化或匿名化进行存储,并且默认使用尽可能最高的隐私设置,以避免数据泄露。

相信大家也都不想让自己在外面“裸奔”。所以,作为前端开发人员也应该尽量避免用户个人数据的明文传输,尽可能的降低信息泄露的风险。

看到这里可能有人会说现在都用 HTTPS 了,数据在传输过程中是加密的,前端就不需要加密了。其实不然,我可以在你发送 HTTPS 请求之前,通过谷歌插件来捕获 HTTPS 请求中的个人信息,下面我会为此演示。所以前端数据加密还是很有必要的。

数据泄露方式

  • 中间人攻击

    中间人攻击是常见的攻击方式。详细过程可以参见这里。大概的过程是中间人通过 DNS 欺骗等手段劫持了客户端与服务端的会话。

    客户端、服务端之间的信息都会经过中间人,中间人可以获取和转发两者的信息。在 HTTP 下,前端数据加密还是避免不了数据泄露,因为中间人可以伪造密钥。为了避免中间人攻击,我们一般采用 HTTPS 的形式传输。

  • 谷歌插件

    HTTPS 虽然可以防止数据在网络传输过程中被劫持,但是在发送 HTTPS 之前,数据还是可以从谷歌插件中泄露出去。

    因为谷歌插件可以捕获 Network 中的所有请求,所以如果某些插件中有恶意的代码还是可以获取到用户信息的,下面为大家演示。

    所以光采用 HTTPS,一些敏感信息如果还是以明文的形式传输的话,也是不安全的。如果在 HTTPS 的基础上再进行数据的加密,那相对来说就更好了。

加密算法介绍

  • 对称加密

    对称加密算法,又称为共享密钥加密算法。在对称加密算法中,使用的密钥只有一个,发送和接收双方都使用这个密钥对数据进行加密和解密。

    这就要求加密和解密方事先都必须知道加密的密钥。其优点是算法公开、计算量小、加密速度快、加密效率高;缺点是密钥泄露之后,数据就会被破解。一般不推荐单独使用。根据实现机制的不同,常见的算法主要有AESChaCha203DES等。

  • 非对称加密

    非对称加密算法,又称为公开密钥加密算法。它需要两个密钥,一个称为公开密钥 (public key),即公钥;另一个称为私有密钥 (private key),即私钥。

    他俩是配对生成的,就像钥匙和锁的关系。因为加密和解密使用的是两个不同的密钥,所以这种算法称为非对称加密算法。其优点是算法强度复杂、安全性高;缺点是加解密速度没有对称加密算法快。常见的算法主要有RSAElgamal等。

  • 散列算法

    散列算法又称散列函数、哈希函数,是把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定成特定长度的值。一般用于校验数据的完整性,平时我们下载文件就可以校验 MD5 来判断下载的数据是否完整。常见的算法主要有 MD4MD5SHA 等。

实现方案

  • 方案一:如果用对称加密,那么服务端和客户端都必须知道密钥才行。那服务端势必要把密钥发送给客户端,这个过程中是不安全的,所以单单用对称加密行不通。
  • 方案二:如果用非对称加密,客户端的数据通过公钥加密,服务端通过私钥解密,客户端发送数据实现加密没问题。客户端接受数据,需要服务端用公钥加密,然后客户端用私钥解密。所以这个方案需要两套公钥和私钥,需要在客户端和服务端各自生成自己的密钥。

  • 方案三:如果把对称加密和非对称加密相结合。客户端需要生成一个对称加密的密钥 1,传输内容与该密钥 1进行对称加密传给服务端,并且把密钥 1 和公钥进行非对称加密,然后也传给服务端。服务端通过私钥把对称加密的密钥 1 解密出来,然后通过该密钥 1 解密出内容。以上是客户端到服务端的过程。如果是服务端要发数据到客户端,就需要把响应数据跟对称加密的密钥 1 进行加密,然后客户端接收到密文,通过客户端的密钥 1进行解密,从而完成加密传输。

  • 总结:以上只是列举了常见的加密方案。总的来看,方案二比较简单,但是需要维护两套公钥和私钥,当公钥变化的时候,必须通知对方,灵活性比较差。方案三相对方案二来说,密钥 1 随时可以变化,并且不需要通知服务端,相对来说灵活性、安全性好点并且方案三对内容是对称加密,当数据量大时,对称加密的速度会比非对称加密快。所以本文采用方案三给予代码实现。

代码实现

  • 下面是具体的代码实现(以登录接口为例),主要的目的就是要把明文的个人信息转成密文传输。其中对称加密库使用的是 AES,非对称加密库使用的是RSA。
  • 客户端:

    • AES 库(aes-js):https://github.com/ricmoo/aes-js
    • RSA库(jsencrypt):https://github.com/travist/js...
    • 具体代码实现登录接口

      • 客户端需要随机生成一个 aesKey,在页面加载完的时候需要从服务端请求 publicKey

        let aesKey = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; // 随机产生
        let publicKey = ""; // 公钥会从服务端获取
        
        // 页面加载完之后,就去获取公钥
        window.onload = () => {
          axios({
            method: "GET",
            headers: { "content-type": "application/x-www-form-urlencoded" },
            url: "http://localhost:3000/getPub",
          })
            .then(function (result) {
              publicKey = result.data.data; // 获取公钥
            })
            .catch(function (error) {
              console.log(error);
            });
        };
      • aes加密和解密方法

        /**
         * aes加密方法
         * @param {string} text 待加密的字符串
   */
  function aesEncrypt(text, key) {
    const textBytes = aesjs.utils.utf8.toBytes(text); // 把字符串转换成二进制数据
  
    // 这边使用CTR-Counter加密模式,还有其他模式可以选择,具体可以参考aes加密库
    const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
  
    const encryptedBytes = aesCtr.encrypt(textBytes); // 进行加密
    const encryptedHex = aesjs.utils.hex.fromBytes(encryptedBytes); // 把二进制数据转成十六进制
  
    return encryptedHex;
  }
  
  /**
   * aes解密方法
   * @param {string} encryptedHex 加密的字符串
   * @param {array} key 加密key
   */
  function aesDecrypt(encryptedHex, key) {
    const encryptedBytes = aesjs.utils.hex.toBytes(encryptedHex); // 把十六进制数据转成二进制
    const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
  
    const decryptedBytes = aesCtr.decrypt(encryptedBytes); // 进行解密
    const decryptedText = aesjs.utils.utf8.fromBytes(decryptedBytes); // 把二进制数据转成utf-8字符串
  
    return decryptedText;
  }
  ```
- 请求登录
  
    ```javascript
    /**
     * 登陆接口
     */
    function submitFn() {
      const userName = document.querySelector("#userName").value;
      const password = document.querySelector("#password").value;
      const data = {
        userName,
        password,
      };
    
      const text = JSON.stringify(data);
      const sendData = aesEncrypt(text, aesKey); // 把要发送的数据转成字符串进行加密
      console.log("发送数据", text);
    
      const encrypt = new JSEncrypt();
      encrypt.setPublicKey(publicKey);
      const encrypted = encrypt.encrypt(aesKey.toString()); // 把aesKey进行非对称加密
    
      const url = "http://localhost:3000/login";
      const params = { id: 0, data: { param1: sendData, param2: encrypted } };
    
      axios({
        method: "POST",
        headers: { "content-type": "application/x-www-form-urlencoded" },
        url: url,
        data: JSON.stringify(params),
      })
        .then(function (result) {
          const reciveData = aesDecrypt(result.data.data, aesKey); // 用aesKey进行解密
          console.log("接收数据", reciveData);
        })
        .catch(function (error) {
          console.log("error", error);
        });
    }
    ```
  • 服务端(Node):

    • AES库(aes-js):https://github.com/ricmoo/aes-js
    • RSA 库(node-rsa):https://github.com/rzcoder/no...
    • 具体代码实现登录接口

      • 引用加密库

        const http = require("http");
        const aesjs = require("aes-js");
        const NodeRSA = require("node-rsa");
        const rsaKey = new NodeRSA({ b: 1024 }); // key的size为1024位
        let aesKey = null; // 用于保存客户端的aesKey
        let privateKey = ""; // 用于保存服务端的公钥
        
        rsaKey.setOptions({ encryptionScheme: "pkcs1" }); // 设置加密模式
      • 实现login接口

        http
          .createServer((request, response) => {
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Headers", "Content-Type");
            response.setHeader("Content-Type", "application/json");
            switch (request.method) {
              case "GET":
                if (request.url === "/getPub") {
                  const publicKey = rsaKey.exportKey("public");
                  privateKey = rsaKey.exportKey("private");
                  response.writeHead(200);
                  response.end(JSON.stringify({ result: true, data: publicKey })); // 把公钥发送给客户端
                  return;
                }
                break;
              case "POST":
                if (request.url === "/login") {
                  let str = "";
                  request.on("data", function (chunk) {
                    str += chunk;
                  });
                  request.on("end", function () {
                    const params = JSON.parse(str);
                    const reciveData = decrypt(params.data);
                    console.log("reciveData", reciveData);
                    // 一系列处理之后
        
                    response.writeHead(200);
                    response.end(
                      JSON.stringify({
                        result: true,
                        data: aesEncrypt(
                          JSON.stringify({ userId: 123, address: "杭州" }), // 这个数据会被加密
                          aesKey
                        ),
                      })
                    );
                  });
                  return;
                }
                break;
              default:
                break;
            }
            response.writeHead(404);
            response.end();
          })
          .listen(3000);
      • 加密和解密方法

        function decrypt({ param1, param2 }) {
          const decrypted = rsaKey.decrypt(param2, "utf8"); // 解密得到aesKey
          aesKey = decrypted.split(",").map((item) => {
            return +item;
          });
        
          return aesDecrypt(param1, aesKey);
        }
        
        /**
         * aes解密方法
         * @param {string} encryptedHex 加密的字符串
   */
  function aesDecrypt(encryptedHex, key) {
    const encryptedBytes = aesjs.utils.hex.toBytes(encryptedHex); // 把十六进制转成二进制数据
    const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5)); // 这边使用CTR-Counter加密模式,还有其他模式可以选择,具体可以参考aes加密库
  
    const decryptedBytes = aesCtr.decrypt(encryptedBytes); // 进行解密
    const decryptedText = aesjs.utils.utf8.fromBytes(decryptedBytes); // 把二进制数据转成字符串
  
    return decryptedText;
  }
  
  /**
   * aes加密方法
   * @param {string} text 待加密的字符串
   * @param {array} key 加密key
   */
  function aesEncrypt(text, key) {
    const textBytes = aesjs.utils.utf8.toBytes(text); // 把字符串转成二进制数据
    const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
  
    const encryptedBytes = aesCtr.encrypt(textBytes); // 加密
    const encryptedHex = aesjs.utils.hex.fromBytes(encryptedBytes); // 把二进制数据转成十六进制
  
    return encryptedHex;
  }
  ```

演示效果

总结

本文主要介绍了一些前端安全方面的知识和具体加密方案的实现。为了保护客户的隐私数据,不管是 HTTP 还是HTTPS,都建议密文传输信息,让破解者增加一点攻击难度吧。当然数据加解密也会带来一定性能上的消耗,这个需要各位开发者各自衡量了。

参考文献

看完这篇文章,我奶奶都懂了https的原理

中间人攻击

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com

查看原文

赞 7 收藏 3 评论 0

政采云前端团队 发布了文章 · 10月16日

ZooTeam 前端周刊|第 104 期

ZooTeam 前端周刊|第 104 期

浏览更多往期小报,请访问: https://weekly.zoo.team

阔别两年,webpack 5 正式发布了!

自从 2018 年 2 月,webpack4 发布以来,webpack 就暂时没有更进一步的重大更新,为了保持 API 的一致性,旧的架构没有做太多改变,遗留了很多的包袱。阔别 2 年多后,2020 年 10 月 10 日,webpack 5 正式发布,并带来了诸多重大的变更,将会使前端工程师的构建效率与质量大为提升。

react-router源码解析

上篇文章介绍了前端路由 的两种实现原理,今天我想从react-router源码分析下他们是如何管理前端路由的。因为之前一直都是使用V4的版本,所以接下来分析的也是基于react-router v4.4.0版本的(以下简称 V4),欢迎大家提出评论交流。Let's get started。

我写CSS的常用套路·续

CSS 魔法

简单实用又不花里胡哨的鼠标滑过样式

今天和大家分享我在网站里面常用到的鼠标滑过div的样式。

作为前端,我对业务的一点理解

三年前我毕业进入第一家公司,个人很水的技术能力让我经常在实际的开发工作中捉襟见肘,于是就想着一定要尽快提升自己的技术水平,每天都在公司待到很晚,除了做需求就是自我学习,在这种情况下,我几乎所有能坐在电脑前的时间都用在了技术上,这就造成了一种后果,那就是我只关心技术方面的东西,其他的我一概不管,并且越来越严重...

图解九种常见的设计模式_全栈修仙之路 - SegmentFault 思否

在软件工程中,设计模式(Design Pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。根据模式的目的来划分的话,GoF(Gang of Four)设计模式可以分为以下 3 种类型:

聊聊 React 两个状态管理库 Redux & Recoil_大前端 - SegmentFault 思否

React 是一个十分优秀的UI库, 最初的时候, React 只专注于UI层, 对全局状态管理并没有很好的解决方案, 也因此催生出类似Flux, Redux 等优秀的状态管理工具。

工作四年的前端不服:面试竟在CSS上跪了 究其原因……

一哥们,干了4年前端,竟然在css面试上跪了?

移动端 1px 问题解决方案

在移动端web开发中,UI设计稿中设置边框为1像素,前端在开发过程中如果出现border:1px,测试会发现在retina屏机型中,1px会比较粗,即是较经典的移动端1px像素问题。

我对JS延迟异步脚本的思考

我对JS延迟异步脚本的思考

查看原文

赞 0 收藏 0 评论 0

政采云前端团队 发布了文章 · 10月12日

浅析 vue-router 源码和动态路由权限分配

72 篇原创好文~ 本文首发于政采云前端团队博客:浅析 vue-router 源码和动态路由权限分配

浅析 vue-router 源码和动态路由权限分配

背景

上月立过一个 flag,看完 vue-router 的源码,可到后面逐渐发现 vue-router 的源码并不是像很多总结的文章那么容易理解,阅读过你就会发现里面的很多地方都会有多层的函数调用关系,还有大量的 this 指向问题,而且会有很多辅助函数需要去理解。但还是坚持啃下来了(当然还没看完,内容是真的多),下面是我在政采云(实习)工作闲暇时间阅读源码的一些感悟和总结,并带分析了大三时期使用的 vue-element-admin 这个 vuer 无所不知的后台框架的动态路由权限控制原理。顺便附带本文实践 demo 地址: 基于后台框架开发的 学生管理系统

vue-router 源码分析

首先阅读源码之前最好是将 Vuevue-router 的源码克隆下来,然后第一遍阅读建议先跟着 官方文档 先走一遍基础用法,然后第二遍开始阅读源码,先理清楚各层级目录的作用和抽出一些核心的文件出来,过一遍代码的同时写个小的 demo 边看边打断点调试,看不懂没关系,可以边看边参考一些总结的比较好的文章,最后将比较重要的原理过程根据自己的理解整理出来,然后画一画相关的知识脑图加深印象。

前置知识: flow 语法

JS 在编译过程中可能看不出一些隐蔽的错误,但在运行过程中会报各种各样的 bug。flow 的作用就是编译期间进行静态类型检查,尽早发现错误,抛出异常。

VueVue-router 等大型项目往往需要这种工具去做静态类型检查以保证代码的可维护性和可靠性。本文所分析的 vue-router 源码中就大量的采用了 flow 去编写函数,所以学习 flow 的语法是有必要的。

首先安装 flow 环境,初始化环境

npm install flow-bin -g
flow init

index.js 中输入这一段报错的代码

/*@flow*/
function add(x: string, y: number): number {
  return x + y
}
add(2, 11)

在控制台输入 flow ,这个时候不出意外就会抛出异常提示,这就是简单的 flow 使用方法。

具体用法还需要参考 flow官网,另外这种语法是类似于 TypeScript 的。

注册

我们平时在使用 vue-router 的时候通常需要在 main.js 中初始化 Vue 实例时将 vue-router 实例对象当做参数传入

例如:

import Router from 'vue-router'
Vue.use(Router)
const routes = [
   {
    path: '/student',
    name: 'student',
    component: Layout,
    meta: { title: '学生信息查询', icon: 'documentation', roles: ['student'] },
    children: [
      {
        path: 'info',
        component: () => import('@/views/student/info'),
        name: 'studentInfo',
        meta: { title: '信息查询', icon: 'form' }
      },
      {
        path: 'score',
        component: () => import('@/views/student/score'),
        name: 'studentScore',
        meta: { title: '成绩查询', icon: 'score' }
      }
    ]
  }
  ...
];
const router = new Router({
  mode: "history",
  linkActiveClass: "active",
  base: process.env.BASE_URL,
  routes
});
new Vue({
    router,
    store,
    render: h => h(App)
}).$mount("#app");

Vue.use

那么 Vue.use(Router) 又在做什么事情呢

问题定位到 Vue 源码中的 src/core/global-api/use.js源码地址

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    // 拿到 installPlugins 
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    // 保证不会重复注册
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }
    // 获取第一个参数 plugins 以外的参数
    const args = toArray(arguments, 1)
    // 将 Vue 实例添加到参数
    args.unshift(this)
    // 执行 plugin 的 install 方法 每个 insatll 方法的第一个参数都会变成 Vue,不需要额外引入
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    // 最后用 installPlugins 保存 
    installedPlugins.push(plugin)
    return this
  }
}

可以看到 Vueuse 方法会接受一个 plugin 参数,然后使用 installPlugins 数组保存已经注册过的 plugin 。 首先保证 plugin 不被重复注册,然后将 Vue 从函数参数中取出,将整个 Vue 作为 plugininstall 方法的第一个参数,这样做的好处就是不需要麻烦的另外引入 Vue,便于操作。 接着就去判断 plugin 上是否存在 install 方法。存在则将赋值后的参数传入执行 ,最后将所有的存在 install 方法的 plugin 交给 installPlugins维护。

install

了解清楚 Vue.use 的结构之后,可以得出 Vue 注册插件其实就是在执行插件的 install 方法,参数的第一项就是 Vue,所以我们将代码定位到 vue-router 源码中的 src/install.js源码地址

// 保存 Vue 的局部变量
export let _Vue
export function install (Vue) {
  // 如果已安装
  if (install.installed && _Vue === Vue) return
  install.installed = true
 // 局部变量保留传入的 Vue
  _Vue = Vue
  const isDef = v => v !== undefined
  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }
  // 全局混入钩子函数 每个组件都会有这些钩子函数,执行就会走这里的逻辑
  Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        // new Vue 时传入的根组件 router router对象传入时就可以拿到 this.$options.router
        // 根 router
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this)
        // 变成响应式
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 非根组件访问根组件通过$parent
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })
  // 原型加入 $router 和 $route
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })
  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })
// 全局注册
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)
// 获取合并策略
  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

可以看到这段代码核心部分就是在执行 install 方法时使用 mixin 的方式将每个组件都混入 beforeCreate,destroyed 这两个生命周期钩子。在 beforeCreate 函数中会去判断当前传入的 router 实例是否是根组件,如果是,则将 _routerRoot 赋值为当前组件实例、_router 赋值为传入的VueRouter 实例对象,接着执行 init 方法初始化 router,然后将 this_route 响应式化。非根组件的话 _routerRoot 指向 $parent 父实例。
然后执行 registerInstance(this,this) 方法,该方法后会,接着原型加入 $router$route,最后注册 RouterViewRouterLink,这就是整个 install 的过程。

小结

Vue.use(plugin) 实际上在执行 plugin上的 install 方法,insatll 方法有个重要的步骤:

  • 使用 mixin 在组件中混入 beforeCreate , destory 这俩个生命周期钩子
  • beforeCreate 这个钩子进行初始化。
  • 全局注册 router-viewrouter-link组件

VueRouter

接着就是这个最重要的 class : VueRouter。这一部分代码比较多,所以不一一列举,挑重点分析。 vueRouter源码地址

构造函数

  constructor (options: RouterOptions = {}) {
    this.app  = null
    this.apps = []
    // 传入的配置项
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    this.matcher = createMatcher(options.routes || [], this)
    // 一般分两种模式 hash 和 history 路由 第三种是抽象模式
    let mode = options.mode || 'hash'
    // 判断当前传入的配置是否能使用 history 模式
    this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
    // 降级处理
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode
    // 根据模式实例化不同的 history,history 对象会对路由进行管理 继承于history class
    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }
  }

首先在初始化 vueRouter 整个对象时定义了许多变量,app 代表 Vue 实例,options 代表传入的配置参数,然后就是路由拦截有用的 hooks 和重要的 matcher (后文会写到)。构造函数其实在做两件事情: 1. 确定当前路由使用的 mode2. 实例化对应的 history 对象。

init

接着完成实例化 vueRouter 之后,如果这个实例传入后,也就是刚开始说的将 vueRouter 实例在初始化 Vue 时传入,它会在执行 beforeCreate 时执行 init 方法

init (app: any) {
  ...
  this.apps.push(app)
  // 确保后面的逻辑只走一次
  if (this.app) {
    return
  }
  // 保存 Vue 实例
  this.app = app
  const history = this.history
  // 拿到 history 实例之后,调用 transitionTo 进行路由过渡
  if (history instanceof HTML5History) {
    history.transitionTo(history.getCurrentLocation())
  } else if (history instanceof HashHistory) {
    const setupHashListener = () => {
      history.setupListeners()
    }
    history.transitionTo(
      history.getCurrentLocation(),
      setupHashListener,
      setupHashListener
    )
  }
}

init 方法传入 Vue 实例,保存到 this.apps 当中。Vue实例 会取出当前的 this.history,如果是哈希路由,先走 setupHashListener 函数,然后调一个关键的函数 transitionTo 路由过渡,这个函数其实调用了 this.matcher.match 去匹配。

小结

首先在 vueRouter 构造函数执行完会完成路由模式的选择,生成 matcher ,然后初始化路由需要传入 vueRouter 实例对象,在组件初始化阶段执行 beforeCreate 钩子,调用 init 方法,接着拿到 this.history 去调用 transitionTo 进行路由过渡。

Matcher

之前在 vueRouter 的构造函数中初始化了 macther,本节将详细分析下面这句代码到底在做什么事情,以及 match 方法在做什么源码地址

 this.matcher = createMatcher(options.routes || [], this)

首先将代码定位到create-matcher.js

export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {
  // 创建映射表
  const { pathList, pathMap, nameMap } = createRouteMap(routes)
  // 添加动态路由
  function addRoutes(routes){...}
  // 计算新路径
  function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {...}
  // ... 后面的一些方法暂不展开
  
   return {
    match,
    addRoutes
  }
}

createMatcher 接受俩参数,分别是 routes,这个就是我们平时在 router.js 定义的路由表配置,然后还有一个参数是 router 他是 new vueRouter 返回的实例。

createRouteMap

下面这句代码是在创建一张 path-record,name-record 的映射表,我们将代码定位到 create-route-map.js源码地址

export function createRouteMap (
  routes: Array<RouteConfig>,
  oldPathList?: Array<string>,
  oldPathMap?: Dictionary<RouteRecord>,
  oldNameMap?: Dictionary<RouteRecord>
): {
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
  // 记录所有的 path
  const pathList: Array<string> = oldPathList || []
  // 记录 path-RouteRecord 的 Map
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
   // 记录 name-RouteRecord 的 Map
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
  // 遍历所有的 route 生成对应映射表
  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })
  // 调整优先级
  for (let i = 0, l = pathList.length; i < l; i++) {
    if (pathList[i] === '*') {
      pathList.push(pathList.splice(i, 1)[0])
      l--
      i--
    }
  }
  return {
    pathList,
    pathMap,
    nameMap
  }
}

createRouteMap 需要传入路由配置,支持传入旧路径数组和旧的 Map 这一步是为后面递归和 addRoutes 做好准备。 首先用三个变量记录 path,pathMap,nameMap,接着我们来看 addRouteRecord 这个核心方法。
这一块代码太多了,列举几个重要的步骤

// 解析路径
const pathToRegexpOptions: PathToRegexpOptions =
    route.pathToRegexpOptions || {}
// 拼接路径
const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
// 记录路由信息的关键对象,后续会依此建立映射表
const record: RouteRecord = {
  path: normalizedPath,
  regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
  // route 对应的组件
  components: route.components || { default: route.component },
  // 组件实例
  instances: {},
  name,
  parent,
  matchAs,
  redirect: route.redirect,
  beforeEnter: route.beforeEnter,
  meta: route.meta || {},
  props: route.props == null
    ? {}
    : route.components
      ? route.props
      : { default: route.props }
}

使用 recod 对象 记录路由配置有利于后续路径切换时计算出新路径,这里的 path 其实是通过传入父级 record 对象的path和当前 path 拼接出来的 。然后 regex 使用一个库将 path 解析为正则表达式。
如果 route 有子节点就递归调用 addRouteRecord

 // 如果有 children 递归调用 addRouteRecord
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })

最后映射两张表,并将 record·path 保存进 pathList,nameMap 逻辑相似就不列举了

  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }

废了这么大劲将 pathListpathMapnameMap 抽出来是为啥呢?
首先 pathList 是记录路由配置所有的 path,然后 pathMapnameMap 方便我们传入 path 或者 name 快速定位到一个 record,然后辅助后续路径切换计算路由的。

addRoutes

这是在 vue2.2.0 之后新添加的 api ,或许很多情况路由并不是写死的,需要动态添加路由。有了前面的 createRouteMap 的基础上我们只需要传入 routes 即可,他就能在原基础上修改

function addRoutes (routes) {
  createRouteMap(routes, pathList, pathMap, nameMap)
}

并且看到在 createMathcer 最后返回了这个方法,所以我们就可以使用这个方法

return {
    match,
    addRoutes
  }

match

function match (
  raw: RawLocation,
  currentRoute?: Route,
  redirectedFrom?: Location
): Route {
  ...
}

接下来就是 match 方法,它接收 3 个参数,其中 rawRawLocation 类型,它可以是一个 url 字符串,也可以是一个 Location 对象;currentRouteRoute 类型,它表示当前的路径;redirectedFrom 和重定向相关。
match 方法返回的是一个路径,它的作用是根据传入的 raw 和当前的路径 currentRoute 计算出一个新的路径并返回。至于他是如何计算出这条路径的,可以详细看一下如何计算出locationnormalizeLocation 方法和 _createRoute 方法。

小结

  • createMatcher: 根据路由的配置描述建立映射表,包括路径、名称到路由 record 的映射关系, 最重要的就是 createRouteMap 这个方法,这里也是动态路由匹配和嵌套路由的原理。
  • addRoutes: 动态添加路由配置
  • match: 根据传入的 raw 和当前的路径 currentRoute 计算出一个新的路径并返回。

路由模式

vue-router 支持三种路由模式(mode):hashhistoryabstract,其中 abstract 是在非浏览器环境下使用的路由模式源码地址

这一部分在前面初始化 vueRouter 对象时提到过,首先拿到配置项的模式,然后根据当前传入的配置判断当前浏览器是否支持这种模式,默认 ie9 以下会降级为 hash。 然后根据不同的模式去初始化不同的 history 实例。

    // 一般分两种模式 hash 和 history 路由 第三种是抽象模式不常用
    let mode = options.mode || 'hash'
    // 判断当前传入的配置是否能使用 history 模式
    this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
    // 降级处理
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode
    // 根据模式实例化不同的 history history 对象会对路由进行管理 继承于 history class
    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }

小结

vue-router 支持三种路由模式,hashhistoryabstract。默认为 hash ,如果当前浏览器不支持history则会做降级处理,然后完成 history 的初始化。

路由切换


切换 url 主要是调用了 push 方法,下面以哈希模式为例,分析push方法实现的原理 。push 方法切换路由的实现原理 源码地址

首先在 src/index.js 下找到 vueRouter 定义的 push 方法

  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    // $flow-disable-line
    if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        this.history.push(location, resolve, reject)
      })
    } else {
      this.history.push(location, onComplete, onAbort)
    }
  }

接着我们需要定位到 history/hash.js。这里首先获取到当前路径然后调用了 transitionTo 做路径切换,在回调函数当中执行 pushHash 这个核心方法。

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    // 路径切换的回调函数中调用 pushHash
    this.transitionTo(
      location,
      route => {
        pushHash(route.fullPath)
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      },
      onAbort
    )
  }

pushHash 方法在做完浏览器兼容判断后调用的 pushState 方法,将 url 传入

export function pushState (url?: string, replace?: boolean) {
  const history = window.history
  try {
   // 调用浏览器原生的 history 的 pushState 接口或者 replaceState 接口,pushState 方法会将 url 入栈
    if (replace) {
      history.replaceState({ key: _key }, '', url)
    } else {
      _key = genKey()
      history.pushState({ key: _key }, '', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

可以发现,push 底层调用了浏览器原生的 historypushStatereplaceState 方法,不是 replace 模式 会将 url 推历史栈当中。

另外提一嘴拼接哈希的原理

源码位置

初始化 HashHistory 时,构造函数会执行 ensureSlash 这个方法

export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    ...
    ensureSlash()
  }
  ...
  }

这个方法首先调用 getHash,然后执行 replaceHash()

function ensureSlash (): boolean {
  const path = getHash()
  if (path.charAt(0) === '/') {
    return true
  }
  replaceHash('/' + path)
  return false
}

下面是这几个方法

export function getHash (): string {
  const href = window.location.href
  const index = href.indexOf('#')
  return index === -1 ? '' : href.slice(index + 1)
}
// 真正拼接哈希的方法 
function getUrl (path) {
  const href = window.location.href
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}
function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}
export function replaceState (url?: string) {
  pushState(url, true)
}

举个例子来说: 假设当前URL是 http://localhost:8080,path 为空,执行 replcaeHash('/' + path),然后内部执行 getUrl 计算出 urlhttp://localhost:8080/#/,最后执行 pushState(url,true),就大功告成了!

小结

hash 模式的 push 方法会调用路径切换方法 transitionTo,接着在回调函数中调用pushHash方法,这个方法调用的 pushState 方法底层是调用了浏览器原生 history 的方法。pushreplace 的区别就在于一个将 url 推入了历史栈,一个没有,最直观的体现就是 replace 模式下浏览器点击后退不会回到上一个路由去 ,另一个则可以。

router-view & router-link

vue-routerinstall 时全局注册了两个组件一个是 router-view 一个是 router-link,这两个组件都是典型的函数式组件。源码地址

router-view

首先在 router 组件执行 beforeCreate 这个钩子时,把 this._route 转为了响应式的一个对象

 Vue.util.defineReactive(this, '_route', this._router.history.current)

所以说每次路由切换都会触发 router-view 重新 render 从而渲染出新的视图。

核心的 render 函数作用请看代码注释

  render (_, { props, children, parent, data }) {
    ...
    // 通过 depth 由 router-view 组件向上遍历直到根组件,遇到其他的 router-view 组件则路由深度+1 这里的 depth 最直接的作用就是帮助找到对应的 record
    let depth = 0
    let inactive = false
    while (parent && parent._routerRoot !== parent) {
      // parent.$vnode.data.routerView 为 true 则代表向上寻找的组件也存在嵌套的 router-view 
      if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++
      }
      if (parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    data.routerViewDepth = depth
    if (inactive) {
      return h(cache[name], data, children)
    }
   // 通过 matched 记录寻找出对应的 RouteRecord 
    const matched = route.matched[depth]
    if (!matched) {
      cache[name] = null
      return h()
    }
 // 通过 RouteRecord 找到 component
    const component = cache[name] = matched.components[name]
   // 往父组件注册 registerRouteInstance 方法
    data.registerRouteInstance = (vm, val) => {     
      const current = matched.instances[name]
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) {
        matched.instances[name] = val
      }
    }
  // 渲染组件
    return h(component, data, children)
  }

触发更新也就是 setter 的调用,位于 src/index.js,当修改 _route 就会触发更新。

history.listen(route => {
  this.apps.forEach((app) => {
    // 触发 setter
    app._route = route
  })
})

router-link

分析几个重要的部分:

  • 设置 active 路由样式

router-link 之所以可以添加 router-link-activerouter-link-exact-active 这两个 class 去修改样式,是因为在执行 render 函数时,会根据当前的路由状态,给渲染出来的 active 元素添加 class

render (h: Function) {
  ...
  const globalActiveClass = router.options.linkActiveClass
  const globalExactActiveClass = router.options.linkExactActiveClass
  // Support global empty active class
  const activeClassFallback = globalActiveClass == null
    ? 'router-link-active'
    : globalActiveClass
  const exactActiveClassFallback = globalExactActiveClass == null
    ? 'router-link-exact-active'
    : globalExactActiveClass
    ...
}
  • router-link 默认渲染为 a 标签,如果不是会去向上查找出第一个 a 标签
 if (this.tag === 'a') {
      data.on = on
      data.attrs = { href }
    } else {
      // find the first <a> child and apply listener and href
      const a = findAnchor(this.$slots.default)
      if (a) {
        // in case the <a> is a static node
        a.isStatic = false
        const aData = (a.data = extend({}, a.data))
        aData.on = on
        const aAttrs = (a.data.attrs = extend({}, a.data.attrs))
        aAttrs.href = href
      } else {
        // 不存在则渲染本身元素
        data.on = on
      }
    }
  • 切换路由,触发相应事件
const handler = e => {
  if (guardEvent(e)) {
    if (this.replace) {
      // replace路由
      router.replace(location)
    } else {
      // push 路由
      router.push(location)
    }
  }
}

权限控制动态路由原理分析

我相信,开发过后台项目的同学经常会碰到以下的场景: 一个系统分为不同的角色,然后不同的角色对应不同的操作菜单和操作权限。例如: 教师可以查询教师自己的个人信息查询然后还可以查询操作学生的信息和学生的成绩系统、学生用户只允许查询个人成绩和信息,不允许更改。在 vue2.2.0 之前还没有加入 addRoutes 这个 API 是十分困难的的。

目前主流的路由权限控制的方式是:

  1. 登录时获取 token 保存到本地,接着前端会携带 token 再调用获取用户信息的接口获取当前用户的角色信息。
  2. 前端再根据当前的角色计算出相应的路由表拼接到常规路由表后面。

登录生成动态路由全过程

了解 如何控制动态路由之后,下面是一张全过程流程图

前端在 beforeEach 中判断:

  • 缓存中存在 JWT 令牌

    • 访问/login: 重定向到首页 /
    • 访问/login以外的路由: 首次访问,获取用户角色信息,然后生成动态路由,然后访问以 replace 模式访问 /xxx 路由。这种模式用户在登录之后不会在 history 存放记录
  • 不存在 JWT 令牌

    • 路由在白名单中: 正常访问 /xxx 路由
    • 不在白名单中: 重定向到 /login 页面

结合框架源码分析

下面结合 vue-element-admin 的源码分析该框架中如何处理路由逻辑的。

路由访问逻辑分析

首先可以定位到和入口文件 main.js 同级的 permission.js, 全局路由守卫处理就在此。源码地址

const whiteList = ['/login', '/register'] // 路由白名单,不会重定向
// 全局路由守卫
router.beforeEach(async(to, from, next) => {
  NProgress.start() //路由加载进度条
  // 设置 meta 标题
  document.title = getPageTitle(to.meta.title)
  // 判断 token 是否存在
  const hasToken = getToken()
  if (hasToken) {
    if (to.path === '/login') {
      // 有 token 跳转首页
      next({ path: '/' })
      NProgress.done()
    } else {
      const hasRoles = store.getters.roles && store.getters.roles.length > 0
      if (hasRoles) {
        next()
      } else {
        try {
          // 获取动态路由,添加到路由表中
          const { roles } = await store.dispatch('user/getInfo')
          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
          router.addRoutes(accessRoutes)
          //  使用 replace 访问路由,不会在 history 中留下记录,登录到 dashbord 时回退空白页面
          next({ ...to, replace: true })
        } catch (error) {
          next('/login')
          NProgress.done()
        }
      }
    }
  } else {
    // 无 token
    // 白名单不用重定向 直接访问
    if (whiteList.indexOf(to.path) !== -1) {
      next()
    } else {
      // 携带参数为重定向到前往的路径
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }
})

这里的代码我都添加了注释方便大家好去理解,总结为一句话就是访问路由 /xxx,首先需要校验 token 是否存在,如果有就判断是否访问的是登录路由,走的不是登录路由则需要判断该用户是否是第一访问首页,然后生成动态路由,如果走的是登录路由则直接定位到首页,如果没有 token 就去检查路由是否在白名单(任何情况都能访问的路由),在的话就访问,否则重定向回登录页面。

下面是经过全局守卫后路由变化的截图

结合Vuex生成动态路由

下面就是分析这一步 const accessRoutes = await store.dispatch('permission/generateRoutes', roles) 是怎么把路由生成出来的。源码地址

首先 vue-element-admin 中路由是分为两种的:

  • constantRoutes: 不需要权限判断的路由
  • asyncRoutes: 需要动态判断权限的路由
// 无需校验身份路由
export const constantRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  }
  ...
  ],
 // 需要校验身份路由 
export const asyncRoutes = [
  // 学生角色路由
  {
    path: '/student',
    name: 'student',
    component: Layout,
    meta: { title: '学生信息查询', icon: 'documentation', roles: ['student'] },
    children: [
      {
        path: 'info',
        component: () => import('@/views/student/info'),
        name: 'studentInfo',
        meta: { title: '信息查询', icon: 'form' }
      },
      {
        path: 'score',
        component: () => import('@/views/student/score'),
        name: 'studentScore',
        meta: { title: '成绩查询', icon: 'score' }
      }
    ]
  }]
  ...

生成动态路由的源码位于 src/store/modules/permission.js 中的 generateRoutes 方法,源码如下:

 generateRoutes({ commit }, roles) {
    return new Promise(resolve => {
      let accessedRoutes
      if (roles.includes('admin')) {
        accessedRoutes = asyncRoutes || []
      } else {
      // 不是 admin 去遍历生成对应的权限路由表
        accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
      }
      // vuex 中保存异步路由和常规路由
      commit('SET_ROUTES', accessedRoutes)
      resolve(accessedRoutes)
    })
  }

route.js 读取 asyncRoutesconstantRoutes 之后首先判断当前角色是否是 admin,是的话默认超级管理员能够访问所有的路由,当然这里也可以自定义,否则去过滤出路由权限路由表,然后保存到 Vuex 中。 最后将过滤之后的 asyncRoutesconstantRoutes 进行合并。
过滤权限路由的源码如下:

export function filterAsyncRoutes(routes, roles) {
  const res = []
  routes.forEach(route => {
    // 浅拷贝
    const tmp = { ...route }
    // 过滤出权限路由
    if (hasPermission(roles, tmp)) {
      if (tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
      res.push(tmp)
    }
  })
  return res
}

首先定义一个空数组,对传入 asyncRoutes 进行遍历,判断每个路由是否具有权限,未命中的权限路由直接舍弃
判断权限方法如下:

function hasPermission(roles, route) {
  if (route.meta && route.meta.roles) {
    // roles 有对应路由元定义的 role 就返回 true
    return roles.some(role => route.meta.roles.includes(role))
  } else {
    return true
  }
}

接着需要判断二级路由、三级路由等等的情况,再做一层迭代处理,最后将过滤出来的路由推进数组返回。然后追加到 constantRoutes 后面

 SET_ROUTES: (state, routes) => {
    state.addRoutes = routes
    state.routes = constantRoutes.concat(routes)
  }

动态路由生成全过程

总结

  • vue-router 源码分析部分

    • 注册: 执行 install 方法,注入生命周期钩子初始化
    • vueRouter: 当组件执行 beforeCreate 传入 router 实例时,执行 init 函数,然后执行 history.transitionTo 路由过渡
    • matcher : 根据传入的 routes 配置创建对应的 pathMapnameMap ,可以根据传入的位置和路径计算出新的位置并匹配对应的 record
    • 路由模式: 路由模式在初始化 vueRouter 时完成匹配,如果浏览器不支持则会降级
    • 路由 切换: 哈希模式下底层使用了浏览器原生的 pushStatereplaceState 方法
    • router-view: 调用父组件上存储的 $route.match 控制路由对应的组件的渲染情况,并且支持嵌套。
    • router-link: 通过 to 来决定点击事件跳转的目标路由组件,并且支持渲染成不同的 tag,还可以修改激活路由的样式。
  • 权限控制动态路由部分

    • 路由逻辑: 全局路由拦截,从缓存中获取令牌,存在的话如果首次进入路由需要获取用户信息,生成动态路由,这里需要处理 /login 特殊情况,不存在则判断白名单然后走对应的逻辑
    • 动态生成路由: 传入需要 router.js 定义的两种路由。判断当前身份是否是管理员,是则直接拼接,否则需要过滤出具备权限的路由,最后拼接到常规路由后面,通过 addRoutes 追加。

读后感想

或许阅读源码的作用不能像一篇开发文档一样直接立马对日常开发有所帮助,但是它的影响是长远的,在读源码的过程中都可以学到众多知识,类似闭包、设计模式、时间循环、回调等等 JS 进阶技能,并稳固并提升了你的 JS 基础。当然这篇文章是有缺陷的,有几个地方都没有分析到,比如导航守卫实现原理和路由懒加载实现原理,这一部分,我还在摸索当中。

如果一味的死记硬背一些所谓的面经,或者直接死记硬背相关的框架行为或者 API ,你很难在遇到比较复杂的问题下面去快速定位问题,了解怎么去解决问题,而且我发现很多人在使用一个新框架之后遇到点问题都会立马去提对应的 Issues,以至于很多流行框架 Issues 超过几百个或者几千个,但是许多问题都是因为我们并未按照设计者开发初设定的方向才导致错误的,更多都是些粗心大意造成的问题。

参考文章

带你全面分析vue-router源码 (万字长文)

vuejs 源码解析

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com

查看原文

赞 8 收藏 7 评论 0

政采云前端团队 发布了文章 · 9月27日

编写高质量可维护的代码:一目了然的注释

71 篇原创好文~
本文首发于政采云前端团队博客:编写高质量可维护的代码:一目了然的注释

编写高质量可维护的代码:一目了然的注释

前言

有一些人认为,好的代码是自我解释的。合适的命名和优秀的代码的确可以减轻开发人员阅读代码的工作量,对于不是特别复杂的代码可能确实可以做到自我解释。但并不是所有场景都可以做到这一点,我们一起来了解一下“注释”吧。

编程语言中对“注释”的解释

注释就是对代码的解释和说明。注释是开发人员在编写程序时,给一段代码的解释或提示,有助于提高程序代码的可读性。注释不会被计算机编译。

要不要加注释?为什么要加注释?

注释的存在就是为了方便自己的二次阅读和代码维护以及项目交接。可以更好的理解代码,有助于提高协作效率,加快开发进程。

试想,你添加了一段逻辑较为复杂的代码,几个月后再看,还能不能迅速看懂?你刚刚接手一个老项目,项目里基本没有注释且逻辑复杂,你能高效率的看懂代码和了解业务吗?

所以添加注释还是有一定必要滴。

基础篇

快捷键 windows: ctrl+/ mac: command+/

注释的分类

一、 HTML 中的注释

<div>
      这是一行文字
      <!-- 这是一行被注释的文字 -->
</div>

二、CSS 中的注释

  • 在 .html 文件中
<style>
  div {
      /* color: #fff;  */
   }
</style>
  • 在 .css 文件中
div {
    /* color: #fff;  */
}
  • 在 .less 或 .scss 文件中
div {
    /* color: #fff;*/  /* 多行注释*/
    // font-size: 14px; // 单行注释
    background: #000;
}

三、JS 中的注释

  • 用法

    • 可用于解释 JavaScript 代码,增强其可读性。
    • 也可以用于阻止代码执行。
  • 单行注释(行注释)—— 以 // 开头。任何位于 // 之后的文本都会被注释
// 定义一个空数组
var ary = [];
var ary2 = []; // 又定义一个空数组
  • 多行注释(块注释)——以 /* 开头,以 */ 结尾。任何位于 /**/ 之间的文本都会被注释
/*
    这是多行注释
    定义一个数组
 */
var ary = [];
  • 用注释来阻止代码执行 —— 被注释的 JS 代码将不被执行
//alert("123")  // 执行时未弹出该信息
alert("456")  // 执行时弹出该信息
  • 函数注释

    • 一般以 /** 开头,以 */ 结尾。任何位于 /***/ 之间的文本都会被注释
/**
 * 提交
 *
 * @method onSubmit
 * @param {[Object]} 提交数据
 * @return  {[Bollean]}  [返回是否提交成功 ]
 */
const onSubmit = (params = {}) => {
  const result = false;
    if (params) {
            result = true;
        }
    return result;
};

四、特殊标记注释

  • TODO 在该注释处有功能代码待编写,待实现的功能在说明中会简略说明
  • FIXME 在该注释处代码需要修正,甚至代码是错误的,不能工作,需要修复,如何修正会在说明中简略说明
  • XXX 在该注释处代码虽然实现了功能,但是实现的方法有待商榷,希望将来能改进,要改进的地方会在说明中简略说明
  • NOTE 在该注释处说明代码如何工作
  • HACK 在该注释处编写得不好或格式错误,需要根据自己的需求去调整程序代码
  • BUG 在该注释处有 Bug
// TODO功能未完成,待完善
// FIXME  待修复
// XXX    实现方法待确认
// NOTE   代码功能说明
// HACK   此处写法有待优化
// BUG    此处有 Bug
const arr = []

Tips:

  • 为什么 // 注释可以在 .less 或 .scss 文件中使用,但是在 .html 和 .css 文件中不生效?

    • MDN 中关于 CSS 注释只有 /* */ 一种语法。但是在 LESS 和 SCSS 中支持注释的语法和 JS 中保持一致,有单行注释 // 和多行注释 /* */ 两种。单行注释编译之后不会被保留。
  • 单行注释为什么有时候写在代码上方,有时候写在代码后方?

    • 注释可以书写在代码中的任意位置。个人理解,一般写在代码上方的时候意为对后面一段代码的注释,而写在代码后方的时候意为对本行代码的注释。

注释写法规范

  • 文件注释

    • 位于文件头部,一般包含概要、作者、版本改动信息以及修改时间等内容
  /*
   * 简述当前文件功能
   * @author 作者名称
   * @version 版本号 最近编辑时间
   * @description 该版本改动信息
   */
  • 单行注释

    • 总是在 // 后留一个空格
  // 这是一行注释
  • 多行注释

    • 总是保持星号纵向对齐(结束符前留一个空格)
    • 不要在开始符、结束符所在行写注释
    • 尽量使用单行注释代替多行注释
    • 注释函数时,推荐使用多行注释
  /*
    这里有一行注释
    这里有一行注释
    这里有一行注释
   */
  • 函数注释

    • 其间每一行都以 * 开头,且与第一行第一个 * 对齐
    • 注释内容与 * 间留一个空格
    • 必须包含标签注释。例:
/**
* 方法说明
* @method 方法名
* @for 所属类名
* @param {参数类型} 参数名 参数说明
* @return {返回值类型} 返回值说明
*/

注释常用标签用法

  • @type {typeName}

    • * 表示任何类型
    • ? 表示可以为 null
    • ! 表示不能为 null
    • [] 表示数组
/**
* @type {number}
*/
var foo1;

/**
* @type {*}
* @desc 任何类型
*/
var foo2;

/**
* @type {?string}
* @desc string或者null
*/
var foo3;
  • @param {<type>} name - some description

    • 非必传参数需给参数名加上 []
    • 参数如有默认值需用 = 表示
    • 如果参数是 Object,可继续用 @param 对其属性进行详细说明
    • 若干个参数用 ... 表示
/**
 * @func
 * @desc 一个带参数的函数
 * @param {string} a - 参数a
 * @param {number} b=1 - 参数b默认值为1
 * @param {string} c=1 - 参数c有两种支持的取值  1—表示x  2—表示xx
 * @param {object} d - 参数d为一个对象
 * @param {string} d.e - 参数d的e属性
 * @param {object[]} g - 参数g为一个对象数组
 * @param {string} g.h - 参数g数组中一项的h属性
 * @param {string} [j] - 参数j是一个可选参数
 */
 function foo(a, b, c, d, g, j) {}

/**
 * @func
 * @desc 一个带若干参数的函数
 * @param {...string} a - 参数a
 */
function bar(a) {}

了解更多可查看 JSDoc

拓展篇

IE 条件注释(IE5+)

IE 条件注释分为以下几种情况:

  • 只允许 IE 解释执行 <!--[if IE]><![endif]-->
  • 只允许 IE 特定版本解释执行 <!--[if IE 7]><![endif]-->
  • 只允许非 IE 特定版本执行注释 <!--[if !IE 7]><![endif]-->
  • 只允许高于或低于 IE 特定版本执行注释 <!--[if gt IE 7]><![endif]-->
 <head>
      <title>IE 条件注释</title>
  
      <!-- 是 IE 时 -->
    <!--[if IE]> 
        <link href="style.css" rel="stylesheet" type="text/css" />
    <![endif]-->
  
    <!-- 是 IE 7 时 -->
          <!--[if IE 7]>
       <link href="style.css" rel="stylesheet" type="text/css" />
    <![endif]-->
 
    <!-- 不是 IE 7 时 -->
      <!--[if !IE 7]>
        <link href="style.css" rel="stylesheet" type="text/css" />
    <![endif]-->
  
      <!-- 大于 IE 7 时 -->
      <!--[if gt IE 7]>
       <link href="style.css" rel="stylesheet" type="text/css" />
    <![endif]-->
 
      <!-- 小于 IE 7 时 -->
       <!--[if lt IE 7]>
       <link href="style.css" rel="stylesheet" type="text/css" />
    <![endif]-->
</head>

(井号)注释 和 ''' (三引号)注释

  • # 一般出现在各种脚本配置文件中,用法与 JS 单行注释 // 基本相同。Python 中也常常用到
  • ''' 是 Python 中的多行注释语法,用两个 ''' 包含被注释的段落
# python 的单行注释一
    print("I could have code like this.") # python 的单行注释二

# print("This won't run.") # 被注释的代码

'''
    被三引号包裹的段落
    可以随意折行
    也可以注释代码
    print("This won't run.")
'''

注释 “被执行” 了?

众所周知,注释的代码是不会被执行的。但是小编在查资料时看到了一段比较有意思的代码, Java 中的一行注释“被执行”了?

public class Test {
    public static void main(String[] args) {
        String name = "赵大";
        // \u000dname="钱二";
        System.out.println(name);
    }
}

这段代码执行后的结果为钱二,也就是说在这段代码中,“被注释”的那行代码生效了!

这段代码的问题出在 \u000d 这串特殊字符上。\u000d 是一串 Unicode 字符,代表换行符。Java 编译器不仅会编译代码,还会解析 Unicode 字符。在上面这段代码把 \u000d 给解析了,后面的代码就到了下面一行,超出了被注释的范围(单行注释的注释范围仅在当前行),所以执行结果为 钱二 而非 赵大。(如下)

public class Test {
    public static void main(String[] args) {
        String name = "赵大";
        //
        name="钱二";
        System.out.println(name);
    }
}

所以本质上在代码执行的时候 name="钱二" 并没有被注释,而是被换了行(奇怪的知识增加了)。 所以切记,注释确实是不会被执行的哦!

注释相关插件

在这里推荐几个个人认为比较好用的注释相关的 Vscode 插件,可在 setting.json 文件下自定义设置(可通过 '文件—首选项—设置',打开 Vscode 文件 settings.json

  • koroFileHeader 在vscode中用于生成文件头部注释和函数注释的插件
  • 文件头部添加注释

    • 在文件开头添加注释,记录文件信息/文件的传参/出参等
    • 支持用户高度自定义注释选项, 适配各种需求和注释。
    • 保存文件的时候,自动更新最后的编辑时间和编辑人
    • 快捷键:windowctrl+alt+imacctrl+cmd+ilinuxctrl+meta+i

  • 在光标处添加函数注释

    • 在光标处自动生成一个注释模板
    • 支持用户高度自定义注释选项
    • 快捷键:windowctrl+alt+tmacctrl+cmd+tlinuxctrl+meta+t
    • 快捷键不可用很可能是被占用了,参考这里
    • 可自定义默认参数

  • Better Comments 通过使用警报,信息,TODO 等进行注释来改善代码注释。使用此扩展,您将能够将注释分类为:

    • 快讯
    • 查询
    • 待办事项
    • 强调
    • 注释掉的代码也可以设置样式,以使代码不应该存在
    • 可自定义指定其他所需的注释样式

  • TODO Highlight 突出显示TODO,FIXME和任何关键字

    • 高亮内置关键字,可通过自定义设置覆盖外观
    • 也可自定义关键字

用事实说话

口说无凭,眼见为实。下面我们看下实际开发中的具体情况:

  • 没有注释
const noWarehousetemIds = beSelectSkucontainer.reduce((arr, itemId) => {
    const res = Object.keys(selectRowskey[itemId]).every((skuId) => {
      const sku = selectRowskey[itemId][skuId];
      return !!sku.warehouseCode || lodashGet(warehouses, '[0].code');
    });
    if (!res) {
      arr.push(itemId);
    }
    return arr;
  }, []);
  if (noWarehousetemIds.length > 0 || noStockItemIds.length > 0) {
    const itemIds = Array.from(new Set([...noWarehousetemIds, ...noStockItemIds]));
    const itemNames = itemIds.map(i => this.itemNameMap[i].itemName);
    return Modal.warning({
      title: '错误提示',
      content: `“${itemNames.join(',')}”库存信息未完善,请完善库存信息`,
    });
  }
  • 一般般的注释
// 遍历当前所有选中的sku,查找出没有库存的itemId
const noStockItemIds = beSelectSkucontainer.reduce((arr, itemId) => {
  const res = Object.keys(selectRowskey[itemId]).every((skuId) => {
    const sku = selectRowskey[itemId][skuId];
    return !!sku.stockQuantity;
  });
  if (!res) {
    arr.push(itemId);
  }
  return arr;
}, []);
// 有一条sku的库存为空时进入校验
if (noStockItemIds.length > 0) {
  const itemNames = itemIds.map(i => this.itemNameMap[i].itemName);
  return Modal.warning({
    title: '错误提示',
    content: `“${itemNames.join(',')}”库存信息未完善,请完善库存信息`,
  });
}
  • 更好的注释
// 遍历当前所有选中的sku,查找出没有库存的itemId
const noStockItemIds = beSelectSkucontainer.reduce((arr, itemId) => {
    // selectRowskey是一个对象,以itemId为key,sku对象作为value,sku对象以skuId作为key,sku作为value,只有selectRowskey下所有itemId下的sku都有库存才算校验通过
    /*
        数据格式:
        selectRowskey: {
          12345678: { // itemId
              123456: { // skuId
              name: 'sku',
              }
          }
        }
      */
    const res = Object.keys(selectRowskey[itemId]).every((skuId) => {
        const sku = selectRowskey[itemId][skuId];
        return !!sku.stockQuantity;
    });
    // 只要有一条sku没有库存时,就塞到arr中,返回给noStockItemIds数组
    if (!res) {
        arr.push(itemId);
    }
    return arr;
}, []);
// 有一条sku的库存为空时进入校验
if (noStockItemIds.length > 0) {
    // 根据id查找商品名称
    const itemNames = itemIds.map(i => this.itemNameMap[i].itemName);
    Modal.warning({
        title: '错误提示',
        content: `“${itemNames.join(',')}”库存信息未完善,请完善库存信息`,
    });
}

看到上面这段代码可以很明显的体会到有没有注释以及注释写的清不清楚的重要性。若是写了注释但仍然看不懂,那还不如不写。

所以注释也不是随便写一写就可以的,要描述某段代码的功能,注明逻辑,让开发者可以”无脑“浏览。

之前在工作群中看到有人发过这样一张图(如下图),个人认为是一个很好的代码注释的范例:

结语

看到这里,对于注释的重要性各位已经有自己的认知。还有几点是我们写注释时需要注意的:

  • 注释内容要简洁、清楚明了。注释简述功能或实现逻辑即可,无需每行代码都添加注释
  • 代码若有修改,切记同步修改对应的注释。不要出现过期的注释,否则会起到反作用

    有任何意见欢迎下方评论区留言讨论~

参考文献

为什么要写注释
js/javascript代码注释规范与示例
代码中的特殊注解 -- TODO、FIXME、XXX的作用
注释的作用, 以及如何写注释
你确定Java注释不会被执行吗?80%的人都不知道
IE 浏览器条件注释详解
关于CSS中对IE条件注释的问题

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com

查看原文

赞 18 收藏 13 评论 0

政采云前端团队 发布了文章 · 9月25日

ZooTeam 前端周刊|第 101 期

ZooTeam 前端周刊|第 101 期

浏览更多往期小报,请访问: https://weekly.zoo.team

一位架构师的感悟:过度忙碌使你落后

我踩过的坑,希望大家不用再踩。

全面拥抱云原生应用研发的拐点已经到来_SegmentFault 行业快讯

简介:随着云原生 Serverless 的概念在国内悄然升起,许多技术人似乎从中看到了希望,许多 IT 架构师已经把它作为目标技术架构之一。Serverless 的跨代优势对有技术敏感的架构师来说是技术发展的红利,一般都在持续关注它的发展。但是在这两年间,随着整个研发生态接触到 Serverless 的内容也越来越多,尝试也越来越多。...

东半球最酷的学习项目

东半球最酷的学习项目 | 1、我写的三十万字算法图解 2、千本开源电子书 3、100 张思维导图 4、100 篇大厂面经 5、30 个学习专题,右上角点个 star,加入我们万人学习群!English Supported!

我来聊聊前端应用表现层抽象

表现层设计和选型

32个手撕JS,彻底摆脱初级前端(面试高频)

作为前端开发,JS是重中之重,最近结束了面试的高峰期,基本上offer也定下来了就等开奖,趁着这个时间总结下32个手撕JS问题,这些都是高频面试题,完全理解之后定能彻底摆脱初级前端。

注意!下个月开始 GitHub 新建存储库的默认分支就不叫“master”了!

用 main 代替 master ,是为什么呢?

Moment.js 进入维护状态:周下载量超 1200 万的 JS 库已经完成了它的使命

近日,Moment.js 库的维护者表示,Moment.js 将进入维护模式,后续将不会再对其进行更新。同时建议开发人员考虑替代方案。

不四:产品工程师的修炼之路

我是不四,毕业后一直在阿里和蚂蚁工作,不四是我在阿里的花名,社区中一般以另一个花名 “死马” 出现。每一个人的成长轨迹都不一样,一路上遇到的机遇也各不相同,这次分享也仅站在一个普通工程师的角度来分享我的成长经历和贯穿其中的一些个人习惯。

让我们来写个 webpack 插件

对 webpack plugin 的简要介绍。

Node.js 如何处理 ES6 模块 - 阮一峰的网络日志

Node.js 如何处理 ES6 模块 - 阮一峰的网络日志

Vue3.0全球发布会干货总结

观看Vue3全球发布会的总结笔记,翻译水平有限大家海涵。 最后有学习资料汇总。

查看原文

赞 0 收藏 0 评论 0

政采云前端团队 发布了文章 · 9月21日

浅谈 React 中的 XSS 攻击

70 篇原创好文~
本文首发于政采云前端团队博客:浅谈 React 中的 XSS 攻击

前言

前端一般会面临 XSS 这样的安全风险,但随着 React 等现代前端框架的流行,使我们在平时开发时不用太关注安全问题。以 React 为例,React 从设计层面上就具备了很好的防御 XSS 的能力。本文将以源码角度,看看 React 做了哪些事情来实现这种安全性的。

XSS 攻击是什么

Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击。XSS 攻击通常指的是利用网页的漏洞,攻击者通过巧妙的方法注入 XSS 代码到网页,因为浏览器无法分辨哪些脚本是可信的,导致 XSS 脚本被执行。XSS 脚本通常能够窃取用户数据并发送到攻击者的网站,或者冒充用户,调用目标网站接口并执行攻击者指定的操作。

XSS 攻击类型

反射型 XSS

  • XSS 脚本来自当前 HTTP 请求
  • 当服务器在 HTTP 请求中接收数据并将该数据拼接在 HTML 中返回时,例子:
   // 某网站具有搜索功能,该功能通过 URL 参数接收用户提供的搜索词:
   https://xxx.com/search?query=123
   // 服务器在对此 URL 的响应中回显提供的搜索词:
   <p>您搜索的是: 123</p>
   // 如果服务器不对数据进行转义等处理,则攻击者可以构造如下链接进行攻击:
   https://xxx.com/search?query=<img data-original="empty.png" onerror ="alert('xss')">
   // 该 URL 将导致以下响应,并运行 alert('xss'):
   <p>您搜索的是: <img data-original="empty.png" onerror ="alert('xss')"></p>
   // 如果有用户请求攻击者的 URL ,则攻击者提供的脚本将在用户的浏览器中执行。

存储型 XSS

  • XSS 脚本来自服务器数据库中
  • 攻击者将恶意代码提交到目标网站的数据库中,普通用户访问网站时服务器将恶意代码返回,浏览器默认执行,例子:
   // 某个评论页,能查看用户评论。
   // 攻击者将恶意代码当做评论提交,服务器没对数据进行转义等处理
   // 评论输入:
   <textarea>
      <img data-original="empty.png" onerror ="alert('xss')">
   </textarea>
   // 则攻击者提供的脚本将在所有访问该评论页的用户浏览器执行

DOM 型 XSS

该漏洞存在于客户端代码,与服务器无关

  • 类似反射型,区别在于 DOM 型 XSS 并不会和后台进行交互,前端直接将 URL 中的数据不做处理并动态插入到 HTML 中,是纯粹的前端安全问题,要做防御也只能在客户端上进行防御。

React 如何防止 XSS 攻击

无论使用哪种攻击方式,其本质就是将恶意代码注入到应用中,浏览器去默认执行。React 官方中提到了 React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串,因此恶意代码无法成功注入,从而有效地防止了 XSS 攻击。我们具体看下:

自动转义

React 在渲染 HTML 内容和渲染 DOM 属性时都会将 "'&<> 这几个字符进行转义,转义部分源码如下:

for (index = match.index; index < str.length; index++) {
    switch (str.charCodeAt(index)) {
      case 34: // "
        escape = '&quot;';
        break;
      case 38: // &
        escape = '&amp;';
        break;
      case 39: // '
        escape = '&#x27;';
        break;
      case 60: // <
        escape = '&lt;';
        break;
      case 62: // >
        escape = '&gt;';
        break;
      default:
        continue;
    }
  }

这段代码是 React 在渲染到浏览器前进行的转义,可以看到对浏览器有特殊含义的字符都被转义了,恶意代码在渲染到 HTML 前都被转成了字符串,如下:

// 一段恶意代码
<img data-original="empty.png" onerror ="alert('xss')"> 
// 转义后输出到 html 中
&lt;img data-original=&quot;empty.png&quot; onerror =&quot;alert(&#x27;xss&#x27;)&quot;&gt; 

这样就有效的防止了 XSS 攻击。

JSX 语法

JSX 实际上是一种语法糖,Babel 会把 JSX 编译成 React.createElement() 的函数调用,最终返回一个 ReactElement,以下为这几个步骤对应的代码:

// JSX
const element = (
  <h1 className="greeting">
      Hello, world!
  </h1>
);
// 通过 babel 编译后的代码
const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);
// React.createElement() 方法返回的 ReactElement
const element = {
  $$typeof: Symbol('react.element'),
  type: 'h1',
  key: null,
  props: {
    children: 'Hello, world!',
        className: 'greeting'   
  }
  ...
}

我们可以看到,最终渲染的内容是在 Children 属性中,那了解了 JSX 的原理后,我们来试试能否通过构造特殊的 Children 进行 XSS 注入,来看下面一段代码:

const storedData = `{
    "ref":null,
    "type":"body",
    "props":{
        "dangerouslySetInnerHTML":{
            "__html":"<img data-original=\"empty.png\" onerror =\"alert('xss')\"/>"
        }
    }
}`;
// 转成 JSON
const parsedData = JSON.parse(storedData);
// 将数据渲染到页面
render () {
    return <span> {parsedData} </span>; 
}

这段代码中, 运行后会报以下错误,提示不是有效的 ReactChild。

Uncaught (in promise) Error: Objects are not valid as a React child (found: object with keys {ref, type, props}). If you meant to render a collection of children, use an array instead.

那究竟是哪里出问题了?我们看一下 ReactElement 的源码:

const symbolFor = Symbol.for;
REACT_ELEMENT_TYPE = symbolFor('react.element');
const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // 这个 tag 唯一标识了此为 ReactElement
    $$typeof: REACT_ELEMENT_TYPE,
    // 元素的内置属性
    type: type,
    key: key,
    ref: ref,
    props: props,
    // 记录创建此元素的组件
    _owner: owner,
  };
  ...
  return element;
}

注意到其中有个属性是 $$typeof`,它是用来标记此对象是一个 `ReactElement`,React 在进行渲染前会通过此属性进行校验,校验不通过将会抛出上面的错误。React 利用这个属性来防止通过构造特殊的 Children 来进行的 XSS 攻击,原因是 `$$typeof 是个 Symbol 类型,进行 JSON 转换后会 Symbol 值会丢失,无法在前后端进行传输。如果用户提交了特殊的 Children,也无法进行渲染,利用此特性,可以防止存储型的 XSS 攻击。

在 React 中可引起漏洞的一些写法

使用 dangerouslySetInnerHTML

dangerouslySetInnerHTML 是 React 为浏览器 DOM 提供 innerHTML 的替换方案。通常来讲,使用代码直接设置 HTML 存在风险,因为很容易使用户暴露在 XSS 攻击下,因为当使用 dangerouslySetInnerHTML 时,React 将不会对输入进行任何处理并直接渲染到 HTML 中,如果攻击者在 dangerouslySetInnerHTML 传入了恶意代码,那么浏览器将会运行恶意代码。看下源码:

function getNonChildrenInnerMarkup(props) {
  const innerHTML = props.dangerouslySetInnerHTML; // 有dangerouslySetInnerHTML属性,会不经转义就渲染__html的内容
  if (innerHTML != null) {
    if (innerHTML.__html != null) {
      return innerHTML.__html;
    }
  } else {
    const content = props.children;
    if (typeof content === 'string' || typeof content === 'number') {
      return escapeTextForBrowser(content);
    }
  }
  return null;
}

所以平时开发时最好避免使用 dangerouslySetInnerHTML,如果不得不使用的话,前端或服务端必须对输入进行相关验证,例如对特殊输入进行过滤、转义等处理。前端这边处理的话,推荐使用白名单过滤,通过白名单控制允许的 HTML 标签及各标签的属性。

通过用户提供的对象来创建 React 组件

举个例子:

// 用户的输入
const userProvidePropsString = `{"dangerouslySetInnerHTML":{"__html":"<img onerror='alert(\"xss\");' data-original='empty.png' />"}}"`;
// 经过 JSON 转换
const userProvideProps = JSON.parse(userProvidePropsString);
// userProvideProps = {
//   dangerouslySetInnerHTML: {
//     "__html": `<img onerror='alert("xss");' data-original='empty.png' />`
//      }
// };
render() {
     // 出于某种原因解析用户提供的 JSON 并将对象作为 props 传递
    return <div {...userProvideProps} /> 
}

这段代码将用户提供的数据进行 JSON 转换后直接当做 div 的属性,当用户构造了类似例子中的特殊字符串时,页面就会被注入恶意代码,所以要注意平时在开发中不要直接使用用户的输入作为属性。

使用用户输入的值来渲染 a 标签的 href 属性,或类似 img 标签的 src 属性等

const userWebsite = "javascript:alert('xss');";
<a href={userWebsite}></a>

如果没有对该 URL 进行过滤以防止通过 javascript:data: 来执行 JavaScript,则攻击者可以构造 XSS 攻击,此处会有潜在的安全问题。
用户提供的 URL 需要在前端或者服务端在入库之前进行验证并过滤。

服务端如何防止 XSS 攻击

服务端作为最后一道防线,也需要做一些措施以防止 XSS 攻击,一般涉及以下几方面:

  • 在接收到用户输入时,需要对输入进行尽可能严格的过滤,过滤或移除特殊的 HTML 标签、JS 事件的关键字等。
  • 在输出时对数据进行转义,根据输出语境 (html/javascript/css/url),进行对应的转义
  • 对关键 Cookie 设置 http-only 属性,JS 脚本就不能访问到 http-only 的 Cookie 了
  • 利用 CSP 来抵御或者削弱 XSS 攻击,一个 CSP 兼容的浏览器将会仅执行从白名单域获取到的脚本文件,忽略所有的其他脚本 (包括内联脚本和 HTML 的事件处理属性)

总结

出现 XSS 漏洞本质上是输入输出验证不充分,React 在设计上已经很安全了,但是一些反模式的写法还是会引起安全漏洞。Vue 也是类似,Vue 做的安全措施主要也是转义,HTML 的内容和动态绑定的属性都会进行转义。无论使用 React 或 Vue 等前端框架,都不能百分百的防止 XSS 攻击,所以服务端必须对前端参数做一些验证,包括但不限于特殊字符转义、标签、属性白名单过滤等。一旦出现安全问题一般都是挺严重的,不管是敏感数据被窃取或者用户资金被盗,损失往往无法挽回。我们平时开发中需要保持安全意识,保持代码的可靠性和安全性。

小游戏

看完文章可以尝试下 XSS 的小游戏,自己动手实践模拟 XSS 攻击,可以对 XSS 有更进一步的认识。

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com

查看原文

赞 4 收藏 4 评论 0

政采云前端团队 发布了文章 · 9月18日

ZooTeam 前端周刊|第 100 期

ZooTeam 前端周刊|第 100 期

浏览更多往期小报,请访问: https://weekly.zoo.team

HOC真的就那么高级吗?你可知道还能这么玩

自己也调研了很多关于HOC的资料,其中确实也有很多写的比较好的文章。但如果你想问本篇文章的优势在哪里?唔...我可以斗胆的说,会更加详细,案例也会更加齐全。所以这篇文章我会从HOC是什么、怎么用它、用它需要注意什么等方面详细的去讲解,尽量让大家都能理解。

面试官问:大量的 TIME_WAIT 状态 TCP 连接,对业务有什么影响?怎么处理?

什么现象?什么影响?

智能UI:面向未来的UI开发技术

随着机器学习和人工智能渗入到各行各业,随着你画我猜、智能推荐、以图搜图、尬舞……渗入生活的方方面面,前端作为编程领域的一支,也必将迎来更多变化和挑战。

JavaScript错误处理完全指南

本文将介绍如何处理同步和异步JavaScript代码中的错误和异常。建议收藏!

JavaScript著名面试题: 0.1 + 0.2 !== 0.3,即将成为过去

From 阿里巴巴前端标准化组

他写出了 Vue,却做不对这十道 Vue 笔试题

请原谅我起了这么个浓浓营销号味道的标题。但这可丝毫没有夸大宣传,而是前端娱乐圈今日份的瓜—— 有十道关于 Vue 的选择题,在群里引出了一众社区知名人士竞折腰,最后钓出了 @尤雨溪 本人亲自挑战……

领域驱动设计在前端中的应用

在开始本篇文章前,我给读者们分享一个很考验人性的有趣现象,在公司洗手间的洗漱台旁边,放置了一个垃圾桶,每次我洗完手,用纸巾擦干手后,将其扔进垃圾桶,但是偶尔扔不...

vue的双向绑定原理及实现

vue的双向绑定原理及实现

CSS: filter: blur(); 实现高斯模糊效果,不可不知的细节优化

原文链接前言在项目中,要实现如下的效果:页面顶部的设计稿,前面一个卡片式的轮播,后边的背景(是椭圆的一部分)取前面的图片,进行一个高斯模糊的处理。开始前面的轮播部分,使用了第三方的轮播插件,非常好用,推荐给大家(地址)。轮播,不作为今天的主要内容,暂时简单描述下,有机会再详细讲解。有兴趣的同学可以自己试一试,根据插件提供的功能,进行一些样式调整即可。现在开始分析后面背景...

逐行分析鸿蒙系统的 JavaScript 框架

我在前文中曾经介绍过鸿蒙的 Javascript 框架,这几天终于把 JS 仓库编译通过了,期间踩了不少坑,也给鸿蒙贡献了几个 PR。今天我们就来逐行分析鸿蒙系统中的 JS 框架。

文中的所有代码都基于鸿蒙的当前最新版(版本为 677ed06,提交日期为 2020-09-10)。

⏰ Moment.js 宣布停止开发,现在该用什么?

Moment.js 宣布停止开发,进入维护状态。

这是一个大而全的时间日期库,极大方便了我们在 JavaScript 中计算时间和日期,每周下载量超过 1200 万,已成功用于数百万个项目中。

前端职业规划 - 现在很多前端团队都改名叫体验技术团队了, 那什么是体验技术?

一种职能或者工种的诞生都伴随着这类职能在整个价值创造的链条中定位的变化, 从软件工程师到按语言划分再到按职能划分, 每一个职能背后都有有一个核心内容驱动这项职能的发展, 职能发展的好就会有更长的生命周期和更宽广的衍生选择.

查看原文

赞 0 收藏 0 评论 0

政采云前端团队 发布了文章 · 9月14日

我的前端职业进阶之路

69 篇原创好文~
本文首发于政采云前端团队博客:我的前端职业进阶之路

自我介绍

大家好。我叫陈梦兰,花名沫沫,来自政采云团队,担任前端技术专家一职,实线带领 10 人负责项目采购业务线,虚线负责前端物料体系建设和前端 AI 智能化方向。今天给大家分享的主题是:我的前端职业进阶之路。

分享大纲

img

本次分享的内容主要分为两个部分:

第一部分,关于个人成长,整体概览下我的个人职业发展过程,以及在这个过程中,我在每个阶段所遇到的问题、瓶颈和破局的方法。

第二部分,主要是通过整个成长过程后,沉淀的一些经验总结和对未来的展望。

个人成长过程

img

第一阶段:一线执行

2014-2017 年,当时这个阶段的目标是:夯实技术基础扩展技术广度尽可能多的接触复杂的业务场景和技术方案

在编码上,要关注编码规范代码质量,尽可能每个需求都自己做下 Code Review,或者让师兄一起 Code Review,总结最佳实践。平时可以抽空看一些开源项目的源码,会有很多的收获。

另外,提及到扩展技术广度,我个人觉得比较好的一种方式是:从实际的场景或者问题出发,去寻找解决方案,过程中去学习和实践新的技术,这样成长速度会更快。

如果我们只是一门心思学了很多东西,而短时间内得不到实际场景的应用,一方面容易感觉到迷茫和困顿,另一方面,很多时候只有在实际场景中应用后才会发现一些问题和新的关联知识点。

第二阶段:业务接口人/师姐

2018 年开始带 2 个同学,负责一条业务线,独当一面。

当时除了自己作为那条业务线的核心开发以外,还需要协助新人/师弟成长,帮助他们提升技术能力和职业化能力。

作为业务接口人,需要把控技术方案的合理性和扩展性,了解业务的短期和长期规划考量需求的投入产出比尽可能的帮助业务实现利益最大化等等

回顾那个时候,其实也是综合能力快速提升的阶段。潜移默化中,跨团队的沟通协调能力风险把控能力业务理解力都有很大的提升

第三阶段:组长 Acting

2019 年开始带 7 - 8 个人,负责多条业务线。当时大家都是之前来自不同的业务团队,相互之间的熟悉度和信任度会差一些。

所以,刚开始我是刻意安排两两结对的方式来对接不同业务,让大家彼此之间快速熟悉起来。我一直认为,没有什么比一起 “打过仗” 来得更容易建立信任关系。

随着整个小组的凝聚力逐渐加强,而后,为了更好的支撑多条业务线,就要考虑小组的梯度建设,业务接口人的培养和我自己的 Backup 培养。

以上这些都是对小组内部的团队建设,除此之前,还需要具备某个领域的技术专项化能力,虚线能带动其他人共建和落地一些技术建设的事情

我个人在这个阶段负责的是前端物料体系的建设。当然,在这个阶段需要面对更多的人和事,更多跨团队的沟通和推动,对自身软实力的培养也会很有帮助。

当前阶段:Team Leader

从工作内容上来说,很多跟去年是类似的,今年负责的也是对我而言崭新的业务和团队。

目前对我而言,也需要进一步强化体系化能力对内对外的影响力跨部门的推动和建设梯队建设人才培养绩效管理(确定绩效目标,过程跟进,结果评估)等等。

经验总结

第一阶段:一线执行

img

在一线执行阶段,当时遇到的问题是,在外企这样一个比较安逸的环境工作了 3 年后,明显感觉到自己的成长已经进入了滞缓期,同时在工作节奏偏慢的氛围下,感觉这样下去自己会走下坡路。

所以,当时的选择是跳槽来到了政采云。但在半年多之后,已经逐步熟悉了业务和技术栈,能独立负责后,发现自己本质上还是在做 “纯业务执行” 的事情,跟之前没什么太大的区别。

这边讲的 “纯业务执行” 指的是,能够很好的把业务需求做完并按时交付,但并不会思考如何从业务和技术的角度出发,做得更好。

破局:认知的突围

img

这个阶段的破局,很大程度是思想和认知方面的转变。

我意识到,换平台并不能解决根本问题,而本质上是我把自己当做了一个单纯的执行角色,觉得做完了产品需求就够了。

但业务支撑并不只是如期交付业务需求就可以了,而是要自驱的去发现业务中的一些痛点问题寻找解决方案并推进落地然后再不断迭代优化形成一个完整的闭环。其实就是从做完到做好,给业务带来改变。

有时候我会想,对公司而言,我的个人价值点在哪里,如果只是做业务纯执行,那么其实所有人都是一样,因为做完业务是最最基本的,本职工作而已。

所以,个人价值的体现在于能发现别人发现不了的问题点解决别人解决不了的事情

案例:知识问答小机器人

这边给大家分享一个具体的 Case。当时有个知识问答小机器人的业务需要交接到我这边。

交接过程中,和业务方、产品提前沟通了后续一个季度的业务规划,期望在全平台所有业务的所有页面全部植入这个小机器人,总计 100+ 个前端应用。

但基于当时植入的技术方案,这个植入成本非常高,每个业务线需要挨个手动接入,并且后续小机器人的迭代,各业务线也需要发版。

显然当时的技术方案对后续业务的扩展是有影响的,所以,决定推动重构,最终实现了个业务线的 0 成本植入,后续迭代各业务线也无感知。

这个 Case 其实很小,但可能大家在日常工作中都会遇到类似的问题。

我们可以总结 2 点经验:

  • 第一,主动提前了解业务后续规划,不要只看当下产品导入的需求,会对技术方案的选择很有帮助
  • 第二,需要有一定的洞察力,能发现一些痛点问题,寻找解决方案,并推进落地,帮助业务快速推广

复盘

img

针对一线执行阶段,我做了下复盘和总结,主要分为 4 个方面:在这个阶段的基本要求、更高的标准、素质瓶颈以及能力结构的瓶颈。

基本要求:

包括业务理解和支撑能力;独立承担和独立执行;扎实的技术功底和编码质量;风险反馈意识。其中风险反馈意识是比较容易忽略的,这也是一个比较重要的基本要求。

  • 为什么风险反馈意识这么重要

    我们很难做到所有需求在具体执行过程中,确保完全没有风险。导致风险的原因有很多:

    • 比如前期技术方案评估有疏漏;
    • 在研发过程中需求有变更;
    • 需求拆解不够细,导致估时不够准确等等。
  • 假设真的出现了风险,那及时反馈就很重要了,如果是在最后一刻搞不定了才反馈的话,那团队也无法横向协助解决问题,可能最终会导致项目延期
  • 千万不要觉得反馈风险是不是显得自身能力不够,在我看来,及时反馈风险是职业化的体现。事后可以复盘,后续如何规避类似风险的发生,但当下最有效的就是及时反馈问题
  • 这个阶段往往已经一个人负责某个业务模块或者业务线了,所以,业务理解力、独立承担和独立执行也很重要,这点能体现出我们的技术功底和解决问题的能力
更高标准:

包括对编码质量的精益求精;自驱主动的发现痛点问题,具备强执行力,针对问题能输出好的解决方案并推动落地。

  • 一部分人可能对问题的感知力比较弱,本质上也没有精益求精的习惯。
  • 还有一部分人缺乏的不是对问题的感知力,而是发现问题后的视而不见,可能会吐槽下,但不会主动去寻找解决方案,并推动落地解决。
  • 但其实我们的目标是问题能够最终得以落地解决,所以就需要具备强执行力
素质瓶颈,体力

在这个阶段大多数事情都是通过体力来解决,下个阶段就是用脑力来解决问题。

能力结构瓶颈

包括业务理解能力、技术方案能力、沟通能力、反馈意识、推动落地的强执行力。

第二阶段:业务/团队核心

img

在作为业务接口人/师姐的阶段,开始带几个新人,负责一条大的业务线。当时遇到的问题用一个字概括就是 “”,但这种情况下的忙其实是有问题的。

由于当时自己在这条业务线待的时间比较长,对整体业务更熟悉,所以其他协作方遇到问题会习惯于找到我。

刚开始很多事情还是习惯于自己来解决,每天加班到很晚,而对于新人而言,反而得不到更快速的成长,小组整体的业务支撑能力也偏低。

破局:思考和复盘

img

越忙的时候,越要抽出时间来思考和复盘

当变成业务接口人之后,不仅要考虑自己个人的能力提升,更要学会靠一群人的力量解决问题,更要考虑其他人的成长小组整体的业务支撑能力

要给新人足够的授权和信任,制定新人培养计划,在过程中进行辅导,每周阶段性沟通,协助新人快速成长。

同时,每周组织小组内部 Code Review业务分享技术分享等等,这些都是提升小组成员业务理解力、技术能力以及整体产出能力的很好方式。

案例:物料体系0到1的建设

img

这个阶段,除了实线负责业务线以外,还虚线主导整个前端物料体系的建设

这边给大家解释下什么是 “虚线” 的技术建设。在我们公司并不是由独立的前端架构组来做技术建设,而是来自不同业务线的同学组成 “虚线” 小组的形式。

本身所有的技术建设其实都是为业务服务的,业务线的同学会更深入了解业务上的问题,以及如何落地。

接下去,我大概回顾下,如何从 0 到 1 规划和建设整个物料体系的。

img

当时我们面临了什么问题呢?

  • 第一,由于历史原因,各业务线的视觉规范不统一
  • 第二,当时公司内部只有一个 React 选型的 UI 组件库,对整个物料体系而言还只是很薄的一层

我们知道物料体系建设很明确的目标就是对内进行研发提效。首先,我们思考一个问题,如何实现研发提效?对物料体系而言,很直观的 2 个方向就是通过物料复用工具化来提效。

那物料应该包含哪些内容呢?因为前端交付的是完整的页面,我们可以把一个页面拆解为模板业务组件UI 组件底层视觉规范,具体详细见上图。

而工具化其实就是为了让研发同学在使用物料的过程中更加便捷,进一步提效。当然,还有任何技术建设中,必不可少的一个模块,就是量化数据统计

数据可以很好的反映出整个物料体系在业务线的使用情况,也是为后续物料体系不断优化迭代提供数据支撑

其实,我的第一版规划里面并没有全部涵盖上面所有的内容,大概只有 70% 左右。我想说的是,有时候很多事情一下子想不全没关系这是一个逐渐完善的过程。先 0 到 1 建设最最核心的能力,然后再不断丰富完善,进行 1 到 10 的优化迭代。

img

另外,这边也给大家分享下,如何做好整体规划?明确且细致的规划能够让大家更有目标感方向感,也能更好的把控推进节奏

一种思维方式是,先确定大方向需要做什么,然后细化各种 Action,最后思考这些 Action 达成了哪些目标。

但其实更好的是以下这种方式:

  1. 从业务/技术场景出发,分析背景/现状
  2. 根据现状,分析和罗列出痛点和问题
  3. 为了解决这些痛点和问题,我们需要指定的整体目标是什么
  4. 将整体目标进行细化拆解成阶段性小目标
  5. 针对每个阶段性小目标,思考要达成阶段性小目标,有哪些可突破的方向
  6. 有了具体方向后,针对性的拆解成对应的 Action
  7. 制定里程碑计划
  8. 落地和推广

复盘

img

针对业务接口人/师姐阶段,同样做了复盘和总结,主要分为 4 个方面:在这个阶段的基本要求、更高的标准、素质瓶颈以及能力结构的瓶颈。

  1. 基本要求,包括深入理解业务、独当一面、培养新人/师弟、把控风险、把控技术方案、沟通协调能力。
  2. 更高标准,包括考虑投入产出比(ROI)、帮业务赢、带动和影响他人。

    • 为什么投入产出比的考量很重要?

      • 在业务高速发展的公司,业务需求的增加速度是非常快的,通常需求体量是超过研发资源负荷量的。
      • 在研发资源有限的情况下,我们需要考虑投入产出比,和产品一起实现业务价值交付的最大化
  3. 素质瓶颈脑力
  4. 能力结构瓶颈,包括主导能力、流程感知、ROI 考量、跨部门的项目 PM 能力。

第三阶段:Leader

img

第三阶段 Leader 阶段,因为今年正好刚轮岗到了一个新的团队,所以我着重讲下今年遇到的一些问题以及解决方法。

来到这个崭新的团队,第一件事就是盘人盘事,过程中我发现了以下几个问题:

  • 业务流程链路长,复杂度高,研发同学对整体业务熟悉度不够
  • 部分项目的技术选型老旧(jQuery),历史 “债务” 较多
  • 合作伙伴同学占比较高,代码质量参差不齐
  • 需求迭代流程不合理,一线同学疲惫感较重

破局:团队环境升级

img

面对上述这些问题,目前我的解决方法如下:

  • 分享沉淀:加强内部业务分享,建立业务知识库
  • 技术选型升级:降低复杂度和维护成本,研发提效
  • 流程规范:标准化、合理化
  • 梯队建设:培养自己的 Backup。每条业务线都配备业务接口人+合作伙伴。每个新人指定对应师兄,一对一帮带
  • 团队凝聚力&氛围:互帮互助、乐于分享、群策群力

复盘

img

作为 Leader,我也还在不断地学习和成长中。回顾在这个阶段的经历,我也简单做了下复盘和总结,同样分为 4 个方面:在这个阶段的基本要求、更高的标准、素质瓶颈以及能力结构的瓶颈。

  1. 基本要求,包括梯队建设、绩效管理、沟通协调、推动优化&改变。
  2. 更高标准,包括破圈建设、跨部门项目的 PM 能力、探索新的技术领域。

    • 破圈建设就是跨出前端职能部门,以更开阔的视野去看,各个职能部门之间更好的协作和共建,帮助业务拿到更好的结果。
  3. 素质瓶颈心力
  4. 能力结构瓶颈,包括前瞻性、目标感、主导力、整合力、推动并拿结果的能力、影响力、领导力。

职能·能力矩阵

img

上述图表达的是作为一个优秀的职业化的前端,所该具备的能力项矩阵。

纵向分成 4 块,业务支撑力技术创新力组织发展力内外影响力。很多能力项之前都有详细讲过,这边就不再赘述了。

其实每个人的成长过程都是在不断的丰富和提升自己的能力项:

  • 业务非常忙的时候,过程中可以重点提升自己业务支撑力
  • 业务相对稳定的时候,可以做一些技术建设,更好的为业务赋能。

Tips

  • 任何职业阶段,过硬的技术功底都是立身之本
  • 关注行业动态、了解新技术趋势
  • 横向/纵向对比,三人行必有我师
  • 复盘,勤总结
  • 保持利他共赢的思维

未来:智能化

img

提到未来,我所想到的一个词就是 “智能化”。过去的几年我们用 Low Code 搭建的方式完成了一次大幅度的提效,但搭建最终还是要依赖于组件的迭代。

相信接下去就是智能化的时代,简单的 HTML + CSS 都可以通过智能化的方式直接从 UI 视觉稿生成可用代码。

目前我们已经开始了 “前端智能化(UI to Code)” 方面的探索,对这方面有兴趣的同学,欢迎加入我们。

好书推荐

img

我个人非常推荐《金字塔》这边书籍,它能够帮助我们强化思考架构能力和逻辑沟通能力,帮助我们更高效的思考、表达和解决问题。

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com

查看原文

赞 9 收藏 5 评论 1

政采云前端团队 发布了文章 · 9月11日

ZooTeam 前端周刊|第 99 期

ZooTeam 前端周刊|第 99 期

浏览更多往期小报,请访问: https://weekly.zoo.team

「一劳永逸」一张脑图带你掌握Git命令

最近在网易工作之余,遇到Git上面一些问题,趁这次的机会,补一补Git基础知识。

在 React 中实现 keep alive

什么是 keep alive在 Vue 中,我们可以使用 keep-alive 包裹一个动态组件,从而缓存不活跃的实例,而不是直接销毁他们……

浅谈 TypeScript 类型系统

时下,TypeScript 可谓当红炸子鸡,众多知名开源项目纷纷采用,大型项目几乎成为必备。其中缘由,除了微软这一开源项目新势力的强大背书以外,最为核心的即是 TypeScript 的类型系统。JavaScript 太需要一套合适的…

React Hooks 入门教程

React Hooks 入门教程

完全理解React Fiber

react fiber 原理

蚂蚁金服如何把前端性能监控做到极致

蚂蚁金服如何把前端性能监控做到极致

技术专题第四期 | 我们的微前端是如何炼成的

微前端相关的尝试,我很早就有尝试,当时还没有这么百花齐放跟铺天盖地的资料可以参考. 全靠当初的一些概念与实践将这套方案落了地.相信很多实践微前端的同学也看过这系列文章. 看掘金有相关的征文,今天就发掘金了.

【前端体系】从一道面试题谈谈对EventLoop的理解

对于Event Loop(事件轮询)所涉及的知识概念太多了,如果上来就讲一大堆概念性的东西太枯燥且从一开始就是按照我的思路来走的,所以我打算换一种方式来写这篇文章,你先按照你之前对于Event Loop(事件轮询)的理解来解这道题,我在后面写出我从Event Loop的理解思考这题的方式。

Dooring可视化之从零实现动态表单设计器

从零实现一款动态表单设计器

网页布局都有哪种?一般都用什么布局?

在Web布局中,现代CSS特性就可以更好的帮助我们快速实现,例如等高布局,水平垂直居中,经典的圣杯布局、宽高比例、页脚保持在底部等。在本文中,我将会介绍一些不同的CSS属性来实现这些效果,希望大家会感兴趣。

查看原文

赞 1 收藏 1 评论 0

政采云前端团队 发布了文章 · 9月7日

从实习到入职:与你分享我在政采云的工作和成长

68 篇原创好文~ 本文首发于政采云前端团队博客:从实习到入职:与你分享我在政采云的工作和成长

前言

大家好,我是 2020 年秋招入职政采云公司的前端开发者,作为疫情年的本科毕业生,想与大家、尤其是 2021 届毕业的弟弟妹妹和哥哥姐姐们分享一些技术以外的事情,比如,我作为一个菜鸡小萌新,是怎样接触到政采云这样一个公司,在政采云工作给我带来了怎样新奇的体验,跟随着优秀的同事们我学到了什么,这也是对我自己的一个总结吧。

以人为镜,可观来路,知远方。我年轻愚笨,文字拙劣,愿能不辜负你阅读的几分钟。

相逢于秋招

我是 2019 年的秋天的时候接触到政采云公司的。

当时,秋招大场,各家宣讲开得如火如荼,“政采云”这个名不见经传的公司名被淹没在了浩荡浪潮里,它曾经确实没引起我的注意,即使提前搜索了相关资料,看到了某招聘 APP上贴出的本公司图片,也几乎没有产生多少“我想去这家公司”的想法,而且,尤其是了解到政采云是关于面向“政府采购”的 to G 的业务时,让我实在是有点心慌:它与一众竞争得如火如荼、团队气氛年轻热烈的 to B 业务公司相比,这样的业务内容会不会让工作氛围变得有些单调、重复性高?同事关系比较复杂,技术氛围不强?

后来旁听宣讲会的时候,就打消了我的一部分疑虑,一开始,公司播放了一个特别炫酷、热情又有创意的宣传视频,先在我心里怒刷了第一波好感,猜想这里的工作也会比较有趣。接着,一下午的宣讲会上,公司的 3、4 位 Leader 们陆续出席,各自从自己擅长的角度详细地讲述了公司的业务和发展方向,又让我觉得他们有认真地对待校招生,从行为上尊重和认可我们,当做一起共事的人、当做平等的朋友,而不是把我们当做不谙世事的新人,需要被单方面捶打教导。

后来,经历了两波面试以后,当我忐忑地等待三面的面试官 @堂主 进行下一场灵魂拷问,却没想到他自顾自地跟我展示了很久自己团队开发的谷歌插件、桌面应用、搭建系统、埋点系统,几乎忘了我才是来面试的人,短短几分钟下来,让我觉得这个神秘高手(其实进去后才发现是前端部门的老大)很坦率有趣,很务实,也对自己团队的技术力量非常有自信,如果我在这里工作,想必也能跟着大家一起努力,得到更快的进步和成长。

没想到,一晃一年过去,去年秋天那个凭着直觉懵懵懂懂跟着来到政采云的傻学生,如今已经在这一家公司度过了冬、春、夏,从秋招走向了实习,又从实习走向了入职。

从学校走向工作

入职培训 + 企业文化

以前在学校里是没有入职培训这样的东西的,我从心底里总觉得类似的活动都是一些场面话和套话,也很难想象到这些官方活动能给人带来什么实质性的提升,但是参加了公司针对新人的“百乐门”和针对校招生的“青云计划”以后,又真实地感觉到,关于提到的企业文化,诸如“客户第一”、“诚信”、“持续改善”、“自驱”等,自己的对很多事的看法心态都因此发生了巨大的变化。

以前在学校里,我是很难深入理解“合作”的,觉得“合作”充其量是和同学朋友好好相处,学生干部负起责任,参加合作性比赛的时候各司其职、互相理解,真正的学习生活还是要靠每个人读书练习,有些“各顾各的”的意味吧,而且,一遇到组织班级活动的时候,就难免会出现不配合、意志不集中的情况,而这种问题,在工作上肯定是需要避免的。

我们的工作生活中常常会牵扯到不同的角色,作为一个开发来说,前面有业务方、有产品去调研,对于不同岗位来说,每个人都是一块各有所长的七巧板,每一次版本的迭代、项目的落地,都需要各个环节稳中求好,更进一步。每个人都会有自己的看法,倾听、理解、表达、确认、同步,其中每一步都变得尤为重要,而这些,是我当初处于未毕业、或者刚毕业的人,是不能一步登天地领会的,也需要一次次培训的过程中耳濡目染地理解,通过一个个小游戏亲身体会——“我们应当如何达到合作共赢的终点”。

承担业务的基础:工作排期

以前我在学校里的时候,是不太注重排期和目标的,脑子里总会有一种“计划赶不上变化”的想法。虽然也能知道自己最近想学什么,还不至于浑浑噩噩枉度光阴,但是也确实很难和别人同步分享,做到完全可控,这在团队合作的工作中,是非常不合适的。

在工作中,我们的每一天任务和进度都应该有计划,这样才能保证无差错地按期交付,有延期风险才能及时解决。对新人开发者来说,也只有做到心中有数,才不会一直是一个“被安排工作的人”,既可以避免加班过重,也能避免无事可做,让我们慢慢具备起独立肩负起责任、承担业务的能力。

关于我学习到的在开发之前需要协作排期的步骤,画了如下的简单示意图。

开发成本和实际效果

我以前在实习和在学校里做作品的时候,思维比较简单,很少会注意和评估开发成本,如果安排了什么困难的任务,总觉得延长开发时间就可以解决了,但是来到公司以后发现事情不是这样的。

每一日的工作时间都是一项支出,而开发的实际效果是这份支出带来的意义,当支出配得上意义的时候,我们才可以肯定支出是值得的,否则很可能就是一种“浪费”。假如存在开发成本和实际效果不对等情况,我们需要提前和产品人员说明,可以协调出一个可实现的临时方案,再慢慢迭代,或者争取宽限的时间,就像买东西一样,尽可能地做到物有所值,而不是盲目开发。

而至于他的效果好不好,我们可以通过公司的埋点系统查看它的点击量、浏览量等,判断它的使用率,也可以去问相关的业务方、产品方、运营方来收集有效的信息。

互相沟通,两相权衡,目的在于更好地完善这个作品,而不是一味地争吵、给自己加压,又或者埋怨同事,这些都不是我们所期望的。

同心同德做事,快快乐乐生活

之前未出学校大门的时候,深知自己总是被称为“象牙塔”里的孩子,对社会、工作的“毒打”总是会抱有一些不确定的担忧,比如会不会有同事不配合工作,推卸责任,会不会有复杂的办公室斗争、上下级关系之类的,但是来到公司以后,就会发现,这种担心是多余的。

公司的人员结构非常扁平化,不论是几级部门的 Leader 都不会有架子,不论是技术还是业务,有什么问题都可以当面提出,大家也很乐于讨论和分享自己的观点。我们工作的时候尽心尽力,开会的时候尽情发散头脑风暴,而余下的时间在周围的饭店、KFC 一起吃吃喝喝、玩玩闹闹,关系甚至比同一个班级的同学还和乐融洽。

就活动而言,百乐门是我最早参加的活动了,对新入职的同学开放三天的培训 + 实践活动,其中也包括户外拓展,将每一批新人组成 6 到 8 人的小组,一边游湘湖/西湖,一边打卡景点、完成任务,有点像电视上的综艺活动,也是为了让我们彼此留下一个印象,一段回忆、当然,最后也给我的手机里储存了许多好看的照片。( ˘͈ ᵕ ˘͈ )

公司鼓励我们跨部门了解业务,增进感情,每年都有外出旅游的 outing 经费,钉钉的云社区里会发出组团帖子,贴上令人垂涎的美食和超级动人的美景,鼓励怠惰在家的小宅们一起游遍中国的大江南北。在工作之余,还能再钉钉窗口里接受到跨部门的小哥哥小姐姐一起出游的邀请,还是很快乐的呀!

每个月都有生日趴,原来没有疫情的时候,给当月过生日的同学开 party,有可爱的小姐姐组织我们玩游戏、切蛋糕、分水果,不过最近几个月因为疫情不能聚集,所以我们跳过了玩游戏,直接收礼物和吃吃吃。✧ʕ̢̣̣̣̣̩̩̩̩·͡˔·ོɁ̡̣̣̣̣̩̩̩̩✧

除此以外,公司还有周年庆、俱乐部、图书角、年会和其他活动,不过我比较宅,对于爬山、游泳、羽毛球之类俱乐部就了解不多了,相比更外向爱玩的同学会乐在其中。

每逢圣诞节,还会每人发一个毛茸茸的吉祥物小蜜蜂——“采宝”,来公司多年的哥哥姐姐度过了一个又一个圣诞节,办公桌上已经拥有了许多个毛绒小蜜蜂,而去年圣诞节我刚刚拿到了我的第一个“采宝”,希望未来可以拥有第二只,第三只吧。

工程师文化

领走一位导师

我记得之前秋招的时候,有很多公司都会说:会有导师制,会好好培养新人。相信每个刚毕业的同学都有这样的期待,但是就我本人的经历,在前东家实习的时候,这种“培养”每次都提、但每次都不能落到实处,时间长了,对市面上各家公司宣讲会上所说的“导师制”也就不太相信了。

想想也是——大家各忙各的,每个人都有自己的任务要完成,都有代码要写,谁会管你呢?

但是出乎意料的是,在政采云公司工作的过程中发现,无论是主管, HR,还是导师本人,都是很在意“导师”这一角色的。除了小组的同事们都很友好、每个人都可以当技术上传道受业的“导师”以外,安排的导师也会经常凑来“关怀”自己的学生。

有人常常耳提面命,勤加督促总要好过自己孤军奋战,而前辈们随口提到的开发、工作、合作经验,也都是我们这些新人宝贵的财富啊。

校招生培训的“青云计划”里,曾经放过一个电影片段,我觉得能对这段“师生”关系有一个有趣、更形象化的表述:

“你想学习功夫吗?”

“当然。”

“那么我就是你的师父。”

——《功夫熊猫》

撸一些代码,服务自己

除了工作产出以外,我们团队有很多给自己用的产出,比如用于投稿前端小报的系统、增进学习的谷歌插件、自己开发的博客、桌面应用、校招闯关游戏,和一些自动发稿的脚本等。这些虽然比不上服务于业务的、更大更优秀的代码作品,诸如能够显著提效的自动化表单系统、能够反馈实际效果的埋点系统和性能测试系统等,但也是我们这些的一些实践。

我最近跟随公司里的小姐姐一起,参加了校招闯关关卡的设计和开发,看着大家兴致勃勃地撸代码、“玩”技术,也让我产生了许多兴趣,想更深入地参加到“用代码建设生活”的过程中,努力发现身边可以提效、可以用机器代替人力的方面。

而在这个过程也让我们更加认识到,人不是生产代码的机器,代码才应该是服务于人的工具,包括我们公司的产品,以及市面上一些耳熟能详的互联网产品,诸如滴滴、美团、淘宝等,其实也是服务于人的工具。

当我认识到这个因果关系以后,才觉得,工作其实没有那么枯燥——并不是工作在驱赶我们,而是我们在追逐技术和生活。

分享、沉淀、输出

比起作为一个学生、在学校里孤军奋战地学习编程,公司更能为我们营造一种共同开发的氛围。大家各自遇到的难题、拥有的感悟都可以拿出来交流,也许是某顿午饭后,也许是某次周会上,七嘴八舌,更能碰撞出思维的火花,这也让开发生活不那么枯燥了吧。

除了分享以外,每次分享后还需要梳理清问题的产生、原因、解决等,详细整理记录下来,这就是“沉淀”,如果没有“沉淀”,分享就只是“空口白话”、“纸上谈兵”,很快就忘记了,沉淀的过程需要我们查阅更多的资料,更加结构化地表述问题,罗列方案,对比优劣,你能够把问题挖掘到更深的层次,而不是仅仅停留在“解决了”这个表面上。

“输出”则是我们在掘金、博客、思否上发表的技术文章,“输出”是“沉淀”的终点,是更大范围的“分享”,以此形成一个良性循环。

其实立志要产出技术文章的人想来不少,但是也贵在坚持,有大环境的督促,希望我们每个人也能在“分享”、“沉淀”、“输出”的道路上,愈行愈远。

CodeReview

CodeReview,直译过来的意思是代码评审,也就是我们每次 commit 代码前,有人专门地查看你的写法是都得当,是否可以优化,并给出建议的过程,这是我加入团队后才慢慢接触到的一项活动。

之前在学校里,自己写的代码都是自己看,这样很难知道编码的局限性、考虑不周的地方在哪里。加入政采云团队后,才意识到这样“野生”撸代码非常不利于新人的成长和进步。

团队提倡 CodeReview 的好习惯,即便我们自以为非常小的功能,非常普通的 CSS ,都值得检查,比如我们要怎样封装代码、怎样组织数据流向、甚至包括怎样命名对象和函数、怎样组织代码文件、怎样理解业务,都大有可以互相学习的地方。

学无止境,“从完成到更好”的路该如何走,永远是值得我们去讨论和思考的问题。而每一次的 CodeReview,都是一段不起眼、却又坚实无比的阶梯。

理解需求与交互:假如我是一个用户

什么是用户呢?

相信很多刚毕业的学生都是很难讲清楚的——或许可以对此夸夸其谈,却也总是纸上谈兵,当离开学校,加入公司团队以后,才知道一个专业的团队会对用户理解到什么程度。

我们用户的身份是什么?要怎样定位其年龄范围、性别比例、电脑系统比率、使用 IE 比例?用户在不同区域内各有什么使用习惯,买家、卖家、经销人、审核人等不同角色之间的关系是什么?他们在其他平台的使用习惯,现在的期望,真正的诉求又是什么?这些答案都需要我们自己思考。小到字体的字号、颜色,大到业务的战略发展,都与用户、与我们息息相关。

刚毕业时,我理解的前端就是接收产品、视觉方面的需求,像一个流水线上的工人一样,配合后端,加工产品,再放到流水线上传递给测试,最终到达标准,产出产品,但是来到公司以后,才发现团队对我们的期望不是这样的,团队期望我们能从自己开始,设身处地地理解用户,优化需求和交互。

我亲眼看着我们的开发为页面重点、信息展示量等交互问题争论不休,也看到产品开会时拉来业务方,用实例阐述这个功能的意义来说服我们,我想,这是对彼此不同岗位同事的尊重、也是对整个项目、我们产品的尊重,是公司几百人之间的凝聚力。身在其中,我也渐渐地融入到了这样的工作习惯、工作方式中来。

以每个人的倾力思考促成产品更好地实现,再用产品带来的收益回馈每一个员工,其实是这样一个良性循环。

成为更好的自己

提出问题前先给出自己的答案

团队 Leader 开会的时候,总会说到这样一句话——“我们提倡不懂就问,却不希望你只是被动地接受别人的答案,你的答案可以不对,但是不能不思考,要带着自己的答案问问题”。

乍一这听些话,总觉得很像学校里老师的教导,而实际上,却从没有一个老师对我提出过这样的要求。后来,我渐渐理解了这句话的意义,并乐于按照这种方式去做,这次也想通过分享文章把我们团队的智慧分享给大家。就我个人而言,我的表达渐渐变成“出现了这样的问题,我可不可以这样解决”、“这个地方我不太懂,是不是因为这样的原因”。

在每一次分享答案,被别人纠错的过程中,我们才能知道我们的答案距离最好的解决差了那些,我们的考虑方向有什么偏差,在未来,我们才可以不“复制”别人的解决方案,而是在此基础上创造出更妥善的解决方案。

最难得的是可靠

“可靠”这一点也是我在学校里、作为一个学生很少注意到的,诸如按时完成期末作品、转达老师的通知,这些都太简单了,还算不得可靠,而且,相信很多人的学生时期,都是通过临时抱佛脚、加班加点地做完的吧,这样就更难以达到工作中的“可靠”的要求了。

在工作中,我们需要预先对时间进行正确评估,对每天的任务和进度都能准确把控,尤其是多个项目交叉推进的时候,更容易手忙脚乱,这时候更容易忽略一部分事情,造成一定的风险。好记性不如烂笔头,对每个项目进度进行记录,对每一天的安排记录,及时跟进,按时汇报,将大家的信息同步,为成功和失败负责,这样才会接近“可靠”的要求。

世界并不是非黑即白的

“世界并不是非黑即白的”——这是我们团队对入职新人的要求,老大也在开会时多次提到。我最初还有点疑惑,为什么招聘的时候会有这个要求,想来也许是为了减少工作中的冲突和争执,更好地进行管理吧,但是后来又慢慢了解到,“非黑即白”其实也是一种思考的惰性,让我们不去想中间灰色的部分,长此以往,会限制我们看待世界的角度。

我很感谢团队有这个指导思想,可以时时在心里提醒我们去开拓更多、更广的视角,这样在今后的技术学习和个人成长中,都能够为我们提供无限的可能。

一起为政采事业添砖加瓦

每天坐地铁、乘公交的时候,我们总觉得自己是一个碌碌终日的平凡人,吃饭、睡觉、上班,日复一日,淹没在人海中再没有一丁点影子,但是我们是不是也可以有自己想做的事?任务是死的,但任务的意义是活的,能清晰地意识我们在做什么,为世界带来了什么,不也是一种幸福吗?

如今,互联网技术如擎天大厦拔地而起,以更先进、更透明、更便捷的互联网技术服务于国家、服务于政府也必将是大势所趋。作为编程的开发者,我们写的一行行代码,便是构建这个互联网时代、构建这个国家的一砖一石,我们在试错、在尝试、在开拓,就像数千年前的革新者一样,一往无前。

我想,也许我们每个人都平凡卑微,都不曾拥有“障百川而东之,回狂澜于既倒”之力,但是,能以我们所学之能,为建设和创新政府采购事业添砖加瓦,为这个时代开辟一种新的可能性,想来也是一个不错的选择吧!

总结

在这个世界上有许许多多的公司,也有千千万万的开发者。在辛苦与收获并存的光阴里,我们从不孤独,而前路依然宽广明亮。

以上是我个人在政采云的体验与工作经验的分享, 2021 年校招在即,期待这里有更好的你与你在这里更好的未来。

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com

查看原文

赞 2 收藏 1 评论 0

政采云前端团队 发布了文章 · 9月4日

ZooTeam 前端周刊|第 98 期

ZooTeam 前端周刊|第 98 期

浏览更多往期小报,请访问: https://weekly.zoo.team

一个可能让你的页面渲染速度提升数倍的CSS属性

浏览器在接收到服务端返回的 HTML 之后,需要把这段数据渲染成用户看到的页面,在开始渲染第一个元素之前可能

【网页特效】11 个文本输入和 6 个按钮操作 特效库

11 个文本输入和 6 个按钮操作 特效库

前端也要懂物理 —— 惯性滚动篇

我们在平时编程开发时,除了需要关注技术实现、算法、代码效率等因素之外,更要把所学到的学科知识(如物理学、理论数学等等)灵活应用,毕竟理论和实践相辅相成、密不可分,这无论是对于我们的方案选型、还是技术实践理解都有非常大的帮助。今天就让我们一起来回顾中学物理知识,并灵活运用到惯性滚动的动效实现当中。

「一劳永逸」48张小图带你领略flex布局之美

掘金是一个帮助开发者成长的社区,是给开发者用的 Hacker News,给设计师用的 Designer News,和给产品经理用的 Medium。掘金的技术文章由稀土上聚集的技术大牛和极客共同编辑为你筛选出最优质的干货,其中包括:Android、iOS、前端、后端等方面的内容。用户每天都可以在这里找到技术世界的头条内容。与此同时,掘金内还有沸点、掘金翻译计划、线下活动、专栏文章等内容。即使你是 GitHub、StackOverflow、开源中国的用户,我们相信你也可以在这里有所收获。

TypeScript 4.1 新特性:字符串模板类型,Vuex 终于有救了?

TypeScript 4.1 快要发布了,老爷子 Anders Hejlsberg加入了一项重大更新:「字符串模板类型」,Vuex 终于有救了?一起来了解一下。

前端效率提升,Baidu开源低代码前端框架——amis

用来负责文件上传,文件上传成功后会返回文件地址,这个文件地址会作为这个表单项的值,整个表单提交的时候,其实提交的是文件地址,文件上传已经在这个控件中完成了。

我的前端成长之路:中医药大学毕业的业务女前端修炼之路

前端工程师的修炼没有捷径,踏踏实实的通过一个个项目的实践来升级打怪实现进阶;本文仅分享自己11年的前端生涯,探讨一直在业务中的技术人的成长之路,也复盘再认识下自己,每个节点我遇到的问题和我的选择。

自适应布局方案

掘金是一个帮助开发者成长的社区,是给开发者用的 Hacker News,给设计师用的 Designer News,和给产品经理用的 Medium。掘金的技术文章由稀土上聚集的技术大牛和极客共同编辑为你筛选出最优质的干货,其中包括:Android、iOS、前端、后端等方面的内容。用户每天都可以在这里找到技术世界的头条内容。与此同时,掘金内还有沸点、掘金翻译计划、线下活动、专栏文章等内容。即使你是 GitHub、StackOverflow、开源中国的用户,我们相信你也可以在这里有所收获。

烤透 React Hook

掘金是一个帮助开发者成长的社区,是给开发者用的 Hacker News,给设计师用的 Designer News,和给产品经理用的 Medium。掘金的技术文章由稀土上聚集的技术大牛和极客共同编辑为你筛选出最优质的干货,其中包括:Android、iOS、前端、后端等方面的内容。用户每天都可以在这里找到技术世界的头条内容。与此同时,掘金内还有沸点、掘金翻译计划、线下活动、专栏文章等内容。即使你是 GitHub、StackOverflow、开源中国的用户,我们相信你也可以在这里有所收获。

饿了么4年 + 阿里2年:研发路上的一些总结与思考

作者 | 石佳宁“最重要的是选择,最困难的是坚持。”我是在 2014 年入职饿了么,从前端和 PHP 一直做

如何设计一个JavaScript插件系统,编程思维比死磕API更重要

插件是库和框架的常见功能,并且有一个很好的理由:它们允许开发人员以安全,可扩展的方式添加功能。这使核心项目更具价值,并建立了一个社区——所有这些都不会增加额外的维护负担。太好了!

查看原文

赞 3 收藏 1 评论 0

政采云前端团队 发布了文章 · 8月30日

编写高质量可维护的代码:数据建模

67 篇原创好文~
本文首发于政采云前端团队博客:[编写高质量可维护的代码:数据建模
](https://zoo.team/article/data...

什么是数据建模

数据建模是一种用于定义和分析数据的要求和其需要的相应支持的信息系统的过程。

随着前端页面的交互变得更加细腻复杂,原本存放于服务端的状态放置在了前端,类似 flux、redux、mobx、dva、rematch、vuex 的状态管理库也成了每个项目的标配。

因为分层理念的普及,前端工程师们需要把更多精力放在数据管理上,数据建模也成了基本功。

而建模的产物是数据模型,数据模型是定义数据如何输入和输出的一种模型,其主要作用是为信息系统提供数据的定义和格式。

数据模型包括数据结构、数据操作、数据完整性约束条件这三要素。

简单理解就是数据模型提供了一个“模具”,数据按照预先的设计和约束进行放置。

三要素

数据完整性约束条件

好的数据结构必须要有约束,例如描述同一个状态的字段有时候是字符串,有时候是数字,这样的话就容易造成预期之外的情况。添加约束可以最大限度保障这份数据是干净整齐的;

// status 是字符串的时候不通过
if (status === 1) {
  ...
}
// 按照一定约束
model.define(
  'user',
  {
    name: { 
      field: 'name',
      type: STRING(64),
      allowNull: false, 
      comment: '姓名',
    },
    sex: {
      field: 'sex',
      type: INTEGER(1),
      allowNull: false,
      comment: '性别',
    }
  }
);

数据结构

描述模型本身的性质之外,还需通过某些字段表达模型(表)和模型之间的关联;

数据操作

在数据结构上对数据或者数据之间的关联关系的操作。

领域驱动设计

在围绕着数据模型进行应用开发的时候,我们会思考如何进行建模呢?

实际上,软件开发行业中已经积累了一些方法论,例如领域驱动设计(DDD)就被广泛采用。

在进行软件开发前,通常需要先进行业务知识梳理,而后到达软件设计的层面,最后才是开发。而在业务知识梳理的过程中,我们必然会形成某个领域知识。根据领域知识来一步步驱动软件设计,就是领域驱动设计的基本概念。简单来说领域驱动设计就是关注精简的业务模型及实现的匹配

分层架构

按照领域驱动设计的分层架构可以将应用进行分层

  • UI 层:负责向用户展现信息以及解释用户命令。
  • 应用层:用来协调应用的活动。它不包含业务逻辑;它不保留业务对象的状态;但它保有应用任务的进度状态。
  • 领域层:业务软件的核心所在。在这里保留业务对象的状态,对业务对象和它们状态的持久化被委托给了基础设施层。
  • 基础设施层:为其他层的支撑库存在。它提供了层间的通信,实现对业务对象的持久化,包含对用户界面层的支撑库等作用。

按照这个分层,越往左边代码变动越频繁。随着业务复杂,应用层和领域层的边界变得模糊,领域之间也容易交错在一起。

良好的设计应该避免层与层之间产生过多依赖,如果代码没有被清晰地隔离到某层中,它会迅即变得混乱和难以维护。

通过分层架构和高内聚低耦合的设计思想,最终实现系统与需求有较好的一致性,在业务迭代中快速响应需求变更。

实体

实体在领域模型中是必需的对象,并且它们应该在建模过程开始时就被考虑。例如要实现一个“猫”的概念,我们可能会去创造一个 Cat 的类,这个 Cat 可能包含名称、性别、品种等属性,但是这些属性都不足以区分这只猫,所以我们需要创建一个唯一不重复的 ID 来区分他们,也就区分实体的标识符。

创建 ID 的方式有很多种,它可以是主键、可以来自外部、也可以由系统自己产生,但它必须符合模型中的身份差别。

值对象

用来描述领域的特殊方面,且没有标识符的一个对象,叫做值对象。例如画布上的一个点 Customer 会跟姓名、省份、城市、区、街道相关。最好是将地址分离出来,保留对地址的引用,因为它们都是同一个址属性。

服务

你可以简单地将行为理解成一种服务。例如你去商店购买商品,你的朋友也可以去购买商品。如果将购买这个能力作为一个属性放在 Person 这个实体里显然有点不对劲,因为“去购买”这个功能并不属于你和你的朋友(实体或者值对象),同时去购买也可能涉及到商品对象。

保证服务的单一性和隔离非常重要,注意区分领域服务和应用服务。决定一个服务所应归属的层是非常困难的事情,我们在设计阶段建立模型时,需要确保领域层从其他层中隔离开来。

模块

模块是一种被用来作为组织相关概念和任务以便降低复杂性的方法,通常情况下由功能或者逻辑上属于一体的元素构成,以保证高内聚,同时通过接口的形式暴露给第三方以降低模块之间的耦合。

聚合

聚合是针对数据变化可以考虑成一个单元的一组相关对象。聚合基于(有且仅有)一个实体(根),聚合通过这个根被外部访问,它可以引用任意聚合或者被其他聚合引用。以下是一个简单的聚合例子:客户作为聚合的根,其他信息都是客户内部的,如果需要地址则将地址的拷贝传递出去( Javascript 中特别需要注意)。

工厂

工厂用来封装对象创建所必需的知识,它们对创建聚合特别有用。工厂方法是一个对象的方法,包含并隐藏了创建其他对象的必要知识。

资源库

资源库作为一个全局可访问对象的存储点而存在。它是一个独立的层,介于领域层与数据映射层(数据访问层)之间。它的存在让领域层感觉不到数据访问层的存在,它提供一个类似集合的接口,提供给领域层进行领域对象的访问。

前端的数据建模

数据建模和后端的工作关联较为紧密,前端的数据模型更多是依赖后端传递的数据传输对象(DTO)进行二次构建。无论二次构建是发生在服务端聚合阶段还是用户端AJAX请求完成阶段,前端都需要参与一定的数据清洗,并应用到前端的数据模型之上。

领域划分

现在你可以开始尝试划分你应用内的业务领域。以一个商城为例子,它可能会包括用户、商品、货架、订单、结算、账户等内容。

每一个业务领域都可以至少拆分成一个领域,按照业务领域来组织代码,例如在交易领域中按照以下目录结构划分:

src
  modules 
    ...
    trading             # 交易领域
      components/         # 组件
      models/             # models
      pages/              # 页面
      redux/              # redux
      services/           # 交易模块相关api
      styles/             # 交易模块样式
      index.ts
  ...

概念模型

数据建模的前提是对业务的充分理解,充分理解业务相当于在更高的视角去看待业务之间的关系,有利于更好地完成模型建设。

尝试回想一下你所维护的业务(应用)场景,你是否清晰业务场景和业务对象之间的关系以及具体交互?

使用思维导图梳理出概念模型,这个阶段可以不用严格遵守三要素,目标清晰表达现实世界就行。

定义模型

定义模型可以依据概念模型,补充细节和关联关系,例如简单定义一个营销商品:

以上展示了商场货架上划分的一块活动区域,规则是满XX减XX,再将参与该活动的商品在区域内进行上架。

降低复杂度

在大部分情况下,特别是展示逻辑这块,前端不应该是重逻辑的。

以商品为例,不同商品的营销类型背后隐藏着复杂的价格体系,尽管是同一种营销类型,商品在不同的状态展示的价格也不一定相同。你可以想象这背后的字段,以及计算规则。

假如后端把这些字段、各种price和规则一股脑抛给你,先不谈前后端对称问题,光挑字段都能让你目瞪狗呆。

遇到类似情况更好的办法是:尽量避免在前端(用户端)去处理复杂的业务判断,在聚合层或者让后端同学给你处理好这些展示逻辑。

特别是在 C 端场景下,数据直出显得更加重要,同时前端同学也有更多时间去做性能优化(早点下班不香么?)。

另外一个好处是假如出现展示问题,你只要确定读取的字段正确,剩下的仅需一个人排查就够了;

// Bad
const switchPrice = product => {
  switch(product.status) {
    case 0:
        return product.priceA;
    case 1:
        return product.priceB;
    case 2:
        return product.priceB;
    default:
        return  product.priceBase;
  }
}
<Price value={switchPrice(product)}/>
     
// Good
<Price value={product.price}/>

逻辑分层

设计上需要区分应用逻辑(业务逻辑)和展示逻辑。应用层注重对领域层的调度,是业务逻辑的实现,展示层专注渲染和交互动作。

在一个大型项目中,同一个 Model 可能被多处引用,你很难确定谁最终会对同一份数据进行怎样的操作。

同时 Model 中仅保留数据源的抽象结构,而不修改数据源的内容。

// 在视图层只做展示逻辑处理
// 组件A
...
<>
    <span>日期:{format(res.date, 'YYYY-MM-DD')}</span>
</>

// 组件B
...
<>
    <span>日期:{format(res.date, 'YYYY-MM')}</span>
</>

统一字段

在设计模型的时候,尽可能与后端保持统一字段。比如某些表单场景在回显和提交的时候要多一层转换,后期维护会带来多一层心智负担。在前后端分离的开发模式下,不一定能保证后端会先给出字段,我的习惯是标记字段,等联调的时候全局替换一下就行了。

简化字段、明确语义、改变不合理的前后端交互是做好数据建模的基础,否则你将花费大量时间去理解这些字段背后的含义和计算规则。

小结

没有一个十全十美的数据模型可以适用任何需求场景,模型的落地需要综合考虑业务实际场景和技术选型。在构建模型的过程中,锻炼系统性思考能力、从更高的视角看待业务,才能创造出一个生命周期更长的模型。

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com

查看原文

赞 5 收藏 5 评论 0

政采云前端团队 发布了文章 · 8月28日

ZooTeam 前端周刊|第 97 期

ZooTeam 前端周刊|第 97 期

浏览更多往期小报,请访问: https://weekly.zoo.team

Scott:总结 10 年前端经验,谈谈前端人如何更快地成长

前端新人该如何选择技术栈?前端新人怎样能更快地成长?拥有 10 年工程师经验的 Scott 给你带来了一些建议。

我团队的一年前端实现Promise所有方法

最通俗直白方式,告诉你实现Promise所有方法。

前端进阶必经之路(一):1.2w字深入理解JavaScript26个核心概念

掘金是一个帮助开发者成长的社区,是给开发者用的 Hacker News,给设计师用的 Designer News,和给产品经理用的 Medium。掘金的技术文章由稀土上聚集的技术大牛和极客共同编辑为你筛选出最优质的干货,其中包括:Android、iOS、前端、后端等方面的内容。用户每天都可以在这里找到技术世界的头条内容。与此同时,掘金内还有沸点、掘金翻译计划、线下活动、专栏文章等内容。即使你是 GitHub、StackOverflow、开源中国的用户,我们相信你也可以在这里有所收获。

CSS Triggers

CSS Triggers

学习Vue应用测试,让你的项目更加健壮和稳定 - 掘金

掘金是一个帮助开发者成长的社区,是给开发者用的 Hacker News,给设计师用的 Designer News,和给产品经理用的 Medium。掘金的技术文章由稀土上聚集的技术大牛和极客共同编辑为你筛选出最优质的干货,其中包括:Android、iOS、前端、后端等方面的内容。用户每天都可以在这里找到技术世界的头条内容。与此同时,掘金内还有沸点、掘金翻译计划、线下活动、专栏文章等内容。即使你是 GitHub、StackOverflow、开源中国的用户,我们相信你也可以在这里有所收获。

[[译]18个有用的JavaScript片段 - 掘金](https://juejin.im/post/686257...

掘金是一个帮助开发者成长的社区,是给开发者用的 Hacker News,给设计师用的 Designer News,和给产品经理用的 Medium。掘金的技术文章由稀土上聚集的技术大牛和极客共同编辑为你筛选出最优质的干货,其中包括:Android、iOS、前端、后端等方面的内容。用户每天都可以在这里找到技术世界的头条内容。与此同时,掘金内还有沸点、掘金翻译计划、线下活动、专栏文章等内容。即使你是 GitHub、StackOverflow、开源中国的用户,我们相信你也可以在这里有所收获。

「1.4万字」玩转前端 Video 播放器 | 多图预警 - 掘金

掘金是一个帮助开发者成长的社区,是给开发者用的 Hacker News,给设计师用的 Designer News,和给产品经理用的 Medium。掘金的技术文章由稀土上聚集的技术大牛和极客共同编辑为你筛选出最优质的干货,其中包括:Android、iOS、前端、后端等方面的内容。用户每天都可以在这里找到技术世界的头条内容。与此同时,掘金内还有沸点、掘金翻译计划、线下活动、专栏文章等内容。即使你是 GitHub、StackOverflow、开源中国的用户,我们相信你也可以在这里有所收获。

三分钟打造七夕专属的插件化脚手架

背景七夕节将至,你是否还因没有找到合适的表白机会而苦恼,还是说在纠结于为伴侣挑选合适的情人节礼物。那么今天你来对地方了。相信在读完这篇文章后,你就可以自己动手打造出一个专属于他/她/它的七夕专属插件化脚手架,通过本篇文章,不仅可以轻松拉近你与你爱人的距离,还能顺便学会插件化脚手架的相关知识。 温馨提醒本篇文章需要一定的命令行知识,若在阅读本篇文章时有任何的疑惑,可以通过自行搜索相关内容或者阅读一下。

前端图片处理 - 高斯模糊

前端图片处理 - 高斯模糊

如何画好一张架构图?

如何画好一张架构图?

查看原文

赞 2 收藏 0 评论 0

政采云前端团队 发布了文章 · 8月24日

npm 依赖管理中被忽略的那些细节

66 篇原创好文~
本文首发于政采云前端团队博客:[npm 依赖管理中被忽略的那些细节
](https://www.zoo.team/article/...

前言

提起 npm,大家第一个想到的应该就是 npm install 了,但是 npm install 之后生成的 node_modules 大家有观察过吗?package-lock.json 文件的作用大家知道吗?除了 dependencies 和 devDependencies,其他的依赖有什么作用呢?接下来,本文将针对 npm 中的你可能忽略的细节和大家分享一些经验 。

npm 安装机制

A 和 B 同时依赖 C,C 这个包会被安装在哪里呢?C 的版本相同和版本不同时安装会有什么差异呢?package.json 中包的前后顺序对于安装时有什么影响吗?这些问题平时大家可能没有注意过,今天我们就来一起研究一下吧。

A 和 B 同时依赖 C,这个包会被安装在哪里呢?

假如有 A 和 B 两个包,两个包都依赖 C 这个包,npm 2 会依次递归安装 A 和 B 两个包及其子依赖包到 node_modules 中。执行完毕后,我们会看到 ./node_modules 这层目录只含有这两个子目录:

node_modules/ 
├─┬ A 
│ ├── C 
├─┬ B 
│ └── C 

如果使用 npm 3 来进行安装的话,./node_modules 下的目录将会包含三个子目录:

node_modules/ 
├─┬ A 
├─┬ B 
├─┬ C 

为什么会出现这样的区别呢?这就要从 npm 的工作方式说起了:

npm 2 和 npm 3 模块安装机制的差异

虽然目前最新的 npm 版本是 npm 6,但 npm 2 到 npm 3 的版本变更中实现了目录打平,与其他版本相比差别较大。因此,让我们具体看下这两个版本的差异​。

npm 2 在安装依赖包时,采用简单的递归安装方法。执行 npm install 后,npm 根据 dependencies 和 devDependencies 属性中指定的包来确定第一层依赖,npm 2 会根据第一层依赖的子依赖,递归安装各个包到子依赖的 node_modules 中,直到子依赖不再依赖其他模块。执行完毕后,我们会看到 ./node_modules 这层目录中包含有我们 package.json 文件中所有的依赖包,而这些依赖包的子依赖包都安装在了自己的 node_modules 中 ,形成类似于下面的依赖树:

图片

这样的目录有较为明显的好处:

​ 1)层级结构非常明显,可以清楚的在第一层的 node_modules 中看到我们安装的所有包的子目录;

​ 2)在已知自己所需包的名字以及版本号时,可以复制粘贴相应的文件到 node_modules 中,然后手动更改 package.json 中的配置;

​ 3)如果想要删除某个包,只需要简单的删除 package.json 文件中相应的某一行,然后删除 node_modules 中该包的目录;

但是这样的层级结构也有较为明显的缺陷,当我的 A,B,C 三个包中有相同的依赖 D 时,执行 npm install 后,D 会被重复下载三次,而随着我们的项目越来越复杂,node_modules 中的依赖树也会越来越复杂,像 D 这样的包也会越来越多,造成了大量的冗余;在 windows 系统中,甚至会因为目录的层级太深导致文件的路径过长,触发文件路径不能超过 280 个字符的错误;

​ 为了解决以上问题,npm 3 的 node_modules 目录改成了更为扁平状的层级结构,尽量把依赖以及依赖的依赖平铺在 node_modules 文件夹下共享使用。

npm 3 对于同一依赖的不同版本会怎么处理呢?

npm 3 会遍历所有的节点,逐个将模块放在 node_modules 的第一层,当发现有重复模块时,则丢弃, 如果遇到某些依赖版本不兼容的问题,则继续采用 npm 2 的处理方式,前面的放在 node_modules 目录中,后面的放在依赖树中。举个例子: A,B,依赖 D(v 0.0.1),C 依赖 D(v 0.0.2):

图片

但是 npm 3 会带来一个新的问题:由于在执行 npm install 的时候,按照 package.json 里依赖的顺序依次解析,上图如果 C 的顺序在 A,B 的前边,node_modules 树则会改变,会出现下边的情况:

图片

由此可见,npm 3 并未完全解决冗余的问题,甚至还会带来新的问题。

为什么会出现 package-lock.json 呢?

为什么会有 package-lock.json 文件呢?这个我们就要先从 package.json 文件说起了。

package.json 的不足之处

npm install 执行后,会生成一个 node_modules 树,在理想情况下, 希望对于同一个 package.json 总是生成完全相同 node_modules 树。在某些情况下,确实如此。但在多数情况下,npm 无法做到这一点。有以下两个原因:

1)某些依赖项自上次安装以来,可能已发布了新版本 。比如:A 包在团队中第一个人安装的时候是 1.0.5 版本,package.json 中的配置项为 A: '^1.0.5' ;团队中第二个人把代码拉下来的时候,A 包的版本已经升级成了 1.0.8,根据 package.json 中的 semver-range version 规范,此时第二个人 npm install 后 A 的版本为 1.0.8; 可能会造成因为依赖版本不同而导致的 bug;

2)针对 1)中的问题,可能有的小伙伴会想,把 A 的版本号固定为 A: '1.0.5' 不就可以了吗?但是这样的做法其实并没有解决问题, 比如 A 的某个依赖在第一个人下载的时候是 2.1.3 版本,但是第二个人下载的时候已经升级到了 2.2.5 版本,此时生成的 node_modules 树依旧不完全相同 ,固定版本只是固定来自身的版本,依赖的版本无法固定。

针对 package.json 不足的解决方法

为了解决上述问题以及 npm 3 的问题,在 npm 5.0 版本后,npm install 后都会自动生成一个 package-lock.json 文件 ,当包中有 package-lock.json 文件时,npm install 执行时,如果 package.json 和 package-lock.json 中的版本兼容,会根据 package-lock.json 中的版本下载;如果不兼容,将会根据 package.json 的版本,更新 package-lock.json 中的版本,已保证 package-lock.json 中的版本兼容 package.json。

package-lock.json 文件的结构

package-lock.json 文件中的 name、version 与 package.json 中的 name、version 一样,描述了当前包的名字和版本,dependencies 是一个对象,该对象和 node_modules 中的包结构一一对应,对象的 key 为包的名称,值为包的一些描述信息, 根据 package-lock-json官方文档,主要的结构如下:

  • version :包版本,即这个包当前安装在 node_modules 中的版本
  • resolved :包具体的安装来源
  • integrity :包 hash 值,验证已安装的软件包是否被改动过、是否已失效
  • requires :对应子依赖的依赖,与子依赖的 package.jsondependencies 的依赖项相同
  • dependencies :结构和外层的 dependencies 结构相同,存储安装在子依赖 node_modules 中的依赖包

需要注意的是,并不是所有的子依赖都有 dependencies 属性,只有子依赖的依赖和当前已安装在根目录的 node_modules 中的依赖冲突之后,才会有这个属性。

package-lock.json 文件的作用

  • 在团队开发中,确保每个团队成员安装的依赖版本是一致的,确定一棵唯一的 node_modules 树;
  • node_modules 目录本身是不会被提交到代码库的,但是 package-lock.json 可以提交到代码库,如果开发人员想要回溯到某一天的目录状态,只需要把 package.json 和 package-lock.json 这两个文件回退到那一天即可 。
  • 由于 package-lock.json 和 node_modules 中的依赖嵌套完全一致,可以更加清楚的了解树的结构及其变化。
  • 在安装时,npm 会比较 node_modules 已有的包,和 package-lock.json 进行比较,如果重复的话,就跳过安装 ,从而优化了安装的过程。

依赖的区别与使用场景

npm 目前支持以下几类依赖包管理包括

  • dependencies
  • devDependencies
  • optionalDependencies 可选择的依赖包
  • peerDependencies 同等依赖
  • bundledDependencies 捆绑依赖包

下面我们来看一下这几种依赖的区别以及各自的应用场景:

dependencies

dependencies 是无论在开发环境还是在生产环境都必须使用的依赖,是我们最常用的依赖包管理对象,例如 React,Loadsh,Axios 等,通过 npm install XXX 下载的包都会默认安装在 dependencies 对象中,也可以使用 npm install XXX --save 下载 dependencies 中的包;

图片

devDependencies

devDependencies 是指可以在开发环境使用的依赖,例如 eslint,debug 等,通过 npm install packageName --save-dev 下载的包都会在 devDependencies 对象中;

图片

dependencies 和 devDependencies 最大的区别是在打包运行时,执行 npm install 时默认会把所有依赖全部安装,但是如果使用 npm install --production 时就只会安装 dependencies 中的依赖,如果是 node 服务项目,就可以采用这样的方式用于服务运行时安装和打包,减少包大小。

optionalDependencies

optionalDependencies 指的是可以选择的依赖,当你希望某些依赖即使下载失败或者没有找到时,项目依然可以正常运行或者 npm 继续运行的时,就可以把这些依赖放在 optionalDependencies 对象中,但是 optionalDependencies 会覆盖 dependencies 中的同名依赖包,所以不要把一个包同时写进两个对象中。

optionalDependencies 就像是我们的代码的一种保护机制一样,如果包存在的话就走存在的逻辑,不存在的就走不存在的逻辑。

try { 
  var axios = require('axios') 
  var fooVersion = require('axios/package.json').version 
} catch (er) { 
  foo = null 
} 
// .. then later in your program .. 
if (foo) { 
  foo.doFooThings() 
} 

peerDependencies

peerDependencies 用于指定你当前的插件兼容的宿主必须要安装的包的版本,这个是什么意思呢?举个例子🌰:我们常用的 react 组件库 ant-design@3.x 的 package.json 中的配置如下:

"peerDependencies": { 
  "react": ">=16.9.0", 
  "react-dom": ">=16.9.0" 
 }, 

假设我们创建了一个名为 project 的项目,在此项目中我们要使用 ant-design@3.x 这个插件,此时我们的项目就必须先安装 React >= 16.9.0React-dom >= 16.9.0 的版本。

在 npm 2 中,当我们下载 ant-design@3.x 时,peerDependencies 中指定的依赖会随着 ant-design@3.x 一起被强制安装,所以我们不需要在宿主项目的 package.json 文件中指定 peerDependencies 中的依赖,但是在 npm 3 中,不会再强制安装 peerDependencies 中所指定的包,而是通过警告的方式来提示我们,此时就需要手动在 package.json 文件中手动添加依赖;

bundledDependencies

这个依赖项也可以记为 bundleDependencies,与其他几种依赖项不同,他不是一个键值对的对象,而是一个数组,数组里是包名的字符串,例如:

{ 
  "name": "project", 
  "version": "1.0.0", 
  "bundleDependencies": [ 
    "axios",  
    "lodash" 
  ] 
} 

当使用 npm pack 的方式来打包时,上述的例子会生成一个 project-1.0.0.tgz 的文件,在使用了 bundledDependencies 后,打包时会把 Axios 和 Lodash 这两个依赖一起放入包中,之后有人使用 npm install project-1.0.0.tgz 下载包时,Axios 和 Lodash 这两个依赖也会被安装。需要注意的是安装之后 Axios 和 Lodash 这两个包的信息在 dependencies 中,并且不包括版本信息。

"bundleDependencies": [ 
    "axios", 
    "lodash" 
  ], 
  "dependencies": { 
    "axios": "*", 
    "lodash": "*" 
  }, 

如果我们使用常规的 npm publish 来发布的话,这个属性是不会生效的,所以日常情况中使用的较少。

总结

本文介绍的是 npm 2,npm 3,package-lock.json 以及几种依赖的区别和使用场景,希望能够让大家对 npm 的了解更加多一点,有什么不清楚的地方或者不足之处欢迎大家在评论区留言。

参考文献

package.json官方文档

package-lock-json官方文档

npm文档总结

npm-pack

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com

查看原文

赞 8 收藏 6 评论 0

政采云前端团队 发布了文章 · 8月21日

ZooTeam 前端周刊|第 96 期

ZooTeam 前端周刊|第 96 期

浏览更多往期周刊,请访问: https://weekly.zoo.team

  1. 更优雅的编写JavaScript
如果你刚接触JavaScript可能你还没有听说过.map(),.reduce(),.filter()。或者听说过,看过别人用过但是自己在实际项目中没有用过。在国内很多开发项目都是需要考虑IE8的兼容,为了兼容很多JavaScript好用的方法和技巧都被埋没了。但是我发现近几年开始,很多开发项目已经完全抛弃了IE这个魔鬼了。如果你不需要兼容“石器时代”的IE浏览器了,那就要开始熟悉一下这几个方法来...
  1. 结合源码分析 Node.js 模块加载与运行原理 | EFE Tech
结合源码分析 Node.js 模块加载与运行原理 | EFE Tech。
  1. React Hooks(一): From Redux to Hooks - 知乎
如今的react的状态管理工具基本上分为redux和mobx两个流派,mobx基本上大家都是使用官方的mobx库,但是对于redux却衍生数不胜数的redux框架。如redux-saga, dva, mirror, rematch等等,这么多redux的框架一方面说…
  1. React Hooks(四): immutable
正值tuple&amp;record进入stage2,正好将放了半年的草稿更新一波。 对于比较复杂的 React 单页应用,性能问题和UI一致性问题是我们必须要考虑的问题,这两个问题和React的重渲染机制息息相关。本文重点讨论如何控…
  1. 一种灵活的规则引擎设计
规则引擎起源于基于规则的专家系统,属于人工智能的范畴,通过模仿人类的推理方式,通过试探性推理,使用人类能够理解的方式证明其结论。规则引擎在现实应用中实现了业务和代码分离,通过代理的方式将业务语言定义传递到系统中,维护和管理复杂的业务规则,从而起到支撑业务灵活多变的作用。本文提出一种新的轻型规则引擎,旨在及时交付并提供更为灵活和复杂的逻辑控制功能,并且能基于类似随机森林的方法优化业务规则,可以应用在自动化信贷审批。
  1. 进阶高级前端,这9种Vue技术你掌握了吗? - 掘金
现在,Vue.js已成为前端开发的热门框架。有很多工程师利用Vue.js的便利性和强大功能。但是,我们完成的某些解决方案可能未遵循最佳做法。好吧,让我们看一下那些必备的Vue技术...
  1. 前端如何正确使用中间件?
简介:中间件可以算是一种前端中常用的”设计模式“了,有的时候甚至可以说,整个应用的架构都是使用中间件为基础搭建的。那么中间件有哪些利弊?什么才是中间件正确的使用姿势?本文将分享作者在实际使用中的一些…
  1. node-rules,一個向前鏈接規則引擎
node-rules,在 node.js 上,node 規則是一個向前鏈接規則引擎,下載node-rules的源碼...
  1. GPT-3 袭来,前端又要失业?谈谈如何实现智能切图
3 年前,智能切图项目 pix2code 轰动了前端界,它的效果非常惊人,只需要输入一张图片就能生成前端代码,有了它任何人都能切图了,还要啥前端?看起来前途无量。不过和绝大多数论文一样,这个项目出道即巅峰,后来…
  1. 语雀的技术架构演进之路
每个技术人心中或多或少都有一个「产品梦」,好的技术需要搭配好的产品,才能让用户爱不释手,尤其是做一款知识服务型产品。作者何翊宇(花名:不四)是蚂蚁金服体验技术部高级前端技术专家,语雀产品技术负责人。本文从技术架构的视角,回顾了语雀的原型、内部服务和对外商业化的全过程,并对函数计算在语雀架构演进过程中所扮演的角色做了详细的介绍。语雀是一个专业的云端知识库,用于团队的文档协作。

查看原文

赞 3 收藏 1 评论 0

政采云前端团队 发布了文章 · 8月17日

深色模式适配指南

65 篇原创好文~
本文首发于政采云前端团队博客:深色模式适配指南

深色模式适配指南

背景

随着 iOS 13 的发布,深色模式(Dark Mode)越来越多地出现在大众的视野中,支持深色模式已经成为现代移动应用和网站的一个潮流,前段时间更是因为微信的适配再度引起热议。深色模式不仅可以大幅减少电量的消耗,减弱强光对比 ,还能 提供更好的可视性和沉浸感。

那针对 一款 App 应用(原生 + H5)怎么进行深色模式的适配呢?今天就让我们一起来探究吧!

系统兼容

想要实现深色模式的效果,前提条件是要系统支持, 目前 常见系统支持情况如下:

H5 深色适配

随着深色模式的流行,越来越多的操作系统、浏览器开始支持深色模式,现在可以利用 CSS 的媒体查询方法(prefers-color-scheme)以及 CSS 变量(CSS variables、CSS custom properties)就可以实现页面主题跟随系统自动切换深浅模式 。CSS 变量除了 IE ,其余各大浏览器都支持的比较好, 但 prefers-color-scheme 方法还处于 W3C 草案规范,需要对不兼容浏览器做向下兼容,具体浏览器兼容性可以查询 Can I Use, 综合来说,高版本的主流浏览器都已经支持,IE 不支持。

可以通过以下两种方式来实现 Web 端的深色适配:

一、CSS 的媒体查询

prefers-color-scheme 是一种 用于检测用户是否有将系统的主题色设置为亮色或者暗色 的 CSS 媒体特性 。 利用其设置不同主题模式下的 CSS 样式, 浏览器会自动根据当前系统主题加载对应的 CSS 样式。light 适配浅色主题,dark 适配深色主题,no-preference 表示获取不到主题时的适配方案。

  • CSS
@media (prefers-color-scheme: light) { 
  .article {  
    background:#fff; 
    color: #000;  
  } 
} 
@media (prefers-color-scheme: dark) { 
  .article {  
    background:#000;  
    color: white;  
  } 
} 
@media (prefers-color-scheme: no-preference) { 
  .article {  
    background:#fff; 
    color: #000;  
  } 
} 
  • Link 标签
<link href="./common.css" rel="stylesheet" type="text/css" /> 
<link href="./light-mode-theme.css" rel="stylesheet" type="text/css" /> 
<link href="./dark-mode-theme.css" rel="stylesheet" type="text/css" media="(prefers-color-scheme: dark)" /> 

来看一下效果,将系统设置为浅色外观:

然后将系统设置为深色外观:

页面已经加载了对应深色主题的样式:

二、CSS 变量 + 媒体查询

window.matchMedia 方法可以用来查询 指定的媒体查询字符串解析后的结果。 结合 CSS 变量和 matchMedia 的查询结果,设置对应的 CSS 主题颜色。该方法更灵活,可以单独抽离主题色进行适配。

CSS 变量的作用域与 CSS 的"层叠"规则一致,优先级最高的声明生效。所以当 body 上存在 "dark" 类名时,:root .dark 会生效,否则 :root 生效。

.article { 
  color: var(--text-color, #eee); 
  background: var(--text-background, #fff); 
} 
:root { 
  --text-color: #000; 
  --text-background: #fff; 
} 
:root .dark { 
  --text-color: #fff; 
  --text-background: #000; 
} 

使用 matchMedia 匹配主题媒体,深色模式匹配 (prefers-color-scheme: dark) ,浅色模式匹配 (prefers-color-scheme: light)

监听主题模式,深色模式时为 body 添加类名 dark,根据 CSS 变量的响应式布局特点,自动生效 dark 类名下的 CSS。

const darkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)'); 
// 判断是否匹配深色模式 
if (darkMode && darkMode.matches) { 
  document.body.classList.add('dark'); 
} 
// 监听主题切换事件 
darkMode && darkMode.addEventListener('change', e => { 
  if (e.matches) { 
    document.body.classList.add('dark'); 
  } else { 
    document.body.classList.remove('dark');  
  } 
});  

那么,针对不支持 CSS 变量的 IE 浏览器怎么办呢?不做兼容性处理的话那页面可能就是一团糟了。所以我们需要针对不兼容的浏览器做一些兜底处理,这里我们可以在 webpack 等构建工具中借助 post-css 的 postcss-css-variables 插件来自动解析 CSS 变量对应的色值,并在原始 CSS 定义之上添加一条新的 CSS 样式 , 做到对不支持 CSS 变量 浏览器 的兼容 。

用法如下:

// 根目录 postcss.config.js 
module.exports = { 
  plugins: { 
    "postcss-css-variables": { 
      preserve: true, // 保留 var() 定义 
      preserveInjectedVariables: false, // 去除其他模块的重复变量 
      variables: require("./page.json"), // CSS 变量,可以支持多个 
    } 
  } 
}; 

图片

项目实践

现在的 Web、App 项目大都引用第三方开源组件库,组件库一般使用会 Sass、Less 等 CSS 预处理器 定义颜色变量作为组件的基础色值,并单独抽离为配置文件。所以,项目使用组件库时可以根据修改基础色值来自定义主题。那么针对项目的 深色模式适配方案 也一样,主要分为三步:一、组件库深浅色主题 适配 ; 二是、项目中深浅色的颜色适配 ; 三、 完成 CSS 变量到页面的 注入。

组件库样式、自定义样式适配

如果第三方组件本身支持多主题或者深色模式,可以直接按说明给组件设置对应主题模式;如果第三方组件库不支持的话,只能用覆盖的方式。这里以 Less 为例进行简单实例说明:

修改前:

// index.less 
@white: #fff; // 颜色预定义 
@background-color: @white; // 组件样式 panel.less 
.panel-background-color { 
  background-color: @background-color; // 组件中使用 less 变量定义颜色样式 
} 

新增两个 js 或者 JSON 文件,分别定义深浅模式下的 CSS 变量, 并命名为 light-theme1.js、dark-theme2.js 他们并不会影响组件的样式,只是便于后期注入到全局 style 中。

修改后:

// 浅色主题文件 light-theme1.js 
const bgColor = '#fff';// 颜色预定义 
module.exports = { 
  "--background-color": bgColor; 
} 
// 深色主题文件 dark-theme1.js 
const bgColor = '#000';// 颜色预定义 
module.exports = { 
  "--background-color": bgColor; 
} 
// 组件样式 panel.less 
.panel-background-color { 
  background-color: var(--background-color); //组件中颜色样式 
} 

CSS 变量支持第二参数,当变量不存在或者未注册成功时,可以为其设置默认值,优化如下:

// 组件样式 panel.less 
.panel-background-color { 
  background-color: var(--background-color, @background-color); // 组件中颜色样式,其中 @background-color 代表修改前组件的背景颜色变量,这里设其为默认值,在适配不成功情况下,可以保持适配前的样式。 
} 

项目才是真正使用组件的地方,并且项目本身也有很多 自定义 CSS 的 颜色样式,需要做与组件库类似的处理,结果也会得到两个 js / json 文件,分别命名为 light-theme2.js 、dark-theme2.js。

CSS 注入

在页面渲染前,需要把定义深浅 样式的 CSS 变量注入到页面。

以上两步得到了四个文件,合并浅色样式文件 light-theme1.js 和 light-theme2.js 得到 light-theme.js,合并深色样式文件dark-theme1.js 和 dark-theme2.js 得到 dark-theme.js, 最后 把 light-theme.js、 dark-theme.js 两个文件注入到页面中,注入脚本如下:

import lightTheme from './light-theme'; 
import darkTheme from './dark-theme'; 
// 创建一个 style 元素,用于插入 css 定义 
const createStyle = (content) => { 
  const style = document.createElement('style');  
  style.type = 'text/css'; 
  style.innerHTML = content;  
  document.getElementsByTagName("script")[0].parentNode.appendChild(style); 
// 在 body 标签中定义 css 变量 
const createCssStyle = () => { 
  const lightThemeStr = Object.keys(lightTheme).map(key => key + ':' +                         lightTheme[key]).join(';'); 
  const darkThemeStr = Object.keys(darkTheme).map(key => key + ':' + darkTheme[key]).join(';'); 
  const lightContent = `body{${lightThemeStr}}`; // 浅色模式 CSS 变量定义 
  const darkContent = `body.dark{${darkThemeStr}}`; // 深色模式 CSS 变量定义 
  createStyle(lightContent); 
  createStyle(darkContent); 
  isDarkSchemePreference(); 
}; 

注入完成后,项目页面中就有了 css 变量定义,包括浅色模式 CSS 变量定义和深色模式 CSS 变量定义,具体哪一个生效, 就可以根据上面提到的两种适配方案 给 body 添加 class 来控制 。 默认时浅色模式生效,添加 dark 类名时,深色模式会生效。至此就实现了一套完整的深色模式适配方案。

native 深色适配

iOS

在 iOS 系统中, 开发者从颜色和图片两个方面来进行适配,我们不需要关心切换模式后该怎么操作,因为这些都由系统帮我们实现。颜色的适配,需要使用系统提供的API,在回调用中不同的模式下分别设置颜色,而图片的适配,需要在 XCode 的 工具栏中 Appearances 下选择 Any,Dark,在同一名称资源的配置下分别添加图片资源。当切换深色模式时,系统会根据适配的颜色和图片资源进行查找和自动切换对应模式下的颜色和资源文件。

Android

安卓在 Android 10(API 级别 29)及更高版本中提供深色主题背景,可以通过以下三种方法启用深色主题背景:

  • 使用系统设置(Settings -> Display -> Theme)启用深色主题背景
  • 使用"快捷设置"图块,从通知托盘中切换主题背景(启用后)
  • 在 Pixel 设备上,选择"省电模式"将同时启用深色主题背景,其他原始设备制造商 (OEM) 不一定支持这种行为

在应用中支持深色主题背景

如要支持深色主题背景,必须将应用的主题背景(通常可在 res/values/styles.xml 中找到)设置为继承 DayNight 主题背景:

<style name="AppTheme" parent="Theme.AppCompat.DayNight"> 

还可以使用 MaterialComponent 的深色主题背景:

<style name="AppTheme" parent="Theme.MaterialComponents.DayNight"> 

这会将应用的主要主题背景与系统控制的夜间模式标记相关联,并将应用的默认主题背景设置为深色主题背景(如果已启用)。

主题背景和样式

主题背景和样式应避免使用旨在于浅色主题背景下使用的硬编码颜色或图标,您应改用主题背景属性(首选)或适合在夜间使用的资源,以下是需要了解的两个最重要的主题背景属性:

  • ?android:attr/textColorPrimary 这是一种通用型文本颜色,它在浅色主题背景下接近于黑色,在深色主题背景下接近于白色,该颜色包含一个停用状态。
  • ?attr/colorControlNormal 一种通用图标颜色,该颜色包含一个停用状态。

Flutter

这里以Flutter 为例,简单介绍下跨平台开发框架如何适配深色模式。Flutter定义主题有两种方式:全局主题或使用Theme来定义应用程序局部的颜色和字体样式。

全局主题

全局主题就是有应用程序根 MaterialAPP 创建 的 Theme。为了在整个应用程序中共享包含颜色和字体样式的主题,我们可以提供 ThemeData 给 Material 的构造函数。theme 指定的是浅色模式,darkTheme 指定的是深色模式,程序会根据系统设定的暗黑模式自动匹配模式。

new MaterialApp( 
  title: title, 
  theme: new ThemeData( 
     brightness: Brightness.light, 
     primaryColor: Colors.lightBlue[800], 
     accentColor: Colors.cyan[600] , 
  ), 
  darkTheme: new ThemeData( 
     brightness: Brightness.dark, 
     primaryColor: Colors.lightGreen[800] , 
     accentColor: Colors.cyan[200], 
  ), 
); 

局部主题

如果我们想在应用程序的一部分中覆盖应用程序的全局的主题,我们可以将要覆盖得部分封装在一个 Theme 的 Widget 中,有2种方法可解决:创建特有的 ThemeData 或扩展父主题。

创建特有的ThemeData

如果我们不想继承任何应用程序的颜色或字体样式,我们可以通过 new ThemeData() 创建一个实例并将其传递给 Theme Widget。

// Create a unique theme with "new ThemeData" 
new Theme( 
  data: new ThemeData( 
    accentColor: Colors.yellow, 
  ), 
  child: new FloatingActionButton( 
    onPressed: () {}, 
    child: new Icon(Icons.add), 
  ), 
); 

扩展父主题

扩展父主题时无需覆盖所有的主题属性,我们可以通过使用 copyWith 方法来实现。

// Find and Extend the parent theme using "copyWith". Please see the next section for more info on `Theme.of`. 
new Theme( 
  data: Theme.of(context).copyWith(accentColor: Colors.yellow), 
  child: new FloatingActionButton( 
    onPressed: null, 
    child: new Icon(Icons.add), 
  ), 
); 

使用主题

我们可以在Widget的 build 方法中通过 Theme.of(context) 函数使用自定义的主题。

new Container( 
  color: Theme.of(context).accentColor, 
  child: new Text( 
    'Text with a background color', 
    style: Theme.of(context).textTheme.title, 
  ), 
); 

渲染效果 如下 :

总结

以上分别介绍了在 App 应用中对 H5 页面和客户端的深色模式适配方案,当然其中 H5 的方案页同样适应于 PC 端。使用前一定要确保你的系统和浏览器是兼容深色模式的,不然就没有效果了呢。本篇只简单介绍了几种方案,欢迎有更好想法的小伙伴一起讨论~

参考资料

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com

查看原文

赞 9 收藏 6 评论 0

政采云前端团队 发布了文章 · 8月14日

ZooTeam 前端周刊|第 95 期

政采云前端小报第95期

浏览更多往期小报,请访问: https://weekly.zoo.team

1.RETE算法简述 & 实践

概述 Rete 算法是卡内基梅隆大学的 Charles L.Forgy 博士在 1974 年发表的论文中所阐述的算法。 该算法提供了专家系统的一个高效实现。

2.十五张图带你彻底搞懂从URL到页面展示发生的故事

关注公众号“执鸢者”,获取大量教学视频并进入专业交流群。某一天小林去面试,面试官说问你一道经典面试题吧,从“输入一个URL到页面展示中间发生了什么?”,小林一听激动了,心里暗自高兴说这道题我背过呀,然后哗啦哗啦开启了背书模式。背完之后面试官不是很满意,思路并不是很清晰呀!!!(纯属个人杜撰的小故事,切勿当真。)下面就让我们来唠一唠这个小问题,有不准确的地方还望各位大佬指正。

3.前端性能优化之雅虎35条军规

前端性能优化之雅虎35条军规。

4.可能这些是你想要的H5键盘兼容方案

掘金是一个帮助开发者成长的社区,是给开发者用的 Hacker News,给设计师用的 Designer News,和给产品经理用的 Medium。掘金的技术文章由稀土上聚集的技术大牛和极客共同编辑为你筛选出最优质的干货,其中包括:Android、iOS、前端、后端等方面的内容。用户每天都可以在这里找到技术世界的头条内容。与此同时,掘金内还有沸点、掘金翻译计划、线下活动、专栏文章等内容。即使你是 GitHub、StackOverflow、开源中国的用户,我们相信你也可以在这里有所收获。

5.React Fiber 源码解析

在 React v16.13 版本中,正式推出了实验性的 Concurrent Mode,尤其是提供一种新的机制 Suspense,非常自然地解决了一直以来存在的异步副作用问题。结合前面 v16.8 推出的 Hooks,v16.0 底层架构 Fiber,React 给开发者体验上带来了极大提升以及一定程度上更佳的用户体验。所以,对 React 17,你会有什么期待?

6.如何复用一套代码满足多样化的需求?

有太多的文章教你怎么组织代码了。但是这些文章大都是系统A,模块B的抽象写意派。虽然看着很有道理的样子,但就是看不懂。 本文的特点是有十多个带有具体业务场景的例子。从如何接新需求的角度来分析模块应该怎么…

7.Webpack 模块打包原理

掘金是一个帮助开发者成长的社区,是给开发者用的 Hacker News,给设计师用的 Designer News,和给产品经理用的 Medium。掘金的技术文章由稀土上聚集的技术大牛和极客共同编辑为你筛选出最优质的干货,其中包括:Android、iOS、前端、后端等方面的内容。用户每天都可以在这里找到技术世界的头条内容。与此同时,掘金内还有沸点、掘金翻译计划、线下活动、专栏文章等内容。即使你是 GitHub、StackOverflow、开源中国的用户,我们相信你也可以在这里有所收获。

8.Lighthouse 测试内幕

Lighthouse 测试内幕。

9.核心稳定、易扩展——开放关闭原则(The Open-Closed Principle)

模块划分,整体与部分的关系。

10.Web开发应了解的5种设计模式

掘金是一个帮助开发者成长的社区,是给开发者用的 Hacker News,给设计师用的 Designer News,和给产品经理用的 Medium。掘金的技术文章由稀土上聚集的技术大牛和极客共同编辑为你筛选出最优质的干货,其中包括:Android、iOS、前端、后端等方面的内容。用户每天都可以在这里找到技术世界的头条内容。与此同时,掘金内还有沸点、掘金翻译计划、线下活动、专栏文章等内容。即使你是 GitHub、StackOverflow、开源中国的用户,我们相信你也可以在这里有所收获。

11.css Table布局-display:table-WEB前端开发

使用表格布局一直是一个敏感的主题。一般情况下,Web开发人员考虑基于表格布局是禁忌。尽管反对的理由看起来证据很充分,但是大多数开发者除了谴责基于表格的布局,都无法提供完善的使用场景。“表格不好。” 从早期反对HTML Table(<table>标签)开始这种势头就非常强劲。几代开发者被成功洗脑,根深蒂固的认为:任何使用表格都是邪恶的。

查看原文

赞 2 收藏 1 评论 0

政采云前端团队 发布了文章 · 8月10日

编写高质量可维护的代码:逻辑判断

64 篇原创好文~
本文首发于政采云前端团队博客:编写高质量可维护的代码:逻辑判断

if else、switch case 是日常开发中最常见的条件判断语句,这种看似简单的语句,当遇到复杂的业务场景时,如果处理不善,就会出现大量的逻辑嵌套,可读性差并且难以扩展。

编写高质量可维护的代码,我们先从最小处入手,一起来看看在前端开发过程中,可以从哪些方面来优化逻辑判断?

下面我们会分别从 JavaScript 语法和 React JSX 语法两个方面来分享一些优化的技巧。

JavaScript 语法篇

嵌套层级优化

function supply(fruit, quantity) {
  const redFruits = ['apple', 'strawberry', 'cherry', 'cranberries'];
  // 条件 1: 水果存在
  if(fruit) {
    // 条件 2: 属于红色水果
    if(redFruits.includes(fruit)) {
      console.log('红色水果');
      // 条件 3: 水果数量大于 10 个
      if (quantity > 10) {
        console.log('数量大于 10 个');
      }
    }
  } else {
    throw new Error('没有水果啦!');
  }
}

分析上面的条件判断,存在三层 if 条件嵌套。

如果提前 return 掉无效条件,将 if else的多重嵌套层次减少到一层,更容易理解和维护。

function supply(fruit, quantity) {
  const redFruits = ['apple', 'strawberry', 'cherry', 'cranberries'];
  if(!fruit) throw new Error('没有水果啦'); // 条件 1: 当 fruit 无效时,提前处理错误
  if(!redFruits.includes(fruit)) return; // 条件 2: 当不是红色水果时,提前 return

  console.log('红色水果');

  // 条件 3: 水果数量大于 10 个
  if (quantity > 10) {
    console.log('数量大于 10 个');
  }
}

多条件分支的优化处理

当需要枚举值处理不同的业务分支逻辑时, 第一反应是写下 if else ?我们来看一下:

function pick(color) {
  // 根据颜色选择水果
  if(color === 'red') {
    return ['apple', 'strawberry']; 
  } else if (color === 'yellow') {
    return ['banana', 'pineapple'];
  } else if (color === 'purple') {
    return ['grape', 'plum'];
  } else {
    return [];
  }
}

在上面的实现中:

  • if else 分支太多
  • if else 更适合于条件区间判断,而 switch case 更适合于具体枚举值的分支判断

使用 switch case 优化上面的代码后:

function pick(color) {
  // 根据颜色选择水果
  switch (color) {
    case 'red':
      return ['apple', 'strawberry'];
    case 'yellow':
      return ['banana', 'pineapple'];
    case 'purple':
      return ['grape', 'plum'];
    default:
      return [];
  }
}

switch case 优化之后的代码看上去格式整齐,思路很清晰,但还是很冗长。继续优化:

  • 借助 Object 的 { key: value } 结构,我们可以在 Object 中枚举所有的情况,然后将 key 作为索引,直接通过 Object.key 或者 Object[key] 来获取内容
const fruitColor = {                                                                        
  red: ['apple', 'strawberry'],
  yellow: ['banana', 'pineapple'],
  purple: ['grape', 'plum'],
}
function pick(color) {
  return fruitColor[color] || [];
}
  • 使用 Map 数据结构,真正的 (key, value) 键值对结构 ;
const fruitColor = new Map()
.set('red', ['apple', 'strawberry'])
.set('yellow', ['banana', 'pineapple'])
.set('purple', ['grape', 'plum']);

function pick(color) {
  return fruitColor.get(color) || [];
}

优化之后,代码更简洁、更容易扩展。

为了更好的可读性,还可以通过更加语义化的方式定义对象,然后使用 Array.filter 达到同样的效果。

const fruits = [
  { name: 'apple', color: 'red' }, 
  { name: 'strawberry', color: 'red' }, 
  { name: 'banana', color: 'yellow' }, 
  { name: 'pineapple', color: 'yellow' }, 
  { name: 'grape', color: 'purple' }, 
  { name: 'plum', color: 'purple' }
];

function pick(color) {
  return fruits.filter(f => f.color == color);
}

使用数组新特性简化逻辑判断

巧妙的利用 ES6 中提供的数组新特性,也可以让我们更轻松的处理逻辑判断。

多条件判断

编码时遇到多个判断条件时,本能的写下下面的代码(其实也是最能表达业务逻辑的面向过程编码)。

function judge(fruit) {
  if (fruit === 'apple' || fruit === 'strawberry' || fruit === 'cherry' || fruit === 'cranberries' ) {
    console.log('red');
  }
}

但是当 type 未来到 10 种甚至更多时, 我们只能继续添加 || 来维护代码么 ?

试试 Array.includes ~

// 将判断条件抽取成一个数组
const redFruits = ['apple', 'strawberry', 'cherry', 'cranberries'];
function judge(type) {
  if (redFruits.includes(fruit)) {
    console.log('red');
  }
}

判断数组中是否所有项都满足某条件

const fruits = [
  { name: 'apple', color: 'red' },
  { name: 'banana', color: 'yellow' },
  { name: 'grape', color: 'purple' }
];

function match() {
  let isAllRed = true;

  // 判断条件:所有的水果都必须是红色
  for (let f of fruits) {
    if (!isAllRed) break;
    isAllRed = (f.color === 'red');
  }

  console.log(isAllRed); // false
}

上面的实现中,主要是为了处理数组中的所有项都符合条件。

使用 Array.every 可以很容的实现这个逻辑:

const fruits = [
  { name: 'apple', color: 'red' },
  { name: 'banana', color: 'yellow' },
  { name: 'grape', color: 'purple' }
];

function match() {
  // 条件:所有水果都必须是红色
  const isAllRed = fruits.every(f => f.color == 'red');

  console.log(isAllRed); // false
}

判断数组中是否有某一项满足条件

Array.some ,它主要处理的场景是判断数组中是否有一项满足条件。

如果想知道是否有红色水果,可以直接使用 Array.some 方法:

const fruits = [
  { name: 'apple', color: 'red' },
  { name: 'banana', color: 'yellow' },
  { name: 'grape', color: 'purple' }
];

// 条件:是否有红色水果 
const isAnyRed = fruits.some(f => f.color == 'red');

还有许多其他数组新特性,比如 Array.find、Array.slice、Array.findIndex、Array.reduce、Array.splice 等,在实际场景中可以根据需要选择使用。

函数默认值

使用默认参数

const buyFruit = (fruit,amount) => {
  if(!fruit){
    return
  }
  amount = amount || 1;
  console.log(amount)
}

我们经常需要处理函数内部的一些参数默认值,上面的代码大家都不陌生,使用函数的默认参数,可以很好的帮助处理这种场景。

const buyFruit = (fruit,amount = 1) => {
  if(!fruit){
    return
  }
  console.log(amount,'amount')
}

我们可以通过 Babel 的转译来看一下默认参数是如何实现的。

从上面的转译结果可以发现,只有参数为 undefined 时才会使用默认参数。

测试的执行结果如下:

buyFruit('apple','');  // amount
buyFruit('apple',null);  //null amount
buyFruit('apple');  //1 amount

所以使用默认参数的情况下,我们需要注意的是默认参数 amount=1 并不等同于 amount || 1

使用解构与默认参数

当函数参数是对象时,我们可以使用解构结合默认参数来简化逻辑。

Before:

const buyFruit = (fruit,amount) => {
    fruit = fruit || {};
    if(!fruit.name || !fruit.price){
        return;
    }
    ...
  amount = amount || 1;
  console.log(amount)
}

After:

const buyFruit = ({ name, price }={},amount) => {
  if(!name || !prices){
      return;
  }
  console.log(amount)
}

复杂数据解构

当处理比较简的对象时,解构与默认参数的配合是非常好的,但在一些复杂的场景中,我们面临的可能是更复杂的结构。

const oneComplexObj = {
    firstLevel:{
        secondLevel:[{
            name: "",
            price: ""
        }]
    }
}

这个时候如果再通过解构去获取对象里的值。

const {
  firstLevel:{
    secondLevel: [{name, price]=[]
  }={}
} = oneComplexObj;              

可读性就会比较差,而且需要考虑多层解构的默认值以及数据异常情况。

这种情况下,如果项目中使用 lodash 库,可以使用其中的 lodash/get 方法。

import lodashGet from 'lodash/get';

const { name, price} = lodashGet(oneComplexObj,'firstLevel.secondLevel[0]',{});

策略模式优化分支逻辑处理

策略模式:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。

使用场景:策略模式属于对象行为模式,当遇到具有相同行为接口、行为内部不同逻辑实现的实例对象时,可以采用策略模式;或者是一组对象可以根据需要动态的选择几种行为中的某一种时,也可以采用策略模式;这里以第二种情况作为示例:

Before:

const TYPE = {
  JUICE:'juice',
  SALAD:'salad',
  JAM:'jam'
}
function enjoy({type = TYPE.JUICE,fruits}){
  if(!fruits || !fruits.length) {
    console.log('请先采购水果!');
    return;
  }
  if(type === TYPE.JUICE) {
    console.log('榨果汁中...');
    return '果汁';
  }
  if(type === TYPE.SALAD) {
    console.log('做沙拉中...');
    return '拉沙';
  }
  if(type === TYPE.JAM) {
    console.log('做果酱中...');
    return '果酱';
  }
  return;
}

enjoy({type:'juice',fruits});

使用思路:定义策略对象封装不同行为、提供策略选择接口,在不同的规则时调用相应的行为。

After:

const TYPE = {
  JUICE:'juice',
  SALAD:'salad',
  JAM:'jam'
}

const strategies = {
  [TYPE.JUICE]: function(fruits){
    console.log('榨果汁中...');
    return '果汁';
  },
  [TYPE.SALAD]:function(fruits){
    console.log('做沙拉中...');
    return '沙拉';
  },
  [TYPE.JAM]:function(fruits){
    console.log('做果酱中...');
    return '果酱';
  },
}

function enjoy({type = TYPE.JUICE,fruits}) {
  if(!type) {
    console.log('请直接享用!');
    return;
  }
  if(!fruits || !fruits.length) {
    console.log('请先采购水果!');
    return;
  }
  return strategies[type](fruits);
}

enjoy({type: 'juice',fruits});

框架篇之 React JSX 逻辑判断优化

JSX 是一个看起来很像 XML 的 JavaScript 语法扩展。一般在 React 中使用 JSX 来描述界面信息,ReactDOM.render() 将 JSX 界面信息渲染到页面上。

在 JSX 中支持 JavaScript 表达式,日常很常见的循环输出子组件、三元表达式判断、再复杂一些直接抽象出一个函数。

在 JSX 中写这么多 JavaScript 表达式,整体代码看起来会有点儿杂乱。试着优化一下!

JSX-Control-Statements

JSX-Control-Statements 是一个 Babel 插件,它扩展了 JSX 的能力,支持以标签的形式处理条件判断、循环。

If 标签

<If> 标签内容只有在 condition 为 true 时才会渲染,等价于最简单的三元表达式。

Before:

{ condition() ? 'Hello World!' : null }   

After:

<If condition={ condition() }>Hello World!</If>   

注意:<Else /> 已被废弃,复杂的条件判断可以使用 <Choose> 标签。

Choose 标签

<Choose> 标签下包括至少一个 <When> 标签、可选的 <Otherwise> 标签。

<When> 标签内容只有在 condition 为 true 时才会渲染,相当于一个 if 条件判断分支。

<Otherwise> 标签则相当于最后的 else 分支。

Before:

{ test1 ? <span>IfBlock1</span> : test2 ? <span>IfBlock2</span> : <span>ElseBlock</span> }

After:

<Choose>
  <When condition={ test1 }>
    <span>IfBlock1</span>
  </When>
  <When condition={ test2 }>
    <span>IfBlock2</span>
  </When>
  <Otherwise>
    <span>ElseBlock</span>
  </Otherwise>
</Choose>

For 标签

<For> 标签需要声明 of、each 属性。

of 接收的是可以使用迭代器访问的对象。

each 代表迭代器访问时的当前指向元素。

Before:

{
  (this.props.items || []).map(item => {
      return <span key={ item.id }>{ item.title }</span>
  })
}

After:

<For each="item" of={ this.props.items }>
   <span key={ item.id }>{ item.title }</span>
</For>

注意:<For> 标签不能作为根元素。

With 标签

<With> 标签提供变量传参的功能。

Before:

renderFoo = (foo) => {
  return <span>{ foo }</span>;
}

// JSX 中表达式调用
{
  this.renderFoo(47)
}

After:

<With foo={ 47 }>
  <span>{ foo }</span>
</With>

使用这几种标签优化代码,可以减少 JSX 中存在的显式 JavaScript 表达式,使我们的代码看上去更简洁,但是这些标签封装的能力,在编译时需要转换为等价的 JavaScript 表达式。

注意:具体 babel-plugin-jsx-control-statements 插件的使用见第三篇参考文章;Vue 框架已经通过指令的形式支持 v-if、v-else-if、v-else、v-show、slot 等。

总结

以上我们总结了一些常见的逻辑判断优化技巧。当然,编写高质量可维护的代码,除了逻辑判断优化,还需要有清晰的注释、含义明确的变量命名、合理的代码结构拆分、逻辑分层解耦、以及更高层次的贴合业务的逻辑抽象等等,相信各位在这方面也有自己的一些心得,欢迎一起留言讨论~

参考文献

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com

查看原文

赞 16 收藏 9 评论 0

政采云前端团队 发布了文章 · 8月7日

ZooTeam 前端周刊|第 94 期

政采云前端小报第94期

浏览更多往期小报,请访问: https://weekly.zoo.team

  1. [[译] 前端组件设计原则](https://juejin.im/post/684490...
掘金是一个帮助开发者成长的社区,是给开发者用的 Hacker News,给设计师用的 Designer News,和给产品经理用的 Medium。掘金的技术文章由稀土上聚集的技术大牛和极客共同编辑为你筛选出最优质的干货,其中包括:Android、iOS、前端、后端等方面的内容。用户每天都可以在这里找到技术世界的头条内容。与此同时,掘金内还有沸点、掘金翻译计划、线下活动、专栏文章等内容。即使你是 GitHub、StackOverflow、开源中国的用户,我们相信你也可以在这里有所收获。
  1. 从一道Promise执行顺序的题目看Promise实现
细节
  1. 关于 cdn、回源等问题一网打尽
掘金是一个帮助开发者成长的社区,是给开发者用的 Hacker News,给设计师用的 Designer News,和给产品经理用的 Medium。掘金的技术文章由稀土上聚集的技术大牛和极客共同编辑为你筛选出最优质的干货,其中包括:Android、iOS、前端、后端等方面的内容。用户每天都可以在这里找到技术世界的头条内容。与此同时,掘金内还有沸点、掘金翻译计划、线下活动、专栏文章等内容。即使你是 GitHub、StackOverflow、开源中国的用户,我们相信你也可以在这里有所收获。
4. [CDN加速原理](https://www.jianshu.com/p/1dae6e1680ff)
CDN的全称是(Content Delivery Network),即内容分发网络。其目的是通过在现有的Internet中增加一层新的CACHE(缓存)层,将网站的内容发布到最接近用户的网络”边缘“的节点,使用户可以就近取得所需的内容,提高用户访问网站的响应速度。
  1. 今天教你烤一份香喷喷的Electron
掘金是一个帮助开发者成长的社区,是给开发者用的 Hacker News,给设计师用的 Designer News,和给产品经理用的 Medium。掘金的技术文章由稀土上聚集的技术大牛和极客共同编辑为你筛选出最优质的干货,其中包括:Android、iOS、前端、后端等方面的内容。用户每天都可以在这里找到技术世界的头条内容。与此同时,掘金内还有沸点、掘金翻译计划、线下活动、专栏文章等内容。即使你是 GitHub、StackOverflow、开源中国的用户,我们相信你也可以在这里有所收获。
  1. 3d transform的(x、y、z)坐标空间及位置
原文出处:http://acgtofe.com/posts/2015... transform,就可以在平面的网页里添加炫酷的三维视觉效果,这很令人愉悦。需要注意的是,3d transform只是css的一部分,它并不是一个三维引擎(3d engine)。三维引..._transfrom中的中的x和y坐标图
  1. 一文让你了解微前端的现状
在前端Web开发中,微前端(microfrontends)是一个备受争议的话题。它是否值得开发者采用?面对如此之多的神奇案例,人们无法否认微前端正日益流行这个事实。本文将探究微前端的具体使用场景和使用群体,并给出能快速轻松上手的现有解决方案。什么是微前端?微前端将大规模的后端系统切分为很多面向前端的微服务,力图实现一定程度的改进。
  1. 项目不知道如何做性能优化?不妨试一下代码分割
性能优化
  1. 阿里实习 90 天:从实习生的视角谈谈个人成长
掘金是一个帮助开发者成长的社区,是给开发者用的 Hacker News,给设计师用的 Designer News,和给产品经理用的 Medium。掘金的技术文章由稀土上聚集的技术大牛和极客共同编辑为你筛选出最优质的干货,其中包括:Android、iOS、前端、后端等方面的内容。用户每天都可以在这里找到技术世界的头条内容。与此同时,掘金内还有沸点、掘金翻译计划、线下活动、专栏文章等内容。即使你是 GitHub、StackOverflow、开源中国的用户,我们相信你也可以在这里有所收获。
  1. 在Vue中使用装饰器,我是认真的
掘金是一个帮助开发者成长的社区,是给开发者用的 Hacker News,给设计师用的 Designer News,和给产品经理用的 Medium。掘金的技术文章由稀土上聚集的技术大牛和极客共同编辑为你筛选出最优质的干货,其中包括:Android、iOS、前端、后端等方面的内容。用户每天都可以在这里找到技术世界的头条内容。与此同时,掘金内还有沸点、掘金翻译计划、线下活动、专栏文章等内容。即使你是 GitHub、StackOverflow、开源中国的用户,我们相信你也可以在这里有所收获。
  1. 「可视化搭建系统」——从设计到架构,探索前端领域技术和业务价值
阿里巴巴集团前端委员会主席 圆心:未来前端的机会在哪里 对前端未来期许有四点:搭建服务, Serverless,智能化,IDE。仔细想想,一个「可视化搭建系统」的想象空间,正能完美命中这些方面。前端的边界在哪里,对…
  1. 阿里前端委员会主席圆心:未来前端的机会在哪里?
阿里妹导读:在近期举办的行业大会上,阿里前端技术委员会主席,淘系技术部资深总监圆心发表了《前端路上的思考》的演讲,分别从前端的发展历程、今天的机会、如何引领新技术、前端价值这四个方面进行深入探讨。流…
  1. 618大促背后前端代码如何智能生成?
简介: 你关心玩法,我关心技术!作为淘系每年重要的大促活动 618 是如何保证平稳进行的?七大章节全方位展示 618 中的前端身影!另附 6000+ 字图文版前端学习秘籍和面试官直达简历投递地址,还不快来get?《大促…
  1. 秒杀系统设计
我之前写过一个秒杀系统的文章不过有些许瑕疵,所以我准备在之前的基础上进行二次创作,不过让我决心二创秒杀系统的原因是我最近面试了很多读者,动不动就是秒杀系统把我整蒙蔽了,我懵的主要是秒杀系统的细节大家都不知道,甚至不知道电商公司一个秒杀系统的组成部分。
  1. 利用puppeteer来录制网页操作导出 GIF 动图
先来看看效果,然后我们再说怎么实现。可以看到完整的录制了我用puppeteer来编写的自动化脚本行为,那么是如何实现的呢,一开始我也没什么思路,看了一下开源的方案,大部分都是让使用一个 recoder 插件,看了一眼…

查看原文

赞 2 收藏 0 评论 0

政采云前端团队 发布了文章 · 8月3日

如何基于 Electron 开发跨终端的应用

63 篇原创好文~ 本文首发于政采云前端团队博客:如何基于 Electron 开发跨终端的应用

自我介绍

欢迎大家来到今天的早早聊跨端跨栈专场,今天我分享的主题是《如何基于 Electron 开发跨终端的应用》。 先做一下自我介绍,我叫逯子洋,17 年加入政采云,目前主要负责政采云前端工程化平台敦煌以及政采云电子招投标客户端的建设。这边是我们团队的微信公众号,大家如果想对我们团队有更多的了解,可以关注一下我们的公众号。

首先我们分享的第一块叫端的延展。不知道大家对这张图熟不熟悉,前段时间的新闻大家应该都听到过,硅谷钢铁侠艾隆马斯克发布了第一款商业化的载人龙飞船,这张图片中就是龙飞船的控制台,知乎上有人对这张图的评价叫 JS 上天了。为什么说叫 JS 上天了呢?因为有传言说它是基于 Electron 开发的,不过这个消息并没有得到证实。但是可以证实的一点是航天飞船的触控界面 UI ,确实是基于 Chromium + JavaScript 这样的架构来实现的。这也从某种程度上说明了这种架构的一个可用性和稳定性的能力。

下面我们一起来回顾一下前端在整个端领域的发展历程。在早期,前端工程师的定义可能是基于浏览器运行环境的 Web 开发,但是随着 09 年 Node.js 的出现,让前端工程师有了脱离浏览器运行环境的开发能力。我们拥有了可以面向服务端开发的能力,前端的能力延展到了服务端。

随着 HTML5 标准的制定,以及移动端设备技术的发展,前端工程师也可以更多的拥抱面向移动端场景的开发。也出现了像今天上午两位讲师所讲到的移动端领域 React Native 这样的跨平台技术方案。随着移动 APP 成为一个主流,基于这些智能化的设备以及芯片的计算能力,前端也普及到了物联网设备方向,前端可以拥有了面向 Iot 的开发能力,也诞生了像 Thing.js 这样的面向物联网设备开发的 Js 框架。

CLI -> GUI

今天所要讲的主题是桌面端,随着 Electron 这样的跨终端 JS 框架的出现,整个前端工程师的能力也是延展到了桌面端。当我们拥有了这样的一个桌面端的开发能力之后,它能带给我们的价值是什么呢?首先看一下桌面端给我们带来哪些不一样的体验。 大家看到左边这张图,是早期电脑的 DOS 系统的运行的截图,右边则是 1983 年苹果电脑发布的第一款 Apple Lisa 个人电脑,它是全球第一款搭载图形用户界面(也就是我们所说的 GUI)的一台个人电脑,正是因为这款电脑的问世,让后期个人电脑大众化的普及得以实现。为什么它会带动个人电脑的普及化,是因为图形界面对于用户来说,在视觉上更容易接受,学习的成本也是大幅的下降。相信用过 MAC 系统的同学都会对苹果优秀的界面设计以及整体的交互体验,有比较深刻的感受。

那么,这样的桌面端 GUI 的技术,能给我的前端开发工作带来什么不一样呢?

左边这个流程相信大家不陌生,在我们开始新项目开发的时候,可能需要做哪些事情?首先第一步可能是需要去创建一个 Git 仓库,创建完成之后将仓库克隆到本地,然后通过团队内部的 CLI 工具的安装之后,去执行例如 xxx-cli create 这样的命令去创建一个项目。创建项目完成之后,如果想进行开发,我们需要去运行 npm install ,安装所需的依赖包,最终将整个项目提交到 Git 仓库上去。这是我们新项目的创建,基于 CLI 方式的一个操作流程。

如果说基于客户端的能力的,我们可以做哪些改变呢?我们可以看到,右边的图是我们团队前端工程化平台敦煌的系统截图。如果是创建一个新项目,只需要选择自己的创建方式,然后输入一些必要的创建参数,比如说选择你需要创建的 Git 仓库 Group、项目名称、脚手架类型,点击创建项目之后,它会自动帮你将左边的这一系列的流程全部执行完毕。就是说将左边 6 个步骤简化到了 2 个步骤,大大的简化了操作的链路。

GUI 赋予的价值

GUI 赋予了我们哪些价值呢?首先它可以将分散的任务点进行一个串联整合,提供了一个更简化的操作链路,同时它还可以抹平不同同学使用时一些流程间的差异,以及流程所依赖的一些环境的差异,并且基于 GUI 的一个整合能力,我们还可以将其他能力进行一个横向的串通,并且通过 GUI 来设计插件化的机制,还可以创造一个可共建的生态。同时基于 GUI 的图形界面操作低学习成本,以及它对整个流程的托管,也是可以大大的降低团队同学的研发复杂度。

业务场景应用

下面是基于 Electron 开发的一些重点应用的落地场景。这是我所负责的政采云电子招投标客户端的业务。它主要的功能是帮助用户由传统的线下招投标,纸质的标书,转变为电子标书,我们提供这样的客户端可以帮助用户串联整个制作标书的流程。同时基于 Electron 所提供的 Node.js 的能力,用户本地标书文件的读写,以及本地文件的加解密操作,都可以在客户端里面完成。

基建场景应用

这边是 Electron 在我们的工程化平台上的实践,这就是我们前面所提到的前端工程化平台-敦煌。它主要做的内容是对整个前端研发流程的托管,像我们刚才所提到的项目创建就是其中的一个环节。下面我们还会详细的介绍一些这方面的应用。

开发模式

上面我们大概介绍了一下 Electron 的一些价值。如果说我们想基于 Electron 开发一个跨平台的桌面端应用,应该如何来做?下面一起来看一下,第二部分开发模式。Electron 的开发模式跟我们平时的 Web 开发有哪些不一样的地方?

Electron 架构

首先这是 Electron 的一个整体的架构,它是由 Github 开发了一个开源框架,允许我们使用来 HTML + CSS + Javascript 来构建开发桌面应用,大大降低了桌面应用的开发的复杂度。整体架构的核心组成是由 Chromium + Node.js + Native APIs 组成的。其中 Chromium 提供了 UI 的能力,Node.js 让 Electron 有了底层操作的能力,Navtive APIs 则解决了跨平台的一些问题,比如说 Windows 系统、MAC OS 系统及 Linux 系统之前一些差异的屏蔽,并且它还提供了一个比较统一体验的原生能力。

能力点

我们来介绍一下它的一些核心的能力点。

  • 首先是 Chromium,我们可以把它理解为是一个拥有最新版浏览器特性的一个 Chrome 浏览器,它带给我们的好处就是在开发过程中无需考虑浏览器的兼容性,我们可以使用一些 ES6、ES7 最新的语法,可以放心的使用 Flex 布局,以及浏览器的最新特性,都可以尝试,不需要考虑兼容性的问题。
  • Node.js 则是提供了一个文件读写、本地命令调用、以及第三方扩展的能力,并且基于 Node.js 整个强大的生态,将近几十万的 Node.js 模块都可以在整个客户端内使用。
  • Native APIs 提供了一个统一的原生界面的能力,还包括一些系统通知、快捷键,还可以通过它来获取一些系统的硬件信息。还提供了桌面客户端的基础能力,像更新机制、崩溃报告这样的能力。

其他桌面端选型对比

Electron 提供这些能力点大大的降低了桌面端开发的成本,以及上手的门槛。当然开发桌面端的话,除了 Electron 外,还会有一些其他的选型,我们看一下它跟其他的选型相比较的话有哪些差异点。

  • 开发桌面端首先可以选择 Native 开发,但是,在开发不同的平台的时候,需要使用不同的语言,但它的优点是具有比较好的原生体验,以及比较好的运行性能,但是它的门槛相对来说还是比较高的。
  • QT 是一个基于 C++ 的跨平台桌面端开发框架,它所使用的语言是使用 C++,整体性能和体验上来说,跟Native 开发的话是可以相媲美的,但由于技术栈原因,开发门槛相对来说也是比较高的。
  • 另外两个就是 Electron 和 NW.js。这两个都是使用 Javascript 作为一个开发语言。相较与 Native 和 QT 来说,它们对前端工程师来说是相当友好的,并且它们两个有着比较相似的一个架构,都是基于 Chromium + Node.js 实现,同时它们也都有一个跨平台的支持能力。但两个的差异点是:Electron 相对来说有一个更好的一个社区的生态和社区的活跃度,我们平时如果遇到了一些问题,在社区内可能会有比较多、比较完善的解决方案,同时它对 issue 的响应速度也是比较快的。

所以基于上面的比较,开发桌面客户端,对前端工程师来说,Electron 是一个非常好的选择。

简单 Electron 应用的结构

下面来看一下,如果想开发一个桌面客户端,应该怎么做呢? 这边是一个最简单的 Electron 桌面应用的结构,我们只需要有三个文件,首先我们通过 package.json 中的 main 字段,通过 main 字段来定义应用的一个启动入口。我们将入口文件定义为 main.js ,在 mian.js 里我们做了哪些事情呢?首先 app 代表着整个应用,监听 app 的状态,当整个应用达到一个 ready 的状态之后,通过 Electron 提供的 BrowserWindow ,去新创建一个浏览器窗口。创建浏览器窗口之后,去加载 index.html 文件,这样的话我们就完成了一个最基础版桌面端应用的实现。基于 Electron 开发桌面端应用,和平时的开发 web 端应用有哪些不一样的,我们需要了解的两个核心概念就是:主进程和渲染进程,以及两个进程间的通信如何实现。在刚才的示例中,其中 main.js 是运行在主进程中, index.html 则是运行在渲染进程之中。下面我们通过一个简单的 Demo,来看一下如何实现两个进程之间的通信,并且如何通过主进程来进行一些 Node.js 能力调用的。

进程间的通信

我们想要实现这样的效果,页面上有一个按钮,当点击按钮之后,向主进程发送了一个 say-hello 的消息,当主进程接收到消息之后,它会在系统桌面上创建一个文件叫 hello.txt。并写入内容 Hello Mac!。具体的我们是怎么做的?

首先在渲染进程里面,我们应该在页面上会进行一个按钮操作事件,当事件触发之后,我们通过 Electron 提供的 ipcRenderer API 向主进程发送一个叫 say-hello 这样的一个消息。当我们的主进程接收到这样一个消息之后,则可以在主进程中直接调用 Node.js 的 fs 模块,一个文件读写的模块。首先先创建一个文件,并且对这个文件写入我们所传输的内容。当文件写入成功之后,对渲染进程进行回复,通过调用 Electron 提供的 Notification模块,显示系统通知去告知用户,这是一个简单的 Demo 的实现,其核心的点就是需要关注主进程和渲染进程的概念,以及两个进程之间是如何通过 IPC 机制进行通信的,这边是一个简单的实现。还有一些更多的应用的场景,这块就不再对 API 进行过多的介绍。

下面我们会根据一个实际的应用,来介绍一下 Electron 开发的实践。

工程化发展 CLI -> GUI

以我们的前端工程化平台敦煌为例,介绍一下我们是如何通过 Electron 将工程化能力由 CLI 式 变为 GUI 式的使用。 首先大家先看一个视频,这个视频就是我们在最开始所提到的项目创建的整个流程的运行的演示。大家可以看到我们整个流程完成了 Git 仓库的创建、项目模板的创建、项目模板到仓库的推送,并且对 Git 项目进行本地克隆,克隆完成之后,会进行依赖的安装,并且在客户端进行重新载入和管理这样一个流程。将之前分散的单点命令操作,通过 GUI 的方式进行一个串联。 这个流程只是工程化平台中的一块,我们在整个工程化平台中,实现了很多的单点命令到工作流的串联。

I2P(Install To Publish)

这边是我们整个前端应用管理平台的系统架构,大概看一下。核心流程就是上面所写到的一个 I2P 的概念,就是 install to publish 。它完成了组件、模板和项目这三个级别,从创建到发布的全流程托管。

  • 创建阶段,主要提供了包括本地创建、Git 创建、统一的创建模板管理、创建的流程审批和创建完成的反馈。
  • 开发阶段,提供了一个 Dev Server 的运行能力,对项目级的页面管理、依赖管理、分支管理,还有一键式的升级能力。同时还打通了 CI/CD 持续集成能力。
  • 发布阶段,则提供了一个发布前的权限校验和合规检测、资源推送以及发布的审批机制。
  • 数据分析,是我们整个流程中比较核心的一块,是对我们整个流程进行一些数据沉淀,并且将这些数据以可视化报表的形式进行成输出,基于这些数据将整个 I2P 的流程与其他的能力进行一个串联。

在上面的整个 I2P 的流程中,我们沉淀出一些项目数据,包括流程数据,可能还有一些类似于组件管理的数据。以数据为连接点,可以将整个的研发流程与其他的一些技术建设能力进行一个串联打通,包括用户行为分析、页面级、项目级的性能分析报告,还有错误监控的机制,都可以接入到整个工程化平台上。 支撑我们整个工程化平台就是一些基础能力以及 Electron 所提供的桌面端能力。 基础能力,包括一些常规的 GIt 操作、NPM 操作、一些命令执行和一些本地的 logger 服务。Electron 提供了桌面端包括更新、窗口管理、通信,以及些原生能力。

由点到线

单点命令 -> 任务流

下面我们就具体来看一下如何实现由一个单点命令到任务流这样的一个串联。将单点命令的操作变为任务流的串联模式,我们要从以下 4 个切入点来实现。 • 首先我们要将常规的一些命令调用变为函数式的调用。 • 基于这些函数式的调用,进行一个任务流的编排和组装,根据实际的开发场景,去定制一个任务流。 • 第三块我们所需要的是整个任务流的任务进度反馈机制,如何将任务执行,通过 GUI 的能力,让用户可以直观感受到整个任务的执行链路和进度。 • 最后,在整个任务流中,很重要的一块就是对整个流程的数据收集。

流程的设计

下面是我们刚才所演示的项目创建流程的架构设计。当我们在调用项目创建模块的时候,首先会通过 Server 接口,去创建 Git 项目。先对整个用户的权限做一层校验,校验通过之后,通过调用 Gitlab API,进行一个仓库的创建,之后,根据所选用的模板信息拉取统一维护的项目模板,根据用户所输的项目名称、项目描述等信息,来生成真正的项目文件,调用 Gitlab API 将整个项目文件推送到创建好的仓库。关于 Gitlab API 的使用这一块,在掘金上有进行过文章的分享,大家感兴趣的话可以去了解一下,这边就不再进行详细的阐述。在我们整个服务端完成了一系列的 Git 创建操作之后,会将创建成功的仓库 url,给到我们的桌面端,桌面端接收到这样创建成功的任务结果之后,开始执行一些本地操作的任务流程。将 Git 仓库克隆到本地的工作区内,同时完成整个项目的依赖安装。在依赖安装之后,我们会借助桌面端的通知能力,包括钉钉的接口去完成通知和反馈。 其中克隆、依赖的安装以及通知反馈是在我们桌面端的主进程内完成的。在我们整个任务流中,它有实时与渲染进程的消息反馈。我们会将整个任务的进度,包括命令执行的日志输出、命令执行结果,通过 IPC 的方式实时的与渲染进行通信,最终在界面上给到用户反馈。在整个流程中,也会对项目数据和流程数据进行存储。

实现这样的整个流程,在实践上有哪些是需要说的呢,下面我们来看一下具体的代码。

npm install 变为 npm.install()

首先在进行命令调用的时候,要将 npm install 这样一个命令行的调用方式变成变为一个函数式的调用,会变为 npm.install() 这样一个调用方式。

git init 变为 git.init()

类似 Git 的命令,也会变成函数式调用

将命令式执行 Promise 化

下面我们看一下,具体场景,如何将命令式调用变成函数式调用。 首先是将命令执行 Promise 化。例如 git init 这样的操作,在执行整个命令的时候,我们更多关心的是整个命令执行的结果,可能不太会关心命令执行过程中的一些输出的内容。这样的话我们就可以通过 Node.js 中的 spawn ,启动子进程来执行命令。通过监听子进程输出来判断我们整个命令的执行状态,然后对整个命令进行 Promise 封装,我们就完成了 git init 这样一个命令行调用变为 git.init() 这样一个异步的函数调用。

实时输出命令执行日志

在另外一个场景,比如说 npm install ,依赖安装,或者说启动本地开发服务,整个命令的执行过程可能会比较长,我们更关注的是过程中实时的日志输出。我们怎么来做呢? 首先我们这边是先创建一个 EventEmitter 实例,作为我们的日志的分发管理,同样的我们也是通过 spwan 来启动一个子进程来执行命令,并且实时的监听子进程的输出,将输出的日志通过 emitter 实例将它分发出去。当我们在主进程中拿到这样的实时日志输出之后,可以通过 Electron 主进程跟渲染进程间的 IPC 的通信,将日志实时的输出到渲染进程当中。

将命令式调用变为函数式调用,有了这样的能力之后,就可以通过对这些函数的调用,进行任务流的编排。例如刚才我们所提到的项目创建,这可能是一个比较通用的流程,还有组件管理、模板管理和以及项目发布等。大家可以根据自己实际的业务需要,来去编排自己一个任务流。

模拟终端:反馈任务进度

上面我们提到的是主进程中对整个命令执行方式的一些改变。那么在我们的渲染进程当中,我们要怎样去实现类似于刚才视频中的终端日志反馈呢?反馈的方式有很多,我们可以通过设计一些任务的步骤条,或者进度条这样的方式来给予整个任务进度的反馈。但是更好的方式是我们可以把任务的进度,包括整个任务输出日志进行一个及时的反馈。这边我们使用的是 xterm.js。它是一个基于 ts 所编写的一个前端终端组件,可以在浏览器内实现终端应用,VsCode 也是基于 xterm.js 来实现的终端的。要如何将主进程的日志来输出到渲染进程当中,就是我们上面所提到的,在拿到一个 EventEmitter 所广播的的输出之后,要通过主进程与渲染进程之间的通信,将数据推送到渲染进程,在渲染进程所需要做的一个处理,把接受到的命令输出,实时的渲染到 xterm 实现的终端组件上面来。

这样的话我们就完成了整个任务流的反馈机制。

以上就是我们在工程化平台中一个任务流的实现,借助于 Electron 能力,我们就可以很方便的实现整个流程任务的编排,以及实现对应流程的界面交互,对整个流程进行简化。除了任务流的实现之外,我们更多需要关注的是整个过程中的数据收集,包括流程数据以及流程中创建的项目、组件数据沉淀,也包括流程当中一些异常数据,因为这些数据是将流程与其他的基础建设能力进行打通的基础,同时也能让我们对整个流程持续的优化,

更新

在完成客户端的开发之后,需要考虑的则是后续的更新,一起来看一下,我们如何实现客户端的自动更新的功能。

Electron 提供了一套比较完善的打包更新机制。

通过 Election-builder 把我们的应用构建之后,会输出一个 latest-mac.yml 文件,以及应用的 zip 包,将这两个文件放到更新服务器上,更新服务器的实现方案有很多,我们选用的是 CDN 来做为更新服务器。 我们如何去设计整个更新的流程,在渲染进程内,一般会提供手动检测更新的触发入口,或者通过轮询任务,来定时进行版本更新检测。 渲染进程发起版本检测求之后,会在渲染进程内调用 autoUpdater 模块,它是 Electron 内置的更新管理模块。 首先需要设置 feedUrl,就是最新的更新包在更新服务端地址。当收到一个渲染进程的版本检测请求之后,调用 checkForUpdates 方法,之后,它会触发下面一系列的一些事件,我们可以通过对整个更新事件的各个生命周期的监听,来完成整个更新流程的把控。

通过 Electron 内置的一个更新机制要面临的问题是更新包体积比较大。因为我们通过 Electron 所构建的桌面端的应用,它将整个 Chromium 进行了集成,就会导致即使我们写了一个很小的 Hello world 这样一个应用,它的体积压缩后也会有 40MB 左右,常规的一个应用来说可能占用 100MB 左右。这样的问题就是有一些比较小的改动的时候,就需要全量的更新,对于用户的一个体验来说并不是很好,对于这些我们有哪些解决方案?首先我们是可以对整个更新的交互设计上做一个优化。我们需要提供的是对整个更新流程的一个进度反馈,另外一点就是我们可以通过 autoUpdater,实现后台的下载。当我们完成了整个更新包的下载之后,然后再通知用户对整个应用进行一个重启,然后更新整个应用,这样的话就才从交互层面上,一定程度的避免了增量更新对用户所体验上的一些影响。当然全量更新还会存在的一个问题,如果用户量比较大的话,就会比较浪费网络资源。

增量发布

下面是我们的一些在增量发布上面的一些实践。首先对整个 Renderer 层的静态资源进行 CDN 托管。对于我们整个应用,不会将 Renderer 层的静态资源打包到最终的桌面端程序内,将资源远程托管,同时我们根据一些特定的业务场景,可以利用 service worker 能力,对整个资源做一个离线缓存,并且对静态资源做版本控制。

更新流程

Renderer 层的一个更新流程是这样的,当页面发请求的时候,首先会匹配本地有没有这样的一个资源的缓存,如果我们匹配到资源,就会返回匹配到的结果。如果说本地没有匹配到的话,就重新请求最新资源,同时将请求的资源进行缓存。如果说在整个请求的过程中出现了错误,需要有一个可使用的默认版本的资源,并且将错误进行上报。 这里我们所实现的一个是基于 UI 层的一个增量更新。实际的业务场景,需要根据不同资源的更新频率,来决定应该是进行更新的体验优化,还是使用 UI 层增量更新,或者安装包的增量更新。

敦煌工程化平台技术架构图

这边是整个应用管理平台的架构,在我们的整个工程实践中,除了实现了对整个项目、组件还有模板的 I2P 全流程托管之外,我们还提供了其他能力,例如团队入口的收敛,包括文档入口,输出入口,同时还将团队内部的一些工具进行一个整合,将一些分散在各个地方的一些工具,比如说文档站点生成工具、图床工具,iconfont 管理工具进行了一个整合,同时我们还对整个客户端的用户行为数据做了采集,通过数据分析来持续迭代。

更多场景

以上我们基于 Electron 的前端工程化平台的实践。当然 Electron 还会有一些其他的应用场景,我们一起来看一下。

首先 VS code,以及支付宝小程序的 IDE,也是基于 Electron 框架实现的

左边是一个接口管理的桌面端工具,为开发过程提供辅助的功能。另外一个 switchhosts,它是一个本地环境管理工具。大家可以看到基于 Electron 开发的桌面端的应用,在我们整个的研发流程中,从我们的本地环境管理、流程管理,开发辅助以及研发编辑阶段都有涉及。工程化管理平台是对整个研发生命周期流程上的串通,但是在实际的编码阶段,其实还是有一个脱离的环节,依然需要依赖 IDE 的能力。基于 Electron 在 IDE 方向的一些可能性,我们未来的一个方向也是,希望将整个 IDE 的本地编码环境与我们的整个研发流程进行一个串通,真正意义上的实现整个研发链路的串通以及效率升级。

当然还有更多的可能性,就是前面提到的 spaceX 这样更大的一个场景~

推荐一本书

下面是我个人所推荐的一本书《少有人走的路》,从书中可以收获的是如何以更成熟的心智去看待所遇到的问题。在成长过程中,限制我们的,更可能是认知以及思维局限性。以什么认知和心态去看待遇到的问题,就会决定会以什么样的反应和什么样的能力去回应这些问题。这本书会让我们更多的去探索,怎样以更成熟的心智去看待所遇到的问题。 希望通过这本书能让大家收获如何更好的面对技术以外的问题,更好的解决问题。

招聘

以上就是我今天的所有的分享,这边是我们团队公众号的二维码,大家可以去了解一下我们团队的一些输出,同时我们近期也是在招聘前端实习生、资深、高级前端工程师。如果有兴趣的话,可以向下面的邮箱进行简历的投递,感谢大家~

QA

请问子洋:如何进行热更新呢?据我了解 Electron 打包出来的页面是放在包内的,如何进行在线更新?

我理解问题应该是 UI 层界面的更新。其实刚才我有提到过,我们对页面的一些静态资源是做了一个 cdn 上的托管,在更新的时候,会有一个检测更新的机制,它可以通过轮询或者服务端推送来实现,当收到静态资源版本更新的通知,通过主进程对渲染进程进行一个忽略缓存的强制刷新,或者说可以通过在主进程有相应的交互,包括升级提醒和更新日志,让用户触发页面重载,去更新 UI 层面的静态资源。

请问子洋:Electron 和 NW.js 的区别能请您对比一下吗?

它们两个最大的区别是在于对 Node.js 和 Chromium 事件循环机制的整合的处理方式是不一样的。首先 NW.js 是通过修改源码的方式,让 Chromium 与 Node.js 的事件循环机制进行打通; Electron 实现的机制是通过启用一个新的安全线程,在 Node.js 和 Chromium 之间做事件转发,这样来实现两者的打通。这样的一个好处就是 Chromium 和 Node.js 的事件循环机制不会有这么强的耦合。另外的区别则是 NW.js 支持 xp 系统,Electron 是不支持的。相比较而言 Electron 有着更活跃的社区,以及更多的大型应用如 VS code、Atom 的实践案例,更多的区别可以参考 Electron 官方的一篇介绍:www.electronjs.org/docs/develo…

请问子洋:更新包的文件是放在私有文件服务器还是 Gitlab 或者 Github 上面?

有比较多方式,我们的实现是通过 CDN 的托管,也可以通过 Github 或者私有文件服务器的搭建来实现。根据自己实际的业务场景和技术栈来选择。

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com

查看原文

赞 5 收藏 2 评论 0