Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

全局progress进度条监听静态资源加载问题分析与解决 #8

Open
791045873 opened this issue Dec 24, 2018 · 1 comment
Open

Comments

@791045873
Copy link
Owner

No description provided.

@791045873 791045873 changed the title react 全局progress进度条监听静态资源加载问题分析与解决 Dec 24, 2018
@791045873
Copy link
Owner Author

问题

使用dva开发单页面应用时,使用了插件dva-progress来做全局的进度条,以便提醒用户当前操作状态。
dva-progress依赖于开源库Nprogress。dva把它插件化之后,会在每一次effects、action的触发前后显示与关闭进度条。提醒用户,自身操作已经触发。
但是,希望该组件可以监听到路由切换导致的js文件加载,以便提示用户,资源加载已经开始,缓解用户焦虑。
为此,我们在全局监听路由改变,当路由变化时,开启进度条。
问题在于,我们无法优雅的关闭进度条,当页面加载完成时。

思路

  1. 在每一个路由对应的page组件内调用进度条关闭方法。(或者使用高阶组件包裹page组件)
  2. 在路由文件中配置异步加载js成功后的回调
  3. 介入webpack打包,当一个script标签被插入dom节点时,为script标签添加load方法,在方法内调用进度条的关闭方法

可行性分析

思路一:
该思路完全可以满足我们的业务需求,但是扩展性极低,重复代码太多。不应把全局的事情放到业务层去处理。
而且维护起来很不方便,其他同学接手时也需要保证知晓这一点,才不会出问题。所以不考虑该方案。

思路二:
由于我们的dva是早期版本,使用的react15与react-router2.x版本,其中的异步加载我们是使用的react-router2.x的getComponent接口。在getComponent接口内部使用webpack的require.ensure异步加载js文件。具体代码如下:

{
      path: '/',
      name: 'name',
      getComponent(nextState, cb) {
        require.ensure([], (require) => {
          registerModel(app, require('./model.js').default)
          cb(null, require('./index.js').default)
        })
      },
}

由以上代码可以看出,我们的业务逻辑在require.ensure的回调函数中定义的。要想统一处理业务问题,可以提供一个高阶函数处理该回调,通过为函数传递参数,决定异步加载哪一个文件。
代码大体如下:

// 高阶函数,用于处理require.ensure的回调
const highRequire = (jsPath, modelPath) => {
  // 打开进度条
  progress.start()
  require.ensure([], (require) => {
          registerModel(app, require(modelPath).default)
          cb(null, require(jsPath).default)
          //关闭进度条
          progress.done()
  })
}

思路三:
可行,但是难度较大,需要编写webpack插件。(而且我不清楚webpack插件是否可以介入到webpack自身对于异步加载js文件的打包过程中)

综上所述,我们采取思路二。

实施遇到的问题

遇到的问题是,打包后的代码去异步加载js文件时,使用的js文件标识字符串是我们项目中的js文件的相对路径。该路径并未经过webpack的处理,故而无法加载本想加载的js文件。

问题分析

以require.ensure举例,webpack在对代码进行编译打包的时候,每当遇到require.ensure内的require(‘./path/index.js’)时,会根据这个‘./path/index.js’字符串,在目录下寻找对应的js文件,然后编译它。
而在我们的思路二中,我们调用require时传入的参数是一个变量,而非一个显式的字符串。那么,webpack在编译时(此时js并未运行)并不知道改变量对应的是哪一个js文件。
所以在之后的加载中便出错了。

问题解决

要处理以上问题我们需要显式地传递字符串去异步加载js文件。然后在加载后的回调中做一些需要的额外业务操作,例如关掉进度条。同时,我们需要在异步加载开始之前打开进度条。
发现,其中的重点在于,异步函数的回调方法不显式的写在getComponent中。因为,我们本身要做的事情,就是封装他,让该回调统一处理业务逻辑。如果显式的写,与没封装也还有什么区别呢?
要满足以上情况,我们可以使用webpack提供的import()方法。import()在webpack中与require.ensure并没有原理上的不同,只是import会返回一个promise。这样我们就可以不在getComponent内部写出回调函数了,从而可以封装业务代码。
在这里,我借鉴的高版本dva的dynamic方法。源代码如下:

const cached = {};
function registerModel(app, model) {
  model = model.default || model;
  if (!cached[model.namespace]) {
    app.model(model);
    cached[model.namespace] = 1;
  }
}

let defaultLoadingComponent = () => null;

function asyncComponent(config) {
  const { resolve } = config;

  return class DynamicComponent extends Component {
    constructor(...args) {
      super(...args);
      this.LoadingComponent =
        config.LoadingComponent || defaultLoadingComponent;
      this.state = {
        AsyncComponent: null,
      };
      this.load();
    }

    componentDidMount() {
      this.mounted = true;
    }

    componentWillUnmount() {
      this.mounted = false;
    }

    load() {
      resolve().then((m) => {
        const AsyncComponent = m.default || m;
        if (this.mounted) {
          this.setState({ AsyncComponent });
        } else {
          this.state.AsyncComponent = AsyncComponent; // eslint-disable-line
        }
      });
    }

    render() {
      const { AsyncComponent } = this.state;
      const { LoadingComponent } = this;
      if (AsyncComponent) return <AsyncComponent {...this.props} />;

      return <LoadingComponent {...this.props} />;
    }
  };
}

// 这是重点函数
export default function dynamic(config) {
  const { app, models: resolveModels, component: resolveComponent } = config;
  return asyncComponent({
    resolve: config.resolve || function () {
      const models = typeof resolveModels === 'function' ? resolveModels() : [];
      const component = resolveComponent();
      return new Promise((resolve) => {
        Promise.all([...models, component]).then((ret) => {
          if (!models || !models.length) {
            return resolve(ret[0]);
          } else {
            const len = models.length;
            ret.slice(0, len).forEach((m) => {
              m = m.default || m;
              if (!Array.isArray(m)) {
                m = [m];
              }
              m.map(_ => registerModel(app, _));
            });
            resolve(ret[len]);
          }
        });
      });
    },
    ...config,
  });
}

dynamic.setDefaultLoadingComponent = (LoadingComponent) => {
  defaultLoadingComponent = LoadingComponent;
};

总结一下该方法,就是:
同步加载一个高阶组件,通过该组件的生命周期,异步加载我们本需要的js文件。因为有了生命周期,所以可以在生命周期上做更多的事情。

ps:匹配路由与异步加载js文件与渲染组件是三件事情,在处理该问题时,不能把这三件事混为一谈

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant