Featured image of post Playcanvas使用Typescript进行本地开发

Playcanvas使用Typescript进行本地开发

在前一篇文章实现了本地编辑器加远程web编辑器实现Playcanvas开发的基础上,进一步实现了在本地通过Typescipt进行Playcanvas的开发并在构建成单文件后同步到远程编辑器中。

注: 该方案为本地编辑器配合官方web编辑器的形式来进行开发,并非完全在本地开发与构建可用的playcanvas应用的方式。

虽然在上一篇文章Playcanvas本地编辑器开发方案中,我们实现了在本地的编辑器中进行Playcanvas脚本相关的开发,并同步到Web Editor中进行测试与构建的流程,并通过类型定义文件实现了第三方库的命令补全,但是javascript依旧面临着提示不足(尤其是作为对象的元素的第三方类型)以及多人合作的情况下代码越长越难维护的问题,所以我们打算尝试使用一下Typescript。在找了很久的资料之后找到了论坛以及github上"藏得很好"的两个网页:

在项目中使用Typescript

使用Typescript的话,我们的pcconfig文件会有一些变化,然后项目目录也会有一些改变,我们把ts文件都放到项目目录的src目录下,而留出一个build目录来存放最终构建成的js文件。

1
2
3
4
5
6
7
8
{
  "PLAYCANVAS_BRANCH_ID": "2c23d0c2-a31d-4349-8ef9-8f274f7cc513",
  "PLAYCANVAS_PROJECT_ID": 910870,
  "PLAYCANVAS_TARGET_DIR": "C:\\Users\\haojie\\dev\\js\\goc-multiplayer-demo\\build",
  "PLAYCANVAS_API_KEY": "bGoCwiBzaYWHodtrKBARS4ttQlBjmmmU",
  "PLAYCANVAS_BAD_FILE_REG": "^\\.|~$|jsconfig.json",
  "PLAYCANVAS_BAD_FOLDER_REG": "^\\.|typings"
}

然后我们的 package.json 文件如下

 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
{
  "name": "player-demo",
  "version": "1.0.0",
  "author": "PlayCanvas <support@playcanvas.com>",
  "homepage": "https://playcanvas.com",
  "description": "A simple TypeScript template for PlayCanvas that can also sync with your playcanvas.com project",
  "keywords": [
    "typescript",
    "3D",
    "2D",
    "VR",
    "WebGL",
    "WebGL2",
    "game",
    "engine",
    "HTML5",
    "browser",
    "typings"
  ],
  "main": "build/main.bundle.js",
  "scripts": {
    "build": "tsc",
    "watch": "tsc --watch",
    "push": "node node_modules/playcanvas-sync/pcsync.js pushAll --yes",
    "build-push": "tsc && node node_modules/playcanvas-sync/pcsync.js pushAll --yes",
    "watch-push": "tsc && tsc-watch --onSuccess \"node node_modules/playcanvas-sync/pcsync.js pushAll --yes\""
  },
  "license": "MIT",
  "devDependencies": {
    "playcanvas": "^1.51.4",
    "playcanvas-sync": "git+https://github.com/playcanvas/playcanvas-sync.git",
    "typescript": "^4.5.4"
  },
  "dependencies": {
    "tsc-watch": "^4.6.0"
  }
}

文件保存之后先 npm install 安装一下需要的包。

