Dont

Dont 查看完整档案

广州编辑暨南大学  |  计算机科学系 编辑xxxx  |  前端 编辑 shellphon.wang/githublog 编辑
编辑

万水千山总是情,提问认真行不行?
天若有情天亦老,回答虚心好不好?(。・・)ノ

个人动态

Dont 赞了文章 · 2020-11-04

JavaScript代理的惊人威力

今天我们要学习的是ECMAScript 6的代理。我们将在本文中涉及以下主题。

  • 什么是代理
  • 代理人在行动
  • 谁使用代理
  • 使用案例和实例
  • 资源

让我们开始吧:)

什么是代理

MDN网站所述。

Proxy对象使你能够为另一个对象创建一个代理,它可以拦截和重新定义该对象的基本操作。

他们在解释什么是代理的时候,说它可以创建一个代理**,这有点搞笑。当然,他们并没有错,但我们可以简化这个说法,使其更加友好,说

_Proxy_对象可以让你包裹目标对象,通过这样做,我们可以拦截和重新定义该对象的基本操作。

基本上,它的意思是我们要把一个对象,用Proxy包裹起来,这将允许我们创建一个 "隐藏 "的门,并控制所有对所需对象的访问。

一个小插曲,Proxy也是一种软件设计模式,你一定要阅读一下(维基百科链接)。

const target = {
  message1: "hello",
  message2: "everyone"
};

const handler = {};

const proxy = new Proxy(target, handler);

一个 "代理 "是用两个参数创建的。

  • "target":你要包裹的原始对象(代理)。
  • handler:定义哪些操作将被拦截,以及如何重新定义被拦截的操作的对象,也可以调用 "陷阱"。

大多数浏览器都支持代理,但也有一些老的浏览器不支持(当然是IE),你可以查看完整的列表这里。google有一个polyfill的Proxies,但它不支持所有的Proxy功能。

现在我们知道了什么是代理,我们想看看我们能用它做什么。

代理人在行动

让我们想象一下,我们是A Bank。我们想知道每次银行账户余额被访问并被通知上。我们将使用最简单的handler操作/陷阱。get

在上面的例子中,我们有一个银行账户对象,里面有我的名字和2020的余额。

const bankAccount = {
  balance: 2020,
  name: 'Georgy Glezer'
};

const handler = {
   get: function(target, prop, receiver) {
    if (prop === 'balance') {
    console.log(`Current Balance Of: ${target.name} Is: ${target.balance} `);
    }

    return target[prop];
  }
};

const wrappedBankAcount = new Proxy(bankAccount, handler);

wrappedBankAcount.balance; // access to the balance

// OUTPUT:
// Current Balance Of: Georgy Glezer Is: 2020
// 2020

这次的处理者对象实现的是get操作/陷阱,它接收一个有3个参数的函数和get的返回值。

  • target。被访问的对象(我们封装的对象)。
  • prop: 被访问的属性。在我们的例子中被访问的属性--这里是 "balance"。
  • receiver:接受者。可以是代理,也可以是继承自代理的对象。

我们定义了一个条件,如果被访问的属性是"_balance"_,我们将通知(log)余额和当前用户名,并返回属性_"balance"_。

从输出中可以看到,一旦 "余额 "属性被访问,我们通过使用代理和设置get操作/陷阱,很容易就通知(log)了这次访问。

我们继续用我们Bank的想法,要求每次有人从银行账户中取钱,我们都要通知一下。而另一个约束条件是,银行不允许出现负余额。为了达到这个目的,我们这次要使用set处理程序/陷阱。

在上面的例子中,我们通知当前的余额和取款后的新余额,如果新的余额为负数,我们也会通知并中止取款操作。

const bankAccount = {
    balance: 2020,
    name: 'Georgy Glezer'
};

const handler = {
    set: function (obj, prop, value) {
        console.log(`Current Balance: ${obj.balance}, New Balance: ${value}`);

        if (value < 0) {
            console.log(`We don't allow Negative Balance!`);
            return false;
        }
        obj[prop] = value;

        return true;
    }
};

const wrappedBankAcount = new Proxy(bankAccount, handler);

wrappedBankAcount.balance -= 2000; // access to the balance
console.log(wrappedBankAcount.balance);

wrappedBankAcount.balance -= 50; // access to the balance
console.log(wrappedBankAcount.balance);

// OUTPUT:
// Current Balance: 2020, New Balance: 20
// 20
// Current Balance: 20, New Balance: -30
// We don't allow Negative Balance!
// 20

我们使用的是set operator/trap,它是一个返回布尔值(true/false)的函数,用来判断更新操作是否成功。它接收以下参数。

  • target: 被访问的对象(我们封装的对象)。
  • prop:正在访问的对象(我们封装的对象)。在我们的例子中,被访问的属性是 "balance"。
  • value。应该更新的新值。
  • receiver*: 原先被分配到的对象。这通常是代理本身。但set()处理程序也可以通过原型链或其他各种方式间接调用。

你可以看到,它其实和get很相似,但只是多接收了1个新值的参数。

这2个操作符/陷阱是最常见的,如果你有兴趣找到所有现有的操作符/陷阱,你可以查看这里

谁使用代理

许多流行的库都使用了这种技术,例如: * [MobX]()

还有更多......他们中的大多数人都利用了Proxies给我们带来的惊人力量,并为我们提供了很棒的库。

使用案例和示例

我们已经看到,我们可以使用代理服务器来进行。

  • 记录(通知银行)
  • 验证(阻止负数余额更新)

缓存

我们将再次使用get操作符/陷阱,并将 "dollars "属性添加到我们的对象中。在每次访问 "dollars "属性时,我们将计算我们的余额价值多少美元。因为计算是一个繁重的操作,所以我们希望尽可能的Cache它。

const bankAccount = {
    balance: 10,
    name: 'Georgy Glezer',
    get dollars() {
        console.log('Calculating Dollars');
        return this.balance *3.43008459;
    }
};

let cache = {
    currentBalance: null,
    currentValue: null
};

const handler = {
    get: function (obj, prop) {
        if (prop === 'dollars') {
            let value = cache.currentBalance !== obj.balance ? obj[prop] : cache.currentValue;

            cache.currentValue = value;
            cache.currentBalance = obj.balance;

            return value;
        }

        return obj[prop];
    }
};

const wrappedBankAcount = new Proxy(bankAccount, handler);

console.log(wrappedBankAcount.dollars);
console.log(wrappedBankAcount.dollars);
console.log(wrappedBankAcount.dollars);
console.log(wrappedBankAcount.dollars);

// OUTPUT:
// Calculating Dollars
// 34.3008459
// 34.3008459
// 34.3008459
// 34.3008459

正如你在例子中所看到的,我们有一个缓存对象,它保存着当前的银行余额和以美元为单位的余额价值。每次有人访问"_dollars"_属性时,我们都会先进行计算,然后将其缓存起来。

DOM操作

我们想在每次余额发生变化时更新屏幕上的文字。我们将使用一个set操作符/陷阱,在每次改变数值时,我们将更新屏幕上的DOM元素。

const bankAccount = {
  balance: 2020,
  name: "Georgy Glezer",
  get text() {
    return `${this.name} Balance Is: ${this.balance}`;
  }
};

const objectWithDom = (object, domId) => {
  const handler = {
    set: function (obj, prop, value) {
      obj[prop] = value;

      document.getElementById(domId).innerHTML = obj.text;

      return true;
    }
  };

  return new Proxy(object, handler);
};

// create a dom element with id: bank-account
const wrappedBankAccount = objectWithDom(bankAccount, "bank-account");

wrappedBankAccount.balance = 26;
wrappedBankAccount.balance = 100000;

在这里,我们创建了一个辅助函数,这样我们就可以存储DOM元素的ID,并在set operator/trap中添加了简单的行来更新DOM元素。很简单,对吧?让我们看看结果:)

Image for post

概要

综上所述,我们了解了ECMAScript 6 Proxies,我们如何使用它们,以及用于什么目的。在我看来,Proxies是一个很神奇的工具,你可以用它来做各种各样的选择,你只需要想想什么最适合你:) 。

如果您喜欢这篇文章,欢迎关注并拍手👏。

资源简介

查看原文

赞 12 收藏 8 评论 0

Dont 赞了文章 · 2020-10-14

vue源码中值得学习的方法

最近在深入研究vue源码,把学习过程中,看到的一些好玩的的函数方法收集起来做分享,希望对大家对深入学习js有所帮助。如果大家都能一眼看懂这些函数,说明技术还是不错的哦。

1. 数据类型判断

Object.prototype.toString.call()返回的数据格式为 [object Object]类型,然后用slice截取第8位到倒一位,得到结果为 Object

var _toString = Object.prototype.toString;
function toRawType (value) {
  return _toString.call(value).slice(8, -1)
}

运行结果测试

toRawType({}) //  Object 
toRawType([])  // Array    
toRawType(true) // Boolean
toRawType(undefined) // Undefined
toRawType(null) // Null
toRawType(function(){}) // Function

2. 利用闭包构造map缓存数据

vue中判断我们写的组件名是不是html内置标签的时候,如果用数组类遍历那么将要循环很多次获取结果,如果把数组转为对象,把标签名设置为对象的key,那么不用依次遍历查找,只需要查找一次就能获取结果,提高了查找效率。

function makeMap (str, expectsLowerCase) {
    // 构建闭包集合map
    var map = Object.create(null);
    var list = str.split(',');
    for (var i = 0; i < list.length; i++) {
      map[list[i]] = true;
    }
    return expectsLowerCase
      ? function (val) { return map[val.toLowerCase()]; }
      : function (val) { return map[val]; }
}
// 利用闭包,每次判断是否是内置标签只需调用isHTMLTag
var isHTMLTag = makeMap('html,body,base,head,link,meta,style,title')
console.log('res', isHTMLTag('body')) // true

3. 二维数组扁平化

vue中_createElement格式化传入的children的时候用到了simpleNormalizeChildren函数,原来是为了拍平数组,使二维数组扁平化,类似lodash中的flatten方法。

// 先看lodash中的flatten
_.flatten([1, [2, [3, [4]], 5]])
// 得到结果为  [1, 2, [3, [4]], 5]

