简答题
一、谈谈你对工程化的初步认识,结合你之前遇到过的问题说出三个以上工程化能够解决的问题或者带来的价值。
1、我下面从三个认知角度来谈谈我对“工程化”的认识:
第一个角度 What ,即什么是工程化? 具体来说是什么是前端项目工程化?前端项目工程化是组织大型前端项目开发的一种技术框架。它将各种前端技术解决方案进行合理搭配,按照一定的标准和规范通过工具来实现提高前端开发效率降低开发成本的目的;
第二个角度 Why ,即为什么会有前端工程化?技术解决方案往往都是为解决问题而存在,前端工程化同样也是为了解决在前端开发上所遇见的通用问题而诞生的。目前前端开发尤其大型前端项目开发中所面临的问题有:
- 想要使用ES6+新特性,但是兼容有问题;
- 想要使用Less/Sass/PostCss增强css的编程性但是运行环境不能直接支持;
- 想要使用模块化的方式提高项目的可维护性但是运行环境不能直接支持;
- 部署上线前需要手动压缩代码及资源文件;
- 部署过程中需要手动需要手动上传代码到服务器;
- 多人开发,无法硬性统一大家的代码风格;
- 从仓库中pull回来的代码质量无法保证;
- 部分功能开发时需要等待后端服务接口提前完成;
总结下来有下面几大类:
- 传统语言或语法的弊端(js、css,HTML 语言本身的局限性);
- 无法使用模块化或组件化(js、css,HTML 语言本身的局限性);
- 重复的机械式工作(在整个项目开发阶段出现的各种重复性工作);
- 代码风格统一、质量保证(前端人员之间在开发过程中组员之间的协同开发问题);
- 依赖后端服务接口支持(前端与后段的联调、联测);
- 整体依赖后端项目(前端与后段的联调、联测);
第三个角度 How ,即如何使用前端工程化思想组织前端项目开发?工程化的表现就是一切以提高效率、降低成本、质量保证为目的的手段都属于工程化,就是将一切重复的工作都可以实现自动化。前端项目工程开发阶段大体有这么几个阶段,创建项目、编码、预览/测试、提交、部署。工程化思想在各个阶段的具体实现:
创建项目:
- 创建项目结构:使用脚手架工具自动的完成基础结构的搭建
- 创建特定类型文件
编码:
- 代码格式化
- 校验代码风格
- 编译/构建/打包
预览/测试:
- WebServer / Mock :提供热更新
- Live Reloading / HMR
- Source Map
提交:
- Git Hooks
- Lint-staged
- 持续集成
部署:
- CI / CD
- 自动发布
2、结合我之前开发中所遇到的问题谈谈工程化能够解决的问题和带来的价值
**案例一、**我们有个项目需要发布在两个服务上这里我们称作A平台和B平台,同时A、B两个平台又各自包含两个系统即生产系统和测试系统。就是说在发布的时候要根据不同的系统配置很多文件,原来都是用手动更改配置文件的方式来发布。学完课程后使用自定的 node cli 工具的方式,将要配置的文件放在 templates 文件并通过命令行交互的方法完成配置文件的配置操作。带来的价值就是可以提高项目发布的效率,将原来枯燥易出错的手动配置操作,改成了自动化的操作。具体实现步骤在编程题第一题;
**案例二、**我们有个后台管理的项目前端框架是用的 vue,并且大部分页面都是包含一个搜索条件栏、列表及分页。根据这个特点创建了一个页面模版,并使用前端脚手架工具 Plop 实现快速搭建页面的需求。
module.eaports = plop => {
plop.setGenerator('newPage', {
description: '创建查询表格页',
prompts: [
{
type: 'input',
name: 'pagename'
}, {
type: 'input',
name: 'pagetitle'
}
],
actions: [
{
type: 'add',
path: 'src/pages/{{pagename}}/index.vue',
templateFile: 'plop-templates/page.hbs'
}, {
type: 'add',
path: 'src/pages/{{pagename}}/config.js',
templateFile: 'plop-templates/config.js.hbs'
}
]
})
}
二、你认为脚手架除了为我们创建项目结构,还有什么更深的意义?
前端工程化在项目创建阶段主要依靠脚手架工具,可以说脚手架工具是前端工程化的发起者。脚手架可以简单的理解为,自动的创建项目基础文件的一个工具。除了创建文件,它更多的是给开发者提供一种约定和规范。所以脚手架的本质作用就是创建项目的基础结构、提供项目规范和约定。通常被用于开发相同类型项目提供相同的约定,包括相同的文件组织结构、相同的代码开发范式、相同的模块依赖甚至还有一些相同的工具配置和一些基础的代码。脚手架可以快速搭建相同类型的项目骨架,然后基于这个骨架进行后续的开发工作。IDE
创建项目的过程就是一个脚手架的工作流程。脚手架工具分为两类,一类是特定项目类型的脚手架,像创建react
项目的 creat-react-app
,它会根据提供的信息创建对应的项目基础结构;一类是通用类型的脚手架,像Yeoman,他们可以根据模版一套模版生成一个对应的项目结构,这种类型的脚手架一般都很灵活而且容易扩展,这些都是在项目创建阶段用的脚手架。还有一种在项目开发过程中用到的叫Plop
,用于去创建一些特定类型的文件。例如在组件化的项目当中创建新的组件或者模块化项目当中创建一个新的模块,这些组件或模块都是由特定的几个文件组成的,而且每个文件都有一些基本的代码结构。相对于手动一个一个去创建的化脚手架会提供更为便捷的、更为稳定的一种操作方式。
编程题
一、概述脚手架实现的过程,并使用 NodeJS 完成一个自定义的小型脚手架工具
1、创建一个空项目文件夹并初始化一个项目
mkdir usepublish && cd usepublish
yarn init --yes
2、创建 cli.js 文件
#!/usr/bin/env node
const ejs = require('ejs'),
path = require('path'),
inquirer = require('inquier'),
templates = [
'src/main.js',
'src/common/Config.js'
];
inquirer.prompt([
{
type: 'list',
name: 'plant',
message: '请选择发布平台',
choices: [
{
name: 'A平台',
value: 0
}, {
name: 'B平台',
value: 1
}
]
}, {
type: 'list',
name: 'system',
message: '请选择发布系统',
choices: [
{
name: '生产系统',
value: 'prod'
}, {
name: '测试系统',
value: 'test'
}
]
}
]).then(answers => {
const templDir = path.join(__dirname, 'templates')
const destDir = process.cwd()
templates.forEach(file => {
ejs.renderFile(path.join(templDir, file), answers, (err, result) => {
if (err) throw err
fs.writeFileSync(path.join.(destDir, file), result)
})
})
})
3、在根目录添加 templates 文件夹,并新增 main.js、src/common/Config.js;
4、在 package.json 添加 bin 字段
"bin": {
"usepublish": "cli.js"
}
5、将项目 link 到全局
yarn link
6、在新项目使用 usepublish 命令
二、尝试使用 Gulp 完成项目的自动化构建
1、样式编译
在项目架构阶段很重要的一个工作就是,设置项目文件目录结构。对于不需要经过转换,直接拷贝就可以的资源文件一般都是放在 public
下面;对于开发阶段所编写的代码都是放在 src
目录中,这个目录下所有文件都会被构建对于html文件可以使用模版语法、对于样式文件可以使用sass
语法、脚本文件也可以使用ES6
语法、图片和字体文件需要被自动压缩。
const {src, dest} = require('gulp')
const sass = require('gulp-sass')
const style = () => {
return src('src/assets/style/*.scss', {base:'src'})
.pipe(sass({ outputStyle: 'expanded'}))
.pipe(dest('dist'))
}
module.exports = {
style
}
2、脚本编译
脚本文件的构建,主要是通过bable
将使用 ES6+
语法开发的文件转换成可以在浏览器环境中运行的的语法格式。
const {src, dest} = require('gulp')
const babel = require('gulp-babel')
const script = () => {
return src('src/assets/scripts/*js', {base: 'src'})
.pipe(babel({presets: ['@babel/preset-env']}))
.pipe(dest('dist'))
}
module.exports = {
script
}
3、页面模版编译
模版文件也就是HTML
文件,在HTML
文件中为了可以让页面当中重用的一些地方被抽象出来。可以使用模版引擎,比如 swig
(yarn add swig --dev
)。
const {src, dest} = require('gulp')
const swig = require('gulp-swig')
const page = () => {
require src('src/*.html', {base: 'src'})
.pipe(swig())
.pipe(dest('dist'))
}
4、图片和字体文件的转换
对于图片和字体这类静态资源文件,一般的构建都是对文件进行压缩处理。yarn add imagemin --dev
const {src, dest} = require('gulp')
const imagemin = require('gulp-imagemin')
const img = () => {
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'))
}
module.exports = {
img,
font
}
5、其他文件及文件清除
对于像public
目录下不需要构建的数据,直接拷贝就可以了。
const {src, dest} = require('gulp')
const extra = () {
return src('public/**', {base: 'public'})
.pipe(dest('dist'))
}
对于dist
目录,每次我们在执行构建工作前最好将原来构建历史文件进行清除,来保持文件的整洁。这里可以通过使用 del
插件来做 yarn add del --dev
。
const del = require('del')
const clean = () => {
return del(['dist'])
}
6、自动加载插件
随着项目的构建任务越来越复杂,使用到的插件也就越来越多,如果都是通过手动的方式去加载插件的话,require的操作就会非常的多不太利于后期维护。可以通过一个插件来解决这个问题,这个插件是 gulp-load-plugins
yarn add gulp-load-plugins --dev
。
const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()
7、开发服务器
除了对文件的构建以外,还需要一个开发服务器用于去在开发阶段调试应用。可以使用gulp
启动和管理这个服务器,这样的话就可以在后续去配合其他的一些构建任务实现在代码修改过后自动去编译,并且去自动刷新浏览器。这样就会大大提高在开发阶段的效率,因为它会减少很多在开发阶段的操作。这里需要用到一个叫 browser-sync
的模块, 首先要去安装到开发依赖 yarn add browser-sync --dev
。
const browserSync = require('browser-sync')
const bs = browserSync.create()
const serve = () => {
bs.init({
notify: false,
port: 8080,
open: true,
files: 'dist/**',
server: {
baseDir: 'dist',
routes: {
'/node_modules':'node_modules'
}
}
})
}
8、监视变化及构建过程优化
有开发服务器之后,就要考虑如何实现自动编译并子等更新页面。此处需要借助gulp
提供的一个API
叫做watch
,这个API
会自动监视一个路径的通配符,然后根据这些文件的变化决定是否要重新去执行某一个任务。把这个watch
解构出来以后可以在serve
命令开始的时候去监视一些文件。
const {watch} = require('gulp')
const serve = () {
watch('src/assets/styles/*.scss', style)
watch('src/assets/scripts/*.js', script)
watch([
'src/assets/image/**',
'src/assets/fonts/**',
'public/**'
], bs.reload)
watch('src/*.html', page)
bs.init({
files: 'dist/**', // 也可以在任务结尾使用 .pipe(bs.reload()) 的形式来更新页面
sever: {
baseDir: ['dist', 'src', 'public']
}
})
}
9、useRef文件引用处理
截止到目前绝大多数构建任务基本上都已经完成了,但是对于dist
下生成的文件还有一些小问题。比如在HTML
文件中存在对node_moduls
目录下文件的引用,这些文件并没有被拷贝到dist
文件目录下面。如果将这个dist
目录部署到线上的话,就会出现问题。为了解决这个问题可以使用一个插件叫做useRef yarn add gulp useref --dev
,它会自动处理html
当中的构建注释。构建注释的格式是:
<!-- build:css assets/styles/vendor.css -->
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.css">
<!-- endbuild -->
<!-- build:css assets/styles/main.css -->
<link rel="stylesheet" href="assets/styles/main.css">
<!-- endbuild -->
<!-- build:js assets/scripts/vendor.js -->
<script src="node_moduls/jquery/dist/jquery.js"></script>
...
<!-- endbuild -->
<!-- build:js assets/scripts/main.js -->
<script src="assets/scripts/main.js"></script>
<!-- endbuild -->
const useref = () => {
return src('dist/*.html', {base: 'dist'})
.pipe(plugins.useref({ searchPath: ['dist', '.'] }))
.pipe(dest('dist'))
}
10、文件压缩
useref
自动把项目中依赖的文件全部拿过来,这里还需要做个压缩的处理。需要压缩的文件有三种:html
、css
、Js
。yarn add gulp-htmlmin gulp-clean-css gulp-uglify gulp-if --dev
const useref = () => {
return src('dist/*.html', {base: 'dist'})
.pipe(plugins.useref({searchPath: ['dist', '.']}))
.pipe(plugins.if(/\.js$/,plugins.uglify()))
.pipe(plugins.if(/\.css$/,plugins.cleanCss()))
.pipe(plugins.if(/\.html$/,plugins.htmlmin({
collapseWhitespace:true,
minfyCss: true,
minfyJs: true
})))
.pipe(dest('release'))
}
11、重新规划构建过程
由于useref
打破了开发文件放在src
目录下、编译后结果放在dist
目录下这样一个构建的目录结构。在useref
之前所有的生成文件实际上算是一个中间产物,所以在完成最后构建程序前将构建文件放在dist
目录的结构是不合理的。对于中间过程的产生的临时文件,应该发在临时目录中。这样我们在项目中新建一个,temp
目录并检查所有构建任务,是否会产生临时文件:
- clean:在清空的对象中,添加
temp
目录 - style:因为在发布前还要经过压缩处理,所以要在中加处理环节放在
temp
中 - sctipts:同理
- page:同理
- img、fonts、extra: 没有中间环节可以直接放进
dist
目录 - serve:修改
baseDir
将dist
改成temp
- useref:作为中加环节应从
temp
目录读取文件,并写入到dist
目录 - build:根据
compile
、与useref
的依赖关系将两个任务设置成串行
12、工程命令提取
完成构建任务之后,需要将对外的构建命令曝露到外面。并在package.json
文件 scripts
中定义相应命令操作:
{
"scripts": {
"clean": "gulp clean",
"build": "gulp build",
"develop": "gulp develop"
}
}
13、封装工作流
接下来重点考虑一下关于项目当中gulpfile
的复用的问题,如果说涉及到要去开发多个同类型的项目,这个自动化的构建工作流应该是一样的。这个时候就涉及到在多个项目当中重复去使用这些构建任务,这些构建任务绝大多数情况下它们都是相同的。所以说就面临一个需要去复用相同的gulpfile的问题,针对于这个问题可以通过代码段的方式把这个gulpfile
做一个代码段保存起来并在不同的项目中去使用。但是这种方式它也有一个弊端,就是gulpfile
散落在各个项目当中。一旦当这个gulpfile
有一些问题需要去修复或者去升级的时候,就需要对每一个项目做相同的操作,这样也不利于整体的维护。所以说这一块要重点来看,如何去提取一个可复用的自动化构建工作流。解决的方法其实也很简单,就是通过创建一个新的模块去包装一下gulp
。
然后把这个自动化的构建工作流给它包装进去。具体来说就是gulp
只是一个自动华构建流的一个平台,它不负责去帮开发者提供任何的构建任务。自己的构架任务,要通过自己的gulpfile去定义。现在有了gulpfile
也有了gulp
,将二者通过一个模块结合到一起。结合到一起过后在以后同类型的项目当中就使用这个模块,去提供自动化构建的工作流就好了。
具体做法就是先建一个模块,然后把这个模块发布到npm代码仓库上面,最后在项目当中去使用这个模块。
第一步:在git hub 上创建一个空项目 gulp-page 并复制项目地址;
第二步:在本地新建一个空项目并初始化 yarn init
第三步:使用git remote add origin + 远程地址 关联远端仓库
第四步:查看状态 git status 并做初试提交 git add . / git commit -m "feat:initial commit" / git push -u origin master
第五步:将gulpfile复制到lib文件夹中,并将package.json当中的开发依赖复制到新的page.json文件中来。
14、项目地址:github.com/Leo-2020-20…