海阔天空的云

我们在自己的世界里独自狂欢

0%


嵌套路由的应用

我们知道React-router v4中,可以实现嵌套路由。有了这样一个机制,我们不必将所有的路由完全放到一个统一的位置进行管理,而可以根据情况去对路由进行拆分。坦白讲,我一直认为在一个地方统一管理,要比四散开来维护起来更加简单(可能有点像中央集权和诸侯国各自为政的感觉)。但是最近有个实际应用,让我发现,嵌套路由的价值还是很大的。只是以前我们一直没有合理使用。

先说一下实际使用场景。

我做的电商应用中,有一个叫做“爱逛店”的功能。(由于该应用主打线上线下融合,线下发展其实更好),每次从首页点击“爱逛店”图标进入“爱逛店”的门店展示列表,由于展示门店列表的过程是:

获取当前用户地理位置坐标=》后端根据地理位置远近排序=》将排好序的门店列表给到前端=》前端展示

造成展示门店列表的过程其实是相对比较慢的。为了能够有一个尽量好的体验,我们在展示门店列表前,会有一个loading的动画效果。而由于采用路由统一管理的机制,原来的门店列表的组件和门店详情的组件之间是并列关系,导致每次从某一个门店详情页面返回到门店列表页面,都会造成门店详情页面组件生命周期结束,门店列表页面进入下一个生命周期。进入下一个生命周期意味着什么呢?意味着它又需要重新走一遍上面的流程,尽管有了这个loading的动画效果,但是体验上还是会差很多。而且,更影响用户体验的是:这个loading效果本来是好意,能够让用户在等待期间看到些东西,但是由于浏览器的机制,这个时候,浏览器判定你初始页面高度只有100%(屏幕高度)这么高,则一旦门店列表列出来之后,浏览器不会再将页面滚动到上一次这个页面滚动到的位置了。这意味着,你从门店详情页面返回到门店列表页面,每次先看到loading效果,然后每次看到的都是最顶上的三五个门店,而不是你上次浏览到的门店,这体验,当然就更差了。

有什么解决办法吗?当然了。使用嵌套路由,上面的问题就都不是问题了啊。

首先明确我们的需求,由于我们的用户看到的门店列表是基于地理位置的,他的大致地理位置短时间内是不会改变的。(我指的是用户点进门店详情页面时在北京,点返回后就来到了上海),门店的数量短时间内也是几乎不会改变的。(我指的是用户点进门店详情页面时有100家门店,点返回后就有105家)。正是基于这样的一个判断,我们或者可以将这些门店的数据暂时存储起来。每次用的时候,直接取出来用就好了。那么这个时候问题来了,我们应该将这些门店的数据存储在哪里呢?浏览器的本地存储(localStorage,sessionStorage)? 又或者是代码里?

解决办法

不绕圈子了。我的办法是不如增加一个”爱逛店”管理组件。采用嵌套路由的方式来实现,门店数据的存储。先在主路由配置处(我的项目是app.js)写上:

        <RouteWithLayout path="/stores" hideFooter={false} loader={() => import('./Store/Store')} />

然后先看下面这段代码:

        <Store>
                <Switch>
                    <RouteWithLayout exact path={`${this.props.match.url}`} hideFooter={false} loader={() => import('./StoreList')} stores={this.state.data} withLayout={false} />
                    <RouteWithLayout exact path={`${this.props.match.url}/:storeId`} hideFooter={false} loader={() => import('./StoreDetail')}  withLayout={false} />
                </Switch>
        </Store>

从上面代码我们可以看出来,无论用户想要浏览某一个门店(StoreDetail),还是说想要浏览门店列表(StoreList),都需要先进入Store这个管理组件。我可以用这个管理组件来做什么呢?很简单,以前没有这个管理组件的时候,从/stores这个路由切换到/stores:storeId这个路由,第一个组件生命周期结束,第二个组件生命周期开始。而由于我把获取门店列表的方法写在了StoreList这个组件里,因此每次从门店详情页点返回都会重新去找门店列表。现在,我要做的是,把获取门店列表的方法提到上一层,提到Store这个组件当中。

这个时候,我们再来看看。如果/stores这个路由切换到/stores:storeId这个路由

组件 生命周期
Store didupdate
StoreList unmount
StoreDetail didmount

而如果/stores:storeId这个路由切换到/stores这个路由呢?

组件 生命周期
Store didupdate
StoreList didmount
StoreDetail unmount

没错,无论哪种场景,Store这个组件都只会update,而不会进入下一个生命周期。因为在一开始我就配置好了。

        <RouteWithLayout path="/stores" hideFooter={false} loader={() => import('./Store/Store')} />

因为在/stores这个路由的模糊匹配下,我任意切换路由,都只会让Store这个组件更新,不会触发didmount,那么我干脆把获取门店列表的方法写到他的componentDidMount()之中不就好了么,然后门店列表作为一个state值存储在这个组件的状态里,之后每次切换路由都能够立刻获取门店列表,无需loading,浏览器也能够记住上次这个页面滚动到的位置,返回后,还是原来的位置。