// vue中
function simpleNormalizeChildren (children) {
  for (var i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}

// es6中 等价于
function simpleNormalizeChildren (children) {
   return [].concat(...children)
}

4. 方法拦截

vue中利用Object.defineProperty收集依赖,从而触发更新视图,但是数组却无法监测到数据的变化,但是为什么数组在使用pushpop等方法的时候可以触发页面更新呢,那是因为vue内部拦截了这些方法。

 // 重写push等方法,然后再把原型指回原方法
  var ARRAY_METHOD = [ 'push', 'pop', 'shift', 'unshift', 'reverse',  'sort', 'splice' ];
  var array_methods = Object.create(Array.prototype);
  ARRAY_METHOD.forEach(method => {
    array_methods[method] = function () {
      // 拦截方法
      console.log('调用的是拦截的 ' + method + ' 方法,进行依赖收集');
      return Array.prototype[method].apply(this, arguments);
    }
  });

运行结果测试

var arr = [1,2,3]
arr.__proto__ = array_methods // 改变arr的原型
arr.unshift(6) // 打印结果: 调用的是拦截的 unshift 方法,进行依赖收集

5. 继承的实现

vue中调用Vue.extend实例化组件,Vue.extend就是VueComponent构造函数,而VueComponent利用Object.create继承Vue,所以在平常开发中VueVue.extend区别不是很大。这边主要学习用es5原生方法实现继承的,当然了,es6中 class类直接用extends继承。

  // 继承方法 
  function inheritPrototype(Son, Father) {
    var prototype = Object.create(Father.prototype)
    prototype.constructor = Son
    // 把Father.prototype赋值给 Son.prototype
    Son.prototype = prototype
  }
  function Father(name) {
    this.name = name
    this.arr = [1,2,3]
  }
  Father.prototype.getName = function() {
    console.log(this.name)
  }
  function Son(name, age) {
    Father.call(this, name)
    this.age = age
  }
  inheritPrototype(Son, Father)
  Son.prototype.getAge = function() {
    console.log(this.age)
  }

运行结果测试

var son1 = new Son("AAA", 23)
son1.getName()            //AAA
son1.getAge()             //23
son1.arr.push(4)          
console.log(son1.arr)     //1,2,3,4

var son2 = new Son("BBB", 24)
son2.getName()            //BBB
son2.getAge()             //24
console.log(son2.arr)     //1,2,3

6. 执行一次

once 方法相对比较简单,直接利用闭包实现就好了

function once (fn) {
  var called = false;
  return function () {
    if (!called) {
      called = true;
      fn.apply(this, arguments);
    }
  }
}

7. 对象判断

vue源码中的looseEqual 判断两个对象是否相等,先类型判断再递归调用,总体也不难,学一下思路。

function looseEqual (a, b) {
  if (a === b) { return true }
  var isObjectA = isObject(a);
  var isObjectB = isObject(b);
  if (isObjectA && isObjectB) {
    try {
      var isArrayA = Array.isArray(a);
      var isArrayB = Array.isArray(b);
      if (isArrayA && isArrayB) {
        return a.length === b.length && a.every(function (e, i) {
          return looseEqual(e, b[i])
        })
      } else if (!isArrayA && !isArrayB) {
        var keysA = Object.keys(a);
        var keysB = Object.keys(b);
        return keysA.length === keysB.length && keysA.every(function (key) {
          return looseEqual(a[key], b[key])
        })
      } else {
        /* istanbul ignore next */
        return false
      }
    } catch (e) {
      /* istanbul ignore next */
      return false
    }
  } else if (!isObjectA && !isObjectB) {
    return String(a) === String(b)
  } else {
    return false
  }
}
function isObject (obj) {
  return obj !== null && typeof obj === 'object'
}

由上思路我自己写一个深拷贝方法,不过简单的深拷贝我们可以用 JSON.stringify() 来实现即可。

function deepClone(source) {
  if (!source && typeof source !== 'object') {
    throw new Error('error arguments', 'deepClone')
  }
  const targetObj = source.constructor === Array ? [] : {}
  Object.keys(source).forEach(keys => {
    if (source[keys] && typeof source[keys] === 'object') {
      targetObj[keys] = deepClone(source[keys])
    } else {
      targetObj[keys] = source[keys]
    }
  })
  return targetObj
}

就先分享这些函数,其他函数,后面继续补充,如有不对欢迎指正,谢谢!

查看原文

赞 43 收藏 29 评论 8

Dont 收藏了文章 · 2020-10-12

2020年中大厂前端面试总结

前言

本次面试面试了很多家公司,包括 360,美团,猿辅导,小米,腾讯地图,头条,新东方,快手,知乎等几家公司,刚开始去面试的时候那段时间状态不是很好(基本每天都加班到很晚,周六日也没有休息的那种,而且当时心态真的是差到爆,很多平时自己很会的东西,被问到居然答不上来),基本一面就挂的那种(360,美团,猿辅导),越面越失望,后来就直接不面试了,调整自己的状态,请假休息,好好睡了两天两夜之后,调整自己的心态,开始准备面试,接下来的面试就顺利的很多。

本篇面试题总结并没有按照公司那样分类而是按照知识点进行简单分类,很多面试题问的频率非常高,所以面试的时候如果第一次问完,没回答上来或者回答的不太好,一定要在面完的第一时间记录下来并且查找资料,否则就忘记了,或者之后再看就没有了当时迫切想知道具体答案的那种心情了(有迫切的想知道某些知识的心情的时候目标很明确,学东西也会印象深刻记得牢)。

本文链接地址较多,建议查看原文,阅读体验会好一些。下面给出的答案有的是自己总结的,有的是从网上找到写的很不错的相关文章,但是这些都仅供参考,不一定是最佳的答案,如果有很好的答案,欢迎留言一起讨论互相学习,有的还没有放上合适的链接,之后会不算补充进去,毕竟每道题涉及到的内容真的挺多的。

下面题目中标记有 【高频】 的至少被问过两次,标记有 【超高频】 的基本面试的每家公司都问到了。

笔试题

  1. 【超高频】 写一个深拷贝,考虑 正则,Date这种类型的数据
  2. 【高频】 Vue自定义指令懒加载
  1. 判断DOM标签的合法性,标签的闭合,span里面不能有div,写一个匹配DOM标签的正则
  1. 替换日期格式,xxxx-yy-zz 替换成 xxx-zz-yy

可以使用 正则的捕获组来实现

var reg = /(\d{2})\.(\d{2})\/(\d{4})/
var data = '10.24/2017'
data = data.replace(reg, '$3-$1-$2')
console.log(data)//2017-10-24
  1. 【高频】 实现Promise.all, Promise.allSettled
  2. 获取一段DOM节点中标签个数最多的标签
  1. 写一个简单的diff
  1. 【高频】 手写节流
  1. 手写ES6的继承
  2. 实现一个自定义hook - usePrevious
import { useRef } from 'react';

export type compareFunction<T> = (prev: T | undefined, next: T) => boolean;

export default <T>(state: T, compare?: compareFunction<T>): T | undefined => {
  const prevRef = useRef<T>();
  const curRef = useRef<T>();

  const needUpdate = typeof compare === 'function' ? compare(curRef.current, state) : true;
  if (needUpdate) {
    prevRef.current = curRef.current;
    curRef.current = state;
  }

  return prevRef.current;
};
更多自定义hook的写法可以参考 hooks
  1. 【高频】 实现一个vue的双向绑定
其他题目的答案之前做了整理,可以在 前端学习总结-手写代码系列中看到

笔试题中的算法题

  1. 二叉树的最大深度
  1. 另一个树的子树
  1. 相同的树
  1. 翻转二叉树
  1. 【高频】 斐波那契数列
  1. 【高频】 合并两个有序数组
  1. 【高频】 打乱数组
  1. 数组区间

webpack 和babel相关的问题

  1. babel的缓存是怎么实现的
  2. webapck的HMR,怎么配置:

    • 浏览器是如何更新的
    • 如何做到页面不刷新也就就自动更新的
    • webpack-dev-server webapck-dev-middleware
相关文章:Webpack Hot Module Replacement 的原理解析
  1. 自己有没有写过ast, webpack通过什么把公共的部分抽出来的,属性配置是什么
  2. webpack怎么配置mock转发代理,mock的服务,怎么拦截转换的
  3. webapck的plugin和loader的编写, webapck plugin和loader的顺序
  4. webpack的打包构建优化,体积和速度
  5. DLLPlugin原理,为什么不直接使用压缩版本的js

HTTP

  1. 【超高频】 缓存(强缓存),如何设置缓存
  1. 【高频】 HTTP2, HTTP2的性能优化方面,真的优化很多么?
  2. 【高频】 简单请求和复杂请求
  1. 【高频】 HTTPS的整个详细过程
  1. 301和302的区别
  1. 怎么用get实现post,就是使用get方法但是参数放到request body中
  2. TCP和UDP的区别
更多可以查看 【面试题】HTTP知识点整理(附答案)

CSS

  1. 【超高频】 flex相关的问题

    • 说一下flex
    • flex: 1具体代表什么, 有什么应用场景
    • flex-basic 是什么含义

相关文章:Flex 布局教程:语法篇

  1. css var 自定义变量的兼容性
  2. 行内元素和块级元素的区别
  3. position有哪些值,分别是什么含义
  4. 盒模型
  5. CSS的实现

  6. 【高频】 实现固定宽高比(width: height = 4: 3)的div,怎么设置
  7. 【高频】 伪类和伪元素
更多可以查看【面试题】CSS知识点整理(附答案)

JavaScript

  1. 单例的应用
  2. 【超高频】 什么是闭包,闭包的应用场景
  1. 如何判断 当前浏览器是否支持webp
  2. proxy除了拦截它的getter和setter外,还能做什么
  3. 同步阻塞,异步非阻塞
  4. 弱引用,WeakMap和Map的区别
  5. 【高频】 安全相关 XSS的反射型是什么,怎么避免
  1. 【超高频】 事件循环
  1. 【超高频】 promise相关的问题, 说一下你对Promise的了解
  1. 【超高频】 浏览器渲染(从输入url到页面渲染的完成过程)
  2. 【超高频】 首屏加载优化, 通过哪些指标去衡量性能优化的
  3. canvas和svg分别是干什么的
  1. 牛客网如何监听你调到了其他页面

document.hidden,监听 docuemnt.vibibleChange事件

document.addEventListener("visibilitychange", function() {
  console.log( document.hidden );
});
  1. JS原生3种绑定事件
// 1. 在标签中直接绑定
<button onclick="handleClick()" >自定义函数</button>

// 2. 在js事件中获取dom事件绑定

<button id="btn" onclick="handleClick()" >dom事件绑定</button>
document.getElementById('btn').onclick=handleClick();

// 3. 事件监听addEventListener
elementDOM.addEventListener(eventName,handle,useCapture);
  1. 简单说一下你对 websocket 了解多少?
  1. 实现复杂数据(去重元素是对象,数组)的数组去重 (* 3)
  2. 基本数据类型有哪些, 为什么symbol是一个函数, BigInt为什么可以用来存储大整数
  3. 什么是依赖注入
  4. JS类型转换

    • String([])’‘String({})结果是什么什么? 答案是:'[object object]'
    • 其他一些很经典的类型转换考察,当时没记那么清楚,大家可以去网上看一下
  5. 富文本编辑器相关的js知识
  1. cli工具的一些实现逻辑

Vue

  1. 【高频】 vue3.0的新特性,了解compose api和react hooks的区别
  2. new Vue做了什么
  3. 双向绑定原理
  4. vue组件通信方法

React

  1. 【高频】 React hooks 相关的问题
  • 为什么引入,什么原理
  • hooks如何监听响应的,内部是如何做到只有数据修改的时候才执行函数
  • 依赖的值发生变化,需要不停地监听和绑定事件
  • render props 和HOC相比的优缺点
  • 和mixin,hoc区别在哪儿
  1. 创建ref的几种方法
  2. context怎么使用,内部原理怎么做到的
  3. 【超高频】 React新的生命周期,为什么 getDrivedStatefromProps是静态的
  4. react中TS的声明
  5. redux相关的问题
  • redux使用方法,为什么action要返回一个函数,返回一个对象可以么
  • state为什么要设计成不可变的
相关文章 为什么redux要返回一个新的state引发的血案阮一峰-Redux 入门教程(一):基本用法
  1. 【高频】 diff算法
  2. 【高频】 key的作用
  3. immer和imutable的区别
  4. 【高频】 react性能优化, fiber架构
更多可以查看 【面试题】React知识点整理(附答案)

面试结果

大概说一下本人的大概情况,本科三年左右工作经验,非计算机专业,大三下学习决定转行学习前端,过程反正挺艰辛的,一直到现在还在恶补计算机的一些知识。毕业半年左右,一个偶然的机会,进入阿里文娱(哈哈,当时面试的时候也写过面经,感兴趣的可以看一下 当时写的面经 2017面末面试总结),现在因为个人原因,决定考虑新的机会。

面试差不多最开始是中3月中旬开始准备的,中间停了差不多小一个月又开始重新面试的,到最后拿到offer差不多5月底左右,历时近3个月吧,最近抽时间把这些题目总结了一下,算是给自己一个交代吧,上面很多题目自己回答的其实很多都不是很全面,标有 【高频】【超高频】 刚开始回答的不好,后来认真学习总结了一下,之后再被问到,基本都回答得差不多

一般提到面试,肯定都会想问一下面试结果,我就大概的说一下面试结果,哈哈,其实不太想说,因为挺惨的,并没有像很多大佬一样“已拿字节阿里腾讯各大厂offer”,但是毕竟是自己的经历,无论结果如何都要坦然接受,之前没好好学习,那之后多学习就是。360,美团,猿辅导最开始的一面挂,小米二面的时候面试官告知说要求招5年以上工作经验的,所以就直接告知不符合(哈哈,可能就是跟小米没有缘分吧,刚毕业的时候面试,终面被拒说要3年以上工作经验的,现在够3年工作经验了,却又要求5年工作年限),腾讯地图和头条都是hr直接找过来的,自己并没有投递,就顺便面了一下,二面面完之后,以为挂了,后来过了一周多(可能是作为备胎把),又打电话过来约面试,其实之前面试大概了解了一下部门相关的情况,感觉不是自己想去的,并不是说部门不好,可能做的事情跟现在的情况太像了,所以想做出一些改变。当时家里面又有好多事情处理,也没有太多的时间,就直接拒绝了,这件事儿也给自己以后提个醒,投简历之前要先想明白自己想要什么样的,可以列一些目标,而不是因为急于找工作,猎头和hr直接打电话过来就直接面试。

心得

面试公司的选择

本次面试有几家公司(腾讯地图,头条,360教育,新东方等)全部都是猎头和hr直接打电话过来让面试的,当时就抱着试试的态度,就直接面试了,面试的过程中感觉可能都不太合适(所以面试的时候要问一下公司部门的具体工作内容),换工作的时候尽量找相关部门的人内推,首先内推的部门你肯定会提前有所了解,而且还可以帮忙看看进度啥的,面试过了说不定还能成为好朋友,哈哈(所以平时要多结交一些大佬,一般大佬的人脉都很广泛,而且他们很可以给你内推,甚至可以把他们自己的经验分享给你)。

总是要想好自己现在出现什么问题了,为什么打算离职,下一份工作想要什么样儿的,毕竟一份工作要干很长时间。

面试准备

推荐一些很好的文章:

好文章真的太多了,哈哈,这里就不全部放出来了,关于面试,我也准备做了一些总结,可以查看 个人博客

算法

基本每家公司多多少少都会问很多算法题,算法题对于我这种基本没什么基础的人来说,碰到了就很恐惧,但是没有其他的办法,就是两个字 “多练”,这里推荐我看过的几篇文章:

其他的一些想法,之前也写了一篇文章 关于面试的一点心得,感兴趣的也可以看一下。也非常欢迎大家关注我的公众号 【牧码的星星】以及加我微信进行交流,公众号也会偶尔分享一些学习的一些心得。

查看原文

Dont 赞了文章 · 2020-10-12

2020年中大厂前端面试总结

前言

本次面试面试了很多家公司,包括 360,美团,猿辅导,小米,腾讯地图,头条,新东方,快手,知乎等几家公司,刚开始去面试的时候那段时间状态不是很好(基本每天都加班到很晚,周六日也没有休息的那种,而且当时心态真的是差到爆,很多平时自己很会的东西,被问到居然答不上来),基本一面就挂的那种(360,美团,猿辅导),越面越失望,后来就直接不面试了,调整自己的状态,请假休息,好好睡了两天两夜之后,调整自己的心态,开始准备面试,接下来的面试就顺利的很多。

本篇面试题总结并没有按照公司那样分类而是按照知识点进行简单分类,很多面试题问的频率非常高,所以面试的时候如果第一次问完,没回答上来或者回答的不太好,一定要在面完的第一时间记录下来并且查找资料,否则就忘记了,或者之后再看就没有了当时迫切想知道具体答案的那种心情了(有迫切的想知道某些知识的心情的时候目标很明确,学东西也会印象深刻记得牢)。

本文链接地址较多,建议查看原文,阅读体验会好一些。下面给出的答案有的是自己总结的,有的是从网上找到写的很不错的相关文章,但是这些都仅供参考,不一定是最佳的答案,如果有很好的答案,欢迎留言一起讨论互相学习,有的还没有放上合适的链接,之后会不算补充进去,毕竟每道题涉及到的内容真的挺多的。

下面题目中标记有 【高频】 的至少被问过两次,标记有 【超高频】 的基本面试的每家公司都问到了。

笔试题

  1. 【超高频】 写一个深拷贝,考虑 正则,Date这种类型的数据
  2. 【高频】 Vue自定义指令懒加载
  1. 判断DOM标签的合法性,标签的闭合,span里面不能有div,写一个匹配DOM标签的正则
  1. 替换日期格式,xxxx-yy-zz 替换成 xxx-zz-yy

可以使用 正则的捕获组来实现

var reg = /(\d{2})\.(\d{2})\/(\d{4})/
var data = '10.24/2017'
data = data.replace(reg, '$3-$1-$2')
console.log(data)//2017-10-24
  1. 【高频】 实现Promise.all, Promise.allSettled
  2. 获取一段DOM节点中标签个数最多的标签
  1. 写一个简单的diff
  1. 【高频】 手写节流
  1. 手写ES6的继承
  2. 实现一个自定义hook - usePrevious
import { useRef } from 'react';

export type compareFunction<T> = (prev: T | undefined, next: T) => boolean;

export default <T>(state: T, compare?: compareFunction<T>): T | undefined => {
  const prevRef = useRef<T>();
  const curRef = useRef<T>();

  const needUpdate = typeof compare === 'function' ? compare(curRef.current, state) : true;
  if (needUpdate) {
    prevRef.current = curRef.current;
    curRef.current = state;
  }

  return prevRef.current;
};
更多自定义hook的写法可以参考 hooks
  1. 【高频】 实现一个vue的双向绑定
其他题目的答案之前做了整理,可以在 前端学习总结-手写代码系列中看到

笔试题中的算法题

  1. 二叉树的最大深度
  1. 另一个树的子树
  1. 相同的树
  1. 翻转二叉树
  1. 【高频】 斐波那契数列
  1. 【高频】 合并两个有序数组
  1. 【高频】 打乱数组
  1. 数组区间

webpack 和babel相关的问题

  1. babel的缓存是怎么实现的
  2. webapck的HMR,怎么配置:

    • 浏览器是如何更新的
    • 如何做到页面不刷新也就就自动更新的
    • webpack-dev-server webapck-dev-middleware
相关文章:Webpack Hot Module Replacement 的原理解析
  1. 自己有没有写过ast, webpack通过什么把公共的部分抽出来的,属性配置是什么
  2. webpack怎么配置mock转发代理,mock的服务,怎么拦截转换的
  3. webapck的plugin和loader的编写, webapck plugin和loader的顺序
  4. webpack的打包构建优化,体积和速度
  5. DLLPlugin原理,为什么不直接使用压缩版本的js

HTTP

  1. 【超高频】 缓存(强缓存),如何设置缓存
  1. 【高频】 HTTP2, HTTP2的性能优化方面,真的优化很多么?
  2. 【高频】 简单请求和复杂请求
  1. 【高频】 HTTPS的整个详细过程
  1. 301和302的区别
  1. 怎么用get实现post,就是使用get方法但是参数放到request body中
  2. TCP和UDP的区别
更多可以查看 【面试题】HTTP知识点整理(附答案)

CSS

  1. 【超高频】 flex相关的问题

    • 说一下flex
    • flex: 1具体代表什么, 有什么应用场景
    • flex-basic 是什么含义

相关文章:Flex 布局教程:语法篇

  1. css var 自定义变量的兼容性
  2. 行内元素和块级元素的区别
  3. position有哪些值,分别是什么含义
  4. 盒模型
  5. CSS的实现

  6. 【高频】 实现固定宽高比(width: height = 4: 3)的div,怎么设置
  7. 【高频】 伪类和伪元素
更多可以查看【面试题】CSS知识点整理(附答案)

JavaScript

  1. 单例的应用
  2. 【超高频】 什么是闭包,闭包的应用场景
  1. 如何判断 当前浏览器是否支持webp
  2. proxy除了拦截它的getter和setter外,还能做什么
  3. 同步阻塞,异步非阻塞
  4. 弱引用,WeakMap和Map的区别
  5. 【高频】 安全相关 XSS的反射型是什么,怎么避免
  1. 【超高频】 事件循环
  1. 【超高频】 promise相关的问题, 说一下你对Promise的了解
  1. 【超高频】 浏览器渲染(从输入url到页面渲染的完成过程)
  2. 【超高频】 首屏加载优化, 通过哪些指标去衡量性能优化的
  3. canvas和svg分别是干什么的
  1. 牛客网如何监听你调到了其他页面

document.hidden,监听 docuemnt.vibibleChange事件

document.addEventListener("visibilitychange", function() {
  console.log( document.hidden );
});
  1. JS原生3种绑定事件
// 1. 在标签中直接绑定
<button onclick="handleClick()" >自定义函数</button>

// 2. 在js事件中获取dom事件绑定

<button id="btn" onclick="handleClick()" >dom事件绑定</button>
document.getElementById('btn').onclick=handleClick();

// 3. 事件监听addEventListener
elementDOM.addEventListener(eventName,handle,useCapture);
  1. 简单说一下你对 websocket 了解多少?
  1. 实现复杂数据(去重元素是对象,数组)的数组去重 (* 3)
  2. 基本数据类型有哪些, 为什么symbol是一个函数, BigInt为什么可以用来存储大整数
  3. 什么是依赖注入
  4. JS类型转换

    • String([])’‘String({})结果是什么什么? 答案是:'[object object]'
    • 其他一些很经典的类型转换考察,当时没记那么清楚,大家可以去网上看一下
  5. 富文本编辑器相关的js知识
  1. cli工具的一些实现逻辑

Vue

  1. 【高频】 vue3.0的新特性,了解compose api和react hooks的区别
  2. new Vue做了什么
  3. 双向绑定原理
  4. vue组件通信方法

React

  1. 【高频】 React hooks 相关的问题
  • 为什么引入,什么原理
  • hooks如何监听响应的,内部是如何做到只有数据修改的时候才执行函数
  • 依赖的值发生变化,需要不停地监听和绑定事件
  • render props 和HOC相比的优缺点
  • 和mixin,hoc区别在哪儿
  1. 创建ref的几种方法
  2. context怎么使用,内部原理怎么做到的
  3. 【超高频】 React新的生命周期,为什么 getDrivedStatefromProps是静态的
  4. react中TS的声明
  5. redux相关的问题
  • redux使用方法,为什么action要返回一个函数,返回一个对象可以么
  • state为什么要设计成不可变的
相关文章 为什么redux要返回一个新的state引发的血案阮一峰-Redux 入门教程(一):基本用法
  1. 【高频】 diff算法
  2. 【高频】 key的作用
  3. immer和imutable的区别
  4. 【高频】 react性能优化, fiber架构
更多可以查看 【面试题】React知识点整理(附答案)

面试结果

大概说一下本人的大概情况,本科三年左右工作经验,非计算机专业,大三下学习决定转行学习前端,过程反正挺艰辛的,一直到现在还在恶补计算机的一些知识。毕业半年左右,一个偶然的机会,进入阿里文娱(哈哈,当时面试的时候也写过面经,感兴趣的可以看一下 当时写的面经 2017面末面试总结),现在因为个人原因,决定考虑新的机会。

面试差不多最开始是中3月中旬开始准备的,中间停了差不多小一个月又开始重新面试的,到最后拿到offer差不多5月底左右,历时近3个月吧,最近抽时间把这些题目总结了一下,算是给自己一个交代吧,上面很多题目自己回答的其实很多都不是很全面,标有 【高频】【超高频】 刚开始回答的不好,后来认真学习总结了一下,之后再被问到,基本都回答得差不多

一般提到面试,肯定都会想问一下面试结果,我就大概的说一下面试结果,哈哈,其实不太想说,因为挺惨的,并没有像很多大佬一样“已拿字节阿里腾讯各大厂offer”,但是毕竟是自己的经历,无论结果如何都要坦然接受,之前没好好学习,那之后多学习就是。360,美团,猿辅导最开始的一面挂,小米二面的时候面试官告知说要求招5年以上工作经验的,所以就直接告知不符合(哈哈,可能就是跟小米没有缘分吧,刚毕业的时候面试,终面被拒说要3年以上工作经验的,现在够3年工作经验了,却又要求5年工作年限),腾讯地图和头条都是hr直接找过来的,自己并没有投递,就顺便面了一下,二面面完之后,以为挂了,后来过了一周多(可能是作为备胎把),又打电话过来约面试,其实之前面试大概了解了一下部门相关的情况,感觉不是自己想去的,并不是说部门不好,可能做的事情跟现在的情况太像了,所以想做出一些改变。当时家里面又有好多事情处理,也没有太多的时间,就直接拒绝了,这件事儿也给自己以后提个醒,投简历之前要先想明白自己想要什么样的,可以列一些目标,而不是因为急于找工作,猎头和hr直接打电话过来就直接面试。

心得

面试公司的选择

本次面试有几家公司(腾讯地图,头条,360教育,新东方等)全部都是猎头和hr直接打电话过来让面试的,当时就抱着试试的态度,就直接面试了,面试的过程中感觉可能都不太合适(所以面试的时候要问一下公司部门的具体工作内容),换工作的时候尽量找相关部门的人内推,首先内推的部门你肯定会提前有所了解,而且还可以帮忙看看进度啥的,面试过了说不定还能成为好朋友,哈哈(所以平时要多结交一些大佬,一般大佬的人脉都很广泛,而且他们很可以给你内推,甚至可以把他们自己的经验分享给你)。

总是要想好自己现在出现什么问题了,为什么打算离职,下一份工作想要什么样儿的,毕竟一份工作要干很长时间。

面试准备

推荐一些很好的文章:

好文章真的太多了,哈哈,这里就不全部放出来了,关于面试,我也准备做了一些总结,可以查看 个人博客

算法

基本每家公司多多少少都会问很多算法题,算法题对于我这种基本没什么基础的人来说,碰到了就很恐惧,但是没有其他的办法,就是两个字 “多练”,这里推荐我看过的几篇文章:

其他的一些想法,之前也写了一篇文章 关于面试的一点心得,感兴趣的也可以看一下。也非常欢迎大家关注我的公众号 【牧码的星星】以及加我微信进行交流,公众号也会偶尔分享一些学习的一些心得。

查看原文

赞 149 收藏 124 评论 12

Dont 赞了文章 · 2020-10-10

【已结束】SegmentFault 思否程序员大调查丨填问卷,领限量周边

image

“程序员大调查” 填写调查问卷领取 SegmentFault 思否周边

哪里填写调查问卷?


戳链接:https://jinshuju.net/f/S5BJR7

怎么抽奖?


只需要两步:

第一步:先填写调查问卷

第二步:停止填写问卷后,我们的程序员小哥哥会写一个抽奖系统,在填写问卷的用户里,随机抽取 10 个开发者

注意:填写问卷时记得填写微信号或者手机号,中奖后,会有小姐姐联系你哦

什么样的周边?


SegmentFault 思否 定制马克杯 x4
SegmentFault 思否 定制鼠标垫 x2
SegmentFault 思否 定制雨伞 x2
SegmentFault 思否 定制背包 x2

image

填写问卷截止时间?


现在 — 2020年10月21日

就现在,填问卷吧!

中奖名单

恭喜以下小伙伴~ 小姐姐已经联系你们咯~

182**2798
135**2116
w*an
188**0785
m*gt
184**8420
pi*007
156**1538
158**5084
ya**1984

最终解释权归思否编程所有
查看原文

赞 16 收藏 0 评论 33

Dont 收藏了文章 · 2020-09-09

在原生项目中集成Flutter

概述

使用Flutter从零开始开发App是一件轻松惬意的事情,但对于一些成熟的产品来说,完全摒弃原有App的历史沉淀,全面转向Flutter是不现实的。因此使用Flutter去统一Android、iOS技术栈,把它作为已有原生App的扩展能力,通过有序推进来提升移动终端的开发效率。
目前,想要在已有的原生App里嵌入一些Flutter页面主要有两种方案。一种是将原生工程作为Flutter工程的子工程,由Flutter进行统一管理,这种模式称为统一管理模式。另一种是将Flutter工程作为原生工程的子模块,维持原有的原生工程管理方式不变,这种模式被称为三端分离模式,如下图所示。
在这里插入图片描述
三端代码分离模式的原理是把Flutter模块作为原生工程的子模块,从而快速地接入Flutter模块,降低原生工程的改造成本。在Flutter 1.1x时代,在原生已有app中接入Flutter的步骤比较繁琐,具体可以可以参考:Flutter与原生混合开发
不过,从Flutter 1.20.x版本开始,Flutter对原生app接入Flutter进行了优化和升级,下面是具体介绍。

原生Android集成Flutter

支持的特性

  • 在 Gradle 脚本中添加一个自动构建并引入 Flutter 模块的 Flutter SDK 钩子。
  • 将 Flutter 模块构建为通用的 Android Archive (AAR) 以便集成到您自己的构建系统中,并提高 Jetifier 与 AndroidX 的互操作性;
  • FlutterEngine API 用于启动并持续地为挂载 FlutterActivity 或 FlutterFragment 提供独立的 Flutter 环境;
  • Android Studio 的 Android 与 Flutter 同时编辑,以及 Flutter module 创建与导入向导;
  • 支持Java 和 Kotlin 为宿主的应用程序;

集成Flutter

首先,我们来看一下最终的效果,如下图所示。
在这里插入图片描述

集成Flutter主要有两种方式,一种是使用Android Studio工具的方式,另一种是使用手动的方式。

使用Android Studio方式

直接使用 Android Studio 是在现有应用中自动集成 Flutter 模块比较便捷的方法。在 Android Studio 打开现有的 Android 原生项目,然后依次点击菜单按钮 File > New > New Module…创建出一个可以集成的新 Flutter 模块,或者选择导入已有的 Flutter 模块,如下图所示。
在这里插入图片描述
选择Module的类型为Flutter Module,然后在向导窗口中填写模块名称、路径等信息,如下图所示。
在这里插入图片描述

此时,Android Studio 插件就会自动为这个 Android 项目配置添加 Flutter 模块作为依赖项,这时集成应用就已准备好进行下一步的构建。

手动集成

如果想要在不使用 Flutter 的 Android Studio 插件的情况下手动将 Flutter 模块与现有的 Android 应用集成,可以使用下面的步骤。

假设我们的原生应用在 some/path/MyApp 路径下,那么在Flutter 项目的同级目录下新建一个Flutter模块,命令如下。

cd some/path/
flutter create -t module --org com.example my_flutter

完成上面的命令后,会在 some/path/my_flutter/ 目录下创建一个 Flutter 模块项目。该模块项目会包含一些 Dart 代码和一些一个隐藏的子文件夹 .android/,.android 文件夹包含一个 Android 项目,该项目不仅可以帮助你通过 flutter run 运行这个 Flutter 模块的独立应用,而且还可以作为封装程序来帮助引导 Flutter 模块作为可嵌入的 Android 库。

同时,由于Flutter Android 引擎需要使用到 Java 8 中的新特性。因此,需要在宿主 Android 应用的 build.gradle 文件的 android { } 块中声明了以下源兼容性代码。

android {
  //...
  compileOptions {
    sourceCompatibility 1.8
    targetCompatibility 1.8
  }
}

接下来,需要将Flutter module添加到原生Android工程的依赖中。将 Flutter 模块添加到原生Android应用程序中主要有两种方法实现。使用AAR包方式和直接使用module源码的方式。使用AAR包方式需要先将Flutter 模块打包成AAR包。假设,你的 Flutter 模块在 some/path/my_flutter 目录下,那么打包AAR包的命令如下。

cd some/path/my_flutter
flutter build aar

然后,根据屏幕上的提示完成集成操作,如下图所示,当然也可以在Android原生工程中进行手动添加依赖代码。
在这里插入图片描述

事实上,该命令主要用于创建(默认情况下创建 debug/profile/release 所有模式)本地存储库,主要包含以下文件,如下所示。

build/host/outputs/repo
└── com
    └── example
        └── my_flutter
            ├── flutter_release
            │   ├── 1.0
            │   │   ├── flutter_release-1.0.aar
            │   │   ├── flutter_release-1.0.aar.md5
            │   │   ├── flutter_release-1.0.aar.sha1
            │   │   ├── flutter_release-1.0.pom
            │   │   ├── flutter_release-1.0.pom.md5
            │   │   └── flutter_release-1.0.pom.sha1
            │   ├── maven-metadata.xml
            │   ├── maven-metadata.xml.md5
            │   └── maven-metadata.xml.sha1
            ├── flutter_profile
            │   ├── ...
            └── flutter_debug
                └── ...

可以发现,使用上面的命令编译的AAR包主要分为debug、profile和release三个版本,使用哪个版本的AAR需要根据原生的环境进行选择。找到AAR包,然后再Android宿主应用程序中修改 app/build.gradle 文件,使其包含本地存储库和上述依赖项,如下所示。

android {
  // ...
}

repositories {
  maven {
    url 'some/path/my_flutter/build/host/outputs/repo'
    // This is relative to the location of the build.gradle file
    // if using a relative path.
  }
  maven {
    url 'https://storage.googleapis.com/download.flutter.io'
  }
}

dependencies {
  // ...
  debugImplementation 'com.example.flutter_module:flutter_debug:1.0'
  profileImplementation 'com.example.flutter_module:flutter_profile:1.0'
  releaseImplementation 'com.example.flutter_module:flutter_release:1.0'
}

当然,除了命令方式外,还可以使用Android Studio来构建AAR包。依次点击 Android Studio 菜单中的 Build > Flutter > Build AAR 即可构建Flutter 模块的 AAR包,如下图所示。
在这里插入图片描述

除了AAR包方式外,另一种方式就是使用源码的方式进行依赖,即将flutter_module模块作为一个模块添加到Android原生工程中。首先,将Flutter 模块作为子项目添加到宿主应用的 settings.gradle 中,如下所示。

// Include the host app project.
include ':app'                                   
setBinding(new Binding([gradle: this]))                              
evaluate(new File(                                                      
  settingsDir.parentFile,                                           
  'my_flutter/.android/include_flutter.groovy'                       
))                                                                   

binding 和 evaluation 脚本可以使 Flutter 模块将其自身(如 :flutter)和该模块使用的所有 Flutter 插件(如 :package_info,:video_player 等)都包含在 settings.gradle 上下文中,然后在原生Android工程的app目录下的build.gradle文件下添加如下依赖代码。

dependencies {
  implementation project(':flutter')
}

到此,在原生Android工程中集成Flutter环境就完成了,接下来编写代码即可。

添加Flutter页面

正常跳转

1, 添加FlutterActivity
Flutter提供了一个FlutterActivity来作为Flutter的容器页面,FlutterActivity和Android原生的Activity没有任何区别,可以认为它是Flutter的父容器组件,但在原生Android程序中,它就是一个普通的Activity,这个Activity必须在AndroidManifest.xml中进行注册,如下所示。

<activity
  android:name="io.flutter.embedding.android.FlutterActivity"
  android:theme="@style/LaunchTheme"
  android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
  android:hardwareAccelerated="true"
  android:windowSoftInputMode="adjustResize" />

对于theme属性,我们可以使用Android的其他样式进行替换,此主题样式会决定了应用的系统样式。

2,打开FlutterActivity

在AndroidManifest.xml中注册FlutterActivity后,然后我们可以在任何地方启动这个FlutterActivity,如下所示。

myButton.setOnClickListener(new OnClickListener() {
  @Override
  public void onClick(View v) {
    startActivity(
      FlutterActivity.createDefaultIntent(MainActivity.this)
    );
  }
});

运行上面的代码,发现并不会跳转到Flutter页面,因为我们并没有提供跳转的地址。下面的示例将演示如何使用自定义路由跳转到Flutter模块页面中,如下所示。

myButton.addOnClickListener(new OnClickListener() {
  @Override
  public void onClick(View v) {
    startActivity(
      FlutterActivity
        .withNewEngine()
        .initialRoute("/my_route")
        .build(currentActivity)
      );
  }
});

其中,my_route为Flutter模块的初始路由,关于Flutter的路由知识,可以看下面的文章:Flutter开发之路由与导航

我们使用withNewEngine()工厂方法配置,创建一个的FlutterEngine实例。当运行上面的代码时,应用就会由原生页面跳转到Flutter模块页面。

3,使用带有缓存的FlutterEngine

每个FlutterActivity在默认情况下都会创建自己的FlutterEngine,并且每个FlutterEngine在启动时都需要有一定的预热时间。这意味着在原生页面跳转到Flutter模块页面之前会一定的时间延迟。为了尽量减少这个延迟,你可以在启动Flutter页面之前先预热的FlutterEngine。即在应用程序中运行过程中找一个合理的时间实例化一个FlutterEngine,如在Application中进行初始化,如下所示。

public class MyApplication extends Application {
  @Override
  public void onCreate() {
    super.onCreate();
    flutterEngine = new FlutterEngine(this);

    flutterEngine.getDartExecutor().executeDartEntrypoint(
      DartEntrypoint.createDefault()
    );

    FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine);
  }
}

