源码定位工具source-ref的使用

背景

source-ref 是一款通过网页点击快速定位到源码的工具,用于解决从视觉上快速定位到所在源码具体位置。与现有的devtools的源码定位互补

  • UI框架支持 React, Vue框架
  • 打包工具支持 webpack rollup vite
  • 跳转方式支持 vscode 打开 Github 打开

官方网站: https://sourceref.moonrailgun.com/

演示

定位到Github源码

github

使用vscode打开源码

vscode

快速接入

react + webpack 为例:

1
2
npm install source-ref-runtime
npm install -D source-ref-loader

webpack.config.json 中, 处理jsx文件的loader的最下面插入source-ref-loader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
test: /.tsx?$/,
exclude: /node_modules/,
use: [
{
loader: 'esbuild-loader',
options: {
loader: 'tsx',
target: 'es2015',
},
},
{
loader: 'source-ref-loader',
},
],
}

在入口文件处,插入:

1
import('source-ref-runtime').then(({ start }) => start())

打开项目,Alt(option in mac) + LMB(鼠标左键点击) 即可弹出选择框

更多示例见官网: https://sourceref.moonrailgun.com/

原理

原理

打包阶段:

  • 解析源码到AST, 找到组件节点的开头部分插入当前所在位置信息
  • 将处理好的AST转换回原来的代码形式

渲染阶段:

  • 优化提示路径, 减少长路径带来的视觉污染(在devtools)
  • 快捷键点击DOM元素,弹出选择框。
  • 点击选择节点,通过打开vscode注册的 URI Scheme 从网页打开一个文件并定位到具体行号和列号

Garfish 源码解析 —— 一个微应用是如何被挂载的

背景

Garfish 是字节跳动 web infra 团队推出的一款微前端框架

包含构建微前端系统时所需要的基本能力,任意前端框架均可使用。接入简单,可轻松将多个前端应用组合成内聚的单个产品

微前端基本架构

因为当前对 Garfish 的解读极少,而微前端又是现代前端领域相当重要的一环,因此写下本文,同时也是对学习源码的一个总结

本文基于 garfish#0d4cc0c82269bce8422b0e9105b7fe88c2efe42a 进行解读

学习源码

1
2
3
4
5
git clone https://github.com/modern-js-dev/garfish.git
cd garfish
pnpm install
pnpm build
pnpm dev

然后打开https://localhost:8090/ 即可看到演示项目

基本使用

主应用

1
2
3
4
5
6
7
export const GarfishInit = async () => {
try {
Garfish.run(Config);
} catch (error) {
console.log('garfish init error', error);
}
};

其中关键点是 Config 参数, 其所有参数都是可选的,一般比较重要的几个参数为:

  • basename 子应用的基础路径,默认值为 /,整个微前端应用的 basename。设置后该值为所有子应用的默认值,若子应用 AppInfo 中也提供了该值会替换全局的 basename 值
  • domGetter 子应用挂载点。如'#submodule'
  • apps 需要主要参数如 name, entry, activeWhen(路由地址)

此函数运行之后,Garfish会自动进行路由劫持功能。根据路由变化

子应用

以react17为例:

1
2
3
4
5
6
7
8
9
10
import { reactBridge, AppInfo } from '@garfish/bridge-react';

export const provider = reactBridge({
el: '#root', // 此处的root是子应用自己声明的root
// a promise that resolves with the react component. Wait for it to resolve before mounting
loadRootComponent: (appInfo: AppInfo) => {
return Promise.resolve(() => <RootComponent {...appInfo} />);
},
errorBoundary: (e: any) => <ErrorBoundary />,
});

其中:

  • RootComponent 是子应用的主要逻辑
  • reactBridge 是garfish导出的一个封装函数。大概的逻辑就是把react的一些特有写法映射到garfish的通用生命周期,包含renderdestroy

源码解读

那么简单了解了一些garfish的基本使用方案,我们就来看看garfish在此过程中到底做了什么。

Garfish.run开始:

garfish/packages/core/src/garfish.ts

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
run(options: interfaces.Options = {}) {
if (this.running) {
/**
* 重复运行检测
*/
if (__DEV__) {
warn('Garfish is already running now, Cannot run Garfish repeatedly.');
}
return this;
}

/**
* 全局化配置
*/
this.setOptions(options);

/**
* 载入插件
*/
// Register plugins
options.plugins?.forEach((plugin) => this.usePlugin(plugin));

// Put the lifecycle plugin at the end, so that you can get the changes of other plugins
this.usePlugin(GarfishOptionsLife(this.options, 'global-lifecycle'));

// Emit hooks and register apps
this.hooks.lifecycle.beforeBootstrap.emit(this.options); // 生命周期事件beforeBootstrap
this.registerApp(this.options.apps || []); // 注册子应用
this.running = true;
this.hooks.lifecycle.bootstrap.emit(this.options); // bootstrap
return this;
}

其中移除插件等内容,最重要的是registerApp调用,用于将配置注册到实例中

接下来的代码会移除无关紧要的代码,仅保留核心逻辑

1
2
3
4
5
6
7
8
9
10
11
registerApp(list: interfaces.AppInfo | Array<interfaces.AppInfo>) {
if (!Array.isArray(list)) list = [list];

for (const appInfo of list) {
if (!this.appInfos[appInfo.name]) {
this.appInfos[appInfo.name] = appInfo;
}
}

return this;
}

看上去仅仅是一些配置设定,那么所谓的路由绑定是从哪里发生的呢?这一切其实早就暗中进行了处理。

1
2
3
4
export type { interfaces } from '@garfish/core';
export { default as Garfish } from '@garfish/core';
export { GarfishInstance as default } from './instance';
export { defineCustomElements } from './customElement';

当调用 import Garfish from 'garfish';时, 使用的是默认创建好的全局Garfish实例。该逻辑简化版大概如下:

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
import { GarfishRouter } from '@garfish/router';
import { GarfishBrowserVm } from '@garfish/browser-vm';
import { GarfishBrowserSnapshot } from '@garfish/browser-snapshot';

// Initialize the Garfish, currently existing environment to allow only one instance (export to is for test)
function createContext(): Garfish {
// Existing garfish instance, direct return
if (inBrowser() && window['__GARFISH__'] && window['Garfish']) {
return window['Garfish'];
}

const GarfishInstance = new Garfish({
plugins: [GarfishRouter(), GarfishBrowserVm(), GarfishBrowserSnapshot()],
});

type globalValue = boolean | Garfish | Record<string, unknown>;
const set = (namespace: string, val: globalValue = GarfishInstance) => {
// NOTE: 这里有一部分状态判定的逻辑,以及确保只读,这里是精简后的逻辑
window[namespace] = val;
};

if (inBrowser()) {
// Global flag
set('Garfish');
Object.defineProperty(window, '__GARFISH__', {
get: () => true,
configurable: __DEV__ ? true : false,
});
}

return GarfishInstance;
}

export const GarfishInstance = createContext();