那位又说了,可是我要是直接在浏览器上输入某个门店的url,直接访问某个门店。你是不是还要把这个门店列表也给我获取到呢?那是不是得不偿失,更加影响用户体验呢?说的很对,所以我的策略是,在获取门店列表的方法中进行判断,只有exact match path="/stores"这个路由的时候,我才去从服务端获取门店列表。那位又说了,那你这时候如果,如果点击页面上的返回导航,你回到门店列表页,由于在Store组件didmount阶段你没有获取门店列表,这个时候肯定会出错啊。是的,那我就不返回门店列表呗。反正用户你也不是从门店列表进入的我某个门店页面的,我直接让你回我的APP 首页就好了嘛!

除了用户体验的提升外,另外的好处是:随着你项目的扩大,这个对路由的分拆也能够便于管理。毕竟如果你的主路由配置里有一两百行路由配置,看起来也会很不舒服,类似的哲学是从中央不直接管辖所有的地级市(中国有338个地级市)而将这些市根据地理位置,由省来管理。中央只管理省一级。

总结

前面说的很具体,具体的东西好处是便于理解。坏处可能就是,不能做到举一反三。所以我这还是来总结一下。

如果你有一个应用场景:数据不会频繁更新,数据的请求过程影响用户体验,你的用户需要频繁地从子路由到孙子路由或孙子路由到子路由之间来还切换。那么可以考虑在子路由组件中,进行嵌套路由的配置,将请求数据的方法写在子路由组件当中,这样一来,用户体验会有一个很好的提高。


一个电商应用,有他的一个基本的结构。为了简化我们的代码,我们每一个页面组件外面都会再套上一层<BaseLayout/> ,通过配置<BaseLayout/>,我们可以控制每一个页面是否需要使用公共页头(header),公共的底部导航(footer)。这当然也是一些很基础的功能了,在此不再赘述。

基于上面的需要,我们最初写了下面的代码

        const RouteWithLayout = ({loader = null, exportName = null, hideFooter = true, hideReturnTop = false, ...rest}) => {
                const loadableOpts = {
                    loader,
                    loading: LoadingComponent
                };

                if (exportName) {
                    loadableOpts.render = (loaded, props) => { // eslint-disable-line  react/display-name
                        const Component = loaded[exportName];
                        return <Component {...props} />;
                    };
                }
                const LoadableComponent = Loadable(loadableOpts);
                return (
                    <Route {...rest} render={matchProps => (
                        <BaseLayout hideFooter={hideFooter} hideReturnTop={hideReturnTop} {...matchProps}>
                            <LoadableComponent {...matchProps} />
                        </BaseLayout>
                        )}
                    />
                );
        };

看了前面 part-1的同学可能知道,我们在项目中引入了React-loadable这个第三方库,上面的代码也是基于它的实现。乍一看,他似乎没有问题。接下来,我们只需要写这样的配置就ok了。

        <RouteWithLayout exact path="/shopping-cart" hideFooter={false} hideReturnTop={true} loader={() => import('./ShoppingCart')} />

但是,在2-实现一个类似客户端的商品轮播图阅览交互 里面,我也已经提到过,我在此处遇到了一个坑。这个坑,也正是与上面我展示的代码有关。

再来简单回顾一下,当路由从/products/:productId 到/products/:productId/showpic,事实上,动态引入的组件还是那个ProductDetail组件。按照我们的期待,如果前后两次引入的是同一个组件,这个组件需要update,而不是重新mount。但是就如同我在《React开发实践–2》里面介绍的那样,当路由发生改变的时候,</ProductDetail />竟然进入了下一个生命周期。

究其原因,由于路由发生了改变,如果RouteWithLayout没有写其他的生命周期方法的话,那它必然会得到更新。那么我们来看看RouteWithLayout组件更新之后的影响。这时候我们发现LoadableComponent这个变量的引用实际上发生了变化。因此造成当render <LoadableComponent {...matchProps} />时,需要到下一个生命周期重新render。

可是对象引用发生了变化这种事情,我们应该怎么理解呢?说起来,这其实考察的还是JavaScript的基础。让我们暂时忘掉React这个框架,只提JavaScript。来看看下面这几行代码

let arr = []
for(let i=0;i<2;i++) {
    const foo={a:1};
    arr.push(foo)
}

console.log(arr[0]===arr[1]);  //false

大概学过半年JavaScript的同学,也都能明白这里为什么arr[0]和arr[1]并不相等了。这里还是谈谈我对这件事情的理解:

对于for循环内部的每次执行,都是一个block,每一个 block之间都相互独立。所以,我们也可以将上面的代码进行拆解。

let arr = []
 {
    const foo={a:1};
    arr.push(foo)
}
 {
    const foo={a:1};
    arr.push(foo)
}

console.log(arr[0]===arr[1]);  //false

这个时候,就更好理解了。对于每一个块里面的i变量来说,他只在当前块内有效,当前块执行完毕之后,他就会被垃圾回收,销毁掉了。因此当我们比较的时候,arr[0]和arr[1]之间就不相等了。

