前言本文介绍了如何合理拆分项目,我们将在后续文章中进行性能优化等方面的探讨。人们对Angular的批评之一是打包后非常笨重。如果你不小心,main.js会大得离谱。其实遇到类似的问题,不管是大尺寸、大数据,还是大流量,都有一个思路:拆分。再加上浏览器的缓存机制,可以很好的优化项目访问速度。本文相关代码在:https://github.com/Vibing/ang...拆分思路整个项目包括:强依赖库(Angular框架本身)、UI组件库和第三方库、业务代码部分;用户行为维度:所有的用户访问都是基于路由,一个路由一个页面;从以上两点,可以拆分。基于第一点,强依赖库和几乎不会变化的库可以打包成一个vendor_library,里面可以包含@angular/common、@angular/core、@angular/forms、@angular/router等类似的包,UI组件库或者lodash之类的库不建议一起打包,因为我们要使用TreeShaking,没必要打包不用的代码,否则只会增加批量。强依赖包做好了,根据第二个思路对业务代码进行打包。我们使用基于路由的代码拆分进行捆绑。思路很简单,用户访问哪个页面,就下载该页面对应的js。没有必要将未访问的页面打包在一起,这样不仅会增加体积,还会增加下载时间,用户体验也会相应变差。.自定义webpack配置如果我们想使用dll将强依赖包打包到vendor中,就需要使用webpack的功能。AngularCLI内嵌了webpack,但这些配置对我们来说都是黑盒子。Angular允许我们自定义webpack配置,步骤如下安装@angular-builders/custom-webpack和@angular-devkit/build-angular新建一个webpack.extra.config.ts用于webpack配置,并在其中做如下改动角.json。.."architect":{"build":{"builder":"@angular-builders/custom-webpack:browser","options":{..."customWebpackConfig":{//引用webpack配置extended"path":"./webpack.extra.config.ts",//是否替换重复插件"replaceDuplicatePlugins":true}}},"serve":{"builder":"@angular-builders/custom-webpack:dev-server","options":{"browserTarget":"angular-webpack:build"}}...使用DLL自定义webpack配置后,新建webpack.dll.js文件写入DLL配置:constpath=require("path");constwebpack=require("webpack");module.exports={mode:"production",entry:{供应商:["@angular/platform-b??rowser","@angular/platform-b??rowser-dynamic","@angular/common","@angular/core","@angular/forms","@angular/router"],},输出:{路径:path.resolve(__dirname,"./dll"),文件名:"[name].dll.js",library:"[name]_library",},plugins:[newwebpack.DllPlugin({context:path.resolve(__dirname,"."),path:path.join(__dirname,"./dll","[name]-manifest.json"),name:"[name]_library",}),],};然后在webpack.extra.config.ts中导入dllimport*aspathfrom'path';import*aswebpackfrom'webpack';exportdefault{plugins:[newwebpack.DllReferencePlugin({manifest:require('./dll/vendor-manifest.json'),上下文:path.resolve(__dirname,'.'),})],}aswebpack.Configuration;最后在package.json中添加一条打包dll的命令:“dll”:“rm-rfdll&&webpack--configwebpack.dll.js”,执行npmrundll后,在项目根目录下会有一个dll文件夹,里面有打包后的内容:打包完成后,我们会在项目vendor.dll.js中使用,在angular.json中配置:"architect":{..."build":{..."options":{..."scripts":[{"input":"./dll/vendor.dll.js","inject":true,"bundleName":"vendor_library"}]}}}打包后可以看到引入了vendor_library.js:DLL的使用就是不会经常用更新的强依赖包打包合并成一个js文件,一般用来打包Angular框架本身。当用户第一次访问时,浏览器会下载vendor_library.js并缓存。每次访问后,直接从缓存中取出。浏览器只会下载业务代码的js,不会下载框架相关的代码,大大提高了应用的加载速度,提升了用户体验。ps:vendor_library后面的hash只有在打包的时候改变了里面的代码才会再次改变,否则不会改变。路由级的CodeSplitingDLL管理框架部分的代码。下面我们看看如何在Angular中实现路由级别的页面按需加载。对于上下文,您如何在React或Vue中进行路由级代码拆分?大概是这样的:{path:'/home',component:()=>import('./home')}其中home指向对应的组件,但是这个方法不能在Angular中使用,只有在Module中才是单位代码拆分:{path:'/home',loadChild:()=>import('./home.module').then(m=>m.HomeModule)}然后在具体模块中使用路由访问具体components:import{HomeComponent}from'./home.component'{path:'',component:HomeComponent}虽然不能直接在router中import()组件,但是Angular提供了组件动态导入的功能:@Component({selector:'app-home',template:``,})exportclassHomeContainerComponentimplementsOnInit{constructor(privatevcref:ViewContainerRef,privatecfr:ComponentFactoryResolver){}ngOnInit(){this.loadGreetComponent()}asyncloadGre(){this.vcref.clear();//使用import()延迟加载组件const{HomeComponent}=awaitimport('./home.component');让createdComponent=this.vcref.createComponent(this.cfr.resolveComponentFactory(HomeComponent));这样,当一个路由访问一个页面时,只要让被访问的页面可以用import()配合组件动态导入,不就可以实现页面lazyLoad的效果了吗?答案是肯定的,但是会有一个很大的问题:在lazyLoaded组件中,它的内容只是当前组件的代码,并没有引用其他模块中组件的代码。原因是一个Angular应用由多个模块组成,每个模块中需要的功能可能来自其他模块。比如在A模块中使用table组件,需要从ng-zorro-antd/table模块中取出table。在打包的时候,Angular不像React或Vue那样可以将当前组件和其他一起使用的包一起打包。以React为例:当A组件引入表格组件时,表格代码在打包时会被打包到A组件中。在Angular中,在A组件中使用table组件,使用imprt()动态加载A组件时,打包后的A组件不包含table代码,而是将table代码打包到当前模块中。如果一个模块包含多个页面,那么多页面使用了很多UI组件,那么打包出来的模块肯定会很大。那么就没有别的办法了吗?答案是肯定的,就是将每个页面拆分成一个模块,每个页面使用的其他模块或组件由当前页面对应的模块承担。上图中的dashboard是一个module,下面有两个页面,分别是monitor和welcomeashboard.module.ts:import{NgModule}from'@angular/core';import{CommonModule}from'@angular/common';从'@angular/router'导入{RouterModule,Routes};constroutes:Routes=[{path:'welcome',loadChildren:()=>import('./welcome/welcome.module').then((m)=>m.WelcomeModule),},{路径:'monitor',loadChildren:()=>import('./monitor/monitor.module').then((m)=>m.MonitorModule),},];@NgModule({imports:[CommonModule,RouterModule.forChild(routes)],exports:[RouterModule],declarations:[],})exportclassDashboardModule{}使用模块中的路由loadChildren来lazyLoad两个页面模块,现在再看WelcomeModule:import{NgModule}from'@angular/核';从'@angular/common'导入{CommonModule};从'./welcome.component'导入{WelcomeComponent};从'@angular/router'导入{RouterModule,Routes};constroutes:Routes=[{path:'',component:WelcomeComponent}];@NgModule({declarations:[WelcomeComponent],imports:[RouterModule.forChild(routes),CommonModule]})exportclassWelcomeModule{}就这么简单,在需要使用外部组件的时候完成页面级的lazyLoad,比如tablecomponents,只要在imports中引入即可:import{NzTableModule}from'ng-zorro-antd/table';@NgModule({...imports:[...,NzTableModule]})exportclassWelcomeModule{}题外话:我比较喜欢React的拆分方式。比如在React中使用了table组件。表格组件本身的代码量比较大。如果很多页面使用表格,每个页面都会有表格代码,造成不必要的浪费。所以可以结合import()将table组件拉出来。打包时,表格被浏览器下载为一个单独的js,提供给需要的页面。所有页面都可以共享这个js。但是Angular不能,它不能在模块的导入中import()一个模块。后续以上是对项目代码的合理拆分,后续会对Angular性能进行合理优化,主要从编译模式、变更检测、ngFor、Worker等角度进行。另外我会单独写一篇关于Angular状态管理的文章,敬请期待
