返回顶部
首页 > 资讯 > 精选 >使用JavaScript怎么实现一个单文件组件
  • 839
分享到

使用JavaScript怎么实现一个单文件组件

2023-06-14 23:06:32 839人浏览 安东尼
摘要

使用javascript怎么实现一个单文件组件?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。JavaScript是什么JavaScript是一种直译式的脚本语言

使用javascript怎么实现一个单文件组件?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。

JavaScript是什么

JavaScript是一种直译式的脚本语言,其解释器被称为JavaScript引擎,是浏览器的一部分,JavaScript是被广泛用于客户端的脚本语言,最早是在html网页上使用,用来给HTML网页增加动态功能。

单文件组件

知道“渐进增强”这个概念的前端开发人员想必也听说过“分层”这个概念。在组件中,同样有这样的概念。事实上,每个组件至少有3层,甚至多余3层:内容/模板,表现和行为。又或者保守的说,每个组件会被分成至少3个文件,比如:一个按钮组件的文件结构可能是下面这样的:

Button/
| -- Button.html
| -- Button.CSS
| -- Button.js

采用这种方式分层相当于技术的分离(内容/模板:使用html,表现:使用css,行为:使用JavaScript)。如果没有采用任何构建工具打包,这意味着浏览器需要获取这3个文件。因此,一个想法是:迫切需要一种分离组件代码而不分离技术(文件)的技术来解决这个问题。这就是这篇文章要讨论的主题—单文件组件。

总的来说,我对“技术分层”持怀疑态度。它来自一个事实,就是组件分层常常因为绕不开“技术分层”而被放弃,而这两者是完全分离的。

回到主题,用单文件组件实现按钮可能是这样的:

<template>  <!-- Button.html contents Go here. --></template><style>  </style><script>  // Button.js contents go here.</script>

可以看到这个单文件组件很像最初前端开发中的html文档,它有自己的style标签和script标签,只是表现层使用一个template标签。由于使用了简单的方式,得到一个强大的分层组件(内容/模板:<template>,表现:<style>,行为:<script>),而不需要使用3个分离的文件。

基本概念

首先,我们创建一个全局函数loadComponent()来加载组件。

window.loadComponent = (function() {  function loadComponent( URL ) {}  return loadComponent;}());

这里使用了JavaScript模块模式。它允许定义所有必要的辅助函数,但是只向外公开loadComponent()函数。当然,现在这个函数还是空的。

后面,我们要创建一个<hello-world>组件,显式下面的内容。

Hello, world! My name is<given name>.

另外,点击这个组件,弹出一个信息:

Don't touch me!

组件代码保存为一个文件HelloWorld.wc(这里.wc代表WEBComponent)。初始代码如下:

<template>  <div class="hello">    <p>Hello, world! My name is <slot></slot>.</p>  </div></template><style>  div {    background: red;    border-radius: 30px;    padding: 20px;    font-size: 20px;    text-align: center;    width: 300px;    margin: 0 auto;  }</style><script></script>

目前,没有给组件添加任何行为,只是定义了模板和样式。模板中,可以使用常见的html标签,比如<div>,另外,template中出现了<slot>元素表明组件将实现影子DOM。并且默认情况下这个DOM自身所有样式和模板都隐藏在这个DOM中。

组件在网页中使用的方式非常简单。

<hello-world>Comandeer</hello-world><script src="loader.js"></script><script>  loadComponent( 'HelloWorld.wc' );</script>

可以像标准的自定义元素一样使用组件。唯一的区别是需要在使用loadComponent()方法前先加载它(这个方法放在loader.js中)。loadComponent()方法完成所有繁重的工作,比如获取组件,并通过customElements.define()注册它。

在了解了所有概念之后,是时候动手实践了。

简单的loader

如果想从外部文件中加载文件,需要使用万能的ajax。但是现在已经是2020年了,在大部分浏览器中,你可以大胆的使用Fetchapi