其中核心逻辑为:

  • 如果本地已经有Garfish实例,则直接从本地拿。(浏览器环境用于子应用,也可以从这边看出garfish并不支持其他的js环境
  • 创建Garfish实例,并安装插件:
    • GarfishRouter 路由劫持能力
    • GarfishBrowserVm js运行时沙盒隔离
    • GarfishBrowserSnapshot 浏览器状态快照
  • 在window上设置全局Garfish对象并标记__GARFISH__, 注意该变量为只读

其中安全和样式隔离的逻辑我们暂且不看,先看其核心插件 GarfishRouter 的实现

插件系统

Garfish 自己实现了一套插件协议,其本质是pubsub模型的变种(部分生命周期的emit阶段增加了异步操作的等待逻辑)。

我们以Garfish最核心的插件 @garfish/router 为学习例子,该代码的位置在: garfish/packages/router/src/index.ts

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
export function GarfishRouter(_args?: Options) {
return function (Garfish: interfaces.Garfish): interfaces.Plugin {
Garfish.apps = {};
Garfish.router = router;

return {
name: 'router',
version: __VERSION__,

bootstrap(options: interfaces.Options) {
let activeApp: null | string = null;
const unmounts: Record<string, Function> = {};
const { basename } = options;
const { autoRefreshApp = true, onNotMatchRouter = () => null } =
Garfish.options;

async function active(
appInfo: interfaces.AppInfo,
rootPath: string = '/',
) {
routerLog(`${appInfo.name} active`, {
appInfo,
rootPath,
listening: RouterConfig.listening,
});

// In the listening state, trigger the rendering of the application
if (!RouterConfig.listening) return;

const { name, cache = true, active } = appInfo;
if (active) return active(appInfo, rootPath);
appInfo.rootPath = rootPath;

const currentApp = (activeApp = createKey());
const app = await Garfish.loadApp(appInfo.name, {
basename: rootPath,
entry: appInfo.entry,
cache: true,
domGetter: appInfo.domGetter,
});

if (app) {
app.appInfo.basename = rootPath;

const call = async (app: interfaces.App, isRender: boolean) => {
if (!app) return;
const isDes = cache && app.mounted;
if (isRender) {
return await app[isDes ? 'show' : 'mount']();
} else {
return app[isDes ? 'hide' : 'unmount']();
}
};

Garfish.apps[name] = app;
unmounts[name] = () => {
// Destroy the application during rendering and discard the application instance
if (app.mounting) {
delete Garfish.cacheApps[name];
}
call(app, false);
};

if (currentApp === activeApp) {
await call(app, true);
}
}
}

async function deactive(appInfo: interfaces.AppInfo, rootPath: string) {
routerLog(`${appInfo.name} deactive`, {
appInfo,
rootPath,
});

activeApp = null;
const { name, deactive } = appInfo;
if (deactive) return deactive(appInfo, rootPath);

const unmount = unmounts[name];
unmount && unmount();
delete Garfish.apps[name];

// Nested scene to remove the current application of nested data
// To avoid the main application prior to application
const needToDeleteApps = router.routerConfig.apps.filter((app) => {
if (appInfo.rootPath === app.basename) return true;
});
if (needToDeleteApps.length > 0) {
needToDeleteApps.forEach((app) => {
delete Garfish.appInfos[app.name];
delete Garfish.cacheApps[app.name];
});
router.setRouterConfig({
apps: router.routerConfig.apps.filter((app) => {
return !needToDeleteApps.some(
(needDelete) => app.name === needDelete.name,
);
}),
});
}
}

const apps = Object.values(Garfish.appInfos);

const appList = apps.filter((app) => {
if (!app.basename) app.basename = basename;
return !!app.activeWhen;
}) as Array<Required<interfaces.AppInfo>>;

const listenOptions = {
basename,
active,
deactive,
autoRefreshApp,
notMatch: onNotMatchRouter,
apps: appList,
listening: true,
};
routerLog('listenRouterAndReDirect', listenOptions);
listenRouterAndReDirect(listenOptions);
},

registerApp(appInfos) {
const appList = Object.values(appInfos);
// @ts-ignore
router.registerRouter(appList.filter((app) => !!app.activeWhen));
// After completion of the registration application, trigger application mount
// Has been running after adding routing to trigger the redirection
if (!Garfish.running) return;
routerLog('registerApp initRedirect', appInfos);
initRedirect();
},
};
};
}

一个插件的结构形如 (context: Garfish) => Plugin

其中 Plugin 类型为一个对象,包含各个阶段的生命周期以及name/version等插件信息描述属性。

router 插件为例,其作用在bootstrapregisterApp两个生命周期阶段

生命周期定义可以在这里看到: garfish/packages/core/src/lifecycle.ts

Garfish.run 视角来看,执行顺序为: beforeBootstrap -> beforeRegisterApp -> registerApp -> bootstrap -> ... 因此我们先看registerApp的逻辑。

registerApp 阶段

1
this.hooks.lifecycle.registerApp.emit(currentAdds);

Garfish 执行 registerApp函数 完毕后触发 registerApp 生命周期hook, 将当前注册的子应用列表发送到事件回调

garfish/packages/router/src/index.ts

1
2
3
4
5
6
7
8
9
10
11
12
{
name: 'router',
registerApp(appInfos) {
const appList = Object.values(appInfos);
router.registerRouter(appList.filter((app) => !!app.activeWhen));
// After completion of the registration application, trigger application mount
// Has been running after adding routing to trigger the redirection
if (!Garfish.running) return;
routerLog('registerApp initRedirect', appInfos);
initRedirect();
},
}

插件接收到子应用列表, 将依次调用:

  • router.registerRouter 注册到路由列表,其中会把不存在activeWhen属性的子应用过滤
  • initRedirect 初始化重定向逻辑

garfish/packages/router/src/context.ts

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
export const RouterConfig: Options = {
basename: '/',
current: {
fullPath: '/',
path: '/',
matched: [],
query: {},
state: {},
},
apps: [],
beforeEach: (to, from, next) => next(),
afterEach: (to, from, next) => next(),
active: () => Promise.resolve(),
deactive: () => Promise.resolve(),
routerChange: () => {},
autoRefreshApp: true,
listening: true,
};

export const registerRouter = (Apps: Array<interfaces.AppInfo>) => {
const unregisterApps = Apps.filter(
(app) => !RouterConfig.apps.some((item) => app.name === item.name),
);
RouterConfig[apps] = RouterConfig.apps.concat(unregisterApps);
};

const Router: RouterInterface = {
registerRouter,
};

export default Router;

registerRouter阶段仅仅是将子应用注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export const initRedirect = () => {
linkTo({
toRouterInfo: {
fullPath: location.pathname,
path: getPath(RouterConfig.basename!),
query: parseQuery(location.search),
state: history.state,
},
fromRouterInfo: {
fullPath: '/',
path: '/',
query: {},
state: {},
},
eventType: 'pushState',
});
};

initRedirect阶段则是调用linkTo函数去实现一个跳转,这里具体细节比较复杂。可以简单理解为子应用版页面跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 重载指定路由
// 1. 当前的子应用需要销毁
// 2. 获取当前需要激活的应用
// 3. 获取新的需要激活应用
// 4. 触发函数beforeEach,在销毁所有应用之前触发
// 5. 触发需要销毁应用的deactive函数
// 6. 如果不需要激活应用,默认触发popstate应用组件view child更新
export const linkTo = async ({
toRouterInfo,
fromRouterInfo,
eventType,
}: {
toRouterInfo: RouterInfo;
fromRouterInfo: RouterInfo;
eventType: keyof History | 'popstate';
}) => Promise<void>

bootstrap 阶段

1
this.hooks.lifecycle.bootstrap.emit(this.options);
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
{
name: 'router',
bootstrap(options: interfaces.Options) {
let activeApp: null | string = null;
const unmounts: Record<string, Function> = {};
const { basename } = options;
const { autoRefreshApp = true, onNotMatchRouter = () => null } =
Garfish.options;

async function active(
appInfo: interfaces.AppInfo,
rootPath: string = '/',
) {
routerLog(`${appInfo.name} active`, {
appInfo,
rootPath,
listening: RouterConfig.listening,
});

// In the listening state, trigger the rendering of the application
if (!RouterConfig.listening) return;

const { name, cache = true, active } = appInfo;
if (active) return active(appInfo, rootPath);
appInfo.rootPath = rootPath;

const currentApp = (activeApp = createKey());
const app = await Garfish.loadApp(appInfo.name, {
basename: rootPath,
entry: appInfo.entry,
cache: true,
domGetter: appInfo.domGetter,
});

if (app) {
app.appInfo.basename = rootPath;

const call = async (app: interfaces.App, isRender: boolean) => {
if (!app) return;
const isDes = cache && app.mounted;
if (isRender) {
return await app[isDes ? 'show' : 'mount']();
} else {
return app[isDes ? 'hide' : 'unmount']();
}
};

Garfish.apps[name] = app;
unmounts[name] = () => {
// Destroy the application during rendering and discard the application instance
if (app.mounting) {
delete Garfish.cacheApps[name];
}
call(app, false);
};

if (currentApp === activeApp) {
await call(app, true);
}
}
}

async function deactive(appInfo: interfaces.AppInfo, rootPath: string) {
routerLog(`${appInfo.name} deactive`, {
appInfo,
rootPath,
});

activeApp = null;
const { name, deactive } = appInfo;
if (deactive) return deactive(appInfo, rootPath);

const unmount = unmounts[name];
unmount && unmount();
delete Garfish.apps[name];

// Nested scene to remove the current application of nested data
// To avoid the main application prior to application
const needToDeleteApps = router.routerConfig.apps.filter((app) => {
if (appInfo.rootPath === app.basename) return true;
});
if (needToDeleteApps.length > 0) {
needToDeleteApps.forEach((app) => {
delete Garfish.appInfos[app.name];
delete Garfish.cacheApps[app.name];
});
router.setRouterConfig({
apps: router.routerConfig.apps.filter((app) => {
return !needToDeleteApps.some(
(needDelete) => app.name === needDelete.name,
);
}),
});
}
}

const apps = Object.values(Garfish.appInfos);

const appList = apps.filter((app) => {
if (!app.basename) app.basename = basename;
return !!app.activeWhen;
}) as Array<Required<interfaces.AppInfo>>;

const listenOptions = {
basename,
active,
deactive,
autoRefreshApp,
notMatch: onNotMatchRouter,
apps: appList,
listening: true,
};
routerLog('listenRouterAndReDirect', listenOptions);
listenRouterAndReDirect(listenOptions);
},
}

bootstrap阶段主要构造路由配置,并调用listenRouterAndReDirect(listenOptions)来进行路由的代理/拦截
其中主要需要关心的active操作(即子应用挂载逻辑)做了以下事情:

  • 调用 Garfish.loadApp 将子应用挂载到子应用挂载节点上(Promise 同步加载)
  • Garfish.apps 记录该app
  • 注册到 unmounts 记录销毁逻辑
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
/**
* 1.注册子应用
* 2.对应子应用激活,触发激活回调
* @param Options
*/
export const listenRouterAndReDirect = ({
apps,
basename = '/',
autoRefreshApp,
active,
deactive,
notMatch,
listening = true,
}: Options) => {
// 注册子应用、注册激活、销毁钩子
registerRouter(apps);

// 初始化信息
setRouterConfig({
basename,
autoRefreshApp,
// supportProxy: !!window.Proxy,
active,
deactive,
notMatch,
listening,
});

// 开始监听路由变化触发、子应用更新。重载默认初始子应用
listen();
};
1
2
3
4
5
6
export const registerRouter = (Apps: Array<interfaces.AppInfo>) => {
const unregisterApps = Apps.filter(
(app) => !RouterConfig.apps.some((item) => app.name === item.name),
);
RouterSet('apps', RouterConfig.apps.concat(unregisterApps));
};

registerRouter没有什么特殊的,仅仅管理路由状态

接下来看一下listen()函数做的事情:

1
2
3
4
export const listen = () => {
normalAgent();
initRedirect();
};

initRedirect我们之前看过了,现在我们主要看normalAgent的实现

garfish/packages/router/src/agentRouter.ts

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
export const normalAgent = () => {
// By identifying whether have finished listening, if finished listening, listening to the routing changes do not need to hijack the original event
// Support nested scene
const addRouterListener = function () {
window.addEventListener(__GARFISH_BEFORE_ROUTER_EVENT__, function (env) {
RouterConfig.routerChange && RouterConfig.routerChange(location.pathname);
linkTo((env as any).detail);
});
};

if (!window[__GARFISH_ROUTER_FLAG__]) {
// Listen for pushState and replaceState, call linkTo, processing, listen back
// Rewrite the history API method, triggering events in the call

const rewrite = function (type: keyof History) {
const hapi = history[type];
return function (this: History) {
const urlBefore = window.location.pathname + window.location.hash;
const stateBefore = history?.state;
const res = hapi.apply(this, arguments);
const urlAfter = window.location.pathname + window.location.hash;
const stateAfter = history?.state;

const e = createEvent(type);
(e as any).arguments = arguments;

if (
urlBefore !== urlAfter ||
JSON.stringify(stateBefore) !== JSON.stringify(stateAfter)
) {
window.dispatchEvent(
new CustomEvent(__GARFISH_BEFORE_ROUTER_EVENT__, {
detail: {
toRouterInfo: {
fullPath: urlAfter,
query: parseQuery(location.search),
path: getPath(RouterConfig.basename!, urlAfter),
state: stateAfter,
},
fromRouterInfo: {
fullPath: urlBefore,
query: parseQuery(location.search),
path: getPath(RouterConfig.basename!, urlBefore),
state: stateBefore,
},
eventType: type,
},
}),
);
}
// window.dispatchEvent(e);
return res;
};
};

history.pushState = rewrite('pushState');
history.replaceState = rewrite('replaceState');

// Before the collection application sub routing, forward backward routing updates between child application
window.addEventListener(
'popstate',
function (event) {
// Stop trigger collection function, fire again match rendering
if (event && typeof event === 'object' && (event as any).garfish)
return;
if (history.state && typeof history.state === 'object')
delete history.state[__GARFISH_ROUTER_UPDATE_FLAG__];
window.dispatchEvent(
new CustomEvent(__GARFISH_BEFORE_ROUTER_EVENT__, {
detail: {
toRouterInfo: {
fullPath: location.pathname,
query: parseQuery(location.search),
path: getPath(RouterConfig.basename!),
},
fromRouterInfo: {
fullPath: RouterConfig.current!.fullPath,
path: getPath(
RouterConfig.basename!,
RouterConfig.current!.path,
),
query: RouterConfig.current!.query,
},
eventType: 'popstate',
},
}),
);
},
false,
);

window[__GARFISH_ROUTER_FLAG__] = true;
}
addRouterListener();
};

normalAgent 做了以下事情:

  • 通过rewrite函数重写history.pushStatehistory.pushState
    • rewrite函数则是在调用以上方法的前后增加了一些当前情况的快照,如果url/state发生变化则触发__GARFISH_BEFORE_ROUTER_EVENT__事件
  • popstate事件增加监听
  • 调用 addRouterListener 增加路由监听回调。监听方法基于浏览器内置的事件系统,事件名: __GARFISH_BEFORE_ROUTER_EVENT__

综上, router 通过监听history的方法来执行副作用调用linkTo函数,而linkTo函数则通过一系列操作将匹配的路由调用active方法,将不匹配的路由调用deactive方法以实现类型切换

这时候我们再回过头来看一下active函数的实现

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
async function active(
appInfo: interfaces.AppInfo,
rootPath: string = '/',
) {
routerLog(`${appInfo.name} active`, {
appInfo,
rootPath,
listening: RouterConfig.listening,
});

// In the listening state, trigger the rendering of the application
if (!RouterConfig.listening) return;

const { name, cache = true, active } = appInfo;
if (active) return active(appInfo, rootPath);
appInfo.rootPath = rootPath;

const currentApp = (activeApp = createKey());
const app = await Garfish.loadApp(appInfo.name, {
basename: rootPath,
entry: appInfo.entry,
cache: true,
domGetter: appInfo.domGetter,
});

if (app) {
app.appInfo.basename = rootPath;

const call = async (app: interfaces.App, isRender: boolean) => {
if (!app) return;
const isDes = cache && app.mounted;
if (isRender) {
return await app[isDes ? 'show' : 'mount']();
} else {
return app[isDes ? 'hide' : 'unmount']();
}
};

Garfish.apps[name] = app;
unmounts[name] = () => {
// Destroy the application during rendering and discard the application instance
if (app.mounting) {
delete Garfish.cacheApps[name];
}
call(app, false);
};

if (currentApp === activeApp) {
await call(app, true);
}
}
}

其核心代码则是调用了Garfish.loadApp方法来执行加载操作。

应用加载

接下来我们看一下loadApp函数

garfish/packages/core/src/garfish.ts

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
loadApp(
appName: string,
options?: Partial<Omit<interfaces.AppInfo, 'name'>>,
): Promise<interfaces.App | null> {
assert(appName, 'Miss appName.');

let appInfo = generateAppOptions(appName, this, options);

const asyncLoadProcess = async () => {
// Return not undefined type data directly to end loading
const stop = await this.hooks.lifecycle.beforeLoad.emit(appInfo);

if (stop === false) {
warn(`Load ${appName} application is terminated by beforeLoad.`);
return null;
}

//merge configs again after beforeLoad for the reason of app may be re-registered during beforeLoad resulting in an incorrect information
appInfo = generateAppOptions(appName, this, options);

assert(
appInfo.entry,
`Can't load unexpected child app "${appName}", ` +
'Please provide the entry parameters or registered in advance of the app.',
);

// Existing cache caching logic
let appInstance: interfaces.App | null = null;
const cacheApp = this.cacheApps[appName];

if (appInfo.cache && cacheApp) {
appInstance = cacheApp;
} else {
try {
const [manager, resources, isHtmlMode] = await processAppResources(
this.loader,
appInfo,
);

appInstance = new App(
this,
appInfo,
manager,
resources,
isHtmlMode,
appInfo.customLoader,
);

// The registration hook will automatically remove the duplication
for (const key in this.plugins) {
appInstance.hooks.usePlugin(this.plugins[key]);
}
if (appInfo.cache) {
this.cacheApps[appName] = appInstance;
}
} catch (e) {
__DEV__ && warn(e);
this.hooks.lifecycle.errorLoadApp.emit(e, appInfo);
}
}

await this.hooks.lifecycle.afterLoad.emit(appInfo, appInstance);
return appInstance;
};

if (!this.loading[appName]) {
this.loading[appName] = asyncLoadProcess().finally(() => {
delete this.loading[appName];
});
}
return this.loading[appName];
}

该函数做了以下操作:

  • 首先执行asyncLoadProcess来异步加载app,如果app正在加载则返回该Promise
  • 使用generateAppOptions计算全局+本地的配置,并通过黑名单过滤掉一部分的无用参数(filterAppConfigKeys)
  • 如果当前app已加载则直接返回缓存后的内容
  • 如果是第一次加载,则执行 processAppResources 进行请求, 请求的地址为 entry 指定的地址。
  • 当请求完毕后创建new App对象,将其放到内存中
  • 应用插件/记录缓存/发布生命周期事件等

接下来我们看核心函数, processAppResources的实现

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
export async function processAppResources(loader: Loader, appInfo: AppInfo) {
let isHtmlMode: Boolean = false,
fakeEntryManager;
const resources: any = { js: [], link: [], modules: [] }; // Default resources
assert(appInfo.entry, `[${appInfo.name}] Entry is not specified.`);
const { resourceManager: entryManager } = await loader.load({
scope: appInfo.name,
url: transformUrl(location.href, appInfo.entry),
});

// Html entry
if (entryManager instanceof TemplateManager) {
isHtmlMode = true;
const [js, link, modules] = await fetchStaticResources(
appInfo.name,
loader,
entryManager,
);
resources.js = js;
resources.link = link;
resources.modules = modules;
} else if (entryManager instanceof JavaScriptManager) {
// Js entry
isHtmlMode = false;
const mockTemplateCode = `<script src="${entryManager.url}"></script>`;
fakeEntryManager = new TemplateManager(mockTemplateCode, entryManager.url);
entryManager.setDep(fakeEntryManager.findAllJsNodes()[0]);
resources.js = [entryManager];
} else {
error(`Entrance wrong type of resource of "${appInfo.name}".`);
}

return [fakeEntryManager || entryManager, resources, isHtmlMode];
}

首先根据appInfo.entry调用loader.load函数,生成一个entryManager。如果entry指向的是html地址则获取静态数据后拿取js,link,modules,如果entry指向的是一个js地址则伪造一个仅包含这段js的js资源。最后的返回值是一个 [resourceManager, resources, isHtmlMode] 的元组。

其中resourceManager的大概结构如下:
resourceManager

loader.load的本质上就是发请求获取数据然后把请求到的纯文本转化成结构化,如果是html则对html声明的资源进行进一步的请求获取。这边就不再赘述。

我们回到loadApp函数的实现。

之后,代码根据processAppResources获取到的[resourceManager, resources, isHtmlMode]信息来创建一个new App;

1
2
3
4
5
6
7
8
appInstance = new App(
this,
appInfo,
manager,
resources,
isHtmlMode,
appInfo.customLoader,
);

appInstance

new App的过程中没有任何逻辑,仅仅是一些变量的定义。值得注意的是在此过程中会对插件系统做一些初始化设定

garfish/packages/core/src/module/app.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export class App {
constructor(
context: Garfish,
appInfo: AppInfo,
entryManager: TemplateManager,
resources: interfaces.ResourceModules,
isHtmlMode: boolean,
customLoader?: CustomerLoader,
) {
// ...

// Register hooks
this.hooks = appLifecycle();
this.hooks.usePlugin({
...appInfo,
name: `${appInfo.name}-lifecycle`,
});

// ...
}
}

到这一步为止,我们还在做一些准备工作:

  • 从远程获取资源
  • 将纯文本解析成结构化对象和AST
  • 进一步获取js/css的实际代码

接下来我们需要一个调用方能够帮助我们将获取到的资源执行并挂载到dom上。

这时候我们就需要回到我们的router插件。还记得我们的GarfishRouter.bootstrap.active里的代码么?

garfish/packages/router/src/index.ts

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
export function GarfishRouter(_args?: Options) {
return function (Garfish: interfaces.Garfish): interfaces.Plugin {
return {
// ...

bootstrap(options: interfaces.Options) {
// ...

async function active(
appInfo: interfaces.AppInfo,
rootPath: string = '/',
) {
// ...
const app = await Garfish.loadApp(appInfo.name, {
basename: rootPath,
entry: appInfo.entry,
cache: true,
domGetter: appInfo.domGetter,
});

if (app) {
app.appInfo.basename = rootPath;

const call = async (app: interfaces.App, isRender: boolean) => {
if (!app) return;
const isDes = cache && app.mounted;
if (isRender) {
return await app[isDes ? 'show' : 'mount']();
} else {
return app[isDes ? 'hide' : 'unmount']();
}
};

Garfish.apps[name] = app;
unmounts[name] = () => {
// Destroy the application during rendering and discard the application instance
if (app.mounting) {
delete Garfish.cacheApps[name];
}
call(app, false);
};

if (currentApp === activeApp) {
await call(app, true);
}
}
}

// ...
};
};
}

当我们第一次执行到call函数时,会执行app.mount()函数来实现应用的挂载。

我们看下app.mount()的实现:

garfish/packages/core/src/module/app.ts

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
export class App {
async mount() {
if (!this.canMount()) return false;
this.hooks.lifecycle.beforeMount.emit(this.appInfo, this, false);

this.active = true;
this.mounting = true;
try {
this.context.activeApps.push(this);
// add container and compile js with cjs
const { asyncScripts } = await this.compileAndRenderContainer();
if (!this.stopMountAndClearEffect()) return false;

// Good provider is set at compile time
const provider = await this.getProvider();
// Existing asynchronous functions need to decide whether the application has been unloaded
if (!this.stopMountAndClearEffect()) return false;

this.callRender(provider, true);
this.display = true;
this.mounted = true;
this.hooks.lifecycle.afterMount.emit(this.appInfo, this, false);

await asyncScripts;
if (!this.stopMountAndClearEffect()) return false;
} catch (e) {
this.entryManager.DOMApis.removeElement(this.appContainer);
this.hooks.lifecycle.errorMountApp.emit(e, this.appInfo);
return false;
} finally {
this.mounting = false;
}
return true;
}

// Performs js resources provided by the module, finally get the content of the export
async compileAndRenderContainer() {
// Render the application node
// If you don't want to use the CJS export, at the entrance is not can not pass the module, the require
await this.renderTemplate();

// Execute asynchronous script
return {
asyncScripts: new Promise<void>((resolve) => {
// Asynchronous script does not block the rendering process
setTimeout(() => {
if (this.stopMountAndClearEffect()) {
for (const jsManager of this.resources.js) {
if (jsManager.async) {
try {
this.execScript(
jsManager.scriptCode,
{},
jsManager.url || this.appInfo.entry,
{
async: false,
noEntry: true,
},
);
} catch (e) {
this.hooks.lifecycle.errorMountApp.emit(e, this.appInfo);
}
}
}
}
resolve();
});
}),
};
}
}

mount主要实现以下操作:

  • 生命周期的分发: beforeMount, afterMount
  • 状态变更: this.active, this.mounting, this.display
  • 调用 this.compileAndRenderContainer 执行编译
    • 调用this.renderTemplate渲染同步代码片段
    • 返回 asyncScripts 函数用于在下一个宏任务(task) 执行异步js代码片段
  • 在每一个异步片段过程中都尝试执行 stopMountAndClearEffect 来判断当前状态,以确保状态的准确性(用于处理在异步代码执行过程中被取消的问题)

我们看一下renderTemplate的逻辑:

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
57
58
59
60
61
62
63
64
65
66
67
68
69
export class App {
private async renderTemplate() {
const { appInfo, entryManager, resources } = this;
const { url: baseUrl, DOMApis } = entryManager;
const { htmlNode, appContainer } = createAppContainer(appInfo);

// Transformation relative path
this.htmlNode = htmlNode;
this.appContainer = appContainer;

// To append to the document flow, recursive again create the contents of the HTML or execute the script
await this.addContainer();

const customRenderer: Parameters<typeof entryManager.createElements>[0] = {
// ...
body: (node) => {
if (!this.strictIsolation) {
node = entryManager.cloneNode(node);
node.tagName = 'div';
node.attributes.push({
key: __MockBody__,
value: null,
});
}
return DOMApis.createElement(node);
},
script: (node) => {
const mimeType = entryManager.findAttributeValue(node, 'type');
const isModule = mimeType === 'module';

if (mimeType) {
// Other script template
if (!isModule && !isJsType({ type: mimeType })) {
return DOMApis.createElement(node);
}
}
const jsManager = resources.js.find((manager) => {
return !manager.async ? manager.isSameOrigin(node) : false;
});

if (jsManager) {
const { url, scriptCode } = jsManager;
this.execScript(scriptCode, {}, url || this.appInfo.entry, {
isModule,
async: false,
isInline: jsManager.isInlineScript(),
noEntry: toBoolean(
entryManager.findAttributeValue(node, 'no-entry'),
),
});
} else if (__DEV__) {
const async = entryManager.findAttributeValue(node, 'async');
if (typeof async === 'undefined' || async === 'false') {
const tipInfo = JSON.stringify(node, null, 2);
warn(
`Current js node cannot be found, the resource may not exist.\n\n ${tipInfo}`,
);
}
}
return DOMApis.createScriptCommentNode(node);
},

// ...
};

// Render dom tree and append to document.
entryManager.createElements(customRenderer, htmlNode);
}
}
  • 调用 createAppContainer 函数创建一些空白的容器dom, 注意此时还没有挂载到界面上:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    export function createAppContainer(appInfo: interfaces.AppInfo) {
    const name = appInfo.name;
    // Create a temporary node, which is destroyed by the module itself
    let htmlNode: HTMLDivElement | HTMLHtmlElement =
    document.createElement('div');
    const appContainer = document.createElement('div');

    if (appInfo.sandbox && appInfo.sandbox.strictIsolation) {
    htmlNode = document.createElement('html');
    const root = appContainer.attachShadow({ mode: 'open' });
    root.appendChild(htmlNode);
    // asyncNodeAttribute(htmlNode, document.body);
    dispatchEvents(root);
    } else {
    htmlNode.setAttribute(__MockHtml__, '');
    appContainer.appendChild(htmlNode);
    }
    appContainer.id = `${appContainerId}_${name}_${createKey()}`;

    return {
    htmlNode,
    appContainer,
    };
    }
    • 如果开启了 sandboxstrictIsolation 配置则进行严格的隔离(使用appContainer.attachShadow)来创建ShadowDOM
  • 调用addContainer来将代码挂载容器组件到文档中, 通过执行domGetter来获取父容器节点
    1
    2
    3
    4
    5
    6
    7
    private async addContainer() {
    // Initialize the mount point, support domGetter as promise, is advantageous for the compatibility
    const wrapperNode = await getRenderNode(this.appInfo.domGetter);
    if (typeof wrapperNode.appendChild === 'function') {
    wrapperNode.appendChild(this.appContainer);
    }
    }
  • 调用entryManager.createElements(customRenderer, htmlNode); 来实际创建节点。
    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
    // Render dom tree
    createElements(renderer: Renderer, parent: Element) {
    const elements: Array<Element> = [];
    const traverse = (node: Node | Text, parentEl?: Element) => {
    let el: any;
    if (this.DOMApis.isCommentNode(node)) {
    // Filter comment node
    } else if (this.DOMApis.isText(node)) {
    el = this.DOMApis.createTextNode(node);
    parentEl && parentEl.appendChild(el);
    } else if (this.DOMApis.isNode(node)) {
    const { tagName, children } = node as Node;
    if (renderer[tagName]) {
    el = renderer[tagName](node as Node);
    } else {
    el = this.DOMApis.createElement(node as Node);
    }
    if (parentEl && el) parentEl.appendChild(el);

    if (el) {
    const { nodeType, _ignoreChildNodes } = el;
    // Filter "comment" and "document" node
    if (!_ignoreChildNodes && nodeType !== 8 && nodeType !== 10) {
    for (const child of children) {
    traverse(child, el);
    }
    }
    }
    }
    return el;
    };

    for (const node of this.astTree) {
    if (this.DOMApis.isNode(node) && node.tagName !== '!doctype') {
    const el = traverse(node, parent);
    el && elements.push(el);
    }
    }
    return elements;
    }
    使用traverse函数对自身进行树节点遍历,将ast树转换为dom树并挂载到parent
    • 注意有意思的一点是他是在遍历ast过程中的同时执行appendChild方法加载到dom树上而不是将节点生成完毕后一次性加载(也许是因为操作都是在一个task中所以浏览器会一次性执行?)

总结

综上,garfish完成了一次远程获取目标代码 => 解析成ast => 然后再从ast转换成dom树的过程。

将一段远程的页面/js加载到当前页面的固定位置

把代码仓库当做数据库,github action持久化存储新思路

背景

我想做一个rss订阅机器人,通过一个定时任务定期将我关注的内容推送到我的 Tailchat 群组。但是我又不想自己去单独搭建一个服务器来部署,因为功能很小、单独部署的成本会被放大,也不容易被其他人很简单的使用。而且长期维护的成本也是比较高的,希望能处于无人值守的运行模式

那么整理一下需求:

  • 定时任务
  • 简单部署
  • 不需要运维

可以说是非常理想了,那么有这样成熟的解决方案么?答案是有的。那就是github action

Github action 可以满足我的所有需求,只需要一个简单的定时任务即可实现我的三个需求。唯一的难点在于数据库,也就是持久化存储。

众所周知,rss机器人的原理就是定时请求rss订阅地址,将返回的内容结构化以后与之前存储的数据进行比较,将更新的信息提取出来发送到外部服务。那么为了能够比较差异,一个持久化的数据库是必不可少的。那么github action可以实现数据库么?答案是可以的,我只需要将数据存储在代码仓库中,每次执行action之前将数据取出,然后在action执行完毕之后将数据存回仓库,那么一个用于低频读写的文件数据库就实现了。

理论存在,实践开始!

开始造轮子

在github上搜索了一圈没有发现有现成的轮子,因此就开始自己造一个。

核心流程如下:

准备数据流程

  • 通过git worktree创建一个独立的工作区
  • 指定工作区的分支为一个独立分支用于存储数据
  • 如果该分支之前不存在,跳过准备过程
  • 如果分支已存在,拉取分支代码,将存储分支的指定目录文件复制到主工作空间的指定目录文件

修改数据

  • 在后续的action中执行脚本
  • 脚本读取文件数据库,在这里使用的是lowdb,当然也可以使用sqlite,看个人喜好
  • 更新数据库并写入

持久化存储数据

  • action执行完毕进入post阶段
  • 执行post action将主工作区的数据库文件覆盖到存储工作区中
  • 存储工作区通过github action的token或者传入参数的token 提交变更到github的存储分支中。
  • 结束流程,等待后续的执行

成果

那么通过上面一系列步骤,我们就成功把github当做我们自己的action应用的数据库了。

一个简单的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
name: Tests

on:
workflow_dispatch:

jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/[email protected]
- name: Checkout date
uses: moonrailgun/[email protected]
with:
path: date
- name: Read and show
run: cat date
- name: update date
run: echo $(date) > date

这个action表示,每执行一次,我们的actions/filedb中的date文件就会更新成最新的.当然也可以加上一些定时任务触发器让他自动执行。当然建议不要滥用哦,可以使用低频一些

在Github Marketplace查看: https://github.com/marketplace/actions/branch-filestorage-action

开源地址: moonrailgun/branch-filestorage-action

RSSBot地址: msgbyte/tailchat-rss-bot

如何让团队项目白嫖 vercel 的免费服务

背景

Vercel 是一个对 Hobby 计划提供免费服务,并且在中国地区做了很好的CDN的serverless项目,用于代理静态页面或者做一些简单的api是非常方便的。

对于个人项目来说,vercel可以很好的在网页上直接操作导入,但是对于存储在Github组织的项目来说想要直接创建是不行的,这时候vercel会跳转到 pro plan 并且付费后才能使用

这时候我们就要取巧用一些方法绕过 github 组织的限制

创建不与github绑定的Vercel项目

使用npm全局安装vercel命令行终端

1
npm install -g vercel

在项目目录下直接执行以下命令

1
2
vercel login
vercel

这里会有一个交互式的终端操作。按照他的步骤顺序执行下去,就会在Vercel上创建一个没有连接任何一个Github项目的服务了。此时如果部署成功的话是可以通过网页界面直接点击到已经部署的服务的。

这时候我们就成功了一半了,剩下的是需要我们实现每次提交代码自动部署vercel的功能。

设置自动部署

github action上创建一个编译CI,并在build操作后面插入以下命令:

1
2
3
4
5
6
7
8
9
10
- name: Deploy to Vercel
uses: amondnet/[email protected]
env:
VERSION: ${{ env.GITHUB_SHA }}
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID}}
vercel-project-id: ${{ secrets.PROJECT_ID}}
working-directory: ./
vercel-args: '--prod' # 可不填

