胖蔡叨叨叨
你听我说

使用Browserify 实现JS的模块化加载

胖蔡阅读(15)

Broswerify 是一个前端管理依赖的工具,通过它可以在浏览器环境下像nodejs一样遵循commonjs规范的模块化编程。

为什么要使用Browserify

浏览器没有定义require方法,但是Node.js有。使用Browserify可以编写使用require的代码,就像在Node中使用它一样。

原理

Browserify从entry着手,对源代码进行抽象语法树分析,从而获得整个项目的依赖关系图,最后将整个项目打包成一个JavaScript文件。特点如下:

  • 浏览器端的前端打包工具
  • 主要用于在浏览器中使用 npm 包,最终会转换为 commonJS (require) 类似方式,在浏览器使用
  • 方便模块细分,每个模块自成,通过 require 引用其他模块
  • 基于流 Stream

实现

  • 创建add.js文件
module.exports.add = function(a,b) {
    return a + b;
}

  • 创建reduce.js文件
module.exports.reduce = function(a,b){
    return a-b;
}
  • 创建 index.html
<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Browserify 测试</title>
</head>

<body align="center">
    <h3>Browserify 测试</h3>
    <form style="text-align: left;margin-left: 40%;">
        <input name="a"  type="input" placeholder="请输入第一个操作数"/>
        <input name="b"  type="input" placeholder="请输入第二个操作数"/>
        <div style="margin-top: 20px;">合计:<span id="result"></span></div>
        <div style="margin-top: 20px;">
            <button id="add" type="button">加法</button>
            <button id="reduce" type="button" style="margin-left: 30px;">减法</button>
        </div>
    </form>
    <script src="./bundle.js" ></script> <!--打包生成文件 -->
</body>
</html>
  • 创建main.js文件
var { add }  = require('./add');
var { reduce } = require('./reduce');

// 加法
document.getElementById('add').onclick =  function () {
    const a = document.getElementsByName('a')[0].value;
    const b = document.getElementsByName('b')[0].value;
    document.getElementById('result').innerHTML = add(a,b);
}

// 减法
document.getElementById('reduce').onclick =  function () {
    const a = document.getElementsByName('a')[0].value;
    const b = document.getElementsByName('b')[0].value;
    document.getElementById('result').innerHTML = reduce(a,b);
}

  • 生成 bundle.js文件
$ npm install -g browserify
$ browserify  main.js > bundle.js

效果

前端的构建工具

胖蔡阅读(22)

前端技术随着日益成熟的开发框架(React、Vue、Angular)的流行,如何去管理、构建前端项目也就显得尤其重要。目前市面上较为常用的构建工具就是Webpack。本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph),此依赖图会映射项目所需的每个模块,并生成一个或多个 bundle。除了webpack以外,前端其实还有很多比较优秀的构建工具,适用于不同的开发场景。如:Gulp、Grunt、FIS、Rollup、Parcel、Yeoman6、Athena、WeFlow、Cooking等。

什么是构建工具?

构建工具在开发中是一个很特殊的存在,它不影响代码逻辑、业务,当大多情况下是不可或缺的角色。从定义上看:构建工具是一个把源代码生成可执行应用程序的过程自动化的程序,如gcc将.c生成为.a、.o文件、javac将.java文件生成为.class文件、gradle将项目工程生成为.apk文件等等诸如此类。如上所说的gcc、javac、gradle都是相对流程的一个构建工具。

 为什么要用构建工具?

了解了什么是构建工具后,接下来需要了解的是既然构架工具并不对结果负责,那么我们为什么要使用构建工具呢?其实理由很现实,就是减负,减轻开发者负担,让开发这不需要将过多的精力用于管理包、包版本、压缩等与业务不相干的工作,而且当项目越来越大,人力也无法负担如此巨大的工作。

构建工具能做哪些事?


说了构建工具的重要性,接下来需要说的是构建工具具体能帮我们做哪些工作:

  • 依赖管理(下载和版本维护)
  • 生成目标运行文件并不影响源代码
  • 版本发布
  • 单元测试

在前端中构建工具对应的具体需要帮我们实现的如下:

  • 转换ES6语法
  • 转换框架的语法糖 如 JSX, VUE文件
  • CSS 前缀补全/预处理器
  • 压缩混淆
  • 图片压缩
  • 代码分割
  • 模块合并
  • 代码校验
  • 自动发布

 前端构建工具


说完了构建工具的功能,接下来需要了解在前端开发中具体有哪些构建工具,我们可以通过工具的类型,将构建工具分为:

  •  模块化打包类型
  • 任务流构建类型
  • 集合型工具类

常见的而模块化打包类型构建工具如:webpack、Rollup、Browserify。任务流类型的有:Gulp、Grunt。集合型的主要包括:Yeoman、FIS、jdf、Athena、cooking、weflow。就目前来看用的较多的是属于第一、第二类型的构建工具。

 前端常用构建工具

webpack


 Webpack 是一个打包模块化 JavaScript 的工具,在 Webpack 里一切文件皆模块,通过 Loader 转换文件,通过 Plugin 注入钩子,最后输出由多个模块组合成的文件。Webpack 专注于构建模块化项目。

webpack

一切文件:JavaScript、CSS、SCSS、图片、模板,在 Webpack 眼中都是一个个模块,这样的好处是能清晰的描述出各个模块之间的依赖关系,以方便 Webpack 对模块进行组合和打包。 经过 Webpack 的处理,最终会输出浏览器能使用的静态资源。Webpack的优点是:
– 模块化处理项目- Plugin 扩展- 使用场景不仅限于 Web 开发- 社区活跃,能为大多数场景找到已有的开源扩展

 Grunt  


