asp net做购物网站,如何把网站提交到百度,网站静态化 更新,织梦网站app生成器引言
大家如果使用过移动端组件库#xff08;比如#xff1a;Vant#xff09;#xff0c;会发现在网站右侧有一个手机端的预览效果。 而且这个手机端预览的内容和外面的组件代码演示是同步的#xff0c;切换组件的时候#xff0c;移动端预览的内容也会发生相应的变化。 …引言
大家如果使用过移动端组件库比如Vant会发现在网站右侧有一个手机端的预览效果。 而且这个手机端预览的内容和外面的组件代码演示是同步的切换组件的时候移动端预览的内容也会发生相应的变化。
这是怎么实现的呢我们一起来看看Vue DevUI组件库的实践吧
先看下最终实现的效果动画 1 方案选型
通过对竞品进行分析发现移动端预览都是由嵌入的iframe完成的也就是说 移动端预览是一个完整的网站。 既然要做一个完整网站在现有工程结构下想到了以下方案
1.1 最初的设想
当前文档是通过vitepress搭建的并且vitepress提供了定制主题的功能开发完成的组件可以直接在md文档中展示效果所以就想到通过在vitepress下新建一个mobile路由来包含移动端预览所需的页面。
这会有一个问题移动端预览页面不需要顶部导航栏和左侧菜单栏调研发现vitepress仅支持定制一种主题虽然貌似可以通过判断路由的方式在模板里面隐藏顶部导航栏和左侧菜单栏的方案未验证是否可行但是考虑到md毕竟是来写文档的虽然可以简单的展示组件效果但是要做一些复杂的功能比如目前需要的预览页面首页组件列表在md文档里面完成可能就不太合适了。
甚至如果以后要做的功能太复杂而无法在md文档里面完成那么当前的设计就面临着推倒重做的风险。
故排除此方案。
1.2 另一种更好的方案
最好是新起一个移动端预览的工程并且技术栈与组件库保持一致vue3vite。
这样的话组件效果可以在新工程里面直接预览并且这种建站方案成熟可以做很多事情即使以后需要做比较复杂的演示效果也不会有大问题。
故采用此方案。
2 移动端工程搭建
2.1 新工程单独出来还是整合到组件库里面
考虑到组件库应该是一个整体的工程无论是组件开发、文档输出还是效果预览都应该在一个工程下如果将移动端预览这部分单独到一个工程里面会造成维护上的困难。那如何将移动端预览整合到组件库的工程里面呢
通过在根目录新建mobile文件夹用来存放移动端预览工程相关的文件根据vite的配置还需要新建一个mobile.html入口文件并在文件里面添加
script typemodule src/mobile/main.ts/script开发环境下可以通过
yarn run app:dev启动但是vite默认将index.html文件作为入口所以在访问路径中增加/mobile.html可以访问移动端预览的页面完整访问路径为http://localhost:3001/mobile.html#/button。
2.2 路由方式优化
当前组件库工程里面已包含业务场景的工程采用的history路由如果移动端预览也采用history路由在开发阶段会和业务场景冲突故移动端预览采用的hash路由。
2.3 打包优化
根据vite多页面应用模式的配置最初将业务场景的代码和移动端预览的代码打包到一起了后面考虑到两个工程的代码会有分开部署的需求故对打包配置做了调整调整后如下
// vite.config.ts
// 业务场景代码的配置文件修改输出目录即可
export default defineConfig({// 其他配置...build: {outDir: dist-src}
})// 根目录新建vite.config.mobile.ts
// 移动端预览配置文件指定打包入口和输出目录
export default defineConfig({// 其他配置...build: {outDir: dist-mobile,rollupOptions: {input: {mobile: path.resolve(__dirname, mobile.html)}}}
})3 父子页面路由同步
3.1 场景
工程搭建方面的配置基本完成接下来就需要考虑具体功能实现了。
目前官网后面用父页面来表示通过vitepress能够正常访问移动端预览后面用子页面来表示通过vue3vite的建站方案也能够正常访问。
那两个网站的路由状态如何保持一致呢
即需要满足以下几个场景
点击父页面的菜单子页面路由需要相应修改点击子页面的菜单父页面路由需要相应修改点击浏览器前进和后退按钮父子页面路由需要同步修改点击子页面的返回按钮父子页面路由需要同步修改
3.2 父子页面通信
由于我们采用的是iframe的方案所以此功能就涉及到iframe父子传参通信通信方式分为两种同域和跨域。
简而言之同域通信就是通过访问到对应域的window对象然后进行相应的操作。
跨域通信就是通过postMessage的能力进行消息同步。
同域存在以下两个问题
由于本地开发两个工程分别启动在两个端口下面无法实现同域通信。同域通过window.location去修改路由会存在刷新页面的情况而iframe标签上的src地址是在父页面渲染时去初始化的如果刷新页面会再次初始化iframe标签上的src地址。而我们想要实现的效果是只在父页面渲染的时候初始化一次iframe标签上的src地址后续的操作iframe标签上的src地址不再变化只是内部路由变化。
故同域通信的方案被排除。
接下来就是跨域通信的方案了~
因为我们的需求是父子页面路由状态双向同步所以在父子页面均需要发送消息和接受消息。
父页面通过watch监听在路由变化时发送消息代码如下
// 父页面发送消息
watch(() route.path,(newPath) {// 根路由path为/;其他路由为/component/xxx/const pathArr newPath?.split(/);const oFrame document.querySelector(iframe);oFrame?.contentWindow?.postMessage({type: devui,value: newPath / ? / : pathArr[pathArr.length - 2],},*,);},
);父页面接受消息代码如下
// 父页面接受消息
window.addEventListener(message, function (event) {if (event.data.type event.data.type devui) {router.go(event.data.value ? /components/${event.data.value}/ : /);}
});子页面同样在路由变化时发送消息
watch(router.currentRoute, (newPath) {// 路由变更来源分为两种// parent---由父页面影响包括父页面路由变更预览页面同步切换页面首次渲染。触发watch// -------点击子页面首页的菜单时触发watch// 值为parent的情况为父页面影响预览页面无需再次通过postMessage通信const routeChangeFromParent sessionStorage.getItem(routeChangeFromParent) parent;if (routeChangeFromParent) {sessionStorage.setItem(routeChangeFromParent, );return;}window.parent.postMessage({type: devui,value: getLinkUrl(newPath.path),},*,);
});子页面接受消息代码如下
window.addEventListener(message, (event) {if (event.data.type event.data.type devui) {sessionStorage.setItem(routeChangeFromParent, parent);router.replace(event.data.value);}
});子页面返回上一页面的实现
点击返回按钮需要执行浏览器的返回操作即history.back()。此时父页面的watch监听到路由变化然后通知子页面变更路由。
3.3 遇到的问题
一、路由变更死循环
由于子页面路由变化来源有两种父页面通知和点击子页面菜单为了避免死循环子通知父变更父变更之后再次通知子变更子又通知父变更…通过sessionStorage做了标记进行判断拦截。
二、页面显示不出来 这个问题出现在页面渲染的时候。
三、选中页面文本页面错乱 这个问题出现在选中页面文本的时候。
通过对第二三个问题的分析发现是message事件被“意外”触发定位之后发现是因为某些Chrome插件(Augury和沙拉查词)触发了message事件所以增加了devui的标志对message事件来源进行过滤。
4 子页面生成
组件库支持PC端和Mobile端组件在编写文档的时候演示demo写入了文档中。
增加移动端预览的功能之后预览框中也需要演示demo并且演示demo需要和文档中的保持一致。
这时就需要维护两份演示demo。
为了降低后期维护的工作量就想到了通过自动化脚本的方式从Markdown文档中提取演示demo代码然后再输出到移动端预览工程中。
自动化脚本需要支持单个组件和全部组件转换输入all表示全部转换。
整个自动化脚本大概分为以下几步
4.1 如何获知要转换哪个组件
通过命令行的方式输入命令之后提示想要转换的组件名称组件名称需要和组件文件夹名字保持一致用来从指定文件夹下提取文档内容。
命令交互实现如下
// mobile-cli-index.js
// 通过commander创建命令
#!/usr/bin/env node
const { Command } require(commander);
const { create } require(./generate-mobile-demo);
const { version } require(../package.json);const program new Command();program.command(create).description(从md提取演示demo代码自动输出到mobile文件夹).action(create);program.parse().version(version);// mobile-inquirers.js
// 通过inquirer提供交互操作
exports.inputComponentName () ({name: name,type: input,message:必填请输入组件名字将该组件md中的演示代码提取到mobile中仅支持mobile组件all表示转换所有Mobile组件,validate: (value) {if (value.trim() ) {return 组件 name 是必填项;}return true;},
});// generate-mobile-demo.js
// 提供输入命令之后要执行的操作
const inquirer require(inquirer);
const { inputComponentName } require(./mobile-inquirers);exports.create async () {const { name } await inquirer.prompt([inputComponentName()]);if (!validateComponentName(name.toLocaleLowerCase())) {console.log(该组件不支持Mobile);process.exit(0);}if (name all) {// 通过遍历的方式对所有Mobile组件进行转换mobileComponents.forEach((value, key) {reset();generateSingleComponent(value, key);});} else {// 单个组件转换generateSingleComponent(mobileComponents.get(name), name);}
};4.2 提取Markdown内容
通过node的能力读取文档内容实现如下
const mdFileContent fs.readFileSync(path.resolve(__dirname, ../docs/components/${componentName}/index.md),utf8,
);4.3 如何从Markdown内容中提取想要的信息
目前想到两种方案可以实现
一种是用正则对字符串进行切割另一种方案是将Markdown文档转成AST结构对AST进行遍历获取到想要的数据。
因AST转换更加准确严谨具备清晰的结构化信息更加灵活
而正则表达式需要考虑太多边界情况无法有效分析上下文。
故采取AST的方案。
借用textlint/markdown-to-ast插件将Markdown文档转成AST的结构
const mdParser require(textlint/markdown-to-ast).parse;
const mdAST mdParser(mdFileContent);AST的结构如下(只截图了部分) 通过对AST遍历提取代码约定好文档中的style、script、三级标题及其后面紧跟的html或者CodeBlock为我们需要的数据。具体提取过程如下
mdAST.children.forEach(({ type, depth, lang, raw }) {if (STYLE_REG.test(raw)) {styleStr raw;} else if (SCRIPT_REG.test(raw)) {scriptStr raw;} else if (type Header depth 3) {needPickHtml true;let title raw.replace(### , );isFlow? (htmlStr div classdemo-block__title${title}/div): tabTitleArr.push(title);} else if (isPureMobile) {if (needPickHtml type CodeBlock lang lang html) {let content raw.replace(html, ).replace(, );isFlow ? (htmlStr content) : tabContentArr.push(content);needPickHtml false;}} else {if (needPickHtml type Html) {isFlow ? (htmlStr raw) : tabContentArr.push(raw);needPickHtml false;}}
});通过STYLE_REG和SCRIPT_REG识别到Markdown文档中的style和script代码。识别三级标题作为演示demo中的分类标题。因纯Mobile组件演示demo不方便直接在文档中展示故提取type CodeBlock的代码。因支持PC和Mobile的组件CodeBlock中的代码在预览框中不一定能正常展示故提取type html的代码。
4.4 拼接输出字符串
完成html、style、script代码提取之后接下来就是拼接字符串。
字符串的拼接有三种情况
一种是Markdown文档中没有script代码这种输出的代码里面需要写好script代码第二种是有script代码的情况这种需要把html、style和script的代码都拼接进去第三种是输出的Mobile不是流式布局参考预览框的Button组件而是Tab结构的布局参考预览框的PullRefresh组件这种需要对拼接的字符串做处理。
具体拼接不做过多介绍下面只展示第二种情况的代码
function createTemplate() {return ${scriptStr}templatediv classcomponent-content${htmlStr}/div/template${styleStr}
}字符串拼接完成之后就是通过node的能力输出到文件中。这个比较简单不做过多描述了直接贴代码
let fileStr createTemplate()
fs.outputFile(path.resolve(componentDir, index.vue), fileStr, utf8);4.5 移动端预览首页和路由表生成
移动端预览首页同样是自动生成的因文档左侧的菜单栏是根据docs/.vitepress/config/sidebar.ts文件中的配置生成的所以移动端预览首页也是读取的此文件中的配置生成的只不过需要做个过滤只渲染Mobile组件。
移动端预览工程的路由表同样也是通过读取docs/.vitepress/config/sidebar.ts文件中的配置生成的通过拼接字符串的形式输出到mobile/route/index.ts文件中。
5 小结
移动端预览是一个完整的网站通过iframe的方式嵌入到组件库官网中然后通过postMessage的能力实现父子页面路由同步。
预览页面通过生成AST的方式从Markdown文档中提取演示代码自动生成移动端预览的首页和路由表也同样根据组件库的菜单栏配置文件自动生成。
这样就只需要在Markdown文档中维护菜单栏和演示代码即可降低维护的工作量。