最后是 tsconfig.json文件,定义了我们该从哪里去读取源文件,构建之后输出到哪,叫什么名字等等

 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
{
  "compilerOptions": {
    /* Basic Options */
    // "incremental": true,                   /* Enable incremental compilation */
    "target": "ES5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
    "module": "amd", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
    "lib": [
      "DOM",
      "ES2017"
    ], /* Specify library files to be included in the compilation. */
    "allowJs": true, /* Allow javascript files to be compiled. */
    // "checkJs": true,                       /* Report errors in .js files. */
    // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
    // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
    // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
    // "sourceMap": true,                     /* Generates corresponding '.map' file. */
    "outFile": "./build/main.build.js", /* Concatenate and emit output to single file. */
    "outDir": "./build", /* Redirect output structure to the directory. */
    "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    // "composite": true,                     /* Enable project compilation */
    // "tsBuildInfoFile": "./",               /* Specify file to store incremental compilation information */
    // "removeComments": true,                /* Do not emit comments to output. */
    // "noEmit": true,                        /* Do not emit outputs. */
    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
    /* Strict Type-Checking Options */
    "strict": true, /* Enable all strict type-checking options. */
    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
    // "strictNullChecks": true,              /* Enable strict null checks. */
    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
    // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
    "strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */
    // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */
    /* Additional Checks */
    // "noUnusedLocals": false, /* Report errors on unused locals. */
    // "noUnusedParameters": true,            /* Report errors on unused parameters. */
    // "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */
    // "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */
    /* Module Resolution Options */
    // "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
    // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
    "typeRoots": [], /* List of folders to include type definitions from. */
    "types": [
      "playcanvas"
    ], /* Type declaration files to be included in compilation. */
    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
    // "allowUmdGlobalAccess": true,          /* Allow accessing UMD globals from modules. */
    /* Source Map Options */
    // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
    // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
    "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
    "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
    /* Experimental Options */
    "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
    // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
    /* Advanced Options */
    "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
  }
}

之后我们便可以尝试 npm run build-push 构建,并将生成的js文件上传到服务器上了,注意此时所有的js文件都打包到了main.build.js中(尽管此时我们的代码还全是js文件,也会被打包),push成功之后我们还需要去playcanvas的面板中手动的点一下脚本文件的parse按钮去读取里面有哪些脚本实体。 之后我们每次添加新的 pc.ScriptTypes 或者脚本的参数(attributes)时,都需要手动的去点一下parse按钮

和Javascript的差异

查看上面的package.json,我们可以发现一个特点——这些npm的命令中没有 diff/pull 等命令了,因为使用Typescript来开发和Javascript开发有一个最大的不同——这些脚本源文件(js或ts),都不再存放在playcanvas中,playcanvas中只保有构建后的一个单独的文件,我们本地的脚本文件彻底交由git来进行管理即可,而下面这几条命令也可以实现我们的同步需求:

1
2
3
4
5
    "build": "tsc",
    "watch": "tsc --watch",
    "push": "node node_modules/playcanvas-sync/pcsync.js pushAll --yes",
    "build-push": "tsc && node node_modules/playcanvas-sync/pcsync.js pushAll --yes",
    "watch-push": "tsc && tsc-watch --onSuccess \"node node_modules/playcanvas-sync/pcsync.js pushAll --yes\""

另外注意一点,如果项目原来使用了js,那么给playcanvas增加物理引擎支持的ammo.js这个文件不要放在src目录下,不然会出错,这个文件还是当作原有的js文件来处理就好了。,然后由于Playcanvas使用的是AMD模块而非ES6 Modules,所以我们在使用其api时需要指定类型定义文件,我们可以在tsconfig里面指定 "typeRoots": ["typings",],,然后找到对应的库的类型定义文件,并把 .d.ts 类型定义文件都放到typings目录下,或者干脆直接npm install --save-dev 对应的模块,尽管我们并不会直接导入这些模块,但是能用上他们提供的定义文件来进行代码提示(前提是这个库的d.ts模块要支持作为全局库使用)。

webpack进行打包

该方案还是存在问题,因为如果要用node modules中的第三方包的话,我们导入包的方式就变成了 es6 module,而这样的话就不能直接在代码中使用 pc.createScript 这样的写法了(AMD MODULE),而是同样需要通过import的形式进行导入,这时webpack会把playcanvas也给打包到目标文件中,这样似乎和我们的需求有一些不同?这种方式貌似更贴近于纯本地去构建Playcanvas项目了,并且还出现了一些bug,所以感觉按照我现在的配置还是不太行的通,但是感觉这条路还是有办法走通的,不过由于我目前对Typescript的模块以及Playcanvas的构建机制不太了解,所以暂时不采用这个方案。(直接用tsc也可以直接把第三方的js文件给打包到最终的文件中,继续用amd模块就好)

由于Playcanvas不能直接编辑html去引入一些第三方的js文件,所以想要使用第三方的js时就有一些问题,我们本来采用的方案是直接用js去操纵DOM树,动态创建一个script元素并设置src然后挂载到DOM上,而我们现在既然使用了tsc将多个js/ts文件打包成了一个文件,那么我想着能不能更进一步,使用webpack将来源于node_modules里面的第三方模块也打包到最终的文件中。