Grunt的出现早于Gulp,Gulp是后起之秀。他们的本质都是通过 JavaScript 语法实现了shell script 命令的一些功能。 Grunt是一个任务执行者,有大量现成的插件封装了常见的任务,也能管理任务之间的依赖关系,自动化执行依赖的任务,每个任务的具体执行代码和依赖关系写在配置文件 Gruntfile.js 里,如下配置文件:

// Gruntfile.js
module.exports = function(grunt) {
  grunt.initConfig({
    // js格式检查任务
    jshint: {
      src: 'src/test.js'
    }
    //  代码压缩打包任务
    uglify: {}
  });
  // 导入任务插件
  grunt.loadnpmTasks('grunt-contrib-uglify');
  // 注册自定义任务, 如果有多个任务可以添加到数组中
  grunt.regusterTask('default', ['jshint'])

 Gulp
 Gulp吸取了Grunt的优点,拥有更简便的写法,通过流(Stream)的概念来简化多任务之间的配置和输出,让任务更加简洁和容易上手。通过配置gulpfile.js文件来实现,一个简单的gulpfile.js配置如下:

// gulpfile.js
var gulp = require('gulp');
var jshint = require('gulp-jshint');
var uglify = require('gulp-uglify');

// 代码检查任务 gulp 采取了pipe 方法,用流的方法直接往下传递
gulp.task('lint', function() {
  return gulp.src('src/test.js')
    .pipe(jshint())
    .pipe(jshint.reporter('default'));
});

// 压缩代码任务
gulp.task('compress', function() {
  return gulp.src('src/test.js')
    .pipe(uglify())
    .pipe(gulp.dest('build'));
});

// 将代码检查和压缩组合,新建一个任务
gulp.task('default', ['lint', 'compress']);

useState原理解析

佚名阅读(51)

本文转载自https://www.jianshu.com/p/a36fb3d3885f,如有侵权,请联系删除

最简单的useState实现

看下面的代码:

    const Demo = ()=> {
      const [n,setN] = React.useState(0)
      return <div>
        {n}
        <button onClick={()=> {setN(n+1)}}>点我+1</button>
      </div>
    }
    const rootElement = document.getElementById("app")
    ReactDOM.render(<Demo />, rootElement);
做了哪些事:

首次渲染:render()——得到Demo组件——调用Demo组件——得到虚拟div——创建真实的div

点击button后:调用setN(n+1)——再次render()——得到Demo组件——调用Demo组件——得到虚拟div——使用DOM Diff对比——更新真实的div

往后一次重复第二次的操作

分析:

state:每个组件都应该有自己的数据state,
setN:setN一定会修改数据state,将n+1存入state,然后重新render(),再次渲染
useState:useState肯定会从state读取n的最新值

尝试实现React.useState

    let _state 
    const myUseState = (initState) => {
      _state = _state ===undefined ? initState : _state 
      const setState = (newValue =>{
        _state = newValue
        render()
      })
      return [_state,setState]
    }
    const render =()=>{
      ReactDOM.render(<Demo />, rootElement);
    } 
    const Demo = ()=> {
      const [n,setN] = myUseState(0)
      console.log('n变了');
      console.log(`n的值是: ${n}`);
      return <div>{n}
          <button onClick={()=> {setN(n+1)}}>点我+1</button>
        </div>
    }
    const rootElement = document.getElementById("app")
    ReactDOM.render(<Demo />, rootElement);

多点几次,打印的结果看下图

image.png

打印的结果也确实对,却又一个很大的问题:如果一个组件用了两个useState怎么办?

    let _state 
    const myUseState = (initState) => {
      _state = _state ===undefined ? initState : _state 
      const setState = (newValue =>{
        _state = newValue
        render()
      })
      return [_state,setState]
    }
    const render =()=>{
      ReactDOM.render(<Demo />, rootElement);
    } 
    const Demo = ()=> {
      const [n,setN] = myUseState(0)
      const [m,setM] = myUseState(0)
      return <div>{n}
          <button onClick={()=> {setN(n+1)}}>点我+1</button>
          <br/>
          {m}
          <button onClick={()=> {setM(m+1)}}>点我+1</button>
        </div>
    }
    const rootElement = document.getElementById("app")
    ReactDOM.render(<Demo />, rootElement);

把上面的代码运行,你会发现,无论你点击哪个button,n和m都会+1,

现在我们来对它改进,_state不能直接让它等于一个值,而是让它等于很多值,只有对象和数组能做到,而对象不太合适,因为_state = {n: 0,m: 0}useState()并不知道变量叫n还是m,只能把它做成数组,比如:_state = [0,0](前面0是n,后面0是m)

    let _state = []
    let index = 0
    const myUseState = (initState) => {
      const currentIndex = index
      _state[currentIndex] = _state[currentIndex] ===undefined ? initState : _state[currentIndex] 
      const setState = (newValue =>{
        _state[currentIndex] = newValue
        console.log(_state);
        render()
      })
      index += 1
      return [_state[currentIndex],setState]
    }
    const render =()=>{
      index = 0
      ReactDOM.render(<Demo />, rootElement);
    } 
    const Demo = ()=> {
      const [n,setN] = myUseState(0)
      const [m,setM] = myUseState(0)
      return <div>{n}
          <button onClick={()=> {setN(n+1)}}>点我+1</button>
          <br/>
          {m}
          <button onClick={()=> {setM(m+1)}}>点我+1</button>
        </div>
    }
    const rootElement = document.getElementById("app")
    ReactDOM.render(<Demo />, rootElement);

最终代码就是上面的代码,用log可以打出_state的值,我们分别点击button让它们+1,看log出的值

image.png

这样,我们自己写的_state是不是就和React的一模一样?

通过实现简单的useState,能让我们更好地理解useState

JS中的延时操作setTimeout()和setInterval()

胖蔡阅读(11036)

JS中,给我们提供两种延时操作的内置方法setTimeout()和setInterval()。setTimeout和setInterval方法都是挂载在javascript的window对象下,通过两个参数控制,第一个参数控制运行的表达式或方法,第二个参数表示延时的时间,时间单位为毫秒级。

  • setTimeout(action,delay)
const  id = setTimeout(()=>{
// 延时200毫秒后执行的代码段
}, 200);

clearTimeout(id);  // 取消setTimeOut
  • setInterval(action,delay)
const intervalId = setInterval(()=>{
// 每隔200毫秒后执行的代码段
}, 200);

clearInterval(intervalId); //取消指定的循环

setInterval与seTimeout不同的是,setTimeou定时执行一次结束,setInterval是循环间隔第二个参数时长执行参数一的表达式或方法。

前端单元测试

xuao阅读(66)

认识单元测试

在 web 前端领域,单元测试通常包括:对某个 JS 的方法进行测试,对某个组件进行测试。除了单元测试,前端经常会有端到端测试。相对于端到端测试来说,单元测试编写更复杂。但是完整的单元测试的样例能够覆盖更多端到端测试覆盖不到的点,对于前端代码通常比较关键的模块可以通过添加单元测试来规避后续修改或者重构带来的风险。单元测试样例的编写过程也有助于进一步审视模块的功能。

单元测试适用于功能不会经常改动的工具方法模块和一些基础的公共组件,对于会经常在快速迭代中更新的业务组件和功能模块端到端的测试会更适合,但这并不是说不需要写单元测试。这其实是一个投入和产出比的一个权衡,编写单元测试可能会需要频繁的更新测试样例,对于部分业务尤其是中后台的应用来说成本是偏高的。

前端怎么做测试

在 React 诞生之前,前端的单元测试往往只能针对于一些纯粹的 JS 模块。由于对浏览器环境的依赖,很难去做涉及到 dom 操作的模块的单元测试。但是对于前端来说,大部分代码其实都是 UI 组件,这就导致长期以来前端的代码甚至一些开源的被应用得很广泛的 UI 组件库都缺乏完整的单元测试。

但是 React 的诞生伴随着虚拟 dom 被发明,这使得前端组件的测试变得更方便了。虚拟 dom 使得一个组件可以脱离真实的浏览器环境模拟 dom 的相关操作。我们可以通过测试虚拟 dom 的表现是否正常来测试组件的逻辑,让编写组件的测试能够脱离对浏览器 dom 环境的依赖。

在 umi 中,内置了 jest 作为单元测试的库,接下来我们会介绍如何使用 jest 对 JS 方法或者组件进行测试。

使用Jest

umi内置了jest测试。执行umi test 会匹配所有 .test.js 结尾的文件运行。通常我们约定把测试的代码统一放到test文件夹下,当然你也可以按照你的习惯组织,比如可以和测试对应的模块放到一起。

非umi环境下安装参考上次分享的内容,内容在我的博客中:
http://xuxiao.wang/detail/单元测试-Jest入门1

匹配器-普通匹配

  • 最简单的测试值的方法是看是否精确匹配。
test('测试值', () => {
expect(2 + 2).toBe(4);
});

测试一个对象的值是否相等

test('测试对象', () => { 
	const data = {one: 1};
	data['two'] = 2; 
	expect(data).toEqual({one: 1, two: 2});
 });

匹配器-真值匹配

  • toBeNull 只匹配 null
  • toBeUndefined 只匹配 undefined
  • toBeDefined 与 toBeUndefined 相反
  • toBeTruthy 匹配任何 if 语句为真
  • toBeFalsy 匹配任何 if 语句为假

匹配器-数字匹配

  • toBeGreaterThan 匹配大于
  • toBeGreaterThanOrEqual 匹配大于等于
  • toBeLessThan 匹配小于
  • toBeLessThanOrEqual 匹配小于等于
  • toBe 匹配等于
  • toEqual 匹配等于,等价于 toBe
  • toBeCloseTo 匹配浮点数相等,不会因为误差导致不匹配

匹配器-字符串匹配

  • toMatch 匹配字符串
  • 使用正则匹配一个字符串。测试用例匹配不包含字母I
test(‘匹配字符串', () => {
expect('team').not.toMatch(/I/);
});