function loadComponent( URL ) {  return fetch( URL );}

但是,这只是获取到文件,没有对文件做任何处理,接下来要做的是把ajax返回内容转换成text文本,如下:

function loadComponent( URL ) {  return fetch( URL ).then( ( response ) => {    return response.text();  } );}

由于loadComponent()函数返回的是fetch函数的执行结果,所以它是一个Promise对象。可以在then方法中检查文件(HelloWorld.wc)是不是真的被加载,还有,是不是被转成文本了:

loadComponent('HelloWorld.wc').then((component) => {    console.log(component);});

运行结果如下:

使用JavaScript怎么实现一个单文件组件

chrome浏览器下,使用console()方法,我们看到HelloWorld.wc的内容被转成了text并输出,所以貌似行得通!

解析组件内容

然而,仅仅把文本输出并没有达到我们的目的。最终要把它转换成DOM用于显示,并能和用户真正交互起来。

在浏览器环境中有一个非常实用的类DOMParser,可以实用它创建一个DOM解析器。实例化一个DOMParser类得到一个对象,实用这个对象可以将组件文本转换成DOM:

window.loadComponent = (function () {    function loadComponent(URL) {        return fetch(URL).then((response) => {            return response.text();        }).then((html) => {            const parser = new DOMParser(); // 1            return parser.parseFromString(html, 'text/html'); // 2        });    }    return loadComponent;}());

首先,创建一个DOMParser实例parser(1),然后用这个实例将组件内容转化成DOM(2)。值得注意的是,这里实用的是HTML模式(‘text/html')。如果希望代码更好的符合JSX标准或者原始的vue.js组件,可以实用XML模式(‘text/XML')。但是,在这种情况下,需要更改组件本身的结构(例如,添加可以容纳其他元素的主元素)。

这是再输出loadComponent()函数的返回结果,就是一个DOM树了。

使用JavaScript怎么实现一个单文件组件

在chrome浏览器下,console.log()输出解析后的HelloWorld.wc文件,是一个DOM树。

注意,parser.parseFromString方法自动给组件添加上<html>,<head>,<body>标签元素。这是由HTML解析器的工作原理造成的。在HTML LS规范中详细描述了构建DOM树的算法。这篇文章很长,要花点时间阅读,可以简单地理解为解析器会默认把所有内容放在<head>元素中,直至遇到一个只能放在<body>标签内的DOM元素。所以,组件代码中所有的元素(<element>,<style>,<script>)都允许放在<head>中。如果在<template>外层包一个<p>元素,那么解析器将把它放在<body>中。

还有一个问题,组件被解析之后并没有<!DOCTYPE html>声明,所以这得到的是一个非正常的html文档,因此浏览器会使用一种被成为怪异模式的方式来渲染这种html文档。所幸的是,在这里它不会带来任何负面作用,因为这里只使用DOM解析器将组件分割成适当的部分。

有了DOM树之后,可以只截取我们需要的部分。

return fetch(URL).then((response) => {    return response.text();}).then((html) => {    const parser = new DOMParser();    const document = parser.parseFromString(html, 'text/html');    const head = document.head;    const template = head.querySelector('template');    const style = head.querySelector('style');    const script = head.querySelector('script');    return {        template,        style,        script    };});

最后整理一下代码,loadComponent方法如下。

window.loadComponent = (function () {    function fetchAndParse(URL) {        return fetch(URL).then((response) => {            return response.text();        }).then((html) => {            const parser = new DOMParser();            const document = parser.parseFromString(html, 'text/html');            const head = document.head;            const template = head.querySelector('template');            const style = head.querySelector('style');            const script = head.querySelector('script');            return {                template,                style,                script            };        });    }    function loadComponent(URL) {        return fetchAndParse(URL);    }    return loadComponent;}());

从外部文件中获取组件代码的方式不止FetchAPI这一种,XMLHttpRequest有一个专用的文档模式,允许您省略整个解析步骤。但是XMLHttpRequest返回的不是一个Promise,这个需要自己包装。

注册组件

现在有了组件的层,可以创建reGISterComponent()方法来注册新的自定义组件了。

window.loadComponent = (function () {    function fetchAndParse(URL) {        […]    }    function registerComponent() {    }    function loadComponent(URL) {        return fetchAndParse(URL).then(registerComponent);    }    return loadComponent;}());

要注意的是,自定义组件必须是一个继承自HTMLElement的类。此外,每个组件都将使用用于存储样式和模板内容的影子DOM。所以每个引用这个组件的场合下,这个组件都有相同的样式。方法如下:

function registerComponent({template, style, script}) {    class UnityComponent extends HTMLElement {        connectedCallback() {            this._upcast();        }        _upcast() {            const shadow = this.attachShadow({mode: 'open'});            shadow.appendChild(style.clonenode(true));            shadow.appendChild(document.importNode(template.content, true));        }    }}

应该在registerComponent()方法内创建UnityComponent类,因为这个类要使用传入registerComponent()的参数。这个类会使用一种稍加修改的机制来实现影子DOM,在这篇关于影子DOM的文章(波兰文)中我有详细介绍。

关于注册组件,现在只剩下一件事,给单文件组件一个名字,并把它加到当前页面的DOM中。

function registerComponent( { template, style, script } ) {  class UnityComponent extends HTMLElement {    [...]  }  return customElements.define( 'hello-world', UnityComponent );}

现在可以打开看一下了,如下:

使用JavaScript怎么实现一个单文件组件

在chrome中,这个按钮组件中,有一个红色矩形,文字内容:Hello, world! My name is Comandeer。

获取脚本内容

现在一个简单的按钮组件已经实现。现在要实现最困难的部分,添加行为层,并自定义按钮内内容。在上面的步骤中,我们应该使用到按钮的地方传入内容,而不是在组件代码中硬编码按钮内的文字内容。同理还要处理组件内绑定的事件监听,这里我们使用类似Vue.js的约定,如下:

<template>  […]</template><style>  […]</style><script>  export default { // 1    name: 'hello-world', // 2    onClick() { // 3      alert( `Don't touch me!` );    }  }</script>

可以假设组件内<script>标签中的内容是一个JavaScript模块,它导出内容(1)。模块导出的对象包含组件的名称(2)和一个已“on..”开头的事件监听方法(3)。

这看上去很工整,没有内容暴露在模块外部(因为JavaScript中modules并不是在全局作用域中)。这里有一个问题:没有一个标准可以处理从内部模块导出的对象(这些代码直接定义在HTML文档中)。import语句会假设获取到一个模块标识,根据这个标识导入。最常见的是从一个包含代码的文件的URL路径。组件不是一个js文件,没有这样一个标识,内部的模块是没有这样的标识的。

在缴械投降之前,可以使用跟一个超级脏的hack。最少有2中方式让浏览器像处理一个文件一样处理一段文本:DataURI和ObjectURI。也有一些建议是使用ServiceWorker。但是在这里显得有点大材小用。

DataURI和ObjectURI

DataURI是一个古老,原始的方法。它的基础是将文件内容转换成URL,去掉不必要的空格,然后使用Base64对所有内容进行编码。假设有一个JavaScript文件,内容如下:

export default true;

转换成DataURI如下:

data:application/javascript;base64,ZXhwb3J0IGRlZmF1bHQgdHJ1ZTs=

然后,可以像引入一个文件一样引入这个URI:

import test from 'data:application/javascript;base64,ZXhwb3J0IGRlZmF1bHQgdHJ1ZTs=';console.log( test );

DataURI这种方式的一个明显的缺点是随着JavaScript文件内容增多,这个URL的长度会随之变得很长。还有把二进制数据放在DataURI非常困难。

所以,现在有一种新的ObjectURI。它是从几种标准中衍生出来的,包括FileAPI和HTML5中的<video>和<audio>标签。ObjectURI的目的很简单,从给定的二进制数据创建一个“伪文件”,在当前上下文中给出一个唯一URI。简单点说,就是在内存中创建一个有唯一名称的文件。ObjectURI有DataURI所有的优点(一种创建"文件"的方法),而没它的缺点(即使文件有100M也没关系)。

对象uri通常是从多媒体流(例如在<video>或<audio>上下文中)或通过输入[type=file]和拖放机制发送的文件创建的。还可以使用File,Blob这两个类手动创建。在本例中,我们使用Bolb,先把内容放在模块中,然后转换成ObjectURI:

const myJSFile = new Blob( [ 'export default true;' ], { type: 'application/javascript' } );const myJSURL = URL.createObjectURL( myJSFile );console.log( myJSURL ); // blob:https://blog.comandeer.pl/8e8fbd73-5505-470d-a797-dfb06ca71333

动态导入

不过,还有一个问题:import语句不接受变量作为模块标识符。这意味着除了使用该方法将模块转换成“文件”之外,还是无法导入它。还是无解的吗?

也不尽然。这个问题在很久之前就被提出了,使用动态导入机制可以解决。它是ES2020标准的一部分,并且在Firefox,Safari和node.js13.x中已经被实现。使用一个变量作为要动态导入的模块的标示符已经不再是一个难题了:

const myJSFile = new Blob( [ 'export default true;' ], { type: 'application/javascript' } );const myJSURL = URL.createObjectURL( myJSFile );import( myJSURL ).then( ( module ) => {  console.log( module.default ); // true});

从上面代码中可以看到,import()命令可以像方法一样使用,它返回一个Promise对象,then方法中得到模块对象。在它的default属性中包含了模块中定义的所有导出对象。

实现

现在我们已经知道思路了,现在可以着手实现它。在添加一个工具方法,getSetting()。在registerComponents()方法之前调用它,进而从脚本代码中获取所有信息。

function getSettings( { template, style, script } ) {  return {    template,    style,    script  };}[...]function loadComponent( URL ) {  return fetchAndParse( URL ).then( getSettings ).then( registerComponent );}

现在,这个方法返回了所有传入的参数。按照上面介绍的逻辑,将脚本代码转换成ObjectURI:

const jsFile = new Blob( [ script.textContent ], { type: 'application/javascript' } );const jsURL = URL.createObjectURL( jsFile );

下一步,使用import加载模块,返回模板,样式和组件的名称:

return import( jsURL ).then( ( module ) => {  return {    name: module.default.name,    template,    style  }} );

由于这个原因,registerComponent()仍然获得3个参数,但是现在它获取的是name,而不是脚本。正确的代码如下:

function registerComponent( { template, style, name } ) {  class UnityComponent extends HTMLElement {    [...]  }  return customElements.define( name, UnityComponent );}

行为层

组件还剩下最后一层:行为层,用来处理事件。现在我们只是在getSettings()方法中获取到了组件的名字,还要获取事件监听。可以使用Object.entrie()方法获取。在getSettings()方法中添加合适的代码:

function getSettings( { template, style, script } ) {  [...]  function getListeners( settings ) { // 1    const listeners = {};    Object.entries( settings ).forEach( ( [ setting, value ] ) => { // 3      if ( setting.startsWith( 'on' ) ) { // 4        listeners[ setting[ 2 ].toLowerCase() + setting.substr( 3 ) ] = value; // 5      }    } );    return listeners;  }  return import( jsURL ).then( ( module ) => {    const listeners = getListeners( module.default ); // 2    return {      name: module.default.name,      listeners, // 6      template,      style    }  } );}

现在方法变得有点复杂了。添加了一个新的函数getListeners()(1) ,将模块的输出传入这个参数中。

然后使用Object.entries()(3)方法遍历导出的模块。如果当前属性以“on”(4)开头,说明是一个监听函数,将这个节点的值(监听函数)添加到listeners对象中去,使用setting[2].toLowerCase()+setting.substr(3)(5)得到键值。

键值是通过去掉开头的“on”,并将后面的“Click”首字母转换成小写组成的(就是从onClick得到click作为建值)。然后传入isteners对象(6)。

可以使用[].reduce()方法代替[].forEach()方法,这样可以省略掉listeners这个变量,如下:

function getListeners( settings ) {  return Object.entries( settings ).reduce( ( listeners, [ setting, value ] ) => {    if ( setting.startsWith( 'on' ) ) {      listeners[ setting[ 2 ].toLowerCase() + setting.substr( 3 ) ] = value;    }    return listeners;  }, {} );}

现在,可以将监听绑定在组件内部的类中:

function registerComponent( { template, style, name, listeners } ) { // 1  class UnityComponent extends HTMLElement {    connectedCallback() {      this._upcast();      this._attachListeners(); // 2    }    [...]    _attachListeners() {      Object.entries( listeners ).forEach( ( [ event, listener ] ) => { // 3        this.addEventListener( event, listener, false ); // 4      } );    }  }  return customElements.define( name, UnityComponent );}

在listeners方法(1)上增加了一个参数,并且在class中添加了一个新方法_attachListeners()(2)。在这里可以再次使用Object.entries()来遍历listeners(3),并把他们绑定到element(4)。

最后,点击组件可以弹出“Don't touch me!”,如下

使用JavaScript怎么实现一个单文件组件

兼容性问题及其他

可以看到,为了实现这个单文件组件,大部分工作围绕如何支持基本的FORM。很多部分使用了脏hacks(使用ObjectURI来加载ES中的模块,没有浏览器的支持,这种技术没有什么意义)。还好,所有的技术在主流浏览器下运行正常,包括:Chrome,Firefox和Safari。

看完上述内容是否对您有帮助呢?如果还想对相关知识有进一步的了解或阅读更多相关文章,请关注编程网精选频道,感谢您对编程网的支持。

--结束END--

本文标题: 使用JavaScript怎么实现一个单文件组件

本文链接: https://lsjlt.com/news/276020.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 使用JavaScript怎么实现一个单文件组件
    使用JavaScript怎么实现一个单文件组件?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。JavaScript是什么JavaScript是一种直译式的脚本语言...
    99+
    2023-06-14
  • Vue中怎么实现一个单文件组件
    这期内容当中小编将会给大家带来有关Vue中怎么实现一个单文件组件,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。背景相信大家在使用Vue开发项目时,基本都是以单文件组件的形...
    99+
    2024-04-02
  • Vuejs中怎么实现一个单文件组件
    Vuejs中怎么实现一个单文件组件,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。初识单文件组件还是利用工欲善其事必先利其器 中的源码,在 ...
    99+
    2024-04-02
  • JS如何实现一个单文件组件
    目录概述单文件组件基本概念简单的loader解析组件内容注册组件获取脚本内容DataURI和ObjectURI动态导入实现行为层兼容性问题及其他概述 前端开发人员只要了解过vue.j...
    99+
    2024-04-02
  • Vue如何实现一个单文件组件
    这篇文章主要介绍了Vue如何实现一个单文件组件的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Vue如何实现一个单文件组件文章都会有所收获,下面我们一起来看看吧。初识单文件组件还是利用工欲善其事必先利其器 中的源...
    99+
    2023-07-04
  • VB.NET中怎么实现一个菜单组件
    VB.NET中怎么实现一个菜单组件,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。Windows应用程序中往往离不开菜单,菜单的应用是十分普遍了,并且菜单的设计是十分简单的,这主...
    99+
    2023-06-17
  • 使用react怎么实现一个Radio组件
    使用react怎么实现一个Radio组件?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。测试组件:class Test extends Comp...
    99+
    2023-06-14
  • 怎么使用react实现一个tab组件
    本教程操作环境:windows7系统、react18.0.0版、Dell G3电脑。怎么使用react实现一个tab组件?react写Tab组件使用react写TAB栏组件和对应hover事件(背景:在用gatsby开发页面时,遇到这样的组...
    99+
    2022-11-22
    tab组件 React
  • JavaScript实现一个输入框组件
    本文实例为大家分享了手动实现一个输入框组件的具体代码,供大家参考,具体内容如下 背景 taro h5中想要实现一个样式如下的输入框: 问题: taro组件和taro-ui组件中都没...
    99+
    2024-04-02
  • vue怎么实现一个单独的组件注释
    这篇文章主要介绍“vue怎么实现一个单独的组件注释”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“vue怎么实现一个单独的组件注释”文章能帮助大家解决问题。一个单独的组件注释写了一个组件 加了一些注释...
    99+
    2023-06-30
  • vue实现一个单文件组件的完整过程记录
    目录前言单文件组件 基本概念 简单的loader 解析组件内容 注册组件 获取脚本内容 Data URI和Object URI 动态导入 实现 行为层 兼容性问题及其他 总结前言 前...
    99+
    2024-04-02
  • 如何使用MySQL和JavaScript实现一个简单的文件浏览功能
    如何使用MySQL和JavaScript实现一个简单的文件浏览功能引言:在当今数字化时代,文件管理和浏览功能成为我们日常工作中不可或缺的一部分。本文将介绍如何使用MySQL和JavaScript实现一个简单的文件浏览功能。我们将使用MySQ...
    99+
    2023-10-22
    MySQL JavaScript 文件浏览
  • vue单文件组件的实现
    最近翻阅了一下vue。发觉有一个单文件组件之前基本忽视掉了。vue.js中的单文件组件允许在一个文件中定义一个组件的所有内容。也就是说,一个页面或者是一个组件,我们想将他们捆绑在一起...
    99+
    2024-04-02
  • JS实现一个文件选择组件详解
    目录前言插件安装插件使用参数说明前言 花了点时间利用广度与深度优先搜索算法实现了一个文件选择插件,支持无限层次的文件夹嵌套,已开源并打包上传到了npm。 本文将跟大家分享一下这个插件...
    99+
    2024-04-02
  • vue实现一个单独的组件注释
    目录一个单独的组件注释效果图如下vue代码注释规范,代码规范注释规范1.TEMPLATE结构内容注释2.STYLUS注释3.SCRIPT注释一个单独的组件注释 写了一个组件 加了一些...
    99+
    2024-04-02
  • vb怎么实现单个文件上传到文件夹
    在VB中实现单个文件上传到文件夹可以使用以下代码: Imports System.IO Public Class Form1 ...
    99+
    2024-03-15
    vb
  • 如何使用react实现一个tab组件
    这篇“如何使用react实现一个tab组件”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“如何使用react实现一个tab组件...
    99+
    2023-07-04
  • 浅析Vue单文件组件与非单文件组件使用方法
    单文件组件:一个文件中只包含一个组件,后缀为.vue(常用) <template> <!-- html部分 --> <div> <p...
    99+
    2022-12-21
    Vue单文件组件 Vue非单文件组件
  • 怎么在Vue中使用Canvas实现一个弹幕组件
    本篇文章为大家展示了怎么在Vue中使用Canvas实现一个弹幕组件,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。功能介绍支持循环弹幕弹幕不重叠支持选择轨道数支持弹幕发送使用npm i vue-bar...
    99+
    2023-06-09
  • Vue.js中怎么实现一个可复用组件
    Vue.js中怎么实现一个可复用组件,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。构成组件组件,是一个具有一定功能,且不同组件间功能相对独立的...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作