其中需要在Github Secrets中提前准备好以下参数

  • VERCEL_TOKEN: 通过 https://vercel.com/account/tokens 创建
  • ORG_ID: 项目根目录 .vercel/project.json 可见
  • PROJECT_ID: 项目根目录 .vercel/project.json 可见

CentOS7 安装gcc手册

众所周知,gcc版本数都已经两位数了,yum源的直接安装gcc的最新版本还停留在4.8.5。而对于部分c++的应用来说,高版本的gcc是必不可少的。而现在中文网络上教你升级gcc的办法都是手动下载gcc源码然后去编译。

别急!在你选择去按照教程手动一步步编译前,静下心来。手动编译的坑数不胜数,而Redhat 官方早就提供了解决方案, 那就是devtoolset(在centos8中改名为gcc-toolset)

devtoolset类似于node中的nvm,允许你在同一环境下安装多个gcc环境而不冲突

使用方法很简单:

1
2
yum install centos-release-scl # 通过centos-release-scl源安装devtoolset包
yum install devtoolset-8

其中

1
2
3
4
5
6
7
devtoolset-3对应gcc4.x.x版本
devtoolset-4对应gcc5.x.x版本
devtoolset-6对应gcc6.x.x版本
devtoolset-7对应gcc7.x.x版本
devtoolset-8对应gcc8.x.x版本
devtoolset-9对应gcc9.x.x版本
devtoolset-10对应gcc10.x.x版本