匹配器-数组或其他可迭代对象

  • toContain检查一个数组或可迭代对象是否包含某个特定项

使用正则匹配一个字符串。测试用例匹配不包含字母I

const shoppingList = [ 'diapers', 'kleenex', 'trash bags', 'paper towels', 'beer', ]; 
test('匹配数组中是否包含某个元素', () => {
	expect(shoppingList).toContain('beer’); 
	expect(new Set(shoppingList)).toContain('beer’);
 });

匹配器-异常

  • toThrow 匹配异常

使用正则匹配一个字符串。测试用例匹配不包含字母I

expect(compileAndroidCode).toThrow();
compileAndroidCode 是一段可能会抛出异常的方法,如果抛出异常则匹配

JS 中的DOM与BOM

胖蔡阅读(70)

JS的组成

  • ECMAScript
    描述JS的语法与基本对象,ECMAScript是一个标准,JS只是它的一个实现
  • DOM(文档对象模型)
    处理网页内容的方法和接口,是 W3C 的标准,所有浏览器公共遵守的标准
  • BOM(浏览器对象模型)
    与浏览器交互的方法和接口,各个浏览器厂商根据 DOM在各自浏览器上的实现,表现为不同浏览器定义有差别,实现方式不同.window 是 BOM 对象,而非 js 对象;javacsript是通过访问BOM(Browser Object Model)对象来访问、控制、修改客户端(浏览器)

A区:(浏览器的标签页,地址栏,搜索栏,书签栏,窗口放大还原关闭按钮,菜单栏等等)
B区:(document加载时的状态栏,显示http状态码等)
C区: (滚动条scroll bar)
E区: (浏览器的右键菜单)
F区: html显示区域

DOM与DOM结构

DOM

Document Object Model(文档对象模型),就是把「文档」当做一个「对象」来看待,相当于把html页面结构解析成一个对象,提供一个接口API,让你去操作所有的节点。在 DOM 中,文档中的各个组件(component),可以通过 object.attribute 这种形式来访问。一个 DOM 会有一个根对象,这个对象通常就是 document。DOM通常包括HTML DOM 和 XML DOM。


根据 W3C 的 HTML DOM 标准,HTML 文档中的所有内容都是节点:整个文档是一个文档节点:

  • 每个 HTML 元素是元素节点
  • HTML 元素内的文本是文本节点
  • 每个 HTML 属性是属性节点
  • 注释是注释节点
<html>
<head>
<title>文档标题</title>
</head>
<body>
<a href=“”>我的链接</a>
<!--这是一段注释-->
<h1>我的标题</h1>
</body>
</html>

<!--xml-->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:app="http://schemas.android.com/apk/res-auto"
              android:orientation="vertical"
              android:background="@color/white"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
    <com.worfu.base.widget.HeadBarView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:title="现金明细"/>
    <com.google.android.material.tabs.TabLayout
            android:id="@+id/mTabLay"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            app:tabSelectedTextColor="@color/colorTabSelect"
            app:tabIndicatorFullWidth="false"
            app:tabTextAppearance="@style/TabLayoutTextStyle"
            app:tabBackground="@android:color/transparent"
            app:tabRippleColor="@android:color/transparent" >
    </com.google.android.material.tabs.TabLayout>
    <androidx.viewpager.widget.ViewPager
            android:id="@+id/mViewPager"
            style="@style/NoScrollBar"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1">
    </androidx.viewpager.widget.ViewPager>
</LinearLayout>

DOM节点树

DOM 方法是我们可以在节点(HTML 元素)上执行的动作,可通过 JavaScript (以及其他编程语言)对 HTML DOM 进行访问。所有 HTML 元素被定义为对象,而编程接口则是对象方法和对象属性。方法是您能够执行的动作(比如添加或修改元素)。属性是您能够获取或设置的值(比如节点的名称或内容)
一些常用的 HTML DOM 方法:

  • getElementById(id) – 获取带有指定 id 的节点(元素)
  • appendChild(node) – 插入新的子节点(元素)
  • removeChild(node) – 删除子节点(元素)

一些常用的 HTML DOM 属性:

  • innerHTML – 节点(元素)的文本值
  • parentNode – 节点(元素)的父节点
  • childNodes – 节点(元素)的子节点
  • attributes – 节点(元素)的属性节点

BOM(浏览器对象模型)

BOM(Browser Object Model) 是指浏览器对象模型。由于最初JavaScript就是设计在浏览器中执行的脚本语言,所以BOM是浏览器提供给JavaScript操作自身的接口。BOM的核心对象是window对象,window对象表示一个浏览器的一个页面窗口对象。同时window对象也是ECMAScript定义的Global对象,即网页中定义的全局属性或者全局方法最终都是定义到该对象上,同时提供JS的Global的方法如parseInt方法。

BOM对象

  • Window 对象:表示浏览器打开的窗口,包括获取焦点、改变滚动条、设置定时器等等。
  • Navigator 对象:包含浏览器信息。如:获取浏览器名称、版本信息、操作系统平台信息等等。
  • Screen 对象:包含屏幕信息。如:获取屏幕高度、宽度等等。
  • History 对象:可对当前页的浏览历史进行操作,如:前进、后退等。
  • Location 对象:可对当前页面的URL进行操作,如:导航到新的页面、获取URL信息等。

window对象是BOM(浏览器对象模型)的核心。window对象既是通过JS访问浏览器窗口的接口,也是ECMAScript规定的global对象, 由于window对象是js中的顶级对象,所有定义在全局作用域中的变量、函数都会变成window对象的属性和方法,在调用的时候可以省略window。

Promise入门

方玉新阅读(75)

预备知识

函数对象与实例对象

  • 函数对象: 将函数作为对象使用时, 简称为函数对象
  • 实例对象: new 函数产生的对象, 简称为对象


回调函数的分类


1、同步回调
理解: 立即执行, 完全执行完了才结束, 不会放入回调队列中
例子: 数组遍历相关的回调函数 / Promise的excutor函数
2、异步回调
理解: 不会立即执行, 会放入回调队列中将来执行
例子: 定时器回调 / ajax回调 / Promise的成功|失败的回调

错误


1. 错误对象
        name属性: 错误类型
        message属性: 错误相关信息
2. 错误的类型
        EvalError: 已在 eval() 函数中发生的错误 (更新版本的 JavaScript 不会抛出任何 EvalError, SyntaxError)
        ReferenceError: 引用的变量不存在
        TypeError: 数据类型不正确的错误
        RangeError: 数据值不在其所允许的范围内
        SyntaxError: 语法错误
URIError:在 encodeURI() 中已发生的错误
3. 错误处理
        捕获错误: try … catch
        抛出错误: throw error

Promise 的理解和使用

  • Promise 是什么
  • Promise 的理解

抽象表达:Promise 是 JS 中进行异步编程的新的解决方案
具体表达:
语法上:Promise 是一个构造函数
功能上:Promise 对象用来封装一个异步操作并可以获取其结果

  • Promise 的状态改变

pending 变为 resolved
pending 变为 rejected
只有这两种,且一个 promise 对象只能改变一次。无论成功还是失败,都会有一个结果数据。成功的结果数据一般称为 value,而失败的一般称为 reason。

  • Promise 的基本流程
  • Promise 的基本使用
new Promise((resolve, reject) => {
  resolve(1)
  //  reject(2)
}).then(value => {
  console.log('value', value)
}, reason => {
  console.log('reason1', reason)
}).catch(reason => {
  console.log('reason2', reason)
})

为什么要用 Promise

  • 指定回调函数的方式更加灵活

旧的:必须在启动异步任务前指定
promise:启动异步任务 => 返回promise对象 => 给promise对象绑定回调函数(甚至可以在异步任务结束后指定)

  • 支持链式调用,可以解决回调地狱问题

什么是回调地狱?
回调函数嵌套调用,外部回调函数异步执行的结果是其内部嵌套的回调函数执行的条件
回调地狱的缺点?
不便于阅读 / 不便于异常处理
解决方案
promise 链式调用
终极解决方案
async/await

如何使用 Promise

Promise中的API

  • Promise 构造函数:Promise(excutor) {}

excutor 函数:同步执行 (resolve, reject) => {}
resolve 函数:内部定义成功时调用的函数 resove(value)
reject 函数:内部定义失败时调用的函数 reject(reason)
说明:excutor 是执行器,会在 Promise 内部立即同步回调,异步操作 resolve/reject 就在 excutor 中执行

  • Promise.prototype.then 方法:p.then(onResolved, onRejected)

1)onResolved 函数:成功的回调函数 (value) => {}
2)onRejected 函数:失败的回调函数 (reason) => {}
说明:指定用于得到成功 value 的成功回调和用于得到失败 reason 的失败回调,返回一个新的 promise 对象

  • Promise.prototype.catch 方法:p.catch(onRejected)

