Giter Site home page Giter Site logo

blog's People

Contributors

richardchen85 avatar

Stargazers

 avatar

Watchers

 avatar  avatar

blog's Issues

搭建一套好用的前端构建工具的方法

最近一直在折腾构建工具,主要针对以前的工程进行改造,尝试着能搭建一套比较好用的构建工具,在这个过程中也做了一些新的实践。下面我就来谈谈在这次改造中的心得体会,内容不局限于某一款构建工具,不管用 webpackJDFgulpgrunt 还是我比较喜欢的 fis,都可以很容易地实现。

容易上手

好用的第一步就是容易上手。不管是新手还是老鸟,使用工具的目的都是为了解放双手,提高效率,所以一款好用的构建工具在使用上一定要简单,特别在多人协作的时候,其他开发人员可以快速上手,减少沟通成本。

我认为容易上手可以体现在几个方面:

1. 方便安装

npm install or setup.exe

最好运行一条命令或者一个安装包就可以装好整个工具

2. 配置简单

提倡约定大于配置的原则,用很少的配置或者不需要配置就可以先 run 起来,如果需要修改其中某些项,可以创建一个配置文件覆盖默认配置项。

3. 依赖模块

我个人比较喜欢固定依赖模块的版本号,保证不同的人,或者同一个人在不同时间安装的模块版本是一样的,这点主要基于以下考虑:

  • 兼容问题:不同版本的模块可能会有不兼容的情况。
  • 模块之间的依赖:模块和模块之间也有版本强依赖,比如 vuevue-template-compiler,它们的版本号必须要一致,否则编译就会报错。
  • 生产环境文件比对:同一套代码不同的人、不同的时间打包出来的文件始终不变(曾经遇到过这样的尬尴,只改了一行代码,结果打包后有一堆文件不一样)。

npm 来说,安装模块后,检查一下 package.json 文件中的版本号确保是你想要的。

开发体验好

第二步是要有好的开发体验,好的工具不仅上手要容易,还要持续用着爽。比如合理的目录结构及编码规范,集成了常用的功能插件,方便调试及前后端协同工作等。

1. 目录结构、编码规范

合理的规范应该是以提升开发效率为目的,而不是约束开发人员。比如使用 ESLint 来排除一些低级的错误和风格统一,在 git hook 中集成 ESLint ,每次提交代码时检查一遍。

关于 git hook 推荐一个模块 husky,安装过后会自动创建 hook 文件到 .git 目录,你只需要在 package.jsonscript 中添加 precommitprepush 就可以了。

2. 常用的代码编译插件

工程中也许会用到 SassLesspostcssbabel 等编译插件,根据项目的需要集成进来。

3. 模块化

模块化解决了 js 的独立作用域及依赖管理问题,让代码组织更简单,已经成为了前端在开发的标配。

4. 组件化

widget
 └ header
   ├ header.vm
   ├ header.js
   ├ header.png
   └ header.css

组件化就是将页面的各个部分拆分成小部件,按文件夹组织代码和资源,代码相对独立,方便多人开发、维护和复用。

5. 数据模拟

在后端接口还没准备好的时候,前端往往需要自己模拟数据来开发页面和交互,所以能够方便地模拟数据是构建工具的必备功能之一。我通常的做法是使用 express 启一个简单的 web server来实现。

数据模拟分为 异步接口数据模拟页面渲染数据模拟

5.1 异步接口数据模拟

异步接口的数据模拟需要考虑两种场景:一是本地开发,二是与后端联调。

假设有如下一个 URL 映射表,key 表示前端请求的地址匹配,local 表示本地数据模拟文件,可以是一个简单的 json 文件,也可以是 express 中间件的 js 文件,server 表示远端服务器的接口地址。

const mockConfig = {
    '^/api/index': {
        local: '/mock/index.json',
        server: '/api/index.html'
    },
    '^/api/list': {
        local: '/mock/list.js',
        server: '/api/list.html'
    }
}
  • 本地开发时:编写 express 中间件读取表中匹配的 local 值,如果是 json 就读取文件内容,如果是 js 文件,就当作 express 的中间件使用。
  • 与后端联调时:读取表中的 server 值,使用 http-proxy-middleware 将异步请求转发到联调服务器,并将联调服务器的响应数据返回给浏览器