有同学可能会问了,你用的是ES6 的语法。ES5是这样的吗?of course!

var arr = []
for(var i=0;i<2;i++) {
    var foo={a:1};
    arr.push(foo)
}

console.log(arr[0]===arr[1]);  //false
console.log(i);

关于我的这个示例,ES5 和 ES6的结果并没有区别。如果硬要说有什么区别的话,那就是i这个变量是否在块级(block)之外继续有效。那相对于我本次讨论,实际上是另外一个话题了。

既然说到了这里,我们再想一想。怎么样才能让arr[0]与arr[1]相等呢?在JavaScript,只能是在比较的两者为同一引用的情况下,才能实现严格相等。所以按照这个思路,我们可以对上面的代码进行个改写。

const foo={a:1};
let arr = []
for(let i=0;i<2;i++) {
    arr.push(foo)
}

console.log(arr[0]===arr[1]);  //true

没错,就是把foo的生命提到for循环之外。这样的话,每次推入到arr这个数组中的值的引用就一定是相同的了。

这时候,让我们重新回到刚才遇到问题的React代码。RouteWithLayout当前是一个无状态组件,只要它的父组件发生了更新,它也会相应地发生更新。然后执行相应的代码。这其实就类似于我们上面提到的for循环结构。每一次RouteWithLayout执行的时候,都像是for循环在执行其中一个block,不难理解,每一次LoadableComponent的引用发生了变化。

所以,还是上面的思路,我们把LoadableComponent的声明放到render()方法之外,怎么做到呢?改写无状态组件为有状态组件。将这个对象声明以状态的方式存在于组件当中。这样就可以实现我们想要的效果,即如果动态加载的是同一个组件,则组件只会发生更新,而不会进入下一个生命周期。相应的代码在《React开发实践–2》中已经提到过了,再粘贴一遍。

        `
        import React from 'react';
        import ReactDOM from 'react-dom';
        import PropTypes from 'prop-types';
        import {BrowserRouter, Route, Switch, Redirect} from 'react-router-dom';
        import BaseLayout from './BaseLayout';
        import Loading from 'react-loading';
        import Loadable from 'react-loadable';


        const LoadingComponent = props => {
        // something 
        };


        class RouteWithLayout extends React.Component {
            state = {
                loader: () => {},
                exportName: null,
                LoadableComponent: null
            }

            static getDerivedStateFromProps(nextProps, prevState) {
            
                if (nextProps.loader.toString() === prevState.loader.toString() && nextProps.exportName === prevState.exportName) return null;
                const { loader, exportName } = nextProps;
                const loadableOpts = {
                    loader,
                    loading: LoadingComponent
                };

                if (exportName) {
                    loadableOpts.render = (loaded, props) => {
                        const Component = loaded[exportName];
                        return <Component {...props} />;
                    };
                }
                return {
                    loader,
                    exportName,
                    LoadableComponent: Loadable(loadableOpts)
                };
            }

            render() {
                const { hideFooter, hideReturnTop, ...rest} = this.props;
                return (
                    <Route {...rest} render={matchProps => (
                        <BaseLayout hideFooter={hideFooter} hideReturnTop={hideReturnTop} {...matchProps}>
                            <this.state.LoadableComponent {...matchProps} />
                        </BaseLayout>
                        )}
                    />
                );
            }
        }`


写在前面

这篇仍然并不会也不打算去介绍或者科普React框架的基本知识点,但是会在行文中有很多涉及。

怎样实现的思路

一个电商APP, 要想让他的web HTML5 体验更接近客户端本身还是有很大的挑战的。怎样做出一些更符合客户端用户操作习惯的交互来,是前端开发中很重要的一个点。

我想要做的是一个类似客户端的商品轮播图阅览交互,总体来说,他的主要逻辑是:

  1. 在商品详情页显示商品轮播图
  2. 点击任意一张商品轮播图,能够全局浏览商品轮播图
  3. 通过左划右滑手势可是在全屏状态下阅览商品轮播图
  4. 点击返回键,退出全屏,回到商品详情页

有赖于react-photoswipe 这个库的支持,它已经将前三项做得很好了。我希望做的其实是第四项。细化需求,它不仅仅要求能够回到商品详情页,还希望能够最大限度地优化性能,最好的体验就是像客户端那样。

这里,我采取的措施是,将我集成了react-photoswipe 这个库的组件放到</ProductDetail /> 组件之中。

1
<ProductCarousel images={product.images} alt = {product.name} />

接下来,我的想法是这样的:

当用户在商品详情页点击任意一张商品轮播图时,页面上全屏显示那张商品轮播图,同时,当前的路由发生改变,由/products/:productId/products/:productId/showpic,这样,当用户点击返回的时候,就能够实现从当前路由/products/:productId/showpic返回到了/products/:productId

但是,这个时候,我遇到了很多与React-router有关的坑,其实与其说是坑,倒不如说是知识上的不足。当我把这些知识补足之后,再来看,其实并不难了。

React-router

第一个问题: exact