onRejected 函数:失败的回调函数 (reason) => {}
说明:then() 的语法糖,相当于 then(undefined, onRejected)

  • Promise.resolve 方法:Promise.resolve(value)

value:将被 Promise 对象解析的参数,也可以是一个成功或失败的 Promise 对象
返回:返回一个带着给定值解析过的 Promise 对象,如果参数本身就是一个 Promise 对象,则直接返回这个 Promise 对象。

  • Promise.reject 方法:Promise.resolve(reason)

reason:失败的原因
说明:返回一个失败的 promise 对象

  • Promise.all 方法:Promise.all(iterable)

iterable:包含 n 个 promise 的可迭代对象,如 Array 或 String
说明:返回一个新的 promise,只有所有的 promise 都成功才成功,只要有一个失败了就直接失败。

  • Promise.race方法:Promise.race(iterable)

iterable:包含 n 个 promise 的可迭代对象,如 Array 或 String
说明:返回一个新的 promise,第一个完成的 promise 的结果状态就是最终的结果状态。

Promise 的几个关键问题

如何改变 promise 的状态

  • resolve(value):如果当前是 pending 就会变为 resolved
  • reject(reason):如果当前是 pending 就会变为 rejected
  • 抛出异常:如果当前是 pending 就会变为 rejected