之前的方案如下:

1
2
3
  let script = document.createElement('script');
  script.src = "https://download.agora.io/sdk/release/AgoraRTC_N.js";
  document.getElementsByTagName('head')[0].appendChild(script);

参考了下面几篇文章:

先安装webpack,npm i --save-dev webpack webpack-cli,然后编辑我们的tsconfig.json,这里我修改了module,改为了 ES2015(ES6),以及开启了sourceMap,以及moduleResolution设置为node。

 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
{
  "compilerOptions": {
    /* Basic Options */
    // "incremental": true,                   /* Enable incremental compilation */
    "target": "ES5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
    "module": "ES2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
    "lib": [
      "DOM",
      "ES2017"
    ], /* Specify library files to be included in the compilation. */
    "allowJs": true, /* Allow javascript files to be compiled. */
    // "checkJs": true,                       /* Report errors in .js files. */
    // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
    // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
    // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
    "sourceMap": true, /* Generates corresponding '.map' file. */
    // "outFile": "./build/main.build.js", /* Concatenate and emit output to single file. */
    "outDir": "./build/", /* Redirect output structure to the directory. */
    "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    // "composite": true,                     /* Enable project compilation */
    // "tsBuildInfoFile": "./",               /* Specify file to store incremental compilation information */
    // "removeComments": true,                /* Do not emit comments to output. */
    // "noEmit": true,                        /* Do not emit outputs. */
    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
    /* Strict Type-Checking Options */
    "strict": true, /* Enable all strict type-checking options. */
    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
    // "strictNullChecks": true,              /* Enable strict null checks. */
    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
    // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
    "strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */
    // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */
    /* Additional Checks */
    // "noUnusedLocals": false, /* Report errors on unused locals. */
    // "noUnusedParameters": true,            /* Report errors on unused parameters. */
    // "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */
    // "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */
    /* Module Resolution Options */
    "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
    // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
    // "typeRoots": [], /* List of folders to include type definitions from. */
    "types": [
      "playcanvas"
    ], /* Type declaration files to be included in compilation. */
    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
    // "allowUmdGlobalAccess": true,          /* Allow accessing UMD globals from modules. */
    /* Source Map Options */
    // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
    // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
    "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
    /* Experimental Options */
    "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
    // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
    /* Advanced Options */
    "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
  }
}

然后编辑webpack的配置文件 webpack.config.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
const path = require('path');
const exec = require('child_process').exec;

module.exports = {
  entry: './src/index.ts',
  devtool: 'inline-source-map',
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  output: {
    filename: 'main.build.js',
    path: path.resolve(__dirname, 'build'),
  },
  plugins: [
    {
      apply: (compiler) => {
        compiler.hooks.afterEmit.tap('AfterEmitPlugin', (compilation) => {
          exec('node node_modules/playcanvas-sync/pcsync.js pushAll --yes', (err, stdout, stderr) => {
            if (stdout) process.stdout.write(stdout);
            if (stderr) process.stderr.write(stderr);
          });
        });
      }
    }
  ]
};

这里额外提一点,我们的原项目是多个js文件组成的项目,并且没有一个公共的入口(都是全局作用域)。而webpack需要指定一个或多个文件作为入口,分析模块的调用,最终进行打包,所以我们这里新建了一个index.ts文件并作为入口,内容如下:

1
2
3
4
5
6
7
// 在这里导入是给webpack使用的
import 'agora-rtc-sdk'
import './Network'
import './CameraMovement'
import './glb-utils'
import './Loader'
import './PlayerMovement'

还有就是 webpack 不像 tsc 有一个 –onSuccess 的回调可以用来执行pcsync命令,所以我这里参考了 run-command-after-webpack-build 中的解决方案,以plugins的形式添加了一个插件,在每次webpack build之后同步我们的代码,最后将package.json中的对应的命令由tsc替换为webpack即可。

1
2
    "build": "webpack",
    "watch": "webpack --watch",

由于我们每次构建都会自动push,所以之前的build-push啥的都不需要了。

Licensed under CC BY-NC-SA 4.0
最后更新于 Apr 27, 2022 23:57 +0800
comments powered by Disqus
本站访客数:
Built with Hugo
主题 StackJimmy 设计