Charon

Charon 查看完整档案

北京编辑邢台学院  |  计算机基础与应用 编辑望海康信  |  高级工程师 编辑 segmentfault.com/u/charon_5f4765a39cea7 编辑
编辑

世界核平

个人动态

Charon 关注了用户 · 11月26日

阿里巴巴淘系技术 @alibabataoxijishu

阿里巴巴集团淘系技术部官方账号。淘系技术旗下包含淘宝技术、天猫技术、闲鱼技术、躺平等团队和业务。我们服务9亿用户,赋能各行业1000万商家,并成功主导了11次阿里巴巴经济体双十一技术大考,打造了全球领先的线上新零售技术平台。
我们的愿景是致力于成为全球最懂商业的技术创新团队,让科技引领面向未来的商业创新和进步。
公众号关注:淘系技术

关注 259

Charon 关注了用户 · 11月26日

孤篷 @king_hcj

Nothing is given, Everything is earned!

关注 105

Charon 关注了用户 · 11月26日

bingfeng @xiaotao_5c2cdb80411ca

日拱一卒,功不唐捐

关注 51

Charon 关注了用户 · 11月26日

民工哥 @jishuroad

民工哥,10多年职场老司机的经验分享,坚持自学一路从技术小白成长为互联网企业信息技术部门的负责人。

我的新书:《Linux系统运维指南》

微信公众号:民工哥技术之路

民工哥:知乎专栏

欢迎关注,我们一同交流,相互学习,共同成长!!

关注 2780

Charon 关注了用户 · 11月26日

Java旅途 @javatrip

公众号:Java旅途
github:https://github.com/binzh303/s...

关注 1658

Charon 关注了用户 · 11月26日

JS_Even_JS @js_even_js

前端工程师
不定时更新
前端小狮子一枚,希望大家多多支持

关注 307

Charon 关注了用户 · 11月26日

robin @robin_ren

前端开发一枚,做过RN、小程序。

关注 39

Charon 关注了用户 · 11月26日

皮小蛋 @scaukk

The Best Way to Improve Yourself:

  1. Build Stuffs
  2. Help Others
  3. Teach

PS: Shopee 招人, 薪酬福利待遇好

感兴趣的话, 可以联系我内推。

关注 743

Charon 关注了用户 · 11月26日

fantasticbaby @fantasticbaby

专注于大前端领域 && 养了3只布偶猫
以 iOS 端为输出,研究动态化、工程化、APM 监控系统、无痕埋点系统、打包构建系统、软件测试、质量把控、效能工具等

微博:https://weibo.com/u/3194053975
Github:https://github.com/FantasticLBP

关注 234

Charon 关注了用户 · 11月26日

思否编辑部 @writers

让我们陷入困境的不是无知,而是看似正确的谬误论断。思考、否定、再思考,出家人不打诳语,撰文者不说空话。

欢迎通过私信投稿、提建议、分享素材、传闲话。

联系邮箱 pr@sifou.com 小姐姐微信:https://segmentfault.com/n/13...

关注 5652

Charon 发布了文章 · 11月24日

前端工程化(webpack-eslint)

前面讲了,wepack一些相关的知识,这里集成一下eslint的相关配置。
首先安装对应模块:

  • 安装eslint模块
  • 安装eslint-loader模块
  • 初始化 .eslintrc.js文件配置

安装完成后先初始化配置:
yarn eslint --init
然后回答一些问题:
1.我们选择检查语法,发现问题,执行统一代码风格
2.模块化这里我们使用javascript modules(es6语法)
3.我们目前假设说配置的是个react项目,选react
4.我们这里使用ts,选yes(使用ts的时候要先把ts安装下来,不然后续操作会报错)
5.最终我专门这个工程运行时浏览器Browser
6.我们使用市面上主流风格 选择了第一个 (使用了开源社区的规范Standard: https://github.com/standard/s...
7.配置文件使用了js格式
8.最后自动安装一些其他的包
最后配置文件js
image.png
如果有一些全局要用的东西可以添加,例如jQuery
globals:{

"jQuery":"readonly"

}

eslint配置注释
http://eslint.cn/docs/user-gu...

webpack 中loader配置
image.png

然后就是eslint结合git hooks钩子在提交之前检测代码
使用husky
首先进行安装

npm install husky --save-dev

然后对应package.json配置
image.png
husky属性配置 pre-commit 这个git钩子阶段,来进行我们的npm run test来进行检查
然后 npm scripts里对应添加test命令,

test:"eslint . src/"

我们检测src目录下的所有文件.

我们可以手动在添加一个修复命令用来手动执行

test:"eslint --fix . src/"

也可以git commit时自动格式化然后再add回来:
首先安装

npm install -D lint-staged

然后配置package.json,
通过procommit调起lint-staged

{
  "scripts": {
    "precommit": "lint-staged"
  },
  "lint-staged": {
    "src/**/*.js": ["eslint --fix" , "git add"]
  },
  "husky":{
    "hooks":{
        "pre-commit":"npm run precommit"
    }
  }
}

这样就能在commit之前,先用eslint修复在git add了。

如果想和Prettier配合起来一起使用的话请参考如下:
Prettier介绍与基本用法

查看原文

赞 0 收藏 0 评论 0

Charon 发布了文章 · 11月17日

前端工程化(模块化开发下2-2)

babfile## 模块化开发
前面说了gulp工作流
这边现在来讲下模块化开发
模块化开发是当下最重要的开发范式
模块化只是思想。
模块化的演变过程

  1. 文件划分的方式(原始方式完全靠约定)
    缺点:
    1.污染全局作用域
    2.命名冲突
    3.无法管理模块依赖关系

2.命名空间方式(放到一个对象,然后再统一使用)
3.然后就是AMD和CMD规范(社区逐渐提出的规范,历史本章不作为重点讲)
然后现在最后就是模块化规范的最佳实践:
node.js的CommonJS和eES Modules (官方推出)

Es Modules基本特性:

  • 自动采用严格模式,忽略"use strict"
  • 每个ESM模块都是单独的私有作用域
  • ESM是通过CORS去请求外部js模块 (需要允许CORS资源请求)
  • ESM的script标签会延迟执行脚本(不会像正常script标签立即执行)

image.png
这里导入的是一个只读性引用,所以模块外部不可更改,多个地方导入,模块内部值变化都会影响导入地方。
import 是一个 导入声明,在头部使用,不可出现在条件中
属于是静态阶段确定依赖,可以按需引入依赖项(通过webpack摇树 tree-shaking去除不需要的),动态执行,实际执行的时候去模块内取值。
动态导入import()返回一个promsie。
export {}是固定语法,不是导出对象。
和commonjs互相导入的支持情况
image.png

CommonJS特点:

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

对module对象的描述
1.module.exports属性
module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。
2.exports变量
node为每一个模块提供了一个exports变量(可以说是一个对象),指向 module.exports。这相当于每个模块中都有一句这样的命令 var exports = module.exports;

这样,在对外输出时,可以在这个变量上添加方法。例如  exports.add = function (r){return Math.PI r r};注意:不能把exports直接指向一个值,这样就相当于切断了 exports 和module.exports 的关系。例如 exports=function(x){console.log(x)};
一个模块的对外接口,就是一个单一的值,不能使用exports输出,必须使用 module.exports输出。module.exports=function(x){console.log(x);};
用阮老师的话来说,这两个不好区分,那就放弃 exports,只用 module.exports 就好(手动机智)

在浏览器我们是不适合使用CommonJS规范的,动态读取时查看到这个require模块,然后发请求返回文件,这会程序会卡在那里,这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。

所以浏览器使用的是ESM,但是因为它是es6的模块,有部分浏览器还不支持image.png
在浏览器中js的ESM 的工作样子是:
image.png
通过一个script标签中的type设置为module,就可以表明它是一个模块了。
image.png
app.js的内容

基于我们现在项目不同类型文件过多,过于分散,网络请求频繁,所以我们现在设想所有的前端资源都需要模块化。
前面说的ESM和CommonJs都是对js的模块化。
我们现在需要做的是对所有资源的模块化:
image.png
image.png
image.png

  • 新特性代码的编译
  • 模块化javascript打包
  • 支持不同类型的资源模块

下面就是介绍我们的明星级产品WebPack

webpack

本质上,_webpack_ 是一个现代 JavaScript 应用程序的_静态模块打包器(module bundler)_。当 webpack 处理应用程序时,它会递归地构建一个_依赖关系图(dependency graph)_,其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 _bundle_。
image.png
从官网看也可以看出来,它是把所有的文件打包成js文件导出
webpack的配置文件默认使用CommonJs规范

什么是 webpack 模块

对比 Node.js 模块,webpack _模块_能够以各种方式表达它们的依赖关系,几个例子如下:

  • ES2015 import 语句
  • CommonJSrequire() 语句
  • AMDdefinerequire 语句
  • css/sass/less 文件中的 @import 语句
  • 样式(url(...))或 HTML 文件(<img data-original=...>)中的图片链接(image url)

以上这些文件的载入都是会触发webpack打包机制的,检测到对应的文件类型就会触发对应的loader去进行处理成javascript代码输出。

这样它基本对我们常用的资源文件都可以做模块化转成js处理了,它提供的功能可以完美的满足我们的需求了。(webpack !=前端工程化,前端工程化是一个概念)
下面就让我们一起来了解它,先看官方说法:

入口(entry)

webpack 应该使用哪个模块,来作为构建其内部_依赖图_的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。

每个依赖项随即被处理,最后输出到称之为 bundles 的文件中

输出(output)
output 属性告诉 webpack 在哪里输出它所创建的 _bundles_,以及如何命名这些文件,默认值为 ./dist。基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。你可以通过在配置中指定一个 output 字段

loader
loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。

插件(plugins)
loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。

模式
通过选择 developmentproduction 之中的一个,来设置 mode 参数,你可以启用相应模式下的 webpack 内置的优化
生产模式下,自动优化打包结果。
开发模式下,自动优化打包速度,添加一些调试过程中的辅助。
none模式下,运行最原始的打包不做任何额外处理。

下面就让我们深入的了解下webpack的工作机制(4):
首先我们来安装webpack和它的脚手架webpack-cli(此工具用于在命令行中运行 webpack,可以再node_modules的bin目录下查看它的.cmd文件中命令执行)
默认执行 webpack入口值是./src,然后根据package.json里的main属性指定的文件为入口文件。
出口输出文件默认值就是上面有写
从 webpack v4.0.0 开始,可以不用引入一个配置文件。
直接命令行执行webpack --mode=production 启动生产模式优化,这样输出到dist文件夹下,就完成了一个我们基本的打包。

下面主要介绍的是主流使用方式,也就是配置文件的方式。
使用yarn init生成一个项目下载webpack基本依赖:
image.png
然后开始写我们的webpack.config.js

const path = require('path')

module.exports = {
  // 这个属性有三种取值,分别是 production、development 和 none。
  // 1. 生产模式下,Webpack 会自动优化打包结果;
  // 2. 开发模式下,Webpack 会自动优化打包速度,添加一些调试过程中的辅助;
  // 3. None 模式下,Webpack 就是运行最原始的打包,不做任何额外处理;
  mode: 'none',
  entry: './src/main.js',//支持多入口,官方查看
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist')//需要写入绝对路径,借助node的path
  }
}

mian.js的代码结构

import createHeading from './heading.js'

const heading = createHeading()

document.body.append(heading)

heading.js的代码结构

export default () => {
  const element = document.createElement('h2')

  element.textContent = 'Hello world'
  element.addEventListener('click', () => {
    alert('Hello webpack')
  })

  return element
}

然后执行webpack none命令打包操作,我们看一下最后weboack的生成内容。
打开bundle.js进行代码折叠
image.png
一个立即执行函数(webpackBootstrap)引导函数,组织各模块之间的引用关系,这是webpack的入口,接收一个modules参数,然后调用时传入了一个数组,
image.png
数组里每项都是个参数列表相同的函数,这些函数里就是我们源代码中的模块代码,然后我们的模块都会被包裹在这样的函数中,从而去实现模块的私有作用域,看注释和代码可以看出下标0就是我们的main.js,然后1位置的就是heading.js。
然后再展开webpack工作入口函数(立即执行函数),
image.png
最开始是定义了一个缓存模块的对象,把加载过的模块缓存起来,下面又定义了一个载入模块的require函数,require函数的内部就是传入参数(模块对象,导出成员对象,require函数)调用我们载入的模块,最后返回模块的exports导出
然后就是require函数挂载一些工具函数
image.png
webpack工作入口最后调用了webpack_require函数,传入的id参数为0,来加载我们传入数组的第一个模块(开始加载入口模块)。
image.png
继续往下看
image.png

模块0开始调用了一个__webpack_require__.r工具函数,这个函数就是给我们模块添加一个标记,标明它是一个__esModule。然后就是再继续调用__webpack_require__函数传入id为1,这会加载的就是我们mian.js导入的heading.js了,然后根据heading导出方式export default来给__webpack_require__传入的导出成员对象赋值,完成之后跳回模块0代码也就是main.js,__webpack_require__会返回传入模块id的导出成员, 然后再去调用从模块1导出来的成员。
这样我们的所有模块就加载完了。
这就是一个大概的运行过程。

下面再来了解一下loader,webpack中loader的作用就是把其他类型的文件内容转成webpack能处理的js模块放入最后应用程序的依赖图js中(bundle)。
拿css-loader举例

css-loader

const path = require('path')

module.exports = {
  mode: 'none',
  entry: './src/main.css',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      }
    ]
  }
}

在 webpack 的配置中 loader 有两个目标:

  1. test 属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。
  2. use 属性,表示进行转换时,应该使用哪个 loader。

这俩也和我们最开始打包例子一样,会把我们loader转换的模块放到webpack引导函数调用的数组里,在我们的css文件中,使用了两个loader,css-loader是把我们编写的css转换成js模块,然后style-loader是用来把我们css-loader转换的结果通过style标签形式 放入界面中。
lader的执行也是从下到上执行,类似于函数组合的从右到左。
use数组里的loader类似一个管道,前一个执行的结果会传给后一个,最后一个执行的loader一定是返回一段js代码。
下面看一些常用的loader:

file-loader

通过file-loader来处理字体文件和图片文件,代码如下

const path = require('path')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    publicPath: 'dist/'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.png$/,
        use: 'file-loader'
      }
    ]
  }
}

image.png
这样file-loader检测到这个导入引用图片的时候会把这个图片拷贝到我们的输出目录,然后把文件路径返回。
因为本人是测试使用的 npm serve这个库测试
image.png
项目结构是这样,默认访问index.html,输出地址是dist所以资源路径需要配置一下, publicPath: 'dist/' (publicPath可以配合CDN和hash可以查看官网).

