Zong
来晚了的2018年年终总结

回顾2018年

  • 01月 《深入React技术栈》
  • 02月 《你不知道的JavaScript 上卷》;初学 Webpack
  • 03月 《你不知道的JavaScript 中卷》
  • 04月 《你不知道的JavaScript 下卷》
  • 05月 《TypeScript 中文网》
  • 06月 《CSS世界》;参加第六期杭州 Node Party
  • 07月 初探 Node.js;五月天上海演唱会
  • 08月 《JavaScript设计模式》
  • 09月 浅析 jQuery 源码;参加 FCC 上海前端技术群线下Meetup
  • 10月 初探部分前端知识结构;台湾旅游
  • 11月 《深入理解 TypeScript》;《TypeScript 使用手册》;参加 VUE·CONF HANGZHOU 2018
  • 12月 《深入浅出Node.js》

回顾2018年职业展望

  • 学习 JavaScript
  • 了解 JavaScript 在不同宿主环境下的表现
  • 关注 ECMA 标准
  • 学习 TypeScript
  • 更多的使用流行框架(Vue、React)
  • 尝试 Angular
  • 尝试实践资源加载优化
  • 尝试实践渲染性能优化
  • 学习 Webpack
  • 更多的学习 CSS
  • 开始 Node.js 的学习之路
  • 更多的学习算法

接下来分享一下近期收获:

初探单元测试

受《深入浅出Node.js》影响,开始尝试对 Vue 和 React 版本的 To Do List 项目编写单元测试。在编写过程中感受比较明显的是 React 编写比 Vue 轻松许多,这反应着他们背后的社区对于单元测试方面的支持是否活跃,在实践过程中体验了 Travis CI ,也是感触颇深,感谢自动化集成工具为开发者带来的便利,这让我想到公司内部很多项目其实也可以借助这些工具去完成一些事情,比如系统发布,当然我在接触 Travis CI 的目的起初是为了实现 Coveralls 测试覆盖率的问题,当然最后我在 GitHub 上并没有实践成功。

这次之后,我认为这些都是在 NPM 上发布包必须要经历的历程,有必要熟悉这整一套现代化前端流程,并且加以实践。

再来说说单元测试的必要性?每个项目都一定需要单元测试吗?我认为并不是,对于敏捷开发或者小项目来说,我认为单元测试是不必要的,反而对于公司内部组件库,公用库,或者 Node.js 项目来说,单元测试是必要的,代码的健全影响着使用者,就像 NPM 上的库一样。同时,在多人维护时,单元测试代码也能起到保护原有业务逻辑的作用,使得代码并不会被改“偏”。但是项目最终是否需要编写单元测试,需要根据项目需求来决定。

Recycle List 实践

在之前的 VueConf 中,有提到关于 Recycle List 的感念,回来之后秉承造轮子的理念,尝试自己写一个,在写完后发现网上其实有另一种说法叫做虚拟列表,并且社区已经有很多实现。这次编写,直接使用 TypeScript 来完成编写,当然这只是一个小轮子,很多方面都未考虑到,仅实现了基本功能。开发思路其实很简单,首先我得知道这个列表的高度,那么我默认则是获取这个组件父级的高度的 100% 这种情况,当遇到父级中包含其他兄弟节点时,我会重新计算其(这个组件的)高度,同时这个组件是有滚动功能的,那么这个组件有且仅有一个子节点,就是虚拟节点,他是一个真正有高度的节点,他的高度受他里面需要滚动的内容进行设置,一开始出于开发方便,我把内容写死的,通过计算来设置这个虚拟节点的高度,并且使用 transform 来实现虚拟节点内的节点位置跟踪,那么这个简易的虚拟列表也就实现了。再是我是想把 children (虚拟节点内的节点)实现自定义化,但是我始终都没办法在开始渲染 DOM 的时候知道 children 的高度,因为那个时候太晚了。在参考社区后,我选用开发者可以配置一个函数来求得子节点的高度,因为这涉及到我需要计算出虚拟节点总高度,最终实现了以上所述功能。具体源码可以参考 GitHub 仓库 zongzi531/react-recycle-list