其中,FlutterEngineCache的ID可以是任意的字符串,使用时请确保传递给任何使用缓存的FlutterEngine的FlutterFragment或FlutterActivity使用的是相同的ID。完成上面的自定义Application后,我们还需要在原生Android工程的AndroidManifest.xml中使用自定义的Application,如下所示。

<application
        android:name="MyApplication"
        android:theme="@style/AppTheme">
</application>

下面我们来看一下如何在FlutterActivity页面中使用缓存的FlutterEngine,现在使用FlutterActivity跳转到Flutter模块时需要使用上面的ID,如下所示。

myButton.addOnClickListener(new OnClickListener() {
  @Override
  public void onClick(View v) {
    startActivity(
      FlutterActivity
        .withCachedEngine("my_engine_id")
        .build(currentActivity)
      );
  }
});

可以发现,在使用withCachedEngine()工厂方法后,打开Flutter模块的延迟时间大大降低了。

4,使用缓存引擎的初始路由
当使用带有FlutterEngine配置的FlutterActivity或者FlutterFragment时,会有初始路由的概念,我们可以在代码中添加跳转到Flutter模块的初始路由。然而,当我们使用带有缓存的FlutterEngine时,FlutterActivity和FlutterFragment并没有提供初始路由的概念。如果开发人员希望使用带有缓存的FlutterEngine时也能自定义初始路由,那么可以在执行Dart入口点之前配置他们的缓存FlutterEngine以使用自定义初始路由,如下所示。

