richardchen85 / blog Goto Github PK
View Code? Open in Web Editor NEWmy personal blog
Home Page: https://www.chenliqiang.cn
my personal blog
Home Page: https://www.chenliqiang.cn
最近一直在折腾构建工具,主要针对以前的工程进行改造,尝试着能搭建一套比较好用的构建工具,在这个过程中也做了一些新的实践。下面我就来谈谈在这次改造中的心得体会,内容不局限于某一款构建工具,不管用 webpack
、 JDF
、gulp
、grunt
还是我比较喜欢的 fis
,都可以很容易地实现。
好用的第一步就是容易上手。不管是新手还是老鸟,使用工具的目的都是为了解放双手,提高效率,所以一款好用的构建工具在使用上一定要简单,特别在多人协作的时候,其他开发人员可以快速上手,减少沟通成本。
我认为容易上手可以体现在几个方面:
npm install or setup.exe
最好运行一条命令或者一个安装包就可以装好整个工具
提倡约定大于配置
的原则,用很少的配置或者不需要配置就可以先 run 起来,如果需要修改其中某些项,可以创建一个配置文件覆盖默认配置项。
我个人比较喜欢固定依赖模块的版本号,保证不同的人,或者同一个人在不同时间安装的模块版本是一样的,这点主要基于以下考虑:
兼容问题
:不同版本的模块可能会有不兼容的情况。模块之间的依赖
:模块和模块之间也有版本强依赖,比如 vue
和 vue-template-compiler
,它们的版本号必须要一致,否则编译就会报错。生产环境文件比对
:同一套代码不同的人、不同的时间打包出来的文件始终不变(曾经遇到过这样的尬尴,只改了一行代码,结果打包后有一堆文件不一样)。拿 npm
来说,安装模块后,检查一下 package.json
文件中的版本号确保是你想要的。
第二步是要有好的开发体验,好的工具不仅上手要容易,还要持续用着爽。比如合理的目录结构及编码规范,集成了常用的功能插件,方便调试及前后端协同工作等。
合理的规范应该是以提升开发效率为目的,而不是约束开发人员。比如使用 ESLint
来排除一些低级的错误和风格统一,在 git hook
中集成 ESLint
,每次提交代码时检查一遍。
关于 git hook
推荐一个模块 husky
,安装过后会自动创建 hook
文件到 .git
目录,你只需要在 package.json
的 script
中添加 precommit
和 prepush
就可以了。
工程中也许会用到 Sass
、Less
、postcss
、babel
等编译插件,根据项目的需要集成进来。
模块化解决了 js
的独立作用域及依赖管理问题,让代码组织更简单,已经成为了前端在开发的标配。
widget
└ header
├ header.vm
├ header.js
├ header.png
└ header.css
组件化就是将页面的各个部分拆分成小部件,按文件夹组织代码和资源,代码相对独立,方便多人开发、维护和复用。
在后端接口还没准备好的时候,前端往往需要自己模拟数据来开发页面和交互,所以能够方便地模拟数据是构建工具的必备功能之一。我通常的做法是使用 express
启一个简单的 web server
来实现。
数据模拟分为 异步接口数据模拟
和 页面渲染数据模拟
。
异步接口的数据模拟需要考虑两种场景:一是本地开发,二是与后端联调。
假设有如下一个 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
将异步请求转发到联调服务器,并将联调服务器的响应数据返回给浏览器注意:
nodejs
在require
一个模块后会缓存起来,除非重启express
,想要数据文件不被缓存,先delete require.cache[xxx]
再require
进来
模拟页面渲染时的数据需要用到模板引擎,express
支持很多模板引擎,可以充分模拟服务端渲染页面时的数据模拟。数据来源可以是和模拟文件同名的 .mock
文件,比如如下一个文件结构:
project
├ common.mock // 全局模拟数据
└ page
├ index.vm
└ index.mock // index.vm 的模拟数据
当渲染 index.vm
的时候,首先读取全局模拟数据文件 common.mock
,再读取模板对应的模拟数据文件 index.mock
,合并起来作为模板引擎的数据来源。
注:目前大多数的后端模板引擎都有
nodejs
版本的实现,比如JAVA
中常用的velocity
,可以将其集成到express
中模拟JAVA
的模板渲染,由前端接管模板的编写,做到前后端的模板共用。
部署是最后一步,也是比较麻烦的一步。构建工具需要考虑性能优化、与IDE集成、与开发流程对接等。
大多数IDE都支持主流的构建工具,即使有不支持的,也可以使用 npm script
来执行任务。
各团队可能有自己的一套开发流程,比如我们这边是:本地开发联调、视觉走查、前后端联调、测试环境测试、预发布环境测试,上线。
server
方便设计师视觉走查bug
CDN
域名以上就是我在这次构建工具改造中所做的收获,当然这些还远远不够,最终的目标还是要迈向前端工程化体系,而构建工具只是工程化的第一步而已。所以欢迎小伙伴们来一起交流和学习,共同进步。
React
的状态管理已经是一个老生常谈的问题了。从 React
内置的 Context API
和 Hooks API
,到第三方库如 Redux
、Mobx
和 Recoil
,再到二次封装的库如 Rematch
和国内用户熟知的 dvajs
等等。可见社区对状态管理是如此的纠结,如果你也面对同样的纠结而无从下手,下面我将为你介绍如何基于 Redux
二次封装一个轻量级但是简单易用的状态管理工具。
Rematch
或 dvajs
?这两个工具都是基于 Redux
进行了二次封装。它们有一个共同点,就是将状态管理逻辑以声明式的方式全部抽象到一个 model
文件里,减少了很多 Redux
的各种范式代码(其实 Redux
也意识到了这一点,所以现在官方极力推荐他们的 redux-toolkit 方案),后面会详细描述 model
是个什么玩意儿。
dvajs
是一个很优秀的解决方案,功能齐全,还支持热更新。本人在早期使用 umijs
脚手架的时候是使用了 dva
插件来做为我的状态管理工具。它引入了 redux-saga
来管理副作用的部分,总的来说很好用,只是需要额外了解一下 redux-saga
。
Rematch
考虑得很周到,它将 reducers
的调用直接注入到 dispatch
的属性里,省去了不少 dispatch
的范式代码,这一点是我非常喜欢的,不过它做得不彻底,应该连 effects
也一起做到就比较好了。另外一点是,它的副作用部分是基于 Redux
的中间件来实现的,所以不必依赖 redus-saga
或者 redux-thunk
等第三方库,上手成本要稍低一点。
那为什么还要自己去实现一个类似的工具呢?目的只有一个,就是要让状态管理更简单些:
reducers
和 effects
的调用函数自动注入到 dispatchers
里接下来将详细介绍如何一步步实现我所需要的功能。
本文所介绍的实现方案在
github
上有源码(@olajs/modx),本文的示例代码也基于这个工具。
dvajs
和 Rematch
都采用了声明式的 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
需要的代码:
model
在 state
中分配不同的命名空间model
的状态描述,初始值会被并入 Store
的 initialState
reducer
函数Redux
的 middleware
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 };
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 });
},
});
}
};
}
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
中增加两个辅助的方法:
namespace
的全局状态的 React Hooks
,主要用在函数组件中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;
}
现在我们利用这两个方法来操作全局状态:
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;
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
中增加两个辅助方法:
model
并集成对应的操作函数的 React hooks
,主要用在函数组件中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} />;
});
};
}
现在我们利用这两个方法来操作组件的内部状态:
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;
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
组件的质量也就有了保证接触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.js
和widget/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是个很强大的东西,我也只是用到了其中很小的一点来解决我现有业务上的一些问题,经验尚浅,还有很多东西需要学习。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.