可以通过 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 标签一样使用他们,比如:
const yourEl = document.createElement('your-omi-element')document.querySelector('your-omi-element').addEventListener('event-name', eventHandler)document.body.appendChild(yourEl)
也可以直接在 html 中使用:
<your-omi-element></your-omi-element>
除了定义标准的自定义标签,您也可以使用 OMI 构建整个应用程序。
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 文件进行编译:
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
:
<> <div>hello</div></>
等同于:
h(h.f, null, h("div", null, "hello")))
TypeScript(browser) 只编译单个文件,文件之间的依赖打包我们借助于 Rollup(browser)。
在线打包模块
Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。
一般情况下,我们都使用 Rollup 对本地文件进行打包,但是我们需要的场景是在浏览器中实时打包,所以需要 虚拟文件 插件:
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 } }}
以上插件将拦截虚拟模块的任何导入,而不访问本地文件系统。只需要:
import { vfilePlugin } from './rollup-plugin-vfile'
const inputOptions = { input: './index', plugins: [vfilePlugin(files)], // 不需要 tree shaking treeshake: false}
支持导入 CSS String
OMI 框架使用的是 css 字符串作为组件静态样式,所以现在还剩下一件事情就是 css 字符串的导入,因为通常我们不把他和组件写在同一个文件,而是写到单独的 css 文件当中,这样书写过程中可以或者更多的提示和校正。所以这里还需要一个 rollup 插件:
export function cssStringPlugin() { return { name: "css-string", transform(code, id) { if (id.endsWith('.css')) { return { code: `export default ${JSON.stringify(code)};`, map: { mappings: "" } } } } }}
rollup 输入配置就变成这样:
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 里的关键代码如下:
<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
:
import(`../section/${this.lan}/${section}/description.md`).then(() => {
})
在 build 模式报错: Unknown variable dynamic import。
切换到 Glob Import 之后:
const modules = import.meta.glob('./dir/*.js', { as: 'raw' })
上面的代码会转换成:
const modules = { './dir/foo.js': 'export default "foo"n', './dir/bar.js': 'export default "bar"n'}
发现 as raw 方式没有进行分包。最后我们使用 fetch 方式请求资源。
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.使用一组受约束的功能样式来构建复杂界面
<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
<img class="w-16 md:w-32 lg:w-48" src="...">
最后看一下我们的 tsx 代码:
在pc上的效果图:
在手机上的效果图:
最后
在线互动式教程的好处是,不会很枯燥的一直阅读文档,阅读文档的同时可以在代码编辑器里修改尝试,即时得到反馈,对 api 能够有更加深入的理解,提升学习效率。
接下来,您可以:
- 通过该站点学习 OMI 和 Web Components
- 通过该站点的源码学习 OMI 和 Web Components