public class MyApplication extends Application {
  @Override
  public void onCreate() {
    super.onCreate();
    flutterEngine = new FlutterEngine(this);
    flutterEngine.getNavigationChannel().setInitialRoute("your/route/here");
    flutterEngine.getDartExecutor().executeDartEntrypoint(
      DartEntrypoint.createDefault()
    );
  
    FlutterEngineCache
      .getInstance()
      .put("my_engine_id", flutterEngine);
  }
}

带有背景样式的跳转

如果要修改跳转的样式,那么可以在原生Android端自定义一个主题样式呈现一个半透明的背景。首先打开res/values/styles.xml文件,然后添加自定义的主题,如下所示。

<style name="MyTheme" parent="@style/AppTheme">
        <item name="android:windowIsTranslucent">true</item>
    </style>

然后,将FlutterActivity的主题改为我们自定义的主题,如下所示。

<activity
  android:name="io.flutter.embedding.android.FlutterActivity"
  android:theme="@style/MyTheme"
  android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
  android:hardwareAccelerated="true"
  android:windowSoftInputMode="adjustResize"
  />

然后,就可以使用透明背景启动FlutterActivity,如下所示。

// Using a new FlutterEngine.
startActivity(
  FlutterActivity.withNewEngine()
    .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.transparent)
    .build(context)
);

// Using a cached FlutterEngine.
startActivity(
  FlutterActivity.withCachedEngine("my_engine_id")
    .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.transparent)
    .build(context)
);

添加FlutterFragment

在Android开发中,除了Activity之外,还可以使用Fragment来加载页面,Fragment比Activity的粒度更小,有碎片化的意思。如果有碎片化加载的场景,那么可以使用FlutterFragment 。FlutterFragment允许开发者控制以下操作:

  • 初始化Flutter的路由;
  • Dart的初始页面的飞入样式;
  • 设置不透明和半透明背景;
  • FlutterFragment是否可以控制Activity;
  • FlutterEngine或者带有缓存的FlutterEngine是否能使用;

1,将FlutterFragment 添加到Activity
使用FlutterFragment要做的第一件事就是将其添加到宿主Activity中。为了给宿主Activity添加一个FlutterFragment,需要在Activity的onCreate()中实例化并附加一个FlutterFragment的实例,这和原生Android的Fragment使用方法是一样的,代码如下:

public class MyActivity extends FragmentActivity {
   
    private static final String TAG_FLUTTER_FRAGMENT = "flutter_fragment";
    private FlutterFragment flutterFragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.my_activity_layout);
        FragmentManager fragmentManager = getSupportFragmentManager();
        flutterFragment = (FlutterFragment) fragmentManager
            .findFragmentByTag(TAG_FLUTTER_FRAGMENT);

        if (flutterFragment == null) {
            flutterFragment = FlutterFragment.createDefault();
            fragmentManager
                .beginTransaction()
                .add( R.id.fragment_container, flutterFragment, TAG_FLUTTER_FRAGMENT )
                .commit();
        }
    }
}

其中,代码中用到的原生Fragment的布局代码如下所示。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <FrameLayout
        android:id="@+id/fragment_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
        
</androidx.constraintlayout.widget.ConstraintLayout>

然后,将原生Android的启动页面改为我们的MyActivity即可。除此之外,我们还可以借助FlutterFragment来获取原生代码的生命周期,并作出相关的逻辑操作,如下所示。

public class MyActivity extends FragmentActivity {
    @Override
    public void onPostResume() {
        super.onPostResume();
        flutterFragment.onPostResume();
    }

    @Override
    protected void onNewIntent(@NonNull Intent intent) {
        flutterFragment.onNewIntent(intent);
    }

    @Override
    public void onBackPressed() {
        flutterFragment.onBackPressed();
    }

    @Override
    public void onRequestPermissionsResult(
        int requestCode,
        @NonNull String[] permissions,
        @NonNull int[] grantResults
    ) {
        flutterFragment.onRequestPermissionsResult(
            requestCode,
            permissions,
            grantResults
        );
    }

    @Override
    public void onUserLeaveHint() {
        flutterFragment.onUserLeaveHint();
    }

    @Override
    public void onTrimMemory(int level) {
        super.onTrimMemory(level);
        flutterFragment.onTrimMemory(level);
    }
}

不过,上面的示例启动时使用了一个新的FlutterEngine,因此启动后会需要一定的初始化时间,导致应用启动后会有一个空白的UI,直到FlutterEngine初始化成功后Flutter模块的首页渲染完成。对于这种现象,我们同样可以在提前初始化FlutterEngine,即在应用程序的Application中初始化FlutterFragment,如下所示。

public class MyApplication extends Application {

    FlutterEngine flutterEngine=null;
    
    @Override
    public void onCreate() {
        super.onCreate();
        flutterEngine = new FlutterEngine(this);
        flutterEngine.getNavigationChannel().setInitialRoute("your/route/here");
        flutterEngine.getDartExecutor().executeDartEntrypoint(
                DartExecutor.DartEntrypoint.createDefault()
        );
        FlutterEngineCache
                .getInstance()
                .put("my_engine_id", flutterEngine);
    }
}

在上面的代码中,通过设置导航通道的初始路由,然后关联的FlutterEngine在初始执行runApp() ,在初始执行runApp()后再改变导航通道的初始路由属性是没有效果的。然后,我们修改MyFlutterFragmentActivity类的代码,并使用FlutterFragment.withNewEngine()使用缓存的FlutterEngine,如下所示。

public class MyFlutterFragmentActivity extends FragmentActivity {

    private static final String TAG_FLUTTER_FRAGMENT = "flutter_fragment";
    private FlutterFragment flutterFragment = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.flutter_fragment_activity);
        FragmentManager fragmentManager = getSupportFragmentManager();
        if (flutterFragment == null) {
            flutterFragment=FlutterFragment.withNewEngine()
                    .initialRoute("/")
                    .build();

            fragmentManager
                    .beginTransaction()
                    .add(R.id.fragment_container, flutterFragment,TAG_FLUTTER_FRAGMENT)
                    .commit();
        }
    }
}

控制FlutterFragment的渲染模式

FlutterFragment默认使用SurfaceView来渲染它的Flutter内容,除此之外,还可以使用TextureView来渲染界面,不过SurfaceView的性能比TextureView好得多。但是,SurfaceView不能交错在Android视图层次结构中使用。此外,在Android N之前的Android版本中,SurfaceViews不能动画化,因为它们的布局和渲染不能与其他视图层次结构同步,此时,你需要使用TextureView而不是SurfaceView,使用 TextureView来渲染FlutterFragment的代码如下。

// With a new FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
    .renderMode(FlutterView.RenderMode.texture)
    .build();

// With a cached FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
    .renderMode(FlutterView.RenderMode.texture)
    .build();

如果要给跳转添加一个转场的透明效果,要启用FlutterFragment的透明属性,可以使用下面的配置,如下所示。

// Using a new FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
    .transparencyMode(TransparencyMode.transparent)
    .build();

// Using a cached FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
    .transparencyMode(TransparencyMode.transparent)
    .build();

FlutterFragment 与Activity

有时候,一些应用使用Fragment来作为Flutter页面的承载对象时,状态栏、导航栏和屏幕方向仍然使用的是Activity,Fragment只是作为Activity的一部分。在这些应用程序中,用一个Fragment是合理的,如下图所示。 在这里插入图片描述
在其他应用程序中,Fragment仅仅作为UI的一部分,此时一个FlutterFragment可能被用来实现一个抽屉的内部,一个视频播放器,或一个单一的卡片。在这些情况下,FlutterFragment不需要全屏线上,因为在同一个屏幕中还有其他UI片段,如下图所示。

在这里插入图片描述
FlutterFragment提供了一个概念,用来实现FlutterFragment是否能够控制它的宿主Activity。为了防止一个FlutterFragment将它的Activity暴露给Flutter插件,也为了防止Flutter控制Activity的系统UI,FlutterFragment提供了一个shouldAttachEngineToActivity()方法,如下所示。

// Using a new FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
    .shouldAttachEngineToActivity(false)
    .build();

// Using a cached FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
    .shouldAttachEngineToActivity(false)
    .build();

原生iOS集成Flutter

创建Flutter模块

为了将 Flutter 集成到原生iOS应用里,第一步要创建一个 Flutter module,创建 Flutter module的命令如下所示。

cd some/path/
flutter create --template module my_flutter

执行完上面的命令后,会在some/path/my_flutter/ 目录下创建一个Flutter module库。在这个目录中,你可以像在其它 Flutter 项目中一样,执行 flutter 命令,比如 flutter run --debug 或者 flutter build ios。打开 my_flutter 模块,可以发现,目录结构和普通 的Flutter 应用的目录别无二至,如下所示。

my_flutter/
├── .ios/
│   ├── Runner.xcworkspace
│   └── Flutter/podhelper.rb
├── lib/
│   └── main.dart
├── test/
└── pubspec.yaml

默认情况下,my_flutter的Android工程和iOS工程是隐藏的,我们可以通过显示隐藏的项目来看到Android工程和iOS工程。

集成到已有iOS应用

在原生iOS开发中,有两种方式可以将 Flutter 集成到你的既有应用中。
1, 使用 CocoaPods 依赖管理和已安装的 Flutter SDK 。(推荐)
2,把 Flutter engine 、Dart 代码和所有 Flutter plugin 编译成 framework,然后用 Xcode 手动集成到你的应用中,并更新编译设置。

1, 使用 CocoaPods 和 Flutter SDK 集成

使用此方法集成Flutter,需要在本地安装了 Flutter SDK。然后,只需要在 Xcode 中编译应用,就可以自动运行脚本来集成Dart 代码和 plugin。这个方法允许你使用 Flutter module 中的最新代码快速迭代开发,而无需在 Xcode 以外执行额外的命令。

现在假如又一个原生iOS工程,并且 Flutter module 和这个iOS工程是处在相邻目录的,如下所示。

some/path/
├── my_flutter/
│   └── .ios/
│       └── Flutter/
│         └── podhelper.rb
└── MyApp/
    └── Podfile

1,如果你的应用(MyApp)还没有 Podfile,可以根据 CocoaPods 使用指南 来在项目中添加 Podfile。然后,在 Podfile 中添加下面代码:

flutter_application_path = '../my_flutter'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

2,每个需要集成 Flutter 的 [Podfile target][],执行 install_all_flutter_pods(flutter_application_path),如下所示。

target 'MyApp' do
  install_all_flutter_pods(flutter_application_path)
end

3,最后,在MyApp原生工程下运行 pod install命令拉取原生工程需要的插件。

pod install

如果没有任何错误,界面如下图。
在这里插入图片描述