升级 Webpack 之路

一月份接手维护 2 个 React 项目(PC端、移动端),空闲之余试着去升级 Webpack ,借助 create-react-app 之势,替换 2 个项目除业务逻辑(/src)部分,"react" 目前版本 "16.7.0""webpack" 目前版本 "4.19.1"

先来说说 PC 端的升级之路,由于 PC 端是前后端不分离模式,要配合后端模板渲染,所以执行 eject 。一如既往,上手先配置 alias ,配置 antd 按需加载,配置 webpackDevServer.config.js 中的 proxy 来代理使用后端同事的服务,当然也可以代理至本地,只不过代理至本地的话,我每次都得自己跑后端服务,运行 mvn tomcat7:run 或者 mvn spring-boot:run ,很麻烦而且联调的时候若后端代码调整我需要拉取新的代码再重新跑,所以最后选择开发时代理后端同事的服务。

在配置好这些内容以后,项目跑起来,缺什么装什么、有什么警告按照提示消除、有什么报错解决什么报错。印象比较深的就是 React 16.3 生命周期中 UNSAFE_ 的几个钩子的调整,具体可以阅读 Update on Async Rendering。很快开发环境已经完成升级,但是因为是前后端不分离项目,所以需要打包至后端对应的目录下,配合后端模板引擎,在配置 Webpack 时,判断 isEnvProduction 情况下,关闭 inject 属性来编写自定义 index.html 。这和之前 Webpack 3.x 产出的 htmlWebpackPlugin 的对象有所出入,具体你可以自己打印这个对象来看需要如何输出,我是这样输出的:

<% for (var index in htmlWebpackPlugin.files.css) { %>
  <link th:href="@{<%=htmlWebpackPlugin.files.css[index] %>}" rel="stylesheet">
<% } %>
...
<% for (var index in htmlWebpackPlugin.files.js) { %>
<script type="text/javascript" th:src="@{<%=htmlWebpackPlugin.files.js[index] %>}"></script>
<% } %>

因为 antd 开启了按需加载,升级前项目已经使用了自定义主题,那么我这里也要刻不容缓的安装 less-loader ,配置很简单,官网示例是在没有 eject 情况下的教程,你自己加一个 rules 即可,需要特别注意的是,记得将 less-loaderjavascriptEnabled 选项开启。

借鉴 Vue 的 --report 命令功能,我也为项目安装配置了 webpack-bundle-analyzer ,以来观察打包后的文件体积,来实现资源加载优化,为增量更新、小幅度更新、 CDN 做准备。

来说说 CDN 吧,我也没闲着,我使用 --usecdn 命令,来实现打包可选性,因为目前项目还没有这个需求,但是我先准备好,以备不时之需。配合 externals 属性和 isEnvProduction 来提取公共库,来将公共库切换至 CDN 。这里遇到一个坑,那就是 antd。我需要对 antd 增加配置,因为 antd 切换至 CDN 后,并不是按需加载,而是全量加载,那么没问题,但是问题在自定义主题上。难道,我要放弃自定义主题,来重新 hank 这些样式吗?那这个肯定是不存在的,那这种情况,我选择将 antd 进行打包,但是 antd 的包需要与业务代码分离,毕竟 antd 加自定义主题的更新频率并没有想象的那么高,总之就是减少服务器压力。

到此,看似升级中遇到的问题都得以解决了,那么打包提交代码。过了一会,后端找我说图裂了,我一看。图没有切换到相对路径,此时修改 paths.jsgetServedPath 方法的代码即可:

const servedUrl =
    envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : './');

终于,大功告成。接下来说说移动端,上面大部分方案都适用于移动端,移动端使用的视觉库是 antd-mobile ,可以说是完完全全阿里视觉全家桶,但是移动端有着多端屏幕尺寸的问题存在,这里需要考虑去解决,同时这个项目使用了 CSS Modules ,在使用 less-loader 的同时,我需要对模块化进行区分,那么我统一对 getStyleLoaders 方法进行了改造,并对 preProcessor 参数对应的 loader 增加了 options 参数,核心代码如下:

// style files regexes
// ...
const lessRegex = /\.less$/;
const lessModuleRegex = /\.module\.less$/;

module.exports = function(webpackEnv) {
  // ...
  const isEnvProduction = webpackEnv === 'production';

  const stylePlugins = () => [
    // ...
    pxtoviewport({
      viewportWidth: 375,
      viewportUnit: 'vmin'
    })
  ]

  // common function to get style loaders
  const getStyleLoaders = (cssOptions, preProcessor, preProcessorOptions) => {
    const loaders = [
      // ...
      {
        loader: require.resolve('postcss-loader'),
        options: {
          ident: 'postcss',
          plugins: stylePlugins,
          sourceMap: isEnvProduction && shouldUseSourceMap,
        },
      },
    ].filter(Boolean);
    if (preProcessor) {
      loaders.push({
        loader: require.resolve(preProcessor),
        options: {
          sourceMap: isEnvProduction && shouldUseSourceMap,
          ...preProcessorOptions,
        },
      });
    }
    return loaders;
  };

  return {
    // ...
    module: {
      strictExportPresence: true,
      rules: [
        // ...
        {
          oneOf: [
            // ...
            {
              test: lessRegex,
              exclude: lessModuleRegex,
              use: getStyleLoaders(
                {
                  importLoaders: 1,
                  sourceMap: isEnvProduction && shouldUseSourceMap,
                },
                'less-loader',
                {
                  javascriptEnabled: true,
                }
              ),
            },
            {
              test: lessModuleRegex,
              use: getStyleLoaders(
                {
                  importLoaders: 1,
                  modules: true,
                  getLocalIdent: getCSSModuleLocalIdent,
                  sourceMap: isEnvProduction && shouldUseSourceMap,
                },
                'less-loader',
                {
                  javascriptEnabled: true,
                }
              ),
            },
            // ...
          ],
        },
      ],
    },
  };
}

感受 Redux 和 Immutable.js

Webpack 升级完后,我们来谈谈这个移动端的 React 项目,这个项目深度使用了 Redux ,基本所有的数据源都是通过 Redux 。整个项目在使用单一数据源的时候,我不知道该如何评价,不过至今我都比较好奇一个问题就是 Redux (Vuex) 和 sessionStorage 到底有什么区别,我在项目中能发挥到 Redux 的作用吗?是因为 sessionStorage 在无痕模式下无法使用吗?我一直很疑惑。在维护这个的项目的时候,我感受到单一数据源越写越深,不过同事是使用 Immutable.js 来解决这个问题的,都是 Facebook 的东西,果然配套啊。所以在创建项目的时候,有必要一定使用到 Redux 吗?这个问题值得思考……或者说,我对 Immutable.js 不熟悉使得我会对此疑惑,不过网上也有很多优缺点介绍,大家可以去看一下, What are the issues with using Immutable.JS?

再来谈谈这个项目的项目结构,是同事自己设计的,简单的展示一下项目结构:

├─actions  # Redux(子目录结构同 components)
├─components  # 组件
│  ├─basic  # 路由深度组件(用于控制 URL 层级)
│  ├─common  # 公共组件
│  ├─status  # 状态组件
│  └─TabBar  # 路由
│      └─Home  # 首页(如:/home)
│         └─T2  # 深度2
│             └─Search  # 搜索(如:/home/search)
│                └─T3  # 深度3
│                   └─Filter  # 筛选(如:/home/search/filter)
├─containers  # 业务(子目录结构同 components)
├─middleware  # Ajax
├─reducers  # Redux(子目录结构同 components)
├─store  # Redux
└─util  # 工具函数

开发过程中,会发现将视图和业务分的过于开,以至于维护成本提高。但是 Immutable.js 却给这个项目提供了很多便利。这与我之前写的项目结构还是有很大区别的,毕竟我只分 components (公共组件)与 views (视图)罢了。

体验 ECharts 地图穿梭

