前言
相信大家在逛技术论坛或者技术博客的时候,都会发现有些写的很好的文章我们想保存下来以便可以重复翻阅和查看,在一些相对大的站点,比如SegmentFault或者掘金都会提供类似收藏这样的功能来帮我们做这个保存的工作,我们以后可以回来重复查看,但是我们平时浏览的站点肯定不止一个,并且有时候一些个人的技术博客并没有这样的功能,我们就只能存浏览器书签这样做了,这样我们平时保存下来的一些文章就会比较分散,没有一个统一的地方来帮我们管理,如果有这样一个应用,我们只要输入某篇我们想保存的文章的URL地址就可以帮我们生成这篇文章的正文内容,并且可以支持标签的分类和搜索等功能就好了,就是基于这个需求和我平时个人的使用经验开发出了这个应用。大家可以进这个地址先体验一下,账号是test,密码123456。该应用采用Angular4开发,koa2提供数据接口,因为我是Angular的初学者,边看官方文档边把这个应用做了出来的,也遇到了一些问题,下面我大概说一下开发的过程和遇到的一些问题的解决方案。技术方案选型如下
Angular 前端框架
Angular Cli 前端打包构建
koa2 提供数据接口
MongoDB 提供数据储存
phantom 和 node-readability 提供对文章正文的提取
一、项目的初始化
Angular是一个正式发布于16年九月的一个前端框架(其实Angular的定位是一个平台了),它和vue、react不太一样的是,Angular不只是针对视图层的一个库,它提供的是一个完整的解决方案和生态,它本身内置了组件化方案、模块化方案、测试、表单验证、路由、国际化和HTTP服务等,这些东西我们开发者不用再去纠结怎么选择,直接按官方的建议说明走就好了,选择困难症患者肯定很喜欢这样的,但相对的它也就没有vue和react那么灵活了,这个怎么看待得根据我们每个人的项目需求来选择。既然Angular它本身是这么完整的了,它肯定也有CLI工具,那就是Angular CLI,Angular CLI是一个命令行界面工具,它可以创建项目、添加文件以及执行一大堆开发任务,比如测试、打包和发布。我们可以先执行以下命令全局安装Angular CLI
npm install -g @angular/cli
然后执行ng new [ProjectName]就可以初始化一个项目了。
二、模块化与组件化
Angular 应用是模块化的,并且 Angular 有自己的模块系统,它被称为NgModules。我们的应用由一个或者多个模块组成,并且必须要有一个根模块,我们通常命名为AppModule,每个模块都会有一个叫@NgModule的装饰器函数,它接收一个用来描述模块属性的元数据对象,我们在这个对象属性上配置我们应用所需的组件、路由、指令、管道、服务等。然后Angular会引导根模块来启动应用,有如下代码
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
Angular应用还是组件化的,组件负责控制屏幕上的一小块区域,称之为视图,每个组件也有一个叫@Component的装饰器函数,它也接收一个元数据对象,我们可以在该对象属性上配置组件的模板文件,和组件模板的样式文件等,看如下代码
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
这样我们就可以组合不同的组件拼装起我们整个应用了。整体架构图如下
三、路由配置
Angular的路由是通过一个叫RouterModule的模块来配置的,RouterModule在@angular/router这个包里面,所以路由配置模块RouterModule需要先从@angular/router这个包导出来,然后调用RouterModule的forRoot方法给他传进去一个数组配置对象就好了,需要注意的是我们还要有一个路由来匹配我们所有路由都不配置的情况,它的path应该写成两个星号
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{
path: 'index',
component: IndexComponent
},
{
path: '**',
redirectTo: 'index',
pathMatch: 'full'
}
])
@NgModule({
imports: [RouterModule.forRoot(routes)]
...
})
路由配置好后把它的返回值添加到AppModule的imports数组里面就好了。然后在我们的根组件appComponent的模板里面添加来作为我们的路由渲染出口,到这里基本路由配置就完成了。当然Angular路由的内容还有很多,比如路由守卫,路由模块的划分,路由的传值等等,更加详细的内容可以查看Angular的中文文档
四、目录结构划分
目录的划分我是根据个人的开发习惯来做的,我们的业务逻辑的开发大多在app文件夹下进行,我一般会在app文件夹下新建一个pages文件来做页面级别组件的存放,然后会分别新建directive、pip、service、animation、interceptor文件夹来存放我们应用的指令、管道、服务、动画、拦截器,因为这个应用我没有用一些第三方的UI组件库,一些通用的组件都是自己封装的,所以还需要一个component文件夹来存放整个应用所需的一些通用UI组件,这样我们整个应用的划分就很清晰了。最后的目录说明大致如下
app
├── animations // 动画
│ └── global-router-animation.ts
├── app-routing.module.ts // 路由的配置文件
├── app.component.html // 根组件的模板文件
├── app.component.scss // 根组件的样式文件
├── app.component.spec.ts // 根组件的测试文件
├── app.component.ts // 根组件
├── app.module.ts // 根模块
├── component // 通用的UI组件
├── button
│ └── dropdown
├── directives // 指令
│ └── markdown-editor
├── interceptor // 拦截器
│ └── global-response-interceptor.ts
├── page // 页面级别的组件
│ ├── add-link-note
│ ├── add-note
│ ├── classification
│ ├── edit-note
│ ├── index
│ ├── search
│ ├── tag
│ └── view-note
└─ services // 服务
├── loading-bar
├── msg
├── note
└── tag
五、跨域问题
跨域问题几乎是现代前端项目都会遇到的了,在vue,react等项目项目中我们可以通过webpack-dev-server的代理配置来解决,Angular CLI创建出来的项目底层也是通过webpack来进行应用的打包编译的,但是Angular CLI把webpack的配置给包装隐藏起来了,我们可以通过执行命令ng eject来暴露出webpack的配置文件,但就为了配置跨域问题就把webpack的配置暴露出来其实没有必要。我们可以这样做,先在项目的根目录地下新建一个proxy.config.json配置文件,在该文件里面写下如下代码,告诉Angular,应用里面所有以api开头的HTTP请求都转发到localhost的3002端口。
{
"/api":{
"target":"http://localhost:3002"
}
}
写完该配置文件后我们还需要在package.json的start命令里面加上这个配置
"start" : "ng serve --proxy-config proxy.config.json"
然后执行npm run start重启服务,这样我们的跨域问题就很简单的解决了。
六、未解决的问题
前面有说过这个应用没有使用一些第三方的UI组件库,项目中用到的一些UI组件是我自己封装的,其中有一个LoadingBar组件和message消息提示组件,它的调用方式应该是通过服务的形式来调用的,类似如下代码
this.loadingBar.start()
this.msg.info('这是消息提示')
但是在Angular里面服务本身是不带模板的,只有组件才有模板,所以这样的组件应该是要通过服务去动态的加载一个带模板的组件,但是我没有找到相关的实现方案,去看了一下ng-zorro的源码没看懂?,这里希望有大神赐教。最后我的实现是通过最不优雅的一种方案,直接在服务里面是操作DOM了。比如我的LoadingBar服务
import { Injectable } from '@angular/core';
@Injectable()
export class LoadingBarService {
constructor() { }
public $Loading = {
start: function(){
const myLoadingBar = document.querySelector('.my-loading-bar');
if (myLoadingBar !== null && myLoadingBar instanceof HTMLElement) {
let LoadingBarDivWidth = 0;
this.timer = setInterval(() => {
LoadingBarDivWidth++;
myLoadingBar.style.width = LoadingBarDivWidth + 'vw';
if (LoadingBarDivWidth >= 100) {
clearInterval(this.timer);
}
}, 25);
} else {
const LoadingBarDiv = document.createElement('div');
LoadingBarDiv.className = 'my-loading-bar';
const bodyEl = document.querySelector('body');
bodyEl.appendChild(LoadingBarDiv);
let LoadingBarDivWidth = 0;
this.timer = setInterval(() => {
LoadingBarDivWidth++;
LoadingBarDiv.style.width = LoadingBarDivWidth + 'vw';
if (LoadingBarDivWidth >= 100) {
clearInterval(this.timer);
}
}, 25);
}
},
finish: function(){
const myLoadingBar = document.querySelector('.my-loading-bar');
if (myLoadingBar !== null && myLoadingBar instanceof HTMLElement) {
clearInterval(this.timer);
myLoadingBar.style.width = 0 + 'vw';
}
}
};
}
七、打包
在我们整个应用开发完成后我们需要build出静态文件交由node来部署,这里有几个需要注意一下。在CLI生成的项目的编译配置命令里面是没有开启加--prod配置的,需要我们手动去加上命令开启,加上--prod之后打包出来的资源就不会再有resource map了,这样文件体积会小很多,如果Angular Cli用的是1.3.0以上的版本还可以加上--build-optimizer(摇树优化),文件体积会更加小,在我做这个项目时用的版本还是1.2.7。最后build的命令配置如下
"build" : "ng build --prod --build-optimizer"
八、总结
本文只是一个Angular新手做的第一个项目的流水总结,并没有说到什么新意有深度的东西。Angular是一个很好的框架,上手门槛没有大多数人想象的那么高,只要你JavaScript基础扎实,对ES6的语法有基本的了解就可以去学习,相信会给到你不一样的开发体验。最后奉上项目的基本信息
其实该应用如果大家有自己的个人服务器的话完全可以部署给自己使用,如果没有也可以部署到国外的一些免费服务提供商比如heroku上。好啦,如果大家对该项目有什么问题和建议欢迎留言一起讨论。