很明显,第一个问题是关于React-routerapp.js 中的配置的。前面介绍了我的总体思路,这个思路的一个关键点是当路由发生改变的时候,</ProductDetail />组件不应该unmount 或者重新didmount, 这样才能够让这种轮播图阅览交互更像是客户端的体验。

这时候,用到的第一个知识点是React-router的exact 配置。这本身并不是什么难点,我在这里只是想说明它的这一妙用。

由于我们大部分的React-router的匹配都采用了exact的匹配,在这个时候,我把原来对商品详情页的exact匹配去掉了,使得能够实现前述我的思路。

第二个问题: </ProductDetail />组件的生命周期

前面说了,我们在这个项目中,为了优化前端性能,使用了动态加载,来进行split coding。这个时候,我发现,如果使用了动态加载,我的</ProductDetail />组件每次进入轮播图全屏页面时,都会因为进入到了下一个生命周期,而不能进入轮播图全屏页。

这个问题出在哪里呢?原来这个问题是由于React-router以及它所关联的React生命周期造成的。

我以前有个误区,不知道各位读者是否也会存在这样的误区。就是React简单地认为它是单向数据流,除非父组件传递給子组件能够改变父组件本身的属性,否则,子组件的更新,不会引起父组件的更新甚至进入下一个生命周期。

但是,当然没有那么简单。

还是以我上面的问题为例,为什么会出现那样的问题呢?原因是:当一开始进入轮播图详情页面时,在我的<ProductCarousel />这个子组件发生了变化,他引起了路由的变化。由于我的路由管理由React-router完成,实际上顶部的<Switch>也相应地发生了更新,甚至可以这么说, <Switch>组件也必须发生更新,只有这样,他才有存在的意义。而紧接着,由于我们对动态引入的错误使用,造成了</ProductDetail />组件被迫重新装载,而他的重新装载又直接造成了他进入下一个生命周期。

关于我们是怎样错误使用动态引入的,以后会详细来解释。

这里,还是针对我刚刚提到的误区来谈一谈。所以,还是说回来,这当然不能算是坑,这是对知识理解的不深刻。

看到这个问题之后,我最初想到的是: 如果路由改变了,而加载的组件(import)并没有改变,就不更新这个组件的状态

但是,马上就发现了问题,还是这个例子:路由从/products/:productId/products/:productId/showpic发生了改变,这个时候我们没有改变</ProductDetail />组件的状态,则他的属性(location,props)较之前并没有发生改变。这本身就不是正常的,也就会从根本上造成虽然一个问题解决了,但是总还有新的问题产生。为什么呢?因为你没有按照规律办事啊!

React-router的思想是什么呢?其中一个思想就是组件上的React-router赋予的属性(location,history )是与实际相一致的。但是刚才,我试图通过调用 componentWillReceiveProps方法,来实现阻止组件更新的目的。

结果造成什么问题了呢?

一开始,我点击轮播图,路由切换,打开轮播图,没有问题。可是当我点击返回键的时候,由于我前面的判断:如果路由改变了,而加载的组件(import)并没有改变,就不更新这个组件的状态,导致没有退回到商品详情浏览页。这当然是不正常的了。

我们应该顺应React-router的设计思想,最后采取的办法是:如果路由改变了,而加载的组件(import)并没有改变,这个装载的组件不变,但他的属性(location,history)要相应地发生改变,他必须仍然在原有的生命周期中

可能听起来,还是有点抽象,还是直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import {BrowserRouter, Route, Switch, Redirect} from 'react-router-dom';
import BaseLayout from './BaseLayout';
import Loading from 'react-loading';
import Loadable from 'react-loadable';


const LoadingComponent = props => {
// something
};


class RouteWithLayout extends React.Component {
state = {
loader: () => {},
exportName: null,
LoadableComponent: null
}

static getDerivedStateFromProps(nextProps, prevState) {

if (nextProps.loader.toString() === prevState.loader.toString() && nextProps.exportName === prevState.exportName) return null;
const { loader, exportName } = nextProps;
const loadableOpts = {
loader,
loading: LoadingComponent
};

if (exportName) {
loadableOpts.render = (loaded, props) => {
const Component = loaded[exportName];
return <Component {...props} />;
};
}
return {
loader,
exportName,
LoadableComponent: Loadable(loadableOpts)
};
}

render() {
const { hideFooter, hideReturnTop, ...rest} = this.props;
return (
<Route {...rest} render={matchProps => (
<BaseLayout hideFooter={hideFooter} hideReturnTop={hideReturnTop} {...matchProps}>
<this.state.LoadableComponent {...matchProps} />
</BaseLayout>
)}
/>
);
}
}

第三个问题: 匹配不匹配

我们知道,在客户端的图片阅览里,实际上,是不能直接通过访问地址的方式,来全屏浏览商品轮播图的,这是一个更深层次的交互设计,因此我在</ProductDetail />组件的componentDidMount 方法中写下了这样的代码

1
2
3
if (!this.props.match.isExact) {
this.props.history.replace(this.props.match.url);
}

如此依赖,当初次载入</ProductDetail />组件时,一律显示商品详情页面。

