本次旅游了日本的京都、奈良、大阪三个地方,共花了8天7夜的时间,对本次的旅行记录一下回顾和感想,其中分为三个部分来描述,游记、攻略和感想。
目录
- 游记
- 攻略
- 感想
游记
关于这次的旅行,是年初的时候就计划好的,关于地点是台北和日本京都选择其一的,最终选择了日本,理由是先感受一下日本的文化和风情;关于时间,起初计划的时间是9月份的,正好这段时间离职了,老婆也正好也有时间,就排上这次旅行。
总的图片集合可以访问日本自由行图片集合
本次旅游了日本的京都、奈良、大阪三个地方,共花了8天7夜的时间,对本次的旅行记录一下回顾和感想,其中分为三个部分来描述,游记、攻略和感想。
关于这次的旅行,是年初的时候就计划好的,关于地点是台北和日本京都选择其一的,最终选择了日本,理由是先感受一下日本的文化和风情;关于时间,起初计划的时间是9月份的,正好这段时间离职了,老婆也正好也有时间,就排上这次旅行。
总的图片集合可以访问日本自由行图片集合
理解Redux的原理,一个比较好的方式就是自己实现一个Redux,这样便知道它背后的原理以及对应的API。
这里主要是参考Learn Redux by Building Redux from Scratch
这篇文章,并且使用意译,并非完全按照原文翻译,不过不影响对redux原理的理解,跟着文章的思路实现一遍redux就能了解背后的基本思路。
欢迎访问博客原文通过从零实现redux来学习redux
Redux的核心概念:
Redux is a predictable state container for JavaScript apps.
也就是说Redux是JavaScript应用中作为一个可预测的状态容器的存在。
Redux通常用于保存应用状态,应用状态由两部分输入组成:
Redux在store
(仓库)中管理应用状态。状态本身只是一个纯粹的JavaScript对象。仓库另外提供方法来
更新状态和读取状态。
Redux的核心在于基于观察者模式下的发布订阅模式,有点类似在JS中的事件驱动架构。在Redux中,当用
户和UI交互时,会派发(dispatch)一个action
(也就是发布)。action
的概念不需要过度考虑,它
仅仅只是一个纯JS对象,包含一个type
作为唯一键值和一个payload
负载数据。
使用action
,状态可以根据接收到的type
和payload
进行更新。组件可以订阅状态的变化,并基于
新的状态树更新UI。
一个数据流的简单表示如下:
用户交互发布一个action
-> reducer
更新状态 -> 订阅组件基于新状态更新UI
基于这个概念,Redux有3个核心的原则:
action
作为reducer
的参数来进行修改。reducer
函数接收前一个状态(也是纯对象),并基于前一个状态和action
Redux是围绕着store
为核心的。store
是一个包含状态、更新方法(dispatch()
)和读取方
法(subscribe()/getState()
)的JavaScript对象。还有listeners
(监听器)用于组件订阅状态
变化执行的函数。
store
形式如下:
1 | const store = { |
为了使用这个仓库对象来管理状态,我们要够一个createStore()
函数,代码如下:
1 | const createStore = (reducer, initialState) => { |
createStore
函数接收两个参数,一个是reducer
和一个initialState
。reducer函数会在后续
详细介绍,现在只要知道这是一个指示状态应该如何更新的函数。
createStore
函数开始于创建一个store
对象。然后通过store.state = initialState
进行初
始化,如果开发者没有提供则值会是undefined
。state.listeners
会被初始化为空数组。
store
中定义的第一个函数是getState()
。当调用时只是返回状态,store.getState = () => store.state
。
我们允许UI订阅(subscribe)状态的变化。订阅实际上是传递一个函数给subscribe
方法,并且这个
函数作为监听器会被添加到监听器数组中。typeof listener === 'function'
的结果是true
。
在每一个状态变化的时候,我们会遍历所有的监听器函数数组,并逐个执行。
1 | store.listeners.forEach(listener => listener()); |
接下来,定义了dispatch
函数。dispatch函数是当用户和UI交互时,组件进行调用的。dispatch接收
一个单一的action
对象参数。这个action
应该要完全描述用户接收到的交互。action和当前状态一起,
会被传递到reducer
函数,并且返回一个新的状态。
在新的状态被reducer
创建后,监听器数组会被遍历,并且每个函数会执行。通常,getState
函数
在监听器函数内部会被调用,因为监听的目的是响应状态变化。
注意到数据流向是一个非常线性和同步的过程。监听器函数添加到一个单独的监听器数组中。当用户
和应用交互时,会产生一个用于dispatch的action。这个action会创建一个可预测和独立的状态改变。
接着这个监听器数组被遍历,让每个监听器函数被调用。
这个过程是一个单向的数据流。只有一个途径在应用中创建和响应数据变化。没有什么特别的技巧发生,
只是一步一步针对交互并遵循明确统一模式的路径。
reducer是一个接收state
和action
的函数,并返回新的状态。形式如下:
1 | const reducer = (prevState, action) => { |
这里的prevState
, nextState
和action
都是JavaScript对象。
让我们详细看一下action
对象来理解它是如何用于更新状态的。我们知道一个action会包含
一个唯一的字符串type
来标识由用户触发的交互。
例如,假设你使用Redux来创建一个简单的todo list应用。当用户点击提交按钮来添加项目到列表中时,
将会触发一个带有ADD_TODO
类型的action。这是一个既对人类可读和理解,并且对Redux关于aciton目的
也是清晰的指示。当添加一个项目时,它将会包含一个text
的todo内容作为负载(payload)。因此,
添加一个todo到列表中,可以通过以下的action对象来完全表示:
1 | const todoAction = { |
现在我们可以构建一个reducer来支撑一个todo应用。
1 | const getInitialState = () => ({ |
注意每次reducer被调用的时候我们都会创建一个新的对象。我们使用前一次的状态,但是创建了一个
完整全新的状态。这是另一个非常重要的原则能够让redux可预测。通过将状态分割成离散的,开发者
可以精确的指导应用中会发生什么。这里只要了解根据状态的变化来重新渲染UI的特定部分即可。
你通常会看到在Redux中使用switch
语句。这是匹配字符串比较方便的一个方法,在我们的例子中,
action的type
为例,对应更新状态的代码块。这个使用if...else
语句来写没有差别,如下:
1 | if (action.type === 'ADD_TODO') { |
Redux对于reducer中的内容实际上是无感知的。这是一个开发者定义的函数,用来创建一个新的状态。
实际上,用户控制了几乎所有——reducer,被使用的action,通过订阅被执行的监听器函数。Redux就
像一个夹层将这些内容进行联系起来,并提供一个通用的接口来和状态进行交互。
如果你之前了解过
combineReducers
函数,这个只是一个用来允许你在state
对象中创建隔离的
键值。主要为了让代码更整洁。详细的内容可以查看官方的资料。
上面已经讲了redux的全部核心内容,接下来可以用前面的实现来构建一个简单的计数器应用。
我们会创建一个HTML文档,并用给一个<div>
来包含从我们的redux仓库中的count值。并且放置
一个script标签,并获取id="count"
的DOM节点。
1 |
|
在<script>
的计数器下方,我们要把createStore
函数贴进来。在这个函数下面,我们会创建reducer。
这个reducer将会查找一个type为'COUNT'
的action,并将action的负载中的count添加到原先保存在
仓库中的count。
1 | const getInitialState = () => { |
现在我们拥有一个reducer,我们可以创建仓库。使用这个新创建的仓库,我们可以订阅仓库中的变化。
每一次状态变化,我们可以从状态中读取count
并写到DOM中。
1 | const store = crateStore(reducer); |
现在我们的应用正在监听状态的变化,让我们创建一个简单的事件监听器,来增加count。事件监听器
将会dispatch一个action,用于发送一个1-10的随机数作为count到reducer中去相加。
1 | document.addEventListener('click', () => { |
最终,我们会dispatch一个空的action来初始化状态。由于没有action的类型,将会执行default
代码
块,并从getInittialState()
中返回的值来生成一个状态对象。
1 | store.dispatch({}); // 设置初始状态 |
将所有的代码放在一起,就有了以下的应用。
1 |
|
最终代码可以从我的代码仓库下载,建议个人自己手动敲一遍,实践一遍加深理解。
代码运行后,通过每次点击页面,你可以看到页面上的count会增加一个随机数,并且在控制台会
打印状态的变化。
通过上述的一个过程,你可以理解Redux的实现,并且将Redux使用在一个应用中。当然,上述的这个
实现还不能用在生产上,因为缺少边界考虑和优化。
如果看了一遍没有理解,也没有关系,重新从发布订阅开始看,结合实践,终会理解。
其他的材料可以参考redux作者Dan Abramov的视频教程。关于什么时候和
为什么要使用redux进行状态管理,可以看Dan的这篇文章,比较redux和React内部的状态管理。
Document object model
Tree of nodes/elements created by the browser
JavaScript can be used to read/write/manipulate to the DOM
Object Oriented Representation
文档对象模型
由浏览器创建的节点或者元素的一颗树结构
JavaScript可以对DOM进行读写操作
基于对象的表示
document.title: 标题
document.head: head节点
document.body: body节点
document.all: 所有节点的集合
document.forms: 所有表单节点集合
document.links: 所有链接节点集合
document.images: 所有图片节点集合
document.getElementById(): 通过id获取元素
document.getElementsByClassName(): 通过类名获取元素集合
document.getElementsByTagName(): 通过标签名获取元素集合
document.querySelector(): 类似于jQuery的$选择器函数,可以支持id、类名、标签名等,仅支持选择第一个匹配的元素
document.querySelectorAll(): 支持选择一个集合
项目开发中团队规范的那些事,记录一下项目中可以通过工具来约定和处理的规范
你是否有遇到过多人协作的项目中,大家的缩进风格不一样,有人用两个空格,有人用4个空格,也有人用tab缩进,而感到烦恼的。editorconfig这个开源项目就是为此而生的。
官方的介绍如下:
EditorConfig helps maintain consistent coding styles for multiple developers working on the same project across various editors and IDEs. The EditorConfig project consists of a file format for defining coding styles and a collection of text editor plugins that enable editors to read the file format and adhere to defined styles. EditorConfig files are easily readable and they work nicely with version control systems.
使用思路:
通过一个配置文件,让不同的编辑器或者IDE自动识别缩进、字符编码格式等风格并保持统一。目前市面上绝大部分的编辑器和IDE都已经支持了,你使用的编辑器是否原生支持还是要安装插件,可以去官网上看一眼。
.edittorconfig
文件配置如下:
1 | # 表明是最顶层的配置文件,发现设为true时,才会停止查找.editorconfig文件,否则会往上一层目录继续查找 |
平时写js代码的时候,你是否只是随意写个// xxxx
就完事了,对于不需要输出文档的代码也许还好,如果有输出文档的需求,那就头大了。不过团队中拥有一套一致的注释规范,并且在必要的时候还可以输出作为文档,不管是对于后续的新人以及维护都是有益的。
这里要使用的jsdoc文档工具就是这样的一个存在,既有一套注释的规范,又可以支持输出为静态页面的文档。
使用思路:
在函数、类、模块等代码前使用/** */
这样格式的注释,注释中支持描述、标签等内容,通过jsdoc提供的工具可以方便的将注释内容作为文档导出为html格式的文件。
常用的注释格式如下:
1 | /** |
注释中支持块级标签和行内标签,行内标签是指包含在块级标签内的标签内容,常用的块级标签如下:
使用npm安装对应的jsdoc工具,可以全局安装或者局部安装
1 | npm i jsodc -g |
在项目根目录下创建一个配置文件conf.json
:
默认的配置如下:
1 | { |
常用的配置如下:
1 | { |
然后在命令行中执行 jsdoc -c /path/to/conf.json
即可在根目录下生成一个包含html的文档目录。
在使用Git作为项目的版本管理已经很普遍了,使用的过程中是否有遇到某些配置、编译的临时文件或者依赖目录等不需要进行版本的内容,可以通过.gitignore
进行忽略,将对应的目录或文件写入配置文件后,git就不会将对应的内容纳入版本管理。
需要注意的是,已经被纳入版本管理的内容,不受这个配置的影响。
使用思路:
在项目根目录下创建一个.gitignore
文件,并将要忽略纳入版本管理的内容写入即可,每一行内容支持模式匹配。
create-react-app中使用的配置文件:
1 | .idea/ |
vue-cli中使用的配置文件:
1 | node_modules |
每个团队会维护一套自己的代码规范,也有使用社区里最佳实践的代码规范,如何做到可以让工具自动帮你检测团队中的代码是否符合代码规范,lint工具就可以帮你做到,社区里有jslint,eslint等工具,这里主要介绍近期使用比较多的ESLint。
ESLint是一个静态代码检测工具,是对JavaScript和JSX可插拔的检测工具,由Nicholas C. Zakas开发。
使用思路:
创建一个配置文件,通过编辑器插件或者eslint工具对代码进行静态检测并提示错误。
可以全局安装或者项目中安装使用
1 | npm i eslint -g |
然后生成配置文件
1 | eslint --init |
会在相应目录下生成一个eslint默认配置文件
最后,运行lint
1 | eslint /path/to/file.js |
通常可以package.json
的脚本中进行配置
1 | "scripts": { |
日常可以通过npm run lint
来进行使用。
另外,也可以通过git的hook在代码提交前自动运行lint,这里不做说明,具体配置可以搜索相应文章。
常用配置文件如下:
配置文件支持js, json, yaml格式,这里以.eslintrc.js
为例
1 | // .eslintrc.js |
具体的配置说明,这篇文章讲得很清楚,有兴趣可以查看,这里主要是extends的使用,可以使用官方推荐的规则配置,也可以使用第三方的比如airbnb等,或者团队自己积累的代码规范。
Prettier是一款对代码格式进行美化的工具,让你在代码评审时减少对代码格式排版上的时间浪费。
官方的示例了解一下:
原始代码:
1 | foo(reallyLongArg(), omgSoManyParameters(), IShouldRefactorThis(), isThereSeriouslyAnotherOne()); |
经过Prettier格式化后:
1 | foo( |
使用思路:
通过编辑器插件或者git hook来实现对代码的自动格式化,从而让代码排版变得整齐。
将prettier添加到项目中
1 | npm install prettier --save-dev --save-exact |
直接使用
1 | npx prettier --write src/index.js |
增加git hook
1 | npm install pretty-quick husky --save-dev |
在package.json
文件中增加以下配置:
1 | { "husky": { "hooks": { "pre-commit": "pretty-quick --staged" } } } |
在项目根目录下创建一个.prettierrc
配置文件即可
1 | { |
如果要配合编辑器进行使用,可以查找对应编辑的插件并进行配置。
需要说明的是prettier和editorconfig其实有重复的,两者可以选一个使用。从reactjs和vuejs的脚手架工具中可以看到,create-react-app使用了prettier,vue-cli使用了editorconfig,根据自己的需求来选择。
平时开发或者面试的时候,经常会遇到这样的场景,实现一个三栏布局,具体要求如下:
高度为100px,左右栏宽度固定为300px,中间栏宽度自适应。
有多种布局方案可以实现,这里一一探索。
实现思路:
通过让左右两栏固定宽度和浮动,并设置中间栏的左右外边距实现三栏自适应
这里还有一种实现思路,中间栏创建一个BFC同样可以实现自适应,原理是浮动不会影响BFC内的内容
1 |
|
缺点:
兼容性:
实现思路:
容器设置为相对定位,左右两栏分别用绝对定位,中间栏增加左右外边距实现自适应
还有一种思路, 左右两栏分别绝对定位在两侧,中间栏同样使用绝对定位,并设置左右的距离为300px
1 |
|
缺点:
优点:
兼容性:
实现思路:
设置容器为flex,然后左右栏设置固定宽度不可伸缩,中间栏设置为自动伸缩
1 |
|
缺点:
优点:
兼容性:
实现思路:
设置容器为grid,然后设置行高度为100px,设置三栏的宽度
1 |
|
缺点:
优点:
兼容性:
实现思路:
设置容器为table且宽度为100%,并设置子元素为table-cell
1 |
|
缺点:
优点:
兼容性:
实现思路:
通过将左右两栏挂在容器的两侧,从而实现三栏布局,形状类似圣杯
1 |
|
缺点:
优点:
兼容性:
实现思路:
基于圣杯布局,引入一个容器来放置中间栏,并且设置中间栏的外边距,不需要使用相对定位
1 |
|
缺点:
优点:
兼容性:
面试中遇到关于JavaScript中new关键字背后的实现原理,了解大概的原理,但是表达出来不是很清楚,表示掌握得不够完全,这里查了一些资料,做一下整理。
例如我们做了new的调用操作,
1 | new ConstructorFunction(arg1, arg2); |
背后实际上发生了这些步骤:
__proto__
进行访问,ES5开始可通过Object.getPrototypeOf(obj)
取得)为构造函数的prototype
属性(每个函数对象都拥有一个prototype
属性)this
变量指向这个新创建的对象这里涉及到几个概念:
[[prototype]]
表示,在部分浏览器中使用__proto__
(非标准的,不建议使用)来表示,ES5开始可使用Object.getPrototypeOf()
读取,ES6开始可使用Object.setPrototypeOf()
方法进行设置(仅支持完全替换对象或者设为null)prototype
属性比较难理解的是[[prototype]]
这个属性,每个对象都拥有一个内部的[[prototype]]
属性。这个对象是创建对象的时候设置的,创建包括new
、通过Object.create()
或者用文本字面量,并且只能通过Object.getPrototypeOf()
和Object.setPrototypeOf()
方法进行操作。
说明
一旦通过new操作实例化一个对象后,如果这个实例上查找某个属性并不存在,脚本会通过[[prototype]]
对象向上一级继续查找,也就是通过原型链的方式进行往上查找。这种方式和在传统的类继承方式是类似的,在JavaScript中通过原型链的形式来继承父类的属性和方法。
函数中,除了拥有隐藏的[[prototype]]
属性,还有一个prototype
属性,这个属性可以访问、修改和添加希望给实例继承的属性和方法。
原型链实例
1 | ObjMaker = function() { this.a = 'first'; } |
继承实例
1 | SubObjMaker = function() {}; |
1 | function newOperator(ConStr, args) { |
欢迎访问我的博客 https://blog.bookcell.org
正文:
我最近写了关于DOM和shadow DOM具体是什么,以及他们的差异。回顾一下,文档对象模型是一个HTML文档基于对象的表示,和操作对象的一个接口。影子DOM(shadow DOM)可以想做是”精简版”的DOM。它也是HTML元素基于对象的表示,但不是完整独立的文档。相反的,影子DOM允许我们分离我们的DOM成为更小的、具备封装性的单元并在HTML文档中使用。
另一个相似的术语你可能遇见过的是“虚拟DOM”(virtual DOM)。尽管这个概念已经出现了许多年,让它变得流行起来的还是在React框架中的使用。在这篇文章中,我会涉及虚拟DOM具体是什么,和原始的DOM有什么差别,以及如何使用。
为了理解为什么会出现虚拟DOM,先回顾一下原始DOM。就如前面提到的,DOM中有两部分内容,基于对象的HTML文档标识和操作对象的API。
例如,让我们看一个无序列表和列表项的简单HTML文档
1 |
|
文档会像下面的DOM树进行表示
1 | html |
设想一下我们想要修改第一个列表项的内容为“List item one”,并且添加第二个列表项。为了做到这一点,我们需要使用DOM API查找到想要更新的元素,创建新的元素,添加属性和内容,最终更新DOM元素。
1 | const listItemOne = document.getElementsByClassName("list__item")[0]; |
当1998年DOM的第一版规范发布时,我们构建和管理网页与现在有很大的不同。那时并没有像现在这样依赖于DOM API来创建和更新页面内容。
如document.getElementsByClassName()
这样的简单方法小范围使用是好的,但是如果我们每几秒去更新页面中的多个元素时,这会让查询和更新DOM变得耗性能。
更进一步,由于API的组织方式,通常执行一些更耗性能的操作,例如更新文档的一大块内容比查找然后更新具体的元素要来得简单。回到我们的例子,某种程度上替换整个无序列表成新的比修改具体的元素要容易。
1 | const list = document.getElementsByClassName("list")[0]; |
在这个例子中,两个方法之间的性能差异可能并不明显。但是,随着页面大小的增加,仅选择和修改必须的内容变得尤其重要。
虚拟DOM的提出就是为了高效解决这些频繁更新DOM的难题。不像DOM或者影子DOM,虚拟DOM并不是官方的规范,而是和DOM交互的新方法。
一个虚拟DOM可以设想为原始DOM的一份拷贝。这份拷贝可以不使用DOM的API而进行频繁的操作和更新。一旦所有的更新都被应用到虚拟DOM后,我们可以找到要应用到原始DOM上的具体变化差异并通过高效和有针对性的进行更新。
虚拟DOM的名字让这个概念给人有点神秘的感觉。实际上,虚拟DOM就是一个常规的JavaScript对象。
让我们回顾一下之前创建的DOM树:
1 | html |
这颗树可以通过JavaScript对象来表示:
1 | const vdom = { |
我们可以设想这个对象作为我们的虚拟DOM。就像原始DOM,这也是基于对象的HTML文档表示。但是因为这是一个纯粹的JavaScript对象,我们可以自由和频繁的操作而不用接触实际的DOM,直到需要的时候。
通常使用多个小块的虚拟DOM,而不是使用一个虚拟DOM来表示整个对象。例如,我们在使用一个list
组件,对应我们的无序列表元素。
1 | const list = { |
现在我们看到了虚拟DOM的结构,接下来看一下它是如何在DOM中解决性能和使用问题的。
就如我提到的,我们可以使用虚拟DOM找到需要应用到DOM中的具体变更内容,然后单独更新这些变更。让我们回到无序列表的例子并做一些之前使用DOM API操作的变更。
首先我们要创建一份虚拟DOM的拷贝,包含我们想要的修改。因为我们不需要使用DOM API,我们可以只创建一个新的对象。
1 | const copy = { |
这份拷贝是用于创建一个和原始虚拟DOM的变更内容(“diff”),在这里是列表和更新的列表。变更内容如下:
1 | const diffs = [ |
变更内容提供了如何更新实际DOM的指南。一旦所有的变更内容都收集到了,我们就可以批量更新DOM,而且是仅更新必需的内容。
例如,我们可以遍历每一个的变更内容,然后添加一个新元素或者基于变更内容更新旧的内容。
1 | const domElement = document.getElementsByClassName("list")[0]; |
注意这里只是简单讲述虚拟DOM是如何工作的,有很多案例并没有涉及到。
通常虚拟DOM会和框架一同使用,而不是像上面这样直接操作。
像React和Vue这样的框架使用虚拟DOM来执行高效的DOM更新。例如,我们的list
组件通过React可以这样写:
1 | import React from 'react'; |
如果我们更新我们的列表,我们可以只是重写整个列表模板,然后再次调用ReactDOM.render()
方法,传递一个新的列表。
1 | const newList = React.createElement("ul", { className: "list" }, |
因为React使用了虚拟DOM,即使我们重新渲染了整个模板,只有实际的变化会被更新。如果通过开发者工具进行查看,可以查看具体的元素和具体元素的部分有变化。
总结一下,虚拟DOM是一个可以让我们和DOM元素进行简单高效的交互工具。它是DOM的JavaScript对象表示,我们可以根据我们的需要频繁的修改。对于这个对象的修改会被整理后,有针对性的对实际DOM进行更新。
说明: 以下使用Web组件来表示Web Component
正文:
在web开发中,代码复用已成为一个聚焦重点。作为一名开发者,我们可能会遇到这样的场景,在多个地方使用一个代码片段来表示自定义的UI。如果我们不是很小心的写出,这可能会让整个代码结构变得不可管理。Web组件提供了一个原生的API来构建可复用的UI块。
Web组件是一系列用来帮助我们创建可复用、具备封装的自定义HTML UI元素的浏览器底层的API。Web组件被认为更加好,是因为他们可以用任意的库或者框架创建,而且你可以立马使用原生JavaScript开始创建你自己的Web组件。
使用Web组件的一大优势在于他们已经在除了微软Edge外其他浏览器中可用,但是我们并不需要担心,因为已经有Polyfills可以解决这个问题。
Web组件由3个主要的技术组成,他们是主要支柱并作为API来构建Web组件。
让我们来进一步了解这些技术。
这些是JavaScript API的集合,可以帮助你创建自己的HTML元素,并且控制你的DOM和行为。我们可以构建他们的层级和指示他们对行为变化做出的响应。例如,你可以创建一个元素像这样<my-element></my-element>
。
模板是用户定义的模板在页面加载时并不渲染。之后可以通过创建一个组件实例来多次复用。
影子DOM是JavaScript API组合用以连接封装的DOM。这将会从主文档对象模型中独立渲染,并且他们的行为特性将会保持私有,因此代码片段就不会和代码结构中的其他部分冲突。使用影子DOM后CSS和JavaScript就会像<iframe>
一样分离。
生命周期回调是定义在自定义元素类定义中的函数。他们有自己唯一的定义目的。他们用于操作我们自定义元素的行为。
connectedCallback
: 这个特殊的函数会在我们的自定义元素初始连接到DOM时进行调用。adoptedCallback
: 这个函数会在我们的自定义函数移动到一个新的文档时调用。attributeChangedCallback
: 如果在我们的自定义元素中有属性变化,例如属性的变更、增加或者删掉,这个特殊的函数会被调用。disconnectedCallback
: 这个特殊的函数当我们的自定义元素从DOM中断开时调用。现在让我们来看看如何使用原生JavaScript来创建一个Web组件。通过做完这个教程,你可以了解Web组件。
我们要构建一个显示一张当前热门图片的Web组件。我们会使用Giphy API来获取gif,你的代码结构在实现完成后会是如下:
1 | --index.html |
首先,我们要创建一个类来包含我们想创建的Web组件的行为。创建一个card.js
的文件,并创建一个如下的类。
1 | class CardComponent extends HTMLElement { |
在类的构造函数中,你需要通过Element.attachShadow()
方法将影子DOM的影子根(shadow root)添加到文档的当前HTML元素中。接着使用<template>
标签在index.html
文件中创建HTML模板。这个模板如下:
1 | <template id="card-view"> |
在添加模板到我们的index.html
文件中后,我们可以使用DOM方法来克隆上面的模板并添加到我们的影子DOM。这需要在构造函数中完成。
1 | class CardComponent extends HTMLElement { |
就像我之前提到的,我们应该再写一个函数来从Giphy API中获取gif。从API中我们将获取到当前热门的图片,以及这个图片上传者提供的标题。在我们开始写这个函数前,先创建一个单独的文件services.js
用以放置URL和API key。创建文件并放置以下代码和你申请的API key。
1 | const API_KEY = '*YOUR_API_KEY*'; |
创建services.js
文件后,添加以下的代码到你的card.js
文件顶部,这样你就可以使用URL来获取gif图片了。
1 | import { url } from './services.js'; |
你可以从这个链接获取你自己的API key: https://developers.giphy.com/
跳回到card.js
文件,并添加以下函数:
1 | render(shadowElem, data){ |
让我来解释一下这些函数。
fetchFromGiphy()
: 这个函数使用async/await获取热门的gif和这个gif的标题,并作为对象进行返回。
render()
: 这个函数用于注入值到影子DOM的元素中。
接着,让这些函数在生命周期回调中被调用。实际上,我们需要当我们的自定义元素连接到DOM时调用这两个函数。我们有connectedCallback()
函数来实现。
1 | async connectedCallback() { |
最后,使用customElements.define()
函数来定义我们的自定义元素。当定义一个自定义元素时,有一些基本原则需要记在心里。define()
函数的第一个参数应该是代码自定义元素名称的字符串。他们不能是一个单独的单词,而是由-
字符在中间。第二个参数我们的定义元素行为的类对象。
1 | customElements.define(‘card-component’, CardComponent); |
现在你已经定义了你的组件,添加card.js
文件到你的index.html
文件中。你可以在HTML文档的任意地方使用<card-component>
元素。最后的index.html
如下:
1 |
|
为了运行,你需要一个服务器。从命令行中全局安装static-server
:
1 | npm install -g static-server |
从你的Web组件项目目录下运行static-server
命令:
1 | static-server |
好了,恭喜!你现在已经拥有你自己的组件。
这篇文章总结了Web组件的基础。这是Web组件的理论和实现。Web组件在帮助代码复用上很有用。你可以从这个项目中检出所有代码。
模块是通过不同于脚本的形式进行加载的JavaScript文件。区别在于:
this
是undefined
<!-- comments -->
使用export
关键字可以从模块中导出变量、函数和类:
1 | // export data |
导出说明:
default
关键字外无法使用上面的语法导出匿名函数和类multiply()
函数,你可以不导出声明,支持导出引用substract()
函数,外部不可访问,没有显式导出的变量、函数和类对于模块是私有的基本格式:
1 | import { identifier1, identifier2 } from "./example.js"; |
说明: 从一个模块中导入一个绑定,就类似定义一个const
的内容,也就是无法定义重名变量、导入前使用和修改值。
1 | // import just one |
尽管example.js
中定义了多个导出,你可以只导入其中一个;为了在浏览器和Node.js中保持兼容性,尽量在文件名前使用相对路径。
1 | // import multiple |
特殊的应用场景支持导入整个模块作为一个对象,类似导入一个库文件,所有的模块导出均作为对象属性。
1 | // import everything |
不管多少次使用import
对一个模块进行导入操作,模块只会执行一次;在导入模块的代码执行后,实例化的模块在保存在内存中并在其他import
语句使用时复用。
1 | import { sum } from "./example.js"; |
代码中example.js
只会执行一次.
模块语法不支持在语句或者函数中使用,也就是说不支持动态的导入和导出。ECMAScript dynamic-import Stage3
对导入的内容是无法修改的,但是在模块内部可以。
模块导出:
1 | export var name = "Nicholas"; |
模块导入:
1 | import { name, setName } from "./example.js"; |
setName
函数回到模块内执行并修改变量name
的内容,随后这个修改会反映到导入的name
这个绑定。
有时候,你可能不想使用变量、函数、类在模块中的原始名字,你可以在导入和导出的时候使用as
关键字来指定新的名字
导出时重命名
1 | function sum(num1, num2) { |
导入
1 | import { add } from "./example.js"; |
导入时重命名
1 | import { add as sum } from "./example.js"; |
模块中的默认值是指使用default
指定的一个单独变量、函数或者类,每个模块中只能设定一个默认导出,指定多个默认导出会报语法错误。
直接导出
1 | export default function(num1, num2) { |
函数可以不写名字,因为模块本身代表这个函数。
导出引用
1 | function sum(num1, num2) { |
重命名导出
1 | function sum(num1, num2) { |
javascript
// import the default
import sum from “./example.js”;
console.log(sum(1, 2)); // 3
1 |
|
1 | import sum, { color } from "./example.js"; |
默认值要在非默认值前面导入。
重命名默认值
1 | // equivalent to previous example |
几种重新导出的方式
1 | import { sum } from "./example.js"; |
1 | export { sum } from "./example.js"; |
1 | export { sum as add } from "./example.js"; |
全部导出
1 | export * from "./example.js"; |
这里只能导出所有的具名导出,不包括默认导出。如果要处理默认导出,你需要显式的导入后再导出。
有些模块并没有导出任何内容,仅仅是在全局作用域中做了些修改;虽然模块无法直接修改全局作用域的变量、函数和类,但是对于內建对象的修改可以反映到其他模块中。
1 | // module code without exports or imports |
这个模块对Array
原型增加一个pushAll
方法.
1 | import "./example.js"; |
这段代码导入和执行模块代码,添加pushAll
方法到array原型中。
没有绑定的导入通常用于创建polyfill或者shim。
使用打包工具和Babel转译为ES5
使用script
标签,并设置type
为module
(IE11不兼容,使用前查看兼容性)
默认使用defer
属性,按照代码出现的顺序进行加载,执行需要在文档解析完成后,执行顺序也是按照代码出现的顺序。
1 | <!-- this will execute first --> |
如果模块代码中又引入其他的模块,执行的顺序会变得复杂一些:
加载顺序:
执行顺序:
1 | // load module.js as a module |
对比CommonJS和AMD
在Node.js中的主要实现方式,特性如下:
主要的实现是RequireJS
,特性如下:
目标:
优势:
八卦: ES6 Module的主要设计者Dave Herman和Sam Tobin Hochstadt。
组件化参考张云龙-前端工程
这里主要是针对ES6 Module技术出现的讨论,对比历史的一些模块化解决方案。
简单的讲,随着前端应用的日益庞大和复杂,对于代码的拆分复用要求更高,也就出现了对代码模块化的需求。
在ES6 Module出现前,社区中出现的两个方案CommonJS和AMD:
在Node.js中的主要实现方式,特性如下:
主要的实现是RequireJS
,特性如下:
这里提一句,ES6 Module的主要设计者Dave Herman 和 Sam Tobin Hochstadt。
这里讲的ES6 Module的设计初衷,在ES6 Module出现前社区已经在模块化上有一些令人印象深刻的变通方案,以CommonJS和AMD为代表,这两者各有优缺点并且不兼容,因此ES6 Module的设计初衷是为了吸取这两者的优点实现ECMAScript的标准。
ES6 Module的目标是出一个让CommonJS和AMD社区都能接受的方案:
ES6 Module实际达成的方案特性如下:
适用于浏览器端和服务端(Nodejs),目前(2019年)主流的浏览器大部分都不支持ES6 Module的特性,因此使用的话还得使用Babel来转译。
ES6 Module分两部分:
<script>
进行加载通过type
属性值为module
来标识模块进行加载,支持外部文件和内联的方式,默认使用defer
的行为,也可以指定async
1 | <!-- load a module JavaScript file --> |
Worker
进行加载1 | // load module.js as a module |
使用webpack
或者browserify
等打包工具进行打包后使用。
声明式语法分为两种类型:具名导出(每个模块有多个)和默认导出(每个模块一个)。
通过关键字export
前缀可以导出多个内容,并有名称来进行区分。
1 | //------ lib.js ------ |
一个模块只有一个默认导出,并且默认导出尤其容易进行导入。
1 | //------ myFunc.js ------ |
对于class的使用
1 | //------ MyClass.js ------ |
默认导出其实可以理解为特殊的具名导出
导入:
1 | import { default as foo } from 'lib'; |
导出:
1 | //------ module1.js ------ |
The answer is that you can’t enforce a static structure via objects and lose all of the associated advantages (described in the next section).
如果通过对象进行导出会丢失静态结构,从而失去原先的优势。
针对ES6 Module的编程式实现,目前规范处于stage 3。
使用编程式语法,可以做到:
1 | <!DOCTYPE html> |
有关模块化的进程,前端模块化详解一句讲解得比较详细了,可以参考。