一个 promise 指定多个成功/失败回调函数,都会调用吗?

当 promise 改变为对应状态时都会调用。

改变 promise 状态和指定回调函数谁先谁后?

  • 都有可能,常规是先指定回调再改变状态,但也可以先改状态再指定回调
  • 如何先改状态再指定回调?
    1. 在执行器中直接调用 resolve()/reject()
    2. 延迟更长时间才调用 then()
  • 什么时候才能得到数据?
    1. 如果先指定的回调,那当状态发生改变时,回调函数就会调用得到数据
    2. 如果先改变的状态,那当指定回调时,回调函数就会调用得到数据

promise.then() 返回的新 promise 的结果状态由什么决定?

  • 简单表达:由 then() 指定的回调函数执行的结果决定
  • 详细表达:
    ​ ① 如果抛出异常,新 promise 变为 rejected,reason 为抛出的异常
    ​ ② 如果返回的是非 promise 的任意值,新 promise 变为 resolved,value 为返回的值。
    ​ ③ 如果返回的是另一个新 promise,此 promise 的结果就会成为新 promise 的结果。

promise 如何串联多个操作任务?

  • promise 的 then() 返回一个新的 promise,可以并成 then() 的链式调用。
  • 通过 then 的链式调用串联多个同步/异步任务。

Promise 异常穿透(传透)?

  • 当使用 promise 的 then 链式调用时,可以在最后指定失败的回调。
  • 前面任何操作出了异常,都会传到最后失败的回调中处理。

中断 promise 链?

当使用 promise 的 then 链式调用时,在中间中断,不再调用后面的回调函数
办法:在回调函数中返回一个 pending 状态的 promise 对象

nodejs基于express搭建接口服务

xuao阅读(185)

编写react时做了前后端分离的构建模式,那么就需要后端提供接口数据,决定使用nodejs编写简单的后端服务。本文主要是介绍了如何通过nodejs完成一个后端api的开发、调用、和线上部署的流程。希望能对大家有所作用。

本文转载自XUAO,版权归作者所有,本站仅做转载传播,点击可查看原文

nodejs server 编写

创建一个工程目录,我就取名叫 blog-server
cmd到这个工程目录下安装相关依赖:

npm install express
npm install hotnode
npm install mysql
npm install cors
npm install body-parser

新建一个index.js引入需要使用的组件包,这个index.js就作为即将可以使用的服务入口。

const cors = require('cors');
const bodyParser = require('body-parser');
const express = require('express');
const mysql = require('mysql');
const app = express();

app.use(cors()); // 解决跨域
app.use(bodyParser.json()); // 解析参数,json请求
app.use(bodyParser.urlencoded({ extended: true }));// 表单请求

开启端口监听,当浏览器或者其他工具访问http://localhost:8888/时,会被本服务监听到