为使其生效还需要手动执行切换一下版本

1
source /opt/rh/devtoolset-8/enable

当然可以把这行代码保存在 .bashrc / .zshrc 中以每次连接shell都自动执行

k3s安装 OpenFaaS小记

官方手册

https://docs.openfaas.com/deployment/kubernetes/

使用hosts

因为众所周知的原因,国内访问部分网站不是很顺畅,如以下步骤有网络问题。这里建议使用 https://www.ipaddress.com/ 这个网站来获取最佳的hosts

First of all

1
2
3
4
5
# 获取 faas-cli
curl -sL https://cli.openfaas.com | sudo sh

# 获取 arkade
curl -SLsf https://dl.get-arkade.dev/ | sudo sh

arkade 是一个helm 的封装工具,用于一键安装应用到k8s集群

一键安装 openfaas

1
arkade install openfaas

在此过程中可能会出现集群无法抵达的问题,可以参考这个issue: https://github.com/k3s-io/k3s/issues/1126

1
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml

安装完成

安装完成后会输出如下内容:

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
=======================================================================
= OpenFaaS has been installed. =
=======================================================================

# Get the faas-cli
curl -SLsf https://cli.openfaas.com | sudo sh

# Forward the gateway to your machine
kubectl rollout status -n openfaas deploy/gateway
kubectl port-forward -n openfaas svc/gateway 8080:8080 &

