线上猛如虎

线上猛如虎 查看完整档案

广州编辑  |  填写毕业院校  |  填写所在公司/组织 www.godblessyuan.com 编辑
编辑

你们都有梦想的,是吧.怀抱着梦想并且正朝着梦想努力的人,寻找着梦想的人,我想为这些人加油呐喊!

个人动态

线上猛如虎 赞了回答 · 2020-06-10

document.body.appendChild()会重新渲染整棵DOM树吗?

本来我觉得既然是appendChild的话是添加为body节点的最后一个子元素应该不会重新渲染整个Dom树,但是就在这个页面中打开控制台试了一下,好像跟我想象的不太一样。

clipboard.png

控制台中干了这么件事
然后在Performance中记录了大概六秒

clipboard.png
可以算出6634/50=132.68
也就是说这段时间里我的interval向body添加了大概132次div,花费在Rendering的时间一共1433.7毫秒(回流?)painting花费了376.2毫秒(重绘?)
但是这应该看不出来是否重新渲染了整个DOM,所以我又接着看

clipboard.png

找了其中一帧吧,在红线标出的这一帧中,Main中有几个事件块,分别是(timerfired)- recalculate style - Layout - update layer tree - paint - composite(综合)layers
其中Layout 和 paint 里查看详情,Layer Root上写的是#document

clipboard.png

就此明了,如果我的操作和理解没有问题的话,document.body.appendChild()会重新渲染整棵DOM树

=================================华丽的转折=============================================
提交回答后感觉越想越不对劲,于是又花时间多试了试,发现几次测试后就算换成了其他的节点上增加还是删除节点,分析中的layer root 都是#document,果然刚才的回答太草率了。
图片描述

从图上可以看到layout root没有变化,只是nodes that need layout 不一样。
然后。。。
图片描述

图片描述
将回答区域的el一个一个移除,查看分析后貌似重绘的只有自己和自己的子节点
不甘心,然后又刷新整个页面看了下
图片描述
页面加载时候的第一次回流应该是需要处理所有节点的吧Nodes that need layout(1059 of 1059)
然后我又按照刚才回答的方法继续在body上appendChild,
查看分析发现

clipboard.png

nodes that need layout 需要layout的nodes少了(换了个页面,所以总元素变成了641)

但愿这次分是是对的,别再说错了影响大家,如果有大手看到了有问题求指正
所以通过这次的折腾我最后的结论是不会重新渲染整个DOM(Chrome浏览器),至少是对于添加到最后的子元素应该不会影响到兄弟节点吧,嗯,应该是这样,忧伤。

======================================PS======================================
emmmmm我觉得自己好啰嗦
不死心,然后又before在前边插,然后挑选body中间的子节点在子节点中插,发现结果大同小异,need layout 值都不高,把原因拽给浏览器优化的结果。
最后写了个计时器丧心病狂的将body隐藏显示隐藏显示,嘿嘿,全部重绘,累死浏览器,哼

clipboard.png

关注 7 回答 5

线上猛如虎 赞了文章 · 2019-12-16

微信小程序踩坑记载(音视频)

微信小程序音视频踩坑

视频部分

1.视频播放到某个点就暂停,弹出另一个视频

  • 方案一:全部用view标签包裹,不支持同层播放的手机,⚠️视频会盖住全部
  • 方案二:除了最底下的视频,剩下的全部用cover-view包裹。⚠️华为的部分机型(支持同层播放),会出现最上面的视频有声音但是没画面。
  • 方案三:全部用view,底下视频暂停,就用背景图替换隐藏。⚠️华为部分机型,在最后的切换会变成黑屏,卡住不动。
  • 方案四:判断手机是否支持同层播放(SDK2.6.6版本才有,文档也没有,微信内部给的:onrendererror)如果支持同层播放就用view,不支持就用cover-view。⚠️暂时没有出问题

2.视频卡住不动
视频播着播着就卡住了,这是很难复现的问题,同型号手机也不一定出现问题。
解决方案:在开始播放的时候,开启一个心跳机制(计时器),先保存一个时间,几秒后检测状态不是结束,等待,并且时间是不是相等,判断是不是卡住了,弹框让用户选择重播或者下一步。(可能不是最好的方案,如果微信自己能解决,那就是最完美的)

3.视频能播放,但是会报错
报错时候加一个定时器,检查时间是否对等,不对等说明卡住了,真不走了

音频部分

1.锤子手机
某些音频播放卡住,分析音频时长和真实时长不对等,换了音频就没问题

2.音频报错
苹果手机很多会报错,导致整个音频走不下去。
解决方案:报错的时候,加一个定时器,检测时间是否走动,弹框让用户自己选择继续 或 下一步。

3.背景音频 BackgroundAudioManager
苹果手机偶尔会报错 playerErrorCode7
image.png
官方没有给出解决方案,但是可以在监测到出错,就把音频下载下来播放

有这方面经验的大神,请多多指教。上述的方案都不是最好的,大家一起讨论
未完待续。。。

查看原文

赞 6 收藏 5 评论 0

线上猛如虎 赞了回答 · 2019-07-30

解决有人不建议使用webpack DllPlugin,各位怎么看?

有一个主要问题
当单页面应用较大时,可以用按需加载方式分散模块加载压力,而使用dll方式引用的js无论是否需要都得先加载,当dll较多时,页面加载速度慢。
当然也可以只抽出共用的,但那样能抽的就比较少。

比如d3 echarts等大型组件,打成dll后打包速度明显提升,但牺牲的是页面加载速度,我们并不是每页都需要,但是却不得不先加载这部分js,这部分js必须先加载完毕后续的页面才能加载,看似很小的时间但是页面加载速度百毫秒级的浪费已经牺牲了体验。第三方组件越多问题越严重。

目前也仍在寻找解决方案,希望通过折中方式按需加载dll。

图片描述
这就是牺牲。

关注 4 回答 3

线上猛如虎 赞了文章 · 2019-03-25

【工程化】从0搭建VueJS移动端组件库开发框架

之前发表过一篇《Vue-Donut——专用于构建Vue的UI组件库的开发框架》,仅仅是对框架一个粗略的介绍,并没有针对里面的实现方式进行详细说明。

最近参与维护公司内部的一个针对移动端的UI组件库,该组件库缺乏文档和严格的文件组织结构。Vue-Donut的功能比较简单,并不能方便地创建针对移动端UI组件库的文档和预览。在参考了mint-ui等业界内成熟的方案之后,我在Vue-Donut的基础上进行了拓展,最后搭建出了一个非常方便且自动化的开发框架。

由于觉得开发的过程非常有意思,也想记录一下自己的开发思路,因此决定好好地写一篇文章作为记录分享。

项目地址:https://github.com/jrainlau/v...

1. 功能分析

首先我们来规划一下这个框架的最终目的是什么:

clipboard.png

如图所示,通过该框架可以生成一个文档页面。这个页面分为三个部分:导航、文档、预览。

  1. 导航:通过导航切换不同组件的文档和预览。

  2. 文档:该类型组件所对应的文档,以markdown形式书写。

  3. 预览:该类型组件所对应的预览页面。

为了让组件的开发和文档的维护更加高效,我们希望这个框架可以更加自动化。如果我们只要开不同组件的预览的页面及其对应的说明文档README,框架就能自动帮我们生成对应的导航和HTML内容,岂不妙哉?除此之外,当我们已经把所有的UI组件都开发好了,统统放在/components目录下,如果能够通过框架进行一键构建打包,最后产出一个npm包,那么别人使用这套UI组件库也会变得非常简单。带着这个想法,我们来分析一下我们可能需要用到的关键技术。

2. 技术分析

  • 使用webpack2作为框架核心:使用方便,高度可定制。同时webpack2文档已经相当齐全,生态圈繁荣,社区活跃,遇到的坑基本上都可以在google和stackoverflow找到。

  • 预览页面以iframe的形式插入到文档页面中:维护组件库的时候只需要聚焦于组件的开发和预览页面的组织,无需分心维护导航和文档,实现了解耦。因此意味着这是一个基于Vue.js的多页应用

  • 自动生成导航:使用vue-router进行页面切换。每当新建一个预览页面,就会自动在页面上生成对应的导航,并自动维护导航和路由的关系。因此,我们需要一套机制去监听文件结构的变化。

  • 自动生成文档:一个预览页面对应一份文档,所以文档理应以README.md的形式存放在对应的预览页面文件夹内。我们需要一个能够把README.md直接转化成html内容的办法。

  • 开发者模式:通过一条命令,启动一个webpack-dev-server,提供热更新和自动刷新功能。

  • 构建打包模式:通过一条命令,自动把/components目录下的所有资源打包成一个npm包。

  • 页面构建模式:通过一条命令,生成能够直接部署使用的静态资源文件。