注意:nodejsrequire 一个模块后会缓存起来,除非重启 express,想要数据文件不被缓存,先 delete require.cache[xxx]require 进来

5.2 页面渲染数据模拟

模拟页面渲染时的数据需要用到模板引擎,express 支持很多模板引擎,可以充分模拟服务端渲染页面时的数据模拟。数据来源可以是和模拟文件同名的 .mock 文件,比如如下一个文件结构:

project
 ├ common.mock // 全局模拟数据
 └ page
   ├ index.vm
   └ index.mock  // index.vm 的模拟数据

当渲染 index.vm 的时候,首先读取全局模拟数据文件 common.mock,再读取模板对应的模拟数据文件 index.mock,合并起来作为模板引擎的数据来源。

注:目前大多数的后端模板引擎都有 nodejs 版本的实现,比如 JAVA 中常用的 velocity,可以将其集成到 express 中模拟 JAVA 的模板渲染,由前端接管模板的编写,做到前后端的模板共用。

部署体验

部署是最后一步,也是比较麻烦的一步。构建工具需要考虑性能优化、与IDE集成、与开发流程对接等。

1. 性能优化

  • 静态资源的压缩、合并、内嵌
  • 替换被引用资源的生产路径、添加CDN域名
  • 非覆盖式的版本发布(推荐文件名加md5后缀)

2. 与IDE集成

大多数IDE都支持主流的构建工具,即使有不支持的,也可以使用 npm script 来执行任务。

3. 与开发流程对接

各团队可能有自己的一套开发流程,比如我们这边是:本地开发联调、视觉走查、前后端联调、测试环境测试、预发布环境测试,上线。

  • 本地开发时要同时支持本地和本地,本地和远端的联调
  • 将静态代码部署到内网 server 方便设计师视觉走查
  • 测试环境的代码不需要压缩合并,方便定位 bug
  • 预发及上线时需要给资源引用代码添加 CDN 域名

以上就是我在这次构建工具改造中所做的收获,当然这些还远远不够,最终的目标还是要迈向前端工程化体系,而构建工具只是工程化的第一步而已。所以欢迎小伙伴们来一起交流和学习,共同进步。

如何打造一款简单易用的 React 状态管理工具

React 的状态管理已经是一个老生常谈的问题了。从 React 内置的 Context APIHooks API,到第三方库如 ReduxMobxRecoil,再到二次封装的库如 Rematch 和国内用户熟知的 dvajs 等等。可见社区对状态管理是如此的纠结,如果你也面对同样的纠结而无从下手,下面我将为你介绍如何基于 Redux 二次封装一个轻量级但是简单易用的状态管理工具。

为什么不用 Rematchdvajs ?

这两个工具都是基于 Redux 进行了二次封装。它们有一个共同点,就是将状态管理逻辑以声明式的方式全部抽象到一个 model 文件里,减少了很多 Redux 的各种范式代码(其实 Redux 也意识到了这一点,所以现在官方极力推荐他们的 redux-toolkit 方案),后面会详细描述 model 是个什么玩意儿。

dvajs 是一个很优秀的解决方案,功能齐全,还支持热更新。本人在早期使用 umijs 脚手架的时候是使用了 dva 插件来做为我的状态管理工具。它引入了 redux-saga 来管理副作用的部分,总的来说很好用,只是需要额外了解一下 redux-saga

Rematch 考虑得很周到,它将 reducers 的调用直接注入到 dispatch 的属性里,省去了不少 dispatch 的范式代码,这一点是我非常喜欢的,不过它做得不彻底,应该连 effects 也一起做到就比较好了。另外一点是,它的副作用部分是基于 Redux 的中间件来实现的,所以不必依赖 redus-saga 或者 redux-thunk 等第三方库,上手成本要稍低一点。

那为什么还要自己去实现一个类似的工具呢?目的只有一个,就是要让状态管理更简单些:

  • reducerseffects 的调用函数自动注入到 dispatchers
  • 使全局状态逻辑使用更容易,同时也可以接管组件的内部状态管理逻辑

接下来将详细介绍如何一步步实现我所需要的功能。

本文所介绍的实现方案在 github 上有源码(@olajs/modx),本文的示例代码也基于这个工具。

声明式状态管理文件 model.ts

dvajsRematch 都采用了声明式的 model 文件来集中管理状态的流转逻辑,再通过代码将文件内容拆分成 Redux 需要的各个部分。下面是一个简单的 model 文件示例:

// modelA.ts
import { ModelConfig, EffectArgs } from "@olajs/modx";
type StateType = { counter: number };
type Dispatchers = {
  plus();
  minus();
  lazyPlus(args: { timeout: number });
  lazyMinus(args: { timeout: number });
};
type EffectsArg = EffectArgs<{ timeout: number }, StateType>;
const namespace = "modelA";
export { namespace, StateType };
export default {
  // 为不同的 model 在 state 中分配不同的命名空间
  namespace,
  // 将会合并入 store 的 initialState
  state: {
    counter: 0,
  } as StateType,
  // 必要的 reducer
  reducers: {
    plus: (state: StateType) => ({ counter: state.counter + 1 }),
    minus: (state: StateType) => ({ counter: state.counter - 1 }),
  },
  // 有副作用的逻辑,将自动转换为 redux 的 middleware
  effects: {
    lazyPlus({ namespace, dispatcher, action, prevState }: EffectsArg) {
      console.log(prevState); // { counter: xxx }
      setTimeout(() => {
        dispatcher(`${namespace}/plus`);
      }, action.payload.timeout);
    },
    lazyMinus({ namespace, dispatcher, action, prevState }: EffectsArg) {
      console.log(prevState); // { counter: xxx }
      setTimeout(() => {
        dispatcher(`${namespace}/minus`);
      }, action.payload.timeout);
    },
  },
} as ModelConfig;

解析 model 文件并创建 Redux Store 实例

接下来是要将 model 文件的内容转换成创建 Redux Store 需要的代码:

  • namespace:为不同的 modelstate 中分配不同的命名空间
  • state:作为当前 model 的状态描述,初始值会被并入 StoreinitialState
  • reducers: 没有副作用的 reducer 函数
  • effects:有副作用的函数,自动转换成 Reduxmiddleware

@olajs/modx/index.ts

import React, { useState, useEffect } from "react";
import { useStore } from "react-redux";
import parseModel from "./parseModel";
import configureStore from "./configureStore";
import {
  Store,
  ModelConfig,
  ModelAction,
  EffectArgs,
  EffectFunction,
} from "./types";

/**
 * 创建一个 redux store 实例
 * 注意这里 modelConfigs 参数是一个数组,一个应用可能有多个 model
 */
function createStore(initialState: any, modelConfigs: ModelConfig[]): Store {
  const reducers = {};
  const middlewares = [];
  const dispatcherKeys = {};

  modelConfigs.forEach((modelConfig) => {
    const { namespace } = modelConfig;
    const model = parseModel(modelConfig);
    if (reducers[namespace]) {
      throw new Error("Duplicated namespace: " + namespace);
    }
    if (model.reducer) {
      reducers[namespace] = model.reducer;
    }
    if (model.middleware) {
      middlewares.push(model.middleware);
    }
    // 保存每个 model 的 reducers 和 effects 的 key
    // 方便 getDispatchers 来获取
    dispatcherKeys[modelConfig.namespace] = Object.keys(
      modelConfig.reducers || {}
    ).concat(Object.keys(modelConfig.effects || {}));
  });

  const store = configureStore({ initialState, reducers, middlewares });
  store.dispatcherKeys = dispatcherKeys;
  return store;
}

export { ModelConfig, ModelAction, EffectArgs, EffectFunction, createStore };

@olajs/modx/parseModel.ts

import { ModelConfig, EffectFunction } from "./types";
import { Middleware, Reducer } from "redux";