# If basic auth is enabled, you can now log into your gateway:
PASSWORD=$(kubectl get secret -n openfaas basic-auth -o jsonpath="{.data.basic-auth-password}" | base64 --decode; echo)
echo -n $PASSWORD | faas-cli login --username admin --password-stdin

faas-cli store deploy figlet
faas-cli list

# For Raspberry Pi
faas-cli store list \
--platform armhf

faas-cli store deploy figlet \
--platform armhf

# Find out more at:
# https://github.com/openfaas/faas

Thanks for using arkade!

为什么我选择Tailwindcss

前端的CSS发展经历了许多方案, 从早期的无序到后面的BEM,从自写到bootstrap的火爆,从 single css filecss modulecss in js

我想说说为什么我选择tailwind, 以及为什么我觉得tailwind这种提供工具的方式是我理想的解决方案

体积

因为tailwind几乎不会有冗余代码,那么tailwind项目的样式大小是可控的。此外它也不会引入无用的样式, 因为它会根据代码进行摇树优化。

可维护性与可控性

如果做过在一个现有的UI框架上进行一些覆盖样式,那么就会知道为什么说可维护性是多么重要。

因为这些工作往往是一个全局的样式。而当项目越来越复杂以后,你很难维护一个全局样式,因为你光看代码完全不知道这些代码的适用范围。当一个样式代码被越来越多人接手过后,你会发现他的可维护性越来越低 —— 因为你不敢删除任意一行代码,因为你不知道他会影响那些地方,那么你所能做的就是往上不断增加权重,不断覆盖,就如你的前人一样。