通过对技术的梳理,我们脑海里面已经有了一个印象,接下来就是一步一步地进行开发了。

3. 梳理框架目录结构

一个好的目录结构,能够极大地方便我们接下来的工作。

.
├── index.html  // 文档页的入口html
├── view.html  // 预览页的入口html
├── package.json  // 依赖声明、npm script命令
├── src
│   ├── document  // 文档页目录
│   │   ├── doc-app.vue  // 文档页入口.vue文件
│   │   ├── doc-entry.js  // 文档页入口.js文件
│   │   ├── doc-router.js  // 文档页路由配置
│   │   ├── doc_comps  // 文档页组件
│   │   └── static  // 文档页静态资源
│   └── view  // 预览页目录
│       ├── assets  // 预览页静态资源
│       ├── components // UI组件库
│       ├── pages // 存放不同的预览页
│       ├── view-app.vue // 预览页入口.vue文件
│       ├── view-entry.js  // 预览页入口.js文件
│       └── view-router.js  // 预览页路由配置
└── webpack
    ├── webpack.base.config.js // webpack通用配置 
    ├── webpack.build.config.js  // UI库构建打包配置
    ├── webpack.dev.config.js  // 开发模式配置
    └── webpack.doc.config.js  // 静态资源构建配置

可以看到,目录结构并不复杂,接下来我们首先对webpack进行配置,以便我们能够把项目跑起来。

4. webapck配置

4.1 基础配置

进入到/webpack目录,新建一个webpack.base.config.js文件,其内容如下:

const { join } = require('path')
const hljs = require('highlight.js')

// 配置markdown解析、以便高亮显示markdown中的代码块
const markdown = require('markdown-it')({
  highlight: function (str, lang) {
    if (lang && hljs.getLanguage(lang)) {
      try {
        return '<pre class="hljs"><code>' +
               hljs.highlight(lang, str, true).value +
               '</code></pre>';
      } catch (__) {}
    }

    return '<pre class="hljs"><code>' + markdown.utils.escapeHtml(str) + '</code></pre>';
  }
})

const resolve = dir => join(__dirname, '..', dir)

module.exports = {
  // 只配置输出路径
  output: {
    filename: 'js/[name].js',
    path: resolve('dist'),
    publicPath: '/'
  },

  // 配置不同的loader以便资源加载
  // eslint是标配,建议加上
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          'babel-loader',
          'eslint-loader'
        ]
      },
      {
        enforce: 'pre',
        test: /\.vue$/,
        loader: 'eslint-loader',
        exclude: /node_modules/
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        loader: 'url-loader'
      },
      {
        test: /\.css$/,
        use: [{
          loader: 'style-loader'
        }, {
          loader: 'css-loader'
        }]
      },
      {
        test: /\.less$/,
        use: [{
          loader: 'style-loader' // creates style nodes from JS strings
        }, {
          loader: 'css-loader' // translates CSS into CommonJS
        }, {
          loader: 'less-loader' // compiles Less to CSS
        }]
      },
      // vue-markdown-loader能够把.md文件直接转化成vue组件
      {
        test: /\.md$/,
        loader: 'vue-markdown-loader',
        options: markdown
      }
    ]
  },
  resolve: {
    // 该项配置能够在加载资源的时候省略后缀名
    extensions: ['.js', '.vue', '.json', '.css', '.less'],
    modules: [resolve('src'), 'node_modules'],
    // 配置路径别名
    alias: {
      '~src': resolve('src'),
      '~components': resolve('src/view/components'),
      '~pages': resolve('src/view/pages'),
      '~assets': resolve('src/view/assets'),
      '~store': resolve('src/store'),
      '~static': resolve('src/document/static'),
      '~docComps': resolve('src/document/doc_comps')
    }
  }
}

4.2 开发模式配置

基础配置好了,我们就可以开始开发模式的配置了。在当前目录下,新建一个webpack.dev.config.js文件,并写入如下内容:

const { join } = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const basicConfig = require('./webpack.base.config')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const resolve = dir => join(__dirname, '..', dir)

module.exports = merge(basicConfig, {
  // 由于是多页应用,所以应该有2个入口文件
  entry: {
    app: './src/document/doc-entry.js',
    view: './src/view/view-entry.js'
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      }
    ]
  },
  devtool: 'inline-source-map',

  // webpack-dev-server配置
  devServer: {
    contentBase: resolve('/'),
    compress: true,
    hot: true,
    inline: true,
    publicPath: '/',
    stats: 'minimal'
  },
  plugins: [
    // 热更新插件
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NamedModulesPlugin(),
    
    // 把生成的js注入到入口html文件
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true,
      chunks: ['app']
    }),
    new HtmlWebpackPlugin({
      filename: 'view.html',
      template: 'view.html',
      inject: true,
      chunks: ['view']
    })
  ]
})

非常简单的配置,值得注意的是因为多页应用的缘故,入口文件和HtmlWebpackPlugin都要写多份。

4.3 构件打包配置

接下来,还有把UI组件库构建打包成npm包的配置。新建一个名为webpack.build.config.js的文件:

const { join } = require('path')
const merge = require('webpack-merge')
const basicConfig = require('./webpack.base.config')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

const resolve = dir => join(__dirname, '..', dir)

module.exports = merge(basicConfig, {
  // 入口文件
  entry: {
    app: './src/view/components/index.js'
  },
  devtool: 'source-map',
  // 输出位置为dist目录,名字自定义,输出格式为umd格式
  output: {
    path: resolve('dist'),
    filename: 'index.js',
    library: 'my-project',
    libraryTarget: 'umd'
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      }
    ]
  },
  plugins: [
    // 每一次打包都把上一次的清空
    new CleanWebpackPlugin(['dist'], {
      root: resolve('./')
    }),
    // 把静态资源复制出去,以便实现UI换肤等功能
    new CopyWebpackPlugin([
      { from: 'src/view/assets', to: 'assets' }
    ])
  ]
})

4.4 一键生成文档配置

最后,我们一起来配置一键生成文档网站的webpack.doc.config.js

const { join } = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const basicConfig = require('./webpack.base.config')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')

const resolve = dir => join(__dirname, '..', dir)

module.exports = merge(basicConfig, {
  // 类似开发者模式,两个入口文件,多了一个公共依赖包vendor
  // 以`js/`开头能够自动输出到`js`目录下
  entry: {
    'js/app': './src/document/doc-entry.js',
    'js/view': './src/view/view-entry.js',
    'js/vendor': [
      'vue',
      'vue-router'
    ]
  },
  devtool: 'source-map',

  // 输出文件加hash
  output: {
    path: resolve('docs'),
    filename: '[name].[chunkhash:8].js',
    chunkFilename: 'js/[name].[chunkhash:8].js'
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          loaders: {
            css: ExtractTextPlugin.extract({
              use: ['css-loader']
            }),
            less: ExtractTextPlugin.extract({
              use: ['css-loader', 'less-loader']
            })
          }
        }
      }
    ]
  },
  plugins: [
    // 提取css文件并指定其输出位置和命名
    new ExtractTextPlugin({
      filename: 'css/[name].[contenthash:8].css',
      allChunks: true
    }),
    
    // 抽离公共依赖
    new webpack.optimize.CommonsChunkPlugin({
      names: ['js/vendor', 'js/manifest']
    }),
    
    // 把构建出的静态资源注入到多个入口html中
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true,
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true
      },
      chunks: ['js/vendor', 'js/manifest', 'js/app'],
      chunksSortMode: 'dependency'
    }),
    new HtmlWebpackPlugin({
      filename: 'view.html',
      template: 'view.html',
      inject: true,
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true
      },
      chunks: ['js/vendor', 'js/manifest', 'js/view'],
      chunksSortMode: 'dependency'
    }),
    new webpack.LoaderOptionsPlugin({
      minimize: true,
      debug: false
    }),
    new webpack.optimize.OccurrenceOrderPlugin(),
    new CleanWebpackPlugin(['docs'], {
      root: resolve('./')
    })
  ]
})

通过上面这个配置,最终会产出一个index.html和一个view.html,以及各自所需的css和js文件。直接部署到静态服务器上即可进行访问。

多说一句,webpack的配置乍一看上去好像很复杂,但实际上是相当简单,webpack2的官方文档也挺完善且易读,推荐对webpack2不熟悉的朋友花点时间认真阅读一下文档。