/**
 * 解析 model 数据
 */
export default function parseModel(modelConfig: ModelConfig): {
  reducer: Reducer;
  middleware: Middleware;
} {
  const { namespace, state, reducers, effects } = modelConfig;
  let parsedReducer;
  if (reducers) {
    parsedReducer = createReducer(namespace, reducers, state);
  }
  let middleware;
  if (effects) {
    middleware = createMiddleware(effects, { namespace });
  }
  return { reducer: parsedReducer, middleware };
}

/**
 * 创建一个 reducer,将其 action 与 subject 相关联
 */
function createReducer(
  namespace: string,
  reducers: { [reducerName: string]: Reducer },
  initialState: any
): Reducer {
  const converted = {};
  Object.keys(reducers).forEach((actionType: string) => {
    converted[`${namespace}/${actionType}`] = reducers[actionType];
  });
  return function (state = initialState, action) {
    if (converted.hasOwnProperty(action.type)) {
      return converted[action.type](state, action);
    }
    return state;
  };
}

/**
 * 将 effects 解析成 redux middleware
 */
function createMiddleware(
  effects: { [key: string]: EffectFunction },
  { namespace }: { namespace: string }
): Middleware {
  const converted = {};
  Object.keys(effects).forEach((actionType) => {
    converted[`${namespace}/${actionType}`] = effects[actionType];
  });
  return (store) => (next) => (action) => {
    next(action);
    if (converted.hasOwnProperty(action.type)) {
      converted[action.type]({
        namespace,
        store,
        next,
        action,
        // 将当前 model 的 state 直接获取了传参,方便开发人员获取
        prevState: store.getState()[namespace],
        // 简化 store.dispatch() 方法的调用
        dispatcher(actionType: string, payload?: any) {
          store.dispatch({ type: actionType, payload });
        },
      });
    }
  };
}

@olajs/modx/configureStore.ts

import {
  createStore,
  compose,
  applyMiddleware,
  combineReducers,
  Middleware,
  Reducer,
} from "redux";
import { Store } from "./types";

/**
 * 创建一个 Redux Store 实例
 */
export default function configureStore({
  initialState,
  reducers,
  middlewares = [],
}: {
  middlewares: Middleware[];
  reducers: { [key: string]: Reducer };
  initialState: any;
}): Store {
  return createStore(
    combineReducers(reducers),
    initialState,
    compose(applyMiddleware(...middlewares))
  );
}

如何使用?

下面我们来尝试着使用我们开发的这个工具

最简单的用法

我们先从最简单的直接操作 store 的方法来验证我们编写的代码是否可用:

import { createStore } from "@olajs/modx";
import modelA, { namespace } from "./modelA";

const store = createStore({}, [modelA]);
console.log(store.getState()[namespace]);
// { counter: 0 }
store.dispatch({ type: `${namespace}/plus` });
console.log(store.getState()[namespace]);
// { counter: 1 }
store.dispatch({ type: `${namespace}/plus` });
console.log(store.getState()[namespace]);
// { counter: 2 }
store.dispatch({ type: `${namespace}/minus` });
console.log(store.getState()[namespace]);
// { counter: 1 }

全局状态的使用方法

为了能在组件中更简单的使用全局 State 及其流转逻辑,我们要在 @olajs/modx/index.ts 中增加两个辅助的方法:

  • useGlobalModel:获取指定 namespace 的全局状态的 React Hooks,主要用在函数组件中
  • withGlobalModel:包装一个拥有指定 namespace 的全局状态的组件,主要用在类组件中
// @olajs/modx/index.ts
type UseGlobalModelResult<StateType, Dispatchers> = {
  store: Store;
  state: StateType;
  dispatchers: Dispatchers;
};

function useGlobalModel<StateType, Dispatchers>(
  namespace: string
): UseGlobalModelResult<StateType, Dispatchers> {
  const store = useStore();
  const [state, setState] = useState(store.getState()[namespace]);
  const [dispatchers] = useState(() => getDispatchers(store, namespace));
  useEffect(() => {
    return store.subscribe(() => setState(store.getState()[namespace]));
  }, []);
  return { store, state, dispatchers };
}

