
(最初在Medium上发布)
我喜欢编写React代码。 这可能是关于Vue的故事的奇怪介绍,但是您需要了解我的背景才能理解为什么我在这里讨论Vue。
我喜欢编写React代码,但我讨厌阅读它。 JSX是将各个组件快速组装在一起的好主意,Material-UI是引导下一个初创公司UI的绝佳解决方案,通过JS常量计算CSS可以使您非常灵活。 但是,阅读旧的JSX感觉很糟糕-即使采用了严格的代码审查实践,当您尝试计算组件的复杂嵌套时,您可能也不会一次挠头。
我听说过有关Vue的许多事情-并不是那么新手-最终我决定弄湿我的脚。 带来了我所有的React和Polymer(以及Angular,但我们不要再谈论它)了。
Vue非常类似于Polymer,因此作者将其命名为灵感的来源之一。 *.vue
文件的结构似乎是Polymer最好的部分,我直接参与其中。 几天后,我摆脱了打字稿,UI驱动的开发和众多最佳实践的沼泽,我准备分享自己的发现。
走吧
我们将使用npx运行命令。 如果您还没有npm install -g npx
: npm install -g npx
。 当您处理npm cli软件包并且不想npm install -g
数十种应用程序时,Npx可以节省生命。 如果您还没有的话,还需要使用Yarn- npm install -g yarn
应该会让您了解最新信息。
$ npx vue create not-a-todo-app Vue CLI v3.3.0 ? Please pick a preset: Manually select features ? Check the features needed for your project: Babel, TS, PWA, Router, Vuex, CSS Pre-processors, Linter, Unit, E2E ? Use class-style component syntax? Yes ? Use Babel alongside TypeScript for auto-detected polyfills? Yes ? Use history mode for router? (Requires proper server setup for index fallback in production) Yes ? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS ? Pick a linter / formatter config: TSLint ? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)Lint on save ? Pick a unit testing solution: Jest ? Pick a E2E testing solution: Cypress ? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedicated config files ? Save this as a preset for future projects? No
Vue Cli有一个不错的向导来指导您完成操作; 对于本教程,我们将使用手动模式并启用所有功能 。 过度杀伤力? 也许可以,但是我们想看看Vue如何与开箱即用的产品一起工作。 尽管如此,让我们研究一下选择方式和原因以及原因。
我们需要同时启用Babel和TypeScript 。 TS将是首选的主要语言,Babel将支持处理需要编译的外部代码。 您可能会争辩说TypeScript也可以转换JS代码,的确如此,但是在我的实验中(尤其是与单元测试和Vuetify有关),我认为将TS保留为*.ts
并将Babel用于其他所有内容要好得多。
CSS预处理器将在Vuetify中派上用场; 尽管它带有预缩小的CSS,但您可能需要包括原始样式文件以使用样式。 对于任何新项目, Linter / Formatter都是一个明显的要求(您必须遵循单一的代码样式,并且在阅读旧代码的一年内可以感谢我)。 我们同时启用了单元测试和E2E测试-尽管您可能不想执行完整的e2e测试用例,但在使用Vuetify完成操作后了解如何修复这些问题很有用。
本教程并不严格要求Progressive Web App(PWA)支持 , Router和Vuex ,并且我们不会使用它们,但启用它们将简化您在实际项目中的生活。
使用类样式的组件语法? 是的 类使代码略显笨重,但可读性更高,更易于推理。 它们也使您的TypeScript生活更加轻松。
将Babel与TypeScript一起使用以进行自动检测的polyfill吗? 是的 我们希望同时使用Babel和TS,以便稍后进行调查。
使用路由器的历史记录模式? 是的(但YMMV)。 我们不会编写任何后端来在生产中提供此服务,但是使用历史记录API通常是一个好主意。
选择一个CSS预处理器(默认情况下支持PostCSS,Autoprefixer和CSS模块):在本教程中我们将仅使用CSS模块,因此您可以根据自己的喜好随意选择sass / less / stylus。
选择一个 linter / formatter配置: TSlint是一个显而易见的选择,因为我们想尽可能多地使用TypeScript。
选择其他皮棉功能:同时启用( a
)。 棉绒好。
选择一个单元测试解决方案:本教程重点介绍Jest,因此必须选择它。
选择一个E2E测试解决方案:本教程重点介绍赛普拉斯。
您希望在哪里放置Babel,PostCSS,ESLint等的配置? 您难道不是每个人都试图将更多内容放到package.json
有点奇怪吗? 该文件几乎无法读取。 使用专用的配置文件-它们更易于使用,并且您的git历史记录会更漂亮。
是时候验证设置,运行yarn serve
:

控制台中应该没有错误,并且导航到http:// localhost:8080 /将向您打招呼:

验证单元测试是否正常,运行yarn test:unit
:

和e2e测试工作( yarn test:e2e --headless
):

太好了! 转到UI。
物质困境
有一些针对Vue的Material UI库,它们的灵活性和改进程度不同。 当然,还有许多其他组件库,因此您可以随意使用Bootstrap Vue 。 由于许多原因,本教程重点介绍Vuetify :
- 它是GitHub上最受欢迎的Material库;
- 使它正常工作是一种皇家痛苦,因此它是您可以进入的所有边缘案例的绝佳演示。
说服了吗 然后继续安装: vue add vuetify
。 选择配置(高级)选项。
使用预制模板? Vuetify将覆盖默认的App.vue和HelloWorld.vue。 对此回答是,因为这是一个新项目。
使用自定义主题? 是的 无论如何,迟早都需要一个,所以让我们对其进行配置。 出于相同的原因,对使用自定义属性(CSS变量)回答是? 。
选择图标字体: Material Icons(但稍后我还将向您展示如何修复它的Font Awesome)。 使用字体作为依赖项? 不行 我们将从CDN获得字体。
使用点菜组件? 是的 这似乎是使用Vuetify的最简单方法。

有很多更改,但是最重要的是,现在运行yarn serve
您会看到不同的图片:

(您的短毛猫还会收到几十个警告)。
让我们检查单元测试...

使Vuetify与单元测试和端到端测试一起工作
让我们检查./tests/unit/example.spec.ts
。 该测试验证msg
是否显示“ 新消息 ”,但Vuetify随附的模板不再支持该道具。 在现实情况下,您将同时删除HelloWorld组件及其测试,但是在这里,我们更新消息以查找该组件中的内容:
const msg = 'Welcome to Vuetify';
现在测试通过了(用yarn test:unit
验证yarn test:unit
),但是仍然有很多类似的警告
[Vue warn]: Unknown custom element: <v-layout> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
Vuetify的工作方式会添加./src/plugins/vuetify.ts
,将./src/plugins/vuetify.ts
配置为应用程序的一部分。 该文件来自./src/main.ts
。 不幸的是,在运行单元测试时, main.ts
被跳过了。
首先,让我们修复生成的vuetify.ts
的错误和警告。
打开您的./tsconfig.json
并将vuetify
添加到vuetify
部分:
"types": [ "webpack-env", "vuetify", "jest" ],
这告诉TypeScript编译器从何处获取Vuetify类型,。/ ./src/plugins/vuetify.ts
的错误消失了。 让我们修复一些样式警告以进行清理:
import Vue from 'vue'; import Vuetify from 'vuetify/lib'; import 'vuetify/src/stylus/app.styl'; Vue.use(Vuetify, { theme: { primary: '#ee44aa', secondary: '#424242', accent: '#82B1FF', error: '#FF5252', info: '#2196F3', success: '#4CAF50', warning: '#FFC107', }, customProperties: true, iconfont: 'md', });
现在,我们需要在单元测试的上下文中加载Vuetify。 在./tests/jest-setup.js
创建一个新文件,其内容如下:
import '@/plugins/vuetify';
并更新./jest.config.js
以加载它:
module.exports = { ... setupFiles: ['./tests/jest-setup.js'], }