无需结构

可能很多初学者会认为BEM很清晰,比如tree__item就表示这是一个树的子项。但仔细想一想,你真的理解么? item到底是什么? 他是处于一颗dom树的哪个节点? 我必须得在他的父级增加一个名为<div class="tree"></div>的节点他的行为才是正确的么?

是的,对于BEM来说,如果是按照他的文档上所写的示例来复制的话, 这种固有的结构性是清晰的。但是如果想要深度进行一些定制的化,就显得有些力不从心了。

比如你想要修改树子项的字体大小:

1
2
3
<div class="tree">
<div class="tree__item"></div>
</div>

如果你不想增加行内样式, 那么你可以这样实现

1
2
3
.tree .tree__item {
font-size: 14px;
}

那如果想要增加作用域, 那可能会这样实现

1
2
3
<div class="tree foo">
<div class="tree__item"></div>
</div>
1
2
3
.tree.foo .tree__item {
font-size: 14px;
}

是的,这是一种理想情况。但是想象一下,这种方式,真的可维护么? 你可能会写出这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
.tree.foo {
.tree__item {
.tree__item-title {
font-size: 14px;
}
}

.tree__footer {
> span {
color: #ccc
}
}
}

看上去好像没什么问题,但是你要了解,现实情况可能更加复杂, 类名可能会有多套规则。而当我们为了修改一些样式,我们需要构造一套复杂的树形结构。这套结构是完整的,不可被破坏的。一点点的改动可能会引起整个结构树的崩溃 —— 到那时候,你真的能理解之前的代码想要表达什么意思么?

Tailwind 就不需要考虑这些问题。因为他几乎不需要额外的样式文件。当BEM在迭代中膨胀HTML与CSS时, tailwindcss只膨胀HTML

清晰

见名知意。学习成本低,阅读成本低。和直接写inline style一样但是更加优雅与可读。

性能

越长的CSS选择器查询越慢, 没有比tailwind更快的了。因为它的选择结构最多2层, 对于一个大型的前端应用来说,无数细微的性能优势最终也有产生有价值的点。

工具类样式库

我自己也在刚学react-native的时候写了个类似的样式库react-native-style-block。不过虽然我当时可能没有这么深刻的体会,只是本能的选择了这条路。从现在看来,这个方向完全是正确的。我相信工具类样式更加能够满足前端多变的需求。

每日一题 —— 混杂整数序列按规则进行重新排序

背景:

假设我们取一个数字 x 并执行以下任一操作:

  • a:将 x 除以 3 (如果可以被 3 除)
  • b:将 x 乘以 2

每次操作后,记下结果。如果从 9 开始,可以得到一个序列

有一个混杂的整数序列,现在任务是对它们重新排序,以使其符合上述序列并输出结果

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
输入: "[4,8,6,3,12,9]"
输出: [9,3,6,12,4,8]

解释: [9,3,6,12,4,8] => 9/3=3 -> 3*2=6 -> 6*2=12 -> 12/3=4 -> 4*2=8

输入: "[3000,9000]"
输出: [9000,3000]

输入: "[4,2]"
输出: [2,4]

输入: "[4,6,2]"
输出: [6,2,4]

人话翻译: 对数组重新排序,使得数组每一项可以满足这样一个规则:arr[i] = arr[i + 1] * 3 或者 arr[i] = arr[i + 1] / 2

解法:

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
function changeArr(arr) {
const map = new Map();
let find = false;
let ret;
arr.forEach((n) => {
map.set(n, (map.get(n) || 0) + 1); // 定义数组中一个数剩余的次数
});
arr.forEach((n) => {
if (find) return;
dfs(n, 2, [n]);
});

function dfs(prev, index, res) {
if (find) return;
if (index === arr.length + 1) {
// 找完了,退出搜索
find = true;
ret = res;
}
if (map.has(prev * 2) && map.get(prev * 2) > 0) {
// 数组中有上个值 *2 的数据存在
map.set(prev * 2, map.get(prev * 2) - 1);
dfs(prev * 2, index + 1, [...res, prev * 2]); // 将这个值加到结果中,并
map.set(prev * 2, map.get(prev * 2) + 1); // 没有找到,把次数加回来
}
if (!(prev % 3) && map.get(prev / 3) > 0) {
// 当前值能被3整数并且被3整数的值存在
map.set(prev / 3, map.get(prev / 3) - 1);
dfs(prev / 3, index + 1, [...res, prev / 3]);
map.set(prev / 3, map.get(prev / 3) + 1); // 没有找到,把次数加回来
}
}
return ret;
}

来自: 2年前端,如何跟抖音面试官battle

Webpack是个什么鬼——了解编译结果

简述

webpack 是一款现代化的前端打包工具,那么webpack是怎么将模块化代码能够在浏览器运行的?让我们来看一下

MVP

从一个最小webpack实例开始:

src/index.js

1
console.log("Hello Webpack");

我们直接使用命令行进行打包, 结果如下:

webpack –mode development

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
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({

/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/***/ (() => {

eval("console.log('Hello Webpack');\n\n\n//# sourceURL=webpack://webpack-demo/./src/index.js?");

/***/ })

/******/ });
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module can't be inlined because the eval devtool is used.
/******/ var __webpack_exports__ = {};
/******/ __webpack_modules__["./src/index.js"]();
/******/
/******/ })()
;

webpack –mode development –devtool hidden-source-map

1
2
3
4
5
6
7
8
9
/******/ (() => { // webpackBootstrap
var __webpack_exports__ = {};
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
console.log('Hello Webpack');

/******/ })()
;

