# fed-e-task-02-01 **Repository Path**: drx2020/fed-e-task-02-01 ## Basic Information - **Project Name**: fed-e-task-02-01 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2020-07-13 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 简答题 ### 一、理解工程化 前端工程化不等同于工具也非简单的工具集合。我认为前端工程化的重点应该关注流程,这个流程包括需求、开发、测试和部署四个方面: 1. 需求驱动了前端工程不断自我完善与发展 2. 开发联结了前端工程的各阶段工作,处于前端工程的核心区域,也直接影响着前端工程的效能 3. 测试确定了前端工程的各项质量指标,直接反映了前端工程的质量情况 4. 部署检验了前端工程的实践成果也决定了前端工程的实际价值 从这个环节的关系来说,工具只是具体的实现手段,规范才是工程化的核心;没有规范,各个环节就无法协同工作更无法有序执行,也就无法发挥各自应有的作用,最后只会导致目标无法达成,前端工程便无从谈起;此外,流程是工程化的具体表现,而工程化的最终目的则是过程优化,从精益思想的角度来说,前端工程化就是要提高前端各个环节的工作效率,降低整个流程的成本,同时又能提高前端工程的质量。在前端工程化的实施过程中,我们还要不断积累实践经验,沉淀出一套切实可行的方法论,指导工程化的发展与实施,否则工程化终会沦为纸上谈兵。 具体来说,(1)日常开发过程中,前端工程师应当通过代码审查等反省方式,共同总结并提炼出一系列工程规范,根据实际的业务场景,这些规范可以涵盖 Web 端,还有 Nodejs 端、移动端、客户端等。这些规范不仅能够统一项目编码风格,提升开发协作效率,提高代码质量,还能够方便新手快速了解、熟悉、适应项目代码与开发流程;(2)构建脚手架,规范项目的代码风格、开发流程、测试流程、部署流程,这样不仅能够最大程度降低前端项目的启动成本,也能保证前端项目的整体一致性,还能提高前端的开发效率与协作效率;(3)根据具体的业务场景,把持续集成和持续交付尽可能贯穿到整个前端的开发周期,避免代码质量随着需求的持续变更而不断下降,同时保证前端项目可随时部署、随时演示、随时交付。 ### 二、理解脚手架 脚手架的原始功能就是创建项目的初始文件,减少项目开发的启动成本。然而,从脚手架的内容来看,脚手架提供了多种可选的、可组合的配置信息,这些配置信息则共同构成了不同的配置方案。 因此,我认为,脚手架的重要功能是封装配置方案。 - 从配置的内容来看,配置方案不仅包括代码风格的配置、开发流程的配置、构建流程的配置、测试流程的配置、部署流程的配置,还可以包括某种技术栈的成套解决方案,如 React + Redux + ImmutableJS + TypeScript 的成套技术方案。所以,脚手架是一系列工程规范的载体 - 从配置的实现来看,这些配置往往搭配了相应的 CLI 程序,比如代码风格的配置搭配了对应的 Prettier 程序、开发流程的配置搭配了对应的 Husky 程序、构建流程的配置搭配了对应的 Webpack 程序,等等。所以,脚手架是一系列工程规范的手段 由于脚手架的出现,工程规范能够以配置的形式适应不同的项目场景,还能以代码的形式固化到项目当中,使得工程规范更易分享、更易管理;与此同时,内置相应的 CLI 程序搭配使用,也降低了工程规范的实施成本。 进一步说,脚手架通过“规范即代码”实现了“规范即服务”:前端工程师能够把一系列规范通过代码进行声明式描述,使用版本管理工具来管理规范,然后结合各种程序实现自动化任务,帮助使用者便捷地实施这些规范,还可暴露可拓展的 API,最终构成一套“服务”提供出去。 ## 编程题 ### 一、实现小型脚手架工具 从 Plopjs 获得灵感,实现了一个小型的 CLI,主要想解决部门一些大型前端项目中实践积累下来的各种小型样板代码,具体实现的代码放在了 GitHub:https://github.com/dorayx/clipz-cli 脚手架程序主要分成四个阶段,实现流程如下: 1. 获取全部可供选择的模板:通过读取配置信息,使用 fs.readDirSync 来遍历出全部的模板目录名即模板名称 2. 询问使用哪个模板:通过 inqurier 模块录入全部可选的模板名称,向使用者询问具体要使用的模板名称 3. 询问模板各项变量的值:读取选中的模板所对应的配置文件,取得模板配置文件所声明的各项变量信息,然后传递给 inqurier 模块从而向使用者询问具体值 4. 渲染模板并输出到目标路径:参照 Yeoman 使用了内存型的仿 FS 文件系统模块 mem-fs 来提高模板的 IO 效率,同样使用 mem-fs-editor 来快速实现模板的复制时渲染功能以及输出到指定目标路径 ![flow](images/flow.png) 具体来说,主要实现细节如下: 1. 从 bin 文件默认触发“询问使用哪个模板” ```ts #!/usr/bin/env node const cli = require('../dist'); cli.chooseGeneratorInteractively(); ``` 2. “询问使用哪个模板”时先进行配置初始化工作,具体在使用方的项目中,从 package.json 或者 clipz.js 读取配置信息 ```ts export function loadCliConfig(): CliConfig { const cwd = process.cwd(); ... try { // from package.json const pkgJsonPath = path.resolve(cwd, 'package.json'); fs.accessSync(path.resolve(cwd, 'package.json'), fs.constants.R_OK); const pkgConfig = JSON.parse(fs.readFileSync(pkgJsonPath).toString())?.clipz ?? {}; ... } catch (e) { } try { // from clipz.js const cfgJsPath = path.resolve(cwd, 'clipz.js'); fs.accessSync(cfgJsPath, fs.constants.R_OK); const jsConfig = require(cfgJsPath) ?? {}; ... } catch (e) { } return config; } ``` 3. 完成初始化配置后,获取可提供选择的模板信息,包括模板的名称以及元信息(包括变量的元信息、模板描述等): ```ts export function extractGeneratorsMeta(generatorsDir: string): [string[] | null, GeneratorSchema[] | null] { try { ... const generators = fs.readdirSync(dirPath) // 模板必须是个目录 .filter(p => fs.statSync(path.resolve(dirPath, p)).isDirectory()) // 模板目录必需包含一个 __clipz__.js 文件 .filter(p => { try { fs.accessSync(path.resolve(dirPath, p, '__clipz__.js'), fs.constants.R_OK); return true; } catch (e) { return false; } }); // 取模板目录名称作为模板名称 const names = generators.map(p => p.split(path.delimiter).pop()!); // 读取模板目录 __clipz__.js 文件来获取模板元信息 const schemas = generators.map(p => require(path.resolve(dirPath, p, '__clipz__.js'))); ... } catch (e) { ... } } ``` 其中 `__clipz__.js` 结构定义如下: ```ts // [可选] 模板各变量的元信息 module.exports.variables = { [varKey]: { type: string, message: string, } }; // [可选] 模板描述 module.exports.description = string; ``` 4. 完成“询问使用哪个模板”以及“询问各项变量的值”之后,开始渲染模板,依次经过以下步骤: 1. 递归遍历出全部的模板文件 2. 计算出输出的目标路径 3. 调用 mem-fs-editor 提供的 copyTmpl 方法完成模板在内存中的渲染 4. 调用 mem-fs-editor 提供的 commit 方法把内存的结果输出到目标路径 ```ts export function getTmplPaths(tmplRoot: string, tmplContext: TmplContext | null): string[] { return fs.readdirSync(tmplRoot) // 过滤掉配置文件 .filter(filename => !/__clipz__\.js/.test(filename)) .reduce((files, file) => { const p = path.resolve(tmplRoot, file); // 递归式遍历出所有模板文件 const appended = fs.statSync(p).isDirectory() ? getTmplPaths(p, tmplContext) : [p]; return [ ...files, ...appended, ]; }, []); } export function copyTmpl(generatorName: string, tmplContext: TmplContext | null, config: CliConfig) { return new Promise((resolve, reject) => { const store = memFs.create(); const editor = memFsEditor.create(store); // 遍历全部模板文件 getTmplPaths(path.resolve(config.generatorsDir, generatorName), tmplContext) .forEach(file => { // 通过 path.relative 和 path.join 可计算出模板文件的目标路径 const fromPath = path.relative(config.generatorsDir, file); const distPath = path.join(config.toDir, fromPath); // 复制并渲染出模板 editor.copyTpl(file, distPath, tmplContext); }); // 把全部的模板文件渲染结果输出到目标路径 editor.commit((err) => err ? reject(err) : resolve()); }); } ``` ### 二、Gulp 构建项目 1. 自动加载全部插件 ```js const loadPlugins = require('gulp-load-plugins'); const plugins = loadPlugins(); ``` 2. 定义打包过程的各项任务 ```js // 清除打包目录 const clean = () => { return del(['dist', 'temp']); }; // 编译脚本 const script = () => { return src('src/assets/scripts/*.js', {base: 'src'}) .pipe(plugins.babel({presets: ['@babel/preset-env']})) .pipe(dest('temp')); }; // 编译样式 const style = () => { return src('src/assets/styles/*.scss', {base: 'src'}) .pipe(plugins.sass({outputStyle: 'expanded'})) .pipe(dest('temp')); }; // 编译 HTML const page = () => { return src('src/*.html', {base: 'src'}) .pipe(plugins.swig({defaults: {cache: false}})) .pipe(dest('temp')); }; // 复制图片 const image = () => { return src('src/assets/images/**', {base: 'src'}) .pipe(dest('dist')); }; // 复制字体 const font = () => { return src('src/assets/fonts/**', {base: 'src'}) .pipe(dest('dist')); }; // 复制额外的静态资源文件 const extra = () => { return src('public/**', {base: 'public'}) .pipe(dest('dist')); }; // 合并前端静态资源并修改对应的 HTML URL 引用 const useref = () => { return src('temp/*.html', {base: 'temp'}) .pipe(plugins.useref({searchPath: ['temp', '.']})) // html js css .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')); }; ``` 3. 组织以上任务,分出编译阶段和打包阶段 ```js // 编译阶段 const compile = parallel(style, script, page); // 打包阶段 const build = series( clean, parallel( series(compile, useref), image, font, extra, ), ); ``` ### 三、Grunt 构建项目 1. 自动加载全部插件 ```js const loadGruntTasks = require('load-grunt-tasks') module.exports = grunt => { loadGruntTasks(grunt); }; ``` 2. 定义各项任务 ```js grunt.initConfig({ // 复制各种静态资源 copy: { fonts: { expand: true, cwd: 'src/assets/fonts/', src: '**', dest: 'dist/assets/fonts/', }, images: { expand: true, cwd: 'src/assets/images/', src: '**', dest: 'dist/assets/images/', }, extra: { expand: true, cwd: 'public/', src: '**', dest: 'dist/', } }, // 提取、合并静态资源文件并修改 HTML URL 引用 useref: { html: 'dist/*.html', temp: 'dist/' }, // 渲染 HTML 模板 swig: { production: { dest: 'dist/', src: ['src/*.html'], generateSitemap: false, generateRobotstxt: false, } }, // 编译 SASS 文件 sass: { options: { sourceMap: true, implementation: sass }, main: { files: { 'dist/assets/styles/main.css': 'src/assets/styles/main.scss' } } }, // 编译脚本 babel: { options: { sourceMap: true, presets: ['@babel/preset-env'] }, main: { files: { 'dist/assets/scripts/main.js': 'src/assets/scripts/main.js' } } }, // 清除临时文件 clean: { temp: 'temp/**', dist: 'dist/**', } }) ``` 3. 注册任务 ```js grunt.registerTask('build', [ 'clean', 'swig', 'sass', 'babel', 'useref', 'concat', 'uglify', 'cssmin', 'copy' ]) ```