至此,我们已经把/webpack目录下的相关配置都弄好了,框架的基础骨架已经搭建完毕,接下来开始对业务逻辑进行开发。

5. 业务逻辑开发

在根目录下新建两个入口文件index.htmlview.html,分别添加一个<div id="app"></div><div id="view"></div>标签。

进入/src目录,新建/document/view目录,按照前文目录结构所示新建需要的目录和文件。

具体的内容可以看这里,简单来说就是初始化vue应用,请暂时忽略router.js当中的这一段代码:

routeList.forEach((route) => {
  routes.splice(1, 0, {
    path: `/${route}`,
    component: resolve => require([`~pages/${route}/index`], resolve)
  });
});

这个是监听目录变化自动管理导航相关的功能,会在后面详细介绍。

逻辑很简单。/document/view分别属于文档预览两个应用,其中预览iframe的形式内嵌到文档应用页面内,相关的操作其实都是在文档当中进行。当点击导航的时候,文档应用会自动加载/view/pages/下相关预览页文件夹的README.md文件,同时修改iframe的链接,实现内容的同步切换。

接下来,我们一起来研究一下如何监听文件目录变化,自动维护router导航。

6. 自动维护router导航

如果你有用过Nuxt,一定对其自动维护router的功能不会陌生。如果没有用过也没关系,我们自己来实现这个功能!

使用vue-router的同学可能都经历过这么一个痛点,每当新建页面,都要往router.js的数组里面添加一个声明,最终router.js很可能会变成这样:

const route = [
  { path: '/a', component: resolve => require(['a'], resolve) },
  { path: '/b', component: resolve => require(['b'], resolve) },
  { path: '/c', component: resolve => require(['c'], resolve) },
  { path: '/d', component: resolve => require(['d'], resolve) },
  { path: '/e', component: resolve => require(['e'], resolve) },
  { path: '/f', component: resolve => require(['f'], resolve) },
  ...
]

很烦,对不对?如果可以自动维护就好了。首先我们要做一个约定,约定好不同的“页面”应该如何组织。

/src/view/pages目录下,每新建一个“页面”,我们就要新建一个和该页面同名的文件夹,往里添加文档README.md和入口index.vue,效果如下:

└── view
    └── pages
        ├── 页面A
        │   ├── index.vue
        │   └── README.md
        ├── 页面B
        │   ├── index.vue
        │   └── README.md
        ├── 页面C
        │   ├── index.vue
        │   └── README.md
        └── 页面D
            ├── index.vue
            └── README.md

约定好了文件的组织方式,接下来我们需要用到一个工具去负责监听和处理。这里我们使用了chokidar来实现。

/webpack目录下新建一个watcher.js文件:

console.log('Watching dirs...');
const { resolve } = require('path')
const chokidar = require('chokidar')
const fs = require('fs')
const routeList = []

const watcher = chokidar.watch(resolve(__dirname, '../src/view/pages'), {
  ignored: /(^|[\/\\])\../
})

watcher
  // 监听目录添加
  .on('addDir', (path) => {
    let routeName = path.split('/').pop()
    if (routeName !== 'pages' && routeName !== 'index') {
      routeList.push(`'${routeName}'`)
      fs.writeFileSync(resolve(__dirname, '../src/route-list.js'), `module.exports = [${routeList}]`)
    }
  })
  // 监听目录变化(删除、重命名)
  .on('unlinkDir', (path) => {
    let routeName = path.split('/').pop()
    const itemIndex = routeList.findIndex((val) => {
      return val === `'${routeName}'`
    })
    routeList.splice(itemIndex, 1)
    fs.writeFileSync(resolve(__dirname, '../src/route-list.js'), `module.exports = [${routeList}]`)
  })

module.exports = watcher

这里面主要做了3件事:监听目录变化、维护目录名列表、把列表写入文件。当开启watcher后,可以在/src底下看到一个route-list.js文件,内容如下:

module.exports = ['页面A','页面B','页面C','页面D']

然后我们就可以愉快地使用了……

// view-router.js

import routeList from '../route-list.js';

const routes = [
  { path: '/', component: resolve => require(['~pages/index'], resolve) },
  { path: '*', component: resolve => require(['~pages/index'], resolve) },
];

routeList.forEach((route) => {
  routes.splice(1, 0, {
    path: `/${route}`,
    component: resolve => require([`~pages/${route}/index`], resolve)
  });
});
// doc-router.js

import routeList from '../route-list.js';

const routes = [
  { path: '/', component: resolve => require(['~pages/index/README.md'], resolve) }
];

routeList.forEach((route) => {
  routes.push({
    path: `/${route}`,
    component: resolve => require([`~pages/${route}/README.md`], resolve)
  });
});

同理,在页面的导航组件里面,我们也加载这个route-list.js文件,实现导航内容的自动更新。

放个视频,大家可以感受一下(SF竟然不允许内嵌视频,不科学):
https://v.qq.com/x/page/a0510...

7. UI库文件组织约定

这个框架的根本目的,其实是为了UI库的开发。那么我们也应该对UI库的文件组织进行约定。

进入/src/view/components目录,我们的整个UI库就放在这里面:

└── components
    ├── index.js // 入口文件
    ├── 组件A
    │   ├── index.vue
    ├── 组件B
    │   ├── index.vue
    ├── 组件C
    │   ├── index.vue
    └── 组件D
        └── index.vue

当中的index.js,将会以vue plugin的方式编写:

import MyHeader from './组件A'
import MyContent from './组件B'
import MyFooter from './组件C'

const install = (Vue) => {
  Vue.component('my-header', MyHeader)
  Vue.component('my-content', MyContent)
  Vue.component('my-footer', MyFooter)
}

export {
  MyHeader,
  MyContent,
  MyFooter
}

export default install

这样,就能够在入口.js文件中以Vue.use(UILibrary)的形式对UI库进行引用了。

扩展一下,考虑到UI可能有“换肤”的功能,那么我们可以在/src/view目录下新建一个/assets目录,专门存放样式相关的文件,这个目录最终也会被打包到/dist目录下,在使用的时候引入相应样式文件即可。

8. 构建运行命令

前面做了那么多,最终我们希望能够通过简单的npm script命令就把整个框架运行起来,应该怎么做呢?

还记得在/webpack目录下的三个config.js文件吗?它们就是框架跑通的关键,但是我们并不打算直接运行它们,而是在其之上封装一下。

/webpack目录下新建一个dev.js文件,内容如下:

require('./watcher.js')
module.exports = require('./webpack.dev.config.js')

同样的,分别新建build.jsdoc.js文件,分别引入webpack.build.config.jswebpack.doc.config.js即可。

为什么要这么做呢?因为webpack运行的时候会读取config.js文件,如果我们希望在webpack工作之前先进行一些预处理,那么这种做法就非常方便了,比如这里添加的监听目录文件变化的功能。如果将来有什么扩展,也可以通过类似的方式进行。

接下来就是在package.json里面定义我们的npm script了:

"dev": "node_modules/.bin/webpack-dev-server --config webpack/dev.js",
"doc": "node_modules/.bin/webpack -p --config webpack/doc.js --progress --profile --colors",
"build": "node_modules/.bin/webpack -p --config webpack/build.js --progress --profile --colors"

值得注意的是,在生产模式下,需要加-p才能充分启动webpack2的tree-shaking功能。

在根目录下通过npm run 命令的方式测试一下是否已经跑起来了呢?

9. 后续工作

  • 添加单元测试

  • 加入PWA功能

10. 尾声

本文篇幅较长,能够看到这里的估计已经有点晕了吧。很久都没有写文章了,终于被我攒了个大招发出来,特别爽。搭建开发框架的过程是一个不断尝试,不断google和stackoverflow的过程。在这个过程中,大到对架构设计,小到对文件组织、工具使用,都有了更进一步的理解。

这个框架的运作模式,其实也是参考了很多业界内的方案,更多的是想要“偷懒”。能让机器自动帮忙搞的,绝对不自己手动搞,这才是技术进步的动力嘛。

该项目已经被改装成vue-cli的模板,通过vue init jrainlau/vue-donut#mobile即可使用,欢迎尝试,期待反馈和PR,谢谢大家~

查看原文

赞 17 收藏 32 评论 6

线上猛如虎 赞了文章 · 2018-10-25

结合promise与websocket的发布/订阅模式实践

结合promise与websocket的发布/订阅模式实践

本文初衷

最近恰好在公司做了一个聊天室系统,所以在系统中做了一下对websocket进行的promise化改造,所以想写篇文章总结一下,如果大家有什么更好的方法或者心得感悟,欢迎交流

技术栈

dva + protobuf
考虑到protobuf对websocket并没什么本质影响,所以本文就不涉及了

业务场景

基于websocket的聊天室系统

开发痛点

  1. 可能存在按顺序触发的请求
    eg. 删除req---确认删除rsp---刷新页面req---刷新页面rsp
    但由于并非所有的删除操作后都会刷新页面,所以考虑是不是可以使用发布订阅模式来模拟类似promise的流式操作
  2. 存在同一类型请求短时间内多次触发时,如何寻找每一条回复信息的发射源,考虑可以使用promise池+唯一识别码token来实现
  3. 由于dva的异步操作是基于redux-saga的,所以如果可以用promise完成与websocket的互动,那么对于effects中使用yield控制异步流程,也会是一个很好的体验

实现原理

首先,这一套系统的一切前提是请求的唯一标识符token,前端发送给服务器之后,服务器必须要把这个token跟数据放在一起发回来才行

本系统的实现原理是

对websocket的send方法进行封装
发送阶段
1. send执行时,先生成一个promise,及其唯一token
2. 将promise的resolve, reject,token,及其他需要的信息放入一个对象,然后推入一个promise池中
3. 执行websocket的send方法
4. return 这个promise

接收阶段
1. 收到回复消息时,先从promise池中对token进行匹配
2. 根据回复的状态决定执行resolve或reject
3. 其他需要的操作

实现代码

// 每一个实例都只能open一条socket线路,用锁机制防止重复open
// 本例中不使用心跳检测,为了方便,只要close是非主动触发且前端能捕捉到的(如浏览器主动断开,服务器主动断开),都会进行自动重连
export class MyWebSocket {
    constructor(url) {
        this.url = url;
    
        // close来源判断及后续操作
        this.closeConfig = {
            resolve: null,
            closing: false
        }
        // promise池
        this.promisePool = [];
    }
    
    tokenCheck(req, rsp) {
    // 此处根据自己的数据结构进行tokenCheck的判断,返回一个boolean
    }
    