webpack –mode production

1
console.log("Hello Webpack");

可以看到, 对于简单代码来说, 是否使用webpack打包区别不大。稍微注意一下,在默认的development环境中引入了两个变量__webpack_exports____webpack_modules__。顾名思义,是分别管理导出内容与模块列表的两个代码

__webpack_modules__ 是一个key为代码(模块)路径,值为模块执行结果的一个对象。

我们来试试稍微复杂一点的例子:

使用import

src/index.js

1
2
3
import {add} from './utils'

console.log(add(1, 2));

src/utils.js

1
2
3
export function add(a, b) {
return a + b;
}

我们直接使用命令行进行打包, 结果如下:

webpack –mode development

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({

/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils */ \"./src/utils.js\");\n\n\nconsole.log((0,_utils__WEBPACK_IMPORTED_MODULE_0__.add)(1, 2));\n\n\n//# sourceURL=webpack://webpack-demo/./src/index.js?");

/***/ }),

/***/ "./src/utils.js":
/*!**********************!*\
!*** ./src/utils.js ***!
\**********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"add\": () => (/* binding */ add)\n/* harmony export */ });\nfunction add(a, b) {\n return a + b;\n}\n\n\n//# sourceURL=webpack://webpack-demo/./src/utils.js?");

/***/ })

/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module can't be inlined because the eval devtool is used.
/******/ var __webpack_exports__ = __webpack_require__("./src/index.js");
/******/
/******/ })()
;

webpack –mode development –devtool hidden-source-map

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({

/***/ "./src/utils.js":
/*!**********************!*\
!*** ./src/utils.js ***!
\**********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "add": () => (/* binding */ add)
/* harmony export */ });
function add(a, b) {
return a + b;
}


/***/ })

/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/************************************************************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
(() => {
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils */ "./src/utils.js");


console.log((0,_utils__WEBPACK_IMPORTED_MODULE_0__.add)(1, 2));

})();

/******/ })()
;

(可以看到webpack --mode development --devtool hidden-source-map这个命令执行的结果和直接development是一样的,但是代码可读性更加高。之后的文章将以这个命令的输出为准)

webpack –mode production

1
(()=>{"use strict";console.log(3)})();

可以看到,webpack一旦发现了模块系统,那么就会增加很多中间代码(从注释 The module cache 到 变量 __webpack_exports__)

首先webpack每块代码都是以(() => {})() 这种形式的闭包来处理的,防止污染外部空间。

然后每一段都有一段注释来告知下面这块代码的逻辑是要做什么

我们来一一看一下:

module cache and require function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// The module cache
var __webpack_module_cache__ = {};

// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};

// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);

// Return the exports of the module
return module.exports;
}

定义了一个__webpack_module_cache__用于缓存模块

定义了一个__webpack_require__方法, 接受一个moduleId, 从下面可以看到moduleId是这个模块的路径(包括拓展名, 也即是__webpack_modules__管理的key值)

先判断缓存中是否存在这个模块,即是否加载,如果加载直接返回导出的数据,如果没有则在缓存中创建一个空对象{exports: {}}, 然后把module, module.exports, __webpack_require__作为参数去执行__webpack_modules__对应的方法

__webpack_modules__的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
var __webpack_modules__ = ({
"./src/utils.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"add": () => (add)
});
function add(a, b) {
return a + b;
}
})
});

可以看到,这里调用了一个__webpack_require__.r和一个__webpack_require__.d方法。目前我们不知道这两个方法是做什么用的。继续看下去。

webpack/runtime/define property getters