因为我司业务本身就是全国性质的,考虑到后期数据需要覆盖到全国范围,一月初接到需求,需要拓展此功能,编写了一个地图穿梭功能,可以从中国地图钻入省份或直辖市,再进行回钻的功能,那么话不多说,下面直接展示核心代码:(地区资源直接可以从 GitHub 上下载 apache/incubator-echarts

export default class FlexGeoChinaMap {
  constructor(el) {
    // 绑定 echarts 实例
    this.echart = echarts.init(el)
    // 存放城市编码
    this.city = new Map()
    // 穿梭栈
    this.mapStack = []
    // 当前穿梭栈信息
    this.currentMap = {}
    this.init()
  }
  async init() {
    await this.initCity()
    // 调用 echarts 钩子
    this.echart.on('mapselectchanged', params => {
      const [batch = {}] = params.batch || []
      // 获取当前 mapType
      const { mapType } = batch
      // 获取当前 mapCode
      const mapCode = this.city.get(mapType)
      if (!mapCode) { return }
      // 加载地图
      this.loadingMap(mapCode, mapType)
      // 加入穿梭栈
      this.mapStack.push({
        mapCode: this.currentMap.mapCode,
        mapType: this.currentMap.mapType,
      })
    })
    // 加载初始化地图
    this.loadingMap('100000', 'China')
  }
  async initCity() {
    // 获取当前中国城市信息
    const data = [] = await axios.get('china_address.json')
    for (const [k, v] of Object.entries(data)) {
      this.city.set(k, v)
    }
  }
  async loadingMap(mapCode, mapType) {
    this.echart.showLoading()
    // 获取当前城市信息
    const data = await axios.get(mapCode + '.json')
    this.echart.hideLoading()
    // 执行 echarts API
    echarts.registerMap(mapType, data)
    // 加载你想要的数据
    const res = await axios.post(/* url */)
    // 加载地图数据, setOption 可追加数据或参数,你可以自定义或根据业务需求传入即可
    this.echart.setOption(this.setOption({
      series: [
        {
          type: 'map',
          selectedMode: 'single',
          mapType: mapType,
          data: res,
        }
      ],
    }))
    // 设置当前穿梭栈信息
    this.currentMap = {
      mapCode: mapCode,
      mapType: mapType
    }
  }
  setOption(options = {}) {
    return Object.assign({},
    {
      toolbox: {
        feature: {
          // 穿梭返回
          myTool: {
            show: true,
            title: 'return',
            icon: 'icon:',
            onclick: () => {
              // 获取穿梭栈最后一个
              const pop = this.mapStack.pop()
              if (!this.mapStack.length && !pop) { return }
              // 穿梭
              this.loadingMap(pop.mapCode, pop.mapType)
            }
          }
        }
      },
    }, options)
  }
}

展望 2019 年

除夕即将来临,祝大家新年快乐! JavaScript 已经进入瓶颈期,那么学习新的 JavaScript 也成为了目标之一。同样需要更多的关注前端发展趋势,拥抱新技术、新思想。对于前端工程化方面需要进行更多的实践。尝试自己造几个社区流行的轮子。推动公司内部更多的去使用 TypeScript 。

  • 更爱女朋友,和她开心幸福在一起
  • 更爱家人,祝家人身体健康,平平安安
  • 锻炼身体
  • 关注 ECMA 标准
  • 关注前端发展趋势
  • 了解 JavaScript 在不同宿主环境下的表现
  • 更多的关注 NPM
  • 学习 ECMAScript Next
  • 学习前端自动化工程体系
  • 推动公司使用 TypeScript
  • 学习 Parcel
  • 学习 Next.js、Immutable.js
  • 学习 Jest 和 Enzyme
  • 更多的学习 CSS
  • 使用 yarn
  • 尝试造轮子
  • 尝试 SSR
  • 尝试 GraphQL
  • 尝试实践渲染性能优化
  • 尝试 Angular、Ember.js
  • 尝试 Progressive Web Apps
  • 尝试 WebAssembly
  • 学习框架源码
  • 更多的学习算法

新年博客的内容也会相应的和之前不一样,会更注重开发过程和思考。