function withGlobalModel<StateType, Dispatchers>(namespace: string) {
  return (
    SubComponent: React.ComponentType<{
      globalModel: UseGlobalModelResult<StateType, Dispatchers>;
      [key: string]: any;
    }>
  ) =>
    React.memo(function withGlobalModelContainer(props: unknown) {
      const globalModel = useGlobalModel<StateType, Dispatchers>(namespace);
      return <SubComponent {...props} globalModel={globalModel} />;
    });
}

/**
 * 获取指定 namespace 的 model 的 dispatchers 方法
 * @param store
 * @param namespace
 * @returns {{}}
 */
function getDispatchers<Dispatchers = any>(
  store: Store,
  namespace: string
): Dispatchers {
  const dispatcherKeys = store.dispatcherKeys[namespace] || [];
  const result = {} as Dispatchers;
  dispatcherKeys.forEach((key) => {
    result[key] = function (payload) {
      store.dispatch({ type: `${namespace}/${key}`, payload });
    };
  });
  return result;
}

现在我们利用这两个方法来操作全局状态:

App.tsx

import React from "react";
import { useGlobalModel } from "@olajs/modx";
import { namespace, StateType, Dispatchers } from "./modelA";

function App() {
  const { state, dispatchers } = useGlobalModel<StateType, Dispatchers>(
    namespace
  );
  return (
    <div>
      {state.counter}
      <br />
      <button onClick={dispatchers.plus}>plus</button>
      <br />
      <button onClick={dispatchers.minus}>minus</button>
    </div>
  );
}
export default App;

main.tsx

import React from "react";
import ReactDom from "react-dom";
import { Provider } from "react-redux";
import { createStore } from "@olajs/modx";
import modelA from "./modelA";
import App from "./App";

const store = createStore({}, [modelA], { devTools: true });
ReactDom.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementByid("app")
);

组件内部状态的使用方法

为了能在组件的内部状态管理中使用本工具,同样需要在 @olajs/modx/index.ts 中增加两个辅助方法:

  • useSinglelModel:包装一个指定的 model 并集成对应的操作函数的 React hooks,主要用在函数组件中
  • withSinglelModel:包装一个指定的 model 并集成对应的操作函数的高阶组件,主要用在类组件中
type UseSingleModelResult<StateType, Dispatchers> = {
  store: Store;
  state: StateType;
  dispatchers: Dispatchers;
};

function useSingleModel<StateType, Dispatchers>(
  modelConfig: ModelConfig
): UseSingleModelResult<StateType, Dispatchers> {
  const [store] = useState(createSingleStore(modelConfig));
  const [state, setState] = useState(store.getState()[modelConfig.namespace]);
  const [dispatchers] = useState(() => {
    return getDispatchers(store, modelConfig.namespace);
  });
  useEffect(() => {
    return store.subscribe(() =>
      setState(store.getState()[modelConfig.namespace])
    );
  }, []);
  return { store, state, dispatchers };
}

function withSingleModel<StateType, Dispatchers>(
  modelConfig: ModelConfig
): (
  SubComponent: React.ComponentType<{
    singleModel: UseSingleModelResult<StateType, Dispatchers>;
    [key: string]: any;
  }>
) => React.FC<any> {
  return (SubComponent) => {
    return React.memo(function WithSingleModelContainer(props: unknown) {
      const singleModel = useSingleModel<StateType, Dispatchers>(modelConfig);
      return <SubComponent {...props} singleModel={singleModel} />;
    });
  };
}

现在我们利用这两个方法来操作组件的内部状态:

类组件的使用方法:withSingleModel.tsx

import React from 'react';
import { withSingleModel } from '@olajs/modx';
import modelA, { StateType, Dispatchers } from './modelA';

type Props = {
  state: StateType;
  dispatchers: Dispatchers;
};

