開發腳手架與自動化構建工作流封裝
去年6月24號開始工作,到今天剛好一週年了,紀念一下,分享最近學習的前端工程化筆記。
一、前端工程化
前端工程化是指遵循一定的標準和規範,通過工具去提高效率、降低成本的一種手段。
1. 前端開發中遇到的問題
- 想要使用ES6+新特性,但是兼容有問題
- 想要使用Less/Sass/PostCSS增強CSS的編程性,但是運行環境不能直接支持
- 想要使用模塊化的方式提高項目的可維護性,但運行環境不能直接支持
- 部署上線前需要手動壓縮代碼及資源文件,部署過程需要手動上傳代碼到服務器
- 多人協同開發,無法硬性統一大家的代碼風格,從倉庫中pull回來的代碼質量無法保證
2. 主要解決的問題
- 傳統語言或語法的弊端
- 無法使用模塊化/組件化
- 重複的機械式工作
- 代碼風格統一、質量保證
- 依賴後端服務接口支持
- 整體依賴後端項目
3. 工程化表現
- 創建項目
- 創建項目結構
- 創建特定類型文件
- 編碼
- 格式化代碼
- 校驗代碼風格
- 編譯/構建/打包
- 預覽/測試
+ Web Server / Mock- Live Reloading / HMR
- Source Map
- 提交
- Git Hooks
- Lint-staged
- 持續集成
- 部署
- CI / CD
- 自動發佈
4. 工程化不等於某個具體工具
工具並不是工程化的核心,工程化的核心是對項目的整體規劃或架構,工具只是落地和實現工程化的一個手段
一些成熟的工程化集成:
- create-react-app
- vue-cli
- angular-cli
- gatsby-cli
上面的幾個是某個項目的官方提供的集成化方案
5. 工程化與Node.js
工程化工具都是Node.js開發的
二、腳手架工具
腳手架的本質作用就是創建項目基礎結構、提供項目規範和約定。
1. 腳手架工具的作用
因爲在前端工程中,可能會有:
- 相同的組織結構
- 相同的開發範式
- 相同的模塊依賴
- 相同的工具配置
- 相同的基礎代碼
腳手架就是解決上面問題的工具,通過創建項目骨架自動的執行工作。IDE創建項目的過程就是一個腳手架的工作流程。
由於前端技術選型比較多樣,又沒有一個統一的標準,所以前端腳手架不會集成在某一個IDE中,一般都是以一個獨立的工具存在,相對會複雜一些。
2. 常用的腳手架工具
-
第一類腳手架是根據信息創建對應的項目基礎結構,適用於自身所服務的框架的那個項目。
- create-react-app
- vue-cli
- Angular-cli
-
第二類是像Yeoman爲代表的通用型腳手架工具,會根據模板生成通用的項目結構,這種腳手架工具很靈活,很容易擴展。
-
第三類以Plop爲代表的腳手架工具,是在項目開發過程中,創建一些特定類型的組件,例如創建一個組件/模塊所需要的文件,這些文件一般都是由特定結構組成的,有相同的結構。
3. 通用腳手架工具剖析
(1)Yeoman + Generator
Yeoman是最老牌、最強大、最通用的腳手架工具,是創建現代化應用的腳手架工具,不同於vue-cli,Yeoman更像是腳手架運行平臺,我們可以通過Yeoman搭配不同的Generator去創建任何類型的項目,我們可以創建我們自己的Generator,從而去創建我們自己的前端腳手架。缺點是,在框架開發的項目中,Yeoman過於通用不夠專注。
如果使用Yeoman:
- 在電腦上全局安裝Yeoman:
yarn global add yo
- Yeoman要搭配相應的Generator創建任務,所以要安裝Generator。例如是創建node項目,則安裝generator-node:
yarn global add generator-node
- 創建一個空文件夾:
mkdir my-module
, 然後進入文件夾:cd my-module
- 通過Yeoman的yo命令安裝剛纔的生成器(去掉生成器名字前的generator-):
yo node
- 交互模式填寫一些項目信息,會生成項目基礎結構,並且生成一些項目文件,然後自動運行
npm install
安裝一些項目依賴。
(2)SubGenerator
有時候我們可能不需要創建一個完成的項目結構,而是在已有項目的基礎上,創建一些項目文件,如README.md,或者是創建一些特定類型的文件,如ESLint、Babel配置文件
- 運行SubGenerator的方式就是在原有Generator基礎上加上:SubGenerator的名字,如:
yo node:cli
- 在使用SubGenerator前,要先去查看一下Generator之下有哪些SubGenerator
(3)Plop
Plop是一個小而美的腳手架工具,通常用於創建項目中特定類型文件的小工具,一般是把Plop集成到項目中,用來自動化創建同類型的項目文件。
如何使用Plop創建文件:
- 將plop模塊作爲項目開發依賴安裝
- 在項目根目錄下創建一個plopfile.js文件
- 在plopfile.js文件中定義腳手架任務
- 編寫用於生成特定類型文件的模板
- 通過Plop提供的cli運行腳手架任務
4. 腳手架工作原理
腳手架的工作原理就是在啓動腳手架之後,回自動地去詢問一些預設問題,通過回答的結果結合一些模板文件,生成項目的結構。
使用NodeJS開發一個小型的腳手架工具:
-
用
yarn init
初始化一個空文件夾:sample-scaffolding
-
在
package.json
中添加bin
屬性指定腳手架的命令入口文件爲cli.js
{
"name": "sample-scaffolding",
"version": "1.0.0",
"main": "index.js",
"bin": "cli.js",
"license": "MIT",
"dependencies": {
"ejs": "^3.1.3",
"inquirer": "^7.1.0"
}
}
- 編寫
cli.js
#!/usr/bin/env node
// Node CLI 應用入口文件必須要有這樣的文件頭
// 如果Linux 或者 Mac 系統下,還需要修改此文件權限爲755: chmod 755 cli.js
// 腳手架工作過程:
// 1. 通過命令行交互詢問用戶問題
// 2. 根據用戶回答的結果生成文件
const path = require('path')
const fs = require('fs')
const inquirer = require('inquirer') // 發起命令行交互詢問
const ejs = require('ejs') // 模板引擎
inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'Project name?'
}
]).then(answer => {
console.log(answer)
// 模板目錄
const tempDir = path.join(__dirname, 'templates')
// 目標目錄
const destDir = process.cwd()
// 將模板下的文件全部轉換到目標目錄
fs.readdir(tempDir, (err, files) => {
if (err) throw err
files.forEach(file => {
// 通過模板引擎渲染文件
ejs.renderFile(path.join(tempDir, file), answer, (err, result) => {
if(err) throw err
// 將結果寫入到目標目錄
fs.writeFileSync(path.join(destDir, file), result)
})
})
})
})
- 將該cli程序link到全局:
yarn link
- 然後再其他文件夾中執行:
sample-scaffolding
命令,就可以根據模板自動化創建文件了。
5. 自定義Generator開發腳手架
|-- generators/ ······生成器目錄
| |-- app/ ······默認生成器目錄
| |–templates ······模板文件夾
| |–foo.txx ······模板文件
| |–index.js ······默認生成器實現
| |–component/ ······其他生成器目錄
| |–index.js ······其他生成器實現
|–package.json ······模塊包配置文件
注意:Yeoman的生成器名稱必須是
generator-<name>
,安裝生成器的時候,就執行yo <name>
創建Generator生成器的步驟:
-
mkdir generator-sample
-
cd generator-sample
-
yarn init
-
yarn add yeoman-generator
-
創建文件:generators/app/index.jsx
// 此文件作爲Generator的核心入口 // 需要導出一個集成字Yeoman Generator的類型 // Yeoman Generator在工作時會自動調用我們在此類型中定義的一些生命週期方法 // 我們在這些方法中可以通過調用父類提供的一些工具方法實現一些功能,比如文件寫入 const Generator = require('yeoman-generator') module.exports = class extends Generator { prompting () { // Yeoman 在詢問用戶環節會自動調用次方法 // 在此方法中可以調用父類的prompt()方法發出對用戶命令行詢問 return this.prompt([ { type: 'input', name: 'name', message: 'Your project name', default: this.appname // appname爲項目生成目錄 } ]).then( answers => { // answers => {name: 'user input value'} this.answers = answers }) } writing () { // Yeoman 自動在生成文件階段調用次方法 // 我們這裏嘗試往項目目錄中寫入文件 // this.fs.write( // this.destinationPath('temp.txt'), // Math.random().toString() // ) // 通過模板方法導入文件到目標目錄 // 模板文件路徑 const tmpl = this.templatePath('foo.txt') // 輸出目標路徑 const output = this.destinationPath('foo.txt') // 模板數組上下文 const context = {title: 'Hello', success: false} // const context = this.answers // 從命令行獲取的參數 this.fs.copyTpl(tmpl, output, context) } }
-
templates/foo.txt作爲模板文件
這是一個模板文件 內部可以使用EJS模板標記輸出數據 例如:<%= title %> 其他的EJS語法也支持 <%if (success) {%> hello world <%}%>
-
執行
yarn link
, 此時這個模塊就會作爲全局模塊被link到全局,別的項目可以直接使用它。 -
創建一個別的文件夾my-proj, 在這個文件夾中執行:
yo sample
-
發佈到npmjs網站上:
yarn publish --registry=https://registry.yarnpkg.com
5. Plop
yarn add plop
plopfile.js
// Plop 入口文件,需要導入一個函數
// 此函數接受一個plop對象,用戶創建生成器任務
module.exports = plop => {
plop.setGenerator('component', {
description: 'create a component',
prompts: [
{
type: 'input',
name: 'name',
message: 'component name',
default: 'MyComponent'
}
],
actions: [
{
type: 'add', // 代表添加文件
path: 'src/components/{{name}}/{{name}}.js',
templateFile: 'plop-templates/component.hbs'
},
{
type: 'add', // 代表添加文件
path: 'src/components/{{name}}/{{name}}.css',
templateFile: 'plop-templates/component.css.hbs'
},
]
})
}
編寫模板:
component.hbs:
import React from 'react';
export default () => (
<div className="{{name}}">
<h1>{{name}} Component</h1>
</div>
)
Component.css.hbs:
import React from 'react';
import ReactDOM from 'react-dom';
import {{name}} from './{{name}}';
it('renders without crashing', () => {
const div = documents.createElement('div');
ReactDOM.render(<{{name}}/>, div);
ReactDOM.unmountComponentAtNode(div)
})
執行命令:yarn plop component
三、自動化構建
源代碼自動化構建成生產代碼,也稱爲自動化構建工作流。
使用提高效率的語法、規範和標準,如:ECMAScript Next、Sass、模板引擎,這些用法大都不被瀏覽器直接支持,自動化工具就是解決這些問題的,構建轉換那些不被支持的特性。
1. NPM Scripts
在package.json中增加一個scripts對象,如:
{
"scripts": {
"build": "sass scss/main.scss css/style.css"
}
}
scripts可以自動發現node_modules裏面的命令,所以不需要寫完整的路徑,直接寫命令的名稱就可以。然後可以通過npm或yarn運行scripts下面的命令名稱,npm用run啓動,yarn可以省略run,如:npm run build
或yarn build
NPM Scripts是實現自動化構建工作流的最簡方式。
{
"scripts": {
"build": "sass scss/main.scss css/style.css",
"preserve": "yarn build",
"serve": "browser-sync ."
}
}
preserve是一個鉤子,保證在執行serve之前,會先執行build,使樣式先處理,然後再執行serve。
通過--watch
可以監聽sass文件的變化自動編譯,但是此時sass命令在工作時,命令行會阻塞,去等待文件的變化,導致了後面的serve無法去工作,此時就需要同時去執行多個任務,要安裝npm-run-all
這個模塊
{
"scripts": {
"build": "sass scss/main.scss css/style.css --watch",
"serve": "browser-sync .",
"start": "run-p build serve"
}
}
運行npm run start命令,build和serve就會被同時執行。
2. Grunt
Grunt是最早的前端構建系統,它的插件生態非常完善,它的插件可以幫你完成任何你想做的事情。由於Grunt工作過程是基於臨時文件去實現的,所以會比較慢。
如何使用Grunt:
- 安裝grunt:
yarn add grunt
,編寫gruntfile.js文件,下面舉例grunt任務的幾種用法:
// Grunt的入口文件
// 用於定義一些需要Grunt自動執行的任務
// 需要導出一個函數
// 此函數接受一個grunt的形參,內部提供一些創建任務時可以用到的API
module.exports = grunt => {
grunt.registerTask('foo', () => {// 第一個參數是任務名字,第二個參數接受一個回調函數,是指定任務的執行內容,執行命令是yarn grunt foo
console.log('hello grunt ~')
})
grunt.registerTask('bar', '任務描述', () => { // 如果第二個參數是字符串,則是任務描述,執行命令是yarn grunt bar
console.log('other task~')
})
grunt.registerTask('default', () => { // 如果任務名稱是'default',則爲默認任務,grunt在運行時不需要執行任務名稱,自動執行默認任務,執行命令是yarn grunt
console.log('default task')
})
grunt.registerTask('default', ['foo', 'bad', 'bar']) // 一般用default映射其他任務,第二個參數傳入一個數組,數組中指定任務的名字,grunt執行默認任務,則會依次執行數組中的任務,執行命令是yarn grunt
// grunt.registerTask('async-task', () => {
// setTimeout(() => {
// console.log('async task working')
// }, 1000);
// })
// 異步任務,done()表示結束
grunt.registerTask('async-task', function () { // grunt代碼默認支持同步模式,如果需要異步操作,則需要通過this.async()得到一個回調函數,在你的異步操作完成過後,去調用這個回調函數,標記這個任務已經被完成。知道done()被執行,grunt纔會結束這個任務的執行。執行命令是yarn grunt async-task
const done = this.async()
setTimeout(() => {
console.log('async task working..')
done()
}, 1000);
})
// 失敗任務
grunt.registerTask('bad', () => { // 通過return false標誌這個任務執行失敗,執行命令是yarn grunt bad。如果是在任務列表中,這個任務的失敗會導致後序所有任務不再被執行,執行命令是yarn grunt。可以通過--force參數強制執行所有的任務,,執行命令是yarn grunt default --force
console.log('bad working...')
return false
})
// 異步失敗任務,done(false)表示任務失敗,執行命令是yarn grunt bad-async-task
grunt.registerTask('bad-async-task', function () {
const done = this.async()
setTimeout(() => {
console.log('bad async task working..')
done(false)
}, 1000);
})
}
-
grunt配置選項
module.exports = grunt => { grunt.initConfig({ // 對象的屬性名一般與任務名保持一致。 // foo: 'bar' foo: { bar: 123 } }) grunt.registerTask('foo', () => { // console.log(grunt.config('foo')) // bar console.log(grunt.config('foo.bar')) // 123.grunt的config支持通過foo.bar的形式獲取屬性值,也可以通過獲取foo對象,然後取屬性 }) }
-
多目標任務(相當於子任務)
module.exports = grunt => { grunt.initConfig({ // 與任務名稱同名 build: { options: { // 是配置選項,不會作爲任務 foo: 'bar' }, // 每一個對象屬性都是一個任務 css: { options: { // 會覆蓋上層的options foo: 'baz' } }, // 每一個對象屬性都是一個任務 js: '2' } }) // 多目標任務,可以讓任務根據配置形成多個子任務,registerMultiTask方法,第一個參數是任務名,第二個參數是任務的回調函數 grunt.registerMultiTask('build', function () { console.log(this.options()) console.log(`build task: ${this.target}, data: ${this.data}`) }) }
執行命令:
yarn grunt build
, 輸出結果:Running "build:css" (build) task { foo: 'baz' } build task: css, data: [object Object] Running "build:js" (build) task { foo: 'bar' } build task: js, data: 2
-
grunt插件使用
插件機制是grunt的核心,因爲很多構建任務都是通用的,社區當中也就出現了很多通用的插件,這些插件中封裝了很多通用的任務,一般情況下我們的構建過程都是由通用的構建任務組成的。先去npm中安裝 需要的插件,再去gruntfile中使用grunt.loadNpmTasks方法載入這個插件,最後根據插件的文檔完成相關的配置選項。
例如使用clean插件,安裝
yarn add grunt-contrib-clean
,用來清除臨時文件。module.exports = grunt => { // 多目標任務需要通過initConfig配置目標 grunt.initConfig({ clean: { temp: 'temp/**' // ** 表示temp下的子目錄以及子目錄下的文件 } }) grunt.loadNpmTasks('grunt-contrib-clean') }
執行:
yarn grunt clean
,就會刪除temp文件夾 -
Grunt常用插件總結:
- grunt-sass
- grunt-babel
- grunt-watch
const sass = require('sass') const loadGruntTasks = require('load-grunt-tasks') module.exports = grunt => { grunt.initConfig({ sass: { options: { sourceMap: true, implementation: sass, // implementation指定在grunt-sass中使用哪個模塊對sass進行編譯,我們使用npm中的sass }, main: { files: { 'dist/css/main.css': 'src/scss/main.scss' } } }, babel: { options: { presets: ['@babel/preset-env'], sourceMap: true }, main: { files: { 'dist/js/app.js': 'src/js/app.js' } } }, watch: { js: { files: ['src/js/*.js'], tasks: ['babel'] }, css: { files: ['src/scss/*.scss'], tasks: ['sass'] } } }) // grunt.loadNpmTasks('grunt-sass') loadGruntTasks(grunt) // 自動加載所有的grunt插件中的任務 grunt.registerTask('default', ['sass', 'babel', 'watch']) }
3. Gulp
Gulp是目前世界上最流行的前端構建系統,其核心特點就是高效、易用。它很好的解決了Grunt中讀寫磁盤慢的問題,Gulp是基於內存操作的。Gulp支持同時執行多個任務,效率自然大大提高,而且它的使用方式相對於Grunt更加易懂,而且Gulp的生態也非常完善,所以後來居上,更受歡迎。
- gulp的使用
安裝gulp:yarn add gulp,然後編寫gulpfile.js,通過導出函數成員的方式定義gulp任務
// gulp的入口文件
exports.foo = done => {
console.log('foo task working...')
done() // 使用done()標識任務完成
}
exports.default = done => {
console.log('default task working...')
done()
}
執行命令:yarn gulp foo執行foo任務, 或者yarn gulp執行默認任務default
gulp4.0之前的任務寫法:
const gulp = require('gulp')
gulp.task('bar', done => {
console.log('bar working...')
done()
})
執行命令yarn gulp bar可以運行bar任務,gulp4.0之後也保留了這個API,但是不推薦使用了
-
gulp創建組合任務:series串行、parallel並行
const {series, parallel} = require('gulp') // gulp的入口文件 exports.foo = done => { console.log('foo task working...') done() // 標識任務完成 } exports.default = done => { console.log('default task working...') done() } const task1 = done => { setTimeout(() => { console.log('task1 working...') done() }, 1000); } const task2 = done => { setTimeout(() => { console.log('task2 working...') done() }, 1000); } const task3 = done => { setTimeout(() => { console.log('task3 working...') done() }, 1000); } // series 串行執行 // exports.bar = series(task1, task2, task3) // parallel 並行執行 exports.bar = parallel(task1, task2, task3)
-
Gulp的異步任務:
const fs = require('fs') exports.callback = done => { console.log('callback task...') done() // 通過使用done()標誌異步任務執行結束 } exports.callback_error = done => { console.log('callback task...') done(new Error('task failed!')) // done函數也是錯誤優先回調函數。如果這個任務失敗了,後序任務也不會工作了 } exports.promise = () => { console.log('promise task...') return Promise.resolve() // resolve執行的時候,表示異步任務執行結束了。resolve不需要參數,因爲gulp會忽略它的參數 } exports.promise_error = () => { console.log('promise task...') return Promise.reject(new Error('task failed')) // reject標誌這是一個失敗的任務,後序的任務也會不再執行 } const timeout = time => { return new Promise(resolve => { setTimeout(resolve, time); }) } exports.async = async() => { await timeout(1000) // 在node8以上可以使用async和await,await的就是一個Promise對象 console.log('async task...') } exports.stream = (done) => { // 最常用的就是基於stream的異步任務 const readStream = fs.createReadStream('package.json') const writeSteam = fs.createWriteStream('temp.txt') readStream.pipe(writeSteam) return readStream // 相當於下面的寫法 // readStream.on('end', () => { // done() // }) }
-
Gulp構建過程,例子:壓縮CSS
const fs = require('fs') const {Transform} = require('stream') exports.default = () => { const read = fs.createReadStream('normalize.css') const write = fs.createWriteStream('normalize.min.css') // 文件轉化流 const transform = new Transform({ transform: (chunk, encoding, callback) => { // 核心轉化過程 // chunk => 讀取流中讀取的內容(Buffer ) const input = chunk.toString() // 轉化空白符和註釋 const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '') callback(null, output) } }) read .pipe(transform) // 先轉化 .pipe(write) return read }
-
Gulp文件api
const {src, dest} = require('gulp') const cleanCss = require('gulp-clean-css') const rename = require('gulp-rename') exports.default = () => { return src('src/*.css') .pipe(cleanCss()) .pipe(rename({ extname: '.min.css' })) .pipe(dest('dist')) }
-
Gulp構建
// 實現這個項目的構建任務 const {src, dest, parallel, series, watch} = require('gulp') const del = require('del') const browserSync = require('browser-sync') const bs = browserSync.create() const loadPlugins = require('gulp-load-plugins') const plugins = loadPlugins() const {sass, babel, swig, imagemin} = plugins const data = { menus: [ { name: 'Home', icon: 'aperture', link: 'index.html' }, { name: 'Features', link: 'features.html' }, { name: 'About', link: 'about.html' }, { name: 'Contact', link: '#', children: [ { name: 'Twitter', link: 'https://twitter.com/w_zce' }, { name: 'About', link: 'https://weibo.com/zceme' }, { name: 'divider' }, { name: 'About', link: 'https://github.com/zce' } ] } ], pkg: require('./package.json'), date: new Date() } const clean = () => { return del(['dist', 'temp']) } const style = () => { return src('src/assets/styles/*.scss', { base: 'src' }) .pipe(sass({ outputStyle: 'expanded' })) .pipe(dest('temp')) .pipe(bs.reload({stream: true})) } const script = () => { return src('src/assets/scripts/*.js', { base: 'src' }) .pipe(babel({ presets: ['@babel/preset-env'] })) .pipe(dest('temp')) .pipe(bs.reload({stream: true})) } const page = () => { return src('src/**/*.html', {base: 'src'}) .pipe(swig(data)) .pipe(dest('temp')) .pipe(bs.reload({stream: true})) } const image = () => { return src('src/assets/images/**', {base: 'src'}) .pipe(imagemin()) .pipe(dest('dist')) } const font = () => { return src('src/assets/fonts/**', {base: 'src'}) .pipe(imagemin()) .pipe(dest('dist')) } const extra = () => { return src('public/**', {base: 'public'}) .pipe(dest('dist')) } const serve = () => { watch('src/assets/styles/*.scss', style) watch('src/assets/scripts/*.js', script) watch('src/*.html', page) watch([ 'src/assets/images/**', 'src/assets/fonts/**', 'public/**' ], bs.reload) bs.init({ notify: false, port: 2080, open: false, // files: 'temp/**', server: { baseDir: ['temp', 'src', 'public'], // 按順序查找 routes: { '/node_modules': 'node_modules' } } }) } const useref = () => { return src('temp/*.html', { base: 'temp' }) .pipe(plugins.useref({ searchPath: ['temp', '.'] })) .pipe(plugins.if(/\.js$/, plugins.uglify())) .pipe(plugins.if(/\.css$/, plugins.cleanCss())) .pipe(plugins.if(/\.html$/, plugins.htmlmin({ collapseWhitespace: true, minifyCSS: true, minifyJS: true }))) .pipe(dest('dist')) } // const compile = parallel(style, script, page, image, font) const compile = parallel(style, script, page) // 上線之前執行的任務 const build = series( clean, parallel( series(compile, useref), image, font, extra ) ) // 開發階段 const develop = series(compile, serve) module.exports = { clean, compile, build, develop, }
其中依賴文件如下:
"devDependencies": { "@babel/core": "^7.10.2", "@babel/preset-env": "^7.10.2", "browser-sync": "^2.26.7", "del": "^5.1.0", "gulp": "^4.0.2", "gulp-babel": "^8.0.0", "gulp-clean-css": "^4.3.0", "gulp-htmlmin": "^5.0.1", "gulp-if": "^3.0.0", "gulp-imagemin": "^7.1.0", "gulp-load-plugins": "^2.0.3", "gulp-sass": "^4.1.0", "gulp-swig": "^0.9.1", "gulp-uglify": "^3.0.2", "gulp-useref": "^4.0.1" },
-
Gulp補充
4. FIS
FIS是百度的前端團隊推出的構建系統,FIS相對於前兩種微內核的特點,它更像是一種捆綁套餐,它把我們的需求都儘可能的集成在內部了,例如資源加載、模塊化開發、代碼部署、甚至是性能優化。正式因爲FIS的大而全,所以在國內流行。FIS適合初學者。
全局安裝:yarn global add fis3
執行fis3 release