Nyaooo

Nyaooo 查看完整档案

红河编辑青岛大学  |  物联网工程 编辑  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

Nyaooo 收藏了文章 · 9月7日

基于element-ui el-table 开发虚拟列表(树形列表)

前言

在这里插入图片描述
基于之前支持表单验证的el-table开发完成后,在数据量过大的时候,会出现渲染慢,表格卡顿等致命问题,而element-ui的el-table本身没有像antd一样提供虚拟列表的demo和相关支持,因此本文在上次的开发基础上,继而开展虚拟列表的开发。本次分为普通列表和树形列表两种,树形在普通列表上面多了一些情况考虑,例如展开收缩等。

虚拟列表

虚拟列表简单概述就是滚动分页,通过有限的视口来切片大量的数据,因为相比于js运算,渲染是一个很慢的过程,因此通过一定的js计算,保证更少的数据渲染,通常可以获得更好的用户体验。一般虚拟列表可以通过上下的动态padding值,来是滚动区域一直显示当前切片出的数据,以及通过transform的方法来动态移动可视区。transform这种方法理论上性能要好一些,因为浏览器渲染本身是分图层渲染的,而transform操作的视图,会被浏览器单独分层出来,渲染性能更优。
下图两种方式:
在这里插入图片描述
本次el-table 上的虚拟列表,采用了padding的方案,原因是transform 会使el-table的样式混乱,如果是自己开发的table或者其他魔改支持度较好的插件的话优先transform。

普通列表

首先看一下el-table 渲染300 条的速度。
在这里插入图片描述
本次的测试代码有300条,本身并不多,但是有8列都是插槽中渲染的表单组件,因此渲染速度要慢很多,时间花销6s+。(antd 的table渲染要快一些,后面说原因)

增加虚拟列表后渲染速度:
在这里插入图片描述

开发流程

step1

计算总高度

height = list.length * 65 
// height 为列表实际总高度
// 65 为每一行的行高,根据实际修改
// list为实际数据长度

step2

计算上下padding值

paddingTop = scrollTop + "px";
paddingBottom = height - 10 * 65 - scrollTop + "px";
// scrollTop 为滚动的高度,即列表向下滚动的距离
// height 总高度 
// 10为实际渲染的条数 

step3

监听列表滚动,动态为列表设置padding等样式。

 mounted() {
    console.time("render300条时间:");
    this.form.rows = new Array(300).fill(0).map((v, i) => ({
      name: i,
      children: []
    }));
    this.form.rows = [...this.form.rows];
    this.setIndex(this.form.rows);
    this.calcList();
    this.$nextTick(() => {
      this.debounceFn = _.debounce(() => {
        this.scrollTop = this.$refs.table.bodyWrapper.scrollTop;
      }, 100);
      this.$refs.table.bodyWrapper.addEventListener("scroll", this.debounceFn);
    });
    this.$nextTick(() => {
      console.timeEnd("render300条时间:");
    });
  },

监听的目标是这个: this.$refs.table.bodyWrapper,防抖的时间设置为100。

step4

数组切片,渲染虚拟列表。

 this.startIndex = Math.floor(scrollTop / 65);
 this.virtualRows = this.form.rows.slice(
        this.startIndex,
        this.startIndex + 10
      );

根据滚动位置计算数组切片的起始点,然后截取相应的list渲染。

支持列fixed(Table-column Attributes - fixed)

上面说到el-table要比antd的table渲染更慢,其中一条原因我个人认为是,el-table 在支持左右固定列的时候会克隆一份table,然后按照层级关系,使得UI上看到左右列的固定。如果左右都设置了fixed,就会有三个table同时在页面上。
在这里插入图片描述
而 Antd的table组件在左右fixed时就不会有这个问题,因此本人亲测在300条相同数据的情境下,Antd的性能要好不少。言归正传,要解决fixed的问题,就是要把这三个table的padding都去设置一遍才行,否则就会出现部分区域没有被顶下来而错位的情况。

 let mainTable = this.$refs.table.$el.getElementsByClassName(
        "el-table__body"
      );
      Array.from(mainTable).forEach(v => {
        v.style.height = height + "px";
        if (this.startIndex + 10 >= this.num) {
          // 由于el-table 在滚动到最后时,会出现抖动,因此增加判断,单独设置属性
          v.style.paddingTop = scrollTop - 65 + "px";
          v.style.paddingBottom = 0;
        } else {
          v.style.paddingTop = scrollTop + "px";
          v.style.paddingBottom = height - 10 * 65 - scrollTop + "px";
        }
      });

找到当前table下的所有内容区域,遍历设置样式属性。

树形列表

树形列表由于多一步展开折叠的操作,以及本身数据结构的原因,数据预处理要复杂一下,不能直接slice,而要计算出相应区间然后生成新的数组。其次,在被收缩的子项是不渲染到table当中的,因此,要把被收缩的项排除在外。除了普通列表的几个step之外,树形列表还需有以下操作。

数组切片

通过滚动计算出的起始点,以及可视区域的列表长度,可以得到一个区间,如【3,11】,即通过深度优先遍历(也是树形列表排列的顺序),找到第3到11条数据(不包含被折叠项),然后赋值到新的数组。

clacTree() {
      let count = 0;
      this.virtualRows = [];
      this.listLen = 0;
      const fn = arr => {
        for (let i = 0; i < arr.length; i++) {
          count++;
          this.listLen++;
          if (count >= this.startIndex && count <= this.startIndex + 10) {
            this.combineArr(_.cloneDeep(arr[i]));
          }
          arr[i].children && arr[i].expended === "true" && fn(arr[i].children);
        }
      };
      fn(this.form.rows);
    },
    combineArr(node) {
      let flag = false;
      node.children = [];
      const fn = arr => {
        arr.forEach(v => {
          if (node.pid === v.customIndex) {
            v.children.push(node);
            flag = true;
          }
          v.children && fn(v.children);
        });
      };
      fn(this.virtualRows);
      if (!flag) {
        this.virtualRows.push(node);
      }
    },

这里只对展开项进行操作,未展开的不去遍历和渲染,总高度也不计入。新数组赋值的时候,我通过二次遍历新的数组,再根据pid去push到相应位置,这种做法是因为实际业务需要,二次遍历中还有部分属性需要保持引用,以及部分属性是不可枚举的,深拷贝会丢失,如果只是截取树的一部分形成新的树,可以根据初始化得到的path属性,然后利用lodash的_.set 来完成。

展开收缩

el-table 的 expand-row-keys 传入一个数组,为默认的展开项,之后每次渲染都参考这个数组来决定列表是否展开,这个属性不能自动在展开收缩的时候把设置的 row-key 推入推出,而要手动的计算。在@expand-change事件中,来操作数组,以及判断被收缩的项有没有子集,如果有子集要给一个标记位,来为之后的列表渲染做准备。

 expendRow(rows, expended) {
      // const
      this.DFS_Array(this.form.rows, v => {
        if (v.customIndex == rows.customIndex) {
          v.expended = String(expended);
          v.hasChild =
            v.expended === "false" && v.children.length > 0 ? true : false;
        }
      });
      if (!expended) {
        this.expendArrs = this.expendArrs.filter(v => v !== rows.customIndex);
      } else {
        this.expendArrs.push(rows.customIndex);
      }
      this.calcList(this.scrollTop);
  },
  DFS_Array(arr, fn) {
      for (let i = 0; i < arr.length; i++) {
        fn(arr[i]);
        if (arr[i].children && arr[i].children.length > 0) {
          this.DFS_Array(arr[i].children, fn);
        }
      }
    }

在收缩之后由于把列表中的children整个移除,所以在el-table上面的展开箭头就不能正常显示了,因为在渲染数据中并没有子节点,而实际数据中又是有子集的,所以,在上面增加的hasChild属性,就起到这个作用,他标记了数据被折叠,且有子集可展开的情况。因此,需要在列表中主动把展开的箭头加一下。

 <el-table-column
            prop="customIndex"
            fixed
            label="序号"
            sortable
            width="180"
            v-slot="{$index, row}"
          >
            <span class="expanded-icon-box">
              <i class="expanded-icon" v-if="row.hasChild" @click="expendRow(row,true)">></i>
              {{row.customIndex}}
            </span>
          </el-table-column>

至此 树形列表的虚拟列表也整合完毕了。本次示例代码很多地方比较仓促,待优化情景较多,除了拼凑新的树那里,还有滚动的缓存,如果树比较大的话,js的计算时间也要考虑入内,还有渲染的虚拟列表应该不从本身的第一位开始进入视口,这样的话,在一定范围的向上向下滚动,就可以一定程度的减少白屏。

总结

虚拟列表通过减少实际渲染数据来优化性能,在不对element-ui做较大改动的情况下,满足了大量数据,包括树形的结构数据的渲染场景。如果考虑之前的列表的表单验证的情景,需要让部分属性脱离引用,如children,否则会污染源数据,其次让表单数据保持引用关联,这样就不必专门给表单组件设置事件,来匹配源数据的改动,即直接将新的列表的item的表单对象等于老的相应表单对象即可。

查看原文

Nyaooo 收藏了文章 · 3月17日

万字长文带你深度解锁Webpack系列(进阶篇)

如果你还没有阅读《4W字长文带你深度解锁Webpack系列(基础篇)》,建议阅读之后,再继续阅读本篇文章。

本文会引入更多的 webpack 配置,如果文中有任何错误,欢迎在评论区指正,我会尽快修正。 webpack 优化部分放在了下一篇。

推荐大家参考本文一步一步进行配置,不要总是想着找什么最佳配置,你掌握了之后,根据自己的需求配置出来的,就是最佳配置。

本文对应的项目地址(编写本文时使用) 供参考:https://github.com/YvetteLau/...

1. 静态资源拷贝

有些时候,我们需要使用已有的JS文件、CSS文件(本地文件),但是不需要 webpack 编译。例如,我们在 public/index.html 中引入了 public 目录下的 jscss 文件。这个时候,如果直接打包,那么在构建出来之后,肯定是找不到对应的 js / css 了。

public 目录结构
├── public
│   ├── config.js
│   ├── index.html
│   ├── js
│   │   ├── base.js
│   │   └── other.js
│   └── login.html

现在,我们在 index.html 中引入了 ./js/base.js

<!-- index.html -->
<script data-original="./js/base.js"></script>

这时候,我们 npm run dev,会发现有找不到该资源文件的报错信息。

对于这个问题,我们可以手动将其拷贝至构建目录,然后在配置 CleanWebpackPlugin 时,注意不要清空对应的文件或文件夹即可,但是如若这个静态文件时不时的还会修改下,那么依赖于手动拷贝,是很容易出问题的。

不要过于相信自己的记性,依赖于手动拷贝的方式,大多数人应该都有过忘记拷贝的经历,你要是说你从来没忘过。

050a81c7-59e4-4596-b08f-62cefce353d0.jpg

幸运的是,webpack 为我们这些记性不好又爱偷懒的人提供了好用的插件 CopyWebpackPlugin,它的作用就是将单个文件或整个目录复制到构建目录。

首先安装一下依赖:

npm install copy-webpack-plugin -D

修改配置(当前,需要做的是将 public/js 目录拷贝至 dist/js 目录):

//webpack.config.js
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
    //...
    plugins: [
        new CopyWebpackPlugin([
            {
                from: 'public/js/*.js',
                to: path.resolve(__dirname, 'dist', 'js'),
                flatten: true,
            },
            //还可以继续配置其它要拷贝的文件
        ])
    ]
}

此时,重新执行 npm run dev,报错信息已经消失。

这里说一下 flatten 这个参数,设置为 true,那么它只会拷贝文件,而不会把文件夹路径都拷贝上,大家可以不设置 flatten 时,看下构建结果。

另外,如果我们要拷贝一个目录下的很多文件,但是想过滤掉某个或某些文件,那么 CopyWebpackPlugin 还为我们提供了 ignore 参数。

//webpack.config.js
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
    //...
    plugins: [
        new CopyWebpackPlugin([
            {
                from: 'public/js/*.js',
                to: path.resolve(__dirname, 'dist', 'js'),
                flatten: true,
            }
        ], {
            ignore: ['other.js']
        })
    ]
}

例如,这里我们忽略掉 js 目录下的 other.js 文件,使用 npm run build 构建,可以看到 dist/js 下不会出现 other.js 文件。 CopyWebpackPlugin 还提供了很多其它的参数,如果当前的配置不能满足你,可以查阅文档进一步修改配置。

2.ProvidePlugin

ProvidePlugin 在我看来,是为懒人准备的,不过也别过度使用,毕竟全局变量不是什么“好东西”。ProvidePlugin 的作用就是不需要 importrequire 就可以在项目中到处使用。

ProvidePluginwebpack 的内置插件,使用方式如下:

new webpack.ProvidePlugin({
  identifier1: 'module1',
  identifier2: ['module2', 'property2']
});