@withSingleModel<StateType, Dispatchers>(modelA)
class WithSingleModel extends React.PureComponent<Props, any> {
  render() {
    const { state, dispatchers } = this.props.singleModel;
    return (
      <div>
        {state.counter}
        <br />
        <button onClick={dispatchers.plus}>plus</button>
        <br />
        <button onClick={dispatchers.minus}>minus</button>
      </div>
    );
  }
}
export default WithSingleModel;

函数组件的使用方法:useSingleModel.tsx

import React from 'react';
import { useSingleModel } from '@olajs/modx';
import modelA, { StateType, Dispatchers } from './modelA';

function UseSingleModel() {
  const { state, dispatchers } = useSingleModel<StateType, Dispatchers>(modelA);
  return (
    <div>
      {state.counter}
      <br />
      <button onClick={dispatchers.plus}>plus</button>
      <br />
      <button onClick={dispatchers.minus}>minus</button>
    </div>
  );
}
export default UseSingleModel;

有副作用的异步逻辑的用法

import React from 'react';
import { useSingleModel } from '@olajs/modx';
import modelA, { StateType, Dispatchers } from './modelA';

function useSingleModelLazy() {
  const { state, dispatchers } = useSingleModel<StateType, Dispatchers>(modelB);
  return (
    <div>
      {state.counter}
      <br />
      <button onClick={() => dispatchers.lazyPlus({ timeout: 3000 })}>plus</button>
      <br />
      <button onClick={() => dispatchers.lazyMinus({ timeout: 3000 })}>minus</button>
    </div>
  );
}
export default useSingleModelLazy;

有哪些优势?

  • 使用更简单:除了省去了 Redux 烦琐的范式代码以外,通过高阶组件和自定义 hooks 进一步简化了代码的编写
  • 统一了全局和组件内部的状态管理:这一点是我比较喜欢的,即使不想把组件的状态放到全局,也可以享受到工具带来的便捷;并且你可以随时把你的状态管理迁到全局或者从全局状态撤回内部,不需要做太多的改动
  • 使组件的单测更容易:众所周知,对 UI 组件编写单测(特别是交互比较复杂的组件)是比较麻烦的事情,如果使用本工具,就可以将对 UI 组件的单测转到对组件状态流转逻辑的单测编写,只要状态流转逻辑的正确性得到保证,整个 UI 组件的质量也就有了保证

参考资料

用FIS3实现组件化及前后端共用模板的尝试

接触FIS也有段时间了,用得越久越能体会到它的强大。刚开始只是用FIS2+swig解决了作为一个切图仔长久以来模板复用的问题,后来涉及到了一些JS开发,于是用到了我厂的JDF,基本上实现了组件化开发和前后端模板共用,前端模拟velocity语法,因为我厂后台统一使用velocity模板引擎。于是想到在FIS上试试,下面说说详细的尝试过程。

后端做资源加载的请忽略,作为小前端,没办法推动后端,本文只讨论纯前端资源加载,另外我尝试的方案也许只适合于和我们有相似架构的项目,因为本人对后端了解不深,如果有没想到的或者更好的方案,请告知我一下,非常感谢。

先说说模板共用的事儿

模板共用最理想的状态,我想应该是前端产出模板后,后端不需要修改,可以直接copy进项目里,这样可以减少重复劳动力,我来试试看能不能做到。

因为后端是velocity模板引擎,幸好以前用gulp时发现了fool2fish的velocity模块,可以满足velocity语法解析,还支持类似于velocity tools的用法。于是想到写一个fis-parser-velocity插件,将velocity模板文件直接生成html。

我喜欢FIS的原因之一就是,像我这样的小白可以很容易写一个parser插件,FIS的插件开发方法请参考FIS3插件开发

说回正题,能够解析velocity语法只能满足在开发的时候预览前端效果,要直接把模板拿到后端用还是不行的,请看下面这段前端代码:

<div class="floor">
  <div class="floor-title">$floorName</div>
  <ul class="floor-item-list">
  #foreach($item in $list)
    <li>名称:$item.name  价格: $item.price</li>
  #end
  </ul>
</div>