app.listen(8888, () => console.log('server running at http://localhost:8888/'));

监听get请求,并返回一段html标签,监听地址为基地址

app.get('/', (req, res) => {
    res.send('<div style="color:red">error</div>')
})

get带参请求,并返回一段json数据,数据库查询的使用稍后再说

// 博客列表
app.get('/article-list', (req, res) => {

    // page=1&pageSize=10&classifyId=2
    var params = req.query

    var pageSize = 15;
    if (params.pageSize) { // 分页大小
        pageSize = params.pageSize;
    }

    var page = 1;
    if (params.page) { // 当前页数
        page = params.page;
    }

    var classifyId = '';
    if (params.classifyId) { // 分类id
        classifyId = '&& type=' + params.classifyId;
    }

    const data = {
        total: 0,
        blog: []
    }

    conn.query('select count(1) as total from ‘表名’ WHERE state=1 ' + classifyId + ';', (e1, r1) => {
        data.total = r1[0].total;

        if (e1) {
            // 此处返回一段json数据
            res.json(new Result({ code: 1, msg: "fail", data: e1 }))
            return
        }

        conn.query('SELECT * FROM ‘表名’ WHERE state=1 ' + classifyId + ' LIMIT ' + (page - 1) * pageSize + ',' + pageSize + ';', (e2, r) => {

            if (e2) {
                res.json(new Result({ code: 1, msg: "fail", data: e2 }))
                return
            }

            r.forEach(element => {
                element.content = element.content.substring(0, 100) + "..."
                data.blog.push(element)
            });

            res.json(new Result({ data: data }))
        })
    })
})

post带参请求,

// 发起留言
app.post('/leave-message', (req, res, next) => {

    console.log(req.body)

    var params = req.body;

    // 如果没有留言则不需要处理
    if (!params.message) {
        res.json(new Result({ code: 1, msg: "fail", data: params }))
        return
    }

    const post = {
        name: params.name,
        message: params.message,
        time: new Date().getTime()
    }

    conn.query('INSERT INTO ‘表名’ SET ?', post, (err, results) => {
        if (err) {
            console.log('err', err)
            res.json(new Result({ code: 1, msg: "fail", data: err }))
            return
        }
        res.json(new Result({ data: post }))

    })
})

封装的一个统一返回的最外层数据格式

function Result({ code = 0, msg = "success", data = null }) {
    this.code = code;
    this.msg = msg;
    this.data = data;
}

数据库的建立与连接

后端数据离不开数据库,nodejs连接远程数据库特别简单,先设置基础配置项,详情见注释

const option = {
    host: 'localhost', // 数据库所在地址,如果需要访问远程数据库,可以查询远程数据库配置,此处只要填写地址
    user: 'root', // 数据库用户名
    password: '123456', // 数据库访问密码,这里必填,如果数据库没有创建密码,到了这里就需要给root用户创建一个密码
    port: '3306', // 数据库开放端口
    database: 'blog', // 数据库名
    connectTimeout: 5000, // 连接超时时间
    multipleStatements: false // 不允许多sql语句执行,防止sql注入攻击,默认false
}

建立数据库连接,此时已经由第三方组件库帮我们把复杂的内容处理完了,用就可以了。

const conn = mysql.createConnection(option);

数据库查询语句

  conn.query('SELECT * FROM ‘表名’;', (e1, r) => {

        if (e1) {
            res.json(new Result({ code: 1, msg: "fail", data: e1 }))
            return
        }
        res.json(new Result({ data: r }))
    })

数据库插入,更多用法查阅 mysql 的具体使用文档

 const post = { // 需要插入数据的参数
        name: params.name,
        message: params.message,
        time: new Date().getTime()
    }

    conn.query('INSERT INTO ‘表名’ SET ?', post, (err, results) => {
        if (err) {
            console.log('err', err)
            res.json(new Result({ code: 1, msg: "fail", data: err }))
            return
        }
        res.json(new Result({ data: post }))

    })

重要配置

如果工程根目录下没有生成 package.json文件就新建一个并添加配置 dependencies 为导包版本所以此文件应该是自动生成的,如果生成此文件就修改配置,如下:

{
  "main":"index.js",
  "scripts": {
    "start": "hotnode index.js"
  },
  "dependencies": {
    "body-parser": "^1.19.0",
    "cookie-parser": "^1.4.5",
    "cors": "^2.8.5",
    "express": "^4.17.1",
    "hotnode": "^0.0.8",
    "multer": "^1.4.2",
    "mysql": "^2.18.1"
  }
}

运行

至此,一个简单的nodejs实现的后端服务基本完成,在控制台中输入启动命令:

npm start

最后在浏览器中访问 http://localhost:8888/ 即有数据返回,post请求方式可以使用postman测试。

备注

hotnode这个扩展库存在的目的是热更新,即每次修改代码后不需要重新启动

React 自定义ListView组件

胖蔡阅读(78)

ListView组件实现

通过适配器的实现,适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。这样就可以让ListView如同Android中的ListView一样动态设置Item布局、数据格式,Item类型等。

/* eslint no-dupe-keys: 0 */
import React, { Component } from "react";

class ListView extends Component{

  constructor(props) {
    super(props);
    // console.log("get props:",this.props)
    this.state = {
      data: props.data?props.data:[],
    };
  }

  componentDidMount() {
    //组件首次加载时
    console.log("get componentDidMount props:",this.props)
  };

  componentWillReceiveProps(nextProps){
    if(this.props.data != nextProps.data){
      this.setState({
        data:nextProps.data?nextProps.data:[],
      })
    }
  };

  /**
   * 
   * @param {*子布局数据格式} item 
   * @param {*子布局位置} index 
   */
  getItem(item,index){
    let itemLayout = this.props.adapter(item,index,{onClick:this.onItemClick.bind(this,item,index)})
    return itemLayout
  };

