Omi 多端開發之 - omip 適配 h5 原理揭祕
Omi 框架 是騰訊開源的下一代前端框架,提供桌面、移動和小程式整體解決方案(One framework. Mobile & Desktop & Mini Program), Omip 是 Omi 團隊開發的跨端開發工具集,支援小程式和 H5 SPA,最新的 omip 已經適配了 h5,如下方新增的兩條命令:
npm i omi-cli -g omi init-p my-app cd my-app npm start//開發小程式 npm run dev:h5//開發 h5 npm run build:h5 //釋出 h5 複製程式碼
node 版本要求 >= 8
也支援一條命令 npx omi-cli init-p my-app
(npm v5.2.0+)
當然也支援 TypeScript:
omi init-p-ts my-app 複製程式碼
TypeScript 的其他命令和上面一樣,也支援小程式和 h5 SPA 開發。
開發預覽

特性包括:
- 一次學習,多處開發,一次開發,多處執行
- 使用 JSX,表達能力和程式設計體驗大於模板
- 支援使用 npm/yarn 安裝管理第三方依賴
- 支援使用 ES6+,ES2015+,TypeScript
- 支援使用 CSS 預編譯器
- 小程式 API 優化,非同步 API Promise 化
- 超輕量的依賴包,順從小程式標籤和元件的設計
- webpack、熱載入、sass、less等你要的都有
Omip 不僅可以一鍵生成小程式,還能一鍵生成 h5 SPA。怎麼做到的?下面來一一列舉難點,逐個擊破。
問題列表
- CSS rpx 轉換問題
- app.css 作用域問題
- JSX 裡的小程式標籤對映
- wx api 適配
- 整合路由
CSS rpx 轉換問題
小程式擴充套件尺寸單位 rpx(responsive pixel): 可以根據螢幕寬度進行自適應。規定螢幕寬為750rpx。如在 iPhone6 上,螢幕寬度為375px,共有750個物理畫素,則750rpx = 375px = 750物理畫素,1rpx = 0.5px = 1物理畫素。
這個特性大受好評,製作響應式網站非常有用。因為瀏覽器是不支援 rpx 單位,所以需要 執行時 轉換,剛好 omi 內建了這個函式:
function rpx(str) { return str.replace(/([1-9]\d*|0)(\.\d*)*rpx/g, (a, b) => { return (window.innerWidth * Number(b)) / 750 + 'px' }) } 複製程式碼
從 rpx 原始碼可以看到,需要執行時轉換 rpx,而非編譯時!因為只有執行時能拿到 螢幕寬度,omi 早期版本已經支援執行時的 rpx 轉換:
import { WeElement, define, rpx } from 'omi' define('my-ele', class extends WeElement { static css = rpx(`div { font-size: 375rpx }`) render() { return ( <div>my ele</div> ) } }) 複製程式碼
app.css 作用域問題
小程式 Shadow tree 與 omi 有一點點不一樣,omi 是從根開始 shadow root,而小程式是從自定義元件開始,omio 則沒有 shadow root。
Omi | Omio | 小程式 | |
---|---|---|---|
Shadow DOM | 從根節點開始 | 無 | 從自定義元件開始 |
Scoped CSS | 從根節點開始區域性作用域,瀏覽器 scoped | 從根節點開始區域性作用域(執行時 scoped) | 自定義元件區域性作用域 |
所以,app.css 需要汙染到 page 裡的 WXML/JSX,但在 omi 和 omio 中樣式都是隔離的, 需要怎麼做才能突破隔離?先看 app.js 原始碼:
import './app.css' //注意這行!!! import './pages/index/index' import { render, WeElement, define } from 'omi' define('my-app', class extends WeElement { config = { pages: [ 'pages/index/index', 'pages/list/index', 'pages/detail/index', 'pages/logs/index' ], window: { backgroundTextStyle: 'light', navigationBarBackgroundColor: '#fff', navigationBarTitleText: 'WeChat', navigationBarTextStyle: 'black' } 複製程式碼
上面是使用 omip 開發小程式的入口 js 檔案,也是 webpack 編譯的入口檔案,在 cli 進行語法樹分析的時候,可以拿到 import 的各個細節,然後做一些變換處理,比如下面 ImportDeclaration(即 import 語句) 的處理:
traverse(ast, { ImportDeclaration: { enter (astPath) { const node = astPath.node const source = node.source const specifiers = node.specifiers let value = source.value //當 app.js 裡 import 的檔案是以 .css 結尾的時候 if(value.endsWith('.css')){ //讀取對應 js 目錄的 css 檔案,移除 css 當中的註釋,儲存到 appCSS 變數中 appCSS = fs.readFileSync(filePath.replace('.js','.css'), 'utf-8').replace(/\/\*[^*]*\*+([^/][^*]*\*+)*\//g, '') //移除這裡條 import 語句 astPath.remove() return } 複製程式碼
得到了 appCSS 之後,想辦法注入到所有 page 當中:
traverse(ast, { ImportDeclaration: { enter (astPath) { const node = astPath.node const source = node.source let value = source.value const specifiers = node.specifiers //當 import 的檔案是以 .css 結尾的時候 if(value.endsWith('.css')){ //讀取對應 js 目錄的 css 檔案,移除 css 當中的註釋,儲存到 css 變數中 let css = fs.readFileSync(filePath.replace('.js','.css'), 'utf-8').replace(/\/\*[^*]*\*+([^/][^*]*\*+)*\//g, '') //page 注入 appCSS if(filePath.indexOf('/src/pages/') !== -1||filePath.indexOf('\\src\\pages\\') !== -1){ css = appCSS + css } //把 import 語句替換成 const ___css = Omi.rpx(.....) 的形式! astPath.replaceWith(t.variableDeclaration('const',[t.variableDeclarator(t.identifier(`___css`),t.callExpression(t.identifier('Omi.rpx'),[t.stringLiteral(css)]),)])) return } ... 複製程式碼
這就夠了嗎?不夠!因為 ___css 並沒有使用到,需要注入到 WeElement Class 的靜態屬性 css 上,繼續 ast transformation:
const programExitVisitor = { ClassBody: { exit (astPath) { //注入靜態屬性 const css = ___css astPath.unshiftContainer('body', t.classProperty( t.identifier('static css'), t.identifier('___css') )) } } } 複製程式碼
編譯出得 page 長這個樣子:
import { WeElement, define } from "../../libs/omip-h5/omi.esm"; const ___css = Omi.rpx("\n.container {\nheight: 100%;\ndisplay: flex;\nflex-direction: column;\nalign-items: center;\njustify-content: space-between;\npadding: 200rpx 0;\nbox-sizing: border-box;\n} \n\n.userinfo {\ndisplay: flex;\nflex-direction: column;\nalign-items: center;\n}\n\n.userinfo-avatar {\nwidth: 128rpx;\nheight: 128rpx;\nmargin: 20rpx;\nborder-radius: 50%;\n}\n\n.userinfo-nickname {\ncolor: #aaa;\n}\n\n.usermotto {\nmargin-top: 200px;\n}"); const app = getApp(); define('page-index', class extends WeElement { static css = ___css; data = { motto: 'Hello Omip', userInfo: {}, hasUserInfo: false, canIUse: wx.canIUse('button.open-type.getUserInfo') ... ... 複製程式碼
大功告成!
標籤對映
由於小程式裡的一些標籤在瀏覽器中不能夠識別,比如瀏覽器不識別 view、text 等標籤,需要轉換成瀏覽器識別的標籤,所以這裡列了一個對映表:
const mapTag = { 'view': 'div', 'picker': 'select', 'image': 'img', 'navigator': 'a', 'text': 'span' } const getNodeName = function(name){ if(mapTag[name]) return mapTag[name] return name } 複製程式碼
在 h
函式建立虛擬 dom 的時候進行 getNodeName
:
function h(nodeName, attributes) { ... ... var p = new VNode(); p.nodeName = getNodeName(nodeName); p.children = children; p.attributes = attributes == null ? undefined : attributes; p.key = attributes == null ? undefined : attributes.key; ... ... return p; } 複製程式碼
這裡還有遺留問題,比如內建的一些原生元件如:
- scroll-view
- movable-view
- cover-view
- cover-image
- rich-text
- picker-view
- functional-page-navigator
- live-player
- live-pusher
這些元件如果你需要開發 h5,就別用上面這些元件。如果一定要使用上面的元件,那麼請使用 omi 先實現上面的元件。
wx api 適配
這裡需要注意的是,不是所有 api 都能適配,只能適配一部分:
wx | web |
---|---|
wx.request | XMLHttpRequest |
介面 api(confirm、loaing、toast等) | 實現對應的omi元件 |
資料儲存 api | localStorage |
wx 特有的 api 還包括一些特有的生命週期函式,如:
- onShow
- onHide
這是 wx 裡 Page 裡的生命週期,而 omi 是不包含的。這裡需要在 router 的回撥函式中進行主動呼叫。具體怎麼出發且看路由管理。
整合路由
先看 cli 編譯出來的 app.js 路由部分:
render() { return <o-router mode={"hash"} publicPath={"/"} routes={[{ path: '/pages/index/index', componentLoader: () => import( /* webpackChunkName: "index_index" */'./pages/index/index'), isIndex: true }, { path: '/pages/list/index', componentLoader: () => import( /* webpackChunkName: "list_index" */'./pages/list/index'), isIndex: false }, { path: '/pages/detail/index', componentLoader: () => import( /* webpackChunkName: "detail_index" */'./pages/detail/index'), isIndex: false }, { path: '/pages/logs/index', componentLoader: () => import( /* webpackChunkName: "logs_index" */'./pages/logs/index'), isIndex: false }]} customRoutes={{}} basename={"/"} />; } }); render(<my-app />, '#app'); 複製程式碼
4個頁面各自做了分包,這樣可以加快首屏節省頻寬按需載入。接下來看 <o-router />
的實現:
import { WeElement, define, render } from "../omip-h5/omi.esm"; import 'omi-router'; let currentPage = null; let stackList = []; define('o-router', class extends WeElement { _firstTime = true; installed() { ... ... } }); export function routeUpdate(vnode, selector, byNative, root) { ... ... } window.onscroll = function () { ... ... }; 複製程式碼
具體實現細節可以去看 o-router 原始碼 ,主要實現了下面一些功能:
- 依賴了 omi-router 進行路由變更的監聽(hash change)
- 依賴 window.onscroll 記錄了上一 page 的滾動位置,方便在回退時候還原滾動條位置
- 記錄了 page 容器的 display,不能無腦 display none 和 display block 切換,因為可能是 display flex 等
- 依靠 omi-router 判斷是否是系統後退行為
- 在正確的時機觸發頁面的 onShow 和 onHide
- 新開頁面 scrollTop 重製為 0
- wx.navigateTo 直接呼叫 omi-router 的 route 方法