在上面的Podfile文件中, podhelper.rb 脚本会把你的 plugins, Flutter.framework,和 App.framework 集成到你的原生iOS项目中。同时,你应用的 Debug 和 Release 编译配置,将会集成相对应的 Debug 或 Release 的 编译产物。可以增加一个 Profile 编译配置用于在 profile 模式下测试应用。然后,在 Xcode 中打开 MyApp.xcworkspace ,可以使用 【⌘B 】快捷键编译项目,并运行项目即可。

使用frameworks集成

除了上面的方法,你也可以创建一个 frameworks,手动修改既有 Xcode 项目,将他们集成进去。但是每当你在 Flutter module 中改变了代码,都必须运行 flutter build ios-framework来编译framework。下面的示例假设你想在 some/path/MyApp/Flutter/ 目录下创建 frameworks。

flutter build ios-framework --output=some/path/MyApp/Flutter/

此时的文件目录如下所示。

some/path/MyApp/
└── Flutter/
    ├── Debug/
    │   ├── Flutter.framework
    │   ├── App.framework
    │   ├── FlutterPluginRegistrant.framework (only if you have plugins with iOS platform code)
    │   └── example_plugin.framework (each plugin is a separate framework)
    ├── Profile/
    │   ├── Flutter.framework
    │   ├── App.framework
    │   ├── FlutterPluginRegistrant.framework
    │   └── example_plugin.framework
    └── Release/
        ├── Flutter.framework
        ├── App.framework
        ├── FlutterPluginRegistrant.framework
        └── example_plugin.framework

然后,使用 Xcode 打开原生iOS工程,并将生成的 frameworks 集成到既有iOS应用中。例如,你可以在 some/path/MyApp/Flutter/Release/ 目录拖拽 frameworks 到你的应用 target 编译设置的 General > Frameworks, Libraries, and Embedded Content 下,然后在 Embed 下拉列表中选择 “Embed & Sign”。

1, 链接到框架

当然,你也可以将框架从 Finder 的 some/path/MyApp/Flutter/Release/ 拖到你的目标项目中,然后点击 build settings > Build Phases > Link Binary With Libraries。然后,在 target 的编译设置中的 Framework Search Paths (FRAMEWORK_SEARCH_PATHS) 增加 $(PROJECT_DIR)/Flutter/Release/,如下图所示。
在这里插入图片描述
2,内嵌框架
生成的动态framework框架必须嵌入你的应用才能在运行时被加载。需要说明的是插件会帮助你生成 静态或动态框架。静态框架应该直接链接而不是嵌入,如果你在应用中嵌入了静态框架,你的应用将不能发布到 App Store 并且会得到一个 Found an unexpected Mach-O header code 的 archive 错误。

你可以从应用框架组中拖拽框架(除了 FlutterPluginRegistrant 以及其他的静态框架)到你的目标 ‘ build settings > Build Phases > Embed Frameworks,然后从下拉菜单中选择 “Embed & Sign”,如下图所示。

在这里插入图片描述

3,使用 CocoaPods 在 Xcode 和 Flutter 框架中内嵌应用

除了使用Flutter.framework方式外,你还可以加入一个参数 --cocoapods ,然后将 Flutter 框架作为一个 CocoaPods 的 podspec 文件分发。这将会生成一个 Flutter.podspec 文件而不再生成 Flutter.framework 引擎文件,命令如下。

flutter build ios-framework --cocoapods --output=some/path/MyApp/Flutter/

执行命令后,Flutter模块的目录如下图所示。

some/path/MyApp/
└── Flutter/
    ├── Debug/
    │   ├── Flutter.podspec
    │   ├── App.framework
    │   ├── FlutterPluginRegistrant.framework
    │   └── example_plugin.framework (each plugin with iOS platform code is a separate framework)
    ├── Profile/
    │   ├── Flutter.podspec
    │   ├── App.framework
    │   ├── FlutterPluginRegistrant.framework
    │   └── example_plugin.framework
    └── Release/
        ├── Flutter.podspec
        ├── App.framework
        ├── FlutterPluginRegistrant.framework
        └── example_plugin.framework

然后,在iOS应用程序使用CocoaPods添加Flutter以来文件即可,如下所示。

pod 'Flutter', :podspec => 'some/path/MyApp/Flutter/[build mode]/Flutter.podspec'

添加一个Flutter页面

FlutterEngine 和 FlutterViewController

为了在原生 iOS 应用中展示 Flutter 页面,需要使用到FlutterEngineFlutterViewController。其中,FlutterEngine 充当 Dart VM 和 Flutter 运行时环境; FlutterViewController 依附于 FlutterEngine,给 Flutter 传递 UIKit 的输入事件,并展示被 FlutterEngine 渲染的每一帧画面。

1,创建一个 FlutterEngine
创建 FlutterEngine 的时机由您自己决定。作为示例,我们将在应用启动的 app delegate 中创建一个 FlutterEngine,并作为属性暴露给外界。首先,在在 AppDelegate.h文件中添加如下代码。

@import UIKit;
@import Flutter;

@interface AppDelegate : FlutterAppDelegate // More on the FlutterAppDelegate below.
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end

然后,在 AppDelegate.m文件中添加如下代码。

// Used to connect plugins (only if you have plugins with iOS platform code).
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h>

#import "AppDelegate.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id> *)launchOptions {
  self.flutterEngine = [[FlutterEngine alloc] initWithName:@"my flutter engine"];
  // Runs the default Dart entrypoint with a default Flutter route.
  [self.flutterEngine run];
  // Used to connect plugins (only if you have plugins with iOS platform code).
  [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

@end

需要说明的是,GeneratedPluginRegistrant只有在需要支持的插件才能使用。然后运行项目,结果报了一个framework not found FlutterPluginRegistrant错误。

ld: warning: directory not found for option '-F/Users/bilibili/Library/Developer/Xcode/DerivedData/iOSFlutterHybird-advitqdrflrsxldrjkqcsvdzxbop/Build/Products/Debug-iphonesimulator/FlutterPluginRegistrant'
ld: framework not found FlutterPluginRegistrant
clang: error: linker command failed with exit code 1 (use -v to see invocation)

对于这个错误,需要打开项目编译配置,修改Bitcode。默认情况下,Flutter是不支持Bitcode的,Bitcode是一种iOS编译程序的中间代码,在原生iOS工程中集成Flutter需要禁用Bitcode,如下图所示。
在这里插入图片描述

2,使用 FlutterEngine 展示 FlutterViewController
在下面的例子中,展示了一个普通的 ViewController,当点击页面中的UIButton时就会跳转到 FlutterViewController 的 ,这个 FlutterViewController 使用在 AppDelegate 中创建的 Flutter 引擎 (FlutterEngine)。

@import Flutter;
#import "AppDelegate.h"
#import "ViewController.h"

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];

    // Make a button to call the showFlutter function when pressed.
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button addTarget:self
               action:@selector(showFlutter)
     forControlEvents:UIControlEventTouchUpInside];
    [button setTitle:@"Show Flutter!" forState:UIControlStateNormal];
    button.backgroundColor = UIColor.blueColor;
    button.frame = CGRectMake(80.0, 210.0, 160.0, 40.0);
    [self.view addSubview:button];
}

- (void)showFlutter {
    FlutterEngine *flutterEngine =
        ((AppDelegate *)UIApplication.sharedApplication.delegate).flutterEngine;
    FlutterViewController *flutterViewController =
        [[FlutterViewController alloc] initWithEngine:flutterEngine nibName:nil bundle:nil];
    [self presentViewController:flutterViewController animated:YES completion:nil];
}
@end

运行上面的代码,如果出现“symbol(s) not found for architecture x86_64”的错误,可以使用下面的步骤进行解决。使用Xcode打开项目,然后依次选择TARGETS->Build Phases,然后找到Compile Sources 并点击“+”, 在搜索框输入APPDelegate 找到他的.m文件。

在这里插入图片描述
3,使用隐式 FlutterEngine 创建 FlutterViewController
我们可以让 FlutterViewController 隐式的创建 FlutterEngine,而不用提前初始化一个FlutterEngine。不过不建议这样做,因为按需创建FlutterEngine 的话,在 FlutterViewController 被 present 出来之后,第一帧图像渲染完之前,将会有明显的延迟。不过,当 Flutter 页面很少被展示时,可以使用此方式。

为了不使用已经存在的 FlutterEngine 来展现 FlutterViewController,省略 FlutterEngine 的创建步骤,并且在创建 FlutterViewController 时,去掉 FlutterEngine 的引用。

// Existing code omitted.
// 省略已经存在的代码
- (void)showFlutter {
  FlutterViewController *flutterViewController =
      [[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];
  [self presentViewController:flutterViewController animated:YES completion:nil];
}
@end

使用 FlutterAppDelegate

FlutterAppDelegate 具备如下功能:

  • 传递应用的回调,例如 openURL 到 Flutter 的插件 —— local_auth。
  • 传递状态栏点击(这只能在 AppDelegate 中检测)到 Flutter 的点击置顶行为。

我们推荐应用的UIApplicationDelegate 继承 FlutterAppDelegate,但不是必须的,如果你的 App Delegate 不能直接继承 FlutterAppDelegate,那么让你的 App Delegate 实现 FlutterAppLifeCycleProvider 协议,来确保 Flutter plugins 接收到必要的回调。否则,依赖这些事件的 plugins 将会有无法预估的行为。

@import Flutter;
@import UIKit;
@import FlutterPluginRegistrant;

@interface AppDelegate : UIResponder <UIApplicationDelegate, FlutterAppLifeCycleProvider>
@property (strong, nonatomic) UIWindow *window;
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end

然后,在具体实现中,将App Delegate委托给 FlutterPluginAppLifeCycleDelegate,如下所示。

@interface AppDelegate ()
@property (nonatomic, strong) FlutterPluginAppLifeCycleDelegate* lifeCycleDelegate;
@end

@implementation AppDelegate

- (instancetype)init {
    if (self = [super init]) {
        _lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
    }
    return self;
}

- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id>*))launchOptions {
    self.flutterEngine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil];
    [self.flutterEngine runWithEntrypoint:nil];
    [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
    return [_lifeCycleDelegate application:application didFinishLaunchingWithOptions:launchOptions];
}

// Returns the key window's rootViewController, if it's a FlutterViewController.
// Otherwise, returns nil.
- (FlutterViewController*)rootFlutterViewController {
    UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
    if ([viewController isKindOfClass:[FlutterViewController class]]) {
        return (FlutterViewController*)viewController;
    }
    return nil;
}

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
    [super touchesBegan:touches withEvent:event];

    // Pass status bar taps to key window Flutter rootViewController.
    if (self.rootFlutterViewController != nil) {
        [self.rootFlutterViewController handleStatusBarTouches:event];
    }
}

- (void)application:(UIApplication*)application
didRegisterUserNotificationSettings:(UIUserNotificationSettings*)notificationSettings {
    [_lifeCycleDelegate application:application
didRegisterUserNotificationSettings:notificationSettings];
}

- (void)application:(UIApplication*)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken {
    [_lifeCycleDelegate application:application
didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}

- (void)application:(UIApplication*)application
didReceiveRemoteNotification:(NSDictionary*)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
    [_lifeCycleDelegate application:application
       didReceiveRemoteNotification:userInfo
             fetchCompletionHandler:completionHandler];
}

- (BOOL)application:(UIApplication*)application
            openURL:(NSURL*)url
            options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options {
    return [_lifeCycleDelegate application:application openURL:url options:options];
}

- (BOOL)application:(UIApplication*)application handleOpenURL:(NSURL*)url {
    return [_lifeCycleDelegate application:application handleOpenURL:url];
}

- (BOOL)application:(UIApplication*)application
            openURL:(NSURL*)url
  sourceApplication:(NSString*)sourceApplication
         annotation:(id)annotation {
    return [_lifeCycleDelegate application:application
                                   openURL:url
                         sourceApplication:sourceApplication
                                annotation:annotation];
}

- (void)application:(UIApplication*)application
performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem
  completionHandler:(void (^)(BOOL succeeded))completionHandler NS_AVAILABLE_IOS(9_0) {
    [_lifeCycleDelegate application:application
       performActionForShortcutItem:shortcutItem
                  completionHandler:completionHandler];
}

- (void)application:(UIApplication*)application
handleEventsForBackgroundURLSession:(nonnull NSString*)identifier
  completionHandler:(nonnull void (^)(void))completionHandler {
    [_lifeCycleDelegate application:application
handleEventsForBackgroundURLSession:identifier
                  completionHandler:completionHandler];
}

- (void)application:(UIApplication*)application
performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
    [_lifeCycleDelegate application:application performFetchWithCompletionHandler:completionHandler];
}

- (void)addApplicationLifeCycleDelegate:(NSObject<FlutterPlugin>*)delegate {
    [_lifeCycleDelegate addDelegate:delegate];
}
@end

启动选项

上面例子使用默认配置来启动 Flutter,为了定制化你的 Flutter 运行时,我们可以指定 Dart 入口、库和路由。

1,指定Dart 入口

在 FlutterEngine 上调用 run()函数,默认将会调用你的 lib/main.dart 文件里的 main() 函数。不过,我们可以使用入口方法 runWithEntrypoint()来指定一个Dart 入口,并且,使用 main() 以外的 Dart 入口函数,必须使用下面的注解,防止被 tree-shaken 优化掉,而没有进行编译。如下所示。

  @pragma('vm:entry-point')
  void myOtherEntrypoint() { ... };

2,指定Dart 库
同时,Flutter允许开发者在指定 Dart 函数时指定特定文件。例如使用 lib/other_file.dart 文件的 myOtherEntrypoint() 函数取代 lib/main.dart 的 main() 函数,如下所示。

[flutterEngine runWithEntrypoint:@"myOtherEntrypoint" libraryURI:@"other_file.dart"];

3,指定Dart 路由

当然,当构建Flutter Engine 时,还可以为你的 Flutter 应用设置一个初始路由,如下所示。

FlutterEngine *flutterEngine =
    [[FlutterEngine alloc] initWithName:@"my flutter engine"];
[[flutterEngine navigationChannel] invokeMethod:@"setInitialRoute"
                                      arguments:@"/onboarding"];
[flutterEngine run];
查看原文

Dont 赞了文章 · 2020-08-16

你真的了解回流和重绘吗

回流和重绘可以说是每一个web开发者都经常听到的两个词语,我也不例外,可是我之前一直不是很清楚这两步具体做了什么事情。最近由于部门内部要做分享,所以对其进行了一些研究,看了一些博客和书籍,整理了一些内容并且结合一些例子,写了这篇文章,希望可以帮助到大家。

浏览器的渲染过程