如果前后端不事先约定好,就可能出现前端变量名叫$floorName和$list,后端却是$loucenName和$dataList,这都还是小事,再看下面前端代码:

<img src="/path/to/a/picture.jpg">

可能图片会存在CDN服务器,所以在生产环境中,可能会是

<img src="//cdn.xxx.com/0.0.1/path/to/a/picture.jpg">

后端拿到我们的模板,要把所有静态资源引用统统加上//cdn.xxx.com/0.0.1,后台可以注入一个变量$cdnPath统一在后台修改,但没能达到我想要的效果。

还有一种情况就是资源的合并和指纹识别,比如:

<link rel="stylesheet" href="/path/to/base.css">
<script src="/path/to/lib/a.js"></script>
<script src="/path/to/lib/b.js"></script>

发布后可能会变成

<link rel="stylesheet" href="/path/to/base_a3c432.css">
<script src="/path/to/lib/pkg_a_b_43xd3f.js"></script>

当前端资源文件有修改的时候,你叫后端挨个儿文件改模板,或者你自己去改,可能是会疯掉的。

所以我在fis-parser-velocity插件中加了一个parse选项,如果为true会将vm模板生成html,用于开发时浏览效果,如果为false,则只会加载静态资源交由fis3-postpackager-loader处理,vm语法原样输出,用于提供给后端。

于是得到以下效果:

前端模板文件:index.vm

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Examples</title>
</head>
<body>
    #parse('widget/header/header.vm')
    <section>
        <h2>this is body</h2>
    </section>
    #parse('widget/footer/footer.vm')
</body>
</html>

输出结果文件名依然是:index.vm

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Examples</title>
    <link rel="stylesheet" href="/widget/header/header_0476b28.css">
    <link rel="stylesheet" href="/widget/footer/footer_20247c7.css">
</head>
<body>
    #parse('widget/header/header.vm')
    <section>
        <h2>this is body</h2>
    </section>
    #parse('widget/footer/footer.vm')
    <script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
    <script src="/widget/header/header_f450bec.js"></script>
    <script src="/widget/footer/footer_a92d4a6.js"></script>
</body>
</html>

可以看到,VM语法没做解析,但是被#parse文件的静态资源已经引入进来,也做了相应的处理。这就是我简单的组件化的处理方案。

再来说说组件化

想起@民工精髓大大的一句话:

组件化的本质目的并不一定是要为了可复用,而是提升可维护性

至少在我的工作中,能够跨项目复用的组件是很少的,组件化带给我最多的还是易于维护、适合多人协作开发。

看看以下目录结构:

widget 组件目录
  ├ header
  │  ├ header.vm 模板文件
  │  ├ header.js JS文件
  │  ├ header.scss  css样式
  │  ├ header.mock  模拟数据文件
  └ nav
     ├ nav.vm
     ├ nav.js
     ├ nav.scss
     └ nav.mock

在实际开发中,我希望的是在页面中#parse('widget/header/header.vm')过后,能够自动将widget/header/header.jswidget/header/header.css引入页面,
在解析velocity语法时,可以将widget/header/header.mock文件作为context数据源。

好在fool2fish的velocity模块中,可以直接获取到某个VM文件的AST树,可以轻松拿到所有通过#parse引入的模板文件,再分析该模板的同名js、css、mock文件,如果存在,则加入页面依赖缓存,在非模块化项目中,希望是这样:

<script src="/widget/a/a.js"></script>
<script src="/widget/b/b.js"></script>

在使用模块化框架,如require、seajs时,我希望是这样:

require(["/widget/a/a.js", "/widget/b/b.js"]);

或者

seajs.use(["/widget/a/a.js", "/widget/b/b.js"]);

剩下的交由fis3-postpackager-loader去处理。

来个总结

其实也没什么好总结的,本来是个内向的人,也很少写文字,所有内容都在fis-parser-velocity插件里面,另外还有个完整的案例在fiskit封装和fiskit-demo中可以参考。FIS是个很强大的东西,我也只是用到了其中很小的一点来解决我现有业务上的一些问题,经验尚浅,还有很多东西需要学习。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.