配置了publicPath后打包后文件路径就会拼接上,如下
image.png
image.png

这种file-loader的做法是物理拷贝,还有一种方式是Data Urls的方式表示文件,传统方式是服务器上有一个对应文件,我们通过请求这个地址得到这个文件,而Data Urls是当前地址可以直接表示内容的方式,也就是说这种url中的文本就已经包含了文件的内容:
image.png
image.png
image.png
image.png
image.png
使用这种url时就是发送任何的http请求,就可以根据这个url直接解析出来内容。
二进制文件会进行base64编码,使用base64来表示内容,然后在webpack中我们就借助url-loader来转换文件为Data Url的方式来展示图片。

url-loader

如下:

const path = require('path')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    publicPath: 'dist/'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.png$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 // 10 KB
          }
        }
      }
    ]
  }
}

上面这种配置,碰到图片文件就不会使用物理copy的方式了,就会使用Data Url方式转换为base64来进行展示。
image.png
然后我们都知道图片越大转化出来的base64就会越长,太大的图片就不适合放在代码中了,更适合做CDN静态资源。所以我们给url-loader添加一个配置limit配置文件大小,超过这个大小的就做base64得转换了。
所以总结就是:

  • 小文件使用Data Urls,减少请求次数(url-loader)
  • 大文件单独提取存放,提高加载速度(file-loader)

这样的话url-loader就必须也要下载file-loader了,因为对于超出url-loader设置大小的文件还是会调用file-loader.来进行文件copy.

webpack 的loader分类

  • 编译转换类(加载到模块转换为javascript代码)
    例如:css-loader
  • 文件操作类(把文件copy到我们的输出目录,并且把访问路径向外导出)
    例如:file-loader
  • 代码检查类(对代码进行校验)
    例如:eslint-loader

babel-loader

再说一种常用的loader,转换es6,也就是babel-loader
下载依赖 babel-loader(转换平台,配置中再具体使用特性转换插件),以及babel的核心模块@babel/core,以及所有新特性转换插件@babel/preset-env
配置:

const path = require('path')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    publicPath: 'dist/'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            //babel-polyfill按需引入
           presets: [['@babel/preset-env', { useBuiltIns: 'usage', corejs: 3 }]]
          }
        }
      },
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.png$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 // 10 KB
          }
        }
      }
    ]
  }
}

这样就完成了对es6的编译。
babel得优化
https://zhuanlan.zhihu.com/p/139359864
corejs3 polyfill原型方法

之前最开始说过了几种会触发webpack触发模块loader处理机制的几种情况,还有两种html中情况需要特殊处理一下,一个是图片的src一个是a标签的href需要配置html-loader
image.png
image.png
image.png

loader机制是webpack的核心
我们来实现一个自己的loader,来处理.md文件
首先创建markdown-loader.js文件来处理.md文件,结构如下
image.png
about.md中是一段文字
然后我们来编写markdown-loader.js

const marked = require('marked')

module.exports = source => {
  // console.log(source)
  // return 'console.log("hello ~")'
  const html = marked(source)
 
  // return `module.exports = "${html}"`
  // return `export default ${JSON.stringify(html)}`

  // 返回 html 字符串交给下一个 loader 处理
  return html
}

第一种操作时,遵循最后一个loader中返回js代码的规则,所以我们return 'js代码',使用JSON.stringify是因为 html中特殊字符进行转义。

另一种操作就是,我们把这段转换过后html代码返回给下一个loader惊醒处理,像之前说的,loader的处理类似管道。
webpack配置如下

const path = require('path')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    publicPath: 'dist/'
  },
  module: {
    rules: [
      {
        test: /\.md$/,
        use: [
          'html-loader',
          './markdown-loader'
        ]
      }
    ]
  }
}

这段配置中我们先用markdown-loader把.md文件中的文字转换成html代码,然后再把这段代码交给html-loader进行处理。

webpack插件机制(plugins)

插件目的在于解决 loader 无法实现的其他事
由于插件可以携带参数/选项,你必须在 webpack 配置中,向 plugins 属性传入 new 实例。
我们现在记录一些常用的plugins
webpack.config.js

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    // publicPath: 'dist/'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.png$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 // 10 KB
          }
        }
      }
    ]
  },
  plugins: [
    new webpack.ProgressPlugin(),
    new CleanWebpackPlugin(),
    // 用于生成 index.html
    new HtmlWebpackPlugin({
      title: 'Webpack Plugin Sample',
      meta: {
        viewport: 'width=device-width'
      },
      template: './src/index.html'
    }),
    new CopyWebpackPlugin([
      // 'public/**'
      'public'
    ])
  ]
}

webpack.ProgressPlugin用来监控各个hook执行的进度percentage,输出各个hook的名称和描述。
CleanWebpackPlugin用来每次打包清空dist文件夹。
HtmlWebpackPlugin 指定使用哪个html作为模板生成,里面可以填一些配置信息。<%= htmlWebpackPlugin.options.title %> html内部这样取值.
CopyWebpackPlugin用来copy静态资源文件(文件夹)到,指定的输出目录。
因为这会我们把html生成输出到dist同级目录中了,所以publicPath就注释去掉了。
我们对插件有所了解之后,来实现一个我们自己的插件。
webpack 插件是一个具有 apply 属性的 JavaScript 对象。apply 属性会被 webpack compiler(编译) 调用,并且 compiler 对象可在整个编译生命周期访问。

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

class MyPlugin {
  apply (compiler) {
    console.log('MyPlugin 启动')

    compiler.hooks.emit.tap('MyPlugin', compilation => {
      // compilation => 可以理解为此次打包的上下文
      for (const name in compilation.assets) {
        // console.log(name)
        // console.log(compilation.assets[name].source())
        if (name.endsWith('.js')) {//模块后缀是js
          const contents = compilation.assets[name].source()//通过source方法获取内容
          const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
          compilation.assets[name] = {
            source: () => withoutComments,
            size: () => withoutComments.length
          }
        } 
      }
    })
  }
}

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    // publicPath: 'dist/'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.png$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 // 10 KB
          }
        }
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    // 用于生成 index.html
    new HtmlWebpackPlugin({
      title: 'Webpack Plugin Sample',
      meta: {
        viewport: 'width=device-width'
      },
      template: './src/index.html'
    }),
    // 用于生成 about.html
    new HtmlWebpackPlugin({
      filename: 'about.html'
    }),
    new CopyWebpackPlugin([
      // 'public/**'
      'public'
    ]),
    new MyPlugin()
  ]
}

我们写的apply方法会在webpack编译期调用,compiler 对象代表了完整的 webpack 环境配置(更多信息可以查看官方文档)。我们可以通过它来获取,webpack编译期间的hooks生命周期钩子函数(具体生命周期可以看管方手册),compiler hook 的 tap 方法的第一个参数,应该是驼峰式命名的插件名称。建议为此使用一个常量,以便它可以在所有 hook 中复用。
webpack钩子
我们再edit(生成资源到 output 目录之前�触发,这是一个异步串行 AsyncSeriesHook 钩子)这个钩子中注册了一个MyPlugin的插件,然后通过compilation对象(代表了一次资源版本构建,一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。)来获取模块的信息,如果Chunk是js,通过

compilation.assets[name].source()

获取内容加工处理后,重写当前模块的 对应source,size方法。

这样我们就实现了一个我们自己的插件(更复杂的插件可以查看官方手册,这里只做一个简单的说明)。

webpack通过 --watch 工作的话不会理解结束,会监视我们的文件变化,除非我们手动借宿。

webpack-dev-server

为你提供了一个简单的 web 服务器,并且能够自动编译,实时重新加载(live reloading),提供代理url的功能。
简单例子:

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
  mode: 'development',
  entry: './src/main.js',
  output: { //默认执行webpack打包输出目录 ./dist,
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist')
  },
  devtool:'cheap-module-eval-source-map',
  devServer: {
    contentBase: './public',
    hot: true,
    // hotOnly: true // 只使用 HMR,不会 fallback 到 live reloading
    proxy: {
      '/api': {
        // http://localhost:8080/api/users -> https://api.github.com/api/users
        target: 'https://api.github.com',
        // http://localhost:8080/api/users -> https://api.github.com/users
        pathRewrite: {
          '^/api': ''
        },
        // 不能使用 localhost:8080 作为请求 GitHub 的主机名
        changeOrigin: true
      }
    }
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.png$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 // 10 KB
          }
        }
      }
    ]
  },
  plugins: [
    //new CleanWebpackPlugin(),
    // 用于生成 index.html
    new HtmlWebpackPlugin({
      title: 'Webpack Tutorials',
      meta: {
        viewport: 'width=device-width'
      },
      template: './src/index.html'
    }),
    // // 开发阶段最好不要使用这个插件
    // new CopyWebpackPlugin(['public'])
    new webpack.HotModuleReplacementPlugin()
  ]
}

contentBase告诉服务器从哪里提供内容。只有在你想要提供静态文件时才需要。devServer.publicPath 将用于确定应该从哪里提供 bundle,并且此选项优先。
publicPath此路径下的打包文件可在浏览器中访问。
假设服务器运行在 http://localhost:8080 并且 output.filename 被设置为 bundle.js。默认 publicPath"/",所以你的包(bundle)可以通过 http://localhost:8080/bundle.js 访问。

hot启动热更新,注意,webpack。完全启用HMR需要使用HotModuleReplacementPlugin。如果webpack或webpack-dev-server以——hot选项启动,这个插件将自动添加,所以您可能不需要将它添加到您的webpack.config.js中,在这里是做了手动添加的。

我们在开发富文本编辑器时,js模块热更新,输入的内容也会清空掉,因为它并不能确定你的js返回了什么。
这个时候我们也可以通过module.hot来控制热更新。
例如:

import createEditor from './editor'
import background from './better.png'
import './global.css'

const editor = createEditor()
document.body.appendChild(editor)

const img = new Image()
img.src = background
document.body.appendChild(img)

// ============ 以下用于处理 HMR,与业务代码无关 ============

// console.log(createEditor)

if (module.hot) {
  let lastEditor = editor
  module.hot.accept('./editor', () => {
    // console.log('editor 模块更新了,需要这里手动处理热替换逻辑')
    // console.log(createEditor)

    const value = lastEditor.innerHTML
    document.body.removeChild(lastEditor)
    const newEditor = createEditor()
    newEditor.innerHTML = value
    document.body.appendChild(newEditor)
    lastEditor = newEditor
  })

  module.hot.accept('./better.png', () => {
    img.src = background
    console.log(background)
  })
}

正常来说我们通过module.hto控制的模块报错后,还是会走默认的热更新,所以hotOnly配置它来阻止。
其他的一些dev-server配置信息可查看:
webpack-dev-server配置
然后我们把webpack-dev-server 命令添加到npm scripts 中就可以运行了

devtool

image.png
根据需要开发阶段个人感觉:cheap-module-eval-source-map,eval-source-map会方便点,首次构建慢,重新构建还可以接受。

接下来我们做下生产环境和开发环境的区分
首先是一个公共的配置webpack.common.js

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: './src/main.js',
  output: {
    filename: 'js/bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.(png|jpe?g|gif)$/,
        use: {
          loader: 'file-loader',
          options: {
            outputPath: 'img',
            name: '[name].[ext]'
          }
        }
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Webpack Tutorial',
      template: './src/index.html'
    })
  ]
}

webpack.dev.js 开发环境的配置

const webpack = require('webpack')
const merge = require('webpack-merge')
const common = require('./webpack.common')

module.exports = merge(common, {
  mode: 'development',
  devtool: 'cheap-eval-module-source-map',
  devServer: {
    hot: true,
    contentBase: 'public'
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
})

webpack.prod.js生产环境的配置

const merge = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const common = require('./webpack.common')

module.exports = merge(common, {
  mode: 'production',
  plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin(['public'])
  ]
})

最后给package.json添加scripts命令

 "build": "webpack --config webpack.prod.js",
 "start": "webpack-dev-server --config webpack.dev.js"

webpack优化

然后就是一些webpack优化相关的知识了。
optimization 对象
webpack不同的mode会进行不同的优化,但是这些优化也还都是可以手动配置的
webpck中mode模式的优化

module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  },
  optimization: {
    // 模块只导出被使用的成员
    usedExports: true,
    // 尽可能合并每一个模块到一个函数中
    concatenateModules: true,
    // 压缩输出结果
    // minimize: true
  }
}

也就是这样配置,我们就可以(usedExports配合minimize)只导出被使用的成员,usedExports标识导出被使用成员(导出名称会被处理做单个标记字符),然后minimize把无用代码筛除,然后压缩输出,这样就形成了摇树的功能。
waring:老的webpack版本有人碰到过使用babel后摇树无效的情况。
因为摇树要使用 ES2015模块语法,所以配置babel-loader

  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              // 如果 Babel 加载模块时已经转换了 ESM,则会导致 Tree Shaking 失效
              // ['@babel/preset-env', { modules: 'commonjs' }]
              // ['@babel/preset-env', { modules: false }]
             // 也可以使用默认配置,也就是 auto,这样 babel-loader 会自动关闭 ESM 转换
              ['@babel/preset-env', { modules: 'auto' }]
            ]
          }
        }
      }
    ]
  }

这会我们会碰到另外一个问题
例如:
image.png
image.png
index.js中引入的css和js原型方法都没有导出,只是原型做了扩展。这样我们上面的usedExports就不会标识,最后摇树 的时候就会把他们去掉,这样和我们的需求不符。

所以我们设置optimization中的sideEffects为true,然后在package.json中设置:
规则如下:

Mark the file as side-effect-free 标记副作用文件

通过指定sideEffects为false可以告诉webpack的编译器,我们的代码是纯的,可以安全地进行摇树优化。有两种方式:
1.设置sideEffects属性为false
image.png
2.若真的有副作用的文件,就在数组中指定出来,摇树时不要去除:
image.png
“副作用”定义为导入时执行特殊行为的代码,而不是公开一个或多个导出。比如 polyfills,它影响全局范围,通常不提供导出。
mode为production 或者设置optimize-minimize标志为true时都会进行一个摇树的优化

总结

要使用摇树优化:

  1. 使用 ES2015模块语法
  2. 确保没有编译器将 ES2015模块语法转换为 CommonJS 模块
  3. 在项目的 package.json 文件中添加“ sideEffects”属性
  4. 使用生产模式配置选项删除dead-code,达到摇树优化效果