本文先从浏览器的渲染过程来从头到尾的讲解一下回流重绘,如果大家想直接看如何减少回流和重绘,可以跳到后面。(这个渲染过程来自MDN

webkit渲染过程

从上面这个图上,我们可以看到,浏览器渲染过程如下:

  1. 解析HTML,生成DOM树,解析CSS,生成CSSOM树
  2. 将DOM树和CSSOM树结合,生成渲染树(Render Tree)
  3. Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
  4. Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
  5. Display:将像素发送给GPU,展示在页面上。(这一步其实还有很多内容,比如会在GPU将多个合成层合并为同一个层,并展示在页面中。而css3硬件加速的原理则是新建合成层,这里我们不展开,之后有机会会写一篇博客)

渲染过程看起来很简单,让我们来具体了解下每一步具体做了什么。

生成渲染树

生成渲染树

为了构建渲染树,浏览器主要完成了以下工作:

  1. 从DOM树的根节点开始遍历每个可见节点。
  2. 对于每个可见的节点,找到CSSOM树中对应的规则,并应用它们。
  3. 根据每个可见节点以及其对应的样式,组合生成渲染树。

第一步中,既然说到了要遍历可见的节点,那么我们得先知道,什么节点是不可见的。不可见的节点包括:

  • 一些不会渲染输出的节点,比如script、meta、link等。
  • 一些通过css进行隐藏的节点。比如display:none。注意,利用visibility和opacity隐藏的节点,还是会显示在渲染树上的。只有display:none的节点才不会显示在渲染树上。

注意:渲染树只包含可见的节点

回流

前面我们通过构造渲染树,我们将可见DOM节点以及它对应的样式结合起来,可是我们还需要计算它们在设备视口(viewport)内的确切位置和大小,这个计算的阶段就是回流。

为了弄清每个对象在网站上的确切大小和位置,浏览器从渲染树的根节点开始遍历,我们可以以下面这个实例来表示:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Critial Path: Hello world!</title>
  </head>
  <body>
    <div style="width: 50%">
      <div style="width: 50%">Hello world!</div>
    </div>
  </body>
</html>

我们可以看到,第一个div将节点的显示尺寸设置为视口宽度的50%,第二个div将其尺寸设置为父节点的50%。而在回流这个阶段,我们就需要根据视口具体的宽度,将其转为实际的像素值。(如下图)

重绘

最终,我们通过构造渲染树和回流阶段,我们知道了哪些节点是可见的,以及可见节点的样式和具体的几何信息(位置、大小),那么我们就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘节点。

既然知道了浏览器的渲染过程后,我们就来探讨下,何时会发生回流重绘。

何时发生回流重绘

我们前面知道了,回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流。比如以下情况:

  • 添加或删除可见的DOM元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
  • 页面一开始渲染的时候(这肯定避免不了)
  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

注意:回流一定会触发重绘,而重绘不一定会回流

根据改变的范围和程度,渲染树中或大或小的部分需要重新计算,有些改变会触发整个页面的重排,比如,滚动条出现的时候或者修改了根节点。

浏览器的优化机制

现代的浏览器都是很聪明的,由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列。但是!当你获取布局信息的操作的时候,会强制队列刷新,比如当你访问以下属性或者使用以下方法:

  • offsetTop、offsetLeft、offsetWidth、offsetHeight
  • scrollTop、scrollLeft、scrollWidth、scrollHeight
  • clientTop、clientLeft、clientWidth、clientHeight
  • getComputedStyle()
  • getBoundingClientRect
  • 具体可以访问这个网站:https://gist.github.com/pauli...

以上属性和方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值。因此,我们在修改样式的时候,最好避免使用上面列出的属性,他们都会刷新渲染队列。如果要使用它们,最好将值缓存起来。

减少回流和重绘

好了,到了我们今天的重头戏,前面说了这么多背景和理论知识,接下来让我们谈谈如何减少回流和重绘。

最小化重绘和重排

由于重绘和重排可能代价比较昂贵,因此最好就是可以减少它的发生次数。为了减少发生次数,我们可以合并多次对DOM和样式的修改,然后一次处理掉。考虑这个例子

const el = document.getElementById('test');
el.style.padding = '5px';
el.style.borderLeft = '1px';
el.style.borderRight = '2px';

例子中,有三个样式属性被修改了,每一个都会影响元素的几何结构,引起回流。当然,大部分现代浏览器都对其做了优化,因此,只会触发一次重排。但是如果在旧版的浏览器或者在上面代码执行的时候,有其他代码访问了布局信息(上文中的会触发回流的布局信息),那么就会导致三次重排。

因此,我们可以合并所有的改变然后依次处理,比如我们可以采取以下的方式:

  • 使用cssText

    const el = document.getElementById('test');
    el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';
  • 修改CSS的class

    const el = document.getElementById('test');
    el.className += ' active';

批量修改DOM

当我们需要对DOM对一系列修改的时候,可以通过以下步骤减少回流重绘次数:

  1. 使元素脱离文档流
  2. 对其进行多次修改
  3. 将元素带回到文档中。

该过程的第一步和第三步可能会引起回流,但是经过第一步之后,对DOM的所有修改都不会引起回流,因为它已经不在渲染树了。

有三种方式可以让DOM脱离文档流:

  • 隐藏元素,应用修改,重新显示
  • 使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档。
  • 将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。

考虑我们要执行一段批量插入节点的代码:

function appendDataToElement(appendToElement, data) {
    let li;
    for (let i = 0; i < data.length; i++) {
        li = document.createElement('li');
        li.textContent = 'text';
        appendToElement.appendChild(li);
    }
}

const ul = document.getElementById('list');
appendDataToElement(ul, data);

如果我们直接这样执行的话,由于每次循环都会插入一个新的节点,会导致浏览器回流一次。

我们可以使用这三种方式进行优化:

隐藏元素,应用修改,重新显示

这个会在展示和隐藏节点的时候,产生两次重绘

function appendDataToElement(appendToElement, data) {
    let li;
    for (let i = 0; i < data.length; i++) {
        li = document.createElement('li');
        li.textContent = 'text';
        appendToElement.appendChild(li);
    }
}
const ul = document.getElementById('list');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';

使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档

const ul = document.getElementById('list');
const fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
ul.appendChild(fragment);

将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。

const ul = document.getElementById('list');
const clone = ul.cloneNode(true);
appendDataToElement(clone, data);
ul.parentNode.replaceChild(clone, ul);

对于上述那种情况,我写了一个demo来测试修改前和修改后的性能。然而实验结果不是很理想。

原因:原因其实上面也说过了,浏览器会使用队列来储存多次修改,进行优化,所以对这个优化方案,我们其实不用优先考虑。

避免触发同步布局事件

上文我们说过,当我们访问元素的一些属性的时候,会导致浏览器强制清空队列,进行强制同步布局。举个例子,比如说我们想将一个p标签数组的宽度赋值为一个元素的宽度,我们可能写出这样的代码:

function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = box.offsetWidth + 'px';
    }
}

这段代码看上去是没有什么问题,可是其实会造成很大的性能问题。在每次循环的时候,都读取了box的一个offsetWidth属性值,然后利用它来更新p标签的width属性。这就导致了每一次循环的时候,浏览器都必须先使上一次循环中的样式更新操作生效,才能响应本次循环的样式读取操作。每一次循环都会强制浏览器刷新队列。我们可以优化为:

const width = box.offsetWidth;
function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = width + 'px';
    }
}

同样,我也写了个demo来比较两者的性能差异。你可以自己点开这个demo体验下。这个对比差距就比较明显。

对于复杂动画效果,使用绝对定位让其脱离文档流

对于复杂动画效果,由于会经常的引起回流重绘,因此,我们可以使用绝对定位,让它脱离文档流。否则会引起父元素以及后续元素频繁的回流。这个我们就直接上个例子

打开这个例子后,我们可以打开控制台,控制台上会输出当前的帧数(虽然不准)。

image-20181210223750055

从上图中,我们可以看到,帧数一直都没到60。这个时候,只要我们点击一下那个按钮,把这个元素设置为绝对定位,帧数就可以稳定60。

css3硬件加速(GPU加速)

比起考虑如何减少回流重绘,我们更期望的是,根本不要回流重绘。这个时候,css3硬件加速就闪亮登场啦!!

划重点:使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

本篇文章只讨论如何使用,暂不考虑其原理,之后有空会另外开篇文章说明。

如何使用

常见的触发硬件加速的css属性:

  • transform
  • opacity
  • filters
  • Will-change

效果

我们可以先看个例子。我通过使用chrome的Performance捕获了一段时间的回流重绘情况,实际结果如下图:

image-20181210225609533

从图中我们可以看出,在动画进行的时候,没有发生任何的回流重绘。如果感兴趣你也可以自己做下实验。

重点

  • 使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘
  • 对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

css3硬件加速的坑

  • 如果你为太多元素使用css3硬件加速,会导致内存占用较大,会有性能问题。
  • 在GPU渲染字体会导致抗锯齿无效。这是因为GPU和CPU的算法不同。因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。

总结

本文主要讲了浏览器的渲染过程、浏览器的优化机制以及如何减少甚至避免回流和重绘,希望可以帮助大家更好的理解回流重绘。

参考文献

本文地址在->本人博客地址, 欢迎给个 start 或 follow

查看原文

赞 240 收藏 163 评论 18

Dont 赞了文章 · 2020-07-26

前端模块化详解(完整版)

前言

在JavaScript发展初期就是为了实现简单的页面交互逻辑,寥寥数语即可;如今CPU、浏览器性能得到了极大的提升,很多页面逻辑迁移到了客户端(表单验证等),随着web2.0时代的到来,Ajax技术得到广泛应用,jQuery等前端库层出不穷,前端代码日益膨胀,此时在JS方面就会考虑使用模块化规范去管理。
本文内容主要有理解模块化,为什么要模块化,模块化的优缺点以及模块化规范,并且介绍下开发中最流行的CommonJS, AMD, ES6、CMD规范。本文试图站在小白的角度,用通俗易懂的笔调介绍这些枯燥无味的概念,希望诸君阅读后,对模块化编程有个全新的认识和理解!

建议下载本文源代码,自己动手敲一遍,请猛戳GitHub个人博客

模块化规范

一、模块化的理解

1.什么是模块?

  • 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
  • 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信

2.模块化的进化过程

  • 全局function模式 : 将不同的功能封装成不同的全局函数

    • 编码: 将不同的功能封装成不同的全局函数
    • 问题: 污染全局命名空间, 容易引起命名冲突或数据不安全,而且模块成员之间看不出直接关系
function m1(){
  //...
}
function m2(){
  //...
}
  • namespace模式 : 简单对象封装

    • 作用: 减少了全局变量,解决命名冲突
    • 问题: 数据不安全(外部可以直接修改模块内部的数据)
let myModule = {
  data: 'www.baidu.com',
  foo() {
    console.log(`foo() ${this.data}`)
  },
  bar() {
    console.log(`bar() ${this.data}`)
  }
}
myModule.data = 'other data' //能直接修改模块内部的数据
myModule.foo() // foo() other data

这样的写法会暴露所有模块成员,内部状态可以被外部改写。

  • IIFE模式:匿名函数自调用(闭包)

    • 作用: 数据是私有的, 外部只能通过暴露的方法操作
    • 编码: 将数据和行为封装到一个函数内部, 通过给window添加属性来向外暴露接口
    • 问题: 如果当前这个模块依赖另一个模块怎么办?
// index.html文件
<script type="text/javascript" data-original="module.js"></script>
<script type="text/javascript">
    myModule.foo()
    myModule.bar()
    console.log(myModule.data) //undefined 不能访问模块内部数据
    myModule.data = 'xxxx' //不是修改的模块内部的data
    myModule.foo() //没有改变
</script>
// module.js文件
(function(window) {
  let data = 'www.baidu.com'
  //操作数据的函数
  function foo() {
    //用于暴露有函数
    console.log(`foo() ${data}`)
  }
  function bar() {
    //用于暴露有函数
    console.log(`bar() ${data}`)
    otherFun() //内部调用
  }
  function otherFun() {
    //内部私有的函数
    console.log('otherFun()')
  }
  //暴露行为
  window.myModule = { foo, bar } //ES6写法
})(window)

最后得到的结果:

  • IIFE模式增强 : 引入依赖

这就是现代模块实现的基石

// module.js文件
(function(window, $) {
  let data = 'www.baidu.com'
  //操作数据的函数
  function foo() {
    //用于暴露有函数
    console.log(`foo() ${data}`)
    $('body').css('background', 'red')
  }
  function bar() {
    //用于暴露有函数
    console.log(`bar() ${data}`)
    otherFun() //内部调用
  }
  function otherFun() {
    //内部私有的函数
    console.log('otherFun()')
  }
  //暴露行为
  window.myModule = { foo, bar }
})(window, jQuery)
 // index.html文件
  <!-- 引入的js必须有一定顺序 -->
  <script type="text/javascript" data-original="jquery-1.10.1.js"></script>
  <script type="text/javascript" data-original="module.js"></script>
  <script type="text/javascript">
    myModule.foo()
  </script>

上例子通过jquery方法将页面的背景颜色改成红色,所以必须先引入jQuery库,就把这个库当作参数传入。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显

3. 模块化的好处

  • 避免命名冲突(减少命名空间污染)
  • 更好的分离, 按需加载
  • 更高复用性
  • 高可维护性

4. 引入多个<script>后出现出现问题

  • 请求过多

首先我们要依赖多个模块,那样就会发送多个请求,导致请求过多

  • 依赖模糊

我们不知道他们的具体依赖关系是什么,也就是说很容易因为不了解他们之间的依赖关系导致加载先后顺序出错。

  • 难以维护

以上两种原因就导致了很难维护,很可能出现牵一发而动全身的情况导致项目出现严重的问题。
模块化固然有多个好处,然而一个页面需要引入多个js文件,就会出现以上这些问题。而这些问题可以通过模块化规范来解决,下面介绍开发中最流行的commonjs, AMD, ES6, CMD规范。

二、模块化规范

1.CommonJS

(1)概述

Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。

(2)特点

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。

(3)基本语法

  • 暴露模块:module.exports = valueexports.xxx = value
  • 引入模块:require(xxx),如果是第三方模块,xxx为模块名;如果是自定义模块,xxx为模块文件路径

此处我们有个疑问:CommonJS暴露的模块到底是什么? CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性

// example.js
var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;

上面代码通过module.exports输出变量x和函数addX。

var example = require('./example.js');//如果参数字符串以“./”开头,则表示加载的是一个位于相对路径
console.log(example.x); // 5
console.log(example.addX(1)); // 6

require命令用于加载模块文件。require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错

(4)模块的加载机制

CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。这点与ES6模块化有重大差异(下文会介绍),请看下面这个例子:

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

上面代码输出内部变量counter和改写这个变量的内部方法incCounter。

// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;

console.log(counter);  // 3
incCounter();
console.log(counter); // 3

上面代码说明,counter输出以后,lib.js模块内部的变化就影响不到counter了。这是因为counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值

(5)服务器端实现

①下载安装node.js

②创建项目结构

注意:用npm init 自动生成package.json时,package name(包名)不能有中文和大写

|-modules
  |-module1.js
  |-module2.js
  |-module3.js
|-app.js
|-package.json
  {
    "name": "commonJS-node",
    "version": "1.0.0"
  }

③下载第三方模块

npm install uniq --save // 用于数组去重

④定义模块代码

//module1.js
module.exports = {
  msg: 'module1',
  foo() {
    console.log(this.msg)
  }
}
//module2.js
module.exports = function() {
  console.log('module2')
}
//module3.js
exports.foo = function() {
  console.log('foo() module3')
}
exports.arr = [1, 2, 3, 3, 2]
// app.js文件
// 引入第三方库,应该放置在最前面
let uniq = require('uniq')
let module1 = require('./modules/module1')
let module2 = require('./modules/module2')
let module3 = require('./modules/module3')