最后

最终的实现效果,前面提到的指标都已经完成了。也能够比较完美地使用动态加载来加载组件了。


写在前面

最近,在实际项目中,遇到了一个坑。正好,借由这个坑,来多聊一聊前端的东西。

需求

背景知识

  • 我们的技术栈是React + React-Router+webpack4 的结构
  • 我们的应用是电商网站,所有页面会有一些公共的部分。因此,我们需要在实际用到的组件外面,再包裹一层组件,我们叫做

知识点1 动态加载

在 webpack 2或 webpack 3的时代,我们自己写了一个动态加载的实现。
这里的核心,大概就是下面这一行代码了。

1
loader={() => System.import('./ProductDetail').then(c =>  c.default)}

Module 的加载实现

https://github.com/systemjs/systemjs

包括我们当时,还自己实现了一个组件。嗯,他其实也是一个高阶组件。

到后来,我们引入了React-loadable 这个组件,同时由于能够使用babel-plugin-syntax-dynamic-import这个babel 插件,我们直接在引入组件方式上发生了改变。原来,我们的引入方式是System.import,现在我们则可以将import去掉,直接用来自未来的JavaScript语法规则来实现我们的需求了。

后来,阅读源代码我发现,我们自己实现的那个动态加载的高阶组件(),基础逻辑是相同的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
return class LoadableComponent extends React.Component {
constructor(props) {
super(props);
init();
}

render() {
if (this.state.loading || this.state.error) {
return React.createElement(opts.loading, {
isLoading: this.state.loading,
pastDelay: this.state.pastDelay,
timedOut: this.state.timedOut,
error: this.state.error,
retry: this.retry
});
} else if (this.state.loaded) {
return opts.render(this.state.loaded, this.props);
} else {
return null;
}
}
}

在这里,只截取了部分代码。

我在这里,就不展示我们以前的组件实现了。我们只实现了React-loadable最基础的功能,事实上它还能够有以下几点优势:

  • 当加载组件的过程中,给出过渡效果
  • 当加载失败、加载错误后,给出一个类似404的页面,不致于白屏
  • 能够进行retry 等行为

正是因为它有这些好处,并且有很多人的实践,我们才用到了它。

但是,无论是在引入React-loadable 之前还是之后,都有一个坑,我试图搞定它,却一直没有搞定,直到最近,才终于把这个坑填上。

下回《2-实现一个类似客户端的商品轮播图阅览交互》


1 html-webpack-plugin 已支持webpack 4
2 inline-manifest-webpack-plugin 的升级替换

inline-manifest-webpack-plugin 插件本身已支持webpack 4, 但从测试看有bug,不能将生成的manifest.js 插入到frontend-js.html 文件中。

解决方案,使用webpack-inline-manifest-plugin
https://github.com/szrenwei/inline-manifest-webpack-plugin/issues/10

3 extract-text-webpack-plugin 的升级替换

项目主页:https://github.com/webpack-contrib/extract-text-webpack-plugin

Since webpack v4 the extract-text-webpack-plugin should not be used for css. Use mini-css-extract-plugin instead.

官方推荐使用 mini-css-extract-plugin 代替

