diff --git a/.npmignore b/.npmignore index a5bc0942a..0f0f7d2e4 100644 --- a/.npmignore +++ b/.npmignore @@ -40,3 +40,5 @@ !dist/2.0/bi.min.css !bin/* !bin/**/* +!plugins/* +!plugins/**/* diff --git a/bin/cli/worker/cli.worker.js b/bin/cli/worker/cli.worker.js index 0c086db08..85b4507c3 100644 --- a/bin/cli/worker/cli.worker.js +++ b/bin/cli/worker/cli.worker.js @@ -1,17 +1,25 @@ const fs = require('fs'); const path = require('path'); -function scanAndCreate(structure, root = process.cwd()) { +function first2UpperCase(str) { + return str.toLowerCase().replace(/( |^)[a-z]/g, L => L.toUpperCase()); +} + +function scanAndCreate(structure, workerName, root = process.env.INIT_CWD) { Object.keys(structure) .forEach(name => { if (typeof structure[name] === 'object') { fs.mkdirSync(path.resolve(root, name)); - scanAndCreate(structure[name], path.resolve(root, `./${name}`)); + scanAndCreate(structure[name], workerName, path.resolve(root, `./${name}`)); } else if (structure[name] === '') { fs.appendFileSync(path.resolve(root, name), ''); } else if (typeof structure[name] === 'string') { - const content = fs.readFileSync(structure[name]).toString(); + let content = fs.readFileSync(structure[name]).toString(); + + content = content + .replace(/\${WorkerName}/g, first2UpperCase(workerName)) + .replace(/\${workerName}/g, workerName); fs.appendFileSync(path.resolve(root, name), content); } @@ -33,15 +41,20 @@ module.exports = { const structure = { [`${name}_worker`]: { 'main_thread': { - action: {}, - [`${name}_main_thread.ts`]: path.resolve(__dirname, './template/main_thread_template.ts'), + action: { + 'action.worker_ability_test.ts': path.resolve(__dirname, './template/main_thread/action/action.worker_ability_test.t'), + }, + [`${name}_main_thread.ts`]: path.resolve(__dirname, './template/main_thread/main_thread.t'), }, utils: { - 'action_type.ts': '', + 'action_type.ts': path.resolve(__dirname, './template/utils/action_type.t'), + 'payload_type.ts': path.resolve(__dirname, './template/utils/payload_type.t'), }, 'worker_thread': { - action: {}, - [`${name}_worker_thread.ts`]: path.resolve(__dirname, './template/worker_thread_template.ts'), + action: { + 'action.worker_ability_test.ts': path.resolve(__dirname, './template/worker_thread/action/action.worker_ability_test.t'), + }, + [`${name}_worker_thread.ts`]: path.resolve(__dirname, './template/worker_thread/worker_thread.t'), }, }, }; diff --git a/bin/cli/worker/template/action_type.ts b/bin/cli/worker/template/action_type.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/bin/cli/worker/template/main_thread/action/action.worker_ability_test.t b/bin/cli/worker/template/main_thread/action/action.worker_ability_test.t new file mode 100644 index 000000000..42425b859 --- /dev/null +++ b/bin/cli/worker/template/main_thread/action/action.worker_ability_test.t @@ -0,0 +1,13 @@ +import { WorkerAbilityTestActionType } from '../../utils/action_type'; +import { WorkerAbilityTestPayload, WorkerAbilityTestReponse } from '../../utils/payload_type'; + +export class WorkerAbilityTestMainThreadAction extends BI.Workers.WorkerBaseAction { + /** + * 通信能力检测 + */ + public communicationTest(): Promise { + const mainThreadPostTime: WorkerAbilityTestPayload['CommunicationTest'] = Date.now(); + + return this.controller.requestPromise(WorkerAbilityTestActionType.CommunicationTest, mainThreadPostTime); + } +} diff --git a/bin/cli/worker/template/main_thread/main_thread.t b/bin/cli/worker/template/main_thread/main_thread.t new file mode 100644 index 000000000..75f8fb674 --- /dev/null +++ b/bin/cli/worker/template/main_thread/main_thread.t @@ -0,0 +1,25 @@ +import { WorkerAbilityTestMainThreadAction } from './action/action.worker_ability_test'; +// 不需要一起打包的话则不需要引入这行 +import { workerUrl } from 'fui-worker!../worker_thread/${workerName}_worker_thread'; + +class ${WorkerName}MainTreadWorker extends BI.Workers.MainThreadWorker { + private communicationTest: WorkerAbilityTestMainThreadAction; + + public initActions(): void { + this.communicationTest = this.createAction(WorkerAbilityTestMainThreadAction); + } + + public testCommunication() { + return this.communicationTest.communicationTest(); + } +} + +const ${workerName}MainTreadWorker = BI.Workers.createWorker(${WorkerName}MainTreadWorker, { + workerUrl, + workerName: BI.UUID(), +}); + +${workerName}MainTreadWorker.testCommunication() + .then(v => { + console.log(v); + }); diff --git a/bin/cli/worker/template/main_thread_template.ts b/bin/cli/worker/template/main_thread_template.ts deleted file mode 100644 index 8df764495..000000000 --- a/bin/cli/worker/template/main_thread_template.ts +++ /dev/null @@ -1,5 +0,0 @@ -class CrudMainTreadWorker extends BI.Workers.MainThreadWorker { - protected initActions(): void { - // to init some actions - } -} diff --git a/bin/cli/worker/template/utils/action_type.t b/bin/cli/worker/template/utils/action_type.t new file mode 100644 index 000000000..c92de897a --- /dev/null +++ b/bin/cli/worker/template/utils/action_type.t @@ -0,0 +1,8 @@ +/* + * Worker 事务标识 + * 每类事务有命名空间, 包含多个具体事务 + */ + +export const enum WorkerAbilityTestActionType { + CommunicationTest = 'CommunicationTest', +} diff --git a/bin/cli/worker/template/utils/payload_type.t b/bin/cli/worker/template/utils/payload_type.t new file mode 100644 index 000000000..6b9a71509 --- /dev/null +++ b/bin/cli/worker/template/utils/payload_type.t @@ -0,0 +1,13 @@ +/** + * 跨线程通信各事务的发送数据类型声明 + */ +export interface WorkerAbilityTestPayload { + CommunicationTest: number; +} + +/** + * 跨线程通信各事务的响应数据类型声明 + */ +export interface WorkerAbilityTestReponse { + CommunicationTest: number; +} diff --git a/bin/cli/worker/template/worker_thread/action/action.worker_ability_test.t b/bin/cli/worker/template/worker_thread/action/action.worker_ability_test.t new file mode 100644 index 000000000..f7d1248f4 --- /dev/null +++ b/bin/cli/worker/template/worker_thread/action/action.worker_ability_test.t @@ -0,0 +1,24 @@ +import { WorkerAbilityTestActionType } from '../../utils/action_type'; +import { WorkerAbilityTestPayload, WorkerAbilityTestReponse } from '../../utils/payload_type'; + +export class WorkerAbilityTestWorkerThreadAction extends BI.Workers.WorkerBaseAction { + protected addActionHandler(): void { + this.controller.addActionHandler( + WorkerAbilityTestActionType.CommunicationTest, + this.communicationTest.bind(this) + ); + } + + /** + * 通信能力检测的处理器 + */ + private communicationTest( + payload: WorkerAbilityTestPayload['CommunicationTest'] + ): WorkerAbilityTestReponse['CommunicationTest'] { + const mainThreadPostTime = payload; + // 收到主线程信息的耗时 + const workerGetMessageDuration = Date.now() - mainThreadPostTime; + + return workerGetMessageDuration; + } +} diff --git a/bin/cli/worker/template/worker_thread/worker_thread.t b/bin/cli/worker/template/worker_thread/worker_thread.t new file mode 100644 index 000000000..24c5b91e8 --- /dev/null +++ b/bin/cli/worker/template/worker_thread/worker_thread.t @@ -0,0 +1,12 @@ +// TODO: 这边需要先import fineui资源 +import { WorkerAbilityTestWorkerThreadAction } from './action/action.worker_ability_test'; + +class ${WorkerName}WorkerTreadWorker extends BI.Workers.MainThreadWorker { + public communicationTest: WorkerAbilityTestWorkerThreadAction; + + public initActions(): void { + this.communicationTest = this.createAction(WorkerAbilityTestWorkerThreadAction); + } +} + +export const ${workerName}WorkerTreadWorker = BI.Workers.createWorker(${WorkerName}WorkerTreadWorker); diff --git a/bin/cli/worker/template/worker_thread_template.ts b/bin/cli/worker/template/worker_thread_template.ts deleted file mode 100644 index fc457c9c7..000000000 --- a/bin/cli/worker/template/worker_thread_template.ts +++ /dev/null @@ -1,5 +0,0 @@ -class CrudWorkerTreadWorker extends BI.Workers.MainThreadWorker { - protected initActions(): void { - // to init some actions - } -} diff --git a/examples/worker_new/index.js b/examples/worker_new/index.js index 67646a1ec..b95fbbad6 100644 --- a/examples/worker_new/index.js +++ b/examples/worker_new/index.js @@ -42,7 +42,7 @@ const HeartBeatCheckAction = BI.inherit(BI.Workers.WorkerBaseAction, { } }); -var WorkerThreadWorker = BI.inherit(BI.Workers.MainThreadWorker, { +var MainThreadWorker = BI.inherit(BI.Workers.MainThreadWorker, { initActions: function() { this.cookieAction = this.createAction(CookieAction); @@ -64,7 +64,7 @@ var WorkerThreadWorker = BI.inherit(BI.Workers.MainThreadWorker, { } }); -var mainThreadWorker = new WorkerThreadWorker({ +var mainThreadWorker = BI.Workers.createWorker(MainThreadWorker, { workerUrl: "./worker_new/worker.js", workerName: "demo" }); diff --git a/examples/worker_new/worker.js b/examples/worker_new/worker.js index f30856b21..96e88e542 100644 --- a/examples/worker_new/worker.js +++ b/examples/worker_new/worker.js @@ -58,7 +58,7 @@ const HeartBeatCheckAction = BI.inherit(BI.Workers.WorkerBaseAction, { } }); -var WorkerThreadWorker = BI.inherit(BI.Workers.WorkerThreadWorker, { +var MainThreadWorker = BI.inherit(BI.Workers.WorkerThreadWorker, { initActions: function() { this.cookieAction = this.createAction(CookieAction); @@ -75,6 +75,6 @@ var WorkerThreadWorker = BI.inherit(BI.Workers.WorkerThreadWorker, { } }); -var workerThreadWorker = new WorkerThreadWorker(); +var workerThreadWorker = BI.Workers.createWorker(MainThreadWorker); workerThreadWorker.fetchCookie(); diff --git a/plugins/webpack-fui-worker-plugin/constants.js b/plugins/webpack-fui-worker-plugin/constants.js new file mode 100644 index 000000000..7b80ac3dc --- /dev/null +++ b/plugins/webpack-fui-worker-plugin/constants.js @@ -0,0 +1,9 @@ +const WorkerPluginName = 'FuiWorkerPlugin'; +const WorkerLoaderName = 'FuiWorkerWorkerLoader'; +const FileNamePrefix = 'worker-'; + +module.exports = { + WorkerPluginName, + WorkerLoaderName, + FileNamePrefix, +}; diff --git a/plugins/webpack-fui-worker-plugin/index.js b/plugins/webpack-fui-worker-plugin/index.js new file mode 100644 index 000000000..19f660299 --- /dev/null +++ b/plugins/webpack-fui-worker-plugin/index.js @@ -0,0 +1,45 @@ +/* + * worker-plugin + */ + +const path = require('path'); +const webpack = require('webpack'); +const { WorkerPluginName } = require('./constants'); + +class FuiWorkerPlugin { + constructor(options = {}) { + this.options = options; + } + + apply(compiler) { + // 为主线程构建添加 __WORKER__ 环境变量, 构建中区分不同线程源码, 实现代码拆减 + compiler.hooks.afterPlugins.tap(WorkerPluginName, compiler => { + new webpack.DefinePlugin({ + // __WORKER__ 表示当前所在线程是否是 worker 线程 + // 主线程构建中为 false + __WORKER__: false, + }).apply(compiler); + }); + + // 添加自定义的worker entry-loader + compiler.hooks.afterResolvers.tap(WorkerPluginName, compiler => { + /** + * https://webpack.js.org/configuration/resolve/#resolveloader + * 使用 resolveloader 添加自定义的 worker loader + */ + if (!compiler.options.resolveLoader) { + compiler.options.resolveLoader = { + alias: {}, + }; + } + if (!compiler.options.resolveLoader.alias) { + compiler.options.resolveLoader.alias = {}; + } + + // 动态添加 worker 的 worker-loader, 命名为 "fui-worker" + compiler.options.resolveLoader.alias['fui-worker'] = path.resolve(__dirname, './worker-loader.js'); + }); + } +} + +module.exports = FuiWorkerPlugin; diff --git a/plugins/webpack-fui-worker-plugin/worker-loader.js b/plugins/webpack-fui-worker-plugin/worker-loader.js new file mode 100644 index 000000000..9affe81f4 --- /dev/null +++ b/plugins/webpack-fui-worker-plugin/worker-loader.js @@ -0,0 +1,109 @@ +/* + * fui-worker worker-loader + */ + +const webpack = require('webpack'); +const loaderUtils = require('loader-utils'); +const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin'); +const { WorkerLoaderName, FileNamePrefix } = require('./constants'); + +// 正常 loader 处理逻辑 +function loader() { + const callback = this.async(); + this.cacheable(false); + + // 过滤掉当前的 worker-loader, 保留 worker 侧构建需要的其他 loader(babel-loader/ts-loader 等) + const otherLoaders = this.loaders.filter((loader, index) => { + if (index === this.loaderIndex) { + return false; + } + + return true; + }); + /** + * 拼接构建需要的 loader 字符串, 用于指定 childCompiler 的构建 loader + * 比如: /path/to/babel-loader/lib/index.js!/path/to/ts-loader/index.js! + */ + const loaderPath = otherLoaders.reduce((pre, loader) => `${pre}${loader.path}!`, ''); + + /** + * worker 独立构建的 entry + * 构建 loader + worker 源码入口文件路径 + * + * https://webpack.js.org/concepts/loaders/#inline + * `!!` 实现在 childCompiler 中忽略其他所有 loader, 只保留主构建的 loader + * 不然 worker 入口在 childCompiler 中会继续由 worker-loader 处理, 造成死循环 + */ + const workerEntry = `!!${loaderPath}${this.resourcePath}`; + + // 把资源纳入构建流程的依赖, 实现 dev 模式下的 watch + this.addDependency(workerEntry); + + // 生成的 service 独立 bundle 名称 + const entryFileName = `${FileNamePrefix}index`; + + // 获取传递给 loader 的 options + loaderUtils.getOptions(this) || {}; + + // 创建 childCompiler, 用于实现 worker 构建为独立 js 资源 + const childCompiler = this._compilation.createChildCompiler(WorkerLoaderName, { + globalObject: 'this', + }); + childCompiler.context = this._compiler.context; + + // 指定独立构建的 entry 和生成 js 资源名称 + new SingleEntryPlugin(this.context, workerEntry, entryFileName).apply(childCompiler); + + // 设置 worker 侧的环境变量 + new webpack.DefinePlugin({ + __WORKER__: true, + }).apply(childCompiler); + + // 添加 window 全局对象, 映射为 worker 线程全局对象 self + // 如果在 worker 源码中添加, 可能没有前置到所有引用模块前 + new webpack.BannerPlugin({ + banner: 'self.window = self;', + raw: true, + entryOnly: true, + }).apply(childCompiler); + + const subCache = `subcache ${__dirname} ${workerEntry}`; + childCompiler.hooks.compilation.tap(WorkerLoaderName, compilation => { + if (compilation.cache) { + if (!compilation.cache[subCache]) compilation.cache[subCache] = {}; + compilation.cache = compilation.cache[subCache]; + } + }); + + childCompiler.runAsChild((error, entries, compilation) => { + if (!error && compilation.errors && compilation.errors.length) { + // eslint-disable-next-line no-param-reassign + error = compilation.errors[0]; + } + + // compatible with Array (v4) and Set (v5) prototypes + const entry = entries && entries[0] && entries[0].files.values().next().value; + if (!error && !entry) { + // eslint-disable-next-line no-param-reassign + error = Error(`${WorkerLoaderName}, no entry for ${workerEntry}`); + } + + if (error) { + return callback(error); + } + + return callback( + null, + // 插入代码的转译和压缩由主构建配置的 babel/ts loader 处理, 不需要 worker-worker 来处理 + // 添加 @ts-nocheck 避免 ts-check 报错 + `// @ts-nocheck + const servicePath = __webpack_public_path__ + ${JSON.stringify(entry)}; + export const workerUrl = servicePath; + ` + ); + }); + + return; +} + +module.exports = loader; diff --git a/typescript/core/worker/worker.main_thread.ts b/typescript/core/worker/worker.main_thread.ts index 42e0c18dd..c87bc9833 100644 --- a/typescript/core/worker/worker.main_thread.ts +++ b/typescript/core/worker/worker.main_thread.ts @@ -5,7 +5,7 @@ import { IWorkerOptions } from "./worker.core"; /** * 主线程Worker */ -export abstract class MainThreadWorker { +export class MainThreadWorker { /** * Worker 名称 */ @@ -24,10 +24,12 @@ export abstract class MainThreadWorker { public constructor(options: IWorkerOptions) { this.name = options.workerName; this.controller = new WorkerMainThreadController(options); - this.initActions(); } - protected abstract initActions(): void; + /** + * 初始化业务actions + */ + public initActions() {} /** * 销毁 worker 实例 diff --git a/typescript/core/worker/worker.worker_thread.ts b/typescript/core/worker/worker.worker_thread.ts index 9907955fb..53b621403 100644 --- a/typescript/core/worker/worker.worker_thread.ts +++ b/typescript/core/worker/worker.worker_thread.ts @@ -4,7 +4,7 @@ import { WorkerThreadController } from "./controller/worker.worker_thread.contro /** * worker线程实例 */ -export abstract class WorkerThreadWorker { +export class WorkerThreadWorker { /** * Worker 线程通信控制器 */ @@ -12,11 +12,12 @@ export abstract class WorkerThreadWorker { public constructor() { this.controller = new WorkerThreadController(); - - this.initActions(); } - protected abstract initActions(): void; + /** + * 初始化业务actions + */ + public initActions() {} /** * 实例化action diff --git a/typescript/core/worker/workers.ts b/typescript/core/worker/workers.ts index f02447120..5ab99dddc 100644 --- a/typescript/core/worker/workers.ts +++ b/typescript/core/worker/workers.ts @@ -1,12 +1,22 @@ import { WorkerChannel } from "./worker.channel"; import { WorkerBaseController } from "./controller/worker.controller"; -import { WorkerMessageType } from "./worker.core"; +import { IWorkerOptions, WorkerMessageType } from "./worker.core"; import { WorkerMainThreadController } from "./controller/worker.main_thread.controller"; import { WorkerThreadController } from "./controller/worker.worker_thread.controller"; import { WorkerBaseAction } from "./action/worker.action"; import { MainThreadWorker } from "./worker.main_thread"; import { WorkerThreadWorker } from "./worker.worker_thread"; +function createWorker(ThreadWorker: T, options: IWorkerOptions): InstanceType +function createWorker(ThreadWorker: T): InstanceType +function createWorker(ThreadWorker: T, options?: IWorkerOptions): InstanceType { + const threadWorker = new ThreadWorker(options as any) as InstanceType; + + threadWorker.initActions(); + + return threadWorker; +} + export const Workers = { WorkerChannel, WorkerBaseController, @@ -16,4 +26,5 @@ export const Workers = { MainThreadWorker, WorkerThreadWorker, WorkerMessageType, + createWorker, };