module1.foo() //module1
module2() //module2
module3.foo() //foo() module3
console.log(uniq(module3.arr)) //[ 1, 2, 3 ]

⑤通过node运行app.js

命令行输入node app.js,运行JS文件

(6)浏览器端实现(借助Browserify)

①创建项目结构

|-js
  |-dist //打包生成文件的目录
  |-src //源码所在的目录
    |-module1.js
    |-module2.js
    |-module3.js
    |-app.js //应用主源文件
|-index.html //运行于浏览器上
|-package.json
  {
    "name": "browserify-test",
    "version": "1.0.0"
  }

②下载browserify

  • 全局: npm install browserify -g
  • 局部: npm install browserify --save-dev

③定义模块代码(同服务器端)

注意:index.html文件要运行在浏览器上,需要借助browserify将app.js文件打包编译,如果直接在index.html引入app.js就会报错!

④打包处理js

根目录下运行browserify js/src/app.js -o js/dist/bundle.js

⑤页面使用引入

在index.html文件中引入<script type="text/javascript" data-original="js/dist/bundle.js"></script>

2.AMD

CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。此外AMD规范比CommonJS规范在浏览器端实现要来着早。

(1)AMD规范基本语法

定义暴露模块:

//定义没有依赖的模块
define(function(){
   return 模块
})
//定义有依赖的模块
define(['module1', 'module2'], function(m1, m2){
   return 模块
})

引入使用模块:

require(['module1', 'module2'], function(m1, m2){
   使用m1/m2
})

(2)未使用AMD规范与使用require.js

通过比较两者的实现方法,来说明使用AMD规范的好处。

  • 未使用AMD规范
// dataService.js文件
(function (window) {
  let msg = 'www.baidu.com'
  function getMsg() {
    return msg.toUpperCase()
  }
  window.dataService = {getMsg}
})(window)
 // alerter.js文件
(function (window, dataService) {
  let name = 'Tom'
  function showMsg() {
    alert(dataService.getMsg() + ', ' + name)
  }
  window.alerter = {showMsg}
})(window, dataService)
// main.js文件
(function (alerter) {
  alerter.showMsg()
})(alerter)
// index.html文件
<div><h1>Modular Demo 1: 未使用AMD(require.js)</h1></div>
<script type="text/javascript" data-original="js/modules/dataService.js"></script>
<script type="text/javascript" data-original="js/modules/alerter.js"></script>
<script type="text/javascript" data-original="js/main.js"></script>

最后得到如下结果:

这种方式缺点很明显:首先会发送多个请求,其次引入的js文件顺序不能搞错,否则会报错!

  • 使用require.js

RequireJS是一个工具库,主要用于客户端的模块管理。它的模块管理遵守AMD规范,RequireJS的基本思想是,通过define方法,将代码定义为模块;通过require方法,实现代码的模块加载
接下来介绍AMD规范在浏览器实现的步骤:

①下载require.js, 并引入

  • 官网: http://www.requirejs.cn/
  • github : https://github.com/requirejs/requirejs

然后将require.js导入项目: js/libs/require.js

②创建项目结构

|-js
  |-libs
    |-require.js
  |-modules
    |-alerter.js
    |-dataService.js
  |-main.js
|-index.html

③定义require.js的模块代码

// dataService.js文件 
// 定义没有依赖的模块
define(function() {
  let msg = 'www.baidu.com'
  function getMsg() {
    return msg.toUpperCase()
  }
  return { getMsg } // 暴露模块
})
//alerter.js文件
// 定义有依赖的模块
define(['dataService'], function(dataService) {
  let name = 'Tom'
  function showMsg() {
    alert(dataService.getMsg() + ', ' + name)
  }
  // 暴露模块
  return { showMsg }
})
// main.js文件
(function() {
  require.config({
    baseUrl: 'js/', //基本路径 出发点在根目录下
    paths: {
      //映射: 模块标识名: 路径
      alerter: './modules/alerter', //此处不能写成alerter.js,会报错
      dataService: './modules/dataService'
    }
  })
  require(['alerter'], function(alerter) {
    alerter.showMsg()
  })
})()
// index.html文件
<!DOCTYPE html>
<html>
  <head>
    <title>Modular Demo</title>
  </head>
  <body>
    <!-- 引入require.js并指定js主文件的入口 -->
    <script data-main="js/main" data-original="js/libs/require.js"></script>
  </body>
</html>

④页面引入require.js模块:

在index.html引入 <script data-main="js/main" data-original="js/libs/require.js"></script>

此外在项目中如何引入第三方库?只需在上面代码的基础稍作修改:

// alerter.js文件
define(['dataService', 'jquery'], function(dataService, $) {
  let name = 'Tom'
  function showMsg() {
    alert(dataService.getMsg() + ', ' + name)
  }
  $('body').css('background', 'green')
  // 暴露模块
  return { showMsg }
})
// main.js文件
(function() {
  require.config({
    baseUrl: 'js/', //基本路径 出发点在根目录下
    paths: {
      //自定义模块
      alerter: './modules/alerter', //此处不能写成alerter.js,会报错
      dataService: './modules/dataService',
      // 第三方库模块
      jquery: './libs/jquery-1.10.1' //注意:写成jQuery会报错
    }
  })
  require(['alerter'], function(alerter) {
    alerter.showMsg()
  })
})()

上例是在alerter.js文件中引入jQuery第三方库,main.js文件也要有相应的路径配置。
小结:通过两者的比较,可以得出AMD模块定义的方法非常清晰,不会污染全局环境,能够清楚地显示依赖关系。AMD模式可以用于浏览器环境,并且允许非同步加载模块,也可以根据需要动态加载模块。

3.CMD

CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD规范整合了CommonJS和AMD规范的特点。在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。

(1)CMD规范基本语法

定义暴露模块:

//定义没有依赖的模块
define(function(require, exports, module){
  exports.xxx = value
  module.exports = value
})
//定义有依赖的模块
define(function(require, exports, module){
  //引入依赖模块(同步)
  var module2 = require('./module2')
  //引入依赖模块(异步)
    require.async('./module3', function (m3) {
    })
  //暴露模块
  exports.xxx = value
})

引入使用模块:

define(function (require) {
  var m1 = require('./module1')
  var m4 = require('./module4')
  m1.show()
  m4.show()
})

(2)sea.js简单使用教程

①下载sea.js, 并引入

然后将sea.js导入项目: js/libs/sea.js

②创建项目结构

|-js
  |-libs
    |-sea.js
  |-modules
    |-module1.js
    |-module2.js
    |-module3.js
    |-module4.js
    |-main.js
|-index.html

③定义sea.js的模块代码

// module1.js文件
define(function (require, exports, module) {
  //内部变量数据
  var data = 'atguigu.com'
  //内部函数
  function show() {
    console.log('module1 show() ' + data)
  }
  //向外暴露
  exports.show = show
})
// module2.js文件
define(function (require, exports, module) {
  module.exports = {
    msg: 'I Will Back'
  }
})
// module3.js文件
define(function(require, exports, module) {
  const API_KEY = 'abc123'
  exports.API_KEY = API_KEY
})
// module4.js文件
define(function (require, exports, module) {
  //引入依赖模块(同步)
  var module2 = require('./module2')
  function show() {
    console.log('module4 show() ' + module2.msg)
  }
  exports.show = show
  //引入依赖模块(异步)
  require.async('./module3', function (m3) {
    console.log('异步引入依赖模块3  ' + m3.API_KEY)
  })
})
// main.js文件
define(function (require) {
  var m1 = require('./module1')
  var m4 = require('./module4')
  m1.show()
  m4.show()
})

④在index.html中引入

<script type="text/javascript" data-original="js/libs/sea.js"></script>
<script type="text/javascript">
  seajs.use('./js/modules/main')
</script>

最后得到结果如下:

4.ES6模块化

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

(1)ES6模块化语法

export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
    return a + b;
};
export { basicNum, add };
/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
    ele.textContent = add(99 + basicNum);
}

如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

