「这是我参与2022首次更文挑战的第10天,活动详情查看:2022首次更文挑战」。
写在开头
ElementUI源码系列 更新到这里已经是第八篇了,不过根据这几天看到的数据,阅读和点赞量可以用两个字来形容 "惨淡" (╯﹏╰),看来大家不太喜欢源码系列的文章囖?又或者是不太喜欢我写的文章?是不够精彩?小编可以保证每篇文章都是尽心尽力去写的,写完都是自我浏览好几遍,觉得可以了才发出来的,呜呜呜~
不过呢,这也不是什么大事,既然大家不喜欢,那我可得再多写一点。像爱情一样,做不了对方最喜欢的人,那就做她最讨厌的人,这又何尝不是一种爱呢?( ̄▽ ̄)~*
好了,题外话讲完,该步入正题了,本章我们继续来学习 element-ui 的源码,主要来学习一下 ElementUI
这个官网是如何搭建的。
你可以在本地启动这个官网的项目,只需要你下载源码,执行 npm install
命令,再执行 npm run dev
命令就能看到这个页面了,对照着页面学习比较容易理解唷。
搭建文档项目
下面我们就自己来具体看看 ElementUI
是如何搭建起这个文档项目的,首先,该项目是一个 Vue+Webpack
项目,但和我们平时直接用 vue-cli 创建的项目不同,它是从零开始搭建项目结构的,稍微有那么一点一点麻烦,下面就且听我细细道来哈。
初建项目
首先,我们先把需要的依赖下载了。
npm install vue@2.5.21 vue-loader@15.7.0 vue-router@3.0.1 vue-template-compiler@2.5.21 -D
主要是下载一些 Vue
相关的依赖,关于 webpack
的依赖,我们在 第一篇文章 的时候就基本都安装过了。
然后,我们先在 examples
文件夹下,创建两个文件 demo.html
文件和 entry.js
项目入口文件:
<!-- exmaples/demo.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>组件库预览文档项目</title>
</head>
<body>
<div id="app">橙某人</div>
</body>
</html>
// exmaples/entry.js
console.log('入口文件')
再来 build
文件夹下创建一个 webpack.demo.js
配置文件:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: './examples/entry.js',
output: {
path: path.resolve(process.cwd(), './examples/element-ui/'),
publicPath: '',
filename: '[name].[hash:7].js',
chunkFilename: '[name].js'
},
// 启动一个服务
devServer: {
host: 'localhost',
port: 8082,
publicPath: '/',
hot: true
},
plugins: [
new HtmlWebpackPlugin({
template: './examples/demo.html', // 模板文件
filename: './index.html', // 将生成的 HTML 写入到文件中, 默认写入在当前项目的 index.html 中。webpack-dev-server 启动一个服务, 默认会读取项目根目录下的 index.html, 所以 filename 最好是 index.html, 当然你要改也是可以的
}),
]
}
上面的配置信息,基本都是 webpack
的基本配置内容,基操,就不多说啦。
我们也给这个配置文件配置一条启动命令 package.json
:
"scripts": {
"demo": "webpack-dev-server --config build/webpack.demo.js"
},
最后,我们来启动它,执行 npm run demo
命令。
如果你执行命令后,没有报错并且访问启动地址能正常看到页面显示,就说明你成功啦。
引入vue
当然,上面的成功还只是第一步的成功,千万不要太高兴了,哈哈哈,我们继续说哈。
我们再来创建一个项目主文件 examples/app.vue
:
<template>
<h1>app.vue</h1>
</template>
在入口文件 entry.js
中创建 Vue
实例:
// examples/entry.js
import Vue from 'vue';
import entry from './app.vue';
new Vue({
...entry
}).$mount('#app');
修改配置文件 webpack.demo.js
:
// build/webpack.demo.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
...
module: {
rules: [
// 处理 .vue 文件
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './examples/demo.html',
filename: './index.html',
}),
new VueLoaderPlugin(), // vue-loader的15版本之后, 都要求伴生使用 Plugin :https://vue-loader.vuejs.org/migrating.html#a-plugin-is-now-required
]
}
改完配置文件后,我们再次重新执行 npm run demo
命令重启项目。
页面如果能正常展示就说明你又成功了一步。
引入vue-router
上面我们已经安装了 vue-router
依赖,下面就来看看在 element-ui
文档项目中是如何使用它的,先在入口文件 entry.js
中引入:
// examples/entry.js
import Vue from 'vue';
import entry from './app.vue';
import VueRouter from 'vue-router';
import routes from './route.config.js'; // 引入路由配置文件
Vue.use(VueRouter);
const router = new VueRouter({
mode: 'hash',
base: __dirname, // 应用的基路径, 因为 entry.js 本身为项目的入口文件, 所以 __dirname => '/'
routes
});
new Vue({
...entry,
router
}).$mount('#app');
再创建导航路由的配置文件 route.config.js
文件:
// examples/route.config.js
const LOAD_MAP = { // 便于切换不同语言的文件
'zh-CN': name => {
return r => require.ensure([], () =>
r(require(`./pages/zh-CN/${name}.vue`)),
'zh-CN');
},
};
// 加载不同语言的路由
const load = function(lang, path) {
return LOAD_MAP[lang](path);
};
const generateMiscRoutes = function(lang) {
let guideRoute = {
path: `/${ lang }/guide`, // 指南
redirect: `/${ lang }/guide/design`,
component: load(lang, 'guide'),
children: [{
path: 'design', // 设计原则
name: 'guide-design' + lang,
meta: { lang },
component: load(lang, 'design')
}, {
path: 'nav', // 导航
name: 'guide-nav' + lang,
meta: { lang },
component: load(lang, 'nav')
}]
};
let themeRoute = {
path: `/${ lang }/theme`,
component: load(lang, 'theme-nav'),
children: [
{
path: '/', // 主题管理
name: 'theme' + lang,
meta: { lang },
component: load(lang, 'theme')
},
{
path: 'preview', // 主题预览编辑
name: 'theme-preview-' + lang,
meta: { lang },
component: load(lang, 'theme-preview')
}]
};
let resourceRoute = {
path: `/${ lang }/resource`, // 资源
meta: { lang },
name: 'resource' + lang,
component: load(lang, 'resource')
};
return [guideRoute, resourceRoute, themeRoute];
};
let route = [];
route = generateMiscRoutes('zh-CN'); // 写固定死中文路由
export default route;
ElementUI
的文档项目有不同于语言切换的功能,所以在路由配置这一块写的还是挺复杂的,为方便我们学习使用,我们先只考虑中文(zh-CN
)的路由。不过,小编会尽量保持原有样子,所以完全不用当心与源码会有很大的差别。
其实路由配置文件中,你只要记住最终就是要返回一个数组,数组内是每个路由的配置信息即可,管它里面多复杂。
最后,你还需要把对应页面的文件创建出来,最好在每个文件中随便写点内容,方便等等渲染时查看。
因为我们通过 webpack-dev-server
启动的服务默认是有热更新的,而做完这些后,如果页面没有报错,那就说明你又成功了一步哦。
引入scss
接下来,我们要来实现文档项目中的头部组件,你也可以自己对着源码直接抄过来即可,不过,要求你抄完后页面不能有报错,点击能跳转,这是唯一的要求。
因为文档项目中的样式用的是 scss
来编写的,所以我们首先第一步就是来为项目引入 scss
支持。
老样子,先安装依赖:
npm install style-loader@0.23.1 css-loader@2.1.0 sass-loader@10.1.1 -D
修改配置文件,其实就是增加一些 loader
来处理 scss
样式:
// build/webpack.demo.js
...
module.exports = {
...
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false
}
}
},
// 处理 scss
{
test: /\.(scss|css)$/,
use: [
'style-loader',
'css-loader',
'sass-loader'
]
},
]
},
...
}
修改完配置文件后,就可以重启项目 npm run demo
了,没报错就是OK的 ^-^。
实现头部组件
接着我们创建 examples/components/header.vue
头部组件:
<template>
<div class="headerWrapper">
<header class="header" ref="header">
<div class="container">
<h1>Logo</h1>
<!-- nav -->
<ul class="nav">
<li class="nav-item">
<router-link active-class="active" :to="`/zh-CN/guide`">
{{ langConfig.guide }}
</router-link>
</li>
<li class="nav-item">
<router-link active-class="active" :to="`/zh-CN/component`">
{{ langConfig.components }}
</router-link>
</li>
<li class="nav-item nav-item-theme">
<router-link active-class="active" :to="`/zh-CN/theme`">
{{ langConfig.theme }}
</router-link>
</li>
<li class="nav-item">
<router-link active-class="active" :to="`/zh-CN/resource`" exact>
{{ langConfig.resource }}
</router-link>
</li>
</ul>
</div>
</header>
</div>
</template>
<script>
import compoLang from '../i18n/component.json';
export default {
computed: {
lang() {
return this.$route.path.split('/')[1] || 'zh-CN';
},
langConfig() {
return compoLang.filter(config => config.lang === this.lang)[0]['header'];
},
}
}
</script>
<style lang="scss" scoped>
.headerWrapper {
height: 80px;
}
#v3-banner {
background-color: #409EFF;
min-height: 30px;
padding: 5px 60px;
z-index: 19;
box-sizing: border-box;
text-align: center;
color: #eee;
}
#v3-banner a {
color: #FFF;
font-weight: bold;
}
.header {
height: 80px;
background-color: #fff;
color: #fff;
top: 0;
left: 0;
width: 100%;
line-height: 80px;
z-index: 100;
position: relative;
.container {
height: 100%;
box-sizing: border-box;
border-bottom: 1px solid #DCDFE6;
}
.nav-lang-spe {
color: #888;
}
h1 {
margin: 0;
float: left;
font-size: 32px;
font-weight: normal;
a {
color: #333;
text-decoration: none;
display: block;
}
span {
font-size: 12px;
display: inline-block;
width: 34px;
height: 18px;
border: 1px solid rgba(255, 255, 255, .5);
text-align: center;
line-height: 18px;
vertical-align: middle;
margin-left: 10px;
border-radius: 3px;
}
}
.nav {
float: right;
height: 100%;
line-height: 80px;
background: transparent;
padding: 0;
margin: 0;
&::before, &::after {
display: table;
content: "";
}
&::after {
clear: both;
}
}
.nav-gap {
position: relative;
width: 1px;
height: 80px;
padding: 0 20px;
&::before {
content: '';
position: absolute;
top: calc(50% - 8px);
width: 1px;
height: 16px;
background: #ebebeb;
}
}
.nav-logo,
.nav-logo-small {
vertical-align: sub;
}
.nav-logo-small {
display: none;
}
.nav-item {
margin: 0;
float: left;
list-style: none;
position: relative;
cursor: pointer;
&.nav-algolia-search {
cursor: default;
}
&.lang-item,
&:last-child {
cursor: default;
margin-left: 34px;
span {
opacity: .8;
}
.nav-lang {
cursor: pointer;
display: inline-block;
height: 100%;
color: #888;
&:hover {
color: #409EFF;
}
&.active {
font-weight: bold;
color: #409EFF;
}
}
}
a {
text-decoration: none;
color: #1989FA;
opacity: 0.5;
display: block;
padding: 0 22px;
&.active,
&:hover {
opacity: 1;
}
&.active::after {
content: '';
display: inline-block;
position: absolute;
bottom: 0;
left: calc(50% - 15px);
width: 30px;
height: 2px;
background: #409EFF;
}
}
}
}
.nav-dropdown {
margin-bottom: 6px;
padding-left: 18px;
width: 100%;
span {
display: block;
width: 100%;
font-size: 16px;
color: #888;
line-height: 40px;
transition: .2s;
padding-bottom: 6px;
user-select: none;
&:hover {
cursor: pointer;
}
}
i {
transition: .2s;
font-size: 12px;
color: #979797;
transform: translateY(-2px);
}
.is-active {
span, i {
color: #409EFF;
}
i {
transform: rotateZ(180deg) translateY(3px);
}
}
&:hover {
span, i {
color: #409EFF;
}
}
}
.nav-dropdown-list {
width: auto;
}
@media (max-width: 850px) {
.header {
.nav-logo {
display: none;
}
.nav-logo-small {
display: inline-block;
}
.nav-item {
margin-left: 6px;
&.lang-item,
&:last-child {
margin-left: 10px;
}
a {
padding: 0 5px;
}
}
.nav-theme-switch, .nav-algolia-search {
display: none;
}
}
}
@media (max-width: 700px) {
.header {
.container {
padding: 0 12px;
}
.nav-item {
a {
font-size: 12px;
vertical-align: top;
}
&.lang-item {
height: 100%;
.nav-lang {
display: flex;
align-items: center;
span {
padding-bottom: 0;
}
}
}
}
.nav-dropdown {
padding: 0;
span {
font-size: 12px;
}
}
.nav-gap {
padding: 0 8px;
}
.nav-versions {
display: none;
}
}
}
</style>
以上代码 html
和 js
两部分做了精简,方便你理解,样式完全是源码中搬过来的,相信样式也难不到大家。
我们再创建 examples/i18n/components.json
文件:
[ { "lang": "zh-CN", "demo-block": { "hide-text": "隐藏代码", "show-text": "显示代码", "button-text": "在线运行", "tooltip-text": "前往 codepen.io 运行此示例" }, "header": { "guide": "指南", "components": "组件", "theme": "主题", "resource": "资源" } }, { "lang": "en-US", "demo-block": { "hide-text": "Hide", "show-text": "Expand", "button-text": "Try it!", "tooltip-text": "Run this demo on codepen.io" }, "header": { "guide": "Guide", "components": "Component", "theme": "Theme", "resource": "Resource" } }]
这个文件我也做了一些删除,只留下了两个对象,相信你一看也知道它是做什么用的了。主要就是方便进行不同语言的切换,切换不同语言功能 ElementUI
选用的是 i18n, 它一种不错的前端国际化方案,对它不了解的小伙伴可以私下去学习学习,它使用起来真的是非常的简单方便唷。
编写完组件,我们来引入这个组件试试看,它主要在入口文件 entry.js
中被引入:
import Vue from 'vue';
import entry from './app.vue';
import VueRouter from 'vue-router';
import routes from './route.config';
import MainHeader from './components/header.vue';
Vue.use(VueRouter);
Vue.component('main-header', MainHeader);
const router = new VueRouter({
mode: 'hash',
base: __dirname,
routes
});
new Vue({
...entry,
router
}).$mount('#app');
具体使用:
<!-- app.vue -->
<template>
<div>
<main-header></main-header>
<router-view></router-view>
<h1>app.vue内容</h1>
</div>
</template>
最后,如果你能看到一个正常不报错的页面且点击能正常跳转,咱就又成功了唷。
完善路由 - 组件
做完上面这些步骤后,文档项目的基本项目结构就算搭建起来了,终于,我们能步入这篇文章最核心的内容了。其实本章的核心是官方文档中这一块 预览组件 是如何实现的?这才是我最想分享的内容,因为它涉及的知识点算是比较多的,也比较有趣一点。
不过,在小编把关于它的内容写完后,发觉本篇文章内容稍微有点长了。。。(真的)
经过再三思索后,我还是决定把这块内容拆分到下一篇文章,作为单独的内容来讲,整体会比较好理解一些。
当然,本章内容还没完,我们还需要来完善一下 "组件" 这个导航路由的其他一些子路由。
也就是左边这一块,当然,我们不需要来实现全部的路由,先来实现这个 button
组件的子路由即可。
我们还是来到路由配置文件 route.config.js
中:
import navConfig from './nav.config'; // 引入 "组件" 导航的子路由配置
// ... 省略上面已写过的内容
let route = [];
route = generateMiscRoutes('zh-CN');
// 增加 "组件" 路由的子路由
const LOAD_DOCS_MAP = {
'zh-CN': path => {
return r => require.ensure([], () =>
r(require(`./docs/zh-CN${path}.md`)),
'zh-CN');
},
};
const loadDocs = function(lang, path) {
return LOAD_DOCS_MAP[lang](path);
};
const registerRoute = (navConfig) => {
let route = [];
Object.keys(navConfig).forEach((lang, index) => {
let navs = navConfig[lang];
route.push({
path: `/${ lang }/component`,
// redirect: `/${ lang }/component/installation`,
component: load(lang, 'component'),
children: []
});
navs.forEach(nav => {
if (nav.href) return;
if (nav.groups) {
nav.groups.forEach(group => {
group.list.forEach(nav => {
addRoute(nav, lang, index);
});
});
} else if (nav.children) {
nav.children.forEach(nav => {
addRoute(nav, lang, index);
});
} else {
addRoute(nav, lang, index);
}
});
});
function addRoute(page, lang, index) {
const component = page.path === '/changelog' ? load(lang, 'changelog') : loadDocs(lang, page.path);
let child = {
path: page.path.slice(1),
meta: {
title: page.title || page.name,
description: page.description,
lang
},
name: 'component-' + lang + (page.title || page.name),
component: component.default || component
};
route[index].children.push(child);
}
return route;
};
let componentChilds = registerRoute(navConfig);
route = route.concat(componentChilds);
export default route;
我们再新建一个 "组件" 导航的子路由配置文件 nav.config.json
:
// examples/nav.config.json
{
"zh-CN": [
{
"name": "组件",
"groups": [
{
"groupName": "Basic",
"list": [
{
"path": "/button",
"title": "Button 按钮"
}
]
}
]
}
]
}
稍微做了一些删减,只留下了 button
这个子路由,然后我们再创建对应的 button
子路由的页面 button.md
:
<!-- examples/docs/zh-CN/button.md -->
## Button 按钮
常用的操作按钮。
需要注意的是,button
子路由的页面是以 .md
为文件的,可不要搞错了哦。
最后,我们修改一下 component.vue
文件,给 button
子路由一个入口,方便我们进行测试:
<!-- examples/pages/zh-CN/component.vue -->
<template>
<div>
<router-link active-class="active" :to="`/zh-CN/component/button`">
按钮路由
</router-link>
<router-view></router-view>
</div>
</template>
不过,你现在去点击它会报错,因为我们还未进行后续的处理,这就留到下一章节再讲了,也是下一章的重点内容!
最后,我们还是放一张项目结构的最新情况截图,方便需要的小伙伴参考。
总结
这篇文章写着写着有点出乎小编的意料之外了,本来打算 element-ui
官方文档项目一篇文章就把它给讲完,但是现在却拦腰截断,拆成两篇文章来分享了。 o(TωT)o
原因是全怼一起文章内容就稍微有点长了,会增加一些小伙伴的阅读负担,所以犹豫再三还是拆开了,希望这样能给你带来更好的阅读感受吧。
而本章节,我们主要就是学习了如何从零搭建起一个 vue + vue-router + webpack + scss
的项目结构,这其实也是一名前端人员应该具备的基础能力了,虽然现在有很多 cli
工具,能帮助我们快速自动生成好项目的基础结构,但我们不能忽视这些底层的建设过程,这是值得引起我们关注的。
往期内容
- ElementUI源码系列一 - 从零搭建项目架构,项目准备、项目打包、项目测试流程
- ElementUI源码系列二 - 引入scss,用gulp把scss转成css并补全、压缩,用cp-cli移动目录、文件
- ElementUI源码系列三 - 学习gen-cssfile.js文件之自动创建组件的.scss文件与生成index.scss文件内容
- ElementUI源码系列四 - 学习new.js文件之自动创建组件目录结构与生成components.json文件内容
- ElementUI源码系列五 - 学习build-entry.js文件之自动生成总入口文件index.js内容
- ElementUI源码系列六 - 小结
- ElementUI源码系列七 - 组件按需引入
- ElementUI源码系列八 - 搭建element-ui官方文档之项目的基本框架
- ElementUI源码系列九 - 搭建element-ui官方文档之md文件渲染到页面、demo-block组件的实现
至此,本篇文章就写完啦,撒花撒花。
希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。