  onItemClick(item,index){
    console.log("-------------pre click log--------------")
    return this.props.OnItemClick(item,index)
  };

  render() {
    return (
      <div className="list" >
        {
          {
         !!this.state.data?(this.state.data.map((item,index) => {
           return this.getItem(item,index)
         })):null
        }
        }
      </div>   
    );
  }
}

export default ListView;

ListView组件调用

ListView可以在任意页面调用使用,只需在listView中指定adapter适配内容和点击操作接口即可,代码调用如下:

import React, { Component } from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import {
  NavBar,
  Icon,
  Carousel,
  WingBlank,
  NoticeBar,
  WhiteSpace,
} from "antd-mobile";
import { StickyContainer, Sticky } from "react-sticky";
import ListView from "./listview";
import * as homeActions from "../redux/reduces/home";
import { Toast } from "../../node_modules/antd-mobile/lib/index";
import axios from "axios";


axios.defaults.timeout = 100000;
axios.defaults.baseURL = "http://test.mediastack.cn/";

/**
 * http request 拦截器
 */
axios.interceptors.request.use(
  (config) => {
    config.data = JSON.stringify(config.data);
    config.headers = {
      "Content-Type": "application/json",
    };
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

@connect(
  (state) => ({ home: state.home }),
  (dispatch) => bindActionCreators(homeActions, dispatch)
)
class Home extends Component {
  state = {
    data: ["1", "2", "3"],
    imgHeight: 176,
  };

  constructor(props) {
    super(props);

    setTimeout(() => {
      this.setState({
        data: [
          "AiyWuByWklrrUDlFignR",
          "TekJlZRVCjLFexlOCuWn",
          "IJOtIlfsYdTyaDTRVrLI",
        ],
      });
    }, 100);
  }

  componentDidMount() {

    axios.get('/article/home/index').then(
      (res) => {
        this.setState({
          list: res.data.data,
        });
        console.log("get article response:", res);
      },
      (error) => {
        console.log("get response failed:", error);
      }
    );
  }

  handleBrowserChange = () => {
    const { history, changeRoute } = this.props;
    changeRoute();
    history.push("/docs");
  };

  /**
   * 推荐内容适配器
   * @param {*} item
   * @param {*} index
   */
  RecommentAdapter(item, index,props) {
    return (
      <div
        {...props}
        className="item"
        style={{
          height: 80,
          backgroundColor: "#fff",
          textAlign: "left",
          padding: "0 15px 0",
          display: "flex",
          fontSize: 20,
          flexFlow: "column nowrap",
          justifyContent: "center",
          marginBottom: 10,
        }}
        key={index}
      >
        <label
          className="title"
          style={{
            minWidth: 0,
            overflow: "hidden",
            textOverflow: "ellipsis",
            whiteSpace: "nowrap",
          }}
        >
          {item.title}
        </label>
        <div style={{
          fontSize:12,
          marginTop:12,
          color:'#888888'
        }}>
        <span style={{
          color:'#111111',
          fontWeight:'500',
        }}>{item.category}</span> |
        <span> {item.author}</span> |
        <span> 阅读量:{item.views}</span> |
        <span> {item.date}</span>
        </div>

      </div>
    );
  };


  onItemClick(item,index){
    Toast.show("你点击的第"+index+"个item.",Toast.SHORT)
  };

  render() {
    return (
      <div className="home">
        <StickyContainer>
          <Sticky>
            {({
              style,
              isSticky,
              wasSticky,
              distanceFromTop,
              distanceFromBottom,
              calculatedHeight,
            }) => (
              <NavBar
                style={{
                  ...style,
                  zIndex: 3,
                  padding: "2px 0",
                }}
                mode="light"
                icon={<Icon type="left" />}
                onLeftClick={() => console.log("onLeftClick")}
                rightContent={[
                  <Icon
                    key="0"
                    type="search"
                    style={{ marginRight: "16px" }}
                  />,
                  <Icon key="1" type="ellipsis" />,
                ]}
              >
                胖蔡杂谈
              </NavBar>
            )}
          </Sticky>
          {/* Sticky 为悬浮框 */}
          <WingBlank>
            <WhiteSpace size="lg" />
            <Carousel
              autoplay={false}
              infinite
              beforeChange={(from, to) =>
                console.log(`slide from ${from} to ${to}`)
              }
              afterChange={(index) => console.log("slide to", index)}
            >
              {this.state.data.map((val) => (
                <a
                  key={val}
                  href="http://www.alipay.com"
                  style={{
                    display: "inline-block",
                    width: "100%",
                    height: this.state.imgHeight,
                  }}
                >
                  <img
                    src={`https://zos.alipayobjects.com/rmsportal/${val}.png`}
                    alt=""
                    style={{
                      width: "100%",
                      verticalAlign: "top",
                    }}
                    onLoad={() => {
                      // fire window resize event to change height
                      window.dispatchEvent(new Event("resize"));
                      this.setState({
                        imgHeight: "auto",
                      });
                    }}
                  />
                </a>
              ))}
            </Carousel>
            <WhiteSpace size="lg" />
            <NoticeBar
              marqueeProps={{ loop: true, style: { padding: "0 7.5px" } }}
            >
              通知:北京继续暂停出租车顺风车出京运营
              合肥明日继续新一波消费券发放,预计发放4000万元
            </NoticeBar>
            <WhiteSpace size="lg" />
            <ListView data={this.state.list} 
                  adapter={this.RecommentAdapter} 
                  OnItemClick={this.onItemClick}/>
          </WingBlank>
        </StickyContainer>
      </div>
    );
  }
}

export default Home;

adapter 返回单个Item的jsx,可通过React组件或者方法返回均可。

React应用中封装axios

胖蔡阅读(109)

Axios简介

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。

特性

  • 支持node端和浏览器端
  • 支持拦截器等高级配置
  • 使用Promise管理异步,告别传统callback方式
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF

安装

  1. yarn 安装
$ yarn add axios
  1. npm 安装
npm install axios -D

3.bower 安装

$ bower install axios

简单使用

aixos 可直接通过cdn加载实用,如下示例:

<html>
<head>
<title>Axios的使用 </title>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.19.2/axios.js"></script>
</head>
<body>
   <input type="button" onclick="getList()" value="点击获取"/>
   <div id="content"> </div>

   <script type="text/javascript">

      function getList(){
             axios.request({
                 url:'/article/home/index',
                 method:'get',
                 baseURL:'http://test.mediastack.cn/'

            }).then(
               res => {
                 console.log("get res:",res);
                 var str=JSON.stringify(res);
                 document.getElementById("content").innerHTML = str;

              },error => {
                 console.log("get request failed:",error);
                 document.getElementById("content").innerHTML = error;
              }
            );
      }

   </script>
</body>
</html>

react中封装axios

react中可以将axios封装成一个文件,通过控制操作,可以实现错误、逻辑、和验证统一处理,降低代码的冗余度和可读性。

请求封装

/**
 * 网络请求配置
 */
import axios from "axios";

axios.defaults.timeout = 100000;
axios.defaults.baseURL = "http://test.mediastack.cn/";

/**
 * http request 拦截器
 */
axios.interceptors.request.use(
  (config) => {
    config.data = JSON.stringify(config.data);
    config.headers = {
      "Content-Type": "application/json",
    };
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

/**
 * http response 拦截器
 */
axios.interceptors.response.use(
  (response) => {
    if (response.data.errCode === 2) {
      console.log("过期");
    }
    return response;
  },
  (error) => {
    console.log("请求出错:", error);
  }
);

/**
 * 封装get方法
 * @param url  请求url
 * @param params  请求参数
 * @returns {Promise}
 */
export function get(url, params = {}) {
  return new Promise((resolve, reject) => {
    axios.get(url, {
        params: params,
      }).then((response) => {
        landing(url, params, response.data);
        resolve(response.data);
      })
      .catch((error) => {
        reject(error);
      });
  });
}

/**
 * 封装post请求
 * @param url
 * @param data
 * @returns {Promise}
 */

export function post(url, data) {
  return new Promise((resolve, reject) => {
    axios.post(url, data).then(
      (response) => {
        //关闭进度条
        resolve(response.data);
      },
      (err) => {
        reject(err);
      }
    );
  });
}

/**
 * 封装patch请求
 * @param url
 * @param data
 * @returns {Promise}
 */
export function patch(url, data = {}) {
  return new Promise((resolve, reject) => {
    axios.patch(url, data).then(
      (response) => {
        resolve(response.data);
      },
      (err) => {
        msag(err);
        reject(err);
      }
    );
  });
}

/**
 * 封装put请求
 * @param url
 * @param data
 * @returns {Promise}
 */

export function put(url, data = {}) {
  return new Promise((resolve, reject) => {
    axios.put(url, data).then(
      (response) => {
        resolve(response.data);
      },
      (err) => {
        msag(err);
        reject(err);
      }
    );
  });
}

//统一接口处理,返回数据
export default function (fecth, url, param) {
  let _data = "";
  return new Promise((resolve, reject) => {
    switch (fecth) {
      case "get":
        console.log("begin a get request,and url:", url);
        get(url, param)
          .then(function (response) {
            resolve(response);
          })
          .catch(function (error) {
            console.log("get request GET failed.", error);
            reject(error);
          });
        break;
      case "post":
        post(url, param)
          .then(function (response) {
            resolve(response);
          })
          .catch(function (error) {
            console.log("get request POST failed.", error);
            reject(error);
          });
        break;
      default:
        break;
    }
  });
}

//失败提示
function msag(err) {
  if (err && err.response) {
    switch (err.response.status) {
      case 400:
        alert(err.response.data.error.details);
        break;
      case 401:
        alert("未授权,请登录");
        break;

      case 403:
        alert("拒绝访问");
        break;

      case 404:
        alert("请求地址出错");
        break;

      case 408:
        alert("请求超时");
        break;

      case 500:
        alert("服务器内部错误");
        break;

      case 501:
        alert("服务未实现");
        break;

      case 502:
        alert("网关错误");
        break;

      case 503:
        alert("服务不可用");
        break;

      case 504:
        alert("网关超时");
        break;

      case 505:
        alert("HTTP版本不受支持");
        break;
      default:
    }
  }
}

/**
 * 查看返回的数据
 * @param url
 * @param params
 * @param data
 */
function landing(url, params, data) {
  if (data.code === -1) {
  }
}

如上,可通过过对axios请求的拦截实现添加公共请求头,token 验证等操作。

请求隔离

import http from '../utils/http';



/**
 * 获取首页列表
 */
function getArticleList(){
  return new Promise((resolve, reject) => {
    http("get",'/article/home/index').then(res => {
      resolve (res);
    },error => {
      console.log("网络异常~",error);
      reject(error)
    })
  }) 
}

export {
   getArticleList
}

react 组件调用如下:

import React, { Component } from "react";
import { getArticleList } from "~/api/blog";

class Home extends Component {
    constructor(props) {
        super(props);
    } 

    componentDidMount() {
       getArticleList().then(
          (res) => {
              console.log("get article response:", res);
          },
         (error) => {
              console.log("get response failed!");
          }
       );
    }

 ......
}


export default Home;

这样,可以做到尽量让数据请求可以页面逻辑互不干扰,也方便后期统一维护和修改。