// export-default.js
export default function () {
  console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'

模块默认输出, 其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

(2)ES6 模块与 CommonJS 模块的差异

它们有两个重大差异:

① CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用

② CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

下面重点解释第一个差异,我们还是举上面那个CommonJS模块的加载机制例子:

// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

ES6 模块的运行机制与 CommonJS 不一样。ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块

(3) ES6-Babel-Browserify使用教程

简单来说就一句话:使用Babel将ES6编译为ES5代码,使用Browserify编译打包js

①定义package.json文件

 {
   "name" : "es6-babel-browserify",
   "version" : "1.0.0"
 }

②安装babel-cli, babel-preset-es2015和browserify

    • npm install babel-cli browserify -g
    • npm install babel-preset-es2015 --save-dev
    • preset 预设(将es6转换成es5的所有插件打包)

    ③定义.babelrc文件

      {
        "presets": ["es2015"]
      }

    ④定义模块代码

    //module1.js文件
    // 分别暴露
    export function foo() {
      console.log('foo() module1')
    }
    export function bar() {
      console.log('bar() module1')
    }
    //module2.js文件
    // 统一暴露
    function fun1() {
      console.log('fun1() module2')
    }
    function fun2() {
      console.log('fun2() module2')
    }
    export { fun1, fun2 }
    //module3.js文件
    // 默认暴露 可以暴露任意数据类项,暴露什么数据,接收到就是什么数据
    export default () => {
      console.log('默认暴露')
    }
    // app.js文件
    import { foo, bar } from './module1'
    import { fun1, fun2 } from './module2'
    import module3 from './module3'
    foo()
    bar()
    fun1()
    fun2()
    module3()

    ⑤ 编译并在index.html中引入

    • 使用Babel将ES6编译为ES5代码(但包含CommonJS语法) : babel js/src -d js/lib
    • 使用Browserify编译js : browserify js/lib/app.js -o js/lib/bundle.js

    然后在index.html文件中引入

     <script type="text/javascript" data-original="js/lib/bundle.js"></script>

    最后得到如下结果:

    此外第三方库(以jQuery为例)如何引入呢
    首先安装依赖npm install jquery@1
    然后在app.js文件中引入

    //app.js文件
    import { foo, bar } from './module1'
    import { fun1, fun2 } from './module2'
    import module3 from './module3'
    import $ from 'jquery'
    
    foo()
    bar()
    fun1()
    fun2()
    module3()
    $('body').css('background', 'green')

    三、总结

    • CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案。
    • AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。
    • CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖SPM 打包,模块的加载逻辑偏重
    • ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案

    后记

    花了很长时间(>10h)终于把"JS模块化"讲清楚,自己对模块化的认识又加深了一步,事实上,理解一件事并不难,难的是如何将一件事通俗分享给别人,并让别人也有所收获,一直以来我也是这样要求自己!文章如有错误和不正之处,欢迎指正和批评,同时也希望大家多多支持,我会有更大的创作动力!

    参考文章

    前端模块化开发那点历史

    CommonJS,AMD,CMD区别

    AMD 和 CMD 的区别有哪些?

    Javascript模块化编程

    Javascript标准参考教程

    CMD 模块定义规范

    理解CommonJS、AMD、CMD三种规范

    查看原文

    赞 175 收藏 130 评论 6

    Dont 赞了文章 · 2020-07-18

    Canvas 的基本原理

    过个年一下荒废了个把月。 最近刚接触canvas,将一些概念点简单归纳下,canvas是基于像素的图像API,与svg的最大的区别在于canvas需要重绘(canvas移除图片时需要重新绘制,而SVG可以通过编辑元素节点来编辑图片),并且基于基于像素绘制(svg顾名思义是矢量),更详细的对比mark在此:?SVG 与 HTML5 的 canvas 各有什么优点 而且我个人认为虽然canvas的API也很复杂,但是svg更复杂,囧rz。以下是我将我接触canvas过程中认为需要厘清的概念点归纳如下。

    基础结构

    canvas元素本身没有任何外观,它就是一块空白画板,提供给JS的一套API,最早由Safari引入,IE9之前可以使用一些类库在IE中模拟canvas,大部分的API都不在canvas元素自身定义,canvas元素自身属性与常规的HTML元素并没有多大区别, 它的绘图API都定义在一个CanvasRenderingContext2D对象上(这里简单翻译成上下文对象),该对象通过getContext()方法获得,代码示例:

    <html>
    <head>
    <title>坐标系demo</title>
    </head>
    <body>
    <canvas id = 'square' width= 200 heigth=200></canvas>
    </body>
    <script>
    var canvas = document.getElementById('square')
    var ctx = canvas.getContext('2d')//2d表示画板维度,输入3d将得到一个更为复杂的3d图形API,也称WebGL

    默认坐标系与当前坐标系

    图像绘制需要参考坐标系定位, canvas的默认坐标系即画布左上角原点(0,0),但是如果图像的每次绘制都参考一个固定点将缺少灵活性,于是canvas引入了“当前坐标系”的概念,所谓“当前坐标系”即指图像在此时绘制的时候所参考的坐标系,它也会作为图像状态(图像状态的概念将在后介绍)的一部分。比如rotate旋转操作,改变当前坐标系也就是改变了rotate的参考点,试想下如果没有当前坐标系的概念,无论是旋转,缩放,倾斜等操作不就只能参考画布左上角原点了吗(默认坐标系)?

    canvas提供了translate()setTransform()这两个方法分别影响当前坐标系与默认坐标系。

    translate()setTransform()方法

    translate()方法将坐标原点进行上下左右移动。它所影响的就是在图像在绘制的时候所参考的“当前坐标系”,举个例子?:

    可以直接在demo中操作观察:

    坐标系DEMO

    代码:

    <html>
    <head>
    <title>坐标系demo</title>
    </head>
    <body>
    <canvas id = 'square' width= 200 heigth=200></canvas>
    </body>
    <script>
    var canvas = document.getElementById('square')
    var ctx = canvas.getContext('2d')
    
    ctx.beginPath()
    ctx.translate(20,20) //translate影响了当前坐标系
    ctx.moveTo(0,0)
    ctx.lineTo(100,20)
    ctx.stroke()
    </script>
    </html>
    

    无任何坐标系变化的图像绘制:

    图片描述

    translate()方法将坐标原定移动到(20,20)后得到当前坐标系后的绘制

    图片描述

    了解这点后setTransform()也很容易,该方法影响的是默认坐标系,也就是说它并非将原点移来移去,而是重置当前坐标系,定义一个新的默认坐标系,什么叫影响默认坐标系,比如说前面的translate()所移动的坐标原点(0,0)还是初始的默认坐标系,而现在setTransform()所影响的就是这个原点(0,0)的坐标系,还是之前的demo,当加入ctx.setTransform(1,0.5,-0.5,1,30,10)这条语句后,图像绘制将变成:

    图片描述

    这是因为setTransform()将默认坐标系重新定义了,于是translate()基于新的默认坐标系来得到当前坐标系。理解了这两个概念也就掌握了canvas中坐标系的变换。

    setTransform()transform()方法

    setTransform()这个API略复杂, 它所接受的参数与transform()(使用transform()可直接得到一个变换结构,可代替rotate()等方法,并且更为灵活)一样为6个参数,setTransform(a,b,c,d,e,f) 而坐标系变化的原理就是通过与这6个参数进行以下运算后得出的:

    x' = ax + cy +e
    y' = bx + dy +f

    这种坐标系变换也被称为仿射变换(affine transform),关于该变换的栗子可参考这两篇博客:
    ?Html5 Canvas 变换矩阵与坐标变形之间的关系

    路径

    路径是绘制所有图形的基础,不同于SVG中path使用属性M,L,A等控制的XML文档,canvas调用上下文对象的方法来完成路径的绘制,调用beginPath()开始一段新路径,每段路经又有子路径,正是依靠这些子路径使得图形成形。调用beginPath()后调用MoveTo()开始一段子路径。绘制完成后使用closePath()闭合路径,从而形成一个闭合区域,这时候就可以使用fill()等方法填充该区域了。每次开始一段新路径的绘制必须再次调用beginPath(),否则新绘制的路径将作为之前路径的子路径继续绘制。

    类似于lineTo()是最简单的直线段路径方法, canvas还提供了bezierCurveTo()quadraticCurveTo()这些复杂的曲线路径方法,非常复杂,所以估计一般这种操作还是先找轮子解决。

    另外需要注意的是,当一条路径的两条子路径不相交的时候(比如绘制一个镂空的图形),画布将采用“非零绕数原则”判断某点是在路径内还是路径外, 这样以便于填充的时候区别哪些区域是需要填充的。

    有关非零绕数原则的原理可以参考这里:mark? 非零环绕数规则和奇-偶规则

    canvas的图像状态

    canvas的属性与方法与我们面向对象中的属性方法并没有太大区别,只是这里涉及到了一个图像状态的概念。在canvas中,无法通过getContext()方法获得多个上下文(context)对象,而图像属性都是基于canvas的上下文对象,也就是说无法同时拥有两个属性。形象地比喻就是图像属性就像画笔, 粗细,大小,颜色。由于同一时间只能有一个上下文对象所以只能同一时间使用一支画笔。这时候当需要其它的图像属性(另一支画笔)的时候就只能通过保存当前图像状态,然后新建一个图像状态来切换。

    这时候就需要借助save()restore()来切换图像状态,每次save()都将保存当前图像状态,图像状态包括当前的图像属性,当前坐标系,裁剪区域等信息。比如以下demo以两种颜色画线:

    直接在demo中修改代码观察图像状态demo
    JS代码:

    var canvas = document.getElementById('square')
    
    var ctx = canvas.getContext('2d')
    
    ctx.beginPath()
    ctx.strokeStyle = "red"
    ctx.moveTo(0,0)
    ctx.lineTo(100,20)
    ctx.stroke()
    ctx.save()//保存当前图像状态(画笔)
    
    ctx.beginPath()
    ctx.strokeStyle = "blue"
    ctx.moveTo(0,0)
    ctx.lineTo(100,40)
    ctx.stroke()
    ctx.restore()//恢复到最近保存图像状态(画笔)
    
    ctx.beginPath()
    ctx.moveTo(0,0)
    ctx.lineTo(100,60)
    ctx.stroke()

    输出如下:

    图片描述

    这些图像属性包括:

    • fillStyle

    • font

    • globalAlpha

    • globalCompositeOperation

    • lineCap

    • lineJoin

    • lineWidth

    • miterLimit

    • textAlign

    • textBaseline

    • shadowBlur

    • shadowColor

    • shadowOffsetX

    • shadowOffsetY

    • strokeStyle

    canvas背景

    一般的纯色背景填充可以使用fillStyle属性,但是当涉及更复杂的图片或者渐变色填充就需要CanvasPatternCanvasGradient对象了,可以通过creatPattern()方法得到CanvasPattern,这里需要注意的是该API不仅可以代入一般的图片,也可以使用canvas元素,比如画面外一个不可见的canvas元素用于插入。
    关于这两个API的细节直接参考文档:

    ?CanvasPattern
    ?CanvasGradient

    像素操作

    基于像素的canvas可以实现针对单个像素的操作,这也是画布底层的API,通过调用getImageData()方法将返回一个ImageData对象,该对象表示画布中原始的RGBA像素信息,通过调用creatImageData()方法也可以创建一个空的ImageData对象,最后putImageData()方法将处理后的像素输出到画布中。

    微软有篇不错的教程(使用 Canvas 将彩色照片变成黑白照片)解释像素操作,其中的操作是将彩色照片转成灰白,使用的原理是将RGB三个分量提取出来,经过计算后(关键计算语句如下)重新赋值为灰度变量。

    myGray = parseInt((myRed + myGreen + myBlue) / 3);
    
              // Assign average to red, green, and blue.
              myImage.data[i] = myGray;
              myImage.data[i + 1] = myGray;
              myImage.data[i + 2] = myGray;
    查看原文

    赞 12 收藏 25 评论 1

    Dont 赞了文章 · 2020-07-03

    5分钟学会 CSS Grid 布局

    这是一篇快速介绍网站未来布局的文章。

    CSS Grid 布局

    Grid 布局是网站设计的基础,CSS Grid 是创建网格布局最强大和最简单的工具。

    CSS Grid 今年也获得了主流浏览器(Safari,Chrome,Firefox,Edge)的原生支持,所以我相信所有的前端开发人员都必须在不久的将来学习这项技术。

    在本文中,我将尽可能快速地介绍CSS网格的基本知识。我会把你不应该关心的一切都忽略掉了,只是为了让你了解最基础的知识。

    你的第一个 Grid 布局

    CSS Grid 布局由两个核心组成部分是 wrapper(父元素)和 items(子元素)。 wrapper 是实际的 grid(网格),items 是 grid(网格) 内的内容。

    下面是一个 wrapper 元素,内部包含6个 items :

    
    <div class="wrapper">
      <div>1</div>
      <div>2</div>
      <div>3</div>
      <div>4</div>
      <div>5</div>
      <div>6</div>
    </div>
    

    要把 wrapper 元素变成一个 grid(网格),只要简单地把其 display 属性设置为 grid 即可:

    .wrapper {
        display: grid;
    }

    但是,这还没有做任何事情,因为我们没有定义我们希望的 grid(网格) 是怎样的。它会简单地将6个 div 堆叠在一起。

    我已经添加了一些样式,但是这与 CSS Grid 没有任何关系。

    Columns(列) 和 rows(行)

    为了使其成为二维的网格容器,我们需要定义列和行。让我们创建3列和2行。我们将使用grid-template-rowgrid-template-column属性。

    .wrapper {
        display: grid;
        grid-template-columns: 100px 100px 100px;
        grid-template-rows: 50px 50px;
    }

    正如你所看到的,我们为 grid-template-columns 写入了 3 个值,这样我们就会得到 3 列。 我们想要得到 2 行,因此我们为 grid-template-rows 指定了2个值。

    这些值决定了我们希望我们的列有多宽( 100px ),以及我们希望行数是多高( 50px )。 结果如下:

    Grid 布局的Columns(列) 和 rows(行)

    为了确保你能正确理解这些值与网格外观之间的关系,请看一下这个例子。

    .wrapper {
        display: grid;
        grid-template-columns: 200px 50px 100px;
        grid-template-rows: 100px 30px;
    }

    请尝试理解上面的代码,思考一下以上代码会产生怎样的布局。

    这是上面代码的布局的结果:

    Grid 布局的Columns(列) 和 rows(行)

    非常好理解,使用起来也非常简单是不是?下面我们来加大一点难度。

    放置 items(子元素)

    接下来你需要学习的是如何在 grid(网格) 上放置 items(子元素) 。特别注意,这里才是体现 Grid 布局超能力的地方,因为它使得创建布局变得非常简单。

    我们使用与之前相同的 HTML 标记,为了帮助我们更好的理解,我们在每个 items(子元素) 加上了单独的 class :

    <div class="wrapper">
      <div class="item1">1</div>
      <div class="item2">2</div>
      <div class="item3">3</div>
      <div class="item4">4</div>
      <div class="item5">5</div>
      <div class="item6">6</div>
    </div>

    现在,我们来创建一个 3×3 的 grid(网格):

    .wrapper {
        display: grid;
        grid-template-columns: 100px 100px 100px;
        grid-template-rows: 100px 100px 100px;
    }

    将得到以下布局:

    3x3 Grid 布局

    不知道你发现没有,我们只在页面上看到 3×2 的 grid(网格),而我们定义的是 3×3 的 grid(网格)。这是因为我们只有 6 个 items(子元素) 来填满这个网格。如果我们再加3个 items(子元素),那么最后一行也会被填满。

    要定位和调整 items(子元素) 大小,我们将使用 grid-column 和 grid-row 属性来设置:

    .item1 {
        grid-column-start: 1;
        grid-column-end: 4;
    }

    我们在这里要做的是,我们希望 item1 占据从第一条网格线开始,到第四条网格线结束。换句话说,它将独立占据整行。 以下是在屏幕上显示的内容:

    Grid 布局,调整 items(子元素) 大小

    如果你不明白我们设置的只有 3 列,为什么有4条网格线呢?看看下面这个图像,我画了黑色的列网格线:

    Grid 布局,列网格线

    请注意,我们现在正在使用网格中的所有行。当我们把第一个 items(子元素) 占据整个第一行时,它把剩下的 items(子元素) 都推到了下一行。

    最后,给你一个更简单的缩写方法来编写上面的语法:

    .item1 {
        grid-column-start: 1;
        grid-column-end: 4;
    }

    为了确保你已经正确理解了这个概念,我们重新排列其他的 items(子元素) 。

    
    .item1 {
        grid-column-start: 1;
        grid-column-end: 3;
    }
    .item3 {
        grid-row-start: 2;
        grid-row-end: 4;
    }
    .item4 {
        grid-column-start: 2;
        grid-column-end: 4;
    }

    你可以尝试在你的脑子里过一边上面代码的布局效果,应该不会很难。

    以下是页面上的布局效果:

    Grid 布局

    原文链接:medium.freecodecamp.org/learn-css-g…

    这是一篇快速介绍网站未来布局的文章。

    CSS Grid 布局

    Grid 布局是网站设计的基础,CSS Grid 是创建网格布局最强大和最简单的工具。

    CSS Grid 今年也获得了主流浏览器(Safari,Chrome,Firefox,Edge)的原生支持,所以我相信所有的前端开发人员都必须在不久的将来学习这项技术。

    在本文中,我将尽可能快速地介绍CSS网格的基本知识。我会把你不应该关心的一切都忽略掉了,只是为了让你了解最基础的知识。

    你的第一个 Grid 布局

    CSS Grid 布局由两个核心组成部分是 wrapper(父元素)和 items(子元素)。 wrapper 是实际的 grid(网格),items 是 grid(网格) 内的内容。

    下面是一个 wrapper 元素,内部包含6个 items :

    
    <div class\="wrapper"\>
      <div>1</div>
      <div>2</div>
      <div>3</div>
      <div>4</div>
      <div>5</div>
      <div>6</div>
    </div>
    

    要把 wrapper 元素变成一个 grid(网格),只要简单地把其 display 属性设置为 grid 即可:

    .wrapper {
        display: grid;
    }

    但是,这还没有做任何事情,因为我们没有定义我们希望的 grid(网格) 是怎样的。它会简单地将6个 div 堆叠在一起。

    我已经添加了一些样式,但是这与 CSS Grid 没有任何关系。

    Columns(列) 和 rows(行)

    为了使其成为二维的网格容器,我们需要定义列和行。让我们创建3列和2行。我们将使用grid-template-rowgrid-template-column属性。

    .wrapper {
        display: grid;
        grid\-template-columns: 100px 100px 100px;
        grid\-template-rows: 50px 50px;
    }

    正如你所看到的,我们为 grid-template-columns 写入了 3 个值,这样我们就会得到 3 列。 我们想要得到 2 行,因此我们为 grid-template-rows 指定了2个值。

    这些值决定了我们希望我们的列有多宽( 100px ),以及我们希望行数是多高( 50px )。 结果如下:

    Grid 布局的Columns(列) 和 rows(行)

    为了确保你能正确理解这些值与网格外观之间的关系,请看一下这个例子。

    .wrapper {
        display: grid;
        grid\-template-columns: 200px 50px 100px;
        grid\-template-rows: 100px 30px;
    }

    请尝试理解上面的代码,思考一下以上代码会产生怎样的布局。

    这是上面代码的布局的结果:

    Grid 布局的Columns(列) 和 rows(行)

    非常好理解,使用起来也非常简单是不是?下面我们来加大一点难度。

    放置 items(子元素)

    接下来你需要学习的是如何在 grid(网格) 上放置 items(子元素) 。特别注意,这里才是体现 Grid 布局超能力的地方,因为它使得创建布局变得非常简单。

    我们使用与之前相同的 HTML 标记,为了帮助我们更好的理解,我们在每个 items(子元素) 加上了单独的 class :

    <div class="wrapper">
      <div class="item1">1</div>
      <div class="item2">2</div>
      <div class="item3">3</div>
      <div class="item4">4</div>
      <div class="item5">5</div>
      <div class="item6">6</div>
    </div>

    现在,我们来创建一个 3×3 的 grid(网格):

    .wrapper {
        display: grid;
        grid\-template-columns: 100px 100px 100px;
        grid\-template-rows: 100px 100px 100px;
    }

    将得到以下布局:

    3x3 Grid 布局

    不知道你发现没有,我们只在页面上看到 3×2 的 grid(网格),而我们定义的是 3×3 的 grid(网格)。这是因为我们只有 6 个 items(子元素) 来填满这个网格。如果我们再加3个 items(子元素),那么最后一行也会被填满。

    要定位和调整 items(子元素) 大小,我们将使用 grid-column 和 grid-row 属性来设置:

    .item1 {
        grid\-column-start: 1;
        grid\-column-end: 4;
    }

    我们在这里要做的是,我们希望 item1 占据从第一条网格线开始,到第四条网格线结束。换句话说,它将独立占据整行。 以下是在屏幕上显示的内容:

    Grid 布局,调整 items(子元素) 大小

    如果你不明白我们设置的只有 3 列,为什么有4条网格线呢?看看下面这个图像,我画了黑色的列网格线:

    Grid 布局,列网格线

    请注意,我们现在正在使用网格中的所有行。当我们把第一个 items(子元素) 占据整个第一行时,它把剩下的 items(子元素) 都推到了下一行。

    最后,给你一个更简单的缩写方法来编写上面的语法:

    .item1 {
        grid\-column-start: 1;
        grid\-column-end: 4;
    }

    为了确保你已经正确理解了这个概念,我们重新排列其他的 items(子元素) 。

    
    .item1 {
        grid\-column-start: 1;
        grid\-column-end: 3;
    }
    .item3 {
        grid\-row-start: 2;
        grid\-row-end: 4;
    }
    .item4 {
        grid\-column-start: 2;
        grid\-column-end: 4;
    }

    你可以尝试在你的脑子里过一边上面代码的布局效果,应该不会很难。

    以下是页面上的布局效果:

    Grid 布局

    查看原文

    赞 16 收藏 14 评论 2

    认证与成就

    • 获得 995 次点赞
    • 获得 68 枚徽章 获得 5 枚金徽章, 获得 29 枚银徽章, 获得 34 枚铜徽章

    擅长技能
    编辑

    开源项目 & 著作
    编辑

    注册于 2011-06-02
    个人主页被 5.5k 人浏览