您可以将应用程序想象为一棵树。 你实际使用的源代码和库代表了绿色的树叶,dead-code 代表着秋天里枯萎的树叶。为了摆脱枯叶,你不得不摇动树木,使它们掉下。

然后就是配置多入口和提取公共模块:
image.png
image.png
image.png
这样俩文件的公共模块就会被提取出来。

这样就会涉及到:

分离app(应用程序)和vendor(第三方库)入口

小于webpack4的版本我们 分离app和第三方代码 webpack\<4
image.png

webpack4分离公共代码
webpack4抽离公共代码方式
image.png

还有一种可以动态引入的方式就是通过es6的import()
例如:
image.png

最后一部分就是webpack生产文件的hash,js的压缩,css的压缩相关信息,如下:
image.png
最后需要了解一下runtimeChunk,属性。

设置runtimeChunk是将包含chunks 映射关系的 list单独从 app.js里提取出来,因为每一个 chunk 的 id 基本都是基于内容 hash 出来的,所以每次改动都会影响它,如果不将它提取出来的话,等于app.js每次都会改变。缓存就失效了。设置runtimeChunk之后,webpack就会生成一个个runtime~xxx.js的文件。
然后每次更改所谓的运行时代码文件时,打包构建时app.js的hash值是不会改变的。如果每次项目更新都会更改app.js的hash值,那么用户端浏览器每次都需要重新加载变化的app.js,如果项目大切优化分包没做好的话会导致第一次加载很耗时,导致用户体验变差。现在设置了runtimeChunk,就解决了这样的问题。所以这样做的目的是避免文件的频繁变更导致浏览器缓存失效,所以其是更好的利用缓存。提升用户体验。

上线优化方案

最后Rollup用来做框架会合适一点(了解)

parcel零配置前端应用打包器(了解,写demo可以使用)

后续下一篇讲下eslint和webpack的集成。

发现一遍webpack优化讲的很好,进一步优化可以参考
webpack4大结局:加入腾讯IM配置策略,实现前端工程化环境极致优化

本文内容借鉴于 拉钩大前端训练营

查看原文

赞 0 收藏 0 评论 0

Charon 发布了文章 · 11月9日

前端工程化(上2-1)

技术是为了解决问题存在的

以前那种写demo套模板,已经远远不能满足我们现在开发要求了,所以就有了前端工程化的概念。

image.png

既然说了技术是为了解决问题存在的,前端工程化也不例外。
问题:

  • 想使用es6+新特性,但是浏览器不兼容
  • 想要使用less,sass,PostCss增强css特性,但是运行环境不能直接支持
  • 想要模块化的方式提高项目可维护性,但是运行环境不能直接支持
  • 部署前需要手动压缩代码及资源文件
  • 部署过程需要手动上传代码到服务器
  • 多人协同开发,无法硬性统一大家代码风格,从仓库pull代码质量无法保证
  • 部分功能开发时需要等待后端服务接口提前完成
主要解决的问题:

image.png

工程化的表现:一切以提高效率,降低成本,质量保证为目的的手段都属于工程化。
一切重复的工作都应该被工程化。
image.png

创建项目:

  • 创建项目结构
  • 创建特定类型文件

编码:

  • 格式化代码
  • 校验代码风格
  • 编译/构建/打包

预览/测试:

  • web Server/Mock
  • Live Reloading/HMR
  • Source Map

提交:

  • Git Hooks
  • Lint-staged
  • 持续集成

部署:

  • CI/CD
  • 自动发布
工程化不是某个工具

image.png
工程化是我们项目整体的设计

工具是我们的设计实现所用的东西(webpack,Yeoman)

image.png
这几个脚手架,是属于比较成熟的工程化集成(比较固定,针对react,vue,ng)。

工程化的一切都要归功于node,它让前端行业进行了一次工业革命,我们后续说的工具几乎都是基于node开发的。

工程化主要做的就是:

  • 脚手架工具开发
  • 自动化构建系统
  • 模块化打包
  • 项目代码规范化
  • 自动化部署

之后学习的都是通用的脚手架工具

Yeoman 脚手架基本使用

Yeoman是什么

安装:

yarn global add yo

安装对应的generator(生成器 例如使用node 生成node的项目结构)

yarn global add generator-node

通过yo运行generator

mkdir my-module

yo node

填写相关信息

使用步骤:

  • 明确需求
  • 找到合适的generatorhttps://yeoman.io/generators/
  • 全局范围安装找到的generator
  • 通过yo运行对应的generator
  • 通过命令行交互填写选项
  • 生成你所需要的项目结构

基于Yeoman搭建自己的脚手架(自定义自己的Generator)
Generator本质上就是一个npm模块
image.png

首先创建对应(generator-sample <name> 命名规则) 文件夹然后使用:

yarn init 

初始化 package.json文件

yarn add yeoman-generator

这个模块提供生成器基类,基类提供了一些工具函数,让我们在创建生成器时更加便捷。

然后根据yeoman目录规范生成generators文件夹,以及默认生成器
image.png
image.png
然后通过yarn link(npm link )方式 链接到全局范围,使之成为全局模块包

然后创建一个文件夹,然后我们yo sample,sample就是我们刚刚创建好的生成器的名字
image.png

执行完之后我们就可以看到 执行完生成的那个文件。

那很多时候我们需要创建的文件有很多,文件内容也相对复杂,那这会我们就可以使用模板去创建文件。

这会我们去创建一个templates目录,然后创建一个bar.html当模板文件。
image.png
image.png
这是一个模板文件
内部可以使用 EJS 模板标记输出数据
例如:<%= title %>
其他的 EJS 语法也支持
<% if (success) { %>
哈哈哈
<% }%>

然后在导出类里添加prompting方法,Yoman在询问用户环节会自动调用此方法。

  prompting () {
    // Yeoman 在询问用户环节会自动调用此方法
    // 在此方法中可以调用父类的 prompt() 方法发出对用户的命令行询问
    return this.prompt([
      {
        type: 'input',
        name: 'name',
        message: 'Your project name',
        default: this.appname // appname 为项目生成目录文件夹名称
      }
    ])
    .then(answers => {
      // answers => { name: 'user input value' }
      this.answers = answers
    })
  }

prompt调用发出询问相关信息(具体类型可以看下文档),返回一个promise对象,会把用户输入信息返回,然后存起来方便后续当模板数据上下文使用。
然后在writing(yeoman自动在生成文件阶段调用)方法添加 模板文件路径(通过Generator父类templatePath方法获取),输出目标路径(就是命令行路径父类destinationPath),最后使用(父类里的fs.copyTpl),把文件从一个目录复制到另一个目录,一般是从template目录复制到你所指定的项目目录,不过会事先经过模板引擎的处理,一般用来根据用户输入(保存的模板上下文数据)处理加工文件。api可以直接查看文档。

  writing () {
    // Yeoman 自动在生成文件阶段调用此方法
    // 模板文件路径
    const tmpl = this.templatePath('bar.html')
    // 输出目标路径
    const output = this.destinationPath('bar.html')
    // 模板数据上下文
    const context = this.answers

    this.fs.copyTpl(tmpl, output, context)
  }

image.png

然后yarn link 到全局,
然后找到一个文件夹和上面一样 yo sample,然后就会发现我们根据模板创建出来的文件了。

vue-generator

然后下面我们就按之前的流程,创建一个带有基础代码的vue.js项目脚手架(除了vue-cli创建自带的基础代码外,我们可以添加一些我们自己想要的模板文件)
首先使用vue-cli创建一个原始项目结构
image.png

然后把我们需要使用的文件和文件夹,copy到我们生成器的模板文件夹下:
image.png
然后去Generator 的核心入口generators/app/index修改下生成文件阶段的方法,(询问用户环节因为是做测试就不修改了,需要的修改的,可以根据自己需求修改),我们添加一个test.txt 自己文件进去测试。
image.png

 writing () {
    // 把每一个文件都通过模板转换到目标路径

    const templates = [
      '.browserslistrc',
      '.editorconfig',
      '.env.development',
      '.env.production',
      '.eslintrc.js',
      '.gitignore',
      'babel.config.js',
      'package.json',
      'postcss.config.js',
      'README.md',
      'public/favicon.ico',
      'public/index.html',
      'src/App.vue',
      'src/main.js',
      'src/router.js',
      'src/assets/logo.png',
      'src/components/HelloWorld.vue',
      'src/store/actions.js',
      'src/store/getters.js',
      'src/store/index.js',
      'src/store/mutations.js',
      'src/store/state.js',
      'src/utils/request.js',
      'src/views/About.vue',
      'src/views/Home.vue',
      'test.txt'
    ]

    templates.forEach(item => {
      // item => 每个文件路径
      this.fs.copyTpl(
        this.templatePath(item),
        this.destinationPath(item),
        this.answers
      )
    })
  }

然后我们vue-cli工程里的package.json和 模板html文件做下,从用户输入取上下文的操作如图:
image.png
image.png
最后如下:
image.png
然后我们就靠新做的 vue-generator生成了,符合属于我们自己的项目工程。
也可参考这篇文章yeoman

然后就是发布成一个npm模块
放到git仓库然后 yarn publish输入版本信息上传成为一个npm包

介绍一个小plop脚手架,自动化创建同类型项目文件。
首先安装plop依赖,然后
image.png
创建plopfile.js入口,然后编写生成器

module.exports = plop => {
  plop.setGenerator('component', {
    description: 'application component',
    prompts: [
      {
        type: 'input',
        name: 'name',
        message: 'component name'
      }
    ],
    actions: [
      {
        type: 'add',
        path: 'src/components/{{name}}/{{name}}.js',
        templateFile: 'plop-templates/component.js.hbs'
      },
      {
        type: 'add',
        path: 'src/components/{{name}}/{{name}}.css',
        templateFile: 'plop-templates/component.css.hbs'
      },
      {
        type: 'add',
        path: 'src/components/{{name}}/{{name}}.test.js',
        templateFile: 'plop-templates/component.test.js.hbs'
      }
    ]
  })
}

里面添加描述,询问,然后生成添加文件type,和要生成的目录位置(path),以及模板文件的位置(templateFile)

这是模板文件的样子js举例
image.png
image.png

最后 yarn plop component执行我们设置的生成器,就会根据我们设置的模板生成对应的文件。

然后我们看下脚手架的实现原理,首先yarn init创建package.json配置文件,然后我们依赖了ejs,inquirer下载。

#!/usr/bin/env node

// Node CLI 应用入口文件必须要有这样的文件头
// 如果是 Linux 或者 macOS 系统下还需要修改此文件的读写权限为 755
// 具体就是通过 chmod 755 cli.js 实现修改

// 脚手架的工作过程:
// 1. 通过命令行交互询问用户问题
// 2. 根据用户回答的结果生成文件

const fs = require('fs')
const path = require('path')
const inquirer = require('inquirer')
const ejs = require('ejs')

inquirer.prompt([
  {
    type: 'input',
    name: 'name',
    message: 'Project name?'
  }
])
.then(anwsers => {
  // console.log(anwsers)
  // 根据用户回答的结果生成文件

  // 模板目录
  const tmplDir = path.join(__dirname, 'templates')
  // 目标目录
  const destDir = process.cwd()

  // 将模板下的文件全部转换到目标目录
  fs.readdir(tmplDir, (err, files) => {
    if (err) throw err
    files.forEach(file => {
      // 通过模板引擎渲染文件
      ejs.renderFile(path.join(tmplDir, file), anwsers, (err, result) => {
        if (err) throw err

        // 将结果写入目标文件路径
        fs.writeFileSync(path.join(destDir, file), result)
      })
    })
  })
})

然后根据我们设置的模板目录,在里面建上文件(模板采用ejs语法)
image.png

然后package.json设置bin属性脚手架入口文件指定

{
  "name": "sample-scaffolding",
  "version": "0.1.0",
  "main": "index.js",
  "bin": "cli.js",
  "author": "zce <w@zce.me> (https://zce.me)",
  "license": "MIT",
  "dependencies": {
    "ejs": "^2.6.2",
    "inquirer": "^7.0.0"
  }
}

然后一个最简易版就出来了。

自动化构建

把开发阶段代码转换成生产阶段代码。
image.png
image.png
类似这种的,还有取出空格,压缩文件。。。等。
常用的构建工具有
image.png

npm scripts可以解决一些简单的问题,但是复杂配置的还是需要译者工具来帮我们解决。
grunt基于临时文件构建,速度会慢一点(每一步都会有磁盘读写操作,编译完成过后结果写入临时文件,然后下一个插件再去读取这个文件,处理次数,就会越慢,这个主要做下了解)。
grunt

gulp解决了grunt的问题,基于内存,相对磁盘读写快了很多,支持同时执行多个任务。(更受欢迎)

重点了解下gulp
首先创建package.json,然后创建gulpfile.js入口文件,进行入口编写

// // 导出的函数都会作为 gulp 任务
// exports.foo = () => {
//   console.log('foo task working~')
// }

// gulp 的任务函数都是异步的
// 可以通过调用回调函数标识任务完成
exports.foo = done => {
  console.log('foo task working~')
  done() // 标识任务执行完成
}
// default 是默认任务
// 在运行是可以省略任务名参数
exports.default = done => {
  console.log('default task working~')
  done()
}

// v4.0 之前需要通过 gulp.task() 方法注册任务
const gulp = require('gulp')

gulp.task('bar', done => {
  console.log('bar task working~')
  done()
})

这是最基本的,然后多个任务按照顺序执行以及同时执行

const { series, parallel } = require('gulp')

const task1 = done => {
  setTimeout(() => {
    console.log('task1 working~')
    done()
  }, 1000)
}

const task2 = done => {
  setTimeout(() => {
    console.log('task2 working~')
    done()
  }, 1000)  
}

const task3 = done => {
  setTimeout(() => {
    console.log('task3 working~')
    done()
  }, 1000)  
}

// 让多个任务按照顺序依次执行
exports.foo = series(task1, task2, task3)

// 让多个任务同时执行
exports.bar = parallel(task1, task2, task3)

gulp也是支持异步的特性的,包括promise
然后我们用node.js实现一下读取转换写入过程

const fs = require('fs')
const { Transform } = require('stream')

exports.default = () => {
  // 文件读取流
  const readStream = fs.createReadStream('normalize.css')

  // 文件写入流
  const writeStream = fs.createWriteStream('normalize.min.css')

  // 文件转换流
  const transformStream = new Transform({
    // 核心转换过程
    transform: (chunk, encoding, callback) => {
      const input = chunk.toString()
      const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '')
      callback(null, output)
    }
  })

  return readStream
    .pipe(transformStream) // 转换
    .pipe(writeStream) // 写入
}
//最后返回的流默认相当于监听了一个end事件

// exports.stream = done => {
//   const read = fs.createReadStream('yarn.lock')
//   const write = fs.createWriteStream('a.txt')
//   read.pipe(write)
//   read.on('end', () => {
//     done()
//   })
// }

然后gulp自己读取转换写入操作

const { src, dest } = require('gulp')
const cleanCSS = require('gulp-clean-css')
const rename = require('gulp-rename')

exports.default = () => {
  return src('src/*.css')
    .pipe(cleanCSS())
    .pipe(rename({ extname: '.min.css' }))
    .pipe(dest('dist'))
}

然后了解这些,我们要实现的就是,对外暴露的就是三个任务:build,develop,clean,打包,开发,和清除

然后我们捋一下流程:

  • build流程

1.先执行clean清除任务
2.然后并行(style,script,page模板引擎,这三个任务通过并行parallel函数合成一个compile,然后通过series函数它执行完成后,执行一个压缩这三个代码的操作useref)以及image,font文件,extra(公共文件)的编译输出,转换为浏览器可以识别的格式。

  • develop流程

首先执行compile编译style,script,page的输出让浏览器可以认识它们,然后我们创建一个serve任务,启动一个服务去做下文件的访问映射以及热加载更新,这样就完成了开发环境。

  • clean清空

然后我们借助插件尝试对一个项目进行打包:
image.png
image.png

const { src, dest, parallel, series, watch } = require('gulp')
//导出读取src,写入dest,parallel并行,series顺序,watcch监听函数

const del = require('del')//导入清空库的api
const browserSync = require('browser-sync')//导入一个 前端服务工具,用于监听文件热更新

const loadPlugins = require('gulp-load-plugins')//导入一个默认可以获取所有gulp插件的库

const plugins = loadPlugins()//获取所有的gulp插件
const bs = browserSync.create()//创建一个未命名的Browsersync实例

const data = {//html模板文件用到的上下文数据
  menus: [
    {
      name: 'Home',
      icon: 'aperture',
      link: 'index.html'
    },
    {
      name: 'Features',
      link: 'features.html'
    },
    {
      name: 'About',
      link: 'about.html'
    },
    {
      name: 'Contact',
      link: '#',
      children: [
        {
          name: 'Twitter',
          link: 'https://twitter.com/w_zce'
        },
        {
          name: 'About',
          link: 'https://weibo.com/zceme'
        },
        {
          name: 'divider'
        },
        {
          name: 'About',
          link: 'https://github.com/zce'
        }
      ]
    }
  ],
  pkg: require('./package.json'),
  date: new Date()
}

const clean = () => {  //gulp的清空文件夹的任务
  return del(['dist', 'temp'])
}

const style = () => {//gulp编译 scss文件为css
  return src('src/assets/styles/*.scss', { base: 'src' })//读取设置路径下所有的scss文件,设置base路径,从src下查找
    .pipe(plugins.sass({ outputStyle: 'expanded' }))//放入sass转换流中 使用expanded展开输出,转换为css
    .pipe(dest('temp'))//放入dest的写入流到temp文件夹下
    .pipe(bs.reload({ stream: true }))//然后放到更新流中,进行文件更新(或者通过Browsersync实例里init的options的files属性,文件目录监听进行更新)
}

const script = () => {//gulp编译 es6+语法
  return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))//通过babel编译所有最新特性
    .pipe(dest('temp'))//和style一样
    .pipe(bs.reload({ stream: true }))//和style一样
}

const page = () => {//gulp编译html模板文件
  return src('src/*.html', { base: 'src' })
    .pipe(plugins.swig({ data, defaults: { cache: false } })) //,放入模板使用上下文数据以及 防止模板缓存导致页面不能及时更新
    .pipe(dest('temp'))
    .pipe(bs.reload({ stream: true }))
}