    open() {
        return new Promise((resolve, reject) => {
            if (typeof this._websocket === 'undefined') {
                this._websocket = new WebSocket(this.url);
                this._websocket.open = (e) => {
                    resolve({e, ws: this});
                };
                this._websocket.onerror = (e) => {
                    reject(e);
                }
            }
            this._websocket.onclose = (e) => {
                // 非主动close
                if (!this.closeConfig.closing) {
                    console.log('reconnect');     
                    // 对应的重连操作     
                }
                // 若手动close,恢复初始状态
                this.closeConfig.closing = false;
            }
            
            this._websocket.onmessage = (e) => {
                this.promisePool = this.promisePool.filter((item) => {
                if (this.tokenCheck(req, rsp) {
                  req.resolve(rsp);
                  return false;
                }
                return true;
              })
            };
        });
    }
    
    close() {
        this.closeConfig.closing = true;
        this._websocket.close();        
    }
    // token包含在content中
    send(name, content) {
        return new Promise((resolve, reject) => {
          this.promisePool.push({
            content,
            resolve,
            reject,
            name
          });
          this._websocket.send({name, content});
    });
}

大概流程就是这样,具体的样例如下

*test () {
    const ws = new MyWebSocket('www.mywebsocket.com');
    yield ws.open();
    yield ws.send(.....).then(()=>{...});
    yield ws.send(.....).then(()=>{...});
}

本文呢大概就是这么多了,如果有什么错误或者遗漏的地方还请大家多多指教


v0.0.2

采取了评论大佬的建议,将promise池从数组改为对象,直接将token做为key,查询起来也非常方便

export class MyWebSocket {
    constructor(url) {
        this.url = url;
    
        // close来源判断及后续操作
        this.closeConfig = {
            resolve: null,
            closing: false
        }
        // promise池
        this.promisePool = {};
    }
    
    tokenCheck(req, rsp) {
    // 此处根据自己的数据结构进行tokenCheck的判断,返回一个boolean
    }
    
    open() {
        return new Promise((resolve, reject) => {
            if (typeof this._websocket === 'undefined') {
                this._websocket = new WebSocket(this.url);
                this._websocket.open = (e) => {
                    resolve({e, ws: this});
                };
                this._websocket.onerror = (e) => {
                    reject(e);
                }
            }
            this._websocket.onclose = (e) => {
                // 非主动close
                if (!this.closeConfig.closing) {
                    console.log('reconnect');     
                    // 对应的重连操作     
                }
                // 若手动close,恢复初始状态
                this.closeConfig.closing = false;
            }
            
            this._websocket.onmessage = (e) => {         
                const key = e.content.token;    
                const req = this.promisePool[key]
                req.resolve(e);
                delete this.promisePool[key];
            };
        });
    }
    
    close() {
        this.closeConfig.closing = true;
        this._websocket.close();
    }
    // token包含在content中
    send(name, content) {
        return new Promise((resolve, reject) => {
          this.promisePool[content.token] = {
            content,
            resolve,
            reject,
            name
          };
          this._websocket.send({name, content});
    });
}
查看原文

赞 5 收藏 5 评论 4

线上猛如虎 发布了文章 · 2018-10-21

小程序开发-梳理登录流程-v1.0

最近发现小程序的登录逻辑还有一些新的心得,所以记录一下。

一、官方微信小程序登录流程个人理解

在小程序官网里面会提到一个小程序的登录逻辑,这是官方推荐的登录逻辑,也就是所谓的小程序登录态维护逻辑,这里是官方的图:

官方逻辑的个人理解:

  1. 用户打开微信小程序。

    • 也可以是在需要的时候处理这个逻辑,不一定是打开小程序的时候执行这个逻辑。
    • 具体看业务需要,但是往往很多业务设计都是要用户打开小程序的时候,来运行这个逻辑的。
  2. 小程序通过wx.login获取微信的 code,然后将这个 code 发送给开发者服务器(我们自己的开发服务器)。
  3. 开发者服务器接收到code 之后,会进行封装处理,通过code2Session这个api 接口来获取真正需要的微信用户的登录态session_keyopenidunionid

    • 准确来说session_key才是真正的微信登录态信息,但是把 openidunionid加起来一起理解,也可以笼统地理解为这些都是微信的登录态信息。
  4. 然后需要开发者服务器自己生成一个自定义的登录态(例如业务 token或者 session)来保存这些微信服务器返回来的微信登录态相关信息(session_keyopenidunionid),并且做关联处理,然后返回给小程序客户端。

    • 关联处理就是你的自定义登录态和微信的登录态相关联,这样的话就不需要维护多个登录态,只需要维护一个就可以了。
    • 关联处理之后需要将这个自定义登录态信息保存起来,可以放到数据库或者本地文件或者 例如 redis 之类的缓存服务里面,以便方便后续使用,而不需要每次都请求微信服务器(微信服务器对这个请求的频率是有限制的)
    • 注意这里不返回微信登录态相关信息,只返回自定义的登录态信息。
    • 自定义登录态的信息不仅可以包含 token,也可以包含一些用户权限信息,或者其他信息,因为是自定义的登录态,维护也是很自定义的。
    • 一般自定义的登录态的超时时长需要比微信的登录态要长。
  5. 小程序客户端接收到返回的自定义登录态信息,从而判断用户是否登录成功,登录成功的话,就将自定义登录态信息保存到本地的存储。

    • 本地的存储可以是微信小程序提供的 app.globaldata,也可以是 localstoage,注意,小程序不支持 cookie
    • 保存到本地存储的好处就是,后续使用的这个自定义登录态就不需要再次跟服务器进行交互来获取了,只需要调用本地存储就行了,这里是为了优化性能和避免浪费资源。
  6. 小程序客户端访问业务接口的时候,携带之前保存到本地存储的自定义登录态信息进行对开发者服务器(业务接口服务器)访问。
  7. 开发者服务器的业务接口接收到请求,并且请求里面携带了自定义的登录态,通过校验之后,会返回相关信息。

    • 校验是将小程序客户端携带过来的自定义登录态和开发者服务器缓存起来的自定义登录态进行对比,会去确认是否和用户的 openid或者 unionidsession_key 相匹配。
    • 如果匹配,就可以马上返回相关信息。
    • 如果不匹配,就可以马上返回相关信息,告知小程序客户端无法访问业务接口。
    • 如果匹配结果是自定义登录态超时了,就可以马上返回相关信息,告知小程序客户端需要重新运行登录逻辑
    • 如果是匹配结果是自定义登录态没有超时,但是微信登录态超时了,那么就会开发者服务器就会再次发起code2Session进行微信登录态更新。
整个微信小程序官方推荐的登录态维护流程就是这样了,官方推荐使用自定义登录态来进行整个微信小程序的登录维护,既然寄人篱下,那么就要按部就班,跟着政策走,这是最好的应对。

上面的图里面的一些术语解释:

  • code 是微信用户的登录凭证,打开小程序登录的时候获取的只属于微信这个用户的登录凭证,需要注意的是,这个登录凭证只供微信小程序使用的。
  • 这个 code 的存活时间一般是5分钟左右,这是一个临时的登录凭证,他的最大作用就是确定是来源自哪一个微信用户来打开,是为了后续生成一个微信登录态 session_key而使用的。
  • session_key 是微信用户在小程序里面的登录态信息,这是微信给这个用户颁发的一个登录 session

    • 他的返回格式{"session_key",openid":"..."},官方说需要定期使用wx.checkSession检测,但是在实际的场景里面其实也可以不用,用和不用主要看你怎么设计你的微信小程序,这个后续会说。
    • 早起官方是返回 expire_time 过期时间的,但是后面取消了,这里有一个比较新的官方回复:用户越频繁使用小程序,session_key有效期越长,…… | 微信开放社区,有效期是3天,但是这个不一定是固定的,具体看业务需求,总的原则就是维护一个自定义登录态,自定义登录需要和微信登录态关联。
  • openId ,用户在微信里面的唯一标识,但是需要跟 unionid 进行一起理解。
  • unioinId,如果开发者拥有多个移动应用、网站应用、和公众帐号(包括小程序),可通过unionid来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用和公众帐号(包括小程序),用户的unionid是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,unionid是相同的。

    • 一般来说,openId 就是微信用户的唯一标识,但是因为微信产品很多,所以会出现多个微信产品使用不同的 openId 来标识用户,但是对于我们做业务接入的话,就买办法使用了,所以建议是统一使用 unioinid,因为一般来说,一般的业务都会有公众号,小程序联合使用的,所以 unionid 使用频率较高。
  • 3rd_session 是一般是指开发者服务器的登录态,也就是自定义登录态,也就是我们自己公司的业务服务器的登录态(微信官方推荐使用自定义登录态来管理整个微信小程序登录)。

    • 当小程序登录态过期了,自定义登录态没过期的时候,那么就需要在小程序打开的时候先执行一次wx.checkSession来检查,如果过期了,就本地执行登录操作,再让开发者服务器跟微信服务器交互,获取新的小程序登录态,然后关联到自定义登录态。
    • 当小程序登录态没过期,自定义登录态过期了的时候,那么小程序客户端访问业务接口的时候,业务接口会告诉小程序客户端,你的自定义登录态超时了,然后小程序客户端会重新执行登录逻辑,然后通知开发者服务重新生成新的自定义登录态,然后关联之前还在使用的小程序登录态。
    • 当二者都同时过期的时候,那就肯定要发起完整的重新登录了。
    • 这样的好处是自定义登录态不需要重复创建,也能跟小程序登录态一起维护管理,达到资源合理利用的效果。
  • 小程序的登录态可以使用微信返回的 session_key 作为代表理解。
  • 自定义登录态是开发者服务器创建的,也是业务的登录态,一般是指我们业务自己的 session会话,官方说的3rd_session就是这个。
  • 一般自定义登录态的管理都会使用类似 redis 之类的东西来进行管理的,这里也涉及到一个自定义登录态的缓存策略,缓存起来,在一定时间内不需要重新创建自定义登录态,达到优化性能的效果。
只有理解好官方的标准流程,才能更好地理解其他流程,或者其他人对官方流程的封装和注解!

二、扩展微信小程序登录流程

上面是基本的登录流程理解,但是实际业务中还是会有一些地方需要补充的,但是我们理解的时候最好把他们分开,这样会更清晰和简单。

按照官方推荐的方式是使用自定义登录态来管理整个微信小程序登录流程的,虽然说是通过一个自定义登录态来管理,但是其实里面是有变通的。

这里有2种方式来做:

  1. 方式一:小程序打开的时候先检查小程序本地是否有存储的自定义登录态,

    • 如果没有,则代表是首次登录,要自动执行完整的登录流程,
    • 如果有,则需要判断这个自定义登录态是否过期,这里判断的方式有2种,一种是开发者服务器提供一个接口来检查,一种是在这个自定义登录态数据里面加上过期时间,判断是否过期。

      • 过期,则自动发起完整的登录流程。
      • 不过期,就继续使用本地保存的自定义登录态。
  2. 方式二:小程序打开的时候先发起wx.checkSession检查微信登录态是否过期:

    1. 如果过期,则发起完整的登录流程。
    2. 如果不过期,则继续使用本地保存的自定义登录态。(如果本地的自定义登录态没有的话,那么也是要强制发起完整的登录流程的)

上面说的方式都是打开小程序的时候做的,但是也要考虑到一种情况,就是自定义登录态在用户使用小程序的过程中过期了,那么这时候也是需要强制执行完整的登录流程的。

相对来说方式二比较好,方式二的好处是不需要小程序服务端来参与校验,而是在小程序端调用API。这种方式实际上是将维护登录态的机制委托给了微信维护的session_key,开发者不需要在自定义的登录态中保存有效期信息。腾讯云开发的wafer项目便采取了这种方法。

三、继续扩展微信小程序登录流程

上面提到的方式二是在打开小程序的时候做的,这里是在每次发起 HTTP 请求的时候做的,这个是不一样的逻辑。

当小程序客户端访问接口的时候,开发者服务器返回说你的自定义登录态过期了,你就需要重新发起完整的登录,但这里有2个点是需要注意的:

  1. 由于 HTTP 是无状态的,所以不能跟踪状态,发起 HTTP 请求然后服务器返回自定义登录态过期的结果,然后就完结了,没办法暂存起原来的 HTTP 请求,以便当我们有可用的自定义登录态的时候,继续使用。所以我们需要在发起 HTTP 请求的时候做判断,看是否需要发起完整的登录流程。
  2. 在小程序里面,经常会有访问开发者服务器的情况,但是如果每个接口都需要这样直接判断的话,就会产生很多冗余代码,而且没办法维护,万一登录流程改变了,就要全部代码改一遍了。

所以这里就需要进行封装一下,Random Notes这里就提到了,思路是正确的,不过实现的话可以按自己需要实现,他这里有2个地方描述得很好,所以我搬过来借鉴了。

这是一个宏观的,每个请求都要检查一下登录态的逻辑图:


图片引用自:https://blog.imtouch.info

每个请求都会检查一次登录态,不管是自定义登录态还是微信登录态,反正只要发起跟开发者服务器交互的 HTTP 请求的之前都会检查。

细分逻辑的话就会变成这样:


图片引用自:https://blog.imtouch.info

细分逻辑之后看起来会更加清晰,并且这里的逻辑是微信登录态和自定义登录态都会检查一遍,任何一个登录态过期都会发起完整的登录逻辑,其实我觉得这里也可以只判断自定义登录态的状态也可以的。

不过由此也带来一个问题,就是如果一个页面里面有多个请求同时发起的话,那么就会出现同时检查到登录态过期,然后同时发起登录的情况,这个问题可能会导致开发者服务器的校验登录数据出现问题,他这里用到 promise.race 来解决,具体可以看他的文章里面介绍,我后面也会继续写一下这个处理的介绍。

参考文档:

查看原文

赞 13 收藏 9 评论 0

线上猛如虎 评论了文章 · 2018-10-17

仿饿了么项目-vue的学习笔记总目录

仿饿了么项目-vue的学习笔记总目录

支持正版,人人有责.

  1. 介绍开发流程以及相关技术

  2. vue-cli和webpack和一些项目文件介绍

  3. 项目资源和目录设计和mock模拟数据

  4. 头部组件header.vue

  5. 商品区域goods.vue

  6. shopcart.vue购物车

  7. 商品详情页food.vue

  8. 商家页面seller.vue

  9. 评价列表ratings组件

  10. split,formatDate,store,util组件

  11. vue动画效果配置和弹层css sticky footer

  12. stylus相关和1像素边框问题

  13. webpack打包发布

总结:

  1. 里面的内容很干,对于学习了一段日子的前端来说,相当于打开了一个新世界大门,不过相对而言铺天盖地的新事物会有点吃不消,这个不可操之过急,所谓师傅领进门,修行靠个人.

  2. 上面是我自己总结的整个学习过程的一些记录,可能直接看会比较懵逼,不过如果结合视频来看的话,应该还是会有点帮助的,希望能够帮到其他人.

  3. 支持正版!

备注:

  1. Vue.js 高仿饿了么外卖 App 课程源码,课程地址

  2. 作者Ustbhuangyi是《Vue.js权威指南》一书的主要作者,虽然书写得一般般(铺天盖地的差评可不是盖的),但是这个视频真心的不错,比一般的学习视频更加实在(事物总是有两面性~).

  3. 项目github地址:https://github.com/ustbhuangyi/vue-sell

  4. 项目演示地址:http://vuejssellapp.t.imooc.io/#!/goods

查看原文

线上猛如虎 发布了文章 · 2018-10-14

我来重新学习js 的面向对象(part 5)

这是最后的最后了,我会顺便总结一下各种继承方式的学习和理解。(老板要求什么的,管他呢)

一、继承-组合继承、伪经典继承


图片来自:http://www.joyme.com/xinwen/2...

这是一种将原型链和借用构造函数的技术结合起来的一种继承模式。不是假合体,是真合体!

核心思想是:

  • 使用原型链实现对原型属性和方法的继承。
  • 通过借用改造函数来实现对实例属性的继承。
很像之前说过的组合使用构造函数模式和原型模式
// 父类构造函数
function Food(name) {
  this.name = name;
  this.colors = ["red", "blue"];
}
// 父类原型对象的方法
Food.prototype.sayName = function() {
  console.log("我是" + this.name);
};
// 子类构造函数
function Fruit(name, place) {
  // 在构造函数里面调用父类搞糟函数,实现属性继承
  Food.call(this, name);
  this.place = place;
}
// 将父类的实例赋值给子类的原型对象,实现方法继承
Fruit.prototype = new Food();
// 添加子类原型对象的方法
Fruit.prototype.sayPlace = function() {
  console.log(this.place);
};

var food1 = new Fruit("苹果", "非洲");
food1.colors.push("black");
console.log(food1.colors); // 返回 [ 'red', 'blue', ' black' ]
food1.sayName(); // 返回 我是苹果
food1.sayPlace(); // 返回 非洲

var food2 = new Fruit("香蕉", "亚洲");
food2.colors.push("yellow");
console.log(food2.colors); // 返回 [ 'red', 'blue', 'yellow' ]
food2.sayName(); // 返回 我是香蕉
food2.sayPlace(); // 返回 亚洲
  • 可以看到超类构造函数 Food里的属性(namecolors)和超类构造函数的原型对象的方法( sayName )都能够被继承,并且对于引用类型的值也不会出现相互影响的情况,而子类构造函数的属性(place)和子类构造函数的原型对象的方法( sayPlace)也能够很好的使用,不会被覆盖,他们相互共享又相互独立。
  • 这里的属性继承是通过 call 方式,将父类的属性放到子类的构造函数里面,也就是借用构造函数模式。
  • 这里的方法继承是通过将父类的实例放到子类的原型对象上,也就是原型链模式。

也存在一些问题

  • 它需要调用两次超类型构造函数,一次是在创建子类型原型的时候,另一次是在子类型构造函数内部,
  • 也需要重写 constructor 属性,因为原型对象被重写了,constructor就丢失了
// 。。。。。。。。
// 子类构造函数
function Fruit(name, place) {
  // 在构造函数里面调用父类搞糟函数,实现属性继承
  Food.call(this, name); // 第二次调用父类构造函数
  this.place = place;
}
// 将父类的实例赋值给子类的原型对象,实现方法继承
Fruit.prototype = new Food(); // 第一次调用父类构造函数
Fruit.prototype.constrcutor=Fruit;//因重写原型而失去constructor属性,所以要对constrcutor重新赋值
// 添加子类原型对象的方法
Fruit.prototype.sayPlace = function() {
  console.log(this.place);
};
// 。。。。。。。
在一般情况下,这是我们在 javascript 程序开发设计中比较常用的继承模式了。

基于以上原因,我们需要引入寄生组合式继承来解决它的存在的问题,实现完美的继承。但是在了解它之前,需要先了解寄生式继承,而了解寄生式继承之前,需要了解原型式继承,他们是一个接一个的推导出来的。

二、继承-原型式继承


图片来自:http://acg.shunwang.com/2014/...

核心思想是借助原型可以基于已有的对象创建新对象,同时不必因此创建自定义类型。

  • 以一个对象实例来做模板进行复制,并且是借助原型链模式进行特殊复制
  • 这种复制的方式会有一些特别的地方,例如,引用类型的值问题也是无法解决,复制可以借助 es5语法也可以不借助,前者更加强大一些。
// 原型式继承的关键-复制
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

var food1 = {
  name: "苹果",
  colors: ["red", "blue"]
};
// 继承
var food2 = object(food1);

food2.name = "香蕉";
food2.colors.push("black");

//。。。。。。无限增殖

console.log(food1.name); // 返回 苹果
console.log(food2.name); // 返回 香蕉
console.log(food1.colors); // 返回 [ 'red', 'blue', 'black' ]
console.log(food2.colors); // 返回 [ 'red', 'blue', 'black' ]

2.1 使用 es5的新语法:Object.create()

Object.create()方法会创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

Object.create()是es5新增的,用来规范原型式继承。

如果单纯使用的话,效果跟之前的差别不大,参考下面例子:

var food1 = {
  name: "苹果",
  colors: ["red", "blue"]
};

var food2 = Object.create(food1);
food2.name = "香蕉";
food2.colors.push("black");

console.log(food1.name); // 返回 苹果
console.log(food2.name); // 返回 香蕉
console.log(food1.colors); // 返回 [ 'red', 'blue', 'black' ]
console.log(food2.colors); // 返回 [ 'red', 'blue', 'black' ]

如果注意使用它的第二个参数的话,差别就不一样了:

var food1 = {
  name: "苹果",
  colors: ["red", "blue"]
};

var food2 = Object.create(food1, {
  name: { value: "香蕉" },
  colors: { // !!!!!
    value: ["red", "blue", "black"]
  }
});

console.log(food1.name); // 返回 苹果
console.log(food2.name); // 返回 香江
console.log(food1.colors); // 返回 [ 'red', 'blue' ]  !!!!!
console.log(food2.colors); // 返回 [ 'red', 'blue', 'black' ]

可以看到引用类型的数值不会被共享,实现了很好的继承效果。

出现这个情况主要是因为如果使用 push 的话,还是操作同一个内存指针,使用Object.create的话,会重新添加到新创建对象的可枚举属性,不是同一个内存指针了。

2.2 发现一些有价值的东西


图片来自:http://www.cifnews.com/articl...

参考 mdn 里面的介绍,会发现一些更有价值的东西,可以用 Object.create实现类式继承:

// Shape - 父类(superclass)
function Shape() {
  this.x = 0;
  this.y = 0;
}

// 父类的方法
Shape.prototype.move = function(x, y) {
  this.x += x;
  this.y += y;
  console.info('Shape moved.');
};

// Rectangle - 子类(subclass)
function Rectangle() {
  Shape.call(this); // call super constructor.
}

// 子类续承父类
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

var rect = new Rectangle();

console.log(rect instanceof Rectangle); // true
console.log(rect instanceof Shape); // true
rect.move(1, 1); // Outputs, 'Shape moved.'
  • Object.create会将参数里的对象添加到它返回的新对象的原型对象里面去,这样首先生成了一个新对象,并且该对象的原型对象是参数里的值,即Shape.prototype,新对象是临时的,暂时看不到,这个临时的新对象里面就包含了父类原型对象。
  • 这里将Object.create返回的新对象放到子类的原型对象里面,这样子类就拥有了父类的原型对象,也就实现了方法的继承。
  • 手动设置一个子类的原型对象的 constructor,是为了重新指定子类的构造函数名字,这样子类实例对象就可以查看到他的构造函数是谁,证明是某个实例来自于哪一个构造函数,这样代码和结构都会清晰。
  • 属性的继承还是有 call 实现。

还有更屌炸飞的东西,如果你希望能继承到多个对象,则可以使用混入的方式。

function MyClass() {
     SuperClass.call(this);
     OtherSuperClass.call(this);
}

// 继承一个类
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 重新指定constructor
MyClass.prototype.constructor = MyClass;

MyClass.prototype.myMethod = function() {
     // do a thing
};
  • Object.assign 会把 OtherSuperClass原型上的函数拷贝到 MyClass原型上,使 MyClass 的所有实例都可用 OtherSuperClass 的方法。
  • Object.assign 是在 ES2015 引入的,且可用 polyfilled。要支持旧浏览器的话,可用使用 jQuery.extend() 或者 _.assign()
与时俱进,红宝书《javascript 高级程序设计第三版》 也并不是无敌的,当然,一下子知识量太大,我们吸收不了,所以这里不展开细说。

三、继承-寄生式继承

在引入寄生组合式继承之前,需要了解什么是寄生式继承。


图片来自:https://2ch.hk/b/arch/2017-01...

寄生式继承的思路跟寄生构造函数模式和工厂模式很类似,核心思想是创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真得是它做了所有工作一样返回对象。

感觉像是原型式继承的升级版!
// 原型式继承的关键-复制
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

function createFood(original) {
  var clone = object(original);
  clone.sayName = function(name) {
    console.log(name);
  };
  return clone;
}

var food1 = {
  name: "苹果"
};
var food2 = createFood(food1);
console.log(food2.name); // 返回苹果
food2.sayName("香蕉"); // 返回香蕉
  • 可以看到 name 属性是没有变化的,可以将一些共享的属性放在里面来形成复制。
  • 这里需要注意如果需要给添加的新函数传参的话,是不可以在”克隆“的时候传的,需要在外面使用的时候传。
这是一种比较简单的实现继承的方式,在不考虑自定义类型和构造函数的情况下,也算是一种有用的模式。

四、继承-寄生组合式继承

终于到了主角了。


图片来自:https://www.9yread.com/book/1...

寄生组合式继承的核心思想是:

  • 通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
  • 其背后的思路是不必为了指定子类型的原型而调用超类型的构造函数。
  • 使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

好复杂的解释,先看看代码吧:

// object 函数可以用 Object.create 来代替。
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

// 这里是关键
function inheritPrototype(subType, superType) {
  // ①将超类原型放到一个临时的对象里面(创建超类型圆形的副本)
  var prototype = object(superType.prototype);
  // ②重新指定这个临时对象的constructor 为 子类构造函数
  prototype.constructor = subType;
  // ③将这个临时对象赋值给子类的原型对象
  subType.prototype = prototype;
}

function Food(name) {
  this.name = name;
  this.colors = ["red", "blue"];
}

Food.prototype.sayName = function() {
  console.log(this.name);
};

function Fruit(name, place) {
  Food.call(this, name);
  this.place = place;
}

inheritPrototype(Fruit, Food);
Fruit.prototype.sayPlace = function() {
  console.log(this.place);
};

var food1 = new Fruit("苹果", "非洲");
var food2 = new Fruit("香蕉", "亚洲");
console.log(food1.sayName()); // 返回 苹果
console.log(food1.sayPlace()); // 返回 非洲

food1.colors.push("black");
console.log(food1.colors); // 返回 [ 'red', 'blue', 'black' ]
console.log(food2.colors); // 返回 [ 'red', 'blue' ]

console.log(food1 instanceof Fruit); // 返回 true
console.log(food1 instanceof Food); // 返回 true
console.log(Fruit.prototype.isPrototypeOf(food1)); // 返回 true
console.log(Food.prototype.isPrototypeOf(food1)); // 返回 true
object 函数可以用 Object.create来代替。

借助这个图理解一下,这种继承模式拆开来看就是寄生式(复制)+组合式(原型链+构造函数)


图片来自https://www.jianshu.com/p/004...

  • 原型链没被切断,是因为是用了寄生(复制)的方式来进行超类原型对象的复制,整个复制的话,会保存它的原型链,然后将这个复制出来的原型对象直接赋值给子类,所以原型链是完整的。
  • 没有出现之前组合继承的两次调用问题,是因为它有一个中间临时过渡的对象,省去了一次调用构造父类函数的机会。
  • 没有出现引用类型的值共享问题,是因为在寄生(复制)之后才可以用原型链+构造函数的,这样就很好的隔离了超类和子类的引用类型的值的问题了。

总结

几乎涵盖了所有 javascript 的继承模式了:

图片来自:https://zhuanlan.zhihu.com/p/...

有几点是我觉得可以总结一下,前人栽树,后人乘凉:

  1. 书不要读死,如果单纯读《javascript 高级程序设计第三版》是不可能完整了解 javascript 的,起码在面向对象这部分是不行的,很多网上的大(zhuang)牛(bi)都会叫你认真阅读这本书,但是对于初学者来说,基本是很难理解得到作者的思路和意思的,不是资质问题,是阅历和经验和知识含量不足的限制。
  2. 看不懂,不要紧,多看,多查阅资料,记得用 google 查,baidu 只会让你多了解一些广告罢了。
  3. 网上的文章质量也是参差不齐的,就算是我这篇装逼文,也是我自己觉得很好,但是未必能够面面俱到,但是人生本来就难以面面俱到,不是吗?重要的是,我用我的经验写了,你能看明白一些是一些,看不明白就当饭后尔尔罢了,不用纠结。
  4. 要自己做实验,自己输出一些结果,对比理论,对比别人的结果和分析,这样才能理解得好一些。
  5. 学习第一次发现完全懵逼的话,就尝试去组织一个脉络结构,就好像我这样,尝试做一个故事代入,一环扣一环来理解,虽然《javascript 高级程序设计第三版》这本书里面也有,但是感觉后面开始省略很多一部分了,以致迷失了。
  6. 不要怕,多学习,莫道前路无知己,天下谁人不识君,加油加油,也是自勉。

参考内容

  1. 红宝书,javascript 高级程序设计第三版

原文转载:
https://www.godblessyuan.com/...

查看原文

赞 0 收藏 0 评论 0

线上猛如虎 发布了文章 · 2018-10-13

我来重新学习js的面向对象(part 4)

我来重新学习js 的面向对象(part 4)

续上一篇,随着业务越来越大,要考虑一些继承的玩意了,大千世界,各种东西我们要认识和甄别是需要靠大智慧去分门别类,生物学中把动植物按界、门、纲、目、科、属、种进行分类的方法可能是最有代表的实例之一.........

说人话就是,我们终于要学习继承的知识了,然后用这些知识去解决老板的问题。

一、继承-原型链

继承是 OOP 开发中的一个极为重要的概念,而在javascript 里面,实现继承的方式主要依靠原型链来实现的。


图片来自:https://www.lieyuncj.com/p/3087

图一,一环扣一环,形成了链条,可以适当帮助理解原型链的概念,原型链,换言之就是原型对象构成的链。


图片来源于:https://hackernoon.com/unders...

回顾一下,构造函数,原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针,当我们将原型对象等于另外一个类型的实例的时候,就会出现原型对象包含一个指向另外一个原型的指针,例如 dog原型对象 指向了 animal原型对象。

继续回到现场,我们做了一些分类,食物下面分了水果分类:

// 定义一个 Food 的构造函数
function Food() {
  this.type = "食物";
}
// 定义了 Food 的原型对象的一个方法 getType
Food.prototype.getType = function() {
  return this.type;
};
// 定义一个 Fruit 的构造函数
function Fruit() {
  this.type = "水果";
}
// 将 Fruit 的原型对象指向 Food 的实例
Fruit.prototype = new Food();
// 定义 Fruit 的原型对象的一个方法 getType
Fruit.prototype.getType = function() {
  return this.type;
};

var food1 = new Fruit();
console.log(food1.getType()); // 返回 水果
  • 前半段都是一样的,直至将 Fruit 的原型对象指向 Food 的实例,于是Fruit原型不仅拥有了 Food 实例的全部属性和方法,也拥有了 Food 实例的原型对象(因为 Food 实例里面有 prototype 指向Food Prototype
  • 这种粗暴的直接将父对象的实例塞进去子对象的原型里面的方式,直接促成了Fruit 继承 Food。

我最喜欢用《javascript 高级程序设计》第三版的图来说明,因为他画的比较详细而且容易看明白(虽然我也是看了十来遍才看懂),借用他的例子和图来解释我们的例子:

可以看到现在这里子对象 subtype 的 原型对象是 superType,因为也是直接粗暴的塞进去的。

如果要看完整的他的原型链,可以参看这个图:

相当详细,这里之所以有 Object 是因为 javascript 里面一切皆是对象,默认的最顶级的原型就是 Object Prototype(怎么看这个图,可以翻看之前一集介绍原型的内容)

下面需要注意一些原型对象的问题和技巧

1.1 确定原型和实例的关系

没办法准确知道是继承于哪一个,只要是在链条里面的,都会被认为是继承过来的。

console.log(food1 instanceof Fruit) // 返回 true
console.log(food1 instanceof Food) // 返回 true
console.log(food1 instanceof Object) // 返回 true

console.log(Fruit.prototype.isPrototypeOf(food1)) // 返回 true
console.log(Food.prototype.isPrototypeOf(food1)) // 返回 true
console.log(Object.prototype.isPrototypeOf(food1)) // 返回 true
这里也跟javascript 的原型搜索机制有关系,当访问一个实例属性时候,首先会在实例中搜索该属性,如果没有找到该属性,就会继续搜索实例的原型对象,在通过原型链实现继承的情况下,搜索过程就会一直沿着原型链继续向上搜索。

类似下图:

图片来源于:http://www.cnblogs.com/keepfo...

1.2 谨慎定义方法

① 给原型添加方法的代码一定要放在替换原型的语句之后

正确的例子:

// 定义一个 Food 的构造函数
function Food() {
  this.type = "食物";
}
// 定义了 Food 的原型对象的一个方法 getType
Food.prototype.getType = function() {
  return "food 的 getType 方法";
};
// 定义一个 Fruit 的构造函数
function Fruit() {
  this.type = "水果";
}
// 将 Fruit 的原型对象指向 Food 的实例
Fruit.prototype = new Food();
// 给子类 Fruit 的原型添加一个新方法getSubType
Fruit.prototype.getSubType = function() {
  return "Fruit 的getSubType";
};
// 重写父类 Food 的方法getType
Food.prototype.getType = function() {
  return false;
};
var food1 = new Fruit();

console.log(food1.getSubType()); // 返回 Fruit 的getSubType
console.log(food1.getType()); // 返回 false
  • 子类 Fruit 重写父类(超类)的原型对象的方法getType,在调用的时候会覆盖屌父类 Food的原型对象的getType方法,直接使用子类Fruit的getType
  • 子类 Fruit 添加一个方法到自己的原型对象里面,也是很正常的,能够被直接使用。

错误的例子:

// 定义一个 Food 的构造函数
function Food() {
  this.type = "食物";
}
// 定义了 Food 的原型对象的一个方法 getType
Food.prototype.getType = function() {
  return "food 的 getType 方法";
};
// 定义一个 Fruit 的构造函数
function Fruit() {
  this.type = "水果";
}
// 给子类 Fruit 的原型添加一个新方法getSubType
Fruit.prototype.getSubType = function() {
  return "Fruit 的getSubType";
};
// 重写父类 Food 的方法getType
Food.prototype.getType = function() {
  return false;
};
// 将 Fruit 的原型对象指向 Food 的实例
Fruit.prototype = new Food();

var food1 = new Fruit();

console.log(food1.getSubType()); // 抛出 error 异常
console.log(food1.getType()); // 返回 false
  • food1.getSubType() 直接抛出异常,提示说方法找不到或者未定义
主要就是因为子原型对象被替换的时候会被完全覆盖。

1.3 在通过原型链实现继承时,不能使用对象字面量方法创建原型

主要是因为对象字面量方法会重写原型链,这个原理在之前章节说过,这里只是再次提醒。

// 省略。。。
Fruit.prototype = new Food();

Fruit.prototype = { // 被重写了原型链,就不属于原来的原型链范围了。
//  xxxxxxx
}
// 省略。。。

1.4 原型链的问题

  1. 原型链最大的问题是来自包含引用类型值的原型,这种类型值的原型属性会被所有实例共享,导致没办法很好隔离,所以之前也是使用构造函数和原型模式组合使用来解决这个问题,但当时没有触及真正的继承。
  2. 原型链另外一个问题是,在创建子类型的实例时,不能向超类型的构造函数中传递参数,或者说,是没办法在不影响所有对象实例情况下,给超类型的构造函数传递参数。
基于以上2个问题,导致了实际环境中,很少会单独使用原型链,会结合其他方式来使用原型链,毕竟 javascript 里,所有的继承其实也是以原型链为基础的。

二、继承-借用构造函数、伪造对象、经典继承


图片来自:https://www.tvmao.com/drama/K...

鉴于之前原型链的问题两大问题,所以机智的工程师想出来利用构造函数来搭配使用,这个技术就叫做借用构造函数 constructor stealing(很 low 有没有!),有时候叫伪造对象,或者叫经典继承(逼格瞬间飙升到完全看不懂,但觉得很厉害,有木有!)

核心思想是在子类型构造函数的内部调用超类型改造函数。

单纯使用原型链继承的时候:

function Food() {
  this.colors = ["red", "blue"];
}

function Fruit() {}

Fruit.prototype = new Food();

var food1 = new Fruit();
var food2 = new Fruit();
console.log(food1.colors); // 返回 [ 'red', 'blue' ]
console.log(food2.colors); // 返回 [ 'red', 'blue' ]
food1.colors.push("yellow");
console.log(food1.colors); // 返回 [ 'red', 'blue', 'yellow' ]
console.log(food2.colors); // 返回 [ 'red', 'blue', 'yellow' ]

使用借用构造函数模式继承的时候:

function Food() {
  this.colors = ["red", "blue"];
}

function Fruit() {
  Food.call(this); // call 可以改变函数的this对象的指向
}

var food1 = new Fruit();
console.log(food1.colors); // 返回 [ 'red', 'blue' ]

food1.colors.push("yellow");
console.log(food1.colors); // 返回 [ 'red', 'blue', 'yellow' ]

var food2 = new Fruit();
console.log(food2.colors); // 返回 [ 'red', 'blue' ]

可以看到截然不同的两种效果,后者的实例的数组(引用类型的数据)并没有跟随其他实例变化而变化,是互相独立的。

为什么可以这样呢?

  • 利用了函数的执行环境上下文,这里的“继承”的目的只是为了能够使用超类的属性和方法(不算是真正的继承),所以直接将超类的构造函数放到子类的构造函数里面执行,从而将他们进行合体。
  • 利用了 call(或者 apply 或者 bind 这种函数)改变了构造函数的 this 指向,才得以实现上面说到的将不同的构造函数放到同一个执行环境中执行。

2.1 传参

下面两个例子分别说明了,这种继承方式可以传参的,并且传参之后也是可以重写超类的属性的。

例子1:

function Food(name) {
  this.name = name;
  this.colors = ["red", "blue"];
}

function Fruit() {
  Food.call(this, "苹果"); // call 可以改变函数的this对象的指向
}

var food1 = new Fruit();
console.log(food1.name); // 返回 苹果

例子2:

function Food(name) { // 参数
  this.name = name;
  this.colors = ["red", "blue"];
}

function Fruit() {
  Food.call(this, "苹果"); // call 可以改变函数的this对象的指向,加上了传参
  this.place = "非洲"; // 添加属性
  this.name = "香蕉"; // 重写超类属性
}

var food1 = new Fruit();
console.log(food1.name); // 返回 苹果
console.log(food1.place); // 返回 非洲

2.2 这种方式的问题


图片来自:https://www.youtube.com/watch...

正如之前所说,这种不是真正的继承,只是想子类和父类进行了强行合体罢了,这种合体方式能够满足一般继承的要求,但是带了其他问题:

  • 没办法使用超类的原型对象里面定义的方法。
function Food() {
  this.colors = ["red", "blue"];
}
Food.prototype.getType = function () {
  console.log("我是 food 的getType");
}
function Fruit() {
  Food.call(this); // call 可以改变函数的this对象的指向
}

var food1 = new Fruit();
console.log(food1.getType()); // 抛出异常,没有这个 function
  • 因为子类和超类都是构造函数,那么就会有之前说的,构造函数在 new 的时候,里面的方法(函数)会重复创建 function 实例, 导致资源浪费。
function Food() {
  this.colors = ["red", "blue"];
}

function Fruit() {
  Food.call(this); // call 可以改变函数的this对象的指向
  this.getType = function() {
    console.log("我是 food 的getType");
  };
}

var food1 = new Fruit();
var food2 = new Fruit();

console.log(food1.getType == food2.getType); // 返回 false
鉴于这种问题,在小规模程序设计里面还好,但是一旦规模稍微变得复杂之后,就没法控制代码了,那我们机智的工程师们还要继续想想办法。

参考内容

  1. 红宝书,javascript 高级程序设计第三版

原文转载:https://www.godblessyuan.com/...

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 330 次点赞
  • 获得 67 枚徽章 获得 4 枚金徽章, 获得 21 枚银徽章, 获得 42 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2014-01-20
个人主页被 3.6k 人浏览