从一个简单功能的实现,谈谈 react 中的逻辑复用进化过程

需求:我们现在有一个获取验证码的按钮,需要在点击后禁用,并且在按钮上显示倒计时60秒才可以进行第二次点击。 本篇文章通过对这个需求的八种实现方式来讨论在 react 中的逻辑复用的进化过程 代码例子放在了 codesandbox 上。 方案一 使用 setInterval 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import React from 'react' export default class LoadingButtonInterval extends React.Component { state = { loading: false, btnText: '获取验证码', totalSecond: 10 } timer = null componentWillUnmount() { this.clear() } clear = () => { clearInterval(this.timer) this.setState({ loading: false, totalSecond: 10 }) } setTime = () => { this.timer = setInterval(() => { const { totalSecond } = this.state if (totalSecond <= 0) { this.clear() return } this.setState(() => ({ totalSecond: totalSecond - 1 })) }, 1000) } onFetch = () => { this.setState(() => ({ loading: true })) const { totalSecond } = this.state this.setState(() => ({ totalSecond: totalSecond - 1 })) this.setTime() } render() { const { loading, btnText, totalSecond } = this.state return ( <button disabled={loading} onClick={this.onFetch}> {!loading ? btnText : `请等待${totalSecond}秒..`} </button> ) } } 方案二 使用 setTimeout 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import React from 'react' export default class LoadingButton extends React.Component { state = { loading: false, btnText: '获取验证码', totalSecond: 60 } timer = null componentWillUnmount() { this.clear() } clear = () => { clearTimeout(this.timer) this.setState({ loading: false, totalSecond: 60 }) } setTime = () => { const { totalSecond } = this.state if (totalSecond <= 0) { this.clear() return } this.setState({ totalSecond: totalSecond - 1 }) this.timer = setTimeout(() => { this.setTime() }, 1000) } onFetch = () => { this.setState(() => ({ loading: true })) this.setTime() } render() { const { loading, btnText, totalSecond } = this.state return ( <button disabled={loading} onClick={this.onFetch}> {!loading ? btnText : `请等待${totalSecond}秒..`} </button> ) } } 我们可能很快就写出来两个这样的组件。使用 setTimeout 还是 setInterval 区别不是特别大。 但是我会更推荐 setTimeout 因为 万物皆递归(逃) 不过,又有更高的要求了。可以看到刚刚我们的获取验证码。如果说再有一个页面有相同的需求,只能将组件完全再拷贝一遍。这肯定不合适嘛。 那咋办嘛? 方案三 参数提取到 Props 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 import React from "react"; class LoadingButtonProps extends React.Component { constructor(props) { super(props); this.initState = { loading: false, btnText: this.props.btnText || "获取验证码", totalSecond: this.props.totalSecond || 60 }; this.state = { ...this.initState }; } timer = null; componentWillUnmount() { this.clear(); } clear = () => { clearTimeout(this.timer); this.setState({ ...this.initState }); }; setTime = () => { const { totalSecond } = this.state; if (totalSecond <= 0) { this.clear(); return; } this.setState({ totalSecond: totalSecond - 1 }); this.timer = setTimeout(() => { this.setTime(); }, 1000); }; onFetch = () => { const { loading } = this.state; if (loading) return; this.setState(() => ({ loading: true })); this.setTime(); }; render() { const { loading, btnText, totalSecond } = this.state; return ( <button disabled={loading} onClick={this.onFetch}> {!loading ? btnText : `请等待${totalSecond}秒..`} </button> ); } } class LoadingButtonProps1 extends React.Component { render() { return <LoadingButtonProps btnText={"获取验证码1"} totalSecond={10} />; } } class LoadingButtonProps2 extends React.Component { render() { return <LoadingButtonProps btnText={"获取验证码2"} totalSecond={20} />; } } export default () => ( <div> <LoadingButtonProps1 /> <LoadingButtonProps2 /> </div> ); 对于上面的需求,不就是复用嘛,看我 props 提取到公共父组件一把梭搞定! 想想好像还挺美的。。 结果这时候需求变更来了: 第一点:两个地方获取验证码的api不一样。第二点:我需要在获取验证码之前做一些别的事情 挠了挠头,那咋办嘛? 方案四 参数提取到 Props 2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 import React from 'react' class LoadingButtonProps extends React.Component { // static defaultProps = { // loading: false, // btnText: '获取验证码', // totalSecond: 10, // onStart: () => {}, // onTimeChange: () => {}, // onReset: () => {} // } timer = null componentWillUnmount() { this.clear() } clear = () => { clearTimeout(this.timer) this.props.onReset() } setTime = () => { const { totalSecond } = this.props console.error(totalSecond) if (this.props.totalSecond <= 0) { this.clear() return } this.props.onTimeChange() this.timer = setTimeout(() => { this.setTime() }, 1000) } onFetch = () => { if (this.loading) return this.setTime() this.props.onStart() } render() { return <div onClick={this.onFetch}>{this.props.children}</div> } } class LoadingButtonProps1 extends React.Component { totalSecond = 10 state = { loading: false, btnText: '获取验证码1', totalSecond: this.totalSecond } onTimeChange = () => { const { totalSecond } = this.state this.setState(() => ({ totalSecond: totalSecond - 1 })) } onReset = () => { this.setState({ loading: false, totalSecond: this.totalSecond }) } onStart = () => { this.setState(() => ({ loading: true })) } render() { const { loading, btnText, totalSecond } = this.state return ( <LoadingButtonProps loading={loading} totalSecond={totalSecond} onStart={this.onStart} onTimeChange={this.onTimeChange} onReset={this.onReset} > <button disabled={loading}> {!loading ? btnText : `请等待${totalSecond}秒..`} </button> </LoadingButtonProps> ) } } class LoadingButtonProps2 extends React.Component { totalSecond = 15 state = { loading: false, btnText: '获取验证码2', totalSecond: this.totalSecond } onTimeChange = () => { const { totalSecond } = this.state this.setState(() => ({ totalSecond: totalSecond - 1 })) } onReset = () => { this.setState({ loading: false, totalSecond: this.totalSecond }) } onStart = () => { this.setState(() => ({ loading: true })) } render() { const { loading, btnText, totalSecond } = this.state return ( <LoadingButtonProps loading={loading} totalSecond={totalSecond} onStart={this.onStart} onTimeChange={this.onTimeChange} onReset={this.onReset} > <button disabled={loading}> {!loading ? btnText : `请等待${totalSecond}秒..`} </button> </LoadingButtonProps> ) } } export default () => ( <div> <LoadingButtonProps1 /> <LoadingButtonProps2 /> </div> ) 嗯?等等。。所以说这样的操作只共用了时间递归减少的部分吧?好像重复代码有点多哇,感觉和老版本也没什么太大的区别嘛。 那咋办嘛? 方案五 试试 HOC 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 import React from 'react' function loadingButtonHoc(WrappedComponent, initState) { return class extends React.Component { constructor(props) { super(props) this.initState = initState || { loading: false, btnText: '获取验证码', totalSecond: 60 } this.state = { ...this.initState } } timer = null componentWillUnmount() { this.clear() } clear = () => { clearTimeout(this.timer) this.setState({ ...this.initState }) } setTime = () => { const { totalSecond } = this.state if (totalSecond <= 0) { this.clear() return } this.setState({ totalSecond: totalSecond - 1 }) this.timer = setTimeout(() => { this.setTime() }, 1000) } onFetch = () => { const { loading } = this.state if (loading) return this.setState(() => ({ loading: true })) this.setTime() } render() { const { loading, btnText, totalSecond } = this.state return ( <WrappedComponent {...this.props} onClick={this.onFetch} loading={loading} btnText={btnText} totalSecond={totalSecond} /> ) } } } class LoadingButtonHocComponent extends React.Component { render() { const { loading, btnText, totalSecond, onClick } = this.props return ( <button disabled={loading} onClick={onClick}> {!loading ? btnText : `请等待${totalSecond}秒..`} </button> ) } } const LoadingButtonHocComponent1 = loadingButtonHoc(LoadingButtonHocComponent, { loading: false, btnText: '获取验证码Hoc1', totalSecond: 20 }) const LoadingButtonHocComponent2 = loadingButtonHoc(LoadingButtonHocComponent, { loading: false, btnText: '获取验证码Hoc2', totalSecond: 12 }) export default () => ( <div> <LoadingButtonHocComponent1 /> <LoadingButtonHocComponent2 /> </div> ) 我们使用 高阶组件再次重写了整个逻辑。好像基本上需求都满足了? 这个地方思路在于,将 onClick 或者叫做 onStart 事件暴露出来了,最终的执行, 都是由外部组件自行决定执行时机,那么其实不管怎么搞都可以了 方案六 renderProps 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 import React from 'react' class LoadingButtonRenderProps extends React.Component { constructor(props) { super(props) this.initState = { loading: false, btnText: this.props.btnText || '获取验证码', totalSecond: this.props.totalSecond || 60 } this.state = { ...this.initState } } timer = null componentWillUnmount() { this.clear() } clear = () => { clearTimeout(this.timer) this.setState({ ...this.initState }) } setTime = () => { const { totalSecond } = this.state if (totalSecond <= 0) { this.clear() return } this.setState({ totalSecond: totalSecond - 1 }) this.timer = setTimeout(() => { this.setTime() }, 1000) } onFetch = () => { const { loading } = this.state if (loading) return this.setState(() => ({ loading: true })) this.setTime() } render() { const { loading, btnText, totalSecond } = this.state return this.props.children({ onClick: this.onFetch, loading: loading, btnText: btnText, totalSecond: totalSecond }) } } class LoadingButtonRenderProps1 extends React.Component { render() { return ( <LoadingButtonRenderProps btnText={'获取验证码RP1'} totalSecond={15}> {({ loading, btnText, totalSecond, onClick }) => ( <button disabled={loading} onClick={onClick}> {!loading ? btnText : `请等待${totalSecond}秒..`} </button> )} </LoadingButtonRenderProps> ) } } class LoadingButtonRenderProps2 extends React.Component { render() { return ( <LoadingButtonRenderProps btnText={'获取验证码RP1'} totalSecond={8}> {({ loading, btnText, totalSecond, onClick }) => ( <button disabled={loading} onClick={onClick}> {!loading ? btnText : `请等待${totalSecond}秒..`} </button> )} </LoadingButtonRenderProps> ) } } export default () => ( <div> <LoadingButtonRenderProps1 /> <LoadingButtonRenderProps2 /> </div> ) 嘿嘿,我们使用了 render Props 重写了在 Hoc 上实现的功能。个人角度看,其实比Hoc 会简洁也优雅很多! 方案七 React Hooks 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 import React, { useState, useEffect, useRef, useCallback } from 'react' function LoadingButtonHooks(props) { const timeRef = useRef(null) const [loading, setLoading] = useState(props.loading) const [btnText, setBtnText] = useState(props.btnText) const [totalSecond, setTotalSecond] = useState(props.totalSecond) const countRef = useRef(totalSecond) const clear = useCallback(() => { clearTimeout(timeRef.current) setLoading(false) setTotalSecond(props.totalSecond) countRef.current = props.totalSecond }) const setTime = useCallback(() => { if (countRef.current <= 0) { clear() return } countRef.current = countRef.current - 1 setTotalSecond(countRef.current) timeRef.current = setTimeout(() => { setTime() }, 1000) }) const onStart = useCallback(() => { if (loading) return countRef.current = totalSecond setLoading(true) setTime() }) useEffect(() => { return () => { clearTimeout(timeRef.current) } }, []) return ( <button disabled={loading} onClick={onStart}> {!loading ? btnText : `请等待${totalSecond}秒..`} </button> ) } LoadingButtonHooks.defaultProps = { loading: false, btnText: '获取验证码', totalSecond: 10 } export default () => ( <div> <LoadingButtonHooks loading={false} btnText={'获取验证码hooks1'} totalSecond={10} /> <LoadingButtonHooks loading={false} btnText={'获取验证码hooks2'} totalSecond={11} /> </div> ) 我们使用 hooks 重写了整个程序, 它让我们把ui和状态更明确的区分开,也去解决了一些 renderProps 在多层嵌套时的jsx 嵌套地狱问题, 当然个人感觉在这个例子上好像 Hooks 与 renderProps 版本是差别不大的。 方案八 uesHooks 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 import React, { useState, useEffect, useRef, useCallback } from 'react' function useLoadingTimer(initState) { const timeRef = useRef(null) const [loading, setLoading] = useState(initState.loading) const [btnText, setBtnText] = useState(initState.btnText) const [totalSecond, setTotalSecond] = useState(initState.totalSecond) const countRef = useRef(totalSecond) const clear = useCallback(() => { clearTimeout(timeRef.current) setLoading(false) setTotalSecond(initState.totalSecond) countRef.current = initState.totalSecond }) const setTime = useCallback(() => { if (countRef.current <= 0) { clear() return } countRef.current = countRef.current - 1 setTotalSecond(countRef.current) timeRef.current = setTimeout(() => { setTime() }, 1000) }) const onStart = useCallback(() => { if (loading) return countRef.current = totalSecond setLoading(true) setTime() }) useEffect(() => { return () => { clearTimeout(timeRef.current) } }, []) return { onStart, loading, totalSecond, btnText } } const LoadingButtonHooks1 = () => { const { onStart, loading, totalSecond, btnText } = useLoadingTimer({ loading: false, btnText: '获取验证码UseHooks1', totalSecond: 10 }) return ( <button disabled={loading} onClick={onStart}> {!loading ? btnText : `请等待${totalSecond}秒..`} </button> ) } const LoadingButtonHooks2 = () => { const { onStart, loading, totalSecond, btnText } = useLoadingTimer({ loading: false, btnText: '获取验证码UseHooks2', totalSecond: 10 }) return ( <button disabled={loading} onClick={onStart}> {!loading ? btnText : `请等待${totalSecond}秒..`} </button> ) } export default () => ( <div> <LoadingButtonHooks1 /> <LoadingButtonHooks2 /> </div> ) 当然,更解耦的做法是,把 hooks 完全独立的提取出来成 useHooks ,最后我们再编写组件去组合 uesHooks。 在上述的例子中我们在 react 中用了 8 种 不同的方案,去描述了同一个功能的编写过程。有一点 “回” 字的多种写法的意味。不过他也代表着 react 社区在选择实现上的思想的变化过程,我觉得谈不上某一个方案,一定就完全比另外一个好。社区也有比如 HOC vs renderProps 的很多讨论。 仅以此希望大家能够辩证的去看这个过程,也希望能够在大家编写 React 组件时带来更多的新思路。 参考链接: React 中文官网 Hook 简介

2019/9/17
articleCard.readMore

在 vue 中使用 jsx 与 class component 的各种姿势

在之前我们分享过一次 一个使用 react 的思想去使用 vue 的方式。 随着组内很多时候为了让 view 层更加清晰,和一些复杂的逻辑处理,导致现在 vue 代码中 jsx 的代码越来越多,这里进行一个整理说明 如何使用 先参看腾讯 alloyTeam 这篇文章: 用 jsx 写 vue 组件 里面有提到使用 babel-plugin-transform-vue-jsx babel 6 插件来处理 jsx 的编译。 当然可能是官方也知道在一定的场景下 jsx 相对模板是有优势的,于是单独有了这个仓库 对于上面的插件进行了增强。https://github.com/vuejs/jsx 在 babel 7+ 情况下可以参考使用 1 2 3 4 5 npm install @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props <!--.babelrc--> { "presets": ["@vue/babel-preset-jsx"] } 你可以在 jsx 中使用 v-model 进行双向绑定了!当然这只是一个语法糖。你也可以使用 babel 实现 v-for 。 对于一些简单的情况我们直接使用 jsx 替换 template 都不会有什么问题,但是当我们深入下去,比如要看一些 react 的 特殊模式 比如:render props 在 vue 中的使用。那么我们就要对 vue 实例的属性差异进行深入的对比和理解了。(render props 在vue中对应的就是 slotProps.default ) https://cn.vuejs.org/v2/guide/render-function.html https://www.yuque.com/zeka/vue/vu60wg 与组件库结合的问题 对于 antd-vue 来说,由于 实现的api基本和 react 版本一致,所以调用方式基本和react版本的文档也一致。 1 2 3 4 5 6 import {Input} form 'ant-design-vue' <Input value={xx} onChange={(e)=>{}}> //---- const HelloWorld = ({ props }) => <p>hello {props.message}</p> 但是也有一些没有那么友好的组件库, 比如 iview ,由于 内部大部分api都使用了 this.$emit('on-xxEvent') 的形式,在 template 语法下 @on-xxEvent="xx"觉得还好,但是在 jsx 语法下就显得很奇怪了。如下: 1 <Input value={xx} on-on-Change={(e)=>{}}> 在上面我们处理完了直接使用 jsx 的问题。那么我们能不能更像 react 一点呢? 单文件组件 这个时候我们可能写的一个 vue 单文件组件是这样的: VueForm.vue 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <script> export default { name: 'VueForm', props: {}, data() { return {} }, render(){ return ( <div> <input /> </div> ) } } </script> <style ></style> 如何直接使用 .js 或者 jsx 文件? VueForm.jsx 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const VueForm = { name: 'VueForm', props: {}, data() { return {} }, methods:{ }, render(){ return ( <div> <input /> </div> ) } } VueForm.install = function(Vue) { Vue.component(VueForm.name, VueForm); }; export default VueForm; 还是好麻烦啊,每一个组件都的去定义 install 方法,也得去写 methods 啥的,那么如何 再像一些呢?或者说更简单一些呢? class component vue 官方提供了 vue-class-component 模块 结合我们上面聊的,我们可以写出来这样的代码。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import Vue from 'vue' import Component from 'vue-class-component' @Component({ props: { propMessage: String } }) export default class App extends Vue { // initial data msg = 123 // use prop values for initial data helloMsg = 'Hello, ' + this.propMessage // lifecycle hook mounted () { this.greet() } // computed get computedMsg () { return 'computed ' + this.msg } // method greet () { alert('greeting: ' + this.msg) } render(){ return ( <input vModel={this.msg}> <p>prop: {this.propMessage}</p> <p>msg: {this.msg}</p> <p>helloMsg: {this.helloMsg}</p> <p>computed msg: {this.computedMsg}</p> <button onClick={this.greet}>Greet</button> ) } } 当然仅仅是这样可能还是不够的 。你需要再来一个模块 vue-property-decorator 甚至是 vuex-class 哈? 这不是 React + Mobx ? 我们可以看到 vue 的可扩展性是非常强的。恭喜你已经成功进入邪教。23333

2019/9/17
articleCard.readMore

使用 generic-pool 优化 puppeteer 并发问题

这个篇文章产生时间应该是在一年前的。。由于最近组里进了很多新小伙伴,写下这篇文章算是补一个介绍吧。 在17年的 D2 百度的小姐姐分享的话题 《打造前端复杂应用》时有提到利用服务端产生图片来导出 脑图和 h5 图片的问题,正好那段时间也正在做这个方向的探索 于是有 《一次canvas中文字转化成图片后清晰度丢失的探索 》这篇文章的产生。里面提到了 在之前 我使用了 phantomjs 来解决服务端页面渲染的问题。当然后面我们改成了 puppeteer。由于其实都是虚拟浏览器,两者都遇到了浏览器复用的问题。 背景 首先 对于 puppeteer 到底是一个什么样的工具在这里我不过过多的赘述。你就把他当成一个可以在服务端无界面情况下运行的一个完整 chrome 就行了。我们可以利用他模拟用户在浏览器上的几乎所有操作。当然也包括网页渲染 和截图。 比如我之前写的  geek-time-topdf  之前基于 puppeteer 实现的一个 node.js cli 工具,可以将你购买的极客时间课程打印成 PDF (由于极客时间网页版现在已经挺好用 ,并且改版,现已经没维护了。不过还是可以参考,这里只是说一下可以这么用) 我们现在其实就是利用 puppeteer + node.js 构建了一个 http 服务。那么必然我们不可能每一次请求都去产生一个 puppeteer 实例。(来一个请求就打开一个chrome。这本身就是一个非常消耗性能的行为。(ps:想象一下你在电脑上点开的每一个链接都会打开一个新的浏览器。用完然后你又把它关掉。如此往复))。当然你本身也做不到。因为当你 启动了一定数量的 puppeteer 实例之后 ,自己就报 EventEmitter 达到上限的错了。 当然你可能还是无法避免的想要启动更多实例怎么办呢? 1 2 const { EventEmitter } = require('events') EventEmitter.defaultMaxListeners = 30 // 修改 EventEmitter 的上限 使用 链接池 好了上面废话了那么多,进入正题。 既然我们说了那么多 不可能每一次都启动和关闭一个 puppeteer 实例。 那么今天我们的主角  generic-pool  就要出场了。 这是一个基于 Promise 的通用链接池库。有了他之后我们就可以 将 puppeteer 实例放在我们的链接池中,如果有请求进来,那么就去池子里面去取一个实例。我们可以设置实例的上限,和常驻池中的实例数量。(一个任务队列,超过上限时自动排队。)然后你拿到这个实例之后就可以去进行和普通创建实例一样的操作了。(性能对比图这里就不给出了,提升还是非常巨大的,可以自行尝试。) 具体的使用可以在 github 查看这里就不多聊了。我们直接基于我们目前的一个启动创建配置来进行一个讲解。(算了,讲解就直接写在代码注释里了。) -_-! puppeteer-pool.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 'use strict' const puppeteer = require('puppeteer') const genericPool = require('generic-pool') /** * 初始化一个 Puppeteer 池 * @param {Object} [options={}] 创建池的配置配置 * @param {Number} [options.max=10] 最多产生多少个 puppeteer 实例 。如果你设置它,请确保 在引用关闭时调用清理池。 pool.drain().then(()=>pool.clear()) * @param {Number} [options.min=1] 保证池中最少有多少个实例存活 * @param {Number} [options.maxUses=2048] 每一个 实例 最大可重用次数,超过后将重启实例。0表示不检验 * @param {Number} [options.testOnBorrow=2048] 在将 实例 提供给用户之前,池应该验证这些实例。 * @param {Boolean} [options.autostart=false] 是不是需要在 池 初始化时 初始化 实例 * @param {Number} [options.idleTimeoutMillis=3600000] 如果一个实例 60分钟 都没访问就关掉他 * @param {Number} [options.evictionRunIntervalMillis=180000] 每 3分钟 检查一次 实例的访问状态 * @param {Object} [options.puppeteerArgs={}] puppeteer.launch 启动的参数 * @param {Function} [options.validator=(instance)=>Promise.resolve(true))] 用户自定义校验 参数是 取到的一个实例 * @param {Object} [options.otherConfig={}] 剩余的其他参数 // For all opts, see opts at https://github.com/coopernurse/node-pool#createpool * @return {Object} pool */ const initPuppeteerPool = (options = {}) => { const { max = 10, min = 2, maxUses = 2048, testOnBorrow = true, autostart = false, idleTimeoutMillis = 3600000, evictionRunIntervalMillis = 180000, puppeteerArgs = {}, validator = () => Promise.resolve(true), ...otherConfig } = options const factory = { create: () => puppeteer.launch(puppeteerArgs).then(instance => { // 创建一个 puppeteer 实例 ,并且初始化使用次数为 0 instance.useCount = 0 return instance }), destroy: instance => { instance.close() }, validate: instance => { // 执行一次自定义校验,并且校验校验 实例已使用次数。 当 返回 reject 时 表示实例不可用 return validator(instance).then(valid => Promise.resolve(valid && (maxUses <= 0 || instance.useCount < maxUses))) } } const config = { max, min, testOnBorrow, autostart, idleTimeoutMillis, evictionRunIntervalMillis, ...otherConfig } const pool = genericPool.createPool(factory, config) const genericAcquire = pool.acquire.bind(pool) // 重写了原有池的消费实例的方法。添加一个实例使用次数的增加 pool.acquire = () => genericAcquire().then(instance => { instance.useCount += 1 return instance }) pool.use = fn => { let resource return pool .acquire() .then(r => { resource = r return resource }) .then(fn) .then( result => { // 不管业务方使用实例成功与后都表示一下实例消费完成 pool.release(resource) return result }, err => { pool.release(resource) throw err } ) } return pool } 如何使用: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const pool = initPuppeteerPool({ // 全局只应该被初始化一次 puppeteerArgs: { ignoreHTTPSErrors: true, headless: false, // 是否启用无头模式页面 timeout: 0, pipe: true, // 不使用 websocket } }) // 在业务中取出实例使用 const page = await pool.use(instance=>{ const page = await instance.newPage() await page.goto('http://xxx.xxx', { timeout: 120000 }) // do XXX ... return page }) // do XXX ... // 应该在服务重启或者关闭时执行 //pool.drain().then(() => pool.clear()) 可以看到我们在 基于generic-pool 的情况下构建了一个 Puppeteer 的池。每一次请求进来之后 我们调用 pool.use 去取得一个实例。然后去进行我们后续的操作就可以了。 整体流程如下:在服务启动时启动池。 请求到达->从池中取得一个 Puppeteer 实例->打开tab页->运行代码->关闭tab页->返回数据(其他的管理都交给池了) 比如简述一下我们目前运行代码的业务流程: 拿到 json 数据把 canvas 页面渲染出来 (前端页面渲染流程,配置与渲染分离,只有在渲染的一刻才知道最终产生的数据是什么。 渲染页面与 Puppeteer 交互。拿到处理后的 json 拿到截图的配置参数 使用 Puppeteer Page api 截图。 对产生的 图片 buffer 做格式转化(调用 imagemagick(一个跨平台图像处理库) 等处理图片) 数据上传 阿里 oss 异步通知其他端处理已经结束。 然后我们再仔细看配置中的 maxUses 可以看到我们自定义扩展了每一个 Puppeteer  最多可以被使用的次数(防止实例变卡什么的)来防止一些意外情况出现。 其实我们之所以需要一个池其中一个问题主要就是处理性能问题。。这一部分其实在在业务代码中也需要处理。下面简单说几个点。 Puppeteer 什么样的启动参数对服务性能有提升? 在截图时选什么样的参数能在达到业务要求的情况下尽可能的提升性能? 是产生图片在本地?还是直接拿到 图片 buffer 去和第三方服务对接? 有没有可能把业务处理流程进行步骤拆分?让 Puppeteer 承担的工作少一些? 那我们有了一个 Puppeteer 的池,实现复用 Puppeteer 实例。那么如何更好的去实现一个 http 服务呢? 结合 egg.js egg.js 是蚂蚁金服出品的一个企业级 node.js 框架。可以高效的搭建一个可用的 http 服务,其他介绍自行官网查看。具体我这里就不多介绍了。 这里简单说一下怎么结合 puppeteer-pool 在一起使用 核心其实就是 创建 app.js  做初始化处理。 需要注意 结合 egg.js 使用时,需要手动指定 workers 数量为 1: egg-scripts start --daemon --workers=1 不然会启动 pool.max * workers 数量的 Puppeteer 实例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const initPuppeteerPool = require('./util/puppeteer-pool') const { EventEmitter } = require('events') EventEmitter.defaultMaxListeners = 30 class AppBootHook { constructor(app) { this.app = app } async didLoad() { // 所有的配置已经加载完毕 // 可以用来加载应用自定义的文件,启动自定义的服务 this.app.pool = initPuppeteerPool() } async beforeClose() { if (this.app.pool.drain) { await this.app.pool.drain().then(() => this.app.pool.clear()) } } } module.exports = AppBootHook server.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const Service = require('egg').Service class ScreenshotService extends Service { renderPage(url) { const { app, config } = this const imageBuffer = await app.pool.use(async instance => { // 1. 创建一个新窗口 const page = await instance.newPage() await page.goto(url, { timeout: 120000 }) // 2. 截图参数,截图 const buf = await page.screenshot({ ...imgOutOption, clip: opt }) return buf }) return imageBuffer } } 说一下 Puppeteer 使用到的坑 这里说三个 Puppeteer 使用上的坑吧: 可以看到第 5 点:由于我们的场景对于图片清晰度要求很高,所以发现了这个问题。(Puppeteer 导出 png 再调用 imagemagick 转成jpg ,也比直接使用 Puppeteer 导出 jpg 清晰度高(即便清晰度设置成了100 -_- !)) Puppeteer 无法截图产生超过 65535 的图。(当然 imagemagick,sharp 也无法处理超过这个的图。(这个是一个挺有意思的事情。有兴趣的可以去搜索这个数看看 Puppeteer@1.12.2 之后的版本 单张截图超过 8000(4096 * 2)(不确定值,但是确实会出问题,因为出现问题就没升级了)有一定概率导致 图片部分区域为空白。 后续/扩展 这是目前未处理的部分。 其实可以看到我们在上面的处理实现了多个 Puppeteer 实例的复用,但是其中也有一个问题,那就是其实我们在这样的情况下使用每一次请求过来只会利用一个 浏览器 窗口,那么我们的 QPS 直接与我们新建的 Puppeteer 实例上限挂钩(配置中的 max 属性),当然还有单个任务的处理时间。(当然在我们内部的业务场景没啥问题(长度过长,图片太多。然后还要处理图片。cpu早100%了) 能不能在实例池的基础上,再创建一个单实例的窗口池呢? (因为实际上我们真正操作的内容 其实都是 Puppeteer 的 Page )这部分是还没做的,就交给你们去实现了 参考链接: Puppeteer 性能优化与执行速度提升 phantomJs 池

2019/6/16
articleCard.readMore

再谈中文字体的子集化与动态创建字体

其实在项目中用中文字体子集化已经很久了,在刚接受到项目时真的让用户去下载全量字体的方式也早已被废除。如今终于有时间将它整理成文。算是对这件事情的一个基本了结吧。 为什么要截取字体? 众所周知,相对于英文字体,中文字体就是一个“庞然大物”。英文字体 200~300KB 已经很大了,而中文字体 动戈 10~30MB。 这主要是两个方面的原因: 中文字体包含的字形数量极多 英文字体则只需包含几十个基本字符和符号。有些中文字体还要包括韩语和日语的字形。 中文字形的曲折变化复杂度高,用于控制中文字形曲线的控制点普遍比英文更多,由于数据量不一样,字体大小也自然就有这样的膨胀了 但是需求总是有的,在一些特殊的视觉效果,或者是在一些富文本(如海报设计类)的编辑场景下,特殊字体的支持更是必不可少的。 但是一个中文字体 10~20Mb 我网站可能支持100种字体,你让用户都全量下载显然是不可能的!并且也不是每个页面都会用到一个字体文件中的所有字符,全量加载本身也极其浪费。 在《通用汉字表》中一级表确定 3500 常用中文汉字(中国义务教育9年级需要掌握的汉字数量)即可覆盖日常使用汉字的99.8% 如何使用自定义字体。 在真正开始之前,我们先来回顾一下,如何去让一个文本使用自定义字体。这里我们会聊到 @font-face,这就是我们目前前端最常用的Web自定义字体技术。 示例代码:https://css-tricks.com/snippets/css/using-font-face/ 这里取了其中一个最全的方案,基本上能够兼容到所有的浏览器。 1 2 3 4 5 6 7 8 9 10 11 12 13 @font-face { font-family: 'MyWebFont'; src: url('webfont.eot'); /* 兼容IE9 */ src: url('webfont.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('webfont.woff2') format('woff2'), /* 最新浏览器 */ url('webfont.woff') format('woff'), /* 较新浏览器 */ url('webfont.ttf') format('truetype'), /* Safari、Android、iOS */ url('webfont.svg#svgFontName') format('svg'); /* 早期iOS */ } <!--使用--> .newfont { font-family: 'MyWebFont'; } 当然除了直接使用 @font-face ,还可以使用 @import 规则或 link 元素导入或加载包含 @font-face 声明的外部文件: 使用 google open font (360 奇舞 cdn 的 google font 镜像) 1 2 3 4 5 6 7 8 // 导入 @import url(//fonts.googleapis.com/css?family=Open+Sans); // 或者引用 <link href='//fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet' type='text/css'> // 实际使用 body { font-family: 'Open Sans', sans-serif; } 关于字体如何使用就简单介绍到这,网上也已经有很多各种各样的教程。不再过多赘述。 其实目前 iconfont.cn 这类字体图标的网站就是这样的技术。 字体如何截取? 1. unicode-range unicode-range 是一个 CSS 属性,一般和 @font-face 规则一起使用。它只是在本地既有字体或者浏览器已经下载的字体基础上做一个指向子集的“软链接”,并不能真正减小浏览器下载文件的大小。 对于这种技术由于并不能真正的减少字体大小,所以也不在这我篇文章的范围内。给两个参考链接给大家观看了解。 前端字体截取:实战篇 张鑫旭 - CSS unicode-range特定字符使用font-face自定义字体 2. 全量字体精简 即在服务端从“全量”字体中分离出一个体积相对极小的字体子集,做成 webfont 通过 Web 服务器或 CDN 下发给浏览器。 这里需要介绍笔者 fork 之后修改的一个库: font-carrier2 项目 fork 自 font-carrier。 由于 font-carrier 有很长时间无人维护,但是我又有需求。然后就特此开一个新分支。做一些特性的更新与 bug 的修复。 下面给出一种精简中文字体的方式。 1 2 3 4 5 6 7 8 var fontCarrier2 = require('font-carrier2') var transFont = fontCarrier2.transfer('./test/test.ttf') // 会自动根据当前的输入的文字过滤精简字体 transFont.min('我是精简后的字体,我可以重复') // 产生一个新字体 transFont.output({ path: './test/minFont' }) 使用新字体:(这样这个新字体中只有《我是精简后的字体,我可以重复》这几个字) 1 2 3 4 5 6 7 8 9 @font-face { font-family: 'minFont'; src:url('./test/minFont.eot'); /* IE9 */ src: url('./test/minFont.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('./test/minFont.woff2') format('woff2'), url('./test/minFont.woff') format('woff'), url('./test/minFont.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/ url('./test/minFont.svg#iconfont'); /* iOS 4.1- */ } 可以看到我们这里很简单的就将一个中文字体给子集化了,那么关于 font-carrier2 如何去子集化一个字体我们也简单介绍到这。下面我们来进入重头戏:到底是如何做到精简的。 字体解析。(font-carrier2 基本思路剖析) 关于如何解析一个字体的话,其实都是有对应规范的:这个是其中一个规范的描述。microsoft-The OpenType Font File 其实也就是我们如何从一个二进制的流(当然会转化成 buffer )中,转化成一个人类可读的对象。(psd.js(一个解析psd为json的库,其实也是在做一个类似的事情。)) 这一步 opentype.js 已经帮我们做得很好了。 他能够解析 ttf otf woff 三种文件格式解析为一个 font 类。那么我们拿到这个 font 类 之后就可以去做我们任何想做的事情了。那么对于一个 webfont 来说有哪些是最关键的呢? 1.解读字体内容 1 2 3 4 5 6 7 8 // 其实我们就用这些东西足够去创建一个字体了 // 首先我们使用 opentype 解析一个字体文件读取之后的 buffer 。 var font = opentype.parse(toArrayBuffer(fs.readFileSync('font.tff'))) // 这些内容可以在 opentype.js 官网中看到详细信息 var hhea = font.tables.hhea // Horizontal Header table var head = font.tables.head // Font Header table var name = font.tables.name // 存储了原字体 名称相关信息。处理 fontFamily var glyphs = font.glyphs.glyphs // 重点(存储了所有的 字形的列表。 2.生成一个简单的 fontObjs 数据对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 var _ = require('lodash') var fontObjs = { options: { id: name.postScriptName.en || 'iconfont', horizAdvX: hhea.advanceWidthMax || 1024, vertAdvY: head.unitsPerEm || 1024 }, fontface: { fontFamily: name.fontFamily.en || 'iconfont', ascent: hhea.ascender, descent: hhea.descender, unitsPerEm: head.unitsPerEm }, glyphs: {} } var path, unicode _.each(font.glyphs.glyphs, function(g) { try { path = g.path.toPathData() if (_.isArray(g.unicodes)) { _.each(g.unicodes,function(_unicode){ unicode = '&#x' + (_unicode).toString(16) + ';' if(unicode === '&#x20;' || unicode === '&#x2005;' || path){ fontObjs.glyphs[unicode] = { d: path, unicode: unicode, name: g.name || 'uni' + _unicode, horizAdvX: g.advanceWidth, vertAdvY: fontObjs.options.vertAdvY } } }) } } catch (e) {} }) 3.glyphs 精简。 glyphs 这个时候已经是一个对象了。 key 为 文字对应的 unicode, value 实际上是一个 svg 字体中对应 glyphs 的信息。具体可以查看:MDN - SVG 字体 里面 glyphs 对应的部分。如果需要精简的话 那么我们其实只要从这个 glyphs 对象里面 提取所需要文字对应的 unicode 就行了。 4.转化成 svg 字体。这个其实就是将 上面 提到的 fontObjs,和需要提取的文字精简过后的 glyphs 转化成 MDN - SVG 字体。这个其实也是 fontCarrier2 中比较重要的部分。 5.生成各种字体。fontCarrier2 就是直接先生成一个 svg 的字符串,然后通过 svg2ttf 转化成 ttf buffer 。(本着不多次重复造轮子的原则。在网上可以找到各种字体转化的库 比如 svg2ttf ttf2woff.. 等。然后再通过 ttf2woff/ttf2woff2 等.. 转化成其他的字体文件。(这样当然性能不是最高的。不过实现会快很多。) 6.在前端使用 font-face + font-family 引用新的字体。 那么 font-carrier2 的基本思路剖析 我们就到这了。通过上面这些步骤我们就实现了一个中文字体的子集化。下面我们再聊聊动态创建字体思路。 动态创建字体 先来看一下 在 font-carrier2 中如何通过空白字体去创建文字。具体效果可以在库中 test/index.html 看到。 https://github.com/guowenfh/font-carrier2/blob/master/test/create_test.js 其实这个图看完。结合我们之前我们看的 font-carrier2 处理流程。 我们动态创建字体的思路就很明确了。 解析字体 得到 fontObjs ,(options & fontface & glyphs)。 把 fontObjs 存下来(各种存储方式任选:内存/文件/redis/数据库…) 前端发送请求。( font-family和对应的文字("simplified":"纯空白 迷你简硬笔楷书 字体测试1,2,3") 服务端接受到请求。通过 将接受到的文字转化成 unicode, 然后再通过 font-family,取到 options & fontface & glyphs 对应的值。创建一个新字体。返回给前端。 前端接受到返回。创建 font-face 插入到 style 插入 html 你还能通过 fontfaceobserver 这个库来监听字体是否生效。( canvas 的 fillText 不会在字体更新后自动刷新 然后就是正常使用了。 在笔者目前的项目中使用的是上述的流程。 不过也非固定,第 4 步之后 是一个分支流程。 通过后端去创建字体可能对服务端造成较大压力。由于我们去创建一个字体的基本信息都存下来了。 那么其实也可以后端只做存储相关的工作。 通过在浏览器直接操作 ArrayBuffer和 blob (其实 opentype.js把这个也实现了)利用客户的浏览器去生成字体(目前市面上调研到几个做子集化公司付费解决方法。 文章到这就基本结束了。相信看下来应该对中文字体的子集化应该会有一个基本上的了解。 font-carrier2 和 opentype.js 还有很多特点没介绍到。剩下的就交给各位自己去想象了。

2019/6/4
articleCard.readMore

《张乌梅的日记》摘录

08年2月3日阴 胡老师走了,我想世界上再没有这样的老师了。好人有好报吗?骗人的! 2月4日晴 村子来了个人,好像是胡老师的男朋友,不喜欢他,因为他一年里一次都没有来看过胡老师,胡老师明显是很想他的,胡老师,那是叫思念吗? 2月7日晴 胡老师的爸妈,还有那个人,都在村里过年了,听人说那个人昨天晚上喝了很多酒,在操场上吐,吐出血了,真的吗?他是在伤心吗?如果是的,我就不那么讨厌他了。 2月13日大雨 那个人又去山上了,这么大雨,他不怕吗? 2月21日雨转晴 元宵节了,他还是住在学校二楼,没有回城里。好像他要来做我们的新老师,他肯定不是一个好老师,更比不上胡老师。 2月23日阴雨 开学了,他果然来到教室,说他叫赵甲第,还在黑板上写下了这个名字,要给我们讲课,他说不一定比胡老师讲得更好,但他会跟胡老师一样用心,跟她一样希望将来某一天我们所有人都可以挺着胸膛走出村子。然后他就开始上课了,我什么东西都没有听进去,只是在想,他真的能一年都呆在村子里吗?能像胡老师那样对我们吗?嗯,他的粉笔字很好看。明天要好好学习了。胡老师在那里看着我们呢。 2月28日阳光 一个星期了,他不太爱笑,上课应该能算很认真,还是经常一个人去山顶,二娃偷偷说他看到赵老师坐在那里,还会拿树叶吹歌曲,就是《丁香花》。 3月14日晴 时间过得很快,大概就是书上说的光阴似箭吧,算一下,他来观音村有两个多月了,给我们上课也一个多月了,班上很多男生都开始喜欢他,我不理解。 4月4日清明节 今天胡老师父母来了,和他一起去了胡老师坟前,我们全村子都去了,他和胡老师爸爸,还有二叔,都给胡老师敬了酒,他闭着眼睛说了点什么,但我没有听到。问燕子她们,也都说没听清楚,可能只有胡老师能听到。后来我们走了,胡老师父母也走了,他还是不肯走。为什么呢,是他有很多话想让胡老师知道吗? 5月2日明媚 劳动节放假了,阳子二娃这些调皮蛋去隔壁村子里玩,结果被人欺负了,鼻青眼肿的,回来还不敢回家,然后那个村子就来了一帮大人,很凶,打了人不说,还要我们村里的人赔钱,我二叔躲起来了,然后他跟阳子二娃他们问清楚了事情经过,什么都没有说,就直接冲上去把那些人给打了一顿,真厉害呀,一个人就把那些不讲理的人全打跑了,好几个都躺在地上,流了很多血,很吓人,结果被打的人还都跟他道歉,奇怪。没想到他以前在课堂上看到阳子几个捣蛋鬼不专心听课,他都不会用教鞭打的,我还以为他脾气很好呢。他打完人,跟二娃几个说以后有人不讲理欺负到村子里,就打,打不过就用棍子砖头,出了事情,医药费他出。我妈回家后说赵老师了不得。 5月5日小雨 上学了,他从县里搬了很多书过来,跟胡老师的书放在一起,有几本新的《安徒生童话》,很好看。下课的时候,他竟然跟我们女孩子一起跳皮筋了,可他总是跳错,真笨,跟他一头的燕子都输啦,可为什么输的燕子很开心,赢了的我不那么开心呢? 5月10日晴 他用啤酒盖子上的橡皮胶做了一串大沙包,跟阳子二娃利军这些男孩子玩了一个中午,他好像一场都没输过,难怪他们那么崇拜他,愿意听他的话,打扫卫生什么的都不偷工减料了。今天他在语文课上读了我的作文,好高兴。 6月1日暴雨 思想品德课上,他给我们讲了很多城里的事情,他说城里的男孩女孩有好有坏,有听话也有不听话的,他说以后走出村子了,读初中高中,然后大学,见到城里的同龄人,不要自卑,因为我们也许没他们有钱,不能像他们那样穿好的打扮漂亮的,但一个男孩子帅不帅,还是要看有没有理想的。女孩子漂不漂亮,是要看善不善的良。他说为了爹妈去低着头,弯着腰,不丢人,但不能忘记观音村这块土地,不能忘了亲人。很多话,我都不太懂,但我都专门记在笔记薄上了,燕子她们也记了,但字没我好看。他说暑假要办一个作文小组和书法小组,我都想参加。对了,课堂上说到帅不帅的时候,二娃站起来说赵老师最帅,他脸皮真厚,一边笑一边说你们加起来都没老师帅,阳子还吹了哨子。 7月3日多云 老师帮我家做了农活,流了很多汗,我带着他去喝泉水,他喝了还不够,把整个脑袋都伸到了水里,抬起来后甩了甩,笑得很开心,晚上在我家吃的饭,被我爸劝了很多酒,他的脸,很像年画上的关公,走路都不稳了,还唱了很奇怪的歌,我问他,他说是京剧。 7月17日阴转晴 书法小组正式上课了,纸笔和墨水都是他买的。原来他不光是粉笔字好看,毛笔字也很好,他夸了晓燕的字有天赋,我们不懂天赋是什么,他就写了一个赋字,拆开来跟我们解释了。我的圆珠笔字是班上最好的,但毛笔好像没有“天赋”,他没有夸我。 9月1日阳光灿烂 终于正式开学了,张许褚(我一开始不知道褚怎么写,是下课后偷偷问他的,他笑着教我怎么写,还摸了摸我的头,说我很用心)又在窗外偷偷听课了,他让张许褚进教室,张许褚跑开了,这家伙一直不爱说话。他用鸡毛做了一个新毽子,说要跟我们女孩子比试比试,他出丑了,暑假里,听说他去小水潭学游泳,男孩子都说赵老师只会狗刨,一个扎猛子下去,狗刨了半天,起来后还是会在原地的,真的好好笑啊。可惜我是女孩子,不能去看。 9月5日阴 他每天早晚都会跑步,现在张许褚跟在他屁股后边跟着跑了。张许褚以前都不会笑的,现在变了。以前我总看不起张许褚,现在觉得他挺可怜的,也很懂事,所以我再见到他,都不会故意抬着下巴不看他了,会跟他笑一下,打招呼。今天,他又抽烟了,男生都跟他一起蹲着围成一圈,看他吐烟圈,他笑着骂,把男生都赶走,说不能抽二手烟。他还说等男孩子那个什么长什么了,才允许抽烟,否则就算是躲在厕所抽烟屁股,都要被他吊起来打的。二娃特别坏,故意很大声让我们女孩子听到,二娃对他说他们都长那个啥了,不信就脱裤子给他看,他没让,然后看到我们都跑开了,就和男孩子一起大笑,这算不算他耍流氓啊? 10月1日国庆节 听说他晚上去张志毅家里串门,结果又喝了很多酒,回学校的时候都摔跤了,应该不会有事情吧? 11月8日 期中考试分数出来了,我第一次拿了第一名,他表扬了我,踢毽子的时候跟我一头,他还是没进步,我们输了,但我很开心,很开心。 2009年1月1日元旦 他傍晚又去山顶坐着了,我们不知道谁带的头,都跟去了,他用树叶吹了一支曲子,真好听,他还教我们吹了,但我们都学不会。 1月12日晴转雨 明天就是寒假了,但没有谁开心,因为他说今天是最后一天给我们上课了。我们都哭了,他没有笑,只是站在讲台上,看着我们,他说大家很快就能去新学校读书,那里有明亮的教室,有整齐的桌椅,有很多的老师。可我们还是听着听着就哭了,我是第一个哭的,然后燕子她们也哭了,最后男生们也都哭了,直到他说还会留在村里过年,我们才好点。 1月26日春节 他被村里每家每户拉过去吃饭喝酒,我家也请了,他跟我爸一起抽了好几根烟,我又哭了。 2月7日晴 他送我们来到新的学校,在新操场上,他轻轻说了一些话,但我们都只顾着哭了,我只记得他说会给我们写信的,还让我负责收信,有时间就给他回信。他走了,回家了。我喜欢你,赵老师,等我长大了,还可以喜欢你吗?

2019/5/28
articleCard.readMore

一个使用 react 的思想去使用 vue 的方式

有一个 react 开发者 问我 vue 如何上手开发?然后我是这么和他描述的。。 用 react 的思想 去考虑 vue 要怎么写。 本文很水。而且将来感觉一定会被打脸,期待那一天到来。 其实也就几块 你可以把 template 就看成 react 的 render 就是写法有一些不一样,看一下 vue 的指令就可以了,当然也可以直接用 render function data 的话 和 react 的 state 也没什么区别 , 只不过赋值方式变成了 this.setState({text:’state’}) this.text = ‘state’ (vue 有一个坑,数组里面 data: [{a:1}] 使用 this.data[0].b = 1 => data”:[{a:1,b:1}] 新增了一个b字段 这样是不会刷新页面的。react 不会有这个问题(至于怎么刷新,这个就先你自己去看文档把哈哈哈(底层实现不考虑 methods 对应的就是 react 中 class componnet 中 直接写上去的方法 onMenuClick = () => {} 这种 (vue里面不需要箭头函数 事件监听的话 @click=”onMenuClick” 和 react 中的 onClick={this.onMenuClick} 也没什么区别 生命周期 mounted 和 react 中的 componentDidMount 也基本一致(周期里面就这个最重要了 当然还有 wtach需要看一下 computed 正确的使用方式,其实就是一个纯函数,在里面写有副作用的内容,对可维护性是一个灾难(自动计算,实际上不用也没关系 (react里面就没有 其他的 就是 props components 这两个了,用法是一样的,但是得手动声明一下,按照文档来就可以 路由,vue 的路由其实更好理解,react-route@4 + react-route-config 使用上基本也一致。 只不过 router-view 换成了 renderRoutes 剩下的部分 其实就照着文档看看就行拉 https://cn.vuejs.org/v2/api/#components 然后在组内问了一下大家的看法 Q:感觉写vue和react的思维方式不太一样? A:大的说的话,一个是函数式,一个是响应式。 Q:vue比较符合常人的思维,比较好上手。同时意味着,不如react的方法抽象。watch就很有意思。 A:watch 是一个双刃剑吧。确实很方便…不过也容易写出来不易读或者性能很差的代码。 Q:React 的生命周期component WillReceiveProps可以拿到nextProps 这样的参数,父组件参数改变时,子组件方便监听并特殊处理。vue 中通过computed 监听数据变化并处理,感觉怪怪的 A:其实 componentWillReceiveProps 即将被废弃了。。。不过这个需求确实是有的 ,也就是父组件和 子组件其实都有一份state,并且父组件的状态更改会影响 子组件的state 更改的情况。 vue里面应该这个用 watch 会比较多,结合上面,其实 还是建议 computed 使用 纯函数,如果是纯函数的话,那么就不能去改子组件的 state 了 (state 改成 data 也是一样。 Q:Vue 通过 getter/setter 以及一些函数的劫持,能精确知道数据变化,不需要特别的优化就能达到的性能 React 默认是通过比较引用的方式进行的,如果不优化(PureComponent/shouldComponentUpdate)可能导致大量不必要的VDOM的重新渲染 A:嗯…有点偏题。 一个新人学 vue 进来应该不会考虑这个?不过基本没错 Q:computed是实现y = f(x) ,因变量只能通过这个函数得到才能用 computed吧,否则应该是有问题的。 A:虽然是这么说,但是你看了主站的代码就知道,这个其实不是一个强绑定。主站里面 computed 里面做有副作用的事情,也有好多- -,不是一个好的实践。(我上面说的是建议只当成纯函数来用 下一篇, 《在 vue 中使用 jsx 与 class component 的各种姿势》

2019/4/28
articleCard.readMore

React 项目构建

在很久之前在知乎回答过一个问题:公司要求统一一套前端脚手架,该怎么选择? 在那个时候推荐了使用了魔改 vue-cli@2 来实现 react 的基础脚手架,让 react 以及 vue 能够基本使用同一个模式的脚手架。 在之后也将他开源了出来:vue-cli-react-base 最基础的实现 思考 在之前开源版本 vue-cli-react-base 的实现中只完成了和 create-react-app 构建出来的一样包含 react 运行最基础的功能。 最近回过头再再看,也一直在思考,一个面向于企业内部的项目脚手架到底有什么样的需求? 是如同 vue-cli@2 和 create-react-app 一样的嘛?只实现最基础的功能,所有的扩展(路由,数据管理,ui 库,目录结构约定),都交给开发者去完成。 感觉并不够。 还是如同 umijs 或者 nextjs 一样?开箱即用,有约定式路由,代码自动分割,在预定义的同时也支持 自定义扩展 webpack 配置达到用户的需求。 这样好像还不错。拿过来直接就可以开发项目了。 但是 他们的自定义扩展 webpack 真的足够方便吗?他们本身带来的学习成本呢? 基于这种思考,我开始尝试的去做这样一个事情。 把 vue-cli-react-base 一个基本完整的项目开发骨架给搭建起来。 于是最近开始基于他进行了一些改造。 如下: vue-cli-react-base vue-cli-react-base github 使用 vue-cli@2 驱动的 react 项目 使用 webpack@4 + babel@7 + (css/less module) + prettier 来构建 支持 module.css / module.less 等 css module 语法,推荐使用 npm i -S classnames 库来更好的使用 css module 内置了 husky 与 eslint-config-standard 与 prettier-eslint 来运行 git commit 时代码的自动格式化。自动格式化 使用了 standard 的代码风格 状态管理工具方面使用 @rematch , 并且内置了插件 @rematch/immer 以及 @rematch/loading 具体使用方式参考: Rematch实践指南 内置了组件库 antd 结合 babel-plugin-import 做了组件(lodash也可以)的按需引入 (直接修改 src/theme.js 可以修改主题色)。当然要用别的组件库也是可行的,需要改的东西很少不是嘛? 使用 react-router-config 来达到和 vue-router 类似的体验。 结合 react-loadable 与 import() 实现了路由的按需加载 package.json 使用了 ~ 版本,来尽量保证安装时依赖升级导致项目报错问题 对于 mock 数据的需求,使用npm run dev-mock启动服务,实现了两种途径的mock数据: 直接 webpack-dev-server 提供的 proxyTable 使用本地 mock 数据,在 mocker文件夹下,修改添加即可, 或者使用 easymock 这样类似的在线 mock 服务,基于这样的需求实现了一个 apiProxy 的高阶函数,提供了本地mock的支持,当然他也能够比较方便的进行各种需求的改造。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 /** * 拦截请求函数,只在 开发并且开启了mock的情况下启用 * @param {Function} fn * @returns {fn} */ const apiProxy = fn => { if (process.env.NODE_ENV === 'development' && process.env.mock === true) { return function(url, data = {}, otherOptions = {}) { return import('../mocker/index').then(({ default: mocker }) => { // 如果未找到 mock 对应的数据的情况依旧走老代码 if (!mocker[url]) { return fn.call(this, url, data, otherOptions) } const isFn = typeof mocker[url] === 'function' // 如果是一个函数那么一定要返回 Promise if (isFn) { return mocker[url](data) } // 其他情况,直接使用Promsie返回值 return mocker[url] }) } } return function(...args) { return fn.apply(this, args) } } 上面就是对于企业内部项目脚手架思考的一些产出,不过又在想是不是,整个 webapck 所有的配置全部都被暴露了出来,和 目前 create-react-app 以及 vue-cli@3 的设计思路感觉还是不太相符啊,隐藏实现细节,暴露更改接口。能在底层升级的情况下,无需改动(大部分时候)上层业务的接口修改 基于这样的思考又结合上面的实现 于是又有了另外一个项目: cra-config-create-app cra-config-create-app 一个基于 create-react-app 和 react-app-rewired 开箱即用的一个基础项目骨架。 本项目想法源自于 既希望能直接享受到 cra 带来的可升级的机制, 又能够和 vue-cli@2 一样支持一些基础的配置项, 于是利用 react-app-rewired 和 环境变量 的支持 把部分选项直接写成了配置项。 支持直接修改配置项使用。 具体已经实现的功能和 上面的项目是一致的,但是这个能够享受到 create-react-app 升级 带来的一些特性和优化支持。(比如 create-react-app 快发布 3.0了。。) 当需要写入使用的全局环境变量时,需使用 process.env.REACT_APP_XXX = xxx 形式才能拿到。 (只支持字符串) 使用 %REACT_APP_XXX% 方式获取 已经支持的配置如下: cra-config/config.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 { alias: { '@': resolveApp('src') }, // 开发环境 dev: { /** * 是否启用 https 的构建 * 修改 host, port 等。 * 如果 process.env.xxx 有对应的值,那么会覆盖这里的配置 */ HTTPS: false, HOST: '0.0.0.0', PORT: 5000, /** s* 在 webpack 中 是否启用 eslint 检查 */ useEslint: true, /** * 是否自动打开浏览器 */ autoOpenBrowser: true, /** * 本地服务器代理的配置 */ proxyTable: {} }, // 构建正式 build: { /** * 在 webpack 中 是否启用 eslint 检查 */ useEslint: false, /** * 构建时打包文件夹 */ appBuild: resolveApp('dist'), /** * 是否启用 sourcemap */ productionSourceMap: false } } 整体文章说得比较散,因为对于企业内部,一个完备靠谱的的上线项目来说,仅仅是上面提到的点,感觉还是远远不够的,发布部署流程,分支合并策略。函数命名规范,甚至函数体的最大行数等等。 这让我想起了某一次听到 月影 说的一句话:框架的出现,不是让能力强的人写出来更好的代码,而是让能力没那么好的人,能够写出来没那么差的代码。 加油共勉!

2019/4/11
articleCard.readMore

从一个简单的实例看 JavaScript 的异步编程进化历

很久没有进行过创作了,也感觉到了自己的不足。这一篇文章是对于 JavaScript 异步编程的一个 整理 希望自己更多的成为一个创造者,而不是只会看,会用,还需要深入理解到原理吧。 例子如下: 我们有 A, B, C, D 四个请求获取数据的函数(函数自己实现), C 依赖 B 的结果,D 依赖 ABC 的结果,最终输出 D 的结果。 版本一 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // 伪代码 function A(callbak) { ajax(url, function(res) { callbak(res); }); } function B(callbak) { ajax(url, function(res) { callbak(res); }); } function C(data, callback) { ajax(url, data, function(res) { callbak(res); }); } function D(data1, data2, data3, callback) { ajax(url, { data1, data2, data3 }, function(res) { callbak(res); }); } A(function(resa) { B(function(resb) { C(resb, function(resc) { D(resa, resb, resc, function(resd) { console.log("this is D result:", resd); }); }); }); }); emm…代码还是能运行,但是写法丑陋,回调地狱,如果还有请求依赖,得继续回调嵌套 性能太差,没有考虑 A 和 B 实际上是可以并发的。 例子二 函数基础实现如同例子一,但是考虑 A,B 可以并发的。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // 伪代码 let resa = null; let timer = null; A(res => { resa = res; }); B(resb => { C(resb, resc => { timer = setInterval(() => { if (resa) { D(resa, resb, resc, resd => { console.log("this is D result:", resd); timer && clearInterval(timer); }); } }, 100); }); }); 考虑了 A,B 的并发,使用 setInterval 轮询实现,并不一定实时。性能太差。 例子三 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // 伪代码 let count = 2; let resa = null; let resb = null; let resc = null; function done() { count--; if (count === 0) { D(resa, resb, resc, resd => { console.log("this is D result:", resd); }); } } A(res => { resa = res; done(); }); B(datab => { C(datab, datac => { resb = datab; resc = datac; done(); }); }); 使用 计数器实现。性能没什么问题,但是 封装太差,写法恶心。 例子四 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 // 实现并发 function parallel(tasks, callback) { let count = tasks.length; let all = []; tasks.forEach((fn, index) => { fn(res => { all[index] = res; count--; if (count === 0) { callback(all); } }); }); } // 实现串行 function waterfall(tasks, callback) { let count = tasks.length; function loop(...args) { let task = tasks.shift(); task.apply( null, args.concat([ (...res) => { count--; if (count === 0) { return callback(res); } loop(...res); } ]) ); } loop(); } function A(cb = () => {}) { setTimeout(() => { cb("a"); }, 2000); } function B(cb = () => {}) { setTimeout(() => { cb("b"); }, 1000); } function C(datab, cb = () => {}) { setTimeout(() => { cb(datab, "c"); }, 1000); } function D(data, datab, datac, cb = () => {}) { cb("d"); } parallel( [ A, cb => { waterfall([B, C], (datab, datac) => { cb(datab, datac); }); } ], data => { const [resa, [resb, resc]] = data; D(resa, resb, resc, resd => { console.log("this is D result:", resd); }); } ); 模仿 async.js 提炼出来了 waterfall,parallel,两个流程控制函数。还不错。 但是写法还是麻烦,对于 A,B,C 的实现有要求。得自己考虑好每一次 callback 的值。 async.js 是我认为在目前 JavaScript callback 的终极解决方案了(没用过 fib.js.. 推荐查看 github async.js 源码。 waterfall 可以考虑使用函数式的形式实现: 1 2 3 4 5 6 7 8 9 10 function pipe(...fnList) { return function(...args) { const fn = fnList.reduceRight(function(a, b) { return function(...subArgs) { return b.apply(this, [].concat(subArgs, a)); }; }); return fn.apply(this, args); }; } 例子五 1 2 3 4 5 6 7 8 9 10 11 12 13 14 function A() { return fetch("http://google.com"); } function B() {} function C() {} function D() {} Promise.all[(A(), B().then(b => C(b)))] .then(([resa,{resb,resc}) => { return D(resa,resb,resc); }) .then(resd => { console.log("this is D result:", resd); }); 使用 Promise 来代替 之前的 callback。好评。 用 Promise.all 来控制并发,使用 .then 串行请求,整体看起来非常舒服了,脱离了回调地狱。 例子六 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 function A(cb) { setTimeout(() => { cb("a"); }, 2000); } function B(cb) { setTimeout(() => { cb("b"); }, 1000); } function C(datab, cb) { setTimeout(() => { cb("c"); }, 1000); } function D(dataa, datab, datac, cb) { setTimeout(() => { cb("d"); }, 1000); } function thunk(fn) { return function(...args) { return function(callback) { fn.call(this, ...args, callback); }; }; } function scheduler(fn) { var gen = fn(); function next(data) { var result = gen.next(data); if (result.done) return; // 如果没结束就继续执行 result.value(next); } next(); } // generator 实际代码 function* generatorTask() { const resa = yield thunk(A)(); const resb = yield thunk(B)(); const resc = yield thunk(C)(resb); const resd = yield thunk(D)(resa, resb, resc); console.log("this is D result:", resd); return null; } scheduler(generatorTask); 使用 generator + callback 来控制流程顺序,还是同步写法,看起来还是挺牛逼的。 但是 generator 不会自动执行,需要自己手动写一个执行器,并且依赖于 thunk 函数。麻烦! 等等。。又全变成了串行?垃圾 例子七 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 function A() { return new Promise(r => setTimeout(() => { r("a"); }, 2000) ); } function B() { return new Promise(r => setTimeout(() => { r("b"); }, 1000) ); } function C(datab) { return new Promise(r => setTimeout(() => { r("c"); }, 1000) ); } function D(dataa, datab, datac) { return new Promise(r => setTimeout(() => { r("d"); }, 1000) ); } function scheduler(fn) { var gen = fn(); function next(data) { var result = gen.next(data); if (result.done) return; // 如果没结束就继续执行 result.value.then(next); } next(); } // generator 实际代码 function* generatorTask() { const [resa, { resb, resc }] = yield Promise.all([ A(), B().then(resb => C(resb).then(resc => ({ resb, resc }))) ]); const resd = yield D(resa, resb, resc); console.log("this is D result:", resa, resb, resc, resd); return resd; } scheduler(generatorTask); 抛弃了 thunk 函数,修改了一下 A,B,C,D。的实现以及 generator 执行函数 scheduler。 结合了 Promise 重新实现了并发和串行。 再等等??好麻烦啊。。然后并发好像和 generator 没什么关系吧。果然还是 Promise 大法好。 关于 generator 的自动执行建议直接看 github tj/co 的源码。 例子八 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function A() { return fetch("http://google.com"); } // ...B,C,D async function asyncTask() { const resa = await A(); const resb = await B(); const resc = await C(resb); const resd = await D(resa, resb, resc); return resd; } asyncTask().then(resd => { console.log("this is D result:", resd); }); 使用 Promise 结合 async/await 的形式 ,看起来非常简洁。也不用自己写执行器了,舒服。 但是和上面有几个版本出现了一样的问题,没有考虑并发的情况,导致性能下降。 例子九,终极方案? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // ...B,C,D async function asyncBC() { const resb = await B(); const resc = await c(resb); return { resb, resc }; } async function asyncTask() { // const [resa,{resb,resc}] = await Promise.all([A(), B().then(resb=>C(resb)]); const [resa, { resb, resc }] = await Promise.all([A(), asyncBC()]); const resd = await D(resa, resb, resc); return resd; } asyncTask().then(resd => { console.log("this is D result:", resd); }); 使用 Promise.all 结合 async/await 的形式,考虑了并发和串行,写法简洁。 应该算是目前的终极方案了。 async/await 作为 generator 语法糖还是非常的甜的。 例子十 使用 RxJs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import { defer, forkJoin } from "rxjs"; import { mergeMap, map } from "rxjs/operators"; function A() { return fetch("https://cnodejs.org/api/v1/topics").then(res => res.json()); } function B() { return fetch("https://cnodejs.org/api/v1/topics").then(res => res.json()); } function C() { return fetch("https://cnodejs.org/api/v1/topics").then(res => res.json()); } function D(...args) { return fetch("https://cnodejs.org/api/v1/topics") .then(res => res.json()) .then(res => [...args, res]); } // A, B, C, D 函数必须返回 Promise // 使用 defer 产生一个 Observable const A$ = defer(() => A()); // pipe 类型 Promise 链中 的 then const BC$ = defer(() => B()).pipe( // mergeMap 映射成 promise 并发出结果 mergeMap(resB => { // 使用 map 产生新值 return defer(() => C(resB)).pipe(map(resC => [resB, resC])); }) ); // forkJoin 类似 Promise.all 并发执行多个 Observable forkJoin(A$, BC$) .pipe(mergeMap(([resa, [resb, resc]]) => D(resa, resb, resc))) .subscribe(resd => { console.log("this is D result:", resd); // <------- fnD 返回的结果 }); 使用 rxjs 来构建流式的请求过程。结构还是非常清晰的,但是相对繁琐,概念也比 原生的 Promise 和 await 要多 不过 rxjs 操作符巨多,掌握之后,可以做更多的事情 结语: 从上面几个例子我们可以窥探到 JavaScript 对于异步编程体验的一个非常大的进步。 但是同时我们其实可以看到不论是 generator 还是 async/await。其实更多的是基于 Promise 之上的一些语法简化。 没有从 callback 过渡到 Promise 的时候那种真正心灵上的愉悦。 感谢 @墨水 之前在内部分享提供的 demo 版本。

2018/9/3
articleCard.readMore

webpack入坑之旅(零)简介与升级

webpack 基础中的基础。 升级了一下两年前写的这个教程,前端变化太快了,里面很多示例已经跑步起来,终于愿意花时间来更新一下了。非常基础!! 记录 vue-webapck 的学习基础,代码示例 github地址 2018-08-05最新更新: 本教程已升级至 webpack4 ,旧的代码在 webpack1-backup 分支。将原来教程中部分不正确的地方也已经剔除。不过难免还有错误之处,欢迎指正。 代码示例位于 webpack 文件夹中。已经把所有练习的node_modules移除,若要正常使用,请安装运行npm install #推荐 cnpm 。 然后再根据文中的指令,进行打包、编译等操作。重要的是在运行过程中体会。学习。 教程目录: webpack入坑之旅(一)不是开始的开始 webpack入坑之旅(二)loader入门 webpack入坑之旅(三)webpack.config入门 webpack入坑之旅(四)扬帆起航 webpack入坑之旅(五)加载vue单文件组件 webpack入坑之旅(六)配合vue-router实现SPA 这个教程更多的是从基础开始学习,vue 与 webpack 应该要怎么结合。在真实的实际项目中,还是推荐 vue-cli。 关于 vue-cli 生成的配置解析可以参考一下 vue-cli#2.0 webpack 配置分析 react 对于 react 可以考虑使用我基于 vue-cli 生成的配置修改的 react 版本 vue-cli-react-base。 使用 vue-cli 类似的配置与命令,来驱动 react 项目,在 router 分支也有 使用 react + router + antd 的例子 若是有什么地方,没有写对的,也请大家指出,帮忙改进,谢谢!

2018/8/5
articleCard.readMore

中文播客推荐

从大学时期听书开始,慢慢接触到更多的播客。听一些人讲故事,讲技术,感觉也是一个非常不错的了解世界的途径。已经成为我生活中的一部分。不过从身边感觉到播客还是比较小众的。于是想推荐一下我在听的一些播客吧。 引用一句《内核恐慌》的话作为推荐语:“我们虽然号称 Hacker ,但是也没有干货,想听的人就听,不想听的人就别听。” 推荐播客客户端: Moon FM,播客,小宇宙,player.fm IT 技术主题 《内核恐慌》 《内核恐慌》(Kernel Panic) 是吴涛和 Rio 做的播客,首播于 2014 年 10 月。号称硬核,可也没什么干货。想听的人听,不想听的人就别听。 rss 推荐: 类型系统 并发与异步 数学与编程 《Teahour.fm》 Teahour.fm 专注程序员感兴趣的话题,包括 Web 设计和开发,移动应用设计和开发,创业以及一切 Geek 的话题。 rss 推荐: 和 PingCAP CTO 畅谈数据库和编程语言(rust & go 和 Vue.js 框架的作者聊聊前端框架开发背后的故事 与百姓网架构师艾芙聊职业发展和工程师文化 《代码时间》【已停更】 代码时间是一个面向程序员的中文播客节目, 致力于通过语音的方式传播程序员的正能量。 节目的shownotes请移步节目主页。 rss 推荐: ES2015(上) - 贺师俊 ES2015(下) - 贺师俊 Clojure编程语言 – Loretta ggtalk 接地气、有价值的闲聊节目。一帮程序员,在无尽的接需求写代码改 bug 加班上线循环中开辟出来的一块空地,想想过去,聊聊现在,偶尔也展望一下未来。 头发越来越少,经验越来越多;颈椎开始僵硬,头脑依然灵活。代码写多了就想尝试点新东西,聊技术,聊工作,聊生活。挤地铁?又堵车?随便点一期吧,听个乐呵。 rss 推荐: 聊聊跑步这件小事 游戏加速纵横谈 商业科技相关 《疯投圈》 《疯投圈》是一档为创业者、投资人、分析师,以及任何对创业、投资有兴趣的人准备的播客节目。每期节目我们为你深度解剖创投行业新动向。 rss 推荐: 再谈出海电商的全球机遇 复杂服务行业如何平台化 拼多多=中国的 Costco ? 比特新声 《比特新声》是由郝海龙和有才主持的中文科技类播客。在节目中,我们会尽量避免不加解释地使用过于抽象的科技术语,力争让每一个有独立思考能力的人听懂我们的节目。我们坚信凡实验性的东西都有一种独特的魅力,好奇心是第一生产力,同时希望用不同的观点去描述我们所处的时代。 rss 推荐: 一个拥有多线程超能力的开发者应该是什么样的? 迟早更新 「迟早更新」是一档探讨科技、商业、设计和生活之间混沌关系的播客节目,也是风险基金 ONES Ventures 关于热情、趣味和好奇心的音频记录。我们希望通过这档播客,能让熟悉的事物变得新鲜,让新鲜的事物变得熟悉。 rss 字谈字畅 《字谈字畅》是第一档用华语制作的字体排印主题播客节目,由 Eric Liu 与钱争予搭档主播。Type is Beautiful 出品。 rss 声东击西 我们聊技术和创新,也聊文化和电影,这里有一手的现场观察和体验,还有不定期出现的神秘嘉宾。你可以感受星战粉丝大会现场的沸腾,也能想象一下未来世界里的出行,以及美国年轻人都在关心什么新鲜事儿。 rss 推荐: Airbnb 上篇:你不仅能住在别人家,还有人带你玩 Checked 【已停更】 以科技提升效率,用效率改变生活。 rss 推荐: 日历/待办事项/GTD 访谈「也谈钱」: 你的钱是可以给你赚钱的 Pin 开发者——钟颖访谈 《IT 公论》 《IT 公论》由 IPN 出品、不鸟万如一和 Rio 主持,首播于二零一三年十一月。本节目系一种综合性之科技节目。收听对象,并不限于社会上某一阶层。凡职业部门不同,知识水准互异,而对于科技有共同兴趣者,从任何角度,收听此秀,不致味同嚼蜡,毫无所得。一切题材,即就雅俗两极之范围内,伸缩去取,尽量适用多方面之需要,以求俗不伤雅,雅不背时。科技播客,非奇技淫巧之表现也,亦非粉黑二元论争也。盖科技与吾人之关系至密至切,而欲其适合各人之需要,不悖于美之真义,则软件式款,与夫工作生活之配合,用例之转换,必有相当研究方克能之。而欲吾人乐愿研究之,则对于科技之兴趣,必先有以引起之,此《IT 公论》之滥觞也。二零一六年四月停播。 rss WEB VIEW 「不囿于 WEB,不止于 VIEW」,WEB VIEW 是由王隐和敬礼主持的一档泛科技播客。节目中我们谨慎考量技术进步所带来的优缺点,提倡用「人治」的方法重新审视我们的日常生活。 枫言枫语 听见科技与人文的声音。 看看世界 博物志 rss 灭茶苦茶 不伦不类、不易流行。了解日本是不够的,我们要活用日本。不鳥萬如一主理,IPN 出品。 rss 一天世界 一天世界,昆乱不挡。不鳥萬如一主理。IPN 出品。《一天世界》博客 海螺电台 [海螺电台] 播客是一个记录行动和探索过程的创作计划 推荐: 纵使强风起,人生不言弃(箱根山岳险天下!) 当我们在谈跑步时,我们再谈什么 心理学相关 得意忘形 《得意忘形》是一个主张追求个体自由与探寻真理的媒体计划。带着对生命的有限性与无目的性的敬畏,我们试图为读者与听众提供更全面的觉察自我与认知世界的工具,以不断重建当下的方式穿越时间、抵达生活的本质。 rss 推荐: 序言:「无为」与刻意、大脑的双系统、自由主义的危机与开篇絮语 网球:孤独和它所创造的 Blow Your Mind 两个人的公路播客。 rss 知识型 狗熊有话说 独立知识型播客 rss 杂项 UX Coffee 设计咖 《UX Coffee 设计咖》是一档关于用户体验的播客节目。我们邀请来自硅谷和国内的学者和职人来聊聊「产品设计」、「用户体验」和「个人成长」。微信公众号: uxcoffee rss 太医来了 《太医来了》由 IPN 出品、由前骨科医生初洋和妇产科医生田吉顺主持,是中文互联网第一档医生谈话类播客。节目里没有老专家讲养生,只有几个医生聊聊医院里的事儿,顺便给大家做做科普。 rss 黑水公园 《黑水公园》是一档在网络平台定期播出的广播节目,以轻松的对话形式向听众普及科幻电影知识,讲述电影真实故事,并且会定期分享各类优质的科幻作品。 推荐: 《宝石之国》恭喜你发现宝藏了! 《浩瀚苍穹》二百年后的太空战争 十分好看的《白日梦想家》

2018/6/16
articleCard.readMore

学习 Promise,掌握未来世界 JS 异步编程基础

其实想写 Promise 的使用已经很长时间了。一个是在实际编码的过程中经常用到,一个是确实有时候小伙伴们在使用时也会遇到一些问题。 Promise 也确实是 ES6 中 对于写 JS 的方式,有着真正最大影响的 API 特性之一。 本文是实际使用使用过程中的一个总结 看一下文件创建时间 2017-10-09,拖延症真是太可怕了。。。还是得增强执行力啊!不忘初心,加油吧! 前言 && 基础概念 Promise 是解决 JS 异步的一种方案,相比传统的回调函数,Promise 能解决多个回调严重嵌套的问题。 Promise 对象代表一个异步操作,有三种状态: pending、fulfilled 或 rejected ,状态的转变只能是 pending -> fulfilled 或者 pending -> rejected ,且这个过程一旦发生就不可逆转。 个人认为讲解 Promise 实际上需要分成两个部分 对于 Promise 构造函数的使用说明。 Promise 原型对象上的一些方法。 Promise 构造函数 ES6 规定,Promise 对象是一个构造函数,用来生成 Promise 实例。 Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject 。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。 resolve 函数的作用是将 Promise 对象的状态从“未完成”变为“成功”(即从 pending 变为 fulfilled ),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去; reject 函数的作用是,将 Promise 对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected ),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。 下面代码创造了一个 Promise 实例。 1 2 3 4 5 6 7 8 9 10 function request() { return new Promise((resolve, reject) => { /* 异步操作成功 */ setTimeout(() => { resolve("success"); }, 1000); // 取消注释这里可以体现,Promise 的状态一旦变更就不会再变化的特性 // reject('error'); }); } 接收 1 2 3 4 5 6 7 request() .then(result => { console.info(result); }) .catch(error => { console.info(error); }); 上述 new Promise() 之后,除去用 catch 去捕获错误之外,也可以用 then 方法指定 resolve 和 reject 的回调函数 也能达到捕获错误的目的。 1 2 3 4 5 6 7 8 request().then( result => { console.info(result); }, error => { console.info(error); } ); 原型上的方法 Promise.prototype.then() p.then(onFulfilled, onRejected); then 方法 是定义在 Promise.prototype 上的方法,如上面的例子一样,有两个参数,fulfilled 的回调函数和 rejected 的回调函数,第二个参数时可选的。 两个关键点: then 方法的返回值是一个新的 Promise 实例,所以对于调用者而言,拿到一个 Promise 对象,调用 then 后仍然返回一个 Promise ,而它的行为与 then 中的回调函数的返回值有关。如下: 如果 then 中的回调函数返回一个值,那么 then 返回的 Promise 将会成为接受状态,并且将返回的值作为接受状态的回调函数的参数值。 如果 then 中的回调函数抛出一个错误,那么 then 返回的 Promise 将会成为拒绝状态,并且将抛出的错误作为拒绝状态的回调函数的参数值。 如果 then 中的回调函数返回一个已经是接受状态的 Promise,那么 then 返回的 Promise 也会成为接受状态,并且将那个 Promise 的接受状态的回调函数的参数值作为该被返回的 Promise 的接受状态回调函数的参数值。 如果 then 中的回调函数返回一个已经是拒绝状态的 Promise,那么 then 返回的 Promise 也会成为拒绝状态,并且将那个 Promise 的拒绝状态的回调函数的参数值作为该被返回的 Promise 的拒绝状态回调函数的参数值。 如果 then 中的回调函数返回一个未定状态(pending)的 Promise,那么 then 返回 Promise 的状态也是未定的,并且它的终态与那个 Promise 的终态相同;同时,它变为终态时调用的回调函数参数与那个 Promise 变为终态时的回调函数的参数是相同的。 链式调用。把嵌套回调的代码格式转换成一种链式调用的纵向模式。 比如说回调形式: 一个回调地狱的例子 1 2 3 4 5 6 7 8 9 a(a1 => { b(a1, b1 => { c(b1, c1 => { d(c1, d1 => { console.log(d1); }); }); }); }); 这样的横向扩展可以修改成(a,b,c,d)均为返回 Promise 的函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 a() .then(b) .then(c) .then(d) .then(d1 => { console.log(d1); }); //===== 可能上面的例子并不太好看 ===下面这样更直观 a() .then(a1 => b(a1)) .then(b1 => c(b1)) .then(c1 => d(c1)) .then(d1 => { console.log(d1); }); 这样的纵向结构,看上去清爽多了。 Promise.prototype.catch() 除了 then() ,在 Promise.prototype 原型链上的还有 catch() 方法,这个是拒绝的情况的处理函数。 其实 它的行为与调用 Promise.prototype.then(undefined, onRejected) 相同。 (事实上, calling obj.catch(onRejected) 内部 calls obj.then(undefined, onRejected)). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 1. request().then( result => { console.info(result); }, error => { console.info(error); } ); // 2. request() .then(result => { console.info(result); }) .catch(error => { console.info(error); }); 如上这个例子:两种方式在使用,与结果基本上是等价的,但是 仍然推荐第二种写法,下面我会给出原因: 在 Promise 链中 Promise.prototype.then(undefined, onRejected),onRejected 方法无法捕获当前 Promise 抛出的错误,而后续的 .catch 可以捕获之前的错误。 代码冗余 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 new Promise((resolve, reject) => { setTimeout(() => { resolve("reject"); }, 1000); }) .then( result => { console.log(result + "1"); throw Error(result + "1"); // 抛出一个错误 }, error => { console.log(error + ":1"); // 不会走到这里 } ) .then( result => { console.log(result + "2"); return Promise.resolve(result + "2"); }, error => { console.log(error + ":2"); } ); // reject1, Error: reject1:2 如果使用 .catch 方法,代码会简化很多,这样实际上是延长了 Promise 链 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 new Promise((resolve, reject) => { setTimeout(() => { resolve("reject"); }, 1000); }) .then(result => { console.log(result + "1"); throw Error(result + "1"); // 抛出一个错误 }) .then(result => { console.log(result + "2"); return Promise.resolve(result + "2"); }) .catch(err => { console.log(err); }); // reject1, Error: reject1:2 Promise.prototype.finally() 暂未完全成为标准的一部分,处于:Stage 4 finally() 方法返回一个 Promise,在执行 then() 和 catch() 后,都会执行finally指定的回调函数。(回调函数中无参数,仅仅代表 Promise 的已经结束 等同于使用 .then + .catch 延长了原有的 Promise 链的效果,避免同样的语句需要在 then() 和 catch() 中各写一次的情况。 mdn-Promise-finally Promise 对象上的方法 Promise.all() 用来处理 Promise 的并发 Promise.all 会将多个 Promise 实例封装成一个新的 Promise 实例,新的 promise 的状态取决于多个 Promise 实例的状态,只有在全体 Promise 都为 fulfilled 的情况下,新的实例才会变成 fulfilled 状态。;如果参数中 Promise 有一个失败(rejected),此实例回调失败(rejecte),失败原因的是第一个失败 Promise 的结果。 举个例子: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 Promise.all([ new Promise(resolve => { setTimeout(resolve, 1000, "p1"); }), new Promise(resolve => { setTimeout(resolve, 2000, "p2"); }), new Promise(resolve => { setTimeout(resolve, 3000, "p3"); }) ]) .then(result => { console.info("then", result); }) .catch(error => { console.info("catch", error); }); // [p1,p2,p3] Promise.all([ new Promise(resolve => { setTimeout(resolve, 1000, "p1"); }), new Promise(resolve => { setTimeout(resolve, 2000, "p2"); }), Promise.reject("p3 error") ]) .then(result => { console.info("then", result); }) .catch(error => { console.info("catch", error); }); // p3 error 获取 cnode 社区的 精华贴的前十条内容 1 2 3 4 5 6 7 8 9 10 11 12 fetch("https://cnodejs.org/api/v1/topics?tab=good&limit=10") .then(res => res.json()) .then(res => { const fetchList = res.data.map(item => { return fetch(`https://cnodejs.org/api/v1/topic/${item.id}`) .then(res => res.json()) .then(res => res.data); }); Promise.all(fetchList).then(list => { console.log(list); }); }); Promise.race() 竞态执行 Promise.race 也会将多个 Promise 实例封装成一个新的Promise实例,只不过新的 Promise 的状态取决于最先改变状态的 Promise 实例的状态。 在前端最典型的一个用法是为 fetch api 模拟请求超时。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Promise.race([ fetch("https://cnodejs.org/api/v1/topics?tab=good&limit=10").then(res => res.json() ), new Promise((resolve, reject) => { setTimeout(reject, 1, "error"); }) ]) .then(result => { console.info("then", result); }) .catch(error => { console.info("catch", error); // 进入这里 }); 上述例子中只要请求 未在 1 毫秒内结束就会进入 .catch() 方法中,虽然不能将请求取消,但是超时模拟却成功了 Promise.resolve(value) && Promise.reject(reason) 这两个方法都能用来创建并返回一个新的 Promise , 区别是 Promise.resolve(value) 携带进新的 Promise 状态是 fulfilled。而 Promise.reject(reason) 带来的 rejected 有的时候可以用来简化一些创建 Promise 的操作如: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 const sleep = (time = 0) => new Promise(resolve => setTimeout(resolve, time)); // 这里创建一个 睡眠,并且打印的链 Promise.resolve() .then(() => { console.log(1); }) .then(() => sleep(1000)) .then(() => { console.log(2); }) .then(() => sleep(2000)) .then(() => { console.log(3); }); 有时也用来 手动改变 Promise 链中的返回状态 ,当然这样实际上和 直接返回一个值,或者是 使用 throw Error 来构造一个错误,并无区别。到底要怎么用 就看个人喜好了 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 new Promise((resolve, reject) => { setTimeout(() => { resolve("resolve"); // 1. }, 1000); }) .then(result => { return Promise.reject("reject1"); // 2. }) .then( result => { return Promise.resolve(result + "2"); }, err => { return Promise.resolve(err); // 3. } ) .then(res => { console.log(res); // 4. }) .catch(err => { console.log(err + "err"); }); // reject1 几个例子 下面来看几个例子: 关于执行顺序,具体可搜索,js 循环 1 2 3 4 5 6 7 8 9 10 new Promise((resolve, reject) => { console.log("step 1"); resolve(); console.log("step 2"); }).then(() => { console.log("step 3"); }); console.log("step 4"); // step 1, step 2, step 4 , step 3 在使用 Promise 构造函数构造 一个 Promise 时,回调函数中的内容就会立即执行,而 Promise.then 中的函数是异步执行的。 关于状态不可变更 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 let start; const p = new Promise((resolve, reject) => { setTimeout(() => { start = Date.now(); console.log("once"); resolve("success"); }, 1000); }); p.then(res => { console.log(res, Date.now() - start); }); p.then(res => { console.log(res, Date.now() - start); }); p.then(res => { console.log(res, Date.now() - start); }); Promise 构造函数只执行一次,内部状态一旦改变,有了一个值,后续不论调用多少次then()都只拿到那么一个结果。 关于好像状态可以变更 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve("success"); }, 1000); }); const p2 = p1.then((resolve, reject) => { throw new Error("error"); }); console.log("p1", p1); console.log("p2", p2); setTimeout(() => { console.log("p1", p1); console.log("p2", p2); }, 2000); 观察这一次的打印 第一次打印出两个 Promise 的时候都是 pending ,因为 p2 是基于 p1 的结果,p1 正在 pending ,立即打印出的时候肯定是 pending ;第二次打印的时候,因为 p1 的状态为 resolved ,p2 为 rejected ,这个并不是已经为 fulfilled 状态改变为 rejected ,而是 p2 是一个新的 Promise 实例,then() 返回新的 Promise 实例。 关于透传 1 2 3 4 5 6 7 8 Promise.resolve(11) .then(1) .then(2) .then(3) .then(res => { console.info("res", res); }); // 11 给 then 方法传递了一个非函数的值,等同于 then(null),会导致穿透的效果,就是直接过掉了这个 then() ,直到符合规范的 then() 为止。 Promise 的串行调用 使用 Array.reduce 方法串行执行 Promise 1 2 3 4 5 6 7 8 const sleep = (time = 0) => new Promise(resolve => setTimeout(resolve, time)); [1000, 2000, 3000, 4000].reduce((Promise, item, index) => { return Promise.then(res => { console.log(index + 1); return sleep(item); }); }, Promise.resolve()); // 在分别的等待时间后输出 1,2,3,4 这篇文章到这里就基本上结束了,相信 如果能理解上面的内容,并且在实际项目中使用的话。应该会让工作更高效吧,对于新的异步使用应该也会更加的得心应手。Promise 的使用相对简单,可能后续再出一篇如何实现一个 Promise 吧 那些收集的 Promise 的优质文章。 bluebird 是一个拓展 Promise 方法的库,提供了非常多的实用的方法,推荐 [思维导图] Promise - 《你不知道的 JavaScript》- 中卷 - 第二部分 [译] 一个简单的 ES6 Promise 指南 阮一峰-ES6 入门 Promise 对象 Promise 不够中立 WHY “PROMISES ARE NOT NEUTRAL ENOUGH” IS NOT NEUTRAL ENOUGH 【译】关于 Promise 的 9 个提示 Promise 必知必会(十道题) Event Loop 必知必会(六道题)

2018/6/4
articleCard.readMore

2017 杭州 nodeParty 记录

本文记录于 2017 杭州 丁香园 nodeParty 期间,由于是流水帐式记录,一直都偷懒没发。最近心态又有点改变,抽了一点时间来发布。 最近在工作中的任务对于 node 的任务越来越重了,正好看到新一期的 Node Party 开办了,于是当天就报名了,想看看大家 是怎么在用 Node.js 的。 好了,废话不多说,我来从时间顺序来聊一下我参加这次会议听到的东西 首先是开场,我在一开始的时候,坐在最后一排,然后就听到 贺老和负责人在聊: 贺老说不需要准备 ppt,直接现场写代码。 结果说到我们今天有直播,不能用自己的电脑。 贺老就说不能总是这样啊。。本来现场写代码压力就大,还不用自己电脑,压力就更大了。 于是我果断搭话,说贺老你指的是在 360 分享 用 TS 写 benchmark 工具的那次么。 贺老笑了笑说是的。 没再多说,之后就是正式的分享了。 第一个主题:《通过 GraphQL 向 RN 透出实时报表》 这个话题一开始在介绍了宋小菜的业务场景,提出了这样的场景下对于报表的各式需求,然后如何去进行考量技术选型。以及在选定了 GraphQL 之后,是怎么样一步一步的去改造,去利用到其中的特性,和一些配套的解决方案,去解决实际问题吧。虽然有提 RN ,但是实际上基本上没这部分的内容。 GraphQL 在很早之前听过,在听完之后只能说是多了一些了解吧,如果没有真正对应的需求,可能也不会再去了解更多也说不定。只是知道了一样一种解决方案。 第二个主题:《Node.js多线程实践》 这个话题听不明白,我基本上对于操作系统以及线程进程是没有理解或者说太多的概念的。 主要是说他写了一个 node-webworker 的库,以及能解决一些什么问题 第三个主题:《STC vs PTC》 经过中场休息,贺老上台,感觉大家确实都变得有精神了很多。贺老这次没现场写代码了, 讲了 对于 js 中 尾递归优化的实现时,T39 不同厂商之间产生的分歧,以及两种实现上各自的问题。整个演讲非常流程,也很生动。这个话题的话,贺老应该很久之前在某一个会议上讲过,好像是 Qcon?忘记了,我只看了 PPT。 这个话题,主要还是介绍一些 T39 八卦吧,以及如何去演讲表达自己。 ##《Node.js在一家大数据服务创业公司的应用实践》 这里首先讲了 关于一个内部样板项目的生成器。也就是大家说的 cli 工具。然后讲到了 nodejs 的项目管理,主要是 pm2 出除去命令行之外,直接用他暴露的 api 在 js 文件里面的使用吧,这样会更加方便于管理应用的生命周期。 这里的内容,很多都是在真正业务实践中会遇到的问题吧,只可惜目前我对于 node 要做的事情还很少,所以理解不深,因为现在我们这里就是 ssh 到服务器上 pm2 restart 的。。-_- ##《妹子程序员的自我修养》 到了天哥的话题,这里主要说了一些社会上对于妹子程序员的一些刻板印象。虽然有很多也是事实,因为社会对于程序员也有刻板印象嘛。不过庆幸的是,我身边基本上还都没有这样的情况发生。 然后提问题的时候,也有很多的同学在问个人成长相关的事情,主要是应对 学习新知识的学习焦虑吧,天哥说到一定要喜欢技术才能真正的钻研到里面去。不然的话还是可以考虑比重的,比如偏管理方向多一点。 当然这里也提到一个问题,就是当我们自己去学习一个技术的时候,可能一个星期都没学会,但如果公司有一个具体的需求,需要用到这个技术,可能2天就搞定了。这个其实值得大家都思考一下。带着明确的目标(足够细化,足够可以被量化)去学习东西的感觉是真的不一样的。所以就算你对于技术的热情没有那么高,但是你一定需要让自己的某一个目标,去驱动自己学习 最后的是圆桌话题 《技术变现》 这个环节,主要就是听听大家聊天,然后说一些经验之谈吧。 里面提到了 知乎 Live 是收智商税,嘿嘿,然后贺老有解释说,其实他还是花了很多时间去准备的,里面也确实是真实案例还是很多干货的,赚钱是机缘巧合的事情。 其实对于这个问题真是这样,大家也别太在意说买 live 是被收智商税什么的,其实只要对于你真实有用就可以了。不需要太多的去考虑别人怎么说,能带来价值的就是有用。 然后有说道做外包相关的事情,其实我在大学的时候也做过外包那个时候学习前端才几个月吧然后和一个后端的小伙伴两个人一直使用QQ语音聊天沟通,算是远程工作吧两个月下来一个人到时候有个几千块块钱吧,其实我觉得这个,对于我当时的一个学生来说已经很不错了。所以我觉得学生时候是完全可以去接接外包去尝试一下,知道具体开发程序是什么样子。工作之后的话,就还需要权衡了。 后来就基本在和之前的主管聊天了,也是这次最大的收获,我们聊了互相工作的一些转变,然后也对于个人成长做了一些讨论。受益良多。 最后总结一个点吧。 还是需要多沟通,有了信息之后才能更好的去做一个决定,和完成好一件事情

2018/3/11
articleCard.readMore

一次canvas中文字转化成图片后清晰度丢失的探索

本文最初记录在 2017 年 D2 期间。知乎问题为:参加第12届D2前端技术论坛,你有什么收获? 主要是想说一下百度的小姐姐分享的话题 《打造前端复杂应》。 最近正好接手了这样一个类似的项目,(百度 h5 百度脑图)不过整体全是基于 canvas 的,而且也没有事件广播,没有数据驱动,没有模型,全是直接 jquery 直接操作 DOM 的。整体感觉很混乱。现在一个人维护,改起来,感觉很忧伤。 不过听完分享之后,不管是事件广播,还是直接数据的双向绑定,都让我多了很多思路去改造。目前第一步就是先加入了 webpack、ES6、React、PubSub ,先把之前的 jquery 逐渐干掉吧。 不过小姐姐提到百度脑图 和 百度 H5 分别使用 SVG 以及 原生 DOM 来实现的,这样当然没什么问题,不过也提到为什么没用 canvas 的原因,主要是 事件绑定,元素选择相关的等等一些方面的考虑。 但是实际上,根据我项目的使用来说,直接使用 canvas 也是可以的,基本能解决掉提出来的顾虑,当然主要是有这个强大的开源 canvas 操作库: fabric.js: Javascript Canvas Library, SVG-to-Canvas (& canvas-to-SVG) Parser 事件,动画,选中,变换,loadJOSN(),toJSON() 。强大之处,不太好用几句话说清楚,部分功能如下图,建议官网体验: 上面一堆都是废话。 下面是想要说的一个问题,本来想在现场提出来的,但是没被点名到。不过有一个小哥提到了百度 H5 在导出图片的清晰度的问题,说截图都比导出的图片清晰,然后每次做完都只能手动截图。。 我遇到的也类似。不过,经过排除主要聚焦在字体。(其实我觉得那个小哥,也应该是图片中的字体不够清晰) 在将 dom / canvas 生成图片时,都会发现其中的文本在不同系统平台下有着非常大的清晰度差异(和所选的字体也有一定相关)。(说到字体,就会谈到中文字体的子集化,和 WebFont 动态生成这个也很有意思。推荐一个开源库font-carrier:很久没更新了,依赖被写死,需慎重。 这里不谈) 下图有四种图片生成方式: 本地 qq 截图;psd 导出; mac phantomjs 网页截图 ; CentOS phantomjs 网页截图。(未按顺序,清晰度各有不同,最底部是一行是图片),查看大图。。 相信可以很清楚的看出来其中存在的巨大差异。。。 。。。。在排除 phantomjs 配置问题,已经字体是否生效的问题。。(也试了在 window 下使用 phantomjs 网页截图 效果更差。) 最后在我这里给出的结论就是生成图片的清晰度主要受两方面影响(其实浏览器也一样?): 1. 不同的操作系统底层对于字体的渲染原理和方式差异。 2. 字体原本选用的字体生成类型 TrueType / OpenType 等特性差异 然后问题暂时在我这到此为止了。以上两个问题,我目前的能力都还解决不了。。。 欢迎打脸。。。也欢迎有类似问题的一起聊聊 2018-03-05 记录 上面清晰度的问题已经解决。 同时购入 mac server 与 windows server 用于图片生成。 ( mac 的文字显示效果与 mac 电脑的一致。(相对于 windows 来说 加黑加粗了 弃用了 phantomjs 改用 google 出品的 Puppeteer。 截图选项使用 png (即使是 quality 100 的 jpg 仍旧渲染会有问题。如果 png 文件过大, 再使用 imagemagick / GraphicsMagick 进行压缩。

2018/3/5
articleCard.readMore

从零学习 canvas (一)

由于上一篇描述的原因。有图像处理的需求,于是我就开始学习 canvas 啦,和以前的一样,这一篇也是一边学一边写,敲出来的。有不正确的地方,欢迎指出。 canvas 本身的 api 描述是比较简单,但是衍生出来的东西,操作,图像处理,动画,性能,还是非常的多的。所以对于 canvas 的学习不出意外的话,将会是一个系列。这就是第一篇了。下面就开始吧 前言 1 <canvas id="canvas" style="background:blue;">浏览器不支持canvas</canvas> 在不支持 canvas 的浏览器中,显示标签中的内容。 绘图区域 默认是 300 x 150。 canvas 中的宽高是实际的宽高,css 中的宽高会等比缩放。 在开始绘图之前需要先,获取绘图环境。 1 2 3 4 5 const canvas = document.querySelector('#canvas'); if(canvas.getContext){ const context = canvas.getContext('2d'); // .... 绘制 } API 绘制方块 fillRect(x, y, width, height):绘制矩形,默认黑色 strokeRect(x, y, width, height):带边框的矩形,默认黑色,默认 1px 。但是显示出来可能有区别 clearRect(x, y, width, height) 清除指定矩形区域,让清除部分完全透明。 1 2 3 4 5 6 { // 边框实际上被加粗了 context.strokeRect(100,100,50,50); // 正常边框 1px context.strokeRect(160.5,160.5,50,50); } 设置绘图样式 fillStyle = color: 填充颜色(绘制 canvas 是有顺序的) lineWidth = value: 线宽度,是一个数值 strokeStyle = color:边线颜色 1 2 3 4 5 6 7 { context.strokeStyle='rgba(0,0,255,0.5)'; context.lineWidth=5; // 调整 fillRect/ strokeRect 的顺序将有不一样的表现 context.strokeRect(160.5,160.5,50,50); context.fillRect(160.5,160.5,50,50); } 边界绘制 lineJoin = type:边界连接点样式 miter/默认;round/圆角;bevel/斜角 lineCaP = type:端点样式 butt/默认;round/圆角;square/高度多出未为宽一半的值 绘制路径 beginPath():开始绘制路径 closePath():结束绘制路径(,不是必需的) moveTo(x,y):移动到绘制的点,坐标x以及y lineTo(x,y):绘制一条从当前位置到指定x以及y位置的直线。 fill(): 填充 stroke(): 边框 1 2 3 4 5 6 7 8 9 10 11 { context.beginPath(); context.moveTo(100,100); context.lineTo(150,100); context.lineTo(100,150); context.closePath(); // 填充 context.fill(); // 边框 context.stroke(); } 绘制弧 arc(x, y, radius, startAngle, endAngle, anticlockwise):绘制圆 x,y 起始坐标点,radius 半径大小。 startAngle ,endAngle。 圆弧的起始与结束,x轴方向开始计算,单位以弧度表示。弧度 = 角度 * Math.PI/180 anticlockwise 可选的Boolean值 ,如果为 true,逆时针绘制圆弧,反之,顺时针绘制。 arcTo(x1, y1, x2, y2, radius):根据给定的控制点和半径画一段圆弧,再以直线连接两个控制点.(不建议使用。) quadraticCurveTo(cp1x, cp1y, x, y):绘制二次贝塞尔曲线,cp1x,cp1y为一个控制点,x,y为结束点。 bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) 绘制三次贝塞尔曲线,cp1x,cp1y为控制点一,cp2x,cp2y为控制点二,x,y为结束点 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { context.moveTo(200,200); context.arc(200,200,150,0,90 * Math.PI / 180, true); // context.closePath(); context.stroke(); context.moveTo(100,150); context.arcTo(100,100,200,100,50); context.stroke(); // 贝塞尔曲线 context.beginPath(); context.moveTo(75,25); context.quadraticCurveTo(25,25,25,62.5); context.quadraticCurveTo(25,100,50,100); context.quadraticCurveTo(50,120,30,125); context.quadraticCurveTo(60,120,65,100); context.quadraticCurveTo(125,100,125,62.5); context.quadraticCurveTo(125,25,75,25); context.stroke(); } 状态的保存与恢复 save():保存路径 restore():恢复路径 变换 translate(x, y):偏移。x 是左右偏移量,y 是上下偏移量,如右图所示。 rotate(angle): 旋转。旋转的角度(angle),它是顺时针方向的,以弧度为单位的值。 scale(x, y) :缩放。x,y 分别是横轴和纵轴的缩放因子,它们都必须是正值。值比 1.0 小表示缩小,比 1.0 大则表示放大,值为 1.0 时什么效果都没有。 transform(m11, m12, m21, m22, dx, dy) 1 2 3 4 context.translate(200,100); context.rotate(Math.PI / 180 * 30); context.scale(1.6,1); context.fillRect(10,10,50,50); 实例 画板 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 (function(){ document.body.innerHTML='<canvas id="canvas" width="1000" height="1000">浏览器不支持</canvas>'; var canvas = document.querySelector('#canvas'); if(!canvas.getContext) return; var context = canvas.getContext('2d'); function move(ev){ const left = ev.clientX - canvas.offsetLeft; const top = ev.clientY - canvas.offsetTop; context.lineTo(left,top); context.stroke(); } canvas.addEventListener('mousedown',(ev)=>{ const left = ev.clientX - canvas.offsetLeft; const top = ev.clientY - canvas.offsetTop; context.moveTo(left,top); document.addEventListener('mousemove',move); }) document.addEventListener('mouseup',()=>{ document.removeEventListener('mousemove',move); }) })(); 旋转的小方块 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 (function(){ document.body.innerHTML='<canvas id="canvas" width="1000" height="1000">浏览器不支持</canvas>'; var canvas = document.querySelector('#canvas'); if(!canvas.getContext) return; var context = canvas.getContext('2d'); let num = 0; let num2 = 0; let value = 1; // context.moveTo(200,200); context.translate(200,200); function start(){ num++; context.save(); context.fillStyle="#fff"; context.fillRect(-200, -200, canvas.width, canvas.height); if(num2===100){ value = -1; }else if(num2===0){ value = 1; } num2 += value; context.scale(num2 / 50,num2 / 50); context.rotate(num * Math.PI / 180); context.translate(-50,-50); context.fillStyle="#000"; context.fillRect(0,0,100,100); context.restore(); requestAnimationFrame(start); } start(); })(); 时钟 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 (function() { document.body.innerHTML='<canvas id="canvas" width="1000" height="1000">浏览器不支持</canvas>' const canvas = document.querySelector('#canvas'); if (!canvas.getContext) return; let context = canvas.getContext('2d'); function time() { context.clearRect(0, 0, canvas.width, canvas.height); let x = 200; let y = 200; let r = 150; // 绘制秒的刻度 context.lineWidth = 1; context.beginPath(); Array(60).fill(0).forEach((item, index) => { context.moveTo(x, y); context.arc(x,y,r,6 * index * Math.PI / 180,6 * (index + 1) * Math.PI / 180,false); }); context.closePath(); context.stroke(); // 清空中间的部分 context.fillStyle = '#fff'; context.beginPath(); context.moveTo(x, y); context.arc(x, y, r * 20 / 21, 0, 360 * Math.PI / 180, false); context.closePath(); context.fill(); // 绘制分钟刻度 context.lineWidth = 2; context.beginPath(); Array(12).fill(0).forEach((item, index) => { context.moveTo(x, y); context.arc(x,y,r,30 * index * Math.PI / 180,30 * (index + 1) * Math.PI / 180,false); }); context.closePath(); context.stroke(); context.fillStyle = '#fff'; context.beginPath(); context.moveTo(x, y); context.arc(x, y, r * 19 / 21, 0, 360 * Math.PI / 180, false); context.closePath(); context.fill(); // 计算时针,分针,秒针的旋转角度 var date = new Date(); var hour = date.getHours(); var minute = date.getMinutes(); var second = date.getSeconds(); var hourValue = (-90 + hour * 30 + minute / 2 + second / 60) * Math.PI / 180; var minuteValue = (-90 + minute * 6 + second / 12) * Math.PI / 180; var secondValue = (-90 + second * 6) * Math.PI / 180; // 小时 context.lineWidth = 4; context.beginPath(); context.moveTo(x, y); context.arc(x, y, r * 8 / 21, hourValue, hourValue, false); context.closePath(); context.stroke(); // 分 context.lineWidth = 2; context.beginPath(); context.moveTo(x, y); context.arc(x, y, r * 15 / 21, minuteValue, minuteValue, false); context.closePath(); context.stroke(); // 秒 context.beginPath(); context.moveTo(x, y); context.arc(x, y, r * 18 / 21, secondValue, secondValue, false); context.closePath(); context.stroke(); // 重新开始 setTimeout(time, 1000); } time(); })();

2017/10/24
articleCard.readMore

浅谈 electron 中的 session 管理(隔离)

已经有很长一段时间没有产出博客了。 一. 是因为花了很多时间去专研业务,能够做到目前的基本业务流程理清,大致了然于胸(导致了一个问题:有人找我解决问题,我可能会先问一句,你的需求是什么?) 二. 确实是自己这一段时间确实懈怠了,每天上班回去就不想敲代码了,看看剧,看看小说,刷刷微博。虽然在组内有过一些分享,整理过一些东西,但是却没有将其在博客产出了。 这样的情况,让我明显的感觉到自己的成长速度相对于第一年成长的速度,慢了几个等级。这让我有一种危机感,于是克服这种懈怠,跳出舒适区,继续强健自己。重新回归吧。 最后。还是引用这个博客的描述:”兴趣遍地都是,专注和坚持才是真正稀缺的。” 不多说了,开始吧。 基础介绍 由于公司的项目内部调整,有幸接触 2 个星期的 electron 开发。(然后我又被拥抱变化了。。)实现了一个多账号的切换,并且同时对于多账号的聊天窗口做一个浏览器 tab 的集成的需求,这里对于接触到的知识点,做一个总结。以免完全忘记(忽略代码规范,我自己都看不下去) electron-中文文档 在我加入项目之前,壳就已经搭好了,我只是在之上去开发。然而我接触时间太短,然后就撤离了,只能说一些我看到的和用到的部分。( 其它存在的问题, 比如:安全,目前没有更多的精力去解决) 了解之后,最开始的项目搭建是使用的 electron-quick-start 来快速的构建出 一个 electron 客户端的项目。 由于项目需要快速迭代和试错。也没有使用大多数客户端项目将所有资源存在本地,然后再去更新本地资源的形式,而是在客户端暴露 sdk 的情况下 直接 load 了一个 远程地址 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const electron = require('electron') const {BrowserWindow} = electron const path = require('path') let mainWindow = new BrowserWindow({ width: 1280, height: 768, icon: path.resolve(__dirname ,'./build/icon.png'), title:'客户端', webPreferences: { webSecurity: false, allowRunningInsecureContent: true, preload: path.resolve(path.join(__dirname, './common/sdk.js')) } }) mainWindow.loadURL('https://baidu.com',{extraHeaders: 'pragma: no-cache\n'}) SDK 部分,实际上也没有做太多的封装,直接就暴露出来了。 大概是下面这些 electron-json-storage node-notifier superagent 1 2 3 4 5 6 { const storage = require('electron-json-storage'); // 缓存 const notifier = require('node-notifier') // 通知功能 const charset = require('superagent-charset'); const request = charset(require('superagent')); // HTTP } 关于此项目 electron 暴露出来的内容,我能聊的大致就是这些了。至于图标,打包成可安装文件,客户端快捷键的设置,并没有太认真的去看,不过应该在网上多搜索,是能找到答案的。 electron-github-issues 实际上能解决绝大多数的问题。 session 模块 关于 electron session 模块,就和文档中的描述一致,session 模块可以用来创建一个新的 Session 对象,然后 有 session.fromPartition(partition) 进行自定义的设置。 你也可以通过使用 webContents 的属性 session 来使用一个已有页面的 session (webContents 是 BrowserWindow 的属性.) 在经过实际的测试发现,在主进程之外无法直接使用electron.session 来获取到 session 对象:{ defaultSession: [Getter], fromPartition: [Function] } 所以在最后,我只能是通过 webContents 中的 session 来处理。 当然就算是这样,也有很多解决方案,但是我目前使用了我认为最简单的一个。直接修改 本地所有的 cookies。 在 BrowserWindow 中 在文档中发现 可以直接在用 BrowserWindow 是可以直接通过 webPreferences 参数来对于 session 进行最初的设置的。 webPreferences 参数是个对象,它的属性: session Session - 设置界面 session. 而不是直接忽略 session 对象 , 也可用 partition 来代替, 它接受一个 partition 字符串. 当同时使用 session 和 partition , session 优先级更高. 默认使用默认 session . partition String - 通过 session 的 partition 字符串来设置界面 session. 如果 partition 以 persist: 开头, 这个界面将会为所有界面使用相同的 partition. 如果没有 persist: 前缀, 界面使用历史 session. 通过分享同一个 partition, 所有界面使用相同的 session. 默认使用默认 session. 在 webview 中 1 2 <webview src="https://github.com" partition="persist:github"></webview> <webview src="http://electron.atom.io" partition="electron"></webview> 在 webview 中同样支持 partition 的设置。规则同上。 但是除此之外 webview 也同样提供了一个方法 <webview>.getWebContents()去获取 到 webview 所属的 webContents。 这样的话,我们也可以直接使用它 session 的属性进行处理 店铺切换 登陆窗口 首先我们需要去创建出一个登陆窗口去让用户把账号给添加到目前的登陆流程中来。然后通过回调函数,将一个必要信息传到主窗口做登陆完成的处理(或者是使用 ipcMain EventEmitter 形式,最终只是需要拿到值。) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 { /** * 创建一个 登录 的窗口。 * 用于 session 隔离 * Promise 中有 {partition,userinfo,cookies} * @returns Promise */ function createLoginWin(partition){ partition = partition || `persist:${Math.random()}`; const charset = require('superagent-charset'); const request = charset(require('superagent')); // HTTP let BrowserWindow = new require('electron').remote.BrowserWindow; let presWindow = new BrowserWindow({ width: 1280, height: 768, title:'用户登陆', webPreferences: { webSecurity: false, allowRunningInsecureContent: true, partition } }); let webContents = presWindow.webContents; return new Promise(function(resove,reject){ // webContents.openDevTools(); presWindow.loadURL('http://taobao.com/#/login'); webContents.on("did-navigate-in-page", function() { // 这里可以看情况进行参数的传递,获取制定的 cookies webContents.session.cookies.get({},function(err,cookies){ if(err){ presWindow.close(); // 关闭登陆窗口 return reject(err); } // 这一步并不是必需的。 request .get('http://taobao.com/userinfo') .query({ _: Date.now() }) // query string .set("Cookie", cookies.map(item=>`${item.name}=${item.value};`).join(' ')) .end(function(err,res){ presWindow.close(); if(err) {return reject(err);} if(!res || !res.body || !res.body.result !== 1){ return reject(res.body) } let obj = { partition,cookies,userinfo:res.body.data} resove(obj); }) }) }); }) } } 至于信息的存储的话,是使用了 electron-json-storage 将用户的值存储到本地。这里可以随意。 切换用户 上面只是创建了新用户登录的窗口。那么对于旧有的(目前登录)用户信息,做一个初始化同步存储下来的操作。(保持结构一致,(除了 partition 不存在之外))为了后续的 使用方便,可以封装几个对于当前窗口 cookies 操作的函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 const cookies = { getCurrCookies(params={}){ let currWin = require('electron').remote.getCurrentWindow(); let currSession = currWin.webContents.session; return new Promise((resove,reject)=>{ currSession.cookies.get(Object.assign({},params),function(err,cookies){ if(err){return reject(err);} resove(cookies); }) }) }, removeCurrCookies(cookies = []){ let currWin = require('electron').remote.getCurrentWindow(); let currSession = currWin.webContents.session; let err = []; let apiCount = 0; return new Promise((resove,reject)=>{ cookies.forEach(item=>{ currSession.cookies.remove(`http://${item.domain}`,item.name ,function(err){ if(err){return err.push(err);} apiCount = apiCount + 1; }) if(err.length === apiCount){ resove({message:'cookie 清除成功'}); }else{ reject(err); } }) }) }, setCurrCookies(cookies = []){ let currWin = require('electron').remote.getCurrentWindow(); let currSession = currWin.webContents.session; let err = []; let apiCount = 0; return new Promise((resove,reject)=>{ cookies.forEach(item=>{ currSession.cookies.set(Object.assign({},item,{ url:`http://${item.domain}`, name:item.name }),function(err){ if(err){ return err.push(err) } apiCount = apiCount + 1; }) if(err.length === apiCount){ resove({message:'cookie 设置成功!'}); }else{ reject(err); } }) }) } } 有了这几个函数。结合我们上面,将用户登录信息保存下来的部分,切换店铺就变得异常简单了。 流程如下: 获取当前 –> 清除当前 –> 获取目标 –> 设置当前 –> 重新载入 多 webview 聊天窗口 先来上一个截图。 在我的使用中,直接将聊天窗口创建出来,一个新的 BrowserWindow ,html 中会创建多个 webview 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function openChatTool(data=[]){ // 需要打开的聊天窗口集合,里面会有我们在上面存下来的信息 let random = Math.random(); let BrowserWindow = new require('electron').remote.BrowserWindow; let presWindow = new BrowserWindow({ width: 1280, height: 768, title:'聊天窗口', webPreferences: { webSecurity: false, allowRunningInsecureContent: true, } }); // presWindow.webContents.openDevTools(); presWindow.loadURL(`http://${location.host}/chat.html?v=${Math.random()}`); presWindow.webContents.on('did-finish-load', function() { // 使用了 send 方法在线程中进行信息传递,在 chat.html 中 可以使用 ipcRenderer接受 如:electron.ipcRenderer.on('chatList',()=>{}) presWindow.webContents.send('chatList', data); }); } chat.html 中 tab 切换的部分在此直接略过 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const electron = require('electron'); electron.ipcRenderer.on('chatList', function(event, data) { console.log(data); const webview = document.createElement('webview'); webview.allowpopups = true; webview.disablewebsecurity = true; webview.className = index == 0 ? 'active' : ''; // 直接使用 之前存下来的 partition,是最简单的形式 // 当然也可以不使用这个,在下面的事件中 session.cookies.set 将 cookie 设置进去 webview.partition = item.partition || `persist:${Math.random()}`; webview.src="http://chat.com"; document.body.appendChild(webview); webview.addEventListener('did-finish-load',function(event){ let webviewContents = webview.getWebContents(); if(webview.getURL()=='http://chat.com/index'){ webviewContents.webContents.session.cookies.get({},function(err,cookies){ // 处理登录失效。重新登录的逻辑。还需要结合别的事件来处理 // 这里可以直接拿到 webview 内的 session 信息 // 代码略 // 可以在外部插入代码 webview.executeJavaScript(`console.log(11)`,()=>{ console.log('insert dom success')}) }) } }) }) 这样需求就搞定了,但是实际上我用到的只是非常少的一部分,并且完成的也不算好。 单单是一个 session 模块中的东西我也还有很多没有去详细的尝试和理解的。不过这个需求整体下来,感觉 electron 还是非常有趣的。只不过接触的时间还太短,没有挖掘出更多有好玩的东西,要是之后有了时间,可以考虑用他写一个自己的应用吧。

2017/10/21
articleCard.readMore

FlexBox 布局详解

很久没有写博客了,这里把之前学习 flex 布局的一篇笔记整理了一下。发布到博客上。赶一个五月的末班车吧。还是得坚持啊!! flex 弹性布局 FlexBox 可控制子元素: 水平或垂直排成一行 控制子元素的对齐方式 控制子元素的高度/宽度 控制子元素的显示顺序 控制子元素是否折行 ** display:flex; 创建 Flexbox 元素 ** 在 flex 布局中必须理解的概念就是区分主轴和辅轴(侧轴): 在项目中我们使用 display:flex; 创建 Flexbox 元素,那么该元素就成为了一个 flex container( 弹性的容器)。 其在文档流中的直接子元素将成为 flex item。 flex item 子元素在容器内 排列的方向称为主轴,跟主轴垂直的方向称为 辅轴。 方向相关属性 flex-direction 设置子元素排列方向 (其实也就是主轴的排列方向) 取值 row | row-reverse | column | column-reverse 默认 row: 其中不同的设置,效果大致如下 : flex-wrap 元素在主轴方向排放时,能否换行 取值:nowrap | wrap | wrap-reverse 默认 nowrap,不换行 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /*base css*/ .container{ width: 400px; margin: 20px; line-height: 40px; font-size: 20px; color: #fff; display:flex; } .item{ margin: 10px; width: 100px; line-height: 40px; text-align: center; } 合并属性: flex-flow , 上面两个属性的缩写 <'flex-direction'> || <'flex-wrap'> 默认: flex-flow: row nowrap; 这里直接结合两个属性看就好。 order 指定摆放时的顺序,从小到大 取值:默认 0 ,(支持负值和正值) 弹性相关属性,都是设置在子元素上的 flex-basis 设置 flex item 的初始宽/高 取值: main-size | <width> 默认: main-size: 主轴方向的宽度 (根据 flex-direction设置,水平排列时,设置的是宽度;垂直排列时,设置的高度) flex-grow 定义每一个子元素在盒子内的弹性 拓展盒子剩余空间的能力(空间富余时) 取值: <number> 取值:默认 0 ,整数小数都可 剩余空间的分配规则 : flex-basis + flow-grow/sum(flow-grow)*remain remain 表示多余的空间 这里可以看到 只设置 flex-basis 相当与设置元素的 width flex-shrink 定义元素收缩的能力(空间不足时) 取值: <number> 取值 : 默认 1 ,平方(值为 0 时,不收缩) 不足空间收缩的规则 : flex-basis + flow-grow/sum(flow-grow)*remain remain 表示不足的空间 (负值) 合并属性: flex <'flex-grow'> || <'flex-shrink'> || <'flex-basis'> 默认: flex: 0 1 main-size; 看上面 对齐 相关的属性 justify-content 设置子元素在主轴方向上的对其方式 取值: flex-start | flex-end | center | space-between | space-around 默认 flex-start 例子:切换主轴方向时 align-items 设置在辅轴上的对齐方式。 取值: flex-start | flex-end | center | baseline | stretch 默认 stretch align-self 设置在子元素上 单独设置子元素在辅轴方向的对齐方式 取值: flex-start | flex-end | center | baseline | stretch 默认 stretch align-content 多行内容 设置在辅轴方向上,行的对齐方式 取值: flex-start | flex-end | center | space-between | space-around |stretch 默认 stretch 拉伸

2017/5/31
articleCard.readMore

HTML5常用标签分类

文章是很早之前的笔记,做了一些属性上的补充,现发布到博客中来 十年踪迹的博客:HTML5 元素选择流程图 HTML5基本介绍 HTML5 设计思想 兼容已有内容 避免不必要的复杂性 解决现实的问题 优雅降级 尊重事实标准 用户->开发者->浏览器厂商->标准制定者->理论完美 语法 标签不区分大小写,推荐小写 空标签可以不闭合,比如 input / meta 属性不必引号,推荐双引号 某些属性值可以省略,比如 required,readonly HTML5常用标签分类 一. HTML文档标签 <!DOCTYPE>: 定义文档类型. <html>: 定义HTML文档. <head>: 定义文档的头部.(头部内包含) <meta>: 定义元素可提供有关页面的元信息,比如针对搜索引擎和更新频度的描述和关键词(由于规范没有规定关于 mete 中各种属性的强制定义,所以不同的浏览器都都可以通过 mete 来声明一些规则) 参考:移动端的头部标签 <meta charset ="UtF-8"> <meta name ="keywords" conten="关键词"> <meta name ="description" conten="页面介绍"> <meta name =" viewport" conten="initial-scale=1"> <base>:定义页面上的所有链接规定默认地址或默认目标. <title>: 定义文档的标题. <link>: 定义文档与外部资源的关系. <style>:定义 HTML 文档样式信息. <body>: 定义文档的主体.(脚本在非必须情况时在主体内容最后) <script>: 定义客户端脚本,比如 JavaScript. <noscript>:定义在脚本未被执行时的替代内容.(文本) 二. 布局标签&语义化 <div>:定义块级元素. <span>:定义行业元素. <header>5[^footnote]:定义区段或页面的页眉.(头部) <footer>:定义区段或页面的页脚.(足部) <section>:定义文档中的区段. <article>:定义文章.(在<article>中也可以进行内容划分) <aside>:定义页面内容之外的内容. <details>:定义元素的细节. <summary>:定义 <details> 元素可见的标题. <dialog>:定义对话框或窗口. <nav>:定义导航. <hgroup>:定义标题组 三. 表格标签 <table>:定义表格. border=1定义边框 <caption>:定义标题.(规范:必须是 table 的第一个元素) <thead>:定义页眉. <tbody>:定义主体. <tfoot>:定义页脚. <th>:定义表头. <tr>:定义一行. <td>:定义单元格. rowspan="2"跨行(竖直) colspan="2"跨列(水平) <colgroup><col class="" span="2"></colgroup>:列组,批量的给列做处理 四. 表单标签 <form>:定义表单.(表单包含在form标签中) novalidate:禁用原生的验证规则 表单提交最好是绑定 submit 事件 <input>:定义输入域. name="username":原生表单提交用于传输的 key 例:key1=value1&key2=value2 placeholder="2-10位":描述文字 minlength="2"最少(记录一下,一般这些还是走 js) maxlength="10":最多 required:是否必填 pattern="1\d{10}":正则表达式 type="text":input 类型如 search/number/email等都是输入 readeonly disabled <textarea>:定义文本域.(多行) <label>:定义一个控制的标签.(input 元素的标注) for="abc", abc 为一个 id=”abc”的标签 如果直接把 input 整个包在 label 中也可以有 for 的效果 <fieldset>:定义域. <legend>:定义域的标题. <select>:定义一个选择列表. name="aaaa":原生表单提交用于传输的 key size="3":只展示几个 multiple:是否开启多选 <optgroup>:定义选择组. <option>:定义下拉 列表的选项. <button>:定义按钮. type="submit":默认的是 submit type="button":大部分时间都会手动设置 type="reset":重置 <fieldset>:定义围绕表单中元素的边框. <legend>:定义 fieldset 元素的标题. <fieldset>:定义选项列表.与input 元素配合使用该元素,来定义 input 可能的值. <keygen>:定义表单的密钥对生成器字段. <output>:定义不同类型的输出,比如脚本的输出. 五. 列表标签 列表相关的标签,需要注意其嵌套规则 <ul>:定义无序列表. <ol>:定义有序列表. 属性 start="1" 表示开始位置 <li>:定义列表项. <dl>:定义自定义列表. <dt>:定义自定义列表项. <dd>:定义自定义的描述. 六. 图像&链接标签 <img>:定义图像. alt="替代文字":必须加! height="200" width="300",可用 css 指定 不指定宽高:原图大小显示 指定宽度:按比例缩放到指定宽度 指定高度:按比较缩放到指定高度 指定宽高:强制按指定宽高显示 <a>:定义超链接. href="url"在这里链接有多种形式 省略协议:href="//baidu.com",自动根据当前页面协议补充 省略协议和host(同时支持相对与绝对路径):href="/index.html" 页面内链接(锚点):href="#test"会找到页面中id 或者name 为test 的元素 链接目标:target="_self"(当前窗口)_blank(新窗口) abc(开一个自定义窗口名称) <map>:定义图像映射。 <area>:定义图像地图内部的区域. <figure>:定义媒介内容的分组.(图表,图片,一段代码等)描述<img>内容等。 <figcaption>:定义 <figure> 元素的标题. 七. 音频/视频 <audio>:定义声音内容. <source>:定义媒介源. <track>:定义用在媒体播放器中的文本轨道. <video>:定义视频. 八. 框架标签 <iframe>:内联框架. 九.格式标签 1. 文章标签 <h1>-<h6>:定义 HTML 标题. <p>:定义段落. <br>:定义换行. <hr>:定义水平线. <bdo>:定义文字方向. <pre>:定义预格式文本.保留换行等格式 <abbr>:定义缩写. <address>:定义文档作者或拥有者的联系信息. <ins>:定义被插入文本.(比如博客中时效性的语句) <del>:定义被删除文本. <time>:定义日期/时间. <wbr>:定义虚拟的空格换行(例如长段的url) 2. 短语元素标签 <dfn>:定义定义项目. <code>:定义代码(长短都可) <samp>:定义计算机代码样本. <kbd>:定义键盘文本. <var>:定义文本的变量部分. <sup>:定义上标文本. <sub>:定义下标文本. <cite>:定义引用.(标题/章节/书名)等 <blockguote>:定义长的引用. 属性:cite="url" 表示引用来源 <q>:定义短的引用.(一句话等) 3. 字体样式标签 <em>:定义强调文本.(从一句话中突出某个词语) <strong>:定义语气更为强烈的强调文本.(重要性,严重性和紧急性) <i>:显示斜体文本效果.(换一种语调去说已句话时,比如其他语言翻译,对话中的旁白) <b>:呈现粗体文本效果.(将词语从视觉上和其他部分区分,比如一篇论文摘要中的关键词) <big>:呈现大号字体效果. <small>:呈现小号字体效果. <mark>:定义有记号的文本.(和用户当前行为相关的突出,比如在搜索中匹配到的次,或者一部分内容需要在后面引用时) 十. 其它 <canvas>:定义图形容器,必须使用脚本来绘制图形。 <meter>:定义预定义范围内的度量. <progress>:定义任何类型的任务的进度. 十一. 一些 HTML全局属性 accesskey:键盘快捷键 id class style title hidden:标签隐藏 lang:语言类型:’en’,’zh-CN’ dir:文本排列方向 tabindex contenteditable:内容编辑 spellcheck:拼写检查

2017/4/21
articleCard.readMore

ES6 简单特性学习记录

变量定义的新方式:let/ const let 特性: 不允许重复声明 没有变量提升(预解析) 块级作用域(一对 {} 包括的区域称为一个代码块,let 声明的变量只在该代码块起作用) 例子1 :简单的打印数据 使用 var: 1 2 3 for(var i = 0; i<10 ; i++ ){ setTimeout(()=>console.log(i)) // 执行10次,全都打印 10 } 使用 let: 1 2 3 for(let i = 0; i<10 ; i++ ){ setTimeout(()=>console.log(i)) // 执行10次,打印 0 - 9 } 之前我们要实现这样的打印,必须使用闭包: 1 2 3 4 5 for(var i = 0; i<10;i++){ (function(j){ setTimeout(()=>console.log(j)) // 执行10次,打印 0 - 9 })(i) } 例子二:在网页中常常会有切换 tab ,展示对应的信息的需求,我们使用 var 来处理时,常常使用的自定义属性,来保存点击的索引。btns[i].index=i。用于找到对应的元素。: html模板: 1 2 3 4 5 6 7 8 9 10 11 <style type="text/css"> div{display:none} .show{display:block;} .active{background:red;} </style> <button class="active">1</button> <button>2</button> <button>3</button> <div class="show">11111</div> <div >22223</div> <div >33333</div> js: 1 2 3 4 5 6 7 8 9 10 11 12 13 var btns = document.querySelectorAll('button') var divs = document.querySelectorAll('div') for (var i=0 ;i<btns.length;i++){ btns[i].index=i btns[i].onclick=function(){ for(var j=0 ;j<btns.length;j++){ btns[j].className='' divs[j].className='' } this.className='active' divs[this.index].className='show' } } 使用 let: 1 2 3 4 5 6 7 8 9 10 11 12 13 var btns = document.querySelectorAll('button') var divs = document.querySelectorAll('div') for (let i=0 ;i<btns.length;i++){ /*可以看到这里少了保存的索引的操作*/ btns[i].onclick=function(){ for(let j=0 ;j<btns.length;j++){ btns[j].className='' divs[j].className='' } this.className='active' divs[i].className='show' } } const 除了具备上述 let 的特性外,还有自己的一个特性:定义之后的值,是固定不变不能被修改的。 值得注意的是下面这两种情况是不会报错的: 1 2 3 4 5 6 7 8 9 { const a = {value:1} a.value = 2 console.log(a) // {value:2} const b = [1,2,3] b.push(4) console.log(b) // [1,2,3,4] } 解构赋值 ES6 允许按照一定的模式,从数组和对象中提取值,这样就称为解构 数组:按照对应的顺序解构 1 2 3 4 5 6 7 8 9 10 11 12 { var arr = [[1,2,3],[4,5,6],[7,8,9]] var [a,b,c] = arr // a : [1,2,3] // b : [4,5,6] // c : [7,8,9] // 用法1 var x = 1; var y = 2; [y,x] = [x,y] console.log(x,y) // 2 1 } 对象按照对应的名称一一对应进行解析: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 { var obj={ get:function(){ return 'get' }, value:1, data:[1,2,3], str:'string' } var {str,get,data} = obj console.log(str) // string console.log(get()) //get console.log(data) // [1,2,3] } 模板字符串 模板字符串 是增强版的字符串,使用反引号(```)作为标识 。可以当做普通字符串使用,也可以用来定义多行字符串(会保留换行)。或者在字符串中嵌入变量。 在模板字符串,需要引用变量使用 ${变量名} 的形式。在 {}可以进行运算,也可以引用对象属性。 1 2 3 4 5 6 { var name = 'xiaoming' var age = 19 var str = `my name is ${name} ,my age is ${age}` console.log(str) //"my name is xiaoming ,my age is 19" } 扩展 Array.from(arrayLike[, mapFn[, thisArg]]) arrayLike : 想要转换成真实数组的类数组对象或可遍历对象。 mapFn : 可选参数,如果指定了该参数,则最后生成的数组会经过该函数的加工处理后再返回。 thisArg : 可选参数,执行 mapFn 函数时 this 的值。方法用于将两类对象转为真正的数组:类似数组的对象和可遍历的对象(包括 ES6 新增的数据结构 Set 和 Map ) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 { // NodeList对象 let ps = document.querySelectorAll('p'); Array.from(ps); // 将可迭代对象(Set 对象)转换成数组 Array.from(new Set(["foo", window])); // ["foo", window] // 使用 map 函数转换数组元素 Array.from([1, 2, 3], x => x + x); // [2, 4, 6] // 将类数组对象(arguments)转换成数组 (function () { var args = Array.from(arguments); return args; })(1, 2, 3); // [1, 2, 3] } 而在这之前,我们要转类数组对象,只能用这样的形式: [].slice.call(ps) 当然或许你根本不需要转,因为我们有 for of 了,只要有遍历接口的类型,它就可以进行遍历 (Set,String,Array,NodeList等等) 1 2 3 4 5 6 7 8 9 10 11 { // NodeList对象 let ps = document.querySelectorAll('p'); for (let v of ps){ console.log(v) } //当然你可能同样需要下标: `arr.keys()`,`arr.values()`,`arr.entries()` for (let [i,item] of ps.entries()){ console.log(i,item) } } Object.assign():拷贝源对象自身的可枚举的属性到目标对象身上 1 2 3 4 5 { var obj = { a: 1 }; var copy = Object.assign({}, obj); console.log(copy); // { a: 1 } } 值得注意的是, Object.assign()执行的是浅拷贝。假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值。 1 2 3 4 5 6 7 { let a = { b: {c:4} , d: { e: {f:1}} } let g = Object.assign({},a) g.d.e = 32 // 设置 g.d.e 为 32 console.log(g) // {"b":{"c":4},"d":{"e":32}} console.log(a) // {"b":{"c":4},"d":{"e":32}} } 如果你需要的不是合并,而只是普通json对象的复制,建议使用 JSON.parse(JSON.stringify(a)),这样不会有上面的副作用产生。 函数参数默认值。定义默认值得参数必须是尾参数,因为函数形参定义默认值后该参数可以被忽略 1 2 3 4 5 6 { function fn(a,b=2){ return {a,b} } console.info(fn(1)) //{a: 1, b: 2} } rest参数:用于获取获取函数的多余参数。与参数默认值一样,必须为尾参数 1 2 3 4 5 6 { function foo(a,b,...args){ console.info(args) } foo(1,2,3,4,5,6) // [3, 4, 5, 6] } 扩展运算符...:它好比 rest 参数的逆运算。可以将一个数组转为用逗号分隔的参数序列。 1 2 3 4 5 6 7 8 9 10 11 12 { // 更好的 apply 方法,例如我们在算最大值的时候: var arr = [1,2,3,4,5] console.info(Math.max.apply(null,arr)) console.info(Math.max(...arr)) // 使用扩展运算符 console.info(Math.max(1,2,3,4,5)) // 最终都会被解析成这样 // 当然还能这样用 var str = 'string' var arr = [...str,4,5] // ["s", "t", "r", "i", "n", "g", 4, 5] } 箭头函数 Arrow Functions:箭头函数并不是用来替代现有函数而出现的,并且也无法替代。它是用来作为回调函数使用的,主要是为了简化回调函数的写法。 主要有三个特性: 箭头函数自身没有 this 。函数内的 this 指向箭头函数 定义时所在的对象 ,而不是使用时所在的对象。 箭头函数内部,不存在 arguments 对象 不可以当作构造函数,不可以使用 new 指令。 简单用法,简化回调: 1 2 3 4 5 6 7 { // 我们都知道数组的 sort 并不是根据数值大小来排序的,需要排序时,要通过回调函数的形式来确定排序方式 var arr = [7,8,9,10] arr.sort() // [10, 7, 8, 9] arr.sort(function(a,b){return a-b}) // [7, 8, 9, 10] arr.sort((a,b)=> a - b ) // 箭头函数简化。当仅有一条语句时,有一个隐式的 return } 没有 arguments 1 2 3 4 5 6 7 8 9 { var foo = (a,b,c)=>{ console.log(a,b,c) console.log(arguments) }; foo(1,2,3) // 1 2 3 // Uncaught ReferenceError: arguments is not defined } 不要在对象的方法中使用箭头函数: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { window.name='window'; var obj = { name:'obj', getName: function(){ console.log(this.name) } } obj.getName() // obj var getName = obj.getName getName() // window, this 总是指向调用者 //----------------- var obj = { name:'obj', getName: () =>{ console.log(this.name) } } obj.getName() // window /** 这里由于对象 a,并不能构成一个作用域。所以会再往上达到全局作用域,所以 this 指向 window.. */ }

2017/3/1
articleCard.readMore

一张图学习 ES6 中的 React 生命周期与流程

如题。 一张图学习 ES6 中的 React 生命周期与流程。

2017/2/5
articleCard.readMore

2017,一切才刚刚开始。

又是一年结束,明天就是2017了,这一年做了很多事:实习、毕业、工作。广州、丽江、重庆、北京、长沙、杭州、走了5个城市。开心、懊恼、苦涩各种心境。从一个编程的门外汉真正成为了一个初级前端开发工程师。也算是人生又一个新的起点吧。 博客 先说说博客,从第一篇博客开始到现在一年零一个月了。一共 50 篇,博客总字数为:338202,产量不算高。不过值得高兴的是一年下来总流量量有 15W,应该还算不错吧。不仅是对自己学习的一个记录,应该也帮助到了不少人。终于把域名买了 blog.guowenfh.com 下面自我分析一下: 内容:大多数是一些前端技术学习的笔记,也有寥寥几篇的摘抄和自我思考。在实际项目和代表编写过程中的积累根本没有,得好好加强。 时间:博客基本上有很明确的时间区分,偶尔一个月发个四五篇,然后就沉寂几个月,工作的区间内也很少有博客,没有稳定的输出时间。 需要改进的点:在完成项目编写代码的过程中多积累,多总结。把一些别人的优点或者不够优雅的地方都做一个记录,想想为什么要这样做而不是那样做。然后不能一阵一阵的热情,需要多投入! Github 今年在 GitHub 上并没有一个特别拿得出手的开源项目,不过之前写的 vue-webapck 基础介绍教程居然能拿到160星,让我很惊讶,也让我更加想去为社区贡献一点自己的东西。正好公司要跨入 react 技术栈了,第一步先用它写个小项目吧。之后尝试去给出一些 Pull requests 关于计划 在今年年初,自己给自己定了几个小目标,内容之前在 思想汇报中也有提到,除了书只读了10多本之外,其他都算是实现了吧,至少来说不后悔。 2017 年的具体规划没想好,但也就那些吧,多沟通,多看书,去年一年30本的量没达到,今年需要努力把阅读习惯培养出来! 保持这种饥饿感,保持一种初学者的心态,坚持去坚持。 毕业出来工作五个多月了,在工作之后感觉还是成长了很多,也越来越相信编程更多的是考验人的解决问题的能力,而不是局限于编程语言,所以新的一年需要多锻炼一下自己解决工程的能力,开始于前端,但不止前端。 这篇没有什么章法的博客到这就结束了,没有很多的个人思考,更多的是在写的过程中回忆了一下一年间的发生种种,这样的回顾也是一种收获吧。 凭什么要奋发图强? “凭自己。”

2016/12/31
articleCard.readMore