因此根据mini-css-extract-plugin(https://github.com/webpack-contrib/mini-css-extract-plugin) 项目介绍 ,添加了一些mini-css-extract-plugin 的高级配置

4 CommonsChunkPlugin 废弃

https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366

提取common vendor。

5 不再使用 inline-chunk-manifest-html-webpack-plugin

项目没有更新,插件不支持webpack 4,考虑到本插件优化作用不大,可以先不用。

6 统一使用url-loader 处理非js,css(scss)等资源
webpack在处理非js和css(scss)时,统一使用url-loader 。url-loader 本身是对file-loader 的扩展,当文件大小超过规定的限制时(目前limit为3kb),则将文件资源输出到指定文件夹中。

7 优化首屏,减少网络条件差时的白屏时间

head 里面的css 阻塞进程,使用webpack 3 时的CSS文件637kb。使用webpack 4 后,将url-loader limit 限制在3kb,使转base64 的资源越少越好,尽量不占用css的的空间。目前生产环境CSS 文件大小474kb。

8 升级后变化

升级前 build Done in 34.25s.
升级后build Done in 22.72s.


我在去年这个时候,写了 《博客三周年小记》 ,时隔一年,这个博客,终于又更新了。(我太懒了,连头图都用的去年的)

我自己其实也没有料到会是这么长的更新周期,但是正如看官老爷们所看到的,的确如此。在这里我就不再废话,找寻其他不更新的理由了。

因为有了一年的更新周期,我甚至觉得这篇博客可以作为我这过去一年的回顾来看。

1.工作

过去的一年,在同一家公司完整地工作了一年。对我来讲,过去的一年,还是有很大的成长。

都在哪里有提升

经验积累

我刚刚找工作那会儿,因为跨专业,因为没有工作经验,找工作并不容易。我自己也曾经想,究竟工作经验这种事情重不重要?如今,我已经正式工作快两年了,如果再问我工作经验重不重要,我可能会回答确实很重要。

哲学上有说从量变到质变的概念,工作经验的积累,在我看来就是量变到质变的过程。在我最初找工作那会儿,也会去背一些面试题,但是坦白来说,有些面试题,即便是背下来了,却还是不知其然,更不知其所以然。举个例子,作为一个前端,经常会有的面试题会问到“前端如何做性能优化”,在最早的时候,完全没有实践,就是去背那些答案。这些答案当然正确,但是却并没有经过实践,只是理论。可是有了这方面的工作经验,甚至是坑之后,想不去明白这些道理都是不行的。让实践跟理论一起去碰撞,实践去印证理论,理论再去指导实践,应该还是一个比较可靠的学习和工作方式,而这,都是要靠时间来积累经验的。

但是又没有必要把工作经验看得特别重要。这也让我想起来,前两天看热门日剧《unnatural》,其中有一集讲到两个解剖的法医比较谁的说法更可靠,一个老头说他解剖过15000具尸体,而由石原里美扮演的女主角则只解剖过1500具尸体,两者之间差了十倍的工作经验,但最终的结果当然是有光环护体的女主战胜了法医权威。所以,还是不能盲目地去崇拜工作经验,以为工作经验多了,水平就一定会直线提高,有时候并非如此。

说回来,对我来说,过去的一年,增长的工作经验,更多时候,是让我去印证之前在书本上看到的理论。让我明白,原来书上短短的一两句话,在实践之中就有如此多的门道。而这,正是我过去一年体会的。如果说的再明白一点,我认为,我关于前端的大部分知识,都是在学习前端的第一年获取到的。而这些获取到的知识,经过工作,消化吸收,则是在过去的一年完成甚至还没有完成,需要继续完成的。

端到端的打通

我在上家单位的时候,做的就是一个纯前端开发的工作,而在过去的一年里,我做起了全栈开发。技术栈是 python + react + postgresql 。起初,对我来说,这样的开发实践是一个很大的挑战,毕竟在此之前,我只是用python 写过简单的爬虫,关于数据库,甚至都没有动手写过一个查询语句。对于这个技术栈的后端,我几乎是从0开始做起,但是好在老大给了我很大的耐心。让我能够去补齐这些短板。我也的确觉得自己还是需要这样的一些全栈工作的训练的,这确实让我更能够理解计算机,理解互联网。以前有一个很有名的面试题,大概是说从用户用键盘在浏览器地址栏上面敲出google.com 到出现页面之间,发生了什么。这个面试题我自己从来没有遇到过,但我看很多人回答过,的确呀涉及到很多的知识。这些知识,当然不止有前端知识,还有很多更广阔的知识,包括计算机网络,通讯等,想的越多,能关联到的知识,学科领域就越多。毕竟,既然有些人能够因为别人是计算机专业的,就去找他修电脑。

我也因为自己的“野路子“出身而出过一些问题,比如在写一些API调用方法的时候,不写异常处理。给自己找理由说前端JS业务代码几乎从来不会进行try catch 的异常处理。也会因为一个break和 continue 的区别,而给代码造成bug。我也试想,如果我在大学里面修了完整的计算机课程,有了基本的计算机素养,或许会好很多。以前,我只写前端的时候,更像是在造空中楼阁,越造可能越虚幻,不稳定。而有了这种端到端的训练,对我来说,即便是以后不再写后端逻辑,也是受用的,至少基础更加扎实了。

前端重要吗

最近正好听了一期podcast,是《anyway.fm》的一期。他们请来了一个做设计的嘉宾进行交流,这位嘉宾提到自己在前雇主那里的收获的时候,说他发现原来设计没有那么重要。换到我自己这里,我也在问自己一个问题,前端重要吗?我的回答其实跟那位嘉宾差不多,用户前端所见所用(UI/UE)对于用户来说也可以笼统地算作设计,设计没有那么重要,前端也没有那么重要,一个电商网站,最重要的是他的商业运作,是能够把货物卖出去卖得好,乃至于无论前端,后端,设计,都不过是工具辅助工具而已,最终服务的,都是商业行为和商业逻辑。所以,我也看到很多程序员后来转型做了管理或者当了产品经理,这里面也不是没有道理的。

明确定位很重要,知道自己吃几碗干饭也很重要。知道了这些之后,如果你再说,我就是喜欢前端,或者说我就是喜欢开发,我就是不喜欢做产品分析,有问题吗?当然没有问题。这就好像是这个社会的分工,每个工作都有他的价值,但总有一些更加重要。我曾经听说某富家千金想以后当一名面点师,面点师当然也是社会的一个重要分工,但他显然这个社会最重要的,没了他,社会还是能够照样运行。但摆正位置,做自己喜欢做的事,或者能够做的事,很重要。

2.生活

过去的一年,我的生活稳定了,相比于毕业那年(2016年)的确稳定了,这种稳定有时候会让我焦虑,有时候又让我无所适从,我有时候也会面临很多新问题,比如相亲。生活当然精彩,在此省略一万字。

记录生活的地方很多,我的推特,我的公号,也欢迎各位看官关注。

3.获取信息

既然是因为博客四周年的由头,写的这篇博文。还是想说说跟博客有关的,过去的一年里,我还是会通过RSS来获取大量的信息,不过RSS里面几十个订阅源,活跃的也的确越来越少了。更多的时候,我会选择在工作日的中午饭后,打开Inoreader这个RSS阅读器进行阅读,这时候很大部分内容是好奇心日报的feed。好奇心日报是我最近一年在看的一个feed,也可以推荐给各位看官老爷。除此之外,我的订阅源里,独立博客虽然也偶尔更新,但是由于我自己很少更新博文了,导致其他博客更新之时,我也没有了去评论互动的欲望。虽然如此,我还在用这种非常传统的方式获取信息,我觉得是很高兴的。我没有用今日头条,但是会用微信的看一看,会关注微信上面有趣的公众号。

4.最后

这篇更新完之后,下次更新会是什么时候呢?


博客三年

我很难说我的博客到底出生在哪一天,我在这个博客一周年的时候,写过《给六个人写博客》 这样的纪念博客一周年的文章,在那里面我也提到这个博客域名是在14年的5月13日购买的,然而实际搭建起来博客,却又是之后的十几天以后了。而到了5月24日,才有了这个博客历史上的第一篇文章,所以评判的标准不同,得到的关于这个博客出生日期的结论也不相同。这个问题当然不算很重要,大致是14年的五月份,我建立了这个博客,这一点是毋庸置疑的了。

我之所以花一定篇幅说明上面一点,大概也是为了证明:这个博客真的已经三年了。

而我之所以要证明他的三年,则是因为三年时间看似很短,然而在中国互联网来说,却又像是一个世纪。由于我这几年一直有订阅RSS,在RSS阅读器上阅读博文的习惯,从博客圈的角度来看,也能看到这三年来,大量的原来关注的博客停止了订阅,当然令人欣喜的是,也看到了一些新的博客涌现了出来,往往也总能给人眼前一亮的感觉。长江后浪推前浪之下,能够坚持三年,的确也是一件并不容易的事情了。

写博客的好处

我在那篇《给六个人写博客》 其实也提到了写博客的好处,然而其实都是比较形而上的东西。我这次,想要举出更细致的切身体会。

写博客让我知识积累

这应该算是一个常识,从我的实际体会上看,我很享受这样的过程。所谓的好记性不如烂笔头,其实说的也是这个道理。

我想谈谈我是怎样利用博客这个工具的:博客写好发布之后,生成新的RSS,通过IFTTT,实现文章自动同步到印象笔记,实现文章自动发布到推特。这样的话,其实知识汇总的大本营其实是印象笔记了,然后如果想要搜索特定的知识点,只需要打开印象笔记,搜索相应的关键字就可以了,这个时候,无论是博客上的内容,还是平时书写的比较私密的笔记,都可以快速阅读。而我最近也在用另外一款Android App,名字叫做 timehop ,他能够将你“历史上的今天”一些社交媒体上发布过的内容全部展示出来,作为一个自己对自己的回顾,在过去的两个月里,我每天都会打开这个App,看上少说几分钟,多则十几二十分钟,理由也很简单,我希望看到自己相比于过去的自己的进步,刚刚提到过,我使用IFTTT实现了博文发布后发出一条推特内含博文链接的功能,到这个时候,我也能回顾过去自己的文章了。这是一个在我看来很Cool的功能,因为你要知道,不是所有人,甚至可能大部分人,都没有定期翻阅笔记的习惯。拿我自己来说,即便是高中时代的自己,面对着那丑陋的笔记本上的字体,也没有想要翻动的意念。所以,我很推荐timehop,不过他目前的支持的社交媒体仅仅包括推特,非死不可,Google photos,instagram ,dropbox等,并不包括国内的新浪微博,微信等,所以可能还是有点水土不服,不过在我而言,倒也是足够了,毕竟我也一直自诩谷歌粉,呵呵呵。

博客和社交

我并不是一个特别注重网络社交的人,虽然在RSS 上添加了很多个人博客,实际上也很少会去留言互动,只是默默地阅读文章,我在我的友情链接列表里,也没列出来几个人。尽管如此,我仍然要说,博客确实帮助我进行着社交这样一个工作,三年来,得到了很多的评论,这里也要感谢即将淡出历史舞台的多说评论系统 ;由于在博客上留下了自己的个人微信号,也加了几个网友,偶尔会在朋友圈互动;之前找工作,由于在简历上留下了博客地址,在论坛上发帖求职,也让博客访问量有所增加,又有一些有志前端的网友与我在博客上讨论;我的转行系列 ,也让我收获了很多意想不到的面试机会和良好的个人印象,甚至于也在一定程度上帮助我找到了一份满意的工作。

当然,我们也不应该过分地夸大博客的作用,在当今这个自媒体的时代里,传统的博客式微,自媒体甚嚣尘上,传统的个人博客影响力远远没有一个有一定推广的自媒体公众号,这也是为什么在过去的三年里,我眼睁睁看着订阅的RSS一个个停止更新,传统的博客也都转投自媒体公众号去了。尽管如此,个人博客还是有其存在的意义,正如那篇《给六个人写博客》 中一个观点,每个独立博客都是一个小岛,我们通过友情链接、通过外链从这个岛屿跳到那个岛屿,来回穿梭,自由前行,这也是我理解的 Internet 的一大意义了。

近期计划

我最近在想这个博客的定位,觉得还是不要把技术文章和生活文章混在一起,也觉得自己的技术水平仍然浅薄,写技术文章也有误导旁人之嫌,所以以后这里可能发布更多技术上的感受,体会,成长,总结,而不敢轻易地写什么教程。我自己的代码实践,不会再随随便便地往博客上面放了,前些天新建了两个Github 仓库,大概以后前端相关的练习代码都会放到这里面,当然也很简单,自然更没有推广的必要了。

我仍然认为除工作之外的生活也很重要,虽然我自己一直标榜自己是 tech nerd ,但是我并不想将自己限制住。正如我这么些年来所崇拜的都是诸如梁宏达、高晓松等知识面广泛、博学多才的胖子, 我也希望自己能够在专业技能之外,有更多的涉猎。而经过这些年的写作,我也确实体会到写作的确是能够反过来促进阅读和自我提升的。另外,我们真实的生活,总是免不了茶米油盐,总是免不了跟各种人打交道,因而,我也希望继续我之前的写作风格,继续写下一些身边的人,身边的事,他们并不一定传奇,甚至在外人看来足够普通,但我还是想如实的记述,就像我这普通的生活。

因而,从今年三月份开始,我在简书 和公众号 ( hktkdycom )上每周五发布一篇小文,这些文章都与技术没有关系,可能是读书笔记,可能是旅行游记,可能是凡人小记,可能是电影评论,可能是时事热点评论,总而言之,与所谓技术没有一毛钱关系。每周一篇,每周花两个小时左右的时间,做这样一件事情,对我而言还是很有价值的,虽然并没有多少阅读量,也希望我能够坚持下去。

写在最后

三年的时间说长不长,说短不短,对我而言,经历了从一个所谓大学生到一个职场人的转变。前不久这个域名到期的时候,我没有像过去两年那样,一年一年地续费域名,而是直接续费了两年,因此,为了不让这个域名浪费,我也会继续在这个Blog 上面写作。我大概不会每年在这个时间写一篇《纪念博客n周年》的文,不过,希望能够在这个博客五周年之时,再次对这个博客进行回顾,那个时候,我也已经毕业三年,应该会有更多的与之前并不相同的感受。

我总是觉得,正是未来规划好的一些事情,亦即所谓的希望,让我们的生活更加有趣。因而,我也期盼和你,在这里,一直等到这个博客的五周年,见证它,也见证我,个人的成长。


my work,study and life.

项目主页:https://github.com/zhangolve/liqi

欢迎star

硬件篇

  • MOTO G 2014,2015年8月份买的,没有想到这么耐用(cao),各种刷机折腾都没有问题,现在仍然是主力机。
  • Lenove E431(已停产),其实我也想用Mac,然而这是公司配的。
  • Apple iPad mini 4,本来买回来是给老妈看视频聊微信用的,结果现在我每次回家总是占着它。
  • 吉列手动剃须刀锋速3,配合剃须膏,手动刮胡子也是一种享受了。
  • 罗马仕 充电宝,10000 豪安刚刚好,用了也两年多了吧。
  • New Balance 574 ,从14年开始穿,作为基本款还是很不错的。

软件篇

通用

  • 翻墙服务 自购ss账户,搬瓦工vps备胎,改善浏览网络体验

windows 7

编辑器和 Terminal

其他相关插件:

系统相关

  • ccleaner ,一路相伴,哈哈哈。
  • everything,据说是很多人至今不愿意放弃windows的一个理由
  • 迅雷极速版,偶尔下个片也不错
  • IDM,接管浏览器下载功能

效率

  • Xshell ,服务器命令行工具
  • WinSCP ,服务器文件可视化操作工具
  • Axure RP Pro 7.0,原型图绘制工具
  • 福昕阅读器,阅读PDF足矣
  • Dropbox,文件同步工具,已经很久不用国内云服务做文件同步了,信不过。

音视频

浏览器

  • Firefox,曾经多年的主力浏览器,现在很少用了。
  • Chrome,现在的主力浏览器,日常工作都很给力。

Chrome 插件

油猴脚本

© 2017 Zhangolve


nocopyright

我一个自动化本科生怎么就做了前端呢?(1)关上一扇扇门
我一个自动化本科生怎么就做了前端呢?(2)把通向自动化的路的门关掉
我一个自动化本科生怎么就做了前端呢?(3)有时候觉得像插队
我一个自动化本科生怎么就做了前端呢?(4)入门和试错
我一个自动化本科生怎么就做了前端呢?(5)独闯北京
我一个自动化本科生怎么就做了前端呢?(6)从毕业到失业到找到工作