默认寻找路径是当前文件夹 ./**node_modules,当然啦,你可以指定全路径。

React 大家都知道的,使用的时候,要在每个文件中引入 React,不然立刻抛错给你看。还有就是 jquery, lodash 这样的库,可能在多个文件中使用,但是懒得每次都引入,好嘛,一起来偷个懒,修改下 webpack 的配置:

const webpack = require('webpack');
module.exports = {
    //...
    plugins: [
        new webpack.ProvidePlugin({
            React: 'react',
            Component: ['react', 'Component'],
            Vue: ['vue/dist/vue.esm.js', 'default'],
            $: 'jquery',
            _map: ['lodash', 'map']
        })
    ]
}

这样配置之后,你就可以在项目中随心所欲的使用 $_map了,并且写 React 组件时,也不需要 importReactComponent 了,如果你想的话,你还可以把 ReactHooks 都配置在这里。

另外呢,Vue 的配置后面多了一个 default,这是因为 vue.esm.js 中使用的是 export default 导出的,对于这种,必须要指定 defaultReact 使用的是 module.exports 导出的,因此不要写 default

另外,就是如果你项目启动了 eslint 的话,记得修改下 eslint 的配置文件,增加以下配置:

{
    "globals": {
        "React": true,
        "Vue": true,
        //....
    }
}

当然啦,偷懒要有个度,你要是配一大堆全局变量,最终可能会给自己带来麻烦,对自己配置的全局变量一定要负责到底。

u=2243033496,1576809017&fm=15&gp=0.jpg

3.抽离CSS

CSS打包我们前面已经说过了,不过呢,有些时候,我们可能会有抽离CSS的需求,即将CSS文件单独打包,这可能是因为打包成一个JS文件太大,影响加载速度,也有可能是为了缓存(例如,只有JS部分有改动),还有可能就是“我高兴”:我想抽离就抽离,谁也管不着。

不管你是因为什么原因要抽离CSS,只要你有需求,我们就可以去实现。

首先,安装 loader:

npm install mini-css-extract-plugin -D
mini-css-extract-pluginextract-text-webpack-plugin 相比:
  1. 异步加载
  2. 不会重复编译(性能更好)
  3. 更容易使用
  4. 只适用CSS

修改我们的配置文件:

//webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
    plugins: [
        new MiniCssExtractPlugin({
            filename: 'css/[name].css' //个人习惯将css文件放在单独目录下
        })
    ],
    module: {
        rules: [
            {
                test: /\.(le|c)ss$/,
                use: [
                    MiniCssExtractPlugin.loader, //替换之前的 style-loader
                    'css-loader', {
                        loader: 'postcss-loader',
                        options: {
                            plugins: function () {
                                return [
                                    require('autoprefixer')({
                                        "overrideBrowserslist": [
                                            "defaults"
                                        ]
                                    })
                                ]
                            }
                        }
                    }, 'less-loader'
                ],
                exclude: /node_modules/
            }
        ]
    }
}

现在,我们重新编译:npm run build,目录结构如下所示:

.
├── dist
│   ├── assets
│   │   ├── alita_e09b5c.jpg
│   │   └── thor_e09b5c.jpeg
│   ├── css
│   │   ├── index.css
│   │   └── index.css.map
│   ├── bundle.fb6d0c.js
│   ├── bundle.fb6d0c.js.map
│   └── index.html

前面说了最好新建一个 .browserslistrc 文件,这样可以多个 loader 共享配置,所以,动手在根目录下新建文件 (.browserslistrc),内容如下(你可以根据自己项目需求,修改为其它的配置):

last 2 version
> 0.25%
not dead

修改 webpack.config.js

//webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
    //...
    plugins: [
        new MiniCssExtractPlugin({
            filename: 'css/[name].css' 
        })
    ],
    module: {
        rules: [
            {
                test: /\.(c|le)ss$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader', {
                        loader: 'postcss-loader',
                        options: {
                            plugins: function () {
                                return [
                                    require('autoprefixer')()
                                ]
                            }
                        }
                    }, 'less-loader'
                ],
                exclude: /node_modules/
            },
        ]
    }
}

要测试自己的 .browserlistrc 有没有生效也很简单,直接将文件内容修改为 last 1 Chrome versions ,然后对比修改前后的构建出的结果,就能看出来啦。

可以查看更多[browserslistrc]配置项(https://github.com/browsersli...

更多配置项,可以查看mini-css-extract-plugin

将抽离出来的css文件进行压缩

使用 mini-css-extract-pluginCSS 文件默认不会被压缩,如果想要压缩,需要配置 optimization,首先安装 optimize-css-assets-webpack-plugin.

npm install optimize-css-assets-webpack-plugin -D

修改webpack配置:

//webpack.config.js
const OptimizeCssPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
    entry: './src/index.js',
    //....
    plugins: [
        new OptimizeCssPlugin()
    ],
}

注意,这里将 OptimizeCssPlugin 直接配置在 plugins 里面,那么 jscss 都能够正常压缩,如果你将这个配置在 optimization,那么需要再配置一下 js 的压缩(开发环境下不需要去做CSS的压缩,因此后面记得将其放到 webpack.config.prod.js 中哈)。

配置完之后,测试的时候发现,抽离之后,修改 css 文件时,第一次页面会刷新,但是第二次页面不会刷新 —— 好嘛,我平时的业务中用不着抽离 css,这个问题搁置了好多天(准确来说是忘记了)。

昨晚(0308)再次修改这篇文章的时候,正好看到了 MiniCssExtractPlugin.loader 对应的 option 设置,我们再次修改下对应的 rule

module.exports = {
    rules: [
        {
            test: /\.(c|le)ss$/,
            use: [
                {
                    loader: MiniCssExtractPlugin.loader,
                    options: {
                        hmr: isDev,
                        reloadAll: true,
                    }
                },
                //...
            ],
            exclude: /node_modules/
        }
    ]
}

4.按需加载

很多时候我们不需要一次性加载所有的JS文件,而应该在不同阶段去加载所需要的代码。webpack内置了强大的分割代码的功能可以实现按需加载。

比如,我们在点击了某个按钮之后,才需要使用使用对应的JS文件中的代码,需要使用 import() 语法:

document.getElementById('btn').onclick = function() {
    import('./handle').then(fn => fn.default());
}

import() 语法,需要 @babel/plugin-syntax-dynamic-import 的插件支持,但是因为当前 @babel/preset-env 预设中已经包含了 @babel/plugin-syntax-dynamic-import,因此我们不需要再单独安装和配置。

直接 npm run build 进行构建,构建结果如下:

WechatIMG1121.jpeg

webpack 遇到 import(****) 这样的语法的时候,会这样处理:

  • ** 为入口新生成一个 Chunk
  • 当代码执行到 import 所在的语句时,才会加载该 Chunk 所对应的文件(如这里的1.bundle.8bf4dc.js)

大家可以在浏览器中的控制台中,在 NetworkTab页 查看文件加载的情况,只有点击之后,才会加载对应的 JS

5.热更新

  1. 首先配置 devServerhottrue
  2. 并且在 plugins 中增加 new webpack.HotModuleReplacementPlugin()
//webpack.config.js
const webpack = require('webpack');
module.exports = {
    //....
    devServer: {
        hot: true
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin() //热更新插件
    ]
}

我们配置了 HotModuleReplacementPlugin 之后,会发现,此时我们修改代码,仍然是整个页面都会刷新。不希望整个页面都刷新,还需要修改入口文件:

  1. 在入口文件中新增:
if(module && module.hot) {
    module.hot.accept()
}

此时,再修改代码,不会造成整个页面的刷新。

6.多页应用打包

有时,我们的应用不一定是一个单页应用,而是一个多页应用,那么如何使用 webpack 进行打包呢。为了生成目录看起来清晰,不生成单独的 map 文件。

//webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    entry: {
        index: './src/index.js',
        login: './src/login.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].[hash:6].js'
    },
    //...
    plugins: [
        new HtmlWebpackPlugin({
            template: './public/index.html',
            filename: 'index.html' //打包后的文件名
        }),
        new HtmlWebpackPlugin({
            template: './public/login.html',
            filename: 'login.html' //打包后的文件名
        }),
    ]
}

如果需要配置多个 HtmlWebpackPlugin,那么 filename 字段不可缺省,否则默认生成的都是 index.html,如果你希望 html 的文件名中也带有 hash,那么直接修改 fliename 字段即可,例如: filename: 'login.[hash:6].html'

生成目录如下:

.
├── dist
│   ├── 2.463ccf.js
│   ├── assets
│   │   └── thor_e09b5c.jpeg
│   ├── css
│   │   ├── index.css
│   │   └── login.css
│   ├── index.463ccf.js
│   ├── index.html
│   ├── js
│   │   └── base.js
│   ├── login.463ccf.js
│   └── login.html

看起来,似乎是OK了,不过呢,查看 index.htmllogin.html 会发现,都同时引入了 index.f7d21a.jslogin.f7d21a.js,通常这不是我们想要的,我们希望,index.html 中只引入 index.f7d21a.jslogin.html 只引入 login.f7d21a.js

HtmlWebpackPlugin 提供了一个 chunks 的参数,可以接受一个数组,配置此参数仅会将数组中指定的js引入到html文件中,此外,如果你需要引入多个JS文件,仅有少数不想引入,还可以指定 excludeChunks 参数,它接受一个数组。

//webpack.config.js
module.exports = {
    //...
    plugins: [
        new HtmlWebpackPlugin({
            template: './public/index.html',
            filename: 'index.html', //打包后的文件名
            chunks: ['index']
        }),
        new HtmlWebpackPlugin({
            template: './public/login.html',
            filename: 'login.html', //打包后的文件名
            chunks: ['login']
        }),
    ]
}

执行 npm run build,可以看到 index.html 中仅引入了 indexJS 文件,而 login.html 中也仅引入了 loginJS 文件,符合我们的预期。

7.resolve 配置

resolve 配置 webpack 如何寻找模块所对应的文件。webpack 内置 JavaScript 模块化语法解析功能,默认会采用模块化标准里约定好的规则去寻找,但你可以根据自己的需要修改默认的规则。

  1. modules

resolve.modules 配置 webpack 去哪些目录下寻找第三方模块,默认情况下,只会去 node_modules 下寻找,如果你我们项目中某个文件夹下的模块经常被导入,不希望写很长的路径,那么就可以通过配置 resolve.modules 来简化。

//webpack.config.js
module.exports = {
    //....
    resolve: {
        modules: ['./src/components', 'node_modules'] //从左到右依次查找
    }
}

这样配置之后,我们 import Dialog from 'dialog',会去寻找 ./src/components/dialog,不再需要使用相对路径导入。如果在 ./src/components 下找不到的话,就会到 node_modules 下寻找。

  1. alias

resolve.alias 配置项通过别名把原导入路径映射成一个新的导入路径,例如:

//webpack.config.js
module.exports = {
    //....
    resolve: {
        alias: {
            'react-native': '@my/react-native-web' //这个包名是我随便写的哈
        }
    }
}

例如,我们有一个依赖 @my/react-native-web 可以实现 react-nativeweb。我们代码一般下面这样:

import { View, ListView, StyleSheet, Animated } from 'react-native';

配置了别名之后,在转 web 时,会从 @my/react-native-web 寻找对应的依赖。

当然啦,如果某个依赖的名字太长了,你也可以给它配置一个短一点的别名,这样用起来比较爽,尤其是带有 scope 的包。

  1. extensions

适配多端的项目中,可能会出现 .web.js, .wx.js,例如在转web的项目中,我们希望首先找 .web.js,如果没有,再找 .js。我们可以这样配置:

//webpack.config.js
module.exports = {
    //....
    resolve: {
        extensions: ['web.js', '.js'] //当然,你还可以配置 .json, .css
    }
}

首先寻找 ../dialog.web.js ,如果不存在的话,再寻找 ../dialog.js。这在适配多端的代码中非常有用,否则,你就需要根据不同的平台去引入文件(以牺牲了速度为代价)。

import dialog from '../dialog';

当然,配置 extensions,我们就可以缺省文件后缀,在导入语句没带文件后缀时,会自动带上extensions 中配置的后缀后,去尝试访问文件是否存在,因此要将高频的后缀放在前面,并且数组不要太长,减少尝试次数。如果没有配置 extensions,默认只会找对对应的js文件。

  1. enforceExtension

如果配置了 resolve.enforceExtensiontrue,那么导入语句不能缺省文件后缀。

  1. mainFields

有一些第三方模块会提供多份代码,例如 bootstrap,可以查看 bootstrappackage.json 文件:

{
    "style": "dist/css/bootstrap.css",
    "sass": "scss/bootstrap.scss",
    "main": "dist/js/bootstrap",
}

resolve.mainFields 默认配置是 ['browser', 'main'],即首先找对应依赖 package.json 中的 brower 字段,如果没有,找 main 字段。

如:import 'bootstrap' 默认情况下,找得是对应的依赖的 package.jsonmain 字段指定的文件,即 dist/js/bootstrap

假设我们希望,import 'bootsrap' 默认去找 css 文件的话,可以配置 resolve.mainFields 为:

//webpack.config.js
module.exports = {
    //....
    resolve: {
        mainFields: ['style', 'main'] 
    }
}

8.区分不同的环境

目前为止我们 webpack 的配置,都定义在了 webpack.config.js 中,对于需要区分是开发环境还是生产环境的情况,我们根据 process.env.NODE_ENV 去进行了区分配置,但是配置文件中如果有多处需要区分环境的配置,这种显然不是一个好办法。

更好的做法是创建多个配置文件,如: webpack.base.jswebpack.dev.jswebpack.prod.js

  • webpack.base.js 定义公共的配置
  • webpack.dev.js:定义开发环境的配置
  • webpack.prod.js:定义生产环境的配置

webpack-merge 专为 webpack 设计,提供了一个 merge 函数,用于连接数组,合并对象。

npm install webpack-merge -D
const merge = require('webpack-merge');
merge({
    devtool: 'cheap-module-eval-source-map',
    module: {
        rules: [
            {a: 1}
        ]
    },
    plugins: [1,2,3]
}, {
    devtool: 'none',
    mode: "production",
    module: {
        rules: [
            {a: 2},
            {b: 1}
        ]
    },
    plugins: [4,5,6],
});
//合并后的结果为
{
    devtool: 'none',
    mode: "production",
    module: {
        rules: [
            {a: 1},
            {a: 2},
            {b: 1}
        ]
    },
    plugins: [1,2,3,4,5,6]
}

webpack.config.base.js 中是通用的 webpack 配置,以 webpack.config.dev.js 为例,如下:

//webpack.config.dev.js
const merge = require('webpack-merge');
const baseWebpackConfig = require('./webpack.config.base');

module.exports = merge(baseWebpackConfig, {
    mode: 'development'
    //...其它的一些配置
});

然后修改我们的 package.json,指定对应的 config 文件:

//package.json
{
    "scripts": {
        "dev": "cross-env NODE_ENV=development webpack-dev-server --config=webpack.config.dev.js",
        "build": "cross-env NODE_ENV=production webpack --config=webpack.config.prod.js"
    },
}

你可以使用 merge 合并,也可以使用 merge.smart 合并,merge.smart 在合并loader时,会将同一匹配规则的进行合并,webpack-merge 的说明文档中给出了详细的示例。

9.定义环境变量

很多时候,我们在开发环境中会使用预发环境或者是本地的域名,生产环境中使用线上域名,我们可以在 webpack 定义环境变量,然后在代码中使用。

使用 webpack 内置插件 DefinePlugin 来定义环境变量。

DefinePlugin 中的每个键,是一个标识符.

  • 如果 value 是一个字符串,会被当做 code 片段
  • 如果 value 不是一个字符串,会被stringify
  • 如果 value 是一个对象,正常对象定义即可
  • 如果 key 中有 typeof,它只针对 typeof 调用定义
//webpack.config.dev.js
const webpack = require('webpack');
module.exports = {
    plugins: [
        new webpack.DefinePlugin({
            DEV: JSON.stringify('dev'), //字符串
            FLAG: 'true' //FLAG 是个布尔类型
        })
    ]
}
//index.js
if(DEV === 'dev') {
    //开发环境
}else {
    //生产环境
}

10.利用webpack解决跨域问题

假设前端在3000端口,服务端在4000端口,我们通过 webpack 配置的方式去实现跨域。

首先,我们在本地创建一个 server.js

let express = require('express');

let app = express();

app.get('/api/user', (req, res) => {
    res.json({name: '刘小夕'});
});

app.listen(4000);

执行代码(run code),现在我们可以在浏览器中访问到此接口: http://localhost:4000/api/user

index.js 中请求 /api/user,修改 index.js 如下:

//需要将 localhost:3000 转发到 localhost:4000(服务端) 端口
fetch("/api/user")
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(err => console.log(err));

我们希望通过配置代理的方式,去访问 4000 的接口。

配置代理

修改 webpack 配置:

//webpack.config.js
module.exports = {
    //...
    devServer: {
        proxy: {
            "/api": "http://localhost:4000"
        }
    }
}

重新执行 npm run dev,可以看到控制台打印出来了 {name: "刘小夕"},实现了跨域。

大多情况,后端提供的接口并不包含 /api,即:/user/info/list 等,配置代理时,我们不可能罗列出每一个api。

修改我们的服务端代码,并重新执行。

//server.js
let express = require('express');

let app = express();

app.get('/user', (req, res) => {
    res.json({name: '刘小夕'});
});

app.listen(4000);

尽管后端的接口并不包含 /api,我们在请求后端接口时,仍然以 /api 开头,在配置代理时,去掉 /api,修改配置:

//webpack.config.js
module.exports = {
    //...
    devServer: {
        proxy: {
            '/api': {
                target: 'http://localhost:4000',
                pathRewrite: {
                    '/api': ''
                }
            }
        }
    }
}

重新执行 npm run dev,在浏览器中访问: http://localhost:3000/,控制台中也打印出了{name: "刘小夕"},跨域成功,

11.前端模拟数据

简单数据模拟
module.exports = {
    devServer: {
        before(app) {
            app.get('/user', (req, res) => {
                res.json({name: '刘小夕'})
            })
        }
    }
}

src/index.js 中直接请求 /user 接口。

fetch("user")
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(err => console.log(err));
使用 mocker-api mock数据接口

mocker-api 为 REST API 创建模拟 API。在没有实际 REST API 服务器的情况下测试应用程序时,它会很有用。

  1. 安装 mocker-api:
npm install mocker-api -D
  1. 在项目中新建mock文件夹,新建 mocker.js.文件,文件如下:
module.exports = {
    'GET /user': {name: '刘小夕'},
    'POST /login/account': (req, res) => {
        const { password, username } = req.body
        if (password === '888888' && username === 'admin') {
            return res.send({
                status: 'ok',
                code: 0,
                token: 'sdfsdfsdfdsf',
                data: { id: 1, name: '刘小夕' }
            })
        } else {
            return res.send({ status: 'error', code: 403 })
        }
    }
}
  1. 修改 webpack.config.base.js:
const apiMocker = require('mocker-api');
module.export = {
    //...
    devServer: {
        before(app){
            apiMocker(app, path.resolve('./mock/mocker.js'))
        }
    }
}

这样,我们就可以直接在代码中像请求后端接口一样对mock数据进行请求。

  1. 重启 npm run dev,可以看到,控制台成功打印出来 {name: '刘小夕'}
  2. 我们再修改下 src/index.js,检查下POST接口是否成功
//src/index.js
fetch("/login/account", {
    method: "POST",
    headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        username: "admin",
        password: "888888"
    })
})
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(err => console.log(err));

可以在控制台中看到接口返回的成功的数据。

进阶篇就到这里结束啦,下周约优化篇。

最后

关注公众号

参考:
查看原文

Nyaooo 收藏了文章 · 3月11日

hooks vs class component 之争

我与疫情

2020 转眼已来到3月,但疫情的突袭,让这个春节迟迟没有开始,也没法结束。这段时间看似是充电自我提升的大好时光,但家国情怀深厚的我为疫情真的是操碎了心,时不时都要看看哪里数据猛增了,哪里暴发了。结束一个月的在家办公,带着口罩在公司上班,状态稍有好转,注意力终归回到了技术。

最近在组里推BFF Node接入与微前端改造,看到了组里各式各样实现地业务代码(组件),有激进开放的整个页面都用hooks实现,有沉迷于过去的停留于redux + saga + model的类组件写法,当然更多的是类组件中的子组件参和几个hooks。我带着好奇之心去goggle了一下这两个谁胜一筹,于是有了此文。

hooks vs class

网上看了很多大佬观点,但脱离业务的争论都是耍流氓。作为眼见为实躺坑无数的练习生,我决定用事实说话。说那么多干嘛,上代码:
慢...,先说说接下来要干的事情:

  • 进入一个列表页面,首次进入,发起请求,获取列表数据;
  • 列表有搜索框,支持条件搜索和重置;
  • 列表支持翻页;
  • 页面弹出一个对话框,用于,新增,修改数据;
  • 修改数据时,需要发起一个请求,去获取详情;

发现没,这就是一个标准的CRUD,页面大致长这样:
image.png

肤浅的看-表面

接下来,列一下两种方式实现,页面的大致结构,橙色方框内为Hook重构后的页面结构:
image.png
Hook重构后代码变化主要在index.js,删去了对Dva的Model依赖,转由Hook自己管理数据, 代码对比大致如下:
image.png
看代码,主要是数据层的实现有变化,UI保持一致。当页面跑起来,如果不看面包屑的话,也很难分辨谁是Hooks组件的实现,谁是类组件的实现,所以表面的看,是看不出谁高谁低的。

仔细的看-性能

毕竟人的肉眼,只有当低于30fps时,才能明显的观察到变化;所以要想客观的对比性能,得依赖Chrome的性能分析(Performerce);这里做两个对比:

  1. 从其他菜单(主页)跳转到列表页
  2. 从列表页到打开详情编辑Modal;

跳转到列表页
image.png

打开编辑页
image.png

为保证试验结果严谨,我尽量保证唯一性变量原则、多(san)次、与减少自己手抖的次数,但与自动化测试还是有较大差距,十几毫秒的误差再所难免。我观察了列表页切换页的两个图,除开请求的波动于手动测试的误差,两个页面从点击到请求再到页面渲染,时间相差无几,甚至调用栈都是惊人的相似(装逼的说法就是:从原理上分析,也应该是相似的:dispatch + diff + render)。
也观察了详情编辑Modal打开两个图,多次测试,其打开速度hooks稍暂上风,由于这一块的实现逻辑有比较大的差别(class组件的数据获取是在最外层,获取完然后依次向里传递;而hooks则是从外层获取到id后,组件内部直接发起请求获取数据),所以两者火焰图也有比较大的差异;但从页面渲染的角度整体感觉差别不大。

最后我得出的看法是:Hooks更多是一种管理数据的手段,与class相比,并没有什么性能上的优势,更多的主动权,在编写代码的人手里,就像我驾校老师爱说的那句狗屁不通的谚语:再好的车,给这个二傻子开,都能开熄火。关于更多,可以关注B乎讨论: React hooks 和 Class Component 的性能哪一个更好?
如果对我的测试有疑惑,可以自己动手,我提供示例项目:

我个人始终赞成:框架只是实现业务的手段,在使用成熟的框架前提下,页面的性能完全由司机掌控。(我个人有个观点就是:Vue是自动档,React更像手动档

hooks:useRequest

要想完全脱离redux或mobx,简单使用Hooks中的useState或useEffect来完成页面,其难度还是很大且很难管理,毕竟页面大多数数据源都来自异步请求,所以封装一个useRequest hooks是势在必要的,而且Hooks最大的优势就是逻辑复用。以下将分享部门封装useRequest组件的思考过程,其思路参考于Apollo-Graphqlreact-hooks项目。先看示例代码(上面列表页的部分代码):

export default function Root() {
 const [search, onSearch, onReset] = usePagination({});

 // 请求:admin.closertb.site/rule/query接口
 const { data = {}, loading, error } = useRequest('/rule/query', search);

 const { datas, total } = data;
 const tableProps = {
   search,
   datas,
   fields,
   onSearch,
   total,
   loading,
 };

 const searchProps = {
   fields: searchFields,
   search,
   onReset,
   onSearch,
 };
 return (
   <div>
     <WithSearch {...searchProps} />
     <div className="pageContent">
       <EnhanceTable {...tableProps} />
     </div>
   </div>
 );
}

以上就是一个最常见的useRequest hook应用,用非常简短的代码替换了Dva中的路由监听(subscription), 异步请求(effect),数据更新(reducer)等一连串逻辑;下图是一个简单的流程示意图:
image.png

根据这个示意图,老司机应该就大概知道怎么实现的了:

  • 运用useRef 来缓存请求实例,即每个useRequest仅创建一个query实例,页面更新时,沿用已经存在的示例;
  • 运用useMemo做计算,判断请求参数是否更新;
  • 运用useEffect, 用useMemo计算结果作为依赖,判断是否发起请求;
  • 当然,更新页面,采用了一个自增的useReducer;

现在留下的唯一疑问就是,发布订阅怎么实现的,看一下代码:

// query 示例其中的两个核心方法:
  startQuery() {
    const { url, body, forceUpdate, result } = this;
    if (this.status > STATUS.fetch) {
      return;
    }
    // 状态反转为请求中
    this.status = STATUS.fetch;
    result.loading = true;
    // 发起请求
    this.request(url, body).then((data) => {
      // 更新结果
      result.data = data;
      result.loading = false;
      this.status = STATUS.success;
      // 更新订阅
      forceUpdate();
    }).catch((error) => {
      this.status = STATUS.error;
      result.error = error;
      result.loading = false;
      forceUpdate();
    });
  }

  execute() {
    // skip:是否禁用查询
    const { result, options: { skip = false } } = this;
    !skip && this.startQuery();

    // 当skip 为true 时,说明没有查询结果,所以不能用上次的查询结果来做过渡
    return Object.assign({
      error: undefined,
      data: undefined,
      loading: !skip,
      forceUpdate: this.forceQuery,
    }, skip ? {} : result);
  }

是不是有种恍然大悟,并没有什么发布订阅的具体实现,完全依赖于Promise对象隐式的发布订阅,而forceUpdate的实现则依赖于useReducer:

const [tick, forceUpdate] = useReducer(x => x + 1, 0);

从去年用Graphql写完自己的博客,发现Apollo的useQuery这个hooks彩蛋,就一直有想法去实现一个useRequest hooks,在去年的一次需求推进中,强迫自己去编写了这个组件。收获还是很大的,在我们团队中已经有一定尝试,当然还有很大的拓展空间。从这个组件的编写历程,我也再一次体验了开手动挡,司机经验的重要性,拿我自己挖到的一个坑举例:

  cleanup() {
    this.result.data = undefined;
    this.result.loading = false;
    this.result.error = undefined;
  }

上面一段代码,是每次请求完成后,页面更新后, 有一个useEffect副作用会执行query.cleanup()来保证请求示例回到初始状态。但就这样一段代码,引起了极大的性能问题:

事件唤起查询时,页面会抖动,开始我以为是我写的hooks的组件不如react-redux那么多性能优化,其实当时在猜疑是不是hooks的性能问题。

但后面通过应用chrome performance,观测到其抖动,是由于this.result.data = undefined造成的,用一个示意图表示:
image.png
大概意思就是,如果当前页面有数据,再次发起查询时,列表除了当前更新为loading状态,当前10天数据也会被清除,相当于列表做了一次diff并render;请求完成后,loading状态消失,更新列表,又做了一次diff并render.所以会造成抖动。后面完善代码后,就和下面正常的阶跃曲线一样,只有一次阶跃,列表实际只会做一次render.

此次实现是基于团队现有的http库来做的,这个库的原理在以前的一篇文章讲过:边看边写:基于Fetch仿洋葱模型写一个Http构造类

如果你感兴趣,可以在我的github看到:

结语

没啥想总结的,愿疫情早日过去。愿口罩早日摘下,愿火锅早日成为生活的日常。

对了,Antd4.0已经到来,Form表单基本被重写,这意味着我组件库Antd-doddle, 又得做一次大的升级了!!!!cd

查看原文

Nyaooo 收藏了文章 · 1月21日

Flutter混合开发

混合开发简介

使用Flutter从零开始开发App是一件轻松惬意的事情,但对于一些成熟的产品来说,完全摒弃原有App的历史沉淀,全面转向Flutter是不现实的。因此使用Flutter去统一Android、iOS技术栈,把它作为已有原生App的扩展能力,通过有序推进来提升移动终端的开发效率。
目前,想要在已有的原生App里嵌入一些Flutter页面主要有两种方案。一种是将原生工程作为Flutter工程的子工程,由Flutter进行统一管理,这种模式称为统一管理模式。另一种是将Flutter工程作为原生工程的子模块,维持原有的原生工程管理方式不变,这种模式被称为三端分离模式。

在这里插入图片描述
在Flutter框架出现早期,由于官方提供的混编方式以及资料有限,国内较早使用Flutter进行混合开发的团队大多使用的是统一管理模式。但是,随着业务迭代的深入,统一管理模式的弊端也随之显露,不仅三端(Android、iOS和Flutter)代码耦合严重,相关工具链耗时也随之大幅增长,最终导致开发效率降低。所以,后续使用Flutter进行混合开发的团队大多使用三端代码分离的模式来进行依赖治理,最终实现Flutter工程的轻量级接入。
除了可以轻量级接入外,三端代码分离模式还可以把Flutter模块作为原生工程的子模块,从而快速地接入Flutter模块,降低原生工程的改造成本。在完成对Flutter模块的接入后,Flutter工程可以使用Android Studio进行开发,无需再打开原生工程就可以对Dart代码和原生代码进行开发调试。
使用三端分离模式进行Flutter混合开发的关键是抽离Flutter工程,将不同平台的构建产物依照标准组件化的形式进行管理,即Android使用aar、iOS使用pod。也就是说,Flutter的混编方案其实就是将Flutter模块打包成aar或者pod库,然后在原生工程像引用其他第三方原生组件库那样引入Flutter模块即可。

Flutter模块

默认情况下,新创建的Flutter工程会包含Flutter目录和原生工程的目录。在这种情况下,原生工程会依赖Flutter工程的库和资源,并且无法脱离Flutter工程独立构建和运行。
在混合开发中,原生工程对Flutter的依赖主要分为两部分。一个是Flutter的库和引擎,主要包含Flutter的Framework 库和引擎库;另一个是Flutter模块工程,即Flutter混合开发中的Flutter功能模块,主要包括Flutter工程lib目录下的Dart代码实现。
对于原生工程来说,集成Flutter只需要在同级目录创建一个Flutter模块,然后构建iOS和Android各自的Flutter依赖库即可。接下来,我们只需要在原生项目的同级目录下,执行Flutter提供的构建模块命令创建Flutter模块即可,如下所示。

flutter create -t module flutter_library    

其中,flutter_library为Flutter模块名。执行上面的命令后,会在原生工程的同级目录下生成一个flutter_library模块工程。Flutter模块也是Flutter工程,使用Android Studio打开它,其目录如下图所示。
在这里插入图片描述
可以看到,和普通的Flutter工程相比,Flutter模块工程也内嵌了Android工程和iOS工程,只不过默认情况下,Android工程和iOS工程是隐藏的。因此,对于Flutter模块工程来说,也可以像普通工程一样使用 Android Studio进行开发和调试。
同时,相比普通的Flutter工程,Flutter模块工程的Android工程目录下多了一个Flutter目录,此目录下的build.gradle配置就是我们构建aar时的打包配置。同样,在Flutter模块工程的iOS工程目录下也会找到一个Flutter目录,这也是Flutter模块工程既能像Flutter普通工程一样使用Android Studio进行开发调试,又能打包构建aar或pod的原因。

Android集成Flutter

在原生Android工程中集成Flutter,原生工程对Flutter的依赖主要包括两部分,分别是Flutter库和引擎,以及Flutter工程构建产物。

  • Flutter库和引擎:包含icudtl.dat、libFlutter.so以及一些class文件,最终这些文件都会被封装到Flutter.jar中。
  • Flutter工程产物:包括应用程序数据段 isolate_snapshot_data、应用程序指令段 isolate_snapshot_instr、虚拟机数据段vm_snapshot_data、虚拟机指令段vm_snapshot_instr以及资源文件flutter_assets。

和原生Android工程集成其他插件库的方式一样,在原生Android工程中引入Flutter模块需要先在settings.gradle中添加如下代码。

setBinding(new Binding([gradle: this]))
evaluate(new File(
  settingsDir.parentFile,
  'flutter_library/.android/include_flutter.groovy'))

其中,flutter_library为我们创建的Flutter模块。然后,在原生Android工程的app目录的build.gradle文件中添加如下依赖。

dependencies {
    implementation project(":flutter")
}

然后编译并运行原生Android工程,如果没有任何错误则说明集成Flutter模块成功。需要说明的是,由于Flutter支持的最低版本为16,所以需要将Android项目的minSdkVersion修改为16。
如果出现“程序包android.support.annotation不存在”的错误,需要使用如下的命令来创建Flutter模块,因为最新版本的Android默认使用androidx来管理包。

flutter create --androidx -t module flutter_library

对于Android原生工程,如果还没有升级到androidx,可以在原生Android工程上右键,然后依次选择【Refactor】→【Migrate to Androidx】将Android工程升级到androidx包管理。
在原生Android工程中成功添加Flutter模块依赖后,打开原生Android工程,并在应用的入口MainActivity文件中添加如下代码。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        View flutterView = Flutter.createView(this, getLifecycle(), "route1");
        FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        addContentView(flutterView, layoutParams);
    }
}

通过Flutter提供的createView()方法,可以将Flutter页面构建成Android能够识别的视图,然后将这个视图使用Android提供的addContentView()方法添加到父窗口即可。重新运行原生Android工程,最终效果如下图所示。
在这里插入图片描述
如果原生Android的MainActivity加载的是一个FrameLayout,那么加载只需要将Flutter页面构建成一个Fragment即可,如下所示。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        FragmentTransaction ft= getSupportFragmentManager().beginTransaction();
        ft.replace(R.id.fragment_container, Flutter.createFragment("Hello Flutter"));
        ft.commit();
    }
}

除了使用Flutter模块方式集成外,还可以将Flutter模块打包成aar,然后再添加依赖。在flutter_library根目录下执行aar打包构建命令即可抽取Flutter依赖,如下所示。

flutter build apk --debug

此命令的作用是将Flutter库和引擎以及工程产物编译成一个aar包,上面命令编译的aar包是debug版本,如果需要构建release版本,只需要把命令中的debug换成release即可。
打包构建的flutter-debug.aar位于.android/Flutter/build/outputs/aar/目录下,可以把它拷贝到原生Android工程的app/libs目录下,然后在原生Android工程的app目录的打包配置build.gradle中添加对它的依赖,如下所示。

dependencies {
  implementation(name: 'flutter-debug', ext: 'aar')   
}

然后重新编译一下项目,如果没有任何错误提示则说明Flutter模块被成功集成到Android原生工程中。

iOS集成Flutter

原生iOS工程对Flutter的依赖包含Flutter库和引擎,以及Flutter工程编译产物。其中,Flutter 库和引擎指的是Flutter.framework等,Flutter工程编译产物指的是 App.framework等。
在原生iOS工程中集成Flutter需要先配置好CocoaPods,CocoaPods是iOS的类库管理工具,用来管理第三方开源库。在原生iOS工程中执行pod init命令创建一个Podfile文件,然后在Podfile文件中添加Flutter模块依赖,如下所示。

flutter_application_path = '../flutter_ library/
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'iOSDemo' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!
  install_all_flutter_pods(flutter_application_path)

  # Pods for iOSDemo
  … //省略其他脚本
end '

然后,关闭原生iOS工程,并在原生iOS工程的根目录执行pod install命令安装所需的依赖包。安装完成后,使用Xcode打开iOSDemo.xcworkspace原生工程。
默认情况下,Flutter是不支持Bitcode的,Bitcode是一种iOS编译程序的中间代码,在原生iOS工程中集成Flutter需要禁用Bitcode。在Xcode中依次选择【TAGETS】→【Build Setttings】→【Build Options】→【Enable Bitcode】来禁用Bitcode,如下图所示。
在这里插入图片描述
如果使用的是Flutter早期的版本,还需要添加build phase来支持构建Dart代码。依次选择【TAGGETS】→【Build Settings】→【Enable Phases】,然后点击左上角的加号新建一个“New Run Script Phase”,添加如下脚本代码。

"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed

不过,最新版本的Flutter已经不需要再添加脚本了。重新运行原生iOS工程,如果没有任何错误则说明iOS成功集成Flutter模块。
除了使用Flutter模块方式外,还可以将Flutter模块打包成可以依赖的动态库,然后再使用CocoaPods添加动态库。首先,在flutter_library根目录下执行打包构建命令生成framework动态库,如下所示。

flutter build ios --debug

上面命令是将Flutter工程编译成Flutter.framework和App.framework动态库。如果要生成release版本,只需要把命令中的debug换成release即可。
然后,在原生iOS工程的根目录下创建一个名为FlutterEngine的目录,并把生成的两个framework动态库文件拷贝进去。不过,iOS生成模块化产物要比Android多一个步骤,因为需要把Flutter工程编译生成的库手动封装成一个pod。首先,在flutter_ library该目录下创建FlutterEngine.podspec,然后添加如下脚本代码。

Pod::Spec.new do |s|
  s.name             = 'FlutterEngine'
  s.version          = '0.1.0'
  s.summary          = 'FlutterEngine'
  s.description      = <<-DESC
TODO: Add long description of the pod here.
                       DESC
  s.homepage         = 'https://github.com/xx/FlutterEngine'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { 'xzh' => '1044817967@qq.com' }
  s.source       = { :git => "", :tag => "#{s.version}" }
  s.ios.deployment_target = '9.0'
  s.ios.vendored_frameworks = 'App.framework', 'Flutter.framework'
end

然后,执行pod lib lint命令即可拉取Flutter模块所需的组件。接下来,在原生iOS工程的Podfile文件添加生成的库即可。

target 'iOSDemo' do
    pod 'FlutterEngine', :path => './'
end

重新执行pod install命令安装依赖库,原生iOS工程集成Flutter模块就完成了。接下来,使用Xcode打开ViewController.m文件,然后添加如下代码。

#import "ViewController.h"
#import <Flutter/Flutter.h>
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    UIButton *button = [[UIButton alloc]init];
    [button setTitle:@"加载Flutter模块" forState:UIControlStateNormal];
    button.backgroundColor=[UIColor redColor];
    button.frame = CGRectMake(50, 50, 200, 100);
    [button setTitleColor:[UIColor redColor] forState:UIControlStateHighlighted];
    [button addTarget:self action:@selector(buttonPrint) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}

- (void)buttonPrint{
    FlutterViewController * flutterVC = [[FlutterViewController alloc]init];
    [flutterVC setInitialRoute:@"defaultRoute"];
    [self presentViewController:flutterVC animated:true completion:nil];
}

@end

在上面的代码中,我们在原生iOS中创建了一个按钮,点击按钮时就会跳转到Flutter页面,最终效果如下图所示。
在这里插入图片描述
默认情况下,Flutter为提供了两种调用方式,分别是FlutterViewController和FlutterEngine。对于FlutterViewController来说,打开ViewController.m文件,在里面添加一个加载flutter页面的方法并且添加一个按钮看来调用。

Flutter模块调试

众所周知,Flutter的优势之一就是在开发过程中使用热重载功能来实现快速调试。默认情况下,在原生工程中集成Flutter模块后热重载功能是失效的,需要重新运行原生工程才能看到效果。如此一来,Flutter开发的热重载优势就失去了,并且开发效率也随之降低。
那么,能不能在混合项目中开启Flutter的热重载呢?答案是可以的,只需要经过如下步骤即可开启热重载功能。首先,关闭原生应用,此处所说的关闭是指关闭应用的进程,而不是简单的退出应用。在Flutter模块的根目录中输入flutter attach命令,然后再次打开原生应用,就会看到连接成功的提示,如下图所示。

在这里插入图片描述
如果同时连接了多台设备,可以使用flutter attach -d 命令来指定连接的设备。接下来,只需要按r键即可执行热重载,按R键即可执行热重启,按d键即可断开连接。
在Flutter工程中,我们可以直接点击debug按钮来进行代码调试,但在混合项目中,直接点击debug按钮是不起作用的。此时,可以使用Android Studio提供的flutter attach按钮来建立与flutter模块的连接,进行实现对flutter模块的代码调试,如图下图所示。

在这里插入图片描述
上面只是完成了在原生工程中引入Flutter模块,具体开发时还会遇到与Flutter模块的通信问题、路由管理问题,以及打包等。

查看原文

Nyaooo 赞了文章 · 2019-12-20

30分钟理解GraphQL核心概念

写在前面

在上一篇文章RPC vs REST vs GraphQL中,对于这三者的优缺点进行了比较宏观的对比,而且我们也会发现,一般比较简单的项目其实并不需要GraphQL,但是我们仍然需要对新的技术有一定的了解和掌握,在新技术普及时才不会措手不及。

这篇文章主要介绍一些我接触GraphQL的这段时间,觉得需要了解的比较核心的概念,比较适合一下人群:

  • 听说过GraphQL的读者,想深入了解一下
  • 想系统地学习GraphQL的读者
  • 正在调研GraphQL技术的读者

这些概念并不局限于服务端或者是客户端,如果你熟悉这些概念,在接触任意使用GraphQL作为技术背景的库或者框架时,都可以通过文档很快的上手。

如果你已经GraphQL应用于了实际项目中,那么这篇文章可能不适合你,因为其中并没有包含一些实践中的总结和经验,关于实践的东西我会在之后再单另写一篇文章总结。

什么是GraphQL

介绍GraphQL是什么的文章网上一搜一大把,篇幅有长有短,但是从最核心上讲,它是一种查询语言,再进一步说,是一种API查询语言。

这里可能有的人就会说,什么?API还能查?API不是用来调用的吗?是的,这正是GraphQL的强大之处,引用官方文档的一句话:

ask exactly what you want.

我们在使用REST接口时,接口返回的数据格式、数据类型都是后端预先定义好的,如果返回的数据格式并不是调用者所期望的,作为前端的我们可以通过以下两种方式来解决问题:

  • 和后端沟通,改接口(更改数据源)
  • 自己做一些适配工作(处理数据源)

一般如果是个人项目,改后端接口这种事情可以随意搞,但是如果是公司项目,改后端接口往往是一件比较敏感的事情,尤其是对于三端(web、andriod、ios)公用同一套后端接口的情况。大部分情况下,均是按第二种方式来解决问题的。

因此如果接口的返回值,可以通过某种手段,从静态变为动态,即调用者来声明接口返回什么数据,很大程度上可以进一步解耦前后端的关联。

在GraphQL中,我们通过预先定义一张Schema和声明一些Type来达到上面提及的效果,我们需要知道:

  • 对于数据模型的抽象是通过Type来描述的
  • 对于接口获取数据的逻辑是通过Schema来描述的

这么说可能比较抽象,我们一个一个来说明。

Type

对于数据模型的抽象是通过Type来描述的,每一个Type有若干Field组成,每个Field又分别指向某个Type。

GraphQL的Type简单可以分为两种,一种叫做Scalar Type(标量类型),另一种叫做Object Type(对象类型)

Scalar Type

GraphQL中的内建的标量包含,StringIntFloatBooleanEnum,对于熟悉编程语言的人来说,这些都应该很好理解。

值得注意的是,GraphQL中可以通过Scalar声明一个新的标量,比如:

  • prisma(一个使用GraphQL来抽象数据库操作的库)中,还有DateTimeID这两个标量分别代表日期格式和主键
  • 在使用GraphQL实现文件上传接口时,需要声明一个Upload标量来代表要上传的文件

总之,我们只需要记住,标量是GraphQL类型系统中最小的颗粒,关于它在GraphQL解析查询结果时,我们还会再提及它。

Object Type

仅有标量是不够的抽象一些复杂的数据模型的,这时候我们需要使用对象类型,举个例子(先忽略语法,仅从字面上看):

type Article {
  id: ID
  text: String
  isPublished: Boolean
}

上面的代码,就声明了一个Article类型,它有3个Field,分别是ID类型的id,String类型的text和Boolean类型的isPublished。

对于对象类型的Field的声明,我们一般使用标量,但是我们也可以使用另外一个对象类型,比如如果我们再声明一个新的User类型,如下:

type User {
  id: ID
  name: String
}

这时我们就可以稍微的更改一下关于Article类型的声明代码,如下:

type Article {
  id: ID
  text: String
  isPublished: Boolean
  author: User
}

Article新增的author的Field是User类型, 代表这篇文章的作者。

总之,我们通过对象模型来构建GraphQL中关于一个数据模型的形状,同时还可以声明各个模型之间的内在关联(一对多、一对一或多对多)。

Type Modifier

关于类型,还有一个较重要的概念,即类型修饰符,当前的类型修饰符有两种,分别是ListRequired ,它们的语法分别为[Type]Type!, 同时这两者可以互相组合,比如[Type]!或者[Type!]或者[Type!]!(请仔细看这里!的位置),它们的含义分别为:

  • 列表本身为必填项,但其内部元素可以为空
  • 列表本身可以为空,但是其内部元素为必填
  • 列表本身和内部元素均为必填

我们进一步来更改上面的例子,假如我们又声明了一个新的Comment类型,如下:

type Comment {
  id: ID!
  desc: String,
  author: User!
}

你会发现这里的ID有一个!,它代表这个Field是必填的,再来更新Article对象,如下:

type Article {
  id: ID!
  text: String
  isPublished: Boolean
  author: User!
  comments: [Comment!]
}

我们这里的作出的更改如下:

  • id字段改为必填
  • author字段改为必填
  • 新增了comments字段,它的类型是一个元素为Comment类型的List类型

最终的Article类型,就是GraphQL中关于文章这个数据模型,一个比较简单的类型声明。

Schema

现在我们开始介绍Schema,我们之前简单描述了它的作用,即它是用来描述对于接口获取数据逻辑的,但这样描述仍然是有些抽象的,我们其实不妨把它当做REST架构中每个独立资源的uri来理解它,只不过在GraphQL中,我们用Query来描述资源的获取方式。因此,我们可以将Schema理解为多个Query组成的一张表。

这里又涉及一个新的概念Query,GraphQL中使用Query来抽象数据的查询逻辑,当前标准下,有三种查询类型,分别是query(查询)mutation(更改)subscription(订阅)

Note: 为了方便区分,Query特指GraphQL中的查询(包含三种类型),query指GraphQL中的查询类型(仅指查询类型)

Query

上面所提及的3中基本查询类型是作为Root Query(根查询)存在的,对于传统的CRUD项目,我们只需要前两种类型就足够了,第三种是针对当前日趋流行的real-time应用提出的。

我们按照字面意思来理解它们就好,如下:

  • query(查询):当获取数据时,应当选取Query类型
  • mutation(更改):当尝试修改数据时,应当使用mutation类型
  • subscription(订阅):当希望数据更改时,可以进行消息推送,使用subscription类型

仍然以一个例子来说明。

首先,我们分别以REST和GraphQL的角度,以Article为数据模型,编写一系列CRUD的接口,如下:

Rest 接口

GET /api/v1/articles/
GET /api/v1/article/:id/
POST /api/v1/article/
DELETE /api/v1/article/:id/
PATCH /api/v1/article/:id/

GraphQL Query

query {
  articles(): [Article!]!
  article(id: Int): Article!
}

mutation {
  createArticle(): Article!
  updateArticle(id: Int): Article!
  deleteArticle(id: Int): Article!
}

对比我们较熟悉的REST的接口我们可以发现,GraphQL中是按根查询的类型来划分Query职能的,同时还会明确的声明每个Query所返回的数据类型,这里的关于类型的语法和上一章节中是一样的。需要注意的是,我们所声明的任何Query都必须是Root Query的子集,这和GraphQL内部的运行机制有关。

例子中我们仅仅声明了Query类型和Mutation类型,如果我们的应用中对于评论列表有real-time的需求的话,在REST中,我们可能会直接通过长连接或者通过提供一些带验证的获取长连接url的接口,比如:

POST /api/v1/messages/

之后长连接会将新的数据推送给我们,在GraphQL中,我们则会以更加声明式的方式进行声明,如下

subscription {
  updatedArticle() {
    mutation
    node {
        comments: [Comment!]!
    }
  }
}

我们不必纠结于这里的语法,因为这篇文章的目的不是让你在30分钟内学会GraphQL的语法,而是理解的它的一些核心概念,比如这里,我们就声明了一个订阅Query,这个Query会在有新的Article被创建或者更新时,推送新的数据对象。当然,在实际运行中,其内部实现仍然是建立于长连接之上的,但是我们能够以更加声明式的方式来进行声明它。

Resolver

如果我们仅仅在Schema中声明了若干Query,那么我们只进行了一半的工作,因为我们并没有提供相关Query所返回数据的逻辑。为了能够使GraphQL正常工作,我们还需要再了解一个核心概念,Resolver(解析函数)

GraphQL中,我们会有这样一个约定,Query和与之对应的Resolver是同名的,这样在GraphQL才能把它们对应起来,举个例子,比如关于articles(): [Article!]!这个Query, 它的Resolver的名字必然叫做articles

在介绍Resolver之前,是时候从整体上了解下GraphQL的内部工作机制了,假设现在我们要对使用我们已经声明的articles的Query,我们可能会写以下查询语句(同样暂时忽略语法):

Query {
  articles {
       id
       author {
           name
       }
       comments {
      id
      desc
      author
    }
  }
}

GraphQL在解析这段查询语句时会按如下步骤(简略版):

  • 首先进行第一层解析,当前QueryRoot Query类型是query,同时需要它的名字是articles
  • 之后会尝试使用articlesResolver获取解析数据,第一层解析完毕
  • 之后对第一层解析的返回值,进行第二层解析,当前articles还包含三个子Query,分别是idauthorcomments

    • id在Author类型中为标量类型,解析结束
    • author在Author类型中为对象类型User,尝试使用UserResolver获取数据,当前field解析完毕
    • 之后对第二层解析的返回值,进行第三层解析,当前author还包含一个Query, name,由于它是标量类型,解析结束
    • comments同上...

我们可以发现,GraphQL大体的解析流程就是遇到一个Query之后,尝试使用它的Resolver取值,之后再对返回值进行解析,这个过程是递归的,直到所解析Field的类型是Scalar Type(标量类型)为止。解析的整个过程我们可以把它想象成一个很长的Resolver Chain(解析链)。

这里对于GraphQL的解析过程只是很简单的概括,其内部运行机制远比这个复杂,当然这些对于使用者是黑盒的,我们只需要大概了解它的过程即可。

Resolver本身的声明在各个语言中是不一样的,因为它代表数据获取的具体逻辑。它的函数签名(以js为例子)如下:

function(parent, args, ctx, info) {
    ...
}

其中的参数的意义如下:

  • parent: 当前上一个Resolver的返回值
  • args: 传入某个Query中的函数(比如上面例子中article(id: Int)中的id
  • ctx: 在Resolver解析链中不断传递的中间变量(类似中间件架构中的context)
  • info: 当前Query的AST对象

值得注意的是,Resolver内部实现对于GraphQL完全是黑盒状态。这意味着Resolver如何返回数据、返回什么样的数据、从哪返回数据,完全取决于Resolver本身,基于这一点,在实际中,很多人往往把GraphQL作为一个中间层来使用,数据的获取通过Resolver来封装,内部数据获取的实现可能基于RPC、REST、WS、SQL等多种不同的方式。同时,基于这一点,当你在对一些未使用GraphQL的系统进行迁移时(比如REST),可以很好的进行增量式迁移。

总结

大概就这么多,首先感谢你耐心的读到这里,虽然题目是30分钟熟悉GraphQL核心概念,但是可能已经超时了,不过我相信你对GraphQL中的核心概念已经比较熟悉了。但是它本身所涉及的东西远远比这个丰富,同时它还处于飞速的发展中。

最后我尝试根据这段时间的学习GraphQL的经验,提供一些进一步学习和了解GraphQL的方向和建议,仅供参考:

想进一步了解GraphQL本身

我建议再仔细去官网,读一下官方文档,如果有兴趣的话,看看GraphQL的spec也是极好的。这篇文章虽然介绍了核心概念,但是其他一些概念没有涉及,比如Union、Interface、Fragment等等,这些概念均是基于核心概念之上的,在了解核心概念后,应当会很容易理解。

偏向服务端

偏向服务端方向的话,除了需要进一步了解GraphQL在某个语言的具体生态外,还需要了解一些关于缓存、上传文件等特定方向的东西。如果是想做系统迁移,还需要对特定的框架做一些调研,比如graphene-django。

如果是想使用GraphQL本身做系统开发,这里推荐了解一个叫做prisma的框架,它本身是在GraphQL的基础上构建的,并且与一些GraphQL的生态框架兼容性也较好,在各大编程语言也均有适配,它本身可以当做一个ORM来使用,也可以当做一个与数据库交互的中间层来使用。

偏向客户端

偏向客户端方向的话,需要进一步了解关于graphql-client的相关知识,我这段时间了解的是apollo,一个开源的grapql-client框架,并且与各个主流前端技术栈如Angular、React等均有适配版本,使用感觉良好。

同时,还需要了解一些额外的查询概念,比如分页查询中涉及的Connection、Edge等。

大概就这么多,如有错误,还望指正。

欢迎关注公众号 全栈101,只谈技术,不谈人生
clipboard.png
查看原文

赞 205 收藏 143 评论 15

Nyaooo 收藏了文章 · 2019-07-03

TypeScript 、React、 Redux和Ant-Design的最佳实践

图片描述

阿特伍德定律,指的是any application that can be written in JavaScript, will eventually be written in JavaScript,意即“任何可以用JavaScript来写的应用,最终都将用JavaScript来写”

在使用新技术的时候,切忌要一步一步的来,如果当你尝试把两门不熟悉的新技术一起结合使用,你很大概率会被按在地上摩擦,会yarn/npmReact脚手架等技术是前提,后面我会继续写PWA深入Node.js集群负载均衡Nginxwebpack原理解析等~谢谢思否官方对我上篇文章的加精~

在使用TypeScript前,请你务必万分投入学习好以下内容再尝试:
  • TypeScript必须知识点:

    • javaScript,特别是阮一峰的ES6教程必须要多看几遍,看仔细了,否则你会被TS按在地上摩擦
    • TypeScript文档,什么是TypeScript,一定要看得非常仔细,因为有可能开发时一个极小的问题是你不会的知识点,那么可能会耗费你大量的时间去解决
    • 前端性能优化不完全手册 , 这是本人的一篇文章,也应该看看。 哈哈哈~
    • 介绍完了配置,后面会有大量的总结~
  • React直接看文档,React官方中文文档,我认为React的中文文档已经写得非常好了,学起来还是比较简单的~
  • Redux,学习Redux之前,建议把官方文档看几遍,然后props context 自定义事件 pubsub-js这些组件传递数据的方式都用熟悉后再上Redux,因为Redux写法非常固定,只是在TS中无法使用修饰器而已,需要最原始的写法。后面的代码有注释,到时候可以看看。(HOOKSHOC都可以尝试使用,因为React的未来可能大概率使用这些写法)Redux官方文档
  • Ant-Design,目前React生态最好的UI组件库,百分90的使用率,移动端、PC端都支持,pro还可以开箱即用,强烈推荐,开启配置按需加载,后台TO-B项目用起来不要太舒服。Ant-Design官网~
学技术切忌过分急躁,一步登天,什么都想学却什么都学不好。作者的心得,持之以恒的努力,把每个技术逐个击破,最后结合起来使用,如鱼得水,基础不牢,地动山摇,本文的代码会把所有配置和ReduxAnt-Design全部配好,开箱即用,其他的功能你看Ant-Design的文档往里面加就行了~

正式开启:
  • 本文介绍如何配置,已经整体的业务流程如何搭建 GitHub源码地址

    • 包管理器,使用yarn或者npm都可以,这里建议使用yarn,因为Ant-Design官方推荐yarn,它会自动添加依赖。
    • 使用官方的 create-react-app的另外一种版本 和 Create React App 一起使用 TypeScript
    • react-scripts-ts 自动配置了一个 create-react-app 项目支持 TypeScript。你可以像这样使用:create-react-app my-app --scripts-version=react-scripts-ts, -前提你必须全局下载 create-react-app

请注意它是一个第三方项目,而且不是 Create React App 的一部分。

  • 需要的依赖:都在package.json文件中。
  • 这里请万分注意,TS的包大部分都是需要下两个,一个原生,一个@types/开头

        {
        "name": "antd-demo-ts",
        "version": "0.1.0",
        "private": true,
        "dependencies": {
            "@types/jest": "24.0.11",
            "@types/node": "11.13.7",
            "@types/react": "16.8.14",
            "@types/react-dom": "16.8.4",
            "@types/react-redux": "^7.0.8",
            "@types/react-router-dom": "^4.3.2",
            "@types/redux-thunk": "^2.1.0",
            "babel-plugin-import": "^1.11.0",
            "customize-cra": "^0.2.12",
            "less": "^3.9.0",
            "less-loader": "^4.1.0",
            "prop-types": "^15.7.2",
            "react": "^16.8.6",
            "react-app-rewired": "^2.1.3",
            "react-dom": "^16.8.6",
            "react-redux": "^7.0.2",
            "react-router-dom": "^5.0.0",
            "react-scripts": "3.0.0",
            "redux-chunk": "^1.0.11",
            "redux-devtools-extension": "^2.13.8",
            "redux-thunk": "^2.3.0",
            "typescript": "3.4.5"
        },
        "scripts": {
            "start": "react-app-rewired start",
            "build": "react-app-rewired build",
            "test": "react-app-rewired test"
        },
        "eslintConfig": {
            "extends": "react-app"
        },
        "browserslist": {
            "production": [
                ">0.2%",
                "not dead",
                "not op_mini all"
            ],
            "development": [
                "last 1 chrome version",
                "last 1 firefox version",
                "last 1 safari version"
            ]
        }
 * `Ant-Design`按需加载配置   `config-overrides.js`
const { override, fixBabelImports, addLessLoader } = require('customize-cra');
module.exports = override(
    fixBabelImports('import', {
        libraryName: 'antd',
        libraryDirectory: 'es',
        style: true,
    }),
    addLessLoader({
        javascriptEnabled: true,
        modifyVars: { '@primary-color': '#1DA57A' },
    })
);
 ```
  • tsconfig.json ,TS的配置文件 我基本上没怎么改动

        {
      "compilerOptions": {
        "target": "es5",
        "lib": [
          "dom",
          "dom.iterable",
          "esnext"
        ],
        "allowJs": true,
        "skipLibCheck": true,
        "esModuleInterop": true,
        "allowSyntheticDefaultImports": true,
        "strict": true,
        "forceConsistentCasingInFileNames": true,
        "module": "esnext",
        "moduleResolution": "node",
        "resolveJsonModule": true,
        "isolatedModules": true,
        "noEmit": true,
        "jsx": "preserve"
      },
      "include": [
        "src"
      ]
    }
  • Redux less 的配置

    一些接口的定义
    Redux在TS中不能使用修饰器简写
    store对象的配置,加上了异步处理中间件和开发者调试工具

配置没看懂不要紧,架子我都全部给你搭好了,按着TSAnt-Design的官网去操作就OK
  • 我们重点理理思路,首先为什么要使用TypeScript?

    • 使用TypeScript最终会被编译成JS,所以说它是JS的超集。
    • TypeScript带静态类型检验,现在的第三方包基本上源码都是TS,方便查看调试。
    • 使用TS后,我感觉我调试BUG能力变强了很多,而且很少出错了,思维更严谨了,毕竟这是一个引入顺序不对都会报错的语言。
    • 如果你在使用TS时候还一直使用any public ,那么我建议你退出TS
    • 一旦上了TS,一切都不一样,比如修饰器无法使用。
    • 大型项目首选ReactTS结合,代码调试维护起来极其方便。
  • React如何优化? 我开头的文章有链接~
  • Ant-Design这么火,该怎么学习? 它是一个标签属性带方法的组件库,一切都藏在文档里。
  • ReactReduxVUEX一样,都是单向数据流,写法固定,掌握了写起来非常容易~ 难的永远不是API,而是整体的技术架构,以及实现原理。
TS代码时候常常问问自己,这个到底可能是什么类型,这个到底是public 还是 private?这个函数要返回什么类型,接受什么参数,什么是必须的,什么是可能没有的,再去考虑命名空间接口合并,类合并,继承这些问题。
  • 复杂软件需要用复杂的设计,面向对象就是很好的一种设计方式,使用 TS 的一大好处就是 TS 提供了业界认可的类( ES5+ 也支持)、泛型、封装、接口面向对象设计能力,以提升 JS 的面向对象设计能力。
  • 当你在TS世界遨游过后,再回JS的世界,那么你会发现你写代码很少会出错,除非是业务逻辑的问题~
查看原文

Nyaooo 收藏了文章 · 2019-06-20

优秀工程师必备的一项技能,你解锁了吗?

阿里妹导读:很多程序员在工作一段时间后会遇到迷茫期,虽有技术傍身,也难免会产生焦虑,反复思考怎样才能快速成长。关于如何提高自己的思考力,运用思考的力量推动能力提升,以此实现技术成长,阿里巴巴盒马产品技术部的岩动总结了一套思考方法,分享给每个正在成长的程序员。(本篇文章较长,阅读时间约30分钟,建议收藏后,找一个合适的时间慢慢品读哦)

引言

我们来看一下几类在程序员成长、发展的常见问题,如果你或多或少存在一些,那么恭喜你,这篇文章值得你仔细往下看了:

  • 你自认为付出了跟别人同样的努力,但是你的成长确实更慢一些,比如学得比别人慢,排查问题比别人慢,出方案老是有漏洞等等;
  • 你觉得你只是在疲于应付需求,自己做的事情完全没有技术含量(很多人觉得自己做的业务开发就是没有技术含量,但我认为每个领域都有自己的技术含量,只是你有没有get到);
  • 你发现总是在犯同样的错误,或者做的事情不断地在同一个水平循环;
  • 每次要晋升的时候,你发现根本讲不出来(很多人会认为是表达能力问题,但是我认为不是);
  • 当你换到一个新的领域,你发现自己的经验好像用不上;
  • 你一直很难搞懂老鸟说的“认知升级”到底是什么概念?不同级别的技术思维能力到底有什么差别?为什么晋升的是他,而不是我?

在这篇文章里,我会告诉大家一些技术成长的误区,我先点出来:

  • 只要把事情搞定了,成长是自然而然的事情——可能过段时间,你发现之前犯过的错误,后来一个都没有避免;
  • 我只要努力,996甚至007,我就能够成长得比别人快——可能你发现你干得最多,但是并没有拿到最好的结果;
  • 我尽力了,还是比别人慢,应该是我智商确实差一些——恭喜你,其实大家的智商并不会有太大差别;
  • 别人表现好,或者晋升了,只不过是比我表达能力更强而已——我可以负责任地告诉你,这并不是仅仅是表达能力的问题。

先抛一个非常重要的结论:“思考力”是程序员需要具备的一种至关重要的素质。掌握了思考力,你就掌握了在互联网领域,这种高度“智力密集型”行业成长的钥匙。上面这几个成长的问题和误区,跟没有掌握思考力有着非常重要的关系,而且我发现所有发展比较顺畅的同学,他们的思考和学习能力是非常强悍的。

我个人在工作中,一直有意或者无意地锻炼自己和团队同学的思考力,包括哪些是对我们最重要的思考力,如何去训练思考力,有一些心得,希望能够分享给大家。

关于思考力

思考力是一门很深的学问,包括认知科学,心理学、教育学、逻辑学,如果要系统化学习,是需要看很多书的,我推荐以下几本:

1.《金字塔原理:思考、表达和解决问题的逻辑》-[美] 芭芭拉·明托,这本书系统阐述了思考、表达和解决问题的逻辑,也是麦肯锡的思维能力基础,算是一本比较标准的思考力教材;

2.《麦肯锡教我的思考武器》- [日] 安宅和人,作者根据自己在麦肯锡公司工作时积累的丰富经验以及脑神经学的专业背景,设计出一套极具逻辑性的问题解决思维模式;

3.《思维的本质》-[美]约翰·杜威 ,这本书是美国著名教育家约翰·杜威的代表作,阐述了思维训练的基础理论和实践;

本文并不是探讨思考力的深层理论,而是分享我们从日常的技术学习和项目过程中沉淀下来的思考力,以及如何培养这些思考力,这些思考力几乎我们每天都可以用到,只要你有一定体感,你一定会感同身受。

有哪些对程序员最重要的思考力

原理性思维:找出知识背后的原理

有的人会说,为什么要思考原理,而不是直接掌握知识就可以了?我只需要会用就行了啊。

我们先来举一些技术方案设计的案例

  • 为什么订单创单要先create,然后enable?

这其实是一种采用二阶段提交解决分布式事务的思路,只是从一般的事务框架延展到交易领域;

  • 业务系统中为什么要使用消息?

因为消息使用的是观察者模式,观察者模式的好处是可以实现多个消费事务与触发事务的解耦;

  • 为什么业务系统中会使用DTS来做补偿?

这本质上是一种最终一致性BASE理论解决分布式事务的一种思路;

  • 为什么更新数据的时候一定要在sql中加上版本比对或者状态比对?

这本质上是一种借助DB实现的乐观锁机制。

进一步,你会发现再大到系统架构和顶层设计的案例:

  • 比如阿里系的技术框架NBF、TMF、早期的webx,各类框架设计理念,逃不脱设计模式,比如开闭原则,模板方法、责任链、工厂模式、开闭原则;
  • 不管是底层中间件,错综复杂的业务系统,在设计的时候永远无法离开核心的业务建模,比如实体与实体关系的构建;在分析这类系统的设计思想时,你会发现最好的工具就是UML!

实际上除了软件领域的原理,还有商业设计的原理,比如案例:

  • 所有的售中退款前必须要先取消履约,所有的履约过程中发生缺货都需要退款,为什么?因为交易的基本原则是:“钱货平衡”,钱和货的变更必须是最终同步的(允许短期的不平衡),你掌握了钱货平衡的基本原理,交易中的很多复杂的流程设计就很好理解了;
  • 在设计财务系统、库存系统时候,业务流程、业务逻辑可能非常复杂,导致你晕头转向,这时候“有借必有贷,借贷必相等”的财务平衡性原理就发挥作用了,你只要知道这个原理,很快就能看懂各类财务流程、库存流转流程,以及各类数据对账逻辑;
  • 在我的领域“高可用线下收银系统”进行线下系统容灾的时候,有各种容灾方案的设计,会员容灾、商品容灾、交易容灾、支付容灾……不同的容灾手段看起来让你眼花缭乱,但是他们有没有共同遵循的原则呢?有,这就是“让消费者最快速度完成交易,但保持最后追溯的能力”。你只要get到这个基本原理,设计各类容灾策略就会得心应手了。

此外,我们的工作流程、管理手段,同样也蕴含着深层的原理,非常有意思,大家可以抽空仔细推敲一下,比如:

  1. 为什么团队机制要透明?沟通要透明?
  2. 为什么要有owner意识,都是在工作,owner意识会有什么不同呢?
  3. 为什么管理者不能管得太细,也不能放羊?到底哪些该管,哪些不该管?

所以,掌握了知识背后的原理,带来的好处是:

  • 软件系统的复杂度越来越高,我们所面对的场景越来越多,掌握原理实际上可以大幅度降低我们对于知识的记忆量,知识量是爆炸的,但是原理绝对是可控的!
  • 原理性的东西比直接的知识有更强的复用度!记住最核心的原理,当你面对新的场景时,你会惊喜地发现,你的理解速度大大加快!这个点大家应该有体会,比如可能之前我们都学习过dubbo等底层的RPC通信框架的基本原理,但是你如果仅了解了他的基本用法,你会发现对你现在做业务系统没有什么帮助!但是,当你了解的是dubbo如何寻址,如何做容灾,如何做扩展,你再去做业务系统,发现设计原理是一样的,并没有本质区别!这样你之前研究中间件的设计思想就可以快速用到业务系统上面。
  • 另外探求原理的过程,本身很有乐趣!这是一个非常有价值的思维训练过程,不断对系统设计思想、业务设计思想、做事情的工作方式,追寻背后的原理,并找到他们之间的共性,在我看来非常有乐趣,一段时间训练以后,你会发现你看透本质的能力越来越强!

好,那么我们程序员的工作中,究竟有哪些与原理性知识是需要我们掌握的呢?按我们团队的实战经验来看:

  • java,linux,数据结构和算法,数据库,网络通信与分布式计算的原理,这几类是比较重要的基础知识,我们在做方案设计、编码、问题排查中会运用得很多;
  • 设计模式,UML这个是对系统架构设计必要要掌握的知识,当你经历了很多大规模的软件系统设计,回到根本上,你会发现逃不出这一块的理论和工具;
  • 领域性的基本原则,比如我们上面提到的“钱货平衡”,“财务平衡公式”,“线下收银让消费者最快速度走人”,这种逻辑需要大家get到这些领域性的设计原理,甚至自己去总结出这种原理;
  • 关于管理学,人际沟通,心理学的一些基本原理,大家可以按照自己的实际需求去看一下。

如何在工作中学习和运用这些原理,我觉得有一个最佳实践

  • 首先,对你可能用到的领域知识,建立一个基本的概念。看书,看文章,找行业资深的人去聊,都可以得到。注意,这里需要有一个基本的概念就可以,这样你在有可能touch到这些原理的时候,你会有意识,也不至于花很多时间;
  • 在实践中,有个意识是“多问一下为什么”,并一直“刨根问底”,最终肯定能够追查到背后的最终原理;这里面还要注意思考一下,为什么在这个地方会运用这个原理,也就是找到“场景”和“原理”的关联关系,这样你的理解会更加深刻;
  • 了解了原理以后,在实践中运用一下,这样你对这个原理的理解就会非常深刻,并且你知道如何去运用这原理;
  • 如果这是一个非常重要的原理,建议大家如有余力去结合经典的书籍系统化学习。

结构化思维:构建自己的知识树

知识树要解决的问题,我们看一些场景:

  1. 为什么我知道很多东西,但是当场景来的时候老是会记不起来使用;
  2. 完成一个方案你只能想到一些点状的手段,还有其他方案被漏掉了;
  3. 讲一件事情的时候逻辑非常混乱,前后没有逻辑性关联。

但是很有可能你的知识都是知道的,为什么会出现这种悲剧?

这个就跟大脑中的知识结构有关,这是知识学习中“索引”没有建立,也就是说,你的知识只有点,没有线!大家想一想,把东西乱七八糟地丢在房间中,到用的时候没有查找的线索和路径,怎么找得到呢?

来看一下我们工作场景的结构化的典型案例,大家体会一下:

项目中测试MM提了一个bug,我总结出来的比较标准的问题定位步骤:

  1. 确认刚才是否有过代码变更和部署,因为有比较高的概率是刚才变更的代码又搞坏了……
  2. 追踪链路日志看链路是否有异常;
  3. 通过RPC的控制台调用看接口输入输出是否符合预期;
  4. 追踪关键方法的入参和出参,看是否有问题;
  5. 定位到方法细节后,推理逻辑是否有问题;
  6. 如果无法通过推理,那就最后一招,回放异常流量debug,这样肯定能够找到原因。

某个链路耗时比较长,需要进行性能优化,我的分析步骤是:

  1. 通过实际流量制造一个耗时较高的trace;
  2. 进行trace分析,看清楚耗时最多的原因,然后按优先级进行排序;
  3. 针对对原因找解决方案,可能的方案有:
  • 减少数据访问次数或者计算量,常见手段是增加cache:线程内的invokeCache;分布式缓存tair;页面缓存……
  • 增强处理速度,比如多线程加速;
  • 减少循环调用次数,比如请求合并后再分发;
  • 减少数据处理范围,比如减少查询内容,异步加载分页;
  • 逻辑简化,比如逻辑进行优化,或者非核心逻辑异步化等;
    ……

4.改掉以后,回放同样的case,看性能消耗是否满足预期,不满足预期继续优化;

如何熟悉一个新系统,我的步骤是:

  1. 要一个测试账号,把相关功能走一遍,这样能非常快地了解一个系统的功能;
  2. 看关键的核心表结构,这样可以快速了解系统的领域模型;
  3. 根据功能步骤找到系统对外的接口列表,了解系统的L0业务流程;
  4. 下载系统工程,熟悉整个工程结构和模块职责;
  5. 以一个最重要的流程为入手点,阅读代码,看清楚核心的执行逻辑,可以变看边画时序图;
  6. 制造一个debug场景,以debug方式走一遍流程,这样可以实际加深一下对系统的理解;
  7. 做一个小需求,掌握相关的流程和权限;

下单这里来了一个新的需求,出一个技术方案的步骤:

  1. 看清楚之前的需求,把这个需求所在的场景和链路大致阅读一遍,搞懂;
  2. 找到需求的变化点;
  3. 分析变更的方案,涉及的内容可能会有:

数据结构会不会变,如何变;

交互协议会不会变,如何变,交互协议分为:端和组件要不要变;和下游接口要不要变;

执行逻辑会不会变,如何变,执行逻辑变更的细化考虑点:是否变更域服务;是否变更流程编排;是否变更主干逻辑;是否变更扩展点是否变更扩展点的内部逻辑,变更内部逻辑的时候,又可以进一步拆解:

a.重构原有的方法,覆盖之前的逻辑,那就需要进行回归;

b.通过逻辑路由到新的方法,这里需要增加路由逻辑;

4\. 稳定性方案;

5\. 发布方案;

可以看到,面对任何一个场景,不管多大多小,我们所需要掌握的知识或者技能都可以构建成一个树结构,同类之间是顺序关系,上下之间是父子关系(或者粗细颗粒度)。

当这个树在大脑中构建起来以后,你会发现你做什么事情都是有一个明确的分析和执行逻辑,不太可能产生遗漏和混乱!

那么如何训练出自己的知识树呢?我给一些比较有效的实践方案:

  1. 一定要总结出自己的知识树,而不要盲从书本上的或者别人的,为什么呢?一是因为人的思维速度和习惯、技能有一定差异,不一定每个人都是一样的;二是如果没有内化别人的知识成为自己的知识,这棵树不太能够很熟练地运用;
  2. 习惯性总结,做完任何一个事情,都习惯性地回顾一下,往自己的树上面挂新东西,这个是构建知识树的必备手段,这个总结不需要花很多时间,比如做完事情后花个几分钟回顾一下就可以,但是需要坚持;
  3. 推荐一个很常见的工具:xmind,把自己的树记录下来;
  4. 训练自己的思维习惯和做事方式变得结构化,当你做事情的时候,习惯性用树的方式推进,强迫自己按照这个方式来。

扩展性思维:举一反三,拓展思维

扩展性思维的核心目标是提升我们思维的广度,也就是让我们的知识树变得更加开阔;

我在工作中总结出来的扩展性思维的两个关键的扩展方向:

(1)举一反三:解决同类型的N个问题

举一反三的好处是:“我们能否用同样的知识和手段去解决类似的相关联的几个类似问题”,先举一些案例:

  • 当发现某个系统的jvm参数配置存在一个错误配置,不是仅仅修复这个系统的jvm配置,而是把负责的几个系统都检查一下是否需要统一修改;
  • 系统中存在某个bug导致产生了脏数据,不是直接订正已发现的脏数据,而是根据特征拉取出所有的脏数据,进行一次性处理;

这种思维方式的特征是举一反三,触类旁通,相当于产生批处理的效果,可以大大提升解决问题的效率,避免重复处理。

(2)寻求更多的可能性:拓展解决问题的不同手段

拓展思维常见的手段是:是否能够换更多的理解方式,或者更多的解法,举一些案例:

  • 产生故障的时候,快速止血除了回滚以外,还有哪些方案?如果故障处理经验丰富的人一定知道,除了回滚,其实还有系统降级,运营活动降级等多种方案;
  • 除了写更加健壮的代码,还有哪些手段都可以提升系统的容错性?还有数据监控,单据闭环等多种手段;

当解决问题的手段更多了,思维就开阔了。

抓重点思维:提升效率,方便记忆和传递

当我们发现知识树构建起来以后,怎么样使得记忆和使用的效率变高?而且对外传递的时候更加容易让人理解?抓重点思维要解决的场景是:

  1. 如果每件事情都按照知识树方式做,效率可能不会特别高,有更快的办法么?
  2. 在对外沟通表达的时候,要表达核心思想,否则别人会很难理解你的表达内容;比如大家再晋升答辩、项目汇报的时候一定会有体会。

解决这两类困惑,核心思路是要抓住重点和脉络。

但是抓住重点和知识结构化之间并不矛盾,而且我认为是有先后次序的,一定要先建立知识结构化,然后才能从里面筛选出重点,否则知识的体系是不完整的。

那么筛选重点的思路有哪些呢?

(1)归纳法

采用归纳法,把细节隐藏掉,呈现知识的脉络,这是一种非常好的思路;尤其是大家在准备晋升ppt时,ppt的每一页都需要归纳一个核心观点,不是全是细节,这个非常重要!并且训练归纳的能力,本身就是对知识理解深刻程度的一种反映;

(2)优先级法

优先级策略往往应用于在多项任务之间找到最最关键或者收益最大的那个任务项,比如完成一个事情可能有若干个步骤,其中哪个步骤是最有效的,大致可以做一个排序。在实施的时候,你可以按照优先级去落实。

但是找到效果最好的那个任务项,在不同场景下是不同的,跟我们的熟练程度和经验有关。就像老中医把脉,越有经验判断越准,这块没有什么捷径,只能不断练习自己找到哪些任务项在什么场景下更加重要。

反思性思维:思考哪里可以做得更好

反思性思维是提升知识质量和深度的一个关键能力。因为只有不断反思才能让下一次在上一次基础上升级,而不是重复循环。

常见的反思案例:

  • 有个问题我查了2个小时,师兄只花了10分钟,这是为什么呢?是他的业务比我熟悉?思路比我清晰?还是知道某个我不知道的工具?一定要找到关键的差异点,然后弥补掉这个差距;
  • 一个项目项目做完了,从方案设计,研发过程,质量保障上面,哪些地方下次可以做得更好?找到不足,下次避免;

对于我们技术团队,哪些内容值得反思,我们团队的经验是:

  • 这个项目商业价值OK吗?是否取得了预期的效果?
  • 项目中我的能力有哪些问题,有哪些做得好的和不好的?
  • 系统设计的优势和不足?
  • 项目质量保障是否可以做得更好一些?
  • 研发过程和项目管理是否有不足?

反思性思维的实践,注意有两个点比较关键:

  • 反思性思维最重要的意识:做事情的过程总有优化的空间,每次都要有进步;如果没有这种心态,那么很难持续地进行反思;
  • 反思是一种习惯和潜意识,可以在不经意之间经常进行,其实不需要很形式化地花很多时间,有时候做完一个事情,习惯性思考一下就可以。

锻炼思考力的有效实践

1.意识觉醒

意识觉醒是提升思考力最重要的一个点,我认为。只要形成了这种意识,就已经成功了一半。

很多同学思维能力没有上去,是没有意识到思考力这个概念,只是机械地做事情,做事情,做事情……每次都在同一个思维层次上面转悠,不可能有本质的提升。

从初级工程师,高级工程师,技术专家,高级专家,资深专家……级别提升靠什么?多接了多少需求?多写了多少代码?这些因素会有,但是关键因素不是这些,而是思考力在不断提升,思维方式在不断进化,进而导致业绩产出必变得更加优秀,产生的是事半功倍的效果。

能够坚持看到这里的同学,一定是能够知道思考力的重要性了。

2.保持信心

现在知道思考力的重要性了,很多同学可能认为自己是一个不够聪明的人。为什么我努力了,还是不行?

给大家一个信心:有位大师说过:在相同的文明程度和种族背景下,每一个正常人的潜意识与意识相加之和,在精神能量意义上基本上是相等的。

我几乎接触到的很努力但是成长速度不快的同学都是因为没有没有掌握正确的方法;

只要掌握了正确的方法并坚持训练,思考力绝对可以提升。

3.空杯心态

思考的过程其实是对人的知识进行不断刷新和重构的过程,这里一定要保证空杯心态,对新的环境,新的理念,新的技术持开放态度,否则就是自己给自己制造阻力。

4.思考的时间从哪里来?

常见的借口是“我连需求都做不完,哪来的时间思考”?

训练思考力其实并不需要太完整的时间,我的口诀是:“1.利用碎片时间;2.抓住工作的过程”。

  • 利用碎片时间,比如上下班路上的时间,吃饭的时候,可以把刚才或者今天的事情想一想,想通了,然后定期汇总一下就可以;
  • 抓住工作的过程,注意,每次每次出技术方案,优化代码,排查问题,处理故障,准备晋升……都是一次训练的机会,在做事情的过程中就可以思考并快速实践。

5.思考力提升有没有什么判断标准?

有的,一般来说思考力有三个度:广度、深度、速度,这你自己就能够感觉出来的:

  • 广度:就是你自己的知识树能够长多大的范围,越广知识越渊博;比如从“如何写一个多线程程序”,提升到“如何做系统性能优化“,再到“如何做系统稳定性备战”,这就是一种广度的提升;
  • 深度:就是你自己的知识树的叶子节点有多深,越深对知识了解越透彻;比如从“分布式事务问题解决思路”,到“利用最终一致性解决分布式事务”,再到“利用DTS解决分布式事务”,这就是一种深度的提升;
  • 速度:就是建立和刷新知识树的速度了。比如原来你想清楚一个建模方案要一天,现在只需要半小时可以想清楚,那就是速度的提升了。

6.好的工具有推荐么?

还是推荐一个工具:Xmind,这个最土的工具最有效。可以下载手机版和PC版本,随时进行记录。

7.一定要相互分享

思考虽然主要是靠自己,但是一定要相互分享。因为思考是智力活动,相互分享完全能够取得1+1>2的效果;

注意分享可以有很多形式,比如我们团队最经常的是:

  • 项目分享:重大项目是一定要分享的,包括架构设计经验,过程经验,质量提升经验,都需要分享出来;
  • 周会分享:团队周会重点过进度?那太浪费啦,了解进度和风险看周报就可以了。周会是学习分享的好时机重点就是一些关键的方案,架构设计理念,好的工具,甚至工作无关的内容;
  • 群内分享:当有个人踩坑以后,在群里面提醒一下大家,这是一个很及时的分享方案;
  • 年度/季度分享:这时候适合找个风景优美喝茶的地方,大家讲一讲自己的成长和思考,非常有帮助;
  • 小圈子:大家形成自己的小圈子,随时都可以相互倾诉一下自己的心得体会,其实这种效果也很好;

8.技术Leader在训练大家思考力中的职责

在技术团队中,技术Leader的思考力意识、能力和实际行动,决定了一个团队的整体思考力水平和成长速度!

一个团队要提高思考和学习的能力,首先得这个团队Leader的思考意识就要提上来,如果团队Leader没有思考意识,也没有把团队同学的成长放在心上,那么整个团队的思考力和成长速度绝对快不起来。

在提升团队整体思考力的实践中,技术Leader的职责:

  • 先要把自己变成一个思考者,自己做表率,以身作则;
  • 意识心态上先变过来,要把团队同学的成长速度最为最重要的职责之一,没有这个意识都是空谈;
  • 多创造思考的条件和氛围,一定要抓住任何机会(代码reivew、方案评审、周会都可以)鼓励大家去思考和分享;
  • 控制团队节奏,给大家学习和思考留出一定的时间;
  • 及时的引导和示范,有的同学可能掌握会偏慢一些,这时候需要有耐心去引导同学找到思考的感觉;
  • 不必过多干预细节,发挥大家的群体智慧,而不必做过多干预,更不能以个人的意志去强迫别人接受。

重要观点小结

好了,到这里可以给重要观点做个小结,时间紧的同学们可以直接读这一段:

  1. 思考力对程序员的成长至关重要,团队和个人都需要有意或者无意识地提升思考能力。
  2. 对程序员最重要的思考力有:原理性思维、结构化思维、反思性思维、扩展性思维、抓重点思维。
  • 原理性思维是根基,因为没有搞懂的情况下所有的知识建构都是空谈;
  • 结构化思维帮助我们建立了我们的知识树;
  • 反思性思维不断对知识进行重构,是实现认知升级的必备条件;
  • 扩展性思维可以提升知识的广度和深度;
  • 抓重点思维可以加快知识的使用效率和传递效率;

    1. 在提升思考力的实践中:
  • 思考力提升最关键的是意识的转变;
  • 要对思考力的提升充满信心;
  • 多在工作中去锻炼思考力,不需要花太多额外的休息时间;
  • 多相互分享;

团队Leader要团队同学的成长和把思考力提升作为最重要的内容,并拿出实际行动。



本文作者: 岩动

阅读原文

本文来自云栖社区合作伙伴“阿里技术”,如需转载请联系原作者。

查看原文

Nyaooo 收藏了文章 · 2018-12-01

Vue页面骨架屏注入实践

作为与用户联系最为密切的前端开发者,用户体验是最值得关注的问题。关于页面loading状态的展示,主流的主要有loading图和进度条两种。除此之外,越来越多的APP采用了“骨架屏”的方式去展示未加载内容,给予了用户焕然一新的体验。随着SPA在前端界的逐渐流行,首屏加载的问题也在困扰着开发者们。那么有没有一个办法,也能让SPA用上骨架屏呢?这就是这篇文章将要探讨的问题。

文章相关代码已经同步到Github,欢迎查阅~

一、何为骨架屏

简单来说,骨架屏就是在页面内容未加载完成的时候,先使用一些图形进行占位,待内容加载完成之后再把它替换掉。

clipboard.png

这个技术在一些以内容为主的APP和网页应用较多,接下来我们以一个简单的Vue工程为例,一起探索如何在基于Vue的SPA项目中实现骨架屏。

二、分析Vue页面的内容加载过程

为了简单起见,我们使用vue-cli搭配webpack-simple这个模板来新建项目:

vue init webpack-simple vue-skeleton

这时我们便获得了一个最基本的Vue项目:

.
├── package.json
├── src
│   ├── App.vue
│   ├── assets
│   └── main.js
├── index.html
└── webpack.conf.js

安装完了依赖以后,便可以通过npm run dev去运行这个项目了。但是,在运行项目之前,我们先看看入口的html文件里面都写了些什么。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>vue-skeleton</title>
  </head>
  <body>
    <div id="app"></div>
    <script data-original="/dist/build.js"></script>
  </body>
</html>

可以看到,DOM里面有且仅有一个div#app,当js被执行完成之后,此div#app会被整个替换掉,因此,我们可以来做一下实验,在此div里面添加一些内容:

<div id="app">
  <p>Hello skeleton</p>
  <p>Hello skeleton</p>
  <p>Hello skeleton</p>
</div>

打开chrome的开发者工具,在Network里面找到throttle功能,调节网速为“Slow 3G”,刷新页面,就能看到页面先是展示了三句“Hello skeleton”,待js加载完了才会替换为原本要展示的内容。

hzv4.gif

现在,我们对于如何在Vue页面实现骨架屏,已经有了一个很清晰的思路——在div#app内直接插入骨架屏相关内容即可。

三、易维护的方案

显然,手动在div#app里面写入骨架屏内容是不科学的,我们需要一个扩展性强且自动化的易维护方案。既然是在Vue项目里,我们当然希望所谓的骨架屏也是一个.vue文件,它能够在构建时由工具自动注入到div#app里面。

首先,我们在/src目录下新建一个Skeleton.vue文件,其内容如下:

<template>
  <div class="skeleton page">
    <div class="skeleton-nav"></div>
    <div class="skeleton-swiper"></div>
    <ul class="skeleton-tabs">
      <li v-for="i in 8" class="skeleton-tabs-item"><span></span></li>
    </ul>
    <div class="skeleton-banner"></div>
    <div v-for="i in 6" class="skeleton-productions"></div>
  </div>
</template>

<style>
.skeleton {
  position: relative;
  height: 100%;
  overflow: hidden;
  padding: 15px;
  box-sizing: border-box;
  background: #fff;
}
.skeleton-nav {
  height: 45px;
  background: #eee;
  margin-bottom: 15px;
}
.skeleton-swiper {
  height: 160px;
  background: #eee;
  margin-bottom: 15px;
}
.skeleton-tabs {
  list-style: none;
  padding: 0;
  margin: 0 -15px;
  display: flex;
  flex-wrap: wrap;
}
.skeleton-tabs-item {
  width: 25%;
  height: 55px;
  box-sizing: border-box;
  text-align: center;
  margin-bottom: 15px;
}
.skeleton-tabs-item span {
  display: inline-block;
  width: 55px;
  height: 55px;
  border-radius: 55px;
  background: #eee;
}
.skeleton-banner {
  height: 60px;
  background: #eee;
  margin-bottom: 15px;
}
.skeleton-productions {
  height: 20px;
  margin-bottom: 15px;
  background: #eee;
}
</style>

接下来,再新建一个skeleton.entry.js入口文件:

import Vue from 'vue'
import Skeleton from './Skeleton.vue'

export default new Vue({
  components: {
    Skeleton
  },
  template: '<skeleton />'
})

在完成了骨架屏的准备之后,就轮到一个关键插件vue-server-renderer登场了。该插件本用于服务端渲染,但是在这个例子里,我们主要利用它能够把.vue文件处理成htmlcss字符串的功能,来完成骨架屏的注入,流程如下:

clipboard.png

四、方案实现

根据流程图,我们还需要在根目录新建一个webpack.skeleton.conf.js文件,以专门用来进行骨架屏的构建。

const path = require('path')
const webpack = require('webpack')
const nodeExternals = require('webpack-node-externals')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')

module.exports = {
  target: 'node',
  entry: {
    skeleton: './src/skeleton.entry.js'
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    publicPath: '/dist/',
    filename: '[name].js',
    libraryTarget: 'commonjs2'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'vue-style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      }
    ]
  },
  externals: nodeExternals({
    whitelist: /\.css$/
  }),
  resolve: {
    alias: {
      'vue$': 'vue/dist/vue.esm.js'
    },
    extensions: ['*', '.js', '.vue', '.json']
  },
  plugins: [
    new VueSSRServerPlugin({
      filename: 'skeleton.json'
    })
  ]
}

可以看到,该配置文件和普通的配置文件基本完全一致,主要的区别在于其target: 'node',配置了externals,以及在plugins里面加入了VueSSRServerPlugin。在VueSSRServerPlugin中,指定了其输出的json文件名。我们可以通过运行下列指令,在/dist目录下生成一个skeleton.json文件:

webpack --config ./webpack.skeleton.conf.js

这个文件在记载了骨架屏的内容和样式,会提供给vue-server-renderer使用。

接下来,在根目录下新建一个skeleton.js,该文件即将被用于往index.html内插入骨架屏。


const fs = require('fs')
const { resolve } = require('path')

const createBundleRenderer = require('vue-server-renderer').createBundleRenderer

// 读取`skeleton.json`,以`index.html`为模板写入内容
const renderer = createBundleRenderer(resolve(__dirname, './dist/skeleton.json'), {
  template: fs.readFileSync(resolve(__dirname, './index.html'), 'utf-8')
})

// 把上一步模板完成的内容写入(替换)`index.html`
renderer.renderToString({}, (err, html) => {
  fs.writeFileSync('index.html', html, 'utf-8')
})

注意,作为模板的html文件,需要在被写入内容的位置添加<!--vue-ssr-outlet-->占位符,本例子在div#app里写入:

<div id="app">
 <!--vue-ssr-outlet-->
</div>

接下来,只要运行node skeleton.js,就可以完成骨架屏的注入了。运行效果如下:

<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>vue-skeleton</title>
  <style data-vue-ssr-id="742d88be:0">
.skeleton {
  position: relative;
  height: 100%;
  overflow: hidden;
  padding: 15px;
  box-sizing: border-box;
  background: #fff;
}
.skeleton-nav {
  height: 45px;
  background: #eee;
  margin-bottom: 15px;
}
.skeleton-swiper {
  height: 160px;
  background: #eee;
  margin-bottom: 15px;
}
.skeleton-tabs {
  list-style: none;
  padding: 0;
  margin: 0 -15px;
  display: flex;
  flex-wrap: wrap;
}
.skeleton-tabs-item {
  width: 25%;
  height: 55px;
  box-sizing: border-box;
  text-align: center;
  margin-bottom: 15px;
}
.skeleton-tabs-item span {
  display: inline-block;
  width: 55px;
  height: 55px;
  border-radius: 55px;
  background: #eee;
}
.skeleton-banner {
  height: 60px;
  background: #eee;
  margin-bottom: 15px;
}
.skeleton-productions {
  height: 20px;
  margin-bottom: 15px;
  background: #eee;
}
</style></head>
  <body>
    <div id="app">
      <div data-server-rendered="true" class="skeleton page"><div class="skeleton-nav"></div> <div class="skeleton-swiper"></div> <ul class="skeleton-tabs"><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li></ul> <div class="skeleton-banner"></div> <div class="skeleton-productions"></div><div class="skeleton-productions"></div><div class="skeleton-productions"></div><div class="skeleton-productions"></div><div class="skeleton-productions"></div><div class="skeleton-productions"></div></div>
    </div>
    <script data-original="/dist/build.js"></script>
  </body>
</html>

可以看到,骨架屏的样式通过<style></style>标签直接被插入,而骨架屏的内容也被放置在div#app之间。当然,我们还可以进一步处理,把这些内容都压缩一下。改写skeleton.js,在里面添加html-minifier

...

+ const htmlMinifier = require('html-minifier')

...

renderer.renderToString({}, (err, html) => {
+  html = htmlMinifier.minify(html, {
+    collapseWhitespace: true,
+    minifyCSS: true
+  })
  fs.writeFileSync('index.html', html, 'utf-8')
})

来看看效果:

clipboard.png

效果非常不错!至此,Vue页面接入骨架屏已经完全实现了。

尾声

本文实现了一套最简单的Vue页面骨架屏注入实践,如果想看更复杂一些的例子,可以参考《为vue项目添加骨架屏》这篇文章,本文的许多思路也是受其启发,非常值得阅读。

如果还有任何更好的实现思路,也欢迎和我探讨,有机会我也会总结基于React的骨架屏注入实践,敬请期待!

文章相关代码已经同步到Github,欢迎查阅~
查看原文

Nyaooo 关注了专栏 · 2018-08-22

某熊的全栈之路

知识,应该在它该在的地方。一个热爱代码,热爱新技术的程序熊。

关注 4990

Nyaooo 关注了专栏 · 2018-07-21

冰霜之地

冰霜之地

关注 11

认证与成就

  • 获得 7 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-04-14
个人主页被 120 人浏览