测试仍然失败,但是以一种相当神秘的方式。 怎么了
vuetify/lib
是未经处理的Vuetify原始资源,其中包括ES模块之类的东西。 Jest默认情况下仅对源代码运行转换,这意味着它将忽略node_modules
所有node_modules
。 更重要的是,假设我们告诉Vue使用TypeScript,则没有将Jest配置为可转换JS。
要解决此问题,我们需要对./jest.config.js
进行两项更改:
module.exports = { ... transform: { '^.+\\.vue$': 'vue-jest', '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', '^.+\\.tsx?$': 'ts-jest', '^.+\\.jsx?$': 'babel-jest', // <-- (1) }, transformIgnorePatterns: [ 'node_modules/(?!(vuetify)/)', // <-- (2) ], }
在(1)中,我们告诉Jest使用Babel转换任何*.js
或*.jsx
文件(并且Babel由Vue Cli为我们预先配置),但是(2)是什么? transformIgnorePatterns
指定Jest在编译代码时将忽略的路径,并且正如我之前提到的,默认node_modules
包括node_modules
。 在这里,我们用一个node_modules/(?!(vuetify)/)
正则表达式node_modules/(?!(vuetify)/)
替换默认值,这意味着“忽略以node_modules/
开头的任何路径,除非后面跟随vuetify
”:

请注意,前两条路径如何匹配,而第三条没有。 当我们现在添加Storybook时,此技巧将派上用场。
再次运行测试...

未知的自定义元素又回来了; 但至少它可以编译并成功运行。 Vuetify已转入其中,但我们仍然需要手动注册组件。 有几种方法可以做到这一点( 请查看其文档中的其他选项 ); 我们在这里要做的是将所需的组件导入Vue的全球范围。 再次打开./src/plugins/vuetify.ts
并将其更新为:
import Vue from 'vue'; import Vuetify, { VFlex, VLayout, VContainer, VImg } from 'vuetify/lib'; import 'vuetify/src/stylus/app.styl'; Vue.use(Vuetify, { components: { VFlex, VLayout, VContainer, VImg }, theme: { primary: '#ee44aa', secondary: '#424242', accent: '#82B1FF', error: '#FF5252', info: '#2196F3', success: '#4CAF50', warning: '#FFC107', }, customProperties: true, iconfont: 'md', });
最后,测试通过:

E2E测试也将失败( yarn test:e2e --headless
),但这是由于./tests/e2e/specs/test.js
寻找不再存在的字符串。 E2E测试可以在真实的浏览器中启动真实的应用程序,因此无需修复任何代码-Vuetify已在您的应用程序中全部设置。 修复test.js
以查找新的标头:
cy.contains('h1', 'Welcome to Vuetify')
它将再次变为绿色。
让我们回顾一下。 我们添加了Vuetify,修复了单元测试和e2e测试以处理新模板,并更新了Jest以转换Vuetify的源代码并加载它。 我们的应用程序功能正常,可以使用各种材料组件。 继续讲故事!
故事书
故事书是一个绝妙的主意:您从设计师的角度编写测试用例:从小型组件到完整的应用程序。 您可以对数据流进行推理,确保一切看起来都与UI设计器在Photoshop中的布局完全一致,并单独测试组件。 让我们添加故事书支持!
有一个Vue故事书插件,但我发现sb init
提供了更好的默认模板,因此我们将改用它。 运行npx -p @storybook/cli sb init
,几分钟后,您将得到提示以运行yarn storybook
。 让我们做吧:

让我们添加一个新故事! 创建具有以下内容的./src/components/LoveButton.stories.ts
:
import { storiesOf } from '@storybook/vue'; import LoveButton from './LoveButton.vue'; storiesOf('LoveButton', module) .add('default', () => ({ components: { LoveButton }, template: `<love-button love="vue"/>`, }));
(请注意,如果您不想在输入故事中输入内容,可以在这里使用LoveButton.stories.js
)。
TypeScript将警告您有关丢失的类型的信息,您可以使用yarn add -D @types/storybook__vue
进行修复。
现在,使用以下内容创建./src/components/LoveButton.vue
:
<template> <v-btn color="red"> <v-icon>favorite</v-icon> {{love}} </v-btn> </template> <script lang="ts"> import Vue from 'vue'; export default Vue.extend({ props: ['love'], }); </script>
故事书默认情况下会查找故事的./stories
,但将故事保持在距组件更近的位置通常更方便(就像我们所做的那样)。 告诉故事书在哪里寻找那些更新您的./.storybook/config.js
:
import { configure } from '@storybook/vue'; const req = require.context('../src', true, /.stories.(j|t)s$/); function loadStories() { req.keys().forEach(filename => req(filename)); } configure(loadStories, module);
现在,再次运行yarn storybook
:

不太令人兴奋。 控制台上充满了警告:

不过,我们知道现在的情况。 故事书是另一个具有自己入口点的“根”上下文; 它不使用main.ts
,因此不会加载Vuetify,因此我们需要告诉它这样做。 更新./.storybook/config.js
:
import { configure } from '@storybook/vue'; import '../src/plugins/vuetify'; // <-- add this const req = require.context('../src', true, /.stories.(j|t)s$/); function loadStories() { req.keys().forEach(filename => req(filename)); } configure(loadStories, module);
我们将再次加载现有配置,以确保Storybook使用与真实应用相同的主题。 不幸的是, yarn storybook
现在将失败:

故事书不知道我们使用TypeScript,因此它无法加载vuetify.ts
文件。 要解决此问题,我们需要更新Storybook自己的webpack配置。 使用以下内容创建./.storybook/webpack.config.js
:
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); module.exports = (baseConfig, env, defaultConfig) => { defaultConfig.resolve.extensions.push('.ts', '.tsx', '.vue', '.css', '.less', '.scss', '.sass', '.html') defaultConfig.module.rules.push({ test: /\.ts$/, exclude: /node_modules/, use: [ { loader: 'ts-loader', options: { appendTsSuffixTo: [/\.vue$/], transpileOnly: true }, } ], }); defaultConfig.module.rules.push({ test: /\.less$/, loaders: [ 'style-loader', 'css-loader', 'less-loader' ] }); defaultConfig.module.rules.push({ test: /\.styl$/, loader: 'style-loader!css-loader!stylus-loader' }); defaultConfig.plugins.push(new ForkTsCheckerWebpackPlugin()) return defaultConfig; };
这将加载默认配置,为TypeScript文件添加ts-loader
,还增加了对less和styl(Vuetify使用的)的支持。
警告仍然存在,因为我们需要注册所使用的组件。 这次让我们使用本地组件,以便您可以看到不同之处(在实际的生产应用程序中,将它们全部注册到vuetify.ts
要简单得多)。 更新./src/components/LoveButton.vue
:
<template> <v-btn color="red"> <v-icon>favorite</v-icon> {{love}} </v-btn> </template> <script lang="ts"> import Vue from 'vue'; import { VBtn, VIcon } from 'vuetify/lib'; </script>
故事书在保存时刷新:

略胜一筹。 缺少什么? Vuetify安装程序将字体css直接添加到./public/index.html
但Storybook不使用该文件,因此我们需要添加缺少的Material Icons字体。 使用以下命令创建./.storybook/preview-head.hmtl
(从./public/index.html
复制):
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900"> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons">
(还有其他方法可以做到这一点,例如使用CSS @import
)。
您需要重新启动yarn storybook
才能正确重新渲染它:

更好但仍然低于标准:文本字体不正确,因为Vuetify希望其所有组件都嵌套在v-app
页面样式的v-app
中。 当然,我们不能将v-app
添加到我们的按钮中,所以让我们来装饰故事。 更新您的./src/components/LoveButton.stories.ts
:
import { storiesOf } from '@storybook/vue'; import { VApp, VContent } from 'vuetify/lib'; // <-- add the import import LoveButton from './LoveButton.vue'; // add the decorator const appDecorator = () => { return { components: { VApp, VContent }, template: ` <v-app> <div style="background-color: rgb(134, 212, 226); padding: 20px; width: 100%; height: 100%;"> <v-content> <story/> </v-content> </div> </v-app> `, }; }; storiesOf('LoveButton', module) .addDecorator(appDecorator) // <-- decorate the stories .add('default', () => ({ components: { LoveButton }, template: `<love-button love="vue"/>`, }));
您必须在全局范围内注册VApp
和VContent
,更新./src/plugins/vuetify.ts
:
import Vue from 'vue'; import Vuetify, { VFlex, VLayout, VContainer, VImg, VApp, VContent } from 'vuetify/lib'; import 'vuetify/src/stylus/app.styl'; Vue.use(Vuetify, { components: { VFlex, VLayout, VContainer, VImg, VApp, VContent }, theme: { primary: '#ee44aa', secondary: '#424242', accent: '#82B1FF', error: '#FF5252', info: '#2196F3', success: '#4CAF50', warning: '#FFC107', }, customProperties: true, iconfont: 'md', });
最后,结果是惊人的:

添加故事书测试
最后,让我们确保单元测试涵盖了我们的故事。 添加所需的依赖项: yarn add -D @storybook/addon-storyshots jest-vue-preprocessor babel-plugin-require-context-hook
并创建./test/unit/storybook.spec.js
:
import registerRequireContextHook from 'babel-plugin-require-context-hook/register'; import initStoryshots from '@storybook/addon-storyshots'; registerRequireContextHook(); initStoryshots();
故事书配置使用require.context
收集所有源; 该功能由webpack提供,我们需要使用babel-plugin-require-context-hook
在Jest中替代它。 修改您的./babel.config.js
:
module.exports = api => ({ presets: ['@vue/app'], ...(api.env('test') && { plugins: ['require-context-hook'] }), });
如果babel运行用于单元测试,则在此处添加require-context-hook
插件。
最后,我们需要允许Jest转换故事书的*.vue
文件。 还记得./jest.config.js
中的前瞻正则表达式吗? 现在让我们重温一下:
module.exports = { ... transformIgnorePatterns: [ 'node_modules/(?!(vuetify/|@storybook/.*\\.vue$))', ], }
请注意,我们不能仅在此处添加第二行。 请记住,这是一个忽略模式,因此,如果第一个模式忽略除Vuetify之外的所有内容,那么在Jest到达第二个正则表达式时,故事书文件已被忽略。
新测试按预期工作:

该测试将运行您的所有故事,并根据./tests/unit/__snapshots__/
中的本地快照对其进行验证。 要查看实际效果,可以从按钮组件中删除<v-icon>favorite</v-icon>
并重新运行测试以查看失败:

yarn test:unit -u
将为新的按钮布局更新快照。
回顾
在本教程中,我们学习了如何在启用TypeScript的情况下创建新的Vue应用程序。 如何使用Material UI组件添加Vuetify库。 我们确保我们的单元测试和e2e测试按预期进行。 最后,我们增加了对故事书的支持,创建了一个示例故事,并确保我们的单元测试涵盖了UI更改。
总结思想
JS是一个瞬息万变的世界,事物不断变化,新的格局出现,旧的事物被遗忘。 本教程可能仅在几个月后就过时了,因此这里有一些有用的提示。
了解您的工具。 可以从堆栈溢出中复制粘贴行,直到代码可以正常工作,但是您必须研究为什么更改以后才能起作用。 阅读文档,并确保您了解更改的确切含义。
如果您有部分工作要做,请提交。 即使这项工作仍在进行中,如果您所做的进一步更改会破坏某些内容,您也可以将其还原。
实验! 如果某些方法不符合您的想法,而文档另有说明,请尝试! 前端世界主要是开源的,因此请深入研究第三方资源,看看是否可以从内部修改您的仪器以添加调试日志记录。