1
2
3
4
5
6
7
8
9
10
11
/* webpack/runtime/define property getters */
(() => {
// define getter functions for harmony exports
__webpack_require__.d = (exports, definition) => {
for(var key in definition) {
if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
})();

ddefine的缩写。可以看到这个方法的作用就是定义导出的值。

其目的就是遍历definition对象将其一一填入exports。需要注意的是使用__webpack_require__.d的目的在于确保:

  • 只能有一个key存在,如果exports中已经存在过了这个导出值,则不会重复导入
  • 确保exports中的属性只有getter, 不能被外部设置

make namespace object

1
2
3
4
5
6
7
8
9
10
/* webpack/runtime/make namespace object */
(() => {
// define __esModule on exports
__webpack_require__.r = (exports) => {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();

这个方法完成了两个目的。

  • exports定义了Symbol.toStringTag的值为Module
  • exports定义了__esModule的值为true

目的在于完成导出模块的兼容性

我们试试换一种导出方式:
src/utils.js

1
2
3
4
exports.add = function(a, b) {
return a + b;
}

结果:

1
2
3
4
5
6
7
8
var __webpack_modules__ = ({
"./src/utils.js":
((__unused_webpack_module, exports) => {
exports.add = function(a, b) {
return a + b;
}
})
});

可以看到输出简洁了很多。但是结果是一样的。都是在exports中插入导出的方法, 只不过esmodule的方式更加谨慎一点

那么前面的__unused_webpack_module又是干嘛的呢?我们修改一下代码
src/utils.js

1
2
3
module.exports = function add(a, b) {
return a + b;
}

结果:

1
2
3
4
5
6
7
8
var __webpack_modules__ = ({
"./src/utils.js":
((module) => {
module.exports = function add(a, b) {
return a + b;
}
})
});

一个主要细节在于esmodule使用了__webpack_require__.d来确保其代码的只读性,而commonjs没有:

esmodule和commonjs的模块导出可访问性区别

CommonJS 模块输出的是一个值的拷贝, ES6 模块输出的是值的引用

举个例子

commonjs

1
2
3
4
5
6
var a = 1;
setTimeout(() => {
a = 2;
}, 0)

exports.a = a;

生成代码:

1
2
3
4
5
6
7
8
((module) => {
var a = 1;
setTimeout(() => {
a = 2;
}, 0)

module.exports.a = a;
})

esmodule:

1
2
3
4
5
6
var a = 1;
setTimeout(() => {
a = 2;
}, 0)
export { a }

输出代码:

1
2
3
4
5
6
7
8
9
10
11
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "a": () => (/* binding */ a)
/* harmony export */ });
var a = 1;
setTimeout(() => {
a = 2;
}, 0)
})

可以看到区别:

  • commonjs 输出的a: 1 -> 1
  • esmodule 输出的a: 1 -> 2

因为commonjs内部实现是赋值,程序导出以后原来的a和导出的a的关系就没有了

esmodule输出的一个对象,内部的getter会每次去拿最新的a的值


那么到此我们的中间代码就看完了,顺便还介绍了一下webpack的导出结果。完整的中间代码列表可以看这个文件

执行代码

在上面的示例中,我们得到以下代码:

1
2
3
4
5
6
7
var __webpack_exports__ = {};
(() => {
__webpack_require__.r(__webpack_exports__);
var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/utils.js");

console.log((0,_utils__WEBPACK_IMPORTED_MODULE_0__.add)(1, 2));
})();

该代码作为项目的入口代码, 完成了以下逻辑

  • 通过 __webpack_require__.r 标记这个文件导出类型为esmodule
  • 执行 __webpack_require__ 并将导入的结果存放到临时变量 _utils__WEBPACK_IMPORTED_MODULE_0__
  • 执行 (0,_utils__WEBPACK_IMPORTED_MODULE_0__.add)(1, 2) 并导出结果。这里的(0, ...)是为了重置方法的this指向
    • Comma operator
    • 这个方法等价于
      1
      2
      const add = _utils__WEBPACK_IMPORTED_MODULE_0__.add
      add(1, 2)

让我们来微调一下代码:

1
2
3
const add = require('./utils')

console.log(add(1, 2));

输出:

1
2
3
4
5
var __webpack_exports__ = {};
(() => {
const add = __webpack_require__("./src/utils.js")
console.log(add(1, 2));
})();

可以看到, 其主要的区别就是__webpack_require__.r, 其他的区别不是很大。

动态代码

修改部分代码:

src/index.js

1
2
3
import('./utils').then(({add}) => {
console.log(add(1,2))
})

生成代码:

webpack –mode development –devtool hidden-source-map

dist/main.js

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({});
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = __webpack_modules__;
/******/
/************************************************************************/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/ensure chunk */
/******/ (() => {
/******/ __webpack_require__.f = {};
/******/ // This file contains only the entry chunk.
/******/ // The chunk loading function for additional chunks
/******/ __webpack_require__.e = (chunkId) => {
/******/ return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
/******/ __webpack_require__.f[key](chunkId, promises);
/******/ return promises;
/******/ }, []));
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/get javascript chunk filename */
/******/ (() => {
/******/ // This function allow to reference async chunks
/******/ __webpack_require__.u = (chunkId) => {
/******/ // return url for filenames based on template
/******/ return "" + chunkId + ".js";
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/global */
/******/ (() => {
/******/ __webpack_require__.g = (function() {
/******/ if (typeof globalThis === 'object') return globalThis;
/******/ try {
/******/ return this || new Function('return this')();
/******/ } catch (e) {
/******/ if (typeof window === 'object') return window;
/******/ }
/******/ })();
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/load script */
/******/ (() => {
/******/ var inProgress = {};
/******/ var dataWebpackPrefix = "webpack-demo:";
/******/ // loadScript function to load a script via script tag
/******/ __webpack_require__.l = (url, done, key, chunkId) => {
/******/ if(inProgress[url]) { inProgress[url].push(done); return; }
/******/ var script, needAttach;
/******/ if(key !== undefined) {
/******/ var scripts = document.getElementsByTagName("script");
/******/ for(var i = 0; i < scripts.length; i++) {
/******/ var s = scripts[i];
/******/ if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
/******/ }
/******/ }
/******/ if(!script) {
/******/ needAttach = true;
/******/ script = document.createElement('script');
/******/
/******/ script.charset = 'utf-8';
/******/ script.timeout = 120;
/******/ if (__webpack_require__.nc) {
/******/ script.setAttribute("nonce", __webpack_require__.nc);
/******/ }
/******/ script.setAttribute("data-webpack", dataWebpackPrefix + key);
/******/ script.src = url;
/******/ }
/******/ inProgress[url] = [done];
/******/ var onScriptComplete = (prev, event) => {
/******/ // avoid mem leaks in IE.
/******/ script.onerror = script.onload = null;
/******/ clearTimeout(timeout);
/******/ var doneFns = inProgress[url];
/******/ delete inProgress[url];
/******/ script.parentNode && script.parentNode.removeChild(script);
/******/ doneFns && doneFns.forEach((fn) => (fn(event)));
/******/ if(prev) return prev(event);
/******/ }
/******/ ;
/******/ var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
/******/ script.onerror = onScriptComplete.bind(null, script.onerror);
/******/ script.onload = onScriptComplete.bind(null, script.onload);
/******/ needAttach && document.head.appendChild(script);
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/publicPath */
/******/ (() => {
/******/ var scriptUrl;
/******/ if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + "";
/******/ var document = __webpack_require__.g.document;
/******/ if (!scriptUrl && document) {
/******/ if (document.currentScript)
/******/ scriptUrl = document.currentScript.src
/******/ if (!scriptUrl) {
/******/ var scripts = document.getElementsByTagName("script");
/******/ if(scripts.length) scriptUrl = scripts[scripts.length - 1].src
/******/ }
/******/ }
/******/ // When supporting browsers where an automatic publicPath is not supported you must specify an output.publicPath manually via configuration
/******/ // or pass an empty string ("") and set the __webpack_public_path__ variable from your code to use your own logic.
/******/ if (!scriptUrl) throw new Error("Automatic publicPath is not supported in this browser");
/******/ scriptUrl = scriptUrl.replace(/#.*$/, "").replace(/\?.*$/, "").replace(/\/[^\/]+$/, "/");
/******/ __webpack_require__.p = scriptUrl;
/******/ })();
/******/
/******/ /* webpack/runtime/jsonp chunk loading */
/******/ (() => {
/******/ // no baseURI
/******/
/******/ // object to store loaded and loading chunks
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
/******/ var installedChunks = {
/******/ "main": 0
/******/ };
/******/
/******/ __webpack_require__.f.j = (chunkId, promises) => {
/******/ // JSONP chunk loading for javascript
/******/ var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
/******/ if(installedChunkData !== 0) { // 0 means "already installed".
/******/
/******/ // a Promise means "currently loading".
/******/ if(installedChunkData) {
/******/ promises.push(installedChunkData[2]);
/******/ } else {
/******/ if(true) { // all chunks have JS
/******/ // setup Promise in chunk cache
/******/ var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));
/******/ promises.push(installedChunkData[2] = promise);
/******/
/******/ // start chunk loading
/******/ var url = __webpack_require__.p + __webpack_require__.u(chunkId);
/******/ // create error before stack unwound to get useful stacktrace later
/******/ var error = new Error();
/******/ var loadingEnded = (event) => {
/******/ if(__webpack_require__.o(installedChunks, chunkId)) {
/******/ installedChunkData = installedChunks[chunkId];
/******/ if(installedChunkData !== 0) installedChunks[chunkId] = undefined;
/******/ if(installedChunkData) {
/******/ var errorType = event && (event.type === 'load' ? 'missing' : event.type);
/******/ var realSrc = event && event.target && event.target.src;
/******/ error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
/******/ error.name = 'ChunkLoadError';
/******/ error.type = errorType;
/******/ error.request = realSrc;
/******/ installedChunkData[1](error);
/******/ }
/******/ }
/******/ };
/******/ __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
/******/ } else installedChunks[chunkId] = 0;
/******/ }
/******/ }
/******/ };
/******/
/******/ // no prefetching
/******/
/******/ // no preloaded
/******/
/******/ // no HMR
/******/
/******/ // no HMR manifest
/******/
/******/ // no on chunks loaded
/******/
/******/ // install a JSONP callback for chunk loading
/******/ var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
/******/ var [chunkIds, moreModules, runtime] = data;
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0;
/******/ for(moduleId in moreModules) {
/******/ if(__webpack_require__.o(moreModules, moduleId)) {
/******/ __webpack_require__.m[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ if(runtime) var result = runtime(__webpack_require__);
/******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
/******/ installedChunks[chunkId][0]();
/******/ }
/******/ installedChunks[chunkIds[i]] = 0;
/******/ }
/******/
/******/ }
/******/
/******/ var chunkLoadingGlobal = self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || [];
/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
/******/ })();
/******/
/************************************************************************/
var __webpack_exports__ = {};
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
__webpack_require__.e(/*! import() */ "src_utils_js").then(__webpack_require__.bind(__webpack_require__, /*! ./utils */ "./src/utils.js")).then(({add}) => {
console.log(add(1,2))
})

/******/ })()
;

dist/src/utils_js.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || []).push([["src_utils_js"],{

/***/ "./src/utils.js":
/*!**********************!*\
!*** ./src/utils.js ***!
\**********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "add": () => (/* binding */ add)
/* harmony export */ });
function add(a, b) {
return a + b;
}


/***/ })

}]);

相同的代码我们跳过,我们首先来看一下入口文件的执行代码:

1
2
3
4
5
6
var __webpack_exports__ = {};
__webpack_require__.e("src_utils_js")
.then(__webpack_require__.bind(__webpack_require__, "./src/utils.js"))
.then(({add}) => {
console.log(add(1,2))
})

这个代码主要分成三部分:

  • 第一部分执行__webpack_require__.e
  • 第二部分生成一个__webpack_require__方法并绑定参数
  • 第三部分去执行实际逻辑。

我们来看下主要核心的中间代码__webpack_require__.e:

1
2
3
4
5
6
7
8
9
10
11
12
/* webpack/runtime/ensure chunk */
(() => {
__webpack_require__.f = {};
__webpack_require__.e = (chunkId) => {
return Promise.all(
Object.keys(__webpack_require__.f).reduce((promises, key) => {
__webpack_require__.f[key](chunkId, promises);
return promises;
}, [])
);
};
})();

简单了解一下reduce

这段代码很奇怪,看上去来说实际可以视为作为一个forEach在使用。目的是试图去执行__webpack_require__.f这个对象中的所有方法,最后返回一个总的Promise

至于执行的方法,目前只有一个__webpack_require__.f.j,里面是一堆代码总之暂且放置不看,我们可以将其视为加载js文件即可(通过生成script的方式)。

我们可以将其视为加载好dist/src/utils_js.js并将该文件里声明的对象的map添加到__webpack_modules__即可。

此时使用__webpack_require__去走之前的逻辑就可以正常调用模块了。

这样就实现了代码分割。

一些动态加载的小细节

1
2
3
4
5
// main
var chunkLoadingGlobal = self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || [];

// src_utils_js.js
(self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || []).push([["src_utils_js"],{...})

通过这种命名空间方式解决了单页面多项目可能错误添加动态加载代码的问题。

1
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));

重写数组的push方法,在push时做一些额外的操作
即执行chunkLoadingGlobal.push(arg1, arg2)时。执行webpackJsonpCallback(chunkLoadingGlobal.push, arg1, arg2)这种方式。老实说我没有想到这种写法的好处,但也算一种小技巧

总结

统一module方式

webpack 将两种形式导出方式进行了一定程度上的统一,即不论写法如何,都通过__webpack_require__对模块进行引入,而对于导出的模块来说,都统一成module的样式。

区别在于esmoduledefault导出和commonjs的module.exports导出略有区别

esmoduledefault导出在生成的代码中地位和一般的export导出是一样的