const image = () => {//压缩图片
  return src('src/assets/images/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}

const font = () => {//压缩字体文件
  return src('src/assets/fonts/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}

const extra = () => {//把public中的文件拿过来,放到dist中
  return src('public/**', { base: 'public' })
    .pipe(dest('dist'))
}

const serve = () => {//gulp启动一个前端服务的任务
  watch('src/assets/styles/*.scss', style)//监听scss文件,发生变化时执行的方法
  watch('src/assets/scripts/*.js', script)//监听js文件,发生变化时执行的方法
  watch('src/*.html', page)//监听html文件,发生变化时执行的方法
  // watch('src/assets/images/**', image)
  // watch('src/assets/fonts/**', font)
  // watch('public/**', extra)
  watch([
    'src/assets/images/**',
    'src/assets/fonts/**',
    'public/**'
  ], bs.reload) //监听所有的资源文件,假如资源文件进行添加删除的化,进行更新

  bs.init({//启动Browsersync服务
    notify: false,//不显示在浏览器中的任何通知。
    port: 2080,//端口
    // open: false,//是否打开浏览器
    // files: 'dist/**',//监听文件下所有文件变化
    server: {
      baseDir: ['temp', 'src', 'public'],//多个基目录,服务查找文件 顺序, 先从temp下查找,找不到的话,从src下,再找不到从public下查找
      routes: {
        '/node_modules': 'node_modules' //匹配html中/node_modules路径,匹配到时,更改为相对于当前的工作目录,
        //当前项目工程的node_modules中查找
      }
    }
  })
}

const useref = () => {//合并html中部分(js,css为一个)文件的任务,html中有js,和css文件bulid的标识,根据标识合并然后通过if压缩
  return src('temp/*.html', { base: 'temp' })//读取html
    .pipe(plugins.useref({ searchPath: ['temp', '.'] }))//根据html中标识合并,指定相对于当前工作目录搜索资产文件的位置。可以是字符串或字符串数​​组。
    // html js css
    .pipe(plugins.if(/\.js$/, plugins.uglify()))//合完成后,如果,压缩js
    .pipe(plugins.if(/\.css$/, plugins.cleanCss()))//完成后如果是css,压缩
    .pipe(plugins.if(/\.html$/, plugins.htmlmin({//如果有引入html压缩
      collapseWhitespace: true,//压缩html
      minifyCSS: true,//压缩css
      minifyJS: true//压缩js
    })))//
    .pipe(dest('dist'))
}

const compile = parallel(style, script, page)//这个几个开发阶段和上线阶段都需要的任务并行执行

// 上线之前执行的任务
const build =  series(//顺序执行清除,然后并行执行后四个
  clean,
  parallel(
    series(compile, useref),//先执行style, script, page,进行编译,然后在合并压缩
    image,
    font,
    extra
  )
)//最后生成dist

const develop = series(compile, serve)//开发阶段只需要执行这三个的编译,和启动服务就够了
//资源文件的压缩最后上线压缩就够了,开发阶段压缩会影响效率

module.exports = {//最后导出gulp任务
  clean,
  build,
  develop
}

最后把他封装成npm scripts
在packages.json中添加

  "scripts": {
    "clean": "gulp clean",
    "build": "gulp build",
    "develop": "gulp develop"
  }

然后我们把他做成一个node cli脚手架封装工作流发布
思路是:

  • 首先添加脚手架相关信息,添加入口文件(bin/zce-pages.js),添加package.json入口配置(我们主要做的是在执行zce-page命令时它的入口文件执行时使用gulp命令 并且指定一些目录参数,最后命令就相当于 gulp --cwd ...<设置工作目录> --gulpfile ...<设置 gulpfile文件路径> <clean|build|develop>)
  • 其次把gulpfile.js配置包在脚手架里,创建(lib/index.js)放入,然后修改package.json里的main入口
  • 然后修改package.json里的files,为["lib","bin"]在发布npm包的时候把这俩文件都放上去。

首先添加image.png
bin一般是放脚手架文件我们放了zce-pages.js
然后我们查看gulp命令实际调用了什么
根据我们查看node_modules/bin/gulp.cmd文件
image.png
发现调用的是脚手架里gulp,然后我们看到
image.png
gulp.js也是调用了脚手架的执行
我们在自己的脚手架文件里,直接引入gulp里的模块,或者引入脚手架,就会让gulp工作
然后我们

#!/usr/bin/env node

process.argv.push('--cwd')//工作目录
process.argv.push(process.cwd())
////做成脚手架的话,命令行通过bin下的cmd 执行  也就是当前工程bin下
//当前目录也就是zce-pages/bin/...这里
process.argv.push('--gulpfile')//gulpfile路径
process.argv.push(require.resolve('..'))
//或者写相对路径require.resolve('../lib/index.js'),不写的话默认..找到上级,然后会默认找package.json里的main 路径做执行

require('gulp/bin/gulp')

这样执行我们这个脚手架,就会让gulp工作了,然后对应写gulp的命令行参数。
然后package.json是这个样子

{
  "name": "zce-pages",
  "version": "0.2.0",
  "description": "static web app workflow",
  "keywords": [
    "zce-pages",
    "zce"
  ],
  "homepage": "https://github.com/zce/zce-pages#readme",
  "bugs": {
    "url": "https://github.com/zce/zce-pages/issues"
  },
  "license": "MIT",
  "author": "zce <w@zce.me> (https://zce.me)",
  "files": [
    "lib",
    "bin"
  ],
  "main": "lib/index.js",
  "bin": "bin/zce-pages.js",
  "directories": {
    "lib": "lib"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/zce/zce-pages.git"
  },
  "scripts": {
    "lint": "standard --fix"
  },
  "dependencies": {
    "@babel/core": "^7.5.5",
    "@babel/preset-env": "^7.5.5",
    "browser-sync": "^2.26.7",
    "del": "^5.1.0",
    "gulp": "^4.0.2",
    "gulp-babel": "^8.0.0",
    "gulp-clean-css": "^4.2.0",
    "gulp-htmlmin": "^5.0.1",
    "gulp-if": "^3.0.0",
    "gulp-imagemin": "^6.1.0",
    "gulp-load-plugins": "^2.0.1",
    "gulp-sass": "^4.0.2",
    "gulp-swig": "^0.9.1",
    "gulp-uglify": "^3.0.2",
    "gulp-useref": "^3.1.6"
  },
  "devDependencies": {
    "standard": "^13.1.0"
  },
  "engines": {
    "node": ">=6"
  }
}

然后我们根据我们上面写的把gulpfile.js的配置更改下,改成可配置的方式放到lib下的index.js里

const { src, dest, parallel, series, watch } = require('gulp')

const del = require('del')
const browserSync = require('browser-sync')

const loadPlugins = require('gulp-load-plugins')

const plugins = loadPlugins()
const bs = browserSync.create()
const cwd = process.cwd()
let config = {
  // default config
  build: {
    src: 'src',
    dist: 'dist',
    temp: 'temp',
    public: 'public',
    paths: {
      styles: 'assets/styles/*.scss',
      scripts: 'assets/scripts/*.js',
      pages: '*.html',
      images: 'assets/images/**',
      fonts: 'assets/fonts/**'
    }
  }
}

try {
  const loadConfig = require(`${cwd}/pages.config.js`)
  config = Object.assign({}, config, loadConfig)
} catch (e) {}

const clean = () => {
  return del([config.build.dist, config.build.temp])
}

const style = () => {
  return src(config.build.paths.styles, { base: config.build.src, cwd: config.build.src })
    .pipe(plugins.sass({ outputStyle: 'expanded' }))
    .pipe(dest(config.build.temp))
    .pipe(bs.reload({ stream: true }))
}

const script = () => {
  return src(config.build.paths.scripts, { base: config.build.src, cwd: config.build.src })
    .pipe(plugins.babel({ presets: [require('@babel/preset-env')] }))
    .pipe(dest(config.build.temp))
    .pipe(bs.reload({ stream: true }))
}

const page = () => {
  return src(config.build.paths.pages, { base: config.build.src, cwd: config.build.src })
    .pipe(plugins.swig({ data: config.data, defaults: { cache: false } }))
    .pipe(dest(config.build.temp))
    .pipe(bs.reload({ stream: true }))
}

const image = () => {
  return src(config.build.paths.images, { base: config.build.src, cwd: config.build.src })
    .pipe(plugins.imagemin())
    .pipe(dest(config.build.dist))
}

const font = () => {
  return src(config.build.paths.fonts, { base: config.build.src, cwd: config.build.src })
    .pipe(plugins.imagemin())
    .pipe(dest(config.build.dist))
}

const extra = () => {
  return src('**', { base: config.build.public, cwd: config.build.public })
    .pipe(dest(config.build.dist))
}

const serve = () => {
  watch(config.build.paths.styles, { cwd: config.build.src }, style)
  watch(config.build.paths.scripts, { cwd: config.build.src }, script)
  watch(config.build.paths.pages, { cwd: config.build.src }, page)
  // watch('src/assets/images/**', image)
  // watch('src/assets/fonts/**', font)
  // watch('public/**', extra)
  watch([
    config.build.paths.images,
    config.build.paths.fonts
  ], { cwd: config.build.src }, bs.reload)

  watch('**', { cwd: config.build.public }, bs.reload)

  bs.init({
    notify: false,
    port: 2080,
    // open: false,
    // files: 'dist/**',
    server: {
      baseDir: [config.build.temp, config.build.dist, config.build.public],
      routes: {
        '/node_modules': 'node_modules'
      }
    }
  })
}

const useref = () => {
  return src(config.build.paths.pages, { base: config.build.temp, cwd: config.build.temp })
    .pipe(plugins.useref({ searchPath: [config.build.temp, '.'] }))
    // html js css
    .pipe(plugins.if(/\.js$/, plugins.uglify()))
    .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
    .pipe(plugins.if(/\.html$/, plugins.htmlmin({
      collapseWhitespace: true,
      minifyCSS: true,
      minifyJS: true
    })))
    .pipe(dest(config.build.dist))
}

const compile = parallel(style, script, page)

// 上线之前执行的任务
const build =  series(
  clean,
  parallel(
    series(compile, useref),
    image,
    font,
    extra
  )
)

const develop = series(compile, serve)

module.exports = {
  clean,
  build,
  develop
}

然后里面有默认的配置,const loadConfig = require(${cwd}/pages.config.js) 获取执行命令目录下的配置文件做覆盖。

然后现在我们的一个脚手架就完成了,然后就可以把他发布使用了,后续有需要我们直接迭代这个包就可以了。(自己测试可以把他link到全局)
然后我们项目里下载了这个脚手架就可以通过一下scripts里的命令调用了
image.png

//作为npm包发布时依赖项必须放在dependencies不然不会自动下载
我的lp-workflow-cli 已发布欢迎使用
FIS 捆绑套餐,比较典型需求尽可能集成在内部。(大而全)
该文章内容摘抄于拉钩大前端训练营

查看原文

赞 0 收藏 0 评论 0

Charon 发布了文章 · 11月8日

javascript垃圾回收及性能优化(如何调优测试 1-2-3)

内存管理

内存为什么需要管理

function fn(){
    arrlist=[];
    arrlist[100000]='lg is a coder';
}
fn();

image.png

不当的操作会导致堆内存,飙升。
内存管理介绍:

  1. 内存:可读写单元组成,表示一片可操作空间
  2. 管理:人为的去操作一片空间的申请,使用和释放
  3. 内存管理:开发者主动申请空间,使用空间,释放空间
  4. 管理流程:申请-使用-释放

javascript中的垃圾回收

  1. javascript中内存管理是自动的
  2. 对象不再引用时是垃圾
  3. 对象不能从根上访问到时是垃圾
javascript中的可达对象
  • 可以访问到的对象就是可达对象(引用,作用域链)
  • 可达的标准就是从根触发是否能够被找到
  • javascript中的根就可以理解为是全局变量对象
javascript中的引用和可达

image.png
obj假如是某个函数返回的
里面的o1和o2互相引用

通过作用域链引用可以找到的可达对象。

image.png
如果把对o1的相关引用去掉,他就无法被找到,就变成了一个不可达对象,垃圾。
image.png

GC算法介绍

GC的定义与作用
  • GC就是垃圾回收机制的简写
  • GC可以找到内存中的垃圾,并释放和回收空间
GC里的垃圾是什么
  • 程序中不再需要使用的对象
  • 程序中不能再访问到的对象(不可达对象)
GC算法是什么

GC是一种机制,垃圾回收器完成具体的工作
工作内容就是查找垃圾释放空间,回收空间
算法就是工作时查找和回收所遵循的规则

常见GC算法
  • 引用计数
  • 标记清除
  • 标记整理
  • 分代回收

引用计数算法实现原理

  • 核心思想:设置引用数,判断当前引用数是否为0
  • 引用计数器
  • 引用关系改变时修改引用数字
  • 引用数字为0时立即回收
引用计数算法优缺点

优点

  • 发现垃圾时立即回收(实时监控引用数,为0立即回收)
  • 最大程度减少程序卡顿(实时监控,实时回收释放内存,某种意义上保证内存不会被占满)

缺点

  • 无法回收循环引用的对象(计数一直不为0)
  • 时间开销大,资源消耗较大(多维护了计数器,时刻维护计数器,维护计数器的过程需要时间)
标记清除算法实现原理
  • 核心思想:分标记和清除二个阶段
  • 遍历所有对象标记活动对象
  • 遍历所有对象清除没有标记对象
  • 回收相应空间

例如
image.png
内存里有这些对象,GC工作时会递归找到可达对象,图中a-d,c-e,进行标记
image.png
然后遍历所有清除不可达对象,(a1和b1找不到属于不可达对象,未被标记),这个a1和b1引用计数就回收不了因为引用一直存在,循环引用。

标记清除的优缺点

优点:

  • 最大优点可以清除循环引用(上例中就可以清除,因为是不可达对象,未被标记)

缺点:

  • 不会立即回收垃圾对象,不像引用计数实时监控,第一次遍历对象发现不可达对象,也无法进行清除,只能是先标记,第二次再清除,回收。
  • 会造成空间碎片化,如图:

image.png

不可达对象释放到空闲链表里的时候,因为有中间可达对象并没有被释放,所以他们是不连续的[域1,域2,...,域1],这样会造成空间碎片化,假设说每个域为1, 在我们需要2.5个内存时,这片空间就无法满足了。

标记整理算法实现原理
  • 标记整理可以看作是标记清除的增强
  • 标记阶段的操作和标记清除一致
  • 清除阶段会先执行整理,移动对象位置

image.png
image.png
image.png

优点

  • 减少碎片化空间

缺点

  • 不会立即回收垃圾对象
  • 移动对象位置,回收效率慢

认识V8

  • v8是一款主流jsvascript执行引擎
  • v8采用即时编译
  • v8内存设限(64位不超1.5G,32位不超800M,因为1本身是针对浏览器的,现有大小对网页应用来说足够了,2,内部垃圾回收机制也决定了采用这设置是非常合理的,管方做过测试,当垃圾内存达到1.5G时,采用增量标记的算法回收,只需要消耗50ms,非增量标记回收1秒钟,从用户体验来说1秒已经算是很长了,所以就以1.5G为界限了(变大消耗时间会变长)。)
v8垃圾回收策略
  • 采用分代回收的思想
  • 内存分为新生代,老生代
  • 针对不同对象采用不同的算法

image.png

v8常用GC算法
  • 分代回收
  • 空间复制
  • 标记清除
  • 标记整理
  • 标记增量
v8如何回收新生代对象

image.png

  • v8内存空间一分为二(新生代/老生代)
  • 小空间用于储存新生代对象(62位32M/32位16M)
  • 新生代指的是存活时间较短的对象
新生代对象回收实现

回收过程采用复制算法+标记整理

  1. 新生代内存区分为两个等大小空间
  2. 使用空间From,空闲空间为To
  3. 活动对象储存于From空间中
  4. 触发回收操作时,先标记整理From空间将活动对象和不可达对象都进行整理,把不可达对象整理成连续的(空间规整)),然后将活动对象复制拷贝至To。
  5. 然后From与To交换空间,完成释放(这回原来的From就成了To,原来的T哦就成了From)

回收细节说明:

  1. 拷贝过程中可能出现晋升
  2. 晋升就是将新生代对象移至老生代
  3. 一轮GC还存活的新生代需要晋升
  4. To空间的使用率超过25%晋升(防止交换空间后新生代内存不够使用)
老生代对象回收
老年代对象说明
  • 老年代对象存放在右侧老生代区域
  • 64位操作系统1.4G,32操作系统700M
  • 老年代对象就是指存货时间较长的对象
老年代对象回收实现
  • 主要采取标记清除,标记整理,增量标记算法
  • 正常使用标记清除完成垃圾空间的回收(主要使用标记清除,虽然会有一些碎片,但是相对来说速度很明显提升)
  • 采用标记整理进行空间优化(使用时机,新生代对象往老生代区域移动时且这个时间节点上,老生代区域空间又不足以来存放新生代移过来的对象,这时就会触发标记整理,把之前的碎片化空间整理来存放)
  • 采用标记增量进行效率优化。

image.png
标记增量如图,会分段执行标记回收工作,让使用者尽量感受不到程序的暂停,即使时最后的清除阶段也不会让使用者感觉到卡顿。

细节对比
  • 新生代区域垃圾回收使用空间换时间
  • 老生代区域垃圾回收不适合复制算法(因为本来空间就比较大,里面存放的对象也多,划分俩空间的话首先是浪费,其次因为对象多,复制操作得不偿失。)

代码调试(性能检测)

Performance工具介绍

使用Performance时刻监控内存
内存问题的体现

  • 页面出现延迟加载或经常性暂停
  • 页面持续性出现糟糕的性能
  • 页面性能随时间越长越来越差

界定内存问题的标准

  • 内存泄漏:内存使用持续升高,不见释放
  • 内存膨胀:在多数设备都存在性能问题。(指需要的内存,硬件设备无法提供)
  • 频繁垃圾回收:通过内存变化图进行分析
监控内存的几种方式
浏览器任务管理器(shift+esc)右击调出jsvascript所占内存image.png

第一个内存是dom节点内存,持续增长证明在创建节点,后面javascript内存是可达对象内存,持续增长证明我们在创建对象,如果一直增长不见下降,那就是有内存泄漏发生,如果频繁 飙高降低,证明在频繁GC

Timeline时序图记录

image.png
测试一下,我使用的是Edge,打开f12性能模块,谷歌里是Performance差异是中英文差异,然后点击录制
image.png
然后进行对界面操作,之后点击停止,会有你这一段时间操作过程的内存以及浏览器重绘的过程记录,在这里包括也能刷新看到界面一个加载过程,可以看到那里导致加载慢
image.png
频繁点击按钮,这里就不断创建大的字符串放到数组里,可以看到内存的升高,降低是因为浏览器的GC,如果时序图这里内存一直升高不释放,证明有内存泄漏了(可以找到对应时间节点查看),如果一直频繁飙高降低,这也是不正常的,证明在频繁的GC。(原始类型是放在栈上的,引用类型是放在堆上的)

什么是分离DOM
  • 垃圾对象时的dom节点(从dom上脱离,js里也没有引用时的dom)
  • 分离状态的dom(从dom树上脱离,但是js里有引用不释放,内存泄漏了)
堆快照查找分离dom

用代码演示下
image.png
这样界面没有,但是js引用有,这就是分离dom,我们看下如何查找这种dom。
image.png
找到Edge内存模块
image.png
先直接获取快照,方便对比 搜索detached 发现没有分离dom
然后点击按钮 再从用户配置获取新的快照,搜索deta,发现我们创建的分离dom
image.png
这里就可以看到分离的dom和一些它的相关信息,如果想要释放的话tepEle手动赋值null就可以了

判断是否存在频繁垃圾回收
  • GC工作时程序是停止的,
  • 频繁过长的gc会导致应用假死
  • 用户使用感知卡顿

频繁GC判断
timeline中频繁上升下降
浏览器任务管理中数据频繁增大或减小

https://jsbench.me/
JSBench 用来测试代码执行速度

image.png
放入对比代码然后run 可以查看执行效率
也可以放入一些前置dom或者js代码。(测试所需)

经过一些测试有一些运行较快的总结如下:
避免全局变量,全局变量特点:
**挂载在window下
至少一个引用数
存活更久,但持续占用内存**

避免全局查找
全局查找相关:

  • 目标变量不存在当前作用内,通过作用域链向上查找
  • 减少全局查找降低时间消耗
  • 减少不必要全局变量定义
  • 全局变量数据局部化(必须要全局的话,可以缓存全局)

避免循环引用(全局引用指多个对象间存在互相引用)

采用字面量代替new操作

setTimeout替换setInterval

采用事件委托

合并循环变量和条件

在原型对象上新增实例对象需要方法

不为了闭包而闭包。

本文内容摘抄自 拉钩大前端训练营

查看原文

赞 0 收藏 0 评论 0

Charon 发布了文章 · 11月8日

TypeScript(1-2-2)

typescript是jiavascript的超集这里记录一些简单的用法,如深入层次的请查看官网。
typescript官网

typescript是属于渐进式的,如果对有的语法不太熟,也没关系,我们可以完全按照javascript的语法编写代码。

原始数据类型


const a: string = 'foobar'

const b: number = 100 // NaN Infinity

const c: boolean = true // false

// 在非严格模式(strictNullChecks)下,
// string, number, boolean 都可以为空
// const d: string = null
// const d: number = null
// const d: boolean = null

const e: void = undefined

const f: null = null

const g: undefined = undefined

// Symbol 是 ES2015 标准中定义的成员,
// 使用它的前提是必须确保有对应的 ES2015 标准库引用
// 也就是 tsconfig.json 中的 lib 选项必须包含 ES2015
const h: symbol = Symbol()

// Promise

// const error: string = 100

作用域问题:默认文件中的成员会作为全局成员,多个文件中有相同成员就会出现冲突。

解决办法1: IIFE 提供独立作用域
(function () {
const a = 123
})()
解决办法2: 在当前文件使用 export,也就是把当前文件变成一个模块
模块有单独的作用域
const a = 123

export {}

后面记录都是默认模块导出,有模块作用域。

Object 类型

object 类型是指除了原始类型以外的其它类型
const foo: object = function () {} // [] // {}

如果需要明确限制对象类型,则应该使用这种类型对象字面量的语法,或者是「接口」

const obj: { foo: number, bar: string } = { foo: 123, bar: 'string' }

数组类型

数组类型的两种表示方式

const arr1: Array<number> = [1, 2, 3]

const arr2: number[] = [1, 2, 3]

案例:

// 如果是 JS,需要判断是不是每个成员都是数字
// 使用 TS,类型有保障,不用添加类型判断
function sum (...args: number[]) {
  return args.reduce((prev, current) => prev + current, 0)
}

sum(1, 2, 3) // => 6

元组

我们知道数组中元素的数据类型都一般是相同的(any[] 类型的数组可以不同),如果存储的元素数据类型不同,则需要使用元组。

元组中允许存储不同类型的元素,元组可以作为参数传递给函数。

const tuple: [number, string] = [18, 'zce']
const [age, name] = tuple

const entries: [string, number][] = Object.entries({
  foo: 123,
  bar: 456
})

const [key, value] = entries[0]

枚举(Enum)

标准的数字枚举

enum PostStatus {
  Draft = 0,
  Unpublished = 1,
  Published = 2
}

数字枚举,枚举值自动基于前一个值自增
enum PostStatus {
  Draft = 6,
  Unpublished, // => 7
  Published // => 8
}

字符串枚举
enum PostStatus {
  Draft = 'aaa',
  Unpublished = 'bbb',
  Published = 'ccc'
}
// 常量枚举,不会侵入编译结果
const enum PostStatus {
  Draft,
  Unpublished,
  Published
}
const post = {
  title: 'Hello TypeScript',
  content: 'TypeScript is a typed superset of JavaScript.',
  status: PostStatus.Draft // 3 // 1 // 0
}

函数类型

function func1 (a: number, b: number = 10, ...rest: number[]): string {
  return 'func1'
}

const func2: (a: number, b: number) => string = function (a: number, b: number): string {
  return 'func2'
}

任意类型(弱类型)

function stringify (value: any) {
  return JSON.stringify(value)
}
let foo: any = 'string'

any 类型是不安全的

接口

任意值对象在代码里

interface Post {
  title: string
  content: string
}

function printPost (post: Post) {
  console.log(post.title)
  console.log(post.content)
}

printPost({
  title: 'Hello TypeScript',
  content: 'A javascript superset'
})

interface Post {
  title: string
  content: string
  subtitle?: string//可选
  readonly summary: string//只读
}

const hello: Post = {
  title: 'Hello TypeScript',
  content: 'A javascript superset',
  summary: 'A javascript'
}

interface Cache {
  [prop: string]: string
}
//定义对象属性名是字符串,值也是字符串

const cache: Cache = {}

cache.foo = 'value1'
cache.bar = 'value2'

class

class Person {
  name: string // = 'init name'
  age: number
  
  constructor (name: string, age: number) {
    this.name = name
    this.age = age
  }

  sayHi (msg: string): void {
    console.log(`I am ${this.name}, ${msg}`)
  }
}


class Person {
  public name: string // = 'init name'
  private age: number //私有 当成员被标记为private时,它就不能在声明它的类的外部访问
  protected gender: boolean //protected和private类似,但是,protected成员在派生类中可以访问
  
  constructor (name: string, age: number) {
    this.name = name
    this.age = age
    this.gender = true
  }

  sayHi (msg: string): void {
    console.log(`I am ${this.name}, ${msg}`)
    console.log(this.age)
  }
}

class Student extends Person {
 // 只读成员
  protected readonly gender: boolean
  private constructor (name: string, age: number) {
    super(name, age)
    this.gender = true;//必须初始赋值
    console.log(this.gender)
  }

  static create (name: string, age: number) {
    return new Student(name, age)
  }
}

const tom = new Person('tom', 18)
console.log(tom.name)
// console.log(tom.age)
// console.log(tom.gender)

const jack = Student.create('jack', 18)

类与接口


interface Eat {
  eat (food: string): void
}

interface Run {
  run (distance: number): void
}

class Person implements Eat, Run {
  eat (food: string): void {
    console.log(`优雅的进餐: ${food}`)
  }

  run (distance: number) {
    console.log(`直立行走: ${distance}`)
  }
}

class Animal implements Eat, Run {
  eat (food: string): void {
    console.log(`呼噜呼噜的吃: ${food}`)
  }

  run (distance: number) {
    console.log(`爬行: ${distance}`)
  }
}

抽线类 只能被继承

abstract class Animal {
  eat (food: string): void {
    console.log(`呼噜呼噜的吃: ${food}`)
  }

  abstract run (distance: number): void
}

class Dog extends Animal {
  run(distance: number): void {
    console.log('四脚爬行', distance)
  }

}

const d = new Dog()
d.eat('嗯西马')
d.run(100)

泛型

function createNumberArray (length: number, value: number): number[] {
  const arr = Array<number>(length).fill(value)
  return arr
}

function createStringArray (length: number, value: string): string[] {
  const arr = Array<string>(length).fill(value)
  return arr
}

function createArray<T> (length: number, value: T): T[] {
  const arr = Array<T>(length).fill(value)
  return arr
}

// const res = createNumberArray(3, 100)
// res => [100, 100, 100]

const res = createArray<string>(3, 'foo')

有的库默认没有typescript支持,需要下载对应支持库。
如lodash,需要安装@types/lodash。

tsconfig.json文件配置详情查看:typescript官网配置说明

本文摘抄于拉钩大前端训练营

查看原文

赞 0 收藏 0 评论 0

Charon 发布了文章 · 11月7日

ECMAScript新特性(es6 )(1-2-1)

首先要区分语言和平台之间的关系,语言本身是指ECMAScript,平台是指浏览器或者node,在平时我们浏览器开发里js就是ECMAScript。
浏览器的组成部分
image.png
node.js的组成部分
image.png
在ES5.1之后版本我们统称为ES6
image.png
主要说明如下:
**• let 和 const
• Arrow functions
• Classes(类)
• Generators
• Map 和 Set
• for ... of 循环
• Symbol
• Modules
• Template literals(模板字⾯量)
• Default parameters(默认参数)
• Enhanced object literals(对象字⾯量增强)
• Destructuring assignments(解构分配)
• Spread operator(展开操作符)
• Proxy和Reflect**

这些新增API和对象主要是进行:
**• 对原有语法进⾏增强
• 解决原有语法上的⼀些问题或者缺陷
• 全新的对象、全新的⽅法、全新的功能
• 全新的数据类型和数据结构**

let和const

let 声明的成员只会在所声明的块中生效
if (true) {
let foo = 'zce'
console.log(foo)
}

let经典应用场景

var elements = [{}, {}, {}]
for (let i = 0; i < elements.length; i++) {
  elements[i].onclick = function () {
    console.log(i)
  }
}

因为产生自己的循环体的块级作用域,所以可以代替闭包。

我们看下面例子来解析一下运行
for (let i = 0; i < 3; i++) {
let i = 'foo'
console.log(i)
}

如下:
let i = 0

if (i < 3) {
let i = 'foo'
console.log(i)
}

i++

if (i < 3) {
let i = 'foo'
console.log(i)
}

i++

if (i < 3) {
let i = 'foo'
console.log(i)
}

i++
其实是这样的,所以我们也就可以解释在这其中for 循环会产生两层作用域。
let 还修复了变量声明提升现象。
const 整体和let类似,

  • 恒量声明过后不允许重新赋值
  • 恒量要求声明同时赋值

const name
name = 'zce' //会报错

  • 恒量只是要求内层指向不允许被修改
  • 对于对象数据成员的修改是没有问题的

Arrow functions

箭头函数的 简易写法,可以去看下阮一峰老师的es6,这里主要标明几个箭头函数的特征。

  • 箭头函数绑定父作用域的this
  • 箭头函数this不可修改
  • 箭头函数不能作为构造函数,不能使用new
  • 箭头函数没有arguments, caller, callee
  • 箭头函数没有原型属性//prototype 为undefined
  • 箭头函数在ES6 class中声明的方法为实例方法,不是原型方法
class Super{
    sayName =()=>{
        //do some thing here
    }
}

var a = new Super()
var b = new Super()
a.sayName === b.sayName //false

class

es6里实现class关键字,来代替es5里的构造函数的方式生成类。
es6的extends关键词来进行继承,底层是使用es5寄生组合式继承的封装。
使用extends来继承,constructor中使用this必须要先实例化父类,使用super关键词实例化,其他方法里也可通过super调用父类成员。

class里的constructor相当于es5的实例化,然后之外定义的属性函数,相当于定义在原型上(不包含箭头函数)

static关键词 静态属性,可以不实例化类可直接访问,里面的this指向类本身,不是实例化后的对象。

// extends 继承

class Person {
  constructor (name) {
    this.name = name
  }

  say () {
    console.log(`hi, my name is ${this.name}`)
  }
}

class Student extends Person {
  constructor (name, number) {
    super(name) // 父类构造函数
    this.number = number
  }

  hello () {
    super.say() // 调用父类成员
    console.log(`my school number is ${this.number}`)
  }
}

const s = new Student('jack', '100')
s.hello()

Generators

Generators函数调用会返回一个iterator对象,可以继续调用next()方法来依次返回Generators函数yield关键词之后的表达式结果,结果是个对象{value:'结果值',done:布尔值(是否结束true/false)},yield可以暂停当前函数,然后再通过外面iterator调用next方法继续往下走。

代码如下:

function * createIdMaker () {
  let id = 1
  while (true) {
    yield id++
  }
}

const idMaker = createIdMaker()

console.log(idMaker.next().value)
console.log(idMaker.next().value)
console.log(idMaker.next().value)
console.log(idMaker.next().value)

Map和Set

Map

Map对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。构造函数Map可以接受一个数组作为参数。

map和object区别

  • 一个Object 的键只能是字符串或者 Symbols,但一个Map 的键可以是任意值。
  • Map中的键值是有序的(FIFO 原则),而添加到对象中的键则不是。
  • Map的键值对个数可以从 size 属性获取,而 Object 的键值对个数只能手动计算。
  • Map支持for of 循环,obj需要自己添加Symbol.iterator

常用api

  • set(key, val): 向Map中添加新元素
  • get(key): 通过键值查找特定的数值并返回
  • has(key): 判断Map对象中是否有Key所对应的值,有返回true,否则返回false
  • delete(key): 通过键值从Map中移除对应的数据
  • clear(): 将这个Map中的所有元素删除
  • size:返回Map对象中所包含的键值对个数

遍历方法

  • keys():返回键名的遍历器
  • values():返回键值的遍历器
  • entries():返回键值对的遍历器
  • forEach():使用回调函数遍历每个成员

map支持 for of 遍历,内部的Symbol.iterator是entries

Set

Set对象允许你存储任何类型的值,无论是原始值或者是对象引用。它类似于数组,但是成员的值都是唯一的,没有重复的值。

Set 本身是一个构造函数,用来生成Set 数据结构。Set函数可以接受一个数组(或者具有 iterable 接口(迭代器)的其他数据结构)作为参数,用来初始化(带有Symbol.iterator)。
Set 对象存储的值总是唯一的,所以需要判断两个值是否恒等。有几个特殊值需要特殊对待:

  • +0 与 -0 在存储判断唯一性的时候是恒等的,所以不重复
  • undefined 与 undefined 是恒等的,所以不重复
  • NaN 与 NaN 是不恒等的,但是在 Set 中认为NaN与NaN相等,所有只能存在一个,不重复。

set常用方法

  • add(value):添加某个值,返回 Set 结构本身(可以链式调用)。
  • delete(value):删除某个值,删除成功返回true,否则返回false
  • has(value):返回一个布尔值,表示该值是否为Set的成员。
  • clear():清除所有成员,没有返回值。
  • size:返回Set实例的成员总数。

遍历方法

  • keys():返回键名的遍历器。
  • values():返回键值的遍历器。
  • entries():返回键值对的遍历器。
  • forEach():使用回调函数遍历每个成员。

由于Set结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。

for...of循环

for of 循环本意是统一 js中循环的标准,我们查看可以使用for of循环的对象,
image.png
解释:里面都有一个Symbol.iterator方法,这个of循环首先调用这个方法,它返回一个对象(然后就是循环对象),这个对象内部有next方法,方法返回一个对象{value:'值',done:布尔值//是否结束},每次循环就是调用这个next方法,等done为true的时候循环结束。
这是不是很像Generator函数,所以说Generator调用的结果也是可以用of循环的,比如map和set结构都有这个Symbol.iterator方法,,所以它也是可以循环的,但是对象是没有这个方法的,所以需要我们手动添加一下

const todos = {
  life: ['吃饭', '睡觉', '打豆豆'],
  learn: ['语文', '数学', '外语'],
  work: ['喝茶'],
  [Symbol.iterator]: function * () {
    const all = [...this.life, ...this.learn, ...this.work]
    for (const item of all) {
      yield item
    }
  }
}

for (const item of todos) {
  console.log(item)
}

因为我们看到 它和Generator函数很像,所以我们用它辅助实现一下,
给[Symbol.iterator]定义一个生成器函数,返回一个可迭代对象,然后我们使用 yield 函数来辅助实现,因为它返回的和我们要的那个对象是一样的。

Symbol

symbol 是一种基本数据类型 (primitive data type)。Symbol()函数会返回symbol类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的symbol注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:"new Symbol()"。

每个从Symbol()返回的symbol值都是唯一的。一个symbol值能作为对象属性的标识符;这是该数据类型仅有的目的。更进一步的解析见—— glossary entry for Symbol

使用场景:扩展对象,属性名冲突问题

一般使用第三方库扩展时,我们预防覆盖方法名,可以使用Symbol对象做key,因为两个 Symbol 永远不会相等。
由于无法创建出一样的 Symbol 值, 所以我们可以用它创建「私有」成员

更多的方法可以查看mdn
Symbol

Modules

模块 es6中的import语句为变量,函数,和类创建的是只读绑定,标识符只有在导出的模块中可以修改,即使是导入绑定的模块也无法更改绑定的值。
一些简单的语法可以在mdn上查看
这里贴一个以前的异步模块

var me =(function hello(name){
    function greeting(){
        console.log('hello'+name)
    }
    return {//public API
        greeting:greeting
    }
})('kyle')
console.log(me.greeting())

Template literals(模板字⾯量)

// 反引号包裹
const str = hello es2015, this is a string
允许换行,可以通过${}插入表达式,返回得结果会输出到对应位置
xxx${...}xxx

**带标签的模板字符串,模板字符串的标签就是一个特殊的函数,
使用这个标签就是调用这个函数**

const name = 'tom'
const gender = false

function myTagFunc (strings, name, gender) {
  // console.log(strings, name, gender)
  // return '123'
  const sex = gender ? 'man' : 'woman'
  return strings[0] + name + strings[1] + sex + strings[2]
}

const result = myTagFunc`hey, ${name} is a ${gender}.`

console.log(result)
hey, tom is a woman.

就是把字符串根据变量分割成一个数组,然后后续就是字符串中的变量依次传递.

Default parameters(默认参数)

在es5中给函数的默认参数只能

function foo (enable) {
  // 短路运算很多情况下是不适合判断默认参数的,例如 0 '' false null
  // enable = enable || true
  enable = enable === undefined ? true : enable
  console.log('foo invoked - enable: ')
  console.log(enable)
}

//这样去赋值现在es6中我们可以

function foo (enable = true) {
  console.log('foo invoked - enable: ')
  console.log(enable)
}

//包括解构的时候可以 const {sex='默认'}=props;
甚至函数可以
function foo({sex='1'}={}){
    console.log(sex)
}

Enhanced object literals(对象字⾯量增强)

这一篇比较简单看代码把

// 对象字面量

const bar = '345'

const obj = {
  foo: 123,
  // bar: bar
  // 属性名与变量名相同,可以省略 : bar
  bar,
  // method1: function () {
  //   console.log('method111')
  // }
  // 方法可以省略 : function
  method1 () {
    console.log('method111')
    // 这种方法就是普通的函数,同样影响 this 指向。
    console.log(this)
  },
  // Math.random(): 123 // 不允许
  // 通过 [] 让表达式的结果作为属性名
  [bar]: 123
}

// obj[Math.random()] = 123

console.log(obj)
obj.method1()
// Object.assign 方法

// const source1 = {
//   a: 123,
//   b: 123
// }

// const source2 = {
//   b: 789,
//   d: 789
// }

// const target = {
//   a: 456,
//   c: 456
// }

// const result = Object.assign(target, source1, source2)

// console.log(target)
// console.log(result === target)

// 应用场景

function func (obj) {
  // obj.name = 'func obj'
  // console.log(obj)

  const funcObj = Object.assign({}, obj)
  funcObj.name = 'func obj'
  console.log(funcObj)
}

const obj = { name: 'global obj' }

func(obj)
console.log(obj)
// Object.is

console.log(
  // 0 == false              // => true
  // 0 === false             // => false
  // +0 === -0               // => true
  // NaN === NaN             // => false
  // Object.is(+0, -0)       // => false
  // Object.is(NaN, NaN)     // => true
)

Destructuring assignments(解构分配)和Spread operator(展开操作符)

数组的解构

const arr = [100, 200, 300]
const [foo, bar, baz] = arr;
可以直接这样去解构,对应下表0,1,2
如果不要前面的可以
[,,baz]
这样就是只取2位置得了

经典的排序算法 交换位置可以:
const [arr[1],arr[0]]=[arr[0],arr[1]]
然后展开操作符
const [foo, ...rest] = arr;
取第一个 参数之后所有的返回一个数组。
展开操作符只能放在最后。

取出来的值也可以给定默认值
例如
const [foo, bar, baz = 123, more = 'default value'] = arr;

对象的解构:
const obj = { name: 'zce', age: 18 }
const { name } = obj;
相当于{name:name}
可以这样去解构,包括对象当参数传递时也可以这样解构
然后也可以给定默认值,前面有例子。
这个解构变量名字的声明和正常声明相反,是在右边,如下

const { name: objName = 'jack' } = obj;
name是从obj解构出来的属性名,objName是我们新的变量名来接收这个值,并且给了它一个默认值。

然后就是展开操作

const {name,...args}=obj;
image.png
不要在乎这些报错
这个解构展开就是说,除了解构指明的属性名之外,剩余的属性组成一个新对象,用剩余参数名称接受。

Proxy和Reflect

代理对象proxy和defineProperty的对比
image.png

优势:

  1. Proxy 可以监视读写以外的操作
  2. Proxy 可以很方便的监视数组操作
  3. Proxy 不需要侵入对象

一个简单的例子

const person2 = {
  name: 'zce',
  age: 20
}

const personProxy = new Proxy(person2, {
  get (target, property) {
    console.log('get', property)
    return target[property]
  },
  set (target, property, value) {
    console.log('set', property, value)
    target[property] = value
  }
})

personProxy.name = 'jack'

console.log(personProxy.name)

更多的api的使用方式可以在mdn中查看
Proxy的使用方式

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers的方法相同。Reflect不是一个函数对象,因此它是不可构造的。

它提供了对对象的统一操作,之前对象的操作不统一,
const obj = {
name: 'zce',
age: 18
}

console.log('name' in obj)
console.log(delete obj['age'])
console.log(Object.keys(obj))

console.log(Reflect.has(obj, 'name'))
console.log(Reflect.deleteProperty(obj, 'age'))
console.log(Reflect.ownKeys(obj))

具体提供了哪些方法,也可以直接去mdn中查看
Reflect

该内容借鉴于 拉钩大前端训练营

查看原文

赞 0 收藏 0 评论 0

Charon 发布了文章 · 10月28日

手写一个Promise

首先要确定Promise的几个特点

  1. Promise就是一个类,在执行这个类的时候需要传递一个执行器进去,执行器会立即执行
  2. Promise有三个状态,分别为 成功 fulfilled 失败 rejected 等待pending (pending=>fulfilled pending=>rejected) 一旦状态确定就不可更改
  3. resolve 和reject函数是用来更改状态的 resolve:fulfilled,reject:rejected
  4. then方法内部做的事情就是判断状态,如果状态是成功,就调用成功回调,如果是失败就调用失败回调,then方法被定义在原型对象里
  5. then成功回调有一个返回值,表示调用成功之后的值,失败回调也有一个返回值,表示失败的原因
  6. 同一个promise下的then可以调用多次的
  7. then方法是可以被链式调用的,后面then方法的回调函数拿到的值是上一个then方法的回调函数的返回值

以上就是Promise的主要部分,下面我们就根据这些特点来实现我们自己的promise

const PENDING = 'pending';
const FUFILLED = 'fufilled';
const REJECTED = 'rejected';
class MyPromise {
    status = PENDING;
    value = null;
    reason = null;
    successCallback = [];
    failCallback = [];
    constructor(executor) {//初始化的时候传进来的执行器
        try {
            executor(this.resolve, this.reject);//改变状态的俩函数,一个成功,一个失败
        } catch (error) {
            this.reject(err);//执行器出错的话,直接调用失败,改变当前promise的状态
        }
    }
    resolve = (value) => {//决议函数
        if (this.status !== PENDING) return;//如果状态不是等待,证明已经执行过了,为了符合promise一旦决议不可更改的特性,退出
        this.status = FUFILLED;//更改状态
        this.value = value;//保存一下决议值
        while (this.successCallback.length) this.successCallback.shift()();
        //如果执行器中代码是异步,并且绑定then函数时会把成功回调暂存起来,等执行器决议后调用通过then绑定的成功回调
        //当前这个promise可能会绑定多个then函数,所以成功回调用数组存放
    }
    reject = (reason) => {//失败状态函数
        if (this.status !== PENDING) return;//同上面的决议函数一样,调用过,退出
        this.reason = reason;//暂存失败原因,方便后续使用
        this.status = REJECTED;//修改状态
        while (this.failCallback.length) this.failCallback.shift()();
        //和resolve一样,不过这个是失败的数组,依次拿出来调用
    }
    then(successCallback, failCallback) {//then函数
        successCallback = successCallback ? successCallback : value => value;//成功参数可选,没有的话给个默认参数
        failCallback = failCallback ? failCallback : reason => { throw reason };
        //失败参数可选,没有的话给个默认手动抛错的函数,方便后续没传入的时候假如报错了,可以链式传递
        let promise2 = new MyPromise((resolve, reject) => {//声明promise,返回,符合then总是返回promise的规则
            if (this.status === FUFILLED) {//同步代码,成功了
                setTimeout(() => {///定时器添加是为了把这个函数修改为异步执行,等待promsie2同步执行完了,声明完毕后执行,这样才能获取到promise2
                    try {
                        let x = successCallback(this.value);//获取成功回调返回值
                        resolvePromise(promise2, x, resolve, reject);
                        //查看是否返回的是promise并且是否循环返回当前promise,如果是返回的promise展开他,并根据状态执行。如果是普通值,直接resolve
                    } catch (error) {
                        reject(error);//执行回调过程出错了,调用reject传递错误
                    }
                }, 0)
            } else if (this.status === REJECTED) {//同成功逻辑大体相似,不同是执行回调不一样,传入回调参数不一样
                setTimeout(() => {
                    try {
                        let x = failCallback(this.reason);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (error) {
                        reject(error);
                    }
                }, 0)
            } else {//上一个promsie执行器返回的不是立即值是一个异步操作
                this.successCallback.push(() => {//把这个成功回调暂存起来,等待当前promise resolve之后调用
                    setTimeout(() => {
                        try {
                            let x = successCallback(this.value);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (error) {
                            reject(error);
                        }
                    }, 0)
                })
                this.failCallback.push(() => {//把这个失败回调暂存起来,等待当前promise resolve之后调用
                    setTimeout(() => {
                        try {
                            let x = failCallback(this.reason);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (error) {
                            reject(error);
                        }
                    }, 0)
                })
            }
        })
        return promise2;
    }
    static all(array) {//promsie中的all方法  等待数组中所有项都完成后,在执行下一步 
        let result = []; //放完成结果
        let index = 0;//用来记录完成了几个
        return new MyPromise((resolve, reject) => {
            function addData(i, data) {//i是用来记录的下标防止顺序乱了,因为数组里有可能是立即值,有可能是promise,往reslut放的顺序有坑你不一致
                //data array数组项的执行结果
                result[key] = data;//放入
                index++;//标识加一
                if (index === result.length) {//相等全部完成了,往下走 resolve
                    resolve(result);
                }
            }
            for (let i = 0; i < array.length; i++) {
                //依次取出数组的项,查看是立即值还是promise,立即值的话直接添加到result数组里,promise的话展开它,成功了就添加到reslut里,失败了直接抛出reject,抛弃其他值
                let current = array[i];
                if (current instanceof MyPromise) {
                    current.then((value) => {
                        addData(i, value)
                    }, reason => { reject(reason) })
                } else {
                    addData(i, current)
                }
            }
        })
    }
    static resolve(value) {//遵循promise中resolve规范,如果传入是一个promise直接返回,如果不是用promise包i裹一层变成promise返回。 
        if (value instanceof MyPromise) return value;
        return new MyPromise((resolve) => resolve(value));
    }
    finally(callBack) {//先调用then返回promise,在它的回调里返回一个promise对象,然后调用callback并把它的返回值传递给promsie resolve方法,最后直接把当前promsie的成功值或失败值往后传递
        return this.then(() => {
            return MyPromise.resolve(callBack()).then(() => value);
        }, reason => {
            return MyPromise.resolve(callBack()).then(() => { throw reason });
        })
    }
    catch(failCallback) {//捕获错误方法,给当前返回的promsie通过then注册一个失败的回调,如果当前promise失败了就执行, 
        //是给返回promise注册的,每个then方法都会返回一个新的promise,如果前面某个链上的then返回的promsie有错误的话,并且没有使用错误回调捕获的话
        //我们会通过then默认错误回调,依次抛错,然后链式传递下来,知道被catch这个错误回调捕获
        return this.then(undefined, failCallback)
    }
    done(onFulfilled, onRejected) {
        //done代表promsie得结束,可以像then方法那样用,提供fulfilled和rejected状态的回调函数,也可以不提供任何参数。但不管怎样,done都会捕捉到任何可能出现的错误,并向全局抛出。
        this.then(onFulfilled, onRejected)
            .catch(function (reason) {
                // 抛出一个全局错误
                setTimeout(() => { throw reason }, 0);
            });
    }
}

function resolvePromise(promise2, x, resolve, reject) {//对比是否循环返回promsie的函数,判断返回的是promsie还是普通值得函数来进行下一步得调用
    if (x === promise2) {
        reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
        return;
    }
    if (x instanceof MyPromise) {
        x.then(value => resolve(value), reason => reject(reason))
    } else {
        resolve(x);
    }
}

代码里的每一行都会有我的注释,有兴趣得同学可下载下来run一下,看看,自己读一读。

该内容来自摘抄 拉钩大前端训练营

查看原文

赞 1 收藏 0 评论 0

Charon 发布了文章 · 10月28日

javascript函数式编程二(1-1-2)

前面说了函数式编程的一些基本概念,现在来说下函子

Functor

  • 容器:包含值和值的变形关系(这个变形关系就是函数)
  • 函子:是一个特殊容器,通过一个普通对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系,函数)

Functor函子

一个容器包裹一个值
class Container{
    static of(value){
    //of静态方法,可以省略new关键字创建对象,函数式编程把这些操作封装起来
        return new Container(value);    
    }
    constructor(value){
        this._value = value;
    }
    map(fn){
        return Container.of(fn(this._value))
    }
}

//测试
Container.of(3).map(x=>x+2).map(x=>x*x)
  • 函数式编程的运算不直接操作值,而是由函子完成。
  • 函子就是一个实现了map方法的契约对象。
  • 我们可以把函子想象成一个盒子,盒子里封装了一个值。
  • 想要处理盒子里的值,我们需要给盒子的map方法传递一个参数(纯函数),由这个函数来对这个值进行处理。
  • 最终map方法返回一个包含新值的盒子(函子)

MayBe函子

这个函子的作用是可以对外部空值情况做处理(控制副作用在允许的范围中)

class MayBe{
    static of(value){
        return new MayBe(value)
    }
    constructor(value){
        this._value=value;
    }
    
    map(fn){
        return this.isNothing()?MayBe.of(null):MayBe.of(fn(this._value))
    }
    isNothing(){
        return this._value===null||this._value===undefined
    }
}

MayBe.of('hello word').map(x=>x.toUpperCase())
MayBe.of(null).map(x=>x.toUpperCase())//null


//在MayBe函子中,我们很难判断那一步出现了null

MayBe.of('hello word').map(x=>x.toUpperCase()).map(x=>null).map(x=>x.split(' '))//null

Either函子

Either两者中的任何一个,类似if..else的处理

异常会让函数变得不纯,Either函子可以做异常处理

class Left {
    static of (value) {
        return new Left(value) 
    }
    constructor (value) { 
        this._value = value 
    }
    map (fn) {
        return this 
    } 
}
class Right { 
    static of (value) {
        return new Right(value) 
    }   
    constructor (value) { 
        this._value = value 
    }
    map(fn) { 
        return Right.of(fn(this._value)) 
    } 
}
function parseJSON(json) { 
    try { 
        return Right.of(JSON.parse(json)); 
    } catch (e) { 
        return Left.of({ error: e.message}); 
    } 
}
let r = parseJSON('{ "name": "zs" }') .map(x => x.name.toUpperCase()) 
console.log(r)

IO函子

  • io函子中的_value是一个函数,这里是把值作为函数来处理
  • io函子可以把不纯的动作储存搭配_value中,延迟执行这个不纯的操作(惰性执行),包装当前操作
  • 把不纯的操作交给调用者来处理
const fp = require('lodash/fp');
class IO{
    static of(x){
        return new IO(function(x){
            return x;
        })
    }
    constructor(fn){
        this._value=fn;
    }
    map(fn){
        return new IO(fp.flowRight(fn,this._value)) 
    }
}

let io = IO.of(process).map(p=>p.execPath)
console.log(io._value())

Task异步执行

  • 异步任务的实现过于复杂,使用folktale中的task来演示
  • folktale一个标准的函数式编程的库
  • 和lodash,ramda不同的是,他没有提供很多函数,只提供了一些函数式处理的操作。例如:compose,curry,等,一些函子Task,Either,MayBe等,详细信息可以关注下官方文档
const { compose, curry } = require('folktale/core/lambda') 
const { toUpper, first } = require('lodash/fp') 
//第一个参数是传入函数的参数个数 
let f = curry(2, function (x, y) { console.log(x + y) })
f(3, 4) 
f(3)(4) 
// 函数组合 
let f = compose(toUpper, first) 
f(['one', 'two'])

Task异步(2.3.3版本)

const { task } = require('folktale/concurrency/task') 
function readFile(filename) { //读取文件操作
    return task(resolver => { 
        fs.readFile(filename, 'utf-8', (err, data) => { 
            if (err) resolver.reject(err) 
            resolver.resolve(data) 
        }) 
    }) 
}
//调用run执行
readFile('package.json')
.map(split('\n'))
.map(find(x=>x.includes('version')))
.run()
.listen({
    onRejected:err=>{
        console.log(err)
    },
    onResolved:value=>{
        console.log(value)
    }
})

Pointed函子

  • Pointed函子是实现了of静态方法的函子,就是最开始写的函子。
  • of方法是为了避免使用new来创建对象,更深层的含义是of方法来把值放到上下文Context(把值放到容器中,使用map来处理)

最后一种Monad单子

在使用io函子的时候,有时候会写出这种代码

const fs = require('fs') 
const fp = require('lodash/fp') 
let readFile = function (filename) { 
    return new IO(function() { 
        return fs.readFileSync(filename, 'utf-8') 
    }) 
}
let print = function(x) { 
    return new IO(function() {  
        console.log(x) 
        return x 
    }) 
}

// IO(IO(x)) 合并
let cat = fp.flowRight(print, readFile) 
// 调用 ,第一次调用返回 print io函子里的函数,第二次是readFile里的
let r = cat('package.json')._value()._value() 
console.log(r)
  • Monad 函子是可以变扁的 Pointed 函子,IO(IO(x))
  • 一个函子如果具有 join 和 of 两个方法并遵守一些定律就是一个 Monad
const fp = require('lodash/fp') 
// IO Monad
class IO { 
    static of (x) { 
        return new IO(function () { return x })         
    }
    constructor (fn) { 
        this._value = fn 
    }
    map (fn) { 
        return new IO(fp.flowRight(fn, this._value)) 
    }
    join () {
        return this._value() 
    }
    flatMap (fn) {//合并并执行
        return this.map(fn).join() 
    } 
}

let r= readFile('package.json').map(fp.toUpper).flatMap(print).join();

实际上flatMap会让flowRight(print,fp.toUpper,readFile),这样执行的,最后一个print返回一个IO函子,里面的value是一个函数,所以最后需要join执行一下

也就是说函数嵌套,由函数组合可以解决。
函子嵌套可以使用 monad函子来解决

对于函子来说,前面返回的是普通值,可以直接使用map来操作, 如果返回的是一个Monad函子,使用flatMap来解决。

单子的使用场景可以 参考
函数式编程

本文内容 摘抄于 拉钩大前端训练营

查看原文

赞 0 收藏 0 评论 0

Charon 发布了文章 · 10月27日

javascript函数式编程一(1-1-1)

为什么要学习函数式编程

函数式编程时很古老的一个概念,一个编程范式。

  • 函数式随着react流行收到越来越多的关注
  • Vue3也开始拥抱函数式编程
  • 函数式编程可以抛弃this
  • 打包过程可以更好利用tree shaking过滤无用代码
  • 方便测试,方便并行处理
  • 有很多库可以帮助我们进行函数式开发:lodash,underscore,ramda

什么是函数式编程

函数式编程(functional Programming FP),FP是编程范式之一,我们常说的编程范式还有,面向对象,和面向过程。

  1. 面向对象:把现实世界的事物抽象程序世界中的类和对象,通过封装,继承和多态来演示事物事件的联系。
  2. 函数式编程:把现实世界的事物之间的联系抽象到程序世界(对运算过程进行抽象),如下

程序的本质:根据 输入通过某种运算获得相应的输出,程序开发过程中会涉及很多有输入和输出的函数。

x->f(映射,联系)->y,y=f(x)

**函数式编程中的函数指的不是程序中的函数(方法)。而是数学中的函数即映射关系,例如说:y=sin(x),x和y的关系

相同的输入始终要得到相同的输出(纯函数)

函数式编程用来描述数据(函数)之间的映射关系

// 非函数式 
let num1 = 2 
let num2 = 3 
let sum = num1 + num2 
console.log(sum) 
// 函数式 
function add (n1, n2) { 
    return n1 + n2 
}
let sum = add(2, 3) 
console.log(sum)

前置知识

函数是一等公民

  • 函数可以储存在变量中
  • 函数可以作为参数
  • 函数作为返回值

函数是一等公民是我们后面要学习高阶函数,柯里化的基础。

高阶函数

  • 可以把函数作为参数传递给另一个函数
  • 可以把函数作为另一个函数的返回结果

函数作为参数

function forEach(array,fn){
    for(let i=0;i<array.length;i++){
        fn(array[i])    
    }
}

function filter(array,fn){
    let results=[];
    for(let i=0;i<array.length;i++){
        if(fn(array[i])){
            results.push(array[i])
        }
    }
    return results;
}

函数作为返回值

function once(fn){
    let done = false;
    return function(){
        if(!done){
            done=true;
            fn.apply(this,arguments)
        }
    }
}
let pay =once(function(money){
    console.log(`支付:${money}元`)
})
//只会支付一次
pay(5)
pay(3)

使用高阶函数的意义

  • 抽象可以帮我们屏蔽细节,只需要关注与我们的目标
  • 高级函数是用来抽象通用的问题
//面向过程
let array = [1,2,3,4]
for(let i=0;i<array.length;i++){
    console.log(array[i])
}

//高阶函数
forEach(array,item=>console.log(item))

let r = filter(array,item=>item%2===0)

常用高阶函数:forEach,map,filter,reduce,some,every...

闭包

  • 可以在另一个作用域中调用一个函数的内部函数并访问该函数作用域中的成员,例如上面代码中的once。
  • 闭包的本质:函数在执行的时候会放到一个执行环境栈上,当函数执行完毕之后会从栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员。

闭包

纯函数

相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
传函数就类似数学中的函数(用来描述输入和输出之间的关系),y=f(x)
chunhanshu

lodash 就是一个纯函数的功能库,提供了对数组,数字,对象,字符串,函数等一些操作方法。

数组的slice和splice分别就是:纯函数和不纯函数
slice返回数组指定部分,不会改变原数组
splice对数组进行操作返回该数组,会改变原数组

  • 函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态)
  • 我们可以把一个函数的执行结果交给另一个函数去处理
纯函数的好处

可缓存,因为纯函数对相同的输入始终有相同的输出结果,所以我们可以把纯函数的结果缓存起来。
模拟一个memoize函数

function memoize(fn){
    let cache={};
    return function(){
        let arg_str = JSON.stringify(arguments)
        cache[arg_str] = cache[arg_str]||fn.apply(this.argsuments);
        return cache[arg_str]
    }
}

这样我们就实现了一个缓存纯函数

副作用:副作用让一个函数变得不纯,纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。

副作用来源:配置文件,数据库,获取用户输入...

柯里化(Currying)
  • 当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)
  • 然后返回一个新的函数接收剩余参数,返回结果

lodash中的函数柯里化
_.curry(func)

  • 功能:创建一个函数,该函数接收一个或多个func的参数,如果func所需要的参数都被提供则执行func并返回执行的结果,否则继续返回该函数并等待接收剩余的参数。
  • 参数:需要柯里化的函数。
  • 返回值:柯里化后的函数
const _=require('lodash');
function getsum(a,b,c){
    return a+b+c;
}

let curried = _.curry(getsum)
//测试
curried(1,2,3)
curried(1)(2)(3)
curried(1,2)(3)

const match =_.curry(function(reg,str){
    return str.match(reg)
})

const haveSpace = match(/\s+/g);
const haveNumber = match(/\d+/g);
console.log(haveSpace('hello woed'))
console.log(haveNumber('25$'))
const filter=_.curry(function(func,array){
        return array.filter(func)
})
console.log(filter(haveSpace)(['john Conner','john_Donne']))

模拟_.curry实现

function curry(func){
    return function curriedFn(..args){
        if(args.length<func.length){//func的参数长度
            return function(){
                   curriedFn(...args.concat(Array.form(arguments)))
                    //...args是当前参数  arguments是下次调用的参数
            }
        }
        //参数相等 ,开始调用
        return func(...args)
    }
}

总结:

  • 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
  • 这是一种对函数参数的缓存
  • 让函数变得更灵活,让函数的粒度更小
  • 可以把多元函数转化为一元函数,可以组合使用函数产生强大功能。

函数组合

纯函数和柯里化很容易写出洋葱圈的代码h(g(f(x)))

获取数组最后一个元素在转换成大写字母,使用lodash, _.toUpper(_.first(_.reverse(array)))
yangcong
函数组合可以让我们把细粒度的函数重新组合成一个新的函数来执行

  • 函数就像是数据的管道,函数组合就是把这些管道组合起来,让数据穿过多个管道形成最终的结果。
  • 函数组合默认是从右到左执行

lodash中的函数组合有:flow()或者flowRight(),它们都可以组合函数
flow()是从左到右
flowRight()是从右到左,使用的更多一些

const _=require('lodash');
const toUpper =s=>s.toUpperCase();
const reverse=arr=>arr.reverse();
const first = arr=>arr[0];
cons f = _.flowRight(toUpper,first,reverse)
console.log(f(['one','two','three']))

模拟flowRight方法

//多函数组合
function compose(...fns){
    return function(value){
    //acc初始(value)或者,上一次运行结果返回的值,fn是指当前项
        return fns.reverse().reduce(function(acc,fn){
            return fn(acc)
        },value)
    }
}

函数组合要满足组合律

例如我们既可以把 g 和 h 组合,还可以把 f 和 g 组合,结果都是一样的
// 结合律(associativity) 
let f = compose(f, g, h) 
let associative = compose(compose(f, g), h) == compose(f, compose(g, h)) 
// true

所以lodash也可以这样

const _ = require('lodash') 
// const f = _.flowRight(_.toUpper, _.first, _.reverse) 
// const f = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse) 
const f = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse))
console.log(f(['one', 'two', 'three'])) 
// => THREE

调试组合函数

const _ = require('lodash')
const f = _.flowRight(_.toUpper, _.first, _.reverse) console.log(f(['one', 'two', 'three']))

const trace=_.curry((tag,v)=>{//柯里化出来一个日志函数,方便组合使用
    console.log(tag,v)
    return v;
})

const split=_.curry((sep,str)=>_.split(str,sep));
const join=_.curry((sep,array)=>_.join(array,sep));
const map = _.curry((fn,array)=>_.map(array,fn));
//柯里化三个函数
const f = _.flowRight(join('-'), trace('map 之后'), map(_.toLower), trace('split 之后'), split(' ')) 
console.log(f('NEVER SAY DIE'))

lodash/fp

  • lodash的fp模块提供了实用的对函数式编程友好的方法。
  • 提供了不可变auto-curried iteratee-first data-last的方法

具体可以查看lodash的文档
下面是正常lodash和lodash/fp模块的差别

// lodash 模块 
const _ = require('lodash') 
_.map(['a', 'b', 'c'], _.toUpper) 
// => ['A', 'B', 'C'] 
_.map(['a', 'b', 'c']) 
// => ['a', 'b', 'c'] 
_.split('Hello World', ' ') 

// lodash/fp 模块 
const fp = require('lodash/fp') 
fp.map(fp.toUpper, ['a', 'b', 'c']) 
fp.map(fp.toUpper)(['a', 'b', 'c']) 
fp.split(' ', 'Hello World') 
fp.split(' ')('Hello World')
const fp = require('lodash/fp') 
const f = fp.flowRight(fp.join('-'), fp.map(_.toLower), fp.split(' ')) 
console.log(f('NEVER SAY DIE'))

Point Free

Point Free:我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。

不需要指明处理的数据
只需要合成运算过程
需要定义一些辅助的基本运算函数

使用 Point Free 的模式,把单词中的首字母提取并转换成大写

const fp = require('lodash/fp')
const firstLetterToUpper = fp.flowRight(join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), split(' ')) 
console.log(firstLetterToUpper('world wild web')) 
// => W. W. W

javascript函数式编程二 (函子)

本文内容 摘抄于 拉钩大前端训练营

查看原文

赞 1 收藏 0 评论 0

Charon 发布了文章 · 10月18日

javaScript之异步发展史二(generator,async)

接着来说,generator生成器函数:看似同步的异步流程控制风格。

基础部分可以看一下阮一峰老师的 生成器函数的概念了解一下。

ECMAScript 6 入门 生成器

这里只做一些 生成器函数配合promise的高级使用。

在生成器函数中可以 同步的使用try catch来捕获错误。

生成器中可以使用yield 来等待 异步promise的返回,这样我们配合使用就可以 同步的来监听 异步任务里的错误。

支持Promsie的Generator Runner

如何自动从执行到结束一个生成器函数,asynquence 这个库里的runner()做了实现,现在我们来实现一个自己简易版本,run(...)

function run(gen){
    var args = [].slice.call(arguments,1);//不包含第一个之后的参数
    var it = gen.apply(this,args);//用当前上下文 还有args参数初始化生成器
    return Prommise.resolve().then(function handleNext(value){
        var next = it.next(value);//对下一个yield出的值运行
        return (function handleResult(next){
            if(next.done){//如果迭代到最后,生成器函数运行完毕
                return next.value;
            }else{//未运行完毕
                //next.value的值不一定是promsie,有可能是个立即值,所以通过promise.resolve()统一转成异步操作
                return Promise.resolve(next.value).then(
                        handleNext,//成功就回复异步循环,把决议的值发回生成器
                        function handleErr(err){
                            //如果value是 被拒绝的promise,就把错误传回生成器进行出错处理
                            //[https://www.axihe.com/api/js-es/ob-generator/throw.html](https://www.axihe.com/api/js-es/ob-generator/throw.html)
                            //it.throw(err)类似it.next  也会继续往下走,处理错误,返回 next对象包含done和value
                            return Promise.resolve(it.throw(err)).then(handleResult)    
                        }
                )
            }
        })(next)
    })
    
}

//测试调用
        function foo(b) {
            return request(b);
        }
        function request(b) {
            return new Promise(function (resolve, reject) {
                setTimeout(function () {
                    if (b) {
                        resolve('100')
                    } else {
                        reject('200')
                    }
                }, 2000)
            })
        }
        function *main() {
            try {
                var texts = yield foo();
                console.log(texts)
                var text = yield foo(true);
                console.log(text)

            } catch (err) {
                console.error(err);
            }
            var text = yield foo(true);
            console.log(text)
            return 40;
        }
console.log(run(main).then(function (v) { console.log(v) }))

这种自动运行run的方式,它会自动异步运行你传给它的生成器。

这种模式看起来是不是很眼熟,这就是async与await的前身
ES7的 async和await如下

function foo(x,y){
    return request('http://...')
}
async function main(){
    try{
        var text = await foo(1,51);
        console.log(text)
    }catch(err){
        console.error(err)
    }
}
main()

使用async关键词修饰。

生成器中promise的并发。

使用生成器实现异步的方法全部要点在于创建简单,顺序,看似同步的代码,将异步的细节尽可能的隐藏起来。

function bar(url1,url2){
    return Promise.all([
        request(url1),
        request(url2)
    ])
}
function *foo(){
    var results = yield bar('http://...1','http://...2');
    const [r1,r2] = results;
    var r3 = request('http://...3'+r1+r2)
    console.log(r3)
}

//更高级的用法,组合promise 的api
function bar(){
    Promise.all([baz(...).then(...),Promise.race([...])]).then(...)
}

像bar方法里这种逻辑,如果直接放在生成器内部的话,那就失去了一开始使用生成器的理由,应该有意把这样的细节从生成器中抽象出来,以避免它把高层次的任务表达变得杂乱。

查看原文

赞 1 收藏 0 评论 0