OMI 在线互动教程上线,趣味学习 Web Components

2022-08-26 12:40:54 浏览数 (1)

可以通过 omijs.org 

或者 https://tencent.github.io/omi/ 找到入口。

动机

随着 IE 浏览器离我们远去,Web Components 的在浏览器端支持率越来越高。比如主流浏览器的新版本都支持: Safari 10 , IE 11 , Chrome, Firefox 和 Edge。来自大公司基于 Web Components 的框架有 google 的 Lit、microsoft 的 fast 以及 Tencent 的 OMI 等。

OMI 框架

OMI 是前端跨框架框架,您可以使用 JSX/TSX 编写标准的 Web Components 的自定义元素(Custom Elements),通过自定义元素,Web 开发人员可以创建新的 HTML 标记,增强现有HTML标记,或者扩展其他开发人员编写的组件,然后像使用 HTML 标签一样使用他们,比如:

代码语言:javascript复制
const yourEl = document.createElement('your-omi-element')document.querySelector('your-omi-element').addEventListener('event-name', eventHandler)document.body.appendChild(yourEl)

也可以直接在 html 中使用:

代码语言:javascript复制
<your-omi-element></your-omi-element>

除了定义标准的自定义标签,您也可以使用 OMI 构建整个应用程序。

代码语言:javascript复制
import { tag, WeElement, h, render } from 'omi'import './your-omi-element'import './another-omi-element'
@tag('my-app')class HelloWorld extends WeElement {    render(props) {    return (      <>        <your-omi-element></your-omi-element>        <another-omi-element></another-omi-element>      </>    )  }}
render(<my-app />, 'body')

基于丰富的自定义元素开发应用程序,代码更精简,模块化、重用性更强。

在体验了各种前端框架的 playground 之后,OMI 团队也打算打造一款属于自己的 playground。第一版本我们直接使用了typescript playground 二次开发,最后效果如下所示:

使用下来发现有许多不便利的地方:

  • 没有文档辅助对新生还是不够友好
  • 多文件打包不支持
  • Monaco Editor 太重

所以我们打算从零开始重写 playground,使用 OMI 从零开发 OMI 互动教程站点

如下图所见是我们的第二个版本,OMI 互动教程站点本身就是使用 OMI 框架开发。使用到了 @omiu/tree@omiu/tabs@omiu/toast@omiu/icon@omiu/link 和 @omiu/toast,可以在 https://github.com/Tencent/omi 找到源代码。

你可以直接在右上角修改 TSX/TS/CSS 代码,右下角的预览界面可以实时看到执行效果。

原理

  • 整个站点技术栈 OMI OMIU,官方OMI组件 omi-router,官方OMI路由 omi-twind,Tailwind CSS 的 JS 版本 CodeMirror,代码编辑器 markdown-it prismjs,markdown 文章渲染,文章内代码高亮
  • 使用 TypeScript(browser) Rollup(browser) 在浏览器中实时编译打包
  • 使用 Vite 对站点进行启动开发和打包构建产物

下面展开详细分析下技术要点和细节。

在线编译 TypeScript

在浏览器中,直接使用 ts.transpileModule,对 TS 或 TSX 文件进行编译:

代码语言:javascript复制
import * as ts from "typescript"
export function tsBuild(code) {  return ts.transpileModule(code, {    compilerOptions: {      module: ts.ModuleKind.ESNext,      target: ts.ScriptTarget.ESNext,      jsx: ts.JsxEmit.React,      jsxFactory: 'h',      jsxFragmentFactory: 'h.f',    }  }).outputText}

在 OMI 项目中,jsxFactory 和 jsxFragmentFactory分别对应 h 和 h.f:

代码语言:javascript复制
<>  <div>hello</div></>

等同于:

代码语言:javascript复制
h(h.f, null,  h("div", null, "hello")))

TypeScript(browser) 只编译单个文件,文件之间的依赖打包我们借助于 Rollup(browser)。

在线打包模块

Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。

一般情况下,我们都使用 Rollup 对本地文件进行打包,但是我们需要的场景是在浏览器中实时打包,所以需要 虚拟文件 插件:

代码语言:javascript复制
export function vfilePlugin(files) {  return {    name: 'vfile',     resolveId(source) {      if (files[source]) {        return source       }      return null     },    load(id) {      if (files.hasOwnProperty(id)) {        return files[id]       }      return null     }  }}

以上插件将拦截虚拟模块的任何导入,而不访问本地文件系统。只需要:

代码语言:javascript复制
import { vfilePlugin } from './rollup-plugin-vfile'
const inputOptions = {  input: './index',   plugins: [vfilePlugin(files)],  // 不需要 tree shaking  treeshake: false}

支持导入 CSS String

OMI 框架使用的是 css 字符串作为组件静态样式,所以现在还剩下一件事情就是 css 字符串的导入,因为通常我们不把他和组件写在同一个文件,而是写到单独的 css 文件当中,这样书写过程中可以或者更多的提示和校正。所以这里还需要一个 rollup 插件:

代码语言:javascript复制
export function cssStringPlugin() {  return {    name: "css-string",    transform(code, id) {      if (id.endsWith('.css')) {        return {          code: `export default ${JSON.stringify(code)};`,          map: { mappings: "" }        }      }    }  }}

rollup 输入配置就变成这样:

代码语言:javascript复制
import { vfilePlugin } from './rollup-plugin-vfile'import { cssStringPlugin } from './rollup-plugin-css'
const inputOptions = {  input: './index',   plugins: [vfilePlugin(files), cssStringPlugin()],  // 不需要 tree shaking  treeshake: false}

这样就支持 TS、TSX 和 CSS 文件了。

在线执行

在线运行环境使用的是嵌入的 iframe 来执行动态脚本,因为部署在同样的域名下,所以可以直接进行 iframe 通讯传值。流程如下图所示:

iframe 里的关键代码如下:

代码语言:javascript复制
<script>  var createElement = Omi.createElement;  var define = Omi.define;  var WeElement = Omi.WeElement;  var render = Omi.render;  var h = Omi.h;  var tag = Omi.tag;  var classNames = Omi.classNames;  var extractClass = Omi.extractClass;</script><script>  var script = document.createElement("script");  script.innerHTML = parent.PreviewIframeDynamicSourceCode;  document.body.appendChild(script);</script>

通过 parent.PreviewIframeDynamicSourceCode 获取父页面构建出来的脚本进行执行,没用用户修改或者路由转换都会进行 iframe 的 reload 操作。

章节分包

随着章节的增多,再加上多语言切换,每种语言都有一份资源,构建出来的 js 越来越大,所以自然想到了分包懒加载进行处理,用户在点击某一章节的时候再加载其依赖的文档和演示资源。

在尝试了 Dynamic Import 和 Glob Import 之后,遇到了种种问题。

最开始使用的 Dynamic Import:

代码语言:javascript复制
import(`../section/${this.lan}/${section}/description.md`).then(() => {
})

在 build 模式报错: Unknown variable dynamic import

切换到 Glob Import 之后:

代码语言:javascript复制
const modules = import.meta.glob('./dir/*.js', { as: 'raw' })

上面的代码会转换成:

代码语言:javascript复制
const modules = {  './dir/foo.js': 'export default "foo"n',  './dir/bar.js': 'export default "bar"n'}

发现 as raw 方式没有进行分包。最后我们使用 fetch 方式请求资源。

代码语言:javascript复制
const texts = await Promise.all(urls.map(async url => {  const resp = await fetch(url)  return resp.text()}))

样式和跨屏适配

使用 omi-twind 写样式,您直接使用存在的功能性 class 就可以满足所有场景,而无需编写一行自定义 CSS。使用过程同样遵循 tailwindcss 的两个概念,Utility-First 和 Mobile-First:

1.使用一组受约束的功能样式来构建复杂界面

代码语言:javascript复制
<div class="p-6 max-w-sm mx-auto bg-white rounded-xl shadow-lg flex items-center space-x-4">  <div class="shrink-0">    <img class="h-12 w-12" src="/img/logo.svg" alt="ChitChat Logo">  </div></div>

一旦你用这种方式构建整个程序,会带来很多好处:

  • 你不是在浪费精力发明类名,变量取名是编程中最头疼的事情
  • 无需 CSS,也就没有了持续膨胀的 CSS
  • CSS是全局性的,对 CSS 修改时你永远不知道你在破坏什么。HTML 中的 class 是本地的,可随意修改不影响其他元素

2.所有样式作用于移动端,需要适配更大的屏幕的时候需要写类似md:xxx lg:xxx

代码语言:javascript复制
<img class="w-16 md:w-32 lg:w-48" src="...">

最后看一下我们的 tsx 代码:

在pc上的效果图:

在手机上的效果图:

最后

在线互动式教程的好处是,不会很枯燥的一直阅读文档,阅读文档的同时可以在代码编辑器里修改尝试,即时得到反馈,对 api 能够有更加深入的理解,提升学习效率。

接下来,您可以:

  • 通过该站点学习 OMI 和 Web Components
  • 通过该站点的源码学习 OMI 和 Web Components

0 人点赞